From c330e041253748978b2094e0c982e32314ec9671 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Feb 2024 22:44:33 -0500 Subject: [PATCH 001/974] Save point --- inc/shared/shared.h | 33 +- meson.build | 21 +- src/action/a_cmds.c | 180 ++- src/action/a_ctf.c | 23 +- src/action/a_dom.c | 7 + src/action/a_esp.c | 2116 +++++++++++++++++++++++++++ src/action/a_esp.h | 129 ++ src/action/a_game.c | 168 ++- src/action/a_game.h | 6 +- src/action/a_items.c | 7 +- src/action/a_match.c | 145 +- src/action/a_match.h | 1 + src/action/a_team.c | 740 +++++++++- src/action/a_team.h | 11 + src/action/a_vote.c | 8 +- src/action/a_xgame.c | 94 +- src/action/a_xmenu.c | 2 +- src/action/acesrc/acebot.h | 11 +- src/action/acesrc/acebot.txt | 196 +-- src/action/acesrc/acebot_ai.c | 7 + src/action/acesrc/acebot_cmds.c | 12 +- src/action/acesrc/acebot_items.c | 6 + src/action/acesrc/acebot_movement.c | 138 +- src/action/acesrc/acebot_nodes.c | 6 +- src/action/acesrc/acebot_spawn.c | 248 +++- src/action/acesrc/bot_ai.c | 2 +- src/action/acesrc/botchat.c | 77 +- src/action/cgf_sfx_glass.c | 4 +- src/action/g_chase.c | 39 + src/action/g_cmds.c | 58 +- src/action/g_combat.c | 39 +- src/action/g_ext.c | 352 +++-- src/action/g_func.c | 6 +- src/action/g_grapple.c | 2 +- src/action/g_items.c | 16 +- src/action/g_local.h | 531 +++++-- src/action/g_main.c | 166 ++- src/action/g_misc.c | 35 +- src/action/g_save.c | 79 +- src/action/g_spawn.c | 232 ++- src/action/g_utils.c | 47 +- src/action/g_weapon.c | 37 +- src/action/p_antilag.c | 6 +- src/action/p_client.c | 720 ++++++--- src/action/p_hud.c | 517 ++++++- src/action/p_view.c | 101 +- src/action/p_weapon.c | 108 +- src/action/tng_balancer.c | 6 +- src/action/tng_irc.c | 14 +- src/action/tng_jump.c | 395 ++++- src/action/tng_jump.h | 8 + src/action/tng_stats.c | 373 ++++- src/shared/shared.c | 8 + 53 files changed, 7117 insertions(+), 1176 deletions(-) create mode 100644 src/action/a_esp.c create mode 100644 src/action/a_esp.h diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 224c59d2c..2dff10a58 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -451,10 +451,20 @@ static inline int Q_gcd(int a, int b) return a; } +void ProjectPointOnPlane (vec3_t dst, const vec3_t p, const vec3_t normal); +void PerpendicularVector (vec3_t dst, const vec3_t src); +void RotatePointAroundVector (vec3_t dst, const vec3_t dir, + const vec3_t point, float degrees); + +void VectorRotate( vec3_t in, vec3_t angles, vec3_t out ); // a_doorkick.c +void VectorRotate2( vec3_t v, float degrees ); + void Q_srand(uint32_t seed); uint32_t Q_rand(void); uint32_t Q_rand_uniform(uint32_t n); +#define bound(a,b,c) ((a) >= (c) ? (a) : (b) < (a) ? (a) : (b) > (c) ? (c) : (b)) + #define clamp(a,b,c) ((a)<(b)?(a)=(b):(a)>(c)?(a)=(c):(a)) #define cclamp(a,b,c) ((b)>(c)?clamp(a,c,b):clamp(a,b,c)) @@ -1288,12 +1298,13 @@ enum { STAT_GRENADES, STAT_TEAM3_PIC, STAT_TEAM3_SCORE, - STAT_TEAM1_HEADER, - STAT_TEAM2_HEADER, MAX_STATS = 32 }; +#define STAT_TEAM1_HEADER 30 +#define STAT_TEAM2_HEADER 31 + // STAT_LAYOUTS flags #define LAYOUTS_LAYOUT BIT(0) #define LAYOUTS_INVENTORY BIT(1) @@ -1320,8 +1331,11 @@ enum { #define DF_QUAD_DROP BIT(14) #define DF_FIXED_FOV BIT(15) +// ACTION +#define DF_WEAPON_RESPAWN BIT(16) + // RAFAEL -#define DF_QUADFIRE_DROP BIT(16) +//#define DF_QUADFIRE_DROP BIT(16) //ROGUE #define DF_NO_MINES BIT(17) @@ -1338,6 +1352,19 @@ enum { #define UF_MUTE_MISC BIT(5) #define UF_PLAYERFOV BIT(6) +// PaTMaN - Flags for ToGgle +#define TG_LASER BIT(0) +#define TG_SLIPPERS BIT(1) +#define TG_SILENCER BIT(2) +#define TG_VEST BIT(3) +#define TG_KICKABLE BIT(4) +#define TG_HELMET BIT(5) + +#define TG_HUD_RANGE BIT(7) + +#define TG_IR BIT(13) +// Why the gaps, I do not know? + /* ========================================================== diff --git a/meson.build b/meson.build index c65e4cb33..098409044 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,7 @@ action_src = [ 'src/action/a_ctf.c', 'src/action/a_dom.c', 'src/action/a_doorkick.c', + 'src/action/a_esp.c', 'src/action/a_game.c', 'src/action/a_items.c', 'src/action/a_match.c', @@ -544,16 +545,16 @@ executable('q2proded', common_src, server_src, ## Disabling building gamelib for now, no need -# shared_library('game' + cpu, action_src, -# name_prefix: '', -# dependencies: game_deps, -# include_directories: 'inc', -# gnu_symbol_visibility: 'hidden', -# link_args: dll_link_args, -# install: system_wide, -# install_dir: libdir / get_option('base-game'), -# override_options: get_option('game-build-options'), -# ) +shared_library('game' + cpu, action_src, + name_prefix: '', + dependencies: game_deps, + include_directories: 'inc', + gnu_symbol_visibility: 'hidden', + link_args: dll_link_args, + install: system_wide, + install_dir: libdir / get_option('base-game'), + override_options: get_option('game-build-options'), +) config.set('REVISION', meson.project_version().substring(1).split('~')[0].to_int()) config.set_quoted('VERSION', meson.project_version()) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index c60c3359a..563ba5b10 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -133,7 +133,8 @@ #include "g_local.h" #include -#ifdef _WIN32 +#ifdef USE_AQTION +#ifdef WIN32 #if _MSC_VER >= 1920 && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include @@ -141,6 +142,7 @@ #else #include #endif +#endif /*---------------------------------------- * SP_LaserSight @@ -160,19 +162,36 @@ void SP_LaserSight(edict_t * self, gitem_t * item) return; } //zucc code to make it be used with the right weapons - - switch (self->client->weapon->typeNum) { - case MK23_NUM: - case MP5_NUM: - case M4_NUM: - break; - default: - // laser is on but we want it off - if (lasersight) { - G_FreeEdict(lasersight); - self->client->lasersight = NULL; + // edited to allow laser to be used with dual pistols if enabled -ds + if (!gun_dualmk23_enhance->value) { + switch (self->client->weapon->typeNum) { + case MK23_NUM: + case MP5_NUM: + case M4_NUM: + break; + default: + // laser is on but we want it off + if (lasersight) { + G_FreeEdict(lasersight); + self->client->lasersight = NULL; + } + return; + } + } else { // include dualmk23 + switch (self->client->weapon->typeNum) { + case DUAL_NUM: + case MK23_NUM: + case MP5_NUM: + case M4_NUM: + break; + default: + // laser is on but we want it off + if (lasersight) { + G_FreeEdict(lasersight); + self->client->lasersight = NULL; + } + return; } - return; } if (lasersight) { //Lasersight is already on @@ -422,6 +441,34 @@ void Cmd_Reload_f(edict_t * ent) //+BD END CODE BLOCK +// zoom_comp +int calc_zoom_comp(edict_t * ent) +{ +// Determine client ping to indicate how quickly we zoom from 0x to 1x + int default_idle_frames = 6; + int pingfloor = 80; + int clping = ent->client->ping; + + int idle_weapon_frames = 0; + + // No compensation if player ping is less than pingfloor + // For every tier of pingfloor you reduce your frames by 1 + if (clping < pingfloor){ + idle_weapon_frames = default_idle_frames; + } else if ((clping > pingfloor) && (clping <= (pingfloor * 2))){ + idle_weapon_frames = 5; + } else if ((clping > (pingfloor * 2)) && (clping <= (pingfloor * 3))){ + idle_weapon_frames = 4; + } else if ((clping > (pingfloor * 3))){ + idle_weapon_frames = 3; + } else { + // Somehow clping wasn't calculated correctly, default to be safe + idle_weapon_frames = default_idle_frames; + } + + return idle_weapon_frames; +} + //tempfile BEGIN /* Function _SetSniper @@ -474,7 +521,11 @@ void _SetSniper(edict_t * ent, int zoom) if (oldmode == SNIPER_1X && ent->client->weaponstate != WEAPON_RELOADING) { //do idleness stuff when switching from 1x, see function below ent->client->weaponstate = WEAPON_BUSY; - ent->client->idle_weapon = 6; + if(zoom_comp->value) { + ent->client->idle_weapon = calc_zoom_comp(ent); + } else { + ent->client->idle_weapon = 6; + } ent->client->ps.gunframe = 22; } } @@ -761,6 +812,13 @@ void Cmd_Bandage_f(edict_t *ent) qboolean can_use_medkit = (ent->client->medkit > 0) && (ent->health < ent->max_health); + // TODO: This breaks the ability for players to jump out of the water, so I am not checking for + // this at the moment + // if (ent->client->bleeding == 0 && esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM) && ! can_use_medkit){ + // gi.cprintf(ent, PRINT_HIGH, "Not bleeding: No need to bandage\n"); + // return; + // } + if (ent->client->bleeding == 0 && ent->client->leg_damage == 0 && ! can_use_medkit) { gi.cprintf(ent, PRINT_HIGH, "No need to bandage\n"); return; @@ -819,7 +877,10 @@ void Bandage(edict_t * ent) ent->client->bandaging = 0; ent->client->attacker = NULL; ent->client->bandage_stopped = 1; - ent->client->idle_weapon = BANDAGE_TIME; + if (esp->value && esp_leaderenhance->value && IS_LEADER(ent)) + ent->client->idle_weapon = ENHANCED_BANDAGE_TIME; + else + ent->client->idle_weapon = BANDAGE_TIME; } void Cmd_ID_f(edict_t * ent) @@ -1035,6 +1096,9 @@ void Cmd_Choose_f(edict_t * ent) } ent->client->pers.chosenItem = GET_ITEM(itemNum); break; + case C_KIT_NUM: + case S_KIT_NUM: + case A_KIT_NUM: default: gi.cprintf(ent, PRINT_HIGH, "Invalid weapon or item choice.\n"); return; @@ -1046,7 +1110,25 @@ void Cmd_Choose_f(edict_t * ent) item = ent->client->pers.chosenItem; itmText = (item && item->pickup_name) ? item->pickup_name : "NONE"; - gi.cprintf(ent, PRINT_HIGH, "Weapon selected: %s\nItem selected: %s\n", wpnText, itmText ); + if (item_kit_mode->value) { + if (itemNum == C_KIT_NUM){ + itmText = "Commando Kit (Bandolier + Kevlar Helmet)"; + } else if (itemNum == A_KIT_NUM){ + itmText = "Assassin Kit (Laser Sight + Silencer)"; + } else if (itemNum == S_KIT_NUM){ + if (esp_enhancedslippers->value){ + itmText = "Stealth Kit (Enhanced Stealth Slippers + Silencer)"; + } else { + itmText = "Stealth Kit (Stealth Slippers + Silencer)"; + } + } else { + // How did you pick a kit not on the list? + itmText = "NONE"; + } + gi.cprintf(ent, PRINT_HIGH, "Weapon selected: %s\nItem kit selected: %s\n", wpnText, itmText ); + } else { + gi.cprintf(ent, PRINT_HIGH, "Weapon selected: %s\nItem selected: %s\n", wpnText, itmText ); + } } // AQ:TNG - JBravo adding tkok @@ -1201,6 +1283,10 @@ void Cmd_Ghost_f(edict_t * ent) ent->client->resp.deaths = ghost->deaths; ent->client->resp.damage_dealt = ghost->damage_dealt; ent->client->resp.ctf_caps = ghost->ctf_caps; + ent->client->resp.ctf_capstreak = ghost->ctf_capstreak; + ent->client->resp.team_kills = ghost->team_kills; + ent->client->resp.streakKillsHighest = ghost->streakKillsHighest; + ent->client->resp.streakHSHighest = ghost->streakHSHighest; if (teamplay->value && ghost->team && ghost->team != ent->client->resp.team) JoinTeam( ent, ghost->team, 1 ); @@ -1226,9 +1312,11 @@ void Cmd_Ghost_f(edict_t * ent) num_ghost_players--; } -void generate_uuid() + +#ifdef USE_AQTION +void generate_uuid(void) { -#ifdef _WIN32 +#ifdef WIN32 #if _MSC_VER >= 1920 && !__INTEL_COMPILER UUID uuid; unsigned char* uuidStr; @@ -1262,6 +1350,7 @@ void generate_uuid() //gi.dprintf("%s UUID: %s\n", __func__, game.matchid); #endif } +#endif #ifndef NO_BOTS void Cmd_Placenode_f (edict_t *ent) @@ -1276,3 +1365,58 @@ void Cmd_Placenode_f (edict_t *ent) ACEND_AddNode(ent,NODE_MOVE); } #endif + +void Cmd_Volunteer_f(edict_t * ent) +{ + int teamNum; + edict_t *oldLeader; + + // Ignore if not Espionage mode + if (!esp->value) { + gi.cprintf(ent, PRINT_HIGH, "This command needs Espionage to be enabled\n"); + return; + } + + // Cannot use this command directly in Matchmode + if (matchmode->value){ + gi.cprintf(ent, PRINT_HIGH, "Only the Captain can become the Leader\n"); + return; + } + + // Ignore entity if not on a team + teamNum = ent->client->resp.team; + if (teamNum == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "You need to be on a team for that...\n"); + return; + } + + // Ignore entity if they are a sub + if (ent->client->resp.subteam == teamNum) { + gi.cprintf(ent, PRINT_HIGH, "Subs cannot be leaders...\n"); + return; + } + + // If the current leader is issuing this command again, remove them as leader + oldLeader = teams[teamNum].leader; + if (oldLeader == ent) { + if (team_round_going || lights_camera_action > 0) { + gi.cprintf(ent, PRINT_HIGH, "You cannot resign as leader while a round is in progress\n"); + return; + } + EspSetLeader( teamNum, NULL ); + // This is the last time we know this entity was the leader, so do some cleanup first + oldLeader->client->resp.is_volunteer = false; + oldLeader->client->resp.esp_leadertime = level.realFramenum; + return; + } + + // If the team already has a leader, send this message to the ent volunteering + if (oldLeader) { + gi.cprintf( ent, PRINT_HIGH, "Your team already has a leader (%s)\nYou are now volunteering for duty should he fall\n", + teams[teamNum].leader->client->pers.netname ); + ent->client->resp.is_volunteer = true; + return; + } + + EspSetLeader( teamNum, ent ); +} \ No newline at end of file diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index fff0eec5e..18a54a2f8 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -85,7 +85,7 @@ qboolean CTFLoadConfig(char *mapname) memset(&ctfgame, 0, sizeof(ctfgame)); - gi.dprintf("Trying to load CTF configuration file for %s\n", mapname); + gi.dprintf("Trying to load CTF configuration file\n", mapname); /* zero is perfectly acceptable respawn time, but we want to know if it came from the config or not */ ctfgame.spawn_red = -1; @@ -230,6 +230,8 @@ void CTFSetTeamSpawns(int team, char *str) if(team == TEAM2) team_spawn_name = "info_player_team2"; + + /* find and remove all team spawns for this team */ while ((spawn = G_Find(spawn, FOFS(classname), team_spawn_name)) != NULL) { G_FreeEdict (spawn); @@ -329,7 +331,7 @@ void ResetPlayers() } } -void CTFSwapTeams(void) +void CTFSwapTeams() { edict_t *ent; int i; @@ -931,10 +933,10 @@ void CTFCalcScores(void) ctfgame.total2 += game.clients[i].resp.score; } - #if USE_AQTION + #ifdef USE_AQTION // Needed to add this here because this is called separately from TallyEndOfLevelTeamScores (teamplay) if (stat_logs->value) { - LogMatch(); // Generates end of match logs + LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match stats } #endif @@ -1270,6 +1272,7 @@ void CTFCapReward(edict_t * ent) band = ent->client->resp.ctf_capstreak; client = ent->client; + // give initial knife if none if (WPF_ALLOWED(KNIFE_NUM) && ent->client->inventory[ITEM_INDEX(GET_ITEM(KNIFE_NUM))] == 0) ent->client->inventory[ITEM_INDEX(GET_ITEM(KNIFE_NUM))] += 1; @@ -1386,7 +1389,7 @@ void CTFCapReward(edict_t * ent) ReadySpecialWeapon(ent); } - // give health times cap streak + // give health times cap streak and awards ent->health = ent->max_health * (ent->client->resp.ctf_capstreak > 4 ? 4 : ent->client->resp.ctf_capstreak); if(ent->client->resp.ctf_capstreak == 2) @@ -1398,5 +1401,13 @@ void CTFCapReward(edict_t * ent) else if(ent->client->resp.ctf_capstreak > 4) gi.centerprintf(ent, "CAPTURED YET AGAIN!\n\nYou have been rewarded QUAD health and %d times your ammo!\n\nNow go get some more!", ent->client->resp.ctf_capstreak); - else gi.centerprintf(ent, "CAPTURED!\n\nYou have been rewarded.\n\nNow go get some more!"); + if (use_rewards->value) { + if(ent->client->resp.ctf_capstreak == 5) + Announce_Reward(ent, DOMINATING); + if(ent->client->resp.ctf_capstreak == 10) + Announce_Reward(ent, UNSTOPPABLE); + } + else gi.cprintf(ent, PRINT_MEDIUM, "CAPTURED!\n\nYou have been rewarded.\n\nNow go get some more!"); + + LogCapture(ent); } diff --git a/src/action/a_dom.c b/src/action/a_dom.c index 8c2459f7f..bb9c5e1bf 100644 --- a/src/action/a_dom.c +++ b/src/action/a_dom.c @@ -156,6 +156,13 @@ void DomFlagThink( edict_t *flag ) location, teams[ flag->owner->client->resp.team ].name ); + // Stats! + flag->owner->client->resp.dom_caps++; + flag->owner->client->resp.dom_capstreak++; + if (flag->owner->client->resp.dom_capstreak > flag->owner->client->resp.dom_capstreakbest) + flag->owner->client->resp.dom_capstreakbest = flag->owner->client->resp.dom_capstreak; + LogCapture(flag->owner); + if( (dom_team_flags[ flag->owner->client->resp.team ] == dom_flag_count) && (dom_flag_count > 1) ) gi.bprintf( PRINT_HIGH, "%s TEAM IS DOMINATING!\n", teams[ flag->owner->client->resp.team ].name ); diff --git a/src/action/a_esp.c b/src/action/a_esp.c new file mode 100644 index 000000000..6fbfbc9a7 --- /dev/null +++ b/src/action/a_esp.c @@ -0,0 +1,2116 @@ +// Espionage Mode by darksaint +// File format inspired by a_dom.c by Raptor007 and a_ctf.c from TNG team +// Re-worked from scratch from the original AQDT team, Black Monk, hal9000 and crew + +#include "g_local.h" + +espsettings_t espsettings; + +// edict_t *potential_spawns[MAX_SPAWNS]; +// int num_potential_spawns; + +//cvar_t *esp_respawn = NULL; +//int esp_potential_spawns; +int esp_last_chosen_spawn = 0; +int esp_spawnpoint_index[TEAM_TOP] = {-1}; + + +unsigned int esp_team_effect[] = { + EF_BLASTER | EF_ROTATE | EF_TELEPORTER, + EF_FLAG1, + EF_GREEN_LIGHT | EF_COLOR_SHELL +}; + +unsigned int esp_team_fx[] = { + RF_GLOW, + RF_FULLBRIGHT, + RF_FULLBRIGHT, + RF_SHELL_GREEN +}; + +int esp_flag_count = 0; +int esp_team_flags[ TEAM_TOP ] = {0}; +int esp_winner = NOTEAM; +int esp_flag = 0; +int esp_leader_pics[ TEAM_TOP ] = {0}; +int esp_last_score = 0; + + +/* +EspModeCheck + +Returns which mode we are running +*/ +int EspModeCheck(void) +{ + if (!esp->value) + return -1; // Espionage is disabled + if (atl->value) + return ESPMODE_ATL; // ATL mode + else if (etv->value) + return ESPMODE_ETV; // ETV mode + + return -1; // Espionage is disabled +} + +/* +Toggles between the two game modes +*/ +void EspForceEspionage(int espmode) +{ + gi.cvar_forceset("esp", "1"); + if (espmode == 0 || esp_atl->value) { + gi.cvar_forceset("atl", "1"); + gi.cvar_forceset("etv", "0"); + } else if (espmode == 1) { + gi.cvar_forceset("etv", "1"); + gi.cvar_forceset("atl", "0"); + } + + //gi.dprintf("Espionage mode set to %s\n", (espmode == 0) ? "ATL" : "ETV"); +} + +int EspCapturePointOwner( edict_t *flag ) +{ + if( flag->s.effects == esp_team_effect[ TEAM1 ] ) + return TEAM1; + return NOTEAM; +} + + +char *timedMessageStrings[NUM_MESSAGES] = { + "LAST ROUND BEFORE HALFTIME..." + // Add more messages as needed +}; +/* +Timed message handler for Espionage, FMI look for the +TimedMessageIndex enum and timedMessageStrings array in a_esp.h + +Example usage: addTimedMessage(TEAM1, NULL, 10, ESP_HALFTIME_WARNING) +would send message ESP_HALFTIME_WARNING to team 1, 10 seconds into a new round +TEAM0 would send to all players +Team is ignored when an entity is supplied as the second argument +*/ +void EspTimedMessageHandler(int teamNum, edict_t *ent, int seconds, int timedMsgIndex) +{ + // Take care to specify an entity if referencing a player, else this can crash the server + // Validate entity is a player + if (teamNum > teamCount || teamNum < 0) + return; + if (ent && !ent->client){ + gi.dprintf("%s: Invalid entity passed\n", __FUNCTION__); + return; + } + // Convert frames to seconds + //seconds = seconds * 10; + + addTimedMessage(teamNum, ent, seconds, timedMessageStrings[timedMsgIndex]); +} + +/* +Supply two edicts, return distance between the two +*/ +int _EspDistanceFromEdict(edict_t *ent, edict_t *target) +{ + vec3_t dist; + VectorSubtract(ent->s.origin, target->s.origin, dist); + return VectorLength(dist); +} + +/* +Espionage Defender Leader Bonus +*/ +void _EspBonusDefendLeader(edict_t *targ, edict_t *attacker) +{ + edict_t *leader = NULL; + + // You can't defend a leader on a team that doesn't have one + if (attacker->client->resp.team == TEAM2 && etv->value) + return; + + if (IS_LEADER(targ)) + leader = targ; + /* + Leader protection bonus + */ + if (leader && leader != attacker && attacker->client->resp.team == leader->client->resp.team) { + int attacker_leader_dist = _EspDistanceFromEdict(attacker, leader); + int defender_leader_dist = _EspDistanceFromEdict(targ, leader); + + /* If attacker or defender are within ESP_ATTACKER_PROTECT_RADIUS units of the leader + or the leader is visible to either player + */ + if (attacker_leader_dist < ESP_ATTACKER_PROTECT_RADIUS || + defender_leader_dist < ESP_ATTACKER_PROTECT_RADIUS || + visible(leader, targ, MASK_SOLID) || visible(leader, attacker, MASK_SOLID)) { + attacker->client->resp.score += ESP_LEADER_DANGER_PROTECT_BONUS; + attacker->client->resp.esp_leaderprotectcount++; + gi.bprintf(PRINT_MEDIUM, "%s gets %d bonus points for defending %s in the field!\n", + attacker->client->pers.netname, ESP_LEADER_DANGER_PROTECT_BONUS, teams[attacker->client->resp.team].leader_name); + } + } + // Set framenum to prevent multiple bonuses too quickly + attacker->client->resp.esp_lastprotectcap = level.realFramenum; + return; +} + +/* +Espionage Harrass Enemy Leader Bonus +*/ +void _EspBonusHarassLeader(edict_t *targ, edict_t *attacker) +{ + edict_t *leader = NULL; + + // You can't attack a leader on a team that doesn't have one + if (attacker->client->resp.team == TEAM1 && etv->value) + return; + + if (IS_LEADER(targ)) + leader = targ; + /* + Leader attack bonus + */ + if (leader && leader != attacker && attacker->client->resp.team != leader->client->resp.team) { + int attacker_leader_dist = _EspDistanceFromEdict(attacker, leader); + + /* If attacker or defender are within ESP_ATTACKER_PROTECT_RADIUS units of the leader + or the leader is visible to either player + */ + if (attacker_leader_dist < ESP_ATTACKER_HARASS_RADIUS || + visible(leader, targ, MASK_SOLID) || visible(leader, attacker, MASK_SOLID)) { + attacker->client->resp.score += ESP_LEADER_HARASS_BONUS; + gi.bprintf(PRINT_MEDIUM, "%s gets %d bonus points for harassing %s in the field!\n", + attacker->client->pers.netname, ESP_LEADER_HARASS_BONUS, teams[targ->client->resp.team].leader_name); + } + } + // Set framenum to prevent multiple bonuses too quickly + attacker->client->resp.esp_lasthurtleader = level.framenum; + return; +} + +/* +Espionage Leader Frag Bonus +*/ +void _EspBonusFragLeader(edict_t *targ, edict_t *attacker) +{ + int i, enemyteam; + edict_t *ent; + + if (IS_LEADER(targ)){ + attacker->client->resp.esp_lasthurtleader = level.framenum; + attacker->client->resp.score += ESP_LEADER_FRAG_BONUS; + gi.bprintf(PRINT_MEDIUM, + "%s gets %d bonus points for eliminating the enemy leader!\n", + attacker->client->pers.netname, ESP_LEADER_FRAG_BONUS); + + // Stats + attacker->client->resp.esp_leaderfragcount++; + + // NULL check + if (espsettings.lastkilledleader) { + /* If the killer of the leader was not the same as the last killer of the leader + then reset the lastkilledleader and set the new edict + */ + if (espsettings.lastkilledleader != attacker) { + // Set high killstreak if it's higher than the current best + if (espsettings.lastkilledleader->client->resp.esp_leaderkillstreak > espsettings.lastkilledleader->client->resp.esp_leaderkillstreakbest) { + espsettings.lastkilledleader->client->resp.esp_leaderkillstreakbest = espsettings.lastkilledleader->client->resp.esp_leaderkillstreak; + } + // Reset leader killstreak for that player + espsettings.lastkilledleader->client->resp.esp_leaderkillstreak = 0; + // Set the new player as the killer of the leader and award a streak start + espsettings.lastkilledleader = attacker; + attacker->client->resp.esp_leaderkillstreak++; + } else { // Same killer of the leader, increase killstreak + attacker->client->resp.esp_leaderkillstreak++; + } + } else { + espsettings.lastkilledleader = attacker; + } + + enemyteam = (targ->client->resp.team != attacker->client->resp.team); + for (i = 1; i <= game.maxclients; i++) { + ent = g_edicts + i; + if (ent->inuse && ent->client->resp.team == enemyteam) + ent->client->resp.esp_lasthurtleader = 0; + } + + // Set framenum to prevent multiple bonuses too quickly + attacker->client->resp.esp_lasthurtleader = level.realFramenum; + return; + } +} + +/* +Espionage Capture Bonus +*/ +void _EspBonusCapture(edict_t *attacker, edict_t *flag) +{ + edict_t *ent; + + gi.bprintf( PRINT_HIGH, "%s has reached the %s for %s! +%d points!\n", + flag->owner->client->pers.netname, + espsettings.target_name, + teams[ flag->owner->client->resp.team ].name, + ESP_LEADER_CAPTURE_BONUS ); + + // Stats + flag->owner->client->resp.esp_caps++; + flag->owner->client->resp.esp_capstreak++; + if (flag->owner->client->resp.esp_capstreak > flag->owner->client->resp.esp_capstreakbest) + flag->owner->client->resp.esp_capstreakbest = flag->owner->client->resp.esp_capstreak; + LogCapture(flag->owner); + + // Bonus points awarded + flag->owner->client->resp.score += ESP_LEADER_CAPTURE_BONUS; + + // Check if teammates are nearby, they deserve a bonus too + int i; + for (i = 0; i < game.maxclients; i++){ + ent = &g_edicts[1 + i]; + if( !(ent->inuse && ent->client && ent->client->resp.team) ) + continue; + else if( ent == flag->owner ) + continue; + else if( ent->client->resp.team != flag->owner->client->resp.team ) + continue; + else if( _EspDistanceFromEdict(ent, flag) > ESP_TARGET_PROTECT_RADIUS ) + continue; + else { + // Bonus point awarded + ent->client->resp.score += ESP_LEADER_ESCORT_BONUS; + } + } +} + +/* +Espionage Defender Capture Point Bonus +*/ +void _EspBonusDefendCapture(edict_t *targ, edict_t *attacker) +{ + // Only ETV mode, and only for those on TEAM2 + if (!etv->value) + return; + if (attacker->client->resp.team != TEAM2) + return; + + edict_t *cap = espsettings.capturepoint; + int attacker_cap_dist = _EspDistanceFromEdict(attacker, cap); + int defender_cap_dust = _EspDistanceFromEdict(targ, cap); + + // If the attacker or defender are within 400 units of the capture point, + // or the capture point is visible to either player + if (attacker_cap_dist < ESP_ATTACKER_PROTECT_RADIUS || + defender_cap_dust < ESP_ATTACKER_PROTECT_RADIUS || + visible(cap, targ, MASK_SOLID) || + visible(cap, attacker, MASK_SOLID)) { + // we defended the base flag + attacker->client->resp.score += ESP_FLAG_DEFENSE_BONUS; + if (cap->solid == SOLID_NOT) { + gi.bprintf(PRINT_MEDIUM, "%s defends the %s.\n", + attacker->client->pers.netname, espsettings.target_name); + IRC_printf(IRC_T_GAME, "%n defends the %n.\n", + attacker->client->pers.netname, + espsettings.target_name); + } + } + + // Stats + attacker->client->resp.esp_capdefendercount++; + + // Set framenum to prevent multiple bonuses too quickly + attacker->client->resp.esp_lastprotectcap = level.realFramenum; + return; +} + +void EspScoreBonuses(edict_t * targ, edict_t * attacker) +{ + /* + Multiple bonus points, for: + - Fragging the enemy leader + - Defending your leader + - Defending the capture point as TEAM2 + */ + + // You don't get bonus points after the round is over + if (!team_round_going) + return; + + // no bonus for fragging yourself + if (!targ->client || !attacker->client || targ == attacker) + return; + + int enemyteam; + enemyteam = (targ->client->resp.team != attacker->client->resp.team); + if (!enemyteam) + return; // whoever died isn't on a team + + // Is the attacker eligible for these bonuses? + int lpc = attacker->client->resp.esp_lastprotectcap; + int lpl = attacker->client->resp.esp_lastprotectleader; + int lhl = attacker->client->resp.esp_lasthurtleader; + + if (esp_debug->value){ + int bonusEligible = (level.realFramenum - ESP_BONUS_COOLDOWN); + gi.dprintf("%s: lpc is %d, lpl is %d, lhl is %d\n", __FUNCTION__, lpc, lpl, lhl); + gi.dprintf("%s: attacker is %s, targ is %s\n", __FUNCTION__, attacker->client->pers.netname, targ->client->pers.netname); + gi.dprintf("%s: level.realFramenum is %i, subtracted ESP_BONUS_COOLDOWN is %i\n", __FUNCTION__, level.realFramenum, bonusEligible); + } + + // If the attacker has already received a bonus in the last ESP_BONUS_COOLDOWN frames + if (lpc > (level.realFramenum - ESP_BONUS_COOLDOWN)) + _EspBonusDefendCapture(targ, attacker); + if (lpl > (level.realFramenum - ESP_BONUS_COOLDOWN)) + _EspBonusDefendLeader(targ, attacker); + if (lhl > (level.realFramenum - ESP_BONUS_COOLDOWN)) + _EspBonusHarassLeader(targ, attacker); + + // This will always award a bonus if the attacker is eligible, no time limit + _EspBonusFragLeader(targ, attacker); + +} + +void EspCapturePointThink( edict_t *flag ) +{ + // If the flag was touched this frame, make it owned by that team. + if( flag->owner && flag->owner->client && flag->owner->client->resp.team ) { + unsigned int effect = esp_team_effect[ flag->owner->client->resp.team ]; + if( flag->s.effects != effect ) + { + edict_t *ent = NULL; + int prev_owner = EspCapturePointOwner( flag ); + + //gi.dprintf("prev flag owner team is %d\n", prev_owner); + + if( prev_owner != NOTEAM ) + esp_team_flags[ prev_owner ] --; + + flag->s.effects = effect; + flag->s.renderfx = esp_team_fx[ flag->owner->client->resp.team ]; + esp_team_flags[ flag->owner->client->resp.team ] ++; + flag->s.modelindex = esp_flag; + + // Get flag location if possible. + + // Commented out but may still be useful at some point + // This grabs the map location name + // qboolean has_loc = false; + // char location[ 128 ] = "("; + // has_loc = GetPlayerLocation( flag, location + 1 ); + // if( has_loc ) + // strcat( location, ") " ); + // else + // location[0] = '\0'; + + _EspBonusCapture(flag->owner, flag); + + // This is the game state, where the leader made it to the capture point + espsettings.escortcap = true; + + // Escort point captured, end round and start again + gi.sound( &g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("aqdt/aqg_bosswin.wav"), 1.0, ATTN_NONE, 0.0 ); + espsettings.escortcap = flag->owner->client->resp.team; + if (esp_punish->value) + EspPunishment(OtherTeam(flag->owner->client->resp.team)); + + if (use_rewards->value) { + if(teams[TEAM1].leader->client->resp.esp_capstreak == 5) + Announce_Reward(teams[TEAM1].leader, DOMINATING); + if(teams[TEAM1].leader->client->resp.esp_capstreak == 10) + Announce_Reward(teams[TEAM1].leader, UNSTOPPABLE); + } + + for( ent = g_edicts + 1; ent <= g_edicts + game.maxclients; ent ++ ) + { + if( ! (ent->inuse && ent->client && ent->client->resp.team) ) + continue; + else if( ent == flag->owner ) + unicastSound( ent, gi.soundindex("tng/flagcap.wav"), 0.5 ); + } + } + } + flag->owner = NULL; + + flag->nextthink = level.framenum + FRAMEDIV; +} + +void EspTouchCapturePoint( edict_t *flag, edict_t *player, cplane_t *plane, csurface_t *surf ) +{ + if( ! player->client ) + return; + if( ! player->client->resp.team ) + return; + if( (player->health < 1) || ! IS_ALIVE(player) ) + return; + if( lights_camera_action || in_warmup ) + return; + if( player->client->uvTime ) + return; + // Player must be team leader on team 1 to activate the flag + if (!IS_LEADER(player) || player->client->resp.team != TEAM1) + return; + + // If the flag hasn't been touched this frame, the player will take it. + if( ! flag->owner ) + flag->owner = player; +} + +void EspMakeCapturePoint(edict_t *flag) +{ + vec3_t dest = {0}; + trace_t tr = {0}; + + if (esp_debug->value) + gi.dprintf("%s: Creating a new capturepoint\n", __FUNCTION__); + VectorSet( flag->mins, -15, -15, -15 ); + VectorSet( flag->maxs, 15, 15, 15 ); + + // Put the flag on the ground. + VectorCopy( flag->s.origin, dest ); + dest[2] -= 128; + tr = gi.trace( flag->s.origin, flag->mins, flag->maxs, dest, flag, MASK_SOLID ); + if( ! tr.startsolid ) + VectorCopy( tr.endpos, flag->s.origin ); + + VectorCopy( flag->s.origin, flag->old_origin ); + + flag->solid = SOLID_TRIGGER; + flag->movetype = MOVETYPE_NONE; + flag->s.modelindex = esp_flag; + flag->s.skinnum = 0; + flag->s.effects = esp_team_effect[ NOTEAM ]; + flag->s.renderfx = esp_team_fx[ NOTEAM ]; + flag->owner = NULL; + flag->touch = EspTouchCapturePoint; + NEXT_KEYFRAME( flag, EspCapturePointThink ); + flag->classname = "item_flag"; + flag->svflags &= ~SVF_NOCLIENT; + gi.linkentity( flag ); + + /* Indicator arrow + This appears regardless of indicator settings + */ + #ifdef AQTION_EXTENSION + if (!flag->obj_arrow){ + flag->obj_arrow = G_Spawn(); + flag->obj_arrow->solid = SOLID_NOT; + flag->obj_arrow->movetype = MOVETYPE_NOCLIP; + flag->obj_arrow->classname = "ind_arrow_objective"; + flag->obj_arrow->owner = flag; + flag->obj_arrow->s.effects |= RF_INDICATOR | RF_GLOW; + flag->obj_arrow->s.modelindex = level.model_arrow; + + VectorCopy(flag->s.origin, flag->obj_arrow->s.origin); + // float arrow above briefcase + flag->obj_arrow->s.origin[2] += 75; + + gi.linkentity(flag->obj_arrow); + + if (esp_debug->value){ + gi.dprintf("%s: ** Indicator arrow spawned at <%d %d %d>\n", __FUNCTION__, flag->obj_arrow->s.origin[0], flag->obj_arrow->s.origin[1], flag->obj_arrow->s.origin[2]); + gi.dprintf("%s: ** Flag coordinates are: <%d %d %d>\n", __FUNCTION__, flag->s.origin[0], flag->s.origin[1], flag->s.origin[2]); + } + } + #endif + + esp_flag_count ++; +} + +// Function to find the sole capturepoint, remove it and recreate it exactly as it was +// This is used to reset the capturepoint after a round resets +void EspResetCapturePoint(void) +{ + edict_t *ent = NULL; + edict_t *flag = NULL; + + // Reset escortcap value + espsettings.escortcap = false; + + // Find the flag + while ((ent = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { + flag = ent; + } + if (flag){ + EspMakeCapturePoint(flag); + } +} + +void EspSetTeamSpawns(int team, char *str) +{ + edict_t *spawn = NULL; + char *next; + vec3_t pos; + float angle; + espsettings_t *es = &espsettings; + int esp_potential_spawns = 0; + + char *team_spawn_name = "info_player_team1"; + if(team == TEAM2) + team_spawn_name = "info_player_team2"; + if (teamCount == 3 && team == TEAM3) + team_spawn_name = "info_player_team3"; + + /* find and remove all team spawns for this team */ + while ((spawn = G_Find(spawn, FOFS(classname), team_spawn_name)) != NULL) { + G_FreeEdict (spawn); + } + + next = strtok(str, ","); + do { + if (sscanf(next, "<%f %f %f %f>", &pos[0], &pos[1], &pos[2], &angle) != 4) { + if (esp_debug->value) + gi.dprintf("%s: invalid spawn point: %s, expected \n", __FUNCTION__, next); + continue; + } + + spawn = G_Spawn(); + VectorCopy(pos, spawn->s.origin); + spawn->s.angles[YAW] = angle; + spawn->classname = ED_NewString (team_spawn_name); + + if (esp_debug->value) + gi.dprintf("%s: Created spawnpoint for %s at <%f %f %f %f>\n", __FUNCTION__, team_spawn_name, pos[0], pos[1], pos[2], angle); + es->custom_spawns[team][esp_potential_spawns] = spawn; + esp_potential_spawns++; + if (esp_potential_spawns >= MAX_SPAWNS) + { + if (esp_debug->value) + gi.dprintf("%s: Warning: MAX_SPAWNS exceeded\n", __FUNCTION__); + break; + } + + next = strtok(NULL, ","); + } while(next != NULL); + +} + +void EspEnforceDefaultSettings(char *defaulttype) +{ + qboolean default_team = (Q_stricmp(defaulttype,"team")==0) ? true : false; + qboolean default_respawn = (Q_stricmp(defaulttype,"respawn")==0) ? true : false; + qboolean default_author = (Q_stricmp(defaulttype,"author")==0) ? true : false; + int i = 0; + + if (esp_debug->value) + gi.dprintf("%s: defaulttype is %s\n", __FUNCTION__, defaulttype); + + if(default_author) { + EspForceEspionage(ESPMODE_ATL); + Q_strncpyz(espsettings.author, "AQ2World Team", sizeof(espsettings.author)); + Q_strncpyz(espsettings.name, "Time for Action!", sizeof(espsettings.name)); + + gi.dprintf(" Author : %s\n", espsettings.author); + gi.dprintf(" Name : %s\n", espsettings.name); + gi.dprintf(" Game type : Assassinate the Leader\n"); + } + + if(default_respawn) { + for (i = TEAM1; i <= teamCount; i++) { + teams[i].respawn_timer = ESP_DEFAULT_RESPAWN_TIME; + } + gi.dprintf(" Respawn Rate: %d seconds\n", ESP_DEFAULT_RESPAWN_TIME); + } + + if(default_team) { + /// Default skin/team/names - red team + Q_strncpyz(teams[TEAM1].name, ESP_RED_TEAM, sizeof(teams[TEAM1].name)); + Q_strncpyz(teams[TEAM1].skin, ESP_RED_SKIN, sizeof(teams[TEAM1].skin)); + Q_strncpyz(teams[TEAM1].leader_name, ESP_RED_LEADER_NAME, sizeof(teams[TEAM1].leader_name)); + Q_strncpyz(teams[TEAM1].leader_skin, ESP_RED_LEADER_SKIN, sizeof(teams[TEAM1].leader_skin)); + Q_strncpyz(teams[TEAM1].skin_index, "../pics/ctf_r_i", sizeof(teams[TEAM1].skin_index)); + Q_snprintf(teams[TEAM1].leader_skin_index, sizeof(teams[TEAM1].leader_skin_index), "../players/%s_i", teams[TEAM1].leader_skin); + + /// Default skin/team/names - blue team + Q_strncpyz(teams[TEAM2].name, ESP_BLUE_TEAM, sizeof(teams[TEAM2].name)); + Q_strncpyz(teams[TEAM2].skin, ESP_BLUE_SKIN, sizeof(teams[TEAM2].skin)); + Q_strncpyz(teams[TEAM2].leader_name, ESP_BLUE_LEADER_NAME, sizeof(teams[TEAM2].leader_name)); + Q_strncpyz(teams[TEAM2].leader_skin, ESP_BLUE_LEADER_SKIN, sizeof(teams[TEAM2].leader_skin)); + Q_strncpyz(teams[TEAM2].skin_index, "../pics/ctf_b_i", sizeof(teams[TEAM2].skin_index)); + Q_snprintf(teams[TEAM2].leader_skin_index, sizeof(teams[TEAM2].leader_skin_index), "../players/%s_i", teams[TEAM2].leader_skin); + if(teamCount == 3) { + /// Default skin/team/names - green team + Q_strncpyz(teams[TEAM3].name, ESP_GREEN_TEAM, sizeof(teams[TEAM3].name)); + Q_strncpyz(teams[TEAM3].skin, ESP_GREEN_SKIN, sizeof(teams[TEAM3].skin)); + Q_strncpyz(teams[TEAM3].leader_name, ESP_GREEN_LEADER_NAME, sizeof(teams[TEAM3].leader_name)); + Q_strncpyz(teams[TEAM3].leader_skin, ESP_GREEN_LEADER_SKIN, sizeof(teams[TEAM3].leader_skin)); + Q_strncpyz(teams[TEAM3].skin_index, "../pics/ctf_g_i", sizeof(teams[TEAM3].skin_index)); + Q_snprintf(teams[TEAM3].leader_skin_index, sizeof(teams[TEAM3].leader_skin_index), "../players/%s_i", teams[TEAM3].leader_skin); + } + gi.dprintf(" Red Team: %s -- Skin: %s\n", ESP_RED_TEAM, ESP_RED_SKIN); + gi.dprintf(" Red Leader: %s -- Skin: %s\n", ESP_RED_LEADER_NAME, ESP_RED_LEADER_SKIN); + gi.dprintf(" Blue Team: %s -- Skin: %s\n", ESP_BLUE_TEAM, ESP_BLUE_SKIN); + if(atl->value) + gi.dprintf(" Blue Leader: %s -- Skin: %s\n", ESP_BLUE_LEADER_NAME, ESP_BLUE_LEADER_SKIN); + if(teamCount == 3){ + gi.dprintf(" Green Team: %s -- Skin: %s\n", ESP_GREEN_TEAM, ESP_GREEN_SKIN); + gi.dprintf(" Green Leader: %s -- Skin: %s\n", ESP_GREEN_LEADER_NAME, ESP_GREEN_LEADER_SKIN); + } + } +} + +qboolean EspLoadConfig(const char *mapname) +{ + char buf[1024]; + char *ptr; + qboolean no_file = false; + qboolean loaded_default_file = false; + espsettings_t *es = &espsettings; + FILE *fh; + int i = 0; + + memset(&espsettings, 0, sizeof(espsettings)); + + //esp_flag = gi.modelindex("models/items/bcase/g_bc1.md2"); + esp_flag = gi.modelindex("models/cases/b_case.md3"); + + gi.dprintf("** Trying to load Espionage configuration file for %s **\n", mapname); + + sprintf (buf, "%s/tng/%s.esp", GAMEVERSION, mapname); + + fh = fopen (buf, "r"); + if (!fh) { + //Default to ATL mode in this case + gi.dprintf ("Warning: Espionage configuration file \" %s \" was not found.\n", buf); + EspForceEspionage(ESPMODE_ATL); + sprintf (buf, "%s/tng/default.esp", GAMEVERSION); + fh = fopen (buf, "r"); + if (!fh){ + gi.dprintf ("Warning: Default Espionage configuration file was not found.\n"); + gi.dprintf ("Using hard-coded Assassinate the Leader scenario settings.\n"); + no_file = true; + } else { + gi.dprintf("Found %s, attempting to load it...\n", buf); + loaded_default_file = true; + } + } else { + // Check if the file is empty + fseek(fh, 0, SEEK_END); + long size; + size = ftell(fh); + if (size == 0) { + no_file = true; + gi.dprintf("ERROR: Espionage file %s is empty, loading safe defaults\n", buf); + } + } + + // Hard-coded scenario settings so things don't break + if(no_file){ + EspForceEspionage(ESPMODE_ATL); + // TODO: A better GHUD method to display this? + gi.dprintf("-------------------------------------\n"); + gi.dprintf("Hard-coded Espionage configuration loaded\n"); + + // Set game type to ATL + /// Default game settings + EspEnforceDefaultSettings("author"); + EspEnforceDefaultSettings("respawn"); + EspEnforceDefaultSettings("team"); + + } else { // A valid file was found, load it + + gi.dprintf("-------------------------------------\n"); + gi.dprintf("Espionage configuration found at %s\n", buf); + ptr = INI_Find(fh, "esp", "author"); + if(ptr) { + gi.dprintf("- Author : %s\n", ptr); + Q_strncpyz(espsettings.author, ptr, sizeof(espsettings.author)); + } + ptr = INI_Find(fh, "esp", "name"); + if(ptr) { + gi.dprintf("- Name : %s\n", ptr); + Q_strncpyz(espsettings.name, ptr, sizeof(espsettings.name)); + } + + ptr = INI_Find(fh, "esp", "type"); + char *gametypename = ESPMODE_ATL_NAME; + if(!strcmp(ptr, ESPMODE_ATL_SNAME) && !strcmp(ptr, ESPMODE_ETV_SNAME)){ + gi.dprintf("Warning: Value for '[esp] type is not 'etv' or 'atl', forcing ATL mode\n"); + gi.dprintf("- Game type : %s\n", ESPMODE_ATL_NAME); + EspForceEspionage(ESPMODE_ATL); + gametypename = ESPMODE_ATL_NAME; + } else { + if(ptr) { + if((strcmp(ptr, ESPMODE_ETV_SNAME) == 0 && esp_atl->value == 0)){ + EspForceEspionage(ESPMODE_ETV); + gametypename = ESPMODE_ETV_NAME; + } else { + EspForceEspionage(ESPMODE_ATL); + gametypename = ESPMODE_ATL_NAME; + } + } + if (use_3teams->value) { + // Only ATL available in 3 team mode + EspForceEspionage(ESPMODE_ATL); + gametypename = ESPMODE_ATL_NAME; + } + gi.dprintf("- Game type : %s\n", gametypename); + } + // Force ATL mode trying to get ETV mode going with 3teams + if (etv->value && use_3teams->value){ + gi.dprintf("%s and use_3team are incompatible, defaulting to %s", ESPMODE_ETV_NAME, ESPMODE_ATL_NAME); + EspForceEspionage(ESPMODE_ATL); + } + + gi.dprintf("- Respawn times\n"); + char *r_respawn_time, *b_respawn_time, *g_respawn_time; + + r_respawn_time = INI_Find(fh, "respawn", "red"); + b_respawn_time = INI_Find(fh, "respawn", "blue"); + if(teamCount == 3) + g_respawn_time = INI_Find(fh, "respawn", "green"); + else + g_respawn_time = NULL; + + if ((!r_respawn_time || !b_respawn_time) || (teamCount == 3 && !g_respawn_time)){ + gi.dprintf("Warning: Malformed or missing settings for respawn times\n"); + gi.dprintf("Enforcing defaults\n"); + EspEnforceDefaultSettings("respawn"); + } else { + if(r_respawn_time) { + gi.dprintf(" Red : %s seconds\n", r_respawn_time); + teams[TEAM1].respawn_timer = atoi(r_respawn_time); + } + if(b_respawn_time) { + gi.dprintf(" Blue : %s seconds\n", b_respawn_time); + teams[TEAM2].respawn_timer = atoi(b_respawn_time); + } + if (teamCount == 3){ + if(g_respawn_time) { + gi.dprintf(" Green : %s seconds\n", g_respawn_time); + teams[TEAM3].respawn_timer = atoi(g_respawn_time); + } + } + } + + // Only set the flag if the scenario is ETV + if(etv->value) { + gi.dprintf("- Target\n"); + ptr = INI_Find(fh, "target", "escort"); + if(ptr) { + ptr = strchr( ptr, '<' ); + while( ptr ) + { + edict_t *flag = G_Spawn(); + + char *space = NULL, *end = strchr( ptr + 1, '>' ); + if( end ) + *end = '\0'; + + flag->s.origin[0] = atof( ptr + 1 ); + space = strchr( ptr + 1, ' ' ); + if( space ) + { + flag->s.origin[1] = atof( space ); + space = strchr( space + 1, ' ' ); + if( space ) + { + flag->s.origin[2] = atof( space ); + space = strchr( space + 1, ' ' ); + if( space ) + flag->s.angles[YAW] = atof( space ); + } + } + + EspMakeCapturePoint( flag ); + + // Set the capture point in the settings + es->capturepoint = flag; + + ptr = strchr( (end ? end : ptr) + 1, '<' ); + } + + if( !esp_flag_count ) { + gi.dprintf("Warning: Espionage needs 'escort' target coordinates in: tng/%s.esp\n", mapname); + gi.dprintf("* Forcing ATL mode to be safe\n"); + EspForceEspionage(ESPMODE_ATL); + } + + ptr = INI_Find(fh, "target", "name"); + size_t ptr_len = strlen(ptr); + if(ptr) { + if (ptr_len <= MAX_ESP_STRLEN) { + gi.dprintf(" Area : %s\n", ptr); + } else { + gi.dprintf("Warning: [target] name > 32 characters, setting to \"The Spot\""); + ptr = "The Spot"; + } + Q_strncpyz(espsettings.target_name, ptr, sizeof(espsettings.target_name)); + } + } else { + gi.dprintf( "Warning: Escort target coordinates not found in tng/%s.esp\n", mapname ); + gi.dprintf("* Forcing ATL mode to be safe\n"); + EspForceEspionage(ESPMODE_ATL); + } + } + + // No custom spawns in default file + if (loaded_default_file){ + // Clear custom spawns + for (i = TEAM1; i <= teamCount; i++) { + memset(es->custom_spawns[i], 0, sizeof(es->custom_spawns[i])); + } + } + + if (!loaded_default_file) { + ptr = INI_Find(fh, "spawns", "red"); + if(ptr) { + gi.dprintf("Team 1 spawns: %s\n", ptr); + EspSetTeamSpawns(TEAM1, ptr); + } + ptr = INI_Find(fh, "spawns", "blue"); + if(ptr) { + gi.dprintf("Team 2 spawns: %s\n", ptr); + EspSetTeamSpawns(TEAM2, ptr); + } + if (teamCount == 3){ + ptr = INI_Find(fh, "spawns", "green"); + if(ptr) { + gi.dprintf("Team 3 spawns: %s\n", ptr); + EspSetTeamSpawns(TEAM3, ptr); + } + } + } + + gi.dprintf("- Teams\n"); + for (i = TEAM1; i <= teamCount; i++) { + const char *team_color; + switch (i) { + case TEAM1: + team_color = "red_team"; + break; + case TEAM2: + team_color = "blue_team"; + break; + case TEAM3: + team_color = "green_team"; + break; + default: + continue; + } + + ptr = INI_Find(fh, team_color, "name"); + if (ptr) { + Q_strncpyz(teams[i].name, ptr, sizeof(teams[i].name)); + } + + ptr = INI_Find(fh, team_color, "skin"); + if (ptr) { + Q_strncpyz(teams[i].skin, ptr, sizeof(teams[i].skin)); + } + + ptr = INI_Find(fh, team_color, "leader_name"); + if (ptr) { + Q_strncpyz(teams[i].leader_name, ptr, sizeof(teams[i].leader_name)); + } + + ptr = INI_Find(fh, team_color, "leader_skin"); + if (ptr) { + Q_strncpyz(teams[i].leader_skin, ptr, sizeof(teams[i].leader_skin)); + } + } + + qboolean missing_property = false; + for (i = TEAM1; i <= teamCount; i++) { + if (strlen(teams[i].skin) == 0 || + strlen(teams[i].name) == 0 || + strlen(teams[i].leader_name) == 0 || + strlen(teams[i].leader_skin) == 0) { + missing_property = true; + break; + } + } + if (missing_property) { + gi.dprintf("Warning: Could not read value for team skin, name, leader or leader_skin; review your file\n"); + gi.dprintf("* Enforcing safe defaults\n"); + EspEnforceDefaultSettings("team"); + espsettings.custom_skins = false; + } else { + espsettings.custom_skins = true; + } + + for (i = TEAM1; i <= teamCount; i++) { + const char *team_color; + switch (i) { + case TEAM1: + team_color = "Red"; + break; + case TEAM2: + team_color = "Blue"; + break; + case TEAM3: + team_color = "Green"; + break; + default: + continue; + } + + gi.dprintf(" %s Team: %s, Skin: %s\n", team_color, teams[i].name, teams[i].skin); + gi.dprintf(" %s Leader: %s, Skin: %s\n\n", team_color, teams[i].leader_name, teams[i].leader_skin); + } + } + + gi.dprintf("-------------------------------------\n"); + + if (fh) + fclose(fh); + + // Load skin indexes + if (!no_file || !loaded_default_file) { + if (esp_debug->value) + gi.dprintf("%s: *** Loading skin indexes\n", __FUNCTION__); + Q_snprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); + Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); + Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); + Q_snprintf(teams[TEAM1].leader_skin_index, sizeof(teams[TEAM1].leader_skin_index), "../players/%s_i", teams[TEAM1].leader_skin); + if (atl->value) { + Q_snprintf(teams[TEAM2].leader_skin_index, sizeof(teams[TEAM2].leader_skin_index), "../players/%s_i", teams[TEAM2].leader_skin); + Q_snprintf(teams[TEAM3].leader_skin_index, sizeof(teams[TEAM3].leader_skin_index), "../players/%s_i", teams[TEAM3].leader_skin); + } + } + + if((etv->value) && teamCount == 3){ + gi.dprintf("Warning: ETV mode requested with use_3teams enabled, forcing ATL mode"); + EspForceEspionage(ESPMODE_ATL); + } + + return true; +} + +int EspGetRespawnTime(edict_t *ent) +{ + int spawntime = teams[ent->client->resp.team].respawn_timer; + if(ent->client->resp.team == TEAM1 && teams[TEAM1].respawn_timer > -1) + spawntime = teams[TEAM1].respawn_timer; + else if(ent->client->resp.team == TEAM2 && teams[TEAM2].respawn_timer > -1) + spawntime = teams[TEAM2].respawn_timer; + else if((teamCount == 3) && ent->client->resp.team == TEAM3 && teams[TEAM3].respawn_timer > -1) + spawntime = teams[TEAM3].respawn_timer; + + if (!IS_LEADER(ent) && team_round_going) { + gi.centerprintf(ent, "You will respawn in %d seconds\n", spawntime); + } + return spawntime; +} + +/* +TODO: Fire this when there's 4 seconds left before a respawn +*/ +void EspRespawnLCA(edict_t *ent) +{ + + // Print out all conditions below as debug prints + // This is massively noisy so I'm setting it so that esp_debug must be 2 to see it + // if (esp_debug->value == 2) + // gi.dprintf("%s: ent->inuse is %d\n", __FUNCTION__, ent->inuse); + // gi.dprintf("%s: ent->client->resp.team is %d\n", __FUNCTION__, ent->client->resp.team); + // gi.dprintf("%s: ent->client->respawn_framenum is %d\n", __FUNCTION__, ent->client->respawn_framenum); + // gi.dprintf("%s: IS_LEADER(ent) is %d\n", __FUNCTION__, IS_LEADER(ent)); + // gi.dprintf("%s: ent->is_bot is %d\n", __FUNCTION__, ent->is_bot); + // gi.dprintf("%s: team_round_going is %d\n", __FUNCTION__, team_round_going); + + // Basically we just want real, dead players who are in the respawn waiting period + if (!ent->inuse || + ent->client->resp.team == NOTEAM || + ent->client->respawn_framenum <= 0 || + IS_LEADER(ent) || + ent->is_bot || + !team_round_going) + return; + + if (ent->client->resp.team && !IS_ALIVE(ent)){ + int timercalc = (ent->client->respawn_framenum - level.framenum); + + if (esp_debug->value) + gi.dprintf("%s: Level framenum is %d, respawn timer was %d for %s, timercalc is %i, esp_respawn_sounds was %i\n", + __FUNCTION__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname, timercalc, ent->client->resp.esp_respawn_sounds); + + // Subtract current framenum from respawn_timer to get a countdown + if (timercalc <= 0){ + // Play no sound, they've respawned by now + return; + } else if (timercalc <= (20 * FRAMEDIV) && ent->client->resp.esp_respawn_sounds == 2) { + gi.centerprintf(ent, "CAMERA..."); + unicastSound(ent, gi.soundindex("atl/camera.wav"), 1.0); + //gi.sound(ent, CHAN_VOICE, level.snd_camera, 1.0, ATTN_STATIC, 0.0); + ent->client->resp.esp_respawn_sounds = 1; + return; + } else if (timercalc <= (40 * FRAMEDIV) && ent->client->resp.esp_respawn_sounds == 0) { + gi.centerprintf(ent, "LIGHTS..."); + unicastSound(ent, gi.soundindex("atl/lights.wav"), 1.0); + //gi.sound(ent, CHAN_VOICE, level.snd_lights, 1.0, ATTN_STATIC, 0.0); + ent->client->resp.esp_respawn_sounds = 2; + return; + } + } else { + return; + } +} + +/* +Espionage respawn logic, depends on modified respawn() to tell +the game to spawn the players at their leader spot +*/ +void EspRespawnPlayer(edict_t *ent) +{ + // Leaders do not respawn + if (IS_LEADER(ent)) + return; + + // Only respawn if the round is going + if (team_round_going) { + // Don't respawn until the current framenum is more than the respawn timer's framenum + if (esp_debug->value) + gi.dprintf("%s: Level framenum is %d, respawn timer was %d for %s\n", __FUNCTION__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname); + + if (level.framenum > ent->client->respawn_framenum) { + if (esp_debug->value) { + gi.dprintf("Is it ETV mode? %f\n", etv->value); + gi.dprintf("Is team 1 leader alive? %d\n", IS_ALIVE(teams[TEAM1].leader)); + gi.dprintf("Is team 1's leader NULL? %d\n", teams[TEAM1].leader == NULL); + } + + // Only respawn if leader(s) are still alive and the round is still going + if (atl->value) { + if (teams[ent->client->resp.team].leader != NULL && IS_ALIVE(teams[ent->client->resp.team].leader)) { + gi.centerprintf(ent, "ACTION!"); + unicastSound(ent, gi.soundindex("atl/action.wav"), 1.0); + //gi.sound(ent, CHAN_VOICE, level.snd_action, 1.0, ATTN_STATIC, 0.0); + ent->client->resp.esp_respawn_sounds = 0; + respawn(ent); + } + + } else if (etv->value) { + if (teams[TEAM1].leader != NULL && IS_ALIVE(teams[TEAM1].leader)) { + gi.centerprintf(ent, "ACTION!"); + unicastSound(ent, gi.soundindex("atl/action.wav"), 1.0); + //gi.sound(ent, CHAN_VOICE, level.snd_action, 1.0, ATTN_STATIC, 0.0); + ent->client->resp.esp_respawn_sounds = 0; + respawn(ent); + } + } + } + } +} + +/* +Internally used only, spot check if leader is alive +depending on the game mode +Please run a NULL check on the leader before calling this +*/ +qboolean _EspLeaderAliveCheck(edict_t *ent, edict_t *leader, int espmode) +{ + if (espmode < 0) { + gi.dprintf("Warning: Invalid espmode returned from EspModeCheck()\n"); + return false; + } + + if (!leader) + return false; + + if (espmode == ESPMODE_ATL) { + if (teams[leader->client->resp.team].leader && + IS_ALIVE(teams[leader->client->resp.team].leader)) + return true; + else + return false; + } + + if (espmode == ESPMODE_ETV) { + if (ent->client->resp.team == TEAM1 && + IS_ALIVE(teams[TEAM1].leader)) + return true; + else + return false; + } + + return false; +} + +/* +This should return one spawnpoint coordinate for each team when round begins, +so that everyone per team spawns in the same place. After that, it should +randomly choose a spawnpoint for each team, based on the es->custom_spawns[index#] + +This is reset back to default in EspEndOfRoundCleanup() +*/ +edict_t *SelectEspCustomSpawnPoint(edict_t * ent) +{ + espsettings_t *es = &espsettings; + int teamNum = ent->client->resp.team; + + // An index has been set, so spawn there, else find a random one + if (esp_spawnpoint_index[teamNum] >= 0) + return es->custom_spawns[teamNum][esp_spawnpoint_index[teamNum]]; + + // fresh round, so pick a random spawn + else { + int count = EspSpawnpointCount(teamNum); + int random_index = rand() % count; + if (count > 0) { + do { + random_index = (random_index + 1) % count; // Cycle through the spawn points + } while (count > 1 && random_index == esp_last_chosen_spawn); // Keep generating a new index until it is different from the last one, unless there is only one spawn point + } else { + // If we count zero custom spawns, then we need to safely return a better function + // so we can spawn teams apart + if (esp_debug->value) + gi.dprintf("%s: With zero spawnpoints, I am calling SelectRandomDeathmatchSpawnPoint()\n", __FUNCTION__); + return SelectRandomDeathmatchSpawnPoint(); + } + // Keep track of which spawn was last chosen + esp_last_chosen_spawn = random_index; + esp_spawnpoint_index[teamNum] = esp_last_chosen_spawn; + if (esp_debug->value) + gi.dprintf("%s: For team %d, random index is %d\n", __FUNCTION__, teamNum, esp_spawnpoint_index[teamNum]); + } + // Everyone on each team spawns on the same spawnpoint index (spawn together) + edict_t *spawn_point = es->custom_spawns[teamNum][esp_spawnpoint_index[teamNum]]; + // if (spawn_point != NULL) { + // gi.dprintf("For team %d, random index is %d, spawn coordinates were %f %f %f\n", teamNum, esp_spawnpoint_index[teamNum], spawn_point->s.origin[0], spawn_point->s.origin[1], spawn_point->s.origin[2]); + // } else { + // gi.dprintf("For team %d, random index is %d, but the spawn point is NULL\n", teamNum, esp_spawnpoint_index[teamNum]); + // } + + if (spawn_point) + return spawn_point; + else { + gi.dprintf("%s: No spawnpoint found, safely return NULL so we can try another one\n", __FUNCTION__); + return NULL; + } +} + +edict_t *EspRespawnOnLeader(edict_t *ent, char *cname) +{ + edict_t *teamLeader, *spawn; + vec3_t respawn_coords; + + teamLeader = teams[ent->client->resp.team].leader; + VectorCopy(teamLeader->s.origin, respawn_coords); + + spawn = G_Spawn(); + respawn_coords[2] += 9; // So they don't spawn in the floor + VectorCopy(respawn_coords, spawn->s.origin); + spawn->s.angles[YAW] = teamLeader->s.angles[YAW]; // Facing the same direction as the leader on respawn + spawn->classname = ED_NewString(cname); + spawn->think = G_FreeEdict; + spawn->nextthink = level.framenum + 1; + //ED_CallSpawn(spawn); + + //gi.dprintf("Respawn coordinates are %f %f %f %f\n", teamLeader->s.origin[0], teamLeader->s.origin[1], teamLeader->s.origin[2], teamLeader->s.angles[YAW]); + + return spawn; +} + +edict_t *SelectEspSpawnPoint(edict_t *ent) +{ + //edict_t *spot, *spot1, *spot2; + //int count = 0; + //int selection; + //float range, range1, range2; + + char *cname; + int teamNum = ent->client->resp.team; + + ent->client->resp.esp_state = ESP_STATE_PLAYING; + + switch (teamNum) { + case TEAM1: + cname = "info_player_team1"; + break; + case TEAM2: + cname = "info_player_team2"; + break; + case TEAM3: + cname = "info_player_deathmatch"; + break; + default: + cname = "info_player_deathmatch"; + break; + } + + /* Regular initial round spawn logic: + Check if this is a respawn or a round start spawn + Respawn Logic: + 1. It's ETV mode + 2. You are on TEAM1 + 3. TEAM1's leader is alive + 4. Then spawn at the leader's location + (if you are on TEAM2, you respawn at your original spawnpoint) + or + 1. It's ATL mode + 2. Your leader is alive + 3. Then spawn at your leader's location + + If none of this is true, it's a round start spawn + */ + + if (in_warmup) + return SelectDeathmatchSpawnPoint(); + + if (esp_debug->value){ + gi.dprintf("%s: Is team round going? %d\n", __FUNCTION__, team_round_going); + gi.dprintf("%s: Is the team leader alive? %d\n", __FUNCTION__, _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())); + } + + // Time to respawn on the leader! + if (team_round_going && _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())) { + return EspRespawnOnLeader(ent, cname); + } else { + // Custom spawns take precedence over standard spawns + if ((EspSpawnpointCount(teamNum) > 0)) { + return SelectEspCustomSpawnPoint(ent); + + // but if there are none, then we go back to old faithful + } else { + if (esp_debug->value) + gi.dprintf("%s: No custom spawns, defaulting to teamplay spawn\n", __FUNCTION__); + + // NULL check, if there are no teamplay (info_player_team...) spawns, then default to deathmatch spawns + if (SelectTeamplaySpawnPoint(ent)) + return SelectTeamplaySpawnPoint(ent); + else + return SelectDeathmatchSpawnPoint(); + } + } + // All else fails, use deathmatch spawn points + if (esp_debug->value) + gi.dprintf("%s: Defaulted all the way down here\n", __FUNCTION__); + return SelectFarthestDeathmatchSpawnPoint(); +} + +void SetEspStats( edict_t *ent ) +{ + int i; + + // GHUD team icons, ATL gets leader skin indexes, ETV gets team skin indexes + for(i = TEAM1; i <= teamCount; i++) + if (etv->value) + level.pic_teamskin[i] = gi.imageindex(teams[i].skin_index); + else if (atl->value) + level.pic_teamskin[i] = gi.imageindex(teams[i].leader_skin_index); + + // Load scoreboard images + level.pic_esp_teamtag[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); + level.pic_esp_teamicon[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); + level.pic_esp_leadericon[TEAM1] = gi.imageindex(teams[TEAM1].leader_skin_index); + gi.imageindex("sbfctf1"); + + level.pic_esp_teamtag[TEAM2] = gi.imageindex(teams[TEAM2].skin_index); + level.pic_esp_teamicon[TEAM2] = gi.imageindex(teams[TEAM2].skin_index); + level.pic_esp_leadericon[TEAM2] = gi.imageindex(teams[TEAM2].leader_skin_index); + gi.imageindex("sbfctf2"); + + if (atl->value && teamCount == 3) { + level.pic_esp_teamtag[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); + level.pic_esp_teamicon[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); + level.pic_esp_leadericon[TEAM3] = gi.imageindex(teams[TEAM3].leader_skin_index); + gi.imageindex("sbfctf3"); + } + + // Now set the HUD + // Set the new respawn icon indicator + ent->client->ps.stats[STAT_TIMER_ICON] = level.pic_esp_respawn_icon; + + ent->client->ps.stats[ STAT_TEAM1_HEADER ] = level.pic_esp_teamtag[ TEAM1 ]; + ent->client->ps.stats[ STAT_TEAM2_HEADER ] = level.pic_esp_teamtag[ TEAM2 ]; + + // Team scores for the score display and HUD. + ent->client->ps.stats[ STAT_TEAM1_SCORE ] = teams[ TEAM1 ].score; + ent->client->ps.stats[ STAT_TEAM2_SCORE ] = teams[ TEAM2 ].score; + + // Team icons for the score display and HUD. + ent->client->ps.stats[ STAT_TEAM1_PIC ] = level.pic_esp_teamicon[ TEAM1 ]; + ent->client->ps.stats[ STAT_TEAM2_PIC ] = level.pic_esp_teamicon[ TEAM2 ]; + // ent->client->ps.stats[ STAT_TEAM1_LEADERPIC ] = level.pic_esp_leadericon[ TEAM1 ]; + // ent->client->ps.stats[ STAT_TEAM2_LEADERPIC ] = level.pic_esp_leadericon[ TEAM2 ]; + + if (teamCount == 3) { + ent->client->ps.stats[ STAT_TEAM3_SCORE ] = teams[ TEAM3 ].score; + ent->client->ps.stats[ STAT_TEAM3_PIC ] = level.pic_esp_teamicon[ TEAM3 ]; + //ent->client->ps.stats[ STAT_TEAM3_LEADERPIC ] = level.pic_esp_leadericon[ TEAM3 ]; + } + + // Shows the timer and icon, but does not count down if ent is Leader, it stays at 0 + + if (ent->client->respawn_framenum > 0 && + ent->client->respawn_framenum - level.framenum > 0){ + ent->client->ps.stats[STAT_TIMER_ICON] = level.pic_esp_respawn_icon; + if (!IS_LEADER(ent)) + ent->client->ps.stats[STAT_TIMER] = (ent->client->respawn_framenum - level.framenum) / HZ; + else + ent->client->ps.stats[STAT_TIMER] = 0; + } + + // During gameplay, flash your team's icon + if( (team_round_going || lights_camera_action > 0) && ((level.realFramenum / FRAMEDIV) & 4) ) + { + if (ent->client->resp.team == TEAM1) + ent->client->ps.stats[ STAT_TEAM1_PIC ] = 0; + else if (ent->client->resp.team == TEAM2) + ent->client->ps.stats[ STAT_TEAM2_PIC ] = 0; + else if (ent->client->resp.team == TEAM3) + ent->client->ps.stats[ STAT_TEAM3_PIC ] = 0; + } + + // During intermission, blink the team icon of the winning team. + if( level.intermission_framenum && ((level.realFramenum / FRAMEDIV) & 8) ) + { + if (esp_winner == TEAM1) + ent->client->ps.stats[ STAT_TEAM1_PIC ] = 0; + else if (esp_winner == TEAM2) + ent->client->ps.stats[ STAT_TEAM2_PIC ] = 0; + else if (teamCount == 3 && esp_winner == TEAM3) + ent->client->ps.stats[ STAT_TEAM3_PIC ] = 0; + } + + ent->client->ps.stats[STAT_ID_VIEW] = 0; + if (!ent->client->pers.id) + SetIDView(ent); +} + +void EspSwapTeams(void) +{ + edict_t *ent; + int i; + + // Swap members of both teams and strip leadership from TEAM1 + for (i = 0; i < game.maxclients; i++) { + ent = &g_edicts[1 + i]; + if (ent->inuse && ent->client->resp.team) { + EspLeaderLeftTeam(ent); + ent->client->resp.is_volunteer = false; + ent->client->resp.team = OtherTeam(ent->client->resp.team); + AssignSkin(ent, teams[ent->client->resp.team].skin, false); + } + } + + /* swap scores too! */ + i = teams[TEAM1].score; + teams[TEAM1].score = teams[TEAM2].score; + teams[TEAM2].score = i; + + teams_changed = true; +} + +void KillEveryone(int teamNum) +{ + edict_t *ent; + int i; + + for (i = 0; i < game.maxclients; i++){ + ent = &g_edicts[1 + i]; + if (!ent->inuse) + continue; + if(ent->solid == SOLID_NOT && !ent->deadflag) + continue; + if(IS_LEADER(ent)) // Don't kill a dead leader + continue; + if (ent->client->resp.team == teamNum){ + killPlayer(ent, false); + } + } +} + +qboolean EspCheckETVRules(void) +{ + int t1 = teams[TEAM1].score; + int t2 = teams[TEAM2].score; + int roundlimitwarn = (((int)roundlimit->value / 2) - 1); + + // Espionage ETV uses the same roundlimit cvars as teamplay + if(roundlimit->value && + (teams[TEAM1].score >= roundlimit->value || + teams[TEAM2].score >= roundlimit->value) ){ + gi.bprintf(PRINT_HIGH, "Roundlimit hit.\n"); + IRC_printf(IRC_T_GAME, "Roundlimit hit.\n"); + return true; + } + // Must be etv mode, halftime must have not occured yet, and be enabled, and the roundlimit must be set + + // Print all condition states + if (esp_debug->value) { + gi.dprintf("-- Debug start %s --\n", __FUNCTION__); + gi.dprintf("Roundlimit is %f\n", roundlimit->value); + gi.dprintf("Team 1 score is %d\n", teams[TEAM1].score); + gi.dprintf("Team 2 score is %d\n", teams[TEAM2].score); + gi.dprintf("Team 1 + Team 2 score is %d\n", t1 + t2); + gi.dprintf("Roundlimit warning is %d\n", roundlimitwarn); + gi.dprintf("Halftime is %d\n", espsettings.halftime); + gi.dprintf("ETV is %f\n", etv->value); + gi.dprintf("Use warnings is %f\n", use_warnings->value); + gi.dprintf("ETV halftime is %f\n", esp_etv_halftime->value); + gi.dprintf("-- Debug end %s --\n", __FUNCTION__); + } + + if (!roundlimit->value && esp_etv_halftime->value){ + // No roundlimit means no halftime either + disablecvar(esp_etv_halftime, "No Roundlimit"); + return false; + } + + // This is checking for conditions involving halftime + if (esp_etv_halftime->value) { + static qboolean halftimeMessageAdded = false; + + if(!espsettings.halftime && (t1 + t2 == roundlimitwarn)){ + if( use_warnings->value && !halftimeMessageAdded ){ + gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("world/incoming.wav"), 1.0, ATTN_NONE, 0.0 ); + EspTimedMessageHandler(TEAM1, NULL, 3, ESP_HALFTIME_WARNING); + halftimeMessageAdded = true; + } + } else if(!espsettings.halftime && (t1 + t2 == (roundlimit->value / 2))){ + team_round_going = team_round_countdown = team_game_going = 0; + MakeAllLivePlayersObservers(); + EspSwapTeams(); + CenterPrintLevelTeam(TEAM1, PRINT_LOW, "The teams have been switched!\nYour team needs a leader to volunteer!\n"); + CenterPrintLevelTeam(TEAM2, PRINT_LOW, "The teams have been switched!\nYou are now defending!\n"); + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, + gi.soundindex("misc/secret.wav"), 1.0, ATTN_NONE, 0.0); + espsettings.halftime = 1; + } + } + + // Default return false, which includes !esp_etv_halftime->value + return false; +} + +qboolean EspCheckRules(void) +{ + // Expand if we introduce other Espionage modes + if (etv->value) + return EspCheckETVRules(); + + return false; +} + +/* +Skin Check +Validates players and leaders have the correct skin for their team +*/ +void EspSkinCheck(void) +{ + edict_t *ent; + int i; + + // If we don't have proper leaders, don't run this + if (!EspLeaderCheck()) { + gi.dprintf("Leaders not found, not re-skinning"); + return; + } + + for (i = 0; i < game.maxclients; i++) { + ent = &g_edicts[1 + i]; + if (!ent->inuse || ent->client->resp.team == NOTEAM) + continue; + if (ent->client->resp.team == TEAM1) { + if (!IS_LEADER(ent)) { + AssignSkin(ent, teams[TEAM1].skin, false); + } else { + AssignSkin(ent, teams[TEAM1].leader_skin, false); + } + } else if (ent->client->resp.team == TEAM2) { + if (!IS_LEADER(ent)) { + AssignSkin(ent, teams[TEAM2].skin, false); + } else { + AssignSkin(ent, teams[TEAM2].leader_skin, false); + } + } else if (ent->client->resp.team == TEAM3) { + if (!IS_LEADER(ent)) { + AssignSkin(ent, teams[TEAM3].skin, false); + } else { + AssignSkin(ent, teams[TEAM3].leader_skin, false); + } + } + } +} + +/* +This check is similiar to checking that all teams have +Captains in matchmode +*/ +qboolean AllTeamsHaveLeaders(void) +{ + int teamsWithLeaders = 0; + int i = 0; + + //AQ2:TNG Slicer Matchmode + if (matchmode->value && !TeamsReady()) + return false; + //AQ2:TNG END + + for (i = TEAM1; i <= teamCount; i++){ + if (HAVE_LEADER(i)) { + teamsWithLeaders++; + } + } + + // Only Team 1 needs a leader in ETV mode + if((etv->value) && HAVE_LEADER(TEAM1)) { + //gi.dprintf("ETV team has a leader\n"); + return true; + } else if(atl->value && (teamsWithLeaders == teamCount)){ + //gi.dprintf("Teams with leaders is the same as the team count\n"); + return true; + } else { + return false; + } + if (esp_debug->value) + gi.dprintf("%s: Leadercount: %d\n", __FUNCTION__, teamsWithLeaders); + return false; +} + +qboolean EspSetLeader( int teamNum, edict_t *ent ) +{ + edict_t *oldLeader = teams[teamNum].leader; + char temp[128]; + + if (teamNum == NOTEAM){ + ent = NULL; + return false; + } + + if (etv->value && teamNum != TEAM1) { + gi.cprintf(ent, PRINT_MEDIUM, "** Only the Red team (team 1) has a leader in ETV mode **\n"); + return false; + } + + // NULL check and checks if leadertime is more than 0 and less than 10 seconds ago + if ((ent && ent->client->resp.esp_leadertime > 0) && + (level.realFramenum - ent->client->resp.esp_leadertime < 10 * HZ)){ + if (IS_CAPTAIN(ent)){ // This is to avoid printing this message when becoming captain + gi.cprintf(ent, PRINT_HIGH, "** You must wait 10 seconds between toggling your leader role! **\n"); + } + return false; + } + + + teams[teamNum].leader = ent; + // if(ent) // Only assign a skin to an ent + // AssignSkin(ent, teams[teamNum].leader_skin, false); + + if (!ent) { + if (!team_round_going || (gameSettings & GS_ROUNDBASED)) { + if (teams[teamNum].ready) { + Q_snprintf( temp, sizeof( temp ), "%s has lost their leader and is no longer ready to play!", teams[teamNum].name ); + CenterPrintAll( temp ); + } + teams[teamNum].ready = 0; + } + if (oldLeader) { + Q_snprintf(temp, sizeof(temp), "%s is no longer %s's leader\n", oldLeader->client->pers.netname, teams[teamNum].name ); + CenterPrintAll(temp); + //gi.bprintf( PRINT_HIGH, "%s needs a new leader! Enter 'volunteer' to apply for duty\n", teams[teamNum].name ); + } + teams[teamNum].locked = 0; + } + + if (ent != oldLeader && ent != NULL) { + Q_snprintf(temp, sizeof(temp), "%s is now %s's leader\n", ent->client->pers.netname, teams[teamNum].name ); + CenterPrintAll(temp); + gi.cprintf( ent, PRINT_CHAT, "You are the leader of '%s'\n", teams[teamNum].name ); + unicastSound(ent, gi.soundindex("aqdt/leader.wav"), 1.0); + //gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex( "misc/comp_up.wav" ), 1.0, ATTN_NONE, 0.0 ); + AssignSkin(ent, teams[teamNum].leader_skin, false); + // Set the time the player became leader so they can't unleader immediately after + ent->client->resp.esp_leadertime = level.realFramenum; + return true; + } + + return false; +} + +/* +Call this if the leader of a team disconnects/leaves +and there are no active volunteers for that team +*/ +qboolean EspChooseRandomLeader(int teamNum) +{ + int players[TEAM_TOP] = { 0 }, i, numPlayers = 0; + edict_t *ent, *playerList[MAX_CLIENTS]; + + if (team_round_going) { + if (esp_debug->value) + gi.dprintf("%s: I was called because someone disconnected\n", __FUNCTION__); + + if (matchmode->value && !TeamsReady()) + return false; + + // Count the number of players on the team and add them to the playerList + for (i = 0; i < game.maxclients; i++) { + ent = &g_edicts[1 + i]; + // Must be on a team and alive, as leaders can't respawn, choosing a dead ent will stop the game + if (!ent->inuse || game.clients[i].resp.team == NOTEAM || !IS_ALIVE(ent)) + continue; + if (!game.clients[i].resp.subteam && game.clients[i].resp.team == teamNum) { + players[teamNum]++; + playerList[numPlayers++] = ent; + } + } + + // If no players are left on the team, return + if (players[teamNum] == 0) + return false; + + if (esp_debug->value) + gi.dprintf("%s: players on team %d: %d\n", __FUNCTION__, teamNum, players[teamNum]); + + // Choose a random player from the playerList + ent = playerList[rand() % numPlayers]; + + if (esp_debug->value) + gi.dprintf("%s: Randomly selected player on team %d: %s\n", __FUNCTION__, teamNum, ent->client->pers.netname); + + // Set the selected player as the leader + EspSetLeader(teamNum, ent); + return true; + } // If we get this far, that the only players here are bots and non-volunteers. Halt the game + return false; +} + +/* +Check if this team has any volunteers, choose them first before +randomly choosing non-volunteers. Returns NULL if none are found. +*/ +edict_t *EspVolunteerCheck(int teamNum) +{ + int i = 0; + edict_t *ent; + + for (i = 0; i < game.maxclients; i++) { + ent = g_edicts + 1 + i; + if (ent->inuse && ent->client->resp.team == teamNum && ent->client->resp.is_volunteer) { + return ent; + } + } + // No volunteer found + return NULL; +} + +/* +Check if each team has a leader, if not, choose a volunteer, else choose one at random +This should only fail if there is no one to choose +*/ +qboolean EspLeaderCheck(void) +{ + int i = 0; + edict_t *newLeader; + qboolean athl = AllTeamsHaveLeaders(); + + // Quick sanity check in case this ever occurs as I've seen it on rare occasions + for (i = TEAM1; i <= teamCount; i++) { + if (teams[i].leader && !IS_ALIVE(teams[i].leader) && team_round_going && !holding_on_tie_check) { + if (esp_debug->value) + gi.dprintf("%s: Team %i leader is dead, but the round is still going...?\n", __FUNCTION__, teams[i].leader->client->resp.team); + athl = false; + } + } + + // Bot scan, rotate leaders if a leader is a bot and it died last round + for (i = TEAM1; i <= teamCount; i++) { + if (teams[i].leader && teams[i].leader->is_bot && teams[i].leader_dead) { + EspChooseRandomLeader(i); + } + } + + // If we all have leaders, then we're good + if (athl) { + if (esp_debug->value) + gi.dprintf("%s: I don't need a leader!\n", __FUNCTION__); + return true; + } else { + if (esp_debug->value) + gi.dprintf("%s: I need a leader!\n", __FUNCTION__); + // We do not all have leaders, so we must cycle through each team + for (i = TEAM1; i <= teamCount; i++) { + if (!HAVE_LEADER(i)) { // If this team does not have a leader, get one + newLeader = EspVolunteerCheck(i); + if (newLeader) { + if (EspSetLeader(i, newLeader)) { + if (esp_debug->value) + gi.dprintf("%s: I found a leader!\n", __FUNCTION__); + return true; + } + } else { // Oops, no volunteers, then we force someone to be a leader + if (EspChooseRandomLeader(i)) { + if (esp_debug->value) + gi.dprintf("%s: I need a random leader!\n", __FUNCTION__); + return true; + } + } + + // If we still don't have a leader, then the next round can't begin + if (!newLeader) { + if (team_round_going){ + gi.bprintf( PRINT_HIGH, "%s no longer has a leader, they can not win this round\n", teams[i].name ); + teams[i].leader_dead = true; // This should trigger round end + KillEveryone(i); + } + teams[i].leader = NULL; // Clear this in case some strange things happen + gi.bprintf( PRINT_HIGH, "%s needs a new leader! Enter 'leader' to apply for duty\n", teams[i].name ); + } + } + } + } + return false; +} + +void EspLeaderLeftTeam( edict_t *ent ) +{ + int teamNum = ent->client->resp.team; + + ent->client->resp.is_volunteer = false; + if (!IS_LEADER(ent)){ + return; + } else { + EspSetLeader( teamNum, NULL ); + + ent->client->resp.subteam = 0; + + if (!teams[teamNum].leader) { + EspLeaderCheck(); + } + } +} + +/* +This is called from player_die, and only called +if the player was a leader +*/ +int EspReportLeaderDeath(edict_t *ent) +{ + // Get the team the leader was on + int dead_leader_team = ent->client->resp.team; + int winner = 0; + + // Set the dead leader val + teams[dead_leader_team].leader_dead = true; + + // This checks if leader was on TEAM 1 in ETV mode + if (etv->value) { + if (dead_leader_team == TEAM1) { + winner = TEAM2; + } + } + + // ATL mode - 2 team winner checks + if (atl->value) { + if (teamCount == 2) { + if (dead_leader_team == TEAM1) + winner = TEAM2; + else if (dead_leader_team == TEAM2) + winner = TEAM1; + // 3 team winner checks + } else { + if (dead_leader_team == TEAM1) { + if (IS_ALIVE(teams[TEAM2].leader) && !IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM2; + else if (!IS_ALIVE(teams[TEAM2].leader) && IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM3; + } + else if (dead_leader_team == TEAM2) { + if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM1; + else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM3; + } + else if (dead_leader_team == TEAM3) { + if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM2].leader)) + winner = TEAM1; + else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM2].leader)) + winner = TEAM2; + } + } + } + + // Find all players in the game and play this sound + gi.sound(&g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("tng/leader_death.wav"), 1.0, ATTN_NONE, 0.0); + if (esp_punish->value) + EspPunishment(dead_leader_team); + + // Stats Reset + if (ent->client->resp.esp_capstreak > ent->client->resp.esp_capstreakbest) + ent->client->resp.esp_capstreakbest = ent->client->resp.esp_capstreak; + ent->client->resp.esp_capstreak = 0; + + // gi.dprintf("\n\nAdded message handler\n\n"); + // EspTimedMessageHandler(0, NULL, 15, ESP_LEADER_DIED); + + return winner; +} + +void MakeTeamInvulnerable(int winner, int uvtime) +{ + edict_t *ent; + int i = 0; + + for (i = 0; i < game.maxclients; i++){ + ent = &g_edicts[1 + i]; + // Make alive clients invulnerable + if ((game.clients[i].resp.team == winner) && (IS_ALIVE(ent))){ + ent->client->uvTime = uvtime; + } + } +} + +void EspPunishment(int teamNum) +{ + // Only perform team punishments if there's only 2 teams + if (esp->value && teamCount == 2){ + if(esp_punish->value == 1){ + // Immediately kill all losing members of the remaining team + KillEveryone(teamNum); + } else if (esp_punish->value == 2){ + // Grant uv shield to winning team + int uvtime = 60; + MakeTeamInvulnerable(OtherTeam(teamNum), uvtime); + } + } +} + +/* +This adds a medkit to the player's inventory, up to the medkit_max value +Passing a true parameter instantly provides this medkit +Passing a false parameter assumes you want to generate one at a time interval set by medkit_time +*/ +void GenerateMedKit(qboolean instant) +{ + int i = 0; + edict_t *ent; + int roundseconds = current_round_length / 10; + int interval = (int)medkit_time->value; + int max_kits = (int)medkit_max->value; + + // Do nothing if the ent already has max medkits + for (i = 0; i < game.maxclients; i++) { + ent = &g_edicts[1 + i]; + if (IS_LEADER(ent)) { + if (ent->client->medkit >= max_kits) + return; + else if (roundseconds - ent->client->resp.medkit_award_time >= interval) { + ent->client->resp.medkit_award_time = roundseconds; + ent->client->medkit++; + + if (esp_debug->value){ + gi.dprintf("%s: I generated a medkit for %s, %d seconds since the round started, %d frames in\n", __FUNCTION__, ent->client->pers.netname, roundseconds, roundseconds); + gi.dprintf("%s: %s has %d medkits now\n", __FUNCTION__, ent->client->pers.netname, ent->client->medkit); + } + } + } + } +} + +void EspSetupStatusbar( void ) +{ + Q_strncatz(level.statusbar, + // Respawn indicator + "yb -220 " "if 9 xr -28 pic 9 endif " "xr -100 num 4 10 ", + sizeof(level.statusbar) ); + + Q_strncatz(level.statusbar, + // Red Team + "yb -172 " "if 24 xr -32 pic 24 endif " "xr -100 num 4 26 " + // Blue Team + "yb -140 " "if 25 xr -32 pic 25 endif " "xr -100 num 4 27 ", + sizeof(level.statusbar) ); + + if( teamCount >= 3 ) + { + Q_strncatz(level.statusbar, + // Green Team + "yb -108 " "if 30 xr -32 pic 30 endif " "xr -100 num 4 31 ", + sizeof(level.statusbar) ); + } +} + +void EspAnnounceDetails( qboolean timewarning ) +{ + int i; + edict_t *ent; + + // This is warning the players that the round is about to end + // and they need to accomplish their goals + if (timewarning){ + for (i = 0; i < game.maxclients; i++){ + ent = g_edicts + 1 + i; + if (!ent->inuse || ent->is_bot || ent->client->resp.team == NOTEAM) + continue; + if (atl->value){ + CenterPrintAll("You're running low on time! Kill the enemy leader!\n"); + } else if (etv->value){ + CenterPrintTeam(TEAM1, "Capture that briefcase or the other team wins!\n"); + CenterPrintTeam(TEAM2, "Keep it up! If they can't cap, they can't win!\n"); + } + } + } + + // This is used at the beginning of the round to tell players what to do + if (!timewarning) { + for (i = 0; i < game.maxclients; i++){ + ent = g_edicts + 1 + i; + if (!ent->inuse) + continue; + if (IS_LEADER(ent)){ + //gi.sound(ent, CHAN_VOICE, gi.soundindex("aqdt/leader.wav"), 1, ATTN_STATIC, 0); + unicastSound(ent, gi.soundindex("aqdt/leader.wav"), 1.0); + gi.cprintf(ent, PRINT_MEDIUM, "Take cover, you're the leader!\n"); + } + if (!IS_LEADER(ent)) { + if (atl->value){ + gi.cprintf(ent, PRINT_MEDIUM, "Defend your leader and attack the other one to win!\n"); + } else if (etv->value){ + if (ent->client->resp.team == TEAM1) + gi.cprintf(ent, PRINT_HIGH, "Escort your leader to the briefcase!\n"); + else + gi.cprintf(ent, PRINT_HIGH, "Kill the enemy leader to win!\n"); + } + } + } + } +} + +/* +Because some rounds continue for a while, let's clean up the bodies periodically +*/ +int cleanupInterval = 20; // Initial value +void EspCleanUp(void) +{ + int roundseconds = current_round_length / 10; + int intervalAdd = 20; + + if (esp_debug->value) + gi.dprintf("%s: Cleanupinterval is %d, the time is now %d\n", __FUNCTION__, cleanupInterval, roundseconds); + if (roundseconds >= cleanupInterval) { + CleanBodies(); + cleanupInterval = cleanupInterval + intervalAdd; + + if (esp_debug->value) + gi.dprintf("%s: Bodies cleaned, cleanupinterval is now %d\n", __FUNCTION__, cleanupInterval); + } + //gi.dprintf("the time is now %d\n", roundseconds); + +} + +/* +Call this at the end of the round to get us back to a good state +*/ +void EspEndOfRoundCleanup(void) +{ + int i = 0; + // Reset leader_dead for all teams before next round starts + // as well as any bots that were leaders, somehow + for (i = TEAM1; i <= teamCount; i++) { + if (teams[i].leader && teams[i].leader->is_bot) + teams[i].leader = NULL; + teams[i].leader_dead = false; + } + + // Check that we have leaders for the next round + EspLeaderCheck(); + + // Reset cleanup interval + cleanupInterval = 20; + + // Reset the last spawnpoint index, this is reset to + // -1 because 0 is a valid index + for (i = TEAM1; i <= teamCount; i++) { + esp_spawnpoint_index[i] = -1; + } + + // Reset these three timed stats for the next round for each player + edict_t *ent; + for (i = 0; i < game.maxclients; i++) { + ent = g_edicts + 1 + i; + if (!ent->inuse) + continue; + ent->client->resp.esp_lastprotectcap = 0; + ent->client->resp.esp_lastprotectleader = 0; + ent->client->resp.esp_lasthurtleader = 0; + } + + /* + Note: Resetting the ETV escort point is not done here, + it is performed in EspResetCapturePoint() and MUST be called at + the _beginning_ of the round, not the end. Calling it at the + end of the round has the possibility of recapturing the point + after the round has ended but before the next round begins + */ +} + +/* + +*/ +int EspSpawnpointCount(int teamNum) +{ + espsettings_t *es = &espsettings; + int i; + int spawn_count = 0; + + // Get count of how many team spawns there are in espsettings.customspawns + for (i = 0; i < MAX_SPAWNS; i++) { + if (es->custom_spawns[teamNum][i]) { + if (es->custom_spawns[teamNum][i] == NULL) + break; + if (esp_debug->value) + gi.dprintf("%s: Team %d spawncount %d coordinates are: %f, %f, %f\n", __FUNCTION__, teamNum, i, es->custom_spawns[teamNum][i]->s.origin[0], es->custom_spawns[teamNum][i]->s.origin[1], es->custom_spawns[teamNum][i]->s.origin[2]); + spawn_count++; + } + } + + if (esp_debug->value) + gi.dprintf("%s: Team %d has %d custom spawnpoints\n", __FUNCTION__, teamNum, spawn_count); + return spawn_count; +} + +void EspDebug(void) +{ + // Debugging: + int i; + for (i = TEAM1; i <= teamCount; i++) { + + if (HAVE_LEADER(i)) { + gi.dprintf("%s: Team %i leader: %s\n", __FUNCTION__, i, teams[i].leader->client->pers.netname); + } else { + gi.dprintf("%s: Team %d does not have a leader\n", __FUNCTION__, i); + } + } + edict_t *ent; + int entcount = 0; + for (i = 0; i < game.maxclients; i++) { + ent = g_edicts + 1 + i; + if (!ent->inuse || !ent->client) + continue; + if (ent->client->resp.is_volunteer) { + gi.dprintf("%s is a volunteer for team %i\n", ent->client->pers.netname, ent->client->resp.team); + } + if (ent->client->resp.team == TEAM1){ + gi.dprintf("%s is on team 1\n", ent->client->pers.netname); + entcount++; + } + if (ent->client->resp.team == TEAM2){ + gi.dprintf("%s is on team 2\n", ent->client->pers.netname); + entcount++; + } + if (ent->client->resp.team == NOTEAM){ + gi.dprintf("%s is not on a team\n", ent->client->pers.netname); + entcount++; + } + } + gi.dprintf("There are %d players\n", entcount); + + // Respawn timers + for (i = TEAM1; i <= teamCount; i++) { + gi.dprintf("Spawn timer for team %d: %d\n", i, teams[i].respawn_timer); + } + + for (i = 0; i < game.maxclients; i++) { + ent = g_edicts + 1 + i; + if (!ent->inuse || !ent->client) + continue; + if (ent->client->resp.team == TEAM1){ + gi.dprintf("%s has %d frames left to respawn\n", ent->client->pers.netname, ent->client->respawn_framenum - level.realFramenum); + } + if (ent->client->resp.team == TEAM2){ + gi.dprintf("%s has %d frames left to respawn\n", ent->client->pers.netname, ent->client->respawn_framenum - level.realFramenum); + } + } +} \ No newline at end of file diff --git a/src/action/a_esp.h b/src/action/a_esp.h new file mode 100644 index 000000000..d23eb462d --- /dev/null +++ b/src/action/a_esp.h @@ -0,0 +1,129 @@ +// This is set to 1 if either atl or etv are 1 +extern cvar_t *esp; + +// Discrete game modes +extern cvar_t *atl; +extern cvar_t *etv; + +#define IS_LEADER(ent) (teams[(ent)->client->resp.team].leader == (ent)) +#define HAVE_LEADER(teamNum) (teams[(teamNum)].leader) +#define MAX_ESP_STRLEN 32 + +// Game modes +#define ESPMODE_ATL 0 +#define ESPMODE_ETV 1 + +#define ESPMODE_ATL_NAME "Assassinate the Leader" +#define ESPMODE_ETV_NAME "Escort the VIP" + +#define ESPMODE_ATL_SNAME "atl" +#define ESPMODE_ETV_SNAME "etv" + + +// Game default settings +#define ESP_DEFAULT_RESPAWN_TIME 10 +#define ESP_RED_SKIN "male/ctf_r" +#define ESP_BLUE_SKIN "male/ctf_b" +#define ESP_GREEN_SKIN "male/ctf_g" +#define ESP_RED_LEADER_SKIN "male/babarracuda" +#define ESP_BLUE_LEADER_SKIN "male/blues" +#define ESP_GREEN_LEADER_SKIN "male/hulk2" +#define ESP_RED_TEAM "The B-Team" +#define ESP_BLUE_TEAM "Mall Cops" +#define ESP_GREEN_TEAM "Mop-up Crew" +#define ESP_RED_LEADER_NAME "B. A. Barracuda" +#define ESP_BLUE_LEADER_NAME "Frank the Cop" +#define ESP_GREEN_LEADER_NAME "The Incredible Chulk" + +int EspCapturePointOwner( edict_t *flag ); +void EspRemember( const edict_t *ent, const gitem_t *item ); +qboolean EspLoadConfig( const char *mapname ); +void EspSetupStatusbar( void ); +void SetEspStats( edict_t *ent ); + +typedef enum +{ + ESP_STATE_START, + ESP_STATE_PLAYING +} +espstate_t; + +typedef struct espsettings_s +{ + char author[MAX_ESP_STRLEN*3]; + char name[MAX_ESP_STRLEN]; + edict_t *custom_spawns[TEAM_TOP][MAX_SPAWNS]; + qboolean custom_skins; + int halftime; + qboolean escortcap; + char target_name[MAX_ESP_STRLEN]; + edict_t *capturepoint; + edict_t *lastkilledleader; +} espsettings_t; + +extern espsettings_t espsettings; + +extern gitem_t *team_flag[TEAM_TOP]; + +typedef enum { + ESP_HALFTIME_WARNING, + ESP_LEADER_DIED, + MESSAGE_THREE, + // Add more messages as needed + NUM_MESSAGES +} EspTimedMessageIndex; + +#define DF_ESP_FORCEJOIN 131072 + +// Team score bonuses +#define TS_TEAM_BONUS 1 // this is the bonus point teams get for fragging enemy leader + +// Individual score bonuses +#define ESP_LEADER_FRAG_BONUS 5 // points player receives for fragging enemy leader +#define ESP_LEADER_CAPTURE_BONUS 5 // points player receives if they are leader and they successfully touch escort marker +#define ESP_LEADER_ESCORT_BONUS 2 // points player receives if they are not the leader and they are near the leader when point is captured +#define ESP_LEADER_DANGER_PROTECT_BONUS 2 // bonus for fragging someone who has recently hurt your leader +#define ESP_LEADER_PROTECT_BONUS 1 // bonus for fragging someone while either you or your target are near your leader +#define ESP_FLAG_DEFENSE_BONUS 1 // bonus for fragging someone while either you or your target are near your flag +#define ESP_LEADER_HARASS_BONUS 2 // points for attacking defenders of the leader + +// Score bonus critieria/limitations +#define ESP_TARGET_PROTECT_RADIUS 400 // the radius around an object being defended where a target will be worth extra frags +#define ESP_ATTACKER_PROTECT_RADIUS 400 // the radius around an object being defended where an attacker will get extra frags when making kills +#define ESP_ATTACKER_HARASS_RADIUS 400 // the radius around an object being defended where an attacker will get extra frags when assaulting the leader +#define ESP_BONUS_COOLDOWN 500 // the number of frames after a bonus is awarded before another bonus can be awarded + +void EspForceEspionage(int espmode); +void EspSetTeamSpawns(int, char *); +int EspGetRespawnTime(edict_t *ent); + +void Cmd_Volunteer_f(edict_t * ent); +qboolean EspSetLeader(int teamNum, edict_t *ent); +qboolean AllTeamsHaveLeaders(void); +void EspLeaderLeftTeam( edict_t *ent ); +void EspPunishment(int teamNum); +void EspRespawnPlayer(edict_t *ent); + +edict_t *SelectEspSpawnPoint (edict_t * ent); +int EspReportLeaderDeath(edict_t *ent); +int EspSpawnpointCount(int teamNum); +void EspResetCapturePoint( void ); +void GenerateMedKit(qboolean instant); + +void EspTouchCapturePoint( edict_t *marker, edict_t *player, cplane_t *plane, csurface_t *surf ); +void EspScoreBonuses(edict_t * targ, edict_t * attacker); + +qboolean EspCheckRules (void); +qboolean HasFlag (edict_t * ent); + +void EspSkinCheck(void); +void ResetPlayers (void); +void GetEspScores(int *t1score, int *t2score); +void EspCapReward(edict_t *); +void EspAnnounceDetails( qboolean timewarning ); +qboolean EspChooseRandomLeader(int teamNum); +qboolean EspLeaderCheck(void); +void EspEndOfRoundCleanup(void); +void EspRespawnLCA(edict_t *ent); +void EspCleanUp(void); +void EspDebug(void); \ No newline at end of file diff --git a/src/action/a_game.c b/src/action/a_game.c index 231aa96eb..fd06e57b7 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -215,6 +215,97 @@ void ReadMOTDFile() fclose(motd_file); } +void _PrintGameMsgToClient(char* msg, edict_t* ent) +{ + if (!auto_menu->value || ent->client->pers.menu_shown) { + gi.centerprintf(ent, "%s", msg); + } else { + gi.cprintf(ent, PRINT_LOW, "%s", msg); + } +} + +/* +Take great care with this function. It will continuously print +a message to players after the MOTD, so it should be something +that has a condition where it would stop printing. Otherwise, +you'll have a lot of pissed off players who will complain about +text on their screen. + +Only return true if conditions are met, else return false. +*/ + +qboolean PrintGameMessage(edict_t *ent) +{ + /* + Each condition is checked in order, and the first one that is true + will be the message that is sent. If none are true, then no message + will be sent (return false) + + Always always ALWAYS remember to add + msg_ready = true; + if you add a new message, else the condition check will not work and the + function will return false. + */ + + char msg_buf[1024]; + qboolean msg_ready = false; + + /* + This message is printed before a game starts in Espionage, and tells players what they need to do + in order to begin the match. Once both teams have leaders, this message will no longer be printed. + */ + if (esp->value) { + if (!team_round_going && !AllTeamsHaveLeaders()) { + if (atl->value) { + Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for each team to have a leader\nType 'leader' in console to volunteer for duty.\n"); + msg_ready = true; + } else if (etv->value) { + if (ent->client->resp.team == TEAM1) + Q_snprintf(msg_buf, sizeof(msg_buf), "Your team needs a leader!\nType 'leader' in console to volunteer for duty."); + else + Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for team 1 to have a leader...\n"); + msg_ready = true; + } + } + } + + // **** + /* Add all other messages before this one */ + // **** + /* + This should be the last message to be evaluated, and will only be printed if the match is about to start. It will + present the game rules to the players for a given game type during the countdown, and disappear when the countdown ends + */ + + if (!team_game_going && (team_round_countdown > 20 && team_round_countdown < 101)) { + char* matchRules = PrintMatchRules(); + if (matchRules != NULL && matchRules[0] != '\0') { + Q_snprintf(msg_buf, sizeof(msg_buf), "%s", matchRules); + msg_ready = true; + } + } + + // ----- No more messages after this point ----- // + if (msg_ready) { + _PrintGameMsgToClient(msg_buf, ent); + return true; + } + // By default, return false to stop printing + return false; +} + +void Cmd_PrintRules_f(edict_t *ent) +{ + char msg_buf[1024]; + + char* matchRules = PrintMatchRules(); + if (matchRules != NULL && matchRules[0] != '\0') { + Q_snprintf(msg_buf, sizeof(msg_buf), "%s", matchRules); + } + + _PrintGameMsgToClient(msg_buf, ent); +} + // AQ2:TNG Deathwatch - Ohh, lovely MOTD - edited it void PrintMOTD(edict_t * ent) { @@ -224,7 +315,7 @@ void PrintMOTD(edict_t * ent) //Welcome Message. This shows the Version Number and website URL, followed by an empty line - strcpy(msg_buf, TNG_TITLE " v" VERSION "\n" "http://aq2-tng.sourceforge.net/" "\n\n"); + strcpy(msg_buf, TNG_TITLE " v" VERSION "\n" TNG_WEBSITE " -- " AQ2_DISCORD "\n"); lines = 3; /* @@ -262,6 +353,10 @@ void PrintMOTD(edict_t * ent) else server_type = "Team Deathmatch"; } + else if (esp->value) // Is it Espionage? + { + server_type = "Espionage"; + } else if (use_tourney->value) // Is it Tourney? server_type = "Tourney"; else // No? Then it must be Teamplay @@ -345,6 +440,58 @@ void PrintMOTD(edict_t * ent) } } + /* new Espionage settings added here for better readability */ + if(esp->value) { + strcat(msg_buf, "\n"); + lines++; + + if(atl->value) + sprintf(msg_buf + strlen(msg_buf), "Espionage Mode: Assassinate the Leader\n"); + else if(etv->value) + sprintf(msg_buf + strlen(msg_buf), "Espionage Mode: Escort the VIP\n"); + else + strcat(msg_buf, "\n"); + lines++; + + if(teams[TEAM1].respawn_timer > -1 || teams[TEAM2].respawn_timer > -1 || teams[TEAM3].respawn_timer > -1) { + sprintf(msg_buf + strlen(msg_buf), "Spawn times:\n"); + if(teams[TEAM1].respawn_timer > -1) + sprintf(msg_buf + strlen(msg_buf), "%s: %ds\n", teams[TEAM1].name, teams[TEAM1].respawn_timer); + if(teams[TEAM2].respawn_timer > -1) + sprintf(msg_buf + strlen(msg_buf), "%s: %ds\n", teams[TEAM2].name, teams[TEAM2].respawn_timer); + if(use_3teams->value){ + if(teams[TEAM3].respawn_timer > -1) + sprintf(msg_buf + strlen(msg_buf), "%s: %ds\n", teams[TEAM3].name, teams[TEAM3].respawn_timer); + } + strcat(msg_buf, "\n"); + lines++; + } + + qboolean espcspawns = false; + if (espsettings.custom_spawns[0] != NULL) + espcspawns = true; + sprintf(msg_buf + strlen(msg_buf), "Using %s spawns\n", + (espcspawns ? "CUSTOM" : "ORIGINAL")); + lines++; + + if(strlen(espsettings.author) > 0) { + strcat(msg_buf, "\n"); + lines++; + + sprintf(msg_buf + strlen(msg_buf), "Espionage configuration by %s\n", + espsettings.author); + lines++; + + /* no comment without author, grr */ + if(strlen(espsettings.name) > 0) { + /* max line length is 39 chars + new line */ + Q_strncatz(msg_buf + strlen(msg_buf), espsettings.name, 39); + strcat(msg_buf, "\n"); + lines++; + } + } + } + /* Darkmatch */ @@ -383,10 +530,19 @@ void PrintMOTD(edict_t * ent) else strcat(msg_buf, "Roundlimit: none"); - if ((int)roundtimelimit->value) // What is the roundtimelimit? - sprintf(msg_buf + strlen(msg_buf), " Roundtimelimit: %d\n", (int)roundtimelimit->value); - else - strcat(msg_buf, " Roundtimelimit: none\n"); + if (!esp->value) { // No Roundtimelimits on Espionage + if ((int)roundtimelimit->value) // What is the roundtimelimit? + sprintf(msg_buf + strlen(msg_buf), " Roundtimelimit: %d\n", (int)roundtimelimit->value); + else + strcat(msg_buf, " Roundtimelimit: none\n"); + } + + if (esp->value && etv->value) { + if ((int) capturelimit->value) // What is the capturelimit? + sprintf(msg_buf + strlen(msg_buf), " Capturelimit: %d\n", (int) capturelimit->value); + else + strcat(msg_buf, " Capturelimit: none\n"); + } lines++; } else if (ctf->value) // If we're in CTF, we want to know the capturelimit @@ -417,7 +573,7 @@ void PrintMOTD(edict_t * ent) if (tgren->value > 0) sprintf(grenade_num, "%d grenade%s", (int)tgren->value, (int)tgren->value == 1 ? "" : "s"); - sprintf(msg_buf + strlen(msg_buf), "Bandolier w/ %s%s%s\n", + sprintf(msg_buf + strlen(msg_buf), " Bandolier w/ %s%s%s\n", !(ir->value) ? "no IR" : "", (tgren->value > 0 && !(ir->value)) ? " & " : "", tgren->value > 0 ? grenade_num : ""); diff --git a/src/action/a_game.h b/src/action/a_game.h index 78e43a18e..10b820490 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -86,9 +86,11 @@ // AQ2:TNG Deathwatch - Updated the Version variables to show TNG Stuff #ifndef VERSION -#define VERSION "2.82 git" +#define VERSION "2.83 git" #endif #define TNG_TITLE "AQ2: The Next Generation" +#define TNG_WEBSITE "https://www.aq2world.com" +#define AQ2_DISCORD "https://discord.aq2world.com" // AQ2:TNG Deathwatch End //AQ2:TNG Slicer This is the max players writen on last killed target //SLIC2 @@ -101,6 +103,7 @@ extern int num_maps, cur_map, rand_map, num_allvotes; // num_allvotes added by I void ReadConfigFile (void); void ReadMOTDFile (void); void PrintMOTD (edict_t *ent); +qboolean PrintGameMessage(edict_t *ent); void stuffcmd (edict_t *ent, char *s); void unicastSound(edict_t *ent, int soundIndex, float volume); @@ -144,3 +147,4 @@ void MakeAllLivePlayersObservers( void ); //a_cmds.c void Cmd_NextMap_f( edict_t * ent ); +void Cmd_PrintRules_f(edict_t *ent); diff --git a/src/action/a_items.c b/src/action/a_items.c index 6973a7747..3a53f3c66 100644 --- a/src/action/a_items.c +++ b/src/action/a_items.c @@ -135,23 +135,22 @@ void SpecThink(edict_t * spec) static void MakeTouchSpecThink(edict_t * ent) { - ent->touch = Touch_Item; if (allitem->value) { - ent->nextthink = level.framenum + 1 * HZ; + ent->nextthink = eztimer(1); ent->think = G_FreeEdict; return; } if (gameSettings & GS_ROUNDBASED) { - ent->nextthink = level.framenum + 60 * HZ; //FIXME: should this be roundtime left + ent->nextthink = eztimer(60); //FIXME: should this be roundtime left ent->think = G_FreeEdict; return; } if (gameSettings & GS_WEAPONCHOOSE) { - ent->nextthink = level.framenum + 6 * HZ; + ent->nextthink = eztimer(6); ent->think = G_FreeEdict; return; } diff --git a/src/action/a_match.c b/src/action/a_match.c index 436a1d0f8..b79363292 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -85,16 +85,21 @@ void SendScores(void) } gi.bprintf(PRINT_HIGH, "Match is over, waiting for next map, please vote a new one..\n"); - #if USE_AQTION + #ifdef USE_AQTION // Needed to add this here because Matchmode does not call BeginIntermission, but other teamplay modes do call it if (stat_logs->value) { - LogMatch(); // Generates end of match logs + LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match stats } #endif // Stats: Reset roundNum game.roundNum = 0; // Stats end + + #ifndef NO_BOTS + // Clear LTK bot names + LTKClearBotNames(); + #endif } void Cmd_Sub_f(edict_t * ent) @@ -140,6 +145,9 @@ void MM_SetCaptain( int teamNum, edict_t *ent ) ent = NULL; teams[teamNum].captain = ent; + if (esp->value) { + EspSetLeader(teamNum, ent); + } if (!ent) { if (!team_round_going || (gameSettings & GS_ROUNDBASED)) { if (teams[teamNum].ready) { @@ -157,6 +165,8 @@ void MM_SetCaptain( int teamNum, edict_t *ent ) } if (ent != oldCaptain) { + if (mm_captain_teamname->value) + Cmd_Teamname_f(ent); gi.bprintf( PRINT_HIGH, "%s is now %s's captain\n", ent->client->pers.netname, teams[teamNum].name ); gi.cprintf( ent, PRINT_CHAT, "You are the captain of '%s'\n", teams[teamNum].name ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex( "misc/comp_up.wav" ), 1.0, ATTN_NONE, 0.0 ); @@ -166,6 +176,8 @@ void MM_SetCaptain( int teamNum, edict_t *ent ) gi.cprintf( ent, PRINT_HIGH, "Team %i wants to reset scores, type 'resetscores' to accept\n", i ); } } + // Change the name of the team if enabled + } void MM_LeftTeam( edict_t *ent ) @@ -189,15 +201,69 @@ qboolean TeamsReady( void ) else if( TeamHasPlayers(i) ) return false; } - return (ready >= 2); } +void MM_CaptainLeader(edict_t * ent) +{ + int teamNum; + edict_t *oldLeader; + + // Ignore if not Espionage mode + if (!esp->value) { + gi.cprintf(ent, PRINT_HIGH, "This command needs Espionage to be enabled\n"); + return; + } + + // Ignore entity if not on a team + teamNum = ent->client->resp.team; + if (teamNum == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "You need to be on a team for that...\n"); + return; + } + + // Ignore entity if they are a sub + if (ent->client->resp.subteam == teamNum) { + gi.cprintf(ent, PRINT_HIGH, "Subs cannot be leaders...\n"); + return; + } + + // If the current leader is issuing this command again, remove them as leader + oldLeader = teams[teamNum].leader; + if (oldLeader == ent) { + if (team_round_going || lights_camera_action > 0) { + gi.cprintf(ent, PRINT_HIGH, "You cannot resign as leader while a round is in progress\n"); + return; + } + EspSetLeader( teamNum, NULL ); + // This is the last time we know this entity was the leader, so do some cleanup first + oldLeader->client->resp.is_volunteer = false; + oldLeader->client->resp.esp_leadertime = level.realFramenum; + return; + } + + // If the team already has a leader, send this message to the ent volunteering + if (oldLeader) { + gi.cprintf( ent, PRINT_HIGH, "Your team already has a leader (%s)\nYou are now volunteering for duty should he fall\n", + teams[teamNum].leader->client->pers.netname ); + ent->client->resp.is_volunteer = true; + return; + } + + EspSetLeader( teamNum, ent ); +} + void Cmd_Captain_f(edict_t * ent) { int teamNum; edict_t *oldCaptain; + // Aliases `captain` command to `volunteer` if Espionage is enabled + if (esp->value && !matchmode->value) { + MM_CaptainLeader(ent); + return; + } + if (!matchmode->value) { gi.cprintf(ent, PRINT_HIGH, "This command needs matchmode to be enabled\n"); return; @@ -212,6 +278,9 @@ void Cmd_Captain_f(edict_t * ent) oldCaptain = teams[teamNum].captain; if (oldCaptain == ent) { MM_SetCaptain( teamNum, NULL ); + if (esp->value) { + EspSetLeader(teamNum, NULL); + } return; } @@ -221,6 +290,9 @@ void Cmd_Captain_f(edict_t * ent) } MM_SetCaptain( teamNum, ent ); + if (esp->value) { + EspSetLeader( teamNum, ent ); + } } //extern int started; // AQ2:M - Matchmode - Used for ready command @@ -272,7 +344,12 @@ void Cmd_Teamname_f(edict_t * ent) } if(ctf->value) { - gi.cprintf(ent, PRINT_HIGH, "You can't change teamnames in ctf mode\n"); + gi.cprintf(ent, PRINT_HIGH, "You can't change teamnames in CTF mode\n"); + return; + } + + if(esp->value) { + gi.cprintf(ent, PRINT_HIGH, "You can't change teamnames in Espionage mode\n"); return; } @@ -298,18 +375,22 @@ void Cmd_Teamname_f(edict_t * ent) return; } - argc = gi.argc(); - if (argc < 2) { - gi.cprintf( ent, PRINT_HIGH, "Your team name is %s\n", team->name ); - return; - } - - Q_strncpyz(temp, gi.argv(1), sizeof(temp)); - for (i = 2; i < argc; i++) { - Q_strncatz(temp, " ", sizeof(temp)); - Q_strncatz(temp, gi.argv(i), sizeof(temp)); + if (mm_captain_teamname->value){ + snprintf(temp, sizeof(temp), "Team %s", ent->client->pers.netname); + temp[23] = '\0'; // Ensure that the team name is not too long + } else { + argc = gi.argc(); + if (argc < 2) { + gi.cprintf( ent, PRINT_HIGH, "Your team name is %s\n", team->name ); + return; + } + Q_strncpyz(temp, gi.argv(1), sizeof(temp)); + for (i = 2; i < argc; i++) { + Q_strncatz(temp, " ", sizeof(temp)); + Q_strncatz(temp, gi.argv(i), sizeof(temp)); + } + temp[18] = 0; } - temp[18] = 0; if (!temp[0]) strcpy( temp, "noname" ); @@ -328,6 +409,11 @@ void Cmd_Teamskin_f(edict_t * ent) team_t *team; edict_t *e; + if (!esp->value) { + gi.cprintf(ent, PRINT_HIGH, "Espionage skins are set in the .esp file\n"); + return; + } + if (!matchmode->value) { gi.cprintf(ent, PRINT_HIGH, "This command needs matchmode to be enabled\n"); return; @@ -632,3 +718,32 @@ void Cmd_TogglePause_f(edict_t * ent, qboolean pause) } } +/* +Sets default values for Espionage Matchmode +*/ +void MM_EspDefaultSettings(void) +{ + if (!matchmode->value || !esp->value) + return; + + /* + + Enforced settings for official matches + + * Normal Slippers + * No team punishment + * Leader gets 1 weapon and all items + * Leader is enhanced and receives a maximum of 1 healthkit + + */ + + gi.cvar_forceset("esp_enhancedslippers", "0"); + gi.cvar_forceset("esp_punish", "0"); + gi.cvar_forceset("esp_leaderequip", "1"); + gi.cvar_forceset("esp_leaderenhance", "1"); + gi.cvar_forceset("medkit_max", "1"); + gi.cvar_forceset("medkit_value", "25"); + gi.cvar_forceset("esp_respawn_uvtime", "10"); + gi.cvar_forceset("timelimit", "20"); + gi.dprintf("** Espionage default matchmode settings enforced **\n"); +} \ No newline at end of file diff --git a/src/action/a_match.h b/src/action/a_match.h index f8afd4aa0..49fb6aaf9 100644 --- a/src/action/a_match.h +++ b/src/action/a_match.h @@ -39,3 +39,4 @@ int CheckForCaptains (int cteam); void Cmd_SetAdmin_f (edict_t * ent); void Cmd_TogglePause_f(edict_t * ent, qboolean pause); void Cmd_ResetScores_f(edict_t * ent); +void MM_EspDefaultSettings(void); \ No newline at end of file diff --git a/src/action/a_team.c b/src/action/a_team.c index 9e051727e..5b22b0aa3 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -324,7 +324,7 @@ team_t teams[TEAM_TOP]; int teamCount = 2; int gameSettings; -#define MAX_SPAWNS 512 // max DM spawn points supported +//#define MAX_SPAWNS 512 // moved to g_local.h edict_t *potential_spawns[MAX_SPAWNS]; int num_potential_spawns; @@ -422,6 +422,96 @@ void ReprintMOTD (edict_t * ent, pmenu_t * p) PrintMOTD (ent); } +char* PrintMatchRules(void) +{ + static char rulesmsg[1024]; + + // Espionage rules + if (esp->value) { + int rndlimit = (int)roundlimit->value; + if (atl->value) { + if (!teams[TEAM1].leader && !teams[TEAM2].leader && !teams[TEAM3].leader) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "Frag the other team's leader to win, but don't forget to protect your own!\n"); + } + else if (teamCount == TEAM2) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s leader: %s (%s)\n\n%s leader: %s (%s)\n\nFrag the other team's leader to win, but don't forget to protect your own!\n", + teams[TEAM1].name, teams[TEAM1].leader->client->pers.netname, teams[TEAM1].leader_name, + teams[TEAM2].name, teams[TEAM2].leader->client->pers.netname, teams[TEAM2].leader_name ); + } else { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s leader: %s (%s)\n\n%s leader: %s (%s)\n\n%s leader: %s (%s)\n\nFrag the other team's leaders to win, but don't forget to protect your own!\n", + teams[TEAM1].name, teams[TEAM1].leader->client->pers.netname, teams[TEAM1].leader_name, + teams[TEAM2].name, teams[TEAM2].leader->client->pers.netname, teams[TEAM2].leader_name, + teams[TEAM3].name, teams[TEAM3].leader->client->pers.netname, teams[TEAM3].leader_name ); + } + // Roundlimit info + static char addmsg[64]; + Q_snprintf(addmsg, sizeof(addmsg), "\n** The first team to %i points wins! **", (int)rndlimit); + Q_strncatz( rulesmsg, addmsg, sizeof( rulesmsg ) ); + + } else if (etv->value) { + if (!teams[TEAM1].leader) + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "\nTeam 1 is trying to escort their leader to the briefcase!\n\nTeam 2 is tasked with preventing that from happening!\n"); + else if (teams[TEAM1].leader) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "\n%s: Escort your leader %s to the %s! Don't get them killed!\n\n%s: DO NOT let %s get to the %s! Use lethal force!\n\nTeam 1 Respawn Timer: %i seconds\nTeam 2 Respawn Timer: %i seconds\n\nThe first team to %i points wins!\n", + teams[TEAM1].name, teams[TEAM1].leader->client->pers.netname, espsettings.target_name, + teams[TEAM2].name, teams[TEAM1].leader->client->pers.netname, espsettings.target_name, + (int)teams[TEAM1].respawn_timer, (int)teams[TEAM2].respawn_timer, + rndlimit ); + // Append a little extra if halftime is enabled + if(esp_etv_halftime->value && roundlimit->value > 3){ + static char addmsg[64]; + Q_snprintf(addmsg, sizeof(addmsg), "\n** Halftime is enabled: Teams switch at round %i **", (int)rndlimit/2); + Q_strncatz( rulesmsg, addmsg, sizeof( rulesmsg ) ); + } + } + } + } + // CTF rules + else if (ctf->value) + { + if (!capturelimit->value) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\nNo capturelimit set!\n", + teams[TEAM1].name, teams[TEAM2].name ); + } else { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\nThe first team to %s captures wins!\n", + teams[TEAM1].name, teams[TEAM2].name, capturelimit->string ); + } + } + // Domination rules + else if (dom->value) + { + if (teamCount == TEAM2) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture all of the checkpoints!\n", + teams[TEAM1].name, teams[TEAM2].name ); + } else { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\nvs\n%s\n\nCapture all of the checkpoints!\n", + teams[TEAM1].name, teams[TEAM2].name, teams[TEAM3].name ); + } + } + // This covers both Teamplay and TeamDM + else if (teamplay->value) + { + if (teamCount == TEAM2) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nFrag the other team!\n", + teams[TEAM1].name, teams[TEAM2].name ); + } else if (teamCount == TEAM3) { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\nvs\n%s\n\nFrag the other teams!\n", + teams[TEAM1].name, teams[TEAM2].name, teams[TEAM3].name ); + } + } + else // If nothing else matches, just say glhf + { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "Frag 'em all! Good luck and have fun!\n"); + } + + if (rulesmsg[0] == '\0') { + // Handle empty + return ""; + } + + return rulesmsg; +} + void JoinTeamAuto (edict_t * ent, pmenu_t * p) { int i, team = TEAM1, num1 = 0, num2 = 0, num3 = 0, score1, score2, score3; @@ -468,6 +558,11 @@ void JoinTeamAuto (edict_t * ent, pmenu_t * p) JoinTeam(ent, team, 0); } +void JoinTeamAutobalance (edict_t * ent) +{ + JoinTeamAuto(ent, NULL); +} + void JoinTeam1 (edict_t * ent, pmenu_t * p) { JoinTeam(ent, TEAM1, 0); @@ -495,7 +590,11 @@ void SelectWeapon2(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(MP5_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/mp5slide.wav"), 1.0); } @@ -503,7 +602,11 @@ void SelectWeapon3(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(M3_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/m3in.wav"), 1.0); } @@ -511,7 +614,11 @@ void SelectWeapon4(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(HC_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/cclose.wav"), 1.0); } @@ -519,7 +626,11 @@ void SelectWeapon5(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(SNIPER_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/ssgbolt.wav"), 1.0); } @@ -527,7 +638,11 @@ void SelectWeapon6(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(M4_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/m4a1slide.wav"), 1.0); } @@ -535,7 +650,11 @@ void SelectWeapon0(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(KNIFE_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/swish.wav"), 1.0); } @@ -543,13 +662,21 @@ void SelectWeapon9(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenWeapon = GET_ITEM(DUAL_NUM); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } unicastSound(ent, gi.soundindex("weapons/mk23slide.wav"), 1.0); } void SelectItem1(edict_t *ent, pmenu_t *p) { ent->client->pers.chosenItem = GET_ITEM(KEV_NUM); + if(item_kit_mode->value){ + // This is so it clears the chosenItem2 if a previous kit was chosen + ent->client->pers.chosenItem2 = NULL; + } PMenu_Close(ent); unicastSound(ent, gi.soundindex("misc/veston.wav"), 1.0); } @@ -589,6 +716,36 @@ void SelectItem6(edict_t *ent, pmenu_t *p) unicastSound(ent, gi.soundindex("misc/veston.wav"), 1.0); } +// Commando kit +void SelectKit1(edict_t *ent, pmenu_t *p) +{ + ent->client->pers.chosenItem = GET_ITEM(BAND_NUM); + ent->client->pers.chosenItem2 = GET_ITEM(HELM_NUM); + + PMenu_Close(ent); + unicastSound(ent, gi.soundindex("misc/veston.wav"), 1.0); +} + +// Stealth kit +void SelectKit2(edict_t *ent, pmenu_t *p) +{ + ent->client->pers.chosenItem = GET_ITEM(SLIP_NUM); + ent->client->pers.chosenItem2= GET_ITEM(SIL_NUM); + + PMenu_Close(ent); + unicastSound(ent, gi.soundindex("misc/screw.wav"), 1.0); +} + +// Assassin kit +void SelectKit3(edict_t *ent, pmenu_t *p) +{ + ent->client->pers.chosenItem = GET_ITEM(LASER_NUM); + ent->client->pers.chosenItem2= GET_ITEM(SIL_NUM); + + PMenu_Close(ent); + unicastSound(ent, gi.soundindex("misc/lasersight.wav"), 1.0); +} + // newrand returns n, where 0 >= n < top int newrand (int top) { @@ -622,7 +779,11 @@ void SelectRandomWeapon(edict_t *ent, pmenu_t *p) unicastSound(ent, gi.soundindex(selected_weapon.sound), 1.0); gi.centerprintf(ent, "You selected %s", selected_weapon.name); PMenu_Close(ent); - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } } void SelectRandomItem(edict_t *ent, pmenu_t *p) @@ -683,6 +844,8 @@ void SelectRandomItem(edict_t *ent, pmenu_t *p) void SelectRandomWeaponAndItem(edict_t *ent, pmenu_t *p) { + int i; + int rand = newrand(7); // WEAPON menu_list_weapon weapon_list[7] = { { .num = MP5_NUM, .sound = "weapons/mp5slide.wav", .name = MP5_NAME }, @@ -694,7 +857,6 @@ void SelectRandomWeaponAndItem(edict_t *ent, pmenu_t *p) { .num = DUAL_NUM, .sound = "weapons/mk23slide.wav", .name = DUAL_NAME } }; - int rand = newrand(7); menu_list_weapon selected_weapon = weapon_list[rand]; // prevent picking current weapon if (ent->client->pers.chosenWeapon) { @@ -749,7 +911,7 @@ void SelectRandomWeaponAndItem(edict_t *ent, pmenu_t *p) } } - for (int i = 0; i < listCount; i++) { + for (i = 0; i < listCount; i++) { gi.cprintf(ent, PRINT_HIGH, "%i %s\n", item_list[i].num, item_list[i].name); } @@ -859,7 +1021,7 @@ pmenu_t weapmenu[] = { {"Return to Main Menu", PMENU_ALIGN_LEFT, NULL, CreditsReturnToMain}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, //AQ2:TNG END - {"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, + {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, @@ -882,7 +1044,26 @@ pmenu_t itemmenu[] = { {"Random Item", PMENU_ALIGN_LEFT, NULL, SelectRandomItem}, //AQ2:TNG end adding itm_flags {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, - {"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, + {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, + {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, + {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, + {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, + {"v" VERSION, PMENU_ALIGN_RIGHT, NULL, NULL}, +}; + +pmenu_t itemkitmenu[] = { + {"*" TNG_TITLE, PMENU_ALIGN_CENTER, NULL, NULL}, + {"\x9D\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9F", PMENU_ALIGN_CENTER, NULL, NULL}, + {"Select your Item", PMENU_ALIGN_CENTER, NULL, NULL}, + {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, + //AQ2:TNG Igor adding itm_flags + {KEV_NAME, PMENU_ALIGN_LEFT, NULL, NULL}, // "Kevlar Vest", SelectItem1 + {C_KIT_NAME, PMENU_ALIGN_LEFT, NULL, NULL}, // "Commando Kit", SelectKit1 + {S_KIT_NAME, PMENU_ALIGN_LEFT, NULL, NULL}, // "Stealth Kit", SelectKit2 + {A_KIT_NAME, PMENU_ALIGN_LEFT, NULL, NULL}, // "Assassin Kit", SelectKit3 + //AQ2:TNG end adding itm_flags + {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, + {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, @@ -901,13 +1082,48 @@ pmenu_t randmenu[] = { {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, {"Return to Main Menu", PMENU_ALIGN_LEFT, NULL, CreditsReturnToMain}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, - {"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, + {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, {"v" VERSION, PMENU_ALIGN_RIGHT, NULL, NULL}, }; +/* +PaTMaN's JMOD +*/ +void ToggleLaser(edict_t *ent, pmenu_t *p) +{ + Cmd_Toggle_f(ent, "laser"); +} + +void ToggleSlippers(edict_t *ent, pmenu_t *p) +{ + //strcpy(gi.args(),"togglecode slippers"); + Cmd_Toggle_f(ent, "slippers"); +} + + +//PaTMaN - Item Menu +pmenu_t pmitemmenu[] = { + {"*Item Menu (command binds)", PMENU_ALIGN_LEFT, NULL, NULL }, + { "-----------------------------------------------", PMENU_ALIGN_LEFT, NULL, NULL }, + { "Laser Sight (jmod laser)", PMENU_ALIGN_LEFT, NULL, ToggleLaser }, + { "Slippers (jmod slippers)", PMENU_ALIGN_LEFT, NULL, ToggleSlippers }, + { NULL, PMENU_ALIGN_LEFT, NULL, NULL }, + { "Respawn to Closest Spawn (jmod spawnc)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoPC_f }, + { "Respawn to Random Spawn (jmod spawnp)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoP_f }, + +}; + + +void OpenPMItemMenu(edict_t *ent) +{ + PMenu_Open(ent, pmitemmenu, 2, sizeof(pmitemmenu) / sizeof(pmenu_t)); +} + +//End PaTMaN's jmod add + //AQ2:TNG - slicer void VotingMenu (edict_t * ent, pmenu_t * p) { @@ -936,7 +1152,7 @@ pmenu_t joinmenu[] = { {"MOTD", PMENU_ALIGN_LEFT, NULL, ReprintMOTD}, {"Credits", PMENU_ALIGN_LEFT, NULL, CreditsMenu}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, - {"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, + {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, @@ -956,6 +1172,8 @@ void killPlayer( edict_t *ent, qboolean suicidePunish ) if (!IS_ALIVE(ent)) return; + int damage = 100000; + if (suicidePunish && punishkills->value) { edict_t *attacker = ent->client->attacker; @@ -975,11 +1193,22 @@ void killPlayer( edict_t *ent, qboolean suicidePunish ) } } } + + // Throws bloody gibs everywhere + if (sv_killgib->value) { + int n; + for (n = 0; n < 10; n++) + ThrowGib(ent, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); + ThrowClientHead(ent, damage); + ent->client->anim_priority = ANIM_DEATH; + ent->client->anim_end = 0; + //ent->takedamage = DAMAGE_NO; + } ent->flags &= ~FL_GODMODE; ent->health = 0; meansOfDeath = MOD_SUICIDE; - player_die(ent, ent, ent, 100000, vec3_origin); + player_die(ent, ent, ent, damage, vec3_origin); ent->deadflag = DEAD_DEAD; } @@ -991,6 +1220,11 @@ char *TeamName (int team) return "None"; } +/* +AssignSkin + +Anytime this is called, all player models are re-skinned +*/ void AssignSkin (edict_t * ent, const char *s, qboolean nickChanged) { int playernum = ent - g_edicts - 1; @@ -1004,8 +1238,37 @@ void AssignSkin (edict_t * ent, const char *s, qboolean nickChanged) default_skin = force_skin->string; } - if( (ctf->value || dom->value) && ! matchmode->value ) - { + // Accounts for each Espionage mode, in matchmode or not + if (esp->value){ + /* + In ATL mode (espsettings == 0), all teams must have a leader, and will have their own skin + In ETV mode (espsettings == 1), only TEAM1 gets a leader. + */ + + switch (ent->client->resp.team) { + case TEAM1: + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM1].skin); + if (IS_LEADER(ent)){ + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM1].leader_skin); + } + break; + case TEAM2: + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM2].skin); + if ((atl->value) && IS_LEADER(ent)){ + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM2].leader_skin); + } + break; + case TEAM3: + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM3].skin); + if ((atl->value) && IS_LEADER(ent)){ + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM3].leader_skin); + } + break; + default: + Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, default_skin); + break; + } + } else if ((ctf->value || dom->value) && !matchmode->value) { // forcing CTF model if(ctf_model->string[0]) { /* copy at most bytes that the skin name itself fits in with the delimieter and NULL */ @@ -1033,7 +1296,7 @@ void AssignSkin (edict_t * ent, const char *s, qboolean nickChanged) break; } } - else + else // Normal teamplay mode, not CTF/DOM/ESP/Matchmode { switch (ent->client->resp.team) { @@ -1113,6 +1376,11 @@ void Team_f (edict_t * ent) return; } //PG BUND - END (Tourney extension) + + if (esp->value && IS_LEADER(ent)) { + gi.cprintf(ent, PRINT_MEDIUM, "You are a team leader, you cannot change teams.\n"); + return; + } Q_strncpyz(team, gi.args(), sizeof(team)); t = team; @@ -1225,15 +1493,35 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) G_UpdatePlayerStatusbar(ent, 1); } +#ifdef AQTION_EXTENSION + if (desired_team == NOTEAM) + HUD_SetType(ent, 1); + else + HUD_SetType(ent, -1); +#endif + if (level.intermission_framenum) return; + // Espionage join a game in progress + if (esp->value && team_round_going && ent->inuse && ent->client->resp.team) + { + PutClientInServer (ent); + //AddToTransparentList (ent); + } + if (!(gameSettings & GS_ROUNDBASED) && team_round_going && ent->inuse && ent->client->resp.team) { PutClientInServer (ent); AddToTransparentList (ent); } + #if USE_AQTION + if (in_warmup && warmup_bots->value) { + PutClientInServer (ent); + } + #endif + //AQ2:TNG END if (!skip_menuclose && (gameSettings & GS_WEAPONCHOOSE) && !use_randoms->value) OpenWeaponMenu(ent); @@ -1243,6 +1531,8 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) SelectRandomWeaponAndItem(ent, weapmenu); } + gi.cprintf(ent, PRINT_CHAT, "You joined Team %i (%s).\n", desired_team, TeamName(desired_team)); + teams_changed = true; } @@ -1262,10 +1552,17 @@ void LeaveTeam (edict_t * ent) MM_LeftTeam( ent ); + if (esp->value) + EspLeaderLeftTeam ( ent ); + ent->client->resp.joined_team = 0; ent->client->resp.team = NOTEAM; G_UpdatePlayerStatusbar(ent, 1); +#ifdef AQTION_EXTENSION + HUD_SetType(ent, 1); +#endif + teams_changed = true; } @@ -1275,7 +1572,7 @@ void ReturnToMain (edict_t * ent, pmenu_t * p) OpenJoinMenu (ent); } -char *menu_itemnames[ITEM_MAX_NUM] = { +char *menu_itemnames[KIT_MAX_NUM] = { "", MK23_NAME, MP5_NAME, @@ -1286,16 +1583,27 @@ char *menu_itemnames[ITEM_MAX_NUM] = { "Akimbo Pistols", "Combat Knives", GRENADE_NAME, - SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, "Laser Sight", HELM_NAME, - "" + "", + "", + "", + "", + "", + "", + "", + "", + "", + C_KIT_NAME_FULL, + S_KIT_NAME_FULL, + A_KIT_NAME_FULL, }; + typedef struct menuentry_s { int itemNum; @@ -1343,6 +1651,39 @@ void OpenItemMenu (edict_t * ent) PMenu_Close(ent); } +void OpenItemKitMenu (edict_t * ent) +{ + menuentry_t *kitmenuEntry, kit_menu_items[] = { + { KEV_NUM, SelectItem1 }, + { C_KIT_NUM, SelectKit1 }, + { S_KIT_NUM, SelectKit2 }, + { A_KIT_NUM, SelectKit3 } + }; + int i, count, pos = 4; + + count = sizeof( kit_menu_items ) / sizeof( kit_menu_items[0] ); + + for (kitmenuEntry = kit_menu_items, i = 0; i < count; i++, kitmenuEntry++) { + itemkitmenu[pos].text = menu_itemnames[kitmenuEntry->itemNum]; + itemkitmenu[pos].SelectFunc = kitmenuEntry->SelectFunc; + pos++; + } + + if ( pos > 4 ) + { + for (; pos < 10; pos++) + { + itemkitmenu[pos].text = NULL; + itemkitmenu[pos].SelectFunc = NULL; + } + + PMenu_Open(ent, itemkitmenu, 4, sizeof(itemkitmenu) / sizeof(pmenu_t)); + return; + } + + PMenu_Close(ent); +} + void OpenWeaponMenu (edict_t * ent) { if (use_randoms->value) @@ -1388,7 +1729,11 @@ void OpenWeaponMenu (edict_t * ent) } } - OpenItemMenu(ent); + if(!item_kit_mode->value){ + OpenItemMenu(ent); + } else { + OpenItemKitMenu(ent); + } } // AQ2:TNG Deathwatch - Updated this for the new menu @@ -1595,8 +1940,10 @@ void ResetScores (qboolean playerScores) ent->client->resp.damage_dealt = 0; ent->client->resp.streakHS = 0; ent->client->resp.streakKills = 0; + ent->client->resp.roundStreakKills = 0; ent->client->resp.ctf_caps = 0; ent->client->resp.ctf_capstreak = 0; + ent->client->resp.esp_capstreak = 0; ent->client->resp.deaths = 0; ent->client->resp.team_kills = 0; ent->client->resp.team_wounds = 0; @@ -1615,11 +1962,43 @@ void CenterPrintAll (const char *msg) for (i = 0; i < game.maxclients; i++) { ent = &g_edicts[1 + i]; + if (ent->is_bot) + continue; if (ent->inuse) gi.centerprintf (ent, "%s", msg); } } +void CenterPrintTeam (int teamNum, const char *msg) +{ + int i; + edict_t *ent; + + for (i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[1 + i]; + if (ent->is_bot) + continue; + if (ent->inuse && ent->client->resp.team == teamNum) + gi.centerprintf (ent, "%s", msg); + } +} + +void CenterPrintLevelTeam (int teamNum, int printlvl, const char *msg) +{ + int i; + edict_t *ent; + + for (i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[1 + i]; + if (ent->is_bot) + continue; + if (ent->inuse && ent->client->resp.team == teamNum) + gi.cprintf(ent, printlvl, "%s", msg); + } +} + int TeamHasPlayers (int team) { int i, players; @@ -1684,11 +2063,7 @@ int CheckForWinner(void) int players[TEAM_TOP] = { 0 }, i = 0, teamNum = 0, teamsWithPlayers = 0; edict_t *ent; - if (!(gameSettings & GS_ROUNDBASED)) - return WINNER_NONE; - - for (i = 0; i < game.maxclients; i++) - { + for (i = 0; i < game.maxclients; i++){ ent = &g_edicts[1 + i]; if (!ent->inuse || ent->solid == SOLID_NOT) continue; @@ -1699,20 +2074,57 @@ int CheckForWinner(void) players[teamNum]++; } - teamsWithPlayers = 0; - for (i = TEAM1; i <= teamCount; i++) - { + for (i = TEAM1; i <= teamCount; i++){ if (players[i]) { teamsWithPlayers++; teamNum = i; } } - if (teamsWithPlayers) - return (teamsWithPlayers > 1) ? WINNER_NONE : teamNum; + if (!(gameSettings & GS_ROUNDBASED)) + return WINNER_NONE; + + if (esp->value){ + if (atl->value) { + if (teamCount == TEAM2) { + if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead) { + return WINNER_TIE; + } else if (teams[TEAM1].leader_dead) { + return TEAM2; + } else if (teams[TEAM2].leader_dead) { + return TEAM1; + } + } else if (teamCount == TEAM3) { + if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead && teams[TEAM3].leader_dead) { + return WINNER_TIE; + } else if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead) { + return TEAM3; + } else if (teams[TEAM1].leader_dead && teams[TEAM3].leader_dead) { + return TEAM2; + } else if (teams[TEAM2].leader_dead && teams[TEAM3].leader_dead) { + return TEAM1; + } + } + } else if (etv->value) { + // Check if this value is true, which means the escorting team wins + // By default it is false + if (espsettings.escortcap == true) { + return TEAM1; + } else if (teams[TEAM1].leader_dead){ + return TEAM2; + } else if (teamsWithPlayers) // This is in case the last person on team 2 leaves without dying, TEAM1 wins + return (teamsWithPlayers > 1) ? WINNER_NONE : teamNum; + } + + } else if (!esp->value) { + if (teamsWithPlayers) + return (teamsWithPlayers > 1) ? WINNER_NONE : teamNum; - return WINNER_TIE; + //gi.dprintf("I was called somehow\n\n\n"); + return WINNER_TIE; + } + return WINNER_NONE; } // CheckForForcedWinner: A winner is being forced, find who it is. @@ -1723,6 +2135,33 @@ int CheckForForcedWinner(void) int i, teamNum, bestTeam, secondBest; edict_t *ent; + /* + In ATL mode, if all leaders are alive, the round ends in a tie + health notwithstanding + In ETV mode, if the escorting team has + not captured the point, the defending team wins + */ + if (esp->value){ + if (atl->value){ + if (teamCount == TEAM2){ + if (IS_ALIVE(teams[TEAM1].leader) && + IS_ALIVE(teams[TEAM2].leader)){ + return WINNER_TIE; + } + } else if (teamCount == TEAM3){ + if (IS_ALIVE(teams[TEAM1].leader) && + IS_ALIVE(teams[TEAM2].leader) && + IS_ALIVE(teams[TEAM3].leader)){ + return WINNER_TIE; + } + } + } else if (etv->value){ + if (espsettings.escortcap == false){ + return TEAM2; + } + } + } + for (i = 0; i < game.maxclients; i++) { ent = &g_edicts[1 + i]; @@ -1771,6 +2210,9 @@ static void SpawnPlayers(void) int i; edict_t *ent; + if (esp->value) + NS_SetupTeamSpawnPoints (); + if (gameSettings & GS_ROUNDBASED) { if (!use_oldspawns->value) @@ -1871,6 +2313,13 @@ void RunWarmup (void) gi.centerprintf(ent, "WARMUP"); } } + #if USE_AQTION + if (warmup_bots->value){ + gi.cvar_forceset("am", "1"); + gi.cvar_forceset("am_botcount", warmup_bots->string); + attract_mode_bot_check(); + } + #endif } void StartRound (void) @@ -1881,9 +2330,13 @@ void StartRound (void) static void StartLCA(void) { - if (gameSettings & (GS_WEAPONCHOOSE|GS_ROUNDBASED)) + if ((gameSettings & (GS_WEAPONCHOOSE|GS_ROUNDBASED))) CleanLevel(); + if (esp->value) + // Re-skin everyone to ensure only one leader skin + EspSkinCheck(); + if (use_tourney->value && !tourney_lca->value) { lights_camera_action = TourneySetTime (T_SPAWN); @@ -1896,6 +2349,11 @@ static void StartLCA(void) lights_camera_action = 43; // TempFile changed from 41 } SpawnPlayers(); + + if (esp->value) { + EspResetCapturePoint(); + EspAnnounceDetails(false); + } } // FindOverlap: Find the first (or next) overlapping player for ent. @@ -1962,6 +2420,10 @@ void MakeAllLivePlayersObservers (void) if(ctf->value) CTFResetFlags(); + // Reset Espionage flag + if (esp->value && etv->value) + EspResetCapturePoint(); + for (i = 0; i < game.maxclients; i++) { ent = &g_edicts[1 + i]; @@ -2022,11 +2484,11 @@ qboolean CheckTimelimit( void ) return true; } - // CTF with use_warnings should have the same warnings when the map is ending as it does for halftime (see CTFCheckRules). + // CTF or Espionage with use_warnings should have the same warnings when the map is ending as it does for halftime (see CTFCheckRules). // Otherwise, use_warnings should warn about 3 minutes and 1 minute left, but only if there aren't round ending warnings. if( use_warnings->value && (ctf->value || ! roundtimelimit->value) ) { - if( timewarning < 3 && ctf->value && level.matchTime >= timelimit->value * 60 - 10 ) + if( timewarning < 3 && (ctf->value && level.matchTime >= timelimit->value * 60 - 10 )) { gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("world/10_0.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 3; @@ -2036,6 +2498,11 @@ qboolean CheckTimelimit( void ) CenterPrintAll( "1 MINUTE LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/1_minute.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 2; + if (esp->value){ + if (esp_debug->value) + gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); + EspAnnounceDetails(true); + } } else if( timewarning < 1 && (! ctf->value) && timelimit->value > 3 && level.matchTime >= (timelimit->value - 3) * 60 ) { @@ -2086,6 +2553,11 @@ static qboolean CheckRoundTimeLimit( void ) CenterPrintAll( "1 MINUTE LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex( "tng/1_minute.wav" ), 1.0, ATTN_NONE, 0.0 ); timewarning = 2; + if (esp->value) { + if (esp_debug->value) + gi.dprintf("%s: roundLimitFrames = %d\n", __FUNCTION__, roundLimitFrames); + EspAnnounceDetails(true); + } } else if (roundLimitFrames <= 1800 && timewarning < 1 && roundtimelimit->value > 3) { @@ -2151,6 +2623,8 @@ int WonGame (int winner) if(use_warnings->value) gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, level.snd_teamwins[0], 1.0, ATTN_NONE, 0.0); + if (esp->value) + EspEndOfRoundCleanup(); PrintScores (); } else @@ -2170,7 +2644,6 @@ int WonGame (int winner) player->client->pers.netname); TourneyWinner (player); } - game.roundNum++; } else { @@ -2181,15 +2654,8 @@ int WonGame (int winner) gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, level.snd_teamwins[winner], 1.0, ATTN_NONE, 0.0); // end of changing sound dir teams[winner].score++; - - #ifdef AQTION_EXTENSION - #ifdef AQTION_HUD - Ghud_SetFlags(teams[winner].ghud_icon, GHF_BLINK); - Ghud_SetFlags(teams[winner].ghud_num, GHF_BLINK); - teams[winner].ghud_resettime = level.time + 3; - #endif - #endif - + if (esp->value) + EspEndOfRoundCleanup(); gi.cvar_forceset(teams[winner].teamscore->name, va("%i", teams[winner].score)); PrintScores (); @@ -2223,6 +2689,14 @@ int WonGame (int winner) // Increment roundNum for tracking game.roundNum++; + // Reset kill streaks in team modes + if (use_killcounts->value){ + for (i = 0; i < game.maxclients; i++) { + cl_ent = g_edicts + 1 + i; + cl_ent->client->resp.roundStreakKills = 0; + } + } + return 0; } @@ -2261,6 +2735,10 @@ int CheckTeamRules (void) if (holding_on_tie_check) { + if (esp->value) { + holding_on_tie_check = 0; + checked_tie = 1; + } holding_on_tie_check--; if (holding_on_tie_check > 0) return 0; @@ -2273,19 +2751,39 @@ int CheckTeamRules (void) team_round_countdown--; if(!team_round_countdown) { - if (BothTeamsHavePlayers ()) + if (!esp->value && BothTeamsHavePlayers()) { in_warmup = 0; team_game_going = 1; StartLCA(); } + else if (esp->value && AllTeamsHaveLeaders() && BothTeamsHavePlayers()) + { + if (esp_debug->value) + gi.dprintf("%s: Esp mode on, All teams have leaders, Both teams have players\n", __FUNCTION__); + in_warmup = 0; + team_game_going = 1; + StartLCA(); + } else { - if (!matchmode->value || TeamsReady()) + if (esp->value && !AllTeamsHaveLeaders()) { + if (atl->value){ + CenterPrintAll ("All Teams Must Have a Leader!\nType 'leader' in console to volunteer!"); + } else if (etv->value) { + CenterPrintTeam(TEAM1, "Team 1 Must Have a Leader!\nType 'leader' in console to volunteer!"); + CenterPrintTeam(TEAM2, "Waiting on Team 1 to select a leader..."); + } + EspEndOfRoundCleanup(); + } else if (!matchmode->value || TeamsReady()) { CenterPrintAll ("Not enough players to play!"); - else + // Remove all leaders if they are bots + if (esp->value){ + + } + } else { CenterPrintAll ("Both Teams Must Be Ready!"); - + } team_round_going = team_round_countdown = team_game_going = 0; MakeAllLivePlayersObservers (); } @@ -2300,6 +2798,51 @@ int CheckTeamRules (void) { gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); + + #if USE_AQTION + // Cleanup and remove all bots, it's go time! + if (warmup_bots->value){ + gi.cvar_forceset("am", "0"); + gi.cvar_forceset("am_botcount", "0"); + attract_mode_bot_check(); + ACESP_RemoveBot("all"); + CenterPrintAll("All bots removed, good luck and have fun!"); + + //Re-enable stats now that the bots are gone + game.ai_ent_found = false; + gi.cvar_forceset(stat_logs->name, "1"); + } + + // Move spectators to the capturepoint + if (esp->value && etv->value) { + // find the capturepoint + edict_t *capturepoint; + edict_t *ent; + capturepoint = G_Find(NULL, FOFS(classname), "item_flag"); + if (!capturepoint) // Somehow we're in ETV with no capture point? + gi.dprintf("ERROR: No capture point (item_flag) found for ETV!?\n"); + + if (capturepoint) { + gi.dprintf("INFO: capture point (item_flag) found\n"); + VectorCopy( capturepoint->s.origin, level.poi_origin ); + VectorCopy( capturepoint->s.angles, level.poi_angle ); + } + + // move all spectators to the capture point + for (i = 0, ent = g_edicts + 1; i < game.maxclients; i++, ent++) + { + if (!ent->inuse || !ent->client || ent->is_bot) + continue; + if (ent->solid != SOLID_NOT) + continue; + // if (!ent->client->pers.spectator) + // continue; + gi.dprintf("INFO: moving spectators\n"); + + MoveClientToPOI(ent, capturepoint); + } + } + #endif } } if(team_round_countdown == 41 && !matchmode->value) @@ -2336,14 +2879,25 @@ int CheckTeamRules (void) return 1; if (vCheckVote()) { - EndDMLevel (); + EndDMLevel(); team_round_going = team_round_countdown = team_game_going = 0; return 1; } + if (esp->value) { + if (!AllTeamsHaveLeaders()) + return 1; + + if (EspCheckRules()){ + EndDMLevel(); + team_round_going = team_round_countdown = team_game_going = 0; + return 1; + } + } + if (!team_round_countdown) { - if (BothTeamsHavePlayers ()) + if (BothTeamsHavePlayers() || (esp->value && AllTeamsHaveLeaders() && BothTeamsHavePlayers())) { if (use_tourney->value) { @@ -2355,7 +2909,11 @@ int CheckTeamRules (void) { int warmup_length = max( warmup->value, round_begin->value ); char buf[64] = ""; - sprintf( buf, "The round will begin in %d seconds!", warmup_length ); + if (esp->value && BothTeamsHavePlayers()) { + sprintf( buf, "All teams are ready!\nThe round will begin in %d seconds!", warmup_length ); + } else { + sprintf( buf, "The round will begin in %d seconds!", warmup_length ); + } CenterPrintAll( buf ); team_round_countdown = warmup_length * 10 + 2; @@ -2403,10 +2961,12 @@ int CheckTeamRules (void) return 1; } - if (!BothTeamsHavePlayers()) + if (!BothTeamsHavePlayers() || (esp->value && !AllTeamsHaveLeaders())) { if (!matchmode->value || TeamsReady()) CenterPrintAll( "Not enough players to play!" ); + else if (esp->value && !AllTeamsHaveLeaders()) + CenterPrintAll ("Both Teams Must Have a Leader!\nType 'leader' in console to volunteer!"); else CenterPrintAll( "Both Teams Must Be Ready!" ); @@ -2418,6 +2978,24 @@ int CheckTeamRules (void) } return 0; //CTF and teamDM dont need to check winner, its not round based } + else if ((gameSettings & GS_ROUNDBASED)) + // Team round is going, and it's GS_ROUNDBASED + { + if (esp->value) { + //Debugging + if (esp_debug->value) + EspDebug(); + + if (EspCheckRules()){ + EndDMLevel(); + team_round_going = team_round_countdown = team_game_going = 0; + return 1; + } + GenerateMedKit(false); + EspCleanUp(); + EspLeaderCheck(); + } + } winner = CheckForWinner(); if (winner != WINNER_NONE) @@ -2452,7 +3030,14 @@ int CheckTeamRules (void) void A_Scoreboard (edict_t * ent) { - int wteam = 0; + int wteam = 0; + + // int timeRemaining = (level.matchTime >= (timelimit->value - 1) * 60); + + // if (timelimit->value && timeRemaining < 60){ + // ent->client->ps.stats[STAT_TIMER_ICON] = level.pic_esp_respawn_icon; + // ent->client->ps.stats[STAT_TIMER] = timeRemaining / HZ; + // } if (ent->client->layout == LAYOUT_SCORES) { @@ -2717,6 +3302,13 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) tpic[TEAM1][0] = 30; tpic[TEAM2][0] = 31; } + else if(esp->value) + { + base_x += 8; + tpic[TEAM1][0] = STAT_TEAM1_PIC; + tpic[TEAM2][0] = STAT_TEAM2_PIC; + tpic[TEAM3][0] = STAT_TEAM3_PIC; + } else if(teamdm->value) { scoreWidth = 3; @@ -2902,7 +3494,7 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) totalaliveprinted++; playername[0] = 0; - if (IS_CAPTAIN(cl_ent)) { + if (IS_CAPTAIN(cl_ent) || IS_LEADER(cl_ent)) { playername[0] = '@'; playername[1] = 0; } @@ -3218,7 +3810,7 @@ void TallyEndOfLevelTeamScores (void) } // Stats begin - #if USE_AQTION + #ifdef USE_AQTION if (stat_logs->value && !matchmode->value) { LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match logs @@ -3229,13 +3821,17 @@ void TallyEndOfLevelTeamScores (void) // Stats end } - /* * Teamplay spawning functions... */ edict_t *SelectTeamplaySpawnPoint (edict_t * ent) { + // Print all of the teamplay_spawns + // int i; + // for (i = 0; i < num_potential_spawns; i++) + // gi.dprintf ("teamplay_spawn %d: %s\n", i, potential_spawns[i]->classname); + // gi.dprintf("%s was called!\n\n\n", __FUNCTION__); return teamplay_spawns[ent->client->resp.team - 1]; } @@ -3363,7 +3959,7 @@ void SelectFarTeamplaySpawnPoint (int team, qboolean teams_assigned[]) // // Setup the points at which the teams will spawn. // -void SetupTeamSpawnPoints () +void SetupTeamSpawnPoints (void) { qboolean teams_assigned[MAX_TEAMS]; int i, l; @@ -3386,7 +3982,7 @@ void SetupTeamSpawnPoints () // TNG:Freud New Spawning system // NS_GetSpawnPoints: // Put the potential spawns into arrays for each team. -void NS_GetSpawnPoints () +void NS_GetSpawnPoints (void) { int x, i; @@ -3525,7 +4121,7 @@ qboolean NS_SelectFarTeamplaySpawnPoint (int team, qboolean teams_assigned[]) // TNG:Freud // NS_SetupTeamSpawnPoints // Finds and assigns spawn points to each team. -void NS_SetupTeamSpawnPoints () +void NS_SetupTeamSpawnPoints (void) { qboolean teams_assigned[MAX_TEAMS]; int l; @@ -3543,3 +4139,23 @@ void NS_SetupTeamSpawnPoints () return; } } + +/* +Simple function that just returns the opposite team number +Obviously, do not use this for 3team functions +*/ +int OtherTeam(int teamNum) +{ + if (teamNum < 0 || teamNum > TEAM2) { + gi.dprintf("OtherTeam() was called but parameter supplied is not 1 or 2"); + return 0; + } + + if (teamNum == 1) + return TEAM2; + else + return TEAM1; + + // Returns zero if teamNum is not 1 or 2 + return 0; +} \ No newline at end of file diff --git a/src/action/a_team.h b/src/action/a_team.h index 7bb58b29c..909633282 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -69,6 +69,7 @@ void UpdateJoinMenu( void ); void OpenJoinMenu (edict_t *); void OpenWeaponMenu (edict_t *); void OpenItemMenu (edict_t * ent); +void OpenItemKitMenu (edict_t * ent); void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose); edict_t *FindOverlap (edict_t * ent, edict_t * last_overlap); int CheckTeamRules (void); @@ -89,12 +90,22 @@ void RemoveFromTransparentList (edict_t *); qboolean OnTransparentList( const edict_t *ent ); void PrintTransparentList (void); void CenterPrintAll (const char *msg); +void CenterPrintTeam (int teamNum, const char *msg); +void CenterPrintLevelTeam (int teamNum, int printlvl, const char *msg); int TeamHasPlayers( int team ); +char* PrintMatchRules(void); //TNG:Freud - new spawning system void NS_GetSpawnPoints (void); qboolean NS_SelectFarTeamplaySpawnPoint (int team, qboolean teams_assigned[]); void NS_SetupTeamSpawnPoints (void); +int OtherTeam(int teamNum); + +//PaTMaN - Menu support +void OpenPMItemMenu (edict_t * ent); + +//Expose auto-join functionality +void JoinTeamAutobalance (edict_t * ent); typedef struct spawn_distances_s { diff --git a/src/action/a_vote.c b/src/action/a_vote.c index 740635c5d..d08a3fafe 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -2016,7 +2016,7 @@ cvar_t *_InitScrambleVote (ini_t * ini) qboolean ScrambleTeams(void) { int i, j, numplayers, newteam; - edict_t *ent, *players[MAX_CLIENTS], *oldCaptains[TEAM_TOP] = {NULL}; + edict_t *ent, *players[MAX_CLIENTS], *oldCaptains[TEAM_TOP] = {NULL}, *oldLeaders[TEAM_TOP] = {NULL}; numplayers = 0; for (i = 0, ent = &g_edicts[1]; i < game.maxclients; i++, ent++) @@ -2044,6 +2044,8 @@ qboolean ScrambleTeams(void) for (i = TEAM1; i <= teamCount; i++) { oldCaptains[i] = teams[i].captain; teams[i].captain = NULL; + if (esp->value) + teams[i].leader = NULL; } } @@ -2055,6 +2057,10 @@ qboolean ScrambleTeams(void) if (oldCaptains[ent->client->resp.team] == ent && !teams[newteam].captain) teams[newteam].captain = ent; + + if (esp->value) + if (oldLeaders[ent->client->resp.team] == ent && !teams[newteam].leader) + teams[newteam].leader = ent; ent->client->resp.team = newteam; diff --git a/src/action/a_xgame.c b/src/action/a_xgame.c index 99e59d040..896caad62 100644 --- a/src/action/a_xgame.c +++ b/src/action/a_xgame.c @@ -528,7 +528,7 @@ void VideoCheckClient(edict_t *ent) if (ent->client->resp.gllockpvs != 0) { gi.cprintf(ent, PRINT_HIGH, "This server does not allow using that value for gl_lockpvs, set it to '0'\n"); - gi.bprintf(PRINT_HIGH, "%s was using an illegal setting\n", + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_lockpvs setting\n", ent->client->pers.netname); Kick_Client(ent); return; @@ -538,20 +538,38 @@ void VideoCheckClient(edict_t *ent) if (ent->client->resp.glclear != 0) { gi.cprintf(ent, PRINT_HIGH, "This server does not allow using that value for gl_clear, set it to '0'\n"); - gi.bprintf(PRINT_HIGH, "%s was using an illegal setting\n", + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_clear setting\n", ent->client->pers.netname); Kick_Client(ent); return; } } - if (darkmatch->value) { - if (ent->client->resp.gldynamic != 1) { + if (darkmatch->value && ent->client->resp.gldynamic != 1) { + ent->client->resp.gldynamic = 1; + stuffcmd (ent, "gl_dynamic 1\n"); + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_dynamic setting, forcing it to 1\n", + ent->client->pers.netname); + // Double check + if (ent->client->resp.gldynamic != 1){ gi.cprintf(ent, PRINT_HIGH, "This server does not allow using that value for gl_dynamic, set it to '1'\n"); - gi.bprintf(PRINT_HIGH, "%s was using an illegal setting\n", + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_dynamic setting, kicking\n", + ent->client->pers.netname); + Kick_Client(ent); + } + } + if (darkmatch->value && ent->client->resp.glbrightness != 0) { + ent->client->resp.glbrightness = 0; + stuffcmd (ent, "gl_brightness 0\n"); + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_brightness setting, forcing it to 0\n", + ent->client->pers.netname); + // Double check + if (ent->client->resp.glbrightness != 0){ + gi.cprintf(ent, PRINT_HIGH, + "This server does not allow using that value for gl_brightness, set it to '0'\n"); + gi.bprintf(PRINT_HIGH, "%s was using an illegal gl_brightness setting, kicking\n", ent->client->pers.netname); Kick_Client(ent); - return; } } //Starting Modulate checks @@ -621,3 +639,67 @@ void VideoCheckClient(edict_t *ent) } } } + +/* + Sends a specified message to all clients at a specified time in the round +*/ +Message *timedMessages = NULL; +int numMessages = 0; + +qboolean TimedMessageAtTimeAll() +{ + int crl = (current_round_length / 10); + qboolean anyMessageFired = false; + int i; + + for (i = 0; i < numMessages; i++) { + if (!timedMessages[i].fired && + team_round_going && + crl >= timedMessages[i].seconds && + timedMessages[i].teamNum != -1) { + if(timedMessages[i].teamNum == NOTEAM) + CenterPrintAll(timedMessages[i].msg); + else + CenterPrintTeam(timedMessages[i].teamNum, timedMessages[i].msg); + timedMessages[i].fired = true; + anyMessageFired = true; + } + } + + return anyMessageFired; +} + +qboolean TimedMessageAtTimeEnt() +{ + int crl = (current_round_length / 10); + qboolean anyMessageFired = false; + int i; + for (i = 0; i < numMessages; i++) { + if (!timedMessages[i].fired && + team_round_going && + crl >= timedMessages[i].seconds && + timedMessages[i].ent != NULL) { + gi.centerprintf(timedMessages[i].ent, timedMessages[i].msg); + timedMessages[i].fired = true; + anyMessageFired = true; + } + } + + return anyMessageFired; +} + +void FireTimedMessages() +{ + TimedMessageAtTimeAll(); + TimedMessageAtTimeEnt(); +} + +void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg) { + timedMessages = realloc(timedMessages, sizeof(Message) * (numMessages + 1)); + timedMessages[numMessages].teamNum = teamNum; + timedMessages[numMessages].ent = ent; + timedMessages[numMessages].seconds = seconds; + timedMessages[numMessages].msg = msg; + timedMessages[numMessages].fired = false; + numMessages++; +} diff --git a/src/action/a_xmenu.c b/src/action/a_xmenu.c index 54a09e689..e7d14793e 100644 --- a/src/action/a_xmenu.c +++ b/src/action/a_xmenu.c @@ -19,7 +19,7 @@ static XMENU_TITLE xRaw[] = { "\x80\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x82", // double line "previous page", "next page", - "Use [ and ] to move cursor", + "Use arrows to move cursor", "ENTER to select, TAB to exit", "\x1D\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1F", // single line "\x80\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x81\x82" // double line diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 9fb361caf..0e4d6fbd1 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -96,7 +96,7 @@ extern cvar_t *ltk_chat; // Chat setting for bots, off or on (0,1) extern cvar_t *ltk_routing; // Set to 1 to drop nodes, otherwise you won't do it! extern cvar_t *ltk_botfile; // Set this to adjust which botdata file to load, default is botdata extern cvar_t *ltk_loadbots; // Set to 0 to not load bots from ltk_botfile value, 1 for normal operation -extern cvar_t *ltk_showbots; // Set to 1 to have bots show up as normal players in server listings +extern cvar_t *ltk_classic; // Set to 0 to use the new naming system and skins. 1 for classic operation extern int lights_camera_action; @@ -170,7 +170,6 @@ extern int lights_camera_action; #define VEC_ORIGIN tv(0,0,0) - typedef struct nodelink_s { short int targetNode; @@ -220,6 +219,7 @@ void ClientUserinfoChanged (edict_t *ent, char *userinfo); void CopyToBodyQue (edict_t *ent); qboolean ClientConnect (edict_t *ent, char *userinfo); void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); +void hurt_touch (edict_t * self, edict_t * other, cplane_t * plane, csurface_t * surf); // acebot_ai.c protos void ACEAI_Think (edict_t *self); @@ -295,10 +295,11 @@ void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); void ACESP_ReAddBots(void); void ACESP_RemoveBot(char *name); -void safe_cprintf (struct edict_s * ent, int printlevel, const char *fmt, ...); -void safe_centerprintf (struct edict_s * ent, const char *fmt, ...); +void attract_mode_bot_check(void); +void safe_cprintf (edict_t *ent, int printlevel, char *fmt, ...); +void safe_centerprintf (edict_t *ent, char *fmt, ...); void debug_printf (char *fmt, ...); -int GetBotCount(void); +void LTKClearBotNames(void); // bot_ai.c protos qboolean BOTAI_NeedToBandage(edict_t *bot); diff --git a/src/action/acesrc/acebot.txt b/src/action/acesrc/acebot.txt index 60f94f40e..f5a628567 100644 --- a/src/action/acesrc/acebot.txt +++ b/src/action/acesrc/acebot.txt @@ -1,98 +1,98 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 - \/ \/ \/ \/ \/ READ BELOW \/ \/ \/ \/ \/ -// -// This file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -// -// Please see license.txt in the source directory for the copyright -// information regarding those files belonging to Id Software, Inc. -// -// Should you decide to release a modified version of ACE, you MUST -// include the following text (minus the BEGIN and END lines) in the -// documentation for your modification. -// -// --- BEGIN --- -// -// The ACE Bot is a product of Steve Yeager, and is available from -// the ACE Bot homepage, at http://www.axionfx.com/ace. -// -// This program is a modification of the ACE Bot, and is therefore -// in NO WAY supported by Steve Yeager. -// -// This program MUST NOT be sold in ANY form. If you have paid for -// this product, you should contact Steve Yeager immediately, via -// the ACE Bot homepage. -// -// --- END --- -// -// I, Steve Yeager, hold no responsibility for any harm caused by the -// use of this source code, especially to small children and animals. -// It is provided as-is with no implied warranty or support. -// -// I also wish to thank and acknowledge the great work of others -// that has helped me to develop this code. -// -// John Cricket - For ideas and swapping code. -// Ryan Feltrin - For ideas and swapping code. -// SABIN - For showing how to do true client based movement. -// BotEpidemic - For keeping us up to date. -// Telefragged.com - For giving ACE a home. -// Microsoft - For giving us such a wonderful crash free OS. -// id - Need I say more. -// -// And to all the other testers, pathers, and players and people -// who I can't remember who the heck they were, but helped out. -// -/////////////////////////////////////////////////////////////////////// - - -ACEBOT II BASE ------------------------------------------------------------------------ - - I decided to release the base code for ACEBOT II for further study. -Hopefully it will provide a simple base on which to build future bots. -I think I have solved most of the basic problems associated with bots and -by using this source as a starting point, much more advanced bots can be -created. My time now is quite limited and I am not sure what I will be -doing with this source in the future. I do have plans for some interesting -"additions" to this bot, but time may not make it possible for these -to be realized. - - A number of the basic routines have been "dumbed" down from the -original ACE sourcecode and some are only put there just to make the bot -function. Even so, it still plays a pretty good game. - - I hope my comments in the source code are easy enough to read and I will -answer questions about the source as long as they are well thought out. I -will not respond to stupid or obvious questions. So if you are new to -C, I ain't gonna spend my time teaching you the basics. I've also -simplified my C usage and some routines are written for readability vs. -performance. - - If you wish to add to the base code and think others would benifit from -your addition, please email me and we can discuss it. - - All code was written and compiled using MS Visual C++ 6.0+SP1. To install -the source, create a directory named "ace" under :\quake2 (c:\quake2\ace). -Unzip the source to \quake2\ace and it will create a src directory for the -source code and a \quake2\nav directory for the node files. - - I would also like to see the work that others create using this base. -So if you create the next "killer" mod, please let me know about it. - - Good luck and happy coding! - - Steve Yeager - www.axionfx.com - syeager@axionfx.com - - - - - - +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 - \/ \/ \/ \/ \/ READ BELOW \/ \/ \/ \/ \/ +// +// This file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +// +// Please see license.txt in the source directory for the copyright +// information regarding those files belonging to Id Software, Inc. +// +// Should you decide to release a modified version of ACE, you MUST +// include the following text (minus the BEGIN and END lines) in the +// documentation for your modification. +// +// --- BEGIN --- +// +// The ACE Bot is a product of Steve Yeager, and is available from +// the ACE Bot homepage, at http://www.axionfx.com/ace. +// +// This program is a modification of the ACE Bot, and is therefore +// in NO WAY supported by Steve Yeager. +// +// This program MUST NOT be sold in ANY form. If you have paid for +// this product, you should contact Steve Yeager immediately, via +// the ACE Bot homepage. +// +// --- END --- +// +// I, Steve Yeager, hold no responsibility for any harm caused by the +// use of this source code, especially to small children and animals. +// It is provided as-is with no implied warranty or support. +// +// I also wish to thank and acknowledge the great work of others +// that has helped me to develop this code. +// +// John Cricket - For ideas and swapping code. +// Ryan Feltrin - For ideas and swapping code. +// SABIN - For showing how to do true client based movement. +// BotEpidemic - For keeping us up to date. +// Telefragged.com - For giving ACE a home. +// Microsoft - For giving us such a wonderful crash free OS. +// id - Need I say more. +// +// And to all the other testers, pathers, and players and people +// who I can't remember who the heck they were, but helped out. +// +/////////////////////////////////////////////////////////////////////// + + +ACEBOT II BASE +----------------------------------------------------------------------- + + I decided to release the base code for ACEBOT II for further study. +Hopefully it will provide a simple base on which to build future bots. +I think I have solved most of the basic problems associated with bots and +by using this source as a starting point, much more advanced bots can be +created. My time now is quite limited and I am not sure what I will be +doing with this source in the future. I do have plans for some interesting +"additions" to this bot, but time may not make it possible for these +to be realized. + + A number of the basic routines have been "dumbed" down from the +original ACE sourcecode and some are only put there just to make the bot +function. Even so, it still plays a pretty good game. + + I hope my comments in the source code are easy enough to read and I will +answer questions about the source as long as they are well thought out. I +will not respond to stupid or obvious questions. So if you are new to +C, I ain't gonna spend my time teaching you the basics. I've also +simplified my C usage and some routines are written for readability vs. +performance. + + If you wish to add to the base code and think others would benifit from +your addition, please email me and we can discuss it. + + All code was written and compiled using MS Visual C++ 6.0+SP1. To install +the source, create a directory named "ace" under :\quake2 (c:\quake2\ace). +Unzip the source to \quake2\ace and it will create a src directory for the +source code and a \quake2\nav directory for the node files. + + I would also like to see the work that others create using this base. +So if you create the next "killer" mod, please let me know about it. + + Good luck and happy coding! + + Steve Yeager + www.axionfx.com + syeager@axionfx.com + + + + + + diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 0fc9b0cf7..6af8008fc 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -81,6 +81,13 @@ void ACEAI_Think (edict_t *self) self->enemy = NULL; self->movetarget = NULL; + // Force respawn if during warmup mode + if (self->deadflag == DEAD_DEAD && (in_warmup)) + { + self->client->buttons = 0; + ucmd.buttons = BUTTON_ATTACK; + } + // Stop trying to think if the bot can't respawn. if( ! IS_ALIVE(self) && ((gameSettings & GS_ROUNDBASED) || (self->client->respawn_framenum > level.framenum)) ) { diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index f87fd9bd3..1844a6d06 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -140,7 +140,7 @@ qboolean ACECM_Commands(edict_t *ent) /////////////////////////////////////////////////////////////////////// // Called when the level changes, store maps and bots (disconnected) /////////////////////////////////////////////////////////////////////// -void ACECM_Store() +void ACECM_Store(void) { // Stop overwriting good node tables with bad! if( numnodes < 100 ) @@ -191,15 +191,13 @@ void debug_printf(char *fmt, ...) } -// void (*real_cprintf) (edict_t * ent, int printlevel, char *fmt, ...) = NULL; -// void (*real_centerprintf) (edict_t * ent, char *fmt, ...) = NULL; -void (*real_cprintf) (struct edict_s * ent, int printlevel, const char *fmt, ...) = NULL; -void (*real_centerprintf) (struct edict_s * ent, const char *fmt, ...) = NULL; +void (*real_cprintf) (edict_t * ent, int printlevel, const char *fmt, ...) = NULL; +void (*real_centerprintf) (edict_t * ent, const char *fmt, ...) = NULL; /////////////////////////////////////////////////////////////////////// // botsafe cprintf /////////////////////////////////////////////////////////////////////// -void safe_cprintf (struct edict_s * ent, int printlevel, const char *fmt, ...) +void safe_cprintf (edict_t *ent, int printlevel, char *fmt, ...) { char bigbuffer[0x10000]; va_list argptr; @@ -220,7 +218,7 @@ void safe_cprintf (struct edict_s * ent, int printlevel, const char *fmt, ...) /////////////////////////////////////////////////////////////////////// // botsafe centerprintf /////////////////////////////////////////////////////////////////////// -void safe_centerprintf (struct edict_s * ent, const char *fmt, ...) +void safe_centerprintf (edict_t *ent, char *fmt, ...) { char bigbuffer[0x10000]; va_list argptr; diff --git a/src/action/acesrc/acebot_items.c b/src/action/acesrc/acebot_items.c index 347a57a19..a633ffdd1 100644 --- a/src/action/acesrc/acebot_items.c +++ b/src/action/acesrc/acebot_items.c @@ -65,6 +65,7 @@ edict_t *players[MAX_CLIENTS]; // pointers to all players in the game void ACEIT_RebuildPlayerList( void ) { size_t i; + int bcount = 0; num_players = 0; @@ -76,7 +77,12 @@ void ACEIT_RebuildPlayerList( void ) players[ num_players ] = ent; num_players ++; } + + if(ent->is_bot){ + bcount++; + } } + game.bot_count = bcount; } /////////////////////////////////////////////////////////////////////// diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index 72beeeed0..e9da23941 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -69,14 +69,6 @@ int c_yes = 0, c_no = 0; #define STEPSIZE 18 -void VectorRotate2( vec3_t v, float degrees ) -{ - float radians = DEG2RAD(degrees); - float x = v[0], y = v[1]; - v[0] = x * cosf(radians) - y * sinf(radians); - v[1] = y * cosf(radians) + x * sinf(radians); -} - qboolean M_CheckBottom( edict_t *ent ) { vec3_t mins = {0,0,0}, maxs = {0,0,0}, start = {0,0,0}, stop = {0,0,0}; @@ -187,12 +179,14 @@ qboolean CanMoveSafely(edict_t *self, vec3_t angles) // create a position a distance below it VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN; + dest2[2] -= TRACE_DOWN + max( lights_camera_action, self->client->uvTime ) * 32; //BOTUT_TempLaser (trace.endpos, dest2); trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); - if( (trace.fraction == 1.0) // long drop! - || (trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA + if( (trace.fraction == 1.0) // long drop! + || (trace.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (trace.ent && (trace.ent->touch == hurt_touch)) // avoid MOD_TRIGGER_HURT + || (trace.surface && (trace.surface->flags & SURF_SKY)) ) // avoid falling onto skybox { return (false); } @@ -238,7 +232,9 @@ qboolean ACEMV_CanMove(edict_t *self, int direction) //AQ2 ADDED MASK_SOLID tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); - if(tr.fraction == 1.0 || tr.contents & (CONTENTS_LAVA|CONTENTS_SLIME)) + if( ((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self,angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch)) ) // avoid MOD_TRIGGER_HURT { if( self->last_door_time < level.framenum ) { @@ -288,7 +284,9 @@ qboolean ACEMV_CanJumpInternal(edict_t *self, int direction) //AQ2 ADDED MASK_SOLID tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); - if(tr.fraction == 1.0 || tr.contents & (CONTENTS_LAVA|CONTENTS_SLIME)) + if( ((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self,angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch)) ) // avoid MOD_TRIGGER_HURT { if( self->last_door_time < level.framenum ) { @@ -674,6 +672,8 @@ void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) Cmd_OpenDoor_f ( self ); // Open the door self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! ucmd->forwardmove = 0; + VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); + ACEMV_ChangeBotAngle( self ); return; } } @@ -688,6 +688,58 @@ void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) return; } } + else + { + // If trace from bot to next node hits rotating door, it should just strafe toward the path. + VectorCopy( self->s.origin, vStart ); + VectorCopy( nodes[self->next_node].origin, vDest ); + VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); + if( VectorLength(v) < 32 ) + { + PRETRACE(); + tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID ); + POSTTRACE(); + if( (tTrace.fraction < 1.) + && (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) + && (VectorLength(tTrace.ent->avelocity) > 0.) ) + { + if( self->next_node != self->current_node ) + { + // Decide if the bot should strafe to stay on course. + vec3_t dist = {0,0,0}; + VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); + v[2] = 0; + VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); + dist[2] = 0; + if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) + { + float left = 0; + VectorNormalize( v ); + VectorRotate2( v, 90 ); + left = DotProduct( v, dist ); + if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) + ucmd->sidemove = SPEED_RUN; + else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) + ucmd->sidemove = -SPEED_RUN; + } + } + else + { + ucmd->sidemove = 0; + if( (self->bot_strafe >= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) + ucmd->sidemove = SPEED_RUN; + else if( ACEMV_CanMove( self, MOVE_LEFT ) ) + ucmd->sidemove = -SPEED_RUN; + self->bot_strafe = ucmd->sidemove; + } + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_RUN; + VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); + ACEMV_ChangeBotAngle( self ); + return; + } + } + } } } @@ -875,6 +927,58 @@ void ACEMV_Move(edict_t *self, usercmd_t *ucmd) return; } } + else + { + // If trace from bot to next node hits rotating door, it should just strafe toward the path. + vec3_t v = {0,0,0}; + VectorCopy( self->s.origin, vStart ); + VectorCopy( nodes[self->next_node].origin, vDest ); + VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); + if( VectorLength(v) < 32 ) + { + PRETRACE(); + tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID ); + POSTTRACE(); + if( (tTrace.fraction < 1.) + && (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) + && (VectorLength(tTrace.ent->avelocity) > 0.) ) + { + if( self->next_node != self->current_node ) + { + // Decide if the bot should strafe to stay on course. + VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); + v[2] = 0; + VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); + dist[2] = 0; + if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) + { + float left = 0; + VectorNormalize( v ); + VectorRotate2( v, 90 ); + left = DotProduct( v, dist ); + if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) + ucmd->sidemove = SPEED_RUN; + else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) + ucmd->sidemove = -SPEED_RUN; + } + } + else + { + ucmd->sidemove = 0; + if( (self->bot_strafe >= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) + ucmd->sidemove = SPEED_RUN; + else if( ACEMV_CanMove( self, MOVE_LEFT ) ) + ucmd->sidemove = -SPEED_RUN; + self->bot_strafe = ucmd->sidemove; + } + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_RUN; + VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); + ACEMV_ChangeBotAngle( self ); + return; + } + } + } } //////////////////////////////////////////////////////// @@ -1418,7 +1522,7 @@ void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) c = random(); if(c < 0.6 && ACEMV_CanMove(self,MOVE_FORWARD)) ucmd->forwardmove += SPEED_RUN; - else if(c < 0.8 && ACEMV_CanMove(self,MOVE_FORWARD)) + else if(c < 0.8 && ACEMV_CanMove(self,MOVE_BACK)) ucmd->forwardmove -= SPEED_RUN; } //AQ2 END @@ -1431,7 +1535,7 @@ void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) // Randomly choose a vertical movement direction c = random(); - if(c < 0.10) //Werewolf: was 0.15 + if( (c < 0.10) && FRAMESYNC ) //Werewolf: was 0.15 { if (ACEMV_CanJump(self)) ucmd->upmove += SPEED_RUN; @@ -1564,6 +1668,10 @@ void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) else if( (self->react < 0.5f) && (dist > 300.f) ) iFactor ++; + // Shoot less accurately at moving targets. + if( VectorLength(self->enemy->velocity) > 300.f ) + iFactor ++; + target[0] += sign[0] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; target[1] += sign[1] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; target[2] += sign[2] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index a57798f3c..8788215b9 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -386,7 +386,7 @@ qboolean ACEND_FollowPath(edict_t *self) { // Failed to find a path if( debug_mode ) - gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path to %i \n", + gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path (Next: %i)\n", self->client->pers.netname, self->goal_node, self->next_node); return false; } @@ -1206,7 +1206,7 @@ void ACEND_RemoveNodeEdge(edict_t *self, int from, int to) // This function will resolve all paths that are incomplete // usually called before saving to disk /////////////////////////////////////////////////////////////////////// -void ACEND_ResolveAllPaths() +void ACEND_ResolveAllPaths(void) { int i, from, to; int num=0; @@ -1249,7 +1249,7 @@ void ACEND_ResolveAllPaths() // save out to a node file around 50-200k, so compression is not really // a big deal. /////////////////////////////////////////////////////////////////////// -void ACEND_SaveNodes() +void ACEND_SaveNodes(void) { FILE *pOut; char filename[60]; diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index eaf78d961..4175d9aa9 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -47,6 +47,7 @@ #include "acebot.h" #include "botchat.h" #include "botscan.h" +#include //AQ2 ADD #define CONFIG_FILE_VERSION 1 @@ -57,6 +58,7 @@ void AllWeapons( edict_t *ent ); void EquipClient( edict_t *ent ); char *TeamName(int team); void LTKsetBotName( char *bot_name ); +void LTKsetBotNameNew(char *bot_name); void ACEAI_Cmd_Choose( edict_t *ent, char *s); //========================================== @@ -118,7 +120,7 @@ void ACESP_JoinTeam(edict_t *ent, int desired_team) //====================================== // Using RiEvEr's new config file // -void ACESP_LoadBotConfig() +void ACESP_LoadBotConfig(void) { FILE *pIn; cvar_t *game_dir = NULL, *botdir = NULL; @@ -180,7 +182,6 @@ if (ltk_loadbots->value){ strcat(filename, ltk_botfile->string); strcat(filename, ".cfg"); #endif - // No bot file available, get out of here! if((pIn = fopen(filename, "rb" )) == NULL) { gi.dprintf("WARNING: No file containing bot data was found, no bots loaded.\n"); @@ -242,7 +243,7 @@ if (ltk_loadbots->value){ // edict_t *ACESP_SpawnBotFromConfig( char *inString ) { - edict_t *bot; + edict_t *bot = NULL; char userinfo[MAX_INFO_STRING]; int count=1; char name[32]; @@ -282,7 +283,6 @@ edict_t *ACESP_SpawnBotFromConfig( char *inString ) // NAME (parameter 1) if(count == 1 && ttype == STRLIT) { -// strncpy( name, tokenString, 32 ); strcpy( name, tokenString); continue; } @@ -332,10 +332,13 @@ edict_t *ACESP_SpawnBotFromConfig( char *inString ) Info_SetValueForKey( userinfo, "spectator", "0" ); // NOT a spectator Info_SetValueForKey( userinfo, "gender", gender ); - Q_snprintf( team_str, 2, "%i", team ); - - bot = ACESP_SpawnBot( team_str, name, modelskin, userinfo ); - + // Only spawn from config if attract mode is disabled + if (Q_stricmp(am->string, "0") == 0) { + bot = ACESP_SpawnBot( team_str, name, modelskin, userinfo ); + } else { + gi.dprintf("Warning: attract mode is enabled, I am not spawning bots from config.\n"); + } + // FIXME: This might have to happen earlier to take effect. if( bot ) { @@ -389,7 +392,7 @@ void ACESP_HoldSpawn(edict_t *self) void ACESP_PutClientInServer( edict_t *bot, qboolean respawn, int team ) { bot->is_bot = true; - + // Use 'think' to pass the value of respawn to PutClientInServer. if( ! respawn ) { @@ -418,7 +421,7 @@ void ACESP_Respawn (edict_t *self) { respawn( self ); - if( random() < 0.15) + if( random() < 0.05) { // Store current enemies available int i, counter = 0; @@ -500,12 +503,13 @@ void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team) char bot_skin[MAX_INFO_STRING]; char bot_name[MAX_INFO_STRING]; - // Set the name for the bot. - // name if( (!name) || !strlen(name) ) { // RiEvEr - new code to get random bot names - LTKsetBotName(bot_name); + if(!am_newnames->value) + LTKsetBotName(bot_name); + else + LTKsetBotNameNew(bot_name); } else strcpy(bot_name,name); @@ -518,27 +522,27 @@ void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team) if(rnd < 0.05) sprintf(bot_skin,"male/bluebeard"); else if(rnd < 0.1) - sprintf(bot_skin,"female/brianna"); + sprintf(bot_skin,"female/leeloop"); else if(rnd < 0.15) sprintf(bot_skin,"male/blues"); else if(rnd < 0.2) - sprintf(bot_skin,"female/ensign"); + sprintf(bot_skin,"female/sarah_ohconnor"); else if(rnd < 0.25) - sprintf(bot_skin,"female/jezebel"); + sprintf(bot_skin,"actionmale/chucky"); else if(rnd < 0.3) - sprintf(bot_skin,"female/jungle"); + sprintf(bot_skin,"actionmale/axef"); else if(rnd < 0.35) sprintf(bot_skin,"sas/sasurban"); else if(rnd < 0.4) sprintf(bot_skin,"terror/urbanterr"); else if(rnd < 0.45) - sprintf(bot_skin,"female/venus"); + sprintf(bot_skin,"aqmarine/urban"); else if(rnd < 0.5) sprintf(bot_skin,"sydney/sydney"); else if(rnd < 0.55) sprintf(bot_skin,"male/cajin"); else if(rnd < 0.6) - sprintf(bot_skin,"male/commando"); + sprintf(bot_skin,"aqmarine/desert"); else if(rnd < 0.65) sprintf(bot_skin,"male/grunt"); else if(rnd < 0.7) @@ -597,10 +601,12 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo bot->yaw_speed = 1000; // deg/sec // To allow bots to respawn - if( ! userinfo ) + if( ! userinfo ) { + // Classic naming method ACESP_SetName( bot, name, skin, team_str ); // includes ClientConnect - else + } else { ClientConnect( bot, userinfo ); + } ClientBeginDeathmatch( bot ); @@ -613,6 +619,23 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo team = GetNextTeamNumber(); team_str = LocalTeamNames[ team ]; } + + if(am->value) { + if(am_team->value && !teamplay->value){ + // am_team set but not teamplay + team = 0; + } + if(am_team->value){ + team = (int)am_team->value; + if ((!use_3teams->value) && (team >= TEAM3)){ + gi.dprintf("Warning: am_team was %d, but use_3teams is not enabled! Bots will default to team 1.\n", team); + gi.cvar_forceset("am_team", "1"); + team = 1; + } + } + + + } ACESP_PutClientInServer( bot, true, team ); @@ -643,7 +666,6 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo LTK_Chat( bot, myplayer[rand()%counter], DBC_WELCOME); } } - return bot; } @@ -669,7 +691,7 @@ void ACESP_RemoveBot(char *name) if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) { bot->health = 0; - player_die (bot, bot, bot, 100000, bot->s.origin); + player_die (bot, bot, bot, 100000, vec3_origin); // don't even bother waiting for death frames // bot->deadflag = DEAD_DEAD; // bot->inuse = false; @@ -705,10 +727,94 @@ void ACESP_RemoveBot(char *name) // ACESP_SaveBots(); // Save them again } +void attract_mode_bot_check(void) +{ + int maxclientsminus2 = (int)(maxclients->value - 2); + int adj = 0; + + ACEIT_RebuildPlayerList(); + // Some sanity checking before we proceed + if (am_botcount->value < 0){ + gi.cvar_forceset("am_botcount", "0"); + } + if (am_botcount->value >= maxclients->value) { + gi.cvar_forceset("am_botcount", va("%d", maxclientsminus2)); + if (warmup_bots->value){ + gi.cvar_forceset("warmup_bots", va("%d", maxclientsminus2)); + } + } + + // Cannot have the am_botcount at a value of N-2 of the maxclients + if ((am->value == 2) && (am_botcount->value >= maxclientsminus2)) + { + // If maxclients is 10 or more, set the botcount value to 6, else disable attract mode. Increase your maxclients! + if(maxclients->value >= 10){ + gi.dprintf( "am is 2, am_botcount is %d, maxclients is too low at %d, forcing it to default (6)\n", (int)am_botcount->value, (int)maxclients->value); + gi.cvar_forceset("am_botcount", "6"); + } else { + gi.dprintf( "am is 2, am_botcount is %d, maxclients is too low at %d, reducing bot count\n", (int)am_botcount->value, (int)maxclients->value); + adj = (maxclientsminus2 - 2); + gi.cvar_forceset("am_botcount", va("%d", adj)); + } + } + + int tgt_bot_count = (int)am_botcount->value; + int real_player_count = (num_players - game.bot_count); + + /* // Debug area, uncomment for gratiuitous amounts of spam + if (teamplay->value){ + gi.dprintf("Team 1: %d - Team 2: %d, - Team 3: %d\n", team1, team2, team3); + } + gi.dprintf("tgt_bot_count is %d, real_player_count is %d, num_players is %d, game.bot_count is %d\n", tgt_bot_count, real_player_count, num_players, game.bot_count); + */ // End debug area + + // Bot Maintenance + /* Logic is as follows: + If (am_botcount - real_player_count) == game.botcount + (Current bots - real players is equal to am_botcount value) + Then do nothing, we are where we want to be + + Else + + If (am_botcount - real_player_count) > game.botcount + (Current Bots + Real Players is less than the am_botcount value) + Then add a bot until these numbers are equal + + Else + + If (am_botcount - real_player_count) < game.botcount AND if am is 1 + (Current Bots + Real Players is more than the am_botcount value) + Then remove a bot until these numbers are equal + + Else + + If (total players == (maxclients - 1)) AND if am is 2 + (Current Bots + Real Players is more than the am_botcount value) + Then remove a bot only if we're near the maxclients number + + */ + + if (tgt_bot_count - real_player_count == game.bot_count) { + return; + } else if (tgt_bot_count - real_player_count > game.bot_count) { + //gi.dprintf("I'm adding a bot because %d - %d < %d", tgt_bot_count, real_player_count, game.bot_count); + ACESP_SpawnBot(NULL, NULL, NULL, NULL); + } else if ((tgt_bot_count - real_player_count < game.bot_count) && (am->value == 1)) { + // This removes 1 bot per real player + + //gi.dprintf("I'm removing a bot because %d - %d > %d", tgt_bot_count, real_player_count, game.bot_count); + ACESP_RemoveBot(""); + } else if ((num_players == maxclientsminus2) && (am->value == 2)) { + // This removes 1 bot once we are at maxclients - 2 so we have room for a real player + gi.dprintf("Removing a bot because num_players = %d and maxclients is %d", num_players, game.maxclients); + ACESP_RemoveBot(""); + } + +} + //==================================== // Stuff to generate pseudo-random names //==================================== -#define NUMNAMES 10 char *names1[NUMNAMES] = { "Bad", "Death", "L33t", "Fast", "Real", "Lethal", "Hyper", "Hard", "Angel", "Red"}; @@ -721,36 +827,79 @@ char *names3[NUMNAMES] = { char *names4[NUMNAMES] = { "ders", "rog", "born", "dor", "fing", "galad", "bon", "loss", "orch", "riel" }; -qboolean nameused[NUMNAMES][NUMNAMES]; - //==================================== -// AQ2World Staff Names -- come shoot at us! -// Find time to implement this! Or better yet, -// load names from a file rather than this array +// AQ2World Staff Names -- come shoot at our bots! +// TODO: Find time to implement this better //==================================== -// #define AQ2WORLDNUMNAMES 14 -// char *aq2names[AQ2WORLDNUMNAMES] = { -// "bAron", "darksaint", "FragBait", "matic", "stan0x", "TgT", "dmc", "dox", "KaniZ", "keffo", "QuimBy", "Rezet", "Royce", "vrol" -// }; -//qboolean adminnameused[AQ2WORLDNUMNAMES]; +char *aq2names[] = { + "[BOT]bAron", "[BOT]darksaint", "[BOT]FragBait", + "[BOT]matic", "[BOT]JukS", "[BOT]TgT", "[BOT]dmc", + "[BOT]dox", "[BOT]KaniZ", "[BOT]keffo", "[BOT]QuimBy" + + "Rezet", "Royce", "vrol", "mikota", + "Reki", "ReKTeK", "Ralle", "Tech", + + "-ROBO-JukS", "-ROBO-Nevi", "-ROBO-topdeck", + "-ROBO-dmc", "-ROBO-Raptor007", "-ROBO-Ferrick", + + "Igor[ROCK].bot", "Suislide.bot", "Bartender.bot", + "Fex.bot", "Shagg.bot", "Black Angel.bot", "Rookie.bot", + + "Fireblade>beep", "Cail>beep", "Gooseman>beep", "Ace12GA>beep", + "BlackMonk>beep", "hal9k>beep", "Fool Killer>beep", "Inghaw>beep", + + "_NME_GreyDeath", "_NME_Ellusion", "_NME_Deathwatch", + "_NME_Freud", "_NME_slicer", "_NME_JBravo", "_NME_Elviz" + }; + // END AQ2World Staff Names // +// New AQ2World team bot names (am_newnames 1) +void LTKsetBotNameNew(char *bot_name) +{ + edict_t ltknames; + int randomname = 0; + // /* TODO: Free up previously used names to be reused. + // This may cause duplicates in-game though. + // Fix this at some point. Otherwise the game + // runs out of valid values and will softlock + // when generating LTK bots. + + do + { + randomname = rand() % AQ2WTEAMSIZE; + if (!ltknames.newnameused[randomname]) + { + ltknames.newnameused[randomname] = true; + break; + } + } while (ltknames.newnameused[randomname]); + + strcpy(bot_name, aq2names[randomname]); + + return; +} + //==================================== -// New random bot naming routine +// Classic random bot naming routine //==================================== void LTKsetBotName( char *bot_name ) { int part1,part2; part1 = part2 = 0; + edict_t ltknames; - do // Load random bot names from NUMNAMES lists + do { part1 = rand()% NUMNAMES; part2 = rand()% NUMNAMES; - }while( nameused[part1][part2]); + }while( ltknames.nameused[part1][part2]); // Mark that name as used - nameused[part1][part2] = true; + // TODO: This is causing crashes, figure out another way to mark them as used + + ltknames.nameused[part1][part2] = true; + // Now put the name together if( random() < 0.5 ) { @@ -764,18 +913,15 @@ void LTKsetBotName( char *bot_name ) } } -int GetBotCount(void) -{ - int count = 0; - edict_t *pers = NULL; - // Count bots - for (int i = 0; i < game.maxclients; i++) - { - if (pers->is_bot) - { - count++; +void LTKClearBotNames(void) { + edict_t ltknames; + int i, j; + for (i = 0; i < NUMNAMES; i++) { + for (j = 0; j < NUMNAMES; j++) { + ltknames.nameused[i][j] = false; // Reset all elements to false } } - return count; -} - + for (i = 0; i < AQ2WTEAMSIZE; i++) { + ltknames.newnameused[i] = false; // Reset all elements to false + } +} \ No newline at end of file diff --git a/src/action/acesrc/bot_ai.c b/src/action/acesrc/bot_ai.c index 85934a13b..145f096a6 100644 --- a/src/action/acesrc/bot_ai.c +++ b/src/action/acesrc/bot_ai.c @@ -370,7 +370,7 @@ void BOTAI_Think(edict_t *bot) memset (&cmd, 0, sizeof(usercmd_t)); // Stop trying to think if the bot can't respawn. - if( ! IS_ALIVE(bot) && ((gameSettings & GS_ROUNDBASED) || (bot->client->respawn_framenum > level.framenum)) ) + if( (!esp->value) || (! IS_ALIVE(bot) && ((gameSettings & GS_ROUNDBASED) || (bot->client->respawn_framenum > level.framenum))) ) goto LeaveThink; bs = bot->botState; diff --git a/src/action/acesrc/botchat.c b/src/action/acesrc/botchat.c index 972dd9eac..ec5919b25 100644 --- a/src/action/acesrc/botchat.c +++ b/src/action/acesrc/botchat.c @@ -32,11 +32,11 @@ char *ltk_welcomes[DBC_WELCOMES] = "Give your arse a rest, %s. try talking through your mouth", "Damn! I hoped Thresh would be here...", "Hey Mom, they gave me a gun!", - "I gotta find a better server...", "Hey, any of you guys played before?", "Hi %s, I wondered if you'd show your face around here again.", "Nice :-) :-) :-)", "OK %s, let's get this over with" + "SUG BALLE SUG BALLE SUG BALLE" }; #define DBC_KILLEDS 13 @@ -44,7 +44,7 @@ char *ltk_killeds[DBC_KILLEDS] = { "B*stard! %s messed up my hair.", "All right, %s. Now I'm feeling put out!", - "Hey! Go easy on me! I'm a newbie!", + "Why you little!", "Ooooh, %s, that smarts!", "%s's mother cooks socks in hell!", "Hey, %s, how about a match - your face and my arse!", @@ -79,26 +79,69 @@ char *ltk_insults[DBC_INSULTS] = }; + +#define AQTION_KILLDS 8 +char *aqtion_killeds[AQTION_KILLDS] = +{ + "SUG BALLE SUG BALLE" + "hahahaha", + "lol", + "Try using a real gun, %s!", + "rofl", + "you are getting better, I think", + "nice shot", + "scared me half to death, %s" +}; + +#define AQTION_INSULTS 8 +char *aqtion_insults[AQTION_INSULTS] = +{ + "Check out the Discord server! --> https://discord.aq2world.com <--", + "Have you visited the forums recently? --> https://forums.aq2world.com <--", + "no achievement for u!", + "%s is probably still using a ball mouse lol", + "well that was unforunate", + "gottem", + "peek a boooo!", + "meh!" +}; + + void LTK_Chat (edict_t *bot, edict_t *object, int speech) { char final[150]; char *text = NULL; - if ((!object) || (!object->client)) - return; - - if (speech == DBC_WELCOME) - text = ltk_welcomes[rand()%DBC_WELCOMES]; - else if (speech == DBC_KILLED) - text = ltk_killeds[rand()%DBC_KILLEDS]; - else if (speech == DBC_INSULT) - text = ltk_insults[rand()%DBC_INSULTS]; - else - { - if( debug_mode ) - gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); - return; - } + if ((!object) || (!object->client)) { + return; + } + + if(!ltk_classic->value){ + if (speech == DBC_WELCOME) { + text = ltk_welcomes[rand()%DBC_WELCOMES]; + } else if (speech == DBC_KILLED) { + text = ltk_killeds[rand()%DBC_KILLEDS]; + } else if (speech == DBC_INSULT) { + text = ltk_insults[rand()%DBC_INSULTS]; + } else { + if( debug_mode ) + gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); + return; + } + } else { + if (speech == DBC_WELCOME) + return; + else if (speech == DBC_KILLED) + text = aqtion_killeds[rand()%AQTION_KILLDS]; + else if (speech == DBC_INSULT) + text = aqtion_insults[rand()%AQTION_INSULTS]; + else + { + if( debug_mode ) + gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); + return; + } + } sprintf (final, text, object->client->pers.netname); diff --git a/src/action/cgf_sfx_glass.c b/src/action/cgf_sfx_glass.c index 72aa2f3d7..23ca4490a 100644 --- a/src/action/cgf_sfx_glass.c +++ b/src/action/cgf_sfx_glass.c @@ -69,7 +69,7 @@ extern "C" // emits glass fragments from aPoint, to show effects of firing thru window void CGF_SFX_BreakGlass (edict_t * aGlassPane, edict_t * anOther, - edict_t * anAttacker, int aDamage, const vec3_t aPoint, + edict_t * anAttacker, int aDamage, vec3_t aPoint, vec_t aPaneDestructDelay); // breaks glass @@ -452,7 +452,7 @@ CGF_SFX_TouchGlass (edict_t * self, edict_t * other, cplane_t * plane, void CGF_SFX_BreakGlass (edict_t * aGlassPane, edict_t * anInflictor, - edict_t * anAttacker, int aDamage, const vec3_t aPoint, + edict_t * anAttacker, int aDamage, vec3_t aPoint, vec_t aPaneDestructDelay) { // based on func_explode, but with lotsa subtle differences diff --git a/src/action/g_chase.c b/src/action/g_chase.c index 5bec42153..7d808e439 100644 --- a/src/action/g_chase.c +++ b/src/action/g_chase.c @@ -395,6 +395,13 @@ void GetChaseTarget( edict_t * ent ) continue; if (limchase && ent->client->resp.team != e->client->resp.team) continue; + // Slightly convoluted here, do not let player chase enemy leader + + //gi.dprintf("team: %i, enemy team: %i", ent->client->resp.team, teams[e->client->resp.team].leader->client->resp.team); + // TODO: Fix this + // if (esp->value && ent->client->resp.team != teams[e->client->resp.team].leader->client->resp.team) { + // continue; + // } SetChase( ent, e ); return; @@ -402,3 +409,35 @@ void GetChaseTarget( edict_t * ent ) //gi.cprintf(ent, PRINT_HIGH, "No players to chase.\n"); } + +void TeamplayChaseCam(edict_t *self, edict_t *attacker) +{ + if ((limchasecam->value < 2 && attacker && attacker->client) && attacker != NULL) { + self->client->resp.last_chase_target = attacker; // Always reset chase to killer + } else { + self->client->resp.last_chase_target = NULL; + } +} + +void EspionageChaseCam(edict_t *self, edict_t *attacker) +{ + edict_t *last_target = self->client->resp.last_chase_target; + edict_t *team_leader = teams[self->client->resp.team].leader; + int player_team = self->client->resp.team; + + // ATL is simple, chase your own leader + if (atl->value && team_leader != NULL) { + if (IS_ALIVE(team_leader)) + last_target = team_leader; // Chase cam your own leader + else + last_target = NULL; // If you can't chase your own leader, the round is over anyway + } else { // ETV is more difficult, only one team has a leader + if (player_team == TEAM1 && team_leader != NULL) + last_target = team_leader; // Chase cam your own leader + else if (player_team != TEAM1 || team_leader == NULL) + last_target = NULL; + } + + // Cast to (void) to avoid compiler warning because this will only ever be called in Espionage mode + (void)last_target; +} \ No newline at end of file diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index e6911d81f..a30c911e7 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -727,7 +727,7 @@ static void Cmd_Notarget_f (edict_t * ent) else msg = "notarget ON\n"; - gi.cprintf (ent, PRINT_HIGH, "%s", msg); + gi.cprintf (ent, PRINT_HIGH, msg); } @@ -738,7 +738,7 @@ Cmd_Noclip_f argv(0) noclip ================== */ -static void Cmd_Noclip_f (edict_t * ent) +void Cmd_Noclip_f (edict_t * ent) { char *msg; @@ -753,7 +753,7 @@ static void Cmd_Noclip_f (edict_t * ent) msg = "noclip ON\n"; } - gi.cprintf (ent, PRINT_HIGH, "%s", msg); + gi.cprintf (ent, PRINT_HIGH, msg); } @@ -946,6 +946,12 @@ void Cmd_Inven_f (edict_t * ent) cl->pers.menu_shown = true; + // PaTMaN's Jmod menu support + if (jump->value) { + OpenPMItemMenu (ent); + return; + } + if (teamplay->value && !ent->client->resp.team) { OpenJoinMenu (ent); return; @@ -1585,9 +1591,9 @@ static void dmflagsSettings( char *s, size_t size, int flags ) if (flags & DF_NO_FRIENDLY_FIRE) Q_strncatz( s, "256 = no ff ", size ); if (flags & DF_SPAWN_FARTHEST) - Q_strncatz( s, "512 = spawn fartherst ", size ); + Q_strncatz( s, "512 = spawn farthest ", size ); if (flags & DF_FORCE_RESPAWN) - Q_strncatz( s, "1024 = forse respawn ", size ); + Q_strncatz( s, "1024 = force respawn ", size ); //if(flags & DF_NO_ARMOR) // Q_strncatz(s, "2048 = no armor ", size); if (flags & DF_ALLOW_EXIT) @@ -1674,8 +1680,8 @@ static void Cmd_PrintSettings_f( edict_t * ent ) length = strlen( text ); } - Q_snprintf( text + length, sizeof( text ) - length, "sv_antilag = %d\n", (int)sv_antilag->value ); - length = strlen( text ); + Q_snprintf( text + length, sizeof( text ) - length, "sv_antilag = %d\n", (int)sv_antilag->value ); + length = strlen( text ); Q_snprintf( text + length, sizeof( text ) - length, "dmflags %i: ", (int)dmflags->value ); dmflagsSettings( text, sizeof( text ), (int)dmflags->value ); @@ -1689,15 +1695,25 @@ static void Cmd_PrintSettings_f( edict_t * ent ) itmflagsSettings( text, sizeof( text ), (int)itm_flags->value ); length = strlen( text ); + #ifdef USE_AQTION Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" - "limchasecam %2d tgren %2d hc_single %2d\n" - "use_punch %2d use_classic %2d\n", + "limchasecam %2d tgren %2d antilag_interp %2d\n" + "use_xerp %2d llsound %2d\n", (int)timelimit->value, (int)roundlimit->value, (int)roundtimelimit->value, - (int)limchasecam->value, (int)tgren->value, (int)hc_single->value, - (int)use_punch->value, (int)use_classic->value ); + (int)limchasecam->value, (int)tgren->value, (int)sv_antilag_interp->value, + (int)use_xerp->value, (int)llsound->value ); + #else + Q_snprintf( text + length, sizeof( text ) - length, "\n" + "timelimit %2d roundlimit %2d roundtimelimit %2d\n" + "limchasecam %2d tgren %2d antilag_interp %2d\n" + "use_xerp %2d llsound %2d\n", + (int)timelimit->value, (int)roundlimit->value, (int)roundtimelimit->value, + (int)limchasecam->value, (int)tgren->value, (int)sv_antilag_interp->value, + (int)llsound->value ); + #endif - gi.cprintf( ent, PRINT_HIGH, "%s", text ); + gi.cprintf( ent, PRINT_HIGH, text ); } static void Cmd_Follow_f( edict_t *ent ) @@ -1806,7 +1822,8 @@ static void Cmd_CPSI_f (edict_t * ent) ent->client->resp.gllockpvs = atoi(gi.argv(2)); ent->client->resp.glclear = atoi(gi.argv(3)); ent->client->resp.gldynamic = atoi(gi.argv(4)); - Q_strncpyz(ent->client->resp.gldriver, gi.argv (5), sizeof(ent->client->resp.gldriver)); + ent->client->resp.glbrightness = atof(gi.argv(5)); + Q_strncpyz(ent->client->resp.gldriver, gi.argv (6), sizeof(ent->client->resp.gldriver)); // strncpy(ent->client->resp.vidref,gi.argv(4),sizeof(ent->client->resp.vidref-1)); // ent->client->resp.vidref[15] = 0; } @@ -1930,8 +1947,12 @@ static cmdList_t commandList[] = { "voteconfig", Cmd_Voteconfig_f, 0 }, { "configlist", Cmd_Configlist_f, 0 }, { "votescramble", Cmd_Votescramble_f, 0 }, - // JumpMod - { "jmod", Cmd_Jmod_f, 0 } + { "printrules", Cmd_PrintRules_f, 0}, + // JumpMod / jmod -- all commands are prefaced with 'jmod' ex: 'jmod spawnc' + { "jmod", Cmd_Jmod_f, 0 }, + // Espionage, aliased command so it's easy to remember + { "volunteer", Cmd_Volunteer_f, 0}, + { "leader", Cmd_Volunteer_f, 0} }; #define MAX_COMMAND_HASH 64 @@ -1996,6 +2017,11 @@ void ClientCommand (edict_t * ent) hash = Cmd_HashValue( text ) & (MAX_COMMAND_HASH - 1); for (cmd = commandHash[hash]; cmd; cmd = cmd->hashNext) { if (!Q_stricmp( text, cmd->name )) { + // if ((cmd->flags & CMDF_JMOD) && !jump->value) { + // gi.cprintf(ent, PRINT_HIGH, "You must run the server with '+set jump 1' to enable this command.\n"); + // return; + // } + if ((cmd->flags & CMDF_CHEAT) && !sv_cheats->value) { gi.cprintf(ent, PRINT_HIGH, "You must run the server with '+set cheats 1' to enable this command.\n"); return; @@ -2004,6 +2030,8 @@ void ClientCommand (edict_t * ent) if ((cmd->flags & CMDF_PAUSE) && level.pauseFrames) return; + + cmd->function( ent ); return; } diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 2eb1533bf..cd0c2737e 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -202,7 +202,8 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) Killed ============ */ -void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, int damage, vec3_t point) +void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, int damage, + const vec3_t point) { if (targ->health < -999) targ->health = -999; @@ -396,7 +397,7 @@ void spray_sniper_blood(edict_t *self, vec3_t start, const vec3_t dir) spray_blood( self, start, dir, 0, mod ); } -void VerifyHeadShot(vec3_t point, const vec3_t dir, float height, vec3_t newpoint) +void VerifyHeadShot(vec3_t point, vec3_t dir, float height, vec3_t newpoint) { vec3_t normdir; @@ -413,7 +414,7 @@ void VerifyHeadShot(vec3_t point, const vec3_t dir, float height, vec3_t newpoin #define HEAD_HEIGHT 12.0f void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const vec3_t dir, - vec3_t point, const vec3_t normal, int damage, int knockback, int dflags, + const vec3_t point, const vec3_t normal, int damage, int knockback, int dflags, int mod) { gclient_t *client; @@ -442,8 +443,9 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve //FIREBLADE if (mod != MOD_TELEFRAG) { - if (lights_camera_action) + if (lights_camera_action) { return; + } if (client) { @@ -538,28 +540,8 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if(attacker->client->resp.streakHS % 3 == 0) { - if (use_rewards->value) - { - Q_snprintf(buf, sizeof(buf), "ACCURACY %s!", attacker->client->pers.netname); - CenterPrintAll(buf); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/accuracy.wav"), 1.0, ATTN_NONE, 0.0); - - #if USE_AQTION - - #ifndef NO_BOTS - // Check if there's an AI bot in the game, if so, do nothing - if (game.ai_ent_found) { - return; - } - #endif - if (stat_logs->value) { - char steamid[24]; - char discordid[24]; - Q_strncpyz(steamid, Info_ValueForKey(attacker->client->pers.userinfo, "steamid"), sizeof(steamid)); - Q_strncpyz(discordid, Info_ValueForKey(attacker->client->pers.userinfo, "cl_discord_id"), sizeof(discordid)); - LogAward(steamid, discordid, ACCURACY); - } - #endif + if (use_rewards->value) { + Announce_Reward(attacker, ACCURACY); } } } @@ -904,9 +886,14 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve { if (!friendlyFire && !in_warmup) { attacker->client->resp.damage_dealt += damage; + // All normal weapon damage if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; } + // Grenade splash, kicks and punch damage + if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH))) { + attacker->client->resp.gunstats[mod].damage += damage; + } } client->attacker = attacker; diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 962881a49..1af961076 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -37,16 +37,18 @@ extension_func_t *g_extension_funcs; int(*engine_Client_GetVersion)(edict_t *ent); int(*engine_Client_GetProtocol)(edict_t *ent); -void(*engine_Ghud_SendUpdates)(edict_t *ent); -int(*engine_Ghud_NewElement)(int type); -void(*engine_Ghud_SetFlags)(int i, int val); -void(*engine_Ghud_UnicastSetFlags)(edict_t *ent, int i, int val); -void(*engine_Ghud_SetInt)(int i, int val); -void(*engine_Ghud_SetText)(int i, char *text); -void(*engine_Ghud_SetPosition)(int i, int x, int y, int z); -void(*engine_Ghud_SetAnchor)(int i, float x, float y); -void(*engine_Ghud_SetColor)(int i, int r, int g, int b, int a); -void(*engine_Ghud_SetSize)(int i, int x, int y); +void(*engine_Ghud_ClearForClient)(edict_t *ent); +int(*engine_Ghud_NewElement)(edict_t *ent, int type); +void(*engine_Ghud_RemoveElement)(edict_t *ent, int i); +void(*engine_Ghud_SetFlags)(edict_t *ent, int i, int val); +void(*engine_Ghud_SetInt)(edict_t *ent, int i, int val); +void(*engine_Ghud_SetText)(edict_t *ent, int i, char *text); +void(*engine_Ghud_SetPosition)(edict_t *ent, int i, int x, int y, int z); +void(*engine_Ghud_SetAnchor)(edict_t *ent, int i, float x, float y); +void(*engine_Ghud_SetColor)(edict_t *ent, int i, int r, int g, int b, int a); +void(*engine_Ghud_SetSize)(edict_t *ent, int i, int x, int y); + +void(*engine_CvarSync_Set)(int index, const char *name, const char *val); // // optional new entrypoints the engine may want to call @@ -56,80 +58,232 @@ trace_t q_gameabi XERP_trace(vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end) return gi.trace(start, mins, maxs, end, xerp_ent, MASK_PLAYERSOLID); } -int G_customizeentityforclient(edict_t *client, edict_t *ent, entity_state_t *state) +int G_customizeentityforclient(edict_t *clent, edict_t *ent, entity_state_t *state) { + if (!clent) // don't touch mvds + { + if (!strcmp(ent->classname, "ind_arrow")) + return false; + return true; + } + // first check visibility masks - if (!(max(1, ent->dimension_visible) & max(1, client->client->dimension_observe))) + if (!(max(1, ent->dimension_visible) & max(1, clent->client->dimension_observe))) return false; - // extrapolation, if we want that kind of thing, which we pretty much don't because antilag exists. - // this could be used in future if antilag is disabled. -#if 0 - if (ent->client) // extrapolate clients + if (ent->client) // client specific changes + { + if ((int)use_newirvision->value) + { + // don't show teammates in irvision + if (teamplay->value && (ent->client->resp.team == clent->client->resp.team)) + state->renderfx &= ~RF_IR_VISIBLE; + } + } + + if (!strcmp(ent->classname, "ind_arrow")) { - if (sv_antilag_interp->value) // don't extrapolate when our antilag accounts for lerp - return true; + if (clent == ent->owner || ent->owner == clent->client->chase_target) + return false; + + if (!clent->client->pers.cl_indicators) // never show if client doesn't want them + return false; + + if (!teamplay->value) // don't use indicators in non-teamplay + return false; + + // Espionage allows indicators for leaders if set to 2 + if (esp->value && use_indicators->value == 2 && esp_showleader->value && clent->client->resp.team) { + // Quad is a blue glow, pent is a red glow + if (clent->client->resp.team == TEAM1 && IS_LEADER(clent)) + ent->s.effects = EF_PENT; + else if (clent->client->resp.team == TEAM2 && IS_LEADER(clent)) + ent->s.effects = EF_QUAD; + } + if ((use_indicators->value == 2 && clent->client->resp.team) || (clent->client->resp.team && clent->client->pers.cl_indicators != 2)) // disallow indicators for players in use_indicators 2, and don't use them for players unless cl_indicators 2 + return false; - float xerp_amount = FRAMETIME; - if (!sv_antilag->value) // we don't add extra xerp with antilag - xerp_amount += client->client->ping / 1000; + trace_t trace; + vec3_t view_pos, arrow_pos, vnull; + VectorClear(vnull); - pmove_t pm; - xerp_ent = ent; - memcpy(&pm.s, &ent->client->ps.pmove, sizeof(pmove_state_t)); + VectorCopy(ent->owner->s.origin, arrow_pos); + arrow_pos[2] += ent->owner->maxs[2] - 4; - pm.trace = XERP_trace; - pm.pointcontents = gi.pointcontents; - pm.s.pm_type = PM_NORMAL; + VectorCopy(clent->s.origin, view_pos); + view_pos[2] += clent->viewheight; + + trace = gi.trace(view_pos, vnull, vnull, arrow_pos, ent->owner, CONTENTS_SOLID); + if (trace.fraction < 1) + return false; - pm.cmd = ent->client->cmd_last; // just assume client is gonna keep pressing the same buttons for next frame - // we need to reign in A/D spamming at low velocities - if ((VectorLength(pm.s.velocity)/8) < 280) - { - pm.cmd.sidemove = 0; - pm.cmd.forwardmove = 0; - } + VectorCopy(clent->client->v_angle, state->angles); + } - pm.cmd.msec = xerp_amount * 1000; + // objective arrow in Espionage + if (!strcmp(ent->classname, "ind_arrow_objective")) + { + if (clent == ent->owner || ent->owner == clent->client->chase_target) // if the objective marker is on the viewer, don't send it + return false; + + if (!clent->client->pers.cl_indicators) // never show if client doesn't want them + return false; + + if (use_indicators->value <= 0) // if the server disallows them, don't send + return false; - gi.Pmove(&pm); + trace_t trace; + vec3_t view_pos, arrow_pos, vnull; + VectorClear(vnull); - VectorScale(pm.s.origin, 0.125, state->origin); + VectorCopy(ent->owner->s.origin, arrow_pos); + arrow_pos[2] += ent->owner->maxs[2] - 4; + + VectorCopy(clent->s.origin, view_pos); + view_pos[2] += clent->viewheight; + + //if (0) // this stops it from being show through walls + //{ + trace = gi.trace(view_pos, vnull, vnull, arrow_pos, ent->owner, CONTENTS_SOLID); + if (trace.fraction < 1) + return false; + //} + + VectorCopy(clent->client->v_angle, state->angles); } - else // extrapolate other physics entities + + // extrapolation, if we want that kind of thing (client and server both want it, client not a spectator) + if (clent->client->pers.cl_xerp && use_xerp->value && clent->solid != SOLID_NOT) { - if (ent->movetype) + if (ent->client) // extrapolate clients { - vec3_t start, end, velocity; - VectorCopy(state->origin, start); - VectorCopy(ent->velocity, velocity); + if (sv_antilag_interp->value) // don't extrapolate when our antilag accounts for lerp + return true; + + float xerp_amount = FRAMETIME; + if (!sv_antilag->value) // we don't add extra xerp with antilag + xerp_amount += clent->client->ping / 1000; + + if (clent->client->pers.cl_xerp == 2) + xerp_amount = min(xerp_amount, FRAMETIME/2); + else + xerp_amount = min(xerp_amount, 0.25); - switch (ent->movetype) + pmove_t pm; + xerp_ent = ent; + memcpy(&pm.s, &ent->client->ps.pmove, sizeof(pmove_state_t)); + + pm.trace = XERP_trace; + pm.pointcontents = gi.pointcontents; + pm.s.pm_type = PM_NORMAL; + + pm.cmd = ent->client->cmd_last; // just assume client is gonna keep pressing the same buttons for next frame + // we need to reign in A/D spamming at low velocities + if ((VectorLength(pm.s.velocity)/8) < 280) { - case MOVETYPE_BOUNCE: - case MOVETYPE_TOSS: - velocity[2] -= ent->gravity * sv_gravity->value * FRAMETIME; - case MOVETYPE_FLY: - VectorMA(start, FRAMETIME, velocity, end); - break; - default: - return true; + pm.cmd.sidemove = 0; + pm.cmd.forwardmove = 0; } - trace_t trace; - trace = gi.trace(start, ent->mins, ent->maxs, end, ent, ent->clipmask ? ent->clipmask : MASK_SOLID); - VectorCopy(trace.endpos, state->origin); + pm.cmd.msec = xerp_amount * 1000; + + gi.Pmove(&pm); + + VectorScale(pm.s.origin, 0.125, state->origin); + } + else // extrapolate other physics entities + { + if (ent->movetype) + { + float xerp_amount = FRAMETIME; + xerp_amount += clent->client->ping / 1000; + + if (clent->client->pers.cl_xerp == 2) + xerp_amount = min(xerp_amount, FRAMETIME / 2); + else + xerp_amount = min(xerp_amount, 0.25); + + vec3_t start, end, velocity; + VectorCopy(state->origin, start); + VectorCopy(ent->velocity, velocity); + + switch (ent->movetype) + { + case MOVETYPE_BOUNCE: + case MOVETYPE_TOSS: + velocity[2] -= ent->gravity * sv_gravity->value * xerp_amount; + case MOVETYPE_FLY: + VectorMA(start, xerp_amount, velocity, end); + break; + default: + return true; + } + + trace_t trace; + trace = gi.trace(start, ent->mins, ent->maxs, end, ent, ent->clipmask ? ent->clipmask : MASK_SOLID); + VectorCopy(trace.endpos, state->origin); + } } } -#endif return true; } +void G_CvarSync_Updated(int index, edict_t *clent) +{ + gclient_t *client = clent->client; + + if (!client) // uh... this shouldn't happen + return; + + const char *val = client->cl_cvar[index]; + int val_i = atoi(val); + int val_f = atof(val); + + switch (index) + { + case clcvar_cl_antilag:; + int antilag_value = client->pers.antilag_optout; + if (atoi(val) > 0) + client->pers.antilag_optout = qfalse; + else if (atoi(val) <= 0) + client->pers.antilag_optout = qtrue; + + if (sv_antilag->value && antilag_value != client->pers.antilag_optout) + gi.cprintf(clent, PRINT_MEDIUM, "YOUR CL_ANTILAG IS NOW SET TO %i\n", !client->pers.antilag_optout); + break; + + case clcvar_cl_xerp: + client->pers.cl_xerp = val_i; + break; + + case clcvar_cl_indicators: + client->pers.cl_indicators = val_i; + break; + + case clcvar_cl_spectatorhud: + if (val_i) + client->pers.spec_flags |= (SPECFL_SPECHUD | SPECFL_SPECHUD_NEW); + else + client->pers.spec_flags &= ~(SPECFL_SPECHUD | SPECFL_SPECHUD_NEW); + + if (Client_GetProtocol(clent) >= 38) + client->pers.spec_flags &= ~SPECFL_SPECHUD; // don't use old spec hud + break; + + case clcvar_cl_spectatorkillfeed: + if (val_i) + client->pers.spec_flags |= SPECFL_KILLFEED; + else + client->pers.spec_flags &= ~SPECFL_KILLFEED; + break; + } +} void G_InitExtEntrypoints(void) { g_addextension("customizeentityforclient", G_customizeentityforclient); + g_addextension("CvarSync_Updated", G_CvarSync_Updated); } @@ -182,92 +336,92 @@ int Client_GetProtocol(edict_t *ent) // // game hud // -void Ghud_SendUpdates(edict_t *ent) +void Ghud_ClearForClient(edict_t *ent) { - if (!engine_Ghud_SendUpdates) + if (!engine_Ghud_ClearForClient) return; - engine_Ghud_SendUpdates(ent); + engine_Ghud_ClearForClient(ent); } -int Ghud_NewElement(int type) +int Ghud_NewElement(edict_t *ent, int type) { if (!engine_Ghud_NewElement) return 0; - return engine_Ghud_NewElement(type); + return engine_Ghud_NewElement(ent, type); } -void Ghud_SetFlags(int i, int val) +void Ghud_RemoveElement(edict_t *ent, int i) { - if (!engine_Ghud_SetFlags) + if (!engine_Ghud_RemoveElement) return; - engine_Ghud_SetFlags(i, val); + engine_Ghud_RemoveElement(ent, i); } -void Ghud_UnicastSetFlags(edict_t *ent, int i, int val) +void Ghud_SetFlags(edict_t *ent, int i, int val) { - if (!engine_Ghud_UnicastSetFlags) + if (!engine_Ghud_SetFlags) return; - engine_Ghud_UnicastSetFlags(ent, i, val); + engine_Ghud_SetFlags(ent, i, val); } -void Ghud_SetInt(int i, int val) +void Ghud_SetInt(edict_t *ent, int i, int val) { if (!engine_Ghud_SetInt) return; - engine_Ghud_SetInt(i, val); + engine_Ghud_SetInt(ent, i, val); } -void Ghud_SetText(int i, char *text) +void Ghud_SetText(edict_t *ent, int i, char *text) { if (!engine_Ghud_SetText) return; - engine_Ghud_SetText(i, text); + engine_Ghud_SetText(ent, i, text); } -void Ghud_SetPosition(int i, int x, int y) +void Ghud_SetPosition(edict_t *ent, int i, int x, int y) { if (!engine_Ghud_SetPosition) return; - engine_Ghud_SetPosition(i, x, y, 0); + engine_Ghud_SetPosition(ent, i, x, y, 0); } -void Ghud_SetPosition3D(int i, int x, int y, int z) +void Ghud_SetPosition3D(edict_t *ent, int i, int x, int y, int z) { if (!engine_Ghud_SetPosition) return; - engine_Ghud_SetPosition(i, x, y, z); + engine_Ghud_SetPosition(ent, i, x, y, z); } -void Ghud_SetSize(int i, int x, int y) +void Ghud_SetSize(edict_t *ent, int i, int x, int y) { if (!engine_Ghud_SetSize) return; - engine_Ghud_SetSize(i, x, y); + engine_Ghud_SetSize(ent, i, x, y); } -void Ghud_SetAnchor(int i, float x, float y) +void Ghud_SetAnchor(edict_t *ent, int i, float x, float y) { if (!engine_Ghud_SetAnchor) return; - engine_Ghud_SetAnchor(i, x, y); + engine_Ghud_SetAnchor(ent, i, x, y); } -void Ghud_SetColor(int i, int r, int g, int b, int a) +void Ghud_SetColor(edict_t *ent, int i, int r, int g, int b, int a) { if (!engine_Ghud_SetColor) return; - engine_Ghud_SetColor(i, r, g, b, a); + engine_Ghud_SetColor(ent, i, r, g, b, a); } @@ -275,34 +429,46 @@ void Ghud_SetColor(int i, int r, int g, int b, int a) // game hud abstractions, // makes code a bit cleaner // -int Ghud_AddIcon(int x, int y, int image, int sizex, int sizey) +int Ghud_AddIcon(edict_t *ent, int x, int y, int image, int sizex, int sizey) { - int index = Ghud_NewElement(GHT_IMG); - Ghud_SetPosition(index, x, y); - Ghud_SetSize(index, sizex, sizey); - Ghud_SetInt(index, image); + int index = Ghud_NewElement(ent, GHT_IMG); + Ghud_SetPosition(ent, index, x, y); + Ghud_SetSize(ent, index, sizex, sizey); + Ghud_SetInt(ent, index, image); return index; } -int Ghud_AddText(int x, int y, char *text) +int Ghud_AddText(edict_t *ent, int x, int y, char *text) { - int index = Ghud_NewElement(GHT_TEXT); - Ghud_SetPosition(index, x, y); - Ghud_SetText(index, text); + int index = Ghud_NewElement(ent, GHT_TEXT); + Ghud_SetPosition(ent, index, x, y); + Ghud_SetText(ent, index, text); return index; } +void Ghud_SetTextFlags(edict_t *ent, int i, int uiflags) +{ + Ghud_SetSize(ent, i, (uiflags & 0xFFFF), (uiflags >> 16) & 0xFFFF); +} -int Ghud_AddNumber(int x, int y, int value) + +int Ghud_AddNumber(edict_t *ent, int x, int y, int value) { - int index = Ghud_NewElement(GHT_NUM); - Ghud_SetPosition(index, x, y); - Ghud_SetInt(index, value); + int index = Ghud_NewElement(ent, GHT_NUM); + Ghud_SetPosition(ent, index, x, y); + Ghud_SetInt(ent, index, value); return index; } +void CvarSync_Set(int index, const char *name, const char *val) +{ + if (!engine_CvarSync_Set) + return; + + engine_CvarSync_Set(index, name, val); +} #endif \ No newline at end of file diff --git a/src/action/g_func.c b/src/action/g_func.c index 2fe0286ba..9223b9423 100644 --- a/src/action/g_func.c +++ b/src/action/g_func.c @@ -855,7 +855,7 @@ static void button_touch(edict_t *self, edict_t *other, cplane_t *plane, csurfac button_fire(self); } -static void button_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +static void button_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) { self->activator = attacker; self->health = self->max_health; @@ -1227,7 +1227,7 @@ static void door_blocked(edict_t *self, edict_t *other) } } -static void door_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +static void door_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) { edict_t *ent; @@ -2034,7 +2034,7 @@ static void door_secret_blocked(edict_t *self, edict_t *other) T_Damage(other, self, self, vec3_origin, other->s.origin, vec3_origin, self->dmg, 1, 0, MOD_CRUSH); } -static void door_secret_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +static void door_secret_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) { self->takedamage = DAMAGE_NO; door_secret_use(self, attacker, attacker); diff --git a/src/action/g_grapple.c b/src/action/g_grapple.c index ed6e87653..a4c7a81b5 100644 --- a/src/action/g_grapple.c +++ b/src/action/g_grapple.c @@ -261,7 +261,7 @@ void CTFFireGrapple (edict_t *self, vec3_t start, vec3_t dir, int damage, int sp } } -void CTFGrappleFire (edict_t *ent, const vec3_t g_offset, int damage, int effect) +void CTFGrappleFire (edict_t *ent, vec3_t g_offset, int damage, int effect) { vec3_t forward, right; vec3_t start; diff --git a/src/action/g_items.c b/src/action/g_items.c index 51623dcb8..64d048db7 100644 --- a/src/action/g_items.c +++ b/src/action/g_items.c @@ -67,7 +67,6 @@ //----------------------------------------------------------------------------- #include "g_local.h" -#include "shared/shared.h" qboolean Pickup_Weapon (edict_t * ent, edict_t * other); @@ -415,14 +414,14 @@ void Drop_Special (edict_t * ent, gitem_t * item) void DropSpecialItem (edict_t * ent) { // this is the order I'd probably want to drop them in... - if (INV_AMMO(ent, LASER_NUM)) - Drop_Special (ent, GET_ITEM(LASER_NUM)); - else if (INV_AMMO(ent, SLIP_NUM)) - Drop_Special (ent, GET_ITEM(SLIP_NUM)); - else if (INV_AMMO(ent, SIL_NUM)) + if (INV_AMMO(ent, SIL_NUM)) Drop_Special (ent, GET_ITEM(SIL_NUM)); else if (INV_AMMO(ent, BAND_NUM)) Drop_Special (ent, GET_ITEM(BAND_NUM)); + else if (INV_AMMO(ent, LASER_NUM)) + Drop_Special (ent, GET_ITEM(LASER_NUM)); + else if (INV_AMMO(ent, SLIP_NUM)) + Drop_Special (ent, GET_ITEM(SLIP_NUM)); else if (INV_AMMO(ent, HELM_NUM)) Drop_Special (ent, GET_ITEM(HELM_NUM)); else if (INV_AMMO(ent, KEV_NUM)) @@ -1507,7 +1506,7 @@ world_model_flags int copied to 'ent->s.effects' (see s.effects fo Drop_Weapon, Weapon_Gas, NULL, - "models/items/ammo/grenades/medium/tris.md2", + "models/objects/grenade2/tris.md2", 0, "models/weapons/v_handgr/tris.md2", "a_m61frag", @@ -1704,7 +1703,7 @@ always owned, never in the world^M Drop_Special, NULL, "misc/veston.wav", // sound - "models/items/slippers/slippers.md2", + "models/items/slippers/tris.md2", 0, NULL, /* icon */ "slippers", @@ -2001,5 +2000,4 @@ void SetItemNames (void) it = &itemlist[i]; gi.configstring (CS_ITEMS + i, it->pickup_name); } - } diff --git a/src/action/g_local.h b/src/action/g_local.h index 8b95082e2..1cede38df 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -299,14 +299,6 @@ #define getEnt(entnum) (edict_t *)((char *)globals.edicts + (globals.edict_size * entnum)) //AQ:TNG Slicer - This was missing #define GAMEVERSION "action" // the "gameversion" client command will print this plus compile date - -#define GMF_CLIENTNUM 0x00000001 -#define GMF_PROPERINUSE 0x00000002 -#define GMF_MVDSPEC 0x00000004 -#define GMF_WANT_ALL_DISCONNECTS 0x00000008 -#define GMF_VARIABLE_FPS 0x00000800 -#define GMF_EXTRA_USERINFO 0x00001000 - #ifndef NO_FPS #define G_GMF_VARIABLE_FPS GMF_VARIABLE_FPS #else @@ -325,6 +317,8 @@ #define svc_stufftext 11 #define svc_configstring 13 +// These are mapped to the enum svc_ops_t in protocol.h q2pro +// If these need adjusted in q2pro, adjust them here too #define svc_extend 30 #define svc_userstatistic 31 @@ -368,6 +362,7 @@ // edict->client->pers.spec_flags #define SPECFL_KILLFEED 0x00000001 #define SPECFL_SPECHUD 0x00000002 +#define SPECFL_SPECHUD_NEW 0x00000004 // variable server FPS #ifndef NO_FPS @@ -583,6 +578,14 @@ bind 6 "use Sniper Rifle" #define HELM_NAME "Kevlar Helmet" #define LASER_NAME "Lasersight" +#define C_KIT_NAME "Commando Kit" +#define S_KIT_NAME "Stealth Kit" +#define A_KIT_NAME "Assassin Kit" + +#define C_KIT_NAME_FULL "Commando Kit (Bandolier + Helm)" +#define S_KIT_NAME_FULL "Stealth Kit (Slippers + Silencer)" +#define A_KIT_NAME_FULL "Assassin Kit (Laser + Silencer)" + #define NO_NUM 0 #define MK23_NUM 1 @@ -615,15 +618,24 @@ bind 6 "use Sniper Rifle" #define ITEM_MAX_NUM 24 +#define C_KIT_NUM 25 +#define S_KIT_NUM 26 +#define A_KIT_NUM 27 + +#define KIT_MAX_NUM 28 + #define WEAPON_COUNT 9 #define ITEM_COUNT 6 #define AMMO_COUNT 5 +#define KIT_COUNT 3 #define WEAPON_FIRST 1 #define WEAPON_MAX WEAPON_FIRST+WEAPON_COUNT #define ITEM_FIRST WEAPON_MAX #define ITEM_MAX ITEM_FIRST+ITEM_COUNT #define AMMO_FIRST ITEM_MAX #define AMMO_MAX AMMO_FIRST+AMMO_COUNT +#define KIT_FIRST C_KIT_NUM +#define KIT_MAX KIT_MAX_NUM //AQ2:TNG - Igor adding wp_flags/itm_flags #define STRINGIFY(x) #x @@ -638,6 +650,9 @@ bind 6 "use Sniper Rifle" #define ITF_DEFAULT 63 //ITF_MASK #define ITF_DEFAULT_STR TOSTRING(ITF_DEFAULT) #define ITF_ALLOWED(typeNum) ((int)itm_flags->value & items[typeNum].flag) + +#define MAX_SPAWNS 512 // max DM spawn points supported + //AQ2:TNG End adding flags typedef struct itemList_s @@ -670,19 +685,20 @@ extern itemList_t items[ITEM_MAX_NUM]; // these should be server variables, when I get around to it //#define UNIQUE_WEAPONS_ALLOWED 2 //#define UNIQUE_ITEMS_ALLOWED 1 -#define SPEC_WEAPON_RESPAWN 1 -#define BANDAGE_TIME 27 // 10 = 1 second -#define BLEED_TIME 10 // 10 = 1 second is time for losing 1 health at slowest bleed rate +#define SPEC_WEAPON_RESPAWN 1 +#define BANDAGE_TIME 27 // 10 = 1 second +#define ENHANCED_BANDAGE_TIME 10 +#define BLEED_TIME 10 // 10 = 1 second is time for losing 1 health at slowest bleed rate // Igor's back in Time to hard grenades :-) -#define GRENADE_DAMRAD_CLASSIC 170 -#define GRENADE_DAMRAD 250 +#define GRENADE_DAMRAD_CLASSIC 170 +#define GRENADE_DAMRAD 250 typedef struct gitem_s { char *classname; // spawning name - qboolean (*pickup) (struct edict_s * ent, struct edict_s * other); + qboolean (*pickup) (struct edict_s * ent, struct edict_s * other); void (*use) (struct edict_s * ent, struct gitem_s * item); void (*drop) (struct edict_s * ent, struct gitem_s * item); void (*weaponthink) (struct edict_s * ent); @@ -740,6 +756,7 @@ typedef struct int gamemodeflags; int roundNum; qboolean ai_ent_found; + int bot_count; } game_locals_t; @@ -800,6 +817,9 @@ typedef struct int model_null; int model_lsight; +#ifdef AQTION_EXTENSION + int model_arrow; +#endif edict_t *current_entity; // entity running from G_RunFrame @@ -812,6 +832,17 @@ typedef struct float matchTime; float emptyTime; int weapon_sound_framenum; + int pic_teamplay_timer_icon; + + int pic_leaderskin[TEAM_TOP]; + int pic_esp_teamtag[TEAM_TOP]; + int pic_esp_teamicon[TEAM_TOP]; + int pic_esp_leadericon[TEAM_TOP]; + int pic_esp_respawn_icon; + + // Point of interest + vec3_t poi_origin; + vec3_t poi_angle; } level_locals_t; @@ -884,78 +915,104 @@ extern spawn_temp_t st; extern int sm_meat_index; // means of death -#define MOD_UNKNOWN 0 - -#define MOD_MK23 1 -#define MOD_MP5 2 -#define MOD_M4 3 -#define MOD_M3 4 -#define MOD_HC 5 -#define MOD_SNIPER 6 -#define MOD_DUAL 7 -#define MOD_KNIFE 8 -#define MOD_KNIFE_THROWN 9 -#define MOD_GRENADE 10 -#define MOD_G_SPLASH 11 -#define MOD_HANDGRENADE 12 -#define MOD_HG_SPLASH 13 -#define MOD_PUNCH 14 -#define MOD_BLASTER 15 -#define MOD_HYPERBLASTER 16 -#define MOD_WATER 17 -#define MOD_SLIME 18 -#define MOD_LAVA 19 -#define MOD_CRUSH 20 -#define MOD_TELEFRAG 21 -#define MOD_FALLING 22 -#define MOD_SUICIDE 23 -#define MOD_HELD_GRENADE 24 -#define MOD_EXPLOSIVE 25 -#define MOD_BARREL 26 -#define MOD_BOMB 27 -#define MOD_EXIT 28 -#define MOD_SPLASH 29 -#define MOD_TARGET_LASER 30 -#define MOD_TRIGGER_HURT 31 -#define MOD_HIT 32 -#define MOD_TARGET_BLASTER 33 -//zucc -#define MOD_BLEEDING 34 -#define MOD_KICK 35 -#define MOD_GRAPPLE 36 -#define MOD_TOTAL 37 -#define MOD_FRIENDLY_FIRE 0x8000000 +typedef enum { + MOD_UNKNOWN = 0, + MOD_MK23, + MOD_MP5, + MOD_M4, + MOD_M3, + MOD_HC, + MOD_SNIPER, + MOD_DUAL, + MOD_KNIFE, + MOD_KNIFE_THROWN, + MOD_GRENADE, + MOD_G_SPLASH, + MOD_HANDGRENADE, + MOD_HG_SPLASH, + MOD_PUNCH, + MOD_BLASTER, + MOD_HYPERBLASTER, + MOD_WATER, + MOD_SLIME, + MOD_LAVA, + MOD_CRUSH, + MOD_TELEFRAG, + MOD_FALLING, + MOD_SUICIDE, + MOD_HELD_GRENADE, + MOD_EXPLOSIVE, + MOD_BARREL, + MOD_BOMB, + MOD_EXIT, + MOD_SPLASH, + MOD_TARGET_LASER, + MOD_TRIGGER_HURT, + MOD_HIT, + MOD_TARGET_BLASTER, + MOD_BLEEDING, + MOD_KICK, + MOD_GRAPPLE, + MOD_TOTAL, + MOD_FRIENDLY_FIRE = 0x8000000 +} ModTable; // types of locations that can be hit -#define LOC_HDAM 1 // head -#define LOC_CDAM 2 // chest -#define LOC_SDAM 3 // stomach -#define LOC_LDAM 4 // legs -#define LOC_KVLR_HELMET 5 // kevlar helmet Freud, for %D -#define LOC_KVLR_VEST 6 // kevlar vest Freud, for %D -#define LOC_NO 7 // Shot by shotgun or handcannon -#define LOC_MAX 8 +typedef enum { + LOC_HDAM = 1, // head + LOC_CDAM, // chest + LOC_SDAM, // stomach + LOC_LDAM, // legs + LOC_KVLR_HELMET, // kevlar helmet Freud, for %D + LOC_KVLR_VEST, // kevlar vest Freud, for %D + LOC_NO, // Shot by shotgun or handcannon + LOC_MAX +} ModLocation; // Awards -#define ACCURACY 0 -#define IMPRESSIVE 1 -#define EXCELLENT 2 +typedef enum { + ACCURACY, + IMPRESSIVE, + EXCELLENT, + DOMINATING, + UNSTOPPABLE, + AWARD_MAX +} Awards; // Game Modes -#define GM_TEAMPLAY 0 -#define GM_TEAMDM 1 -#define GM_CTF 2 -#define GM_TOURNEY 3 -#define GM_DEATHMATCH 4 -#define GM_DOMINATION 5 +typedef enum { + GM_TEAMPLAY, + GM_TEAMDM, + GM_CTF, + GM_TOURNEY, + GM_DEATHMATCH, + GM_DOMINATION, + GM_ASSASSINATE_THE_LEADER, + GM_ESCORT_THE_VIP, + GM_MAX +} GameMode; // Game Mode Flags #define GMF_NONE 0 #define GMF_3TEAMS 1 -//#define NEW_MODE 2 // If new game mode flags are created, use 2 for its value first +//#define GMF_NEW_MODE 2 // If new game mode flags are created, use 2 for its value first #define GMF_DARKMATCH 4 #define GMF_MATCHMODE 8 +// Game Mode Names +#define GMN_TEAMPLAY "Teamplay" +#define GMN_TEAMDM "TeamDM" +#define GMN_CTF "CTF" +#define GMN_TOURNEY "Tourney" +#define GMN_DEATHMATCH "Deathmatch" +#define GMN_DOMINATION "Domination" +#define GMN_ESPIONAGE "Espionage" +#define GMN_JUMP "Jump" +#define GMN_3TEAMS "3 Teams" +//#define GMN_NEW_MODE 2 // If new game mode flags are created, use 2 for its value first +#define GMN_DARKMATCH "Darkmatch" +#define GMN_MATCHMODE "Matchmode" + extern int meansOfDeath; // zucc for hitlocation of death extern int locOfDeath; @@ -974,6 +1031,11 @@ extern edict_t *g_edicts; #define DMFLAGS(x) (((int)dmflags->value & x) != 0) +#ifndef NO_BOTS +#define AQ2WTEAMSIZE 46 +#define NUMNAMES 10 +#endif + extern cvar_t *maxentities; extern cvar_t *deathmatch; extern cvar_t *dmflags; @@ -1084,6 +1146,7 @@ extern cvar_t *itm_flags; extern cvar_t *use_classic; // Use_classic resets weapon balance to 1.52 extern cvar_t *warmup; +extern cvar_t *warmup_bots; extern cvar_t *round_begin; extern cvar_t *spectator_hud; @@ -1155,12 +1218,42 @@ extern cvar_t *bholelife; extern cvar_t *medkit_drop; extern cvar_t *medkit_time; extern cvar_t *medkit_instant; +extern cvar_t *medkit_max; +extern cvar_t *medkit_value; // BEGIN AQ2 ETE -extern cvar_t *e_enhancedSlippers; +extern cvar_t *esp; +extern cvar_t *atl; +extern cvar_t *etv; +extern cvar_t *esp_atl; +extern cvar_t *esp_punish; +extern cvar_t *esp_etv_halftime; +extern cvar_t *esp_showleader; +extern cvar_t *esp_showtarget; +extern cvar_t *esp_leaderequip; +extern cvar_t *esp_leaderenhance; +extern cvar_t *esp_enhancedslippers; +extern cvar_t *esp_matchmode; +extern cvar_t *esp_respawn_uvtime; +extern cvar_t *esp_debug; // END AQ2 ETE +// 2023 +extern cvar_t *use_killcounts; +extern cvar_t *am; +extern cvar_t *am_newnames; +extern cvar_t *am_botcount; +extern cvar_t *am_delay; +extern cvar_t *am_team; +extern cvar_t *zoom_comp; +extern cvar_t *item_kit_mode; +extern cvar_t *gun_dualmk23_enhance; +extern cvar_t *printrules; +extern cvar_t *timedmsgs; +extern cvar_t *mm_captain_teamname; +extern cvar_t *sv_killgib; + #ifdef AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); extern int (*engine_Client_GetProtocol)(edict_t *ent); @@ -1168,32 +1261,37 @@ extern int (*engine_Client_GetProtocol)(edict_t *ent); int Client_GetVersion(edict_t *ent); int Client_GetProtocol(edict_t *ent); -extern void (*engine_Ghud_SendUpdates)(edict_t *ent); -extern int(*engine_Ghud_NewElement)(int type); -extern void(*engine_Ghud_SetFlags)(int i, int val); -extern void(*engine_Ghud_UnicastSetFlags)(edict_t *ent, int i, int val); -extern void(*engine_Ghud_SetInt)(int i, int val); -extern void(*engine_Ghud_SetText)(int i, char *text); -extern void(*engine_Ghud_SetPosition)(int i, int x, int y, int z); -extern void(*engine_Ghud_SetAnchor)(int i, float x, float y); -extern void(*engine_Ghud_SetColor)(int i, int r, int g, int b, int a); -extern void(*engine_Ghud_SetSize)(int i, int x, int y); - -void Ghud_SendUpdates(edict_t *ent); -int Ghud_NewElement(int type); -void Ghud_SetFlags(int i, int val); -void Ghud_UnicastSetFlags(edict_t *ent, int i, int val); -void Ghud_SetInt(int i, int val); -void Ghud_SetText(int i, char *text); -void Ghud_SetPosition(int i, int x, int y); -void Ghud_SetPosition3D(int i, int x, int y, int z); -void Ghud_SetAnchor(int i, float x, float y); -void Ghud_SetColor(int i, int r, int g, int b, int a); -void Ghud_SetSize(int i, int x, int y); - -int Ghud_AddIcon(int x, int y, int image, int sizex, int sizey); -int Ghud_AddText(int x, int y, char *text); -int Ghud_AddNumber(int x, int y, int value); +extern void(*engine_Ghud_ClearForClient)(edict_t *ent); +extern int(*engine_Ghud_NewElement)(edict_t *ent, int type); +extern void(*engine_Ghud_RemoveElement)(edict_t *ent, int i); +extern void(*engine_Ghud_SetFlags)(edict_t *ent, int i, int val); +extern void(*engine_Ghud_SetInt)(edict_t *ent, int i, int val); +extern void(*engine_Ghud_SetText)(edict_t *ent, int i, char *text); +extern void(*engine_Ghud_SetPosition)(edict_t *ent, int i, int x, int y, int z); +extern void(*engine_Ghud_SetAnchor)(edict_t *ent, int i, float x, float y); +extern void(*engine_Ghud_SetColor)(edict_t *ent, int i, int r, int g, int b, int a); +extern void(*engine_Ghud_SetSize)(edict_t *ent, int i, int x, int y); + +void Ghud_ClearForClient(edict_t *ent); +int Ghud_NewElement(edict_t *ent, int type); +void Ghud_RemoveElement(edict_t *ent, int i); +void Ghud_SetFlags(edict_t *ent, int i, int val); +void Ghud_SetInt(edict_t *ent, int i, int val); +void Ghud_SetText(edict_t *ent, int i, char *text); +void Ghud_SetPosition(edict_t *ent, int i, int x, int y); +void Ghud_SetPosition3D(edict_t *ent, int i, int x, int y, int z); +void Ghud_SetAnchor(edict_t *ent, int i, float x, float y); +void Ghud_SetColor(edict_t *ent, int i, int r, int g, int b, int a); +void Ghud_SetSize(edict_t *ent, int i, int x, int y); + +int Ghud_AddIcon(edict_t *ent, int x, int y, int image, int sizex, int sizey); +int Ghud_AddText(edict_t *ent, int x, int y, char *text); +void Ghud_SetTextFlags(edict_t *ent, int i, int uiflags); +int Ghud_AddNumber(edict_t *ent, int x, int y, int value); + + +extern void(*engine_CvarSync_Set)(int index, const char *name, const char *val); +void CvarSync_Set(int index, const char *name, const char *val); #endif // 2022 @@ -1208,6 +1306,12 @@ extern cvar_t *gm; // Gamemode extern cvar_t *gmf; // Gamemodeflags extern cvar_t *sv_idleremove; // Remove idlers +#ifdef AQTION_EXTENSION +extern cvar_t *use_newirvision; // enable new irvision (only highlight baddies) +extern cvar_t *use_indicators; // enable/allow indicators +extern cvar_t *use_xerp; // allow clients to use cl_xerp +#endif + // Discord SDK integration with Q2Pro extern cvar_t *cl_discord; extern cvar_t *cl_discord_id; @@ -1215,11 +1319,6 @@ extern cvar_t *cl_discord_discriminator; extern cvar_t *cl_discord_username; extern cvar_t *cl_discord_avatar; - -// Q2Pro / TNG Merge -extern void func_explosive_explode(edict_t *, edict_t *, edict_t *, int, vec3_t); -extern void player_die(edict_t *, edict_t *, edict_t *, int, vec3_t); - #define world (&g_edicts[0]) // item spawnflags @@ -1295,6 +1394,7 @@ void ChangeWeapon (edict_t * ent); void PrecacheItems( void ); void SpawnItem (edict_t * ent, gitem_t * item); void Think_Weapon (edict_t * ent); +void AddItem(edict_t *ent, gitem_t *item); qboolean Add_Ammo (edict_t * ent, gitem_t * item, int count); void Touch_Item (edict_t * ent, edict_t * other, cplane_t * plane, csurface_t * surf); @@ -1303,9 +1403,9 @@ void Touch_Item (edict_t * ent, edict_t * other, cplane_t * plane, // g_utils.c // qboolean KillBox (edict_t *ent); -void G_ProjectSource(const vec3_t point, const vec3_t distance, const vec3_t forward, const vec3_t right, vec3_t result); +void G_ProjectSource(vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result); edict_t *G_Find(edict_t *from, ptrdiff_t fieldofs, char *match); -edict_t *findradius(edict_t *from, const vec3_t org, float rad); +edict_t *findradius(edict_t *from, vec3_t org, float rad); edict_t *G_PickTarget(char *targetname); void G_UseTargets(edict_t *ent, edict_t * activator); void G_SetMovedir(vec3_t angles, vec3_t movedir); @@ -1327,13 +1427,15 @@ qboolean visible(edict_t *self, edict_t *other, int mask); qboolean ai_visible( edict_t *self, edict_t *other ); qboolean infront( edict_t *self, edict_t *other ); #endif +void disablecvar(cvar_t *cvar, char *msg); +int eztimer(int seconds); // Re-enabled for bots float *tv (float x, float y, float z); char *vtos (const vec3_t v); float vectoyaw (vec3_t vec); -void vectoangles (const vec3_t vec, vec3_t angles); +void vectoangles (vec3_t vec, vec3_t angles); // // g_combat.c @@ -1341,7 +1443,7 @@ void vectoangles (const vec3_t vec, vec3_t angles); qboolean OnSameTeam (edict_t * ent1, edict_t * ent2); qboolean CanDamage (edict_t * targ, edict_t * inflictor); void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, - const vec3_t dir, vec3_t point, const vec3_t normal, int damage, + const vec3_t dir, const vec3_t point, const vec3_t normal, int damage, int knockback, int dflags, int mod); void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, edict_t * ignore, float radius, int mod); @@ -1375,25 +1477,25 @@ void SP_misc_teleporter_dest(edict_t* ent); // g_weapon.c // void ThrowDebris (edict_t * self, char *modelname, float speed, - const vec3_t origin); -void fire_bullet (edict_t * self, const vec3_t start, vec3_t aimdir, int damage, + vec3_t origin); +void fire_bullet (edict_t * self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod); -void fire_shotgun (edict_t * self, const vec3_t start, vec3_t aimdir, int damage, +void fire_shotgun (edict_t * self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int count, int mod); //SLIC2 changed argument name hyper to hyperb -void fire_blaster (edict_t * self, const vec3_t start, vec3_t aimdir, int damage, +void fire_blaster (edict_t * self, vec3_t start, vec3_t aimdir, int damage, int speed, int effect, qboolean hyperb); -void fire_grenade2 (edict_t * self, const vec3_t start, const vec3_t aimdir, int damage, +void fire_grenade2 (edict_t * self, vec3_t start, vec3_t aimdir, int damage, int speed, int timer, float damage_radius, qboolean held); void kick_attack(edict_t *ent); void punch_attack(edict_t *ent); -int knife_attack(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick); -void knife_throw(edict_t *self, const vec3_t start, vec3_t dir, int damage, int speed); +int knife_attack(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick); +void knife_throw(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed); void knife_touch(edict_t* ent, edict_t* other, cplane_t* plane, csurface_t* surf); -void fire_bullet_sparks(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod); -void fire_bullet_sniper(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod); +void fire_bullet_sparks(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod); +void fire_bullet_sniper(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod); void setFFState(edict_t* ent); // @@ -1417,7 +1519,8 @@ void* G_FetchGameExtension(char *name); // g_player.c // void player_pain (edict_t * self, edict_t * other, float kick, int damage); -void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); +void player_die (edict_t * self, edict_t * inflictor, edict_t * attacker, + int damage, const vec3_t point); // // g_svcmds.c @@ -1439,6 +1542,7 @@ qboolean OnLadder( edict_t *ent ); // p_hud.c // void MoveClientToIntermission (edict_t * client); +void MoveClientToPOI(edict_t *ent, edict_t *poi); void G_SetStats (edict_t * ent); void G_CheckChaseStats (edict_t * ent); void ValidateSelectedItem (edict_t * ent); @@ -1448,7 +1552,7 @@ void DeathmatchScoreboardMessage (edict_t * client, edict_t * killer); // // g_pweapon.c // -void PlayerNoise (edict_t * who, const vec3_t where, int type); +void PlayerNoise (edict_t * who, vec3_t where, int type); // // g_phys.c @@ -1460,6 +1564,7 @@ void G_RunEntity (edict_t * ent); // void EndDMLevel (void); void ExitLevel (void); +edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot); // // g_chase.c @@ -1471,6 +1576,8 @@ void SetChase( edict_t *ent, edict_t *target ); void ChaseNext (edict_t * ent); void ChasePrev (edict_t * ent); void GetChaseTarget (edict_t * ent); +void TeamplayChaseCam(edict_t *self, edict_t *attacker); +void EspionageChaseCam(edict_t *self, edict_t *attacker); // @@ -1483,19 +1590,22 @@ void G_UpdateSpectatorStatusbar( void ); void G_UpdatePlayerStatusbar( edict_t *ent, int force ); int Gamemodeflag(void); int Gamemode(void); +#ifdef USE_AQTION void generate_uuid(void); - +#endif // // p_client.c // edict_t* SelectRandomDeathmatchSpawnPoint(void); edict_t* SelectFarthestDeathmatchSpawnPoint(void); +edict_t* SelectDeathmatchSpawnPoint(void); float PlayersRangeFromSpot(edict_t* spot); void ClientLegDamage(edict_t* ent); void ClientFixLegs(edict_t *ent); void ClientUserinfoChanged(edict_t* ent, char* userinfo); void ClientDisconnect(edict_t* ent); void CopyToBodyQue(edict_t* ent); +void Announce_Reward(edict_t *ent, int rewardType); //p_weapon.c void Weapon_Generic( edict_t * ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST, @@ -1505,18 +1615,21 @@ void Weapon_Generic( edict_t * ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST void( *fire ) (edict_t * ent) ); void PlayWeaponSound( edict_t *ent ); -void P_ProjectSource(gclient_t *client, const vec3_t point, const vec3_t distance, const vec3_t forward, const vec3_t right, vec3_t result); +void P_ProjectSource(gclient_t *client, vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result); void weapon_grenade_fire(edict_t* ent, qboolean held); void InitTookDamage(void); void ProduceShotgunDamageReport(edict_t*); //tng_stats.c void StatBotCheck(void); +#if USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); +void LogCapture(edict_t *capturer); void LogMatch(void); -void LogAward(char* steamid, char* discordid, int award); +void LogAward(edict_t *ent, int award); void LogEndMatchStats(void); +#endif //============================================================================ @@ -1532,6 +1645,7 @@ void LogEndMatchStats(void); #define ANIM_REVERSE -1 #define MAX_SKINLEN 32 +#define MAX_TEAMNAMELEN 32 #define MAX_GUNSTAT MOD_GRENADE //Max MOD to track @@ -1589,10 +1703,14 @@ typedef struct int menu_shown; // has the main menu been shown qboolean dm_selected; // if dm weapon selection has been done once - // Reki - added these options, controllable via userinfo cvar + // Reki - added these options, controllable via userinfo cvar (Reki update 2/24/23, controllable via cvarsync as well) int limp_nopred; int spec_flags; qboolean antilag_optout; +#ifdef AQTION_EXTENSION + int cl_xerp; + int cl_indicators; +#endif int mk23_mode; // firing mode, semi or auto int mp5_mode; @@ -1604,8 +1722,13 @@ typedef struct int irvision; // ir on or off (only matters if player has ir device, currently bandolier) ignorelist_t ignorelist; + gitem_t *chosenItem2; // Support for item kit mode + + #if USE_AQTION + char steamid[24]; + char discordid[24]; + #endif - bool is_bot; // client is a bot } client_persistant_t; @@ -1644,6 +1767,8 @@ typedef struct int motd_refreshes; int last_motd_refresh; + int gamemsg_refreshes; + int last_gamemsg_refresh; edict_t *last_chase_target; // last person they chased, to resume at the same place later... // Number of team kills this game @@ -1667,12 +1792,13 @@ typedef struct int shotsTotal; //Total number of shots int hitsTotal; //Total number of hits int streakKills; //Kills in a row + int roundStreakKills; //Kills in a row in that round int streakHS; //Headshots in a Row int streakKillsHighest; //Highest kills in a row int streakHSHighest; //Highest headshots in a Row int hitsLocations[LOC_MAX]; //Number of hits for different locations - gunStats_t gunstats[MAX_GUNSTAT]; //Number of shots/hits for different guns + gunStats_t gunstats[MOD_TOTAL]; //Number of shots/hits for different guns, adjusted to MOD_TOTAL to allow grenade, kick and punch stats //AQ2:TNG - Slicer: Video Checking and further Cheat cheking vars char vidref[16]; @@ -1681,6 +1807,7 @@ typedef struct float glmodulate; float glclear; float gldynamic; + float glbrightness; qboolean checked; int checkframe[3]; @@ -1692,7 +1819,39 @@ typedef struct vec3_t jmp_teleport_v_angle; qboolean jmp_teleport_ducked; - //char skin[MAX_SKINLEN]; +#ifdef AQTION_EXTENSION + int hud_items[128]; + int hud_type; +#endif + + int esp_state; + int esp_caps; // How many times a player has captured the case + int esp_capstreak; // As leader, how many caps in a row + int esp_capstreakbest; // Best cap streak + int esp_leaderkillstreak; // How many enemy leader kills a player has had in a row + int esp_leaderkillstreakbest; // Best leader kill streak + int esp_leaderprotectcount; // How many times a player has protected the leader + int esp_leaderfragcount; // How many times this player has fragged a leader + int esp_capdefendercount; // How many times a player has defended the capture point + + // timers + int esp_lastprotectcap; // Last time this player protected the capture point (framenum) + int esp_lastprotectleader; // Last time this player protected the leader (framenum) + int esp_lasthurtleader; // Last time this player hurt the leader (framenum) + int esp_leadertime; // Last time this player was leader (framenum) to prevent spamming + int esp_respawn_sounds; // 3: Played Lights, 2: Played Camera, 1: Played Action, 0: Play nothing + qboolean is_volunteer; + + int medkit_award_time; + + //PaTMaN's jmod + int toggle_lca; + int toggles; + + // Domination stats + int dom_caps; // How many times a player captured a dom point + int dom_capstreak; // How many times a player captured a dom point in a row + int dom_capstreakbest; // Best cap streak for domination } client_respawn_t; @@ -1707,6 +1866,11 @@ struct gclient_s // known to compatible server int clientNum; + // Reki: cvar sync +#ifdef AQTION_EXTENSION + cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; +#endif + // private to game client_persistant_t pers; client_respawn_t resp; @@ -1887,13 +2051,16 @@ struct gclient_s int ctf_grapplestate; // true if pulling int ctf_grapplereleaseframe; // frame of grapple release +#ifdef AQTION_EXTENSION + //AQTION - Reki: Teammate indicators + edict_t *arrow; +#endif // used for extrapolation usercmd_t cmd_last; // visiblity mask unsigned int dimension_observe; - }; @@ -1979,7 +2146,7 @@ struct edict_s void (*touch)(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); void (*use)(edict_t *self, edict_t *other, edict_t *activator); void (*pain)(edict_t *self, edict_t *other, float kick, int damage); - void (*die)(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); + void (*die)(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point); int touch_debounce_framenum; // are all these legit? do we need more/less of them? int pain_debounce_framenum; @@ -2067,7 +2234,7 @@ struct edict_s qboolean is_bot; qboolean is_jumping; - qboolean is_triggering; + qboolean is_triggering; // For movement vec3_t move_vector; @@ -2119,7 +2286,14 @@ struct edict_s int bot_speed; qboolean bCrawl; qboolean bLastJump; - vec3_t lastPosition; + vec3_t lastPosition; + qboolean nameused[NUMNAMES][NUMNAMES]; + qboolean newnameused[AQ2WTEAMSIZE]; + #ifdef AQTION_EXTENSION + //AQTION - Reki: Entity indicators + edict_t *obj_arrow; + #endif + #endif }; @@ -2137,10 +2311,20 @@ typedef struct int shotsTotal; int hitsTotal; int hitsLocations[LOC_MAX]; - gunStats_t gunstats[MAX_GUNSTAT]; + gunStats_t gunstats[MOD_TOTAL]; int team; gitem_t *weapon; gitem_t *item; + // Extended stats + #if USE_AQTION + char steamid[24]; + char discordid[24]; + #endif + int ctf_capstreak; + int team_kills; + int streakKillsHighest; + int streakHSHighest; + } gghost_t; @@ -2168,6 +2352,7 @@ void Cmd_TKOk (edict_t * ent); // AQ:TNG - JBravo adding tkok void Cmd_FF_f( edict_t *ent ); void Cmd_Time (edict_t * ent); // AQ:TNG - JBravo adding time void Cmd_Roundtimeleft_f(edict_t *ent); // AQ:TNG - DW added roundtimeleft +void Cmd_Noclip_f(edict_t *ent); void DropSpecialWeapon (edict_t * ent); void ReadySpecialWeapon (edict_t * ent); void DropSpecialItem (edict_t * ent); @@ -2183,7 +2368,7 @@ void Drop_Spec (edict_t * ent, gitem_t * item); void SpecThink (edict_t * spec); void DeadDropSpec (edict_t * ent); -void temp_think_specweap (edict_t * ent); // p_weapons.c +void SpecialWeaponRespawnTimer (edict_t * ent); // p_weapons.c void ThinkSpecWeap (edict_t * ent); void DropExtraSpecial (edict_t * ent); void TransparentListSet (solid_t solid_type); @@ -2197,7 +2382,7 @@ void A_ScoreboardMessage( edict_t * ent, edict_t * killer ); //local to g_combat but needed in p_view void SpawnDamage (int type, const vec3_t origin, const vec3_t normal, int damage); void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, - int damage, vec3_t point); + int damage, const vec3_t point); void Add_Frag(edict_t * ent, int mod); void Subtract_Frag (edict_t * ent); @@ -2243,7 +2428,7 @@ void Cmd_AutoRecord_f(edict_t * ent); typedef struct team_s { - char name[20]; + char name[24]; char skin[MAX_SKINLEN]; char skin_index[MAX_QPATH]; int score, total; @@ -2251,14 +2436,13 @@ typedef struct team_s int pauses_used, wantReset; cvar_t *teamscore; edict_t *captain; - -#ifdef AQTION_EXTENSION -#ifdef AQTION_HUD - int ghud_resettime; - byte ghud_icon; - byte ghud_num; -#endif -#endif + // Espionage + edict_t *leader; + int respawn_timer; + qboolean leader_dead; + char leader_name[MAX_SKINLEN]; + char leader_skin[MAX_QPATH]; + char leader_skin_index[MAX_QPATH]; }team_t; extern team_t teams[TEAM_TOP]; @@ -2275,16 +2459,65 @@ extern int gameSettings; #include "a_ctf.h" #include "a_dom.h" +#include "a_esp.h" #ifdef AQTION_EXTENSION -extern int ghud_team1_icon; -extern int ghud_team1_num; -extern int ghud_team2_icon; -extern int ghud_team2_num; -extern int ghud_team3_icon; -extern int ghud_team3_num; +#define HAS_CVARSYNC(ent) (Client_GetProtocol(ent) == 38 && Client_GetVersion(ent) >= 3013) + +// hud (through ghud extension) +typedef enum { + h_nameplate_l = 0, + h_nameplate_r = 30, + h_nameplate_end = 61, + h_team_l, + h_team_l_num, + h_team_r, + h_team_r_num, +} huditem_t; + +void HUD_SetType(edict_t *clent, int type); +void HUD_ClientSetup(edict_t *clent); +void HUD_ClientUpdate(edict_t *clent); +void HUD_SpectatorSetup(edict_t *clent); +void HUD_SpectatorUpdate(edict_t *clent); + +// cvar sync +typedef enum { + clcvar_cl_antilag, + clcvar_cl_indicators, + clcvar_cl_xerp, + clcvar_cl_spectatorhud, + clcvar_cl_spectatorkillfeed, +} clcvar_t; + +// UI flags from q2pro +#define UI_LEFT 0x00000001 +#define UI_RIGHT 0x00000002 +#define UI_CENTER (UI_LEFT | UI_RIGHT) +#define UI_BOTTOM 0x00000004 +#define UI_TOP 0x00000008 +#define UI_MIDDLE (UI_BOTTOM | UI_TOP) +#define UI_DROPSHADOW 0x00000010 +#define UI_ALTCOLOR 0x00000020 +#define UI_IGNORECOLOR 0x00000040 +#define UI_XORCOLOR 0x00000080 +#define UI_AUTOWRAP 0x00000100 +#define UI_MULTILINE 0x00000200 +#define UI_DRAWCURSOR 0x00000400 #endif #ifndef NO_BOTS #include "acesrc/acebot.h" #endif + +typedef struct { + int teamNum; + edict_t *ent; + int seconds; + char *msg; + qboolean fired; +} Message; +extern Message *timedMessages; + +void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); +void FireTimedMessages(void); diff --git a/src/action/g_main.c b/src/action/g_main.c index fb6ef6274..1620f37c6 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -458,12 +458,15 @@ cvar_t *radio_repeat_time; cvar_t *use_classic; // Used to reset spread/gren strength to 1.52 cvar_t *warmup; +cvar_t *warmup_bots; cvar_t *round_begin; cvar_t *spectator_hud; cvar_t *medkit_drop; cvar_t *medkit_time; cvar_t *medkit_instant; +cvar_t *medkit_max; +cvar_t *medkit_value; #ifndef NO_BOTS cvar_t *ltk_jumpy; @@ -473,16 +476,30 @@ cvar_t *ltk_chat; cvar_t *ltk_routing; cvar_t *ltk_botfile; cvar_t *ltk_loadbots; -cvar_t *ltk_showbots; +cvar_t *ltk_classic; #endif cvar_t *jump; // jumping mod // BEGIN AQ2 ETE -cvar_t *e_enhancedSlippers; +cvar_t *esp; +cvar_t *atl; +cvar_t *etv; +cvar_t *esp_atl; +cvar_t *esp_punish; +cvar_t *esp_etv_halftime; +cvar_t *esp_showleader; +cvar_t *esp_showtarget; +cvar_t *esp_leaderequip; +cvar_t *esp_leaderenhance; +cvar_t *esp_enhancedslippers; +cvar_t *esp_matchmode; +cvar_t *esp_respawn_uvtime; +cvar_t *esp_debug; // END AQ2 ETE // 2022 + cvar_t *sv_limp_highping; cvar_t *server_id; cvar_t *stat_logs; @@ -494,6 +511,27 @@ cvar_t *gmf; cvar_t *sv_idleremove; cvar_t *g_spawn_items; +// 2023 +cvar_t *use_killcounts; // Display kill counts in console to clients on frag +cvar_t *am; // Attract mode toggle +cvar_t *am_newnames; // Attract mode new names, use new LTK bot names +cvar_t *am_botcount; // Attract mode botcount, how many bots at minimum at all times +cvar_t *am_delay; // Attract mode delay, unused at the moment +cvar_t *am_team; // Attract mode team, which team do you want the bots to join +cvar_t *zoom_comp; // Compensates zoom-in frames with ping (high ping = fewer frames) +cvar_t *item_kit_mode; // Toggles item kit mode +cvar_t *gun_dualmk23_enhance; // Enables laser sight for dual mk23 pistols +cvar_t *printrules; // Centerprint game rules when the countdown begins +cvar_t *timedmsgs; // Toggles timed messages +cvar_t *mm_captain_teamname; // Toggles if we want to use the captain's name for the team in matchmode +cvar_t *sv_killgib; // Gibs on 'kill' command + +#ifdef AQTION_EXTENSION +cvar_t *use_newirvision; +cvar_t *use_indicators; +cvar_t *use_xerp; +#endif + // Discord SDK integration with Q2Pro cvar_t *cl_discord; cvar_t *cl_discord_id; @@ -501,7 +539,7 @@ cvar_t *cl_discord_discriminator; cvar_t *cl_discord_username; cvar_t *cl_discord_avatar; -void SpawnEntities (const char *mapname, const char *entities, const char *spawnpoint); +void SpawnEntities (char *mapname, char *entities, char *spawnpoint); void ClientThink (edict_t * ent, usercmd_t * cmd); qboolean ClientConnect (edict_t * ent, char *userinfo); void ClientUserinfoChanged (edict_t * ent, char *userinfo); @@ -510,10 +548,10 @@ void ClientBegin (edict_t * ent); void ClientCommand (edict_t * ent); void CheckNeedPass (void); void RunEntity (edict_t * ent); -void WriteGame (const char *filename, qboolean autosave); -void ReadGame (const char *filename); -void WriteLevel (const char *filename); -void ReadLevel (const char *filename); +void WriteGame (char *filename, qboolean autosave); +void ReadGame (char *filename); +void WriteLevel (char *filename); +void ReadLevel (char *filename); void InitGame (void); void G_RunFrame (void); @@ -554,7 +592,7 @@ void ShutdownGame (void) and global variables ================= */ -q_exported game_export_t *GetGameAPI (game_import_t * import) +game_export_t *GetGameAPI (game_import_t * import) { gi = *import; #ifndef NO_BOTS @@ -595,16 +633,18 @@ q_exported game_export_t *GetGameAPI (game_import_t * import) engine_Client_GetProtocol = gi.CheckForExtension("Client_GetProtocol"); engine_Client_GetVersion = gi.CheckForExtension("Client_GetVersion"); - engine_Ghud_SendUpdates = gi.CheckForExtension("Ghud_SendUpdates"); + engine_Ghud_ClearForClient = gi.CheckForExtension("Ghud_ClearForClient"); engine_Ghud_NewElement = gi.CheckForExtension("Ghud_NewElement"); + engine_Ghud_RemoveElement = gi.CheckForExtension("Ghud_RemoveElement"); engine_Ghud_SetFlags = gi.CheckForExtension("Ghud_SetFlags"); - engine_Ghud_UnicastSetFlags = gi.CheckForExtension("Ghud_UnicastSetFlags"); engine_Ghud_SetInt = gi.CheckForExtension("Ghud_SetInt"); engine_Ghud_SetText = gi.CheckForExtension("Ghud_SetText"); engine_Ghud_SetPosition = gi.CheckForExtension("Ghud_SetPosition"); engine_Ghud_SetAnchor = gi.CheckForExtension("Ghud_SetAnchor"); engine_Ghud_SetColor = gi.CheckForExtension("Ghud_SetColor"); engine_Ghud_SetSize = gi.CheckForExtension("Ghud_SetSize"); + + engine_CvarSync_Set = gi.CheckForExtension("CvarSync_Set"); #endif @@ -612,19 +652,6 @@ q_exported game_export_t *GetGameAPI (game_import_t * import) } #ifndef GAME_HARD_LINKED -// this is only here so the functions in q_shared.c and q_shwin.c can link -void Sys_Error (const char *error, ...) -{ - va_list argptr; - char text[1024]; - - va_start (argptr, error); - vsnprintf (text, sizeof(text),error, argptr); - va_end (argptr); - - gi.error("%s", text); -} - // this is only here so the functions in q_shared.c can link void Com_LPrintf(print_type_t type, const char *fmt, ...) { @@ -653,6 +680,32 @@ void Com_Error(error_type_t type, const char *fmt, ...) gi.error("%s", text); } + +// // this is only here so the functions in q_shared.c and q_shwin.c can link +// void Sys_Error (const char *error, ...) +// { +// va_list argptr; +// char text[1024]; + +// va_start (argptr, error); +// vsnprintf (text, sizeof(text),error, argptr); +// va_end (argptr); + +// gi.error("%s", text); +// } + +// void Com_Printf (const char *msg, ...) +// { +// va_list argptr; +// char text[1024]; + +// va_start (argptr, msg); +// vsnprintf (text, sizeof(text), msg, argptr); +// va_end (argptr); + +// gi.dprintf("%s", text); +// } + #endif @@ -738,6 +791,9 @@ void ClientEndServerFrames (void) if (ent->client->chase_target) UpdateChaseCam(ent); } + + if (timedmsgs->value) + FireTimedMessages(); } /* @@ -949,29 +1005,6 @@ void CheckDMRules (void) if (!FRAMESYNC) return; -#ifdef AQTION_EXTENSION -#ifdef AQTION_HUD - // Reki - // Update our ghud values for the team score - int i; - for (i = TEAM1; i < TEAM_TOP; i++) - { - if (teams[i].ghud_num <= 0) - continue; - - if (teams[i].ghud_resettime && level.time > teams[i].ghud_resettime) - { - teams[i].ghud_resettime = 0; - Ghud_SetFlags(teams[i].ghud_icon, 0); - Ghud_SetFlags(teams[i].ghud_num, 0); - } - - Ghud_SetInt(teams[i].ghud_num, teams[i].score); - } -#endif -#endif - - if (CheckTeamRules()) return; } @@ -1000,7 +1033,7 @@ void CheckDMRules (void) { gi.bprintf (PRINT_HIGH, "Fraglimit hit.\n"); IRC_printf (IRC_T_GAME, "Fraglimit hit."); - if (ctf->value) + if (ctf->value || esp->value) ResetPlayers (); EndDMLevel (); return; @@ -1270,3 +1303,42 @@ void CheckNeedPass (void) } //FROM 3.20 END + +// This doesn't work yet but I'll keep trying +edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) +{ + int i, j; + edict_t *ent; + edict_t *plist[ MAX_CLIENTS ] = {NULL}; + int pcount = 0; + + // Supplied paramter must be a valid team number + if (teamNum < TEAM1 && teamNum > TEAM3) + return 0; + if (teamCount == 2 && teamNum == 3) { + // Somehow passing team 3 in a 2-team match? + gi.dprintf("Warning: Unable to ChooseRandomPlayer for a team that doesn't exist\n"); + return 0; + } + + for (i = 0, ent = &g_edicts[1]; i < game.maxclients; i++, ent++) + { + // Client must exist and be on a team, and not be a sub + if (!ent->inuse || !ent->client || !ent->client->resp.team || ent->client->resp.subteam) + continue; + if (!allowBot) + if (ent->is_bot) + continue; + pcount++; + players[i] = ent; + //gi.dprintf("%s\n", players[i]->client->pers.netname); + } + + j = rand() % pcount; + + ent = plist[j]; + gi.dprintf("%s\n", ent->client->pers.netname); + + // Returns a random player + return ent; +} \ No newline at end of file diff --git a/src/action/g_misc.c b/src/action/g_misc.c index 3ebb9b285..debb6d605 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -144,13 +144,15 @@ gib_touch (edict_t * self, edict_t * other, cplane_t * plane, } } -void gib_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +void +gib_die (edict_t * self, edict_t * inflictor, edict_t * attacker, int damage, + vec3_t point) { G_FreeEdict (self); } void -ThrowGib (edict_t *self, char *gibname, int damage, int type) +ThrowGib (edict_t * self, char *gibname, int damage, int type) { edict_t *gib; vec3_t vd; @@ -252,16 +254,8 @@ ThrowClientHead (edict_t * self, int damage) vec3_t vd; char *gibname; - if (rand () & 1) - { - gibname = "models/objects/gibs/head2/tris.md2"; - self->s.skinnum = 1; // second skin is player - } - else - { - gibname = "models/objects/gibs/skull/tris.md2"; - self->s.skinnum = 0; - } + gibname = "models/objects/gibs/skull/tris.md2"; + self->s.skinnum = 0; self->s.origin[2] += 32; self->s.frame = 0; @@ -302,7 +296,7 @@ debris_die (edict_t * self, edict_t * inflictor, edict_t * attacker, } void -ThrowDebris (edict_t * self, char *modelname, float speed, const vec3_t origin) +ThrowDebris (edict_t * self, char *modelname, float speed, vec3_t origin) { edict_t *chunk; vec3_t v; @@ -723,8 +717,8 @@ SP_func_object (edict_t * self) /*QUAKED func_explosive (0 .5 .8) ? Trigger_Spawn ANIMATED ANIMATED_FAST Any brush that you want to explode or break apart. If you want an -explosion, set dmg and it will do a radius explosion of that amount -at the center of the brush. +ex0plosion, set dmg and it will do a radius explosion of that amount +at the center of the bursh. If targeted it will not be shootable. @@ -734,8 +728,9 @@ mass defaults to 75. This determines how much debris is emitted when it explodes. You get one large chunk per 100 of mass (up to 8) and one small chunk per 25 of mass (up to 16). So 800 gives the most. */ -void func_explosive_explode (edict_t * self, edict_t * inflictor, - edict_t * attacker, int damage, vec3_t point) +void +func_explosive_explode (edict_t * self, edict_t * inflictor, + edict_t * attacker, int damage, const vec3_t point) { vec3_t origin; vec3_t chunkorigin; @@ -801,12 +796,14 @@ void func_explosive_explode (edict_t * self, edict_t * inflictor, G_FreeEdict (self); } -void func_explosive_use (edict_t * self, edict_t * other, edict_t * activator) +void +func_explosive_use (edict_t * self, edict_t * other, edict_t * activator) { func_explosive_explode (self, self, other, self->health, vec3_origin); } -void func_explosive_spawn (edict_t * self, edict_t * other, edict_t * activator) +void +func_explosive_spawn (edict_t * self, edict_t * other, edict_t * activator) { self->solid = SOLID_BSP; self->svflags &= ~SVF_NOCLIENT; diff --git a/src/action/g_save.c b/src/action/g_save.c index d502ffa90..6914217ad 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -415,6 +415,8 @@ void InitGame( void ) medkit_drop = gi.cvar( "medkit_drop", "0", 0 ); medkit_time = gi.cvar( "medkit_time", "30", 0 ); medkit_instant = gi.cvar( "medkit_instant", "0", 0 ); + medkit_max = gi.cvar( "medkit_max", "3", 0 ); + medkit_value = gi.cvar( "medkit_value", "25", 0 ); dom = gi.cvar( "dom", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gmf' (gamemodeflags) use_grapple = gi.cvar( "use_grapple", "0", 0 ); mv_public = gi.cvar( "mv_public", "0", 0 ); //slicer @@ -541,13 +543,37 @@ void InitGame( void ) jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO|*/ CVAR_LATCH); // jumping mod -- removed from serverinfo 2022 warmup = gi.cvar( "warmup", "0", CVAR_LATCH ); + warmup_bots = gi.cvar( "warmup_bots", "0", CVAR_LATCH ); round_begin = gi.cvar( "round_begin", "15", 0 ); - spectator_hud = gi.cvar( "spectator_hud", "0", CVAR_LATCH ); + spectator_hud = gi.cvar( "spectator_hud", "1", CVAR_LATCH ); use_mvd2 = gi.cvar( "use_mvd2", "0", 0 ); // JBravo: q2pro MVD2 recording. 0 = off, 1 = on // BEGIN AQ2 ETE - e_enhancedSlippers = gi.cvar( "e_enhancedSlippers", "0", 0); + esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + if (esp->value) { + atl = gi.cvar( "atl", "1", CVAR_LATCH ); + etv = gi.cvar( "etv", "0", CVAR_LATCH ); + }; + esp_atl = gi.cvar( "esp_atl", "0", 0 ); // This forces ATL mode even if ETV mode is set in the .esp file + esp_punish = gi.cvar("esp_punish", "0", 0); + esp_etv_halftime = gi.cvar("esp_etv_halftime", "0", CVAR_LATCH); + if (esp_etv_halftime->value && roundlimit->value < 4) { + // Disabling halftime because roundlimit is not set + disablecvar(esp_etv_halftime, "Roundlimit set too low for halftime, minimum is 4 rounds"); + } + esp_showleader = gi.cvar("esp_showleader", "1", 0); + esp_showtarget = gi.cvar("esp_showtarget", "1", 0); + esp_leaderequip = gi.cvar("esp_leaderequip", "1", 0); + esp_leaderenhance = gi.cvar("esp_leaderenhance", "0", 0); + esp_enhancedslippers = gi.cvar("esp_enhancedslippers", "0", 0); + esp_matchmode = gi.cvar("esp_matchmode", "0", 0); + esp_respawn_uvtime = gi.cvar("esp_respawn_uvtime", "10", 0); + if (esp_respawn_uvtime->value > 20) { + gi.dprintf("esp_respawn_uvtime was set too high, setting to 2 seconds\n"); + gi.cvar_forceset("esp_respawn_uvtime", "20"); + } + esp_debug = gi.cvar("esp_debug", "0", 0); // Set to 1 to enable debug messages for Espionage // END AQ2 ETE // 2022 @@ -562,12 +588,37 @@ void InitGame( void ) sv_antilag_interp = gi.cvar("sv_antilag_interp", "0", CVAR_SERVERINFO); sv_limp_highping = gi.cvar("sv_limp_highping", "70", 0); // Removed it from Serverinfo mapvote_next_limit = gi.cvar( "mapvote_next_limit", "0", 0); - stat_apikey = gi.cvar("stat_apikey", "none", 0); + stat_apikey = gi.cvar("stat_apikey", "none", 0); // Never include this in serverinfo! stat_url = gi.cvar("stat_url", "https://apigateway.aq2world.com/api/v1/stats", 0); gm = gi.cvar("gm", "dm", CVAR_SERVERINFO); gmf = gi.cvar("gmf", "0", CVAR_SERVERINFO); sv_idleremove = gi.cvar("sv_idleremove", "0", 0); - g_spawn_items = gi.cvar("g_spawn_items", "0", CVAR_LATCH); + g_spawn_items = gi.cvar("g_spawn_items", "0", CVAR_LATCH); + + // 2023 + use_killcounts = gi.cvar("use_killcounts", "0", 0); + am = gi.cvar("am", "0", CVAR_SERVERINFO); + am_newnames = gi.cvar("am_newnames", "1", 0); + am_botcount = gi.cvar("am_botcount", "6", CVAR_SERVERINFO); + if (am_botcount->value < 0){ + gi.cvar_forceset("am_botcount", "0"); + } + am_delay = gi.cvar("am_delay", "30", 0); + am_team = gi.cvar("am_team", "0", 0); + zoom_comp = gi.cvar("zoom_comp", "0", 0); + item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); + gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); + printrules = gi.cvar("printrules", "0", 0); + timedmsgs = gi.cvar("timedmsgs", "0", 0); + mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", 0); + sv_killgib = gi.cvar("sv_killgib", "0", 0); + + // new AQtion Extension cvars +#ifdef AQTION_EXTENSION + use_newirvision = gi.cvar("use_newirvision", "1", 0); + use_indicators = gi.cvar("use_indicators", "1", 0); + use_xerp = gi.cvar("use_xerp", "1", 0); +#endif // Discord SDK integration with Q2Pro cl_discord = gi.cvar("cl_discord", "0", 0); @@ -583,9 +634,9 @@ void InitGame( void ) ltk_showpath = gi.cvar( "ltk_showpath", "0", 0 ); ltk_chat = gi.cvar( "ltk_chat", "1", 0 ); ltk_routing = gi.cvar( "ltk_routing", "0", 0 ); - ltk_botfile = gi.cvar( "ltk_botfile", "botdata.cfg", 0); + ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); - ltk_showbots = gi.cvar ( "ltk_showbots", "0", 0); + ltk_classic = gi.cvar( "ltk_classic", "1", 0); #endif // items @@ -665,22 +716,30 @@ void InitGame( void ) gi.cvar_forceset("g_view_predict", "1"); gi.cvar_forceset("g_view_high", va("%d", STANDING_VIEWHEIGHT)); gi.cvar_forceset("g_view_low", va("%d", CROUCHING_VIEWHEIGHT)); + +#ifdef AQTION_EXTENSION + CvarSync_Set(clcvar_cl_antilag, "cl_antilag", "1"); + CvarSync_Set(clcvar_cl_indicators, "cl_indicators", "1"); + CvarSync_Set(clcvar_cl_xerp, "cl_xerp", "0"); + CvarSync_Set(clcvar_cl_spectatorhud, "cl_spectatorhud", "1"); + CvarSync_Set(clcvar_cl_spectatorkillfeed, "cl_spectatorkillfeed", "0"); +#endif } //========================================================= -void WriteGame (const char *filename, qboolean autosave) +void WriteGame (char *filename, qboolean autosave) { } -void ReadGame (const char *filename) +void ReadGame (char *filename) { } -void WriteLevel (const char *filename) +void WriteLevel (char *filename) { } -void ReadLevel (const char *filename) +void ReadLevel (char *filename) { } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index d33dc912e..b5098517d 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -242,6 +242,7 @@ void SP_misc_teleporter (edict_t * self); void SP_misc_teleporter_dest (edict_t * self); void SP_misc_blackhole (edict_t * self); + //zucc - item replacement function void CheckItem (edict_t * ent); int LoadFlagsFromFile (const char *mapname); @@ -377,7 +378,7 @@ void ED_CallSpawn (edict_t * ent) else G_FreeEdict(ent); } - else if (ctf->value) + else if (ctf->value || esp->value) { if(item->flags & IT_FLAG) SpawnItem(ent, item); @@ -567,8 +568,8 @@ Parses an edict out of the given string, returning the new position ed should be a properly initialized empty edict. ==================== */ -char * -ED_ParseEdict (char *data, edict_t * ent) +const char * +ED_ParseEdict (const char *data, edict_t * ent) { qboolean init; char keyname[256]; @@ -581,7 +582,7 @@ ED_ParseEdict (char *data, edict_t * ent) while (1) { // parse key - com_token = COM_ParseC(&data); + com_token = COM_Parse (&data); if (com_token[0] == '}') break; if (!data) @@ -590,7 +591,7 @@ ED_ParseEdict (char *data, edict_t * ent) Q_strncpyz(keyname, com_token, sizeof(keyname)); // parse value - com_token = COM_ParseC(&data); + com_token = COM_Parse (&data); if (!data) gi.error ("ED_ParseEntity: EOF without closing brace"); @@ -816,7 +817,11 @@ void G_LoadLocations( void ) gi.dprintf( "Found %d locations.\n", ml_count ); } -int Gamemode(void) // These are distinct game modes; you cannot have a teamdm tourney mode, for example + +/* +These are distinct game modes; you cannot have a teamdm tourney mode, for example +*/ +int Gamemode(void) { int gamemode = 0; if (teamdm->value) { @@ -831,13 +836,20 @@ int Gamemode(void) // These are distinct game modes; you cannot have a teamdm to gamemode = GM_DOMINATION; } else if (deathmatch->value) { gamemode = GM_DEATHMATCH; + } else if (esp->value && atl->value) { + gamemode = GM_ASSASSINATE_THE_LEADER; + } else if (esp->value && etv->value) { + gamemode = GM_ESCORT_THE_VIP; } return gamemode; } +/* +These are gamemode flags that change the rules of gamemodes. +For example, you can have a darkmatch matchmode 3team teamplay server +*/ int Gamemodeflag(void) -// These are gamemode flags that change the rules of gamemodes. -// For example, you can have a darkmatch matchmode 3team teamplay server + { int gamemodeflag = 0; char gmfstr[16]; @@ -864,7 +876,7 @@ Creates a server's entity / program execution context by parsing textual entity definitions out of an ent file. ============== */ -void SpawnEntities (const char *mapname, const char *entities, const char *spawnpoint) +void SpawnEntities (char *mapname, const char *entities, char *spawnpoint) { edict_t *ent = NULL; gclient_t *client; @@ -880,6 +892,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn teams[i].ready = teams[i].locked = 0; teams[i].pauses_used = teams[i].wantReset = 0; teams[i].captain = NULL; + if (esp->value) { + teams[i].leader = NULL; + teams[i].leader_dead = false; + } gi.cvar_forceset(teams[i].teamscore->name, "0"); } @@ -894,7 +910,15 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if (jump->value) { gi.cvar_forceset(gm->name, "jump"); + + // Force disable settings for jump mode gi.cvar_forceset(stat_logs->name, "0"); // Turn off stat logs for jump mode + gi.cvar_forceset(dm_choose->name, "0"); // Turn off dm_choose for jump mode + gi.cvar_forceset(uvtime->name, "0"); // Turn off uvtime in jump mode + gi.cvar_forceset(unique_items->name, "6"); // Enables holding all items at once, if toggled + gi.cvar_forceset(am->name, "0"); // Turns off attract mode + gi.cvar_forceset(ltk_loadbots->name, "0"); // Turns off bots + // if (teamplay->value) { gi.dprintf ("Jump Enabled - Forcing teamplay ff\n"); @@ -979,8 +1003,57 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn Q_strncpyz(teams[TEAM2].name, "BLUE", sizeof(teams[TEAM2].name)); Q_strncpyz(teams[TEAM1].skin, "male/ctf_r", sizeof(teams[TEAM1].skin)); Q_strncpyz(teams[TEAM2].skin, "male/ctf_b", sizeof(teams[TEAM2].skin)); - Q_strncpyz(teams[TEAM1].skin_index, "i_ctf1", sizeof(teams[TEAM1].skin_index)); - Q_strncpyz(teams[TEAM2].skin_index, "i_ctf2", sizeof(teams[TEAM2].skin_index)); + Q_strncpyz(teams[TEAM1].skin_index, "ctf_r", sizeof(teams[TEAM1].skin_index)); + Q_strncpyz(teams[TEAM2].skin_index, "ctf_b", sizeof(teams[TEAM2].skin_index)); + } + else if (esp->value) + { + gi.cvar_forceset(gm->name, "esp"); + //gameSettings |= GS_WEAPONCHOOSE; + gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); + + // Make sure teamplay is enabled + if (!teamplay->value) + { + gi.dprintf ("Espionage Enabled - Forcing teamplay on\n"); + gi.cvar_forceset(teamplay->name, "1"); + } + // ETV mode doesn't support 3 teams, but ATL does + if (etv->value && use_3teams->value) { + gi.dprintf ("Espionage ETV Enabled - Incompatible with 3 Teams, reverting to ATL mode\n"); + EspForceEspionage(ESPMODE_ATL); + } + if(teamdm->value) + { + gi.dprintf ("Espionage Enabled - Forcing Team DM off\n"); + gi.cvar_forceset(teamdm->name, "0"); + } + if (use_tourney->value) + { + gi.dprintf ("Espionage Enabled - Forcing Tourney off\n"); + gi.cvar_forceset(use_tourney->name, "0"); + } + if (dom->value) + { + gi.dprintf ("Espionage Enabled - Forcing Domination off\n"); + gi.cvar_forceset(dom->name, "0"); + } + if (!DMFLAGS(DF_NO_FRIENDLY_FIRE)) + { + gi.dprintf ("Espionage Enabled - Forcing Friendly Fire off\n"); + gi.cvar_forceset(dmflags->name, va("%i", (int)dmflags->value | DF_NO_FRIENDLY_FIRE)); + } + // uvtime is controlled via respawn logic + if (uvtime->value) + { + gi.cvar_forceset(uvtime->name, "0"); + } + // Sane defaults for timelimit or roundlimit + // favoring a roundlimit over a timelimit, if not defined + if (!roundlimit->value) + gi.cvar_forceset(roundlimit->name, "20"); + else if (!timelimit->value) + gi.cvar_forceset(timelimit->name, "20"); } else if (dom->value) { @@ -1011,10 +1084,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn Q_strncpyz(teams[TEAM3].name, "GREEN", sizeof(teams[TEAM3].name)); Q_strncpyz(teams[TEAM1].skin, "male/ctf_r", sizeof(teams[TEAM1].skin)); Q_strncpyz(teams[TEAM2].skin, "male/ctf_b", sizeof(teams[TEAM2].skin)); - Q_strncpyz(teams[TEAM3].skin, "male/commando", sizeof(teams[TEAM3].skin)); - Q_strncpyz(teams[TEAM1].skin_index, "i_ctf1", sizeof(teams[TEAM1].skin_index)); - Q_strncpyz(teams[TEAM2].skin_index, "i_ctf2", sizeof(teams[TEAM2].skin_index)); - Q_strncpyz(teams[TEAM3].skin_index, "i_pack", sizeof(teams[TEAM3].skin_index)); + Q_strncpyz(teams[TEAM3].skin, "male/ctf_g", sizeof(teams[TEAM3].skin)); + Q_strncpyz(teams[TEAM1].skin_index, "ctf_r_i", sizeof(teams[TEAM1].skin_index)); + Q_strncpyz(teams[TEAM2].skin_index, "ctf_b_i", sizeof(teams[TEAM2].skin_index)); + Q_strncpyz(teams[TEAM3].skin_index, "ctf_g_i", sizeof(teams[TEAM3].skin_index)); } else if(teamdm->value) { @@ -1064,6 +1137,16 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.dprintf ("Matchmode Enabled - Forcing Tourney off\n"); gi.cvar_forceset(use_tourney->name, "0"); } + if (esp->value && esp_matchmode->value) + { + gi.dprintf ("Matchmode Enabled - Forcing Espionage defaults\n"); + MM_EspDefaultSettings(); + } + if (auto_join->value == 2) + { + gi.dprintf ("Matchmode Enabled - Forcing auto_join off\n"); + gi.cvar_forceset(auto_join->name, "0"); + } } else if (use_tourney->value) { @@ -1116,7 +1199,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn // Set serverinfo correctly for gamemodeflags Gamemodeflag(); - #if USE_AQTION + #ifdef USE_AQTION generate_uuid(); // Run this once every time a map loads to generate a unique id for stats (game.matchid) #endif @@ -1155,7 +1238,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn { client->clientNum = i; - if( auto_join->value ) + if( auto_join->value == 1 ) client->resp.team = saved_team; // combine name and skin into a configstring @@ -1224,8 +1307,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } } } - else if( dom->value ) - DomLoadConfig( level.mapname ); + else if(dom->value) + DomLoadConfig(level.mapname); + else if (esp->value) + EspLoadConfig(level.mapname); G_FindTeams(); @@ -1255,7 +1340,16 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn // Reload nodes and any persistent bots. ACEND_InitNodes(); ACEND_LoadNodes(); - ACESP_LoadBotConfig(); + + // Normal operations, load LTK bots as normal + if (Q_stricmp(am->string, "0") == 0) { + ACESP_LoadBotConfig(); + } else { + // Reset bot count, load initial bots + game.bot_count = 0; + attract_mode_bot_check(); + } + #endif } @@ -1381,6 +1475,8 @@ void G_SetupStatusbar( void ) } else if( dom->value ) DomSetupStatusbar(); + else if( esp->value ) + EspSetupStatusbar(); else if( jump->value ) strcpy( level.statusbar, jump_statusbar ); } @@ -1473,8 +1569,7 @@ G_UpdatePlayerStatusbar void G_UpdatePlayerStatusbar( edict_t * ent, int force ) { char *playerStatusbar; - - if (!teamplay->value || teamCount != 2 || spectator_hud->value < 0 || (spectator_hud->value == 0 && !(ent->client->pers.spec_flags & SPECFL_SPECHUD))) { + if (!teamplay->value || teamCount != 2 || spectator_hud->value <= 0 || !(ent->client->pers.spec_flags & SPECFL_SPECHUD)) { return; } @@ -1564,6 +1659,8 @@ void SP_worldspawn (edict_t * ent) level.pic_health = gi.imageindex("i_health"); gi.imageindex("field_3"); + level.pic_esp_respawn_icon = gi.imageindex("i_ctf1t"); // Espionage respawn icon + level.pic_teamplay_timer_icon = gi.imageindex("i_ctf1t"); // Teamplay timer icon // zucc - preload sniper stuff level.pic_sniper_mode[1] = gi.imageindex("scope2x"); @@ -1608,24 +1705,28 @@ void SP_worldspawn (edict_t * ent) gi.imageindex("sbfctf2"); } - for(i = TEAM1; i <= teamCount; i++) - { - if (teams[i].skin_index[0] == 0) { - // If the action.ini file isn't found, set default skins rather than kill the server - gi.dprintf("WARNING: No skin was specified for team %i in config file, server either could not find it or is does not exist.\n", i); - gi.dprintf("Setting default team names, skins and skin indexes.\n"); - Q_strncpyz(teams[TEAM1].name, "RED", sizeof(teams[TEAM1].name)); - Q_strncpyz(teams[TEAM2].name, "BLUE", sizeof(teams[TEAM2].name)); - Q_strncpyz(teams[TEAM3].name, "GREEN", sizeof(teams[TEAM3].name)); - Q_strncpyz(teams[TEAM1].skin, "male/ctf_r", sizeof(teams[TEAM1].skin)); - Q_strncpyz(teams[TEAM2].skin, "male/ctf_b", sizeof(teams[TEAM2].skin)); - Q_strncpyz(teams[TEAM3].skin, "male/commando", sizeof(teams[TEAM3].skin)); - Q_strncpyz(teams[TEAM1].skin_index, "i_ctf1", sizeof(teams[TEAM1].skin_index)); - Q_strncpyz(teams[TEAM2].skin_index, "i_ctf2", sizeof(teams[TEAM2].skin_index)); - Q_strncpyz(teams[TEAM3].skin_index, "i_pack", sizeof(teams[TEAM3].skin_index)); - //exit(1); + // Espionage HUD is setup in SetEspStats() + if (!esp->value) { + for(i = TEAM1; i <= teamCount; i++) + { + if (teams[i].skin_index[0] == 0) { + // If the action.ini file isn't found, set default skins rather than kill the server + // Espionage has its own defaults + gi.dprintf("WARNING: No skin was specified for team %i in config file, server either could not find it or is does not exist.\n", i); + gi.dprintf("Setting default team names, skins and skin indexes.\n"); + Q_strncpyz(teams[TEAM1].name, "RED", sizeof(teams[TEAM1].name)); + Q_strncpyz(teams[TEAM2].name, "BLUE", sizeof(teams[TEAM2].name)); + Q_strncpyz(teams[TEAM3].name, "GREEN", sizeof(teams[TEAM3].name)); + Q_strncpyz(teams[TEAM1].skin, "male/ctf_r", sizeof(teams[TEAM1].skin)); + Q_strncpyz(teams[TEAM2].skin, "male/ctf_b", sizeof(teams[TEAM2].skin)); + Q_strncpyz(teams[TEAM3].skin, "male/ctf_g", sizeof(teams[TEAM3].skin)); + Q_strncpyz(teams[TEAM1].skin_index, "ctf_r", sizeof(teams[TEAM1].skin_index)); + Q_strncpyz(teams[TEAM2].skin_index, "ctf_b", sizeof(teams[TEAM2].skin_index)); + Q_strncpyz(teams[TEAM3].skin_index, "ctf_g", sizeof(teams[TEAM3].skin_index)); + //exit(1); + } + level.pic_teamskin[i] = gi.imageindex(teams[i].skin_index); } - level.pic_teamskin[i] = gi.imageindex(teams[i].skin_index); } level.snd_lights = gi.soundindex("atl/lights.wav"); @@ -1662,6 +1763,15 @@ void SP_worldspawn (edict_t * ent) gi.soundindex("misc/secret.wav"); // used for ctf swap sound gi.soundindex("weapons/grenlf1a.wav"); // respawn sound + // Precache insane sound effects, 11 of them total (1-11) + int insanesounds; + for (insanesounds = 1; insanesounds < 12; insanesounds++) + { + char soundname[32]; + sprintf(soundname, "insane/insane%i.wav", insanesounds); + gi.soundindex(soundname); + } + PrecacheItems(); PrecacheRadioSounds(); PrecacheUserSounds(); @@ -1697,7 +1807,7 @@ void SP_worldspawn (edict_t * ent) gi.soundindex("*fall2.wav"); gi.soundindex("*gurp1.wav"); // drowning damage gi.soundindex("*gurp2.wav"); - gi.soundindex("*jump1.wav"); // player jump + //gi.soundindex("*jump1.wav"); // player jump - AQ2 doesn't use this gi.soundindex("*pain25_1.wav"); gi.soundindex("*pain25_2.wav"); gi.soundindex("*pain50_1.wav"); @@ -1723,6 +1833,11 @@ void SP_worldspawn (edict_t * ent) level.model_null = gi.modelindex("sprites/null.sp2"); // null sprite level.model_lsight = gi.modelindex("sprites/lsight.sp2"); // laser sight dot sprite +#ifdef AQTION_EXTENSION + level.model_arrow = gi.modelindex("models/indicator/arrow_red.md2"); + gi.modelindex("models/indicator/arrow_blue.md2"); + gi.modelindex("models/indicator/arrow_green.md2"); +#endif gi.soundindex("player/gasp1.wav"); // gasping for air gi.soundindex("player/gasp2.wav"); // head breaking surface, not gasping @@ -1747,7 +1862,6 @@ void SP_worldspawn (edict_t * ent) sm_meat_index = gi.modelindex("models/objects/gibs/sm_meat/tris.md2"); gi.modelindex("models/objects/gibs/skull/tris.md2"); - gi.modelindex("models/objects/gibs/head2/tris.md2"); // @@ -1782,40 +1896,6 @@ void SP_worldspawn (edict_t * ent) gi.configstring(CS_LIGHTS + 11, "abcdefghijklmnopqrrqponmlkjihgfedcba"); // 11 SLOW PULSE NOT FADE TO BLACK // styles 32-62 are assigned by the light program for switchable lights gi.configstring(CS_LIGHTS + 63, "a"); // 63 testing - - -#ifdef AQTION_EXTENSION -#ifdef AQTION_HUD - if (teamplay->value) - { - int base_y = -8; - int team_amt = 2; - - if (use_3teams->value) - { - teams[TEAM3].ghud_icon = -1; // just so we know these are unused - teams[TEAM3].ghud_num = -1; // - - base_y -= 112; - team_amt = 3; - } - else - base_y -= 72; - - int i; - for (i = 1; i <= team_amt; i++) - { - teams[i].ghud_icon = Ghud_AddIcon(8, base_y, level.pic_teamskin[i], 32, 32); - Ghud_SetAnchor(teams[i].ghud_icon, 0, 1); - - teams[i].ghud_num = Ghud_AddNumber(44, base_y + 2, 0); - Ghud_SetAnchor(teams[i].ghud_num, 0, 1); - - base_y += 40; - } - } -#endif -#endif } int LoadFlagsFromFile (const char *mapname) @@ -1887,7 +1967,7 @@ int LoadFlagsFromFile (const char *mapname) // This function changes the nearest two spawnpoint from each flag // to info_player_teamX, so the other team won't restart // beneath the flag of the other team -void ChangePlayerSpawns () +void ChangePlayerSpawns (void) { edict_t *flag1 = NULL, *flag2 = NULL; edict_t *spot, *spot1, *spot2, *spot3, *spot4; diff --git a/src/action/g_utils.c b/src/action/g_utils.c index a3f0674cf..2daef7604 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -16,7 +16,7 @@ #include "g_local.h" -void G_ProjectSource (const vec3_t point, const vec3_t distance, const vec3_t forward, const vec3_t right, +void G_ProjectSource (vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result) { result[0] = point[0] + forward[0] * distance[0] + right[0] * distance[1]; @@ -70,7 +70,7 @@ Returns entities that have origins within a spherical area findradius (origin, radius) ================= */ -edict_t *findradius (edict_t * from, const vec3_t org, float rad) +edict_t *findradius (edict_t * from, vec3_t org, float rad) { vec3_t eorg; int j; @@ -288,11 +288,8 @@ VectorToString This is just a convenience function for printing vectors - -// No longer needed as Q2Pro inc/shared/shared.h provides this ============= */ - // char *vtos (const vec3_t v) // { // static int index; @@ -358,7 +355,7 @@ float vectoyaw (vec3_t vec) } -void vectoangles (const vec3_t value1, vec3_t angles) +void vectoangles (vec3_t value1, vec3_t angles) { float forward; float yaw, pitch; @@ -722,3 +719,41 @@ qboolean infront( edict_t *self, edict_t *other ) return false; } #endif + +/* +============= +Toggle Cvars +============= +*/ + +void disablecvar(cvar_t *cvar, char *msg) +{ + // Cvar is already disabled, do nothing + if (!cvar->value) + return; + + if (msg) + gi.dprintf("%s: disabling %s\n", msg, cvar->name); + + gi.cvar_forceset(cvar->name, "0"); +} + +void enablecvar(cvar_t *cvar, char *msg) +{ + // Cvar is already enabled, do nothing + if (cvar->value) + return; + + if (msg) + gi.dprintf("%s: enabling %s\n", msg, cvar->name); + + gi.cvar_forceset(cvar->name, "1"); +} + +/* +Supply integer in seconds, calculates number of seconds +based on variable FPS (HZ) +*/ +int eztimer(int seconds){ + return (level.framenum + seconds * HZ); +} \ No newline at end of file diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index 718cf16ac..372a32104 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -91,7 +91,7 @@ fire_lead This is an internal support routine used for bullet/pellet based weapons. ================= */ -static void fire_lead (edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int te_impact, int hspread, int vspread, int mod) +static void fire_lead (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int te_impact, int hspread, int vspread, int mod) { trace_t tr; vec3_t dir, forward, right, up, end; @@ -258,7 +258,7 @@ Fires a single round. Used for machinegun and chaingun. Would be fine for pistols, rifles, etc.... ================= */ -void fire_bullet(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) +void fire_bullet(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) { setFFState(self); antilag_rewind_all(self); @@ -268,7 +268,7 @@ void fire_bullet(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, i // zucc fire_load_ap for rounds that pass through soft targets and keep going -static void fire_lead_ap(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int te_impact, int hspread, int vspread, int mod) +static void fire_lead_ap(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int te_impact, int hspread, int vspread, int mod) { trace_t tr; vec3_t dir, forward, right, up, end; @@ -466,7 +466,7 @@ static void fire_lead_ap(edict_t *self, const vec3_t start, vec3_t aimdir, int d } // zucc - for the M4 -void fire_bullet_sparks (edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) +void fire_bullet_sparks (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) { setFFState(self); antilag_rewind_all(self); @@ -475,7 +475,7 @@ void fire_bullet_sparks (edict_t *self, const vec3_t start, vec3_t aimdir, int d } // zucc - for sniper -void fire_bullet_sniper (edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) +void fire_bullet_sniper (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int mod) { setFFState (self); antilag_rewind_all(self); @@ -548,7 +548,7 @@ fire_shotgun Shoots shotgun pellets. Used by shotgun and super shotgun. ================= */ -void fire_shotgun(edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int count, int mod) +void fire_shotgun(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int hspread, int vspread, int count, int mod) { int i; @@ -605,7 +605,7 @@ void blaster_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t * } //SLIC2 changed argument name hyper to hyperb -void fire_blaster(edict_t *self, const vec3_t start, vec3_t dir, int damage, int speed, int effect, qboolean hyperb) +void fire_blaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, qboolean hyperb) { edict_t *bolt; trace_t tr; @@ -745,8 +745,8 @@ static void Grenade_Touch (edict_t *ent, edict_t *other, cplane_t *plane, csurfa //Grenade_Explode(ent); } -void fire_grenade2(edict_t *self, const vec3_t start, const vec3_t aimdir, int damage, - int speed, int timer, float damage_radius, qboolean held) +void fire_grenade2 (edict_t * self, vec3_t start, vec3_t aimdir, int damage, + int speed, int timer, float damage_radius, qboolean held) { edict_t *grenade; vec3_t dir; @@ -792,8 +792,8 @@ void fire_grenade2(edict_t *self, const vec3_t start, const vec3_t aimdir, int d } -void P_ProjectSource (gclient_t *client, const vec3_t point, const vec3_t distance, - const vec3_t forward, const vec3_t right, vec3_t result); +void P_ProjectSource (gclient_t *client, vec3_t point, vec3_t distance, + vec3_t forward, vec3_t right, vec3_t result); void kick_attack (edict_t *ent) { @@ -833,6 +833,11 @@ void kick_attack (edict_t *ent) if (tr.ent->health <= 0) return; + // PaTMaN's jmod kickable toggle + if (tr.ent->client && jump->value) + if (!(tr.ent->client->resp.toggles & TG_KICKABLE)) + return; + if (tr.ent->client) { if (tr.ent->client->uvTime) @@ -854,6 +859,9 @@ void kick_attack (edict_t *ent) else T_Damage(tr.ent, ent, ent, forward, tr.endpos, tr.plane.normal, damage, kick, 0, MOD_KICK); + // Stat add + Stats_AddHit(ent, MOD_KICK, LOC_NO); + // Stat end gi.sound(ent, CHAN_WEAPON, level.snd_kick, 1, ATTN_NORM, 0); PlayerNoise (ent, ent->s.origin, PNOISE_SELF); if (tr.ent->client && (tr.ent->client->curr_weap == M4_NUM @@ -934,6 +942,9 @@ void punch_attack(edict_t * ent) T_Damage(tr.ent, ent, ent, forward, tr.endpos, tr.plane.normal, damage, kick, 0, MOD_PUNCH); + // Stat add + Stats_AddHit(ent, MOD_PUNCH, LOC_NO); + // Stat end gi.sound(ent, CHAN_WEAPON, level.snd_kick, 1, ATTN_NORM, 0); PlayerNoise(ent, ent->s.origin, PNOISE_SELF); @@ -974,7 +985,7 @@ void punch_attack(edict_t * ent) // 1 - hit player // 2 - hit wall -int knife_attack (edict_t *self, const vec3_t start, vec3_t aimdir, int damage, int kick) +int knife_attack (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick) { trace_t tr; vec3_t end; @@ -1121,7 +1132,7 @@ void knife_touch(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf } -void knife_throw(edict_t *self, const vec3_t start, vec3_t dir, int damage, int speed) +void knife_throw(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed) { edict_t *knife; trace_t tr; diff --git a/src/action/p_antilag.c b/src/action/p_antilag.c index 25a59adbe..9a3a8521c 100644 --- a/src/action/p_antilag.c +++ b/src/action/p_antilag.c @@ -74,7 +74,8 @@ void antilag_rewind_all(edict_t *ent) edict_t *who; antilag_t *state; - for (int i = 1; i < game.maxclients; i++) + int i; + for (i = 1; i < game.maxclients; i++) { who = g_edicts + i; if (!who->inuse) @@ -117,7 +118,8 @@ void antilag_unmove_all(void) edict_t *who; antilag_t *state; - for (int i = 1; i < game.maxclients; i++) + int i; + for (i = 1; i < game.maxclients; i++) { who = g_edicts + i; if (!who->inuse) diff --git a/src/action/p_client.c b/src/action/p_client.c index 884814812..e6bd8aadb 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -320,6 +320,7 @@ #include "m_player.h" #include "cgf_sfx_glass.h" + static void FreeClientEdicts(gclient_t *client) { //remove lasersight @@ -339,91 +340,100 @@ static void FreeClientEdicts(gclient_t *client) G_FreeEdict(client->ctf_grapple); client->ctf_grapple = NULL; } + +#ifdef AQTION_EXTENSION + //remove arrow + if (client->arrow) { + G_FreeEdict(client->arrow); + client->arrow = NULL; + } +#endif +} + +void Announce_Reward(edict_t *ent, int rewardType) { + char buf[256]; + char *soundFile; + char *playername = ent->client->pers.netname; + + switch (rewardType) { + case IMPRESSIVE: + sprintf(buf,"-------------------\n" "IMPRESSIVE %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakKills/5); + soundFile = "tng/impressive.wav"; + break; + case EXCELLENT: + sprintf(buf,"-------------------\n" "EXCELLENT %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakKills/12); + soundFile = "tng/excellent.wav"; + break; + case ACCURACY: + sprintf(buf,"-------------------\n" "ACCURACY %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakHS/3); + soundFile = "tng/accuracy.wav"; + break; + case DOMINATING: + sprintf(buf,"-------------------\n" "%s IS DOMINATING!\n" "-------------------\n", playername); + soundFile = "radio/male/deliv3.wav"; + break; + case UNSTOPPABLE: + sprintf(buf,"-------------------\n" "%s IS UNSTOPPABLE!\n" "-------------------\n", playername); + soundFile = "radio/male/deliv3.wav"; + break; + default: + gi.dprintf("%s: Unknown reward type %d for %s\n", __FUNCTION__, rewardType, playername); + return; // Something didn't jive here? + } + + CenterPrintAll(buf); + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); + + #ifdef USE_AQTION + if (stat_logs->value) + LogAward(ent, rewardType); + #endif } void Add_Frag(edict_t * ent, int mod) { - char buf[256]; int frags = 0; if (in_warmup) return; ent->client->resp.kills++; + // All normal weapon damage if (mod > 0 && mod < MAX_GUNSTAT) { ent->client->resp.gunstats[mod].kills++; } + // Grenade splash, kicks and punch damage + if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH))) { + ent->client->resp.gunstats[mod].kills++; + } - if (teamplay->value && teamdm->value != 2) + if (IS_ALIVE(ent)) { - ent->client->resp.score++; // just 1 normal kill + ent->client->resp.streakKills++; + ent->client->resp.roundStreakKills++; + if (ent->client->resp.streakKills > ent->client->resp.streakKillsHighest) + ent->client->resp.streakKillsHighest = ent->client->resp.streakKills; - if (IS_ALIVE(ent)) + if (ent->client->resp.streakKills % 5 == 0 && use_rewards->value) { - ent->client->resp.streakKills++; - if (ent->client->resp.streakKills > ent->client->resp.streakKillsHighest) - ent->client->resp.streakKillsHighest = ent->client->resp.streakKills; - - if (ent->client->resp.streakKills % 5 == 0 && use_rewards->value) - { - sprintf(buf, "IMPRESSIVE %s!", ent->client->pers.netname); - CenterPrintAll(buf); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, - gi.soundindex("tng/impressive.wav"), 1.0, ATTN_NONE, 0.0); - - #if USE_AQTION - - #ifndef NO_BOTS - // Check if there's an AI bot in the game, if so, do nothing - if (game.ai_ent_found) { - return; - } - #endif - if (stat_logs->value) { - char steamid[24]; - char discordid[24]; - Q_strncpyz(steamid, Info_ValueForKey(ent->client->pers.userinfo, "steamid"), sizeof(steamid)); - Q_strncpyz(discordid, Info_ValueForKey(ent->client->pers.userinfo, "cl_discord_id"), sizeof(discordid)); - LogAward(steamid, discordid, IMPRESSIVE); - } - #endif - } - else if (ent->client->resp.streakKills % 12 == 0 && use_rewards->value) - { - sprintf(buf, "EXCELLENT %s (%dx)!", ent->client->pers.netname,ent->client->resp.streakKills/12); - CenterPrintAll(buf); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, - gi.soundindex("tng/excellent.wav"), 1.0, ATTN_NONE, 0.0); - - #if USE_AQTION - #ifndef NO_BOTS - // Check if there's an AI bot in the game, if so, do nothing - if (game.ai_ent_found) { - return; - } - #endif - if (stat_logs->value) { - char steamid[24]; - char discordid[24]; - Q_strncpyz(steamid, Info_ValueForKey(ent->client->pers.userinfo, "steamid"), sizeof(steamid)); - Q_strncpyz(discordid, Info_ValueForKey(ent->client->pers.userinfo, "cl_discord_id"), sizeof(discordid)); - LogAward(steamid, discordid, EXCELLENT); - } - #endif - } + Announce_Reward(ent, IMPRESSIVE); + } + else if (ent->client->resp.streakKills % 12 == 0 && use_rewards->value) + { + Announce_Reward(ent, EXCELLENT); } + } - if(teamdm->value) - teams[ent->client->resp.team].score++; - // end changing sound dir - } else { //Deathmatch + // Regular frag for teamplay/matchmode + if (teamplay->value && teamdm->value != 2) + ent->client->resp.score++; // just 1 normal kill - if (IS_ALIVE(ent)) { - ent->client->resp.streakKills++; - if (ent->client->resp.streakKills > ent->client->resp.streakKillsHighest) - ent->client->resp.streakKillsHighest = ent->client->resp.streakKills; - } + // Increment team score if TeamDM is enabled + if(teamdm->value) + teams[ent->client->resp.team].score++; + // Streak kill rewards in Deathmatch mode + if (deathmatch->value && !teamplay->value) { if (ent->client->resp.streakKills < 4 || ! use_rewards->value) frags = 1; else if (ent->client->resp.streakKills < 8) @@ -446,39 +456,48 @@ void Add_Frag(edict_t * ent, int mod) } ent->client->resp.score += frags; - if(ent->client->resp.streakKills) - gi.cprintf(ent, PRINT_HIGH, "Kill count: %d\n", ent->client->resp.streakKills); - + // Award team with appropriate streak reward count if(teamdm->value) teams[ent->client->resp.team].score += frags; - } - // AQ:TNG Igor[Rock] changing sound dir - if (fraglimit->value && use_warnings->value) { - if (ent->client->resp.score == fraglimit->value - 1) { - if (fragwarning < 3) { - CenterPrintAll("1 FRAG LEFT..."); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, - gi.soundindex("tng/1_frag.wav"), 1.0, ATTN_NONE, 0.0); - fragwarning = 3; - } - } else if (ent->client->resp.score == fraglimit->value - 2) { - if (fragwarning < 2) { - CenterPrintAll("2 FRAGS LEFT..."); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, - gi.soundindex("tng/2_frags.wav"), 1.0, ATTN_NONE, 0.0); - fragwarning = 2; - } - } else if (ent->client->resp.score == fraglimit->value - 3) { - if (fragwarning < 1) { - CenterPrintAll("3 FRAGS LEFT..."); - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, - gi.soundindex("tng/3_frags.wav"), 1.0, ATTN_NONE, 0.0); - fragwarning = 1; + // AQ:TNG Igor[Rock] changing sound dir + if (fraglimit->value && use_warnings->value) { + if (ent->client->resp.score == fraglimit->value - 1) { + if (fragwarning < 3) { + CenterPrintAll("1 FRAG LEFT..."); + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, + gi.soundindex("tng/1_frag.wav"), 1.0, ATTN_NONE, 0.0); + fragwarning = 3; + } + } else if (ent->client->resp.score == fraglimit->value - 2) { + if (fragwarning < 2) { + CenterPrintAll("2 FRAGS LEFT..."); + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, + gi.soundindex("tng/2_frags.wav"), 1.0, ATTN_NONE, 0.0); + fragwarning = 2; + } + } else if (ent->client->resp.score == fraglimit->value - 3) { + if (fragwarning < 1) { + CenterPrintAll("3 FRAGS LEFT..."); + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, + gi.soundindex("tng/3_frags.wav"), 1.0, ATTN_NONE, 0.0); + fragwarning = 1; + } } } + // end of changing sound dir + } + + // Announce kill streak to player if use_killcounts is enabled on server + if (use_killcounts->value) { + // Report only killstreak during that round + if(ent->client->resp.roundStreakKills) + gi.cprintf(ent, PRINT_HIGH, "Kill count: %d\n", ent->client->resp.roundStreakKills); + } else { + // Report total killstreak across previous rounds + if(ent->client->resp.streakKills) + gi.cprintf(ent, PRINT_HIGH, "Kill count: %d\n", ent->client->resp.streakKills); } - // end of changing sound dir } void Subtract_Frag(edict_t * ent) @@ -489,6 +508,7 @@ void Subtract_Frag(edict_t * ent) ent->client->resp.kills--; ent->client->resp.score--; ent->client->resp.streakKills = 0; + ent->client->resp.roundStreakKills = 0; if(teamdm->value) teams[ent->client->resp.team].score--; } @@ -499,8 +519,10 @@ void Add_Death( edict_t *ent, qboolean end_streak ) return; ent->client->resp.deaths ++; - if( end_streak ) + if( end_streak ) { ent->client->resp.streakKills = 0; + ent->client->resp.roundStreakKills = 0; + } } // FRIENDLY FIRE functions @@ -697,6 +719,10 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) int n; self->client->resp.ctf_capstreak = 0; + self->client->resp.dom_capstreak = 0; + + if (esp->value && IS_LEADER(self)) + self->client->resp.esp_capstreak = 0; friendlyFire = meansOfDeath & MOD_FRIENDLY_FIRE; mod = meansOfDeath & ~MOD_FRIENDLY_FIRE; @@ -706,7 +732,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) // Reki: Print killfeed to spectators who ask for easily parsable stuff edict_t *other; - for (int j = 1; j <= game.maxclients; j++) { + int j; + for (j = 1; j <= game.maxclients; j++) { other = &g_edicts[j]; if (!other->inuse || !other->client || !teamplay->value) continue; @@ -832,7 +859,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); - #if USE_AQTION + #ifdef USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogKill(self, inflictor, self->client->attacker); } @@ -869,8 +896,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) } self->enemy = NULL; - - #if USE_AQTION + + #ifdef USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogWorldKill(self); } @@ -1211,7 +1238,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); - #if USE_AQTION + #ifdef USE_AQTION if (stat_logs->value) { LogKill(self, inflictor, attacker); } @@ -1241,7 +1268,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) PrintDeathMessage(death_msg, self); IRC_printf(IRC_T_DEATH, death_msg); - #if USE_AQTION + #ifdef USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogWorldKill(self); } @@ -1280,7 +1307,7 @@ void EjectWeapon(edict_t * ent, gitem_t * item) ent->client->v_angle[YAW] += spread; drop->spawnflags = DROPPED_PLAYER_ITEM; if (!in_warmup) - drop->think = temp_think_specweap; + drop->think = SpecialWeaponRespawnTimer; } } @@ -1310,9 +1337,13 @@ void EjectMedKit( edict_t *ent, int medkit ) void TossItemsOnDeath(edict_t * ent) { gitem_t *item; - qboolean quad; + qboolean quad = false; int i; + // Don't drop items if leader, this is just a mess + if (esp->value && IS_LEADER(ent)) + return; + // don't bother dropping stuff when allweapons/items is active if (allitem->value) { // remove the lasersight because then the observer might have it @@ -1354,21 +1385,6 @@ void TossItemsOnDeath(edict_t * ent) quad = false; else quad = (ent->client->quad_framenum > (level.framenum + HZ)); - - // if (quad) { - // edict_t *drop; - // float spread; - - // spread = 300.0 * crandom(); - // ent->client->v_angle[YAW] += spread; - // drop = Drop_Item(ent, FindItemByClassname("item_quad")); - // ent->client->v_angle[YAW] -= spread; - // drop->spawnflags |= DROPPED_PLAYER_ITEM; - - // drop->touch = Touch_Item; - // drop->nextthink = ent->client->quad_framenum; - // drop->think = G_FreeEdict; - // } } void TossClientWeapon(edict_t * self) @@ -1400,17 +1416,6 @@ void TossClientWeapon(edict_t * self) self->client->v_angle[YAW] += spread; drop->spawnflags = DROPPED_PLAYER_ITEM; } - - // if (quad) { - // self->client->v_angle[YAW] += spread; - // drop = Drop_Item(self, FindItemByClassname("item_quad")); - // self->client->v_angle[YAW] -= spread; - // drop->spawnflags |= DROPPED_PLAYER_ITEM; - - // drop->touch = Touch_Item; - // drop->nextthink = self->client->quad_framenum; - // drop->think = G_FreeEdict; - // } } /* @@ -1449,7 +1454,7 @@ void LookAtKiller(edict_t * self, edict_t * inflictor, edict_t * attacker) player_die ================== */ -void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) { int n, mod; @@ -1485,6 +1490,9 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage if (ctf->value) { self->client->respawn_framenum = level.framenum + CTFGetRespawnTime(self) * HZ; } + else if (esp->value) { + self->client->respawn_framenum = level.framenum + EspGetRespawnTime(self) * HZ; + } else if(teamdm->value) { self->client->respawn_framenum = level.framenum + (int)(teamdm_respawn->value * HZ); } @@ -1497,6 +1505,10 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage if (ctf->value) CTFFragBonuses(self, inflictor, attacker); + // TODO: Make this work + if (esp->value) + EspScoreBonuses(self, attacker); + //TossClientWeapon (self); TossItemsOnDeath(self); @@ -1509,9 +1521,16 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage if (!teamplay->value) Cmd_Help_f(self); // show scores - // always reset chase to killer, even if NULL - if(limchasecam->value < 2 && attacker && attacker->client) - self->client->resp.last_chase_target = attacker; + /* + Updated chase cam calls + */ + if (limchasecam->value < 2 && attacker && attacker->client) { + // if (esp->value) + // EspionageChaseCam(self, attacker); + // else + if (teamplay->value) + TeamplayChaseCam(self, attacker); + } } // remove powerups self->client->quad_framenum = 0; @@ -1618,6 +1637,12 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage // in ctf, when a player dies check if he should be moved to the other team if(ctf->value) CheckForUnevenTeams(self); + + if (esp->value && IS_LEADER(self)) { + if (!in_warmup && team_round_going) { + EspReportLeaderDeath(self); + } + } } /* @@ -1659,6 +1684,44 @@ float PlayersRangeFromSpot(edict_t * spot) return bestplayerdistance; } +/* +================ +SelectAnyDeathmatchSpawnPoint + +I just need a spawnpoint, any spawnpoint... +================ +*/ +edict_t *SelectAnyDeathmatchSpawnPoint(void) +{ + edict_t *spot = NULL; + edict_t **spots = NULL; + int count = 0; + + gi.dprintf("Warning: too few spawnpoints in this map\n"); + + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + spots = realloc(spots, sizeof(*spots) * (count + 1)); + if (!spots) { + // Handle memory allocation error + return NULL; + } + spots[count++] = spot; + } + + if (count == 0) { + // No DM spawns found, womp womp + return NULL; + } + + // Select a random spot + spot = spots[rand() % count]; + + // Free the spots array + free(spots); + + return spot; +} + /* ================ SelectRandomDeathmatchSpawnPoint @@ -1753,10 +1816,72 @@ edict_t *SelectFarthestDeathmatchSpawnPoint(void) edict_t *SelectDeathmatchSpawnPoint(void) { + edict_t *spot = NULL; + if (DMFLAGS(DF_SPAWN_FARTHEST)) - return SelectFarthestDeathmatchSpawnPoint(); + spot = SelectFarthestDeathmatchSpawnPoint(); else - return SelectRandomDeathmatchSpawnPoint(); + spot = SelectRandomDeathmatchSpawnPoint(); + + return spot; +} + +/* +UncommonSpawnPoint + +This is used when a map does not have the appropriate spawn points for the +game mode being played. This is used to prevent server crashes if a player +cannot spawn. +*/ + +edict_t *UncommonSpawnPoint(void) +{ + edict_t *spot = NULL; + + if (!spot) { + gi.dprintf("Warning: failed to find deathmatch spawn point, unexpected spawns will be utilized\n"); + + /* + Try all possible classes of spawn points, and use DM weapon spawns as a last resort. + */ + char* spawnpoints[] = { + "info_player_start", + "info_player_coop", + "info_player_team1", + "info_player_team2", + "info_player_team3", + "info_player_deathmatch", + "weapon_bfg", + "weapon_chaingun", + "weapon_machinegun", + "weapon_rocketlauncher", + "weapon_shotgun", + "weapon_supershotgun", + "weapon_railgun" + }; + size_t num_spawnpoints = sizeof(spawnpoints) / sizeof(spawnpoints[0]); + int i; + for (i = 0; i < num_spawnpoints; ++i) { + while ((spot = G_Find(spot, FOFS(classname), spawnpoints[i])) != NULL) { + if (!game.spawnpoint[0] && !spot->targetname) + break; + + if (!game.spawnpoint[0] || !spot->targetname) + continue; + + if (Q_stricmp(game.spawnpoint, spot->targetname) == 0) + break; + } + + if (spot) { + gi.dprintf("Warning: Uncommon spawn point of class %s\n", spawnpoints[i]); + gi.dprintf("**If you are the map author, you need to be utilizing MULTIPLE info_player_deathmatch or info_player_team entities**\n"); + break; + } + } + } + + return spot; } /* @@ -1764,49 +1889,45 @@ edict_t *SelectDeathmatchSpawnPoint(void) SelectSpawnPoint Chooses a player start, deathmatch start, coop start, etc +Espionage uses custom spawn points and custom respawns, but only +once a round has started, otherwise it uses the normal deathmatch +chosen ones ============ */ void SelectSpawnPoint(edict_t * ent, vec3_t origin, vec3_t angles) { edict_t *spot = NULL; + //espsettings_t *es = &espsettings; //FIREBLADE - if (ctf->value) + if (ctf->value) { spot = SelectCTFSpawnPoint(ent); - else if (dom->value) + } else if (esp->value) { + // SelectEspSpawnPoint handles respawns as well as initial spawnpoints + spot = SelectEspSpawnPoint(ent); + } else if (dom->value) { spot = SelectDeathmatchSpawnPoint(); - else if (!(gameSettings & GS_DEATHMATCH) && ent->client->resp.team && !in_warmup) { + } else if (!(gameSettings & GS_DEATHMATCH) && ent->client->resp.team && !in_warmup) { spot = SelectTeamplaySpawnPoint(ent); + } else if (jump->value) { + spot = SelectFarthestDeathmatchSpawnPoint(); } else { spot = SelectDeathmatchSpawnPoint(); } - // find a single player start spot - if (!spot) { - gi.dprintf("Warning: failed to find deathmatch spawn point\n"); + // desperation mode, find a spawnpoint or the server will crash + if (!spot) + spot = UncommonSpawnPoint(); - while ((spot = G_Find(spot, FOFS(classname), "info_player_start")) != NULL) { - if (!game.spawnpoint[0] && !spot->targetname) - break; - - if (!game.spawnpoint[0] || !spot->targetname) - continue; - - if (Q_stricmp(game.spawnpoint, spot->targetname) == 0) - break; - } - - if (!spot) { - if (!game.spawnpoint[0]) { // there wasn't a spawnpoint without a target, so use any - spot = G_Find(spot, FOFS(classname), "info_player_start"); - } - if (!spot) { - gi.error("Couldn't find spawn point %s\n", game.spawnpoint); - return; - } - } - } + // Last resort, just choose any info_player_deathmatch even if it turns into + // a messy telefrag nightmare + if (!spot) + spot = SelectAnyDeathmatchSpawnPoint(); + // If still no spot, then this map is just not playable, sorry + if (!spot) + Com_Error(ERR_DROP, "Couldn't find spawn point, map is not playable!"); + VectorCopy(spot->s.origin, origin); origin[2] += 9; VectorCopy(spot->s.angles, angles); @@ -1826,7 +1947,7 @@ void InitBodyQue(void) } } -void body_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +void body_die(edict_t * self, edict_t * inflictor, edict_t * attacker, int damage, const vec3_t point) { /* int n;*/ @@ -1895,7 +2016,7 @@ void CopyToBodyQue(edict_t * ent) gi.linkentity(body); } -void CleanBodies() +void CleanBodies(void) { int i; edict_t *ent; @@ -1912,6 +2033,8 @@ void CleanBodies() void respawn(edict_t *self) { + //gi.dprintf("%s tried to respawn\n", self->client->pers.netname); + if (self->solid != SOLID_NOT || self->deadflag == DEAD_DEAD) CopyToBodyQue(self); @@ -1938,6 +2061,16 @@ void respawn(edict_t *self) } self->client->respawn_framenum = level.framenum + 2 * HZ; + + + if (esp->value && team_round_going){ + // Optional respawn invulnerability in Espionage + if (esp_respawn_uvtime->value){ + if (esp_debug->value) + gi.dprintf("%s: Invuln activated\n", __FUNCTION__); + self->client->uvTime = (int)esp_respawn_uvtime->value; + } + } } //============================================================== @@ -2166,11 +2299,16 @@ void EquipClient(edict_t * ent) break; } + memset(&etemp, 0, sizeof(etemp)); if (client->pers.chosenItem) { - memset(&etemp, 0, sizeof(etemp)); etemp.item = client->pers.chosenItem; Pickup_Special(&etemp, ent); } + if (item_kit_mode->value && client->pers.chosenItem2){ + client->inventory[ITEM_INDEX(client->pers.chosenItem2)] = 1; + client->unique_item_total++; + } + } // Igor[Rock] start @@ -2302,6 +2440,11 @@ void ClientLegDamage(edict_t *ent) ent->client->leg_damage = 1; ent->client->leghits++; + if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) { // we don't limp with enhanced slippers, so just ignore this leg damage. + ent->client->leg_damage = 0; + return; + } + // Reki: limp_nopred behavior switch (ent->client->pers.limp_nopred & 255) { @@ -2324,7 +2467,7 @@ void ClientLegDamage(edict_t *ent) break; ent->client->pers.limp_nopred |= 256; case 1: - if (e_enhancedSlippers->value && INV_AMMO(ent, SLIP_NUM)) // we don't limp with enhanced slippers, so just ignore this leg damage. + if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) // we don't limp with enhanced slippers, so just ignore this leg damage. break; ent->client->ps.pmove.pm_flags |= PMF_NO_PREDICTION; @@ -2368,6 +2511,9 @@ void PutClientInServer(edict_t * ent) client_persistant_t pers; client_respawn_t resp; gitem_t *item; +#ifdef AQTION_EXTENSION + cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; +#endif // find a spawn point // do it before setting health back up, so farthest @@ -2382,12 +2528,19 @@ void PutClientInServer(edict_t * ent) // deathmatch wipes most client data every spawn resp = client->resp; pers = client->pers; +#ifdef AQTION_EXTENSION + memcpy(cl_cvar, client->cl_cvar, sizeof(client->cl_cvar)); +#endif memset(client, 0, sizeof(*client)); +#ifdef AQTION_EXTENSION + memcpy(client->cl_cvar, cl_cvar, sizeof(client->cl_cvar)); +#endif client->pers = pers; client->resp = resp; + client->clientNum = index; //zucc give some ammo @@ -2439,6 +2592,7 @@ void PutClientInServer(edict_t * ent) client->grenade_max = 2; client->desired_fov = 90; + // clear entity values ent->groundentity = NULL; ent->client = &game.clients[index]; @@ -2473,6 +2627,18 @@ void PutClientInServer(edict_t * ent) ent->s.skinnum = ent - g_edicts - 1; ent->s.modelindex = 255; // will use the skin specified model +#ifdef AQTION_EXTENSION + // teammate indicator arrows + if (use_indicators->value && teamplay->value && !client->arrow && client->resp.team) + { + client->arrow = G_Spawn(); + client->arrow->solid = SOLID_NOT; + client->arrow->movetype = MOVETYPE_NOCLIP; + client->arrow->classname = "ind_arrow"; + client->arrow->owner = ent; + } +#endif + // zucc vwep //ent->s.modelindex2 = 255; // custom gun model ShowGun(ent); @@ -2503,10 +2669,11 @@ void PutClientInServer(edict_t * ent) ent->is_triggering = false; ent->grenadewait = 0; ent->react = 0.f; + if( ent->is_bot ) { ent->classname = "bot"; - + ent->enemy = NULL; ent->movetarget = NULL; if( ! teamplay->value ) @@ -2629,6 +2796,11 @@ void PutClientInServer(edict_t * ent) ent->solid = SOLID_NOT; ent->svflags |= SVF_NOCLIENT; ent->client->ps.gunindex = 0; + +#ifdef AQTION_EXTENSION + if (!ent->client->resp.team) + HUD_SetType(ent, 1); +#endif gi.linkentity(ent); return; } @@ -2637,6 +2809,10 @@ void PutClientInServer(edict_t * ent) } // end if( respawn ) #endif +#ifdef AQTION_EXTENSION + HUD_SetType(ent, -1); +#endif + if (!teamplay->value) { // this handles telefrags... KillBox(ent); } else { @@ -2675,6 +2851,45 @@ void PutClientInServer(edict_t * ent) if (allweapon->value) AllWeapons(ent); + /* + Team leaders in Espionage can receive different loadouts + 0 - No different loadout + 1 - Leaders get AllItems (default) + 2 - Leaders get AllWeapons + 3 - Leaders get AllItems and AllWeapons + + With leaderenhance enabled, the leaders also get a medkit + */ + if (esp->value) { + if (esp_leaderequip->value){ + for (i = TEAM1; i <= teamCount; i++){ + if (ent == teams[i].leader) { + if (esp_leaderequip->value == 1) { + AllItems(ent); + } else if (esp_leaderequip->value == 2) { + AllWeapons(ent); + } else if (esp_leaderequip->value == 3) { + AllItems(ent); + AllWeapons(ent); + } + } + } + } + if (esp_leaderenhance->value){ + for (i = TEAM1; i <= teamCount; i++){ + if (ent == teams[i].leader) { + if (esp_leaderequip->value == 1) { + ent->client->medkit = 1; + } + } + } + } + // For respawn-on-leader, if leader is ducking, client should also be ducking + if (team_round_going && HAVE_LEADER(ent->client->resp.team)){ + ent->client->ps.pmove.pm_flags |= (teams[ent->client->resp.team].leader->client->ps.pmove.pm_flags & PMF_DUCKED); + } + } + // force the current weapon up client->newweapon = client->weapon; ChangeWeapon(ent); @@ -2698,6 +2913,17 @@ void ClientBeginDeathmatch(edict_t * ent) ent->client->resp.enterframe = level.framenum; ent->client->resp.gldynamic = 1; + +#ifdef AQTION_EXTENSION + if (teamplay->value) + { + HUD_SetType(ent, 1); + } + else + { + HUD_SetType(ent, -1); + } +#endif if (!ent->client->pers.connected) { ent->client->pers.connected = true; @@ -2739,7 +2965,14 @@ void ClientBeginDeathmatch(edict_t * ent) #ifndef NO_BOTS ACEIT_RebuildPlayerList(); +#ifdef USE_AQTION StatBotCheck(); + #ifdef USE_AQTION + if(am->value){ + attract_mode_bot_check(); + } + #endif +#endif #endif // locate ent at a spawn point @@ -2761,8 +2994,10 @@ void ClientBeginDeathmatch(edict_t * ent) IRC_printf(IRC_T_SERVER, "%n entered the game", ent->client->pers.netname); // TNG:Freud Automaticly join saved teams. - if (saved_team && auto_join->value && teamplay->value) + if (saved_team && auto_join->value == 1 && teamplay->value) JoinTeam(ent, saved_team, 1); + else if (auto_join->value == 2 && teamplay->value) + JoinTeamAutobalance(ent); if (!level.intermission_framenum) { @@ -2773,6 +3008,12 @@ void ClientBeginDeathmatch(edict_t * ent) PrintMOTD(ent); } + if(am->value && game.bot_count > 0){ + char msg[128]; + Q_snprintf(msg, sizeof(msg), "** This server contains BOTS for you to play with until real players join up! Enjoy! **"); + gi.centerprintf(ent, msg); + } + ent->client->resp.motd_refreshes = 1; //AQ2:TNG - Slicer: Set time to check clients @@ -2920,29 +3161,42 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) } #endif - // Reki - spectator options, force team overlay/send easily parsable kill feed prints - s = Info_ValueForKey(userinfo, "cl_spectatorhud"); - if (atoi(s)) - client->pers.spec_flags |= SPECFL_SPECHUD; - else - client->pers.spec_flags &= SPECFL_SPECHUD; - s = Info_ValueForKey(userinfo, "cl_spectatorkillfeed"); - if (atoi(s)) - client->pers.spec_flags |= SPECFL_KILLFEED; - else - client->pers.spec_flags &= SPECFL_KILLFEED; - - // Reki - disable antilag for *my own shooting*, not others shooting at me - s = Info_ValueForKey(userinfo, "cl_antilag"); - int antilag_value = client->pers.antilag_optout; - if (s[0] == 0 || atoi(s) > 0) - client->pers.antilag_optout = qfalse; - else if (atoi(s) <= 0) - client->pers.antilag_optout = qtrue; - - if (sv_antilag->value && antilag_value != client->pers.antilag_optout) - gi.cprintf(ent, PRINT_MEDIUM, "YOUR CL_ANTILAG IS NOW SET TO %i\n", !client->pers.antilag_optout); +#ifdef AQTION_EXTENSION + if (!HAS_CVARSYNC(ent)) // only do these cl cvars if cvarsync isn't a thing, since it's much better than userinfo + { +#endif + // Reki - spectator options, force team overlay/send easily parsable kill feed prints + s = Info_ValueForKey(userinfo, "cl_spectatorhud"); + if (atoi(s)) + client->pers.spec_flags |= SPECFL_SPECHUD | SPECFL_SPECHUD_NEW; + else + client->pers.spec_flags &= ~(SPECFL_SPECHUD | SPECFL_SPECHUD_NEW); + + #ifdef AQTION_EXTENSION + if (Client_GetProtocol(ent) == 38) // Reki: new clients get new spec hud + client->pers.spec_flags &= ~SPECFL_SPECHUD; + #endif + + s = Info_ValueForKey(userinfo, "cl_spectatorkillfeed"); + if (atoi(s)) + client->pers.spec_flags |= SPECFL_KILLFEED; + else + client->pers.spec_flags &= ~SPECFL_KILLFEED; + + // Reki - disable antilag for *my own shooting*, not others shooting at me + s = Info_ValueForKey(userinfo, "cl_antilag"); + int antilag_value = client->pers.antilag_optout; + if (s[0] == 0 || atoi(s) > 0) + client->pers.antilag_optout = qfalse; + else if (atoi(s) <= 0) + client->pers.antilag_optout = qtrue; + + if (sv_antilag->value && antilag_value != client->pers.antilag_optout) + gi.cprintf(ent, PRINT_MEDIUM, "YOUR CL_ANTILAG IS NOW SET TO %i\n", !client->pers.antilag_optout); +#ifdef AQTION_EXTENSION + } +#endif } /* @@ -2998,6 +3252,15 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) Q_strncpyz(ent->client->pers.ip, ipaddr_buf, sizeof(ent->client->pers.ip)); Q_strncpyz(ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo)); + #if USE_AQTION + value = Info_ValueForKey(userinfo, "steamid"); + if (*value) + Q_strncpyz(ent->client->pers.steamid, value, sizeof(ent->client->pers.steamid)); + value = Info_ValueForKey(userinfo, "discordid"); + if (*value) + Q_strncpyz(ent->client->pers.discordid, value, sizeof(ent->client->pers.discordid)); + #endif + if (game.serverfeatures & GMF_MVDSPEC) { value = Info_ValueForKey(userinfo, "mvdspec"); if (*value) { @@ -3044,7 +3307,18 @@ void ClientDisconnect(edict_t * ent) if (!ent->client) return; + if (esp->value && matchmode->value) { + char tempmsg[128]; + // We have to kill him first before he is removed as captain/leader + killPlayer(ent, false); + sprintf(tempmsg, "The captain of %s (%s) disconnected, the other team wins by default!", teams[ent->client->resp.team].name, ent->client->pers.netname); + CenterPrintAll(tempmsg); + } + MM_LeftTeam( ent ); + if (esp->value) + EspLeaderLeftTeam(ent); + ent->client->resp.team = 0; // drop items if they are alive/not observer @@ -3110,7 +3384,15 @@ void ClientDisconnect(edict_t * ent) ent->is_bot = false; ent->think = NULL; ACEIT_RebuildPlayerList(); +#ifdef USE_AQTION StatBotCheck(); + + #ifdef USE_AQTION + if(am->value){ + attract_mode_bot_check(); + } + #endif +#endif #endif } @@ -3140,6 +3422,10 @@ void CreateGhost(edict_t * ent) strcpy(ghost->ip, ent->client->pers.ip); strcpy(ghost->netname, ent->client->pers.netname); + #if USE_AQTION + strcpy(ghost->steamid, ent->client->pers.steamid); + strcpy(ghost->discordid, ent->client->pers.discordid); + #endif ghost->enterframe = ent->client->resp.enterframe; ghost->disconnect_frame = level.framenum; @@ -3150,6 +3436,10 @@ void CreateGhost(edict_t * ent) ghost->kills = ent->client->resp.kills; ghost->deaths = ent->client->resp.deaths; ghost->ctf_caps = ent->client->resp.ctf_caps; + ghost->ctf_capstreak = ent->client->resp.ctf_capstreak; + ghost->team_kills = ent->client->resp.team_kills; + ghost->streakKillsHighest = ent->client->resp.streakKillsHighest; + ghost->streakHSHighest = ent->client->resp.streakHSHighest; // Teamplay variables if (teamplay->value) { @@ -3171,7 +3461,7 @@ void CreateGhost(edict_t * ent) edict_t *pm_passent; // pmove doesn't need to know about passent and contentmask -trace_t q_gameabi PM_trace(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end) +trace_t q_gameabi PM_trace(vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end) { if (pm_passent && pm_passent->health > 0) return gi.trace(start, mins, maxs, end, pm_passent, MASK_PLAYERSOLID); @@ -3309,8 +3599,8 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) client->cmd_last = *ucmd; // Stumbling movement with leg damage. - // darksaint ETE edit: if e_enhancedSlippers are enabled/equipped, negate all stumbling - qboolean has_enhanced_slippers = e_enhancedSlippers->value && INV_AMMO(ent, SLIP_NUM); + // darksaint ETE edit: if esp_enhancedslippers are enabled/equipped, negate all stumbling + qboolean has_enhanced_slippers = esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM); if( client->leg_damage && ent->groundentity && ! has_enhanced_slippers ) { #ifdef AQTION_EXTENSION @@ -3489,16 +3779,31 @@ void ClientBeginServerFrame(edict_t * ent) if (sv_antilag->value) // if sv_antilag is enabled, we want to track our player position for later reference antilag_update(ent); + //PaTMaN's jmod + if(jump->value) { + if ((client->resp.toggle_lca) && (client->pers.spectator)) + client->resp.toggle_lca = 0; + else if (client->resp.toggle_lca) + Cmd_PMLCA_f(ent); + } + #ifdef AQTION_EXTENSION // resync pm_timestamp so all limps are roughly synchronous, to try to maintain original behavior unsigned short world_timestamp = (int)(level.time * 1000) % 60000; client->ps.pmove.pm_timestamp = world_timestamp; - // network any pending ghud updates - Ghud_SendUpdates(ent); - // update dimension mask for team-only entities client->dimension_observe = 1 | (1 << client->resp.team); + + if (client->resp.hud_type == 1) + { + client->dimension_observe |= 0xE; // true spectators can see all teams + HUD_SpectatorUpdate(ent); + } + else + { + HUD_ClientUpdate(ent); + } #endif if (client->resp.penalty > 0 && level.realFramenum % HZ == 0) @@ -3517,6 +3822,16 @@ void ClientBeginServerFrame(edict_t * ent) client->resp.motd_refreshes++; PrintMOTD( ent ); } + // This next message appears perpetually until the conditions to make the message go away are met + } else if (ent->client->layout != LAYOUT_MENU) { + if (printrules->value) { // Do not print rules unless printrules is 1 + if (PrintGameMessage(ent)) { + if (client->resp.last_gamemsg_refresh + 2 * HZ < level.realFramenum) { + client->resp.last_gamemsg_refresh = level.realFramenum; + client->resp.gamemsg_refreshes++; + } + } + } } // show team or weapon menu immediately when connected @@ -3566,7 +3881,7 @@ void ClientBeginServerFrame(edict_t * ent) // wait for any button just going down if (level.framenum > client->respawn_framenum) { - + // Special consideration here for Espionage, as we DO want to respawn in a GS_ROUNDBASED game if (teamplay->value) { going_observer = ((gameSettings & GS_ROUNDBASED) || !client->resp.team || client->resp.subteam); } @@ -3601,6 +3916,12 @@ void ClientBeginServerFrame(edict_t * ent) ent->client->chase_mode = 0; NextChaseMode( ent ); } + + if (esp->value) { + // LCA countdown occurs below in EspRespawnLCA() + // then Action! + EspRespawnPlayer(ent); + } } else { @@ -3611,6 +3932,9 @@ void ClientBeginServerFrame(edict_t * ent) client->latched_buttons = 0; } } + } else { // !(level.framenum > client->respawn_framenum) + if (esp->value) + EspRespawnLCA(ent); } return; } @@ -3624,8 +3948,9 @@ void ClientBeginServerFrame(edict_t * ent) client->punch_desired = false; if( (ppl_idletime->value > 0) && idleframes && (idleframes % (int)(ppl_idletime->value * HZ) == 0) ) - //plays a random sound/insane sound, insane1-9.wav - gi.sound( ent, CHAN_VOICE, gi.soundindex(va( "insane/insane%i.wav", rand() % 9 + 1 )), 1, ATTN_NORM, 0 ); + //plays a random sound/insane sound, insane1-11.wav + if (!jump->value) // Don't play insane sounds in jmod + gi.sound( ent, CHAN_VOICE, gi.soundindex(va( "insane/insane%i.wav", rand() % 11 + 1 )), 1, ATTN_NORM, 0 ); if( (sv_idleremove->value > 0) && (idleframes > (sv_idleremove->value * HZ)) && client->resp.team ) { @@ -3663,8 +3988,7 @@ void ClientBeginServerFrame(edict_t * ent) (client->resp.team == ctfgame.offence ? "ATTACKING" : "DEFENDING"), CTFOtherTeamName(ctfgame.offence)); - } - else { + } else { gi.centerprintf(ent, "ACTION!"); } } diff --git a/src/action/p_hud.c b/src/action/p_hud.c index f64a455b6..1a635bf43 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -51,16 +51,6 @@ #include "g_local.h" -#ifdef AQTION_EXTENSION -int ghud_team1_icon; -int ghud_team1_num; -int ghud_team2_icon; -int ghud_team2_num; -int ghud_team3_icon; -int ghud_team3_num; -#endif - - /* ====================================================================== @@ -129,6 +119,9 @@ void BeginIntermission(edict_t *targ) int i; edict_t *ent; + // Clear all timed messages + free(timedMessages); + if (level.intermission_framenum) return; // already activated @@ -139,7 +132,7 @@ void BeginIntermission(edict_t *targ) } else if (teamplay->value) { TallyEndOfLevelTeamScores(); } - #if USE_AQTION + #ifdef USE_AQTION // Generates stats for non-CTF, Teamplay or Matchmode else if (stat_logs->value && !matchmode->value) { LogMatch(); @@ -193,6 +186,96 @@ void BeginIntermission(edict_t *targ) } InitTransparentList(); + #ifndef NO_BOTS + // Clear LTK bot names + LTKClearBotNames(); + #endif +} + +/* + ====================================================================== + Point of Interest + + This isn't perfect, but it gets you in the 'area' of a point of interest + Could use some refactoring, but it works for now. + Works much like intermission, but it more or less 'teleports' spectators + and those who are not 'spawned in' to the point of interest. + + Points of interest are generally on the ground, so this adds 25 units + to each axis to move the camera in the area without being directly + on top of the point of interest. + ====================================================================== +*/ +void MoveClientToPOI(edict_t *ent, edict_t *poi) +{ + PMenu_Close(ent); + + VectorCopy(level.poi_origin, ent->s.origin); + ent->s.origin[0] -= 25; + ent->s.origin[1] -= 25; + ent->s.origin[2] += 25; + + ent->client->ps.pmove.origin[0] = (level.poi_origin[0] - 25) * 8; + ent->client->ps.pmove.origin[1] = (level.poi_origin[1] - 25) * 8; + ent->client->ps.pmove.origin[2] = (level.poi_origin[2] + 25) * 8; + + vec3_t ownerv, o, ownerv_forward, ownerv_right; + vec3_t angles; + + VectorCopy(level.poi_origin, ownerv); + ownerv[2] += poi->viewheight; + + VectorCopy(ent->client->ps.viewangles, angles); + AngleVectors(angles, ownerv_forward, ownerv_right, NULL); + + VectorNormalize(ownerv_forward); + VectorMA(ownerv, -75, ownerv_forward, o); + + VectorCopy(o, ent->s.origin); + VectorCopy(o, ent->client->ps.pmove.origin); + + VectorSubtract(level.poi_origin, ent->s.origin, ent->client->ps.viewangles); + vectoangles(ent->client->ps.viewangles, ent->client->ps.viewangles); + + VectorClear(ent->client->ps.kick_angles); + ent->client->ps.gunindex = 0; + ent->client->ps.blend[3] = 0; + ent->client->ps.rdflags &= ~RDF_UNDERWATER; + ent->client->ps.stats[STAT_FLASHES] = 0; + + // clean up powerup info + ent->client->quad_framenum = 0; + ent->client->invincible_framenum = 0; + ent->client->breather_framenum = 0; + ent->client->enviro_framenum = 0; + ent->client->grenade_blew_up = false; + ent->client->grenade_framenum = 0; + + ent->watertype = 0; + ent->waterlevel = 0; + ent->viewheight = 0; + ent->s.modelindex = 0; + ent->s.modelindex2 = 0; + ent->s.modelindex3 = 0; + ent->s.modelindex4 = 0; + ent->s.effects = 0; + ent->s.renderfx = 0; + ent->s.sound = 0; + ent->s.event = 0; + ent->s.solid = 0; + ent->solid = SOLID_NOT; + ent->svflags = SVF_NOCLIENT; + + ent->client->resp.sniper_mode = SNIPER_1X; + ent->client->desired_fov = 90; + ent->client->ps.fov = 90; + ent->client->ps.stats[STAT_SNIPER_ICON] = 0; + ent->client->pickup_msg_framenum = 0; + +#ifndef NO_BOTS + if( ent->is_bot ) + return; +#endif } /* @@ -540,31 +623,41 @@ void G_SetStats (edict_t * ent) // // timers // - if (ent->client->quad_framenum > level.framenum) - { - ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_quad"); - ent->client->ps.stats[STAT_TIMER] = (ent->client->quad_framenum - level.framenum) / HZ; - } - else if (ent->client->invincible_framenum > level.framenum) - { - ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_invulnerability"); - ent->client->ps.stats[STAT_TIMER] = (ent->client->invincible_framenum - level.framenum) / HZ; - } - else if (ent->client->enviro_framenum > level.framenum) - { - ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_envirosuit"); - ent->client->ps.stats[STAT_TIMER] = (ent->client->enviro_framenum - level.framenum) / HZ; - } - else if (ent->client->breather_framenum > level.framenum) - { - ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_rebreather"); - ent->client->ps.stats[STAT_TIMER] = (ent->client->breather_framenum - level.framenum) / HZ; - } - else - { - ent->client->ps.stats[STAT_TIMER_ICON] = 0; - ent->client->ps.stats[STAT_TIMER] = 0; - } + // if (ent->client->quad_framenum > level.framenum) + // { + // ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_quad"); + // ent->client->ps.stats[STAT_TIMER] = (ent->client->quad_framenum - level.framenum) / HZ; + // } + // else if (ent->client->invincible_framenum > level.framenum) + // { + // ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_invulnerability"); + // ent->client->ps.stats[STAT_TIMER] = (ent->client->invincible_framenum - level.framenum) / HZ; + // } + // else if (ent->client->enviro_framenum > level.framenum) + // { + // ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_envirosuit"); + // ent->client->ps.stats[STAT_TIMER] = (ent->client->enviro_framenum - level.framenum) / HZ; + // } + // else if (ent->client->breather_framenum > level.framenum) + // { + // ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex ("p_rebreather"); + // ent->client->ps.stats[STAT_TIMER] = (ent->client->breather_framenum - level.framenum) / HZ; + // } + // else + // if (esp->value) { + // if (!IS_LEADER(ent) && ent->client->respawn_framenum > 0 && ent->client->respawn_framenum > level.framenum > 0){ + // ent->client->ps.stats[STAT_TIMER_ICON] = level.pic_esp_respawn_icon; + // ent->client->ps.stats[STAT_TIMER] = (ent->client->respawn_framenum - level.framenum) / HZ; + // } else { + // ent->client->ps.stats[STAT_TIMER_ICON] = 0; + // ent->client->ps.stats[STAT_TIMER] = 0; + // } + // } + // else + // { + ent->client->ps.stats[STAT_TIMER_ICON] = 0; + ent->client->ps.stats[STAT_TIMER] = 0; + //} // // selected item @@ -645,7 +738,359 @@ void G_SetStats (edict_t * ent) SetCTFStats (ent); else if (dom->value) SetDomStats (ent); + else if (esp->value) + SetEspStats (ent); else if (teamplay->value) A_Scoreboard (ent); //FIREBLADE } + + +#ifdef AQTION_EXTENSION + +void HUD_SetType(edict_t *clent, int type) +{ + if (clent->client->resp.hud_type == type) + return; + + if (type == -1) + HUD_ClientSetup(clent); + else if (type == 1) + HUD_SpectatorSetup(clent); + else + { + Ghud_ClearForClient(clent); + clent->client->resp.hud_type = 0; + } +} + +void HUD_ClientSetup(edict_t *clent) +{ + Ghud_ClearForClient(clent); + clent->client->resp.hud_type = -1; + +} + +void HUD_ClientUpdate(edict_t *clent) +{ + +} + + +void HUD_SpectatorSetup(edict_t *clent) +{ + Ghud_ClearForClient(clent); + clent->client->resp.hud_type = 1; + + int *hud = clent->client->resp.hud_items; + int i; + + if (teamplay->value && spectator_hud->value) + { + for (i = 0; i < 6; i++) + { + int x, y; + int h_base = h_nameplate_l + (i * 5); + int h; + + x = 0; + y = 28 + (28 * i); + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetSize(clent, hud[h], 144, 24); + Ghud_SetColor(clent, hud[h], 110, 45, 45, 230); + + h = h_base + 1; // health bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetSize(clent, hud[h], 0, 24); + Ghud_SetColor(clent, hud[h], 220, 60, 60, 230); + + h = h_base + 2; // name + hud[h] = Ghud_AddText(clent, x + 142, y + 3, ""); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); + + h = h_base + 3; // k/d + hud[h] = Ghud_AddText(clent, x + 142, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); + + h = h_base + 4; // weapon select + hud[h] = Ghud_AddIcon(clent, x + 2, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 0, 0); + } + + for (i = 0; i < 6; i++) + { + int x, y; + int h_base = h_nameplate_r + (i * 5); + int h; + + x = -144; + y = 28 + (28 * i); + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 144, 24); + Ghud_SetColor(clent, hud[h], 30, 60, 110, 230); + + h = h_base + 1; // health bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 0, 24); + Ghud_SetColor(clent, hud[h], 40, 80, 220, 230); + + h = h_base + 2; // name + hud[h] = Ghud_AddText(clent, x + 2, y + 3, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 3; // k/d + hud[h] = Ghud_AddText(clent, x + 2, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 4; // weapon select + hud[h] = Ghud_AddIcon(clent, x + 122, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 1, 0); + } + + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); + hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); + Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); + + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); + hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); + Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); + } +} + +void HUD_SpectatorUpdate(edict_t *clent) +{ + int i; + + if (teamplay->value && spectator_hud->value) + { + int *hud = clent->client->resp.hud_items; + + if (!(clent->client->pers.spec_flags & SPECFL_SPECHUD_NEW)) // hide all elements since client doesn't want them + { + for (i = 0; i <= h_team_r_num; i++) + { + Ghud_SetFlags(clent, hud[i], GHF_HIDE); + } + return; + } + + gclient_t *team1_players[6]; + gclient_t *team2_players[6]; + gclient_t *sortedClients[MAX_CLIENTS]; + int totalClients, team1Clients, team2Clients; + + memset(team1_players, 0, sizeof(team1_players)); + memset(team2_players, 0, sizeof(team2_players)); + + team1Clients = 0; + team2Clients = 0; + totalClients = G_SortedClients(sortedClients); + + for (i = 0; i < totalClients; i++) + { + gclient_t *cl = sortedClients[i]; + + if (!cl->resp.team) + continue; + + if (cl->resp.subteam) + continue; + + if (cl->resp.team == TEAM1) + { + if (team1Clients >= 6) + continue; + + team1_players[team1Clients] = cl; + team1Clients++; + continue; + } + if (cl->resp.team == TEAM2) + { + if (team2Clients >= 6) + continue; + + team2_players[team2Clients] = cl; + team2Clients++; + continue; + } + } + + + // team 1 (red team) + Ghud_SetFlags(clent, hud[h_team_l], 0); + Ghud_SetFlags(clent, hud[h_team_l_num], 0); + Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); + + for (i = 0; i < 6; i++) + { + int x, y; + int h = h_nameplate_l + (i * 5); + gclient_t *cl = team1_players[i]; + + if (!cl) // if there is no player, hide all the elements for their plate + { + Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); + continue; + } + + x = 0; + y = 28 + (28 * i); + + // unhide our elements + Ghud_SetFlags(clent, hud[h + 0], 0); + Ghud_SetFlags(clent, hud[h + 1], 0); + Ghud_SetFlags(clent, hud[h + 2], 0); + Ghud_SetFlags(clent, hud[h + 3], 0); + Ghud_SetFlags(clent, hud[h + 4], 0); + + // tint for deadness + edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); + if (!IS_ALIVE(cl_ent)) + { + Ghud_SetSize(clent, hud[h + 1], 0, 24); + + Ghud_SetPosition(clent, hud[h + 0], x, y); + Ghud_SetSize(clent, hud[h + 0], 144, 24); + } + else + { + float hp_invfrac; + float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); + hp_invfrac = floorf((1 - hp_frac) * 144) / 144; + hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps + + Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); + Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); + Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); + } + + // generate strings + char nm_s[17]; + char kdr_s[24]; + memcpy(nm_s, cl->pers.netname, 16); + if (IS_ALIVE(cl_ent)) + snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); + else + snprintf(kdr_s, sizeof(kdr_s), "(%i)%c %5i", cl->resp.deaths, 06, cl->resp.kills); + + nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated + kdr_s[23] = 0; + + // update fields + Ghud_SetText(clent, hud[h + 2], nm_s); + Ghud_SetText(clent, hud[h + 3], kdr_s); + + if (cl->curr_weap) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); + else + Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); + } + + + // team 2 (blue team) + Ghud_SetFlags(clent, hud[h_team_r], 0); + Ghud_SetFlags(clent, hud[h_team_r_num], 0); + Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); + if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes + Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); + else + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); + + for (i = 0; i < 6; i++) + { + int x, y; + int h = h_nameplate_r + (i * 5); + gclient_t *cl = team2_players[i]; + + if (!cl) // if there is no player, hide all the elements for their plate + { + Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); + continue; + } + + x = -144; + y = 28 + (28 * i); + + // unhide our elements + Ghud_SetFlags(clent, hud[h + 0], 0); + Ghud_SetFlags(clent, hud[h + 1], 0); + Ghud_SetFlags(clent, hud[h + 2], 0); + Ghud_SetFlags(clent, hud[h + 3], 0); + Ghud_SetFlags(clent, hud[h + 4], 0); + + // tint for deadness + edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); + if (!IS_ALIVE(cl_ent)) + { + Ghud_SetSize(clent, hud[h + 1], 0, 24); + Ghud_SetSize(clent, hud[h + 0], 144, 24); + //Ghud_SetPosition(clent, hud[h + 0], x, y); + } + else + { + float hp_invfrac; + float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); + hp_invfrac = floorf((1 - hp_frac) * 144) / 144; + hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps + + Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); + Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); + Ghud_SetPosition(clent, hud[h + 1], x + (144 * hp_invfrac), y); + //Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); + } + + // generate strings + char nm_s[17]; + char kdr_s[24]; + memcpy(nm_s, cl->pers.netname, 16); + if (IS_ALIVE(cl_ent)) + snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); + else + snprintf(kdr_s, sizeof(kdr_s), "%-5i %c(%i)", cl->resp.kills, 06, cl->resp.deaths); + + nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated + kdr_s[23] = 0; + + // update fields + Ghud_SetText(clent, hud[h + 2], nm_s); + Ghud_SetText(clent, hud[h + 3], kdr_s); + + if (cl->curr_weap) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); + else + Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); + } + } +} + +#endif + diff --git a/src/action/p_view.c b/src/action/p_view.c index 4814b9b51..985c774f2 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -615,6 +615,10 @@ void P_FallingDamage (edict_t * ent) if (lights_camera_action || ent->client->uvTime > 0) return; + // PaTMaN's jmod/jump LCA invulnerable + if (jump->value && ent->client->resp.toggle_lca) + return; + if (ent->s.modelindex != 255) return; // not in the player model @@ -634,6 +638,7 @@ void P_FallingDamage (edict_t * ent) delta = ent->velocity[2] - oldvelocity[2]; ent->client->jumping = 0; } + delta = delta * delta * 0.0001; // never take damage if just release grapple or on grapple @@ -694,6 +699,8 @@ void P_FallingDamage (edict_t * ent) ent->s.event = EV_FALLFAR; else // all falls are far ent->s.event = EV_FALLFAR; + if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) + ent->s.event = EV_FALL; } ent->pain_debounce_framenum = KEYFRAME(FRAMEDIV); // no normal pain sound @@ -706,14 +713,13 @@ void P_FallingDamage (edict_t * ent) // zucc scale this up damage *= 10; - // darksaint - reduce damage if e_enhancedSlippers are on and equipped - if (e_enhancedSlippers->value && INV_AMMO(ent, SLIP_NUM)) + // darksaint - reduce damage if esp_enhancedslippers are on and equipped + if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) damage /= 2; VectorSet (dir, 0, 0, 1); - if (jump->value) - { + if (jump->value && (!ent->client->resp.toggle_lca || lights_camera_action)) { gi.cprintf(ent, PRINT_HIGH, "Fall Damage: %d\n", damage); ent->client->resp.jmp_falldmglast = damage; } else { @@ -815,24 +821,25 @@ void P_WorldEffects (void) if (waterlevel == 3) { // breather or envirosuit give air - if (breather || envirosuit) - { - current_player->air_finished_framenum = level.framenum + 10 * HZ; - - if (((current_client->breather_framenum - level.framenum) % (25 * FRAMEDIV)) == 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? - } - } + // AQ2 doesn't use the rebreather + // if (breather || envirosuit) + // { + // current_player->air_finished_framenum = level.framenum + 10 * HZ; + + // if (((current_client->breather_framenum - level.framenum) % (25 * FRAMEDIV)) == 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 out of air, start drowning if (current_player->air_finished_framenum < level.framenum) @@ -1225,17 +1232,28 @@ void Do_MedKit( edict_t *ent ) if( ent->client->bandaging && (ent->client->bleeding || ent->client->leg_damage) ) return; - for( i = 0; i < 2; i ++ ) - { - // Make sure we have any medkit and need to use it. - if( ent->client->medkit <= 0 ) - return; - if( ent->health >= ent->max_health ) + // Don't use a medkit if ent doesn't have one, and don't use one if ent is at max_health + if( ent->client->medkit <= 0 ) return; + if( ent->health >= ent->max_health ) + return; - ent->health ++; - ent->client->medkit --; + // Espionage handles medkits differently, it uses medkits like healthpacks + if (!esp->value) { + for( i = 0; i < 2; i ++ ){ + // One medkit == One health point, use all medkits in one bandage attempt + ent->health++; + ent->client->medkit--; + } + } else { + // Subtract one medkit, gain health per medkit_value + ent->health = ent->health + (int)medkit_value->value; + ent->client->medkit--; } + + // Handle overheals + if (ent->health > 100) + ent->health = 100; } @@ -1351,6 +1369,27 @@ void ClientEndServerFrame (edict_t * ent) current_player = ent; current_client = ent->client; +#ifdef AQTION_EXTENSION + if (current_client->arrow) + { + // set new origin + VectorCopy(ent->s.origin, current_client->arrow->s.origin); + current_client->arrow->s.origin[2] += ent->maxs[2]; + current_client->arrow->s.origin[2] += 8 + sin(level.time * 2); + // + + // fix oldorigin + VectorCopy(ent->s.old_origin, current_client->arrow->s.old_origin); + current_client->arrow->s.old_origin[2] += ent->maxs[2]; + current_client->arrow->s.old_origin[2] += 8 + sin((level.time - FRAMETIME) * 2); + // + + current_client->arrow->s.modelindex = level.model_arrow + (current_client->resp.team - 1); + current_client->arrow->s.renderfx = RF_INDICATOR; + current_client->arrow->dimension_visible = (1 << current_client->resp.team); + } +#endif + //AQ2:TNG - Slicer : Stuffs the client x seconds after he enters the server, needed for Video check if (ent->client->resp.checkframe[0] <= level.framenum) { @@ -1373,7 +1412,7 @@ void ClientEndServerFrame (edict_t * ent) || video_check_glclear->value || darkmatch->value) { if (ent->client->resp.vidref && Q_stricmp(ent->client->resp.vidref, "soft")) - stuffcmd (ent, "%cpsi $gl_modulate $gl_lockpvs $gl_clear $gl_dynamic $gl_driver\n"); + stuffcmd (ent, "%cpsi $gl_modulate $gl_lockpvs $gl_clear $gl_dynamic $gl_brightness $gl_driver\n"); } } diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 91085a4a2..ed53484ed 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -101,8 +101,8 @@ static qboolean is_quad; -void P_ProjectSource(gclient_t* client, const vec3_t point, const vec3_t distance, - const vec3_t forward, const vec3_t right, vec3_t result) +void P_ProjectSource(gclient_t* client, vec3_t point, vec3_t distance, + vec3_t forward, vec3_t right, vec3_t result) { vec3_t _distance; @@ -126,8 +126,8 @@ void P_ProjectSource(gclient_t* client, const vec3_t point, const vec3_t distanc // used for setting up the positions of the guns in shell ejection void -Old_ProjectSource(gclient_t* client, const vec3_t point, const vec3_t distance, - const vec3_t forward, const vec3_t right, vec3_t result) +Old_ProjectSource(gclient_t* client, vec3_t point, vec3_t distance, + vec3_t forward, vec3_t right, vec3_t result) { vec3_t _distance; @@ -144,8 +144,8 @@ Old_ProjectSource(gclient_t* client, const vec3_t point, const vec3_t distance, // this one is the real old project source void -Knife_ProjectSource(gclient_t* client, const vec3_t point, const vec3_t distance, - const vec3_t forward, const vec3_t right, vec3_t result) +Knife_ProjectSource(gclient_t* client, vec3_t point, vec3_t distance, + vec3_t forward, vec3_t right, vec3_t result) { vec3_t _distance; @@ -171,7 +171,7 @@ PlayerNoise to a noise in hopes of seeing the player from there. =============== */ -void PlayerNoise(edict_t* who, const vec3_t where, int type) +void PlayerNoise(edict_t* who, vec3_t where, int type) { /* if (type == PNOISE_WEAPON) @@ -620,8 +620,7 @@ Use_Weapon(edict_t* ent, gitem_t* item) -edict_t* -FindSpecWeapSpawn(edict_t* ent) +edict_t *FindSpecWeapSpawn(edict_t* ent) { edict_t* spot = NULL; @@ -643,8 +642,7 @@ FindSpecWeapSpawn(edict_t* ent) return spot; } -static void -SpawnSpecWeap(gitem_t* item, edict_t* spot) +static void SpawnSpecWeap(gitem_t* item, edict_t* spot) { /* edict_t *ent; vec3_t forward, right; @@ -683,41 +681,61 @@ SpawnSpecWeap(gitem_t* item, edict_t* spot) gi.linkentity(spot); } -void temp_think_specweap(edict_t* ent) +void SpecialWeaponRespawnTimer(edict_t* ent) { ent->touch = Touch_Item; + /* + G_FreeEdict frees the weapon after the nextthink timer expires + Placeholder is used to keep the weapon around for a very long time + ThinkSpecWeap is used to respawn the weapon at a different spawn point + */ + + // Allweapon setting makes dropped weapons disappear in 1s if (allweapon->value) { // allweapon set - ent->nextthink = level.framenum + 1 * HZ; + ent->nextthink = eztimer(1); ent->think = G_FreeEdict; return; } - + // Removes weapons more often during warmup (10s) + if (in_warmup) { + ent->nextthink = eztimer(10); + ent->think = G_FreeEdict; + return; + } + // Espionage, CTF and Domination weapons disappear after 30s + if (esp->value || dom->value || ctf->value) { + ent->nextthink = eztimer(30); + ent->think = G_FreeEdict; + return; + } + // Normal teamplay, weapons basically never disappear if (gameSettings & GS_ROUNDBASED) { - ent->nextthink = level.framenum + 1000 * HZ; + ent->nextthink = eztimer(1000); ent->think = PlaceHolder; return; } - + // Deathmatch with weapon choose, weapons disappear in 6s if (gameSettings & GS_WEAPONCHOOSE) { - ent->nextthink = level.framenum + 6 * HZ; + ent->nextthink = eztimer(6); ent->think = ThinkSpecWeap; + return; } + // unless weapon respawn dmflag is set, then use the weapon_respawn cvar else if (DMFLAGS(DF_WEAPON_RESPAWN)) { ent->nextthink = level.framenum + (weapon_respawn->value * 0.6f) * HZ; ent->think = G_FreeEdict; + return; } - else { - ent->nextthink = level.framenum + weapon_respawn->value * HZ; - ent->think = ThinkSpecWeap; - } + // Catch-all, should just remove weapons after weapon_respawn setting + // and then indicate that the weapon should respawn at its set spawn point + ent->nextthink = level.framenum + eztimer((int)weapon_respawn->value / 10) * HZ; + ent->think = ThinkSpecWeap; + return; } - - // zucc make dropped weapons respawn elsewhere -void -ThinkSpecWeap(edict_t* ent) +void ThinkSpecWeap(edict_t* ent) { edict_t* spot; @@ -728,7 +746,7 @@ ThinkSpecWeap(edict_t* ent) } else { - ent->nextthink = level.framenum + 1 * HZ; + ent->nextthink = eztimer(1); ent->think = G_FreeEdict; } } @@ -790,7 +808,7 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) } ent->client->unique_weapon_total--; // dropping 1 unique weapon temp = Drop_Item(ent, item); - temp->think = temp_think_specweap; + temp->think = SpecialWeaponRespawnTimer; ent->client->inventory[index]--; } else if (item->typeNum == M4_NUM) @@ -807,7 +825,7 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) } ent->client->unique_weapon_total--; // dropping 1 unique weapon temp = Drop_Item(ent, item); - temp->think = temp_think_specweap; + temp->think = SpecialWeaponRespawnTimer; ent->client->inventory[index]--; } else if (item->typeNum == M3_NUM) @@ -823,7 +841,7 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) } ent->client->unique_weapon_total--; // dropping 1 unique weapon temp = Drop_Item(ent, item); - temp->think = temp_think_specweap; + temp->think = SpecialWeaponRespawnTimer; ent->client->inventory[index]--; } else if (item->typeNum == HC_NUM) @@ -838,7 +856,7 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) } ent->client->unique_weapon_total--; // dropping 1 unique weapon temp = Drop_Item(ent, item); - temp->think = temp_think_specweap; + temp->think = SpecialWeaponRespawnTimer; ent->client->inventory[index]--; } else if (item->typeNum == SNIPER_NUM) @@ -856,7 +874,7 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) } ent->client->unique_weapon_total--; // dropping 1 unique weapon temp = Drop_Item(ent, item); - temp->think = temp_think_specweap; + temp->think = SpecialWeaponRespawnTimer; ent->client->inventory[index]--; } else if (item->typeNum == DUAL_NUM) @@ -1449,7 +1467,10 @@ Weapon_Generic(edict_t* ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST, if (ent->client->ps.gunframe == FRAME_DEACTIVATE_LAST) { ent->client->weaponstate = WEAPON_BUSY; - ent->client->idle_weapon = BANDAGE_TIME; + if (esp->value && esp_leaderenhance->value && IS_LEADER(ent)) + ent->client->idle_weapon = ENHANCED_BANDAGE_TIME; + else + ent->client->idle_weapon = BANDAGE_TIME; return; } ent->client->ps.gunframe++; @@ -2050,10 +2071,15 @@ int AdjustSpread(edict_t* ent, int spread) if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) // crouching return (spread * .65); - if (INV_AMMO(ent, LASER_NUM) && (ent->client->curr_weap == MK23_NUM - || ent->client->curr_weap == MP5_NUM || ent->client->curr_weap == M4_NUM)) + if (INV_AMMO(ent, LASER_NUM) && + (ent->client->curr_weap == MK23_NUM || + ent->client->curr_weap == MP5_NUM || + ent->client->curr_weap == M4_NUM)) laser = 1; + // Include the Dual MK23 pistol if enabled + if (gun_dualmk23_enhance->value && (INV_AMMO(ent, LASER_NUM)) && (ent->client->curr_weap == DUAL_NUM)) + laser = 1; // running if (xyspeed > running* running) @@ -2917,6 +2943,8 @@ void Dual_Fire(edict_t* ent) ent->client->weapon_sound = MZ_BLASTER2; // Becomes MZ_BLASTER. + if (gun_dualmk23_enhance->value && INV_AMMO(ent, SIL_NUM)) + ent->client->weapon_sound |= MZ_SILENCED; PlayWeaponSound(ent); } else @@ -2989,6 +3017,8 @@ void Dual_Fire(edict_t* ent) ent->client->weapon_sound = MZ_BLASTER2; // Becomes MZ_BLASTER. + if (gun_dualmk23_enhance->value && INV_AMMO(ent, SIL_NUM)) + ent->client->weapon_sound |= MZ_SILENCED; PlayWeaponSound(ent); } @@ -3186,7 +3216,10 @@ Weapon_Generic_Knife(edict_t* ent, int FRAME_ACTIVATE_LAST, if (ent->client->ps.gunframe == FRAME_DEACTIVATE_LAST) { ent->client->weaponstate = WEAPON_BUSY; - ent->client->idle_weapon = BANDAGE_TIME; + if (esp->value && esp_leaderenhance->value && IS_LEADER(ent)) + ent->client->idle_weapon = ENHANCED_BANDAGE_TIME; + else + ent->client->idle_weapon = BANDAGE_TIME; return; } ent->client->ps.gunframe++; @@ -3681,7 +3714,10 @@ void Weapon_Gas(edict_t* ent) if (ent->client->ps.gunframe == 0) { ent->client->weaponstate = WEAPON_BUSY; - ent->client->idle_weapon = BANDAGE_TIME; + if (esp->value && esp_leaderenhance->value && IS_LEADER(ent)) + ent->client->idle_weapon = ENHANCED_BANDAGE_TIME; + else + ent->client->idle_weapon = BANDAGE_TIME; return; } ent->client->ps.gunframe--; diff --git a/src/action/tng_balancer.c b/src/action/tng_balancer.c index 8619bd9b2..a228d7453 100644 --- a/src/action/tng_balancer.c +++ b/src/action/tng_balancer.c @@ -10,7 +10,7 @@ edict_t *FindNewestPlayer(int team) for (i = 0, e = &g_edicts[1]; i < game.maxclients; i++, e++) { - if (!e->inuse || e->client->resp.team != team) + if (!e->inuse || e->client->resp.team != team || (esp->value && IS_LEADER(e))) // don't move leaders continue; if (!newest || e->client->resp.joined_team > newest->client->resp.joined_team) { @@ -59,6 +59,10 @@ qboolean CheckForUnevenTeams (edict_t *ent) swap_ent = FindNewestPlayer(mostPlayers); } + // Never move a leader + if(swap_ent && IS_LEADER(swap_ent)) + return false; + if(swap_ent && (!ent || ent == swap_ent)) { gi.centerprintf (swap_ent, "You have been swapped to the other team to even the game."); unicastSound(swap_ent, gi.soundindex("misc/talk1.wav"), 1.0); diff --git a/src/action/tng_irc.c b/src/action/tng_irc.c index 192ae68d2..454c1cadb 100644 --- a/src/action/tng_irc.c +++ b/src/action/tng_irc.c @@ -13,7 +13,7 @@ // //----------------------------------------------------------------------------- -#ifdef _WIN32 +#ifdef WIN32 #include #include #define bzero(a,b) memset(a,0,b) @@ -49,7 +49,7 @@ tng_irc_t irc_data; -#ifdef _WIN32 +#ifdef WIN32 // Windows junk int IRCstartWinsock() { @@ -164,7 +164,7 @@ IRC_init ( void ) // init our internal structure irc_data.ircstatus = IRC_DISABLED; bzero(irc_data.input, sizeof(irc_data.input)); -#ifdef _WIN32 +#ifdef WIN32 IRCstartWinsock(); #endif } @@ -191,7 +191,7 @@ irc_connect ( void ) struct sockaddr_in hostaddress; struct in_addr ipnum; struct hostent *hostdata; -#ifndef _WIN32 +#ifndef WIN32 int flags; #else IRC_identd_start (); @@ -238,7 +238,7 @@ irc_connect ( void ) strcpy (ircstatus->string, IRC_ST_DISABLED); } else { gi.dprintf ("IRC: connected to %s:%d\n", irc_data.ircserver, ntohs((unsigned short) irc_data.ircport)); -#ifdef _WIN32 +#ifdef WIN32 set_nonblocking(irc_data.ircsocket); #else flags = fcntl(irc_data.ircsocket, F_GETFL); @@ -255,7 +255,7 @@ irc_connect ( void ) send (irc_data.ircsocket, outbuf, strlen(outbuf), 0); if (ircdebug->value) gi.dprintf("IRC: >> NICK %s\nIRC: >> USER tng-mbot * * :%s\n", irc_data.ircuser, hostname->string); -#ifndef _WIN32 +#ifndef WIN32 } #endif } @@ -406,7 +406,7 @@ irc_getinput ( void ) anz_net = recv (irc_data.ircsocket, inbuf, sizeof(inbuf), 0); if (anz_net <= 0) { -#ifdef _WIN32 +#ifdef WIN32 if ((anz_net == 0) || ((anz_net == -1) && (WSAGetLastError() != WSAEWOULDBLOCK))) { #else if ((anz_net == 0) || ((anz_net == -1) && (errno != EAGAIN))) { diff --git a/src/action/tng_jump.c b/src/action/tng_jump.c index 6c3b73c65..89565d502 100644 --- a/src/action/tng_jump.c +++ b/src/action/tng_jump.c @@ -80,8 +80,7 @@ void Cmd_Jmod_f (edict_t *ent) { char *cmd = NULL; - if( ! jump->value ) - { + if( ! jump->value ) { gi.cprintf(ent, PRINT_HIGH, "The server does not have JumpMod enabled.\n"); return; } @@ -92,6 +91,13 @@ void Cmd_Jmod_f (edict_t *ent) gi.cprintf(ent, PRINT_HIGH, " jmod recall - teleport back to saved point\n"); gi.cprintf(ent, PRINT_HIGH, " jmod reset - remove saved point\n"); gi.cprintf(ent, PRINT_HIGH, " jmod clear - reset stats\n"); + gi.cprintf(ent, PRINT_HIGH, " jmod noclip - toggle noclip\n"); + gi.cprintf(ent, PRINT_HIGH, " jmod spawnp <#> - teleport, optional spawnpoint \n"); + gi.cprintf(ent, PRINT_HIGH, " jmod spawnc - teleport closest spawnpoint\n"); + gi.cprintf(ent, PRINT_HIGH, " jmod goto <#> <#> <#> - teleport to x y z coordinates\n"); + gi.cprintf(ent, PRINT_HIGH, " jmod lca - start Lights Camera Action\n"); + gi.cprintf(ent, PRINT_HIGH, " jmod lasersight - toggle lasersight\n"); + return; } @@ -112,15 +118,280 @@ void Cmd_Jmod_f (edict_t *ent) Cmd_Reset_f(ent); return; } - else if(Q_stricmp(cmd, "clear") == 0) + else if(Q_stricmp(cmd, "clear") == 0 || Q_stricmp(cmd, "rhs") == 0) { Cmd_Clear_f(ent); return; } + else if(Q_stricmp(cmd, "goto") == 0) + { + Cmd_Goto_f(ent); + return; + } + else if(Q_stricmp(cmd, "spawnp") == 0) + { + Cmd_GotoP_f(ent); + return; + } + else if(Q_stricmp(cmd, "spawnc") == 0) + { + Cmd_GotoPC_f(ent); + return; + } + else if(Q_stricmp(cmd, "lca") == 0) + { + Cmd_PMLCA_f(ent); + return; + } + else if(Q_stricmp(cmd, "noclip") == 0) + { + Cmd_Noclip_f(ent); + return; + } + else if(Q_stricmp(cmd, "lasersight") == 0) + { + PMLaserSight(ent); + return; + } + else if(Q_stricmp(cmd, "slippers") == 0) + { + PMStealthSlippers(ent); + return; + } gi.cprintf(ent, PRINT_HIGH, "Unknown jmod command\n"); } +edict_t *SelectClosestDeathmatchSpawnPoint (void) +{ + edict_t *bestspot; + float bestdistance, bestplayerdistance; + edict_t *spot; + + spot = NULL; + bestspot = NULL; + bestdistance = -1; + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + bestplayerdistance = PlayersRangeFromSpot (spot); + if ((bestplayerdistance < bestdistance) || (bestdistance < 0)) + { + bestspot = spot; + bestdistance = bestplayerdistance; + } + } + return bestspot; +} + +void Cmd_PMLCA_f(edict_t *ent) +{ + if (ent->client->pers.spectator) + { + gi.cprintf(ent,PRINT_HIGH,"This command cannot be used by spectators\n"); + ent->client->resp.toggle_lca = 0; + return; + } + + // Set this to 1 to prevent damage during LCA + lights_camera_action = 1; + if (!ent->client->resp.toggle_lca) + { + gi.centerprintf (ent,"LIGHTS...\n"); + gi.sound(ent, CHAN_VOICE, gi.soundindex("atl/lights.wav"), 1, ATTN_STATIC, 0); + ent->client->resp.toggle_lca = 43; + } + else if (ent->client->resp.toggle_lca == 23) + { + gi.centerprintf (ent,"CAMERA...\n"); + gi.sound(ent, CHAN_VOICE, gi.soundindex("atl/camera.wav"), 1, ATTN_STATIC, 0); + } + else if (ent->client->resp.toggle_lca == 3) + { + gi.centerprintf (ent,"ACTION!\n"); + gi.sound(ent, CHAN_VOICE, gi.soundindex("atl/action.wav"), 1, ATTN_STATIC, 0); + } + ent->client->resp.toggle_lca--; + // Set it back to 0 for normal damage to occur + lights_camera_action = 0; +} + +edict_t *PMSelectSpawnPoint (int number) +{ + edict_t *spot; + int count = 0; + int selection; + + spot = NULL; + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) + count++; + + if (!count) + return NULL; + + //if random was selected, pick one + if (number == 0) { + selection = rand() % count + 1; + } else { + //if person wanted tele 7 but only 5 found, set to 5 + if (number > count) + number = count; + //if person input a negative, set to 0 + else if (number < 0) + number = 0; + selection = number; + } + + spot = NULL; + do + { + spot = G_Find (spot, FOFS(classname), "info_player_deathmatch"); + selection--; + } while (selection > 0); + + return spot; +} + +void jmodTeleport (edict_t *ent, edict_t *spot) +{ + vec3_t teleport_goto, angles; + int i; + + VectorCopy (spot->s.origin, teleport_goto); + teleport_goto[2] += 9; + VectorCopy (spot->s.angles, angles); + + ent->client->jumping = 0; + ent->movetype = MOVETYPE_NOCLIP; + gi.unlinkentity (ent); + + VectorCopy (teleport_goto, ent->s.origin); + VectorCopy (teleport_goto, ent->s.old_origin); + + // clear the velocity and hold them in place briefly + VectorClear (ent->velocity); + + ent->client->ps.pmove.pm_time = 160>>3; // hold time + + // draw the teleport splash on the player + ent->s.event = EV_PLAYER_TELEPORT; + + VectorClear (ent->s.angles); + VectorClear (ent->client->ps.viewangles); + VectorClear (ent->client->v_angle); + + VectorCopy(angles,ent->s.angles); + VectorCopy(ent->s.angles,ent->client->v_angle); + + for (i=0;i<2;i++) + ent->client->ps.pmove.delta_angles[i] = ANGLE2SHORT(ent->client->v_angle[i] - ent->client->resp.cmd_angles[i]); + if (ent->client->pers.spectator) + ent->solid = SOLID_BBOX; + else + ent->solid = SOLID_TRIGGER; + + ent->deadflag = DEAD_NO; + + gi.linkentity (ent); + + ent->movetype = MOVETYPE_WALK; + + // Run LCA right after spawn + Cmd_PMLCA_f(ent); +} + +void Cmd_Goto_f (edict_t *ent) +{ + int i; + vec3_t teleport_goto; + + if (!ent->deadflag && !ent->client->pers.spectator) + { + // 5 = jmod goto x y z + if (gi.argc() == 5) + { + // Verifying input + if (Q_stricmp(gi.argv(0), "jmod") == 0 && Q_stricmp(gi.argv(1), "goto") == 0){ + // Hacky shit, set gi.argv(2) as teleport_goto[0], etc..: + for (i = 0; i < 3; i++){ + teleport_goto[i] = atoi(gi.argv(i+2)); + } + } + teleport_goto[2] -= ent->viewheight; + + ent->client->jumping = 0; + ent->movetype = MOVETYPE_NOCLIP; + gi.unlinkentity (ent); + + VectorCopy (teleport_goto, ent->s.origin); + VectorCopy (teleport_goto, ent->s.old_origin); + + // clear the velocity and hold them in place briefly + VectorClear (ent->velocity); + + ent->client->ps.pmove.pm_time = 160>>3; // hold time + + // draw the teleport splash on the player + ent->s.event = EV_PLAYER_TELEPORT; + + VectorClear (ent->s.angles); + VectorClear (ent->client->ps.viewangles); + VectorClear (ent->client->v_angle); + + for (i=0;i<2;i++) + ent->client->ps.pmove.delta_angles[i] = ANGLE2SHORT(ent->client->v_angle[i] - ent->client->resp.cmd_angles[i]); + if (ent->client->pers.spectator) + ent->solid = SOLID_BBOX; + else + ent->solid = SOLID_TRIGGER; + + ent->deadflag = DEAD_NO; + + gi.linkentity (ent); + + ent->movetype = MOVETYPE_WALK; + } + else + gi.cprintf(ent,PRINT_HIGH,"Wrong syntax: goto <#> <#> <#>\n"); + } + else + gi.cprintf(ent,PRINT_HIGH,"This command cannot be used by spectators\n"); + +} + +void Cmd_GotoP_f (edict_t *ent) +{ + edict_t *spot = NULL; + char *buffer="\0"; + + if (ent->deadflag || ent->client->pers.spectator) { + gi.cprintf(ent, PRINT_HIGH, "This command cannot be used by spectators\n"); + return; + } + + if (gi.argc() >= 3) { + buffer = strtok(gi.args()," "); + buffer = gi.argv(2); + gi.dprintf("User supplied %s\n", buffer); + spot = PMSelectSpawnPoint(atoi(buffer)); + } else { + spot = PMSelectSpawnPoint(0); + } + jmodTeleport(ent, spot); +} + +void Cmd_GotoPC_f (edict_t *ent) +{ + edict_t *spot = NULL; + + if (ent->deadflag || ent->client->pers.spectator) { + gi.cprintf(ent, PRINT_HIGH, "This command cannot be used by spectators\n"); + return; + } + + spot = SelectClosestDeathmatchSpawnPoint(); + jmodTeleport(ent, spot); +} + void Cmd_Clear_f(edict_t *ent) { ent->client->resp.jmp_highspeed = 0; @@ -219,3 +490,121 @@ void Cmd_Recall_f (edict_t *ent) ent->movetype = MOVETYPE_WALK; } + +void PMLaserSight(edict_t *self) +{ + vec3_t start,forward,right,end; + edict_t *lasersight = self->client->lasersight; + + if (lasersight) { // laser is on + G_FreeEdict(lasersight); + self->client->lasersight = NULL; + return; + } else { + AngleVectors (self->client->v_angle, forward, right, NULL); + + VectorSet(end,100 , 0, 0); + G_ProjectSource (self->s.origin, end, forward, right, start); + + lasersight = G_Spawn(); + self->client->lasersight = lasersight; + lasersight->owner = self; + lasersight->movetype = MOVETYPE_NOCLIP; + lasersight->solid = SOLID_NOT; + lasersight->classname = "lasersight"; + lasersight->s.modelindex = level.model_lsight; + lasersight->s.renderfx = RF_TRANSLUCENT; + lasersight->ideal_yaw = self->viewheight; + lasersight->count = 0; + lasersight->think = LaserSightThink; + lasersight->nextthink = level.framenum + 1; + LaserSightThink( lasersight ); + VectorCopy( lasersight->s.origin, lasersight->s.old_origin ); + VectorCopy( lasersight->s.origin, lasersight->old_origin ); + } +} + +void PMStealthSlippers(edict_t *self) +{ + // Removes stealth slippers + if (self->client->inventory[ITEM_INDEX(GET_ITEM(SLIP_NUM))]) { + self->client->inventory[ITEM_INDEX(GET_ITEM(SLIP_NUM))]--; + } else { + // Adds stealth slippers + AddItem(self, GET_ITEM(SLIP_NUM)); + } +} + +//PaTMaN - New Toggle Command +void Cmd_Toggle_f(edict_t *ent, char *toggle) +{ + //char *s; + char ACT [2][12] = { "Deactivated\0","Activated\0" }; + //char ENA [2][9] = { "Disabled\0","Enabled\0" }; + int spec, val=0; + + //s = strtok(gi.args()," "); + + //if ((gi.argc() == 1) && (s == NULL)) + // if (toggle) + // { + // gi.cprintf(ent,PRINT_HIGH,"Options to toggle: laser, vest, slippers, silencer, helmet, ir\n"); + // return; + // } + + // if (!Q_stricmp(s,"togglecode")) + // s = strtok(NULL," "); + // else + // s = strtok(gi.args()," "); + + spec = ent->client->pers.spectator; + + if ( Q_stricmp(toggle, "laser") == 0 ) + { + if (spec) goto spec; + if (ent->client->resp.toggles & TG_LASER) { + ent->client->resp.toggles -= TG_LASER; + PMLaserSight(ent); + } else { + ent->client->resp.toggles += TG_LASER; + val=1; + PMLaserSight(ent); + } + gi.cprintf(ent, PRINT_HIGH, "Laser %s\n",ACT[val]); + } + else if ( Q_stricmp(toggle, "slippers") == 0 ) + { + if (spec) goto spec; + if (ent->client->resp.toggles & TG_SLIPPERS) { + ent->client->resp.toggles -= TG_SLIPPERS; + PMStealthSlippers(ent); + //ent->client->inventory[ITEM_INDEX(GET_ITEM(SLIP_NUM))]--; + } + else { + ent->client->resp.toggles += TG_SLIPPERS; + val=1; + PMStealthSlippers(ent); + //AddItem(ent, GET_ITEM(SLIP_NUM)); + } + gi.cprintf(ent, PRINT_HIGH, "Slippers %s\n",ACT[val]); + } + // else if ( Q_stricmp(s, "kickable") == 0 ) + // { + // if (spec) goto spec; + // if (ent->client->resp.toggles & TG_KICKABLE) + // { + // ent->client->resp.toggles -= TG_KICKABLE; + // gi.cprintf(ent,PRINT_HIGH, "You are now NOT KICKABLE\n"); + // } + // else + // { + // ent->client->resp.toggles += TG_KICKABLE; + // gi.cprintf(ent,PRINT_HIGH, "You are now KICKABLE\n"); + // } + // } + else + gi.cprintf(ent, PRINT_HIGH, "\"%s\" isn't a valid toggle option\n",toggle); + return; +spec: + gi.cprintf(ent,PRINT_HIGH,"This command cannot be used by spectators\n"); +} diff --git a/src/action/tng_jump.h b/src/action/tng_jump.h index bb9caa912..fa927519a 100644 --- a/src/action/tng_jump.h +++ b/src/action/tng_jump.h @@ -9,7 +9,15 @@ void Jmp_EquipClient(edict_t *ent); void Jmp_SetStats(edict_t *ent); void Cmd_Jmod_f (edict_t *ent); +void Cmd_PMLCA_f (edict_t *ent); +void Cmd_RHS_f (edict_t *ent); +void Cmd_Goto_f (edict_t *ent); +void Cmd_GotoP_f (edict_t *ent); +void Cmd_GotoPC_f (edict_t *ent); void Cmd_Clear_f (edict_t *ent); void Cmd_Reset_f (edict_t *ent); void Cmd_Store_f (edict_t *ent); void Cmd_Recall_f (edict_t *ent); +void Cmd_Toggle_f(edict_t *ent, char *toggle); +void PMLaserSight(edict_t *ent); +void PMStealthSlippers(edict_t *ent); \ No newline at end of file diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index bf5cb078f..ddac82c82 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -75,6 +75,7 @@ void ResetStats(edict_t *ent) { int i; + if(!ent->client) return; @@ -112,21 +113,24 @@ void Stats_AddHit( edict_t *ent, int gun, int hitPart ) if( in_warmup ) return; - if ((unsigned)gun >= MAX_GUNSTAT) { - gi.dprintf( "Stats_AddHit: Bad gun number!\n" ); - return; - } + // Adjusted logic to be inclusive rather than exclusive + if (((unsigned)gun <= MAX_GUNSTAT) || ((unsigned)gun == MOD_KICK) || ((unsigned)gun == MOD_PUNCH)) { - if (!teamplay->value || team_round_going || stats_afterround->value) { - ent->client->resp.hitsTotal++; - ent->client->resp.gunstats[gun].hits++; - ent->client->resp.hitsLocations[hitPart]++; + if (!teamplay->value || team_round_going || stats_afterround->value) { + ent->client->resp.hitsTotal++; + ent->client->resp.gunstats[gun].hits++; + ent->client->resp.hitsLocations[hitPart]++; - if (headShot) - ent->client->resp.gunstats[gun].headshots++; + if (headShot) + ent->client->resp.gunstats[gun].headshots++; + } + if (!headShot) { + ent->client->resp.streakHS = 0; + } } - if (!headShot) { - ent->client->resp.streakHS = 0; + else { + gi.dprintf( "Stats_AddHit: Bad gun number!\n" ); + return; } } @@ -486,7 +490,7 @@ void Cmd_Statmode_f(edict_t* ent) stuffcmd(ent, stuff); } -#if USE_AQTION +#ifdef USE_AQTION // Revisit one day... @@ -543,6 +547,8 @@ void Cmd_Statmode_f(edict_t* ent) // curl_global_cleanup(); // } + + #ifndef NO_BOTS /* ================= @@ -551,7 +557,8 @@ Bot Check */ void StatBotCheck(void) { - for (int i = 0; i < num_players; i++) + int i; + for (i = 0; i < num_players; i++) { if (players[i]->is_bot) { @@ -601,14 +608,15 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) { int mod; int loc; + int chosenItem = 0; int gametime = 0; int roundNum; int eventtime; int vt = 0; //Default victim team is 0 (no team) int kt = 0; //Default killer team is 0 (no team) int ttk = 0; //Default TTK (time to kill) is 0 - int vl = 0; //Placeholder victimleader until Espionage gets ported - int kl = 0; //Placeholder killerleader until Espionage gets ported + int vl = 0; //Default victimleader is 0, 1 if leader + int kl = 0; //Default killerleader is 0, 1 if leader char msg[1024]; // Whole stat line in JSON format char v[24]; // Victim's Steam ID char vn[128]; // Victim's name @@ -632,7 +640,8 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) if (gameSettings & GS_DEATHMATCH) // Only check if in DM { int oc = 0; // Opponent count - for (int i = 0; i < game.maxclients; i++) + int i; + for (i = 0; i < game.maxclients; i++) { // If player is connected and not spectating, add them as an opponent if (game.clients[i].pers.connected && game.clients[i].pers.spectator == false) @@ -645,6 +654,13 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) return; } + if (esp->value){ + if (IS_LEADER(self)) + vl = 1; + if (IS_LEADER(attacker)) + kl = 1; + } + if ((team_round_going && !in_warmup) || (gameSettings & GS_DEATHMATCH)) // If round is active OR if deathmatch { mod = meansOfDeath & ~MOD_FRIENDLY_FIRE; @@ -679,6 +695,29 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) sprintf(vloc, "%5.0f,%5.0f,%5.0f", self->s.origin[0], self->s.origin[1], self->s.origin[2]); sprintf(kloc, "%5.0f,%5.0f,%5.0f", attacker->s.origin[0], attacker->s.origin[1], attacker->s.origin[2]); + // Item identifier, taking item kit mode into account + if (!item_kit_mode->value) { + chosenItem = attacker->client->pers.chosenItem->typeNum; + } else { + if (attacker->client->pers.chosenItem->typeNum == KEV_NUM) { + chosenItem = KEV_NUM; + } else { + // Commando Kit + if (attacker->client->pers.chosenItem->typeNum == BAND_NUM && + attacker->client->pers.chosenItem2->typeNum == HELM_NUM ) { + chosenItem = C_KIT_NUM; + // Stealth Kit + } else if (attacker->client->pers.chosenItem->typeNum == SLIP_NUM && + attacker->client->pers.chosenItem2->typeNum == SIL_NUM ) { + chosenItem = S_KIT_NUM; + // Assassin Kit + } else if (attacker->client->pers.chosenItem->typeNum == LASER_NUM && + attacker->client->pers.chosenItem2->typeNum == SIL_NUM ) { + chosenItem = A_KIT_NUM; + } + } + } + Q_snprintf( msg, sizeof(msg), "{\"frag\":{\"sid\":\"%s\",\"mid\":\"%s\",\"v\":\"%s\",\"vn\":\"%s\",\"vi\":\"%s\",\"vt\":%i,\"vl\":%i,\"k\":\"%s\",\"kn\":\"%s\",\"ki\":\"%s\",\"kt\":%i,\"kl\":%i,\"w\":%i,\"i\":%i,\"l\":%i,\"ks\":%i,\"gm\":%i,\"gmf\":%i,\"ttk\":%d,\"t\":%d,\"gt\":%d,\"m\":\"%s\",\"r\":%i,\"vd\":\"%s\",\"kd\":\"%s\",\"vloc\":\"%s\",\"kloc\":\"%s\"}}\n", @@ -695,7 +734,7 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) kt, kl, mod, - attacker->client->pers.chosenItem->typeNum, + chosenItem, loc, attacker->client->resp.streakKills + 1, Gamemode(), @@ -728,7 +767,7 @@ void LogWorldKill(edict_t *self) int eventtime; int vt = 0; //Default victim team is 0 (no team) int ttk = 0; //Default TTK (time to kill) is 0 - int vl = 0; //Placeholder victimleader until Espionage gets ported + int vl = 0; //Is victim leader? 0 = false, 1 = true char msg[1024]; char v[24]; char vn[128]; @@ -746,7 +785,8 @@ void LogWorldKill(edict_t *self) if (gameSettings & GS_DEATHMATCH) // Only check if in DM { int oc = 0; // Opponent count - for (int i = 0; i < game.maxclients; i++) + int i; + for (i = 0; i < game.maxclients; i++) { // If player is connected and not spectating, add them as an opponent if (game.clients[i].pers.connected && game.clients[i].pers.spectator == false) @@ -759,6 +799,10 @@ void LogWorldKill(edict_t *self) return; } + if (esp->value && IS_LEADER(self)){ + vl = 1; + } + if ((team_round_going && !in_warmup) || (gameSettings & GS_DEATHMATCH)) // If round is active OR if deathmatch { mod = meansOfDeath & ~MOD_FRIENDLY_FIRE; @@ -821,12 +865,71 @@ void LogWorldKill(edict_t *self) } } +/* +================== +LogCapture +================= +*/ +void LogCapture(edict_t *capturer) +{ + int eventtime; + char msg[1024]; + eventtime = (int)time(NULL); + int caps, capstreak; + int ttc = current_round_length / 10; // TimeToCapture (how many seconds in the round) + + // Check if there's an AI bot in the game, if so, do nothing + if (game.ai_ent_found) { + return; + } + + int mode = Gamemode(); + switch (mode) { + case GM_CTF: + caps = capturer->client->resp.ctf_caps; + capstreak = capturer->client->resp.ctf_capstreak; + break; + case GM_ESCORT_THE_VIP: + caps = capturer->client->resp.esp_caps; + capstreak = capturer->client->resp.esp_capstreak; + break; + case GM_DOMINATION: + caps = capturer->client->resp.dom_caps; + capstreak = capturer->client->resp.dom_capstreak; + break; + default: + // Safety measure in case something isn't right + caps = 0; + capstreak = 0; + break; + } + + // (matchid,capturer,capturername,team,gamemode,gamemodeflag,cap,capstreak) + // (mid,steamid,cn,t,team,gm,gmf,c,cs,ttc) + + Q_snprintf( + msg, sizeof(msg), + "{\"capture\":{\"mid\":\"%s\",\"steamid\":\"%s\",\"cn\":\"%s\",\"t\":\"%d\",\"team\":\"%i\",\"gm\":%i,\"gmf\":%i,\"c\":%i,\"cs\":%i,\"ttc\":%i}}\n", + game.matchid, + capturer->client->pers.steamid, + capturer->client->pers.netname, + eventtime, + capturer->client->resp.team, + Gamemode(), + Gamemodeflag(), + caps, + capstreak, + ttc + ); + Write_Stats(msg); +} + /* ================== LogMatch ================= */ -void LogMatch() +void LogMatch(void) { int eventtime; char msg[1024]; @@ -840,8 +943,8 @@ void LogMatch() return; } - // Check for scoreless teamplay, don't log if so - if ((t1 == 0 && t2 == 0 && t3 == 0) && (teamplay->value)) { + // Check for scoreless teamplay, don't log + if (teamplay->value && t1 == 0 && t2 == 0 && t3 == 0) { return; } @@ -866,7 +969,7 @@ void LogMatch() LogAward ================= */ -void LogAward(char* steamid, char* discordid, int award) +void LogAward(edict_t *ent, int award) { int gametime = 0; int eventtime; @@ -890,57 +993,46 @@ void LogAward(char* steamid, char* discordid, int award) eventtime, gametime, award, - steamid, + ent->client->pers.steamid, mod, - discordid + ent->client->pers.discordid ); Write_Stats(msg); } /* ================== -LogEndMatchStats +WriteLogEndMatchStats + +This function is called from LogEndMatchStats because we're writing +real clients and ghost clients, to reduce code duplication. ================= */ -void LogEndMatchStats() + +void WriteLogEndMatchStats(gclient_t *cl) { - int i; - char msg[1024]; - gclient_t *sortedClients[MAX_CLIENTS], *cl; - int totalClients, secs, shots; + int secs, shots; double accuracy, fpm; - char steamid[24]; - char discordid[24]; - totalClients = G_SortedClients(sortedClients); - - // Check if there's an AI bot in the game, if so, do nothing - if (game.ai_ent_found) { - return; - } + char msg[1024]; - for (i = 0; i < totalClients; i++){ - cl = sortedClients[i]; - shots = min( cl->resp.shotsTotal, 9999 ); - secs = (level.framenum - cl->resp.enterframe) / HZ; + shots = min( cl->resp.shotsTotal, 9999 ); + secs = (level.framenum - cl->resp.enterframe) / HZ; - if (shots) - accuracy = (double)cl->resp.hitsTotal * 100.0 / (double)cl->resp.shotsTotal; - else - accuracy = 0; - if (secs > 0) - fpm = (double)cl->resp.score * 60.0 / (double)secs; - else - fpm = 0.0; - - Q_strncpyz(steamid, Info_ValueForKey(cl->pers.userinfo, "steamid"), sizeof(steamid)); - Q_strncpyz(discordid, Info_ValueForKey(cl->pers.userinfo, "cl_discord_id"), sizeof(discordid)); + if (shots) + accuracy = (double)cl->resp.hitsTotal * 100.0 / (double)cl->resp.shotsTotal; + else + accuracy = 0; + if (secs > 0) + fpm = (double)cl->resp.score * 60.0 / (double)secs; + else + fpm = 0.0; - Q_snprintf( + Q_snprintf( msg, sizeof(msg), - "{\"matchstats\":{\"sid\":\"%s\",\"mid\":\"%s\",\"s\":\"%s\",\"sc\":%i,\"sh\":%i,\"a\":%f,\"f\":%f,\"dd\":%i,\"d\":%i,\"k\":%i,\"ctfc\":%i,\"ctfcs\":%i,\"ht\":%i,\"tk\":%i,\"t\":%i,\"hks\":%i,\"hhs\":%i,\"dis\":\"%s\",\"pt\":%i}}\n", + "{\"matchstats\":{\"sid\":\"%s\",\"mid\":\"%s\",\"s\":\"%s\",\"sc\":%i,\"sh\":%i,\"a\":%f,\"f\":%f,\"dd\":%i,\"d\":%i,\"k\":%i,\"ctfc\":%i,\"ctfcs\":%i,\"ht\":%i,\"tk\":%i,\"t\":%i,\"hks\":%i,\"hhs\":%i,\"dis\":\"%s\",\"pt\":%i,\"hlh\":%i,\"hlc\":%i,\"hls\":%i,\"hll\":%i,\"hlkh\":%i,\"hlkv\":%i,\"hln\":%i,\"gss1\":%i,\"gss2\":%i,\"gss3\":%i,\"gss4\":%i,\"gss5\":%i,\"gss6\":%i,\"gss7\":%i,\"gss8\":%i,\"gss9\":%i,\"gss13\":%i,\"gss14\":%i,\"gss35\":%i,\"gsh1\":%i,\"gsh2\":%i,\"gsh3\":%i,\"gsh4\":%i,\"gsh5\":%i,\"gsh6\":%i,\"gsh7\":%i,\"gsh8\":%i,\"gsh9\":%i,\"gsh13\":%i,\"gsh14\":%i,\"gsh35\":%i,\"gshs1\":%i,\"gshs2\":%i,\"gshs3\":%i,\"gshs4\":%i,\"gshs5\":%i,\"gshs6\":%i,\"gshs7\":%i,\"gshs8\":%i,\"gshs9\":%i,\"gshs13\":%i,\"gshs14\":%i,\"gshs35\":%i,\"gsk1\":%i,\"gsk2\":%i,\"gsk3\":%i,\"gsk4\":%i,\"gsk5\":%i,\"gsk6\":%i,\"gsk7\":%i,\"gsk8\":%i,\"gsk9\":%i,\"gsk13\":%i,\"gsk14\":%i,\"gsk35\":%i,\"gsd1\":%i,\"gsd2\":%i,\"gsd3\":%i,\"gsd4\":%i,\"gsd5\":%i,\"gsd6\":%i,\"gsd7\":%i,\"gsd8\":%i,\"gsd9\":%i,\"gsd13\":%i,\"gsd14\":%i,\"gsd35\":%i,\"ecdc\":%i,\"ecs\":%i,\"ecsb\":%i,\"elfc\":%i,\"elks\":%i,\"elksb\":%i,\"elpc\":%i}}\n", server_id->string, game.matchid, - steamid, + cl->pers.steamid, cl->resp.score, shots, accuracy, @@ -955,10 +1047,165 @@ void LogEndMatchStats() cl->resp.team, cl->resp.streakKillsHighest, cl->resp.streakHSHighest, - discordid, - secs - ); - Write_Stats(msg); + cl->pers.discordid, + secs, + cl->resp.hitsLocations[LOC_HDAM], + cl->resp.hitsLocations[LOC_CDAM], + cl->resp.hitsLocations[LOC_SDAM], + cl->resp.hitsLocations[LOC_LDAM], + cl->resp.hitsLocations[LOC_KVLR_HELMET], + cl->resp.hitsLocations[LOC_KVLR_VEST], + cl->resp.hitsLocations[LOC_NO], + cl->resp.gunstats[MOD_MK23].shots, + cl->resp.gunstats[MOD_MP5].shots, + cl->resp.gunstats[MOD_M4].shots, + cl->resp.gunstats[MOD_M3].shots, + cl->resp.gunstats[MOD_HC].shots, + cl->resp.gunstats[MOD_SNIPER].shots, + cl->resp.gunstats[MOD_DUAL].shots, + cl->resp.gunstats[MOD_KNIFE].shots, + cl->resp.gunstats[MOD_KNIFE_THROWN].shots, + cl->resp.gunstats[MOD_HG_SPLASH].shots, + cl->resp.gunstats[MOD_PUNCH].shots, + cl->resp.gunstats[MOD_KICK].shots, + cl->resp.gunstats[MOD_MK23].hits, + cl->resp.gunstats[MOD_MP5].hits, + cl->resp.gunstats[MOD_M4].hits, + cl->resp.gunstats[MOD_M3].hits, + cl->resp.gunstats[MOD_HC].hits, + cl->resp.gunstats[MOD_SNIPER].hits, + cl->resp.gunstats[MOD_DUAL].hits, + cl->resp.gunstats[MOD_KNIFE].hits, + cl->resp.gunstats[MOD_KNIFE_THROWN].hits, + cl->resp.gunstats[MOD_HG_SPLASH].hits, + cl->resp.gunstats[MOD_PUNCH].hits, + cl->resp.gunstats[MOD_KICK].hits, + cl->resp.gunstats[MOD_MK23].headshots, + cl->resp.gunstats[MOD_MP5].headshots, + cl->resp.gunstats[MOD_M4].headshots, + cl->resp.gunstats[MOD_M3].headshots, + cl->resp.gunstats[MOD_HC].headshots, + cl->resp.gunstats[MOD_SNIPER].headshots, + cl->resp.gunstats[MOD_DUAL].headshots, + cl->resp.gunstats[MOD_KNIFE].headshots, + cl->resp.gunstats[MOD_KNIFE_THROWN].headshots, + cl->resp.gunstats[MOD_HG_SPLASH].headshots, + cl->resp.gunstats[MOD_PUNCH].headshots, + cl->resp.gunstats[MOD_KICK].headshots, + cl->resp.gunstats[MOD_MK23].kills, + cl->resp.gunstats[MOD_MP5].kills, + cl->resp.gunstats[MOD_M4].kills, + cl->resp.gunstats[MOD_M3].kills, + cl->resp.gunstats[MOD_HC].kills, + cl->resp.gunstats[MOD_SNIPER].kills, + cl->resp.gunstats[MOD_DUAL].kills, + cl->resp.gunstats[MOD_KNIFE].kills, + cl->resp.gunstats[MOD_KNIFE_THROWN].kills, + cl->resp.gunstats[MOD_HG_SPLASH].kills, + cl->resp.gunstats[MOD_PUNCH].kills, + cl->resp.gunstats[MOD_KICK].kills, + cl->resp.gunstats[MOD_MK23].damage, + cl->resp.gunstats[MOD_MP5].damage, + cl->resp.gunstats[MOD_M4].damage, + cl->resp.gunstats[MOD_M3].damage, + cl->resp.gunstats[MOD_HC].damage, + cl->resp.gunstats[MOD_SNIPER].damage, + cl->resp.gunstats[MOD_DUAL].damage, + cl->resp.gunstats[MOD_KNIFE].damage, + cl->resp.gunstats[MOD_KNIFE_THROWN].damage, + cl->resp.gunstats[MOD_HG_SPLASH].damage, + cl->resp.gunstats[MOD_PUNCH].damage, + cl->resp.gunstats[MOD_KICK].damage, + cl->resp.esp_capdefendercount, + cl->resp.esp_capstreak, + cl->resp.esp_capstreakbest, + cl->resp.esp_leaderfragcount, + cl->resp.esp_leaderkillstreak, + cl->resp.esp_leaderkillstreakbest, + cl->resp.esp_leaderprotectcount + ); + Write_Stats(msg); +} + + +/* +================= +LogEndMatchStats +================= +*/ +void LogEndMatchStats(void) +{ + int i; + gclient_t *sortedClients[MAX_CLIENTS], *cl; + int totalClients; + totalClients = G_SortedClients(sortedClients); + + // Check if there's an AI bot in the game, if so, do nothing + if (game.ai_ent_found) { + return; + } + + // Write out the stats for each client still connected to the server + for (i = 0; i < totalClients; i++){ + cl = sortedClients[i]; + + //gi.dprintf("Writing real stats for %s\n", cl->pers.netname); + WriteLogEndMatchStats(cl); + } + + // Write out the stats for each ghost client + if (!use_ghosts->value || num_ghost_players == 0) { + // Ghostbusters got em all + return; + } else { + gghost_t *ghost = NULL; + edict_t *ent = NULL; + gclient_t *ghlient = NULL; + + // Initialize the ghost client + ent = G_Spawn(); + ent->client = gi.TagMalloc(sizeof(gclient_t), TAG_LEVEL); + if (!ent->client) { + gi.error("%s: Couldn't allocate client memory\n", __FUNCTION__); + return; + } + ghlient = ent->client; + + for (i = 0, ghost = ghost_players; i < num_ghost_players; i++, ghost++) { + //gi.dprintf("I found %i ghosts, Writing ghost stats for %s\n", num_ghost_players, ghost->netname); + + // Copy ghost data to this dummy entity + strcpy(ghlient->pers.ip, ghost->ip); + strcpy(ghlient->pers.netname, ghost->netname); + strcpy(ghlient->pers.steamid, ghost->steamid); + strcpy(ghlient->pers.discordid, ghost->discordid); + + ghlient->resp.enterframe = ghost->enterframe; + ghlient->resp.score = ghost->score; + ghlient->resp.kills = ghost->kills; + ghlient->resp.deaths = ghost->deaths; + ghlient->resp.damage_dealt = ghost->damage_dealt; + ghlient->resp.ctf_caps = ghost->ctf_caps; + ghlient->resp.ctf_capstreak = ghost->ctf_capstreak; + ghlient->resp.team_kills = ghost->team_kills; + ghlient->resp.streakKillsHighest = ghost->streakKillsHighest; + ghlient->resp.streakHSHighest = ghost->streakHSHighest; + ghlient->resp.shotsTotal = ghost->shotsTotal; + ghlient->resp.hitsTotal = ghost->hitsTotal; + + if (teamplay->value && ghost->team) + ghlient->resp.team = ghost->team; + + // Copy all gunstats and hit locations to this dummy entity + memcpy(ghlient->resp.hitsLocations, ghost->hitsLocations, sizeof(ghlient->resp.hitsLocations)); + memcpy(ghlient->resp.gunstats, ghost->gunstats, sizeof(ghlient->resp.gunstats)); + + WriteLogEndMatchStats(ghlient); + } + // Loop is over, set this to zero + num_ghost_players = 0; + // Be free, little dummy entity + G_FreeEdict(ent); } } -#endif \ No newline at end of file +#endif diff --git a/src/shared/shared.c b/src/shared/shared.c index 07507af47..fcf890d46 100644 --- a/src/shared/shared.c +++ b/src/shared/shared.c @@ -20,6 +20,14 @@ with this program; if not, write to the Free Software Foundation, Inc., const vec3_t vec3_origin = { 0, 0, 0 }; +void VectorRotate2( vec3_t v, float degrees ) +{ + float radians = DEG2RAD(degrees); + float x = v[0], y = v[1]; + v[0] = x * cosf(radians) - y * sinf(radians); + v[1] = y * cosf(radians) + x * sinf(radians); +} + void AngleVectors(const vec3_t angles, vec3_t forward, vec3_t right, vec3_t up) { float angle; From 4c4cb5b8cf866cc3cc1d3a9d8609a83ac0d04907 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Feb 2024 23:17:33 -0500 Subject: [PATCH 002/974] Cleaned up some more warnings --- src/action/a_cmds.c | 4 ++-- src/action/a_ctf.c | 6 +++--- src/action/a_esp.c | 4 ++-- src/action/a_xgame.c | 8 ++++---- src/action/acesrc/acebot.h | 4 ++-- src/action/acesrc/acebot_cmds.c | 4 ++-- src/action/g_cmds.c | 6 +++--- src/action/g_func.c | 6 +++--- src/action/g_local.h | 4 ++-- src/action/g_main.c | 8 ++++---- src/action/g_save.c | 8 ++++---- src/action/g_spawn.c | 2 +- src/action/p_client.c | 8 ++++---- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 563ba5b10..614be9e42 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -134,7 +134,7 @@ #include "g_local.h" #include #ifdef USE_AQTION -#ifdef WIN32 +#ifdef _WIN32 #if _MSC_VER >= 1920 && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include @@ -1316,7 +1316,7 @@ void Cmd_Ghost_f(edict_t * ent) #ifdef USE_AQTION void generate_uuid(void) { -#ifdef WIN32 +#ifdef _WIN32 #if _MSC_VER >= 1920 && !__INTEL_COMPILER UUID uuid; unsigned char* uuidStr; diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 18a54a2f8..289220e3b 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -85,7 +85,7 @@ qboolean CTFLoadConfig(char *mapname) memset(&ctfgame, 0, sizeof(ctfgame)); - gi.dprintf("Trying to load CTF configuration file\n", mapname); + gi.dprintf("Trying to load CTF configuration file for %s\n", mapname); /* zero is perfectly acceptable respawn time, but we want to know if it came from the config or not */ ctfgame.spawn_red = -1; @@ -317,7 +317,7 @@ int CTFOtherTeam(int team) /*--------------------------------------------------------------------------*/ -void ResetPlayers() +void ResetPlayers(void) { edict_t *ent; int i; @@ -331,7 +331,7 @@ void ResetPlayers() } } -void CTFSwapTeams() +void CTFSwapTeams(void) { edict_t *ent; int i; diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 6fbfbc9a7..d7574ebbf 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -507,8 +507,8 @@ void EspMakeCapturePoint(edict_t *flag) gi.linkentity(flag->obj_arrow); if (esp_debug->value){ - gi.dprintf("%s: ** Indicator arrow spawned at <%d %d %d>\n", __FUNCTION__, flag->obj_arrow->s.origin[0], flag->obj_arrow->s.origin[1], flag->obj_arrow->s.origin[2]); - gi.dprintf("%s: ** Flag coordinates are: <%d %d %d>\n", __FUNCTION__, flag->s.origin[0], flag->s.origin[1], flag->s.origin[2]); + gi.dprintf("%s: ** Indicator arrow spawned at <%f %f %f>\n", __FUNCTION__, flag->obj_arrow->s.origin[0], flag->obj_arrow->s.origin[1], flag->obj_arrow->s.origin[2]); + gi.dprintf("%s: ** Flag coordinates are: <%f %f %f>\n", __FUNCTION__, flag->s.origin[0], flag->s.origin[1], flag->s.origin[2]); } } #endif diff --git a/src/action/a_xgame.c b/src/action/a_xgame.c index 896caad62..8c7a8b6c5 100644 --- a/src/action/a_xgame.c +++ b/src/action/a_xgame.c @@ -646,7 +646,7 @@ void VideoCheckClient(edict_t *ent) Message *timedMessages = NULL; int numMessages = 0; -qboolean TimedMessageAtTimeAll() +qboolean TimedMessageAtTimeAll(void) { int crl = (current_round_length / 10); qboolean anyMessageFired = false; @@ -669,7 +669,7 @@ qboolean TimedMessageAtTimeAll() return anyMessageFired; } -qboolean TimedMessageAtTimeEnt() +qboolean TimedMessageAtTimeEnt(void) { int crl = (current_round_length / 10); qboolean anyMessageFired = false; @@ -679,7 +679,7 @@ qboolean TimedMessageAtTimeEnt() team_round_going && crl >= timedMessages[i].seconds && timedMessages[i].ent != NULL) { - gi.centerprintf(timedMessages[i].ent, timedMessages[i].msg); + gi.centerprintf(timedMessages[i].ent, "%s", timedMessages[i].msg); timedMessages[i].fired = true; anyMessageFired = true; } @@ -688,7 +688,7 @@ qboolean TimedMessageAtTimeEnt() return anyMessageFired; } -void FireTimedMessages() +void FireTimedMessages(void) { TimedMessageAtTimeAll(); TimedMessageAtTimeEnt(); diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 0e4d6fbd1..11dd82bd2 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -296,8 +296,8 @@ edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); void ACESP_ReAddBots(void); void ACESP_RemoveBot(char *name); void attract_mode_bot_check(void); -void safe_cprintf (edict_t *ent, int printlevel, char *fmt, ...); -void safe_centerprintf (edict_t *ent, char *fmt, ...); +void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); +void safe_centerprintf (edict_t *ent, const char *fmt, ...); void debug_printf (char *fmt, ...); void LTKClearBotNames(void); diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 1844a6d06..7287e3289 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -197,7 +197,7 @@ void (*real_centerprintf) (edict_t * ent, const char *fmt, ...) = NULL; /////////////////////////////////////////////////////////////////////// // botsafe cprintf /////////////////////////////////////////////////////////////////////// -void safe_cprintf (edict_t *ent, int printlevel, char *fmt, ...) +void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...) { char bigbuffer[0x10000]; va_list argptr; @@ -218,7 +218,7 @@ void safe_cprintf (edict_t *ent, int printlevel, char *fmt, ...) /////////////////////////////////////////////////////////////////////// // botsafe centerprintf /////////////////////////////////////////////////////////////////////// -void safe_centerprintf (edict_t *ent, char *fmt, ...) +void safe_centerprintf (edict_t *ent, const char *fmt, ...) { char bigbuffer[0x10000]; va_list argptr; diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index a30c911e7..52e57fc57 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -727,7 +727,7 @@ static void Cmd_Notarget_f (edict_t * ent) else msg = "notarget ON\n"; - gi.cprintf (ent, PRINT_HIGH, msg); + gi.cprintf (ent, PRINT_HIGH, "%s", msg); } @@ -753,7 +753,7 @@ void Cmd_Noclip_f (edict_t * ent) msg = "noclip ON\n"; } - gi.cprintf (ent, PRINT_HIGH, msg); + gi.cprintf (ent, PRINT_HIGH, "%s", msg); } @@ -1713,7 +1713,7 @@ static void Cmd_PrintSettings_f( edict_t * ent ) (int)llsound->value ); #endif - gi.cprintf( ent, PRINT_HIGH, text ); + gi.cprintf( ent, PRINT_HIGH, "%s", text); } static void Cmd_Follow_f( edict_t *ent ) diff --git a/src/action/g_func.c b/src/action/g_func.c index 9223b9423..2fe0286ba 100644 --- a/src/action/g_func.c +++ b/src/action/g_func.c @@ -855,7 +855,7 @@ static void button_touch(edict_t *self, edict_t *other, cplane_t *plane, csurfac button_fire(self); } -static void button_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) +static void button_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { self->activator = attacker; self->health = self->max_health; @@ -1227,7 +1227,7 @@ static void door_blocked(edict_t *self, edict_t *other) } } -static void door_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) +static void door_killed(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { edict_t *ent; @@ -2034,7 +2034,7 @@ static void door_secret_blocked(edict_t *self, edict_t *other) T_Damage(other, self, self, vec3_origin, other->s.origin, vec3_origin, self->dmg, 1, 0, MOD_CRUSH); } -static void door_secret_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) +static void door_secret_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { self->takedamage = DAMAGE_NO; door_secret_use(self, attacker, attacker); diff --git a/src/action/g_local.h b/src/action/g_local.h index 1cede38df..2b94c8777 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1520,7 +1520,7 @@ void* G_FetchGameExtension(char *name); // void player_pain (edict_t * self, edict_t * other, float kick, int damage); void player_die (edict_t * self, edict_t * inflictor, edict_t * attacker, - int damage, const vec3_t point); + int damage, vec3_t point); // // g_svcmds.c @@ -2146,7 +2146,7 @@ struct edict_s void (*touch)(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); void (*use)(edict_t *self, edict_t *other, edict_t *activator); void (*pain)(edict_t *self, edict_t *other, float kick, int damage); - void (*die)(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point); + void (*die)(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); int touch_debounce_framenum; // are all these legit? do we need more/less of them? int pain_debounce_framenum; diff --git a/src/action/g_main.c b/src/action/g_main.c index 1620f37c6..a639f13b4 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -548,10 +548,10 @@ void ClientBegin (edict_t * ent); void ClientCommand (edict_t * ent); void CheckNeedPass (void); void RunEntity (edict_t * ent); -void WriteGame (char *filename, qboolean autosave); -void ReadGame (char *filename); -void WriteLevel (char *filename); -void ReadLevel (char *filename); +void WriteGame (const char *filename, qboolean autosave); +void ReadGame (const char *filename); +void WriteLevel (const char *filename); +void ReadLevel (const char *filename); void InitGame (void); void G_RunFrame (void); diff --git a/src/action/g_save.c b/src/action/g_save.c index 6914217ad..11e51b12a 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -728,18 +728,18 @@ void InitGame( void ) //========================================================= -void WriteGame (char *filename, qboolean autosave) +void WriteGame (const char *filename, qboolean autosave) { } -void ReadGame (char *filename) +void ReadGame (const char *filename) { } -void WriteLevel (char *filename) +void WriteLevel (const char *filename) { } -void ReadLevel (char *filename) +void ReadLevel (const char *filename) { } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index b5098517d..864a8be2c 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -876,7 +876,7 @@ Creates a server's entity / program execution context by parsing textual entity definitions out of an ent file. ============== */ -void SpawnEntities (char *mapname, const char *entities, char *spawnpoint) +void SpawnEntities (char *mapname, const char *entities, const char *spawnpoint) { edict_t *ent = NULL; gclient_t *client; diff --git a/src/action/p_client.c b/src/action/p_client.c index e6bd8aadb..4d7624452 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1454,7 +1454,7 @@ void LookAtKiller(edict_t * self, edict_t * inflictor, edict_t * attacker) player_die ================== */ -void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, const vec3_t point) +void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { int n, mod; @@ -1947,7 +1947,7 @@ void InitBodyQue(void) } } -void body_die(edict_t * self, edict_t * inflictor, edict_t * attacker, int damage, const vec3_t point) +void body_die(edict_t * self, edict_t * inflictor, edict_t * attacker, int damage, vec3_t point) { /* int n;*/ @@ -3011,7 +3011,7 @@ void ClientBeginDeathmatch(edict_t * ent) if(am->value && game.bot_count > 0){ char msg[128]; Q_snprintf(msg, sizeof(msg), "** This server contains BOTS for you to play with until real players join up! Enjoy! **"); - gi.centerprintf(ent, msg); + gi.centerprintf(ent, "%s", msg); } ent->client->resp.motd_refreshes = 1; @@ -3461,7 +3461,7 @@ void CreateGhost(edict_t * ent) edict_t *pm_passent; // pmove doesn't need to know about passent and contentmask -trace_t q_gameabi PM_trace(vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end) +trace_t q_gameabi PM_trace(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end) { if (pm_passent && pm_passent->health > 0) return gi.trace(start, mins, maxs, end, pm_passent, MASK_PLAYERSOLID); From 6104d86b9a3648939945ba8f5570d7fbb6bf09f1 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Feb 2024 23:25:47 -0500 Subject: [PATCH 003/974] IRC bot stuff before we decide to remove it entirely --- src/action/a_cmds.c | 4 ++-- src/action/addons/tngbot/tngbot.c | 16 +++++++++++----- src/action/tng_irc.c | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 614be9e42..31d207c7c 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -135,7 +135,7 @@ #include #ifdef USE_AQTION #ifdef _WIN32 -#if _MSC_VER >= 1920 && !__INTEL_COMPILER +#if _MSC_VER && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include #endif @@ -1317,7 +1317,7 @@ void Cmd_Ghost_f(edict_t * ent) void generate_uuid(void) { #ifdef _WIN32 -#if _MSC_VER >= 1920 && !__INTEL_COMPILER +#if _MSC_VER && !__INTEL_COMPILER UUID uuid; unsigned char* uuidStr; diff --git a/src/action/addons/tngbot/tngbot.c b/src/action/addons/tngbot/tngbot.c index 7b1c24ea7..375b0901d 100644 --- a/src/action/addons/tngbot/tngbot.c +++ b/src/action/addons/tngbot/tngbot.c @@ -70,18 +70,24 @@ */ #include -#include #include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#define bzero(a,b) memset(a,0,b) +#else +#include #include #include #include #include #include -#include -#include #include -#include - +#endif #define IRC_PORT 6667 #define BUFLEN 2048 diff --git a/src/action/tng_irc.c b/src/action/tng_irc.c index 454c1cadb..ccf371566 100644 --- a/src/action/tng_irc.c +++ b/src/action/tng_irc.c @@ -13,7 +13,7 @@ // //----------------------------------------------------------------------------- -#ifdef WIN32 +#ifdef _WIN32 #include #include #define bzero(a,b) memset(a,0,b) From a07c91227a102aa4245c6107953cb85cdb360c3e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Feb 2024 23:48:15 -0500 Subject: [PATCH 004/974] GameAPI issues at runtime now --- src/action/g_main.c | 2 +- src/action/g_spawn.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/g_main.c b/src/action/g_main.c index a639f13b4..494cd238e 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -592,7 +592,7 @@ void ShutdownGame (void) and global variables ================= */ -game_export_t *GetGameAPI (game_import_t * import) +q_exported game_export_t *GetGameAPI(game_import_t *import) { gi = *import; #ifndef NO_BOTS diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 864a8be2c..a16a59d7c 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1038,7 +1038,7 @@ void SpawnEntities (char *mapname, const char *entities, const char *spawnpoint) gi.dprintf ("Espionage Enabled - Forcing Domination off\n"); gi.cvar_forceset(dom->name, "0"); } - if (!DMFLAGS(DF_NO_FRIENDLY_FIRE)) + if ((int)dmflags->value & DF_NO_FRIENDLY_FIRE) { gi.dprintf ("Espionage Enabled - Forcing Friendly Fire off\n"); gi.cvar_forceset(dmflags->name, va("%i", (int)dmflags->value | DF_NO_FRIENDLY_FIRE)); From 77048a81dc498e51bff9736382d54b825ace9ded Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Feb 2024 23:22:04 -0500 Subject: [PATCH 005/974] Still broken but at least segfaults --- src/action/a_cmds.c | 6 ++--- src/action/a_team.c | 2 +- src/action/g_local.h | 27 ++++++++++++++++++-- src/action/g_main.c | 3 ++- src/action/g_save.c | 24 +++++++++++++++-- src/action/g_spawn.c | 61 ++++++++++++++++++++++++++++++++------------ 6 files changed, 97 insertions(+), 26 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 31d207c7c..a049b84e2 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -918,7 +918,7 @@ void SetIDView(edict_t * ent) if (ent->client->chase_mode) { if (ent->client->chase_target && ent->client->chase_target->inuse) { ent->client->ps.stats[STAT_ID_VIEW] = - CS_PLAYERSKINS + (ent->client->chase_target - g_edicts - 1); + game.csr.playerskins + (ent->client->chase_target - g_edicts - 1); } return; } @@ -934,7 +934,7 @@ void SetIDView(edict_t * ent) tr = gi.trace(ent->s.origin, NULL, NULL, forward, ent, MASK_SOLID); POSTTRACE(); if (tr.fraction < 1 && tr.ent && tr.ent->client) { - ent->client->ps.stats[STAT_ID_VIEW] = CS_PLAYERSKINS + (ent - g_edicts - 1); + ent->client->ps.stats[STAT_ID_VIEW] = game.csr.playerskins + (ent - g_edicts - 1); return; } @@ -958,7 +958,7 @@ void SetIDView(edict_t * ent) } } if (best != NULL && bd > 0.90) { - ent->client->ps.stats[STAT_ID_VIEW] = CS_PLAYERSKINS + (best - g_edicts - 1); + ent->client->ps.stats[STAT_ID_VIEW] = game.csr.playerskins + (best - g_edicts - 1); } } diff --git a/src/action/a_team.c b/src/action/a_team.c index 5b22b0aa3..0cd2244af 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1311,7 +1311,7 @@ void AssignSkin (edict_t * ent, const char *s, qboolean nickChanged) } } - gi.configstring(CS_PLAYERSKINS + playernum, skin); + gi.configstring(game.csr.playerskins + playernum, skin); } /* diff --git a/src/action/g_local.h b/src/action/g_local.h index 2b94c8777..ef303aeb5 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -305,7 +305,13 @@ #define G_GMF_VARIABLE_FPS 0 #endif -#define G_FEATURES (GMF_CLIENTNUM | GMF_PROPERINUSE | GMF_MVDSPEC | GMF_WANT_ALL_DISCONNECTS | G_GMF_VARIABLE_FPS) +#if USE_PROTOCOL_EXTENSIONS +#define G_GMF_PROTOCOL_EXTENSIONS GMF_PROTOCOL_EXTENSIONS +#else +#define G_GMF_PROTOCOL_EXTENSIONS 0 +#endif + +#define G_FEATURES (GMF_CLIENTNUM | GMF_PROPERINUSE | GMF_MVDSPEC | GMF_WANT_ALL_DISCONNECTS | G_GMF_VARIABLE_FPS | G_GMF_PROTOCOL_EXTENSIONS) // protocol bytes that can be directly added to messages #define svc_muzzleflash 1 @@ -749,6 +755,9 @@ typedef struct // items int num_items; + + //q2pro protocol extensions + cs_remap_t csr; // stats char matchid[MAX_QPATH]; @@ -1156,6 +1165,7 @@ extern cvar_t *maptime; extern cvar_t *capturelimit; extern cvar_t *password; extern cvar_t *g_select_empty; +extern cvar_t *g_protocol_extensions; extern cvar_t *dedicated; extern cvar_t *steamid; @@ -2092,7 +2102,7 @@ struct edict_s int clipmask; edict_t *owner; - + entity_state_extension_t x; // DO NOT MODIFY ANYTHING ABOVE THIS, THE SERVER // EXPECTS THE FIELDS IN THAT ORDER! @@ -2521,3 +2531,16 @@ extern Message *timedMessages; void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); void FireTimedMessages(void); + +/* +===================================================================== + CONFIG STRING REMAPPING +===================================================================== +*/ + +#if USE_PROTOCOL_EXTENSIONS + +extern const cs_remap_t cs_remap_old; +extern const cs_remap_t cs_remap_new; + +#endif \ No newline at end of file diff --git a/src/action/g_main.c b/src/action/g_main.c index 494cd238e..8da807901 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -347,6 +347,7 @@ cvar_t *password; cvar_t *maxclients; cvar_t *maxentities; cvar_t *g_select_empty; +cvar_t *g_protocol_extensions; cvar_t *dedicated; cvar_t *steamid; cvar_t *filterban; @@ -1144,7 +1145,7 @@ void CycleLights (void) } temp[0] = brightness[day_cycle_at]; temp[1] = 0; - gi.configstring (CS_LIGHTS + 0, temp); + gi.configstring (game.csr.lights + 0, temp); day_next_cycle = level.time + day_cycle->value; } } diff --git a/src/action/g_save.c b/src/action/g_save.c index 11e51b12a..92c2557e4 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -323,6 +323,8 @@ void InitGame( void ) { cvar_t *cv; + int features = G_FEATURES; + InitCommandList(); IRC_init(); @@ -540,6 +542,24 @@ void InitGame( void ) // flood control flood_threshold = gi.cvar( "flood_threshold", "4", 0 ); + gi.dprintf( "Reading extra server features\n" ); + cv = gi.cvar( "sv_features", NULL, 0 ); + if (cv) { + game.serverfeatures = (int)cv->value; + + if (game.serverfeatures & GMF_CLIENTNUM) { + gi.dprintf( "...server supports GMF_CLIENTNUM\n" ); + } + + if (game.serverfeatures & GMF_PROTOCOL_EXTENSIONS && (int)g_protocol_extensions->value) { + features |= GMF_PROTOCOL_EXTENSIONS; + gi.dprintf( "...server supports GMF_PROTOCOL_EXTENSIONS\n" ); + game.csr = cs_remap_new; + } else { + game.csr = cs_remap_old; + } + } + jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO|*/ CVAR_LATCH); // jumping mod -- removed from serverinfo 2022 warmup = gi.cvar( "warmup", "0", CVAR_LATCH ); @@ -649,7 +669,7 @@ void InitGame( void ) // initialize all entities for this game game.maxentities = maxentities->value; - clamp(game.maxentities, globals.num_edicts, MAX_EDICTS); + clamp(game.maxentities, globals.num_edicts, game.csr.max_edicts); g_edicts = gi.TagMalloc( game.maxentities * sizeof(g_edicts[0]), TAG_GAME ); globals.edicts = g_edicts; globals.max_edicts = game.maxentities; @@ -712,7 +732,7 @@ void InitGame( void ) } #endif - gi.cvar_forceset("g_features", va("%d", G_FEATURES)); + gi.cvar_forceset("g_features", va("%d", features)); gi.cvar_forceset("g_view_predict", "1"); gi.cvar_forceset("g_view_high", va("%d", STANDING_VIEWHEIGHT)); gi.cvar_forceset("g_view_low", va("%d", CROUCHING_VIEWHEIGHT)); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index a16a59d7c..869bc1dc4 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -817,6 +817,43 @@ void G_LoadLocations( void ) gi.dprintf( "Found %d locations.\n", ml_count ); } +static char *const lightstyles[] = { + // 0 normal + "m", + + // 1 FLICKER (first variety) + "mmnmmommommnonmmonqnmmo", + + // 2 SLOW STRONG PULSE + "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba", + + // 3 CANDLE (first variety) + "mmmmmaaaaammmmmaaaaaabcdefgabcdefg", + + // 4 FAST STROBE + "mamamamamama", + + // 5 GENTLE PULSE 1 + "jklmnopqrstuvwxyzyxwvutsrqponmlkj", + + // 6 FLICKER (second variety) + "nmonqnmomnmomomno", + + // 7 CANDLE (second variety) + "mmmaaaabcdefgmmmmaaaammmaamm", + + // 8 CANDLE (third variety) + "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa", + + // 9 SLOW STROBE (fourth variety) + "aaaaaaaazzzzzzzz", + + // 10 FLUORESCENT FLICKER + "mmamammmmammamamaaamammma", + + // 11 SLOW PULSE NOT FADE TO BLACK + "abcdefghijklmnopqrrqponmlkjihgfedcba", +}; /* These are distinct game modes; you cannot have a teamdm tourney mode, for example @@ -1877,25 +1914,15 @@ void SP_worldspawn (edict_t * ent) 3 - using the day_cycle to change the lights every xx seconds as defined by day_cycle */ if (darkmatch->value == 1) - gi.configstring (CS_LIGHTS + 0, "a"); // Pitch Black + gi.configstring (game.csr.lights + 0, "a"); // Pitch Black else if (darkmatch->value == 2) - gi.configstring (CS_LIGHTS + 0, "b"); // Dusk + gi.configstring (game.csr.lights + 0, "b"); // Dusk else - gi.configstring (CS_LIGHTS + 0, "m"); // 0 normal - - gi.configstring(CS_LIGHTS + 1, "mmnmmommommnonmmonqnmmo"); // 1 FLICKER (first variety) - gi.configstring(CS_LIGHTS + 2, "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba"); // 2 SLOW STRONG PULSE - gi.configstring(CS_LIGHTS + 3, "mmmmmaaaaammmmmaaaaaabcdefgabcdefg"); // 3 CANDLE (first variety) - gi.configstring(CS_LIGHTS + 4, "mamamamamama"); // 4 FAST STROBE - gi.configstring(CS_LIGHTS + 5, "jklmnopqrstuvwxyzyxwvutsrqponmlkj"); // 5 GENTLE PULSE 1 - gi.configstring(CS_LIGHTS + 6, "nmonqnmomnmomomno"); // 6 FLICKER (second variety) - gi.configstring(CS_LIGHTS + 7, "mmmaaaabcdefgmmmmaaaammmaamm"); // 7 CANDLE (second variety) - gi.configstring(CS_LIGHTS + 8, "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa"); // 8 CANDLE (third variety) - gi.configstring(CS_LIGHTS + 9, "aaaaaaaazzzzzzzz"); // 9 SLOW STROBE (fourth variety) - gi.configstring(CS_LIGHTS + 10, "mmamammmmammamamaaamammma"); // 10 FLUORESCENT FLICKER - gi.configstring(CS_LIGHTS + 11, "abcdefghijklmnopqrrqponmlkjihgfedcba"); // 11 SLOW PULSE NOT FADE TO BLACK - // styles 32-62 are assigned by the light program for switchable lights - gi.configstring(CS_LIGHTS + 63, "a"); // 63 testing + gi.configstring (game.csr.lights + 0, "m"); // 0 normal + + for (int i = 0; i < q_countof(lightstyles); i++) + gi.configstring(game.csr.lights + i, lightstyles[i]); + gi.configstring(game.csr.lights + 63, "a"); } int LoadFlagsFromFile (const char *mapname) From 1638a7da9fea4f0d4c04ef6ba6cc5d6dc4ea3db3 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Feb 2024 23:39:37 -0500 Subject: [PATCH 006/974] Fixed map loading issue --- src/action/g_save.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/g_save.c b/src/action/g_save.c index 92c2557e4..598671606 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -532,6 +532,7 @@ void InitGame( void ) CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) g_select_empty = gi.cvar( "g_select_empty", "0", CVAR_ARCHIVE ); + g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH); run_pitch = gi.cvar( "run_pitch", "0.002", 0 ); run_roll = gi.cvar( "run_roll", "0.005", 0 ); From 71f29ad4555b766391ff2c7861a80e28aef09899 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 5 Feb 2024 14:26:16 -0500 Subject: [PATCH 007/974] Some cleanup --- inc/shared/shared.h | 17 +++++++++-------- src/action/a_ctf.c | 3 --- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 2dff10a58..b2bb8d2cf 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1335,7 +1335,8 @@ enum { #define DF_WEAPON_RESPAWN BIT(16) // RAFAEL -//#define DF_QUADFIRE_DROP BIT(16) +// Note: DF_QUADFIRE_DROP shares the same bit as DF_WEAPON_RESPAWN, never use simultaneously +#define DF_QUADFIRE_DROP DF_WEAPON_RESPAWN //ROGUE #define DF_NO_MINES BIT(17) @@ -1353,16 +1354,16 @@ enum { #define UF_PLAYERFOV BIT(6) // PaTMaN - Flags for ToGgle -#define TG_LASER BIT(0) -#define TG_SLIPPERS BIT(1) -#define TG_SILENCER BIT(2) -#define TG_VEST BIT(3) -#define TG_KICKABLE BIT(4) +#define TG_LASER BIT(0) +#define TG_SLIPPERS BIT(1) +#define TG_SILENCER BIT(2) +#define TG_VEST BIT(3) +#define TG_KICKABLE BIT(4) #define TG_HELMET BIT(5) -#define TG_HUD_RANGE BIT(7) +#define TG_HUD_RANGE BIT(7) -#define TG_IR BIT(13) +#define TG_IR BIT(13) // Why the gaps, I do not know? /* diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 289220e3b..500f099e1 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -230,8 +230,6 @@ void CTFSetTeamSpawns(int team, char *str) if(team == TEAM2) team_spawn_name = "info_player_team2"; - - /* find and remove all team spawns for this team */ while ((spawn = G_Find(spawn, FOFS(classname), team_spawn_name)) != NULL) { G_FreeEdict (spawn); @@ -1272,7 +1270,6 @@ void CTFCapReward(edict_t * ent) band = ent->client->resp.ctf_capstreak; client = ent->client; - // give initial knife if none if (WPF_ALLOWED(KNIFE_NUM) && ent->client->inventory[ITEM_INDEX(GET_ITEM(KNIFE_NUM))] == 0) ent->client->inventory[ITEM_INDEX(GET_ITEM(KNIFE_NUM))] += 1; From 8f95ae7ff0e3631b1906ffff10d2331c23304be0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 5 Feb 2024 15:22:34 -0500 Subject: [PATCH 008/974] Protocol stuff, still broken, but 36 still works --- src/action/g_local.h | 13 ----------- src/action/g_save.c | 10 ++++----- src/client/parse.c | 51 +++++++++++++++++++++++-------------------- src/server/commands.c | 4 +++- src/server/main.c | 3 ++- src/server/server.h | 2 +- src/server/user.c | 15 +++++++++---- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index ef303aeb5..799000ae2 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2531,16 +2531,3 @@ extern Message *timedMessages; void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); void FireTimedMessages(void); - -/* -===================================================================== - CONFIG STRING REMAPPING -===================================================================== -*/ - -#if USE_PROTOCOL_EXTENSIONS - -extern const cs_remap_t cs_remap_old; -extern const cs_remap_t cs_remap_new; - -#endif \ No newline at end of file diff --git a/src/action/g_save.c b/src/action/g_save.c index 598671606..d3dfa1d70 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -669,11 +669,11 @@ void InitGame( void ) globals.num_edicts = game.maxclients + 1; // initialize all entities for this game - game.maxentities = maxentities->value; - clamp(game.maxentities, globals.num_edicts, game.csr.max_edicts); - g_edicts = gi.TagMalloc( game.maxentities * sizeof(g_edicts[0]), TAG_GAME ); - globals.edicts = g_edicts; - globals.max_edicts = game.maxentities; + game.maxentities = maxentities->value; + clamp(game.maxentities, (int)maxclients->value + 1, game.csr.max_edicts); + g_edicts = gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME); + globals.edicts = g_edicts; + globals.max_edicts = game.maxentities; CTFInit(); diff --git a/src/client/parse.c b/src/client/parse.c index f6070ce5d..d3cc50081 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -656,31 +656,34 @@ static void CL_ParseServerData(void) Com_DPrintf("AQTION server state %d\n", i); cl.serverstate = i; - i = MSG_ReadByte(); - if (i) { - Com_DPrintf("AQTION strafejump hack enabled\n"); - cl.pmp.strafehack = true; - } - i = MSG_ReadByte(); //atu QWMod - if (i) { - Com_DPrintf("AQTION QW mode enabled\n"); - PmoveEnableQW(&cl.pmp); - } - cl.esFlags |= MSG_ES_UMASK; - cl.esFlags |= MSG_ES_LONGSOLID; - cl.esFlags |= MSG_ES_BEAMORIGIN; - cl.esFlags |= MSG_ES_SHORTANGLES; - - // waterjump hack - i = MSG_ReadByte(); - if (i) { - Com_DPrintf("AQTION waterjump hack enabled\n"); - cl.pmp.waterhack = true; - } + i = MSG_ReadWord(); + if (i & Q2PRO_PF_STRAFEJUMP_HACK) { + Com_DPrintf("Q2PRO strafejump hack enabled\n"); + cl.pmp.strafehack = true; + } + if (i & Q2PRO_PF_QW_MODE) { + Com_DPrintf("Q2PRO QW mode enabled\n"); + PmoveEnableQW(&cl.pmp); + } + if (i & Q2PRO_PF_WATERJUMP_HACK) { + Com_DPrintf("Q2PRO waterjump hack enabled\n"); + cl.pmp.waterhack = true; + } + if (i & Q2PRO_PF_EXTENSIONS) { + Com_DPrintf("Q2PRO protocol extensions enabled\n"); + cl.csr = cs_remap_new; + } - cl.pmp.speedmult = 2; - cl.pmp.flyhack = true; // fly hack is unconditionally enabled - cl.pmp.flyfriction = 4; + cl.esFlags |= MSG_ES_UMASK | MSG_ES_LONGSOLID; + if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN) { + cl.esFlags |= MSG_ES_BEAMORIGIN; + } + if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES) { + cl.esFlags |= MSG_ES_SHORTANGLES; + } + cl.pmp.speedmult = 2; + cl.pmp.flyhack = true; // fly hack is unconditionally enabled + cl.pmp.flyfriction = 4; } else if (cls.serverProtocol == PROTOCOL_VERSION_Q2PRO) { i = MSG_ReadWord(); if (!Q2PRO_SUPPORTED(i)) { diff --git a/src/server/commands.c b/src/server/commands.c index 334daabfe..1aef4c5ce 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -1741,7 +1741,9 @@ void SV_ListSounds_f(void) Com_Printf("-- List of Loaded Sounds:\n"); for (i = 1; i < MAX_SOUNDS; i++) { string = sv.configstrings[CS_SOUNDS + i]; - Com_Printf("%i: %s\n", i, string); + if (string && string[0] != '\0') { // Check if string is not null and not empty + Com_Printf("%i: %s\n", i, string); + } } } diff --git a/src/server/main.c b/src/server/main.c index 3ba61a78d..25e2a8609 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -840,7 +840,8 @@ static bool parse_enhanced_params(conn_params_t *p) if (!CLIENT_COMPATIBLE(&svs.csr, p)) { return reject("This is a protocol limit removing enhanced server.\n" "Your client version is not compatible. Make sure you are " - "running latest Q2PRO client version.\n"); + "running latest Q2PRO client version.\nYour major protocol version: %d\n " + "Your minor protocol version: %d\n", p->protocol, p->version); } return true; diff --git a/src/server/server.h b/src/server/server.h index ed3f90b45..b218ce880 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -190,7 +190,7 @@ typedef struct { sv.state == ss_game && \ EDICT_POOL(c, e)->solid == SOLID_BSP) #define CLIENT_COMPATIBLE(csr, c) \ - (!(csr)->extended || ((c)->protocol == PROTOCOL_VERSION_Q2PRO && \ + (!(csr)->extended || (((c)->protocol == PROTOCOL_VERSION_Q2PRO || (c)->protocol == PROTOCOL_VERSION_AQTION) && \ (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS)) #define ENT_EXTENSION(csr, ent) ((csr)->extended ? &(ent)->x : NULL) diff --git a/src/server/user.c b/src/server/user.c index af30cf43f..485c19f6e 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -420,10 +420,17 @@ void SV_New_f(void) break; case PROTOCOL_VERSION_AQTION: MSG_WriteShort(sv_client->version); - MSG_WriteByte(sv.state); - MSG_WriteByte(sv_client->pmp.strafehack); - MSG_WriteByte(sv_client->pmp.qwmode); - MSG_WriteByte(sv_client->pmp.waterhack); + if (sv.state == ss_cinematic && sv_client->version < PROTOCOL_VERSION_Q2PRO_CINEMATICS) + MSG_WriteByte(ss_pic); + else + MSG_WriteByte(sv.state); + if (sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) { + MSG_WriteShort(q2pro_protocol_flags()); + } else { + MSG_WriteByte(sv_client->pmp.strafehack); + MSG_WriteByte(sv_client->pmp.qwmode); + MSG_WriteByte(sv_client->pmp.waterhack); + } break; } From e302fdc98e227e4c9b3bddef8ab11f0642928b9c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 5 Feb 2024 16:15:12 -0500 Subject: [PATCH 009/974] De-constified a bunch of stuff, what a hassle --- src/action/a_cmds.c | 4 +++- src/action/a_team.c | 5 ++++- src/action/acesrc/acebot_spawn.c | 4 +++- src/action/cgf_sfx_glass.c | 8 ++++++-- src/action/g_combat.c | 15 ++++++++++----- src/action/g_ext.c | 2 +- src/action/g_grapple.c | 7 +++++-- src/action/g_local.h | 2 +- src/action/g_main.c | 2 +- src/action/g_misc.c | 7 +++++-- src/action/g_spawn.c | 2 +- src/action/p_client.c | 4 +++- src/action/p_weapon.c | 8 ++++++-- src/client/demo.c | 10 ++++++++++ src/client/parse.c | 2 +- 15 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index a049b84e2..e556b14e3 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -850,7 +850,9 @@ void Cmd_Bandage_f(edict_t *ent) if(ent->client->quad_framenum > level.framenum) damage *= 1.5f; - fire_grenade2(ent, ent->s.origin, vec3_origin, damage, 0, 2 * HZ, damage * 2, false); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + fire_grenade2(ent, ent->s.origin, non_const_origin, damage, 0, 2 * HZ, damage * 2, false); INV_AMMO(ent, GRENADE_NUM)--; if (INV_AMMO(ent, GRENADE_NUM) <= 0) { diff --git a/src/action/a_team.c b/src/action/a_team.c index 0cd2244af..8bc72a42f 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1208,7 +1208,10 @@ void killPlayer( edict_t *ent, qboolean suicidePunish ) ent->flags &= ~FL_GODMODE; ent->health = 0; meansOfDeath = MOD_SUICIDE; - player_die(ent, ent, ent, damage, vec3_origin); + + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + player_die(ent, ent, ent, damage, non_const_origin); ent->deadflag = DEAD_DEAD; } diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 4175d9aa9..3c1fde943 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -691,7 +691,9 @@ void ACESP_RemoveBot(char *name) if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) { bot->health = 0; - player_die (bot, bot, bot, 100000, vec3_origin); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + player_die (bot, bot, bot, 100000, non_const_origin); // don't even bother waiting for death frames // bot->deadflag = DEAD_DEAD; // bot->inuse = false; diff --git a/src/action/cgf_sfx_glass.c b/src/action/cgf_sfx_glass.c index 23ca4490a..6ef92a632 100644 --- a/src/action/cgf_sfx_glass.c +++ b/src/action/cgf_sfx_glass.c @@ -279,8 +279,10 @@ CGF_SFX_ShootBreakableGlass (edict_t * aGlassPane, edict_t * anAttacker, if (destruct) { // break glass (and hurt if doing kick) + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); CGF_SFX_BreakGlass (aGlassPane, anAttacker, 0, aGlassPane->health, - vec3_origin, FRAMETIME); + non_const_origin, FRAMETIME); if (mod == MOD_KICK) { vec3_t bloodorigin; @@ -421,7 +423,9 @@ CGF_SFX_TouchGlass (edict_t * self, edict_t * other, cplane_t * plane, goto knife_and_grenade_handling; // break glass - CGF_SFX_BreakGlass (glass, other, other, glass->health, vec3_origin, + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + CGF_SFX_BreakGlass (glass, other, other, glass->health, non_const_origin, 3.0f * FRAMETIME); // glass can take care of itself, but the trigger isn't needed anymore G_FreeEdict (self); diff --git a/src/action/g_combat.c b/src/action/g_combat.c index cd0c2737e..01677de54 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -202,8 +202,7 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) Killed ============ */ -void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, int damage, - const vec3_t point) +void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, int damage, vec3_t point) { if (targ->health < -999) targ->health = -999; @@ -507,8 +506,12 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if (from_top < 2 * HEAD_HEIGHT) { - vec3_t new_point; - VerifyHeadShot(point, dir, HEAD_HEIGHT, new_point); + vec3_t new_point, deconst_point, deconst_dir; + + //De-constify the point and dir + VectorCopy(point, deconst_point); + VectorCopy(dir, deconst_dir); + VerifyHeadShot(deconst_point, deconst_dir, HEAD_HEIGHT, new_point); VectorSubtract(new_point, targ->s.origin, new_point); //gi.cprintf(attacker, PRINT_HIGH, "z: %d y: %d x: %d\n", (int)(targ_maxs2 - new_point[2]),(int)(new_point[1]) , (int)(new_point[0]) ); @@ -843,7 +846,9 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if ((targ->svflags & SVF_MONSTER) || client) targ->flags |= FL_NO_KNOCKBACK; - Killed(targ, inflictor, attacker, take, point); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + Killed(targ, inflictor, attacker, take, non_const_origin); return; } } diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 1af961076..2e9c1c686 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -53,7 +53,7 @@ void(*engine_CvarSync_Set)(int index, const char *name, const char *val); // // optional new entrypoints the engine may want to call edict_t *xerp_ent; -trace_t q_gameabi XERP_trace(vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end) +trace_t q_gameabi XERP_trace(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end) { return gi.trace(start, mins, maxs, end, xerp_ent, MASK_PLAYERSOLID); } diff --git a/src/action/g_grapple.c b/src/action/g_grapple.c index a4c7a81b5..58a6630a8 100644 --- a/src/action/g_grapple.c +++ b/src/action/g_grapple.c @@ -310,9 +310,12 @@ void CTFGrappleFire (edict_t *ent, vec3_t g_offset, int damage, int effect) void CTFWeapon_Grapple_Fire (edict_t *ent) { int damage; - + vec3_t non_const_origin; damage = 10; - CTFGrappleFire (ent, vec3_origin, damage, 0); + + // Copy const to non-const + VectorCopy(vec3_origin, non_const_origin); + CTFGrappleFire (ent, non_const_origin, damage, 0); ent->client->ps.gunframe++; } diff --git a/src/action/g_local.h b/src/action/g_local.h index 799000ae2..b5fa02abe 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2392,7 +2392,7 @@ void A_ScoreboardMessage( edict_t * ent, edict_t * killer ); //local to g_combat but needed in p_view void SpawnDamage (int type, const vec3_t origin, const vec3_t normal, int damage); void Killed (edict_t * targ, edict_t * inflictor, edict_t * attacker, - int damage, const vec3_t point); + int damage, vec3_t point); void Add_Frag(edict_t * ent, int mod); void Subtract_Frag (edict_t * ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index 8da807901..0808d9c00 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -540,7 +540,7 @@ cvar_t *cl_discord_discriminator; cvar_t *cl_discord_username; cvar_t *cl_discord_avatar; -void SpawnEntities (char *mapname, char *entities, char *spawnpoint); +void SpawnEntities (const char *mapname, const char *entities, const char *spawnpoint); void ClientThink (edict_t * ent, usercmd_t * cmd); qboolean ClientConnect (edict_t * ent, char *userinfo); void ClientUserinfoChanged (edict_t * ent, char *userinfo); diff --git a/src/action/g_misc.c b/src/action/g_misc.c index debb6d605..c05887af6 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -730,7 +730,7 @@ one small chunk per 25 of mass (up to 16). So 800 gives the most. */ void func_explosive_explode (edict_t * self, edict_t * inflictor, - edict_t * attacker, int damage, const vec3_t point) + edict_t * attacker, int damage, vec3_t point) { vec3_t origin; vec3_t chunkorigin; @@ -799,7 +799,10 @@ func_explosive_explode (edict_t * self, edict_t * inflictor, void func_explosive_use (edict_t * self, edict_t * other, edict_t * activator) { - func_explosive_explode (self, self, other, self->health, vec3_origin); + vec3_t origin; + VectorCopy(vec3_origin, origin); + // This needs to be non-const + func_explosive_explode (self, self, other, self->health, origin); } void diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 869bc1dc4..18ee1e71b 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -913,7 +913,7 @@ Creates a server's entity / program execution context by parsing textual entity definitions out of an ent file. ============== */ -void SpawnEntities (char *mapname, const char *entities, const char *spawnpoint) +void SpawnEntities (const char *mapname, const char *entities, const char *spawnpoint) { edict_t *ent = NULL; gclient_t *client; diff --git a/src/action/p_client.c b/src/action/p_client.c index 4d7624452..795bd5533 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1561,7 +1561,9 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage // Reset Grenade Damage to 1.52 when requested: int damrad = use_classic->value ? GRENADE_DAMRAD_CLASSIC : GRENADE_DAMRAD; self->client->ps.gunframe = 0; - fire_grenade2( self, self->s.origin, vec3_origin, damrad, 0, 2 * HZ, damrad * 2, false ); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + fire_grenade2( self, self->s.origin, non_const_origin, damrad, 0, 2 * HZ, damrad * 2, false ); } mod = meansOfDeath & ~MOD_FRIENDLY_FIRE; diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index ed53484ed..698a08d91 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -929,7 +929,9 @@ void Drop_Weapon(edict_t* ent, gitem_t* item) if (ent->client->quad_framenum > level.framenum) damage *= 1.5f; - fire_grenade2(ent, ent->s.origin, vec3_origin, damage, 0, 2 * HZ, damage * 2, false); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + fire_grenade2(ent, ent->s.origin, non_const_origin, damage, 0, 2 * HZ, damage * 2, false); INV_AMMO(ent, GRENADE_NUM)--; ent->client->newweapon = GET_ITEM(MK23_NUM); @@ -3748,7 +3750,9 @@ void Weapon_Gas(edict_t* ent) if (is_quad) damage *= 1.5f; - fire_grenade2(ent, ent->s.origin, vec3_origin, damage, 0, 2 * HZ, damage * 2, false); + vec3_t non_const_origin; // Convert to non-const + VectorCopy(vec3_origin, non_const_origin); + fire_grenade2(ent, ent->s.origin, non_const_origin, damage, 0, 2 * HZ, damage * 2, false); INV_AMMO(ent, GRENADE_NUM)--; if (INV_AMMO(ent, GRENADE_NUM) <= 0) diff --git a/src/client/demo.c b/src/client/demo.c index 8a704e839..996f30e44 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -416,8 +416,13 @@ static void CL_Record_f(void) // send the serverdata MSG_WriteByte(svc_serverdata); + #if USE_AQTION + if (cl.csr.extended) + MSG_WriteLong(PROTOCOL_VERSION_AQTION); + #else if (cl.csr.extended) MSG_WriteLong(PROTOCOL_VERSION_EXTENDED); + #endif else MSG_WriteLong(min(cls.serverProtocol, PROTOCOL_VERSION_DEFAULT)); MSG_WriteLong(cl.servercount); @@ -1181,8 +1186,13 @@ demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info) goto fail; } c = MSG_ReadLong(); + #if USE_AQTION + if (c == PROTOCOL_VERSION_AQTION) { + csr = &cs_remap_new; + #else if (c == PROTOCOL_VERSION_EXTENDED) { csr = &cs_remap_new; + #endif } else if (c < PROTOCOL_VERSION_OLD || c > PROTOCOL_VERSION_DEFAULT) { goto fail; } diff --git a/src/client/parse.c b/src/client/parse.c index d3cc50081..c63d8ef82 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -566,7 +566,7 @@ static void CL_ParseServerData(void) cls.serverProtocol, protocol); } // BIG HACK to let demos from release work with the 3.0x patch!!! - if (protocol == PROTOCOL_VERSION_EXTENDED) { + if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_AQTION) { cl.csr = cs_remap_new; protocol = PROTOCOL_VERSION_DEFAULT; } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_AQTION) { From 1d6306281c2c4e3b51466607ba0d5baf8eae7415 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 6 Feb 2024 16:30:06 -0500 Subject: [PATCH 010/974] Synced with latest skuller master --- src/action/g_local.h | 11 ++++++++--- src/action/g_save.c | 17 ++++++++--------- src/action/p_view.c | 6 +++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index b5fa02abe..1f83238c5 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -727,6 +727,10 @@ typedef struct gitem_s } gitem_t; +typedef struct precache_s { + struct precache_s *next; + void (*func)(void); +} precache_t; // // this structure is left intact through an entire game @@ -755,9 +759,6 @@ typedef struct // items int num_items; - - //q2pro protocol extensions - cs_remap_t csr; // stats char matchid[MAX_QPATH]; @@ -766,6 +767,10 @@ typedef struct int roundNum; qboolean ai_ent_found; int bot_count; + + //q2pro protocol extensions + cs_remap_t csr; + precache_t *precaches; } game_locals_t; diff --git a/src/action/g_save.c b/src/action/g_save.c index d3dfa1d70..3c7e1e94b 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -663,18 +663,17 @@ void InitGame( void ) // items InitItems(); - // initialize all clients for this game - game.maxclients = (int)maxclients->value; - game.clients = gi.TagMalloc( game.maxclients * sizeof(game.clients[0]), TAG_GAME ); - globals.num_edicts = game.maxclients + 1; - - // initialize all entities for this game - game.maxentities = maxentities->value; - clamp(game.maxentities, (int)maxclients->value + 1, game.csr.max_edicts); + // initialize all entities for this game + game.maxentities = Q_clip(maxentities->value, (int)maxclients->value + 1, game.csr.max_edicts); g_edicts = gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME); globals.edicts = g_edicts; globals.max_edicts = game.maxentities; + // initialize all clients for this game + game.maxclients = maxclients->value; + game.clients = gi.TagMalloc(game.maxclients * sizeof(game.clients[0]), TAG_GAME); + globals.num_edicts = game.maxclients + 1; + CTFInit(); //PG BUND - must be at end of gameinit: @@ -712,7 +711,7 @@ void InitGame( void ) { int framediv = (int) cv->value / BASE_FRAMERATE; - clamp(framediv, 1, MAX_FRAMEDIV); + Q_clip(framediv, 1, MAX_FRAMEDIV); game.framerate = framediv * BASE_FRAMERATE; game.frametime = BASE_FRAMETIME_1000 / framediv; diff --git a/src/action/p_view.c b/src/action/p_view.c index 985c774f2..674edd577 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -370,9 +370,9 @@ void SV_CalcViewOffset (edict_t * ent) // absolutely bound offsets // so the view can never be outside the player box - clamp(v[0], -14, 14); - clamp(v[1], -14, 14); - clamp(v[2], -22, 30); + ent->client->ps.viewoffset[0] = Q_clipf(v[0], -14, 14); + ent->client->ps.viewoffset[1] = Q_clipf(v[1], -14, 14); + ent->client->ps.viewoffset[2] = Q_clipf(v[2], -22, 30); VectorCopy (v, ent->client->ps.viewoffset); } From 9861270b4093a6aacf6fcb80a8edd58d840a8aed Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Tue, 6 Feb 2024 18:40:31 -0500 Subject: [PATCH 011/974] Protocol 38.. works again..? --- inc/common/protocol.h | 6 +++--- src/client/parse.c | 4 ++-- src/common/msg.c | 38 +++++++++++++++++----------------- src/server/entities.c | 47 +++++++++++++++++++++++++------------------ src/server/main.c | 17 ++++++++++------ 5 files changed, 61 insertions(+), 51 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 27db7bc43..4850471b3 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -271,9 +271,9 @@ typedef enum { #define EPS_CLIENTNUM BIT(6) // aqtion protocol specific flags -#define AQPS_PMFLAGS (1<<0) -#define AQPS_TIMESTAMP (1<<1) -#define AQPS_LEGHITS (1<<2) +#define AQPS_PMFLAGS BIT(0) +#define AQPS_TIMESTAMP BIT(1) +#define AQPS_LEGHITS BIT(2) //============================================== diff --git a/src/client/parse.c b/src/client/parse.c index 4a2b7e25b..a74cb2f8b 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -329,7 +329,7 @@ static void CL_ParseFrame(int extrabits) frame.areabytes = 0; } - if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol == PROTOCOL_VERSION_AQTION) { + if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { if (MSG_ReadByte() != svc_playerinfo) { Com_Error(ERR_DROP, "%s: not playerinfo", __func__); } @@ -393,7 +393,7 @@ static void CL_ParseFrame(int extrabits) } // parse packetentities - if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol == PROTOCOL_VERSION_AQTION) { + if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { if (MSG_ReadByte() != svc_packetentities) { Com_Error(ERR_DROP, "%s: not packetentities", __func__); } diff --git a/src/common/msg.c b/src/common/msg.c index 29c6d2a90..be0d3d311 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -1211,8 +1211,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, int pflags, eflags, aqtflags; int statbits; - if (!to) - Com_Error(ERR_DROP, "%s: NULL", __func__); + Q_assert(to); if (!from) from = &nullPlayerState; @@ -1250,8 +1249,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, if (to->pmove.gravity != from->pmove.gravity) pflags |= PS_M_GRAVITY; - } - else { + } else { // save previous state VectorCopy(from->pmove.velocity, to->pmove.velocity); to->pmove.pm_time = from->pmove.pm_time; @@ -1262,8 +1260,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, if (!(flags & MSG_PS_IGNORE_DELTAANGLES)) { if (!VectorCompare(from->pmove.delta_angles, to->pmove.delta_angles)) pflags |= PS_M_DELTA_ANGLES; - } - else { + } else { // save previous state VectorCopy(from->pmove.delta_angles, to->pmove.delta_angles); } @@ -1305,8 +1302,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, if (!(flags & MSG_PS_IGNORE_GUNINDEX)) { if (to->gunindex != from->gunindex) pflags |= PS_WEAPONINDEX; - } - else { + } else { // save previous state to->gunindex = from->gunindex; } @@ -1320,8 +1316,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, if (!VectorCompare(from->gunangles, to->gunangles)) eflags |= EPS_GUNANGLES; - } - else { + } else { // save previous state to->gunframe = from->gunframe; VectorCopy(from->gunoffset, to->gunoffset); @@ -1432,8 +1427,12 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, MSG_WriteChar(to->kick_angles[2]); } - if (pflags & PS_WEAPONINDEX) - MSG_WriteByte(to->gunindex); + if (pflags & PS_WEAPONINDEX) { + if (flags & MSG_PS_EXTENSIONS) + MSG_WriteShort(to->gunindex); + else + MSG_WriteByte(to->gunindex); + } if (pflags & PS_WEAPONFRAME) MSG_WriteByte(to->gunframe); @@ -2614,15 +2613,12 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, int statbits; int aqtflags; - if (!to) { - Com_Error(ERR_DROP, "%s: NULL", __func__); - } + Q_assert(to); // clear to old value before delta parsing if (!from) { memset(to, 0, sizeof(*to)); - } - else if (to != from) { + } else if (to != from) { memcpy(to, from, sizeof(*to)); } @@ -2669,7 +2665,6 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, to->pmove.delta_angles[2] = MSG_ReadShort(); } - // // parse the aqtion extensions // @@ -2724,8 +2719,11 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, } if (flags & PS_WEAPONINDEX) { - to->gunindex = MSG_ReadByte(); - } + if (psflags & MSG_PS_EXTENSIONS) + to->gunindex = MSG_ReadWord(); + else + to->gunindex = MSG_ReadByte(); + } if (flags & PS_WEAPONFRAME) { to->gunframe = MSG_ReadByte(); diff --git a/src/server/entities.c b/src/server/entities.c index 9cdbbddba..a6a4f8d37 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -393,8 +393,7 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) if (oldframe) { oldstate = &oldframe->ps; delta = client->framenum - client->lastframe; - } - else { + } else { oldstate = NULL; delta = 31; } @@ -435,26 +434,35 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) } clientEntityNum = 0; - if (frame->ps.pmove.pm_type < PM_DEAD && !client->settings[CLS_RECORDING]) { - clientEntityNum = frame->clientNum + 1; - } - if (client->settings[CLS_NOPREDICT]) { - psFlags |= MSG_PS_IGNORE_PREDICTION; - } - suppressed = client->frameflags; - + if (client->protocol == PROTOCOL_VERSION_AQTION) { + if (frame->ps.pmove.pm_type < PM_DEAD && !client->settings[CLS_RECORDING]) { + clientEntityNum = frame->clientNum + 1; + } + if (client->settings[CLS_NOPREDICT]) { + psFlags |= MSG_PS_IGNORE_PREDICTION; + } + suppressed = client->frameflags; + } else { + suppressed = client->suppress_count; + } + if (client->csr->extended) { + psFlags |= MSG_PS_EXTENSIONS; + } - // delta encode the playerstate - MSG_WriteByte(svc_playerinfo); + // delta encode the playerstate extraflags = MSG_WriteDeltaPlayerstate_Aqtion(oldstate, &frame->ps, psFlags); - - // delta encode the clientNum - int clientNum = oldframe ? oldframe->clientNum : 0; - if (clientNum != frame->clientNum) { - extraflags |= EPS_CLIENTNUM; - MSG_WriteByte(frame->clientNum); - } + if (client->protocol == PROTOCOL_VERSION_AQTION) { + // delta encode the clientNum + if ((oldframe ? oldframe->clientNum : 0) != frame->clientNum) { + extraflags |= EPS_CLIENTNUM; + if (client->version < PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT) { + MSG_WriteByte(frame->clientNum); + } else { + MSG_WriteShort(frame->clientNum); + } + } + } // save 3 high bits of extraflags *b1 = svc_frame | (((extraflags & 0x70) << 1)); @@ -467,7 +475,6 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) client->frameflags = 0; // delta encode the entities - MSG_WriteByte(svc_packetentities); SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum); #ifdef AQTION_EXTENSION diff --git a/src/server/main.c b/src/server/main.c index 393c40612..3f2fa88f5 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1046,12 +1046,17 @@ static void init_pmove_and_es_flags(client_t *newcl) } newcl->pmp.flyhack = true; newcl->pmp.flyfriction = 4; - newcl->esFlags |= MSG_ES_UMASK; - newcl->esFlags |= MSG_ES_LONGSOLID; - newcl->esFlags |= MSG_ES_BEAMORIGIN; - if (newcl->version >= PROTOCOL_VERSION_Q2PRO_MINIMUM) { - force = 1; - } + newcl->esFlags |= MSG_ES_UMASK | MSG_ES_LONGSOLID; + if (newcl->version >= PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN) { + newcl->esFlags |= MSG_ES_BEAMORIGIN; + } + if (newcl->version >= PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES) { + newcl->esFlags |= MSG_ES_SHORTANGLES; + } + if (svs.csr.extended) { + newcl->esFlags |= MSG_ES_EXTENSIONS; + } + force = 1; } newcl->pmp.waterhack = sv_waterjump_hack->integer >= force; } From aa63de3ad934bcc4807051a07b4e7e3eb5ead22b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 7 Feb 2024 17:53:46 -0500 Subject: [PATCH 012/974] Fixed some things --- src/action/a_team.c | 6 +++--- src/action/g_local.h | 20 ++++---------------- src/action/g_save.c | 22 +++++++++------------- src/action/p_client.c | 4 ++-- src/client/parse.c | 2 +- 5 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 8bc72a42f..0bc1eb089 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1519,7 +1519,7 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) AddToTransparentList (ent); } - #if USE_AQTION + #ifdef USE_AQTION if (in_warmup && warmup_bots->value) { PutClientInServer (ent); } @@ -2316,7 +2316,7 @@ void RunWarmup (void) gi.centerprintf(ent, "WARMUP"); } } - #if USE_AQTION + #ifdef USE_AQTION if (warmup_bots->value){ gi.cvar_forceset("am", "1"); gi.cvar_forceset("am_botcount", warmup_bots->string); @@ -2802,7 +2802,7 @@ int CheckTeamRules (void) gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); - #if USE_AQTION + #ifdef USE_AQTION // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ gi.cvar_forceset("am", "0"); diff --git a/src/action/g_local.h b/src/action/g_local.h index 1f83238c5..b5a547acc 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -299,19 +299,7 @@ #define getEnt(entnum) (edict_t *)((char *)globals.edicts + (globals.edict_size * entnum)) //AQ:TNG Slicer - This was missing #define GAMEVERSION "action" // the "gameversion" client command will print this plus compile date -#ifndef NO_FPS -#define G_GMF_VARIABLE_FPS GMF_VARIABLE_FPS -#else -#define G_GMF_VARIABLE_FPS 0 -#endif - -#if USE_PROTOCOL_EXTENSIONS -#define G_GMF_PROTOCOL_EXTENSIONS GMF_PROTOCOL_EXTENSIONS -#else -#define G_GMF_PROTOCOL_EXTENSIONS 0 -#endif - -#define G_FEATURES (GMF_CLIENTNUM | GMF_PROPERINUSE | GMF_MVDSPEC | GMF_WANT_ALL_DISCONNECTS | G_GMF_VARIABLE_FPS | G_GMF_PROTOCOL_EXTENSIONS) +#define G_FEATURES (GMF_CLIENTNUM | GMF_PROPERINUSE | GMF_MVDSPEC | GMF_WANT_ALL_DISCONNECTS | GMF_VARIABLE_FPS) // protocol bytes that can be directly added to messages #define svc_muzzleflash 1 @@ -1637,7 +1625,7 @@ void ProduceShotgunDamageReport(edict_t*); //tng_stats.c void StatBotCheck(void); -#if USE_AQTION +#ifdef USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); void LogCapture(edict_t *capturer); @@ -1739,7 +1727,7 @@ typedef struct ignorelist_t ignorelist; gitem_t *chosenItem2; // Support for item kit mode - #if USE_AQTION + #ifdef USE_AQTION char steamid[24]; char discordid[24]; #endif @@ -2331,7 +2319,7 @@ typedef struct gitem_t *weapon; gitem_t *item; // Extended stats - #if USE_AQTION + #ifdef USE_AQTION char steamid[24]; char discordid[24]; #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 3c7e1e94b..6b55a00c3 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -554,13 +554,20 @@ void InitGame( void ) if (game.serverfeatures & GMF_PROTOCOL_EXTENSIONS && (int)g_protocol_extensions->value) { features |= GMF_PROTOCOL_EXTENSIONS; - gi.dprintf( "...server supports GMF_PROTOCOL_EXTENSIONS\n" ); + gi.dprintf("...server supports GMF_PROTOCOL_EXTENSIONS\n ...and protocol extensions are enabled\n"); game.csr = cs_remap_new; } else { + if (game.serverfeatures & GMF_PROTOCOL_EXTENSIONS) { + gi.dprintf("...server supports GMF_PROTOCOL_EXTENSIONS\n ...but protocol extensions are disabled\n"); + } else { + gi.dprintf("...server does not support GMF_PROTOCOL_EXTENSIONS\n"); + } + if ((int)g_protocol_extensions->value) { + gi.dprintf("...g_protocol_extensions is enabled\n ...but server does not support GMF_PROTOCOL_EXTENSIONS\n"); + } game.csr = cs_remap_old; } } - jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO|*/ CVAR_LATCH); // jumping mod -- removed from serverinfo 2022 warmup = gi.cvar( "warmup", "0", CVAR_LATCH ); @@ -678,17 +685,6 @@ void InitGame( void ) //PG BUND - must be at end of gameinit: vInitGame(); - - gi.dprintf( "Reading extra server features\n" ); - cv = gi.cvar( "sv_features", NULL, 0 ); - if (cv) { - game.serverfeatures = (int)cv->value; - - if (game.serverfeatures & GMF_CLIENTNUM) { - gi.dprintf( "...server supports GMF_CLIENTNUM\n" ); - } - } - // setup framerate parameters game.framerate = BASE_FRAMERATE; diff --git a/src/action/p_client.c b/src/action/p_client.c index 795bd5533..85654bb60 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3254,7 +3254,7 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) Q_strncpyz(ent->client->pers.ip, ipaddr_buf, sizeof(ent->client->pers.ip)); Q_strncpyz(ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo)); - #if USE_AQTION + #ifdef USE_AQTION value = Info_ValueForKey(userinfo, "steamid"); if (*value) Q_strncpyz(ent->client->pers.steamid, value, sizeof(ent->client->pers.steamid)); @@ -3424,7 +3424,7 @@ void CreateGhost(edict_t * ent) strcpy(ghost->ip, ent->client->pers.ip); strcpy(ghost->netname, ent->client->pers.netname); - #if USE_AQTION + #ifdef USE_AQTION strcpy(ghost->steamid, ent->client->pers.steamid); strcpy(ghost->discordid, ent->client->pers.discordid); #endif diff --git a/src/client/parse.c b/src/client/parse.c index a74cb2f8b..8d9948efb 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -331,7 +331,7 @@ static void CL_ParseFrame(int extrabits) if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { if (MSG_ReadByte() != svc_playerinfo) { - Com_Error(ERR_DROP, "%s: not playerinfo", __func__); + Com_Error(ERR_DROP, "%s: not playerinfo, value: %i", __func__, MSG_ReadByte()); } } From 043f63bd6caa897e86e1326a1b36d62382964749 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 7 Feb 2024 19:48:56 -0500 Subject: [PATCH 013/974] My latest attempt to figure out why the gamelib is sending large indexes --- src/action/a_esp.c | 8 +++++++- src/action/a_vote.c | 9 ++++++++- src/action/acesrc/acebot.h | 2 +- src/action/acesrc/acebot_items.c | 2 +- src/action/acesrc/acebot_nodes.c | 2 +- src/action/g_cmds.c | 9 ++++++++- src/action/g_items.c | 4 ++-- src/action/g_local.h | 2 +- src/action/g_main.c | 11 ++++++++--- src/action/g_misc.c | 26 -------------------------- src/action/g_spawn.c | 2 +- src/action/g_utils.c | 8 ++++---- src/action/p_hud.c | 18 ++++++++++++++---- src/action/tng_stats.c | 18 ++++++++++++++++-- src/client/screen.c | 2 +- src/server/game.c | 1 + 16 files changed, 74 insertions(+), 50 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index d7574ebbf..65786d1f5 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1626,7 +1626,12 @@ and there are no active volunteers for that team qboolean EspChooseRandomLeader(int teamNum) { int players[TEAM_TOP] = { 0 }, i, numPlayers = 0; - edict_t *ent, *playerList[MAX_CLIENTS]; + edict_t *ent; + edict_t **playerList = malloc(game.csr.maxclients * sizeof(edict_t*)); + if (playerList == NULL) { + gi.dprintf("%s: Could not allocate memory for playerList\n", __FUNCTION__); + return false; + } if (team_round_going) { if (esp_debug->value) @@ -1664,6 +1669,7 @@ qboolean EspChooseRandomLeader(int teamNum) EspSetLeader(teamNum, ent); return true; } // If we get this far, that the only players here are bots and non-volunteers. Halt the game + free(playerList); return false; } diff --git a/src/action/a_vote.c b/src/action/a_vote.c index d08a3fafe..ad98caff6 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -2016,7 +2016,12 @@ cvar_t *_InitScrambleVote (ini_t * ini) qboolean ScrambleTeams(void) { int i, j, numplayers, newteam; - edict_t *ent, *players[MAX_CLIENTS], *oldCaptains[TEAM_TOP] = {NULL}, *oldLeaders[TEAM_TOP] = {NULL}; + edict_t *ent, *oldCaptains[TEAM_TOP] = {NULL}, *oldLeaders[TEAM_TOP] = {NULL}; + edict_t **players = malloc(game.csr.maxclients * sizeof(edict_t*)); + if (players == NULL) { + gi.dprintf("%s: Could not allocate memory for players\n", __FUNCTION__); + return false; + } numplayers = 0; for (i = 0, ent = &g_edicts[1]; i < game.maxclients; i++, ent++) @@ -2028,6 +2033,7 @@ qboolean ScrambleTeams(void) } if (numplayers <= teamCount) + free(players); return false; for (i = numplayers - 1; i > 0; i--) { @@ -2080,6 +2086,7 @@ qboolean ScrambleTeams(void) ent->client->resp.scramblevote = 0; } + free(players); return true; } diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 11dd82bd2..cab9fb76f 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -203,7 +203,7 @@ extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game // extern decs extern node_t nodes[MAX_NODES]; -extern item_table_t item_table[MAX_EDICTS]; +extern item_table_t item_table[MAX_EDICTS_OLD]; extern qboolean debug_mode; extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" extern int numnodes; diff --git a/src/action/acesrc/acebot_items.c b/src/action/acesrc/acebot_items.c index a633ffdd1..a61a5705e 100644 --- a/src/action/acesrc/acebot_items.c +++ b/src/action/acesrc/acebot_items.c @@ -56,7 +56,7 @@ int num_players = 0; int num_items = 0; -item_table_t item_table[MAX_EDICTS]; +item_table_t item_table[MAX_EDICTS_OLD]; edict_t *players[MAX_CLIENTS]; // pointers to all players in the game /////////////////////////////////////////////////////////////////////// diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 8788215b9..d548a2a8d 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1374,7 +1374,7 @@ void ACEND_LoadNodes(void) fclose(pIn); // Raptor007: Do not trust saved pointers! - for(i=0;ivalue || !noscore->value) @@ -1218,6 +1224,7 @@ static void Cmd_Players_f (edict_t * ent) } gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); + free(sortedClients); } /* diff --git a/src/action/g_items.c b/src/action/g_items.c index 64d048db7..8e6e80824 100644 --- a/src/action/g_items.c +++ b/src/action/g_items.c @@ -861,7 +861,7 @@ void Touch_Item (edict_t * ent, edict_t * other, cplane_t * plane, else other->client->ps.stats[STAT_PICKUP_ICON] = gi.imageindex(ent->item->icon); - other->client->ps.stats[STAT_PICKUP_STRING] = CS_ITEMS + ITEM_INDEX (ent->item); + other->client->ps.stats[STAT_PICKUP_STRING] = game.csr.items + ITEM_INDEX (ent->item); other->client->pickup_msg_framenum = level.realFramenum + 3 * HZ; // change selected item @@ -1998,6 +1998,6 @@ void SetItemNames (void) for (i = 0; i < game.num_items; i++) { it = &itemlist[i]; - gi.configstring (CS_ITEMS + i, it->pickup_name); + gi.configstring (game.csr.items + i, it->pickup_name); } } diff --git a/src/action/g_local.h b/src/action/g_local.h index b5a547acc..9c1dfe8ef 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2331,7 +2331,7 @@ typedef struct } gghost_t; -#define MAX_GHOSTS 64 //MAX_CLIENTS +#define MAX_GHOSTS 64 // If we need more ghosts than this, let's cross that bridge extern gghost_t ghost_players[MAX_GHOSTS]; extern int num_ghost_players; diff --git a/src/action/g_main.c b/src/action/g_main.c index 0808d9c00..248e7444b 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -1310,15 +1310,21 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) { int i, j; edict_t *ent; - edict_t *plist[ MAX_CLIENTS ] = {NULL}; int pcount = 0; + edict_t **plist = malloc(game.csr.maxclients * sizeof(edict_t*)); + if (plist == NULL) { + gi.dprintf("%s\n", "Error: Unable to allocate memory for ChooseRandomPlayer plist\n"); + return NULL; + } // Supplied paramter must be a valid team number if (teamNum < TEAM1 && teamNum > TEAM3) + free(plist); return 0; if (teamCount == 2 && teamNum == 3) { // Somehow passing team 3 in a 2-team match? gi.dprintf("Warning: Unable to ChooseRandomPlayer for a team that doesn't exist\n"); + free(plist); return 0; } @@ -1338,8 +1344,7 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) j = rand() % pcount; ent = plist[j]; - gi.dprintf("%s\n", ent->client->pers.netname); - // Returns a random player + free(plist); return ent; } \ No newline at end of file diff --git a/src/action/g_misc.c b/src/action/g_misc.c index c05887af6..b2a403d2d 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -517,22 +517,6 @@ Default _cone value is 10 (used to set size of light for spotlights) #define START_OFF 1 -/* -static void -light_use (edict_t * self, edict_t * other, edict_t * activator) -{ - if (self->spawnflags & START_OFF) - { - gi.configstring (CS_LIGHTS + self->style, "m"); - self->spawnflags &= ~START_OFF; - } - else - { - gi.configstring (CS_LIGHTS + self->style, "a"); - self->spawnflags |= START_OFF; - } -} -*/ void SP_light (edict_t * self) { @@ -541,16 +525,6 @@ void SP_light (edict_t * self) //{ G_FreeEdict (self); return; - /*} - - if (self->style >= 32) - { - self->use = light_use; - if (self->spawnflags & START_OFF) - gi.configstring (CS_LIGHTS + self->style, "a"); - else - gi.configstring (CS_LIGHTS + self->style, "m"); - }*/ } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 18ee1e71b..cdec82836 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1689,7 +1689,7 @@ void SP_worldspawn (edict_t * ent) gi.configstring(CS_SKYROTATE, va("%f", st.skyrotate)); gi.configstring(CS_SKYAXIS, va("%f %f %f", st.skyaxis[0], st.skyaxis[1], st.skyaxis[2])); gi.configstring(CS_CDTRACK, va("%i", ent->sounds)); - gi.configstring(CS_MAXCLIENTS, va("%i", game.maxclients)); + gi.configstring(game.csr.maxclients, va("%i", game.maxclients)); G_SetupStatusbar(); gi.configstring(CS_STATUSBAR, level.statusbar); diff --git a/src/action/g_utils.c b/src/action/g_utils.c index 2daef7604..76b7344b6 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -534,13 +534,13 @@ G_TouchTriggers void G_TouchTriggers (edict_t * ent) { int i, num; - edict_t *touch[MAX_EDICTS], *hit; + edict_t *touch[MAX_EDICTS_OLD], *hit; // dead things don't activate triggers! if ((ent->client || (ent->svflags & SVF_MONSTER)) && (ent->health <= 0)) return; - num = gi.BoxEdicts (ent->absmin, ent->absmax, touch, MAX_EDICTS, AREA_TRIGGERS); + num = gi.BoxEdicts (ent->absmin, ent->absmax, touch, q_countof(touch), AREA_TRIGGERS); // be careful, it is possible to have an entity in this // list removed before we get to it (killtriggered) @@ -566,9 +566,9 @@ to force all entities it covers to immediately touch it void G_TouchSolids (edict_t * ent) { int i, num; - edict_t *touch[MAX_EDICTS], *hit; + edict_t *touch[MAX_EDICTS_OLD], *hit; - num = gi.BoxEdicts (ent->absmin, ent->absmax, touch, MAX_EDICTS, AREA_SOLID); + num = gi.BoxEdicts (ent->absmin, ent->absmax, touch, q_countof(touch), AREA_SOLID); // be careful, it is possible to have an entity in this // list removed before we get to it (killtriggered) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 1a635bf43..881f46248 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -290,14 +290,19 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) char string[1024]; int stringlength; int i, j, totalClients; - gclient_t *sortedClients[MAX_CLIENTS]; int x, y; gclient_t *cl; edict_t *cl_ent; char *tag; + gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); + if (sortedClients == NULL) { + gi.dprintf("%s: Could not allocate memory for sortedClients\n", __func__); + return; + } #ifndef NO_BOTS if (ent->is_bot) + free(sortedClients); return; #endif @@ -312,7 +317,7 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) } else A_ScoreboardMessage(ent, killer); - + free(sortedClients); return; } @@ -366,6 +371,7 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) gi.WriteByte (svc_layout); gi.WriteString (string); + free(sortedClients); } @@ -896,7 +902,11 @@ void HUD_SpectatorUpdate(edict_t *clent) gclient_t *team1_players[6]; gclient_t *team2_players[6]; - gclient_t *sortedClients[MAX_CLIENTS]; + gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); + if (sortedClients == NULL) { + gi.dprintf("%s: Could not allocate memory for sortedClients\n", __func__); + } + int totalClients, team1Clients, team2Clients; memset(team1_players, 0, sizeof(team1_players)); @@ -935,7 +945,7 @@ void HUD_SpectatorUpdate(edict_t *clent) continue; } } - + free(sortedClients); // team 1 (red team) Ghud_SetFlags(clent, hud[h_team_l], 0); diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index ddac82c82..4fe2742e5 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -318,7 +318,8 @@ void Cmd_Stats_f (edict_t *targetent, char *arg) void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) { char string[2048]; - gclient_t *sortedClients[MAX_CLIENTS], *cl; + gclient_t *cl; + gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); int maxsize = 1000, i, line_y; int totalClients, secs, shots; double accuracy, fpm; @@ -326,6 +327,10 @@ void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) int totalscore[TEAM_TOP] = {0}; int name_pos[TEAM_TOP] = {0}; + if (sortedClients == NULL) { + gi.dprintf("%s: Couldn't allocate memory for sortedClients\n", __func__); + } + totalClients = G_SortedClients(sortedClients); ent->client->ps.stats[STAT_TEAM_HEADER] = level.pic_teamtag; @@ -459,6 +464,7 @@ void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) gi.WriteByte(svc_layout); gi.WriteString(string); + free(sortedClients); } void Cmd_Statmode_f(edict_t* ent) @@ -1136,12 +1142,19 @@ LogEndMatchStats void LogEndMatchStats(void) { int i; - gclient_t *sortedClients[MAX_CLIENTS], *cl; + gclient_t *cl; + gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); int totalClients; + + if (sortedClients == NULL) { + gi.dprintf("%s: Couldn't allocate memory for sortedClients\n", __FUNCTION__); + } + totalClients = G_SortedClients(sortedClients); // Check if there's an AI bot in the game, if so, do nothing if (game.ai_ent_found) { + free(sortedClients); return; } @@ -1206,6 +1219,7 @@ void LogEndMatchStats(void) num_ghost_players = 0; // Be free, little dummy entity G_FreeEdict(ent); + free(sortedClients); } } #endif diff --git a/src/client/screen.c b/src/client/screen.c index 0e52077bd..507886b27 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -2133,7 +2133,7 @@ static xhair_weapon_cfg_t xhair_weapon_cfgs[9] = { {"models/weapons/v_handgr/tris.md2", XHAIR_MAX_GAP, 1, 1, -1, -1, 1} }; static int XHAIR_GetWeaponIndex(void) { - char* wepname = cl.configstrings[CS_MODELS + cl.frame.ps.gunindex]; + char* wepname = cl.configstrings[cl.csr.models + cl.frame.ps.gunindex]; for(int i=0; i<9; i++) { if (Q_stricmp(wepname, xhair_weapon_cfgs[i].wepname) == 0) { return i; diff --git a/src/server/game.c b/src/server/game.c index a62fd83f0..41d00a32c 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1138,6 +1138,7 @@ void SV_InitGameProgs(void) Com_Printf("Game supports Q2PRO protocol extensions.\n"); svs.csr = cs_remap_new; } + Com_Printf("Extended protocol: %s\n", svs.csr.extended ? "enabled" : "disabled"); // sanitize edict_size unsigned min_size = svs.csr.extended ? sizeof(edict_t) : q_offsetof(edict_t, x); From 3831497368701e730a697a57502024c19b323c7e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 7 Feb 2024 22:45:49 -0500 Subject: [PATCH 014/974] Fixed #if/#ifdef, BIT() some things in g_local... --- inc/client/client.h | 2 +- inc/common/common.h | 2 +- inc/common/msg.h | 2 +- inc/shared/game.h | 8 +-- inc/shared/shared.h | 8 +-- src/action/a_cmds.c | 4 +- src/action/a_ctf.c | 2 +- src/action/a_esp.c | 2 +- src/action/a_match.c | 2 +- src/action/a_team.c | 12 ++-- src/action/g_cmds.c | 2 +- src/action/g_ext.c | 2 +- src/action/g_local.h | 152 ++++++++++++++++++++++------------------- src/action/g_main.c | 4 +- src/action/g_save.c | 4 +- src/action/g_spawn.c | 4 +- src/action/p_client.c | 54 +++++++-------- src/action/p_hud.c | 5 +- src/action/p_view.c | 2 +- src/action/tng_stats.c | 2 +- src/client/client.h | 2 +- src/client/ghud.c | 2 +- src/client/main.c | 4 +- src/client/parse.c | 2 +- src/client/screen.c | 4 +- src/common/common.c | 2 +- src/common/msg.c | 8 +-- src/common/pmove.c | 2 +- src/server/entities.c | 6 +- src/server/game.c | 10 +-- src/server/ghud.c | 2 +- src/server/mvd.c | 2 +- src/server/server.h | 12 ++-- src/server/user.c | 8 +-- 34 files changed, 175 insertions(+), 166 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index f7a35dd7e..5660330d3 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/net/net.h" #include "common/utils.h" -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION #include "shared/ghud.h" #endif diff --git a/inc/common/common.h b/inc/common/common.h index 2067aebaa..194eeefc4 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -113,7 +113,7 @@ void Com_AddConfigFile(const char *name, unsigned flags); #define COM_DEDICATED 1 #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void G_InitializeExtensions(void); #endif diff --git a/inc/common/msg.h b/inc/common/msg.h index b52aec7b6..9065d6ca4 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -161,7 +161,7 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, player_st void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, player_state_t *to, int flags, int extraflags, msgPsFlags_t psflags); #endif void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, player_state_t *to, int flags, msgPsFlags_t psflags); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int MSG_DeltaGhud(ghud_element_t *from, ghud_element_t *to, int protocolmask); void MSG_WriteGhud(ghud_element_t *element, int flags); #if USE_CLIENT || USE_MVD_CLIENT diff --git a/inc/shared/game.h b/inc/shared/game.h index f7b0a90fa..4a447458a 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., // game.h -- game dll information visible to server // -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION #define GAME_API_VERSION 4 #else #define GAME_API_VERSION 3 @@ -100,7 +100,7 @@ struct gclient_s { // the game dll can add anything it wants after // this point in the structure -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif }; @@ -224,7 +224,7 @@ typedef struct { void (*DebugGraph)(float value, int color); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void *(*CheckForExtension)(char *text); #endif } game_import_t; @@ -271,7 +271,7 @@ typedef struct { // of the parameters void (*ServerCommand)(void); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void* (*FetchGameExtension)(char *name); #endif diff --git a/inc/shared/shared.h b/inc/shared/shared.h index a9079f552..bef83860d 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -106,7 +106,7 @@ typedef enum { PRINT_NOTICE // print in cyan color } print_type_t; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION #include "shared/ghud.h" #endif @@ -847,7 +847,7 @@ typedef struct cvar_s { // ------ new stuff ------ #if USE_CLIENT || USE_SERVER -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int sync_index; #endif int integer; @@ -1002,7 +1002,7 @@ typedef enum { //KEX -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // pmove->pm_aq2_flags #define PMF_AQ2_LIMP 0x01 // used to predict limping #endif @@ -1022,7 +1022,7 @@ typedef struct { short gravity; short delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION short pm_aq2_flags; // limping, bandaging, etc unsigned short pm_timestamp; // timestamp, resets every 60 seconds byte pm_aq2_leghits; // number of leg hits diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index e556b14e3..e83113c87 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -133,7 +133,7 @@ #include "g_local.h" #include -#ifdef USE_AQTION +#if USE_AQTION #ifdef _WIN32 #if _MSC_VER && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") @@ -1315,7 +1315,7 @@ void Cmd_Ghost_f(edict_t * ent) } -#ifdef USE_AQTION +#if USE_AQTION void generate_uuid(void) { #ifdef _WIN32 diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 500f099e1..42a6f6808 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -931,7 +931,7 @@ void CTFCalcScores(void) ctfgame.total2 += game.clients[i].resp.score; } - #ifdef USE_AQTION + #if USE_AQTION // Needed to add this here because this is called separately from TallyEndOfLevelTeamScores (teamplay) if (stat_logs->value) { LogMatch(); // Generates end of game stats diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 65786d1f5..ef7eead14 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -490,7 +490,7 @@ void EspMakeCapturePoint(edict_t *flag) /* Indicator arrow This appears regardless of indicator settings */ - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION if (!flag->obj_arrow){ flag->obj_arrow = G_Spawn(); flag->obj_arrow->solid = SOLID_NOT; diff --git a/src/action/a_match.c b/src/action/a_match.c index b79363292..1e67871fa 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -85,7 +85,7 @@ void SendScores(void) } gi.bprintf(PRINT_HIGH, "Match is over, waiting for next map, please vote a new one..\n"); - #ifdef USE_AQTION + #if USE_AQTION // Needed to add this here because Matchmode does not call BeginIntermission, but other teamplay modes do call it if (stat_logs->value) { LogMatch(); // Generates end of game stats diff --git a/src/action/a_team.c b/src/action/a_team.c index 0bc1eb089..724a4f186 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1496,7 +1496,7 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) G_UpdatePlayerStatusbar(ent, 1); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (desired_team == NOTEAM) HUD_SetType(ent, 1); else @@ -1519,7 +1519,7 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) AddToTransparentList (ent); } - #ifdef USE_AQTION + #if USE_AQTION if (in_warmup && warmup_bots->value) { PutClientInServer (ent); } @@ -1562,7 +1562,7 @@ void LeaveTeam (edict_t * ent) ent->client->resp.team = NOTEAM; G_UpdatePlayerStatusbar(ent, 1); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION HUD_SetType(ent, 1); #endif @@ -2316,7 +2316,7 @@ void RunWarmup (void) gi.centerprintf(ent, "WARMUP"); } } - #ifdef USE_AQTION + #if USE_AQTION if (warmup_bots->value){ gi.cvar_forceset("am", "1"); gi.cvar_forceset("am_botcount", warmup_bots->string); @@ -2802,7 +2802,7 @@ int CheckTeamRules (void) gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); - #ifdef USE_AQTION + #if USE_AQTION // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ gi.cvar_forceset("am", "0"); @@ -3813,7 +3813,7 @@ void TallyEndOfLevelTeamScores (void) } // Stats begin - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value && !matchmode->value) { LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match logs diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index a0b50d1cc..9700d8b73 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1702,7 +1702,7 @@ static void Cmd_PrintSettings_f( edict_t * ent ) itmflagsSettings( text, sizeof( text ), (int)itm_flags->value ); length = strlen( text ); - #ifdef USE_AQTION + #if USE_AQTION Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" "limchasecam %2d tgren %2d antilag_interp %2d\n" diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 2e9c1c686..a24328ab4 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -8,7 +8,7 @@ #include "g_local.h" #include "q_ghud.h" -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // // Reki // Setup struct and macro for defining engine-callable entrypoints diff --git a/src/action/g_local.h b/src/action/g_local.h index 9c1dfe8ef..bc9d66959 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -313,6 +313,7 @@ // These are mapped to the enum svc_ops_t in protocol.h q2pro // If these need adjusted in q2pro, adjust them here too +// Warning: this breaks backwards compatbility with earlier versions of AQtion if you change it #define svc_extend 30 #define svc_userstatistic 31 @@ -330,33 +331,33 @@ // edict->spawnflags // these are set with checkboxes on each entity in the map editor -#define SPAWNFLAG_NOT_EASY 0x00000100 -#define SPAWNFLAG_NOT_MEDIUM 0x00000200 -#define SPAWNFLAG_NOT_HARD 0x00000400 -#define SPAWNFLAG_NOT_DEATHMATCH 0x00000800 -#define SPAWNFLAG_NOT_COOP 0x00001000 +#define SPAWNFLAG_NOT_EASY BIT(8) +#define SPAWNFLAG_NOT_MEDIUM BIT(9) +#define SPAWNFLAG_NOT_HARD BIT(10) +#define SPAWNFLAG_NOT_DEATHMATCH BIT(11) +#define SPAWNFLAG_NOT_COOP BIT(12) // edict->flags -#define FL_FLY 0x00000001 -#define FL_SWIM 0x00000002 // implied immunity to drowining -#define FL_IMMUNE_LASER 0x00000004 -#define FL_INWATER 0x00000008 -#define FL_GODMODE 0x00000010 -#define FL_NOTARGET 0x00000020 -#define FL_IMMUNE_SLIME 0x00000040 -#define FL_IMMUNE_LAVA 0x00000080 -#define FL_PARTIALGROUND 0x00000100 // not all corners are valid -#define FL_WATERJUMP 0x00000200 // player jumping out of water -#define FL_TEAMSLAVE 0x00000400 // not the first on the team -#define FL_NO_KNOCKBACK 0x00000800 -#define FL_POWER_ARMOR 0x00001000 // power armor (if any) is active -#define FL_ACCELERATE 0x20000000 // accelerative movement -#define FL_RESPAWN 0x80000000 // used for item respawning +#define FL_FLY BIT(0) +#define FL_SWIM BIT(1) // implied immunity to drowining +#define FL_IMMUNE_LASER BIT(2) +#define FL_INWATER BIT(3) +#define FL_GODMODE BIT(4) +#define FL_NOTARGET BIT(5) +#define FL_IMMUNE_SLIME BIT(6) +#define FL_IMMUNE_LAVA BIT(7) +#define FL_PARTIALGROUND BIT(8) // not all corners are valid +#define FL_WATERJUMP BIT(9) // player jumping out of water +#define FL_TEAMSLAVE BIT(10) // not the first on the team +#define FL_NO_KNOCKBACK BIT(11) +#define FL_POWER_ARMOR BIT(12) // power armor (if any) is active +#define FL_ACCELERATE BIT(29) // accelerative movement +#define FL_RESPAWN BIT(31) // used for item respawning // edict->client->pers.spec_flags -#define SPECFL_KILLFEED 0x00000001 -#define SPECFL_SPECHUD 0x00000002 -#define SPECFL_SPECHUD_NEW 0x00000004 +#define SPECFL_KILLFEED BIT(0) +#define SPECFL_SPECHUD BIT(1) +#define SPECFL_SPECHUD_NEW BIT(2) // variable server FPS #ifndef NO_FPS @@ -440,21 +441,21 @@ ammo_t; #define GIB_METALLIC 1 //monster ai flags -#define AI_STAND_GROUND 0x00000001 -#define AI_TEMP_STAND_GROUND 0x00000002 -#define AI_SOUND_TARGET 0x00000004 -#define AI_LOST_SIGHT 0x00000008 -#define AI_PURSUIT_LAST_SEEN 0x00000010 -#define AI_PURSUE_NEXT 0x00000020 -#define AI_PURSUE_TEMP 0x00000040 -#define AI_HOLD_FRAME 0x00000080 -#define AI_GOOD_GUY 0x00000100 -#define AI_BRUTAL 0x00000200 -#define AI_NOSTEP 0x00000400 -#define AI_DUCKED 0x00000800 -#define AI_COMBAT_POINT 0x00001000 -#define AI_MEDIC 0x00002000 -#define AI_RESURRECTING 0x00004000 +#define AI_STAND_GROUND BIT(0) +#define AI_TEMP_STAND_GROUND BIT(1) +#define AI_SOUND_TARGET BIT(2) +#define AI_LOST_SIGHT BIT(3) +#define AI_PURSUIT_LAST_SEEN BIT(4) +#define AI_PURSUE_NEXT BIT(5) +#define AI_PURSUE_TEMP BIT(6) +#define AI_HOLD_FRAME BIT(7) +#define AI_GOOD_GUY BIT(8) +#define AI_BRUTAL BIT(9) +#define AI_NOSTEP BIT(10) +#define AI_DUCKED BIT(11) +#define AI_COMBAT_POINT BIT(12) +#define AI_MEDIC BIT(13) +#define AI_RESURRECTING BIT(14) //monster attack state #define AS_STRAIGHT 1 @@ -481,15 +482,15 @@ ammo_t; // game.serverflags values -#define SFL_CROSS_TRIGGER_1 0x00000001 -#define SFL_CROSS_TRIGGER_2 0x00000002 -#define SFL_CROSS_TRIGGER_3 0x00000004 -#define SFL_CROSS_TRIGGER_4 0x00000008 -#define SFL_CROSS_TRIGGER_5 0x00000010 -#define SFL_CROSS_TRIGGER_6 0x00000020 -#define SFL_CROSS_TRIGGER_7 0x00000040 -#define SFL_CROSS_TRIGGER_8 0x00000080 -#define SFL_CROSS_TRIGGER_MASK 0x000000ff +#define SFL_CROSS_TRIGGER_1 BIT(0) +#define SFL_CROSS_TRIGGER_2 BIT(1) +#define SFL_CROSS_TRIGGER_3 BIT(2) +#define SFL_CROSS_TRIGGER_4 BIT(3) +#define SFL_CROSS_TRIGGER_5 BIT(4) +#define SFL_CROSS_TRIGGER_6 BIT(5) +#define SFL_CROSS_TRIGGER_7 BIT(6) +#define SFL_CROSS_TRIGGER_8 BIT(7) +#define SFL_CROSS_TRIGGER_MASK (BIT(8) - 1) // noise types for PlayerNoise @@ -819,7 +820,7 @@ typedef struct int model_null; int model_lsight; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int model_arrow; #endif @@ -1257,7 +1258,7 @@ extern cvar_t *timedmsgs; extern cvar_t *mm_captain_teamname; extern cvar_t *sv_killgib; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); extern int (*engine_Client_GetProtocol)(edict_t *ent); @@ -1309,7 +1310,7 @@ extern cvar_t *gm; // Gamemode extern cvar_t *gmf; // Gamemodeflags extern cvar_t *sv_idleremove; // Remove idlers -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION extern cvar_t *use_newirvision; // enable new irvision (only highlight baddies) extern cvar_t *use_indicators; // enable/allow indicators extern cvar_t *use_xerp; // allow clients to use cl_xerp @@ -1513,7 +1514,7 @@ void ClientBeginServerFrame (edict_t * ent); // // g_ext.c // -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void G_InitExtEntrypoints(void); void* G_FetchGameExtension(char *name); #endif @@ -1593,7 +1594,7 @@ void G_UpdateSpectatorStatusbar( void ); void G_UpdatePlayerStatusbar( edict_t *ent, int force ); int Gamemodeflag(void); int Gamemode(void); -#ifdef USE_AQTION +#if USE_AQTION void generate_uuid(void); #endif // @@ -1625,7 +1626,7 @@ void ProduceShotgunDamageReport(edict_t*); //tng_stats.c void StatBotCheck(void); -#ifdef USE_AQTION +#if USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); void LogCapture(edict_t *capturer); @@ -1710,7 +1711,7 @@ typedef struct int limp_nopred; int spec_flags; qboolean antilag_optout; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int cl_xerp; int cl_indicators; #endif @@ -1727,7 +1728,7 @@ typedef struct ignorelist_t ignorelist; gitem_t *chosenItem2; // Support for item kit mode - #ifdef USE_AQTION + #if USE_AQTION char steamid[24]; char discordid[24]; #endif @@ -1822,7 +1823,7 @@ typedef struct vec3_t jmp_teleport_v_angle; qboolean jmp_teleport_ducked; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int hud_items[128]; int hud_type; #endif @@ -1870,7 +1871,7 @@ struct gclient_s int clientNum; // Reki: cvar sync -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif @@ -2054,7 +2055,7 @@ struct gclient_s int ctf_grapplestate; // true if pulling int ctf_grapplereleaseframe; // frame of grapple release -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION //AQTION - Reki: Teammate indicators edict_t *arrow; #endif @@ -2292,7 +2293,7 @@ struct edict_s vec3_t lastPosition; qboolean nameused[NUMNAMES][NUMNAMES]; qboolean newnameused[AQ2WTEAMSIZE]; - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION //AQTION - Reki: Entity indicators edict_t *obj_arrow; #endif @@ -2319,7 +2320,7 @@ typedef struct gitem_t *weapon; gitem_t *item; // Extended stats - #ifdef USE_AQTION + #if USE_AQTION char steamid[24]; char discordid[24]; #endif @@ -2446,6 +2447,13 @@ typedef struct team_s char leader_name[MAX_SKINLEN]; char leader_skin[MAX_QPATH]; char leader_skin_index[MAX_QPATH]; +#if AQTION_EXTENSION +#if AQTION_HUD + int ghud_resettime; + byte ghud_icon; + byte ghud_num; +#endif +#endif }team_t; extern team_t teams[TEAM_TOP]; @@ -2464,7 +2472,7 @@ extern int gameSettings; #include "a_dom.h" #include "a_esp.h" -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION #define HAS_CVARSYNC(ent) (Client_GetProtocol(ent) == 38 && Client_GetVersion(ent) >= 3013) // hud (through ghud extension) @@ -2494,19 +2502,19 @@ typedef enum { } clcvar_t; // UI flags from q2pro -#define UI_LEFT 0x00000001 -#define UI_RIGHT 0x00000002 +#define UI_LEFT BIT(0) +#define UI_RIGHT BIT(1) #define UI_CENTER (UI_LEFT | UI_RIGHT) -#define UI_BOTTOM 0x00000004 -#define UI_TOP 0x00000008 +#define UI_BOTTOM BIT(2) +#define UI_TOP BIT(3) #define UI_MIDDLE (UI_BOTTOM | UI_TOP) -#define UI_DROPSHADOW 0x00000010 -#define UI_ALTCOLOR 0x00000020 -#define UI_IGNORECOLOR 0x00000040 -#define UI_XORCOLOR 0x00000080 -#define UI_AUTOWRAP 0x00000100 -#define UI_MULTILINE 0x00000200 -#define UI_DRAWCURSOR 0x00000400 +#define UI_DROPSHADOW BIT(4) +#define UI_ALTCOLOR BIT(5) +#define UI_IGNORECOLOR BIT(6) +#define UI_XORCOLOR BIT(7) +#define UI_AUTOWRAP BIT(8) +#define UI_MULTILINE BIT(9) +#define UI_DRAWCURSOR BIT(10) #endif #ifndef NO_BOTS diff --git a/src/action/g_main.c b/src/action/g_main.c index 248e7444b..c9e6ab807 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -527,7 +527,7 @@ cvar_t *timedmsgs; // Toggles timed messages cvar_t *mm_captain_teamname; // Toggles if we want to use the captain's name for the team in matchmode cvar_t *sv_killgib; // Gibs on 'kill' command -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION cvar_t *use_newirvision; cvar_t *use_indicators; cvar_t *use_xerp; @@ -627,7 +627,7 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) globals.edict_size = sizeof (edict_t); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION G_InitExtEntrypoints(); globals.FetchGameExtension = G_FetchGameExtension; diff --git a/src/action/g_save.c b/src/action/g_save.c index 6b55a00c3..86aa40214 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -642,7 +642,7 @@ void InitGame( void ) sv_killgib = gi.cvar("sv_killgib", "0", 0); // new AQtion Extension cvars -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION use_newirvision = gi.cvar("use_newirvision", "1", 0); use_indicators = gi.cvar("use_indicators", "1", 0); use_xerp = gi.cvar("use_xerp", "1", 0); @@ -733,7 +733,7 @@ void InitGame( void ) gi.cvar_forceset("g_view_high", va("%d", STANDING_VIEWHEIGHT)); gi.cvar_forceset("g_view_low", va("%d", CROUCHING_VIEWHEIGHT)); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION CvarSync_Set(clcvar_cl_antilag, "cl_antilag", "1"); CvarSync_Set(clcvar_cl_indicators, "cl_indicators", "1"); CvarSync_Set(clcvar_cl_xerp, "cl_xerp", "0"); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index cdec82836..7b01afc03 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1236,7 +1236,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn // Set serverinfo correctly for gamemodeflags Gamemodeflag(); - #ifdef USE_AQTION + #if USE_AQTION generate_uuid(); // Run this once every time a map loads to generate a unique id for stats (game.matchid) #endif @@ -1870,7 +1870,7 @@ void SP_worldspawn (edict_t * ent) level.model_null = gi.modelindex("sprites/null.sp2"); // null sprite level.model_lsight = gi.modelindex("sprites/lsight.sp2"); // laser sight dot sprite -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION level.model_arrow = gi.modelindex("models/indicator/arrow_red.md2"); gi.modelindex("models/indicator/arrow_blue.md2"); gi.modelindex("models/indicator/arrow_green.md2"); diff --git a/src/action/p_client.c b/src/action/p_client.c index 85654bb60..0c7848243 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -341,7 +341,7 @@ static void FreeClientEdicts(gclient_t *client) client->ctf_grapple = NULL; } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION //remove arrow if (client->arrow) { G_FreeEdict(client->arrow); @@ -384,7 +384,7 @@ void Announce_Reward(edict_t *ent, int rewardType) { CenterPrintAll(buf); gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value) LogAward(ent, rewardType); #endif @@ -859,7 +859,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogKill(self, inflictor, self->client->attacker); } @@ -897,7 +897,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) self->enemy = NULL; - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogWorldKill(self); } @@ -1238,7 +1238,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value) { LogKill(self, inflictor, attacker); } @@ -1268,7 +1268,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) PrintDeathMessage(death_msg, self); IRC_printf(IRC_T_DEATH, death_msg); - #ifdef USE_AQTION + #if USE_AQTION if (stat_logs->value) { // Only create stats logs if stat_logs is 1 LogWorldKill(self); } @@ -2513,7 +2513,7 @@ void PutClientInServer(edict_t * ent) client_persistant_t pers; client_respawn_t resp; gitem_t *item; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif @@ -2530,13 +2530,13 @@ void PutClientInServer(edict_t * ent) // deathmatch wipes most client data every spawn resp = client->resp; pers = client->pers; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION memcpy(cl_cvar, client->cl_cvar, sizeof(client->cl_cvar)); #endif memset(client, 0, sizeof(*client)); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION memcpy(client->cl_cvar, cl_cvar, sizeof(client->cl_cvar)); #endif client->pers = pers; @@ -2629,7 +2629,7 @@ void PutClientInServer(edict_t * ent) ent->s.skinnum = ent - g_edicts - 1; ent->s.modelindex = 255; // will use the skin specified model -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // teammate indicator arrows if (use_indicators->value && teamplay->value && !client->arrow && client->resp.team) { @@ -2799,7 +2799,7 @@ void PutClientInServer(edict_t * ent) ent->svflags |= SVF_NOCLIENT; ent->client->ps.gunindex = 0; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (!ent->client->resp.team) HUD_SetType(ent, 1); #endif @@ -2811,7 +2811,7 @@ void PutClientInServer(edict_t * ent) } // end if( respawn ) #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION HUD_SetType(ent, -1); #endif @@ -2916,7 +2916,7 @@ void ClientBeginDeathmatch(edict_t * ent) ent->client->resp.enterframe = level.framenum; ent->client->resp.gldynamic = 1; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (teamplay->value) { HUD_SetType(ent, 1); @@ -2967,9 +2967,9 @@ void ClientBeginDeathmatch(edict_t * ent) #ifndef NO_BOTS ACEIT_RebuildPlayerList(); -#ifdef USE_AQTION +#if USE_AQTION StatBotCheck(); - #ifdef USE_AQTION + #if USE_AQTION if(am->value){ attract_mode_bot_check(); } @@ -3143,7 +3143,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) // Reki - disable prediction on limping -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (Client_GetProtocol(ent) == 38) // if we're using AQTION protocol, we have limp prediction { client->pers.limp_nopred = 0; @@ -3159,12 +3159,12 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) client->pers.limp_nopred = 2 | (client->pers.limp_nopred & 256); // client doesn't specify, so use auto threshold else if (limp == 0) client->pers.limp_nopred = 0; // client explicity wants old behavior -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION } #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (!HAS_CVARSYNC(ent)) // only do these cl cvars if cvarsync isn't a thing, since it's much better than userinfo { #endif @@ -3175,7 +3175,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) else client->pers.spec_flags &= ~(SPECFL_SPECHUD | SPECFL_SPECHUD_NEW); - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION if (Client_GetProtocol(ent) == 38) // Reki: new clients get new spec hud client->pers.spec_flags &= ~SPECFL_SPECHUD; #endif @@ -3196,7 +3196,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) if (sv_antilag->value && antilag_value != client->pers.antilag_optout) gi.cprintf(ent, PRINT_MEDIUM, "YOUR CL_ANTILAG IS NOW SET TO %i\n", !client->pers.antilag_optout); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION } #endif } @@ -3254,7 +3254,7 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) Q_strncpyz(ent->client->pers.ip, ipaddr_buf, sizeof(ent->client->pers.ip)); Q_strncpyz(ent->client->pers.userinfo, userinfo, sizeof(ent->client->pers.userinfo)); - #ifdef USE_AQTION + #if USE_AQTION value = Info_ValueForKey(userinfo, "steamid"); if (*value) Q_strncpyz(ent->client->pers.steamid, value, sizeof(ent->client->pers.steamid)); @@ -3386,10 +3386,10 @@ void ClientDisconnect(edict_t * ent) ent->is_bot = false; ent->think = NULL; ACEIT_RebuildPlayerList(); -#ifdef USE_AQTION +#if USE_AQTION StatBotCheck(); - #ifdef USE_AQTION + #if USE_AQTION if(am->value){ attract_mode_bot_check(); } @@ -3424,7 +3424,7 @@ void CreateGhost(edict_t * ent) strcpy(ghost->ip, ent->client->pers.ip); strcpy(ghost->netname, ent->client->pers.netname); - #ifdef USE_AQTION + #if USE_AQTION strcpy(ghost->steamid, ent->client->pers.steamid); strcpy(ghost->discordid, ent->client->pers.discordid); #endif @@ -3605,7 +3605,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) qboolean has_enhanced_slippers = esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM); if( client->leg_damage && ent->groundentity && ! has_enhanced_slippers ) { - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION pm.s.pm_aq2_flags |= PMF_AQ2_LIMP; pm.s.pm_aq2_leghits = min(client->leghits, 255); #else @@ -3625,7 +3625,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) pm.s.pm_flags |= PMF_JUMP_HELD; #endif } - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION else { pm.s.pm_aq2_flags &= ~PMF_AQ2_LIMP; @@ -3789,7 +3789,7 @@ void ClientBeginServerFrame(edict_t * ent) Cmd_PMLCA_f(ent); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // resync pm_timestamp so all limps are roughly synchronous, to try to maintain original behavior unsigned short world_timestamp = (int)(level.time * 1000) % 60000; client->ps.pmove.pm_timestamp = world_timestamp; diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 881f46248..3b6441be3 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -132,7 +132,7 @@ void BeginIntermission(edict_t *targ) } else if (teamplay->value) { TallyEndOfLevelTeamScores(); } - #ifdef USE_AQTION + #if USE_AQTION // Generates stats for non-CTF, Teamplay or Matchmode else if (stat_logs->value && !matchmode->value) { LogMatch(); @@ -405,6 +405,7 @@ void Cmd_Score_f(edict_t *ent) { ent->client->showinventory = false; + gi.dprintf("-#-# Layout: %d\n", ent->client->layout); if (ent->client->layout == LAYOUT_MENU) PMenu_Close(ent); @@ -752,7 +753,7 @@ void G_SetStats (edict_t * ent) } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void HUD_SetType(edict_t *clent, int type) { diff --git a/src/action/p_view.c b/src/action/p_view.c index 674edd577..b81c2b508 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1369,7 +1369,7 @@ void ClientEndServerFrame (edict_t * ent) current_player = ent; current_client = ent->client; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (current_client->arrow) { // set new origin diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 4fe2742e5..69c78927b 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -496,7 +496,7 @@ void Cmd_Statmode_f(edict_t* ent) stuffcmd(ent, stuff); } -#ifdef USE_AQTION +#if USE_AQTION // Revisit one day... diff --git a/src/client/client.h b/src/client/client.h index fc704a58a..16047802c 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -257,7 +257,7 @@ typedef struct client_state_s { char layout[MAX_NET_STRING]; // general 2D overlay int inventory[MAX_ITEMS]; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION ghud_3delement_t *ghud_3dlist; ghud_element_t ghud[MAX_GHUDS]; #endif diff --git a/src/client/ghud.c b/src/client/ghud.c index 8c8405336..41361f88e 100644 --- a/src/client/ghud.c +++ b/src/client/ghud.c @@ -1,6 +1,6 @@ #include "client.h" -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void CL_Ghud_Clear() diff --git a/src/client/main.c b/src/client/main.c index 205fb5f9f..b69a62726 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -1581,7 +1581,7 @@ void CL_CheckForResend(void) cls.serverProtocol = cl_protocol->integer; if (cls.serverProtocol < PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { - #ifdef AQTION_EXTENSION + #if AQTION_EXTENSION cls.serverProtocol = PROTOCOL_VERSION_AQTION; #else cls.serverProtocol = PROTOCOL_VERSION_Q2PRO; @@ -1867,7 +1867,7 @@ void CL_ClearState(void) SCR_ClearCenterPrints(); CL_ClearEffects(); CL_ClearTEnts(); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION CL_Clear3DGhudQueue(); #endif LOC_FreeLocations(); diff --git a/src/client/parse.c b/src/client/parse.c index 8d9948efb..3b4dd1cf0 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -1529,7 +1529,7 @@ void CL_ParseServerMessage(void) continue; case svc_ghudupdate: -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (cls.serverProtocol != PROTOCOL_VERSION_AQTION) goto badbyte; //CL_ParseGhud(); diff --git a/src/client/screen.c b/src/client/screen.c index 507886b27..aeb71bc1d 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -2304,7 +2304,7 @@ static void SCR_DrawClassicCrosshair(void) { scr.crosshair_height, scr.crosshair_pic); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void CL_Clear3DGhudQueue(void) { ghud_3delement_t *link; @@ -2579,7 +2579,7 @@ static void SCR_Draw2D(void) SCR_DrawLayout(); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // Draw game defined hud elements SCR_DrawGhud(); diff --git a/src/common/common.c b/src/common/common.c index 6eea8b0c5..31c20d447 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -866,7 +866,7 @@ void Qcommon_Init(int argc, char **argv) Key_Init(); Prompt_Init(); Con_Init(); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION G_InitializeExtensions(); #endif diff --git a/src/common/msg.c b/src/common/msg.c index be0d3d311..43cdd1a7f 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -1334,7 +1334,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, // // aqtion extension checks // -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (to->pmove.pm_aq2_flags != from->pmove.pm_aq2_flags) aqtflags |= AQPS_PMFLAGS; if (to->pmove.pm_timestamp != from->pmove.pm_timestamp) @@ -1394,7 +1394,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, // MSG_WriteByte(aqtflags); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (aqtflags & AQPS_PMFLAGS) MSG_WriteByte(to->pmove.pm_aq2_flags); if (aqtflags & AQPS_TIMESTAMP) @@ -2059,7 +2059,7 @@ void MSG_ReadDeltaUsercmd_Enhanced(const usercmd_t *from, usercmd_t *to) -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION int MSG_DeltaGhud(ghud_element_t *from, ghud_element_t *to, int protocolmask) { @@ -2668,7 +2668,7 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, // // parse the aqtion extensions // -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION aqtflags = MSG_ReadByte(); if (aqtflags & AQPS_PMFLAGS) diff --git a/src/common/pmove.c b/src/common/pmove.c index 285d81f4d..06faf5bf0 100644 --- a/src/common/pmove.c +++ b/src/common/pmove.c @@ -1040,7 +1040,7 @@ void Pmove(pmove_t *pmove, pmoveParams_t *params) // clear all pmove local vars memset(&pml, 0, sizeof(pml)); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // add msec to the timestamp pm->s.pm_timestamp = (pm->s.pm_timestamp + pm->cmd.msec) % 60000; diff --git a/src/server/entities.c b/src/server/entities.c index a6a4f8d37..0200ba750 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -314,7 +314,7 @@ void SV_WriteFrameToClient_Enhanced(client_t *client) SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION static void SV_Ghud_SendUpdateToClient(client_t *client, client_frame_t *oldframe, client_frame_t *frame) { if (client->protocol == PROTOCOL_VERSION_AQTION && client->version >= PROTOCOL_VERSION_AQTION_GHUD) @@ -477,7 +477,7 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) // delta encode the entities SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (oldframe) SV_Ghud_SendUpdateToClient(client, oldframe, frame); #endif @@ -711,7 +711,7 @@ void SV_BuildClientFrame(client_t *client) entity_state_t ent_state; ent_state = ent->s; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (GE_customizeentityforclient) if (!GE_customizeentityforclient(client->edict, ent, &ent_state)) continue; diff --git a/src/server/game.c b/src/server/game.c index 41d00a32c..1e5b6cf8b 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -904,7 +904,7 @@ void SV_ShutdownGameProgs(void) Cvar_Set("g_view_predict", "0"); Z_LeakTest(TAG_FREE); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION GE_customizeentityforclient = NULL; GE_CvarSync_Updated = NULL; #endif @@ -944,7 +944,7 @@ static void *SV_LoadGameLibrary(const char *libdir, const char *gamedir) } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION typedef struct extension_func_s { char name[MAX_QPATH]; @@ -1080,7 +1080,7 @@ void SV_InitGameProgs(void) // unload anything we have now SV_ShutdownGameProgs(); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION SV_Ghud_Clear(); SV_CvarSync_Clear(); #endif @@ -1112,7 +1112,7 @@ void SV_InitGameProgs(void) // load a new game dll import = game_import; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION import.CheckForExtension = G_CheckForExtension; #endif @@ -1153,7 +1153,7 @@ void SV_InitGameProgs(void) Com_Error(ERR_DROP, "Game library returned bad number of max_edicts"); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION GE_customizeentityforclient = ge->FetchGameExtension("customizeentityforclient"); GE_CvarSync_Updated = ge->FetchGameExtension("CvarSync_Updated"); #endif diff --git a/src/server/ghud.c b/src/server/ghud.c index b6fb6ecab..32f931b6d 100644 --- a/src/server/ghud.c +++ b/src/server/ghud.c @@ -1,5 +1,5 @@ #include "server.h" -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void SV_Ghud_Clear(void) diff --git a/src/server/mvd.c b/src/server/mvd.c index b7b08aee9..2536269d0 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -772,7 +772,7 @@ static void emit_frame(void) ent_state = ent->s; ent_active = entity_is_active(ent); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (ent_active && GE_customizeentityforclient) if (!GE_customizeentityforclient(NULL, ent, &ent_state)) ent_active = false; diff --git a/src/server/server.h b/src/server/server.h index b218ce880..c929c05ec 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -111,7 +111,7 @@ typedef struct { byte areabits[MAX_MAP_AREA_BYTES]; // portalarea visibility bits unsigned sentTime; // for ping calculations int latency; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION ghud_element_t ghud[MAX_GHUDS]; #endif } client_frame_t; @@ -393,7 +393,7 @@ typedef struct client_s { char *ac_token; #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION ghud_element_t ghud[MAX_GHUDS]; #endif } client_t; @@ -505,7 +505,7 @@ typedef struct server_static_s { challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // Reki: cvar sync entries cvarsync_t cvarsync_list[CVARSYNC_MAX]; byte cvarsync_length; @@ -789,7 +789,7 @@ void SV_InitEdict(edict_t *e); void PF_Pmove(pmove_t *pm); -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION void G_InitializeExtensions(void); #endif @@ -810,7 +810,7 @@ void SV_RegisterSavegames(void); #define SV_RegisterSavegames() (void)0 #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // // sv_ghud.c // @@ -827,7 +827,7 @@ void SV_Ghud_SetColor(edict_t *ent, int i, int r, int g, int b, int a); void SV_Ghud_SetSize(edict_t *ent, int i, int x, int y); #endif -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION extern int(*GE_customizeentityforclient)(edict_t *client, edict_t *ent, entity_state_t *state); // 0 don't send, 1 send normally extern void(*GE_CvarSync_Updated)(int index, edict_t *clent); #endif diff --git a/src/server/user.c b/src/server/user.c index 5b64d6804..ef3e5e2ac 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -55,7 +55,7 @@ static void SV_CreateBaselines(void) } } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION // clear ghud from previous level memset(sv_client->ghud, 0, sizeof(ghud_element_t) * MAX_GHUDS); #endif @@ -506,7 +506,7 @@ void SV_New_f(void) SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); } -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION if (svs.cvarsync_length && sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_CVARSYNC) { MSG_WriteByte(svc_extend); @@ -1112,7 +1112,7 @@ static const ucmd_t ucmds[] = { #endif { "aclist", SV_AC_List_f }, { "acinfo", SV_AC_Info_f }, -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION { "cvarsync", SV_CvarSync_f }, #endif @@ -1760,7 +1760,7 @@ void SV_ExecuteClientMessage(client_t *client) case clc_cvarsync: if (client->protocol != PROTOCOL_VERSION_AQTION) goto badbyte; -#ifdef AQTION_EXTENSION +#if AQTION_EXTENSION index = MSG_ReadByte(); MSG_ReadString(sv_player->client->cl_cvar[index], CVARSYNC_MAXSIZE); From c8cba35bf8dd8db7d80f6d2a5a8cf206caa96254 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 7 Feb 2024 22:58:18 -0500 Subject: [PATCH 015/974] Scoreboard fixed at least... --- src/action/a_esp.c | 8 +------- src/action/a_vote.c | 10 ++-------- src/action/g_cmds.c | 9 +-------- src/action/g_main.c | 9 +-------- src/action/p_hud.c | 15 ++------------- src/action/tng_stats.c | 17 ++--------------- 6 files changed, 9 insertions(+), 59 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index ef7eead14..e6c03c281 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1626,12 +1626,7 @@ and there are no active volunteers for that team qboolean EspChooseRandomLeader(int teamNum) { int players[TEAM_TOP] = { 0 }, i, numPlayers = 0; - edict_t *ent; - edict_t **playerList = malloc(game.csr.maxclients * sizeof(edict_t*)); - if (playerList == NULL) { - gi.dprintf("%s: Could not allocate memory for playerList\n", __FUNCTION__); - return false; - } + edict_t *ent, *playerList[MAX_CLIENTS]; if (team_round_going) { if (esp_debug->value) @@ -1669,7 +1664,6 @@ qboolean EspChooseRandomLeader(int teamNum) EspSetLeader(teamNum, ent); return true; } // If we get this far, that the only players here are bots and non-volunteers. Halt the game - free(playerList); return false; } diff --git a/src/action/a_vote.c b/src/action/a_vote.c index ad98caff6..8dd45b424 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -2016,12 +2016,8 @@ cvar_t *_InitScrambleVote (ini_t * ini) qboolean ScrambleTeams(void) { int i, j, numplayers, newteam; - edict_t *ent, *oldCaptains[TEAM_TOP] = {NULL}, *oldLeaders[TEAM_TOP] = {NULL}; - edict_t **players = malloc(game.csr.maxclients * sizeof(edict_t*)); - if (players == NULL) { - gi.dprintf("%s: Could not allocate memory for players\n", __FUNCTION__); - return false; - } + edict_t *ent, *players[MAX_CLIENTS], *oldCaptains[TEAM_TOP] = {NULL}, *oldLeaders[TEAM_TOP] = {NULL}; + numplayers = 0; for (i = 0, ent = &g_edicts[1]; i < game.maxclients; i++, ent++) @@ -2033,7 +2029,6 @@ qboolean ScrambleTeams(void) } if (numplayers <= teamCount) - free(players); return false; for (i = numplayers - 1; i > 0; i--) { @@ -2086,7 +2081,6 @@ qboolean ScrambleTeams(void) ent->client->resp.scramblevote = 0; } - free(players); return true; } diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 9700d8b73..a50c9b06a 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1187,13 +1187,7 @@ static void Cmd_Players_f (edict_t * ent) int count = 0; char small[64]; char large[1024]; - gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); - gclient_t *cl; - - if (sortedClients == NULL) { - gi.dprintf("%s: malloc failed for sortedClients\n", __func__); - return; - } + gclient_t *sortedClients[MAX_CLIENTS], *cl; if (!teamplay->value || !noscore->value) @@ -1224,7 +1218,6 @@ static void Cmd_Players_f (edict_t * ent) } gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); - free(sortedClients); } /* diff --git a/src/action/g_main.c b/src/action/g_main.c index c9e6ab807..41510bdb2 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -1311,20 +1311,14 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) int i, j; edict_t *ent; int pcount = 0; - edict_t **plist = malloc(game.csr.maxclients * sizeof(edict_t*)); - if (plist == NULL) { - gi.dprintf("%s\n", "Error: Unable to allocate memory for ChooseRandomPlayer plist\n"); - return NULL; - } + edict_t *plist[ MAX_CLIENTS ] = {NULL}; // Supplied paramter must be a valid team number if (teamNum < TEAM1 && teamNum > TEAM3) - free(plist); return 0; if (teamCount == 2 && teamNum == 3) { // Somehow passing team 3 in a 2-team match? gi.dprintf("Warning: Unable to ChooseRandomPlayer for a team that doesn't exist\n"); - free(plist); return 0; } @@ -1345,6 +1339,5 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) ent = plist[j]; // Returns a random player - free(plist); return ent; } \ No newline at end of file diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 3b6441be3..5bcfd3cfc 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -290,19 +290,14 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) char string[1024]; int stringlength; int i, j, totalClients; + gclient_t *sortedClients[MAX_CLIENTS]; int x, y; gclient_t *cl; edict_t *cl_ent; char *tag; - gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); - if (sortedClients == NULL) { - gi.dprintf("%s: Could not allocate memory for sortedClients\n", __func__); - return; - } #ifndef NO_BOTS if (ent->is_bot) - free(sortedClients); return; #endif @@ -371,7 +366,6 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) gi.WriteByte (svc_layout); gi.WriteString (string); - free(sortedClients); } @@ -405,7 +399,6 @@ void Cmd_Score_f(edict_t *ent) { ent->client->showinventory = false; - gi.dprintf("-#-# Layout: %d\n", ent->client->layout); if (ent->client->layout == LAYOUT_MENU) PMenu_Close(ent); @@ -903,10 +896,7 @@ void HUD_SpectatorUpdate(edict_t *clent) gclient_t *team1_players[6]; gclient_t *team2_players[6]; - gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); - if (sortedClients == NULL) { - gi.dprintf("%s: Could not allocate memory for sortedClients\n", __func__); - } + gclient_t *sortedClients[MAX_CLIENTS]; int totalClients, team1Clients, team2Clients; @@ -946,7 +936,6 @@ void HUD_SpectatorUpdate(edict_t *clent) continue; } } - free(sortedClients); // team 1 (red team) Ghud_SetFlags(clent, hud[h_team_l], 0); diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 69c78927b..504f1b0e5 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -318,8 +318,7 @@ void Cmd_Stats_f (edict_t *targetent, char *arg) void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) { char string[2048]; - gclient_t *cl; - gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); + gclient_t *sortedClients[MAX_CLIENTS], *cl; int maxsize = 1000, i, line_y; int totalClients, secs, shots; double accuracy, fpm; @@ -327,10 +326,6 @@ void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) int totalscore[TEAM_TOP] = {0}; int name_pos[TEAM_TOP] = {0}; - if (sortedClients == NULL) { - gi.dprintf("%s: Couldn't allocate memory for sortedClients\n", __func__); - } - totalClients = G_SortedClients(sortedClients); ent->client->ps.stats[STAT_TEAM_HEADER] = level.pic_teamtag; @@ -464,7 +459,6 @@ void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer) gi.WriteByte(svc_layout); gi.WriteString(string); - free(sortedClients); } void Cmd_Statmode_f(edict_t* ent) @@ -1142,19 +1136,13 @@ LogEndMatchStats void LogEndMatchStats(void) { int i; - gclient_t *cl; - gclient_t **sortedClients = malloc(game.csr.maxclients * sizeof(gclient_t*)); + gclient_t *sortedClients[MAX_CLIENTS], *cl; int totalClients; - if (sortedClients == NULL) { - gi.dprintf("%s: Couldn't allocate memory for sortedClients\n", __FUNCTION__); - } - totalClients = G_SortedClients(sortedClients); // Check if there's an AI bot in the game, if so, do nothing if (game.ai_ent_found) { - free(sortedClients); return; } @@ -1219,7 +1207,6 @@ void LogEndMatchStats(void) num_ghost_players = 0; // Be free, little dummy entity G_FreeEdict(ent); - free(sortedClients); } } #endif From 336d2496337ce49ecd45b22752e1d2e0c837fc44 Mon Sep 17 00:00:00 2001 From: Reki Date: Wed, 7 Feb 2024 23:48:38 -0500 Subject: [PATCH 016/974] Protocol : Clean up some missing cases or inconsistency --- inc/common/protocol.h | 13 +++++++------ src/client/demo.c | 10 ---------- src/client/parse.c | 31 ++++++++++++++++++++++++------- src/server/server.h | 4 ++-- src/server/user.c | 7 ++++--- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 4850471b3..ef25aaddc 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -55,15 +55,16 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PROTOCOL_VERSION_MVD_CURRENT 2011 // r2894 -#define PROTOCOL_VERSION_AQTION_MINIMUM 3011 // minimum is equivalent to PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS +#define PROTOCOL_VERSION_AQTION_MINIMUM 3011 // minimum is equivalent to PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS #ifndef AQTION_EXTENSION #define PROTOCOL_VERSION_AQTION_CURRENT 3011 #else -#define PROTOCOL_VERSION_AQTION_GHUD 3012 // game dll defined hud elements -#define PROTOCOL_VERSION_AQTION_CVARSYNC 3013 -#define PROTOCOL_VERSION_AQTION_GHUD2 3014 -#define PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS 3015 -#define PROTOCOL_VERSION_AQTION_CURRENT 3015 +#define PROTOCOL_VERSION_AQTION_GHUD 3012 // game dll defined hud elements +#define PROTOCOL_VERSION_AQTION_CVARSYNC 3013 +#define PROTOCOL_VERSION_AQTION_GHUD2 3014 +#define PROTOCOL_VERSION_AQTION_CINEMATICS 3015 +#define PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS 3016 +#define PROTOCOL_VERSION_AQTION_CURRENT 3016 #endif diff --git a/src/client/demo.c b/src/client/demo.c index a6ecc81ab..31840c692 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -416,13 +416,8 @@ static void CL_Record_f(void) // send the serverdata MSG_WriteByte(svc_serverdata); - #if USE_AQTION - if (cl.csr.extended) - MSG_WriteLong(PROTOCOL_VERSION_AQTION); - #else if (cl.csr.extended) MSG_WriteLong(PROTOCOL_VERSION_EXTENDED); - #endif else MSG_WriteLong(min(cls.serverProtocol, PROTOCOL_VERSION_DEFAULT)); MSG_WriteLong(cl.servercount); @@ -1186,13 +1181,8 @@ demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info) goto fail; } c = MSG_ReadLong(); - #if USE_AQTION - if (c == PROTOCOL_VERSION_AQTION) { - csr = &cs_remap_new; - #else if (c == PROTOCOL_VERSION_EXTENDED) { csr = &cs_remap_new; - #endif } else if (c < PROTOCOL_VERSION_OLD || c > PROTOCOL_VERSION_DEFAULT) { goto fail; } diff --git a/src/client/parse.c b/src/client/parse.c index 3b4dd1cf0..df37daf7c 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -566,7 +566,7 @@ static void CL_ParseServerData(void) cls.serverProtocol, protocol); } // BIG HACK to let demos from release work with the 3.0x patch!!! - if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_AQTION) { + if (protocol == PROTOCOL_VERSION_EXTENDED) { cl.csr = cs_remap_new; protocol = PROTOCOL_VERSION_DEFAULT; } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_AQTION) { @@ -651,12 +651,15 @@ static void CL_ParseServerData(void) "Current client version is %d.", i, PROTOCOL_VERSION_AQTION_CURRENT); } Com_DPrintf("Using minor AQTION protocol version %d\n", i); - cls.protocolVersion = i; - i = MSG_ReadByte(); - Com_DPrintf("AQTION server state %d\n", i); - cl.serverstate = i; - - i = MSG_ReadWord(); + cls.protocolVersion = i; + i = MSG_ReadByte(); + if (1) {// this is always true, since AQtion is a superset of PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT //(cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_SERVER_STATE) { + Com_DPrintf("Q2PRO server state %d\n", i); + cl.serverstate = i; + cinematic = i == ss_pic || i == ss_cinematic; + } + if (cls.protocolVersion >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { + i = MSG_ReadWord(); if (i & Q2PRO_PF_STRAFEJUMP_HACK) { Com_DPrintf("Q2PRO strafejump hack enabled\n"); cl.pmp.strafehack = true; @@ -673,6 +676,20 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO protocol extensions enabled\n"); cl.csr = cs_remap_new; } + } else { + if (MSG_ReadByte()) { + Com_DPrintf("Q2PRO strafejump hack enabled\n"); + cl.pmp.strafehack = true; + } + if (MSG_ReadByte()) { + Com_DPrintf("Q2PRO QW mode enabled\n"); + PmoveEnableQW(&cl.pmp); + } + if (MSG_ReadByte()) { + Com_DPrintf("Q2PRO waterjump hack enabled\n"); + cl.pmp.waterhack = true; + } + } cl.esFlags |= MSG_ES_UMASK | MSG_ES_LONGSOLID; if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN) { diff --git a/src/server/server.h b/src/server/server.h index c929c05ec..3efcb0467 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -190,8 +190,8 @@ typedef struct { sv.state == ss_game && \ EDICT_POOL(c, e)->solid == SOLID_BSP) #define CLIENT_COMPATIBLE(csr, c) \ - (!(csr)->extended || (((c)->protocol == PROTOCOL_VERSION_Q2PRO || (c)->protocol == PROTOCOL_VERSION_AQTION) && \ - (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS)) + (!(csr)->extended || (((c)->protocol == PROTOCOL_VERSION_Q2PRO && (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) \ + || ((c)->protocol == PROTOCOL_VERSION_AQTION && (c)->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS))) #define ENT_EXTENSION(csr, ent) ((csr)->extended ? &(ent)->x : NULL) diff --git a/src/server/user.c b/src/server/user.c index ef3e5e2ac..6c2ee33d0 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -420,11 +420,11 @@ void SV_New_f(void) break; case PROTOCOL_VERSION_AQTION: MSG_WriteShort(sv_client->version); - if (sv.state == ss_cinematic && sv_client->version < PROTOCOL_VERSION_Q2PRO_CINEMATICS) + if (sv.state == ss_cinematic && sv_client->version < PROTOCOL_VERSION_AQTION_CINEMATICS) MSG_WriteByte(ss_pic); else MSG_WriteByte(sv.state); - if (sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) { + if (sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { MSG_WriteShort(q2pro_protocol_flags()); } else { MSG_WriteByte(sv_client->pmp.strafehack); @@ -482,7 +482,8 @@ void SV_New_f(void) if (sv_client->netchan.type == NETCHAN_OLD) { write_configstrings(); write_baselines(); - } else if (sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) { + } else if ((sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) || + (sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS)) { write_configstring_stream(); write_baseline_stream(); } else { From 72a17bc82f1011801824e4addccd2d03c694fb8c Mon Sep 17 00:00:00 2001 From: Reki Date: Thu, 8 Feb 2024 00:16:30 -0500 Subject: [PATCH 017/974] Protocol : Fixed mismatched clientnum parsing, added PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT --- inc/common/protocol.h | 7 ++++--- src/client/parse.c | 20 +++++++++++++------- src/server/entities.c | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index ef25aaddc..8be9bfd6d 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -62,9 +62,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PROTOCOL_VERSION_AQTION_GHUD 3012 // game dll defined hud elements #define PROTOCOL_VERSION_AQTION_CVARSYNC 3013 #define PROTOCOL_VERSION_AQTION_GHUD2 3014 -#define PROTOCOL_VERSION_AQTION_CINEMATICS 3015 -#define PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS 3016 -#define PROTOCOL_VERSION_AQTION_CURRENT 3016 +#define PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT 3015 +#define PROTOCOL_VERSION_AQTION_CINEMATICS 3016 +#define PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS 3017 +#define PROTOCOL_VERSION_AQTION_CURRENT 3017 #endif diff --git a/src/client/parse.c b/src/client/parse.c index df37daf7c..3e53157e0 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -347,13 +347,19 @@ static void CL_ParseFrame(int extrabits) Com_LPrintf(PRINT_DEVELOPER, "\n"); } #endif - // parse clientNum - if (extraflags & EPS_CLIENTNUM) { - frame.clientNum = MSG_ReadByte(); - } - else if (oldframe) { - frame.clientNum = oldframe->clientNum; - } + // parse clientNum + if (extraflags & EPS_CLIENTNUM) { + if (cls.protocolVersion < PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT) { + frame.clientNum = MSG_ReadByte(); + } else { + frame.clientNum = MSG_ReadShort(); + } + if (!VALIDATE_CLIENTNUM(&cl.csr, frame.clientNum)) { + Com_Error(ERR_DROP, "%s: bad clientNum", __func__); + } + } else if (oldframe) { + frame.clientNum = oldframe->clientNum; + } } else if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); #if USE_DEBUG diff --git a/src/server/entities.c b/src/server/entities.c index 0200ba750..4ed790be9 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -456,7 +456,7 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) // delta encode the clientNum if ((oldframe ? oldframe->clientNum : 0) != frame->clientNum) { extraflags |= EPS_CLIENTNUM; - if (client->version < PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT) { + if (client->version < PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT) { MSG_WriteByte(frame->clientNum); } else { MSG_WriteShort(frame->clientNum); From b84c14b25372b62c2369e23cdbe9f8fe59e2a5f3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 8 Feb 2024 13:19:57 +0300 Subject: [PATCH 018/974] Make lightgrid points unsigned. --- inc/common/bsp.h | 2 +- src/common/bsp.c | 2 +- src/refresh/world.c | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 34b57bfb6..45b954d3a 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -331,7 +331,7 @@ void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, m void BSP_TransformedLightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, mnode_t *headnode, int nolm_mask, const vec3_t origin, const vec3_t angles); -lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, int32_t point[3]); +lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]); #endif byte *BSP_ClusterVis(bsp_t *bsp, byte *mask, int cluster, int vis); diff --git a/src/common/bsp.c b/src/common/bsp.c index 016aaf949..37999d74f 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -1045,7 +1045,7 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) #define FLAG_OCCLUDED BIT(30) #define FLAG_LEAF BIT(31) -lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, int32_t point[3]) +lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]) { uint32_t nodenum = grid->rootnode; diff --git a/src/refresh/world.c b/src/refresh/world.c index fb7117c2f..2cc32cb97 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -73,7 +73,7 @@ void GL_SampleLightPoint(vec3_t color) static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t color) { vec3_t point, avg; - int point_i[3]; + uint32_t point_i[3]; vec3_t samples[8]; int i, j, mask, numsamples; @@ -88,18 +88,18 @@ static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t colo VectorClear(avg); for (i = mask = numsamples = 0; i < 8; i++) { - int32_t tmp[3]; + uint32_t tmp[3]; tmp[0] = point_i[0] + ((i >> 0) & 1); tmp[1] = point_i[1] + ((i >> 1) & 1); tmp[2] = point_i[2] + ((i >> 2) & 1); - VectorClear(samples[i]); - lightgrid_sample_t *s = BSP_LookupLightgrid(grid, tmp); if (!s) continue; + VectorClear(samples[i]); + for (j = 0; j < grid->numstyles && s->style != 255; j++, s++) { lightstyle_t *style = LIGHT_STYLE(s->style); VectorMA(samples[i], style->white, s->rgb, samples[i]); From d326fb396c251a16303bb0670475fde62fefde78 Mon Sep 17 00:00:00 2001 From: ReKTeK Date: Fri, 9 Feb 2024 11:14:12 +0800 Subject: [PATCH 019/974] Added Windows MSVC Build Support --- AQtion/AQtion.sln | 31 + AQtion/AQtion.vcxproj | 510 +++++++++++ AQtion/AQtion.vcxproj.filters | 1265 ++++++++++++++++++++++++++++ AQtion/AQtion.vcxproj.user | 11 + AQtion/README.md | 14 + AQtion/github_pull_update.bat | 6 + AQtion/meson_debug_build.bat | 7 + AQtion/meson_debug_clean.bat | 3 + AQtion/meson_debug_configure.bat | 3 + AQtion/meson_debug_rebuild.bat | 8 + AQtion/meson_release_build.bat | 7 + AQtion/meson_release_clean.bat | 3 + AQtion/meson_release_configure.bat | 4 + AQtion/meson_release_rebuild.bat | 8 + 14 files changed, 1880 insertions(+) create mode 100644 AQtion/AQtion.sln create mode 100644 AQtion/AQtion.vcxproj create mode 100644 AQtion/AQtion.vcxproj.filters create mode 100644 AQtion/AQtion.vcxproj.user create mode 100644 AQtion/README.md create mode 100644 AQtion/github_pull_update.bat create mode 100644 AQtion/meson_debug_build.bat create mode 100644 AQtion/meson_debug_clean.bat create mode 100644 AQtion/meson_debug_configure.bat create mode 100644 AQtion/meson_debug_rebuild.bat create mode 100644 AQtion/meson_release_build.bat create mode 100644 AQtion/meson_release_clean.bat create mode 100644 AQtion/meson_release_configure.bat create mode 100644 AQtion/meson_release_rebuild.bat diff --git a/AQtion/AQtion.sln b/AQtion/AQtion.sln new file mode 100644 index 000000000..e1bd941d3 --- /dev/null +++ b/AQtion/AQtion.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AQtion", "AQtion.vcxproj", "{4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Debug|x64.ActiveCfg = Debug|x64 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Debug|x64.Build.0 = Debug|x64 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Debug|x86.ActiveCfg = Debug|Win32 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Debug|x86.Build.0 = Debug|Win32 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Release|x64.ActiveCfg = Release|x64 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Release|x64.Build.0 = Release|x64 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Release|x86.ActiveCfg = Release|Win32 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60B8F3DF-61A0-4888-A375-00AA8FC47296} + EndGlobalSection +EndGlobal diff --git a/AQtion/AQtion.vcxproj b/AQtion/AQtion.vcxproj new file mode 100644 index 000000000..8a2b8b292 --- /dev/null +++ b/AQtion/AQtion.vcxproj @@ -0,0 +1,510 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + {4A622773-BA18-4BAA-9CE1-B05EBF1BCF26} + Win32Proj + + + + Makefile + true + v143 + + + Makefile + false + v143 + + + Makefile + true + v143 + + + Makefile + false + v143 + + + + + + + + + + + + + + + + + + + + + meson_debug_build.bat + $(Configuration)\q2pro.exe + meson_debug_clean.bat + meson_debug_rebuild.bat + _DEBUG;$(NMakePreprocessorDefinitions);HAVE_CONFIG_H + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Debug\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Debug\ + $(VC_SourcePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Debug\ + + + + + meson compile -C $(Configuration) + $(Configuration)\q2pro.exe + meson compile -C $(Configuration) --clean + meson compile -C $(Configuration) --clean && meson compile -C $(Configuration) + WIN32;_DEBUG;$(NMakePreprocessorDefinitions) + + + meson compile -C $(Configuration) + $(Configuration)\q2pro.exe + meson compile -C $(Configuration) --clean + meson compile -C $(Configuration) --clean && meson compile -C $(Configuration) + WIN32;NDEBUG;$(NMakePreprocessorDefinitions) + + + meson_release_build.bat + $(Configuration)\q2pro.exe + meson_release_clean.bat + meson_release_rebuild.bat + NDEBUG;$(NMakePreprocessorDefinitions);HAVE_CONFIG_H + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Release\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Release\ + $(VC_SourcePath);$(ProjectDir)..\inc\;$(ProjectDir)..\AQtion\Release\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AQtion/AQtion.vcxproj.filters b/AQtion/AQtion.vcxproj.filters new file mode 100644 index 000000000..2bfc54010 --- /dev/null +++ b/AQtion/AQtion.vcxproj.filters @@ -0,0 +1,1265 @@ + + + + + {c61a0d2c-553c-4c30-99fc-d4b4a48d3430} + + + {16604d31-c8bd-4600-bb35-e75932148736} + + + {3bfe056e-fc94-4586-b6e5-2d61c3d94d75} + + + {0d512c66-3f69-40fe-9eac-4b5f2e83acc4} + + + {4e7d39ec-e832-4f83-b40b-0fe0f25994ec} + + + {fa2692f1-6de4-47a1-980f-1e2ee151d1c7} + + + {004de5cb-77d3-45f2-b691-2381e859416e} + + + {0d262dbb-8f78-4583-8121-b7e741652f9e} + + + {ac4ebd2e-ad94-4ccf-80e3-700df2f47d90} + + + {47522520-13a7-458d-8f4b-ab764e36c3c6} + + + {7b20a963-4c4a-4c24-8f3d-ef02f90b89cc} + + + {38822057-a242-4f4b-8f8a-a23894a14326} + + + {1a1d1677-7b4a-45ef-b9d0-732e7a94e1c9} + + + {0841e986-1b8d-4b96-bfd8-8a8b4aaa4a7c} + + + {801fdf8b-2d69-4c7f-9e10-c9d289c67bf3} + + + {d852dbcc-a2b2-4d37-83b8-6f34c6f1a31f} + + + {b4c8c703-9e90-429c-b693-452e74910af6} + + + {2cc6f960-2e93-4fbe-a1d6-d63436e8f53a} + + + {28be1289-4e68-45eb-b56b-45d58e2e76e9} + + + {4f562c17-8d3b-48ba-ace5-8a7d6170cac8} + + + {d20f24af-8a7e-4dbe-b282-c3b225c892b7} + + + {e0e79ec0-2f49-4838-932e-57f4828eddb8} + + + {91f6f017-c5a1-4b28-9a8c-9fdc1b0f6913} + + + {1cac76d3-ae62-4ed6-900b-c6cd496ddb6b} + + + {e33ae617-0aff-45af-bb30-a2aecce03999} + + + {f527253a-ae71-4b6e-a74a-28d5196b77b7} + + + {b64b950a-f8d6-4edd-8a1d-8553cce7f68c} + + + {f0764e12-fe39-4fa7-a46f-798d44e03d9b} + + + {afdc68cf-b44a-495f-ba71-973712d5da08} + + + {e8a3f168-f2e8-4244-9d28-a2db6a274eb8} + + + {da04e258-7bcf-43e0-8338-b3ccc368b76b} + + + {c4cb9d7f-88de-4bde-9ebf-103302ec9c60} + + + {614cfeeb-ee49-4f30-8238-ca6f95ecb179} + + + {131572c4-f889-4c76-a267-8b1c52ca5d64} + + + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\addons\tngbot + + + src\shared + + + src\shared + + + src\client\ui + + + src\client\ui + + + src\client\ui + + + src\client\ui + + + src\client\ui + + + src\client\ui + + + src\client\ui + + + src\client\sound + + + src\client\sound + + + src\client\sound + + + src\client\sound + + + src\client\sound + + + src\client\sound + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\client + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common + + + src\common\net + + + src\common\net + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server + + + src\server\mvd + + + src\server\mvd + + + src\server\mvd + + + src\unix\video + + + src\unix\video + + + src\unix\video + + + src\unix\video\keytables + + + src\unix\video\keytables + + + src\unix\sound + + + src\unix + + + src\unix + + + src\unix + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + src\windows + + + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + src\action\acebot + + + inc\shared + + + inc\shared + + + inc\shared + + + inc\shared + + + inc\shared + + + inc\shared + + + inc\shared + + + inc\client + + + inc\client + + + inc\client + + + inc\client + + + inc\client + + + inc\client\sound + + + inc\client\sound + + + inc\common\net + + + inc\common\net + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\common + + + inc\format + + + inc\format + + + inc\format + + + inc\format + + + inc\format + + + inc\format + + + inc\format + + + inc\refresh + + + inc\server + + + inc\server\mvd + + + inc\server\mvd + + + inc\system + + + inc\system + + + inc\system + + + src\client\ui + + + src\client\sound + + + src\client\sound + + + src\client + + + src\common + + + src\common\net + + + src\common\net + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\game + + + src\refresh + + + src\refresh + + + src\refresh + + + src\refresh + + + src\server + + + src\server\mvd + + + src\unix\video\keytables + + + src\unix\video\keytables + + + src\unix + + + src\windows + + + src\windows + + + AQtion + + + inc\shared + + + + + src\client\ui + + + src\game + + + src\unix\video + + + src\unix\res + + + src\unix\res + + + src\unix\res + + + src\unix\res + + + src\unix + + + src\windows + + + + + + + + + AQtion + + + AQtion + + + AQtion + + + AQtion + + + AQtion + + + AQtion + + + AQtion + + + AQtion + + + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + + + src\windows\res + + + src\windows\res + + + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + src\windows\res + + + + + + \ No newline at end of file diff --git a/AQtion/AQtion.vcxproj.user b/AQtion/AQtion.vcxproj.user new file mode 100644 index 000000000..d0cceeba0 --- /dev/null +++ b/AQtion/AQtion.vcxproj.user @@ -0,0 +1,11 @@ + + + + AQtion\AQtion\q2pro.exe + WindowsLocalDebugger + + + AQtion\AQtion\q2pro.exe + WindowsLocalDebugger + + \ No newline at end of file diff --git a/AQtion/README.md b/AQtion/README.md new file mode 100644 index 000000000..8ee539009 --- /dev/null +++ b/AQtion/README.md @@ -0,0 +1,14 @@ +Building for Microsoft Visual Studio +===== + +AQtion now supports MSVC 2022. + +First time use, create the build directories by running: +- meson_debug_configure.bat +- meson_release_configure.bat +This should create a AQtion/Debug and AQtion/Release directory. + +Open AQtion.sln in Microsoft Visual Studio 2022 + +Compiling outputs to AQtion/AQtion/ +You can copy your game assets to this directory to debug in MSVC \ No newline at end of file diff --git a/AQtion/github_pull_update.bat b/AQtion/github_pull_update.bat new file mode 100644 index 000000000..6c8b4dbf7 --- /dev/null +++ b/AQtion/github_pull_update.bat @@ -0,0 +1,6 @@ +@echo off +cd.. +echo Update (pull) from github https://github.com/actionquake/q2pro/tree/sync/aq2-tng +pause +git pull +pause \ No newline at end of file diff --git a/AQtion/meson_debug_build.bat b/AQtion/meson_debug_build.bat new file mode 100644 index 000000000..9eae56515 --- /dev/null +++ b/AQtion/meson_debug_build.bat @@ -0,0 +1,7 @@ +REM Meson DEBUG: build and copy files +cd.. +meson compile -C AQtion\Debug +xcopy /y AQtion\Debug\q2pro.exe AQtion\Debug\..\AQtion\ +xcopy /y AQtion\Debug\q2proded.exe AQtion\Debug\..\AQtion\ +xcopy /y AQtion\Debug\gamex86_64.dll AQtion\Debug\..\AQtion\action\ +pause \ No newline at end of file diff --git a/AQtion/meson_debug_clean.bat b/AQtion/meson_debug_clean.bat new file mode 100644 index 000000000..45be61bc8 --- /dev/null +++ b/AQtion/meson_debug_clean.bat @@ -0,0 +1,3 @@ +REM Meson DEBUG: clean +cd.. +meson compile -C AQtion\Debug --clean \ No newline at end of file diff --git a/AQtion/meson_debug_configure.bat b/AQtion/meson_debug_configure.bat new file mode 100644 index 000000000..560a9f9dc --- /dev/null +++ b/AQtion/meson_debug_configure.bat @@ -0,0 +1,3 @@ +REM Meson DEBUG: Build a 'Debug' build directory +meson -Dwrap_mode=forcefallback -Ddebug=true -Doptimization=0 AQtion\Debug +pause \ No newline at end of file diff --git a/AQtion/meson_debug_rebuild.bat b/AQtion/meson_debug_rebuild.bat new file mode 100644 index 000000000..854011e0a --- /dev/null +++ b/AQtion/meson_debug_rebuild.bat @@ -0,0 +1,8 @@ +REM Meson DEBUG: clean, build, and copy files +cd.. +meson compile -C AQtion\Debug --clean +meson compile -C AQtion\Debug +xcopy /y AQtion\Debug\q2pro.exe AQtion\Debug\..\AQtion\ +xcopy /y AQtion\Debug\q2proded.exe AQtion\Debug\..\AQtion\ +xcopy /y AQtion\Debug\gamex86_64.dll AQtion\Debug\..\AQtion\action\ +pause \ No newline at end of file diff --git a/AQtion/meson_release_build.bat b/AQtion/meson_release_build.bat new file mode 100644 index 000000000..3036bdb97 --- /dev/null +++ b/AQtion/meson_release_build.bat @@ -0,0 +1,7 @@ +REM Meson RELEASE: build and copy files +cd.. +meson compile -C AQtion\Release +xcopy /y AQtion\Release\q2pro.exe AQtion\Release\..\AQtion\ +xcopy /y AQtion\Release\q2proded.exe AQtion\Release\..\AQtion\ +xcopy /y AQtion\Release\gamex86_64.dll AQtion\Release\..\AQtion\action\ +pause \ No newline at end of file diff --git a/AQtion/meson_release_clean.bat b/AQtion/meson_release_clean.bat new file mode 100644 index 000000000..9f131687b --- /dev/null +++ b/AQtion/meson_release_clean.bat @@ -0,0 +1,3 @@ +REM Meson RELEASE: clean +cd.. +meson compile -C AQtion\Release --clean \ No newline at end of file diff --git a/AQtion/meson_release_configure.bat b/AQtion/meson_release_configure.bat new file mode 100644 index 000000000..d73ec8dff --- /dev/null +++ b/AQtion/meson_release_configure.bat @@ -0,0 +1,4 @@ +REM Meson RELEASE: Build a 'Release' build directory +cd.. +meson -Dwrap_mode=forcefallback AQtion\Release --buildtype=debugoptimized +pause \ No newline at end of file diff --git a/AQtion/meson_release_rebuild.bat b/AQtion/meson_release_rebuild.bat new file mode 100644 index 000000000..5302696ee --- /dev/null +++ b/AQtion/meson_release_rebuild.bat @@ -0,0 +1,8 @@ +REM Meson RELEASE: clean, build, and copy files +cd.. +meson compile -C AQtion\Release --clean +meson compile -C AQtion\Release +xcopy /y AQtion\Release\q2pro.exe AQtion\Release\..\AQtion\ +xcopy /y AQtion\Release\q2proded.exe AQtion\Release\..\AQtion\ +xcopy /y AQtion\Release\gamex86_64.dll AQtion\Release\..\AQtion\action\ +pause \ No newline at end of file From f83e4a503b5a5c1491f320ebaafabd22944a32d7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 8 Feb 2024 23:08:00 -0500 Subject: [PATCH 020/974] Fixed a rogue free() --- meson.build | 2 -- src/action/acesrc/acebot.h | 2 +- src/action/acesrc/acebot_items.c | 2 +- src/action/acesrc/acebot_nodes.c | 2 +- src/action/p_hud.c | 1 - 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index cee571f60..c70f62e99 100644 --- a/meson.build +++ b/meson.build @@ -543,8 +543,6 @@ executable('q2proded', common_src, server_src, install: system_wide, ) -## Disabling building gamelib for now, no need - shared_library('game' + cpu, action_src, name_prefix: '', dependencies: game_deps, diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index cab9fb76f..11dd82bd2 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -203,7 +203,7 @@ extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game // extern decs extern node_t nodes[MAX_NODES]; -extern item_table_t item_table[MAX_EDICTS_OLD]; +extern item_table_t item_table[MAX_EDICTS]; extern qboolean debug_mode; extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" extern int numnodes; diff --git a/src/action/acesrc/acebot_items.c b/src/action/acesrc/acebot_items.c index a61a5705e..a633ffdd1 100644 --- a/src/action/acesrc/acebot_items.c +++ b/src/action/acesrc/acebot_items.c @@ -56,7 +56,7 @@ int num_players = 0; int num_items = 0; -item_table_t item_table[MAX_EDICTS_OLD]; +item_table_t item_table[MAX_EDICTS]; edict_t *players[MAX_CLIENTS]; // pointers to all players in the game /////////////////////////////////////////////////////////////////////// diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index d548a2a8d..8788215b9 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1374,7 +1374,7 @@ void ACEND_LoadNodes(void) fclose(pIn); // Raptor007: Do not trust saved pointers! - for(i=0;i Date: Fri, 9 Feb 2024 12:29:47 +0800 Subject: [PATCH 021/974] Fixed LTK softlocking when picking an unused name. Fixed readme formatting. --- AQtion/README.md | 2 ++ src/action/g_spawn.c | 3 +++ 2 files changed, 5 insertions(+) diff --git a/AQtion/README.md b/AQtion/README.md index 8ee539009..b97078cd6 100644 --- a/AQtion/README.md +++ b/AQtion/README.md @@ -6,9 +6,11 @@ AQtion now supports MSVC 2022. First time use, create the build directories by running: - meson_debug_configure.bat - meson_release_configure.bat + This should create a AQtion/Debug and AQtion/Release directory. Open AQtion.sln in Microsoft Visual Studio 2022 Compiling outputs to AQtion/AQtion/ + You can copy your game assets to this directory to debug in MSVC \ No newline at end of file diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 7b01afc03..fb00df5de 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1387,6 +1387,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn attract_mode_bot_check(); } + // Clear LTK bot names + LTKClearBotNames(); + #endif } From 44a99ef2d384573e4e9d9a0a375b65c30687cc2e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 8 Feb 2024 23:52:19 -0500 Subject: [PATCH 022/974] Some warnings fixed, made LTKsetBotNameNew better --- src/action/a_game.c | 4 ++-- src/action/a_radio.c | 2 +- src/action/acesrc/acebot_spawn.c | 20 ++++++++++++++------ src/action/cgf_sfx_glass.c | 6 +++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index fd06e57b7..07eedd1ee 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -88,7 +88,7 @@ int motd_num_lines; * the "###" designator at end of sections. * -Fireblade */ -void ReadConfigFile() +void ReadConfigFile(void) { FILE *config_file; char buf[MAX_STR_LEN], reading_section[MAX_STR_LEN], inipath[MAX_STR_LEN]; @@ -174,7 +174,7 @@ void ReadConfigFile() fclose(config_file); } -void ReadMOTDFile() +void ReadMOTDFile(void) { FILE *motd_file; char buf[1000]; diff --git a/src/action/a_radio.c b/src/action/a_radio.c index 8e2e8b636..53b402664 100644 --- a/src/action/a_radio.c +++ b/src/action/a_radio.c @@ -122,7 +122,7 @@ radio_msg_t globalRadio[] = { {"radio/female/rdeath.wav", 30, 0} }; -void PrecacheRadioSounds () +void PrecacheRadioSounds (void) { int i; char path[MAX_QPATH]; diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 3c1fde943..9d6503c69 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -856,16 +856,24 @@ char *aq2names[] = { // END AQ2World Staff Names // +edict_t ltknames; // New AQ2World team bot names (am_newnames 1) void LTKsetBotNameNew(char *bot_name) { - edict_t ltknames; int randomname = 0; - // /* TODO: Free up previously used names to be reused. - // This may cause duplicates in-game though. - // Fix this at some point. Otherwise the game - // runs out of valid values and will softlock - // when generating LTK bots. + // Check if all names have been used + bool allNamesUsed = true; + for (int i = 0; i < AQ2WTEAMSIZE; i++) { + if (!ltknames.newnameused[i]) { + allNamesUsed = false; + break; + } + } + + // If all names have been used, reset the newnameused array + if (allNamesUsed) { + LTKClearBotNames(); + } do { diff --git a/src/action/cgf_sfx_glass.c b/src/action/cgf_sfx_glass.c index 6ef92a632..360fdb57d 100644 --- a/src/action/cgf_sfx_glass.c +++ b/src/action/cgf_sfx_glass.c @@ -115,7 +115,7 @@ extern "C" void -CGF_SFX_InstallGlassSupport () +CGF_SFX_InstallGlassSupport (void) { breakableglass = gi.cvar ("breakableglass", "0", 0); glassfragmentlimit = gi.cvar ("glassfragmentlimit", "30", 0); @@ -123,7 +123,7 @@ CGF_SFX_InstallGlassSupport () int -CGF_SFX_IsBreakableGlassEnabled () +CGF_SFX_IsBreakableGlassEnabled (void) { // returns whether breakable glass is enabled (cvar) and allowed (dm mode) return breakableglass->value; @@ -635,7 +635,7 @@ CGF_SFX_AttachDecalToGlass (edict_t * aGlassPane, edict_t * aDecal) void -CGF_SFX_RebuildAllBrokenGlass () +CGF_SFX_RebuildAllBrokenGlass (void) { // iterate over all func_explosives edict_t *glass; From 698f93f0bf8d1c602f4020275d3888d4f9e6447d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Feb 2024 00:42:24 -0500 Subject: [PATCH 023/974] Experimenting with returning to warmup phase in matchmode unready state --- src/action/a_team.c | 15 +++++++++++++-- src/action/acesrc/acebot_spawn.c | 11 ++++++++--- src/action/g_local.h | 3 +++ src/action/g_main.c | 3 +++ src/action/g_save.c | 3 +++ src/server/commands.c | 10 ++++++++-- 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 724a4f186..ff44ebffa 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2287,7 +2287,11 @@ void RunWarmup (void) int i, dead; edict_t *ent; - if (!warmup->value || (matchmode->value && level.matchTime > 0) || team_round_going || lights_camera_action || (team_round_countdown > 0 && team_round_countdown <= 101)) + if (!warmup->value || team_round_going || lights_camera_action || (team_round_countdown > 0 && team_round_countdown <= 101)) + return; + + // Allows re-warmup if teams unready during matchmode + if (!warmup_unready->value && (matchmode->value && level.matchTime > 0)) return; if (!in_warmup) @@ -2313,7 +2317,10 @@ void RunWarmup (void) ent->client->latched_buttons = 0; PutClientInServer(ent); AddToTransparentList(ent); - gi.centerprintf(ent, "WARMUP"); + if (matchmode->value && level.matchTime > 0) + gi.centerprintf(ent, "TEAMS NOT READY: RETURNING TO WARMUP"); + else + gi.centerprintf(ent, "WARMUP"); } } #if USE_AQTION @@ -2789,6 +2796,8 @@ int CheckTeamRules (void) } team_round_going = team_round_countdown = team_game_going = 0; MakeAllLivePlayersObservers (); + if (matchmode->value && warmup_unready->value) + RunWarmup(); } } else if(use_tourney->value) @@ -2975,6 +2984,8 @@ int CheckTeamRules (void) team_round_going = team_round_countdown = team_game_going = 0; MakeAllLivePlayersObservers(); + if (matchmode->value && warmup_unready->value) + RunWarmup(); /* try to restart the game */ while (CheckForUnevenTeams( NULL )); diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 9d6503c69..21c8ef54d 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -847,8 +847,8 @@ char *aq2names[] = { "Igor[ROCK].bot", "Suislide.bot", "Bartender.bot", "Fex.bot", "Shagg.bot", "Black Angel.bot", "Rookie.bot", - "Fireblade>beep", "Cail>beep", "Gooseman>beep", "Ace12GA>beep", - "BlackMonk>beep", "hal9k>beep", "Fool Killer>beep", "Inghaw>beep", + "Fireblade<<", "Cail<<", "Gooseman<<", "Ace12GA<<", + "BlackMonk<<", "hal9k<<", "Fool Killer<<", "Inghaw<<", "_NME_GreyDeath", "_NME_Ellusion", "_NME_Deathwatch", "_NME_Freud", "_NME_slicer", "_NME_JBravo", "_NME_Elviz" @@ -861,8 +861,9 @@ edict_t ltknames; void LTKsetBotNameNew(char *bot_name) { int randomname = 0; + // Check if all names have been used - bool allNamesUsed = true; + qboolean allNamesUsed = true; for (int i = 0; i < AQ2WTEAMSIZE; i++) { if (!ltknames.newnameused[i]) { allNamesUsed = false; @@ -923,6 +924,10 @@ void LTKsetBotName( char *bot_name ) } } + +//==================================== +// LTKCLearBotNames -- Reset the bot name array +//==================================== void LTKClearBotNames(void) { edict_t ltknames; int i, j; diff --git a/src/action/g_local.h b/src/action/g_local.h index bc9d66959..5c08d41a0 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1258,6 +1258,9 @@ extern cvar_t *timedmsgs; extern cvar_t *mm_captain_teamname; extern cvar_t *sv_killgib; +// 2024 +extern cvar_t *warmup_unready; + #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); extern int (*engine_Client_GetProtocol)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index 41510bdb2..b43b95091 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -527,6 +527,9 @@ cvar_t *timedmsgs; // Toggles timed messages cvar_t *mm_captain_teamname; // Toggles if we want to use the captain's name for the team in matchmode cvar_t *sv_killgib; // Gibs on 'kill' command +// 2024 +cvar_t *warmup_unready; // Toggles warmup if captains unready + #if AQTION_EXTENSION cvar_t *use_newirvision; cvar_t *use_indicators; diff --git a/src/action/g_save.c b/src/action/g_save.c index 86aa40214..03eba2a81 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -641,6 +641,9 @@ void InitGame( void ) mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", 0); sv_killgib = gi.cvar("sv_killgib", "0", 0); + // 2024 + warmup_unready = gi.cvar("warmup_unready", "0", 0); + // new AQtion Extension cvars #if AQTION_EXTENSION use_newirvision = gi.cvar("use_newirvision", "1", 0); diff --git a/src/server/commands.c b/src/server/commands.c index 63ae3ba3c..057b9c561 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -1736,15 +1736,21 @@ Lists all loaded sounds, good for debugging PF_SoundIndex overflows void SV_ListSounds_f(void) { int i; + int count = 0; char *string; Com_Printf("-- List of Loaded Sounds:\n"); - for (i = 1; i < MAX_SOUNDS; i++) { - string = sv.configstrings[CS_SOUNDS + i]; + for (i = 1; i < svs.csr.max_sounds; i++) { + if (svs.csr.extended) + string = sv.configstrings[CS_SOUNDS + i]; + else + string = sv.configstrings[CS_SOUNDS_OLD + i]; if (string && string[0] != '\0') { // Check if string is not null and not empty Com_Printf("%i: %s\n", i, string); + count++; } } + Com_Printf("-- Total Used Sound Indexes: %i\n-- Total Available Sound Indexes: %i\n", count, svs.csr.max_sounds); } //=========================================================== From f9860dc2155d8a04ce86c68223299bf798d795c6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Feb 2024 13:13:34 +0300 Subject: [PATCH 024/974] Remove unused function. --- src/game/g_local.h | 1 - src/game/g_utils.c | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/src/game/g_local.h b/src/game/g_local.h index eb474c3f4..27418adae 100644 --- a/src/game/g_local.h +++ b/src/game/g_local.h @@ -612,7 +612,6 @@ edict_t *G_Spawn(void); void G_FreeEdict(edict_t *e); void G_TouchTriggers(edict_t *ent); -void G_TouchSolids(edict_t *ent); char *G_CopyString(char *in); diff --git a/src/game/g_utils.c b/src/game/g_utils.c index f685c504e..4bd19f6ba 100644 --- a/src/game/g_utils.c +++ b/src/game/g_utils.c @@ -399,34 +399,6 @@ void G_TouchTriggers(edict_t *ent) } } -/* -============ -G_TouchSolids - -Call after linking a new trigger in during gameplay -to force all entities it covers to immediately touch it -============ -*/ -void G_TouchSolids(edict_t *ent) -{ - int i, num; - edict_t *touch[MAX_EDICTS_OLD], *hit; - - num = gi.BoxEdicts(ent->absmin, ent->absmax, touch, q_countof(touch), AREA_SOLID); - - // be careful, it is possible to have an entity in this - // list removed before we get to it (killtriggered) - for (i = 0; i < num; i++) { - hit = touch[i]; - if (!hit->inuse) - continue; - if (ent->touch) - ent->touch(hit, ent, NULL, NULL); - if (!ent->inuse) - break; - } -} - /* ============================================================================== From eb13803c6cd3b49aecc92569987baec2046bb053 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 9 Feb 2024 10:49:15 -0500 Subject: [PATCH 025/974] Quieting some messaging if conditions aren't met --- src/action/g_save.c | 2 +- src/action/g_spawn.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index 03eba2a81..48eadb199 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -586,7 +586,7 @@ void InitGame( void ) esp_atl = gi.cvar( "esp_atl", "0", 0 ); // This forces ATL mode even if ETV mode is set in the .esp file esp_punish = gi.cvar("esp_punish", "0", 0); esp_etv_halftime = gi.cvar("esp_etv_halftime", "0", CVAR_LATCH); - if (esp_etv_halftime->value && roundlimit->value < 4) { + if (esp->value && esp_etv_halftime->value && roundlimit->value < 4) { // Disabling halftime because roundlimit is not set disablecvar(esp_etv_halftime, "Roundlimit set too low for halftime, minimum is 4 rounds"); } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index fb00df5de..5d83a3a57 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1215,8 +1215,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if (matchmode->value) { gameSettings |= GS_MATCHMODE; - gi.dprintf ("Matchmode Enabled - Forcing g_spawn_items off\n"); - gi.cvar_forceset(g_spawn_items->name, "0"); // Turn off spawning of items for matchmode games + if (g_spawn_items->value) + gi.dprintf ("Matchmode Enabled - Forcing g_spawn_items off\n"); + gi.cvar_forceset(g_spawn_items->name, "0"); // Turn off spawning of items for matchmode games } if (use_3teams->value) { From 6c4d59bb55e40e53928a3f545230a1b55b9c9332 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Feb 2024 22:36:54 +0300 Subject: [PATCH 026/974] Set default C standard to just C11. --- meson.build | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 3d2a3c524..fed6fd3ef 100644 --- a/meson.build +++ b/meson.build @@ -3,7 +3,7 @@ project('q2pro', 'c', version: run_command(find_program('python3'), 'version.py', check: true).stdout().strip(), meson_version: '>= 0.59.0', default_options: [ - meson.version().version_compare('>=1.3.0') ? 'c_std=gnu11,c11' : 'c_std=gnu11', + 'c_std=c11', 'buildtype=debugoptimized', ], ) @@ -195,7 +195,9 @@ if system_wide endif common_args = ['-DHAVE_CONFIG_H', '-DUSE_PROTOCOL_EXTENSIONS=1'] -if not win32 +if win32 + common_args += '-D_USE_MATH_DEFINES' +else common_args += '-D_GNU_SOURCE' endif @@ -240,7 +242,7 @@ if cc.get_argument_syntax() == 'gcc' dll_link_args += '-static-libgcc' endif elif cc.get_id() == 'msvc' - common_args += ['/D_USE_MATH_DEFINES', '/wd4146', '/wd4244', '/wd4305'] + common_args += ['/wd4146', '/wd4244', '/wd4305'] endif add_project_arguments(common_args, language: 'c') From 5d3ae9bc1ad86ea6f35b738101482dbec4f0e5d6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Feb 2024 22:37:38 +0300 Subject: [PATCH 027/974] Optimize testing for Clang-specific warnings. --- meson.build | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index fed6fd3ef..050b9bf2d 100644 --- a/meson.build +++ b/meson.build @@ -219,14 +219,16 @@ if cc.get_argument_syntax() == 'gcc' '-fsigned-char', '-fms-extensions', '-fno-math-errno', - '-Wno-microsoft-anon-tag', - '-Wno-tautological-constant-out-of-range-compare', '-Wpointer-arith', '-Wformat-security', '-Werror=vla', '-Wstrict-prototypes', ] + if cc.get_id() == 'clang' + test_args += ['-Wno-microsoft-anon-tag'] + endif + common_args += cc.get_supported_arguments(test_args) engine_args += cc.get_supported_arguments(['-Wmissing-prototypes']) From 6b3e36b68eafa3beb402aca9e693661c08b6a778 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Feb 2024 23:08:40 +0300 Subject: [PATCH 028/974] Update CI workflow. --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 721b08df3..45911fd7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,9 +35,9 @@ jobs: matrix: arch: ["i686", "x86_64"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: subprojects/packagecache key: ${{ hashFiles('subprojects/*.wrap') }} @@ -60,9 +60,9 @@ jobs: matrix: arch: ["x86", "x64"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: subprojects/packagecache key: ${{ hashFiles('subprojects/*.wrap') }} @@ -87,7 +87,7 @@ jobs: matrix: cc: [gcc, clang] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | From a98de5682222d36f2bfc34f1031cfe927e9243b8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 12 Feb 2024 13:55:55 +0300 Subject: [PATCH 029/974] Clear cached bits if MAX_PACKET_USERCMDS is exceeded. Broken since commit 84cfacb2. --- src/client/input.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/input.c b/src/client/input.c index cfeae0ed5..a29bf0ea9 100644 --- a/src/client/input.c +++ b/src/client/input.c @@ -1014,7 +1014,7 @@ static void CL_SendBatchedCmd(void) numCmds = history->cmdNumber - oldest->cmdNumber; if (numCmds >= MAX_PACKET_USERCMDS) { Com_WPrintf("%s: MAX_PACKET_USERCMDS exceeded\n", __func__); - SZ_Clear(&msg_write); + MSG_BeginWriting(); break; } totalCmds += numCmds; From 47a25502178e16a665b5d4056b9c032019c2c9b2 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Feb 2024 12:19:22 -0500 Subject: [PATCH 030/974] Added some comments for cvars --- src/action/g_local.h | 134 +++++++++++++++++++++---------------------- src/action/g_save.c | 2 +- src/action/g_spawn.c | 3 +- 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 5c08d41a0..145d818b1 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1081,22 +1081,22 @@ extern cvar_t *use_buggy_bandolier; extern cvar_t *uvtime; extern cvar_t *use_mapvote; // enable map voting extern cvar_t *use_scramblevote; -extern cvar_t *sv_gib; +extern cvar_t *sv_gib; // Enables or disables gibs extern cvar_t *sv_crlf; extern cvar_t *vrot; extern cvar_t *rrot; extern cvar_t *empty_rotate; extern cvar_t *empty_exec; extern cvar_t *strtwpn; -extern cvar_t *llsound; +extern cvar_t *llsound; // Enable or disable the lowlag sounds (needed for selectable gun sounds) extern cvar_t *loud_guns; extern cvar_t *sync_guns; extern cvar_t *silentwalk; extern cvar_t *slopefix; extern cvar_t *use_cvote; -extern cvar_t *new_irvision; -extern cvar_t *use_rewards; -extern cvar_t *use_warnings; +extern cvar_t *new_irvision; // Enables or disables the new infra-red vision +extern cvar_t *use_rewards; // Enables or disables the rewards system (Accuracy, Impressive, Excellent, etc) +extern cvar_t *use_warnings; // Enables or disables the warnings system (One minute left...etc) extern cvar_t *video_check; //AQ2:TNG - Slicer: For Video Checking extern cvar_t *video_checktime; //interval between cheat checks extern cvar_t *video_max_3dfx; @@ -1106,14 +1106,14 @@ extern cvar_t *video_check_lockpvs; extern cvar_t *video_check_glclear; extern cvar_t *video_force_restart; extern cvar_t *check_time; -extern cvar_t *matchmode; -extern cvar_t *darkmatch; +extern cvar_t *matchmode; // Enables or disables matchmode +extern cvar_t *darkmatch; // Enables or disables darkmatch extern cvar_t *day_cycle; // If darkmatch is on, this value is the nr of seconds between each interval (day, dusk, night, dawn) extern cvar_t *use_flashlight; // Allow flashlight when not darkmatch? extern cvar_t *hearall; // used in match mode -extern cvar_t *deadtalk; -extern cvar_t *force_skin; +extern cvar_t *deadtalk; // Allow dead players to be heard by live players (chat) +extern cvar_t *force_skin; // Forces model/skin on all players extern cvar_t *mm_forceteamtalk; extern cvar_t *mm_adminpwd; extern cvar_t *mm_allowlock; @@ -1124,17 +1124,17 @@ extern cvar_t *teamdm; extern cvar_t *teamdm_respawn; extern cvar_t *respawn_effect; -extern cvar_t *item_respawnmode; +extern cvar_t *item_respawnmode; // 0 sets items to respawn in the classic way (info_player_deathmatch), 1 spawns them at specific locations extern cvar_t *use_mvd2; // JBravo: activate mvd2 recording on servers running q2pro -extern cvar_t *item_respawn; -extern cvar_t *weapon_respawn; -extern cvar_t *ammo_respawn; +extern cvar_t *item_respawn; // Timer: Interval between item respawns +extern cvar_t *weapon_respawn; // Timer: Interval between weapon respawns +extern cvar_t *ammo_respawn; // Timer: Interval between ammo respawns -extern cvar_t *wave_time; +extern cvar_t *wave_time; // How often a player can use the 'wave' commands in seconds -extern cvar_t *use_punch; +extern cvar_t *use_punch; // Enable or disable the punch command extern cvar_t *radio_max; extern cvar_t *radio_time; @@ -1143,15 +1143,15 @@ extern cvar_t *radio_repeat; //SLIC2 extern cvar_t *radio_repeat_time; -extern cvar_t *hc_single; -extern cvar_t *wp_flags; -extern cvar_t *itm_flags; +extern cvar_t *hc_single; // Enable or disable the single shot handcannon +extern cvar_t *wp_flags; // Weapon flags (bans) +extern cvar_t *itm_flags; // Item flags (bans) extern cvar_t *use_classic; // Use_classic resets weapon balance to 1.52 -extern cvar_t *warmup; -extern cvar_t *warmup_bots; -extern cvar_t *round_begin; -extern cvar_t *spectator_hud; +extern cvar_t *warmup; // Enables warmup (value in seconds) +extern cvar_t *warmup_bots; // Enables bots to spawn during warmup (value in number of bots) +extern cvar_t *round_begin; // Enables round_begin (value in seconds) +extern cvar_t *spectator_hud; // Enables or disables the fancy spectator hud extern cvar_t *fraglimit; extern cvar_t *timelimit; @@ -1159,7 +1159,7 @@ extern cvar_t *maptime; extern cvar_t *capturelimit; extern cvar_t *password; extern cvar_t *g_select_empty; -extern cvar_t *g_protocol_extensions; +extern cvar_t *g_protocol_extensions; // Enable or disable protocol extensions. For legacy compatibility, set this to "0" extern cvar_t *dedicated; extern cvar_t *steamid; @@ -1183,14 +1183,14 @@ extern cvar_t *bob_roll; extern cvar_t *sv_cheats; extern cvar_t *maxclients; -extern cvar_t *unique_weapons; -extern cvar_t *unique_items; +extern cvar_t *unique_weapons; // Max number of unique weapons a player can carry +extern cvar_t *unique_items; // Max number of unique items a player can carry extern cvar_t *ir; //toggles if bandolier works as infra-red sensor -extern cvar_t *knifelimit; -extern cvar_t *tgren; -extern cvar_t *allweapon; -extern cvar_t *allitem; +extern cvar_t *knifelimit; // Maximum number of knives a player can carry +extern cvar_t *tgren; // Maximum number of grenades players start with (with bandolier) +extern cvar_t *allweapon; // Enable or disable all weapons on spawn +extern cvar_t *allitem; // Enable or disable all items on spawn extern cvar_t *allow_hoarding; // Allow carrying multiple of the same special item or unique weapon. extern cvar_t *stats_endmap; // If on (1), show the accuracy/etc stats at the end of a map @@ -1200,24 +1200,24 @@ extern cvar_t *auto_join; // Automaticly join clients to teams they were on in l extern cvar_t *auto_equip; // Remember weapons and items for players between maps. extern cvar_t *auto_menu; // Automatically show the join menu -extern cvar_t *dm_choose; -extern cvar_t *dm_shield; +extern cvar_t *dm_choose; // Enables or disables DM mode weapon/item choosing +extern cvar_t *dm_shield; // Enables or disables DM shield extern cvar_t *tourney_lca; // Enables or disabled lights camera action for tourney mode // TNG:Freud - new spawning system -extern cvar_t *use_oldspawns; +extern cvar_t *use_oldspawns; // Legacy old spawn system // TNG:Freud - ghosts -extern cvar_t *use_ghosts; +extern cvar_t *use_ghosts; // Enable or disable ghosts (stats) // zucc from action -extern cvar_t *sv_shelloff; -extern cvar_t *shelllimit; -extern cvar_t *shelllife; -extern cvar_t *splatlimit; -extern cvar_t *bholelimit; -extern cvar_t *splatlife; -extern cvar_t *bholelife; +extern cvar_t *sv_shelloff; // Enable or disable shell ejection +extern cvar_t *shelllimit; // Maximum number of shells on the ground +extern cvar_t *shelllife; // Time before shells disappear +extern cvar_t *splatlimit; // Maximum number of blood splat decals +extern cvar_t *bholelimit; // Maximum number of bullet hole decals +extern cvar_t *splatlife; // Time before blood splats disappear +extern cvar_t *bholelife; // Time before bullet holes disappear extern cvar_t *medkit_drop; extern cvar_t *medkit_time; @@ -1226,37 +1226,37 @@ extern cvar_t *medkit_max; extern cvar_t *medkit_value; // BEGIN AQ2 ETE -extern cvar_t *esp; -extern cvar_t *atl; -extern cvar_t *etv; -extern cvar_t *esp_atl; -extern cvar_t *esp_punish; -extern cvar_t *esp_etv_halftime; -extern cvar_t *esp_showleader; -extern cvar_t *esp_showtarget; -extern cvar_t *esp_leaderequip; -extern cvar_t *esp_leaderenhance; -extern cvar_t *esp_enhancedslippers; -extern cvar_t *esp_matchmode; -extern cvar_t *esp_respawn_uvtime; -extern cvar_t *esp_debug; +extern cvar_t *esp; // Enable or disable Espionage mode +extern cvar_t *atl; // Enable or disable Assassinate the Leader mode (do not set this manually) +extern cvar_t *etv; // Enable or disable Escort the VIP mode (do not set this manually) +extern cvar_t *esp_atl; // Prefer ATL mode even if ETV mode is available +extern cvar_t *esp_punish; // Enable or disable punishment for losing the around +extern cvar_t *esp_etv_halftime; // Enable or disable halftime in ETV mode +extern cvar_t *esp_showleader; // Enable or disable showing the leader (not implemented) +extern cvar_t *esp_showtarget; // Enable or disable showing the target (not implemented) +extern cvar_t *esp_leaderequip; // Enable or disable leader equipment enhancements +extern cvar_t *esp_leaderenhance; // Enable or disable leader enhancements (fast bandage, medkits) +extern cvar_t *esp_enhancedslippers; // Enable or disable enhanced slippers (does not require Espionage) +extern cvar_t *esp_matchmode; // Enable or disable matchmode in Espionage mode +extern cvar_t *esp_respawn_uvtime; // Enable or disable respawn time in UV mode in seconds +extern cvar_t *esp_debug; // Enable or disable debug mode (very spammy) // END AQ2 ETE // 2023 -extern cvar_t *use_killcounts; -extern cvar_t *am; -extern cvar_t *am_newnames; -extern cvar_t *am_botcount; -extern cvar_t *am_delay; -extern cvar_t *am_team; -extern cvar_t *zoom_comp; -extern cvar_t *item_kit_mode; -extern cvar_t *gun_dualmk23_enhance; -extern cvar_t *printrules; -extern cvar_t *timedmsgs; -extern cvar_t *mm_captain_teamname; -extern cvar_t *sv_killgib; +extern cvar_t *use_killcounts; // Adjust how kill streaks are counted +extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) +extern cvar_t *am_newnames; // Enable or disable new names for Attract Mode (ltk bots) +extern cvar_t *am_botcount; // Number of bots in Attract Mode +extern cvar_t *am_delay; // Delay between bot spawns after players leave in Attract Mode (not implemented) +extern cvar_t *am_team; // Set which team the bots will join in Attract Mode +extern cvar_t *zoom_comp; // Enable or disable zoom compensation +extern cvar_t *item_kit_mode; // Enable or disable item kit mode +extern cvar_t *gun_dualmk23_enhance; // Enable or disable enhanced dual mk23s (laser + silencer) +extern cvar_t *printrules; // Enable or disable printing of rules +extern cvar_t *timedmsgs; // Enable or disable timed messages +extern cvar_t *mm_captain_teamname; // Set the team name to the captain's name in matchmode +extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command // 2024 extern cvar_t *warmup_unready; diff --git a/src/action/g_save.c b/src/action/g_save.c index 48eadb199..c3b746589 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -625,7 +625,7 @@ void InitGame( void ) // 2023 use_killcounts = gi.cvar("use_killcounts", "0", 0); - am = gi.cvar("am", "0", CVAR_SERVERINFO); + am = gi.cvar("am", "0", CVAR_LATCH | CVAR_SERVERINFO); am_newnames = gi.cvar("am_newnames", "1", 0); am_botcount = gi.cvar("am_botcount", "6", CVAR_SERVERINFO); if (am_botcount->value < 0){ diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 5d83a3a57..53991cdc6 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1215,9 +1215,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if (matchmode->value) { gameSettings |= GS_MATCHMODE; - if (g_spawn_items->value) + if (g_spawn_items->value) { gi.dprintf ("Matchmode Enabled - Forcing g_spawn_items off\n"); gi.cvar_forceset(g_spawn_items->name, "0"); // Turn off spawning of items for matchmode games + } } if (use_3teams->value) { From 2821ec02941b5374e6b76db0d9b16f2fd9f63d9b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 14 Feb 2024 19:21:28 +0300 Subject: [PATCH 031/974] Reduce bfg_lightramp[] array size. --- src/client/entities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entities.c b/src/client/entities.c index e762f77fc..a19b60bce 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -963,7 +963,7 @@ static void CL_AddPacketEntities(void) CL_BfgParticles(&ent); i = 200; } else { - static const int bfg_lightramp[6] = {300, 400, 600, 300, 150, 75}; + static const uint16_t bfg_lightramp[6] = {300, 400, 600, 300, 150, 75}; i = bfg_lightramp[Q_clip(s1->frame, 0, 5)]; } V_AddLight(ent.origin, i, 0, 1, 0); From 534a61c32f606c76479e38ffef144ae5ec70a318 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 14 Feb 2024 19:22:08 +0300 Subject: [PATCH 032/974] Convert NOPART_*, etc defines to BIT() macro. --- src/client/client.h | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 083d355ab..a01e6a65e 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -504,18 +504,18 @@ extern char cl_cmdbuf_text[MAX_STRING_CHARS]; //============================================================================= -#define NOPART_GRENADE_EXPLOSION 1 -#define NOPART_GRENADE_TRAIL 2 -#define NOPART_ROCKET_EXPLOSION 4 -#define NOPART_ROCKET_TRAIL 8 -#define NOPART_BLOOD 16 - -#define NOEXP_GRENADE 1 -#define NOEXP_ROCKET 2 - -#define DLHACK_ROCKET_COLOR 1 -#define DLHACK_SMALLER_EXPLOSION 2 -#define DLHACK_NO_MUZZLEFLASH 4 +#define NOPART_GRENADE_EXPLOSION BIT(0) +#define NOPART_GRENADE_TRAIL BIT(1) +#define NOPART_ROCKET_EXPLOSION BIT(2) +#define NOPART_ROCKET_TRAIL BIT(3) +#define NOPART_BLOOD BIT(4) + +#define NOEXP_GRENADE BIT(0) +#define NOEXP_ROCKET BIT(1) + +#define DLHACK_ROCKET_COLOR BIT(0) +#define DLHACK_SMALLER_EXPLOSION BIT(1) +#define DLHACK_NO_MUZZLEFLASH BIT(2) // // cvars From 4939234c3d3be9f6c6e85efccda88b62028ad73d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 14 Feb 2024 20:35:05 +0300 Subject: [PATCH 033/974] Add some function attributes supported by MSVC. --- inc/common/common.h | 3 ++- inc/common/zone.h | 15 ++++++++++----- inc/shared/game.h | 2 +- inc/shared/platform.h | 18 ++++++++++++------ inc/shared/shared.h | 9 +++++---- inc/system/system.h | 7 +++++-- src/server/mvd/client.h | 3 ++- 7 files changed, 37 insertions(+), 20 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index 470d3ddf0..93740cb95 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -86,7 +86,8 @@ void Com_AbortFunc(void (*func)(void *), void *arg); void Com_SetLastError(const char *msg); const char *Com_GetLastError(void); -void Com_Quit(const char *reason, error_type_t type) q_noreturn; +q_noreturn +void Com_Quit(const char *reason, error_type_t type); void Com_SetColor(color_index_t color); diff --git a/inc/common/zone.h b/inc/common/zone.h index 443a5efc2..0cada9337 100644 --- a/inc/common/zone.h +++ b/inc/common/zone.h @@ -46,11 +46,16 @@ void Z_Free(void *ptr); void Z_Freep(void *ptr); void *Z_Realloc(void *ptr, size_t size); void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size); -void *Z_Malloc(size_t size) q_malloc; -void *Z_Mallocz(size_t size) q_malloc; -void *Z_TagMalloc(size_t size, memtag_t tag) q_malloc; -void *Z_TagMallocz(size_t size, memtag_t tag) q_malloc; -char *Z_TagCopyString(const char *in, memtag_t tag) q_malloc; +q_malloc +void *Z_Malloc(size_t size); +q_malloc +void *Z_Mallocz(size_t size); +q_malloc +void *Z_TagMalloc(size_t size, memtag_t tag); +q_malloc +void *Z_TagMallocz(size_t size, memtag_t tag); +q_malloc +char *Z_TagCopyString(const char *in, memtag_t tag); void Z_FreeTags(memtag_t tag); void Z_LeakTest(memtag_t tag); void Z_Stats_f(void); diff --git a/inc/shared/game.h b/inc/shared/game.h index f17c65cbe..df89239d7 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -145,7 +145,7 @@ typedef struct { // they connect, and changes are sent to all connected clients. void (*configstring)(int num, const char *string); - void (* q_noreturn q_printf(1, 2) error)(const char *fmt, ...); + void (* q_noreturn_ptr q_printf(1, 2) error)(const char *fmt, ...); // the *index functions create configstrings and some internal server state int (*modelindex)(const char *name); diff --git a/inc/shared/platform.h b/inc/shared/platform.h index 9eed15eac..2b3b7fe8e 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -92,6 +92,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_printf(f, a) __attribute__((format(printf, f, a))) #endif #define q_noreturn __attribute__((noreturn)) +#define q_noreturn_ptr q_noreturn #define q_noinline __attribute__((noinline)) #define q_malloc __attribute__((malloc)) #if __GNUC__ >= 4 @@ -126,21 +127,26 @@ with this program; if not, write to the Free Software Foundation, Inc., #else /* __GNUC__ */ -#define q_printf(f, a) +#ifdef _MSC_VER +#define q_noreturn __declspec(noreturn) +#define q_noinline __declspec(noinline) +#define q_malloc __declspec(restrict) +#define q_alignof(t) __alignof(t) +#else #define q_noreturn #define q_noinline #define q_malloc +#define q_alignof(t) 1 +#endif + +#define q_printf(f, a) +#define q_noreturn_ptr #define q_sentinel #define q_cold #define q_likely(x) (x) #define q_unlikely(x) (x) #define q_offsetof(t, m) ((size_t)&((t *)0)->m) -#ifdef _MSC_VER -#define q_alignof(t) __alignof(t) -#else -#define q_alignof(t) 1 -#endif #define q_gameabi diff --git a/inc/shared/shared.h b/inc/shared/shared.h index d453a3428..251c2948d 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -103,10 +103,11 @@ typedef enum { PRINT_NOTICE // print in cyan color } print_type_t; -void Com_LPrintf(print_type_t type, const char *fmt, ...) -q_printf(2, 3); -void Com_Error(error_type_t code, const char *fmt, ...) -q_cold q_noreturn q_printf(2, 3); +q_printf(2, 3) +void Com_LPrintf(print_type_t type, const char *fmt, ...); + +q_cold q_noreturn q_printf(2, 3) +void Com_Error(error_type_t code, const char *fmt, ...); #define Com_Printf(...) Com_LPrintf(PRINT_ALL, __VA_ARGS__) #define Com_WPrintf(...) Com_LPrintf(PRINT_WARNING, __VA_ARGS__) diff --git a/inc/system/system.h b/inc/system/system.h index 3199c1d82..6d021d721 100644 --- a/inc/system/system.h +++ b/inc/system/system.h @@ -55,8 +55,11 @@ void Sys_Printf(const char *fmt, ...) q_printf(1, 2); #define Sys_Printf(...) (void)0 #endif -void Sys_Error(const char *error, ...) q_noreturn q_printf(1, 2); -void Sys_Quit(void) q_noreturn; +q_noreturn q_printf(1, 2) +void Sys_Error(const char *error, ...); + +q_noreturn +void Sys_Quit(void); void Sys_ListFiles_r(listfiles_t *list, const char *path, int depth); diff --git a/src/server/mvd/client.h b/src/server/mvd/client.h index 27dc782a9..1ce9d7e3f 100644 --- a/src/server/mvd/client.h +++ b/src/server/mvd/client.h @@ -199,7 +199,8 @@ extern jmp_buf mvd_jmpbuf; extern cvar_t *mvd_shownet; #endif -void MVD_Destroyf(mvd_t *mvd, const char *fmt, ...) q_noreturn q_printf(2, 3); +q_noreturn q_printf(2, 3) +void MVD_Destroyf(mvd_t *mvd, const char *fmt, ...); void MVD_Shutdown(void); mvd_t *MVD_SetChannel(int arg); From 9485929cd07b970f147fa5e3bd3ea89ce57f1bce Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 14 Feb 2024 22:59:01 -0500 Subject: [PATCH 034/974] esp_enhancedslippers 2 debuts --- src/action/p_view.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/action/p_view.c b/src/action/p_view.c index b81c2b508..a44088a6f 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -595,6 +595,11 @@ qboolean OnLadder( edict_t *ent ) /* ================= P_FallingDamage + +With esp_enhancedslippers, the player will take less damage from falls: +0 = no change +1 = 50% less damage +2 = 25% increased fall height before damage ================= */ void P_FallingDamage (edict_t * ent) @@ -602,6 +607,7 @@ void P_FallingDamage (edict_t * ent) float delta; int damage; vec3_t dir, oldvelocity; + int fallheight = 30; // Minimum height to fall to take damage, in quake units? //if (!FRAMESYNC) // return; @@ -641,6 +647,11 @@ void P_FallingDamage (edict_t * ent) delta = delta * delta * 0.0001; + // If slippers are equipped and the cvar is set to 2, increase height before fall damage occurs + if (esp_enhancedslippers->value == 2 && INV_AMMO(ent, SLIP_NUM)) { + delta *= 0.75; + } + // never take damage if just release grapple or on grapple if (level.framenum - ent->client->ctf_grapplereleaseframe <= 2*FRAMEDIV || (ent->client->ctf_grapple && @@ -692,13 +703,14 @@ void P_FallingDamage (edict_t * ent) return; } - + // Play falling/landing sound if (ent->health > 0) { if (delta >= 55) ent->s.event = EV_FALLFAR; - else // all falls are far + else ent->s.event = EV_FALLFAR; + // Play different sound if wearing slippers if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) ent->s.event = EV_FALL; } @@ -707,14 +719,14 @@ void P_FallingDamage (edict_t * ent) if (!DMFLAGS(DF_NO_FALLING)) { - damage = (int) (((delta - 30) / 2)); + damage = (int) (((delta - fallheight) / 2)); if (damage < 1) damage = 1; // zucc scale this up damage *= 10; - // darksaint - reduce damage if esp_enhancedslippers are on and equipped - if (esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM)) + // darksaint - reduce damage if esp_enhancedslippers are 1 and equipped + if (esp_enhancedslippers->value == 1 && INV_AMMO(ent, SLIP_NUM)) damage /= 2; VectorSet (dir, 0, 0, 1); From 2af76b0c6770b97eb732c1c5a2dc429e2886cff9 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 14 Feb 2024 23:45:03 -0500 Subject: [PATCH 035/974] Converted tng manual to markdown --- doc/action.md | 566 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 566 insertions(+) create mode 100644 doc/action.md diff --git a/doc/action.md b/doc/action.md new file mode 100644 index 000000000..ab5bbd6b1 --- /dev/null +++ b/doc/action.md @@ -0,0 +1,566 @@ +# Action Quake 2: The Next Generation +**Version 2.83 (Nov 14th, 2023)** +[http://www.aq2tng.barrysworld.net](http://www.aq2tng.barrysworld.net) +by Igor, JBravo, Slicer, Deathwatch, Freud, Elviz, Rektek +Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team + +--- + +## Introduction + +The Next Generation started as three separate projects: Action Quake 2: Edition, maintained by PG Bund and Igor, Action Quake 2: JBravo Edition, maintained by JBravo and Action Quake 2: Millennium, maintained by Slicer, Mort and Deathwatch. It has continued as a project under the AQ2World team, with contributions from many others, in the form of **AQtion** (packaged q2pro + aq2-tng) available on Steam now for free. + +--- + +## Installation + +Unpack the quake2/action directory and edit the ini files in the action/config directory to suit your needs. Edit the server.cfg to whatever settings you want to use. Make sure you set your inifile variable to point to the correct action.ini in action/config. + +--- + +## Features + +### Mapvoting + +TNG offers mapvoting so that clients can change the map by voting for one. This is useful for those servers that wish to offer the maps the people want to play on. There is a 40 second voting block to prevent players with fast connections to join and vote for a map before others got a chance to join/vote. + +#### Commands + +- `use_mapvote [0/1]` - toggle to turn mapvoting on (1) or off (0). +- `mapvote_pass [0..100]` - Percentage of votes a single map needs before the vote passes. +- `mapvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. +- `mapvote_min [#]` - Minimum number of players that need to vote for a single map. 0 to ignore that. +- `mapvote_waittime [#]` - Number of seconds people can't vote for a new map when they enter a map. +- `maplistname "maplist.ini"` - filename/location of the file containing all the maps people can vote for. +- `mv_public [0/1]` - When on (1), it will show what maps someone votes for. + +### Kickvoting +This offers clients to kick those unwanted players from the server when no admin is about. + +#### Commands +- `use_kickvote [0/1]` - toggle to turn kickvoting on (1) or off (0). +- `kickvote_pass [0..100]` - Percentage of votes a single player needs before the vote passes. +- `kickvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. +- `kickvote_min [#]` - Minimum number of players that need to vote for a single player. 0 to ignore that. +- `kickvote_tempban [0/1]` - When set to 1, it will ban the kicked person until the map changes +- `vk_public [0/1]` - When on (1), it will show who votes for who. +- `kicklist` - this will display a list of all players with their id number. (client side) +- `votekick "player"` - this will cast your votekick vote for player "player". (client side) +- `votekicknum [#]` - this will cast your votekick vote for the player with id number #. (client side) + +### Configvoting +Configvoting allows clients to vote for a certain configuration, predefined by the server admin, to be put on the server, in the same way as mapvoting. To vote for a config, open up the menu ('menu' in the console) and select Configvoting from the list. + +#### Commands +- `use_cvote [0/1]` - toggle to turn configvoting on (1) or off (0). +- `cvote_pass [0..100]` - Percentage of votes a single config needs before the vote passes. +- `cvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. +- `cvote_min [#]` - Minimum number of players that need to vote for a single config. 0 to ignore that. +- `configlistname "configlist.ini"` - filename/location of the file containing all the configs people can vote for. + +### Teamplay +Enables standard 2-team round-based teamplay. Kill everyone on the other team to win a round. +- `teamplay [0/1]` (default: "0") - setting to 1 enables teamplay, requires new map or server restart +- `sv t[i]name ` - Where i is 1, 2 or 3, sets a team name in the absence of action.ini. Will take effect immediately. Example: sv t1name "Robbers" +- `sv t[i]skin ` - Where i is 1, 2 or 3, sets a team skin in the absence of action.ini. Will take effect on a new round of teamplay or a new map. Example: sv t2skin "male/ctf_b" +- `sv t[i]skin_index ` - Where i is 1, 2 or 3, sets a team skin index in the absence of action.ini. Will take effect on a new map or server restart. Example: sv t3skin_index "siris_i" + +### Tourney +For those who want to offer a Rocket Arena style one on one server, this mode is ideal. It will let two players spawn and face each other. The winner will stay for the next round and the loser will go back to the queue. The winner then takes up the next player in the queue. + +Example: There are 4 players on a server, A, B, C and Z. Both A and B play each other, C and Z are in the queue. B wins and A gets put back in the queue (which then is C, Z and A). Then, B plays the next in the queue, namely C. If B would win, he'd face Z and C would get put back in the queue. If C would win, he would play Z next and B would be put back in the queue. + +#### Commands +- `use_tourney [0/1]` - This will turn tourney mode on (1) or off (0). To use it, teamplay needs to be on. (1) +- `tourney_lca [0/1]` - Set to 1 to enable "Lights Camera Action" for tourney mode, set to 0 for classic silence + +### 3 Teams Teamplay +For more exciting teamplay games, TNG offers a 3 Teams mode of play. This variant of Teamplay will put three teams against each other. + +#### Commands +- `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). To use it, teamplay needs to be on. (1) + +### Capture the Flag +The popular gameplay mode from many games and mods is available in TNG as well. In this mode, there are two teams, Red and Blue. Both have a flag on the map, and they need to take the enemy flag and bring it to their own to score a point. (Capture the Flag, or cap for short) The other team will attempt the same of course. + +To prevent spawn camping, there is an invulnerability/shield mode to give some people some protection. + +Flag locations and CTF player spawns should be specified in tng/mapname.ctf files. + +#### Commands +- `ctf [0/1]` - This will turn CTF on (1) or off (0). It will automatically turn Teamplay on (1). +- `capturelimit [#]` - The maximum number of captures before a map will change. Set to 0 to ignore that. +- `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. +- `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. +- `drop flag` - Drop the flag if you're holding it. (client side) +- `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) + +### Domination +This is most closely related to Unreal Tournament's domination mode, but is also similar to king of the hill and conquest modes in general. There are fixed flags (typically 3) scattered around the map, and touching a flag will claim it for your team. Keep control of flags to score points; your team gains 1 point per flag controlled every second. The score limit is 200 per flag in 2-team mode, or 150 per flag in 3-team mode, which means a typical 2-team game with 3 flags has a score limit of 600. + +Flag locations can be specified in tng/mapname.dom files. If not found, the server will attempt to generate them automatically from player spawn locations. + +#### Commands +- `dom [0/1]` - This will turn Domination mode on (1) or off (0), and also turns Teamplay on (1). +- `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). +- `uvtime [#]` - The number of seconds * 10 of the duration of the 'shield' effect (ex: 40 = 4 secs). + +### Matchmode +Matchmode is a special form of Teamplay, made for clanmatches. It has several features which can be useful during matches and league games. It makes sure the whole timelimit is played. + +Clients will have a few more things to do during matchmode: they have to have a captain and substitutes. Everybody in a team will only be able to talk to his/her own team, with the exception of the captain (see below). Subs are players who are part of the team but aren't playing at the moment. With limchasecam enabled, they will be limited to viewing their own team. + +#### Commands +- `matchmode [0/1]` - Turns Matchmode on (1) or off (0). Turning Matchmode on will turn Teamplay on too. +- `captain` - Become the team's captain if not captain exists yet. A captain can ready/unready his team and speak to the other team. (client side) +- `sub` - this will make you a sub for the team or remove you from the subs and back in the team. (client side) +- `ready` - this will ready/unready the team. A new round won't start if a team isn't read. (client side) +- `teamname "name"` - allows the captain to set the name of his/her team. (client side) +- `teamskin "male/resdog"` - allows the captain to set the name of his/her team. (client side) +- `mm_forceteamtalk [0/1/2]` - this will change the way people can talk on the server. When set to 0, it will be the same as with normal teamplay. When set to 1, the teams will not be able to talk with each other, except for the admin and the captains. When set to 2, teams can talk with each other until the match starts and when the match is paused or over. +- `mm_adminpwd ` - this will set the password for match admins. +- `matchadmin ` - this will allow a player to get admin status. (client side) +- `mm_allowlock [0/1]` - when on (1), this will allow captains to lock/unlock their teams. +- `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map. (client side) +- `unlock` - allows a captain to unlock his team. (client side) +- `mm_captain_teamname - [0/1]` - default 0, if enabled (1) then the team name will change to the name of the captain, prepended with the string "Team", for example, "Team Suislide" if Suislide is the captain of that team. + +### Voice Command +The voice command allows clients to play taunts for other players to hear. (as long as they have the sound file) + +#### Commands +- `use_voice [0/1]` - When on (1), it will allow the use of voice commands. +- `voice "sound.wav"` - this will play sound.wav for all players to hear. (as long as the others have sound.wav) This command requires the .wav extension. (client side) + +### Low Lag Sounds +To reduce the number of packets being sent by AQ2 during big firefights, TNG has included the option to turn Low Lag Sounds on. This will use a different set of sounds for each weapon, ones that reduce the number of packets being sent. + +When you use this, the sounds of the weapons will change to those of Quake 2 weapons. You can update your sounds with the following: + +- MK23 Pistol: `sound/weapons/mk23fire.wav` +- Handcannon: `sound/weapons/cannon_fire.wav` +- MP5 Submachinegun: `sound/weapons/mp5fire.wav` +- SSG-3000 Sniper Rifle: `sound/weapons/ssgfire.wav` +- M4 Assault Rifle: `sound/weapons/m4a1fire.wav` +- M3 Super Shotgun: `sound/weapons/shotgf1b.wav` + +#### Commands +- `llsound [0/1]` - When on (1), Low Lag Sounds will be used. +- `cl_mk23_sound` +- `cl_mp5_sound` +- `cl_m4_sound` +- `cl_m3_sound` +- `cl_hc_sound` +- `cl_ssg_sound` + +### Announcer +The announcer that says "Lights, Camera, Action!" has his vocabulary increased by several words, announcing when people perform several feats, such as high accuracy ("Accuracy") or make an impressive shot ("Impressive") but also when a team wins ("Team X Wins") or when the fraglimit is about to get hit. + +#### Commands +- `use_warnings [0/1]` - When on (1), it will play all warnings such as "3 minutes left" and "Team X Wins". +- `use_rewards [0/1]` - When on (1), it will play the sounds on rewards such as "Impressive". +- `use_killcounts [0/1]` - When on (1), server will announce kill counts. + +### Kevlar Helmet +A new item, the Kevlar Helmet acts like the Kevlar Vest does, only it protects the head of the player wearing it. To disable it, use item banning. + +### Single Barreled Handcannon +We have added the feature to let clients fire one barrel of the Handcannon at the same time. This has as benefit that the Handcannon will be able to fire a second shot fast after the next one, but as drawback is is that it is not as strong as a double barreled shot, or shotgun shot for that matter. + +#### Commands +- `hc_single [0/1]` - Set to 1 to enable single barreled handcannon. Default is 1. + +### Enemy Down Radio Reporting +Upgraded the 'enemy down' radio sound to report the number of enemies that have been killed as well. + +### Player Ignoring +Player Ignoring can be done through the menu. When ignoring a player, you won't hear any messages from him. + +#### Commands +- `menu` - this will open the TNG menu. (client side) +- `ignore "player"` - this will ignore the player with the name "player". +- `ignorelist` - this will display a list of all players on the server with their id. +- `ignorenum [#]` - ignorenum will ignore the player with the id given. +- `ignoreclear` - this will clear your ignore list. + +### Video Setting Checking +TNG offers a way to check for gl_lockpvs and to cap the gl_modulate settings. + +#### Commands +- `video_check [0/1]` - turn checking for gl_modulate capping on (1) or off (0) +- `video_check_lockpvs [0/1]` - turn checking for gl_lockpvs on (1) or off (0) +- `video_check_glclear [0/1]` - turn checking for gl_clear on (1) or off (0) +- `video_checktime [#]` - sets interval between checks in seconds +- `video_max_3dfx [#]` - Maximum value for gl_modulate for 3dfx users. +- `video_max_3dfxam [#]` - Maximum value for gl_modulate for 3dfx users with the AMD patch. +- `video_max_opengl [#]` - Maximum value for gl_modulate for OpenGL users. +- `video_force_restart [0/1]` - If on (1), the server will force a client to do a video_restart when he connects. + +### Location Files +To support the %L and %S commands, the server needs to install the location files. Location files are files which contain coordinates for locations on the map and names for them. The location files itself have the extension of .aqg and should be put in the action/tng directory on the server. + +### Punching +We have added the extra 'punch' attack, which basically is a weaker version of a kick. It is very close range and does up to 20 damage and can punch the weapon of a player out of his hands. When bandaging, shooting, reloading or zoomed with a sniper rifle, you cannot punch. + +#### Commands +- `use_punch [0/1]` - when on (1), it will allow clients to use the punch command +- `punch` - this will preform a punch. (client side) + +### Lens command +The lens command is in TNG and offers increased control over the zooming of the sniper rifle. + +#### Commands +- `lens` - changes zoom on sniper rifle. (Client side) +- `lens [#]` - 1,2,4,6 - if you use any other number it'll go to the nearest zoom mode. (Client side) +- `lens in` - zooms the rifle in to the next zooming level. (client side) +- `lens out` - zooms the rifle out to the previous zooming level. (client side) + +### New Say Variables +TNG offers several new variables clients can use in their text: +- `%K` - shows the nickname of the person you killed last. +- `%P` - shows the nickname of the person you damanged last. +- `%D` - shows the location where you hit your enemy last. This can either be head, chest, stomach, legs, kevlar vest or kevlar helmet. +- `%L` - shows the location where you are at that moment. (if the map has a location file) +- `%S` - shows the location where you are looking at. (if the map has a location file) +- `%E` - shows the enemy you are looking at. +- `%F` - shows the weapon the enemy is holding. +- `%me` - shows your nickname like IRC's /me. + +### Roundtimeleft +By typing 'roundtimeleft' in the console during a round, it will give a general indication of the amount of time left that round. This only works when there is a roundtimelimit. + +#### Commands +- `roundtimeleft` - displays remaining roundtime. (client side) + +### Time +By typing 'time' in the console, a client can see the remaining time on that map. This doesn't work in matchmode + +#### Commands +- `time` - display the remaining time (client side) + +### sv stuffcmd +This feature will allow people who got rcon to send commands to the clients. + +#### Commands +- `sv stuffcmd ` - If you use $, it will display your own setting for that variable. If you use ! it will display the client's variable name. + +Example: +- `sv stuffcmd all say !gl_modulate` - This will make all clients say their gl_modulate setting. + +### Grenade Strength +Grenades are a little bit more powerful, so they're not as useless as they were since AQ 1.52 (250 instead 170 - in AQ2 1.51 they were even stronger) - should stop people using grenade jumps in AQ2 (after all, it should be an ACTION mod - have you EVER seen someone doing grenade jumps in a real life action film? (exclude Matrix - it's a science fiction...)) + +#### Commands +- `use_classic [0/1]` - Turn on (1) to use the original 1.52 values. + +### Total Kills +The scoreboard of TNG will now show the total kills for each player. Kills is the total number of kills without the negatives (suicides, cratering, teamkills) subtracted. + +### Random Rotation +Random Map Rotation will make the server pick a random map from the maplist when the current map ends. This will make the rotations less static. + +#### Commands +- `rrot [0/1]` - Set to 1 to turn random rotation on. + +### Vote Rotation +Voterotation computes the all time votes for maps and changes the maplist as wanted. If a map is played the amount of players is subtracted from the all time votes value. + +#### Commands +- `vrot [0/1]` - Turn on (1), to use voterotation. + +### MapVote Next +Basically, this is a simplified and immediate form of Vote Rotation. When the current map ends, if there are any active map votes, the server moves to the most voted map instead of the next in rotation. + +#### Commands +- `mapvote_next [0/1]` - Default on (1), set to 0 to disable this feature. +- `mapvote_next_limit [#]` - Default (0, disabled), how many seconds remaining in a map (via timelimit) to restrict the use of votemap, so last second voters can't change the next map. Enforced on mapvote_next=1. + +### Empty Rotate +Prevent unpopular maps from keeping your server empty. This feature will rotate the map after the server has been empty for some number of minutes. You can also specify a config file to execute, which can be used to reset the server to default settings after configvote or rotate between multiple mode configs. + +#### Commands +- `empty_rotate [#]` - Number of minutes before rotating empty server. Default 0 disables this feature. +- `empty_exec "file.cfg"` - Optional config to exec with empty rotate. Default blank disables this. + +### Bandage Text +When a client starts bandaging, a message ("You've started bandaging") will appear. Also the Health icon will appear on the bottom of the screen. + +### Deathmatch Weapon +A new variable allows the server to give players a starting weapon during deathmatch. When set, the players will get the weapon of that type with full ammo when they spawn. + +#### Commands +- `dmweapon "weaponname"` - Starting weapon for DM mode (dmweapon), default is "MK23 Pistol". +- `dm_choose [0/1]` - allows players to choose their starting weapon in DM mode, 0 disables/1 enables +- `dm_shield [#]` - Sets a time value for how long the invulnerability shield in DM mode. Value of 30 = 3 seconds, for example. + +### Control Characters +Some proxies use control characters to hide the name of the user. However, it is used by several players as well to cause abuse and do spam messages. + +#### Commands +- `sv_crlf [0/1]` - if off (0), clients are not allowed to issue CR/LF control characters in chat messages. + +### Anti Camping +To discourage camping, TNG has a feature which will make a client who is standing still for a certain number of seconds to make noises. + +#### Commands +- `ppl_idletime [#]` - Allowed idle time in seconds before a client will start to make sounds. 0 to turn it off. + +### Anti Idle +To help with problem idlers, use this feature to remove them from their teams so that someone else can join in without needing to votekick. This affects teamplay modes only, as regular DM does not use teams. If you want to remove in DM mode, use sv_idlekick. + +#### Commands +- `sv_idleremove [#]` - Allow idle time in seconds before a player gets removed from their team. Suggested this be a value over 60. Set to 0 to disable. + +### Gibs +Gibs are a rewarding sight for successful headshots. TNG offers a variable which can turn gibs on headshots on. Also handcannon shots that do enough damage will gib someone. + +#### Commands +- `sv_gib [0/1]` - when on (1), it will display + +### Automatic Reloading of Pistol +When someone has akimbo pistols and they are both empty, and got one clip left, to press reload and it will switch back to single pistol and reload that with the clip. + +### Weapon Banning +To allow/disallow the usage of certain weapons, TNG supports weapon banning. + +**Commands:** +`wp_flags [#]` - The number for this variable is the combination of the following: +- MK 23 - 1 +- MP5 - 2 +- M4 - 4 +- Shotgun - 8 +- Handcannon - 16 +- Sniper - 32 +- Akimbo - 64 +- Knife - 128 +- Grenade - 256 + +For all weapons: 511 (default) + +**Example:** You want to make a sniper rifle only server: you set wp_flags to 32, the only weapon you allow. +Or, if you only want pistols: 65 (1+64). + +### Item Banning +To allow/disallow the usage of certain items, TNG supports item banning. + +**Commands:** +`itm_flags [#]` - The number for this variable is the combination of the following: +- Silencer - 1 +- Slippers - 2 +- Bandolier - 4 +- Kevlar Vest - 8 +- Lasersight - 16 +- Kevlar Helmet - 32 + +For all weapons: 63 (default) + +**Example:** You want to have slippers and silencers only on your server: you set itm_flags to 3 (1+2). + +### Teamkilling after a Round +To add a little more fun in the game, we've added an option which will allow a team to TK each other after a round has ended during Teamplay. This is only available to servers where FF is off. + +**Commands:** +`ff_afterround [0/1]` - Set to 1 to allow TKing (without punishments) after a round has ended. FF needs to be off. + +### New IR Vision +An updated IR Vision is available. This does, besides adding the red glow to players, another visual effect. + +**Commands:** +`new_irvision [0/1]` - Set to 1 to turn the new IR Vision on. + +### Radio and Voice Flood Protection +To stop people spamming with the Radio and Voice command, there is flood protection for both in TNG. + +**Commands:** +- `radio_repeat [#]` - Number of the same radio/voice calls before being stopped. 0 is off. +- `radio_ban [#]` - Number of seconds a player will be silenced for after spamming. +- `radio_time [#]` - Time, in seconds, where a client may have a radio_max number of radio/voice calls in. +- `radio_max [#]` - Number of radio/voice calls allowed with the radio_time period. + +### Darkmatch +Ever wanted to play AQ2 in total darkness? TNG allows you to experience this by using darkmatch. Darkmatch has 3 modes. In it's first mode, the map will be pitch black, except for certain lights. This means that players have to be careful where they walk as they do not know when they will walk off a building. Also, firing your weapon will make you show up clearly. + +In the second mode, the map is in its near black mode. The map will be very dark, and it will allow for more stealthy play. + +The third mode will simulate a real day. The map will go from normal lighting to pitch black and then back again to normal lighting. + +Of course, to assist players, there is a flashlight available in darkmatch only. When used, it will create a circle of light at wherever you point, but it will also make you show up. + +**Commands:** +- `darkmatch [0/1/2/3]` - when set to 1 it's total darkness, when set to 2, its near darkness, when set to 3 it will simulate day/night. +- `day_cycle [#]` - Time in seconds where Darkmatch mode 3 will change its light settings +- `use_flashlight [0/1]` - allow flashlight use in non-darkmatch +- `flashlight` - this toggle will turn the flashlight on and off. (client side) + +### Map Restarting +Sometimes you do want to restart a map, to reset the scores there. But a normal map command will disconnect everybody and will reset everything on the server. Or you want to load a map without disconnecting everybody. + +For this, TNG has two new commands. + +**Commands:** +- `sv softmap ` - this command will load 'mapname' as if it was next in the rotation. +- `sv map_restart` - this command will restart the map as if it was next in the rotation. + +### Statistics +To see how well players are doing, we have implemented statistics into TNG. This feature will track various statistics of each player, for example accuracy or frags per minute. + +**Commands:** +- `stats_endmap [0/1]` - when set to 1, this will display the stats scoreboard at the end of the map instead of the normal screen. +- `stats_afterround [0/1]` - when on (1), this will also record stats for events that happen when a round ends, for example when ff_afterround or FF is on. +- `stats` - this will display the individual stats of the player. (client side) +- `stats list` - this will display a list of all players and their ID. (client side) +- `stats [#]` - this will display the stats for the player with the id given. (client side) +- `stats_mode [0/1/2]` - when set to 1, it will automatically display the stats of the player at the end of each round. When set to 2, it will automatically display the stats of the player at the end of the map. By default this is set to 0 (off). (client side) + +### Automatic Joining/Equipping +For the lazy players under us, we have created two new client commands to make things easier. + +**Commands:** +- `auto_join [0/1/2]` - when on (1), the players will automatically join the team he was in during the last map. If set to 2, it will automatically join the team with the least players, or randomly if equal. +- `auto_equip [0/1]` - when on (1), the players will automatically be given the equipment he had during the last map. + +### Automatic Demo Recording +To ease the recording of demos by a client over several maps, we have implemented a feature which will let the client automatically record a demo on each new map. When enabled, the server will keep track of the recording of the client, and will create a demo of the map with the following name: date_time-map.dm2 In matchmode, the demo's name will be: date_time-team1_vs_team2-map.dm2 + +**Commands:** +`autorecord` - Starts automatic recording of demos. + +### Spawn Code +TNG offers a new spawn code for Teamplay. This mode prevents teams to spawn twice on the same spot and will go through most spawns. + +**Commands:** +`use_oldspawns [0/1]` - Defaults to 0 (off). When set to 0, it will use the new spawning method for teamplay. + +### Ghost +With the use of the ghost command, the client can get back when he has been disconnected and get your frags and stats back. Ghost does require the client to have the same nick and ip when using the ghost command. + +**Commands:** +- `use_ghosts [0/1]` - when set to on (1), the server allows the clients to use the ghost command. +- `ghost` - this will restore the scores, stats, teams and items of the client. (client side) + +### Bandolier behavior +TNG updates the way the bandolier behaves when dropping it. It will prevent people from keeping the grenade(s) (when enabled) after having dropped the bandolier. + +**Commands:** +`use_buggy_bandolier [0/1]` - if you wish to revert back to the old bandolier behavior, set this to 1 + +### Bots +Now you can fill out your teams with bots, or create an entire team of bots to fight. You can define persistent bots in bots/botdata.cfg, or create and remove them on demand. Bots wander pretty stupidly unless you create a linked network of nodes for them to follow. + +**Commands:** +- `sv addbot [team] [name]` - add a bot for the duration of the current map +- `sv removebot [name]` - remove a bot +- `ltk_skill [0-10]` - set bot skill (default 5) +- `ltk_chat [0/1]` - allow bots to chat +- `ltk_loadbots [0/1]` - 0 to stop bots from automatically joining the game, 1 for default behavior +- `ltk_botfile [filename]` - Set filename of the botfile you want to load. Default is 'botdata' (this loads bots/botdata.cfg) +- `sv botdebug [on/off]` - toggle bot node editing and output for debugging +- `addnode [type]` - place a new node at your current position (it will tell you its number) +- `addlink [num1] [num2]` - add a link from node num1 to num2, and a reverse link if possible +- `removelink [num1] [num2]` - remove the link from node num1 to num2; does not remove reverse +- `movenode [num]` - move an existing node to your current position +- `nodetype [num] [type]` - show or change the type of a node +- `shownode [num]` - display a temporary marker of a node's location +- `sv initnodes` - clear all nodes +- `sv loadnodes` - load saved nodes from terrain/.ltk +- `sv savenodes` - save all nodes and links for this map to terrain/.ltk + +### Slap +Server admins can slap players into the air, and optionally deal damage. + +**Commands:** +`sv slap [name] [damage] [power]` - slap a player, with optional damage (default 0) and power (default 100) + +### Variable Framerate Support +Although Quake 2 typically runs the server at a fixed 10 frames per second, some advanced Quake 2 servers can be built with variable framerate support to allow faster updating. AQ2-TNG now supports this. At higher fps, lag is reduced: the server responds more quickly to shooting and other client commands. + +**Commands:** +- `sv_fps [10/20/30/40/50/60]` - set server framerate (default is 10) +- `sync_guns [0/1/2]` - 0 plays gun sounds on any frame, 1 syncs with recent shots, 2 delays all to 10fps (default 1) + +### Q2pro MVD server demo support +When AQ2TNG is running under a q2pro Quake2 executable that is properly configured (built with CONFIG_MVD_SERVER set to 1 at compile time it can record every single match into a dedicated file. For this to work the cvar sv_mvd_enable must also be set to 1. The demos go into the action/demos folder on the server and contain all players points of view for the entire match. To activate this simply set use_mvd2 1 in your server config. + +### Latency Compensation +Antilag allows server operator to enable lag-compensation for aiming with hitscan weapons, useful for making high ping games more fair. Optionally, the server operator can enable interpolation along with antilag for aiming directly at player models to hit them. This has the side effect of being 'shot around corners', so the best use for this setting tends to be for matches where all players pings are very low. + +**Commands:** +- `sv_antilag [0/1]` (default: "1") - Setting to "1" enables lag compensation functionality when firing hitscan weapons. +- `sv_antilag_interp [0/1]` (default: "0") - Setting to "1" enables interpolation for hitscan weapons. Requires sv_antilag "1". + +### General quality of life improvements +`sv_limp_highping [#]` - server cvar, players above this ping threshold will have movement prediction disabled with leg damage to make things less jittery. Value is set in ping ms, players with a ping value equal or higher to this value will have less jittery movement. Default value is '70' + +### Client Prediction +`limp_nopred [0/1]` - client cvar, clients can set this to force on or off movement prediction when taking leg damage. Set to "0" for classic behavior, set to "1" to fix the jitter + +### Force Spawn items +`g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for normal play. + +### Attract Mode +`am` - server cvar, this will set the server up in 'attract mode', where bots spawn in but are replaced by humans, and vice versa. This is meant to keep a server 'populated' without being overrun by bots, when humans can take their place. It depends on several other cvars for proper operation. +- `am [0/1/2]` + - 0 is disabled, LTK bots will spawn normally from their configs. This is the default value. + - 1 is enabled, and human replace bots as they join, and bots replace humans as they leave. Depends on am_botcount for this threshold value + - 2 is enabled, and humans will not replace bots until the total number of players (real people + bots) is (maxclients - 2). +- `am_botcount [#]` + - This value is the number of players + bots you want at any given time. Default is 6. +- `am_team [0/1/2/3]` + - This value is which team you want bots to spawn in on. If you only want bots on team 1, set this value to 1. 0 means bots will autojoin teams via the autobalancer. If you try to set this to 3 without use_3teams, they will default to team 1. Default is 0. +- `am_newnames [0/1]` + - This enables new bot names, stemming from the AQ2World team, in case you want to change things up a bit. Default is 1. + +### Zoom Compensation +`zoom_comp [0/1]` - server cvar, set to "1" to compensate zoom delay based on ping. Every 80ms ping reduces 1 frame, minimum of 1 frame. Default is "0", do not compensate + +### Warmup +- `warmup [#]` - server cvar, set in seconds, minimum is 15, 20 is common. This is the amount of time in seconds after both captains ready up that warmups will continue until the first round begins. Set to 0 to disable warmup entirely. +- `warmup_bots [#]` - server cvar, set in number of bots to add. This adds this many bots to the warmup phase of matchmode. Set to '0' to disable warmup_bots, suggested value for usage is '4' to '6'. + +### Item Kit Mode +`item_kit_mode [0/1]` - server cvar, default 0. Works in any mode where you choose a weapon (GS_WEAPONCHOOSE), it combines items into kits: +- Commando Kit: Bandolier + Kevlar Helm +- Stealth Kit: Slippers + Silencer +- Assassin Kit: Laser Sight + Silencer +Players may drop the items anytime (using `drop item`) but you can only pick one up at a time, assuming the server doesn't allow for more. On player entity death, both items will drop. LTK bots do not use kits, they will retain their normal behavior. esp_enhancedslippers settings are honored, boosting the effectiveness of the Stealth Kit even more. + +### Print rules +- `printrules [0/1]` - server cvar, default 0. If enabled, a printout of the rules will display once the countdown begins in Teamplay modes. + +### Espionage +See included Espionage.md file for details. Taken from AQ2:ETE, these additions are optional server vars to create a larger variety of gameplay. Review the document called Espionage.md to understand how to run a server and how to create and edit scene files. +- `esp [0/1]` - Activates Espionage mode. Default 0 +- `atl [0/1]` - Sets Assassinate the Leader mode, if esp is enabled. +- `etv [0/1]` - Sets Escort the VIP mode, will default to Assassinate the Leader if the map configuration is missing or malformed +- `esp_punish [0/1/2]` - Sets punishment mode for losing team. 0 is default post-round teamplay with FF on. 1 immediately kills all remaining members of the losing team. 2 generates an invulnerability shield on the remaining winning team members for the duration of post-round celebrations, so they can deal with the remaining losing team members in style +- `esp_etv_halftime [0/1]` - Sets halftime rule of swapping teams in an ETV scenario after half of the timelimit has passed +- `esp_showleader [0/1]` - GHUD setting, enabling this will show a marker over your team's leader at all times so that you can find him +- `esp_showtarget [0/1]` - GHUD setting, enabling this will show a marker over your escort target, where the leader needs to reach to win the round +- `esp_leaderequip [0/1/2/3]` - Adjusts the loadout of all leaders, see Espionage.md for details +- `esp_enhancedslippers [0|1]` (default: "0") to remove limping from leg damage (falling and shooting), and 50% damage reduction when falling long distances + +## Contact Information +If you wish to contact the TNG team, to report a bug, suggest a new feature, or offer JBravo your sister, you can find us on IRC on QuakeNet (irc.quakenet.org) in #action.dev + +Send your bug reports to tng-bugs@ra.is and for normal inquiries, email tng@ra.is. + +> (NOTE: I doubt any of the above is still accurate, but I'm leaving it here to honor the original devs.) + +Bug reports are now handled on GitHub: [https://github.com/Raptor007/aq2-tng/issues](https://github.com/Raptor007/aq2-tng/issues) + +## Credits +Thanks go to the following people who have helped us make AQ2:The Next Generation in one way or another. + +- Mort - Original Millennium CTF and lots of help with coding +- QDevels - Information and code samples +- Clan dW, Clan ROCK, Clan DP and Clan QNI - Testing and suggestions +- All BWAQSL Admins for suggestions and playtesting +- BarrysWorld, Clix, IOL, The Last Resort and Simnet for providing test servers +- iD Software - Original Flag Models/Sounds +- Davross and Chaka - Flag/Banner Skins +- Kobra - Announcer sounds +- BlackMonk - Supplying us with all kinds of info +- Dome - For helping us with some issues and supplying ideas +- Papst - For helping with the overflow problem +- Everybody who reported bugs and features to us +- And of course every AQ2 player, let's keep AQ2 alive! \ No newline at end of file From 50ad53200427a868d1354f841e98062b925080d2 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 14 Feb 2024 23:46:00 -0500 Subject: [PATCH 036/974] Added Espionage markdown --- doc/espionage.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 doc/espionage.md diff --git a/doc/espionage.md b/doc/espionage.md new file mode 100644 index 000000000..14b0cec4a --- /dev/null +++ b/doc/espionage.md @@ -0,0 +1,160 @@ +# Action Quake 2: Espionage +--- + +## Introduction +This is the main documentation for the Espionage game mode. It will cover each mode, how to operate a server with this mode, and how to create your own scene files, or edit existing ones. + +### Where did this come from? +A little history lesson: when the 1.52 code was leaked/released (depends on who you ask?), it was still very early in the lifecycle of the game. The European scene decided to enhance this code into their own fork of the mod, which came to be known as [AQ2: The Next Generation (TNG)](https://aq2-tng.sourceforge.net/), and the North American scene did the same thing, this one by a team known as the [Action Quake Developers Team (AQDT)](https://assets.aq2world.com/archive/websites/aqdt.fear.net/). + +Utilizing some ideas from [the Gangsters mod](https://assets.aq2world.com/archive/websites/www.planetquake.com/gangsters/) of Action Quake, as well as [Mr. Brian's CTB](https://assets.aq2world.com/archive/websites/gauntlet.telefragged.com/action/index.html), despite mentioning to the contrary that the mods were not related, as well as some ideas of their own, the AQDT created the Espionage mod. On top of standard Deathmatch and Teamplay modes, Espionage mode was yet another way to experience Action Quake 2. When players asked for a way to handle official matches, Tournament Edition was added, and Espionage Tournament Edition was what the modification was eventually named (**AQ2: ETE**). + +### Great, but what _is_ Espionage? +Espionage incorporated several 'sub-modes' that were [teamplay-focused objective-based scenarios](https://assets.aq2world.com/archive/websites/aqdt.fear.net/aqdtdocs/esp.htm) and encouraged a bit of creativity from the author to create fun and engaging scenes to play in. The original premise of the game was to act like the players were in a movie, so now they have a script! [There were six new modes to play](https://wiki.aq2world.com/wiki/Game_Modes:Espionage): + +1. Assassinate the Leader (ATL) +1. Escort the VIP (ETV) +1. Capture the Briefcase (CTB) +1. Capture and Hold (CNH) +1. One Flag Capture (OFC) +1. Find, Retrieve, Defend (FRD) + +### How come it's taken so long to add these to TNG? +Sadly, the source code for ETE was never publicly released, as much as I pleaded and asked. So there was only one true option left -- write it from scratch. This is no small undertaking, but I think it's doable. + +### Assassinate the Leader +The first sub-mode that we're looking at porting is Assassinate the Leader. This is because the game flow is very straightforward and is almost exactly like normal teamplay as far as basic game logic is concerned, with a few major differences: + +1. A player can `volunteer` to become a leader for their team. There's only one leader per team. +1. The goal of each team is to frag the opposing team's leader, while protecting their own. +1. Configurable via server cvar, each leader can provided with `all weapons`, `all items`, or both. +1. Consistent respawning to keep everyone in the action. Respawn timers are configurable. +1. In Matchmode, the Captain is also the Leader. + +Teams only gain points and rounds end when a leader gets fragged. Getting fragged in the defense of your leader is an honorable act and is awarded with extra points and recognition, as well is assaulting the opposing team's leader. + +### Escort the VIP +The second sub-mode is Escort the VIP. It's a mixture of Assassinate the Leader sprinkled in with some CTF, except there's no need to return the flag, merely touching the capture point is enough to win the round. +1. Team 1 is the only team that has a Leader. +2. Team 1's goal is to have their Leader touch the capture point (briefcase) on the map. If this is achieved, they win the round. +3. Team 2's goal is to prevent Team 1's Leader from touching the capture point. This can be achieved in two ways: + 1. Killing Team 1's Leader before he reaches the capture point + 2. Waiting for the `roundtimelimit` to expire (if set), causing a Team 2 win by default. Suggested `roundtimelimit` for most maps is 2 minutes. +4. Halftime, if enabled and a `roundlimit` is set, will swap teams when half of the roundlimit is reached. This is so that the experience is fair to both teams for the map. + + +### Sounds great, how do I enable Espionage mode on my servers? +There are some settings you need to setup to enable it: + +* **esp** [0/1] - Activates Espionage mode. Default 0. +* **esp_punish** [0/1/2] - Sets punishment mode for losing team when their leader dies. This occurs instantly. A value of 2 is only compatible with 2 Team Assassinate the Leader mode. In 3 Team mode, only 0 and 1 are available. + * 0 (default) is normal post-round teamplay with FF on. + * 1 immediately kills all remaining members of the losing team. + * 2 generates an invunerability shield on the remaining winning team members for the duration of post-round celebrations, so they can deal with the remaining losing team members in style +* **esp_showleader** [0/1] - GHUD setting, enabling this will show a marker over your team's leader at all times so that you can find him + * 0 disables the indicator hovering over your leader + * 1 enables the indicator hovering over your leader, making him easy to keep track of +* **esp_leaderequip** [0/1/2/3] - This determines the loadout of equipment for a team's leader + * 0 means that the leaders do not get any special loadout, one item and one weapon of their choosing + * 1 (default) means that the leaders can choose their weapon, and have all items available at once when they spawn + * 2 means that the leaders can choose their item, and have all weapons available at once when they spawn + * 3 means that the leaders have all items and all weapons when they spawn +* **esp_leaderenhance** [0/1] - This enhances the leader in that: + * The leader's bandage time is halved + * The leader receives periodic medkits, if medkit_max is > 0, to be used when they bandage +* **esp_enhancedslippers** [0/1] - to remove limping from leg damage (falling and shooting), and 50% damage reduction when falling long distances. This used to be called `e_enhancedSlippers` from the original ETE, but was renamed to keep in with the naming convention of Espionage options +* **esp_atl** [0/1] - default `0`, this forces ATL mode if `1`, even if the scenario loaded for the map is for ETV mode. Has no effect on any map that is specifically setup for ATL mode or maps that load safe defaults, which are always ATL mode. + +* **ETV-Specific Cvars**: + * **esp_etv_halftime** [0/1] - Sets halftime rule of swapping teams in an ETV scenario after half of the roundlimit has passed. A `roundlimit` of `0` automatically disables halftime. + * **esp_showtarget** [0/1] - GHUD setting, enabling this will show a marker over your escort target, where the leader needs to reach to win the round + * 0 disables the indicator pointing at your escort target location + * 1 enables the indicator to assist your team in escorting your leader + +* **esp_matchmode** [0/1] - toggles setting hard-coded defaults if matchmode is enabled, as posted below: + +``` +* Normal Slippers +* No team punishment +* Leader gets 1 weapon and all items +* Leader is enhanced and receives a maximum of 1 healthkit worth 25 health +``` + + +Generally, these settings are great for all public servers: +``` +esp 1 +esp_punish 1 +esp_showleader 1 +esp_leaderequip 1 +esp_leaderenhance 1 +esp_enhancedslippers 1 +``` +Some may not like the punishment system, as it does not give any time for the team that lost their leader a few extra seconds to extract vengeance, so test this setting with your players. + +### I would like to create my own scenes! +Great! It's relatively easy. There are configuration files that live inside of the `action/tng` directory with the extension of `.esp`. These are Espionage config files. I will go over the example of `urban4.esp`: +``` +[esp] +type=atl +author=darksaint +name=Urban Gang Wars + +[respawn] +red=10 +blue=10 +green=10 + +[spawns] +red=<274 1165 476 -148>,<553 -293 277 90>,<-783 -64 444 -60>,<342 632 319 180> +blue=<-1404 -13 59 90>,<-1548 1077 61 -90>,<-1387 1402 179 0>,<-1601 206 452 0> +green=<-635 592 65 45>,<-900 573 433 0>,<-660 -112 46 0>,<-904 826 473 -45> + +[red_team] +name=The B-Team +leader_name=B.A. Barracuda +skin=male/ctf_r +leader_skin=male/babarracuda + +[blue_team] +name=Peacekeepers +leader_name=Frank The Cop +skin=male/ctf_b +leader_skin=male/blues + +[green_team] +name=Landscaping Crew +leader_name=The Incredible Chulk +skin=male/ctf_g +leader_skin=male/hulk2 +``` + +* Under the `[esp]` header is information about the scene; the type (`atl`), the author and the name of the scene. +* Under the `[respawn]` header is how many seconds each team respawns. Generally these should all be the same for fairness, but in cases where your `[spawns]` may heavily favor one team over the other on purpose (such as creating a scene for defending territory), you have the freedom to play with these values. +* Under the `[spawns]` header are the locations of the custom spawns that this scene uses. You can defer to use the map's spawn points, in which case you simply remove this header and its contents entirely. + * To generate spawn point locations, load the map locally and move your player to the location you want a spawn point to be. In the console, type `viewpos`. This will give you the vector locations (x y z) as well as the direction you are facing. + * Reformat that into comma-delimited `` where `w` is the direction you are facing + * Each team can have up to 4 custom spawn points +* Under each team header, you need to specify the following critiera, in no specific order: + * `name` - Name of the team + * `skin` - The team's skin + * `leader_name` - The name of that team's leader + * `leader_skin` - The skin that the leader will use + * The keys and values here are case-sensitive, but the `name` and `leader_name` can have uppercase, spaces and _some_ special characters. +* Tips: + * When creating spawn points, the `z` value should be at least 10 units higher than what viewpos returns, to avoid potentially getting stuck in the floor. This means if your `viewpos` value is `24` then it should be `34` in the file. For negative values (`-24`), you must add 10, so it's not `-34`, it's `-14`. + * Double check your scenario file before committing it in case your spawn points don't work (stuck in a tree/wall, accidentially set a negative value instead of a positive, etc) + * Reference skins that are in the AQtion distrib, so players don't see the default `male/resdog` skin if they don't have it. If you want to suggest a new skin to use in your killer scenario, contact the AQ2World team in Discord. + * Err on the side of shorter respawn times rather than longer ones -- it's more fun to play than to watch. Some playtesting to get the perfect timings down may be needed, especially on maps where it's easy to defend a capture point, perhaps Team 2's respawn timer is a tad longer than Team 1's, for example. + * When creating an ETV target, ensure that not only is it in a good spot, but keep in mind that the indicator arrow is 75 quake units floating above it -- this may clip into low ceilings and not look good. The value of 75 units is so that the arrow doesn't interfere with visibility from the player's view. + * Fill in details for the third team, even if you only envision this scenario being played out with 2 teams. In the event that a server operator wants to run 3 team ATL mode 24/7, this will use your config rather than falling back on defaults. + +Save your map-specific file with the name of the map to `action/tng/mapname.esp`. If you formatted it correctly, it should load those values in the next time the map is loaded. + +If something is malformatted or wrong, the game will do its best to handle it gracefully. If it is having a problem finding values or other things are wrong with the file, it should ignore the file entirely and load `default.esp` values instead. If this file is missing or also malformatted, there are some safe defaults that the gamelib has hard-coded that will work for any map, as it does not use custom spawn points. + +--- +### Some little-known features in Espionage mode +* In ETE mode, it would calculate pings and average across both teams. This calculcation would be used to determine if the words "FAIR" or "NOT FAIR" would appear on the scoreboard. +* You could ignore a player, which is something we only recently added to TNG thanks to ReKTeK! +* Enhanced stealth slippers that stopped limping and increased the height that fall damage would occur. These are now also in TNG but slightly modified in that you take 50% damage from falling rather than the increased height. Imagine all of the cool jumps you can do now with little damage! From 8a6ea08b551782e56d5c70c25eb8c28f506ddc8f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 14 Feb 2024 23:49:40 -0500 Subject: [PATCH 037/974] Adjustments, added espionage.md --- doc/action.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/action.md b/doc/action.md index ab5bbd6b1..b0048cba0 100644 --- a/doc/action.md +++ b/doc/action.md @@ -495,7 +495,7 @@ Antilag allows server operator to enable lag-compensation for aiming with hitsca `limp_nopred [0/1]` - client cvar, clients can set this to force on or off movement prediction when taking leg damage. Set to "0" for classic behavior, set to "1" to fix the jitter ### Force Spawn items -`g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for normal play. +`g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for classic play. ### Attract Mode `am` - server cvar, this will set the server up in 'attract mode', where bots spawn in but are replaced by humans, and vice versa. This is meant to keep a server 'populated' without being overrun by bots, when humans can take their place. It depends on several other cvars for proper operation. @@ -519,9 +519,9 @@ Antilag allows server operator to enable lag-compensation for aiming with hitsca ### Item Kit Mode `item_kit_mode [0/1]` - server cvar, default 0. Works in any mode where you choose a weapon (GS_WEAPONCHOOSE), it combines items into kits: -- Commando Kit: Bandolier + Kevlar Helm -- Stealth Kit: Slippers + Silencer -- Assassin Kit: Laser Sight + Silencer +- Commando Kit: `Bandolier + Kevlar Helm` +- Stealth Kit: `Slippers + Silencer` +- Assassin Kit: `Laser Sight + Silencer` Players may drop the items anytime (using `drop item`) but you can only pick one up at a time, assuming the server doesn't allow for more. On player entity death, both items will drop. LTK bots do not use kits, they will retain their normal behavior. esp_enhancedslippers settings are honored, boosting the effectiveness of the Stealth Kit even more. ### Print rules @@ -537,7 +537,10 @@ See included Espionage.md file for details. Taken from AQ2:ETE, these additions - `esp_showleader [0/1]` - GHUD setting, enabling this will show a marker over your team's leader at all times so that you can find him - `esp_showtarget [0/1]` - GHUD setting, enabling this will show a marker over your escort target, where the leader needs to reach to win the round - `esp_leaderequip [0/1/2/3]` - Adjusts the loadout of all leaders, see Espionage.md for details -- `esp_enhancedslippers [0|1]` (default: "0") to remove limping from leg damage (falling and shooting), and 50% damage reduction when falling long distances +- `esp_enhancedslippers [0|1|2]`: default `0` + - If enabled, this removes all limping, and: + - `1`: 50% damage reduction on falls + - `2`: 25% increased fall height before falling damage is taken ## Contact Information If you wish to contact the TNG team, to report a bug, suggest a new feature, or offer JBravo your sister, you can find us on IRC on QuakeNet (irc.quakenet.org) in #action.dev @@ -546,7 +549,7 @@ Send your bug reports to tng-bugs@ra.is and for normal inquiries, email tng@ra.i > (NOTE: I doubt any of the above is still accurate, but I'm leaving it here to honor the original devs.) -Bug reports are now handled on GitHub: [https://github.com/Raptor007/aq2-tng/issues](https://github.com/Raptor007/aq2-tng/issues) +Bug reports are now handled on GitHub: [https://github.com/actionquake/q2pro/issues](https://github.com/actionquake/q2pro/issues) ## Credits Thanks go to the following people who have helped us make AQ2:The Next Generation in one way or another. From f1aa69351f356a820c6bdce0b9aaec296db8b4e3 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 15 Feb 2024 00:00:06 -0500 Subject: [PATCH 038/974] Added table of contents to action.md --- doc/action.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/doc/action.md b/doc/action.md index b0048cba0..9de753647 100644 --- a/doc/action.md +++ b/doc/action.md @@ -4,6 +4,127 @@ by Igor, JBravo, Slicer, Deathwatch, Freud, Elviz, Rektek Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team + +# Table of Contents +1. [Introduction](#introduction) +2. [Installation](#installation) +3. [Features](#features) + - [Mapvoting](#mapvoting) + - [Commands](#commands) + - [Kickvoting](#kickvoting) + - [Commands](#commands-1) + - [Configvoting](#configvoting) + - [Commands](#commands-2) +5. [Teamplay](#teamplay) +6. [Tourney](#tourney) + - [Commands](#commands-3) +7. [3 Teams Teamplay](#3-teams-teamplay) + - [Commands](#commands-4) +8. [Capture the Flag](#capture-the-flag) + - [Commands](#commands-5) +9. [Domination](#domination) + - [Commands](#commands-6) +10. [Matchmode](#matchmode) + - [Commands](#commands-7) +11. [Voice Command](#voice-command) + - [Commands](#commands-8) +12. [Low Lag Sounds](#low-lag-sounds) + - [Commands](#commands-9) +13. [Announcer](#announcer) + - [Commands](#commands-10) +14. [Kevlar Helmet](#kevlar-helmet) +15. [Single Barreled Handcannon](#single-barreled-handcannon) + - [Commands](#commands-11) +16. [Enemy Down Radio Reporting](#enemy-down-radio-reporting) +17. [Player Ignoring](#player-ignoring) + - [Commands](#commands-12) +18. [Video Setting Checking](#video-setting-checking) + - [Commands](#commands-13) +19. [Location Files](#location-files) +20. [Punching](#punching) + - [Commands](#commands-14) +21. [Lens command](#lens-command) + - [Commands](#commands-15) +22. [New Say Variables](#new-say-variables) +23. [Roundtimeleft](#roundtimeleft) + - [Commands](#commands-16) +24. [Time](#time) + - [Commands](#commands-17) +25. [sv stuffcmd](#sv-stuffcmd) + - [Commands](#commands-18) +26. [Grenade Strength](#grenade-strength) + - [Commands](#commands-19) +27. [Total Kills](#total-kills) +28. [Random Rotation](#random-rotation) + - [Commands](#commands-20) +29. [Vote Rotation](#vote-rotation) + - [Commands](#commands-21) +30. [MapVote Next](#mapvote-next) + - [Commands](#commands-22) +31. [Empty Rotate](#empty-rotate) + - [Commands](#commands-23) +32. [Bandage Text](#bandage-text) +33. [Deathmatch Weapon](#deathmatch-weapon) + - [Commands](#commands-24) +34. [Control Characters](#control-characters) + - [Commands](#commands-25) +35. [Anti Camping](#anti-camping) + - [Commands](#commands-26) +36. [Anti Idle](#anti-idle) + - [Commands](#commands-27) +37. [Gibs](#gibs) + - [Commands](#commands-28) +38. [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) +39. [Weapon Banning](#weapon-banning) + - [Commands](#commands-29) +40. [Item Banning](#item-banning) + - [Commands](#commands-30) +41. [Teamkilling after a Round](#teamkilling-after-a-round) + - [Commands](#commands-31) +42. [New IR Vision](#new-ir-vision) + - [Commands](#commands-32) +43. [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) + - [Commands](#commands-33) +44. [Darkmatch](#darkmatch) + - [Commands](#commands-34) +45. [Map Restarting](#map-restarting) + - [Commands](#commands-35) +46. [Statistics](#statistics) + - [Commands](#commands-36) +47. [Automatic Joining/Equipping](#automatic-joining-equipping) + - [Commands](#commands-37) +48. [Automatic Demo Recording](#automatic-demo-recording) + - [Commands](#commands-38) +49. [Spawn Code](#spawn-code) + - [Commands](#commands-39) +50. [Ghost](#ghost) + - [Commands](#commands-40) +51. [Bandolier behavior](#bandolier-behavior) + - [Commands](#commands-41) +52. [Bots](#bots) + - [Commands](#commands-42) +53. [Slap](#slap) + - [Commands](#commands-43) +54. [Variable Framerate Support](#variable-framerate-support) + - [Commands](#commands-44) +55. [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) +56. [Latency Compensation](#latency-compensation) + - [Commands](#commands-45) +57. [General quality of life improvements](#general-quality-of-life-improvements) +58. [Client Prediction](#client-prediction) +59. [Force Spawn items](#force-spawn-items) +60. [Attract Mode](#attract-mode) + - [Commands](#commands-46) +61. [Zoom Compensation](#zoom-compensation) +62. [Warmup](#warmup) + - [Commands](#commands-47) +63. [Item Kit Mode](#item-kit-mode) +64. [Print rules](#print-rules) + - [Commands](#commands-48) +65. [Espionage](#espionage) + - [Commands](#commands-49) +66. [Contact Information](#contact-information) +67. [Credits](#credits) --- ## Introduction From e82d12e6a71eb2e6381cbceda509365fb7558568 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 15 Feb 2024 00:01:29 -0500 Subject: [PATCH 039/974] Added table of contents to action.md --- doc/action.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/action.md b/doc/action.md index 9de753647..ac5994f32 100644 --- a/doc/action.md +++ b/doc/action.md @@ -657,7 +657,7 @@ See included Espionage.md file for details. Taken from AQ2:ETE, these additions - `esp_etv_halftime [0/1]` - Sets halftime rule of swapping teams in an ETV scenario after half of the timelimit has passed - `esp_showleader [0/1]` - GHUD setting, enabling this will show a marker over your team's leader at all times so that you can find him - `esp_showtarget [0/1]` - GHUD setting, enabling this will show a marker over your escort target, where the leader needs to reach to win the round -- `esp_leaderequip [0/1/2/3]` - Adjusts the loadout of all leaders, see Espionage.md for details +- `esp_leaderequip [0/1/2/3]` - Adjusts the loadout of all leaders, see [espionage.md](./espionage.md) for details - `esp_enhancedslippers [0|1|2]`: default `0` - If enabled, this removes all limping, and: - `1`: 50% damage reduction on falls From 6846b8537daaca0b5b152018b0119638513772c7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 15 Feb 2024 00:03:29 -0500 Subject: [PATCH 040/974] Updated contact info --- doc/action.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/action.md b/doc/action.md index ac5994f32..214ed93be 100644 --- a/doc/action.md +++ b/doc/action.md @@ -664,14 +664,21 @@ See included Espionage.md file for details. Taken from AQ2:ETE, these additions - `2`: 25% increased fall height before falling damage is taken ## Contact Information +Contacting the AQ2World Team is easy, join our Discord or visit our forums, or leave a Github Issue. We are always looking for feedback and suggestions. +- Discord Server: [https://discord.aq2world.com](https://discord.aq2world.com) +- Forums: [https://forums.aq2world.com](https://forums.aq2world.com) + +Bug reports are now handled on GitHub: [https://github.com/actionquake/q2pro/issues](https://github.com/actionquake/q2pro/issues) + +--- + +*Legacy contact information for the original TNG team:* If you wish to contact the TNG team, to report a bug, suggest a new feature, or offer JBravo your sister, you can find us on IRC on QuakeNet (irc.quakenet.org) in #action.dev Send your bug reports to tng-bugs@ra.is and for normal inquiries, email tng@ra.is. > (NOTE: I doubt any of the above is still accurate, but I'm leaving it here to honor the original devs.) -Bug reports are now handled on GitHub: [https://github.com/actionquake/q2pro/issues](https://github.com/actionquake/q2pro/issues) - ## Credits Thanks go to the following people who have helped us make AQ2:The Next Generation in one way or another. From ca5fb1d2fbd407d4ecd0f1d1417b900978bfd8a2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Feb 2024 19:41:55 +0300 Subject: [PATCH 041/974] =?UTF-8?q?Update=20=E2=80=98allow=5Fdownload=5Fma?= =?UTF-8?q?ps=E2=80=99=20documentation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #345. --- doc/server.asciidoc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index 4b85327ec..2a75957d6 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -289,10 +289,6 @@ allow_download:: allow_download_maps:: Enables downloading of files from ‘maps/’ subdirectory. Default value is 1. - - 0 — map downloads are disabled - - 1 — map downloads are enabled for physical files and disabled for files - from packs - - 2 — map downloads are enabled for all files allow_download_models:: Enables downloading of files from ‘models/’ and ‘sprites/’ subdirectories. From cb1910a3642226f50e6782f440df711a084080c7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 21:55:00 -0500 Subject: [PATCH 042/974] Updated documentation a bit, default zoom_comp to 1 --- .github/workflows/build.yml | 2 +- doc/action.md | 13 +++++++++---- src/action/g_save.c | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34e33f209..7fbe51ac5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -222,7 +222,7 @@ jobs: - name: Build run: | - meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX }} builddir + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX_X11 }} builddir meson compile -vC builddir env: CC: ${{ matrix.cc }} diff --git a/doc/action.md b/doc/action.md index 214ed93be..333f3d2e6 100644 --- a/doc/action.md +++ b/doc/action.md @@ -16,6 +16,7 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Configvoting](#configvoting) - [Commands](#commands-2) 5. [Teamplay](#teamplay) + - [Commands](#commands-3) 6. [Tourney](#tourney) - [Commands](#commands-3) 7. [3 Teams Teamplay](#3-teams-teamplay) @@ -181,6 +182,8 @@ Configvoting allows clients to vote for a certain configuration, predefined by t ### Teamplay Enables standard 2-team round-based teamplay. Kill everyone on the other team to win a round. + +#### Commands - `teamplay [0/1]` (default: "0") - setting to 1 enables teamplay, requires new map or server restart - `sv t[i]name ` - Where i is 1, 2 or 3, sets a team name in the absence of action.ini. Will take effect immediately. Example: sv t1name "Robbers" - `sv t[i]skin ` - Where i is 1, 2 or 3, sets a team skin in the absence of action.ini. Will take effect on a new round of teamplay or a new map. Example: sv t2skin "male/ctf_b" @@ -339,7 +342,7 @@ The lens command is in TNG and offers increased control over the zooming of the ### New Say Variables TNG offers several new variables clients can use in their text: - `%K` - shows the nickname of the person you killed last. -- `%P` - shows the nickname of the person you damanged last. +- `%P` - shows the nickname of the person you damaged last. - `%D` - shows the location where you hit your enemy last. This can either be head, chest, stomach, legs, kevlar vest or kevlar helmet. - `%L` - shows the location where you are at that moment. (if the map has a location file) - `%S` - shows the location where you are looking at. (if the map has a location file) @@ -533,12 +536,13 @@ To see how well players are doing, we have implemented statistics into TNG. This - `stats [#]` - this will display the stats for the player with the id given. (client side) - `stats_mode [0/1/2]` - when set to 1, it will automatically display the stats of the player at the end of each round. When set to 2, it will automatically display the stats of the player at the end of the map. By default this is set to 0 (off). (client side) -### Automatic Joining/Equipping -For the lazy players under us, we have created two new client commands to make things easier. +### Automatic Joining/Equipping/Menu +For the lazy players under us, we have created three new commands to make things easier. **Commands:** - `auto_join [0/1/2]` - when on (1), the players will automatically join the team he was in during the last map. If set to 2, it will automatically join the team with the least players, or randomly if equal. - `auto_equip [0/1]` - when on (1), the players will automatically be given the equipment he had during the last map. +- `auto_menu [0/1]` - when on (1), the menu will automatically open when you join the server. ### Automatic Demo Recording To ease the recording of demos by a client over several maps, we have implemented a feature which will let the client automatically record a demo on each new map. When enabled, the server will keep track of the recording of the client, and will create a demo of the map with the following name: date_time-map.dm2 In matchmode, the demo's name will be: date_time-team1_vs_team2-map.dm2 @@ -649,7 +653,7 @@ Players may drop the items anytime (using `drop item`) but you can only pick one - `printrules [0/1]` - server cvar, default 0. If enabled, a printout of the rules will display once the countdown begins in Teamplay modes. ### Espionage -See included Espionage.md file for details. Taken from AQ2:ETE, these additions are optional server vars to create a larger variety of gameplay. Review the document called Espionage.md to understand how to run a server and how to create and edit scene files. +Inspired by AQ2:ETE, these additions are optional server vars to create a different variety of gameplay. Review the document called Espionage.md to understand how to run a server and how to create and edit scene files. - `esp [0/1]` - Activates Espionage mode. Default 0 - `atl [0/1]` - Sets Assassinate the Leader mode, if esp is enabled. - `etv [0/1]` - Sets Escort the VIP mode, will default to Assassinate the Leader if the map configuration is missing or malformed @@ -662,6 +666,7 @@ See included Espionage.md file for details. Taken from AQ2:ETE, these additions - If enabled, this removes all limping, and: - `1`: 50% damage reduction on falls - `2`: 25% increased fall height before falling damage is taken + - You do not need to have Espionage mode enabled to use this feature. ## Contact Information Contacting the AQ2World Team is easy, join our Discord or visit our forums, or leave a Github Issue. We are always looking for feedback and suggestions. diff --git a/src/action/g_save.c b/src/action/g_save.c index c3b746589..01e01cd74 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -633,7 +633,7 @@ void InitGame( void ) } am_delay = gi.cvar("am_delay", "30", 0); am_team = gi.cvar("am_team", "0", 0); - zoom_comp = gi.cvar("zoom_comp", "0", 0); + zoom_comp = gi.cvar("zoom_comp", "1", 0); item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); printrules = gi.cvar("printrules", "0", 0); From b948421284f154460cfa6076113b5d82eaa9d8bc Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 22:39:17 -0500 Subject: [PATCH 043/974] Cleaned up docs a bit, set a permanent dlserver for AQtion clients --- doc/action.md | 187 +++++++++++++++++++++++++-------------------- doc/espionage.md | 6 +- inc/common/files.h | 4 + src/client/http.c | 10 ++- src/server/main.c | 6 ++ 5 files changed, 127 insertions(+), 86 deletions(-) diff --git a/doc/action.md b/doc/action.md index 333f3d2e6..439bcf3d5 100644 --- a/doc/action.md +++ b/doc/action.md @@ -147,47 +147,52 @@ Unpack the quake2/action directory and edit the ini files in the action/config d TNG offers mapvoting so that clients can change the map by voting for one. This is useful for those servers that wish to offer the maps the people want to play on. There is a 40 second voting block to prevent players with fast connections to join and vote for a map before others got a chance to join/vote. #### Commands - -- `use_mapvote [0/1]` - toggle to turn mapvoting on (1) or off (0). -- `mapvote_pass [0..100]` - Percentage of votes a single map needs before the vote passes. -- `mapvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. -- `mapvote_min [#]` - Minimum number of players that need to vote for a single map. 0 to ignore that. -- `mapvote_waittime [#]` - Number of seconds people can't vote for a new map when they enter a map. -- `maplistname "maplist.ini"` - filename/location of the file containing all the maps people can vote for. -- `mv_public [0/1]` - When on (1), it will show what maps someone votes for. +- Server settings: + - `use_mapvote [0/1]` - toggle to turn mapvoting on (1) or off (0). + - `mapvote_pass [0..100]` - Percentage of votes a single map needs before the vote passes. + - `mapvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. + - `mapvote_min [#]` - Minimum number of players that need to vote for a single map. 0 to ignore that. + - `mapvote_waittime [#]` - Number of seconds people can't vote for a new map when they enter a map. + - `maplistname "maplist.ini"` - filename/location of the file containing all the maps people can vote for. + - `mv_public [0/1]` - When on (1), it will show what maps someone votes for. ### Kickvoting This offers clients to kick those unwanted players from the server when no admin is about. #### Commands -- `use_kickvote [0/1]` - toggle to turn kickvoting on (1) or off (0). -- `kickvote_pass [0..100]` - Percentage of votes a single player needs before the vote passes. -- `kickvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. -- `kickvote_min [#]` - Minimum number of players that need to vote for a single player. 0 to ignore that. -- `kickvote_tempban [0/1]` - When set to 1, it will ban the kicked person until the map changes -- `vk_public [0/1]` - When on (1), it will show who votes for who. -- `kicklist` - this will display a list of all players with their id number. (client side) -- `votekick "player"` - this will cast your votekick vote for player "player". (client side) -- `votekicknum [#]` - this will cast your votekick vote for the player with id number #. (client side) +- Server settings: + - `use_kickvote [0/1]` - toggle to turn kickvoting on (1) or off (0). + - `kickvote_pass [0..100]` - Percentage of votes a single player needs before the vote passes. + - `kickvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. + - `kickvote_min [#]` - Minimum number of players that need to vote for a single player. 0 to ignore that. + - `kickvote_tempban [0/1]` - When set to 1, it will ban the kicked person until the map changes + - `vk_public [0/1]` - When on (1), it will show who votes for who. +- Client settings: + - `kicklist` - this will display a list of all players with their id number + - `votekick "player"` - this will cast your votekick vote for player "player" + - `votekicknum [#]` - this will cast your votekick vote for the player with id number # + ### Configvoting Configvoting allows clients to vote for a certain configuration, predefined by the server admin, to be put on the server, in the same way as mapvoting. To vote for a config, open up the menu ('menu' in the console) and select Configvoting from the list. #### Commands -- `use_cvote [0/1]` - toggle to turn configvoting on (1) or off (0). -- `cvote_pass [0..100]` - Percentage of votes a single config needs before the vote passes. -- `cvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. -- `cvote_min [#]` - Minimum number of players that need to vote for a single config. 0 to ignore that. -- `configlistname "configlist.ini"` - filename/location of the file containing all the configs people can vote for. +- Server settings: + - `use_cvote [0/1]` - toggle to turn configvoting on (1) or off (0). + - `cvote_pass [0..100]` - Percentage of votes a single config needs before the vote passes. + - `cvote_need [0..100]` - Percentage of players that has to vote before any vote may pass. + - `cvote_min [#]` - Minimum number of players that need to vote for a single config. 0 to ignore that. + - `configlistname "configlist.ini"` - filename/location of the file containing all the configs people can vote for. ### Teamplay Enables standard 2-team round-based teamplay. Kill everyone on the other team to win a round. #### Commands -- `teamplay [0/1]` (default: "0") - setting to 1 enables teamplay, requires new map or server restart -- `sv t[i]name ` - Where i is 1, 2 or 3, sets a team name in the absence of action.ini. Will take effect immediately. Example: sv t1name "Robbers" -- `sv t[i]skin ` - Where i is 1, 2 or 3, sets a team skin in the absence of action.ini. Will take effect on a new round of teamplay or a new map. Example: sv t2skin "male/ctf_b" -- `sv t[i]skin_index ` - Where i is 1, 2 or 3, sets a team skin index in the absence of action.ini. Will take effect on a new map or server restart. Example: sv t3skin_index "siris_i" +- Server settings: + - `teamplay [0/1]` (default: "0") - setting to 1 enables teamplay, requires new map or server restart + - `sv t[i]name ` - Where i is 1, 2 or 3, sets a team name in the absence of action.ini. Will take effect immediately. Example: sv t1name "Robbers" + - `sv t[i]skin ` - Where i is 1, 2 or 3, sets a team skin in the absence of action.ini. Will take effect on a new round of teamplay or a new map. Example: sv t2skin "male/ctf_b" + - `sv t[i]skin_index ` - Where i is 1, 2 or 3, sets a team skin index in the absence of action.ini. Will take effect on a new map or server restart. Example: sv t3skin_index "siris_i" ### Tourney For those who want to offer a Rocket Arena style one on one server, this mode is ideal. It will let two players spawn and face each other. The winner will stay for the next round and the loser will go back to the queue. The winner then takes up the next player in the queue. @@ -195,14 +200,16 @@ For those who want to offer a Rocket Arena style one on one server, this mode is Example: There are 4 players on a server, A, B, C and Z. Both A and B play each other, C and Z are in the queue. B wins and A gets put back in the queue (which then is C, Z and A). Then, B plays the next in the queue, namely C. If B would win, he'd face Z and C would get put back in the queue. If C would win, he would play Z next and B would be put back in the queue. #### Commands -- `use_tourney [0/1]` - This will turn tourney mode on (1) or off (0). To use it, teamplay needs to be on. (1) -- `tourney_lca [0/1]` - Set to 1 to enable "Lights Camera Action" for tourney mode, set to 0 for classic silence +- Server settings: + - `use_tourney [0/1]` - This will turn tourney mode on (1) or off (0). To use it, teamplay needs to be on. (1) + - `tourney_lca [0/1]` - Set to 1 to enable "Lights Camera Action" for tourney mode, set to 0 for classic silence ### 3 Teams Teamplay For more exciting teamplay games, TNG offers a 3 Teams mode of play. This variant of Teamplay will put three teams against each other. #### Commands -- `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). To use it, teamplay needs to be on. (1) +- Server settings: + - `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). To use it, teamplay needs to be on. (1) ### Capture the Flag The popular gameplay mode from many games and mods is available in TNG as well. In this mode, there are two teams, Red and Blue. Both have a flag on the map, and they need to take the enemy flag and bring it to their own to score a point. (Capture the Flag, or cap for short) The other team will attempt the same of course. @@ -212,12 +219,14 @@ To prevent spawn camping, there is an invulnerability/shield mode to give some p Flag locations and CTF player spawns should be specified in tng/mapname.ctf files. #### Commands -- `ctf [0/1]` - This will turn CTF on (1) or off (0). It will automatically turn Teamplay on (1). -- `capturelimit [#]` - The maximum number of captures before a map will change. Set to 0 to ignore that. -- `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. -- `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. -- `drop flag` - Drop the flag if you're holding it. (client side) -- `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) +- Server settings: + - `ctf [0/1]` - This will turn CTF on (1) or off (0). It will automatically turn Teamplay on (1). + - `capturelimit [#]` - The maximum number of captures before a map will change. Set to 0 to ignore that. + - `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. + - `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. + - `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) +- Client settings: + - `drop flag` - Drop the flag if you're holding it ### Domination This is most closely related to Unreal Tournament's domination mode, but is also similar to king of the hill and conquest modes in general. There are fixed flags (typically 3) scattered around the map, and touching a flag will claim it for your team. Keep control of flags to score points; your team gains 1 point per flag controlled every second. The score limit is 200 per flag in 2-team mode, or 150 per flag in 3-team mode, which means a typical 2-team game with 3 flags has a score limit of 600. @@ -225,9 +234,10 @@ This is most closely related to Unreal Tournament's domination mode, but is also Flag locations can be specified in tng/mapname.dom files. If not found, the server will attempt to generate them automatically from player spawn locations. #### Commands -- `dom [0/1]` - This will turn Domination mode on (1) or off (0), and also turns Teamplay on (1). -- `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). -- `uvtime [#]` - The number of seconds * 10 of the duration of the 'shield' effect (ex: 40 = 4 secs). +- Server settings: + - `dom [0/1]` - This will turn Domination mode on (1) or off (0), and also turns Teamplay on (1). + - `use_3teams [0/1]` - This will turn 3 Teams on (1) or off (0). + - `uvtime [#]` - The number of seconds * 10 of the duration of the 'shield' effect (ex: 40 = 4 secs). ### Matchmode Matchmode is a special form of Teamplay, made for clanmatches. It has several features which can be useful during matches and league games. It makes sure the whole timelimit is played. @@ -235,31 +245,35 @@ Matchmode is a special form of Teamplay, made for clanmatches. It has several fe Clients will have a few more things to do during matchmode: they have to have a captain and substitutes. Everybody in a team will only be able to talk to his/her own team, with the exception of the captain (see below). Subs are players who are part of the team but aren't playing at the moment. With limchasecam enabled, they will be limited to viewing their own team. #### Commands -- `matchmode [0/1]` - Turns Matchmode on (1) or off (0). Turning Matchmode on will turn Teamplay on too. -- `captain` - Become the team's captain if not captain exists yet. A captain can ready/unready his team and speak to the other team. (client side) -- `sub` - this will make you a sub for the team or remove you from the subs and back in the team. (client side) -- `ready` - this will ready/unready the team. A new round won't start if a team isn't read. (client side) -- `teamname "name"` - allows the captain to set the name of his/her team. (client side) -- `teamskin "male/resdog"` - allows the captain to set the name of his/her team. (client side) -- `mm_forceteamtalk [0/1/2]` - this will change the way people can talk on the server. When set to 0, it will be the same as with normal teamplay. When set to 1, the teams will not be able to talk with each other, except for the admin and the captains. When set to 2, teams can talk with each other until the match starts and when the match is paused or over. -- `mm_adminpwd ` - this will set the password for match admins. -- `matchadmin ` - this will allow a player to get admin status. (client side) -- `mm_allowlock [0/1]` - when on (1), this will allow captains to lock/unlock their teams. -- `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map. (client side) -- `unlock` - allows a captain to unlock his team. (client side) -- `mm_captain_teamname - [0/1]` - default 0, if enabled (1) then the team name will change to the name of the captain, prepended with the string "Team", for example, "Team Suislide" if Suislide is the captain of that team. +- Server settings: + - `matchmode [0/1]` - Turns Matchmode on (1) or off (0). Turning Matchmode on will turn Teamplay on too. + - `mm_forceteamtalk [0/1/2]` - this will change the way people can talk on the server. When set to 0, it will be the same as with normal teamplay. When set to 1, the teams will not be able to talk with each other, except for the admin and the captains. When set to 2, teams can talk with each other until the match starts and when the match is paused or over. + - `mm_adminpwd ` - this will set the password for match admins. + - `mm_allowlock [0/1]` - when on (1), this will allow captains to lock/unlock their teams. + - `mm_captain_teamname - [0/1]` - default 0, if enabled (1) then the team name will change to the name of the captain, prepended with the string "Team", for example, "Team Suislide" if Suislide is the captain of that team. +- Client settings: + - `captain` - Become the team's captain if not captain exists yet. A captain can ready/unready his team and speak to the other team + - `sub` - this will make you a sub for the team or remove you from the subs and back in the team + - `ready` - this will ready/unready the team. A new round won't start if a team isn't read + - `teamname "name"` - allows the captain to set the name of his/her team + - `teamskin "male/resdog"` - allows the captain to set the name of his/her team + - `matchadmin ` - this will allow a player to get admin status + - `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map + - `unlock` - allows a captain to unlock his team ### Voice Command The voice command allows clients to play taunts for other players to hear. (as long as they have the sound file) #### Commands -- `use_voice [0/1]` - When on (1), it will allow the use of voice commands. -- `voice "sound.wav"` - this will play sound.wav for all players to hear. (as long as the others have sound.wav) This command requires the .wav extension. (client side) +- Server settings: + - `use_voice [0/1]` - When on (1), it will allow the use of voice commands. +- Client settings: + - `voice "sound.wav"` - this will play sound.wav for all players to hear. (as long as the others have sound.wav) This command requires the .wav extension. (client side) ### Low Lag Sounds To reduce the number of packets being sent by AQ2 during big firefights, TNG has included the option to turn Low Lag Sounds on. This will use a different set of sounds for each weapon, ones that reduce the number of packets being sent. -When you use this, the sounds of the weapons will change to those of Quake 2 weapons. You can update your sounds with the following: +In AQtion, there are already gun sounds installed that you can select from. If using non-AQtion clients, when you use this, the sounds of the weapons will change to those of Quake 2 weapons. You can update your sounds with the following: - MK23 Pistol: `sound/weapons/mk23fire.wav` - Handcannon: `sound/weapons/cannon_fire.wav` @@ -269,21 +283,24 @@ When you use this, the sounds of the weapons will change to those of Quake 2 wea - M3 Super Shotgun: `sound/weapons/shotgf1b.wav` #### Commands -- `llsound [0/1]` - When on (1), Low Lag Sounds will be used. -- `cl_mk23_sound` -- `cl_mp5_sound` -- `cl_m4_sound` -- `cl_m3_sound` -- `cl_hc_sound` -- `cl_ssg_sound` +- Server setting: + - `llsound [0/1]` +- Client settings: + - `cl_mk23_sound` + - `cl_mp5_sound` + - `cl_m4_sound` + - `cl_m3_sound` + - `cl_hc_sound` + - `cl_ssg_sound` ### Announcer The announcer that says "Lights, Camera, Action!" has his vocabulary increased by several words, announcing when people perform several feats, such as high accuracy ("Accuracy") or make an impressive shot ("Impressive") but also when a team wins ("Team X Wins") or when the fraglimit is about to get hit. #### Commands -- `use_warnings [0/1]` - When on (1), it will play all warnings such as "3 minutes left" and "Team X Wins". -- `use_rewards [0/1]` - When on (1), it will play the sounds on rewards such as "Impressive". -- `use_killcounts [0/1]` - When on (1), server will announce kill counts. +- Server settings: + - `use_warnings [0/1]` - When on (1), it will play all warnings such as "3 minutes left" and "Team X Wins". + - `use_rewards [0/1]` - When on (1), it will play the sounds on rewards such as "Impressive". + - `use_killcounts [0/1]` - When on (1), server will announce kill counts. ### Kevlar Helmet A new item, the Kevlar Helmet acts like the Kevlar Vest does, only it protects the head of the player wearing it. To disable it, use item banning. @@ -327,20 +344,23 @@ To support the %L and %S commands, the server needs to install the location file We have added the extra 'punch' attack, which basically is a weaker version of a kick. It is very close range and does up to 20 damage and can punch the weapon of a player out of his hands. When bandaging, shooting, reloading or zoomed with a sniper rifle, you cannot punch. #### Commands -- `use_punch [0/1]` - when on (1), it will allow clients to use the punch command -- `punch` - this will preform a punch. (client side) +- Server setting: + - `use_punch [0/1]` - when on (1), it will allow clients to use the punch command +- Client setting: + - `punch` - this will preform a punch ### Lens command The lens command is in TNG and offers increased control over the zooming of the sniper rifle. #### Commands -- `lens` - changes zoom on sniper rifle. (Client side) -- `lens [#]` - 1,2,4,6 - if you use any other number it'll go to the nearest zoom mode. (Client side) -- `lens in` - zooms the rifle in to the next zooming level. (client side) -- `lens out` - zooms the rifle out to the previous zooming level. (client side) +- Client settings: + - `lens` - changes zoom on sniper rifle + - `lens [#]` - 1,2,4,6 - if you use any other number it'll go to the nearest zoom mode + - `lens in` - zooms the rifle in to the next zooming level + - `lens out` - zooms the rifle out to the previous zooming level ### New Say Variables -TNG offers several new variables clients can use in their text: +TNG offers several new variables clients can use in their text (Client settings): - `%K` - shows the nickname of the person you killed last. - `%P` - shows the nickname of the person you damaged last. - `%D` - shows the location where you hit your enemy last. This can either be head, chest, stomach, legs, kevlar vest or kevlar helmet. @@ -350,17 +370,14 @@ TNG offers several new variables clients can use in their text: - `%F` - shows the weapon the enemy is holding. - `%me` - shows your nickname like IRC's /me. -### Roundtimeleft +### Time and Roundtimeleft +By typing 'time' in the console, a client can see the remaining time on that map. This doesn't work in matchmode. By typing 'roundtimeleft' in the console during a round, it will give a general indication of the amount of time left that round. This only works when there is a roundtimelimit. #### Commands -- `roundtimeleft` - displays remaining roundtime. (client side) - -### Time -By typing 'time' in the console, a client can see the remaining time on that map. This doesn't work in matchmode - -#### Commands -- `time` - display the remaining time (client side) +- Client settings: + - `time` - display the remaining time + - `roundtimeleft` - display the remaining roundtime ### sv stuffcmd This feature will allow people who got rcon to send commands to the clients. @@ -403,8 +420,9 @@ Basically, this is a simplified and immediate form of Vote Rotation. When the cu Prevent unpopular maps from keeping your server empty. This feature will rotate the map after the server has been empty for some number of minutes. You can also specify a config file to execute, which can be used to reset the server to default settings after configvote or rotate between multiple mode configs. #### Commands -- `empty_rotate [#]` - Number of minutes before rotating empty server. Default 0 disables this feature. -- `empty_exec "file.cfg"` - Optional config to exec with empty rotate. Default blank disables this. +- Server settings: + - `empty_rotate [#]` - Number of minutes before rotating empty server. Default 0 disables this feature. + - `empty_exec "file.cfg"` - Optional config to exec with empty rotate. Default blank disables this. ### Bandage Text When a client starts bandaging, a message ("You've started bandaging") will appear. Also the Health icon will appear on the bottom of the screen. @@ -413,9 +431,10 @@ When a client starts bandaging, a message ("You've started bandaging") will appe A new variable allows the server to give players a starting weapon during deathmatch. When set, the players will get the weapon of that type with full ammo when they spawn. #### Commands -- `dmweapon "weaponname"` - Starting weapon for DM mode (dmweapon), default is "MK23 Pistol". -- `dm_choose [0/1]` - allows players to choose their starting weapon in DM mode, 0 disables/1 enables -- `dm_shield [#]` - Sets a time value for how long the invulnerability shield in DM mode. Value of 30 = 3 seconds, for example. +- Server settings: + - `dmweapon "weaponname"` - Starting weapon for DM mode (dmweapon), default is "MK23 Pistol". + - `dm_choose [0/1]` - allows players to choose their starting weapon in DM mode, 0 disables/1 enables + - `dm_shield [#]` - Sets a time value for how long the invulnerability shield in DM mode. Value of 30 = 3 seconds, for example. ### Control Characters Some proxies use control characters to hide the name of the user. However, it is used by several players as well to cause abuse and do spam messages. diff --git a/doc/espionage.md b/doc/espionage.md index 14b0cec4a..8159c1601 100644 --- a/doc/espionage.md +++ b/doc/espionage.md @@ -62,7 +62,11 @@ There are some settings you need to setup to enable it: * **esp_leaderenhance** [0/1] - This enhances the leader in that: * The leader's bandage time is halved * The leader receives periodic medkits, if medkit_max is > 0, to be used when they bandage -* **esp_enhancedslippers** [0/1] - to remove limping from leg damage (falling and shooting), and 50% damage reduction when falling long distances. This used to be called `e_enhancedSlippers` from the original ETE, but was renamed to keep in with the naming convention of Espionage options +* **esp_enhancedslippers** [0/1/2] - This enhances the Stealth Slippers: + * 0 means the Stealth Slippers are not enhanced in any way (classic) + * 1 removes limping from all leg damage and 50% damage reduction when falling long distances + * 2 removes limping from all leg damage and increase the height before fall damage occurs by 25%. + This used to be called `e_enhancedSlippers` from the original ETE, but was renamed to keep in with the naming convention of the new Espionage options. *This option does not require Espionage to be enabled, it can be used in any game mode.* * **esp_atl** [0/1] - default `0`, this forces ATL mode if `1`, even if the scenario loaded for the map is for ETV mode. Has no effect on any map that is specifically setup for ATL mode or maps that load safe defaults, which are always ATL mode. * **ETV-Specific Cvars**: diff --git a/inc/common/files.h b/inc/common/files.h index c221e1b6e..0f19b5ca9 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -125,3 +125,7 @@ FILE *Q_fopen(const char *path, const char *mode); extern cvar_t *fs_game; extern char fs_gamedir[]; + +#if USE_AQTION +#define DOWNLOADSERVER "http://gameassets.aqtiongame.com/" +#endif diff --git a/src/client/http.c b/src/client/http.c index 3607dfd52..d2e6c366a 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -503,9 +503,17 @@ void HTTP_SetServer(const char *url) if (!url) { url = cl_http_default_url->string; download_default_repo = true; - } else { + } + #if USE_AQTION + else { + url = DOWNLOADSERVER; + download_default_repo = true; + } + #else + else { download_default_repo = false; } + #endif if (!*url) return; diff --git a/src/server/main.c b/src/server/main.c index 3f2fa88f5..5ca98e6cc 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1082,6 +1082,12 @@ static void send_connect_packet(client_t *newcl, int nctype) dlstring1 = " dlserver="; dlstring2 = sv_downloadserver->string; } + #if USE_AQTION + else { + dlstring1 = " dlserver="; + dlstring2 = DOWNLOADSERVER; + } + #endif Netchan_OutOfBand(NS_SERVER, &net_from, "client_connect%s%s%s%s map=%s", ncstring, acstring, dlstring1, dlstring2, newcl->mapname); From c7a36a597d5c0ac6db1ddac6b10da7cc0f4d6752 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 22:40:59 -0500 Subject: [PATCH 044/974] Moved DOWNLOADSERVER define to common.h --- inc/common/common.h | 4 ++++ inc/common/files.h | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index 194eeefc4..3c5fe5173 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -49,6 +49,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #define COM_HISTORYFILE_NAME ".conhistory" #define COM_DEMOCACHE_NAME ".democache" +#if USE_AQTION +#define DOWNLOADSERVER "http://gameassets.aqtiongame.com/" +#endif + #define MAXPRINTMSG 4096 #define MAXERRORMSG 1024 diff --git a/inc/common/files.h b/inc/common/files.h index 0f19b5ca9..c221e1b6e 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -125,7 +125,3 @@ FILE *Q_fopen(const char *path, const char *mode); extern cvar_t *fs_game; extern char fs_gamedir[]; - -#if USE_AQTION -#define DOWNLOADSERVER "http://gameassets.aqtiongame.com/" -#endif From ac197c581b7a3947222c24adc631bbd3027edce3 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 22:46:56 -0500 Subject: [PATCH 045/974] Moved DOWNLOADSERVER define to common.h --- src/client/http.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/http.c b/src/client/http.c index d2e6c366a..f6d2d23a8 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -805,6 +805,19 @@ static void process_downloads(void) goto fail1; } + //404 is non-fatal unless accessing default repository + if (response == 404) { + if (!download_default_repo || !dl->path[0]) { + level = PRINT_ALL; + goto fail1; + } else { + // Try the fallback server + snprintf(fallback_url, sizeof(fallback_url), "%s%s", DOWNLOADSERVER, dl->path); + start_download(dl, fallback_url); + return; + } + } + //every other code is treated as fatal //not marking download as done since //we are falling back to UDP From b9402f06c68fc8d87b96bc8e04d03713b4ac9aea Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 23:08:03 -0500 Subject: [PATCH 046/974] Will look at this some other time in another branch --- src/client/http.c | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/client/http.c b/src/client/http.c index f6d2d23a8..3607dfd52 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -503,17 +503,9 @@ void HTTP_SetServer(const char *url) if (!url) { url = cl_http_default_url->string; download_default_repo = true; - } - #if USE_AQTION - else { - url = DOWNLOADSERVER; - download_default_repo = true; - } - #else - else { + } else { download_default_repo = false; } - #endif if (!*url) return; @@ -805,19 +797,6 @@ static void process_downloads(void) goto fail1; } - //404 is non-fatal unless accessing default repository - if (response == 404) { - if (!download_default_repo || !dl->path[0]) { - level = PRINT_ALL; - goto fail1; - } else { - // Try the fallback server - snprintf(fallback_url, sizeof(fallback_url), "%s%s", DOWNLOADSERVER, dl->path); - start_download(dl, fallback_url); - return; - } - } - //every other code is treated as fatal //not marking download as done since //we are falling back to UDP From 5ac850e5ef788efd30a7a2d60c9eabbc9b935835 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 16 Feb 2024 23:21:23 -0500 Subject: [PATCH 047/974] Suppressed warning on darwin --- meson.build | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meson.build b/meson.build index c70f62e99..e215ba9e3 100644 --- a/meson.build +++ b/meson.build @@ -403,6 +403,8 @@ if not win32 endif if host_machine.system() == 'darwin' + # Darwin is warning-spamming because of its SDL_config defines + add_project_arguments('-Wno-macro-redefined', language : 'c') opengl = dependency('opengl', allow_fallback: win32, default_options: fallback_opt, From 5c70f51265ab98c112bb9956daa83e295a7341fd Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 17 Feb 2024 12:18:44 -0500 Subject: [PATCH 048/974] Added JukS's grenade impact, new mod, sound effects needed --- src/action/g_combat.c | 62 ++++++++++++++++++++++++++++--------------- src/action/g_local.h | 4 +++ src/action/g_spawn.c | 9 ++++--- src/action/g_weapon.c | 15 +++++++++++ src/action/p_client.c | 4 +++ 5 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 01677de54..b0f868992 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -412,6 +412,24 @@ void VerifyHeadShot(vec3_t point, vec3_t dir, float height, vec3_t newpoint) #define HEAD_HEIGHT 12.0f +qboolean check_head_success(vec3_t point, vec3_t dir, vec3_t targ_origin, float targ_maxs2) { + vec3_t new_point, deconst_point, deconst_dir; + + // De-constify the point and dir + VectorCopy(point, deconst_point); + VectorCopy(dir, deconst_dir); + VerifyHeadShot(deconst_point, deconst_dir, HEAD_HEIGHT, new_point); + VectorSubtract(new_point, targ_origin, new_point); + + if ((targ_maxs2 - new_point[2]) < HEAD_HEIGHT + && (fabsf(new_point[1])) < HEAD_HEIGHT * .8 + && (fabsf(new_point[0])) < HEAD_HEIGHT * .8) + { + return true; + } + return false; +} + void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const vec3_t dir, const vec3_t point, const vec3_t normal, int damage, int knockback, int dflags, int mod) @@ -496,6 +514,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve case MOD_SNIPER: case MOD_KNIFE: case MOD_KNIFE_THROWN: + case MOD_GRENADE_IMPACT: z_rel = point[2] - targ->s.origin[2]; from_top = targ_maxs2 - z_rel; @@ -504,23 +523,8 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve bleeding = 1; instant_dam = 0; - if (from_top < 2 * HEAD_HEIGHT) - { - vec3_t new_point, deconst_point, deconst_dir; - - //De-constify the point and dir - VectorCopy(point, deconst_point); - VectorCopy(dir, deconst_dir); - VerifyHeadShot(deconst_point, deconst_dir, HEAD_HEIGHT, new_point); - VectorSubtract(new_point, targ->s.origin, new_point); - //gi.cprintf(attacker, PRINT_HIGH, "z: %d y: %d x: %d\n", (int)(targ_maxs2 - new_point[2]),(int)(new_point[1]) , (int)(new_point[0]) ); - - if ((targ_maxs2 - new_point[2]) < HEAD_HEIGHT - && (abs (new_point[1])) < HEAD_HEIGHT * .8 - && (abs (new_point[0])) < HEAD_HEIGHT * .8) - { - head_success = 1; - } + if (from_top < 2 * HEAD_HEIGHT) { + head_success = check_head_success(point, dir, targ->s.origin, targ_maxs2); } if (head_success) @@ -557,8 +561,11 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if (attacker->client) gi.cprintf(attacker, PRINT_HIGH, "You hit %s in the head\n", client->pers.netname); - if (mod != MOD_KNIFE && mod != MOD_KNIFE_THROWN) + if (mod == MOD_GRENADE_IMPACT) + gi.sound(targ, CHAN_VOICE, level.snd_grenhead, 1, ATTN_NORM, 0); + else if (mod != MOD_KNIFE && mod != MOD_KNIFE_THROWN) gi.sound(targ, CHAN_VOICE, level.snd_headshot, 1, ATTN_NORM, 0); + } else if (mod == MOD_SNIPER) { @@ -580,10 +587,18 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve { gi.cprintf( attacker, PRINT_HIGH, "%s has a Kevlar Helmet - AIM FOR THE BODY!\n", client->pers.netname ); - gi.cprintf( targ, PRINT_HIGH, "Kevlar Helmet absorbed a part of %s's shot\n", - attacker->client->pers.netname ); + if (mod == MOD_GRENADE_IMPACT) { + gi.cprintf( targ, PRINT_HIGH, "Kevlar Helmet absorbed a part of %s's grenade\n", + attacker->client->pers.netname ); + } else { + gi.cprintf( targ, PRINT_HIGH, "Kevlar Helmet absorbed a part of %s's shot\n", + attacker->client->pers.netname ); + } } - gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); + if (mod == MOD_GRENADE_IMPACT) + gi.sound(targ, CHAN_VOICE, level.snd_grenhelm, 1, ATTN_NORM, 0); + else + gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); damage = (int)(damage / 2); bleeding = 0; instant_dam = 1; @@ -666,7 +681,10 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve gi.cprintf(targ, PRINT_HIGH, "Kevlar Vest absorbed most of %s's shot\n", attacker->client->pers.netname); } - gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); + if (mod == MOD_GRENADE_IMPACT) + gi.sound(targ, CHAN_VOICE, level.snd_grenbody, 1, ATTN_NORM, 0); + else + gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); damage = (int)(damage / 10); bleeding = 0; instant_dam = 1; diff --git a/src/action/g_local.h b/src/action/g_local.h index 145d818b1..7078f5b16 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -817,6 +817,9 @@ typedef struct int snd_knifethrow; int snd_kick; int snd_noammo; + int snd_grenhead; + int snd_grenhelm; + int snd_grenbody; int model_null; int model_lsight; @@ -956,6 +959,7 @@ typedef enum { MOD_BLEEDING, MOD_KICK, MOD_GRAPPLE, + MOD_GRENADE_IMPACT, MOD_TOTAL, MOD_FRIENDLY_FIRE = 0x8000000 } ModTable; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 53991cdc6..abebb734d 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1781,11 +1781,14 @@ void SP_worldspawn (edict_t * ent) } level.snd_silencer = gi.soundindex("misc/silencer.wav"); // all silencer weapons - level.snd_headshot = gi.soundindex("misc/headshot.wav"); // headshot sound - level.snd_vesthit = gi.soundindex("misc/vest.wav"); // kevlar hit + level.snd_headshot = gi.soundindex("misc/headshot.wav"); // the glorious headshot sound + level.snd_vesthit = gi.soundindex("misc/vest.wav"); // kevlar hit (pew pew) level.snd_knifethrow = gi.soundindex("misc/flyloop.wav"); // throwing knife level.snd_kick = gi.soundindex("weapons/kick.wav"); // not loaded by any item, kick sound - level.snd_noammo = gi.soundindex("weapons/noammo.wav"); + level.snd_noammo = gi.soundindex("weapons/noammo.wav"); // click! + level.snd_grenhead = gi.soundindex("weapons/grenhead.wav"); // grenade impact to head + level.snd_grenhelm = gi.soundindex("weapons/grenhelm.wav"); // grenade impact to helmet + level.snd_grenbody = gi.soundindex("weapons/grenbody.wav"); // grenade impact to body gi.soundindex("tng/1_minute.wav"); gi.soundindex("tng/3_minutes.wav"); diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index 372a32104..a57391da5 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -739,7 +739,22 @@ static void Grenade_Touch (edict_t *ent, edict_t *other, cplane_t *plane, csurfa } return; } + // From JukS mod: + // New stuff: Make some damage if grenade hits to someone -JukS- + // Grenade velocities by Throw type around: Long(920), Medium(720), Short(400) + float grenSpeed = VectorNormalize(ent->velocity); + if (grenSpeed > 500) // Don't make damage with slow speeds + { + trace_t tr; + vec3_t end; + // Formula to handle damage amount. With /25-20 we'll get 0 dmg at minimum speed (500) + int dmgBySpeed = (int)(grenSpeed) / 25 - 20; + PRETRACE(); + tr = gi.trace(ent->owner->s.origin, NULL, NULL, end, ent->owner, MASK_SHOT); + POSTTRACE(); + T_Damage(other, ent, ent->owner, ent->s.origin, tr.endpos, tr.plane.normal, dmgBySpeed, 0, DAMAGE_NO_ARMOR, MOD_GRENADE_IMPACT); + } // zucc not needed since grenades don't blow up on contact //ent->enemy = other; //Grenade_Explode(ent); diff --git a/src/action/p_client.c b/src/action/p_client.c index 0c7848243..85624b15e 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1228,6 +1228,10 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) message = " was caught by"; message2 = "'s grapple"; break; + case MOD_GRENADE_IMPACT: + message = " was deeply impacted by"; + message2 = "'s grenade"; + break; } //end of case (mod) if (message) From 9428b28bb0ee831663768b05fb78dea946b2ccba Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 17 Feb 2024 13:06:45 -0500 Subject: [PATCH 049/974] Updated sound channel for grenade bonks --- src/action/g_combat.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index b0f868992..82d184a6f 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -412,7 +412,7 @@ void VerifyHeadShot(vec3_t point, vec3_t dir, float height, vec3_t newpoint) #define HEAD_HEIGHT 12.0f -qboolean check_head_success(vec3_t point, vec3_t dir, vec3_t targ_origin, float targ_maxs2) { +qboolean check_head_success(const vec3_t point, const vec3_t dir, vec3_t targ_origin, float targ_maxs2) { vec3_t new_point, deconst_point, deconst_dir; // De-constify the point and dir @@ -596,7 +596,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve } } if (mod == MOD_GRENADE_IMPACT) - gi.sound(targ, CHAN_VOICE, level.snd_grenhelm, 1, ATTN_NORM, 0); + gi.sound(targ, CHAN_ITEM, level.snd_grenhelm, 1, ATTN_NORM, 0); else gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); damage = (int)(damage / 2); @@ -682,7 +682,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve attacker->client->pers.netname); } if (mod == MOD_GRENADE_IMPACT) - gi.sound(targ, CHAN_VOICE, level.snd_grenbody, 1, ATTN_NORM, 0); + gi.sound(targ, CHAN_ITEM, level.snd_grenbody, 1, ATTN_NORM, 0); else gi.sound(targ, CHAN_ITEM, level.snd_vesthit, 1, ATTN_NORM, 0); damage = (int)(damage / 10); From f27aac40d3e013818b6ca3275ebef513337bef7d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Feb 2024 17:09:11 +0300 Subject: [PATCH 050/974] Simplify overflow checks in BSP loading code. --- src/common/bsp.c | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 37999d74f..87f1d9da4 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -233,7 +233,7 @@ LOAD(Brushes) { mbrush_t *out; int i; - uint32_t firstside, numsides, lastside; + uint32_t firstside, numsides; bsp->numbrushes = count; bsp->brushes = ALLOC(sizeof(*out) * count); @@ -242,8 +242,7 @@ LOAD(Brushes) for (i = 0; i < count; i++, out++) { firstside = BSP_Long(); numsides = BSP_Long(); - lastside = firstside + numsides; - if (lastside < firstside || lastside > bsp->numbrushsides) { + if ((uint64_t)firstside + numsides > bsp->numbrushsides) { DEBUG("bad brushsides"); return Q_ERR_INVALID_FORMAT; } @@ -368,7 +367,7 @@ LOAD(Faces) { mface_t *out; int i, j; - uint32_t firstedge, numedges, lastedge; + uint32_t firstedge, numedges; uint32_t planenum, texinfo, side; uint32_t lightofs; @@ -389,7 +388,6 @@ LOAD(Faces) firstedge = BSP_Long(); numedges = BSP_ExtLong(); - lastedge = firstedge + numedges; if (numedges < 3) { DEBUG("too few surfedges"); return Q_ERR_INVALID_FORMAT; @@ -398,7 +396,7 @@ LOAD(Faces) DEBUG("too many surfedges"); return Q_ERR_INVALID_FORMAT; } - if (lastedge < firstedge || lastedge > bsp->numsurfedges) { + if ((uint64_t)firstedge + numedges > bsp->numsurfedges) { DEBUG("bad surfedges"); return Q_ERR_INVALID_FORMAT; } @@ -463,10 +461,10 @@ LOAD(Leafs) mleaf_t *out; int i; uint32_t cluster, area; - uint32_t firstleafbrush, numleafbrushes, lastleafbrush; + uint32_t firstleafbrush, numleafbrushes; #if USE_REF int j; - uint32_t firstleafface, numleaffaces, lastleafface; + uint32_t firstleafface, numleaffaces; #endif if (!count) { @@ -512,8 +510,7 @@ LOAD(Leafs) firstleafface = BSP_ExtLong(); numleaffaces = BSP_ExtLong(); - lastleafface = firstleafface + numleaffaces; - if (lastleafface < firstleafface || lastleafface > bsp->numleaffaces) { + if ((uint64_t)firstleafface + numleaffaces > bsp->numleaffaces) { DEBUG("bad leaffaces"); return Q_ERR_INVALID_FORMAT; } @@ -528,8 +525,7 @@ LOAD(Leafs) firstleafbrush = BSP_ExtLong(); numleafbrushes = BSP_ExtLong(); - lastleafbrush = firstleafbrush + numleafbrushes; - if (lastleafbrush < firstleafbrush || lastleafbrush > bsp->numleafbrushes) { + if ((uint64_t)firstleafbrush + numleafbrushes > bsp->numleafbrushes) { DEBUG("bad leafbrushes"); return Q_ERR_INVALID_FORMAT; } @@ -551,7 +547,7 @@ LOAD(Nodes) int i, j; uint32_t planenum, child; #if USE_REF - uint32_t firstface, numfaces, lastface; + uint32_t firstface, numfaces; #endif if (!count) { @@ -597,8 +593,7 @@ LOAD(Nodes) firstface = BSP_ExtLong(); numfaces = BSP_ExtLong(); - lastface = firstface + numfaces; - if (lastface < firstface || lastface > bsp->numfaces) { + if ((uint64_t)firstface + numfaces > bsp->numfaces) { DEBUG("bad faces"); return Q_ERR_INVALID_FORMAT; } @@ -621,7 +616,7 @@ LOAD(SubModels) int i, j; uint32_t headnode; #if USE_REF - uint32_t firstface, numfaces, lastface; + uint32_t firstface, numfaces; #endif if (!count) { @@ -665,8 +660,7 @@ LOAD(SubModels) } firstface = BSP_Long(); numfaces = BSP_Long(); - lastface = firstface + numfaces; - if (lastface < firstface || lastface > bsp->numfaces) { + if ((uint64_t)firstface + numfaces > bsp->numfaces) { DEBUG("bad faces"); return Q_ERR_INVALID_FORMAT; } @@ -704,7 +698,7 @@ LOAD(Areas) { marea_t *out; int i; - uint32_t numareaportals, firstareaportal, lastareaportal; + uint32_t numareaportals, firstareaportal; if (count > MAX_MAP_AREAS) { DEBUG("too many areas"); @@ -718,8 +712,7 @@ LOAD(Areas) for (i = 0; i < count; i++, out++) { numareaportals = BSP_Long(); firstareaportal = BSP_Long(); - lastareaportal = firstareaportal + numareaportals; - if (lastareaportal < firstareaportal || lastareaportal > bsp->numareaportals) { + if ((uint64_t)firstareaportal + numareaportals > bsp->numareaportals) { DEBUG("bad areaportals"); return Q_ERR_INVALID_FORMAT; } @@ -1282,7 +1275,7 @@ static size_t BSP_ParseExtensionHeader(bsp_t *bsp, lump_t *out, const byte *buf, for (int i = 0; i < numlumps; i++, l++) { for (int j = 0; j < q_countof(bspx_lumps); j++) { const xlump_info_t *e = &bspx_lumps[j]; - uint32_t ofs, len, end; + uint32_t ofs, len; if (strcmp(l->name, e->name)) continue; @@ -1294,8 +1287,7 @@ static size_t BSP_ParseExtensionHeader(bsp_t *bsp, lump_t *out, const byte *buf, break; } - end = ofs + len; - if (end < ofs || end > filelen) { + if ((uint64_t)ofs + len > filelen) { Com_WPrintf("Ignoring out of bounds %s lump\n", e->name); break; } @@ -1334,7 +1326,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) byte *buf; dheader_t *header; const lump_info_t *info; - uint32_t filelen, ofs, len, end, count, maxpos; + uint32_t filelen, ofs, len, count, maxpos; int i, ret; uint32_t lump_ofs[q_countof(bsp_lumps)]; uint32_t lump_count[q_countof(bsp_lumps)]; @@ -1392,8 +1384,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) for (i = 0, info = bsp_lumps; i < q_countof(bsp_lumps); i++, info++) { ofs = LittleLong(header->lumps[info->lump].fileofs); len = LittleLong(header->lumps[info->lump].filelen); - end = ofs + len; - if (end < ofs || end > filelen) { + if ((uint64_t)ofs + len > filelen) { Com_SetLastError(va("%s lump out of bounds", info->name)); ret = Q_ERR_INVALID_FORMAT; goto fail2; @@ -1415,7 +1406,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) // round to cacheline memsize += ALIGN(count * info->memsize, 64); - maxpos = max(maxpos, end); + maxpos = max(maxpos, ofs + len); } // load into hunk From 7a1b06ce970fc8afaa8dc398ca95f24853437574 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Feb 2024 20:17:23 +0300 Subject: [PATCH 051/974] Pack some BSP structures. --- inc/common/bsp.h | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 45b954d3a..4826ef911 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -49,8 +49,8 @@ typedef struct mtexinfo_s { // used internally due to name len probs //ZOID vec3_t axis[2]; vec2_t offset; struct image_s *image; // used for texturing - int numframes; struct mtexinfo_s *next; // used for animation + int numframes; #endif #if USE_CLIENT char material[16]; @@ -82,28 +82,27 @@ typedef struct { typedef struct mface_s { msurfedge_t *firstsurfedge; - int numsurfedges; - cplane_t *plane; - int drawflags; // DSURF_PLANEBACK, etc byte *lightmap; byte styles[MAX_LIGHTMAPS]; byte numstyles; byte hash; + uint16_t numsurfedges; mtexinfo_t *texinfo; vec3_t lm_axis[2]; vec2_t lm_offset; vec2_t lm_scale; - int lm_width; - int lm_height; + uint16_t lm_width; + uint16_t lm_height; int texnum[3]; // FIXME MAX_TMUS + int drawflags; // DSURF_PLANEBACK, etc int statebits; int firstvert; - int light_s, light_t; + uint16_t light_s, light_t; float stylecache[MAX_LIGHTMAPS]; unsigned drawframe; From fa205d1f4bb617ad915fde29c7e19fb1f7803c14 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 21 Feb 2024 19:45:06 -0500 Subject: [PATCH 052/974] More BIT()'ing, some comments and cleanup --- src/action/g_local.h | 16 ++++++++-------- src/action/g_spawn.c | 3 +-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 7078f5b16..d610a19f7 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -361,9 +361,9 @@ // variable server FPS #ifndef NO_FPS -#define HZ game.framerate +#define HZ game.framerate // (int)sv_fps->value #define FRAMETIME game.frametime -#define FRAMEDIV game.framediv +#define FRAMEDIV game.framediv // (sv_fps / 10) #define FRAMESYNC !(level.framenum % game.framediv) #else #define HZ BASE_FRAMERATE @@ -1460,12 +1460,12 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, edict_t * ignore, float radius, int mod); // damage flags -#define DAMAGE_RADIUS 0x00000001 // damage was indirect -#define DAMAGE_NO_ARMOR 0x00000002 // armour does not protect from this damage -#define DAMAGE_ENERGY 0x00000004 // damage is from an energy based weapon -#define DAMAGE_NO_KNOCKBACK 0x00000008 // do not affect velocity, just view angles -#define DAMAGE_BULLET 0x00000010 // damage is from a bullet (used for ricochets) -#define DAMAGE_NO_PROTECTION 0x00000020 // armor, shields, invulnerability, and godmode have no effect +#define DAMAGE_RADIUS BIT(0) // damage was indirect +#define DAMAGE_NO_ARMOR BIT(1) // armour does not protect from this damage +#define DAMAGE_ENERGY BIT(2) // damage is from an energy based weapon +#define DAMAGE_NO_KNOCKBACK BIT(3) // do not affect velocity, just view angles +#define DAMAGE_BULLET BIT(4) // damage is from a bullet (used for ricochets) +#define DAMAGE_NO_PROTECTION BIT(5) // armor, shields, invulnerability, and godmode have no effect #define DEFAULT_BULLET_HSPREAD 300 #define DEFAULT_BULLET_VSPREAD 500 diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index abebb734d..ae71310b9 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1765,7 +1765,6 @@ void SP_worldspawn (edict_t * ent) Q_strncpyz(teams[TEAM1].skin_index, "ctf_r", sizeof(teams[TEAM1].skin_index)); Q_strncpyz(teams[TEAM2].skin_index, "ctf_b", sizeof(teams[TEAM2].skin_index)); Q_strncpyz(teams[TEAM3].skin_index, "ctf_g", sizeof(teams[TEAM3].skin_index)); - //exit(1); } level.pic_teamskin[i] = gi.imageindex(teams[i].skin_index); } @@ -1784,7 +1783,7 @@ void SP_worldspawn (edict_t * ent) level.snd_headshot = gi.soundindex("misc/headshot.wav"); // the glorious headshot sound level.snd_vesthit = gi.soundindex("misc/vest.wav"); // kevlar hit (pew pew) level.snd_knifethrow = gi.soundindex("misc/flyloop.wav"); // throwing knife - level.snd_kick = gi.soundindex("weapons/kick.wav"); // not loaded by any item, kick sound + level.snd_kick = gi.soundindex("weapons/kick.wav"); // kick and punch sound level.snd_noammo = gi.soundindex("weapons/noammo.wav"); // click! level.snd_grenhead = gi.soundindex("weapons/grenhead.wav"); // grenade impact to head level.snd_grenhelm = gi.soundindex("weapons/grenhelm.wav"); // grenade impact to helmet From 02b5177ba459794aa24dee277677657dd919b436 Mon Sep 17 00:00:00 2001 From: mikota Date: Thu, 22 Feb 2024 05:41:59 +0100 Subject: [PATCH 053/974] variable fps interpolation fixes fixes for death cam tilt interpolation and crouching while spectating interpolation on sv_fps 20+ servers --- src/client/entities.c | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/client/entities.c b/src/client/entities.c index eaeda5fad..75a4c127b 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1303,6 +1303,11 @@ loop if rendering is disabled but sound is running. void CL_CalcViewValues(void) { player_state_t *ps, *ops; +#if USE_FPS + player_state_t *keyps, *keyops; + keyps = &cl.keyframe.ps; + keyops = &cl.oldkeyframe.ps; +#endif vec3_t viewoffset; float lerp; @@ -1346,14 +1351,16 @@ void CL_CalcViewValues(void) } else { int i; - // just use interpolated values for (i = 0; i < 3; i++) { cl.refdef.vieworg[i] = SHORT2COORD(ops->pmove.origin[i] + lerp * (ps->pmove.origin[i] - ops->pmove.origin[i])); } - - LerpVector(ops->viewoffset, ps->viewoffset, lerp, viewoffset); +#if USE_FPS + LerpVector(keyops->viewoffset, keyps->viewoffset, cl.keylerpfrac, viewoffset); +#else + LerpVector(ops->viewoffset, ps->viewoffset, lerp, viewoffset); +#endif } // if not running a demo or on a locked frame, add the local angle movement @@ -1367,10 +1374,19 @@ void CL_CalcViewValues(void) } else if (ps->pmove.pm_type < PM_DEAD) { // use predicted values VectorCopy(cl.predicted_angles, cl.refdef.viewangles); - } else if (ops->pmove.pm_type < PM_DEAD && cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { + } +#if USE_FPS + else if (keyops->pmove.pm_type < PM_DEAD && cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { +#else + else if (ops->pmove.pm_type < PM_DEAD && cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { +#endif // lerp from predicted angles, since enhanced servers // do not send viewangles each frame +#if USE_FPS + LerpAngles(cl.predicted_angles, keyps->viewangles, cl.keylerpfrac, cl.refdef.viewangles); +#else LerpAngles(cl.predicted_angles, ps->viewangles, lerp, cl.refdef.viewangles); +#endif } else { // just use interpolated values LerpAngles(ops->viewangles, ps->viewangles, lerp, cl.refdef.viewangles); @@ -1409,6 +1425,7 @@ void CL_CalcViewValues(void) VectorCopy(cl.v_up, listener_up); } + /* =============== CL_AddEntities From 8555b39a5e31968d303bf11807675032f18e3210 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 23 Feb 2024 10:54:20 -0500 Subject: [PATCH 054/974] Cleaned up some more warnings --- src/action/g_combat.c | 2 +- src/action/p_client.c | 11 ++++++----- src/action/p_view.c | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 82d184a6f..de3e5a53c 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -435,7 +435,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve int mod) { gclient_t *client; - char buf[256]; + //char buf[256]; // Unused int take, save; int asave, psave; int te_sparks, do_sparks = 0; diff --git a/src/action/p_client.c b/src/action/p_client.c index 85624b15e..369798dcf 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1341,7 +1341,7 @@ void EjectMedKit( edict_t *ent, int medkit ) void TossItemsOnDeath(edict_t * ent) { gitem_t *item; - qboolean quad = false; + //qboolean quad = false; int i; // Don't drop items if leader, this is just a mess @@ -1385,10 +1385,11 @@ void TossItemsOnDeath(edict_t * ent) } // special items - if (!DMFLAGS(DF_QUAD_DROP)) - quad = false; - else - quad = (ent->client->quad_framenum > (level.framenum + HZ)); + // Quad is unused in AQ2 + // if (!DMFLAGS(DF_QUAD_DROP)) + // quad = false; + // else + // quad = (ent->client->quad_framenum > (level.framenum + HZ)); } void TossClientWeapon(edict_t * self) diff --git a/src/action/p_view.c b/src/action/p_view.c index a44088a6f..c8f8a156a 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -750,7 +750,7 @@ P_WorldEffects */ void P_WorldEffects (void) { - qboolean breather; + //qboolean breather; // Breather is not used in AQ2 qboolean envirosuit; int waterlevel, old_waterlevel; @@ -764,7 +764,7 @@ void P_WorldEffects (void) old_waterlevel = current_client->old_waterlevel; current_client->old_waterlevel = waterlevel; - breather = current_client->breather_framenum > level.framenum; + //breather = current_client->breather_framenum > level.framenum; envirosuit = current_client->enviro_framenum > level.framenum; // From e909438f594d7f89905ae272b12ba5ab49d09d85 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 20:07:43 -0500 Subject: [PATCH 055/974] Added use_gren_bonk, esp_punishment_phase, SV_CalcViewOffset changes --- src/action/a_esp.c | 2 + src/action/a_esp.h | 3 +- src/action/a_team.c | 1 + src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_weapon.c | 28 ++++++------ src/action/p_client.c | 2 +- src/action/p_view.c | 104 ++++++++++++++++++++++++------------------ 9 files changed, 84 insertions(+), 59 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index e6c03c281..453e7b748 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -34,6 +34,7 @@ int esp_winner = NOTEAM; int esp_flag = 0; int esp_leader_pics[ TEAM_TOP ] = {0}; int esp_last_score = 0; +qboolean esp_punishment_phase = false; /* @@ -1854,6 +1855,7 @@ void MakeTeamInvulnerable(int winner, int uvtime) void EspPunishment(int teamNum) { + esp_punishment_phase = true; // Only perform team punishments if there's only 2 teams if (esp->value && teamCount == 2){ if(esp_punish->value == 1){ diff --git a/src/action/a_esp.h b/src/action/a_esp.h index d23eb462d..616ebaa7e 100644 --- a/src/action/a_esp.h +++ b/src/action/a_esp.h @@ -126,4 +126,5 @@ qboolean EspLeaderCheck(void); void EspEndOfRoundCleanup(void); void EspRespawnLCA(edict_t *ent); void EspCleanUp(void); -void EspDebug(void); \ No newline at end of file +void EspDebug(void); +extern qboolean esp_punishment_phase; \ No newline at end of file diff --git a/src/action/a_team.c b/src/action/a_team.c index ff44ebffa..85cad652a 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2361,6 +2361,7 @@ static void StartLCA(void) SpawnPlayers(); if (esp->value) { + esp_punishment_phase = false; EspResetCapturePoint(); EspAnnounceDetails(false); } diff --git a/src/action/g_local.h b/src/action/g_local.h index d610a19f7..153003dc7 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1151,6 +1151,7 @@ extern cvar_t *hc_single; // Enable or disable the single shot handcannon extern cvar_t *wp_flags; // Weapon flags (bans) extern cvar_t *itm_flags; // Item flags (bans) extern cvar_t *use_classic; // Use_classic resets weapon balance to 1.52 +extern cvar_t *use_gren_bonk; // Toggle on/off direct grenade impact damage extern cvar_t *warmup; // Enables warmup (value in seconds) extern cvar_t *warmup_bots; // Enables bots to spawn during warmup (value in number of bots) diff --git a/src/action/g_main.c b/src/action/g_main.c index b43b95091..3a92e0e1e 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -457,6 +457,7 @@ cvar_t *radio_repeat; // same as radio_max, only for repeats cvar_t *radio_repeat_time; cvar_t *use_classic; // Used to reset spread/gren strength to 1.52 +cvar_t *use_gren_bonk; cvar_t *warmup; cvar_t *warmup_bots; diff --git a/src/action/g_save.c b/src/action/g_save.c index 01e01cd74..e97aa2aea 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -528,6 +528,7 @@ void InitGame( void ) day_cycle = gi.cvar( "day_cycle", "10", 0 ); // Darkmatch cycle time. use_flashlight = gi.cvar( "use_flashlight", "0", 0 ); use_classic = gi.cvar( "use_classic", "0", 0 ); // Reset Grenade Strength to 1.52 + use_gren_bonk = gi.cvar( "use_gren_bonk", "0", 0 ); // Grenade Bonk CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index a57391da5..55b204a7b 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -742,18 +742,20 @@ static void Grenade_Touch (edict_t *ent, edict_t *other, cplane_t *plane, csurfa // From JukS mod: // New stuff: Make some damage if grenade hits to someone -JukS- // Grenade velocities by Throw type around: Long(920), Medium(720), Short(400) - float grenSpeed = VectorNormalize(ent->velocity); + if (use_gren_bonk->value) { + float grenSpeed = VectorNormalize(ent->velocity); - if (grenSpeed > 500) // Don't make damage with slow speeds - { - trace_t tr; - vec3_t end; - // Formula to handle damage amount. With /25-20 we'll get 0 dmg at minimum speed (500) - int dmgBySpeed = (int)(grenSpeed) / 25 - 20; - PRETRACE(); - tr = gi.trace(ent->owner->s.origin, NULL, NULL, end, ent->owner, MASK_SHOT); - POSTTRACE(); - T_Damage(other, ent, ent->owner, ent->s.origin, tr.endpos, tr.plane.normal, dmgBySpeed, 0, DAMAGE_NO_ARMOR, MOD_GRENADE_IMPACT); + if (grenSpeed > 500) // Don't make damage with slow speeds + { + trace_t tr; + vec3_t end; + // Formula to handle damage amount. With /25-20 we'll get 0 dmg at minimum speed (500) + int dmgBySpeed = (int)(grenSpeed) / 25 - 20; + PRETRACE(); + tr = gi.trace(ent->owner->s.origin, NULL, NULL, end, ent->owner, MASK_SHOT); + POSTTRACE(); + T_Damage(other, ent, ent->owner, ent->s.origin, tr.endpos, tr.plane.normal, dmgBySpeed, 0, DAMAGE_NO_ARMOR, MOD_GRENADE_IMPACT); + } } // zucc not needed since grenades don't blow up on contact //ent->enemy = other; @@ -855,7 +857,7 @@ void kick_attack (edict_t *ent) if (tr.ent->client) { - if (tr.ent->client->uvTime) + if (tr.ent->client->uvTime && !esp_punishment_phase) return; if (tr.ent != ent && ent->client && OnSameTeam( tr.ent, ent )) @@ -930,7 +932,7 @@ void punch_attack(edict_t * ent) if (tr.ent->client) { - if (tr.ent->client->uvTime) + if (tr.ent->client->uvTime && !esp_punishment_phase) return; if (tr.ent != ent && ent->client && OnSameTeam(tr.ent, ent)) diff --git a/src/action/p_client.c b/src/action/p_client.c index 85624b15e..16fbd5b62 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3701,7 +3701,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) // stop manipulating doors client->doortoggle = 0; - if( client->jumping && (ent->solid != SOLID_NOT) && ! lights_camera_action && ! client->uvTime && ! jump->value ) + if( client->jumping && (ent->solid != SOLID_NOT) && ! lights_camera_action && (!client->uvTime && !esp_punishment_phase) && ! jump->value ) { kick_attack( ent ); client->punch_desired = false; diff --git a/src/action/p_view.c b/src/action/p_view.c index a44088a6f..07fc692f6 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -290,61 +290,77 @@ void SV_CalcViewOffset (edict_t * ent) float delta; vec3_t v = {0, 0, 0}; - //if (!FRAMESYNC) - // return; - // base angles angles = ent->client->ps.kick_angles; + if (ent->movetype == MOVETYPE_NOCLIP) { + // don't add any kicks/bobs for spectators + VectorClear(ent->client->ps.kick_angles); + VectorSet(ent->client->ps.viewoffset, 0, 0, ent->viewheight); + return; + } + // if dead, fix the angle and don't add any kick if (ent->deadflag) { - VectorClear (angles); - + VectorClear(angles); ent->client->ps.viewangles[ROLL] = 40; ent->client->ps.viewangles[PITCH] = -15; ent->client->ps.viewangles[YAW] = ent->client->killer_yaw; } - else - { + + if (!FRAMESYNC) + return; + + if (!ent->deadflag) { // add angles based on weapon kick - VectorCopy (ent->client->kick_angles, angles); + if ((level.framenum % (int)(0.1f * HZ)) == 0) { + VectorCopy (ent->client->kick_angles, angles); - // add angles based on damage kick - ratio = (ent->client->v_dmg_time - level.time) / DAMAGE_TIME; - if (ratio < 0) - { - ratio = 0; - ent->client->v_dmg_pitch = 0; - ent->client->v_dmg_roll = 0; + // add angles based on damage kick + ratio = (ent->client->v_dmg_time - level.time) / DAMAGE_TIME; + // from OpenTDM + //ratio = (ent->client->v_dmg_time - level.framenum) / (float)DAMAGE_FRAMES; + + if (ratio < 0) + { + ratio = 0; + ent->client->v_dmg_pitch = 0; + ent->client->v_dmg_roll = 0; + } + angles[PITCH] += ratio * ent->client->v_dmg_pitch; + angles[ROLL] += ratio * ent->client->v_dmg_roll; + + // add pitch based on fall kick + ratio = (ent->client->fall_time - level.time) / FALL_TIME; + // from OpenTDM + //ratio = (ent->client->fall_time - level.framenum) / (float)FALL_FRAMES; + + if (ratio < 0) + ratio = 0; + angles[PITCH] += ratio * ent->client->fall_value; + + // add angles based on velocity + delta = DotProduct (ent->velocity, forward); + angles[PITCH] += delta * run_pitch->value; + + delta = DotProduct (ent->velocity, right); + angles[ROLL] += delta * run_roll->value; + + // add angles based on bob + delta = bobfracsin * bob_pitch->value * xyspeed; + if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) + delta *= 6; // crouching + angles[PITCH] += delta; + delta = bobfracsin * bob_roll->value * xyspeed; + if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) + delta *= 6; // crouching + if (bobcycle & 1) + delta = -delta; + angles[ROLL] += delta; + } else { + VectorCopy (ent->client->kick_angles, angles); } - angles[PITCH] += ratio * ent->client->v_dmg_pitch; - angles[ROLL] += ratio * ent->client->v_dmg_roll; - - // add pitch based on fall kick - ratio = (ent->client->fall_time - level.time) / FALL_TIME; - if (ratio < 0) - ratio = 0; - angles[PITCH] += ratio * ent->client->fall_value; - - // add angles based on velocity - delta = DotProduct (ent->velocity, forward); - angles[PITCH] += delta * run_pitch->value; - - delta = DotProduct (ent->velocity, right); - angles[ROLL] += delta * run_roll->value; - - // add angles based on bob - delta = bobfracsin * bob_pitch->value * xyspeed; - if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) - delta *= 6; // crouching - angles[PITCH] += delta; - delta = bobfracsin * bob_roll->value * xyspeed; - if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) - delta *= 6; // crouching - if (bobcycle & 1) - delta = -delta; - angles[ROLL] += delta; } //=================================== @@ -750,7 +766,7 @@ P_WorldEffects */ void P_WorldEffects (void) { - qboolean breather; + //qboolean breather; // AQ2 doesn't use the breather qboolean envirosuit; int waterlevel, old_waterlevel; @@ -764,7 +780,7 @@ void P_WorldEffects (void) old_waterlevel = current_client->old_waterlevel; current_client->old_waterlevel = waterlevel; - breather = current_client->breather_framenum > level.framenum; + //breather = current_client->breather_framenum > level.framenum; envirosuit = current_client->enviro_framenum > level.framenum; // From c09733e5a1f6e1e31da93545ed13e2e4eca728b0 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 20:31:26 -0500 Subject: [PATCH 056/974] Missed a CI update --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0f692469..b40dc4894 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,7 +122,7 @@ jobs: matrix: cc: [gcc, clang] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed4e7c7a1..f1603ac1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: actionquake/q2pro ref: ${{ github.ref }} From da16468c12b8f5e0b4fdfa222ecc5d189dd0a390 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 20:36:13 -0500 Subject: [PATCH 057/974] Added documentation for new features --- doc/action.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/action.md b/doc/action.md index 439bcf3d5..8f91da761 100644 --- a/doc/action.md +++ b/doc/action.md @@ -458,7 +458,8 @@ To help with problem idlers, use this feature to remove them from their teams so Gibs are a rewarding sight for successful headshots. TNG offers a variable which can turn gibs on headshots on. Also handcannon shots that do enough damage will gib someone. #### Commands -- `sv_gib [0/1]` - when on (1), it will display +- `sv_gib [0/1]` - when on (1), it will display gibs +- `sv_killgib [0/1]` - when on (1), upon using the `kill` command, the player will explode into giblets instead of just dying ### Automatic Reloading of Pistol When someone has akimbo pistols and they are both empty, and got one clip left, to press reload and it will switch back to single pistol and reload that with the clip. @@ -687,6 +688,10 @@ Inspired by AQ2:ETE, these additions are optional server vars to create a differ - `2`: 25% increased fall height before falling damage is taken - You do not need to have Espionage mode enabled to use this feature. +### Gun mechanics/enhancements +- `gun_dualmk23_enhance [0/1]` - server cvar, default 0. If enabled, this allows both the silencer and the laser sight to be used on the Dual MK23 Pistols. +- `use_gren_bonk [0/1]` - server cvar, default 0. If enabled, this enables impact damage of the grenade to cause damage on direct contact with a player. The speed of which the grenade is thrown will determine the damage dealt. Thanks to JukS for the idea and the code. + ## Contact Information Contacting the AQ2World Team is easy, join our Discord or visit our forums, or leave a Github Issue. We are always looking for feedback and suggestions. - Discord Server: [https://discord.aq2world.com](https://discord.aq2world.com) From a96355144690327cf0ae0d8217faf4d1c098bdda Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 20:54:34 -0500 Subject: [PATCH 058/974] Moved all of the Discord functionality to its own file if we want to revisit one day --- meson.build | 2 + src/client/discord.c | 1155 +++++++++++++++++++++++++++++++++++++++++ src/client/main.c | 1156 ------------------------------------------ 3 files changed, 1157 insertions(+), 1156 deletions(-) create mode 100644 src/client/discord.c diff --git a/meson.build b/meson.build index bc5fe8172..05221f846 100644 --- a/meson.build +++ b/meson.build @@ -508,6 +508,8 @@ if get_option('discord-sdk') discord_dep_path = join_paths(meson.current_source_dir(), discord_inc_path) discord_dep = cc.find_library('discord_game_sdk', dirs : discord_dep_path) client_deps += discord_dep + + action_src += ['src/client/discord.c'] endif #endif endif diff --git a/src/client/discord.c b/src/client/discord.c new file mode 100644 index 000000000..14d4097b2 --- /dev/null +++ b/src/client/discord.c @@ -0,0 +1,1155 @@ +//====================================================================== + +//===================================================================================================== +#if USE_DISCORD && USE_CURL && USE_AQTION //rekkie -- discord -- s +//===================================================================================================== + +//rekkie -- external ip -- s +#include + +cvar_t* cl_extern_ip; // External IP address + +static size_t CurlWriteCallback(char* buf, size_t size, size_t nmemb, void* up) +{ + if (strlen(buf) <= 16) // Length of an IP address + { + buf[strlen(buf) - 1] = '\0'; // Remove the newline + cl_extern_ip = Cvar_Set("cl_extern_ip", buf); + } + return size * nmemb; // Return how many bytes we read +} +static void CL_GetExternalIP(void) +{ + CURL* curl = curl_easy_init(); // Init the curl session + curl_easy_setopt(curl, CURLOPT_URL, "http://icanhazip.com"); // URL to get IP + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); // Write the data + CURLcode res = curl_easy_perform(curl); // Perform the request + if (res != CURLE_OK) // Something went wrong + { + // Failover - switching to a redundant URL + curl_easy_setopt(curl, CURLOPT_URL, "http://checkip.amazonaws.com/"); // URL to get IP + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); // Write the data + res = curl_easy_perform(curl); // Perform the request + if (res != CURLE_OK) // Something went wrong, again! + { + Com_Printf("%s [CURL] could not resolve the external IP, curl returned: %i\n", __func__, res); + } + } + + // If all went well + Com_Printf("External IP: %s\n", cl_extern_ip->string); + + curl_easy_cleanup(curl); // Cleanup +} + +// Determine if connection is loopback, private ip, or public ip -- https://en.wikipedia.org/wiki/Private_network +static qboolean CL_IsPrivateNetwork(void) +{ + //-------------------------------------------------------------- + // Loopback + if (cls.serverAddress.type == NA_LOOPBACK) // Loopback + { + return true; // 127.x + } + else if (cls.serverAddress.type >= NA_BROADCAST) // LAN, IPv4, IPv6 + { + qboolean ispv = NET_IsLanAddress(&cls.serverAddress); + if (ispv) // Private IP + { + return true; // (10.x 172.x 192.x etc) + } + else // Public IP + { + return false; + } + } + else + return false; + //-------------------------------------------------------------- +} +//rekkie -- external ip -- e + + +// Using the Discord API +// https://discord.com/developers/docs/game-sdk/sdk-starter-guide +// +// Requires the Discord Game SDK +// https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip +// +// Extract the contents of the zip to /extern/discord/ + +#include "../extern/discord/c/discord_game_sdk.h" + +#if _MSC_VER >= 1920 && !__INTEL_COMPILER +#if defined( _WIN64 ) + #pragma comment(lib, "../extern/discord/lib/x86_64/discord_game_sdk.dll.lib") + #elif defined( _WIN32 ) + #pragma comment(lib, "../extern/discord/lib/x86/discord_game_sdk.dll.lib") + #endif // End 64/32 bit check +#elif _MSC_VER < 1920 // older MSC +// Push/Pop fix needed for older versions of Visual Studio to prevent unexpected crashes due to compile configurations + #pragma pack(push, 8) + #include "../extern/discord/c/discord_game_sdk.h" + #pragma pack(pop) +#endif // _MSC_VER >= 1920 && !__INTEL_COMPILER + +#define DISCORD_APP_ID 1002762540247433297 // Discord application ID (also referred to as "Client ID" is the game's unique identifier across Discord) +#define DISCORD_APP_TEXT "AQtion" // Tooltip name +#define DISCORD_APP_IMAGE "aqtion" // Rich presence -> art asset -> asset name +#define DISCORD_UPDATE_MSEC 1000 // Time between updates, 1000 = 1 second. +#define DISCORD_ACTIVITY_UPDATE_MSEC 15000 // Time between updates, 1000 = 1 second. + +cvar_t *cl_discord; // Allow the user to disable Discord features for privacy +cvar_t *cl_discord_id; // User ID +cvar_t *cl_discord_username; // User name +cvar_t *cl_discord_discriminator; // User's unique discrim ( username#discriminator -> bob#1900 ) +cvar_t *cl_discord_avatar; // Hash of the user's avatar +cvar_t *cl_discord_accept_join_requests; // If true automatically accept join request, else let the user decide + +enum discord_message_type { + DISCORD_MSG_NULL, // Null packet + DISCORD_MSG_PING, // Ping -> pong + DISCORD_MSG_CONNECT, // Client wants to connect to the game server + DISCORD_MSG_OWNERSHIP // Transfer of ownership +}; + +struct Application { + struct IDiscordCore* core; + struct IDiscordApplicationManager* application; + struct IDiscordUserManager* user; + struct IDiscordImageManager* images; + struct IDiscordActivityManager* activities; + struct IDiscordRelationshipManager* relationships; + struct IDiscordLobbyManager* lobbies; + struct IDiscordNetworkManager* network; + struct IDiscordOverlayManager* overlay; + struct IDiscordStorageManager* storage; + struct IDiscordStoreManager* store; + struct IDiscordVoiceManager* voice; + struct IDiscordAchievementManager* achievements; +}; + +typedef struct { + struct Application app; // Discord app + struct DiscordCreateParams params; // Creation parameters + + // Events + IDiscordCoreEvents* events; // Core events + struct IDiscordUserEvents users_events; // Users + struct IDiscordActivityEvents activities_events; // Activities + struct IDiscordRelationshipEvents relationships_events; // Relationships + struct IDiscordLobbyEvents lobbies_events; // Lobbies + struct IDiscordAchievementEvents achievements_events; // Achievements + struct IDiscordNetworkEvents* network_events; // Network + struct IDiscordOverlayEvents* overlay_events; // Overlay + IDiscordStorageEvents* storage_events; // Storage + struct IDiscordStoreEvents* store_events; // Store + struct IDiscordVoiceEvents* voice_events; // Voice + struct IDiscordAchievementEvents* achievement_events; // Achievements + + // Activities + struct DiscordActivity activity; // Activities (rich presence) + + // Lobbies + struct IDiscordLobbyTransaction *transaction; // Transaction + struct DiscordLobby lobby; // Lobby + + // User + struct DiscordUser user; // User data (the user's discord id, username, etc) + + // Init + qboolean init; // Discord is initialized true/false + qboolean discord_found; // If Discord is running + + // Callback + int result; + + // Timers + int last_discord_runtime; // Last time (in msec) discord was updated + int last_activity_time; // Last time (in msec) activity was updated + + char server_hostname[64]; // Cache hostname + char mapname[MAX_QPATH]; // Cache map + byte curr_players; // How many players currently connected to the server + byte prev_players; // How many players previously connected to the server + +} discord_t; +discord_t discord; + +void CL_InitDiscord(void); +void CL_CreateDiscordLobby_f(void); +void CL_DeleteDiscordLobby(void); +void CL_RunDiscord(void); +void CL_ShutdownDiscord(void); + + +static void DiscordCallback(void* data, enum EDiscordResult result) +{ + //Com_Printf("%s %s\n", __func__, data); + + discord.result = result; + switch (discord.result) + { + case DiscordResult_Ok: + // Com_Printf("%s DiscordResult_Ok\n", __func__); + break; + case DiscordResult_ServiceUnavailable: + Com_Printf("%s DiscordResult_ServiceUnavailable\n", __func__); + break; + case DiscordResult_InvalidVersion: + Com_Printf("%s DiscordResult_InvalidVersion\n", __func__); + break; + case DiscordResult_LockFailed: + Com_Printf("%s DiscordResult_LockFailed\n", __func__); + break; + case DiscordResult_InternalError: + Com_Printf("%s DiscordResult_InternalError\n", __func__); + break; + case DiscordResult_InvalidPayload: + Com_Printf("%s DiscordResult_InvalidPayload\n", __func__); + break; + case DiscordResult_InvalidCommand: + Com_Printf("%s DiscordResult_InvalidCommand\n", __func__); + break; + case DiscordResult_InvalidPermissions: + Com_Printf("%s DiscordResult_InvalidPermissions\n", __func__); + break; + case DiscordResult_NotFetched: + Com_Printf("%s DiscordResult_NotFetched\n", __func__); + break; + case DiscordResult_NotFound: + Com_Printf("%s DiscordResult_NotFound\n", __func__); + break; + case DiscordResult_Conflict: + Com_Printf("%s DiscordResult_Conflict\n", __func__); + break; + case DiscordResult_InvalidSecret: + Com_Printf("%s DiscordResult_InvalidSecret\n", __func__); + break; + case DiscordResult_InvalidJoinSecret: + Com_Printf("%s DiscordResult_InvalidJoinSecret\n", __func__); + break; + case DiscordResult_NoEligibleActivity: + Com_Printf("%s DiscordResult_NoEligibleActivity\n", __func__); + break; + case DiscordResult_InvalidInvite: + Com_Printf("%s DiscordResult_InvalidInvite\n", __func__); + break; + case DiscordResult_NotAuthenticated: + Com_Printf("%s DiscordResult_NotAuthenticated\n", __func__); + break; + case DiscordResult_InvalidAccessToken: + Com_Printf("%s DiscordResult_InvalidAccessToken\n", __func__); + break; + case DiscordResult_ApplicationMismatch: + Com_Printf("%s DiscordResult_ApplicationMismatch\n", __func__); + break; + case DiscordResult_InvalidDataUrl: + Com_Printf("%s DiscordResult_InvalidDataUrl\n", __func__); + break; + case DiscordResult_InvalidBase64: + Com_Printf("%s DiscordResult_InvalidBase64\n", __func__); + break; + case DiscordResult_NotFiltered: + Com_Printf("%s DiscordResult_NotFiltered\n", __func__); + break; + case DiscordResult_LobbyFull: + Com_Printf("%s DiscordResult_LobbyFull\n", __func__); + break; + case DiscordResult_InvalidLobbySecret: + Com_Printf("%s DiscordResult_InvalidLobbySecret\n", __func__); + break; + case DiscordResult_InvalidFilename: + Com_Printf("%s DiscordResult_InvalidFilename\n", __func__); + break; + case DiscordResult_InvalidFileSize: + Com_Printf("%s DiscordResult_InvalidFileSize\n", __func__); + break; + case DiscordResult_InvalidEntitlement: + Com_Printf("%s DiscordResult_InvalidEntitlement\n", __func__); + break; + case DiscordResult_NotInstalled: + Com_Printf("%s DiscordResult_NotInstalled\n", __func__); + break; + case DiscordResult_NotRunning: + Com_Printf("%s DiscordResult_NotRunning\n", __func__); + break; + case DiscordResult_InsufficientBuffer: + Com_Printf("%s DiscordResult_InsufficientBuffer\n", __func__); + break; + case DiscordResult_PurchaseCanceled: + Com_Printf("%s DiscordResult_PurchaseCanceled\n", __func__); + break; + case DiscordResult_InvalidGuild: + Com_Printf("%s DiscordResult_InvalidGuild\n", __func__); + break; + case DiscordResult_InvalidEvent: + Com_Printf("%s DiscordResult_InvalidEvent\n", __func__); + break; + case DiscordResult_InvalidChannel: + Com_Printf("%s DiscordResult_InvalidChannel\n", __func__); + break; + case DiscordResult_InvalidOrigin: + Com_Printf("%s DiscordResult_InvalidOrigin\n", __func__); + break; + case DiscordResult_RateLimited: + Com_Printf("%s DiscordResult_RateLimited\n", __func__); + break; + case DiscordResult_OAuth2Error: + Com_Printf("%s DiscordResult_OAuth2Error\n", __func__); + break; + case DiscordResult_SelectChannelTimeout: + Com_Printf("%s DiscordResult_SelectChannelTimeout\n", __func__); + break; + case DiscordResult_GetGuildTimeout: + Com_Printf("%s DiscordResult_GetGuildTimeout\n", __func__); + break; + case DiscordResult_SelectVoiceForceRequired: + Com_Printf("%s DiscordResult_SelectVoiceForceRequired\n", __func__); + break; + case DiscordResult_CaptureShortcutAlreadyListening: + Com_Printf("%s DiscordResult_CaptureShortcutAlreadyListening\n", __func__); + break; + case DiscordResult_UnauthorizedForAchievement: + Com_Printf("%s DiscordResult_UnauthorizedForAchievement\n", __func__); + break; + case DiscordResult_InvalidGiftCode: + Com_Printf("%s DiscordResult_InvalidGiftCode\n", __func__); + break; + case DiscordResult_PurchaseError: + Com_Printf("%s DiscordResult_PurchaseError\n", __func__); + break; + case DiscordResult_TransactionAborted: + // Com_Printf("%s DiscordResult_TransactionAborted\n", __func__); + break; + default: + Com_Printf("%s Unknown error code %i\n", __func__, discord.result); + break; + } +} + +// Log output - error, warning, information, debug +static void DiscordLogCallback(void* hook_data, enum EDiscordLogLevel level, const char* message) +{ + if (level == DiscordLogLevel_Error) + Com_Printf("%s ERROR: %s\n", __func__, message); + else if (level == DiscordLogLevel_Warn) + Com_Printf("%s WARN: %s\n", __func__, message); + else if (level == DiscordLogLevel_Info) + Com_Printf("%s INFO: %s\n", __func__, message); + else if (level == DiscordLogLevel_Debug) + Com_Printf("%s DEBUG: %s\n", __func__, message); +} + +// Not used anywhere? +// static void OnOAuth2Token(void* data, enum EDiscordResult result, struct DiscordOAuth2Token* token) +// { +// if (result == DiscordResult_Ok) +// Com_Printf("%s access token: %s\n", __func__, token->access_token); +// else +// { +// Com_Printf("%s GetOAuth2Token failed with: ", __func__); +// DiscordCallback(NULL, result); +// } +// } + +// Filter Discord friends +static bool RelationshipPassFilter(void* data, struct DiscordRelationship* relationship) +{ + return (relationship->type == DiscordRelationshipType_Friend); // Return true if they are a Discord friend +} +// List all Discord friends +static void OnRelationshipsRefresh(void* data) +{ + struct Application* app = (struct Application*)data; + struct IDiscordRelationshipManager* module = app->relationships; + + module->filter(module, app, RelationshipPassFilter); + int32_t relation_count = 0; + DiscordCallback(NULL, module->count(module, &relation_count)); + if (discord.result == DiscordResult_Ok) + { + for (int32_t i = 0; i < relation_count; i += 1) + { + struct DiscordRelationship relationship; + DiscordCallback(NULL, module->get_at(module, i, &relationship)); + //if (discord.result == DiscordResult_Ok) + // Com_Printf("%lld %s#%s\n", (long long)relationship.user.id, relationship.user.username, relationship.user.discriminator); + } + } +} +static void OnRelationshipUpdate(void* data, struct DiscordRelationship* relationship) +{ +} + +// User event +static void OnUserUpdated(void* data) +{ + struct Application* app = (struct Application*)data; + struct DiscordUser user; + discord.app.user->get_current_user(app->user, &user); + discord.user = user; + + cl_discord_id = Cvar_Set("cl_discord_id", va("%lld", (long long)discord.user.id)); + cl_discord_username = Cvar_Set("cl_discord_username", discord.user.username); + cl_discord_discriminator = Cvar_Set("cl_discord_discriminator", discord.user.discriminator); + cl_discord_avatar = Cvar_Set("cl_discord_avatar", discord.user.avatar); + + //Com_Printf("%s User profile updated: %lld %s#%s\n", __func__, (long long)discord.user.id, discord.user.username, discord.user.discriminator); +} + +// Fires when the user receives a join or spectate invite. +// We let the user decide what they want to do, instead of forcing them to join; this function is currently here just to log the event +static void OnActivityInvite(void* data, enum EDiscordActivityActionType type, struct DiscordUser *user, struct DiscordActivity *activity) +{ + if (type == DiscordActivityActionType_Join) + { + //Com_Printf("%s DiscordActivityActionType_Join %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); + + // If we ever wanted to force the user to join, call this function + //discord.app.activities->accept_invite(discord.app.activities, user->id, "Accepted", DiscordCallback); + } + else // DiscordActivityActionType_Spectate + { + //Com_Printf("%s DiscordActivityActionType_Spectate %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); + + // If we ever wanted to force the user to join, call this function + //discord.app.activities->accept_invite(discord.app.activities, user->id, "Accepted", DiscordCallback); + } +} +static void DiscordLobbyCallBack(void* data, enum EDiscordResult result, struct DiscordLobby* lobby) +{ + DiscordCallback("DiscordLobbyCallBack", result); // Check the result + + //if (lobby != NULL) + // Com_Printf("%s %s ID[%lld] SECRET[%s]\n", __func__, (char*)data, (long long)lobby->id, lobby->secret); +} + +// This function runs when a player clicks the join button when invited through Discord +// The secret received is the concatenation of lobby_id + lobby_secret +static void OnActivityJoin(void* event_data, const char* secret) +{ + //Com_Printf("%s secret[%s]\n", __func__, secret); + + if (discord.lobby.id == 0 && strlen(secret) > 0) + { + DiscordLobbyId lobby_id; + DiscordLobbySecret lobby_secret; + char activity_secret[128]; // Size here is sizeof DiscordLobbySecret + Q_strlcpy(activity_secret, secret, sizeof(activity_secret)); + + // Copy lobby secret (used if lobby ownership is transferred to us) + Q_snprintf(discord.activity.secrets.join, sizeof(discord.activity.secrets.join), "%s", activity_secret); + + // Extract lobby_id and lobby_secret from activity_secret (lobby_id:secret) + //------------------------------------------------------------------------- + // Get lobby_id + char* token = strtok(activity_secret, ":"); + if (token != NULL) + lobby_id = atoll(token); + else + lobby_id = 0; + + // Get lobby_secret + token = strtok(NULL, ":"); + if (token != NULL) + Q_strlcpy(lobby_secret, token, sizeof(lobby_secret)); + else + strcpy(lobby_secret, ""); + //------------------------------------------------------------------------- + + if (lobby_id) + { + // Connect the user to the lobby (secret must be lobby_id:lobby_secret) + Q_strlcpy(activity_secret, secret, sizeof(activity_secret)); + discord.app.lobbies->connect_lobby_with_activity_secret(discord.app.lobbies, activity_secret, "OnActivityJoin", DiscordLobbyCallBack); + + if (discord.result == DiscordResult_Ok) // Result from DiscordLobbyCallBack() + { + //Com_Printf("%s Connected to lobby %lld\n", __func__, (long long)lobby_id); + discord.lobby.id = lobby_id; + Q_strlcpy(discord.lobby.secret, lobby_secret, sizeof(discord.lobby.secret)); + } + } + } +} +static void OnActivitySpectate(void* event_data, const char* secret) +{ + //Com_Printf("%s secret[%s]\n", __func__, secret); +} + +// Fires when a user asks to join our game. +static void OnActivityJoinRequest(void* event_data, struct DiscordUser* user) +{ + //Com_Printf("%s %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); + + // Here we can either automatically accept a join request by calling up send_request_reply() + // ... or we can do nothing and let the user decide if they want to accept or deny + if (cl_discord_accept_join_requests->value) + discord.app.activities->send_request_reply(discord.app.activities, user->id, DiscordActivityJoinRequestReply_Yes, "Accepted", DiscordCallback); + else + ; // let user decide + +} + +static void CL_DiscordParseServerStatus(serverStatus_t* status, const char* string) +{ + const char* s; + size_t infolen; + + s = Q_strchrnul(string, '\n'); // parse '\n' terminated infostring + + // Due to off-by-one error in the original version of Info_SetValueForKey, + // some servers produce infostrings up to 512 characters long. Work this + // bug around by cutting off the last character(s). + infolen = s - string; + if (infolen >= MAX_INFO_STRING) + infolen = MAX_INFO_STRING - 1; + + // copy infostring off + memcpy(status->infostring, string, infolen); + status->infostring[infolen] = 0; + + if (!Info_Validate(status->infostring)) + strcpy(status->infostring, "\\hostname\\badinfo"); + + Q_snprintf(discord.server_hostname, sizeof(discord.server_hostname), "%s", Info_ValueForKey(string, "hostname")); + + // parse player list + discord.curr_players = 0; + while (discord.curr_players < MAX_STATUS_PLAYERS) + { + COM_Parse(&s); // Score + COM_Parse(&s); // Ping + COM_Parse(&s); // Name + if (!s) + break; + discord.curr_players++; + } +} + +// Allow players to invite or join a server via Discord +// A lobby is created when a player joins a server, and deleted when they leave the game +static void CL_DiscordGameInvite(void) +{ + // Create a lobby if none exists + if (discord.lobby.id == 0) + { + CL_CreateDiscordLobby_f(); + } + + // Always update these + if (discord.lobby.id) + { + Q_snprintf(discord.activity.party.id, sizeof(discord.activity.party.id), "%lld\n", (long long)discord.lobby.id); + + // Join -- the join secret must be a concatenation of lobby id and secret ==> lobby_id:lobby_secret + Q_snprintf(discord.activity.secrets.join, sizeof(discord.activity.secrets.join), "%lld:%s", (long long)discord.lobby.id, discord.lobby.secret); // Secrets.join must be unique + //Com_Printf("%s %s\n", __func__, discord.activity.secrets.join); + + discord.activity.party.size.current_size = discord.curr_players; + discord.activity.party.size.max_size = cl.maxclients; + } +} + +static void CL_UpdateActivity(void) +{ + if (cls.serverAddress.type == NA_UNSPECIFIED) + return; + + if (cls.demo.playback) + { + if (discord.lobby.id) // If not connected, remove the lobby (if any) + CL_DeleteDiscordLobby(); + + discord.last_activity_time = 0; + discord.server_hostname[0] = '\0'; + sprintf(discord.activity.details, "Watching"); + discord.activity.state[0] = '\0'; + + // Since the user isn't in a map, just use game logo + Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", DISCORD_APP_IMAGE); + Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", DISCORD_APP_TEXT); + + // Reset the small logo because we're using the large logo instead + discord.activity.assets.small_image[0] = '\0'; + discord.activity.assets.small_text[0] = '\0'; + } + else if (cls.state == ca_active) // Client remote connection + { + // Get the hostname and player count (local and remote games) + if (discord.last_activity_time < cls.realtime) + { + // NOTE: Only update every ~15 seconds because this requests an infostring size string + // from the server that is up to 512 bytes long. + discord.last_activity_time = cls.realtime + DISCORD_ACTIVITY_UPDATE_MSEC; + + // Get hostname and player count by sending a request to the game server, + // the reply is processed by CL_DiscordParseServerStatus(). + // The result is cached to discord.server_hostname + netadr_t adr; + neterr_t ret; + + adr = cls.netchan.remote_address; + CL_AddRequest(&adr, REQ_STATUS_DISCORD); + + NET_Config(NET_CLIENT); + + ret = OOB_PRINT(NS_CLIENT, &adr, "status"); + if (ret == NET_ERROR) + Com_Printf("%s to %s\n", NET_ErrorString(), NET_AdrToString(&adr)); + + return; // Wait for server to respond + } + + // Apply map name and player count + if (strlen(cl.mapname) > 0) + { + if (cls.state == ca_active) // Player fully in game + Q_snprintf(discord.activity.state, sizeof(discord.activity.state), "Playing %s [%i/%i]", cl.mapname, discord.curr_players, cl.maxclients); + else // Connection: handshake, downloading, loading, etc. + sprintf(discord.activity.state, "Connecting..."); + } + + // If connection is loopback, ignore users 'hostname' setting, which defaults to 'noname' + if (cls.serverAddress.type == NA_LOOPBACK) + sprintf(discord.server_hostname, "Local Game"); + + // Hostname + Q_snprintf(discord.activity.details, sizeof(discord.activity.details), "%s", discord.server_hostname); + + // Add the map image (defaults to app icon if not found) + Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", cl.mapname); // Set map image + Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", cl.mapname); // Set map name + + // Add the game logo under the map image + Q_snprintf(discord.activity.assets.small_image, sizeof(discord.activity.assets.small_image), "%s", DISCORD_APP_IMAGE); + Q_snprintf(discord.activity.assets.small_text, sizeof(discord.activity.assets.small_text), "%s", DISCORD_APP_TEXT); + + CL_DiscordGameInvite(); // Creates a lobby and opens up game invites + } + else // Main menu + { + if (discord.lobby.id) // If not connected, remove the lobby (if any) + CL_DeleteDiscordLobby(); + + discord.last_activity_time = 0; + discord.server_hostname[0] = '\0'; + sprintf(discord.activity.details, "Main menu"); + discord.activity.state[0] = '\0'; + + // Since the user isn't in a map, just use game logo + Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", DISCORD_APP_IMAGE); + Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", DISCORD_APP_TEXT); + + // Reset the small logo because we're using the large logo instead + discord.activity.assets.small_image[0] = '\0'; + discord.activity.assets.small_text[0] = '\0'; + } + + discord.activity.type = DiscordActivityType_Playing; + discord.activity.application_id = DISCORD_APP_ID; + discord.activity.timestamps.start = 0; + discord.activity.timestamps.end = 0; + discord.activity.instance = true; + discord.app.activities->update_activity(discord.app.activities, &discord.activity, "update_activity", DiscordCallback); +} + +static void OnLobbyUpdate(void* event_data, int64_t lobby_id) +{ + //Com_Printf("%s lobbyID[%lld]\n", __func__, (long long)lobby_id); +} +static void OnLobbyDelete(void* event_data, int64_t lobby_id, uint32_t reason) +{ + //Com_Printf("%s lobbyID[%lld] reason[%i]\n", __func__, (long long)lobby_id, reason); +} +static void OnLobbyConnect(void* data, enum EDiscordResult result, struct DiscordLobby* lobby) +{ + if (result == DiscordResult_Ok) + { + //Com_Printf("%s lobby_id[%lld] lobby_type[%i] owner_id[%lld] secret[%s] capacity[%i] locked[%i]\n", __func__, (long long)lobby->id, lobby->type, (long long)lobby->owner_id, lobby->secret, lobby->capacity, lobby->locked); + discord.lobby.id = lobby->id; + discord.lobby.type = lobby->type; + discord.lobby.owner_id = lobby->owner_id; + Q_snprintf(discord.lobby.secret, sizeof(discord.lobby.secret), "%s", lobby->secret); + discord.lobby.capacity = lobby->capacity; + discord.lobby.locked = lobby->locked; + } + else + { + Com_Printf("%s OnLobbyConnect failed with: ", __func__); + DiscordCallback(NULL, result); + } +} +static void OnLobbyMemberConnect(void* event_data, int64_t lobby_id, int64_t user_id) +{ + char addr[32]; // IP + Port + char msg[256]; + + //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); + + // If the host is using a private network IP, ensure the invited client connects to the host's public IP + if (CL_IsPrivateNetwork()) // Private ip + { + + if (strlen(cl_extern_ip->string)) // We have a valid external IP + { + // Copy the external ip + port + Q_snprintf(addr, sizeof(addr), "%s:%s", cl_extern_ip->string, net_port->string); + } + else + return; // No external IP, or couldn't connect to http://icanhazip.com to get our IP + } + else // Public IP - Just use the server IP + Port + { + // Copy the remote server ip + port + Q_snprintf(addr, sizeof(addr), "%s", NET_AdrToString(&cls.serverAddress)); + } + + //Com_Printf("%s %s\n", __func__, addr); + + // Send new member the server connection details + // We sent the user_id to identify who the message is for + // If the game is password protected + if (strlen(info_password->string) > 0) + { + // Send msg type + intended recipient + ip:port + game password + Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|%s|%s|", DISCORD_MSG_CONNECT, (long long)user_id, addr, info_password->string); + } + else + { + // Send msg type + intended recipient + ip:port + empty pass (no password) + Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|%s| |", DISCORD_MSG_CONNECT, (long long)user_id, addr); + } + + //Com_Printf("%s [%s]\n", __func__, msg); + + // Broadcast + discord.app.lobbies->send_lobby_message(discord.app.lobbies, discord.lobby.id, (uint8_t*)msg, strlen(msg), "OnLobbyMemberConnect", DiscordCallback); +} +static void OnLobbyMemberUpdate(void* event_data, int64_t lobby_id, int64_t user_id) +{ + //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); +} +static void OnLobbyMemberDisconnect(void* event_data, int64_t lobby_id, int64_t user_id) +{ + //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); +} +static void OnLobbyMessage(void* event_data, int64_t lobby_id, int64_t user_id, uint8_t* data, uint32_t data_length) +{ + if (data_length == 0 || data_length > 1024) + return; + + int64_t intended_user_id = 0; + + char msg[1024]; + Q_strlcpy(msg, (char*)data, data_length); + + //Com_Printf("%s lobby_id[%lld] user_id[%lld] data[%s] data_length[%i]\n", __func__, (long long)lobby_id, (long long)user_id, msg, data_length); + + int msg_len = 0; + enum discord_message_type msg_type = 0; + + // Get the message type + char* token = strtok(msg, "|"); + if (token) + { + msg_type = atoi(token); + msg_len += strlen(token) + 1; + } + else + { + Com_Printf("%s received an invalid message type (malformed data)\n", __func__); + return; + } + + // Get the intended user_id + token = strtok(NULL, "|"); + if (token) + { + intended_user_id = atoll(token); + msg_len += strlen(token) + 1; + } + else + { + Com_Printf("%s received an invalid user_id (malformed data)\n", __func__); + return; + } + + // Special case - we disconnect from lobby only after we've fully transferred ownership + if (msg_type == DISCORD_MSG_OWNERSHIP && user_id == discord.user.id) + { + //Com_Printf("%s We've successfully transfered ownership of the lobby to [%lld]\n", __func__, (long long)intended_user_id); + discord.app.lobbies->disconnect_lobby(discord.app.lobbies, lobby_id, "disconnect_lobby", DiscordCallback); + } + + // Check if the msg was intended for someone else + if (discord.user.id != intended_user_id) + return; + + // We've just received ownership of the lobby + if (msg_type == DISCORD_MSG_OWNERSHIP) + { + //Com_Printf("%s You've taken ownership of the lobby\n", __func__); + + discord.lobby.id = lobby_id; + Q_snprintf(discord.activity.party.id, sizeof(discord.activity.party.id), "%lld\n", (long long)discord.lobby.id); + discord.activity.party.size.current_size = discord.curr_players; + discord.activity.party.size.max_size = cl.maxclients; + + return; + } + + // We recevied a request to connect to a quake2 server + if (msg_type == DISCORD_MSG_CONNECT && cls.serverAddress.type < NA_IP) // Not connected + { + // Extract server addr and password from the message + //-------------------------------------------------- + char addr[64]; + char pass[64]; + pass[0] = '\n'; + + // Copy the ip:port + token = strtok(NULL, "|"); + if (token) + { + Q_snprintf(addr, sizeof(addr), "%s", token); + msg_len += strlen(token) + 1; + } + else + { + Com_Printf("%s received invalid server ip:port (malformed data)\n", __func__); + return; + } + + // Copy the password + token = strtok(NULL, "|"); + if (token) + { + Q_snprintf(pass, sizeof(pass), "%s", token); + if (strcmp(pass, " ") != 0) // Has a password, and not " " empty space + { + Cvar_Set("password", pass); // Set server password + //Com_Printf("%s addr[%s] pass[%s]\n", __func__, addr, pass); + } + else + { + //Com_Printf("%s addr[%s]\n", __func__, addr); + } + } + //--------------------------------------------------------- + + //if (pass[0] != '\0') // Set the game password, if any + // Cvar_Set("password", pass); + + // Connect player to server + netadr_t address; + int protocol; + protocol = cl_protocol->integer; + if (!protocol) { + protocol = PROTOCOL_VERSION_Q2PRO; + } + if (!NET_StringToAdr(addr, &address, PORT_SERVER)) { + Com_Printf("Bad server address\n"); + return; + } + Q_strlcpy(cls.servername, addr, sizeof(cls.servername)); // copy early to avoid potential cmd_argv[1] clobbering + SV_Shutdown("Server was killed.\n", ERR_DISCONNECT); // if running a local server, kill it and reissue + NET_Config(NET_CLIENT); + CL_Disconnect(ERR_RECONNECT); + cls.serverAddress = address; + cls.serverProtocol = protocol; + cls.protocolVersion = 0; + cls.passive = false; + cls.state = ca_challenging; + cls.connect_time -= CONNECT_FAST; + cls.connect_count = 0; + Con_Popup(true); + CL_CheckForResend(); + + return; + } +} +static void OnLobbySpeaking(void* event_data, int64_t lobby_id, int64_t user_id, bool speaking) +{ + //Com_Printf("%s lobby_id[%lld] user_id[%lld] speaking[%i]\n", __func__, (long long)lobby_id, (long long)user_id, speaking); +} +static void OnLobbyNetworkMessage(void* event_data, int64_t lobby_id, int64_t user_id, uint8_t channel_id, uint8_t* data, uint32_t data_length) +{ + //Com_Printf("%s lobby_id[%lld] user_id[%lld] channel_id[%i] data[%s] data_length[%i]\n", __func__, (long long)lobby_id, (long long)user_id, channel_id, data, data_length); +} + +// Build a list of lobbies +// Note: must run a query before using lobby functions (discord.app.lobbies->func) +static void CL_QueryLobby(void) +{ + struct IDiscordLobbySearchQuery* query; + discord.app.lobbies->get_search_query(discord.app.lobbies, &query); + query->limit(query, 1); + discord.app.lobbies->search(discord.app.lobbies, query, "lobbies->search", DiscordCallback); +} + +void CL_CreateDiscordLobby_f(void) +{ + // Call CL_QueryLobby to build a list of lobbies before creating a new lobby + CL_QueryLobby(); + if (discord.result == DiscordResult_Ok) // Result from CL_QueryLobby() -> search() + { + // Search for lobbies + int32_t lobby_count; + discord.app.lobbies->lobby_count(discord.app.lobbies, &lobby_count); + if (lobby_count == 0) // If no existing lobbies + { + DiscordCallback(NULL, discord.app.lobbies->get_lobby_create_transaction(discord.app.lobbies, &discord.transaction)); + if (discord.result == DiscordResult_Ok) // If get_lobby_create_transaction was okay + { + // Setup lobby + DiscordMetadataKey key = "quake2"; + DiscordMetadataValue value = "rocks"; + discord.transaction->set_metadata(discord.transaction, key, value); // Metadata + discord.transaction->set_capacity(discord.transaction, cl.maxclients); // Capacity + discord.transaction->set_type(discord.transaction, DiscordLobbyType_Public); // Lobby type (DiscordLobbyType_Private, DiscordLobbyType_Public) + discord.transaction->set_locked(discord.transaction, false); // Locked + + // Create lobby + discord.app.lobbies->create_lobby(discord.app.lobbies, discord.transaction, "create_lobby", OnLobbyConnect); + } + } + } +} + +void CL_DeleteDiscordLobby(void) +{ + if (discord.init && discord.lobby.id) + { + // Call CL_QueryLobby to build a list of lobbies before we delete it + CL_QueryLobby(); + if (discord.result == DiscordResult_Ok) // Result from CL_QueryLobby() -> search() + { + // Search for lobbies to destroy + int32_t lobby_count = 0; + DiscordLobbyId lobby_id; + discord.app.lobbies->lobby_count(discord.app.lobbies, &lobby_count); + if (lobby_count == 1) + { + // Get lobby id + discord.app.lobbies->get_lobby_id(discord.app.lobbies, 0, &lobby_id); + + //Com_Printf("%s Ownership owner_id[%lld] user.id[%lld]\n", __func__, (long long)discord.lobby.owner_id, (long long)discord.user.id); + + // Transfer ownership of lobby + if (discord.lobby.owner_id == discord.user.id) // If we own the lobby + { + // Find a lobby member to transfer ownership to + int32_t member_count = 0; + DiscordUserId user_id = 0; + discord.app.lobbies->member_count(discord.app.lobbies, lobby_id, &member_count); + //Com_Printf("%s member_count %i\n", __func__, member_count); + for (int i = 0; i < member_count; i++) + { + discord.app.lobbies->get_member_user_id(discord.app.lobbies, lobby_id, i, &user_id); + //Com_Printf("%s found user_id %lld\n", __func__, (long long)user_id); + if (user_id && user_id != discord.user.id) + break; + else + user_id = 0; + } + + // If we found a member (other than ourself) to transfer ownership to + if (user_id) + { + //Com_Printf("%s Transferring ownership of lobby %lld from %lld to %lld\n", __func__, (long long)lobby_id, (long long)discord.user.id, (long long)user_id); + + discord.app.lobbies->get_lobby_update_transaction(discord.app.lobbies, lobby_id, &discord.transaction); + //if (discord.result == DiscordResult_Ok) // If get_lobby_update_transaction was okay + { + discord.transaction->set_owner(discord.transaction, user_id); // Transfer ownership + discord.app.lobbies->update_lobby(discord.app.lobbies, lobby_id, discord.transaction, "update_lobby", DiscordCallback); // Update lobby + + // Broadcast the change to the lobby + char msg[256]; + Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|", DISCORD_MSG_OWNERSHIP, (long long)user_id); + discord.app.lobbies->send_lobby_message(discord.app.lobbies, discord.lobby.id, (uint8_t*)msg, strlen(msg), "Transfer ownership", DiscordCallback); + } + } + else // If no one else is in the lobby, delete it + { + // Destroy lobby + discord.app.lobbies->delete_lobby(discord.app.lobbies, lobby_id, "delete_lobby", DiscordCallback); + //Com_Printf("%s lobby[%lld] destroyed\n", __func__, (long long)lobby_id); + } + } + + // Clear mem + memset(&discord.transaction, 0, sizeof(discord.transaction)); + memset(&discord.lobby, 0, sizeof(discord.lobby)); + } + } + } +} + +void CL_InitDiscord(void) +{ + Com_Printf("==== %s ====\n", __func__); + + // Creation + memset(&discord.app, 0, sizeof(discord.app)); + memset(&discord.params, 0, sizeof(discord.params)); + + // Events + memset(&discord.users_events, 0, sizeof(discord.users_events)); + memset(&discord.activities_events, 0, sizeof(discord.activities_events)); + memset(&discord.relationships_events, 0, sizeof(discord.relationships_events)); + + // Activities + Lobbies + memset(&discord.activity, 0, sizeof(discord.activity)); + memset(&discord.transaction, 0, sizeof(discord.transaction)); + memset(&discord.lobby, 0, sizeof(discord.lobby)); + + // On events + discord.relationships_events.on_refresh = OnRelationshipsRefresh; + discord.relationships_events.on_relationship_update = OnRelationshipUpdate; + discord.users_events.on_current_user_update = OnUserUpdated; + discord.activities_events.on_activity_invite = OnActivityInvite; + discord.activities_events.on_activity_join = OnActivityJoin; + discord.activities_events.on_activity_spectate = OnActivitySpectate; + discord.activities_events.on_activity_join_request = OnActivityJoinRequest; + discord.lobbies_events.on_lobby_update = OnLobbyUpdate; + discord.lobbies_events.on_lobby_delete = OnLobbyDelete; + discord.lobbies_events.on_member_connect = OnLobbyMemberConnect; + discord.lobbies_events.on_member_update = OnLobbyMemberUpdate; + discord.lobbies_events.on_member_disconnect = OnLobbyMemberDisconnect; + discord.lobbies_events.on_lobby_message = OnLobbyMessage; + discord.lobbies_events.on_speaking = OnLobbySpeaking; + discord.lobbies_events.on_network_message = OnLobbyNetworkMessage; + + // Creation + Params + DiscordCreateParamsSetDefault(&discord.params); + discord.params.client_id = DISCORD_APP_ID; + discord.params.flags = DiscordCreateFlags_NoRequireDiscord; // Does not require Discord to be running, use this on other platforms + discord.params.event_data = &discord.app; + discord.params.events = discord.events; + discord.params.user_events = &discord.users_events; + discord.params.activity_events = &discord.activities_events; + discord.params.relationship_events = &discord.relationships_events; + discord.params.lobby_events = &discord.lobbies_events; + discord.params.network_events = discord.network_events; + discord.params.overlay_events = discord.overlay_events; + discord.params.storage_events = discord.storage_events; + discord.params.store_events = discord.store_events; + discord.params.voice_events = discord.voice_events; + discord.params.achievement_events = discord.achievement_events; + + DiscordCallback(NULL, DiscordCreate(DISCORD_VERSION, &discord.params, &discord.app.core)); + + if (discord.result == DiscordResult_Ok) + { + // Managers + discord.app.application = discord.app.core->get_application_manager(discord.app.core); + discord.app.user = discord.app.core->get_user_manager(discord.app.core); + discord.app.images = discord.app.core->get_image_manager(discord.app.core); + discord.app.activities = discord.app.core->get_activity_manager(discord.app.core); + discord.app.relationships = discord.app.core->get_relationship_manager(discord.app.core); + discord.app.lobbies = discord.app.core->get_lobby_manager(discord.app.core); + discord.app.network = discord.app.core->get_network_manager(discord.app.core); + discord.app.overlay = discord.app.core->get_overlay_manager(discord.app.core); + discord.app.storage = discord.app.core->get_storage_manager(discord.app.core); + discord.app.store = discord.app.core->get_store_manager(discord.app.core); + discord.app.voice = discord.app.core->get_voice_manager(discord.app.core); + discord.app.achievements = discord.app.core->get_achievement_manager(discord.app.core); + + // Steam + discord.app.activities->register_steam(discord.app.activities, 1978800); // Aqtion Steam application ID + + // Command line + //discord.app.activities->register_command(discord.app.activities, "action.exe +set game action"); + //discord.app.activities->register_command(discord.app.activities, "steam://run/1978800/connect/127.0.0.1:27910"); + + // Logs + discord.app.core->set_log_hook(discord.app.core, DiscordLogLevel_Debug, NULL /*void* hook_data*/, DiscordLogCallback); + + // Init + discord.init = true; + discord.server_hostname[0] = '\0'; + discord.last_discord_runtime = cls.realtime + DISCORD_UPDATE_MSEC; + discord.last_activity_time = 0; + } + else // Could not connect to the discord network, or the discord app isn't running + { + // Init + discord.init = false; + discord.discord_found = false; + discord.server_hostname[0] = '\0'; + discord.last_discord_runtime = 0; + discord.last_activity_time = 0; + + Com_Printf("%s could not be initialized because Discord is not running\n", __func__); + } +} + +void CL_RunDiscord(void) // Run in main loop +{ + // Discord cvar disabled, or not running and connected to the Discord network + if (cl_discord->value != 1 || discord.discord_found == false) + { + // If Discord was initialized previously, shut it down + if (discord.init) + CL_ShutdownDiscord(); + + return; + } + + // Ensure Discord is initialized + if (discord.init == false) + { + CL_InitDiscord(); + return; // Give it time to fully init + } + + // Run discord integration + if (cl_discord->value) + { + // Run discord callbacks. Must be run first, as per https://discord.com/developers/docs/game-sdk/networking#flush-vs-runcallbacks + discord.app.core->run_callbacks(discord.app.core); + + if (discord.last_discord_runtime < cls.realtime) + { + // Update timer + discord.last_discord_runtime = cls.realtime + DISCORD_UPDATE_MSEC; + + // Run discord callbacks. Must be run first, as per https://discord.com/developers/docs/game-sdk/networking#flush-vs-runcallbacks + //discord.app.core->run_callbacks(discord.app.core); + + // Run activity + CL_UpdateActivity(); + + // Flushes the network. Must be run last, and after all networking messages. + discord.app.network->flush(discord.app.network); + } + } + else // Shutdown discord integration + { + CL_ShutdownDiscord(); + } +} + +static void CL_ClearDiscordAcivity(void) +{ + discord.app.activities->clear_activity(discord.app.activities, "clear_activity", DiscordCallback); + memset(&discord.activity, 0, sizeof(discord.activity)); +} + +void CL_ShutdownDiscord(void) +{ + Com_Printf("==== %s ====\n", __func__); + CL_ClearDiscordAcivity(); + CL_DeleteDiscordLobby(); + + discord.app.core->run_callbacks(discord.app.core); + discord.app.core->destroy(discord.app.core); + discord.discord_found = true; + discord.init = false; +} +//===================================================================================================== +#endif //rekkie -- discord -- e +//===================================================================================================== diff --git a/src/client/main.c b/src/client/main.c index b69a62726..ce66e7906 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -201,1162 +201,6 @@ static request_t *CL_FindRequest(void) return NULL; } -//====================================================================== - -//===================================================================================================== -#if USE_DISCORD && USE_CURL && USE_AQTION //rekkie -- discord -- s -//===================================================================================================== - -//rekkie -- external ip -- s -#include - -cvar_t* cl_extern_ip; // External IP address - -static size_t CurlWriteCallback(char* buf, size_t size, size_t nmemb, void* up) -{ - if (strlen(buf) <= 16) // Length of an IP address - { - buf[strlen(buf) - 1] = '\0'; // Remove the newline - cl_extern_ip = Cvar_Set("cl_extern_ip", buf); - } - return size * nmemb; // Return how many bytes we read -} -static void CL_GetExternalIP(void) -{ - CURL* curl = curl_easy_init(); // Init the curl session - curl_easy_setopt(curl, CURLOPT_URL, "http://icanhazip.com"); // URL to get IP - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); // Write the data - CURLcode res = curl_easy_perform(curl); // Perform the request - if (res != CURLE_OK) // Something went wrong - { - // Failover - switching to a redundant URL - curl_easy_setopt(curl, CURLOPT_URL, "http://checkip.amazonaws.com/"); // URL to get IP - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlWriteCallback); // Write the data - res = curl_easy_perform(curl); // Perform the request - if (res != CURLE_OK) // Something went wrong, again! - { - Com_Printf("%s [CURL] could not resolve the external IP, curl returned: %i\n", __func__, res); - } - } - - // If all went well - Com_Printf("External IP: %s\n", cl_extern_ip->string); - - curl_easy_cleanup(curl); // Cleanup -} - -// Determine if connection is loopback, private ip, or public ip -- https://en.wikipedia.org/wiki/Private_network -static qboolean CL_IsPrivateNetwork(void) -{ - //-------------------------------------------------------------- - // Loopback - if (cls.serverAddress.type == NA_LOOPBACK) // Loopback - { - return true; // 127.x - } - else if (cls.serverAddress.type >= NA_BROADCAST) // LAN, IPv4, IPv6 - { - qboolean ispv = NET_IsLanAddress(&cls.serverAddress); - if (ispv) // Private IP - { - return true; // (10.x 172.x 192.x etc) - } - else // Public IP - { - return false; - } - } - else - return false; - //-------------------------------------------------------------- -} -//rekkie -- external ip -- e - - -// Using the Discord API -// https://discord.com/developers/docs/game-sdk/sdk-starter-guide -// -// Requires the Discord Game SDK -// https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip -// -// Extract the contents of the zip to /extern/discord/ - -#include "../extern/discord/c/discord_game_sdk.h" - -#if _MSC_VER >= 1920 && !__INTEL_COMPILER -#if defined( _WIN64 ) - #pragma comment(lib, "../extern/discord/lib/x86_64/discord_game_sdk.dll.lib") - #elif defined( _WIN32 ) - #pragma comment(lib, "../extern/discord/lib/x86/discord_game_sdk.dll.lib") - #endif // End 64/32 bit check -#elif _MSC_VER < 1920 // older MSC -// Push/Pop fix needed for older versions of Visual Studio to prevent unexpected crashes due to compile configurations - #pragma pack(push, 8) - #include "../extern/discord/c/discord_game_sdk.h" - #pragma pack(pop) -#endif // _MSC_VER >= 1920 && !__INTEL_COMPILER - -#define DISCORD_APP_ID 1002762540247433297 // Discord application ID (also referred to as "Client ID" is the game's unique identifier across Discord) -#define DISCORD_APP_TEXT "AQtion" // Tooltip name -#define DISCORD_APP_IMAGE "aqtion" // Rich presence -> art asset -> asset name -#define DISCORD_UPDATE_MSEC 1000 // Time between updates, 1000 = 1 second. -#define DISCORD_ACTIVITY_UPDATE_MSEC 15000 // Time between updates, 1000 = 1 second. - -cvar_t *cl_discord; // Allow the user to disable Discord features for privacy -cvar_t *cl_discord_id; // User ID -cvar_t *cl_discord_username; // User name -cvar_t *cl_discord_discriminator; // User's unique discrim ( username#discriminator -> bob#1900 ) -cvar_t *cl_discord_avatar; // Hash of the user's avatar -cvar_t *cl_discord_accept_join_requests; // If true automatically accept join request, else let the user decide - -enum discord_message_type { - DISCORD_MSG_NULL, // Null packet - DISCORD_MSG_PING, // Ping -> pong - DISCORD_MSG_CONNECT, // Client wants to connect to the game server - DISCORD_MSG_OWNERSHIP // Transfer of ownership -}; - -struct Application { - struct IDiscordCore* core; - struct IDiscordApplicationManager* application; - struct IDiscordUserManager* user; - struct IDiscordImageManager* images; - struct IDiscordActivityManager* activities; - struct IDiscordRelationshipManager* relationships; - struct IDiscordLobbyManager* lobbies; - struct IDiscordNetworkManager* network; - struct IDiscordOverlayManager* overlay; - struct IDiscordStorageManager* storage; - struct IDiscordStoreManager* store; - struct IDiscordVoiceManager* voice; - struct IDiscordAchievementManager* achievements; -}; - -typedef struct { - struct Application app; // Discord app - struct DiscordCreateParams params; // Creation parameters - - // Events - IDiscordCoreEvents* events; // Core events - struct IDiscordUserEvents users_events; // Users - struct IDiscordActivityEvents activities_events; // Activities - struct IDiscordRelationshipEvents relationships_events; // Relationships - struct IDiscordLobbyEvents lobbies_events; // Lobbies - struct IDiscordAchievementEvents achievements_events; // Achievements - struct IDiscordNetworkEvents* network_events; // Network - struct IDiscordOverlayEvents* overlay_events; // Overlay - IDiscordStorageEvents* storage_events; // Storage - struct IDiscordStoreEvents* store_events; // Store - struct IDiscordVoiceEvents* voice_events; // Voice - struct IDiscordAchievementEvents* achievement_events; // Achievements - - // Activities - struct DiscordActivity activity; // Activities (rich presence) - - // Lobbies - struct IDiscordLobbyTransaction *transaction; // Transaction - struct DiscordLobby lobby; // Lobby - - // User - struct DiscordUser user; // User data (the user's discord id, username, etc) - - // Init - qboolean init; // Discord is initialized true/false - qboolean discord_found; // If Discord is running - - // Callback - int result; - - // Timers - int last_discord_runtime; // Last time (in msec) discord was updated - int last_activity_time; // Last time (in msec) activity was updated - - char server_hostname[64]; // Cache hostname - char mapname[MAX_QPATH]; // Cache map - byte curr_players; // How many players currently connected to the server - byte prev_players; // How many players previously connected to the server - -} discord_t; -discord_t discord; - -void CL_InitDiscord(void); -void CL_CreateDiscordLobby_f(void); -void CL_DeleteDiscordLobby(void); -void CL_RunDiscord(void); -void CL_ShutdownDiscord(void); - - -static void DiscordCallback(void* data, enum EDiscordResult result) -{ - //Com_Printf("%s %s\n", __func__, data); - - discord.result = result; - switch (discord.result) - { - case DiscordResult_Ok: - // Com_Printf("%s DiscordResult_Ok\n", __func__); - break; - case DiscordResult_ServiceUnavailable: - Com_Printf("%s DiscordResult_ServiceUnavailable\n", __func__); - break; - case DiscordResult_InvalidVersion: - Com_Printf("%s DiscordResult_InvalidVersion\n", __func__); - break; - case DiscordResult_LockFailed: - Com_Printf("%s DiscordResult_LockFailed\n", __func__); - break; - case DiscordResult_InternalError: - Com_Printf("%s DiscordResult_InternalError\n", __func__); - break; - case DiscordResult_InvalidPayload: - Com_Printf("%s DiscordResult_InvalidPayload\n", __func__); - break; - case DiscordResult_InvalidCommand: - Com_Printf("%s DiscordResult_InvalidCommand\n", __func__); - break; - case DiscordResult_InvalidPermissions: - Com_Printf("%s DiscordResult_InvalidPermissions\n", __func__); - break; - case DiscordResult_NotFetched: - Com_Printf("%s DiscordResult_NotFetched\n", __func__); - break; - case DiscordResult_NotFound: - Com_Printf("%s DiscordResult_NotFound\n", __func__); - break; - case DiscordResult_Conflict: - Com_Printf("%s DiscordResult_Conflict\n", __func__); - break; - case DiscordResult_InvalidSecret: - Com_Printf("%s DiscordResult_InvalidSecret\n", __func__); - break; - case DiscordResult_InvalidJoinSecret: - Com_Printf("%s DiscordResult_InvalidJoinSecret\n", __func__); - break; - case DiscordResult_NoEligibleActivity: - Com_Printf("%s DiscordResult_NoEligibleActivity\n", __func__); - break; - case DiscordResult_InvalidInvite: - Com_Printf("%s DiscordResult_InvalidInvite\n", __func__); - break; - case DiscordResult_NotAuthenticated: - Com_Printf("%s DiscordResult_NotAuthenticated\n", __func__); - break; - case DiscordResult_InvalidAccessToken: - Com_Printf("%s DiscordResult_InvalidAccessToken\n", __func__); - break; - case DiscordResult_ApplicationMismatch: - Com_Printf("%s DiscordResult_ApplicationMismatch\n", __func__); - break; - case DiscordResult_InvalidDataUrl: - Com_Printf("%s DiscordResult_InvalidDataUrl\n", __func__); - break; - case DiscordResult_InvalidBase64: - Com_Printf("%s DiscordResult_InvalidBase64\n", __func__); - break; - case DiscordResult_NotFiltered: - Com_Printf("%s DiscordResult_NotFiltered\n", __func__); - break; - case DiscordResult_LobbyFull: - Com_Printf("%s DiscordResult_LobbyFull\n", __func__); - break; - case DiscordResult_InvalidLobbySecret: - Com_Printf("%s DiscordResult_InvalidLobbySecret\n", __func__); - break; - case DiscordResult_InvalidFilename: - Com_Printf("%s DiscordResult_InvalidFilename\n", __func__); - break; - case DiscordResult_InvalidFileSize: - Com_Printf("%s DiscordResult_InvalidFileSize\n", __func__); - break; - case DiscordResult_InvalidEntitlement: - Com_Printf("%s DiscordResult_InvalidEntitlement\n", __func__); - break; - case DiscordResult_NotInstalled: - Com_Printf("%s DiscordResult_NotInstalled\n", __func__); - break; - case DiscordResult_NotRunning: - Com_Printf("%s DiscordResult_NotRunning\n", __func__); - break; - case DiscordResult_InsufficientBuffer: - Com_Printf("%s DiscordResult_InsufficientBuffer\n", __func__); - break; - case DiscordResult_PurchaseCanceled: - Com_Printf("%s DiscordResult_PurchaseCanceled\n", __func__); - break; - case DiscordResult_InvalidGuild: - Com_Printf("%s DiscordResult_InvalidGuild\n", __func__); - break; - case DiscordResult_InvalidEvent: - Com_Printf("%s DiscordResult_InvalidEvent\n", __func__); - break; - case DiscordResult_InvalidChannel: - Com_Printf("%s DiscordResult_InvalidChannel\n", __func__); - break; - case DiscordResult_InvalidOrigin: - Com_Printf("%s DiscordResult_InvalidOrigin\n", __func__); - break; - case DiscordResult_RateLimited: - Com_Printf("%s DiscordResult_RateLimited\n", __func__); - break; - case DiscordResult_OAuth2Error: - Com_Printf("%s DiscordResult_OAuth2Error\n", __func__); - break; - case DiscordResult_SelectChannelTimeout: - Com_Printf("%s DiscordResult_SelectChannelTimeout\n", __func__); - break; - case DiscordResult_GetGuildTimeout: - Com_Printf("%s DiscordResult_GetGuildTimeout\n", __func__); - break; - case DiscordResult_SelectVoiceForceRequired: - Com_Printf("%s DiscordResult_SelectVoiceForceRequired\n", __func__); - break; - case DiscordResult_CaptureShortcutAlreadyListening: - Com_Printf("%s DiscordResult_CaptureShortcutAlreadyListening\n", __func__); - break; - case DiscordResult_UnauthorizedForAchievement: - Com_Printf("%s DiscordResult_UnauthorizedForAchievement\n", __func__); - break; - case DiscordResult_InvalidGiftCode: - Com_Printf("%s DiscordResult_InvalidGiftCode\n", __func__); - break; - case DiscordResult_PurchaseError: - Com_Printf("%s DiscordResult_PurchaseError\n", __func__); - break; - case DiscordResult_TransactionAborted: - // Com_Printf("%s DiscordResult_TransactionAborted\n", __func__); - break; - default: - Com_Printf("%s Unknown error code %i\n", __func__, discord.result); - break; - } -} - -// Log output - error, warning, information, debug -static void DiscordLogCallback(void* hook_data, enum EDiscordLogLevel level, const char* message) -{ - if (level == DiscordLogLevel_Error) - Com_Printf("%s ERROR: %s\n", __func__, message); - else if (level == DiscordLogLevel_Warn) - Com_Printf("%s WARN: %s\n", __func__, message); - else if (level == DiscordLogLevel_Info) - Com_Printf("%s INFO: %s\n", __func__, message); - else if (level == DiscordLogLevel_Debug) - Com_Printf("%s DEBUG: %s\n", __func__, message); -} - -// Not used anywhere? -// static void OnOAuth2Token(void* data, enum EDiscordResult result, struct DiscordOAuth2Token* token) -// { -// if (result == DiscordResult_Ok) -// Com_Printf("%s access token: %s\n", __func__, token->access_token); -// else -// { -// Com_Printf("%s GetOAuth2Token failed with: ", __func__); -// DiscordCallback(NULL, result); -// } -// } - -// Filter Discord friends -static bool RelationshipPassFilter(void* data, struct DiscordRelationship* relationship) -{ - return (relationship->type == DiscordRelationshipType_Friend); // Return true if they are a Discord friend -} -// List all Discord friends -static void OnRelationshipsRefresh(void* data) -{ - struct Application* app = (struct Application*)data; - struct IDiscordRelationshipManager* module = app->relationships; - - module->filter(module, app, RelationshipPassFilter); - int32_t relation_count = 0; - DiscordCallback(NULL, module->count(module, &relation_count)); - if (discord.result == DiscordResult_Ok) - { - for (int32_t i = 0; i < relation_count; i += 1) - { - struct DiscordRelationship relationship; - DiscordCallback(NULL, module->get_at(module, i, &relationship)); - //if (discord.result == DiscordResult_Ok) - // Com_Printf("%lld %s#%s\n", (long long)relationship.user.id, relationship.user.username, relationship.user.discriminator); - } - } -} -static void OnRelationshipUpdate(void* data, struct DiscordRelationship* relationship) -{ -} - -// User event -static void OnUserUpdated(void* data) -{ - struct Application* app = (struct Application*)data; - struct DiscordUser user; - discord.app.user->get_current_user(app->user, &user); - discord.user = user; - - cl_discord_id = Cvar_Set("cl_discord_id", va("%lld", (long long)discord.user.id)); - cl_discord_username = Cvar_Set("cl_discord_username", discord.user.username); - cl_discord_discriminator = Cvar_Set("cl_discord_discriminator", discord.user.discriminator); - cl_discord_avatar = Cvar_Set("cl_discord_avatar", discord.user.avatar); - - //Com_Printf("%s User profile updated: %lld %s#%s\n", __func__, (long long)discord.user.id, discord.user.username, discord.user.discriminator); -} - -// Fires when the user receives a join or spectate invite. -// We let the user decide what they want to do, instead of forcing them to join; this function is currently here just to log the event -static void OnActivityInvite(void* data, enum EDiscordActivityActionType type, struct DiscordUser *user, struct DiscordActivity *activity) -{ - if (type == DiscordActivityActionType_Join) - { - //Com_Printf("%s DiscordActivityActionType_Join %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); - - // If we ever wanted to force the user to join, call this function - //discord.app.activities->accept_invite(discord.app.activities, user->id, "Accepted", DiscordCallback); - } - else // DiscordActivityActionType_Spectate - { - //Com_Printf("%s DiscordActivityActionType_Spectate %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); - - // If we ever wanted to force the user to join, call this function - //discord.app.activities->accept_invite(discord.app.activities, user->id, "Accepted", DiscordCallback); - } -} -static void DiscordLobbyCallBack(void* data, enum EDiscordResult result, struct DiscordLobby* lobby) -{ - DiscordCallback("DiscordLobbyCallBack", result); // Check the result - - //if (lobby != NULL) - // Com_Printf("%s %s ID[%lld] SECRET[%s]\n", __func__, (char*)data, (long long)lobby->id, lobby->secret); -} - -// This function runs when a player clicks the join button when invited through Discord -// The secret received is the concatenation of lobby_id + lobby_secret -static void OnActivityJoin(void* event_data, const char* secret) -{ - //Com_Printf("%s secret[%s]\n", __func__, secret); - - if (discord.lobby.id == 0 && strlen(secret) > 0) - { - DiscordLobbyId lobby_id; - DiscordLobbySecret lobby_secret; - char activity_secret[128]; // Size here is sizeof DiscordLobbySecret - Q_strlcpy(activity_secret, secret, sizeof(activity_secret)); - - // Copy lobby secret (used if lobby ownership is transferred to us) - Q_snprintf(discord.activity.secrets.join, sizeof(discord.activity.secrets.join), "%s", activity_secret); - - // Extract lobby_id and lobby_secret from activity_secret (lobby_id:secret) - //------------------------------------------------------------------------- - // Get lobby_id - char* token = strtok(activity_secret, ":"); - if (token != NULL) - lobby_id = atoll(token); - else - lobby_id = 0; - - // Get lobby_secret - token = strtok(NULL, ":"); - if (token != NULL) - Q_strlcpy(lobby_secret, token, sizeof(lobby_secret)); - else - strcpy(lobby_secret, ""); - //------------------------------------------------------------------------- - - if (lobby_id) - { - // Connect the user to the lobby (secret must be lobby_id:lobby_secret) - Q_strlcpy(activity_secret, secret, sizeof(activity_secret)); - discord.app.lobbies->connect_lobby_with_activity_secret(discord.app.lobbies, activity_secret, "OnActivityJoin", DiscordLobbyCallBack); - - if (discord.result == DiscordResult_Ok) // Result from DiscordLobbyCallBack() - { - //Com_Printf("%s Connected to lobby %lld\n", __func__, (long long)lobby_id); - discord.lobby.id = lobby_id; - Q_strlcpy(discord.lobby.secret, lobby_secret, sizeof(discord.lobby.secret)); - } - } - } -} -static void OnActivitySpectate(void* event_data, const char* secret) -{ - //Com_Printf("%s secret[%s]\n", __func__, secret); -} - -// Fires when a user asks to join our game. -static void OnActivityJoinRequest(void* event_data, struct DiscordUser* user) -{ - //Com_Printf("%s %lld %s#%s\n", __func__, (long long)user->id, user->username, user->discriminator); - - // Here we can either automatically accept a join request by calling up send_request_reply() - // ... or we can do nothing and let the user decide if they want to accept or deny - if (cl_discord_accept_join_requests->value) - discord.app.activities->send_request_reply(discord.app.activities, user->id, DiscordActivityJoinRequestReply_Yes, "Accepted", DiscordCallback); - else - ; // let user decide - -} - -static void CL_DiscordParseServerStatus(serverStatus_t* status, const char* string) -{ - const char* s; - size_t infolen; - - s = Q_strchrnul(string, '\n'); // parse '\n' terminated infostring - - // Due to off-by-one error in the original version of Info_SetValueForKey, - // some servers produce infostrings up to 512 characters long. Work this - // bug around by cutting off the last character(s). - infolen = s - string; - if (infolen >= MAX_INFO_STRING) - infolen = MAX_INFO_STRING - 1; - - // copy infostring off - memcpy(status->infostring, string, infolen); - status->infostring[infolen] = 0; - - if (!Info_Validate(status->infostring)) - strcpy(status->infostring, "\\hostname\\badinfo"); - - Q_snprintf(discord.server_hostname, sizeof(discord.server_hostname), "%s", Info_ValueForKey(string, "hostname")); - - // parse player list - discord.curr_players = 0; - while (discord.curr_players < MAX_STATUS_PLAYERS) - { - COM_Parse(&s); // Score - COM_Parse(&s); // Ping - COM_Parse(&s); // Name - if (!s) - break; - discord.curr_players++; - } -} - -// Allow players to invite or join a server via Discord -// A lobby is created when a player joins a server, and deleted when they leave the game -static void CL_DiscordGameInvite(void) -{ - // Create a lobby if none exists - if (discord.lobby.id == 0) - { - CL_CreateDiscordLobby_f(); - } - - // Always update these - if (discord.lobby.id) - { - Q_snprintf(discord.activity.party.id, sizeof(discord.activity.party.id), "%lld\n", (long long)discord.lobby.id); - - // Join -- the join secret must be a concatenation of lobby id and secret ==> lobby_id:lobby_secret - Q_snprintf(discord.activity.secrets.join, sizeof(discord.activity.secrets.join), "%lld:%s", (long long)discord.lobby.id, discord.lobby.secret); // Secrets.join must be unique - //Com_Printf("%s %s\n", __func__, discord.activity.secrets.join); - - discord.activity.party.size.current_size = discord.curr_players; - discord.activity.party.size.max_size = cl.maxclients; - } -} - -static void CL_UpdateActivity(void) -{ - if (cls.serverAddress.type == NA_UNSPECIFIED) - return; - - if (cls.demo.playback) - { - if (discord.lobby.id) // If not connected, remove the lobby (if any) - CL_DeleteDiscordLobby(); - - discord.last_activity_time = 0; - discord.server_hostname[0] = '\0'; - sprintf(discord.activity.details, "Watching"); - discord.activity.state[0] = '\0'; - - // Since the user isn't in a map, just use game logo - Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", DISCORD_APP_IMAGE); - Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", DISCORD_APP_TEXT); - - // Reset the small logo because we're using the large logo instead - discord.activity.assets.small_image[0] = '\0'; - discord.activity.assets.small_text[0] = '\0'; - } - else if (cls.state == ca_active) // Client remote connection - { - // Get the hostname and player count (local and remote games) - if (discord.last_activity_time < cls.realtime) - { - // NOTE: Only update every ~15 seconds because this requests an infostring size string - // from the server that is up to 512 bytes long. - discord.last_activity_time = cls.realtime + DISCORD_ACTIVITY_UPDATE_MSEC; - - // Get hostname and player count by sending a request to the game server, - // the reply is processed by CL_DiscordParseServerStatus(). - // The result is cached to discord.server_hostname - netadr_t adr; - neterr_t ret; - - adr = cls.netchan.remote_address; - CL_AddRequest(&adr, REQ_STATUS_DISCORD); - - NET_Config(NET_CLIENT); - - ret = OOB_PRINT(NS_CLIENT, &adr, "status"); - if (ret == NET_ERROR) - Com_Printf("%s to %s\n", NET_ErrorString(), NET_AdrToString(&adr)); - - return; // Wait for server to respond - } - - // Apply map name and player count - if (strlen(cl.mapname) > 0) - { - if (cls.state == ca_active) // Player fully in game - Q_snprintf(discord.activity.state, sizeof(discord.activity.state), "Playing %s [%i/%i]", cl.mapname, discord.curr_players, cl.maxclients); - else // Connection: handshake, downloading, loading, etc. - sprintf(discord.activity.state, "Connecting..."); - } - - // If connection is loopback, ignore users 'hostname' setting, which defaults to 'noname' - if (cls.serverAddress.type == NA_LOOPBACK) - sprintf(discord.server_hostname, "Local Game"); - - // Hostname - Q_snprintf(discord.activity.details, sizeof(discord.activity.details), "%s", discord.server_hostname); - - // Add the map image (defaults to app icon if not found) - Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", cl.mapname); // Set map image - Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", cl.mapname); // Set map name - - // Add the game logo under the map image - Q_snprintf(discord.activity.assets.small_image, sizeof(discord.activity.assets.small_image), "%s", DISCORD_APP_IMAGE); - Q_snprintf(discord.activity.assets.small_text, sizeof(discord.activity.assets.small_text), "%s", DISCORD_APP_TEXT); - - CL_DiscordGameInvite(); // Creates a lobby and opens up game invites - } - else // Main menu - { - if (discord.lobby.id) // If not connected, remove the lobby (if any) - CL_DeleteDiscordLobby(); - - discord.last_activity_time = 0; - discord.server_hostname[0] = '\0'; - sprintf(discord.activity.details, "Main menu"); - discord.activity.state[0] = '\0'; - - // Since the user isn't in a map, just use game logo - Q_snprintf(discord.activity.assets.large_image, sizeof(discord.activity.assets.large_image), "%s", DISCORD_APP_IMAGE); - Q_snprintf(discord.activity.assets.large_text, sizeof(discord.activity.assets.large_text), "%s", DISCORD_APP_TEXT); - - // Reset the small logo because we're using the large logo instead - discord.activity.assets.small_image[0] = '\0'; - discord.activity.assets.small_text[0] = '\0'; - } - - discord.activity.type = DiscordActivityType_Playing; - discord.activity.application_id = DISCORD_APP_ID; - discord.activity.timestamps.start = 0; - discord.activity.timestamps.end = 0; - discord.activity.instance = true; - discord.app.activities->update_activity(discord.app.activities, &discord.activity, "update_activity", DiscordCallback); -} - -static void OnLobbyUpdate(void* event_data, int64_t lobby_id) -{ - //Com_Printf("%s lobbyID[%lld]\n", __func__, (long long)lobby_id); -} -static void OnLobbyDelete(void* event_data, int64_t lobby_id, uint32_t reason) -{ - //Com_Printf("%s lobbyID[%lld] reason[%i]\n", __func__, (long long)lobby_id, reason); -} -static void OnLobbyConnect(void* data, enum EDiscordResult result, struct DiscordLobby* lobby) -{ - if (result == DiscordResult_Ok) - { - //Com_Printf("%s lobby_id[%lld] lobby_type[%i] owner_id[%lld] secret[%s] capacity[%i] locked[%i]\n", __func__, (long long)lobby->id, lobby->type, (long long)lobby->owner_id, lobby->secret, lobby->capacity, lobby->locked); - discord.lobby.id = lobby->id; - discord.lobby.type = lobby->type; - discord.lobby.owner_id = lobby->owner_id; - Q_snprintf(discord.lobby.secret, sizeof(discord.lobby.secret), "%s", lobby->secret); - discord.lobby.capacity = lobby->capacity; - discord.lobby.locked = lobby->locked; - } - else - { - Com_Printf("%s OnLobbyConnect failed with: ", __func__); - DiscordCallback(NULL, result); - } -} -static void OnLobbyMemberConnect(void* event_data, int64_t lobby_id, int64_t user_id) -{ - char addr[32]; // IP + Port - char msg[256]; - - //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); - - // If the host is using a private network IP, ensure the invited client connects to the host's public IP - if (CL_IsPrivateNetwork()) // Private ip - { - - if (strlen(cl_extern_ip->string)) // We have a valid external IP - { - // Copy the external ip + port - Q_snprintf(addr, sizeof(addr), "%s:%s", cl_extern_ip->string, net_port->string); - } - else - return; // No external IP, or couldn't connect to http://icanhazip.com to get our IP - } - else // Public IP - Just use the server IP + Port - { - // Copy the remote server ip + port - Q_snprintf(addr, sizeof(addr), "%s", NET_AdrToString(&cls.serverAddress)); - } - - //Com_Printf("%s %s\n", __func__, addr); - - // Send new member the server connection details - // We sent the user_id to identify who the message is for - // If the game is password protected - if (strlen(info_password->string) > 0) - { - // Send msg type + intended recipient + ip:port + game password - Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|%s|%s|", DISCORD_MSG_CONNECT, (long long)user_id, addr, info_password->string); - } - else - { - // Send msg type + intended recipient + ip:port + empty pass (no password) - Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|%s| |", DISCORD_MSG_CONNECT, (long long)user_id, addr); - } - - //Com_Printf("%s [%s]\n", __func__, msg); - - // Broadcast - discord.app.lobbies->send_lobby_message(discord.app.lobbies, discord.lobby.id, (uint8_t*)msg, strlen(msg), "OnLobbyMemberConnect", DiscordCallback); -} -static void OnLobbyMemberUpdate(void* event_data, int64_t lobby_id, int64_t user_id) -{ - //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); -} -static void OnLobbyMemberDisconnect(void* event_data, int64_t lobby_id, int64_t user_id) -{ - //Com_Printf("%s lobby_id[%lld] user_id[%lld]\n", __func__, (long long)lobby_id, (long long)user_id); -} -static void OnLobbyMessage(void* event_data, int64_t lobby_id, int64_t user_id, uint8_t* data, uint32_t data_length) -{ - if (data_length == 0 || data_length > 1024) - return; - - int64_t intended_user_id = 0; - - char msg[1024]; - Q_strlcpy(msg, (char*)data, data_length); - - //Com_Printf("%s lobby_id[%lld] user_id[%lld] data[%s] data_length[%i]\n", __func__, (long long)lobby_id, (long long)user_id, msg, data_length); - - int msg_len = 0; - enum discord_message_type msg_type = 0; - - // Get the message type - char* token = strtok(msg, "|"); - if (token) - { - msg_type = atoi(token); - msg_len += strlen(token) + 1; - } - else - { - Com_Printf("%s received an invalid message type (malformed data)\n", __func__); - return; - } - - // Get the intended user_id - token = strtok(NULL, "|"); - if (token) - { - intended_user_id = atoll(token); - msg_len += strlen(token) + 1; - } - else - { - Com_Printf("%s received an invalid user_id (malformed data)\n", __func__); - return; - } - - // Special case - we disconnect from lobby only after we've fully transferred ownership - if (msg_type == DISCORD_MSG_OWNERSHIP && user_id == discord.user.id) - { - //Com_Printf("%s We've successfully transfered ownership of the lobby to [%lld]\n", __func__, (long long)intended_user_id); - discord.app.lobbies->disconnect_lobby(discord.app.lobbies, lobby_id, "disconnect_lobby", DiscordCallback); - } - - // Check if the msg was intended for someone else - if (discord.user.id != intended_user_id) - return; - - // We've just received ownership of the lobby - if (msg_type == DISCORD_MSG_OWNERSHIP) - { - //Com_Printf("%s You've taken ownership of the lobby\n", __func__); - - discord.lobby.id = lobby_id; - Q_snprintf(discord.activity.party.id, sizeof(discord.activity.party.id), "%lld\n", (long long)discord.lobby.id); - discord.activity.party.size.current_size = discord.curr_players; - discord.activity.party.size.max_size = cl.maxclients; - - return; - } - - // We recevied a request to connect to a quake2 server - if (msg_type == DISCORD_MSG_CONNECT && cls.serverAddress.type < NA_IP) // Not connected - { - // Extract server addr and password from the message - //-------------------------------------------------- - char addr[64]; - char pass[64]; - pass[0] = '\n'; - - // Copy the ip:port - token = strtok(NULL, "|"); - if (token) - { - Q_snprintf(addr, sizeof(addr), "%s", token); - msg_len += strlen(token) + 1; - } - else - { - Com_Printf("%s received invalid server ip:port (malformed data)\n", __func__); - return; - } - - // Copy the password - token = strtok(NULL, "|"); - if (token) - { - Q_snprintf(pass, sizeof(pass), "%s", token); - if (strcmp(pass, " ") != 0) // Has a password, and not " " empty space - { - Cvar_Set("password", pass); // Set server password - //Com_Printf("%s addr[%s] pass[%s]\n", __func__, addr, pass); - } - else - { - //Com_Printf("%s addr[%s]\n", __func__, addr); - } - } - //--------------------------------------------------------- - - //if (pass[0] != '\0') // Set the game password, if any - // Cvar_Set("password", pass); - - // Connect player to server - netadr_t address; - int protocol; - protocol = cl_protocol->integer; - if (!protocol) { - protocol = PROTOCOL_VERSION_Q2PRO; - } - if (!NET_StringToAdr(addr, &address, PORT_SERVER)) { - Com_Printf("Bad server address\n"); - return; - } - Q_strlcpy(cls.servername, addr, sizeof(cls.servername)); // copy early to avoid potential cmd_argv[1] clobbering - SV_Shutdown("Server was killed.\n", ERR_DISCONNECT); // if running a local server, kill it and reissue - NET_Config(NET_CLIENT); - CL_Disconnect(ERR_RECONNECT); - cls.serverAddress = address; - cls.serverProtocol = protocol; - cls.protocolVersion = 0; - cls.passive = false; - cls.state = ca_challenging; - cls.connect_time -= CONNECT_FAST; - cls.connect_count = 0; - Con_Popup(true); - CL_CheckForResend(); - - return; - } -} -static void OnLobbySpeaking(void* event_data, int64_t lobby_id, int64_t user_id, bool speaking) -{ - //Com_Printf("%s lobby_id[%lld] user_id[%lld] speaking[%i]\n", __func__, (long long)lobby_id, (long long)user_id, speaking); -} -static void OnLobbyNetworkMessage(void* event_data, int64_t lobby_id, int64_t user_id, uint8_t channel_id, uint8_t* data, uint32_t data_length) -{ - //Com_Printf("%s lobby_id[%lld] user_id[%lld] channel_id[%i] data[%s] data_length[%i]\n", __func__, (long long)lobby_id, (long long)user_id, channel_id, data, data_length); -} - -// Build a list of lobbies -// Note: must run a query before using lobby functions (discord.app.lobbies->func) -static void CL_QueryLobby(void) -{ - struct IDiscordLobbySearchQuery* query; - discord.app.lobbies->get_search_query(discord.app.lobbies, &query); - query->limit(query, 1); - discord.app.lobbies->search(discord.app.lobbies, query, "lobbies->search", DiscordCallback); -} - -void CL_CreateDiscordLobby_f(void) -{ - // Call CL_QueryLobby to build a list of lobbies before creating a new lobby - CL_QueryLobby(); - if (discord.result == DiscordResult_Ok) // Result from CL_QueryLobby() -> search() - { - // Search for lobbies - int32_t lobby_count; - discord.app.lobbies->lobby_count(discord.app.lobbies, &lobby_count); - if (lobby_count == 0) // If no existing lobbies - { - DiscordCallback(NULL, discord.app.lobbies->get_lobby_create_transaction(discord.app.lobbies, &discord.transaction)); - if (discord.result == DiscordResult_Ok) // If get_lobby_create_transaction was okay - { - // Setup lobby - DiscordMetadataKey key = "quake2"; - DiscordMetadataValue value = "rocks"; - discord.transaction->set_metadata(discord.transaction, key, value); // Metadata - discord.transaction->set_capacity(discord.transaction, cl.maxclients); // Capacity - discord.transaction->set_type(discord.transaction, DiscordLobbyType_Public); // Lobby type (DiscordLobbyType_Private, DiscordLobbyType_Public) - discord.transaction->set_locked(discord.transaction, false); // Locked - - // Create lobby - discord.app.lobbies->create_lobby(discord.app.lobbies, discord.transaction, "create_lobby", OnLobbyConnect); - } - } - } -} - -void CL_DeleteDiscordLobby(void) -{ - if (discord.init && discord.lobby.id) - { - // Call CL_QueryLobby to build a list of lobbies before we delete it - CL_QueryLobby(); - if (discord.result == DiscordResult_Ok) // Result from CL_QueryLobby() -> search() - { - // Search for lobbies to destroy - int32_t lobby_count = 0; - DiscordLobbyId lobby_id; - discord.app.lobbies->lobby_count(discord.app.lobbies, &lobby_count); - if (lobby_count == 1) - { - // Get lobby id - discord.app.lobbies->get_lobby_id(discord.app.lobbies, 0, &lobby_id); - - //Com_Printf("%s Ownership owner_id[%lld] user.id[%lld]\n", __func__, (long long)discord.lobby.owner_id, (long long)discord.user.id); - - // Transfer ownership of lobby - if (discord.lobby.owner_id == discord.user.id) // If we own the lobby - { - // Find a lobby member to transfer ownership to - int32_t member_count = 0; - DiscordUserId user_id = 0; - discord.app.lobbies->member_count(discord.app.lobbies, lobby_id, &member_count); - //Com_Printf("%s member_count %i\n", __func__, member_count); - for (int i = 0; i < member_count; i++) - { - discord.app.lobbies->get_member_user_id(discord.app.lobbies, lobby_id, i, &user_id); - //Com_Printf("%s found user_id %lld\n", __func__, (long long)user_id); - if (user_id && user_id != discord.user.id) - break; - else - user_id = 0; - } - - // If we found a member (other than ourself) to transfer ownership to - if (user_id) - { - //Com_Printf("%s Transferring ownership of lobby %lld from %lld to %lld\n", __func__, (long long)lobby_id, (long long)discord.user.id, (long long)user_id); - - discord.app.lobbies->get_lobby_update_transaction(discord.app.lobbies, lobby_id, &discord.transaction); - //if (discord.result == DiscordResult_Ok) // If get_lobby_update_transaction was okay - { - discord.transaction->set_owner(discord.transaction, user_id); // Transfer ownership - discord.app.lobbies->update_lobby(discord.app.lobbies, lobby_id, discord.transaction, "update_lobby", DiscordCallback); // Update lobby - - // Broadcast the change to the lobby - char msg[256]; - Q_snprintf(msg, sizeof(msg) - 1, "%i|%lld|", DISCORD_MSG_OWNERSHIP, (long long)user_id); - discord.app.lobbies->send_lobby_message(discord.app.lobbies, discord.lobby.id, (uint8_t*)msg, strlen(msg), "Transfer ownership", DiscordCallback); - } - } - else // If no one else is in the lobby, delete it - { - // Destroy lobby - discord.app.lobbies->delete_lobby(discord.app.lobbies, lobby_id, "delete_lobby", DiscordCallback); - //Com_Printf("%s lobby[%lld] destroyed\n", __func__, (long long)lobby_id); - } - } - - // Clear mem - memset(&discord.transaction, 0, sizeof(discord.transaction)); - memset(&discord.lobby, 0, sizeof(discord.lobby)); - } - } - } -} - -void CL_InitDiscord(void) -{ - Com_Printf("==== %s ====\n", __func__); - - // Creation - memset(&discord.app, 0, sizeof(discord.app)); - memset(&discord.params, 0, sizeof(discord.params)); - - // Events - memset(&discord.users_events, 0, sizeof(discord.users_events)); - memset(&discord.activities_events, 0, sizeof(discord.activities_events)); - memset(&discord.relationships_events, 0, sizeof(discord.relationships_events)); - - // Activities + Lobbies - memset(&discord.activity, 0, sizeof(discord.activity)); - memset(&discord.transaction, 0, sizeof(discord.transaction)); - memset(&discord.lobby, 0, sizeof(discord.lobby)); - - // On events - discord.relationships_events.on_refresh = OnRelationshipsRefresh; - discord.relationships_events.on_relationship_update = OnRelationshipUpdate; - discord.users_events.on_current_user_update = OnUserUpdated; - discord.activities_events.on_activity_invite = OnActivityInvite; - discord.activities_events.on_activity_join = OnActivityJoin; - discord.activities_events.on_activity_spectate = OnActivitySpectate; - discord.activities_events.on_activity_join_request = OnActivityJoinRequest; - discord.lobbies_events.on_lobby_update = OnLobbyUpdate; - discord.lobbies_events.on_lobby_delete = OnLobbyDelete; - discord.lobbies_events.on_member_connect = OnLobbyMemberConnect; - discord.lobbies_events.on_member_update = OnLobbyMemberUpdate; - discord.lobbies_events.on_member_disconnect = OnLobbyMemberDisconnect; - discord.lobbies_events.on_lobby_message = OnLobbyMessage; - discord.lobbies_events.on_speaking = OnLobbySpeaking; - discord.lobbies_events.on_network_message = OnLobbyNetworkMessage; - - // Creation + Params - DiscordCreateParamsSetDefault(&discord.params); - discord.params.client_id = DISCORD_APP_ID; - discord.params.flags = DiscordCreateFlags_NoRequireDiscord; // Does not require Discord to be running, use this on other platforms - discord.params.event_data = &discord.app; - discord.params.events = discord.events; - discord.params.user_events = &discord.users_events; - discord.params.activity_events = &discord.activities_events; - discord.params.relationship_events = &discord.relationships_events; - discord.params.lobby_events = &discord.lobbies_events; - discord.params.network_events = discord.network_events; - discord.params.overlay_events = discord.overlay_events; - discord.params.storage_events = discord.storage_events; - discord.params.store_events = discord.store_events; - discord.params.voice_events = discord.voice_events; - discord.params.achievement_events = discord.achievement_events; - - DiscordCallback(NULL, DiscordCreate(DISCORD_VERSION, &discord.params, &discord.app.core)); - - if (discord.result == DiscordResult_Ok) - { - // Managers - discord.app.application = discord.app.core->get_application_manager(discord.app.core); - discord.app.user = discord.app.core->get_user_manager(discord.app.core); - discord.app.images = discord.app.core->get_image_manager(discord.app.core); - discord.app.activities = discord.app.core->get_activity_manager(discord.app.core); - discord.app.relationships = discord.app.core->get_relationship_manager(discord.app.core); - discord.app.lobbies = discord.app.core->get_lobby_manager(discord.app.core); - discord.app.network = discord.app.core->get_network_manager(discord.app.core); - discord.app.overlay = discord.app.core->get_overlay_manager(discord.app.core); - discord.app.storage = discord.app.core->get_storage_manager(discord.app.core); - discord.app.store = discord.app.core->get_store_manager(discord.app.core); - discord.app.voice = discord.app.core->get_voice_manager(discord.app.core); - discord.app.achievements = discord.app.core->get_achievement_manager(discord.app.core); - - // Steam - discord.app.activities->register_steam(discord.app.activities, 1978800); // Aqtion Steam application ID - - // Command line - //discord.app.activities->register_command(discord.app.activities, "action.exe +set game action"); - //discord.app.activities->register_command(discord.app.activities, "steam://run/1978800/connect/127.0.0.1:27910"); - - // Logs - discord.app.core->set_log_hook(discord.app.core, DiscordLogLevel_Debug, NULL /*void* hook_data*/, DiscordLogCallback); - - // Init - discord.init = true; - discord.server_hostname[0] = '\0'; - discord.last_discord_runtime = cls.realtime + DISCORD_UPDATE_MSEC; - discord.last_activity_time = 0; - } - else // Could not connect to the discord network, or the discord app isn't running - { - // Init - discord.init = false; - discord.discord_found = false; - discord.server_hostname[0] = '\0'; - discord.last_discord_runtime = 0; - discord.last_activity_time = 0; - - Com_Printf("%s could not be initialized because Discord is not running\n", __func__); - } -} - -void CL_RunDiscord(void) // Run in main loop -{ - // Discord cvar disabled, or not running and connected to the Discord network - if (cl_discord->value != 1 || discord.discord_found == false) - { - // If Discord was initialized previously, shut it down - if (discord.init) - CL_ShutdownDiscord(); - - return; - } - - // Ensure Discord is initialized - if (discord.init == false) - { - CL_InitDiscord(); - return; // Give it time to fully init - } - - // Run discord integration - if (cl_discord->value) - { - // Run discord callbacks. Must be run first, as per https://discord.com/developers/docs/game-sdk/networking#flush-vs-runcallbacks - discord.app.core->run_callbacks(discord.app.core); - - if (discord.last_discord_runtime < cls.realtime) - { - // Update timer - discord.last_discord_runtime = cls.realtime + DISCORD_UPDATE_MSEC; - - // Run discord callbacks. Must be run first, as per https://discord.com/developers/docs/game-sdk/networking#flush-vs-runcallbacks - //discord.app.core->run_callbacks(discord.app.core); - - // Run activity - CL_UpdateActivity(); - - // Flushes the network. Must be run last, and after all networking messages. - discord.app.network->flush(discord.app.network); - } - } - else // Shutdown discord integration - { - CL_ShutdownDiscord(); - } -} - -static void CL_ClearDiscordAcivity(void) -{ - discord.app.activities->clear_activity(discord.app.activities, "clear_activity", DiscordCallback); - memset(&discord.activity, 0, sizeof(discord.activity)); -} - -void CL_ShutdownDiscord(void) -{ - Com_Printf("==== %s ====\n", __func__); - CL_ClearDiscordAcivity(); - CL_DeleteDiscordLobby(); - - discord.app.core->run_callbacks(discord.app.core); - discord.app.core->destroy(discord.app.core); - discord.discord_found = true; - discord.init = false; -} -//===================================================================================================== -#endif //rekkie -- discord -- e -//===================================================================================================== - static void CL_UpdateGunSetting(void) { int nogun; From e4f98300af4fc5d38797dd5da8d41f2fbfe475f8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 21:10:16 -0500 Subject: [PATCH 059/974] Set attract mode stuff to CVAR_LATCH --- src/action/g_save.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index e97aa2aea..075dcf3be 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -627,8 +627,8 @@ void InitGame( void ) // 2023 use_killcounts = gi.cvar("use_killcounts", "0", 0); am = gi.cvar("am", "0", CVAR_LATCH | CVAR_SERVERINFO); - am_newnames = gi.cvar("am_newnames", "1", 0); - am_botcount = gi.cvar("am_botcount", "6", CVAR_SERVERINFO); + am_newnames = gi.cvar("am_newnames", "1", CVAR_LATCH); + am_botcount = gi.cvar("am_botcount", "6", CVAR_LATCH | CVAR_SERVERINFO); if (am_botcount->value < 0){ gi.cvar_forceset("am_botcount", "0"); } From 803a5c3352dd4efa94bee7f421e859ca9a09f533 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 22:36:34 -0500 Subject: [PATCH 060/974] Added extended curl and libjansson (JSON) support --- .github/workflows/build.yml | 8 ++-- meson.build | 35 ++++++++++++--- meson_options.txt | 5 +++ src/action/a_xgame.c | 88 +++++++++++++++++++++++++++++++++++++ src/action/a_xgame.h | 2 + src/action/g_local.h | 40 +++++++++++++++++ src/action/g_main.c | 17 +++++++ src/action/g_save.c | 17 +++++++ src/action/p_client.c | 6 +++ src/action/tng_stats.c | 29 ++++++++++++ src/action/tng_stats.h | 1 + 11 files changed, 240 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b40dc4894..622e9145f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -209,7 +209,7 @@ jobs: matrix: cc: [gcc] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # - name: Install dependencies # run: | @@ -246,11 +246,13 @@ jobs: matrix: arch: [x86_64] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | - brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo + brew install pkg-config meson libpng sdl2 \ + openal-soft zlib curl ffmpeg jpeg-turbo \ + ossp-uuid jansson - name: Build run: | diff --git a/meson.build b/meson.build index 05221f846..5b2e5cf82 100644 --- a/meson.build +++ b/meson.build @@ -205,6 +205,7 @@ action_src = [ 'src/action/tng_flashlight.c', 'src/action/tng_ini.c', 'src/action/tng_irc.c', + 'src/action/tng_net.c', 'src/action/tng_jump.c', 'src/action/tng_stats.c', 'src/shared/shared.c' @@ -241,6 +242,8 @@ cpuremap = { 'aarch64': 'arm64', } +inc_dirs = ['inc'] + cpu = host_machine.cpu_family() if cpu in cpuremap cpu = cpuremap[cpu] @@ -364,7 +367,7 @@ endif common_deps = [zlib] client_deps = [png, curl, sdl2] -server_deps = [] +server_deps = [curl] game_deps = [zlib] jpeg = dependency('libjpeg', @@ -403,7 +406,23 @@ if not win32 allow_fallback: win32, default_options: fallback_opt, ) - game_deps += uuid + if uuid.found() + client_deps += uuid + config.set10('USE_UUID', true) + endif + + jansson = dependency('jansson', + required: get_option('jansson'), + allow_fallback: win32, + default_options: fallback_opt, + ) + + if jansson.found() + client_deps += jansson + config.set10('USE_JANSSON', true) + dll_link_args += '-L/opt/homebrew/lib' + dll_link_args += '-ljansson' + endif endif if host_machine.system() == 'darwin' @@ -414,6 +433,8 @@ if host_machine.system() == 'darwin' default_options: fallback_opt, ) client_deps += opengl + + inc_dirs += '/opt/homebrew/include' endif # require FFmpeg >= 5.1.3 @@ -493,7 +514,11 @@ if get_option('aqtion-build') # Win32 winsock compat if win32 dll_link_args += '-lws2_32' + dll_link_args += '-lcurl' + dll_link_args += '-lcrypt32' + dll_link_args += '-lwsock32' endif + dll_link_args += '-lcurl' endif if get_option('discord-sdk') @@ -533,7 +558,7 @@ endif executable('q2pro', common_src, client_src, refresh_src, dependencies: common_deps + client_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', win_subsystem: 'windows,6.0', link_args: exe_link_args, @@ -543,7 +568,7 @@ executable('q2pro', common_src, client_src, refresh_src, executable('q2proded', common_src, server_src, dependencies: common_deps + server_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', win_subsystem: 'console,6.0', link_args: exe_link_args, @@ -554,7 +579,7 @@ executable('q2proded', common_src, server_src, shared_library('game' + cpu, action_src, name_prefix: '', dependencies: game_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', link_args: dll_link_args, install: system_wide, diff --git a/meson_options.txt b/meson_options.txt index 729b33e83..066d1879e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -69,6 +69,11 @@ option('icmp-errors', value: 'auto', description: 'Handle ICMP errors on UDP sockets') +option('jansson', + type: 'feature', + value: 'auto', + description: 'AQtion jansson support') + option('libcurl', type: 'feature', value: 'auto', diff --git a/src/action/a_xgame.c b/src/action/a_xgame.c index 8c7a8b6c5..7736e0c10 100644 --- a/src/action/a_xgame.c +++ b/src/action/a_xgame.c @@ -703,3 +703,91 @@ void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg) { timedMessages[numMessages].fired = false; numMessages++; } + +int CountRealPlayers(void) +{ + int i; + int count = 0; + edict_t *ent; + + for (i = 0, ent = g_edicts + 1; i < game.maxclients; i++, ent++){ + if (!ent->inuse || !ent->client || ent->is_bot) + continue; + count++; + } + return count; +} + +qboolean is_valid_ipv4(char *ip_str) +{ + int num, dots = 0; + char *ptr; + int parts[4] = {0}; + + if (ip_str == NULL) { + gi.dprintf("%s: ip was NULL\n", __FUNCTION__); + return false; + } + + char *ip_copy = strdup(ip_str); + if (ip_copy == NULL) { + gi.dprintf("%s: failed to allocate memory for ip_copy\n", __FUNCTION__); + return false; + } + + ptr = strtok(ip_copy, "."); + + if (ptr == NULL) { + gi.dprintf("%s: ip did not contain dots: %s\n", __FUNCTION__, ip_str); + free(ip_copy); + return false; + } + + int part_count = 0; + while (ptr) { + + /* after parsing string, it must contain only digits */ + if (!isdigit(*ptr)) { + gi.dprintf("%s: Not a valid digit: %s, IP: %s\n", __FUNCTION__, ptr, ip_str); + free(ip_copy); + return false; + } + + num = atoi(ptr); + + /* check for valid IP */ + if (num >= 0 && num <= 255) { + /* parse remaining string */ + ptr = strtok(NULL, "."); + if (ptr != NULL) { + dots++; + parts[part_count] = num; + part_count++; + } + } else { + gi.dprintf("%s: Not a valid octet: %i, IP: %s\n", __FUNCTION__, num, ip_str); + free(ip_copy); + return false; + } + } + + /* valid IP string must contain 3 dots */ + if (dots != 3) { + gi.dprintf("%s: Not a valid IP: %s\n", __FUNCTION__, ip_str); + free(ip_copy); + return false; + } + + /* check for private IP ranges */ + if ((parts[0] == 10) || + (parts[0] == 172 && parts[1] >= 16 && parts[1] <= 31) || + (parts[0] == 192 && parts[1] == 168) || + (parts[0] == 127)) { + gi.dprintf("%s: Not a public IP\n", __FUNCTION__); + free(ip_copy); + return false; + } + + free(ip_copy); + return true; +} \ No newline at end of file diff --git a/src/action/a_xgame.h b/src/action/a_xgame.h index 29c1209dd..2a0c8c309 100644 --- a/src/action/a_xgame.h +++ b/src/action/a_xgame.h @@ -60,3 +60,5 @@ void ParseSayText(edict_t *ent, char *text, size_t size); void Cmd_SetFlag1_f(edict_t *self); void Cmd_SetFlag2_f(edict_t *self); void Cmd_SaveFlags_f(edict_t *self); +int CountRealPlayers(void); +qboolean is_valid_ipv4(char *ip_str); diff --git a/src/action/g_local.h b/src/action/g_local.h index 153003dc7..f0240dc0c 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -291,6 +291,7 @@ #include "tng_jump.h" #include "g_grapple.h" #include "p_antilag.h" +#include "tng_net.h" #ifndef NO_BOTS #include "acesrc/botnav.h" @@ -423,6 +424,12 @@ typedef enum } ammo_t; +//tng_net.c +typedef enum { + NOTIFY_NONE, + SERVER_WARMING_UP, + NOTIFY_MAX +} Discord_Notifications; //deadflag #define DEAD_NO 0 @@ -757,6 +764,9 @@ typedef struct qboolean ai_ent_found; int bot_count; + // API-related + int srv_announce_timeout; + //q2pro protocol extensions cs_remap_t csr; precache_t *precaches; @@ -849,6 +859,9 @@ typedef struct // Point of interest vec3_t poi_origin; vec3_t poi_angle; + + // tng_net.c + int lc_recently_sent[NOTIFY_MAX]; // Used to prevent spamming of the endpoint } level_locals_t; @@ -1265,6 +1278,16 @@ extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command // 2024 extern cvar_t *warmup_unready; +// cURL integration +extern cvar_t *sv_curl_enable; +extern cvar_t *sv_discord_announce_enable; +extern cvar_t *sv_curl_stat_api_url; +extern cvar_t *sv_curl_discord_chat_url; +extern cvar_t *sv_curl_discord_server_url; +extern cvar_t *server_ip; +extern cvar_t *server_port; +extern cvar_t *sv_last_announce_interval; +extern cvar_t *sv_last_announce_time; #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); @@ -1313,6 +1336,7 @@ extern cvar_t *stat_logs; // Enables/disables logging of stats extern cvar_t *mapvote_next_limit; // Time left that disables map voting extern cvar_t *stat_apikey; // Stats URL key extern cvar_t *stat_url; // Stats URL endpoint +extern cvar_t *server_announce_url; // Server announce URL endpoint extern cvar_t *g_spawn_items; // Enables item spawning in GS_WEAPONCHOOSE games extern cvar_t *gm; // Gamemode extern cvar_t *gmf; // Gamemodeflags @@ -1685,6 +1709,12 @@ typedef struct gunStats_s int damage; //Damage dealt } gunStats_t; +typedef struct lt_stats_s +{ + int frags; + int deaths; + int64_t damage; +} lt_stats_t; // client data that stays across multiple level loads typedef struct @@ -1864,6 +1894,9 @@ typedef struct int dom_caps; // How many times a player captured a dom point int dom_capstreak; // How many times a player captured a dom point in a row int dom_capstreakbest; // Best cap streak for domination + + // Long term stats retreived from database + lt_stats_t* lt_stats; // Long-term stats } client_respawn_t; @@ -2540,3 +2573,10 @@ extern Message *timedMessages; void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); void FireTimedMessages(void); + +//tng_net.c +void lc_shutdown_function(void); +qboolean lc_init_function(void); +void lc_once_per_gameframe(void); +void lc_discord_webhook(char* message); +void lc_start_request_function(request_t* request); \ No newline at end of file diff --git a/src/action/g_main.c b/src/action/g_main.c index 3a92e0e1e..9313db40c 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -530,6 +530,17 @@ cvar_t *sv_killgib; // Gibs on 'kill' command // 2024 cvar_t *warmup_unready; // Toggles warmup if captains unready +// cURL integration / tng_net.c +cvar_t *sv_curl_enable; +cvar_t *sv_discord_announce_enable; +cvar_t *sv_curl_stat_api_url; +cvar_t *sv_curl_discord_chat_url; +cvar_t *sv_curl_discord_server_url; +cvar_t *server_ip; +cvar_t *server_port; +cvar_t *sv_last_announce_interval; +cvar_t *sv_last_announce_time; +cvar_t *server_announce_url; #if AQTION_EXTENSION cvar_t *use_newirvision; @@ -1190,6 +1201,12 @@ void G_RunFrame (void) CycleLights (); + //Run pending curl requests + #ifdef USE_CURL + if (sv_curl_enable->value) + lc_once_per_gameframe(); + #endif + // // treat each object in turn // even the world gets a chance to think diff --git a/src/action/g_save.c b/src/action/g_save.c index 075dcf3be..767789aa0 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -644,6 +644,17 @@ void InitGame( void ) // 2024 warmup_unready = gi.cvar("warmup_unready", "0", 0); + // curl / tng_net.c + sv_curl_enable = gi.cvar("sv_curl_enable", "0", 0); + sv_discord_announce_enable = gi.cvar("sv_discord_announce_enable", "0", 0); + sv_curl_stat_api_url = gi.cvar("sv_curl_stat_api_url", "disabled", 0); + sv_curl_discord_chat_url = gi.cvar("sv_curl_discord_chat_url", "disabled", 0); + sv_curl_discord_server_url = gi.cvar("sv_curl_discord_server_url", "disabled", 0); + server_ip = gi.cvar("server_ip", "", 0); // Never include this in serverinfo! + server_port = gi.cvar("server_port", "", 0); // Never include this in serverinfo! + sv_last_announce_time = gi.cvar("sv_last_announce_time", "0", 0); + sv_last_announce_interval = gi.cvar("sv_last_announce_interval", "1800", 0); + server_announce_url = gi.cvar("server_announce_url", "disabled", 0); // new AQtion Extension cvars #if AQTION_EXTENSION @@ -671,6 +682,12 @@ void InitGame( void ) ltk_classic = gi.cvar( "ltk_classic", "1", 0); #endif + // Initialize libcurl capabilities if enabled + #ifdef USE_CURL + if (sv_curl_enable->value) + lc_init_function(); + #endif + // items InitItems(); diff --git a/src/action/p_client.c b/src/action/p_client.c index 16fbd5b62..8a098ce9c 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -2899,6 +2899,12 @@ void PutClientInServer(edict_t * ent) // force the current weapon up client->newweapon = client->weapon; ChangeWeapon(ent); + + // Tell the world! + #ifdef USE_CURL + if (sv_curl_enable->value && sv_discord_announce_enable->value) + announce_server_populating(); + #endif } /* diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 504f1b0e5..4d1373ca1 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -492,6 +492,35 @@ void Cmd_Statmode_f(edict_t* ent) #if USE_AQTION +/* +If player has a steamid, return the entity with the matching steamid +else return NULL +*/ +edict_t *find_player_by_steamid(const char* steamid) +{ + edict_t *ent; + int i; + + // Don't do anything if steamid is null/emtpy/zero + if (steamid == NULL || steamid[0] == '\0' || strcmp(steamid, "0") == 0) + return NULL; + + for (i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[1 + i]; + if( !ent->inuse || !ent->client || ent->is_bot) + continue; + if (strcmp(ent->client->pers.steamid, steamid) == 0) + { + // Found the entity with the matching steamid + gi.dprintf("I found %s!\n", ent->client->pers.netname); + return ent; + } + } + // No entity with the matching steamid was found + return NULL; +} + // Revisit one day... // #include diff --git a/src/action/tng_stats.h b/src/action/tng_stats.h index 1c6048eef..98303575d 100644 --- a/src/action/tng_stats.h +++ b/src/action/tng_stats.h @@ -6,3 +6,4 @@ void Stats_AddHit(edict_t *ent, int gun, int hitPart); void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer); void Cmd_Stats_f (edict_t *targetent, char *arg); void Cmd_Statmode_f(edict_t *ent); +edict_t *find_player_by_steamid(const char* steamid); From 4491e6e7ae89e7bfd14518b6df889e7f03a44051 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 25 Feb 2024 23:01:28 -0500 Subject: [PATCH 061/974] Fixed spacing --- src/action/g_local.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index f0240dc0c..848ead7e1 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -426,7 +426,7 @@ ammo_t; //tng_net.c typedef enum { - NOTIFY_NONE, + NOTIFY_NONE, SERVER_WARMING_UP, NOTIFY_MAX } Discord_Notifications; From d8b4571aa392c5ebdaedd46103414608202781d3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Feb 2024 20:18:47 +0300 Subject: [PATCH 062/974] =?UTF-8?q?Print=20BSP=20stats=20in=20=E2=80=98bsp?= =?UTF-8?q?list=E2=80=99=20verbose=20mode.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/bsp.c | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/common/bsp.c b/src/common/bsp.c index 87f1d9da4..3dfc89226 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -756,6 +756,11 @@ typedef struct { uint32_t memsize; } lump_info_t; +typedef struct { + int ofs; + const char *name; +} bsp_stat_t; + #define L(name, lump, mem_t, disksize1, disksize2) \ { BSP_Load##name, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } @@ -784,12 +789,64 @@ static const lump_info_t bsp_lumps[] = { #undef L +#define F(x) { q_offsetof(bsp_t, num##x), #x } + +static const bsp_stat_t bsp_stats[] = { + F(brushsides), + F(texinfo), + F(planes), + F(nodes), + F(leafs), + F(leafbrushes), + F(models), + F(brushes), + F(visibility), + F(entitychars), + F(areas), + F(areaportals), +#if USE_REF + F(faces), + F(leaffaces), + F(lightmapbytes), + F(vertices), + F(edges), + F(surfedges), +#endif +}; + +#undef F + static list_t bsp_cache; +static void BSP_PrintStats(bsp_t *bsp) +{ + for (int i = 0; i < q_countof(bsp_stats); i++) { + const bsp_stat_t *s = &bsp_stats[i]; + Com_Printf("%8d : %s\n", *(int *)((byte *)bsp + s->ofs), s->name); + } +#if USE_REF + if (bsp->lightgrid.numleafs) { + lightgrid_t *grid = &bsp->lightgrid; + Com_Printf( + "%8u : lightgrid styles\n" + "%8u : lightgrid nodes\n" + "%8u : lightgrid leafs\n" + "%8u : lightgrid samples\n", + grid->numstyles, grid->numnodes, grid->numleafs, grid->numsamples); + } + if (bsp->lm_decoupled) + Com_Printf("DECOUPLED_LM lump present\n"); +#endif + if (bsp->extended) + Com_Printf("QBSP extended format\n"); + Com_Printf("------------------\n"); +} + static void BSP_List_f(void) { bsp_t *bsp; size_t bytes; + bool verbose = Cmd_Argc() > 1; if (LIST_EMPTY(&bsp_cache)) { Com_Printf("BSP cache is empty\n"); @@ -802,6 +859,8 @@ static void BSP_List_f(void) LIST_FOR_EACH(bsp_t, bsp, &bsp_cache, entry) { Com_Printf("%8zu : %s (%d refs)\n", bsp->hunk.mapped, bsp->name, bsp->refcount); + if (verbose) + BSP_PrintStats(bsp); bytes += bsp->hunk.mapped; } Com_Printf("Total resident: %zu\n", bytes); From e1bc32d81ed8d45d5675a5348ee71b5a010ef95e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 23 Feb 2024 17:29:21 +0300 Subject: [PATCH 063/974] Mark Com_SetLastError() functions as cold. --- inc/common/common.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inc/common/common.h b/inc/common/common.h index 93740cb95..16824f686 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -83,7 +83,10 @@ void Com_EndRedirect(void); void Com_AbortFunc(void (*func)(void *), void *arg); +q_cold void Com_SetLastError(const char *msg); + +q_cold const char *Com_GetLastError(void); q_noreturn From 696f4d747c1122e6d8f5d59f82d8c6f29d2164e7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 24 Feb 2024 17:39:05 +0300 Subject: [PATCH 064/974] Reject BSP files with too many models. --- src/common/bsp.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/bsp.c b/src/common/bsp.c index 3dfc89226..cb5e2b862 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -623,6 +623,10 @@ LOAD(SubModels) DEBUG("map with no models"); return Q_ERR_INVALID_FORMAT; } + if (count > MAX_MODELS - 2) { + DEBUG("too many models"); + return Q_ERR_INVALID_FORMAT; + } bsp->nummodels = count; bsp->models = ALLOC(sizeof(*out) * count); From 5010e2895adfad28a2e533fee6b63c9d1ed6848d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Feb 2024 14:00:50 +0300 Subject: [PATCH 065/974] Make lightgrid_node_t point[] unsigned too. --- inc/common/bsp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 4826ef911..c1af4f7b2 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -204,7 +204,7 @@ typedef struct mmodel_s { #if USE_REF typedef struct { - int32_t point[3]; + uint32_t point[3]; uint32_t children[8]; } lightgrid_node_t; From 4b1ec1ff420a31816ccb07bc9a267192a4a458d2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Feb 2024 21:52:30 +0300 Subject: [PATCH 066/974] Allow PCX files larger than 640x480. --- src/refresh/images.c | 19 ++++++++++++++----- src/refresh/models.c | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 216e576c5..9872df050 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -124,7 +124,7 @@ PCX LOADING ================================================================= */ -static int _IMG_LoadPCX(byte *rawdata, size_t rawlen, byte *pixels, +static int IMG_LoadPCX_(byte *rawdata, size_t rawlen, byte *pixels, byte *palette, int *width, int *height) { byte *raw, *end; @@ -152,7 +152,7 @@ static int _IMG_LoadPCX(byte *rawdata, size_t rawlen, byte *pixels, w = (LittleShort(pcx->xmax) - LittleShort(pcx->xmin)) + 1; h = (LittleShort(pcx->ymax) - LittleShort(pcx->ymin)) + 1; - if (w < 1 || h < 1 || w > 640 || h > 480) { + if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -271,13 +271,20 @@ static int IMG_Unpack8(uint32_t *out, const uint8_t *in, int width, int height) IMG_LOAD(PCX) { - byte buffer[640 * 480]; + byte *buffer; int w, h, ret; - ret = _IMG_LoadPCX(rawdata, rawlen, buffer, NULL, &w, &h); + ret = IMG_LoadPCX_(rawdata, rawlen, NULL, NULL, &w, &h); if (ret < 0) return ret; + buffer = IMG_AllocPixels(w * h); + ret = IMG_LoadPCX_(rawdata, rawlen, buffer, NULL, NULL, NULL); + if (ret < 0) { + IMG_FreePixels(buffer); + return ret; + } + if (image->type == IT_SKIN) IMG_FloodFill(buffer, w, h); @@ -287,6 +294,8 @@ IMG_LOAD(PCX) image->upload_height = image->height = h; image->flags |= IMG_Unpack8((uint32_t *)*pic, buffer, w, h); + IMG_FreePixels(buffer); + return Q_ERR_SUCCESS; } @@ -2038,7 +2047,7 @@ void IMG_GetPalette(void) goto fail; } - ret = _IMG_LoadPCX(data, len, NULL, pal, NULL, NULL); + ret = IMG_LoadPCX_(data, len, NULL, pal, NULL, NULL); FS_FreeFile(data); diff --git a/src/refresh/models.c b/src/refresh/models.c index 4e0421832..61a6ec41b 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -247,8 +247,8 @@ static const char *MOD_ValidateMD2(const dmd2header_t *header, size_t length) ENSURE(!(header->ofs_frames % q_alignof(dmd2frame_t)), "odd frames offset"); ENSURE(!(header->framesize % q_alignof(dmd2frame_t)), "odd frame size"); - ENSURE(header->skinwidth >= 1 && header->skinwidth <= MD2_MAX_SKINWIDTH, "bad skin width"); - ENSURE(header->skinheight >= 1 && header->skinheight <= MD2_MAX_SKINHEIGHT, "bad skin height"); + ENSURE(header->skinwidth >= 1 && header->skinwidth <= MAX_TEXTURE_SIZE, "bad skin width"); + ENSURE(header->skinheight >= 1 && header->skinheight <= MAX_TEXTURE_SIZE, "bad skin height"); return NULL; } From 2afce5df37b8e1a1dc98a99cc5953354836f1af0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Feb 2024 23:18:24 +0300 Subject: [PATCH 067/974] Allow testing specific images. --- src/common/tests.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/tests.c b/src/common/tests.c index 907a387cc..83c0c91b5 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -577,8 +577,12 @@ static void Com_TestImages_f(void) void **list; int i, count, errors; unsigned start, end; + const char *filter = ".pcx;.wal;.png;.jpg;.tga"; - list = FS_ListFiles(NULL, ".pcx;.wal;.png;.jpg;.tga", FS_SEARCH_RECURSIVE, &count); + if (Cmd_Argc() > 1) + filter = Cmd_Argv(1); + + list = FS_ListFiles(NULL, filter, FS_SEARCH_RECURSIVE, &count); if (!list) { Com_Printf("No images found\n"); return; From ad430c786774460da8170afce8187da9ef7b3d20 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Feb 2024 23:41:47 +0300 Subject: [PATCH 068/974] Simplify WAL loading code. --- src/refresh/images.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 9872df050..eab0be6cf 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -311,7 +311,7 @@ WAL LOADING IMG_LOAD(WAL) { miptex_t *mt; - unsigned w, h, offset, size, endpos; + unsigned w, h, offset; if (rawlen < sizeof(miptex_t)) { return Q_ERR_FILE_TOO_SMALL; @@ -326,16 +326,13 @@ IMG_LOAD(WAL) return Q_ERR_INVALID_FORMAT; } - size = w * h; - offset = LittleLong(mt->offsets[0]); - endpos = offset + size; - if (endpos < offset || endpos > rawlen) { + if ((uint64_t)offset + w * h > rawlen) { Com_SetLastError("data out of bounds"); return Q_ERR_INVALID_FORMAT; } - *pic = IMG_AllocPixels(size * 4); + *pic = IMG_AllocPixels(w * h * 4); image->upload_width = image->width = w; image->upload_height = image->height = h; From 249d0587116fe0debc117daacd957d9ecd904b1e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 10:31:06 -0500 Subject: [PATCH 069/974] Made grenades destroy func_buttons, fixed merge issue --- src/action/g_combat.c | 9 +++++++++ src/action/p_view.c | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index de3e5a53c..37b5e43d8 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -151,6 +151,14 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) return false; } + // Allows grenades to destroy func_buttons if they have health (city radio room for example) + if ((0 == Q_stricmp("func_button", targ->classname)) && 0 == Q_stricmp("hgrenade", inflictor->classname)) { + PRETRACE (); + trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); + POSTTRACE(); + return true; + } + PRETRACE (); trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); if (trace.fraction == 1.0) { @@ -193,6 +201,7 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) if (trace.fraction == 1.0) return true; + //gi.dprintf("CanDamage: %s can't damage %s\n", inflictor->classname, targ->classname); return false; } diff --git a/src/action/p_view.c b/src/action/p_view.c index 3a3879100..07fc692f6 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -766,11 +766,7 @@ P_WorldEffects */ void P_WorldEffects (void) { -<<<<<<< HEAD - //qboolean breather; // Breather is not used in AQ2 -======= //qboolean breather; // AQ2 doesn't use the breather ->>>>>>> da16468c12b8f5e0b4fdfa222ecc5d189dd0a390 qboolean envirosuit; int waterlevel, old_waterlevel; From 23ac37f2588d21bd0e7df024b2049dd3ba9adb62 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 10:38:19 -0500 Subject: [PATCH 070/974] Re-enabled coop mode (training maps?) --- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 10 +++++----- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 153003dc7..f2598bbff 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1045,6 +1045,7 @@ extern edict_t *g_edicts; extern cvar_t *maxentities; extern cvar_t *deathmatch; +extern cvar_t *coop; extern cvar_t *dmflags; extern cvar_t *needpass; extern cvar_t *hostname; diff --git a/src/action/g_main.c b/src/action/g_main.c index 3a92e0e1e..d82d799c4 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -338,6 +338,7 @@ cvar_t *use_warnings; cvar_t *use_mapvote; cvar_t *use_scramblevote; cvar_t *deathmatch; +cvar_t *coop; cvar_t *dmflags; cvar_t *fraglimit; cvar_t *timelimit; diff --git a/src/action/g_save.c b/src/action/g_save.c index e97aa2aea..b10bec543 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -359,11 +359,11 @@ void InitGame( void ) gi.dprintf( "Turning deathmatch on.\n" ); gi.cvar_forceset( "deathmatch", "1" ); } - cv = gi.cvar( "coop", "0", CVAR_LATCH ); - if (cv->value) { - gi.dprintf( "Turning coop off.\n" ); - gi.cvar_forceset( "coop", "0" ); - } + coop = gi.cvar( "coop", "0", CVAR_LATCH ); + // if (cv->value) { + // gi.dprintf( "Turning coop off.\n" ); + // gi.cvar_forceset( "coop", "0" ); + // } dmflags = gi.cvar( "dmflags", "0", CVAR_SERVERINFO ); fraglimit = gi.cvar( "fraglimit", "0", CVAR_SERVERINFO ); From 2204d989f86809d762fc921a5535ec4cabe0b46c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 11:22:10 -0500 Subject: [PATCH 071/974] Re-added coop spawnpoint for training maps --- src/action/p_client.c | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/action/p_client.c b/src/action/p_client.c index 88d3ebe2e..29e18c203 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1891,6 +1891,40 @@ edict_t *UncommonSpawnPoint(void) return spot; } +edict_t *SelectCoopSpawnPoint(edict_t *ent) +{ + int index; + edict_t *spot = NULL; + char *target; + + index = ent->client - game.clients; + + // player 0 starts in normal player spawn point + if (!index) + return NULL; + + spot = NULL; + + // assume there are four coop spots at each spawnpoint + while (1) { + spot = G_Find(spot, FOFS(classname), "info_player_coop"); + if (!spot) + return NULL; // we didn't have enough... + + target = spot->targetname; + if (!target) + target = ""; + if (Q_stricmp(game.spawnpoint, target) == 0) { + // this is a coop spawn point for one of the clients here + index--; + if (!index) + return spot; // this is it + } + } + + return spot; +} + /* =========== SelectSpawnPoint @@ -1907,7 +1941,9 @@ void SelectSpawnPoint(edict_t * ent, vec3_t origin, vec3_t angles) //espsettings_t *es = &espsettings; //FIREBLADE - if (ctf->value) { + if (coop->value){ + spot = SelectCoopSpawnPoint(ent); + } else if (ctf->value) { spot = SelectCTFSpawnPoint(ent); } else if (esp->value) { // SelectEspSpawnPoint handles respawns as well as initial spawnpoints From 774020df9e52ee86272e3a1f7d723bd82a67e7f6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 12:20:25 -0500 Subject: [PATCH 072/974] Added training_mode cvar --- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 7 +++++++ src/action/p_client.c | 3 +-- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index f2598bbff..6e171ad55 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1266,6 +1266,7 @@ extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command // 2024 extern cvar_t *warmup_unready; +extern cvar_t *training_mode; // Sets training mode vars #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index d82d799c4..6f1bee714 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -531,6 +531,7 @@ cvar_t *sv_killgib; // Gibs on 'kill' command // 2024 cvar_t *warmup_unready; // Toggles warmup if captains unready +cvar_t *training_mode; // Sets training mode vars #if AQTION_EXTENSION cvar_t *use_newirvision; diff --git a/src/action/g_save.c b/src/action/g_save.c index b10bec543..c46bb8a48 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -644,6 +644,13 @@ void InitGame( void ) // 2024 warmup_unready = gi.cvar("warmup_unready", "0", 0); + training_mode = gi.cvar("training_mode", "0", 0); + if (training_mode->value){ + gi.cvar_forceset("item_respawnmode", "1"); + gi.cvar_forceset("items", "2"); + gi.cvar_forceset("dmweapon", "Combat Knife"); + gi.cvar_forceset("bholelimit", "30"); + } // new AQtion Extension cvars #if AQTION_EXTENSION diff --git a/src/action/p_client.c b/src/action/p_client.c index 29e18c203..5d9fae2ba 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1938,7 +1938,6 @@ chosen ones void SelectSpawnPoint(edict_t * ent, vec3_t origin, vec3_t angles) { edict_t *spot = NULL; - //espsettings_t *es = &espsettings; //FIREBLADE if (coop->value){ @@ -1946,7 +1945,7 @@ void SelectSpawnPoint(edict_t * ent, vec3_t origin, vec3_t angles) } else if (ctf->value) { spot = SelectCTFSpawnPoint(ent); } else if (esp->value) { - // SelectEspSpawnPoint handles respawns as well as initial spawnpoints + // SelectEspSpawnPoint handles on-leader respawns as well as initial spawnpoints spot = SelectEspSpawnPoint(ent); } else if (dom->value) { spot = SelectDeathmatchSpawnPoint(); From 223328bf735c2c420a58452ee101af86fa1a3609 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 12:21:50 -0500 Subject: [PATCH 073/974] LATCHing training_mode --- src/action/g_save.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index c46bb8a48..beeef94c2 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -644,7 +644,7 @@ void InitGame( void ) // 2024 warmup_unready = gi.cvar("warmup_unready", "0", 0); - training_mode = gi.cvar("training_mode", "0", 0); + training_mode = gi.cvar("training_mode", "0", CVAR_LATCH); if (training_mode->value){ gi.cvar_forceset("item_respawnmode", "1"); gi.cvar_forceset("items", "2"); From 5943d6db574c59a67f5c70e4c18c10ef0ea8f607 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 26 Feb 2024 13:11:48 -0500 Subject: [PATCH 074/974] How did I forget to add these files --- src/action/tng_net.c | 409 +++++++++++++++++++++++++++++++++++++++++++ src/action/tng_net.h | 26 +++ 2 files changed, 435 insertions(+) create mode 100644 src/action/tng_net.c create mode 100644 src/action/tng_net.h diff --git a/src/action/tng_net.c b/src/action/tng_net.c new file mode 100644 index 000000000..e5e0f4423 --- /dev/null +++ b/src/action/tng_net.c @@ -0,0 +1,409 @@ +/* + +Support for activities around cURL-based egress network connections +Special thanks to Phatman for a bulk of the core code + +*/ + +#include "g_local.h" +#include + +// You will need one of these for each of the requests ... +// ... if you allow concurrent requests to be sent at the same time +request_list_t *active_requests, *unused_requests; +request_list_t request_nodes[MAX_REQUESTS]; +CURLM *stack = NULL; +size_t current_requests = 0; + +void init_requests(void) +{ + size_t i; + for (i = 0; i < MAX_REQUESTS - 1; i++) + request_nodes[i].next = request_nodes + i + 1; + request_nodes[MAX_REQUESTS - 1].next = NULL; + unused_requests = request_nodes; + active_requests = NULL; +} + +request_t* new_request(void) +{ + request_list_t *current = unused_requests; + if (current == NULL) + return NULL; // Ran out of request slots + unused_requests = unused_requests->next; + current->next = active_requests; + active_requests = current; + return ¤t->request; +} + +qboolean recycle_request(request_t* request) +{ + request_list_t *previous = NULL, *current = active_requests; + if (current == NULL) + return false; // No active requests exist in the list + while (¤t->request != request) + { + if (!current->next) + return false; // Could not find this request in the list + previous = current; + current = current->next; + } + if (previous == NULL) + active_requests = current->next; // Was first in the list + else + previous->next = current->next; + current->next = unused_requests; + unused_requests = current; + memset(¤t->request, 0, sizeof current->request); + return true; +} + +// This is faster than counting them and checking if it is 0 +qboolean has_active_requests(void) +{ + return active_requests != NULL; +} + +size_t count_active_requests(void) +{ + request_list_t *current; + size_t count; + for (count = 0, current = active_requests; current; current = current->next, count++) + ; // Phatman: Don't delete this line + return count; +} + +/* +*/ +void lc_get_player_stats(char* message) +{ + request_t *request; + + // Don't run this if curl is disabled or the webhook URL is set to "disabled" + if (!sv_curl_enable->value || strcmp(sv_curl_stat_api_url->string, "disabled") == 0) + return; + + // Use webhook.site to test curl, it's very handy! + //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; + char* url = sv_curl_stat_api_url->string; + + // Get a new request object + request = new_request(); + if (request == NULL) { + gi.dprintf("Ran out of request slots\n"); + return; + } + + request->url = url; + lc_start_request_function(request); +} + + +void announce_server_populating(void) +{ + // Don't announce again before sv_last_announce_interval seconds have passed + if ((int)time(NULL) - (int)sv_last_announce_time->value < (int)sv_last_announce_interval->value) + return; + + // Do not announce matchmode games + if (matchmode->value) + return; + + // Minimum 10 maxclients + if (maxclients->value < 10) + return; + + // Do not send if IP is invalid or private + if (!is_valid_ipv4(server_ip->string)) { + gi.dprintf("Server IP is invalid, deactivating announcements\n"); + gi.cvar_forceset("sv_discord_announce_enable", "0"); + return; + } + + int playercount = CountRealPlayers(); + // Do not announce if player count is less than 25% of maxclients + float threshold = fmax((game.maxclients * 0.2), 3); + if (playercount < threshold) + return; + + // All the checks are out of the way, now to form the data and send it + + json_t *srv_announce = json_object(); + + json_object_set_new(srv_announce, "hostname", json_string(hostname->string)); + json_object_set_new(srv_announce, "server_ip", json_string(server_ip->string)); + json_object_set_new(srv_announce, "server_port", json_string(server_port->string)); + json_object_set_new(srv_announce, "player_count", json_integer(playercount)); + json_object_set_new(srv_announce, "maxclients", json_integer(game.maxclients)); + json_object_set_new(srv_announce, "mapname", json_string(level.mapname)); + + json_t *root = json_object(); + json_object_set_new(root, "srv_announce", srv_announce); + json_object_set_new(root, "webhook_url", json_string(sv_curl_discord_server_url->string)); + + char *message = json_dumps(root, 0); // 0 is the flags parameter, you can set it to JSON_INDENT(4) for pretty printing + + lc_server_announce("/srv_announce_filling", message); + + json_decref(root); + + // Update the cvar so we don't announce again for sv_last_announce_interval seconds + gi.cvar_forceset("sv_last_announce_time", va("%d", (int)time(NULL))); +} + +/* +Call this with a string containing the message you want to send to the webhook. +Limited to 1024 chars. +*/ +void lc_discord_webhook(char* message) +{ + request_t *request; + char json_payload[1024]; + + // Don't run this if curl is disabled or the webhook URL is set to "disabled" + if (!sv_curl_enable->value || strcmp(sv_curl_discord_chat_url->string, "disabled") == 0) + return; + + // Use webhook.site to test curl, it's very handy! + //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; + char* url = sv_curl_discord_chat_url->string; + + // Get a new request object + request = new_request(); + if (request == NULL) { + gi.dprintf("Ran out of request slots\n"); + return; + } + + // Remove newline character from the end of message + char* newline = strchr(message, '\n'); + if (newline != NULL) { + *newline = '\0'; + } + + // Format the message as a JSON payload + snprintf(json_payload, sizeof(json_payload), "{\"content\":\"```%s```\"}", message); + request->url = url; + request->payload = strdup(json_payload); + + lc_start_request_function(request); +} + +void lc_server_announce(char *path, char *message) +{ + request_t *request; + char full_url[1024]; + + // Don't run this if curl is disabled or the webhook URLs are set to "disabled" or empty, or if no server IP or port is set + if (!sv_curl_enable->value + || strcmp(server_announce_url->string, "disabled") == 0 + || strcmp(server_announce_url->string, "") == 0 + || strcmp(sv_curl_discord_server_url->string, "disabled") == 0 + || strcmp(sv_curl_discord_server_url->string, "") == 0 + || !server_ip->string + || !server_port->string + ) + return; + + // Get a new request object + request = new_request(); + if (request == NULL) { + gi.dprintf("Ran out of request slots\n"); + return; + } + + // Use webhook.site to test curl, it's very handy! + //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; + char* url = server_announce_url->string; + // Concatenate url and path + sprintf(full_url, "%s%s", url, path); + request->url = full_url; + request->payload = strdup(message); + + gi.dprintf("Sending server announce to %s\n", full_url); + gi.dprintf("With payload: %s\n", message); + + lc_start_request_function(request); +} + +void lc_shutdown_function(void) +{ + if (stack) + { + curl_multi_cleanup(stack); + stack = NULL; + } + current_requests = 0; + curl_global_cleanup(); +} + +qboolean lc_init_function(void) +{ + lc_shutdown_function(); + init_requests(); + if (curl_global_init(CURL_GLOBAL_ALL)) + return false; + stack = curl_multi_init(); + if (!stack) + return false; + if (curl_multi_setopt(stack, CURLMOPT_MAXCONNECTS, MAX_REQUESTS) != CURLM_OK) + { + curl_multi_cleanup(stack); + curl_global_cleanup(); + return false; + } + // ... + return true; +} + +// Your callback function's return type and parameter types _must_ match this: +size_t lc_receive_data_function(char *data, size_t blocks, size_t bytes, void *pvt) +{ + request_t *request; + if (bytes <= 0){ + return 0; + } + + request = (request_t*) pvt; + if (!request) + { + gi.dprintf("%s: ERROR! pvt argument was NULL.\n", __func__); + return 0; + } + if (bytes > MAX_DATA_BYTES - request->data_count) + { + gi.dprintf("%s: ERROR! Too much data.\n", __func__); + return 0; // TODO: Ensure this cancels the request + } + memcpy(request->data + request->data_count, request->data, bytes); + request->data_count += bytes; + return bytes; +} + +// Requires that request->url already be set to the target URL +void lc_start_request_function(request_t* request) +{ + struct curl_slist *headers = NULL; + + request->data_count = 0; + request->handle = curl_easy_init(); + + // Set the headers to indicate we're sending JSON data, and a custom one + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, "x-aqtion-server: true"); + curl_easy_setopt(request->handle, CURLOPT_HTTPHEADER, headers); + + // Set the JSON payload if it exists (as POST request), else this is a GET request + if (request->payload) + curl_easy_setopt(request->handle, CURLOPT_POSTFIELDS, request->payload); + + curl_easy_setopt(request->handle, CURLOPT_WRITEFUNCTION, lc_receive_data_function); + curl_easy_setopt(request->handle, CURLOPT_URL, request->url); + curl_easy_setopt(request->handle, CURLOPT_WRITEDATA, request); // Passed as pvt to lc_receive_data_function + curl_easy_setopt(request->handle, CURLOPT_PRIVATE, request); // Returned by curl_easy_getinfo with the CURLINFO_PRIVATE option + curl_multi_add_handle(stack, request->handle); + current_requests++; +} + +void process_stats(json_t *stats_json) +{ + int i; + if (!json_is_object(stats_json)) { + gi.dprintf("error: stats is not a JSON object\n"); + return; + } + + const char *stat_names[] = {"frags", "deaths", "damage"}; + int num_stats = sizeof(stat_names) / sizeof(stat_names[0]); + + lt_stats_t stats; + for (i = 0; i < num_stats; i++) { + json_t *stat_json = json_object_get(stats_json, stat_names[i]); + if (!stat_json) { + gi.dprintf("error: %s is missing\n", stat_names[i]); + return; + } + + if (!json_is_integer(stat_json)) { + gi.dprintf("error: %s is not an integer\n", stat_names[i]); + return; + } + + int stat_value = json_integer_value(stat_json); + if (strcmp(stat_names[i], "frags") == 0) { + stats.frags = stat_value; + } else if (strcmp(stat_names[i], "deaths") == 0) { + stats.deaths = stat_value; + } else if (strcmp(stat_names[i], "damage") == 0) { + stats.damage = stat_value; + } + } +} + +void lc_parse_response(char* data) +{ + // json_error_t error; + // json_t *root = json_loads(data, 0, &error); + + // if(!root) + // { + // gi.dprintf("error: on line %d: %s\n", error.line, error.text); + // return; + // } + + // json_t *stats_json = json_object_get(root, "stats"); + // if(!json_is_object(stats_json)) + // { + // // If the root is not "stats", do nothing + // json_decref(root); + // return; + // } else { + // //process_stats(stats_json); + // json_decref(root); + // } +} + +void lc_once_per_gameframe(void) +{ + CURLMsg *messages; + CURL *handle; + request_t *request; + int handles, remaining; + + // Debug request counts + //gi.dprintf("%s: current_requests = %d\n", __func__, current_requests); + if (current_requests <= 0) + return; + if (curl_multi_perform(stack, &handles) != CURLM_OK) + { + gi.dprintf("%s: curl_multi_perform error -- resetting libcurl session\n", __func__); + lc_init_function(); + return; + } + while ((messages = curl_multi_info_read(stack, &remaining))) + { + handle = messages->easy_handle; + if (messages->msg == CURLMSG_DONE) + { + curl_easy_getinfo(handle, CURLINFO_PRIVATE, &request); + if (request->data_count < MAX_DATA_BYTES) + request->data[request->data_count] = '\0'; + else + request->data[MAX_DATA_BYTES - 1] = '\0'; + lc_parse_response(request->data); + free(request->payload); // Frees up the memory allocated by strdup + recycle_request(request); + curl_multi_remove_handle(stack, handle); + curl_easy_cleanup(handle); + current_requests--; + } + } + if (handles == 0) + current_requests = 0; +} + +qboolean ready_to_announce(void) +{ + if (level.framenum % 100 == 0 && level.lc_recently_sent) + return true; +} diff --git a/src/action/tng_net.h b/src/action/tng_net.h new file mode 100644 index 000000000..cb9353e61 --- /dev/null +++ b/src/action/tng_net.h @@ -0,0 +1,26 @@ +#include + +#define MAX_DATA_BYTES 16384 +#define MAX_REQUESTS 256 // Must be 1 or more + + +typedef struct request_s { + char* url; + char* payload; + CURL* handle; + size_t data_count; + char data[MAX_DATA_BYTES]; +} request_t; + +typedef struct request_list_s { + struct request_list_s *next; + request_t request; +} request_list_t; + +extern request_list_t *active_requests, *unused_requests; +extern request_list_t request_nodes[MAX_REQUESTS]; +extern CURLM *stack; +extern size_t current_requests; + +void lc_server_announce(char *path, char *message); +void announce_server_populating(void); \ No newline at end of file From 4a8e8c9004cadfebba03451c8fa7984869f48363 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Feb 2024 13:21:15 -0500 Subject: [PATCH 075/974] Fixed a warning, uuid linker fix --- meson.build | 1 + meson_options.txt | 6 +++--- src/action/tng_net.c | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 5b2e5cf82..0575fec3e 100644 --- a/meson.build +++ b/meson.build @@ -409,6 +409,7 @@ if not win32 if uuid.found() client_deps += uuid config.set10('USE_UUID', true) + dll_link_args += '-luuid' endif jansson = dependency('jansson', diff --git a/meson_options.txt b/meson_options.txt index 066d1879e..a0a172999 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -156,9 +156,9 @@ option('tga', value: true, description: 'TGA images support') -option('uuid', - type: 'feature', - value: 'auto', +option('uuid', + type: 'boolean', + value: 'true', description: 'AQtion uuid support') option('variable-fps', diff --git a/src/action/tng_net.c b/src/action/tng_net.c index e5e0f4423..fc4f060e2 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -90,7 +90,7 @@ void lc_get_player_stats(char* message) // Get a new request object request = new_request(); if (request == NULL) { - gi.dprintf("Ran out of request slots\n"); + gi.dprintf("tng_net: request_t ran out of request slots\n"); return; } @@ -406,4 +406,5 @@ qboolean ready_to_announce(void) { if (level.framenum % 100 == 0 && level.lc_recently_sent) return true; + return false; } From 015b7e7d433f187652049579c83057eb965591ba Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 26 Feb 2024 19:05:41 -0500 Subject: [PATCH 076/974] Stats work directly to API, added cvar_check() func --- src/action/a_xgame.c | 16 +++++++++++++ src/action/a_xgame.h | 1 + src/action/g_local.h | 2 +- src/action/g_main.c | 2 +- src/action/g_save.c | 2 +- src/action/tng_net.c | 51 +++++++++++++++++++++++++++++++++--------- src/action/tng_net.h | 4 +++- src/action/tng_stats.c | 21 ++++++++++------- 8 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/action/a_xgame.c b/src/action/a_xgame.c index 7736e0c10..ed0806ded 100644 --- a/src/action/a_xgame.c +++ b/src/action/a_xgame.c @@ -790,4 +790,20 @@ qboolean is_valid_ipv4(char *ip_str) free(ip_copy); return true; +} + +/* +cvar_check + +Supply a cvar and it will return true if it is set and not empty +Also works for checking if cvar strings are empty or unset +*/ +qboolean cvar_check(cvar_t *cvar) { + if (!cvar->value + || cvar->string == NULL + || strcmp(cvar->string, "disabled") == 0 + || strcmp(cvar->string, "") == 0) { + return false; + } + return true; } \ No newline at end of file diff --git a/src/action/a_xgame.h b/src/action/a_xgame.h index 2a0c8c309..6fa2ec166 100644 --- a/src/action/a_xgame.h +++ b/src/action/a_xgame.h @@ -62,3 +62,4 @@ void Cmd_SetFlag2_f(edict_t *self); void Cmd_SaveFlags_f(edict_t *self); int CountRealPlayers(void); qboolean is_valid_ipv4(char *ip_str); +qboolean cvar_check(cvar_t *cvar); \ No newline at end of file diff --git a/src/action/g_local.h b/src/action/g_local.h index 3e2befd34..f5c93cd5f 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1282,7 +1282,7 @@ extern cvar_t *warmup_unready; // cURL integration extern cvar_t *sv_curl_enable; extern cvar_t *sv_discord_announce_enable; -extern cvar_t *sv_curl_stat_api_url; +extern cvar_t *sv_curl_stat_enable; extern cvar_t *sv_curl_discord_chat_url; extern cvar_t *sv_curl_discord_server_url; extern cvar_t *server_ip; diff --git a/src/action/g_main.c b/src/action/g_main.c index 59a5a3376..2588cb0b9 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -534,7 +534,7 @@ cvar_t *warmup_unready; // Toggles warmup if captains unready // cURL integration / tng_net.c cvar_t *sv_curl_enable; cvar_t *sv_discord_announce_enable; -cvar_t *sv_curl_stat_api_url; +cvar_t *sv_curl_stat_enable; cvar_t *sv_curl_discord_chat_url; cvar_t *sv_curl_discord_server_url; cvar_t *server_ip; diff --git a/src/action/g_save.c b/src/action/g_save.c index 38b659672..c7c36fdf0 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -647,7 +647,7 @@ void InitGame( void ) // curl / tng_net.c sv_curl_enable = gi.cvar("sv_curl_enable", "0", 0); sv_discord_announce_enable = gi.cvar("sv_discord_announce_enable", "0", 0); - sv_curl_stat_api_url = gi.cvar("sv_curl_stat_api_url", "disabled", 0); + sv_curl_stat_enable = gi.cvar("sv_curl_stat_enable", "0", 0); sv_curl_discord_chat_url = gi.cvar("sv_curl_discord_chat_url", "disabled", 0); sv_curl_discord_server_url = gi.cvar("sv_curl_discord_server_url", "disabled", 0); server_ip = gi.cvar("server_ip", "", 0); // Never include this in serverinfo! diff --git a/src/action/tng_net.c b/src/action/tng_net.c index fc4f060e2..ca83ab122 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -80,12 +80,12 @@ void lc_get_player_stats(char* message) request_t *request; // Don't run this if curl is disabled or the webhook URL is set to "disabled" - if (!sv_curl_enable->value || strcmp(sv_curl_stat_api_url->string, "disabled") == 0) + if (!sv_curl_enable->value) return; // Use webhook.site to test curl, it's very handy! //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; - char* url = sv_curl_stat_api_url->string; + char* url = AQ2WORLD_STAT_URL; // Get a new request object request = new_request(); @@ -189,21 +189,52 @@ void lc_discord_webhook(char* message) lc_start_request_function(request); } +void lc_aqtion_stat_send(char *stats) +{ + if (!sv_curl_stat_enable->value) + return; + + if (!sv_curl_enable->value) { + gi.dprintf("%s: sv_curl_enable is disabled, disabling stat reporting to API\n", __func__); + gi.cvar_forceset("sv_curl_stat_enable", "0"); + return; + } + + request_t *request; + char full_url[1024]; + char path[] = "/api/v1/stats"; + char* url = AQ2WORLD_STAT_URL; + + // Get a new request object + request = new_request(); + if (request == NULL) { + gi.dprintf("%s: Ran out of request slots\n", __func__); + return; + } + //gi.dprintf("%s: Sending stats to %s\n", __func__, url); + // Use webhook.site to test curl, it's very handy! + //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; + // Concatenate url and path + sprintf(full_url, "%s%s", url, path); + request->url = full_url; + request->payload = strdup(stats); + lc_start_request_function(request); +} + void lc_server_announce(char *path, char *message) { request_t *request; char full_url[1024]; // Don't run this if curl is disabled or the webhook URLs are set to "disabled" or empty, or if no server IP or port is set - if (!sv_curl_enable->value - || strcmp(server_announce_url->string, "disabled") == 0 - || strcmp(server_announce_url->string, "") == 0 - || strcmp(sv_curl_discord_server_url->string, "disabled") == 0 - || strcmp(sv_curl_discord_server_url->string, "") == 0 - || !server_ip->string - || !server_port->string - ) + if (!cvar_check(sv_curl_enable) + || !cvar_check(server_announce_url) + || !cvar_check(sv_curl_discord_server_url) + || !cvar_check(server_ip) + || !cvar_check(server_port)) + { return; + } // Get a new request object request = new_request(); diff --git a/src/action/tng_net.h b/src/action/tng_net.h index cb9353e61..f306057dd 100644 --- a/src/action/tng_net.h +++ b/src/action/tng_net.h @@ -3,6 +3,7 @@ #define MAX_DATA_BYTES 16384 #define MAX_REQUESTS 256 // Must be 1 or more +#define AQ2WORLD_STAT_URL "https://apigateway.aq2world.com" typedef struct request_s { char* url; @@ -23,4 +24,5 @@ extern CURLM *stack; extern size_t current_requests; void lc_server_announce(char *path, char *message); -void announce_server_populating(void); \ No newline at end of file +void announce_server_populating(void); +void lc_aqtion_stat_send(char *stats); \ No newline at end of file diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 4d1373ca1..1687a55f3 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -615,16 +615,21 @@ void Write_Stats(const char* msg, ...) vsprintf(stat_cpy, msg, argptr); va_end(argptr); - logfile_name = gi.cvar("logfile_name", "", CVAR_NOSET); - sprintf(logpath, "action/logs/%s.stats", logfile_name->string); + // Send stats to API, else write to local file + if (sv_curl_enable->value && sv_curl_stat_enable->value) { + lc_aqtion_stat_send(stat_cpy); + } else { + logfile_name = gi.cvar("logfile_name", "", CVAR_NOSET); + sprintf(logpath, "action/logs/%s.stats", logfile_name->string); - if ((f = fopen(logpath, "a")) != NULL) - { - fprintf(f, "%s", stat_cpy); - fclose(f); + if ((f = fopen(logpath, "a")) != NULL) + { + fprintf(f, "%s", stat_cpy); + fclose(f); + } + else + gi.dprintf("Error writing to %s.stats\n", logfile_name->string); } - else - gi.dprintf("Error writing to %s.stats\n", logfile_name->string); } From b01e40b2ecab54955e200ad86d80c71f01662af4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 26 Feb 2024 21:42:43 -0500 Subject: [PATCH 077/974] Maybe official server stuff, maybe not --- src/action/g_local.h | 2 ++ src/action/g_main.c | 2 ++ src/action/g_save.c | 2 ++ src/action/tng_net.c | 21 ++++++++++++++++++++- src/action/tng_net.h | 2 ++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index f5c93cd5f..80e314485 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1283,6 +1283,8 @@ extern cvar_t *warmup_unready; extern cvar_t *sv_curl_enable; extern cvar_t *sv_discord_announce_enable; extern cvar_t *sv_curl_stat_enable; +extern cvar_t *sv_aws_access_key; +extern cvar_t *sv_aws_secret_key; extern cvar_t *sv_curl_discord_chat_url; extern cvar_t *sv_curl_discord_server_url; extern cvar_t *server_ip; diff --git a/src/action/g_main.c b/src/action/g_main.c index 2588cb0b9..079da6a94 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -535,6 +535,8 @@ cvar_t *warmup_unready; // Toggles warmup if captains unready cvar_t *sv_curl_enable; cvar_t *sv_discord_announce_enable; cvar_t *sv_curl_stat_enable; +cvar_t *sv_aws_access_key; +cvar_t *sv_aws_secret_key; cvar_t *sv_curl_discord_chat_url; cvar_t *sv_curl_discord_server_url; cvar_t *server_ip; diff --git a/src/action/g_save.c b/src/action/g_save.c index c7c36fdf0..06c8a8623 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -648,6 +648,8 @@ void InitGame( void ) sv_curl_enable = gi.cvar("sv_curl_enable", "0", 0); sv_discord_announce_enable = gi.cvar("sv_discord_announce_enable", "0", 0); sv_curl_stat_enable = gi.cvar("sv_curl_stat_enable", "0", 0); + sv_aws_access_key = gi.cvar("sv_aws_access_key", "disabled", 0); // Never include this in serverinfo! + sv_aws_secret_key = gi.cvar("sv_aws_secret_key", "disabled", 0); // Never include this in serverinfo! sv_curl_discord_chat_url = gi.cvar("sv_curl_discord_chat_url", "disabled", 0); sv_curl_discord_server_url = gi.cvar("sv_curl_discord_server_url", "disabled", 0); server_ip = gi.cvar("server_ip", "", 0); // Never include this in serverinfo! diff --git a/src/action/tng_net.c b/src/action/tng_net.c index ca83ab122..455a077e6 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -201,9 +201,19 @@ void lc_aqtion_stat_send(char *stats) } request_t *request; + // If both AWS keys are set, this is an official server + if (cvar_check(sv_aws_access_key) && cvar_check(sv_aws_secret_key)) + request->official_server = true; + char full_url[1024]; - char path[] = "/api/v1/stats"; char* url = AQ2WORLD_STAT_URL; + char path[64]; + // Non-official servers use a different path + if (request->official_server) { + char path[] = "/api/v1/stats/official"; + } else { + char path[] = "/api/v1/stats"; + } // Get a new request object request = new_request(); @@ -332,6 +342,15 @@ void lc_start_request_function(request_t* request) curl_easy_setopt(request->handle, CURLOPT_URL, request->url); curl_easy_setopt(request->handle, CURLOPT_WRITEDATA, request); // Passed as pvt to lc_receive_data_function curl_easy_setopt(request->handle, CURLOPT_PRIVATE, request); // Returned by curl_easy_getinfo with the CURLINFO_PRIVATE option + + if (request->official_server){ + // Set up the AWS Signature Version 4 options + curl_easy_setopt(request->handle, CURLOPT_AWS_SIGV4, "aws:amz:us-east-1:execute-api"); + // Set the username and password for HTTP authentication + curl_easy_setopt(request->handle, CURLOPT_USERNAME, sv_aws_access_key->string); + curl_easy_setopt(request->handle, CURLOPT_PASSWORD, sv_aws_secret_key->string); + } + curl_multi_add_handle(stack, request->handle); current_requests++; } diff --git a/src/action/tng_net.h b/src/action/tng_net.h index f306057dd..584ca89e1 100644 --- a/src/action/tng_net.h +++ b/src/action/tng_net.h @@ -8,9 +8,11 @@ typedef struct request_s { char* url; char* payload; + char *user; CURL* handle; size_t data_count; char data[MAX_DATA_BYTES]; + qboolean official_server; } request_t; typedef struct request_list_s { From ac5aced185dc1742674b0a4e50f1d90e26f92908 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 26 Feb 2024 22:09:35 -0500 Subject: [PATCH 078/974] Simple stat collector --- src/action/g_cmds.c | 4 ++-- src/action/tng_net.c | 22 ++-------------------- src/action/tng_net.h | 1 - 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index a50c9b06a..94535b2e6 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1699,10 +1699,10 @@ static void Cmd_PrintSettings_f( edict_t * ent ) Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" "limchasecam %2d tgren %2d antilag_interp %2d\n" - "use_xerp %2d llsound %2d\n", + "use_xerp %2d llsound %2d stats %2d\n", (int)timelimit->value, (int)roundlimit->value, (int)roundtimelimit->value, (int)limchasecam->value, (int)tgren->value, (int)sv_antilag_interp->value, - (int)use_xerp->value, (int)llsound->value ); + (int)use_xerp->value, (int)llsound->value, (int)stat_logs->value ); #else Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 455a077e6..a3d8d9d21 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -121,7 +121,7 @@ void announce_server_populating(void) } int playercount = CountRealPlayers(); - // Do not announce if player count is less than 25% of maxclients + // Do not announce if player count is less than 20% of maxclients float threshold = fmax((game.maxclients * 0.2), 3); if (playercount < threshold) return; @@ -201,19 +201,9 @@ void lc_aqtion_stat_send(char *stats) } request_t *request; - // If both AWS keys are set, this is an official server - if (cvar_check(sv_aws_access_key) && cvar_check(sv_aws_secret_key)) - request->official_server = true; - char full_url[1024]; char* url = AQ2WORLD_STAT_URL; - char path[64]; - // Non-official servers use a different path - if (request->official_server) { - char path[] = "/api/v1/stats/official"; - } else { - char path[] = "/api/v1/stats"; - } + char path[] = "/api/v1/stats"; // Get a new request object request = new_request(); @@ -343,14 +333,6 @@ void lc_start_request_function(request_t* request) curl_easy_setopt(request->handle, CURLOPT_WRITEDATA, request); // Passed as pvt to lc_receive_data_function curl_easy_setopt(request->handle, CURLOPT_PRIVATE, request); // Returned by curl_easy_getinfo with the CURLINFO_PRIVATE option - if (request->official_server){ - // Set up the AWS Signature Version 4 options - curl_easy_setopt(request->handle, CURLOPT_AWS_SIGV4, "aws:amz:us-east-1:execute-api"); - // Set the username and password for HTTP authentication - curl_easy_setopt(request->handle, CURLOPT_USERNAME, sv_aws_access_key->string); - curl_easy_setopt(request->handle, CURLOPT_PASSWORD, sv_aws_secret_key->string); - } - curl_multi_add_handle(stack, request->handle); current_requests++; } diff --git a/src/action/tng_net.h b/src/action/tng_net.h index 584ca89e1..2df650c17 100644 --- a/src/action/tng_net.h +++ b/src/action/tng_net.h @@ -12,7 +12,6 @@ typedef struct request_s { CURL* handle; size_t data_count; char data[MAX_DATA_BYTES]; - qboolean official_server; } request_t; typedef struct request_list_s { From a03c76f377d3658f4634252857391dd8dbcdd0bb Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Feb 2024 13:09:53 -0500 Subject: [PATCH 079/974] Added bot json examples --- bots/bots.bin | Bin 0 -> 586 bytes bots/bots_clan/wesleysnipes.json | 35 ++ bots/bots_pub/anakinskywalker.json | 35 ++ bots/bots_pub/angeloprovolone.json | 35 ++ bots/bots_pub/captivandanko.json | 35 ++ bots/bots_pub/chanceboudreaux.json | 35 ++ bots/bots_pub/disfordigger.json | 35 ++ bots/bots_pub/doctordeathwish.json | 35 ++ bots/bots_pub/indianajones.json | 35 ++ bots/bots_pub/ivankraschinsky.json | 35 ++ bots/bots_pub/jacqueskristoff.json | 35 ++ bots/bots_pub/ltraymondtango.json | 35 ++ bots/bots_pub/nikolaicherenko.json | 35 ++ bots/bots_pub/pitspawnfouldog.json | 35 ++ bots/bots_pub/snapchipshatter.json | 35 ++ bots/bots_pub/takedatakahashi.json | 35 ++ bots/bots_pub/whyarewehere.json | 35 ++ bots/charlen.py | 79 ++++ bots/clans/440.json | 6 + bots/clans/BH.json | 6 + bots/clans/CS.json | 6 + bots/clans/DoS.json | 6 + bots/clans/EL.json | 6 + bots/clans/FZe.json | 6 + bots/clans/TBS.json | 6 + bots/clans/TRB.json | 6 + bots/clans/Tq.json | 6 + bots/clans/Wang.json | 6 + bots/clans/blade.json | 6 + bots/index.txt | 2 + bots/other.txt | 659 +++++++++++++++++++++++++++++ bots/parser.py | 79 ++++ 32 files changed, 1445 insertions(+) create mode 100644 bots/bots.bin create mode 100644 bots/bots_clan/wesleysnipes.json create mode 100644 bots/bots_pub/anakinskywalker.json create mode 100644 bots/bots_pub/angeloprovolone.json create mode 100644 bots/bots_pub/captivandanko.json create mode 100644 bots/bots_pub/chanceboudreaux.json create mode 100644 bots/bots_pub/disfordigger.json create mode 100644 bots/bots_pub/doctordeathwish.json create mode 100644 bots/bots_pub/indianajones.json create mode 100644 bots/bots_pub/ivankraschinsky.json create mode 100644 bots/bots_pub/jacqueskristoff.json create mode 100644 bots/bots_pub/ltraymondtango.json create mode 100644 bots/bots_pub/nikolaicherenko.json create mode 100644 bots/bots_pub/pitspawnfouldog.json create mode 100644 bots/bots_pub/snapchipshatter.json create mode 100644 bots/bots_pub/takedatakahashi.json create mode 100644 bots/bots_pub/whyarewehere.json create mode 100644 bots/charlen.py create mode 100644 bots/clans/440.json create mode 100644 bots/clans/BH.json create mode 100644 bots/clans/CS.json create mode 100644 bots/clans/DoS.json create mode 100644 bots/clans/EL.json create mode 100644 bots/clans/FZe.json create mode 100644 bots/clans/TBS.json create mode 100644 bots/clans/TRB.json create mode 100644 bots/clans/Tq.json create mode 100644 bots/clans/Wang.json create mode 100644 bots/clans/blade.json create mode 100644 bots/index.txt create mode 100644 bots/other.txt create mode 100644 bots/parser.py diff --git a/bots/bots.bin b/bots/bots.bin new file mode 100644 index 0000000000000000000000000000000000000000..ec9d3626ad38ef0d28c0f128d88aae6eb88509fc GIT binary patch literal 586 zcmV-Q0=4}l0RRAaoLy1PPQx$^ewC_c#9*Vq_5eF0wGsz}CO0*!Y2uV5?Nm*Dcbs;L z1gCzszdxndoWWpz$nuZP=R10hQ&uwd=<~ZQR~q<4jhr84@*Qzx8uXr^-P9TyjI;|v zZ~4vP7dm7m+Cz5DO}E_%c({KRN;7~yrA;9l(-#YViTG)O`v_OYPGGAN*~dz58MiWa zOyiQW%FmK2Rn|o@%Z37)|9wOeQlA#JN$wi_%{;n!(oy76C9kQMbf1dVz>U>(qbz~S zI;Z z+5z}bn3XcNUuFE3@G`@7g0o_&@N9}W2=(amaiPaf=#o=r`$IlHO@S=QY2OiZv3Ud&$w@=`o8}V6Svv?BNRzF{@3P|0q|vV|an@bXqQre1 Y8H?T>zfkvIaO=!4mt9ZO5BzGK$eNoXh5!Hn literal 0 HcmV?d00001 diff --git a/bots/bots_clan/wesleysnipes.json b/bots/bots_clan/wesleysnipes.json new file mode 100644 index 000000000..21a13f7d8 --- /dev/null +++ b/bots/bots_clan/wesleysnipes.json @@ -0,0 +1,35 @@ +{ + "name": "Wesley Snipes", + "clan_id": 1, + "skin": "messiah/blade", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.9, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/anakinskywalker.json b/bots/bots_pub/anakinskywalker.json new file mode 100644 index 000000000..8d86bff58 --- /dev/null +++ b/bots/bots_pub/anakinskywalker.json @@ -0,0 +1,35 @@ +{ + "name": "Anakin Skywalker", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/angeloprovolone.json b/bots/bots_pub/angeloprovolone.json new file mode 100644 index 000000000..fb4981f29 --- /dev/null +++ b/bots/bots_pub/angeloprovolone.json @@ -0,0 +1,35 @@ +{ + "name": "Angelo Provolone", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.8, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.1, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/captivandanko.json b/bots/bots_pub/captivandanko.json new file mode 100644 index 000000000..cb7b113f2 --- /dev/null +++ b/bots/bots_pub/captivandanko.json @@ -0,0 +1,35 @@ +{ + "name": "Capt. Ivan Danko", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/chanceboudreaux.json b/bots/bots_pub/chanceboudreaux.json new file mode 100644 index 000000000..5d59784e7 --- /dev/null +++ b/bots/bots_pub/chanceboudreaux.json @@ -0,0 +1,35 @@ +{ + "name": "Chance Boudreaux", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.7, + "hc": -0.0, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": -0.0, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": -0.0, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/disfordigger.json b/bots/bots_pub/disfordigger.json new file mode 100644 index 000000000..9f6a90ca3 --- /dev/null +++ b/bots/bots_pub/disfordigger.json @@ -0,0 +1,35 @@ +{ + "name": "D is for Digger!", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.8, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/doctordeathwish.json b/bots/bots_pub/doctordeathwish.json new file mode 100644 index 000000000..c198d043f --- /dev/null +++ b/bots/bots_pub/doctordeathwish.json @@ -0,0 +1,35 @@ +{ + "name": "Doctor Deathwish", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 1.0, + "movementStyle": 0.9, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/indianajones.json b/bots/bots_pub/indianajones.json new file mode 100644 index 000000000..43434a408 --- /dev/null +++ b/bots/bots_pub/indianajones.json @@ -0,0 +1,35 @@ +{ + "name": "Indiana Jones", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.8, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ivankraschinsky.json b/bots/bots_pub/ivankraschinsky.json new file mode 100644 index 000000000..5b28f6f5c --- /dev/null +++ b/bots/bots_pub/ivankraschinsky.json @@ -0,0 +1,35 @@ +{ + "name": "Ivan Kraschinsky", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.1, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jacqueskristoff.json b/bots/bots_pub/jacqueskristoff.json new file mode 100644 index 000000000..066edd287 --- /dev/null +++ b/bots/bots_pub/jacqueskristoff.json @@ -0,0 +1,35 @@ +{ + "name": "Jacques Kristoff", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.9, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ltraymondtango.json b/bots/bots_pub/ltraymondtango.json new file mode 100644 index 000000000..3421ea564 --- /dev/null +++ b/bots/bots_pub/ltraymondtango.json @@ -0,0 +1,35 @@ +{ + "name": "Lt Raymond Tango", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 1.0, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.0, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nikolaicherenko.json b/bots/bots_pub/nikolaicherenko.json new file mode 100644 index 000000000..10429a953 --- /dev/null +++ b/bots/bots_pub/nikolaicherenko.json @@ -0,0 +1,35 @@ +{ + "name": "Nikolai Cherenko", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.1, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/pitspawnfouldog.json b/bots/bots_pub/pitspawnfouldog.json new file mode 100644 index 000000000..38f2c0131 --- /dev/null +++ b/bots/bots_pub/pitspawnfouldog.json @@ -0,0 +1,35 @@ +{ + "name": "Pitspawn Fouldog", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.5, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.1, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.1, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/snapchipshatter.json b/bots/bots_pub/snapchipshatter.json new file mode 100644 index 000000000..0806e4b66 --- /dev/null +++ b/bots/bots_pub/snapchipshatter.json @@ -0,0 +1,35 @@ +{ + "name": "Snapchip Shatter", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.0 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/takedatakahashi.json b/bots/bots_pub/takedatakahashi.json new file mode 100644 index 000000000..6dd584341 --- /dev/null +++ b/bots/bots_pub/takedatakahashi.json @@ -0,0 +1,35 @@ +{ + "name": "Takeda Takahashi", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/whyarewehere.json b/bots/bots_pub/whyarewehere.json new file mode 100644 index 000000000..4be3e541b --- /dev/null +++ b/bots/bots_pub/whyarewehere.json @@ -0,0 +1,35 @@ +{ + "name": "Why are we here?", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 1.0, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/charlen.py b/bots/charlen.py new file mode 100644 index 000000000..006097495 --- /dev/null +++ b/bots/charlen.py @@ -0,0 +1,79 @@ +# Initialize lists +names_longer_than_16 = [] +names_exactly_16 = [] +names_less_than_10 = [] + +import json +import random +import os +import re + +# Function to generate a random value with a bias towards 0.5, 1 decimal place +def generate_random_value(): + return round(random.normalvariate(0.5, 0.2), 1) + +# Open and read the file +with open('other.txt', 'r') as file: + for line in file: + name = line.strip() # Remove newline characters and whitespace + length = len(name) + + # Open and read the file +with open('other.txt', 'r') as file: + for line in file: + name = line.strip() # Remove newline characters and whitespace + length = len(name) + + # Check the length and create a bot profile if it's 10 characters or less + if length <= 10: + # Randomly select whether the bot should be added to a clan + add_to_clan = random.random() > 0.5 + + bot_profile = { + "name": name, + "clan_id": random.randint(1, 11) if add_to_clan else None, + "skin": "male/indy", + "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, + "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, + "coreTraits": {key: generate_random_value() for key in ["aggressiveness", "teamwork", "curiosity", "aimingSkill", "reactionTime", "communicationFreq", "communicationTone", "movementStyle", "objectiveFocus"]} + } + + # Remove special characters from the filename + filename = re.sub('[^a-zA-Z0-9_]', '', name.lower().replace(' ', '')) + + # Write the bot profile to a new JSON file in the bots_clan/ directory + filename = os.path.join('bots_clan', filename + '.json') + with open(filename, 'w') as outfile: + json.dump(bot_profile, outfile, indent=4) + + # # Check the length and create a bot profile if it's exactly 16 characters + # if length == 16: + # bot_profile = { + # "name": name, + # "clan_id": None, + # "skin": "male/indy", + # "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, + # "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, + # "coreTraits": {key: generate_random_value() for key in ["aggressiveness", "teamwork", "curiosity", "aimingSkill", "reactionTime", "communicationFreq", "communicationTone", "movementStyle", "objectiveFocus"]} + # } + + # # Remove special characters from the filename + # filename = re.sub('[^a-zA-Z0-9_]', '', name.lower().replace(' ', '')) + + # # Write the bot profile to a new JSON file + # filename = os.path.join('bots_pub', filename + '.json') + # with open(filename, 'w') as outfile: + # json.dump(bot_profile, outfile, indent=4) + + # # Check the length and add to the appropriate list + # if length > 16: + # names_longer_than_16.append(name) + # elif length == 16: + # names_exactly_16.append(name) + # elif length < 10: + # names_less_than_10.append(name) + +# Print the lists +#print("Names longer than 16 characters:", names_longer_than_16) +#print("Names exactly 16 characters:", names_exactly_16) +#print("Names less than 10 characters:", names_less_than_10) \ No newline at end of file diff --git a/bots/clans/440.json b/bots/clans/440.json new file mode 100644 index 000000000..af9510754 --- /dev/null +++ b/bots/clans/440.json @@ -0,0 +1,6 @@ +{ + "clan_id": 7, + "clan_tag": "(440)", + "clan_name": "Clan 440", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/BH.json b/bots/clans/BH.json new file mode 100644 index 000000000..36625a90e --- /dev/null +++ b/bots/clans/BH.json @@ -0,0 +1,6 @@ +{ + "clan_id": 5, + "clan_tag": "{BH}-", + "clan_name": "Clan BH", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/CS.json b/bots/clans/CS.json new file mode 100644 index 000000000..c4d5ef31d --- /dev/null +++ b/bots/clans/CS.json @@ -0,0 +1,6 @@ +{ + "clan_id": 6, + "clan_tag": "=[CS]=", + "clan_name": "Clan CS", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/DoS.json b/bots/clans/DoS.json new file mode 100644 index 000000000..a0dd28651 --- /dev/null +++ b/bots/clans/DoS.json @@ -0,0 +1,6 @@ +{ + "clan_id": 9, + "clan_tag": "[DoS]", + "clan_name": "Clan DoS", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/EL.json b/bots/clans/EL.json new file mode 100644 index 000000000..6c1d9537e --- /dev/null +++ b/bots/clans/EL.json @@ -0,0 +1,6 @@ +{ + "clan_id": 3, + "clan_tag": "(EL)-", + "clan_name": "Clan EL", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/FZe.json b/bots/clans/FZe.json new file mode 100644 index 000000000..25021b858 --- /dev/null +++ b/bots/clans/FZe.json @@ -0,0 +1,6 @@ +{ + "clan_id": 4, + "clan_tag": "=]FZe[=", + "clan_name": "Clan FZe", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/TBS.json b/bots/clans/TBS.json new file mode 100644 index 000000000..f2e80ca57 --- /dev/null +++ b/bots/clans/TBS.json @@ -0,0 +1,6 @@ +{ + "clan_id": 10, + "clan_tag": "(TBS)", + "clan_name": "Clan TBS", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/TRB.json b/bots/clans/TRB.json new file mode 100644 index 000000000..5f5abdaef --- /dev/null +++ b/bots/clans/TRB.json @@ -0,0 +1,6 @@ +{ + "clan_id": 11, + "clan_tag": "[TRB]_", + "clan_name": "Clan TRB", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/Tq.json b/bots/clans/Tq.json new file mode 100644 index 000000000..009c1f836 --- /dev/null +++ b/bots/clans/Tq.json @@ -0,0 +1,6 @@ +{ + "clan_id": 2, + "clan_tag": "_Tq_", + "clan_name": "Clan Tq", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/Wang.json b/bots/clans/Wang.json new file mode 100644 index 000000000..55998481e --- /dev/null +++ b/bots/clans/Wang.json @@ -0,0 +1,6 @@ +{ + "clan_id": 8, + "clan_tag": "[Wang]", + "clan_name": "Clan Wang", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/blade.json b/bots/clans/blade.json new file mode 100644 index 000000000..fc8e37ca6 --- /dev/null +++ b/bots/clans/blade.json @@ -0,0 +1,6 @@ +{ + "clan_id": 1, + "clan_tag": "[/]", + "clan_name": "Clan Blade", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/index.txt b/bots/index.txt new file mode 100644 index 000000000..b1ebf9cb2 --- /dev/null +++ b/bots/index.txt @@ -0,0 +1,2 @@ +0,[/]Wesley Snipes,0,292 +1,Indiana Jones,296,286 diff --git a/bots/other.txt b/bots/other.txt new file mode 100644 index 000000000..f91618e2a --- /dev/null +++ b/bots/other.txt @@ -0,0 +1,659 @@ +John Carmack +John Romero +Michael Abrash +Sandy Petersen +Kevin Cloud +Adrian Carmack +American Mcgee +Tim Willits +Sonic Mayhem +Trent Reznor +NiN +Scourge of Armagon +Dissolution of Eternity +The Reckoning +Ground Zero +Enforcer +Gunner +Berserker +Iron Maiden +Gladiator +Medic +Technician +Icarus +Tank +Makron +Quad Machine +Cerberon +Strogg +Bitterman +Crash +Ranger +Phobos +Mynx +Orbb +Sarge +Grunt +Hossman +Daemia +Hunter +Angel +Gorre +Klesk +Slash +Wrack +Biker +Lucy +Patriot +Tank Jr +Anarki +Stripe +Razor +Keel +Visor +Uriel +Bones +Cadaver +Sorlag +Doom +Major +Xaero +Ratbot +HAL-9000 +Brandon Herrera +John Conner +Terminator +T-800 +T-1000 +Anakin Skywalker +Short Round +Barney Ross +The Boss +Dean Stevens +The Dean +Jake Malloy +Joe Tanto +Kit Latura +Robert Rath +Judge Joseph Dredd +Ray Quick +John Spartan +Gabe Walker +Angelo Provolone +Snaps +Lt Raymond Tango +Lincoln Hawk +Lieutenant Marion Cobretti +Cobra +John Rambo +Captain Robert Hatch +Deke DaSilva +Cosmo Carboni +Johnny Kovac +Rocky Balboa +Elmore Caddo +Rocky +Machine Gun Joe Viterbo +Jerry Savage +Stud +Trench +Gordy Brewer +Jericho Cane +Dr Victor Fries +U.S. Marshal John Kruger +The Eraser +Harry Tasker +Jack Slater +Capt. Ivan Danko +Dutch +Mark Kaminsky +Joseph P. Brenner +John Matrix +Kalidor +Conan +Handsome Stranger +Muscleman +Joe Santo +Church +Dink Heimowitz +Mr Suave +Frank Moses +Harrison Hill +Col. Doug Masterson +Mr Goodkat +Sonny Truelove +Lieutenant AK Waters +Jimmy Tudeski +The Tulip +Trey Kincaid +Major General William Deveraux +Harry Stamper +Korben Dallas +Muddy Grimes +Butch Coolidge +Det. Tom Hardy +Hudson Hawk +James Urbanski +Tom Mix +Lee Christmas +Jensen Ames +Terry Leather +Farmer the farmer +Chev Chelios +Handsome Rob +Monk +MVA Agent Evan Funsch +Sgt Jericho Butler +Turkish +Bacon +Jean Vilain +Samson Gaul +Tiano +Vincent Brazil +Frenchy +Jack Robideaux +Phillip Sauvage +Ben Archer +Jacques Kristoff +Rudy Cafmeyer +Charles Le Vaillaint +Replicant +Edward Garrotte +Alain Moreau +Mikhail Suverov +Darren McCord +Colonel William F. Guile +Chance Boudreaux +Luc Deveraux +Alex Wagner +Chad Wagner +Gibson Rickenbacker +Frank Dux +Ivan Kraschinsky +Booker +John Shepherd +Joshua McCord +Jake Fallon +McKenna +Jake Wilder +Frank Shatter +Capt. Ranger Cordell Walker +Matt Hunter +JJ McQuade +Lone Wolf +Sean Kane +John T. Booker +Chuck Slaughter +Colt +Gunner Jensen +Max Gatling +The German +Dr. Sage Mennox +Edward Genn +Icarus +Mike Riggins +Ryder +Xander Ronson +Brixos +Nikolai Cherenko +Lance Rockford +Sgt. Frank Gannon +Sam Decker +Warchild +Major Frank Cross +Waxman +Michael Dane +Nick Gunar +Wellman Anthony Santee +Det. Jack Caine +Lt. Nikolai Rachenko +Ivan Drago +Venz +Tom Steele +Elijah Kane +Rogelio Torrez +Samuel Axel +Ruslan +Tao +Cock Puncher +Simon Ballister +Commander Marshall Lawson +Jonathan Cold +Harlan Banks +Travis Hunter +William Lansing +Professor Robert Burns +Sasha Petrosevitch +Frank Glass +Orin Boyd +Lt. Colonel Austin Travis +Forrest Taft +Casey Ryback +Detective Gino Felino +Mason Storm +General Hugh McKraken +Chief Benjamin Benson +Freddie Wiseman +Dr. Artimus Snodgrass +General Skyler +Adam Beaudreaux +Billy Smith +Hurricane +Colonel Carl Brewster +Kyle Western +Sgt. Jericho Jackson +Action Jackson +Cullen Monroe +Lieutenant Harry Braker +Sundog +Dreamer Tatum +Bateman Hooks +Apollo Creed +Jack Hopper +Yarbro +Hambone +Bad Sam +Clint Armstrong +Not Hasselhoff +Little G +JD Gold +Strom +Leland Duvall +Warden Pitt +Joe Joiner +Huck Finney +Lt. Col. 'Mac' Miller +Jack Saxon +Arklon +Jack Maxwell +Van Vandameer +Victor Lundgren +Alriss Ryder +Di Nardo +Carpenter +John Luger +Sam Striker +Capt. Alvin Luther Regency +Wilson Mahood +Jimmy Jo Walker +Lieutenant Byrd +Stoney Cooper +Reddog +Ramrod +Kimbo Slice +Boy +Gung Ho +Mincer +Major Pain +Jonny Rambone +Dominator +Noogie +Cruton +Heljay +Mongrel +Private Brown +Brown Steel +Doctor Deathwish +Stainmaster B +Kippy +Gyngemaster2000 +Helmut +SGT CAPPA +Saboy +Chipee McSpray +Himdar +Vedro +Icexo +Tahlkora +Bolund +Ellanil +Galul +Mindelos +Ostmor +Sagel +Thalath +Dharmesh +Aegarond +Havo +Jarno +Leiruth +Edoli +Edwig +Faden +Skorgan +Walter +Fred +Morgan +Lawrence +Richard +Michael +Brad +George +Anton +Monty +Bean +Sean +Backfire +Halfwit +Halfbaked +Fullmonty +Nohope +Hitnrun +Missnrun +Oysterhead +Fullthrottle +NoAmmo +Bullseye +Aimless +Blackadder +Newbie +Cledus +Chimichanga +Razor +Tanz +Vic +Malin +Thor +Grog +Drago +Stinger +Tarnok +Wens +Brohn +Ivor +Gordac +Bortack +Morgriff +Kole +Garbok +Korv +Vokur +Gali +Beelz +CraigChrist +WhittlinMan +SpecialEd +Flint +Argo +Milius +Vera +Merki +Tarnen +Royen +Kaolin +Nandet +Nundak +Ryver +Mungri +Chan +Amadi +Zarna +PillowPants +ListerFiend +Ilikechocmilk +Andromeed +Cybr +Applee +uzayTron +[Corrus] +[Mee2] +GreZZer +Drillbit +Ant__Dog +Si +Tille-Teh-Black +itiXOID +Blythe +**Viander** +wysyyy +hozef-8 +ActioN-ReactioN +VeY +Dolin +Vanoss +Imentu +T_bon +cook did it +Panther.Killian +Chess +Dragoooon +Hammerhead +Bright GhOst +CowChop +Bob Ross +Nibbler +Private Ryan +GhOstBl4de +_*NiXXy*_ +_*PixxY*_ +D3c3ptiv3 Jo3 +Merry Christmas +Merry Yeetus +NinjaMantis +SteamPug +TheWay +Scary_Kitty +Mr.Badger +Cannon Balls Of Steel +_Gholem_ +_The Medic_ +VenoM +Broz +Himself +The Only +Way Of The +_GLiCH +Hecker +ParaDox +Two Four Eight +Bilbo T-Bagger +PewDiePie +Jinx +JohnShnowden +Heckin Heck +Why Me? +Why are we here? +Where's Mark Hunt? +Hell-oKitteh +Slice +BroFist +Bad JuJu +Return of the Byeah +Po-Tay-Toe +Lemmie +Wreckage +Butter Jelly +tatu +Major Major +Digz +Figgz +Eggcecutioner +MoreEggcellent +C-Lion +attractiveCow +Grimm +D is for Digger! +Professor Jones +{itzChamp} +Sigil +Willitz** +Chickenn...! +Evo89 +John Romero +=WhiteBeard= +CitizenKane +chipp_ +^^ChoppY^^ +Dr.Jay +Fexel +FXguy +Teabag +Gigavoltz +gogman +GuN +JC Denton +deUSvauLT +exBEE +JCe +Joce +Marku +Pyro Yetti +SergeantBuck +2DCitizen +Amu +BVe +BigMack +doc +Doctor Doom +FUNKANIK +GreetingzGentlemen +Jarvis +kuri +Kor +Kermit D Frog +Jazz +MannY +Hellhound +ApeM +~Shadowfax~ +Socks +Spocket +Spar +Johnny Cage +Kano +Liu Kang +Raiden +Reptile +Scorpion +Shang Tsung +Sonya Blade +Sub-Zero +Baraka +Jade +Jax +Kintaro +Kitana +Kung Lao +Mileena +Noob Saibot +Shao Kahn +Smoke +Chameleon +Cyrax +Ermac +Kabal +Khameleon +Motaro +Nightwolf +Rain +Sektor +Sheeva +Sindel +Stryker +Fujin +Quan Chi +Sareena +Shinnok +Jarek +Kai +Meat +Reiko +Tanya +Tremor +Blaze +Bo' Rai Cho +Drahmin +Frost +Hsu Hao +Kenshi +Li Mei +Mavado +Mokap +Moloch +Nitara +Ashrah +Dairou +Darrius +Havik +Hotaru +Kira +Kobra +Onaga +Shujinko +Daegon +Taven +Dark Kahn +Skarlet +Cassie Cage +D'Vorah +Erron Black +Ferra/Torr +Jacqui Briggs +Kotal Kahn +Kung Jin +Takeda Takahashi +Triborg +Cetrion +Geras +Kollector +Kronika +Belokk +AqtionWoman +AqtionLLama +Bishibosh +Bonebreak +Coldcrow +Rakanishu +Treehead +Griswold +Countess +Pitspawn Fouldog +Flamespike +Boneash +Radament +Bloodwitch +Fangskin +Beetleburst +Leatherarm +Coldworm +Summoner +Kaa +Smithy +Witch Doctor +Stormtree +Sarina +Icehawk +Ismail +Geleb +Bremm +Toorc +Wyand +Endugu +Maffer +Death +Tormentor +Taintbreeder +Riftwraith +Infector of Souls +Lord De Seis +Vizier +Cow King +Corpsefire +Feature Creep +Siege +Barbarian +Axe Dweller +Bonesaw Breaker +Dac Farren +Megaflow Rectifier +Eyeback +Threash +Pindleskin +Snapchip Shatter +Anodized +Vinvear Molech +Tooth Sayer +Magma Torquer +Blaze Ripper +Frozenstein +Nihlathak \ No newline at end of file diff --git a/bots/parser.py b/bots/parser.py new file mode 100644 index 000000000..b3d534f89 --- /dev/null +++ b/bots/parser.py @@ -0,0 +1,79 @@ +import json +import zlib +import struct +import os + +def load_all_clan_data(): + clans = {} + seen_clan_ids = {} + for filename in os.listdir('clans'): + if filename.endswith('.json'): + with open(f'clans/{filename}', 'r') as file: + clan = json.load(file) + if clan['clan_id'] in seen_clan_ids: + raise ValueError(f"Duplicate clan_id {clan['clan_id']} found in files {seen_clan_ids[clan['clan_id']]} and {filename}") + seen_clan_ids[clan['clan_id']] = filename + clans[clan['clan_id']] = clan + + return clans + +def write_bot_to_file(bot, clan, file, index_file, index): + # Add the clan tag to the bot name if a clan is provided + if clan: + if clan['pre_or_post'] == 0: + bot['name'] = clan['clan_tag'] + bot['name'] + else: + bot['name'] = bot['name'] + clan['clan_tag'] + + if len(bot['name']) > 16: + raise ValueError(f"Bot name {bot['name']} is too long: {len(bot['name'])} characters (max 16)") + + # Convert the bot dictionary to a JSON string + json_string = json.dumps(bot) + + # Convert the JSON string to bytes + json_bytes = json_string.encode('utf-8') + + # Compress the bytes + compressed_bytes = zlib.compress(json_bytes) + + # Get the current position in the file + offset = file.tell() + + # Write the length of the compressed data + file.write(struct.pack('I', len(compressed_bytes))) + + # Write the compressed data + file.write(compressed_bytes) + + # Write the bot's index, name, offset, and length to the index file + index_file.write(f'{index},{bot["name"]},{offset},{len(compressed_bytes)}\n') + +# Load all the clan data +clans = load_all_clan_data() + +# Open the binary file and the index file +with open('bots.bin', 'wb') as file, open('index.txt', 'w') as index_file: + index = 0 + # Load the bot data from multiple JSON files in bots_clan directory + for bot_filename in os.listdir('bots_clan'): + if bot_filename.endswith('.json'): + with open(f'bots_clan/{bot_filename}', 'r') as bot_file: + bot = json.load(bot_file) + + # Get the clan data for this bot + clan = clans.get(bot["clan_id"]) + + # Write the bot data to the binary file + write_bot_to_file(bot, clan, file, index_file, index) + index += 1 + + # Load the bot data from multiple JSON files in bots_pub directory + for bot_filename in os.listdir('bots_pub'): + if bot_filename.endswith('.json'): + with open(f'bots_pub/{bot_filename}', 'r') as bot_file: + bot = json.load(bot_file) + + # Write the bot data to the binary file without a clan + write_bot_to_file(bot, None, file, index_file, index) + index += 1 \ No newline at end of file From 8a41e0735d21ca64a5bb7467ea0c0073f3a2d3ab Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Feb 2024 13:15:16 -0500 Subject: [PATCH 080/974] Adjusted clan tag, moved bots to the right dirs --- bots/bots.bin | Bin 586 -> 123146 bytes bots/bots_clan/2dcitizen.json | 35 +++ bots/bots_clan/_gholem_.json | 35 +++ bots/bots_clan/_glich.json | 35 +++ bots/bots_clan/_pixxy_.json | 35 +++ bots/bots_clan/aegarond.json | 35 +++ bots/bots_clan/amadi.json | 35 +++ bots/bots_clan/amu.json | 35 +++ bots/bots_clan/anodized.json | 35 +++ bots/bots_clan/ant__dog.json | 35 +++ bots/bots_clan/apem.json | 35 +++ bots/bots_clan/applee.json | 35 +++ bots/bots_clan/ashrah.json | 35 +++ bots/bots_clan/baraka.json | 35 +++ bots/bots_clan/bean.json | 35 +++ bots/bots_clan/belokk.json | 35 +++ bots/bots_clan/benarcher.json | 35 +++ bots/bots_clan/berserker.json | 35 +++ bots/bots_clan/bitterman.json | 35 +++ bots/bots_clan/blackadder.json | 35 +++ bots/bots_clan/blythe.json | 35 +++ bots/bots_clan/bobross.json | 35 +++ bots/bots_clan/bones.json | 35 +++ bots/bots_clan/booker.json | 35 +++ bots/bots_clan/boy.json | 35 +++ bots/bots_clan/brad.json | 35 +++ bots/bots_clan/bullseye.json | 35 +++ bots/bots_clan/bve.json | 35 +++ bots/bots_clan/cerberon.json | 35 +++ bots/bots_clan/chameleon.json | 35 +++ bots/bots_clan/chan.json | 35 +++ bots/bots_clan/clion.json | 35 +++ bots/bots_clan/coldcrow.json | 35 +++ bots/bots_clan/conan.json | 35 +++ bots/bots_clan/crash.json | 35 +++ bots/bots_clan/cybr.json | 35 +++ bots/bots_clan/dacfarren.json | 35 +++ bots/bots_clan/deusvault.json | 35 +++ bots/bots_clan/digz.json | 35 +++ bots/bots_clan/doc.json | 35 +++ bots/bots_clan/dominator.json | 35 +++ bots/bots_clan/doom.json | 35 +++ bots/bots_clan/dragoooon.json | 35 +++ bots/bots_clan/drahmin.json | 35 +++ bots/bots_clan/drillbit.json | 35 +++ bots/bots_clan/drjay.json | 35 +++ bots/bots_clan/dvorah.json | 35 +++ bots/bots_clan/edwig.json | 35 +++ bots/bots_clan/endugu.json | 35 +++ bots/bots_clan/ermac.json | 35 +++ bots/bots_clan/exbee.json | 35 +++ bots/bots_clan/eyeback.json | 35 +++ bots/bots_clan/faden.json | 35 +++ bots/bots_clan/fangskin.json | 35 +++ bots/bots_clan/fexel.json | 35 +++ bots/bots_clan/figgz.json | 35 +++ bots/bots_clan/flamespike.json | 35 +++ bots/bots_clan/flint.json | 35 +++ bots/bots_clan/frost.json | 35 +++ bots/bots_clan/fullmonty.json | 35 +++ bots/bots_clan/funkanik.json | 35 +++ bots/bots_clan/gali.json | 35 +++ bots/bots_clan/geras.json | 35 +++ bots/bots_clan/gladiator.json | 35 +++ bots/bots_clan/gogman.json | 35 +++ bots/bots_clan/gorre.json | 35 +++ bots/bots_clan/grezzer.json | 35 +++ bots/bots_clan/griswold.json | 35 +++ bots/bots_clan/grunt.json | 35 +++ bots/bots_clan/gun.json | 35 +++ bots/bots_clan/gunner.json | 35 +++ bots/bots_clan/hammerhead.json | 35 +++ bots/bots_clan/havo.json | 35 +++ bots/bots_clan/helmut.json | 35 +++ bots/bots_clan/himdar.json | 35 +++ bots/bots_clan/hitnrun.json | 35 +++ bots/bots_clan/hossman.json | 35 +++ bots/bots_clan/hotaru.json | 35 +++ bots/bots_clan/hsuhao.json | 35 +++ bots/bots_clan/hunter.json | 35 +++ bots/bots_clan/hurricane.json | 35 +++ bots/bots_clan/icarus.json | 35 +++ bots/bots_clan/icehawk.json | 35 +++ bots/bots_clan/icexo.json | 35 +++ bots/bots_clan/ismail.json | 35 +++ bots/bots_clan/itzchamp.json | 35 +++ bots/bots_clan/ivor.json | 35 +++ bots/bots_clan/jacksaxon.json | 35 +++ bots/bots_clan/jade.json | 35 +++ bots/bots_clan/jarno.json | 35 +++ bots/bots_clan/jarvis.json | 35 +++ bots/bots_clan/jazz.json | 35 +++ bots/bots_clan/jce.json | 35 +++ bots/bots_clan/joesanto.json | 35 +++ bots/bots_clan/joetanto.json | 35 +++ bots/bots_clan/johnluger.json | 35 +++ bots/bots_clan/kalidor.json | 35 +++ bots/bots_clan/kaolin.json | 35 +++ bots/bots_clan/keel.json | 35 +++ bots/bots_clan/kintaro.json | 35 +++ bots/bots_clan/kitana.json | 35 +++ bots/bots_clan/klesk.json | 35 +++ bots/bots_clan/kollector.json | 35 +++ bots/bots_clan/kor.json | 35 +++ bots/bots_clan/kronika.json | 35 +++ bots/bots_clan/kunglao.json | 35 +++ bots/bots_clan/kuri.json | 35 +++ bots/bots_clan/lawrence.json | 35 +++ bots/bots_clan/leiruth.json | 35 +++ bots/bots_clan/limei.json | 35 +++ bots/bots_clan/liukang.json | 35 +++ bots/bots_clan/lonewolf.json | 35 +++ bots/bots_clan/maffer.json | 35 +++ bots/bots_clan/malin.json | 35 +++ bots/bots_clan/manny.json | 35 +++ bots/bots_clan/marku.json | 35 +++ bots/bots_clan/mavado.json | 35 +++ bots/bots_clan/meat.json | 35 +++ bots/bots_clan/medic.json | 35 +++ bots/bots_clan/mee2.json | 35 +++ bots/bots_clan/mindelos.json | 35 +++ bots/bots_clan/missnrun.json | 35 +++ bots/bots_clan/mokap.json | 35 +++ bots/bots_clan/monk.json | 35 +++ bots/bots_clan/monty.json | 35 +++ bots/bots_clan/morgan.json | 35 +++ bots/bots_clan/mrbadger.json | 35 +++ bots/bots_clan/mrgoodkat.json | 35 +++ bots/bots_clan/mungri.json | 35 +++ bots/bots_clan/muscleman.json | 35 +++ bots/bots_clan/mynx.json | 35 +++ bots/bots_clan/nandet.json | 35 +++ bots/bots_clan/nightwolf.json | 35 +++ bots/bots_clan/nin.json | 35 +++ bots/bots_clan/nitara.json | 35 +++ bots/bots_clan/noogie.json | 35 +++ bots/bots_clan/orbb.json | 35 +++ bots/bots_clan/orinboyd.json | 35 +++ bots/bots_clan/ostmor.json | 35 +++ bots/bots_clan/paradox.json | 35 +++ bots/bots_clan/patriot.json | 35 +++ bots/bots_clan/pewdiepie.json | 35 +++ bots/bots_clan/potaytoe.json | 35 +++ bots/bots_clan/pyroyetti.json | 35 +++ bots/bots_clan/radament.json | 35 +++ bots/bots_clan/rakanishu.json | 35 +++ bots/bots_clan/ramrod.json | 35 +++ bots/bots_clan/ratbot.json | 35 +++ bots/bots_clan/reiko.json | 35 +++ bots/bots_clan/replicant.json | 35 +++ bots/bots_clan/ruslan.json | 35 +++ bots/bots_clan/sagel.json | 35 +++ bots/bots_clan/sarge.json | 35 +++ bots/bots_clan/sarina.json | 35 +++ bots/bots_clan/scorpion.json | 35 +++ bots/bots_clan/sean.json | 35 +++ bots/bots_clan/seankane.json | 35 +++ bots/bots_clan/sektor.json | 35 +++ bots/bots_clan/sheeva.json | 35 +++ bots/bots_clan/shinnok.json | 35 +++ bots/bots_clan/si.json | 35 +++ bots/bots_clan/sindel.json | 35 +++ bots/bots_clan/slash.json | 35 +++ bots/bots_clan/sorlag.json | 35 +++ bots/bots_clan/specialed.json | 35 +++ bots/bots_clan/spocket.json | 35 +++ bots/bots_clan/steampug.json | 35 +++ bots/bots_clan/stinger.json | 35 +++ bots/bots_clan/stormtree.json | 35 +++ bots/bots_clan/strogg.json | 35 +++ bots/bots_clan/strom.json | 35 +++ bots/bots_clan/subzero.json | 35 +++ bots/bots_clan/summoner.json | 35 +++ bots/bots_clan/sundog.json | 35 +++ bots/bots_clan/t1000.json | 35 +++ bots/bots_clan/t800.json | 35 +++ bots/bots_clan/t_bon.json | 35 +++ bots/bots_clan/tahlkora.json | 35 +++ bots/bots_clan/tank.json | 35 +++ bots/bots_clan/tanya.json | 35 +++ bots/bots_clan/taven.json | 35 +++ bots/bots_clan/teabag.json | 35 +++ bots/bots_clan/theboss.json | 35 +++ bots/bots_clan/thor.json | 35 +++ bots/bots_clan/threash.json | 35 +++ bots/bots_clan/tommix.json | 35 +++ bots/bots_clan/tomsteele.json | 35 +++ bots/bots_clan/toorc.json | 35 +++ bots/bots_clan/treehead.json | 35 +++ bots/bots_clan/tremor.json | 35 +++ bots/bots_clan/trench.json | 35 +++ bots/bots_clan/uriel.json | 35 +++ bots/bots_clan/uzaytron.json | 35 +++ bots/bots_clan/vedro.json | 35 +++ bots/bots_clan/vera.json | 35 +++ bots/bots_clan/visor.json | 35 +++ bots/bots_clan/walter.json | 35 +++ bots/bots_clan/warchild.json | 35 +++ bots/bots_clan/wens.json | 35 +++ bots/bots_clan/willitz.json | 35 +++ bots/bots_clan/wrack.json | 35 +++ bots/bots_clan/xaero.json | 35 +++ bots/bots_clan/zarna.json | 35 +++ bots/bots_pub/_nixxy_.json | 35 +++ bots/bots_pub/aimless.json | 35 +++ bots/bots_pub/anakinskywalker.json | 2 +- bots/bots_pub/anarki.json | 35 +++ bots/bots_pub/andromeed.json | 35 +++ bots/bots_pub/angel.json | 35 +++ bots/bots_pub/angeloprovolone.json | 2 +- bots/bots_pub/anton.json | 35 +++ bots/bots_pub/argo.json | 35 +++ bots/bots_pub/arklon.json | 35 +++ bots/bots_pub/backfire.json | 35 +++ bots/bots_pub/bacon.json | 35 +++ bots/bots_pub/badjuju.json | 35 +++ bots/bots_pub/badsam.json | 35 +++ bots/bots_pub/barbarian.json | 35 +++ bots/bots_pub/beelz.json | 35 +++ bots/bots_pub/bigmack.json | 35 +++ bots/bots_pub/biker.json | 35 +++ bots/bots_pub/bishibosh.json | 35 +++ bots/bots_pub/blaze.json | 35 +++ bots/bots_pub/bloodwitch.json | 35 +++ bots/bots_pub/bolund.json | 35 +++ bots/bots_pub/boneash.json | 35 +++ bots/bots_pub/bonebreak.json | 35 +++ bots/bots_pub/bortack.json | 35 +++ bots/bots_pub/bremm.json | 35 +++ bots/bots_pub/brixos.json | 35 +++ bots/bots_pub/brofist.json | 35 +++ bots/bots_pub/brohn.json | 35 +++ bots/bots_pub/broz.json | 35 +++ bots/bots_pub/cadaver.json | 35 +++ bots/bots_pub/captivandanko.json | 2 +- bots/bots_pub/carpenter.json | 35 +++ bots/bots_pub/cetrion.json | 35 +++ bots/bots_pub/chanceboudreaux.json | 2 +- bots/bots_pub/chess.json | 35 +++ bots/bots_pub/chipp_.json | 35 +++ bots/bots_pub/choppy.json | 35 +++ bots/bots_pub/church.json | 35 +++ bots/bots_pub/cledus.json | 35 +++ bots/bots_pub/cobra.json | 35 +++ bots/bots_pub/coldworm.json | 35 +++ bots/bots_pub/colt.json | 35 +++ bots/bots_pub/corpsefire.json | 35 +++ bots/bots_pub/corrus.json | 35 +++ bots/bots_pub/countess.json | 35 +++ bots/bots_pub/cowchop.json | 35 +++ bots/bots_pub/cowking.json | 35 +++ bots/bots_pub/cruton.json | 35 +++ bots/bots_pub/cyrax.json | 35 +++ bots/bots_pub/daegon.json | 35 +++ bots/bots_pub/daemia.json | 35 +++ bots/bots_pub/dairou.json | 35 +++ bots/bots_pub/darkkahn.json | 35 +++ bots/bots_pub/darrius.json | 35 +++ bots/bots_pub/death.json | 35 +++ bots/bots_pub/dharmesh.json | 35 +++ bots/bots_pub/dinardo.json | 35 +++ bots/bots_pub/disfordigger.json | 2 +- bots/bots_pub/doctordeathwish.json | 2 +- bots/bots_pub/dolin.json | 35 +++ bots/bots_pub/drago.json | 35 +++ bots/bots_pub/dutch.json | 35 +++ bots/bots_pub/edoli.json | 35 +++ bots/bots_pub/ellanil.json | 35 +++ bots/bots_pub/enforcer.json | 35 +++ bots/bots_pub/evo89.json | 35 +++ bots/bots_pub/ferratorr.json | 35 +++ bots/bots_pub/frankdux.json | 35 +++ bots/bots_pub/fred.json | 35 +++ bots/bots_pub/frenchy.json | 35 +++ bots/bots_pub/fujin.json | 35 +++ bots/bots_pub/fxguy.json | 35 +++ bots/bots_pub/galul.json | 35 +++ bots/bots_pub/garbok.json | 35 +++ bots/bots_pub/geleb.json | 35 +++ bots/bots_pub/george.json | 35 +++ bots/bots_pub/ghostbl4de.json | 35 +++ bots/bots_pub/gigavoltz.json | 35 +++ bots/bots_pub/gordac.json | 35 +++ bots/bots_pub/grimm.json | 35 +++ bots/bots_pub/grog.json | 35 +++ bots/bots_pub/gungho.json | 35 +++ bots/bots_pub/hal9000.json | 35 +++ bots/bots_pub/halfbaked.json | 35 +++ bots/bots_pub/halfwit.json | 35 +++ bots/bots_pub/hambone.json | 35 +++ bots/bots_pub/havik.json | 35 +++ bots/bots_pub/hecker.json | 35 +++ bots/bots_pub/heljay.json | 35 +++ bots/bots_pub/hellhound.json | 35 +++ bots/bots_pub/himself.json | 35 +++ bots/bots_pub/hozef8.json | 35 +++ bots/bots_pub/imentu.json | 35 +++ bots/bots_pub/itixoid.json | 35 +++ bots/bots_pub/ivandrago.json | 35 +++ bots/bots_pub/ivankraschinsky.json | 2 +- bots/bots_pub/jacqueskristoff.json | 2 +- bots/bots_pub/jarek.json | 35 +++ bots/bots_pub/jax.json | 35 +++ bots/bots_pub/jcdenton.json | 35 +++ bots/bots_pub/jdgold.json | 35 +++ bots/bots_pub/jinx.json | 35 +++ bots/bots_pub/jjmcquade.json | 35 +++ bots/bots_pub/joce.json | 35 +++ bots/bots_pub/joejoiner.json | 35 +++ bots/bots_pub/johnrambo.json | 35 +++ bots/bots_pub/kaa.json | 35 +++ bots/bots_pub/kabal.json | 35 +++ bots/bots_pub/kai.json | 35 +++ bots/bots_pub/kano.json | 35 +++ bots/bots_pub/kenshi.json | 35 +++ bots/bots_pub/khameleon.json | 35 +++ bots/bots_pub/kippy.json | 35 +++ bots/bots_pub/kira.json | 35 +++ bots/bots_pub/kitlatura.json | 35 +++ bots/bots_pub/kobra.json | 35 +++ bots/bots_pub/kole.json | 35 +++ bots/bots_pub/korv.json | 35 +++ bots/bots_pub/kotalkahn.json | 35 +++ bots/bots_pub/kungjin.json | 35 +++ bots/bots_pub/leatherarm.json | 35 +++ bots/bots_pub/lemmie.json | 35 +++ bots/bots_pub/littleg.json | 35 +++ bots/bots_pub/ltraymondtango.json | 2 +- bots/bots_pub/lucy.json | 35 +++ bots/bots_pub/major.json | 35 +++ bots/bots_pub/majorpain.json | 35 +++ bots/bots_pub/makron.json | 35 +++ bots/bots_pub/mckenna.json | 35 +++ bots/bots_pub/merki.json | 35 +++ bots/bots_pub/michael.json | 35 +++ bots/bots_pub/mileena.json | 35 +++ bots/bots_pub/milius.json | 35 +++ bots/bots_pub/mincer.json | 35 +++ bots/bots_pub/moloch.json | 35 +++ bots/bots_pub/mongrel.json | 35 +++ bots/bots_pub/morgriff.json | 35 +++ bots/bots_pub/motaro.json | 35 +++ bots/bots_pub/mrsuave.json | 35 +++ bots/bots_pub/newbie.json | 35 +++ bots/bots_pub/nibbler.json | 35 +++ bots/bots_pub/nickgunar.json | 35 +++ bots/bots_pub/nihlathak.json | 35 +++ bots/bots_pub/nikolaicherenko.json | 2 +- bots/bots_pub/noammo.json | 35 +++ bots/bots_pub/nohope.json | 35 +++ bots/bots_pub/nundak.json | 35 +++ bots/bots_pub/onaga.json | 35 +++ bots/bots_pub/oysterhead.json | 35 +++ bots/bots_pub/phobos.json | 35 +++ bots/bots_pub/pindleskin.json | 35 +++ bots/bots_pub/pitspawnfouldog.json | 2 +- bots/bots_pub/quanchi.json | 35 +++ bots/bots_pub/raiden.json | 35 +++ bots/bots_pub/rain.json | 35 +++ bots/bots_pub/ranger.json | 35 +++ bots/bots_pub/rayquick.json | 35 +++ bots/bots_pub/razor.json | 35 +++ bots/bots_pub/reddog.json | 35 +++ bots/bots_pub/reptile.json | 35 +++ bots/bots_pub/richard.json | 35 +++ bots/bots_pub/riftwraith.json | 35 +++ bots/bots_pub/rocky.json | 35 +++ bots/bots_pub/royen.json | 35 +++ bots/bots_pub/ryder.json | 35 +++ bots/bots_pub/ryver.json | 35 +++ bots/bots_pub/saboy.json | 35 +++ bots/bots_pub/samdecker.json | 35 +++ bots/bots_pub/sareena.json | 35 +++ bots/bots_pub/sgtcappa.json | 35 +++ bots/bots_pub/shaokahn.json | 35 +++ bots/bots_pub/shujinko.json | 35 +++ bots/bots_pub/siege.json | 35 +++ bots/bots_pub/sigil.json | 35 +++ bots/bots_pub/skarlet.json | 35 +++ bots/bots_pub/skorgan.json | 35 +++ bots/bots_pub/slice.json | 35 +++ bots/bots_pub/smithy.json | 35 +++ bots/bots_pub/smoke.json | 35 +++ bots/bots_pub/snapchipshatter.json | 2 +- bots/bots_pub/snaps.json | 35 +++ bots/bots_pub/socks.json | 35 +++ bots/bots_pub/spar.json | 35 +++ bots/bots_pub/stripe.json | 35 +++ bots/bots_pub/stryker.json | 35 +++ bots/bots_pub/stud.json | 35 +++ bots/bots_pub/takedatakahashi.json | 2 +- bots/bots_pub/tankjr.json | 35 +++ bots/bots_pub/tanz.json | 35 +++ bots/bots_pub/tao.json | 35 +++ bots/bots_pub/tarnen.json | 35 +++ bots/bots_pub/tarnok.json | 35 +++ bots/bots_pub/tatu.json | 35 +++ bots/bots_pub/technician.json | 35 +++ bots/bots_pub/terminator.json | 35 +++ bots/bots_pub/thalath.json | 35 +++ bots/bots_pub/thedean.json | 35 +++ bots/bots_pub/theeraser.json | 35 +++ bots/bots_pub/thegerman.json | 35 +++ bots/bots_pub/theonly.json | 35 +++ bots/bots_pub/thetulip.json | 35 +++ bots/bots_pub/theway.json | 35 +++ bots/bots_pub/tiano.json | 35 +++ bots/bots_pub/tormentor.json | 35 +++ bots/bots_pub/triborg.json | 35 +++ bots/bots_pub/turkish.json | 35 +++ bots/bots_pub/vanoss.json | 35 +++ bots/bots_pub/venom.json | 35 +++ bots/bots_pub/venz.json | 35 +++ bots/bots_pub/vey.json | 35 +++ bots/bots_pub/vic.json | 35 +++ bots/bots_pub/vizier.json | 35 +++ bots/bots_pub/vokur.json | 35 +++ bots/bots_pub/waxman.json | 35 +++ bots/bots_pub/wayofthe.json | 35 +++ bots/bots_pub/whyarewehere.json | 2 +- bots/bots_pub/whyme.json | 35 +++ bots/bots_pub/wreckage.json | 35 +++ bots/bots_pub/wyand.json | 35 +++ bots/bots_pub/wysyyy.json | 35 +++ bots/bots_pub/yarbro.json | 35 +++ bots/clans/FZe.json | 2 +- bots/index.txt | 428 ++++++++++++++++++++++++++++- 427 files changed, 14791 insertions(+), 17 deletions(-) create mode 100644 bots/bots_clan/2dcitizen.json create mode 100644 bots/bots_clan/_gholem_.json create mode 100644 bots/bots_clan/_glich.json create mode 100644 bots/bots_clan/_pixxy_.json create mode 100644 bots/bots_clan/aegarond.json create mode 100644 bots/bots_clan/amadi.json create mode 100644 bots/bots_clan/amu.json create mode 100644 bots/bots_clan/anodized.json create mode 100644 bots/bots_clan/ant__dog.json create mode 100644 bots/bots_clan/apem.json create mode 100644 bots/bots_clan/applee.json create mode 100644 bots/bots_clan/ashrah.json create mode 100644 bots/bots_clan/baraka.json create mode 100644 bots/bots_clan/bean.json create mode 100644 bots/bots_clan/belokk.json create mode 100644 bots/bots_clan/benarcher.json create mode 100644 bots/bots_clan/berserker.json create mode 100644 bots/bots_clan/bitterman.json create mode 100644 bots/bots_clan/blackadder.json create mode 100644 bots/bots_clan/blythe.json create mode 100644 bots/bots_clan/bobross.json create mode 100644 bots/bots_clan/bones.json create mode 100644 bots/bots_clan/booker.json create mode 100644 bots/bots_clan/boy.json create mode 100644 bots/bots_clan/brad.json create mode 100644 bots/bots_clan/bullseye.json create mode 100644 bots/bots_clan/bve.json create mode 100644 bots/bots_clan/cerberon.json create mode 100644 bots/bots_clan/chameleon.json create mode 100644 bots/bots_clan/chan.json create mode 100644 bots/bots_clan/clion.json create mode 100644 bots/bots_clan/coldcrow.json create mode 100644 bots/bots_clan/conan.json create mode 100644 bots/bots_clan/crash.json create mode 100644 bots/bots_clan/cybr.json create mode 100644 bots/bots_clan/dacfarren.json create mode 100644 bots/bots_clan/deusvault.json create mode 100644 bots/bots_clan/digz.json create mode 100644 bots/bots_clan/doc.json create mode 100644 bots/bots_clan/dominator.json create mode 100644 bots/bots_clan/doom.json create mode 100644 bots/bots_clan/dragoooon.json create mode 100644 bots/bots_clan/drahmin.json create mode 100644 bots/bots_clan/drillbit.json create mode 100644 bots/bots_clan/drjay.json create mode 100644 bots/bots_clan/dvorah.json create mode 100644 bots/bots_clan/edwig.json create mode 100644 bots/bots_clan/endugu.json create mode 100644 bots/bots_clan/ermac.json create mode 100644 bots/bots_clan/exbee.json create mode 100644 bots/bots_clan/eyeback.json create mode 100644 bots/bots_clan/faden.json create mode 100644 bots/bots_clan/fangskin.json create mode 100644 bots/bots_clan/fexel.json create mode 100644 bots/bots_clan/figgz.json create mode 100644 bots/bots_clan/flamespike.json create mode 100644 bots/bots_clan/flint.json create mode 100644 bots/bots_clan/frost.json create mode 100644 bots/bots_clan/fullmonty.json create mode 100644 bots/bots_clan/funkanik.json create mode 100644 bots/bots_clan/gali.json create mode 100644 bots/bots_clan/geras.json create mode 100644 bots/bots_clan/gladiator.json create mode 100644 bots/bots_clan/gogman.json create mode 100644 bots/bots_clan/gorre.json create mode 100644 bots/bots_clan/grezzer.json create mode 100644 bots/bots_clan/griswold.json create mode 100644 bots/bots_clan/grunt.json create mode 100644 bots/bots_clan/gun.json create mode 100644 bots/bots_clan/gunner.json create mode 100644 bots/bots_clan/hammerhead.json create mode 100644 bots/bots_clan/havo.json create mode 100644 bots/bots_clan/helmut.json create mode 100644 bots/bots_clan/himdar.json create mode 100644 bots/bots_clan/hitnrun.json create mode 100644 bots/bots_clan/hossman.json create mode 100644 bots/bots_clan/hotaru.json create mode 100644 bots/bots_clan/hsuhao.json create mode 100644 bots/bots_clan/hunter.json create mode 100644 bots/bots_clan/hurricane.json create mode 100644 bots/bots_clan/icarus.json create mode 100644 bots/bots_clan/icehawk.json create mode 100644 bots/bots_clan/icexo.json create mode 100644 bots/bots_clan/ismail.json create mode 100644 bots/bots_clan/itzchamp.json create mode 100644 bots/bots_clan/ivor.json create mode 100644 bots/bots_clan/jacksaxon.json create mode 100644 bots/bots_clan/jade.json create mode 100644 bots/bots_clan/jarno.json create mode 100644 bots/bots_clan/jarvis.json create mode 100644 bots/bots_clan/jazz.json create mode 100644 bots/bots_clan/jce.json create mode 100644 bots/bots_clan/joesanto.json create mode 100644 bots/bots_clan/joetanto.json create mode 100644 bots/bots_clan/johnluger.json create mode 100644 bots/bots_clan/kalidor.json create mode 100644 bots/bots_clan/kaolin.json create mode 100644 bots/bots_clan/keel.json create mode 100644 bots/bots_clan/kintaro.json create mode 100644 bots/bots_clan/kitana.json create mode 100644 bots/bots_clan/klesk.json create mode 100644 bots/bots_clan/kollector.json create mode 100644 bots/bots_clan/kor.json create mode 100644 bots/bots_clan/kronika.json create mode 100644 bots/bots_clan/kunglao.json create mode 100644 bots/bots_clan/kuri.json create mode 100644 bots/bots_clan/lawrence.json create mode 100644 bots/bots_clan/leiruth.json create mode 100644 bots/bots_clan/limei.json create mode 100644 bots/bots_clan/liukang.json create mode 100644 bots/bots_clan/lonewolf.json create mode 100644 bots/bots_clan/maffer.json create mode 100644 bots/bots_clan/malin.json create mode 100644 bots/bots_clan/manny.json create mode 100644 bots/bots_clan/marku.json create mode 100644 bots/bots_clan/mavado.json create mode 100644 bots/bots_clan/meat.json create mode 100644 bots/bots_clan/medic.json create mode 100644 bots/bots_clan/mee2.json create mode 100644 bots/bots_clan/mindelos.json create mode 100644 bots/bots_clan/missnrun.json create mode 100644 bots/bots_clan/mokap.json create mode 100644 bots/bots_clan/monk.json create mode 100644 bots/bots_clan/monty.json create mode 100644 bots/bots_clan/morgan.json create mode 100644 bots/bots_clan/mrbadger.json create mode 100644 bots/bots_clan/mrgoodkat.json create mode 100644 bots/bots_clan/mungri.json create mode 100644 bots/bots_clan/muscleman.json create mode 100644 bots/bots_clan/mynx.json create mode 100644 bots/bots_clan/nandet.json create mode 100644 bots/bots_clan/nightwolf.json create mode 100644 bots/bots_clan/nin.json create mode 100644 bots/bots_clan/nitara.json create mode 100644 bots/bots_clan/noogie.json create mode 100644 bots/bots_clan/orbb.json create mode 100644 bots/bots_clan/orinboyd.json create mode 100644 bots/bots_clan/ostmor.json create mode 100644 bots/bots_clan/paradox.json create mode 100644 bots/bots_clan/patriot.json create mode 100644 bots/bots_clan/pewdiepie.json create mode 100644 bots/bots_clan/potaytoe.json create mode 100644 bots/bots_clan/pyroyetti.json create mode 100644 bots/bots_clan/radament.json create mode 100644 bots/bots_clan/rakanishu.json create mode 100644 bots/bots_clan/ramrod.json create mode 100644 bots/bots_clan/ratbot.json create mode 100644 bots/bots_clan/reiko.json create mode 100644 bots/bots_clan/replicant.json create mode 100644 bots/bots_clan/ruslan.json create mode 100644 bots/bots_clan/sagel.json create mode 100644 bots/bots_clan/sarge.json create mode 100644 bots/bots_clan/sarina.json create mode 100644 bots/bots_clan/scorpion.json create mode 100644 bots/bots_clan/sean.json create mode 100644 bots/bots_clan/seankane.json create mode 100644 bots/bots_clan/sektor.json create mode 100644 bots/bots_clan/sheeva.json create mode 100644 bots/bots_clan/shinnok.json create mode 100644 bots/bots_clan/si.json create mode 100644 bots/bots_clan/sindel.json create mode 100644 bots/bots_clan/slash.json create mode 100644 bots/bots_clan/sorlag.json create mode 100644 bots/bots_clan/specialed.json create mode 100644 bots/bots_clan/spocket.json create mode 100644 bots/bots_clan/steampug.json create mode 100644 bots/bots_clan/stinger.json create mode 100644 bots/bots_clan/stormtree.json create mode 100644 bots/bots_clan/strogg.json create mode 100644 bots/bots_clan/strom.json create mode 100644 bots/bots_clan/subzero.json create mode 100644 bots/bots_clan/summoner.json create mode 100644 bots/bots_clan/sundog.json create mode 100644 bots/bots_clan/t1000.json create mode 100644 bots/bots_clan/t800.json create mode 100644 bots/bots_clan/t_bon.json create mode 100644 bots/bots_clan/tahlkora.json create mode 100644 bots/bots_clan/tank.json create mode 100644 bots/bots_clan/tanya.json create mode 100644 bots/bots_clan/taven.json create mode 100644 bots/bots_clan/teabag.json create mode 100644 bots/bots_clan/theboss.json create mode 100644 bots/bots_clan/thor.json create mode 100644 bots/bots_clan/threash.json create mode 100644 bots/bots_clan/tommix.json create mode 100644 bots/bots_clan/tomsteele.json create mode 100644 bots/bots_clan/toorc.json create mode 100644 bots/bots_clan/treehead.json create mode 100644 bots/bots_clan/tremor.json create mode 100644 bots/bots_clan/trench.json create mode 100644 bots/bots_clan/uriel.json create mode 100644 bots/bots_clan/uzaytron.json create mode 100644 bots/bots_clan/vedro.json create mode 100644 bots/bots_clan/vera.json create mode 100644 bots/bots_clan/visor.json create mode 100644 bots/bots_clan/walter.json create mode 100644 bots/bots_clan/warchild.json create mode 100644 bots/bots_clan/wens.json create mode 100644 bots/bots_clan/willitz.json create mode 100644 bots/bots_clan/wrack.json create mode 100644 bots/bots_clan/xaero.json create mode 100644 bots/bots_clan/zarna.json create mode 100644 bots/bots_pub/_nixxy_.json create mode 100644 bots/bots_pub/aimless.json create mode 100644 bots/bots_pub/anarki.json create mode 100644 bots/bots_pub/andromeed.json create mode 100644 bots/bots_pub/angel.json create mode 100644 bots/bots_pub/anton.json create mode 100644 bots/bots_pub/argo.json create mode 100644 bots/bots_pub/arklon.json create mode 100644 bots/bots_pub/backfire.json create mode 100644 bots/bots_pub/bacon.json create mode 100644 bots/bots_pub/badjuju.json create mode 100644 bots/bots_pub/badsam.json create mode 100644 bots/bots_pub/barbarian.json create mode 100644 bots/bots_pub/beelz.json create mode 100644 bots/bots_pub/bigmack.json create mode 100644 bots/bots_pub/biker.json create mode 100644 bots/bots_pub/bishibosh.json create mode 100644 bots/bots_pub/blaze.json create mode 100644 bots/bots_pub/bloodwitch.json create mode 100644 bots/bots_pub/bolund.json create mode 100644 bots/bots_pub/boneash.json create mode 100644 bots/bots_pub/bonebreak.json create mode 100644 bots/bots_pub/bortack.json create mode 100644 bots/bots_pub/bremm.json create mode 100644 bots/bots_pub/brixos.json create mode 100644 bots/bots_pub/brofist.json create mode 100644 bots/bots_pub/brohn.json create mode 100644 bots/bots_pub/broz.json create mode 100644 bots/bots_pub/cadaver.json create mode 100644 bots/bots_pub/carpenter.json create mode 100644 bots/bots_pub/cetrion.json create mode 100644 bots/bots_pub/chess.json create mode 100644 bots/bots_pub/chipp_.json create mode 100644 bots/bots_pub/choppy.json create mode 100644 bots/bots_pub/church.json create mode 100644 bots/bots_pub/cledus.json create mode 100644 bots/bots_pub/cobra.json create mode 100644 bots/bots_pub/coldworm.json create mode 100644 bots/bots_pub/colt.json create mode 100644 bots/bots_pub/corpsefire.json create mode 100644 bots/bots_pub/corrus.json create mode 100644 bots/bots_pub/countess.json create mode 100644 bots/bots_pub/cowchop.json create mode 100644 bots/bots_pub/cowking.json create mode 100644 bots/bots_pub/cruton.json create mode 100644 bots/bots_pub/cyrax.json create mode 100644 bots/bots_pub/daegon.json create mode 100644 bots/bots_pub/daemia.json create mode 100644 bots/bots_pub/dairou.json create mode 100644 bots/bots_pub/darkkahn.json create mode 100644 bots/bots_pub/darrius.json create mode 100644 bots/bots_pub/death.json create mode 100644 bots/bots_pub/dharmesh.json create mode 100644 bots/bots_pub/dinardo.json create mode 100644 bots/bots_pub/dolin.json create mode 100644 bots/bots_pub/drago.json create mode 100644 bots/bots_pub/dutch.json create mode 100644 bots/bots_pub/edoli.json create mode 100644 bots/bots_pub/ellanil.json create mode 100644 bots/bots_pub/enforcer.json create mode 100644 bots/bots_pub/evo89.json create mode 100644 bots/bots_pub/ferratorr.json create mode 100644 bots/bots_pub/frankdux.json create mode 100644 bots/bots_pub/fred.json create mode 100644 bots/bots_pub/frenchy.json create mode 100644 bots/bots_pub/fujin.json create mode 100644 bots/bots_pub/fxguy.json create mode 100644 bots/bots_pub/galul.json create mode 100644 bots/bots_pub/garbok.json create mode 100644 bots/bots_pub/geleb.json create mode 100644 bots/bots_pub/george.json create mode 100644 bots/bots_pub/ghostbl4de.json create mode 100644 bots/bots_pub/gigavoltz.json create mode 100644 bots/bots_pub/gordac.json create mode 100644 bots/bots_pub/grimm.json create mode 100644 bots/bots_pub/grog.json create mode 100644 bots/bots_pub/gungho.json create mode 100644 bots/bots_pub/hal9000.json create mode 100644 bots/bots_pub/halfbaked.json create mode 100644 bots/bots_pub/halfwit.json create mode 100644 bots/bots_pub/hambone.json create mode 100644 bots/bots_pub/havik.json create mode 100644 bots/bots_pub/hecker.json create mode 100644 bots/bots_pub/heljay.json create mode 100644 bots/bots_pub/hellhound.json create mode 100644 bots/bots_pub/himself.json create mode 100644 bots/bots_pub/hozef8.json create mode 100644 bots/bots_pub/imentu.json create mode 100644 bots/bots_pub/itixoid.json create mode 100644 bots/bots_pub/ivandrago.json create mode 100644 bots/bots_pub/jarek.json create mode 100644 bots/bots_pub/jax.json create mode 100644 bots/bots_pub/jcdenton.json create mode 100644 bots/bots_pub/jdgold.json create mode 100644 bots/bots_pub/jinx.json create mode 100644 bots/bots_pub/jjmcquade.json create mode 100644 bots/bots_pub/joce.json create mode 100644 bots/bots_pub/joejoiner.json create mode 100644 bots/bots_pub/johnrambo.json create mode 100644 bots/bots_pub/kaa.json create mode 100644 bots/bots_pub/kabal.json create mode 100644 bots/bots_pub/kai.json create mode 100644 bots/bots_pub/kano.json create mode 100644 bots/bots_pub/kenshi.json create mode 100644 bots/bots_pub/khameleon.json create mode 100644 bots/bots_pub/kippy.json create mode 100644 bots/bots_pub/kira.json create mode 100644 bots/bots_pub/kitlatura.json create mode 100644 bots/bots_pub/kobra.json create mode 100644 bots/bots_pub/kole.json create mode 100644 bots/bots_pub/korv.json create mode 100644 bots/bots_pub/kotalkahn.json create mode 100644 bots/bots_pub/kungjin.json create mode 100644 bots/bots_pub/leatherarm.json create mode 100644 bots/bots_pub/lemmie.json create mode 100644 bots/bots_pub/littleg.json create mode 100644 bots/bots_pub/lucy.json create mode 100644 bots/bots_pub/major.json create mode 100644 bots/bots_pub/majorpain.json create mode 100644 bots/bots_pub/makron.json create mode 100644 bots/bots_pub/mckenna.json create mode 100644 bots/bots_pub/merki.json create mode 100644 bots/bots_pub/michael.json create mode 100644 bots/bots_pub/mileena.json create mode 100644 bots/bots_pub/milius.json create mode 100644 bots/bots_pub/mincer.json create mode 100644 bots/bots_pub/moloch.json create mode 100644 bots/bots_pub/mongrel.json create mode 100644 bots/bots_pub/morgriff.json create mode 100644 bots/bots_pub/motaro.json create mode 100644 bots/bots_pub/mrsuave.json create mode 100644 bots/bots_pub/newbie.json create mode 100644 bots/bots_pub/nibbler.json create mode 100644 bots/bots_pub/nickgunar.json create mode 100644 bots/bots_pub/nihlathak.json create mode 100644 bots/bots_pub/noammo.json create mode 100644 bots/bots_pub/nohope.json create mode 100644 bots/bots_pub/nundak.json create mode 100644 bots/bots_pub/onaga.json create mode 100644 bots/bots_pub/oysterhead.json create mode 100644 bots/bots_pub/phobos.json create mode 100644 bots/bots_pub/pindleskin.json create mode 100644 bots/bots_pub/quanchi.json create mode 100644 bots/bots_pub/raiden.json create mode 100644 bots/bots_pub/rain.json create mode 100644 bots/bots_pub/ranger.json create mode 100644 bots/bots_pub/rayquick.json create mode 100644 bots/bots_pub/razor.json create mode 100644 bots/bots_pub/reddog.json create mode 100644 bots/bots_pub/reptile.json create mode 100644 bots/bots_pub/richard.json create mode 100644 bots/bots_pub/riftwraith.json create mode 100644 bots/bots_pub/rocky.json create mode 100644 bots/bots_pub/royen.json create mode 100644 bots/bots_pub/ryder.json create mode 100644 bots/bots_pub/ryver.json create mode 100644 bots/bots_pub/saboy.json create mode 100644 bots/bots_pub/samdecker.json create mode 100644 bots/bots_pub/sareena.json create mode 100644 bots/bots_pub/sgtcappa.json create mode 100644 bots/bots_pub/shaokahn.json create mode 100644 bots/bots_pub/shujinko.json create mode 100644 bots/bots_pub/siege.json create mode 100644 bots/bots_pub/sigil.json create mode 100644 bots/bots_pub/skarlet.json create mode 100644 bots/bots_pub/skorgan.json create mode 100644 bots/bots_pub/slice.json create mode 100644 bots/bots_pub/smithy.json create mode 100644 bots/bots_pub/smoke.json create mode 100644 bots/bots_pub/snaps.json create mode 100644 bots/bots_pub/socks.json create mode 100644 bots/bots_pub/spar.json create mode 100644 bots/bots_pub/stripe.json create mode 100644 bots/bots_pub/stryker.json create mode 100644 bots/bots_pub/stud.json create mode 100644 bots/bots_pub/tankjr.json create mode 100644 bots/bots_pub/tanz.json create mode 100644 bots/bots_pub/tao.json create mode 100644 bots/bots_pub/tarnen.json create mode 100644 bots/bots_pub/tarnok.json create mode 100644 bots/bots_pub/tatu.json create mode 100644 bots/bots_pub/technician.json create mode 100644 bots/bots_pub/terminator.json create mode 100644 bots/bots_pub/thalath.json create mode 100644 bots/bots_pub/thedean.json create mode 100644 bots/bots_pub/theeraser.json create mode 100644 bots/bots_pub/thegerman.json create mode 100644 bots/bots_pub/theonly.json create mode 100644 bots/bots_pub/thetulip.json create mode 100644 bots/bots_pub/theway.json create mode 100644 bots/bots_pub/tiano.json create mode 100644 bots/bots_pub/tormentor.json create mode 100644 bots/bots_pub/triborg.json create mode 100644 bots/bots_pub/turkish.json create mode 100644 bots/bots_pub/vanoss.json create mode 100644 bots/bots_pub/venom.json create mode 100644 bots/bots_pub/venz.json create mode 100644 bots/bots_pub/vey.json create mode 100644 bots/bots_pub/vic.json create mode 100644 bots/bots_pub/vizier.json create mode 100644 bots/bots_pub/vokur.json create mode 100644 bots/bots_pub/waxman.json create mode 100644 bots/bots_pub/wayofthe.json create mode 100644 bots/bots_pub/whyme.json create mode 100644 bots/bots_pub/wreckage.json create mode 100644 bots/bots_pub/wyand.json create mode 100644 bots/bots_pub/wysyyy.json create mode 100644 bots/bots_pub/yarbro.json diff --git a/bots/bots.bin b/bots/bots.bin index ec9d3626ad38ef0d28c0f128d88aae6eb88509fc..cb934c6adf4aef52694d44d6e9771a304cd879e8 100644 GIT binary patch literal 123146 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 delta 16 XcmeA=#D0opvz3nq<96>`j7p3EHai7` diff --git a/bots/bots_clan/2dcitizen.json b/bots/bots_clan/2dcitizen.json new file mode 100644 index 000000000..59a2d51a0 --- /dev/null +++ b/bots/bots_clan/2dcitizen.json @@ -0,0 +1,35 @@ +{ + "name": "2DCitizen", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/_gholem_.json b/bots/bots_clan/_gholem_.json new file mode 100644 index 000000000..5e2aa8603 --- /dev/null +++ b/bots/bots_clan/_gholem_.json @@ -0,0 +1,35 @@ +{ + "name": "_Gholem_", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.0, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/_glich.json b/bots/bots_clan/_glich.json new file mode 100644 index 000000000..dd0ff32a4 --- /dev/null +++ b/bots/bots_clan/_glich.json @@ -0,0 +1,35 @@ +{ + "name": "_GLiCH", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.2, + "knives": -0.0, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/_pixxy_.json b/bots/bots_clan/_pixxy_.json new file mode 100644 index 000000000..02a918334 --- /dev/null +++ b/bots/bots_clan/_pixxy_.json @@ -0,0 +1,35 @@ +{ + "name": "_*PixxY*_", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.4, + "grenades": 1.0 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.0 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/aegarond.json b/bots/bots_clan/aegarond.json new file mode 100644 index 000000000..b8bdf54ac --- /dev/null +++ b/bots/bots_clan/aegarond.json @@ -0,0 +1,35 @@ +{ + "name": "Aegarond", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.1, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.9, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/amadi.json b/bots/bots_clan/amadi.json new file mode 100644 index 000000000..193e5bfaf --- /dev/null +++ b/bots/bots_clan/amadi.json @@ -0,0 +1,35 @@ +{ + "name": "Amadi", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.1, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.9, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.1, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/amu.json b/bots/bots_clan/amu.json new file mode 100644 index 000000000..a9aec1da8 --- /dev/null +++ b/bots/bots_clan/amu.json @@ -0,0 +1,35 @@ +{ + "name": "Amu", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/anodized.json b/bots/bots_clan/anodized.json new file mode 100644 index 000000000..99dd4179d --- /dev/null +++ b/bots/bots_clan/anodized.json @@ -0,0 +1,35 @@ +{ + "name": "Anodized", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ant__dog.json b/bots/bots_clan/ant__dog.json new file mode 100644 index 000000000..df23d51e9 --- /dev/null +++ b/bots/bots_clan/ant__dog.json @@ -0,0 +1,35 @@ +{ + "name": "Ant__Dog", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.5, + "hc": 0.9, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.9, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/apem.json b/bots/bots_clan/apem.json new file mode 100644 index 000000000..4c8bdaad7 --- /dev/null +++ b/bots/bots_clan/apem.json @@ -0,0 +1,35 @@ +{ + "name": "ApeM", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/applee.json b/bots/bots_clan/applee.json new file mode 100644 index 000000000..079636c00 --- /dev/null +++ b/bots/bots_clan/applee.json @@ -0,0 +1,35 @@ +{ + "name": "Applee", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.0, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.0, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ashrah.json b/bots/bots_clan/ashrah.json new file mode 100644 index 000000000..f2638b76f --- /dev/null +++ b/bots/bots_clan/ashrah.json @@ -0,0 +1,35 @@ +{ + "name": "Ashrah", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/baraka.json b/bots/bots_clan/baraka.json new file mode 100644 index 000000000..313975ca8 --- /dev/null +++ b/bots/bots_clan/baraka.json @@ -0,0 +1,35 @@ +{ + "name": "Baraka", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bean.json b/bots/bots_clan/bean.json new file mode 100644 index 000000000..4eaba2ace --- /dev/null +++ b/bots/bots_clan/bean.json @@ -0,0 +1,35 @@ +{ + "name": "Bean", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/belokk.json b/bots/bots_clan/belokk.json new file mode 100644 index 000000000..20cddfde3 --- /dev/null +++ b/bots/bots_clan/belokk.json @@ -0,0 +1,35 @@ +{ + "name": "Belokk", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/benarcher.json b/bots/bots_clan/benarcher.json new file mode 100644 index 000000000..c15de74ce --- /dev/null +++ b/bots/bots_clan/benarcher.json @@ -0,0 +1,35 @@ +{ + "name": "Ben Archer", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.9, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/berserker.json b/bots/bots_clan/berserker.json new file mode 100644 index 000000000..aef0dd62b --- /dev/null +++ b/bots/bots_clan/berserker.json @@ -0,0 +1,35 @@ +{ + "name": "Berserker", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.1, + "silencer": 0.4, + "slippers": 0.1, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.9, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bitterman.json b/bots/bots_clan/bitterman.json new file mode 100644 index 000000000..ee35e2c81 --- /dev/null +++ b/bots/bots_clan/bitterman.json @@ -0,0 +1,35 @@ +{ + "name": "Bitterman", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/blackadder.json b/bots/bots_clan/blackadder.json new file mode 100644 index 000000000..f05b37783 --- /dev/null +++ b/bots/bots_clan/blackadder.json @@ -0,0 +1,35 @@ +{ + "name": "Blackadder", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.6, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.9, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/blythe.json b/bots/bots_clan/blythe.json new file mode 100644 index 000000000..55dda37fb --- /dev/null +++ b/bots/bots_clan/blythe.json @@ -0,0 +1,35 @@ +{ + "name": "Blythe", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.1, + "m4": 0.2, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bobross.json b/bots/bots_clan/bobross.json new file mode 100644 index 000000000..ff8d09576 --- /dev/null +++ b/bots/bots_clan/bobross.json @@ -0,0 +1,35 @@ +{ + "name": "Bob Ross", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.8, + "hc": -0.1, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.1, + "silencer": 0.1, + "slippers": 0.6, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 1.1, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bones.json b/bots/bots_clan/bones.json new file mode 100644 index 000000000..4f892ddcc --- /dev/null +++ b/bots/bots_clan/bones.json @@ -0,0 +1,35 @@ +{ + "name": "Bones", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.0, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.0, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/booker.json b/bots/bots_clan/booker.json new file mode 100644 index 000000000..5b0c95c59 --- /dev/null +++ b/bots/bots_clan/booker.json @@ -0,0 +1,35 @@ +{ + "name": "Booker", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.2, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/boy.json b/bots/bots_clan/boy.json new file mode 100644 index 000000000..dc604cd3d --- /dev/null +++ b/bots/bots_clan/boy.json @@ -0,0 +1,35 @@ +{ + "name": "Boy", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.6, + "slippers": 1.0, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/brad.json b/bots/bots_clan/brad.json new file mode 100644 index 000000000..d2feb2669 --- /dev/null +++ b/bots/bots_clan/brad.json @@ -0,0 +1,35 @@ +{ + "name": "Brad", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.0, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bullseye.json b/bots/bots_clan/bullseye.json new file mode 100644 index 000000000..a7b4c6843 --- /dev/null +++ b/bots/bots_clan/bullseye.json @@ -0,0 +1,35 @@ +{ + "name": "Bullseye", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bve.json b/bots/bots_clan/bve.json new file mode 100644 index 000000000..8768f696e --- /dev/null +++ b/bots/bots_clan/bve.json @@ -0,0 +1,35 @@ +{ + "name": "BVe", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/cerberon.json b/bots/bots_clan/cerberon.json new file mode 100644 index 000000000..1e1c715e7 --- /dev/null +++ b/bots/bots_clan/cerberon.json @@ -0,0 +1,35 @@ +{ + "name": "Cerberon", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/chameleon.json b/bots/bots_clan/chameleon.json new file mode 100644 index 000000000..4184aef7d --- /dev/null +++ b/bots/bots_clan/chameleon.json @@ -0,0 +1,35 @@ +{ + "name": "Chameleon", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": -0.1, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.1, + "knives": 0.3, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.1, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/chan.json b/bots/bots_clan/chan.json new file mode 100644 index 000000000..e3d7805d0 --- /dev/null +++ b/bots/bots_clan/chan.json @@ -0,0 +1,35 @@ +{ + "name": "Chan", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.0, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.1, + "laser": 0.9, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/clion.json b/bots/bots_clan/clion.json new file mode 100644 index 000000000..c44a4b774 --- /dev/null +++ b/bots/bots_clan/clion.json @@ -0,0 +1,35 @@ +{ + "name": "C-Lion", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.9, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.1, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/coldcrow.json b/bots/bots_clan/coldcrow.json new file mode 100644 index 000000000..07d444dd7 --- /dev/null +++ b/bots/bots_clan/coldcrow.json @@ -0,0 +1,35 @@ +{ + "name": "Coldcrow", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.5, + "hc": -0.0, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.1, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/conan.json b/bots/bots_clan/conan.json new file mode 100644 index 000000000..00acc70f8 --- /dev/null +++ b/bots/bots_clan/conan.json @@ -0,0 +1,35 @@ +{ + "name": "Conan", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/crash.json b/bots/bots_clan/crash.json new file mode 100644 index 000000000..f6dbf3462 --- /dev/null +++ b/bots/bots_clan/crash.json @@ -0,0 +1,35 @@ +{ + "name": "Crash", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.9, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/cybr.json b/bots/bots_clan/cybr.json new file mode 100644 index 000000000..d6bd50097 --- /dev/null +++ b/bots/bots_clan/cybr.json @@ -0,0 +1,35 @@ +{ + "name": "Cybr", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.1, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 1.0, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/dacfarren.json b/bots/bots_clan/dacfarren.json new file mode 100644 index 000000000..32bc8a9fa --- /dev/null +++ b/bots/bots_clan/dacfarren.json @@ -0,0 +1,35 @@ +{ + "name": "Dac Farren", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.7, + "hc": 0.0, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/deusvault.json b/bots/bots_clan/deusvault.json new file mode 100644 index 000000000..29836464a --- /dev/null +++ b/bots/bots_clan/deusvault.json @@ -0,0 +1,35 @@ +{ + "name": "deUSvauLT", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.4, + "slippers": 1.0, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 1.0, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/digz.json b/bots/bots_clan/digz.json new file mode 100644 index 000000000..21de26a20 --- /dev/null +++ b/bots/bots_clan/digz.json @@ -0,0 +1,35 @@ +{ + "name": "Digz", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/doc.json b/bots/bots_clan/doc.json new file mode 100644 index 000000000..38e44f4fe --- /dev/null +++ b/bots/bots_clan/doc.json @@ -0,0 +1,35 @@ +{ + "name": "doc", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 1.0, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/dominator.json b/bots/bots_clan/dominator.json new file mode 100644 index 000000000..da16641b9 --- /dev/null +++ b/bots/bots_clan/dominator.json @@ -0,0 +1,35 @@ +{ + "name": "Dominator", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.3, + "knives": -0.1, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.9, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 1.2, + "movementStyle": 0.5, + "objectiveFocus": -0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/doom.json b/bots/bots_clan/doom.json new file mode 100644 index 000000000..37e847b22 --- /dev/null +++ b/bots/bots_clan/doom.json @@ -0,0 +1,35 @@ +{ + "name": "Doom", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.7, + "hc": 0.0, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.0 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 1.0, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/dragoooon.json b/bots/bots_clan/dragoooon.json new file mode 100644 index 000000000..50160605c --- /dev/null +++ b/bots/bots_clan/dragoooon.json @@ -0,0 +1,35 @@ +{ + "name": "Dragoooon", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.9, + "m4": 0.8, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.9, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/drahmin.json b/bots/bots_clan/drahmin.json new file mode 100644 index 000000000..e4869c409 --- /dev/null +++ b/bots/bots_clan/drahmin.json @@ -0,0 +1,35 @@ +{ + "name": "Drahmin", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.4, + "m3": 1.0, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 1.0, + "curiosity": 0.5, + "aimingSkill": 0.1, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/drillbit.json b/bots/bots_clan/drillbit.json new file mode 100644 index 000000000..e80710606 --- /dev/null +++ b/bots/bots_clan/drillbit.json @@ -0,0 +1,35 @@ +{ + "name": "Drillbit", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.1, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/drjay.json b/bots/bots_clan/drjay.json new file mode 100644 index 000000000..49040bccb --- /dev/null +++ b/bots/bots_clan/drjay.json @@ -0,0 +1,35 @@ +{ + "name": "Dr.Jay", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/dvorah.json b/bots/bots_clan/dvorah.json new file mode 100644 index 000000000..c6c1242ed --- /dev/null +++ b/bots/bots_clan/dvorah.json @@ -0,0 +1,35 @@ +{ + "name": "D'Vorah", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.8, + "mp5": 0.9, + "m4": 0.5, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/edwig.json b/bots/bots_clan/edwig.json new file mode 100644 index 000000000..27da67351 --- /dev/null +++ b/bots/bots_clan/edwig.json @@ -0,0 +1,35 @@ +{ + "name": "Edwig", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/endugu.json b/bots/bots_clan/endugu.json new file mode 100644 index 000000000..786d31e6c --- /dev/null +++ b/bots/bots_clan/endugu.json @@ -0,0 +1,35 @@ +{ + "name": "Endugu", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ermac.json b/bots/bots_clan/ermac.json new file mode 100644 index 000000000..174643f5f --- /dev/null +++ b/bots/bots_clan/ermac.json @@ -0,0 +1,35 @@ +{ + "name": "Ermac", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/exbee.json b/bots/bots_clan/exbee.json new file mode 100644 index 000000000..776022fd7 --- /dev/null +++ b/bots/bots_clan/exbee.json @@ -0,0 +1,35 @@ +{ + "name": "exBEE", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.4, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/eyeback.json b/bots/bots_clan/eyeback.json new file mode 100644 index 000000000..f04c28236 --- /dev/null +++ b/bots/bots_clan/eyeback.json @@ -0,0 +1,35 @@ +{ + "name": "Eyeback", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.9, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/faden.json b/bots/bots_clan/faden.json new file mode 100644 index 000000000..d9761c71d --- /dev/null +++ b/bots/bots_clan/faden.json @@ -0,0 +1,35 @@ +{ + "name": "Faden", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fangskin.json b/bots/bots_clan/fangskin.json new file mode 100644 index 000000000..632a82cf9 --- /dev/null +++ b/bots/bots_clan/fangskin.json @@ -0,0 +1,35 @@ +{ + "name": "Fangskin", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.2, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fexel.json b/bots/bots_clan/fexel.json new file mode 100644 index 000000000..03e136ac0 --- /dev/null +++ b/bots/bots_clan/fexel.json @@ -0,0 +1,35 @@ +{ + "name": "Fexel", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.1, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/figgz.json b/bots/bots_clan/figgz.json new file mode 100644 index 000000000..ede6fc54b --- /dev/null +++ b/bots/bots_clan/figgz.json @@ -0,0 +1,35 @@ +{ + "name": "Figgz", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/flamespike.json b/bots/bots_clan/flamespike.json new file mode 100644 index 000000000..f8b5e3cba --- /dev/null +++ b/bots/bots_clan/flamespike.json @@ -0,0 +1,35 @@ +{ + "name": "Flamespike", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/flint.json b/bots/bots_clan/flint.json new file mode 100644 index 000000000..bf228245f --- /dev/null +++ b/bots/bots_clan/flint.json @@ -0,0 +1,35 @@ +{ + "name": "Flint", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.0, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 1.2, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/frost.json b/bots/bots_clan/frost.json new file mode 100644 index 000000000..8030e5bc8 --- /dev/null +++ b/bots/bots_clan/frost.json @@ -0,0 +1,35 @@ +{ + "name": "Frost", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": -0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fullmonty.json b/bots/bots_clan/fullmonty.json new file mode 100644 index 000000000..2a34115c5 --- /dev/null +++ b/bots/bots_clan/fullmonty.json @@ -0,0 +1,35 @@ +{ + "name": "Fullmonty", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.2, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/funkanik.json b/bots/bots_clan/funkanik.json new file mode 100644 index 000000000..4e09a11ab --- /dev/null +++ b/bots/bots_clan/funkanik.json @@ -0,0 +1,35 @@ +{ + "name": "FUNKANIK", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": -0.0, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gali.json b/bots/bots_clan/gali.json new file mode 100644 index 000000000..e1745848e --- /dev/null +++ b/bots/bots_clan/gali.json @@ -0,0 +1,35 @@ +{ + "name": "Gali", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.9, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/geras.json b/bots/bots_clan/geras.json new file mode 100644 index 000000000..4bf014188 --- /dev/null +++ b/bots/bots_clan/geras.json @@ -0,0 +1,35 @@ +{ + "name": "Geras", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gladiator.json b/bots/bots_clan/gladiator.json new file mode 100644 index 000000000..d1643abd9 --- /dev/null +++ b/bots/bots_clan/gladiator.json @@ -0,0 +1,35 @@ +{ + "name": "Gladiator", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.9, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 1.0, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.7, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gogman.json b/bots/bots_clan/gogman.json new file mode 100644 index 000000000..ac208dbe7 --- /dev/null +++ b/bots/bots_clan/gogman.json @@ -0,0 +1,35 @@ +{ + "name": "gogman", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gorre.json b/bots/bots_clan/gorre.json new file mode 100644 index 000000000..d3855a271 --- /dev/null +++ b/bots/bots_clan/gorre.json @@ -0,0 +1,35 @@ +{ + "name": "Gorre", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 1.0, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/grezzer.json b/bots/bots_clan/grezzer.json new file mode 100644 index 000000000..2eb259cff --- /dev/null +++ b/bots/bots_clan/grezzer.json @@ -0,0 +1,35 @@ +{ + "name": "GreZZer", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 1.0, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/griswold.json b/bots/bots_clan/griswold.json new file mode 100644 index 000000000..980dd7b39 --- /dev/null +++ b/bots/bots_clan/griswold.json @@ -0,0 +1,35 @@ +{ + "name": "Griswold", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.9, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/grunt.json b/bots/bots_clan/grunt.json new file mode 100644 index 000000000..e6b4e9dc7 --- /dev/null +++ b/bots/bots_clan/grunt.json @@ -0,0 +1,35 @@ +{ + "name": "Grunt", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.1, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gun.json b/bots/bots_clan/gun.json new file mode 100644 index 000000000..90691c9da --- /dev/null +++ b/bots/bots_clan/gun.json @@ -0,0 +1,35 @@ +{ + "name": "GuN", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.5, + "m3": -0.0, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.1, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.9, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gunner.json b/bots/bots_clan/gunner.json new file mode 100644 index 000000000..7c28172b8 --- /dev/null +++ b/bots/bots_clan/gunner.json @@ -0,0 +1,35 @@ +{ + "name": "Gunner", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.9, + "m4": 0.6, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.0 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hammerhead.json b/bots/bots_clan/hammerhead.json new file mode 100644 index 000000000..d108e3603 --- /dev/null +++ b/bots/bots_clan/hammerhead.json @@ -0,0 +1,35 @@ +{ + "name": "Hammerhead", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 1.0, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/havo.json b/bots/bots_clan/havo.json new file mode 100644 index 000000000..68137827b --- /dev/null +++ b/bots/bots_clan/havo.json @@ -0,0 +1,35 @@ +{ + "name": "Havo", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.7, + "hc": 1.0, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/helmut.json b/bots/bots_clan/helmut.json new file mode 100644 index 000000000..afec58b0f --- /dev/null +++ b/bots/bots_clan/helmut.json @@ -0,0 +1,35 @@ +{ + "name": "Helmut", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 1.0, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/himdar.json b/bots/bots_clan/himdar.json new file mode 100644 index 000000000..31b5eb488 --- /dev/null +++ b/bots/bots_clan/himdar.json @@ -0,0 +1,35 @@ +{ + "name": "Himdar", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.8, + "reactionTime": 1.1, + "communicationFreq": 0.7, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hitnrun.json b/bots/bots_clan/hitnrun.json new file mode 100644 index 000000000..91946087b --- /dev/null +++ b/bots/bots_clan/hitnrun.json @@ -0,0 +1,35 @@ +{ + "name": "Hitnrun", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hossman.json b/bots/bots_clan/hossman.json new file mode 100644 index 000000000..3374acfef --- /dev/null +++ b/bots/bots_clan/hossman.json @@ -0,0 +1,35 @@ +{ + "name": "Hossman", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 1.0, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.1, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hotaru.json b/bots/bots_clan/hotaru.json new file mode 100644 index 000000000..20e4f1a62 --- /dev/null +++ b/bots/bots_clan/hotaru.json @@ -0,0 +1,35 @@ +{ + "name": "Hotaru", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.9, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hsuhao.json b/bots/bots_clan/hsuhao.json new file mode 100644 index 000000000..1d6c840c7 --- /dev/null +++ b/bots/bots_clan/hsuhao.json @@ -0,0 +1,35 @@ +{ + "name": "Hsu Hao", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.9, + "silencer": 0.2, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hunter.json b/bots/bots_clan/hunter.json new file mode 100644 index 000000000..d6d704d54 --- /dev/null +++ b/bots/bots_clan/hunter.json @@ -0,0 +1,35 @@ +{ + "name": "Hunter", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 1.0, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hurricane.json b/bots/bots_clan/hurricane.json new file mode 100644 index 000000000..dc85a553f --- /dev/null +++ b/bots/bots_clan/hurricane.json @@ -0,0 +1,35 @@ +{ + "name": "Hurricane", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/icarus.json b/bots/bots_clan/icarus.json new file mode 100644 index 000000000..e44a8ec4c --- /dev/null +++ b/bots/bots_clan/icarus.json @@ -0,0 +1,35 @@ +{ + "name": "Icarus", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.1, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/icehawk.json b/bots/bots_clan/icehawk.json new file mode 100644 index 000000000..83692c00a --- /dev/null +++ b/bots/bots_clan/icehawk.json @@ -0,0 +1,35 @@ +{ + "name": "Icehawk", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/icexo.json b/bots/bots_clan/icexo.json new file mode 100644 index 000000000..f982dd2b2 --- /dev/null +++ b/bots/bots_clan/icexo.json @@ -0,0 +1,35 @@ +{ + "name": "Icexo", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ismail.json b/bots/bots_clan/ismail.json new file mode 100644 index 000000000..ec1430265 --- /dev/null +++ b/bots/bots_clan/ismail.json @@ -0,0 +1,35 @@ +{ + "name": "Ismail", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.0, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/itzchamp.json b/bots/bots_clan/itzchamp.json new file mode 100644 index 000000000..c1295f3b7 --- /dev/null +++ b/bots/bots_clan/itzchamp.json @@ -0,0 +1,35 @@ +{ + "name": "{itzChamp}", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.1, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ivor.json b/bots/bots_clan/ivor.json new file mode 100644 index 000000000..e696ff5d9 --- /dev/null +++ b/bots/bots_clan/ivor.json @@ -0,0 +1,35 @@ +{ + "name": "Ivor", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jacksaxon.json b/bots/bots_clan/jacksaxon.json new file mode 100644 index 000000000..1a09ae121 --- /dev/null +++ b/bots/bots_clan/jacksaxon.json @@ -0,0 +1,35 @@ +{ + "name": "Jack Saxon", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 1.1, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 1.0, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 1.0, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jade.json b/bots/bots_clan/jade.json new file mode 100644 index 000000000..54b9ca693 --- /dev/null +++ b/bots/bots_clan/jade.json @@ -0,0 +1,35 @@ +{ + "name": "Jade", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.1, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jarno.json b/bots/bots_clan/jarno.json new file mode 100644 index 000000000..6fb080956 --- /dev/null +++ b/bots/bots_clan/jarno.json @@ -0,0 +1,35 @@ +{ + "name": "Jarno", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jarvis.json b/bots/bots_clan/jarvis.json new file mode 100644 index 000000000..d60595f54 --- /dev/null +++ b/bots/bots_clan/jarvis.json @@ -0,0 +1,35 @@ +{ + "name": "Jarvis", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.1, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.8, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jazz.json b/bots/bots_clan/jazz.json new file mode 100644 index 000000000..fddbdc65d --- /dev/null +++ b/bots/bots_clan/jazz.json @@ -0,0 +1,35 @@ +{ + "name": "Jazz", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jce.json b/bots/bots_clan/jce.json new file mode 100644 index 000000000..8cdc69e87 --- /dev/null +++ b/bots/bots_clan/jce.json @@ -0,0 +1,35 @@ +{ + "name": "JCe", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/joesanto.json b/bots/bots_clan/joesanto.json new file mode 100644 index 000000000..c8fbd42b0 --- /dev/null +++ b/bots/bots_clan/joesanto.json @@ -0,0 +1,35 @@ +{ + "name": "Joe Santo", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 1.0, + "reactionTime": 0.6, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/joetanto.json b/bots/bots_clan/joetanto.json new file mode 100644 index 000000000..8cdd89459 --- /dev/null +++ b/bots/bots_clan/joetanto.json @@ -0,0 +1,35 @@ +{ + "name": "Joe Tanto", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.1, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 1.0, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/johnluger.json b/bots/bots_clan/johnluger.json new file mode 100644 index 000000000..08c048955 --- /dev/null +++ b/bots/bots_clan/johnluger.json @@ -0,0 +1,35 @@ +{ + "name": "John Luger", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": -0.0, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kalidor.json b/bots/bots_clan/kalidor.json new file mode 100644 index 000000000..59a9bdcd7 --- /dev/null +++ b/bots/bots_clan/kalidor.json @@ -0,0 +1,35 @@ +{ + "name": "Kalidor", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.0, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.1, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kaolin.json b/bots/bots_clan/kaolin.json new file mode 100644 index 000000000..3d6687fe1 --- /dev/null +++ b/bots/bots_clan/kaolin.json @@ -0,0 +1,35 @@ +{ + "name": "Kaolin", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.2, + "mp5": 0.2, + "m4": 0.2, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.1, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/keel.json b/bots/bots_clan/keel.json new file mode 100644 index 000000000..4fc51c7f9 --- /dev/null +++ b/bots/bots_clan/keel.json @@ -0,0 +1,35 @@ +{ + "name": "Keel", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kintaro.json b/bots/bots_clan/kintaro.json new file mode 100644 index 000000000..d3d1982e1 --- /dev/null +++ b/bots/bots_clan/kintaro.json @@ -0,0 +1,35 @@ +{ + "name": "Kintaro", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.0, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.2, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 1.0, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kitana.json b/bots/bots_clan/kitana.json new file mode 100644 index 000000000..a01ceaeec --- /dev/null +++ b/bots/bots_clan/kitana.json @@ -0,0 +1,35 @@ +{ + "name": "Kitana", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/klesk.json b/bots/bots_clan/klesk.json new file mode 100644 index 000000000..05e6c448f --- /dev/null +++ b/bots/bots_clan/klesk.json @@ -0,0 +1,35 @@ +{ + "name": "Klesk", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.0, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": -0.1, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": -0.1, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kollector.json b/bots/bots_clan/kollector.json new file mode 100644 index 000000000..315eba200 --- /dev/null +++ b/bots/bots_clan/kollector.json @@ -0,0 +1,35 @@ +{ + "name": "Kollector", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kor.json b/bots/bots_clan/kor.json new file mode 100644 index 000000000..560a9839d --- /dev/null +++ b/bots/bots_clan/kor.json @@ -0,0 +1,35 @@ +{ + "name": "Kor", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kronika.json b/bots/bots_clan/kronika.json new file mode 100644 index 000000000..37c338e5f --- /dev/null +++ b/bots/bots_clan/kronika.json @@ -0,0 +1,35 @@ +{ + "name": "Kronika", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": -0.0, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.9, + "laser": 0.7, + "silencer": 0.2, + "slippers": 0.1, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kunglao.json b/bots/bots_clan/kunglao.json new file mode 100644 index 000000000..d46c93984 --- /dev/null +++ b/bots/bots_clan/kunglao.json @@ -0,0 +1,35 @@ +{ + "name": "Kung Lao", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.2, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kuri.json b/bots/bots_clan/kuri.json new file mode 100644 index 000000000..4132d5873 --- /dev/null +++ b/bots/bots_clan/kuri.json @@ -0,0 +1,35 @@ +{ + "name": "kuri", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.9, + "m4": 0.8, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/lawrence.json b/bots/bots_clan/lawrence.json new file mode 100644 index 000000000..1955d06f7 --- /dev/null +++ b/bots/bots_clan/lawrence.json @@ -0,0 +1,35 @@ +{ + "name": "Lawrence", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/leiruth.json b/bots/bots_clan/leiruth.json new file mode 100644 index 000000000..3e7339c09 --- /dev/null +++ b/bots/bots_clan/leiruth.json @@ -0,0 +1,35 @@ +{ + "name": "Leiruth", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.0, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/limei.json b/bots/bots_clan/limei.json new file mode 100644 index 000000000..aaff589a5 --- /dev/null +++ b/bots/bots_clan/limei.json @@ -0,0 +1,35 @@ +{ + "name": "Li Mei", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 1.0, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/liukang.json b/bots/bots_clan/liukang.json new file mode 100644 index 000000000..532334b2c --- /dev/null +++ b/bots/bots_clan/liukang.json @@ -0,0 +1,35 @@ +{ + "name": "Liu Kang", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/lonewolf.json b/bots/bots_clan/lonewolf.json new file mode 100644 index 000000000..ba11705d0 --- /dev/null +++ b/bots/bots_clan/lonewolf.json @@ -0,0 +1,35 @@ +{ + "name": "Lone Wolf", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.1, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.9, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/maffer.json b/bots/bots_clan/maffer.json new file mode 100644 index 000000000..c5a750a3f --- /dev/null +++ b/bots/bots_clan/maffer.json @@ -0,0 +1,35 @@ +{ + "name": "Maffer", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.9, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/malin.json b/bots/bots_clan/malin.json new file mode 100644 index 000000000..85678b07b --- /dev/null +++ b/bots/bots_clan/malin.json @@ -0,0 +1,35 @@ +{ + "name": "Malin", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.7, + "hc": 0.0, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/manny.json b/bots/bots_clan/manny.json new file mode 100644 index 000000000..f591b3272 --- /dev/null +++ b/bots/bots_clan/manny.json @@ -0,0 +1,35 @@ +{ + "name": "MannY", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.1, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/marku.json b/bots/bots_clan/marku.json new file mode 100644 index 000000000..11cd4041d --- /dev/null +++ b/bots/bots_clan/marku.json @@ -0,0 +1,35 @@ +{ + "name": "Marku", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mavado.json b/bots/bots_clan/mavado.json new file mode 100644 index 000000000..45d010e3b --- /dev/null +++ b/bots/bots_clan/mavado.json @@ -0,0 +1,35 @@ +{ + "name": "Mavado", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.1, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/meat.json b/bots/bots_clan/meat.json new file mode 100644 index 000000000..200f54225 --- /dev/null +++ b/bots/bots_clan/meat.json @@ -0,0 +1,35 @@ +{ + "name": "Meat", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/medic.json b/bots/bots_clan/medic.json new file mode 100644 index 000000000..5ddc60b1f --- /dev/null +++ b/bots/bots_clan/medic.json @@ -0,0 +1,35 @@ +{ + "name": "Medic", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 1.0, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": -0.1, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mee2.json b/bots/bots_clan/mee2.json new file mode 100644 index 000000000..af450fe61 --- /dev/null +++ b/bots/bots_clan/mee2.json @@ -0,0 +1,35 @@ +{ + "name": "[Mee2]", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mindelos.json b/bots/bots_clan/mindelos.json new file mode 100644 index 000000000..c14050d7b --- /dev/null +++ b/bots/bots_clan/mindelos.json @@ -0,0 +1,35 @@ +{ + "name": "Mindelos", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.0, + "slippers": 0.3, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/missnrun.json b/bots/bots_clan/missnrun.json new file mode 100644 index 000000000..b85f11536 --- /dev/null +++ b/bots/bots_clan/missnrun.json @@ -0,0 +1,35 @@ +{ + "name": "Missnrun", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mokap.json b/bots/bots_clan/mokap.json new file mode 100644 index 000000000..38dfd5809 --- /dev/null +++ b/bots/bots_clan/mokap.json @@ -0,0 +1,35 @@ +{ + "name": "Mokap", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/monk.json b/bots/bots_clan/monk.json new file mode 100644 index 000000000..7c3023976 --- /dev/null +++ b/bots/bots_clan/monk.json @@ -0,0 +1,35 @@ +{ + "name": "Monk", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/monty.json b/bots/bots_clan/monty.json new file mode 100644 index 000000000..411ea7faf --- /dev/null +++ b/bots/bots_clan/monty.json @@ -0,0 +1,35 @@ +{ + "name": "Monty", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.1, + "objectiveFocus": -0.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/morgan.json b/bots/bots_clan/morgan.json new file mode 100644 index 000000000..6b4518ac2 --- /dev/null +++ b/bots/bots_clan/morgan.json @@ -0,0 +1,35 @@ +{ + "name": "Morgan", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.0, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mrbadger.json b/bots/bots_clan/mrbadger.json new file mode 100644 index 000000000..2cfde5d73 --- /dev/null +++ b/bots/bots_clan/mrbadger.json @@ -0,0 +1,35 @@ +{ + "name": "Mr.Badger", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.9, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mrgoodkat.json b/bots/bots_clan/mrgoodkat.json new file mode 100644 index 000000000..501016320 --- /dev/null +++ b/bots/bots_clan/mrgoodkat.json @@ -0,0 +1,35 @@ +{ + "name": "Mr Goodkat", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.2, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mungri.json b/bots/bots_clan/mungri.json new file mode 100644 index 000000000..fffe3a65a --- /dev/null +++ b/bots/bots_clan/mungri.json @@ -0,0 +1,35 @@ +{ + "name": "Mungri", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.0, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/muscleman.json b/bots/bots_clan/muscleman.json new file mode 100644 index 000000000..c16310705 --- /dev/null +++ b/bots/bots_clan/muscleman.json @@ -0,0 +1,35 @@ +{ + "name": "Muscleman", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mynx.json b/bots/bots_clan/mynx.json new file mode 100644 index 000000000..68c4efe47 --- /dev/null +++ b/bots/bots_clan/mynx.json @@ -0,0 +1,35 @@ +{ + "name": "Mynx", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": -0.0, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nandet.json b/bots/bots_clan/nandet.json new file mode 100644 index 000000000..e968c9fc8 --- /dev/null +++ b/bots/bots_clan/nandet.json @@ -0,0 +1,35 @@ +{ + "name": "Nandet", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.9, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nightwolf.json b/bots/bots_clan/nightwolf.json new file mode 100644 index 000000000..475801dce --- /dev/null +++ b/bots/bots_clan/nightwolf.json @@ -0,0 +1,35 @@ +{ + "name": "Nightwolf", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.1, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.9, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nin.json b/bots/bots_clan/nin.json new file mode 100644 index 000000000..35b22b9c7 --- /dev/null +++ b/bots/bots_clan/nin.json @@ -0,0 +1,35 @@ +{ + "name": "NiN", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nitara.json b/bots/bots_clan/nitara.json new file mode 100644 index 000000000..3cde42fe2 --- /dev/null +++ b/bots/bots_clan/nitara.json @@ -0,0 +1,35 @@ +{ + "name": "Nitara", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.0, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/noogie.json b/bots/bots_clan/noogie.json new file mode 100644 index 000000000..b29454436 --- /dev/null +++ b/bots/bots_clan/noogie.json @@ -0,0 +1,35 @@ +{ + "name": "Noogie", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/orbb.json b/bots/bots_clan/orbb.json new file mode 100644 index 000000000..9325a07fc --- /dev/null +++ b/bots/bots_clan/orbb.json @@ -0,0 +1,35 @@ +{ + "name": "Orbb", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/orinboyd.json b/bots/bots_clan/orinboyd.json new file mode 100644 index 000000000..a1cdabb49 --- /dev/null +++ b/bots/bots_clan/orinboyd.json @@ -0,0 +1,35 @@ +{ + "name": "Orin Boyd", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ostmor.json b/bots/bots_clan/ostmor.json new file mode 100644 index 000000000..12d8223cc --- /dev/null +++ b/bots/bots_clan/ostmor.json @@ -0,0 +1,35 @@ +{ + "name": "Ostmor", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.1, + "reactionTime": 1.0, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/paradox.json b/bots/bots_clan/paradox.json new file mode 100644 index 000000000..723e24d7e --- /dev/null +++ b/bots/bots_clan/paradox.json @@ -0,0 +1,35 @@ +{ + "name": "ParaDox", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/patriot.json b/bots/bots_clan/patriot.json new file mode 100644 index 000000000..f65f39de0 --- /dev/null +++ b/bots/bots_clan/patriot.json @@ -0,0 +1,35 @@ +{ + "name": "Patriot", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/pewdiepie.json b/bots/bots_clan/pewdiepie.json new file mode 100644 index 000000000..97bad8559 --- /dev/null +++ b/bots/bots_clan/pewdiepie.json @@ -0,0 +1,35 @@ +{ + "name": "PewDiePie", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.1, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.9, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/potaytoe.json b/bots/bots_clan/potaytoe.json new file mode 100644 index 000000000..bf7d2e26e --- /dev/null +++ b/bots/bots_clan/potaytoe.json @@ -0,0 +1,35 @@ +{ + "name": "Po-Tay-Toe", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.9, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/pyroyetti.json b/bots/bots_clan/pyroyetti.json new file mode 100644 index 000000000..e92244d2f --- /dev/null +++ b/bots/bots_clan/pyroyetti.json @@ -0,0 +1,35 @@ +{ + "name": "Pyro Yetti", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.1, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.1, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/radament.json b/bots/bots_clan/radament.json new file mode 100644 index 000000000..8f7691f86 --- /dev/null +++ b/bots/bots_clan/radament.json @@ -0,0 +1,35 @@ +{ + "name": "Radament", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.1, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/rakanishu.json b/bots/bots_clan/rakanishu.json new file mode 100644 index 000000000..bdb76f4ea --- /dev/null +++ b/bots/bots_clan/rakanishu.json @@ -0,0 +1,35 @@ +{ + "name": "Rakanishu", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ramrod.json b/bots/bots_clan/ramrod.json new file mode 100644 index 000000000..cc2337185 --- /dev/null +++ b/bots/bots_clan/ramrod.json @@ -0,0 +1,35 @@ +{ + "name": "Ramrod", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": -0.0, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.0, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ratbot.json b/bots/bots_clan/ratbot.json new file mode 100644 index 000000000..4d686a99e --- /dev/null +++ b/bots/bots_clan/ratbot.json @@ -0,0 +1,35 @@ +{ + "name": "Ratbot", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.9, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/reiko.json b/bots/bots_clan/reiko.json new file mode 100644 index 000000000..b3dd55422 --- /dev/null +++ b/bots/bots_clan/reiko.json @@ -0,0 +1,35 @@ +{ + "name": "Reiko", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.4, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/replicant.json b/bots/bots_clan/replicant.json new file mode 100644 index 000000000..e08bfe7de --- /dev/null +++ b/bots/bots_clan/replicant.json @@ -0,0 +1,35 @@ +{ + "name": "Replicant", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.2, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.9, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ruslan.json b/bots/bots_clan/ruslan.json new file mode 100644 index 000000000..4d7bd7884 --- /dev/null +++ b/bots/bots_clan/ruslan.json @@ -0,0 +1,35 @@ +{ + "name": "Ruslan", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.1, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.9, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sagel.json b/bots/bots_clan/sagel.json new file mode 100644 index 000000000..1b4122768 --- /dev/null +++ b/bots/bots_clan/sagel.json @@ -0,0 +1,35 @@ +{ + "name": "Sagel", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sarge.json b/bots/bots_clan/sarge.json new file mode 100644 index 000000000..7bd369712 --- /dev/null +++ b/bots/bots_clan/sarge.json @@ -0,0 +1,35 @@ +{ + "name": "Sarge", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.1, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, + "sniper": 1.1, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sarina.json b/bots/bots_clan/sarina.json new file mode 100644 index 000000000..df19ed862 --- /dev/null +++ b/bots/bots_clan/sarina.json @@ -0,0 +1,35 @@ +{ + "name": "Sarina", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/scorpion.json b/bots/bots_clan/scorpion.json new file mode 100644 index 000000000..2c9613612 --- /dev/null +++ b/bots/bots_clan/scorpion.json @@ -0,0 +1,35 @@ +{ + "name": "Scorpion", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sean.json b/bots/bots_clan/sean.json new file mode 100644 index 000000000..8685e43b9 --- /dev/null +++ b/bots/bots_clan/sean.json @@ -0,0 +1,35 @@ +{ + "name": "Sean", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.4, + "slippers": 1.1, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": -0.1, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/seankane.json b/bots/bots_clan/seankane.json new file mode 100644 index 000000000..7f702e35f --- /dev/null +++ b/bots/bots_clan/seankane.json @@ -0,0 +1,35 @@ +{ + "name": "Sean Kane", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sektor.json b/bots/bots_clan/sektor.json new file mode 100644 index 000000000..852c249cb --- /dev/null +++ b/bots/bots_clan/sektor.json @@ -0,0 +1,35 @@ +{ + "name": "Sektor", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.1, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sheeva.json b/bots/bots_clan/sheeva.json new file mode 100644 index 000000000..5870ba495 --- /dev/null +++ b/bots/bots_clan/sheeva.json @@ -0,0 +1,35 @@ +{ + "name": "Sheeva", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/shinnok.json b/bots/bots_clan/shinnok.json new file mode 100644 index 000000000..81f8df222 --- /dev/null +++ b/bots/bots_clan/shinnok.json @@ -0,0 +1,35 @@ +{ + "name": "Shinnok", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/si.json b/bots/bots_clan/si.json new file mode 100644 index 000000000..d65c702ab --- /dev/null +++ b/bots/bots_clan/si.json @@ -0,0 +1,35 @@ +{ + "name": "Si", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sindel.json b/bots/bots_clan/sindel.json new file mode 100644 index 000000000..397f5d109 --- /dev/null +++ b/bots/bots_clan/sindel.json @@ -0,0 +1,35 @@ +{ + "name": "Sindel", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.5, + "m3": -0.0, + "hc": 0.3, + "sniper": 0.1, + "knives": 0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.8, + "silencer": -0.0, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.1, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.1, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/slash.json b/bots/bots_clan/slash.json new file mode 100644 index 000000000..302a81b98 --- /dev/null +++ b/bots/bots_clan/slash.json @@ -0,0 +1,35 @@ +{ + "name": "Slash", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.0, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.9, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sorlag.json b/bots/bots_clan/sorlag.json new file mode 100644 index 000000000..47c78d816 --- /dev/null +++ b/bots/bots_clan/sorlag.json @@ -0,0 +1,35 @@ +{ + "name": "Sorlag", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/specialed.json b/bots/bots_clan/specialed.json new file mode 100644 index 000000000..4d0d80c69 --- /dev/null +++ b/bots/bots_clan/specialed.json @@ -0,0 +1,35 @@ +{ + "name": "SpecialEd", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.9, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.1, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/spocket.json b/bots/bots_clan/spocket.json new file mode 100644 index 000000000..7dae08ea5 --- /dev/null +++ b/bots/bots_clan/spocket.json @@ -0,0 +1,35 @@ +{ + "name": "Spocket", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.2, + "m4": 0.3, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.9, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/steampug.json b/bots/bots_clan/steampug.json new file mode 100644 index 000000000..0def45577 --- /dev/null +++ b/bots/bots_clan/steampug.json @@ -0,0 +1,35 @@ +{ + "name": "SteamPug", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/stinger.json b/bots/bots_clan/stinger.json new file mode 100644 index 000000000..40fd5ffb4 --- /dev/null +++ b/bots/bots_clan/stinger.json @@ -0,0 +1,35 @@ +{ + "name": "Stinger", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": -0.1, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/stormtree.json b/bots/bots_clan/stormtree.json new file mode 100644 index 000000000..379c81446 --- /dev/null +++ b/bots/bots_clan/stormtree.json @@ -0,0 +1,35 @@ +{ + "name": "Stormtree", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.1, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.1, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.0, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/strogg.json b/bots/bots_clan/strogg.json new file mode 100644 index 000000000..8523a5880 --- /dev/null +++ b/bots/bots_clan/strogg.json @@ -0,0 +1,35 @@ +{ + "name": "Strogg", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.1, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/strom.json b/bots/bots_clan/strom.json new file mode 100644 index 000000000..8ee81cd80 --- /dev/null +++ b/bots/bots_clan/strom.json @@ -0,0 +1,35 @@ +{ + "name": "Strom", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/subzero.json b/bots/bots_clan/subzero.json new file mode 100644 index 000000000..03e873f44 --- /dev/null +++ b/bots/bots_clan/subzero.json @@ -0,0 +1,35 @@ +{ + "name": "Sub-Zero", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": -0.1, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.3, + "grenades": 1.1 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/summoner.json b/bots/bots_clan/summoner.json new file mode 100644 index 000000000..5c22f64f7 --- /dev/null +++ b/bots/bots_clan/summoner.json @@ -0,0 +1,35 @@ +{ + "name": "Summoner", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 1.1, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sundog.json b/bots/bots_clan/sundog.json new file mode 100644 index 000000000..02e6ca059 --- /dev/null +++ b/bots/bots_clan/sundog.json @@ -0,0 +1,35 @@ +{ + "name": "Sundog", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/t1000.json b/bots/bots_clan/t1000.json new file mode 100644 index 000000000..df4b5c659 --- /dev/null +++ b/bots/bots_clan/t1000.json @@ -0,0 +1,35 @@ +{ + "name": "T-1000", + "clan_id": 2, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.9, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/t800.json b/bots/bots_clan/t800.json new file mode 100644 index 000000000..74fedf8f0 --- /dev/null +++ b/bots/bots_clan/t800.json @@ -0,0 +1,35 @@ +{ + "name": "T-800", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.1, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.6, + "slippers": 1.0, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/t_bon.json b/bots/bots_clan/t_bon.json new file mode 100644 index 000000000..e6cd56ee2 --- /dev/null +++ b/bots/bots_clan/t_bon.json @@ -0,0 +1,35 @@ +{ + "name": "T_bon", + "clan_id": 3, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tahlkora.json b/bots/bots_clan/tahlkora.json new file mode 100644 index 000000000..bbcfad480 --- /dev/null +++ b/bots/bots_clan/tahlkora.json @@ -0,0 +1,35 @@ +{ + "name": "Tahlkora", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": -0.1, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tank.json b/bots/bots_clan/tank.json new file mode 100644 index 000000000..72a621e8b --- /dev/null +++ b/bots/bots_clan/tank.json @@ -0,0 +1,35 @@ +{ + "name": "Tank", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.8, + "grenades": -0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.2, + "laser": 0.3, + "silencer": 1.2, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tanya.json b/bots/bots_clan/tanya.json new file mode 100644 index 000000000..3c188b59f --- /dev/null +++ b/bots/bots_clan/tanya.json @@ -0,0 +1,35 @@ +{ + "name": "Tanya", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 1.0, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/taven.json b/bots/bots_clan/taven.json new file mode 100644 index 000000000..27b101011 --- /dev/null +++ b/bots/bots_clan/taven.json @@ -0,0 +1,35 @@ +{ + "name": "Taven", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/teabag.json b/bots/bots_clan/teabag.json new file mode 100644 index 000000000..ad652434b --- /dev/null +++ b/bots/bots_clan/teabag.json @@ -0,0 +1,35 @@ +{ + "name": "Teabag", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.0, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": -0.1, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/theboss.json b/bots/bots_clan/theboss.json new file mode 100644 index 000000000..8ff50abd2 --- /dev/null +++ b/bots/bots_clan/theboss.json @@ -0,0 +1,35 @@ +{ + "name": "The Boss", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.1, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.1, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/thor.json b/bots/bots_clan/thor.json new file mode 100644 index 000000000..f4fb905df --- /dev/null +++ b/bots/bots_clan/thor.json @@ -0,0 +1,35 @@ +{ + "name": "Thor", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.0, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/threash.json b/bots/bots_clan/threash.json new file mode 100644 index 000000000..17054a337 --- /dev/null +++ b/bots/bots_clan/threash.json @@ -0,0 +1,35 @@ +{ + "name": "Threash", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tommix.json b/bots/bots_clan/tommix.json new file mode 100644 index 000000000..466277de6 --- /dev/null +++ b/bots/bots_clan/tommix.json @@ -0,0 +1,35 @@ +{ + "name": "Tom Mix", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.3, + "curiosity": 0.1, + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tomsteele.json b/bots/bots_clan/tomsteele.json new file mode 100644 index 000000000..d10496374 --- /dev/null +++ b/bots/bots_clan/tomsteele.json @@ -0,0 +1,35 @@ +{ + "name": "Tom Steele", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.9, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 1.0, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/toorc.json b/bots/bots_clan/toorc.json new file mode 100644 index 000000000..27bb8b0cc --- /dev/null +++ b/bots/bots_clan/toorc.json @@ -0,0 +1,35 @@ +{ + "name": "Toorc", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": -0.0 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/treehead.json b/bots/bots_clan/treehead.json new file mode 100644 index 000000000..b0ce3a08d --- /dev/null +++ b/bots/bots_clan/treehead.json @@ -0,0 +1,35 @@ +{ + "name": "Treehead", + "clan_id": 6, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tremor.json b/bots/bots_clan/tremor.json new file mode 100644 index 000000000..0ade88525 --- /dev/null +++ b/bots/bots_clan/tremor.json @@ -0,0 +1,35 @@ +{ + "name": "Tremor", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/trench.json b/bots/bots_clan/trench.json new file mode 100644 index 000000000..072de464e --- /dev/null +++ b/bots/bots_clan/trench.json @@ -0,0 +1,35 @@ +{ + "name": "Trench", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/uriel.json b/bots/bots_clan/uriel.json new file mode 100644 index 000000000..ce8b80ec5 --- /dev/null +++ b/bots/bots_clan/uriel.json @@ -0,0 +1,35 @@ +{ + "name": "Uriel", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.9, + "m3": 0.4, + "hc": 0.6, + "sniper": -0.0, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.0, + "laser": 0.6, + "silencer": 0.3, + "slippers": 1.0, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 1.0, + "curiosity": 0.6, + "aimingSkill": 1.0, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/uzaytron.json b/bots/bots_clan/uzaytron.json new file mode 100644 index 000000000..2871a37c7 --- /dev/null +++ b/bots/bots_clan/uzaytron.json @@ -0,0 +1,35 @@ +{ + "name": "uzayTron", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.5, + "m3": -0.0, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.7, + "silencer": 1.0, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/vedro.json b/bots/bots_clan/vedro.json new file mode 100644 index 000000000..1e4e850f0 --- /dev/null +++ b/bots/bots_clan/vedro.json @@ -0,0 +1,35 @@ +{ + "name": "Vedro", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.0, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.4, + "laser": 0.0, + "silencer": 1.1, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/vera.json b/bots/bots_clan/vera.json new file mode 100644 index 000000000..2e90292aa --- /dev/null +++ b/bots/bots_clan/vera.json @@ -0,0 +1,35 @@ +{ + "name": "Vera", + "clan_id": 4, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/visor.json b/bots/bots_clan/visor.json new file mode 100644 index 000000000..87d6ab78b --- /dev/null +++ b/bots/bots_clan/visor.json @@ -0,0 +1,35 @@ +{ + "name": "Visor", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.3, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": -0.0, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/walter.json b/bots/bots_clan/walter.json new file mode 100644 index 000000000..126580fb8 --- /dev/null +++ b/bots/bots_clan/walter.json @@ -0,0 +1,35 @@ +{ + "name": "Walter", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.1, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/warchild.json b/bots/bots_clan/warchild.json new file mode 100644 index 000000000..50b38e6b5 --- /dev/null +++ b/bots/bots_clan/warchild.json @@ -0,0 +1,35 @@ +{ + "name": "Warchild", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.0, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.6, + "slippers": 1.0, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 1.1, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/wens.json b/bots/bots_clan/wens.json new file mode 100644 index 000000000..599b9f0b8 --- /dev/null +++ b/bots/bots_clan/wens.json @@ -0,0 +1,35 @@ +{ + "name": "Wens", + "clan_id": 1, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.2, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 1.0, + "communicationTone": 0.6, + "movementStyle": 0.9, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/willitz.json b/bots/bots_clan/willitz.json new file mode 100644 index 000000000..ccc1751f5 --- /dev/null +++ b/bots/bots_clan/willitz.json @@ -0,0 +1,35 @@ +{ + "name": "Willitz**", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.9, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/wrack.json b/bots/bots_clan/wrack.json new file mode 100644 index 000000000..ee6d41464 --- /dev/null +++ b/bots/bots_clan/wrack.json @@ -0,0 +1,35 @@ +{ + "name": "Wrack", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.2, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/xaero.json b/bots/bots_clan/xaero.json new file mode 100644 index 000000000..99ccd4771 --- /dev/null +++ b/bots/bots_clan/xaero.json @@ -0,0 +1,35 @@ +{ + "name": "Xaero", + "clan_id": 7, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.4, + "hc": 0.7, + "sniper": -0.3, + "knives": 0.4, + "grenades": -0.0 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.9, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/zarna.json b/bots/bots_clan/zarna.json new file mode 100644 index 000000000..c233b3f7c --- /dev/null +++ b/bots/bots_clan/zarna.json @@ -0,0 +1,35 @@ +{ + "name": "Zarna", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.9, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/_nixxy_.json b/bots/bots_pub/_nixxy_.json new file mode 100644 index 000000000..960db85fd --- /dev/null +++ b/bots/bots_pub/_nixxy_.json @@ -0,0 +1,35 @@ +{ + "name": "_*NiXXy*_", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": -0.0, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/aimless.json b/bots/bots_pub/aimless.json new file mode 100644 index 000000000..9cddbd222 --- /dev/null +++ b/bots/bots_pub/aimless.json @@ -0,0 +1,35 @@ +{ + "name": "Aimless", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.1, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/anakinskywalker.json b/bots/bots_pub/anakinskywalker.json index 8d86bff58..b4b7aad21 100644 --- a/bots/bots_pub/anakinskywalker.json +++ b/bots/bots_pub/anakinskywalker.json @@ -1,7 +1,7 @@ { "name": "Anakin Skywalker", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 1.0, "dual_mk23": 0.5, diff --git a/bots/bots_pub/anarki.json b/bots/bots_pub/anarki.json new file mode 100644 index 000000000..b82882122 --- /dev/null +++ b/bots/bots_pub/anarki.json @@ -0,0 +1,35 @@ +{ + "name": "Anarki", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.9, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.1, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/andromeed.json b/bots/bots_pub/andromeed.json new file mode 100644 index 000000000..550912232 --- /dev/null +++ b/bots/bots_pub/andromeed.json @@ -0,0 +1,35 @@ +{ + "name": "Andromeed", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.2, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.1, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/angel.json b/bots/bots_pub/angel.json new file mode 100644 index 000000000..9833e55e2 --- /dev/null +++ b/bots/bots_pub/angel.json @@ -0,0 +1,35 @@ +{ + "name": "Angel", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": -0.2, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.0, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/angeloprovolone.json b/bots/bots_pub/angeloprovolone.json index fb4981f29..479426c64 100644 --- a/bots/bots_pub/angeloprovolone.json +++ b/bots/bots_pub/angeloprovolone.json @@ -1,7 +1,7 @@ { "name": "Angelo Provolone", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.8, "dual_mk23": 0.8, diff --git a/bots/bots_pub/anton.json b/bots/bots_pub/anton.json new file mode 100644 index 000000000..be84e6b7e --- /dev/null +++ b/bots/bots_pub/anton.json @@ -0,0 +1,35 @@ +{ + "name": "Anton", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.1, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.0, + "curiosity": 0.6, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 1.0, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/argo.json b/bots/bots_pub/argo.json new file mode 100644 index 000000000..fd27b1fa5 --- /dev/null +++ b/bots/bots_pub/argo.json @@ -0,0 +1,35 @@ +{ + "name": "Argo", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/arklon.json b/bots/bots_pub/arklon.json new file mode 100644 index 000000000..2cc0ed855 --- /dev/null +++ b/bots/bots_pub/arklon.json @@ -0,0 +1,35 @@ +{ + "name": "Arklon", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.1, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 1.0, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/backfire.json b/bots/bots_pub/backfire.json new file mode 100644 index 000000000..65cb0673b --- /dev/null +++ b/bots/bots_pub/backfire.json @@ -0,0 +1,35 @@ +{ + "name": "Backfire", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bacon.json b/bots/bots_pub/bacon.json new file mode 100644 index 000000000..4d0cb2ef2 --- /dev/null +++ b/bots/bots_pub/bacon.json @@ -0,0 +1,35 @@ +{ + "name": "Bacon", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.9, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.9, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/badjuju.json b/bots/bots_pub/badjuju.json new file mode 100644 index 000000000..94ed43eac --- /dev/null +++ b/bots/bots_pub/badjuju.json @@ -0,0 +1,35 @@ +{ + "name": "Bad JuJu", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": -0.0 + } +} \ No newline at end of file diff --git a/bots/bots_pub/badsam.json b/bots/bots_pub/badsam.json new file mode 100644 index 000000000..c7d69c023 --- /dev/null +++ b/bots/bots_pub/badsam.json @@ -0,0 +1,35 @@ +{ + "name": "Bad Sam", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.5, + "hc": 0.2, + "sniper": 1.0, + "knives": 0.2, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/barbarian.json b/bots/bots_pub/barbarian.json new file mode 100644 index 000000000..b0d1b6fbd --- /dev/null +++ b/bots/bots_pub/barbarian.json @@ -0,0 +1,35 @@ +{ + "name": "Barbarian", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/beelz.json b/bots/bots_pub/beelz.json new file mode 100644 index 000000000..ebe22020b --- /dev/null +++ b/bots/bots_pub/beelz.json @@ -0,0 +1,35 @@ +{ + "name": "Beelz", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 1.0, + "teamwork": 0.2, + "curiosity": 0.6, + "aimingSkill": 0.1, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bigmack.json b/bots/bots_pub/bigmack.json new file mode 100644 index 000000000..796ef5be9 --- /dev/null +++ b/bots/bots_pub/bigmack.json @@ -0,0 +1,35 @@ +{ + "name": "BigMack", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/biker.json b/bots/bots_pub/biker.json new file mode 100644 index 000000000..c64672c8c --- /dev/null +++ b/bots/bots_pub/biker.json @@ -0,0 +1,35 @@ +{ + "name": "Biker", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bishibosh.json b/bots/bots_pub/bishibosh.json new file mode 100644 index 000000000..6a18db0fc --- /dev/null +++ b/bots/bots_pub/bishibosh.json @@ -0,0 +1,35 @@ +{ + "name": "Bishibosh", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": -0.1, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/blaze.json b/bots/bots_pub/blaze.json new file mode 100644 index 000000000..79d312b88 --- /dev/null +++ b/bots/bots_pub/blaze.json @@ -0,0 +1,35 @@ +{ + "name": "Blaze", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.4, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.0, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bloodwitch.json b/bots/bots_pub/bloodwitch.json new file mode 100644 index 000000000..0b4349429 --- /dev/null +++ b/bots/bots_pub/bloodwitch.json @@ -0,0 +1,35 @@ +{ + "name": "Bloodwitch", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.9, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bolund.json b/bots/bots_pub/bolund.json new file mode 100644 index 000000000..bfe126a61 --- /dev/null +++ b/bots/bots_pub/bolund.json @@ -0,0 +1,35 @@ +{ + "name": "Bolund", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.0, + "m3": 0.3, + "hc": 0.9, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.1, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 1.0, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/boneash.json b/bots/bots_pub/boneash.json new file mode 100644 index 000000000..b3ec44e1f --- /dev/null +++ b/bots/bots_pub/boneash.json @@ -0,0 +1,35 @@ +{ + "name": "Boneash", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.0, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.0, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bonebreak.json b/bots/bots_pub/bonebreak.json new file mode 100644 index 000000000..a13476819 --- /dev/null +++ b/bots/bots_pub/bonebreak.json @@ -0,0 +1,35 @@ +{ + "name": "Bonebreak", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bortack.json b/bots/bots_pub/bortack.json new file mode 100644 index 000000000..5bdf87799 --- /dev/null +++ b/bots/bots_pub/bortack.json @@ -0,0 +1,35 @@ +{ + "name": "Bortack", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.9, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bremm.json b/bots/bots_pub/bremm.json new file mode 100644 index 000000000..c533fb3d0 --- /dev/null +++ b/bots/bots_pub/bremm.json @@ -0,0 +1,35 @@ +{ + "name": "Bremm", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/brixos.json b/bots/bots_pub/brixos.json new file mode 100644 index 000000000..7ca667cb3 --- /dev/null +++ b/bots/bots_pub/brixos.json @@ -0,0 +1,35 @@ +{ + "name": "Brixos", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.9, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/brofist.json b/bots/bots_pub/brofist.json new file mode 100644 index 000000000..0e934d4f8 --- /dev/null +++ b/bots/bots_pub/brofist.json @@ -0,0 +1,35 @@ +{ + "name": "BroFist", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.0, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/brohn.json b/bots/bots_pub/brohn.json new file mode 100644 index 000000000..0f69e64d8 --- /dev/null +++ b/bots/bots_pub/brohn.json @@ -0,0 +1,35 @@ +{ + "name": "Brohn", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/broz.json b/bots/bots_pub/broz.json new file mode 100644 index 000000000..ea33683f8 --- /dev/null +++ b/bots/bots_pub/broz.json @@ -0,0 +1,35 @@ +{ + "name": "Broz", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.1, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cadaver.json b/bots/bots_pub/cadaver.json new file mode 100644 index 000000000..8e00825f7 --- /dev/null +++ b/bots/bots_pub/cadaver.json @@ -0,0 +1,35 @@ +{ + "name": "Cadaver", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.2, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/captivandanko.json b/bots/bots_pub/captivandanko.json index cb7b113f2..08e9105c3 100644 --- a/bots/bots_pub/captivandanko.json +++ b/bots/bots_pub/captivandanko.json @@ -1,7 +1,7 @@ { "name": "Capt. Ivan Danko", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.2, "dual_mk23": 0.3, diff --git a/bots/bots_pub/carpenter.json b/bots/bots_pub/carpenter.json new file mode 100644 index 000000000..7fdbd3dc4 --- /dev/null +++ b/bots/bots_pub/carpenter.json @@ -0,0 +1,35 @@ +{ + "name": "Carpenter", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.6, + "knives": -0.2, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cetrion.json b/bots/bots_pub/cetrion.json new file mode 100644 index 000000000..f4f4f2f61 --- /dev/null +++ b/bots/bots_pub/cetrion.json @@ -0,0 +1,35 @@ +{ + "name": "Cetrion", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": -0.0, + "silencer": 0.8, + "slippers": 0.1, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/chanceboudreaux.json b/bots/bots_pub/chanceboudreaux.json index 5d59784e7..1912fc3e9 100644 --- a/bots/bots_pub/chanceboudreaux.json +++ b/bots/bots_pub/chanceboudreaux.json @@ -1,7 +1,7 @@ { "name": "Chance Boudreaux", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.3, "dual_mk23": 0.5, diff --git a/bots/bots_pub/chess.json b/bots/bots_pub/chess.json new file mode 100644 index 000000000..059bc3d2f --- /dev/null +++ b/bots/bots_pub/chess.json @@ -0,0 +1,35 @@ +{ + "name": "Chess", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/chipp_.json b/bots/bots_pub/chipp_.json new file mode 100644 index 000000000..45e83768c --- /dev/null +++ b/bots/bots_pub/chipp_.json @@ -0,0 +1,35 @@ +{ + "name": "chipp_", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/choppy.json b/bots/bots_pub/choppy.json new file mode 100644 index 000000000..647cea79a --- /dev/null +++ b/bots/bots_pub/choppy.json @@ -0,0 +1,35 @@ +{ + "name": "^^ChoppY^^", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.9, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/church.json b/bots/bots_pub/church.json new file mode 100644 index 000000000..6373dbf2d --- /dev/null +++ b/bots/bots_pub/church.json @@ -0,0 +1,35 @@ +{ + "name": "Church", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, + "slippers": 1.0, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cledus.json b/bots/bots_pub/cledus.json new file mode 100644 index 000000000..5930991e4 --- /dev/null +++ b/bots/bots_pub/cledus.json @@ -0,0 +1,35 @@ +{ + "name": "Cledus", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cobra.json b/bots/bots_pub/cobra.json new file mode 100644 index 000000000..9c84792b8 --- /dev/null +++ b/bots/bots_pub/cobra.json @@ -0,0 +1,35 @@ +{ + "name": "Cobra", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/coldworm.json b/bots/bots_pub/coldworm.json new file mode 100644 index 000000000..8f7e1eda2 --- /dev/null +++ b/bots/bots_pub/coldworm.json @@ -0,0 +1,35 @@ +{ + "name": "Coldworm", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 1.2, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 1.0, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.0 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.1, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/colt.json b/bots/bots_pub/colt.json new file mode 100644 index 000000000..39869ec49 --- /dev/null +++ b/bots/bots_pub/colt.json @@ -0,0 +1,35 @@ +{ + "name": "Colt", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.6, + "m3": 1.1, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/corpsefire.json b/bots/bots_pub/corpsefire.json new file mode 100644 index 000000000..02990cc6b --- /dev/null +++ b/bots/bots_pub/corpsefire.json @@ -0,0 +1,35 @@ +{ + "name": "Corpsefire", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.1, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/corrus.json b/bots/bots_pub/corrus.json new file mode 100644 index 000000000..199c055de --- /dev/null +++ b/bots/bots_pub/corrus.json @@ -0,0 +1,35 @@ +{ + "name": "[Corrus]", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 1.0, + "helm": -0.2, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/countess.json b/bots/bots_pub/countess.json new file mode 100644 index 000000000..5a99a5d77 --- /dev/null +++ b/bots/bots_pub/countess.json @@ -0,0 +1,35 @@ +{ + "name": "Countess", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cowchop.json b/bots/bots_pub/cowchop.json new file mode 100644 index 000000000..18b2bb15b --- /dev/null +++ b/bots/bots_pub/cowchop.json @@ -0,0 +1,35 @@ +{ + "name": "CowChop", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.6, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cowking.json b/bots/bots_pub/cowking.json new file mode 100644 index 000000000..2ee275b20 --- /dev/null +++ b/bots/bots_pub/cowking.json @@ -0,0 +1,35 @@ +{ + "name": "Cow King", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.9, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cruton.json b/bots/bots_pub/cruton.json new file mode 100644 index 000000000..f7a5f7eea --- /dev/null +++ b/bots/bots_pub/cruton.json @@ -0,0 +1,35 @@ +{ + "name": "Cruton", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.0, + "helm": 0.5, + "laser": 1.0, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cyrax.json b/bots/bots_pub/cyrax.json new file mode 100644 index 000000000..3c943472c --- /dev/null +++ b/bots/bots_pub/cyrax.json @@ -0,0 +1,35 @@ +{ + "name": "Cyrax", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 1.1, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/daegon.json b/bots/bots_pub/daegon.json new file mode 100644 index 000000000..2e55e0687 --- /dev/null +++ b/bots/bots_pub/daegon.json @@ -0,0 +1,35 @@ +{ + "name": "Daegon", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": -0.1 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.9, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/daemia.json b/bots/bots_pub/daemia.json new file mode 100644 index 000000000..46183ad9e --- /dev/null +++ b/bots/bots_pub/daemia.json @@ -0,0 +1,35 @@ +{ + "name": "Daemia", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dairou.json b/bots/bots_pub/dairou.json new file mode 100644 index 000000000..d815662f6 --- /dev/null +++ b/bots/bots_pub/dairou.json @@ -0,0 +1,35 @@ +{ + "name": "Dairou", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/darkkahn.json b/bots/bots_pub/darkkahn.json new file mode 100644 index 000000000..52546c99c --- /dev/null +++ b/bots/bots_pub/darkkahn.json @@ -0,0 +1,35 @@ +{ + "name": "Dark Kahn", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.0, + "knives": 0.4, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 1.0, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/darrius.json b/bots/bots_pub/darrius.json new file mode 100644 index 000000000..0e1345fa8 --- /dev/null +++ b/bots/bots_pub/darrius.json @@ -0,0 +1,35 @@ +{ + "name": "Darrius", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.9, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/death.json b/bots/bots_pub/death.json new file mode 100644 index 000000000..c1cc10179 --- /dev/null +++ b/bots/bots_pub/death.json @@ -0,0 +1,35 @@ +{ + "name": "Death", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.9, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dharmesh.json b/bots/bots_pub/dharmesh.json new file mode 100644 index 000000000..73bffa6f2 --- /dev/null +++ b/bots/bots_pub/dharmesh.json @@ -0,0 +1,35 @@ +{ + "name": "Dharmesh", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 1.0, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dinardo.json b/bots/bots_pub/dinardo.json new file mode 100644 index 000000000..222e3b95d --- /dev/null +++ b/bots/bots_pub/dinardo.json @@ -0,0 +1,35 @@ +{ + "name": "Di Nardo", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/disfordigger.json b/bots/bots_pub/disfordigger.json index 9f6a90ca3..1ec1b1017 100644 --- a/bots/bots_pub/disfordigger.json +++ b/bots/bots_pub/disfordigger.json @@ -1,7 +1,7 @@ { "name": "D is for Digger!", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.5, diff --git a/bots/bots_pub/doctordeathwish.json b/bots/bots_pub/doctordeathwish.json index c198d043f..8367e8992 100644 --- a/bots/bots_pub/doctordeathwish.json +++ b/bots/bots_pub/doctordeathwish.json @@ -1,7 +1,7 @@ { "name": "Doctor Deathwish", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.6, diff --git a/bots/bots_pub/dolin.json b/bots/bots_pub/dolin.json new file mode 100644 index 000000000..800be8309 --- /dev/null +++ b/bots/bots_pub/dolin.json @@ -0,0 +1,35 @@ +{ + "name": "Dolin", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/drago.json b/bots/bots_pub/drago.json new file mode 100644 index 000000000..ad944dcb1 --- /dev/null +++ b/bots/bots_pub/drago.json @@ -0,0 +1,35 @@ +{ + "name": "Drago", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dutch.json b/bots/bots_pub/dutch.json new file mode 100644 index 000000000..8264aa847 --- /dev/null +++ b/bots/bots_pub/dutch.json @@ -0,0 +1,35 @@ +{ + "name": "Dutch", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.4, + "silencer": -0.0, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/edoli.json b/bots/bots_pub/edoli.json new file mode 100644 index 000000000..695310d39 --- /dev/null +++ b/bots/bots_pub/edoli.json @@ -0,0 +1,35 @@ +{ + "name": "Edoli", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, + "sniper": -0.0, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.9, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ellanil.json b/bots/bots_pub/ellanil.json new file mode 100644 index 000000000..5f5fd1db4 --- /dev/null +++ b/bots/bots_pub/ellanil.json @@ -0,0 +1,35 @@ +{ + "name": "Ellanil", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/enforcer.json b/bots/bots_pub/enforcer.json new file mode 100644 index 000000000..3ac6529d7 --- /dev/null +++ b/bots/bots_pub/enforcer.json @@ -0,0 +1,35 @@ +{ + "name": "Enforcer", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.1, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/evo89.json b/bots/bots_pub/evo89.json new file mode 100644 index 000000000..dce93235c --- /dev/null +++ b/bots/bots_pub/evo89.json @@ -0,0 +1,35 @@ +{ + "name": "Evo89", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.5, + "knives": 1.0, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ferratorr.json b/bots/bots_pub/ferratorr.json new file mode 100644 index 000000000..38bfd02e1 --- /dev/null +++ b/bots/bots_pub/ferratorr.json @@ -0,0 +1,35 @@ +{ + "name": "Ferra/Torr", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.5, + "silencer": 1.0, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/frankdux.json b/bots/bots_pub/frankdux.json new file mode 100644 index 000000000..b07293c82 --- /dev/null +++ b/bots/bots_pub/frankdux.json @@ -0,0 +1,35 @@ +{ + "name": "Frank Dux", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.1, + "silencer": 0.3, + "slippers": 0.1, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/fred.json b/bots/bots_pub/fred.json new file mode 100644 index 000000000..2a8648e47 --- /dev/null +++ b/bots/bots_pub/fred.json @@ -0,0 +1,35 @@ +{ + "name": "Fred", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.1, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/frenchy.json b/bots/bots_pub/frenchy.json new file mode 100644 index 000000000..51bde7d77 --- /dev/null +++ b/bots/bots_pub/frenchy.json @@ -0,0 +1,35 @@ +{ + "name": "Frenchy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/fujin.json b/bots/bots_pub/fujin.json new file mode 100644 index 000000000..0283f0152 --- /dev/null +++ b/bots/bots_pub/fujin.json @@ -0,0 +1,35 @@ +{ + "name": "Fujin", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/fxguy.json b/bots/bots_pub/fxguy.json new file mode 100644 index 000000000..bb396f067 --- /dev/null +++ b/bots/bots_pub/fxguy.json @@ -0,0 +1,35 @@ +{ + "name": "FXguy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 1.0, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/galul.json b/bots/bots_pub/galul.json new file mode 100644 index 000000000..143b844f1 --- /dev/null +++ b/bots/bots_pub/galul.json @@ -0,0 +1,35 @@ +{ + "name": "Galul", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.1, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/garbok.json b/bots/bots_pub/garbok.json new file mode 100644 index 000000000..78281b609 --- /dev/null +++ b/bots/bots_pub/garbok.json @@ -0,0 +1,35 @@ +{ + "name": "Garbok", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.1, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 1.1, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/geleb.json b/bots/bots_pub/geleb.json new file mode 100644 index 000000000..9a3b62b65 --- /dev/null +++ b/bots/bots_pub/geleb.json @@ -0,0 +1,35 @@ +{ + "name": "Geleb", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.1, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/george.json b/bots/bots_pub/george.json new file mode 100644 index 000000000..2615325be --- /dev/null +++ b/bots/bots_pub/george.json @@ -0,0 +1,35 @@ +{ + "name": "George", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 1.0 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ghostbl4de.json b/bots/bots_pub/ghostbl4de.json new file mode 100644 index 000000000..4e83b6572 --- /dev/null +++ b/bots/bots_pub/ghostbl4de.json @@ -0,0 +1,35 @@ +{ + "name": "GhOstBl4de", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.0 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 1.0, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gigavoltz.json b/bots/bots_pub/gigavoltz.json new file mode 100644 index 000000000..16a9d4757 --- /dev/null +++ b/bots/bots_pub/gigavoltz.json @@ -0,0 +1,35 @@ +{ + "name": "Gigavoltz", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 1.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gordac.json b/bots/bots_pub/gordac.json new file mode 100644 index 000000000..307396a12 --- /dev/null +++ b/bots/bots_pub/gordac.json @@ -0,0 +1,35 @@ +{ + "name": "Gordac", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.9, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.0 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/grimm.json b/bots/bots_pub/grimm.json new file mode 100644 index 000000000..6ce9860c3 --- /dev/null +++ b/bots/bots_pub/grimm.json @@ -0,0 +1,35 @@ +{ + "name": "Grimm", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.3, + "silencer": -0.1, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": -0.1, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/grog.json b/bots/bots_pub/grog.json new file mode 100644 index 000000000..84ef96798 --- /dev/null +++ b/bots/bots_pub/grog.json @@ -0,0 +1,35 @@ +{ + "name": "Grog", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.7, + "hc": 0.1, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.0, + "laser": 0.8, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gungho.json b/bots/bots_pub/gungho.json new file mode 100644 index 000000000..0485a2ac3 --- /dev/null +++ b/bots/bots_pub/gungho.json @@ -0,0 +1,35 @@ +{ + "name": "Gung Ho", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hal9000.json b/bots/bots_pub/hal9000.json new file mode 100644 index 000000000..526692e31 --- /dev/null +++ b/bots/bots_pub/hal9000.json @@ -0,0 +1,35 @@ +{ + "name": "HAL-9000", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.0 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.0, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/halfbaked.json b/bots/bots_pub/halfbaked.json new file mode 100644 index 000000000..b373a70ac --- /dev/null +++ b/bots/bots_pub/halfbaked.json @@ -0,0 +1,35 @@ +{ + "name": "Halfbaked", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.1, + "laser": 0.3, + "silencer": 0.9, + "slippers": 0.7, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/halfwit.json b/bots/bots_pub/halfwit.json new file mode 100644 index 000000000..d139949a6 --- /dev/null +++ b/bots/bots_pub/halfwit.json @@ -0,0 +1,35 @@ +{ + "name": "Halfwit", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 1.0 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.1, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hambone.json b/bots/bots_pub/hambone.json new file mode 100644 index 000000000..96f09c739 --- /dev/null +++ b/bots/bots_pub/hambone.json @@ -0,0 +1,35 @@ +{ + "name": "Hambone", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.1, + "aimingSkill": 0.4, + "reactionTime": 1.0, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/havik.json b/bots/bots_pub/havik.json new file mode 100644 index 000000000..428e0f3ad --- /dev/null +++ b/bots/bots_pub/havik.json @@ -0,0 +1,35 @@ +{ + "name": "Havik", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hecker.json b/bots/bots_pub/hecker.json new file mode 100644 index 000000000..5f1fc18ef --- /dev/null +++ b/bots/bots_pub/hecker.json @@ -0,0 +1,35 @@ +{ + "name": "Hecker", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.1, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.1, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/heljay.json b/bots/bots_pub/heljay.json new file mode 100644 index 000000000..bed4c2383 --- /dev/null +++ b/bots/bots_pub/heljay.json @@ -0,0 +1,35 @@ +{ + "name": "Heljay", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.0, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.0, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hellhound.json b/bots/bots_pub/hellhound.json new file mode 100644 index 000000000..670387ff2 --- /dev/null +++ b/bots/bots_pub/hellhound.json @@ -0,0 +1,35 @@ +{ + "name": "Hellhound", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/himself.json b/bots/bots_pub/himself.json new file mode 100644 index 000000000..8834c30b2 --- /dev/null +++ b/bots/bots_pub/himself.json @@ -0,0 +1,35 @@ +{ + "name": "Himself", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hozef8.json b/bots/bots_pub/hozef8.json new file mode 100644 index 000000000..7866d7a13 --- /dev/null +++ b/bots/bots_pub/hozef8.json @@ -0,0 +1,35 @@ +{ + "name": "hozef-8", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.1, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/imentu.json b/bots/bots_pub/imentu.json new file mode 100644 index 000000000..32cd96ba9 --- /dev/null +++ b/bots/bots_pub/imentu.json @@ -0,0 +1,35 @@ +{ + "name": "Imentu", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.4, + "hc": 1.1, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/itixoid.json b/bots/bots_pub/itixoid.json new file mode 100644 index 000000000..f2ef97584 --- /dev/null +++ b/bots/bots_pub/itixoid.json @@ -0,0 +1,35 @@ +{ + "name": "itiXOID", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.8, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.1, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 1.0, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ivandrago.json b/bots/bots_pub/ivandrago.json new file mode 100644 index 000000000..87f522564 --- /dev/null +++ b/bots/bots_pub/ivandrago.json @@ -0,0 +1,35 @@ +{ + "name": "Ivan Drago", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.1, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.8, + "knives": 0.8, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ivankraschinsky.json b/bots/bots_pub/ivankraschinsky.json index 5b28f6f5c..50f50594f 100644 --- a/bots/bots_pub/ivankraschinsky.json +++ b/bots/bots_pub/ivankraschinsky.json @@ -1,7 +1,7 @@ { "name": "Ivan Kraschinsky", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.3, "dual_mk23": 0.6, diff --git a/bots/bots_pub/jacqueskristoff.json b/bots/bots_pub/jacqueskristoff.json index 066edd287..20e7e043c 100644 --- a/bots/bots_pub/jacqueskristoff.json +++ b/bots/bots_pub/jacqueskristoff.json @@ -1,7 +1,7 @@ { "name": "Jacques Kristoff", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.7, diff --git a/bots/bots_pub/jarek.json b/bots/bots_pub/jarek.json new file mode 100644 index 000000000..60d8b76aa --- /dev/null +++ b/bots/bots_pub/jarek.json @@ -0,0 +1,35 @@ +{ + "name": "Jarek", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jax.json b/bots/bots_pub/jax.json new file mode 100644 index 000000000..1cb37b1c7 --- /dev/null +++ b/bots/bots_pub/jax.json @@ -0,0 +1,35 @@ +{ + "name": "Jax", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.8, + "knives": -0.0, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.8, + "slippers": 1.0, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jcdenton.json b/bots/bots_pub/jcdenton.json new file mode 100644 index 000000000..993e50535 --- /dev/null +++ b/bots/bots_pub/jcdenton.json @@ -0,0 +1,35 @@ +{ + "name": "JC Denton", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.1, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jdgold.json b/bots/bots_pub/jdgold.json new file mode 100644 index 000000000..e6c821a92 --- /dev/null +++ b/bots/bots_pub/jdgold.json @@ -0,0 +1,35 @@ +{ + "name": "JD Gold", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.0, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jinx.json b/bots/bots_pub/jinx.json new file mode 100644 index 000000000..cf897e2b2 --- /dev/null +++ b/bots/bots_pub/jinx.json @@ -0,0 +1,35 @@ +{ + "name": "Jinx", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jjmcquade.json b/bots/bots_pub/jjmcquade.json new file mode 100644 index 000000000..623726a33 --- /dev/null +++ b/bots/bots_pub/jjmcquade.json @@ -0,0 +1,35 @@ +{ + "name": "JJ McQuade", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/joce.json b/bots/bots_pub/joce.json new file mode 100644 index 000000000..204927846 --- /dev/null +++ b/bots/bots_pub/joce.json @@ -0,0 +1,35 @@ +{ + "name": "Joce", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/joejoiner.json b/bots/bots_pub/joejoiner.json new file mode 100644 index 000000000..351eed738 --- /dev/null +++ b/bots/bots_pub/joejoiner.json @@ -0,0 +1,35 @@ +{ + "name": "Joe Joiner", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/johnrambo.json b/bots/bots_pub/johnrambo.json new file mode 100644 index 000000000..3164be6b0 --- /dev/null +++ b/bots/bots_pub/johnrambo.json @@ -0,0 +1,35 @@ +{ + "name": "John Rambo", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kaa.json b/bots/bots_pub/kaa.json new file mode 100644 index 000000000..ddc6fb47f --- /dev/null +++ b/bots/bots_pub/kaa.json @@ -0,0 +1,35 @@ +{ + "name": "Kaa", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kabal.json b/bots/bots_pub/kabal.json new file mode 100644 index 000000000..1abdaf84e --- /dev/null +++ b/bots/bots_pub/kabal.json @@ -0,0 +1,35 @@ +{ + "name": "Kabal", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.0, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kai.json b/bots/bots_pub/kai.json new file mode 100644 index 000000000..96b5a61d7 --- /dev/null +++ b/bots/bots_pub/kai.json @@ -0,0 +1,35 @@ +{ + "name": "Kai", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kano.json b/bots/bots_pub/kano.json new file mode 100644 index 000000000..b351301af --- /dev/null +++ b/bots/bots_pub/kano.json @@ -0,0 +1,35 @@ +{ + "name": "Kano", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.1, + "laser": 0.0, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.9, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kenshi.json b/bots/bots_pub/kenshi.json new file mode 100644 index 000000000..a5f41d023 --- /dev/null +++ b/bots/bots_pub/kenshi.json @@ -0,0 +1,35 @@ +{ + "name": "Kenshi", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 1.0, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/khameleon.json b/bots/bots_pub/khameleon.json new file mode 100644 index 000000000..229351701 --- /dev/null +++ b/bots/bots_pub/khameleon.json @@ -0,0 +1,35 @@ +{ + "name": "Khameleon", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.6, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kippy.json b/bots/bots_pub/kippy.json new file mode 100644 index 000000000..98101319f --- /dev/null +++ b/bots/bots_pub/kippy.json @@ -0,0 +1,35 @@ +{ + "name": "Kippy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.9, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.1, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kira.json b/bots/bots_pub/kira.json new file mode 100644 index 000000000..fee7249e6 --- /dev/null +++ b/bots/bots_pub/kira.json @@ -0,0 +1,35 @@ +{ + "name": "Kira", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.4, + "hc": -0.0, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 1.0, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 1.0, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kitlatura.json b/bots/bots_pub/kitlatura.json new file mode 100644 index 000000000..abd781e6e --- /dev/null +++ b/bots/bots_pub/kitlatura.json @@ -0,0 +1,35 @@ +{ + "name": "Kit Latura", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.1, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kobra.json b/bots/bots_pub/kobra.json new file mode 100644 index 000000000..47fa83843 --- /dev/null +++ b/bots/bots_pub/kobra.json @@ -0,0 +1,35 @@ +{ + "name": "Kobra", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kole.json b/bots/bots_pub/kole.json new file mode 100644 index 000000000..f05d51455 --- /dev/null +++ b/bots/bots_pub/kole.json @@ -0,0 +1,35 @@ +{ + "name": "Kole", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 1.0, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.1, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/korv.json b/bots/bots_pub/korv.json new file mode 100644 index 000000000..b308b85ed --- /dev/null +++ b/bots/bots_pub/korv.json @@ -0,0 +1,35 @@ +{ + "name": "Korv", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kotalkahn.json b/bots/bots_pub/kotalkahn.json new file mode 100644 index 000000000..338e522ef --- /dev/null +++ b/bots/bots_pub/kotalkahn.json @@ -0,0 +1,35 @@ +{ + "name": "Kotal Kahn", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.1, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.2, + "communicationFreq": 0.2, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kungjin.json b/bots/bots_pub/kungjin.json new file mode 100644 index 000000000..09cfd7bab --- /dev/null +++ b/bots/bots_pub/kungjin.json @@ -0,0 +1,35 @@ +{ + "name": "Kung Jin", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.9, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/leatherarm.json b/bots/bots_pub/leatherarm.json new file mode 100644 index 000000000..707f4c09e --- /dev/null +++ b/bots/bots_pub/leatherarm.json @@ -0,0 +1,35 @@ +{ + "name": "Leatherarm", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.1, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.1, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/lemmie.json b/bots/bots_pub/lemmie.json new file mode 100644 index 000000000..6f2f11008 --- /dev/null +++ b/bots/bots_pub/lemmie.json @@ -0,0 +1,35 @@ +{ + "name": "Lemmie", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": -0.1, + "aimingSkill": 0.3, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/littleg.json b/bots/bots_pub/littleg.json new file mode 100644 index 000000000..06e5bd328 --- /dev/null +++ b/bots/bots_pub/littleg.json @@ -0,0 +1,35 @@ +{ + "name": "Little G", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.0, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ltraymondtango.json b/bots/bots_pub/ltraymondtango.json index 3421ea564..7bc9bf970 100644 --- a/bots/bots_pub/ltraymondtango.json +++ b/bots/bots_pub/ltraymondtango.json @@ -1,7 +1,7 @@ { "name": "Lt Raymond Tango", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.5, "dual_mk23": 1.0, diff --git a/bots/bots_pub/lucy.json b/bots/bots_pub/lucy.json new file mode 100644 index 000000000..389e37fde --- /dev/null +++ b/bots/bots_pub/lucy.json @@ -0,0 +1,35 @@ +{ + "name": "Lucy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/major.json b/bots/bots_pub/major.json new file mode 100644 index 000000000..84097ce15 --- /dev/null +++ b/bots/bots_pub/major.json @@ -0,0 +1,35 @@ +{ + "name": "Major", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.8, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/majorpain.json b/bots/bots_pub/majorpain.json new file mode 100644 index 000000000..4e70adfbf --- /dev/null +++ b/bots/bots_pub/majorpain.json @@ -0,0 +1,35 @@ +{ + "name": "Major Pain", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.9, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": -0.0, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/makron.json b/bots/bots_pub/makron.json new file mode 100644 index 000000000..b90082244 --- /dev/null +++ b/bots/bots_pub/makron.json @@ -0,0 +1,35 @@ +{ + "name": "Makron", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.6, + "knives": -0.1, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.9, + "slippers": 0.5, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 1.0, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mckenna.json b/bots/bots_pub/mckenna.json new file mode 100644 index 000000000..a4413f8a3 --- /dev/null +++ b/bots/bots_pub/mckenna.json @@ -0,0 +1,35 @@ +{ + "name": "McKenna", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/merki.json b/bots/bots_pub/merki.json new file mode 100644 index 000000000..756ab6244 --- /dev/null +++ b/bots/bots_pub/merki.json @@ -0,0 +1,35 @@ +{ + "name": "Merki", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.1, + "movementStyle": 0.3, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_pub/michael.json b/bots/bots_pub/michael.json new file mode 100644 index 000000000..74d74dc90 --- /dev/null +++ b/bots/bots_pub/michael.json @@ -0,0 +1,35 @@ +{ + "name": "Michael", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.3, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mileena.json b/bots/bots_pub/mileena.json new file mode 100644 index 000000000..23e8c0530 --- /dev/null +++ b/bots/bots_pub/mileena.json @@ -0,0 +1,35 @@ +{ + "name": "Mileena", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/milius.json b/bots/bots_pub/milius.json new file mode 100644 index 000000000..5586d0e1a --- /dev/null +++ b/bots/bots_pub/milius.json @@ -0,0 +1,35 @@ +{ + "name": "Milius", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.2, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 1.0, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.9, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mincer.json b/bots/bots_pub/mincer.json new file mode 100644 index 000000000..227203e18 --- /dev/null +++ b/bots/bots_pub/mincer.json @@ -0,0 +1,35 @@ +{ + "name": "Mincer", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/moloch.json b/bots/bots_pub/moloch.json new file mode 100644 index 000000000..cb59121a6 --- /dev/null +++ b/bots/bots_pub/moloch.json @@ -0,0 +1,35 @@ +{ + "name": "Moloch", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.9, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mongrel.json b/bots/bots_pub/mongrel.json new file mode 100644 index 000000000..408f6242f --- /dev/null +++ b/bots/bots_pub/mongrel.json @@ -0,0 +1,35 @@ +{ + "name": "Mongrel", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/morgriff.json b/bots/bots_pub/morgriff.json new file mode 100644 index 000000000..5c594c6ed --- /dev/null +++ b/bots/bots_pub/morgriff.json @@ -0,0 +1,35 @@ +{ + "name": "Morgriff", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/motaro.json b/bots/bots_pub/motaro.json new file mode 100644 index 000000000..11f065538 --- /dev/null +++ b/bots/bots_pub/motaro.json @@ -0,0 +1,35 @@ +{ + "name": "Motaro", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.6, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 1.0, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mrsuave.json b/bots/bots_pub/mrsuave.json new file mode 100644 index 000000000..eac051ed0 --- /dev/null +++ b/bots/bots_pub/mrsuave.json @@ -0,0 +1,35 @@ +{ + "name": "Mr Suave", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.7, + "knives": 1.0, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/newbie.json b/bots/bots_pub/newbie.json new file mode 100644 index 000000000..ea170210d --- /dev/null +++ b/bots/bots_pub/newbie.json @@ -0,0 +1,35 @@ +{ + "name": "Newbie", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.3, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nibbler.json b/bots/bots_pub/nibbler.json new file mode 100644 index 000000000..7dc7505db --- /dev/null +++ b/bots/bots_pub/nibbler.json @@ -0,0 +1,35 @@ +{ + "name": "Nibbler", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.1, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nickgunar.json b/bots/bots_pub/nickgunar.json new file mode 100644 index 000000000..3c2d3aac7 --- /dev/null +++ b/bots/bots_pub/nickgunar.json @@ -0,0 +1,35 @@ +{ + "name": "Nick Gunar", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.1, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nihlathak.json b/bots/bots_pub/nihlathak.json new file mode 100644 index 000000000..513d45ee7 --- /dev/null +++ b/bots/bots_pub/nihlathak.json @@ -0,0 +1,35 @@ +{ + "name": "Nihlathak", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": -0.1, + "laser": 0.5, + "silencer": 0.1, + "slippers": 0.7, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nikolaicherenko.json b/bots/bots_pub/nikolaicherenko.json index 10429a953..2026f63d0 100644 --- a/bots/bots_pub/nikolaicherenko.json +++ b/bots/bots_pub/nikolaicherenko.json @@ -1,7 +1,7 @@ { "name": "Nikolai Cherenko", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.8, diff --git a/bots/bots_pub/noammo.json b/bots/bots_pub/noammo.json new file mode 100644 index 000000000..2278e66d2 --- /dev/null +++ b/bots/bots_pub/noammo.json @@ -0,0 +1,35 @@ +{ + "name": "NoAmmo", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.8, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 1.1, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nohope.json b/bots/bots_pub/nohope.json new file mode 100644 index 000000000..b024d6af0 --- /dev/null +++ b/bots/bots_pub/nohope.json @@ -0,0 +1,35 @@ +{ + "name": "Nohope", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nundak.json b/bots/bots_pub/nundak.json new file mode 100644 index 000000000..9a5384066 --- /dev/null +++ b/bots/bots_pub/nundak.json @@ -0,0 +1,35 @@ +{ + "name": "Nundak", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/onaga.json b/bots/bots_pub/onaga.json new file mode 100644 index 000000000..c7ab8c38e --- /dev/null +++ b/bots/bots_pub/onaga.json @@ -0,0 +1,35 @@ +{ + "name": "Onaga", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.9, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/oysterhead.json b/bots/bots_pub/oysterhead.json new file mode 100644 index 000000000..27b65e339 --- /dev/null +++ b/bots/bots_pub/oysterhead.json @@ -0,0 +1,35 @@ +{ + "name": "Oysterhead", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.0, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.2, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/phobos.json b/bots/bots_pub/phobos.json new file mode 100644 index 000000000..ddcddd364 --- /dev/null +++ b/bots/bots_pub/phobos.json @@ -0,0 +1,35 @@ +{ + "name": "Phobos", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.8, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/pindleskin.json b/bots/bots_pub/pindleskin.json new file mode 100644 index 000000000..1ec692d84 --- /dev/null +++ b/bots/bots_pub/pindleskin.json @@ -0,0 +1,35 @@ +{ + "name": "Pindleskin", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.7, + "hc": 1.1, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.0, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/pitspawnfouldog.json b/bots/bots_pub/pitspawnfouldog.json index 38f2c0131..670eeb5f3 100644 --- a/bots/bots_pub/pitspawnfouldog.json +++ b/bots/bots_pub/pitspawnfouldog.json @@ -1,7 +1,7 @@ { "name": "Pitspawn Fouldog", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.8, "dual_mk23": 0.6, diff --git a/bots/bots_pub/quanchi.json b/bots/bots_pub/quanchi.json new file mode 100644 index 000000000..05f92f03a --- /dev/null +++ b/bots/bots_pub/quanchi.json @@ -0,0 +1,35 @@ +{ + "name": "Quan Chi", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/raiden.json b/bots/bots_pub/raiden.json new file mode 100644 index 000000000..8fd0a09f7 --- /dev/null +++ b/bots/bots_pub/raiden.json @@ -0,0 +1,35 @@ +{ + "name": "Raiden", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/rain.json b/bots/bots_pub/rain.json new file mode 100644 index 000000000..ca66b1781 --- /dev/null +++ b/bots/bots_pub/rain.json @@ -0,0 +1,35 @@ +{ + "name": "Rain", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.8, + "m3": 1.0, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.0, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ranger.json b/bots/bots_pub/ranger.json new file mode 100644 index 000000000..d1865252e --- /dev/null +++ b/bots/bots_pub/ranger.json @@ -0,0 +1,35 @@ +{ + "name": "Ranger", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 1.1, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/rayquick.json b/bots/bots_pub/rayquick.json new file mode 100644 index 000000000..a6b8083e8 --- /dev/null +++ b/bots/bots_pub/rayquick.json @@ -0,0 +1,35 @@ +{ + "name": "Ray Quick", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": -0.0, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/razor.json b/bots/bots_pub/razor.json new file mode 100644 index 000000000..01afa9661 --- /dev/null +++ b/bots/bots_pub/razor.json @@ -0,0 +1,35 @@ +{ + "name": "Razor", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 1.1 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/reddog.json b/bots/bots_pub/reddog.json new file mode 100644 index 000000000..4380779fa --- /dev/null +++ b/bots/bots_pub/reddog.json @@ -0,0 +1,35 @@ +{ + "name": "Reddog", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 1.1, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/reptile.json b/bots/bots_pub/reptile.json new file mode 100644 index 000000000..0eb6d00c9 --- /dev/null +++ b/bots/bots_pub/reptile.json @@ -0,0 +1,35 @@ +{ + "name": "Reptile", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.1, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/richard.json b/bots/bots_pub/richard.json new file mode 100644 index 000000000..9b36327f5 --- /dev/null +++ b/bots/bots_pub/richard.json @@ -0,0 +1,35 @@ +{ + "name": "Richard", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.1, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/riftwraith.json b/bots/bots_pub/riftwraith.json new file mode 100644 index 000000000..0839f1362 --- /dev/null +++ b/bots/bots_pub/riftwraith.json @@ -0,0 +1,35 @@ +{ + "name": "Riftwraith", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.0, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/rocky.json b/bots/bots_pub/rocky.json new file mode 100644 index 000000000..64e1f9e75 --- /dev/null +++ b/bots/bots_pub/rocky.json @@ -0,0 +1,35 @@ +{ + "name": "Rocky", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.3, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/royen.json b/bots/bots_pub/royen.json new file mode 100644 index 000000000..3ac50f04e --- /dev/null +++ b/bots/bots_pub/royen.json @@ -0,0 +1,35 @@ +{ + "name": "Royen", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ryder.json b/bots/bots_pub/ryder.json new file mode 100644 index 000000000..db92d6d1b --- /dev/null +++ b/bots/bots_pub/ryder.json @@ -0,0 +1,35 @@ +{ + "name": "Ryder", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.9, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ryver.json b/bots/bots_pub/ryver.json new file mode 100644 index 000000000..8e220a360 --- /dev/null +++ b/bots/bots_pub/ryver.json @@ -0,0 +1,35 @@ +{ + "name": "Ryver", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/saboy.json b/bots/bots_pub/saboy.json new file mode 100644 index 000000000..62e6468f7 --- /dev/null +++ b/bots/bots_pub/saboy.json @@ -0,0 +1,35 @@ +{ + "name": "Saboy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 1.0, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.2, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/samdecker.json b/bots/bots_pub/samdecker.json new file mode 100644 index 000000000..dd9d31aae --- /dev/null +++ b/bots/bots_pub/samdecker.json @@ -0,0 +1,35 @@ +{ + "name": "Sam Decker", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": -0.1, + "helm": 1.1, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/sareena.json b/bots/bots_pub/sareena.json new file mode 100644 index 000000000..842a65253 --- /dev/null +++ b/bots/bots_pub/sareena.json @@ -0,0 +1,35 @@ +{ + "name": "Sareena", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.6, + "laser": 0.5, + "silencer": -0.0, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/sgtcappa.json b/bots/bots_pub/sgtcappa.json new file mode 100644 index 000000000..61c2b1aa4 --- /dev/null +++ b/bots/bots_pub/sgtcappa.json @@ -0,0 +1,35 @@ +{ + "name": "SGT CAPPA", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/shaokahn.json b/bots/bots_pub/shaokahn.json new file mode 100644 index 000000000..3aecd8276 --- /dev/null +++ b/bots/bots_pub/shaokahn.json @@ -0,0 +1,35 @@ +{ + "name": "Shao Kahn", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 1.0, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/shujinko.json b/bots/bots_pub/shujinko.json new file mode 100644 index 000000000..a1a1118a7 --- /dev/null +++ b/bots/bots_pub/shujinko.json @@ -0,0 +1,35 @@ +{ + "name": "Shujinko", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.9, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/siege.json b/bots/bots_pub/siege.json new file mode 100644 index 000000000..f11436ffb --- /dev/null +++ b/bots/bots_pub/siege.json @@ -0,0 +1,35 @@ +{ + "name": "Siege", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.1, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/sigil.json b/bots/bots_pub/sigil.json new file mode 100644 index 000000000..c52fc8b32 --- /dev/null +++ b/bots/bots_pub/sigil.json @@ -0,0 +1,35 @@ +{ + "name": "Sigil", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.2, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 1.1, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/skarlet.json b/bots/bots_pub/skarlet.json new file mode 100644 index 000000000..cc4e0ccec --- /dev/null +++ b/bots/bots_pub/skarlet.json @@ -0,0 +1,35 @@ +{ + "name": "Skarlet", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.2, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.9, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 1.0, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/skorgan.json b/bots/bots_pub/skorgan.json new file mode 100644 index 000000000..85928ba0a --- /dev/null +++ b/bots/bots_pub/skorgan.json @@ -0,0 +1,35 @@ +{ + "name": "Skorgan", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/slice.json b/bots/bots_pub/slice.json new file mode 100644 index 000000000..844548f95 --- /dev/null +++ b/bots/bots_pub/slice.json @@ -0,0 +1,35 @@ +{ + "name": "Slice", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.8, + "silencer": 1.1, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/smithy.json b/bots/bots_pub/smithy.json new file mode 100644 index 000000000..c6a01b645 --- /dev/null +++ b/bots/bots_pub/smithy.json @@ -0,0 +1,35 @@ +{ + "name": "Smithy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.9, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/smoke.json b/bots/bots_pub/smoke.json new file mode 100644 index 000000000..2b35213f1 --- /dev/null +++ b/bots/bots_pub/smoke.json @@ -0,0 +1,35 @@ +{ + "name": "Smoke", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/snapchipshatter.json b/bots/bots_pub/snapchipshatter.json index 0806e4b66..30ced28e8 100644 --- a/bots/bots_pub/snapchipshatter.json +++ b/bots/bots_pub/snapchipshatter.json @@ -1,7 +1,7 @@ { "name": "Snapchip Shatter", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.7, "dual_mk23": 0.8, diff --git a/bots/bots_pub/snaps.json b/bots/bots_pub/snaps.json new file mode 100644 index 000000000..a572ee056 --- /dev/null +++ b/bots/bots_pub/snaps.json @@ -0,0 +1,35 @@ +{ + "name": "Snaps", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/socks.json b/bots/bots_pub/socks.json new file mode 100644 index 000000000..1c1f5411e --- /dev/null +++ b/bots/bots_pub/socks.json @@ -0,0 +1,35 @@ +{ + "name": "Socks", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/spar.json b/bots/bots_pub/spar.json new file mode 100644 index 000000000..ce7dc229b --- /dev/null +++ b/bots/bots_pub/spar.json @@ -0,0 +1,35 @@ +{ + "name": "Spar", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.1, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.9, + "silencer": -0.1, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stripe.json b/bots/bots_pub/stripe.json new file mode 100644 index 000000000..ebb3b1bf1 --- /dev/null +++ b/bots/bots_pub/stripe.json @@ -0,0 +1,35 @@ +{ + "name": "Stripe", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.1, + "silencer": 0.7, + "slippers": 0.9, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.0, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stryker.json b/bots/bots_pub/stryker.json new file mode 100644 index 000000000..4e214badf --- /dev/null +++ b/bots/bots_pub/stryker.json @@ -0,0 +1,35 @@ +{ + "name": "Stryker", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stud.json b/bots/bots_pub/stud.json new file mode 100644 index 000000000..6612c790a --- /dev/null +++ b/bots/bots_pub/stud.json @@ -0,0 +1,35 @@ +{ + "name": "Stud", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.1, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/takedatakahashi.json b/bots/bots_pub/takedatakahashi.json index 6dd584341..4360493f7 100644 --- a/bots/bots_pub/takedatakahashi.json +++ b/bots/bots_pub/takedatakahashi.json @@ -1,7 +1,7 @@ { "name": "Takeda Takahashi", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.4, "dual_mk23": 0.6, diff --git a/bots/bots_pub/tankjr.json b/bots/bots_pub/tankjr.json new file mode 100644 index 000000000..fdb420949 --- /dev/null +++ b/bots/bots_pub/tankjr.json @@ -0,0 +1,35 @@ +{ + "name": "Tank Jr", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.7, + "laser": 0.1, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.9, + "aimingSkill": 0.1, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tanz.json b/bots/bots_pub/tanz.json new file mode 100644 index 000000000..a8b103be8 --- /dev/null +++ b/bots/bots_pub/tanz.json @@ -0,0 +1,35 @@ +{ + "name": "Tanz", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.1, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tao.json b/bots/bots_pub/tao.json new file mode 100644 index 000000000..29233927d --- /dev/null +++ b/bots/bots_pub/tao.json @@ -0,0 +1,35 @@ +{ + "name": "Tao", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, + "knives": 1.0, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tarnen.json b/bots/bots_pub/tarnen.json new file mode 100644 index 000000000..b3fabdb73 --- /dev/null +++ b/bots/bots_pub/tarnen.json @@ -0,0 +1,35 @@ +{ + "name": "Tarnen", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.1, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": -0.0, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tarnok.json b/bots/bots_pub/tarnok.json new file mode 100644 index 000000000..94765fa54 --- /dev/null +++ b/bots/bots_pub/tarnok.json @@ -0,0 +1,35 @@ +{ + "name": "Tarnok", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.9, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tatu.json b/bots/bots_pub/tatu.json new file mode 100644 index 000000000..9beab37dc --- /dev/null +++ b/bots/bots_pub/tatu.json @@ -0,0 +1,35 @@ +{ + "name": "tatu", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.9, + "communicationTone": 0.2, + "movementStyle": 0.1, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/technician.json b/bots/bots_pub/technician.json new file mode 100644 index 000000000..c164bb44c --- /dev/null +++ b/bots/bots_pub/technician.json @@ -0,0 +1,35 @@ +{ + "name": "Technician", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/terminator.json b/bots/bots_pub/terminator.json new file mode 100644 index 000000000..17a295a14 --- /dev/null +++ b/bots/bots_pub/terminator.json @@ -0,0 +1,35 @@ +{ + "name": "Terminator", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.1, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.1, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/thalath.json b/bots/bots_pub/thalath.json new file mode 100644 index 000000000..a99991830 --- /dev/null +++ b/bots/bots_pub/thalath.json @@ -0,0 +1,35 @@ +{ + "name": "Thalath", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": -0.0, + "aimingSkill": 0.6, + "reactionTime": 0.0, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/thedean.json b/bots/bots_pub/thedean.json new file mode 100644 index 000000000..6db2c0f3d --- /dev/null +++ b/bots/bots_pub/thedean.json @@ -0,0 +1,35 @@ +{ + "name": "The Dean", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.8, + "hc": 1.0, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/theeraser.json b/bots/bots_pub/theeraser.json new file mode 100644 index 000000000..94fb7db8e --- /dev/null +++ b/bots/bots_pub/theeraser.json @@ -0,0 +1,35 @@ +{ + "name": "The Eraser", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/thegerman.json b/bots/bots_pub/thegerman.json new file mode 100644 index 000000000..8ac37bc1b --- /dev/null +++ b/bots/bots_pub/thegerman.json @@ -0,0 +1,35 @@ +{ + "name": "The German", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.9, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/theonly.json b/bots/bots_pub/theonly.json new file mode 100644 index 000000000..758a76a2f --- /dev/null +++ b/bots/bots_pub/theonly.json @@ -0,0 +1,35 @@ +{ + "name": "The Only", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 1.0, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/thetulip.json b/bots/bots_pub/thetulip.json new file mode 100644 index 000000000..5e5d0cac0 --- /dev/null +++ b/bots/bots_pub/thetulip.json @@ -0,0 +1,35 @@ +{ + "name": "The Tulip", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.1, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.4, + "hc": 0.9, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": -0.0, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/theway.json b/bots/bots_pub/theway.json new file mode 100644 index 000000000..5f62374a3 --- /dev/null +++ b/bots/bots_pub/theway.json @@ -0,0 +1,35 @@ +{ + "name": "TheWay", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.9, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.4, + "slippers": 1.0, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tiano.json b/bots/bots_pub/tiano.json new file mode 100644 index 000000000..e6b3c5379 --- /dev/null +++ b/bots/bots_pub/tiano.json @@ -0,0 +1,35 @@ +{ + "name": "Tiano", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.0, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tormentor.json b/bots/bots_pub/tormentor.json new file mode 100644 index 000000000..ab652eee6 --- /dev/null +++ b/bots/bots_pub/tormentor.json @@ -0,0 +1,35 @@ +{ + "name": "Tormentor", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.9, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/triborg.json b/bots/bots_pub/triborg.json new file mode 100644 index 000000000..24d39ba20 --- /dev/null +++ b/bots/bots_pub/triborg.json @@ -0,0 +1,35 @@ +{ + "name": "Triborg", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/turkish.json b/bots/bots_pub/turkish.json new file mode 100644 index 000000000..7eb552140 --- /dev/null +++ b/bots/bots_pub/turkish.json @@ -0,0 +1,35 @@ +{ + "name": "Turkish", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vanoss.json b/bots/bots_pub/vanoss.json new file mode 100644 index 000000000..3ec0e0fe5 --- /dev/null +++ b/bots/bots_pub/vanoss.json @@ -0,0 +1,35 @@ +{ + "name": "Vanoss", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.9, + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/venom.json b/bots/bots_pub/venom.json new file mode 100644 index 000000000..5b7c9392e --- /dev/null +++ b/bots/bots_pub/venom.json @@ -0,0 +1,35 @@ +{ + "name": "VenoM", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.1, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/venz.json b/bots/bots_pub/venz.json new file mode 100644 index 000000000..aabd5ffe5 --- /dev/null +++ b/bots/bots_pub/venz.json @@ -0,0 +1,35 @@ +{ + "name": "Venz", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.1, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.9, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.1, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vey.json b/bots/bots_pub/vey.json new file mode 100644 index 000000000..2a2a05288 --- /dev/null +++ b/bots/bots_pub/vey.json @@ -0,0 +1,35 @@ +{ + "name": "VeY", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vic.json b/bots/bots_pub/vic.json new file mode 100644 index 000000000..1d11d23a1 --- /dev/null +++ b/bots/bots_pub/vic.json @@ -0,0 +1,35 @@ +{ + "name": "Vic", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.8, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vizier.json b/bots/bots_pub/vizier.json new file mode 100644 index 000000000..eddc6fdb1 --- /dev/null +++ b/bots/bots_pub/vizier.json @@ -0,0 +1,35 @@ +{ + "name": "Vizier", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.9, + "m4": 0.4, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.1, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vokur.json b/bots/bots_pub/vokur.json new file mode 100644 index 000000000..e4f29d7b9 --- /dev/null +++ b/bots/bots_pub/vokur.json @@ -0,0 +1,35 @@ +{ + "name": "Vokur", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 1.0, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/waxman.json b/bots/bots_pub/waxman.json new file mode 100644 index 000000000..764b11616 --- /dev/null +++ b/bots/bots_pub/waxman.json @@ -0,0 +1,35 @@ +{ + "name": "Waxman", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.1, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wayofthe.json b/bots/bots_pub/wayofthe.json new file mode 100644 index 000000000..2ec859a94 --- /dev/null +++ b/bots/bots_pub/wayofthe.json @@ -0,0 +1,35 @@ +{ + "name": "Way Of The", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.6, + "silencer": -0.0, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.1, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/whyarewehere.json b/bots/bots_pub/whyarewehere.json index 4be3e541b..aa4ba45d7 100644 --- a/bots/bots_pub/whyarewehere.json +++ b/bots/bots_pub/whyarewehere.json @@ -1,7 +1,7 @@ { "name": "Why are we here?", "clan_id": null, - "skin": "male/indy", + "skin": "null", "weaponPreferences": { "mk23": 0.8, "dual_mk23": 0.5, diff --git a/bots/bots_pub/whyme.json b/bots/bots_pub/whyme.json new file mode 100644 index 000000000..a671af24e --- /dev/null +++ b/bots/bots_pub/whyme.json @@ -0,0 +1,35 @@ +{ + "name": "Why Me?", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wreckage.json b/bots/bots_pub/wreckage.json new file mode 100644 index 000000000..a95663987 --- /dev/null +++ b/bots/bots_pub/wreckage.json @@ -0,0 +1,35 @@ +{ + "name": "Wreckage", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": -0.0, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wyand.json b/bots/bots_pub/wyand.json new file mode 100644 index 000000000..3d7a65203 --- /dev/null +++ b/bots/bots_pub/wyand.json @@ -0,0 +1,35 @@ +{ + "name": "Wyand", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wysyyy.json b/bots/bots_pub/wysyyy.json new file mode 100644 index 000000000..0a45ebbd7 --- /dev/null +++ b/bots/bots_pub/wysyyy.json @@ -0,0 +1,35 @@ +{ + "name": "wysyyy", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.9, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.1, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/yarbro.json b/bots/bots_pub/yarbro.json new file mode 100644 index 000000000..76a458f71 --- /dev/null +++ b/bots/bots_pub/yarbro.json @@ -0,0 +1,35 @@ +{ + "name": "Yarbro", + "clan_id": null, + "skin": "null", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/clans/FZe.json b/bots/clans/FZe.json index 25021b858..4277c8445 100644 --- a/bots/clans/FZe.json +++ b/bots/clans/FZe.json @@ -1,6 +1,6 @@ { "clan_id": 4, - "clan_tag": "=]FZe[=", + "clan_tag": "]FZe[=", "clan_name": "Clan FZe", "pre_or_post": 0 } \ No newline at end of file diff --git a/bots/index.txt b/bots/index.txt index b1ebf9cb2..676cc447c 100644 --- a/bots/index.txt +++ b/bots/index.txt @@ -1,2 +1,426 @@ -0,[/]Wesley Snipes,0,292 -1,Indiana Jones,296,286 +0,]FZe[=GuN,0,291 +1,[DoS]Bean,295,279 +2,=[CS]=Tahlkora,578,288 +3,=[CS]=MannY,870,289 +4,{BH}-Sagel,1163,276 +5,(TBS)Maffer,1443,288 +6,[/]Bullseye,1735,287 +7,[Wang]Willitz**,2026,295 +8,[TRB]_Leiruth,2325,291 +9,_Tq_Hammerhead,2620,288 +10,=[CS]=Spocket,2912,289 +11,=[CS]=T-800,3205,292 +12,(440)Grunt,3501,282 +13,(EL)-T_bon,3787,287 +14,[TRB]_doc,4078,286 +15,]FZe[=Hurricane,4368,292 +16,[DoS]Chameleon,4664,291 +17,]FZe[=Gorre,4959,287 +18,=[CS]=Slash,5250,291 +19,=[CS]=Teabag,5545,291 +20,[/]Kung Lao,5840,285 +21,_Tq_Malin,6129,286 +22,(EL)-Mynx,6419,290 +23,[/]Sarge,6713,283 +24,[DoS]The Boss,7000,293 +25,(440)Mokap,7297,290 +26,=[CS]=Threash,7591,289 +27,{BH}-Zarna,7884,285 +28,(TBS)Berserker,8173,287 +29,(440)Ramrod,8464,288 +30,(440)Xaero,8756,285 +31,(TBS)Helmut,9045,291 +32,[TRB]_Joe Santo,9340,296 +33,_Tq_Drillbit,9640,285 +34,[/]Sorlag,9929,283 +35,[Wang]SteamPug,10216,285 +36,[DoS]Vedro,10505,288 +37,[Wang]Dominator,10797,295 +38,=[CS]=2DCitizen,11096,289 +39,{BH}-Himdar,11389,284 +40,=[CS]=Monty,11677,293 +41,[TRB]_Noogie,11974,288 +42,[DoS]Blackadder,12266,285 +43,(EL)-[Mee2],12555,283 +44,=[CS]=_Gholem_,12842,291 +45,]FZe[=Pyro Yetti,13137,293 +46,(TBS)Trench,13434,281 +47,[TRB]_Taven,13719,285 +48,=[CS]=Patriot,14008,287 +49,[Wang]Geras,14299,282 +50,(TBS)Jazz,14585,287 +51,(TBS)Uriel,14876,296 +52,_Tq_Strogg,15176,282 +53,[Wang]BVe,15462,288 +54,(TBS)Cerberon,15754,287 +55,[/]Frost,16045,287 +56,[Wang]Havo,16336,283 +57,[DoS]Shinnok,16623,284 +58,(440)Mindelos,16911,288 +59,[/]Dragoooon,17203,286 +60,_Tq_Missnrun,17493,282 +61,(EL)-Eyeback,17779,287 +62,(EL)-Fangskin,18070,283 +63,[DoS]Radament,18357,286 +64,(440)Fexel,18647,283 +65,[DoS]Hunter,18934,287 +66,(440)Strom,19225,285 +67,[/]Wesley Snipes,19514,292 +68,[TRB]_Icexo,19810,287 +69,]FZe[=Jarno,20101,286 +70,[Wang]NiN,20391,279 +71,(EL)-Endugu,20674,286 +72,[Wang]Wrack,20964,284 +73,]FZe[=Replicant,21252,287 +74,[DoS]Kaolin,21543,288 +75,[TRB]_Lawrence,21835,288 +76,[TRB]_Doom,22127,290 +77,(440)Dr.Jay,22421,284 +78,[DoS]kuri,22709,285 +79,[DoS]Si,22998,280 +80,_Tq_gogman,23282,278 +81,(TBS)Conan,23564,286 +82,[/]Wens,23854,287 +83,_Tq_FUNKANIK,24145,292 +84,]FZe[=D'Vorah,24441,296 +85,[DoS]Dac Farren,24741,290 +86,{BH}-Tank,25035,288 +87,{BH}-Cybr,25327,290 +88,]FZe[=C-Lion,25621,290 +89,(TBS)Sub-Zero,25915,292 +90,=[CS]=ParaDox,26211,286 +91,_Tq_Amadi,26501,281 +92,(TBS)Gunner,26786,288 +93,[Wang]Mavado,27078,284 +94,[Wang]Marku,27366,285 +95,(TBS)Boy,27655,287 +96,[DoS]Kalidor,27946,282 +97,=[CS]=JCe,28232,285 +98,{BH}-Bob Ross,28521,294 +99,]FZe[=Vera,28819,287 +100,=[CS]=Treehead,29110,285 +101,[DoS]Mr.Badger,29399,291 +102,[Wang]Ratbot,29694,290 +103,{BH}-Hsu Hao,29988,285 +104,{BH}-Mungri,30277,290 +105,(440)Ruslan,30571,285 +106,(440)_*PixxY*_,30860,288 +107,[Wang]Sundog,31152,289 +108,]FZe[=Reiko,31445,288 +109,{BH}-Griswold,31737,287 +110,(440)Warchild,32028,293 +111,[DoS]Scorpion,32325,286 +112,(TBS)Klesk,32615,296 +113,{BH}-Bones,32915,291 +114,[Wang]Baraka,33210,282 +115,[/]Sindel,33496,285 +116,(TBS)Sektor,33785,283 +117,(TBS)Flamespike,34072,288 +118,(EL)-Crash,34364,291 +119,[DoS]Anodized,34659,286 +120,(440)ApeM,34949,286 +121,]FZe[=Tom Steele,35239,293 +122,(EL)-Flint,35536,289 +123,[DoS]Coldcrow,35829,288 +124,[Wang]Orbb,36121,282 +125,[DoS]Ostmor,36407,286 +126,[/]Figgz,36697,281 +127,[/]Applee,36982,288 +128,[Wang]Medic,37274,295 +129,[/]Ben Archer,37573,286 +130,[DoS]Hossman,37863,286 +131,{BH}-Drahmin,38153,288 +132,[Wang]Li Mei,38445,286 +133,{BH}-Sheeva,38735,286 +134,]FZe[=Tremor,39025,285 +135,_Tq_John Luger,39314,295 +136,[TRB]_Kitana,39613,286 +137,(440)exBEE,39903,286 +138,[TRB]_Bitterman,40193,293 +139,_Tq_Sean Kane,40490,281 +140,]FZe[=Chan,40775,291 +141,{BH}-Meat,41070,283 +142,=[CS]=Stinger,41357,293 +143,[/]Digz,41654,283 +144,[DoS]Ivor,41941,284 +145,=[CS]=Kor,42229,292 +146,(440)Kintaro,42525,288 +147,_Tq_Ermac,42817,280 +148,(EL)-Jack Saxon,43101,299 +149,[DoS]Mr Goodkat,43404,291 +150,(440)Belokk,43699,285 +151,(TBS)Jade,43988,288 +152,[/]Icarus,44280,283 +153,_Tq_Morgan,44567,282 +154,(440)Ant__Dog,44853,290 +155,(TBS)Nitara,45147,290 +156,]FZe[=Liu Kang,45441,290 +157,[Wang]Booker,45735,285 +158,[/]Tanya,46024,284 +159,[TRB]_Blythe,46312,291 +160,[TRB]_Hotaru,46607,291 +161,[DoS]Fullmonty,46902,288 +162,[TRB]_Sean,47194,290 +163,_Tq_Po-Tay-Toe,47488,283 +164,(TBS)uzayTron,47775,292 +165,_Tq_GreZZer,48071,286 +166,]FZe[=Keel,48361,285 +167,[Wang]Muscleman,48650,284 +168,=[CS]=Nightwolf,48938,292 +169,(440){itzChamp},49234,289 +170,(440)Edwig,49527,282 +171,[DoS]Gali,49813,288 +172,[/]SpecialEd,50105,288 +173,=[CS]=Icehawk,50397,289 +174,(440)Lone Wolf,50690,287 +175,[Wang]Kollector,50981,289 +176,[/]Nandet,51274,285 +177,(EL)-Brad,51563,291 +178,[Wang]Visor,51858,289 +179,=[CS]=Ashrah,52151,288 +180,]FZe[=Amu,52443,291 +181,(EL)-Jarvis,52738,289 +182,(440)Faden,53031,284 +183,_Tq_Gladiator,53319,287 +184,(TBS)Orin Boyd,53610,288 +185,[/]Tom Mix,53902,291 +186,(440)Joe Tanto,54197,292 +187,[Wang]_GLiCH,54493,299 +188,{BH}-Rakanishu,54796,290 +189,=[CS]=Summoner,55090,292 +190,[Wang]Walter,55386,288 +191,=[CS]=Thor,55678,287 +192,]FZe[=PewDiePie,55969,294 +193,[Wang]Kronika,56267,292 +194,_Tq_Ismail,56563,282 +195,_Tq_T-1000,56849,278 +196,{BH}-Stormtree,57131,289 +197,[DoS]Toorc,57424,288 +198,[DoS]deUSvauLT,57716,289 +199,[TRB]_Hitnrun,58009,290 +200,(EL)-Sarina,58303,284 +201,]FZe[=Aegarond,58591,287 +202,(440)Monk,58882,286 +203,Joe Joiner,59172,282 +204,Major Pain,59458,285 +205,Jax,59747,286 +206,Brixos,60037,283 +207,Nundak,60324,282 +208,Anakin Skywalker,60610,291 +209,Onaga,60905,278 +210,wysyyy,61187,282 +211,Cledus,61473,281 +212,Tormentor,61758,281 +213,Vic,62043,282 +214,Joce,62329,277 +215,Cobra,62610,278 +216,Darrius,62892,284 +217,Anton,63180,285 +218,Ryver,63469,277 +219,Ryder,63750,277 +220,Kira,64031,284 +221,Daegon,64319,283 +222,Mincer,64606,280 +223,Mileena,64890,281 +224,Pindleskin,65175,284 +225,Tanz,65463,276 +226,Moloch,65743,280 +227,Evo89,66027,283 +228,Andromeed,66314,282 +229,Reddog,66600,279 +230,Bolund,66883,288 +231,Chess,67175,281 +232,Ivan Kraschinsky,67460,287 +233,Saboy,67751,280 +234,The Dean,68035,287 +235,Chance Boudreaux,68326,295 +236,Kotal Kahn,68625,285 +237,TheWay,68914,282 +238,Little G,69200,285 +239,Tarnen,69489,282 +240,Cyrax,69775,281 +241,George,70060,281 +242,Raiden,70345,274 +243,Fred,70623,277 +244,Daemia,70904,275 +245,FXguy,71183,280 +246,BroFist,71467,282 +247,Phobos,71753,282 +248,Colt,72039,279 +249,itiXOID,72322,292 +250,Leatherarm,72618,284 +251,Death,72906,282 +252,Technician,73192,284 +253,Kano,73480,283 +254,Himself,73767,282 +255,The Eraser,74053,284 +256,Angelo Provolone,74341,286 +257,Kaa,74631,282 +258,Dark Kahn,74917,287 +259,Tao,75208,281 +260,Bremm,75493,279 +261,Ellanil,75776,281 +262,Rocky,76061,280 +263,Skarlet,76345,284 +264,Cruton,76633,284 +265,Rain,76921,281 +266,Heljay,77206,282 +267,Kung Jin,77492,283 +268,Wyand,77779,279 +269,Dutch,78062,287 +270,Terminator,78353,281 +271,[Corrus],78638,290 +272,Snaps,78932,281 +273,Gung Ho,79217,280 +274,Broz,79501,278 +275,CowChop,79783,284 +276,Jacques Kristoff,80071,291 +277,Ferra/Torr,80366,286 +278,Pitspawn Fouldog,80656,287 +279,Dairou,80947,279 +280,Angel,81230,287 +281,Ray Quick,81521,285 +282,Lucy,81810,280 +283,SGT CAPPA,82094,288 +284,Bad Sam,82386,286 +285,Stryker,82676,281 +286,Jinx,82961,278 +287,Doctor Deathwish,83243,293 +288,Quan Chi,83540,282 +289,Stripe,83826,282 +290,Kole,84112,281 +291,Stud,84397,277 +292,Milius,84678,285 +293,Edoli,84967,286 +294,Major,85257,282 +295,Cow King,85543,285 +296,_*NiXXy*_,85832,292 +297,Grimm,86128,281 +298,Cadaver,86413,281 +299,Halfwit,86698,283 +300,Bloodwitch,86985,284 +301,Boneash,87273,280 +302,Hellhound,87557,284 +303,Turkish,87845,280 +304,^^ChoppY^^,88129,288 +305,Why Me?,88421,283 +306,Mr Suave,88708,282 +307,Enforcer,88994,282 +308,Mongrel,89280,283 +309,Hambone,89567,284 +310,Smithy,89855,276 +311,Nikolai Cherenko,90135,288 +312,NoAmmo,90427,285 +313,Frenchy,90716,279 +314,Kobra,90999,278 +315,Biker,91281,281 +316,Drago,91566,280 +317,Bad JuJu,91850,284 +318,Ivan Drago,92138,285 +319,Merki,92427,279 +320,Oysterhead,92710,286 +321,Hecker,93000,282 +322,Venz,93286,283 +323,Newbie,93573,277 +324,Michael,93854,277 +325,Bacon,94135,283 +326,Dolin,94422,280 +327,Nick Gunar,94706,288 +328,Razor,94998,280 +329,Skorgan,95282,277 +330,Vizier,95563,281 +331,Imentu,95848,287 +332,Wreckage,96139,285 +333,Bortack,96428,281 +334,Kit Latura,96713,282 +335,Vanoss,96999,281 +336,Geleb,97284,284 +337,Kabal,97572,284 +338,Motaro,97860,287 +339,BigMack,98151,279 +340,Shujinko,98434,285 +341,The German,98723,283 +342,VeY,99010,276 +343,Di Nardo,99290,285 +344,Morgriff,99579,283 +345,Brohn,99866,277 +346,The Tulip,100147,287 +347,Tarnok,100438,280 +348,Sareena,100722,284 +349,GhOstBl4de,101010,287 +350,Church,101301,283 +351,Carpenter,101588,288 +352,Vokur,101880,284 +353,Nihlathak,102168,290 +354,chipp_,102462,282 +355,Halfbaked,102748,285 +356,Spar,103037,283 +357,Snapchip Shatter,103324,286 +358,Gigavoltz,103614,285 +359,Reptile,103903,284 +360,Grog,104191,283 +361,John Rambo,104478,283 +362,Kai,104765,280 +363,Royen,105049,280 +364,Sigil,105333,282 +365,Bishibosh,105619,288 +366,Way Of The,105911,291 +367,Frank Dux,106206,284 +368,D is for Digger!,106494,291 +369,Richard,106789,281 +370,Garbok,107074,278 +371,Thalath,107356,282 +372,Havik,107642,276 +373,Aimless,107922,279 +374,Waxman,108205,281 +375,Nibbler,108490,285 +376,Why are we here?,108779,288 +377,Cetrion,109071,283 +378,Coldworm,109358,291 +379,hozef-8,109653,286 +380,JD Gold,109943,285 +381,Khameleon,110232,281 +382,JJ McQuade,110517,288 +383,Korv,110809,279 +384,Makron,111092,287 +385,Bonebreak,111383,280 +386,tatu,111667,278 +387,Corpsefire,111949,279 +388,HAL-9000,112232,291 +389,Smoke,112527,282 +390,Slice,112813,279 +391,Nohope,113096,276 +392,Socks,113376,278 +393,Barbarian,113658,280 +394,Ranger,113942,286 +395,Tiano,114232,282 +396,Gordac,114518,284 +397,Tank Jr,114806,282 +398,Kenshi,115092,281 +399,Yarbro,115377,285 +400,Fujin,115666,275 +401,Kippy,115945,280 +402,VenoM,116229,281 +403,Arklon,116514,286 +404,Jarek,116804,283 +405,Siege,117091,282 +406,Argo,117377,280 +407,Takeda Takahashi,117661,286 +408,Lt Raymond Tango,117951,295 +409,Beelz,118250,284 +410,Shao Kahn,118538,286 +411,Galul,118828,276 +412,Riftwraith,119108,283 +413,Sam Decker,119395,293 +414,Backfire,119692,282 +415,Countess,119978,284 +416,Blaze,120266,285 +417,The Only,120555,285 +418,Capt. Ivan Danko,120844,285 +419,Anarki,121133,282 +420,Indiana Jones,121419,286 +421,McKenna,121709,280 +422,Triborg,121993,282 +423,Dharmesh,122279,282 +424,Lemmie,122565,285 +425,JC Denton,122854,288 From a40d475204954f31ee05b744ba3360abca26b15a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Feb 2024 13:45:14 -0500 Subject: [PATCH 081/974] Added a ton of new clans --- bots/{charlen.py => botmaker.py} | 0 bots/clangen.py | 63 ++++++++++++++++++++ bots/clans/ablecompany.json | 6 ++ bots/clans/armadaavengers.json | 6 ++ bots/clans/auroraannihilator.json | 6 ++ bots/clans/binarybutchers.json | 6 ++ bots/clans/blizzardbulldogs.json | 6 ++ bots/clans/botbandits.json | 6 ++ bots/clans/brokenarrows.json | 6 ++ bots/clans/bytebears.json | 6 ++ bots/clans/command&conquer.json | 6 ++ bots/clans/cosmicchargers.json | 6 ++ bots/clans/crimeguardians.json | 6 ++ bots/clans/datadestroyers.json | 6 ++ bots/clans/defensesentinels.json | 6 ++ bots/clans/dorchesterdukes.json | 6 ++ bots/clans/downtownchinatown.json | 6 ++ bots/clans/eagleeyes.json | 6 ++ bots/clans/earthquakeelite.json | 6 ++ bots/clans/eliteforce.json | 6 ++ bots/clans/firestormheroes.json | 6 ++ bots/clans/fortuna500s.json | 6 ++ bots/clans/gangsterglory.json | 6 ++ bots/clans/gigabears.json | 6 ++ bots/clans/guardianangels.json | 6 ++ bots/clans/hackerhammers.json | 6 ++ bots/clans/highoctane.json | 6 ++ bots/clans/hurricanehorrors.json | 6 ++ bots/clans/icemen.json | 6 ++ bots/clans/ironimpact.json | 6 ++ bots/clans/ironlegends.json | 6 ++ bots/clans/justicewarriors.json | 6 ++ bots/clans/killerkidz.json | 6 ++ bots/clans/lawenforcement.json | 6 ++ bots/clans/lazylizards.json | 6 ++ bots/clans/lightningpunisher.json | 6 ++ bots/clans/magnummadness.json | 6 ++ bots/clans/maskedmarines.json | 6 ++ bots/clans/monsoonmayhem.json | 6 ++ bots/clans/monsoonmortals.json | 6 ++ bots/clans/moonbeamguardians.json | 6 ++ bots/clans/navysharks.json | 6 ++ bots/clans/nebulaninjas.json | 6 ++ bots/clans/nightstalkers.json | 6 ++ bots/clans/nocturnalninjas.json | 6 ++ bots/clans/phoenixrangers.json | 6 ++ bots/clans/pixelpleasure.json | 6 ++ bots/clans/policecommand.json | 6 ++ bots/clans/pulpfictionsquad.json | 6 ++ bots/clans/quantumquicksand.json | 6 ++ bots/clans/ragingbulls.json | 6 ++ bots/clans/reservoirdogs.json | 6 ++ bots/clans/roboticrhinos.json | 6 ++ bots/clans/saberforce.json | 6 ++ bots/clans/safetycrusaders.json | 6 ++ bots/clans/saseagleteam.json | 6 ++ bots/clans/securityrangers.json | 6 ++ bots/clans/silenceofthedead.json | 6 ++ bots/clans/silentassassins.json | 6 ++ bots/clans/solarshowdown.json | 6 ++ bots/clans/solarsuperheroes.json | 6 ++ bots/clans/sonicdoom.json | 6 ++ bots/clans/steelchampions.json | 6 ++ bots/clans/steelfury.json | 6 ++ bots/clans/stormstrikers.json | 6 ++ bots/clans/strikepunishers.json | 6 ++ bots/clans/teamdragonfly.json | 6 ++ bots/clans/teamiceman.json | 6 ++ bots/clans/teamknight.json | 6 ++ bots/clans/teamotters.json | 6 ++ bots/clans/teampurpose.json | 6 ++ bots/clans/teamstar.json | 6 ++ bots/clans/teamwhirlwind.json | 6 ++ bots/clans/teamxray.json | 6 ++ bots/clans/theatomics.json | 6 ++ bots/clans/thecentaurs.json | 6 ++ bots/clans/thechampions.json | 6 ++ bots/clans/thedreamers.json | 6 ++ bots/clans/thedukes.json | 6 ++ bots/clans/theheatwave.json | 6 ++ bots/clans/thejaguars.json | 6 ++ bots/clans/theknights.json | 6 ++ bots/clans/themadmen.json | 6 ++ bots/clans/themarines.json | 6 ++ bots/clans/theravagers.json | 6 ++ bots/clans/theshadows.json | 6 ++ bots/clans/thespitfires.json | 6 ++ bots/clans/thetsunamis.json | 6 ++ bots/clans/thetyphoons.json | 6 ++ bots/clans/thevanguards.json | 6 ++ bots/clans/thevolcanics.json | 6 ++ bots/clans/thunderbolt.json | 6 ++ bots/clans/unseenvigilantes.json | 6 ++ bots/clans/virusvikings.json | 6 ++ bots/clans/volcanicvendetta.json | 6 ++ bots/clans/wackojackos.json | 6 ++ bots/clans/washingtonraiders.json | 6 ++ bots/clans/washingtonwizards.json | 6 ++ bots/clans/windforcetitans.json | 6 ++ bots/clans/wreckingcrew.json | 6 ++ bots/clans/yogoyetis.json | 6 ++ bots/team_names.txt | 99 +++++++++++++++++++++++++++++++ 102 files changed, 756 insertions(+) rename bots/{charlen.py => botmaker.py} (100%) create mode 100644 bots/clangen.py create mode 100644 bots/clans/ablecompany.json create mode 100644 bots/clans/armadaavengers.json create mode 100644 bots/clans/auroraannihilator.json create mode 100644 bots/clans/binarybutchers.json create mode 100644 bots/clans/blizzardbulldogs.json create mode 100644 bots/clans/botbandits.json create mode 100644 bots/clans/brokenarrows.json create mode 100644 bots/clans/bytebears.json create mode 100644 bots/clans/command&conquer.json create mode 100644 bots/clans/cosmicchargers.json create mode 100644 bots/clans/crimeguardians.json create mode 100644 bots/clans/datadestroyers.json create mode 100644 bots/clans/defensesentinels.json create mode 100644 bots/clans/dorchesterdukes.json create mode 100644 bots/clans/downtownchinatown.json create mode 100644 bots/clans/eagleeyes.json create mode 100644 bots/clans/earthquakeelite.json create mode 100644 bots/clans/eliteforce.json create mode 100644 bots/clans/firestormheroes.json create mode 100644 bots/clans/fortuna500s.json create mode 100644 bots/clans/gangsterglory.json create mode 100644 bots/clans/gigabears.json create mode 100644 bots/clans/guardianangels.json create mode 100644 bots/clans/hackerhammers.json create mode 100644 bots/clans/highoctane.json create mode 100644 bots/clans/hurricanehorrors.json create mode 100644 bots/clans/icemen.json create mode 100644 bots/clans/ironimpact.json create mode 100644 bots/clans/ironlegends.json create mode 100644 bots/clans/justicewarriors.json create mode 100644 bots/clans/killerkidz.json create mode 100644 bots/clans/lawenforcement.json create mode 100644 bots/clans/lazylizards.json create mode 100644 bots/clans/lightningpunisher.json create mode 100644 bots/clans/magnummadness.json create mode 100644 bots/clans/maskedmarines.json create mode 100644 bots/clans/monsoonmayhem.json create mode 100644 bots/clans/monsoonmortals.json create mode 100644 bots/clans/moonbeamguardians.json create mode 100644 bots/clans/navysharks.json create mode 100644 bots/clans/nebulaninjas.json create mode 100644 bots/clans/nightstalkers.json create mode 100644 bots/clans/nocturnalninjas.json create mode 100644 bots/clans/phoenixrangers.json create mode 100644 bots/clans/pixelpleasure.json create mode 100644 bots/clans/policecommand.json create mode 100644 bots/clans/pulpfictionsquad.json create mode 100644 bots/clans/quantumquicksand.json create mode 100644 bots/clans/ragingbulls.json create mode 100644 bots/clans/reservoirdogs.json create mode 100644 bots/clans/roboticrhinos.json create mode 100644 bots/clans/saberforce.json create mode 100644 bots/clans/safetycrusaders.json create mode 100644 bots/clans/saseagleteam.json create mode 100644 bots/clans/securityrangers.json create mode 100644 bots/clans/silenceofthedead.json create mode 100644 bots/clans/silentassassins.json create mode 100644 bots/clans/solarshowdown.json create mode 100644 bots/clans/solarsuperheroes.json create mode 100644 bots/clans/sonicdoom.json create mode 100644 bots/clans/steelchampions.json create mode 100644 bots/clans/steelfury.json create mode 100644 bots/clans/stormstrikers.json create mode 100644 bots/clans/strikepunishers.json create mode 100644 bots/clans/teamdragonfly.json create mode 100644 bots/clans/teamiceman.json create mode 100644 bots/clans/teamknight.json create mode 100644 bots/clans/teamotters.json create mode 100644 bots/clans/teampurpose.json create mode 100644 bots/clans/teamstar.json create mode 100644 bots/clans/teamwhirlwind.json create mode 100644 bots/clans/teamxray.json create mode 100644 bots/clans/theatomics.json create mode 100644 bots/clans/thecentaurs.json create mode 100644 bots/clans/thechampions.json create mode 100644 bots/clans/thedreamers.json create mode 100644 bots/clans/thedukes.json create mode 100644 bots/clans/theheatwave.json create mode 100644 bots/clans/thejaguars.json create mode 100644 bots/clans/theknights.json create mode 100644 bots/clans/themadmen.json create mode 100644 bots/clans/themarines.json create mode 100644 bots/clans/theravagers.json create mode 100644 bots/clans/theshadows.json create mode 100644 bots/clans/thespitfires.json create mode 100644 bots/clans/thetsunamis.json create mode 100644 bots/clans/thetyphoons.json create mode 100644 bots/clans/thevanguards.json create mode 100644 bots/clans/thevolcanics.json create mode 100644 bots/clans/thunderbolt.json create mode 100644 bots/clans/unseenvigilantes.json create mode 100644 bots/clans/virusvikings.json create mode 100644 bots/clans/volcanicvendetta.json create mode 100644 bots/clans/wackojackos.json create mode 100644 bots/clans/washingtonraiders.json create mode 100644 bots/clans/washingtonwizards.json create mode 100644 bots/clans/windforcetitans.json create mode 100644 bots/clans/wreckingcrew.json create mode 100644 bots/clans/yogoyetis.json create mode 100644 bots/team_names.txt diff --git a/bots/charlen.py b/bots/botmaker.py similarity index 100% rename from bots/charlen.py rename to bots/botmaker.py diff --git a/bots/clangen.py b/bots/clangen.py new file mode 100644 index 000000000..46f62c58d --- /dev/null +++ b/bots/clangen.py @@ -0,0 +1,63 @@ +import os +import json +import re +import sys +import random + +special_chars = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '[', ']', '<', '>', '/', '\\', '{', '}', '+', '=', '-', '.', ';', ':', '\'', '"', '`', '~'] + +# Read the team names from the file +with open('team_names.txt', 'r') as file: + clan_names = [line.strip() for line in file] + +clan_id = 12 +file_names = [] +clan_infos = {} +clan_name_to_file_name = {} + +for clan_name in clan_names: + words = clan_name.split() + tag = "".join(word[0] for word in words if word[0].isalpha()) + if len(tag) > 3: + tag = tag[:3] + tag = tag.upper() + + # Add a special character at the beginning and/or end of the tag + special_char_start = special_chars[random.randint(0, len(special_chars) - 1)] + special_char_end = special_char_start if random.random() < 0.5 else special_chars[random.randint(0, len(special_chars) - 1)] + tag = special_char_start + tag + special_char_end + + # Ensure the tag is not longer than 5 characters + if len(tag) > 5: + tag = tag[:5] + + # Randomize pre_or_post to be 0 or 1 + pre_or_post = random.randint(0, 1) + + # Prepare the clan information + clan_info = { + "clan_id": clan_id, + "clan_tag": tag, + "clan_name": clan_name, + "pre_or_post": pre_or_post + } + + # Prepare the file name + file_name = re.sub(r'\s+', '', clan_name).lower() + '.json' + if file_name in file_names: + print(f"Conflict detected: {file_name} already exists.") + print(f"Offending Clan Names: {clan_name_to_file_name[file_name]}, {clan_name}") + sys.exit(1) + file_names.append(file_name) + clan_name_to_file_name[file_name] = clan_name + + # Store the clan information + clan_infos[file_name] = clan_info + + # Increment the clan_id + clan_id += 1 + +# Write the clan information to files +for file_name, clan_info in clan_infos.items(): + with open(os.path.join('clans', file_name), 'w') as file: + json.dump(clan_info, file, indent=4) \ No newline at end of file diff --git a/bots/clans/ablecompany.json b/bots/clans/ablecompany.json new file mode 100644 index 000000000..08092b9ff --- /dev/null +++ b/bots/clans/ablecompany.json @@ -0,0 +1,6 @@ +{ + "clan_id": 86, + "clan_tag": "(AC`", + "clan_name": "Able Company", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/armadaavengers.json b/bots/clans/armadaavengers.json new file mode 100644 index 000000000..efab6e7e6 --- /dev/null +++ b/bots/clans/armadaavengers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 40, + "clan_tag": "=AA-", + "clan_name": "Armada Avengers", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/auroraannihilator.json b/bots/clans/auroraannihilator.json new file mode 100644 index 000000000..29dd8ab08 --- /dev/null +++ b/bots/clans/auroraannihilator.json @@ -0,0 +1,6 @@ +{ + "clan_id": 54, + "clan_tag": ">AA>", + "clan_name": "Aurora Annihilator", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/binarybutchers.json b/bots/clans/binarybutchers.json new file mode 100644 index 000000000..aa3a4f233 --- /dev/null +++ b/bots/clans/binarybutchers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 58, + "clan_tag": "#BB\\", + "clan_name": "Binary Butchers", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/blizzardbulldogs.json b/bots/clans/blizzardbulldogs.json new file mode 100644 index 000000000..fc1c44589 --- /dev/null +++ b/bots/clans/blizzardbulldogs.json @@ -0,0 +1,6 @@ +{ + "clan_id": 51, + "clan_tag": "\"BB\"", + "clan_name": "Blizzard Bulldogs", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/botbandits.json b/bots/clans/botbandits.json new file mode 100644 index 000000000..9f1839511 --- /dev/null +++ b/bots/clans/botbandits.json @@ -0,0 +1,6 @@ +{ + "clan_id": 64, + "clan_tag": "-BB^", + "clan_name": "Bot Bandits", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/brokenarrows.json b/bots/clans/brokenarrows.json new file mode 100644 index 000000000..2a8229561 --- /dev/null +++ b/bots/clans/brokenarrows.json @@ -0,0 +1,6 @@ +{ + "clan_id": 89, + "clan_tag": ":BA:", + "clan_name": "Broken Arrows", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/bytebears.json b/bots/clans/bytebears.json new file mode 100644 index 000000000..523b41650 --- /dev/null +++ b/bots/clans/bytebears.json @@ -0,0 +1,6 @@ +{ + "clan_id": 68, + "clan_tag": "=BB$", + "clan_name": "Byte Bears", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/command&conquer.json b/bots/clans/command&conquer.json new file mode 100644 index 000000000..1ca7f769b --- /dev/null +++ b/bots/clans/command&conquer.json @@ -0,0 +1,6 @@ +{ + "clan_id": 110, + "clan_tag": "$CC$", + "clan_name": "Command & Conquer", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/cosmicchargers.json b/bots/clans/cosmicchargers.json new file mode 100644 index 000000000..3d4e7086f --- /dev/null +++ b/bots/clans/cosmicchargers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 55, + "clan_tag": ";CC;", + "clan_name": "Cosmic Chargers", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/crimeguardians.json b/bots/clans/crimeguardians.json new file mode 100644 index 000000000..111f18cb3 --- /dev/null +++ b/bots/clans/crimeguardians.json @@ -0,0 +1,6 @@ +{ + "clan_id": 17, + "clan_tag": "/CG/", + "clan_name": "Crime Guardians", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/datadestroyers.json b/bots/clans/datadestroyers.json new file mode 100644 index 000000000..41e6f7b69 --- /dev/null +++ b/bots/clans/datadestroyers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 60, + "clan_tag": "[DD%", + "clan_name": "Data Destroyers", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/defensesentinels.json b/bots/clans/defensesentinels.json new file mode 100644 index 000000000..686253d54 --- /dev/null +++ b/bots/clans/defensesentinels.json @@ -0,0 +1,6 @@ +{ + "clan_id": 22, + "clan_tag": "%DS\\", + "clan_name": "Defense Sentinels", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/dorchesterdukes.json b/bots/clans/dorchesterdukes.json new file mode 100644 index 000000000..014328ba8 --- /dev/null +++ b/bots/clans/dorchesterdukes.json @@ -0,0 +1,6 @@ +{ + "clan_id": 105, + "clan_tag": "!DD!", + "clan_name": "Dorchester Dukes", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/downtownchinatown.json b/bots/clans/downtownchinatown.json new file mode 100644 index 000000000..eaf14fc50 --- /dev/null +++ b/bots/clans/downtownchinatown.json @@ -0,0 +1,6 @@ +{ + "clan_id": 76, + "clan_tag": "-DC\"", + "clan_name": "Downtown Chinatown", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/eagleeyes.json b/bots/clans/eagleeyes.json new file mode 100644 index 000000000..1f29632a8 --- /dev/null +++ b/bots/clans/eagleeyes.json @@ -0,0 +1,6 @@ +{ + "clan_id": 25, + "clan_tag": "/EE!", + "clan_name": "Eagle Eyes", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/earthquakeelite.json b/bots/clans/earthquakeelite.json new file mode 100644 index 000000000..afd1b0eaf --- /dev/null +++ b/bots/clans/earthquakeelite.json @@ -0,0 +1,6 @@ +{ + "clan_id": 38, + "clan_tag": "#EE#", + "clan_name": "Earthquake Elite", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/eliteforce.json b/bots/clans/eliteforce.json new file mode 100644 index 000000000..73e969e94 --- /dev/null +++ b/bots/clans/eliteforce.json @@ -0,0 +1,6 @@ +{ + "clan_id": 14, + "clan_tag": "[EF=", + "clan_name": "Elite Force", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/firestormheroes.json b/bots/clans/firestormheroes.json new file mode 100644 index 000000000..03dfead75 --- /dev/null +++ b/bots/clans/firestormheroes.json @@ -0,0 +1,6 @@ +{ + "clan_id": 37, + "clan_tag": "(FSH(", + "clan_name": "Fire Storm Heroes", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/fortuna500s.json b/bots/clans/fortuna500s.json new file mode 100644 index 000000000..becb08c20 --- /dev/null +++ b/bots/clans/fortuna500s.json @@ -0,0 +1,6 @@ +{ + "clan_id": 107, + "clan_tag": ")F*", + "clan_name": "Fortuna 500s", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/gangsterglory.json b/bots/clans/gangsterglory.json new file mode 100644 index 000000000..a19e48869 --- /dev/null +++ b/bots/clans/gangsterglory.json @@ -0,0 +1,6 @@ +{ + "clan_id": 80, + "clan_tag": "[GG^", + "clan_name": "Gangster Glory", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/gigabears.json b/bots/clans/gigabears.json new file mode 100644 index 000000000..b1d8f87d9 --- /dev/null +++ b/bots/clans/gigabears.json @@ -0,0 +1,6 @@ +{ + "clan_id": 71, + "clan_tag": "[GB[", + "clan_name": "Giga Bears", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/guardianangels.json b/bots/clans/guardianangels.json new file mode 100644 index 000000000..f50f82895 --- /dev/null +++ b/bots/clans/guardianangels.json @@ -0,0 +1,6 @@ +{ + "clan_id": 26, + "clan_tag": ":GA:", + "clan_name": "Guardian Angels", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/hackerhammers.json b/bots/clans/hackerhammers.json new file mode 100644 index 000000000..1991653d4 --- /dev/null +++ b/bots/clans/hackerhammers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 67, + "clan_tag": "-HH-", + "clan_name": "Hacker Hammers", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/highoctane.json b/bots/clans/highoctane.json new file mode 100644 index 000000000..7471943d6 --- /dev/null +++ b/bots/clans/highoctane.json @@ -0,0 +1,6 @@ +{ + "clan_id": 69, + "clan_tag": "\\HO<", + "clan_name": "High Octane", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/hurricanehorrors.json b/bots/clans/hurricanehorrors.json new file mode 100644 index 000000000..4b9c960f7 --- /dev/null +++ b/bots/clans/hurricanehorrors.json @@ -0,0 +1,6 @@ +{ + "clan_id": 50, + "clan_tag": "+HH+", + "clan_name": "Hurricane Horrors", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/icemen.json b/bots/clans/icemen.json new file mode 100644 index 000000000..be7ffb51d --- /dev/null +++ b/bots/clans/icemen.json @@ -0,0 +1,6 @@ +{ + "clan_id": 99, + "clan_tag": "'I!", + "clan_name": "Icemen", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/ironimpact.json b/bots/clans/ironimpact.json new file mode 100644 index 000000000..8560818d7 --- /dev/null +++ b/bots/clans/ironimpact.json @@ -0,0 +1,6 @@ +{ + "clan_id": 81, + "clan_tag": "#II'", + "clan_name": "Iron Impact", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/ironlegends.json b/bots/clans/ironlegends.json new file mode 100644 index 000000000..2e371609f --- /dev/null +++ b/bots/clans/ironlegends.json @@ -0,0 +1,6 @@ +{ + "clan_id": 32, + "clan_tag": "#IL/", + "clan_name": "Iron Legends", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/justicewarriors.json b/bots/clans/justicewarriors.json new file mode 100644 index 000000000..b2f1614a1 --- /dev/null +++ b/bots/clans/justicewarriors.json @@ -0,0 +1,6 @@ +{ + "clan_id": 18, + "clan_tag": ")JW\"", + "clan_name": "Justice Warriors", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/killerkidz.json b/bots/clans/killerkidz.json new file mode 100644 index 000000000..ac1a7a951 --- /dev/null +++ b/bots/clans/killerkidz.json @@ -0,0 +1,6 @@ +{ + "clan_id": 82, + "clan_tag": "$KK$", + "clan_name": "Killer Kidz", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/lawenforcement.json b/bots/clans/lawenforcement.json new file mode 100644 index 000000000..cca62d71f --- /dev/null +++ b/bots/clans/lawenforcement.json @@ -0,0 +1,6 @@ +{ + "clan_id": 19, + "clan_tag": ">LE(", + "clan_name": "Law Enforcement", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/lazylizards.json b/bots/clans/lazylizards.json new file mode 100644 index 000000000..2f2285fe5 --- /dev/null +++ b/bots/clans/lazylizards.json @@ -0,0 +1,6 @@ +{ + "clan_id": 101, + "clan_tag": "}LL}", + "clan_name": "Lazy Lizards", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/lightningpunisher.json b/bots/clans/lightningpunisher.json new file mode 100644 index 000000000..0d80eff43 --- /dev/null +++ b/bots/clans/lightningpunisher.json @@ -0,0 +1,6 @@ +{ + "clan_id": 41, + "clan_tag": "=LP=", + "clan_name": "Lightning Punisher", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/magnummadness.json b/bots/clans/magnummadness.json new file mode 100644 index 000000000..588947f31 --- /dev/null +++ b/bots/clans/magnummadness.json @@ -0,0 +1,6 @@ +{ + "clan_id": 83, + "clan_tag": "-MM-", + "clan_name": "Magnum Madness", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/maskedmarines.json b/bots/clans/maskedmarines.json new file mode 100644 index 000000000..a16f6792b --- /dev/null +++ b/bots/clans/maskedmarines.json @@ -0,0 +1,6 @@ +{ + "clan_id": 30, + "clan_tag": "`MM`", + "clan_name": "Masked Marines", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/monsoonmayhem.json b/bots/clans/monsoonmayhem.json new file mode 100644 index 000000000..b98fbdf71 --- /dev/null +++ b/bots/clans/monsoonmayhem.json @@ -0,0 +1,6 @@ +{ + "clan_id": 49, + "clan_tag": "@MM+", + "clan_name": "Monsoon Mayhem", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/monsoonmortals.json b/bots/clans/monsoonmortals.json new file mode 100644 index 000000000..5e2ad15a8 --- /dev/null +++ b/bots/clans/monsoonmortals.json @@ -0,0 +1,6 @@ +{ + "clan_id": 48, + "clan_tag": "#MM#", + "clan_name": "Monsoon Mortals", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/moonbeamguardians.json b/bots/clans/moonbeamguardians.json new file mode 100644 index 000000000..c1fd65762 --- /dev/null +++ b/bots/clans/moonbeamguardians.json @@ -0,0 +1,6 @@ +{ + "clan_id": 57, + "clan_tag": "/MG/", + "clan_name": "Moonbeam Guardians", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/navysharks.json b/bots/clans/navysharks.json new file mode 100644 index 000000000..000fe9c27 --- /dev/null +++ b/bots/clans/navysharks.json @@ -0,0 +1,6 @@ +{ + "clan_id": 15, + "clan_tag": "\\NS\\", + "clan_name": "Navy Sharks", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/nebulaninjas.json b/bots/clans/nebulaninjas.json new file mode 100644 index 000000000..9af754ad6 --- /dev/null +++ b/bots/clans/nebulaninjas.json @@ -0,0 +1,6 @@ +{ + "clan_id": 72, + "clan_tag": "}NN&", + "clan_name": "Nebula Ninjas", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/nightstalkers.json b/bots/clans/nightstalkers.json new file mode 100644 index 000000000..5af7b785c --- /dev/null +++ b/bots/clans/nightstalkers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 31, + "clan_tag": "@N@", + "clan_name": "Nightstalkers", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/nocturnalninjas.json b/bots/clans/nocturnalninjas.json new file mode 100644 index 000000000..82b0ade92 --- /dev/null +++ b/bots/clans/nocturnalninjas.json @@ -0,0 +1,6 @@ +{ + "clan_id": 28, + "clan_tag": "(NN(", + "clan_name": "Nocturnal Ninjas", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/phoenixrangers.json b/bots/clans/phoenixrangers.json new file mode 100644 index 000000000..b6150a5ee --- /dev/null +++ b/bots/clans/phoenixrangers.json @@ -0,0 +1,6 @@ +{ + "clan_id": 56, + "clan_tag": "!PR!", + "clan_name": "Phoenix Rangers", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/pixelpleasure.json b/bots/clans/pixelpleasure.json new file mode 100644 index 000000000..4899810b0 --- /dev/null +++ b/bots/clans/pixelpleasure.json @@ -0,0 +1,6 @@ +{ + "clan_id": 84, + "clan_tag": "%PP)", + "clan_name": "Pixel Pleasure", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/policecommand.json b/bots/clans/policecommand.json new file mode 100644 index 000000000..e69ad44cf --- /dev/null +++ b/bots/clans/policecommand.json @@ -0,0 +1,6 @@ +{ + "clan_id": 12, + "clan_tag": "\\PC\\", + "clan_name": "Police Command", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/pulpfictionsquad.json b/bots/clans/pulpfictionsquad.json new file mode 100644 index 000000000..f3d1c29d0 --- /dev/null +++ b/bots/clans/pulpfictionsquad.json @@ -0,0 +1,6 @@ +{ + "clan_id": 74, + "clan_tag": "", + "clan_name": "Solar Showdown", + "pre_or_post": 1 +} \ No newline at end of file diff --git a/bots/clans/solarsuperheroes.json b/bots/clans/solarsuperheroes.json new file mode 100644 index 000000000..7faa58632 --- /dev/null +++ b/bots/clans/solarsuperheroes.json @@ -0,0 +1,6 @@ +{ + "clan_id": 53, + "clan_tag": ";SS:", + "clan_name": "Solar Superheroes", + "pre_or_post": 0 +} \ No newline at end of file diff --git a/bots/clans/sonicdoom.json b/bots/clans/sonicdoom.json new file mode 100644 index 000000000..c464b4324 --- /dev/null +++ b/bots/clans/sonicdoom.json @@ -0,0 +1,6 @@ +{ + "clan_id": 88, + "clan_tag": " Date: Tue, 27 Feb 2024 13:58:35 -0500 Subject: [PATCH 082/974] Re-ran botmaker and parser --- bots/botmaker.py | 7 +- bots/bots.bin | Bin 123146 -> 118939 bytes bots/bots_clan/2dcitizen.json | 35 - bots/bots_clan/_gholem_.json | 35 - bots/bots_clan/_glich.json | 35 - bots/bots_clan/_pixxy_.json | 46 +- bots/bots_clan/aegarond.json | 42 +- bots/bots_clan/aimless.json | 35 + bots/bots_clan/amadi.json | 35 - bots/bots_clan/amu.json | 35 - bots/bots_clan/angel.json | 35 + bots/bots_clan/anodized.json | 36 +- bots/bots_clan/ant__dog.json | 35 - bots/bots_clan/anton.json | 35 + bots/bots_clan/apem.json | 46 +- bots/bots_clan/applee.json | 44 +- bots/bots_clan/ashrah.json | 35 - bots/bots_clan/badjuju.json | 35 + bots/bots_clan/baraka.json | 35 - bots/bots_clan/bean.json | 35 - bots/bots_clan/belokk.json | 35 - bots/bots_clan/benarcher.json | 35 - bots/bots_clan/bitterman.json | 42 +- bots/bots_clan/blackadder.json | 35 - bots/bots_clan/bloodwitch.json | 35 + bots/bots_clan/blythe.json | 48 +- bots/bots_clan/bobross.json | 44 +- bots/bots_clan/bones.json | 48 +- bots/bots_clan/booker.json | 35 - bots/bots_clan/boy.json | 46 +- bots/bots_clan/brad.json | 40 +- bots/bots_clan/broz.json | 35 + bots/bots_clan/bullseye.json | 42 +- bots/bots_clan/bve.json | 35 - bots/bots_clan/cerberon.json | 46 +- bots/bots_clan/cetrion.json | 35 + bots/bots_clan/chameleon.json | 42 +- bots/bots_clan/chan.json | 35 - bots/bots_clan/chess.json | 35 + bots/bots_clan/chipp_.json | 35 + bots/bots_clan/choppy.json | 35 + bots/bots_clan/clion.json | 35 - bots/bots_clan/coldcrow.json | 44 +- bots/bots_clan/{reiko.json => coldworm.json} | 30 +- bots/bots_clan/colt.json | 35 + bots/bots_clan/conan.json | 44 +- bots/bots_clan/corpsefire.json | 35 + bots/bots_clan/cowchop.json | 35 + bots/bots_clan/crash.json | 35 - bots/bots_clan/cruton.json | 35 + bots/bots_clan/cybr.json | 35 - bots/bots_clan/dacfarren.json | 42 +- bots/bots_clan/daemia.json | 35 + bots/bots_clan/darkkahn.json | 35 + bots/bots_clan/deusvault.json | 35 - bots/bots_clan/dharmesh.json | 35 + bots/bots_clan/digz.json | 35 - bots/bots_clan/dinardo.json | 35 + bots/bots_clan/doc.json | 42 +- bots/bots_clan/dominator.json | 35 - bots/bots_clan/doom.json | 35 - bots/bots_clan/drahmin.json | 44 +- bots/bots_clan/drillbit.json | 35 - bots/bots_clan/drjay.json | 35 - bots/bots_clan/dvorah.json | 35 - bots/bots_clan/edoli.json | 35 + bots/bots_clan/edwig.json | 42 +- bots/bots_clan/endugu.json | 44 +- bots/bots_clan/exbee.json | 46 +- bots/bots_clan/eyeback.json | 44 +- bots/bots_clan/faden.json | 46 +- bots/bots_clan/fangskin.json | 46 +- bots/bots_clan/ferratorr.json | 35 + bots/bots_clan/fexel.json | 46 +- bots/bots_clan/figgz.json | 42 +- bots/bots_clan/flamespike.json | 35 - bots/bots_clan/flint.json | 35 - bots/bots_clan/frankdux.json | 35 + bots/bots_clan/fred.json | 35 + bots/bots_clan/frost.json | 44 +- bots/bots_clan/fujin.json | 35 + bots/bots_clan/fullmonty.json | 35 - bots/bots_clan/funkanik.json | 35 - bots/bots_clan/fxguy.json | 35 + bots/bots_clan/gali.json | 35 - bots/bots_clan/geras.json | 38 +- bots/bots_clan/gigavoltz.json | 35 + bots/bots_clan/gladiator.json | 35 - bots/bots_clan/gogman.json | 35 - bots/bots_clan/gordac.json | 35 + bots/bots_clan/gorre.json | 44 +- bots/bots_clan/grezzer.json | 44 +- bots/bots_clan/griswold.json | 50 +- bots/bots_clan/grunt.json | 35 - bots/bots_clan/gun.json | 35 - bots/bots_clan/gungho.json | 35 + bots/bots_clan/gunner.json | 42 +- bots/bots_clan/halfbaked.json | 35 + bots/bots_clan/hammerhead.json | 50 +- bots/bots_clan/havik.json | 35 + bots/bots_clan/havo.json | 35 - bots/bots_clan/hecker.json | 35 + bots/bots_clan/heljay.json | 35 + bots/bots_clan/helmut.json | 35 - bots/bots_clan/himdar.json | 32 +- bots/bots_clan/hitnrun.json | 46 +- bots/bots_clan/hossman.json | 46 +- bots/bots_clan/hotaru.json | 35 - bots/bots_clan/hsuhao.json | 38 +- bots/bots_clan/hunter.json | 35 - bots/bots_clan/hurricane.json | 44 +- bots/bots_clan/icarus.json | 44 +- bots/bots_clan/icehawk.json | 42 +- bots/bots_clan/icexo.json | 35 - bots/bots_clan/ismail.json | 38 +- bots/bots_clan/itixoid.json | 35 + bots/bots_clan/itzchamp.json | 35 - bots/bots_clan/ivor.json | 44 +- bots/bots_clan/jacksaxon.json | 35 - bots/bots_clan/jade.json | 35 - bots/bots_clan/jarek.json | 35 + bots/bots_clan/jarno.json | 46 +- bots/bots_clan/jarvis.json | 48 +- bots/bots_clan/jazz.json | 46 +- bots/bots_clan/jce.json | 42 +- bots/bots_clan/jdgold.json | 35 + bots/bots_clan/jinx.json | 35 + bots/bots_clan/joce.json | 35 + bots/bots_clan/joesanto.json | 35 - bots/bots_clan/joetanto.json | 35 - bots/bots_clan/johnluger.json | 42 +- bots/bots_clan/johnrambo.json | 35 + bots/bots_clan/kai.json | 35 + bots/bots_clan/kalidor.json | 35 - bots/bots_clan/kano.json | 35 + bots/bots_clan/kaolin.json | 42 +- bots/bots_clan/keel.json | 42 +- bots/bots_clan/khameleon.json | 35 + bots/bots_clan/kintaro.json | 35 - bots/bots_clan/kippy.json | 35 + bots/bots_clan/kitana.json | 35 - bots/bots_clan/kitlatura.json | 35 + bots/bots_clan/klesk.json | 44 +- bots/bots_clan/{strom.json => kobra.json} | 30 +- bots/bots_clan/kollector.json | 35 - bots/bots_clan/kor.json | 38 +- bots/bots_clan/kronika.json | 44 +- bots/bots_clan/kunglao.json | 46 +- bots/bots_clan/kuri.json | 35 - bots/bots_clan/lawrence.json | 35 - bots/bots_clan/leatherarm.json | 35 + bots/bots_clan/leiruth.json | 44 +- bots/bots_clan/lemmie.json | 35 + bots/bots_clan/limei.json | 35 - bots/bots_clan/liukang.json | 48 +- bots/bots_clan/lonewolf.json | 35 - bots/bots_clan/lucy.json | 35 + bots/bots_clan/maffer.json | 40 +- bots/bots_clan/{ermac.json => major.json} | 30 +- bots/bots_clan/majorpain.json | 35 + bots/bots_clan/makron.json | 35 + bots/bots_clan/malin.json | 38 +- bots/bots_clan/manny.json | 44 +- bots/bots_clan/marku.json | 40 +- bots/bots_clan/mavado.json | 40 +- bots/bots_clan/mckenna.json | 35 + bots/bots_clan/meat.json | 44 +- bots/bots_clan/medic.json | 42 +- bots/bots_clan/mee2.json | 46 +- bots/bots_clan/merki.json | 35 + bots/bots_clan/michael.json | 35 + bots/bots_clan/mileena.json | 35 + bots/bots_clan/mincer.json | 35 + bots/bots_clan/mindelos.json | 35 - bots/bots_clan/missnrun.json | 40 +- bots/bots_clan/mokap.json | 46 +- bots/bots_clan/monty.json | 35 - bots/bots_clan/morgan.json | 35 - bots/bots_clan/motaro.json | 35 + bots/bots_clan/mrbadger.json | 35 - bots/bots_clan/mrgoodkat.json | 48 +- bots/bots_clan/mrsuave.json | 35 + bots/bots_clan/mungri.json | 35 - bots/bots_clan/muscleman.json | 38 +- bots/bots_clan/mynx.json | 35 - bots/bots_clan/nandet.json | 35 - bots/bots_clan/newbie.json | 35 + bots/bots_clan/nibbler.json | 35 + bots/bots_clan/nightwolf.json | 35 - bots/bots_clan/nihlathak.json | 35 + bots/bots_clan/nin.json | 42 +- bots/bots_clan/nitara.json | 46 +- bots/bots_clan/nohope.json | 35 + bots/bots_clan/noogie.json | 35 - bots/bots_clan/onaga.json | 35 + bots/bots_clan/orbb.json | 42 +- bots/bots_clan/orinboyd.json | 42 +- bots/bots_clan/ostmor.json | 46 +- bots/bots_clan/oysterhead.json | 35 + bots/bots_clan/paradox.json | 40 +- bots/bots_clan/patriot.json | 46 +- bots/bots_clan/pewdiepie.json | 42 +- bots/bots_clan/pindleskin.json | 35 + bots/bots_clan/potaytoe.json | 35 - bots/bots_clan/pyroyetti.json | 35 - bots/bots_clan/radament.json | 34 +- bots/bots_clan/{wrack.json => rain.json} | 30 +- bots/bots_clan/rakanishu.json | 42 +- bots/bots_clan/ramrod.json | 46 +- bots/bots_clan/ranger.json | 35 + bots/bots_clan/ratbot.json | 42 +- bots/bots_clan/rayquick.json | 35 + bots/bots_clan/razor.json | 35 + bots/bots_clan/replicant.json | 35 - bots/bots_clan/richard.json | 35 + bots/bots_clan/rocky.json | 35 + bots/bots_clan/royen.json | 35 + bots/bots_clan/ruslan.json | 35 - bots/bots_clan/sagel.json | 35 - bots/bots_clan/sareena.json | 35 + bots/bots_clan/sarge.json | 44 +- bots/bots_clan/sarina.json | 40 +- bots/bots_clan/sean.json | 42 +- bots/bots_clan/seankane.json | 35 - bots/bots_clan/sektor.json | 44 +- bots/bots_clan/sgtcappa.json | 35 + .../{scorpion.json => shaokahn.json} | 28 +- bots/bots_clan/sheeva.json | 36 +- bots/bots_clan/shinnok.json | 35 - bots/bots_clan/shujinko.json | 35 + bots/bots_clan/si.json | 46 +- bots/bots_clan/sigil.json | 35 + bots/bots_clan/sindel.json | 35 - bots/bots_clan/skorgan.json | 35 + bots/bots_clan/slash.json | 35 - bots/bots_clan/slice.json | 35 + bots/bots_clan/smithy.json | 35 + bots/bots_clan/smoke.json | 35 + bots/bots_clan/snaps.json | 35 + bots/bots_clan/socks.json | 35 + bots/bots_clan/sorlag.json | 48 +- bots/bots_clan/specialed.json | 35 - bots/bots_clan/spocket.json | 35 - bots/bots_clan/steampug.json | 35 - bots/bots_clan/stinger.json | 35 - bots/bots_clan/stormtree.json | 35 - bots/bots_clan/strogg.json | 35 - bots/bots_clan/subzero.json | 42 +- bots/bots_clan/summoner.json | 42 +- bots/bots_clan/sundog.json | 48 +- bots/bots_clan/t1000.json | 35 - bots/bots_clan/t800.json | 50 +- bots/bots_clan/t_bon.json | 35 - bots/bots_clan/tahlkora.json | 35 - bots/bots_clan/tank.json | 48 +- bots/bots_clan/tanya.json | 35 - bots/bots_clan/tarnen.json | 35 + bots/bots_clan/tarnok.json | 35 + bots/bots_clan/taven.json | 42 +- bots/bots_clan/teabag.json | 44 +- bots/bots_clan/terminator.json | 35 + bots/bots_clan/thalath.json | 35 + bots/bots_clan/theboss.json | 50 +- bots/bots_clan/thetulip.json | 35 + bots/bots_clan/thor.json | 35 - bots/bots_clan/threash.json | 42 +- bots/bots_clan/tommix.json | 35 - bots/bots_clan/tomsteele.json | 48 +- bots/bots_clan/toorc.json | 35 - bots/bots_clan/tormentor.json | 35 + bots/bots_clan/treehead.json | 35 - bots/bots_clan/tremor.json | 42 +- bots/bots_clan/trench.json | 46 +- bots/bots_clan/turkish.json | 35 + bots/bots_clan/uriel.json | 46 +- bots/bots_clan/uzaytron.json | 42 +- bots/bots_clan/vedro.json | 44 +- bots/bots_clan/venz.json | 35 + bots/bots_clan/vera.json | 48 +- bots/bots_clan/visor.json | 35 - bots/bots_clan/walter.json | 35 - bots/bots_clan/warchild.json | 35 - bots/bots_clan/waxman.json | 35 + bots/bots_clan/wayofthe.json | 35 + bots/bots_clan/wens.json | 35 - bots/bots_clan/willitz.json | 35 - bots/bots_clan/xaero.json | 35 - bots/bots_clan/yarbro.json | 35 + bots/bots_clan/zarna.json | 35 - bots/bots_pub/2dcitizen.json | 35 + bots/bots_pub/_gholem_.json | 35 + bots/bots_pub/_glich.json | 35 + bots/bots_pub/_nixxy_.json | 38 +- bots/bots_pub/aimless.json | 35 - bots/bots_pub/amadi.json | 35 + bots/bots_pub/amu.json | 35 + bots/bots_pub/anakinskywalker.json | 35 - bots/bots_pub/anarki.json | 46 +- bots/bots_pub/andromeed.json | 48 +- bots/bots_pub/angel.json | 35 - bots/bots_pub/angeloprovolone.json | 35 - bots/bots_pub/ant__dog.json | 35 + bots/bots_pub/anton.json | 35 - bots/bots_pub/argo.json | 48 +- bots/bots_pub/arklon.json | 50 +- bots/bots_pub/ashrah.json | 35 + bots/bots_pub/backfire.json | 40 +- bots/bots_pub/bacon.json | 40 +- bots/bots_pub/badsam.json | 42 +- bots/bots_pub/baraka.json | 35 + bots/bots_pub/barbarian.json | 46 +- bots/bots_pub/bean.json | 35 + bots/bots_pub/beelz.json | 44 +- bots/bots_pub/belokk.json | 35 + bots/bots_pub/benarcher.json | 35 + bots/bots_pub/berserker.json | 35 + bots/bots_pub/bigmack.json | 44 +- bots/bots_pub/biker.json | 48 +- bots/bots_pub/bishibosh.json | 44 +- bots/bots_pub/blackadder.json | 35 + bots/bots_pub/blaze.json | 42 +- bots/bots_pub/bloodwitch.json | 35 - bots/bots_pub/bolund.json | 42 +- bots/bots_pub/boneash.json | 42 +- bots/bots_pub/bonebreak.json | 42 +- bots/bots_pub/booker.json | 35 + bots/bots_pub/bortack.json | 44 +- bots/bots_pub/bremm.json | 44 +- bots/bots_pub/brixos.json | 46 +- bots/bots_pub/brofist.json | 42 +- bots/bots_pub/brohn.json | 42 +- bots/bots_pub/broz.json | 35 - bots/bots_pub/bve.json | 35 + bots/bots_pub/cadaver.json | 40 +- bots/bots_pub/captivandanko.json | 35 - bots/bots_pub/carpenter.json | 46 +- bots/bots_pub/cetrion.json | 35 - bots/bots_pub/chan.json | 35 + bots/bots_pub/chanceboudreaux.json | 35 - bots/bots_pub/chess.json | 35 - bots/bots_pub/chipp_.json | 35 - bots/bots_pub/choppy.json | 35 - bots/bots_pub/church.json | 46 +- bots/bots_pub/cledus.json | 38 +- bots/bots_pub/clion.json | 35 + bots/bots_pub/cobra.json | 46 +- bots/bots_pub/coldworm.json | 35 - bots/bots_pub/colt.json | 35 - bots/bots_pub/corpsefire.json | 35 - bots/bots_pub/corrus.json | 46 +- bots/bots_pub/countess.json | 42 +- bots/bots_pub/cowchop.json | 35 - bots/bots_pub/cowking.json | 44 +- bots/bots_pub/crash.json | 35 + bots/bots_pub/cruton.json | 35 - bots/bots_pub/cybr.json | 35 + bots/bots_pub/cyrax.json | 44 +- bots/bots_pub/daegon.json | 34 +- bots/bots_pub/daemia.json | 35 - bots/bots_pub/dairou.json | 38 +- bots/bots_pub/darkkahn.json | 35 - bots/bots_pub/darrius.json | 40 +- bots/bots_pub/death.json | 40 +- bots/bots_pub/deusvault.json | 35 + bots/bots_pub/dharmesh.json | 35 - bots/bots_pub/digz.json | 35 + bots/bots_pub/dinardo.json | 35 - bots/bots_pub/disfordigger.json | 35 - bots/bots_pub/doctordeathwish.json | 35 - bots/bots_pub/dolin.json | 38 +- bots/bots_pub/dominator.json | 35 + bots/bots_pub/doom.json | 35 + bots/bots_pub/drago.json | 40 +- .../{indianajones.json => dragoooon.json} | 30 +- bots/bots_pub/drillbit.json | 35 + bots/bots_pub/drjay.json | 35 + bots/bots_pub/dutch.json | 38 +- .../dragoooon.json => bots_pub/dvorah.json} | 32 +- bots/bots_pub/edoli.json | 35 - bots/bots_pub/ellanil.json | 42 +- bots/bots_pub/enforcer.json | 44 +- bots/bots_pub/ermac.json | 35 + bots/bots_pub/evo89.json | 42 +- bots/bots_pub/ferratorr.json | 35 - bots/bots_pub/flamespike.json | 35 + bots/bots_pub/flint.json | 35 + bots/bots_pub/frankdux.json | 35 - bots/bots_pub/fred.json | 35 - bots/bots_pub/frenchy.json | 46 +- bots/bots_pub/fujin.json | 35 - bots/bots_pub/fullmonty.json | 35 + bots/bots_pub/funkanik.json | 35 + bots/bots_pub/fxguy.json | 35 - bots/bots_pub/gali.json | 35 + bots/bots_pub/galul.json | 42 +- bots/bots_pub/garbok.json | 46 +- bots/bots_pub/geleb.json | 40 +- bots/bots_pub/george.json | 48 +- bots/bots_pub/ghostbl4de.json | 34 +- bots/bots_pub/gigavoltz.json | 35 - bots/bots_pub/gladiator.json | 35 + bots/bots_pub/gogman.json | 35 + bots/bots_pub/gordac.json | 35 - bots/bots_pub/grimm.json | 50 +- bots/bots_pub/grog.json | 46 +- bots/bots_pub/grunt.json | 35 + bots/bots_pub/gun.json | 35 + bots/bots_pub/gungho.json | 35 - bots/bots_pub/hal9000.json | 46 +- bots/bots_pub/halfbaked.json | 35 - bots/bots_pub/halfwit.json | 44 +- bots/bots_pub/hambone.json | 44 +- bots/bots_pub/havo.json | 35 + bots/bots_pub/hecker.json | 35 - bots/bots_pub/heljay.json | 35 - bots/bots_pub/hellhound.json | 44 +- bots/bots_pub/helmut.json | 35 + bots/bots_pub/himself.json | 44 +- bots/bots_pub/hotaru.json | 35 + bots/bots_pub/hozef8.json | 38 +- bots/bots_pub/hunter.json | 35 + bots/bots_pub/icexo.json | 35 + bots/bots_pub/imentu.json | 46 +- bots/bots_pub/itixoid.json | 35 - bots/bots_pub/itzchamp.json | 35 + bots/bots_pub/ivandrago.json | 44 +- bots/bots_pub/ivankraschinsky.json | 35 - bots/bots_pub/jacksaxon.json | 35 + bots/bots_pub/jacqueskristoff.json | 35 - bots/bots_pub/jade.json | 35 + bots/bots_pub/jarek.json | 35 - bots/bots_pub/jax.json | 44 +- bots/bots_pub/jcdenton.json | 44 +- bots/bots_pub/jdgold.json | 35 - bots/bots_pub/jinx.json | 35 - bots/bots_pub/jjmcquade.json | 44 +- bots/bots_pub/joce.json | 35 - bots/bots_pub/joejoiner.json | 36 +- bots/bots_pub/joesanto.json | 35 + bots/bots_pub/joetanto.json | 35 + bots/bots_pub/johnrambo.json | 35 - bots/bots_pub/kaa.json | 44 +- bots/bots_pub/kabal.json | 42 +- bots/bots_pub/kai.json | 35 - bots/bots_pub/kalidor.json | 35 + bots/bots_pub/kano.json | 35 - bots/bots_pub/kenshi.json | 48 +- bots/bots_pub/khameleon.json | 35 - bots/bots_pub/kintaro.json | 35 + bots/bots_pub/kippy.json | 35 - bots/bots_pub/kira.json | 42 +- bots/bots_pub/kitana.json | 35 + bots/bots_pub/kitlatura.json | 35 - bots/bots_pub/kobra.json | 35 - bots/bots_pub/kole.json | 46 +- bots/bots_pub/{havik.json => kollector.json} | 30 +- bots/bots_pub/korv.json | 42 +- bots/bots_pub/kotalkahn.json | 40 +- bots/bots_pub/kungjin.json | 44 +- .../berserker.json => bots_pub/kuri.json} | 32 +- bots/bots_pub/lawrence.json | 35 + bots/bots_pub/leatherarm.json | 35 - bots/bots_pub/lemmie.json | 35 - bots/bots_pub/limei.json | 35 + bots/bots_pub/littleg.json | 42 +- bots/bots_pub/{badjuju.json => lonewolf.json} | 28 +- bots/bots_pub/ltraymondtango.json | 35 - bots/bots_pub/lucy.json | 35 - bots/bots_pub/major.json | 35 - bots/bots_pub/majorpain.json | 35 - bots/bots_pub/makron.json | 35 - bots/bots_pub/mckenna.json | 35 - bots/bots_pub/merki.json | 35 - bots/bots_pub/michael.json | 35 - bots/bots_pub/mileena.json | 35 - bots/bots_pub/milius.json | 40 +- bots/bots_pub/mincer.json | 35 - bots/bots_pub/mindelos.json | 35 + bots/bots_pub/moloch.json | 44 +- bots/bots_pub/mongrel.json | 48 +- bots/bots_pub/monk.json | 35 + bots/bots_pub/monty.json | 35 + bots/bots_pub/morgan.json | 35 + bots/bots_pub/morgriff.json | 40 +- bots/bots_pub/motaro.json | 35 - bots/bots_pub/mrbadger.json | 35 + bots/bots_pub/mrsuave.json | 35 - bots/bots_pub/mungri.json | 35 + bots/bots_pub/mynx.json | 35 + bots/bots_pub/nandet.json | 35 + bots/bots_pub/newbie.json | 35 - bots/bots_pub/nibbler.json | 35 - bots/bots_pub/nickgunar.json | 48 +- bots/bots_pub/nightwolf.json | 35 + bots/bots_pub/nihlathak.json | 35 - bots/bots_pub/nikolaicherenko.json | 35 - bots/bots_pub/noammo.json | 44 +- bots/bots_pub/nohope.json | 35 - bots/bots_pub/noogie.json | 35 + bots/bots_pub/nundak.json | 46 +- bots/bots_pub/onaga.json | 35 - bots/bots_pub/oysterhead.json | 35 - bots/bots_pub/phobos.json | 40 +- bots/bots_pub/pindleskin.json | 35 - bots/bots_pub/pitspawnfouldog.json | 35 - bots/bots_pub/potaytoe.json | 35 + bots/bots_pub/pyroyetti.json | 35 + bots/bots_pub/quanchi.json | 42 +- bots/bots_pub/raiden.json | 44 +- bots/bots_pub/rain.json | 35 - bots/bots_pub/ranger.json | 35 - bots/bots_pub/rayquick.json | 35 - bots/bots_pub/razor.json | 48 +- bots/bots_pub/reddog.json | 48 +- bots/bots_pub/reiko.json | 35 + bots/bots_pub/replicant.json | 35 + bots/bots_pub/reptile.json | 46 +- bots/bots_pub/richard.json | 35 - bots/bots_pub/riftwraith.json | 48 +- bots/bots_pub/rocky.json | 35 - bots/bots_pub/royen.json | 35 - bots/bots_pub/ruslan.json | 35 + bots/bots_pub/ryder.json | 42 +- bots/bots_pub/ryver.json | 42 +- bots/bots_pub/saboy.json | 42 +- bots/bots_pub/sagel.json | 35 + bots/bots_pub/samdecker.json | 44 +- bots/bots_pub/sareena.json | 35 - bots/bots_pub/scorpion.json | 35 + bots/bots_pub/seankane.json | 35 + bots/bots_pub/sgtcappa.json | 35 - bots/bots_pub/shaokahn.json | 35 - bots/bots_pub/shinnok.json | 35 + bots/bots_pub/shujinko.json | 35 - bots/bots_pub/siege.json | 46 +- bots/bots_pub/sigil.json | 35 - bots/bots_pub/sindel.json | 35 + bots/bots_pub/skarlet.json | 42 +- bots/bots_pub/skorgan.json | 35 - bots/bots_pub/slash.json | 35 + bots/bots_pub/slice.json | 35 - bots/bots_pub/smithy.json | 35 - bots/bots_pub/smoke.json | 35 - bots/bots_pub/snapchipshatter.json | 35 - bots/bots_pub/snaps.json | 35 - bots/bots_pub/socks.json | 35 - bots/bots_pub/spar.json | 50 +- bots/bots_pub/specialed.json | 35 + bots/bots_pub/spocket.json | 35 + bots/bots_pub/steampug.json | 35 + bots/bots_pub/stinger.json | 35 + bots/bots_pub/stormtree.json | 35 + bots/bots_pub/stripe.json | 40 +- bots/bots_pub/strogg.json | 35 + bots/bots_pub/strom.json | 35 + bots/bots_pub/stryker.json | 40 +- bots/bots_pub/stud.json | 44 +- bots/bots_pub/t1000.json | 35 + bots/bots_pub/t_bon.json | 35 + bots/bots_pub/tahlkora.json | 35 + bots/bots_pub/takedatakahashi.json | 35 - bots/bots_pub/tankjr.json | 42 +- bots/bots_pub/tanya.json | 35 + bots/bots_pub/tanz.json | 46 +- bots/bots_pub/tao.json | 44 +- bots/bots_pub/tarnen.json | 35 - bots/bots_pub/tarnok.json | 35 - bots/bots_pub/tatu.json | 44 +- bots/bots_pub/technician.json | 44 +- bots/bots_pub/terminator.json | 35 - bots/bots_pub/thalath.json | 35 - bots/bots_pub/thedean.json | 44 +- bots/bots_pub/theeraser.json | 46 +- bots/bots_pub/thegerman.json | 48 +- bots/bots_pub/theonly.json | 44 +- bots/bots_pub/thetulip.json | 35 - bots/bots_pub/theway.json | 42 +- bots/bots_pub/thor.json | 35 + bots/bots_pub/tiano.json | 48 +- bots/bots_pub/tommix.json | 35 + .../monk.json => bots_pub/toorc.json} | 30 +- bots/bots_pub/tormentor.json | 35 - bots/bots_pub/treehead.json | 35 + bots/bots_pub/triborg.json | 44 +- bots/bots_pub/turkish.json | 35 - bots/bots_pub/vanoss.json | 42 +- bots/bots_pub/venom.json | 38 +- bots/bots_pub/venz.json | 35 - bots/bots_pub/vey.json | 46 +- bots/bots_pub/vic.json | 42 +- bots/bots_pub/visor.json | 35 + bots/bots_pub/vizier.json | 50 +- bots/bots_pub/vokur.json | 44 +- bots/bots_pub/walter.json | 35 + bots/bots_pub/warchild.json | 35 + bots/bots_pub/waxman.json | 35 - bots/bots_pub/wayofthe.json | 35 - bots/bots_pub/wens.json | 35 + bots/bots_pub/whyarewehere.json | 35 - bots/bots_pub/whyme.json | 42 +- bots/bots_pub/willitz.json | 35 + bots/bots_pub/wrack.json | 35 + bots/bots_pub/wreckage.json | 46 +- bots/bots_pub/wyand.json | 46 +- bots/bots_pub/wysyyy.json | 48 +- bots/bots_pub/xaero.json | 35 + bots/bots_pub/yarbro.json | 35 - bots/bots_pub/zarna.json | 35 + bots/index.txt | 838 +++++++++--------- 609 files changed, 11682 insertions(+), 12185 deletions(-) delete mode 100644 bots/bots_clan/2dcitizen.json delete mode 100644 bots/bots_clan/_gholem_.json delete mode 100644 bots/bots_clan/_glich.json create mode 100644 bots/bots_clan/aimless.json delete mode 100644 bots/bots_clan/amadi.json delete mode 100644 bots/bots_clan/amu.json create mode 100644 bots/bots_clan/angel.json delete mode 100644 bots/bots_clan/ant__dog.json create mode 100644 bots/bots_clan/anton.json delete mode 100644 bots/bots_clan/ashrah.json create mode 100644 bots/bots_clan/badjuju.json delete mode 100644 bots/bots_clan/baraka.json delete mode 100644 bots/bots_clan/bean.json delete mode 100644 bots/bots_clan/belokk.json delete mode 100644 bots/bots_clan/benarcher.json delete mode 100644 bots/bots_clan/blackadder.json create mode 100644 bots/bots_clan/bloodwitch.json delete mode 100644 bots/bots_clan/booker.json create mode 100644 bots/bots_clan/broz.json delete mode 100644 bots/bots_clan/bve.json create mode 100644 bots/bots_clan/cetrion.json delete mode 100644 bots/bots_clan/chan.json create mode 100644 bots/bots_clan/chess.json create mode 100644 bots/bots_clan/chipp_.json create mode 100644 bots/bots_clan/choppy.json delete mode 100644 bots/bots_clan/clion.json rename bots/bots_clan/{reiko.json => coldworm.json} (52%) create mode 100644 bots/bots_clan/colt.json create mode 100644 bots/bots_clan/corpsefire.json create mode 100644 bots/bots_clan/cowchop.json delete mode 100644 bots/bots_clan/crash.json create mode 100644 bots/bots_clan/cruton.json delete mode 100644 bots/bots_clan/cybr.json create mode 100644 bots/bots_clan/daemia.json create mode 100644 bots/bots_clan/darkkahn.json delete mode 100644 bots/bots_clan/deusvault.json create mode 100644 bots/bots_clan/dharmesh.json delete mode 100644 bots/bots_clan/digz.json create mode 100644 bots/bots_clan/dinardo.json delete mode 100644 bots/bots_clan/dominator.json delete mode 100644 bots/bots_clan/doom.json delete mode 100644 bots/bots_clan/drillbit.json delete mode 100644 bots/bots_clan/drjay.json delete mode 100644 bots/bots_clan/dvorah.json create mode 100644 bots/bots_clan/edoli.json create mode 100644 bots/bots_clan/ferratorr.json delete mode 100644 bots/bots_clan/flamespike.json delete mode 100644 bots/bots_clan/flint.json create mode 100644 bots/bots_clan/frankdux.json create mode 100644 bots/bots_clan/fred.json create mode 100644 bots/bots_clan/fujin.json delete mode 100644 bots/bots_clan/fullmonty.json delete mode 100644 bots/bots_clan/funkanik.json create mode 100644 bots/bots_clan/fxguy.json delete mode 100644 bots/bots_clan/gali.json create mode 100644 bots/bots_clan/gigavoltz.json delete mode 100644 bots/bots_clan/gladiator.json delete mode 100644 bots/bots_clan/gogman.json create mode 100644 bots/bots_clan/gordac.json delete mode 100644 bots/bots_clan/grunt.json delete mode 100644 bots/bots_clan/gun.json create mode 100644 bots/bots_clan/gungho.json create mode 100644 bots/bots_clan/halfbaked.json create mode 100644 bots/bots_clan/havik.json delete mode 100644 bots/bots_clan/havo.json create mode 100644 bots/bots_clan/hecker.json create mode 100644 bots/bots_clan/heljay.json delete mode 100644 bots/bots_clan/helmut.json delete mode 100644 bots/bots_clan/hotaru.json delete mode 100644 bots/bots_clan/hunter.json delete mode 100644 bots/bots_clan/icexo.json create mode 100644 bots/bots_clan/itixoid.json delete mode 100644 bots/bots_clan/itzchamp.json delete mode 100644 bots/bots_clan/jacksaxon.json delete mode 100644 bots/bots_clan/jade.json create mode 100644 bots/bots_clan/jarek.json create mode 100644 bots/bots_clan/jdgold.json create mode 100644 bots/bots_clan/jinx.json create mode 100644 bots/bots_clan/joce.json delete mode 100644 bots/bots_clan/joesanto.json delete mode 100644 bots/bots_clan/joetanto.json create mode 100644 bots/bots_clan/johnrambo.json create mode 100644 bots/bots_clan/kai.json delete mode 100644 bots/bots_clan/kalidor.json create mode 100644 bots/bots_clan/kano.json create mode 100644 bots/bots_clan/khameleon.json delete mode 100644 bots/bots_clan/kintaro.json create mode 100644 bots/bots_clan/kippy.json delete mode 100644 bots/bots_clan/kitana.json create mode 100644 bots/bots_clan/kitlatura.json rename bots/bots_clan/{strom.json => kobra.json} (50%) delete mode 100644 bots/bots_clan/kollector.json delete mode 100644 bots/bots_clan/kuri.json delete mode 100644 bots/bots_clan/lawrence.json create mode 100644 bots/bots_clan/leatherarm.json create mode 100644 bots/bots_clan/lemmie.json delete mode 100644 bots/bots_clan/limei.json delete mode 100644 bots/bots_clan/lonewolf.json create mode 100644 bots/bots_clan/lucy.json rename bots/bots_clan/{ermac.json => major.json} (52%) create mode 100644 bots/bots_clan/majorpain.json create mode 100644 bots/bots_clan/makron.json create mode 100644 bots/bots_clan/mckenna.json create mode 100644 bots/bots_clan/merki.json create mode 100644 bots/bots_clan/michael.json create mode 100644 bots/bots_clan/mileena.json create mode 100644 bots/bots_clan/mincer.json delete mode 100644 bots/bots_clan/mindelos.json delete mode 100644 bots/bots_clan/monty.json delete mode 100644 bots/bots_clan/morgan.json create mode 100644 bots/bots_clan/motaro.json delete mode 100644 bots/bots_clan/mrbadger.json create mode 100644 bots/bots_clan/mrsuave.json delete mode 100644 bots/bots_clan/mungri.json delete mode 100644 bots/bots_clan/mynx.json delete mode 100644 bots/bots_clan/nandet.json create mode 100644 bots/bots_clan/newbie.json create mode 100644 bots/bots_clan/nibbler.json delete mode 100644 bots/bots_clan/nightwolf.json create mode 100644 bots/bots_clan/nihlathak.json create mode 100644 bots/bots_clan/nohope.json delete mode 100644 bots/bots_clan/noogie.json create mode 100644 bots/bots_clan/onaga.json create mode 100644 bots/bots_clan/oysterhead.json create mode 100644 bots/bots_clan/pindleskin.json delete mode 100644 bots/bots_clan/potaytoe.json delete mode 100644 bots/bots_clan/pyroyetti.json rename bots/bots_clan/{wrack.json => rain.json} (55%) create mode 100644 bots/bots_clan/ranger.json create mode 100644 bots/bots_clan/rayquick.json create mode 100644 bots/bots_clan/razor.json delete mode 100644 bots/bots_clan/replicant.json create mode 100644 bots/bots_clan/richard.json create mode 100644 bots/bots_clan/rocky.json create mode 100644 bots/bots_clan/royen.json delete mode 100644 bots/bots_clan/ruslan.json delete mode 100644 bots/bots_clan/sagel.json create mode 100644 bots/bots_clan/sareena.json delete mode 100644 bots/bots_clan/seankane.json create mode 100644 bots/bots_clan/sgtcappa.json rename bots/bots_clan/{scorpion.json => shaokahn.json} (54%) delete mode 100644 bots/bots_clan/shinnok.json create mode 100644 bots/bots_clan/shujinko.json create mode 100644 bots/bots_clan/sigil.json delete mode 100644 bots/bots_clan/sindel.json create mode 100644 bots/bots_clan/skorgan.json delete mode 100644 bots/bots_clan/slash.json create mode 100644 bots/bots_clan/slice.json create mode 100644 bots/bots_clan/smithy.json create mode 100644 bots/bots_clan/smoke.json create mode 100644 bots/bots_clan/snaps.json create mode 100644 bots/bots_clan/socks.json delete mode 100644 bots/bots_clan/specialed.json delete mode 100644 bots/bots_clan/spocket.json delete mode 100644 bots/bots_clan/steampug.json delete mode 100644 bots/bots_clan/stinger.json delete mode 100644 bots/bots_clan/stormtree.json delete mode 100644 bots/bots_clan/strogg.json delete mode 100644 bots/bots_clan/t1000.json delete mode 100644 bots/bots_clan/t_bon.json delete mode 100644 bots/bots_clan/tahlkora.json delete mode 100644 bots/bots_clan/tanya.json create mode 100644 bots/bots_clan/tarnen.json create mode 100644 bots/bots_clan/tarnok.json create mode 100644 bots/bots_clan/terminator.json create mode 100644 bots/bots_clan/thalath.json create mode 100644 bots/bots_clan/thetulip.json delete mode 100644 bots/bots_clan/thor.json delete mode 100644 bots/bots_clan/tommix.json delete mode 100644 bots/bots_clan/toorc.json create mode 100644 bots/bots_clan/tormentor.json delete mode 100644 bots/bots_clan/treehead.json create mode 100644 bots/bots_clan/turkish.json create mode 100644 bots/bots_clan/venz.json delete mode 100644 bots/bots_clan/visor.json delete mode 100644 bots/bots_clan/walter.json delete mode 100644 bots/bots_clan/warchild.json create mode 100644 bots/bots_clan/waxman.json create mode 100644 bots/bots_clan/wayofthe.json delete mode 100644 bots/bots_clan/wens.json delete mode 100644 bots/bots_clan/willitz.json delete mode 100644 bots/bots_clan/xaero.json create mode 100644 bots/bots_clan/yarbro.json delete mode 100644 bots/bots_clan/zarna.json create mode 100644 bots/bots_pub/2dcitizen.json create mode 100644 bots/bots_pub/_gholem_.json create mode 100644 bots/bots_pub/_glich.json delete mode 100644 bots/bots_pub/aimless.json create mode 100644 bots/bots_pub/amadi.json create mode 100644 bots/bots_pub/amu.json delete mode 100644 bots/bots_pub/anakinskywalker.json delete mode 100644 bots/bots_pub/angel.json delete mode 100644 bots/bots_pub/angeloprovolone.json create mode 100644 bots/bots_pub/ant__dog.json delete mode 100644 bots/bots_pub/anton.json create mode 100644 bots/bots_pub/ashrah.json create mode 100644 bots/bots_pub/baraka.json create mode 100644 bots/bots_pub/bean.json create mode 100644 bots/bots_pub/belokk.json create mode 100644 bots/bots_pub/benarcher.json create mode 100644 bots/bots_pub/berserker.json create mode 100644 bots/bots_pub/blackadder.json delete mode 100644 bots/bots_pub/bloodwitch.json create mode 100644 bots/bots_pub/booker.json delete mode 100644 bots/bots_pub/broz.json create mode 100644 bots/bots_pub/bve.json delete mode 100644 bots/bots_pub/captivandanko.json delete mode 100644 bots/bots_pub/cetrion.json create mode 100644 bots/bots_pub/chan.json delete mode 100644 bots/bots_pub/chanceboudreaux.json delete mode 100644 bots/bots_pub/chess.json delete mode 100644 bots/bots_pub/chipp_.json delete mode 100644 bots/bots_pub/choppy.json create mode 100644 bots/bots_pub/clion.json delete mode 100644 bots/bots_pub/coldworm.json delete mode 100644 bots/bots_pub/colt.json delete mode 100644 bots/bots_pub/corpsefire.json delete mode 100644 bots/bots_pub/cowchop.json create mode 100644 bots/bots_pub/crash.json delete mode 100644 bots/bots_pub/cruton.json create mode 100644 bots/bots_pub/cybr.json delete mode 100644 bots/bots_pub/daemia.json delete mode 100644 bots/bots_pub/darkkahn.json create mode 100644 bots/bots_pub/deusvault.json delete mode 100644 bots/bots_pub/dharmesh.json create mode 100644 bots/bots_pub/digz.json delete mode 100644 bots/bots_pub/dinardo.json delete mode 100644 bots/bots_pub/disfordigger.json delete mode 100644 bots/bots_pub/doctordeathwish.json create mode 100644 bots/bots_pub/dominator.json create mode 100644 bots/bots_pub/doom.json rename bots/bots_pub/{indianajones.json => dragoooon.json} (54%) create mode 100644 bots/bots_pub/drillbit.json create mode 100644 bots/bots_pub/drjay.json rename bots/{bots_clan/dragoooon.json => bots_pub/dvorah.json} (51%) delete mode 100644 bots/bots_pub/edoli.json create mode 100644 bots/bots_pub/ermac.json delete mode 100644 bots/bots_pub/ferratorr.json create mode 100644 bots/bots_pub/flamespike.json create mode 100644 bots/bots_pub/flint.json delete mode 100644 bots/bots_pub/frankdux.json delete mode 100644 bots/bots_pub/fred.json delete mode 100644 bots/bots_pub/fujin.json create mode 100644 bots/bots_pub/fullmonty.json create mode 100644 bots/bots_pub/funkanik.json delete mode 100644 bots/bots_pub/fxguy.json create mode 100644 bots/bots_pub/gali.json delete mode 100644 bots/bots_pub/gigavoltz.json create mode 100644 bots/bots_pub/gladiator.json create mode 100644 bots/bots_pub/gogman.json delete mode 100644 bots/bots_pub/gordac.json create mode 100644 bots/bots_pub/grunt.json create mode 100644 bots/bots_pub/gun.json delete mode 100644 bots/bots_pub/gungho.json delete mode 100644 bots/bots_pub/halfbaked.json create mode 100644 bots/bots_pub/havo.json delete mode 100644 bots/bots_pub/hecker.json delete mode 100644 bots/bots_pub/heljay.json create mode 100644 bots/bots_pub/helmut.json create mode 100644 bots/bots_pub/hotaru.json create mode 100644 bots/bots_pub/hunter.json create mode 100644 bots/bots_pub/icexo.json delete mode 100644 bots/bots_pub/itixoid.json create mode 100644 bots/bots_pub/itzchamp.json delete mode 100644 bots/bots_pub/ivankraschinsky.json create mode 100644 bots/bots_pub/jacksaxon.json delete mode 100644 bots/bots_pub/jacqueskristoff.json create mode 100644 bots/bots_pub/jade.json delete mode 100644 bots/bots_pub/jarek.json delete mode 100644 bots/bots_pub/jdgold.json delete mode 100644 bots/bots_pub/jinx.json delete mode 100644 bots/bots_pub/joce.json create mode 100644 bots/bots_pub/joesanto.json create mode 100644 bots/bots_pub/joetanto.json delete mode 100644 bots/bots_pub/johnrambo.json delete mode 100644 bots/bots_pub/kai.json create mode 100644 bots/bots_pub/kalidor.json delete mode 100644 bots/bots_pub/kano.json delete mode 100644 bots/bots_pub/khameleon.json create mode 100644 bots/bots_pub/kintaro.json delete mode 100644 bots/bots_pub/kippy.json create mode 100644 bots/bots_pub/kitana.json delete mode 100644 bots/bots_pub/kitlatura.json delete mode 100644 bots/bots_pub/kobra.json rename bots/bots_pub/{havik.json => kollector.json} (51%) rename bots/{bots_clan/berserker.json => bots_pub/kuri.json} (50%) create mode 100644 bots/bots_pub/lawrence.json delete mode 100644 bots/bots_pub/leatherarm.json delete mode 100644 bots/bots_pub/lemmie.json create mode 100644 bots/bots_pub/limei.json rename bots/bots_pub/{badjuju.json => lonewolf.json} (53%) delete mode 100644 bots/bots_pub/ltraymondtango.json delete mode 100644 bots/bots_pub/lucy.json delete mode 100644 bots/bots_pub/major.json delete mode 100644 bots/bots_pub/majorpain.json delete mode 100644 bots/bots_pub/makron.json delete mode 100644 bots/bots_pub/mckenna.json delete mode 100644 bots/bots_pub/merki.json delete mode 100644 bots/bots_pub/michael.json delete mode 100644 bots/bots_pub/mileena.json delete mode 100644 bots/bots_pub/mincer.json create mode 100644 bots/bots_pub/mindelos.json create mode 100644 bots/bots_pub/monk.json create mode 100644 bots/bots_pub/monty.json create mode 100644 bots/bots_pub/morgan.json delete mode 100644 bots/bots_pub/motaro.json create mode 100644 bots/bots_pub/mrbadger.json delete mode 100644 bots/bots_pub/mrsuave.json create mode 100644 bots/bots_pub/mungri.json create mode 100644 bots/bots_pub/mynx.json create mode 100644 bots/bots_pub/nandet.json delete mode 100644 bots/bots_pub/newbie.json delete mode 100644 bots/bots_pub/nibbler.json create mode 100644 bots/bots_pub/nightwolf.json delete mode 100644 bots/bots_pub/nihlathak.json delete mode 100644 bots/bots_pub/nikolaicherenko.json delete mode 100644 bots/bots_pub/nohope.json create mode 100644 bots/bots_pub/noogie.json delete mode 100644 bots/bots_pub/onaga.json delete mode 100644 bots/bots_pub/oysterhead.json delete mode 100644 bots/bots_pub/pindleskin.json delete mode 100644 bots/bots_pub/pitspawnfouldog.json create mode 100644 bots/bots_pub/potaytoe.json create mode 100644 bots/bots_pub/pyroyetti.json delete mode 100644 bots/bots_pub/rain.json delete mode 100644 bots/bots_pub/ranger.json delete mode 100644 bots/bots_pub/rayquick.json create mode 100644 bots/bots_pub/reiko.json create mode 100644 bots/bots_pub/replicant.json delete mode 100644 bots/bots_pub/richard.json delete mode 100644 bots/bots_pub/rocky.json delete mode 100644 bots/bots_pub/royen.json create mode 100644 bots/bots_pub/ruslan.json create mode 100644 bots/bots_pub/sagel.json delete mode 100644 bots/bots_pub/sareena.json create mode 100644 bots/bots_pub/scorpion.json create mode 100644 bots/bots_pub/seankane.json delete mode 100644 bots/bots_pub/sgtcappa.json delete mode 100644 bots/bots_pub/shaokahn.json create mode 100644 bots/bots_pub/shinnok.json delete mode 100644 bots/bots_pub/shujinko.json delete mode 100644 bots/bots_pub/sigil.json create mode 100644 bots/bots_pub/sindel.json delete mode 100644 bots/bots_pub/skorgan.json create mode 100644 bots/bots_pub/slash.json delete mode 100644 bots/bots_pub/slice.json delete mode 100644 bots/bots_pub/smithy.json delete mode 100644 bots/bots_pub/smoke.json delete mode 100644 bots/bots_pub/snapchipshatter.json delete mode 100644 bots/bots_pub/snaps.json delete mode 100644 bots/bots_pub/socks.json create mode 100644 bots/bots_pub/specialed.json create mode 100644 bots/bots_pub/spocket.json create mode 100644 bots/bots_pub/steampug.json create mode 100644 bots/bots_pub/stinger.json create mode 100644 bots/bots_pub/stormtree.json create mode 100644 bots/bots_pub/strogg.json create mode 100644 bots/bots_pub/strom.json create mode 100644 bots/bots_pub/t1000.json create mode 100644 bots/bots_pub/t_bon.json create mode 100644 bots/bots_pub/tahlkora.json delete mode 100644 bots/bots_pub/takedatakahashi.json create mode 100644 bots/bots_pub/tanya.json delete mode 100644 bots/bots_pub/tarnen.json delete mode 100644 bots/bots_pub/tarnok.json delete mode 100644 bots/bots_pub/terminator.json delete mode 100644 bots/bots_pub/thalath.json delete mode 100644 bots/bots_pub/thetulip.json create mode 100644 bots/bots_pub/thor.json create mode 100644 bots/bots_pub/tommix.json rename bots/{bots_clan/monk.json => bots_pub/toorc.json} (51%) delete mode 100644 bots/bots_pub/tormentor.json create mode 100644 bots/bots_pub/treehead.json delete mode 100644 bots/bots_pub/turkish.json delete mode 100644 bots/bots_pub/venz.json create mode 100644 bots/bots_pub/visor.json create mode 100644 bots/bots_pub/walter.json create mode 100644 bots/bots_pub/warchild.json delete mode 100644 bots/bots_pub/waxman.json delete mode 100644 bots/bots_pub/wayofthe.json create mode 100644 bots/bots_pub/wens.json delete mode 100644 bots/bots_pub/whyarewehere.json create mode 100644 bots/bots_pub/willitz.json create mode 100644 bots/bots_pub/wrack.json create mode 100644 bots/bots_pub/xaero.json delete mode 100644 bots/bots_pub/yarbro.json create mode 100644 bots/bots_pub/zarna.json diff --git a/bots/botmaker.py b/bots/botmaker.py index 006097495..91627c61d 100644 --- a/bots/botmaker.py +++ b/bots/botmaker.py @@ -31,7 +31,7 @@ def generate_random_value(): bot_profile = { "name": name, - "clan_id": random.randint(1, 11) if add_to_clan else None, + "clan_id": random.randint(1, 110) if add_to_clan else None, "skin": "male/indy", "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, @@ -41,8 +41,9 @@ def generate_random_value(): # Remove special characters from the filename filename = re.sub('[^a-zA-Z0-9_]', '', name.lower().replace(' ', '')) - # Write the bot profile to a new JSON file in the bots_clan/ directory - filename = os.path.join('bots_clan', filename + '.json') + # Write the bot profile to a new JSON file in the appropriate directory + directory = 'bots_clan' if add_to_clan else 'bots_pub' + filename = os.path.join(directory, filename + '.json') with open(filename, 'w') as outfile: json.dump(bot_profile, outfile, indent=4) diff --git a/bots/bots.bin b/bots/bots.bin index cb934c6adf4aef52694d44d6e9771a304cd879e8..a96db1047aaa356f3806ef926452ca65b3f4fbdd 100644 GIT binary patch literal 118939 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

CkaS%Gz=0Uo}GT)kfcFX060(-)HPD1 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%0A5~VV$4s z6utWcIbdU|^&Y}Rc3GdVH@#+wNYPcL=Ak2e>H$S4WDK*OU4qA94FF^qRCQ0RxTfFh z$4D$FP^mAe6fPcCHcPW)&??&FQ9lniP+x8_$P|8Zy)3Q}FFrG;fJ{8o-GTO%fhJ0; zndc?cf*3g_z-e#ok)g8r#+w^grt-QEIPzpc@41OXWql*U!4Bx|}G1w@Y20 zMDKo}IB7JoR}h^P8-P4KQ7MA7Yx1JtRtJzr*&@eev#9UOM=j)ZZF&o{=I>WnFBpIB zZ7XeVwDCh0(E>ujsc?t=clue7%3M49@80n%{TXOtmEYw_t+#qdz7?)M#xPh0D5Ldv%BiN%qT!%u|D&3Z@WW1~ zqiu8+5*lUG+=PS}djo730p}s-2$

{qbtn?Qy1>|1Da*K3gP1 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 z zA=m7B67QN@Nz{hP9j4PrAa~<<7+T|c8}y20Fc8Gm+A!{dxsX>`kRBEJJQ(V-PDDQx zuf0uw->SoIM0MEb|DfSLec5vQ@9;kLs#uY2zaZ2eCeie1q27rP@_xsQoyqt}d7&lg zeCbaJ^OMtsKeYo=hAOExLN9N%B;^cS>J{s;UTyAT%N79?1b^dmC*I+Spe{x|Gr*?p zYb|@Gl2v;)q(GQoay|AZpW!!`HuWrN$#olqr+yWhbfxx3(2OjVzl(r(gLcgygct7k zb@hz>ZYWFAFKPj#+!vz9e5P+?u*^VAIKZiE?`H`1oB{37|Ii-L4mF_D*zo+bfa)2G zwI`E~Po48AiXJ&T?L>s9#23^YbK@SN2Ty5O3+N8jB&GOm$N>8t4g~ ztG>sK5cv4uhbLwF9;t5bJJ5i`H)De-^?N7XG5WbKbjx2nL3gUGV4+%JVTqb6kmZ%+Szwm=b4WQAb%6#^M@7dU+;&qz65K620F(pt@f z;j`Jpy}z&bKxo}cqfOy8kuiVcZ(IHO${`IuF2b@pFQJ=Cl8kk3`csZ%!WuE|rCM6w z%#7@zoNWa(c}A_;oaW0*W;Kt-zji!)&L{OS5@Oj(ogTX5KGZk7XYN z-t;t9Dbl7y@-?BFH`QM#S(zHCjqixcyrO&1v+L6C7}obmQ*&PiTM6e_P7tLR1^+gl z-%ww=@NvykFh#nd%Hw*;;=6e|#|`50Hwp`24C7){jW^zsQp0pnVTs?SYy;3(x%#@N zAGh8ce~}f^X&E-!?4&9=>78u%hST%s*KfrkCWiY6-wfNUcqE>Gm?`!3h=Wp;b}tW( z{ASxuvTGhE@>cE&Gx8a{BG|YFvQO}HN>wf`W`pD|e|63u;vx`i>j_#h{y0t2c&R#0 zv_j=2X`$=^f07Q_{#c&7(sF*5{QO!FY!huU7HG>8C>nJlna_7+qCeA5BOjbzFS@d` z8huhB$ghy?Bp<6ne4$0Wz2~+y&_fo5NUmH7t%7NNVfXHs_cvum z=H!)ztFPpaUdsY{NO07car&=7*PHXlfXtJgbSqzC?aAWY?P-0HTdprQ2nd4Xb+#_^ zux($)n5)s*M&!##QXc>s*pDee{cs<)npdr?r*63CxO;qepT_K#+`Nt_g|3}dg5w5S zuvI7Hr~qDha|`VWKIVUD&m&zgpO##0hSjmykNiiahr06WSI%{H5J;GXlzlwiZt=uD zlS(-FO!+LBE#uC^3J1)VqQYT;Bji0%)c%5;-Wc+9k0w~39X;_SMj5hEOEpJ9N?vMVQ!0<68&6Aeq;R^|`loe9& z5r&rI@?QxB#Xs1-!ntkG`%iYL;>JTkn};FCc!;hn-HFz|e-2SqUhiF7R&_dlJkde8 zfBs-QMpdy1|_`1Qt}g{;5(5+L*`=G@v^_iASj$Zz$l9nooj? z%o+2&%U^E+s5{OhpAwzG+A8<>z`~3<<&@}8=Wlo1ghAfVAaTyFhz4;(8f)Qs>^}2A zs1K>{qnDbf^V0B6r>23CTLUfcni`15gLMReKV2`#J$@*E<3ggu@Ic&k&MMr5gx=i* zr){cA|KB$to3ZZB`5hyNEnXylX}8+W7x&u=ANI<(^cG*T{bG-XD{abxA-XOqXDm*l z;|DV+thd0gQq5wKnI?P!5ayyUQ!)AL3?R&JKI34}fby|b`J|}sm+>QPwK$Fvt=h}) zQXBXHsbZf_L2DjkTG9FIasEfIdsD!8@7-LzA>GU=Z6d;=6c6M1I|5tFpA|=A-5g12 z_@KL`QU+O8Zjw?K07)2Mz0m{OV8lW{3B76`)(kFA2&>TD`?mgkMn&OrHW5LC}*>*@5c21hiN zRtad`xOK^s+aGln^TUy|Q#)h0VIOdolkQ0ki#nV_)jv3p-;Y57t(O^+-*G!4^9`Pr zqh|2Z%u&U?cE3{CxxbJ>#yGQx4*F+t&_7F!GR6lK7)oL#ltND43%~aTLM3cJB@*k{ zqc7HCrGAQQRSk3EpOz36_PM@mg$Kb0bKJXa?dz`{BadL8ajz%djj?TgUjdI?)(zDB zBmQL^n>)>G@0VZ0vRWoe!FF4WG?dk9(?hc9f0}2XeJ@fy%O!v$opp&ir|F3!~w|0z7;;mX47$C!b}8??^j6V{jz1)%HF^`x=E!;$*#PE>OBlLg z7BPlV(T1?AIij&7L@9?o{b^G^KT?h2_3XdN)_%{{Fe>zg4y*vV(xi>fIGisN3OM0$ zODPo?5Gbe~Oxn|Z7X)S^wBSO`;AU?Oiee=lPv;Q*QscZSXJdgY1n z8N9^fpUfAJcgz6+BGe;MA|jZg+EX8S;JfDEmMhpeU+VXM8^zmy)v0IMaN=1)5&kQ( z{!e|2G1Vsj_xE=fn4Tk8cP)Z-p8y`%XUz*Zr=0bs;=y9K+}%et=OQeFXY%rrm;ve} zc5WXaGRQb(q6OitW0C)#5#nslu%?g*?xu1{~hS6+>wByai5s`v&{FR|iIh z4>If`)H}hfa0`Ef&*|a8X^uW zy{j6{Sk*+L*C1r#IAW&9o5&g)6jKRK@2a=eY$Lr=q0}_3?!TkI7L22dU5CWf%D2U{ z(WJU<9wO_cH9s-i(`weqMY>`r_iKDjMznC0E}-~yq*E9gu!fh9mzuAzMiS(aUL}0| z>%C7ZzNS|y7MrhF#MVF_4z_IwCnRvuK`##rwvn-@sA;BE(V22SC1Yuj?G!q`|! z+<5(>IRaVc2h|SDNhq(kBHFvbqSxjgw&S=PTb?;+umdxyui>d-y>yF1E6Y2CeyOuo zNYfRk(?NO*cj@$Se-(^#8cHN1J?nM1QCGm+!(jq84h);ds9^~Ub`*$wjTV3%nP7Wz z{5d(lAAx8BbfJU&a;;@W=M|`yFILMj?)#Zvp8AE^HTe&Js)xc=_8$of$V9K;07$S%glNU)$(Z3Q4!qUd=xc7r2T=x6jDj0+}M__$N? z#0|Mm(q}*(?D%8Gm(+F@VbeUF;DUc~>w%B}roa;fdz)LD{_PiC*aR-vxKyV;2=JaW zGz;aoGkK>r_5(j;T=(ERuJ7#e|9YMkff2hF#RktpE_#NK7f&%MyBB;3W@RoDn6c5( z(AYd?u2%M%H3>?>r!mZ@t2$>)D)n;bh2V}4S#XP`^wNSigPg9-fEg$>It=a|J5v@{ z5DGQt-HkxCQy*N6fG~VdX6m4eT5KG5TZ^n#Mwgv(bKctR^dM+QB4OD>m+t9AGt{ET z`QvpX7RGRhmENqe<%a92qusT{<~?--}#6uYz<_JyHc``qe@-rH7abJbE;%d4M|xhsKKO_@5HAk|L-h>6^&I zJBDLL-oiHvt{R;53iXNCoP4j8=Fq$EocOuJ8;yz<@b>}FaO3y|&7Rpo`C1mcLC1@}`aq?8yPm{_mfoli^mHH*eP9>RJ#ZM#FWD*pjLy6hjQ@Q$uQ@GVM zn@Z`GWc{4@`2`$dNS6rJtdSID?Av9u*M7laUrqM6m^bb0Vv)m);D#T+k_g|Am8kNX z(U!=w9(fMm;v$6^=hS)_pdWGxHaI)CESJujP1ejhXoVvn63UQXn~bX?KKv5%!iQ2A z)4V-4r~>O_d$ZqOHGM3qgYiDwR``DpL@0Y+6MH@T=usJfbnz|NxSVVN=|T&3&Qi>X zz11R?7trjeTHw6mSe^s=X|~E~EYHt`*}b8B6|7<4NM*r7>s^gb(8LkCti)!qXUO=3 zF6bBb`k8L-?~095$6^}B>;Z=QX<|vLPw`uVn%RP;T8j30F;*CK@P4pHN#`C$O49XC1tVg{<&`ZlVMCS2n#!Xo`6?**n5A!%a%MTob#DIYomrZaJh zTmFOpTwg{{UvKm?D?n!b1(;Ih@tJiGkO*Z<6KOZs){-I&wsz}}u4*Lbv(lRZJT1_& z7T;lqXsArtTr!TAp~G(5Ag0q@rZ;sR3_fYIcXQDT+9t8e>D*Tt+q2qazQVsQYFqZ~ zOZ1PdUDIenW7oZM+#v{LL%SBY$k9jNm8)++4bXT4Ct2o+$jD>4mz6MmTlXswy+lp-uY9OO~hWm{Ff~><}#eNeq4Q_kmKBO_~PPOsKvI*<~}V}e78H? zB!CFgIO7Q#EWV0kj6*DYDDQuwjA<-i@S`f8Y-ED?F{g<|jdO0>V$-0Vx~Q(}ZNJMY zErd35y#|%w0rnS0?qC&L^3?vIua!R)9P#X{w)p zc-MtGO?h&S9(V5C`D$~noo%F9PCTFfQ)-$5#ItWs&F;6}B=4G9StT!!Xn%nd*=n8m zPgbcnW5c?a;Fp_f1!R>}{$-VvKRVTZnlEh4{DGhc47ew|X&$@u!fxw~dN#|kV%n4w zGQ|6XxR^Q~sX7AyD-)02W2X1BE)$_DXmv+%{!=1Ej#lomzqlvYnaX8a0-U1+cpLN~ zfBb0z5MfH33neH~F=CD1-&B$>G#=Z|#+wTmX0S6?GiKLc`j7WOR&RfwjU3f- zxl$nHj#)*xApa*Z%`@50u`F|@rnpfl;_mVGl;ozo=)8|-GdWD5ZoZ(-Mip(gU^dlz zzz2bfNs{ZwHXap0wP>pqDrXx#)(W;coiH9`?fH4$w&Cv|OX~r`q2i=*D6?MgVR2lp z?WnF%(hKQDymRi)=F>c^6;c?r&Hql0Xl%F@iU@NqL5#-Z{eSj5ZrmXC1`1)F9ARn2nZh&8G<)Bo5q^Ej6nI+30z4yn|Be2*~vMN=gnkg$lT(E_= zCXL;~y7Sk4Z;@y0Upu!i*||@@nJVW~XzFiWVn^fVwCWoik-YQbr!VgOZhWGDZ>~_0 z{!g0t9?@V}ZCRXL3cHqb&hrENdD(hCxuPTR>3kvZW8HtIH`}5l{hN8=!M#i49_J1* z*yb&c((YS73CB*18tk_0BR|{6o>#P^y~P&=)uKSmNH{9C5ghSugsIE@48=4=-M&(~ z86@i2N-fU)_$DtBbIjj6mLwtI|08Xz622jrp#^25x}8LA3-vTB_&xe0C>ugcLNWzd zLQ>_@1O54u)9BjLWI6_T!beO%!7>kb!>5$~URzj!RMStXQ)p)l92#q9;};5xPIVF3 zwFS9#DTy)>Yq}IK2kmHf>(;4#i^r881&59>6z0n`7n!DtH(Mg1ImeZ!YBJT6b3! zdh^E?E9n@1y}<0ea=CwSWFT&K{z18+e_MRQ-`$PsE^e|lD$CYDc2YwDu1F$M%{Pg( zMT|iraZhCq2R`}j%(CUrPyU${T_eP7pk9>?)LZhNYn&ur^crb2ffE6vxh zE5FKix|#G*K%hd^E-HpV3VVe0s;B5m{ij`lD*Bp|#qPz~Gl&aOaoJw{qi4h{d$sMY z;dc&dfy>}a1&;Tluos`iGTW6wpTq6I9n$T07tGN9?X0B z#Ox+)#`xorH(pR$Q`+k7MS}T>-$xe$OH$Chdalg}(p;6EFqdnk3Zx7 zF;hFx)*lUoseRt>mPP<;n zwB4SUJS2%lv!O(4i;!=Q;pKK9yxgn0jpm)~X_+PpO;&3+2KIaP>1;gdx}MB*PLhMf z!iIJ>A1F`x%lEBHR>yLess4XkDjT!FG>KCV?^`B;<_Iigs{q!Xez(VA-H{&eMNr5V zxiV~ONjlVz97!R-j7~?-Z!O4B%2wMX!tOL&3%#XNvBj!@eYwG;W;0Ooy$2Mm!QA5@ z^D*1mIMaDvlrtX)`Eu`~-+|*PBWmHo-NFp{Mb%!!JwQumd)tT8r2TZq@6^rfa?)@` zro4-nRIkbl&>A9zvGg8REn2dS&is9cvO7W%^`frGyDil!P*CfNLA}5k4ww-f-s6B0gD_=hO6IE!S5HdB!7CA?{ zScO+Ml$_=nYg}#C_<>~s^`~z<_fCLnQSdx*FhQ+4tMX2CFI*IOo(qYkZO>dUUk^Wk z3Lz%h&NTb-cwWO7_axxV=>;YNs?d0_HfjkT4gTB3M#X1%o~Ip#I;k5Rdl2{&rI3f5 z&J?9zBBfp^2J2-)29TcYenoj8&yzv~G1f~9Gev$g-7O6sZ_6%uLNRT_5O#JMVN>Zc zOzhZHI&yl|L$USLRVnagSWnIQ2(SAnc+e$qZP(0w@w*Hn)xbZP1db!0 zXcJH3qJCq3hI~HFw=~#4Z!T*oH4zJa5M!Vi#6v_|P1f;HoTNH+{3$rNY#)8!q8ZH? zI(^1;ixhis?N0h8<`aAw)+p;{7fg_J(lS;b&=bBaNfM}~-T3r8>5^;|x)vf&MoMo| zrPdfV1dz>g;RD{?iaWox4%>!c&6hsW47%_A{iZ923si4W_Fyn?nxPpJu19=&GG5Zj zN%7Gb3En8qgOrPO*X3lC<$6SryWFwjoA|U+tBwPy%k%qYX|XI18rHspjlVX2-kl71`?|PV*L~UZ zHxaKo?(+7D{^y%o3v{5ByD_Hx6999>TYetUEDHGl1rV0(nnrUe99lUH=gu9?Eir;o z%UzcbMlJM7k&%alvoAh`GO53PZOwPDnZ}SMGn?w6O)9j3H13Vvo5#5lGmvAzE1f`r zXv4fIh4y!T)mtS?JmYI;cya_20#Om|NRYkjTd3L6iGzTo11XfKD{x{lz-V@eaOpF1 zH>1u>)+;J4HW4p$A;ML^%1KMO45lFBK@sH%n`ks@>5BT{efNWv`3_i_-w!XlOvW<|bB*u}V9otY}q2SP7)kcobH;kJ>pr zF{yM||BJOrmVp2klRSaA84gP85kIY?rr5_4ghU!Js)$VhTYESUbY}(#C6F!da`)Iu zD-HF+#09uvJ6Y>}nNLVO)o;MA`Js{=Xce}vqIh7}6HRhIoWEq8V706;MjZ=@IC})E zJ|c_;3I&Cxc!HB~m{#UJ;eRX%ey94*xv{j?G2BcShGM1)@nV7-^EA3K3#v)=1?(s= z#c^-{kU?VK+NB0TR%yp;r!#gxqRUS_5ALKYPt@0n4yaSUxe8v> zC`OZ3&E6ccIxt&?#wK?KeTh)xr4I36hwTQmsW)6jZR%7{d2;VMvV7k2!%;O7Mp9N0 zBMaXP8v`FHI%$pR4&UaAoz{9*{gCU$Coc^{>Cklfb@dR~qi$A(OK6*V$}cU9)yq=M zb|5HG13-sQXNdGv&;w#H2jV*MT50*0RqP($Pwkw#|qXqw;W6cY1|S^i;5 zXB9b}oYqxbgIn9ZEB2rCC#z>01vC4X<*7321qYOVtl4NkzhwGjF@%|8y6sKeqPK7H zZKKYlGJl?Orn>WiT)3#MX9GS|m`1^e>YUrn%Zj4wteGl<%n}b&O)5;w_+bnGb?WYQ zE$_?H$%}`BuL?pNCVqZ@T0NC)zy}wEg=mikBWf;dFe7=$dG?NjP<*rfbxElkyY|=@ z@g}pFiE1yt$&{)qD%WGk?>$?}S=q$@AP80%nd)q!O;J`k(PSMTzo<`Yo`z6=C8_GM ze#N4Ne>-lbmNNxB;2&D~kOyUm6OF4EPBz13@E0E)s9gLqng#}%S2J_lpy%%_CDvS$ z0HgX8cL)z0jiRSTBS!~>)wn|^hb80V6)&I1RBjU za>)rs$Zh^md*P=`{gOJsC5~l*f#bOl&(eLV=~mE-sT=(l+6s2sBhnbkSLE2|INv##Rj2pQTXlawu7h6m{RY z?L;}ZBUPQt&T4Wp=+iR7x^Kn_-8VRpDY{n0xUgl0kjjs2msC=M-E!F+1>z?g{tD)46@93wc z9mXZP2nBES#rn0zH7;)XRiDniE_@n{_~z!2Bn*yfK<5+XlnQNjC<*uXPoGT4`qmU3A6$5TDLzZlwqAhq z+>fgPrMOtu#ub~W7%ZijV!SVrZZ;7^2SusUCA*U{mg{@d+|@klF(wTxdzx?MW7lQ> zS320AM=cH`Tlxk-&_q_W1~)?p0;+G?2%2NFI>J50bIydruBRej9fMoW(U83VT>=sI z$*;rB@eYFUfthvfNRU1uq$h_hhR@w+;m_zr(5>VC$m6Y>&9;l?_6&0xJ@A8SPA#3CEi+qsb zLY-R{=K2jK4pIYd?-NOk8CI-FN}5n-@6cypA3%gF(*jmu=W7~G7*)nMmRmx9&MLZB z|J!ntRbw;{^Y=Ung}3MYk_zs$9bQz~UE? zOk6A@l86}nwBL1*VDK4S4l&&#g{|F?`zEo0(=}z5!X#1!kVw~7+aP4ZxZJ;F(e-V8 zgIqu&VQOpDqxFZ%{AiC?;rM3`*Bh492jkrgbfWm`sY2dtnl`xW6-0(-4)-bh(;-OV zxjAMBW(UG3nyRf#7)23vg_CD3ZEF<8-W?c4O&#fSVh%?G?}kR7Js&lDG#(QLeS5 z{TBfGv`(i2{~u{^>RO#iC&Wk*-m-61k}L5utP0nRVUMc&XB4$Lw2A+zNti`})n#V5 zF*fpjmn#k-S3K{_SbQ#@;>rrRM`F{x8dE(>S=N%szkTWX^K@F2E*GCG=8q?@!6vN) zn6%t4?Se_m{itZtb!lcVt_d`Z%IepCUaGCmf-(KEA|t#y;PRd{!d%{IWk-Jas(97@lN`%r+(e{XqwhMC#x zguUA@&Y$~Wn=H3I9P6hQ;|s6yT5zed zMdFqNN{%{T2FP3yiSFjU3(XY8;hr1BL-!JD2_yP{fx4&VjTS$XkNu~{cNZ%Z1Gzuc z%1)M+V#bF|t0GiSlAdFw1s9<^DN|pWz&=2q6}v4SU`FFf05dvGfEjf;=iuk2Tob|dtBpq-ZDmmh=mEf$2Rm%?UjC#$B0-LMZ(AX>880P9Mb4 zz?UuHe!uK<0sHU#JZ4?XOGy7IE_A}29=7LlA}KVgD@e$qD|1u@+Mxn?<;bhYEtvfP ze-tsF7{+=c>`{iIA_l0{t>29<3(jCm4B~HQqFZ;LZqP^7G6KX&xD3RwHIHA#d`%hl zX9gO)Mc3LogT52aXM0Wf_xlP|G-r{!QLG#}4jyk3Qwu7jh_;8$(j|=ZY9}cr^(H}`(hC=^1O&sW{vlvt{4Bp0)fEW_ zp^FBjo!=Hl&WTa3?&mR>>{uTyCbmsV&rZ__#ME_M#icw9opHWY zuI*#O|4B{U_S?$Gf7{&A#q)1INd)e8r;ZdZV}cwMf!)6+WG(nF_f+r`Xsi5=?aZ zwaNd!-GU#yP$OInVIW;b11WSx)|YQZ1Hc=`(kZF??IPmAH(5TdaqsD=sF(NYSru(t zi9+au)h}d8nNh1>07*msB?RRHzAA|irFp9Ne-I-%ai>-~dT))b?H)V=>&o^x9)Kgg z?xztq$Xh6vjRW{6*%`pcqS`IM!FORbqo+oW6W$v_*`LIxpFUahRw@M;TNiR$`CvP^ zOoQ)PB}fHwn}e@PCIb+xMr9&07`8tJmJ&!v9;xBM%EQ-8sF2@xQCdf`QPo_ zA6S|PNHv(lZEW5zA?I#|!+r64iTl~hh8%R@N~n`wL4aI7Md=@>`cg*(p+oUEz`Hh@9l;507u>lE3CB7I|sD(852)X{2;1YDCo0C3R zh3_iR`f>`)>uok`FZMG)PschvI?`dJz~yiGdN_0G`XpzD5OdebZ=IVt>V7|;xiN>Hb(7rVrnw@NoiV!>O!0oMgMR`9MR$xru z?{;n+_nlCL7=;>Gb9MPg^N=$7@)S}=BNusrjGD;KHDrGuWwLh`%E#j7uQ)=9KubwB z))HC37US3aLCmcG=Q|qzdzsotHPm%3B2|=bco%AvLUuxEL0tK;C5p-j7wLnYFni%z zJt0wRB?f`xC4m#MSnoVq4AL)HX<+XSuTl8d-J3cRr`}E^_3cKR!JAy2@Xm~xLBWfh z?9e(1FMWao>sD3UA5UuV+Z%o+TrZj2bP48zbn?tBJIs!$K4n?rPeB-IEy{PPV8Y;KEe3h^>%iznXb?PNCljmuZ zH;Mxe_T=vIjh)pV^p#X~(rr56AGbqLCr9&v?9}1Z^@qZV{Z|dI^6J_QEn&OCv6jQm zy6ldYQgq}32j6x5W(C^0s_gkgaTme!{gU|kk+yRYD7Nm6Dr#R4V(VU^K~Ui+7!*U3rua*nrnS;OuB=UFD71@J zlJ+AW-B^qY36|wHvo4W9ee#;H!$}v6M`euE)8`&|0nj%lMzTo=( za4w6~NqXiGB=<$Zf}iu!JZ zsIc28vuXnImEri@h%tNUr-k)k7kL;aY%N7~>^-9)Td-ivJcW`J{BLjE`B?&P%N1B- ze7LIKw%kMjij?M4uIM$dwmpA+d5!_Ar#-~j=(jN&Tomx;g~$34cUNdboH>=yx;=$% z@gPQAJ4kk1TqGX27>v^D6IiX?W72QXh4@g*XWr9YTBIlsjag=KOiuVwb8~*wU7`U& z^~1jfROiBr9aSXj?(ei9fb?eOOASB64+%da%96U zBNl&ygo2y?DR1ukLcA*Q38Pgep&uwNT{zb$bmio57HMCar2RW;Npi5AX5jv^O~0Rtyj0M#APmdpc6+jIjP$qDfIVO6 z&fvd7=vmOg?Y||p_9=#+z^s-&^x=^xFloRPZx01Qhc;VAtX{jmes~uZE$}REs1=#c z7TE;I--{5NcxUhvkmr*D?+T08D>}?qcKba@qWGl>r*1Gf zWI-A-4$I*jD|ZQF?GGP&_6Tw~dz;&N{DzodO21y5BLXi^IZ>1%z35g*38r)iDUo+A zAtN)-4E4xA2|zor5PRb;Lo)ywtwydq-^#1+3-_`p$-7ZAc z-5vk>>D3duFm9a~Pd;?`3f}&-y|ReEkfXAN03X@fpZ+2M@d;LfghXRqv7oD<ℜ5J0v4 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 literal 123146 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 diff --git a/bots/bots_clan/2dcitizen.json b/bots/bots_clan/2dcitizen.json deleted file mode 100644 index 59a2d51a0..000000000 --- a/bots/bots_clan/2dcitizen.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "2DCitizen", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/_gholem_.json b/bots/bots_clan/_gholem_.json deleted file mode 100644 index 5e2aa8603..000000000 --- a/bots/bots_clan/_gholem_.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "_Gholem_", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.0, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/_glich.json b/bots/bots_clan/_glich.json deleted file mode 100644 index dd0ff32a4..000000000 --- a/bots/bots_clan/_glich.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "_GLiCH", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.2, - "knives": -0.0, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/_pixxy_.json b/bots/bots_clan/_pixxy_.json index 02a918334..789ff3534 100644 --- a/bots/bots_clan/_pixxy_.json +++ b/bots/bots_clan/_pixxy_.json @@ -1,35 +1,35 @@ { "name": "_*PixxY*_", - "clan_id": 7, + "clan_id": 73, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, + "mk23": 0.6, + "dual_mk23": 0.6, "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.4, - "grenades": 1.0 + "m4": 0.5, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.0 + "vest": 0.6, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.1, + "reactionTime": 0.1, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/aegarond.json b/bots/bots_clan/aegarond.json index b8bdf54ac..0ef3e5fb6 100644 --- a/bots/bots_clan/aegarond.json +++ b/bots/bots_clan/aegarond.json @@ -1,35 +1,35 @@ { "name": "Aegarond", - "clan_id": 4, + "clan_id": 38, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.5, + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.1, "m3": 0.6, - "hc": 0.6, + "hc": 0.4, "sniper": 0.3, - "knives": 0.3, + "knives": 0.5, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.1, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.7 + "vest": 0.6, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 1.0 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.8, + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.5, "communicationFreq": 0.6, - "communicationTone": 0.9, - "movementStyle": 0.3, - "objectiveFocus": 0.3 + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/aimless.json b/bots/bots_clan/aimless.json new file mode 100644 index 000000000..43ceb5013 --- /dev/null +++ b/bots/bots_clan/aimless.json @@ -0,0 +1,35 @@ +{ + "name": "Aimless", + "clan_id": 62, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/amadi.json b/bots/bots_clan/amadi.json deleted file mode 100644 index 193e5bfaf..000000000 --- a/bots/bots_clan/amadi.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Amadi", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.1, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.9, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.1, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/amu.json b/bots/bots_clan/amu.json deleted file mode 100644 index a9aec1da8..000000000 --- a/bots/bots_clan/amu.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Amu", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/angel.json b/bots/bots_clan/angel.json new file mode 100644 index 000000000..f97c51695 --- /dev/null +++ b/bots/bots_clan/angel.json @@ -0,0 +1,35 @@ +{ + "name": "Angel", + "clan_id": 62, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/anodized.json b/bots/bots_clan/anodized.json index 99dd4179d..8dc0d859f 100644 --- a/bots/bots_clan/anodized.json +++ b/bots/bots_clan/anodized.json @@ -1,35 +1,35 @@ { "name": "Anodized", - "clan_id": 9, + "clan_id": 58, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, + "mk23": 0.8, + "dual_mk23": 0.6, "mp5": 0.3, - "m4": 0.4, - "m3": 0.1, - "hc": 0.4, - "sniper": 0.6, + "m4": 0.7, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.5, "knives": 0.6, - "grenades": 0.6 + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.5, + "vest": 0.4, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.7, "slippers": 0.5, - "bandolier": 0.3 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.5, "curiosity": 0.5, "aimingSkill": 0.6, "reactionTime": 0.5, - "communicationFreq": 0.5, + "communicationFreq": 0.4, "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/ant__dog.json b/bots/bots_clan/ant__dog.json deleted file mode 100644 index df23d51e9..000000000 --- a/bots/bots_clan/ant__dog.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ant__Dog", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.5, - "hc": 0.9, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.9, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/anton.json b/bots/bots_clan/anton.json new file mode 100644 index 000000000..ffdefb5e3 --- /dev/null +++ b/bots/bots_clan/anton.json @@ -0,0 +1,35 @@ +{ + "name": "Anton", + "clan_id": 108, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.1, + "aimingSkill": 1.0, + "reactionTime": 0.5, + "communicationFreq": 1.0, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/apem.json b/bots/bots_clan/apem.json index 4c8bdaad7..448586268 100644 --- a/bots/bots_clan/apem.json +++ b/bots/bots_clan/apem.json @@ -1,35 +1,35 @@ { "name": "ApeM", - "clan_id": 7, + "clan_id": 85, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.2, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.3, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.3, + "hc": 0.9, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.8, + "vest": 0.8, + "helm": 0.2, "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.1 + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/applee.json b/bots/bots_clan/applee.json index 079636c00..2f4156805 100644 --- a/bots/bots_clan/applee.json +++ b/bots/bots_clan/applee.json @@ -1,35 +1,35 @@ { "name": "Applee", - "clan_id": 1, + "clan_id": 2, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.6, + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.4, "m3": 0.5, - "hc": 0.6, - "sniper": 0.0, - "knives": 0.8, - "grenades": 0.6 + "hc": 0.4, + "sniper": 0.6, + "knives": 0.5, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.8, + "vest": 0.4, + "helm": 0.9, + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.4, "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.0, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/ashrah.json b/bots/bots_clan/ashrah.json deleted file mode 100644 index f2638b76f..000000000 --- a/bots/bots_clan/ashrah.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ashrah", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.0 - } -} \ No newline at end of file diff --git a/bots/bots_clan/badjuju.json b/bots/bots_clan/badjuju.json new file mode 100644 index 000000000..73af61e26 --- /dev/null +++ b/bots/bots_clan/badjuju.json @@ -0,0 +1,35 @@ +{ + "name": "Bad JuJu", + "clan_id": 8, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.9, + "laser": 0.0, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.9, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.9, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/baraka.json b/bots/bots_clan/baraka.json deleted file mode 100644 index 313975ca8..000000000 --- a/bots/bots_clan/baraka.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Baraka", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/bean.json b/bots/bots_clan/bean.json deleted file mode 100644 index 4eaba2ace..000000000 --- a/bots/bots_clan/bean.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bean", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/belokk.json b/bots/bots_clan/belokk.json deleted file mode 100644 index 20cddfde3..000000000 --- a/bots/bots_clan/belokk.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Belokk", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/benarcher.json b/bots/bots_clan/benarcher.json deleted file mode 100644 index c15de74ce..000000000 --- a/bots/bots_clan/benarcher.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ben Archer", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.9, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/bitterman.json b/bots/bots_clan/bitterman.json index ee35e2c81..c291c190f 100644 --- a/bots/bots_clan/bitterman.json +++ b/bots/bots_clan/bitterman.json @@ -1,35 +1,35 @@ { "name": "Bitterman", - "clan_id": 11, + "clan_id": 54, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.3, + "mk23": 0.6, + "dual_mk23": 1.0, + "mp5": -0.0, + "m4": 0.5, "m3": 0.5, "hc": 0.6, - "sniper": 0.2, - "knives": 0.7, + "sniper": 0.8, + "knives": 0.6, "grenades": 0.6 }, "itemPreferences": { - "vest": 1.0, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.8, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.6, + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.1, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.4, "movementStyle": 0.7, - "objectiveFocus": 0.5 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/blackadder.json b/bots/bots_clan/blackadder.json deleted file mode 100644 index f05b37783..000000000 --- a/bots/bots_clan/blackadder.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Blackadder", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.6, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.9, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/bloodwitch.json b/bots/bots_clan/bloodwitch.json new file mode 100644 index 000000000..84708f20b --- /dev/null +++ b/bots/bots_clan/bloodwitch.json @@ -0,0 +1,35 @@ +{ + "name": "Bloodwitch", + "clan_id": 32, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/blythe.json b/bots/bots_clan/blythe.json index 55dda37fb..8ff16ded8 100644 --- a/bots/bots_clan/blythe.json +++ b/bots/bots_clan/blythe.json @@ -1,35 +1,35 @@ { "name": "Blythe", - "clan_id": 11, + "clan_id": 56, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.1, - "m4": 0.2, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.4 + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.4, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.9, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.8, + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.3, "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.8 + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/bobross.json b/bots/bots_clan/bobross.json index ff8d09576..82e30cd61 100644 --- a/bots/bots_clan/bobross.json +++ b/bots/bots_clan/bobross.json @@ -1,35 +1,35 @@ { "name": "Bob Ross", - "clan_id": 5, + "clan_id": 27, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.8, - "hc": -0.1, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.5 + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.9, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.7 }, "itemPreferences": { "vest": 0.7, - "helm": 0.5, - "laser": 0.1, - "silencer": 0.1, - "slippers": 0.6, - "bandolier": 0.9 + "helm": 0.7, + "laser": 0.5, + "silencer": 1.0, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, - "curiosity": 1.1, - "aimingSkill": 0.6, - "reactionTime": 0.7, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.3, + "communicationTone": 0.6, + "movementStyle": -0.0, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/bones.json b/bots/bots_clan/bones.json index 4f892ddcc..05a6380b4 100644 --- a/bots/bots_clan/bones.json +++ b/bots/bots_clan/bones.json @@ -1,35 +1,35 @@ { "name": "Bones", - "clan_id": 5, + "clan_id": 91, "skin": "male/indy", "weaponPreferences": { - "mk23": -0.0, - "dual_mk23": 0.6, + "mk23": 0.7, + "dual_mk23": 0.5, "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.5 + "m4": 0.7, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.4, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.0, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/booker.json b/bots/bots_clan/booker.json deleted file mode 100644 index 5b0c95c59..000000000 --- a/bots/bots_clan/booker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Booker", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.2, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/boy.json b/bots/bots_clan/boy.json index dc604cd3d..d6194a882 100644 --- a/bots/bots_clan/boy.json +++ b/bots/bots_clan/boy.json @@ -1,35 +1,35 @@ { "name": "Boy", - "clan_id": 10, + "clan_id": 87, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, + "mk23": 1.0, + "dual_mk23": 0.8, "mp5": 0.5, - "m4": 0.8, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.6, - "slippers": 1.0, - "bandolier": 0.6 + "vest": 0.8, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/brad.json b/bots/bots_clan/brad.json index d2feb2669..ebc2da503 100644 --- a/bots/bots_clan/brad.json +++ b/bots/bots_clan/brad.json @@ -1,35 +1,35 @@ { "name": "Brad", - "clan_id": 3, + "clan_id": 12, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.8, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.4, "m3": 0.3, - "hc": 0.2, + "hc": 0.8, "sniper": 0.7, - "knives": 0.7, + "knives": 0.9, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.0, + "vest": 0.4, + "helm": 0.5, "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.2 + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.4, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.1, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.3, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/broz.json b/bots/bots_clan/broz.json new file mode 100644 index 000000000..5cb5a1f07 --- /dev/null +++ b/bots/bots_clan/broz.json @@ -0,0 +1,35 @@ +{ + "name": "Broz", + "clan_id": 87, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 1.0, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 1.0, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/bullseye.json b/bots/bots_clan/bullseye.json index a7b4c6843..ff6d9353a 100644 --- a/bots/bots_clan/bullseye.json +++ b/bots/bots_clan/bullseye.json @@ -1,35 +1,35 @@ { "name": "Bullseye", - "clan_id": 1, + "clan_id": 102, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, + "mk23": 0.2, + "dual_mk23": 0.5, "mp5": 0.4, "m4": 0.5, - "m3": 0.2, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.6 + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.7, + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, "communicationTone": 0.3, - "movementStyle": 0.3, + "movementStyle": 0.4, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/bve.json b/bots/bots_clan/bve.json deleted file mode 100644 index 8768f696e..000000000 --- a/bots/bots_clan/bve.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "BVe", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/cerberon.json b/bots/bots_clan/cerberon.json index 1e1c715e7..339f90bad 100644 --- a/bots/bots_clan/cerberon.json +++ b/bots/bots_clan/cerberon.json @@ -1,35 +1,35 @@ { "name": "Cerberon", - "clan_id": 10, + "clan_id": 92, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.3, + "vest": 0.3, + "helm": 0.3, + "laser": 0.7, "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.6 + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.2, + "aggressiveness": 0.1, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/cetrion.json b/bots/bots_clan/cetrion.json new file mode 100644 index 000000000..51f72daf4 --- /dev/null +++ b/bots/bots_clan/cetrion.json @@ -0,0 +1,35 @@ +{ + "name": "Cetrion", + "clan_id": 25, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.8, + "knives": 0.0, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.1, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 1.0, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/chameleon.json b/bots/bots_clan/chameleon.json index 4184aef7d..dbd976f9a 100644 --- a/bots/bots_clan/chameleon.json +++ b/bots/bots_clan/chameleon.json @@ -1,35 +1,35 @@ { "name": "Chameleon", - "clan_id": 9, + "clan_id": 90, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": -0.1, - "mp5": 0.8, - "m4": 0.3, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.1, - "knives": 0.3, - "grenades": 0.8 + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.1, + "vest": 0.6, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.4, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.6, + "aimingSkill": 0.8, "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.2, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/chan.json b/bots/bots_clan/chan.json deleted file mode 100644 index e3d7805d0..000000000 --- a/bots/bots_clan/chan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Chan", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.0, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.1, - "laser": 0.9, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/chess.json b/bots/bots_clan/chess.json new file mode 100644 index 000000000..3c6633e23 --- /dev/null +++ b/bots/bots_clan/chess.json @@ -0,0 +1,35 @@ +{ + "name": "Chess", + "clan_id": 85, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.6, + "m3": 1.1, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/chipp_.json b/bots/bots_clan/chipp_.json new file mode 100644 index 000000000..0ab6b187d --- /dev/null +++ b/bots/bots_clan/chipp_.json @@ -0,0 +1,35 @@ +{ + "name": "chipp_", + "clan_id": 88, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.1, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/choppy.json b/bots/bots_clan/choppy.json new file mode 100644 index 000000000..2f669b92c --- /dev/null +++ b/bots/bots_clan/choppy.json @@ -0,0 +1,35 @@ +{ + "name": "^^ChoppY^^", + "clan_id": 44, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 1.0, + "m4": 0.3, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/clion.json b/bots/bots_clan/clion.json deleted file mode 100644 index c44a4b774..000000000 --- a/bots/bots_clan/clion.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "C-Lion", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.9, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.1, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.9, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/coldcrow.json b/bots/bots_clan/coldcrow.json index 07d444dd7..797f01fb9 100644 --- a/bots/bots_clan/coldcrow.json +++ b/bots/bots_clan/coldcrow.json @@ -1,35 +1,35 @@ { "name": "Coldcrow", - "clan_id": 9, + "clan_id": 46, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.6, + "mk23": 0.7, + "dual_mk23": 0.9, "mp5": 0.6, - "m4": 0.5, + "m4": 0.8, "m3": 0.5, - "hc": -0.0, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 + "hc": 0.4, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.1, - "bandolier": 0.8 + "vest": 0.4, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.6, "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.5, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.2 + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/reiko.json b/bots/bots_clan/coldworm.json similarity index 52% rename from bots/bots_clan/reiko.json rename to bots/bots_clan/coldworm.json index b3dd55422..8069225b4 100644 --- a/bots/bots_clan/reiko.json +++ b/bots/bots_clan/coldworm.json @@ -1,35 +1,35 @@ { - "name": "Reiko", + "name": "Coldworm", "clan_id": 4, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.1, + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.4, "m4": 0.4, - "m3": 0.5, + "m3": 0.4, "hc": 0.6, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.8 + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.3, + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, "silencer": 0.5, "slippers": 0.4, "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.6, + "teamwork": 0.4, "curiosity": 0.5, "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.6, "communicationTone": 0.6, - "movementStyle": 0.3, + "movementStyle": 0.6, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/colt.json b/bots/bots_clan/colt.json new file mode 100644 index 000000000..e53f03616 --- /dev/null +++ b/bots/bots_clan/colt.json @@ -0,0 +1,35 @@ +{ + "name": "Colt", + "clan_id": 9, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.1, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/conan.json b/bots/bots_clan/conan.json index 00acc70f8..3fce206d7 100644 --- a/bots/bots_clan/conan.json +++ b/bots/bots_clan/conan.json @@ -1,35 +1,35 @@ { "name": "Conan", - "clan_id": 10, + "clan_id": 48, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.8, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.2, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.9, + "hc": 0.4, "sniper": 0.4, - "knives": 0.4, - "grenades": 0.5 + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.7, + "vest": 0.7, + "helm": 0.1, + "laser": 0.4, "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.9 + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/corpsefire.json b/bots/bots_clan/corpsefire.json new file mode 100644 index 000000000..5cf4add90 --- /dev/null +++ b/bots/bots_clan/corpsefire.json @@ -0,0 +1,35 @@ +{ + "name": "Corpsefire", + "clan_id": 30, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": -0.1, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.1, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/cowchop.json b/bots/bots_clan/cowchop.json new file mode 100644 index 000000000..dc0eaacad --- /dev/null +++ b/bots/bots_clan/cowchop.json @@ -0,0 +1,35 @@ +{ + "name": "CowChop", + "clan_id": 20, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.4, + "slippers": 1.0, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.7, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/crash.json b/bots/bots_clan/crash.json deleted file mode 100644 index f6dbf3462..000000000 --- a/bots/bots_clan/crash.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Crash", - "clan_id": 3, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.1, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.9, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/cruton.json b/bots/bots_clan/cruton.json new file mode 100644 index 000000000..4aa497ec8 --- /dev/null +++ b/bots/bots_clan/cruton.json @@ -0,0 +1,35 @@ +{ + "name": "Cruton", + "clan_id": 28, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.2, + "grenades": -0.0 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/cybr.json b/bots/bots_clan/cybr.json deleted file mode 100644 index d6bd50097..000000000 --- a/bots/bots_clan/cybr.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cybr", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.1, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 1.0, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/dacfarren.json b/bots/bots_clan/dacfarren.json index 32bc8a9fa..ebb842b80 100644 --- a/bots/bots_clan/dacfarren.json +++ b/bots/bots_clan/dacfarren.json @@ -1,35 +1,35 @@ { "name": "Dac Farren", - "clan_id": 9, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, + "mk23": 0.6, "dual_mk23": 0.5, "mp5": 0.3, - "m4": 0.9, - "m3": 0.7, - "hc": 0.0, - "sniper": 0.5, + "m4": 0.3, + "m3": 0.3, + "hc": 0.8, + "sniper": 0.6, "knives": 0.5, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, + "vest": 0.6, + "helm": 0.8, "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.4 + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.6 + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/daemia.json b/bots/bots_clan/daemia.json new file mode 100644 index 000000000..1241e72cf --- /dev/null +++ b/bots/bots_clan/daemia.json @@ -0,0 +1,35 @@ +{ + "name": "Daemia", + "clan_id": 108, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.1, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.9, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/darkkahn.json b/bots/bots_clan/darkkahn.json new file mode 100644 index 000000000..9b98cc776 --- /dev/null +++ b/bots/bots_clan/darkkahn.json @@ -0,0 +1,35 @@ +{ + "name": "Dark Kahn", + "clan_id": 77, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.1, + "communicationTone": 1.0, + "movementStyle": 0.3, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/deusvault.json b/bots/bots_clan/deusvault.json deleted file mode 100644 index 29836464a..000000000 --- a/bots/bots_clan/deusvault.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "deUSvauLT", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.4, - "slippers": 1.0, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 1.0, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/dharmesh.json b/bots/bots_clan/dharmesh.json new file mode 100644 index 000000000..1b2e30e8a --- /dev/null +++ b/bots/bots_clan/dharmesh.json @@ -0,0 +1,35 @@ +{ + "name": "Dharmesh", + "clan_id": 87, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/digz.json b/bots/bots_clan/digz.json deleted file mode 100644 index 21de26a20..000000000 --- a/bots/bots_clan/digz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Digz", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.1, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/dinardo.json b/bots/bots_clan/dinardo.json new file mode 100644 index 000000000..7d42730ac --- /dev/null +++ b/bots/bots_clan/dinardo.json @@ -0,0 +1,35 @@ +{ + "name": "Di Nardo", + "clan_id": 104, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.1, + "aimingSkill": 0.3, + "reactionTime": 0.8, + "communicationFreq": 0.8, + "communicationTone": 0.9, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/doc.json b/bots/bots_clan/doc.json index 38e44f4fe..78868955c 100644 --- a/bots/bots_clan/doc.json +++ b/bots/bots_clan/doc.json @@ -1,33 +1,33 @@ { "name": "doc", - "clan_id": 11, + "clan_id": 47, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.8, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.9, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.2, + "vest": 0.6, + "helm": 0.8, "laser": 0.8, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.3 + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 1.0, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.4, + "aggressiveness": 0.6, + "teamwork": 1.0, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.1, "communicationTone": 0.7, "movementStyle": 0.4, "objectiveFocus": 0.5 diff --git a/bots/bots_clan/dominator.json b/bots/bots_clan/dominator.json deleted file mode 100644 index da16641b9..000000000 --- a/bots/bots_clan/dominator.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dominator", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.3, - "knives": -0.1, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.9, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 1.2, - "movementStyle": 0.5, - "objectiveFocus": -0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/doom.json b/bots/bots_clan/doom.json deleted file mode 100644 index 37e847b22..000000000 --- a/bots/bots_clan/doom.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Doom", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.7, - "hc": 0.0, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.0 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 1.0, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/drahmin.json b/bots/bots_clan/drahmin.json index e4869c409..7e2c08c58 100644 --- a/bots/bots_clan/drahmin.json +++ b/bots/bots_clan/drahmin.json @@ -1,35 +1,35 @@ { "name": "Drahmin", - "clan_id": 5, + "clan_id": 57, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.3, "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.4, - "m3": 1.0, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.7 + "mp5": 0.6, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.7 + "vest": 0.3, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 1.0, - "curiosity": 0.5, - "aimingSkill": 0.1, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.7, "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/drillbit.json b/bots/bots_clan/drillbit.json deleted file mode 100644 index e80710606..000000000 --- a/bots/bots_clan/drillbit.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Drillbit", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.1, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/drjay.json b/bots/bots_clan/drjay.json deleted file mode 100644 index 49040bccb..000000000 --- a/bots/bots_clan/drjay.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dr.Jay", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/dvorah.json b/bots/bots_clan/dvorah.json deleted file mode 100644 index c6c1242ed..000000000 --- a/bots/bots_clan/dvorah.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "D'Vorah", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.8, - "mp5": 0.9, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/edoli.json b/bots/bots_clan/edoli.json new file mode 100644 index 000000000..d1f2ff20b --- /dev/null +++ b/bots/bots_clan/edoli.json @@ -0,0 +1,35 @@ +{ + "name": "Edoli", + "clan_id": 19, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.1, + "m4": 1.0, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.1, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/edwig.json b/bots/bots_clan/edwig.json index 27da67351..57623a55b 100644 --- a/bots/bots_clan/edwig.json +++ b/bots/bots_clan/edwig.json @@ -1,35 +1,35 @@ { "name": "Edwig", - "clan_id": 7, + "clan_id": 40, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.8 + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { "vest": 0.4, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.3, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.5, "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.5, "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "aimingSkill": 0.8, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/endugu.json b/bots/bots_clan/endugu.json index 786d31e6c..8e22d1807 100644 --- a/bots/bots_clan/endugu.json +++ b/bots/bots_clan/endugu.json @@ -1,35 +1,35 @@ { "name": "Endugu", - "clan_id": 3, + "clan_id": 108, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.4, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.5, "m3": 0.2, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.7 + "hc": 0.3, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.6 + "vest": 0.7, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.9, + "slippers": 0.8, + "bandolier": 0.8 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.7, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.3, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/exbee.json b/bots/bots_clan/exbee.json index 776022fd7..9da3ed30b 100644 --- a/bots/bots_clan/exbee.json +++ b/bots/bots_clan/exbee.json @@ -1,35 +1,35 @@ { "name": "exBEE", - "clan_id": 7, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.4, - "m3": 0.3, - "hc": 0.2, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.6 + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.5, + "vest": 0.3, + "helm": 0.7, + "laser": 0.2, "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.6 + "slippers": 0.4, + "bandolier": 0.2 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "teamwork": 0.7, + "curiosity": 0.1, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/eyeback.json b/bots/bots_clan/eyeback.json index f04c28236..bc337af65 100644 --- a/bots/bots_clan/eyeback.json +++ b/bots/bots_clan/eyeback.json @@ -1,35 +1,35 @@ { "name": "Eyeback", - "clan_id": 3, + "clan_id": 23, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.3, - "m3": 0.9, - "hc": 0.7, + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.8, + "hc": 0.6, "sniper": 0.6, - "knives": 0.7, - "grenades": 0.1 + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, + "vest": 0.8, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.5, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.2, + "aimingSkill": 0.3, + "reactionTime": 0.8, + "communicationFreq": 0.7, "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/faden.json b/bots/bots_clan/faden.json index d9761c71d..e84ab4447 100644 --- a/bots/bots_clan/faden.json +++ b/bots/bots_clan/faden.json @@ -1,35 +1,35 @@ { "name": "Faden", - "clan_id": 7, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.5, + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.8, "hc": 0.3, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.9 + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.6, - "helm": 0.6, - "laser": 0.9, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.5 + "helm": 0.2, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, - "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/fangskin.json b/bots/bots_clan/fangskin.json index 632a82cf9..f971801c0 100644 --- a/bots/bots_clan/fangskin.json +++ b/bots/bots_clan/fangskin.json @@ -1,35 +1,35 @@ { "name": "Fangskin", - "clan_id": 3, + "clan_id": 9, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.5, "m3": 0.6, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.5 + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.7, + "vest": 0.6, + "helm": 0.2, + "laser": 0.9, + "silencer": 0.8, + "slippers": 0.6, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.2, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.1, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/ferratorr.json b/bots/bots_clan/ferratorr.json new file mode 100644 index 000000000..7a0f628b2 --- /dev/null +++ b/bots/bots_clan/ferratorr.json @@ -0,0 +1,35 @@ +{ + "name": "Ferra/Torr", + "clan_id": 46, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fexel.json b/bots/bots_clan/fexel.json index 03e136ac0..84ae8a5bc 100644 --- a/bots/bots_clan/fexel.json +++ b/bots/bots_clan/fexel.json @@ -1,35 +1,35 @@ { "name": "Fexel", - "clan_id": 7, + "clan_id": 110, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.6 + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, + "vest": 0.9, + "helm": 0.7, "laser": 0.6, "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.1, - "objectiveFocus": 0.4 + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/figgz.json b/bots/bots_clan/figgz.json index ede6fc54b..8a7ff8045 100644 --- a/bots/bots_clan/figgz.json +++ b/bots/bots_clan/figgz.json @@ -1,33 +1,33 @@ { "name": "Figgz", - "clan_id": 1, + "clan_id": 24, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.7, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.8 + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.9, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.4, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.6 + "helm": 0.5, + "laser": 0.4, + "silencer": 0.9, + "slippers": 0.3, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.5, + "aggressiveness": 0.8, + "teamwork": 0.9, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.7, "communicationTone": 0.4, "movementStyle": 0.3, "objectiveFocus": 0.4 diff --git a/bots/bots_clan/flamespike.json b/bots/bots_clan/flamespike.json deleted file mode 100644 index f8b5e3cba..000000000 --- a/bots/bots_clan/flamespike.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Flamespike", - "clan_id": 10, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/flint.json b/bots/bots_clan/flint.json deleted file mode 100644 index bf228245f..000000000 --- a/bots/bots_clan/flint.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Flint", - "clan_id": 3, - "skin": "male/indy", - "weaponPreferences": { - "mk23": -0.0, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 1.2, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/frankdux.json b/bots/bots_clan/frankdux.json new file mode 100644 index 000000000..78639c5be --- /dev/null +++ b/bots/bots_clan/frankdux.json @@ -0,0 +1,35 @@ +{ + "name": "Frank Dux", + "clan_id": 15, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.2, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fred.json b/bots/bots_clan/fred.json new file mode 100644 index 000000000..3fa0e5db4 --- /dev/null +++ b/bots/bots_clan/fred.json @@ -0,0 +1,35 @@ +{ + "name": "Fred", + "clan_id": 58, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.9, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/frost.json b/bots/bots_clan/frost.json index 8030e5bc8..cea24e46e 100644 --- a/bots/bots_clan/frost.json +++ b/bots/bots_clan/frost.json @@ -1,35 +1,35 @@ { "name": "Frost", - "clan_id": 1, + "clan_id": 76, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.8, - "mp5": 0.1, - "m4": 0.5, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.3, "m3": 0.5, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.5 + "hc": 0.4, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.8, + "vest": 0.4, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.9, + "aggressiveness": 0.4, + "teamwork": 0.0, + "curiosity": 0.6, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": -0.1 + "reactionTime": 0.4, + "communicationFreq": 0.1, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/fujin.json b/bots/bots_clan/fujin.json new file mode 100644 index 000000000..c1407a35e --- /dev/null +++ b/bots/bots_clan/fujin.json @@ -0,0 +1,35 @@ +{ + "name": "Fujin", + "clan_id": 105, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/fullmonty.json b/bots/bots_clan/fullmonty.json deleted file mode 100644 index 2a34115c5..000000000 --- a/bots/bots_clan/fullmonty.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Fullmonty", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.2, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/funkanik.json b/bots/bots_clan/funkanik.json deleted file mode 100644 index 4e09a11ab..000000000 --- a/bots/bots_clan/funkanik.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "FUNKANIK", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": -0.0, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/fxguy.json b/bots/bots_clan/fxguy.json new file mode 100644 index 000000000..9197b457a --- /dev/null +++ b/bots/bots_clan/fxguy.json @@ -0,0 +1,35 @@ +{ + "name": "FXguy", + "clan_id": 37, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gali.json b/bots/bots_clan/gali.json deleted file mode 100644 index e1745848e..000000000 --- a/bots/bots_clan/gali.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gali", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.9, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/geras.json b/bots/bots_clan/geras.json index 4bf014188..32faea98e 100644 --- a/bots/bots_clan/geras.json +++ b/bots/bots_clan/geras.json @@ -1,35 +1,35 @@ { "name": "Geras", - "clan_id": 8, + "clan_id": 37, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, + "mk23": 0.0, + "dual_mk23": 0.0, "mp5": 0.4, - "m4": 0.3, - "m3": 0.3, - "hc": 0.4, + "m4": 0.7, + "m3": 0.4, + "hc": 0.5, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.6 + "knives": 0.1, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.6, - "helm": 0.2, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.9 + "helm": 0.6, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.3, "teamwork": 0.4, "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.6, - "communicationTone": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.2, "movementStyle": 0.6, - "objectiveFocus": 0.5 + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/gigavoltz.json b/bots/bots_clan/gigavoltz.json new file mode 100644 index 000000000..ed54edbf2 --- /dev/null +++ b/bots/bots_clan/gigavoltz.json @@ -0,0 +1,35 @@ +{ + "name": "Gigavoltz", + "clan_id": 19, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.1, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gladiator.json b/bots/bots_clan/gladiator.json deleted file mode 100644 index d1643abd9..000000000 --- a/bots/bots_clan/gladiator.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gladiator", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.6, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.9, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 1.0, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.7, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gogman.json b/bots/bots_clan/gogman.json deleted file mode 100644 index ac208dbe7..000000000 --- a/bots/bots_clan/gogman.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "gogman", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gordac.json b/bots/bots_clan/gordac.json new file mode 100644 index 000000000..d46b9726a --- /dev/null +++ b/bots/bots_clan/gordac.json @@ -0,0 +1,35 @@ +{ + "name": "Gordac", + "clan_id": 46, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.9, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.2, + "movementStyle": 0.8, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gorre.json b/bots/bots_clan/gorre.json index d3855a271..3ce21a5e0 100644 --- a/bots/bots_clan/gorre.json +++ b/bots/bots_clan/gorre.json @@ -1,35 +1,35 @@ { "name": "Gorre", - "clan_id": 4, + "clan_id": 97, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.5, + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.7, "knives": 0.8, - "grenades": 0.2 + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 1.0, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.2 + "vest": 0.3, + "helm": 1.0, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 1.1, "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "movementStyle": 0.2, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/grezzer.json b/bots/bots_clan/grezzer.json index 2eb259cff..7555c7175 100644 --- a/bots/bots_clan/grezzer.json +++ b/bots/bots_clan/grezzer.json @@ -1,35 +1,35 @@ { "name": "GreZZer", - "clan_id": 2, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 1.0, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.0 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, + "vest": 0.3, + "helm": 0.5, "laser": 0.6, "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.1 + "slippers": 0.8, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.7, + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.8, "movementStyle": 0.8, - "objectiveFocus": 0.3 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/griswold.json b/bots/bots_clan/griswold.json index 980dd7b39..080ea45c1 100644 --- a/bots/bots_clan/griswold.json +++ b/bots/bots_clan/griswold.json @@ -1,35 +1,35 @@ { "name": "Griswold", - "clan_id": 5, + "clan_id": 36, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.0, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.7, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.5 + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": -0.0, + "movementStyle": 0.2, + "objectiveFocus": 1.1 } } \ No newline at end of file diff --git a/bots/bots_clan/grunt.json b/bots/bots_clan/grunt.json deleted file mode 100644 index e6b4e9dc7..000000000 --- a/bots/bots_clan/grunt.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Grunt", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.1, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gun.json b/bots/bots_clan/gun.json deleted file mode 100644 index 90691c9da..000000000 --- a/bots/bots_clan/gun.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "GuN", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.5, - "m3": -0.0, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.1, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.9, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gungho.json b/bots/bots_clan/gungho.json new file mode 100644 index 000000000..d2f675d9b --- /dev/null +++ b/bots/bots_clan/gungho.json @@ -0,0 +1,35 @@ +{ + "name": "Gung Ho", + "clan_id": 77, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": -0.0, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/gunner.json b/bots/bots_clan/gunner.json index 7c28172b8..00cc14417 100644 --- a/bots/bots_clan/gunner.json +++ b/bots/bots_clan/gunner.json @@ -1,35 +1,35 @@ { "name": "Gunner", - "clan_id": 10, + "clan_id": 75, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.9, - "m4": 0.6, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.0 + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.4, - "helm": 0.9, + "helm": 0.4, "laser": 0.4, "silencer": 0.8, - "slippers": 0.8, - "bandolier": 0.4 + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "aggressiveness": 0.9, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/halfbaked.json b/bots/bots_clan/halfbaked.json new file mode 100644 index 000000000..42e75e907 --- /dev/null +++ b/bots/bots_clan/halfbaked.json @@ -0,0 +1,35 @@ +{ + "name": "Halfbaked", + "clan_id": 89, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 1.0, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.9, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/hammerhead.json b/bots/bots_clan/hammerhead.json index d108e3603..d0ee66709 100644 --- a/bots/bots_clan/hammerhead.json +++ b/bots/bots_clan/hammerhead.json @@ -1,35 +1,35 @@ { "name": "Hammerhead", - "clan_id": 2, + "clan_id": 48, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.5 + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.4, + "hc": 1.0, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.3, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 1.0, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.7 + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/havik.json b/bots/bots_clan/havik.json new file mode 100644 index 000000000..bf7d5ea77 --- /dev/null +++ b/bots/bots_clan/havik.json @@ -0,0 +1,35 @@ +{ + "name": "Havik", + "clan_id": 70, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.8, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 1.2 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.1, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.9, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/havo.json b/bots/bots_clan/havo.json deleted file mode 100644 index 68137827b..000000000 --- a/bots/bots_clan/havo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Havo", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.7, - "hc": 1.0, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hecker.json b/bots/bots_clan/hecker.json new file mode 100644 index 000000000..e3de77d40 --- /dev/null +++ b/bots/bots_clan/hecker.json @@ -0,0 +1,35 @@ +{ + "name": "Hecker", + "clan_id": 84, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 1.0, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/heljay.json b/bots/bots_clan/heljay.json new file mode 100644 index 000000000..31be1ee0c --- /dev/null +++ b/bots/bots_clan/heljay.json @@ -0,0 +1,35 @@ +{ + "name": "Heljay", + "clan_id": 53, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.8, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.9, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/helmut.json b/bots/bots_clan/helmut.json deleted file mode 100644 index afec58b0f..000000000 --- a/bots/bots_clan/helmut.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Helmut", - "clan_id": 10, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 1.0, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/himdar.json b/bots/bots_clan/himdar.json index 31b5eb488..2c7a096f0 100644 --- a/bots/bots_clan/himdar.json +++ b/bots/bots_clan/himdar.json @@ -1,35 +1,35 @@ { "name": "Himdar", - "clan_id": 5, + "clan_id": 76, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, + "mk23": 0.4, + "dual_mk23": 0.7, "mp5": 0.5, "m4": 0.5, "m3": 0.6, "hc": 0.5, - "sniper": 0.7, + "sniper": 1.0, "knives": 0.5, - "grenades": 0.4 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, + "vest": 0.2, "helm": 0.6, "laser": 0.4, "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.8 + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.8, - "reactionTime": 1.1, - "communicationFreq": 0.7, - "communicationTone": 0.1, - "movementStyle": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.9, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/hitnrun.json b/bots/bots_clan/hitnrun.json index 91946087b..f459859db 100644 --- a/bots/bots_clan/hitnrun.json +++ b/bots/bots_clan/hitnrun.json @@ -1,35 +1,35 @@ { "name": "Hitnrun", - "clan_id": 11, + "clan_id": 64, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.7 + "mk23": 0.8, + "dual_mk23": -0.0, + "mp5": 0.7, + "m4": 0.2, + "m3": 0.3, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.5, "helm": 0.3, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.6 + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.3, + "bandolier": 1.1 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/hossman.json b/bots/bots_clan/hossman.json index 3374acfef..1b8427ecd 100644 --- a/bots/bots_clan/hossman.json +++ b/bots/bots_clan/hossman.json @@ -1,35 +1,35 @@ { "name": "Hossman", - "clan_id": 9, + "clan_id": 88, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.5, - "knives": 1.0, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.8, + "vest": 0.5, + "helm": 0.6, "laser": 0.5, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.7 + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.1, - "aimingSkill": 0.3, + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.8, "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/hotaru.json b/bots/bots_clan/hotaru.json deleted file mode 100644 index 20e4f1a62..000000000 --- a/bots/bots_clan/hotaru.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hotaru", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hsuhao.json b/bots/bots_clan/hsuhao.json index 1d6c840c7..74a3e0653 100644 --- a/bots/bots_clan/hsuhao.json +++ b/bots/bots_clan/hsuhao.json @@ -1,35 +1,35 @@ { "name": "Hsu Hao", - "clan_id": 5, + "clan_id": 78, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, + "mk23": 0.7, "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.4, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, "sniper": 0.6, "knives": 0.6, - "grenades": 0.7 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.9, - "silencer": 0.2, - "slippers": 0.2, - "bandolier": 0.3 + "vest": 0.8, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.2, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 1.1, "reactionTime": 0.5, - "communicationFreq": 0.4, + "communicationFreq": 0.3, "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/hunter.json b/bots/bots_clan/hunter.json deleted file mode 100644 index d6d704d54..000000000 --- a/bots/bots_clan/hunter.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hunter", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 1.0, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hurricane.json b/bots/bots_clan/hurricane.json index dc85a553f..c5c4440e1 100644 --- a/bots/bots_clan/hurricane.json +++ b/bots/bots_clan/hurricane.json @@ -1,35 +1,35 @@ { "name": "Hurricane", - "clan_id": 4, + "clan_id": 38, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.6, + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.8, + "m3": 0.2, "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.7 + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.2, "helm": 0.8, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.9, - "bandolier": 0.6 + "laser": 0.4, + "silencer": 0.9, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.8, "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/icarus.json b/bots/bots_clan/icarus.json index e44a8ec4c..0af5e3db7 100644 --- a/bots/bots_clan/icarus.json +++ b/bots/bots_clan/icarus.json @@ -1,35 +1,35 @@ { "name": "Icarus", - "clan_id": 1, + "clan_id": 104, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.8, + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.1, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.1, - "silencer": 0.7, + "vest": 0.9, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.8, "slippers": 0.6, - "bandolier": 0.2 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.6, "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.2, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_clan/icehawk.json b/bots/bots_clan/icehawk.json index 83692c00a..1b220944b 100644 --- a/bots/bots_clan/icehawk.json +++ b/bots/bots_clan/icehawk.json @@ -1,35 +1,35 @@ { "name": "Icehawk", - "clan_id": 6, + "clan_id": 17, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.9, - "m4": 0.3, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.3, + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 1.1, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.0, "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 0.6, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, + "aggressiveness": 1.0, + "teamwork": 0.5, "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.4, "communicationTone": 0.7, - "movementStyle": 0.4, + "movementStyle": 0.8, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/icexo.json b/bots/bots_clan/icexo.json deleted file mode 100644 index f982dd2b2..000000000 --- a/bots/bots_clan/icexo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Icexo", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ismail.json b/bots/bots_clan/ismail.json index ec1430265..1e27221b0 100644 --- a/bots/bots_clan/ismail.json +++ b/bots/bots_clan/ismail.json @@ -1,35 +1,35 @@ { "name": "Ismail", - "clan_id": 2, + "clan_id": 101, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.5, + "dual_mk23": 0.7, + "mp5": 0.6, "m4": 0.6, "m3": 0.3, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.4 + "hc": 0.8, + "sniper": 0.8, + "knives": 0.1, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.0, + "aggressiveness": 0.6, "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.3, "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.5, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/itixoid.json b/bots/bots_clan/itixoid.json new file mode 100644 index 000000000..dc541e1be --- /dev/null +++ b/bots/bots_clan/itixoid.json @@ -0,0 +1,35 @@ +{ + "name": "itiXOID", + "clan_id": 69, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.1, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/itzchamp.json b/bots/bots_clan/itzchamp.json deleted file mode 100644 index c1295f3b7..000000000 --- a/bots/bots_clan/itzchamp.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "{itzChamp}", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.1, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.8, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ivor.json b/bots/bots_clan/ivor.json index e696ff5d9..3e49a6c40 100644 --- a/bots/bots_clan/ivor.json +++ b/bots/bots_clan/ivor.json @@ -1,35 +1,35 @@ { "name": "Ivor", - "clan_id": 9, + "clan_id": 45, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.7, - "hc": 0.7, + "mk23": 0.1, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, "sniper": 0.5, - "knives": 0.1, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.9, "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/jacksaxon.json b/bots/bots_clan/jacksaxon.json deleted file mode 100644 index 1a09ae121..000000000 --- a/bots/bots_clan/jacksaxon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jack Saxon", - "clan_id": 3, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 1.1, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 1.0, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 1.0, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/jade.json b/bots/bots_clan/jade.json deleted file mode 100644 index 54b9ca693..000000000 --- a/bots/bots_clan/jade.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jade", - "clan_id": 10, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.1, - "movementStyle": 0.6, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/jarek.json b/bots/bots_clan/jarek.json new file mode 100644 index 000000000..bffd2b133 --- /dev/null +++ b/bots/bots_clan/jarek.json @@ -0,0 +1,35 @@ +{ + "name": "Jarek", + "clan_id": 77, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.8, + "hc": 0.1, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 1.0, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jarno.json b/bots/bots_clan/jarno.json index 6fb080956..4f2fb2cda 100644 --- a/bots/bots_clan/jarno.json +++ b/bots/bots_clan/jarno.json @@ -1,35 +1,35 @@ { "name": "Jarno", - "clan_id": 4, + "clan_id": 47, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.6, + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.4, "m4": 0.4, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.4, "knives": 0.6, - "grenades": 0.3 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.3, + "laser": 0.9, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.6 + "aggressiveness": 0.1, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": -0.1 } } \ No newline at end of file diff --git a/bots/bots_clan/jarvis.json b/bots/bots_clan/jarvis.json index d60595f54..458c35c2b 100644 --- a/bots/bots_clan/jarvis.json +++ b/bots/bots_clan/jarvis.json @@ -1,35 +1,35 @@ { "name": "Jarvis", - "clan_id": 3, + "clan_id": 4, "skin": "male/indy", "weaponPreferences": { - "mk23": 1.1, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.6 + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 0.5, - "silencer": 0.7, + "vest": 0.6, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.4, "slippers": 0.4, - "bandolier": 0.4 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/jazz.json b/bots/bots_clan/jazz.json index fddbdc65d..cd5344040 100644 --- a/bots/bots_clan/jazz.json +++ b/bots/bots_clan/jazz.json @@ -1,35 +1,35 @@ { "name": "Jazz", - "clan_id": 10, + "clan_id": 12, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, + "mk23": 0.3, + "dual_mk23": 1.0, "mp5": 0.4, - "m4": 0.5, - "m3": 0.7, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.9 + "m4": 0.8, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, + "vest": 0.1, "helm": 0.6, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.1 + "laser": 0.8, + "silencer": 0.1, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/jce.json b/bots/bots_clan/jce.json index 8cdc69e87..f3efeb4d0 100644 --- a/bots/bots_clan/jce.json +++ b/bots/bots_clan/jce.json @@ -1,35 +1,35 @@ { "name": "JCe", - "clan_id": 6, + "clan_id": 20, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.3, + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.1, "hc": 0.4, "sniper": 0.7, - "knives": 0.4, - "grenades": 0.3 + "knives": 0.2, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.5, + "vest": 0.7, + "helm": 0.6, + "laser": 0.8, "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.5 + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "movementStyle": 0.3, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/jdgold.json b/bots/bots_clan/jdgold.json new file mode 100644 index 000000000..fd2378603 --- /dev/null +++ b/bots/bots_clan/jdgold.json @@ -0,0 +1,35 @@ +{ + "name": "JD Gold", + "clan_id": 27, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/jinx.json b/bots/bots_clan/jinx.json new file mode 100644 index 000000000..9643bcef6 --- /dev/null +++ b/bots/bots_clan/jinx.json @@ -0,0 +1,35 @@ +{ + "name": "Jinx", + "clan_id": 88, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.2, + "movementStyle": 0.7, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/joce.json b/bots/bots_clan/joce.json new file mode 100644 index 000000000..a6df0cffa --- /dev/null +++ b/bots/bots_clan/joce.json @@ -0,0 +1,35 @@ +{ + "name": "Joce", + "clan_id": 49, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/joesanto.json b/bots/bots_clan/joesanto.json deleted file mode 100644 index c8fbd42b0..000000000 --- a/bots/bots_clan/joesanto.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Joe Santo", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 1.0, - "reactionTime": 0.6, - "communicationFreq": 0.9, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/joetanto.json b/bots/bots_clan/joetanto.json deleted file mode 100644 index 8cdd89459..000000000 --- a/bots/bots_clan/joetanto.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Joe Tanto", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.1, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 1.0, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/johnluger.json b/bots/bots_clan/johnluger.json index 08c048955..9f871ec1f 100644 --- a/bots/bots_clan/johnluger.json +++ b/bots/bots_clan/johnluger.json @@ -1,35 +1,35 @@ { "name": "John Luger", - "clan_id": 2, + "clan_id": 97, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.9, + "mk23": 0.6, + "dual_mk23": 0.8, "mp5": 0.6, - "m4": 0.2, - "m3": 0.6, - "hc": 0.2, + "m4": 0.7, + "m3": 0.5, + "hc": 0.3, "sniper": 0.4, - "knives": 0.3, - "grenades": 0.7 + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": -0.0, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, + "vest": 0.8, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.5, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.7, + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.6, "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.2 + "reactionTime": 0.9, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/johnrambo.json b/bots/bots_clan/johnrambo.json new file mode 100644 index 000000000..b9dc4f677 --- /dev/null +++ b/bots/bots_clan/johnrambo.json @@ -0,0 +1,35 @@ +{ + "name": "John Rambo", + "clan_id": 36, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.3, + "hc": 0.3, + "sniper": 1.1, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.3, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.9, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kai.json b/bots/bots_clan/kai.json new file mode 100644 index 000000000..e277d3b5e --- /dev/null +++ b/bots/bots_clan/kai.json @@ -0,0 +1,35 @@ +{ + "name": "Kai", + "clan_id": 104, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": -0.0, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kalidor.json b/bots/bots_clan/kalidor.json deleted file mode 100644 index 59a9bdcd7..000000000 --- a/bots/bots_clan/kalidor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kalidor", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.0, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.1, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kano.json b/bots/bots_clan/kano.json new file mode 100644 index 000000000..273a2f62b --- /dev/null +++ b/bots/bots_clan/kano.json @@ -0,0 +1,35 @@ +{ + "name": "Kano", + "clan_id": 82, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.9, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": -0.0, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.9, + "reactionTime": 0.8, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kaolin.json b/bots/bots_clan/kaolin.json index 3d6687fe1..523529c05 100644 --- a/bots/bots_clan/kaolin.json +++ b/bots/bots_clan/kaolin.json @@ -1,35 +1,35 @@ { "name": "Kaolin", - "clan_id": 9, + "clan_id": 42, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.2, - "mp5": 0.2, - "m4": 0.2, - "m3": 0.7, - "hc": 0.6, + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.8, "sniper": 0.4, - "knives": 0.5, + "knives": 0.3, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, + "vest": 0.4, + "helm": 0.4, "laser": 0.5, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, + "aggressiveness": 0.8, + "teamwork": 0.2, "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.1, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.8 + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/keel.json b/bots/bots_clan/keel.json index 4fc51c7f9..f67ad7a64 100644 --- a/bots/bots_clan/keel.json +++ b/bots/bots_clan/keel.json @@ -1,35 +1,35 @@ { "name": "Keel", - "clan_id": 4, + "clan_id": 41, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, + "mk23": 0.7, + "dual_mk23": 0.4, "mp5": 0.6, - "m4": 0.8, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.7 + "m4": 0.7, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.0, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, + "vest": 0.8, "helm": 0.7, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.4 + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.6, "communicationTone": 0.4, - "movementStyle": 0.5, + "movementStyle": 0.8, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/khameleon.json b/bots/bots_clan/khameleon.json new file mode 100644 index 000000000..d2abeb1f4 --- /dev/null +++ b/bots/bots_clan/khameleon.json @@ -0,0 +1,35 @@ +{ + "name": "Khameleon", + "clan_id": 21, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 1.0, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 1.1 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 1.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kintaro.json b/bots/bots_clan/kintaro.json deleted file mode 100644 index d3d1982e1..000000000 --- a/bots/bots_clan/kintaro.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kintaro", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.9 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.0, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.2, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 1.0, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kippy.json b/bots/bots_clan/kippy.json new file mode 100644 index 000000000..51fee9e83 --- /dev/null +++ b/bots/bots_clan/kippy.json @@ -0,0 +1,35 @@ +{ + "name": "Kippy", + "clan_id": 11, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.9, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/kitana.json b/bots/bots_clan/kitana.json deleted file mode 100644 index a01ceaeec..000000000 --- a/bots/bots_clan/kitana.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kitana", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kitlatura.json b/bots/bots_clan/kitlatura.json new file mode 100644 index 000000000..ec60eee9a --- /dev/null +++ b/bots/bots_clan/kitlatura.json @@ -0,0 +1,35 @@ +{ + "name": "Kit Latura", + "clan_id": 25, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/klesk.json b/bots/bots_clan/klesk.json index 05e6c448f..11b33afbd 100644 --- a/bots/bots_clan/klesk.json +++ b/bots/bots_clan/klesk.json @@ -1,35 +1,35 @@ { "name": "Klesk", - "clan_id": 10, + "clan_id": 71, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.4, + "mk23": 0.7, + "dual_mk23": 0.3, "mp5": 0.6, - "m4": 0.2, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.7, + "m4": 0.5, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.5, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.0, - "bandolier": 0.9 + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": -0.1, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": -0.1, - "communicationTone": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.3, "movementStyle": 0.7, - "objectiveFocus": 0.4 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/strom.json b/bots/bots_clan/kobra.json similarity index 50% rename from bots/bots_clan/strom.json rename to bots/bots_clan/kobra.json index 8ee81cd80..d1d775613 100644 --- a/bots/bots_clan/strom.json +++ b/bots/bots_clan/kobra.json @@ -1,35 +1,35 @@ { - "name": "Strom", - "clan_id": 7, + "name": "Kobra", + "clan_id": 99, "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.6, + "dual_mk23": 0.7, "mp5": 0.5, - "m4": 0.4, - "m3": 0.8, + "m4": 0.5, + "m3": 0.2, "hc": 0.4, - "sniper": 0.9, + "sniper": 0.5, "knives": 0.7, "grenades": 0.4 }, "itemPreferences": { "vest": 0.6, - "helm": 0.3, + "helm": 0.6, "laser": 0.4, - "silencer": 0.3, + "silencer": 0.7, "slippers": 0.7, - "bandolier": 0.4 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.5, "aimingSkill": 0.7, "reactionTime": 0.5, "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 1.0 + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/kollector.json b/bots/bots_clan/kollector.json deleted file mode 100644 index 315eba200..000000000 --- a/bots/bots_clan/kollector.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kollector", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kor.json b/bots/bots_clan/kor.json index 560a9839d..9df6c3c5e 100644 --- a/bots/bots_clan/kor.json +++ b/bots/bots_clan/kor.json @@ -1,35 +1,35 @@ { "name": "Kor", - "clan_id": 6, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, + "mk23": 0.4, "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.9, - "knives": 0.5, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, + "vest": 0.7, + "helm": 0.6, "laser": 0.6, - "silencer": 0.5, + "silencer": 0.4, "slippers": 0.7, - "bandolier": 0.7 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.1, + "aimingSkill": 0.1, + "reactionTime": 0.8, "communicationFreq": 0.5, - "communicationTone": 0.1, - "movementStyle": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.5, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/kronika.json b/bots/bots_clan/kronika.json index 37c338e5f..a7534db18 100644 --- a/bots/bots_clan/kronika.json +++ b/bots/bots_clan/kronika.json @@ -1,35 +1,35 @@ { "name": "Kronika", - "clan_id": 8, + "clan_id": 62, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": -0.0, - "m3": 0.2, - "hc": 0.2, - "sniper": 0.2, - "knives": 0.6, + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, "grenades": 0.8 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.9, - "laser": 0.7, - "silencer": 0.2, - "slippers": 0.1, + "vest": 0.6, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.6, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "reactionTime": 0.0, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/kunglao.json b/bots/bots_clan/kunglao.json index d46c93984..78fe1213c 100644 --- a/bots/bots_clan/kunglao.json +++ b/bots/bots_clan/kunglao.json @@ -1,35 +1,35 @@ { "name": "Kung Lao", - "clan_id": 1, + "clan_id": 25, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.3, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.9, + "m4": 0.8, + "m3": 0.1, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.4, - "laser": 0.6, + "vest": 0.5, + "helm": 0.9, + "laser": 0.4, "silencer": 0.5, - "slippers": 0.5, + "slippers": 0.6, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.2, - "movementStyle": 0.2, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/kuri.json b/bots/bots_clan/kuri.json deleted file mode 100644 index 4132d5873..000000000 --- a/bots/bots_clan/kuri.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "kuri", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.9, - "m4": 0.8, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.8, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/lawrence.json b/bots/bots_clan/lawrence.json deleted file mode 100644 index 1955d06f7..000000000 --- a/bots/bots_clan/lawrence.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lawrence", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.1, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/leatherarm.json b/bots/bots_clan/leatherarm.json new file mode 100644 index 000000000..16102dc43 --- /dev/null +++ b/bots/bots_clan/leatherarm.json @@ -0,0 +1,35 @@ +{ + "name": "Leatherarm", + "clan_id": 79, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/leiruth.json b/bots/bots_clan/leiruth.json index 3e7339c09..2e2156fd8 100644 --- a/bots/bots_clan/leiruth.json +++ b/bots/bots_clan/leiruth.json @@ -1,35 +1,35 @@ { "name": "Leiruth", - "clan_id": 11, + "clan_id": 4, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.5, + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.6, "m4": 0.6, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.5, + "m3": 0.3, + "hc": 0.8, + "sniper": 0.7, "knives": 0.6, - "grenades": 0.5 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.5, + "vest": 0.5, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.0, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.0, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.4 + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/lemmie.json b/bots/bots_clan/lemmie.json new file mode 100644 index 000000000..adba3b66c --- /dev/null +++ b/bots/bots_clan/lemmie.json @@ -0,0 +1,35 @@ +{ + "name": "Lemmie", + "clan_id": 67, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/limei.json b/bots/bots_clan/limei.json deleted file mode 100644 index aaff589a5..000000000 --- a/bots/bots_clan/limei.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Li Mei", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.8, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 1.0, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/liukang.json b/bots/bots_clan/liukang.json index 532334b2c..98f80599a 100644 --- a/bots/bots_clan/liukang.json +++ b/bots/bots_clan/liukang.json @@ -1,35 +1,35 @@ { "name": "Liu Kang", - "clan_id": 4, + "clan_id": 60, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.8, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.2, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.8 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "teamwork": 0.4, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/lonewolf.json b/bots/bots_clan/lonewolf.json deleted file mode 100644 index ba11705d0..000000000 --- a/bots/bots_clan/lonewolf.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lone Wolf", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.1, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.1, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.9, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/lucy.json b/bots/bots_clan/lucy.json new file mode 100644 index 000000000..1f194dad0 --- /dev/null +++ b/bots/bots_clan/lucy.json @@ -0,0 +1,35 @@ +{ + "name": "Lucy", + "clan_id": 94, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/maffer.json b/bots/bots_clan/maffer.json index c5a750a3f..aeb453057 100644 --- a/bots/bots_clan/maffer.json +++ b/bots/bots_clan/maffer.json @@ -1,35 +1,35 @@ { "name": "Maffer", - "clan_id": 10, + "clan_id": 53, "skin": "male/indy", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.9, - "m3": 0.5, - "hc": 0.3, + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.7, + "hc": 0.7, "sniper": 0.4, - "knives": 0.7, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, + "vest": 0.1, + "helm": 0.1, "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.2 + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.5, "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.1 + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": -0.0, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/ermac.json b/bots/bots_clan/major.json similarity index 52% rename from bots/bots_clan/ermac.json rename to bots/bots_clan/major.json index 174643f5f..94d0a1fdb 100644 --- a/bots/bots_clan/ermac.json +++ b/bots/bots_clan/major.json @@ -1,35 +1,35 @@ { - "name": "Ermac", - "clan_id": 2, + "name": "Major", + "clan_id": 12, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.4, + "dual_mk23": 0.3, + "mp5": 0.5, "m4": 0.7, "m3": 0.6, "hc": 0.6, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.4 + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, + "vest": 0.5, + "helm": 0.7, "laser": 0.4, - "silencer": 0.7, + "silencer": 0.6, "slippers": 0.4, - "bandolier": 0.7 + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, + "aggressiveness": 0.3, + "teamwork": 0.4, "curiosity": 0.3, "aimingSkill": 0.2, "reactionTime": 0.7, "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/majorpain.json b/bots/bots_clan/majorpain.json new file mode 100644 index 000000000..a2974385f --- /dev/null +++ b/bots/bots_clan/majorpain.json @@ -0,0 +1,35 @@ +{ + "name": "Major Pain", + "clan_id": 38, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 1.0, + "curiosity": 0.2, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/makron.json b/bots/bots_clan/makron.json new file mode 100644 index 000000000..4f1a0dfc9 --- /dev/null +++ b/bots/bots_clan/makron.json @@ -0,0 +1,35 @@ +{ + "name": "Makron", + "clan_id": 25, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 1.0 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/malin.json b/bots/bots_clan/malin.json index 85678b07b..d972ffdc0 100644 --- a/bots/bots_clan/malin.json +++ b/bots/bots_clan/malin.json @@ -1,35 +1,35 @@ { "name": "Malin", - "clan_id": 2, + "clan_id": 37, "skin": "male/indy", "weaponPreferences": { "mk23": 0.7, "dual_mk23": 0.3, - "mp5": 0.7, + "mp5": 0.3, "m4": 0.4, - "m3": 0.7, - "hc": 0.0, - "sniper": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.4, "knives": 0.6, - "grenades": 0.4 + "grenades": 0.7 }, "itemPreferences": { "vest": 0.6, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.7 + "helm": 0.3, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.4, "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/manny.json b/bots/bots_clan/manny.json index f591b3272..2467422e5 100644 --- a/bots/bots_clan/manny.json +++ b/bots/bots_clan/manny.json @@ -1,35 +1,35 @@ { "name": "MannY", - "clan_id": 6, + "clan_id": 100, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, + "mk23": 0.6, + "dual_mk23": 0.5, "mp5": 0.6, - "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.6, + "m4": 0.7, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.2, "knives": 0.6, - "grenades": 0.5 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.8, - "silencer": 0.5, + "vest": 0.3, + "helm": 0.0, + "laser": 0.6, + "silencer": 0.2, "slippers": 0.6, - "bandolier": 0.3 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.1, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.8 + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/marku.json b/bots/bots_clan/marku.json index 11cd4041d..3ffa157cb 100644 --- a/bots/bots_clan/marku.json +++ b/bots/bots_clan/marku.json @@ -1,35 +1,35 @@ { "name": "Marku", - "clan_id": 8, + "clan_id": 104, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.4, - "hc": 0.3, + "mk23": 0.5, + "dual_mk23": 0.1, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.1, + "hc": 0.5, "sniper": 0.3, - "knives": 0.5, - "grenades": 0.6 + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.7, + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, "slippers": 0.3, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.5, - "curiosity": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.8, "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "movementStyle": 0.3, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/mavado.json b/bots/bots_clan/mavado.json index 45d010e3b..aad2621d3 100644 --- a/bots/bots_clan/mavado.json +++ b/bots/bots_clan/mavado.json @@ -1,35 +1,35 @@ { "name": "Mavado", - "clan_id": 8, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, + "mk23": 0.5, + "dual_mk23": 0.5, "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, + "m4": 0.6, + "m3": 0.1, "hc": 0.6, - "sniper": 0.2, + "sniper": 0.6, "knives": 0.7, - "grenades": 0.3 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.1, + "vest": 0.0, + "helm": 0.4, "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.3 + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.3, - "communicationFreq": 0.3, + "aggressiveness": 0.9, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": -0.1, + "communicationFreq": 0.5, "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "movementStyle": 0.0, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/mckenna.json b/bots/bots_clan/mckenna.json new file mode 100644 index 000000000..563e57b06 --- /dev/null +++ b/bots/bots_clan/mckenna.json @@ -0,0 +1,35 @@ +{ + "name": "McKenna", + "clan_id": 87, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.5, + "slippers": 1.1, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/meat.json b/bots/bots_clan/meat.json index 200f54225..559c52a97 100644 --- a/bots/bots_clan/meat.json +++ b/bots/bots_clan/meat.json @@ -1,35 +1,35 @@ { "name": "Meat", - "clan_id": 5, + "clan_id": 56, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.8, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.9, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.5, + "vest": 0.4, + "helm": 0.1, + "laser": 0.8, "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.9 + "slippers": 0.6, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.4, "teamwork": 0.3, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.5, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/medic.json b/bots/bots_clan/medic.json index 5ddc60b1f..cf8bcc8e1 100644 --- a/bots/bots_clan/medic.json +++ b/bots/bots_clan/medic.json @@ -1,35 +1,35 @@ { "name": "Medic", - "clan_id": 8, + "clan_id": 22, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 1.0, - "m4": 0.6, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.5, "helm": 0.4, - "laser": 0.6, + "laser": 0.7, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 + "slippers": 0.7, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, + "aggressiveness": 0.3, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": -0.1, - "movementStyle": 0.7, - "objectiveFocus": 0.3 + "aimingSkill": 0.1, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/mee2.json b/bots/bots_clan/mee2.json index af450fe61..15111e6f7 100644 --- a/bots/bots_clan/mee2.json +++ b/bots/bots_clan/mee2.json @@ -1,35 +1,35 @@ { "name": "[Mee2]", - "clan_id": 3, + "clan_id": 25, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, + "mk23": 0.3, + "dual_mk23": 0.4, "mp5": 0.5, - "m4": 0.3, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.5, + "m4": 0.2, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.4, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "aggressiveness": 0.2, + "teamwork": -0.1, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/merki.json b/bots/bots_clan/merki.json new file mode 100644 index 000000000..253a12c5c --- /dev/null +++ b/bots/bots_clan/merki.json @@ -0,0 +1,35 @@ +{ + "name": "Merki", + "clan_id": 91, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.2, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 1.0, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/michael.json b/bots/bots_clan/michael.json new file mode 100644 index 000000000..5e9c366d9 --- /dev/null +++ b/bots/bots_clan/michael.json @@ -0,0 +1,35 @@ +{ + "name": "Michael", + "clan_id": 21, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.9, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.9, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mileena.json b/bots/bots_clan/mileena.json new file mode 100644 index 000000000..c47e91cb5 --- /dev/null +++ b/bots/bots_clan/mileena.json @@ -0,0 +1,35 @@ +{ + "name": "Mileena", + "clan_id": 17, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mincer.json b/bots/bots_clan/mincer.json new file mode 100644 index 000000000..b0e55ab3e --- /dev/null +++ b/bots/bots_clan/mincer.json @@ -0,0 +1,35 @@ +{ + "name": "Mincer", + "clan_id": 106, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mindelos.json b/bots/bots_clan/mindelos.json deleted file mode 100644 index c14050d7b..000000000 --- a/bots/bots_clan/mindelos.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mindelos", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.3, - "silencer": 0.0, - "slippers": 0.3, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/missnrun.json b/bots/bots_clan/missnrun.json index b85f11536..e946ceb15 100644 --- a/bots/bots_clan/missnrun.json +++ b/bots/bots_clan/missnrun.json @@ -1,35 +1,35 @@ { "name": "Missnrun", - "clan_id": 2, + "clan_id": 25, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.8, + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.5, "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, + "hc": 0.5, + "sniper": 0.8, "knives": 0.5, - "grenades": 0.5 + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.2 + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 1.1 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.4, "communicationFreq": 0.4, "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "movementStyle": 0.2, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/mokap.json b/bots/bots_clan/mokap.json index 38dfd5809..459fbc7ab 100644 --- a/bots/bots_clan/mokap.json +++ b/bots/bots_clan/mokap.json @@ -1,35 +1,35 @@ { "name": "Mokap", - "clan_id": 7, + "clan_id": 2, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.3, - "grenades": 0.8 + "mk23": 0.5, + "dual_mk23": 0.1, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, + "vest": 0.3, + "helm": 0.4, "laser": 0.6, - "silencer": 0.5, - "slippers": 0.9, + "silencer": 0.3, + "slippers": 0.6, "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.0, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/monty.json b/bots/bots_clan/monty.json deleted file mode 100644 index 411ea7faf..000000000 --- a/bots/bots_clan/monty.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Monty", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.1, - "objectiveFocus": -0.0 - } -} \ No newline at end of file diff --git a/bots/bots_clan/morgan.json b/bots/bots_clan/morgan.json deleted file mode 100644 index 6b4518ac2..000000000 --- a/bots/bots_clan/morgan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Morgan", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.0, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/motaro.json b/bots/bots_clan/motaro.json new file mode 100644 index 000000000..f5db8a598 --- /dev/null +++ b/bots/bots_clan/motaro.json @@ -0,0 +1,35 @@ +{ + "name": "Motaro", + "clan_id": 99, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.9, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mrbadger.json b/bots/bots_clan/mrbadger.json deleted file mode 100644 index 2cfde5d73..000000000 --- a/bots/bots_clan/mrbadger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mr.Badger", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.2, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.9, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/mrgoodkat.json b/bots/bots_clan/mrgoodkat.json index 501016320..5af82cf9d 100644 --- a/bots/bots_clan/mrgoodkat.json +++ b/bots/bots_clan/mrgoodkat.json @@ -1,35 +1,35 @@ { "name": "Mr Goodkat", - "clan_id": 9, + "clan_id": 14, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.2, + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.3, "m3": 0.6, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 + "hc": 0.4, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.1 + "vest": 0.3, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 1.1, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/mrsuave.json b/bots/bots_clan/mrsuave.json new file mode 100644 index 000000000..6ce201ae1 --- /dev/null +++ b/bots/bots_clan/mrsuave.json @@ -0,0 +1,35 @@ +{ + "name": "Mr Suave", + "clan_id": 35, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/mungri.json b/bots/bots_clan/mungri.json deleted file mode 100644 index fffe3a65a..000000000 --- a/bots/bots_clan/mungri.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mungri", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.0, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.1, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/muscleman.json b/bots/bots_clan/muscleman.json index c16310705..969bc6dba 100644 --- a/bots/bots_clan/muscleman.json +++ b/bots/bots_clan/muscleman.json @@ -1,35 +1,35 @@ { "name": "Muscleman", - "clan_id": 8, + "clan_id": 73, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.4, "m3": 0.5, "hc": 0.2, - "sniper": 0.6, + "sniper": 0.5, "knives": 0.5, - "grenades": 0.7 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.6 + "vest": 0.4, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.4, "teamwork": 0.5, "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.6, "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/mynx.json b/bots/bots_clan/mynx.json deleted file mode 100644 index 68c4efe47..000000000 --- a/bots/bots_clan/mynx.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mynx", - "clan_id": 3, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.7, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": -0.0, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/nandet.json b/bots/bots_clan/nandet.json deleted file mode 100644 index e968c9fc8..000000000 --- a/bots/bots_clan/nandet.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nandet", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.9, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.9, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/newbie.json b/bots/bots_clan/newbie.json new file mode 100644 index 000000000..b5820c954 --- /dev/null +++ b/bots/bots_clan/newbie.json @@ -0,0 +1,35 @@ +{ + "name": "Newbie", + "clan_id": 103, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.2, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nibbler.json b/bots/bots_clan/nibbler.json new file mode 100644 index 000000000..9ada9f81b --- /dev/null +++ b/bots/bots_clan/nibbler.json @@ -0,0 +1,35 @@ +{ + "name": "Nibbler", + "clan_id": 90, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.0, + "mp5": 0.4, + "m4": 1.0, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.9, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nightwolf.json b/bots/bots_clan/nightwolf.json deleted file mode 100644 index 475801dce..000000000 --- a/bots/bots_clan/nightwolf.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nightwolf", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.1, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.9, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/nihlathak.json b/bots/bots_clan/nihlathak.json new file mode 100644 index 000000000..4252da242 --- /dev/null +++ b/bots/bots_clan/nihlathak.json @@ -0,0 +1,35 @@ +{ + "name": "Nihlathak", + "clan_id": 109, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.1 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.9, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/nin.json b/bots/bots_clan/nin.json index 35b22b9c7..7e560415f 100644 --- a/bots/bots_clan/nin.json +++ b/bots/bots_clan/nin.json @@ -1,35 +1,35 @@ { "name": "NiN", - "clan_id": 8, + "clan_id": 92, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.6, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.8, "m3": 0.5, - "hc": 0.8, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.3 + "hc": 0.7, + "sniper": 0.5, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.3, + "vest": 0.1, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.6, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, + "aggressiveness": 0.6, "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/nitara.json b/bots/bots_clan/nitara.json index 3cde42fe2..ccdccfbd6 100644 --- a/bots/bots_clan/nitara.json +++ b/bots/bots_clan/nitara.json @@ -1,35 +1,35 @@ { "name": "Nitara", - "clan_id": 10, + "clan_id": 89, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.6, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, "m4": 0.4, - "m3": 0.6, + "m3": 0.3, "hc": 0.4, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.8 + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.0, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.4, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/nohope.json b/bots/bots_clan/nohope.json new file mode 100644 index 000000000..71ffe98f9 --- /dev/null +++ b/bots/bots_clan/nohope.json @@ -0,0 +1,35 @@ +{ + "name": "Nohope", + "clan_id": 24, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.2, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/noogie.json b/bots/bots_clan/noogie.json deleted file mode 100644 index b29454436..000000000 --- a/bots/bots_clan/noogie.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Noogie", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/onaga.json b/bots/bots_clan/onaga.json new file mode 100644 index 000000000..17ae7ee7f --- /dev/null +++ b/bots/bots_clan/onaga.json @@ -0,0 +1,35 @@ +{ + "name": "Onaga", + "clan_id": 91, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.6, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.8, + "slippers": 1.0, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/orbb.json b/bots/bots_clan/orbb.json index 9325a07fc..87b4add07 100644 --- a/bots/bots_clan/orbb.json +++ b/bots/bots_clan/orbb.json @@ -1,35 +1,35 @@ { "name": "Orbb", - "clan_id": 8, + "clan_id": 1, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.7, + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.4, "knives": 0.5, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.2, - "silencer": 0.4, + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.6, "slippers": 0.5, - "bandolier": 0.3 + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.3, "reactionTime": 0.5, "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "communicationTone": 0.1, + "movementStyle": 0.2, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/orinboyd.json b/bots/bots_clan/orinboyd.json index a1cdabb49..33e6d1993 100644 --- a/bots/bots_clan/orinboyd.json +++ b/bots/bots_clan/orinboyd.json @@ -1,35 +1,35 @@ { "name": "Orin Boyd", - "clan_id": 10, + "clan_id": 101, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, + "mk23": 0.5, + "dual_mk23": 0.7, "mp5": 0.4, - "m4": 0.5, + "m4": 0.3, "m3": 0.1, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.9, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.6, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.8 + "vest": 0.8, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.1, + "aggressiveness": 0.8, "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/ostmor.json b/bots/bots_clan/ostmor.json index 12d8223cc..b042d699e 100644 --- a/bots/bots_clan/ostmor.json +++ b/bots/bots_clan/ostmor.json @@ -1,35 +1,35 @@ { "name": "Ostmor", - "clan_id": 9, + "clan_id": 5, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.5, - "m4": 0.5, + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, "m3": 0.6, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 + "hc": 0.3, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.2, + "vest": 0.1, + "helm": 0.6, "laser": 0.5, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.7 + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.1, - "reactionTime": 1.0, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/oysterhead.json b/bots/bots_clan/oysterhead.json new file mode 100644 index 000000000..9b58187b1 --- /dev/null +++ b/bots/bots_clan/oysterhead.json @@ -0,0 +1,35 @@ +{ + "name": "Oysterhead", + "clan_id": 61, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.0, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.6, + "aimingSkill": 0.9, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/paradox.json b/bots/bots_clan/paradox.json index 723e24d7e..daedceb82 100644 --- a/bots/bots_clan/paradox.json +++ b/bots/bots_clan/paradox.json @@ -1,35 +1,35 @@ { "name": "ParaDox", - "clan_id": 6, + "clan_id": 25, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, + "mk23": 0.1, "dual_mk23": 0.6, "mp5": 0.4, - "m4": 0.5, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.3 + "m4": 0.7, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.9, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.8, + "vest": 0.4, + "helm": 0.5, "laser": 0.4, - "silencer": 0.6, + "silencer": 0.4, "slippers": 0.4, - "bandolier": 0.3 + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/patriot.json b/bots/bots_clan/patriot.json index f65f39de0..bc7c554ef 100644 --- a/bots/bots_clan/patriot.json +++ b/bots/bots_clan/patriot.json @@ -1,35 +1,35 @@ { "name": "Patriot", - "clan_id": 6, + "clan_id": 15, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.6, + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.8, "m3": 0.5, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.6 + "hc": 0.5, + "sniper": 0.8, + "knives": 0.8, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.9, - "communicationTone": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 1.1, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.3, "movementStyle": 0.4, - "objectiveFocus": 0.5 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/pewdiepie.json b/bots/bots_clan/pewdiepie.json index 97bad8559..a9c13944a 100644 --- a/bots/bots_clan/pewdiepie.json +++ b/bots/bots_clan/pewdiepie.json @@ -1,35 +1,35 @@ { "name": "PewDiePie", - "clan_id": 4, + "clan_id": 64, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.4, "dual_mk23": 0.3, - "mp5": 0.1, - "m4": 0.7, - "m3": 0.3, + "mp5": 0.5, + "m4": 1.1, + "m3": 0.5, "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.3 + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.1, + "vest": 0.4, + "helm": 0.8, + "laser": 0.6, "silencer": 0.6, - "slippers": 0.3, + "slippers": 0.4, "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.9, - "movementStyle": 0.2, - "objectiveFocus": 0.5 + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/pindleskin.json b/bots/bots_clan/pindleskin.json new file mode 100644 index 000000000..19dfe068e --- /dev/null +++ b/bots/bots_clan/pindleskin.json @@ -0,0 +1,35 @@ +{ + "name": "Pindleskin", + "clan_id": 23, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/potaytoe.json b/bots/bots_clan/potaytoe.json deleted file mode 100644 index bf7d2e26e..000000000 --- a/bots/bots_clan/potaytoe.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Po-Tay-Toe", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.9, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.9, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/pyroyetti.json b/bots/bots_clan/pyroyetti.json deleted file mode 100644 index e92244d2f..000000000 --- a/bots/bots_clan/pyroyetti.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Pyro Yetti", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.1, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.1, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/radament.json b/bots/bots_clan/radament.json index 8f7691f86..dee5a5f51 100644 --- a/bots/bots_clan/radament.json +++ b/bots/bots_clan/radament.json @@ -1,35 +1,35 @@ { "name": "Radament", - "clan_id": 9, + "clan_id": 72, "skin": "male/indy", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.6, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.5, "m3": 0.6, "hc": 0.4, - "sniper": 0.7, - "knives": 0.1, - "grenades": 0.3 + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, + "vest": 0.2, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.6, "slippers": 0.5, - "bandolier": 0.2 + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.4, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, "aimingSkill": 0.4, "reactionTime": 0.6, "communicationFreq": 0.5, "communicationTone": 0.6, "movementStyle": 0.4, - "objectiveFocus": 0.7 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/wrack.json b/bots/bots_clan/rain.json similarity index 55% rename from bots/bots_clan/wrack.json rename to bots/bots_clan/rain.json index ee6d41464..4c26fb9cb 100644 --- a/bots/bots_clan/wrack.json +++ b/bots/bots_clan/rain.json @@ -1,34 +1,34 @@ { - "name": "Wrack", - "clan_id": 8, + "name": "Rain", + "clan_id": 91, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.3, + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.6, "knives": 0.6, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.2, + "vest": 0.3, + "helm": 0.6, "laser": 0.2, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 + "slippers": 0.9, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.4, "teamwork": 0.6, "curiosity": 0.5, "aimingSkill": 0.3, - "reactionTime": 0.4, + "reactionTime": 0.3, "communicationFreq": 0.4, - "communicationTone": 0.3, + "communicationTone": 0.4, "movementStyle": 0.4, "objectiveFocus": 0.6 } diff --git a/bots/bots_clan/rakanishu.json b/bots/bots_clan/rakanishu.json index bdb76f4ea..bab1f18c4 100644 --- a/bots/bots_clan/rakanishu.json +++ b/bots/bots_clan/rakanishu.json @@ -1,35 +1,35 @@ { "name": "Rakanishu", - "clan_id": 5, + "clan_id": 45, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, + "mk23": 0.4, "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.7, - "hc": 0.8, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, "sniper": 0.7, - "knives": 0.6, + "knives": -0.1, "grenades": 0.2 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.4, + "vest": 0.4, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.5, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": -0.0, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/ramrod.json b/bots/bots_clan/ramrod.json index cc2337185..c545b214b 100644 --- a/bots/bots_clan/ramrod.json +++ b/bots/bots_clan/ramrod.json @@ -1,35 +1,35 @@ { "name": "Ramrod", - "clan_id": 7, + "clan_id": 72, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, + "mk23": 0.7, + "dual_mk23": 0.7, "mp5": 0.2, - "m4": 0.5, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.5 + "m4": 0.4, + "m3": 0.6, + "hc": 0.9, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.1 + "vest": 0.5, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": -0.0, - "teamwork": 0.7, - "curiosity": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.6, "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.0, - "objectiveFocus": 0.7 + "reactionTime": 1.0, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/ranger.json b/bots/bots_clan/ranger.json new file mode 100644 index 000000000..0c8a26761 --- /dev/null +++ b/bots/bots_clan/ranger.json @@ -0,0 +1,35 @@ +{ + "name": "Ranger", + "clan_id": 72, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.5, + "grenades": 1.0 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ratbot.json b/bots/bots_clan/ratbot.json index 4d686a99e..907bd630a 100644 --- a/bots/bots_clan/ratbot.json +++ b/bots/bots_clan/ratbot.json @@ -1,35 +1,35 @@ { "name": "Ratbot", - "clan_id": 8, + "clan_id": 17, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.7, + "mk23": 0.4, + "dual_mk23": 0.6, "mp5": 0.6, - "m4": 0.6, - "m3": 0.7, + "m4": 0.8, + "m3": 0.5, "hc": 0.3, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.3 + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.7, + "vest": 0.7, + "helm": 0.3, + "laser": 0.3, "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.4 + "slippers": 0.7, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, + "aggressiveness": 0.1, + "teamwork": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.5, "reactionTime": 0.3, - "communicationFreq": 0.9, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.6 + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/rayquick.json b/bots/bots_clan/rayquick.json new file mode 100644 index 000000000..7725e724a --- /dev/null +++ b/bots/bots_clan/rayquick.json @@ -0,0 +1,35 @@ +{ + "name": "Ray Quick", + "clan_id": 78, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.0, + "teamwork": 0.7, + "curiosity": 1.1, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.1, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/razor.json b/bots/bots_clan/razor.json new file mode 100644 index 000000000..6128288a5 --- /dev/null +++ b/bots/bots_clan/razor.json @@ -0,0 +1,35 @@ +{ + "name": "Razor", + "clan_id": 34, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.1, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.9, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/replicant.json b/bots/bots_clan/replicant.json deleted file mode 100644 index e08bfe7de..000000000 --- a/bots/bots_clan/replicant.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Replicant", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.2, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.9, - "slippers": 0.6, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/richard.json b/bots/bots_clan/richard.json new file mode 100644 index 000000000..64eefaf02 --- /dev/null +++ b/bots/bots_clan/richard.json @@ -0,0 +1,35 @@ +{ + "name": "Richard", + "clan_id": 5, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.5, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/rocky.json b/bots/bots_clan/rocky.json new file mode 100644 index 000000000..1474eed6e --- /dev/null +++ b/bots/bots_clan/rocky.json @@ -0,0 +1,35 @@ +{ + "name": "Rocky", + "clan_id": 43, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/royen.json b/bots/bots_clan/royen.json new file mode 100644 index 000000000..6638bed7b --- /dev/null +++ b/bots/bots_clan/royen.json @@ -0,0 +1,35 @@ +{ + "name": "Royen", + "clan_id": 86, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 1.0, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/ruslan.json b/bots/bots_clan/ruslan.json deleted file mode 100644 index 4d7bd7884..000000000 --- a/bots/bots_clan/ruslan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ruslan", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.1, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sagel.json b/bots/bots_clan/sagel.json deleted file mode 100644 index 1b4122768..000000000 --- a/bots/bots_clan/sagel.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sagel", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sareena.json b/bots/bots_clan/sareena.json new file mode 100644 index 000000000..b5ff20c6b --- /dev/null +++ b/bots/bots_clan/sareena.json @@ -0,0 +1,35 @@ +{ + "name": "Sareena", + "clan_id": 72, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.2, + "m4": 0.9, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sarge.json b/bots/bots_clan/sarge.json index 7bd369712..10263e25a 100644 --- a/bots/bots_clan/sarge.json +++ b/bots/bots_clan/sarge.json @@ -1,35 +1,35 @@ { "name": "Sarge", - "clan_id": 1, + "clan_id": 32, "skin": "male/indy", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.1, - "mp5": 0.6, - "m4": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, "m3": 0.5, - "hc": 0.4, - "sniper": 1.1, - "knives": 0.4, - "grenades": 0.7 + "hc": 0.2, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.8 + "vest": 0.5, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.7, + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.2, "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.4 + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/sarina.json b/bots/bots_clan/sarina.json index df19ed862..226be0e74 100644 --- a/bots/bots_clan/sarina.json +++ b/bots/bots_clan/sarina.json @@ -1,35 +1,35 @@ { "name": "Sarina", - "clan_id": 3, + "clan_id": 75, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.1, + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.5, "m4": 0.7, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.6 + "m3": 0.7, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.8 + "vest": 0.2, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.0 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.6, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.4, "communicationFreq": 0.5, "communicationTone": 0.6, "movementStyle": 0.3, - "objectiveFocus": 0.2 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/sean.json b/bots/bots_clan/sean.json index 8685e43b9..76a558757 100644 --- a/bots/bots_clan/sean.json +++ b/bots/bots_clan/sean.json @@ -1,35 +1,35 @@ { "name": "Sean", - "clan_id": 11, + "clan_id": 109, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.6, + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.5, "m4": 0.3, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.4 + "m3": -0.1, + "hc": 0.5, + "sniper": 0.1, + "knives": 0.2, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.2, + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, "silencer": 0.4, - "slippers": 1.1, - "bandolier": 0.7 + "slippers": 0.2, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.3, "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": -0.1, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.3, "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.3 + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/seankane.json b/bots/bots_clan/seankane.json deleted file mode 100644 index 7f702e35f..000000000 --- a/bots/bots_clan/seankane.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sean Kane", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sektor.json b/bots/bots_clan/sektor.json index 852c249cb..1059207ca 100644 --- a/bots/bots_clan/sektor.json +++ b/bots/bots_clan/sektor.json @@ -1,35 +1,35 @@ { "name": "Sektor", - "clan_id": 10, + "clan_id": 24, "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 + "mp5": 0.5, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.1 + "vest": 0.4, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.5, + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.4, "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.1, - "objectiveFocus": 0.1 + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/sgtcappa.json b/bots/bots_clan/sgtcappa.json new file mode 100644 index 000000000..565b91057 --- /dev/null +++ b/bots/bots_clan/sgtcappa.json @@ -0,0 +1,35 @@ +{ + "name": "SGT CAPPA", + "clan_id": 66, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.9, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/scorpion.json b/bots/bots_clan/shaokahn.json similarity index 54% rename from bots/bots_clan/scorpion.json rename to bots/bots_clan/shaokahn.json index 2c9613612..9f9afeb0f 100644 --- a/bots/bots_clan/scorpion.json +++ b/bots/bots_clan/shaokahn.json @@ -1,35 +1,35 @@ { - "name": "Scorpion", - "clan_id": 9, + "name": "Shao Kahn", + "clan_id": 55, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.5, + "dual_mk23": 0.8, "mp5": 0.5, "m4": 0.5, - "m3": 0.4, + "m3": 0.5, "hc": 0.5, "sniper": 0.5, - "knives": 0.7, + "knives": 0.5, "grenades": 0.3 }, "itemPreferences": { "vest": 0.8, - "helm": 0.6, - "laser": 0.5, + "helm": 0.4, + "laser": 0.2, "silencer": 0.5, - "slippers": 0.3, + "slippers": 0.7, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.7, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.6, "communicationFreq": 0.4, "communicationTone": 0.3, "movementStyle": 0.5, - "objectiveFocus": 0.5 + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/sheeva.json b/bots/bots_clan/sheeva.json index 5870ba495..44cc367bb 100644 --- a/bots/bots_clan/sheeva.json +++ b/bots/bots_clan/sheeva.json @@ -1,35 +1,35 @@ { "name": "Sheeva", - "clan_id": 5, + "clan_id": 34, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, + "dual_mk23": 0.3, "mp5": 0.2, - "m4": 0.6, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.3 + "m4": 0.5, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.7, + "grenades": 0.9 }, "itemPreferences": { "vest": 0.5, - "helm": 0.3, - "laser": 0.8, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.4 + "helm": 0.5, + "laser": 0.6, + "silencer": 0.9, + "slippers": 0.1, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.3, "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.4, "communicationFreq": 0.3, "communicationTone": 0.7, "movementStyle": 0.5, - "objectiveFocus": 0.1 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/shinnok.json b/bots/bots_clan/shinnok.json deleted file mode 100644 index 81f8df222..000000000 --- a/bots/bots_clan/shinnok.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shinnok", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/shujinko.json b/bots/bots_clan/shujinko.json new file mode 100644 index 000000000..345780aa8 --- /dev/null +++ b/bots/bots_clan/shujinko.json @@ -0,0 +1,35 @@ +{ + "name": "Shujinko", + "clan_id": 64, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/si.json b/bots/bots_clan/si.json index d65c702ab..e28ccbade 100644 --- a/bots/bots_clan/si.json +++ b/bots/bots_clan/si.json @@ -1,35 +1,35 @@ { "name": "Si", - "clan_id": 9, + "clan_id": 68, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, + "mk23": 0.8, + "dual_mk23": 0.9, "mp5": 0.4, - "m4": 0.3, + "m4": 0.5, "m3": 0.3, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.8, - "grenades": 0.6 + "hc": 0.8, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.7 + "vest": 0.8, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.2, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.1, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/sigil.json b/bots/bots_clan/sigil.json new file mode 100644 index 000000000..bd885c6c8 --- /dev/null +++ b/bots/bots_clan/sigil.json @@ -0,0 +1,35 @@ +{ + "name": "Sigil", + "clan_id": 10, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sindel.json b/bots/bots_clan/sindel.json deleted file mode 100644 index 397f5d109..000000000 --- a/bots/bots_clan/sindel.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sindel", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.5, - "m3": -0.0, - "hc": 0.3, - "sniper": 0.1, - "knives": 0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.8, - "silencer": -0.0, - "slippers": 0.5, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.1, - "aimingSkill": 0.5, - "reactionTime": 0.1, - "communicationFreq": 0.1, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/skorgan.json b/bots/bots_clan/skorgan.json new file mode 100644 index 000000000..d0f335abc --- /dev/null +++ b/bots/bots_clan/skorgan.json @@ -0,0 +1,35 @@ +{ + "name": "Skorgan", + "clan_id": 47, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.1, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/slash.json b/bots/bots_clan/slash.json deleted file mode 100644 index 302a81b98..000000000 --- a/bots/bots_clan/slash.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Slash", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.1, - "m4": 0.7, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.0, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.2, - "curiosity": 0.8, - "aimingSkill": 0.9, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/slice.json b/bots/bots_clan/slice.json new file mode 100644 index 000000000..f96b6344d --- /dev/null +++ b/bots/bots_clan/slice.json @@ -0,0 +1,35 @@ +{ + "name": "Slice", + "clan_id": 17, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.1, + "dual_mk23": -0.0, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.9, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/smithy.json b/bots/bots_clan/smithy.json new file mode 100644 index 000000000..314c46de5 --- /dev/null +++ b/bots/bots_clan/smithy.json @@ -0,0 +1,35 @@ +{ + "name": "Smithy", + "clan_id": 22, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 1.0, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/smoke.json b/bots/bots_clan/smoke.json new file mode 100644 index 000000000..c3f500ea1 --- /dev/null +++ b/bots/bots_clan/smoke.json @@ -0,0 +1,35 @@ +{ + "name": "Smoke", + "clan_id": 105, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.1, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/snaps.json b/bots/bots_clan/snaps.json new file mode 100644 index 000000000..62311bba7 --- /dev/null +++ b/bots/bots_clan/snaps.json @@ -0,0 +1,35 @@ +{ + "name": "Snaps", + "clan_id": 47, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/socks.json b/bots/bots_clan/socks.json new file mode 100644 index 000000000..bcaf4c0d0 --- /dev/null +++ b/bots/bots_clan/socks.json @@ -0,0 +1,35 @@ +{ + "name": "Socks", + "clan_id": 20, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_clan/sorlag.json b/bots/bots_clan/sorlag.json index 47c78d816..1df6813dd 100644 --- a/bots/bots_clan/sorlag.json +++ b/bots/bots_clan/sorlag.json @@ -1,35 +1,35 @@ { "name": "Sorlag", - "clan_id": 1, + "clan_id": 61, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.5 + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.3 + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.5, "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.8 + "movementStyle": 0.9, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/specialed.json b/bots/bots_clan/specialed.json deleted file mode 100644 index 4d0d80c69..000000000 --- a/bots/bots_clan/specialed.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "SpecialEd", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.9, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.1, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/spocket.json b/bots/bots_clan/spocket.json deleted file mode 100644 index 7dae08ea5..000000000 --- a/bots/bots_clan/spocket.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Spocket", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.2, - "m4": 0.3, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.9, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/steampug.json b/bots/bots_clan/steampug.json deleted file mode 100644 index 0def45577..000000000 --- a/bots/bots_clan/steampug.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "SteamPug", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/stinger.json b/bots/bots_clan/stinger.json deleted file mode 100644 index 40fd5ffb4..000000000 --- a/bots/bots_clan/stinger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Stinger", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": -0.1, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/stormtree.json b/bots/bots_clan/stormtree.json deleted file mode 100644 index 379c81446..000000000 --- a/bots/bots_clan/stormtree.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Stormtree", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.1, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.1, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.0, - "movementStyle": 0.5, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/strogg.json b/bots/bots_clan/strogg.json deleted file mode 100644 index 8523a5880..000000000 --- a/bots/bots_clan/strogg.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Strogg", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.1, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/subzero.json b/bots/bots_clan/subzero.json index 03e873f44..7bda162ec 100644 --- a/bots/bots_clan/subzero.json +++ b/bots/bots_clan/subzero.json @@ -1,35 +1,35 @@ { "name": "Sub-Zero", - "clan_id": 10, + "clan_id": 110, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": -0.1, + "mk23": 0.6, + "dual_mk23": 0.3, "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.3, - "grenades": 1.1 + "m4": 0.4, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.6, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.6, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.7, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.9, "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/summoner.json b/bots/bots_clan/summoner.json index 5c22f64f7..d80ab9eb0 100644 --- a/bots/bots_clan/summoner.json +++ b/bots/bots_clan/summoner.json @@ -1,35 +1,35 @@ { "name": "Summoner", - "clan_id": 6, + "clan_id": 9, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.6, - "hc": 0.1, + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.5, "sniper": 0.6, "knives": 0.5, - "grenades": 0.4 + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.2 + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 1.1, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": -0.1, + "communicationFreq": 0.8, + "communicationTone": 0.9, "movementStyle": 0.6, - "objectiveFocus": 0.5 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/sundog.json b/bots/bots_clan/sundog.json index 02e6ca059..730e0f2fc 100644 --- a/bots/bots_clan/sundog.json +++ b/bots/bots_clan/sundog.json @@ -1,35 +1,35 @@ { "name": "Sundog", - "clan_id": 8, + "clan_id": 19, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.3, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.7, + "aggressiveness": 0.9, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.4, "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/t1000.json b/bots/bots_clan/t1000.json deleted file mode 100644 index df4b5c659..000000000 --- a/bots/bots_clan/t1000.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "T-1000", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.9, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/t800.json b/bots/bots_clan/t800.json index 74fedf8f0..d7d823fec 100644 --- a/bots/bots_clan/t800.json +++ b/bots/bots_clan/t800.json @@ -1,35 +1,35 @@ { "name": "T-800", - "clan_id": 6, + "clan_id": 69, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.1, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.5 + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.6, - "slippers": 1.0, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/t_bon.json b/bots/bots_clan/t_bon.json deleted file mode 100644 index e6cd56ee2..000000000 --- a/bots/bots_clan/t_bon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "T_bon", - "clan_id": 3, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.2, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.1, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tahlkora.json b/bots/bots_clan/tahlkora.json deleted file mode 100644 index bbcfad480..000000000 --- a/bots/bots_clan/tahlkora.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tahlkora", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": -0.1, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tank.json b/bots/bots_clan/tank.json index 72a621e8b..0699f3aa7 100644 --- a/bots/bots_clan/tank.json +++ b/bots/bots_clan/tank.json @@ -1,35 +1,35 @@ { "name": "Tank", - "clan_id": 5, + "clan_id": 11, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.8, - "knives": 0.8, - "grenades": -0.1 + "mk23": 0.9, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.4, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.2, - "laser": 0.3, - "silencer": 1.2, - "slippers": 0.5, - "bandolier": 0.2 + "vest": 0.4, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.1, + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.3, "movementStyle": 0.4, - "objectiveFocus": 0.5 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/tanya.json b/bots/bots_clan/tanya.json deleted file mode 100644 index 3c188b59f..000000000 --- a/bots/bots_clan/tanya.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tanya", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 1.0, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tarnen.json b/bots/bots_clan/tarnen.json new file mode 100644 index 000000000..ab8d45dc5 --- /dev/null +++ b/bots/bots_clan/tarnen.json @@ -0,0 +1,35 @@ +{ + "name": "Tarnen", + "clan_id": 109, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.1, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.4, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": -0.1, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_clan/tarnok.json b/bots/bots_clan/tarnok.json new file mode 100644 index 000000000..a2f992604 --- /dev/null +++ b/bots/bots_clan/tarnok.json @@ -0,0 +1,35 @@ +{ + "name": "Tarnok", + "clan_id": 49, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/taven.json b/bots/bots_clan/taven.json index 27b101011..b6ff40caf 100644 --- a/bots/bots_clan/taven.json +++ b/bots/bots_clan/taven.json @@ -1,35 +1,35 @@ { "name": "Taven", - "clan_id": 11, + "clan_id": 71, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.6, - "hc": 0.3, + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 1.0, + "hc": 0.9, "sniper": 0.5, - "knives": 0.4, - "grenades": 0.4 + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.7, + "vest": 0.4, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.1, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.6, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.7, "reactionTime": 0.6, - "communicationFreq": 0.7, + "communicationFreq": 0.4, "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/teabag.json b/bots/bots_clan/teabag.json index ad652434b..672cee63a 100644 --- a/bots/bots_clan/teabag.json +++ b/bots/bots_clan/teabag.json @@ -1,35 +1,35 @@ { "name": "Teabag", - "clan_id": 6, + "clan_id": 33, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.2, "m3": 0.3, - "hc": 0.0, - "sniper": 0.4, - "knives": 0.5, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.3, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.6, + "vest": 0.4, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.4, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": -0.1, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/terminator.json b/bots/bots_clan/terminator.json new file mode 100644 index 000000000..333e3539f --- /dev/null +++ b/bots/bots_clan/terminator.json @@ -0,0 +1,35 @@ +{ + "name": "Terminator", + "clan_id": 45, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_clan/thalath.json b/bots/bots_clan/thalath.json new file mode 100644 index 000000000..32228ef78 --- /dev/null +++ b/bots/bots_clan/thalath.json @@ -0,0 +1,35 @@ +{ + "name": "Thalath", + "clan_id": 43, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.0, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/theboss.json b/bots/bots_clan/theboss.json index 8ff50abd2..53d14a5b2 100644 --- a/bots/bots_clan/theboss.json +++ b/bots/bots_clan/theboss.json @@ -1,35 +1,35 @@ { "name": "The Boss", - "clan_id": 9, + "clan_id": 90, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.4 + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.7, + "hc": -0.1, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.1, - "reactionTime": 0.2, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.1, - "objectiveFocus": 0.3 + "aggressiveness": 0.8, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/thetulip.json b/bots/bots_clan/thetulip.json new file mode 100644 index 000000000..ccf227079 --- /dev/null +++ b/bots/bots_clan/thetulip.json @@ -0,0 +1,35 @@ +{ + "name": "The Tulip", + "clan_id": 107, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.9, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.9, + "objectiveFocus": 0.0 + } +} \ No newline at end of file diff --git a/bots/bots_clan/thor.json b/bots/bots_clan/thor.json deleted file mode 100644 index f4fb905df..000000000 --- a/bots/bots_clan/thor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Thor", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.0, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/threash.json b/bots/bots_clan/threash.json index 17054a337..339177c62 100644 --- a/bots/bots_clan/threash.json +++ b/bots/bots_clan/threash.json @@ -1,35 +1,35 @@ { "name": "Threash", - "clan_id": 6, + "clan_id": 80, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.7, - "m3": 0.8, - "hc": 0.2, - "sniper": 0.4, + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.3, "knives": 0.5, - "grenades": 0.7 + "grenades": 0.8 }, "itemPreferences": { "vest": 0.7, "helm": 0.5, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.2 + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/tommix.json b/bots/bots_clan/tommix.json deleted file mode 100644 index 466277de6..000000000 --- a/bots/bots_clan/tommix.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tom Mix", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.2, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.3, - "curiosity": 0.1, - "aimingSkill": 0.8, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tomsteele.json b/bots/bots_clan/tomsteele.json index d10496374..3f2c24917 100644 --- a/bots/bots_clan/tomsteele.json +++ b/bots/bots_clan/tomsteele.json @@ -1,35 +1,35 @@ { "name": "Tom Steele", - "clan_id": 4, + "clan_id": 26, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.4, + "mk23": 1.0, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.7, "m3": 0.6, - "hc": 0.7, - "sniper": 0.9, - "knives": 0.5, - "grenades": 0.4 + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 1.0, - "objectiveFocus": 0.4 + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.2, + "movementStyle": 0.2, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/toorc.json b/bots/bots_clan/toorc.json deleted file mode 100644 index 27bb8b0cc..000000000 --- a/bots/bots_clan/toorc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Toorc", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.9, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.7, - "grenades": -0.0 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tormentor.json b/bots/bots_clan/tormentor.json new file mode 100644 index 000000000..c257fc5c4 --- /dev/null +++ b/bots/bots_clan/tormentor.json @@ -0,0 +1,35 @@ +{ + "name": "Tormentor", + "clan_id": 67, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/treehead.json b/bots/bots_clan/treehead.json deleted file mode 100644 index b0ce3a08d..000000000 --- a/bots/bots_clan/treehead.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Treehead", - "clan_id": 6, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tremor.json b/bots/bots_clan/tremor.json index 0ade88525..ec0e95c28 100644 --- a/bots/bots_clan/tremor.json +++ b/bots/bots_clan/tremor.json @@ -1,35 +1,35 @@ { "name": "Tremor", - "clan_id": 4, + "clan_id": 33, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.3, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.6, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.4 + "helm": 0.5, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.3, - "aimingSkill": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.3, "reactionTime": 0.4, "communicationFreq": 0.2, - "communicationTone": 0.1, - "movementStyle": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.9, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/trench.json b/bots/bots_clan/trench.json index 072de464e..d4cd278f2 100644 --- a/bots/bots_clan/trench.json +++ b/bots/bots_clan/trench.json @@ -1,35 +1,35 @@ { "name": "Trench", - "clan_id": 10, + "clan_id": 64, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.8 + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.8 + "helm": 0.7, + "laser": 0.9, + "silencer": 0.9, + "slippers": 0.7, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.6, "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.7 + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/turkish.json b/bots/bots_clan/turkish.json new file mode 100644 index 000000000..8d5a7893a --- /dev/null +++ b/bots/bots_clan/turkish.json @@ -0,0 +1,35 @@ +{ + "name": "Turkish", + "clan_id": 74, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.9, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/uriel.json b/bots/bots_clan/uriel.json index ce8b80ec5..8367aed73 100644 --- a/bots/bots_clan/uriel.json +++ b/bots/bots_clan/uriel.json @@ -1,35 +1,35 @@ { "name": "Uriel", - "clan_id": 10, + "clan_id": 41, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.9, - "m3": 0.4, - "hc": 0.6, - "sniper": -0.0, - "knives": 0.8, + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.2, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.0, - "laser": 0.6, - "silencer": 0.3, - "slippers": 1.0, - "bandolier": 0.4 + "vest": -0.1, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 1.0, - "curiosity": 0.6, - "aimingSkill": 1.0, + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.3, "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.0 } } \ No newline at end of file diff --git a/bots/bots_clan/uzaytron.json b/bots/bots_clan/uzaytron.json index 2871a37c7..ae0925a88 100644 --- a/bots/bots_clan/uzaytron.json +++ b/bots/bots_clan/uzaytron.json @@ -3,33 +3,33 @@ "clan_id": 10, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.5, - "m3": -0.0, - "hc": 0.2, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.3 + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.7, - "silencer": 1.0, - "slippers": 0.3, + "vest": -0.0, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.5, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, + "aggressiveness": 0.4, + "teamwork": 0.3, "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.3, "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "communicationTone": 0.9, + "movementStyle": 0.6, + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_clan/vedro.json b/bots/bots_clan/vedro.json index 1e4e850f0..5ff89242e 100644 --- a/bots/bots_clan/vedro.json +++ b/bots/bots_clan/vedro.json @@ -1,35 +1,35 @@ { "name": "Vedro", - "clan_id": 9, + "clan_id": 68, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.0, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.5, + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.7, "hc": 0.4, - "sniper": 0.2, - "knives": 0.2, - "grenades": 0.7 + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.4, - "laser": 0.0, - "silencer": 1.1, + "vest": 0.9, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, "slippers": 0.5, - "bandolier": 0.7 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.8, + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.4, "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/venz.json b/bots/bots_clan/venz.json new file mode 100644 index 000000000..4aa29f682 --- /dev/null +++ b/bots/bots_clan/venz.json @@ -0,0 +1,35 @@ +{ + "name": "Venz", + "clan_id": 44, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 1.0, + "m4": 0.6, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.9, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.1, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_clan/vera.json b/bots/bots_clan/vera.json index 2e90292aa..feae567fb 100644 --- a/bots/bots_clan/vera.json +++ b/bots/bots_clan/vera.json @@ -1,35 +1,35 @@ { "name": "Vera", - "clan_id": 4, + "clan_id": 50, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.4, "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.8, - "hc": 0.2, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.7 + "mp5": 0.2, + "m4": 0.8, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.0, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.9, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/visor.json b/bots/bots_clan/visor.json deleted file mode 100644 index 87d6ab78b..000000000 --- a/bots/bots_clan/visor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Visor", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.3, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.9 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": -0.0, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/walter.json b/bots/bots_clan/walter.json deleted file mode 100644 index 126580fb8..000000000 --- a/bots/bots_clan/walter.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Walter", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.2, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.1, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/warchild.json b/bots/bots_clan/warchild.json deleted file mode 100644 index 50b38e6b5..000000000 --- a/bots/bots_clan/warchild.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Warchild", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.0, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.6, - "slippers": 1.0, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 1.1, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/waxman.json b/bots/bots_clan/waxman.json new file mode 100644 index 000000000..864c023cc --- /dev/null +++ b/bots/bots_clan/waxman.json @@ -0,0 +1,35 @@ +{ + "name": "Waxman", + "clan_id": 51, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.4, + "hc": 0.3, + "sniper": 1.1, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_clan/wayofthe.json b/bots/bots_clan/wayofthe.json new file mode 100644 index 000000000..57a876f30 --- /dev/null +++ b/bots/bots_clan/wayofthe.json @@ -0,0 +1,35 @@ +{ + "name": "Way Of The", + "clan_id": 13, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_clan/wens.json b/bots/bots_clan/wens.json deleted file mode 100644 index 599b9f0b8..000000000 --- a/bots/bots_clan/wens.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Wens", - "clan_id": 1, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.4, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.2, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 1.0, - "communicationTone": 0.6, - "movementStyle": 0.9, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/willitz.json b/bots/bots_clan/willitz.json deleted file mode 100644 index ccc1751f5..000000000 --- a/bots/bots_clan/willitz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Willitz**", - "clan_id": 8, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.9, - "mp5": 0.1, - "m4": 0.7, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.6, - "laser": 0.9, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/xaero.json b/bots/bots_clan/xaero.json deleted file mode 100644 index 99ccd4771..000000000 --- a/bots/bots_clan/xaero.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Xaero", - "clan_id": 7, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.4, - "hc": 0.7, - "sniper": -0.3, - "knives": 0.4, - "grenades": -0.0 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.8, - "slippers": 0.9, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/yarbro.json b/bots/bots_clan/yarbro.json new file mode 100644 index 000000000..7c31e57f4 --- /dev/null +++ b/bots/bots_clan/yarbro.json @@ -0,0 +1,35 @@ +{ + "name": "Yarbro", + "clan_id": 72, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_clan/zarna.json b/bots/bots_clan/zarna.json deleted file mode 100644 index c233b3f7c..000000000 --- a/bots/bots_clan/zarna.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Zarna", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.9, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/2dcitizen.json b/bots/bots_pub/2dcitizen.json new file mode 100644 index 000000000..77b31d6bf --- /dev/null +++ b/bots/bots_pub/2dcitizen.json @@ -0,0 +1,35 @@ +{ + "name": "2DCitizen", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": -0.0, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/_gholem_.json b/bots/bots_pub/_gholem_.json new file mode 100644 index 000000000..b8bc0abb2 --- /dev/null +++ b/bots/bots_pub/_gholem_.json @@ -0,0 +1,35 @@ +{ + "name": "_Gholem_", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.0, + "mp5": 0.5, + "m4": 0.9, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 1.0, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.0, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/_glich.json b/bots/bots_pub/_glich.json new file mode 100644 index 000000000..0aca0218f --- /dev/null +++ b/bots/bots_pub/_glich.json @@ -0,0 +1,35 @@ +{ + "name": "_GLiCH", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 1.0, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.3, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.1, + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/_nixxy_.json b/bots/bots_pub/_nixxy_.json index 960db85fd..8fdf2af18 100644 --- a/bots/bots_pub/_nixxy_.json +++ b/bots/bots_pub/_nixxy_.json @@ -1,35 +1,35 @@ { "name": "_*NiXXy*_", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": -0.0, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.1, + "m4": 0.3, "m3": 0.4, - "hc": 0.4, - "sniper": 0.7, + "hc": 0.3, + "sniper": 0.2, "knives": 0.6, - "grenades": 0.8 + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.6, + "vest": 0.6, + "helm": 0.8, + "laser": 0.4, "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.6 + "slippers": 0.6, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.6, "teamwork": 0.2, "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.3, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/aimless.json b/bots/bots_pub/aimless.json deleted file mode 100644 index 9cddbd222..000000000 --- a/bots/bots_pub/aimless.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Aimless", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.1, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/amadi.json b/bots/bots_pub/amadi.json new file mode 100644 index 000000000..8371d3bdb --- /dev/null +++ b/bots/bots_pub/amadi.json @@ -0,0 +1,35 @@ +{ + "name": "Amadi", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.1, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.9, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/amu.json b/bots/bots_pub/amu.json new file mode 100644 index 000000000..2f3400885 --- /dev/null +++ b/bots/bots_pub/amu.json @@ -0,0 +1,35 @@ +{ + "name": "Amu", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.1, + "movementStyle": 0.8, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/anakinskywalker.json b/bots/bots_pub/anakinskywalker.json deleted file mode 100644 index b4b7aad21..000000000 --- a/bots/bots_pub/anakinskywalker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Anakin Skywalker", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.9, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/anarki.json b/bots/bots_pub/anarki.json index b82882122..af75fe942 100644 --- a/bots/bots_pub/anarki.json +++ b/bots/bots_pub/anarki.json @@ -1,35 +1,35 @@ { "name": "Anarki", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, + "mk23": 0.7, + "dual_mk23": 0.7, "mp5": 0.4, - "m4": 0.3, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.9, - "grenades": 0.3 + "m4": 0.4, + "m3": 0.9, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.1, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.6 + "vest": 0.2, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.1, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.4 + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/andromeed.json b/bots/bots_pub/andromeed.json index 550912232..93e62e3bb 100644 --- a/bots/bots_pub/andromeed.json +++ b/bots/bots_pub/andromeed.json @@ -1,35 +1,35 @@ { "name": "Andromeed", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.2, - "m3": 0.7, - "hc": 0.7, + "mk23": 0.5, + "dual_mk23": -0.0, + "mp5": 0.4, + "m4": 0.9, + "m3": 0.3, + "hc": 0.3, "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.1, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/angel.json b/bots/bots_pub/angel.json deleted file mode 100644 index 9833e55e2..000000000 --- a/bots/bots_pub/angel.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Angel", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.9, - "knives": 0.7, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": -0.2, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.0, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/angeloprovolone.json b/bots/bots_pub/angeloprovolone.json deleted file mode 100644 index 479426c64..000000000 --- a/bots/bots_pub/angeloprovolone.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Angelo Provolone", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.8, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.1, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ant__dog.json b/bots/bots_pub/ant__dog.json new file mode 100644 index 000000000..fc05a3b9f --- /dev/null +++ b/bots/bots_pub/ant__dog.json @@ -0,0 +1,35 @@ +{ + "name": "Ant__Dog", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/anton.json b/bots/bots_pub/anton.json deleted file mode 100644 index be84e6b7e..000000000 --- a/bots/bots_pub/anton.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Anton", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.1, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.0, - "curiosity": 0.6, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 1.0, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/argo.json b/bots/bots_pub/argo.json index fd27b1fa5..dddcb2cd1 100644 --- a/bots/bots_pub/argo.json +++ b/bots/bots_pub/argo.json @@ -1,35 +1,35 @@ { "name": "Argo", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.6 + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.7, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.3 + "helm": 0.8, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.2, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/arklon.json b/bots/bots_pub/arklon.json index 2cc0ed855..ec04d9193 100644 --- a/bots/bots_pub/arklon.json +++ b/bots/bots_pub/arklon.json @@ -1,35 +1,35 @@ { "name": "Arklon", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.9, - "knives": 0.6, - "grenades": 0.1 + "mk23": 1.0, + "dual_mk23": 1.0, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.1, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.1, - "bandolier": 0.2 + "vest": 0.2, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 1.0, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.8 + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/ashrah.json b/bots/bots_pub/ashrah.json new file mode 100644 index 000000000..b2abc8029 --- /dev/null +++ b/bots/bots_pub/ashrah.json @@ -0,0 +1,35 @@ +{ + "name": "Ashrah", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.8, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/backfire.json b/bots/bots_pub/backfire.json index 65cb0673b..ddcc0f102 100644 --- a/bots/bots_pub/backfire.json +++ b/bots/bots_pub/backfire.json @@ -1,35 +1,35 @@ { "name": "Backfire", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.1, "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.7 + "sniper": 0.2, + "knives": 0.1, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.5, + "vest": 0.3, + "helm": 0.2, + "laser": 0.6, "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.4 + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, + "aggressiveness": 0.4, + "teamwork": -0.1, "curiosity": 0.6, "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.7 + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/bacon.json b/bots/bots_pub/bacon.json index 4d0cb2ef2..b06fc7d67 100644 --- a/bots/bots_pub/bacon.json +++ b/bots/bots_pub/bacon.json @@ -1,35 +1,35 @@ { "name": "Bacon", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.2, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.8, + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.8, + "m3": 0.4, "hc": 0.6, - "sniper": 0.7, - "knives": 0.9, + "sniper": 0.5, + "knives": 0.5, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.3, + "vest": 0.5, + "helm": 0.1, + "laser": 0.4, + "silencer": 0.8, + "slippers": 0.7, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, + "aggressiveness": 0.7, + "teamwork": 0.1, "curiosity": 0.7, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.9, - "objectiveFocus": 0.8 + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/badsam.json b/bots/bots_pub/badsam.json index c7d69c023..a526afff5 100644 --- a/bots/bots_pub/badsam.json +++ b/bots/bots_pub/badsam.json @@ -1,35 +1,35 @@ { "name": "Bad Sam", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.5, + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.7, "m4": 0.5, - "m3": 0.5, - "hc": 0.2, - "sniper": 1.0, - "knives": 0.2, - "grenades": 0.3 + "m3": 0.2, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.5, + "grenades": 1.1 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.6, "helm": 0.5, "laser": 0.7, - "silencer": 0.1, - "slippers": 0.8, - "bandolier": 0.4 + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.2, "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/baraka.json b/bots/bots_pub/baraka.json new file mode 100644 index 000000000..a86032969 --- /dev/null +++ b/bots/bots_pub/baraka.json @@ -0,0 +1,35 @@ +{ + "name": "Baraka", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.9, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/barbarian.json b/bots/bots_pub/barbarian.json index b0d1b6fbd..84dc8629e 100644 --- a/bots/bots_pub/barbarian.json +++ b/bots/bots_pub/barbarian.json @@ -1,35 +1,35 @@ { "name": "Barbarian", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.7, - "hc": 0.4, + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.5, + "hc": 0.8, "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.8, + "vest": 0.8, + "helm": 0.4, + "laser": 0.1, + "silencer": 0.8, + "slippers": 0.7, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.6 + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/bean.json b/bots/bots_pub/bean.json new file mode 100644 index 000000000..f0f1d4706 --- /dev/null +++ b/bots/bots_pub/bean.json @@ -0,0 +1,35 @@ +{ + "name": "Bean", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.0, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/beelz.json b/bots/bots_pub/beelz.json index ebe22020b..f0d744ac5 100644 --- a/bots/bots_pub/beelz.json +++ b/bots/bots_pub/beelz.json @@ -1,34 +1,34 @@ { "name": "Beelz", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 + "mp5": 0.1, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 1.0, - "teamwork": 0.2, - "curiosity": 0.6, - "aimingSkill": 0.1, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.4, + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.3, "movementStyle": 0.6, "objectiveFocus": 0.7 } diff --git a/bots/bots_pub/belokk.json b/bots/bots_pub/belokk.json new file mode 100644 index 000000000..1936d2917 --- /dev/null +++ b/bots/bots_pub/belokk.json @@ -0,0 +1,35 @@ +{ + "name": "Belokk", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": -0.0, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/benarcher.json b/bots/bots_pub/benarcher.json new file mode 100644 index 000000000..a6734f533 --- /dev/null +++ b/bots/bots_pub/benarcher.json @@ -0,0 +1,35 @@ +{ + "name": "Ben Archer", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.9, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/berserker.json b/bots/bots_pub/berserker.json new file mode 100644 index 000000000..bb0d4e0da --- /dev/null +++ b/bots/bots_pub/berserker.json @@ -0,0 +1,35 @@ +{ + "name": "Berserker", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.2, + "laser": 0.1, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bigmack.json b/bots/bots_pub/bigmack.json index 796ef5be9..e5cea6e34 100644 --- a/bots/bots_pub/bigmack.json +++ b/bots/bots_pub/bigmack.json @@ -1,35 +1,35 @@ { "name": "BigMack", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.2 + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.9, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.5, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 1.0, "reactionTime": 0.6, - "communicationFreq": 0.3, + "communicationFreq": 0.8, "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "movementStyle": 0.2, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/biker.json b/bots/bots_pub/biker.json index c64672c8c..5735c3ef6 100644 --- a/bots/bots_pub/biker.json +++ b/bots/bots_pub/biker.json @@ -1,35 +1,35 @@ { "name": "Biker", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.5, + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, "knives": 0.6, - "grenades": 0.6 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 1.0, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_pub/bishibosh.json b/bots/bots_pub/bishibosh.json index 6a18db0fc..6edbb03f8 100644 --- a/bots/bots_pub/bishibosh.json +++ b/bots/bots_pub/bishibosh.json @@ -1,35 +1,35 @@ { "name": "Bishibosh", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.5, + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.8, "m3": 0.4, "hc": 0.5, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.6 + "sniper": 0.2, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": -0.1, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.7, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.6, "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.9, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.8 + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/blackadder.json b/bots/bots_pub/blackadder.json new file mode 100644 index 000000000..931e92ec1 --- /dev/null +++ b/bots/bots_pub/blackadder.json @@ -0,0 +1,35 @@ +{ + "name": "Blackadder", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.9, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 1.0, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/blaze.json b/bots/bots_pub/blaze.json index 79d312b88..592fff7fd 100644 --- a/bots/bots_pub/blaze.json +++ b/bots/bots_pub/blaze.json @@ -1,35 +1,35 @@ { "name": "Blaze", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.4, - "hc": 0.1, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.6 + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.2 + "vest": 0.5, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.3, + "teamwork": 0.7, + "curiosity": 0.6, "aimingSkill": 0.5, - "reactionTime": 0.0, - "communicationFreq": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.7, "communicationTone": 0.4, - "movementStyle": 0.7, + "movementStyle": 0.3, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/bloodwitch.json b/bots/bots_pub/bloodwitch.json deleted file mode 100644 index 0b4349429..000000000 --- a/bots/bots_pub/bloodwitch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bloodwitch", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.9, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/bolund.json b/bots/bots_pub/bolund.json index bfe126a61..82da7569e 100644 --- a/bots/bots_pub/bolund.json +++ b/bots/bots_pub/bolund.json @@ -1,35 +1,35 @@ { "name": "Bolund", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.2, "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.0, - "m3": 0.3, - "hc": 0.9, - "sniper": 0.7, - "knives": 0.5, + "mp5": 0.2, + "m4": 0.2, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.1, + "knives": 0.4, "grenades": 0.1 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.1, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.5, + "vest": 0.8, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.4, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 1.0, - "objectiveFocus": 0.2 + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.1, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.2, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/boneash.json b/bots/bots_pub/boneash.json index b3ec44e1f..7373e657d 100644 --- a/bots/bots_pub/boneash.json +++ b/bots/bots_pub/boneash.json @@ -1,35 +1,35 @@ { "name": "Boneash", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.7, + "dual_mk23": 0.3, + "mp5": 0.8, "m4": 0.6, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.2 + "m3": 0.5, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.5, + "vest": 0.6, + "helm": 0.9, + "laser": 0.2, + "silencer": 0.4, "slippers": 0.7, - "bandolier": 0.5 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.0, - "teamwork": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.4, "curiosity": 0.5, - "aimingSkill": 0.0, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.5, - "objectiveFocus": 0.2 + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/bonebreak.json b/bots/bots_pub/bonebreak.json index a13476819..ebe750646 100644 --- a/bots/bots_pub/bonebreak.json +++ b/bots/bots_pub/bonebreak.json @@ -1,35 +1,35 @@ { "name": "Bonebreak", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.3 + "mk23": 0.1, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": -0.0, + "sniper": 0.0, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.6, "helm": 0.5, - "laser": 0.4, - "silencer": 0.7, + "laser": 0.3, + "silencer": 0.2, "slippers": 0.6, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.3, + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.5, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/booker.json b/bots/bots_pub/booker.json new file mode 100644 index 000000000..b3820edf7 --- /dev/null +++ b/bots/bots_pub/booker.json @@ -0,0 +1,35 @@ +{ + "name": "Booker", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.1, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.9, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/bortack.json b/bots/bots_pub/bortack.json index 5bdf87799..2693831e8 100644 --- a/bots/bots_pub/bortack.json +++ b/bots/bots_pub/bortack.json @@ -1,35 +1,35 @@ { "name": "Bortack", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.3, - "m4": 0.3, + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.7, "m3": 0.5, - "hc": 0.2, + "hc": 0.4, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.9, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.9, + "aggressiveness": 0.2, "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.1, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/bremm.json b/bots/bots_pub/bremm.json index c533fb3d0..47ae3624c 100644 --- a/bots/bots_pub/bremm.json +++ b/bots/bots_pub/bremm.json @@ -1,35 +1,35 @@ { "name": "Bremm", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.8, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.4, "hc": 0.3, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.4 + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.3 + "vest": 0.8, + "helm": 0.9, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.3, "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/brixos.json b/bots/bots_pub/brixos.json index 7ca667cb3..4bbedbf09 100644 --- a/bots/bots_pub/brixos.json +++ b/bots/bots_pub/brixos.json @@ -1,35 +1,35 @@ { "name": "Brixos", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.6, + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, "silencer": 0.3, - "slippers": 0.9, - "bandolier": 0.3 + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.4, + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.7, "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/brofist.json b/bots/bots_pub/brofist.json index 0e934d4f8..561a2daf6 100644 --- a/bots/bots_pub/brofist.json +++ b/bots/bots_pub/brofist.json @@ -1,35 +1,35 @@ { "name": "BroFist", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.0, - "dual_mk23": 0.9, + "mk23": 0.1, + "dual_mk23": 0.7, "mp5": 0.4, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, + "m4": 0.2, + "m3": 0.5, + "hc": 0.3, "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.7, - "helm": 0.8, - "laser": 0.3, + "helm": 0.5, + "laser": 0.8, "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.4 + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.9 + "aggressiveness": 1.1, + "teamwork": 0.8, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/brohn.json b/bots/bots_pub/brohn.json index 0f69e64d8..50c72a920 100644 --- a/bots/bots_pub/brohn.json +++ b/bots/bots_pub/brohn.json @@ -1,35 +1,35 @@ { "name": "Brohn", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, + "mk23": 0.6, + "dual_mk23": 1.1, "mp5": 0.5, - "m4": 0.7, - "m3": 0.8, - "hc": 0.3, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, "sniper": 0.6, - "knives": 0.4, - "grenades": 0.7 + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.7 + "vest": 0.3, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.4, "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.6, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/broz.json b/bots/bots_pub/broz.json deleted file mode 100644 index ea33683f8..000000000 --- a/bots/bots_pub/broz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Broz", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.1, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/bve.json b/bots/bots_pub/bve.json new file mode 100644 index 000000000..82078bddb --- /dev/null +++ b/bots/bots_pub/bve.json @@ -0,0 +1,35 @@ +{ + "name": "BVe", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": -0.1, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.8, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.9, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.0, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.1, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cadaver.json b/bots/bots_pub/cadaver.json index 8e00825f7..2136bf263 100644 --- a/bots/bots_pub/cadaver.json +++ b/bots/bots_pub/cadaver.json @@ -1,35 +1,35 @@ { "name": "Cadaver", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.8, "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.5, + "mp5": 0.8, + "m4": 0.4, "m3": 0.4, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, + "vest": 0.5, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.9, "slippers": 0.8, - "bandolier": 0.5 + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.7, "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.2, - "objectiveFocus": 0.2 + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/captivandanko.json b/bots/bots_pub/captivandanko.json deleted file mode 100644 index 08e9105c3..000000000 --- a/bots/bots_pub/captivandanko.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Capt. Ivan Danko", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/carpenter.json b/bots/bots_pub/carpenter.json index 7fdbd3dc4..4e705c353 100644 --- a/bots/bots_pub/carpenter.json +++ b/bots/bots_pub/carpenter.json @@ -1,35 +1,35 @@ { "name": "Carpenter", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.3, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.6, - "knives": -0.2, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.5, "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.7 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/cetrion.json b/bots/bots_pub/cetrion.json deleted file mode 100644 index f4f4f2f61..000000000 --- a/bots/bots_pub/cetrion.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cetrion", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": -0.0, - "silencer": 0.8, - "slippers": 0.1, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/chan.json b/bots/bots_pub/chan.json new file mode 100644 index 000000000..2a129a875 --- /dev/null +++ b/bots/bots_pub/chan.json @@ -0,0 +1,35 @@ +{ + "name": "Chan", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/chanceboudreaux.json b/bots/bots_pub/chanceboudreaux.json deleted file mode 100644 index 1912fc3e9..000000000 --- a/bots/bots_pub/chanceboudreaux.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Chance Boudreaux", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.7, - "hc": -0.0, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": -0.0, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": -0.0, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/chess.json b/bots/bots_pub/chess.json deleted file mode 100644 index 059bc3d2f..000000000 --- a/bots/bots_pub/chess.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Chess", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.8, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/chipp_.json b/bots/bots_pub/chipp_.json deleted file mode 100644 index 45e83768c..000000000 --- a/bots/bots_pub/chipp_.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "chipp_", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.9, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.3, - "knives": 0.2, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/choppy.json b/bots/bots_pub/choppy.json deleted file mode 100644 index 647cea79a..000000000 --- a/bots/bots_pub/choppy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "^^ChoppY^^", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.9, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.9, - "slippers": 0.5, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/church.json b/bots/bots_pub/church.json index 6373dbf2d..20699d942 100644 --- a/bots/bots_pub/church.json +++ b/bots/bots_pub/church.json @@ -1,35 +1,35 @@ { "name": "Church", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, + "mk23": 0.2, + "dual_mk23": 0.5, "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.3 + "m4": 1.1, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.6, - "slippers": 1.0, - "bandolier": 0.8 + "vest": 0.5, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.4, "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "aimingSkill": 0.8, + "reactionTime": 0.9, + "communicationFreq": -0.1, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/cledus.json b/bots/bots_pub/cledus.json index 5930991e4..e76a8f762 100644 --- a/bots/bots_pub/cledus.json +++ b/bots/bots_pub/cledus.json @@ -1,35 +1,35 @@ { "name": "Cledus", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.8, + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.7, "m3": 0.6, - "hc": 0.1, - "sniper": 0.5, - "knives": 0.6, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.4, "grenades": 0.6 }, "itemPreferences": { "vest": 0.7, - "helm": 0.5, - "laser": 0.7, + "helm": 0.2, + "laser": 0.5, "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.3 + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.3, "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/clion.json b/bots/bots_pub/clion.json new file mode 100644 index 000000000..d2373eb85 --- /dev/null +++ b/bots/bots_pub/clion.json @@ -0,0 +1,35 @@ +{ + "name": "C-Lion", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": -0.2, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cobra.json b/bots/bots_pub/cobra.json index 9c84792b8..1e5890395 100644 --- a/bots/bots_pub/cobra.json +++ b/bots/bots_pub/cobra.json @@ -1,35 +1,35 @@ { "name": "Cobra", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, + "mk23": 0.2, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.4, + "hc": 0.4, "sniper": 0.4, - "knives": 0.3, - "grenades": 0.6 + "knives": 0.5, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.7, + "vest": 0.7, + "helm": 0.6, + "laser": 0.9, "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.7 + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.0, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/coldworm.json b/bots/bots_pub/coldworm.json deleted file mode 100644 index 8f7e1eda2..000000000 --- a/bots/bots_pub/coldworm.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Coldworm", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 1.2, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 1.0, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.0 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.1, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_pub/colt.json b/bots/bots_pub/colt.json deleted file mode 100644 index 39869ec49..000000000 --- a/bots/bots_pub/colt.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Colt", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.6, - "m3": 1.1, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/corpsefire.json b/bots/bots_pub/corpsefire.json deleted file mode 100644 index 02990cc6b..000000000 --- a/bots/bots_pub/corpsefire.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Corpsefire", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.1, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/corrus.json b/bots/bots_pub/corrus.json index 199c055de..7da1f3e97 100644 --- a/bots/bots_pub/corrus.json +++ b/bots/bots_pub/corrus.json @@ -1,35 +1,35 @@ { "name": "[Corrus]", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.7, - "hc": 0.7, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.3, + "hc": 0.0, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 1.0, - "helm": -0.2, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.7, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.2 + "aggressiveness": 0.4, + "teamwork": 1.0, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.0, + "movementStyle": 0.6, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_pub/countess.json b/bots/bots_pub/countess.json index 5a99a5d77..48cba1c40 100644 --- a/bots/bots_pub/countess.json +++ b/bots/bots_pub/countess.json @@ -1,35 +1,35 @@ { "name": "Countess", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.5, + "mk23": 0.0, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, "grenades": 0.4 }, "itemPreferences": { "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.4, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, + "aggressiveness": 0.5, "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "curiosity": 0.1, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/cowchop.json b/bots/bots_pub/cowchop.json deleted file mode 100644 index 18b2bb15b..000000000 --- a/bots/bots_pub/cowchop.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "CowChop", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/cowking.json b/bots/bots_pub/cowking.json index 2ee275b20..99c287320 100644 --- a/bots/bots_pub/cowking.json +++ b/bots/bots_pub/cowking.json @@ -1,35 +1,35 @@ { "name": "Cow King", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.4, + "dual_mk23": 0.5, + "mp5": -0.0, + "m4": 0.3, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.7, "knives": 0.4, - "grenades": 0.3 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.4, + "vest": 0.9, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.7, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.9, - "objectiveFocus": 0.4 + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/crash.json b/bots/bots_pub/crash.json new file mode 100644 index 000000000..1e13a4b3e --- /dev/null +++ b/bots/bots_pub/crash.json @@ -0,0 +1,35 @@ +{ + "name": "Crash", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 1.1, + "m4": -0.0, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.1, + "helm": 0.1, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 1.0, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cruton.json b/bots/bots_pub/cruton.json deleted file mode 100644 index f7a5f7eea..000000000 --- a/bots/bots_pub/cruton.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cruton", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.0, - "helm": 0.5, - "laser": 1.0, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/cybr.json b/bots/bots_pub/cybr.json new file mode 100644 index 000000000..3e35a496d --- /dev/null +++ b/bots/bots_pub/cybr.json @@ -0,0 +1,35 @@ +{ + "name": "Cybr", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/cyrax.json b/bots/bots_pub/cyrax.json index 3c943472c..956ee28a9 100644 --- a/bots/bots_pub/cyrax.json +++ b/bots/bots_pub/cyrax.json @@ -1,35 +1,35 @@ { "name": "Cyrax", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, "m3": 0.6, "hc": 0.3, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.6 + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, + "vest": 0.2, + "helm": 0.5, "laser": 0.6, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.7 + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 1.1, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.3 + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/daegon.json b/bots/bots_pub/daegon.json index 2e55e0687..57c210436 100644 --- a/bots/bots_pub/daegon.json +++ b/bots/bots_pub/daegon.json @@ -1,35 +1,35 @@ { "name": "Daegon", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.3, "dual_mk23": 0.6, "mp5": 0.3, - "m4": 0.3, - "m3": 0.8, + "m4": 0.6, + "m3": 0.3, "hc": 0.5, "sniper": 0.5, - "knives": 0.3, + "knives": 0.7, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, + "vest": 0.9, + "helm": 0.1, + "laser": 1.0, "silencer": 0.3, - "slippers": 0.4, - "bandolier": -0.1 + "slippers": 0.8, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.9, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, "movementStyle": 0.5, - "objectiveFocus": 0.3 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/daemia.json b/bots/bots_pub/daemia.json deleted file mode 100644 index 46183ad9e..000000000 --- a/bots/bots_pub/daemia.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Daemia", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dairou.json b/bots/bots_pub/dairou.json index d815662f6..d7ea57bf7 100644 --- a/bots/bots_pub/dairou.json +++ b/bots/bots_pub/dairou.json @@ -1,35 +1,35 @@ { "name": "Dairou", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.6, + "mp5": 0.5, "m4": 0.4, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.5 + "m3": 0.4, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.7, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.8, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.6, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.5, "curiosity": 0.7, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.3, "movementStyle": 0.8, - "objectiveFocus": 0.5 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/darkkahn.json b/bots/bots_pub/darkkahn.json deleted file mode 100644 index 52546c99c..000000000 --- a/bots/bots_pub/darkkahn.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dark Kahn", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.0, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 1.0, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/darrius.json b/bots/bots_pub/darrius.json index 0e1345fa8..183cd8a93 100644 --- a/bots/bots_pub/darrius.json +++ b/bots/bots_pub/darrius.json @@ -1,35 +1,35 @@ { "name": "Darrius", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.9, - "hc": 0.2, + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.4, + "hc": 0.3, "sniper": 0.7, "knives": 0.6, - "grenades": 0.8 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, + "vest": 0.7, + "helm": 0.2, + "laser": 0.6, "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.5 + "slippers": 0.8, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.9, + "aggressiveness": 0.3, "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/death.json b/bots/bots_pub/death.json index c1cc10179..9d8ace725 100644 --- a/bots/bots_pub/death.json +++ b/bots/bots_pub/death.json @@ -1,35 +1,35 @@ { "name": "Death", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, + "mk23": 0.6, + "dual_mk23": 0.6, "mp5": 0.3, - "m4": 0.4, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.1 + "m4": 0.6, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.9, - "laser": 0.8, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.3, + "laser": 1.0, + "silencer": -0.0, + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.6, + "teamwork": 0.4, "curiosity": 0.3, - "aimingSkill": 0.8, + "aimingSkill": 0.4, "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.1, - "movementStyle": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.8, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/deusvault.json b/bots/bots_pub/deusvault.json new file mode 100644 index 000000000..a97edab3e --- /dev/null +++ b/bots/bots_pub/deusvault.json @@ -0,0 +1,35 @@ +{ + "name": "deUSvauLT", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dharmesh.json b/bots/bots_pub/dharmesh.json deleted file mode 100644 index 73bffa6f2..000000000 --- a/bots/bots_pub/dharmesh.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dharmesh", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.2, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 1.0, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/digz.json b/bots/bots_pub/digz.json new file mode 100644 index 000000000..4d3501c57 --- /dev/null +++ b/bots/bots_pub/digz.json @@ -0,0 +1,35 @@ +{ + "name": "Digz", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dinardo.json b/bots/bots_pub/dinardo.json deleted file mode 100644 index 222e3b95d..000000000 --- a/bots/bots_pub/dinardo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Di Nardo", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.2, - "hc": 0.2, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/disfordigger.json b/bots/bots_pub/disfordigger.json deleted file mode 100644 index 1ec1b1017..000000000 --- a/bots/bots_pub/disfordigger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "D is for Digger!", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.8, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/doctordeathwish.json b/bots/bots_pub/doctordeathwish.json deleted file mode 100644 index 8367e8992..000000000 --- a/bots/bots_pub/doctordeathwish.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Doctor Deathwish", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.9, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 1.0, - "movementStyle": 0.9, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dolin.json b/bots/bots_pub/dolin.json index 800be8309..15a0fe8fb 100644 --- a/bots/bots_pub/dolin.json +++ b/bots/bots_pub/dolin.json @@ -1,35 +1,35 @@ { "name": "Dolin", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, + "mk23": 0.7, + "dual_mk23": 0.3, "mp5": 0.6, - "m4": 0.5, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.5 + "m4": 0.6, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.9, "helm": 0.4, - "laser": 0.3, + "laser": 0.6, "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.5 + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.2, + "teamwork": 0.8, "curiosity": 0.5, "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/dominator.json b/bots/bots_pub/dominator.json new file mode 100644 index 000000000..4ca33d0e1 --- /dev/null +++ b/bots/bots_pub/dominator.json @@ -0,0 +1,35 @@ +{ + "name": "Dominator", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.8, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 1.0, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_pub/doom.json b/bots/bots_pub/doom.json new file mode 100644 index 000000000..b418b8b1d --- /dev/null +++ b/bots/bots_pub/doom.json @@ -0,0 +1,35 @@ +{ + "name": "Doom", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.2, + "silencer": -0.2, + "slippers": 0.2, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/drago.json b/bots/bots_pub/drago.json index ad944dcb1..d7c84f364 100644 --- a/bots/bots_pub/drago.json +++ b/bots/bots_pub/drago.json @@ -1,35 +1,35 @@ { "name": "Drago", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.7, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.3, "hc": 0.7, "sniper": 0.7, - "knives": 0.4, - "grenades": 0.8 + "knives": 0.8, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, + "vest": 0.4, + "helm": 0.2, "laser": 0.6, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.6 + "silencer": 0.7, + "slippers": 0.8, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.4, + "aggressiveness": 0.3, + "teamwork": 0.9, + "curiosity": 0.6, + "aimingSkill": 0.5, "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.6 + "communicationFreq": -0.2, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/indianajones.json b/bots/bots_pub/dragoooon.json similarity index 54% rename from bots/bots_pub/indianajones.json rename to bots/bots_pub/dragoooon.json index 43434a408..3577f6979 100644 --- a/bots/bots_pub/indianajones.json +++ b/bots/bots_pub/dragoooon.json @@ -1,35 +1,35 @@ { - "name": "Indiana Jones", + "name": "Dragoooon", "clan_id": null, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.8, + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.7, "m3": 0.4, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.8, - "slippers": 0.4, + "vest": 0.8, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.6, "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.6, + "teamwork": 0.4, "curiosity": 0.5, "aimingSkill": 0.7, "reactionTime": 0.4, "communicationFreq": 0.5, "communicationTone": 0.6, "movementStyle": 0.5, - "objectiveFocus": 0.8 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/drillbit.json b/bots/bots_pub/drillbit.json new file mode 100644 index 000000000..6970e1aba --- /dev/null +++ b/bots/bots_pub/drillbit.json @@ -0,0 +1,35 @@ +{ + "name": "Drillbit", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.4, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.9, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": -0.0, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/drjay.json b/bots/bots_pub/drjay.json new file mode 100644 index 000000000..c3496b181 --- /dev/null +++ b/bots/bots_pub/drjay.json @@ -0,0 +1,35 @@ +{ + "name": "Dr.Jay", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.0, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/dutch.json b/bots/bots_pub/dutch.json index 8264aa847..9f64bd65c 100644 --- a/bots/bots_pub/dutch.json +++ b/bots/bots_pub/dutch.json @@ -1,35 +1,35 @@ { "name": "Dutch", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, + "mk23": 0.2, + "dual_mk23": 0.9, "mp5": 0.5, - "m4": 0.1, - "m3": 0.6, + "m4": 0.5, + "m3": 0.1, "hc": 0.5, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.3 + "sniper": 0.9, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.4, - "silencer": -0.0, - "slippers": 0.3, - "bandolier": 0.4 + "vest": 0.7, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.8, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.3, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.7, "communicationFreq": 0.7, "communicationTone": 0.3, - "movementStyle": 0.5, + "movementStyle": 0.4, "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/dragoooon.json b/bots/bots_pub/dvorah.json similarity index 51% rename from bots/bots_clan/dragoooon.json rename to bots/bots_pub/dvorah.json index 50160605c..2974da968 100644 --- a/bots/bots_clan/dragoooon.json +++ b/bots/bots_pub/dvorah.json @@ -1,35 +1,35 @@ { - "name": "Dragoooon", - "clan_id": 1, + "name": "D'Vorah", + "clan_id": null, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.9, - "m4": 0.8, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.7, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, "knives": 0.7, - "grenades": 0.5 + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.5, + "vest": 0.7, "helm": 0.6, - "laser": 0.6, + "laser": 0.5, "silencer": 0.5, - "slippers": 0.9, + "slippers": 0.7, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, "aimingSkill": 0.8, "reactionTime": 0.6, "communicationFreq": 0.6, "communicationTone": 0.9, - "movementStyle": 0.7, + "movementStyle": 0.6, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/edoli.json b/bots/bots_pub/edoli.json deleted file mode 100644 index 695310d39..000000000 --- a/bots/bots_pub/edoli.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Edoli", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.6, - "m3": 0.5, - "hc": 0.4, - "sniper": -0.0, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.8, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.9, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ellanil.json b/bots/bots_pub/ellanil.json index 5f5fd1db4..49bd6770c 100644 --- a/bots/bots_pub/ellanil.json +++ b/bots/bots_pub/ellanil.json @@ -1,35 +1,35 @@ { "name": "Ellanil", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, + "mk23": 0.2, "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.7 + "mp5": 0.6, + "m4": 0.6, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.5, + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.2, - "teamwork": 0.6, + "teamwork": 0.3, "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.1, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/enforcer.json b/bots/bots_pub/enforcer.json index 3ac6529d7..ffab76cab 100644 --- a/bots/bots_pub/enforcer.json +++ b/bots/bots_pub/enforcer.json @@ -1,35 +1,35 @@ { "name": "Enforcer", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.4, + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.9, + "m3": 0.5, "hc": 0.6, "sniper": 0.2, - "knives": 0.6, - "grenades": 0.4 + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.1, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.2 + "vest": 0.4, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.1 + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/ermac.json b/bots/bots_pub/ermac.json new file mode 100644 index 000000000..e63a94b61 --- /dev/null +++ b/bots/bots_pub/ermac.json @@ -0,0 +1,35 @@ +{ + "name": "Ermac", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.5, + "hc": 0.5, + "sniper": 1.0, + "knives": -0.1, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 1.1, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/evo89.json b/bots/bots_pub/evo89.json index dce93235c..b327879be 100644 --- a/bots/bots_pub/evo89.json +++ b/bots/bots_pub/evo89.json @@ -1,35 +1,35 @@ { "name": "Evo89", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.5, - "knives": 1.0, - "grenades": 0.3 + "mk23": 1.0, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.6, "helm": 0.4, "laser": 0.4, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.1, "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_pub/ferratorr.json b/bots/bots_pub/ferratorr.json deleted file mode 100644 index 38bfd02e1..000000000 --- a/bots/bots_pub/ferratorr.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ferra/Torr", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.5, - "silencer": 1.0, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/flamespike.json b/bots/bots_pub/flamespike.json new file mode 100644 index 000000000..24d625fc9 --- /dev/null +++ b/bots/bots_pub/flamespike.json @@ -0,0 +1,35 @@ +{ + "name": "Flamespike", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.1, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/flint.json b/bots/bots_pub/flint.json new file mode 100644 index 000000000..bbad8c272 --- /dev/null +++ b/bots/bots_pub/flint.json @@ -0,0 +1,35 @@ +{ + "name": "Flint", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.7, + "hc": 0.9, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.6, + "silencer": -0.1, + "slippers": 0.1, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": -0.1, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/frankdux.json b/bots/bots_pub/frankdux.json deleted file mode 100644 index b07293c82..000000000 --- a/bots/bots_pub/frankdux.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Frank Dux", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.1, - "silencer": 0.3, - "slippers": 0.1, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/fred.json b/bots/bots_pub/fred.json deleted file mode 100644 index 2a8648e47..000000000 --- a/bots/bots_pub/fred.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Fred", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.1, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/frenchy.json b/bots/bots_pub/frenchy.json index 51bde7d77..a406601cf 100644 --- a/bots/bots_pub/frenchy.json +++ b/bots/bots_pub/frenchy.json @@ -1,35 +1,35 @@ { "name": "Frenchy", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.9, - "knives": 0.6, - "grenades": 0.3 + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.6, "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.2 + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.4, + "aggressiveness": 0.9, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.6, "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "movementStyle": 0.8, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/fujin.json b/bots/bots_pub/fujin.json deleted file mode 100644 index 0283f0152..000000000 --- a/bots/bots_pub/fujin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Fujin", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.9, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/fullmonty.json b/bots/bots_pub/fullmonty.json new file mode 100644 index 000000000..f535a7b2f --- /dev/null +++ b/bots/bots_pub/fullmonty.json @@ -0,0 +1,35 @@ +{ + "name": "Fullmonty", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/funkanik.json b/bots/bots_pub/funkanik.json new file mode 100644 index 000000000..ccba7c02e --- /dev/null +++ b/bots/bots_pub/funkanik.json @@ -0,0 +1,35 @@ +{ + "name": "FUNKANIK", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.9, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.4, + "hc": 0.1, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/fxguy.json b/bots/bots_pub/fxguy.json deleted file mode 100644 index bb396f067..000000000 --- a/bots/bots_pub/fxguy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "FXguy", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.9, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 1.0, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/gali.json b/bots/bots_pub/gali.json new file mode 100644 index 000000000..284e5a975 --- /dev/null +++ b/bots/bots_pub/gali.json @@ -0,0 +1,35 @@ +{ + "name": "Gali", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.8, + "m3": 0.0, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/galul.json b/bots/bots_pub/galul.json index 143b844f1..c3423bcaf 100644 --- a/bots/bots_pub/galul.json +++ b/bots/bots_pub/galul.json @@ -1,35 +1,35 @@ { "name": "Galul", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, + "mk23": 0.1, + "dual_mk23": 0.8, + "mp5": 0.4, "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.5 + "m3": 0.5, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, + "vest": 0.5, "helm": 0.4, - "laser": 0.3, - "silencer": 0.1, + "laser": 0.8, + "silencer": 0.3, "slippers": 0.6, - "bandolier": 0.2 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.6, + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.2, "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.4 + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/garbok.json b/bots/bots_pub/garbok.json index 78281b609..51763656f 100644 --- a/bots/bots_pub/garbok.json +++ b/bots/bots_pub/garbok.json @@ -1,35 +1,35 @@ { "name": "Garbok", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.1 + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.1, - "laser": 0.7, + "vest": 0.3, + "helm": 0.7, + "laser": 0.4, "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.3 + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 1.1, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.1, + "aggressiveness": 0.3, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.7, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/geleb.json b/bots/bots_pub/geleb.json index 9a3b62b65..ac2dfb9d6 100644 --- a/bots/bots_pub/geleb.json +++ b/bots/bots_pub/geleb.json @@ -1,35 +1,35 @@ { "name": "Geleb", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.3, + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.4, "m3": 0.5, "hc": 0.5, - "sniper": 0.1, - "knives": 0.6, - "grenades": 0.4 + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 1.0, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.8, + "vest": 0.3, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.5, "slippers": 0.7, - "bandolier": 0.4 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.5, + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.2, "communicationTone": 0.5, "movementStyle": 0.4, - "objectiveFocus": 0.1 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/george.json b/bots/bots_pub/george.json index 2615325be..487bcdc93 100644 --- a/bots/bots_pub/george.json +++ b/bots/bots_pub/george.json @@ -1,35 +1,35 @@ { "name": "George", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.5, + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, "knives": 0.5, - "grenades": 0.3 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 1.0 + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/ghostbl4de.json b/bots/bots_pub/ghostbl4de.json index 4e83b6572..bfb300111 100644 --- a/bots/bots_pub/ghostbl4de.json +++ b/bots/bots_pub/ghostbl4de.json @@ -1,35 +1,35 @@ { "name": "GhOstBl4de", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.2, "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.7, + "mp5": 0.3, + "m4": 0.5, "m3": 0.7, - "hc": 0.5, + "hc": 0.7, "sniper": 0.2, "knives": 0.5, - "grenades": 0.0 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.5, "helm": 0.6, - "laser": 0.6, - "silencer": 0.5, + "laser": 0.2, + "silencer": 0.4, "slippers": 0.6, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 1.0, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.6, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/gigavoltz.json b/bots/bots_pub/gigavoltz.json deleted file mode 100644 index 16a9d4757..000000000 --- a/bots/bots_pub/gigavoltz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gigavoltz", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 1.1 - } -} \ No newline at end of file diff --git a/bots/bots_pub/gladiator.json b/bots/bots_pub/gladiator.json new file mode 100644 index 000000000..489c3d94e --- /dev/null +++ b/bots/bots_pub/gladiator.json @@ -0,0 +1,35 @@ +{ + "name": "Gladiator", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 1.1, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.9 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.9, + "slippers": 0.3, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gogman.json b/bots/bots_pub/gogman.json new file mode 100644 index 000000000..2a27b4066 --- /dev/null +++ b/bots/bots_pub/gogman.json @@ -0,0 +1,35 @@ +{ + "name": "gogman", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.9, + "m3": 0.4, + "hc": 0.1, + "sniper": 0.7, + "knives": 0.2, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 1.0 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gordac.json b/bots/bots_pub/gordac.json deleted file mode 100644 index 307396a12..000000000 --- a/bots/bots_pub/gordac.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gordac", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.9, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.0 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/grimm.json b/bots/bots_pub/grimm.json index 6ce9860c3..4421d74b0 100644 --- a/bots/bots_pub/grimm.json +++ b/bots/bots_pub/grimm.json @@ -1,35 +1,35 @@ { "name": "Grimm", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.2 + "mk23": 0.5, + "dual_mk23": 0.9, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 0.3, - "silencer": -0.1, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.2, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": -0.1, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.3 + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/grog.json b/bots/bots_pub/grog.json index 84ef96798..d41618cd1 100644 --- a/bots/bots_pub/grog.json +++ b/bots/bots_pub/grog.json @@ -1,35 +1,35 @@ { "name": "Grog", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.7, - "hc": 0.1, - "sniper": 0.4, - "knives": 0.7, + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.3, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.0, - "laser": 0.8, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.7 + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.9, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.8 + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/grunt.json b/bots/bots_pub/grunt.json new file mode 100644 index 000000000..8da6efadb --- /dev/null +++ b/bots/bots_pub/grunt.json @@ -0,0 +1,35 @@ +{ + "name": "Grunt", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gun.json b/bots/bots_pub/gun.json new file mode 100644 index 000000000..3cf7f62d9 --- /dev/null +++ b/bots/bots_pub/gun.json @@ -0,0 +1,35 @@ +{ + "name": "GuN", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.1, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.9, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/gungho.json b/bots/bots_pub/gungho.json deleted file mode 100644 index 0485a2ac3..000000000 --- a/bots/bots_pub/gungho.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gung Ho", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/hal9000.json b/bots/bots_pub/hal9000.json index 526692e31..117e170f7 100644 --- a/bots/bots_pub/hal9000.json +++ b/bots/bots_pub/hal9000.json @@ -1,35 +1,35 @@ { "name": "HAL-9000", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.9, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.0 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.4, + "hc": 0.1, + "sniper": 0.2, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, + "vest": 0.5, + "helm": 0.6, "laser": 0.2, - "silencer": 0.5, - "slippers": 0.4, + "silencer": 0.8, + "slippers": 0.6, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.0, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.3 + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/halfbaked.json b/bots/bots_pub/halfbaked.json deleted file mode 100644 index b373a70ac..000000000 --- a/bots/bots_pub/halfbaked.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Halfbaked", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.1, - "laser": 0.3, - "silencer": 0.9, - "slippers": 0.7, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/halfwit.json b/bots/bots_pub/halfwit.json index d139949a6..fcf0bcd39 100644 --- a/bots/bots_pub/halfwit.json +++ b/bots/bots_pub/halfwit.json @@ -1,35 +1,35 @@ { "name": "Halfwit", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 1.0 + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, + "hc": 0.9, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.1, + "vest": 0.4, + "helm": 0.4, "laser": 0.5, - "silencer": 0.6, - "slippers": 0.3, + "silencer": 0.5, + "slippers": 0.7, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.3, + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.1, + "communicationFreq": 0.4, "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.3 + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/hambone.json b/bots/bots_pub/hambone.json index 96f09c739..7da0e7597 100644 --- a/bots/bots_pub/hambone.json +++ b/bots/bots_pub/hambone.json @@ -1,35 +1,35 @@ { "name": "Hambone", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.5, + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, "hc": 0.3, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.7 + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.9 }, "itemPreferences": { "vest": 0.4, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.6, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, "slippers": 0.4, - "bandolier": 0.7 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.1, - "aimingSkill": 0.4, - "reactionTime": 1.0, - "communicationFreq": 0.7, - "communicationTone": 0.9, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/havo.json b/bots/bots_pub/havo.json new file mode 100644 index 000000000..f1c7fa85f --- /dev/null +++ b/bots/bots_pub/havo.json @@ -0,0 +1,35 @@ +{ + "name": "Havo", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.8, + "m4": 0.2, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.8, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hecker.json b/bots/bots_pub/hecker.json deleted file mode 100644 index 5f1fc18ef..000000000 --- a/bots/bots_pub/hecker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hecker", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.3, - "m4": 0.9, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.1, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.1, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/heljay.json b/bots/bots_pub/heljay.json deleted file mode 100644 index bed4c2383..000000000 --- a/bots/bots_pub/heljay.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Heljay", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.0, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.0, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/hellhound.json b/bots/bots_pub/hellhound.json index 670387ff2..a3c3fc267 100644 --- a/bots/bots_pub/hellhound.json +++ b/bots/bots_pub/hellhound.json @@ -1,35 +1,35 @@ { "name": "Hellhound", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.9, - "m4": 0.3, + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.2, "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.4 + "hc": 0.3, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.8, + "vest": 0.3, + "helm": 0.5, + "laser": 0.7, "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.8 + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.6, + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.3, "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.1 + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/helmut.json b/bots/bots_pub/helmut.json new file mode 100644 index 000000000..d08428d72 --- /dev/null +++ b/bots/bots_pub/helmut.json @@ -0,0 +1,35 @@ +{ + "name": "Helmut", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.1, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/himself.json b/bots/bots_pub/himself.json index 8834c30b2..d2ead7ab3 100644 --- a/bots/bots_pub/himself.json +++ b/bots/bots_pub/himself.json @@ -1,35 +1,35 @@ { "name": "Himself", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.8, - "hc": 0.2, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.1, + "hc": 0.6, "sniper": 0.7, "knives": 0.7, - "grenades": 0.8 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.8, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.4 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/hotaru.json b/bots/bots_pub/hotaru.json new file mode 100644 index 000000000..2b9a4b886 --- /dev/null +++ b/bots/bots_pub/hotaru.json @@ -0,0 +1,35 @@ +{ + "name": "Hotaru", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.1, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/hozef8.json b/bots/bots_pub/hozef8.json index 7866d7a13..0ef55cbe4 100644 --- a/bots/bots_pub/hozef8.json +++ b/bots/bots_pub/hozef8.json @@ -1,35 +1,35 @@ { "name": "hozef-8", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.1, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.4 + "dual_mk23": 0.1, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.8, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.3, "helm": 0.3, "laser": 0.7, - "silencer": 0.1, - "slippers": 0.4, + "silencer": 0.5, + "slippers": 0.6, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.4, "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.5, - "communicationFreq": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/hunter.json b/bots/bots_pub/hunter.json new file mode 100644 index 000000000..e0868db06 --- /dev/null +++ b/bots/bots_pub/hunter.json @@ -0,0 +1,35 @@ +{ + "name": "Hunter", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.8, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/icexo.json b/bots/bots_pub/icexo.json new file mode 100644 index 000000000..cc5001e23 --- /dev/null +++ b/bots/bots_pub/icexo.json @@ -0,0 +1,35 @@ +{ + "name": "Icexo", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/imentu.json b/bots/bots_pub/imentu.json index 32cd96ba9..a7d502ac2 100644 --- a/bots/bots_pub/imentu.json +++ b/bots/bots_pub/imentu.json @@ -1,35 +1,35 @@ { "name": "Imentu", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.4, - "m3": 0.4, - "hc": 1.1, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.3 + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.5, + "vest": 0.4, + "helm": 0.0, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.7, "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.7, "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.1 + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/itixoid.json b/bots/bots_pub/itixoid.json deleted file mode 100644 index f2ef97584..000000000 --- a/bots/bots_pub/itixoid.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "itiXOID", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.8, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.1, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 1.0, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/itzchamp.json b/bots/bots_pub/itzchamp.json new file mode 100644 index 000000000..865190cb0 --- /dev/null +++ b/bots/bots_pub/itzchamp.json @@ -0,0 +1,35 @@ +{ + "name": "{itzChamp}", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.7, + "m4": 0.1, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.7, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.9, + "slippers": 0.9, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.1, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ivandrago.json b/bots/bots_pub/ivandrago.json index 87f522564..743597873 100644 --- a/bots/bots_pub/ivandrago.json +++ b/bots/bots_pub/ivandrago.json @@ -1,35 +1,35 @@ { "name": "Ivan Drago", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.1, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.8, - "knives": 0.8, - "grenades": 0.2 + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.8, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.9, + "vest": 0.5, + "helm": 0.4, + "laser": 0.6, "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.4 + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/ivankraschinsky.json b/bots/bots_pub/ivankraschinsky.json deleted file mode 100644 index 50f50594f..000000000 --- a/bots/bots_pub/ivankraschinsky.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ivan Kraschinsky", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.1, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jacksaxon.json b/bots/bots_pub/jacksaxon.json new file mode 100644 index 000000000..2ef52bd5a --- /dev/null +++ b/bots/bots_pub/jacksaxon.json @@ -0,0 +1,35 @@ +{ + "name": "Jack Saxon", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": -0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jacqueskristoff.json b/bots/bots_pub/jacqueskristoff.json deleted file mode 100644 index 20e7e043c..000000000 --- a/bots/bots_pub/jacqueskristoff.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jacques Kristoff", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.9, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jade.json b/bots/bots_pub/jade.json new file mode 100644 index 000000000..5dfd6666b --- /dev/null +++ b/bots/bots_pub/jade.json @@ -0,0 +1,35 @@ +{ + "name": "Jade", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.9 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.1, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/jarek.json b/bots/bots_pub/jarek.json deleted file mode 100644 index 60d8b76aa..000000000 --- a/bots/bots_pub/jarek.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jarek", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.8, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.8, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jax.json b/bots/bots_pub/jax.json index 1cb37b1c7..2a9e53586 100644 --- a/bots/bots_pub/jax.json +++ b/bots/bots_pub/jax.json @@ -1,35 +1,35 @@ { "name": "Jax", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.4, + "mk23": 0.7, + "dual_mk23": 0.2, + "mp5": 0.5, "m4": 0.5, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.8, - "knives": -0.0, - "grenades": 0.8 + "m3": 0.1, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.8, - "slippers": 1.0, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.2, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.5, "reactionTime": 0.5, "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/jcdenton.json b/bots/bots_pub/jcdenton.json index 993e50535..73a55a148 100644 --- a/bots/bots_pub/jcdenton.json +++ b/bots/bots_pub/jcdenton.json @@ -1,35 +1,35 @@ { "name": "JC Denton", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.6, + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.9, "m4": 0.5, - "m3": 0.8, - "hc": 0.2, + "m3": 0.6, + "hc": 0.4, "sniper": 0.3, - "knives": 0.1, + "knives": 0.8, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.7 + "vest": 0.6, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.9, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/jdgold.json b/bots/bots_pub/jdgold.json deleted file mode 100644 index e6c821a92..000000000 --- a/bots/bots_pub/jdgold.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "JD Gold", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.0, - "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jinx.json b/bots/bots_pub/jinx.json deleted file mode 100644 index cf897e2b2..000000000 --- a/bots/bots_pub/jinx.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jinx", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.6, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jjmcquade.json b/bots/bots_pub/jjmcquade.json index 623726a33..44efee10e 100644 --- a/bots/bots_pub/jjmcquade.json +++ b/bots/bots_pub/jjmcquade.json @@ -1,35 +1,35 @@ { "name": "JJ McQuade", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.4 + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.3, "helm": 0.2, - "laser": 0.4, + "laser": 0.6, "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.7 + "slippers": 0.4, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.9, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/joce.json b/bots/bots_pub/joce.json deleted file mode 100644 index 204927846..000000000 --- a/bots/bots_pub/joce.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Joce", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/joejoiner.json b/bots/bots_pub/joejoiner.json index 351eed738..45d3c5877 100644 --- a/bots/bots_pub/joejoiner.json +++ b/bots/bots_pub/joejoiner.json @@ -1,35 +1,35 @@ { "name": "Joe Joiner", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.3, "dual_mk23": 0.4, - "mp5": 0.4, + "mp5": 0.2, "m4": 0.4, "m3": 0.6, "hc": 0.6, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.5 + "sniper": 1.2, + "knives": 0.1, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.3, + "vest": 0.4, + "helm": 0.6, "laser": 0.5, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.3 + "silencer": 0.8, + "slippers": 0.9, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, + "aggressiveness": 0.2, + "teamwork": 0.6, "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/joesanto.json b/bots/bots_pub/joesanto.json new file mode 100644 index 000000000..e07ddadb8 --- /dev/null +++ b/bots/bots_pub/joesanto.json @@ -0,0 +1,35 @@ +{ + "name": "Joe Santo", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/joetanto.json b/bots/bots_pub/joetanto.json new file mode 100644 index 000000000..1a1fb5bf2 --- /dev/null +++ b/bots/bots_pub/joetanto.json @@ -0,0 +1,35 @@ +{ + "name": "Joe Tanto", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.9, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/johnrambo.json b/bots/bots_pub/johnrambo.json deleted file mode 100644 index 3164be6b0..000000000 --- a/bots/bots_pub/johnrambo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "John Rambo", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kaa.json b/bots/bots_pub/kaa.json index ddc6fb47f..23ab34933 100644 --- a/bots/bots_pub/kaa.json +++ b/bots/bots_pub/kaa.json @@ -1,35 +1,35 @@ { "name": "Kaa", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.4 + "vest": 0.3, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.5, "communicationFreq": 0.7, "communicationTone": 0.9, - "movementStyle": 0.6, + "movementStyle": 0.7, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/kabal.json b/bots/bots_pub/kabal.json index 1abdaf84e..130385fde 100644 --- a/bots/bots_pub/kabal.json +++ b/bots/bots_pub/kabal.json @@ -1,35 +1,35 @@ { "name": "Kabal", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.6, + "mk23": 0.3, + "dual_mk23": 0.3, "mp5": 0.3, - "m4": 0.6, - "m3": 0.6, - "hc": 0.5, + "m4": 0.8, + "m3": 0.7, + "hc": 0.6, "sniper": 0.4, - "knives": 0.8, - "grenades": 0.5 + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, + "aggressiveness": 0.2, + "teamwork": 0.2, "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.0, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.7, "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.6 + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/kai.json b/bots/bots_pub/kai.json deleted file mode 100644 index 96b5a61d7..000000000 --- a/bots/bots_pub/kai.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kai", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kalidor.json b/bots/bots_pub/kalidor.json new file mode 100644 index 000000000..9e73aee8b --- /dev/null +++ b/bots/bots_pub/kalidor.json @@ -0,0 +1,35 @@ +{ + "name": "Kalidor", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kano.json b/bots/bots_pub/kano.json deleted file mode 100644 index b351301af..000000000 --- a/bots/bots_pub/kano.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kano", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.1, - "laser": 0.0, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.9, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kenshi.json b/bots/bots_pub/kenshi.json index a5f41d023..adcdd50e7 100644 --- a/bots/bots_pub/kenshi.json +++ b/bots/bots_pub/kenshi.json @@ -1,35 +1,35 @@ { "name": "Kenshi", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.8, - "knives": 0.3, + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 1.0, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.9, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/khameleon.json b/bots/bots_pub/khameleon.json deleted file mode 100644 index 229351701..000000000 --- a/bots/bots_pub/khameleon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Khameleon", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.6, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kintaro.json b/bots/bots_pub/kintaro.json new file mode 100644 index 000000000..1000029df --- /dev/null +++ b/bots/bots_pub/kintaro.json @@ -0,0 +1,35 @@ +{ + "name": "Kintaro", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.9, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kippy.json b/bots/bots_pub/kippy.json deleted file mode 100644 index 98101319f..000000000 --- a/bots/bots_pub/kippy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kippy", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.9, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.1, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kira.json b/bots/bots_pub/kira.json index fee7249e6..506a14e96 100644 --- a/bots/bots_pub/kira.json +++ b/bots/bots_pub/kira.json @@ -1,35 +1,35 @@ { "name": "Kira", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.9, - "m3": 0.4, - "hc": -0.0, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.2, + "knives": 0.8, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 1.0, - "silencer": 0.3, + "vest": 0.8, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.5, "slippers": 0.5, - "bandolier": 0.2 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 1.0, + "aggressiveness": 0.7, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.2, + "aimingSkill": 0.3, + "reactionTime": 0.5, "communicationFreq": 0.3, - "communicationTone": 0.5, + "communicationTone": 0.4, "movementStyle": 0.4, - "objectiveFocus": 0.8 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/kitana.json b/bots/bots_pub/kitana.json new file mode 100644 index 000000000..e3096c4b4 --- /dev/null +++ b/bots/bots_pub/kitana.json @@ -0,0 +1,35 @@ +{ + "name": "Kitana", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.6, + "knives": 1.1, + "grenades": -0.1 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.2 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.2, + "aimingSkill": 0.1, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/kitlatura.json b/bots/bots_pub/kitlatura.json deleted file mode 100644 index abd781e6e..000000000 --- a/bots/bots_pub/kitlatura.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kit Latura", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.1, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kobra.json b/bots/bots_pub/kobra.json deleted file mode 100644 index 47fa83843..000000000 --- a/bots/bots_pub/kobra.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kobra", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kole.json b/bots/bots_pub/kole.json index f05d51455..6bf303408 100644 --- a/bots/bots_pub/kole.json +++ b/bots/bots_pub/kole.json @@ -1,35 +1,35 @@ { "name": "Kole", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.3, + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.6, "knives": 0.3, - "grenades": 0.5 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.6, + "vest": 0.2, + "helm": 0.8, + "laser": 0.3, "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 1.0, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.1, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.2 + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/havik.json b/bots/bots_pub/kollector.json similarity index 51% rename from bots/bots_pub/havik.json rename to bots/bots_pub/kollector.json index 428e0f3ad..d461744df 100644 --- a/bots/bots_pub/havik.json +++ b/bots/bots_pub/kollector.json @@ -1,34 +1,34 @@ { - "name": "Havik", + "name": "Kollector", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.8, + "mp5": 0.6, + "m4": 0.3, "m3": 0.4, "hc": 0.5, "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, + "vest": 0.5, + "helm": 0.8, "laser": 0.4, - "silencer": 0.4, + "silencer": 0.6, "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.7, "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.4, "movementStyle": 0.6, "objectiveFocus": 0.6 } diff --git a/bots/bots_pub/korv.json b/bots/bots_pub/korv.json index b308b85ed..6877cb6d0 100644 --- a/bots/bots_pub/korv.json +++ b/bots/bots_pub/korv.json @@ -1,35 +1,35 @@ { "name": "Korv", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.5, + "vest": 0.8, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.8, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, + "aggressiveness": 0.7, + "teamwork": 0.6, "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.7, "movementStyle": 0.6, - "objectiveFocus": 0.5 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/kotalkahn.json b/bots/bots_pub/kotalkahn.json index 338e522ef..c216d8721 100644 --- a/bots/bots_pub/kotalkahn.json +++ b/bots/bots_pub/kotalkahn.json @@ -1,35 +1,35 @@ { "name": "Kotal Kahn", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.1, "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, "hc": 0.3, - "sniper": 0.7, - "knives": 0.1, - "grenades": 0.4 + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, + "vest": 0.3, + "helm": 0.2, + "laser": 0.6, "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.4 + "slippers": 0.7, + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.2, - "communicationFreq": 0.2, - "communicationTone": 0.2, - "movementStyle": 0.5, + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.4, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/kungjin.json b/bots/bots_pub/kungjin.json index 09cfd7bab..2c00ac966 100644 --- a/bots/bots_pub/kungjin.json +++ b/bots/bots_pub/kungjin.json @@ -1,35 +1,35 @@ { "name": "Kung Jin", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.5 + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.9, - "silencer": 0.4, + "vest": 0.6, + "helm": 0.3, + "laser": 0.5, + "silencer": -0.0, "slippers": 0.4, - "bandolier": 0.4 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.9, "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.4, "communicationTone": 0.5, - "movementStyle": 0.9, - "objectiveFocus": 0.7 + "movementStyle": 0.4, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/berserker.json b/bots/bots_pub/kuri.json similarity index 50% rename from bots/bots_clan/berserker.json rename to bots/bots_pub/kuri.json index aef0dd62b..be2a41e2e 100644 --- a/bots/bots_clan/berserker.json +++ b/bots/bots_pub/kuri.json @@ -1,23 +1,23 @@ { - "name": "Berserker", - "clan_id": 10, + "name": "kuri", + "clan_id": null, "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.5, + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.2, + "m3": 0.3, "hc": 0.6, - "sniper": 0.5, - "knives": 0.6, + "sniper": 0.6, + "knives": 0.4, "grenades": 0.2 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.1, - "silencer": 0.4, + "vest": 0.4, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.7, "slippers": 0.1, "bandolier": 0.6 }, @@ -26,10 +26,10 @@ "teamwork": 0.4, "curiosity": 0.4, "aimingSkill": 0.6, - "reactionTime": 0.2, + "reactionTime": 0.5, "communicationFreq": 0.6, - "communicationTone": 0.9, - "movementStyle": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.7, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/lawrence.json b/bots/bots_pub/lawrence.json new file mode 100644 index 000000000..efc8b5b91 --- /dev/null +++ b/bots/bots_pub/lawrence.json @@ -0,0 +1,35 @@ +{ + "name": "Lawrence", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.0, + "hc": 0.6, + "sniper": 0.9, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.4, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/leatherarm.json b/bots/bots_pub/leatherarm.json deleted file mode 100644 index 707f4c09e..000000000 --- a/bots/bots_pub/leatherarm.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Leatherarm", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.1, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.1, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/lemmie.json b/bots/bots_pub/lemmie.json deleted file mode 100644 index 6f2f11008..000000000 --- a/bots/bots_pub/lemmie.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lemmie", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": -0.1, - "aimingSkill": 0.3, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/limei.json b/bots/bots_pub/limei.json new file mode 100644 index 000000000..43b52b6e2 --- /dev/null +++ b/bots/bots_pub/limei.json @@ -0,0 +1,35 @@ +{ + "name": "Li Mei", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.9, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/littleg.json b/bots/bots_pub/littleg.json index 06e5bd328..be708edf3 100644 --- a/bots/bots_pub/littleg.json +++ b/bots/bots_pub/littleg.json @@ -1,35 +1,35 @@ { "name": "Little G", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.3, "m3": 0.4, - "hc": 0.2, + "hc": 0.4, "sniper": 0.1, - "knives": 0.4, - "grenades": 0.4 + "knives": 0.9, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.3, + "laser": 0.9, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, + "aggressiveness": 0.7, + "teamwork": 0.6, "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.0, - "communicationFreq": 0.5, - "communicationTone": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, "movementStyle": 0.3, - "objectiveFocus": 0.9 + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/badjuju.json b/bots/bots_pub/lonewolf.json similarity index 53% rename from bots/bots_pub/badjuju.json rename to bots/bots_pub/lonewolf.json index 94ed43eac..f13aefb5e 100644 --- a/bots/bots_pub/badjuju.json +++ b/bots/bots_pub/lonewolf.json @@ -1,35 +1,35 @@ { - "name": "Bad JuJu", + "name": "Lone Wolf", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.7, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.2, "m3": 0.3, "hc": 0.7, "sniper": 0.6, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { "vest": 0.3, - "helm": 0.5, + "helm": 0.6, "laser": 0.7, - "silencer": 0.4, + "silencer": 0.9, "slippers": 0.5, - "bandolier": 0.7 + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.2, - "teamwork": 0.4, + "teamwork": 0.6, "curiosity": 0.6, "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.4, "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": -0.0 + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/ltraymondtango.json b/bots/bots_pub/ltraymondtango.json deleted file mode 100644 index 7bc9bf970..000000000 --- a/bots/bots_pub/ltraymondtango.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lt Raymond Tango", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 1.0, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.9, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.0, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/lucy.json b/bots/bots_pub/lucy.json deleted file mode 100644 index 389e37fde..000000000 --- a/bots/bots_pub/lucy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lucy", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/major.json b/bots/bots_pub/major.json deleted file mode 100644 index 84097ce15..000000000 --- a/bots/bots_pub/major.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Major", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.8, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/majorpain.json b/bots/bots_pub/majorpain.json deleted file mode 100644 index 4e70adfbf..000000000 --- a/bots/bots_pub/majorpain.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Major Pain", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.9, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": -0.0, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/makron.json b/bots/bots_pub/makron.json deleted file mode 100644 index b90082244..000000000 --- a/bots/bots_pub/makron.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Makron", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.6, - "knives": -0.1, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.9, - "slippers": 0.5, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 1.0, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mckenna.json b/bots/bots_pub/mckenna.json deleted file mode 100644 index a4413f8a3..000000000 --- a/bots/bots_pub/mckenna.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "McKenna", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/merki.json b/bots/bots_pub/merki.json deleted file mode 100644 index 756ab6244..000000000 --- a/bots/bots_pub/merki.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Merki", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.1, - "movementStyle": 0.3, - "objectiveFocus": 1.0 - } -} \ No newline at end of file diff --git a/bots/bots_pub/michael.json b/bots/bots_pub/michael.json deleted file mode 100644 index 74d74dc90..000000000 --- a/bots/bots_pub/michael.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Michael", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.7, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mileena.json b/bots/bots_pub/mileena.json deleted file mode 100644 index 23e8c0530..000000000 --- a/bots/bots_pub/mileena.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mileena", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/milius.json b/bots/bots_pub/milius.json index 5586d0e1a..cd9dc943f 100644 --- a/bots/bots_pub/milius.json +++ b/bots/bots_pub/milius.json @@ -1,35 +1,35 @@ { "name": "Milius", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.1, + "dual_mk23": 0.6, + "mp5": 0.5, "m4": 0.5, - "m3": 0.2, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.2, - "grenades": 0.6 + "m3": 0.4, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { "vest": 0.5, - "helm": 0.7, + "helm": 0.6, "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.5 + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.4, "teamwork": 0.6, - "curiosity": 1.0, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.9, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.8, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/mincer.json b/bots/bots_pub/mincer.json deleted file mode 100644 index 227203e18..000000000 --- a/bots/bots_pub/mincer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mincer", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mindelos.json b/bots/bots_pub/mindelos.json new file mode 100644 index 000000000..464979367 --- /dev/null +++ b/bots/bots_pub/mindelos.json @@ -0,0 +1,35 @@ +{ + "name": "Mindelos", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.6, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.4, + "laser": 0.1, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 1.0, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/moloch.json b/bots/bots_pub/moloch.json index cb59121a6..7c7ee9cec 100644 --- a/bots/bots_pub/moloch.json +++ b/bots/bots_pub/moloch.json @@ -1,35 +1,35 @@ { "name": "Moloch", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.9, - "m4": 0.4, + "mk23": 0.6, + "dual_mk23": 0.1, + "mp5": 0.3, + "m4": 0.3, "m3": 0.6, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.2 + "hc": 0.7, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.7 + "vest": 0.4, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.6, + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.4, "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, "movementStyle": 0.5, - "objectiveFocus": 0.2 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/mongrel.json b/bots/bots_pub/mongrel.json index 408f6242f..b96021f68 100644 --- a/bots/bots_pub/mongrel.json +++ b/bots/bots_pub/mongrel.json @@ -1,35 +1,35 @@ { "name": "Mongrel", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.8, - "mp5": 0.5, + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, "m4": 0.7, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.6 + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.3, + "helm": 0.1, + "laser": -0.1, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/monk.json b/bots/bots_pub/monk.json new file mode 100644 index 000000000..30f3429ce --- /dev/null +++ b/bots/bots_pub/monk.json @@ -0,0 +1,35 @@ +{ + "name": "Monk", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.7, + "aimingSkill": 0.9, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/monty.json b/bots/bots_pub/monty.json new file mode 100644 index 000000000..63c653dbf --- /dev/null +++ b/bots/bots_pub/monty.json @@ -0,0 +1,35 @@ +{ + "name": "Monty", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.1 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.2, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/morgan.json b/bots/bots_pub/morgan.json new file mode 100644 index 000000000..30e868b2c --- /dev/null +++ b/bots/bots_pub/morgan.json @@ -0,0 +1,35 @@ +{ + "name": "Morgan", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.9, + "slippers": 0.9, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/morgriff.json b/bots/bots_pub/morgriff.json index 5c594c6ed..658476b26 100644 --- a/bots/bots_pub/morgriff.json +++ b/bots/bots_pub/morgriff.json @@ -1,35 +1,35 @@ { "name": "Morgriff", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.3, + "dual_mk23": 0.4, "mp5": 0.7, - "m4": 0.7, - "m3": 0.8, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.3 + "m4": 0.6, + "m3": 0.5, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.7, - "silencer": 0.8, - "slippers": 0.5, + "vest": 0.4, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.4, "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.4, "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.1, + "communicationFreq": 0.4, + "communicationTone": 0.8, "movementStyle": 0.3, - "objectiveFocus": 0.6 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/motaro.json b/bots/bots_pub/motaro.json deleted file mode 100644 index 11f065538..000000000 --- a/bots/bots_pub/motaro.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Motaro", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 1.0, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mrbadger.json b/bots/bots_pub/mrbadger.json new file mode 100644 index 000000000..68a2f304a --- /dev/null +++ b/bots/bots_pub/mrbadger.json @@ -0,0 +1,35 @@ +{ + "name": "Mr.Badger", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.2, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.1, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mrsuave.json b/bots/bots_pub/mrsuave.json deleted file mode 100644 index eac051ed0..000000000 --- a/bots/bots_pub/mrsuave.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mr Suave", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.7, - "knives": 1.0, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.9, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mungri.json b/bots/bots_pub/mungri.json new file mode 100644 index 000000000..dc356fe55 --- /dev/null +++ b/bots/bots_pub/mungri.json @@ -0,0 +1,35 @@ +{ + "name": "Mungri", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.7, + "hc": 0.1, + "sniper": 0.8, + "knives": 0.4, + "grenades": 1.1 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/mynx.json b/bots/bots_pub/mynx.json new file mode 100644 index 000000000..99bd91e49 --- /dev/null +++ b/bots/bots_pub/mynx.json @@ -0,0 +1,35 @@ +{ + "name": "Mynx", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.6, + "grenades": -0.0 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.1, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.2, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nandet.json b/bots/bots_pub/nandet.json new file mode 100644 index 000000000..337d3be6f --- /dev/null +++ b/bots/bots_pub/nandet.json @@ -0,0 +1,35 @@ +{ + "name": "Nandet", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.9, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.1, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.1, + "curiosity": -0.2, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 1.0, + "communicationTone": 0.3, + "movementStyle": 0.3, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/newbie.json b/bots/bots_pub/newbie.json deleted file mode 100644 index ea170210d..000000000 --- a/bots/bots_pub/newbie.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Newbie", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.2, - "slippers": 0.3, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.2, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/nibbler.json b/bots/bots_pub/nibbler.json deleted file mode 100644 index 7dc7505db..000000000 --- a/bots/bots_pub/nibbler.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nibbler", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.8, - "knives": 0.1, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/nickgunar.json b/bots/bots_pub/nickgunar.json index 3c2d3aac7..e87ae9155 100644 --- a/bots/bots_pub/nickgunar.json +++ b/bots/bots_pub/nickgunar.json @@ -1,35 +1,35 @@ { "name": "Nick Gunar", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.8 + "mk23": 0.2, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 1.0, + "m3": 1.0, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.5, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.1, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.6, "movementStyle": 0.4, - "objectiveFocus": 0.1 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/nightwolf.json b/bots/bots_pub/nightwolf.json new file mode 100644 index 000000000..06872bca7 --- /dev/null +++ b/bots/bots_pub/nightwolf.json @@ -0,0 +1,35 @@ +{ + "name": "Nightwolf", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.0 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nihlathak.json b/bots/bots_pub/nihlathak.json deleted file mode 100644 index 513d45ee7..000000000 --- a/bots/bots_pub/nihlathak.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nihlathak", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": -0.1, - "laser": 0.5, - "silencer": 0.1, - "slippers": 0.7, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/nikolaicherenko.json b/bots/bots_pub/nikolaicherenko.json deleted file mode 100644 index 2026f63d0..000000000 --- a/bots/bots_pub/nikolaicherenko.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nikolai Cherenko", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.1, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.9 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/noammo.json b/bots/bots_pub/noammo.json index 2278e66d2..4dc8289a7 100644 --- a/bots/bots_pub/noammo.json +++ b/bots/bots_pub/noammo.json @@ -1,35 +1,35 @@ { "name": "NoAmmo", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.8, + "mk23": 0.7, + "dual_mk23": 0.9, "mp5": 0.2, - "m4": 0.7, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.5 + "m4": 0.6, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { "vest": 0.2, - "helm": 1.1, - "laser": 0.5, + "helm": 0.7, + "laser": 0.3, "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.6 + "slippers": 0.7, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.1, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.9, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/nohope.json b/bots/bots_pub/nohope.json deleted file mode 100644 index b024d6af0..000000000 --- a/bots/bots_pub/nohope.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nohope", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/noogie.json b/bots/bots_pub/noogie.json new file mode 100644 index 000000000..c430e3039 --- /dev/null +++ b/bots/bots_pub/noogie.json @@ -0,0 +1,35 @@ +{ + "name": "Noogie", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.8, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/nundak.json b/bots/bots_pub/nundak.json index 9a5384066..84794effd 100644 --- a/bots/bots_pub/nundak.json +++ b/bots/bots_pub/nundak.json @@ -1,35 +1,35 @@ { "name": "Nundak", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.3 + "mk23": 0.1, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.5, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.6, + "vest": 0.8, + "helm": 0.6, + "laser": 0.3, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.9 + "teamwork": 0.7, + "curiosity": 0.7, + "aimingSkill": 1.0, + "reactionTime": 0.0, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/onaga.json b/bots/bots_pub/onaga.json deleted file mode 100644 index c7ab8c38e..000000000 --- a/bots/bots_pub/onaga.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Onaga", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.8, - "m4": 0.3, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.9, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/oysterhead.json b/bots/bots_pub/oysterhead.json deleted file mode 100644 index 27b65e339..000000000 --- a/bots/bots_pub/oysterhead.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Oysterhead", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.0, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.2, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/phobos.json b/bots/bots_pub/phobos.json index ddcddd364..bfb258b92 100644 --- a/bots/bots_pub/phobos.json +++ b/bots/bots_pub/phobos.json @@ -1,35 +1,35 @@ { "name": "Phobos", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.6, - "hc": 0.3, + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, "sniper": 0.6, "knives": 0.4, - "grenades": 0.3 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.1, + "vest": 0.5, "helm": 0.6, - "laser": 0.9, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.3 + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.8, - "curiosity": 0.9, + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.8, "aimingSkill": 0.5, - "reactionTime": 0.7, + "reactionTime": 0.5, "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.2 + "communicationTone": 0.0, + "movementStyle": 0.6, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/pindleskin.json b/bots/bots_pub/pindleskin.json deleted file mode 100644 index 1ec692d84..000000000 --- a/bots/bots_pub/pindleskin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Pindleskin", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.7, - "hc": 1.1, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.0, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/pitspawnfouldog.json b/bots/bots_pub/pitspawnfouldog.json deleted file mode 100644 index 670eeb5f3..000000000 --- a/bots/bots_pub/pitspawnfouldog.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Pitspawn Fouldog", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.5, - "hc": 0.1, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.1, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/potaytoe.json b/bots/bots_pub/potaytoe.json new file mode 100644 index 000000000..749c132a4 --- /dev/null +++ b/bots/bots_pub/potaytoe.json @@ -0,0 +1,35 @@ +{ + "name": "Po-Tay-Toe", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.1, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/pyroyetti.json b/bots/bots_pub/pyroyetti.json new file mode 100644 index 000000000..7ce527dd5 --- /dev/null +++ b/bots/bots_pub/pyroyetti.json @@ -0,0 +1,35 @@ +{ + "name": "Pyro Yetti", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.1, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/quanchi.json b/bots/bots_pub/quanchi.json index 05f92f03a..386c1d03f 100644 --- a/bots/bots_pub/quanchi.json +++ b/bots/bots_pub/quanchi.json @@ -1,34 +1,34 @@ { "name": "Quan Chi", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, + "mk23": 1.0, "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.5, - "hc": 0.7, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.5, "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.6 + "vest": 0.5, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, "movementStyle": 0.4, "objectiveFocus": 0.5 } diff --git a/bots/bots_pub/raiden.json b/bots/bots_pub/raiden.json index 8fd0a09f7..27872eebf 100644 --- a/bots/bots_pub/raiden.json +++ b/bots/bots_pub/raiden.json @@ -1,35 +1,35 @@ { "name": "Raiden", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.3, + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.8, "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.5 + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.6, - "slippers": 0.5, + "vest": 0.4, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.6, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.8, "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/rain.json b/bots/bots_pub/rain.json deleted file mode 100644 index ca66b1781..000000000 --- a/bots/bots_pub/rain.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Rain", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.8, - "m3": 1.0, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.0, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ranger.json b/bots/bots_pub/ranger.json deleted file mode 100644 index d1865252e..000000000 --- a/bots/bots_pub/ranger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ranger", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.9, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 1.1, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/rayquick.json b/bots/bots_pub/rayquick.json deleted file mode 100644 index a6b8083e8..000000000 --- a/bots/bots_pub/rayquick.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ray Quick", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": -0.0, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/razor.json b/bots/bots_pub/razor.json index 01afa9661..6fbe8cf55 100644 --- a/bots/bots_pub/razor.json +++ b/bots/bots_pub/razor.json @@ -1,35 +1,35 @@ { "name": "Razor", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.5 + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.8, + "hc": 0.9, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 1.1 + "vest": 0.7, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.7, + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.8, "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/reddog.json b/bots/bots_pub/reddog.json index 4380779fa..f1c63b3c6 100644 --- a/bots/bots_pub/reddog.json +++ b/bots/bots_pub/reddog.json @@ -1,35 +1,35 @@ { "name": "Reddog", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.4, + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.1, "m4": 0.5, - "m3": 1.1, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.7 + "m3": 0.7, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.2 + "vest": 0.4, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/reiko.json b/bots/bots_pub/reiko.json new file mode 100644 index 000000000..bf5a122fa --- /dev/null +++ b/bots/bots_pub/reiko.json @@ -0,0 +1,35 @@ +{ + "name": "Reiko", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.9, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/replicant.json b/bots/bots_pub/replicant.json new file mode 100644 index 000000000..365b8b838 --- /dev/null +++ b/bots/bots_pub/replicant.json @@ -0,0 +1,35 @@ +{ + "name": "Replicant", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.9, + "knives": 0.3, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/reptile.json b/bots/bots_pub/reptile.json index 0eb6d00c9..01c547eca 100644 --- a/bots/bots_pub/reptile.json +++ b/bots/bots_pub/reptile.json @@ -1,35 +1,35 @@ { "name": "Reptile", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.1, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 + "vest": 0.5, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.9 + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/richard.json b/bots/bots_pub/richard.json deleted file mode 100644 index 9b36327f5..000000000 --- a/bots/bots_pub/richard.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Richard", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.1, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/riftwraith.json b/bots/bots_pub/riftwraith.json index 0839f1362..162455a7b 100644 --- a/bots/bots_pub/riftwraith.json +++ b/bots/bots_pub/riftwraith.json @@ -1,35 +1,35 @@ { "name": "Riftwraith", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.4, "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.3 + "mp5": 0.7, + "m4": 0.4, + "m3": 0.6, + "hc": 0.9, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.0, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.8 + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/rocky.json b/bots/bots_pub/rocky.json deleted file mode 100644 index 64e1f9e75..000000000 --- a/bots/bots_pub/rocky.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Rocky", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.1, - "m4": 0.3, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.2, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/royen.json b/bots/bots_pub/royen.json deleted file mode 100644 index 3ac50f04e..000000000 --- a/bots/bots_pub/royen.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Royen", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ruslan.json b/bots/bots_pub/ruslan.json new file mode 100644 index 000000000..ac1b9f157 --- /dev/null +++ b/bots/bots_pub/ruslan.json @@ -0,0 +1,35 @@ +{ + "name": "Ruslan", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.9, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/ryder.json b/bots/bots_pub/ryder.json index db92d6d1b..4cb1570f3 100644 --- a/bots/bots_pub/ryder.json +++ b/bots/bots_pub/ryder.json @@ -1,35 +1,35 @@ { "name": "Ryder", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.4, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.5, "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.6 + "sniper": 0.4, + "knives": 0.0, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.9, - "slippers": 0.4, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.5, "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.9, + "reactionTime": 0.8, + "communicationFreq": 0.1, + "communicationTone": 0.7, "movementStyle": 0.4, - "objectiveFocus": 0.4 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/ryver.json b/bots/bots_pub/ryver.json index 8e220a360..f271848f6 100644 --- a/bots/bots_pub/ryver.json +++ b/bots/bots_pub/ryver.json @@ -1,35 +1,35 @@ { "name": "Ryver", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, + "mk23": 0.5, + "dual_mk23": 0.3, "mp5": 0.5, - "m4": 0.5, - "m3": 0.5, + "m4": 0.4, + "m3": 0.2, "hc": 0.4, "sniper": 0.7, - "knives": 0.2, - "grenades": 0.7 + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.6 + "vest": 1.0, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.3, "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.1, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/saboy.json b/bots/bots_pub/saboy.json index 62e6468f7..b046414e9 100644 --- a/bots/bots_pub/saboy.json +++ b/bots/bots_pub/saboy.json @@ -1,35 +1,35 @@ { "name": "Saboy", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.7, "mp5": 0.6, - "m4": 1.0, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.3 + "m4": 0.5, + "m3": -0.1, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.5, + "vest": 0.1, + "helm": 0.7, + "laser": 0.4, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.2 + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.4, "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.2, - "objectiveFocus": 0.7 + "communicationFreq": 0.7, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/sagel.json b/bots/bots_pub/sagel.json new file mode 100644 index 000000000..3aa0340ab --- /dev/null +++ b/bots/bots_pub/sagel.json @@ -0,0 +1,35 @@ +{ + "name": "Sagel", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/samdecker.json b/bots/bots_pub/samdecker.json index dd9d31aae..9d347583a 100644 --- a/bots/bots_pub/samdecker.json +++ b/bots/bots_pub/samdecker.json @@ -1,35 +1,35 @@ { "name": "Sam Decker", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.8, + "mk23": 0.9, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.6, "grenades": 0.6 }, "itemPreferences": { - "vest": -0.1, - "helm": 1.1, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.4, + "helm": 0.2, + "laser": 0.9, + "silencer": 0.7, + "slippers": 1.0, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.9, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.9, + "communicationTone": 0.3, + "movementStyle": 0.6, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/sareena.json b/bots/bots_pub/sareena.json deleted file mode 100644 index 842a65253..000000000 --- a/bots/bots_pub/sareena.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sareena", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.1, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.6, - "laser": 0.5, - "silencer": -0.0, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/scorpion.json b/bots/bots_pub/scorpion.json new file mode 100644 index 000000000..8e39a1404 --- /dev/null +++ b/bots/bots_pub/scorpion.json @@ -0,0 +1,35 @@ +{ + "name": "Scorpion", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.1, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.9, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.1, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/seankane.json b/bots/bots_pub/seankane.json new file mode 100644 index 000000000..2bd83aa30 --- /dev/null +++ b/bots/bots_pub/seankane.json @@ -0,0 +1,35 @@ +{ + "name": "Sean Kane", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.8, + "knives": 1.0, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.2, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.9, + "objectiveFocus": 0.9 + } +} \ No newline at end of file diff --git a/bots/bots_pub/sgtcappa.json b/bots/bots_pub/sgtcappa.json deleted file mode 100644 index 61c2b1aa4..000000000 --- a/bots/bots_pub/sgtcappa.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "SGT CAPPA", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/shaokahn.json b/bots/bots_pub/shaokahn.json deleted file mode 100644 index 3aecd8276..000000000 --- a/bots/bots_pub/shaokahn.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shao Kahn", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 1.0, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/shinnok.json b/bots/bots_pub/shinnok.json new file mode 100644 index 000000000..c16c19da4 --- /dev/null +++ b/bots/bots_pub/shinnok.json @@ -0,0 +1,35 @@ +{ + "name": "Shinnok", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.5, + "hc": 0.9, + "sniper": 0.5, + "knives": 1.0, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/shujinko.json b/bots/bots_pub/shujinko.json deleted file mode 100644 index a1a1118a7..000000000 --- a/bots/bots_pub/shujinko.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shujinko", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.9, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.2, - "laser": 0.8, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/siege.json b/bots/bots_pub/siege.json index f11436ffb..08b280917 100644 --- a/bots/bots_pub/siege.json +++ b/bots/bots_pub/siege.json @@ -1,35 +1,35 @@ { "name": "Siege", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.7, + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.3, "hc": 0.6, "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.9, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.2, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.1, - "objectiveFocus": 0.8 + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 1.0, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/sigil.json b/bots/bots_pub/sigil.json deleted file mode 100644 index c52fc8b32..000000000 --- a/bots/bots_pub/sigil.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sigil", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.1, - "m4": 0.2, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.5, - "knives": 1.1, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/sindel.json b/bots/bots_pub/sindel.json new file mode 100644 index 000000000..072a0fbdd --- /dev/null +++ b/bots/bots_pub/sindel.json @@ -0,0 +1,35 @@ +{ + "name": "Sindel", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/skarlet.json b/bots/bots_pub/skarlet.json index cc4e0ccec..de355b031 100644 --- a/bots/bots_pub/skarlet.json +++ b/bots/bots_pub/skarlet.json @@ -1,35 +1,35 @@ { "name": "Skarlet", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.2, - "grenades": 0.6 + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.1, "helm": 0.9, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 1.0, + "aggressiveness": 0.8, + "teamwork": 0.7, "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.5, "communicationTone": 0.5, - "movementStyle": 0.5, + "movementStyle": 0.3, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/skorgan.json b/bots/bots_pub/skorgan.json deleted file mode 100644 index 85928ba0a..000000000 --- a/bots/bots_pub/skorgan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Skorgan", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/slash.json b/bots/bots_pub/slash.json new file mode 100644 index 000000000..dd8ef9181 --- /dev/null +++ b/bots/bots_pub/slash.json @@ -0,0 +1,35 @@ +{ + "name": "Slash", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.0, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/slice.json b/bots/bots_pub/slice.json deleted file mode 100644 index 844548f95..000000000 --- a/bots/bots_pub/slice.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Slice", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.8, - "silencer": 1.1, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/smithy.json b/bots/bots_pub/smithy.json deleted file mode 100644 index c6a01b645..000000000 --- a/bots/bots_pub/smithy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Smithy", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.9, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/smoke.json b/bots/bots_pub/smoke.json deleted file mode 100644 index 2b35213f1..000000000 --- a/bots/bots_pub/smoke.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Smoke", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.7, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/snapchipshatter.json b/bots/bots_pub/snapchipshatter.json deleted file mode 100644 index 30ced28e8..000000000 --- a/bots/bots_pub/snapchipshatter.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Snapchip Shatter", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.0 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/snaps.json b/bots/bots_pub/snaps.json deleted file mode 100644 index a572ee056..000000000 --- a/bots/bots_pub/snaps.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Snaps", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.8, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/socks.json b/bots/bots_pub/socks.json deleted file mode 100644 index 1c1f5411e..000000000 --- a/bots/bots_pub/socks.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Socks", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/spar.json b/bots/bots_pub/spar.json index ce7dc229b..cbe526374 100644 --- a/bots/bots_pub/spar.json +++ b/bots/bots_pub/spar.json @@ -1,35 +1,35 @@ { "name": "Spar", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.1, - "grenades": 0.7 + "mk23": 0.1, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.9, - "silencer": -0.1, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 1.0, + "helm": 0.5, + "laser": 0.4, + "silencer": 1.0, + "slippers": 0.3, + "bandolier": 1.0 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/specialed.json b/bots/bots_pub/specialed.json new file mode 100644 index 000000000..18a32c796 --- /dev/null +++ b/bots/bots_pub/specialed.json @@ -0,0 +1,35 @@ +{ + "name": "SpecialEd", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.9, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/spocket.json b/bots/bots_pub/spocket.json new file mode 100644 index 000000000..4e872e171 --- /dev/null +++ b/bots/bots_pub/spocket.json @@ -0,0 +1,35 @@ +{ + "name": "Spocket", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.9, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.0 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 1.1, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.2 + } +} \ No newline at end of file diff --git a/bots/bots_pub/steampug.json b/bots/bots_pub/steampug.json new file mode 100644 index 000000000..16a59f0e6 --- /dev/null +++ b/bots/bots_pub/steampug.json @@ -0,0 +1,35 @@ +{ + "name": "SteamPug", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.1, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stinger.json b/bots/bots_pub/stinger.json new file mode 100644 index 000000000..b22b78237 --- /dev/null +++ b/bots/bots_pub/stinger.json @@ -0,0 +1,35 @@ +{ + "name": "Stinger", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 1.0, + "communicationFreq": 0.3, + "communicationTone": 0.1, + "movementStyle": 0.5, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stormtree.json b/bots/bots_pub/stormtree.json new file mode 100644 index 000000000..3b6c2dde9 --- /dev/null +++ b/bots/bots_pub/stormtree.json @@ -0,0 +1,35 @@ +{ + "name": "Stormtree", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.1, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 1.2, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.8, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stripe.json b/bots/bots_pub/stripe.json index ebb3b1bf1..73d5f654a 100644 --- a/bots/bots_pub/stripe.json +++ b/bots/bots_pub/stripe.json @@ -1,34 +1,34 @@ { "name": "Stripe", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.9, + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, "m4": 0.3, - "m3": 0.2, - "hc": 0.6, + "m3": 0.6, + "hc": 0.9, "sniper": 0.7, - "knives": 0.3, - "grenades": 0.6 + "knives": 0.8, + "grenades": 0.2 }, "itemPreferences": { "vest": 0.7, - "helm": 0.5, - "laser": 0.1, - "silencer": 0.7, - "slippers": 0.9, - "bandolier": 0.4 + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.0, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.3, + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.6, "movementStyle": 0.3, "objectiveFocus": 0.4 } diff --git a/bots/bots_pub/strogg.json b/bots/bots_pub/strogg.json new file mode 100644 index 000000000..59c855a5f --- /dev/null +++ b/bots/bots_pub/strogg.json @@ -0,0 +1,35 @@ +{ + "name": "Strogg", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.6, + "laser": 0.3, + "silencer": -0.0, + "slippers": 0.3, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/strom.json b/bots/bots_pub/strom.json new file mode 100644 index 000000000..94170a264 --- /dev/null +++ b/bots/bots_pub/strom.json @@ -0,0 +1,35 @@ +{ + "name": "Strom", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.7, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/bots_pub/stryker.json b/bots/bots_pub/stryker.json index 4e214badf..46a60aaa6 100644 --- a/bots/bots_pub/stryker.json +++ b/bots/bots_pub/stryker.json @@ -1,35 +1,35 @@ { "name": "Stryker", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.7, + "dual_mk23": 0.3, "mp5": 0.4, - "m4": 0.4, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.4, - "grenades": 0.8 + "m4": 0.7, + "m3": 0.1, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, + "vest": 0.8, + "helm": 0.2, "laser": 0.6, - "silencer": 0.2, - "slippers": 0.3, + "silencer": 0.6, + "slippers": 0.6, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, "aimingSkill": 0.5, - "reactionTime": 0.9, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.2 + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/stud.json b/bots/bots_pub/stud.json index 6612c790a..88d01604a 100644 --- a/bots/bots_pub/stud.json +++ b/bots/bots_pub/stud.json @@ -1,35 +1,35 @@ { "name": "Stud", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.3 + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.7, + "vest": 0.5, + "helm": 0.5, "laser": 0.7, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.3 + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.2, "teamwork": 0.5, - "curiosity": 0.1, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.3, "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "movementStyle": 0.9, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/t1000.json b/bots/bots_pub/t1000.json new file mode 100644 index 000000000..95fa8777c --- /dev/null +++ b/bots/bots_pub/t1000.json @@ -0,0 +1,35 @@ +{ + "name": "T-1000", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.7, + "bandolier": 1.0 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.6, + "curiosity": 0.2, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/t_bon.json b/bots/bots_pub/t_bon.json new file mode 100644 index 000000000..418e0132c --- /dev/null +++ b/bots/bots_pub/t_bon.json @@ -0,0 +1,35 @@ +{ + "name": "T_bon", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.8, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.9, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.9, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.9, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": -0.0, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tahlkora.json b/bots/bots_pub/tahlkora.json new file mode 100644 index 000000000..775f243ef --- /dev/null +++ b/bots/bots_pub/tahlkora.json @@ -0,0 +1,35 @@ +{ + "name": "Tahlkora", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.8, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": -0.0, + "teamwork": 0.5, + "curiosity": 0.0, + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.7 + } +} \ No newline at end of file diff --git a/bots/bots_pub/takedatakahashi.json b/bots/bots_pub/takedatakahashi.json deleted file mode 100644 index 4360493f7..000000000 --- a/bots/bots_pub/takedatakahashi.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Takeda Takahashi", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.3, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/tankjr.json b/bots/bots_pub/tankjr.json index fdb420949..8e687870d 100644 --- a/bots/bots_pub/tankjr.json +++ b/bots/bots_pub/tankjr.json @@ -1,35 +1,35 @@ { "name": "Tank Jr", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.7, + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.2, "m3": 0.6, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.9 + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.1, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.5, + "helm": 0.3, + "laser": 0.9, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.9, - "aimingSkill": 0.1, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.5, "reactionTime": 0.7, "communicationFreq": 0.4, - "communicationTone": 0.5, + "communicationTone": 0.4, "movementStyle": 0.6, - "objectiveFocus": 0.6 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/tanya.json b/bots/bots_pub/tanya.json new file mode 100644 index 000000000..dbde36477 --- /dev/null +++ b/bots/bots_pub/tanya.json @@ -0,0 +1,35 @@ +{ + "name": "Tanya", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tanz.json b/bots/bots_pub/tanz.json index a8b103be8..50069d415 100644 --- a/bots/bots_pub/tanz.json +++ b/bots/bots_pub/tanz.json @@ -1,35 +1,35 @@ { "name": "Tanz", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.7, - "hc": 0.8, - "sniper": 0.6, + "mk23": 0.8, + "dual_mk23": 0.4, + "mp5": 0.1, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, "knives": 0.7, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.8, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.8 + "vest": 0.6, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.1, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/tao.json b/bots/bots_pub/tao.json index 29233927d..ef073f0e2 100644 --- a/bots/bots_pub/tao.json +++ b/bots/bots_pub/tao.json @@ -1,35 +1,35 @@ { "name": "Tao", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.4, "hc": 0.4, - "sniper": 0.3, - "knives": 1.0, - "grenades": 0.9 + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.5, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.7 + "helm": 0.6, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.9, "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.1 + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/tarnen.json b/bots/bots_pub/tarnen.json deleted file mode 100644 index b3fabdb73..000000000 --- a/bots/bots_pub/tarnen.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tarnen", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.1, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": -0.0, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/tarnok.json b/bots/bots_pub/tarnok.json deleted file mode 100644 index 94765fa54..000000000 --- a/bots/bots_pub/tarnok.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tarnok", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.9, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/tatu.json b/bots/bots_pub/tatu.json index 9beab37dc..3c7ae5c5c 100644 --- a/bots/bots_pub/tatu.json +++ b/bots/bots_pub/tatu.json @@ -1,35 +1,35 @@ { "name": "tatu", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.7 + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.1, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.5, + "vest": 0.2, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, + "aggressiveness": 1.0, + "teamwork": 0.3, "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.4, - "communicationFreq": 0.9, - "communicationTone": 0.2, - "movementStyle": 0.1, - "objectiveFocus": 0.2 + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_pub/technician.json b/bots/bots_pub/technician.json index c164bb44c..faf044687 100644 --- a/bots/bots_pub/technician.json +++ b/bots/bots_pub/technician.json @@ -1,34 +1,34 @@ { "name": "Technician", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.2 + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.3, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.1, - "curiosity": 0.8, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.3, "movementStyle": 0.5, "objectiveFocus": 0.6 } diff --git a/bots/bots_pub/terminator.json b/bots/bots_pub/terminator.json deleted file mode 100644 index 17a295a14..000000000 --- a/bots/bots_pub/terminator.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Terminator", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.1, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.1, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/thalath.json b/bots/bots_pub/thalath.json deleted file mode 100644 index a99991830..000000000 --- a/bots/bots_pub/thalath.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Thalath", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": -0.0, - "aimingSkill": 0.6, - "reactionTime": 0.0, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_pub/thedean.json b/bots/bots_pub/thedean.json index 6db2c0f3d..9ba8818e9 100644 --- a/bots/bots_pub/thedean.json +++ b/bots/bots_pub/thedean.json @@ -1,35 +1,35 @@ { "name": "The Dean", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.6, + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, "m4": 0.5, - "m3": 0.8, - "hc": 1.0, - "sniper": 0.4, + "m3": 0.7, + "hc": 0.4, + "sniper": 1.0, "knives": 0.6, - "grenades": 0.4 + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.5, + "vest": 0.7, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.7, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "aggressiveness": 0.9, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/theeraser.json b/bots/bots_pub/theeraser.json index 94fb7db8e..6738ef716 100644 --- a/bots/bots_pub/theeraser.json +++ b/bots/bots_pub/theeraser.json @@ -1,35 +1,35 @@ { "name": "The Eraser", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.7, - "hc": 0.5, + "mk23": 0.9, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.8, + "hc": 0.4, "sniper": 0.6, - "knives": 0.8, - "grenades": 0.5 + "knives": 0.1, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.6, + "vest": 0.2, + "helm": 0.1, + "laser": 0.8, "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.9 + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/thegerman.json b/bots/bots_pub/thegerman.json index 8ac37bc1b..936304235 100644 --- a/bots/bots_pub/thegerman.json +++ b/bots/bots_pub/thegerman.json @@ -1,35 +1,35 @@ { "name": "The German", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.9, - "grenades": 0.7 + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.7 + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.5, + "aggressiveness": 0.1, + "teamwork": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.7, "movementStyle": 0.7, - "objectiveFocus": 0.3 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/theonly.json b/bots/bots_pub/theonly.json index 758a76a2f..4e7b982ba 100644 --- a/bots/bots_pub/theonly.json +++ b/bots/bots_pub/theonly.json @@ -1,35 +1,35 @@ { "name": "The Only", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.1, - "m4": 0.7, + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.9, "m3": 0.5, - "hc": 0.8, - "sniper": 0.6, - "knives": 0.5, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.4, "grenades": 0.5 }, "itemPreferences": { "vest": 0.6, - "helm": 1.0, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.9, - "bandolier": 0.6 + "helm": 0.6, + "laser": 0.2, + "silencer": 1.0, + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/thetulip.json b/bots/bots_pub/thetulip.json deleted file mode 100644 index 5e5d0cac0..000000000 --- a/bots/bots_pub/thetulip.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "The Tulip", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.1, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.4, - "hc": 0.9, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": -0.0, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/theway.json b/bots/bots_pub/theway.json index 5f62374a3..38a2def96 100644 --- a/bots/bots_pub/theway.json +++ b/bots/bots_pub/theway.json @@ -1,35 +1,35 @@ { "name": "TheWay", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.9, - "m4": 0.5, + "mk23": 1.0, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.2, "m3": 0.4, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.4, - "grenades": 0.5 + "hc": 0.6, + "sniper": 0.1, + "knives": 0.6, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.4, - "slippers": 1.0, - "bandolier": 0.6 + "vest": 0.1, + "helm": 1.0, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.6, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.3, "reactionTime": 0.3, - "communicationFreq": 0.1, - "communicationTone": 0.6, + "communicationFreq": 0.0, + "communicationTone": 0.7, "movementStyle": 0.4, - "objectiveFocus": 0.4 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/thor.json b/bots/bots_pub/thor.json new file mode 100644 index 000000000..f55d9b233 --- /dev/null +++ b/bots/bots_pub/thor.json @@ -0,0 +1,35 @@ +{ + "name": "Thor", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.9, + "communicationTone": 0.7, + "movementStyle": 0.1, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/tiano.json b/bots/bots_pub/tiano.json index e6b3c5379..f2abdca92 100644 --- a/bots/bots_pub/tiano.json +++ b/bots/bots_pub/tiano.json @@ -1,35 +1,35 @@ { "name": "Tiano", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.0, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.4 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.1, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.2 + "vest": 0.7, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": -0.1, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/tommix.json b/bots/bots_pub/tommix.json new file mode 100644 index 000000000..b8a3e177f --- /dev/null +++ b/bots/bots_pub/tommix.json @@ -0,0 +1,35 @@ +{ + "name": "Tom Mix", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.4, + "helm": 0.9, + "laser": 0.0, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_clan/monk.json b/bots/bots_pub/toorc.json similarity index 51% rename from bots/bots_clan/monk.json rename to bots/bots_pub/toorc.json index 7c3023976..4254cd6ea 100644 --- a/bots/bots_clan/monk.json +++ b/bots/bots_pub/toorc.json @@ -1,35 +1,35 @@ { - "name": "Monk", - "clan_id": 7, + "name": "Toorc", + "clan_id": null, "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.1, + "mp5": 0.4, + "m4": 0.5, "m3": 0.5, "hc": 0.4, - "sniper": 0.3, + "sniper": 0.8, "knives": 0.6, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, + "vest": 0.4, + "helm": 0.3, "laser": 0.5, - "silencer": 0.5, - "slippers": 0.6, + "silencer": 0.6, + "slippers": 0.2, "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.2, - "reactionTime": 0.5, + "teamwork": 0.1, + "curiosity": 0.7, + "aimingSkill": 0.4, + "reactionTime": 0.4, "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/tormentor.json b/bots/bots_pub/tormentor.json deleted file mode 100644 index ab652eee6..000000000 --- a/bots/bots_pub/tormentor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tormentor", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.9, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/treehead.json b/bots/bots_pub/treehead.json new file mode 100644 index 000000000..1e4de32d9 --- /dev/null +++ b/bots/bots_pub/treehead.json @@ -0,0 +1,35 @@ +{ + "name": "Treehead", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.3, + "hc": -0.1, + "sniper": 0.4, + "knives": 0.8, + "grenades": 0.2 + }, + "itemPreferences": { + "vest": 0.6, + "helm": 0.1, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.8 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/triborg.json b/bots/bots_pub/triborg.json index 24d39ba20..a91ff0a6b 100644 --- a/bots/bots_pub/triborg.json +++ b/bots/bots_pub/triborg.json @@ -1,35 +1,35 @@ { "name": "Triborg", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.5, + "mk23": 0.6, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.4, "m3": 0.4, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.4 + "hc": 0.7, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.5, + "vest": 0.2, + "helm": 0.7, + "laser": 0.7, "silencer": 0.4, - "slippers": 0.4, + "slippers": 0.5, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.9, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/turkish.json b/bots/bots_pub/turkish.json deleted file mode 100644 index 7eb552140..000000000 --- a/bots/bots_pub/turkish.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Turkish", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/vanoss.json b/bots/bots_pub/vanoss.json index 3ec0e0fe5..c7cc80472 100644 --- a/bots/bots_pub/vanoss.json +++ b/bots/bots_pub/vanoss.json @@ -1,35 +1,35 @@ { "name": "Vanoss", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.6, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.3, "m3": 0.6, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.4 + "hc": 0.4, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.5, + "vest": 0.5, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.3, "slippers": 0.3, - "bandolier": 0.7 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.9, "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.9, + "curiosity": 0.0, + "aimingSkill": 0.1, + "reactionTime": 0.7, "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.3 + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/venom.json b/bots/bots_pub/venom.json index 5b7c9392e..d3e0ce67d 100644 --- a/bots/bots_pub/venom.json +++ b/bots/bots_pub/venom.json @@ -1,35 +1,35 @@ { "name": "VenoM", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.3, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.7, + "m4": 0.4, "m3": 0.4, - "hc": 0.5, + "hc": 0.6, "sniper": 0.5, "knives": 0.5, - "grenades": 0.4 + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.1, + "vest": 0.6, + "helm": 0.9, "laser": 0.7, - "silencer": 0.5, - "slippers": 0.2, + "silencer": 0.3, + "slippers": 0.4, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.2, + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.6, "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "communicationFreq": 0.1, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/venz.json b/bots/bots_pub/venz.json deleted file mode 100644 index aabd5ffe5..000000000 --- a/bots/bots_pub/venz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Venz", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.1, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.9, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.1, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/vey.json b/bots/bots_pub/vey.json index 2a2a05288..a57e85ec1 100644 --- a/bots/bots_pub/vey.json +++ b/bots/bots_pub/vey.json @@ -1,35 +1,35 @@ { "name": "VeY", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.4 + "mk23": 0.7, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.0, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, + "vest": 0.8, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, "slippers": 0.3, - "bandolier": 0.5 + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.1, + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/vic.json b/bots/bots_pub/vic.json index 1d11d23a1..dd98757a0 100644 --- a/bots/bots_pub/vic.json +++ b/bots/bots_pub/vic.json @@ -1,35 +1,35 @@ { "name": "Vic", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.1, "dual_mk23": 0.4, - "mp5": 0.4, + "mp5": 0.5, "m4": 0.5, - "m3": 0.8, - "hc": 0.7, + "m3": 0.7, + "hc": 0.5, "sniper": 0.4, - "knives": 0.3, - "grenades": 0.3 + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.9 + "vest": 0.3, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.1, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.2, + "aggressiveness": 0.6, "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.8, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.2 + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 1.0, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/visor.json b/bots/bots_pub/visor.json new file mode 100644 index 000000000..88b196138 --- /dev/null +++ b/bots/bots_pub/visor.json @@ -0,0 +1,35 @@ +{ + "name": "Visor", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.3, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.3, + "helm": 0.4, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.7 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.1 + } +} \ No newline at end of file diff --git a/bots/bots_pub/vizier.json b/bots/bots_pub/vizier.json index eddc6fdb1..b826ae5d1 100644 --- a/bots/bots_pub/vizier.json +++ b/bots/bots_pub/vizier.json @@ -1,35 +1,35 @@ { "name": "Vizier", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.9, - "m4": 0.4, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 + "mk23": 0.3, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.1, - "bandolier": 0.4 + "vest": 0.3, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "aggressiveness": 1.0, + "teamwork": -0.0, + "curiosity": 0.7, + "aimingSkill": 0.2, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/vokur.json b/bots/bots_pub/vokur.json index e4f29d7b9..d0cc6942d 100644 --- a/bots/bots_pub/vokur.json +++ b/bots/bots_pub/vokur.json @@ -1,35 +1,35 @@ { "name": "Vokur", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.9, - "m3": 0.8, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.7 + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 1.0, + "vest": 0.3, + "helm": 0.4, + "laser": 0.6, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.2 + "slippers": 0.1, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.8, + "aggressiveness": 0.9, + "teamwork": 0.3, + "curiosity": 0.4, "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.9, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.2 + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/walter.json b/bots/bots_pub/walter.json new file mode 100644 index 000000000..75f43aaca --- /dev/null +++ b/bots/bots_pub/walter.json @@ -0,0 +1,35 @@ +{ + "name": "Walter", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.5, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.3 + } +} \ No newline at end of file diff --git a/bots/bots_pub/warchild.json b/bots/bots_pub/warchild.json new file mode 100644 index 000000000..6aa2c76d1 --- /dev/null +++ b/bots/bots_pub/warchild.json @@ -0,0 +1,35 @@ +{ + "name": "Warchild", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.8, + "m3": 0.9, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.5 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.0, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/waxman.json b/bots/bots_pub/waxman.json deleted file mode 100644 index 764b11616..000000000 --- a/bots/bots_pub/waxman.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Waxman", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.1, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/wayofthe.json b/bots/bots_pub/wayofthe.json deleted file mode 100644 index 2ec859a94..000000000 --- a/bots/bots_pub/wayofthe.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Way Of The", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.9, - "m4": 0.3, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.6, - "silencer": -0.0, - "slippers": 0.6, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.1, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/wens.json b/bots/bots_pub/wens.json new file mode 100644 index 000000000..8d6a9238a --- /dev/null +++ b/bots/bots_pub/wens.json @@ -0,0 +1,35 @@ +{ + "name": "Wens", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.8, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.3, + "grenades": 0.6 + }, + "itemPreferences": { + "vest": 0.8, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/whyarewehere.json b/bots/bots_pub/whyarewehere.json deleted file mode 100644 index aa4ba45d7..000000000 --- a/bots/bots_pub/whyarewehere.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Why are we here?", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 1.0, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/whyme.json b/bots/bots_pub/whyme.json index a671af24e..c9304d98e 100644 --- a/bots/bots_pub/whyme.json +++ b/bots/bots_pub/whyme.json @@ -1,35 +1,35 @@ { "name": "Why Me?", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.4, + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.1, + "m4": 0.5, + "m3": 0.5, "hc": 0.7, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.6 + "knives": 0.2, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.5, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.2, + "aggressiveness": 0.2, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.8, "communicationFreq": 0.4, "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/willitz.json b/bots/bots_pub/willitz.json new file mode 100644 index 000000000..b8495f7aa --- /dev/null +++ b/bots/bots_pub/willitz.json @@ -0,0 +1,35 @@ +{ + "name": "Willitz**", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.7, + "grenades": 0.8 + }, + "itemPreferences": { + "vest": 0.9, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.1, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.8 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wrack.json b/bots/bots_pub/wrack.json new file mode 100644 index 000000000..f9f0a477d --- /dev/null +++ b/bots/bots_pub/wrack.json @@ -0,0 +1,35 @@ +{ + "name": "Wrack", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.7 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.6 + }, + "coreTraits": { + "aggressiveness": 0.2, + "teamwork": 0.8, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.4 + } +} \ No newline at end of file diff --git a/bots/bots_pub/wreckage.json b/bots/bots_pub/wreckage.json index a95663987..3ef0d4c6e 100644 --- a/bots/bots_pub/wreckage.json +++ b/bots/bots_pub/wreckage.json @@ -1,35 +1,35 @@ { "name": "Wreckage", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.8, - "hc": 0.2, - "sniper": 0.6, + "mk23": 0.9, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.3, "knives": 0.6, - "grenades": 0.5 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.7 + "vest": 0.3, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.8, + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.7, "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": -0.0, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.7 + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/wyand.json b/bots/bots_pub/wyand.json index 3d7a65203..4d1fc7715 100644 --- a/bots/bots_pub/wyand.json +++ b/bots/bots_pub/wyand.json @@ -1,35 +1,35 @@ { "name": "Wyand", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.7, - "hc": 0.4, + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.3, + "hc": 0.8, "sniper": 0.4, - "knives": 0.5, - "grenades": 0.1 + "knives": 0.4, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.8 + "vest": 0.5, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.3, + "slippers": 0.8, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.7, "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/wysyyy.json b/bots/bots_pub/wysyyy.json index 0a45ebbd7..edb6c9113 100644 --- a/bots/bots_pub/wysyyy.json +++ b/bots/bots_pub/wysyyy.json @@ -1,35 +1,35 @@ { "name": "wysyyy", "clan_id": null, - "skin": "null", + "skin": "male/indy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.9, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.7 + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.4, "helm": 0.2, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.6 + "laser": 0.4, + "silencer": 0.9, + "slippers": 0.6, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.9, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.1, - "objectiveFocus": 0.3 + "aggressiveness": 0.3, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/xaero.json b/bots/bots_pub/xaero.json new file mode 100644 index 000000000..753d8d3e1 --- /dev/null +++ b/bots/bots_pub/xaero.json @@ -0,0 +1,35 @@ +{ + "name": "Xaero", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.1, + "m4": 0.7, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.4 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.7, + "bandolier": 0.4 + }, + "coreTraits": { + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.6 + } +} \ No newline at end of file diff --git a/bots/bots_pub/yarbro.json b/bots/bots_pub/yarbro.json deleted file mode 100644 index 76a458f71..000000000 --- a/bots/bots_pub/yarbro.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Yarbro", - "clan_id": null, - "skin": "null", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/zarna.json b/bots/bots_pub/zarna.json new file mode 100644 index 000000000..3570bb9c4 --- /dev/null +++ b/bots/bots_pub/zarna.json @@ -0,0 +1,35 @@ +{ + "name": "Zarna", + "clan_id": null, + "skin": "male/indy", + "weaponPreferences": { + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.3 + }, + "itemPreferences": { + "vest": 0.5, + "helm": 0.9, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.3 + }, + "coreTraits": { + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 + } +} \ No newline at end of file diff --git a/bots/index.txt b/bots/index.txt index 676cc447c..e12ee603e 100644 --- a/bots/index.txt +++ b/bots/index.txt @@ -1,426 +1,412 @@ -0,]FZe[=GuN,0,291 -1,[DoS]Bean,295,279 -2,=[CS]=Tahlkora,578,288 -3,=[CS]=MannY,870,289 -4,{BH}-Sagel,1163,276 -5,(TBS)Maffer,1443,288 -6,[/]Bullseye,1735,287 -7,[Wang]Willitz**,2026,295 -8,[TRB]_Leiruth,2325,291 -9,_Tq_Hammerhead,2620,288 -10,=[CS]=Spocket,2912,289 -11,=[CS]=T-800,3205,292 -12,(440)Grunt,3501,282 -13,(EL)-T_bon,3787,287 -14,[TRB]_doc,4078,286 -15,]FZe[=Hurricane,4368,292 -16,[DoS]Chameleon,4664,291 -17,]FZe[=Gorre,4959,287 -18,=[CS]=Slash,5250,291 -19,=[CS]=Teabag,5545,291 -20,[/]Kung Lao,5840,285 -21,_Tq_Malin,6129,286 -22,(EL)-Mynx,6419,290 -23,[/]Sarge,6713,283 -24,[DoS]The Boss,7000,293 -25,(440)Mokap,7297,290 -26,=[CS]=Threash,7591,289 -27,{BH}-Zarna,7884,285 -28,(TBS)Berserker,8173,287 -29,(440)Ramrod,8464,288 -30,(440)Xaero,8756,285 -31,(TBS)Helmut,9045,291 -32,[TRB]_Joe Santo,9340,296 -33,_Tq_Drillbit,9640,285 -34,[/]Sorlag,9929,283 -35,[Wang]SteamPug,10216,285 -36,[DoS]Vedro,10505,288 -37,[Wang]Dominator,10797,295 -38,=[CS]=2DCitizen,11096,289 -39,{BH}-Himdar,11389,284 -40,=[CS]=Monty,11677,293 -41,[TRB]_Noogie,11974,288 -42,[DoS]Blackadder,12266,285 -43,(EL)-[Mee2],12555,283 -44,=[CS]=_Gholem_,12842,291 -45,]FZe[=Pyro Yetti,13137,293 -46,(TBS)Trench,13434,281 -47,[TRB]_Taven,13719,285 -48,=[CS]=Patriot,14008,287 -49,[Wang]Geras,14299,282 -50,(TBS)Jazz,14585,287 -51,(TBS)Uriel,14876,296 -52,_Tq_Strogg,15176,282 -53,[Wang]BVe,15462,288 -54,(TBS)Cerberon,15754,287 -55,[/]Frost,16045,287 -56,[Wang]Havo,16336,283 -57,[DoS]Shinnok,16623,284 -58,(440)Mindelos,16911,288 -59,[/]Dragoooon,17203,286 -60,_Tq_Missnrun,17493,282 -61,(EL)-Eyeback,17779,287 -62,(EL)-Fangskin,18070,283 -63,[DoS]Radament,18357,286 -64,(440)Fexel,18647,283 -65,[DoS]Hunter,18934,287 -66,(440)Strom,19225,285 -67,[/]Wesley Snipes,19514,292 -68,[TRB]_Icexo,19810,287 -69,]FZe[=Jarno,20101,286 -70,[Wang]NiN,20391,279 -71,(EL)-Endugu,20674,286 -72,[Wang]Wrack,20964,284 -73,]FZe[=Replicant,21252,287 -74,[DoS]Kaolin,21543,288 -75,[TRB]_Lawrence,21835,288 -76,[TRB]_Doom,22127,290 -77,(440)Dr.Jay,22421,284 -78,[DoS]kuri,22709,285 -79,[DoS]Si,22998,280 -80,_Tq_gogman,23282,278 -81,(TBS)Conan,23564,286 -82,[/]Wens,23854,287 -83,_Tq_FUNKANIK,24145,292 -84,]FZe[=D'Vorah,24441,296 -85,[DoS]Dac Farren,24741,290 -86,{BH}-Tank,25035,288 -87,{BH}-Cybr,25327,290 -88,]FZe[=C-Lion,25621,290 -89,(TBS)Sub-Zero,25915,292 -90,=[CS]=ParaDox,26211,286 -91,_Tq_Amadi,26501,281 -92,(TBS)Gunner,26786,288 -93,[Wang]Mavado,27078,284 -94,[Wang]Marku,27366,285 -95,(TBS)Boy,27655,287 -96,[DoS]Kalidor,27946,282 -97,=[CS]=JCe,28232,285 -98,{BH}-Bob Ross,28521,294 -99,]FZe[=Vera,28819,287 -100,=[CS]=Treehead,29110,285 -101,[DoS]Mr.Badger,29399,291 -102,[Wang]Ratbot,29694,290 -103,{BH}-Hsu Hao,29988,285 -104,{BH}-Mungri,30277,290 -105,(440)Ruslan,30571,285 -106,(440)_*PixxY*_,30860,288 -107,[Wang]Sundog,31152,289 -108,]FZe[=Reiko,31445,288 -109,{BH}-Griswold,31737,287 -110,(440)Warchild,32028,293 -111,[DoS]Scorpion,32325,286 -112,(TBS)Klesk,32615,296 -113,{BH}-Bones,32915,291 -114,[Wang]Baraka,33210,282 -115,[/]Sindel,33496,285 -116,(TBS)Sektor,33785,283 -117,(TBS)Flamespike,34072,288 -118,(EL)-Crash,34364,291 -119,[DoS]Anodized,34659,286 -120,(440)ApeM,34949,286 -121,]FZe[=Tom Steele,35239,293 -122,(EL)-Flint,35536,289 -123,[DoS]Coldcrow,35829,288 -124,[Wang]Orbb,36121,282 -125,[DoS]Ostmor,36407,286 -126,[/]Figgz,36697,281 -127,[/]Applee,36982,288 -128,[Wang]Medic,37274,295 -129,[/]Ben Archer,37573,286 -130,[DoS]Hossman,37863,286 -131,{BH}-Drahmin,38153,288 -132,[Wang]Li Mei,38445,286 -133,{BH}-Sheeva,38735,286 -134,]FZe[=Tremor,39025,285 -135,_Tq_John Luger,39314,295 -136,[TRB]_Kitana,39613,286 -137,(440)exBEE,39903,286 -138,[TRB]_Bitterman,40193,293 -139,_Tq_Sean Kane,40490,281 -140,]FZe[=Chan,40775,291 -141,{BH}-Meat,41070,283 -142,=[CS]=Stinger,41357,293 -143,[/]Digz,41654,283 -144,[DoS]Ivor,41941,284 -145,=[CS]=Kor,42229,292 -146,(440)Kintaro,42525,288 -147,_Tq_Ermac,42817,280 -148,(EL)-Jack Saxon,43101,299 -149,[DoS]Mr Goodkat,43404,291 -150,(440)Belokk,43699,285 -151,(TBS)Jade,43988,288 -152,[/]Icarus,44280,283 -153,_Tq_Morgan,44567,282 -154,(440)Ant__Dog,44853,290 -155,(TBS)Nitara,45147,290 -156,]FZe[=Liu Kang,45441,290 -157,[Wang]Booker,45735,285 -158,[/]Tanya,46024,284 -159,[TRB]_Blythe,46312,291 -160,[TRB]_Hotaru,46607,291 -161,[DoS]Fullmonty,46902,288 -162,[TRB]_Sean,47194,290 -163,_Tq_Po-Tay-Toe,47488,283 -164,(TBS)uzayTron,47775,292 -165,_Tq_GreZZer,48071,286 -166,]FZe[=Keel,48361,285 -167,[Wang]Muscleman,48650,284 -168,=[CS]=Nightwolf,48938,292 -169,(440){itzChamp},49234,289 -170,(440)Edwig,49527,282 -171,[DoS]Gali,49813,288 -172,[/]SpecialEd,50105,288 -173,=[CS]=Icehawk,50397,289 -174,(440)Lone Wolf,50690,287 -175,[Wang]Kollector,50981,289 -176,[/]Nandet,51274,285 -177,(EL)-Brad,51563,291 -178,[Wang]Visor,51858,289 -179,=[CS]=Ashrah,52151,288 -180,]FZe[=Amu,52443,291 -181,(EL)-Jarvis,52738,289 -182,(440)Faden,53031,284 -183,_Tq_Gladiator,53319,287 -184,(TBS)Orin Boyd,53610,288 -185,[/]Tom Mix,53902,291 -186,(440)Joe Tanto,54197,292 -187,[Wang]_GLiCH,54493,299 -188,{BH}-Rakanishu,54796,290 -189,=[CS]=Summoner,55090,292 -190,[Wang]Walter,55386,288 -191,=[CS]=Thor,55678,287 -192,]FZe[=PewDiePie,55969,294 -193,[Wang]Kronika,56267,292 -194,_Tq_Ismail,56563,282 -195,_Tq_T-1000,56849,278 -196,{BH}-Stormtree,57131,289 -197,[DoS]Toorc,57424,288 -198,[DoS]deUSvauLT,57716,289 -199,[TRB]_Hitnrun,58009,290 -200,(EL)-Sarina,58303,284 -201,]FZe[=Aegarond,58591,287 -202,(440)Monk,58882,286 -203,Joe Joiner,59172,282 -204,Major Pain,59458,285 -205,Jax,59747,286 -206,Brixos,60037,283 -207,Nundak,60324,282 -208,Anakin Skywalker,60610,291 -209,Onaga,60905,278 -210,wysyyy,61187,282 -211,Cledus,61473,281 -212,Tormentor,61758,281 -213,Vic,62043,282 -214,Joce,62329,277 -215,Cobra,62610,278 -216,Darrius,62892,284 -217,Anton,63180,285 -218,Ryver,63469,277 -219,Ryder,63750,277 -220,Kira,64031,284 -221,Daegon,64319,283 -222,Mincer,64606,280 -223,Mileena,64890,281 -224,Pindleskin,65175,284 -225,Tanz,65463,276 -226,Moloch,65743,280 -227,Evo89,66027,283 -228,Andromeed,66314,282 -229,Reddog,66600,279 -230,Bolund,66883,288 -231,Chess,67175,281 -232,Ivan Kraschinsky,67460,287 -233,Saboy,67751,280 -234,The Dean,68035,287 -235,Chance Boudreaux,68326,295 -236,Kotal Kahn,68625,285 -237,TheWay,68914,282 -238,Little G,69200,285 -239,Tarnen,69489,282 -240,Cyrax,69775,281 -241,George,70060,281 -242,Raiden,70345,274 -243,Fred,70623,277 -244,Daemia,70904,275 -245,FXguy,71183,280 -246,BroFist,71467,282 -247,Phobos,71753,282 -248,Colt,72039,279 -249,itiXOID,72322,292 -250,Leatherarm,72618,284 -251,Death,72906,282 -252,Technician,73192,284 -253,Kano,73480,283 -254,Himself,73767,282 -255,The Eraser,74053,284 -256,Angelo Provolone,74341,286 -257,Kaa,74631,282 -258,Dark Kahn,74917,287 -259,Tao,75208,281 -260,Bremm,75493,279 -261,Ellanil,75776,281 -262,Rocky,76061,280 -263,Skarlet,76345,284 -264,Cruton,76633,284 -265,Rain,76921,281 -266,Heljay,77206,282 -267,Kung Jin,77492,283 -268,Wyand,77779,279 -269,Dutch,78062,287 -270,Terminator,78353,281 -271,[Corrus],78638,290 -272,Snaps,78932,281 -273,Gung Ho,79217,280 -274,Broz,79501,278 -275,CowChop,79783,284 -276,Jacques Kristoff,80071,291 -277,Ferra/Torr,80366,286 -278,Pitspawn Fouldog,80656,287 -279,Dairou,80947,279 -280,Angel,81230,287 -281,Ray Quick,81521,285 -282,Lucy,81810,280 -283,SGT CAPPA,82094,288 -284,Bad Sam,82386,286 -285,Stryker,82676,281 -286,Jinx,82961,278 -287,Doctor Deathwish,83243,293 -288,Quan Chi,83540,282 -289,Stripe,83826,282 -290,Kole,84112,281 -291,Stud,84397,277 -292,Milius,84678,285 -293,Edoli,84967,286 -294,Major,85257,282 -295,Cow King,85543,285 -296,_*NiXXy*_,85832,292 -297,Grimm,86128,281 -298,Cadaver,86413,281 -299,Halfwit,86698,283 -300,Bloodwitch,86985,284 -301,Boneash,87273,280 -302,Hellhound,87557,284 -303,Turkish,87845,280 -304,^^ChoppY^^,88129,288 -305,Why Me?,88421,283 -306,Mr Suave,88708,282 -307,Enforcer,88994,282 -308,Mongrel,89280,283 -309,Hambone,89567,284 -310,Smithy,89855,276 -311,Nikolai Cherenko,90135,288 -312,NoAmmo,90427,285 -313,Frenchy,90716,279 -314,Kobra,90999,278 -315,Biker,91281,281 -316,Drago,91566,280 -317,Bad JuJu,91850,284 -318,Ivan Drago,92138,285 -319,Merki,92427,279 -320,Oysterhead,92710,286 -321,Hecker,93000,282 -322,Venz,93286,283 -323,Newbie,93573,277 -324,Michael,93854,277 -325,Bacon,94135,283 -326,Dolin,94422,280 -327,Nick Gunar,94706,288 -328,Razor,94998,280 -329,Skorgan,95282,277 -330,Vizier,95563,281 -331,Imentu,95848,287 -332,Wreckage,96139,285 -333,Bortack,96428,281 -334,Kit Latura,96713,282 -335,Vanoss,96999,281 -336,Geleb,97284,284 -337,Kabal,97572,284 -338,Motaro,97860,287 -339,BigMack,98151,279 -340,Shujinko,98434,285 -341,The German,98723,283 -342,VeY,99010,276 -343,Di Nardo,99290,285 -344,Morgriff,99579,283 -345,Brohn,99866,277 -346,The Tulip,100147,287 -347,Tarnok,100438,280 -348,Sareena,100722,284 -349,GhOstBl4de,101010,287 -350,Church,101301,283 -351,Carpenter,101588,288 -352,Vokur,101880,284 -353,Nihlathak,102168,290 -354,chipp_,102462,282 -355,Halfbaked,102748,285 -356,Spar,103037,283 -357,Snapchip Shatter,103324,286 -358,Gigavoltz,103614,285 -359,Reptile,103903,284 -360,Grog,104191,283 -361,John Rambo,104478,283 -362,Kai,104765,280 -363,Royen,105049,280 -364,Sigil,105333,282 -365,Bishibosh,105619,288 -366,Way Of The,105911,291 -367,Frank Dux,106206,284 -368,D is for Digger!,106494,291 -369,Richard,106789,281 -370,Garbok,107074,278 -371,Thalath,107356,282 -372,Havik,107642,276 -373,Aimless,107922,279 -374,Waxman,108205,281 -375,Nibbler,108490,285 -376,Why are we here?,108779,288 -377,Cetrion,109071,283 -378,Coldworm,109358,291 -379,hozef-8,109653,286 -380,JD Gold,109943,285 -381,Khameleon,110232,281 -382,JJ McQuade,110517,288 -383,Korv,110809,279 -384,Makron,111092,287 -385,Bonebreak,111383,280 -386,tatu,111667,278 -387,Corpsefire,111949,279 -388,HAL-9000,112232,291 -389,Smoke,112527,282 -390,Slice,112813,279 -391,Nohope,113096,276 -392,Socks,113376,278 -393,Barbarian,113658,280 -394,Ranger,113942,286 -395,Tiano,114232,282 -396,Gordac,114518,284 -397,Tank Jr,114806,282 -398,Kenshi,115092,281 -399,Yarbro,115377,285 -400,Fujin,115666,275 -401,Kippy,115945,280 -402,VenoM,116229,281 -403,Arklon,116514,286 -404,Jarek,116804,283 -405,Siege,117091,282 -406,Argo,117377,280 -407,Takeda Takahashi,117661,286 -408,Lt Raymond Tango,117951,295 -409,Beelz,118250,284 -410,Shao Kahn,118538,286 -411,Galul,118828,276 -412,Riftwraith,119108,283 -413,Sam Decker,119395,293 -414,Backfire,119692,282 -415,Countess,119978,284 -416,Blaze,120266,285 -417,The Only,120555,285 -418,Capt. Ivan Danko,120844,285 -419,Anarki,121133,282 -420,Indiana Jones,121419,286 -421,McKenna,121709,280 -422,Triborg,121993,282 -423,Dharmesh,122279,282 -424,Lemmie,122565,285 -425,JC Denton,122854,288 +0,#EE#Major Pain,0,294 +1,MannY/TJ`,298,289 +2,;SS:Maffer,591,289 +3,Bullseye)TM{,884,287 +4,]FZe[=Leiruth,1175,290 +5,Onaga/WC$,1469,291 +6,#MM#Hammerhead,1764,289 +7,Tormentor-HH-,2057,278 +8,\HOLE(,20080,290 +70,Major\PC\,20374,286 +71,{TT{Jarno,20664,289 +72,NiN[TD[,20957,283 +73,[TK[Endugu,21244,285 +74,Kaolin&SP),21533,286 +75,#IL/Bloodwitch,21823,287 +76,LE(,30846,286 +107,Griswold.TC.,31136,293 +108,[GB[Klesk,31433,285 +109,Bones/WC$,31722,279 +110,+SF+Sektor,32005,279 +111,Kit Latura/EE!,32288,286 +112,#BB\Anodized,32578,289 +113,&TX&ApeM,32871,289 +114,:GA:Tom Steele,33164,288 +115,Motaro'I!,33456,286 +116,Shujinko-BB^,33746,285 +117,Coldcrow:TT$,34035,286 +118,Di Nardo#TS(,34325,291 +119,[/]Orbb,34620,284 +120,{BH}-Ostmor,34908,280 +121,+SF+Figgz,35192,284 +122,_Tq_Applee,35480,284 +123,%DS\Medic,35768,287 +124,The Tulip)F*,36059,287 +125,Tarnok@MM+,36350,289 +126,}NN&Sareena,36643,286 +127,AA>Bitterman,38970,293 +135,LE(,39851,291 +138,John Rambo.TC.,40146,291 +139,Kai#TS(,40441,290 +140,Royen(AC`,40735,287 +141,Meat!PR!,41026,288 +142,(TBS)Sigil,41318,285 +143,Way Of The+SET&,41607,292 +144,\NS\Frank Dux,41903,284 +145,$TV$Ivor,42191,283 +146,#IL/Kor,42478,279 +147,{BH}-Richard,42761,288 +148,.TB.Thalath,43053,283 +149,[EF=Mr Goodkat,43340,293 +150,}TO}Havik,43637,287 +151,$TA$Aimless,43928,287 +152,Waxman"BB",44219,295 +153,Nibbler(TS},44518,291 +154,Cetrion/EE!,44813,289 +155,]FZe[=Coldworm,45106,284 +156,JD Gold*SA*,45394,290 +157,Khameleon"SC",45688,294 +158,Icarus#TS(,45986,292 +159,Makron/EE!,46282,283 +160,Nitara:BA:,46569,286 +161,Liu Kang[DD%,46859,288 +162,Blythe!PR!,47151,287 +163,Corpsefire`MM`,47442,289 +164,Sean`TK`,47735,288 +165,!DD!Smoke,48027,282 +166,(TBS)uzayTron,48313,296 +167,#IL/GreZZer,48613,288 +168,/CG/Slice,48905,289 +169,Keel=LP=,49198,287 +170,Muscleman'YY=,49489,289 +171,+SF+Nohope,49782,284 +172,Socks^TV^,50070,282 +173,Edwig=AA-,50356,283 +174,}NN&Ranger,50643,285 +175,/CG/Icehawk,50932,293 +176,Gordac:TT$,51229,286 +177,Brad\PC\,51519,293 +178,}NN&Yarbro,51816,283 +179,]FZe[=Jarvis,52103,288 +180,!DD!Fujin,52395,284 +181,#IL/Faden,52683,288 +182,[TRB]_Kippy,52975,288 +183,Jarek\RB\,53267,290 +184,Orin Boyd}LL},53561,293 +185,;CC;Shao Kahn,53858,286 +186,$TV$Rakanishu,54148,291 +187,[DoS]Summoner,54443,288 +188,PewDiePie-BB^,54735,289 +189,$TA$Kronika,55028,289 +190,Ismail}LL},55321,285 +191,&S&McKenna,55610,287 +192,&S&Dharmesh,55901,284 +193,Lemmie-HH-,56189,283 +194,Hitnrun-BB^,56476,289 +195,[RD[Sarina,56769,287 +196,#EE#Aegarond,57060,286 +197,GuN,57350,281 +198,Bean,57635,283 +199,Tahlkora,57922,287 +200,Joe Joiner,58213,291 +201,Jax,58508,280 +202,Sagel,58792,283 +203,Brixos,59079,281 +204,Nundak,59364,285 +205,Willitz**,59653,287 +206,Spocket,59944,285 +207,wysyyy,60233,279 +208,Cledus,60516,279 +209,Vic,60799,280 +210,Grunt,61083,279 +211,T_bon,61366,282 +212,Cobra,61652,285 +213,Darrius,61941,283 +214,Ryver,62228,284 +215,Ryder,62516,285 +216,Kira,62805,280 +217,Daegon,63089,284 +218,Slash,63377,282 +219,Tanz,63663,279 +220,Moloch,63946,281 +221,Evo89,64231,283 +222,Andromeed,64518,286 +223,Reddog,64808,280 +224,Bolund,65092,280 +225,Mynx,65376,282 +226,Saboy,65662,282 +227,The Dean,65948,285 +228,Kotal Kahn,66237,283 +229,TheWay,66524,287 +230,Little G,66815,284 +231,Cyrax,67103,278 +232,George,67385,275 +233,Raiden,67664,283 +234,Zarna,67951,283 +235,Berserker,68238,285 +236,Xaero,68527,278 +237,Helmut,68809,284 +238,BroFist,69097,286 +239,Joe Santo,69387,282 +240,Phobos,69673,281 +241,Drillbit,69958,290 +242,SteamPug,70252,277 +243,Dominator,70533,286 +244,2DCitizen,70823,285 +245,Death,71112,284 +246,Technician,71400,280 +247,Monty,71684,281 +248,Himself,71969,281 +249,The Eraser,72254,285 +250,Noogie,72543,278 +251,Blackadder,72825,288 +252,Kaa,73117,276 +253,Tao,73397,277 +254,Bremm,73678,278 +255,Ellanil,73960,279 +256,_Gholem_,74243,288 +257,Skarlet,74535,283 +258,Kung Jin,74822,284 +259,Wyand,75110,279 +260,Dutch,75393,283 +261,Pyro Yetti,75680,284 +262,[Corrus],75968,288 +263,Strogg,76260,280 +264,BVe,76544,293 +265,Dairou,76841,278 +266,Bad Sam,77123,283 +267,Stryker,77410,283 +268,Havo,77697,281 +269,Shinnok,77982,283 +270,Mindelos,78269,286 +271,Quan Chi,78559,286 +272,Dragoooon,78849,277 +273,Stripe,79130,282 +274,Hunter,79416,281 +275,Kole,79701,281 +276,Stud,79986,279 +277,Strom,80269,277 +278,Milius,80550,274 +279,Icexo,80828,278 +280,Cow King,81110,292 +281,_*NiXXy*_,81406,288 +282,Grimm,81698,279 +283,Wrack,81981,281 +284,Replicant,82266,284 +285,Cadaver,82554,279 +286,Halfwit,82837,283 +287,Lawrence,83124,284 +288,Doom,83412,281 +289,Dr.Jay,83697,280 +290,Boneash,83981,282 +291,Hellhound,84267,282 +292,kuri,84553,279 +293,gogman,84836,283 +294,Why Me?,85123,286 +295,Wens,85413,273 +296,Enforcer,85690,283 +297,FUNKANIK,85977,291 +298,Mongrel,86272,285 +299,D'Vorah,86561,279 +300,Hambone,86844,278 +301,Cybr,87126,278 +302,C-Lion,87408,286 +303,Amadi,87698,283 +304,NoAmmo,87985,285 +305,Frenchy,88274,281 +306,Biker,88559,283 +307,Drago,88846,284 +308,Ivan Drago,89134,283 +309,Kalidor,89421,277 +310,Treehead,89702,285 +311,Bacon,89991,277 +312,Mr.Badger,90272,284 +313,Dolin,90560,281 +314,Nick Gunar,90845,285 +315,Razor,91134,281 +316,Mungri,91419,283 +317,Ruslan,91706,281 +318,Reiko,91991,280 +319,Warchild,92275,288 +320,Scorpion,92567,283 +321,Baraka,92854,279 +322,Vizier,93137,286 +323,Imentu,93427,280 +324,Wreckage,93711,282 +325,Bortack,93997,285 +326,Sindel,94286,278 +327,Vanoss,94568,284 +328,Flamespike,94856,280 +329,Crash,95140,292 +330,Geleb,95436,276 +331,Flint,95716,281 +332,Kabal,96001,280 +333,BigMack,96285,288 +334,The German,96577,287 +335,VeY,96868,281 +336,Morgriff,97153,285 +337,Brohn,97442,282 +338,Ben Archer,97728,286 +339,GhOstBl4de,98018,280 +340,Church,98302,290 +341,Carpenter,98596,281 +342,Vokur,98881,285 +343,Li Mei,99170,279 +344,Kitana,99453,286 +345,Spar,99743,283 +346,Sean Kane,100030,288 +347,Reptile,100322,279 +348,Grog,100605,278 +349,Chan,100887,276 +350,Stinger,101167,279 +351,Digz,101450,276 +352,Bishibosh,101730,283 +353,Garbok,102017,282 +354,Kintaro,102303,281 +355,Ermac,102588,286 +356,Jack Saxon,102878,288 +357,Belokk,103170,286 +358,hozef-8,103460,283 +359,Jade,103747,282 +360,JJ McQuade,104033,288 +361,Korv,104325,281 +362,Morgan,104610,283 +363,Ant__Dog,104897,287 +364,Booker,105188,283 +365,Tanya,105475,277 +366,Hotaru,105756,283 +367,Bonebreak,106043,285 +368,tatu,106332,282 +369,HAL-9000,106618,288 +370,Fullmonty,106910,279 +371,Po-Tay-Toe,107193,283 +372,Nightwolf,107480,287 +373,{itzChamp},107771,285 +374,Gali,108060,280 +375,SpecialEd,108344,278 +376,Barbarian,108626,282 +377,Tiano,108912,279 +378,Lone Wolf,109195,286 +379,Kollector,109485,280 +380,Tank Jr,109769,280 +381,Nandet,110053,290 +382,Kenshi,110347,283 +383,Visor,110634,281 +384,Ashrah,110919,282 +385,Amu,111205,279 +386,VenoM,111488,282 +387,Arklon,111774,285 +388,Gladiator,112063,285 +389,Tom Mix,112352,283 +390,Siege,112639,281 +391,Joe Tanto,112924,279 +392,Argo,113207,277 +393,_GLiCH,113488,289 +394,Beelz,113781,280 +395,Galul,114065,283 +396,Riftwraith,114352,284 +397,Walter,114640,282 +398,Sam Decker,114926,287 +399,Backfire,115217,285 +400,Countess,115506,282 +401,Thor,115792,277 +402,Blaze,116073,280 +403,The Only,116357,283 +404,Anarki,116644,280 +405,T-1000,116928,283 +406,Stormtree,117215,285 +407,Triborg,117504,283 +408,Toorc,117791,282 +409,deUSvauLT,118077,282 +410,JC Denton,118363,287 +411,Monk,118654,281 From 43273b97c3bdd6183e5faf38c9f4c1195b8d7ade Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Feb 2024 18:11:20 +0300 Subject: [PATCH 083/974] Clean up PCX loading code. --- src/refresh/images.c | 95 +++++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index eab0be6cf..5c767fabe 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -124,13 +124,44 @@ PCX LOADING ================================================================= */ -static int IMG_LoadPCX_(byte *rawdata, size_t rawlen, byte *pixels, - byte *palette, int *width, int *height) +static int uncompress_pcx(const byte *raw, const byte *end, + int w, int h, int scan, byte *pixels) +{ + int dataByte, runLength; + + for (int y = 0; y < h; y++, pixels += w) { + for (int x = 0; x < scan;) { + if (raw >= end) + return Q_ERR_OVERRUN; + dataByte = *raw++; + + if ((dataByte & 0xC0) == 0xC0) { + runLength = dataByte & 0x3F; + if (x + runLength > scan) + return Q_ERR_OVERRUN; + if (raw >= end) + return Q_ERR_OVERRUN; + dataByte = *raw++; + } else { + runLength = 1; + } + + while (runLength--) { + if (x < w) + pixels[x] = dataByte; + x++; + } + } + } + + return Q_ERR_SUCCESS; +} + +static int load_pcx(byte *rawdata, size_t rawlen, byte **pixels_p, + byte *palette, int *width, int *height) { - byte *raw, *end; dpcx_t *pcx; - int x, y, w, h, scan; - int dataByte, runLength; + int w, h, scan; // // parse the PCX file @@ -175,39 +206,20 @@ static int IMG_LoadPCX_(byte *rawdata, size_t rawlen, byte *pixels, if (rawlen < 768) { return Q_ERR_FILE_TOO_SMALL; } - memcpy(palette, (byte *)pcx + rawlen - 768, 768); + memcpy(palette, rawdata + rawlen - 768, 768); } // // get pixels // - if (pixels) { - raw = pcx->data; - end = (byte *)pcx + rawlen; - for (y = 0; y < h; y++, pixels += w) { - for (x = 0; x < scan;) { - if (raw >= end) - return Q_ERR_OVERRUN; - dataByte = *raw++; - - if ((dataByte & 0xC0) == 0xC0) { - runLength = dataByte & 0x3F; - if (x + runLength > scan) - return Q_ERR_OVERRUN; - if (raw >= end) - return Q_ERR_OVERRUN; - dataByte = *raw++; - } else { - runLength = 1; - } - - while (runLength--) { - if (x < w) - pixels[x] = dataByte; - x++; - } - } + if (pixels_p) { + byte *pixels = IMG_AllocPixels(w * h); + int ret = uncompress_pcx(pcx->data, rawdata + rawlen, w, h, scan, pixels); + if (ret < 0) { + IMG_FreePixels(pixels); + return ret; } + *pixels_p = pixels; } if (width) @@ -271,30 +283,23 @@ static int IMG_Unpack8(uint32_t *out, const uint8_t *in, int width, int height) IMG_LOAD(PCX) { - byte *buffer; + byte *pixels; int w, h, ret; - ret = IMG_LoadPCX_(rawdata, rawlen, NULL, NULL, &w, &h); + ret = load_pcx(rawdata, rawlen, &pixels, NULL, &w, &h); if (ret < 0) return ret; - buffer = IMG_AllocPixels(w * h); - ret = IMG_LoadPCX_(rawdata, rawlen, buffer, NULL, NULL, NULL); - if (ret < 0) { - IMG_FreePixels(buffer); - return ret; - } - if (image->type == IT_SKIN) - IMG_FloodFill(buffer, w, h); + IMG_FloodFill(pixels, w, h); *pic = IMG_AllocPixels(w * h * 4); image->upload_width = image->width = w; image->upload_height = image->height = h; - image->flags |= IMG_Unpack8((uint32_t *)*pic, buffer, w, h); + image->flags |= IMG_Unpack8((uint32_t *)*pic, pixels, w, h); - IMG_FreePixels(buffer); + IMG_FreePixels(pixels); return Q_ERR_SUCCESS; } @@ -2044,7 +2049,7 @@ void IMG_GetPalette(void) goto fail; } - ret = IMG_LoadPCX_(data, len, NULL, pal, NULL, NULL); + ret = load_pcx(data, len, NULL, pal, NULL, NULL); FS_FreeFile(data); From e6eadabf6531d741ef49d00f37ca98bc14722396 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Feb 2024 22:28:36 +0300 Subject: [PATCH 084/974] Move function and #if. --- src/refresh/images.c | 107 +++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 5c767fabe..8af294485 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -116,6 +116,57 @@ static q_noinline void IMG_FloodFill(byte *skin, int skinwidth, int skinheight) } } +/* +=============== +IMG_Unpack8 +=============== +*/ +static int IMG_Unpack8(uint32_t *out, const uint8_t *in, int width, int height) +{ + int x, y, p; + bool has_alpha = false; + + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + p = *in; + if (p == 255) { + has_alpha = true; + // transparent, so scan around for another color + // to avoid alpha fringes + if (y > 0 && *(in - width) != 255) + p = *(in - width); + else if (y < height - 1 && *(in + width) != 255) + p = *(in + width); + else if (x > 0 && *(in - 1) != 255) + p = *(in - 1); + else if (x < width - 1 && *(in + 1) != 255) + p = *(in + 1); + else if (y > 0 && x > 0 && *(in - width - 1) != 255) + p = *(in - width - 1); + else if (y > 0 && x < width - 1 && *(in - width + 1) != 255) + p = *(in - width + 1); + else if (y < height - 1 && x > 0 && *(in + width - 1) != 255) + p = *(in + width - 1); + else if (y < height - 1 && x < width - 1 && *(in + width + 1) != 255) + p = *(in + width + 1); + else + p = 0; + // copy rgb components + *out = d_8to24table[p] & U32_RGB; + } else { + *out = d_8to24table[p]; + } + in++; + out++; + } + } + + if (has_alpha) + return IF_PALETTED | IF_TRANSPARENT; + + return IF_PALETTED | IF_OPAQUE; +} + /* ================================================================= @@ -230,57 +281,6 @@ static int load_pcx(byte *rawdata, size_t rawlen, byte **pixels_p, return Q_ERR_SUCCESS; } -/* -=============== -IMG_Unpack8 -=============== -*/ -static int IMG_Unpack8(uint32_t *out, const uint8_t *in, int width, int height) -{ - int x, y, p; - bool has_alpha = false; - - for (y = 0; y < height; y++) { - for (x = 0; x < width; x++) { - p = *in; - if (p == 255) { - has_alpha = true; - // transparent, so scan around for another color - // to avoid alpha fringes - if (y > 0 && *(in - width) != 255) - p = *(in - width); - else if (y < height - 1 && *(in + width) != 255) - p = *(in + width); - else if (x > 0 && *(in - 1) != 255) - p = *(in - 1); - else if (x < width - 1 && *(in + 1) != 255) - p = *(in + 1); - else if (y > 0 && x > 0 && *(in - width - 1) != 255) - p = *(in - width - 1); - else if (y > 0 && x < width - 1 && *(in - width + 1) != 255) - p = *(in - width + 1); - else if (y < height - 1 && x > 0 && *(in + width - 1) != 255) - p = *(in + width - 1); - else if (y < height - 1 && x < width - 1 && *(in + width + 1) != 255) - p = *(in + width + 1); - else - p = 0; - // copy rgb components - *out = d_8to24table[p] & U32_RGB; - } else { - *out = d_8to24table[p]; - } - in++; - out++; - } - } - - if (has_alpha) - return IF_PALETTED | IF_TRANSPARENT; - - return IF_PALETTED | IF_OPAQUE; -} - IMG_LOAD(PCX) { byte *pixels; @@ -783,9 +783,6 @@ static int IMG_SaveJPG(screenshot_t *restrict s) #endif // USE_JPG - -#if USE_PNG - /* ========================================================= @@ -794,6 +791,8 @@ PNG IMAGES ========================================================= */ +#if USE_PNG + typedef struct { png_bytep next_in; png_size_t avail_in; From 92a677af54e0985b09ab90767dce5ffd807a7324 Mon Sep 17 00:00:00 2001 From: mikota Date: Tue, 27 Feb 2024 21:32:52 +0100 Subject: [PATCH 085/974] Update meson.build --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index e215ba9e3..f427f8dcd 100644 --- a/meson.build +++ b/meson.build @@ -3,7 +3,7 @@ project('q2pro', 'c', version: run_command(find_program('python3'), 'version.py', check: true).stdout().strip(), meson_version: '>= 0.59.0', default_options: [ - meson.version().version_compare('>=1.3.0') ? 'c_std=gnu11,c11' : 'c_std=gnu11', + 'c_std=c11', 'buildtype=debugoptimized', ], ) From 872e139ec325e0140dc4c5fdded92ea36fe70bd1 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Feb 2024 15:47:26 -0500 Subject: [PATCH 086/974] Disabling Discord builds for the moment --- bots/botmaker.py | 13 +- bots/bots_clan/_pixxy_.json | 48 +- bots/bots_clan/aegarond.json | 35 -- bots/bots_clan/aimless.json | 35 -- bots/bots_clan/angel.json | 44 +- bots/bots_clan/anodized.json | 46 +- bots/bots_clan/anton.json | 42 +- bots/bots_clan/apem.json | 35 -- bots/bots_clan/applee.json | 35 -- bots/bots_clan/badjuju.json | 48 +- bots/bots_clan/bitterman.json | 48 +- bots/bots_clan/bloodwitch.json | 50 +- bots/bots_clan/blythe.json | 35 -- bots/bots_clan/bobross.json | 42 +- bots/bots_clan/bones.json | 46 +- bots/bots_clan/boy.json | 35 -- bots/bots_clan/brad.json | 35 -- bots/bots_clan/broz.json | 44 +- bots/bots_clan/bullseye.json | 42 +- bots/bots_clan/cerberon.json | 35 -- bots/bots_clan/cetrion.json | 35 -- bots/bots_clan/chameleon.json | 40 +- bots/bots_clan/chess.json | 48 +- bots/bots_clan/chipp_.json | 35 -- bots/bots_clan/choppy.json | 35 -- bots/bots_clan/coldcrow.json | 35 -- bots/bots_clan/coldworm.json | 35 -- bots/bots_clan/colt.json | 44 +- bots/bots_clan/conan.json | 35 -- bots/bots_clan/corpsefire.json | 42 +- bots/bots_clan/cowchop.json | 35 -- bots/bots_clan/cruton.json | 35 -- bots/bots_clan/dacfarren.json | 35 -- bots/bots_clan/daemia.json | 35 -- bots/bots_clan/darkkahn.json | 48 +- bots/bots_clan/dharmesh.json | 32 +- bots/bots_clan/dinardo.json | 46 +- bots/bots_clan/doc.json | 35 -- bots/bots_clan/drahmin.json | 48 +- bots/bots_clan/edoli.json | 48 +- bots/bots_clan/edwig.json | 35 -- bots/bots_clan/endugu.json | 46 +- bots/bots_clan/exbee.json | 35 -- bots/bots_clan/eyeback.json | 48 +- bots/bots_clan/faden.json | 40 +- bots/bots_clan/fangskin.json | 35 -- bots/bots_clan/ferratorr.json | 35 -- bots/bots_clan/fexel.json | 46 +- bots/bots_clan/figgz.json | 40 +- bots/bots_clan/frankdux.json | 35 -- bots/bots_clan/fred.json | 50 +- bots/bots_clan/frost.json | 35 -- bots/bots_clan/fujin.json | 46 +- bots/bots_clan/fxguy.json | 48 +- bots/bots_clan/geras.json | 35 -- bots/bots_clan/gigavoltz.json | 35 -- bots/bots_clan/gordac.json | 46 +- bots/bots_clan/gorre.json | 52 +- bots/bots_clan/grezzer.json | 44 +- bots/bots_clan/griswold.json | 35 -- bots/bots_clan/gungho.json | 48 +- bots/bots_clan/gunner.json | 40 +- bots/bots_clan/halfbaked.json | 35 -- bots/bots_clan/hammerhead.json | 35 -- bots/bots_clan/havik.json | 35 -- bots/bots_clan/hecker.json | 35 -- bots/bots_clan/heljay.json | 40 +- bots/bots_clan/himdar.json | 42 +- bots/bots_clan/hitnrun.json | 52 +- bots/bots_clan/hossman.json | 44 +- bots/bots_clan/hsuhao.json | 35 -- bots/bots_clan/hurricane.json | 35 -- bots/bots_clan/icarus.json | 50 +- bots/bots_clan/icehawk.json | 35 -- bots/bots_clan/ismail.json | 52 +- bots/bots_clan/itixoid.json | 35 -- bots/bots_clan/ivor.json | 42 +- bots/bots_clan/jarek.json | 35 -- bots/bots_clan/jarno.json | 35 -- bots/bots_clan/jarvis.json | 42 +- bots/bots_clan/jazz.json | 46 +- bots/bots_clan/jce.json | 35 -- bots/bots_clan/jdgold.json | 36 +- bots/bots_clan/jinx.json | 42 +- bots/bots_clan/joce.json | 44 +- bots/bots_clan/johnluger.json | 35 -- bots/bots_clan/johnrambo.json | 35 -- bots/bots_clan/kai.json | 35 -- bots/bots_clan/kano.json | 34 +- bots/bots_clan/kaolin.json | 42 +- bots/bots_clan/keel.json | 48 +- bots/bots_clan/khameleon.json | 35 -- bots/bots_clan/kippy.json | 35 -- bots/bots_clan/kitlatura.json | 35 -- bots/bots_clan/klesk.json | 48 +- bots/bots_clan/kobra.json | 35 -- bots/bots_clan/kor.json | 42 +- bots/bots_clan/kronika.json | 35 -- bots/bots_clan/kunglao.json | 35 -- bots/bots_clan/leatherarm.json | 48 +- bots/bots_clan/leiruth.json | 46 +- bots/bots_clan/lemmie.json | 35 -- bots/bots_clan/liukang.json | 48 +- bots/bots_clan/lucy.json | 35 -- bots/bots_clan/maffer.json | 35 -- bots/bots_clan/major.json | 35 -- bots/bots_clan/majorpain.json | 50 +- bots/bots_clan/makron.json | 35 -- bots/bots_clan/malin.json | 50 +- bots/bots_clan/manny.json | 35 -- bots/bots_clan/marku.json | 35 -- bots/bots_clan/mavado.json | 35 -- bots/bots_clan/mckenna.json | 48 +- bots/bots_clan/meat.json | 35 -- bots/bots_clan/medic.json | 44 +- bots/bots_clan/mee2.json | 35 -- bots/bots_clan/merki.json | 44 +- bots/bots_clan/michael.json | 50 +- bots/bots_clan/mileena.json | 42 +- bots/bots_clan/mincer.json | 44 +- bots/bots_clan/missnrun.json | 35 -- bots/bots_clan/mokap.json | 48 +- bots/bots_clan/motaro.json | 50 +- bots/bots_clan/mrgoodkat.json | 35 -- bots/bots_clan/mrsuave.json | 35 -- bots/bots_clan/muscleman.json | 35 -- bots/bots_clan/newbie.json | 35 -- bots/bots_clan/nibbler.json | 46 +- bots/bots_clan/nihlathak.json | 40 +- bots/bots_clan/nin.json | 35 -- bots/bots_clan/nitara.json | 35 -- bots/bots_clan/nohope.json | 35 -- bots/bots_clan/onaga.json | 35 -- bots/bots_clan/orbb.json | 46 +- bots/bots_clan/orinboyd.json | 40 +- bots/bots_clan/ostmor.json | 48 +- bots/bots_clan/oysterhead.json | 46 +- bots/bots_clan/paradox.json | 35 -- bots/bots_clan/patriot.json | 35 -- bots/bots_clan/pewdiepie.json | 48 +- bots/bots_clan/pindleskin.json | 35 -- bots/bots_clan/radament.json | 46 +- bots/bots_clan/rain.json | 35 -- bots/bots_clan/rakanishu.json | 46 +- bots/bots_clan/ramrod.json | 50 +- bots/bots_clan/ranger.json | 35 -- bots/bots_clan/ratbot.json | 35 -- bots/bots_clan/rayquick.json | 35 -- bots/bots_clan/razor.json | 35 -- bots/bots_clan/richard.json | 35 -- bots/bots_clan/rocky.json | 46 +- bots/bots_clan/royen.json | 35 -- bots/bots_clan/sareena.json | 48 +- bots/bots_clan/sarge.json | 46 +- bots/bots_clan/sarina.json | 35 -- bots/bots_clan/sean.json | 46 +- bots/bots_clan/sektor.json | 50 +- bots/bots_clan/sgtcappa.json | 48 +- bots/bots_clan/shaokahn.json | 35 -- bots/bots_clan/sheeva.json | 50 +- bots/bots_clan/shujinko.json | 35 -- bots/bots_clan/si.json | 48 +- bots/bots_clan/sigil.json | 46 +- bots/bots_clan/skorgan.json | 38 +- bots/bots_clan/slice.json | 52 +- bots/bots_clan/smithy.json | 48 +- bots/bots_clan/smoke.json | 35 -- bots/bots_clan/snaps.json | 46 +- bots/bots_clan/socks.json | 52 +- bots/bots_clan/sorlag.json | 48 +- bots/bots_clan/subzero.json | 35 -- bots/bots_clan/summoner.json | 35 -- bots/bots_clan/sundog.json | 35 -- bots/bots_clan/t800.json | 48 +- bots/bots_clan/tank.json | 35 -- bots/bots_clan/tarnen.json | 48 +- bots/bots_clan/tarnok.json | 44 +- bots/bots_clan/taven.json | 35 -- bots/bots_clan/teabag.json | 44 +- bots/bots_clan/terminator.json | 50 +- bots/bots_clan/thalath.json | 35 -- bots/bots_clan/theboss.json | 35 -- bots/bots_clan/thetulip.json | 35 -- bots/bots_clan/threash.json | 35 -- bots/bots_clan/tomsteele.json | 35 -- bots/bots_clan/tormentor.json | 40 +- bots/bots_clan/tremor.json | 44 +- bots/bots_clan/trench.json | 46 +- bots/bots_clan/turkish.json | 40 +- bots/bots_clan/uriel.json | 46 +- bots/bots_clan/uzaytron.json | 48 +- bots/bots_clan/vedro.json | 40 +- bots/bots_clan/venz.json | 52 +- bots/bots_clan/vera.json | 35 -- bots/bots_clan/waxman.json | 35 -- bots/bots_clan/wayofthe.json | 42 +- bots/bots_clan/wesleysnipes.json | 35 -- bots/bots_clan/yarbro.json | 38 +- bots/bots_pub/2dcitizen.json | 40 +- bots/bots_pub/_gholem_.json | 42 +- bots/bots_pub/_glich.json | 44 +- bots/bots_pub/_nixxy_.json | 44 +- bots/bots_pub/amadi.json | 44 +- bots/bots_pub/amu.json | 35 -- bots/bots_pub/anarki.json | 35 -- bots/bots_pub/andromeed.json | 48 +- bots/bots_pub/ant__dog.json | 44 +- bots/bots_pub/argo.json | 44 +- bots/bots_pub/arklon.json | 35 -- bots/bots_pub/ashrah.json | 35 -- bots/bots_pub/backfire.json | 40 +- bots/bots_pub/bacon.json | 35 -- bots/bots_pub/badsam.json | 35 -- bots/bots_pub/baraka.json | 44 +- bots/bots_pub/barbarian.json | 44 +- bots/bots_pub/bean.json | 35 -- bots/bots_pub/beelz.json | 35 -- bots/bots_pub/belokk.json | 35 -- bots/bots_pub/benarcher.json | 36 +- bots/bots_pub/berserker.json | 48 +- bots/bots_pub/bigmack.json | 44 +- bots/bots_pub/biker.json | 35 -- bots/bots_pub/bishibosh.json | 35 -- bots/bots_pub/blackadder.json | 44 +- bots/bots_pub/blaze.json | 40 +- bots/bots_pub/bolund.json | 35 -- bots/bots_pub/boneash.json | 35 -- bots/bots_pub/bonebreak.json | 35 -- bots/bots_pub/booker.json | 35 -- bots/bots_pub/bortack.json | 44 +- bots/bots_pub/bremm.json | 35 -- bots/bots_pub/brixos.json | 35 -- bots/bots_pub/brofist.json | 46 +- bots/bots_pub/brohn.json | 46 +- bots/bots_pub/bve.json | 46 +- bots/bots_pub/cadaver.json | 44 +- bots/bots_pub/carpenter.json | 48 +- bots/bots_pub/chan.json | 44 +- bots/bots_pub/church.json | 35 -- bots/bots_pub/cledus.json | 35 -- bots/bots_pub/clion.json | 48 +- bots/bots_pub/cobra.json | 35 -- bots/bots_pub/corrus.json | 35 -- bots/bots_pub/countess.json | 35 -- bots/bots_pub/cowking.json | 35 -- bots/bots_pub/crash.json | 46 +- bots/bots_pub/cybr.json | 42 +- bots/bots_pub/cyrax.json | 48 +- bots/bots_pub/daegon.json | 35 -- bots/bots_pub/dairou.json | 35 -- bots/bots_pub/darrius.json | 40 +- bots/bots_pub/death.json | 42 +- bots/bots_pub/deusvault.json | 42 +- bots/bots_pub/digz.json | 46 +- bots/bots_pub/dolin.json | 35 -- bots/bots_pub/dominator.json | 35 -- bots/bots_pub/doom.json | 46 +- bots/bots_pub/drago.json | 35 -- bots/bots_pub/dragoooon.json | 35 -- bots/bots_pub/drillbit.json | 42 +- bots/bots_pub/drjay.json | 35 -- bots/bots_pub/dutch.json | 35 -- bots/bots_pub/dvorah.json | 35 -- bots/bots_pub/ellanil.json | 35 -- bots/bots_pub/enforcer.json | 35 -- bots/bots_pub/ermac.json | 35 -- bots/bots_pub/evo89.json | 35 -- bots/bots_pub/flamespike.json | 35 -- bots/bots_pub/flint.json | 44 +- bots/bots_pub/frenchy.json | 35 -- bots/bots_pub/fullmonty.json | 42 +- bots/bots_pub/funkanik.json | 35 -- bots/bots_pub/gali.json | 35 -- bots/bots_pub/galul.json | 35 -- bots/bots_pub/garbok.json | 44 +- bots/bots_pub/geleb.json | 40 +- bots/bots_pub/george.json | 35 -- bots/bots_pub/ghostbl4de.json | 35 -- bots/bots_pub/gladiator.json | 40 +- bots/bots_pub/gogman.json | 42 +- bots/bots_pub/grimm.json | 35 -- bots/bots_pub/grog.json | 35 -- bots/bots_pub/grunt.json | 35 -- bots/bots_pub/gun.json | 46 +- bots/bots_pub/hal9000.json | 35 -- bots/bots_pub/halfwit.json | 35 -- bots/bots_pub/hambone.json | 35 -- bots/bots_pub/havo.json | 35 -- bots/bots_pub/hellhound.json | 35 -- bots/bots_pub/helmut.json | 35 -- bots/bots_pub/himself.json | 35 -- bots/bots_pub/hotaru.json | 42 +- bots/bots_pub/hozef8.json | 42 +- bots/bots_pub/hunter.json | 35 -- bots/bots_pub/icexo.json | 46 +- bots/bots_pub/imentu.json | 42 +- bots/bots_pub/itzchamp.json | 50 +- bots/bots_pub/ivandrago.json | 46 +- bots/bots_pub/jacksaxon.json | 42 +- bots/bots_pub/jade.json | 44 +- bots/bots_pub/jax.json | 38 +- bots/bots_pub/jcdenton.json | 35 -- bots/bots_pub/jjmcquade.json | 36 +- bots/bots_pub/joejoiner.json | 35 -- bots/bots_pub/joesanto.json | 35 -- bots/bots_pub/joetanto.json | 40 +- bots/bots_pub/kaa.json | 35 -- bots/bots_pub/kabal.json | 35 -- bots/bots_pub/kalidor.json | 38 +- bots/bots_pub/kenshi.json | 35 -- bots/bots_pub/kintaro.json | 40 +- bots/bots_pub/kira.json | 46 +- bots/bots_pub/kitana.json | 38 +- bots/bots_pub/kole.json | 35 -- bots/bots_pub/kollector.json | 35 -- bots/bots_pub/korv.json | 35 -- bots/bots_pub/kotalkahn.json | 42 +- bots/bots_pub/kungjin.json | 42 +- bots/bots_pub/kuri.json | 40 +- bots/bots_pub/lawrence.json | 35 -- bots/bots_pub/limei.json | 40 +- bots/bots_pub/littleg.json | 44 +- bots/bots_pub/lonewolf.json | 46 +- bots/bots_pub/milius.json | 42 +- bots/bots_pub/mindelos.json | 42 +- bots/bots_pub/moloch.json | 35 -- bots/bots_pub/mongrel.json | 44 +- bots/bots_pub/monk.json | 36 +- bots/bots_pub/monty.json | 44 +- bots/bots_pub/morgan.json | 42 +- bots/bots_pub/morgriff.json | 48 +- bots/bots_pub/mrbadger.json | 42 +- bots/bots_pub/mungri.json | 35 -- bots/bots_pub/mynx.json | 44 +- bots/bots_pub/nandet.json | 46 +- bots/bots_pub/nickgunar.json | 50 +- bots/bots_pub/nightwolf.json | 35 -- bots/bots_pub/noammo.json | 48 +- bots/bots_pub/noogie.json | 44 +- bots/bots_pub/nundak.json | 44 +- bots/bots_pub/phobos.json | 35 -- bots/bots_pub/potaytoe.json | 42 +- bots/bots_pub/pyroyetti.json | 42 +- bots/bots_pub/quanchi.json | 44 +- bots/bots_pub/raiden.json | 44 +- bots/bots_pub/razor.json | 40 +- bots/bots_pub/reddog.json | 44 +- bots/bots_pub/reiko.json | 40 +- bots/bots_pub/replicant.json | 42 +- bots/bots_pub/reptile.json | 44 +- bots/bots_pub/riftwraith.json | 40 +- bots/bots_pub/ruslan.json | 35 -- bots/bots_pub/ryder.json | 35 -- bots/bots_pub/ryver.json | 35 -- bots/bots_pub/saboy.json | 35 -- bots/bots_pub/sagel.json | 36 +- bots/bots_pub/samdecker.json | 48 +- bots/bots_pub/scorpion.json | 35 -- bots/bots_pub/seankane.json | 35 -- bots/bots_pub/shinnok.json | 35 -- bots/bots_pub/siege.json | 35 -- bots/bots_pub/sindel.json | 36 +- bots/bots_pub/skarlet.json | 48 +- bots/bots_pub/slash.json | 44 +- bots/bots_pub/spar.json | 35 -- bots/bots_pub/specialed.json | 35 -- bots/bots_pub/spocket.json | 35 -- bots/bots_pub/steampug.json | 35 -- bots/bots_pub/stinger.json | 42 +- bots/bots_pub/stormtree.json | 44 +- bots/bots_pub/stripe.json | 48 +- bots/bots_pub/strogg.json | 42 +- bots/bots_pub/strom.json | 35 -- bots/bots_pub/stryker.json | 35 -- bots/bots_pub/stud.json | 35 -- bots/bots_pub/t1000.json | 36 +- bots/bots_pub/t_bon.json | 44 +- bots/bots_pub/tahlkora.json | 48 +- bots/bots_pub/tankjr.json | 44 +- bots/bots_pub/tanya.json | 46 +- bots/bots_pub/tanz.json | 42 +- bots/bots_pub/tao.json | 48 +- bots/bots_pub/tatu.json | 48 +- bots/bots_pub/technician.json | 38 +- bots/bots_pub/thedean.json | 46 +- bots/bots_pub/theeraser.json | 44 +- bots/bots_pub/thegerman.json | 44 +- bots/bots_pub/theonly.json | 44 +- bots/bots_pub/theway.json | 42 +- bots/bots_pub/thor.json | 35 -- bots/bots_pub/tiano.json | 35 -- bots/bots_pub/tommix.json | 46 +- bots/bots_pub/toorc.json | 35 -- bots/bots_pub/treehead.json | 44 +- bots/bots_pub/triborg.json | 35 -- bots/bots_pub/vanoss.json | 48 +- bots/bots_pub/venom.json | 35 -- bots/bots_pub/vey.json | 40 +- bots/bots_pub/vic.json | 50 +- bots/bots_pub/visor.json | 46 +- bots/bots_pub/vizier.json | 35 -- bots/bots_pub/vokur.json | 40 +- bots/bots_pub/walter.json | 35 -- bots/bots_pub/warchild.json | 35 -- bots/bots_pub/wens.json | 46 +- bots/bots_pub/whyme.json | 46 +- bots/bots_pub/willitz.json | 44 +- bots/bots_pub/wrack.json | 40 +- bots/bots_pub/wreckage.json | 35 -- bots/bots_pub/wyand.json | 35 -- bots/bots_pub/wysyyy.json | 48 +- bots/bots_pub/xaero.json | 42 +- bots/bots_pub/zarna.json | 35 -- bots/{team_names.txt => clan_names.txt} | 0 bots/other.txt | 659 ------------------------ bots/skin_names.txt | 241 +++++++++ meson.build | 2 +- 417 files changed, 5216 insertions(+), 12217 deletions(-) delete mode 100644 bots/bots_clan/aegarond.json delete mode 100644 bots/bots_clan/aimless.json delete mode 100644 bots/bots_clan/apem.json delete mode 100644 bots/bots_clan/applee.json delete mode 100644 bots/bots_clan/blythe.json delete mode 100644 bots/bots_clan/boy.json delete mode 100644 bots/bots_clan/brad.json delete mode 100644 bots/bots_clan/cerberon.json delete mode 100644 bots/bots_clan/cetrion.json delete mode 100644 bots/bots_clan/chipp_.json delete mode 100644 bots/bots_clan/choppy.json delete mode 100644 bots/bots_clan/coldcrow.json delete mode 100644 bots/bots_clan/coldworm.json delete mode 100644 bots/bots_clan/conan.json delete mode 100644 bots/bots_clan/cowchop.json delete mode 100644 bots/bots_clan/cruton.json delete mode 100644 bots/bots_clan/dacfarren.json delete mode 100644 bots/bots_clan/daemia.json delete mode 100644 bots/bots_clan/doc.json delete mode 100644 bots/bots_clan/edwig.json delete mode 100644 bots/bots_clan/exbee.json delete mode 100644 bots/bots_clan/fangskin.json delete mode 100644 bots/bots_clan/ferratorr.json delete mode 100644 bots/bots_clan/frankdux.json delete mode 100644 bots/bots_clan/frost.json delete mode 100644 bots/bots_clan/geras.json delete mode 100644 bots/bots_clan/gigavoltz.json delete mode 100644 bots/bots_clan/griswold.json delete mode 100644 bots/bots_clan/halfbaked.json delete mode 100644 bots/bots_clan/hammerhead.json delete mode 100644 bots/bots_clan/havik.json delete mode 100644 bots/bots_clan/hecker.json delete mode 100644 bots/bots_clan/hsuhao.json delete mode 100644 bots/bots_clan/hurricane.json delete mode 100644 bots/bots_clan/icehawk.json delete mode 100644 bots/bots_clan/itixoid.json delete mode 100644 bots/bots_clan/jarek.json delete mode 100644 bots/bots_clan/jarno.json delete mode 100644 bots/bots_clan/jce.json delete mode 100644 bots/bots_clan/johnluger.json delete mode 100644 bots/bots_clan/johnrambo.json delete mode 100644 bots/bots_clan/kai.json delete mode 100644 bots/bots_clan/khameleon.json delete mode 100644 bots/bots_clan/kippy.json delete mode 100644 bots/bots_clan/kitlatura.json delete mode 100644 bots/bots_clan/kobra.json delete mode 100644 bots/bots_clan/kronika.json delete mode 100644 bots/bots_clan/kunglao.json delete mode 100644 bots/bots_clan/lemmie.json delete mode 100644 bots/bots_clan/lucy.json delete mode 100644 bots/bots_clan/maffer.json delete mode 100644 bots/bots_clan/major.json delete mode 100644 bots/bots_clan/makron.json delete mode 100644 bots/bots_clan/manny.json delete mode 100644 bots/bots_clan/marku.json delete mode 100644 bots/bots_clan/mavado.json delete mode 100644 bots/bots_clan/meat.json delete mode 100644 bots/bots_clan/mee2.json delete mode 100644 bots/bots_clan/missnrun.json delete mode 100644 bots/bots_clan/mrgoodkat.json delete mode 100644 bots/bots_clan/mrsuave.json delete mode 100644 bots/bots_clan/muscleman.json delete mode 100644 bots/bots_clan/newbie.json delete mode 100644 bots/bots_clan/nin.json delete mode 100644 bots/bots_clan/nitara.json delete mode 100644 bots/bots_clan/nohope.json delete mode 100644 bots/bots_clan/onaga.json delete mode 100644 bots/bots_clan/paradox.json delete mode 100644 bots/bots_clan/patriot.json delete mode 100644 bots/bots_clan/pindleskin.json delete mode 100644 bots/bots_clan/rain.json delete mode 100644 bots/bots_clan/ranger.json delete mode 100644 bots/bots_clan/ratbot.json delete mode 100644 bots/bots_clan/rayquick.json delete mode 100644 bots/bots_clan/razor.json delete mode 100644 bots/bots_clan/richard.json delete mode 100644 bots/bots_clan/royen.json delete mode 100644 bots/bots_clan/sarina.json delete mode 100644 bots/bots_clan/shaokahn.json delete mode 100644 bots/bots_clan/shujinko.json delete mode 100644 bots/bots_clan/smoke.json delete mode 100644 bots/bots_clan/subzero.json delete mode 100644 bots/bots_clan/summoner.json delete mode 100644 bots/bots_clan/sundog.json delete mode 100644 bots/bots_clan/tank.json delete mode 100644 bots/bots_clan/taven.json delete mode 100644 bots/bots_clan/thalath.json delete mode 100644 bots/bots_clan/theboss.json delete mode 100644 bots/bots_clan/thetulip.json delete mode 100644 bots/bots_clan/threash.json delete mode 100644 bots/bots_clan/tomsteele.json delete mode 100644 bots/bots_clan/vera.json delete mode 100644 bots/bots_clan/waxman.json delete mode 100644 bots/bots_clan/wesleysnipes.json delete mode 100644 bots/bots_pub/amu.json delete mode 100644 bots/bots_pub/anarki.json delete mode 100644 bots/bots_pub/arklon.json delete mode 100644 bots/bots_pub/ashrah.json delete mode 100644 bots/bots_pub/bacon.json delete mode 100644 bots/bots_pub/badsam.json delete mode 100644 bots/bots_pub/bean.json delete mode 100644 bots/bots_pub/beelz.json delete mode 100644 bots/bots_pub/belokk.json delete mode 100644 bots/bots_pub/biker.json delete mode 100644 bots/bots_pub/bishibosh.json delete mode 100644 bots/bots_pub/bolund.json delete mode 100644 bots/bots_pub/boneash.json delete mode 100644 bots/bots_pub/bonebreak.json delete mode 100644 bots/bots_pub/booker.json delete mode 100644 bots/bots_pub/bremm.json delete mode 100644 bots/bots_pub/brixos.json delete mode 100644 bots/bots_pub/church.json delete mode 100644 bots/bots_pub/cledus.json delete mode 100644 bots/bots_pub/cobra.json delete mode 100644 bots/bots_pub/corrus.json delete mode 100644 bots/bots_pub/countess.json delete mode 100644 bots/bots_pub/cowking.json delete mode 100644 bots/bots_pub/daegon.json delete mode 100644 bots/bots_pub/dairou.json delete mode 100644 bots/bots_pub/dolin.json delete mode 100644 bots/bots_pub/dominator.json delete mode 100644 bots/bots_pub/drago.json delete mode 100644 bots/bots_pub/dragoooon.json delete mode 100644 bots/bots_pub/drjay.json delete mode 100644 bots/bots_pub/dutch.json delete mode 100644 bots/bots_pub/dvorah.json delete mode 100644 bots/bots_pub/ellanil.json delete mode 100644 bots/bots_pub/enforcer.json delete mode 100644 bots/bots_pub/ermac.json delete mode 100644 bots/bots_pub/evo89.json delete mode 100644 bots/bots_pub/flamespike.json delete mode 100644 bots/bots_pub/frenchy.json delete mode 100644 bots/bots_pub/funkanik.json delete mode 100644 bots/bots_pub/gali.json delete mode 100644 bots/bots_pub/galul.json delete mode 100644 bots/bots_pub/george.json delete mode 100644 bots/bots_pub/ghostbl4de.json delete mode 100644 bots/bots_pub/grimm.json delete mode 100644 bots/bots_pub/grog.json delete mode 100644 bots/bots_pub/grunt.json delete mode 100644 bots/bots_pub/hal9000.json delete mode 100644 bots/bots_pub/halfwit.json delete mode 100644 bots/bots_pub/hambone.json delete mode 100644 bots/bots_pub/havo.json delete mode 100644 bots/bots_pub/hellhound.json delete mode 100644 bots/bots_pub/helmut.json delete mode 100644 bots/bots_pub/himself.json delete mode 100644 bots/bots_pub/hunter.json delete mode 100644 bots/bots_pub/jcdenton.json delete mode 100644 bots/bots_pub/joejoiner.json delete mode 100644 bots/bots_pub/joesanto.json delete mode 100644 bots/bots_pub/kaa.json delete mode 100644 bots/bots_pub/kabal.json delete mode 100644 bots/bots_pub/kenshi.json delete mode 100644 bots/bots_pub/kole.json delete mode 100644 bots/bots_pub/kollector.json delete mode 100644 bots/bots_pub/korv.json delete mode 100644 bots/bots_pub/lawrence.json delete mode 100644 bots/bots_pub/moloch.json delete mode 100644 bots/bots_pub/mungri.json delete mode 100644 bots/bots_pub/nightwolf.json delete mode 100644 bots/bots_pub/phobos.json delete mode 100644 bots/bots_pub/ruslan.json delete mode 100644 bots/bots_pub/ryder.json delete mode 100644 bots/bots_pub/ryver.json delete mode 100644 bots/bots_pub/saboy.json delete mode 100644 bots/bots_pub/scorpion.json delete mode 100644 bots/bots_pub/seankane.json delete mode 100644 bots/bots_pub/shinnok.json delete mode 100644 bots/bots_pub/siege.json delete mode 100644 bots/bots_pub/spar.json delete mode 100644 bots/bots_pub/specialed.json delete mode 100644 bots/bots_pub/spocket.json delete mode 100644 bots/bots_pub/steampug.json delete mode 100644 bots/bots_pub/strom.json delete mode 100644 bots/bots_pub/stryker.json delete mode 100644 bots/bots_pub/stud.json delete mode 100644 bots/bots_pub/thor.json delete mode 100644 bots/bots_pub/tiano.json delete mode 100644 bots/bots_pub/toorc.json delete mode 100644 bots/bots_pub/triborg.json delete mode 100644 bots/bots_pub/venom.json delete mode 100644 bots/bots_pub/vizier.json delete mode 100644 bots/bots_pub/walter.json delete mode 100644 bots/bots_pub/warchild.json delete mode 100644 bots/bots_pub/wreckage.json delete mode 100644 bots/bots_pub/wyand.json delete mode 100644 bots/bots_pub/zarna.json rename bots/{team_names.txt => clan_names.txt} (100%) delete mode 100644 bots/other.txt create mode 100644 bots/skin_names.txt diff --git a/bots/botmaker.py b/bots/botmaker.py index 91627c61d..c478eae33 100644 --- a/bots/botmaker.py +++ b/bots/botmaker.py @@ -12,14 +12,11 @@ def generate_random_value(): return round(random.normalvariate(0.5, 0.2), 1) -# Open and read the file -with open('other.txt', 'r') as file: - for line in file: - name = line.strip() # Remove newline characters and whitespace - length = len(name) +with open('skin_names.txt', 'r') as file: + skin_names = [line.strip() for line in file] - # Open and read the file -with open('other.txt', 'r') as file: +# Open and read the file +with open('bot_names.txt', 'r') as file: for line in file: name = line.strip() # Remove newline characters and whitespace length = len(name) @@ -32,7 +29,7 @@ def generate_random_value(): bot_profile = { "name": name, "clan_id": random.randint(1, 110) if add_to_clan else None, - "skin": "male/indy", + "skin": random.choice(skin_names), # Randomly select a skin "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, "coreTraits": {key: generate_random_value() for key in ["aggressiveness", "teamwork", "curiosity", "aimingSkill", "reactionTime", "communicationFreq", "communicationTone", "movementStyle", "objectiveFocus"]} diff --git a/bots/bots_clan/_pixxy_.json b/bots/bots_clan/_pixxy_.json index 789ff3534..53c7edec3 100644 --- a/bots/bots_clan/_pixxy_.json +++ b/bots/bots_clan/_pixxy_.json @@ -1,35 +1,35 @@ { "name": "_*PixxY*_", - "clan_id": 73, - "skin": "male/indy", + "clan_id": 14, + "skin": "sas/ctf_b", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.8, "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.2, - "grenades": 0.6 + "mp5": 0.6, + "m4": 0.0, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.3, + "vest": 0.4, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.4, "slippers": 0.3, - "bandolier": 0.6 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.1, - "reactionTime": 0.1, - "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.6 + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/aegarond.json b/bots/bots_clan/aegarond.json deleted file mode 100644 index 0ef3e5fb6..000000000 --- a/bots/bots_clan/aegarond.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Aegarond", - "clan_id": 38, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.1, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 1.0 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/aimless.json b/bots/bots_clan/aimless.json deleted file mode 100644 index 43ceb5013..000000000 --- a/bots/bots_clan/aimless.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Aimless", - "clan_id": 62, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.4, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.7, - "reactionTime": 0.8, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/angel.json b/bots/bots_clan/angel.json index f97c51695..498fedcec 100644 --- a/bots/bots_clan/angel.json +++ b/bots/bots_clan/angel.json @@ -1,35 +1,35 @@ { "name": "Angel", - "clan_id": 62, - "skin": "male/indy", + "clan_id": 30, + "skin": "male/blues", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.7, - "hc": 0.4, + "mp5": -0.1, + "m4": 0.4, + "m3": 0.6, + "hc": 0.3, "sniper": 0.3, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.6 + "vest": 0.5, + "helm": 0.1, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.8, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "movementStyle": 0.8, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/anodized.json b/bots/bots_clan/anodized.json index 8dc0d859f..2a1e5197a 100644 --- a/bots/bots_clan/anodized.json +++ b/bots/bots_clan/anodized.json @@ -1,35 +1,35 @@ { "name": "Anodized", - "clan_id": 58, - "skin": "male/indy", + "clan_id": 41, + "skin": "actionmale/jungseal", "weaponPreferences": { - "mk23": 0.8, + "mk23": 0.5, "dual_mk23": 0.6, "mp5": 0.3, - "m4": 0.7, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.8 + "m4": 0.6, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.5, "helm": 0.5, - "laser": 0.9, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.4 + "laser": 0.6, + "silencer": 0.4, + "slippers": -0.1, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/anton.json b/bots/bots_clan/anton.json index ffdefb5e3..2d986b70a 100644 --- a/bots/bots_clan/anton.json +++ b/bots/bots_clan/anton.json @@ -1,35 +1,35 @@ { "name": "Anton", - "clan_id": 108, - "skin": "male/indy", + "clan_id": 46, + "skin": "male/hellspawn", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.6, + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.6, + "hc": 0.8, "sniper": 0.3, "knives": 0.6, - "grenades": 0.8 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, + "vest": 0.1, + "helm": 0.5, "laser": 0.6, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.9 + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.3, "teamwork": 0.7, - "curiosity": 0.1, - "aimingSkill": 1.0, + "curiosity": 1.0, + "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 1.0, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.1 + "communicationFreq": 0.3, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/apem.json b/bots/bots_clan/apem.json deleted file mode 100644 index 448586268..000000000 --- a/bots/bots_clan/apem.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "ApeM", - "clan_id": 85, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.3, - "hc": 0.9, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.8, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/applee.json b/bots/bots_clan/applee.json deleted file mode 100644 index 2f4156805..000000000 --- a/bots/bots_clan/applee.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Applee", - "clan_id": 2, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.4, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.5, - "grenades": 1.0 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.8, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.2, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/badjuju.json b/bots/bots_clan/badjuju.json index 73af61e26..adc1ae9ed 100644 --- a/bots/bots_clan/badjuju.json +++ b/bots/bots_clan/badjuju.json @@ -1,35 +1,35 @@ { "name": "Bad JuJu", - "clan_id": 8, - "skin": "male/indy", + "clan_id": 20, + "skin": "male/nut", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.3 + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.9, - "laser": 0.0, - "silencer": 0.4, + "vest": 0.7, + "helm": -0.2, + "laser": 0.2, + "silencer": 0.7, "slippers": 0.4, - "bandolier": 0.4 + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.9, - "curiosity": 0.7, - "aimingSkill": 0.9, - "reactionTime": 0.9, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "teamwork": 0.4, + "curiosity": 0.1, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.6, + "communicationTone": 0.1, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/bitterman.json b/bots/bots_clan/bitterman.json index c291c190f..053a4ffb3 100644 --- a/bots/bots_clan/bitterman.json +++ b/bots/bots_clan/bitterman.json @@ -1,35 +1,35 @@ { "name": "Bitterman", - "clan_id": 54, - "skin": "male/indy", + "clan_id": 59, + "skin": "messiah/neo", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 1.0, - "mp5": -0.0, - "m4": 0.5, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.3, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.3, + "helm": -0.0, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.2, + "aggressiveness": 0.8, "teamwork": 0.3, - "curiosity": 0.1, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.2, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.3, "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "movementStyle": 0.4, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/bloodwitch.json b/bots/bots_clan/bloodwitch.json index 84708f20b..e1287b20f 100644 --- a/bots/bots_clan/bloodwitch.json +++ b/bots/bots_clan/bloodwitch.json @@ -1,35 +1,35 @@ { "name": "Bloodwitch", - "clan_id": 32, - "skin": "male/indy", + "clan_id": 78, + "skin": "male/babarracuda", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.4 + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.4 + "vest": 0.8, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, + "aggressiveness": 0.7, + "teamwork": 0.7, "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.9 + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/blythe.json b/bots/bots_clan/blythe.json deleted file mode 100644 index 8ff16ded8..000000000 --- a/bots/bots_clan/blythe.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Blythe", - "clan_id": 56, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.1, - "hc": 0.4, - "sniper": 0.9, - "knives": 0.4, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/bobross.json b/bots/bots_clan/bobross.json index 82e30cd61..d291a166f 100644 --- a/bots/bots_clan/bobross.json +++ b/bots/bots_clan/bobross.json @@ -1,35 +1,35 @@ { "name": "Bob Ross", - "clan_id": 27, - "skin": "male/indy", + "clan_id": 10, + "skin": "sydney/blonde", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.6, + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.2, "m4": 0.9, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.7 + "m3": 0.8, + "hc": 0.6, + "sniper": -0.0, + "knives": 0.2, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 1.0, + "vest": 0.4, + "helm": 0.4, + "laser": 0.1, + "silencer": 0.5, "slippers": 0.5, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.1, "teamwork": 0.6, - "curiosity": 0.4, + "curiosity": 0.5, "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": -0.0, - "objectiveFocus": 0.4 + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.8, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/bones.json b/bots/bots_clan/bones.json index 05a6380b4..625ea625b 100644 --- a/bots/bots_clan/bones.json +++ b/bots/bots_clan/bones.json @@ -1,35 +1,35 @@ { "name": "Bones", - "clan_id": 91, - "skin": "male/indy", + "clan_id": 87, + "skin": "male/SEALU", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.6, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.3, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, + "vest": 0.6, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.6, "slippers": 0.4, - "bandolier": 0.4 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, + "aggressiveness": 0.1, + "teamwork": 0.1, "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.3 + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/boy.json b/bots/bots_clan/boy.json deleted file mode 100644 index d6194a882..000000000 --- a/bots/bots_clan/boy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Boy", - "clan_id": 87, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/brad.json b/bots/bots_clan/brad.json deleted file mode 100644 index ebc2da503..000000000 --- a/bots/bots_clan/brad.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Brad", - "clan_id": 12, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.9, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.1, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/broz.json b/bots/bots_clan/broz.json index 5cb5a1f07..17a0f5108 100644 --- a/bots/bots_clan/broz.json +++ b/bots/bots_clan/broz.json @@ -1,35 +1,35 @@ { "name": "Broz", - "clan_id": 87, - "skin": "male/indy", + "clan_id": 9, + "skin": "sas/ctf_b", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 1.0, - "mp5": 0.2, - "m4": 0.6, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.7, "m3": 0.6, - "hc": 0.3, - "sniper": 0.5, + "hc": 0.2, + "sniper": 0.6, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 1.0, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.5, + "aggressiveness": 0.1, + "teamwork": 0.9, + "curiosity": 0.2, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/bullseye.json b/bots/bots_clan/bullseye.json index ff6d9353a..180b38d37 100644 --- a/bots/bots_clan/bullseye.json +++ b/bots/bots_clan/bullseye.json @@ -1,35 +1,35 @@ { "name": "Bullseye", - "clan_id": 102, - "skin": "male/indy", + "clan_id": 67, + "skin": "male/pimp", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, + "mk23": 0.4, + "dual_mk23": 0.6, "mp5": 0.4, "m4": 0.5, "m3": 0.6, - "hc": 0.6, + "hc": 0.4, "sniper": 0.6, - "knives": 0.2, - "grenades": 0.1 + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.3, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.5, + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "communicationFreq": 0.9, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/cerberon.json b/bots/bots_clan/cerberon.json deleted file mode 100644 index 339f90bad..000000000 --- a/bots/bots_clan/cerberon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cerberon", - "clan_id": 92, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/cetrion.json b/bots/bots_clan/cetrion.json deleted file mode 100644 index 51f72daf4..000000000 --- a/bots/bots_clan/cetrion.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cetrion", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.8, - "knives": 0.0, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 1.0, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/chameleon.json b/bots/bots_clan/chameleon.json index dbd976f9a..02a5589e2 100644 --- a/bots/bots_clan/chameleon.json +++ b/bots/bots_clan/chameleon.json @@ -1,35 +1,35 @@ { "name": "Chameleon", - "clan_id": 90, - "skin": "male/indy", + "clan_id": 65, + "skin": "female/tankgirl", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.7, + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.4, "m4": 0.4, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.7, + "m3": 0.6, + "hc": 0.4, + "sniper": 0.7, + "knives": 0.6, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.3, "helm": 0.5, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.6 + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.7, "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.3, "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.5 + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/chess.json b/bots/bots_clan/chess.json index 3c6633e23..b4f3b2400 100644 --- a/bots/bots_clan/chess.json +++ b/bots/bots_clan/chess.json @@ -1,35 +1,35 @@ { "name": "Chess", - "clan_id": 85, - "skin": "male/indy", + "clan_id": 67, + "skin": "male/jax-jaguar", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.6, - "m3": 1.1, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.5 + "mk23": 0.9, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.8, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.6, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.1, - "reactionTime": 0.5, + "aggressiveness": 1.1, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.3, "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.6 + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/chipp_.json b/bots/bots_clan/chipp_.json deleted file mode 100644 index 0ab6b187d..000000000 --- a/bots/bots_clan/chipp_.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "chipp_", - "clan_id": 88, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.4, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.1, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/choppy.json b/bots/bots_clan/choppy.json deleted file mode 100644 index 2f669b92c..000000000 --- a/bots/bots_clan/choppy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "^^ChoppY^^", - "clan_id": 44, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 1.0, - "m4": 0.3, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/coldcrow.json b/bots/bots_clan/coldcrow.json deleted file mode 100644 index 797f01fb9..000000000 --- a/bots/bots_clan/coldcrow.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Coldcrow", - "clan_id": 46, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/coldworm.json b/bots/bots_clan/coldworm.json deleted file mode 100644 index 8069225b4..000000000 --- a/bots/bots_clan/coldworm.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Coldworm", - "clan_id": 4, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/colt.json b/bots/bots_clan/colt.json index e53f03616..3f968e4a5 100644 --- a/bots/bots_clan/colt.json +++ b/bots/bots_clan/colt.json @@ -1,35 +1,35 @@ { "name": "Colt", - "clan_id": 9, - "skin": "male/indy", + "clan_id": 84, + "skin": "male/copper", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, + "mk23": 0.3, + "dual_mk23": 0.6, "mp5": 0.6, "m4": 0.5, - "m3": 0.1, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.8, + "vest": 0.5, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.5, "slippers": 0.7, - "bandolier": 0.1 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.4, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/conan.json b/bots/bots_clan/conan.json deleted file mode 100644 index 3fce206d7..000000000 --- a/bots/bots_clan/conan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Conan", - "clan_id": 48, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.9, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.1, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/corpsefire.json b/bots/bots_clan/corpsefire.json index 5cf4add90..da6dcece3 100644 --- a/bots/bots_clan/corpsefire.json +++ b/bots/bots_clan/corpsefire.json @@ -1,35 +1,35 @@ { "name": "Corpsefire", - "clan_id": 30, - "skin": "male/indy", + "clan_id": 92, + "skin": "messiah/ctf_b", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": -0.1, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.1, + "dual_mk23": 0.6, + "mp5": 0.9, + "m4": 0.9, + "m3": 0.5, "hc": 0.8, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.8 + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.7, + "vest": 0.6, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.5, "slippers": 0.6, - "bandolier": 0.5 + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.8, "curiosity": 0.7, "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.8 + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/cowchop.json b/bots/bots_clan/cowchop.json deleted file mode 100644 index dc0eaacad..000000000 --- a/bots/bots_clan/cowchop.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "CowChop", - "clan_id": 20, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.4, - "slippers": 1.0, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.7, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/cruton.json b/bots/bots_clan/cruton.json deleted file mode 100644 index 4aa497ec8..000000000 --- a/bots/bots_clan/cruton.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cruton", - "clan_id": 28, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.2, - "grenades": -0.0 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.1, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/dacfarren.json b/bots/bots_clan/dacfarren.json deleted file mode 100644 index ebb842b80..000000000 --- a/bots/bots_clan/dacfarren.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dac Farren", - "clan_id": 32, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/daemia.json b/bots/bots_clan/daemia.json deleted file mode 100644 index 1241e72cf..000000000 --- a/bots/bots_clan/daemia.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Daemia", - "clan_id": 108, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.1, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.9, - "knives": 0.8, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/darkkahn.json b/bots/bots_clan/darkkahn.json index 9b98cc776..ac97f94b1 100644 --- a/bots/bots_clan/darkkahn.json +++ b/bots/bots_clan/darkkahn.json @@ -1,35 +1,35 @@ { "name": "Dark Kahn", - "clan_id": 77, - "skin": "male/indy", + "clan_id": 46, + "skin": "messiah/chow", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.4, + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.7, + "hc": 1.1, + "sniper": 0.7, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.1, - "slippers": 0.8, - "bandolier": 0.3 + "vest": 0.4, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.3, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.4, "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.1, - "communicationTone": 1.0, - "movementStyle": 0.3, - "objectiveFocus": 0.8 + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/dharmesh.json b/bots/bots_clan/dharmesh.json index 1b2e30e8a..6e2c2c713 100644 --- a/bots/bots_clan/dharmesh.json +++ b/bots/bots_clan/dharmesh.json @@ -1,34 +1,34 @@ { "name": "Dharmesh", - "clan_id": 87, - "skin": "male/indy", + "clan_id": 24, + "skin": "actionmale/aqgthugboss", "weaponPreferences": { "mk23": 0.3, "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.4, + "mp5": 0.5, + "m4": 0.3, "m3": 0.4, - "hc": 0.6, + "hc": 0.2, "sniper": 0.6, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, + "vest": 0.7, "helm": 0.6, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.5 + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, + "aggressiveness": 0.7, + "teamwork": 0.6, "curiosity": 0.5, "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.3, "movementStyle": 0.6, "objectiveFocus": 0.5 } diff --git a/bots/bots_clan/dinardo.json b/bots/bots_clan/dinardo.json index 7d42730ac..238a7abc4 100644 --- a/bots/bots_clan/dinardo.json +++ b/bots/bots_clan/dinardo.json @@ -1,35 +1,35 @@ { "name": "Di Nardo", - "clan_id": 104, - "skin": "male/indy", + "clan_id": 20, + "skin": "male/sabotage", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.6, + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.5, "knives": 0.6, - "grenades": 0.7 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.2, + "vest": 0.6, + "helm": 0.3, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.6, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.1, - "aimingSkill": 0.3, - "reactionTime": 0.8, - "communicationFreq": 0.8, - "communicationTone": 0.9, - "movementStyle": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.7, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/doc.json b/bots/bots_clan/doc.json deleted file mode 100644 index 78868955c..000000000 --- a/bots/bots_clan/doc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "doc", - "clan_id": 47, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.8, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.9, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 1.0, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.8, - "communicationFreq": 0.1, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/drahmin.json b/bots/bots_clan/drahmin.json index 7e2c08c58..555e0ceaf 100644 --- a/bots/bots_clan/drahmin.json +++ b/bots/bots_clan/drahmin.json @@ -1,35 +1,35 @@ { "name": "Drahmin", - "clan_id": 57, - "skin": "male/indy", + "clan_id": 43, + "skin": "actionmale/chucky", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.6, + "mk23": 0.2, + "dual_mk23": 0.8, + "mp5": 0.7, + "m4": 0.2, + "m3": 0.5, "hc": 0.3, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.5 + "sniper": 0.8, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, + "vest": 0.2, "helm": 0.3, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.5 + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 1.0 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/edoli.json b/bots/bots_clan/edoli.json index d1f2ff20b..8aa74251a 100644 --- a/bots/bots_clan/edoli.json +++ b/bots/bots_clan/edoli.json @@ -1,35 +1,35 @@ { "name": "Edoli", - "clan_id": 19, - "skin": "male/indy", + "clan_id": 41, + "skin": "male/mikeyl", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.1, - "m4": 1.0, - "m3": 0.4, - "hc": 0.8, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.3 + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.6, "helm": 0.8, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.2 + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.1, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.3, "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/edwig.json b/bots/bots_clan/edwig.json deleted file mode 100644 index 57623a55b..000000000 --- a/bots/bots_clan/edwig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Edwig", - "clan_id": 40, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/endugu.json b/bots/bots_clan/endugu.json index 8e22d1807..e99004e1c 100644 --- a/bots/bots_clan/endugu.json +++ b/bots/bots_clan/endugu.json @@ -1,35 +1,35 @@ { "name": "Endugu", - "clan_id": 108, - "skin": "male/indy", + "clan_id": 29, + "skin": "messiah/chowblue", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.3, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.1, + "m4": 0.4, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.5, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.9, - "slippers": 0.8, - "bandolier": 0.8 + "vest": 0.5, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, + "aggressiveness": 0.7, + "teamwork": 0.7, "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.2, "movementStyle": 0.3, - "objectiveFocus": 0.6 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/exbee.json b/bots/bots_clan/exbee.json deleted file mode 100644 index 9da3ed30b..000000000 --- a/bots/bots_clan/exbee.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "exBEE", - "clan_id": 32, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.1, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/eyeback.json b/bots/bots_clan/eyeback.json index bc337af65..9b2b53cd5 100644 --- a/bots/bots_clan/eyeback.json +++ b/bots/bots_clan/eyeback.json @@ -1,35 +1,35 @@ { "name": "Eyeback", - "clan_id": 23, - "skin": "male/indy", + "clan_id": 16, + "skin": "terror/ctf_b", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.8, - "hc": 0.6, + "dual_mk23": 0.1, + "mp5": 0.3, + "m4": 0.2, + "m3": 0.3, + "hc": 0.7, "sniper": 0.6, - "knives": 0.5, - "grenades": 0.4 + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 1.2, + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/faden.json b/bots/bots_clan/faden.json index e84ab4447..b87880f08 100644 --- a/bots/bots_clan/faden.json +++ b/bots/bots_clan/faden.json @@ -1,35 +1,35 @@ { "name": "Faden", - "clan_id": 32, - "skin": "male/indy", + "clan_id": 110, + "skin": "aqmarine/marine1", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.9, + "mk23": 0.6, + "dual_mk23": 0.8, + "mp5": 0.7, "m4": 0.3, - "m3": 0.8, - "hc": 0.3, + "m3": 0.6, + "hc": 0.8, "sniper": 0.6, - "knives": 0.1, - "grenades": 0.3 + "knives": 0.8, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.8, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.7, + "aimingSkill": 0.3, "reactionTime": 0.4, "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/fangskin.json b/bots/bots_clan/fangskin.json deleted file mode 100644 index f971801c0..000000000 --- a/bots/bots_clan/fangskin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Fangskin", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.9, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.1, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ferratorr.json b/bots/bots_clan/ferratorr.json deleted file mode 100644 index 7a0f628b2..000000000 --- a/bots/bots_clan/ferratorr.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ferra/Torr", - "clan_id": 46, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/fexel.json b/bots/bots_clan/fexel.json index 84ae8a5bc..5120e42b3 100644 --- a/bots/bots_clan/fexel.json +++ b/bots/bots_clan/fexel.json @@ -1,35 +1,35 @@ { "name": "Fexel", - "clan_id": 110, - "skin": "male/indy", + "clan_id": 2, + "skin": "actionmale/seagull", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, + "mk23": 0.9, + "dual_mk23": 0.2, + "mp5": 0.9, + "m4": 0.4, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.4, + "knives": 1.0, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.5, + "teamwork": 0.2, + "curiosity": 0.4, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/figgz.json b/bots/bots_clan/figgz.json index 8a7ff8045..83d43853d 100644 --- a/bots/bots_clan/figgz.json +++ b/bots/bots_clan/figgz.json @@ -1,35 +1,35 @@ { "name": "Figgz", - "clan_id": 24, - "skin": "male/indy", + "clan_id": 74, + "skin": "actionmale/ctf_r", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.4, "dual_mk23": 0.4, "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, + "m4": 0.5, + "m3": 0.3, "hc": 0.5, - "sniper": 0.9, - "knives": 0.4, + "sniper": 1.2, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.9, - "slippers": 0.3, + "vest": 0.7, + "helm": 0.8, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.8, - "teamwork": 0.9, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.0, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/frankdux.json b/bots/bots_clan/frankdux.json deleted file mode 100644 index 78639c5be..000000000 --- a/bots/bots_clan/frankdux.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Frank Dux", - "clan_id": 15, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.2, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/fred.json b/bots/bots_clan/fred.json index 3fa0e5db4..f828b498a 100644 --- a/bots/bots_clan/fred.json +++ b/bots/bots_clan/fred.json @@ -1,35 +1,35 @@ { "name": "Fred", - "clan_id": 58, - "skin": "male/indy", + "clan_id": 20, + "skin": "sas/sasUC2", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.6 + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.3, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.8, + "grenades": -0.1 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.9, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, + "aggressiveness": 0.7, + "teamwork": 0.6, "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.9, - "objectiveFocus": 0.6 + "aimingSkill": -0.0, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/frost.json b/bots/bots_clan/frost.json deleted file mode 100644 index cea24e46e..000000000 --- a/bots/bots_clan/frost.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Frost", - "clan_id": 76, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.0, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.1, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/fujin.json b/bots/bots_clan/fujin.json index c1407a35e..d1a4e2fef 100644 --- a/bots/bots_clan/fujin.json +++ b/bots/bots_clan/fujin.json @@ -1,35 +1,35 @@ { "name": "Fujin", - "clan_id": 105, - "skin": "male/indy", + "clan_id": 61, + "skin": "actionrally/ctf_b", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.3, + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.2, + "m4": 0.3, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.5, "knives": 0.3, - "grenades": 0.5 + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.8, + "vest": 0.9, + "helm": 0.6, "laser": 0.7, "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.3 + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.3, + "movementStyle": 0.9, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/fxguy.json b/bots/bots_clan/fxguy.json index 9197b457a..73c094ea2 100644 --- a/bots/bots_clan/fxguy.json +++ b/bots/bots_clan/fxguy.json @@ -1,35 +1,35 @@ { "name": "FXguy", - "clan_id": 37, - "skin": "male/indy", + "clan_id": 29, + "skin": "messiah/blade2", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.6, + "vest": 0.4, + "helm": 0.7, + "laser": 0.3, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.5, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.6, "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/geras.json b/bots/bots_clan/geras.json deleted file mode 100644 index 32faea98e..000000000 --- a/bots/bots_clan/geras.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Geras", - "clan_id": 37, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.0, - "dual_mk23": 0.0, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.9, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gigavoltz.json b/bots/bots_clan/gigavoltz.json deleted file mode 100644 index ed54edbf2..000000000 --- a/bots/bots_clan/gigavoltz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gigavoltz", - "clan_id": 19, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.1, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.0 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gordac.json b/bots/bots_clan/gordac.json index d46b9726a..9c9f9ce6a 100644 --- a/bots/bots_clan/gordac.json +++ b/bots/bots_clan/gordac.json @@ -1,35 +1,35 @@ { "name": "Gordac", - "clan_id": 46, - "skin": "male/indy", + "clan_id": 96, + "skin": "terror/jungleterr", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.5, "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.9, - "knives": 0.2, - "grenades": 0.7 + "mp5": 0.8, + "m4": 0.6, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.6, + "vest": 0.5, + "helm": 0.5, "laser": 0.6, - "silencer": 0.8, + "silencer": 0.9, "slippers": 0.6, - "bandolier": 0.8 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.2, - "movementStyle": 0.8, - "objectiveFocus": 0.7 + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/gorre.json b/bots/bots_clan/gorre.json index 3ce21a5e0..643af6a1f 100644 --- a/bots/bots_clan/gorre.json +++ b/bots/bots_clan/gorre.json @@ -1,35 +1,35 @@ { "name": "Gorre", - "clan_id": 97, - "skin": "male/indy", + "clan_id": 35, + "skin": "aqmarine/urban", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.3 + "mk23": 0.6, + "dual_mk23": 1.2, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.4, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 1.0, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.3 + "vest": 0.4, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 1.1, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.9 + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/grezzer.json b/bots/bots_clan/grezzer.json index 7555c7175..c9261a9fa 100644 --- a/bots/bots_clan/grezzer.json +++ b/bots/bots_clan/grezzer.json @@ -1,35 +1,35 @@ { "name": "GreZZer", - "clan_id": 32, - "skin": "male/indy", + "clan_id": 42, + "skin": "male/bluebeard", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.9, + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.7, "m3": 0.5, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.0 + "hc": 0.6, + "sniper": 0.1, + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.3 + "vest": 0.2, + "helm": 0.8, + "laser": 0.1, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.7, + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.4, "aimingSkill": 0.5, - "reactionTime": 0.4, + "reactionTime": 0.1, "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.8, + "communicationTone": 0.9, + "movementStyle": 0.4, "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/griswold.json b/bots/bots_clan/griswold.json deleted file mode 100644 index 080ea45c1..000000000 --- a/bots/bots_clan/griswold.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Griswold", - "clan_id": 36, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.0, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.9, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": -0.0, - "movementStyle": 0.2, - "objectiveFocus": 1.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/gungho.json b/bots/bots_clan/gungho.json index d2f675d9b..f981e629b 100644 --- a/bots/bots_clan/gungho.json +++ b/bots/bots_clan/gungho.json @@ -1,35 +1,35 @@ { "name": "Gung Ho", - "clan_id": 77, - "skin": "male/indy", + "clan_id": 92, + "skin": "terror/terror", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.5 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.3, + "m3": 0.0, + "hc": 0.4, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, + "vest": 0.3, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.2, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": -0.0, - "aimingSkill": 0.8, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/gunner.json b/bots/bots_clan/gunner.json index 00cc14417..0f392eca6 100644 --- a/bots/bots_clan/gunner.json +++ b/bots/bots_clan/gunner.json @@ -1,35 +1,35 @@ { "name": "Gunner", - "clan_id": 75, - "skin": "male/indy", + "clan_id": 107, + "skin": "terror/csw black", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.5, + "mk23": 0.6, + "dual_mk23": 0.9, + "mp5": 0.5, + "m4": 0.7, "m3": 0.7, "hc": 0.7, - "sniper": 0.6, + "sniper": 0.4, "knives": 0.6, - "grenades": 0.6 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.7, + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 1.1, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.9, - "reactionTime": 0.5, + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.2, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/halfbaked.json b/bots/bots_clan/halfbaked.json deleted file mode 100644 index 42e75e907..000000000 --- a/bots/bots_clan/halfbaked.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Halfbaked", - "clan_id": 89, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 1.0, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.9, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hammerhead.json b/bots/bots_clan/hammerhead.json deleted file mode 100644 index d0ee66709..000000000 --- a/bots/bots_clan/hammerhead.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hammerhead", - "clan_id": 48, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.4, - "hc": 1.0, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/havik.json b/bots/bots_clan/havik.json deleted file mode 100644 index bf7d5ea77..000000000 --- a/bots/bots_clan/havik.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Havik", - "clan_id": 70, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.4, - "grenades": 1.2 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.1, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.9, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hecker.json b/bots/bots_clan/hecker.json deleted file mode 100644 index e3de77d40..000000000 --- a/bots/bots_clan/hecker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hecker", - "clan_id": 84, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 1.0, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.2, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/heljay.json b/bots/bots_clan/heljay.json index 31be1ee0c..c62689df6 100644 --- a/bots/bots_clan/heljay.json +++ b/bots/bots_clan/heljay.json @@ -1,35 +1,35 @@ { "name": "Heljay", - "clan_id": 53, - "skin": "male/indy", + "clan_id": 43, + "skin": "male/MikeLowry", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.2, + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.7, "knives": 0.5, - "grenades": 0.3 + "grenades": 0.9 }, "itemPreferences": { "vest": 0.7, - "helm": 0.5, - "laser": 0.9, - "silencer": 0.8, - "slippers": 0.8, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.4, "teamwork": 0.6, "curiosity": 0.8, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.9, - "movementStyle": 0.8, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.7, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/himdar.json b/bots/bots_clan/himdar.json index 2c7a096f0..fa1bae7b4 100644 --- a/bots/bots_clan/himdar.json +++ b/bots/bots_clan/himdar.json @@ -1,35 +1,35 @@ { "name": "Himdar", - "clan_id": 76, - "skin": "male/indy", + "clan_id": 30, + "skin": "messiah/chowred", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.7, + "dual_mk23": 0.5, "mp5": 0.5, - "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 1.0, - "knives": 0.5, - "grenades": 0.7 + "m4": 0.1, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.4, + "vest": 0.8, + "helm": 0.4, + "laser": 0.3, "silencer": 0.5, "slippers": 0.4, - "bandolier": 0.6 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.5, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.9, - "objectiveFocus": 0.5 + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/hitnrun.json b/bots/bots_clan/hitnrun.json index f459859db..d28462123 100644 --- a/bots/bots_clan/hitnrun.json +++ b/bots/bots_clan/hitnrun.json @@ -1,35 +1,35 @@ { "name": "Hitnrun", - "clan_id": 64, - "skin": "male/indy", + "clan_id": 105, + "skin": "sas/fbi", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": -0.0, - "mp5": 0.7, - "m4": 0.2, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.9, - "grenades": 0.5 + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.9, + "knives": 0.8, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.3, - "bandolier": 1.1 + "vest": 0.3, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "aggressiveness": 0.6, + "teamwork": 0.9, + "curiosity": 0.5, + "aimingSkill": 0.9, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/hossman.json b/bots/bots_clan/hossman.json index 1b8427ecd..1ccc1465e 100644 --- a/bots/bots_clan/hossman.json +++ b/bots/bots_clan/hossman.json @@ -1,35 +1,35 @@ { "name": "Hossman", - "clan_id": 88, - "skin": "male/indy", + "clan_id": 10, + "skin": "actionrally/modsquad", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.5, + "vest": 0.8, + "helm": 0.8, + "laser": 0.7, "silencer": 0.4, - "slippers": 0.4, + "slippers": 0.7, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.8, + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.2, "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.7, "movementStyle": 0.3, - "objectiveFocus": 0.5 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/hsuhao.json b/bots/bots_clan/hsuhao.json deleted file mode 100644 index 74a3e0653..000000000 --- a/bots/bots_clan/hsuhao.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hsu Hao", - "clan_id": 78, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 1.1, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/hurricane.json b/bots/bots_clan/hurricane.json deleted file mode 100644 index c5c4440e1..000000000 --- a/bots/bots_clan/hurricane.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hurricane", - "clan_id": 38, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.8, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.8, - "laser": 0.4, - "silencer": 0.9, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/icarus.json b/bots/bots_clan/icarus.json index 0af5e3db7..c347466f2 100644 --- a/bots/bots_clan/icarus.json +++ b/bots/bots_clan/icarus.json @@ -1,35 +1,35 @@ { "name": "Icarus", - "clan_id": 104, - "skin": "male/indy", + "clan_id": 70, + "skin": "male/chowda", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.2, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.1, - "grenades": 0.6 + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.1, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, + "aggressiveness": 0.5, + "teamwork": 0.0, "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.1, - "movementStyle": 0.2, - "objectiveFocus": 0.1 + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/icehawk.json b/bots/bots_clan/icehawk.json deleted file mode 100644 index 1b220944b..000000000 --- a/bots/bots_clan/icehawk.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Icehawk", - "clan_id": 17, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 1.1, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.0, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 1.0, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ismail.json b/bots/bots_clan/ismail.json index 1e27221b0..7a0dfcef1 100644 --- a/bots/bots_clan/ismail.json +++ b/bots/bots_clan/ismail.json @@ -1,35 +1,35 @@ { "name": "Ismail", - "clan_id": 101, - "skin": "male/indy", + "clan_id": 51, + "skin": "aqmarine/woods", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.8, - "knives": 0.1, - "grenades": 0.5 + "mk23": 0.4, + "dual_mk23": 0.9, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.6, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.2 + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": -0.2, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/itixoid.json b/bots/bots_clan/itixoid.json deleted file mode 100644 index dc541e1be..000000000 --- a/bots/bots_clan/itixoid.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "itiXOID", - "clan_id": 69, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.1, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ivor.json b/bots/bots_clan/ivor.json index 3e49a6c40..4d73b3627 100644 --- a/bots/bots_clan/ivor.json +++ b/bots/bots_clan/ivor.json @@ -1,34 +1,34 @@ { "name": "Ivor", - "clan_id": 45, - "skin": "male/indy", + "clan_id": 108, + "skin": "sas/sas-urban", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.5 + "mk23": 0.6, + "dual_mk23": 0.1, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.3, "helm": 0.5, - "laser": 0.8, - "silencer": 0.2, + "laser": 0.3, + "silencer": 0.8, "slippers": 0.5, - "bandolier": 0.2 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.2, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.4, "movementStyle": 0.4, "objectiveFocus": 0.5 } diff --git a/bots/bots_clan/jarek.json b/bots/bots_clan/jarek.json deleted file mode 100644 index bffd2b133..000000000 --- a/bots/bots_clan/jarek.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jarek", - "clan_id": 77, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.6, - "m3": 0.8, - "hc": 0.1, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 1.0, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/jarno.json b/bots/bots_clan/jarno.json deleted file mode 100644 index 4f2fb2cda..000000000 --- a/bots/bots_clan/jarno.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Jarno", - "clan_id": 47, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.9, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": -0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/jarvis.json b/bots/bots_clan/jarvis.json index 458c35c2b..c9875a91f 100644 --- a/bots/bots_clan/jarvis.json +++ b/bots/bots_clan/jarvis.json @@ -1,35 +1,35 @@ { "name": "Jarvis", - "clan_id": 4, - "skin": "male/indy", + "clan_id": 47, + "skin": "actionmale/blood", "weaponPreferences": { - "mk23": 0.2, + "mk23": 0.5, "dual_mk23": 0.7, - "mp5": 0.5, + "mp5": 0.4, "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 + "m3": 0.8, + "hc": 0.2, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.6, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.4, + "helm": 0.2, + "laser": 0.2, + "silencer": 0.2, "slippers": 0.4, - "bandolier": 0.3 + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.9, + "aggressiveness": 0.1, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.2, "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.6 + "movementStyle": 0.8, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/jazz.json b/bots/bots_clan/jazz.json index cd5344040..cf4f2244f 100644 --- a/bots/bots_clan/jazz.json +++ b/bots/bots_clan/jazz.json @@ -1,35 +1,35 @@ { "name": "Jazz", - "clan_id": 12, - "skin": "male/indy", + "clan_id": 34, + "skin": "male/mariom", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 1.0, - "mp5": 0.4, - "m4": 0.8, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.1, - "slippers": 0.6, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.7, "curiosity": 0.3, "aimingSkill": 0.3, - "reactionTime": 0.6, + "reactionTime": 0.2, "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.2 + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/jce.json b/bots/bots_clan/jce.json deleted file mode 100644 index f3efeb4d0..000000000 --- a/bots/bots_clan/jce.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "JCe", - "clan_id": 20, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.1, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/jdgold.json b/bots/bots_clan/jdgold.json index fd2378603..e014173ee 100644 --- a/bots/bots_clan/jdgold.json +++ b/bots/bots_clan/jdgold.json @@ -1,35 +1,35 @@ { "name": "JD Gold", - "clan_id": 27, - "skin": "male/indy", + "clan_id": 50, + "skin": "messiah/cptahab", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, + "mk23": 0.8, + "dual_mk23": 0.6, "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.2, + "m4": 0.2, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.6, "knives": 0.6, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, + "vest": 0.2, "helm": 0.7, - "laser": 0.4, - "silencer": 0.8, + "laser": 0.5, + "silencer": 0.4, "slippers": 0.4, - "bandolier": 0.7 + "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.3, + "teamwork": 0.6, + "curiosity": 0.1, "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.6, + "reactionTime": 0.9, + "communicationFreq": 0.5, + "communicationTone": 1.2, "movementStyle": 0.5, - "objectiveFocus": 0.6 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/jinx.json b/bots/bots_clan/jinx.json index 9643bcef6..d609efbef 100644 --- a/bots/bots_clan/jinx.json +++ b/bots/bots_clan/jinx.json @@ -1,35 +1,35 @@ { "name": "Jinx", - "clan_id": 88, - "skin": "male/indy", + "clan_id": 20, + "skin": "male/sabotage", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.8 + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.8, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { "vest": 0.4, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.4, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.2, "slippers": 0.5, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, + "aggressiveness": 0.7, "teamwork": 0.5, - "curiosity": 0.6, + "curiosity": 0.4, "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.2, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/joce.json b/bots/bots_clan/joce.json index a6df0cffa..473e05f1b 100644 --- a/bots/bots_clan/joce.json +++ b/bots/bots_clan/joce.json @@ -1,35 +1,35 @@ { "name": "Joce", - "clan_id": 49, - "skin": "male/indy", + "clan_id": 6, + "skin": "male/Superman", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.6, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.5, "knives": 0.2, - "grenades": 0.4 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.6, + "vest": 0.9, + "helm": 0.3, + "laser": 0.4, "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.7 + "slippers": 0.6, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, + "aggressiveness": 0.8, + "teamwork": 0.6, "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.2 + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/johnluger.json b/bots/bots_clan/johnluger.json deleted file mode 100644 index 9f871ec1f..000000000 --- a/bots/bots_clan/johnluger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "John Luger", - "clan_id": 97, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.9, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/johnrambo.json b/bots/bots_clan/johnrambo.json deleted file mode 100644 index b9dc4f677..000000000 --- a/bots/bots_clan/johnrambo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "John Rambo", - "clan_id": 36, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.3, - "hc": 0.3, - "sniper": 1.1, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.2, - "slippers": 0.3, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.9, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kai.json b/bots/bots_clan/kai.json deleted file mode 100644 index e277d3b5e..000000000 --- a/bots/bots_clan/kai.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kai", - "clan_id": 104, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.6, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": -0.0, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kano.json b/bots/bots_clan/kano.json index 273a2f62b..8c283fb50 100644 --- a/bots/bots_clan/kano.json +++ b/bots/bots_clan/kano.json @@ -1,35 +1,35 @@ { "name": "Kano", "clan_id": 82, - "skin": "male/indy", + "skin": "messiah/darkbeing", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.9, - "m3": 0.5, - "hc": 0.6, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.9, + "hc": 0.3, "sniper": 0.5, - "knives": 0.5, + "knives": 0.3, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": -0.0, - "silencer": 0.7, + "vest": 0.3, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.6, "slippers": 0.6, "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.9, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.5, "reactionTime": 0.8, - "communicationFreq": 0.2, - "communicationTone": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, "movementStyle": 0.5, - "objectiveFocus": 0.7 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/kaolin.json b/bots/bots_clan/kaolin.json index 523529c05..fb2673579 100644 --- a/bots/bots_clan/kaolin.json +++ b/bots/bots_clan/kaolin.json @@ -1,35 +1,35 @@ { "name": "Kaolin", - "clan_id": 42, - "skin": "male/indy", + "clan_id": 27, + "skin": "sas/sasjungle", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.3, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.6, "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, + "laser": 0.3, + "silencer": 0.5, "slippers": 0.4, - "bandolier": 0.7 + "bandolier": -0.1 }, "coreTraits": { "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.4, "communicationFreq": 0.6, "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "movementStyle": 0.9, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/keel.json b/bots/bots_clan/keel.json index f67ad7a64..f0fbfe587 100644 --- a/bots/bots_clan/keel.json +++ b/bots/bots_clan/keel.json @@ -1,35 +1,35 @@ { "name": "Keel", - "clan_id": 41, - "skin": "male/indy", + "clan_id": 5, + "skin": "male/kw_red", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, + "mk23": 0.3, + "dual_mk23": 0.6, "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.0, - "grenades": 0.4 + "m4": 0.4, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.2, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.7, + "vest": 0.3, + "helm": 0.4, "laser": 0.6, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.5 + "silencer": 0.1, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.7 + "aggressiveness": -0.0, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/khameleon.json b/bots/bots_clan/khameleon.json deleted file mode 100644 index d2abeb1f4..000000000 --- a/bots/bots_clan/khameleon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Khameleon", - "clan_id": 21, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 1.0, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 1.1 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 1.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kippy.json b/bots/bots_clan/kippy.json deleted file mode 100644 index 51fee9e83..000000000 --- a/bots/bots_clan/kippy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kippy", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.9, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kitlatura.json b/bots/bots_clan/kitlatura.json deleted file mode 100644 index ec60eee9a..000000000 --- a/bots/bots_clan/kitlatura.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kit Latura", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.1, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/klesk.json b/bots/bots_clan/klesk.json index 11b33afbd..d80ae52ce 100644 --- a/bots/bots_clan/klesk.json +++ b/bots/bots_clan/klesk.json @@ -1,35 +1,35 @@ { "name": "Klesk", - "clan_id": 71, - "skin": "male/indy", + "clan_id": 65, + "skin": "female/kw_aqua", "weaponPreferences": { - "mk23": 0.7, + "mk23": 0.3, "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.5 + "mp5": 0.4, + "m4": 0.6, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.5 + "vest": 0.0, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.4, + "aggressiveness": 0.8, + "teamwork": 0.8, + "curiosity": 0.2, + "aimingSkill": 0.8, "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.3 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/kobra.json b/bots/bots_clan/kobra.json deleted file mode 100644 index d1d775613..000000000 --- a/bots/bots_clan/kobra.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kobra", - "clan_id": 99, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kor.json b/bots/bots_clan/kor.json index 9df6c3c5e..136a4b4fb 100644 --- a/bots/bots_clan/kor.json +++ b/bots/bots_clan/kor.json @@ -1,35 +1,35 @@ { "name": "Kor", - "clan_id": 32, - "skin": "male/indy", + "clan_id": 53, + "skin": "male/elway", "weaponPreferences": { "mk23": 0.4, "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.3 + "mp5": 0.5, + "m4": -0.0, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.6, + "vest": 0.3, + "helm": 0.7, + "laser": 0.2, "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.4 + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.1, - "aimingSkill": 0.1, - "reactionTime": 0.8, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/kronika.json b/bots/bots_clan/kronika.json deleted file mode 100644 index a7534db18..000000000 --- a/bots/bots_clan/kronika.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kronika", - "clan_id": 62, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.1, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.0, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/kunglao.json b/bots/bots_clan/kunglao.json deleted file mode 100644 index 78fe1213c..000000000 --- a/bots/bots_clan/kunglao.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kung Lao", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.9, - "m4": 0.8, - "m3": 0.1, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/leatherarm.json b/bots/bots_clan/leatherarm.json index 16102dc43..4e68b9a3c 100644 --- a/bots/bots_clan/leatherarm.json +++ b/bots/bots_clan/leatherarm.json @@ -1,35 +1,35 @@ { "name": "Leatherarm", - "clan_id": 79, - "skin": "male/indy", + "clan_id": 19, + "skin": "male/rigs", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 1.0, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 1.0, + "helm": 0.6, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.7, + "aggressiveness": 0.3, + "teamwork": 0.0, + "curiosity": 0.9, + "aimingSkill": 0.8, "reactionTime": 0.4, "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/leiruth.json b/bots/bots_clan/leiruth.json index 2e2156fd8..eb72bd059 100644 --- a/bots/bots_clan/leiruth.json +++ b/bots/bots_clan/leiruth.json @@ -1,35 +1,35 @@ { "name": "Leiruth", - "clan_id": 4, - "skin": "male/indy", + "clan_id": 36, + "skin": "male/kw_red", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.4 + "mk23": 0.3, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.0, - "bandolier": 0.6 + "vest": 0.2, + "helm": 0.5, + "laser": 0.0, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.5, "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.9, "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.5 + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/lemmie.json b/bots/bots_clan/lemmie.json deleted file mode 100644 index adba3b66c..000000000 --- a/bots/bots_clan/lemmie.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lemmie", - "clan_id": 67, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/liukang.json b/bots/bots_clan/liukang.json index 98f80599a..71704fcff 100644 --- a/bots/bots_clan/liukang.json +++ b/bots/bots_clan/liukang.json @@ -1,35 +1,35 @@ { "name": "Liu Kang", - "clan_id": 60, - "skin": "male/indy", + "clan_id": 71, + "skin": "female/kw_pink", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.2, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.5 + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.2, + "grenades": 1.0 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 0.2, + "helm": 0.4, + "laser": 0.9, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.5, "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.4, + "curiosity": 0.1, + "aimingSkill": 0.3, "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "communicationFreq": 0.4, + "communicationTone": 0.9, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/lucy.json b/bots/bots_clan/lucy.json deleted file mode 100644 index 1f194dad0..000000000 --- a/bots/bots_clan/lucy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lucy", - "clan_id": 94, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/maffer.json b/bots/bots_clan/maffer.json deleted file mode 100644 index aeb453057..000000000 --- a/bots/bots_clan/maffer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Maffer", - "clan_id": 53, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.1, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": -0.0, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/major.json b/bots/bots_clan/major.json deleted file mode 100644 index 94d0a1fdb..000000000 --- a/bots/bots_clan/major.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Major", - "clan_id": 12, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.4, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/majorpain.json b/bots/bots_clan/majorpain.json index a2974385f..b8751f428 100644 --- a/bots/bots_clan/majorpain.json +++ b/bots/bots_clan/majorpain.json @@ -1,35 +1,35 @@ { "name": "Major Pain", - "clan_id": 38, - "skin": "male/indy", + "clan_id": 72, + "skin": "male/aqgthug", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.7, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.1, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.1, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.3, + "vest": 0.9, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.7, "slippers": 0.8, - "bandolier": 0.3 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 1.0, - "curiosity": 0.2, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.7, + "communicationFreq": 0.8, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/makron.json b/bots/bots_clan/makron.json deleted file mode 100644 index 4f1a0dfc9..000000000 --- a/bots/bots_clan/makron.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Makron", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 1.0 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/malin.json b/bots/bots_clan/malin.json index d972ffdc0..2544289fb 100644 --- a/bots/bots_clan/malin.json +++ b/bots/bots_clan/malin.json @@ -1,35 +1,35 @@ { "name": "Malin", - "clan_id": 37, - "skin": "male/indy", + "clan_id": 44, + "skin": "actionmale/ctf_r", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.7 + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.4 + "vest": 0.8, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.5, + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/manny.json b/bots/bots_clan/manny.json deleted file mode 100644 index 2467422e5..000000000 --- a/bots/bots_clan/manny.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "MannY", - "clan_id": 100, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.1, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.0, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/marku.json b/bots/bots_clan/marku.json deleted file mode 100644 index 3ffa157cb..000000000 --- a/bots/bots_clan/marku.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Marku", - "clan_id": 104, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.1, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/mavado.json b/bots/bots_clan/mavado.json deleted file mode 100644 index aad2621d3..000000000 --- a/bots/bots_clan/mavado.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mavado", - "clan_id": 32, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.0, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": -0.1, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.0, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/mckenna.json b/bots/bots_clan/mckenna.json index 563e57b06..42a8721df 100644 --- a/bots/bots_clan/mckenna.json +++ b/bots/bots_clan/mckenna.json @@ -1,35 +1,35 @@ { "name": "McKenna", - "clan_id": 87, - "skin": "male/indy", + "clan_id": 82, + "skin": "actionmale/swat", "weaponPreferences": { - "mk23": 0.2, + "mk23": 0.4, "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.9, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.4, "knives": 0.6, - "grenades": 0.4 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.5, - "slippers": 1.1, - "bandolier": 0.5 + "vest": 0.7, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.8, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/meat.json b/bots/bots_clan/meat.json deleted file mode 100644 index 559c52a97..000000000 --- a/bots/bots_clan/meat.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Meat", - "clan_id": 56, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.1, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/medic.json b/bots/bots_clan/medic.json index cf8bcc8e1..f5bb7ba0a 100644 --- a/bots/bots_clan/medic.json +++ b/bots/bots_clan/medic.json @@ -1,35 +1,35 @@ { "name": "Medic", - "clan_id": 22, - "skin": "male/indy", + "clan_id": 72, + "skin": "actionmale/leonreno", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.6, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.8, "m4": 0.4, - "m3": 0.4, - "hc": 0.6, + "m3": 0.7, + "hc": 0.7, "sniper": 0.7, - "knives": 0.7, - "grenades": 0.3 + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.7, + "vest": 0.3, + "helm": 0.6, + "laser": 0.5, "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.2 + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.3, + "aggressiveness": 0.4, "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.1, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/mee2.json b/bots/bots_clan/mee2.json deleted file mode 100644 index 15111e6f7..000000000 --- a/bots/bots_clan/mee2.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "[Mee2]", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": -0.1, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/merki.json b/bots/bots_clan/merki.json index 253a12c5c..9cf7351c8 100644 --- a/bots/bots_clan/merki.json +++ b/bots/bots_clan/merki.json @@ -1,34 +1,34 @@ { "name": "Merki", - "clan_id": 91, - "skin": "male/indy", + "clan_id": 8, + "skin": "male/bruces", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.2, - "mp5": 0.8, - "m4": 0.7, + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.5, "m3": 0.5, - "hc": 0.6, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.3 + "hc": 0.8, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, + "vest": 0.6, + "helm": 0.7, + "laser": 0.2, + "silencer": 0.3, + "slippers": 0.5, "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 1.0, + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.3, + "communicationTone": 0.5, "movementStyle": 0.4, "objectiveFocus": 0.5 } diff --git a/bots/bots_clan/michael.json b/bots/bots_clan/michael.json index 5e9c366d9..ca57b8ff9 100644 --- a/bots/bots_clan/michael.json +++ b/bots/bots_clan/michael.json @@ -1,35 +1,35 @@ { "name": "Michael", - "clan_id": 21, - "skin": "male/indy", + "clan_id": 96, + "skin": "sas/aqmsas", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.9, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.6, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.6 + "vest": 0.8, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.9, - "aimingSkill": 0.9, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_clan/mileena.json b/bots/bots_clan/mileena.json index c47e91cb5..63a19e27e 100644 --- a/bots/bots_clan/mileena.json +++ b/bots/bots_clan/mileena.json @@ -1,35 +1,35 @@ { "name": "Mileena", - "clan_id": 17, - "skin": "male/indy", + "clan_id": 48, + "skin": "male/rredneck", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, + "dual_mk23": 0.3, "mp5": 0.5, - "m4": 0.4, - "m3": 0.7, - "hc": 0.8, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.9 + "m4": 0.5, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, + "vest": 0.7, "helm": 0.4, - "laser": 0.5, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.3 + "laser": 0.7, + "silencer": 0.4, + "slippers": 1.0, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.2, + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, "aimingSkill": 0.6, - "reactionTime": 0.3, + "reactionTime": 0.5, "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.6 + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/mincer.json b/bots/bots_clan/mincer.json index b0e55ab3e..6df86084f 100644 --- a/bots/bots_clan/mincer.json +++ b/bots/bots_clan/mincer.json @@ -1,35 +1,35 @@ { "name": "Mincer", - "clan_id": 106, - "skin": "male/indy", + "clan_id": 46, + "skin": "male/mkscorp", "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.3 + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, + "vest": 0.7, + "helm": 0.2, "laser": 0.6, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.3 + "slippers": 0.9, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.9, "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.9, - "reactionTime": 0.2, - "communicationFreq": 0.6, - "communicationTone": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.5, "movementStyle": 0.2, - "objectiveFocus": 1.0 + "objectiveFocus": -0.1 } } \ No newline at end of file diff --git a/bots/bots_clan/missnrun.json b/bots/bots_clan/missnrun.json deleted file mode 100644 index e946ceb15..000000000 --- a/bots/bots_clan/missnrun.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Missnrun", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 1.1 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/mokap.json b/bots/bots_clan/mokap.json index 459fbc7ab..f8fc51a37 100644 --- a/bots/bots_clan/mokap.json +++ b/bots/bots_clan/mokap.json @@ -1,35 +1,35 @@ { "name": "Mokap", - "clan_id": 2, - "skin": "male/indy", + "clan_id": 1, + "skin": "aqmarine/aquamarine", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.1, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.1, - "knives": 0.6, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.1 + "vest": 0.7, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.2, + "aggressiveness": 0.4, "teamwork": 0.4, - "curiosity": 0.0, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/motaro.json b/bots/bots_clan/motaro.json index f5db8a598..833a1bca6 100644 --- a/bots/bots_clan/motaro.json +++ b/bots/bots_clan/motaro.json @@ -1,35 +1,35 @@ { "name": "Motaro", - "clan_id": 99, - "skin": "male/indy", + "clan_id": 45, + "skin": "messiah/blade", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, + "vest": 0.5, + "helm": 0.6, "laser": 0.4, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.7 + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.9, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.4 + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 1.0, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": -0.0 } } \ No newline at end of file diff --git a/bots/bots_clan/mrgoodkat.json b/bots/bots_clan/mrgoodkat.json deleted file mode 100644 index 5af82cf9d..000000000 --- a/bots/bots_clan/mrgoodkat.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mr Goodkat", - "clan_id": 14, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.9, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 1.1, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/mrsuave.json b/bots/bots_clan/mrsuave.json deleted file mode 100644 index 6ce201ae1..000000000 --- a/bots/bots_clan/mrsuave.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mr Suave", - "clan_id": 35, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/muscleman.json b/bots/bots_clan/muscleman.json deleted file mode 100644 index 969bc6dba..000000000 --- a/bots/bots_clan/muscleman.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Muscleman", - "clan_id": 73, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/newbie.json b/bots/bots_clan/newbie.json deleted file mode 100644 index b5820c954..000000000 --- a/bots/bots_clan/newbie.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Newbie", - "clan_id": 103, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.2, - "mp5": 0.9, - "m4": 0.3, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/nibbler.json b/bots/bots_clan/nibbler.json index 9ada9f81b..ad7f1933e 100644 --- a/bots/bots_clan/nibbler.json +++ b/bots/bots_clan/nibbler.json @@ -1,35 +1,35 @@ { "name": "Nibbler", - "clan_id": 90, - "skin": "male/indy", + "clan_id": 26, + "skin": "female/ctf_b", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.0, + "mk23": 0.7, + "dual_mk23": 0.7, "mp5": 0.4, - "m4": 1.0, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.8 + "m4": 0.9, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.4, + "vest": 0.7, + "helm": 0.3, "laser": 0.6, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.4 + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.8, + "aggressiveness": 0.5, + "teamwork": 0.7, + "curiosity": 0.3, + "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "communicationFreq": 0.9, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/nihlathak.json b/bots/bots_clan/nihlathak.json index 4252da242..e0623d7f9 100644 --- a/bots/bots_clan/nihlathak.json +++ b/bots/bots_clan/nihlathak.json @@ -1,35 +1,35 @@ { "name": "Nihlathak", - "clan_id": 109, - "skin": "male/indy", + "clan_id": 15, + "skin": "terror/swatsnipe", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.3, + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.6, "knives": 0.6, - "grenades": 0.5 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.1 + "vest": 0.4, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.5, + "teamwork": 0.7, + "curiosity": 0.3, "aimingSkill": 0.5, "reactionTime": 0.6, "communicationFreq": 0.9, - "communicationTone": 0.7, + "communicationTone": 0.2, "movementStyle": 0.5, - "objectiveFocus": 0.1 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/nin.json b/bots/bots_clan/nin.json deleted file mode 100644 index 7e560415f..000000000 --- a/bots/bots_clan/nin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "NiN", - "clan_id": 92, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.8, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.9, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/nitara.json b/bots/bots_clan/nitara.json deleted file mode 100644 index ccdccfbd6..000000000 --- a/bots/bots_clan/nitara.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nitara", - "clan_id": 89, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/nohope.json b/bots/bots_clan/nohope.json deleted file mode 100644 index 71ffe98f9..000000000 --- a/bots/bots_clan/nohope.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nohope", - "clan_id": 24, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.2, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.2, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.9, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/onaga.json b/bots/bots_clan/onaga.json deleted file mode 100644 index 17ae7ee7f..000000000 --- a/bots/bots_clan/onaga.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Onaga", - "clan_id": 91, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.8, - "mp5": 0.1, - "m4": 0.6, - "m3": 0.7, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.8, - "slippers": 1.0, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/orbb.json b/bots/bots_clan/orbb.json index 87b4add07..18a81831e 100644 --- a/bots/bots_clan/orbb.json +++ b/bots/bots_clan/orbb.json @@ -1,35 +1,35 @@ { "name": "Orbb", - "clan_id": 1, - "skin": "male/indy", + "clan_id": 22, + "skin": "male/pimp", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.1, - "m3": 0.7, + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.6, "hc": 0.3, "sniper": 0.4, - "knives": 0.5, - "grenades": 0.7 + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.7, + "vest": 0.3, + "helm": 0.4, "laser": 0.6, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.1 + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.1, - "movementStyle": 0.2, - "objectiveFocus": 0.9 + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.9, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.1, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/orinboyd.json b/bots/bots_clan/orinboyd.json index 33e6d1993..91c7dccef 100644 --- a/bots/bots_clan/orinboyd.json +++ b/bots/bots_clan/orinboyd.json @@ -1,34 +1,34 @@ { "name": "Orin Boyd", - "clan_id": 101, - "skin": "male/indy", + "clan_id": 21, + "skin": "aqmarine/marine1", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.1, - "hc": 0.2, + "dual_mk23": 1.0, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.4, + "hc": 0.3, "sniper": 0.7, "knives": 0.6, - "grenades": 0.4 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.2 + "vest": 0.7, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.2, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.6, + "aggressiveness": 0.2, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.9, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.5, "movementStyle": 0.4, "objectiveFocus": 0.6 } diff --git a/bots/bots_clan/ostmor.json b/bots/bots_clan/ostmor.json index b042d699e..5aafc7699 100644 --- a/bots/bots_clan/ostmor.json +++ b/bots/bots_clan/ostmor.json @@ -1,35 +1,35 @@ { "name": "Ostmor", - "clan_id": 5, - "skin": "male/indy", + "clan_id": 89, + "skin": "messiah/suit", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.4, + "mp5": 0.6, "m4": 0.6, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.6 + "m3": 0.4, + "hc": 0.5, + "sniper": 0.9, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.5 + "vest": 0.2, + "helm": 0.4, + "laser": 0.9, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/oysterhead.json b/bots/bots_clan/oysterhead.json index 9b58187b1..db239acc2 100644 --- a/bots/bots_clan/oysterhead.json +++ b/bots/bots_clan/oysterhead.json @@ -1,35 +1,35 @@ { "name": "Oysterhead", - "clan_id": 61, - "skin": "male/indy", + "clan_id": 68, + "skin": "actionmale/Bruce_Wayne", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.6, + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.4, "knives": 0.5, - "grenades": 0.3 + "grenades": 0.1 }, "itemPreferences": { "vest": 0.1, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.0, - "slippers": 0.6, - "bandolier": 0.4 + "helm": 0.4, + "laser": 0.5, + "silencer": 1.0, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.6, - "aimingSkill": 0.9, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.8, - "movementStyle": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.1, + "communicationTone": 0.9, + "movementStyle": 0.3, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/paradox.json b/bots/bots_clan/paradox.json deleted file mode 100644 index daedceb82..000000000 --- a/bots/bots_clan/paradox.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "ParaDox", - "clan_id": 25, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.9, - "knives": 0.9, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/patriot.json b/bots/bots_clan/patriot.json deleted file mode 100644 index bc7c554ef..000000000 --- a/bots/bots_clan/patriot.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Patriot", - "clan_id": 15, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.8, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.8, - "knives": 0.8, - "grenades": 1.0 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 1.1, - "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/pewdiepie.json b/bots/bots_clan/pewdiepie.json index a9c13944a..ffac02248 100644 --- a/bots/bots_clan/pewdiepie.json +++ b/bots/bots_clan/pewdiepie.json @@ -1,35 +1,35 @@ { "name": "PewDiePie", - "clan_id": 64, - "skin": "male/indy", + "clan_id": 83, + "skin": "male/leaf", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, + "mk23": 0.5, + "dual_mk23": 0.2, "mp5": 0.5, - "m4": 1.1, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.9, - "grenades": 0.6 + "m4": 0.7, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.3 + "vest": 0.8, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.6, + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.4, "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/pindleskin.json b/bots/bots_clan/pindleskin.json deleted file mode 100644 index 19dfe068e..000000000 --- a/bots/bots_clan/pindleskin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Pindleskin", - "clan_id": 23, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/radament.json b/bots/bots_clan/radament.json index dee5a5f51..7eb3f35ec 100644 --- a/bots/bots_clan/radament.json +++ b/bots/bots_clan/radament.json @@ -1,35 +1,35 @@ { "name": "Radament", - "clan_id": 72, - "skin": "male/indy", + "clan_id": 97, + "skin": "female/kw_yellow", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.6, + "mk23": 0.3, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.2, + "m3": 0.5, "hc": 0.4, "sniper": 0.6, - "knives": 0.5, - "grenades": 0.5 + "knives": 0.8, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.8 + "vest": 0.9, + "helm": 1.1, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.7, + "curiosity": 0.8, "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "reactionTime": 0.3, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/rain.json b/bots/bots_clan/rain.json deleted file mode 100644 index 4c26fb9cb..000000000 --- a/bots/bots_clan/rain.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Rain", - "clan_id": 91, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.8, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.9, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_clan/rakanishu.json b/bots/bots_clan/rakanishu.json index bab1f18c4..c8c1591c6 100644 --- a/bots/bots_clan/rakanishu.json +++ b/bots/bots_clan/rakanishu.json @@ -1,35 +1,35 @@ { "name": "Rakanishu", - "clan_id": 45, - "skin": "male/indy", + "clan_id": 7, + "skin": "female/kw_red", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.8, + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.5, "m4": 0.5, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.7, - "knives": -0.1, - "grenades": 0.2 + "m3": 0.5, + "hc": 0.7, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.4, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 + "helm": 0.6, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.1, + "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": -0.0, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.6, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/ramrod.json b/bots/bots_clan/ramrod.json index c545b214b..3a77033cd 100644 --- a/bots/bots_clan/ramrod.json +++ b/bots/bots_clan/ramrod.json @@ -1,35 +1,35 @@ { "name": "Ramrod", - "clan_id": 72, - "skin": "male/indy", + "clan_id": 33, + "skin": "actionmale/dubeta", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.6, - "hc": 0.9, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.6 + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.3, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.4, "aimingSkill": 0.4, - "reactionTime": 1.0, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.2 + "reactionTime": 0.6, + "communicationFreq": 0.1, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/ranger.json b/bots/bots_clan/ranger.json deleted file mode 100644 index 0c8a26761..000000000 --- a/bots/bots_clan/ranger.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ranger", - "clan_id": 72, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.4, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.5, - "grenades": 1.0 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 1.0 - } -} \ No newline at end of file diff --git a/bots/bots_clan/ratbot.json b/bots/bots_clan/ratbot.json deleted file mode 100644 index 907bd630a..000000000 --- a/bots/bots_clan/ratbot.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ratbot", - "clan_id": 17, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.8, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/rayquick.json b/bots/bots_clan/rayquick.json deleted file mode 100644 index 7725e724a..000000000 --- a/bots/bots_clan/rayquick.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ray Quick", - "clan_id": 78, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.9, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.0, - "teamwork": 0.7, - "curiosity": 1.1, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.1, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/razor.json b/bots/bots_clan/razor.json deleted file mode 100644 index 6128288a5..000000000 --- a/bots/bots_clan/razor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Razor", - "clan_id": 34, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.1, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.9, - "slippers": 0.6, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/richard.json b/bots/bots_clan/richard.json deleted file mode 100644 index 64eefaf02..000000000 --- a/bots/bots_clan/richard.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Richard", - "clan_id": 5, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.9, - "knives": 0.5, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/rocky.json b/bots/bots_clan/rocky.json index 1474eed6e..bb1fa7369 100644 --- a/bots/bots_clan/rocky.json +++ b/bots/bots_clan/rocky.json @@ -1,35 +1,35 @@ { "name": "Rocky", - "clan_id": 43, - "skin": "male/indy", + "clan_id": 2, + "skin": "terror/adf2", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.3, - "hc": 0.6, + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.2, + "m4": 0.9, + "m3": 0.6, + "hc": 0.1, "sniper": 0.9, - "knives": 0.5, - "grenades": 0.4 + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.3 + "vest": 0.6, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.9 }, "coreTraits": { - "aggressiveness": 0.9, + "aggressiveness": 0.7, "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.5, "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.6 + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/royen.json b/bots/bots_clan/royen.json deleted file mode 100644 index 6638bed7b..000000000 --- a/bots/bots_clan/royen.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Royen", - "clan_id": 86, - "skin": "male/indy", - "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sareena.json b/bots/bots_clan/sareena.json index b5ff20c6b..09d958051 100644 --- a/bots/bots_clan/sareena.json +++ b/bots/bots_clan/sareena.json @@ -1,35 +1,35 @@ { "name": "Sareena", - "clan_id": 72, - "skin": "male/indy", + "clan_id": 44, + "skin": "terror/redterr2", "weaponPreferences": { "mk23": 0.3, - "dual_mk23": 0.2, - "mp5": 0.2, - "m4": 0.9, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.4 + "dual_mk23": 0.1, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.3, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.1, + "vest": 0.6, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.4, "slippers": 0.5, - "bandolier": 0.5 + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.3 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/sarge.json b/bots/bots_clan/sarge.json index 10263e25a..38309d215 100644 --- a/bots/bots_clan/sarge.json +++ b/bots/bots_clan/sarge.json @@ -1,35 +1,35 @@ { "name": "Sarge", - "clan_id": 32, - "skin": "male/indy", + "clan_id": 5, + "skin": "messiah/leon", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, + "mk23": 0.8, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.4, "m3": 0.5, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.3, + "hc": 0.3, + "sniper": 0.9, + "knives": 0.4, "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.2, + "vest": 0.1, + "helm": 0.4, "laser": 0.7, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.3 + "silencer": 0.4, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/sarina.json b/bots/bots_clan/sarina.json deleted file mode 100644 index 226be0e74..000000000 --- a/bots/bots_clan/sarina.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sarina", - "clan_id": 75, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.7, - "hc": 0.1, - "sniper": 0.5, - "knives": 0.9, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.0 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sean.json b/bots/bots_clan/sean.json index 76a558757..34acd7f03 100644 --- a/bots/bots_clan/sean.json +++ b/bots/bots_clan/sean.json @@ -1,35 +1,35 @@ { "name": "Sean", - "clan_id": 109, - "skin": "male/indy", + "clan_id": 100, + "skin": "actionmale/chellobeta", "weaponPreferences": { - "mk23": 0.2, + "mk23": 0.3, "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.3, - "m3": -0.1, - "hc": 0.5, - "sniper": 0.1, - "knives": 0.2, - "grenades": 0.8 + "mp5": 0.7, + "m4": 0.0, + "m3": 0.1, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.3, + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.5, "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 1.0, + "movementStyle": 0.4, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/sektor.json b/bots/bots_clan/sektor.json index 1059207ca..c0e0ce41e 100644 --- a/bots/bots_clan/sektor.json +++ b/bots/bots_clan/sektor.json @@ -1,35 +1,35 @@ { "name": "Sektor", - "clan_id": 24, - "skin": "male/indy", + "clan_id": 1, + "skin": "terror/jungleterr", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.7, + "mk23": 1.0, + "dual_mk23": 0.5, + "mp5": 0.6, + "m4": 0.2, + "m3": 0.2, + "hc": 0.7, + "sniper": 0.9, + "knives": 0.5, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.5, + "helm": -0.1, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.7 + "aggressiveness": 0.5, + "teamwork": 1.0, + "curiosity": 0.6, + "aimingSkill": 0.8, + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_clan/sgtcappa.json b/bots/bots_clan/sgtcappa.json index 565b91057..9e1842a85 100644 --- a/bots/bots_clan/sgtcappa.json +++ b/bots/bots_clan/sgtcappa.json @@ -1,35 +1,35 @@ { "name": "SGT CAPPA", - "clan_id": 66, - "skin": "male/indy", + "clan_id": 105, + "skin": "messiah/blade4", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.9, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.2, + "dual_mk23": 0.9, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.7, + "hc": 0.1, + "sniper": 0.7, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.7, + "vest": 0.1, + "helm": 0.2, + "laser": -0.0, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.5 + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.8, + "curiosity": 0.1, + "aimingSkill": 0.6, "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.3 + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_clan/shaokahn.json b/bots/bots_clan/shaokahn.json deleted file mode 100644 index 9f9afeb0f..000000000 --- a/bots/bots_clan/shaokahn.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shao Kahn", - "clan_id": 55, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.5, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sheeva.json b/bots/bots_clan/sheeva.json index 44cc367bb..a46744921 100644 --- a/bots/bots_clan/sheeva.json +++ b/bots/bots_clan/sheeva.json @@ -1,35 +1,35 @@ { "name": "Sheeva", - "clan_id": 34, - "skin": "male/indy", + "clan_id": 8, + "skin": "terror/mafia2", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.7, - "grenades": 0.9 + "mk23": 0.9, + "dual_mk23": 0.8, + "mp5": 0.6, + "m4": 0.9, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, + "vest": 0.6, "helm": 0.5, - "laser": 0.6, - "silencer": 0.9, - "slippers": 0.1, - "bandolier": 0.3 + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "aggressiveness": 0.2, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/shujinko.json b/bots/bots_clan/shujinko.json deleted file mode 100644 index 345780aa8..000000000 --- a/bots/bots_clan/shujinko.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shujinko", - "clan_id": 64, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.2, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/si.json b/bots/bots_clan/si.json index e28ccbade..765fe4ae2 100644 --- a/bots/bots_clan/si.json +++ b/bots/bots_clan/si.json @@ -1,35 +1,35 @@ { "name": "Si", - "clan_id": 68, - "skin": "male/indy", + "clan_id": 105, + "skin": "terror/swatsnipe", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.9, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.7 + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.2, + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, "slippers": 0.2, - "bandolier": 0.2 + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.1, - "reactionTime": 0.7, - "communicationFreq": 0.4, + "aggressiveness": 0.5, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.9 + "movementStyle": 0.4, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/sigil.json b/bots/bots_clan/sigil.json index bd885c6c8..de19e4189 100644 --- a/bots/bots_clan/sigil.json +++ b/bots/bots_clan/sigil.json @@ -1,35 +1,35 @@ { "name": "Sigil", - "clan_id": 10, - "skin": "male/indy", + "clan_id": 86, + "skin": "aqmarine/woods", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.4 + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.8, + "hc": 0.7, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.2 }, "itemPreferences": { "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.6 + "helm": 0.5, + "laser": 0.9, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.6, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.5, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/skorgan.json b/bots/bots_clan/skorgan.json index d0f335abc..c3a835fdc 100644 --- a/bots/bots_clan/skorgan.json +++ b/bots/bots_clan/skorgan.json @@ -1,35 +1,35 @@ { "name": "Skorgan", - "clan_id": 47, + "clan_id": 109, "skin": "male/indy", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.1, - "m3": 0.2, + "dual_mk23": 0.8, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.1, "hc": 0.6, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 + "knives": 0.2, + "grenades": 0.8 }, "itemPreferences": { "vest": 0.5, "helm": 0.4, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.3 + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, + "aggressiveness": 0.4, + "teamwork": 0.5, "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.6 + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.5, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/slice.json b/bots/bots_clan/slice.json index f96b6344d..0d4af4106 100644 --- a/bots/bots_clan/slice.json +++ b/bots/bots_clan/slice.json @@ -1,35 +1,35 @@ { "name": "Slice", - "clan_id": 17, - "skin": "male/indy", + "clan_id": 38, + "skin": "male/jdredd", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": -0.0, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.7 + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.7, + "m3": 0.6, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.9, - "silencer": 0.1, - "slippers": 0.8, - "bandolier": 0.2 + "vest": 0.6, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.6, + "slippers": 1.2, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.0, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_clan/smithy.json b/bots/bots_clan/smithy.json index 314c46de5..5f7b199fa 100644 --- a/bots/bots_clan/smithy.json +++ b/bots/bots_clan/smithy.json @@ -1,35 +1,35 @@ { "name": "Smithy", - "clan_id": 22, - "skin": "male/indy", + "clan_id": 80, + "skin": "male/madmaxy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.2, - "grenades": 0.7 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.9, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.1, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 1.0, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.5, + "laser": 0.4, + "silencer": 1.0, + "slippers": 0.6, + "bandolier": -0.0 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, + "aggressiveness": 0.7, + "teamwork": 0.7, "curiosity": 0.5, - "aimingSkill": 0.7, + "aimingSkill": 0.4, "reactionTime": 0.8, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.0 + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.9 } } \ No newline at end of file diff --git a/bots/bots_clan/smoke.json b/bots/bots_clan/smoke.json deleted file mode 100644 index c3f500ea1..000000000 --- a/bots/bots_clan/smoke.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Smoke", - "clan_id": 105, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.1, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/snaps.json b/bots/bots_clan/snaps.json index 62311bba7..9217ed41c 100644 --- a/bots/bots_clan/snaps.json +++ b/bots/bots_clan/snaps.json @@ -1,35 +1,35 @@ { "name": "Snaps", - "clan_id": 47, - "skin": "male/indy", + "clan_id": 38, + "skin": "male/nike", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 + "mk23": 0.7, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.7, + "hc": 0.2, + "sniper": 0.2, + "knives": 0.2, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.3, "helm": 0.4, - "laser": 0.6, + "laser": 0.5, "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.5 + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/socks.json b/bots/bots_clan/socks.json index bcaf4c0d0..6ec913ffa 100644 --- a/bots/bots_clan/socks.json +++ b/bots/bots_clan/socks.json @@ -1,35 +1,35 @@ { "name": "Socks", - "clan_id": 20, - "skin": "male/indy", + "clan_id": 18, + "skin": "sas/ctf_r", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.1, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.5 + "mk23": 0.4, + "dual_mk23": 0.4, + "mp5": 0.1, + "m4": 0.4, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.7 + "vest": 0.6, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.9 + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.2, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/sorlag.json b/bots/bots_clan/sorlag.json index 1df6813dd..11e183b96 100644 --- a/bots/bots_clan/sorlag.json +++ b/bots/bots_clan/sorlag.json @@ -1,35 +1,35 @@ { "name": "Sorlag", - "clan_id": 61, - "skin": "male/indy", + "clan_id": 60, + "skin": "actionmale/agent", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.3 + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.3, + "helm": 0.8, + "laser": 0.3, + "silencer": 0.7, "slippers": 0.3, - "bandolier": 0.8 + "bandolier": 1.1 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.1, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.9, - "objectiveFocus": 0.6 + "aggressiveness": 0.6, + "teamwork": 0.1, + "curiosity": 0.6, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.7, + "communicationTone": 0.9, + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_clan/subzero.json b/bots/bots_clan/subzero.json deleted file mode 100644 index 7bda162ec..000000000 --- a/bots/bots_clan/subzero.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sub-Zero", - "clan_id": 110, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.5, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/summoner.json b/bots/bots_clan/summoner.json deleted file mode 100644 index d80ab9eb0..000000000 --- a/bots/bots_clan/summoner.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Summoner", - "clan_id": 9, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": -0.1, - "communicationFreq": 0.8, - "communicationTone": 0.9, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/sundog.json b/bots/bots_clan/sundog.json deleted file mode 100644 index 730e0f2fc..000000000 --- a/bots/bots_clan/sundog.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sundog", - "clan_id": 19, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/t800.json b/bots/bots_clan/t800.json index d7d823fec..ea1b019eb 100644 --- a/bots/bots_clan/t800.json +++ b/bots/bots_clan/t800.json @@ -1,35 +1,35 @@ { "name": "T-800", - "clan_id": 69, - "skin": "male/indy", + "clan_id": 63, + "skin": "male/GreenBeret", "weaponPreferences": { - "mk23": 0.8, + "mk23": 0.4, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.8, - "hc": 0.6, - "sniper": 0.3, + "mp5": 0.5, + "m4": 0.3, + "m3": 1.0, + "hc": 0.3, + "sniper": 0.5, "knives": 0.6, - "grenades": 0.4 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.1, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.8, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/tank.json b/bots/bots_clan/tank.json deleted file mode 100644 index 0699f3aa7..000000000 --- a/bots/bots_clan/tank.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tank", - "clan_id": 11, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.5, - "m3": 0.4, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.9, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tarnen.json b/bots/bots_clan/tarnen.json index ab8d45dc5..376c7585e 100644 --- a/bots/bots_clan/tarnen.json +++ b/bots/bots_clan/tarnen.json @@ -1,35 +1,35 @@ { "name": "Tarnen", - "clan_id": 109, - "skin": "male/indy", + "clan_id": 36, + "skin": "male/jewels", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.1, - "grenades": 0.6 + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.3, + "m3": 0.1, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.8, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.4, - "bandolier": 0.9 + "helm": 0.1, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.7, + "aimingSkill": 0.6, + "reactionTime": 0.3, + "communicationFreq": 0.4, "communicationTone": 0.4, - "movementStyle": -0.1, - "objectiveFocus": 0.8 + "movementStyle": 0.1, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/tarnok.json b/bots/bots_clan/tarnok.json index a2f992604..e3393dda3 100644 --- a/bots/bots_clan/tarnok.json +++ b/bots/bots_clan/tarnok.json @@ -1,35 +1,35 @@ { "name": "Tarnok", - "clan_id": 49, - "skin": "male/indy", + "clan_id": 52, + "skin": "actionmale/scarfchain", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.6, "hc": 0.4, - "sniper": 0.9, - "knives": 0.3, - "grenades": 0.4 + "sniper": 0.5, + "knives": 1.0, + "grenades": 0.9 }, "itemPreferences": { "vest": 0.3, "helm": 0.8, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.3 + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.4, + "aggressiveness": -0.0, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.8, "reactionTime": 0.7, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/taven.json b/bots/bots_clan/taven.json deleted file mode 100644 index b6ff40caf..000000000 --- a/bots/bots_clan/taven.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Taven", - "clan_id": 71, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 1.0, - "hc": 0.9, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.1, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/teabag.json b/bots/bots_clan/teabag.json index 672cee63a..a387fb9bf 100644 --- a/bots/bots_clan/teabag.json +++ b/bots/bots_clan/teabag.json @@ -1,35 +1,35 @@ { "name": "Teabag", - "clan_id": 33, - "skin": "male/indy", + "clan_id": 87, + "skin": "male/kw_yellow", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.2, - "m3": 0.3, + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.5, "hc": 0.6, - "sniper": 0.2, - "knives": 0.3, - "grenades": 0.4 + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.2, + "vest": 0.7, + "helm": 0.4, + "laser": 0.7, "silencer": 0.4, - "slippers": 0.4, + "slippers": 0.3, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.2, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.7, + "movementStyle": 0.7, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/terminator.json b/bots/bots_clan/terminator.json index 333e3539f..37cf17e91 100644 --- a/bots/bots_clan/terminator.json +++ b/bots/bots_clan/terminator.json @@ -1,35 +1,35 @@ { "name": "Terminator", - "clan_id": 45, - "skin": "male/indy", + "clan_id": 15, + "skin": "messiah/chowred", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.6, "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.7 + "mp5": 0.6, + "m4": 0.1, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.3, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.3, + "teamwork": 0.3, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.9, + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_clan/thalath.json b/bots/bots_clan/thalath.json deleted file mode 100644 index 32228ef78..000000000 --- a/bots/bots_clan/thalath.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Thalath", - "clan_id": 43, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.0, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_clan/theboss.json b/bots/bots_clan/theboss.json deleted file mode 100644 index 53d14a5b2..000000000 --- a/bots/bots_clan/theboss.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "The Boss", - "clan_id": 90, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.9, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.7, - "hc": -0.1, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.9 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/thetulip.json b/bots/bots_clan/thetulip.json deleted file mode 100644 index ccf227079..000000000 --- a/bots/bots_clan/thetulip.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "The Tulip", - "clan_id": 107, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.9, - "objectiveFocus": 0.0 - } -} \ No newline at end of file diff --git a/bots/bots_clan/threash.json b/bots/bots_clan/threash.json deleted file mode 100644 index 339177c62..000000000 --- a/bots/bots_clan/threash.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Threash", - "clan_id": 80, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tomsteele.json b/bots/bots_clan/tomsteele.json deleted file mode 100644 index 3f2c24917..000000000 --- a/bots/bots_clan/tomsteele.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tom Steele", - "clan_id": 26, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.2, - "movementStyle": 0.2, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_clan/tormentor.json b/bots/bots_clan/tormentor.json index c257fc5c4..d2c8b0e51 100644 --- a/bots/bots_clan/tormentor.json +++ b/bots/bots_clan/tormentor.json @@ -1,35 +1,35 @@ { "name": "Tormentor", - "clan_id": 67, - "skin": "male/indy", + "clan_id": 19, + "skin": "male/Clan_UNA", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.3, + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.2, "m3": 0.8, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.3, - "grenades": 0.5 + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.3, + "vest": 0.5, + "helm": 0.5, "laser": 0.6, "silencer": 0.6, - "slippers": 0.2, + "slippers": -0.0, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, + "aggressiveness": 0.8, + "teamwork": 0.5, "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.8, + "communicationTone": 0.8, "movementStyle": 0.5, - "objectiveFocus": 0.5 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_clan/tremor.json b/bots/bots_clan/tremor.json index ec0e95c28..c72450cc7 100644 --- a/bots/bots_clan/tremor.json +++ b/bots/bots_clan/tremor.json @@ -1,35 +1,35 @@ { "name": "Tremor", - "clan_id": 33, - "skin": "male/indy", + "clan_id": 90, + "skin": "terror/terror", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.2, - "mp5": 0.5, + "dual_mk23": 0.5, + "mp5": 0.3, "m4": 0.3, - "m3": 0.7, + "m3": 0.6, "hc": 0.7, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.5 + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.9, - "objectiveFocus": 0.3 + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/trench.json b/bots/bots_clan/trench.json index d4cd278f2..67ca24b3f 100644 --- a/bots/bots_clan/trench.json +++ b/bots/bots_clan/trench.json @@ -1,35 +1,35 @@ { "name": "Trench", - "clan_id": 64, - "skin": "male/indy", + "clan_id": 35, + "skin": "actionmale/morpheus", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.7 + "mk23": 0.8, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.4, + "m3": 0.2, + "hc": 0.1, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.9, + "vest": 0.7, + "helm": 0.4, + "laser": 0.8, "silencer": 0.9, "slippers": 0.7, - "bandolier": 0.3 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.5, "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.2 + "aimingSkill": 0.2, + "reactionTime": 0.7, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/turkish.json b/bots/bots_clan/turkish.json index 8d5a7893a..732f12733 100644 --- a/bots/bots_clan/turkish.json +++ b/bots/bots_clan/turkish.json @@ -1,35 +1,35 @@ { "name": "Turkish", - "clan_id": 74, - "skin": "male/indy", + "clan_id": 81, + "skin": "terror/stormtrooper", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.2, - "hc": 0.6, + "mp5": 0.3, + "m4": 0.6, + "m3": 0.4, + "hc": 0.5, "sniper": 0.4, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.8, "helm": 0.5, - "laser": 0.8, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.8 + "laser": 0.1, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, + "aggressiveness": 0.5, + "teamwork": 0.4, "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.9, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.9, + "movementStyle": 0.8, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/uriel.json b/bots/bots_clan/uriel.json index 8367aed73..4999e536e 100644 --- a/bots/bots_clan/uriel.json +++ b/bots/bots_clan/uriel.json @@ -1,35 +1,35 @@ { "name": "Uriel", - "clan_id": 41, - "skin": "male/indy", + "clan_id": 97, + "skin": "male/nut", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, + "mk23": 0.5, + "dual_mk23": 0.8, "mp5": 0.6, - "m4": 0.3, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.5 + "m4": 0.6, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": -0.1, - "helm": 0.4, - "laser": 0.3, + "vest": 1.0, + "helm": 0.7, + "laser": 0.8, "silencer": 0.4, "slippers": 0.4, - "bandolier": 0.6 + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.0 + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_clan/uzaytron.json b/bots/bots_clan/uzaytron.json index ae0925a88..006d75f4a 100644 --- a/bots/bots_clan/uzaytron.json +++ b/bots/bots_clan/uzaytron.json @@ -1,35 +1,35 @@ { "name": "uzayTron", - "clan_id": 10, - "skin": "male/indy", + "clan_id": 40, + "skin": "terror/black cop", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.7, + "mk23": 0.5, + "dual_mk23": 0.5, + "mp5": 0.1, + "m4": 0.6, "m3": 0.5, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.2 + "hc": 0.7, + "sniper": 0.4, + "knives": 0.7, + "grenades": 0.9 }, "itemPreferences": { - "vest": -0.0, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.8, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.9, + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.8, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.5, "movementStyle": 0.6, - "objectiveFocus": 1.0 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_clan/vedro.json b/bots/bots_clan/vedro.json index 5ff89242e..b461c1242 100644 --- a/bots/bots_clan/vedro.json +++ b/bots/bots_clan/vedro.json @@ -1,35 +1,35 @@ { "name": "Vedro", - "clan_id": 68, - "skin": "male/indy", + "clan_id": 46, + "skin": "male/wcwsting3", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.9, + "mk23": 0.8, + "dual_mk23": 0.5, "mp5": 0.7, - "m4": 0.4, - "m3": 0.7, + "m4": 0.6, + "m3": 0.6, "hc": 0.4, - "sniper": 0.4, + "sniper": 0.6, "knives": 0.8, - "grenades": 0.5 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.5, + "vest": 0.2, + "helm": -0.2, "laser": 0.5, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.6 + "silencer": 0.8, + "slippers": 0.2, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.3, "teamwork": 0.4, "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.2 + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/venz.json b/bots/bots_clan/venz.json index 4aa29f682..c34564222 100644 --- a/bots/bots_clan/venz.json +++ b/bots/bots_clan/venz.json @@ -1,35 +1,35 @@ { "name": "Venz", - "clan_id": 44, - "skin": "male/indy", + "clan_id": 35, + "skin": "terror/swat", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 1.0, - "m4": 0.6, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.7, + "hc": 0.1, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.9, - "bandolier": 0.7 + "vest": 0.8, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": 0.3, + "curiosity": 0.2, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 1.0, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/vera.json b/bots/bots_clan/vera.json deleted file mode 100644 index feae567fb..000000000 --- a/bots/bots_clan/vera.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Vera", - "clan_id": 50, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.0, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.9, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_clan/waxman.json b/bots/bots_clan/waxman.json deleted file mode 100644 index 864c023cc..000000000 --- a/bots/bots_clan/waxman.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Waxman", - "clan_id": 51, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.9, - "m3": 0.4, - "hc": 0.3, - "sniper": 1.1, - "knives": 0.7, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.9, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_clan/wayofthe.json b/bots/bots_clan/wayofthe.json index 57a876f30..f579663ee 100644 --- a/bots/bots_clan/wayofthe.json +++ b/bots/bots_clan/wayofthe.json @@ -1,35 +1,35 @@ { "name": "Way Of The", - "clan_id": 13, - "skin": "male/indy", + "clan_id": 48, + "skin": "male/cop", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.4 + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.1, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.3, - "helm": 0.7, - "laser": 0.8, + "helm": 0.6, + "laser": -0.1, "silencer": 0.3, - "slippers": 0.7, + "slippers": 0.5, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.5, "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.9, + "aimingSkill": 0.4, + "reactionTime": -0.1, + "communicationFreq": 0.4, "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_clan/wesleysnipes.json b/bots/bots_clan/wesleysnipes.json deleted file mode 100644 index 21a13f7d8..000000000 --- a/bots/bots_clan/wesleysnipes.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Wesley Snipes", - "clan_id": 1, - "skin": "messiah/blade", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.9, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.9, - "slippers": 0.3, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_clan/yarbro.json b/bots/bots_clan/yarbro.json index 7c31e57f4..d910f81f6 100644 --- a/bots/bots_clan/yarbro.json +++ b/bots/bots_clan/yarbro.json @@ -1,35 +1,35 @@ { "name": "Yarbro", - "clan_id": 72, - "skin": "male/indy", + "clan_id": 26, + "skin": "actionmale/blood", "weaponPreferences": { - "mk23": 0.7, + "mk23": 0.6, "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.7, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.5, "knives": 0.6, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.4, "helm": 0.4, "laser": 0.4, - "silencer": 0.4, + "silencer": 0.9, "slippers": 0.3, - "bandolier": 0.6 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.2, + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.5, "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.2 + "movementStyle": 0.3, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/2dcitizen.json b/bots/bots_pub/2dcitizen.json index 77b31d6bf..73b6f7229 100644 --- a/bots/bots_pub/2dcitizen.json +++ b/bots/bots_pub/2dcitizen.json @@ -1,35 +1,35 @@ { "name": "2DCitizen", "clan_id": null, - "skin": "male/indy", + "skin": "male/habs", "weaponPreferences": { - "mk23": 0.8, + "mk23": 0.9, "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.7, + "mp5": 0.9, + "m4": 0.6, "m3": 0.5, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 + "hc": -0.2, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.5 + "vest": -0.1, + "helm": 0.7, + "laser": 0.7, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.8, + "teamwork": 0.7, "curiosity": 0.6, - "aimingSkill": -0.0, - "reactionTime": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.8, "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/_gholem_.json b/bots/bots_pub/_gholem_.json index b8bc0abb2..77484b7af 100644 --- a/bots/bots_pub/_gholem_.json +++ b/bots/bots_pub/_gholem_.json @@ -1,35 +1,35 @@ { "name": "_Gholem_", "clan_id": null, - "skin": "male/indy", + "skin": "aqmarine/urban", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.0, - "mp5": 0.5, - "m4": 0.9, - "m3": 0.5, + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.6, + "m3": 0.2, "hc": 0.2, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.6 + "sniper": 0.8, + "knives": 0.7, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.6, "helm": 0.5, - "laser": 1.0, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.5 + "laser": 0.8, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.5, + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.7, "aimingSkill": 0.4, - "reactionTime": 0.0, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/_glich.json b/bots/bots_pub/_glich.json index 0aca0218f..fc0020e6b 100644 --- a/bots/bots_pub/_glich.json +++ b/bots/bots_pub/_glich.json @@ -1,35 +1,35 @@ { "name": "_GLiCH", "clan_id": null, - "skin": "male/indy", + "skin": "terror/skyterr", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.4, "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 1.0, - "m3": 0.1, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.6 + "mp5": 0.3, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.9, - "silencer": 0.5, - "slippers": 0.9, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.4, + "laser": 0.3, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.1, + "aggressiveness": 0.5, + "teamwork": 0.5, + "curiosity": 0.4, "aimingSkill": 0.8, - "reactionTime": 0.4, - "communicationFreq": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "movementStyle": 0.1, + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_pub/_nixxy_.json b/bots/bots_pub/_nixxy_.json index 8fdf2af18..bf1523481 100644 --- a/bots/bots_pub/_nixxy_.json +++ b/bots/bots_pub/_nixxy_.json @@ -1,35 +1,35 @@ { "name": "_*NiXXy*_", "clan_id": null, - "skin": "male/indy", + "skin": "sas/deathreat", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.1, - "m4": 0.3, - "m3": 0.4, + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.5, "hc": 0.3, - "sniper": 0.2, - "knives": 0.6, - "grenades": 0.9 + "sniper": 0.1, + "knives": 0.1, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.4, - "silencer": 0.4, + "vest": 0.7, + "helm": 0.4, + "laser": 1.0, + "silencer": 0.6, "slippers": 0.6, - "bandolier": 0.2 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, + "aggressiveness": 0.7, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.7, + "communicationFreq": 0.7, "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/amadi.json b/bots/bots_pub/amadi.json index 8371d3bdb..9aba4bc1a 100644 --- a/bots/bots_pub/amadi.json +++ b/bots/bots_pub/amadi.json @@ -1,35 +1,35 @@ { "name": "Amadi", "clan_id": null, - "skin": "male/indy", + "skin": "male/bluebeard", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.7, "dual_mk23": 0.3, "mp5": 0.7, - "m4": 0.1, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 + "m4": 0.6, + "m3": 0.8, + "hc": 0.6, + "sniper": 1.0, + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.4 + "vest": 0.3, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.2, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.9, + "aggressiveness": 0.8, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.7, "movementStyle": 0.5, - "objectiveFocus": 0.5 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/amu.json b/bots/bots_pub/amu.json deleted file mode 100644 index 2f3400885..000000000 --- a/bots/bots_pub/amu.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Amu", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.1, - "movementStyle": 0.8, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/anarki.json b/bots/bots_pub/anarki.json deleted file mode 100644 index af75fe942..000000000 --- a/bots/bots_pub/anarki.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Anarki", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.9, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/andromeed.json b/bots/bots_pub/andromeed.json index 93e62e3bb..ad328fe80 100644 --- a/bots/bots_pub/andromeed.json +++ b/bots/bots_pub/andromeed.json @@ -1,35 +1,35 @@ { "name": "Andromeed", "clan_id": null, - "skin": "male/indy", + "skin": "male/habs", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": -0.0, - "mp5": 0.4, - "m4": 0.9, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.4, "helm": 0.4, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.7 + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.6, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/ant__dog.json b/bots/bots_pub/ant__dog.json index fc05a3b9f..a8c9589ff 100644 --- a/bots/bots_pub/ant__dog.json +++ b/bots/bots_pub/ant__dog.json @@ -1,35 +1,35 @@ { "name": "Ant__Dog", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/robber", "weaponPreferences": { "mk23": 0.4, "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.9 + "mp5": 0.3, + "m4": 0.5, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.3, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.8, + "aimingSkill": 0.9, "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.2 + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.8, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/argo.json b/bots/bots_pub/argo.json index dddcb2cd1..ca138b77b 100644 --- a/bots/bots_pub/argo.json +++ b/bots/bots_pub/argo.json @@ -1,35 +1,35 @@ { "name": "Argo", "clan_id": null, - "skin": "male/indy", + "skin": "terror/fbiterr", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.5, + "mk23": 1.0, + "dual_mk23": 0.7, + "mp5": 0.6, + "m4": 0.1, "m3": 0.6, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.3 + "hc": 0.5, + "sniper": 0.2, + "knives": 0.1, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.5, + "vest": 0.4, + "helm": 0.4, + "laser": 1.0, + "silencer": 0.6, "slippers": 0.5, - "bandolier": 0.5 + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, + "aggressiveness": 0.2, + "teamwork": 1.0, + "curiosity": 0.4, "aimingSkill": 0.8, - "reactionTime": 0.2, - "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.3 + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/arklon.json b/bots/bots_pub/arklon.json deleted file mode 100644 index ec04d9193..000000000 --- a/bots/bots_pub/arklon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Arklon", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 1.0, - "mp5": 0.4, - "m4": 0.8, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.1, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ashrah.json b/bots/bots_pub/ashrah.json deleted file mode 100644 index b2abc8029..000000000 --- a/bots/bots_pub/ashrah.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ashrah", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.2, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.2, - "reactionTime": 0.8, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/backfire.json b/bots/bots_pub/backfire.json index ddcc0f102..817468e9f 100644 --- a/bots/bots_pub/backfire.json +++ b/bots/bots_pub/backfire.json @@ -1,35 +1,35 @@ { "name": "Backfire", "clan_id": null, - "skin": "male/indy", + "skin": "terror/ctf_b", "weaponPreferences": { - "mk23": 0.1, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.2, - "knives": 0.1, + "mp5": 0.2, + "m4": 0.2, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.7, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, + "vest": 0.6, + "helm": 0.7, "laser": 0.6, "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": -0.1, + "aggressiveness": 0.5, + "teamwork": 0.3, "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.6, + "movementStyle": 0.0, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/bacon.json b/bots/bots_pub/bacon.json deleted file mode 100644 index b06fc7d67..000000000 --- a/bots/bots_pub/bacon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bacon", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.8, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.1, - "laser": 0.4, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/badsam.json b/bots/bots_pub/badsam.json deleted file mode 100644 index a526afff5..000000000 --- a/bots/bots_pub/badsam.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bad Sam", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.5, - "grenades": 1.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/baraka.json b/bots/bots_pub/baraka.json index a86032969..02f5bfd38 100644 --- a/bots/bots_pub/baraka.json +++ b/bots/bots_pub/baraka.json @@ -1,35 +1,35 @@ { "name": "Baraka", "clan_id": null, - "skin": "male/indy", + "skin": "male/ctf_vip_r", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, + "mk23": 0.2, + "dual_mk23": 0.6, "mp5": 0.5, - "m4": 0.3, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, + "m4": 0.6, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.5, "knives": 0.3, - "grenades": 0.6 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.9, - "slippers": 0.9, - "bandolier": 0.6 + "vest": 0.7, + "helm": 0.2, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.4, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.5, + "communicationTone": 0.3, + "movementStyle": 0.6, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/barbarian.json b/bots/bots_pub/barbarian.json index 84dc8629e..b079bc3f7 100644 --- a/bots/bots_pub/barbarian.json +++ b/bots/bots_pub/barbarian.json @@ -1,35 +1,35 @@ { "name": "Barbarian", "clan_id": null, - "skin": "male/indy", + "skin": "male/adidas", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.7, + "mk23": 0.8, + "dual_mk23": 0.6, "mp5": 0.3, - "m4": 0.4, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.5 + "m4": 0.3, + "m3": 0.6, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, + "vest": 0.7, "helm": 0.4, - "laser": 0.1, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.5 + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.5, + "aggressiveness": 0.0, + "teamwork": 0.6, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.6, "communicationFreq": 0.5, - "communicationTone": 0.1, - "movementStyle": 0.4, - "objectiveFocus": 0.8 + "communicationTone": 0.8, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/bean.json b/bots/bots_pub/bean.json deleted file mode 100644 index f0f1d4706..000000000 --- a/bots/bots_pub/bean.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bean", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.2, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.0, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.9, - "movementStyle": 0.3, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/beelz.json b/bots/bots_pub/beelz.json deleted file mode 100644 index f0d744ac5..000000000 --- a/bots/bots_pub/beelz.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Beelz", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/belokk.json b/bots/bots_pub/belokk.json deleted file mode 100644 index 1936d2917..000000000 --- a/bots/bots_pub/belokk.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Belokk", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.7, - "silencer": 0.8, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": -0.0, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/benarcher.json b/bots/bots_pub/benarcher.json index a6734f533..1fb9ea45d 100644 --- a/bots/bots_pub/benarcher.json +++ b/bots/bots_pub/benarcher.json @@ -1,35 +1,35 @@ { "name": "Ben Archer", "clan_id": null, - "skin": "male/indy", + "skin": "aqmarine/centurion", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.7, + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.9, + "m3": 0.2, + "hc": 0.1, + "sniper": 0.1, + "knives": 0.8, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.4, "helm": 0.5, - "laser": 0.5, + "laser": 0.2, "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.8, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.6, "reactionTime": 0.5, "communicationFreq": 0.6, - "communicationTone": 0.3, + "communicationTone": 0.4, "movementStyle": 0.9, - "objectiveFocus": 0.4 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/berserker.json b/bots/bots_pub/berserker.json index bb0d4e0da..40b25f90f 100644 --- a/bots/bots_pub/berserker.json +++ b/bots/bots_pub/berserker.json @@ -1,35 +1,35 @@ { "name": "Berserker", "clan_id": null, - "skin": "male/indy", + "skin": "terror/csw blue", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.4 + "mk23": 0.6, + "dual_mk23": 0.2, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.8, + "hc": 0.6, + "sniper": 0.6, + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.1, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.3 + "vest": 0.5, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.4, + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, + "reactionTime": 0.7, "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.2 + "communicationTone": 0.6, + "movementStyle": 0.2, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/bigmack.json b/bots/bots_pub/bigmack.json index e5cea6e34..7fc282c81 100644 --- a/bots/bots_pub/bigmack.json +++ b/bots/bots_pub/bigmack.json @@ -1,35 +1,35 @@ { "name": "BigMack", "clan_id": null, - "skin": "male/indy", + "skin": "terror/swat", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.2, - "hc": 0.3, + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 1.1, + "m3": 0.5, + "hc": -0.0, "sniper": 0.4, - "knives": 0.4, - "grenades": 0.4 + "knives": 0.5, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.7, - "laser": 0.5, + "vest": 0.3, + "helm": 0.8, + "laser": 0.2, "silencer": 0.8, - "slippers": 0.5, - "bandolier": 0.4 + "slippers": 0.9, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, + "aggressiveness": 0.3, + "teamwork": 0.5, "curiosity": 0.6, - "aimingSkill": 1.0, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.8 + "aimingSkill": 0.4, + "reactionTime": 0.9, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": -0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/biker.json b/bots/bots_pub/biker.json deleted file mode 100644 index 5735c3ef6..000000000 --- a/bots/bots_pub/biker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Biker", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.5, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 1.0, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 1.0 - } -} \ No newline at end of file diff --git a/bots/bots_pub/bishibosh.json b/bots/bots_pub/bishibosh.json deleted file mode 100644 index 6edbb03f8..000000000 --- a/bots/bots_pub/bishibosh.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bishibosh", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.2, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/blackadder.json b/bots/bots_pub/blackadder.json index 931e92ec1..37c451a95 100644 --- a/bots/bots_pub/blackadder.json +++ b/bots/bots_pub/blackadder.json @@ -1,35 +1,35 @@ { "name": "Blackadder", "clan_id": null, - "skin": "male/indy", + "skin": "female/kw_yellow", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.1, - "m4": 0.7, - "m3": 0.9, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.3 + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.6, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.2, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.7, "helm": 0.6, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 1.0, + "aggressiveness": 0.1, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "movementStyle": 0.3, + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_pub/blaze.json b/bots/bots_pub/blaze.json index 592fff7fd..4fce65a34 100644 --- a/bots/bots_pub/blaze.json +++ b/bots/bots_pub/blaze.json @@ -1,35 +1,35 @@ { "name": "Blaze", "clan_id": null, - "skin": "male/indy", + "skin": "sas/norse1", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.4 + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.2, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.7, + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.2, + "vest": 0.4, + "helm": 0.5, "laser": 0.6, - "silencer": 0.4, - "slippers": 0.7, + "silencer": 0.3, + "slippers": 0.4, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.5, + "aggressiveness": 0.6, + "teamwork": -0.1, + "curiosity": 0.4, + "aimingSkill": 0.1, "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.3, "movementStyle": 0.3, - "objectiveFocus": 0.5 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/bolund.json b/bots/bots_pub/bolund.json deleted file mode 100644 index 82da7569e..000000000 --- a/bots/bots_pub/bolund.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bolund", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.2, - "m4": 0.2, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.2, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.1, - "aimingSkill": 0.5, - "reactionTime": 0.1, - "communicationFreq": 0.2, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/boneash.json b/bots/bots_pub/boneash.json deleted file mode 100644 index 7373e657d..000000000 --- a/bots/bots_pub/boneash.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Boneash", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.6, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.9, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/bonebreak.json b/bots/bots_pub/bonebreak.json deleted file mode 100644 index ebe750646..000000000 --- a/bots/bots_pub/bonebreak.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bonebreak", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, - "hc": -0.0, - "sniper": 0.0, - "knives": 0.6, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/booker.json b/bots/bots_pub/booker.json deleted file mode 100644 index b3820edf7..000000000 --- a/bots/bots_pub/booker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Booker", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.1, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.9, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/bortack.json b/bots/bots_pub/bortack.json index 2693831e8..633dc9fd4 100644 --- a/bots/bots_pub/bortack.json +++ b/bots/bots_pub/bortack.json @@ -1,35 +1,35 @@ { "name": "Bortack", "clan_id": null, - "skin": "male/indy", + "skin": "terror/stormtrooper", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.0, "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.5 + "mp5": 0.4, + "m4": 0.8, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.3, + "knives": 1.0, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.9, + "vest": 0.5, + "helm": 0.3, "laser": 0.3, - "silencer": 0.6, - "slippers": 0.7, + "silencer": 0.5, + "slippers": 0.4, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.1, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.5, + "communicationFreq": 0.2, + "communicationTone": 0.0, + "movementStyle": 0.2, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/bremm.json b/bots/bots_pub/bremm.json deleted file mode 100644 index 47ae3624c..000000000 --- a/bots/bots_pub/bremm.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Bremm", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.9, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/brixos.json b/bots/bots_pub/brixos.json deleted file mode 100644 index 4bbedbf09..000000000 --- a/bots/bots_pub/brixos.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Brixos", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.9, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/brofist.json b/bots/bots_pub/brofist.json index 561a2daf6..664ba40c3 100644 --- a/bots/bots_pub/brofist.json +++ b/bots/bots_pub/brofist.json @@ -1,35 +1,35 @@ { "name": "BroFist", "clan_id": null, - "skin": "male/indy", + "skin": "male/jdredd", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.7, + "mk23": 0.3, + "dual_mk23": 0.4, "mp5": 0.4, - "m4": 0.2, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.5 + "m4": 0.4, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.1, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.4, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 1.1, - "teamwork": 0.8, - "curiosity": 0.9, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.3, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.5, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/brohn.json b/bots/bots_pub/brohn.json index 50c72a920..6d66e9dc7 100644 --- a/bots/bots_pub/brohn.json +++ b/bots/bots_pub/brohn.json @@ -1,35 +1,35 @@ { "name": "Brohn", "clan_id": null, - "skin": "male/indy", + "skin": "terror/fbiterr", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 1.1, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.6, + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.7, "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.6 + "sniper": 0.5, + "knives": 1.0, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, + "aggressiveness": 0.4, + "teamwork": 0.1, "curiosity": 0.2, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/bve.json b/bots/bots_pub/bve.json index 82078bddb..a3bdc7a17 100644 --- a/bots/bots_pub/bve.json +++ b/bots/bots_pub/bve.json @@ -1,35 +1,35 @@ { "name": "BVe", "clan_id": null, - "skin": "male/indy", + "skin": "male/ctf_r", "weaponPreferences": { - "mk23": -0.1, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.9, - "knives": 0.6, - "grenades": 0.9 + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.4, + "m4": 0.5, + "m3": 0.3, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.8, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.9, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.0, + "aggressiveness": 0.5, + "teamwork": 1.0, + "curiosity": 0.3, "aimingSkill": 0.6, - "reactionTime": 0.2, + "reactionTime": 0.4, "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.1, - "objectiveFocus": 0.4 + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/cadaver.json b/bots/bots_pub/cadaver.json index 2136bf263..feeffe3b4 100644 --- a/bots/bots_pub/cadaver.json +++ b/bots/bots_pub/cadaver.json @@ -1,35 +1,35 @@ { "name": "Cadaver", "clan_id": null, - "skin": "male/indy", + "skin": "actionrally/natasha", "weaponPreferences": { "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.4, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.5, + "dual_mk23": 0.5, + "mp5": 0.4, + "m4": 0.6, + "m3": 0.1, + "hc": 0.4, + "sniper": 0.4, "knives": 0.5, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.7, - "silencer": 0.9, - "slippers": 0.8, - "bandolier": 0.2 + "vest": 0.4, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.4, + "slippers": 0.9, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.2, + "aggressiveness": 0.4, + "teamwork": 0.4, + "curiosity": 0.6, + "aimingSkill": 0.4, "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "communicationFreq": 0.3, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/carpenter.json b/bots/bots_pub/carpenter.json index 4e705c353..29ccb370d 100644 --- a/bots/bots_pub/carpenter.json +++ b/bots/bots_pub/carpenter.json @@ -1,35 +1,35 @@ { "name": "Carpenter", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/shinobi", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.1 + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.4, + "m4": 0.8, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.7, + "vest": 0.3, + "helm": 0.8, + "laser": 0.7, + "silencer": 0.3, "slippers": 0.7, - "bandolier": 0.5 + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.2, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/chan.json b/bots/bots_pub/chan.json index 2a129a875..f999fc1fc 100644 --- a/bots/bots_pub/chan.json +++ b/bots/bots_pub/chan.json @@ -1,35 +1,35 @@ { "name": "Chan", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/axe", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, + "mk23": 0.5, + "dual_mk23": 0.6, "mp5": 0.8, - "m4": 0.4, - "m3": 0.6, - "hc": 0.7, + "m4": 0.5, + "m3": 0.3, + "hc": 0.6, "sniper": 0.4, - "knives": 0.6, - "grenades": 0.5 + "knives": 0.4, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.4, - "slippers": 0.3, - "bandolier": 0.5 + "vest": 0.9, + "helm": 0.2, + "laser": 0.9, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, + "aggressiveness": 0.3, + "teamwork": 0.8, "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aimingSkill": 0.4, + "reactionTime": 0.9, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.1, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/church.json b/bots/bots_pub/church.json deleted file mode 100644 index 20699d942..000000000 --- a/bots/bots_pub/church.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Church", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 1.1, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.8, - "reactionTime": 0.9, - "communicationFreq": -0.1, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/cledus.json b/bots/bots_pub/cledus.json deleted file mode 100644 index e76a8f762..000000000 --- a/bots/bots_pub/cledus.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cledus", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/clion.json b/bots/bots_pub/clion.json index d2373eb85..79e529226 100644 --- a/bots/bots_pub/clion.json +++ b/bots/bots_pub/clion.json @@ -1,35 +1,35 @@ { "name": "C-Lion", "clan_id": null, - "skin": "male/indy", + "skin": "male/ctf_y", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.6 + "mk23": 0.8, + "dual_mk23": 0.6, + "mp5": 0.3, + "m4": 0.3, + "m3": 0.5, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.1 }, "itemPreferences": { - "vest": -0.2, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.7, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.1, + "reactionTime": 0.2, "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.6 + "communicationTone": 0.9, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/cobra.json b/bots/bots_pub/cobra.json deleted file mode 100644 index 1e5890395..000000000 --- a/bots/bots_pub/cobra.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cobra", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.9, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.0, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.8, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/corrus.json b/bots/bots_pub/corrus.json deleted file mode 100644 index 7da1f3e97..000000000 --- a/bots/bots_pub/corrus.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "[Corrus]", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.3, - "hc": 0.0, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 1.0, - "curiosity": 0.5, - "aimingSkill": 0.9, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.0, - "movementStyle": 0.6, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/countess.json b/bots/bots_pub/countess.json deleted file mode 100644 index 48cba1c40..000000000 --- a/bots/bots_pub/countess.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Countess", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.0, - "dual_mk23": 0.9, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.1, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.9, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/cowking.json b/bots/bots_pub/cowking.json deleted file mode 100644 index 99c287320..000000000 --- a/bots/bots_pub/cowking.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Cow King", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": -0.0, - "m4": 0.3, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.2, - "reactionTime": 0.2, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/crash.json b/bots/bots_pub/crash.json index 1e13a4b3e..1aa49fe60 100644 --- a/bots/bots_pub/crash.json +++ b/bots/bots_pub/crash.json @@ -1,35 +1,35 @@ { "name": "Crash", "clan_id": null, - "skin": "male/indy", + "skin": "sas/norse2", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 1.1, - "m4": -0.0, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.2, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.4, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.2, + "grenades": 0.7 }, "itemPreferences": { "vest": 0.1, - "helm": 0.1, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.6 + "helm": 0.4, + "laser": 0.3, + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.2, "teamwork": 0.3, - "curiosity": 1.0, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.9 + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.0, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/cybr.json b/bots/bots_pub/cybr.json index 3e35a496d..52bb2567f 100644 --- a/bots/bots_pub/cybr.json +++ b/bots/bots_pub/cybr.json @@ -1,35 +1,35 @@ { "name": "Cybr", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/invince", "weaponPreferences": { - "mk23": 0.7, + "mk23": 0.2, "dual_mk23": 0.4, "mp5": 0.4, - "m4": 0.2, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.1, - "knives": 0.4, - "grenades": 0.3 + "m4": 0.1, + "m3": 0.6, + "hc": 0.1, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.7, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, + "aggressiveness": 0.8, + "teamwork": 1.0, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.7, "communicationTone": 0.4, - "movementStyle": 0.5, + "movementStyle": 0.2, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/cyrax.json b/bots/bots_pub/cyrax.json index 956ee28a9..ef74141ec 100644 --- a/bots/bots_pub/cyrax.json +++ b/bots/bots_pub/cyrax.json @@ -1,35 +1,35 @@ { "name": "Cyrax", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/ctf_b", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.3, + "mk23": 0.3, + "dual_mk23": 0.8, + "mp5": 0.6, "m4": 0.4, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 + "m3": 0.5, + "hc": 0.7, + "sniper": 0.3, + "knives": 0.3, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.8, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.3, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/daegon.json b/bots/bots_pub/daegon.json deleted file mode 100644 index 57c210436..000000000 --- a/bots/bots_pub/daegon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Daegon", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.1, - "laser": 1.0, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dairou.json b/bots/bots_pub/dairou.json deleted file mode 100644 index d7ea57bf7..000000000 --- a/bots/bots_pub/dairou.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dairou", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.1, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/darrius.json b/bots/bots_pub/darrius.json index 183cd8a93..a05288fd3 100644 --- a/bots/bots_pub/darrius.json +++ b/bots/bots_pub/darrius.json @@ -1,34 +1,34 @@ { "name": "Darrius", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/thedon", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, + "mk23": 0.7, + "dual_mk23": 0.4, "mp5": 0.5, - "m4": 0.2, - "m3": 0.4, - "hc": 0.3, + "m4": 0.7, + "m3": 0.2, + "hc": 0.4, "sniper": 0.7, "knives": 0.6, - "grenades": 0.7 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.8, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.9, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, "movementStyle": 0.4, "objectiveFocus": 0.7 } diff --git a/bots/bots_pub/death.json b/bots/bots_pub/death.json index 9d8ace725..f408ca189 100644 --- a/bots/bots_pub/death.json +++ b/bots/bots_pub/death.json @@ -1,35 +1,35 @@ { "name": "Death", "clan_id": null, - "skin": "male/indy", + "skin": "terror/stormtrooper", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.1, "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.4, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.4, + "hc": 0.6, + "sniper": 0.5, "knives": 0.4, - "grenades": 0.9 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 1.0, - "silencer": -0.0, - "slippers": 0.6, - "bandolier": 0.4 + "vest": 0.4, + "helm": 0.9, + "laser": -0.1, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.8, "aimingSkill": 0.4, - "reactionTime": 0.6, + "reactionTime": 0.2, "communicationFreq": 0.8, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.3 + "communicationTone": 0.6, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/deusvault.json b/bots/bots_pub/deusvault.json index a97edab3e..d6ba0e332 100644 --- a/bots/bots_pub/deusvault.json +++ b/bots/bots_pub/deusvault.json @@ -1,35 +1,35 @@ { "name": "deUSvauLT", "clan_id": null, - "skin": "male/indy", + "skin": "male/madmaxy", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.5, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.8, + "hc": 0.2, + "sniper": 0.7, + "knives": 0.7, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.9, + "vest": 0.6, "helm": 0.6, - "laser": 0.9, - "silencer": 0.2, - "slippers": 0.5, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.6, + "aggressiveness": 0.7, + "teamwork": 0.8, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.8, + "communicationTone": 0.4, + "movementStyle": 0.5, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/digz.json b/bots/bots_pub/digz.json index 4d3501c57..bbcab2196 100644 --- a/bots/bots_pub/digz.json +++ b/bots/bots_pub/digz.json @@ -1,35 +1,35 @@ { "name": "Digz", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sasjungle", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.8, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.8, + "m3": 0.9, + "hc": 1.0, + "sniper": 0.7, + "knives": 1.0, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.8, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.3, + "vest": 0.4, + "helm": 0.7, + "laser": 0.5, + "silencer": 0.3, + "slippers": 0.4, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.0, + "curiosity": 0.2, "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.8, - "objectiveFocus": 0.6 + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/dolin.json b/bots/bots_pub/dolin.json deleted file mode 100644 index 15a0fe8fb..000000000 --- a/bots/bots_pub/dolin.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dolin", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dominator.json b/bots/bots_pub/dominator.json deleted file mode 100644 index 4ca33d0e1..000000000 --- a/bots/bots_pub/dominator.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dominator", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 1.0, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 1.0 - } -} \ No newline at end of file diff --git a/bots/bots_pub/doom.json b/bots/bots_pub/doom.json index b418b8b1d..18fa0e247 100644 --- a/bots/bots_pub/doom.json +++ b/bots/bots_pub/doom.json @@ -1,35 +1,35 @@ { "name": "Doom", "clan_id": null, - "skin": "male/indy", + "skin": "sas/saspolice", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.8, - "grenades": 0.4 + "mk23": 0.8, + "dual_mk23": 0.7, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.7, "helm": 0.7, - "laser": 0.2, - "silencer": -0.2, - "slippers": 0.2, - "bandolier": 0.3 + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 1.0, + "reactionTime": 0.8, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/drago.json b/bots/bots_pub/drago.json deleted file mode 100644 index d7c84f364..000000000 --- a/bots/bots_pub/drago.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Drago", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.2, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.7, - "slippers": 0.8, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.9, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": -0.2, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dragoooon.json b/bots/bots_pub/dragoooon.json deleted file mode 100644 index 3577f6979..000000000 --- a/bots/bots_pub/dragoooon.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dragoooon", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/drillbit.json b/bots/bots_pub/drillbit.json index 6970e1aba..1ac496017 100644 --- a/bots/bots_pub/drillbit.json +++ b/bots/bots_pub/drillbit.json @@ -1,35 +1,35 @@ { "name": "Drillbit", "clan_id": null, - "skin": "male/indy", + "skin": "male/shaft", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.1, + "mk23": 0.4, + "dual_mk23": 0.2, + "mp5": 0.3, "m4": 0.4, "m3": 0.5, - "hc": 0.5, - "sniper": 0.9, - "knives": 0.7, - "grenades": 0.5 + "hc": 0.4, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": -0.0, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.4, + "vest": 0.6, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.1, + "slippers": 0.5, "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.3, + "aggressiveness": 0.8, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.7, "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "communicationFreq": 0.7, + "communicationTone": 0.7, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/drjay.json b/bots/bots_pub/drjay.json deleted file mode 100644 index c3496b181..000000000 --- a/bots/bots_pub/drjay.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dr.Jay", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.0, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dutch.json b/bots/bots_pub/dutch.json deleted file mode 100644 index 9f64bd65c..000000000 --- a/bots/bots_pub/dutch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Dutch", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.1, - "hc": 0.5, - "sniper": 0.9, - "knives": 0.4, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.2, - "slippers": 0.8, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/dvorah.json b/bots/bots_pub/dvorah.json deleted file mode 100644 index 2974da968..000000000 --- a/bots/bots_pub/dvorah.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "D'Vorah", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.7, - "grenades": 0.9 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.9, - "movementStyle": 0.6, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ellanil.json b/bots/bots_pub/ellanil.json deleted file mode 100644 index 49bd6770c..000000000 --- a/bots/bots_pub/ellanil.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ellanil", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.6, - "m3": 0.6, - "hc": 0.1, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/enforcer.json b/bots/bots_pub/enforcer.json deleted file mode 100644 index ffab76cab..000000000 --- a/bots/bots_pub/enforcer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Enforcer", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.5, - "mp5": 0.1, - "m4": 0.9, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ermac.json b/bots/bots_pub/ermac.json deleted file mode 100644 index e63a94b61..000000000 --- a/bots/bots_pub/ermac.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ermac", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.1, - "m3": 0.5, - "hc": 0.5, - "sniper": 1.0, - "knives": -0.1, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 1.1, - "laser": 0.8, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/evo89.json b/bots/bots_pub/evo89.json deleted file mode 100644 index b327879be..000000000 --- a/bots/bots_pub/evo89.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Evo89", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.3, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/flamespike.json b/bots/bots_pub/flamespike.json deleted file mode 100644 index 24d625fc9..000000000 --- a/bots/bots_pub/flamespike.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Flamespike", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.4, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.1, - "communicationTone": 0.5, - "movementStyle": 0.8, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/flint.json b/bots/bots_pub/flint.json index bbad8c272..4c7fa7f83 100644 --- a/bots/bots_pub/flint.json +++ b/bots/bots_pub/flint.json @@ -1,35 +1,35 @@ { "name": "Flint", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/axef", "weaponPreferences": { "mk23": 0.3, - "dual_mk23": 0.2, + "dual_mk23": 0.6, "mp5": 0.4, - "m4": 0.5, - "m3": 0.7, - "hc": 0.9, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 + "m4": 0.6, + "m3": 0.4, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.6, - "silencer": -0.1, - "slippers": 0.1, - "bandolier": 0.7 + "vest": 0.5, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": -0.1, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.3, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.9, + "communicationFreq": 0.1, + "communicationTone": 0.5, "movementStyle": 0.4, - "objectiveFocus": 0.7 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/frenchy.json b/bots/bots_pub/frenchy.json deleted file mode 100644 index a406601cf..000000000 --- a/bots/bots_pub/frenchy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Frenchy", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.2, - "m4": 0.8, - "m3": 0.2, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.9, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.3, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/fullmonty.json b/bots/bots_pub/fullmonty.json index f535a7b2f..bdfa3d152 100644 --- a/bots/bots_pub/fullmonty.json +++ b/bots/bots_pub/fullmonty.json @@ -1,35 +1,35 @@ { "name": "Fullmonty", "clan_id": null, - "skin": "male/indy", + "skin": "male/blues", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.6, + "mp5": 0.7, + "m4": 0.7, "m3": 0.6, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.3 + "hc": 0.5, + "sniper": 0.3, + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.8, + "vest": 0.6, + "helm": 0.9, "laser": 0.6, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.9 + "silencer": 0.4, + "slippers": 0.0, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.3, - "reactionTime": 0.2, + "aggressiveness": 0.8, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.2, + "reactionTime": 0.6, "communicationFreq": 0.6, - "communicationTone": 0.8, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "communicationTone": 0.1, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/funkanik.json b/bots/bots_pub/funkanik.json deleted file mode 100644 index ccba7c02e..000000000 --- a/bots/bots_pub/funkanik.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "FUNKANIK", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.4, - "hc": 0.1, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.9, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/gali.json b/bots/bots_pub/gali.json deleted file mode 100644 index 284e5a975..000000000 --- a/bots/bots_pub/gali.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Gali", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.0, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/galul.json b/bots/bots_pub/galul.json deleted file mode 100644 index c3423bcaf..000000000 --- a/bots/bots_pub/galul.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Galul", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.8, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.7, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/garbok.json b/bots/bots_pub/garbok.json index 51763656f..51ded52a9 100644 --- a/bots/bots_pub/garbok.json +++ b/bots/bots_pub/garbok.json @@ -1,35 +1,35 @@ { "name": "Garbok", "clan_id": null, - "skin": "male/indy", + "skin": "male/blues", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.7, "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.5 + "mp5": 0.4, + "m4": 0.7, + "m3": 0.3, + "hc": 0.9, + "sniper": 0.3, + "knives": 0.2, + "grenades": 0.8 }, "itemPreferences": { "vest": 0.3, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.7 + "helm": 0.5, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.2, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.1, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 0.2, + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.9, + "reactionTime": 0.4, "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.5 + "communicationTone": 0.9, + "movementStyle": 0.2, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/geleb.json b/bots/bots_pub/geleb.json index ac2dfb9d6..78fd336c1 100644 --- a/bots/bots_pub/geleb.json +++ b/bots/bots_pub/geleb.json @@ -1,35 +1,35 @@ { "name": "Geleb", "clan_id": null, - "skin": "male/indy", + "skin": "male/jules", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, + "mk23": 0.5, + "dual_mk23": 0.2, "mp5": 0.6, - "m4": 0.4, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.6 + "m4": 0.3, + "m3": 0.3, + "hc": 0.8, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.4, + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, "silencer": 0.5, - "slippers": 0.7, + "slippers": 0.4, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.6, + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.7, "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.5, + "reactionTime": 0.3, + "communicationFreq": 0.5, + "communicationTone": 0.7, "movementStyle": 0.4, - "objectiveFocus": 0.5 + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/george.json b/bots/bots_pub/george.json deleted file mode 100644 index 487bcdc93..000000000 --- a/bots/bots_pub/george.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "George", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.2, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ghostbl4de.json b/bots/bots_pub/ghostbl4de.json deleted file mode 100644 index bfb300111..000000000 --- a/bots/bots_pub/ghostbl4de.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "GhOstBl4de", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.6, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/gladiator.json b/bots/bots_pub/gladiator.json index 489c3d94e..3d26791f9 100644 --- a/bots/bots_pub/gladiator.json +++ b/bots/bots_pub/gladiator.json @@ -1,35 +1,35 @@ { "name": "Gladiator", "clan_id": null, - "skin": "male/indy", + "skin": "sas/saspolice", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 1.1, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.4, + "mk23": 0.3, + "dual_mk23": 0.7, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.3, "hc": 0.4, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.9 + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { "vest": 0.3, - "helm": 0.4, - "laser": 0.8, - "silencer": 0.9, - "slippers": 0.3, - "bandolier": 0.7 + "helm": 0.2, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.3, + "aggressiveness": 0.8, + "teamwork": 0.3, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.2, "communicationFreq": 0.4, "communicationTone": 0.4, "movementStyle": 0.4, - "objectiveFocus": 0.7 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/gogman.json b/bots/bots_pub/gogman.json index 2a27b4066..ea7ea9794 100644 --- a/bots/bots_pub/gogman.json +++ b/bots/bots_pub/gogman.json @@ -1,35 +1,35 @@ { "name": "gogman", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/jewels", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.9, - "m3": 0.4, - "hc": 0.1, - "sniper": 0.7, - "knives": 0.2, - "grenades": 0.3 + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, + "vest": 0.6, + "helm": 0.5, "laser": 0.7, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.7 + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.1 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.4, "teamwork": 0.3, "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 1.0 + "aimingSkill": 0.5, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/grimm.json b/bots/bots_pub/grimm.json deleted file mode 100644 index 4421d74b0..000000000 --- a/bots/bots_pub/grimm.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Grimm", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.9, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/grog.json b/bots/bots_pub/grog.json deleted file mode 100644 index d41618cd1..000000000 --- a/bots/bots_pub/grog.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Grog", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.3, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.5, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.3, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/grunt.json b/bots/bots_pub/grunt.json deleted file mode 100644 index 8da6efadb..000000000 --- a/bots/bots_pub/grunt.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Grunt", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.5, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.4, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/gun.json b/bots/bots_pub/gun.json index 3cf7f62d9..8d0e9f422 100644 --- a/bots/bots_pub/gun.json +++ b/bots/bots_pub/gun.json @@ -1,35 +1,35 @@ { "name": "GuN", "clan_id": null, - "skin": "male/indy", + "skin": "terror/adf2", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.9, - "m4": 0.3, + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.2, "m3": 0.5, - "hc": 0.3, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.1 + "hc": 0.9, + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.8, + "vest": 0.6, + "helm": 0.6, + "laser": 0.6, "silencer": 0.5, - "slippers": 0.1, - "bandolier": 0.7 + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/hal9000.json b/bots/bots_pub/hal9000.json deleted file mode 100644 index 117e170f7..000000000 --- a/bots/bots_pub/hal9000.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "HAL-9000", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.4, - "hc": 0.1, - "sniper": 0.2, - "knives": 0.3, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/halfwit.json b/bots/bots_pub/halfwit.json deleted file mode 100644 index fcf0bcd39..000000000 --- a/bots/bots_pub/halfwit.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Halfwit", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.6, - "hc": 0.9, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.7, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.1, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/hambone.json b/bots/bots_pub/hambone.json deleted file mode 100644 index 7da0e7597..000000000 --- a/bots/bots_pub/hambone.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hambone", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.7, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.9 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/havo.json b/bots/bots_pub/havo.json deleted file mode 100644 index f1c7fa85f..000000000 --- a/bots/bots_pub/havo.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Havo", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.8, - "m4": 0.2, - "m3": 0.7, - "hc": 0.2, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.1 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.8, - "laser": 0.4, - "silencer": 0.1, - "slippers": 0.8, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.3, - "movementStyle": 0.8, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/hellhound.json b/bots/bots_pub/hellhound.json deleted file mode 100644 index a3c3fc267..000000000 --- a/bots/bots_pub/hellhound.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hellhound", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.2, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/helmut.json b/bots/bots_pub/helmut.json deleted file mode 100644 index d08428d72..000000000 --- a/bots/bots_pub/helmut.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Helmut", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.6, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, - "curiosity": 0.1, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/himself.json b/bots/bots_pub/himself.json deleted file mode 100644 index d2ead7ab3..000000000 --- a/bots/bots_pub/himself.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Himself", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.4, - "m3": 0.1, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/hotaru.json b/bots/bots_pub/hotaru.json index 2b9a4b886..a0311c225 100644 --- a/bots/bots_pub/hotaru.json +++ b/bots/bots_pub/hotaru.json @@ -1,35 +1,35 @@ { "name": "Hotaru", "clan_id": null, - "skin": "male/indy", + "skin": "male/babarracuda", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.8, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.6, + "m3": 0.5, "hc": 0.3, - "sniper": 0.2, - "knives": 0.2, + "sniper": 0.8, + "knives": 0.6, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.1, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.9, + "helm": 0.5, + "laser": 0.8, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.3, "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/hozef8.json b/bots/bots_pub/hozef8.json index 0ef55cbe4..918ea2ce1 100644 --- a/bots/bots_pub/hozef8.json +++ b/bots/bots_pub/hozef8.json @@ -1,35 +1,35 @@ { "name": "hozef-8", "clan_id": null, - "skin": "male/indy", + "skin": "actionrally/modsquad", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.1, - "mp5": 0.5, - "m4": 0.6, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.6, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.9, + "hc": 0.7, + "sniper": 0.4, "knives": 0.8, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.8, + "helm": 0.4, + "laser": -0.0, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.2, + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.7, "movementStyle": 0.4, - "objectiveFocus": 0.8 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/hunter.json b/bots/bots_pub/hunter.json deleted file mode 100644 index e0868db06..000000000 --- a/bots/bots_pub/hunter.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Hunter", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.8, - "movementStyle": 0.7, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/icexo.json b/bots/bots_pub/icexo.json index cc5001e23..3f3aa79b8 100644 --- a/bots/bots_pub/icexo.json +++ b/bots/bots_pub/icexo.json @@ -1,35 +1,35 @@ { "name": "Icexo", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/bluceree", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.8 + "dual_mk23": 0.5, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.1, + "knives": 0.3, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.2, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.4, + "laser": 0.7, + "silencer": 0.6, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, + "aggressiveness": 0.6, "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.4, - "objectiveFocus": 0.3 + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/imentu.json b/bots/bots_pub/imentu.json index a7d502ac2..2ef0ae0b7 100644 --- a/bots/bots_pub/imentu.json +++ b/bots/bots_pub/imentu.json @@ -1,35 +1,35 @@ { "name": "Imentu", "clan_id": null, - "skin": "male/indy", + "skin": "male/brucelee", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.4, + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": -0.1, + "m4": 0.1, + "m3": 0.6, + "hc": 0.9, + "sniper": 0.2, + "knives": 0.5, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.0, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.1 + "vest": 0.7, + "helm": 0.2, + "laser": 0.6, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.6, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.4, "reactionTime": 0.5, "communicationFreq": 0.6, "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/itzchamp.json b/bots/bots_pub/itzchamp.json index 865190cb0..ef681d0a5 100644 --- a/bots/bots_pub/itzchamp.json +++ b/bots/bots_pub/itzchamp.json @@ -1,35 +1,35 @@ { "name": "{itzChamp}", "clan_id": null, - "skin": "male/indy", + "skin": "sas/fbi", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.2, - "mp5": 0.7, - "m4": 0.1, - "m3": 0.6, - "hc": 0.1, - "sniper": 0.7, - "knives": 0.2, - "grenades": 0.5 + "mk23": 0.5, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": -0.0, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.9, - "slippers": 0.9, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.7, + "slippers": 0.6, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.8 + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/ivandrago.json b/bots/bots_pub/ivandrago.json index 743597873..d358f6181 100644 --- a/bots/bots_pub/ivandrago.json +++ b/bots/bots_pub/ivandrago.json @@ -1,35 +1,35 @@ { "name": "Ivan Drago", "clan_id": null, - "skin": "male/indy", + "skin": "male/image", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.8, - "hc": 0.8, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.8 + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.3, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.2, + "vest": 0.6, + "helm": 0.8, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.7, "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.6, "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.9, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/jacksaxon.json b/bots/bots_pub/jacksaxon.json index 2ef52bd5a..189d6cc43 100644 --- a/bots/bots_pub/jacksaxon.json +++ b/bots/bots_pub/jacksaxon.json @@ -1,34 +1,34 @@ { "name": "Jack Saxon", "clan_id": null, - "skin": "male/indy", + "skin": "male/hall", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.9, - "m4": 0.4, - "m3": 0.6, - "hc": 0.7, + "mk23": 0.2, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.7, + "hc": 0.9, "sniper": 0.4, - "knives": 0.3, - "grenades": -0.1 + "knives": 0.9, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.5, + "vest": 0.7, + "helm": 0.2, + "laser": 0.7, + "silencer": 0.9, "slippers": 0.5, - "bandolier": 0.5 + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, + "aggressiveness": 0.5, + "teamwork": 0.2, + "curiosity": 0.9, + "aimingSkill": 0.3, + "reactionTime": 0.9, + "communicationFreq": 0.7, + "communicationTone": 0.8, "movementStyle": 0.5, "objectiveFocus": 0.6 } diff --git a/bots/bots_pub/jade.json b/bots/bots_pub/jade.json index 5dfd6666b..1e2d4448d 100644 --- a/bots/bots_pub/jade.json +++ b/bots/bots_pub/jade.json @@ -1,35 +1,35 @@ { "name": "Jade", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sas-urban", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.2, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.5 + "mk23": 0.7, + "dual_mk23": 0.9, + "mp5": 0.9, + "m4": 0.7, + "m3": 0.4, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.9 + "vest": 0.7, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.1, - "aimingSkill": 0.6, - "reactionTime": 0.5, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.8, "communicationFreq": 0.5, - "communicationTone": 0.6, + "communicationTone": 0.4, "movementStyle": 0.3, - "objectiveFocus": 0.4 + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/jax.json b/bots/bots_pub/jax.json index 2a9e53586..a7621c08e 100644 --- a/bots/bots_pub/jax.json +++ b/bots/bots_pub/jax.json @@ -1,35 +1,35 @@ { "name": "Jax", "clan_id": null, - "skin": "male/indy", + "skin": "male/invince", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.2, + "mk23": 0.6, + "dual_mk23": 0.5, "mp5": 0.5, - "m4": 0.5, - "m3": 0.1, - "hc": 0.5, + "m4": 0.6, + "m3": 0.2, + "hc": 0.3, "sniper": 0.3, - "knives": 0.4, + "knives": 0.7, "grenades": 0.4 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.5, + "laser": 0.1, + "silencer": 1.1, + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.9, + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.2, "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.3, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/jcdenton.json b/bots/bots_pub/jcdenton.json deleted file mode 100644 index 73a55a148..000000000 --- a/bots/bots_pub/jcdenton.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "JC Denton", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.9, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.8, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/jjmcquade.json b/bots/bots_pub/jjmcquade.json index 44efee10e..344d63372 100644 --- a/bots/bots_pub/jjmcquade.json +++ b/bots/bots_pub/jjmcquade.json @@ -1,35 +1,35 @@ { "name": "JJ McQuade", "clan_id": null, - "skin": "male/indy", + "skin": "male/aqgthug", "weaponPreferences": { - "mk23": 0.7, + "mk23": 1.0, "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.7, + "mp5": 0.8, + "m4": 0.3, "m3": 0.6, "hc": 0.6, "sniper": 0.3, - "knives": 0.3, - "grenades": 0.6 + "knives": 1.1, + "grenades": 0.4 }, "itemPreferences": { "vest": 0.3, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.2 + "helm": 0.5, + "laser": 0.5, + "silencer": 0.8, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.2, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.2, + "reactionTime": 0.5, "communicationFreq": 0.5, - "communicationTone": 0.6, + "communicationTone": 0.7, "movementStyle": 0.6, - "objectiveFocus": 0.6 + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/joejoiner.json b/bots/bots_pub/joejoiner.json deleted file mode 100644 index 45d3c5877..000000000 --- a/bots/bots_pub/joejoiner.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Joe Joiner", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.6, - "hc": 0.6, - "sniper": 1.2, - "knives": 0.1, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.8, - "slippers": 0.9, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.9, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/joesanto.json b/bots/bots_pub/joesanto.json deleted file mode 100644 index e07ddadb8..000000000 --- a/bots/bots_pub/joesanto.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Joe Santo", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/joetanto.json b/bots/bots_pub/joetanto.json index 1a1fb5bf2..457726cad 100644 --- a/bots/bots_pub/joetanto.json +++ b/bots/bots_pub/joetanto.json @@ -1,35 +1,35 @@ { "name": "Joe Tanto", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sasuc", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.6, "dual_mk23": 0.7, - "mp5": 0.9, - "m4": 0.3, + "mp5": 0.1, + "m4": 0.2, "m3": 0.6, - "hc": 0.6, - "sniper": 0.7, - "knives": 0.7, - "grenades": 0.5 + "hc": 1.1, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.7 + "vest": 0.7, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.3, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, + "aggressiveness": 0.8, + "teamwork": 0.8, + "curiosity": 0.3, "aimingSkill": 0.4, "reactionTime": 0.5, "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.5, - "objectiveFocus": 0.6 + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/kaa.json b/bots/bots_pub/kaa.json deleted file mode 100644 index 23ab34933..000000000 --- a/bots/bots_pub/kaa.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kaa", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.8 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.9, - "movementStyle": 0.7, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kabal.json b/bots/bots_pub/kabal.json deleted file mode 100644 index 130385fde..000000000 --- a/bots/bots_pub/kabal.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kabal", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kalidor.json b/bots/bots_pub/kalidor.json index 9e73aee8b..5d302f0ed 100644 --- a/bots/bots_pub/kalidor.json +++ b/bots/bots_pub/kalidor.json @@ -1,35 +1,35 @@ { "name": "Kalidor", "clan_id": null, - "skin": "male/indy", + "skin": "male/hall", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.4, + "dual_mk23": 0.6, + "mp5": 0.1, "m4": 0.6, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.8, - "grenades": 0.3 + "m3": 0.8, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.2, + "vest": 0.2, + "helm": 0.3, + "laser": 0.8, "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.6, + "teamwork": 0.5, + "curiosity": 0.5, "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.1, "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "movementStyle": 0.9, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/kenshi.json b/bots/bots_pub/kenshi.json deleted file mode 100644 index adcdd50e7..000000000 --- a/bots/bots_pub/kenshi.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kenshi", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.3, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.6, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.2, - "laser": 0.5, - "silencer": 0.3, - "slippers": 0.2, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.9, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kintaro.json b/bots/bots_pub/kintaro.json index 1000029df..97897c01e 100644 --- a/bots/bots_pub/kintaro.json +++ b/bots/bots_pub/kintaro.json @@ -1,35 +1,35 @@ { "name": "Kintaro", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/chowred", "weaponPreferences": { "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.3 + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.3, + "m3": 0.3, + "hc": 0.9, + "sniper": 0.5, + "knives": 0.2, + "grenades": 0.5 }, "itemPreferences": { "vest": 0.3, - "helm": 0.7, + "helm": 0.6, "laser": 0.6, - "silencer": 0.9, - "slippers": 0.6, - "bandolier": 0.6 + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.6, "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.8, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": 0.2, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.3, "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/kira.json b/bots/bots_pub/kira.json index 506a14e96..ea9a66211 100644 --- a/bots/bots_pub/kira.json +++ b/bots/bots_pub/kira.json @@ -1,35 +1,35 @@ { "name": "Kira", "clan_id": null, - "skin": "male/indy", + "skin": "sas/ctf_r", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.2, - "knives": 0.8, - "grenades": 0.2 + "mk23": 0.4, + "dual_mk23": 0.3, + "mp5": 0.4, + "m4": 0.7, + "m3": 0.5, + "hc": 0.7, + "sniper": 0.8, + "knives": 0.6, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, + "aggressiveness": 0.5, + "teamwork": 0.8, "curiosity": 0.4, - "aimingSkill": 0.3, + "aimingSkill": 0.7, "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "communicationFreq": 0.2, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/kitana.json b/bots/bots_pub/kitana.json index e3096c4b4..fe1860db6 100644 --- a/bots/bots_pub/kitana.json +++ b/bots/bots_pub/kitana.json @@ -1,35 +1,35 @@ { "name": "Kitana", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/chellobeta", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.7, - "m3": 0.7, - "hc": 0.5, + "dual_mk23": 0.4, + "mp5": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.8, "sniper": 0.6, - "knives": 1.1, - "grenades": -0.1 + "knives": 0.7, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.2, - "silencer": 0.7, + "vest": 0.9, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.3, "slippers": 0.6, - "bandolier": 0.2 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.4, "teamwork": 0.4, - "curiosity": 0.2, - "aimingSkill": 0.1, - "reactionTime": 0.8, + "curiosity": 0.1, + "aimingSkill": 0.8, + "reactionTime": 0.6, "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.5 + "movementStyle": 0.3, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/kole.json b/bots/bots_pub/kole.json deleted file mode 100644 index 6bf303408..000000000 --- a/bots/bots_pub/kole.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kole", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.8, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.2, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.2, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kollector.json b/bots/bots_pub/kollector.json deleted file mode 100644 index d461744df..000000000 --- a/bots/bots_pub/kollector.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Kollector", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.8, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/korv.json b/bots/bots_pub/korv.json deleted file mode 100644 index 6877cb6d0..000000000 --- a/bots/bots_pub/korv.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Korv", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.2, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.4, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/kotalkahn.json b/bots/bots_pub/kotalkahn.json index c216d8721..ef2beb0f4 100644 --- a/bots/bots_pub/kotalkahn.json +++ b/bots/bots_pub/kotalkahn.json @@ -1,35 +1,35 @@ { "name": "Kotal Kahn", "clan_id": null, - "skin": "male/indy", + "skin": "male/roger", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.7, + "mk23": 0.4, + "dual_mk23": 0.6, "mp5": 0.4, - "m4": 0.6, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.6, - "knives": 0.5, + "m4": 0.5, + "m3": 0.5, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.9 + "vest": 0.5, + "helm": 0.8, + "laser": 0.5, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.6, + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.5, "communicationTone": 0.5, "movementStyle": 0.4, - "objectiveFocus": 0.5 + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/kungjin.json b/bots/bots_pub/kungjin.json index 2c00ac966..cc092fe04 100644 --- a/bots/bots_pub/kungjin.json +++ b/bots/bots_pub/kungjin.json @@ -1,35 +1,35 @@ { "name": "Kung Jin", "clan_id": null, - "skin": "male/indy", + "skin": "terror/skyterr", "weaponPreferences": { - "mk23": 0.8, + "mk23": 0.5, "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.8, + "mp5": -0.1, + "m4": 0.5, + "m3": 0.9, "hc": 0.5, - "sniper": 0.8, - "knives": 0.4, - "grenades": 0.9 + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.3, - "laser": 0.5, - "silencer": -0.0, + "vest": 0.3, + "helm": 0.6, + "laser": 0.1, + "silencer": 0.6, "slippers": 0.4, - "bandolier": 0.3 + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.9, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.2, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.4, "movementStyle": 0.4, - "objectiveFocus": 0.3 + "objectiveFocus": 1.0 } } \ No newline at end of file diff --git a/bots/bots_pub/kuri.json b/bots/bots_pub/kuri.json index be2a41e2e..05392cbef 100644 --- a/bots/bots_pub/kuri.json +++ b/bots/bots_pub/kuri.json @@ -1,35 +1,35 @@ { "name": "kuri", "clan_id": null, - "skin": "male/indy", + "skin": "male/coolio", "weaponPreferences": { "mk23": 0.5, "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.2, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.2 + "mp5": 0.3, + "m4": 0.3, + "m3": 0.8, + "hc": 0.3, + "sniper": 0.3, + "knives": 0.8, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.7, + "vest": 0.5, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.5, "slippers": 0.1, - "bandolier": 0.6 + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.6, + "teamwork": 0.8, + "curiosity": 0.5, + "aimingSkill": 0.7, "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "communicationFreq": 0.5, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/lawrence.json b/bots/bots_pub/lawrence.json deleted file mode 100644 index efc8b5b91..000000000 --- a/bots/bots_pub/lawrence.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Lawrence", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.0, - "hc": 0.6, - "sniper": 0.9, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.2, - "aimingSkill": 0.4, - "reactionTime": 0.8, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/limei.json b/bots/bots_pub/limei.json index 43b52b6e2..52bdccb0e 100644 --- a/bots/bots_pub/limei.json +++ b/bots/bots_pub/limei.json @@ -1,35 +1,35 @@ { "name": "Li Mei", "clan_id": null, - "skin": "male/indy", + "skin": "actionrally/ctf_r", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.4, + "mk23": 0.5, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.7, + "m3": 0.3, "hc": 0.7, "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 + "knives": 0.1, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.7, + "vest": 0.7, + "helm": 0.7, + "laser": 0.3, "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.8 + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.4, + "aggressiveness": 0.6, + "teamwork": 0.2, "curiosity": 0.3, - "aimingSkill": 0.9, - "reactionTime": 0.4, - "communicationFreq": 0.2, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.5, "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "movementStyle": 0.4, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/littleg.json b/bots/bots_pub/littleg.json index be708edf3..b029c22f5 100644 --- a/bots/bots_pub/littleg.json +++ b/bots/bots_pub/littleg.json @@ -1,35 +1,35 @@ { "name": "Little G", "clan_id": null, - "skin": "male/indy", + "skin": "female/sarah_ohconnor", "weaponPreferences": { "mk23": 0.7, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.1, - "knives": 0.9, - "grenades": 0.8 + "dual_mk23": 0.5, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.8, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.3, + "vest": 0.5, + "helm": 0.5, "laser": 0.9, - "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 + "silencer": 0.7, + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, + "aggressiveness": 0.6, "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.8 + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 1.1, + "communicationTone": 0.7, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/lonewolf.json b/bots/bots_pub/lonewolf.json index f13aefb5e..d933e0764 100644 --- a/bots/bots_pub/lonewolf.json +++ b/bots/bots_pub/lonewolf.json @@ -1,35 +1,35 @@ { "name": "Lone Wolf", "clan_id": null, - "skin": "male/indy", + "skin": "terror/rmafia", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.4, + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.7, + "m3": 0.5, + "hc": 0.6, + "sniper": 0.3, + "knives": 0.1, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.9, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.1, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.7, + "curiosity": 0.4, + "aimingSkill": 0.5, + "reactionTime": 0.1, + "communicationFreq": 0.5, + "communicationTone": 0.2, "movementStyle": 0.5, - "objectiveFocus": 0.3 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/milius.json b/bots/bots_pub/milius.json index cd9dc943f..a73f2102f 100644 --- a/bots/bots_pub/milius.json +++ b/bots/bots_pub/milius.json @@ -1,35 +1,35 @@ { "name": "Milius", "clan_id": null, - "skin": "male/indy", + "skin": "male/chow", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.3, "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.4, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.4 + "mp5": 0.6, + "m4": -0.1, + "m3": 0.7, + "hc": 0.4, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.9 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.6 + "vest": 0.3, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.1, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.6, "teamwork": 0.6, "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.8, - "movementStyle": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.7, + "communicationFreq": 0.9, + "communicationTone": 0.3, + "movementStyle": 0.7, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/mindelos.json b/bots/bots_pub/mindelos.json index 464979367..25dca0b37 100644 --- a/bots/bots_pub/mindelos.json +++ b/bots/bots_pub/mindelos.json @@ -1,35 +1,35 @@ { "name": "Mindelos", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/lfc", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.8, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.3, "m3": 0.3, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.6 + "hc": 0.8, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.4, - "laser": 0.1, + "vest": 0.2, + "helm": 0.6, + "laser": 0.3, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.4 + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.5, + "aggressiveness": 0.4, + "teamwork": 0.9, + "curiosity": 0.3, "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.7, - "movementStyle": 1.0, - "objectiveFocus": 0.5 + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.8, + "movementStyle": 0.6, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/moloch.json b/bots/bots_pub/moloch.json deleted file mode 100644 index 7c7ee9cec..000000000 --- a/bots/bots_pub/moloch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Moloch", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.1, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.3, - "knives": 0.2, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.6, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mongrel.json b/bots/bots_pub/mongrel.json index b96021f68..ac2a9992b 100644 --- a/bots/bots_pub/mongrel.json +++ b/bots/bots_pub/mongrel.json @@ -1,35 +1,35 @@ { "name": "Mongrel", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/blade", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, - "mp5": 0.7, + "mk23": 0.6, + "dual_mk23": 0.3, + "mp5": 0.4, "m4": 0.7, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.3, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.4, "knives": 0.4, "grenades": 0.7 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.1, - "laser": -0.1, - "silencer": 0.6, - "slippers": 0.3, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.2, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.2, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.1, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.5, + "objectiveFocus": 0.0 } } \ No newline at end of file diff --git a/bots/bots_pub/monk.json b/bots/bots_pub/monk.json index 30f3429ce..3a68ac288 100644 --- a/bots/bots_pub/monk.json +++ b/bots/bots_pub/monk.json @@ -1,35 +1,35 @@ { "name": "Monk", "clan_id": null, - "skin": "male/indy", + "skin": "male/cop", "weaponPreferences": { - "mk23": 0.5, + "mk23": 0.0, "dual_mk23": 0.7, "mp5": 0.5, "m4": 0.5, - "m3": 0.6, + "m3": 0.4, "hc": 0.2, - "sniper": 0.2, - "knives": 0.7, - "grenades": 0.8 + "sniper": 0.7, + "knives": 0.6, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.4, "helm": 0.4, - "laser": 0.6, - "silencer": 0.8, + "laser": 0.3, + "silencer": 0.4, "slippers": 0.4, - "bandolier": 0.6 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.9, + "aggressiveness": 0.8, + "teamwork": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.6, "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "communicationFreq": 0.4, + "communicationTone": 0.7, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/monty.json b/bots/bots_pub/monty.json index 63c653dbf..02d69e260 100644 --- a/bots/bots_pub/monty.json +++ b/bots/bots_pub/monty.json @@ -1,35 +1,35 @@ { "name": "Monty", "clan_id": null, - "skin": "male/indy", + "skin": "male/conan", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.8, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.8, - "grenades": 0.1 + "mk23": 0.4, + "dual_mk23": 0.7, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, + "hc": 0.5, + "sniper": 0.7, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.2, + "vest": 0.5, + "helm": 0.4, "laser": 0.8, - "silencer": 0.6, - "slippers": 0.5, + "silencer": 0.4, + "slippers": 0.4, "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.6, + "aggressiveness": 0.8, + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.2, "movementStyle": 0.6, - "objectiveFocus": 0.5 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/morgan.json b/bots/bots_pub/morgan.json index 30e868b2c..1f5c2ba26 100644 --- a/bots/bots_pub/morgan.json +++ b/bots/bots_pub/morgan.json @@ -1,35 +1,35 @@ { "name": "Morgan", "clan_id": null, - "skin": "male/indy", + "skin": "male/aqgthug", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.3, - "hc": 0.7, - "sniper": 0.8, + "mk23": 0.5, + "dual_mk23": 0.8, + "mp5": 0.9, + "m4": 0.2, + "m3": 0.5, + "hc": 0.5, + "sniper": 0.7, "knives": 0.6, - "grenades": 0.5 + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.2, + "vest": 0.4, "helm": 0.6, "laser": 0.6, - "silencer": 0.9, - "slippers": 0.9, - "bandolier": 0.5 + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.4, + "aggressiveness": 0.4, + "teamwork": 0.1, + "curiosity": 0.5, "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.2, - "communicationTone": 0.7, - "movementStyle": 0.7, - "objectiveFocus": 0.2 + "reactionTime": 0.8, + "communicationFreq": 0.7, + "communicationTone": 0.2, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/morgriff.json b/bots/bots_pub/morgriff.json index 658476b26..353df1002 100644 --- a/bots/bots_pub/morgriff.json +++ b/bots/bots_pub/morgriff.json @@ -1,35 +1,35 @@ { "name": "Morgriff", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/cptahab", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.5, - "hc": 0.1, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.7 + "dual_mk23": 0.5, + "mp5": 0.9, + "m4": 0.7, + "m3": 0.2, + "hc": 0.9, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.3 + "vest": 0.3, + "helm": 0.6, + "laser": 0.6, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.8, - "curiosity": 0.5, - "aimingSkill": 0.9, - "reactionTime": 0.1, - "communicationFreq": 0.4, - "communicationTone": 0.8, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.9, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.6, + "communicationTone": 0.4, + "movementStyle": 0.2, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/mrbadger.json b/bots/bots_pub/mrbadger.json index 68a2f304a..ce94bd5dd 100644 --- a/bots/bots_pub/mrbadger.json +++ b/bots/bots_pub/mrbadger.json @@ -1,35 +1,35 @@ { "name": "Mr.Badger", "clan_id": null, - "skin": "male/indy", + "skin": "female/ctf_r", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.2, - "mp5": 0.7, - "m4": 0.7, - "m3": 0.2, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.1, - "grenades": 0.6 + "mk23": 0.7, + "dual_mk23": 0.3, + "mp5": 0.6, + "m4": 0.8, + "m3": 0.9, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.3, + "vest": 0.8, + "helm": 0.3, + "laser": 0.4, "silencer": 0.4, "slippers": 0.4, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.2, + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.3, + "reactionTime": -0.0, "communicationFreq": 0.4, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "communicationTone": 0.9, + "movementStyle": 0.3, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/mungri.json b/bots/bots_pub/mungri.json deleted file mode 100644 index dc356fe55..000000000 --- a/bots/bots_pub/mungri.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Mungri", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.7, - "hc": 0.1, - "sniper": 0.8, - "knives": 0.4, - "grenades": 1.1 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.9, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/mynx.json b/bots/bots_pub/mynx.json index 99bd91e49..d84fe6aa7 100644 --- a/bots/bots_pub/mynx.json +++ b/bots/bots_pub/mynx.json @@ -1,35 +1,35 @@ { "name": "Mynx", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sasUC2", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.4, "dual_mk23": 0.3, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.2, "hc": 0.2, "sniper": 0.6, - "knives": 0.6, - "grenades": -0.0 + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.1, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.2, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.2, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "aggressiveness": 0.7, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.4, + "reactionTime": 0.6, + "communicationFreq": 0.6, + "communicationTone": 0.7, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/nandet.json b/bots/bots_pub/nandet.json index 337d3be6f..a71d9d589 100644 --- a/bots/bots_pub/nandet.json +++ b/bots/bots_pub/nandet.json @@ -1,35 +1,35 @@ { "name": "Nandet", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/outlaw", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.7, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.9, - "grenades": 0.4 + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.4, + "m4": 0.4, + "m3": 0.5, + "hc": 0.5, + "sniper": 1.0, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.1, + "vest": 0.5, + "helm": 0.6, "laser": 0.5, - "silencer": 0.4, - "slippers": 0.6, + "silencer": 0.6, + "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.1, - "curiosity": -0.2, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 1.0, - "communicationTone": 0.3, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "aggressiveness": 0.9, + "teamwork": 0.3, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.5, + "movementStyle": 0.4, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/nickgunar.json b/bots/bots_pub/nickgunar.json index e87ae9155..af4645941 100644 --- a/bots/bots_pub/nickgunar.json +++ b/bots/bots_pub/nickgunar.json @@ -1,35 +1,35 @@ { "name": "Nick Gunar", "clan_id": null, - "skin": "male/indy", + "skin": "male/grunt", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.7, - "m4": 1.0, - "m3": 1.0, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.8, + "dual_mk23": 0.5, + "mp5": 0.2, + "m4": 0.5, + "m3": 0.4, + "hc": 0.4, + "sniper": 0.2, + "knives": 0.5, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "aggressiveness": 0.5, + "teamwork": 0.4, + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.7, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/nightwolf.json b/bots/bots_pub/nightwolf.json deleted file mode 100644 index 06872bca7..000000000 --- a/bots/bots_pub/nightwolf.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Nightwolf", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.8 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.8, - "objectiveFocus": 0.0 - } -} \ No newline at end of file diff --git a/bots/bots_pub/noammo.json b/bots/bots_pub/noammo.json index 4dc8289a7..e02204ff4 100644 --- a/bots/bots_pub/noammo.json +++ b/bots/bots_pub/noammo.json @@ -1,35 +1,35 @@ { "name": "NoAmmo", "clan_id": null, - "skin": "male/indy", + "skin": "terror/swatsnipe", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.2, - "m4": 0.6, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.3, + "m3": 0.2, + "hc": 0.4, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.8 + "vest": 0.5, + "helm": 0.1, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, + "aggressiveness": 0.8, "teamwork": 0.5, - "curiosity": 0.1, - "aimingSkill": 0.7, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.9, - "objectiveFocus": 0.5 + "curiosity": 0.3, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.1, + "movementStyle": 0.4, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/noogie.json b/bots/bots_pub/noogie.json index c430e3039..538d1f5ab 100644 --- a/bots/bots_pub/noogie.json +++ b/bots/bots_pub/noogie.json @@ -1,35 +1,35 @@ { "name": "Noogie", "clan_id": null, - "skin": "male/indy", + "skin": "male/optimus", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.8, - "grenades": 0.5 + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.7, + "m4": 0.7, + "m3": 0.4, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.6 }, "itemPreferences": { "vest": 0.6, - "helm": 0.5, + "helm": 0.7, "laser": 0.7, - "silencer": 0.4, - "slippers": 0.5, + "silencer": 0.3, + "slippers": 0.4, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.6, + "teamwork": 0.9, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.2, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/nundak.json b/bots/bots_pub/nundak.json index 84794effd..f71214323 100644 --- a/bots/bots_pub/nundak.json +++ b/bots/bots_pub/nundak.json @@ -1,35 +1,35 @@ { "name": "Nundak", "clan_id": null, - "skin": "male/indy", + "skin": "male/kw_white", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.2, - "mp5": 0.6, - "m4": 0.5, - "m3": 0.5, - "hc": 0.8, - "sniper": 0.5, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.7, "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.2 + "vest": 0.6, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.7, + "slippers": 0.9, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 1.0, - "reactionTime": 0.0, - "communicationFreq": 0.7, + "aggressiveness": 0.3, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.2, "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "movementStyle": 0.2, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/phobos.json b/bots/bots_pub/phobos.json deleted file mode 100644 index bfb258b92..000000000 --- a/bots/bots_pub/phobos.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Phobos", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.2, - "bandolier": 0.1 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.0, - "movementStyle": 0.6, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/potaytoe.json b/bots/bots_pub/potaytoe.json index 749c132a4..532590b56 100644 --- a/bots/bots_pub/potaytoe.json +++ b/bots/bots_pub/potaytoe.json @@ -1,35 +1,35 @@ { "name": "Po-Tay-Toe", "clan_id": null, - "skin": "male/indy", + "skin": "male/optimus", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.4, - "m4": 0.6, - "m3": 0.7, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 1.0, + "m4": 0.9, + "m3": 0.8, "hc": 0.4, - "sniper": 0.7, - "knives": 0.1, - "grenades": 0.8 + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.6, + "vest": 0.5, + "helm": 0.8, + "laser": 0.7, "silencer": 0.5, "slippers": 0.5, - "bandolier": 0.3 + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.6, - "movementStyle": 0.1, + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.7, + "aimingSkill": 0.8, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 1.0, + "movementStyle": 0.3, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/pyroyetti.json b/bots/bots_pub/pyroyetti.json index 7ce527dd5..8e1062787 100644 --- a/bots/bots_pub/pyroyetti.json +++ b/bots/bots_pub/pyroyetti.json @@ -1,35 +1,35 @@ { "name": "Pyro Yetti", "clan_id": null, - "skin": "male/indy", + "skin": "male/aqgthug", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, + "mk23": 0.5, + "dual_mk23": 0.7, "mp5": 0.4, - "m4": 0.1, - "m3": 0.6, - "hc": 0.8, - "sniper": 0.6, + "m4": 0.5, + "m3": 0.3, + "hc": 0.3, + "sniper": 0.1, "knives": 0.4, - "grenades": 0.5 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.3 + "vest": 0.5, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.8, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.4, + "aggressiveness": 0.9, + "teamwork": 0.4, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.6, "communicationFreq": 0.5, "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "movementStyle": 0.4, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/quanchi.json b/bots/bots_pub/quanchi.json index 386c1d03f..5e25d3a63 100644 --- a/bots/bots_pub/quanchi.json +++ b/bots/bots_pub/quanchi.json @@ -1,35 +1,35 @@ { "name": "Quan Chi", "clan_id": null, - "skin": "male/indy", + "skin": "actionrally/ctf_b", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.6, + "mk23": 0.3, + "dual_mk23": 0.5, "mp5": 0.6, - "m4": 0.7, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.2, + "m4": 0.1, + "m3": 0.6, + "hc": 0.6, + "sniper": 0.7, + "knives": 0.5, "grenades": 0.7 }, "itemPreferences": { "vest": 0.5, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.9 + "helm": 0.4, + "laser": 0.2, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "aggressiveness": 0.7, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.8, + "reactionTime": 0.6, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/raiden.json b/bots/bots_pub/raiden.json index 27872eebf..56250b05c 100644 --- a/bots/bots_pub/raiden.json +++ b/bots/bots_pub/raiden.json @@ -1,35 +1,35 @@ { "name": "Raiden", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/cptahab", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.9, - "mp5": 0.2, - "m4": 0.7, - "m3": 0.8, + "mk23": 0.2, + "dual_mk23": 0.6, + "mp5": 1.0, + "m4": 0.4, + "m3": 0.6, "hc": 0.3, - "sniper": 0.4, - "knives": 0.4, + "sniper": 0.6, + "knives": 0.5, "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.6, - "bandolier": 0.6 + "vest": 0.6, + "helm": 0.3, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.4, + "bandolier": 0.0 }, "coreTraits": { - "aggressiveness": 0.6, + "aggressiveness": 0.4, "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "curiosity": 0.2, + "aimingSkill": 0.1, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.3, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/razor.json b/bots/bots_pub/razor.json index 6fbe8cf55..68c45fc01 100644 --- a/bots/bots_pub/razor.json +++ b/bots/bots_pub/razor.json @@ -1,35 +1,35 @@ { "name": "Razor", "clan_id": null, - "skin": "male/indy", + "skin": "actionrally/trinity", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.8, - "hc": 0.9, - "sniper": 0.4, - "knives": 0.6, - "grenades": 0.3 + "mk23": 0.0, + "dual_mk23": 0.4, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.5, + "hc": 1.0, + "sniper": 0.5, + "knives": 0.7, + "grenades": 0.8 }, "itemPreferences": { "vest": 0.7, "helm": 0.4, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.5, - "bandolier": 0.7 + "laser": 0.8, + "silencer": 0.5, + "slippers": 0.7, + "bandolier": 0.3 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.8, + "teamwork": 0.6, + "curiosity": 0.2, "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, + "reactionTime": 0.2, + "communicationFreq": 0.4, "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.1 + "movementStyle": 0.5, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/reddog.json b/bots/bots_pub/reddog.json index f1c63b3c6..6f6c5d895 100644 --- a/bots/bots_pub/reddog.json +++ b/bots/bots_pub/reddog.json @@ -1,35 +1,35 @@ { "name": "Reddog", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/blade2", "weaponPreferences": { "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.7, - "hc": 0.7, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.4 + "dual_mk23": 0.7, + "mp5": 0.2, + "m4": 0.8, + "m3": 0.5, + "hc": 0.1, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.4, + "vest": 0.8, "helm": 0.8, - "laser": 0.5, + "laser": 1.1, "silencer": 0.5, - "slippers": 0.4, - "bandolier": 0.7 + "slippers": 0.5, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.2 + "aggressiveness": 0.6, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.3, + "communicationTone": 0.5, + "movementStyle": 0.2, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/reiko.json b/bots/bots_pub/reiko.json index bf5a122fa..29926f624 100644 --- a/bots/bots_pub/reiko.json +++ b/bots/bots_pub/reiko.json @@ -1,35 +1,35 @@ { "name": "Reiko", "clan_id": null, - "skin": "male/indy", + "skin": "male/rredneck", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.7, "dual_mk23": 0.4, - "mp5": 0.6, - "m4": 0.4, + "mp5": 0.4, + "m4": 0.3, "m3": 0.3, - "hc": 0.3, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 + "hc": 0.0, + "sniper": 0.2, + "knives": 0.4, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.2, - "laser": 0.7, + "vest": 1.1, + "helm": 0.6, + "laser": 0.4, "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.4 + "slippers": 0.3, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.8, - "curiosity": 0.9, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.5, + "aggressiveness": 0.6, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.9, + "communicationFreq": 0.6, "communicationTone": 0.6, - "movementStyle": 0.3, + "movementStyle": 0.5, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/replicant.json b/bots/bots_pub/replicant.json index 365b8b838..afae57cd2 100644 --- a/bots/bots_pub/replicant.json +++ b/bots/bots_pub/replicant.json @@ -1,35 +1,35 @@ { "name": "Replicant", "clan_id": null, - "skin": "male/indy", + "skin": "terror/ctf_b", "weaponPreferences": { - "mk23": 0.3, + "mk23": 0.8, "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.7, - "m3": 0.6, + "mp5": 0.3, + "m4": 0.4, + "m3": 0.5, "hc": 0.7, - "sniper": 0.9, - "knives": 0.3, + "sniper": 0.7, + "knives": 0.4, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.4, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.6 + "vest": 1.0, + "helm": 0.5, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.3, + "aggressiveness": 0.4, + "teamwork": 0.4, "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.3 + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.9, + "communicationTone": 1.1, + "movementStyle": 0.1, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/reptile.json b/bots/bots_pub/reptile.json index 01c547eca..e4cb7e916 100644 --- a/bots/bots_pub/reptile.json +++ b/bots/bots_pub/reptile.json @@ -1,35 +1,35 @@ { "name": "Reptile", "clan_id": null, - "skin": "male/indy", + "skin": "male/austin2", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.6, - "hc": 0.3, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 + "mk23": 0.5, + "dual_mk23": 0.6, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.2, + "hc": 0.0, + "sniper": 0.4, + "knives": 0.4, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.2, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.4, - "bandolier": 0.7 + "vest": 0.8, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.9, + "slippers": 0.6, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, + "aggressiveness": 0.1, "teamwork": 0.4, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.4, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.9, + "communicationFreq": 0.7, "communicationTone": 0.5, - "movementStyle": 0.3, + "movementStyle": 0.4, "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/riftwraith.json b/bots/bots_pub/riftwraith.json index 162455a7b..a6fbb5caf 100644 --- a/bots/bots_pub/riftwraith.json +++ b/bots/bots_pub/riftwraith.json @@ -1,35 +1,35 @@ { "name": "Riftwraith", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/ctf_r", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.3, - "mp5": 0.7, + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.3, "m4": 0.4, "m3": 0.6, - "hc": 0.9, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.4 + "hc": 0.6, + "sniper": 0.8, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.7, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.1 + "vest": 0.3, + "helm": 0.6, + "laser": 0.3, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, + "aggressiveness": 0.8, + "teamwork": 0.6, "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.5, + "aimingSkill": 0.3, + "reactionTime": 0.3, "communicationFreq": 0.8, "communicationTone": 0.7, - "movementStyle": 0.8, - "objectiveFocus": 0.3 + "movementStyle": 0.2, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/ruslan.json b/bots/bots_pub/ruslan.json deleted file mode 100644 index ac1b9f157..000000000 --- a/bots/bots_pub/ruslan.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ruslan", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.2, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.9, - "laser": 0.3, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.2, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ryder.json b/bots/bots_pub/ryder.json deleted file mode 100644 index 4cb1570f3..000000000 --- a/bots/bots_pub/ryder.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ryder", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.3, - "mp5": 0.8, - "m4": 0.5, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.0, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.7, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.9, - "reactionTime": 0.8, - "communicationFreq": 0.1, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/ryver.json b/bots/bots_pub/ryver.json deleted file mode 100644 index f271848f6..000000000 --- a/bots/bots_pub/ryver.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Ryver", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.5, - "m4": 0.4, - "m3": 0.2, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.5, - "movementStyle": 0.1, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/saboy.json b/bots/bots_pub/saboy.json deleted file mode 100644 index b046414e9..000000000 --- a/bots/bots_pub/saboy.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Saboy", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.6, - "m4": 0.5, - "m3": -0.1, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.3, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.1, - "helm": 0.7, - "laser": 0.4, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.8, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.7, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/sagel.json b/bots/bots_pub/sagel.json index 3aa0340ab..b07a5284b 100644 --- a/bots/bots_pub/sagel.json +++ b/bots/bots_pub/sagel.json @@ -1,35 +1,35 @@ { "name": "Sagel", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/agent", "weaponPreferences": { "mk23": 0.8, "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.4, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.2, + "mp5": 0.5, + "m4": 0.6, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.7, + "knives": 0.6, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.7, - "laser": 0.6, + "vest": 0.4, + "helm": 0.8, + "laser": 0.1, "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.5 + "slippers": 0.4, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.1, + "aggressiveness": 0.7, + "teamwork": -0.1, "curiosity": 0.3, - "aimingSkill": 0.4, + "aimingSkill": 0.6, "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.5, - "movementStyle": 0.6, + "communicationFreq": 0.7, + "communicationTone": 0.4, + "movementStyle": 0.4, "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/samdecker.json b/bots/bots_pub/samdecker.json index 9d347583a..8c0b5dedc 100644 --- a/bots/bots_pub/samdecker.json +++ b/bots/bots_pub/samdecker.json @@ -1,35 +1,35 @@ { "name": "Sam Decker", "clan_id": null, - "skin": "male/indy", + "skin": "male/nut", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.6, - "mp5": 0.9, - "m4": 0.7, - "m3": 0.5, + "mk23": 0.5, + "dual_mk23": 0.4, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.1, "hc": 0.6, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.6 + "sniper": 0.7, + "knives": 0.7, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.9, - "silencer": 0.7, - "slippers": 1.0, - "bandolier": 0.7 + "vest": 0.8, + "helm": 0.5, + "laser": 0.6, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.9, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "aggressiveness": 0.4, + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.8, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.3, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/scorpion.json b/bots/bots_pub/scorpion.json deleted file mode 100644 index 8e39a1404..000000000 --- a/bots/bots_pub/scorpion.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Scorpion", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.1, - "laser": 0.5, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.9, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.1, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/seankane.json b/bots/bots_pub/seankane.json deleted file mode 100644 index 2bd83aa30..000000000 --- a/bots/bots_pub/seankane.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Sean Kane", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.3, - "hc": 0.2, - "sniper": 0.8, - "knives": 1.0, - "grenades": 0.6 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.7, - "communicationTone": 0.8, - "movementStyle": 0.9, - "objectiveFocus": 0.9 - } -} \ No newline at end of file diff --git a/bots/bots_pub/shinnok.json b/bots/bots_pub/shinnok.json deleted file mode 100644 index c16c19da4..000000000 --- a/bots/bots_pub/shinnok.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Shinnok", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.5, - "hc": 0.9, - "sniper": 0.5, - "knives": 1.0, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.8, - "communicationTone": 0.4, - "movementStyle": 0.1, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/siege.json b/bots/bots_pub/siege.json deleted file mode 100644 index 08b280917..000000000 --- a/bots/bots_pub/siege.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Siege", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.3, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.9, - "helm": 0.3, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 1.0, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/sindel.json b/bots/bots_pub/sindel.json index 072a0fbdd..0ae2fe8a3 100644 --- a/bots/bots_pub/sindel.json +++ b/bots/bots_pub/sindel.json @@ -1,35 +1,35 @@ { "name": "Sindel", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sasjungle", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.6, + "mk23": 0.2, + "dual_mk23": 0.7, "mp5": 0.3, - "m4": 0.5, + "m4": 0.3, "m3": 0.6, - "hc": 0.3, + "hc": 0.6, "sniper": 0.6, - "knives": 0.5, - "grenades": 0.7 + "knives": 0.3, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.8, + "vest": 0.3, + "helm": 0.5, "laser": 0.5, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.3 + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.4, + "aggressiveness": 0.7, + "teamwork": 0.7, + "curiosity": 0.5, "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, "communicationTone": 0.3, "movementStyle": 0.6, - "objectiveFocus": 0.1 + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/skarlet.json b/bots/bots_pub/skarlet.json index de355b031..0ca26d51a 100644 --- a/bots/bots_pub/skarlet.json +++ b/bots/bots_pub/skarlet.json @@ -1,35 +1,35 @@ { "name": "Skarlet", "clan_id": null, - "skin": "male/indy", + "skin": "male/rredneck", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.4, - "knives": 0.8, - "grenades": 0.3 + "mk23": 0.1, + "dual_mk23": 0.0, + "mp5": 0.5, + "m4": 0.5, + "m3": 0.2, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.1, - "helm": 0.9, - "laser": 0.4, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.6, + "helm": 0.2, + "laser": 0.2, + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.8, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.7, - "reactionTime": 0.3, - "communicationFreq": 0.5, + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.6, "communicationTone": 0.5, - "movementStyle": 0.3, - "objectiveFocus": 0.5 + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/slash.json b/bots/bots_pub/slash.json index dd8ef9181..fdc0b423c 100644 --- a/bots/bots_pub/slash.json +++ b/bots/bots_pub/slash.json @@ -1,35 +1,35 @@ { "name": "Slash", "clan_id": null, - "skin": "male/indy", + "skin": "male/kw_yellow", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.3, + "mk23": 1.0, + "dual_mk23": 0.8, + "mp5": 0.3, + "m4": 0.6, "m3": 0.5, - "hc": 0.2, - "sniper": 0.5, - "knives": 0.3, + "hc": 0.4, + "sniper": 0.8, + "knives": 0.4, "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.0, - "slippers": 0.6, - "bandolier": 0.7 + "vest": 0.6, + "helm": 0.7, + "laser": 0.6, + "silencer": 0.4, + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.4, + "aggressiveness": 0.5, "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.6, - "reactionTime": 0.2, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.4, + "communicationFreq": -0.0, + "communicationTone": 0.5, + "movementStyle": 0.7, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/spar.json b/bots/bots_pub/spar.json deleted file mode 100644 index cbe526374..000000000 --- a/bots/bots_pub/spar.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Spar", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.5, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.6, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 1.0, - "helm": 0.5, - "laser": 0.4, - "silencer": 1.0, - "slippers": 0.3, - "bandolier": 1.0 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.1, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.8, - "communicationTone": 0.2, - "movementStyle": 0.5, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/specialed.json b/bots/bots_pub/specialed.json deleted file mode 100644 index 18a32c796..000000000 --- a/bots/bots_pub/specialed.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "SpecialEd", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.9, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/spocket.json b/bots/bots_pub/spocket.json deleted file mode 100644 index 4e872e171..000000000 --- a/bots/bots_pub/spocket.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Spocket", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.9, - "mp5": 0.9, - "m4": 0.2, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.4, - "grenades": 0.0 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.5, - "silencer": 1.1, - "slippers": 0.6, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.2, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/steampug.json b/bots/bots_pub/steampug.json deleted file mode 100644 index 16a59f0e6..000000000 --- a/bots/bots_pub/steampug.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "SteamPug", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.6, - "communicationTone": 0.5, - "movementStyle": 0.1, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/stinger.json b/bots/bots_pub/stinger.json index b22b78237..354b3befb 100644 --- a/bots/bots_pub/stinger.json +++ b/bots/bots_pub/stinger.json @@ -1,35 +1,35 @@ { "name": "Stinger", "clan_id": null, - "skin": "male/indy", + "skin": "aqmarine/urban", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.5, + "dual_mk23": 0.3, "mp5": 0.6, - "m4": 0.5, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.4 + "m4": 0.4, + "m3": 0.1, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.9, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.7 + "vest": 0.5, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.4, - "teamwork": 0.6, + "teamwork": 0.2, "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 1.0, - "communicationFreq": 0.3, - "communicationTone": 0.1, - "movementStyle": 0.5, - "objectiveFocus": 0.4 + "aimingSkill": 0.8, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/stormtree.json b/bots/bots_pub/stormtree.json index 3b6c2dde9..f14555fc2 100644 --- a/bots/bots_pub/stormtree.json +++ b/bots/bots_pub/stormtree.json @@ -1,35 +1,35 @@ { "name": "Stormtree", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/blade4", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.1, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.4, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.3, - "grenades": 0.4 + "dual_mk23": 0.4, + "mp5": -0.1, + "m4": 0.5, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.6, + "knives": 0.6, + "grenades": 0.1 }, "itemPreferences": { - "vest": 0.4, - "helm": 1.2, + "vest": 0.6, + "helm": 0.5, "laser": 0.6, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.4 }, "coreTraits": { "aggressiveness": 0.8, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.8, - "objectiveFocus": 0.5 + "teamwork": 0.6, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.0, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/stripe.json b/bots/bots_pub/stripe.json index 73d5f654a..1e0d2bd13 100644 --- a/bots/bots_pub/stripe.json +++ b/bots/bots_pub/stripe.json @@ -1,35 +1,35 @@ { "name": "Stripe", "clan_id": null, - "skin": "male/indy", + "skin": "sas/nithb", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.3, - "m3": 0.6, - "hc": 0.9, - "sniper": 0.7, - "knives": 0.8, - "grenades": 0.2 + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.7, + "m3": 0.7, + "hc": 0.3, + "sniper": 0.4, + "knives": 0.5, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.1 + "vest": 0.3, + "helm": 0.7, + "laser": 0.3, + "silencer": 0.2, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.8, - "reactionTime": 0.3, - "communicationFreq": 0.2, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "aggressiveness": 0.1, + "teamwork": 0.5, + "curiosity": 0.2, + "aimingSkill": 0.6, + "reactionTime": 0.6, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/strogg.json b/bots/bots_pub/strogg.json index 59c855a5f..720af4fca 100644 --- a/bots/bots_pub/strogg.json +++ b/bots/bots_pub/strogg.json @@ -1,35 +1,35 @@ { "name": "Strogg", "clan_id": null, - "skin": "male/indy", + "skin": "female/kw_black", "weaponPreferences": { - "mk23": 0.2, - "dual_mk23": 0.5, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.5, - "hc": 0.6, - "sniper": 0.7, + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.8, + "m4": 0.5, + "m3": 0.3, + "hc": 0.4, + "sniper": 0.5, "knives": 0.5, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, + "vest": 0.5, "helm": 0.6, - "laser": 0.3, - "silencer": -0.0, - "slippers": 0.3, - "bandolier": 0.5 + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.4, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.6, + "aggressiveness": 0.3, + "teamwork": 0.2, + "curiosity": 0.4, + "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.7 + "communicationFreq": 0.3, + "communicationTone": 1.0, + "movementStyle": 0.7, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/strom.json b/bots/bots_pub/strom.json deleted file mode 100644 index 94170a264..000000000 --- a/bots/bots_pub/strom.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Strom", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.4, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.5, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.8, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.8, - "reactionTime": 0.6, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/stryker.json b/bots/bots_pub/stryker.json deleted file mode 100644 index 46a60aaa6..000000000 --- a/bots/bots_pub/stryker.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Stryker", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.3, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.1, - "hc": 0.2, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.2, - "laser": 0.6, - "silencer": 0.6, - "slippers": 0.6, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.8, - "communicationFreq": 0.7, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/stud.json b/bots/bots_pub/stud.json deleted file mode 100644 index 88d01604a..000000000 --- a/bots/bots_pub/stud.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Stud", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.3, - "m3": 0.7, - "hc": 0.6, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.5, - "bandolier": 0.4 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.3, - "communicationFreq": 0.3, - "communicationTone": 0.4, - "movementStyle": 0.9, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/t1000.json b/bots/bots_pub/t1000.json index 95fa8777c..af1e2c775 100644 --- a/bots/bots_pub/t1000.json +++ b/bots/bots_pub/t1000.json @@ -1,34 +1,34 @@ { "name": "T-1000", "clan_id": null, - "skin": "male/indy", + "skin": "male/police", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.5, + "mk23": 0.4, + "dual_mk23": 0.6, "mp5": 0.7, - "m4": 0.6, - "m3": 0.8, - "hc": 0.4, + "m4": 0.5, + "m3": 0.5, + "hc": 0.1, "sniper": 0.5, - "knives": 0.5, - "grenades": 0.7 + "knives": 0.7, + "grenades": 0.4 }, "itemPreferences": { "vest": 0.6, "helm": 0.5, "laser": 0.5, - "silencer": 0.7, - "slippers": 0.7, - "bandolier": 1.0 + "silencer": 0.6, + "slippers": 0.3, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.6, - "curiosity": 0.2, - "aimingSkill": 0.3, - "reactionTime": 0.7, - "communicationFreq": 0.5, - "communicationTone": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.9, + "curiosity": 0.6, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.9, + "communicationTone": 0.2, "movementStyle": 0.4, "objectiveFocus": 0.3 } diff --git a/bots/bots_pub/t_bon.json b/bots/bots_pub/t_bon.json index 418e0132c..54296db86 100644 --- a/bots/bots_pub/t_bon.json +++ b/bots/bots_pub/t_bon.json @@ -1,35 +1,35 @@ { "name": "T_bon", "clan_id": null, - "skin": "male/indy", + "skin": "terror/csw black", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.8, - "mp5": 0.3, - "m4": 0.2, - "m3": 0.9, - "hc": 0.7, - "sniper": 0.7, - "knives": 0.4, - "grenades": 0.5 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.5, + "m4": 0.3, + "m3": 1.0, + "hc": 0.6, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.7, + "vest": 0.7, + "helm": 0.0, + "laser": 0.5, "silencer": 0.4, - "slippers": 0.7, + "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.9, + "aggressiveness": 0.7, "teamwork": 0.5, - "curiosity": 0.8, - "aimingSkill": 0.6, - "reactionTime": 0.4, - "communicationFreq": -0.0, - "communicationTone": 0.2, - "movementStyle": 0.6, - "objectiveFocus": 0.4 + "curiosity": 0.3, + "aimingSkill": 0.4, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/tahlkora.json b/bots/bots_pub/tahlkora.json index 775f243ef..13d72b91c 100644 --- a/bots/bots_pub/tahlkora.json +++ b/bots/bots_pub/tahlkora.json @@ -1,35 +1,35 @@ { "name": "Tahlkora", "clan_id": null, - "skin": "male/indy", + "skin": "male/kw_blue", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.8, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.4, + "m4": 0.5, "m3": 0.4, - "hc": 0.6, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.6 + "hc": 0.7, + "sniper": 0.4, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.5, - "laser": 0.8, - "silencer": 0.3, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.4, + "helm": 0.4, + "laser": 0.2, + "silencer": 0.9, + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": -0.0, - "teamwork": 0.5, - "curiosity": 0.0, - "aimingSkill": 0.8, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.5, - "objectiveFocus": 0.7 + "aggressiveness": 0.5, + "teamwork": 0.6, + "curiosity": 0.4, + "aimingSkill": 0.3, + "reactionTime": 0.5, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.2, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/tankjr.json b/bots/bots_pub/tankjr.json index 8e687870d..bb03629f2 100644 --- a/bots/bots_pub/tankjr.json +++ b/bots/bots_pub/tankjr.json @@ -1,35 +1,35 @@ { "name": "Tank Jr", "clan_id": null, - "skin": "male/indy", + "skin": "male/babarracuda", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.2, "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.6, - "knives": 0.6, - "grenades": 0.5 + "mp5": 0.4, + "m4": 0.6, + "m3": 0.9, + "hc": 0.2, + "sniper": 0.3, + "knives": 0.9, + "grenades": 0.2 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.9, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.6 + "vest": -0.1, + "helm": 0.6, + "laser": 0.4, + "silencer": 0.3, + "slippers": 0.4, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.4, + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.6, + "communicationFreq": 0.5, "communicationTone": 0.4, - "movementStyle": 0.6, + "movementStyle": 0.4, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/tanya.json b/bots/bots_pub/tanya.json index dbde36477..2e680fc31 100644 --- a/bots/bots_pub/tanya.json +++ b/bots/bots_pub/tanya.json @@ -1,35 +1,35 @@ { "name": "Tanya", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/shinobi", "weaponPreferences": { "mk23": 0.4, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.2, - "m3": 0.8, - "hc": 0.3, - "sniper": 0.8, - "knives": 0.3, - "grenades": 0.7 + "dual_mk23": 1.0, + "mp5": 0.3, + "m4": 0.5, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.4, + "vest": 0.8, + "helm": 0.1, "laser": 0.5, - "silencer": 0.8, - "slippers": 0.6, - "bandolier": 0.6 + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.8, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "aggressiveness": 0.2, + "teamwork": 0.4, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.4, + "communicationTone": 0.4, + "movementStyle": 0.6, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/tanz.json b/bots/bots_pub/tanz.json index 50069d415..7b0292b52 100644 --- a/bots/bots_pub/tanz.json +++ b/bots/bots_pub/tanz.json @@ -1,35 +1,35 @@ { "name": "Tanz", "clan_id": null, - "skin": "male/indy", + "skin": "male/butchy", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.4, - "mp5": 0.1, - "m4": 0.6, - "m3": 0.6, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.7, - "grenades": 0.7 + "mk23": 0.5, + "dual_mk23": 0.2, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.2, + "hc": 0.2, + "sniper": 0.5, + "knives": 0.6, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.2, - "laser": 0.3, - "silencer": 0.4, + "vest": 0.7, + "helm": 0.4, + "laser": 0.6, + "silencer": 0.8, "slippers": 0.5, "bandolier": 0.7 }, "coreTraits": { "aggressiveness": 0.6, - "teamwork": 0.3, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.4, + "teamwork": 0.5, + "curiosity": 0.5, + "aimingSkill": 0.5, + "reactionTime": 0.7, + "communicationFreq": 0.9, + "communicationTone": 0.5, "movementStyle": 0.5, - "objectiveFocus": 0.6 + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/tao.json b/bots/bots_pub/tao.json index ef073f0e2..c6440cea2 100644 --- a/bots/bots_pub/tao.json +++ b/bots/bots_pub/tao.json @@ -1,35 +1,35 @@ { "name": "Tao", "clan_id": null, - "skin": "male/indy", + "skin": "male/mkscorp", "weaponPreferences": { - "mk23": 0.6, + "mk23": 0.7, "dual_mk23": 0.4, - "mp5": 0.4, - "m4": 0.7, - "m3": 0.4, - "hc": 0.4, - "sniper": 0.7, - "knives": 0.6, - "grenades": 0.6 + "mp5": 0.6, + "m4": 0.5, + "m3": 0.6, + "hc": 0.8, + "sniper": 0.3, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.2, - "slippers": 0.6, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.3, + "laser": 0.2, + "silencer": 0.8, + "slippers": 0.1, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.6, - "curiosity": 0.6, - "aimingSkill": 0.3, - "reactionTime": 0.5, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.6, - "objectiveFocus": 0.5 + "aggressiveness": 0.6, + "teamwork": -0.0, + "curiosity": 0.7, + "aimingSkill": 0.5, + "reactionTime": 0.9, + "communicationFreq": 0.3, + "communicationTone": 0.7, + "movementStyle": 0.7, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/tatu.json b/bots/bots_pub/tatu.json index 3c7ae5c5c..9903dfc6a 100644 --- a/bots/bots_pub/tatu.json +++ b/bots/bots_pub/tatu.json @@ -1,35 +1,35 @@ { "name": "tatu", "clan_id": null, - "skin": "male/indy", + "skin": "terror/kgb", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, + "mk23": 0.5, + "dual_mk23": 0.3, "mp5": 0.4, - "m4": 0.7, - "m3": 0.3, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.1, - "grenades": 0.9 + "m4": 0.4, + "m3": 0.6, + "hc": 0.5, + "sniper": 0.4, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.3, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.4, - "bandolier": 0.5 + "vest": 0.6, + "helm": 0.5, + "laser": 0.5, + "silencer": 0.6, + "slippers": 0.1, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 1.0, - "teamwork": 0.3, - "curiosity": 0.5, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.6, - "objectiveFocus": 0.9 + "aggressiveness": 0.4, + "teamwork": 0.6, + "curiosity": 0.8, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.4, + "communicationTone": 0.3, + "movementStyle": 0.4, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/technician.json b/bots/bots_pub/technician.json index faf044687..561d1ce5c 100644 --- a/bots/bots_pub/technician.json +++ b/bots/bots_pub/technician.json @@ -1,35 +1,35 @@ { "name": "Technician", "clan_id": null, - "skin": "male/indy", + "skin": "male/shaft", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.4, + "mk23": 0.6, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.7, "m3": 0.6, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.3, - "grenades": 0.8 + "hc": 0.5, + "sniper": 0.1, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.3, - "laser": 0.2, + "vest": 0.5, + "helm": 0.6, + "laser": 0.3, "silencer": 0.6, - "slippers": 0.4, + "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, + "aggressiveness": 0.2, + "teamwork": 0.5, "curiosity": 0.5, - "aimingSkill": 0.4, + "aimingSkill": 0.5, "reactionTime": 0.5, - "communicationFreq": 0.2, - "communicationTone": 0.3, + "communicationFreq": 0.6, + "communicationTone": 0.4, "movementStyle": 0.5, - "objectiveFocus": 0.6 + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/thedean.json b/bots/bots_pub/thedean.json index 9ba8818e9..4ccf60e9c 100644 --- a/bots/bots_pub/thedean.json +++ b/bots/bots_pub/thedean.json @@ -1,35 +1,35 @@ { "name": "The Dean", "clan_id": null, - "skin": "male/indy", + "skin": "male/beastdl", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.4, + "mk23": 0.4, + "dual_mk23": 0.7, "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.4, - "sniper": 1.0, + "m4": 0.4, + "m3": 0.2, + "hc": 0.8, + "sniper": 0.6, "knives": 0.6, - "grenades": 0.1 + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.7, - "helm": 0.6, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.7, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.4, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.1, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.6, - "communicationFreq": 0.6, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "aggressiveness": 0.7, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.5, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.1, + "objectiveFocus": 0.6 } } \ No newline at end of file diff --git a/bots/bots_pub/theeraser.json b/bots/bots_pub/theeraser.json index 6738ef716..6bbca5c44 100644 --- a/bots/bots_pub/theeraser.json +++ b/bots/bots_pub/theeraser.json @@ -1,35 +1,35 @@ { "name": "The Eraser", "clan_id": null, - "skin": "male/indy", + "skin": "male/aqgvillain", "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.8, + "mk23": 0.6, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.7, "hc": 0.4, - "sniper": 0.6, - "knives": 0.1, - "grenades": 0.1 + "sniper": 0.5, + "knives": 0.3, + "grenades": 0.4 }, "itemPreferences": { - "vest": 0.2, - "helm": 0.1, - "laser": 0.8, - "silencer": 0.5, + "vest": 0.5, + "helm": 0.6, + "laser": 0.7, + "silencer": 0.3, "slippers": 0.5, "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.7, - "curiosity": 0.5, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.3 + "aggressiveness": 0.2, + "teamwork": 0.3, + "curiosity": 0.8, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.8 } } \ No newline at end of file diff --git a/bots/bots_pub/thegerman.json b/bots/bots_pub/thegerman.json index 936304235..b0a64f4bd 100644 --- a/bots/bots_pub/thegerman.json +++ b/bots/bots_pub/thegerman.json @@ -1,35 +1,35 @@ { "name": "The German", "clan_id": null, - "skin": "male/indy", + "skin": "male/police", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.9, - "mp5": 0.5, - "m4": 0.2, - "m3": 0.6, - "hc": 0.5, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.2, + "m4": 0.3, + "m3": 0.5, + "hc": 1.1, "sniper": 0.3, - "knives": 0.4, - "grenades": 0.5 + "knives": 0.5, + "grenades": 0.0 }, "itemPreferences": { - "vest": 0.5, + "vest": 0.8, "helm": 0.4, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.6 + "laser": 0.7, + "silencer": -0.0, + "slippers": 0.7, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.1, - "teamwork": 0.8, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.5, + "curiosity": 0.7, + "aimingSkill": 0.7, + "reactionTime": 0.4, "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "communicationTone": 0.5, + "movementStyle": 0.6, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/theonly.json b/bots/bots_pub/theonly.json index 4e7b982ba..25161c905 100644 --- a/bots/bots_pub/theonly.json +++ b/bots/bots_pub/theonly.json @@ -1,35 +1,35 @@ { "name": "The Only", "clan_id": null, - "skin": "male/indy", + "skin": "male/leservoircat", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.6, - "m4": 0.9, - "m3": 0.5, - "hc": 0.1, + "mk23": 0.4, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.4, + "m3": 0.6, + "hc": 0.6, "sniper": 0.5, - "knives": 0.4, + "knives": 0.9, "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.6, - "laser": 0.2, - "silencer": 1.0, - "slippers": 0.4, - "bandolier": 0.5 + "vest": 0.5, + "helm": 0.5, + "laser": 0.7, + "silencer": 0.5, + "slippers": 0.8, + "bandolier": 0.2 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.5, - "curiosity": 0.6, - "aimingSkill": 0.6, - "reactionTime": 0.7, - "communicationFreq": 0.3, - "communicationTone": 0.3, - "movementStyle": 0.7, - "objectiveFocus": 0.4 + "teamwork": 0.0, + "curiosity": 0.8, + "aimingSkill": 0.5, + "reactionTime": 0.8, + "communicationFreq": 0.5, + "communicationTone": 0.4, + "movementStyle": 0.8, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/theway.json b/bots/bots_pub/theway.json index 38a2def96..91ea211b3 100644 --- a/bots/bots_pub/theway.json +++ b/bots/bots_pub/theway.json @@ -1,35 +1,35 @@ { "name": "TheWay", "clan_id": null, - "skin": "male/indy", + "skin": "male/image", "weaponPreferences": { - "mk23": 1.0, - "dual_mk23": 0.4, + "mk23": 0.5, + "dual_mk23": 0.3, "mp5": 0.7, - "m4": 0.2, + "m4": 0.4, "m3": 0.4, "hc": 0.6, - "sniper": 0.1, - "knives": 0.6, - "grenades": 0.1 + "sniper": 0.0, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.1, - "helm": 1.0, - "laser": 0.7, - "silencer": 0.1, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.6, + "laser": 0.5, + "silencer": 0.5, + "slippers": 0.6, + "bandolier": 0.5 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.2, - "curiosity": 0.5, - "aimingSkill": 0.3, - "reactionTime": 0.3, - "communicationFreq": 0.0, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.7 + "teamwork": 0.3, + "curiosity": 0.3, + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.8, + "communicationTone": 0.6, + "movementStyle": 0.6, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/thor.json b/bots/bots_pub/thor.json deleted file mode 100644 index f55d9b233..000000000 --- a/bots/bots_pub/thor.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Thor", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.4, - "m4": 0.8, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.3, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.7, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.7 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.7, - "curiosity": 0.3, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.9, - "communicationTone": 0.7, - "movementStyle": 0.1, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/tiano.json b/bots/bots_pub/tiano.json deleted file mode 100644 index f2abdca92..000000000 --- a/bots/bots_pub/tiano.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Tiano", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.5, - "m4": 0.7, - "m3": 0.3, - "hc": 0.3, - "sniper": 0.1, - "knives": 0.7, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.7, - "helm": 0.7, - "laser": 0.5, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.4, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.5, - "movementStyle": -0.1, - "objectiveFocus": 0.8 - } -} \ No newline at end of file diff --git a/bots/bots_pub/tommix.json b/bots/bots_pub/tommix.json index b8a3e177f..663ed2ea2 100644 --- a/bots/bots_pub/tommix.json +++ b/bots/bots_pub/tommix.json @@ -1,34 +1,34 @@ { "name": "Tom Mix", "clan_id": null, - "skin": "male/indy", + "skin": "terror/csw crazy", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.8, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.8, - "hc": 0.5, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.5 + "mk23": 0.5, + "dual_mk23": -0.0, + "mp5": 0.1, + "m4": 0.4, + "m3": 0.4, + "hc": 0.9, + "sniper": 0.6, + "knives": 0.4, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.9, - "laser": 0.0, - "silencer": 0.6, - "slippers": 0.5, - "bandolier": 0.4 + "vest": 0.5, + "helm": 0.7, + "laser": 0.4, + "silencer": 0.1, + "slippers": 0.3, + "bandolier": 0.5 }, "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.5, - "curiosity": 0.3, - "aimingSkill": 0.5, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.4, + "aggressiveness": 0.8, + "teamwork": 0.2, + "curiosity": 0.5, + "aimingSkill": 0.7, + "reactionTime": 0.4, + "communicationFreq": 0.6, + "communicationTone": 0.3, "movementStyle": 0.6, "objectiveFocus": 0.6 } diff --git a/bots/bots_pub/toorc.json b/bots/bots_pub/toorc.json deleted file mode 100644 index 4254cd6ea..000000000 --- a/bots/bots_pub/toorc.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Toorc", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.4, - "m4": 0.5, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.4, - "helm": 0.3, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.2, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.1, - "curiosity": 0.7, - "aimingSkill": 0.4, - "reactionTime": 0.4, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.4, - "objectiveFocus": 0.7 - } -} \ No newline at end of file diff --git a/bots/bots_pub/treehead.json b/bots/bots_pub/treehead.json index 1e4de32d9..7fd52eb94 100644 --- a/bots/bots_pub/treehead.json +++ b/bots/bots_pub/treehead.json @@ -1,35 +1,35 @@ { "name": "Treehead", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/castor", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.4, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.3, - "hc": -0.1, - "sniper": 0.4, - "knives": 0.8, - "grenades": 0.2 + "mk23": 0.6, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.1, + "m3": 0.2, + "hc": 0.5, + "sniper": 0.5, + "knives": 0.4, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.6, - "helm": 0.1, - "laser": 0.5, + "vest": 0.8, + "helm": 0.7, + "laser": 0.7, "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.8 + "slippers": 0.3, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.6, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.2, - "communicationFreq": 0.7, + "aggressiveness": 0.4, + "teamwork": 0.7, + "curiosity": 0.6, + "aimingSkill": 0.5, + "reactionTime": 0.4, + "communicationFreq": 0.6, "communicationTone": 0.4, - "movementStyle": 0.4, + "movementStyle": 0.2, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/triborg.json b/bots/bots_pub/triborg.json deleted file mode 100644 index a91ff0a6b..000000000 --- a/bots/bots_pub/triborg.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Triborg", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.4, - "m3": 0.4, - "hc": 0.7, - "sniper": 0.2, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.2, - "helm": 0.7, - "laser": 0.7, - "silencer": 0.4, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.1, - "curiosity": 0.6, - "aimingSkill": 0.7, - "reactionTime": 0.7, - "communicationFreq": 0.2, - "communicationTone": 0.5, - "movementStyle": 0.6, - "objectiveFocus": 0.2 - } -} \ No newline at end of file diff --git a/bots/bots_pub/vanoss.json b/bots/bots_pub/vanoss.json index c7cc80472..ec4ea711e 100644 --- a/bots/bots_pub/vanoss.json +++ b/bots/bots_pub/vanoss.json @@ -1,35 +1,35 @@ { "name": "Vanoss", "clan_id": null, - "skin": "male/indy", + "skin": "male/hall", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.7, - "grenades": 0.6 + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.5, + "m4": 0.6, + "m3": 1.0, + "hc": 0.3, + "sniper": 0.5, + "knives": 0.5, + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.3, - "laser": 0.3, - "silencer": 0.3, - "slippers": 0.3, - "bandolier": 0.3 + "vest": 0.7, + "helm": 0.7, + "laser": 0.8, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.4, - "curiosity": 0.0, - "aimingSkill": 0.1, - "reactionTime": 0.7, - "communicationFreq": 0.1, - "communicationTone": 0.8, + "aggressiveness": 0.6, + "teamwork": 0.5, + "curiosity": 0.8, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.5, + "communicationTone": 0.4, "movementStyle": 0.3, - "objectiveFocus": 0.2 + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/venom.json b/bots/bots_pub/venom.json deleted file mode 100644 index d3e0ce67d..000000000 --- a/bots/bots_pub/venom.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "VenoM", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.7, - "mp5": 0.7, - "m4": 0.4, - "m3": 0.4, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.5, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.6, - "helm": 0.9, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.4, - "aimingSkill": 0.6, - "reactionTime": 0.5, - "communicationFreq": 0.1, - "communicationTone": 0.6, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/vey.json b/bots/bots_pub/vey.json index a57e85ec1..e48bd561f 100644 --- a/bots/bots_pub/vey.json +++ b/bots/bots_pub/vey.json @@ -1,35 +1,35 @@ { "name": "VeY", "clan_id": null, - "skin": "male/indy", + "skin": "male/aqgvillain", "weaponPreferences": { - "mk23": 0.7, - "dual_mk23": 0.8, + "mk23": 0.3, + "dual_mk23": 0.9, "mp5": 0.5, - "m4": 0.5, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.5, - "knives": 0.0, - "grenades": 0.6 + "m4": 0.6, + "m3": 0.5, + "hc": 0.3, + "sniper": 0.9, + "knives": 0.6, + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.8, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.3, + "vest": 0.6, + "helm": 0.2, + "laser": 0.3, + "silencer": 0.4, "slippers": 0.3, - "bandolier": 0.9 + "bandolier": 0.2 }, "coreTraits": { "aggressiveness": 0.8, "teamwork": 0.3, - "curiosity": 0.8, - "aimingSkill": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.8, "reactionTime": 0.5, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.7 + "communicationFreq": 0.2, + "communicationTone": 0.3, + "movementStyle": 0.2, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/vic.json b/bots/bots_pub/vic.json index dd98757a0..4dbcdeeba 100644 --- a/bots/bots_pub/vic.json +++ b/bots/bots_pub/vic.json @@ -1,35 +1,35 @@ { "name": "Vic", "clan_id": null, - "skin": "male/indy", + "skin": "female/kw_white", "weaponPreferences": { - "mk23": 0.1, - "dual_mk23": 0.4, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.7, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.7 + "mk23": 0.7, + "dual_mk23": 0.6, + "mp5": 0.7, + "m4": 0.4, + "m3": 0.4, + "hc": 0.7, + "sniper": 0.6, + "knives": 0.5, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.6, - "laser": 0.6, - "silencer": 0.1, - "slippers": 0.5, - "bandolier": 0.3 + "vest": 0.4, + "helm": 0.5, + "laser": 0.4, + "silencer": 0.5, + "slippers": 0.3, + "bandolier": 0.7 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.4, - "curiosity": 0.4, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 1.0, - "objectiveFocus": 0.5 + "aggressiveness": 0.5, + "teamwork": 0.9, + "curiosity": 0.2, + "aimingSkill": 0.7, + "reactionTime": 0.2, + "communicationFreq": 0.5, + "communicationTone": 0.2, + "movementStyle": 0.4, + "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/visor.json b/bots/bots_pub/visor.json index 88b196138..b8fd2a3e8 100644 --- a/bots/bots_pub/visor.json +++ b/bots/bots_pub/visor.json @@ -1,35 +1,35 @@ { "name": "Visor", "clan_id": null, - "skin": "male/indy", + "skin": "male/brucelee", "weaponPreferences": { - "mk23": 0.5, - "dual_mk23": 0.2, - "mp5": 0.3, - "m4": 0.3, - "m3": 0.3, - "hc": 0.1, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.8, + "m4": 0.6, + "m3": 0.6, + "hc": 1.0, "sniper": 0.5, - "knives": 0.7, - "grenades": 0.6 + "knives": 0.3, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.3, - "helm": 0.4, + "vest": 0.5, + "helm": 0.2, "laser": 0.8, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.7 + "silencer": 0.5, + "slippers": 0.4, + "bandolier": 0.8 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.8, - "curiosity": 0.4, - "aimingSkill": 0.4, - "reactionTime": 0.6, - "communicationFreq": 0.7, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.1 + "aggressiveness": 0.7, + "teamwork": 0.6, + "curiosity": 0.6, + "aimingSkill": 0.2, + "reactionTime": 0.4, + "communicationFreq": 0.4, + "communicationTone": 0.5, + "movementStyle": 0.8, + "objectiveFocus": 0.2 } } \ No newline at end of file diff --git a/bots/bots_pub/vizier.json b/bots/bots_pub/vizier.json deleted file mode 100644 index b826ae5d1..000000000 --- a/bots/bots_pub/vizier.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Vizier", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.8, - "mp5": 0.5, - "m4": 0.5, - "m3": 0.3, - "hc": 0.5, - "sniper": 0.3, - "knives": 0.5, - "grenades": 0.7 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.6, - "silencer": 0.5, - "slippers": 0.5, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 1.0, - "teamwork": -0.0, - "curiosity": 0.7, - "aimingSkill": 0.2, - "reactionTime": 0.6, - "communicationFreq": 0.5, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.6 - } -} \ No newline at end of file diff --git a/bots/bots_pub/vokur.json b/bots/bots_pub/vokur.json index d0cc6942d..04d143778 100644 --- a/bots/bots_pub/vokur.json +++ b/bots/bots_pub/vokur.json @@ -1,35 +1,35 @@ { "name": "Vokur", "clan_id": null, - "skin": "male/indy", + "skin": "sas/mud", "weaponPreferences": { "mk23": 0.6, "dual_mk23": 0.5, - "mp5": 0.7, - "m4": 0.3, - "m3": 0.5, - "hc": 0.5, - "sniper": 0.4, - "knives": 0.2, - "grenades": 0.4 + "mp5": 0.4, + "m4": 0.5, + "m3": 0.4, + "hc": 0.2, + "sniper": 1.1, + "knives": 0.8, + "grenades": 0.5 }, "itemPreferences": { - "vest": 0.3, + "vest": -0.1, "helm": 0.4, - "laser": 0.6, + "laser": 0.9, "silencer": 0.5, - "slippers": 0.1, - "bandolier": 0.4 + "slippers": 0.6, + "bandolier": 0.6 }, "coreTraits": { - "aggressiveness": 0.9, - "teamwork": 0.3, - "curiosity": 0.4, - "aimingSkill": 0.6, + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.2, + "aimingSkill": 0.4, "reactionTime": 0.5, - "communicationFreq": 0.6, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.5 + "communicationFreq": 0.7, + "communicationTone": 0.8, + "movementStyle": 0.7, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/walter.json b/bots/bots_pub/walter.json deleted file mode 100644 index 75f43aaca..000000000 --- a/bots/bots_pub/walter.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Walter", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.6, - "m3": 0.7, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.5, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.4, - "laser": 0.3, - "silencer": 0.8, - "slippers": 0.5, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.5, - "teamwork": 0.4, - "curiosity": 0.6, - "aimingSkill": 0.5, - "reactionTime": 0.6, - "communicationFreq": 0.9, - "communicationTone": 0.4, - "movementStyle": 0.7, - "objectiveFocus": 0.3 - } -} \ No newline at end of file diff --git a/bots/bots_pub/warchild.json b/bots/bots_pub/warchild.json deleted file mode 100644 index 6aa2c76d1..000000000 --- a/bots/bots_pub/warchild.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Warchild", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.3, - "mp5": 0.3, - "m4": 0.8, - "m3": 0.9, - "hc": 0.6, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.5 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.7, - "laser": 0.8, - "silencer": 0.5, - "slippers": 0.6, - "bandolier": 0.6 - }, - "coreTraits": { - "aggressiveness": 0.4, - "teamwork": 0.4, - "curiosity": 0.0, - "aimingSkill": 0.6, - "reactionTime": 0.8, - "communicationFreq": 0.5, - "communicationTone": 0.6, - "movementStyle": 0.3, - "objectiveFocus": 0.4 - } -} \ No newline at end of file diff --git a/bots/bots_pub/wens.json b/bots/bots_pub/wens.json index 8d6a9238a..9e9676bd8 100644 --- a/bots/bots_pub/wens.json +++ b/bots/bots_pub/wens.json @@ -1,35 +1,35 @@ { "name": "Wens", "clan_id": null, - "skin": "male/indy", + "skin": "male/ctf_g", "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.5, - "m4": 0.8, - "m3": 0.8, - "hc": 0.4, - "sniper": 0.8, - "knives": 0.3, - "grenades": 0.6 + "mk23": 0.4, + "dual_mk23": 0.6, + "mp5": 0.6, + "m4": 0.7, + "m3": 0.7, + "hc": 0.6, + "sniper": 0.1, + "knives": 0.5, + "grenades": 0.8 }, "itemPreferences": { - "vest": 0.8, + "vest": 0.3, "helm": 0.5, - "laser": 0.6, - "silencer": 0.3, - "slippers": 0.6, - "bandolier": 0.5 + "laser": 0.8, + "silencer": 0.1, + "slippers": 0.8, + "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.6, - "curiosity": 0.8, - "aimingSkill": 0.5, - "reactionTime": 0.3, - "communicationFreq": 0.4, - "communicationTone": 0.3, - "movementStyle": 0.6, + "aggressiveness": 0.5, + "teamwork": 0.1, + "curiosity": 0.5, + "aimingSkill": 0.6, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.4, + "movementStyle": 0.4, "objectiveFocus": 0.4 } } \ No newline at end of file diff --git a/bots/bots_pub/whyme.json b/bots/bots_pub/whyme.json index c9304d98e..9c82f92a6 100644 --- a/bots/bots_pub/whyme.json +++ b/bots/bots_pub/whyme.json @@ -1,35 +1,35 @@ { "name": "Why Me?", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/ctf_b", "weaponPreferences": { - "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.1, - "m4": 0.5, - "m3": 0.5, - "hc": 0.7, - "sniper": 0.5, - "knives": 0.2, - "grenades": 0.4 + "mk23": 0.7, + "dual_mk23": 0.4, + "mp5": 0.8, + "m4": 0.4, + "m3": 0.6, + "hc": 0.2, + "sniper": 0.6, + "knives": 0.7, + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.4, - "silencer": 0.6, - "slippers": 0.5, + "vest": 0.6, + "helm": 0.2, + "laser": 0.5, + "silencer": 0.7, + "slippers": 0.3, "bandolier": 0.4 }, "coreTraits": { - "aggressiveness": 0.2, - "teamwork": 0.5, - "curiosity": 0.7, - "aimingSkill": 0.6, - "reactionTime": 0.8, + "aggressiveness": 0.6, + "teamwork": 0.8, + "curiosity": 0.6, + "aimingSkill": 0.3, + "reactionTime": 0.5, "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.3, - "objectiveFocus": 0.6 + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.5 } } \ No newline at end of file diff --git a/bots/bots_pub/willitz.json b/bots/bots_pub/willitz.json index b8495f7aa..761b2c253 100644 --- a/bots/bots_pub/willitz.json +++ b/bots/bots_pub/willitz.json @@ -1,35 +1,35 @@ { "name": "Willitz**", "clan_id": null, - "skin": "male/indy", + "skin": "sas/sasUC2", "weaponPreferences": { "mk23": 0.6, - "dual_mk23": 0.6, - "mp5": 0.7, - "m4": 0.5, - "m3": 0.6, - "hc": 0.7, - "sniper": 0.8, + "dual_mk23": 0.5, + "mp5": 0.5, + "m4": 0.8, + "m3": 0.1, + "hc": 0.5, + "sniper": 0.6, "knives": 0.7, - "grenades": 0.8 + "grenades": 1.1 }, "itemPreferences": { - "vest": 0.9, - "helm": 0.5, - "laser": 0.5, - "silencer": 0.6, - "slippers": 0.4, - "bandolier": 0.4 + "vest": 0.2, + "helm": 0.6, + "laser": 0.9, + "silencer": 0.4, + "slippers": 0.5, + "bandolier": 0.6 }, "coreTraits": { "aggressiveness": 0.5, - "teamwork": 0.8, - "curiosity": 0.6, - "aimingSkill": 0.1, - "reactionTime": 0.6, - "communicationFreq": 0.3, - "communicationTone": 0.7, - "movementStyle": 0.4, - "objectiveFocus": 0.8 + "teamwork": 0.7, + "curiosity": 0.5, + "aimingSkill": 0.4, + "reactionTime": 0.4, + "communicationFreq": 0.2, + "communicationTone": 0.2, + "movementStyle": 0.5, + "objectiveFocus": 0.3 } } \ No newline at end of file diff --git a/bots/bots_pub/wrack.json b/bots/bots_pub/wrack.json index f9f0a477d..e5fa0645d 100644 --- a/bots/bots_pub/wrack.json +++ b/bots/bots_pub/wrack.json @@ -1,35 +1,35 @@ { "name": "Wrack", "clan_id": null, - "skin": "male/indy", + "skin": "messiah/darkbeing", "weaponPreferences": { "mk23": 0.5, - "dual_mk23": 0.4, + "dual_mk23": 0.3, "mp5": 0.5, - "m4": 0.5, - "m3": 0.2, - "hc": 0.2, - "sniper": 0.4, + "m4": 0.4, + "m3": 0.7, + "hc": 0.7, + "sniper": 0.3, "knives": 0.3, - "grenades": 0.7 + "grenades": 0.3 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.3, - "slippers": 0.4, - "bandolier": 0.6 + "vest": 0.8, + "helm": 0.4, + "laser": 0.4, + "silencer": 0.6, + "slippers": 0.5, + "bandolier": 0.2 }, "coreTraits": { "aggressiveness": 0.2, - "teamwork": 0.8, + "teamwork": 0.4, "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.2, - "communicationFreq": 0.5, - "communicationTone": 0.7, - "movementStyle": 0.3, - "objectiveFocus": 0.4 + "aimingSkill": 0.5, + "reactionTime": 0.5, + "communicationFreq": 0.6, + "communicationTone": 0.5, + "movementStyle": 0.5, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/wreckage.json b/bots/bots_pub/wreckage.json deleted file mode 100644 index 3ef0d4c6e..000000000 --- a/bots/bots_pub/wreckage.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Wreckage", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.9, - "dual_mk23": 0.5, - "mp5": 0.1, - "m4": 0.7, - "m3": 0.6, - "hc": 0.4, - "sniper": 0.3, - "knives": 0.6, - "grenades": 0.4 - }, - "itemPreferences": { - "vest": 0.3, - "helm": 0.4, - "laser": 0.4, - "silencer": 0.7, - "slippers": 0.4, - "bandolier": 0.5 - }, - "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.7, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.4, - "communicationFreq": 0.4, - "communicationTone": 0.9, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/bots_pub/wyand.json b/bots/bots_pub/wyand.json deleted file mode 100644 index 4d1fc7715..000000000 --- a/bots/bots_pub/wyand.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Wyand", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.5, - "mp5": 0.6, - "m4": 0.8, - "m3": 0.3, - "hc": 0.8, - "sniper": 0.4, - "knives": 0.4, - "grenades": 0.2 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.5, - "laser": 0.7, - "silencer": 0.3, - "slippers": 0.8, - "bandolier": 0.2 - }, - "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.6, - "curiosity": 0.3, - "aimingSkill": 0.7, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.2, - "objectiveFocus": 0.1 - } -} \ No newline at end of file diff --git a/bots/bots_pub/wysyyy.json b/bots/bots_pub/wysyyy.json index edb6c9113..5eabe85a2 100644 --- a/bots/bots_pub/wysyyy.json +++ b/bots/bots_pub/wysyyy.json @@ -1,35 +1,35 @@ { "name": "wysyyy", "clan_id": null, - "skin": "male/indy", + "skin": "actionmale/aqgthugboss", "weaponPreferences": { - "mk23": 0.4, - "dual_mk23": 0.6, - "mp5": 0.3, - "m4": 0.6, - "m3": 0.5, - "hc": 0.4, - "sniper": 0.3, + "mk23": 0.3, + "dual_mk23": 0.3, + "mp5": 0.7, + "m4": 0.5, + "m3": 0.7, + "hc": 0.8, + "sniper": 0.0, "knives": 0.5, - "grenades": 0.5 + "grenades": 0.7 }, "itemPreferences": { - "vest": 0.4, - "helm": 0.2, - "laser": 0.4, - "silencer": 0.9, - "slippers": 0.6, - "bandolier": 0.8 + "vest": 0.3, + "helm": 0.5, + "laser": 0.2, + "silencer": 0.4, + "slippers": 0.7, + "bandolier": 0.3 }, "coreTraits": { - "aggressiveness": 0.3, - "teamwork": 0.4, - "curiosity": 0.7, - "aimingSkill": 0.5, - "reactionTime": 0.2, - "communicationFreq": 0.3, - "communicationTone": 0.5, - "movementStyle": 0.7, - "objectiveFocus": 0.6 + "aggressiveness": 0.7, + "teamwork": 0.5, + "curiosity": 0.3, + "aimingSkill": 0.7, + "reactionTime": 0.3, + "communicationFreq": 0.4, + "communicationTone": 0.6, + "movementStyle": 0.4, + "objectiveFocus": 0.7 } } \ No newline at end of file diff --git a/bots/bots_pub/xaero.json b/bots/bots_pub/xaero.json index 753d8d3e1..0c8c6e4e8 100644 --- a/bots/bots_pub/xaero.json +++ b/bots/bots_pub/xaero.json @@ -1,35 +1,35 @@ { "name": "Xaero", "clan_id": null, - "skin": "male/indy", + "skin": "terror/skyterr", "weaponPreferences": { - "mk23": 0.8, - "dual_mk23": 0.3, + "mk23": 0.6, + "dual_mk23": 0.7, "mp5": 0.1, "m4": 0.7, - "m3": 0.6, - "hc": 0.6, - "sniper": 0.8, + "m3": 0.3, + "hc": 0.5, + "sniper": 0.5, "knives": 0.5, - "grenades": 0.4 + "grenades": 0.6 }, "itemPreferences": { - "vest": 0.5, - "helm": 0.6, - "laser": 0.2, - "silencer": 0.6, - "slippers": 0.7, - "bandolier": 0.4 + "vest": 0.3, + "helm": 0.3, + "laser": 0.7, + "silencer": 0.4, + "slippers": 0.3, + "bandolier": 0.2 }, "coreTraits": { - "aggressiveness": 0.6, - "teamwork": 0.5, - "curiosity": 0.8, + "aggressiveness": 0.7, + "teamwork": 0.9, + "curiosity": 0.6, "aimingSkill": 0.4, - "reactionTime": 0.5, - "communicationFreq": 0.5, - "communicationTone": 0.5, - "movementStyle": 0.4, - "objectiveFocus": 0.6 + "reactionTime": 0.3, + "communicationFreq": 0.7, + "communicationTone": 0.6, + "movementStyle": 0.5, + "objectiveFocus": 0.1 } } \ No newline at end of file diff --git a/bots/bots_pub/zarna.json b/bots/bots_pub/zarna.json deleted file mode 100644 index 3570bb9c4..000000000 --- a/bots/bots_pub/zarna.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "Zarna", - "clan_id": null, - "skin": "male/indy", - "weaponPreferences": { - "mk23": 0.3, - "dual_mk23": 0.6, - "mp5": 0.2, - "m4": 0.4, - "m3": 0.5, - "hc": 0.3, - "sniper": 0.3, - "knives": 0.8, - "grenades": 0.3 - }, - "itemPreferences": { - "vest": 0.5, - "helm": 0.9, - "laser": 0.6, - "silencer": 0.4, - "slippers": 0.7, - "bandolier": 0.3 - }, - "coreTraits": { - "aggressiveness": 0.7, - "teamwork": 0.7, - "curiosity": 0.2, - "aimingSkill": 0.5, - "reactionTime": 0.7, - "communicationFreq": 0.4, - "communicationTone": 0.4, - "movementStyle": 0.6, - "objectiveFocus": 0.5 - } -} \ No newline at end of file diff --git a/bots/team_names.txt b/bots/clan_names.txt similarity index 100% rename from bots/team_names.txt rename to bots/clan_names.txt diff --git a/bots/other.txt b/bots/other.txt deleted file mode 100644 index f91618e2a..000000000 --- a/bots/other.txt +++ /dev/null @@ -1,659 +0,0 @@ -John Carmack -John Romero -Michael Abrash -Sandy Petersen -Kevin Cloud -Adrian Carmack -American Mcgee -Tim Willits -Sonic Mayhem -Trent Reznor -NiN -Scourge of Armagon -Dissolution of Eternity -The Reckoning -Ground Zero -Enforcer -Gunner -Berserker -Iron Maiden -Gladiator -Medic -Technician -Icarus -Tank -Makron -Quad Machine -Cerberon -Strogg -Bitterman -Crash -Ranger -Phobos -Mynx -Orbb -Sarge -Grunt -Hossman -Daemia -Hunter -Angel -Gorre -Klesk -Slash -Wrack -Biker -Lucy -Patriot -Tank Jr -Anarki -Stripe -Razor -Keel -Visor -Uriel -Bones -Cadaver -Sorlag -Doom -Major -Xaero -Ratbot -HAL-9000 -Brandon Herrera -John Conner -Terminator -T-800 -T-1000 -Anakin Skywalker -Short Round -Barney Ross -The Boss -Dean Stevens -The Dean -Jake Malloy -Joe Tanto -Kit Latura -Robert Rath -Judge Joseph Dredd -Ray Quick -John Spartan -Gabe Walker -Angelo Provolone -Snaps -Lt Raymond Tango -Lincoln Hawk -Lieutenant Marion Cobretti -Cobra -John Rambo -Captain Robert Hatch -Deke DaSilva -Cosmo Carboni -Johnny Kovac -Rocky Balboa -Elmore Caddo -Rocky -Machine Gun Joe Viterbo -Jerry Savage -Stud -Trench -Gordy Brewer -Jericho Cane -Dr Victor Fries -U.S. Marshal John Kruger -The Eraser -Harry Tasker -Jack Slater -Capt. Ivan Danko -Dutch -Mark Kaminsky -Joseph P. Brenner -John Matrix -Kalidor -Conan -Handsome Stranger -Muscleman -Joe Santo -Church -Dink Heimowitz -Mr Suave -Frank Moses -Harrison Hill -Col. Doug Masterson -Mr Goodkat -Sonny Truelove -Lieutenant AK Waters -Jimmy Tudeski -The Tulip -Trey Kincaid -Major General William Deveraux -Harry Stamper -Korben Dallas -Muddy Grimes -Butch Coolidge -Det. Tom Hardy -Hudson Hawk -James Urbanski -Tom Mix -Lee Christmas -Jensen Ames -Terry Leather -Farmer the farmer -Chev Chelios -Handsome Rob -Monk -MVA Agent Evan Funsch -Sgt Jericho Butler -Turkish -Bacon -Jean Vilain -Samson Gaul -Tiano -Vincent Brazil -Frenchy -Jack Robideaux -Phillip Sauvage -Ben Archer -Jacques Kristoff -Rudy Cafmeyer -Charles Le Vaillaint -Replicant -Edward Garrotte -Alain Moreau -Mikhail Suverov -Darren McCord -Colonel William F. Guile -Chance Boudreaux -Luc Deveraux -Alex Wagner -Chad Wagner -Gibson Rickenbacker -Frank Dux -Ivan Kraschinsky -Booker -John Shepherd -Joshua McCord -Jake Fallon -McKenna -Jake Wilder -Frank Shatter -Capt. Ranger Cordell Walker -Matt Hunter -JJ McQuade -Lone Wolf -Sean Kane -John T. Booker -Chuck Slaughter -Colt -Gunner Jensen -Max Gatling -The German -Dr. Sage Mennox -Edward Genn -Icarus -Mike Riggins -Ryder -Xander Ronson -Brixos -Nikolai Cherenko -Lance Rockford -Sgt. Frank Gannon -Sam Decker -Warchild -Major Frank Cross -Waxman -Michael Dane -Nick Gunar -Wellman Anthony Santee -Det. Jack Caine -Lt. Nikolai Rachenko -Ivan Drago -Venz -Tom Steele -Elijah Kane -Rogelio Torrez -Samuel Axel -Ruslan -Tao -Cock Puncher -Simon Ballister -Commander Marshall Lawson -Jonathan Cold -Harlan Banks -Travis Hunter -William Lansing -Professor Robert Burns -Sasha Petrosevitch -Frank Glass -Orin Boyd -Lt. Colonel Austin Travis -Forrest Taft -Casey Ryback -Detective Gino Felino -Mason Storm -General Hugh McKraken -Chief Benjamin Benson -Freddie Wiseman -Dr. Artimus Snodgrass -General Skyler -Adam Beaudreaux -Billy Smith -Hurricane -Colonel Carl Brewster -Kyle Western -Sgt. Jericho Jackson -Action Jackson -Cullen Monroe -Lieutenant Harry Braker -Sundog -Dreamer Tatum -Bateman Hooks -Apollo Creed -Jack Hopper -Yarbro -Hambone -Bad Sam -Clint Armstrong -Not Hasselhoff -Little G -JD Gold -Strom -Leland Duvall -Warden Pitt -Joe Joiner -Huck Finney -Lt. Col. 'Mac' Miller -Jack Saxon -Arklon -Jack Maxwell -Van Vandameer -Victor Lundgren -Alriss Ryder -Di Nardo -Carpenter -John Luger -Sam Striker -Capt. Alvin Luther Regency -Wilson Mahood -Jimmy Jo Walker -Lieutenant Byrd -Stoney Cooper -Reddog -Ramrod -Kimbo Slice -Boy -Gung Ho -Mincer -Major Pain -Jonny Rambone -Dominator -Noogie -Cruton -Heljay -Mongrel -Private Brown -Brown Steel -Doctor Deathwish -Stainmaster B -Kippy -Gyngemaster2000 -Helmut -SGT CAPPA -Saboy -Chipee McSpray -Himdar -Vedro -Icexo -Tahlkora -Bolund -Ellanil -Galul -Mindelos -Ostmor -Sagel -Thalath -Dharmesh -Aegarond -Havo -Jarno -Leiruth -Edoli -Edwig -Faden -Skorgan -Walter -Fred -Morgan -Lawrence -Richard -Michael -Brad -George -Anton -Monty -Bean -Sean -Backfire -Halfwit -Halfbaked -Fullmonty -Nohope -Hitnrun -Missnrun -Oysterhead -Fullthrottle -NoAmmo -Bullseye -Aimless -Blackadder -Newbie -Cledus -Chimichanga -Razor -Tanz -Vic -Malin -Thor -Grog -Drago -Stinger -Tarnok -Wens -Brohn -Ivor -Gordac -Bortack -Morgriff -Kole -Garbok -Korv -Vokur -Gali -Beelz -CraigChrist -WhittlinMan -SpecialEd -Flint -Argo -Milius -Vera -Merki -Tarnen -Royen -Kaolin -Nandet -Nundak -Ryver -Mungri -Chan -Amadi -Zarna -PillowPants -ListerFiend -Ilikechocmilk -Andromeed -Cybr -Applee -uzayTron -[Corrus] -[Mee2] -GreZZer -Drillbit -Ant__Dog -Si -Tille-Teh-Black -itiXOID -Blythe -**Viander** -wysyyy -hozef-8 -ActioN-ReactioN -VeY -Dolin -Vanoss -Imentu -T_bon -cook did it -Panther.Killian -Chess -Dragoooon -Hammerhead -Bright GhOst -CowChop -Bob Ross -Nibbler -Private Ryan -GhOstBl4de -_*NiXXy*_ -_*PixxY*_ -D3c3ptiv3 Jo3 -Merry Christmas -Merry Yeetus -NinjaMantis -SteamPug -TheWay -Scary_Kitty -Mr.Badger -Cannon Balls Of Steel -_Gholem_ -_The Medic_ -VenoM -Broz -Himself -The Only -Way Of The -_GLiCH -Hecker -ParaDox -Two Four Eight -Bilbo T-Bagger -PewDiePie -Jinx -JohnShnowden -Heckin Heck -Why Me? -Why are we here? -Where's Mark Hunt? -Hell-oKitteh -Slice -BroFist -Bad JuJu -Return of the Byeah -Po-Tay-Toe -Lemmie -Wreckage -Butter Jelly -tatu -Major Major -Digz -Figgz -Eggcecutioner -MoreEggcellent -C-Lion -attractiveCow -Grimm -D is for Digger! -Professor Jones -{itzChamp} -Sigil -Willitz** -Chickenn...! -Evo89 -John Romero -=WhiteBeard= -CitizenKane -chipp_ -^^ChoppY^^ -Dr.Jay -Fexel -FXguy -Teabag -Gigavoltz -gogman -GuN -JC Denton -deUSvauLT -exBEE -JCe -Joce -Marku -Pyro Yetti -SergeantBuck -2DCitizen -Amu -BVe -BigMack -doc -Doctor Doom -FUNKANIK -GreetingzGentlemen -Jarvis -kuri -Kor -Kermit D Frog -Jazz -MannY -Hellhound -ApeM -~Shadowfax~ -Socks -Spocket -Spar -Johnny Cage -Kano -Liu Kang -Raiden -Reptile -Scorpion -Shang Tsung -Sonya Blade -Sub-Zero -Baraka -Jade -Jax -Kintaro -Kitana -Kung Lao -Mileena -Noob Saibot -Shao Kahn -Smoke -Chameleon -Cyrax -Ermac -Kabal -Khameleon -Motaro -Nightwolf -Rain -Sektor -Sheeva -Sindel -Stryker -Fujin -Quan Chi -Sareena -Shinnok -Jarek -Kai -Meat -Reiko -Tanya -Tremor -Blaze -Bo' Rai Cho -Drahmin -Frost -Hsu Hao -Kenshi -Li Mei -Mavado -Mokap -Moloch -Nitara -Ashrah -Dairou -Darrius -Havik -Hotaru -Kira -Kobra -Onaga -Shujinko -Daegon -Taven -Dark Kahn -Skarlet -Cassie Cage -D'Vorah -Erron Black -Ferra/Torr -Jacqui Briggs -Kotal Kahn -Kung Jin -Takeda Takahashi -Triborg -Cetrion -Geras -Kollector -Kronika -Belokk -AqtionWoman -AqtionLLama -Bishibosh -Bonebreak -Coldcrow -Rakanishu -Treehead -Griswold -Countess -Pitspawn Fouldog -Flamespike -Boneash -Radament -Bloodwitch -Fangskin -Beetleburst -Leatherarm -Coldworm -Summoner -Kaa -Smithy -Witch Doctor -Stormtree -Sarina -Icehawk -Ismail -Geleb -Bremm -Toorc -Wyand -Endugu -Maffer -Death -Tormentor -Taintbreeder -Riftwraith -Infector of Souls -Lord De Seis -Vizier -Cow King -Corpsefire -Feature Creep -Siege -Barbarian -Axe Dweller -Bonesaw Breaker -Dac Farren -Megaflow Rectifier -Eyeback -Threash -Pindleskin -Snapchip Shatter -Anodized -Vinvear Molech -Tooth Sayer -Magma Torquer -Blaze Ripper -Frozenstein -Nihlathak \ No newline at end of file diff --git a/bots/skin_names.txt b/bots/skin_names.txt new file mode 100644 index 000000000..22312dd0f --- /dev/null +++ b/bots/skin_names.txt @@ -0,0 +1,241 @@ +actionmale/agent +actionmale/aqgthugboss +actionmale/axef +actionmale/badmutha +actionmale/BlackStars +actionmale/blood +actionmale/blucedojo +actionmale/bluceree +actionmale/Bruce_Wayne +actionmale/castor +actionmale/chellobeta +actionmale/chucky +actionmale/crip +actionmale/ctf_b +actionmale/ctf_r +actionmale/dafist +actionmale/dubeta +actionmale/invince +actionmale/jewels +actionmale/jungseal +actionmale/killer +actionmale/leonreno +actionmale/lfc +actionmale/morpheus +actionmale/RCMP +actionmale/scarfchain +actionmale/scarf +actionmale/seagull +actionmale/shinobi +actionmale/swat +actionrally/ctf_b +actionrally/ctf_r +actionrally/modsquad +actionrally/natasha +actionrally/pulp +actionrally/trinity +aqmarine/aquamarine +aqmarine/centurion +aqmarine/desert +aqmarine/marine1 +aqmarine/marine2 +aqmarine/urban +aqmarine/usmc +aqmarine/woods +female/ctf_b +female/ctf_r +female/kw_aqua +female/kw_black +female/kw_blue +female/kw_green +female/kw_pink +female/kw_red +female/kw_white +female/kw_yellow +female/leeloop +female/sarah_ohconnor +female/tankgirl +male/adidas +male/aqgthug +male/aqgvillain +male/austin2 +male/babarracuda +male/beastdl +male/bluebeard +male/blues +male/boba +male/borya +male/bravo +male/brucelee +male/bruces +male/butchy +male/chowda2 +male/chowda +male/chow +male/Clan_UNA +male/commando +male/conan +male/coolio +male/cop +male/copper +male/ctf_b +male/ctf_g +male/ctf_r +male/ctf_vip_b +male/ctf_vip_g +male/ctf_vip_r +male/ctf_y +male/cyrus +male/dashaft +male/deadpl +male/elway +male/GreenBeret +male/grunt +male/habs +male/hall +male/hellspawn +male/hollywood +male/homer +male/hulk2 +male/image +male/indy +male/invince +male/jax-jaguar +male/jdredd +male/jewels +male/jules +male/kgb +male/kw_aqua +male/kw_black +male/kw_blue +male/kw_green +male/kw_orange +male/kw_pink +male/kw_red +male/kw_white +male/kw_yellow +male/leaf +male/leservoircat +male/madmaxy +male/mariom +male/marsell +male/mason +male/mclaine +male/MikeLowry +male/mikeyl +male/mkrain +male/mkrept +male/mkscorp +male/mksub +male/Mohoney +male/mr_t +male/nike +male/nut +male/nwom +male/ocdcqb +male/optimus +male/{oz} +male/oz +male/pimp +male/police +male/rembo +male/resdog +male/rigs +male/robber +male/roger +male/rredneck +male/sabotage +male/santa +male/sdes +male/SEALU +male/shaft +male/siris +male/snowcamo +male/Superman +male/t-800m +male/trmntr +male/walter +male/wcwsting3 +messiah/axe +messiah/badger +messiah/blackman +messiah/blade2 +messiah/blade3 +messiah/blade4 +messiah/blade +messiah/chowblue +messiah/chow +messiah/chowred +messiah/cptahab +messiah/crawler +messiah/ctf_b +messiah/ctf_r +messiah/darkbeing +messiah/evil_axe +messiah/keaneo +messiah/kindig +messiah/leon +messiah/mxr_neo +messiah/neo +messiah/outlaw +messiah/robber +messiah/scuba +messiah/slug +messiah/spectre +messiah/suit +messiah/TheCrow +messiah/thedon +sas/aqmsas +sas/atf +sas/ctf_b +sas/ctf_r +sas/deathreat +sas/decsas +sas/fbi +sas/grassland +sas/grunt +sas/mud +sas/nithb +sas/norse1 +sas/norse2 +sas/sas2 +sas/sasdc2 +sas/sasdc +sas/sasjungle +sas/sas +sas/saspolice +sas/sasUC2 +sas/sasuc +sas/sas-urban +sas/sasurban +sas/saswc +sas/sas-woodland +sas/STARS +sydney/blonde +sydney/sydney +terror/adf2 +terror/black cop +terror/blue cop +terror/csw black +terror/csw blue +terror/csw crazy +terror/csw +terror/csw red +terror/ctf_b +terror/ctf_r +terror/desertterr +terror/fbiterr +terror/j_mafia +terror/jungleterr +terror/kgb +terror/mafia2 +terror/mafia +terror/red cop +terror/redterr2 +terror/rmafia +terror/skyterr +terror/stormtrooper +terror/swat +terror/swatsnipe +terror/terror +terror/urbanterr diff --git a/meson.build b/meson.build index 05221f846..2d1c2e41e 100644 --- a/meson.build +++ b/meson.build @@ -502,7 +502,7 @@ if get_option('discord-sdk') # Discord does not have an SDK for this platform yet, setting USE_DISCORD to false config.set10('USE_DISCORD', false) else - config.set10('USE_DISCORD', true) + config.set10('USE_DISCORD', false) # Disabling Discord for now discord_inc_path = join_paths('extern/discord/lib/', host_machine.cpu_family()) dis_inc = include_directories(discord_inc_path) discord_dep_path = join_paths(meson.current_source_dir(), discord_inc_path) From 51422fad39714e8157dda29e094aea2ef0979874 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 28 Feb 2024 14:31:25 -0500 Subject: [PATCH 087/974] Moved discord.c, removed old commennted code, added Highscores from OpenFFA --- meson.build | 4 +- src/action/e_game.c | 240 --------------------------- src/action/g_cmds.c | 30 +++- src/action/g_local.h | 23 +++ src/action/g_main.c | 4 + src/action/g_save.c | 1 + src/action/g_spawn.c | 3 + src/action/p_view.c | 1 + src/action/tng_stats.c | 364 ++++++++++++++++++++++++++++++++++------- src/client/client.h | 111 +++++++++++++ src/client/discord.c | 112 +------------ 11 files changed, 486 insertions(+), 407 deletions(-) delete mode 100644 src/action/e_game.c diff --git a/meson.build b/meson.build index 2d1c2e41e..20a2e9970 100644 --- a/meson.build +++ b/meson.build @@ -502,14 +502,14 @@ if get_option('discord-sdk') # Discord does not have an SDK for this platform yet, setting USE_DISCORD to false config.set10('USE_DISCORD', false) else - config.set10('USE_DISCORD', false) # Disabling Discord for now + config.set10('USE_DISCORD', true) # Disabling Discord for now discord_inc_path = join_paths('extern/discord/lib/', host_machine.cpu_family()) dis_inc = include_directories(discord_inc_path) discord_dep_path = join_paths(meson.current_source_dir(), discord_inc_path) discord_dep = cc.find_library('discord_game_sdk', dirs : discord_dep_path) client_deps += discord_dep - action_src += ['src/client/discord.c'] + client_src += ['src/client/discord.c'] endif #endif endif diff --git a/src/action/e_game.c b/src/action/e_game.c deleted file mode 100644 index 66b42c1c9..000000000 --- a/src/action/e_game.c +++ /dev/null @@ -1,240 +0,0 @@ -// AQ2: ETE migration to TNG // -// One library to rule them all, and in the darkness, frag them // - -#include "g_local.h" - -qboolean EspSceneLoadConfig(char *mapname) -{ - char buf[1024]; - char *ptr; - char def_scnfile[1024]; - FILE *fh; - - memset(&ctfgame, 0, sizeof(ctfgame)); - - gi.dprintf("Trying to load Espionage configuration file\n", mapname); - - /* zero is perfectly acceptable respawn time, but we want to know if it came from the config or not */ - ctfgame.spawn_red = -1; - ctfgame.spawn_blue = -1; - - sprintf (buf, "%s/tng/%s.scn", GAMEVERSION, mapname); - fh = fopen (buf, "r"); - if (!fh) { - gi.dprintf ("Warning: Espionage configuration file %s was not found.\n", buf); - return false; - } - else if - - gi.dprintf("-------------------------------------\n"); - gi.dprintf("CTF configuration loaded from %s\n", buf); - ptr = INI_Find(fh, "ctf", "author"); - if(ptr) { - gi.dprintf(" Author : %s\n", ptr); - Q_strncpyz(ctfgame.author, ptr, sizeof(ctfgame.author)); - } - ptr = INI_Find(fh, "ctf", "comment"); - if(ptr) { - gi.dprintf(" Comment : %s\n", ptr); - Q_strncpyz(ctfgame.comment, ptr, sizeof(ctfgame.comment)); - } - - ptr = INI_Find(fh, "ctf", "type"); - if(ptr) { - gi.dprintf(" Game type : %s\n", ptr); - if(strcmp(ptr, "balanced") == 0) - ctfgame.type = 1; - if(strcmp(ptr, "offdef") == 0) - ctfgame.type = 2; - } - ptr = INI_Find(fh, "ctf", "offence"); - if(ptr) { - gi.dprintf(" Offence : %s\n", ptr); - ctfgame.offence = TEAM1; - if(strcmp(ptr, "blue") == 0) - ctfgame.offence = TEAM2; - } - ptr = INI_Find(fh, "ctf", "grapple"); - gi.cvar_forceset("use_grapple", "0"); - if(ptr) { - gi.dprintf(" Grapple : %s\n", ptr); - if(strcmp(ptr, "1") == 0) - gi.cvar_forceset("use_grapple", "1"); - else if(strcmp(ptr, "2") == 0) - gi.cvar_forceset("use_grapple", "2"); - } - - gi.dprintf(" Spawn times\n"); - ptr = INI_Find(fh, "respawn", "red"); - if(ptr) { - gi.dprintf(" Red : %s\n", ptr); - ctfgame.spawn_red = atoi(ptr); - } - ptr = INI_Find(fh, "respawn", "blue"); - if(ptr) { - gi.dprintf(" Blue : %s\n", ptr); - ctfgame.spawn_blue = atoi(ptr); - } - - gi.dprintf(" Flags\n"); - ptr = INI_Find(fh, "flags", "red"); - if(ptr) { - gi.dprintf(" Red : %s\n", ptr); - CTFSetFlag(TEAM1, ptr); - } - ptr = INI_Find(fh, "flags", "blue"); - if(ptr) { - gi.dprintf(" Blue : %s\n", ptr); - CTFSetFlag(TEAM2, ptr); - } - - gi.dprintf(" Spawns\n"); - ptr = INI_Find(fh, "spawns", "red"); - if(ptr) { - gi.dprintf(" Red : %s\n", ptr); - CTFSetTeamSpawns(TEAM1, ptr); - ctfgame.custom_spawns = true; - } - ptr = INI_Find(fh, "spawns", "blue"); - if(ptr) { - gi.dprintf(" Blue : %s\n", ptr); - CTFSetTeamSpawns(TEAM2, ptr); - ctfgame.custom_spawns = true; - } - - // automagically change spawns *only* when we do not have team spawns - if(!ctfgame.custom_spawns) - ChangePlayerSpawns(); - - gi.dprintf("-------------------------------------\n"); - - fclose(fh); - - return true; -} - - -void G_LoadScenes( void ) -{ - //Based on AQ2:TNG New Location Code G_LoadLocations - char scnfile[MAX_QPATH], buffer[256]; - FILE *f, *d; - int i, x, y, z, rx, ry, rz; - char *locationstr, *param, *line; - cvar_t *game_cvar; - placedata_t *loc; - char def_scnfile[MAX_QPATH], buffer[256]; - char def_scnfilename; - - memset( scn_creator, 0, sizeof( scn_creator ) ); - ml_count = 0; - - def_scnfilename = "atl.scn"; - - game_cvar = gi.cvar ("game", "action", 0); - - if (!*game_cvar->string) - Q_snprintf(scnfile, sizeof(scnfile), "%s/tng/%s.scn", GAMEVERSION, level.mapname); - else - Q_snprintf(scnfile, sizeof(scnfile), "%s/tng/%s.scn", game_cvar->string, level.mapname); - - f = fopen( scnfile, "r" ); - if (!f) { - gi.dprintf( "No scene file for %s\n", level.mapname ); - gi.dprintf( "Attempting to load default scene file %s\n", def_scnfile ); - Q_snprintf(def_scnfile, sizeof(scnfile), "%s/tng/%s.scn", game_cvar->string, def_scnfilename); - d = fopen( def_scnfile, "r"); - - gi.dprintf( "Scene file: %s\n", def_scnfilename ); - if (!d){ - gi.dprintf( "No scene files found, aborting\n" ); - return; - } - } - else { - gi.dprintf( "Scene file: %s\n", scnfile ); - } - - do - { - line = fgets( buffer, sizeof( buffer ), f ); - if (!line) { - break; - } - - if (strlen( line ) < 12) - continue; - - if (line[0] == '#') - { - param = line + 1; - while (*param == ' ') { param++; } - if (*param && !Q_strnicmp(param, "creator", 7)) - { - param += 8; - while (*param == ' ') { param++; } - for (i = 0; *param >= ' ' && i < sizeof( scn_creator ) - 1; i++) { - scn_creator[i] = *param++; - } - scn_creator[i] = 0; - while (i > 0 && scn_creator[i - 1] == ' ') //Remove trailing spaces - scn_creator[--i] = 0; - } - continue; - } - - param = strtok( line, " :\r\n\0" ); - // TODO: better support for file comments - if (!param || param[0] == '#') - continue; - - x = atoi( param ); - - param = strtok( NULL, " :\r\n\0" ); - if (!param) - continue; - y = atoi( param ); - - param = strtok( NULL, " :\r\n\0" ); - if (!param) - continue; - z = atoi( param ); - - param = strtok( NULL, " :\r\n\0" ); - if (!param) - continue; - rx = atoi( param ); - - param = strtok( NULL, " :\r\n\0" ); - if (!param) - continue; - ry = atoi( param ); - - param = strtok( NULL, " :\r\n\0" ); - if (!param) - continue; - rz = atoi( param ); - - param = strtok( NULL, "\r\n\0" ); - if (!param) - continue; - locationstr = param; - - loc = &locationbase[ml_count++]; - loc->x = x; - loc->y = y; - loc->z = z; - loc->rx = rx; - loc->ry = ry; - loc->rz = rz; - Q_strncpyz( loc->desc, locationstr, sizeof( loc->desc ) ); - - if (ml_count >= MAX_LOCATIONS_IN_BASE) { - gi.dprintf( "Cannot read more than %d locations.\n", MAX_LOCATIONS_IN_BASE ); - break; - } - } while (1); - - fclose( f ); - gi.dprintf( "Found %d locations.\n", ml_count ); -} \ No newline at end of file diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index a50c9b06a..ee31abc8c 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1829,6 +1829,33 @@ static void Cmd_CPSI_f (edict_t * ent) } } +void Cmd_HighScores_f(edict_t *ent) +{ + int i; + char date[MAX_QPATH]; + struct tm *tm; + highscore_t *s; + + if (!level.numscores) { + gi.cprintf(ent, PRINT_HIGH, "No high scores available.\n"); + return; + } + + gi.cprintf(ent, PRINT_HIGH, + "\n" + " # Name FPH Date\n" + "-- --------------- ---- ----------------\n"); + for (i = 0; i < level.numscores; i++) { + s = &level.scores[i]; + + tm = localtime(&s->time); + if (!tm || !strftime(date, sizeof(date), "%Y-%m-%d %H:%M", tm)) + strcpy(date, "???"); + gi.cprintf(ent, PRINT_HIGH, "%2d %-15.15s %4d %s\n", + i + 1, s->name, s->score, date); + } +} + #define CMDF_CHEAT 1 //Need cheat to be enabled #define CMDF_PAUSE 2 //Cant use while pause @@ -1952,7 +1979,8 @@ static cmdList_t commandList[] = { "jmod", Cmd_Jmod_f, 0 }, // Espionage, aliased command so it's easy to remember { "volunteer", Cmd_Volunteer_f, 0}, - { "leader", Cmd_Volunteer_f, 0} + { "leader", Cmd_Volunteer_f, 0}, + { "highscores", Cmd_HighScores_f, 0}, }; #define MAX_COMMAND_HASH 64 diff --git a/src/action/g_local.h b/src/action/g_local.h index 6e171ad55..c19b44627 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -648,6 +648,17 @@ bind 6 "use Sniper Rifle" #define MAX_SPAWNS 512 // max DM spawn points supported +// High Scores support from OpenFFA +#define MAX_HIGH_SCORES 10 + +typedef struct highscore_s +{ + char name[MAX_CLIENT_NAME]; + int score; + time_t time; +} highscore_t; + + //AQ2:TNG End adding flags typedef struct itemList_s @@ -760,6 +771,9 @@ typedef struct //q2pro protocol extensions cs_remap_t csr; precache_t *precaches; + + // High Scores support from OpenFFA + char dir[MAX_OSPATH]; // where variable data is stored } game_locals_t; @@ -788,6 +802,11 @@ typedef struct vec3_t intermission_origin; vec3_t intermission_angle; + // high scores from OpenFFA + highscore_t scores[MAX_HIGH_SCORES]; + int numscores; + time_t record; // not zero if scores updated + char *changemap; char statusbar[1024]; //MAX is 1536 = (MAX_QPATH * (CS_AIRACCEL - CS_STATUSBAR)) @@ -1267,6 +1286,7 @@ extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command // 2024 extern cvar_t *warmup_unready; extern cvar_t *training_mode; // Sets training mode vars +extern cvar_t *g_highscores_dir; // Sets the highscores directory #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); @@ -1636,6 +1656,9 @@ void ProduceShotgunDamageReport(edict_t*); //tng_stats.c void StatBotCheck(void); +void G_RegisterScore(void); +int G_CalcRanks(gclient_t **ranks); +void G_LoadScores(void); #if USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); diff --git a/src/action/g_main.c b/src/action/g_main.c index 6f1bee714..20e7679d2 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -532,6 +532,7 @@ cvar_t *sv_killgib; // Gibs on 'kill' command // 2024 cvar_t *warmup_unready; // Toggles warmup if captains unready cvar_t *training_mode; // Sets training mode vars +cvar_t *g_highscores_dir; // Sets the highscores directory #if AQTION_EXTENSION cvar_t *use_newirvision; @@ -833,6 +834,9 @@ void EndDMLevel (void) gi.bprintf (PRINT_HIGH, "Game ending at: %s\n", ltm); IRC_printf (IRC_T_GAME, "Game ending at: %s", ltm); + // High scores from OpenFFA + G_RegisterScore(); + // JBravo: Stop q2pro MVD2 recording if (use_mvd2->value) { diff --git a/src/action/g_save.c b/src/action/g_save.c index dc9557db7..10696c6a0 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -651,6 +651,7 @@ void InitGame( void ) gi.cvar_forceset("dmweapon", "Combat Knife"); gi.cvar_forceset("bholelimit", "30"); } + g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); // new AQtion Extension cvars #if AQTION_EXTENSION diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index ae71310b9..e40ba1dbb 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1371,6 +1371,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn G_LoadLocations(); + // High score load file + G_LoadScores(); + SVCmd_CheckSB_f(); //rekkie -- silence ban UnBan_TeamKillers(); diff --git a/src/action/p_view.c b/src/action/p_view.c index 07fc692f6..af0ad6c75 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -304,6 +304,7 @@ void SV_CalcViewOffset (edict_t * ent) if (ent->deadflag) { VectorClear(angles); + ent->client->ps.viewangles[ROLL] = 40; ent->client->ps.viewangles[PITCH] = -15; ent->client->ps.viewangles[YAW] = ent->client->killer_yaw; diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 504f1b0e5..92599f8a4 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -69,6 +69,7 @@ #include "g_local.h" #include +#include /* Stats Command */ @@ -490,64 +491,317 @@ void Cmd_Statmode_f(edict_t* ent) stuffcmd(ent, stuff); } -#if USE_AQTION +/* +================== +High Scores List -// Revisit one day... - -// #include -// // AQtion stats addon -// // Utilizes AWS API Gateway and AWS SQS -// // Review documentation to understand their use - -// size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp) -// { -// return size * nmemb; -// } -// void StatSend(const char *payload, ...) -// { -// va_list argptr; -// char text[1024]; -// char apikeyheader[64] = "x-api-key: "; -// char apiurl[128] = "\0"; -// int apikey_check; - -// // If stat logs are disabled or stat-apikey is default, just return -// apikey_check = Q_stricmp(stat_apikey->string, "none"); -// if (!stat_logs->value || apikey_check == 0) { -// return; -// } - -// Q_strncatz(apikeyheader, stat_apikey->string, sizeof(apikeyheader)); -// Q_strncpyz(apiurl, stat_url->string, sizeof(apiurl)); - -// va_start (argptr, payload); -// vsnprintf (text, sizeof(text), payload, argptr); -// va_end (argptr); - -// CURL *curl = curl_easy_init(); -// struct curl_slist *headers = NULL; -// headers = curl_slist_append(headers, "Accept: application/json"); -// headers = curl_slist_append(headers, "Content-Type: application/json"); -// headers = curl_slist_append(headers, apikeyheader); - -// curl_easy_setopt(curl, CURLOPT_URL, apiurl); -// curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); -// curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); -// curl_easy_setopt(curl, CURLOPT_POSTFIELDS, text); - -// // Do not print responses from curl request -// // Comment below if you are debugging responses -// // Hint: Forbidden would mean your stat_url is malformed, -// // and a key error indicates your api key is bad or expired -// curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); - -// // Run it! -// curl_easy_perform(curl); -// curl_easy_cleanup(curl); -// curl_global_cleanup(); -// } +Inspired and based on the original code by skullernet from OpenFFA +================== +*/ + + +typedef struct { + int nb_lines; + char **lines; + char path[1]; +} load_file_t; + +q_printf(1, 2) +static load_file_t *G_LoadFile(const char *fmt, ...) +{ + char path[MAX_OSPATH]; + size_t pathlen; + va_list argptr; + int err = 0; + + va_start(argptr, fmt); + pathlen = Q_vsnprintf(path, sizeof(path), fmt, argptr); + va_end(argptr); + + if (pathlen >= sizeof(path)) { + err = ENAMETOOLONG; + goto fail0; + } + + FILE *fp = fopen(path, "rb"); + if (!fp) { + err = errno; + goto fail0; + } + + Q_STATBUF st; + if (os_fstat(os_fileno(fp), &st)) { + err = errno; + goto fail1; + } + + if (st.st_size >= INT_MAX / sizeof(char *)) { + err = EFBIG; + goto fail1; + } + + char *buf = gi.TagMalloc(st.st_size + 1, TAG_GAME); + + if (!fread(buf, st.st_size, 1, fp)) { + err = EIO; + goto fail2; + } + + buf[st.st_size] = 0; + + load_file_t *f = gi.TagMalloc(sizeof(*f) + pathlen, TAG_GAME); + f->nb_lines = 0; + f->lines = NULL; + memcpy(f->path, path, pathlen + 1); + + int max_lines = 0; + while (1) { + char *p = strchr(buf, '\n'); + if (p) { + if (p > buf && *(p - 1) == '\r') + *(p - 1) = 0; + *p = 0; + } + + if (f->nb_lines == max_lines) { + void *tmp = f->lines; + f->lines = gi.TagMalloc(sizeof(char *) * (max_lines += 32), TAG_GAME); + if (tmp) { + memcpy(f->lines, tmp, sizeof(char *) * f->nb_lines); + gi.TagFree(tmp); + } + } + f->lines[f->nb_lines++] = buf; + + if (!p) + break; + buf = p + 1; + } + + fclose(fp); + return f; + +fail2: + gi.TagFree(buf); +fail1: + fclose(fp); +fail0: + gi.dprintf("Couldn't load '%s': %s\n", path, strerror(err)); + return NULL; +} + +static void G_FreeFile(load_file_t *f) +{ + gi.TagFree(f->lines[0]); + gi.TagFree(f->lines); + gi.TagFree(f); +} + +static int G_CreatePath(char *path) +{ + char *p; + int ret; + + // skip leading slash(es) + for (p = path; *p == '/'; p++) + ; + + for (; *p; p++) { + if (*p == '/') { + // create the directory + *p = 0; + ret = os_mkdir(path); + *p = '/'; + if (ret == -1 && errno != EEXIST) + return -1; + } + } + return 0; +} + +static int ScoreCmp(const void *p1, const void *p2) +{ + highscore_t *a = (highscore_t *)p1; + highscore_t *b = (highscore_t *)p2; + + if (a->score > b->score) { + return -1; + } + if (a->score < b->score) { + return 1; + } + if (a->time > b->time) { + return -1; + } + if (a->time < b->time) { + return 1; + } + return 0; +} + +#define SD(s) *s ? "/" : "", s + +static void G_SaveScores(void) +{ + char path[MAX_OSPATH]; + highscore_t *s; + FILE *fp; + int i; + size_t len; + + len = Q_snprintf(path, sizeof(path), "%s%s%s/%s.txt", + GAMEVERSION, SD(g_highscores_dir->string), level.mapname); + if (len >= sizeof(path)) { + return; + } + G_CreatePath(path); + fp = fopen(path, "w"); + if (!fp) { + gi.dprintf("Couldn't open '%s': %s\n", path, strerror(errno)); + return; + } + + for (i = 0; i < level.numscores; i++) { + s = &level.scores[i]; + fprintf(fp, "\"%s\" %d %lu\n", + s->name, s->score, (unsigned long)s->time); + } + + fclose(fp); +} + +static int G_PlayerCmp(const void *p1, const void *p2) +{ + gclient_t *a = *(gclient_t * const *)p1; + gclient_t *b = *(gclient_t * const *)p2; + + int r = b->resp.score - a->resp.score; + if (!r) + r = a->resp.deaths - b->resp.deaths; + if (!r) + r = (byte *)a - (byte *)b; + return r; +} + +int G_CalcRanks(gclient_t **ranks) +{ + int i, total; + + // sort the clients by score, then by eff + total = 0; + for (i = 0; i < game.maxclients; i++) { + if (game.clients[i].pers.connected == true) { + if (ranks) { + ranks[total] = &game.clients[i]; + } + total++; + } + } + + if (ranks) { + qsort(ranks, total, sizeof(gclient_t *), G_PlayerCmp); + } + + return total; +} + +void G_RegisterScore(void) +{ + gclient_t *ranks[MAX_CLIENTS]; + gclient_t *c; + highscore_t *s; + int total; + int sec, score; + + total = G_CalcRanks(ranks); + if (!total) { + return; + } + + // grab our champion + c = ranks[0]; + + // calculate FPH + sec = (level.framenum - c->resp.enterframe) / HZ; + if (!sec) { + sec = 1; + } + score = c->resp.score * 3600 / sec; + + if (score < 1) { + return; // do not record bogus results + } + + if (level.numscores < MAX_HIGH_SCORES) { + s = &level.scores[level.numscores++]; + } else { + s = &level.scores[ level.numscores - 1 ]; + if (score < s->score) { + return; // result not impressive enough + } + } + + strcpy(s->name, c->pers.netname); + s->score = score; + time(&s->time); + + level.record = s->time; + + qsort(level.scores, level.numscores, sizeof(highscore_t), ScoreCmp); + + gi.dprintf("Added highscore entry for %s with %d FPH\n", + c->pers.netname, score); + + G_SaveScores(); +} + +void G_LoadScores(void) +{ + char *token; + const char *data; + highscore_t *s; + load_file_t *f; + int i; + + f = G_LoadFile("%s%s%s/%s.txt", GAMEVERSION, SD(g_highscores_dir->string), level.mapname); + if (!f) { + gi.dprintf("No high scores file loaded for %s\n", level.mapname); + return; + } + + for (i = 0; i < f->nb_lines && level.numscores < MAX_HIGH_SCORES; i++) { + data = f->lines[i]; + + if (data[0] == '#' || data[0] == '/') { + continue; + } + + token = COM_Parse(&data); + if (!*token) { + continue; + } + + s = &level.scores[level.numscores++]; + Q_strlcpy(s->name, token, sizeof(s->name)); + + token = COM_Parse(&data); + s->score = strtoul(token, NULL, 10); + + token = COM_Parse(&data); + s->time = strtoul(token, NULL, 10); + } + + qsort(level.scores, level.numscores, sizeof(highscore_t), ScoreCmp); + + gi.dprintf("Loaded %d scores from '%s'\n", level.numscores, f->path); + + G_FreeFile(f); +} + + +#if USE_AQTION #ifndef NO_BOTS /* diff --git a/src/client/client.h b/src/client/client.h index 54fb21919..f9314c756 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -1103,3 +1103,114 @@ void CL_GTV_Shutdown(void); // crc.c // byte COM_BlockSequenceCRCByte(byte *base, size_t length, int sequence); + +#if USE_DISCORD && USE_CURL && USE_AQTION +#include + +#if _MSC_VER >= 1920 && !__INTEL_COMPILER +#if defined( _WIN64 ) + #pragma comment(lib, "../extern/discord/lib/x86_64/discord_game_sdk.dll.lib") + #elif defined( _WIN32 ) + #pragma comment(lib, "../extern/discord/lib/x86/discord_game_sdk.dll.lib") + #endif // End 64/32 bit check +#elif _MSC_VER < 1920 // older MSC +// Push/Pop fix needed for older versions of Visual Studio to prevent unexpected crashes due to compile configurations + #pragma pack(push, 8) + #include "../extern/discord/c/discord_game_sdk.h" + #pragma pack(pop) +#endif // _MSC_VER >= 1920 && !__INTEL_COMPILER + +#define DISCORD_APP_ID 1002762540247433297 // Discord application ID (also referred to as "Client ID" is the game's unique identifier across Discord) +#define DISCORD_APP_TEXT "AQtion" // Tooltip name +#define DISCORD_APP_IMAGE "aqtion" // Rich presence -> art asset -> asset name +#define DISCORD_UPDATE_MSEC 1000 // Time between updates, 1000 = 1 second. +#define DISCORD_ACTIVITY_UPDATE_MSEC 15000 // Time between updates, 1000 = 1 second. + +cvar_t* cl_extern_ip; // External IP +cvar_t *cl_discord; // Allow the user to disable Discord features for privacy +cvar_t *cl_discord_id; // User ID +cvar_t *cl_discord_username; // User name +cvar_t *cl_discord_discriminator; // User's unique discrim ( username#discriminator -> bob#1900 ) +cvar_t *cl_discord_avatar; // Hash of the user's avatar +cvar_t *cl_discord_accept_join_requests; // If true automatically accept join request, else let the user decide + +enum discord_message_type { + DISCORD_MSG_NULL, // Null packet + DISCORD_MSG_PING, // Ping -> pong + DISCORD_MSG_CONNECT, // Client wants to connect to the game server + DISCORD_MSG_OWNERSHIP // Transfer of ownership +}; + +struct Application { + struct IDiscordCore* core; + struct IDiscordApplicationManager* application; + struct IDiscordUserManager* user; + struct IDiscordImageManager* images; + struct IDiscordActivityManager* activities; + struct IDiscordRelationshipManager* relationships; + struct IDiscordLobbyManager* lobbies; + struct IDiscordNetworkManager* network; + struct IDiscordOverlayManager* overlay; + struct IDiscordStorageManager* storage; + struct IDiscordStoreManager* store; + struct IDiscordVoiceManager* voice; + struct IDiscordAchievementManager* achievements; +}; + +typedef struct { + struct Application app; // Discord app + struct DiscordCreateParams params; // Creation parameters + + // Events + IDiscordCoreEvents* events; // Core events + struct IDiscordUserEvents users_events; // Users + struct IDiscordActivityEvents activities_events; // Activities + struct IDiscordRelationshipEvents relationships_events; // Relationships + struct IDiscordLobbyEvents lobbies_events; // Lobbies + struct IDiscordAchievementEvents achievements_events; // Achievements + struct IDiscordNetworkEvents* network_events; // Network + struct IDiscordOverlayEvents* overlay_events; // Overlay + IDiscordStorageEvents* storage_events; // Storage + struct IDiscordStoreEvents* store_events; // Store + struct IDiscordVoiceEvents* voice_events; // Voice + struct IDiscordAchievementEvents* achievement_events; // Achievements + + // Activities + struct DiscordActivity activity; // Activities (rich presence) + + // Lobbies + struct IDiscordLobbyTransaction *transaction; // Transaction + struct DiscordLobby lobby; // Lobby + + // User + struct DiscordUser user; // User data (the user's discord id, username, etc) + + // Init + qboolean init; // Discord is initialized true/false + qboolean discord_found; // If Discord is running + + // Callback + int result; + + // Timers + int last_discord_runtime; // Last time (in msec) discord was updated + int last_activity_time; // Last time (in msec) activity was updated + + char server_hostname[64]; // Cache hostname + char mapname[MAX_QPATH]; // Cache map + byte curr_players; // How many players currently connected to the server + byte prev_players; // How many players previously connected to the server + +} discord_t; +discord_t discord; + +void CL_InitDiscord(void); +void CL_CreateDiscordLobby_f(void); +void CL_DeleteDiscordLobby(void); +void CL_RunDiscord(void); +void CL_ShutdownDiscord(void); +void CL_RunDiscord(void); // Run in main loop +void CL_GetExternalIP(void); +void CL_DiscordParseServerStatus(serverStatus_t* status, const char* string); + +#endif \ No newline at end of file diff --git a/src/client/discord.c b/src/client/discord.c index 14d4097b2..cd7ba2eea 100644 --- a/src/client/discord.c +++ b/src/client/discord.c @@ -5,9 +5,8 @@ //===================================================================================================== //rekkie -- external ip -- s -#include - -cvar_t* cl_extern_ip; // External IP address +#include +#include "../extern/discord/c/discord_game_sdk.h" static size_t CurlWriteCallback(char* buf, size_t size, size_t nmemb, void* up) { @@ -78,111 +77,6 @@ static qboolean CL_IsPrivateNetwork(void) // // Extract the contents of the zip to /extern/discord/ -#include "../extern/discord/c/discord_game_sdk.h" - -#if _MSC_VER >= 1920 && !__INTEL_COMPILER -#if defined( _WIN64 ) - #pragma comment(lib, "../extern/discord/lib/x86_64/discord_game_sdk.dll.lib") - #elif defined( _WIN32 ) - #pragma comment(lib, "../extern/discord/lib/x86/discord_game_sdk.dll.lib") - #endif // End 64/32 bit check -#elif _MSC_VER < 1920 // older MSC -// Push/Pop fix needed for older versions of Visual Studio to prevent unexpected crashes due to compile configurations - #pragma pack(push, 8) - #include "../extern/discord/c/discord_game_sdk.h" - #pragma pack(pop) -#endif // _MSC_VER >= 1920 && !__INTEL_COMPILER - -#define DISCORD_APP_ID 1002762540247433297 // Discord application ID (also referred to as "Client ID" is the game's unique identifier across Discord) -#define DISCORD_APP_TEXT "AQtion" // Tooltip name -#define DISCORD_APP_IMAGE "aqtion" // Rich presence -> art asset -> asset name -#define DISCORD_UPDATE_MSEC 1000 // Time between updates, 1000 = 1 second. -#define DISCORD_ACTIVITY_UPDATE_MSEC 15000 // Time between updates, 1000 = 1 second. - -cvar_t *cl_discord; // Allow the user to disable Discord features for privacy -cvar_t *cl_discord_id; // User ID -cvar_t *cl_discord_username; // User name -cvar_t *cl_discord_discriminator; // User's unique discrim ( username#discriminator -> bob#1900 ) -cvar_t *cl_discord_avatar; // Hash of the user's avatar -cvar_t *cl_discord_accept_join_requests; // If true automatically accept join request, else let the user decide - -enum discord_message_type { - DISCORD_MSG_NULL, // Null packet - DISCORD_MSG_PING, // Ping -> pong - DISCORD_MSG_CONNECT, // Client wants to connect to the game server - DISCORD_MSG_OWNERSHIP // Transfer of ownership -}; - -struct Application { - struct IDiscordCore* core; - struct IDiscordApplicationManager* application; - struct IDiscordUserManager* user; - struct IDiscordImageManager* images; - struct IDiscordActivityManager* activities; - struct IDiscordRelationshipManager* relationships; - struct IDiscordLobbyManager* lobbies; - struct IDiscordNetworkManager* network; - struct IDiscordOverlayManager* overlay; - struct IDiscordStorageManager* storage; - struct IDiscordStoreManager* store; - struct IDiscordVoiceManager* voice; - struct IDiscordAchievementManager* achievements; -}; - -typedef struct { - struct Application app; // Discord app - struct DiscordCreateParams params; // Creation parameters - - // Events - IDiscordCoreEvents* events; // Core events - struct IDiscordUserEvents users_events; // Users - struct IDiscordActivityEvents activities_events; // Activities - struct IDiscordRelationshipEvents relationships_events; // Relationships - struct IDiscordLobbyEvents lobbies_events; // Lobbies - struct IDiscordAchievementEvents achievements_events; // Achievements - struct IDiscordNetworkEvents* network_events; // Network - struct IDiscordOverlayEvents* overlay_events; // Overlay - IDiscordStorageEvents* storage_events; // Storage - struct IDiscordStoreEvents* store_events; // Store - struct IDiscordVoiceEvents* voice_events; // Voice - struct IDiscordAchievementEvents* achievement_events; // Achievements - - // Activities - struct DiscordActivity activity; // Activities (rich presence) - - // Lobbies - struct IDiscordLobbyTransaction *transaction; // Transaction - struct DiscordLobby lobby; // Lobby - - // User - struct DiscordUser user; // User data (the user's discord id, username, etc) - - // Init - qboolean init; // Discord is initialized true/false - qboolean discord_found; // If Discord is running - - // Callback - int result; - - // Timers - int last_discord_runtime; // Last time (in msec) discord was updated - int last_activity_time; // Last time (in msec) activity was updated - - char server_hostname[64]; // Cache hostname - char mapname[MAX_QPATH]; // Cache map - byte curr_players; // How many players currently connected to the server - byte prev_players; // How many players previously connected to the server - -} discord_t; -discord_t discord; - -void CL_InitDiscord(void); -void CL_CreateDiscordLobby_f(void); -void CL_DeleteDiscordLobby(void); -void CL_RunDiscord(void); -void CL_ShutdownDiscord(void); - - static void DiscordCallback(void* data, enum EDiscordResult result) { //Com_Printf("%s %s\n", __func__, data); @@ -492,7 +386,7 @@ static void OnActivityJoinRequest(void* event_data, struct DiscordUser* user) } -static void CL_DiscordParseServerStatus(serverStatus_t* status, const char* string) +void CL_DiscordParseServerStatus(serverStatus_t* status, const char* string) { const char* s; size_t infolen; From 50ea934790658f9a81bd03445c6f316a8ac9341e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 28 Feb 2024 15:45:25 -0500 Subject: [PATCH 088/974] High scores based on game mode, disabling Discord sdk --- .github/workflows/build.yml | 2 +- src/action/g_cmds.c | 44 ++++++++++++++++++++--------- src/action/g_local.h | 3 ++ src/action/g_spawn.c | 11 +++++++- src/action/tng_stats.c | 55 +++++++++++++++++++++++++++---------- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b40dc4894..1f084fedf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ env: -Dvariable-fps=true -Dwerror=false -Daqtion-build=true - -Ddiscord-sdk=true + -Ddiscord-sdk=false MESON_ARGS_WIN: >- -Dsdl2=disabled diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index ee31abc8c..ad4e1fbe2 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1841,19 +1841,37 @@ void Cmd_HighScores_f(edict_t *ent) return; } - gi.cprintf(ent, PRINT_HIGH, - "\n" - " # Name FPH Date\n" - "-- --------------- ---- ----------------\n"); - for (i = 0; i < level.numscores; i++) { - s = &level.scores[i]; - - tm = localtime(&s->time); - if (!tm || !strftime(date, sizeof(date), "%Y-%m-%d %H:%M", tm)) - strcpy(date, "???"); - gi.cprintf(ent, PRINT_HIGH, "%2d %-15.15s %4d %s\n", - i + 1, s->name, s->score, date); - } + if (teamplay->value){ + gi.cprintf(ent, PRINT_HIGH, + "\n" + " # Name Score FPR Acc Date\n" + "-- --------------- ----- ---- --- -------------\n"); + for (i = 0; i < level.numscores; i++) { + s = &level.scores[i]; + + tm = localtime(&s->time); + if (!tm || !strftime(date, sizeof(date), "%Y-%m-%d %H:%M", tm)) + strcpy(date, "???"); + gi.cprintf(ent, PRINT_HIGH, "%2d %-15.15s %5d %5.1i %5.1f %2s\n", + i + 1, s->name, s->score, (int)s->fragsper, s->accuracy, date); + } + + } else { + gi.cprintf(ent, PRINT_HIGH, + "\n" + " # Name Score FPH Acc Date\n" + "-- --------------- ----- ---- --- -------------\n"); + + for (i = 0; i < level.numscores; i++) { + s = &level.scores[i]; + + tm = localtime(&s->time); + if (!tm || !strftime(date, sizeof(date), "%Y-%m-%d %H:%M", tm)) + strcpy(date, "???"); + gi.cprintf(ent, PRINT_HIGH, "%2d %-15.15s %5d %5.1i %5.1f %2s\n", + i + 1, s->name, s->score, (int)s->fragsper, s->accuracy, date); + } + } } #define CMDF_CHEAT 1 //Need cheat to be enabled diff --git a/src/action/g_local.h b/src/action/g_local.h index c19b44627..ba78e1966 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -655,6 +655,8 @@ typedef struct highscore_s { char name[MAX_CLIENT_NAME]; int score; + float fragsper; + float accuracy; time_t time; } highscore_t; @@ -774,6 +776,7 @@ typedef struct // High Scores support from OpenFFA char dir[MAX_OSPATH]; // where variable data is stored + char mode[MAX_QPATH]; // current game mode } game_locals_t; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index e40ba1dbb..e50a063d6 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -863,20 +863,28 @@ int Gamemode(void) int gamemode = 0; if (teamdm->value) { gamemode = GM_TEAMDM; + strcpy(game.mode, "teamdm"); } else if (ctf->value) { gamemode = GM_CTF; + strcpy(game.mode, "ctf"); } else if (use_tourney->value) { gamemode = GM_TOURNEY; + strcpy(game.mode, "tourney"); } else if (teamplay->value) { gamemode = GM_TEAMPLAY; + strcpy(game.mode, "teamplay"); } else if (dom->value) { gamemode = GM_DOMINATION; + strcpy(game.mode, "dom"); } else if (deathmatch->value) { gamemode = GM_DEATHMATCH; + strcpy(game.mode, "deathmatch"); } else if (esp->value && atl->value) { gamemode = GM_ASSASSINATE_THE_LEADER; + strcpy(game.mode, "atl"); } else if (esp->value && etv->value) { gamemode = GM_ESCORT_THE_VIP; + strcpy(game.mode, "etv"); } return gamemode; } @@ -1235,8 +1243,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.FreeTags(TAG_LEVEL); - // Set serverinfo correctly for gamemodeflags + // Set serverinfo correctly for gamemodeflags and game.mode Gamemodeflag(); + Gamemode(); #if USE_AQTION generate_uuid(); // Run this once every time a map loads to generate a unique id for stats (game.matchid) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 92599f8a4..ccdcea3ce 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -649,8 +649,8 @@ static void G_SaveScores(void) int i; size_t len; - len = Q_snprintf(path, sizeof(path), "%s%s%s/%s.txt", - GAMEVERSION, SD(g_highscores_dir->string), level.mapname); + len = Q_snprintf(path, sizeof(path), "%s%s%s/%s/%s.txt", + GAMEVERSION, SD(g_highscores_dir->string), game.mode, level.mapname); if (len >= sizeof(path)) { return; } @@ -665,8 +665,8 @@ static void G_SaveScores(void) for (i = 0; i < level.numscores; i++) { s = &level.scores[i]; - fprintf(fp, "\"%s\" %d %lu\n", - s->name, s->score, (unsigned long)s->time); + fprintf(fp, "\"%s\" %d %f %f %lu\n", + s->name, s->score, s->fragsper, s->accuracy, (unsigned long)s->time); } fclose(fp); @@ -711,24 +711,42 @@ void G_RegisterScore(void) { gclient_t *ranks[MAX_CLIENTS]; gclient_t *c; - highscore_t *s; + highscore_t *s; int total; - int sec, score; + int score; + int sec; + float accuracy, fragsper; total = G_CalcRanks(ranks); if (!total) { return; } + if (teamplay->value && game.roundNum > 0){ + return; // No rounds were played, so skip + } + // grab our champion c = ranks[0]; - // calculate FPH - sec = (level.framenum - c->resp.enterframe) / HZ; - if (!sec) { - sec = 1; - } - score = c->resp.score * 3600 / sec; + // Just straight up score + score = c->resp.score; + + // Calculate FPR, if mode is teamplay, else FPH + if (teamplay->value && game.roundNum > 0){ + fragsper = c->resp.score / game.roundNum; + } else { + sec = (level.framenum - c->resp.enterframe) / HZ; + if (!sec) + sec = 1; + fragsper = c->resp.score * 3600 / sec; + } + + int shots = min(c->resp.shotsTotal, 9999 ); + if (shots) + accuracy = (double)c->resp.hitsTotal * 100.0 / (double)c->resp.shotsTotal; + else + accuracy = 0; if (score < 1) { return; // do not record bogus results @@ -745,13 +763,15 @@ void G_RegisterScore(void) strcpy(s->name, c->pers.netname); s->score = score; + s->fragsper = fragsper; + s->accuracy = accuracy; time(&s->time); level.record = s->time; qsort(level.scores, level.numscores, sizeof(highscore_t), ScoreCmp); - gi.dprintf("Added highscore entry for %s with %d FPH\n", + gi.dprintf("Added highscore entry for %s with %d score\n", c->pers.netname, score); G_SaveScores(); @@ -765,7 +785,7 @@ void G_LoadScores(void) load_file_t *f; int i; - f = G_LoadFile("%s%s%s/%s.txt", GAMEVERSION, SD(g_highscores_dir->string), level.mapname); + f = G_LoadFile("%s%s%s/%s/%s.txt", GAMEVERSION, SD(g_highscores_dir->string), game.mode, level.mapname); if (!f) { gi.dprintf("No high scores file loaded for %s\n", level.mapname); return; @@ -789,6 +809,13 @@ void G_LoadScores(void) token = COM_Parse(&data); s->score = strtoul(token, NULL, 10); + token = COM_Parse(&data); + s->fragsper = strtoul(token, NULL, 10); // Load fragsper value + + token = COM_Parse(&data); + s->accuracy = strtoul(token, NULL, 10); // Load accuracy value + + token = COM_Parse(&data); s->time = strtoul(token, NULL, 10); } From d1ee05af7b5191b314751b1168372e582fccc14d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Mar 2024 12:52:11 -0500 Subject: [PATCH 089/974] Some script updates, bot name changes and GHUD weapon display fix --- bots/bingen.py | 79 ++++++ bots/bot_names.txt | 659 +++++++++++++++++++++++++++++++++++++++++++++ bots/botgen.py | 72 +++++ src/action/p_hud.c | 16 +- 4 files changed, 822 insertions(+), 4 deletions(-) create mode 100644 bots/bingen.py create mode 100644 bots/bot_names.txt create mode 100644 bots/botgen.py diff --git a/bots/bingen.py b/bots/bingen.py new file mode 100644 index 000000000..b3d534f89 --- /dev/null +++ b/bots/bingen.py @@ -0,0 +1,79 @@ +import json +import zlib +import struct +import os + +def load_all_clan_data(): + clans = {} + seen_clan_ids = {} + for filename in os.listdir('clans'): + if filename.endswith('.json'): + with open(f'clans/{filename}', 'r') as file: + clan = json.load(file) + if clan['clan_id'] in seen_clan_ids: + raise ValueError(f"Duplicate clan_id {clan['clan_id']} found in files {seen_clan_ids[clan['clan_id']]} and {filename}") + seen_clan_ids[clan['clan_id']] = filename + clans[clan['clan_id']] = clan + + return clans + +def write_bot_to_file(bot, clan, file, index_file, index): + # Add the clan tag to the bot name if a clan is provided + if clan: + if clan['pre_or_post'] == 0: + bot['name'] = clan['clan_tag'] + bot['name'] + else: + bot['name'] = bot['name'] + clan['clan_tag'] + + if len(bot['name']) > 16: + raise ValueError(f"Bot name {bot['name']} is too long: {len(bot['name'])} characters (max 16)") + + # Convert the bot dictionary to a JSON string + json_string = json.dumps(bot) + + # Convert the JSON string to bytes + json_bytes = json_string.encode('utf-8') + + # Compress the bytes + compressed_bytes = zlib.compress(json_bytes) + + # Get the current position in the file + offset = file.tell() + + # Write the length of the compressed data + file.write(struct.pack('I', len(compressed_bytes))) + + # Write the compressed data + file.write(compressed_bytes) + + # Write the bot's index, name, offset, and length to the index file + index_file.write(f'{index},{bot["name"]},{offset},{len(compressed_bytes)}\n') + +# Load all the clan data +clans = load_all_clan_data() + +# Open the binary file and the index file +with open('bots.bin', 'wb') as file, open('index.txt', 'w') as index_file: + index = 0 + # Load the bot data from multiple JSON files in bots_clan directory + for bot_filename in os.listdir('bots_clan'): + if bot_filename.endswith('.json'): + with open(f'bots_clan/{bot_filename}', 'r') as bot_file: + bot = json.load(bot_file) + + # Get the clan data for this bot + clan = clans.get(bot["clan_id"]) + + # Write the bot data to the binary file + write_bot_to_file(bot, clan, file, index_file, index) + index += 1 + + # Load the bot data from multiple JSON files in bots_pub directory + for bot_filename in os.listdir('bots_pub'): + if bot_filename.endswith('.json'): + with open(f'bots_pub/{bot_filename}', 'r') as bot_file: + bot = json.load(bot_file) + + # Write the bot data to the binary file without a clan + write_bot_to_file(bot, None, file, index_file, index) + index += 1 \ No newline at end of file diff --git a/bots/bot_names.txt b/bots/bot_names.txt new file mode 100644 index 000000000..bc58d4848 --- /dev/null +++ b/bots/bot_names.txt @@ -0,0 +1,659 @@ +John Carmack +John Romero +Michael Abrash +Sandy Petersen +Kevin Cloud +Adrian Carmack +American Mcgee +Tim Willits +Sonic Mayhem +Trent Reznor +NiN +Scourge of Armagon +Dissolution of Eternity +The Reckoning +Ground Zero +Enforcer +Gunner +Berserker +Iron Maiden +Gladiator +Medic +Technician +Icarus +Tank +Makron +Quad Machine +Cerberon +Strogg +Bitterman +Crash +Ranger +Phobos +Mynx +Orbb +Sarge +Grunt +Hossman +Daemia +Hunter +Angel +Gorre +Klesk +Slash +Wrack +Biker +Lucy +Patriot +Tank Jr +Anarki +Stripe +Razor +Keel +Visor +Uriel +Bones +Cadaver +Sorlag +Doom +Major +Xaero +Ratbot +HAL-9000 +Brandon Herrera +John Conner +Terminator +T-800 +T-1000 +Anakin Skywalker +Short Round +Barney Ross +The Boss +Dean Stevens +The Dean +Jake Malloy +Joe Tanto +Kit Latura +Robert Rath +Judge Joseph Dredd +Ray Quick +John Spartan +Gabe Walker +Angelo Provolone +Snaps +Lt Raymond Tango +Lincoln Hawk +Marion Cobretti +Cobra +John Rambo +Cpt Robert Hatch +Deke DaSilva +Cosmo Carboni +Johnny Kovac +Rocky Balboa +Elmore Caddo +Rocky +Machine Gun Joe +Jerry Savage +Stud +Trench +Gordy Brewer +Jericho Cane +Dr Victor Fries +John Kruger +The Eraser +Harry Tasker +Jack Slater +Capt. Ivan Danko +Dutch +Mark Kaminsky +Joseph P Brenner +John Matrix +Kalidor +Conan +HandsomeStranger +Muscleman +Joe Santo +Church +Dink Heimowitz +Mr Suave +Frank Moses +Harrison Hill +Col. Masterson +Mr Goodkat +Sonny Truelove +AK Waters +Jimmy Tudeski +The Tulip +Trey Kincaid +Major Deveraux +Harry Stamper +Korben Dallas +Muddy Grimes +Butch Coolidge +Det. Tom Hardy +Hudson Hawk +James Urbanski +Tom Mix +Lee Christmas +Jensen Ames +Terry Leather +Farmer +Chev Chelios +Handsome Rob +Monk +Evan Funsch +Jericho Butler +Turkish +Bacon +Jean Vilain +Samson Gaul +Tiano +Vincent Brazil +Frenchy +Jack Robideaux +Phillip Sauvage +Ben Archer +Jacques Kristoff +Rudy Cafmeyer +Charles Le Vaillaint +Replicant +Edward Garrotte +Alain Moreau +Mikhail Suverov +Darren McCord +William F. Guile +Chance Boudreaux +Luc Deveraux +Alex Wagner +Chad Wagner +Gibson Rickenbacker +Frank Dux +Ivan Kraschinsky +Booker +John Shepherd +Joshua McCord +Jake Fallon +McKenna +Jake Wilder +Frank Shatter +Cordell Walker +Matt Hunter +JJ McQuade +Lone Wolf +Sean Kane +John T. Booker +Chuck Slaughter +Colt +Gunner Jensen +Max Gatling +The German +Dr. Sage Mennox +Edward Genn +Icarus +Mike Riggins +Ryder +Xander Ronson +Brixos +Nikolai Cherenko +Lance Rockford +Sgt Frank Gannon +Sam Decker +Warchild +Major Frank Cross +Waxman +Michael Dane +Nick Gunar +Wellman A Santee +Det. Jack Caine +Lt. Rachenko +Ivan Drago +Venz +Tom Steele +Elijah Kane +Rogelio Torrez +Samuel Axel +Ruslan +Tao +Cock Puncher +Simon Ballister +Marshall Lawson +Jonathan Cold +Harlan Banks +Travis Hunter +William Lansing +Robert Burns +Sasha Petrosevitch +Frank Glass +Orin Boyd +Austin Travis +Forrest Taft +Casey Ryback +Gino Felino +Mason Storm +Hugh McKraken +Benjamin Benson +Freddie Wiseman +Dr. Snodgrass +General Skyler +Adam Beaudreaux +Billy Smith +Hurricane +Carl Brewster +Kyle Western +Jericho Jackson +Action Jackson +Cullen Monroe +Harry Braker +Sundog +Dreamer Tatum +Bateman Hooks +Apollo Creed +Jack Hopper +Yarbro +Hambone +Bad Sam +Clint Armstrong +Not Hasselhoff +Little G +JD Gold +Strom +Leland Duvall +Warden Pitt +Joe Joiner +Huck Finney +'Mac' Miller +Jack Saxon +Arklon +Jack Maxwell +Van Vandameer +Victor Lundgren +Alriss Ryder +Di Nardo +Carpenter +John Luger +Sam Striker +Alvin Regency +Wilson Mahood +Jimmy Jo Walker +Lieutenant Byrd +Stoney Cooper +Reddog +Ramrod +Kimbo Slice +Boy +Gung Ho +Mincer +Major Pain +Jonny Rambone +Dominator +Noogie +Cruton +Heljay +Mongrel +Private Brown +Brown Steel +Doctor Deathwish +Stainmaster B +Kippy +Gyngemaster2000 +Helmut +SGT CAPPA +Saboy +Chipee McSpray +Himdar +Vedro +Icexo +Tahlkora +Bolund +Ellanil +Galul +Mindelos +Ostmor +Sagel +Thalath +Dharmesh +Aegarond +Havo +Jarno +Leiruth +Edoli +Edwig +Faden +Skorgan +Walter +Fred +Morgan +Lawrence +Richard +Michael +Brad +George +Anton +Monty +Bean +Sean +Backfire +Halfwit +Halfbaked +Fullmonty +Nohope +Hitnrun +Missnrun +Oysterhead +Fullthrottle +NoAmmo +Bullseye +Aimless +Blackadder +Newbie +Cledus +Chimichanga +Razor +Tanz +Vic +Malin +Thor +Grog +Drago +Stinger +Tarnok +Wens +Brohn +Ivor +Gordac +Bortack +Morgriff +Kole +Garbok +Korv +Vokur +Gali +Beelz +CraigChrist +WhittlinMan +SpecialEd +Flint +Argo +Milius +Vera +Merki +Tarnen +Royen +Kaolin +Nandet +Nundak +Ryver +Mungri +Chan +Amadi +Zarna +PillowPants +ListerFiend +Ilikechocmilk +Andromeed +Cybr +Applee +uzayTron +[Corrus] +[Mee2] +GreZZer +Drillbit +Ant__Dog +Si +Tille-Teh-Black +itiXOID +Blythe +**Viander** +wysyyy +hozef-8 +ActioN-ReactioN +VeY +Dolin +Vanoss +Imentu +T_bon +cook did it +Panther.Killian +Chess +Dragoooon +Hammerhead +Bright GhOst +CowChop +Bob Ross +Nibbler +Private Ryan +GhOstBl4de +_*NiXXy*_ +_*PixxY*_ +D3c3ptiv3 Jo3 +Merry Christmas +Merry Yeetus +NinjaMantis +SteamPug +TheWay +Scary_Kitty +Mr.Badger +Balls Of Steel +_Gholem_ +_The Medic_ +VenoM +Broz +Himself +The Only +Way Of The +_GLiCH +Hecker +ParaDox +Two Four Eight +Bilbo T-Bagger +PewDiePie +Jinx +JohnShnowden +Heckin Heck +Why Me? +Why are we here? +Wheres Mark Hunt +Hell-oKitteh +Slice +BroFist +Bad JuJu +Return of Byeah +Po-Tay-Toe +Lemmie +Wreckage +Butter Jelly +tatu +Major Major +Digz +Figgz +Eggcecutioner +MoreEggcellent +C-Lion +attractiveCow +Grimm +D is for Digger! +Professor Jones +{itzChamp} +Sigil +Willitz** +Chickenn...! +Evo89 +John Romero +=WhiteBeard= +CitizenKane +chipp_ +^^ChoppY^^ +Dr.Jay +Fexel +FXguy +Teabag +Gigavoltz +gogman +GuN +JC Denton +deUSvauLT +exBEE +JCe +Joce +Marku +Pyro Yetti +SergeantBuck +2DCitizen +Amu +BVe +BigMack +doc +Doctor Doom +FUNKANIK +GreetingzGentlemen +Jarvis +kuri +Kor +Kermit D Frog +Jazz +MannY +Hellhound +ApeM +~Shadowfax~ +Socks +Spocket +Spar +Johnny Cage +Kano +Liu Kang +Raiden +Reptile +Scorpion +Shang Tsung +Sonya Blade +Sub-Zero +Baraka +Jade +Jax +Kintaro +Kitana +Kung Lao +Mileena +Noob Saibot +Shao Kahn +Smoke +Chameleon +Cyrax +Ermac +Kabal +Khameleon +Motaro +Nightwolf +Rain +Sektor +Sheeva +Sindel +Stryker +Fujin +Quan Chi +Sareena +Shinnok +Jarek +Kai +Meat +Reiko +Tanya +Tremor +Blaze +Bo' Rai Cho +Drahmin +Frost +Hsu Hao +Kenshi +Li Mei +Mavado +Mokap +Moloch +Nitara +Ashrah +Dairou +Darrius +Havik +Hotaru +Kira +Kobra +Onaga +Shujinko +Daegon +Taven +Dark Kahn +Skarlet +Cassie Cage +D'Vorah +Erron Black +Ferra/Torr +Jacqui Briggs +Kotal Kahn +Kung Jin +Takeda Takahashi +Triborg +Cetrion +Geras +Kollector +Kronika +Belokk +AqtionWoman +AqtionLLama +Bishibosh +Bonebreak +Coldcrow +Rakanishu +Treehead +Griswold +Countess +Pitspawn Fouldog +Flamespike +Boneash +Radament +Bloodwitch +Fangskin +Beetleburst +Leatherarm +Coldworm +Summoner +Kaa +Smithy +Witch Doctor +Stormtree +Sarina +Icehawk +Ismail +Geleb +Bremm +Toorc +Wyand +Endugu +Maffer +Death +Tormentor +Taintbreeder +Riftwraith +Infector of Souls +Lord De Seis +Vizier +Cow King +Corpsefire +Feature Creep +Siege +Barbarian +Axe Dweller +Bonesaw Breaker +Dac Farren +Megaflow Rectifier +Eyeback +Threash +Pindleskin +Snapchip Shatter +Anodized +Vinvear Molech +Tooth Sayer +Magma Torquer +Blaze Ripper +Frozenstein +Nihlathak \ No newline at end of file diff --git a/bots/botgen.py b/bots/botgen.py new file mode 100644 index 000000000..5d4715f6e --- /dev/null +++ b/bots/botgen.py @@ -0,0 +1,72 @@ +# Initialize lists +names_longer_than_16 = [] +names_exactly_16 = [] +names_less_than_10 = [] + +import json +import random +import os +import re + +# Function to generate a random value with a bias towards 0.5, 1 decimal place +def generate_random_value(): + return round(random.normalvariate(0.5, 0.2), 1) + +with open('skin_names.txt', 'r') as file: + skin_names = [line.strip() for line in file] + +# Open and read the file +with open('bot_names.txt', 'r') as file: + for line in file: + name = line.strip() # Remove newline characters and whitespace + length = len(name) + + # Check the length and create a bot profile if it's 10 characters or less + if length <= 10: + # Randomly select whether the bot should be added to a clan + add_to_clan = random.random() > 0.5 + + bot_profile = { + "name": name, + "clan_id": random.randint(1, 110) if add_to_clan else None, + "skin": random.choice(skin_names), # Randomly select a skin + "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, + "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, + "coreTraits": {key: generate_random_value() for key in ["aggressiveness", "teamwork", "curiosity", "aimingSkill", "reactionTime", "communicationFreq", "communicationTone", "movementStyle", "objectiveFocus"]} + } + + # Remove special characters from the filename + filename = re.sub('[^a-zA-Z0-9_]', '', name.lower().replace(' ', '')) + + # Write the bot profile to a new JSON file in the appropriate directory + directory = 'bots_clan' if add_to_clan else 'bots_pub' + filename = os.path.join(directory, filename + '.json') + with open(filename, 'w') as outfile: + json.dump(bot_profile, outfile, indent=4) + + # Check the length and create a bot profile if it's exactly 16 characters + # At 16 characters, the bot will not be added to a clan because they can't add a clan tag to their name + if length == 16: + bot_profile = { + "name": name, + "clan_id": None, + "skin": random.choice(skin_names), # Randomly select a skin + "weaponPreferences": {key: generate_random_value() for key in ["mk23", "dual_mk23", "mp5", "m4", "m3", "hc", "sniper", "knives", "grenades"]}, + "itemPreferences": {key: generate_random_value() for key in ["vest", "helm", "laser", "silencer", "slippers", "bandolier"]}, + "coreTraits": {key: generate_random_value() for key in ["aggressiveness", "teamwork", "curiosity", "aimingSkill", "reactionTime", "communicationFreq", "communicationTone", "movementStyle", "objectiveFocus"]} + } + + # Remove special characters from the filename + filename = re.sub('[^a-zA-Z0-9_]', '', name.lower().replace(' ', '')) + + # Write the bot profile to a new JSON file + filename = os.path.join('bots_pub', filename + '.json') + with open(filename, 'w') as outfile: + json.dump(bot_profile, outfile, indent=4) + + # Check the length and add to the appropriate list + if length > 16: + names_longer_than_16.append(name) + + if len(names_exactly_16) > 0: + print("Excluded bot names due to 16 character limit:", names_exactly_16) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index d383d288c..ccb45a9fd 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1004,9 +1004,13 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetText(clent, hud[h + 2], nm_s); Ghud_SetText(clent, hud[h + 3], kdr_s); - if (cl->curr_weap) + int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + + if (IS_ALIVE(cl_ent) && cl->curr_weap) Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); - else + else if (cl->resp.team == TEAM1 && weapNum) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); + else // no weapon, set to mk23 Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } @@ -1083,9 +1087,13 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetText(clent, hud[h + 2], nm_s); Ghud_SetText(clent, hud[h + 3], kdr_s); - if (cl->curr_weap) + int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + + if (IS_ALIVE(cl_ent) && cl->curr_weap) Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); - else + else if (cl->resp.team == TEAM1 && weapNum) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); + else // no weapon, set to mk23 Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } } From 41e814ac656283828a4a16c6d87e077b89b2c034 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Mar 2024 15:53:49 -0500 Subject: [PATCH 090/974] Reverted fancy award messages --- src/action/p_client.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/action/p_client.c b/src/action/p_client.c index 5d9fae2ba..59f69e961 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -357,23 +357,23 @@ void Announce_Reward(edict_t *ent, int rewardType) { switch (rewardType) { case IMPRESSIVE: - sprintf(buf,"-------------------\n" "IMPRESSIVE %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakKills/5); + sprintf(buf,"IMPRESSIVE %s (%dx)!", playername, ent->client->resp.streakKills/5); soundFile = "tng/impressive.wav"; break; case EXCELLENT: - sprintf(buf,"-------------------\n" "EXCELLENT %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakKills/12); + sprintf(buf,"EXCELLENT %s (%dx)!", playername, ent->client->resp.streakKills/12); soundFile = "tng/excellent.wav"; break; case ACCURACY: - sprintf(buf,"-------------------\n" "ACCURACY %s (%dx)!\n" "-------------------\n", playername, ent->client->resp.streakHS/3); + sprintf(buf,"ACCURACY %s (%dx)!", playername, ent->client->resp.streakHS/3); soundFile = "tng/accuracy.wav"; break; case DOMINATING: - sprintf(buf,"-------------------\n" "%s IS DOMINATING!\n" "-------------------\n", playername); + sprintf(buf,"%s IS DOMINATING!", playername); soundFile = "radio/male/deliv3.wav"; break; case UNSTOPPABLE: - sprintf(buf,"-------------------\n" "%s IS UNSTOPPABLE!\n" "-------------------\n", playername); + sprintf(buf,"%s IS UNSTOPPABLE!", playername); soundFile = "radio/male/deliv3.wav"; break; default: From 156356ba7ecc7cf6effed6174288817dfd1323e1 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 2 Mar 2024 10:18:46 -0500 Subject: [PATCH 091/974] esp_punishment_phase changes, revert award announcement messaging --- src/action/a_esp.c | 3 ++- src/action/p_client.c | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 453e7b748..506bf6c57 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -412,6 +412,7 @@ void EspCapturePointThink( edict_t *flag ) gi.sound( &g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("aqdt/aqg_bosswin.wav"), 1.0, ATTN_NONE, 0.0 ); espsettings.escortcap = flag->owner->client->resp.team; if (esp_punish->value) + esp_punishment_phase = true; EspPunishment(OtherTeam(flag->owner->client->resp.team)); if (use_rewards->value) { @@ -1826,6 +1827,7 @@ int EspReportLeaderDeath(edict_t *ent) // Find all players in the game and play this sound gi.sound(&g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("tng/leader_death.wav"), 1.0, ATTN_NONE, 0.0); if (esp_punish->value) + esp_punishment_phase = true; EspPunishment(dead_leader_team); // Stats Reset @@ -1855,7 +1857,6 @@ void MakeTeamInvulnerable(int winner, int uvtime) void EspPunishment(int teamNum) { - esp_punishment_phase = true; // Only perform team punishments if there's only 2 teams if (esp->value && teamCount == 2){ if(esp_punish->value == 1){ diff --git a/src/action/p_client.c b/src/action/p_client.c index 59f69e961..f61881635 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -356,18 +356,26 @@ void Announce_Reward(edict_t *ent, int rewardType) { char *playername = ent->client->pers.netname; switch (rewardType) { + case ACCURACY: + if (ent->client->resp.streakHS/3 >= 2) { + sprintf(buf,"ACCURACY %s (%dx)!", playername, ent->client->resp.streakHS/3); + } else { + sprintf(buf,"ACCURACY %s!", playername); + } + soundFile = "tng/accuracy.wav"; + break; case IMPRESSIVE: - sprintf(buf,"IMPRESSIVE %s (%dx)!", playername, ent->client->resp.streakKills/5); + if (ent->client->resp.streakKills/5 >= 2) { + sprintf(buf,"IMPRESSIVE %s (%dx)!", playername, ent->client->resp.streakKills/5); + } else { + sprintf(buf,"IMPRESSIVE %s!", playername); + } soundFile = "tng/impressive.wav"; break; case EXCELLENT: sprintf(buf,"EXCELLENT %s (%dx)!", playername, ent->client->resp.streakKills/12); soundFile = "tng/excellent.wav"; break; - case ACCURACY: - sprintf(buf,"ACCURACY %s (%dx)!", playername, ent->client->resp.streakHS/3); - soundFile = "tng/accuracy.wav"; - break; case DOMINATING: sprintf(buf,"%s IS DOMINATING!", playername); soundFile = "radio/male/deliv3.wav"; From 0259aaf99650eb3f3576ee21c4b5cb546e7727fe Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 2 Mar 2024 10:50:04 -0500 Subject: [PATCH 092/974] Set some constraints, highscores cleanup --- src/action/g_combat.c | 15 +++++++++------ src/action/g_local.h | 1 - src/action/g_main.c | 3 ++- src/action/g_spawn.c | 13 +++---------- src/action/tng_stats.c | 4 ++-- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 37b5e43d8..3ac6da3d3 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -152,12 +152,15 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) } // Allows grenades to destroy func_buttons if they have health (city radio room for example) - if ((0 == Q_stricmp("func_button", targ->classname)) && 0 == Q_stricmp("hgrenade", inflictor->classname)) { - PRETRACE (); - trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); - POSTTRACE(); - return true; - } + // in coop mode or training mode only (training maps for example) + if (coop->value || training_mode->value) { + if ((0 == Q_stricmp("func_button", targ->classname)) && 0 == Q_stricmp("hgrenade", inflictor->classname)) { + PRETRACE (); + trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); + POSTTRACE(); + return true; + } + } PRETRACE (); trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); diff --git a/src/action/g_local.h b/src/action/g_local.h index ba78e1966..a158b2ccf 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -776,7 +776,6 @@ typedef struct // High Scores support from OpenFFA char dir[MAX_OSPATH]; // where variable data is stored - char mode[MAX_QPATH]; // current game mode } game_locals_t; diff --git a/src/action/g_main.c b/src/action/g_main.c index 20e7679d2..c80da89aa 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -835,7 +835,8 @@ void EndDMLevel (void) IRC_printf (IRC_T_GAME, "Game ending at: %s", ltm); // High scores from OpenFFA - G_RegisterScore(); + if (!matchmode->value) // Non-disruptive matchmode constraint + G_RegisterScore(); // JBravo: Stop q2pro MVD2 recording if (use_mvd2->value) diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index e50a063d6..9bb7062f0 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -863,28 +863,20 @@ int Gamemode(void) int gamemode = 0; if (teamdm->value) { gamemode = GM_TEAMDM; - strcpy(game.mode, "teamdm"); } else if (ctf->value) { gamemode = GM_CTF; - strcpy(game.mode, "ctf"); } else if (use_tourney->value) { gamemode = GM_TOURNEY; - strcpy(game.mode, "tourney"); } else if (teamplay->value) { gamemode = GM_TEAMPLAY; - strcpy(game.mode, "teamplay"); } else if (dom->value) { gamemode = GM_DOMINATION; - strcpy(game.mode, "dom"); } else if (deathmatch->value) { gamemode = GM_DEATHMATCH; - strcpy(game.mode, "deathmatch"); } else if (esp->value && atl->value) { gamemode = GM_ASSASSINATE_THE_LEADER; - strcpy(game.mode, "atl"); } else if (esp->value && etv->value) { gamemode = GM_ESCORT_THE_VIP; - strcpy(game.mode, "etv"); } return gamemode; } @@ -1243,7 +1235,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.FreeTags(TAG_LEVEL); - // Set serverinfo correctly for gamemodeflags and game.mode + // Set serverinfo correctly for gamemodeflags Gamemodeflag(); Gamemode(); @@ -1381,7 +1373,8 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn G_LoadLocations(); // High score load file - G_LoadScores(); + if (!matchmode->value) // Non-disruptive matchmode constraint + G_LoadScores(); SVCmd_CheckSB_f(); //rekkie -- silence ban diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index ccdcea3ce..ffa963fed 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -650,7 +650,7 @@ static void G_SaveScores(void) size_t len; len = Q_snprintf(path, sizeof(path), "%s%s%s/%s/%s.txt", - GAMEVERSION, SD(g_highscores_dir->string), game.mode, level.mapname); + GAMEVERSION, SD(g_highscores_dir->string), gm->string, level.mapname); if (len >= sizeof(path)) { return; } @@ -785,7 +785,7 @@ void G_LoadScores(void) load_file_t *f; int i; - f = G_LoadFile("%s%s%s/%s/%s.txt", GAMEVERSION, SD(g_highscores_dir->string), game.mode, level.mapname); + f = G_LoadFile("%s%s%s/%s/%s.txt", GAMEVERSION, SD(g_highscores_dir->string), gm->string, level.mapname); if (!f) { gi.dprintf("No high scores file loaded for %s\n", level.mapname); return; From 76317149c5be7461891fa1641b20f85623ac1e4d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 3 Mar 2024 21:08:42 +0300 Subject: [PATCH 093/974] Fix possible UB in BSP_LoadSurfEdges(). --- src/common/bsp.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index cb5e2b862..9527a8389 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -335,21 +335,19 @@ LOAD(Edges) LOAD(SurfEdges) { msurfedge_t *out; - int i, vert; - int32_t index; + int i; + uint32_t index, vert; bsp->numsurfedges = count; bsp->surfedges = ALLOC(sizeof(*out) * count); out = bsp->surfedges; for (i = 0; i < count; i++, out++) { - index = (int32_t)BSP_Long(); + index = BSP_Long(); - vert = 0; - if (index < 0) { + vert = index >> 31; + if (vert) index = -index; - vert = 1; - } if (index >= bsp->numedges) { DEBUG("bad edgenum"); From 043d1015617b2674387e41bc7202c833031f7f6c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 4 Mar 2024 17:49:49 +0300 Subject: [PATCH 094/974] Update libpng to 1.6.43. --- subprojects/libpng.wrap | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index 7548f2e4d..ca8a32400 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = libpng-1.6.42 -source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.42.tar.xz -source_filename = libpng-1.6.42.tar.xz -source_hash = c919dbc11f4c03b05aba3f8884d8eb7adfe3572ad228af972bb60057bdb48450 -patch_filename = libpng_1.6.42-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.42-1/get_patch -patch_hash = b5c709da83556da3e970550d1b5bd2286f44da88db34be8215d17765bcbb356a -wrapdb_version = 1.6.42-1 +directory = libpng-1.6.43 +source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz +source_filename = libpng-1.6.43.tar.xz +source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c +patch_filename = libpng_1.6.43-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-1/get_patch +patch_hash = 0e995446c607ef2e618fb561929acf91e4bdd8017d2e18a7a3b68ba41da345e6 +wrapdb_version = 1.6.43-1 [provide] libpng = libpng_dep From 5187b856ef61bd2120810c4c9ccf49733a4bcc88 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 4 Mar 2024 22:51:08 -0500 Subject: [PATCH 095/974] Allows pin-pulling of grenade during LCA --- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 2 ++ src/action/p_weapon.c | 22 ++++++++++++---------- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index a158b2ccf..2e5610ac9 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1289,6 +1289,7 @@ extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command extern cvar_t *warmup_unready; extern cvar_t *training_mode; // Sets training mode vars extern cvar_t *g_highscores_dir; // Sets the highscores directory +extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index c80da89aa..b711a78d2 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -533,6 +533,7 @@ cvar_t *sv_killgib; // Gibs on 'kill' command cvar_t *warmup_unready; // Toggles warmup if captains unready cvar_t *training_mode; // Sets training mode vars cvar_t *g_highscores_dir; // Sets the highscores directory +cvar_t *lca_grenade; // Allows grenade pin pulling during LCA #if AQTION_EXTENSION cvar_t *use_newirvision; diff --git a/src/action/g_save.c b/src/action/g_save.c index 10696c6a0..3444d6bc9 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -652,6 +652,8 @@ void InitGame( void ) gi.cvar_forceset("bholelimit", "30"); } g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); + lca_grenade = gi.cvar("lca_grenade", "0", 0); + // new AQtion Extension cvars #if AQTION_EXTENSION diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 698a08d91..183e4fa53 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -3782,10 +3782,8 @@ void Weapon_Gas(edict_t* ent) { if (((ent->client->latched_buttons | ent->client->buttons) & BUTTON_ATTACK) && (ent->solid != SOLID_NOT || ent->deadflag == DEAD_DEAD) && - !lights_camera_action && !ent->client->uvTime) + !ent->client->uvTime && (lca_grenade->value || !lights_camera_action)) { - - if (ent->client->ps.gunframe <= GRENADE_PINIDLE_LAST && ent->client->ps.gunframe >= GRENADE_PINIDLE_FIRST) { @@ -3808,14 +3806,18 @@ void Weapon_Gas(edict_t* ent) if (ent->client->ps.gunframe >= GRENADE_IDLE_FIRST && ent->client->ps.gunframe <= GRENADE_IDLE_LAST) { - ent->client->ps.gunframe = GRENADE_THROW_FIRST; - if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) - SetAnimation( ent, FRAME_crattak1 - 1, FRAME_crattak9, ANIM_ATTACK ); - else - SetAnimation( ent, FRAME_attack1 - 1, FRAME_attack8, ANIM_ATTACK ); - ent->client->weaponstate = WEAPON_FIRING; + // Only allow the player to throw the grenade if lights_camera_action is 0 + // This is so we can enable lca_grenade + if (!lights_camera_action) + { + ent->client->ps.gunframe = GRENADE_THROW_FIRST; + if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) + SetAnimation( ent, FRAME_crattak1 - 1, FRAME_crattak9, ANIM_ATTACK ); + else + SetAnimation( ent, FRAME_attack1 - 1, FRAME_attack8, ANIM_ATTACK ); + ent->client->weaponstate = WEAPON_FIRING; + } return; - } if (ent->client->ps.gunframe == GRENADE_PINIDLE_LAST) From 372e2503fe47cfef16ee22209687ffd55edbd933 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 5 Mar 2024 00:18:03 +0300 Subject: [PATCH 096/974] Fix crashes due to G_PickTarget() returning NULL. --- src/game/g_turret.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/game/g_turret.c b/src/game/g_turret.c index 49aa226fb..015741296 100644 --- a/src/game/g_turret.c +++ b/src/game/g_turret.c @@ -197,8 +197,10 @@ void turret_breach_finish_init(edict_t *self) gi.dprintf("%s at %s needs a target\n", self->classname, vtos(self->s.origin)); } else { self->target_ent = G_PickTarget(self->target); - VectorSubtract(self->target_ent->s.origin, self->s.origin, self->move_origin); - G_FreeEdict(self->target_ent); + if (self->target_ent) { + VectorSubtract(self->target_ent->s.origin, self->s.origin, self->move_origin); + G_FreeEdict(self->target_ent); + } } self->teammaster->dmg = self->dmg; @@ -340,6 +342,10 @@ void turret_driver_link(edict_t *self) self->nextthink = level.framenum + 1; self->target_ent = G_PickTarget(self->target); + if (!self->target_ent) { + G_FreeEdict(self); + return; + } self->target_ent->owner = self; self->target_ent->teammaster->owner = self; VectorCopy(self->target_ent->s.angles, self->s.angles); From ed165812c20575e7ae0f05616397e5f45bc0a932 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 5 Mar 2024 22:21:33 +0300 Subject: [PATCH 097/974] Avoid strcpy() in pack_alloc(). --- src/common/files.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index 9e57ebf24..59e43924f 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -2035,8 +2035,10 @@ static pack_t *pack_alloc(FILE *fp, filetype_t type, const char *name, unsigned num_files, size_t names_len) { pack_t *pack; + size_t len; - pack = FS_Malloc(sizeof(*pack) + strlen(name)); + len = strlen(name); + pack = FS_Malloc(sizeof(*pack) + len); pack->type = type; pack->refcount = 0; pack->fp = fp; @@ -2045,7 +2047,7 @@ static pack_t *pack_alloc(FILE *fp, filetype_t type, const char *name, pack->hash_size = 0; pack->file_hash = NULL; pack->names = FS_Malloc(names_len); - strcpy(pack->filename, name); + memcpy(pack->filename, name, len + 1); return pack; } From bd3b8d8a7821a4b9e2e92f9c27fd66516cbb57f5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 7 Mar 2024 19:03:40 +0300 Subject: [PATCH 098/974] Get rid of sb_nums[] array. --- src/client/screen.c | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index ab098e006..bd1fe56aa 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -98,17 +98,6 @@ static cvar_t *ch_y; vrect_t scr_vrect; // position of render window on screen -static const char *const sb_nums[2][STAT_PICS] = { - { - "num_0", "num_1", "num_2", "num_3", "num_4", "num_5", - "num_6", "num_7", "num_8", "num_9", "num_minus" - }, - { - "anum_0", "anum_1", "anum_2", "anum_3", "anum_4", "anum_5", - "anum_6", "anum_7", "anum_8", "anum_9", "anum_minus" - } -}; - const uint32_t colorTable[8] = { U32_BLACK, U32_RED, U32_GREEN, U32_YELLOW, U32_BLUE, U32_CYAN, U32_MAGENTA, U32_WHITE @@ -1255,11 +1244,15 @@ SCR_RegisterMedia */ void SCR_RegisterMedia(void) { - int i, j; + int i; + + for (i = 0; i < STAT_MINUS; i++) + scr.sb_pics[0][i] = R_RegisterPic(va("num_%d", i)); + scr.sb_pics[0][i] = R_RegisterPic("num_minus"); - for (i = 0; i < 2; i++) - for (j = 0; j < STAT_PICS; j++) - scr.sb_pics[i][j] = R_RegisterPic(sb_nums[i][j]); + for (i = 0; i < STAT_MINUS; i++) + scr.sb_pics[1][i] = R_RegisterPic(va("anum_%d", i)); + scr.sb_pics[1][i] = R_RegisterPic("anum_minus"); scr.inven_pic = R_RegisterPic("inventory"); scr.field_pic = R_RegisterPic("field_3"); From bff9c4c73aa2dcf3a9bc268ba731090954500f39 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 10 Mar 2024 17:04:55 +0300 Subject: [PATCH 099/974] Simplify setup_celshading(). --- src/refresh/mesh.c | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 43c3eddc3..2b47810d1 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -402,23 +402,15 @@ static void setup_celshading(void) { float value = Cvar_ClampValue(gl_celshading, 0, 10); - celscale = 0; - - if (value == 0) - return; - - if (glr.ent->flags & (RF_TRANSLUCENT | RF_SHELL_MASK)) - return; - - if (!qglPolygonMode) - return; - - celscale = 1.0f - Distance(origin, glr.fd.vieworg) / 700.0f; + if (value == 0 || (glr.ent->flags & (RF_TRANSLUCENT | RF_SHELL_MASK)) || !qglPolygonMode) + celscale = 0; + else + celscale = 1.0f - Distance(origin, glr.fd.vieworg) / 700.0f; } static void draw_celshading(const QGL_INDEX_TYPE *indices, int num_indices) { - if (celscale < 0.01f || celscale > 1) + if (celscale < 0.01f) return; GL_BindTexture(0, TEXNUM_BLACK); From cde0289333b5e1cb1b238571d385c9351bb9c86c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 10 Mar 2024 17:05:21 +0300 Subject: [PATCH 100/974] Expand comment. --- src/refresh/mesh.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 2b47810d1..285be33f6 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -552,7 +552,8 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, // fall back to entity matrix GL_LoadMatrix(glr.entmatrix); - // avoid drawing hidden faces for transparent gun + // avoid drawing hidden faces for transparent gun by pre-filling depth buffer + // muzzle flashes are excluded by checking for RF_FULLBRIGHT bit if ((glr.ent->flags & (RF_TRANSLUCENT | RF_WEAPONMODEL | RF_FULLBRIGHT)) == (RF_TRANSLUCENT | RF_WEAPONMODEL)) { GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX); From e30a373938c7b6cb318e65d1e26caa1cbd1c5922 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 12 Mar 2024 13:35:41 -0400 Subject: [PATCH 101/974] De-nested EspReportLeaderDeath a bit, added brace guards --- src/action/a_esp.c | 56 +++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 506bf6c57..d7192d9c6 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -411,9 +411,10 @@ void EspCapturePointThink( edict_t *flag ) // Escort point captured, end round and start again gi.sound( &g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("aqdt/aqg_bosswin.wav"), 1.0, ATTN_NONE, 0.0 ); espsettings.escortcap = flag->owner->client->resp.team; - if (esp_punish->value) + if (esp_punish->value) { esp_punishment_phase = true; EspPunishment(OtherTeam(flag->owner->client->resp.team)); + } if (use_rewards->value) { if(teams[TEAM1].leader->client->resp.esp_capstreak == 5) @@ -1774,6 +1775,37 @@ void EspLeaderLeftTeam( edict_t *ent ) } } +/* +Called from EspReportLeaderDeath in ATL mode +*/ +static int EspReportATLWinner(int dead_leader_team) +{ + int winner = 0; + + switch (dead_leader_team) { + case TEAM1: + if (IS_ALIVE(teams[TEAM2].leader) && !IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM2; + else if (!IS_ALIVE(teams[TEAM2].leader) && IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM3; + break; + case TEAM2: + if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM1; + else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM3].leader)) + winner = TEAM3; + break; + case TEAM3: + if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM2].leader)) + winner = TEAM1; + else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM2].leader)) + winner = TEAM2; + break; + } + + return winner; +} + /* This is called from player_die, and only called if the player was a leader @@ -1803,32 +1835,16 @@ int EspReportLeaderDeath(edict_t *ent) winner = TEAM1; // 3 team winner checks } else { - if (dead_leader_team == TEAM1) { - if (IS_ALIVE(teams[TEAM2].leader) && !IS_ALIVE(teams[TEAM3].leader)) - winner = TEAM2; - else if (!IS_ALIVE(teams[TEAM2].leader) && IS_ALIVE(teams[TEAM3].leader)) - winner = TEAM3; - } - else if (dead_leader_team == TEAM2) { - if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM3].leader)) - winner = TEAM1; - else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM3].leader)) - winner = TEAM3; - } - else if (dead_leader_team == TEAM3) { - if (IS_ALIVE(teams[TEAM1].leader) && !IS_ALIVE(teams[TEAM2].leader)) - winner = TEAM1; - else if (!IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM2].leader)) - winner = TEAM2; - } + winner = EspReportATLWinner(dead_leader_team); } } // Find all players in the game and play this sound gi.sound(&g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("tng/leader_death.wav"), 1.0, ATTN_NONE, 0.0); - if (esp_punish->value) + if (esp_punish->value) { esp_punishment_phase = true; EspPunishment(dead_leader_team); + } // Stats Reset if (ent->client->resp.esp_capstreak > ent->client->resp.esp_capstreakbest) From be2d58718a2f82e1b21d4863bc52457a86ef9435 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 10 Mar 2024 22:12:18 +0300 Subject: [PATCH 102/974] Const-ify AddMessage() data argument. --- src/server/mvd.c | 11 +++++++---- src/server/send.c | 10 ++++------ src/server/server.h | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index 0e5c6667f..a61dbf617 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -270,10 +270,10 @@ static void dummy_exec_string(cmdbuf_t *buf, const char *line) dummy_command(); } -static void dummy_add_message(client_t *client, byte *data, +static void dummy_add_message(client_t *client, const byte *data, size_t length, bool reliable) { - char *text; + char text[MAX_STRING_CHARS]; if (!length || !reliable || data[0] != svc_stufftext) { return; // not interesting @@ -283,8 +283,11 @@ static void dummy_add_message(client_t *client, byte *data, return; // not allowed } - data[length] = 0; - text = (char *)(data + 1); + // truncate at MAX_STRING_CHARS + length = min(length, sizeof(text)); + memcpy(text, data + 1, length - 1); + text[length - 1] = 0; + Com_DPrintf("dummy stufftext: %s\n", Com_MakePrintable(text)); Cbuf_AddText(&dummy_buffer, text); } diff --git a/src/server/send.c b/src/server/send.c index 6396193be..14f7985e0 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -458,10 +458,8 @@ static void free_all_messages(client_t *client) client->msg_dynamic_bytes = 0; } -static void add_msg_packet(client_t *client, - byte *data, - size_t len, - bool reliable) +static void add_msg_packet(client_t *client, const byte *data, + size_t len, bool reliable) { message_packet_t *msg; @@ -605,7 +603,7 @@ FRAME UPDATES - OLD NETCHAN =============================================================================== */ -static void add_message_old(client_t *client, byte *data, +static void add_message_old(client_t *client, const byte *data, size_t len, bool reliable) { if (len > client->netchan.maxpacketlen) { @@ -798,7 +796,7 @@ FRAME UPDATES - NEW NETCHAN =============================================================================== */ -static void add_message_new(client_t *client, byte *data, +static void add_message_new(client_t *client, const byte *data, size_t len, bool reliable) { if (reliable) { diff --git a/src/server/server.h b/src/server/server.h index 14e838475..240aeec56 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -360,7 +360,7 @@ typedef struct client_s { int maxclients; // netchan type dependent methods - void (*AddMessage)(struct client_s *, byte *, size_t, bool); + void (*AddMessage)(struct client_s *, const byte *, size_t, bool); void (*WriteFrame)(struct client_s *); void (*WriteDatagram)(struct client_s *); From 0fe48e7326922fc6644c96d15111fbb7378e9097 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 14 Mar 2024 22:09:13 +0300 Subject: [PATCH 103/974] Use common function to check image size. --- src/refresh/images.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 8af294485..0efd38ef0 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -49,6 +49,11 @@ with this program; if not, write to the Free Software Foundation, Inc., static int IMG_Load##x(byte *rawdata, size_t rawlen, \ image_t *image, byte **pic) +static bool check_image_size(unsigned w, unsigned h) +{ + return (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE); +} + /* ==================================================================== @@ -234,7 +239,7 @@ static int load_pcx(byte *rawdata, size_t rawlen, byte **pixels_p, w = (LittleShort(pcx->xmax) - LittleShort(pcx->xmin)) + 1; h = (LittleShort(pcx->ymax) - LittleShort(pcx->ymin)) + 1; - if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { + if (check_image_size(w, h)) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -326,7 +331,7 @@ IMG_LOAD(WAL) w = LittleLong(mt->width); h = LittleLong(mt->height); - if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { + if (check_image_size(w, h)) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -488,7 +493,7 @@ IMG_LOAD(TGA) return Q_ERR_INVALID_FORMAT; } - if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { + if (check_image_size(w, h)) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -667,8 +672,7 @@ static int my_jpeg_start_decompress(j_decompress_ptr cinfo, byte *rawdata, size_ return Q_ERR_INVALID_FORMAT; } - if (cinfo->output_width < 1 || cinfo->output_width > MAX_TEXTURE_SIZE || - cinfo->output_height < 1 || cinfo->output_height > MAX_TEXTURE_SIZE) { + if (check_image_size(cinfo->output_width, cinfo->output_height)) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -853,7 +857,7 @@ static int my_png_read_header(png_structp png_ptr, png_infop info_ptr, return Q_ERR_FAILURE; } - if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { + if (check_image_size(w, h)) { Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -1605,7 +1609,7 @@ static void get_image_dimensions(imageformat_t fmt, image_t *image) FS_CloseFile(f); - if (w < 1 || h < 1 || w > MAX_TEXTURE_SIZE || h > MAX_TEXTURE_SIZE) { + if (check_image_size(w, h)) { return; } From bf647afb3e6da0a6d30d32457870bb4cfaf57d89 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 14 Mar 2024 18:10:07 -0400 Subject: [PATCH 104/974] Added messaging when you hit someone with a grenade splash --- src/action/g_combat.c | 35 +++++++++++++++++++++++++++++++++-- src/action/p_client.c | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 3ac6da3d3..fcd86eb5f 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -925,8 +925,12 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; } - // Grenade splash, kicks and punch damage - if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH))) { + // Grenade splash, grenade impact, kicks and punch damage + if (mod > 0 && + ((mod == MOD_HG_SPLASH) || + (mod == MOD_KICK) || + (mod == MOD_PUNCH) || + (mod == MOD_GRENADE_IMPACT))) { attacker->client->resp.gunstats[mod].damage += damage; } } @@ -954,6 +958,10 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, edict_t *ent = NULL; vec3_t v; vec3_t dir; + char ent_name_list[1024] = ""; // Buffer to hold the names + int ent_count = 0; // Counter for the number of entities + qboolean selfharm = false; + while ((ent = findradius (ent, inflictor->s.origin, radius)) != NULL) { @@ -961,6 +969,19 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, continue; if (!ent->takedamage) continue; + if (ent == attacker) + selfharm = true; + + // Messaging addition + if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker) + { + if (ent_count > 0) + strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); + strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); + ent_count++; + } + + // End messaging addition VectorAdd (ent->mins, ent->maxs, v); VectorMA (ent->s.origin, 0.5, v, v); @@ -992,4 +1013,14 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, } } } + + // Messaging addition + if (ent_count > 3) + gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade.\n"); + else if (selfharm && ent_count > 0) + gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, along with %s\n", ent_name_list); + else if (selfharm) + gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade.\n"); + else if (ent_count > 0) + gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); } diff --git a/src/action/p_client.c b/src/action/p_client.c index f61881635..ba84f889d 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -411,7 +411,7 @@ void Add_Frag(edict_t * ent, int mod) ent->client->resp.gunstats[mod].kills++; } // Grenade splash, kicks and punch damage - if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH))) { + if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH)) || (mod = MOD_GRENADE_IMPACT)) { ent->client->resp.gunstats[mod].kills++; } From c1094443c9423f4bc08c9754d3e4bff4c3cf3a0b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 14 Mar 2024 18:17:04 -0400 Subject: [PATCH 105/974] Set a check to ensure we don't go over 1024 chars in the message buffer --- src/action/g_combat.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index fcd86eb5f..6218266d9 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -973,13 +973,19 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, selfharm = true; // Messaging addition - if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker) - { - if (ent_count > 0) - strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); - strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); - ent_count++; - } + if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker){ + // Calculate the length of the new string + int new_length = strlen(ent_name_list) + strlen(" and ") + strlen(ent->client->pers.netname); + + // Check if the new string would fit in ent_name_list + if (new_length < sizeof(ent_name_list)) + { + if (ent_count > 0) + strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); + strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); + ent_count++; + } + } // End messaging addition From 5497ca7089720586fded679562dbf852ec7b413c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 14 Mar 2024 18:25:44 -0400 Subject: [PATCH 106/974] Set a check to ensure we don't go over 1024 chars in the message buffer --- src/action/g_combat.c | 8 ++++++-- src/action/g_local.h | 1 + src/action/p_client.c | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 6218266d9..f0108945d 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1022,11 +1022,15 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, // Messaging addition if (ent_count > 3) - gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade.\n"); + gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade, nicely done!\n"); else if (selfharm && ent_count > 0) gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, along with %s\n", ent_name_list); else if (selfharm) - gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade.\n"); + gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); else if (ent_count > 0) gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); + + // Stats for fun, tracks the highest amount of players hit by a single grenade + if (ent_count > attacker->client->resp.grenSplash) + attacker->client->resp.grenSplash = ent_count; } diff --git a/src/action/g_local.h b/src/action/g_local.h index a158b2ccf..894f3e0dc 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1835,6 +1835,7 @@ typedef struct int streakHS; //Headshots in a Row int streakKillsHighest; //Highest kills in a row int streakHSHighest; //Highest headshots in a Row + int grenSplash; //Tracks latest amount of players harmed by a single grenade int hitsLocations[LOC_MAX]; //Number of hits for different locations gunStats_t gunstats[MOD_TOTAL]; //Number of shots/hits for different guns, adjusted to MOD_TOTAL to allow grenade, kick and punch stats diff --git a/src/action/p_client.c b/src/action/p_client.c index ba84f889d..deaf92dbe 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -411,7 +411,7 @@ void Add_Frag(edict_t * ent, int mod) ent->client->resp.gunstats[mod].kills++; } // Grenade splash, kicks and punch damage - if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH)) || (mod = MOD_GRENADE_IMPACT)) { + if (mod > 0 && ((mod == MOD_HG_SPLASH) || (mod == MOD_KICK) || (mod == MOD_PUNCH) || (mod = MOD_GRENADE_IMPACT))) { ent->client->resp.gunstats[mod].kills++; } From 5028f8a2c5dc3e935dfa0781c48e90fafcbcab8b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 14 Mar 2024 18:28:34 -0400 Subject: [PATCH 107/974] Minor remessaging --- src/action/g_combat.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index f0108945d..89c3087ea 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1028,7 +1028,7 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, else if (selfharm) gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); else if (ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); + gi.cprintf(attacker, PRINT_HIGH, "%s were blasted by your grenade.\n", ent_name_list); // Stats for fun, tracks the highest amount of players hit by a single grenade if (ent_count > attacker->client->resp.grenSplash) From c5d1207e001ba35ee5fd77681015c57d038eec03 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 15 Mar 2024 00:12:18 -0400 Subject: [PATCH 108/974] Trying to limit the loop to just 4 players --- src/action/g_combat.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 89c3087ea..e9ef01210 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -973,19 +973,23 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, selfharm = true; // Messaging addition - if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker){ - // Calculate the length of the new string - int new_length = strlen(ent_name_list) + strlen(" and ") + strlen(ent->client->pers.netname); - - // Check if the new string would fit in ent_name_list - if (new_length < sizeof(ent_name_list)) - { - if (ent_count > 0) - strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); - strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); - ent_count++; - } - } + if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker){ + // Only add the name to the list if there are less than 4 names + if (ent_count < 4) + { + // Calculate the length of the new string + int new_length = strlen(ent_name_list) + strlen(" and ") + strlen(ent->client->pers.netname); + + // Check if the new string would fit in ent_name_list + if (new_length < sizeof(ent_name_list)) + { + if (ent_count > 0) + strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); + strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); + } + } + ent_count++; + } // End messaging addition From ec25ddaff69123c0653397528f5c77de268eecb6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 19 Mar 2024 12:51:20 -0400 Subject: [PATCH 109/974] Adjusted dis/en/ablecvar messaging --- src/action/g_utils.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/action/g_utils.c b/src/action/g_utils.c index 76b7344b6..c82ded617 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -733,7 +733,9 @@ void disablecvar(cvar_t *cvar, char *msg) return; if (msg) - gi.dprintf("%s: disabling %s\n", msg, cvar->name); + gi.dprintf("DISABLING %s: %s\n", cvar->name, msg); + else + gi.dprintf("DISABLING %s\n", cvar->name); gi.cvar_forceset(cvar->name, "0"); } @@ -745,7 +747,9 @@ void enablecvar(cvar_t *cvar, char *msg) return; if (msg) - gi.dprintf("%s: enabling %s\n", msg, cvar->name); + gi.dprintf("ENABLING %s: %s\n", cvar->name, msg); + else + gi.dprintf("ENABLING %s\n", cvar->name); gi.cvar_forceset(cvar->name, "1"); } From b1a31bc29db2d75e8f1060768fc356b41b0b5c55 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 19 Mar 2024 17:16:44 -0400 Subject: [PATCH 110/974] Re-added ui_colorpingmax --- src/client/ui/servers.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index 246165641..551daec3a 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -192,6 +192,8 @@ static serverslot_t *FindSlot(const netadr_t *search, int *index_p) static uint32_t ColorForStatus(const serverStatus_t *status, unsigned ping) { + ui_colorpingmax = Cvar_Get("ui_colorpingmax", "50", 0); + if (Q_atoi(Info_ValueForKey(status->infostring, "needpass")) >= 1) return uis.color.disabled.u32; From e179cb63dfe00216f81a69c46e2911ffd7cb5e13 Mon Sep 17 00:00:00 2001 From: ReKTeK Date: Thu, 21 Mar 2024 13:00:13 +0800 Subject: [PATCH 111/974] MSVC Support: Direct Com_Printf() output to MSVC debug window --- AQtion/AQtion.vcxproj.user | 1 + src/common/common.c | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/AQtion/AQtion.vcxproj.user b/AQtion/AQtion.vcxproj.user index d0cceeba0..7c4fab190 100644 --- a/AQtion/AQtion.vcxproj.user +++ b/AQtion/AQtion.vcxproj.user @@ -3,6 +3,7 @@ AQtion\AQtion\q2pro.exe WindowsLocalDebugger + false AQtion\AQtion\q2pro.exe diff --git a/src/common/common.c b/src/common/common.c index 31c20d447..0099d5456 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -458,6 +458,13 @@ void Com_LPrintf(print_type_t type, const char *fmt, ...) // debugging console console_write(type, msg); + //rekkie -- MSVC Support -- s + #if USE_DEBUG && _WIN32 && _MSC_VER >= 1920 && !__INTEL_COMPILER + #include + OutputDebugStringA(msg); // Force printing to MSVC debug output window + #endif + //rekkie -- MSVC Support -- e + // remote console //SV_ConsoleOutput(msg); From 3d448450588cc2733348f996ea203030cec688ae Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 26 Mar 2024 13:54:55 -0400 Subject: [PATCH 112/974] Using a safer newrand --- src/action/a_team.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 85cad652a..ece2e6714 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -749,7 +749,7 @@ void SelectKit3(edict_t *ent, pmenu_t *p) // newrand returns n, where 0 >= n < top int newrand (int top) { - return (int) (random () * top); + return rand() % top; } void SelectRandomWeapon(edict_t *ent, pmenu_t *p) From 8c0a1a0b95e8599583fc47fd2d9c4e3feb4e4bca Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 27 Mar 2024 20:55:35 +0300 Subject: [PATCH 113/974] Update libcurl to 8.7.1. --- subprojects/libcurl.wrap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index eedb518b3..4cb52d2a4 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,11 @@ [wrap-file] -directory = curl-8.6.0 -source_url = https://curl.se/download/curl-8.6.0.tar.xz -source_filename = curl-8.6.0.tar.xz -source_hash = 3ccd55d91af9516539df80625f818c734dc6f2ecf9bada33c76765e99121db15 -patch_url = https://skuller.net/meson/libcurl_8.6.0-1_patch.zip -patch_filename = libcurl_8.6.0-1_patch.zip -patch_hash = 0bfb2990a3db9b83ed359a62c68aa25a8007853b8872480d90a040fd3736761d +directory = curl-8.7.1 +source_url = https://curl.se/download/curl-8.7.1.tar.xz +source_filename = curl-8.7.1.tar.xz +source_hash = 6fea2aac6a4610fbd0400afb0bcddbe7258a64c63f1f68e5855ebc0c659710cd +patch_url = https://skuller.net/meson/libcurl_8.7.1-1_patch.zip +patch_filename = libcurl_8.7.1-1_patch.zip +patch_hash = 10bae69337beb371526ff85aba2492eddf77b39278a8fba2128226b598fa3687 [provide] libcurl = libcurl_dep From 11adbf3e3b75e03b2e10f9f859ca3cc794eef1ad Mon Sep 17 00:00:00 2001 From: Frank Richter Date: Sun, 24 Mar 2024 14:37:49 +0100 Subject: [PATCH 114/974] HTTP downloads: Early exit from start_next_download() if HTTP was disabled earlier --- src/client/http.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/http.c b/src/client/http.c index 3607dfd52..61451e4d4 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -924,6 +924,10 @@ static void start_next_download(void) return; } + // Check if HTTP download was aborted + if (!curl_multi) + return; + //not enough downloads running, queue some more! FOR_EACH_DLQ(q) { if (q->state == DL_PENDING) { From 658045cde6c837ff606de268dd4547591806ba09 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 29 Mar 2024 22:51:27 +0300 Subject: [PATCH 115/974] Apply MD5 scale fix from Paril. Fixes #347. --- src/refresh/mesh.c | 7 +- src/refresh/models.c | 159 ++++++++++++++++++++++++------------------- 2 files changed, 90 insertions(+), 76 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 285be33f6..0564ec9fb 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -628,13 +628,10 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, const md5_weight_t *weight = &weights[vert->start + i]; const md5_joint_t *joint = &skeleton[weight->joint]; - vec3_t local_pos; - VectorScale(weight->pos, joint->scale, local_pos); - vec3_t wv; - Quat_RotatePoint(joint->orient, local_pos, wv); + Quat_RotatePoint(joint->orient, weight->pos, wv); - VectorAdd(joint->pos, wv, wv); + VectorMA(joint->pos, joint->scale, wv, wv); VectorMA(out_position, weight->bias, wv, out_position); if (out_normal) { diff --git a/src/refresh/models.c b/src/refresh/models.c index 61a6ec41b..5b147fe24 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1026,6 +1026,7 @@ typedef struct { uint32_t parent; uint32_t flags; uint32_t start_index; + bool scale_pos; } joint_info_t; typedef struct { @@ -1064,7 +1065,9 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, // parent should already be calculated md5_joint_t *thisJoint = &skeleton_frame[i]; - thisJoint->scale = 1.0f; + + if (joint_infos[i].scale_pos) + VectorScale(animated_position, thisJoint->scale, animated_position); int parent = thisJoint->parent = (int32_t)joint_infos[i].parent; if (parent < 0) { @@ -1086,6 +1089,76 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, } } +/** + * Parse some JSON vomit. Don't ask. + */ +static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t *joint_infos) +{ + void *buffer; + const char *s; + int ret; + + ret = FS_LoadFile(path, &buffer); + if (!buffer) + goto fail; + s = buffer; + + MD5_EXPECT("{"); + while (s) { + int joint_id = -1; + char *tok, *tok2; + + tok = COM_Parse(&s); + if (!strcmp(tok, "}")) + break; + + for (int i = 0; i < model->num_joints; i++) { + if (!strcmp(tok, model->jointnames[i])) { + joint_id = i; + break; + } + } + + if (joint_id == -1) + Com_WPrintf("No such joint %s in %s\n", tok, path); + + MD5_EXPECT(":"); + MD5_EXPECT("{"); + + while (s) { + tok = COM_Parse(&s); + if (!strcmp(tok, "}") || !strcmp(tok, "},")) + break; + MD5_EXPECT(":"); + + tok2 = COM_Parse(&s); + if (joint_id == -1) + continue; + + if (!strcmp(tok, "scale_positions")) { + joint_infos[joint_id].scale_pos = !strncmp(tok2, "true", 4); + continue; + } + + unsigned frame_id = Q_atoi(tok); + if (frame_id >= model->num_frames) { + Com_WPrintf("No such frame %d in %s\n", frame_id, path); + continue; + } + + model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = strtof(tok2, NULL); + } + } + + FS_FreeFile(buffer); + return; + +fail: + if (ret != Q_ERR(ENOENT)) + MOD_PrintError(path, ret); + FS_FreeFile(buffer); +} + /** * Load an MD5 animation from file. */ @@ -1149,6 +1222,8 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) MD5_UINT(&joint_info->flags); MD5_UINT(&joint_info->start_index); + joint_info->scale_pos = false; + // validate animated components int num_components = 0; @@ -1195,6 +1270,18 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) OOM_CHECK(mdl->skeleton_frames = MD5_Malloc(sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints)); + // initialize scales + for (i = 0; i < mdl->num_frames * mdl->num_joints; i++) + mdl->skeleton_frames[i].scale = 1.0f; + + // load scales + char scale_path[MAX_QPATH]; + if (COM_StripExtension(scale_path, path, sizeof(scale_path)) < sizeof(scale_path) && + Q_strlcat(scale_path, ".md5scale", sizeof(scale_path)) < sizeof(scale_path)) + MOD_LoadMD5Scale(model->skeleton, scale_path, joint_infos); + else + Com_WPrintf("MD5 scale path too long: %s\n", scale_path); + for (i = 0; i < mdl->num_frames; i++) { MD5_EXPECT("frame"); @@ -1222,76 +1309,10 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) return false; } -// icky icky ""JSON"" parser. it works for re-release *.md5scale files, and I -// don't care about anything else... -static void MOD_LoadMD5Scale(md5_model_t *model, const char *path) -{ - void *buffer; - const char *s; - int ret; - - ret = FS_LoadFile(path, &buffer); - if (!buffer) - goto fail; - s = buffer; - - MD5_EXPECT("{"); - while (s) { - int joint_id = -1; - char *tok; - - tok = COM_Parse(&s); - if (!strcmp(tok, "}")) - break; - - for (int i = 0; i < model->num_joints; i++) { - if (!strcmp(tok, model->jointnames[i])) { - joint_id = i; - break; - } - } - - if (joint_id == -1) - Com_WPrintf("No such joint %s in %s\n", tok, path); - - MD5_EXPECT(":"); - MD5_EXPECT("{"); - - while (s) { - tok = COM_Parse(&s); - if (!strcmp(tok, "}") || !strcmp(tok, "},")) - break; - MD5_EXPECT(":"); - - unsigned frame_id = strtoul(tok, NULL, 10); - float scale = strtof(COM_Parse(&s), NULL); - - if (joint_id == -1) - continue; - - if (frame_id >= model->num_frames) { - Com_WPrintf("No such frame %d in %s\n", frame_id, path); - continue; - } - - model->skeleton_frames[(frame_id * model->num_joints) + joint_id].scale = scale; - } - } - - FS_FreeFile(buffer); - return; - -fail: - if (ret != Q_ERR(ENOENT)) - MOD_PrintError(path, ret); - FS_FreeFile(buffer); -} - static void MOD_LoadMD5(model_t *model) { char model_name[MAX_QPATH], base_path[MAX_QPATH]; char mesh_path[MAX_QPATH], anim_path[MAX_QPATH]; - char scale_path[MAX_QPATH]; COM_SplitPath(model->name, model_name, sizeof(model_name), base_path, sizeof(base_path), true); @@ -1331,10 +1352,6 @@ static void MOD_LoadMD5(model_t *model) } Hunk_End(&model->skeleton_hunk); - - if (Q_concat(scale_path, sizeof(scale_path), base_path, "md5/", model_name, ".md5scale") < sizeof(scale_path)) - MOD_LoadMD5Scale(model->skeleton, scale_path); - return; fail: From dd472255f38353d4735f0e9f0714dfd7caba047d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 29 Mar 2024 23:08:31 +0300 Subject: [PATCH 116/974] Avoid allocating MD5 joint names on hunk. --- src/refresh/gl.h | 3 --- src/refresh/models.c | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index dd5c91e6d..f9e6152dc 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -308,8 +308,6 @@ typedef struct { #define MD5_MAX_WEIGHTS 4096 #define MD5_MAX_FRAMES 1024 -typedef char md5_jointname_t[MD5_MAX_JOINTNAME]; - /* Joint */ typedef struct { int parent; @@ -357,7 +355,6 @@ typedef struct { md5_mesh_t *meshes; md5_joint_t *base_skeleton; md5_joint_t *skeleton_frames; // [num_joints][num_frames] - md5_jointname_t *jointnames; image_t **skins; } md5_model_t; diff --git a/src/refresh/models.c b/src/refresh/models.c index 5b147fe24..0e2fcd30f 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -889,7 +889,6 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_ENSURE(num_joints, "no joints"); MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "too many joints"); OOM_CHECK(mdl->base_skeleton = MD5_Malloc(num_joints * sizeof(mdl->base_skeleton[0]))); - OOM_CHECK(mdl->jointnames = MD5_Malloc(num_joints * sizeof(mdl->jointnames[0]))); mdl->num_joints = num_joints; MD5_EXPECT("numMeshes"); @@ -905,7 +904,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) for (i = 0; i < num_joints; i++) { md5_joint_t *joint = &mdl->base_skeleton[i]; - Q_strlcpy(mdl->jointnames[i], COM_Parse(&s), sizeof(mdl->jointnames[0])); + COM_Parse(&s); // ignore name uint32_t parent; MD5_UINT(&parent); @@ -1023,6 +1022,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) } typedef struct { + char name[MD5_MAX_JOINTNAME]; uint32_t parent; uint32_t flags; uint32_t start_index; @@ -1113,7 +1113,7 @@ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t break; for (int i = 0; i < model->num_joints; i++) { - if (!strcmp(tok, model->jointnames[i])) { + if (!strcmp(tok, joint_infos[i].name)) { joint_id = i; break; } @@ -1216,7 +1216,7 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) for (i = 0; i < mdl->num_joints; ++i) { joint_info_t *joint_info = &joint_infos[i]; - COM_Parse(&s); // ignore name + Q_strlcpy(joint_info->name, COM_Parse(&s), sizeof(joint_info->name)); MD5_UINT(&joint_info->parent); MD5_UINT(&joint_info->flags); From 53796da7085f6c32621e53eefd273c4f1a617d74 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 30 Mar 2024 22:51:13 +0300 Subject: [PATCH 117/974] Assert joint parent is valid. --- src/refresh/models.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/models.c b/src/refresh/models.c index 0e2fcd30f..df6abb3ba 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1076,6 +1076,7 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, continue; } + Q_assert(parent < num_joints); md5_joint_t *parentJoint = &skeleton_frame[parent]; // add positions From 92ee01a04b3cb479448098448048822c4ad0153c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Apr 2024 13:58:44 +0300 Subject: [PATCH 118/974] Allow calling Sys_ListFiles_r() when list is full. --- src/unix/system.c | 4 ++++ src/windows/system.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/unix/system.c b/src/unix/system.c index 4ba4bb163..9e52c2ed6 100644 --- a/src/unix/system.c +++ b/src/unix/system.c @@ -341,6 +341,10 @@ void Sys_ListFiles_r(listfiles_t *list, const char *path, int depth) char *name; void *info; + if (list->count >= MAX_LISTED_FILES) { + return; + } + if ((dir = opendir(path)) == NULL) { return; } diff --git a/src/windows/system.c b/src/windows/system.c index e967b719f..0423f1d7e 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -1022,6 +1022,10 @@ void Sys_ListFiles_r(listfiles_t *list, const char *path, int depth) void *info; const char *filter = list->filter; + if (list->count >= MAX_LISTED_FILES) { + return; + } + // optimize single extension search if (!(list->flags & (FS_SEARCH_BYFILTER | FS_SEARCH_RECURSIVE)) && filter && !strchr(filter, ';')) { From bcbec2c5671b1484370ad1d3cff235c6b8c7fee6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Apr 2024 19:52:02 +0300 Subject: [PATCH 119/974] Convert some recv_func() checks to asserts. --- src/client/http.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client/http.c b/src/client/http.c index 61451e4d4..a971762ef 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define CURL_DISABLE_DEPRECATION #include "client.h" +#include #include #include "shared/atomic.h" @@ -112,6 +113,7 @@ static int progress_func(void *clientp, curl_off_t dltotal, curl_off_t dlnow, cu } // libcurl callback for filelists. +// must be thread safe! static size_t recv_func(void *ptr, size_t size, size_t nmemb, void *stream) { dlhandle_t *dl = (dlhandle_t *)stream; @@ -120,11 +122,8 @@ static size_t recv_func(void *ptr, size_t size, size_t nmemb, void *stream) if (!size || !nmemb) return 0; - if (size > SIZE_MAX / nmemb) - return 0; - - if (dl->position > MAX_DLSIZE) - return 0; + assert(size <= SIZE_MAX / nmemb); + assert(dl->position < MAX_DLSIZE); bytes = size * nmemb; if (bytes >= MAX_DLSIZE - dl->position) @@ -133,7 +132,6 @@ static size_t recv_func(void *ptr, size_t size, size_t nmemb, void *stream) // grow buffer in MIN_DLSIZE chunks. +1 for NUL. new_size = ALIGN(dl->position + bytes + 1, MIN_DLSIZE); if (new_size > dl->size) { - // can't use Z_Realloc here because it's not threadsafe! char *buf = realloc(dl->buffer, new_size); if (!buf) return 0; From 5dd30753e1a00c24375f432ab9880f641ae6a04a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Apr 2024 19:54:06 +0300 Subject: [PATCH 120/974] Simplify formatting filelist path. --- src/client/http.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/http.c b/src/client/http.c index a971762ef..832c54100 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -591,11 +591,9 @@ int HTTP_QueueDownload(const char *path, dltype_t type) //special case for map file lists, i really wanted a server-push mechanism for this, but oh well len = strlen(path); if (len > 4 && !Q_stricmp(path + len - 4, ".bsp")) { - len = Q_snprintf(temp, sizeof(temp), "%s/%s", http_gamedir(), path); - if (len < sizeof(temp) - 5) { - memcpy(temp + len - 4, ".filelist", 10); + len = Q_snprintf(temp, sizeof(temp), "%s/%.*s.filelist", http_gamedir(), (int)(len - 4), path); + if (len < sizeof(temp)) CL_QueueDownload(temp, DL_LIST); - } } return Q_ERR_SUCCESS; From 9a60895840f4a8a9df0da3ba65fea68d53cbbc20 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 00:45:04 +0300 Subject: [PATCH 121/974] Copy over player alpha to weapon. From: Jonathan --- src/client/entities.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/client/entities.c b/src/client/entities.c index a19b60bce..e3ae7a059 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1010,6 +1010,20 @@ static void CL_AddPacketEntities(void) } } +static float player_alpha_hack(void) +{ + centity_t *ent; + + ent = &cl_entities[cl.frame.clientNum + 1]; + if (ent->serverframe != cl.frame.number) + return 1; + + if (!ent->current.modelindex || !ent->current.alpha) + return 1; + + return ent->current.alpha; +} + static int shell_effect_hack(void) { centity_t *ent; @@ -1104,6 +1118,10 @@ static void CL_AddViewWeapon(void) if (cl_gunalpha->value != 1) { gun.alpha = Cvar_ClampValue(cl_gunalpha, 0.1f, 1.0f); gun.flags |= RF_TRANSLUCENT; + } else { + gun.alpha = player_alpha_hack(); + if (gun.alpha != 1) + gun.flags |= RF_TRANSLUCENT; } V_AddEntity(&gun); From 3616d5b7072e73f48918d7863cddffe88161762b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 00:46:09 +0300 Subject: [PATCH 122/974] Always sort entity shells after their main model. From: Jonathan --- src/client/view.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/client/view.c b/src/client/view.c index 5af9c5da7..d2f65fa12 100644 --- a/src/client/view.c +++ b/src/client/view.c @@ -293,10 +293,19 @@ static int entitycmpfnc(const void *_a, const void *_b) const entity_t *b = (const entity_t *)_b; // all other models are sorted by model then skin - if (a->model == b->model) - return a->skin - b->skin; - else - return a->model - b->model; + if (a->model > b->model) + return 1; + if (a->model < b->model) + return -1; + + if (a->skin > b->skin) + return 1; + if (a->skin < b->skin) + return -1; + + bool a_shell = a->flags & RF_SHELL_MASK; + bool b_shell = b->flags & RF_SHELL_MASK; + return a_shell - b_shell; } static void V_SetLightLevel(void) From 647980620b4be7617488ff38c3ebca8617a7d832 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 00:51:56 +0300 Subject: [PATCH 123/974] Rename some OGG functions. --- src/client/sound/ogg.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index 216c666df..6d4c03377 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -155,7 +155,7 @@ static AVFormatContext *ogg_open(const char *name, bool autoplay) return fmt_ctx; } -static bool ogg_play_(void) +static bool ogg_try_play(void) { AVStream *st; const AVCodec *dec; @@ -229,7 +229,7 @@ static void ogg_play(AVFormatContext *fmt_ctx) Q_assert(!ogg.fmt_ctx); ogg.fmt_ctx = fmt_ctx; - if (!ogg_play_()) + if (!ogg_try_play()) ogg_stop(); } @@ -287,7 +287,7 @@ static int read_packet(AVPacket *pkt) } } -static int decode_frame_(void) +static int decode_frame(void) { AVCodecContext *dec = ogg.dec_ctx; AVPacket *pkt = ogg.pkt; @@ -312,9 +312,9 @@ static int decode_frame_(void) } } -static bool decode_frame(void) +static bool decode_next_frame(void) { - int ret = decode_frame_(); + int ret = decode_frame(); if (ret >= 0) return true; @@ -326,7 +326,7 @@ static bool decode_frame(void) // play next file OGG_Play(); - return ogg.dec_ctx && decode_frame_() >= 0; + return ogg.dec_ctx && decode_frame() >= 0; } static int convert_samples(AVFrame *in) @@ -387,7 +387,7 @@ void OGG_Update(void) // if swr buffer is empty, decode more input if (ret == 0) { - if (!decode_frame()) + if (!decode_next_frame()) break; // now that we have a frame, configure output From fbb340e4906352968d34fbc12d4458b1bf096b4b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 18:49:56 +0300 Subject: [PATCH 124/974] Assert raw sample count is valid. --- src/client/sound/ogg.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index 6d4c03377..a2986b4b0 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -338,7 +338,10 @@ static int convert_samples(AVFrame *in) if (out->format < 0) return 0; + // get available free space out->nb_samples = s_api.need_raw_samples(); + Q_assert((unsigned)out->nb_samples <= MAX_RAW_SAMPLES); + ret = swr_convert_frame(ogg.swr_ctx, out, in); if (ret < 0) return ret; From a4b630d67b08274c7c3ea9090ee6613110755524 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 18:50:28 +0300 Subject: [PATCH 125/974] Const-ify some function parameters. --- inc/common/cmodel.h | 2 +- inc/common/field.h | 2 +- inc/common/pmove.h | 2 +- src/client/client.h | 8 ++++---- src/client/crc.c | 2 +- src/client/download.c | 2 +- src/client/gtv.c | 4 ++-- src/client/main.c | 4 ++-- src/client/sound/al.c | 2 +- src/client/view.c | 7 ++----- src/common/cmodel.c | 2 +- src/common/field.c | 4 ++-- src/common/pmove.c | 4 ++-- src/refresh/draw.c | 2 +- src/server/mvd/parse.c | 2 +- src/server/send.c | 2 +- src/server/user.c | 6 +++--- src/windows/client.c | 4 ++-- 18 files changed, 29 insertions(+), 32 deletions(-) diff --git a/inc/common/cmodel.h b/inc/common/cmodel.h index b144bed44..19a8373da 100644 --- a/inc/common/cmodel.h +++ b/inc/common/cmodel.h @@ -80,7 +80,7 @@ bool CM_AreasConnected(cm_t *cm, int area1, int area2); int CM_WriteAreaBits(cm_t *cm, byte *buffer, int area); int CM_WritePortalBits(cm_t *cm, byte *buffer); -void CM_SetPortalStates(cm_t *cm, byte *buffer, int bytes); +void CM_SetPortalStates(cm_t *cm, const byte *buffer, int bytes); bool CM_HeadnodeVisible(mnode_t *headnode, byte *visbits); void CM_WritePortalState(cm_t *cm, qhandle_t f); diff --git a/inc/common/field.h b/inc/common/field.h index f0e632a78..252b5cd18 100644 --- a/inc/common/field.h +++ b/inc/common/field.h @@ -36,4 +36,4 @@ bool IF_CharEvent(inputField_t *field, int key); void IF_Init(inputField_t *field, size_t visibleChars, size_t maxChars); void IF_Clear(inputField_t *field); void IF_Replace(inputField_t *field, const char *text); -int IF_Draw(inputField_t *field, int x, int y, int flags, qhandle_t font); +int IF_Draw(const inputField_t *field, int x, int y, int flags, qhandle_t font); diff --git a/inc/common/pmove.h b/inc/common/pmove.h index 46b706549..e9071ddca 100644 --- a/inc/common/pmove.h +++ b/inc/common/pmove.h @@ -42,7 +42,7 @@ typedef struct { float flyfriction; } pmoveParams_t; -void Pmove(pmove_t *pmove, pmoveParams_t *params); +void Pmove(pmove_t *pmove, const pmoveParams_t *params); void PmoveInit(pmoveParams_t *pmp); void PmoveEnableQW(pmoveParams_t *pmp); diff --git a/src/client/client.h b/src/client/client.h index a01e6a65e..14a3692d1 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -757,8 +757,8 @@ extern qhandle_t gun_model; void V_Init(void); void V_Shutdown(void); void V_RenderView(void); -void V_AddEntity(entity_t *ent); -void V_AddParticle(particle_t *p); +void V_AddEntity(const entity_t *ent); +void V_AddParticle(const particle_t *p); void V_AddLight(const vec3_t org, float intensity, float r, float g, float b); void V_AddLightStyle(int style, float value); void CL_UpdateBlendSetting(void); @@ -1050,7 +1050,7 @@ void HTTP_CleanupDownloads(void); #if USE_CLIENT_GTV void CL_GTV_EmitFrame(void); -void CL_GTV_WriteMessage(byte *data, size_t len); +void CL_GTV_WriteMessage(const byte *data, size_t len); void CL_GTV_Resume(void); void CL_GTV_Suspend(void); void CL_GTV_Transmit(void); @@ -1071,4 +1071,4 @@ void CL_GTV_Shutdown(void); // // crc.c // -byte COM_BlockSequenceCRCByte(byte *base, size_t length, int sequence); +byte COM_BlockSequenceCRCByte(const byte *base, size_t length, int sequence); diff --git a/src/client/crc.c b/src/client/crc.c index 057dd6dbe..44ac3bce8 100644 --- a/src/client/crc.c +++ b/src/client/crc.c @@ -145,7 +145,7 @@ COM_BlockSequenceCRCByte For proxy protecting ==================== */ -byte COM_BlockSequenceCRCByte(byte *base, size_t length, int sequence) +byte COM_BlockSequenceCRCByte(const byte *base, size_t length, int sequence) { int n; const byte *p; diff --git a/src/client/download.c b/src/client/download.c index 6642b3783..fc97cbfc3 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -291,7 +291,7 @@ static void finish_udp_download(const char *msg) CL_StartNextDownload(); } -static bool write_udp_download(byte *data, int size) +static bool write_udp_download(const byte *data, int size) { int ret; diff --git a/src/client/gtv.c b/src/client/gtv.c index 1ce647b6f..1c8b6539a 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -195,7 +195,7 @@ static void drop_client(const char *reason) cls.gtv.state = ca_disconnected; } -static void write_stream(void *data, size_t len) +static void write_stream(const void *data, size_t len) { if (cls.gtv.state <= ca_disconnected) { return; @@ -217,7 +217,7 @@ static void write_message(gtv_serverop_t op) write_stream(msg_write.data, msg_write.cursize); } -void CL_GTV_WriteMessage(byte *data, size_t len) +void CL_GTV_WriteMessage(const byte *data, size_t len) { if (cls.gtv.state != ca_active) return; diff --git a/src/client/main.c b/src/client/main.c index 6e2225c8f..e4c061b48 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -1827,7 +1827,7 @@ typedef struct { static list_t cl_ignore_text; static list_t cl_ignore_nick; -static ignore_t *find_ignore(list_t *list, const char *match) +static ignore_t *find_ignore(const list_t *list, const char *match) { ignore_t *ignore; @@ -1840,7 +1840,7 @@ static ignore_t *find_ignore(list_t *list, const char *match) return NULL; } -static void list_ignores(list_t *list) +static void list_ignores(const list_t *list) { ignore_t *ignore; diff --git a/src/client/sound/al.c b/src/client/sound/al.c index dbedea738..c1717a1e1 100644 --- a/src/client/sound/al.c +++ b/src/client/sound/al.c @@ -300,7 +300,7 @@ static void AL_StopAllSounds(void) } } -static channel_t *AL_FindLoopingSound(int entnum, sfx_t *sfx) +static channel_t *AL_FindLoopingSound(int entnum, const sfx_t *sfx) { int i; channel_t *ch; diff --git a/src/client/view.c b/src/client/view.c index d2f65fa12..00229afb3 100644 --- a/src/client/view.c +++ b/src/client/view.c @@ -69,29 +69,26 @@ static void V_ClearScene(void) r_numparticles = 0; } - /* ===================== V_AddEntity ===================== */ -void V_AddEntity(entity_t *ent) +void V_AddEntity(const entity_t *ent) { if (r_numentities >= MAX_ENTITIES) return; - r_entities[r_numentities++] = *ent; } - /* ===================== V_AddParticle ===================== */ -void V_AddParticle(particle_t *p) +void V_AddParticle(const particle_t *p) { if (r_numparticles >= MAX_PARTICLES) return; diff --git a/src/common/cmodel.c b/src/common/cmodel.c index dc386cb7a..d41b326fb 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -1040,7 +1040,7 @@ int CM_WritePortalBits(cm_t *cm, byte *buffer) return bytes; } -void CM_SetPortalStates(cm_t *cm, byte *buffer, int bytes) +void CM_SetPortalStates(cm_t *cm, const byte *buffer, int bytes) { int i, numportals; diff --git a/src/common/field.c b/src/common/field.c index 966d11a6b..cdc62d205 100644 --- a/src/common/field.c +++ b/src/common/field.c @@ -237,9 +237,9 @@ The input line scrolls horizontally if typing goes beyond the right edge. Returns x offset of the rightmost character drawn. ================ */ -int IF_Draw(inputField_t *field, int x, int y, int flags, qhandle_t font) +int IF_Draw(const inputField_t *field, int x, int y, int flags, qhandle_t font) { - char *text = field->text; + const char *text = field->text; size_t cursorPos = field->cursorPos; size_t offset = 0; int ret; diff --git a/src/common/pmove.c b/src/common/pmove.c index 3ded287d4..17d9abf13 100644 --- a/src/common/pmove.c +++ b/src/common/pmove.c @@ -43,7 +43,7 @@ typedef struct { static pmove_t *pm; static pml_t pml; -static pmoveParams_t *pmp; +static const pmoveParams_t *pmp; // movement parameters static const float pm_stopspeed = 100; @@ -1024,7 +1024,7 @@ Pmove Can be called by either the server or the client ================ */ -void Pmove(pmove_t *pmove, pmoveParams_t *params) +void Pmove(pmove_t *pmove, const pmoveParams_t *params) { pm = pmove; pmp = params; diff --git a/src/refresh/draw.c b/src/refresh/draw.c index f14150880..da33eadf3 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -266,7 +266,7 @@ void R_DrawFill32(int x, int y, int w, int h, uint32_t color) _GL_StretchPic(x, y, w, h, 0, 0, 1, 1, color, TEXNUM_WHITE, 0); } -static inline void draw_char(int x, int y, int flags, int c, image_t *image) +static inline void draw_char(int x, int y, int flags, int c, const image_t *image) { float s, t; diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index 12fa26a53..ad2877cb8 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -202,7 +202,7 @@ static void MVD_ParseMulticast(mvd_t *mvd, mvd_ops_t op, int extrabits) } } -static void MVD_UnicastSend(mvd_t *mvd, bool reliable, byte *data, size_t length, mvd_player_t *player) +static void MVD_UnicastSend(mvd_t *mvd, bool reliable, const byte *data, size_t length, mvd_player_t *player) { mvd_player_t *target; mvd_client_t *client; diff --git a/src/server/send.c b/src/server/send.c index 14f7985e0..4e1af2e4c 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -525,7 +525,7 @@ static bool check_entity(client_t *client, int entnum) } // sounds reliative to entities are handled specially -static void emit_snd(client_t *client, message_packet_t *msg) +static void emit_snd(client_t *client, const message_packet_t *msg) { int flags, entnum; int i; diff --git a/src/server/user.c b/src/server/user.c index ee237fa12..e0e4e85b2 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -126,7 +126,7 @@ static void write_configstrings(void) SV_ClientAddMessage(sv_client, MSG_GAMESTATE); } -static void write_baseline(entity_packed_t *base) +static void write_baseline(const entity_packed_t *base) { MSG_WriteDeltaEntity(NULL, base, sv_client->esFlags | MSG_ES_FORCE); } @@ -260,7 +260,7 @@ static void write_gamestate(void) SV_ClientAddMessage(sv_client, MSG_GAMESTATE); } -static void stuff_cmds(list_t *list) +static void stuff_cmds(const list_t *list) { stuffcmd_t *stuff; @@ -997,7 +997,7 @@ static const ucmd_t ucmds[] = { { NULL, NULL } }; -static void handle_filtercmd(filtercmd_t *filter) +static void handle_filtercmd(const filtercmd_t *filter) { if (filter->action == FA_IGNORE) return; diff --git a/src/windows/client.c b/src/windows/client.c index 7cc023a0b..dd3a5bfad 100644 --- a/src/windows/client.c +++ b/src/windows/client.c @@ -666,7 +666,7 @@ static BOOL check_cursor_pos(void) #define BTN_DN(i) BIT((i) * 2 + 0) #define BTN_UP(i) BIT((i) * 2 + 1) -static void raw_mouse_event(PRAWMOUSE rm) +static void raw_mouse_event(const RAWMOUSE *rm) { int i; @@ -764,7 +764,7 @@ static void pos_changing_event(HWND wnd, WINDOWPOS *pos) pos->cy = max(pos->cy, rc.bottom - rc.top); } -static void pos_changed_event(HWND wnd, WINDOWPOS *pos) +static void pos_changed_event(HWND wnd, const WINDOWPOS *pos) { RECT rc; From 2bfc7dc6256ba93dd261dcab4256c83f0af4a3fd Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 22:20:48 +0300 Subject: [PATCH 126/974] Remove unused prototypes. --- inc/common/cmodel.h | 3 --- src/server/server.h | 1 - 2 files changed, 4 deletions(-) diff --git a/inc/common/cmodel.h b/inc/common/cmodel.h index 19a8373da..317e2c7d1 100644 --- a/inc/common/cmodel.h +++ b/inc/common/cmodel.h @@ -82,6 +82,3 @@ int CM_WriteAreaBits(cm_t *cm, byte *buffer, int area); int CM_WritePortalBits(cm_t *cm, byte *buffer); void CM_SetPortalStates(cm_t *cm, const byte *buffer, int bytes); bool CM_HeadnodeVisible(mnode_t *headnode, byte *visbits); - -void CM_WritePortalState(cm_t *cm, qhandle_t f); -void CM_ReadPortalState(cm_t *cm, qhandle_t f); diff --git a/src/server/server.h b/src/server/server.h index 240aeec56..19309a94a 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -759,7 +759,6 @@ extern const game_export_ex_t *gex; void SV_InitGameProgs(void); void SV_ShutdownGameProgs(void); -void SV_InitEdict(edict_t *e); void PF_Pmove(pmove_t *pm); From 39f1ccbd02c839be807b165e51c2a2418c02208b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 22:21:11 +0300 Subject: [PATCH 127/974] Simplify CONST_STR_LEN() macro. --- inc/common/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/common/common.h b/inc/common/common.h index 16824f686..788674977 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAXPRINTMSG 4096 #define MAXERRORMSG 1024 -#define CONST_STR_LEN(x) x, x ? sizeof(x) - 1 : 0 +#define CONST_STR_LEN(x) x, sizeof(x) - 1 #define STRINGIFY2(x) #x #define STRINGIFY(x) STRINGIFY2(x) From f8dd4cb5006e4307a94e0b5e94e180daab1a2020 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 4 Apr 2024 22:34:17 +0300 Subject: [PATCH 128/974] Remove unneeded const. --- inc/common/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/common/common.h b/inc/common/common.h index 788674977..c100526ce 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc., typedef struct { const char *name; - void (* const func)(void); + void (*func)(void); } ucmd_t; static inline const ucmd_t *Com_Find(const ucmd_t *u, const char *c) From 5fcaa6eedecf71ca1344a54c9a38950f6fa77f0a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 5 Apr 2024 14:36:29 +0300 Subject: [PATCH 129/974] Fix formatting. --- src/server/mvd/game.c | 3 +-- src/windows/dsound.c | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 7228e70db..db3553245 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -450,8 +450,7 @@ static void MVD_FollowStop(mvd_client_t *client) client->ps.viewangles[ROLL] = 0; for (i = 0; i < 3; i++) { - client->ps.pmove.delta_angles[i] = ANGLE2SHORT( - client->ps.viewangles[i]) - client->lastcmd.angles[i]; + client->ps.pmove.delta_angles[i] = ANGLE2SHORT(client->ps.viewangles[i]) - client->lastcmd.angles[i]; } VectorClear(client->ps.kick_angles); diff --git a/src/windows/dsound.c b/src/windows/dsound.c index 6a56abfa4..fdcac522a 100644 --- a/src/windows/dsound.c +++ b/src/windows/dsound.c @@ -21,7 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include #include -typedef HRESULT(WINAPI *LPDIRECTSOUNDCREATE)(LPCGUID, LPDIRECTSOUND *, LPUNKNOWN); +typedef HRESULT (WINAPI *LPDIRECTSOUNDCREATE)(LPCGUID, LPDIRECTSOUND *, LPUNKNOWN); #define SECONDARY_BUFFER_SIZE 0x10000 @@ -198,8 +198,6 @@ static bool DS_CreateBuffers(void) return true; } - - /* ================== DS_Init From 195683de370ce18e5312583d19a3b0b84f7b39d4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 7 Apr 2024 20:11:21 -0400 Subject: [PATCH 130/974] Fixed naggy warning that turned into an error --- src/action/a_team.c | 4 ++-- src/action/tng_jump.c | 10 ++++++++++ src/action/tng_jump.h | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index ece2e6714..1825f885c 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1111,8 +1111,8 @@ pmenu_t pmitemmenu[] = { { "Laser Sight (jmod laser)", PMENU_ALIGN_LEFT, NULL, ToggleLaser }, { "Slippers (jmod slippers)", PMENU_ALIGN_LEFT, NULL, ToggleSlippers }, { NULL, PMENU_ALIGN_LEFT, NULL, NULL }, - { "Respawn to Closest Spawn (jmod spawnc)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoPC_f }, - { "Respawn to Random Spawn (jmod spawnp)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoP_f }, + { "Respawn to Closest Spawn (jmod spawnc)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoPC_f_compat }, + { "Respawn to Random Spawn (jmod spawnp)", PMENU_ALIGN_LEFT, NULL, Cmd_GotoP_f_compat }, }; diff --git a/src/action/tng_jump.c b/src/action/tng_jump.c index 89565d502..f21314aa6 100644 --- a/src/action/tng_jump.c +++ b/src/action/tng_jump.c @@ -358,6 +358,16 @@ void Cmd_Goto_f (edict_t *ent) } +void Cmd_GotoP_f_compat (edict_t *ent, pmenu_t *p) +{ + Cmd_GotoP_f(ent); +} + +void Cmd_GotoPC_f_compat (edict_t *ent, pmenu_t *p) +{ + Cmd_GotoPC_f(ent); +} + void Cmd_GotoP_f (edict_t *ent) { edict_t *spot = NULL; diff --git a/src/action/tng_jump.h b/src/action/tng_jump.h index fa927519a..3e77b630d 100644 --- a/src/action/tng_jump.h +++ b/src/action/tng_jump.h @@ -12,6 +12,8 @@ void Cmd_Jmod_f (edict_t *ent); void Cmd_PMLCA_f (edict_t *ent); void Cmd_RHS_f (edict_t *ent); void Cmd_Goto_f (edict_t *ent); +void Cmd_GotoP_f_compat (edict_t *ent, pmenu_t *p); +void Cmd_GotoPC_f_compat (edict_t *ent, pmenu_t *p); void Cmd_GotoP_f (edict_t *ent); void Cmd_GotoPC_f (edict_t *ent); void Cmd_Clear_f (edict_t *ent); From 704f1c7d4752d0c04af7aafeddca2a135e78bcb7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 10 Apr 2024 19:53:01 +0300 Subject: [PATCH 131/974] Make CONST_STR_LEN() accept string literals only. --- inc/common/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/common/common.h b/inc/common/common.h index c100526ce..c831c966d 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAXPRINTMSG 4096 #define MAXERRORMSG 1024 -#define CONST_STR_LEN(x) x, sizeof(x) - 1 +#define CONST_STR_LEN(x) x, sizeof("" x) - 1 #define STRINGIFY2(x) #x #define STRINGIFY(x) STRINGIFY2(x) From 93400a68283d3b93a97b5f770b4c624d947f0d56 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 10 Apr 2024 19:54:16 +0300 Subject: [PATCH 132/974] Assert file position is valid. --- src/common/files.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/files.c b/src/common/files.c index 59e43924f..07b88953f 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -1025,6 +1025,8 @@ static int read_zip_file(file_t *file, void *buf, size_t len) size_t block, result; int ret; + Q_assert(file->position <= file->length); + len = min(len, file->length - file->position); if (!len) { return 0; @@ -1437,6 +1439,8 @@ static int read_pak_file(file_t *file, void *buf, size_t len) { size_t result; + Q_assert(file->position <= file->length); + len = min(len, file->length - file->position); if (!len) { return 0; From e8950b91c12d4efa815bc206945ae45e908eaa64 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 10 Apr 2024 19:54:29 +0300 Subject: [PATCH 133/974] Replace Q_scnprintf with Q_snprintf in macro functions. Macros are allowed to return value >= size on overflow. --- src/client/main.c | 28 ++++++++++++++-------------- src/common/common.c | 2 +- src/common/net/net.c | 4 ++-- src/refresh/main.c | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/client/main.c b/src/client/main.c index e4c061b48..d14908afe 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2205,7 +2205,7 @@ static size_t CL_Ups_m(char *buffer, size_t size) VectorScale(cl.frame.ps.pmove.velocity, 0.125f, vel); } - return Q_scnprintf(buffer, size, "%.f", VectorLength(vel)); + return Q_snprintf(buffer, size, "%.f", VectorLength(vel)); } static size_t CL_Timer_m(char *buffer, size_t size) @@ -2217,9 +2217,9 @@ static size_t CL_Timer_m(char *buffer, size_t size) hour = min / 60; min %= 60; if (hour) { - return Q_scnprintf(buffer, size, "%i:%i:%02i", hour, min, sec); + return Q_snprintf(buffer, size, "%i:%i:%02i", hour, min, sec); } - return Q_scnprintf(buffer, size, "%i:%02i", min, sec); + return Q_snprintf(buffer, size, "%i:%02i", min, sec); } static size_t CL_DemoPos_m(char *buffer, size_t size) @@ -2234,32 +2234,32 @@ static size_t CL_DemoPos_m(char *buffer, size_t size) sec = framenum / 10; framenum %= 10; min = sec / 60; sec %= 60; - return Q_scnprintf(buffer, size, "%d:%02d.%d", min, sec, framenum); + return Q_snprintf(buffer, size, "%d:%02d.%d", min, sec, framenum); } static size_t CL_Fps_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", C_FPS); + return Q_snprintf(buffer, size, "%i", C_FPS); } static size_t R_Fps_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", R_FPS); + return Q_snprintf(buffer, size, "%i", R_FPS); } static size_t CL_Mps_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", C_MPS); + return Q_snprintf(buffer, size, "%i", C_MPS); } static size_t CL_Pps_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", C_PPS); + return Q_snprintf(buffer, size, "%i", C_PPS); } static size_t CL_Ping_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", cls.measure.ping); + return Q_snprintf(buffer, size, "%i", cls.measure.ping); } static size_t CL_Lag_m(char *buffer, size_t size) @@ -2269,22 +2269,22 @@ static size_t CL_Lag_m(char *buffer, size_t size) if (cls.netchan.total_received) f = (float)cls.netchan.total_dropped / cls.netchan.total_received; - return Q_scnprintf(buffer, size, "%.2f%%", f * 100.0f); + return Q_snprintf(buffer, size, "%.2f%%", f * 100.0f); } static size_t CL_Health_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_HEALTH]); + return Q_snprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_HEALTH]); } static size_t CL_Ammo_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_AMMO]); + return Q_snprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_AMMO]); } static size_t CL_Armor_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_ARMOR]); + return Q_snprintf(buffer, size, "%i", cl.frame.ps.stats[STAT_ARMOR]); } static size_t CL_WeaponModel_m(char *buffer, size_t size) @@ -2295,7 +2295,7 @@ static size_t CL_WeaponModel_m(char *buffer, size_t size) static size_t CL_NumEntities_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%i", cl.frame.numEntities); + return Q_snprintf(buffer, size, "%i", cl.frame.numEntities); } /* diff --git a/src/common/common.c b/src/common/common.c index b158e2734..f94f1e860 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -648,7 +648,7 @@ size_t Com_UptimeLong_m(char *buffer, size_t size) static size_t Com_Random_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%d", Q_rand() % 10); + return Q_snprintf(buffer, size, "%d", Q_rand() % 10); } static size_t Com_MapList_m(char *buffer, size_t size) diff --git a/src/common/net/net.c b/src/common/net/net.c index 8ee6a4e42..1c5de4686 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -524,12 +524,12 @@ static void NET_Stats_f(void) static size_t NET_UpRate_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%zu", net_rate_up); + return Q_snprintf(buffer, size, "%zu", net_rate_up); } static size_t NET_DnRate_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%zu", net_rate_dn); + return Q_snprintf(buffer, size, "%zu", net_rate_dn); } //============================================================================= diff --git a/src/refresh/main.c b/src/refresh/main.c index c13561f80..42fe56a54 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -840,7 +840,7 @@ static void GL_Strings_f(void) static size_t GL_ViewCluster_m(char *buffer, size_t size) { - return Q_scnprintf(buffer, size, "%d", glr.viewcluster1); + return Q_snprintf(buffer, size, "%d", glr.viewcluster1); } static void gl_lightmap_changed(cvar_t *self) From 78cce47205d3266effef4139820b66896f392202 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 10 Apr 2024 23:18:15 +0300 Subject: [PATCH 134/974] Simplify menu cursors registration. --- src/client/ui/ui.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/client/ui/ui.c b/src/client/ui/ui.c index eaaf0908f..2c7155f58 100644 --- a/src/client/ui/ui.c +++ b/src/client/ui/ui.c @@ -614,9 +614,6 @@ UI_Init */ void UI_Init(void) { - char buffer[MAX_QPATH]; - int i; - Cmd_Register(c_ui); ui_debug = Cvar_Get("ui_debug", "0", 0); @@ -628,9 +625,8 @@ void UI_Init(void) uis.cursorHandle = R_RegisterPic("ch1"); R_GetPicSize(&uis.cursorWidth, &uis.cursorHeight, uis.cursorHandle); - for (i = 0; i < NUM_CURSOR_FRAMES; i++) { - Q_snprintf(buffer, sizeof(buffer), "m_cursor%d", i); - uis.bitmapCursors[i] = R_RegisterPic(buffer); + for (int i = 0; i < NUM_CURSOR_FRAMES; i++) { + uis.bitmapCursors[i] = R_RegisterPic(va("m_cursor%d", i)); } uis.color.background.u32 = MakeColor(0, 0, 0, 255); From dad3428024afbdf6c75b93759be0b09f0e4ddbc3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 11 Apr 2024 00:10:26 +0300 Subject: [PATCH 135/974] =?UTF-8?q?Use=20Q=5Frand=5Funiform()=20for=20?= =?UTF-8?q?=E2=80=98random=E2=80=99=20macro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/common.c b/src/common/common.c index f94f1e860..0eacc5000 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -648,7 +648,7 @@ size_t Com_UptimeLong_m(char *buffer, size_t size) static size_t Com_Random_m(char *buffer, size_t size) { - return Q_snprintf(buffer, size, "%d", Q_rand() % 10); + return Q_snprintf(buffer, size, "%d", Q_rand_uniform(10)); } static size_t Com_MapList_m(char *buffer, size_t size) From b11bde0b5e7282e18606b37b83701690a4b01921 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 23 Apr 2024 21:01:12 +0300 Subject: [PATCH 136/974] Avoid using identifiers starting with underscore. --- src/refresh/draw.c | 18 +++++++++--------- src/refresh/images.c | 8 ++++---- src/refresh/world.c | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index da33eadf3..e2d98c22f 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., drawStatic_t draw; -static inline void _GL_StretchPic( +static inline void GL_StretchPic_( float x, float y, float w, float h, float s1, float t1, float s2, float t2, uint32_t color, int texnum, int flags) @@ -74,7 +74,7 @@ static inline void _GL_StretchPic( } #define GL_StretchPic(x,y,w,h,s1,t1,s2,t2,color,image) \ - _GL_StretchPic(x,y,w,h,s1,t1,s2,t2,color,(image)->texnum,(image)->flags) + GL_StretchPic_(x,y,w,h,s1,t1,s2,t2,color,(image)->texnum,(image)->flags) void GL_Blend(void) { @@ -85,7 +85,7 @@ void GL_Blend(void) color.u8[2] = glr.fd.blend[2] * 255; color.u8[3] = glr.fd.blend[3] * 255; - _GL_StretchPic(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, + GL_StretchPic_(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, color.u32, TEXNUM_WHITE, 0); } @@ -98,7 +98,7 @@ void R_ClearColor(void) void R_SetAlpha(float alpha) { draw.colors[0].u8[3] = - draw.colors[1].u8[3] = alpha * 255; + draw.colors[1].u8[3] = alpha * 255; } void R_SetColor(uint32_t color) @@ -235,7 +235,7 @@ void R_DrawPic(int x, int y, qhandle_t pic) void R_DrawStretchRaw(int x, int y, int w, int h) { - _GL_StretchPic(x, y, w, h, 0, 0, 1, 1, U32_WHITE, TEXNUM_RAW, 0); + GL_StretchPic_(x, y, w, h, 0, 0, 1, 1, U32_WHITE, TEXNUM_RAW, 0); } void R_UpdateRawPic(int pic_w, int pic_h, const uint32_t *pic) @@ -256,14 +256,14 @@ void R_DrawFill8(int x, int y, int w, int h, int c) { if (!w || !h) return; - _GL_StretchPic(x, y, w, h, 0, 0, 1, 1, d_8to24table[c & 0xff], TEXNUM_WHITE, 0); + GL_StretchPic_(x, y, w, h, 0, 0, 1, 1, d_8to24table[c & 0xff], TEXNUM_WHITE, 0); } void R_DrawFill32(int x, int y, int w, int h, uint32_t color) { if (!w || !h) return; - _GL_StretchPic(x, y, w, h, 0, 0, 1, 1, color, TEXNUM_WHITE, 0); + GL_StretchPic_(x, y, w, h, 0, 0, 1, 1, color, TEXNUM_WHITE, 0); } static inline void draw_char(int x, int y, int flags, int c, const image_t *image) @@ -382,7 +382,7 @@ void Draw_Lightmaps(void) for (int j = 0; j < rows; j++) { int k = j * cols + i; if (k < lm.nummaps) - _GL_StretchPic(block * i, block * j, block, block, + GL_StretchPic_(block * i, block * j, block, block, 0, 0, 1, 1, U32_WHITE, lm.texnums[k], 0); } } @@ -390,7 +390,7 @@ void Draw_Lightmaps(void) void Draw_Scrap(void) { - _GL_StretchPic(0, 0, 256, 256, + GL_StretchPic_(0, 0, 256, 256, 0, 0, 1, 1, U32_WHITE, TEXNUM_SCRAP, IF_PALETTED | IF_TRANSPARENT); } diff --git a/src/refresh/images.c b/src/refresh/images.c index 0efd38ef0..6394f5c63 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1521,7 +1521,7 @@ static image_t *lookup_image(const char *name, return NULL; } -static int _try_image_format(imageformat_t fmt, image_t *image, byte **pic) +static int try_image_format_(imageformat_t fmt, image_t *image, byte **pic) { byte *data; int len, ret; @@ -1544,7 +1544,7 @@ static int try_image_format(imageformat_t fmt, image_t *image, byte **pic) { // replace the extension memcpy(image->name + image->baselen + 1, img_loaders[fmt].ext, 4); - return _try_image_format(fmt, image, pic); + return try_image_format_(fmt, image, pic); } @@ -1715,7 +1715,7 @@ static int load_image_data(image_t *image, imageformat_t fmt, bool check_dimensi ret = try_other_formats(IM_MAX, image, pic); } else { // first try with original extension - ret = _try_image_format(fmt, image, pic); + ret = try_image_format_(fmt, image, pic); if (ret == Q_ERR(ENOENT)) { // retry with remaining extensions ret = try_other_formats(fmt, image, pic); @@ -1731,7 +1731,7 @@ static int load_image_data(image_t *image, imageformat_t fmt, bool check_dimensi if (fmt == IM_MAX) { ret = Q_ERR_INVALID_PATH; } else { - ret = _try_image_format(fmt, image, pic); + ret = try_image_format_(fmt, image, pic); } #endif diff --git a/src/refresh/world.c b/src/refresh/world.c index 2cc32cb97..6f5ef1e4d 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -153,7 +153,7 @@ static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t colo return true; } -static bool _GL_LightPoint(const vec3_t start, vec3_t color) +static bool GL_LightPoint_(const vec3_t start, vec3_t color) { bsp_t *bsp; int i, index; @@ -316,7 +316,7 @@ void GL_LightPoint(const vec3_t origin, vec3_t color) } // get lighting from world - if (!_GL_LightPoint(origin, color)) { + if (!GL_LightPoint_(origin, color)) { VectorSet(color, 1, 1, 1); } From 8da0d9d68367d86655827c5b118c75abf8cb41cc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 23 Apr 2024 21:16:08 +0300 Subject: [PATCH 137/974] Add MASK() macro for defining bit masks. --- inc/common/cvar.h | 2 +- inc/common/protocol.h | 6 +++--- inc/shared/shared.h | 7 +++++-- src/game/g_local.h | 2 +- src/refresh/world.c | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/inc/common/cvar.h b/inc/common/cvar.h index 8e72db985..f6f350ce3 100644 --- a/inc/common/cvar.h +++ b/inc/common/cvar.h @@ -48,7 +48,7 @@ interface from being ambiguous. #define CVAR_INFOMASK (CVAR_USERINFO | CVAR_SERVERINFO) #define CVAR_MODIFYMASK (CVAR_INFOMASK | CVAR_FILES | CVAR_REFRESH | CVAR_SOUND) #define CVAR_NOARCHIVEMASK (CVAR_NOSET | CVAR_CHEAT | CVAR_PRIVATE | CVAR_ROM | CVAR_NOARCHIVE) -#define CVAR_EXTENDED_MASK (~31) +#define CVAR_EXTENDED_MASK (~MASK(5)) extern cvar_t *cvar_vars; extern int cvar_modified; diff --git a/inc/common/protocol.h b/inc/common/protocol.h index f7cfc6b34..ec2013ab4 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -85,13 +85,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SVCMD_BITS 5 -#define SVCMD_MASK (BIT(SVCMD_BITS) - 1) +#define SVCMD_MASK MASK(SVCMD_BITS) #define FRAMENUM_BITS 27 -#define FRAMENUM_MASK (BIT(FRAMENUM_BITS) - 1) +#define FRAMENUM_MASK MASK(FRAMENUM_BITS) #define SUPPRESSCOUNT_BITS 4 -#define SUPPRESSCOUNT_MASK (BIT(SUPPRESSCOUNT_BITS) - 1) +#define SUPPRESSCOUNT_MASK MASK(SUPPRESSCOUNT_BITS) #define MAX_PACKET_ENTITIES_OLD 128 diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 251c2948d..c3b570fa5 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -176,6 +176,9 @@ typedef struct vrect_s { #define BIT(n) (1U << (n)) #define BIT_ULL(n) (1ULL << (n)) +#define MASK(n) (BIT(n) - 1U) +#define MASK_ULL(n) (BIT_ULL(n) - 1ULL) + #define SWAP(type, a, b) \ do { type SWAP_tmp = a; a = b; b = SWAP_tmp; } while (0) @@ -1435,10 +1438,10 @@ typedef struct { #if USE_PROTOCOL_EXTENSIONS #define ENTITYNUM_BITS 13 -#define ENTITYNUM_MASK (BIT(ENTITYNUM_BITS) - 1) +#define ENTITYNUM_MASK MASK(ENTITYNUM_BITS) #define GUNINDEX_BITS 13 // upper 3 bits are skinnum -#define GUNINDEX_MASK (BIT(GUNINDEX_BITS) - 1) +#define GUNINDEX_MASK MASK(GUNINDEX_BITS) typedef struct { int morefx; diff --git a/src/game/g_local.h b/src/game/g_local.h index 27418adae..fdc7c5ea2 100644 --- a/src/game/g_local.h +++ b/src/game/g_local.h @@ -168,7 +168,7 @@ typedef enum { #define SFL_CROSS_TRIGGER_6 BIT(5) #define SFL_CROSS_TRIGGER_7 BIT(6) #define SFL_CROSS_TRIGGER_8 BIT(7) -#define SFL_CROSS_TRIGGER_MASK (BIT(8) - 1) +#define SFL_CROSS_TRIGGER_MASK MASK(8) // noise types for PlayerNoise #define PNOISE_SELF 0 diff --git a/src/refresh/world.c b/src/refresh/world.c index 6f5ef1e4d..f1dc12add 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -522,7 +522,7 @@ void GL_DrawBspModel(mmodel_t *model) } #define NODE_CLIPPED 0 -#define NODE_UNCLIPPED (BIT(4) - 1) +#define NODE_UNCLIPPED MASK(4) static inline bool GL_ClipNode(mnode_t *node, int *clipflags) { From 3be729809c96de90401eff82fcbe27179add3e98 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 29 Apr 2024 10:46:23 -0400 Subject: [PATCH 138/974] Adjusted MacOS pkgconfig path to OpenAL --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f084fedf..6497d147d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -258,7 +258,7 @@ jobs: meson compile -vC builddir env: CC: "gcc" - PKG_CONFIG_PATH: "/usr/local/opt/openal-soft/lib/pkgconfig" + PKG_CONFIG_PATH: "/opt/homebrew/opt/openal-soft/lib/pkgconfig" CFLAGS: "-mmacosx-version-min=10.7" - name: Set binaries as executable From 3454d883b38dea9856fe11c12e68b3d18d658f1d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 29 Apr 2024 12:15:06 -0400 Subject: [PATCH 139/974] Updated Mac Intel and ARM builds, OpenAL refs --- .github/workflows/build.yml | 85 +++++++++++++++++------------------ .github/workflows/release.yml | 13 +++--- src/client/sound/qal.c | 2 + 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6497d147d..fe628f88d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: meson compile -vC builddir - name: Generate Win Mingw archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: q2pro-mingw-${{ matrix.arch }} path: | @@ -107,7 +107,7 @@ jobs: meson compile -C builddir - name: Generate Win MSVC archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: q2pro-msvc-${{ matrix.arch }} path: | @@ -151,7 +151,7 @@ jobs: chmod +x builddir/q2pro* - name: Generate Linux x64 archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: q2pro-lin-${{ matrix.cc }} path: | @@ -195,7 +195,7 @@ jobs: chmod +x builddir/q2pro* - name: Generate Linux x64 archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: q2pro-lin-wayland-${{ matrix.cc }} path: | @@ -209,7 +209,7 @@ jobs: matrix: cc: [gcc] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 # - name: Install dependencies # run: | @@ -232,7 +232,7 @@ jobs: chmod +x builddir/q2pro* - name: Generate Linux ARM archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: q2pro-lin-arm64 path: | @@ -241,12 +241,9 @@ jobs: builddir/gamearm.so darwin: - runs-on: macos-latest - strategy: - matrix: - arch: [x86_64] + runs-on: macos-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies run: | @@ -258,7 +255,7 @@ jobs: meson compile -vC builddir env: CC: "gcc" - PKG_CONFIG_PATH: "/opt/homebrew/opt/openal-soft/lib/pkgconfig" + PKG_CONFIG_PATH: "/usr/local/opt/openal-soft/lib/pkgconfig" CFLAGS: "-mmacosx-version-min=10.7" - name: Set binaries as executable @@ -266,41 +263,41 @@ jobs: chmod +x builddir/q2pro* - name: Generate Mac archives - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: q2pro-darwin-${{ matrix.arch }} + name: q2pro-darwin-x86_64 path: | builddir/q2pro builddir/q2proded builddir/gamex86_64.dylib - # Github does not support Apple Silicon yet - # macARM64: - # runs-on: macos-latest - # steps: - # - uses: actions/checkout@v2 - - # - name: Setup Homebrew for ARM64 and dependencies - # run: | - # arch -arm64 brew install meson libpng jpeg sdl2 openal-soft zlib curl libvorbis - - # - name: Build - # run: | - # meson setup ${{ env.MESON_ARGS }} builddir - # ninja -vC builddir - # env: - # CC: "gcc" - # PKG_CONFIG_PATH: "/opt/homebrew/Cellar/openal-soft/1.22.2/lib/pkgconfig/:/opt/homebrew/Cellar/jpeg/9e/lib/pkgconfig/" - # CFLAGS: "-target arm64-apple-macos11" - - # - name: Set binaries as executable - # run: | - # chmod +x builddir/q2pro* - - # - name: Generate Mac archives - # uses: actions/upload-artifact@v3 - # with: - # name: q2pro-mac-arm64 - # path: | - # builddir/q2pro - # builddir/q2proded + darwin-arm64: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo + + - name: Build + run: | + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_MAC }} builddir + meson compile -vC builddir + env: + CC: "gcc" + PKG_CONFIG_PATH: "/opt/homebrew/opt/openal-soft/lib/pkgconfig" + CFLAGS: "-target arm64-apple-macos11" + + - name: Set binaries as executable + run: | + chmod +x builddir/q2pro* + + - name: Generate Mac archives + uses: actions/upload-artifact@v4 + with: + name: q2pro-darwin-arm64 + path: | + builddir/q2pro + builddir/q2proded + builddir/gamex86_64.dylib diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1603ac1a..f44563498 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,6 +101,13 @@ jobs: name: q2pro-darwin-x86_64 skip_unpack: true + - name: Download Darwin arm64 build artifacts + uses: dawidd6/action-download-artifact@v2 + with: + workflow: build.yml + name: q2pro-darwin-arm64 + skip_unpack: true + - name: Upload dist tarball to release uses: softprops/action-gh-release@v1 env: @@ -117,8 +124,4 @@ jobs: q2pro-lin-wayland-gcc.zip q2pro-lin-wayland-clang.zip q2pro-darwin-x86_64.zip - - - - - + q2pro-darwin-arm64.zip diff --git a/src/client/sound/qal.c b/src/client/sound/qal.c index ebe610ce4..834cfd900 100644 --- a/src/client/sound/qal.c +++ b/src/client/sound/qal.c @@ -142,6 +142,8 @@ void QAL_Shutdown(void) static const char *const al_drivers[] = { #ifdef _WIN32 "soft_oal", "openal32" +#elif defined(__APPLE__) + "libopenal.dylib.1", "libopenal.dylib" #else "libopenal.so.1", "libopenal.so" #endif From 7e53d8b94bec381b16c30fe6bfc47fa5fa1e61a4 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 29 Apr 2024 12:26:47 -0400 Subject: [PATCH 140/974] Github didn't update the build yaml correctly, no-op --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe628f88d..57534d6f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -300,4 +300,4 @@ jobs: path: | builddir/q2pro builddir/q2proded - builddir/gamex86_64.dylib + builddir/gamex86_64.dylib \ No newline at end of file From 0bfc25647ee69b49204774e7f568f47eea2a468b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 24 Apr 2024 21:42:46 +0300 Subject: [PATCH 141/974] Const-ify more stuff. --- inc/common/bsp.h | 12 ++-- inc/common/cmodel.h | 34 +++++------ inc/common/common.h | 2 +- src/client/client.h | 2 +- src/client/entities.c | 12 ++-- src/client/predict.c | 13 ++--- src/common/bsp.c | 26 ++++----- src/common/cmodel.c | 126 ++++++++++++++++++++--------------------- src/refresh/gl.h | 2 +- src/refresh/sky.c | 2 +- src/refresh/surf.c | 4 +- src/refresh/tess.c | 8 +-- src/refresh/world.c | 28 ++++----- src/server/entities.c | 8 +-- src/server/game.c | 8 +-- src/server/mvd/parse.c | 4 +- src/server/send.c | 4 +- src/server/server.h | 4 +- src/server/world.c | 20 +++---- 19 files changed, 156 insertions(+), 163 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index c1af4f7b2..24dbbcddd 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -326,15 +326,15 @@ typedef struct { float fraction; } lightpoint_t; -void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, mnode_t *headnode, int nolm_mask); +void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, const mnode_t *headnode, int nolm_mask); void BSP_TransformedLightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, - mnode_t *headnode, int nolm_mask, const vec3_t origin, const vec3_t angles); + const mnode_t *headnode, int nolm_mask, const vec3_t origin, const vec3_t angles); -lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]); +const lightgrid_sample_t *BSP_LookupLightgrid(const lightgrid_t *grid, const uint32_t point[3]); #endif -byte *BSP_ClusterVis(bsp_t *bsp, byte *mask, int cluster, int vis); -mleaf_t *BSP_PointLeaf(mnode_t *node, const vec3_t p); -mmodel_t *BSP_InlineModel(bsp_t *bsp, const char *name); +byte *BSP_ClusterVis(const bsp_t *bsp, byte *mask, int cluster, int vis); +const mleaf_t *BSP_PointLeaf(const mnode_t *node, const vec3_t p); +const mmodel_t *BSP_InlineModel(const bsp_t *bsp, const char *name); void BSP_Init(void); diff --git a/inc/common/cmodel.h b/inc/common/cmodel.h index 317e2c7d1..eb0e18a1b 100644 --- a/inc/common/cmodel.h +++ b/inc/common/cmodel.h @@ -40,8 +40,8 @@ void CM_FreeMap(cm_t *cm); int CM_LoadMap(cm_t *cm, const char *name); void CM_LoadOverrides(cm_t *cm, char *server, size_t server_size); -mnode_t *CM_NodeNum(cm_t *cm, int number); -mleaf_t *CM_LeafNum(cm_t *cm, int number); +const mnode_t *CM_NodeNum(const cm_t *cm, int number); +const mleaf_t *CM_LeafNum(const cm_t *cm, int number); #define CM_InlineModel(cm, name) BSP_InlineModel((cm)->cache, name) @@ -49,36 +49,36 @@ mleaf_t *CM_LeafNum(cm_t *cm, int number); #define CM_NumLeaf(cm, leaf) ((cm)->cache ? ((leaf) - (cm)->cache->leafs) : 0) // creates a clipping hull for an arbitrary box -mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs); +const mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs); // returns an ORed contents mask -int CM_PointContents(const vec3_t p, mnode_t *headnode); -int CM_TransformedPointContents(const vec3_t p, mnode_t *headnode, +int CM_PointContents(const vec3_t p, const mnode_t *headnode); +int CM_TransformedPointContents(const vec3_t p, const mnode_t *headnode, const vec3_t origin, const vec3_t angles); void CM_BoxTrace(trace_t *trace, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, - mnode_t *headnode, int brushmask); + const mnode_t *headnode, int brushmask); void CM_TransformedBoxTrace(trace_t *trace, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, - mnode_t *headnode, int brushmask, + const mnode_t *headnode, int brushmask, const vec3_t origin, const vec3_t angles); void CM_ClipEntity(trace_t *dst, const trace_t *src, struct edict_s *ent); // call with topnode set to the headnode, returns with topnode // set to the first node that splits the box -int CM_BoxLeafs(cm_t *cm, const vec3_t mins, const vec3_t maxs, - mleaf_t **list, int listsize, mnode_t **topnode); -mleaf_t *CM_PointLeaf(cm_t *cm, const vec3_t p); +int CM_BoxLeafs(const cm_t *cm, const vec3_t mins, const vec3_t maxs, + const mleaf_t **list, int listsize, const mnode_t **topnode); +const mleaf_t *CM_PointLeaf(const cm_t *cm, const vec3_t p); -byte *CM_FatPVS(cm_t *cm, byte *mask, const vec3_t org); +byte *CM_FatPVS(const cm_t *cm, byte *mask, const vec3_t org); -void CM_SetAreaPortalState(cm_t *cm, int portalnum, bool open); -bool CM_AreasConnected(cm_t *cm, int area1, int area2); +void CM_SetAreaPortalState(const cm_t *cm, int portalnum, bool open); +bool CM_AreasConnected(const cm_t *cm, int area1, int area2); -int CM_WriteAreaBits(cm_t *cm, byte *buffer, int area); -int CM_WritePortalBits(cm_t *cm, byte *buffer); -void CM_SetPortalStates(cm_t *cm, const byte *buffer, int bytes); -bool CM_HeadnodeVisible(mnode_t *headnode, byte *visbits); +int CM_WriteAreaBits(const cm_t *cm, byte *buffer, int area); +int CM_WritePortalBits(const cm_t *cm, byte *buffer); +void CM_SetPortalStates(const cm_t *cm, const byte *buffer, int bytes); +bool CM_HeadnodeVisible(const mnode_t *headnode, const byte *visbits); diff --git a/inc/common/common.h b/inc/common/common.h index c831c966d..681c20e22 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -76,7 +76,7 @@ typedef struct string_entry_s { char string[1]; } string_entry_t; -typedef void (*rdflush_t)(int target, char *buffer, size_t len); +typedef void (*rdflush_t)(int target, const char *buffer, size_t len); void Com_BeginRedirect(int target, char *buffer, size_t buffersize, rdflush_t flush); void Com_EndRedirect(void); diff --git a/src/client/client.h b/src/client/client.h index 14a3692d1..07bd7bbc1 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -287,7 +287,7 @@ typedef struct client_state_s { bsp_t *bsp; qhandle_t model_draw[MAX_MODELS]; - mmodel_t *model_clip[MAX_MODELS]; + const mmodel_t *model_clip[MAX_MODELS]; qhandle_t sound_precache[MAX_SOUNDS]; qhandle_t image_precache[MAX_IMAGES]; diff --git a/src/client/entities.c b/src/client/entities.c index e3ae7a059..10e6fddf4 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1413,9 +1413,9 @@ Called to get the sound spatialization origin */ void CL_GetEntitySoundOrigin(unsigned entnum, vec3_t org) { - centity_t *ent; - mmodel_t *cm; - vec3_t mid; + const centity_t *ent; + const mmodel_t *mod; + vec3_t mid; if (entnum >= cl.csr.max_edicts) Com_Error(ERR_DROP, "%s: bad entity", __func__); @@ -1433,9 +1433,9 @@ void CL_GetEntitySoundOrigin(unsigned entnum, vec3_t org) // offset the origin for BSP models if (ent->current.solid == PACKED_BSP) { - cm = cl.model_clip[ent->current.modelindex]; - if (cm) { - VectorAvg(cm->mins, cm->maxs, mid); + mod = cl.model_clip[ent->current.modelindex]; + if (mod) { + VectorAvg(mod->mins, mod->maxs, mid); VectorAdd(org, mid, org); } } diff --git a/src/client/predict.c b/src/client/predict.c index e977fa468..6123e07e5 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -79,9 +79,9 @@ static void CL_ClipMoveToEntities(trace_t *tr, const vec3_t start, const vec3_t { int i; trace_t trace; - mnode_t *headnode; - centity_t *ent; - mmodel_t *cmodel; + const mnode_t *headnode; + const centity_t *ent; + const mmodel_t *cmodel; for (i = 0; i < cl.numSolidEntities; i++) { ent = cl.solidEntities[i]; @@ -137,10 +137,9 @@ static trace_t q_gameabi CL_PMTrace(const vec3_t start, const vec3_t mins, const static int CL_PointContents(const vec3_t point) { - int i; - centity_t *ent; - mmodel_t *cmodel; - int contents; + const centity_t *ent; + const mmodel_t *cmodel; + int i, contents; contents = CM_PointContents(point, cl.bsp->nodes); diff --git a/src/common/bsp.c b/src/common/bsp.c index 9527a8389..ed9bca616 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -820,7 +820,7 @@ static const bsp_stat_t bsp_stats[] = { static list_t bsp_cache; -static void BSP_PrintStats(bsp_t *bsp) +static void BSP_PrintStats(const bsp_t *bsp) { for (int i = 0; i < q_countof(bsp_stats); i++) { const bsp_stat_t *s = &bsp_stats[i]; @@ -828,7 +828,7 @@ static void BSP_PrintStats(bsp_t *bsp) } #if USE_REF if (bsp->lightgrid.numleafs) { - lightgrid_t *grid = &bsp->lightgrid; + const lightgrid_t *grid = &bsp->lightgrid; Com_Printf( "%8u : lightgrid styles\n" "%8u : lightgrid nodes\n" @@ -1099,7 +1099,7 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) #define FLAG_OCCLUDED BIT(30) #define FLAG_LEAF BIT(31) -lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]) +const lightgrid_sample_t *BSP_LookupLightgrid(const lightgrid_t *grid, const uint32_t point[3]) { uint32_t nodenum = grid->rootnode; @@ -1108,7 +1108,7 @@ lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]) return NULL; if (nodenum & FLAG_LEAF) { - lightgrid_leaf_t *leaf = &grid->leafs[nodenum & ~FLAG_LEAF]; + const lightgrid_leaf_t *leaf = &grid->leafs[nodenum & ~FLAG_LEAF]; uint32_t pos[3]; VectorSubtract(point, leaf->mins, pos); @@ -1122,7 +1122,7 @@ lightgrid_sample_t *BSP_LookupLightgrid(lightgrid_t *grid, uint32_t point[3]) return &grid->samples[leaf->firstsample + index * grid->numstyles]; } - lightgrid_node_t *node = &grid->nodes[nodenum]; + const lightgrid_node_t *node = &grid->nodes[nodenum]; nodenum = node->children[ (point[0] >= node->point[0]) << 2 | (point[1] >= node->point[1]) << 1 | @@ -1202,7 +1202,7 @@ static size_t BSP_ParseLightgridHeader(bsp_t *bsp, const byte *in, size_t filele ALIGN(sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles, 64); } -static bool BSP_ValidateLightgrid_r(lightgrid_t *grid, uint32_t nodenum) +static bool BSP_ValidateLightgrid_r(const lightgrid_t *grid, uint32_t nodenum) { if (nodenum & FLAG_OCCLUDED) return true; @@ -1555,7 +1555,7 @@ HELPER FUNCTIONS static lightpoint_t *light_point; static int light_mask; -static bool BSP_RecursiveLightPoint(mnode_t *node, float p1f, float p2f, const vec3_t p1, const vec3_t p2) +static bool BSP_RecursiveLightPoint(const mnode_t *node, float p1f, float p2f, const vec3_t p1, const vec3_t p2) { vec_t d1, d2, frac, midf, s, t; vec3_t mid; @@ -1611,7 +1611,7 @@ static bool BSP_RecursiveLightPoint(mnode_t *node, float p1f, float p2f, const v return false; } -void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, mnode_t *headnode, int nolm_mask) +void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, const mnode_t *headnode, int nolm_mask) { light_point = point; light_point->surf = NULL; @@ -1622,7 +1622,7 @@ void BSP_LightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, m } void BSP_TransformedLightPoint(lightpoint_t *point, const vec3_t start, const vec3_t end, - mnode_t *headnode, int nolm_mask, const vec3_t origin, const vec3_t angles) + const mnode_t *headnode, int nolm_mask, const vec3_t origin, const vec3_t angles) { vec3_t start_l, end_l; vec3_t axis[3]; @@ -1659,7 +1659,7 @@ void BSP_TransformedLightPoint(lightpoint_t *point, const vec3_t start, const ve #endif -byte *BSP_ClusterVis(bsp_t *bsp, byte *mask, int cluster, int vis) +byte *BSP_ClusterVis(const bsp_t *bsp, byte *mask, int cluster, int vis) { byte *in, *out, *in_end, *out_end; int c; @@ -1744,7 +1744,7 @@ byte *BSP_ClusterVis(bsp_t *bsp, byte *mask, int cluster, int vis) return mask; } -mleaf_t *BSP_PointLeaf(mnode_t *node, const vec3_t p) +const mleaf_t *BSP_PointLeaf(const mnode_t *node, const vec3_t p) { float d; @@ -1756,7 +1756,7 @@ mleaf_t *BSP_PointLeaf(mnode_t *node, const vec3_t p) node = node->children[0]; } - return (mleaf_t *)node; + return (const mleaf_t *)node; } /* @@ -1764,7 +1764,7 @@ mleaf_t *BSP_PointLeaf(mnode_t *node, const vec3_t p) BSP_InlineModel ================== */ -mmodel_t *BSP_InlineModel(bsp_t *bsp, const char *name) +const mmodel_t *BSP_InlineModel(const bsp_t *bsp, const char *name) { int num; diff --git a/src/common/cmodel.c b/src/common/cmodel.c index d41b326fb..f1eb26c6e 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., mtexinfo_t nulltexinfo; -static mleaf_t nullleaf; +static const mleaf_t nullleaf = { .cluster = -1 }; static unsigned floodvalid; static unsigned checkcount; @@ -40,7 +40,7 @@ static cvar_t *map_noareas; static cvar_t *map_allsolid_bug; static cvar_t *map_override_path; -static void FloodAreaConnections(cm_t *cm); +static void FloodAreaConnections(const cm_t *cm); //======================================================================= @@ -211,22 +211,22 @@ int CM_LoadMap(cm_t *cm, const char *name) return Q_ERR_SUCCESS; } -mnode_t *CM_NodeNum(cm_t *cm, int number) +const mnode_t *CM_NodeNum(const cm_t *cm, int number) { if (!cm->cache) { - return (mnode_t *)&nullleaf; + return (const mnode_t *)&nullleaf; } if (number == -1) { - return (mnode_t *)cm->cache->leafs; // special case for solid leaf + return (const mnode_t *)cm->cache->leafs; // special case for solid leaf } if (number < 0 || number >= cm->cache->numnodes) { Com_EPrintf("%s: bad number: %d\n", __func__, number); - return (mnode_t *)&nullleaf; + return (const mnode_t *)&nullleaf; } return cm->cache->nodes + number; } -mleaf_t *CM_LeafNum(cm_t *cm, int number) +const mleaf_t *CM_LeafNum(const cm_t *cm, int number) { if (!cm->cache) { return &nullleaf; @@ -314,7 +314,7 @@ To keep everything totally uniform, bounding boxes are turned into small BSP trees instead of being compared directly. =================== */ -mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs) +const mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs) { box_planes[0].dist = maxs[0]; box_planes[1].dist = -maxs[0]; @@ -332,7 +332,7 @@ mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs) return box_headnode; } -mleaf_t *CM_PointLeaf(cm_t *cm, const vec3_t p) +const mleaf_t *CM_PointLeaf(const cm_t *cm, const vec3_t p) { if (!cm->cache) { return &nullleaf; // server may call this without map loaded @@ -347,12 +347,12 @@ CM_BoxLeafnums Fills in a list of all the leafs touched ============= */ -static int leaf_count, leaf_maxcount; -static mleaf_t **leaf_list; -static const vec_t *leaf_mins, *leaf_maxs; -static mnode_t *leaf_topnode; +static int leaf_count, leaf_maxcount; +static const mleaf_t **leaf_list; +static const vec_t *leaf_mins, *leaf_maxs; +static const mnode_t *leaf_topnode; -static void CM_BoxLeafs_r(mnode_t *node) +static void CM_BoxLeafs_r(const mnode_t *node) { int s; @@ -373,13 +373,13 @@ static void CM_BoxLeafs_r(mnode_t *node) } if (leaf_count < leaf_maxcount) { - leaf_list[leaf_count++] = (mleaf_t *)node; + leaf_list[leaf_count++] = (const mleaf_t *)node; } } static int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, - mleaf_t **list, int listsize, - mnode_t *headnode, mnode_t **topnode) + const mleaf_t **list, int listsize, + const mnode_t *headnode, const mnode_t **topnode) { leaf_list = list; leaf_count = 0; @@ -397,8 +397,8 @@ static int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, return leaf_count; } -int CM_BoxLeafs(cm_t *cm, const vec3_t mins, const vec3_t maxs, - mleaf_t **list, int listsize, mnode_t **topnode) +int CM_BoxLeafs(const cm_t *cm, const vec3_t mins, const vec3_t maxs, + const mleaf_t **list, int listsize, const mnode_t **topnode) { if (!cm->cache) // map not loaded return 0; @@ -410,7 +410,7 @@ int CM_BoxLeafs(cm_t *cm, const vec3_t mins, const vec3_t maxs, CM_PointContents ================== */ -int CM_PointContents(const vec3_t p, mnode_t *headnode) +int CM_PointContents(const vec3_t p, const mnode_t *headnode) { if (!headnode) return 0; @@ -425,14 +425,14 @@ Handles offseting and rotation of the end points for moving and rotating entities ================== */ -int CM_TransformedPointContents(const vec3_t p, mnode_t *headnode, const vec3_t origin, const vec3_t angles) +int CM_TransformedPointContents(const vec3_t p, const mnode_t *headnode, + const vec3_t origin, const vec3_t angles) { vec3_t p_l; vec3_t axis[3]; - if (!headnode) { + if (!headnode) return 0; - } // subtract origin offset VectorSubtract(p, origin, p_l); @@ -470,16 +470,16 @@ static bool trace_ispoint; // optimized case CM_ClipBoxToBrush ================ */ -static void CM_ClipBoxToBrush(const vec3_t p1, const vec3_t p2, trace_t *trace, mbrush_t *brush) +static void CM_ClipBoxToBrush(const vec3_t p1, const vec3_t p2, trace_t *trace, const mbrush_t *brush) { int i; - cplane_t *plane, *clipplane; + const cplane_t *plane, *clipplane; float dist; float enterfrac, leavefrac; float d1, d2; bool getout, startout; float f; - mbrushside_t *side, *leadside; + const mbrushside_t *side, *leadside; if (!brush->numsides) return; @@ -569,13 +569,13 @@ static void CM_ClipBoxToBrush(const vec3_t p1, const vec3_t p2, trace_t *trace, CM_TestBoxInBrush ================ */ -static void CM_TestBoxInBrush(const vec3_t p1, trace_t *trace, mbrush_t *brush) +static void CM_TestBoxInBrush(const vec3_t p1, trace_t *trace, const mbrush_t *brush) { int i; - cplane_t *plane; + const cplane_t *plane; float dist; float d1; - mbrushside_t *side; + const mbrushside_t *side; if (!brush->numsides) return; @@ -608,7 +608,7 @@ static void CM_TestBoxInBrush(const vec3_t p1, trace_t *trace, mbrush_t *brush) CM_TraceToLeaf ================ */ -static void CM_TraceToLeaf(mleaf_t *leaf) +static void CM_TraceToLeaf(const mleaf_t *leaf) { int k; mbrush_t *b, **leafbrush; @@ -636,7 +636,7 @@ static void CM_TraceToLeaf(mleaf_t *leaf) CM_TestInLeaf ================ */ -static void CM_TestInLeaf(mleaf_t *leaf) +static void CM_TestInLeaf(const mleaf_t *leaf) { int k; mbrush_t *b, **leafbrush; @@ -665,9 +665,9 @@ CM_RecursiveHullCheck ================== */ -static void CM_RecursiveHullCheck(mnode_t *node, float p1f, float p2f, const vec3_t p1, const vec3_t p2) +static void CM_RecursiveHullCheck(const mnode_t *node, float p1f, float p2f, const vec3_t p1, const vec3_t p2) { - cplane_t *plane; + const cplane_t *plane; float t1, t2, offset; float frac, frac2; float idist; @@ -682,7 +682,7 @@ static void CM_RecursiveHullCheck(mnode_t *node, float p1f, float p2f, const vec // if plane is NULL, we are in a leaf node plane = node->plane; if (!plane) { - CM_TraceToLeaf((mleaf_t *)node); + CM_TraceToLeaf((const mleaf_t *)node); return; } @@ -758,7 +758,7 @@ CM_BoxTrace void CM_BoxTrace(trace_t *trace, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, - mnode_t *headnode, int brushmask) + const mnode_t *headnode, int brushmask) { const vec_t *bounds[2] = { mins, maxs }; int i, j; @@ -771,9 +771,8 @@ void CM_BoxTrace(trace_t *trace, trace_trace->fraction = 1; trace_trace->surface = &(nulltexinfo.c); - if (!headnode) { + if (!headnode) return; - } trace_contents = brushmask; VectorCopy(start, trace_start); @@ -786,9 +785,9 @@ void CM_BoxTrace(trace_t *trace, // check for position test special case // if (VectorCompare(start, end)) { - mleaf_t *leafs[1024]; - int numleafs; - vec3_t c1, c2; + const mleaf_t *leafs[1024]; + int numleafs; + vec3_t c1, c2; VectorAdd(start, mins, c1); VectorAdd(start, maxs, c2); @@ -842,7 +841,7 @@ rotating entities void CM_TransformedBoxTrace(trace_t *trace, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, - mnode_t *headnode, int brushmask, + const mnode_t *headnode, int brushmask, const vec3_t origin, const vec3_t angles) { vec3_t start_l, end_l; @@ -897,7 +896,7 @@ AREAPORTALS =============================================================================== */ -static void FloodArea_r(cm_t *cm, int number, int floodnum) +static void FloodArea_r(const cm_t *cm, int number, int floodnum) { int i; mareaportal_t *p; @@ -919,7 +918,7 @@ static void FloodArea_r(cm_t *cm, int number, int floodnum) } } -static void FloodAreaConnections(cm_t *cm) +static void FloodAreaConnections(const cm_t *cm) { int i; marea_t *area; @@ -939,7 +938,7 @@ static void FloodAreaConnections(cm_t *cm) } } -void CM_SetAreaPortalState(cm_t *cm, int portalnum, bool open) +void CM_SetAreaPortalState(const cm_t *cm, int portalnum, bool open) { if (!cm->cache) { return; @@ -954,9 +953,9 @@ void CM_SetAreaPortalState(cm_t *cm, int portalnum, bool open) FloodAreaConnections(cm); } -bool CM_AreasConnected(cm_t *cm, int area1, int area2) +bool CM_AreasConnected(const cm_t *cm, int area1, int area2) { - bsp_t *cache = cm->cache; + const bsp_t *cache = cm->cache; if (!cache) { return false; @@ -988,9 +987,9 @@ that area in the same flood as the area parameter This is used by the client refreshes to cull visibility ================= */ -int CM_WriteAreaBits(cm_t *cm, byte *buffer, int area) +int CM_WriteAreaBits(const cm_t *cm, byte *buffer, int area) { - bsp_t *cache = cm->cache; + const bsp_t *cache = cm->cache; int i; int floodnum; int bytes; @@ -1019,7 +1018,7 @@ int CM_WriteAreaBits(cm_t *cm, byte *buffer, int area) return bytes; } -int CM_WritePortalBits(cm_t *cm, byte *buffer) +int CM_WritePortalBits(const cm_t *cm, byte *buffer) { int i, bytes, numportals; @@ -1040,7 +1039,7 @@ int CM_WritePortalBits(cm_t *cm, byte *buffer) return bytes; } -void CM_SetPortalStates(cm_t *cm, const byte *buffer, int bytes) +void CM_SetPortalStates(const cm_t *cm, const byte *buffer, int bytes) { int i, numportals; @@ -1067,10 +1066,10 @@ Returns true if any leaf under headnode has a cluster that is potentially visible ============= */ -bool CM_HeadnodeVisible(mnode_t *node, byte *visbits) +bool CM_HeadnodeVisible(const mnode_t *node, const byte *visbits) { - mleaf_t *leaf; - int cluster; + const mleaf_t *leaf; + int cluster; while (node->plane) { if (CM_HeadnodeVisible(node->children[0], visbits)) @@ -1078,7 +1077,7 @@ bool CM_HeadnodeVisible(mnode_t *node, byte *visbits) node = node->children[1]; } - leaf = (mleaf_t *)node; + leaf = (const mleaf_t *)node; cluster = leaf->cluster; if (cluster == -1) return false; @@ -1095,19 +1094,20 @@ The client will interpolate the view position, so we can't use a single PVS point =========== */ -byte *CM_FatPVS(cm_t *cm, byte *mask, const vec3_t org) +byte *CM_FatPVS(const cm_t *cm, byte *mask, const vec3_t org) { + const bsp_t *bsp = cm->cache; byte temp[VIS_MAX_BYTES]; - mleaf_t *leafs[64]; + const mleaf_t *leafs[64]; int clusters[64]; int i, j, count, longs; size_t *src, *dst; vec3_t mins, maxs; - if (!cm->cache) { // map not loaded + if (!bsp) { // map not loaded return memset(mask, 0, VIS_MAX_BYTES); } - if (!cm->cache->vis) { + if (!bsp->vis) { return memset(mask, 0xff, VIS_MAX_BYTES); } @@ -1116,7 +1116,7 @@ byte *CM_FatPVS(cm_t *cm, byte *mask, const vec3_t org) maxs[i] = org[i] + 8; } - count = CM_BoxLeafs(cm, mins, maxs, leafs, q_countof(leafs), NULL); + count = CM_BoxLeafs_headnode(mins, maxs, leafs, q_countof(leafs), bsp->nodes, NULL); Q_assert(count > 0); // convert leafs to clusters @@ -1124,8 +1124,8 @@ byte *CM_FatPVS(cm_t *cm, byte *mask, const vec3_t org) clusters[i] = leafs[i]->cluster; } - BSP_ClusterVis(cm->cache, mask, clusters[0], DVIS_PVS); - longs = VIS_FAST_LONGS(cm->cache); + BSP_ClusterVis(bsp, mask, clusters[0], DVIS_PVS); + longs = VIS_FAST_LONGS(bsp); // or in all the other leaf bits for (i = 1; i < count; i++) { @@ -1134,7 +1134,7 @@ byte *CM_FatPVS(cm_t *cm, byte *mask, const vec3_t org) goto nextleaf; // already have the cluster we want } } - src = (size_t *)BSP_ClusterVis(cm->cache, temp, clusters[i], DVIS_PVS); + src = (size_t *)BSP_ClusterVis(bsp, temp, clusters[i], DVIS_PVS); dst = (size_t *)mask; for (j = 0; j < longs; j++) { *dst++ |= *src++; @@ -1155,8 +1155,6 @@ void CM_Init(void) { CM_InitBoxHull(); - nullleaf.cluster = -1; - map_noareas = Cvar_Get("map_noareas", "0", 0); map_allsolid_bug = Cvar_Get("map_allsolid_bug", "1", 0); map_override_path = Cvar_Get("map_override_path", "", 0); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index f9e6152dc..43a17641d 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -690,7 +690,7 @@ void GL_LightPoint(const vec3_t origin, vec3_t color); * gl_sky.c * */ -void R_AddSkySurface(mface_t *surf); +void R_AddSkySurface(const mface_t *surf); void R_ClearSkyBox(void); void R_DrawSkyBox(void); void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis); diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 1e301d8f7..36629285e 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -232,7 +232,7 @@ static inline void SkyInverseRotate(vec3_t out, const vec3_t in) R_AddSkySurface ================= */ -void R_AddSkySurface(mface_t *fa) +void R_AddSkySurface(const mface_t *fa) { int i; vec3_t verts[MAX_CLIP_VERTS]; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 46583e4d9..07bf5d95c 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -99,7 +99,7 @@ DYNAMIC BLOCKLIGHTS static float blocklights[MAX_BLOCKLIGHTS * 3]; -static void put_blocklights(mface_t *surf) +static void put_blocklights(const mface_t *surf) { float *bl, add, modulate, scale = lm.scale; int i, j, smax, tmax, stride = 1 << lm.block_shift; @@ -131,7 +131,7 @@ static void put_blocklights(mface_t *surf) } } -static void add_dynamic_lights(mface_t *surf) +static void add_dynamic_lights(const mface_t *surf) { dlight_t *light; vec3_t point; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 4237c9a2f..a502e9687 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -390,7 +390,7 @@ void GL_Flush3D(void) tess.flags = 0; } -static int GL_CopyVerts(mface_t *surf) +static int GL_CopyVerts(const mface_t *surf) { void *src, *dst; int firstvert; @@ -408,7 +408,7 @@ static int GL_CopyVerts(mface_t *surf) return firstvert; } -static image_t *GL_TextureAnimation(mtexinfo_t *tex) +static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) { if (q_unlikely(tex->next)) { unsigned c = (unsigned)glr.ent->frame % tex->numframes; @@ -422,7 +422,7 @@ static image_t *GL_TextureAnimation(mtexinfo_t *tex) return tex->image; } -static void GL_DrawFace(mface_t *surf) +static void GL_DrawFace(const mface_t *surf) { int numtris = surf->numsurfedges - 2; int numindices = numtris * 3; @@ -434,7 +434,7 @@ static void GL_DrawFace(mface_t *surf) texnum[0] = TEXNUM_WHITE; texnum[2] = 0; } else { - image_t *tex = GL_TextureAnimation(surf->texinfo); + const image_t *tex = GL_TextureAnimation(surf->texinfo); texnum[0] = tex->texnum; texnum[2] = surf->texnum[1] ? tex->glow_texnum : 0; } diff --git a/src/refresh/world.c b/src/refresh/world.c index f1dc12add..6107318fd 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -70,7 +70,7 @@ void GL_SampleLightPoint(vec3_t color) } } -static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t color) +static bool GL_LightGridPoint(const lightgrid_t *grid, const vec3_t start, vec3_t color) { vec3_t point, avg; uint32_t point_i[3]; @@ -94,7 +94,7 @@ static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t colo tmp[1] = point_i[1] + ((i >> 1) & 1); tmp[2] = point_i[2] + ((i >> 2) & 1); - lightgrid_sample_t *s = BSP_LookupLightgrid(grid, tmp); + const lightgrid_sample_t *s = BSP_LookupLightgrid(grid, tmp); if (!s) continue; @@ -155,13 +155,13 @@ static bool GL_LightGridPoint(lightgrid_t *grid, const vec3_t start, vec3_t colo static bool GL_LightPoint_(const vec3_t start, vec3_t color) { - bsp_t *bsp; + const bsp_t *bsp; int i, index; lightpoint_t pt; vec3_t end, mins, maxs; - entity_t *ent; - mmodel_t *model; - vec_t *angles; + const entity_t *ent; + const mmodel_t *model; + const vec_t *angles; bsp = gl_static.world.cache; if (!bsp || !bsp->lightmap) @@ -226,7 +226,7 @@ static bool GL_LightPoint_(const vec3_t start, vec3_t color) return true; } -static void GL_MarkLights_r(mnode_t *node, dlight_t *light, uint64_t lightbit) +static void GL_MarkLights_r(const mnode_t *node, const dlight_t *light, uint64_t lightbit) { vec_t dot; int count; @@ -276,7 +276,7 @@ static void GL_MarkLights(void) } } -static void GL_TransformLights(mmodel_t *model) +static void GL_TransformLights(const mmodel_t *model) { int i; dlight_t *light; @@ -343,7 +343,7 @@ static void GL_MarkLeaves(void) static int lastNodesVisible; byte vis1[VIS_MAX_BYTES]; byte vis2[VIS_MAX_BYTES]; - mleaf_t *leaf; + const mleaf_t *leaf; mnode_t *node; size_t *src1, *src2; int cluster1, cluster2, longs; @@ -524,7 +524,7 @@ void GL_DrawBspModel(mmodel_t *model) #define NODE_CLIPPED 0 #define NODE_UNCLIPPED MASK(4) -static inline bool GL_ClipNode(mnode_t *node, int *clipflags) +static inline bool GL_ClipNode(const mnode_t *node, int *clipflags) { int flags = *clipflags; int i, bits, mask; @@ -551,7 +551,7 @@ static inline bool GL_ClipNode(mnode_t *node, int *clipflags) return true; } -static inline void GL_DrawLeaf(mleaf_t *leaf) +static inline void GL_DrawLeaf(const mleaf_t *leaf) { mface_t **face, **last; @@ -570,7 +570,7 @@ static inline void GL_DrawLeaf(mleaf_t *leaf) c.leavesDrawn++; } -static inline void GL_DrawNode(mnode_t *node) +static inline void GL_DrawNode(const mnode_t *node) { mface_t *face, *last = node->firstface + node->numfaces; @@ -603,7 +603,7 @@ static inline void GL_DrawNode(mnode_t *node) c.nodesDrawn++; } -static void GL_WorldNode_r(mnode_t *node, int clipflags) +static void GL_WorldNode_r(const mnode_t *node, int clipflags) { int side; vec_t dot; @@ -615,7 +615,7 @@ static void GL_WorldNode_r(mnode_t *node, int clipflags) } if (!node->plane) { - GL_DrawLeaf((mleaf_t *)node); + GL_DrawLeaf((const mleaf_t *)node); break; } diff --git a/src/server/entities.c b/src/server/entities.c index 4e861f6c5..f1c81f168 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -324,7 +324,7 @@ Build a client frame structure #if USE_FPS static void -fix_old_origin(client_t *client, entity_packed_t *state, edict_t *ent, int e) +fix_old_origin(const client_t *client, entity_packed_t *state, const edict_t *ent, int e) { server_entity_t *sent = &sv.entities[e]; int i, j, k; @@ -367,7 +367,7 @@ fix_old_origin(client_t *client, entity_packed_t *state, edict_t *ent, int e) } #endif -static bool SV_EntityVisible(client_t *client, edict_t *ent, byte *mask) +static bool SV_EntityVisible(const client_t *client, const edict_t *ent, const byte *mask) { if (ent->num_clusters == -1) // too many leafs for individual check, go by headnode @@ -381,7 +381,7 @@ static bool SV_EntityVisible(client_t *client, edict_t *ent, byte *mask) return false; } -static bool SV_EntityAttenuatedAway(vec3_t org, edict_t *ent) +static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) { float dist = Distance(org, ent->s.origin); float dist_mult = SOUND_LOOPATTENUATE; @@ -410,7 +410,7 @@ void SV_BuildClientFrame(client_t *client) entity_packed_t *state; player_state_t *ps; int clientarea, clientcluster; - mleaf_t *leaf; + const mleaf_t *leaf; byte clientphs[VIS_MAX_BYTES]; byte clientpvs[VIS_MAX_BYTES]; bool need_clientnum_fix; diff --git a/src/server/game.c b/src/server/game.c index df1882407..53aae7bf1 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -339,8 +339,6 @@ Also sets mins and maxs for inline bmodels */ static void PF_setmodel(edict_t *ent, const char *name) { - mmodel_t *mod; - if (!ent || !name) Com_Error(ERR_DROP, "PF_setmodel: NULL"); @@ -348,7 +346,7 @@ static void PF_setmodel(edict_t *ent, const char *name) // if it is an inline model, get the size information for it if (name[0] == '*') { - mod = CM_InlineModel(&sv.cm, name); + const mmodel_t *mod = CM_InlineModel(&sv.cm, name); VectorCopy(mod->mins, ent->mins); VectorCopy(mod->maxs, ent->maxs); PF_LinkEdict(ent); @@ -444,7 +442,7 @@ static void PF_WriteFloat(float f) static qboolean PF_inVIS(const vec3_t p1, const vec3_t p2, vis_t vis) { - mleaf_t *leaf1, *leaf2; + const mleaf_t *leaf1, *leaf2; byte mask[VIS_MAX_BYTES]; leaf1 = CM_PointLeaf(&sv.cm, p1); @@ -518,7 +516,7 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, vec3_t origin_v; client_t *client; byte mask[VIS_MAX_BYTES]; - mleaf_t *leaf1, *leaf2; + const mleaf_t *leaf1, *leaf2; message_packet_t *msg; bool force_pos; diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index ad2877cb8..a4de517f5 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -127,7 +127,7 @@ static void MVD_ParseMulticast(mvd_t *mvd, mvd_ops_t op, int extrabits) mvd_client_t *client; client_t *cl; byte mask[VIS_MAX_BYTES]; - mleaf_t *leaf1 = NULL, *leaf2; + const mleaf_t *leaf1 = NULL, *leaf2; vec3_t org; bool reliable = false; byte *data; @@ -449,7 +449,7 @@ static void MVD_ParseSound(mvd_t *mvd, int extrabits) mvd_client_t *client; client_t *cl; byte mask[VIS_MAX_BYTES]; - mleaf_t *leaf1, *leaf2; + const mleaf_t *leaf1, *leaf2; message_packet_t *msg; edict_t *entity; int i; diff --git a/src/server/send.c b/src/server/send.c index 4e1af2e4c..83c3c4d34 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -29,7 +29,7 @@ MISC char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; -void SV_FlushRedirect(int redirected, char *outputbuf, size_t len) +void SV_FlushRedirect(int redirected, const char *outputbuf, size_t len) { byte buffer[MAX_PACKETLEN_DEFAULT]; @@ -257,7 +257,7 @@ void SV_Multicast(const vec3_t origin, multicast_t to) { client_t *client; byte mask[VIS_MAX_BYTES]; - mleaf_t *leaf1 = NULL, *leaf2; + const mleaf_t *leaf1 = NULL, *leaf2; int leafnum q_unused = 0; int flags = 0; diff --git a/src/server/server.h b/src/server/server.h index 19309a94a..7e9f0db9b 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -615,7 +615,7 @@ typedef enum {RD_NONE, RD_CLIENT, RD_PACKET} redirect_t; extern char sv_outputbuf[SV_OUTPUTBUF_LENGTH]; -void SV_FlushRedirect(int redirected, char *outputbuf, size_t len); +void SV_FlushRedirect(int redirected, const char *outputbuf, size_t len); void SV_SendClientMessages(void); void SV_SendAsyncPackets(void); @@ -792,7 +792,7 @@ void PF_UnlinkEdict(edict_t *ent); // call before removing an entity, and before trying to move one, // so it doesn't clip against itself -void SV_LinkEdict(cm_t *cm, edict_t *ent); +void SV_LinkEdict(const cm_t *cm, edict_t *ent); void PF_LinkEdict(edict_t *ent); // Needs to be called any time an entity changes origin, mins, maxs, // or solid. Automatically unlinks if needed. diff --git a/src/server/world.c b/src/server/world.c index 7f3edd576..7428605f1 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -127,14 +127,12 @@ General purpose routine shared between game DLL and MVD code. Links entity to PVS leafs. =============== */ -void SV_LinkEdict(cm_t *cm, edict_t *ent) +void SV_LinkEdict(const cm_t *cm, edict_t *ent) { - mleaf_t *leafs[MAX_TOTAL_ENT_LEAFS]; - int clusters[MAX_TOTAL_ENT_LEAFS]; - int num_leafs; - int i, j; - int area; - mnode_t *topnode; + const mleaf_t *leafs[MAX_TOTAL_ENT_LEAFS]; + int clusters[MAX_TOTAL_ENT_LEAFS]; + int i, j, area, num_leafs; + const mnode_t *topnode; // set the size VectorSubtract(ent->maxs, ent->mins, ent->size); @@ -234,7 +232,7 @@ void PF_UnlinkEdict(edict_t *ent) ent->area.next = ent->area.prev = NULL; } -static uint32_t SV_PackSolid32(edict_t *ent) +static uint32_t SV_PackSolid32(const edict_t *ent) { uint32_t solid32; @@ -437,10 +435,10 @@ Returns a headnode that can be used for testing or clipping an object of mins/maxs size. ================ */ -static mnode_t *SV_HullForEntity(edict_t *ent, bool triggers) +static const mnode_t *SV_HullForEntity(const edict_t *ent, bool triggers) { if (ent->solid == SOLID_BSP || (triggers && ent->solid == SOLID_TRIGGER)) { - bsp_t *bsp = sv.cm.cache; + const bsp_t *bsp = sv.cm.cache; int i = ent->s.modelindex - 1; // account for "hole" in configstring namespace @@ -464,7 +462,7 @@ static mnode_t *SV_HullForEntity(edict_t *ent, bool triggers) SV_WorldNodes ============= */ -static mnode_t *SV_WorldNodes(void) +static const mnode_t *SV_WorldNodes(void) { return sv.cm.cache ? sv.cm.cache->nodes : NULL; } From e00fdd48724c357ecadcfd21fe1db33ae3444b15 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:08:56 +0300 Subject: [PATCH 142/974] Improve SV_HullForEntity() error check. --- src/server/world.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/world.c b/src/server/world.c index 7428605f1..5c4f349d6 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -449,7 +449,7 @@ static const mnode_t *SV_HullForEntity(const edict_t *ent, bool triggers) if (i > 0 && i < bsp->nummodels) return bsp->models[i].headnode; - if (!triggers) + if (ent->solid == SOLID_BSP) Com_Error(ERR_DROP, "%s: inline model %d out of range", __func__, i); } From 60b7ffeee6ddc8383e430a4525201b3bd9379aac Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:15:28 +0300 Subject: [PATCH 143/974] Simplify sending/parsing multicasts. --- src/common/bsp.c | 2 ++ src/server/mvd.c | 27 +++++++++++++----- src/server/mvd/parse.c | 64 +++++++++++++++--------------------------- src/server/send.c | 46 +++++++++++------------------- src/server/server.h | 2 +- 5 files changed, 61 insertions(+), 80 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index ed9bca616..61281b17a 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -1664,6 +1664,8 @@ byte *BSP_ClusterVis(const bsp_t *bsp, byte *mask, int cluster, int vis) byte *in, *out, *in_end, *out_end; int c; + Q_assert(vis == DVIS_PVS || vis == DVIS_PHS); + if (!bsp || !bsp->vis) { return memset(mask, 0xff, VIS_MAX_BYTES); } diff --git a/src/server/mvd.c b/src/server/mvd.c index a61dbf617..a3fb270af 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -1110,10 +1110,13 @@ out-of-band data into the MVD stream. /* ============== SV_MvdMulticast + +`to' must be < MULTICAST_ALL_R. ============== */ -void SV_MvdMulticast(int leafnum, multicast_t to) +void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable) { + int leafnum; mvd_ops_t op; sizebuf_t *buf; @@ -1121,22 +1124,32 @@ void SV_MvdMulticast(int leafnum, multicast_t to) if (!mvd.active) { return; } + if (msg_write.cursize >= 2048) { Com_WPrintf("%s: overflow\n", __func__); return; } - if (leafnum >= UINT16_MAX) { - Com_WPrintf("%s: leafnum out of range\n", __func__); - return; + + if (to) { + leafnum = CM_NumLeaf(&sv.cm, leaf); + if (leafnum >= UINT16_MAX) { + Com_WPrintf("%s: leafnum out of range\n", __func__); + return; + } } - op = mvd_multicast_all + to; - buf = to < MULTICAST_ALL_R ? &mvd.datagram : &mvd.message; + if (reliable) { + op = mvd_multicast_all_r + to; + buf = &mvd.datagram; + } else { + op = mvd_multicast_all + to; + buf = &mvd.message; + } SZ_WriteByte(buf, op | (msg_write.cursize >> 8 << SVCMD_BITS)); SZ_WriteByte(buf, msg_write.cursize & 255); - if (op != mvd_multicast_all && op != mvd_multicast_all_r) { + if (to) { SZ_WriteShort(buf, leafnum); } diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index a4de517f5..fd285fda8 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -122,50 +122,28 @@ void MVD_ParseEntityString(mvd_t *mvd, const char *data) } } -static void MVD_ParseMulticast(mvd_t *mvd, mvd_ops_t op, int extrabits) +static void MVD_ParseMulticast(mvd_t *mvd, multicast_t to, int extrabits) { mvd_client_t *client; - client_t *cl; - byte mask[VIS_MAX_BYTES]; - const mleaf_t *leaf1 = NULL, *leaf2; - vec3_t org; - bool reliable = false; - byte *data; - int length, leafnum; + client_t *cl; + byte mask[VIS_MAX_BYTES]; + const mleaf_t *leaf1, *leaf2; + vec3_t org; + bool reliable = false; + byte *data; + int length, leafnum; length = MSG_ReadByte(); length |= extrabits << 8; - switch (op) { - case mvd_multicast_all_r: + if (to >= MULTICAST_ALL_R) { reliable = true; - // intentional fallthrough - case mvd_multicast_all: - break; - case mvd_multicast_phs_r: - reliable = true; - // intentional fallthrough - case mvd_multicast_phs: - leafnum = MSG_ReadWord(); - if (!mvd->demoseeking) { - leaf1 = CM_LeafNum(&mvd->cm, leafnum); - BSP_ClusterVis(mvd->cm.cache, mask, leaf1->cluster, DVIS_PHS); - } - break; - case mvd_multicast_pvs_r: - reliable = true; - // intentional fallthrough - case mvd_multicast_pvs: - leafnum = MSG_ReadWord(); - if (!mvd->demoseeking) { - leaf1 = CM_LeafNum(&mvd->cm, leafnum); - BSP_ClusterVis(mvd->cm.cache, mask, leaf1->cluster, DVIS_PVS); - } - break; - default: - MVD_Destroyf(mvd, "bad op"); + to -= MULTICAST_ALL_R; } + if (to) + leafnum = MSG_ReadWord(); + // skip data payload data = MSG_ReadData(length); if (!data) { @@ -175,6 +153,11 @@ static void MVD_ParseMulticast(mvd_t *mvd, mvd_ops_t op, int extrabits) if (mvd->demoseeking) return; + if (to) { + leaf1 = CM_LeafNum(&mvd->cm, leafnum); + BSP_ClusterVis(mvd->cm.cache, mask, leaf1->cluster, MULTICAST_PVS - to); + } + // send the data to all relevent clients FOR_EACH_MVDCL(client, mvd) { cl = client->cl; @@ -187,7 +170,7 @@ static void MVD_ParseMulticast(mvd_t *mvd, mvd_ops_t op, int extrabits) continue; } - if (leaf1) { + if (to) { VectorScale(client->ps.pmove.origin, 0.125f, org); leaf2 = CM_PointLeaf(&mvd->cm, org); if (!CM_AreasConnected(&mvd->cm, leaf1->area, leaf2->area)) @@ -368,13 +351,12 @@ MVD_ParseUnicast Attempt to parse the datagram and find custom configstrings, layouts, etc. Give up as soon as unknown command byte is encountered. */ -static void MVD_ParseUnicast(mvd_t *mvd, mvd_ops_t op, int extrabits) +static void MVD_ParseUnicast(mvd_t *mvd, bool reliable, int extrabits) { int clientNum; uint32_t length, last; mvd_player_t *player; byte *data; - bool reliable; int cmd; length = MSG_ReadByte(); @@ -392,8 +374,6 @@ static void MVD_ParseUnicast(mvd_t *mvd, mvd_ops_t op, int extrabits) player = &mvd->players[clientNum]; - reliable = op == mvd_unicast_r; - while (msg_read.readcount < last) { cmd = MSG_ReadByte(); @@ -1095,11 +1075,11 @@ bool MVD_ParseMessage(mvd_t *mvd) case mvd_multicast_all_r: case mvd_multicast_pvs_r: case mvd_multicast_phs_r: - MVD_ParseMulticast(mvd, cmd, extrabits); + MVD_ParseMulticast(mvd, cmd - mvd_multicast_all, extrabits); break; case mvd_unicast: case mvd_unicast_r: - MVD_ParseUnicast(mvd, cmd, extrabits); + MVD_ParseUnicast(mvd, cmd == mvd_unicast_r, extrabits); break; case mvd_configstring: MVD_ParseConfigstring(mvd); diff --git a/src/server/send.c b/src/server/send.c index 83c3c4d34..1abac06a3 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -255,36 +255,22 @@ MULTICAST_PHS send to clients potentially hearable from org */ void SV_Multicast(const vec3_t origin, multicast_t to) { - client_t *client; - byte mask[VIS_MAX_BYTES]; - const mleaf_t *leaf1 = NULL, *leaf2; - int leafnum q_unused = 0; - int flags = 0; + client_t *client; + byte mask[VIS_MAX_BYTES]; + const mleaf_t *leaf1 = NULL; + int flags = 0; - switch (to) { - case MULTICAST_ALL_R: - flags |= MSG_RELIABLE; - // intentional fallthrough - case MULTICAST_ALL: - break; - case MULTICAST_PHS_R: - flags |= MSG_RELIABLE; - // intentional fallthrough - case MULTICAST_PHS: - leaf1 = CM_PointLeaf(&sv.cm, origin); - leafnum = CM_NumLeaf(&sv.cm, leaf1); - BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, DVIS_PHS); - break; - case MULTICAST_PVS_R: + if (to < MULTICAST_ALL || to > MULTICAST_PVS_R) + Com_Error(ERR_DROP, "%s: bad to: %d", __func__, to); + + if (to >= MULTICAST_ALL_R) { flags |= MSG_RELIABLE; - // intentional fallthrough - case MULTICAST_PVS: + to -= MULTICAST_ALL_R; + } + + if (to) { leaf1 = CM_PointLeaf(&sv.cm, origin); - leafnum = CM_NumLeaf(&sv.cm, leaf1); - BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, DVIS_PVS); - break; - default: - Com_Error(ERR_DROP, "SV_Multicast: bad to: %i", to); + BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, MULTICAST_PVS - to); } // send the data to all relevent clients @@ -297,8 +283,8 @@ void SV_Multicast(const vec3_t origin, multicast_t to) continue; } - if (leaf1) { - leaf2 = CM_PointLeaf(&sv.cm, client->edict->s.origin); + if (to) { + const mleaf_t *leaf2 = CM_PointLeaf(&sv.cm, client->edict->s.origin); if (!CM_AreasConnected(&sv.cm, leaf1->area, leaf2->area)) continue; if (leaf2->cluster == -1) @@ -311,7 +297,7 @@ void SV_Multicast(const vec3_t origin, multicast_t to) } // add to MVD datagram - SV_MvdMulticast(leafnum, to); + SV_MvdMulticast(leaf1, to, flags & MSG_RELIABLE); // clear the buffer SZ_Clear(&msg_write); diff --git a/src/server/server.h b/src/server/server.h index 7e9f0db9b..abbc44573 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -645,7 +645,7 @@ void SV_MvdMapChanged(void); void SV_MvdClientDropped(client_t *client); void SV_MvdUnicast(edict_t *ent, int clientNum, bool reliable); -void SV_MvdMulticast(int leafnum, multicast_t to); +void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable); void SV_MvdConfigstring(int index, const char *string, size_t len); void SV_MvdBroadcastPrint(int level, const char *string); void SV_MvdStartSound(int entnum, int channel, int flags, From 90cb2bb335e9a473407892e68c44af13d0fd07d3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:28:47 +0300 Subject: [PATCH 144/974] Simplify GL_MarkLeaves(). --- src/refresh/world.c | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/refresh/world.c b/src/refresh/world.c index 6107318fd..023f7d148 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -346,10 +346,9 @@ static void GL_MarkLeaves(void) const mleaf_t *leaf; mnode_t *node; size_t *src1, *src2; - int cluster1, cluster2, longs; + int i, cluster1, cluster2, longs; vec3_t tmp; - int i; - bsp_t *bsp = gl_static.world.cache; + const bsp_t *bsp = gl_static.world.cache; leaf = BSP_PointLeaf(bsp->nodes, glr.fd.vieworg); cluster1 = cluster2 = leaf->cluster; @@ -405,24 +404,18 @@ static void GL_MarkLeaves(void) if (cluster1 == -1) { continue; } - if (Q_IsBitSet(vis1, cluster1)) { - node = (mnode_t *)leaf; - - // mark parent nodes visible - do { - if (node->visframe == glr.visframe) { - break; - } - node->visframe = glr.visframe; - node = node->parent; - lastNodesVisible++; - } while (node); + if (!Q_IsBitSet(vis1, cluster1)) { + continue; + } + // mark parent nodes visible + for (node = (mnode_t *)leaf; node && node->visframe != glr.visframe; node = node->parent) { + node->visframe = glr.visframe; + lastNodesVisible++; } } finish: c.nodesVisible = lastNodesVisible; - } #define BACKFACE_EPSILON 0.01f From d6336e2ab20122d6f5fe36b9bd88449284ecf6f3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:29:53 +0300 Subject: [PATCH 145/974] Add LM_PIXELS() macro. --- src/refresh/surf.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 07bf5d95c..f8832e0b5 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -97,6 +97,8 @@ DYNAMIC BLOCKLIGHTS #define MAX_LIGHTMAP_EXTENTS 513 #define MAX_BLOCKLIGHTS (MAX_LIGHTMAP_EXTENTS * MAX_LIGHTMAP_EXTENTS) +#define LM_PIXELS(map, s, t) ((map)->buffer + ((t) << lm.block_shift) + ((s) << 2)) + static float blocklights[MAX_BLOCKLIGHTS * 3]; static void put_blocklights(const mface_t *surf) @@ -116,7 +118,7 @@ static void put_blocklights(const mface_t *surf) smax = surf->lm_width; tmax = surf->lm_height; - out = surf->light_m->buffer + (surf->light_t << lm.block_shift) + surf->light_s * 4; + out = LM_PIXELS(surf->light_m, surf->light_s, surf->light_t); for (i = 0, bl = blocklights; i < tmax; i++, out += stride) { byte *dst; @@ -332,8 +334,7 @@ void GL_UploadLightmaps(void) // upload lightmap subimage GL_ForceTexture(1, lm.texnums[i]); qglTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, - GL_RGBA, GL_UNSIGNED_BYTE, - m->buffer + (y << lm.block_shift) + x * 4); + GL_RGBA, GL_UNSIGNED_BYTE, LM_PIXELS(m, x, y)); clear_dirty_region(m); c.texUploads++; c.lightTexels += w * h; From 89eddd38be66dc9b221700bd93bc85908d7dfda6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:30:16 +0300 Subject: [PATCH 146/974] Remove unused argument. --- src/refresh/surf.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index f8832e0b5..6043ca32b 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -469,7 +469,7 @@ static void build_primary_lightmap(mface_t *surf) put_blocklights(surf); } -static void LM_BuildSurface(mface_t *surf, vec_t *vbo) +static void LM_BuildSurface(mface_t *surf) { int smax, tmax, s, t; @@ -770,7 +770,7 @@ static void build_surface_light(mface_t *surf, vec_t *vbo) if (gl_vertexlight->integer) sample_surface_verts(surf, vbo); else - LM_BuildSurface(surf, vbo); + LM_BuildSurface(surf); } // normalizes and stores lightmap texture coordinates in vertices From 644e85c8279e300ce193c581cd6919fe29588012 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Apr 2024 03:31:30 +0300 Subject: [PATCH 147/974] Check for R_NOTEXTURE in R_SetSky(). --- src/refresh/sky.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 36629285e..a9017cb0b 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -402,7 +402,7 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis } FS_NormalizePath(pathname); image = IMG_Find(pathname, IT_SKY, IF_NONE); - if (image->texnum == TEXNUM_DEFAULT) { + if (image == R_NOTEXTURE) { R_UnsetSky(); return; } From 620bd4fcd99d104fd56708b8d6cbd495e37e0109 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 26 Apr 2024 21:42:16 +0300 Subject: [PATCH 148/974] Use vector macros for blocklight updates. --- src/refresh/surf.c | 25 +++++++------------------ src/refresh/world.c | 5 +---- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 6043ca32b..86b284a80 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -182,9 +182,7 @@ static void add_dynamic_lights(const mface_t *surf) dist = td + sd * 0.5f; if (dist < minlight) { frac = rad - dist * scale; - bl[0] += light->color[0] * frac; - bl[1] += light->color[1] * frac; - bl[2] += light->color[2] * frac; + VectorMA(bl, frac, light->color, bl); } bl += 3; } @@ -211,17 +209,11 @@ static void add_light_styles(mface_t *surf) src = surf->lightmap; bl = blocklights; if (style->white == 1) { - for (j = 0; j < size; j++, bl += 3, src += 3) { - bl[0] = src[0]; - bl[1] = src[1]; - bl[2] = src[2]; - } + for (j = 0; j < size; j++, bl += 3, src += 3) + VectorCopy(src, bl); } else { - for (j = 0; j < size; j++, bl += 3, src += 3) { - bl[0] = src[0] * style->white; - bl[1] = src[1] * style->white; - bl[2] = src[2] * style->white; - } + for (j = 0; j < size; j++, bl += 3, src += 3) + VectorScale(src, style->white, bl); } surf->stylecache[0] = style->white; @@ -231,11 +223,8 @@ static void add_light_styles(mface_t *surf) style = LIGHT_STYLE(surf->styles[i]); bl = blocklights; - for (j = 0; j < size; j++, bl += 3, src += 3) { - bl[0] += src[0] * style->white; - bl[1] += src[1] * style->white; - bl[2] += src[2] * style->white; - } + for (j = 0; j < size; j++, bl += 3, src += 3) + VectorMA(bl, style->white, src, bl); surf->stylecache[i] = style->white; } diff --git a/src/refresh/world.c b/src/refresh/world.c index 023f7d148..99719bad5 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -61,10 +61,7 @@ void GL_SampleLightPoint(vec3_t color) temp[2] = w1 * b1[2] + w2 * b2[2] + w3 * b3[2] + w4 * b4[2]; style = LIGHT_STYLE(surf->styles[i]); - - color[0] += temp[0] * style->white; - color[1] += temp[1] * style->white; - color[2] += temp[2] * style->white; + VectorMA(color, style->white, temp, color); lightmap += size; } From d072f3699e7e54c9cd9c116fde0d902b2d59496e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 29 Apr 2024 15:15:33 +0300 Subject: [PATCH 149/974] Print BSP extensions on single line. --- src/common/bsp.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 61281b17a..52b21c3df 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -822,10 +822,13 @@ static list_t bsp_cache; static void BSP_PrintStats(const bsp_t *bsp) { + bool extended = bsp->extended; + for (int i = 0; i < q_countof(bsp_stats); i++) { const bsp_stat_t *s = &bsp_stats[i]; Com_Printf("%8d : %s\n", *(int *)((byte *)bsp + s->ofs), s->name); } + #if USE_REF if (bsp->lightgrid.numleafs) { const lightgrid_t *grid = &bsp->lightgrid; @@ -836,11 +839,20 @@ static void BSP_PrintStats(const bsp_t *bsp) "%8u : lightgrid samples\n", grid->numstyles, grid->numnodes, grid->numleafs, grid->numsamples); } - if (bsp->lm_decoupled) - Com_Printf("DECOUPLED_LM lump present\n"); + extended |= bsp->lm_decoupled; #endif - if (bsp->extended) - Com_Printf("QBSP extended format\n"); + + if (extended) { + Com_Printf("Features :"); + if (bsp->extended) + Com_Printf(" QBSP"); +#if USE_REF + if (bsp->lm_decoupled) + Com_Printf(" DECOUPLED_LM"); +#endif + Com_Printf("\n"); + } + Com_Printf("------------------\n"); } From d6f5601e1bbe5a7354ee609f56ab2920b0d48dc9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 20:13:26 +0300 Subject: [PATCH 150/974] Simplify mins/maxs calculation. --- src/common/cmodel.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/cmodel.c b/src/common/cmodel.c index f1eb26c6e..8d412eae7 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -789,11 +789,9 @@ void CM_BoxTrace(trace_t *trace, int numleafs; vec3_t c1, c2; - VectorAdd(start, mins, c1); - VectorAdd(start, maxs, c2); for (i = 0; i < 3; i++) { - c1[i] -= 1; - c2[i] += 1; + c1[i] = start[i] + mins[i] - 1; + c2[i] = start[i] + maxs[i] + 1; } numleafs = CM_BoxLeafs_headnode(c1, c2, leafs, q_countof(leafs), headnode, NULL); From 3277e899e099030be021d807651fedc0d8163dcf Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 20:26:02 +0300 Subject: [PATCH 151/974] Avoid using separate variable for BSP cache. --- src/common/cmodel.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/common/cmodel.c b/src/common/cmodel.c index 8d412eae7..abb66a90f 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -953,9 +953,7 @@ void CM_SetAreaPortalState(const cm_t *cm, int portalnum, bool open) bool CM_AreasConnected(const cm_t *cm, int area1, int area2) { - const bsp_t *cache = cm->cache; - - if (!cache) { + if (!cm->cache) { return false; } if (map_noareas->integer) { @@ -964,7 +962,7 @@ bool CM_AreasConnected(const cm_t *cm, int area1, int area2) if (area1 < 1 || area2 < 1) { return false; } - if (area1 >= cache->numareas || area2 >= cache->numareas) { + if (area1 >= cm->cache->numareas || area2 >= cm->cache->numareas) { Com_EPrintf("%s: area > numareas\n", __func__); return false; } @@ -987,16 +985,15 @@ This is used by the client refreshes to cull visibility */ int CM_WriteAreaBits(const cm_t *cm, byte *buffer, int area) { - const bsp_t *cache = cm->cache; int i; int floodnum; int bytes; - if (!cache) { + if (!cm->cache) { return 0; } - bytes = (cache->numareas + 7) >> 3; + bytes = (cm->cache->numareas + 7) >> 3; Q_assert(bytes <= MAX_MAP_AREA_BYTES); if (map_noareas->integer || !area) { @@ -1006,7 +1003,7 @@ int CM_WriteAreaBits(const cm_t *cm, byte *buffer, int area) memset(buffer, 0, bytes); floodnum = cm->floodnums[area]; - for (i = 0; i < cache->numareas; i++) { + for (i = 0; i < cm->cache->numareas; i++) { if (cm->floodnums[i] == floodnum) { Q_SetBit(buffer, i); } From d490a77ca26414a05283c9eeaf8143f175beec92 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 21:04:42 +0300 Subject: [PATCH 152/974] Fix crash in SV_HullForEntity() if no map is loaded. --- src/server/world.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/server/world.c b/src/server/world.c index 5c4f349d6..28930398a 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -441,13 +441,15 @@ static const mnode_t *SV_HullForEntity(const edict_t *ent, bool triggers) const bsp_t *bsp = sv.cm.cache; int i = ent->s.modelindex - 1; - // account for "hole" in configstring namespace - if (i >= MODELINDEX_PLAYER && bsp->nummodels >= MODELINDEX_PLAYER) - i--; - - // explicit hulls in the BSP model - if (i > 0 && i < bsp->nummodels) - return bsp->models[i].headnode; + if (bsp) { + // account for "hole" in configstring namespace + if (i >= MODELINDEX_PLAYER && bsp->nummodels >= MODELINDEX_PLAYER) + i--; + + // explicit hulls in the BSP model + if (i > 0 && i < bsp->nummodels) + return bsp->models[i].headnode; + } if (ent->solid == SOLID_BSP) Com_Error(ERR_DROP, "%s: inline model %d out of range", __func__, i); From ae2a9a71e342e1030bbba45732825ac82859bb0d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 22:36:55 +0300 Subject: [PATCH 153/974] Apply some compilation/runtime fixes for macOS. Suggested by kke in #158. Untested. --- inc/shared/platform.h | 2 ++ meson.build | 1 + src/client/sound/qal.c | 6 ++---- src/client/sound/qal.h | 6 ------ 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/inc/shared/platform.h b/inc/shared/platform.h index 2b3b7fe8e..f5043cb38 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -34,6 +34,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #ifdef _WIN32 #define LIBSUFFIX ".dll" +#elif (defined __APPLE__) +#define LIBSUFFIX ".dylib" #else #define LIBSUFFIX ".so" #endif diff --git a/meson.build b/meson.build index 050b9bf2d..c1ff9b937 100644 --- a/meson.build +++ b/meson.build @@ -314,6 +314,7 @@ if jpeg.found() endif openal = dependency('openal', + version: '>= 1.18.0', required: get_option('openal'), default_options: fallback_opt, ) diff --git a/src/client/sound/qal.c b/src/client/sound/qal.c index ebe610ce4..81f7ba05a 100644 --- a/src/client/sound/qal.c +++ b/src/client/sound/qal.c @@ -22,11 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/common.h" #include "common/files.h" -#ifdef __APPLE__ -#include -#else #include -#endif #define QALAPI #include "qal.h" @@ -142,6 +138,8 @@ void QAL_Shutdown(void) static const char *const al_drivers[] = { #ifdef _WIN32 "soft_oal", "openal32" +#elif (defined __APPLE__) + "libopenal.1.dylib", "libopenal.dylib" #else "libopenal.so.1", "libopenal.so" #endif diff --git a/src/client/sound/qal.h b/src/client/sound/qal.h index deb2f01b7..170a90d75 100644 --- a/src/client/sound/qal.h +++ b/src/client/sound/qal.h @@ -20,15 +20,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #define AL_NO_PROTOTYPES -#ifdef __APPLE__ -#include -#include -#include -#else #include #include #include -#endif #ifndef QALAPI #define QALAPI extern From 1d57efd2fb1a624c1eb7f75292337eb1b0e53124 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 22:37:22 +0300 Subject: [PATCH 154/974] Add macOS target to CI workflow. --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45911fd7b..28c1bfa84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,14 @@ env: -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback + MESON_ARGS_MACOS: >- + -Dicmp-errors=disabled + -Dpkg_config_path=/opt/homebrew/opt/openal-soft/lib/pkgconfig + -Dwayland=disabled + -Dwindows-crash-dumps=disabled + -Dwrap_mode=nofallback + -Dx11=disabled + jobs: mingw: runs-on: ubuntu-latest @@ -103,3 +111,18 @@ jobs: meson compile -C builddir env: CC: ${{ matrix.cc }} + + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + brew update + brew install meson sdl2 openal-soft libpng jpeg-turbo + + - name: Build + run: | + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_MACOS }} builddir + meson compile -C builddir From 128d0f2683d8f3f93d75dbd7f7885e9fb5c57c9c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 2 May 2024 23:58:44 +0300 Subject: [PATCH 155/974] Update INSTALL.md. --- INSTALL.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 0fb5c5f73..7c9f05291 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -19,6 +19,10 @@ Linux distributions already provide libjpeg-turbo in place of libjpeg. For playing back cinematics in Ogg Theora format and music in Ogg Vorbis format FFmpeg libraries are required. +OpenAL sound backend requires OpenAL Soft development headers for compilation. +At runtime, OpenAL library from any vendor can be used (but OpenAL Soft is +strongly recommended). + To install the *full* set of dependencies for building Q2PRO on Debian or Ubuntu use the following command: @@ -29,6 +33,11 @@ Ubuntu use the following command: libavcodec-dev libavformat-dev libavutil-dev \ libswresample-dev libswscale-dev +If you intend to build just dedicated server, smaller set of dependencies can +be installed: + + apt-get install meson gcc libc6-dev zlib1g-dev + Users of other distributions should look for equivalent development packages and install them. From 6a94eefab22439f872a7847585437393b7b6e128 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 3 May 2024 20:04:43 +0300 Subject: [PATCH 156/974] Fix indentation. --- src/server/mvd/client.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index f781b0994..b259f7279 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -254,20 +254,20 @@ mvd_t *MVD_SetChannel(int arg) } } else #endif - if (COM_IsUint(s)) { - id = Q_atoi(s); - FOR_EACH_MVD(mvd) { - if (mvd->id == id) { - return mvd; - } + if (COM_IsUint(s)) { + id = Q_atoi(s); + FOR_EACH_MVD(mvd) { + if (mvd->id == id) { + return mvd; } - } else { - FOR_EACH_MVD(mvd) { - if (!strcmp(mvd->name, s)) { - return mvd; - } + } + } else { + FOR_EACH_MVD(mvd) { + if (!strcmp(mvd->name, s)) { + return mvd; } } + } Com_Printf("No such channel ID: %s\n", s); return NULL; From 6f24d6e3e8f1a442d13fe983839ed2194164f7be Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 3 May 2024 20:33:31 +0300 Subject: [PATCH 157/974] Rename macro for readability. --- src/server/main.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/main.c b/src/server/main.c index 5067fe665..4e07b0789 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -631,7 +631,7 @@ typedef struct { // small hack to permit one-line return statement :) #define reject(...) reject_printf(__VA_ARGS__), false -#define reject2(...) reject_printf(__VA_ARGS__), NULL +#define reject_ptr(...) reject_printf(__VA_ARGS__), NULL static bool parse_basic_params(conn_params_t *p) { @@ -955,13 +955,13 @@ static client_t *find_client_slot(conn_params_t *params) // clients that know the password are never redirected if (sv_reserved_slots->integer != params->reserved) - return reject2("Server and reserved slots are full.\n"); + return reject_ptr("Server and reserved slots are full.\n"); // optionally redirect them to a different address if (*s) return redirect(s); - return reject2("Server is full.\n"); + return reject_ptr("Server is full.\n"); } static void init_pmove_and_es_flags(client_t *newcl) From c067dc2504ec41256101aee8d9dfe8ea2e7051fb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 3 May 2024 20:34:44 +0300 Subject: [PATCH 158/974] Replace !USE_CLIENT with USE_SERVER. --- src/common/common.c | 4 ++-- src/common/msg.c | 2 +- src/server/commands.c | 4 ++-- src/server/init.c | 4 ++-- src/server/main.c | 4 ++-- src/server/server.h | 2 +- src/windows/debug.c | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/common/common.c b/src/common/common.c index 0eacc5000..08ba88270 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -611,7 +611,7 @@ static void Com_Quit_f(void) Com_Quit(Cmd_Args(), ERR_DISCONNECT); } -#if !USE_CLIENT +#if USE_SERVER static void Com_Recycle_f(void) { Com_Quit(Cmd_Args(), ERR_RECONNECT); @@ -958,7 +958,7 @@ void Qcommon_Init(int argc, char **argv) Cmd_AddCommand("lasterror", Com_LastError_f); Cmd_AddCommand("quit", Com_Quit_f); -#if !USE_CLIENT +#if USE_SERVER Cmd_AddCommand("recycle", Com_Recycle_f); #endif diff --git a/src/common/msg.c b/src/common/msg.c index a79c450c3..47f7af860 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -1546,7 +1546,7 @@ static inline float MSG_ReadCoord(void) return SHORT2COORD(MSG_ReadShort()); } -#if !USE_CLIENT +#if USE_SERVER static inline #endif void MSG_ReadPos(vec3_t pos) diff --git a/src/server/commands.c b/src/server/commands.c index a6d3f6962..53a44bd8d 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -331,7 +331,7 @@ static void SV_GameMap_f(void) return; } -#if !USE_CLIENT +#if USE_SERVER // admin option to reload the game DLL or entire server if (sv_recycle->integer > 0) { if (sv_recycle->integer > 1) { @@ -352,7 +352,7 @@ static int should_really_restart(void) if (sv.state < ss_game || sv.state == ss_broadcast) return 1; // the game is just starting -#if !USE_CLIENT +#if USE_SERVER if (sv_recycle->integer) return 1; // there is recycle pending #endif diff --git a/src/server/init.c b/src/server/init.c index 55a8508dd..923aab45c 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -53,7 +53,7 @@ static void set_frame_time(void) static void resolve_masters(void) { -#if !USE_CLIENT +#if USE_SERVER time_t now = time(NULL); for (int i = 0; i < MAX_MASTERS; i++) { @@ -368,7 +368,7 @@ void SV_InitGame(unsigned mvd_spawn) // get any latched variable changes (maxclients, etc) Cvar_GetLatchedVars(); -#if !USE_CLIENT +#if USE_SERVER Cvar_Reset(sv_recycle); #endif diff --git a/src/server/main.c b/src/server/main.c index 4e07b0789..d4c5891c9 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -88,7 +88,7 @@ cvar_t *sv_packetdup_hack; #endif cvar_t *sv_allow_map; cvar_t *sv_cinematics; -#if !USE_CLIENT +#if USE_SERVER cvar_t *sv_recycle; #endif cvar_t *sv_enhanced_setplayer; @@ -2228,7 +2228,7 @@ void SV_Init(void) sv_allow_map = Cvar_Get("sv_allow_map", "0", 0); sv_cinematics = Cvar_Get("sv_cinematics", "1", 0); -#if !USE_CLIENT +#if USE_SERVER sv_recycle = Cvar_Get("sv_recycle", "0", 0); #endif diff --git a/src/server/server.h b/src/server/server.h index abbc44573..cc77bafb3 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -540,7 +540,7 @@ extern cvar_t *sv_packetdup_hack; #endif extern cvar_t *sv_allow_map; extern cvar_t *sv_cinematics; -#if !USE_CLIENT +#if USE_SERVER extern cvar_t *sv_recycle; #endif extern cvar_t *sv_enhanced_setplayer; diff --git a/src/windows/debug.c b/src/windows/debug.c index 1d34afded..6f155f5e5 100644 --- a/src/windows/debug.c +++ b/src/windows/debug.c @@ -223,7 +223,7 @@ static LONG WINAPI exception_filter(LPEXCEPTION_POINTERS exceptionInfo) "Would you like to generate a crash report?", CRASH_TITLE, MB_ICONERROR | MB_YESNO -#if !USE_CLIENT +#if USE_SERVER | MB_SERVICE_NOTIFICATION #endif ); From e9c5471724befbde85e119e04d74b180750ae8e3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 4 May 2024 14:34:36 +0300 Subject: [PATCH 159/974] Don't list music from packfiles. --- src/client/sound/ogg.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index a2986b4b0..1d1fc3a2e 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -662,7 +662,7 @@ bool OGG_Load(sizebuf_t *sz) void OGG_LoadTrackList(void) { FS_FreeList(tracklist); - tracklist = FS_ListFiles("music", extensions, FS_SEARCH_STRIPEXT, &trackcount); + tracklist = FS_ListFiles("music", extensions, FS_SEARCH_STRIPEXT | FS_TYPE_REAL, &trackcount); trackindex = 0; } @@ -715,7 +715,7 @@ static void OGG_Cmd_c(genctx_t *ctx, int argnum) } if (argnum == 2 && !strcmp(Cmd_Argv(1), "play")) - FS_File_g("music", extensions, FS_SEARCH_STRIPEXT, ctx); + FS_File_g("music", extensions, FS_SEARCH_STRIPEXT | FS_TYPE_REAL, ctx); } static void OGG_Cmd_f(void) From 700e54754471a7c455ed365a322b38049a37f52f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 4 May 2024 21:24:11 +0300 Subject: [PATCH 160/974] Fix building Wayland video driver on FreeBSD. --- src/unix/video/meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/unix/video/meson.build b/src/unix/video/meson.build index ad6b4e16e..badc32709 100644 --- a/src/unix/video/meson.build +++ b/src/unix/video/meson.build @@ -40,7 +40,7 @@ foreach dep: wayland_deps endif endforeach -if not cc.has_header('linux/input-event-codes.h', required: wayland_opt) +if not cc.has_header('linux/input-event-codes.h', dependencies: wayland_deps[0], required: wayland_opt) subdir_done() endif @@ -67,7 +67,7 @@ foreach p: wayland_protocols ) endforeach -wl_protocols_lib = static_library('wl_protocols', wl_protocols_source + wl_protocols_headers) +wl_protocols_lib = static_library('wl_protocols', wl_protocols_source + wl_protocols_headers, dependencies: wayland_deps[0]) wl_protocols_dep = declare_dependency(link_with: wl_protocols_lib, sources: wl_protocols_headers) client_src += files('wayland.c', 'keytables/evdev.c') From c0d2fb82b05970dffe8c6ef7e92512e3301a3e70 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 4 May 2024 21:58:37 +0300 Subject: [PATCH 161/974] Rename ALIGN macro to Q_ALIGN. FreeBSD's machine/param.h defines ALIGN. --- inc/common/files.h | 2 +- inc/shared/shared.h | 2 +- src/client/demo.c | 2 +- src/client/http.c | 2 +- src/client/sound/dma.c | 2 +- src/client/sound/mem.c | 4 ++-- src/client/ui/menu.c | 2 +- src/client/ui/script.c | 2 +- src/common/bsp.c | 10 +++++----- src/common/files.c | 2 +- src/common/prompt.c | 2 +- src/refresh/texture.c | 2 +- src/server/mvd/client.c | 2 +- src/unix/hunk.c | 6 +++--- src/windows/hunk.c | 6 +++--- 15 files changed, 24 insertions(+), 24 deletions(-) diff --git a/inc/common/files.h b/inc/common/files.h index c221e1b6e..27edbdba2 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -97,7 +97,7 @@ bool FS_ExtCmp(const char *extension, const char *string); const char *FS_NextPath(const char *path); #define FS_ReallocList(list, count) \ - Z_Realloc(list, ALIGN(count, MIN_LISTED_FILES) * sizeof(void *)) + Z_Realloc(list, Q_ALIGN(count, MIN_LISTED_FILES) * sizeof(void *)) void **FS_ListFiles(const char *path, const char *filter, unsigned flags, int *count_p); void **FS_CopyList(void **list, int count); diff --git a/inc/shared/shared.h b/inc/shared/shared.h index c3b570fa5..78f744afc 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -171,7 +171,7 @@ typedef struct vrect_s { #define DEG2RAD(a) ((a) * (M_PI / 180)) #define RAD2DEG(a) ((a) * (180 / M_PI)) -#define ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) +#define Q_ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) #define BIT(n) (1U << (n)) #define BIT_ULL(n) (1ULL << (n)) diff --git a/src/client/demo.c b/src/client/demo.c index 31840c692..aa90b7be9 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -858,7 +858,7 @@ void CL_EmitDemoSnapshot(void) snap->msglen = msg_write.cursize; memcpy(snap->data, msg_write.data, msg_write.cursize); - cls.demo.snapshots = Z_Realloc(cls.demo.snapshots, sizeof(cls.demo.snapshots[0]) * ALIGN(cls.demo.numsnapshots + 1, MIN_SNAPSHOTS)); + cls.demo.snapshots = Z_Realloc(cls.demo.snapshots, sizeof(cls.demo.snapshots[0]) * Q_ALIGN(cls.demo.numsnapshots + 1, MIN_SNAPSHOTS)); cls.demo.snapshots[cls.demo.numsnapshots++] = snap; Com_DPrintf("[%d] snaplen %u\n", cls.demo.frames_read, msg_write.cursize); diff --git a/src/client/http.c b/src/client/http.c index 832c54100..f2ad2b25e 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -130,7 +130,7 @@ static size_t recv_func(void *ptr, size_t size, size_t nmemb, void *stream) return 0; // grow buffer in MIN_DLSIZE chunks. +1 for NUL. - new_size = ALIGN(dl->position + bytes + 1, MIN_DLSIZE); + new_size = Q_ALIGN(dl->position + bytes + 1, MIN_DLSIZE); if (new_size > dl->size) { char *buf = realloc(dl->buffer, new_size); if (!buf) diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index 927a62491..41888a491 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -879,7 +879,7 @@ static void DMA_Update(void) endtime = soundtime + sec * dma.speed; // mix to an even submission block size - endtime = ALIGN(endtime, dma.submission_chunk); + endtime = Q_ALIGN(endtime, dma.submission_chunk); samples = dma.samples >> (dma.channels - 1); endtime = min(endtime, soundtime + samples); diff --git a/src/client/sound/mem.c b/src/client/sound/mem.c index b6d93705a..3a82f71bb 100644 --- a/src/client/sound/mem.c +++ b/src/client/sound/mem.c @@ -50,7 +50,7 @@ static int FindChunk(sizebuf_t *sz, uint32_t search) if (chunk == search) return len; - sz->readcount += ALIGN(len, 2); + sz->readcount += Q_ALIGN(len, 2); } return 0; @@ -151,7 +151,7 @@ static bool GetWavinfo(sizebuf_t *sz) } // save position after "cue " chunk - next_chunk = sz->readcount + ALIGN(chunk_len, 2); + next_chunk = sz->readcount + Q_ALIGN(chunk_len, 2); sz->readcount += 24; samples = SZ_ReadLong(sz); diff --git a/src/client/ui/menu.c b/src/client/ui/menu.c index 7d368b504..ef34cc16a 100644 --- a/src/client/ui/menu.c +++ b/src/client/ui/menu.c @@ -1752,7 +1752,7 @@ void Menu_AddItem(menuFrameWork_t *menu, void *item) if (!menu->nitems) { menu->items = UI_Malloc(MIN_MENU_ITEMS * sizeof(void *)); } else { - menu->items = Z_Realloc(menu->items, ALIGN(menu->nitems + 1, MIN_MENU_ITEMS) * sizeof(void *)); + menu->items = Z_Realloc(menu->items, Q_ALIGN(menu->nitems + 1, MIN_MENU_ITEMS) * sizeof(void *)); } menu->items[menu->nitems++] = item; diff --git a/src/client/ui/script.c b/src/client/ui/script.c index bd464843d..2f491dead 100644 --- a/src/client/ui/script.c +++ b/src/client/ui/script.c @@ -51,7 +51,7 @@ static const cmd_option_t o_common[] = { static void add_string(menuSpinControl_t *s, const char *tok) { if (s->numItems < MAX_MENU_ITEMS) { - s->itemnames = Z_Realloc(s->itemnames, ALIGN(s->numItems + 2, MIN_MENU_ITEMS) * sizeof(char *)); + s->itemnames = Z_Realloc(s->itemnames, Q_ALIGN(s->numItems + 2, MIN_MENU_ITEMS) * sizeof(char *)); s->itemnames[s->numItems++] = UI_CopyString(tok); } } diff --git a/src/common/bsp.c b/src/common/bsp.c index 52b21c3df..86a1e13ad 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -1209,9 +1209,9 @@ static size_t BSP_ParseLightgridHeader(bsp_t *bsp, const byte *in, size_t filele } return - ALIGN(sizeof(grid->nodes[0]) * grid->numnodes, 64) + - ALIGN(sizeof(grid->leafs[0]) * grid->numleafs, 64) + - ALIGN(sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles, 64); + Q_ALIGN(sizeof(grid->nodes[0]) * grid->numnodes, 64) + + Q_ALIGN(sizeof(grid->leafs[0]) * grid->numleafs, 64) + + Q_ALIGN(sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles, 64); } static bool BSP_ValidateLightgrid_r(const lightgrid_t *grid, uint32_t nodenum) @@ -1330,7 +1330,7 @@ static const xlump_info_t bspx_lumps[] = { // returns amount of extra space to allocate static size_t BSP_ParseExtensionHeader(bsp_t *bsp, lump_t *out, const byte *buf, uint32_t pos, uint32_t filelen) { - pos = ALIGN(pos, 4); + pos = Q_ALIGN(pos, 4); if (pos > filelen - 8) return 0; if (RL32(buf + pos) != BSPXHEADER) @@ -1478,7 +1478,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) count++; // round to cacheline - memsize += ALIGN(count * info->memsize, 64); + memsize += Q_ALIGN(count * info->memsize, 64); maxpos = max(maxpos, ofs + len); } diff --git a/src/common/files.c b/src/common/files.c index 07b88953f..6a671546a 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -2973,7 +2973,7 @@ void FS_File_g(const char *path, const char *ext, unsigned flags, genctx_t *ctx) for (i = 0; i < numFiles; i++) { s = list[i]; if (ctx->count < ctx->size && !strncmp(s, ctx->partial, ctx->length)) { - ctx->matches = Z_Realloc(ctx->matches, ALIGN(ctx->count + 1, MIN_MATCHES) * sizeof(char *)); + ctx->matches = Z_Realloc(ctx->matches, Q_ALIGN(ctx->count + 1, MIN_MATCHES) * sizeof(char *)); ctx->matches[ctx->count++] = s; } else { Z_Free(s); diff --git a/src/common/prompt.c b/src/common/prompt.c index 123c3741b..63ddfa142 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -144,7 +144,7 @@ void Prompt_AddMatch(genctx_t *ctx, const char *s) if (ctx->ignoredups && find_dup(ctx, s)) return; - ctx->matches = Z_Realloc(ctx->matches, ALIGN(ctx->count + 1, MIN_MATCHES) * sizeof(char *)); + ctx->matches = Z_Realloc(ctx->matches, Q_ALIGN(ctx->count + 1, MIN_MATCHES) * sizeof(char *)); ctx->matches[ctx->count++] = Z_CopyString(s); } diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 716e10dd3..f0423ac7f 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -766,7 +766,7 @@ void IMG_ReadPixels(screenshot_t *s) qglGetIntegerv(GL_PACK_ALIGNMENT, &align); s->bpp = format == GL_RGBA ? 4 : 3; - s->rowbytes = ALIGN(r_config.width * s->bpp, align); + s->rowbytes = Q_ALIGN(r_config.width * s->bpp, align); s->pixels = Z_Malloc(s->rowbytes * r_config.height); s->width = r_config.width; s->height = r_config.height; diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index b259f7279..af04b65ad 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -639,7 +639,7 @@ static void demo_emit_snapshot(mvd_t *mvd) if (!mvd->snapshots) mvd->snapshots = MVD_Malloc(sizeof(mvd->snapshots[0]) * MIN_SNAPSHOTS); else - mvd->snapshots = Z_Realloc(mvd->snapshots, sizeof(mvd->snapshots[0]) * ALIGN(mvd->numsnapshots + 1, MIN_SNAPSHOTS)); + mvd->snapshots = Z_Realloc(mvd->snapshots, sizeof(mvd->snapshots[0]) * Q_ALIGN(mvd->numsnapshots + 1, MIN_SNAPSHOTS)); mvd->snapshots[mvd->numsnapshots++] = snap; Com_DPrintf("[%d] snaplen %u\n", mvd->framenum, msg_write.cursize); diff --git a/src/unix/hunk.c b/src/unix/hunk.c index eb8c04c09..4bc763ed7 100644 --- a/src/unix/hunk.c +++ b/src/unix/hunk.c @@ -38,7 +38,7 @@ void Hunk_Begin(memhunk_t *hunk, size_t maxsize) // reserve a huge chunk of memory, but don't commit any yet hunk->cursize = 0; - hunk->maxsize = ALIGN(maxsize, pagesize); + hunk->maxsize = Q_ALIGN(maxsize, pagesize); buf = mmap(NULL, hunk->maxsize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (buf == MAP_FAILED) @@ -56,7 +56,7 @@ void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) Q_assert(hunk->cursize <= hunk->maxsize); // round to cacheline - size = ALIGN(size, 64); + size = Q_ALIGN(size, 64); if (size > hunk->maxsize - hunk->cursize) return NULL; @@ -78,7 +78,7 @@ void Hunk_End(memhunk_t *hunk) size_t newsize; Q_assert(hunk->cursize <= hunk->maxsize); - newsize = ALIGN(hunk->cursize, pagesize); + newsize = Q_ALIGN(hunk->cursize, pagesize); if (newsize < hunk->maxsize) { #if defined(__linux__) diff --git a/src/windows/hunk.c b/src/windows/hunk.c index 29a913a7a..db438578d 100644 --- a/src/windows/hunk.c +++ b/src/windows/hunk.c @@ -37,7 +37,7 @@ void Hunk_Begin(memhunk_t *hunk, size_t maxsize) // reserve a huge chunk of memory, but don't commit any yet hunk->cursize = 0; - hunk->maxsize = ALIGN(maxsize, pagesize); + hunk->maxsize = Q_ALIGN(maxsize, pagesize); hunk->base = VirtualAlloc(NULL, hunk->maxsize, MEM_RESERVE, PAGE_NOACCESS); if (!hunk->base) Com_Error(ERR_FATAL, @@ -53,7 +53,7 @@ void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) Q_assert(hunk->cursize <= hunk->maxsize); // round to cacheline - size = ALIGN(size, 64); + size = Q_ALIGN(size, 64); if (size > hunk->maxsize - hunk->cursize) return NULL; @@ -82,7 +82,7 @@ void Hunk_End(memhunk_t *hunk) Q_assert(hunk->cursize <= hunk->maxsize); // for statistics - hunk->mapped = ALIGN(hunk->cursize, pagesize); + hunk->mapped = Q_ALIGN(hunk->cursize, pagesize); } void Hunk_Free(memhunk_t *hunk) From 51c9323a43646790e1fb5a640a4a545456b1af91 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 4 May 2024 20:48:55 +0300 Subject: [PATCH 162/974] Add FreeBSD target to CI workflow. --- .github/workflows/build.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 28c1bfa84..3bf9a810f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,11 @@ env: -Dwrap_mode=nofallback -Dx11=disabled + MESON_ARGS_FREEBSD: >- + -Dicmp-errors=disabled + -Dwindows-crash-dumps=disabled + -Dwrap_mode=nofallback + jobs: mingw: runs-on: ubuntu-latest @@ -126,3 +131,22 @@ jobs: run: | meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_MACOS }} builddir meson compile -C builddir + + freebsd: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Build + uses: cross-platform-actions/action@v0.24.0 + with: + operating_system: freebsd + architecture: x86-64 + version: '14.0' + run: | + sudo pkg update + sudo pkg install -y git meson pkgconf openal-soft \ + sdl2 curl png jpeg-turbo libx11 libdecor evdev-proto wayland-protocols + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_FREEBSD }} builddir + meson compile -C builddir From b2419f8d970637d1755b4d85e71fede10e7f529c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 6 May 2024 16:58:15 +0300 Subject: [PATCH 163/974] Print number of clusters too. --- src/common/bsp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/bsp.c b/src/common/bsp.c index 86a1e13ad..addd7b361 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -828,6 +828,8 @@ static void BSP_PrintStats(const bsp_t *bsp) const bsp_stat_t *s = &bsp_stats[i]; Com_Printf("%8d : %s\n", *(int *)((byte *)bsp + s->ofs), s->name); } + if (bsp->vis) + Com_Printf("%8u : clusters\n", bsp->vis->numclusters); #if USE_REF if (bsp->lightgrid.numleafs) { From 60f6a776f10a349094bc3c45cbdd90c2e8999d90 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 7 May 2024 00:03:48 +0300 Subject: [PATCH 164/974] Simplify sky drawing code. --- src/refresh/sky.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/refresh/sky.c b/src/refresh/sky.c index a9017cb0b..f93daa3ec 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -321,10 +321,6 @@ static void MakeSkyVec(float s, float t, int axis, vec_t *out) out[4] = 1.0f - t; } -#define SKY_VISIBLE(side) \ - (skymins[0][side] < skymaxs[0][side] && \ - skymins[1][side] < skymaxs[1][side]) - /* ============== R_DrawSkyBox @@ -332,7 +328,6 @@ R_DrawSkyBox */ void R_DrawSkyBox(void) { - static const uint8_t skytexorder[6] = {0, 2, 1, 3, 4, 5}; vec5_t verts[4]; int i; @@ -346,11 +341,11 @@ void R_DrawSkyBox(void) GL_TexCoordPointer(2, 5, &verts[0][3]); for (i = 0; i < 6; i++) { - if (!SKY_VISIBLE(i)) { + if (skymins[0][i] >= skymaxs[0][i] || + skymins[1][i] >= skymaxs[1][i]) continue; - } - GL_BindTexture(0, sky_images[skytexorder[i]]); + GL_BindTexture(0, sky_images[i]); MakeSkyVec(skymaxs[0][i], skymins[1][i], i, verts[0]); MakeSkyVec(skymins[0][i], skymins[1][i], i, verts[1]); @@ -381,7 +376,7 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis char pathname[MAX_QPATH]; image_t *image; // 3dstudio environment map names - static const char suf[6][3] = { "rt", "bk", "lf", "ft", "up", "dn" }; + static const char suf[6][3] = { "rt", "lf", "bk", "ft", "up", "dn" }; if (!gl_drawsky->integer) { R_UnsetSky(); From 1d09ab5d666dbabf455752302b653b9495df1620 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 7 May 2024 13:14:33 -0400 Subject: [PATCH 165/974] Refactored time warnings to allow DM and TeamDM --- src/action/a_team.c | 66 +++++++++++++++++++++++++++++++++++++------ src/action/g_combat.c | 4 +-- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 1825f885c..4486a422d 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -309,6 +309,7 @@ qboolean team_game_going = false; // is a team game going right now? qboolean team_round_going = false; // is an actual round of a team game going right now? +int during_countdown = 0; // This is set to 1 when the 10..9..8.. countdown is going on int team_round_countdown = 0; // countdown variable for start of a round int rulecheckfrequency = 0; // accumulator variable for checking rules every 1.5 secs int lights_camera_action = 0; // countdown variable for "lights...camera...action!" @@ -2340,6 +2341,7 @@ void StartRound (void) static void StartLCA(void) { + during_countdown = 0; if ((gameSettings & (GS_WEAPONCHOOSE|GS_ROUNDBASED))) CleanLevel(); @@ -2462,6 +2464,22 @@ void PrintScores (void) } } +void handleTimeWarning(int timeLeft, char* soundFile, int* timeWarning, qboolean condition) { + if (*timeWarning < timeLeft && condition) { + if (timeLeft == 1) { + CenterPrintAll("1 MINUTE LEFT..."); + } else if (timeLeft == 3) { + CenterPrintAll("3 MINUTES LEFT..."); + } + gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); + *timeWarning = timeLeft; + if (esp->value && esp_debug->value) { + gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); + EspAnnounceDetails(true); + } + } +} + qboolean CheckTimelimit( void ) { if (timelimit->value > 0) @@ -2497,14 +2515,42 @@ qboolean CheckTimelimit( void ) // CTF or Espionage with use_warnings should have the same warnings when the map is ending as it does for halftime (see CTFCheckRules). // Otherwise, use_warnings should warn about 3 minutes and 1 minute left, but only if there aren't round ending warnings. - if( use_warnings->value && (ctf->value || ! roundtimelimit->value) ) + + // CTF and Espionage warnings + if( use_warnings->value && (ctf->value || ! roundtimelimit->value) && !(gameSettings & GS_DEATHMATCH) ) + { + if( timewarning < 3 && ((ctf->value) && level.matchTime >= timelimit->value * 60 - 10 )) + { + gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("world/10_0.wav"), 1.0, ATTN_NONE, 0.0 ); + timewarning = 3; + } + else if( timewarning < 2 && level.matchTime >= ((timelimit->value - 1) * 60) && !during_countdown) + { + CenterPrintAll( "1 MINUTE LEFT..." ); + gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/1_minute.wav"), 1.0, ATTN_NONE, 0.0 ); + timewarning = 2; + if (esp->value){ + if (esp_debug->value) + gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); + EspAnnounceDetails(true); + } + } + else if( timewarning < 1 && (!ctf->value) && timelimit->value > 3 && level.matchTime >= ((timelimit->value - 3) * 60) && !during_countdown ) + { + CenterPrintAll( "3 MINUTES LEFT..." ); + gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); + timewarning = 1; + } + } + // Deathmatch and Team Deathmatch warnings + else if( use_warnings->value && deathmatch->value && (gameSettings & GS_DEATHMATCH)) { - if( timewarning < 3 && (ctf->value && level.matchTime >= timelimit->value * 60 - 10 )) + if( timewarning < 3 && (level.matchTime >= timelimit->value * 60 - 10 )) { gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("world/10_0.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 3; } - else if( timewarning < 2 && level.matchTime >= (timelimit->value - 1) * 60 ) + else if( timewarning < 2 && level.matchTime >= ((timelimit->value - 1) * 60) && !during_countdown ) { CenterPrintAll( "1 MINUTE LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/1_minute.wav"), 1.0, ATTN_NONE, 0.0 ); @@ -2515,7 +2561,7 @@ qboolean CheckTimelimit( void ) EspAnnounceDetails(true); } } - else if( timewarning < 1 && (! ctf->value) && timelimit->value > 3 && level.matchTime >= (timelimit->value - 3) * 60 ) + else if( timewarning < 1 && timelimit->value > 3 && level.matchTime >= ((timelimit->value - 3) * 60) && !during_countdown) { CenterPrintAll( "3 MINUTES LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); @@ -2809,6 +2855,7 @@ int CheckTeamRules (void) { if (team_round_countdown == 101) { + during_countdown = 1; gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); @@ -2832,11 +2879,13 @@ int CheckTeamRules (void) edict_t *capturepoint; edict_t *ent; capturepoint = G_Find(NULL, FOFS(classname), "item_flag"); - if (!capturepoint) // Somehow we're in ETV with no capture point? - gi.dprintf("ERROR: No capture point (item_flag) found for ETV!?\n"); + if (!capturepoint) // Somehow we're in ETV with no capture point? + if (esp_debug->value) + gi.dprintf("ERROR: No capture point (item_flag) found for ETV!?\n"); if (capturepoint) { - gi.dprintf("INFO: capture point (item_flag) found\n"); + if (esp_debug->value) + gi.dprintf("INFO: capture point (item_flag) found\n"); VectorCopy( capturepoint->s.origin, level.poi_origin ); VectorCopy( capturepoint->s.angles, level.poi_angle ); } @@ -2850,7 +2899,8 @@ int CheckTeamRules (void) continue; // if (!ent->client->pers.spectator) // continue; - gi.dprintf("INFO: moving spectators\n"); + if (esp_debug->value) + gi.dprintf("INFO: moving spectators\n"); MoveClientToPOI(ent, capturepoint); } diff --git a/src/action/g_combat.c b/src/action/g_combat.c index e9ef01210..dce27760c 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1035,6 +1035,6 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, gi.cprintf(attacker, PRINT_HIGH, "%s were blasted by your grenade.\n", ent_name_list); // Stats for fun, tracks the highest amount of players hit by a single grenade - if (ent_count > attacker->client->resp.grenSplash) - attacker->client->resp.grenSplash = ent_count; + // if (ent_count > attacker->client->resp.grenSplash) + // attacker->client->resp.grenSplash = ent_count; } From 41bf08d0dfaff01d6b8bc6c179368540b7ee0c33 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 7 May 2024 13:22:11 -0400 Subject: [PATCH 166/974] Removed unused code --- src/action/a_team.c | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 4486a422d..0f0e9744a 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2464,22 +2464,6 @@ void PrintScores (void) } } -void handleTimeWarning(int timeLeft, char* soundFile, int* timeWarning, qboolean condition) { - if (*timeWarning < timeLeft && condition) { - if (timeLeft == 1) { - CenterPrintAll("1 MINUTE LEFT..."); - } else if (timeLeft == 3) { - CenterPrintAll("3 MINUTES LEFT..."); - } - gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); - *timeWarning = timeLeft; - if (esp->value && esp_debug->value) { - gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); - EspAnnounceDetails(true); - } - } -} - qboolean CheckTimelimit( void ) { if (timelimit->value > 0) From 095ad47e0485b25ff160b9f400fbdbfc14f6b864 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 7 May 2024 17:42:04 +0300 Subject: [PATCH 167/974] Add support for CustomizeEntity() game API. Closes #306. --- inc/shared/game.h | 22 +++++++++++++++++++++- src/server/entities.c | 12 ++++++++++++ src/server/game.c | 7 ++++++- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index df89239d7..e660b2c17 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -276,9 +276,15 @@ typedef game_export_t *(*game_entry_t)(game_import_t *); * * New fields can be safely added at the end of game_import_ex_t and * game_export_ex_t structures, provided GAME_API_VERSION_EX is also bumped. + * + * API version history: + * 1 - Initial release. + * 2 - Added CustomizeEntity(). */ -#define GAME_API_VERSION_EX 1 +#define GAME_API_VERSION_EX_MINIMUM 1 +#define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 +#define GAME_API_VERSION_EX 2 typedef enum { VIS_PVS = 0, @@ -286,6 +292,19 @@ typedef enum { VIS_NOAREAS = 2 // can be OR'ed with one of above } vis_t; +typedef enum { + CE_SKIP, // don't send this entity to client + CE_PASS, // pass unmodified + CE_CUSTOMIZE // customize (game must fill `temp') +} customize_entity_result_t; + +typedef struct { + entity_state_t s; +#if USE_PROTOCOL_EXTENSIONS + entity_state_extension_t x; +#endif +} customize_entity_t; + typedef struct { uint32_t apiversion; uint32_t structsize; @@ -307,6 +326,7 @@ typedef struct { qboolean (*CanSave)(void); void (*PrepFrame)(void); void (*RestartFilesystem)(void); // called when fs_restart is issued + customize_entity_result_t (*CustomizeEntity)(edict_t *client, edict_t *ent, customize_entity_t *temp); } game_export_ex_t; typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); diff --git a/src/server/entities.c b/src/server/entities.c index f1c81f168..8cb5b8f40 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -542,6 +542,18 @@ void SV_BuildClientFrame(client_t *client) state = &svs.entities[svs.next_entity % svs.num_entities]; MSG_PackEntity(state, &ent->s, ENT_EXTENSION(client->csr, ent)); + // optionally customize it + if (gex && gex->apiversion >= GAME_API_VERSION_EX_CUSTOMIZE_ENTITY && gex->CustomizeEntity) { + customize_entity_t temp; + customize_entity_result_t res = gex->CustomizeEntity(clent, ent, &temp); + if (res == CE_SKIP) + continue; + if (res == CE_CUSTOMIZE) { + Q_assert(temp.s.number == e); + MSG_PackEntity(state, &temp.s, ENT_EXTENSION(client->csr, &temp)); + } + } + #if USE_FPS // fix old entity origins for clients not running at // full server frame rate diff --git a/src/server/game.c b/src/server/game.c index 53aae7bf1..b2df24797 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -989,8 +989,13 @@ void SV_InitGameProgs(void) // get extended api if present game_entry_ex_t entry_ex = Sys_GetProcAddress(game_library, "GetGameAPIEx"); - if (entry_ex) + if (entry_ex) { gex = entry_ex(&game_import_ex); + if (gex->apiversion < GAME_API_VERSION_EX_MINIMUM) + gex = NULL; + else + Com_DPrintf("Game supports Q2PRO extended API version %d.\n", gex->apiversion); + } // initialize ge->Init(); From d1793d8fcf5f7eb18afcceabeccadf5051062a7c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 15:39:11 +0300 Subject: [PATCH 168/974] Remove unused mmodel_t field. --- inc/common/bsp.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 24dbbcddd..489580280 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -181,12 +181,7 @@ typedef struct { unsigned floodvalid; } marea_t; -typedef struct mmodel_s { -#if USE_REF - /* ======> */ - int type; - /* <====== */ -#endif +typedef struct { vec3_t mins, maxs; vec3_t origin; // for sounds or lights mnode_t *headnode; From 65a5103a86c16b0e209734059151f1cd4ab3dc9e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 15:43:33 +0300 Subject: [PATCH 169/974] Reduce size of mleaf_t. --- inc/common/bsp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 489580280..046914e05 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -162,8 +162,8 @@ typedef struct { int contents; int cluster; int area; - mbrush_t **firstleafbrush; int numleafbrushes; + mbrush_t **firstleafbrush; #if USE_REF mface_t **firstleafface; int numleaffaces; From 9ec054a1d9b5d821bdff618297cf46743d75ea03 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 19:32:31 +0300 Subject: [PATCH 170/974] Remove unused structure names. --- inc/client/sound/dma.h | 2 +- inc/common/bsp.h | 2 +- inc/common/cmd.h | 2 +- inc/common/field.h | 2 +- inc/common/net/net.h | 4 ++-- inc/common/prompt.h | 2 +- inc/common/utils.h | 2 +- inc/format/md2.h | 3 +-- inc/refresh/refresh.h | 10 +++++----- inc/shared/shared.h | 12 ++++++------ src/client/client.h | 10 +++++----- src/client/console.c | 2 +- src/client/input.c | 2 +- src/client/keys.c | 2 +- src/client/sound/sound.h | 8 ++++---- src/client/ui/demos.c | 2 +- src/client/ui/playerconfig.c | 2 +- src/client/ui/ui.h | 22 +++++++++++----------- src/common/cmd.c | 4 ++-- src/server/server.h | 2 +- 20 files changed, 48 insertions(+), 49 deletions(-) diff --git a/inc/client/sound/dma.h b/inc/client/sound/dma.h index 607a29bd0..af36a542d 100644 --- a/inc/client/sound/dma.h +++ b/inc/client/sound/dma.h @@ -18,7 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once -typedef struct dma_s { +typedef struct { int channels; int samples; // mono samples in buffer int submission_chunk; // don't mix less than this # diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 046914e05..e00912227 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -231,7 +231,7 @@ typedef struct { #endif -typedef struct bsp_s { +typedef struct { list_t entry; int refcount; diff --git a/inc/common/cmd.h b/inc/common/cmd.h index 86eb70f44..c88422b24 100644 --- a/inc/common/cmd.h +++ b/inc/common/cmd.h @@ -111,7 +111,7 @@ typedef struct { const char *sh, *lo, *help; } cmd_option_t; -typedef struct cmdreg_s { +typedef struct { const char *name; xcommand_t function; xcompleter_t completer; diff --git a/inc/common/field.h b/inc/common/field.h index 252b5cd18..0587e89ef 100644 --- a/inc/common/field.h +++ b/inc/common/field.h @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_FIELD_TEXT 256 -typedef struct inputField_s { +typedef struct { char text[MAX_FIELD_TEXT]; size_t maxChars; size_t visibleChars; diff --git a/inc/common/net/net.h b/inc/common/net/net.h index ae227c645..9844cf524 100644 --- a/inc/common/net/net.h +++ b/inc/common/net/net.h @@ -76,7 +76,7 @@ typedef union { uint64_t u64[2]; } netadrip_t; -typedef struct netadr_s { +typedef struct { netadrtype_t type; netadrip_t ip; uint16_t port; @@ -91,7 +91,7 @@ typedef enum netstate_e { NS_BROKEN // fatal error has been signaled } netstate_t; -typedef struct netstream_s { +typedef struct { struct pollfd *socket; netadr_t address; netstate_t state; diff --git a/inc/common/prompt.h b/inc/common/prompt.h index f0592d37c..dc2f23454 100644 --- a/inc/common/prompt.h +++ b/inc/common/prompt.h @@ -27,7 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MIN_MATCHES 64 #define MAX_MATCHES 250000000 -typedef struct commandPrompt_s { +typedef struct { int inputLineNum; int historyLineNum; diff --git a/inc/common/utils.h b/inc/common/utils.h index 23a9bcac0..1e0862121 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -83,7 +83,7 @@ static inline size_t CS_SIZE(const cs_remap_t *csr, int cs) } #if USE_FPS -typedef struct frametime_s { +typedef struct { int time; // variable server frame time int div; // BASE_FRAMETIME/frametime } frametime_t; diff --git a/inc/format/md2.h b/inc/format/md2.h index 4f964a292..2f3ca8094 100644 --- a/inc/format/md2.h +++ b/inc/format/md2.h @@ -70,8 +70,7 @@ typedef struct { // a vertex consists of a floating point s, a floating point t, // and an integer vertex index. - -typedef struct dmd2header_s { +typedef struct { uint32_t ident; uint32_t version; diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 1ca8deb8a..217a4b66b 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -83,25 +83,25 @@ typedef struct entity_s { float scale; } entity_t; -typedef struct dlight_s { +typedef struct { vec3_t origin; vec3_t transformed; vec3_t color; float intensity; } dlight_t; -typedef struct particle_s { +typedef struct { vec3_t origin; int color; // -1 => use rgba float alpha; color_t rgba; } particle_t; -typedef struct lightstyle_s { - float white; // highest of RGB +typedef struct { + float white; // highest of RGB } lightstyle_t; -typedef struct refdef_s { +typedef struct { int x, y, width, height;// in virtual screen coordinates float fov_x, fov_y; vec3_t vieworg; diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 78f744afc..aa13a5626 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -164,8 +164,8 @@ typedef union { extern const vec3_t vec3_origin; -typedef struct vrect_s { - int x, y, width, height; +typedef struct { + int x, y, width, height; } vrect_t; #define DEG2RAD(a) ((a) * (M_PI / 180)) @@ -806,7 +806,7 @@ COLLISION DETECTION #define AREA_TRIGGERS 2 // plane_t structure -typedef struct cplane_s { +typedef struct { vec3_t normal; float dist; byte type; // for fast side tests @@ -820,7 +820,7 @@ typedef struct cplane_s { #define PLANE_Z 2 #define PLANE_NON_AXIAL 6 -typedef struct csurface_s { +typedef struct { char name[16]; int flags; int value; @@ -889,7 +889,7 @@ typedef struct { #define BUTTON_ANY BIT(7) // any key whatsoever // usercmd_t is sent to the server each client frame -typedef struct usercmd_s { +typedef struct { byte msec; byte buttons; short angles[3]; @@ -1382,7 +1382,7 @@ typedef enum { // entity_state_t is the information conveyed from the server // in an update message about entities that the client will // need to render in some way -typedef struct entity_state_s { +typedef struct { int number; // edict index vec3_t origin; diff --git a/src/client/client.h b/src/client/client.h index 07bd7bbc1..92910c1dc 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -70,7 +70,7 @@ typedef union { }; } centity_state_t; -typedef struct centity_s { +typedef struct { centity_state_t current; centity_state_t prev; // will always be valid, but might just be a copy of current @@ -98,7 +98,7 @@ extern centity_t cl_entities[MAX_EDICTS]; #define MAX_CLIENTWEAPONMODELS 256 // PGM -- upped from 16 to fit the chainfist vwep -typedef struct clientinfo_s { +typedef struct { char name[MAX_QPATH]; qhandle_t skin; qhandle_t icon; @@ -160,7 +160,7 @@ typedef struct { // the client_state_t structure is wiped completely at every // server map change // -typedef struct client_state_s { +typedef struct { int timeoutcount; unsigned lastTransmitTime; @@ -378,7 +378,7 @@ typedef struct { byte data[1]; } demosnap_t; -typedef struct client_static_s { +typedef struct { connstate_t state; keydest_t key_dest; @@ -843,7 +843,7 @@ typedef struct cparticle_s { color_t rgba; } cparticle_t; -typedef struct cdlight_s { +typedef struct { int key; // so entities can reuse same entry vec3_t color; vec3_t origin; diff --git a/src/client/console.c b/src/client/console.c index 732446e44..cb930b23c 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -45,7 +45,7 @@ typedef struct { char text[CON_LINEWIDTH]; } consoleLine_t; -typedef struct console_s { +typedef struct { bool initialized; consoleLine_t text[CON_TOTALLINES]; diff --git a/src/client/input.c b/src/client/input.c index a29bf0ea9..b6ae5200e 100644 --- a/src/client/input.c +++ b/src/client/input.c @@ -228,7 +228,7 @@ Key_Event (int key, bool down, unsigned time); =============================================================================== */ -typedef struct kbutton_s { +typedef struct { int down[2]; // key nums holding it down unsigned downtime; // msec timestamp unsigned msec; // msec down this frame diff --git a/src/client/keys.c b/src/client/keys.c index 292505203..f592d088f 100644 --- a/src/client/keys.c +++ b/src/client/keys.c @@ -40,7 +40,7 @@ static byte buttondown[256 / 8]; static bool key_overstrike; -typedef struct keyname_s { +typedef struct { const char *name; int keynum; } keyname_t; diff --git a/src/client/sound/sound.h b/src/client/sound/sound.h index 6d1824009..5f19dc341 100644 --- a/src/client/sound/sound.h +++ b/src/client/sound/sound.h @@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_SFX_SAMPLES ((1 << 23) - 1) -typedef struct sfxcache_s { +typedef struct { int length; int loopstart; int width; @@ -43,7 +43,7 @@ typedef struct sfxcache_s { #endif } sfxcache_t; -typedef struct sfx_s { +typedef struct { char name[MAX_QPATH]; int registration_sequence; sfxcache_t *cache; @@ -57,7 +57,7 @@ typedef struct sfx_s { // a playsound_t will be generated by each call to S_StartSound, // when the mixer reaches playsound->begin, the playsound will // be assigned to a channel -typedef struct playsound_s { +typedef struct { list_t entry; sfx_t *sfx; float volume; @@ -69,7 +69,7 @@ typedef struct playsound_s { int begin; // begin on this sample } playsound_t; -typedef struct channel_s { +typedef struct { sfx_t *sfx; // sfx number float leftvol; // 0.0-1.0 volume float rightvol; // 0.0-1.0 volume diff --git a/src/client/ui/demos.c b/src/client/ui/demos.c index c2073de56..47fb1c5cd 100644 --- a/src/client/ui/demos.c +++ b/src/client/ui/demos.c @@ -53,7 +53,7 @@ typedef struct { char name[1]; } demoEntry_t; -typedef struct m_demos_s { +typedef struct { menuFrameWork_t menu; menuList_t list; int numDirs; diff --git a/src/client/ui/playerconfig.c b/src/client/ui/playerconfig.c index c53ddf428..a5867c141 100644 --- a/src/client/ui/playerconfig.c +++ b/src/client/ui/playerconfig.c @@ -30,7 +30,7 @@ PLAYER CONFIG MENU #define ID_MODEL 103 #define ID_SKIN 104 -typedef struct m_player_s { +typedef struct { menuFrameWork_t menu; menuField_t name; menuSpinControl_t model; diff --git a/src/client/ui/ui.h b/src/client/ui/ui.h index cbd752713..6c1220570 100644 --- a/src/client/ui/ui.h +++ b/src/client/ui/ui.h @@ -147,7 +147,7 @@ typedef struct menuCommon_s { menuSound_t (*focus)(struct menuCommon_s *, bool gain); } menuCommon_t; -typedef struct menuField_s { +typedef struct { menuCommon_t generic; inputField_t field; cvar_t *cvar; @@ -156,7 +156,7 @@ typedef struct menuField_s { #define SLIDER_RANGE 10 -typedef struct menuSlider_s { +typedef struct { menuCommon_t generic; cvar_t *cvar; bool modified; @@ -179,7 +179,7 @@ typedef struct menuSlider_s { #define MLF_SCROLLBAR BIT(1) #define MLF_COLOR BIT(2) -typedef struct menuListColumn_s { +typedef struct { const char *name; int width; int uiFlags; @@ -213,7 +213,7 @@ typedef struct menuList_s { menuSound_t (*sort)(struct menuList_s *); } menuList_t; -typedef struct menuSpinControl_s { +typedef struct { menuCommon_t generic; cvar_t *cvar; @@ -226,27 +226,27 @@ typedef struct menuSpinControl_s { bool negate; } menuSpinControl_t; -typedef struct menuAction_s { +typedef struct { menuCommon_t generic; char *cmd; } menuAction_t; -typedef struct menuSeparator_s { +typedef struct { menuCommon_t generic; } menuSeparator_t; -typedef struct menuStatic_s { +typedef struct { menuCommon_t generic; int maxChars; } menuStatic_t; -typedef struct menuBitmap_s { +typedef struct { menuCommon_t generic; qhandle_t pics[2]; char *cmd; } menuBitmap_t; -typedef struct menuKeybind_s { +typedef struct { menuCommon_t generic; char binding[32]; char altbinding[32]; @@ -256,7 +256,7 @@ typedef struct menuKeybind_s { #define MAX_PLAYERMODELS 1024 -typedef struct playerModelInfo_s { +typedef struct { int nskins; char **skindisplaynames; char *directory; @@ -273,7 +273,7 @@ void PlayerModel_Free(void); #define NUM_CURSOR_FRAMES 15 -typedef struct uiStatic_s { +typedef struct { bool initialized; unsigned realtime; int width, height; // scaled diff --git a/src/common/cmd.c b/src/common/cmd.c index 86227379b..7751fe832 100644 --- a/src/common/cmd.c +++ b/src/common/cmd.c @@ -212,7 +212,7 @@ void Cbuf_Frame(cmdbuf_t *buf) #define FOR_EACH_ALIAS(alias) \ LIST_FOR_EACH(cmdalias_t, alias, &cmd_alias, listEntry) -typedef struct cmdalias_s { +typedef struct { list_t hashEntry; list_t listEntry; char *value; @@ -769,7 +769,7 @@ void Cmd_AddMacro(const char *name, xmacro_t function) #define FOR_EACH_CMD(cmd) \ LIST_FOR_EACH(cmd_function_t, cmd, &cmd_functions, listEntry) -typedef struct cmd_function_s { +typedef struct { list_t hashEntry; list_t listEntry; diff --git a/src/server/server.h b/src/server/server.h index cc77bafb3..2d458af0b 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -461,7 +461,7 @@ typedef struct { cm_t cm; } mapcmd_t; -typedef struct server_static_s { +typedef struct { bool initialized; // sv_init has completed unsigned realtime; // always increasing, no clamping, etc From c672e70d1f621ae71bad29f6f17a708282650d9c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 19:41:36 +0300 Subject: [PATCH 171/974] Const-ify R_RenderFrame() argument. --- inc/refresh/refresh.h | 2 +- src/refresh/main.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 217a4b66b..93c862ecc 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -205,7 +205,7 @@ void R_EndRegistration(void); #define R_RegisterSkin(name) R_RegisterImage(name, IT_SKIN, IF_NONE) #define R_RegisterSprite(name) R_RegisterImage(name, IT_SPRITE, IF_NONE) -void R_RenderFrame(refdef_t *fd); +void R_RenderFrame(const refdef_t *fd); void R_LightPoint(const vec3_t origin, vec3_t light); void R_ClearColor(void); diff --git a/src/refresh/main.c b/src/refresh/main.c index 42fe56a54..602c82ec9 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -662,7 +662,7 @@ static void GL_WaterWarp(void) qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } -void R_RenderFrame(refdef_t *fd) +void R_RenderFrame(const refdef_t *fd) { GL_Flush2D(); From da597c2e6b561420000cdaefac6c354296237a7f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 19:43:51 +0300 Subject: [PATCH 172/974] Keep GL config structure on stack. --- inc/refresh/refresh.h | 2 +- src/refresh/main.c | 31 +++++++++++++++---------------- src/unix/video/sdl.c | 15 ++++++++------- src/unix/video/wayland.c | 7 ++++--- src/unix/video/x11.c | 13 +++++++------ src/windows/egl.c | 7 ++++--- src/windows/wgl.c | 19 ++++++++++--------- 7 files changed, 49 insertions(+), 45 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 93c862ecc..560b06c2c 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -232,4 +232,4 @@ void R_BeginFrame(void); void R_EndFrame(void); void R_ModeChanged(int width, int height, int flags); -r_opengl_config_t *R_GetGLConfig(void); +void R_GetGLConfig(r_opengl_config_t *cfg); diff --git a/src/refresh/main.c b/src/refresh/main.c index 602c82ec9..e27342b65 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1164,29 +1164,28 @@ void R_Shutdown(bool total) R_GetGLConfig =============== */ -r_opengl_config_t *R_GetGLConfig(void) +void R_GetGLConfig(r_opengl_config_t *cfg) { - static r_opengl_config_t cfg; + memset(cfg, 0, sizeof(*cfg)); - cfg.colorbits = Cvar_ClampInteger(Cvar_Get("gl_colorbits", "0", CVAR_REFRESH), 0, 32); - cfg.depthbits = Cvar_ClampInteger(Cvar_Get("gl_depthbits", "0", CVAR_REFRESH), 0, 32); - cfg.stencilbits = Cvar_ClampInteger(Cvar_Get("gl_stencilbits", "8", CVAR_REFRESH), 0, 8); - cfg.multisamples = Cvar_ClampInteger(Cvar_Get("gl_multisamples", "0", CVAR_REFRESH), 0, 32); + cfg->colorbits = Cvar_ClampInteger(Cvar_Get("gl_colorbits", "0", CVAR_REFRESH), 0, 32); + cfg->depthbits = Cvar_ClampInteger(Cvar_Get("gl_depthbits", "0", CVAR_REFRESH), 0, 32); + cfg->stencilbits = Cvar_ClampInteger(Cvar_Get("gl_stencilbits", "8", CVAR_REFRESH), 0, 8); + cfg->multisamples = Cvar_ClampInteger(Cvar_Get("gl_multisamples", "0", CVAR_REFRESH), 0, 32); - if (cfg.colorbits == 0) - cfg.colorbits = 24; + if (cfg->colorbits == 0) + cfg->colorbits = 24; - if (cfg.depthbits == 0) - cfg.depthbits = cfg.colorbits > 16 ? 24 : 16; + if (cfg->depthbits == 0) + cfg->depthbits = cfg->colorbits > 16 ? 24 : 16; - if (cfg.depthbits < 24) - cfg.stencilbits = 0; + if (cfg->depthbits < 24) + cfg->stencilbits = 0; - if (cfg.multisamples < 2) - cfg.multisamples = 0; + if (cfg->multisamples < 2) + cfg->multisamples = 0; - cfg.debug = Cvar_Get("gl_debug", "0", CVAR_REFRESH)->integer; - return &cfg; + cfg->debug = Cvar_Get("gl_debug", "0", CVAR_REFRESH)->integer; } /* diff --git a/src/unix/video/sdl.c b/src/unix/video/sdl.c index 1be4b1d6c..b9ef5ba96 100644 --- a/src/unix/video/sdl.c +++ b/src/unix/video/sdl.c @@ -59,22 +59,23 @@ OPENGL STUFF static void set_gl_attributes(void) { - r_opengl_config_t *cfg = R_GetGLConfig(); + r_opengl_config_t cfg; + R_GetGLConfig(&cfg); - int colorbits = cfg->colorbits > 16 ? 8 : 5; + int colorbits = cfg.colorbits > 16 ? 8 : 5; SDL_GL_SetAttribute(SDL_GL_RED_SIZE, colorbits); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, colorbits); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, colorbits); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, cfg->depthbits); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, cfg->stencilbits); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, cfg.depthbits); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, cfg.stencilbits); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - if (cfg->multisamples) { + if (cfg.multisamples) { SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg->multisamples); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples); } - if (cfg->debug) { + if (cfg.debug) { SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); } diff --git a/src/unix/video/wayland.c b/src/unix/video/wayland.c index a9f507d23..a4e2d6962 100644 --- a/src/unix/video/wayland.c +++ b/src/unix/video/wayland.c @@ -779,10 +779,11 @@ static bool init(void) CHECK_EGL(eglBindAPI(EGL_OPENGL_API), "eglBindAPI"); - r_opengl_config_t *cfg = R_GetGLConfig(); + r_opengl_config_t cfg; + R_GetGLConfig(&cfg); EGLConfig config; - if (!choose_config(cfg, &config)) { + if (!choose_config(&cfg, &config)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; if (!choose_config(&failsafe, &config)) @@ -807,7 +808,7 @@ static bool init(void) reload_cursor(); EGLint ctx_attr[] = { - EGL_CONTEXT_OPENGL_DEBUG, cfg->debug, + EGL_CONTEXT_OPENGL_DEBUG, cfg.debug, EGL_NONE }; if (egl_major == 1 && egl_minor < 5) diff --git a/src/unix/video/x11.c b/src/unix/video/x11.c index 1e1f3c2fc..c08bcd486 100644 --- a/src/unix/video/x11.c +++ b/src/unix/video/x11.c @@ -244,15 +244,16 @@ static bool init(void) x11.extensions = glx_parse_extension_string(glXQueryExtensionsString(x11.dpy, x11.screen)); - r_opengl_config_t *cfg = R_GetGLConfig(); + r_opengl_config_t cfg; + R_GetGLConfig(&cfg); - if (cfg->multisamples && !(x11.extensions & QGLX_ARB_multisample)) { - Com_WPrintf("GLX_ARB_multisample not found for %d multisamples\n", cfg->multisamples); - cfg->multisamples = 0; + if (cfg.multisamples && !(x11.extensions & QGLX_ARB_multisample)) { + Com_WPrintf("GLX_ARB_multisample not found for %d multisamples\n", cfg.multisamples); + cfg.multisamples = 0; } GLXFBConfig fbc; - if (!choose_fb_config(cfg, &fbc)) { + if (!choose_fb_config(&cfg, &fbc)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; if (!choose_fb_config(&failsafe, &fbc)) @@ -343,7 +344,7 @@ static bool init(void) XFree(list); } - if (cfg->debug) { + if (cfg.debug) { PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB = NULL; if (x11.extensions & QGLX_ARB_create_context) diff --git a/src/windows/egl.c b/src/windows/egl.c index b321a0440..352c0ca7e 100644 --- a/src/windows/egl.c +++ b/src/windows/egl.c @@ -117,10 +117,11 @@ static bool egl_init(void) goto fail; } - r_opengl_config_t *cfg = R_GetGLConfig(); + r_opengl_config_t cfg; + R_GetGLConfig(&cfg); EGLConfig config; - if (!choose_config(cfg, &config)) { + if (!choose_config(&cfg, &config)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; if (!choose_config(&failsafe, &config)) @@ -135,7 +136,7 @@ static bool egl_init(void) EGLint ctx_attr[] = { EGL_CONTEXT_MAJOR_VERSION, 3, - EGL_CONTEXT_OPENGL_DEBUG, cfg->debug, + EGL_CONTEXT_OPENGL_DEBUG, cfg.debug, EGL_NONE }; egl.ctx = qeglCreateContext(egl.dpy, config, EGL_NO_CONTEXT, ctx_attr); diff --git a/src/windows/wgl.c b/src/windows/wgl.c index 316cc37c4..a4c70ce3e 100644 --- a/src/windows/wgl.c +++ b/src/windows/wgl.c @@ -268,6 +268,7 @@ static bool wgl_init(void) { const char *extensions = NULL; unsigned fake_extensions = 0; + r_opengl_config_t cfg; int ret; gl_allow_software = Cvar_Get("gl_allow_software", "0", 0); @@ -278,31 +279,31 @@ static bool wgl_init(void) return false; } - r_opengl_config_t *cfg = R_GetGLConfig(); + R_GetGLConfig(&cfg); // check for extensions by creating a fake window - if (cfg->multisamples || cfg->debug) + if (cfg.multisamples || cfg.debug) fake_extensions = get_fake_window_extensions(); - if (cfg->multisamples) { + if (cfg.multisamples) { if (fake_extensions & QWGL_ARB_multisample) { if (!wgl.ChoosePixelFormatARB) { Com_WPrintf("Ignoring WGL_ARB_multisample, WGL_ARB_pixel_format not found\n"); - cfg->multisamples = 0; + cfg.multisamples = 0; } } else { - Com_WPrintf("WGL_ARB_multisample not found for %d multisamples\n", cfg->multisamples); - cfg->multisamples = 0; + Com_WPrintf("WGL_ARB_multisample not found for %d multisamples\n", cfg.multisamples); + cfg.multisamples = 0; } } - if (cfg->debug && !wgl.CreateContextAttribsARB) { + if (cfg.debug && !wgl.CreateContextAttribsARB) { Com_WPrintf("WGL_ARB_create_context not found\n"); - cfg->debug = false; + cfg.debug = false; } // create window, choose PFD, setup OpenGL context - ret = wgl_setup_gl(cfg); + ret = wgl_setup_gl(&cfg); // attempt to recover if (ret == FAIL_SOFT) { From 176d01c66b0ee588d6d6020d181b1bb8a6df3180 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 8 May 2024 19:50:50 +0300 Subject: [PATCH 173/974] Const-ify pointers to GL config. --- src/unix/video/wayland.c | 2 +- src/unix/video/x11.c | 2 +- src/windows/egl.c | 2 +- src/windows/wgl.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/unix/video/wayland.c b/src/unix/video/wayland.c index a4e2d6962..0b7dff895 100644 --- a/src/unix/video/wayland.c +++ b/src/unix/video/wayland.c @@ -717,7 +717,7 @@ static void egl_error(const char *what) Com_EPrintf("%s failed with error %#x\n", what, eglGetError()); } -static bool choose_config(r_opengl_config_t *cfg, EGLConfig *config) +static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) { EGLint cfg_attr[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, diff --git a/src/unix/video/x11.c b/src/unix/video/x11.c index c08bcd486..0f8fe44c4 100644 --- a/src/unix/video/x11.c +++ b/src/unix/video/x11.c @@ -182,7 +182,7 @@ static int error_handler(Display *dpy, XErrorEvent *event) return 0; } -static bool choose_fb_config(r_opengl_config_t *cfg, GLXFBConfig *fbc) +static bool choose_fb_config(const r_opengl_config_t *cfg, GLXFBConfig *fbc) { int glx_attr[] = { GLX_X_RENDERABLE, True, diff --git a/src/windows/egl.c b/src/windows/egl.c index 352c0ca7e..748d2edc3 100644 --- a/src/windows/egl.c +++ b/src/windows/egl.c @@ -53,7 +53,7 @@ static void print_error(const char *what) Com_EPrintf("%s failed with error %#x\n", what, qeglGetError()); } -static bool choose_config(r_opengl_config_t *cfg, EGLConfig *config) +static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) { EGLint cfg_attr[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, diff --git a/src/windows/wgl.c b/src/windows/wgl.c index a4c70ce3e..0eb8ff101 100644 --- a/src/windows/wgl.c +++ b/src/windows/wgl.c @@ -77,7 +77,7 @@ static void print_error(const char *what) #define FAIL_SOFT -1 #define FAIL_HARD -2 -static int wgl_setup_gl(r_opengl_config_t *cfg) +static int wgl_setup_gl(const r_opengl_config_t *cfg) { PIXELFORMATDESCRIPTOR pfd; int pixelformat; From f81c27fb02bbf437a9123e3c920d8be3731ce652 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 9 May 2024 17:35:33 +0300 Subject: [PATCH 174/974] Remove unused prototype. --- src/client/client.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/client.h b/src/client/client.h index 92910c1dc..e5030d9a4 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -934,7 +934,6 @@ demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info); void LOC_Init(void); void LOC_LoadLocations(void); void LOC_FreeLocations(void); -void LOC_UpdateCvars(void); void LOC_AddLocationsToScene(void); From b01ef625ad4d8ac640f4480c07d50fd8d0bb0f73 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 9 May 2024 20:11:24 +0300 Subject: [PATCH 175/974] Fix rare case of UDP download failing to decompress. If inflate() produces exactly sizeof(buffer) output, progress may not be possible if all input has been consumed. As per zlib usage example, Z_BUF_ERROR must be ignored in this case. --- src/client/download.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/download.c b/src/client/download.c index fc97cbfc3..e9d15654e 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -329,7 +329,7 @@ static bool inflate_udp_download(byte *data, int size, int decompressed_size) z->avail_out = sizeof(buffer); ret = inflate(z, Z_SYNC_FLUSH); - if (ret != Z_OK && ret != Z_STREAM_END) { + if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR) { Com_EPrintf("[UDP] Error %d decompressing download\n", ret); finish_udp_download(NULL); return false; From a563d05f6463abfb322c686d1439a65edb9c336e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 9 May 2024 20:56:24 +0300 Subject: [PATCH 176/974] Const-ify CL_HandleDownload() data pointer. --- src/client/client.h | 2 +- src/client/download.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index e5030d9a4..808c2159e 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -659,7 +659,7 @@ bool CL_IgnoreDownload(const char *path); void CL_FinishDownload(dlqueue_t *q); void CL_CleanupDownloads(void); void CL_LoadDownloadIgnores(void); -void CL_HandleDownload(byte *data, int size, int percent, int decompressed_size); +void CL_HandleDownload(const byte *data, int size, int percent, int decompressed_size); bool CL_CheckDownloadExtension(const char *ext); void CL_StartNextDownload(void); void CL_RequestNextDownload(void); diff --git a/src/client/download.c b/src/client/download.c index e9d15654e..14e8cc68c 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -309,7 +309,7 @@ static bool write_udp_download(const byte *data, int size) #if USE_ZLIB // handles both continuous deflate stream for entire download and chunked // per-packet streams for compatibility. -static bool inflate_udp_download(byte *data, int size, int decompressed_size) +static bool inflate_udp_download(const byte *data, int size, int decompressed_size) { z_streamp z = &cls.download.z; byte buffer[0x10000]; @@ -322,7 +322,7 @@ static bool inflate_udp_download(byte *data, int size, int decompressed_size) return true; // run inflate() until output buffer not full - z->next_in = data; + z->next_in = (Bytef *)data; z->avail_in = size; do { z->next_out = buffer; @@ -362,7 +362,7 @@ CL_HandleDownload An UDP download data has been received from the server. ===================== */ -void CL_HandleDownload(byte *data, int size, int percent, int decompressed_size) +void CL_HandleDownload(const byte *data, int size, int percent, int decompressed_size) { dlqueue_t *q = cls.download.current; int ret; From 5c8b67f45eec06af06104152d44b5ef65e428523 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 10 May 2024 21:23:06 +0300 Subject: [PATCH 177/974] Deduplicate environment map names. --- inc/common/utils.h | 4 ++++ src/client/download.c | 6 +----- src/common/utils.c | 4 ++++ src/refresh/sky.c | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/inc/common/utils.h b/inc/common/utils.h index 1e0862121..7fe7b295c 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -18,6 +18,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +#if USE_CLIENT +extern const char com_env_suf[6][3]; +#endif + typedef enum { COLOR_BLACK, COLOR_RED, diff --git a/src/client/download.c b/src/client/download.c index 14e8cc68c..101da45b2 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -802,12 +802,8 @@ void CL_RequestNextDownload(void) } if (allow_download_textures->integer) { - static const char env_suf[6][3] = { - "rt", "bk", "lf", "ft", "up", "dn" - }; - for (i = 0; i < 6; i++) { - len = Q_concat(fn, sizeof(fn), "env/", cl.configstrings[CS_SKY], env_suf[i], ".tga"); + len = Q_concat(fn, sizeof(fn), "env/", cl.configstrings[CS_SKY], com_env_suf[i], ".tga"); check_file_len(fn, len, DL_OTHER); } } diff --git a/src/common/utils.c b/src/common/utils.c index c8073244f..eca5e6811 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -188,6 +188,10 @@ bool Com_WildCmpEx(const char *filter, const char *string, ============================================================================== */ +#if USE_CLIENT +const char com_env_suf[6][3] = { "rt", "lf", "bk", "ft", "up", "dn" }; +#endif + const char *const colorNames[COLOR_COUNT] = { "black", "red", "green", "yellow", "blue", "cyan", "magenta", "white", diff --git a/src/refresh/sky.c b/src/refresh/sky.c index f93daa3ec..19f32ef94 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -375,8 +375,6 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis int i; char pathname[MAX_QPATH]; image_t *image; - // 3dstudio environment map names - static const char suf[6][3] = { "rt", "lf", "bk", "ft", "up", "dn" }; if (!gl_drawsky->integer) { R_UnsetSky(); @@ -391,7 +389,7 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis for (i = 0; i < 6; i++) { if (Q_concat(pathname, sizeof(pathname), "env/", name, - suf[i], ".tga") >= sizeof(pathname)) { + com_env_suf[i], ".tga") >= sizeof(pathname)) { R_UnsetSky(); return; } From b3e40c70487fe0461a6d3adbb7495c84342d660f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 12 May 2024 01:59:39 +0300 Subject: [PATCH 178/974] Shorten code. --- src/server/send.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/server/send.c b/src/server/send.c index 1abac06a3..83df69a42 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -510,14 +510,11 @@ static bool check_entity(client_t *client, int entnum) return false; } -// sounds reliative to entities are handled specially +// sounds relative to entities are handled specially static void emit_snd(client_t *client, const message_packet_t *msg) { - int flags, entnum; - int i; - - entnum = msg->sendchan >> 3; - flags = msg->flags; + int entnum = msg->sendchan >> 3; + int flags = msg->flags; // check if position needs to be explicitly sent if (!(flags & SND_POS) && !check_entity(client, entnum)) { @@ -543,7 +540,7 @@ static void emit_snd(client_t *client, const message_packet_t *msg) MSG_WriteShort(msg->sendchan); if (flags & SND_POS) { - for (i = 0; i < 3; i++) { + for (int i = 0; i < 3; i++) { MSG_WriteShort(msg->pos[i]); } } @@ -1035,14 +1032,12 @@ void SV_SendAsyncPackets(void) void SV_InitClientSend(client_t *newcl) { - int i; - List_Init(&newcl->msg_free_list); List_Init(&newcl->msg_unreliable_list); List_Init(&newcl->msg_reliable_list); newcl->msg_pool = SV_Malloc(sizeof(newcl->msg_pool[0]) * MSG_POOLSIZE); - for (i = 0; i < MSG_POOLSIZE; i++) { + for (int i = 0; i < MSG_POOLSIZE; i++) { List_Append(&newcl->msg_free_list, &newcl->msg_pool[i].entry); } From cb98f53452263cb3912c8be276557abe1a635a1c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 12 May 2024 02:00:25 +0300 Subject: [PATCH 179/974] Truncate packetentities for protocol 34/35 clients on overflow. If the frame overflows, truncate remaining packetentities, patching current frame to make delta compression happy. This is much better than dropping entire frame, as only truncated entities will be frozen. This also removes frame compression support for protocol 35 clients, which was never efficient. --- doc/server.asciidoc | 10 +++- src/server/entities.c | 123 ++++++++++++++++++++++++++++++++++++++---- src/server/main.c | 2 + src/server/send.c | 25 ++------- src/server/server.h | 7 +-- 5 files changed, 130 insertions(+), 37 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index 2a75957d6..a3de530c0 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -272,8 +272,14 @@ sv_cinematics:: is 1. sv_max_packet_entities:: - Maximum number of entities in client frame. Default value is 0, which picks - optimal value automatically. + Maximum number of entities in client frame. Default value is 0, which is + equivalent to 128 for non-extended servers and 512 for extended servers. + +sv_trunc_packet_entities:: + Truncates packetentities instead of dropping client frame on overflow. This + does not break delta compression and should be generally safe to use. Only + relevant for legacy clients not using Q2PRO network channel implementation. + Default value is 1 (enabled). Downloads ~~~~~~~~~ diff --git a/src/server/entities.c b/src/server/entities.c index 8cb5b8f40..f1c872097 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -30,6 +30,103 @@ Encode a client frame onto the network channel #define Q2PRO_OPTIMIZE(c) \ ((c)->protocol == PROTOCOL_VERSION_Q2PRO && !(c)->settings[CLS_RECORDING]) +/* +============= +SV_TruncPacketEntities + +Truncates remainder of entity_packed_t list, patching current frame to make +delta compression happy. +============= +*/ +static bool SV_TruncPacketEntities(client_t *client, + client_frame_t *from, + client_frame_t *to, + int oldindex, + int newindex) +{ + entity_packed_t *newent; + const entity_packed_t *oldent; + int i, oldnum, newnum, from_num_entities, to_num_entities; + bool ret = true; + + if (!sv_trunc_packet_entities->integer || client->netchan.type) + return false; + + SV_DPrintf(0, "Truncating frame %d at %u bytes for %s\n", + client->framenum, msg_write.cursize, client->name); + + if (!from) + from_num_entities = 0; + else + from_num_entities = from->num_entities; + to_num_entities = to->num_entities; + + oldent = newent = NULL; + while (newindex < to->num_entities || oldindex < from_num_entities) { + if (newindex >= to->num_entities) { + newnum = MAX_EDICTS; + } else { + i = (to->first_entity + newindex) % svs.num_entities; + newent = &svs.entities[i]; + newnum = newent->number; + } + + if (oldindex >= from_num_entities) { + oldnum = MAX_EDICTS; + } else { + i = (from->first_entity + oldindex) % svs.num_entities; + oldent = &svs.entities[i]; + oldnum = oldent->number; + } + + if (newnum == oldnum) { + // skip delta update + *newent = *oldent; + oldindex++; + newindex++; + continue; + } + + if (newnum < oldnum) { + // remove new entity from frame + to->num_entities--; + for (i = newindex; i < to->num_entities; i++) { + svs.entities[(to->first_entity + i ) % svs.num_entities] = + svs.entities[(to->first_entity + i + 1) % svs.num_entities]; + } + continue; + } + + if (newnum > oldnum) { + // drop the frame if entity list got too big. + // should not normally happen. + if (to->num_entities >= MAX_PACKET_ENTITIES) { + ret = false; + break; + } + + // insert old entity into frame + for (i = to->num_entities - 1; i >= newindex; i--) { + svs.entities[(to->first_entity + i + 1) % svs.num_entities] = + svs.entities[(to->first_entity + i ) % svs.num_entities]; + } + + svs.entities[(to->first_entity + newindex) % svs.num_entities] = *oldent; + to->num_entities++; + + // should never go backwards + to_num_entities = max(to_num_entities, to->num_entities); + + oldindex++; + newindex++; + continue; + } + } + + svs.next_entity = to->first_entity + to_num_entities; + return ret; +} + /* ============= SV_EmitPacketEntities @@ -37,15 +134,20 @@ SV_EmitPacketEntities Writes a delta update of an entity_packed_t list to the message. ============= */ -static void SV_EmitPacketEntities(client_t *client, - client_frame_t *from, - client_frame_t *to, - int clientEntityNum) +static bool SV_EmitPacketEntities(client_t *client, + client_frame_t *from, + client_frame_t *to, + int clientEntityNum, + unsigned maxsize) { entity_packed_t *newent; const entity_packed_t *oldent; int i, oldnum, newnum, oldindex, newindex, from_num_entities; msgEsFlags_t flags; + bool ret = true; + + if (msg_write.cursize + 2 > maxsize) + return false; if (!from) from_num_entities = 0; @@ -56,8 +158,8 @@ static void SV_EmitPacketEntities(client_t *client, oldindex = 0; oldent = newent = NULL; while (newindex < to->num_entities || oldindex < from_num_entities) { - if (msg_write.cursize + MAX_PACKETENTITY_BYTES > msg_write.maxsize) { - Com_WPrintf("%s: frame got too large, aborting.\n", __func__); + if (msg_write.cursize + MAX_PACKETENTITY_BYTES > maxsize) { + ret = SV_TruncPacketEntities(client, from, to, oldindex, newindex); break; } @@ -126,6 +228,7 @@ static void SV_EmitPacketEntities(client_t *client, } MSG_WriteShort(0); // end of packetentities + return ret; } static client_frame_t *get_last_frame(client_t *client) @@ -168,7 +271,7 @@ static client_frame_t *get_last_frame(client_t *client) SV_WriteFrameToClient_Default ================== */ -void SV_WriteFrameToClient_Default(client_t *client) +bool SV_WriteFrameToClient_Default(client_t *client, unsigned maxsize) { client_frame_t *frame, *oldframe; player_packed_t *oldstate; @@ -204,7 +307,7 @@ void SV_WriteFrameToClient_Default(client_t *client) // delta encode the entities MSG_WriteByte(svc_packetentities); - SV_EmitPacketEntities(client, oldframe, frame, 0); + return SV_EmitPacketEntities(client, oldframe, frame, 0, maxsize); } /* @@ -212,7 +315,7 @@ void SV_WriteFrameToClient_Default(client_t *client) SV_WriteFrameToClient_Enhanced ================== */ -void SV_WriteFrameToClient_Enhanced(client_t *client) +bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) { client_frame_t *frame, *oldframe; player_packed_t *oldstate; @@ -311,7 +414,7 @@ void SV_WriteFrameToClient_Enhanced(client_t *client) client->frameflags = 0; // delta encode the entities - SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum); + return SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum, maxsize); } /* diff --git a/src/server/main.c b/src/server/main.c index d4c5891c9..0ca7e4dfa 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -80,6 +80,7 @@ cvar_t *sv_calcpings_method; cvar_t *sv_changemapcmd; cvar_t *sv_max_download_size; cvar_t *sv_max_packet_entities; +cvar_t *sv_trunc_packet_entities; cvar_t *sv_strafejump_hack; cvar_t *sv_waterjump_hack; @@ -2217,6 +2218,7 @@ void SV_Init(void) sv_changemapcmd = Cvar_Get("sv_changemapcmd", "", 0); sv_max_download_size = Cvar_Get("sv_max_download_size", "8388608", 0); sv_max_packet_entities = Cvar_Get("sv_max_packet_entities", "0", 0); + sv_trunc_packet_entities = Cvar_Get("sv_trunc_packet_entities", "1", 0); sv_strafejump_hack = Cvar_Get("sv_strafejump_hack", "1", CVAR_LATCH); sv_waterjump_hack = Cvar_Get("sv_waterjump_hack", "1", CVAR_LATCH); diff --git a/src/server/send.c b/src/server/send.c index 83df69a42..f5766f43d 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -713,26 +713,9 @@ static void write_datagram_old(client_t *client) // send over all the relevant entity_state_t // and the player_state_t - client->WriteFrame(client); - if (msg_write.cursize > maxsize) { - unsigned size = msg_write.cursize; - int len = 0; - - // try to compress if it has a chance to fit - // assume it can be compressed by at least 20% - if (size - size / 5 < maxsize) - len = compress_message(client); - + if (!client->WriteFrame(client, maxsize)) { + SV_DPrintf(0, "Frame %d overflowed for %s\n", client->framenum, client->name); SZ_Clear(&msg_write); - - if (len > 0 && len <= maxsize) { - SV_DPrintf(0, "Frame %d compressed for %s: %u into %d\n", - client->framenum, client->name, size, len); - SZ_Write(&msg_write, get_compressed_data(), len); - } else { - SV_DPrintf(0, "Frame %d overflowed for %s: %u > %u (comp %d)\n", - client->framenum, client->name, size, maxsize, len); - } } // now write unreliable messages @@ -797,9 +780,7 @@ static void write_datagram_new(client_t *client) // send over all the relevant entity_state_t // and the player_state_t - client->WriteFrame(client); - - if (msg_write.overflowed) { + if (!client->WriteFrame(client, msg_write.maxsize)) { // should never really happen Com_WPrintf("Frame overflowed for %s\n", client->name); SZ_Clear(&msg_write); diff --git a/src/server/server.h b/src/server/server.h index 2d458af0b..944b1cbc9 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -361,7 +361,7 @@ typedef struct client_s { // netchan type dependent methods void (*AddMessage)(struct client_s *, const byte *, size_t, bool); - void (*WriteFrame)(struct client_s *); + bool (*WriteFrame)(struct client_s *, unsigned); void (*WriteDatagram)(struct client_s *); // netchan @@ -533,6 +533,7 @@ extern cvar_t *sv_calcpings_method; extern cvar_t *sv_changemapcmd; extern cvar_t *sv_max_download_size; extern cvar_t *sv_max_packet_entities; +extern cvar_t *sv_trunc_packet_entities; extern cvar_t *sv_strafejump_hack; #if USE_PACKETDUP @@ -748,8 +749,8 @@ void SV_PrintMiscInfo(void); ((ent)->s.modelindex || (ent)->s.effects || (ent)->s.sound || (ent)->s.event) void SV_BuildClientFrame(client_t *client); -void SV_WriteFrameToClient_Default(client_t *client); -void SV_WriteFrameToClient_Enhanced(client_t *client); +bool SV_WriteFrameToClient_Default(client_t *client, unsigned maxsize); +bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize); // // sv_game.c From 6a1cd59b4d49a0fd18c4b03482f226cc599e5619 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 12 May 2024 18:47:22 +0300 Subject: [PATCH 180/974] Allow sorting entities in client frame by priority. --- doc/server.asciidoc | 5 +++ src/server/entities.c | 90 +++++++++++++++++++++++++++++++++++-------- src/server/main.c | 2 + src/server/server.h | 1 + 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index a3de530c0..a53d14745 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -281,6 +281,11 @@ sv_trunc_packet_entities:: relevant for legacy clients not using Q2PRO network channel implementation. Default value is 1 (enabled). +sv_prioritize_entities:: + Sort entities by priority if number of entities in client frame exceeds + ‘sv_max_packet_entities’ limit. Default value is 0, which simply throws + out entities with higher numbers that don't fit into frame. + Downloads ~~~~~~~~~ diff --git a/src/server/entities.c b/src/server/entities.c index f1c872097..cdd1d1cb5 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -495,6 +495,41 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) return (dist - SOUND_FULLVOLUME) * dist_mult > 1.0f; } +#define HI_PRIO(ent) \ + (ent->s.number <= sv_client->maxclients || ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) || ent->solid == SOLID_BSP) + +#define LO_PRIO(ent) \ + ((ent->s.renderfx & RF_LOW_PRIORITY) || (ent->s.effects & EF_GIB) || (!ent->s.modelindex && !ent->s.effects)) + +static int entpriocmp(const void *p1, const void *p2) +{ + const edict_t *a = *(const edict_t **)p1; + const edict_t *b = *(const edict_t **)p2; + + bool hi_a = HI_PRIO(a); + bool hi_b = HI_PRIO(b); + if (hi_a != hi_b) + return hi_b - hi_a; + + bool lo_a = LO_PRIO(a); + bool lo_b = LO_PRIO(b); + if (lo_a != lo_b) + return lo_a - lo_b; + + float dist_a = DistanceSquared(a->s.origin, sv_player->s.origin); + float dist_b = DistanceSquared(b->s.origin, sv_player->s.origin); + if (dist_a > dist_b) + return 1; + return -1; +} + +static int entnumcmp(const void *p1, const void *p2) +{ + const edict_t *a = *(const edict_t **)p1; + const edict_t *b = *(const edict_t **)p2; + return a->s.number - b->s.number; +} + /* ============= SV_BuildClientFrame @@ -505,7 +540,7 @@ copies off the playerstat and areabits. */ void SV_BuildClientFrame(client_t *client) { - int e; + int i, e; vec3_t org; edict_t *ent; edict_t *clent; @@ -518,6 +553,10 @@ void SV_BuildClientFrame(client_t *client) byte clientpvs[VIS_MAX_BYTES]; bool need_clientnum_fix; int max_packet_entities; + edict_t *edicts[MAX_EDICTS]; + int num_edicts; + customize_entity_result_t (*customize)(edict_t *, edict_t *, customize_entity_t *) = NULL; + customize_entity_t temp; clent = client->edict; if (!clent->client) @@ -571,6 +610,9 @@ void SV_BuildClientFrame(client_t *client) sv_max_packet_entities->integer > 0 ? sv_max_packet_entities->integer : client->csr->extended ? MAX_PACKET_ENTITIES : MAX_PACKET_ENTITIES_OLD; + if (gex && gex->apiversion >= GAME_API_VERSION_EX_CUSTOMIZE_ENTITY) + customize = gex->CustomizeEntity; + CM_FatPVS(client->cm, clientpvs, org); BSP_ClusterVis(client->cm->cache, clientphs, clientcluster, DVIS_PHS); @@ -578,6 +620,7 @@ void SV_BuildClientFrame(client_t *client) frame->num_entities = 0; frame->first_entity = svs.next_entity; + num_edicts = 0; for (e = 1; e < client->ge->num_edicts; e++) { ent = EDICT_NUM2(client->ge, e); @@ -641,20 +684,40 @@ void SV_BuildClientFrame(client_t *client) ent->s.number = e; } + // optionally skip it + if (customize && customize(clent, ent, NULL) == CE_SKIP) + continue; + + edicts[num_edicts++] = ent; + + if (num_edicts == max_packet_entities && !sv_prioritize_entities->integer) + break; + } + + // prioritize entities on overflow + if (num_edicts > max_packet_entities) { + sv_client = client; + sv_player = client->edict; + qsort(edicts, num_edicts, sizeof(edicts[0]), entpriocmp); + sv_client = NULL; + sv_player = NULL; + num_edicts = max_packet_entities; + qsort(edicts, num_edicts, sizeof(edicts[0]), entnumcmp); + } + + for (i = 0; i < num_edicts; i++) { + ent = edicts[i]; + e = ent->s.number; + // add it to the circular client_entities array state = &svs.entities[svs.next_entity % svs.num_entities]; - MSG_PackEntity(state, &ent->s, ENT_EXTENSION(client->csr, ent)); // optionally customize it - if (gex && gex->apiversion >= GAME_API_VERSION_EX_CUSTOMIZE_ENTITY && gex->CustomizeEntity) { - customize_entity_t temp; - customize_entity_result_t res = gex->CustomizeEntity(clent, ent, &temp); - if (res == CE_SKIP) - continue; - if (res == CE_CUSTOMIZE) { - Q_assert(temp.s.number == e); - MSG_PackEntity(state, &temp.s, ENT_EXTENSION(client->csr, &temp)); - } + if (customize && customize(clent, ent, &temp) == CE_CUSTOMIZE) { + Q_assert(temp.s.number == e); + MSG_PackEntity(state, &temp.s, ENT_EXTENSION(client->csr, &temp)); + } else { + MSG_PackEntity(state, &ent->s, ENT_EXTENSION(client->csr, ent)); } #if USE_FPS @@ -691,11 +754,8 @@ void SV_BuildClientFrame(client_t *client) state->solid = sv.entities[e].solid32; } + frame->num_entities++; svs.next_entity++; - - if (++frame->num_entities == max_packet_entities) { - break; - } } if (need_clientnum_fix) diff --git a/src/server/main.c b/src/server/main.c index 0ca7e4dfa..dd668049d 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -81,6 +81,7 @@ cvar_t *sv_changemapcmd; cvar_t *sv_max_download_size; cvar_t *sv_max_packet_entities; cvar_t *sv_trunc_packet_entities; +cvar_t *sv_prioritize_entities; cvar_t *sv_strafejump_hack; cvar_t *sv_waterjump_hack; @@ -2219,6 +2220,7 @@ void SV_Init(void) sv_max_download_size = Cvar_Get("sv_max_download_size", "8388608", 0); sv_max_packet_entities = Cvar_Get("sv_max_packet_entities", "0", 0); sv_trunc_packet_entities = Cvar_Get("sv_trunc_packet_entities", "1", 0); + sv_prioritize_entities = Cvar_Get("sv_prioritize_entities", "0", 0); sv_strafejump_hack = Cvar_Get("sv_strafejump_hack", "1", CVAR_LATCH); sv_waterjump_hack = Cvar_Get("sv_waterjump_hack", "1", CVAR_LATCH); diff --git a/src/server/server.h b/src/server/server.h index 944b1cbc9..ced9554f0 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -534,6 +534,7 @@ extern cvar_t *sv_changemapcmd; extern cvar_t *sv_max_download_size; extern cvar_t *sv_max_packet_entities; extern cvar_t *sv_trunc_packet_entities; +extern cvar_t *sv_prioritize_entities; extern cvar_t *sv_strafejump_hack; #if USE_PACKETDUP From 48e4c196b72a8e45fff4d720dc11fcef37154ef0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 13 May 2024 16:36:38 +0300 Subject: [PATCH 181/974] Use binary search to find entity in frame. --- src/server/send.c | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/server/send.c b/src/server/send.c index f5766f43d..048be6843 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -495,16 +495,25 @@ static void add_msg_packet(client_t *client, const byte *data, // check if this entity is present in current client frame static bool check_entity(client_t *client, int entnum) { - client_frame_t *frame; - int i, j; + const client_frame_t *frame; + int left, right; frame = &client->frames[client->framenum & UPDATE_MASK]; - for (i = 0; i < frame->num_entities; i++) { + left = 0; + right = frame->num_entities - 1; + while (left <= right) { + int i, j; + + i = (left + right) / 2; j = (frame->first_entity + i) % svs.num_entities; - if (svs.entities[j].number == entnum) { + j = svs.entities[j].number; + if (j < entnum) + left = i + 1; + else if (j > entnum) + right = i - 1; + else return true; - } } return false; From 4987d715c6bba5e7a01b5a9e4c8604d18fd0973b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 13 May 2024 20:32:25 +0300 Subject: [PATCH 182/974] Nag the user if demo recording fails to start. --- src/client/demo.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/demo.c b/src/client/demo.c index aa90b7be9..c5bc1b44b 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -230,10 +230,11 @@ void CL_EmitDemoFrame(void) cls.demo.frames_dropped++; // warn the user if drop rate is too high - if (cls.demo.frames_written < 10 && cls.demo.frames_dropped == 50) - Com_WPrintf("Too many demo frames don't fit into %u bytes.\n" - "Try to increase 'cl_demomsglen' value and restart recording.\n", - cls.demo.buffer.maxsize); + if (cls.demo.frames_written < 10 && !(cls.demo.frames_dropped % 50)) { + Com_WPrintf("Too many demo frames don't fit into %u bytes!\n", cls.demo.buffer.maxsize); + if (cls.demo.frames_dropped == 50) + Com_WPrintf("Try to increase 'cl_demomsglen' value and restart recording.\n"); + } } else { SZ_Write(&cls.demo.buffer, msg_write.data, msg_write.cursize); cls.demo.last_server_frame = cl.frame.number; From d881542b759b3f055a9c0fcbfa012d9f7a460022 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 May 2024 14:22:51 -0400 Subject: [PATCH 183/974] Re-trying porting the code --- inc/shared/shared.h | 30 + src/action/acesrc/acebot.h | 1208 ++-- src/action/acesrc/acebot_ai.c | 3330 ++++++---- src/action/acesrc/acebot_cmds.c | 475 +- src/action/acesrc/acebot_compress.c | 600 +- src/action/acesrc/acebot_items.c | 2211 ++++--- src/action/acesrc/acebot_movement.c | 5354 ++++++++++------ src/action/acesrc/acebot_nodes.c | 5926 +++++++++++++----- src/action/acesrc/acebot_spawn.c | 2330 ++++--- src/action/acesrc/bot_ai.c | 2074 +++--- src/action/acesrc/bot_collision.c | 1160 ++-- src/action/acesrc/bot_combat.c | 460 +- src/action/acesrc/bot_env_doors.c | 184 +- src/action/acesrc/bot_movement.c | 458 +- src/action/acesrc/bot_states.c | 334 +- src/action/acesrc/bot_utility.c | 447 +- src/action/acesrc/bot_weapon.c | 460 +- src/action/acesrc/botchat.c | 343 +- src/action/acesrc/botchat.h | 80 +- src/action/acesrc/botnav.c | 965 +-- src/action/acesrc/botnav.h | 167 +- src/action/acesrc/botscan.c | 580 +- src/action/acesrc/botscan.h | 106 +- src/action/botlib/botlib.h | 465 ++ src/action/botlib/botlib_ai.c | 1217 ++++ src/action/botlib/botlib_cmd.c | 794 +++ src/action/botlib/botlib_communication.c | 550 ++ src/action/botlib/botlib_ctf.c | 1042 +++ src/action/botlib/botlib_items.c | 257 + src/action/botlib/botlib_math.c | 154 + src/action/botlib/botlib_movement.c | 7309 ++++++++++++++++++++++ src/action/botlib/botlib_nav.c | 3639 +++++++++++ src/action/botlib/botlib_nodes.c | 5564 ++++++++++++++++ src/action/botlib/botlib_spawn.c | 2091 +++++++ src/action/botlib/botlib_spawnpoints.c | 483 ++ src/action/botlib/botlib_weapons.c | 1237 ++++ src/client/screen.c | 2 +- 37 files changed, 42829 insertions(+), 11257 deletions(-) create mode 100644 src/action/botlib/botlib.h create mode 100644 src/action/botlib/botlib_ai.c create mode 100644 src/action/botlib/botlib_cmd.c create mode 100644 src/action/botlib/botlib_communication.c create mode 100644 src/action/botlib/botlib_ctf.c create mode 100644 src/action/botlib/botlib_items.c create mode 100644 src/action/botlib/botlib_math.c create mode 100644 src/action/botlib/botlib_movement.c create mode 100644 src/action/botlib/botlib_nav.c create mode 100644 src/action/botlib/botlib_nodes.c create mode 100644 src/action/botlib/botlib_spawn.c create mode 100644 src/action/botlib/botlib_spawnpoints.c create mode 100644 src/action/botlib/botlib_weapons.c diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 7c5c0304e..c261e2ecc 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -22,6 +22,30 @@ with this program; if not, write to the Free Software Foundation, Inc., // shared.h -- included first by ALL program modules // +//rekkie -- CMAKE -- s +#if _MSC_VER >= 1920 && !__INTEL_COMPILER + //#define NDEBUG 1 +#define VERSION "ReKTeK" +//#define BUILDSTRING "1" +//#define CPUSTRING "1" +//#define BASEGAME "baseq2" +//#define DEFGAME "baseq2" +#define PLATFORM "Win64" +//#define USE_MVD_SERVER 1 +//#define USE_SERVER 1 +//#define USE_CLIENT 1 +//#define USE_DBGHELP 1 +#pragma warning(disable:4305) // warning C4305: 'initializing': truncation from 'double' to 'const vec_t' +#pragma warning(disable:4244) // warning C4244: '=': conversion from 'int64_t' to 'unsigned int', possible loss of data +#pragma warning(disable:4267) // warning C4267: 'return': conversion from 'size_t' to 'int', possible loss of data +#pragma warning(disable:4018) // warning C4018: '>': signed/unsigned mismatch +#pragma warning(disable:4013) // warning C4013: 'MSG_ShowSVC' undefined; assuming extern returning int +#pragma warning(disable:4047) // warning C4047: 'function': 'HDC' differs in levels of indirection from 'int' +#pragma warning(disable:4133) // warning C4133: 'function': incompatible types - from 'HGLRC' to 'HDC' +#pragma warning(disable:4146) // warning C4146: unary minus operator applied to unsigned type, result still unsigned +#endif +//rekkie -- CMAKE -- e + #if HAVE_CONFIG_H #include "config.h" #endif @@ -375,6 +399,11 @@ typedef struct vrect_s { #define Vector4Compare(v1,v2) ((v1)[0]==(v2)[0]&&(v1)[1]==(v2)[1]&&(v1)[2]==(v2)[2]&&(v1)[3]==(v2)[3]) #define Dot4Product(x, y) ((x)[0]*(y)[0]+(x)[1]*(y)[1]+(x)[2]*(y)[2]+(x)[3]*(y)[3]) +//rekkie -- ENGINE_DLL -- s +void VectorRotate2(vec3_t v, float degrees); +void RotatePointAroundVector(vec3_t dst, const vec3_t dir, const vec3_t point, float degrees); +//#endif // End of ENGINE_DLL + void AngleVectors(const vec3_t angles, vec3_t forward, vec3_t right, vec3_t up); vec_t VectorNormalize(vec3_t v); // returns vector length vec_t VectorNormalize2(const vec3_t v, vec3_t out); @@ -1006,6 +1035,7 @@ typedef enum { #if AQTION_EXTENSION // pmove->pm_aq2_flags #define PMF_AQ2_LIMP 0x01 // used to predict limping +#define PMF_AQ2_FRICTION 0x02 //rekkie -- Increase friction for bots #endif // this structure needs to be communicated bit-accurate diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 11dd82bd2..fa574bd05 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -1,357 +1,851 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot.h 9 29/02/00 11:20 Riever $ - * - * $Log: /LTK2/src/acesrc/acebot.h $ - * - * 9 29/02/00 11:20 Riever - * ChooseWeapon changed to qboolean - * - * 8 27/02/00 13:07 Riever - * Changed enums to defines for better compatibility. - * - * 7 24/02/00 3:07 Riever - * BOTUT_Cmd_Say_f proto added - * - * 6 23/02/00 17:24 Riever - * Added support for 'sv shownodes on/off' - * Enabled creation of nodes for ALL doors. (Stage 1 of new method) - * - * 5 21/02/00 23:45 Riever - * Added GT_ goal selection support and ROAM code protos. Altered movement - * trace lengths to be shorter. - * - * 4 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * - * 3 20/02/00 20:27 Riever - * Added new members and definitions ready for 2nd generation of bots. - * - * 2 17/02/00 17:53 Riever - * Fixed item list to be in the right order! - * - */ - -/////////////////////////////////////////////////////////////////////// -// -// acebot.h - Main header file for ACEBOT -// -// -/////////////////////////////////////////////////////////////////////// - -#ifndef _ACEBOT_H -#define _ACEBOT_H - -// Bots think at the server framerate to make sure they move smoothly. -#define BOT_FPS (game.framerate) - -// Only 100 allowed for now (probably never be enough edicts for 'em) -#define MAX_BOTS 100 - -// Platform states -#define STATE_TOP 0 -#define STATE_BOTTOM 1 -#define STATE_UP 2 -#define STATE_DOWN 3 - -// Maximum nodes -#define MAX_NODES 1200 - -// Link types -#define INVALID -1 - -// Node types -#define NODE_MOVE 0 -#define NODE_LADDER 1 -#define NODE_PLATFORM 2 -#define NODE_TELEPORTER 3 -#define NODE_ITEM 4 -#define NODE_WATER 5 -#define NODE_GRAPPLE 6 -#define NODE_JUMP 7 -#define NODE_DOOR 8 // - RiEvEr -#define NODE_ALL 99 // For selecting all nodes - -// Density setting for nodes -#define NODE_DENSITY 96 - -// Maximum links per node -#define MAXLINKS 12 - -//AQ2 ADD -extern cvar_t *ltk_skill; // Skill setting for bots, range 0-10 -extern cvar_t *ltk_showpath; // Toggles display of bot paths in debug mode -extern cvar_t *ltk_chat; // Chat setting for bots, off or on (0,1) -extern cvar_t *ltk_routing; // Set to 1 to drop nodes, otherwise you won't do it! -extern cvar_t *ltk_botfile; // Set this to adjust which botdata file to load, default is botdata -extern cvar_t *ltk_loadbots; // Set to 0 to not load bots from ltk_botfile value, 1 for normal operation -extern cvar_t *ltk_classic; // Set to 0 to use the new naming system and skins. 1 for classic operation - -extern int lights_camera_action; - -//AQ2 END - -// Bot state types -#define STATE_STAND 0 -#define STATE_MOVE 1 -#define STATE_ATTACK 2 -#define STATE_WANDER 3 -#define STATE_FLEE 4 -#define STATE_POSITION 5 -#define STATE_COVER 6 - -// New state definitions -#define BS_WAIT 0 -#define BS_DEAD 1 -#define BS_ROAM 2 -#define BS_PASSIVE 3 -#define BS_ACTIVE 4 -#define BS_SECURE 5 -#define BS_RETREAT 6 -#define BS_HOLD 7 -#define BS_SUPPORT 8 - -// Secondary states -#define BSS_NONE 0 -#define BSS_POSITION 1 -#define BSS_COLLECT 2 -#define BSS_SEEKENEMY 4 -#define BSS_ATTACK 8 - - -// Goal Types (Extensible) -#define GT_NONE 0 -#define GT_POSITION 1 -#define GT_ENEMY 2 -#define GT_ITEM 3 - - -#define MOVE_LEFT 0 -#define MOVE_RIGHT 1 -#define MOVE_FORWARD 2 -#define MOVE_BACK 3 - -// Used in the BOTCOL functions -#define TRACE_DOWN 128 -#define TRACE_DOWN_STRAFE 32 // don't go off ledges when strafing! -#define TRACE_DIST 48 //dropped from 256 -#define TRACE_DIST_STRAFE 24 // watch that edge! -#define TRACE_DIST_SHORT 32 // for forwards motion -#define TRACE_DIST_LADDER 24 -#define TRACE_DIST_JUMP 128 // to jump over gaps -#define TJUMP_DIST 40 //just enough to stand on -#define TWATER_DIST 8 // For getting to edge in water -#define TWEDGE_DIST 16 // ledge width required to leave water -#define TCRAWL_DIST 32 // not worth crawling otherwise -#define TMOVE_DIST 16 // wall detection -#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) // ouch! - -// Movement speeds -#define SPEED_CAREFUL 10 -#define SPEED_ROAM 100 -#define SPEED_WALK 200 -#define SPEED_RUN 400 - -#define EYES_FREQ 0.2 // Every n seconds the bot's eyes will be checked -#define ROOT2 1.41421 // Square root of 2 (i.e. ROOT2^2 = 2) -#define COS90 -0.34202 -#define STRIDESIZE 24 - -#define VEC_ORIGIN tv(0,0,0) - -typedef struct nodelink_s -{ - short int targetNode; - float cost; // updated for pathsearch algorithm - -}nodelink_t; // RiEvEr - - -// Node structure -typedef struct node_s -{ - vec3_t origin; // Using Id's representation - int type; // type of node - short int nodenum; // node number - RiEvEr -// short int lightlevel; // obvious... - RiEvEr - nodelink_t links[MAXLINKS]; // store all links. - RiEvEr - -} node_t; - -typedef struct item_table_s -{ - int item; - float weight; - edict_t *ent; - int node; - -} item_table_t; - -extern int num_players; -extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game - -// extern decs -extern node_t nodes[MAX_NODES]; -extern item_table_t item_table[MAX_EDICTS]; -extern qboolean debug_mode; -extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" -extern int numnodes; -extern int num_items; - -// id Function Protos I need -void LookAtKiller (edict_t *self, edict_t *inflictor, edict_t *attacker); -void ClientObituary (edict_t *self, edict_t *inflictor, edict_t *attacker); -void TossClientWeapon (edict_t *self); -void ClientThink (edict_t *ent, usercmd_t *ucmd); -void SelectSpawnPoint (edict_t *ent, vec3_t origin, vec3_t angles); -void ClientUserinfoChanged (edict_t *ent, char *userinfo); -void CopyToBodyQue (edict_t *ent); -qboolean ClientConnect (edict_t *ent, char *userinfo); -void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); -void hurt_touch (edict_t * self, edict_t * other, cplane_t * plane, csurface_t * surf); - -// acebot_ai.c protos -void ACEAI_Think (edict_t *self); -void ACEAI_PickLongRangeGoal(edict_t *self); -void ACEAI_PickShortRangeGoal(edict_t *self); -void ACEAI_PickSafeGoal(edict_t *self); -qboolean ACEAI_FindEnemy(edict_t *self, int *total); -qboolean ACEAI_ChooseWeapon(edict_t *self); -void ACEAI_Cmd_Choose( edict_t *ent, char *s ); -void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ); -void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); - -// acebot_cmds.c protos -qboolean ACECM_Commands(edict_t *ent); -void ACECM_Store(void); - -// acebot_items.c protos -void ACEIT_RebuildPlayerList( void ); -qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal); -qboolean ACEIT_IsReachable(edict_t *self,vec3_t goal); -qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item); -//AQ2 ADD -qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item); -//AQ2 END -qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other); -float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ); -int ACEIT_ClassnameToIndex( char *classname ); -void ACEIT_BuildItemNodeTable (qboolean rebuild); - -// acebot_movement.c protos -qboolean ACEMV_SpecialMove(edict_t *self,usercmd_t *ucmd); -void ACEMV_Move(edict_t *self, usercmd_t *ucmd); -void ACEMV_Attack (edict_t *self, usercmd_t *ucmd); -void ACEMV_Wander (edict_t *self, usercmd_t *ucmd); - -// acebot_nodes.c protos -int ACEND_FindCost(int from, int to); -int ACEND_FindCloseReachableNode(edict_t *self, int dist, int type); -int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); -int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type); -void ACEND_SetGoal(edict_t *self, int goal_node); -qboolean ACEND_FollowPath(edict_t *self); -void ACEND_GrapFired(edict_t *self); -qboolean ACEND_CheckForLadder(edict_t *self); -void ACEND_PathMap(edict_t *self); -void ACEND_InitNodes(void); -void ACEND_ShowNode(int node); -void ACEND_DrawPath(edict_t *self); -void ACEND_ShowPath(edict_t *self, int goal_node); -int ACEND_AddNode(edict_t *self, int type); -void ACEND_UpdateNodeEdge(edict_t *self, int from, int to); -void ACEND_RemoveNodeEdge(edict_t *self, int from, int to); -void ACEND_ResolveAllPaths(void); -void ACEND_SaveNodes(void); -void ACEND_LoadNodes(void); - -// acebot_spawn.c protos -void ACESP_SaveBots(void); -void ACESP_LoadBots(void); -void ACESP_LoadBotConfig(void); -edict_t *ACESP_SpawnBotFromConfig( char *inString ); -void ACESP_HoldSpawn(edict_t *self); -void ACESP_PutClientInServer (edict_t *bot, qboolean respawn, int team); -void ACESP_Respawn (edict_t *self); -edict_t *ACESP_FindFreeClient (void); -void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); -edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); -void ACESP_ReAddBots(void); -void ACESP_RemoveBot(char *name); -void attract_mode_bot_check(void); -void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); -void safe_centerprintf (edict_t *ent, const char *fmt, ...); -void debug_printf (char *fmt, ...); -void LTKClearBotNames(void); - -// bot_ai.c protos -qboolean BOTAI_NeedToBandage(edict_t *bot); -void BOTAI_PickLongRangeGoal(edict_t *self, int iType); -void BOTAI_PickShortRangeGoal(edict_t *bot); -void BOTAI_SetGoal(edict_t *self, int goal_node); -void BOTAI_Think(edict_t *bot); -qboolean BOTAI_VisibleEnemy( edict_t *self ); - -// bot_collision.c protos -qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanStand(edict_t *self); -qboolean BOTCOL_CanJumpUp(edict_t *self); -qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal); -qboolean BOTCOL_CheckShot(edict_t *bot); -qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles); -qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd); -qboolean BOTCOL_Visible (edict_t *bot, edict_t *other); - -// bot_combat.c protos -void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles); -void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles); -void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget); - -// bot_movement.c protos -void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); -float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles); -void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest); -void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles); - -// bot_states.c protos -void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd); -void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); - -//bot_utility.c protos -void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg); -void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest); -void BOTUT_ShowNodes (edict_t *ent); -void BOTUT_TempLaser( vec3_t start, vec3_t end); - -// bot_weapon.c protos -int BOTWP_ChangeMK23Mode(edict_t *bot); -int BOTWP_ChangeSniperMode(edict_t *bot); -qboolean BOTWP_ChooseWeapon(edict_t *bot); -int BOTWP_GetMK23Mode(edict_t *bot); -int BOTWP_GetSniperMode(edict_t *bot); -void BOTWP_RemoveSniperZoomMode(edict_t *bot); - -#endif +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot.h 9 29/02/00 11:20 Riever $ + * + * $Log: /LTK2/src/acesrc/acebot.h $ + * + * 9 29/02/00 11:20 Riever + * ChooseWeapon changed to qboolean + * + * 8 27/02/00 13:07 Riever + * Changed enums to defines for better compatibility. + * + * 7 24/02/00 3:07 Riever + * BOTUT_Cmd_Say_f proto added + * + * 6 23/02/00 17:24 Riever + * Added support for 'sv shownodes on/off' + * Enabled creation of nodes for ALL doors. (Stage 1 of new method) + * + * 5 21/02/00 23:45 Riever + * Added GT_ goal selection support and ROAM code protos. Altered movement + * trace lengths to be shorter. + * + * 4 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * + * 3 20/02/00 20:27 Riever + * Added new members and definitions ready for 2nd generation of bots. + * + * 2 17/02/00 17:53 Riever + * Fixed item list to be in the right order! + * + */ + +/////////////////////////////////////////////////////////////////////// +// +// acebot.h - Main header file for ACEBOT +// +// +/////////////////////////////////////////////////////////////////////// + +#ifndef _ACEBOT_H +#define _ACEBOT_H + +// Bots think at the server framerate to make sure they move smoothly. +#define BOT_FPS (game.framerate) + +// Only 100 allowed for now (probably never be enough edicts for 'em) +#define MAX_BOTS 100 + +// Platform states +#define STATE_TOP 0 +#define STATE_BOTTOM 1 +#define STATE_UP 2 +#define STATE_DOWN 3 + +// Maximum nodes +//#define MAX_NODES 1200 + +// Node types +#define INVALID -1 // Invalid / Terminating node/links +#define NAV_INFINITE 99999 //rekkie -- for navigation: Dijkstra's algorithm +// +typedef enum +{ + NODE_NONE, + NODE_MOVE, // Move (forward, left, right, back) + NODE_JUMPPAD, // For large jumps across any distance + NODE_LADDER, // Ladder + NODE_WATER, // Water + NODE_CROUCH, // Crouching with a reduced hitbox/collision height + NODE_BOXJUMP, // Move jump + half size hitbox (for jumping into windows, on boxes, etc) + NODE_POI, // Point of Interest (POI) specific locations that you wish bots to visit from time to time + NODE_POI_LOOKAT, // When using a Point of Interest (POI), this is the node to look at + + NODE_STEP, // For steps / stairs + NODE_JUMP, // Small jumps (+moveup) + NODE_STAND_DROP, // For dropping down while standing + NODE_CROUCH_DROP, // For dropping down while crouching + NODE_UNSAFE_DROP, // For dropping down while crouching, but causing minimal leg damage + NODE_LADDER_UP, // Ladder going up + NODE_LADDER_DOWN, // Ladder going down + NODE_DOOR, // Door node + NODE_PLATFORM, // Moving platforms + NODE_TELEPORTER, // Teleporters + NODE_ITEM, // Items + NODE_GRAPPLE, // CTF grapple hook + NODE_SPAWNPOINT, // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes + NODE_LEARN, // Special node - Human learning + NODE_CTF_FLAG, // CTF Flag + NODE_ALL = 99 // For selecting all nodes +} nodetype_t; +/* +#define NODE_MOVE (1) // Move (forward, left, right, back) +#define NODE_CROUCH (NODE_MOVE + 1) // Crouching (-moveup) +#define NODE_STEP (NODE_CROUCH + 1) // For steps / stairs +#define NODE_JUMP (NODE_STEP + 1) // Small jumps (+moveup) +#define NODE_JUMPPAD (NODE_JUMP + 1) // For large jumps across any distance +#define NODE_STAND_DROP (NODE_JUMPPAD + 1) // For dropping down while standing +#define NODE_CROUCH_DROP (NODE_STAND_DROP + 1) // For dropping down while crouching +#define NODE_UNSAFE_DROP (NODE_CROUCH_DROP + 1) // For dropping down while crouching, but causing minimal leg damage +#define NODE_LADDER (NODE_UNSAFE_DROP + 1) // Ladder +#define NODE_LADDER_UP (NODE_LADDER + 1) // Ladder going up +#define NODE_LADDER_DOWN (NODE_LADDER_UP + 1) // Ladder going down +#define NODE_DOOR (NODE_LADDER_DOWN + 1) // Door node +#define NODE_PLATFORM (NODE_DOOR + 1) // Moving platforms +#define NODE_TELEPORTER (NODE_PLATFORM + 1) // Teleporters +#define NODE_ITEM (NODE_TELEPORTER + 1) // Items +#define NODE_WATER (NODE_ITEM + 1) // Water +#define NODE_GRAPPLE (NODE_WATER + 1) // CTF grapple hook +#define NODE_SPAWNPOINT (NODE_GRAPPLE + 1) // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes +#define NODE_POI (NODE_SPAWNPOINT + 1) // Point of Interest (POI) specific locations that you wish bots to visit from time to time +#define NODE_LEARN (NODE_POI + 1) // Special node - Human learning +// +#define NODE_ALL 99 // For selecting all nodes +*/ + +// Density setting for nodes +#define NODE_DENSITY 96 + +// Maximum links per node +//#define MAXLINKS 12 + +//rekkie -- BSP -- s +//#undef MAX_NODES +//#define MAX_NODES 12288 //7168 //6144 +//#undef MAXLINKS +#define MAXLINKS 32 +//rekkie -- BSP -- e + +#define MAX_BOTSKILL 10 +extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! +extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost +extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +extern cvar_t *bot_showpath; // Show bot paths + +//AQ2 ADD +extern cvar_t *ltk_skill; // Skill setting for bots, range 0-10 +extern cvar_t *ltk_showpath; // Toggles display of bot paths in debug mode +extern cvar_t *ltk_chat; // Chat setting for bots, off or on (0,1) +extern cvar_t *ltk_routing; // Set to 1 to drop nodes, otherwise you won't do it! +extern cvar_t *ltk_botfile; // Set this to adjust which botdata file to load, default is botdata +extern cvar_t *ltk_loadbots; // Set to 0 to not load bots from ltk_botfile value, 1 for normal operation + +extern int lights_camera_action; + +//AQ2 END + +// Bot state types +#define STATE_STAND 0 +#define STATE_MOVE 1 +#define STATE_ATTACK 2 +#define STATE_WANDER 3 +#define STATE_FLEE 4 +#define STATE_POSITION 5 +#define STATE_COVER 6 +#define STATE_ITEM 7 // Bot is grabbing an item + +// New state definitions +#define BS_WAIT 0 +#define BS_DEAD 1 +#define BS_ROAM 2 +#define BS_PASSIVE 3 +#define BS_ACTIVE 4 +#define BS_SECURE 5 +#define BS_RETREAT 6 +#define BS_HOLD 7 +#define BS_SUPPORT 8 + +// Secondary states +#define BSS_NONE 0 +#define BSS_POSITION 1 +#define BSS_COLLECT 2 +#define BSS_SEEKENEMY 4 +#define BSS_ATTACK 8 + + +// Goal Types (Extensible) +#define GT_NONE 0 +#define GT_POSITION 1 +#define GT_ENEMY 2 +#define GT_ITEM 3 + + +#define MOVE_LEFT 0 +#define MOVE_RIGHT 1 +#define MOVE_FORWARD 2 +#define MOVE_BACK 3 + +// Used in the BOTCOL functions +#define TRACE_DOWN 128 +#define TRACE_DOWN_STRAFE 32 // don't go off ledges when strafing! +#define TRACE_DIST 48 //dropped from 256 +#define TRACE_DIST_STRAFE 24 // watch that edge! +#define TRACE_DIST_SHORT 32 // for forwards motion +#define TRACE_DIST_LADDER 24 +#define TRACE_DIST_JUMP 128 // to jump over gaps +#define TJUMP_DIST 40 //just enough to stand on +#define TWATER_DIST 8 // For getting to edge in water +#define TWEDGE_DIST 16 // ledge width required to leave water +#define TCRAWL_DIST 32 // not worth crawling otherwise +#define TMOVE_DIST 16 // wall detection +#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) // ouch! + +// Movement speeds +#define SPEED_CAREFUL 10 +#define SPEED_ROAM 100 +#define SPEED_WALK 200 +#define SPEED_RUN 400 + +#define EYES_FREQ 0.2 // Every n seconds the bot's eyes will be checked +#define ROOT2 1.41421 // Square root of 2 (i.e. ROOT2^2 = 2) +#define COS90 -0.34202 +#define STRIDESIZE 24 + +#define VEC_ORIGIN tv(0,0,0) + + +typedef struct nodelink_s +{ + short int targetNode; + //rekkie -- DEV_1 -- s + byte targetNodeType; // From our node, what type of node do we need the target node to be to reach it + //rekkie -- DEV_1 -- e + float cost; // updated for pathsearch algorithm + +}nodelink_t; // RiEvEr + + +// Node structure +typedef struct node_s +{ + int area; // Area/chunk/group this node belongs to -- optimise and diversify bot pathing + unsigned int area_color; // Sets the area color + vec3_t origin; // The origin of the node + byte type; // Type of node (MOVE, JUMP, LADDER, etc) + short int nodenum; // Node number + qboolean inuse; // If the node is used, deleted nodes are set to 'unused' + vec3_t normal; // The surface normal the node sits on + vec3_t mins; + vec3_t maxs; + vec3_t absmin; + vec3_t absmax; + float weight; // Used to help diversify bot pathing + //int num_vis_nodes; // Total nodes visible to this node + //int vis_nodes[1024]; // Store all vis nodes + + byte num_links; // Total links this node has + nodelink_t links[MAXLINKS]; // store all links. - RiEvEr +} node_t; + +typedef struct item_table_s +{ + int item; + float weight; + edict_t *ent; + int node; + +} item_table_t; + +extern int num_players; +extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game + +// extern decs +//rekkie -- DEV_1 -- s +extern node_t *unsorted_nodes; // Used to generate all links, so they can be sorted, then copied to nodes +#define MAX_PNODES 8096 //8096 +//32768 Absolute max nodes +//extern node_t *nodes; +node_t *nodes; +//node_t *nodes[MAX_PNODES]; +//node_t nodes[MAX_PNODES]; +//extern node_t nodes[MAX_PNODES]; +//rekkie -- DEV_1 -- e + +short int** path_table; +////short int path_table[MAX_PNODES][MAX_PNODES]; + +//extern node_t nodes[MAX_NODES]; +extern item_table_t item_table[MAX_EDICTS]; +extern qboolean debug_mode; +extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" +extern int numnodes; +extern int num_items; + +// id Function Protos I need +void LookAtKiller (edict_t *self, edict_t *inflictor, edict_t *attacker); +void ClientObituary (edict_t *self, edict_t *inflictor, edict_t *attacker); +void TossClientWeapon (edict_t *self); +void ClientThink (edict_t *ent, usercmd_t *ucmd); +void SelectSpawnPoint (edict_t *ent, vec3_t origin, vec3_t angles); +void ClientUserinfoChanged (edict_t *ent, char *userinfo); +void CopyToBodyQue (edict_t *ent); +qboolean ClientConnect (edict_t *ent, char *userinfo); +void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); + +// acebot_ai.c protos +void ACEAI_Think (edict_t *self); +void ACEAI_PickLongRangeGoal(edict_t *self); +void ACEAI_PickShortRangeGoal(edict_t *self); +void ACEAI_PickSafeGoal(edict_t *self); +qboolean ACEAI_ChooseWeapon(edict_t *self); +void ACEAI_Cmd_Choose( edict_t *ent, char *s ); +void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ); +void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); + +// acebot_cmds.c protos +qboolean ACECM_Commands(edict_t *ent); +void ACECM_Store(); + +// acebot_items.c protos +void ACEIT_RebuildPlayerList( void ); +qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal); +qboolean ACEIT_IsReachable(edict_t *self,vec3_t goal); +qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item); +//AQ2 ADD +qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item); +//AQ2 END +qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other); +float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ); +int ACEIT_ClassnameToIndex( char *classname ); +void ACEIT_BuildItemNodeTable (qboolean rebuild); + +// acebot_movement.c protos +qboolean ACEMV_SpecialMove(edict_t *self,usercmd_t *ucmd); +//void ACEMV_Move(edict_t *self, usercmd_t *ucmd); +void ACEMV_Attack (edict_t *self, usercmd_t *ucmd); +//void ACEMV_Wander (edict_t *self, usercmd_t *ucmd); +//rekkie -- Quake3 -- s +// botlib_items.c +void BOTLIB_SmartWeaponSelection(edict_t* self); +// botlib_ai.c +int BOTLIB_AutoAdjustSkill(edict_t* self); + + +void BOTLIB_Attack(edict_t* self, usercmd_t* ucmd); +void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd); +void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint); +qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity); +// BOT RADIO + +//rekkie -- Quake3 -- e + +// acebot_nodes.c protos +int ACEND_FindCost(int from, int to); +int ACEND_FindCloseReachableNode(edict_t *self, int dist, int type); +int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); +int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type); +qboolean BOTLIB_FollowPath(edict_t *self); +int BOTLIB_MakeEntsSolid(solid_t* trigger_solid); +void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid); +qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin); +void ACEND_GrapFired(edict_t *self); +//qboolean ACEND_CheckForLadder(edict_t *self); +//void ACEND_PathMap(edict_t *self); +void BOTLIB_InitNodes(void); +void ACEND_ShowNode(int node); +void ACEND_DrawPath(edict_t *self); +void ACEND_ShowPath(edict_t *self, int goal_node); +//void BOTLIB_GenerateNodeVis(edict_t* self); +int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs); +int BOTLIB_TraceBoxNode(int from, int to); +int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int* nodelist, const int maxnodes, int ignore_node); +int BOTLIB_TestForNodeDist(vec_t* origin, float distance, vec3_t mins, vec3_t maxs); +//int ACEND_AddNode(edict_t *self, int type); +//void ACEND_UpdateNodeEdge(edict_t *self, int from, int to); +void ACEND_RemoveNodeEdge(edict_t *self, int from, int to); +//void ACEND_ResolveAllPaths(); +//void ACEND_SaveNodes(); +//void ACEND_LoadNodes(); + +// acebot_spawn.c protos +//void ACESP_SaveBots(); +//void ACESP_LoadBots(); +void ACESP_LoadBotConfig(); +edict_t *ACESP_SpawnBotFromConfig( char *inString ); +void ACESP_HoldSpawn(edict_t *self); +void ACESP_PutClientInServer (edict_t *bot, qboolean respawn, int team); +void ACESP_Respawn (edict_t *self); +edict_t *ACESP_FindFreeClient (void); +void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); +edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); +//void ACESP_ReAddBots(); +void ACESP_RemoveBot(char *name); +void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); +void safe_centerprintf (edict_t *ent, const char *fmt, ...); +void debug_printf (char *fmt, ...); + +// bot_ai.c protos +qboolean BOTAI_NeedToBandage(edict_t *bot); +void BOTAI_PickLongRangeGoal(edict_t *self, int iType); +void BOTAI_PickShortRangeGoal(edict_t *bot); +void BOTAI_SetGoal(edict_t *self, int goal_node); +void BOTAI_Think(edict_t *bot); +qboolean BOTAI_VisibleEnemy( edict_t *self ); + +// bot_collision.c protos +qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanStand(edict_t *self); +qboolean BOTCOL_CanJumpUp(edict_t *self); +qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal); +qboolean BOTCOL_CheckShot(edict_t *bot); +qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles); +qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd); +qboolean BOTCOL_Visible (edict_t *bot, edict_t *other); + +// bot_combat.c protos +void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles); +void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles); +void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget); + +// bot_movement.c protos +void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); +float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles); +void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest); +void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles); + +// bot_states.c protos +void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd); +void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); + +//bot_utility.c protos +void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg); +void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest); +void BOTUT_ShowNodes (edict_t *ent); +void BOTUT_TempLaser( vec3_t start, vec3_t end); + +// bot_weapon.c protos +int BOTWP_ChangeMK23Mode(edict_t *bot); +int BOTWP_ChangeSniperMode(edict_t *bot); +qboolean BOTWP_ChooseWeapon(edict_t *bot); +int BOTWP_GetMK23Mode(edict_t *bot); +int BOTWP_GetSniperMode(edict_t *bot); +void BOTWP_RemoveSniperZoomMode(edict_t *bot); + +//rekkie -- BSP -- s + +//////////////////////////////////////////////////////////////////////// +// DAIC - Data's 'AI' Child ( taken from S3E16 of TNG ) +// https://en.wikipedia.org/wiki/The_Offspring_(Star_Trek%3A_The_Next_Generation) +// "Data invites Deanna Troi, Wesley Crusher and Geordi La Forge to his lab and surprises them by introducing a featureless humanoid android, whom he created based on his own structural design and recent advances in Federation cybernetics technology, describing it as his child. He names the android Lal (after the Hindi word for "beloved") and encourages it to select a gender and appearance. With Troi's assistance and considering many of the on-board species as well as the databanks, Lal narrows down to four possibilities, including a Klingon male, which, as Troi points out, would make it "a friend for Worf", but in the end selects the appearance of a young female human becoming a Gynoid.Data first aids Lal with cognitiveand standard behavioral algorithms, as well encourages her to interact with other members of the crew to learn behavioraland social customs." +// Notice that Data is attempting to make her in his own image, as we are attempting to make a bot like a human player. His child can also pick its own genderand appearance, like LTK bots do in DM.Additionally, he trains his child with "with cognitive and standard behavioral algorithms". +//////////////////////////////////////////////////////////////////////// +// +// TODO: +// - Make bot aim worst upon first few rounds by taking skill, minus 5 from it, then each shot adds +1 to skill until skill is back to normal. +// copy skill to default_skill, make a tmpskill and set it to skill and subtract -5, subtract tmpskill from skill in the code doing the aiming +// (check if skill is less than 0, set it to 0), then add +1 to tmpskill. Repeat this until skill is back to normal. +// If the bot finds a new enemy or stops firing, reset skill and default_skill. +// +// - When first adding reachabilities for the first time, set them to only use nodes on flat/sloped ground + ladders. +// For jumppads, record player's jumps from node to node and let the bot use these as jumppads so the bot can better pick jumps. +// +// - Make bot aim and strafe +// +// - Make bot be able to move while looking at any direction +// +// - in acebot_movement, when the bot is moving and NOT on ground, check if bot touches another player/bot while in air, if so then set bot to wander or find a new path? +// +// - Antsearch pathing could check if there is something blocking the path and if so, try to find a new path. +// +// - Try to find a way to add a variety of paths: Add varience, randomness, and more path_table's to pick randomly from! +// +// - Upon spawning, head to random spawn point. +// +// - For the top of ladders with rungs (like the ladders in urban2 that have left/right poles and rungs going horizontally in the middle), the rungs don't always +// come up high enough to meet the ground above (small ladder in ground level room that has a hole in roof going up). +// +// - Add random 'action hero' bot names +// +// - Make bot remember last enemy (or enemies) they encountered each round, attack then until they're dead or we gain a new enemy. Perhaps cycle through list of enemies +// we encounter when we're not attacking. +// +// - Punch grenades (allies or enemies), or catch grenade and add to inventory +// +// - Bots aiming through water, some water opacity should limit the bot's ability to aim through water, via water +// +// - [Drg] Varekai — Today at 22:26 +// I've always wondered because we use punch to get to weird places and I imagined that how far someone went had more to do with their initial speed +// as in, hitting them as they're just jumping would be more effective +// darksaint — Today at 22:27 +// ReKTeK aka Mech — Today at 22 : 31 +// Having another quick skim of the code, it doesn't take velocity into account. However, if the player is airborne, they wouldn't be slowed down by any initial friction(if any is applied) plus the height would give extra distance +// Drg] Varekai — Today at 22 : 32 +// so it is mostly random, pretty nuts +// ReKTeK aka Mech — Today at 22 : 33 +// I suppose velocity should be taken into account +// darksaint — Today at 22 : 35 +// Kicking someone in mid - air after being kicked should result in double damage :) using their body like a football +// +// - Roundlimit: add a countdown at 30 seconds, 15 seconds... +// - Donations: name on bill board, name in credits (txt/menu), +// - Bots attack and continue to move up/down ladders... +// - Make a beep/ding/windows sound when minimised and a round has ended. +// - Bot names, Diablo 2 pre, post fix names +// - Chance to shoot weapon out of hand +// - Make maps snow + fog for xmas +// - Make maps have a snow/fog/rain/etc. effect on the bot's view +// +// Yogomaster, Shadowking, Dirty, Psy, MOG, and Rekkie are the best bots. Lord Slice [web] +// + +// Node Stuff +#define MAX_SP_NODES 256 // Maximum Spawn Spots (SP), and Spawn Spot Nodes (SPN) +#define MAX_POI_NODES 512 // Maximum POI nodes +#define MAX_DOOR_NODES 64 // Maximum Door nodes +#define MAX_VIS_NODES 320 // Maximum visibility nodes +// + + + + +// +//#define MOVE_FORWARD_LEFT 4 // +45 degrees left +//#define MOVE_FORWARD_RIGHT 5 // -45 degrees right +//#define MOVE_BACK_LEFT 6 // +135 degrees left +//#define MOVE_BACK_RIGHT 7 // -135 degrees right +// Acebot Node Stuff -- e + +#define STEPSIZE 18 + +#define MINS_PLAYER_STANDING tv(-16, -16, -24) // Player standing +#define MAXS_PLAYER_STANDING tv(16, 16, 32) + +#define MINS_PLAYER_CROUCHING tv(-16, -16, -24) // Player crouching +#define MAXS_PLAYER_SCROUCHING tv(16, 16, 4) + +//rekkie -- DEV_1 -- Gib Players -- s +extern cvar_t* gib_bodies; +extern cvar_t* gib_heads; +//rekkie -- DEV_1 -- Gib Players -- e + +extern cvar_t* bot_maxteam; +extern cvar_t* bot_rush; +extern cvar_t* bot_randvoice; +extern cvar_t* bot_randskill; +extern cvar_t* bot_randname; +//extern cvar_t* bot_randteamskin; + + + + +#define MAX_BOT_NAMES 64 +typedef struct { + char name[16]; // Name +}bot_names_t; +int dc_total_male_names; // Total male names +int dc_total_female_names; // Total female names +bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy +bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy + + + + + + + + + +// Maximum nodes +//#define NODE_MAX 15000 // Max nodes -- 25,000 +//#define NODE_MAXLINKS 16 // Maximum links per node +#define NODE_Z_HEIGHT 32 // Z height of node placement +#define NODE_Z_HALF_HEIGHT 24 // 1/2 of NODE_Z_HEIGHT +#define NODE_Z_HEIGHT_PLUS_STEPSIZE 50 // NODE_Z_HEIGHT + STEPSIZE +#define NODE_MAX_DISTANCE 999999 // Max distance for a node +#define NODE_SIZE 24 // Physical size of the node +#define NODE_GRID_SIZE 16 //(NODE_SIZE*2) // Fit nodes on grid alignment +#define NODE_MAX_JUMP_HEIGHT 60 // Maximum height that a player can jump to reach a higher level surface, such as a box +#define NODE_MAX_FALL_HEIGHT 210 // Maximum height that a player can fall from without damage +#define NODE_MAX_SAFE_CROUCH_FALL_HEIGHT 214 // Maximum height that a player can crouch fall from without damage +#define NODE_MAX_CROUCH_FALL_HEIGHT 224 // Maximum height that a player can crouch fall from without damage +#define NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE 256 // Maximum height that a player can crouch fall + minor leg damage +#define NODE_CROUCH_HEIGHT 29 // Crouch height that a player can fit under +#define MAX_STEEPNESS 0.7 // Maximum normal value - steepness of the surface (0.7) + +// NMesh constraints +#define MAX_NMESH_FACES 18432 //18432 //16384 //12192 //8096 +#define MAX_REACHABILITY 64 +#define FACETYPE_FLOOR 1 +#define FACETYPE_SLOPE 2 +#define FACETYPE_WALL 3 +//#define FACETYPE_LADDER 4 +// Triangles +typedef struct { + vec3_t v[3]; // Verts of the triangle + vec3_t center; // Centoid of the triangle + int face; // The face this triangle belongs to + int node; // Triangle node + + // Reachability of nearby triangles + int reach_triangle[MAX_REACHABILITY]; // Nearby triangles reached + vec3_t reach_origin[MAX_REACHABILITY][3]; // Nearby triangle coordinates + int reach_face[MAX_REACHABILITY]; // The face that the nearby triangle resides in + int reach_num; // The number of nearby triangles reached + + // Reachability of target triangles (jumping) + //float reach_velocity[MAX_REACHABILITY]; // The velocity required to reach the target triangle + //float reach_distance[MAX_REACHABILITY]; // The distance to the target triangle + //float reach_time[MAX_REACHABILITY]; // The time it takes to reach the target triangle + //vec3_t reach_direction[MAX_REACHABILITY]; // The direction to the target triangle + +} ntris_t; +// Verts +typedef struct { + vec3_t v; + int node; // Vert node +} nvert_t; +// Edges +typedef struct { + vec3_t v[2]; // Verts of current edge + vec3_t center; // Center of the edge + int node; // Edge node +} nedge_t; +// Faces +typedef struct nface_s { + + // Verts + int num_verts; // Verts on current face + vec3_t v[64]; // All verts of current floor surface + + // Edges + nedge_t edge[128]; // Max edges per face + int num_edges; // Edges on current face + + // Triangles + ntris_t tris[32]; // Max triangles per face + int num_tris; // Triangles on current face + + int type; // Surface type - FACETYPE_FLOOR, FACETYPE_SLOPE, FACETYPE_WALL + int drawflags; // Surface flags - SURF_SKY, SURF_TRANS33, SURF_TRANS66, ... + int contents; // Surface contents - CONTENTS_SOLID, CONTENTS_WINDOW, ... + vec3_t normal; // Surface normals + +} nface_t; +typedef struct nmesh_s { + + // Totals + int total_faces; // Faces + int total_walls; // Walls + int total_edges; // Edges + int total_verts; // Verts + int total_tris; // Triangles + int total_reach; // Triangles reached + + // Floors & Walls + nface_t face[MAX_NMESH_FACES]; // Max faces (flat, sloped, walls) + + unsigned bsp_checksum; // Map checksum + +} nmesh_t; +nmesh_t nmesh; + + +int num_poi_nodes; +int poi_nodes[MAX_POI_NODES]; +edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +int num_vis_nodes; +int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +int node_vis_list[10][10]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. +//int node_vis[MAX_PNODES][MAX_PNODES]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +//int node_vis_list[MAX_PNODES][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. + + +// Botlib A_TEAM +//void CheckBotRules(void); + + +// acebot_ai.c +qboolean ACEAI_IsEnemy(edict_t* self, edict_t* other); +short BOTLIB_EnemiesAlive(edict_t* self); +short BOTLIB_AlliesAlive(edict_t* self); + +// DC Movement +void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd); +void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd); +void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd); +float VectorDistance(vec3_t start, vec3_t end); +float VectorDistanceXY(vec3_t start, vec3_t end); + + + +// Navigation +#define ANT_FREQ 0.5 // Time gap between calls to the processor intensive search +// ----------- new Pathing Algorithm stuff ----- +qboolean AntPathMove(edict_t* ent); // Called in item and enemy route functions +void AntInitSearch(edict_t* ent); // Resets all the path lists etc. +qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization); // main entry to path algorithms +qboolean AntQuickPath(edict_t* ent, int from, int to); // backup path system +qboolean AntFindPath(edict_t* ent, int from, int to); // Optimised path system +qboolean AntLinkExists(int from, int to); // Detects if we are off the path +// --------- AI Tactics Values ------------- +enum { + AIRoam, // Basic item collection AI + AIAttack, // Basic Attack Enemy AI + AIAttackCollect,// Attack Enemy while collecting Item + AICamp, // Camp at a suitable location and collect the item on respawn + AISnipe, + AIAmbush +}; + +qboolean BOTLIB_DrawPath(edict_t* self); // Draw bot path +void SLLpush_front(botlib_sll_t* thelist, int nodedata); // Add to the front of the list +void SLLpop_front(botlib_sll_t* thelist); // Remove the iten from the front of the list +int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list); //rekkie -- Query the SLL for the next node +int BOTLIB_SLL_Query_All_Nodes(edict_t* ent, botlib_sll_t* list, int* node_list, const int max_nodes); //rekkie -- Query and return all the nodes in the list +int SLLfront(botlib_sll_t* thelist); // Get the integer value from the front of the list, without removing the item (Query the list) +void SLLpush_back(botlib_sll_t* thelist, int nodedata); // Add to the back of the list +qboolean SLLempty(botlib_sll_t* thelist); // See if the list is empty (false if not empty) +void SLLdelete(botlib_sll_t* thelist); // Free all memory from a list + + + + + +// BOTLIB Nodes +void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end); // Rotate around a center, move the vector out and return the new point +void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int* edges); // Gets the nearest BSP face and number of edges to the origin +void BOTLIB_ChangeNodeFunction(edict_t* ent); +void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd); +int BOTLIB_Reachability(int from, int to); +qboolean BOTLIB_AdvanceToSelectNode(edict_t* self, int node); +qboolean NODES_AdvanceToNextNode(edict_t* self); +void BOTLIB_InitNavigation(edict_t* ent); +int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type); +qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost); +void BOTLIB_SelfExpandingNodes(edict_t* ent, int node); +void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t* ent); +void BOTLIB_LinkAllNodesTogether(edict_t* ent); +void BOTLIB_RemoveAllNodeLinksFrom(int from); + +#ifdef USE_ZLIB +void BOTLIB_SaveNavCompressed(void); +void BOTLIB_LoadNavCompressed(void); +#else +void BOTLIB_SaveNav(void); +void BOTLIB_LoadNav(void); +#endif +qboolean NodeTypeToString(edict_t* self, int type, char* string, const int max_string_size); + +//void ACEND_FreeUnsortedNodes(); +//qboolean ACEND_InitUnsortedNodes(); +void ACEND_BSP(edict_t* ent); +void ACEND_BuildSpawnPointNodes(void); +void ACEND_CachePointOfInterestNodes(void); +void ACEND_BuildEntNodeTable(void); +void ACEND_BuildVisibilityNodes(void); +qboolean ACEND_IsNodeVisibleToNodes(short x, short y); +short ACEND_GetRandomVisibleNode(short x); +//void ACEND_ShowAllNodes(void); +void BOTLIB_FreeNodes(void); // Free all nodes from memory +qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type); // Add node +//void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type); +void ACEND_RemoveNode(edict_t* self, int nodenum); + +qboolean ACEND_UpdateNodeReach(int from, int to); +//qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top); +void ACEND_SaveAAS(qboolean update); +void ACEND_LoadAAS(qboolean force); // Load Area Awareness System. Force: generate a new AAS instead of loading from file +void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle); +qboolean LaunchPlayer(edict_t* ent, vec3_t target); +//int ACEND_JumpReachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal); + + +// DAIC Spawn +void ACESP_RemoveTeamplayBot(int team); +void DC_LoadRandomBotName(char* userinfo); +//void DC_LoadRandomBotName(byte gender, char* bot_name); + +//rekkie -- BSP -- e + + +//rekkie -- Quake3 -- s +//=============================== +// Quake 3 Botlib.h Code +//=============================== +/* +* case 0: + SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff + break; + case 1: + SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute + break; + case 2: + SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt + break; + case 3: + SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave + break; + case 4: + default: + SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point +*/ + + +//=============================== +// Quake 3 Multithreading Code +//=============================== +// qthreads.h -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/qthreads.h +void BOTLIB_THREAD_LOADAAS(qboolean force); // Init and run this function threaded +void BOTLIB_THREADING_LOADAAS(void* param); // Running in a thread +typedef struct loadaas_s // Struct used to send parameters to the threaded function +{ + qboolean force; +} loadaas_t; + +void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to); +void BOTLIB_THREADING_DijkstraPath(void* param); +typedef struct dijkstra_path_s // Struct used to send parameters to the threaded function +{ + edict_t *ent; + int from; + int to; +} dijkstra_path_t; +// +extern int numthreads; +void ThreadSetDefault(void); +int GetThreadWork(void); +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)); +void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); +//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); +void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); +void ThreadLock(void); +void ThreadUnlock(void); +//rekkie -- Quake3 -- e + +#endif diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 6af8008fc..6ca40a470 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -1,1247 +1,2083 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot_ai.c 6 29/02/00 11:14 Riever $ - * - * $History: acebot_ai.c $ - * - * ***************** Version 6 ***************** - * User: Riever Date: 29/02/00 Time: 11:14 - * Updated in $/LTK2/src/acesrc - * Made bot ChooseWeapon function qboolean so it can signal when no weapon - * was ready for use. - * Moved weapon selection call to be inside attack function so bot knows - * whether it has to fire a weapon or use a kick attack. - * - * ***************** Version 5 ***************** - * User: Riever Date: 24/02/00 Time: 3:05 - * Updated in $/LTK2/src/acesrc - * Added bot say command support. Changed LTG weighting method. - * - * ***************** Version 4 ***************** - * User: Riever Date: 23/02/00 Time: 23:16 - * Updated in $/LTK2/src/acesrc - * Removed random door opening code - bots now MUST have a routefile to - * get through doors. - * Added support for response to 'treport' radio message. - * - * ***************** Version 3 ***************** - * User: Riever Date: 17/02/00 Time: 17:53 - * Updated in $/LTK2/src/acesrc - * Fixed item list to be in the right order! - * - */ -/////////////////////////////////////////////////////////////////////// -// -// acebot_ai.c - This file contains all of the -// AI routines for the ACE II bot. -// -// -// NOTE: I went back and pulled out most of the brains from -// a number of these functions. They can be expanded on -// to provide a "higher" level of AI. -//////////////////////////////////////////////////////////////////////// - -#include "../g_local.h" -#include "../m_player.h" - -#include "acebot.h" -#include "botchat.h" - -void ACEAI_Cmd_Choose( edict_t *ent, char *s); -void ACEMV_ChangeBotAngle (edict_t *ent); -//RiEvEr AQ2 Radio use -void RadioBroadcast(edict_t *ent, int partner, char *msg); -//R AQ2 - -/////////////////////////////////////////////////////////////////////// -// Main Think function for bot -/////////////////////////////////////////////////////////////////////// -void ACEAI_Think (edict_t *self) -{ - usercmd_t ucmd; - //int curplayer = 0; - int numenemies = 0; - //trace_t tr; - qboolean see_enemies = false; - - // Set up client movement - VectorCopy(self->client->ps.viewangles,self->s.angles); - VectorSet (self->client->ps.pmove.delta_angles, 0, 0, 0); - memset (&ucmd, 0, sizeof (ucmd)); - self->enemy = NULL; - self->movetarget = NULL; - - // Force respawn if during warmup mode - if (self->deadflag == DEAD_DEAD && (in_warmup)) - { - self->client->buttons = 0; - ucmd.buttons = BUTTON_ATTACK; - } - - // Stop trying to think if the bot can't respawn. - if( ! IS_ALIVE(self) && ((gameSettings & GS_ROUNDBASED) || (self->client->respawn_framenum > level.framenum)) ) - { - ucmd.msec = 1000 / BOT_FPS; - self->client->ping = ucmd.msec; - ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); - ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); - ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); - ClientThink( self, &ucmd ); - self->nextthink = level.framenum + (game.framerate / BOT_FPS); - return; - } - - // Force respawn - if (self->deadflag == DEAD_DEAD) - { - self->client->buttons = 0; - ucmd.buttons = BUTTON_ATTACK; - } - - // Teamplay spectator? Reenabled by Werewolf -// if( (self->solid == SOLID_NOT) && (teamplay->value) ) -// return; - - if( self->state == STATE_WANDER && self->wander_timeout < level.framenum ) - ACEAI_PickLongRangeGoal(self); // pick a new long range goal - - //RiEvEr Radio Use - if( ! self->teamReportedIn && (self->lastRadioTime < level.framenum - 2.0 * HZ) ) - { - RadioBroadcast(self, 0, "reportin"); - BOTUT_Cmd_Say_f( self, "Equipped with %W and %I. Current health %H."); - self->lastRadioTime = level.framenum; - self->teamReportedIn = true; - } - //R RU - - // In teamplay pick a random node - if( self->state == STATE_POSITION ) - { - if( level.framenum >= self->teamPauseTime) - { - // We've waited long enough - let's go kick some ass! - self->state = STATE_WANDER; - } - // Don't go here too often - if( self->goal_node == INVALID || self->wander_timeout < level.framenum ) - ACEAI_PickLongRangeGoal(self); - } - - - // Kill the bot if completely stuck somewhere - if(VectorLength(self->velocity) > 37) // - self->suicide_timeout = level.framenum + 10.0 * HZ; - - if( self->suicide_timeout < level.framenum && !teamplay->value ) - killPlayer( self, true ); - - // Find any short range goal - ACEAI_PickShortRangeGoal(self); - - if (self->old_health < self->health) - self->old_health = self->health; - - see_enemies = ACEAI_FindEnemy(self, &numenemies); - -/* // Check how many enemies we have in sight - if ( (self->recheck_timeout < level.time) ) - { - numenemies = 0; - self->recheck_timeout = level.time + (3*random()); - - for (curplayer=0; curplayerteam != self->team) ) - (players[curplayer]->health > 0) && - (players[curplayer] != NULL) && - (players[curplayer] != self) ) - { - tr = gi.trace (self->s.origin, NULL, NULL, players[curplayer]->s.origin, self, MASK_SOLID|MASK_OPAQUE); - if (tr.fraction >= 0.9) - { - numenemies += 1; - LTK_Say(self, "!"); - if (numenemies>1) - break; - } - } - } - - - if (numenemies>1) - { - LTK_Say(self, "Whoa! Many hostiles in sight!"); - if (self->state != STATE_FLEE) - { - self->state = STATE_FLEE; - self->goal_node = INVALID; - ACEAI_PickSafeGoal(self); - } - - } - - } -*/ - - // React when enemies remain in sight, otherwise gradually forget. - self->react += see_enemies ? FRAMETIME : (FRAMETIME * -2.f); - if( (self->react > 6.f) || (ltk_skill->value > 10) ) - self->react = 6.f; - else if( self->react < 0.f ) - self->react = 0.f; - - // Look for enemies - if( (see_enemies) && - (self->client->weaponstate != WEAPON_RELOADING) && - (! self->client->bandaging) && - (((teamplay->value) && (lights_camera_action <= 1)) || !teamplay->value)) - //&& (self->state != STATE_FLEE) ) - { - // Raptor007: Always use ACEMV_Attack to aim with inaccuracy. - ACEMV_Attack( self, &ucmd ); - self->movetarget = self->enemy; -/* - // Moved to the attack function -// ACEAI_ChooseWeapon(self); - // Stop the "spin 180 degrees and shoot" attack :) - // We need to change this to allow the bot to turn and look again - // so that he can find enemies who are firing at him. - if( infront( self, self->enemy )) - { - ACEMV_Attack (self, &ucmd); - } - else// if (self->state != STATE_FLEE) - // turn and look! - { - self->movetarget = self->enemy; - VectorSubtract (self->enemy->s.origin, self->s.origin, self->move_vector); - //self->s.angles[YAW] += 90; - ACEMV_ChangeBotAngle ( self ); - } -*/ -// if( self->health < self->old_health) -// { -// if (self->state != STATE_FLEE) -// { -// self->state = STATE_FLEE; //Look for a nearby node that doesn't have LOS to an enemy. -// self->goal_node = INVALID; -// ACEAI_PickSafeGoal(self); -// } -// } - } - else - { - // Are we hurt or out of ammo? - if( (self->health < self->old_health) || - self->client->leg_damage || - (self->client->weaponstate == WEAPON_RELOADING) ) - { - Cmd_Bandage_f ( self ); - if (self->state != STATE_FLEE) - { - self->state = STATE_FLEE; - self->goal_node = INVALID; - ACEAI_PickSafeGoal(self); - } - self->old_health = self->health; - - } - - - // Execute the move, or wander - if(self->state == STATE_WANDER) - ACEMV_Wander(self,&ucmd); - else if( (self->state == STATE_MOVE) || (self->state == STATE_POSITION) || (self->state == STATE_FLEE) ) - ACEMV_Move(self,&ucmd); - else if (self->state == STATE_COVER) - { - if( self->wander_timeout < level.framenum ) - { - self->state = STATE_WANDER; - } - } - - } - -/* if (self->state == STATE_FLEE) - if (ACEND_DistanceToTargetNode(self) < 32) - { - - - } -*/ -//AQ2 ADD - // Check if there's a door nearby that we cannot trace (like in ACTCITY3) - //Added by Werewolf - if( self->last_door_time < level.framenum - (2.0 + random()) * HZ ) - { - trace_t tTrace; - vec3_t vStart, vDest; - VectorCopy( nodes[self->current_node].origin, vStart); - VectorCopy( nodes[self->next_node].origin, vDest); - // See if we have a clear shot at it - tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID); - if( tTrace.fraction <1.0 ) - { - if( - (strcmp(tTrace.ent->classname,"func_door_rotating")!=0) - && (strcmp(tTrace.ent->classname,"func_door")!=0 ) - ) //No door found, so we can mess with the opendoor func without any danger - { - // Toggle any door that may be nearby - Cmd_OpenDoor_f ( self ); - self->last_door_time = level.framenum + random() * 2.0 * HZ; - } - } - } -//AQ2 END - - //debug_printf("State: %d\n",self->state); - - // Remember where we were, to check if we got stuck. - VectorCopy( self->s.origin, self->lastPosition ); - - // set approximate ping - ucmd.msec = 1000 / BOT_FPS; - self->client->ps.pmove.pm_time = ucmd.msec / 8; - self->client->ping = ucmd.msec; - - // set bot's view angle - ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); - ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); - ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); - - // send command through id's code - ClientThink (self, &ucmd); - - self->nextthink = level.framenum + (game.framerate / BOT_FPS); -} - -/////////////////////////////////////////////////////////////////////// -// Evaluate the best long range goal and send the bot on -// its way. This is a good time waster, so use it sparingly. -// Do not call it for every think cycle. -/////////////////////////////////////////////////////////////////////// -void ACEAI_PickLongRangeGoal(edict_t *self) -{ - - int i = 0; - int node = 0; - float weight = 0.f, best_weight = 0.0; - int current_node = 0, goal_node = 0; - edict_t *goal_ent = NULL; - float cost = 0.f; - - // look for a target - current_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_ALL); - - self->current_node = current_node; - - // Even in teamplay, we wander if no valid node - if(current_node == -1) - { - self->state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - self->goal_node = -1; - return; - } - - //Added by Werewolf - if( self->state == STATE_WANDER ) - self->state = STATE_POSITION; - - //====================== - // Teamplay POSITION state - //====================== - if( self->state == STATE_POSITION ) - { - int counter = 0; - cost = INVALID; - self->goal_node = INVALID; - - // Pick a random node to go to - while( cost == INVALID && counter < 20) // Don't look for too many - { - counter++; - i = (int)(random() * numnodes -1); // Any of the current nodes will do - cost = ACEND_FindCost(current_node, i); - - if(cost == INVALID || cost < 2) // ignore invalid and very short hops - { - cost = INVALID; - i = INVALID; - continue; - } - } - // We have a target node - just go there! - if( i != INVALID ) - { - self->state = STATE_MOVE; - self->tries = 0; // Reset the count of how many times we tried this goal - ACEND_SetGoal(self,i); - self->wander_timeout = level.framenum + 1.0 * HZ; - return; - } - } - - /////////////////////////////////////////////////////// - // Items - /////////////////////////////////////////////////////// - for(i=0;isolid == SOLID_NOT) // ignore items that are not there. - continue; - - cost = ACEND_FindCost(current_node,item_table[i].node); - - if(cost == INVALID || cost < 2) // ignore invalid and very short hops - continue; - - weight = ACEIT_ItemNeed( self, item_table[i].ent ); - - if( weight <= 0 ) // Ignore items we can't pick up. - continue; - - weight *= ( (rand()%5) +1 ); // Allow random variations - weight /= cost; // Check against cost of getting there - - if(weight > best_weight && item_table[i].node != INVALID) - { - best_weight = weight; - goal_node = item_table[i].node; - goal_ent = item_table[i].ent; - } - } - - /////////////////////////////////////////////////////// - // Players - /////////////////////////////////////////////////////// - // This should be its own function and is for now just - // finds a player to set as the goal. - for(i=0;isolid == SOLID_NOT) ) - continue; - - // If it's dark and he's not already our enemy, ignore him - if( self->enemy && players[i] != self->enemy) - { -//Disabled by Werewolf -// if( players[i]->light_level < 30) -// continue; - } - - node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY,NODE_ALL); - // RiEvEr - bug fixing - if( node == INVALID) - cost = INVALID; - else - cost = ACEND_FindCost(current_node, node); - - if(cost == INVALID || cost < 3) // ignore invalid and very short hops - continue; - - // Stop the centipede effect in teamplay - if( teamplay->value ) - { - // Check it's an enemy - // If not an enemy, don't follow him - if( OnSameTeam( self, players[i])) - weight = 0.0; - else - weight = 1.5; //Werewolf: Was 0.3 - } - else - weight = 0.8; //Werewolf: Was 0.3 - - weight *= ( (rand()%5) +1 ); // Allow random variations - weight /= cost; // Check against cost of getting there - - if(weight > best_weight && node != INVALID) - { - best_weight = weight; - goal_node = node; - goal_ent = players[i]; - } - } - - /////////////////////////////////////////////////////// - // Domination Flags - /////////////////////////////////////////////////////// - if( dom->value ) - { - edict_t *flag = NULL; - while(( flag = G_Find( flag, FOFS(classname), "item_flag" ) )) - { - // Only chase flags we don't already control. - if( DomFlagOwner(flag) == self->client->resp.team ) - continue; - - node = ACEND_FindClosestReachableNode( flag, NODE_DENSITY, NODE_ALL ); - if( node == INVALID ) - continue; - - // Always prioritize flags we don't own, so only use cost to differentiate them. - cost = ACEND_FindCost( current_node, node ); - weight = 10000 - cost; - - if( weight > best_weight ) - { - best_weight = weight; - goal_node = node; - goal_ent = flag; - } - } - } - -//Added by Werewolf -//------------------------------- - if(best_weight == 0.0 || goal_node == INVALID ) - { - for(i=0;isolid == SOLID_NOT) || (goal_node != INVALID)) - continue; - if( teamplay->value ) - { - // Check it's an enemy - // If not an enemy, don't follow him - if(!OnSameTeam( self, players[i])) - { - goal_node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY*50,NODE_ALL); - goal_ent = players[i]; - } - } - else - { - goal_node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY*50,NODE_ALL); - goal_ent = players[i]; - } - } - } -//------------------------------- - - // If do not find a goal, go wandering.... - if(best_weight == 0.0 || goal_node == INVALID ) - { - self->goal_node = INVALID; - self->state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - if(debug_mode) - debug_printf("%s did not find a LR goal, wandering.\n",self->client->pers.netname); - return; // no path? - } - - // OK, everything valid, let's start moving to our goal. - self->state = STATE_MOVE; - self->tries = 0; // Reset the count of how many times we tried this goal - - if(goal_ent != NULL && debug_mode) - debug_printf("%s selected a %s at node %d for LR goal.\n",self->client->pers.netname, goal_ent->classname, goal_node); - - ACEND_SetGoal(self,goal_node); - -} - - -/////////////////////////////////////////////////////////////////////// -// Pick best goal based on importance and range. This function -// overrides the long range goal selection for items that -// are very close to the bot and are reachable. -/////////////////////////////////////////////////////////////////////// -void ACEAI_PickShortRangeGoal(edict_t *self) -{ - edict_t *target = NULL; - float weight = 0.f, best_weight = 0.0; - edict_t *best = NULL; - - // look for a target (should make more efficient later) - target = findradius(NULL, self->s.origin, 200); - - while(target) - { - if(target->classname == NULL) - return; - - // Missle avoidance code - // Set our movetarget to be the rocket or grenade fired at us. - if(strcmp(target->classname,"rocket")==0 || strcmp(target->classname,"grenade")==0) - { - if(debug_mode) - debug_printf("ROCKET ALERT!\n"); - - self->movetarget = target; - return; - } - - if (ACEIT_IsReachable(self,target->s.origin)) - { - if (infront(self, target)) - { - weight = ACEIT_ItemNeed( self, target ); - - if(weight > best_weight) - { - best_weight = weight; - best = target; - } - } - } - - // next target - target = findradius(target, self->s.origin, 200); - } - - if(best_weight) - { - self->movetarget = best; - - if(debug_mode && self->goalentity != self->movetarget) - debug_printf("%s selected a %s for SR goal (weight %.1f).\n",self->client->pers.netname, self->movetarget->classname, best_weight); - - self->goalentity = best; - - } - -} - - - -//Werewolf -void ACEAI_PickSafeGoal(edict_t *self) -{ - - int i; - float best_weight=0.0; - int current_node,goal_node = INVALID; - - // look for a target - current_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_ALL); - - self->current_node = current_node; - - i = INVALID; - i = ACEND_FindClosestReachableSafeNode(self,NODE_DENSITY,NODE_ALL); - if( i != INVALID ) - { - self->state = STATE_FLEE; - self->tries = 0; // Reset the count of how many times we tried this goal - ACEND_SetGoal(self,i); - self->wander_timeout = level.framenum + 2.0 * HZ; -// LTK_Say (self, "Under fire, extracting!"); - - return; - } - - - - - // If do not find a goal, go wandering.... - if(best_weight == 0.0 || goal_node == INVALID ) - { - self->goal_node = INVALID; - self->state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - if(debug_mode) - debug_printf("%s did not find a LR goal, wandering.\n",self->client->pers.netname); - return; // no path? - } - - // OK, everything valid, let's start moving to our goal. - self->state = STATE_FLEE; - self->tries = 0; // Reset the count of how many times we tried this goal - -// if(goal_ent != NULL && debug_mode) -// debug_printf("%s selected a %s at node %d for LR goal.\n",self->client->pers.netname, goal_ent->classname, goal_node); - - ACEND_SetGoal(self,goal_node); - -} -//----------------------------------------------------------- - - - - -/////////////////////////////////////////////////////////////////////// -// Scan for enemy (simplifed for now to just pick any visible enemy) -/////////////////////////////////////////////////////////////////////// -// Modified by RiEvEr -// Chooses nearest enemy or last person to shoot us -// -qboolean ACEAI_FindEnemy(edict_t *self, int *total) -{ - int i; - edict_t *bestenemy = NULL; - float bestweight = 99999; - float weight; - vec3_t dist; - vec3_t eyes; - - VectorCopy( self->s.origin, eyes ); - eyes[2] += self->viewheight; - -/* // If we already have an enemy and it is the last enemy to hurt us - if (self->enemy && - (self->enemy == self->client->attacker) && - (!self->enemy->deadflag) && - (self->enemy->solid != SOLID_NOT) - ) - { - return true; - } -*/ - for(i=0;i<=num_players;i++) - { - if(players[i] == NULL || players[i] == self || - players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) ) - continue; - - // If it's dark and he's not already our enemy, ignore him - if( self->enemy && players[i] != self->enemy) - { - if( players[i]->light_level < 30) - continue; - } - -/* if(ctf->value && - self->client->resp.ctf_team == players[i]->client->resp.ctf_team) - continue;*/ -// AQ2 ADD - if(teamplay->value && (team_round_going || lights_camera_action || ! ff_afterround->value) && OnSameTeam( self, players[i]) ) - continue; -// AQ2 END - - if((players[i]->deadflag == DEAD_NO) && ai_visible(self, players[i]) && - gi.inPVS(eyes, players[i]->s.origin) ) - { -// RiEvEr - // Now we assess this enemy - qboolean visible = infront( self, players[i] ); - VectorSubtract(self->s.origin, players[i]->s.origin, dist); - weight = VectorLength( dist ); - - if( ! visible ) - { - // Can we hear their footsteps? - visible = (weight < 300) && !INV_AMMO( players[i], SLIP_NUM ); - } - - if( ! visible ) - { - // Can we hear their weapon firing? - if( players[i]->client->weaponstate == WEAPON_FIRING || players[i]->client->weaponstate == WEAPON_BURSTING ) - { - switch( players[i]->client->weapon->typeNum ) - { - case DUAL_NUM: - case M4_NUM: - case M3_NUM: - case HC_NUM: - visible = true; - break; - case KNIFE_NUM: - case GRENADE_NUM: - break; - default: - visible = !INV_AMMO( players[i], SIL_NUM ); - } - } - } - - if( ! visible ) - { - // Can we see their flashlight? - edict_t *fl = players[i]->client->flashlight; - visible = fl && infront( self, fl ); - if( fl && ! visible ) - { - VectorSubtract( self->s.origin, fl->s.origin, dist ); - visible = (VectorLength(dist) < 100); - } - } - - if( ! visible ) - { - // Can we see their laser sight? - edict_t *laser = players[i]->client->lasersight; - visible = laser && (laser->s.modelindex != level.model_null) && infront( self, laser ) && ai_visible( self, laser ); - } - - // Can we see this enemy, or are they calling attention to themselves? - if( visible ) - { - total+=1; - - // If we can see the enemy flag carrier, always shoot at them. - if( INV_AMMO( players[i], FLAG_T1_NUM ) || INV_AMMO( players[i], FLAG_T2_NUM ) ) - weight = 0; - - // See if it's better than what we have already - if (weight < bestweight) - { - bestweight = weight; - bestenemy = players[i]; - } - } - } - } - // If we found a good enemy set it up - if( bestenemy) - { - self->enemy = bestenemy; - return true; - } - // Check if we've been shot from behind or out of view - if( self->client->attacker && self->client->attacker->inuse && (self->client->attacker != self) ) - { - // Check if it was recent - if( self->client->push_timeout > 0) - { - if( (self->client->attacker->solid != SOLID_NOT) && ai_visible(self, self->client->attacker) && - gi.inPVS(eyes, self->client->attacker->s.origin) ) - { - self->enemy = self->client->attacker; - return true; - } - } - } -//R - // Otherwise signal "no enemy available" - return false; - -} - -/////////////////////////////////////////////////////////////////////// -// Hold fire with RL/BFG? -/////////////////////////////////////////////////////////////////////// -//@@ Modify this to check for hitting teammates in teamplay games. -//Modification request gladly completed by Werewolf :) -qboolean ACEAI_CheckShot(edict_t *self) -{ - trace_t tr; - -//AQ2 tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_OPAQUE); -// tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); -// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); // Werewolf: was enabled - tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_PLAYERSOLID); //Check for friendly fire - - //We would hit something - if (tr.fraction < 0.9) - //If we're in teamplay the the accidentally hit player is a teammate, hold fire - if( (teamplay->value) && team_round_going && (OnSameTeam( self, tr.ent)) ) - return false; - //In deathmatch, don't shoot through glass and stuff -// else if ((!teamplay->value) && (tr.ent->solid==SOLID_BSP)) -// return false; - -// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); // Check for Hard stuff - tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); - if( (tr.fraction < 0.9) && (tr.ent->solid==SOLID_BSP) ) - { - // If we'd hit something solid, see if that's also true aiming for the head. - vec3_t enemy_head; - VectorCopy( self->enemy->s.origin, enemy_head ); - enemy_head[2] += 12.f; // g_combat.c HEAD_HEIGHT - tr = gi.trace( self->s.origin, tv(-2,-2,-2), tv(2,2,2), enemy_head, self, MASK_SOLID|MASK_OPAQUE ); - if( (tr.fraction < 0.9) && (tr.ent->solid==SOLID_BSP) ) - return false; - } - - //else shoot - return true; -} - -/////////////////////////////////////////////////////////////////////// -// Choose the sniper zoom when allowed -/////////////////////////////////////////////////////////////////////// -void _SetSniper( edict_t *ent, int zoom ); -void ACEAI_SetSniper( edict_t *self, int zoom ) -{ - if( (self->client->weaponstate != WEAPON_FIRING) - && (self->client->weaponstate != WEAPON_BUSY) - && ! self->client->bandaging - && ! self->client->bandage_stopped ) - _SetSniper( self, zoom ); -} - -/////////////////////////////////////////////////////////////////////// -// Choose the best weapon for bot (simplified) -// Modified by Werewolf to use sniper zoom -/////////////////////////////////////////////////////////////////////// -//RiEvEr - Changed to boolean. -qboolean ACEAI_ChooseWeapon(edict_t *self) -{ - float range; - vec3_t v; - - // if no enemy, then what are we doing here? - if(!self->enemy) - return (true); -//AQ2 CHANGE - // Currently always favor the dual pistols! - //@@ This will become the "bot choice" weapon -// if(ACEIT_ChangeDualSpecialWeapon(self,FindItem(DUAL_NAME))) -// return; -//AQ2 END - - // Base selection on distance. - VectorSubtract (self->s.origin, self->enemy->s.origin, v); - range = VectorLength(v); - - // Friendly fire after round should be fought with honor. - if( team_round_countdown && ff_afterround->value ) - { - if( ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) - return true; - if( ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) - return true; - if( ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME)) ) - { - self->client->pers.grenade_mode = (range > 500) ? 1 : 0; - return true; - } - } - - // Extreme range - if( range > 1300 ) - { - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 2 ); - return (true); - } - } - - // Longest range - if(range > 1000) - { - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 2 ); - return (true); - } - - if( INV_AMMO(self,SIL_NUM) && ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME)) ) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - } - - // Longer range - if(range > 700) - { - if( INV_AMMO(self,SIL_NUM) ) - { - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - } - - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 2 ); - return (true); - } - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME))) - { - self->client->pers.grenade_mode = 2; - return (true); - } - } - - // Long range - if(range > 500) - { - if( INV_AMMO(self,LASER_NUM) ) - { - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - } - - if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 2 ); - return (true); - } - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME))) - { - self->client->pers.grenade_mode = 1; - return (true); - } - } - - // Medium range - if(range > 200) - { - if( INV_AMMO(self,SLIP_NUM) && ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) - return (true); - - if( INV_AMMO(self,LASER_NUM) ) - { - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - } - - if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME))) - { - self->client->pers.grenade_mode = 1; - return (true); - } - - if(ACEIT_ChangeDualSpecialWeapon(self,FindItem(DUAL_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - - // Raptor007: Throw knives at medium range if we have extras. - if( (INV_AMMO(self,KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 1 ); - return (true); - } - - } - - // Short range - if(ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME))) - return (true); - - if( INV_AMMO(self,LASER_NUM) ) - { - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - } - - if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeDualSpecialWeapon(self,FindItem(DUAL_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME))) - { - self->client->pers.grenade_mode = 0; - return (true); - } - - if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) - { - ACEAI_SetSniper( self, 1 ); - return (true); - } - - - - // We have no weapon available for use. - if(debug_mode) - gi.bprintf(PRINT_HIGH,"%s: No weapon available...\n",self->client->pers.netname); - return (false); - -} - -void ACEAI_Cmd_Choose (edict_t *ent, char *s) -{ - // only works in teamplay - if (!teamplay->value) - return; - - if ( Q_stricmp(s, MP5_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(MP5_NAME); - else if ( Q_stricmp(s, M3_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(M3_NAME); - else if ( Q_stricmp(s, M4_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(M4_NAME); - else if ( Q_stricmp(s, HC_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(HC_NAME); - else if ( Q_stricmp(s, SNIPER_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(SNIPER_NAME); - else if ( Q_stricmp(s, KNIFE_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(KNIFE_NAME); - else if ( Q_stricmp(s, DUAL_NAME) == 0 ) - ent->client->pers.chosenWeapon = FindItem(DUAL_NAME); - else if ( Q_stricmp(s, KEV_NAME) == 0 ) - ent->client->pers.chosenItem = FindItem(KEV_NAME); - else if ( Q_stricmp(s, LASER_NAME) == 0 ) - ent->client->pers.chosenItem = FindItem(LASER_NAME); - else if ( Q_stricmp(s, SLIP_NAME) == 0 ) - ent->client->pers.chosenItem = FindItem(SLIP_NAME); - else if ( Q_stricmp(s, SIL_NAME) == 0 ) - ent->client->pers.chosenItem = FindItem(SIL_NAME); - else if ( Q_stricmp(s, BAND_NAME) == 0 ) - ent->client->pers.chosenItem = FindItem(BAND_NAME); -} - -void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ) -{ - if( !( num && WPF_ALLOWED(num) ) ) - { - int list[ WEAPON_COUNT ] = {0}; - int total = 0; - - // Preferred primary weapons. - if( WPF_ALLOWED(MP5_NUM) ) - { - list[ total ] = MP5_NUM; - total ++; - } - if( WPF_ALLOWED(M4_NUM) ) - { - list[ total ] = M4_NUM; - total ++; - } - if( WPF_ALLOWED(SNIPER_NUM) ) - { - list[ total ] = SNIPER_NUM; - total ++; - } - if( WPF_ALLOWED(M3_NUM) ) - { - list[ total ] = M3_NUM; - total ++; - } - if( WPF_ALLOWED(HC_NUM) ) - { - list[ total ] = HC_NUM; - total ++; - } - - if( total ) - num = list[ rand() % total ]; - // Last resorts if nothing else is allowed. - else if( WPF_ALLOWED(DUAL_NUM) ) - num = DUAL_NUM; - else if( WPF_ALLOWED(KNIFE_NUM) ) - num = KNIFE_NUM; - else if( WPF_ALLOWED(MK23_NUM) ) - num = MK23_NUM; - } - - if( num ) - ent->client->pers.chosenWeapon = FindItemByNum(num); -} - -void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ) -{ - if( !( num && ITF_ALLOWED(num) ) ) - { - int list[ ITEM_COUNT ] = {0}; - int total = 0; - - if( ITF_ALLOWED(KEV_NUM) ) - { - list[ total ] = KEV_NUM; - total ++; - } - if( ITF_ALLOWED(BAND_NUM) ) - { - list[ total ] = BAND_NUM; - total ++; - } - if( ITF_ALLOWED(LASER_NUM) ) - { - list[ total ] = LASER_NUM; - total ++; - } - if( ITF_ALLOWED(SIL_NUM) ) - { - list[ total ] = SIL_NUM; - total ++; - } - if( ITF_ALLOWED(SLIP_NUM) ) - { - list[ total ] = SLIP_NUM; - total ++; - } - if( ITF_ALLOWED(HELM_NUM) ) - { - list[ total ] = HELM_NUM; - total ++; - } - - if( total ) - num = list[ rand() % total ]; - } - - if( num ) - ent->client->pers.chosenItem = FindItemByNum(num); -} +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot_ai.c 6 29/02/00 11:14 Riever $ + * + * $History: acebot_ai.c $ + * + * ***************** Version 6 ***************** + * User: Riever Date: 29/02/00 Time: 11:14 + * Updated in $/LTK2/src/acesrc + * Made bot ChooseWeapon function qboolean so it can signal when no weapon + * was ready for use. + * Moved weapon selection call to be inside attack function so bot knows + * whether it has to fire a weapon or use a kick attack. + * + * ***************** Version 5 ***************** + * User: Riever Date: 24/02/00 Time: 3:05 + * Updated in $/LTK2/src/acesrc + * Added bot say command support. Changed LTG weighting method. + * + * ***************** Version 4 ***************** + * User: Riever Date: 23/02/00 Time: 23:16 + * Updated in $/LTK2/src/acesrc + * Removed random door opening code - bots now MUST have a routefile to + * get through doors. + * Added support for response to 'treport' radio message. + * + * ***************** Version 3 ***************** + * User: Riever Date: 17/02/00 Time: 17:53 + * Updated in $/LTK2/src/acesrc + * Fixed item list to be in the right order! + * + */ +/////////////////////////////////////////////////////////////////////// +// +// acebot_ai.c - This file contains all of the +// AI routines for the ACE II bot. +// +// +// NOTE: I went back and pulled out most of the brains from +// a number of these functions. They can be expanded on +// to provide a "higher" level of AI. +//////////////////////////////////////////////////////////////////////// + +#include "../g_local.h" +#include "acebot.h" +#include "botchat.h" + +void ACEAI_Cmd_Choose( edict_t *ent, char *s); +void ACEMV_ChangeBotAngle (edict_t *ent); +//RiEvEr AQ2 Radio use +//void RadioBroadcast(edict_t *ent, int partner, char *msg); +//R AQ2 + + +/////////////////////////////////////////////////////////////////////// +// Main Think function for bot +/////////////////////////////////////////////////////////////////////// +void ACEAI_Think(edict_t* self) +{ + //Com_Printf("%s\n", __func__); +} // hehe I think not + + +void BOTLIB_PickLongRangeGoal(edict_t* self) +{ + int i = 0; + int node = 0; + float weight = 0.f, best_weight = 0.0; + int goal_node = 0; + edict_t* goal_ent = NULL; + float cost = INVALID; + int counter = 0; + + // Clear old Node -> Node movement types + //self->prev_to_curr_node_type = INVALID; + //self->curr_to_next_node_type = INVALID; + + // Clear old nodes + self->bot.prev_node = INVALID; + self->bot.next_node = INVALID; + self->bot.goal_node = INVALID; + self->current_link = INVALID; // Clear old link + + // Get new current node + int nodelist[MAX_NODELIST]; + int nodes_touched; + /* + // Check if spawn point location touches any nodes + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 64, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) // Make sure we can visit the spawn point node + { + current_node = nodelist[i]; + self->bot.current_node = nodelist[i]; + break; + } + } + */ + //Com_Printf("%s %s [%d] self->bot.current_node[%d]\n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node); + //current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + + if (ctf->value) // CTF has it's own goals + return; + + //Com_Printf("%s %s [%d] self->bot.state == %d\n", __func__, self->client->pers.netname, level.framenum, self->bot.state); + + if (teamplay->value && self->just_spawned_go == false) + self->bot.state = BOT_MOVE_STATE_NAV; + + //Com_Printf("\n%s %s\n", __func__, self->client->pers.netname); + + // Couldn't find a current node so try to wander the map + if (self->bot.current_node == INVALID) + { + //Com_Printf("%s %s [%d] current_node == INVALID\n", __func__, self->client->pers.netname, level.framenum); + self->bot.state = BOT_MOVE_STATE_NAV; // BOT_MOVE_STATE_WANDER + //self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + return; + } + + /* + if (self->bot.state == STATE_ATTACK) + { + if (self->enemy && ACEAI_IsEnemy(self, self->enemy)) // Are they our enemy + { + // Get enemy node + int n = self->enemy->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (BOTLIB_CanVisitNode(self, n)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s visiting enemy %s node %i at %f %f %f\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + BOTLIB_SetGoal(self, n); + } + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + return; + } + */ + + //======================= + // Get navigation + //======================= + //if (self->bot.state == BOT_MOVE_STATE_NAV) + { + //Com_Printf("%s %s [%d] self->bot.state == BOT_MOVE_STATE_NAV\n", __func__, self->client->pers.netname, level.framenum); + + /* + if (0 && random() < 0.1 || (teamplay->value && lights_camera_action)) // Greater than LCA! + { + i = rand() % numnodes; // pick a random node + if (BOTLIB_CanVisitNode(self, nodes[i].nodenum)) + { + //Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); + self->bot.state = BOT_MOVE_STATE_MOVE; + //self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, nodes[i].nodenum); + //self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + + { + //Com_Printf("%s %s BOT_MOVE_STATE_NAV [%d]\n", __func__, self->client->pers.netname, level.framenum); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_WANDER; // BOT_MOVE_STATE_WANDER + //self->wander_timeout = level.framenum + 1.0 * HZ; + return; // no path? + } + } + */ + + //self->bot.get_item = NULL; + //if (BOTLIB_NeedWeaponOrAmmo(self)) + //if (random() < 0.75 && self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME)) + if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME)) + { + //self->bot.get_item = NULL; + //node = BOTLIB_FindDroppedItems(self); + node = BOTLIB_GetEquipment(self); + if (node != INVALID) + { + if (BOTLIB_CanGotoNode(self, nodes[node].nodenum, rand() % 2)) + { + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[node].nodenum); + //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[node].nodenum); + return; + } + } + } + + // Random zero weight nodes (make bot goto nodes rarely visitied) + if (random() < 0.2) + { + for (int j = 0; j < numnodes; j++) + { + i = rand() % numnodes; // Pick a random node + if (nodes[i].weight == 0) // Look for zero weight + break; + } + if (BOTLIB_CanGotoNode(self, nodes[i].nodenum, false)) + { + //Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[i].nodenum); + return; + } + } + + // Check how many bots are sitting on a POI + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (players[p]->is_bot && players[p]->bot.goal_node == poi_nodes[i] && players[p]->solid == SOLID_BBOX) + { + //self->bot.goal_node = INVALID; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + } + + if ((random() < 0.2 || (teamplay->value && lights_camera_action)) && nodes[self->bot.current_node].type != NODE_POI) + { + if (num_poi_nodes) + { + int i = rand() % num_poi_nodes; // Pick a random POI node + + // Check if any other bots have already picked this POI node + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (players[p]->is_bot && players[p]->bot.goal_node == poi_nodes[i] && players[p]->solid == SOLID_BBOX) + { + //Com_Printf("%s %s POI node[%i] already picked by %s\n", __func__, self->client->pers.netname, poi_nodes[i], players[p]->client->pers.netname); + i = INVALID; + break; + } + } + + if (BOTLIB_CanGotoNode(self, nodes[poi_nodes[i]].nodenum, rand() % 2)) // Make sure we can visit the spawn point node + { + //Com_Printf("%s %s visiting POI node[%i]\n", __func__, self->client->pers.netname, nodes[poi_nodes[i]].nodenum); + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[poi_nodes[i]].nodenum); + return; + } + } + } + + // At LCA, randomly pick a spawn spot. This breaks up the bots at LCA, so they spread out. + if (random() < 0.2 || (teamplay->value && lights_camera_action)) // Greater than LCA! + { + // Find all the spawn points + int sp_counter = 0; + vec3_t sp_origin[128]; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + sp_counter++; + } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team1")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + sp_counter++; + } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team2")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + sp_counter++; + } + if (sp_counter) // If we found spawn points + { + byte spot_picked = rand() % sp_counter; // Pick a random spot + + // Check if spawn point location touches any nodes + nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[spot_picked], tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (BOTLIB_CanGotoNode(self, nodelist[i], true)) // Make sure we can visit the spawn point node + { + //if (debug_mode) + // Com_Printf("%s %s visiting spawn spot node[%i] [%f %f %f]\n", __func__, self->client->pers.netname, nodelist[i], nodes[nodelist[i]].origin[0], nodes[nodelist[i]].origin[1], nodes[nodelist[i]].origin[2]); + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodelist[i]); + return; + } + } + } + + return; + } + + // If we have an enemy, go directly for them + if (self->enemy != NULL) + { + qboolean random_path = false; + //if (self->client->pers.chosenItem == FindItemByNum(HC_NUM) || self->client->weapon == FindItemByNum(HC_NUM)) + // random_path = false; + + if (self->bot.see_enemies && self->enemy != NULL && self->enemy->current_node != INVALID) + { + if (BOTLIB_CanGotoNode(self, self->enemy->current_node, random_path)) + { + //if (debug_mode) + // Com_Printf("%s %s visiting enemy %s node %i\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->current_node); + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, self->enemy->current_node); + return; + } + } + + } + + // Try going after a random enemy based on the last node they visited + //if (random() < 0.33 || bot_rush->value == 1 || BOTLIB_EnemiesAlive(self) <= 3) // Just go after them if there's only a few left + { + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + //i = (int)(random() * num_players); // Pick a random player + i = rand() % num_players; // Pick a random player + if (ACEAI_IsEnemy(self, players[i])) // Are they our enemy + { + // Get enemy node + int n = players[i]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (n == INVALID) + continue; + + //if (BOTLIB_CanVisitNode(self, n, rand() % 2)) // Make sure we can visit the node they're at + if (BOTLIB_CanGotoNode(self, n, true)) + { + //if (debug_mode) + // Com_Printf("%s %s visiting enemy %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + } + + // Random node + for (int i = 0; i < 128; i++) + { + int n = rand() % numnodes; // pick a random node + //Com_Printf("%s %s RNG Node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, rand() % 2)) + { + //Com_Printf("%s %s visiting [RNG] node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); + return; + } + } + } + + Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav + return; // no path? +} + +/////////////////////////////////////////////////////////////////////// +// Evaluate the best long range goal and send the bot on +// its way. This is a good time waster, so use it sparingly. +// Do not call it for every think cycle. +/////////////////////////////////////////////////////////////////////// +void ACEAI_PickLongRangeGoal(edict_t *self) +{ +#if 0 + int i = 0; + int node = 0; + float weight = 0.f, best_weight = 0.0; + int current_node = 0, goal_node = 0; + edict_t *goal_ent = NULL; + float cost = 0.f; + + int n, p; + + // Clear old Node -> Node movement types + self->prev_to_curr_node_type = INVALID; + self->curr_to_next_node_type = INVALID; + + // Clear old nodes + self->prev_node = INVALID; + self->next_node = INVALID; + self->goal_node = INVALID; + self->current_link = INVALID; // Clear old link + + // Get new current node + current_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_ALL); + self->current_node = current_node; + + // Even in teamplay, we wander if no valid node + if(current_node == INVALID) + { + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + self->goal_node = INVALID; + return; + } + + //Added by Werewolf + if( self->state == STATE_WANDER ) + self->state = STATE_POSITION; + + //====================== + // Teamplay POSITION state + //====================== + if( self->state == STATE_POSITION ) + { + int counter = 0; + cost = INVALID; + self->goal_node = INVALID; + + //rekkie -- DEV_1 -- s + + if (0) + { + self->state = STATE_MOVE; + self->tries = 0; + if (random() < 0.5) + BOTLIB_SetGoal(self, 5); + else + BOTLIB_SetGoal(self, 0); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + + // At LCA, randomly pick a spawn spot. This breaks up the bots at LCA, so they spread out. + if (0 && teamplay->value && lights_camera_action > 1) // Greater than LCA! + { + byte spot_count = 0, spot_picked; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + spot_count++; + } + if (spot_count) + { + spot_picked = (int)(random() * (spot_count - 1)); // Pick a random spot + spot_count = 0; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + spot_count++; + if (spot_count == spot_picked) + break; + } + + if (spot != NULL) + { + i = ACEND_FindClosestReachableNode(spot, NODE_DENSITY, NODE_ALL); + if (BOTLIB_CanVisitNode(self, i)) // Make sure we can visit the node they're at + { + if (debug_mode) + Com_Printf("%s %s visiting spawn spot node[%i] [%f %f %f]\n", __func__, self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, i); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + } + } + + // Friendly bots sometimes visit allied humans during a match + if (0 && random() < 0.50 && ltk_rush->value == 0) + { + for (p = 0; p < num_players; p++) // Cycle based on how many players we find + { + i = (int)(random() * (num_players - 1)); // Pick a random player + if (players[i]->is_bot == false && players[i]->bot.current_node != 0 && players[i]->solid == SOLID_BBOX && players[i]->deadflag == DEAD_NO && ACEAI_IsEnemy(self, players[i]) == false) // Are they a friend and human? + { + // Get friendly node + n = players[i]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (n == INVALID) + continue; + + if (BOTLIB_CanVisitNode(self, n)) // Make sure we can visit the node they're at + { + if (debug_mode) + Com_Printf("%s %s visiting friendly %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, n); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + } + } + // Try going after a random enemy based on the last node they visited + if (0 && random() < 0.33 || ltk_rush->value == 1 || BOTLIB_EnemiesAlive(self) <= 3) // Just go after them if there's only a few left + { + for (p = 0; p < num_players; p++) // Cycle based on how many players we find + { + i = (int)(random() * num_players); // Pick a random player + if (ACEAI_IsEnemy(self, players[i])) // Are they our enemy + { + // Get enemy node + n = players[i]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (n == INVALID) + continue; + + if (random() < 0.50 && BOTLIB_CanVisitNode(self, n)) // Make sure we can visit the node they're at + { + if (debug_mode) + Com_Printf("%s %s visiting enemy %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, n); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + else if (num_vis_nodes) // Otherwise try to find a visibility node that has sight on the player node + { + int sn = ACEND_GetRandomVisibleNode(n); + if (BOTLIB_CanVisitNode(self, sn)) + { + if (debug_mode) + Com_Printf("%s %s visiting enemy %s [LOS] to node %i from node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, sn, nodes[sn].origin[0], nodes[sn].origin[1], nodes[sn].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, sn); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + } + } + } + // Try going to a Point of Interest (PoI) node + if (0 && num_poi_nodes) // && random() < 0.33) + { + for (int rn = 0; rn < 8; rn++) + { + i = (int)(random() * num_poi_nodes - 1); // Any of the POI nodes will do + if (poi_nodes[i] != INVALID) + { + if (BOTLIB_CanVisitNode(self, i)) + { + if (debug_mode) + debug_printf("%s %s visiting [POI] node %i at %f %f %f\n", __func__, self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, poi_nodes[i]); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + } + } + //rekkie -- DEV_1 -- e + + // Pick a random node to go to + while( cost == INVALID && counter < 20) // Don't look for too many + { + counter++; + // pick a random node + i = rand () % numnodes; + //i = (int)(random() * numnodes -1); // Any of the current nodes will do + + if (nodes[i].inuse == false) continue; // Don't go to nodes not in use + + //rekkie -- DEV_1 -- s + if (BOTLIB_CanVisitNode(self, i) == false) + { + cost = INVALID; + i = INVALID; + continue; + } + // + // Prevent bots from stopping on anything other than a move, spn, or poi node + //if (nodes[i].type != NODE_MOVE && nodes[i].type != NODE_POI && nodes[i].type != NODE_SPAWNPOINT) + //{ + //debug_printf("%s nodes[%i].type != NODE_MOVE\n", __func__, i); + // continue; + //} + // + //if (debug_mode) + // debug_printf("%s Visiting random node %i at %f %f %f\n", __func__, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + //rekkie -- DEV_1 -- e + + cost = ACEND_FindCost(current_node, i); + + if(cost == INVALID || cost < 2) // ignore invalid and very short hops + { + cost = INVALID; + i = INVALID; + continue; + } + } + + //rekkie -- DEV_1 -- s + //if (i != INVALID && nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) + //{ + // ; + //} + //else + //rekkie -- DEV_1 -- e + + // We have a target node - just go there! + if( i != INVALID ) + { + //rekkie -- DEV_1 -- s + //if (debug_mode) + Com_Printf("%s %s visiting [RNG] node %i at %f %f %f\n", __func__, self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + //rekkie -- DEV_1 -- e + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self,i); + self->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + + Com_Printf("%s %s ------ \n", __func__, self->client->pers.netname); + + /////////////////////////////////////////////////////// + // Items + /////////////////////////////////////////////////////// + for(i=0;isolid == SOLID_NOT) // ignore items that are not there. + continue; + + cost = ACEND_FindCost(current_node,item_table[i].node); + + if(cost == INVALID || cost < 2) // ignore invalid and very short hops + continue; + + weight = ACEIT_ItemNeed( self, item_table[i].ent ); + + if( weight <= 0 ) // Ignore items we can't pick up. + continue; + + weight *= ( (rand()%5) +1 ); // Allow random variations + weight /= cost; // Check against cost of getting there + + if(weight > best_weight && item_table[i].node != INVALID) + { + best_weight = weight; + goal_node = item_table[i].node; + goal_ent = item_table[i].ent; + } + } + + /////////////////////////////////////////////////////// + // Players + /////////////////////////////////////////////////////// + // This should be its own function and is for now just + // finds a player to set as the goal. + for(i=0;isolid == SOLID_NOT) ) + continue; + + // If it's dark and he's not already our enemy, ignore him + if( self->enemy && players[i] != self->enemy) + { +//Disabled by Werewolf +// if( players[i]->light_level < 30) +// continue; + } + + node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY,NODE_ALL); + // RiEvEr - bug fixing + if( node == INVALID) + cost = INVALID; + else + cost = ACEND_FindCost(current_node, node); + + if(cost == INVALID || cost < 3) // ignore invalid and very short hops + continue; + + // Stop the centipede effect in teamplay + if( teamplay->value ) + { + // Check it's an enemy + // If not an enemy, don't follow him + if( OnSameTeam( self, players[i])) + weight = 0.0; + else + weight = 1.5; //Werewolf: Was 0.3 + } + else + weight = 0.8; //Werewolf: Was 0.3 + + weight *= ( (rand()%5) +1 ); // Allow random variations + weight /= cost; // Check against cost of getting there + + if(weight > best_weight && node != INVALID) + { + best_weight = weight; + goal_node = node; + goal_ent = players[i]; + } + } + + /////////////////////////////////////////////////////// + // Domination Flags + /////////////////////////////////////////////////////// + if( dom->value ) + { + edict_t *flag = NULL; + while(( flag = G_Find( flag, FOFS(classname), "item_flag" ) )) + { + // Only chase flags we don't already control. + if( DomFlagOwner(flag) == self->client->resp.team ) + continue; + + node = ACEND_FindClosestReachableNode( flag, NODE_DENSITY, NODE_ALL ); + if( node == INVALID ) + continue; + + // Always prioritize flags we don't own, so only use cost to differentiate them. + cost = ACEND_FindCost( current_node, node ); + weight = 10000 - cost; + + if( weight > best_weight ) + { + best_weight = weight; + goal_node = node; + goal_ent = flag; + } + } + } + +//Added by Werewolf +//------------------------------- + if(best_weight == 0.0 || goal_node == INVALID ) + { + for(i=0;isolid == SOLID_NOT) || (goal_node != INVALID)) + continue; + if( teamplay->value ) + { + // Check it's an enemy + // If not an enemy, don't follow him + if(!OnSameTeam( self, players[i])) + { + goal_node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY*50,NODE_ALL); + goal_ent = players[i]; + } + } + else + { + goal_node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY*50,NODE_ALL); + goal_ent = players[i]; + } + } + } +//------------------------------- + + // If do not find a goal, go wandering.... + if(best_weight == 0.0 || goal_node == INVALID ) + { + //rekkie -- DEV_1 -- s + // Try going to a random node + /* + int tries = 0; + while (tries < 256) + { + i = (int)(random() * numnodes - 1); // Any of the POI nodes will do + if (BOTLIB_CanVisitNode(self, i)) + { + if (debug_mode) + debug_printf("%s did not find a LR goal, visiting random node %i at %f %f %f\n", self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, i); + self->wander_timeout = level.framenum * HZ; + return; + } + tries++; + } + */ + //rekkie -- DEV_1 -- e + + self->goal_node = INVALID; + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + //if(debug_mode) + Com_Printf("%s did not find a LR goal, wandering.\n",self->client->pers.netname); + return; // no path? + } + + // OK, everything valid, let's start moving to our goal. + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + + if(goal_ent != NULL && debug_mode) + Com_Printf("%s selected a %s at node %d for LR goal.\n",self->client->pers.netname, goal_ent->classname, goal_node); + + BOTLIB_SetGoal(self,goal_node); +#endif +} + + +/////////////////////////////////////////////////////////////////////// +// Pick best goal based on importance and range. This function +// overrides the long range goal selection for items that +// are very close to the bot and are reachable. +/////////////////////////////////////////////////////////////////////// +void ACEAI_PickShortRangeGoal(edict_t *self) +{ + edict_t *target = NULL; + float weight = 0.f, best_weight = 0.0; + edict_t *best = NULL; + + // look for a target (should make more efficient later) + target = findradius(NULL, self->s.origin, 200); + + while(target) + { + if(target->classname == NULL) + return; + + /* + // Missle avoidance code + // Set our movetarget to be the rocket or grenade fired at us. + if(strcmp(target->classname,"rocket")==0 || strcmp(target->classname,"grenade")==0) + { + if(debug_mode) + debug_printf("ROCKET ALERT!\n"); + + self->movetarget = target; + return; + } + */ + + if (ACEIT_IsReachable(self,target->s.origin)) + { + if (infront(self, target)) + { + weight = ACEIT_ItemNeed( self, target ); + + if(weight > best_weight) + { + best_weight = weight; + best = target; + } + } + } + + // next target + target = findradius(target, self->s.origin, 200); + } + + if(best_weight) + { + self->movetarget = best; + + //if(debug_mode && self->goalentity != self->movetarget) + // debug_printf("%s selected a %s for SR goal (weight %.1f).\n",self->client->pers.netname, self->movetarget->classname, best_weight); + + //self->goalentity = best; + } + +} + + + +//Werewolf +void ACEAI_PickSafeGoal(edict_t *self) +{ + + int i; + float best_weight=0.0; + int current_node,goal_node = INVALID; + + // look for a target + current_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_ALL); + + self->bot.current_node = current_node; + + //rekkie -- DEV_1 -- s + // Wander if no valid node + if (current_node == INVALID) + { + //debug_printf("%s current_node %i is invalid\n", self->client->pers.netname, current_node); + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + return; + } + //rekkie -- DEV_1 -- e + + i = INVALID; + i = ACEND_FindClosestReachableSafeNode(self,NODE_DENSITY,NODE_ALL); + if( i != INVALID ) + { + self->state = STATE_FLEE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self,i); + self->wander_timeout = level.framenum + 2.0 * HZ; +// LTK_Say (self, "Under fire, extracting!"); + + return; + } + + + + // If do not find a goal, go wandering.... + if(best_weight == 0.0 || goal_node == INVALID ) + { + self->bot.goal_node = INVALID; + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + if(debug_mode) + debug_printf("%s did not find a LR goal, wandering.\n",self->client->pers.netname); + return; // no path? + } + + // OK, everything valid, let's start moving to our goal. + self->state = STATE_FLEE; + self->tries = 0; // Reset the count of how many times we tried this goal + +// if(goal_ent != NULL && debug_mode) +// debug_printf("%s selected a %s at node %d for LR goal.\n",self->client->pers.netname, goal_ent->classname, goal_node); + + BOTLIB_SetGoal(self,goal_node); + +} +//----------------------------------------------------------- + +//rekkie -- DEV_1 -- s +/////////////////////////////////////////////////////////////////////// +// Returns true if 'other' is currently our enemy +/////////////////////////////////////////////////////////////////////// +qboolean ACEAI_IsEnemy(edict_t* self, edict_t* other) +{ + if (other == NULL || other == self || other->client == NULL || other->solid == SOLID_NOT || (other->flags & FL_NOTARGET) || other->deadflag != DEAD_NO) + return false; + + if (teamplay->value && (team_round_going || lights_camera_action || !ff_afterround->value) && OnSameTeam(self, other)) + return false; + + return true; +} +/////////////////////////////////////////////////////////////////////// +// Returns how many enemies are alive +/////////////////////////////////////////////////////////////////////// +short BOTLIB_EnemiesAlive(edict_t* self) +{ + short enemies_alive = 0; + for (int i = 0; i <= num_players; i++) + { + if (ACEAI_IsEnemy(self, players[i])) + enemies_alive++; + } + return enemies_alive; +} +/////////////////////////////////////////////////////////////////////// +// Returns how many allies are alive +/////////////////////////////////////////////////////////////////////// +short BOTLIB_AlliesAlive(edict_t* self) +{ + short allies_alive = 0; + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i] == self || players[i]->client == NULL || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) || players[i]->deadflag != DEAD_NO) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + allies_alive++; + } + return allies_alive; +} +//rekkie -- DEV_1 -- e + +// Find visiable allies +short BOTLIB_FindVisibleAllies(edict_t* self) +{ + int i; + edict_t* bestally = NULL; + vec3_t eyes, ally_eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + + //int allies[128]; // Store the allies we find + self->bot.allies_num = 0; + + // Gather visible allies we can engage + for (i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i] == self || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET)) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + //if (teamplay->value && (team_round_going || lights_camera_action || !ff_afterround->value)) + // continue; + + VectorCopy(players[i]->s.origin, ally_eyes); + ally_eyes[2] += players[i]->viewheight; // Get our ally eye level (standing and crouching) + trace_t tr = gi.trace(eyes, NULL, NULL, ally_eyes, self, MASK_SHOT); + if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) + { + self->bot.allies[self->bot.allies_num] = i; + self->bot.allies_num++; + } + } + return self->bot.allies_num; +} + +// Find all viable enemies +qboolean BOTLIB_FindEnemy(edict_t *self) +{ + int i; + const int INFRONT_WEIGHT = 50; + const int VISIBLE_WEIGHT = 100; + edict_t *bestenemy = NULL; + vec3_t eyes, enemy_eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + + //int enemies[128]; // Store the enemies we find + self->bot.enemies_num = 0; + + // If a player enables notarget, disable search for ALL enemies + for (i = 0; i <= num_players; i++) + { + if (players[i] != NULL && players[i]->flags & FL_NOTARGET) + return false; + } + + // Update previous enemy + if (self->enemy != NULL && self->bot.old_enemy != self->enemy) + self->bot.old_enemy = self->enemy; + + + // Add random element by allowing bots to RNG attack teammates after a TP round ends + qboolean can_kill_teammates = false; + if (self->bot.ff_allies_after_rnd && team_round_going == 0 && lights_camera_action == 0 && ((rand() % (3*HZ)) == 1)) // Add some randomness + can_kill_teammates = true; + + + // ALWAYS gather a list of potential enemies. Required for internal and external functions (like BOTLIB_Radio) + // This must come before we check if bot can still attack existing enemy + for (i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i] == self || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) || players[i]->client->uvTime) + continue; + + // If attaked by a team mate after the round ends + if (ff_afterround->value && team_round_going == 0 && lights_camera_action == 0) + { + if (self->client->attacker && self->client->attacker == players[i]) + { + can_kill_teammates = true; // Flag team member as unfriendly + } + } + + // Sometimes attack team mates randomly after a round ends, or if we get attacked by a team mate, + // or the teammate has ff_allies_after_rnd enabled + if (OnSameTeam(self, players[i])) + { + if (players[i]->is_bot && players[i]->bot.ff_allies_after_rnd == true + && team_round_going == 0 && lights_camera_action == 0 && ((rand() % (3*HZ)) == 1)) // Add some randomness + ; // Allow this bot to target other bots that FF after the round ends + else if (can_kill_teammates == false) + continue; // Don't target friendly team members + } + + VectorCopy(players[i]->s.origin, enemy_eyes); + enemy_eyes[2] += players[i]->viewheight; // Get our enemy eye level (standing and crouching) + + if (gi.inPVS(eyes, enemy_eyes)) // Only consider enemies that are in the same PVS as our bot + { + float weight = 0; + + // Target is in front (might not be visible) + //if (infront(self, players[i])) + // weight += INFRONT_WEIGHT; + + // Target is visible + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); + if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) + { + weight += VISIBLE_WEIGHT; + + // Target is in front and visible + if (infront(self, tr.ent)) + weight += INFRONT_WEIGHT; + } + + /* + // Listen for weapon noises. Firing, Bursting, Reloading, Activating + if (INV_AMMO(players[i], SIL_NUM)) // Silencer + { + weight -= 10; // Quiet enemy + } + else // Unsilenced + { + if (players[i]->client->weaponstate == WEAPON_FIRING || players[i]->client->weaponstate == WEAPON_BURSTING) + weight += 10; + if (players[i]->client->weaponstate == WEAPON_RELOADING || players[i]->client->weaponstate == WEAPON_ACTIVATING) + weight += 5; + } + + + + // Listen for footsteps, limited by distance + if (VectorDistance(self->s.origin, players[i]->s.origin) < 300) + { + if (INV_AMMO(players[i], SLIP_NUM)) // Slippers + weight -= 10; // Quiet enemy + else + weight += 10; + } + */ + + if (weight > 0) + { + // Add target to the list of other possibles + self->bot.enemies[self->bot.enemies_num] = i; + self->bot.enemies_weight[self->bot.enemies_num] = weight; + self->bot.enemies_num++; + } + } + } + + // Check if bot can still attack existing enemy + if (self->enemy && self->enemy->solid != SOLID_NOT && (self->enemy->flags & FL_NOTARGET) == 0 && self->enemy->client && self->enemy->client->uvTime == 0) + { + VectorCopy(self->enemy->s.origin, enemy_eyes); + enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) + + // Check if target is visible + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); + if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) + { + VectorCopy(self->enemy->s.origin, self->bot.enemy_seen_loc); // Update last seen location + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); + self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); + self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); + + //Com_Printf("%s %s see %s R[%d vs %d]L\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time, level.framenum); + + if (self->bot.reaction_time > level.framenum) + { + //Com_Printf("%s %s no fire R[%d vs %d]L\n", __func__, self->client->pers.netname, self->bot.reaction_time, level.framenum); + return false; + } + else + { + //Com_Printf("%s %s can fire R[%d vs %d]L\n", __func__, self->client->pers.netname, self->bot.reaction_time, level.framenum); + return true; + } + + //return true; // Go get 'em tiger! + } + + else + { + VectorClear(self->bot.enemy_seen_loc); + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.enemy_seen_time = 0; + self->bot.reaction_time = 0; + self->enemy = NULL; + } + + + + + /* + if (gi.inPVS(eyes, enemy_eyes)) // Only consider enemies that are in the same PVS as our bot + { + qboolean possible_target = false; + + // Listen for (unsilenced) footsteps, limited by distance + if (INV_AMMO(self->enemy, SLIP_NUM) == 0 && VectorDistance(self->s.origin, self->enemy) < 300) + { + possible_target = true; + } + + // Listen for (unsilenced) weapon noises + else if (INV_AMMO(self->enemy, SIL_NUM) == 0 && (self->enemy->client->weaponstate == WEAPON_FIRING || self->enemy->client->weaponstate == WEAPON_BURSTING)) + { + possible_target = true; + } + // Target is in front + else if (infront(self, self->enemy)) // Visible enemies + { + possible_target = true; + } + + if (possible_target) + { + // Check if target is visible + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); + if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) + { + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); + self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); + self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); + return true; // Go get 'em tiger! + } + } + } + */ + + /* + int mask = (MASK_SHOT | MASK_WATER); // Default mask - don't let bot see enemies through water + int pc_self = gi.pointcontents(self->s.origin); // Bot in contents + int pc_enemy = gi.pointcontents(self->enemy->s.origin); // Player in contents + if ( (pc_self & MASK_WATER) && (pc_enemy & MASK_WATER) ) // Bot and enemy are both underwater + { + mask = MASK_SHOT; // Don't mask through water so the bot can see the enemy underwater + } + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, mask); + if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) + { + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); + self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); + self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); + return true; + } + */ + } + else + { + self->bot.enemy_chase_time = 0; // Reset chase delay set for any previous enemies + } + + if (self->bot.enemies_num) + { + // 1) Target high priority enemies (flag carriers, briefcase, vip, etc) + for (i = 0; i < self->bot.enemies_num; i++) + { + if (self->bot.enemies_weight[i] > 300 && INV_AMMO(players[self->bot.enemies[i]], FLAG_T1_NUM) || INV_AMMO(players[self->bot.enemies[i]], FLAG_T2_NUM)) + bestenemy = players[self->bot.enemies[i]]; + } + + // 2) Target enemy based on their weight + if (bestenemy == NULL) + { + float best_weight = 0; + for (i = 0; i < self->bot.enemies_num; i++) + { + if (self->bot.enemies_weight[i] >= (INFRONT_WEIGHT + VISIBLE_WEIGHT) && self->bot.enemies_weight[i] > best_weight) + { + best_weight = self->bot.enemies_weight[i]; + bestenemy = players[self->bot.enemies[i]]; + } + } + } + + // 3) Target enemies that get too close + //if (bestenemy == NULL) + { + float dist; + float closest = 9999999; + int closest_enemy = 0; + for (i = 0; i < self->bot.enemies_num; i++) + { + dist = VectorDistance(self->s.origin, players[self->bot.enemies[i]]->s.origin); + if (dist < closest) + { + closest_enemy = i; + closest = dist; + } + } + // If enemy is super close, go after them + if (closest < 64) + { + bestenemy = players[self->bot.enemies[closest_enemy]]; + } + } + + /* + // 4) Target a random enemy + if (bestenemy == NULL) + { + int rnd_enemy = rand() % self->bot.enemies_num; + if (self->bot.enemies_num) + { + bestenemy = players[self->bot.enemies[rnd_enemy]]; + } + } + */ + } + + // If we found an enemy + if (bestenemy != NULL) + { + self->enemy = bestenemy; + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); + self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); + self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); + + self->bot.reaction_time = level.framenum + 0.75 * HZ; + //Com_Printf("%s %s see %s R[%d vs %d]L\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time, level.framenum); + + /* + // The bot's reaction time when facing a new enemy + self->bot.reaction_time -= FRAMETIME; // Reduce reaction time + if (self->bot.reaction_time < 0) + self->bot.reaction_time = 0; + if (self->bot.reaction_time > bot_reaction->value) + self->bot.reaction_time = bot_reaction->value; + //Com_Printf("%s %s remember enemy [%s] reaction_time[%f]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time); + + if (self->bot.reaction_time == 0) // If we have no reaction time left, we can shoot + return true; + else + return false; // Waiting for reaction time to expire. + */ + } + return false; + + +#if 0 + + int i; + edict_t* bestenemy = NULL; + //float bestweight = 99999; + //float weight; + //vec3_t dist; + //vec3_t eyes; + + int enemies[128]; // Store the enemies we find + self->bot.enemies_num = 0; + + //VectorCopy( self->s.origin, eyes ); + //eyes[2] += self->viewheight; + +/* // If we already have an enemy and it is the last enemy to hurt us + if (self->enemy && + (self->enemy == self->client->attacker) && + (!self->enemy->deadflag) && + (self->enemy->solid != SOLID_NOT) + ) + { + return true; + } +*/ + for(i=0;i<=num_players;i++) + { + if(players[i] == NULL || players[i] == self || + players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) ) + continue; + + // If it's dark and he's not already our enemy, ignore him + if( self->enemy && players[i] != self->enemy) + { + if( players[i]->light_level < 30) + continue; + } + +/* if(ctf->value && + self->client->resp.ctf_team == players[i]->client->resp.ctf_team) + continue;*/ +// AQ2 ADD + if(teamplay->value && (team_round_going || lights_camera_action || ! ff_afterround->value) && OnSameTeam( self, players[i]) ) + continue; +// AQ2 END + + if((players[i]->deadflag == DEAD_NO) && ai_visible(self, players[i]) && + gi.inPVS(eyes, players[i]->s.origin) ) + { +// RiEvEr + // Now we assess this enemy + qboolean visible = infront( self, players[i] ); + VectorSubtract(self->s.origin, players[i]->s.origin, dist); + weight = VectorLength( dist ); + + if( ! visible ) + { + // Can we hear their footsteps? + visible = (weight < 300) && !INV_AMMO( players[i], SLIP_NUM ); + } + + if( ! visible ) + { + // Can we hear their weapon firing? + if( players[i]->client->weaponstate == WEAPON_FIRING || players[i]->client->weaponstate == WEAPON_BURSTING ) + { + switch( players[i]->client->weapon->typeNum ) + { + case DUAL_NUM: + case M4_NUM: + case M3_NUM: + case HC_NUM: + visible = true; + break; + case KNIFE_NUM: + case GRENADE_NUM: + break; + default: + visible = !INV_AMMO( players[i], SIL_NUM ); + } + } + } + + if( ! visible ) + { + // Can we see their flashlight? + edict_t *fl = players[i]->client->flashlight; + visible = fl && infront( self, fl ); + if( fl && ! visible ) + { + VectorSubtract( self->s.origin, fl->s.origin, dist ); + visible = (VectorLength(dist) < 100); + } + } + + if( ! visible ) + { + // Can we see their laser sight? + edict_t *laser = players[i]->client->lasersight; + visible = laser && (laser->s.modelindex != level.model_null) && infront( self, laser ) && ai_visible( self, laser ); + } + + // Can we see this enemy, or are they calling attention to themselves? + if( visible ) + { + self->bot.enemies_num++; + + // If we can see the enemy flag carrier, always shoot at them. + if( INV_AMMO( players[i], FLAG_T1_NUM ) || INV_AMMO( players[i], FLAG_T2_NUM ) ) + weight = 0; + + // See if it's better than what we have already + if (weight < bestweight) + { + bestweight = weight; + bestenemy = players[i]; + } + } + } + } + // If we found a good enemy set it up + if( bestenemy) + { + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw them + self->enemy = bestenemy; + return true; + } + // Check if we've been shot from behind or out of view + if( self->client->attacker && self->client->attacker->inuse && (self->client->attacker != self) ) + { + // Check if it was recent + if( self->client->push_timeout > 0) + { + if( (self->client->attacker->solid != SOLID_NOT) && ai_visible(self, self->client->attacker) && + gi.inPVS(eyes, self->client->attacker->s.origin) ) + { + self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw them + self->enemy = self->client->attacker; + return true; + } + } + } +//R +#endif + + if (0) + { + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.enemy_seen_time = 0; + self->bot.reaction_time = bot_reaction->value; + self->enemy = NULL; // Reset any enemies, including any the bot remembered... + return false; + } + + + // Check if we have an existing enemy, try to remember them and follow their movements even when behind a wall + if (self->enemy && self->enemy->deadflag == DEAD_NO && self->enemy->health > 0 && self->enemy->solid == SOLID_BBOX) + { + if (self->bot.enemy_seen_time > level.framenum) // Remember enemy + { + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.reaction_time += FRAMETIME; // Gradually forget + //Com_Printf("%s %s remember enemy [%s] reaction_time[%f]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time); + return false; + } + } + // Otherwise forget them + else + { + VectorClear(self->bot.enemy_seen_loc); + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.enemy_seen_time = 0; + self->bot.reaction_time = bot_reaction->value; + self->enemy = NULL; // Reset any enemies, including any the bot remembered... + } + + + // Otherwise signal no enemy + return false; + +} + +/////////////////////////////////////////////////////////////////////// +// Hold fire with RL/BFG? +/////////////////////////////////////////////////////////////////////// +//@@ Modify this to check for hitting teammates in teamplay games. +//Modification request gladly completed by Werewolf :) +qboolean ACEAI_CheckShot(edict_t *self) +{ + trace_t tr; + +//AQ2 tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_OPAQUE); +// tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); +// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); // Werewolf: was enabled + tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_PLAYERSOLID); //Check for friendly fire + + //We would hit something + if (tr.fraction < 0.9) + //If we're in teamplay the the accidentally hit player is a teammate, hold fire + if( (teamplay->value) && team_round_going && (OnSameTeam( self, tr.ent)) ) + return false; + //In deathmatch, don't shoot through glass and stuff +// else if ((!teamplay->value) && (tr.ent->solid==SOLID_BSP)) +// return false; + +// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); // Check for Hard stuff + tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), self->enemy->s.origin, self, MASK_SOLID|MASK_OPAQUE); + if( (tr.fraction < 0.9) && (tr.ent->solid==SOLID_BSP) ) + { + // If we'd hit something solid, see if that's also true aiming for the head. + vec3_t enemy_head; + VectorCopy( self->enemy->s.origin, enemy_head ); + enemy_head[2] += 12.f; // g_combat.c HEAD_HEIGHT + tr = gi.trace( self->s.origin, tv(-2,-2,-2), tv(2,2,2), enemy_head, self, MASK_SOLID|MASK_OPAQUE ); + if( (tr.fraction < 0.9) && (tr.ent->solid==SOLID_BSP) ) + return false; + } + + //else shoot + return true; +} + + +/////////////////////////////////////////////////////////////////////// +// Choose the best weapon for bot (simplified) +// Modified by Werewolf to use sniper zoom +/////////////////////////////////////////////////////////////////////// +//RiEvEr - Changed to boolean. +qboolean ACEAI_ChooseWeapon(edict_t *self) +{ + // if no enemy, then what are we doing here? + if(!self->enemy) + return (true); + + if (self->bot.last_weapon_change_time > level.framenum) + return true; + self->bot.last_weapon_change_time = level.framenum + 6 * HZ; + + //if (self->client->weaponstate != WEAPON_READY) + // return true; + + // Let bots use the grenades if already using + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + return true; + // Let bots use the knives if already using + if (self->client->weapon == FindItemByNum(KNIFE_NUM)) + return true; + +//AQ2 CHANGE + // Currently always favor the dual pistols! + //@@ This will become the "bot choice" weapon +// if(ACEIT_ChangeDualSpecialWeapon(self,FindItem(DUAL_NAME))) +// return; +//AQ2 END + + + /* + // Friendly fire after round should be fought with honor. + if( team_round_countdown && ff_afterround->value ) + { + if( ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) + return true; + if( ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) + return true; + if( ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME)) ) + { + self->client->pers.grenade_mode = ( self->bot.enemy_dist > 500) ? 1 : 0; + return true; + } + } + */ + + /* + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + { + if (self->client->weaponstate == WEAPON_ACTIVATING || self->client->weaponstate == WEAPON_FIRING) + return true; + } + */ + + //rekkie -- DEV_1 -- s + // Force bots use grenades only + /* + if (ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) + { + self->client->pers.grenade_mode = 2; + return true; + } + */ + //rekkie -- DEV_1 -- e + + + + // Extreme range + if(self->bot.enemy_dist > 1300 ) + { + if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper( self, 2 ); + return (true); + } + } + + // Longest range + if(self->bot.enemy_dist > 1000) + { + // Random chance to pull out grenades + if ((rand() % 3) == 0 && ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 6) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (ACEIT_ChangeSniperSpecialWeapon(self, FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper(self, 2); + return (true); + } + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + + //if( INV_AMMO(self,SIL_NUM) && ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME)) ) + // return (true); + + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + // Allow bots that picked throwing knives to use them at a distance + if (INV_AMMO(self, KNIFE_NUM) >= 2 && self->client->pers.chosenItem == FindItemByNum(KNIFE_NUM) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (ACEIT_ChangeDualSpecialWeapon(self, FindItem(DUAL_NAME))) + return (true); + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && self->client->bleeding == 0 + && ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) + return (true); + } + + // Longer range + if(self->bot.enemy_dist > 700) + { + // Random chance to pull out grenades + if ((rand() % 2) == 0 && ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + { + self->client->pers.grenade_mode = 2; + return (true); + } + // Random chance to pull out knives + if ((rand() % 3) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (ACEIT_ChangeSniperSpecialWeapon(self, FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper(self, 2); + return (true); + } + + if( INV_AMMO(self,SIL_NUM) ) + { + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) + return (true); + } + + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) + return (true); + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return (true); + } + + if (ACEIT_ChangeDualSpecialWeapon(self, FindItem(DUAL_NAME))) + return (true); + + if (ACEIT_ChangeMK23SpecialWeapon(self, FindItem(MK23_NAME))) + return (true); + + if (ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + } + + // Long range + if(self->bot.enemy_dist > 500) + { + // Random chance to pull out grenades + if ((rand() % 4) == 0 && ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + { + self->client->pers.grenade_mode = 1; + return (true); + } + // Random chance to pull out knives + if ((rand() % 4) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if( INV_AMMO(self,LASER_NUM) ) + { + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + } + + if(ACEIT_ChangeM3SpecialWeapon(self,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeSniperSpecialWeapon(self,FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper( self, 2 ); + return (true); + } + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return (true); + } + + if (ACEIT_ChangeDualSpecialWeapon(self, FindItem(DUAL_NAME))) + return (true); + + if (ACEIT_ChangeMK23SpecialWeapon(self, FindItem(MK23_NAME))) + return (true); + + if (ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + } + + // Medium range + if(self->bot.enemy_dist > 200) + { + //if( INV_AMMO(self,SLIP_NUM) && ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) + //return (true); + + // Always HC at this range + if (ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + + // Random chance to pull out grenades + if ((rand() % 10) == 0 && ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + { + self->client->pers.grenade_mode = 1; + return (true); + } + // Random chance to pull out knives + if ((rand() % 5) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (ACEIT_ChangeM3SpecialWeapon(self, FindItem(M3_NAME))) + return (true); + + if( INV_AMMO(self,LASER_NUM) ) + { + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + } + + if(ACEIT_ChangeM4SpecialWeapon(self,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(self,FindItem(MP5_NAME))) + return (true); + + //rekkie -- DEV_1 -- s + // Force bots to sniper up close + if (ACEIT_ChangeSniperSpecialWeapon(self, FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper(self, 1); + return (true); + } + //rekkie -- DEV_1 -- e + + // Raptor007: Throw knives at medium range if we have extras. + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return (true); + } + + if(ACEIT_ChangeDualSpecialWeapon(self,FindItem(DUAL_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(self,FindItem(MK23_NAME))) + return (true); + + if (ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + return (true); + } + + // Short range + { + if (ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return (true); + + if ((rand() % 5) == 0 && ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + { + self->client->pers.grenade_mode = 0; + return (true); + } + + if (INV_AMMO(self, LASER_NUM)) + { + if (ACEIT_ChangeM4SpecialWeapon(self, FindItem(M4_NAME))) + return (true); + + if (ACEIT_ChangeMP5SpecialWeapon(self, FindItem(MP5_NAME))) + return (true); + } + + if (ACEIT_ChangeM3SpecialWeapon(self, FindItem(M3_NAME))) + return (true); + + if (ACEIT_ChangeM4SpecialWeapon(self, FindItem(M4_NAME))) + return (true); + + if (ACEIT_ChangeMP5SpecialWeapon(self, FindItem(MP5_NAME))) + return (true); + + //rekkie -- DEV_1 -- s + // Force bots to knife and sniper up close + if (ACEIT_ChangeSniperSpecialWeapon(self, FindItem(SNIPER_NAME))) + { + //ACEAI_SetSniper(self, 1); + return (true); + } + //rekkie -- DEV_1 -- e + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return (true); + } + + if (ACEIT_ChangeDualSpecialWeapon(self, FindItem(DUAL_NAME))) + return (true); + + if (ACEIT_ChangeMK23SpecialWeapon(self, FindItem(MK23_NAME))) + return (true); + + if (ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + return (true); + + // Should punch as a last resort? + } + + + // We have no weapon available for use. + if(debug_mode) + gi.bprintf(PRINT_HIGH,"%s: No weapon available...\n",self->client->pers.netname); + return (false); +} + +void ACEAI_Cmd_Choose (edict_t *ent, char *s) +{ + // only works in teamplay + if (!teamplay->value) + return; + + if ( Q_stricmp(s, MP5_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(MP5_NAME); + else if ( Q_stricmp(s, M3_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(M3_NAME); + else if ( Q_stricmp(s, M4_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(M4_NAME); + else if ( Q_stricmp(s, HC_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(HC_NAME); + else if ( Q_stricmp(s, SNIPER_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(SNIPER_NAME); + else if ( Q_stricmp(s, KNIFE_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(KNIFE_NAME); + else if ( Q_stricmp(s, DUAL_NAME) == 0 ) + ent->client->pers.chosenWeapon = FindItem(DUAL_NAME); + else if ( Q_stricmp(s, KEV_NAME) == 0 ) + ent->client->pers.chosenItem = FindItem(KEV_NAME); + else if ( Q_stricmp(s, LASER_NAME) == 0 ) + ent->client->pers.chosenItem = FindItem(LASER_NAME); + else if ( Q_stricmp(s, SLIP_NAME) == 0 ) + ent->client->pers.chosenItem = FindItem(SLIP_NAME); + else if ( Q_stricmp(s, SIL_NAME) == 0 ) + ent->client->pers.chosenItem = FindItem(SIL_NAME); + else if ( Q_stricmp(s, BAND_NAME) == 0 ) + ent->client->pers.chosenItem = FindItem(BAND_NAME); +} + +void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ) +{ + if( !( num && WPF_ALLOWED(num) ) ) + { + int list[ WEAPON_COUNT ] = {0}; + int total = 0; + + // Preferred primary weapons. + if( WPF_ALLOWED(MP5_NUM) ) + { + list[ total ] = MP5_NUM; + total ++; + } + if( WPF_ALLOWED(M4_NUM) ) + { + list[ total ] = M4_NUM; + total ++; + } + if( WPF_ALLOWED(SNIPER_NUM) ) + { + list[ total ] = SNIPER_NUM; + total ++; + } + if( WPF_ALLOWED(M3_NUM) ) + { + list[ total ] = M3_NUM; + total ++; + } + if( WPF_ALLOWED(HC_NUM) ) + { + list[ total ] = HC_NUM; + total ++; + } + + if( total ) + num = list[ rand() % total ]; + // Last resorts if nothing else is allowed. + else if( WPF_ALLOWED(DUAL_NUM) ) + num = DUAL_NUM; + else if( WPF_ALLOWED(KNIFE_NUM) ) + num = KNIFE_NUM; + else if( WPF_ALLOWED(MK23_NUM) ) + num = MK23_NUM; + } + + if( num ) + ent->client->pers.chosenWeapon = FindItemByNum(num); +} + +void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ) +{ + if( !( num && ITF_ALLOWED(num) ) ) + { + int list[ ITEM_COUNT ] = {0}; + int total = 0; + + if( ITF_ALLOWED(KEV_NUM) ) + { + list[ total ] = KEV_NUM; + total ++; + } + if( ITF_ALLOWED(BAND_NUM) ) + { + list[ total ] = BAND_NUM; + total ++; + } + if( ITF_ALLOWED(LASER_NUM) ) + { + list[ total ] = LASER_NUM; + total ++; + } + if( ITF_ALLOWED(SIL_NUM) ) + { + list[ total ] = SIL_NUM; + total ++; + } + if( ITF_ALLOWED(SLIP_NUM) ) + { + list[ total ] = SLIP_NUM; + total ++; + } + if( ITF_ALLOWED(HELM_NUM) ) + { + list[ total ] = HELM_NUM; + total ++; + } + + if( total ) + num = list[ rand() % total ]; + } + + if( num ) + ent->client->pers.chosenItem = FindItemByNum(num); +} diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 7287e3289..2dd155603 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -1,237 +1,238 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot_cmds.c 2 23/02/00 17:24 Riever $ - * - * $History: acebot_cmds.c $ - * - * ***************** Version 2 ***************** - * User: Riever Date: 23/02/00 Time: 17:24 - * Updated in $/LTK2/src/acesrc - * Added support for 'sv shownodes on/off' - * Enabled creation of nodes for ALL doors. (Stage 1 of new method) - * - */ - -/////////////////////////////////////////////////////////////////////// -// -// acebot_cmds.c - Main internal command processor -// -/////////////////////////////////////////////////////////////////////// - -#include "../g_local.h" - - -qboolean debug_mode=false; -qboolean shownodes_mode=false; //RiEvEr - new node showing method - -/////////////////////////////////////////////////////////////////////// -// Special command processor -/////////////////////////////////////////////////////////////////////// -qboolean ACECM_Commands(edict_t *ent) -{ - char *cmd; - int node; - - cmd = gi.argv(0); - - if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) - ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); - - else if(Q_stricmp (cmd, "removelink") == 0 && debug_mode) - ACEND_RemoveNodeEdge(ent,atoi(gi.argv(1)), atoi(gi.argv(2))); - - else if(Q_stricmp (cmd, "addlink") == 0 && debug_mode) - ACEND_UpdateNodeEdge(ent, atoi(gi.argv(1)), atoi(gi.argv(2))); - - else if(Q_stricmp (cmd, "showpath") == 0 && debug_mode) - ACEND_ShowPath(ent,atoi(gi.argv(1))); - - else if(Q_stricmp (cmd, "shownode") == 0 && debug_mode) - ACEND_ShowNode(atoi(gi.argv(1))); - - else if(Q_stricmp (cmd, "findnode") == 0 && debug_mode) - { - node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : ACEND_FindClosestReachableNode(ent,NODE_DENSITY, NODE_ALL); - gi.bprintf(PRINT_MEDIUM,"node: %d type: %d x: %f y: %f z %f\n",node,nodes[node].type,nodes[node].origin[0],nodes[node].origin[1],nodes[node].origin[2]); - } - - else if(Q_stricmp (cmd, "movenode") == 0 && debug_mode) - { - node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : (numnodes - 1); - - if( gi.argc() >= 5 ) - { - nodes[node].origin[0] = atof(gi.argv(2)); - nodes[node].origin[1] = atof(gi.argv(3)); - nodes[node].origin[2] = atof(gi.argv(4)); - } - else - { - VectorCopy( ent->s.origin, nodes[node].origin ); - nodes[node].origin[2] += 8; - } - - gi.bprintf(PRINT_MEDIUM,"node: %d moved to x: %f y: %f z %f\n",node, nodes[node].origin[0],nodes[node].origin[1],nodes[node].origin[2]); - } - - else if(Q_stricmp (cmd, "nodetype") == 0 && debug_mode) - { - node = atoi(gi.argv(1)); - - if( gi.argc() >= 3 ) - nodes[node].type = atoi(gi.argv(2)); - - gi.bprintf(PRINT_MEDIUM,"node %d type %d\n", node, nodes[node].type); - } - - else if(Q_stricmp (cmd, "showlinks") == 0 && debug_mode) - { - int i; - node = atoi(gi.argv(1)); - for( i = 0; i < MAXLINKS; i ++ ) - { - int target = nodes[node].links[i].targetNode; - if( target != INVALID ) - gi.bprintf( PRINT_MEDIUM, "link: %d -> %d\n", node, nodes[node].links[i].targetNode ); - } - } - - else if(Q_stricmp (cmd, "showallnodes") == 0 && debug_mode) - { - int i; - for( i = 1; i < numnodes; i ++ ) - ACEND_ShowNode(i); - } - - else if(Q_stricmp (cmd, "botgoal") == 0 && debug_mode) - { - int i; - node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : ACEND_FindClosestReachableNode( ent, NODE_DENSITY, NODE_ALL ); - for( i = 1; i <= game.maxclients; i ++ ) - { - edict_t *ent = &(g_edicts[ i ]); - if( ent->inuse && ent->is_bot && IS_ALIVE(ent) ) - { - AntInitSearch( ent ); - ACEND_SetGoal( ent, node ); - ent->state = STATE_MOVE; - ent->node_timeout = 0; - } - } - } - - else - return false; - - return true; -} - - -/////////////////////////////////////////////////////////////////////// -// Called when the level changes, store maps and bots (disconnected) -/////////////////////////////////////////////////////////////////////// -void ACECM_Store(void) -{ - // Stop overwriting good node tables with bad! - if( numnodes < 100 ) - return; - - ACEND_SaveNodes(); -} - -/////////////////////////////////////////////////////////////////////// -// These routines are bot safe print routines, all id code needs to be -// changed to these so the bots do not blow up on messages sent to them. -// Do a find and replace on all code that matches the below criteria. -// -// (Got the basic idea from Ridah) -// -// change: gi.cprintf to safe_cprintf -// change: gi.centerprintf to safe_centerprintf -// -/////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////// -// Debug print, could add a "logging" feature to print to a file -/////////////////////////////////////////////////////////////////////// -void debug_printf(char *fmt, ...) -{ - int i; - char bigbuffer[0x10000]; - //int len; - va_list argptr; - edict_t *cl_ent; - - va_start (argptr,fmt); - //len = vsprintf (bigbuffer,fmt,argptr); - vsprintf (bigbuffer,fmt,argptr); - va_end (argptr); - - if (dedicated->value) - gi.cprintf(NULL, PRINT_MEDIUM, "%s", bigbuffer); - - for (i=0 ; iinuse || cl_ent->is_bot) - continue; - - gi.cprintf(cl_ent, PRINT_MEDIUM, "%s", bigbuffer); - } - -} - -void (*real_cprintf) (edict_t * ent, int printlevel, const char *fmt, ...) = NULL; -void (*real_centerprintf) (edict_t * ent, const char *fmt, ...) = NULL; - -/////////////////////////////////////////////////////////////////////// -// botsafe cprintf -/////////////////////////////////////////////////////////////////////// -void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...) -{ - char bigbuffer[0x10000]; - va_list argptr; - //int len; - - if (ent && (!ent->inuse || ent->is_bot)) - return; - - va_start (argptr,fmt); - //len = vsprintf (bigbuffer,fmt,argptr); - vsprintf (bigbuffer,fmt,argptr); - va_end (argptr); - - real_cprintf(ent, printlevel, "%s", bigbuffer); - -} - -/////////////////////////////////////////////////////////////////////// -// botsafe centerprintf -/////////////////////////////////////////////////////////////////////// -void safe_centerprintf (edict_t *ent, const char *fmt, ...) -{ - char bigbuffer[0x10000]; - va_list argptr; - //int len; - - if (!ent->inuse || ent->is_bot) - return; - - va_start (argptr,fmt); - //len = vsprintf (bigbuffer,fmt,argptr); - vsprintf (bigbuffer,fmt,argptr); - va_end (argptr); - - real_centerprintf(ent, "%s", bigbuffer); - -} +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot_cmds.c 2 23/02/00 17:24 Riever $ + * + * $History: acebot_cmds.c $ + * + * ***************** Version 2 ***************** + * User: Riever Date: 23/02/00 Time: 17:24 + * Updated in $/LTK2/src/acesrc + * Added support for 'sv shownodes on/off' + * Enabled creation of nodes for ALL doors. (Stage 1 of new method) + * + */ + +/////////////////////////////////////////////////////////////////////// +// +// acebot_cmds.c - Main internal command processor +// +/////////////////////////////////////////////////////////////////////// + +#include "../g_local.h" + + +qboolean debug_mode=false; +qboolean shownodes_mode=false; //RiEvEr - new node showing method + +/////////////////////////////////////////////////////////////////////// +// Special command processor +/////////////////////////////////////////////////////////////////////// +qboolean ACECM_Commands(edict_t *ent) +{ + char *cmd; + int node; + + cmd = gi.argv(0); + + //if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) + // ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); + + if(Q_stricmp (cmd, "removelink") == 0 && debug_mode) + ACEND_RemoveNodeEdge(ent,atoi(gi.argv(1)), atoi(gi.argv(2))); + + //else if(Q_stricmp (cmd, "addlink") == 0 && debug_mode) + // ACEND_UpdateNodeEdge(ent, atoi(gi.argv(1)), atoi(gi.argv(2))); + + else if(Q_stricmp (cmd, "showpath") == 0 && debug_mode) + ACEND_ShowPath(ent,atoi(gi.argv(1))); + + else if(Q_stricmp (cmd, "shownode") == 0 && debug_mode) + ACEND_ShowNode(atoi(gi.argv(1))); + + else if(Q_stricmp (cmd, "findnode") == 0 && debug_mode) + { + node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : ACEND_FindClosestReachableNode(ent,NODE_DENSITY, NODE_ALL); + gi.bprintf(PRINT_MEDIUM,"node: %d type: %d x: %f y: %f z %f\n",node, nodes[node].type, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + } + + else if(Q_stricmp (cmd, "movenode") == 0 && debug_mode) + { + node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : (numnodes - 1); + + if( gi.argc() >= 5 ) + { + nodes[node].origin[0] = atof(gi.argv(2)); + nodes[node].origin[1] = atof(gi.argv(3)); + nodes[node].origin[2] = atof(gi.argv(4)); + } + else + { + VectorCopy( ent->s.origin, nodes[node].origin ); + nodes[node].origin[2] += 8; + } + + gi.bprintf(PRINT_MEDIUM,"node: %d moved to x: %f y: %f z %f\n",node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + } + + else if(Q_stricmp (cmd, "nodetype") == 0 && debug_mode) + { + node = atoi(gi.argv(1)); + + if( gi.argc() >= 3 ) + nodes[node].type = atoi(gi.argv(2)); + + gi.bprintf(PRINT_MEDIUM,"node %d type %d\n", node, nodes[node].type); + } + + else if(Q_stricmp (cmd, "showlinks") == 0 && debug_mode) + { + int i; + node = atoi(gi.argv(1)); + for( i = 0; i < MAXLINKS; i ++ ) + { + int target = nodes[node].links[i].targetNode; + if( target != INVALID ) + gi.bprintf( PRINT_MEDIUM, "link: %d -> %d\n", node, nodes[node].links[i].targetNode ); + } + } + + else if(Q_stricmp (cmd, "showallnodes") == 0 && debug_mode) + { + int i; + for( i = 1; i < numnodes; i ++ ) + ACEND_ShowNode(i); + } + + else if(Q_stricmp (cmd, "botgoal") == 0 && debug_mode) + { + int i; + node = (gi.argc() >= 2) ? atoi(gi.argv(1)) : ACEND_FindClosestReachableNode( ent, NODE_DENSITY, NODE_ALL ); + for( i = 1; i <= game.maxclients; i ++ ) + { + edict_t *ent = &(g_edicts[ i ]); + if( ent->inuse && ent->is_bot && IS_ALIVE(ent) ) + { + AntInitSearch( ent ); + BOTLIB_SetGoal( ent, node ); + ent->state = STATE_MOVE; + ent->node_timeout = 0; + } + } + } + else + return false; + + return true; +} + + +/////////////////////////////////////////////////////////////////////// +// Called when the level changes, store maps and bots (disconnected) +/////////////////////////////////////////////////////////////////////// +void ACECM_Store() +{ + return; //rekkie -- DEV_1 -- don't call this anymore, nodes will be auto generated by the new nav system + + // Stop overwriting good node tables with bad! + if( numnodes < 100 ) + return; + + //ACEND_SaveNodes(); +} + +/////////////////////////////////////////////////////////////////////// +// These routines are bot safe print routines, all id code needs to be +// changed to these so the bots do not blow up on messages sent to them. +// Do a find and replace on all code that matches the below criteria. +// +// (Got the basic idea from Ridah) +// +// change: gi.cprintf to safe_cprintf +// change: gi.centerprintf to safe_centerprintf +// +/////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////// +// Debug print, could add a "logging" feature to print to a file +/////////////////////////////////////////////////////////////////////// +void debug_printf(char *fmt, ...) +{ + int i; + char bigbuffer[0x10000]; + //int len; + va_list argptr; + edict_t *cl_ent; + + va_start (argptr,fmt); + //len = vsprintf (bigbuffer,fmt,argptr); + vsprintf (bigbuffer,fmt,argptr); + va_end (argptr); + + if (dedicated->value) + gi.cprintf(NULL, PRINT_MEDIUM, "%s", bigbuffer); + + for (i=0 ; iinuse || cl_ent->is_bot) + continue; + + gi.cprintf(cl_ent, PRINT_MEDIUM, "%s", bigbuffer); + } + +} + +void (*real_cprintf) (edict_t * ent, int printlevel, const char *fmt, ...) = NULL; +void (*real_centerprintf) (edict_t * ent, const char *fmt, ...) = NULL; + +/////////////////////////////////////////////////////////////////////// +// botsafe cprintf +/////////////////////////////////////////////////////////////////////// +void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...) +{ + char bigbuffer[0x10000]; + va_list argptr; + //int len; + + if (ent && (!ent->inuse || ent->is_bot)) + return; + + va_start (argptr,fmt); + //len = vsprintf (bigbuffer,fmt,argptr); + vsprintf (bigbuffer,fmt,argptr); + va_end (argptr); + + real_cprintf(ent, printlevel, "%s", bigbuffer); + +} + +/////////////////////////////////////////////////////////////////////// +// botsafe centerprintf +/////////////////////////////////////////////////////////////////////// +void safe_centerprintf (edict_t *ent, const char *fmt, ...) +{ + char bigbuffer[0x10000]; + va_list argptr; + //int len; + + if (!ent->inuse || ent->is_bot) + return; + + va_start (argptr,fmt); + //len = vsprintf (bigbuffer,fmt,argptr); + vsprintf (bigbuffer,fmt,argptr); + va_end (argptr); + + real_centerprintf(ent, "%s", bigbuffer); + +} diff --git a/src/action/acesrc/acebot_compress.c b/src/action/acesrc/acebot_compress.c index 76fc10509..0f5910c9d 100644 --- a/src/action/acesrc/acebot_compress.c +++ b/src/action/acesrc/acebot_compress.c @@ -1,300 +1,300 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header:$ - * - * $History:$ - * - */ -// -// acebot_compress.c - Data compress based on LZSS -// -// Not sure where I got this code, but thanks go to the -// author. I just rewote it to allow the use of buffers -// instead of files. -// -/////////////////////////////////////////////////////////// - -#include -#include -#include -#include - -#define N 4096 /* size of ring buffer */ -#define F 18 /* upper limit for match_length */ -#define THRESHOLD 2 /* encode string into position and length - if match_length is greater than this */ -#define NIL N /* index for root of binary search trees */ - -unsigned long int - textsize = 0, /* text size counter */ - codesize = 0, /* code size counter */ - printcount = 0; /* counter for reporting progress every 1K bytes */ -unsigned char - text_buf[N + F - 1]; /* ring buffer of size N, - with extra F-1 bytes to facilitate string comparison */ -int match_position, match_length, /* of longest match. These are - set by the InsertNode() procedure. */ - lson[N + 1], rson[N + 257], dad[N + 1]; /* left & right children & - parents -- These constitute binary search trees. */ - -void InitTree(void) /* initialize trees */ -{ - int i; - - /* For i = 0 to N - 1, rson[i] and lson[i] will be the right and - left children of node i. These nodes need not be initialized. - Also, dad[i] is the parent of node i. These are initialized to - NIL (= N), which stands for 'not used.' - For i = 0 to 255, rson[N + i + 1] is the root of the tree - for strings that begin with character i. These are initialized - to NIL. Note there are 256 trees. */ - - for (i = N + 1; i <= N + 256; i++) rson[i] = NIL; - for (i = 0; i < N; i++) dad[i] = NIL; -} - -void InsertNode(int r) - /* Inserts string of length F, text_buf[r..r+F-1], into one of the - trees (text_buf[r]'th tree) and returns the longest-match position - and length via the global variables match_position and match_length. - If match_length = F, then removes the old node in favor of the new - one, because the old one will be deleted sooner. - Note r plays double role, as tree node and position in buffer. */ -{ - int i, p, cmp; - unsigned char *key; - - cmp = 1; key = &text_buf[r]; p = N + 1 + key[0]; - rson[r] = lson[r] = NIL; match_length = 0; - for ( ; ; ) { - if (cmp >= 0) { - if (rson[p] != NIL) p = rson[p]; - else { rson[p] = r; dad[r] = p; return; } - } else { - if (lson[p] != NIL) p = lson[p]; - else { lson[p] = r; dad[r] = p; return; } - } - for (i = 1; i < F; i++) - if ((cmp = key[i] - text_buf[p + i]) != 0) break; - if (i > match_length) { - match_position = p; - if ((match_length = i) >= F) break; - } - } - dad[r] = dad[p]; lson[r] = lson[p]; rson[r] = rson[p]; - dad[lson[p]] = r; dad[rson[p]] = r; - if (rson[dad[p]] == p) rson[dad[p]] = r; - else lson[dad[p]] = r; - dad[p] = NIL; /* remove p */ -} - -void DeleteNode(int p) /* deletes node p from tree */ -{ - int q; - - if (dad[p] == NIL) return; /* not in tree */ - if (rson[p] == NIL) q = lson[p]; - else if (lson[p] == NIL) q = rson[p]; - else { - q = lson[p]; - if (rson[q] != NIL) { - do { q = rson[q]; } while (rson[q] != NIL); - rson[dad[q]] = lson[q]; dad[lson[q]] = dad[q]; - lson[q] = lson[p]; dad[lson[p]] = q; - } - rson[q] = rson[p]; dad[rson[p]] = q; - } - dad[q] = dad[p]; - if (rson[dad[p]] == p) rson[dad[p]] = q; else lson[dad[p]] = q; - dad[p] = NIL; -} - -int Encode(char *filename, unsigned char *buffer, int bufsize, int version) -{ - int i, c, len, r, s, last_match_length, code_buf_ptr; - unsigned char code_buf[17], mask; - int bufptr = 0; - FILE *pOut; - //int version; - - pOut = fopen(filename, "wb"); - if(pOut == NULL) - return -1; // bail - - //version = 2; // compressed version - - // Read version info (uneeded here, but moves file ptr) - fwrite(&version,sizeof(int),1,pOut); // write version - fwrite(&bufsize,sizeof(int),1,pOut); // write out the size of the buffer - - InitTree(); /* initialize trees */ - code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and - code_buf[0] works as eight flags, "1" representing that the unit - is an unencoded letter (1 byte), "0" a position-and-length pair - (2 bytes). Thus, eight units require at most 16 bytes of code. */ - code_buf_ptr = mask = 1; - s = 0; r = N - F; - for (i = s; i < r; i++) text_buf[i] = ' '; /* Clear the buffer with - any character that will appear often. */ - for (len = 0; len < F && bufptr < bufsize; len++) - { - c = buffer[bufptr++]; - text_buf[r + len] = c; /* Read F bytes into the last F bytes of - the buffer */ - } - if ((textsize = len) == 0) return -1; /* text of size zero */ - for (i = 1; i <= F; i++) InsertNode(r - i); /* Insert the F strings, - each of which begins with one or more 'space' characters. Note - the order in which these strings are inserted. This way, - degenerate trees will be less likely to occur. */ - InsertNode(r); /* Finally, insert the whole string just read. The - global variables match_length and match_position are set. */ - do { - if (match_length > len) match_length = len; /* match_length - may be spuriously long near the end of text. */ - if (match_length <= THRESHOLD) { - match_length = 1; /* Not long enough match. Send one byte. */ - code_buf[0] |= mask; /* 'send one byte' flag */ - code_buf[code_buf_ptr++] = text_buf[r]; /* Send uncoded. */ - } else { - code_buf[code_buf_ptr++] = (unsigned char) match_position; - code_buf[code_buf_ptr++] = (unsigned char) - (((match_position >> 4) & 0xf0) - | (match_length - (THRESHOLD + 1))); /* Send position and - length pair. Note match_length > THRESHOLD. */ - } - if ((mask <<= 1) == 0) { /* Shift mask left one bit. */ - for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */ - putc(code_buf[i], pOut); /* code together */ - codesize += code_buf_ptr; - code_buf[0] = 0; code_buf_ptr = mask = 1; - } - last_match_length = match_length; - for (i = 0; i < last_match_length && - bufptr < bufsize; i++) - { - c = buffer[bufptr++]; - DeleteNode(s); /* Delete old strings and */ - text_buf[s] = c; /* read new bytes */ - if (s < F - 1) text_buf[s + N] = c; /* If the position is - near the end of buffer, extend the buffer to make - string comparison easier. */ - s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); - /* Since this is a ring buffer, increment the position - modulo N. */ - InsertNode(r); /* Register the string in text_buf[r..r+F-1] */ - } -// if ((textsize += i) > printcount) { -// printf("%12ld\r", textsize); printcount += 1024; -// /* Reports progress each time the textsize exceeds -// multiples of 1024. */ -// } - while (i++ < last_match_length) { /* After the end of text, */ - DeleteNode(s); /* no need to read, but */ - s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); - if (--len) InsertNode(r); /* buffer may not be empty. */ - } - } while (len > 0); /* until length of string to be processed is zero */ - if (code_buf_ptr > 1) { /* Send remaining code. */ - for (i = 0; i < code_buf_ptr; i++) putc(code_buf[i], pOut); - codesize += code_buf_ptr; - } -// printf("In : %ld bytes\n", textsize); /* Encoding is done. */ -// printf("Out: %ld bytes\n", codesize); -// printf("Out/In: %.3f\n", (double)codesize / textsize); - fclose(pOut); - - return codesize; -} - -// Be careful with your buffersize, will return an exit of -1 if failure -int Decode(char *filename, unsigned char *buffer, int bufsize) /* Just the reverse of Encode(). */ -{ - int i, j, k, r, c; - unsigned int flags; - int bufptr=0; - FILE *pIn; - int version; - - pIn = fopen(filename, "rb"); - if(pIn == NULL) - return -1; // bail - - // Read version info (uneeded here, but moves file ptr) - fread(&version,sizeof(int),1,pIn); // read version - fread(&version,sizeof(int),1,pIn); // read buffersize (not needed, so repeat into version) - - - for (i = 0; i < N - F; i++) text_buf[i] = ' '; - r = N - F; flags = 0; - for ( ; ; ) { - if (((flags >>= 1) & 256) == 0) { - if ((c = getc(pIn)) == EOF) break; - flags = c | 0xff00; /* uses higher byte cleverly */ - } /* to count eight */ - if (flags & 1) { - if ((c = getc(pIn)) == EOF) break; - buffer[bufptr++] = c; - if(bufptr > bufsize) - return -1; // check for overflow - text_buf[r++] = c; - r &= (N - 1); - } else { - if ((i = getc(pIn)) == EOF) break; - if ((j = getc(pIn)) == EOF) break; - i |= ((j & 0xf0) << 4); j = (j & 0x0f) + THRESHOLD; - for (k = 0; k <= j; k++) { - c = text_buf[(i + k) & (N - 1)]; - buffer[bufptr++] = c; - if(bufptr > bufsize) - return -1; // check for overflow - text_buf[r++] = c; - r &= (N - 1); - } - } - } - - fclose(pIn); - return bufptr; // return uncompressed size -} -/* -// tester -#include -int main(int argc, char *argv[]) -{ - //char *s; - unsigned char i; - int csize,ucsize; - unsigned char *buffer1; - int bufsize = 20000; - - buffer1 = (unsigned char *)malloc(bufsize); - - buffer1[500] = 'c'; - buffer1[785] = 's'; - - csize = Encode("testbuf.bin", buffer1, bufsize);//sizeof(buffer1)); - - buffer1[500] = 0; - buffer1[785] = 0; - - ucsize = Decode("testbuf.bin", buffer1, bufsize); - - printf("Output: %c %c\n",buffer1[500],buffer1[785]); - printf("Compressed: %d Uncompressed:%d\n",csize,ucsize); - - free(buffer1); - - return EXIT_SUCCESS; -} -*/ +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header:$ + * + * $History:$ + * + */ +// +// acebot_compress.c - Data compress based on LZSS +// +// Not sure where I got this code, but thanks go to the +// author. I just rewote it to allow the use of buffers +// instead of files. +// +/////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#define N 4096 /* size of ring buffer */ +#define F 18 /* upper limit for match_length */ +#define THRESHOLD 2 /* encode string into position and length + if match_length is greater than this */ +#define NIL N /* index for root of binary search trees */ + +unsigned long int + textsize = 0, /* text size counter */ + codesize = 0, /* code size counter */ + printcount = 0; /* counter for reporting progress every 1K bytes */ +unsigned char + text_buf[N + F - 1]; /* ring buffer of size N, + with extra F-1 bytes to facilitate string comparison */ +int match_position, match_length, /* of longest match. These are + set by the InsertNode() procedure. */ + lson[N + 1], rson[N + 257], dad[N + 1]; /* left & right children & + parents -- These constitute binary search trees. */ + +void InitTree(void) /* initialize trees */ +{ + int i; + + /* For i = 0 to N - 1, rson[i] and lson[i] will be the right and + left children of node i. These nodes need not be initialized. + Also, dad[i] is the parent of node i. These are initialized to + NIL (= N), which stands for 'not used.' + For i = 0 to 255, rson[N + i + 1] is the root of the tree + for strings that begin with character i. These are initialized + to NIL. Note there are 256 trees. */ + + for (i = N + 1; i <= N + 256; i++) rson[i] = NIL; + for (i = 0; i < N; i++) dad[i] = NIL; +} + +void InsertNode(int r) + /* Inserts string of length F, text_buf[r..r+F-1], into one of the + trees (text_buf[r]'th tree) and returns the longest-match position + and length via the global variables match_position and match_length. + If match_length = F, then removes the old node in favor of the new + one, because the old one will be deleted sooner. + Note r plays double role, as tree node and position in buffer. */ +{ + int i, p, cmp; + unsigned char *key; + + cmp = 1; key = &text_buf[r]; p = N + 1 + key[0]; + rson[r] = lson[r] = NIL; match_length = 0; + for ( ; ; ) { + if (cmp >= 0) { + if (rson[p] != NIL) p = rson[p]; + else { rson[p] = r; dad[r] = p; return; } + } else { + if (lson[p] != NIL) p = lson[p]; + else { lson[p] = r; dad[r] = p; return; } + } + for (i = 1; i < F; i++) + if ((cmp = key[i] - text_buf[p + i]) != 0) break; + if (i > match_length) { + match_position = p; + if ((match_length = i) >= F) break; + } + } + dad[r] = dad[p]; lson[r] = lson[p]; rson[r] = rson[p]; + dad[lson[p]] = r; dad[rson[p]] = r; + if (rson[dad[p]] == p) rson[dad[p]] = r; + else lson[dad[p]] = r; + dad[p] = NIL; /* remove p */ +} + +void DeleteNode(int p) /* deletes node p from tree */ +{ + int q; + + if (dad[p] == NIL) return; /* not in tree */ + if (rson[p] == NIL) q = lson[p]; + else if (lson[p] == NIL) q = rson[p]; + else { + q = lson[p]; + if (rson[q] != NIL) { + do { q = rson[q]; } while (rson[q] != NIL); + rson[dad[q]] = lson[q]; dad[lson[q]] = dad[q]; + lson[q] = lson[p]; dad[lson[p]] = q; + } + rson[q] = rson[p]; dad[rson[p]] = q; + } + dad[q] = dad[p]; + if (rson[dad[p]] == p) rson[dad[p]] = q; else lson[dad[p]] = q; + dad[p] = NIL; +} + +int Encode(char *filename, unsigned char *buffer, int bufsize, int version) +{ + int i, c, len, r, s, last_match_length, code_buf_ptr; + unsigned char code_buf[17], mask; + int bufptr = 0; + FILE *pOut; + //int version; + + pOut = fopen(filename, "wb"); + if(pOut == NULL) + return -1; // bail + + //version = 2; // compressed version + + // Read version info (uneeded here, but moves file ptr) + fwrite(&version,sizeof(int),1,pOut); // write version + fwrite(&bufsize,sizeof(int),1,pOut); // write out the size of the buffer + + InitTree(); /* initialize trees */ + code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and + code_buf[0] works as eight flags, "1" representing that the unit + is an unencoded letter (1 byte), "0" a position-and-length pair + (2 bytes). Thus, eight units require at most 16 bytes of code. */ + code_buf_ptr = mask = 1; + s = 0; r = N - F; + for (i = s; i < r; i++) text_buf[i] = ' '; /* Clear the buffer with + any character that will appear often. */ + for (len = 0; len < F && bufptr < bufsize; len++) + { + c = buffer[bufptr++]; + text_buf[r + len] = c; /* Read F bytes into the last F bytes of + the buffer */ + } + if ((textsize = len) == 0) return -1; /* text of size zero */ + for (i = 1; i <= F; i++) InsertNode(r - i); /* Insert the F strings, + each of which begins with one or more 'space' characters. Note + the order in which these strings are inserted. This way, + degenerate trees will be less likely to occur. */ + InsertNode(r); /* Finally, insert the whole string just read. The + global variables match_length and match_position are set. */ + do { + if (match_length > len) match_length = len; /* match_length + may be spuriously long near the end of text. */ + if (match_length <= THRESHOLD) { + match_length = 1; /* Not long enough match. Send one byte. */ + code_buf[0] |= mask; /* 'send one byte' flag */ + code_buf[code_buf_ptr++] = text_buf[r]; /* Send uncoded. */ + } else { + code_buf[code_buf_ptr++] = (unsigned char) match_position; + code_buf[code_buf_ptr++] = (unsigned char) + (((match_position >> 4) & 0xf0) + | (match_length - (THRESHOLD + 1))); /* Send position and + length pair. Note match_length > THRESHOLD. */ + } + if ((mask <<= 1) == 0) { /* Shift mask left one bit. */ + for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */ + putc(code_buf[i], pOut); /* code together */ + codesize += code_buf_ptr; + code_buf[0] = 0; code_buf_ptr = mask = 1; + } + last_match_length = match_length; + for (i = 0; i < last_match_length && + bufptr < bufsize; i++) + { + c = buffer[bufptr++]; + DeleteNode(s); /* Delete old strings and */ + text_buf[s] = c; /* read new bytes */ + if (s < F - 1) text_buf[s + N] = c; /* If the position is + near the end of buffer, extend the buffer to make + string comparison easier. */ + s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); + /* Since this is a ring buffer, increment the position + modulo N. */ + InsertNode(r); /* Register the string in text_buf[r..r+F-1] */ + } +// if ((textsize += i) > printcount) { +// printf("%12ld\r", textsize); printcount += 1024; +// /* Reports progress each time the textsize exceeds +// multiples of 1024. */ +// } + while (i++ < last_match_length) { /* After the end of text, */ + DeleteNode(s); /* no need to read, but */ + s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); + if (--len) InsertNode(r); /* buffer may not be empty. */ + } + } while (len > 0); /* until length of string to be processed is zero */ + if (code_buf_ptr > 1) { /* Send remaining code. */ + for (i = 0; i < code_buf_ptr; i++) putc(code_buf[i], pOut); + codesize += code_buf_ptr; + } +// printf("In : %ld bytes\n", textsize); /* Encoding is done. */ +// printf("Out: %ld bytes\n", codesize); +// printf("Out/In: %.3f\n", (double)codesize / textsize); + fclose(pOut); + + return codesize; +} + +// Be careful with your buffersize, will return an exit of -1 if failure +int Decode(char *filename, unsigned char *buffer, int bufsize) /* Just the reverse of Encode(). */ +{ + int i, j, k, r, c; + unsigned int flags; + int bufptr=0; + FILE *pIn; + int version; + + pIn = fopen(filename, "rb"); + if(pIn == NULL) + return -1; // bail + + // Read version info (uneeded here, but moves file ptr) + fread(&version,sizeof(int),1,pIn); // read version + fread(&version,sizeof(int),1,pIn); // read buffersize (not needed, so repeat into version) + + + for (i = 0; i < N - F; i++) text_buf[i] = ' '; + r = N - F; flags = 0; + for ( ; ; ) { + if (((flags >>= 1) & 256) == 0) { + if ((c = getc(pIn)) == EOF) break; + flags = c | 0xff00; /* uses higher byte cleverly */ + } /* to count eight */ + if (flags & 1) { + if ((c = getc(pIn)) == EOF) break; + buffer[bufptr++] = c; + if(bufptr > bufsize) + return -1; // check for overflow + text_buf[r++] = c; + r &= (N - 1); + } else { + if ((i = getc(pIn)) == EOF) break; + if ((j = getc(pIn)) == EOF) break; + i |= ((j & 0xf0) << 4); j = (j & 0x0f) + THRESHOLD; + for (k = 0; k <= j; k++) { + c = text_buf[(i + k) & (N - 1)]; + buffer[bufptr++] = c; + if(bufptr > bufsize) + return -1; // check for overflow + text_buf[r++] = c; + r &= (N - 1); + } + } + } + + fclose(pIn); + return bufptr; // return uncompressed size +} +/* +// tester +#include +int main(int argc, char *argv[]) +{ + //char *s; + unsigned char i; + int csize,ucsize; + unsigned char *buffer1; + int bufsize = 20000; + + buffer1 = (unsigned char *)malloc(bufsize); + + buffer1[500] = 'c'; + buffer1[785] = 's'; + + csize = Encode("testbuf.bin", buffer1, bufsize);//sizeof(buffer1)); + + buffer1[500] = 0; + buffer1[785] = 0; + + ucsize = Decode("testbuf.bin", buffer1, bufsize); + + printf("Output: %c %c\n",buffer1[500],buffer1[785]); + printf("Compressed: %d Uncompressed:%d\n",csize,ucsize); + + free(buffer1); + + return EXIT_SUCCESS; +} +*/ diff --git a/src/action/acesrc/acebot_items.c b/src/action/acesrc/acebot_items.c index a633ffdd1..4c2bc4d13 100644 --- a/src/action/acesrc/acebot_items.c +++ b/src/action/acesrc/acebot_items.c @@ -1,1107 +1,1104 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot_items.c 6 29/02/00 11:11 Riever $ - * - * $History: acebot_items.c $ - * - * ***************** Version 6 ***************** - * User: Riever Date: 29/02/00 Time: 11:11 - * Updated in $/LTK2/src/acesrc - * Added ability to collect more knives. - * - * ***************** Version 5 ***************** - * User: Riever Date: 24/02/00 Time: 19:59 - * Updated in $/LTK2/src/acesrc - * Fixed item handling (Broken by new node-height code. - * - * ***************** Version 4 ***************** - * User: Riever Date: 23/02/00 Time: 23:17 - * Updated in $/LTK2/src/acesrc - * New door node creation implemented and doors removed from item table. - * - * ***************** Version 3 ***************** - * User: Riever Date: 23/02/00 Time: 17:24 - * Updated in $/LTK2/src/acesrc - * Added support for 'sv shownodes on/off' - * Enabled creation of nodes for ALL doors. (Stage 1 of new method) - * - * ***************** Version 2 ***************** - * User: Riever Date: 17/02/00 Time: 17:53 - * Updated in $/LTK2/src/acesrc - * Fixed item list to be in the right order! - * - */ - -/////////////////////////////////////////////////////////////////////// -// -// acebot_items.c - This file contains all of the -// item handling routines for the -// ACE bot, including fact table support -// -/////////////////////////////////////////////////////////////////////// - -#include "../g_local.h" - -#include "acebot.h" - -int num_players = 0; -int num_items = 0; -item_table_t item_table[MAX_EDICTS]; -edict_t *players[MAX_CLIENTS]; // pointers to all players in the game - -/////////////////////////////////////////////////////////////////////// -// Add/Remove player from our list -/////////////////////////////////////////////////////////////////////// -void ACEIT_RebuildPlayerList( void ) -{ - size_t i; - int bcount = 0; - - num_players = 0; - - for( i = 1; i <= game.maxclients; i ++ ) - { - edict_t *ent = &g_edicts[i]; - if( ent->client && ent->client->pers.connected ) - { - players[ num_players ] = ent; - num_players ++; - } - - if(ent->is_bot){ - bcount++; - } - } - game.bot_count = bcount; -} - -/////////////////////////////////////////////////////////////////////// -// Can we get there? -/////////////////////////////////////////////////////////////////////// -qboolean ACEIT_IsReachable(edict_t *self, vec3_t goal) -{ - trace_t trace; - vec3_t v; - - VectorCopy(self->mins,v); - v[2] += 18; // Stepsize - -//AQ2 trace = gi.trace (self->s.origin, v, self->maxs, goal, self, MASK_OPAQUE); - trace = gi.trace (self->s.origin, v, self->maxs, goal, self, MASK_SOLID|MASK_OPAQUE); - - // Yes we can see it - if (trace.fraction == 1.0) - return true; - else - return false; - -} - -/////////////////////////////////////////////////////////////////////// -// Visiblilty check -/////////////////////////////////////////////////////////////////////// -qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal) -{ - trace_t trace; - -//AQ2 trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, goal, self, MASK_OPAQUE); - trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, goal, self, MASK_SOLID|MASK_OPAQUE); - - // Yes we can see it - if (trace.fraction == 1.0) - return true; - else - return false; - -} - -/////////////////////////////////////////////////////////////////////// -// Weapon changing support -/////////////////////////////////////////////////////////////////////// -//AQ2 CHANGE -// Completely rewritten for AQ2! -// -qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - return false; - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (!ent->client->inventory[ammo_index] )//&& !g_select_empty->value) - clips = false; - else - clips = true; - } - - // see if we're already using it - if (item == ent->client->weapon) - { - //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - } - // No ammo - forget it! - if(!loaded && !clips) - return false; - // If it's not loaded - use a new clip - else if( !loaded ) - Cmd_New_Reload_f ( ent ); - return true; - } - - // Change to this weapon - ent->client->newweapon = item; - ChangeWeapon( ent ); - - return true; -} - -//=============================== -// Handling for MK23 (debugging) -//=============================== -qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got MP23\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - // see if we're already using it - if (item == ent->client->weapon) - { - //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - } - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got MK23 Ammo\n"); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload MK23\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // Not current weapon, check ammo - if( (ent->client->mk23_rds < 1) && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No MK23 Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to MK23\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} - -//=============================== -// Handling for HC (debugging) -//=============================== -qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got HC\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 2) - clips = false; - else - clips = true; - } - // Check ammo - if( (ent->client->cannon_rds < 2) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No HC Ammo\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got HC Ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload HC\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // Not current weapon, check ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No HC Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to HandCannon\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} - -//=============================== -// Handling for Sniper Rifle (debugging) -//=============================== -qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got Sniper Rifle\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - // Check ammo - if( (ent->client->sniper_rds < 1) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Sniper Ammo\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got Sniper Ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload Sniper Rifle\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // No ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Sniper Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to Sniper Rifle\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} -//=============================== -// Handling for M3 (debugging) -//=============================== -qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got M3\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - // Check ammo - if( (ent->client->shot_rds < 1) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No M3 Ammo\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got M3 Ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload M3\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // No ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No M3 Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to M3\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} -//=============================== -// Handling for M4 (debugging) -//=============================== -qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got MP4\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1)//&& !g_select_empty->value) - clips = false; - else - clips = true; - } - // Check ammo - if( (ent->client->m4_rds < 1) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No M4 Ammo\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got M4 Ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload M4\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // No ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No M4 Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to M4\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} -//=============================== -// Handling for MP5 (debugging) -//=============================== -qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got MP5\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); -// ammo_item = FindItem(MP5_AMMO_NAME); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1) - { -// gi.bprintf(PRINT_HIGH,"Out of MP5 Mags\n"); - clips = false; - } - else - clips = true; - } - // check ammo - if( (ent->client->mp5_rds < 1) ) - { -// gi.bprintf(PRINT_HIGH,"No MP5 Ammo\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Stopping MP5 use - no ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded && clips) - { -// gi.bprintf(PRINT_HIGH,"Need to reload MP5\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // Not current weapon, check ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No MP5 Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to MP5\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} - -//=============================== -// Handling for DUAL PISTOLS (debugging) -//=============================== -qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got Dual Pistols\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 2)//&& !g_select_empty->value) - clips = false; - else - clips = true; - } - // Not current weapon, check ammo - if( (ent->client->dual_rds < 2) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Dual Ammo\n"); - loaded = false; - } - - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got Dual Ammo\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip - else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload Dual\n"); - Cmd_New_Reload_f ( ent ); - } - return true; - } - - // Not current weapon, check ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Dual Ammo\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to Dual\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} -//AQ2 END - -//=============================== -// Handling for Grenades (debugging) -//=============================== -qboolean ACEIT_ChangeGrenSpecialWeapon (edict_t *ent, gitem_t *item) -{ - int ammo_index; - gitem_t *ammo_item; - qboolean loaded = true; - qboolean clips = true; - - // Has not picked up weapon yet - if(!ent->client->inventory[ITEM_INDEX(item)]) - { -// gi.bprintf(PRINT_HIGH,"Not got Grenades\n"); - return false; - } - - // Do we have ammo for it? - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (ent->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - // Check ammo - if( (ent->client->shot_rds < 1) ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Grenades\n"); - loaded = false; - } - // see if we're already using it - if (item == ent->client->weapon) - { -/* //Do we have one in the chamber? - if( ent->client->weaponstate == WEAPON_END_MAG) - { - loaded = false; - }*/ - // No ammo - forget it! - if(!loaded && !clips) - { -// gi.bprintf(PRINT_HIGH,"Not got Grenades\n"); - DropSpecialWeapon ( ent ); - return false; - } - // If it's not loaded - use a new clip -/* else if( !loaded ) - { -// gi.bprintf(PRINT_HIGH,"Need to reload \n"); - Cmd_New_Reload_f ( ent ); - } -*/ return true; - } - - // No ammo - if( !loaded && !clips ) - { -// gi.bprintf(PRINT_HIGH,"No change - No Grenades\n"); - return false; - } - - // Change to this weapon -// gi.bprintf(PRINT_HIGH,"Changing to Grenades\n"); - ent->client->newweapon = item; - ChangeWeapon ( ent ); - return true; -} - - - - - -extern gitem_armor_t jacketarmor_info; -extern gitem_armor_t combatarmor_info; -extern gitem_armor_t bodyarmor_info; - -/////////////////////////////////////////////////////////////////////// -// Check if we can use the armor -/////////////////////////////////////////////////////////////////////// -qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other) -{ -/* - int old_armor_index; - gitem_armor_t *oldinfo; - gitem_armor_t *newinfo; - int newcount; - float salvage; - int salvagecount; - - // get info on new armor - newinfo = (gitem_armor_t *)item->info; - - old_armor_index = ArmorIndex (other); - - // handle armor shards specially - if (item->tag == ARMOR_SHARD) - return true; - - // get info on old armor - if (old_armor_index == ITEM_INDEX(FindItem("Jacket Armor"))) - oldinfo = &jacketarmor_info; - else if (old_armor_index == ITEM_INDEX(FindItem("Combat Armor"))) - oldinfo = &combatarmor_info; - else // (old_armor_index == body_armor_index) - oldinfo = &bodyarmor_info; - - if (newinfo->normal_protection <= oldinfo->normal_protection) - { - // calc new armor values - salvage = newinfo->normal_protection / oldinfo->normal_protection; - salvagecount = salvage * newinfo->base_count; - newcount = other->client->inventory[old_armor_index] + salvagecount; - - if (newcount > oldinfo->max_count) - newcount = oldinfo->max_count; - - // if we're already maxed out then we don't need the new armor - if (other->client->inventory[old_armor_index] >= newcount) - return false; - - } - - return true; -*/ - return false; -} - - -/////////////////////////////////////////////////////////////////////// -// Determines the NEED for an item -// -// This function can be modified to support new items to pick up -// Any other logic that needs to be added for custom decision making -// can be added here. For now it is very simple. -/////////////////////////////////////////////////////////////////////// -float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ) -{ - gitem_t *item = NULL; - int band = 0; - - if( ! item_ent ) - return 0; - - // Make sure domination flags are always considered important short-range goals. - if( dom->value && (Q_stricmp( item_ent->classname, "item_flag" ) == 0) ) - return (DomFlagOwner(item_ent) != self->client->resp.team) ? 9000 : 0; - - item = FindItemByClassname( item_ent->classname ); - if( ! item ) - return 0; - - band = INV_AMMO(self,BAND_NUM) ? 1 : 0; - - switch( item->typeNum ) - { - case FLAG_T1_NUM: - case FLAG_T2_NUM: - // If we're carrying it already, ignore it. - if( INV_AMMO(self,item->typeNum) ) - return 0.0; - // If we have the other flag, we really want to capture it! - else if( INV_AMMO(self,FLAG_T1_NUM) || INV_AMMO(self,FLAG_T2_NUM) ) - return 1000.0; - // Dropped flags are a very high priority. - else if( item_ent->spawnflags & DROPPED_ITEM ) - return 100.0; - // Go for the enemy flag at a fairly high priority. - else if( self->client->resp.team != item->typeNum + 1 - FLAG_T1_NUM ) - return 2.0; - else - return 0.0; - - case MP5_NUM: - case M4_NUM: - case M3_NUM: - case HC_NUM: - case SNIPER_NUM: - case DUAL_NUM: - if( (self->client->unique_weapon_total < unique_weapons->value + band) && ! INV_AMMO(self,item->typeNum) ) - return 0.7; - else - return 0.0; - - case SIL_NUM: - case SLIP_NUM: - case BAND_NUM: - case KEV_NUM: - case LASER_NUM: - case HELM_NUM: - if( (self->client->unique_item_total < unique_items->value) && ! INV_AMMO(self,item->typeNum) ) - return 0.6; - else - return 0.0; - - case MK23_ANUM: - if( INV_AMMO(self,item->typeNum) < self->client->max_pistolmags ) - return 0.3; - else - return 0.0; - - case M4_ANUM: - if( INV_AMMO(self,item->typeNum) < self->client->max_m4mags ) - return 0.3; - else - return 0.0; - - case MP5_ANUM: - if( INV_AMMO(self,item->typeNum) < self->client->max_mp5mags ) - return 0.3; - else - return 0.0; - - case SNIPER_ANUM: - if( INV_AMMO(self,item->typeNum) < self->client->max_sniper_rnds ) - return 0.3; - else - return 0.0; - - case SHELL_ANUM: - if( INV_AMMO(self,item->typeNum) < self->client->max_shells ) - return 0.3; - else - return 0.0; - - case KNIFE_NUM: - if( INV_AMMO(self,item->typeNum) < self->client->knife_max ) - return 0.3; - else - return 0.0; - - default: - return 0.0; - } -} - - -/////////////////////////////////////////////////////////////////////// -// Convert a classname to its index value -// -// I prefer to use integers/defines for simplicity sake. This routine -// can lead to some slowdowns I guess, but makes the rest of the code -// easier to deal with. -/////////////////////////////////////////////////////////////////////// -int ACEIT_ClassnameToIndex( char *classname ) -{ - gitem_t *item = FindItemByClassname( classname ); - if( item ) - return items[item->typeNum].index; - return INVALID; -} - - -/////////////////////////////////////////////////////////////////////// -// Only called once per level, when saved will not be called again -// -// Downside of the routine is that items can not move about. If the level -// has been saved before and reloaded, it could cause a problem if there -// are items that spawn at random locations. -// -#define DEBUG // uncomment to write out items to a file. -/////////////////////////////////////////////////////////////////////// -void ACEIT_BuildItemNodeTable (qboolean rebuild) -{ - edict_t *items; - int i,item_index; - vec3_t v,v1,v2; - -#ifdef DEBUG - FILE *pOut; // for testing - if((pOut = fopen("items.txt","wt"))==NULL) - return; -#endif - - num_items = 0; - - // Add game items - for(items = g_edicts; items < &g_edicts[globals.num_edicts]; items++) - { - // filter out crap - if(items->solid == SOLID_NOT) - continue; - - if(!items->classname) - continue; - - ///////////////////////////////////////////////////////////////// - // Items - ///////////////////////////////////////////////////////////////// - item_index = ACEIT_ClassnameToIndex(items->classname); - - //////////////////////////////////////////////////////////////// - // SPECIAL NAV NODE DROPPING CODE - //////////////////////////////////////////////////////////////// - // Special node dropping for platforms - if(strcmp(items->classname,"func_plat")==0) - { - if(!rebuild) - ACEND_AddNode(items,NODE_PLATFORM); - item_index = 99; // to allow to pass the item index test - } - - // Special node dropping for teleporters - if(strcmp(items->classname,"misc_teleporter_dest")==0 || strcmp(items->classname,"misc_teleporter")==0) - { - if(!rebuild) - ACEND_AddNode(items,NODE_TELEPORTER); - item_index = 99; - } - - // Special node dropping for doors - RiEvEr - if( - (strcmp(items->classname,"func_door_rotating")==0) - || (strcmp(items->classname,"func_door")==0 ) - ) - { -// item_index = 99; - if(!rebuild) - { - // add a pointer to the item entity -// item_table[num_items].ent = items; -// item_table[num_items].item = item_index; - // create the node -// item_table[num_items].node = ACEND_AddNode(items,NODE_DOOR); - ACEND_AddNode(items,NODE_DOOR); -//#ifdef DEBUG -// fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname, -// item_table[num_items].node, -// items->s.origin[0],items->s.origin[1],items->s.origin[2]); -//#endif -// num_items++; -// continue; - } - continue; - } - - #ifdef DEBUG - if(item_index == INVALID) - fprintf(pOut,"Rejected item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); -// else -// fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); - #endif - - if(item_index == INVALID) - continue; - - // add a pointer to the item entity - item_table[num_items].ent = items; - item_table[num_items].item = item_index; - - // If new, add nodes for items - if(!rebuild) - { - item_table[num_items].node = ACEND_AddNode(items,NODE_ITEM); -#ifdef DEBUG - fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); -#endif - num_items++; - } - else // Now if rebuilding, just relink ent structures - { - // Find stored location - for(i=0;is.origin,v); - v[2] +=8; // Raised all nodes to get better links. - - // Add 16 to item type nodes - if(nodes[i].type == NODE_ITEM) - v[2] += 16; - - // Add 32 to teleporter - if(nodes[i].type == NODE_TELEPORTER) - v[2] += 32; - -/* // Door handling - if( nodes[i].type == NODE_DOOR) - { - vec3_t position; - // Find mid point of door max and min and put the node there - VectorClear(position); -// VectorCopy(items->s.origin, position); - // find center of door - position[0] = items->absmin[0] + ((items->maxs[0] - items->mins[0]) /2); - position[1] = items->absmin[1] + ((items->maxs[1] - items->mins[1]) /2); -// position[2] -= 16; // lower it a little - position[2] = items->absmin[2] + 32; - // Set location - VectorCopy(position, v); - gi.bprintf(PRINT_HIGH, "%d ",items->s.angles[0]); - }*/ - - if(nodes[i].type == NODE_PLATFORM) - { - VectorCopy(items->maxs,v1); - VectorCopy(items->mins,v2); - - // To get the center - v[0] = (v1[0] - v2[0]) / 2 + v2[0]; - v[1] = (v1[1] - v2[1]) / 2 + v2[1]; - v[2] = items->mins[2]+64; - } - - if(v[0] == nodes[i].origin[0] && - v[1] == nodes[i].origin[1] && - v[2] == nodes[i].origin[2]) - { - // found a match now link to facts - item_table[num_items].node = i; -#ifdef DEBUG - fprintf(pOut,"Relink item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); -#endif - num_items++; - - } -/* else - { -#ifdef DEBUG - fprintf(pOut,"Failed item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); -#endif - }*/ - } - } - } - - - } - -#ifdef DEBUG - fclose(pOut); -#endif - -} +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot_items.c 6 29/02/00 11:11 Riever $ + * + * $History: acebot_items.c $ + * + * ***************** Version 6 ***************** + * User: Riever Date: 29/02/00 Time: 11:11 + * Updated in $/LTK2/src/acesrc + * Added ability to collect more knives. + * + * ***************** Version 5 ***************** + * User: Riever Date: 24/02/00 Time: 19:59 + * Updated in $/LTK2/src/acesrc + * Fixed item handling (Broken by new node-height code. + * + * ***************** Version 4 ***************** + * User: Riever Date: 23/02/00 Time: 23:17 + * Updated in $/LTK2/src/acesrc + * New door node creation implemented and doors removed from item table. + * + * ***************** Version 3 ***************** + * User: Riever Date: 23/02/00 Time: 17:24 + * Updated in $/LTK2/src/acesrc + * Added support for 'sv shownodes on/off' + * Enabled creation of nodes for ALL doors. (Stage 1 of new method) + * + * ***************** Version 2 ***************** + * User: Riever Date: 17/02/00 Time: 17:53 + * Updated in $/LTK2/src/acesrc + * Fixed item list to be in the right order! + * + */ + +/////////////////////////////////////////////////////////////////////// +// +// acebot_items.c - This file contains all of the +// item handling routines for the +// ACE bot, including fact table support +// +/////////////////////////////////////////////////////////////////////// + +#include "../g_local.h" + +#include "acebot.h" + +int num_players = 0; +int num_items = 0; +item_table_t item_table[MAX_EDICTS]; +edict_t *players[MAX_CLIENTS]; // pointers to all players in the game + +/////////////////////////////////////////////////////////////////////// +// Add/Remove player from our list +/////////////////////////////////////////////////////////////////////// +void ACEIT_RebuildPlayerList( void ) +{ + size_t i; + + num_players = 0; + + for( i = 1; i <= game.maxclients; i ++ ) + { + edict_t *ent = &g_edicts[i]; + if( ent->client && ent->client->pers.connected ) + { + players[ num_players ] = ent; + num_players ++; + } + } +} + +/////////////////////////////////////////////////////////////////////// +// Can we get there? +/////////////////////////////////////////////////////////////////////// +qboolean ACEIT_IsReachable(edict_t *self, vec3_t goal) +{ + trace_t trace; + vec3_t v; + + VectorCopy(self->mins,v); + v[2] += 18; // Stepsize + +//AQ2 trace = gi.trace (self->s.origin, v, self->maxs, goal, self, MASK_OPAQUE); + trace = gi.trace (self->s.origin, v, self->maxs, goal, self, MASK_SOLID|MASK_OPAQUE); + + // Yes we can see it + if (trace.fraction == 1.0) + return true; + else + return false; + +} + +/////////////////////////////////////////////////////////////////////// +// Visiblilty check +/////////////////////////////////////////////////////////////////////// +qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal) +{ + trace_t trace; + +//AQ2 trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, goal, self, MASK_OPAQUE); + trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, goal, self, MASK_SOLID|MASK_OPAQUE); + + // Yes we can see it + if (trace.fraction == 1.0) + return true; + else + return false; + +} + +/////////////////////////////////////////////////////////////////////// +// Weapon changing support +/////////////////////////////////////////////////////////////////////// +//AQ2 CHANGE +// Completely rewritten for AQ2! +// +qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + return false; + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (!ent->client->inventory[ammo_index] )//&& !g_select_empty->value) + clips = false; + else + clips = true; + } + + // see if we're already using it + if (item == ent->client->weapon) + { + //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + } + // No ammo - forget it! + if(!loaded && !clips) + return false; + // If it's not loaded - use a new clip + else if( !loaded ) + Cmd_New_Reload_f ( ent ); + return true; + } + + // Change to this weapon + ent->client->newweapon = item; + ChangeWeapon( ent ); + + return true; +} + +//=============================== +// Handling for MK23 (debugging) +//=============================== +qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got MP23\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + // see if we're already using it + if (item == ent->client->weapon) + { + //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + } + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got MK23 Ammo\n"); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload MK23\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // Not current weapon, check ammo + if( (ent->client->mk23_rds < 1) && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No MK23 Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to MK23\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} + +//=============================== +// Handling for HC (debugging) +//=============================== +qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got HC\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 2) + clips = false; + else + clips = true; + } + // Check ammo + if( (ent->client->cannon_rds < 2) ) + { +// gi.bprintf(PRINT_HIGH,"No change - No HC Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got HC Ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload HC\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // Not current weapon, check ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No HC Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to HandCannon\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} + +//=============================== +// Handling for Sniper Rifle (debugging) +//=============================== +qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got Sniper Rifle\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + // Check ammo + if(ent->client->sniper_rds < 1) + { +// gi.bprintf(PRINT_HIGH,"No change - No Sniper Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got Sniper Ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload Sniper Rifle\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // No ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No Sniper Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to Sniper Rifle\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} +//=============================== +// Handling for M3 (debugging) +//=============================== +qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got M3\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + // Check ammo + if( (ent->client->shot_rds < 1) ) + { +// gi.bprintf(PRINT_HIGH,"No change - No M3 Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got M3 Ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload M3\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // No ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No M3 Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to M3\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} +//=============================== +// Handling for M4 (debugging) +//=============================== +qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got MP4\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1)//&& !g_select_empty->value) + clips = false; + else + clips = true; + } + // Check ammo + if( (ent->client->m4_rds < 1) ) + { +// gi.bprintf(PRINT_HIGH,"No change - No M4 Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got M4 Ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload M4\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // No ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No M4 Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to M4\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} +//=============================== +// Handling for MP5 (debugging) +//=============================== +qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got MP5\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); +// ammo_item = FindItem(MP5_AMMO_NAME); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1) + { +// gi.bprintf(PRINT_HIGH,"Out of MP5 Mags\n"); + clips = false; + } + else + clips = true; + } + // check ammo + if( (ent->client->mp5_rds < 1) ) + { +// gi.bprintf(PRINT_HIGH,"No MP5 Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Stopping MP5 use - no ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded && clips) + { +// gi.bprintf(PRINT_HIGH,"Need to reload MP5\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // Not current weapon, check ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No MP5 Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to MP5\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} + +//=============================== +// Handling for DUAL PISTOLS (debugging) +//=============================== +qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got Dual Pistols\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 2)//&& !g_select_empty->value) + clips = false; + else + clips = true; + } + // Not current weapon, check ammo + if( (ent->client->dual_rds < 2) ) + { +// gi.bprintf(PRINT_HIGH,"No change - No Dual Ammo\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got Dual Ammo\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip + else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload Dual\n"); + Cmd_New_Reload_f ( ent ); + } + return true; + } + + // Not current weapon, check ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No Dual Ammo\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to Dual\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} +//AQ2 END + +//=============================== +// Handling for Grenades (debugging) +//=============================== +qboolean ACEIT_ChangeGrenSpecialWeapon (edict_t *ent, gitem_t *item) +{ + int ammo_index; + gitem_t *ammo_item; + qboolean loaded = true; + qboolean clips = true; + + // Has not picked up weapon yet + if(!ent->client->inventory[ITEM_INDEX(item)]) + { +// gi.bprintf(PRINT_HIGH,"Not got Grenades\n"); + return false; + } + + // Do we have ammo for it? + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (ent->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + // Check ammo + if( (ent->client->shot_rds < 1) ) + { +// gi.bprintf(PRINT_HIGH,"No change - No Grenades\n"); + loaded = false; + } + // see if we're already using it + if (item == ent->client->weapon) + { +/* //Do we have one in the chamber? + if( ent->client->weaponstate == WEAPON_END_MAG) + { + loaded = false; + }*/ + // No ammo - forget it! + if(!loaded && !clips) + { +// gi.bprintf(PRINT_HIGH,"Not got Grenades\n"); + DropSpecialWeapon ( ent ); + return false; + } + // If it's not loaded - use a new clip +/* else if( !loaded ) + { +// gi.bprintf(PRINT_HIGH,"Need to reload \n"); + Cmd_New_Reload_f ( ent ); + } +*/ return true; + } + + // No ammo + if( !loaded && !clips ) + { +// gi.bprintf(PRINT_HIGH,"No change - No Grenades\n"); + return false; + } + + // Change to this weapon +// gi.bprintf(PRINT_HIGH,"Changing to Grenades\n"); + ent->client->newweapon = item; + ChangeWeapon ( ent ); + return true; +} + + + + + +extern gitem_armor_t jacketarmor_info; +extern gitem_armor_t combatarmor_info; +extern gitem_armor_t bodyarmor_info; + +/////////////////////////////////////////////////////////////////////// +// Check if we can use the armor +/////////////////////////////////////////////////////////////////////// +qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other) +{ +/* + int old_armor_index; + gitem_armor_t *oldinfo; + gitem_armor_t *newinfo; + int newcount; + float salvage; + int salvagecount; + + // get info on new armor + newinfo = (gitem_armor_t *)item->info; + + old_armor_index = ArmorIndex (other); + + // handle armor shards specially + if (item->tag == ARMOR_SHARD) + return true; + + // get info on old armor + if (old_armor_index == ITEM_INDEX(FindItem("Jacket Armor"))) + oldinfo = &jacketarmor_info; + else if (old_armor_index == ITEM_INDEX(FindItem("Combat Armor"))) + oldinfo = &combatarmor_info; + else // (old_armor_index == body_armor_index) + oldinfo = &bodyarmor_info; + + if (newinfo->normal_protection <= oldinfo->normal_protection) + { + // calc new armor values + salvage = newinfo->normal_protection / oldinfo->normal_protection; + salvagecount = salvage * newinfo->base_count; + newcount = other->client->inventory[old_armor_index] + salvagecount; + + if (newcount > oldinfo->max_count) + newcount = oldinfo->max_count; + + // if we're already maxed out then we don't need the new armor + if (other->client->inventory[old_armor_index] >= newcount) + return false; + + } + + return true; +*/ + return false; +} + + +/////////////////////////////////////////////////////////////////////// +// Determines the NEED for an item +// +// This function can be modified to support new items to pick up +// Any other logic that needs to be added for custom decision making +// can be added here. For now it is very simple. +/////////////////////////////////////////////////////////////////////// +float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ) +{ + gitem_t *item = NULL; + int band = 0; + + if( ! item_ent ) + return 0; + + // Make sure domination flags are always considered important short-range goals. + if( dom->value && (Q_stricmp( item_ent->classname, "item_flag" ) == 0) ) + return (DomFlagOwner(item_ent) != self->client->resp.team) ? 9000 : 0; + + item = FindItemByClassname( item_ent->classname ); + if( ! item ) + return 0; + + band = INV_AMMO(self,BAND_NUM) ? 1 : 0; + + switch( item->typeNum ) + { + case FLAG_T1_NUM: + case FLAG_T2_NUM: + // If we're carrying it already, ignore it. + if( INV_AMMO(self,item->typeNum) ) + return 0.0; + // If we have the other flag, we really want to capture it! + else if( INV_AMMO(self,FLAG_T1_NUM) || INV_AMMO(self,FLAG_T2_NUM) ) + return 1000.0; + // Dropped flags are a very high priority. + else if( item_ent->spawnflags & DROPPED_ITEM ) + return 100.0; + // Go for the enemy flag at a fairly high priority. + else if( self->client->resp.team != item->typeNum + 1 - FLAG_T1_NUM ) + return 2.0; + else + return 0.0; + + case MP5_NUM: + case M4_NUM: + case M3_NUM: + case HC_NUM: + case SNIPER_NUM: + case DUAL_NUM: + if( (self->client->unique_weapon_total < unique_weapons->value + band) && ! INV_AMMO(self,item->typeNum) ) + return 0.7; + else + return 0.0; + + case SIL_NUM: + case SLIP_NUM: + case BAND_NUM: + case KEV_NUM: + case LASER_NUM: + case HELM_NUM: + if( (self->client->unique_item_total < unique_items->value) && ! INV_AMMO(self,item->typeNum) ) + return 0.6; + else + return 0.0; + + case MK23_ANUM: + if( INV_AMMO(self,item->typeNum) < self->client->max_pistolmags ) + return 0.3; + else + return 0.0; + + case M4_ANUM: + if( INV_AMMO(self,item->typeNum) < self->client->max_m4mags ) + return 0.3; + else + return 0.0; + + case MP5_ANUM: + if( INV_AMMO(self,item->typeNum) < self->client->max_mp5mags ) + return 0.3; + else + return 0.0; + + case SNIPER_ANUM: + if( INV_AMMO(self,item->typeNum) < self->client->max_sniper_rnds ) + return 0.3; + else + return 0.0; + + case SHELL_ANUM: + if( INV_AMMO(self,item->typeNum) < self->client->max_shells ) + return 0.3; + else + return 0.0; + + case KNIFE_NUM: + if( INV_AMMO(self,item->typeNum) < self->client->knife_max ) + return 0.3; + else + return 0.0; + + default: + return 0.0; + } +} + + +/////////////////////////////////////////////////////////////////////// +// Convert a classname to its index value +// +// I prefer to use integers/defines for simplicity sake. This routine +// can lead to some slowdowns I guess, but makes the rest of the code +// easier to deal with. +/////////////////////////////////////////////////////////////////////// +int ACEIT_ClassnameToIndex( char *classname ) +{ + gitem_t *item = FindItemByClassname( classname ); + if( item ) + return items[item->typeNum].index; + return INVALID; +} + +/* +/////////////////////////////////////////////////////////////////////// +// Only called once per level, when saved will not be called again +// +// Downside of the routine is that items can not move about. If the level +// has been saved before and reloaded, it could cause a problem if there +// are items that spawn at random locations. +// +#define DEBUG // uncomment to write out items to a file. +/////////////////////////////////////////////////////////////////////// +void ACEIT_BuildItemNodeTable (qboolean rebuild) +{ + return; //rekkie -- DEV_1 -- don't call this anymore, nodes will be auto generated by the new nav system + + edict_t *items; + int i,item_index; + vec3_t v,v1,v2; + +#ifdef DEBUG + FILE *pOut; // for testing + if((pOut = fopen("items.txt","wt"))==NULL) + return; +#endif + + num_items = 0; + + // Add game items + for(items = g_edicts; items < &g_edicts[globals.num_edicts]; items++) + { + // filter out crap + if(items->solid == SOLID_NOT) + continue; + + if(!items->classname) + continue; + + ///////////////////////////////////////////////////////////////// + // Items + ///////////////////////////////////////////////////////////////// + item_index = ACEIT_ClassnameToIndex(items->classname); + + //////////////////////////////////////////////////////////////// + // SPECIAL NAV NODE DROPPING CODE + //////////////////////////////////////////////////////////////// + // Special node dropping for platforms + if(strcmp(items->classname,"func_plat")==0) + { + if(!rebuild) + ACEND_AddNode(items,NODE_PLATFORM); + item_index = 99; // to allow to pass the item index test + } + + // Special node dropping for teleporters + if(strcmp(items->classname,"misc_teleporter_dest")==0 || strcmp(items->classname,"misc_teleporter")==0) + { + if(!rebuild) + ACEND_AddNode(items,NODE_TELEPORTER); + item_index = 99; + } + + // Special node dropping for doors - RiEvEr + if( + (strcmp(items->classname,"func_door_rotating")==0) + || (strcmp(items->classname,"func_door")==0 ) + ) + { +// item_index = 99; + if(!rebuild) + { + // add a pointer to the item entity +// item_table[num_items].ent = items; +// item_table[num_items].item = item_index; + // create the node +// item_table[num_items].node = ACEND_AddNode(items,NODE_DOOR); + ACEND_AddNode(items,NODE_DOOR); +//#ifdef DEBUG +// fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname, +// item_table[num_items].node, +// items->s.origin[0],items->s.origin[1],items->s.origin[2]); +//#endif +// num_items++; +// continue; + } + continue; + } + + #ifdef DEBUG + if(item_index == INVALID) + fprintf(pOut,"Rejected item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); +// else +// fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); + #endif + + if(item_index == INVALID) + continue; + + // add a pointer to the item entity + item_table[num_items].ent = items; + item_table[num_items].item = item_index; + + // If new, add nodes for items + if(!rebuild) + { + item_table[num_items].node = ACEND_AddNode(items,NODE_ITEM); +#ifdef DEBUG + fprintf(pOut,"item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); +#endif + num_items++; + } + else // Now if rebuilding, just relink ent structures + { + // Find stored location + for(i=0;is.origin,v); + v[2] +=8; // Raised all nodes to get better links. + + // Add 16 to item type nodes + if(nodes[i].type == NODE_ITEM) + v[2] += 16; + + // Add 32 to teleporter + if(nodes[i].type == NODE_TELEPORTER) + v[2] += 32; + + // Door handling +// if( nodes[i].type == NODE_DOOR) +// { +// vec3_t position; +// // Find mid point of door max and min and put the node there +// VectorClear(position); +// VectorCopy(items->s.origin, position); +// // find center of door +// position[0] = items->absmin[0] + ((items->maxs[0] - items->mins[0]) /2); +// position[1] = items->absmin[1] + ((items->maxs[1] - items->mins[1]) /2); +// position[2] -= 16; // lower it a little +// position[2] = items->absmin[2] + 32; +// // Set location +// VectorCopy(position, v); +// gi.bprintf(PRINT_HIGH, "%d ",items->s.angles[0]); +// } + + + if(nodes[i].type == NODE_PLATFORM) + { + VectorCopy(items->maxs,v1); + VectorCopy(items->mins,v2); + + // To get the center + v[0] = (v1[0] - v2[0]) / 2 + v2[0]; + v[1] = (v1[1] - v2[1]) / 2 + v2[1]; + v[2] = items->mins[2]+64; + } + + if(v[0] == nodes[i].origin[0] && + v[1] == nodes[i].origin[1] && + v[2] == nodes[i].origin[2]) + { + // found a match now link to facts + item_table[num_items].node = i; +#ifdef DEBUG + fprintf(pOut,"Relink item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); +#endif + num_items++; + + } +// else +// { +//#ifdef DEBUG +// fprintf(pOut,"Failed item: %s node: %d pos: %f %f %f\n",items->classname,item_table[num_items].node,items->s.origin[0],items->s.origin[1],items->s.origin[2]); +//#endif +// } + } + } + } + + + } + +#ifdef DEBUG + fclose(pOut); +#endif + +} +*/ diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index e9da23941..4e1251932 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -1,1775 +1,3579 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot_movement.c 7 29/02/00 22:58 Riever $ - * - * $Log: /LTK2/src/acesrc/acebot_movement.c $ - * - * 7 29/02/00 22:58 Riever - * Corner avoidance code added. - * - * 6 29/02/00 11:18 Riever - * Attack function changes: - * Sniper accuracy increased - * JumpKick attack added - * Knife throw added - * Safety check before advancing for knife or kick attack - * Jump Changed to use a velocity hack. - * Jump node support fixed and working well. - * Ledge handling code removed - no longer used. - * - * 5 24/02/00 20:02 Riever - * New door handling code in place. - * - * User: Riever Date: 23/02/00 Time: 23:18 - * New door handling code in place - must have route file. - * User: Riever Date: 21/02/00 Time: 15:16 - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * User: Riever Date: 20/02/00 Time: 20:27 - * Added new members and definitions ready for 2nd generation of bots. - * - */ - -/////////////////////////////////////////////////////////////////////// -// acebot_movement.c - This file contains all of the -// movement routines for the ACE bot -// -/////////////////////////////////////////////////////////////////////// - -#include "../g_local.h" - -#include "acebot.h" -#include "botchat.h" - -qboolean ACEAI_CheckShot(edict_t *self); -void ACEMV_ChangeBotAngle (edict_t *ent); -qboolean ACEND_LadderForward( edict_t *self ); -qboolean ACEMV_CanJump(edict_t *self); - -/* -============= -M_CheckBottom - -Returns false if any part of the bottom of the entity is off an edge that -is not a staircase. - -============= -*/ -int c_yes = 0, c_no = 0; - -#define STEPSIZE 18 - -qboolean M_CheckBottom( edict_t *ent ) -{ - vec3_t mins = {0,0,0}, maxs = {0,0,0}, start = {0,0,0}, stop = {0,0,0}; - trace_t trace; - int x = 0, y = 0; - float mid = 0, bottom = 0; - - VectorAdd( ent->s.origin, ent->mins, mins ); - VectorAdd( ent->s.origin, ent->maxs, maxs ); - - // if all of the points under the corners are solid world, don't bother - // with the tougher checks - // the corners must be within 16 of the midpoint - start[2] = mins[2] - 1; - for( x = 0; x <= 1; x ++ ) - for( y = 0; y <= 1; y ++ ) - { - start[0] = x ? maxs[0] : mins[0]; - start[1] = y ? maxs[1] : mins[1]; - if( gi.pointcontents(start) != CONTENTS_SOLID ) - goto realcheck; - } - - c_yes ++; - return true; // we got out easy - -realcheck: - c_no ++; - // - // check it for real... - // - start[2] = mins[2]; - - // the midpoint must be within 16 of the bottom - start[0] = stop[0] = (mins[0] + maxs[0]) * 0.5; - start[1] = stop[1] = (mins[1] + maxs[1]) * 0.5; - stop[2] = start[2] - 2 * STEPSIZE; - trace = gi.trace( start, vec3_origin, vec3_origin, stop, ent, MASK_MONSTERSOLID ); - - if( trace.fraction == 1.0 ) - return false; - mid = bottom = trace.endpos[2]; - - // the corners must be within 16 of the midpoint - for( x = 0; x <= 1; x ++ ) - for( y = 0; y <= 1; y ++ ) - { - start[0] = stop[0] = x ? maxs[0] : mins[0]; - start[1] = stop[1] = y ? maxs[1] : mins[1]; - - trace = gi.trace( start, vec3_origin, vec3_origin, stop, ent, MASK_MONSTERSOLID ); - - if( trace.fraction != 1.0 && trace.endpos[2] > bottom ) - bottom = trace.endpos[2]; - if( trace.fraction == 1.0 || mid - trace.endpos[2] > STEPSIZE ) - return false; - } - - c_yes ++; - return true; -} - -//============================================================= -// CanMoveSafely (dronebot) -//============================================================= -// Checks for lava and slime -#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) -//#define TRACE_DIST_SHORT 48 -//#define TRACE_DOWN 96 -//#define VEC_ORIGIN tv(0,0,0) -// - -qboolean CanMoveSafely(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest1, dest2; - trace_t trace; - //float this_dist; - -// self->bot_ai.next_safety_time = level.time + EYES_FREQ; - - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - // create a position in front of the bot - VectorMA(self->s.origin, TRACE_DIST_SHORT, dir, dest1); - - // Modified to check for crawl direction - //trace = gi.trace(self->s.origin, mins, self->maxs, dest, self, MASK_SOLID); - //BOTUT_TempLaser (self->s.origin, dest1); - trace = gi.trace(self->s.origin, tv(-16,-16,0), tv(16,16,0), dest1, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (true); - - if (trace.fraction > 0) - { // check that destination is onground, or not above lava/slime - dest1[0] = trace.endpos[0]; - dest1[1] = trace.endpos[1]; - dest1[2] = trace.endpos[2] - 28; - //this_dist = trace.fraction * TRACE_DIST_SHORT; - - if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) - return (true); - - - // create a position a distance below it - VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN + max( lights_camera_action, self->client->uvTime ) * 32; - //BOTUT_TempLaser (trace.endpos, dest2); - trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); - - if( (trace.fraction == 1.0) // long drop! - || (trace.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (trace.ent && (trace.ent->touch == hurt_touch)) // avoid MOD_TRIGGER_HURT - || (trace.surface && (trace.surface->flags & SURF_SKY)) ) // avoid falling onto skybox - { - return (false); - } - else - { - return (true); - } - } - //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); - return (false); -} -/////////////////////////////////////////////////////////////////////// -// Checks if bot can move (really just checking the ground) -// Also, this is not a real accurate check, but does a -// pretty good job and looks for lava/slime. -/////////////////////////////////////////////////////////////////////// -qboolean ACEMV_CanMove(edict_t *self, int direction) -{ - vec3_t forward, right; - vec3_t offset,start,end; - vec3_t angles; - trace_t tr; - - // Now check to see if move will move us off an edge - VectorCopy(self->s.angles,angles); - - if(direction == MOVE_LEFT) - angles[1] += 90; - else if(direction == MOVE_RIGHT) - angles[1] -= 90; - else if(direction == MOVE_BACK) - angles[1] -=180; - - // Set up the vectors - AngleVectors (angles, forward, right, NULL); - - VectorSet(offset, 36, 0, 24); - G_ProjectSource (self->s.origin, offset, forward, right, start); - - VectorSet(offset, 36, 0, -110); // RiEvEr reduced drop distance - G_ProjectSource (self->s.origin, offset, forward, right, end); - - //AQ2 ADDED MASK_SOLID - tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); - - if( ((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self,angles))) // avoid falling after LCA - || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (tr.ent && (tr.ent->touch == hurt_touch)) ) // avoid MOD_TRIGGER_HURT - { - if( self->last_door_time < level.framenum ) - { - if( debug_mode && (level.framenum % HZ == 0) ) - debug_printf( "%s: move blocked (ACEMV_CanMove)\n", self->client->pers.netname ); - - Cmd_OpenDoor_f ( self ); // Open the door - //self->last_door_time = level.framenum + 3 * HZ; // wait! - } - return false; - } - - return true; // yup, can move -} - -/////////////////////////////////////////////////////////////////////// -// Checks if bot can jump (really just checking the ground) -// Also, this is not a real accurate check, but does a -// pretty good job and looks for lava/slime. -/////////////////////////////////////////////////////////////////////// -qboolean ACEMV_CanJumpInternal(edict_t *self, int direction) -{ - vec3_t forward, right; - vec3_t offset,start,end; - vec3_t angles; - trace_t tr; - - // Now check to see if move will move us off an edge - VectorCopy(self->s.angles,angles); - - if(direction == MOVE_LEFT) - angles[1] += 90; - else if(direction == MOVE_RIGHT) - angles[1] -= 90; - else if(direction == MOVE_BACK) - angles[1] -=180; - - // Set up the vectors - AngleVectors (angles, forward, right, NULL); - - VectorSet(offset, 132, 0, 24); - G_ProjectSource (self->s.origin, offset, forward, right, start); - - VectorSet(offset, 132, 0, -110); // RiEvEr reduced drop distance - G_ProjectSource (self->s.origin, offset, forward, right, end); - - //AQ2 ADDED MASK_SOLID - tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); - - if( ((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self,angles))) // avoid falling after LCA - || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (tr.ent && (tr.ent->touch == hurt_touch)) ) // avoid MOD_TRIGGER_HURT - { - if( self->last_door_time < level.framenum ) - { - if( debug_mode && (level.framenum % HZ == 0) ) - debug_printf( "%s: move blocked (ACEMV_CanJumpInternal)\n", self->client->pers.netname ); - - Cmd_OpenDoor_f ( self ); // Open the door - //self->last_door_time = level.framenum + 3 * HZ; // wait! - } - return false; - } - - return true; // yup, can move -} - -qboolean ACEMV_CanJump(edict_t *self) -{ - return( (ACEMV_CanJumpInternal(self, MOVE_LEFT)) && - (ACEMV_CanJumpInternal(self, MOVE_RIGHT)) && - (ACEMV_CanJumpInternal(self, MOVE_BACK)) && - (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) ); - -} - -/////////////////////////////////////////////////////////////////////// -// Handle special cases of crouch/jump -// -// If the move is resolved here, this function returns -// true. -/////////////////////////////////////////////////////////////////////// -qboolean ACEMV_SpecialMove(edict_t *self, usercmd_t *ucmd) -{ - vec3_t dir,forward,right,start,end,offset; - vec3_t top; - trace_t tr; - - // Get current direction - VectorCopy(self->client->ps.viewangles,dir); - dir[YAW] = self->s.angles[YAW]; - AngleVectors (dir, forward, right, NULL); - - VectorSet(offset, 0, 0, 0); // changed from 18,0,0 - G_ProjectSource (self->s.origin, offset, forward, right, start); - offset[0] += 18; - G_ProjectSource (self->s.origin, offset, forward, right, end); - - // trace it - start[2] += 18; // so they are not jumping all the time - end[2] += 18; - tr = gi.trace (start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); - -//RiEvEr if(tr.allsolid) - if( tr.fraction < 1.0) - { - - //RiEvEr - if (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0) - { - ucmd->forwardmove = -SPEED_RUN; // back up in case it's trying to open - return true; - } - //R - - // Check for crouching - start[2] -= 14; - end[2] -= 14; - - // Set up for crouching check - VectorCopy(self->maxs,top); - top[2] = 0.0; // crouching height - tr = gi.trace (start, self->mins, top, end, self, MASK_PLAYERSOLID); - - // Crouch -//RiEvEr if(!tr.allsolid) - if( tr.fraction == 1.0 ) - { - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = -SPEED_RUN; - return true; - } - - // Check for jump - start[2] += 32; - end[2] += 32; - tr = gi.trace (start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); - -//RiEvEr if(!tr.allsolid) - if( tr.fraction == 1.0) - { - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = SPEED_RUN; - return true; - } - //RiEvEr - // My corner management code - if( BOTCOL_CheckBump(self, ucmd )) - { - if( BOTCOL_CanStrafeSafely(self, self->s.angles ) ) - { - ucmd->sidemove = self->bot_strafe; - return true; - } - } - //R - } - - return false; // We did not resolve a move here -} - - -/////////////////////////////////////////////////////////////////////// -// Checks for obstructions in front of bot -// -// This is a function I created origianlly for ACE that -// tries to help steer the bot around obstructions. -// -// If the move is resolved here, this function returns true. -/////////////////////////////////////////////////////////////////////// -qboolean ACEMV_CheckEyes(edict_t *self, usercmd_t *ucmd) -{ - vec3_t forward, right; - vec3_t leftstart, rightstart,focalpoint; - vec3_t /* upstart,*/upend; - vec3_t dir,offset; - - trace_t traceRight,traceLeft,/*traceUp,*/ traceFront; // for eyesight - - // Get current angle and set up "eyes" - VectorCopy(self->s.angles,dir); - AngleVectors (dir, forward, right, NULL); - - // Let them move to targets by walls - if(!self->movetarget) -// VectorSet(offset,200,0,4); // focalpoint - VectorSet(offset,64,0,4); // focalpoint - else - VectorSet(offset,36,0,4); // focalpoint - - G_ProjectSource (self->s.origin, offset, forward, right, focalpoint); - - // Check from self to focalpoint - // Ladder code - VectorSet(offset,36,0,0); // set as high as possible - G_ProjectSource (self->s.origin, offset, forward, right, upend); - //AQ2 ADDED MASK_SOLID - traceFront = gi.trace(self->s.origin, self->mins, self->maxs, upend, self, MASK_SOLID|MASK_OPAQUE); - - if(traceFront.contents & 0x8000000) // using detail brush here cuz sometimes it does not pick up ladders...?? - { - ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = SPEED_RUN; - return true; - } - - // If this check fails we need to continue on with more detailed checks - if(traceFront.fraction == 1) - { - ucmd->forwardmove = SPEED_RUN; - return true; - } - - VectorSet(offset, 0, 18, 4); - G_ProjectSource (self->s.origin, offset, forward, right, leftstart); - - offset[1] -= 36; // want to make sure this is correct - //VectorSet(offset, 0, -18, 4); - G_ProjectSource (self->s.origin, offset, forward, right, rightstart); - - traceRight = gi.trace(rightstart, NULL, NULL, focalpoint, self, MASK_OPAQUE); - traceLeft = gi.trace(leftstart, NULL, NULL, focalpoint, self, MASK_OPAQUE); - - // Wall checking code, this will degenerate progressivly so the least cost - // check will be done first. - - // If open space move ok - if(traceRight.fraction != 1 || traceLeft.fraction != 1 || strcmp(traceLeft.ent->classname,"func_door")!=0) - { -//@@ weird code... -/* // Special uppoint logic to check for slopes/stairs/jumping etc. - VectorSet(offset, 0, 18, 24); - G_ProjectSource (self->s.origin, offset, forward, right, upstart); - - VectorSet(offset,0,0,200); // scan for height above head - G_ProjectSource (self->s.origin, offset, forward, right, upend); - traceUp = gi.trace(upstart, NULL, NULL, upend, self, MASK_OPAQUE); - - VectorSet(offset,200,0,200*traceUp.fraction-5); // set as high as possible - G_ProjectSource (self->s.origin, offset, forward, right, upend); - traceUp = gi.trace(upstart, NULL, NULL, upend, self, MASK_OPAQUE); - - // If the upper trace is not open, we need to turn. - if(traceUp.fraction != 1)*/ - { - if(traceRight.fraction > traceLeft.fraction) - self->s.angles[YAW] += (1.0 - traceLeft.fraction) * 45.0; - else - self->s.angles[YAW] += -(1.0 - traceRight.fraction) * 45.0; - - ucmd->forwardmove = SPEED_RUN; - ACEMV_ChangeBotAngle(self); - return true; - } - } - - return false; -} - -/////////////////////////////////////////////////////////////////////// -// Make the change in angles a little more gradual, not so snappy -// Subtle, but noticeable. -// -// Modified from the original id ChangeYaw code... -/////////////////////////////////////////////////////////////////////// -void ACEMV_ChangeBotAngle (edict_t *ent) -{ - float ideal_yaw; - float ideal_pitch; - float current_yaw; - float current_pitch; - float speed; - vec3_t ideal_angle; - float yaw_move = 0.f; - float pitch_move = 0.f; - float move_ratio = 1.f; - - // Normalize the move angle first - VectorNormalize(ent->move_vector); - - current_yaw = anglemod(ent->s.angles[YAW]); - current_pitch = anglemod(ent->s.angles[PITCH]); - - vectoangles (ent->move_vector, ideal_angle); - - ideal_yaw = anglemod(ideal_angle[YAW]); - ideal_pitch = anglemod(ideal_angle[PITCH]); - - // Raptor007: Compensate for M4 climb. - if( (ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME)) ) - ideal_pitch -= ent->client->kick_angles[PITCH]; - - // Yaw - if (current_yaw != ideal_yaw) - { - yaw_move = ideal_yaw - current_yaw; - speed = ent->yaw_speed / (float) BASE_FRAMERATE; - if (ideal_yaw > current_yaw) - { - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - if (yaw_move > 0) - { - if (yaw_move > speed) - yaw_move = speed; - } - else - { - if (yaw_move < -speed) - yaw_move = -speed; - } - } - - // Pitch - if (current_pitch != ideal_pitch) - { - pitch_move = ideal_pitch - current_pitch; - speed = ent->yaw_speed / (float) BASE_FRAMERATE; - if (ideal_pitch > current_pitch) - { - if (pitch_move >= 180) - pitch_move = pitch_move - 360; - } - else - { - if (pitch_move <= -180) - pitch_move = pitch_move + 360; - } - if (pitch_move > 0) - { - if (pitch_move > speed) - pitch_move = speed; - } - else - { - if (pitch_move < -speed) - pitch_move = -speed; - } - } - - // Raptor007: Interpolate towards desired changes at higher fps. - if( ! FRAMESYNC ) - { - int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; - move_ratio = 1.f / (float) frames_until_sync; - } - - ent->s.angles[YAW] = anglemod( current_yaw + yaw_move * move_ratio ); - ent->s.angles[PITCH] = anglemod( current_pitch + pitch_move * move_ratio ); -} - -/////////////////////////////////////////////////////////////////////// -// Set bot to move to it's movetarget. (following node path) -/////////////////////////////////////////////////////////////////////// -void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) -{ - // If a rocket or grenade is around deal with it - // Simple, but effective (could be rewritten to be more accurate) - if(strcmp(self->movetarget->classname,"rocket")==0 || - strcmp(self->movetarget->classname,"grenade")==0) - { - VectorSubtract (self->movetarget->s.origin, self->s.origin, self->move_vector); - ACEMV_ChangeBotAngle(self); - if(debug_mode) - debug_printf("%s: Oh crap a rocket!\n",self->client->pers.netname); - - // strafe left/right - if( (self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT) ) - ucmd->sidemove = -SPEED_RUN; - else if(ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - else - ucmd->sidemove = 0; - self->bot_strafe = ucmd->sidemove; - return; - - } - else - { - // Are we there yet? - vec3_t v; - trace_t tTrace; - vec3_t vStart, vDest; - - if( nodes[self->current_node].type == NODE_DOOR ) - { - if( nodes[self->next_node].type == NODE_DOOR ) - { - VectorCopy( nodes[self->current_node].origin, vStart ); - VectorCopy( nodes[self->next_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); - } - else - { - VectorCopy( self->s.origin, vStart ); - VectorCopy( nodes[self->current_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->current_node].origin, v ); - } - - if(VectorLength(v) < 32) - { - // See if we have a clear shot at it - PRETRACE(); - tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID); - POSTTRACE(); - if( tTrace.fraction <1.0 ) - { - if( (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) - || (strcmp( tTrace.ent->classname, "func_door") == 0) ) - { - // The door is in the way - // See if it's moving - if( (VectorLength(tTrace.ent->avelocity) > 0.0) - || ((tTrace.ent->moveinfo.state != STATE_BOTTOM) && (tTrace.ent->moveinfo.state != STATE_TOP)) ) - { - if( self->last_door_time < level.framenum ) - self->last_door_time = level.framenum; - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_WALK; - return; - } - else - { - // Open it - if( (self->last_door_time < level.framenum - 2.0 * HZ) && - (tTrace.ent->moveinfo.state != STATE_TOP) && - (tTrace.ent->moveinfo.state != STATE_UP) ) - { - Cmd_OpenDoor_f ( self ); // Open the door - self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! - ucmd->forwardmove = 0; - VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); - ACEMV_ChangeBotAngle( self ); - return; - } - } - } - else if( tTrace.ent->client ) - { - // Back up slowly - may have a teammate in the way - if( self->last_door_time < level.framenum ) - self->last_door_time = level.framenum; - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_WALK; - return; - } - } - else - { - // If trace from bot to next node hits rotating door, it should just strafe toward the path. - VectorCopy( self->s.origin, vStart ); - VectorCopy( nodes[self->next_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); - if( VectorLength(v) < 32 ) - { - PRETRACE(); - tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID ); - POSTTRACE(); - if( (tTrace.fraction < 1.) - && (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) - && (VectorLength(tTrace.ent->avelocity) > 0.) ) - { - if( self->next_node != self->current_node ) - { - // Decide if the bot should strafe to stay on course. - vec3_t dist = {0,0,0}; - VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); - v[2] = 0; - VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); - dist[2] = 0; - if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) - { - float left = 0; - VectorNormalize( v ); - VectorRotate2( v, 90 ); - left = DotProduct( v, dist ); - if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) - ucmd->sidemove = SPEED_RUN; - else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) - ucmd->sidemove = -SPEED_RUN; - } - } - else - { - ucmd->sidemove = 0; - if( (self->bot_strafe >= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) - ucmd->sidemove = SPEED_RUN; - else if( ACEMV_CanMove( self, MOVE_LEFT ) ) - ucmd->sidemove = -SPEED_RUN; - self->bot_strafe = ucmd->sidemove; - } - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_RUN; - VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); - ACEMV_ChangeBotAngle( self ); - return; - } - } - } - } - } - - // Set bot's movement direction - VectorSubtract (self->movetarget->s.origin, self->s.origin, self->move_vector); - ACEMV_ChangeBotAngle(self); - ucmd->forwardmove = SPEED_RUN; - return; - } -} - -/////////////////////////////////////////////////////////////////////// -// Main movement code. (following node path) -/////////////////////////////////////////////////////////////////////// -void ACEMV_Move(edict_t *self, usercmd_t *ucmd) -{ - vec3_t dist; - int current_node_type=-1; - int next_node_type=-1; - int i; - float distance; - - // Do not follow path when teammates are still inside us. - if( OnTransparentList(self) ) - { - self->state = STATE_WANDER; - self->wander_timeout = level.framenum + (random() + 0.5) * HZ; - return; - } - - // Get current and next node back from nav code. - if(!ACEND_FollowPath(self)) - { - if( - ( !teamplay->value ) || - (teamplay->value && level.framenum >= (self->teamPauseTime)) - ) - { - self->state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - } - else - { - // Teamplay mode - just fan out and chill - if (self->state == STATE_FLEE) - { - self->state = STATE_POSITION; - self->wander_timeout = level.framenum + 3.0 * HZ; - } - else - { - self->state = STATE_POSITION; - self->wander_timeout = level.framenum + 1.0 * HZ; - } - } - self->goal_node = INVALID; - return; - } - - current_node_type = nodes[self->current_node].type; - next_node_type = nodes[self->next_node].type; - - /////////////////////////// - // Move To Goal - /////////////////////////// - if (self->movetarget) - ACEMV_MoveToGoal(self,ucmd); - - else if( (self->next_node != self->current_node) && (current_node_type != NODE_LADDER) ) - { - // Decide if the bot should strafe to stay on course. - vec3_t v = {0,0,0}; - VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); - v[2] = 0; - VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); - dist[2] = 0; - if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) - { - float left = 0; - VectorNormalize( v ); - VectorRotate2( v, 90 ); - left = DotProduct( v, dist ); - if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) - ucmd->sidemove = SPEED_RUN; - else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) - ucmd->sidemove = -SPEED_RUN; - } - } - - if(current_node_type == NODE_GRAPPLE) - { - if( self->last_door_time < level.framenum ) - { - ucmd->forwardmove = SPEED_WALK; //walk forwards a little -// Cmd_OpenDoor_f ( self ); // Open the door - self->last_door_time = level.framenum + random() * 2.0 * HZ; // wait! - } - - } - -/* //////////////////////////////////////////////////////// - // Grapple - /////////////////////////////////////////////////////// - if(next_node_type == NODE_GRAPPLE) - { - ACEMV_ChangeBotAngle(self); - ACEIT_ChangeWeapon(self,FindItem("grapple")); - ucmd->buttons = BUTTON_ATTACK; - return; - } - // Reset the grapple if hangin on a graple node - if(current_node_type == NODE_GRAPPLE) - { - CTFPlayerResetGrapple(self); - return; - }*/ - //////////////////////////////////////////////////////// - // Doors - RiEvEr - // modified by Werewolf - /////////////////////////////////////////////////////// - if(current_node_type == NODE_DOOR) - { - // We are trying to go through a door - trace_t tTrace; - vec3_t vStart, vDest; - - if( next_node_type == NODE_DOOR ) - VectorCopy( nodes[self->current_node].origin, vStart ); - else - VectorCopy( self->s.origin, vStart ); - - VectorCopy( nodes[self->next_node].origin, vDest ); - VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); - dist[2] = 0; - distance = VectorLength(dist); - - // See if we have a clear shot at it - PRETRACE(); - tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID); - POSTTRACE(); - - if( (tTrace.fraction < 1.0) && (distance < 512) ) - { - if( (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) - || (strcmp( tTrace.ent->classname, "func_door") == 0) ) - { - // The door is in the way - // See if it's moving - if( (VectorLength(tTrace.ent->avelocity) > 0.0) - || ((tTrace.ent->moveinfo.state != STATE_BOTTOM) && (tTrace.ent->moveinfo.state != STATE_TOP)) ) - { - if( self->last_door_time < level.framenum ) - self->last_door_time = level.framenum; - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_WALK; //walk backwards a little - else if( self->tries && self->groundentity ) - ucmd->upmove = SPEED_RUN; - return; - } - else - { - // Open it - if( (self->last_door_time < level.framenum - 2.0 * HZ) && - (tTrace.ent->moveinfo.state != STATE_TOP) && - (tTrace.ent->moveinfo.state != STATE_UP) ) - { - Cmd_OpenDoor_f ( self ); // Open the door - self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! - if( self->tries && self->groundentity ) - ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = 0; - return; - } - } - } - else if( tTrace.ent->client ) - { - // Back up slowly - may have a teammate in the way - if( self->last_door_time < level.framenum ) - self->last_door_time = level.framenum; - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_WALK; - else if( self->tries && self->groundentity ) - ucmd->upmove = SPEED_RUN; - return; - } - } - else - { - // If trace from bot to next node hits rotating door, it should just strafe toward the path. - vec3_t v = {0,0,0}; - VectorCopy( self->s.origin, vStart ); - VectorCopy( nodes[self->next_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); - if( VectorLength(v) < 32 ) - { - PRETRACE(); - tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID ); - POSTTRACE(); - if( (tTrace.fraction < 1.) - && (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) - && (VectorLength(tTrace.ent->avelocity) > 0.) ) - { - if( self->next_node != self->current_node ) - { - // Decide if the bot should strafe to stay on course. - VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); - v[2] = 0; - VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); - dist[2] = 0; - if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) - { - float left = 0; - VectorNormalize( v ); - VectorRotate2( v, 90 ); - left = DotProduct( v, dist ); - if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) - ucmd->sidemove = SPEED_RUN; - else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) - ucmd->sidemove = -SPEED_RUN; - } - } - else - { - ucmd->sidemove = 0; - if( (self->bot_strafe >= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) - ucmd->sidemove = SPEED_RUN; - else if( ACEMV_CanMove( self, MOVE_LEFT ) ) - ucmd->sidemove = -SPEED_RUN; - self->bot_strafe = ucmd->sidemove; - } - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_RUN; - VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); - ACEMV_ChangeBotAngle( self ); - return; - } - } - } - } - - //////////////////////////////////////////////////////// - // Platforms - /////////////////////////////////////////////////////// - if( (next_node_type == NODE_PLATFORM) && ((current_node_type != NODE_PLATFORM) || (self->current_node == self->next_node)) ) - { - // Check if the platform is directly above us. - vec3_t start, above; - trace_t tr; - VectorCopy( self->s.origin, start ); - VectorCopy( self->s.origin, above ); - start[2] += 32; - above[2] += 2048; - tr = gi.trace( start, tv(-24,-24,-8), tv(24,24,8), above, self, MASK_PLAYERSOLID ); - if( (tr.fraction < 1.0) && tr.ent && (tr.ent->use == Use_Plat) ) - { - // If it is directly above us, get out of the way. - ucmd->sidemove = 0; - if( ACEMV_CanMove( self, MOVE_BACK ) ) - ucmd->forwardmove = -SPEED_WALK; - else if( (self->bot_strafe <= 0) && ACEMV_CanMove( self, MOVE_LEFT ) ) - ucmd->sidemove = -SPEED_WALK; - else if( ACEMV_CanMove( self, MOVE_RIGHT ) ) - ucmd->sidemove = SPEED_WALK; - self->bot_strafe = ucmd->sidemove; - ACEMV_ChangeBotAngle(self); - return; - } - } - if(current_node_type != NODE_PLATFORM && next_node_type == NODE_PLATFORM) - { - // check to see if lift is down? - for(i=0;inext_node) - if(item_table[i].ent->moveinfo.state != STATE_BOTTOM) - return; // Wait for elevator - } - if( (current_node_type == NODE_PLATFORM) && self->groundentity - && ((self->groundentity->moveinfo.state == STATE_UP) || (self->groundentity->moveinfo.state == STATE_DOWN)) ) - { - // Standing on moving elevator. - ucmd->forwardmove = 0; - ucmd->sidemove = 0; - ucmd->upmove = 0; - if( VectorLength(self->groundentity->velocity) < 8 ) - ucmd->upmove = SPEED_RUN; - ACEMV_ChangeBotAngle(self); - if( debug_mode && (level.framenum % HZ == 0) ) - debug_printf( "%s: platform move state %i\n", self->client->pers.netname, self->groundentity->moveinfo.state ); - return; - } - if( (current_node_type == NODE_PLATFORM) && (next_node_type == NODE_PLATFORM) && (self->current_node != self->next_node) ) - { - // Move to the center - self->move_vector[2] = 0; // kill z movement - if(VectorLength(self->move_vector) > 10) - ucmd->forwardmove = SPEED_WALK; // walk to center - - ACEMV_ChangeBotAngle(self); - - return; // No move, riding elevator - } - - //////////////////////////////////////////////////////// - // Jumpto Nodes - /////////////////////////////////////////////////////// - if(next_node_type == NODE_JUMP) -// || -// ( current_node_type == NODE_JUMP -// && next_node_type != NODE_ITEM && nodes[self->next_node].origin[2] > self->s.origin[2]+16) -// ) - { - VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist); - // Lose the vertical component - dist[2] = 0; - // Get the absolute length - distance = VectorLength(dist); - - //if (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) - { - // Jump only when we are moving the correct direction. - vec3_t velocity; - VectorCopy( self->velocity, velocity ); - velocity[2] = 0; - VectorNormalize( velocity ); - VectorNormalize( dist ); - if( DotProduct( velocity, dist ) > 0.8 ) - ucmd->upmove = SPEED_RUN; - - //Kill running movement -// self->move_vector[0]=0; -// self->move_vector[1]=0; -// self->move_vector[2]=0; - // Set up a jump move - if( distance < 256 ) - ucmd->forwardmove = SPEED_RUN * sqrtf( distance / 256 ); - else - ucmd->forwardmove = SPEED_RUN; - - //self->move_vector[2] *= 2; - - ACEMV_ChangeBotAngle(self); - - /* - // RiEvEr - re-instate the velocity hack - if (self->jumphack_timeout < level.framenum) - { - VectorCopy(self->move_vector,dist); - if ((440 * distance / 128) < 440) - VectorScale(dist,(440 * distance / 128),self->velocity); - else - VectorScale(dist,440,self->velocity); - self->jumphack_timeout = level.framenum + 3.0 * HZ; - } - */ - } - /* - else - { - self->goal_node = INVALID; - } - */ - return; - } - - //////////////////////////////////////////////////////// - // Ladder Nodes - /////////////////////////////////////////////////////// - - // Find the distance vector to the next node - VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist); - // Lose the vertical component - dist[2] = 0; - // Get the absolute length - distance = VectorLength(dist); - - // If on the ladder - if( (next_node_type == NODE_LADDER) && (distance < 64) - && ((nodes[self->next_node].origin[2] > self->s.origin[2]) || ! self->groundentity) ) - { - // FIXME: Dirty hack so the bots can actually use ladders. - VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); - VectorNormalize( dist ); - VectorScale( dist, SPEED_RUN, self->velocity ); - if( dist[2] >= 0 ) - self->velocity[2] = min( 200, self->velocity[2] ); - else - self->velocity[2] = max( -200, self->velocity[2] ); - - if( self->velocity[2] < 0 ) - { - // If we are going down the ladder, check for teammates coming up and yield to them. - trace_t tr; - tr = gi.trace( self->s.origin, tv(-16,-16,-8), tv(16,16,8), nodes[self->next_node].origin, self, MASK_PLAYERSOLID ); - if( (tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0) ) - { - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] = 200; - } - } - - ACEMV_ChangeBotAngle(self); - return; - } - - // If getting onto the ladder going up - if( (next_node_type == NODE_LADDER) && (distance < 200) - && (nodes[self->next_node].origin[2] >= self->s.origin[2]) ) - { - // Jump only when we are moving the correct direction. - vec3_t velocity; - VectorCopy( self->velocity, velocity ); - velocity[2] = 0; - VectorNormalize( velocity ); - VectorNormalize( dist ); - if( DotProduct( velocity, dist ) > 0.8 ) - ucmd->upmove = SPEED_RUN; - - ucmd->forwardmove = SPEED_WALK / 2; - - ACEMV_ChangeBotAngle(self); - return; - } - - // If getting off the ladder - if( (current_node_type == NODE_LADDER) && (next_node_type != NODE_LADDER) ) - { - ucmd->forwardmove = SPEED_WALK; - - if( (nodes[self->next_node].origin[2] >= self->s.origin[2]) - && (nodes[self->next_node].origin[2] >= nodes[self->current_node].origin[2]) - && ( ((self->velocity[2] > 0) && ! self->groundentity) || OnLadder(self) ) ) - { - ucmd->upmove = SPEED_RUN; - self->velocity[2] = min( 200, distance * 10 ); // Jump higher for farther node - } - - ACEMV_ChangeBotAngle(self); - return; - } - - // If getting onto the ladder going down - if( (current_node_type != NODE_LADDER) && (next_node_type == NODE_LADDER) ) - { - ucmd->forwardmove = SPEED_WALK / 2; - - if( (distance < 200) && (nodes[self->next_node].origin[2] <= self->s.origin[2] + 16) ) - ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching - - ACEMV_ChangeBotAngle(self); - return; - } - -/* //============================== - // LEDGES etc.. - // If trying to jump up a ledge - if( - (current_node_type == NODE_MOVE && - nodes[self->next_node].origin[2] > self->s.origin[2]+16 && distance < NODE_DENSITY) - ) - { - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = SPEED_RUN; - ACEMV_ChangeBotAngle(self); - return; - }*/ - //////////////////////////////////////////////////////// - // Water Nodes - /////////////////////////////////////////////////////// - if(current_node_type == NODE_WATER) - { - // We need to be pointed up/down - ACEMV_ChangeBotAngle(self); - - // If the next node is not in the water, then move up to get out. - if(next_node_type != NODE_WATER && !(gi.pointcontents(nodes[self->next_node].origin) & MASK_WATER)) // Exit water - ucmd->upmove = SPEED_RUN; - - ucmd->forwardmove = SPEED_RUN * 3 / 4; - return; - - } - - // Falling off ledge? -/* if(!self->groundentity) - { - ACEMV_ChangeBotAngle(self); - - self->velocity[0] = self->move_vector[0] * 360; - self->velocity[1] = self->move_vector[1] * 360; - - return; - }*/ - - // Check to see if stuck, and if so try to free us - // Also handles crouching - VectorSubtract( self->s.origin, self->lastPosition, dist ); - if( (VectorLength(self->velocity) < 37) || (VectorLength(dist) < FRAMETIME) ) - { - // Keep a random factor just in case.... - if(random() > 0.5 && ACEMV_SpecialMove(self, ucmd)) - return; - - if( self->groundentity ) - { - // Check if we are obstructed by an oncoming teammate, and if so strafe right. - trace_t tr; - tr = gi.trace( self->s.origin, tv(-16,-16,-8), tv(16,16,8), nodes[self->next_node].origin, self, MASK_PLAYERSOLID ); - if( (tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client - && (DotProduct( self->velocity, tr.ent->velocity ) <= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) - { - ucmd->sidemove = SPEED_RUN; - ACEMV_ChangeBotAngle(self); - return; - } - } - - self->s.angles[YAW] += random() * 180 - 90; - - ACEMV_ChangeBotAngle(self); - - ucmd->forwardmove = SPEED_RUN; - - // Raptor007: Only jump when trying to gain altitude, not down ledges. - if( nodes[self->next_node].origin[2] > self->s.origin[2] + 16 ) - ucmd->upmove = SPEED_RUN; - - return; - } - - //////////////////////////////////////////////////////// - // Walk Nodes - /////////////////////////////////////////////////////// - ACEMV_ChangeBotAngle(self); - - VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); - dist[2] = 0; - distance = VectorLength(dist); - - if( ! self->groundentity ) - { - // When in air, control speed carefully to avoid overshooting the next node. - if( distance < 256 ) - ucmd->forwardmove = SPEED_RUN * sqrtf( distance / 256 ); - else - ucmd->forwardmove = SPEED_RUN; - } - else if( ! ACEMV_CanMove( self, MOVE_FORWARD ) ) - { - // If we reached a ledge, slow down. - if( distance < 512 ) - { - // Jump unless dropping down to the next node. - if( (nodes[self->next_node].origin[2] > self->s.origin[2] + 7) && (distance < 500) ) - ucmd->upmove = SPEED_RUN; - - ucmd->forwardmove = SPEED_WALK * sqrtf( distance / 512 ); - } - else - ucmd->forwardmove = SPEED_WALK; - } - else - // Otherwise move as fast as we can. - ucmd->forwardmove = SPEED_RUN; -} - - -/////////////////////////////////////////////////////////////////////// -// Wandering code (based on old ACE movement code) -/////////////////////////////////////////////////////////////////////// -// -// RiEvEr - this routine is of a very poor standard and has a LOT of problems -// Maybe replace this with the DroneBot or ReDeMpTiOn wander code? -// -void ACEMV_Wander(edict_t *self, usercmd_t *ucmd) -{ - vec3_t temp; - int content_head = 0, content_feet = 0; - float moved = 0; - - // Do not move - if(self->next_move_time > level.framenum) - return; - - // Special check for elevators, stand still until the ride comes to a complete stop. - if( self->groundentity && (VectorLength(self->groundentity->velocity) >= 8) ) - if(self->groundentity->moveinfo.state == STATE_UP || - self->groundentity->moveinfo.state == STATE_DOWN) // only move when platform not - { - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] = 0; - self->next_move_time = level.framenum + 0.5 * HZ; - return; - } - - - // Is there a target to move to - if (self->movetarget) - ACEMV_MoveToGoal(self,ucmd); - - VectorSubtract( self->s.origin, self->lastPosition, temp ); - moved = VectorLength(temp); - - //////////////////////////////// - // Ladder? - //////////////////////////////// - // To avoid unnecessary falls, don't use ladders when on solid ground. - if( (! self->groundentity) && OnLadder(self) ) - { - ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = 0; - ucmd->sidemove = 0; - self->s.angles[PITCH] = -15; - - // FIXME: Dirty hack so the bots can actually use ladders. - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] = 200; - - if( moved >= FRAMETIME ) - return; - } - - //////////////////////////////// - // Swimming? - //////////////////////////////// - VectorCopy(self->s.origin,temp); - temp[2] += 22; - content_head = gi.pointcontents(temp); - temp[2] = self->s.origin[2] - 8; - content_feet = gi.pointcontents(temp); - - // Just try to keep our head above water. - if( content_head & MASK_WATER ) - { - // If drowning and no node, move up - if(self->client->next_drown_framenum > 0) - { - ucmd->upmove = SPEED_RUN; - self->s.angles[PITCH] = -45; - } - else - ucmd->upmove = SPEED_WALK; - } - - // Don't wade in lava, try to get out! - if( content_feet & (CONTENTS_LAVA|CONTENTS_SLIME) ) - ucmd->upmove = SPEED_RUN; - - // See if we're jumping out of the water. - if( ! self->groundentity - && (content_feet & MASK_WATER) - && ACEMV_SpecialMove( self, ucmd ) ) - { - if( ucmd->upmove > 0 ) - self->velocity[2] = 270; // FIXME: Is there a cleaner way? - if( moved >= FRAMETIME ) - return; - } - -//@@ if(ACEMV_CheckEyes(self,ucmd)) -// return; - - // Check for special movement if we have a normal move (have to test) - if( (VectorLength(self->velocity) < 37) || (moved < FRAMETIME) ) - { - if(random() > 0.1 && ACEMV_SpecialMove(self,ucmd)) - return; - - self->s.angles[YAW] += random() * 180 - 90; - self->s.angles[PITCH] = 0; - - if( content_feet & MASK_WATER ) // Just keep swimming. - ucmd->forwardmove = SPEED_RUN; - else if(!M_CheckBottom(self) && !self->groundentity) // if there is ground continue otherwise wait for next move - ucmd->forwardmove = 0; - else if( ACEMV_CanMove( self, MOVE_FORWARD)) - ucmd->forwardmove = SPEED_WALK; - - return; - } - - // Otherwise move as fast as we can - // If it's safe to move forward (I can't believe ACE didn't check this! - if( ACEMV_CanMove( self, MOVE_FORWARD ) - || (content_feet & MASK_WATER) ) - { - ucmd->forwardmove = SPEED_RUN; - } - else - { - // Need a "findbestdirection" routine in here - // Forget about this route! - ucmd->forwardmove = -SPEED_WALK / 2; - self->movetarget = NULL; - } - -} - -/////////////////////////////////////////////////////////////////////// -// Attack movement routine -// -// NOTE: Very simple for now, just a basic move about avoidance. -// Change this routine for more advanced attack movement. -/////////////////////////////////////////////////////////////////////// -void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) -{ - float c; - vec3_t target; - vec3_t attackvector; - float dist; - qboolean bHasWeapon; // Needed to allow knife throwing and kick attacks - qboolean enemy_front; - - bHasWeapon = self->grenadewait || ACEAI_ChooseWeapon(self); - - // Check distance to enemy - VectorSubtract( self->s.origin, self->enemy->s.origin, attackvector); - dist = VectorLength( attackvector); - - enemy_front = infront( self, self->enemy ); - - //If we're fleeing, don't bother moving randomly around the enemy and stuff... -// if (self->state != STATE_FLEE) - { - -//AQ2 CHANGE - // Don't stand around if all you have is a knife or no weapon. - if( - (self->client->weapon == FindItem(KNIFE_NAME)) - || !bHasWeapon // kick attack - ) - { - // Don't walk off the edge - if( ACEMV_CanMove( self, MOVE_FORWARD)) - { - ucmd->forwardmove += SPEED_RUN; - } - else - { - // Can't get there! - ucmd->forwardmove = -SPEED_WALK; - self->enemy=NULL; - self->state = STATE_WANDER; - return; - } - - // Raptor007: Attempt longer knife throws when carrying many knives. - self->client->pers.knife_mode = 0; - if( (dist < 2000) && (dist < 400 + 300 * INV_AMMO(self,KNIFE_NUM)) ) - { - float kick_dist = 200; - - if( bHasWeapon ) - { - self->client->pers.knife_mode = 1; - kick_dist = 100; - } - - if( dist < kick_dist ) - { - if( dist < 64 ) // Too close - ucmd->forwardmove = -SPEED_WALK; - // Kick Attack needed! - ucmd->upmove = SPEED_RUN; - } - } - } - else //if(!(self->client->weapon == FindItem(SNIPER_NAME))) // Stop moving with sniper rifle - { - // Randomly choose a movement direction - c = random(); - if(c < 0.2 && ACEMV_CanMove(self,MOVE_LEFT)) - ucmd->sidemove -= SPEED_RUN; - else if(c < 0.4 && ACEMV_CanMove(self,MOVE_RIGHT)) - ucmd->sidemove += SPEED_RUN; - - c = random(); - if(c < 0.6 && ACEMV_CanMove(self,MOVE_FORWARD)) - ucmd->forwardmove += SPEED_RUN; - else if(c < 0.8 && ACEMV_CanMove(self,MOVE_BACK)) - ucmd->forwardmove -= SPEED_RUN; - } -//AQ2 END - if( (dist < 600) && - ( !(self->client->weapon == FindItem(KNIFE_NAME)) -)// && !(self->client->weapon == FindItem(SNIPER_NAME))) // Stop jumping with sniper rifle - &&( bHasWeapon ) // Jump already set for kick attack - ) - { - // Randomly choose a vertical movement direction - c = random(); - - if( (c < 0.10) && FRAMESYNC ) //Werewolf: was 0.15 - { - if (ACEMV_CanJump(self)) - ucmd->upmove += SPEED_RUN; - } - else if( c> 0.85 ) // Only crouch sometimes - ucmd->upmove -= SPEED_WALK; - } - - } //The rest applies even for fleeing bots - - // Werewolf: Crouch if no laser light - if( (ltk_skill->value >= 3) - && self->groundentity - && ! INV_AMMO( self, LASER_NUM ) - && ( (self->client->m4_rds && (self->client->weapon == FindItem(M4_NAME))) - || (self->client->mp5_rds && (self->client->weapon == FindItem(MP5_NAME))) - || (self->client->mk23_rds && (self->client->weapon == FindItem(MK23_NAME))) - || (self->client->dual_rds && (self->client->weapon == FindItem(DUAL_NAME))) ) ) - { - // Raptor007: Don't crouch if it blocks the shot. - float old_z = self->s.origin[2]; - self->s.origin[2] -= 14; - if( ACEAI_CheckShot(self) ) - ucmd->upmove = -SPEED_RUN; - self->s.origin[2] = old_z; - } - - // Set the attack - //@@ Check this doesn't break grenades! - //Werewolf: I've checked that. Now it doesn't anymore. Behold my power! Muhahahahaha!!! - if((self->client->weaponstate == WEAPON_READY)||(self->client->weaponstate == WEAPON_FIRING)) - { - // Only shoot if the weapon is ready and we can hit the target! - //@@ Removed to try to help RobbieBoy! - //Reenabled by Werewolf - if( ACEAI_CheckShot( self )) - { - if( (ltk_skill->value >= 0) - && (self->react >= 1.f / (ltk_skill->value + 2.f)) // Reaction time. - && enemy_front - && (FRAMESYNC || (self->client->weaponstate != WEAPON_READY)) // Sync firing with random offsets. - && (self->teamPauseTime == level.framenum - 1) ) // Saw them last frame too. - { - ucmd->buttons = BUTTON_ATTACK; - - if(self->client->weapon == FindItem(GRENADE_NAME)) - { - self->grenadewait = level.framenum + 1.5 * HZ; - ucmd->forwardmove = -SPEED_RUN; //Stalk back, behold of the holy Grenade! - } - else - self->grenadewait = 0; - } - } - else if (self->state != STATE_FLEE) - { - if (ucmd->upmove == -SPEED_RUN) - ucmd->upmove = 0; - if (ltk_jumpy->value) - { - c = random(); - if(c < 0.50) //Only jump at 50% probability - { - if (ACEMV_CanJump(self)) - ucmd->upmove = SPEED_RUN; - } - else if (c < 0.75 && ACEMV_CanMove(self,MOVE_LEFT)) - ucmd->sidemove -= SPEED_WALK; - else if (ACEMV_CanMove(self,MOVE_RIGHT)) - ucmd->sidemove += SPEED_WALK; - } - } - } - - // Raptor007: Don't immediately shoot friendlies after round. - if( (team_round_countdown > 40) && ! self->grenadewait ) - { - if( self->client->weapon == FindItem(KNIFE_NAME) ) - self->client->pers.knife_mode = 1; // Throwing knives are honorable. - else if( self->client->weapon != FindItem(HC_NAME) ) // Handcannon is allowed. - ucmd->buttons &= ~BUTTON_ATTACK; - - if( dist < 128 ) - { - if( self->groundentity && (self->s.origin <= self->enemy->s.origin) ) - ucmd->upmove = SPEED_RUN; // Kicking is the most honorable form of combat. - } - else - { - ucmd->upmove = 0; - if( ACEMV_CanMove( self, MOVE_FORWARD ) ) - ucmd->forwardmove = SPEED_RUN; // We need to get closer to kick. - } - } - - // Aim - VectorCopy(self->enemy->s.origin,target); - - // Werewolf: Aim higher if using grenades - if(self->client->weapon == FindItem(GRENADE_NAME)) - target[2] += 35; - - //AQ2 ADD - RiEvEr - // Alter aiming based on skill level - // Modified by Werewolf and Raptor007 - if( - ( (ltk_skill->value >= 0) && (ltk_skill->value < 10) ) - && ( bHasWeapon ) // Kick attacks must be accurate - && (!(self->client->weapon == FindItem(KNIFE_NAME))) // Knives accurate - && (!(self->client->weapon == FindItem(GRENADE_NAME))) // Grenades accurate - ) - { - short int sign[3], iFactor = 7; - sign[0] = (random() < 0.5) ? -1 : 1; - sign[1] = (random() < 0.5) ? -1 : 1; - sign[2] = (random() < 0.5) ? -1 : 1; - - // Not that complex. We miss by 0 to 80 units based on skill value and random factor - // Unless we have a sniper rifle! - if(self->client->weapon == FindItem(SNIPER_NAME)) - iFactor = 5; - - // Shoot less accurately if we just got hit. - if( self->client->push_timeout > 45 ) - iFactor += self->client->push_timeout - 45; - - // Shoot more accurately after holding aim, but less accurately when first aiming. - if( self->react >= 4.f ) - iFactor --; - else if( (self->react < 0.5f) && (dist > 300.f) ) - iFactor ++; - - // Shoot less accurately at moving targets. - if( VectorLength(self->enemy->velocity) > 300.f ) - iFactor ++; - - target[0] += sign[0] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; - target[1] += sign[1] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; - target[2] += sign[2] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )); - } - //Werewolf: Snipers of skill 10 are complete lethal, so I don't use that code down there -/* else if (ltk_skill->value == 11) - if(self->client->weapon == FindItem(SNIPER_NAME)) - { - short int up, right, iFactor=1; - up = (random() < 0.5)? -1 :1; - right = (random() < 0.5)? -1 : 1; - target[0] += ( right * (10 - 5 +((iFactor*(10 - 5)) *random())) ); - target[2] += ( up * (10 - 5 +((iFactor*(10 - 5)) *random())) ); - } -*/ -//AQ2 END - - // Werewolf: release trigger after 1 second for grenades - if( (self->grenadewait == level.framenum + 1 * HZ) && (self->solid != SOLID_NOT) ) - { - ucmd->buttons = 0; - } - - //Werewolf: Wait for grenade to launch before facing elsewhere - if( level.framenum >= self->grenadewait ) - { - self->grenadewait = 0; - - // Raptor007: Interpolate angle changes, and set new desired direction at 10fps. - // Make sure imperfect angles are calculated when first spotting an enemy, too. - ACEMV_ChangeBotAngle( self ); - if( FRAMESYNC || ! enemy_front || (level.framenum - self->teamPauseTime >= FRAMEDIV) ) - VectorSubtract( target, self->s.origin, self->move_vector ); - } - - // Store time we last saw an enemy - // This value is used to decide if we initiate a long range search or not. - self->teamPauseTime = level.framenum; - -// if(debug_mode) -// debug_printf("%s attacking %s\n",self->client->pers.netname,self->enemy->client->pers.netname); -} - -//========================== -// AntPathMove -//========================== -// -qboolean AntPathMove( edict_t *self ) -{ - //node_t *temp = &nodes[self->current_node]; // For checking our position - - //if( level.time == (float)((int)level.time) ) - if( level.framenum % HZ == 0 ) - { - if( - !AntLinkExists( self->current_node, SLLfront(&self->pathList) ) - && ( self->current_node != SLLfront(&self->pathList) ) - ) - { - // We are off the path - clear out the lists - AntInitSearch( self ); - } - } - - // Boot in our new pathing algorithm - // This will fill ai.pathList with the information we need - if( - SLLempty(&self->pathList) // We have no path and - && (self->current_node != self->goal_node) // we're not at our destination - ) - { - if( !AntStartSearch( self, self->current_node, self->goal_node)) // Set up our pathList - { - // Failed to find a path -// gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path \n", -// self->client->pers.netname, self->goal_node, self->next_node); - return false; - } - return true; - } - - // And change our pathfinding to use it - if ( !SLLempty(&self->pathList) ) // We have a path - { -// if( AtNextNode( self->s.origin, SLLfront(&self->pathList)) // we're at the nextnode - if( ACEND_FindClosestReachableNode(self,NODE_DENSITY*0.66,NODE_ALL) == SLLfront(&self->pathList) // we're at the nextnode - || self->current_node == SLLfront(&self->pathList) - ) - { - self->current_node = SLLfront(&self->pathList); // Show we're there - SLLpop_front(&self->pathList); // Remove the top node - } - if( !SLLempty(&self->pathList) ) - self->next_node = SLLfront(&self->pathList); // Set our next destination - else - self->next_node = INVALID; // We're at the target location - return true; - } - return true; // Pathlist is emptyand we are at our destination -} - +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot_movement.c 7 29/02/00 22:58 Riever $ + * + * $Log: /LTK2/src/acesrc/acebot_movement.c $ + * + * 7 29/02/00 22:58 Riever + * Corner avoidance code added. + * + * 6 29/02/00 11:18 Riever + * Attack function changes: + * Sniper accuracy increased + * JumpKick attack added + * Knife throw added + * Safety check before advancing for knife or kick attack + * Jump Changed to use a velocity hack. + * Jump node support fixed and working well. + * Ledge handling code removed - no longer used. + * + * 5 24/02/00 20:02 Riever + * New door handling code in place. + * + * User: Riever Date: 23/02/00 Time: 23:18 + * New door handling code in place - must have route file. + * User: Riever Date: 21/02/00 Time: 15:16 + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * User: Riever Date: 20/02/00 Time: 20:27 + * Added new members and definitions ready for 2nd generation of bots. + * + */ + +/////////////////////////////////////////////////////////////////////// +// acebot_movement.c - This file contains all of the +// movement routines for the ACE bot +// +/////////////////////////////////////////////////////////////////////// + +#include "../g_local.h" + +#include "acebot.h" +#include "botchat.h" + +qboolean ACEAI_CheckShot(edict_t *self); +void ACEMV_ChangeBotAngle (edict_t *ent); +//qboolean ACEND_LadderForward( edict_t *self ); +qboolean ACEMV_CanJump(edict_t *self); + +/* +============= +M_CheckBottom + +Returns false if any part of the bottom of the entity is off an edge that +is not a staircase. + +============= +*/ +int c_yes = 0, c_no = 0; + +qboolean M_CheckBottom( edict_t *ent ) +{ + vec3_t mins = {0,0,0}, maxs = {0,0,0}, start = {0,0,0}, stop = {0,0,0}; + trace_t trace; + int x = 0, y = 0; + float mid = 0, bottom = 0; + + VectorAdd( ent->s.origin, ent->mins, mins ); + VectorAdd( ent->s.origin, ent->maxs, maxs ); + + // if all of the points under the corners are solid world, don't bother + // with the tougher checks + // the corners must be within 16 of the midpoint + start[2] = mins[2] - 1; + for( x = 0; x <= 1; x ++ ) + for( y = 0; y <= 1; y ++ ) + { + start[0] = x ? maxs[0] : mins[0]; + start[1] = y ? maxs[1] : mins[1]; + if( gi.pointcontents(start) != CONTENTS_SOLID ) + goto realcheck; + } + + c_yes ++; + return true; // we got out easy + +realcheck: + c_no ++; + // + // check it for real... + // + start[2] = mins[2]; + + // the midpoint must be within 16 of the bottom + start[0] = stop[0] = (mins[0] + maxs[0]) * 0.5; + start[1] = stop[1] = (mins[1] + maxs[1]) * 0.5; + stop[2] = start[2] - 2 * STEPSIZE; + trace = gi.trace( start, vec3_origin, vec3_origin, stop, ent, MASK_MONSTERSOLID ); + + if( trace.fraction == 1.0 ) + return false; + mid = bottom = trace.endpos[2]; + + // the corners must be within 16 of the midpoint + for( x = 0; x <= 1; x ++ ) + for( y = 0; y <= 1; y ++ ) + { + start[0] = stop[0] = x ? maxs[0] : mins[0]; + start[1] = stop[1] = y ? maxs[1] : mins[1]; + + trace = gi.trace( start, vec3_origin, vec3_origin, stop, ent, MASK_MONSTERSOLID ); + + if( trace.fraction != 1.0 && trace.endpos[2] > bottom ) + bottom = trace.endpos[2]; + if( trace.fraction == 1.0 || mid - trace.endpos[2] > STEPSIZE ) + return false; + } + + c_yes ++; + return true; +} + +//============================================================= +// CanMoveSafely (dronebot) +//============================================================= +// Checks for lava and slime +#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) +//#define TRACE_DIST_SHORT 48 +//#define TRACE_DOWN 96 +//#define VEC_ORIGIN tv(0,0,0) +// + +qboolean CanMoveSafely(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest1, dest2; + trace_t trace; + //float this_dist; + +// self->bot_ai.next_safety_time = level.time + EYES_FREQ; + + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + // create a position in front of the bot + VectorMA(self->s.origin, TRACE_DIST_SHORT, dir, dest1); + + // Modified to check for crawl direction + //trace = gi.trace(self->s.origin, mins, self->maxs, dest, self, MASK_SOLID); + //BOTUT_TempLaser (self->s.origin, dest1); + trace = gi.trace(self->s.origin, tv(-16,-16,0), tv(16,16,0), dest1, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (true); + + if (trace.fraction > 0) + { // check that destination is onground, or not above lava/slime + dest1[0] = trace.endpos[0]; + dest1[1] = trace.endpos[1]; + dest1[2] = trace.endpos[2] - 28; + //this_dist = trace.fraction * TRACE_DIST_SHORT; + + if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) + return (true); + + + // create a position a distance below it + VectorCopy( trace.endpos, dest2); + dest2[2] -= TRACE_DOWN + max(lights_camera_action, self->client->uvTime) * 32; + //BOTUT_TempLaser (trace.endpos, dest2); + trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); + + if ((trace.fraction == 1.0) // long drop! + || (trace.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (trace.ent && (trace.ent->touch == hurt_touch)) // avoid MOD_TRIGGER_HURT + || (trace.surface && (trace.surface->flags & SURF_SKY))) // avoid falling onto skybox + { + return (false); + } + else + { + return (true); + } + } + //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); + return (false); +} +/////////////////////////////////////////////////////////////////////// +// Checks if bot can move (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +qboolean ACEMV_CanMove(edict_t *self, int direction) +{ + vec3_t forward, right; + vec3_t offset,start,end; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(self->s.angles,angles); + + if (direction == MOVE_LEFT) + angles[1] += 90; + else if (direction == MOVE_RIGHT) + angles[1] -= 90; + else if (direction == MOVE_BACK) + angles[1] -= 180; + + // Set up the vectors + AngleVectors (angles, forward, right, NULL); + + VectorSet(offset, 36, 0, 24); + G_ProjectSource (self->s.origin, offset, forward, right, start); + + VectorSet(offset, 36, 0, -110); // RiEvEr reduced drop distance + G_ProjectSource (self->s.origin, offset, forward, right, end); + + //AQ2 ADDED MASK_SOLID + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); + + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + if (self->last_door_time < level.framenum) + { + if (debug_mode && (level.framenum % HZ == 0)) + debug_printf("%s: move blocked (ACEMV_CanMove)\n", self->client->pers.netname); + + Cmd_OpenDoor_f(self); // Open the door + //self->last_door_time = level.framenum + 3 * HZ; // wait! + } + return false; + } + + return true; // yup, can move +} + +/////////////////////////////////////////////////////////////////////// +// Checks if bot can jump (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +qboolean ACEMV_CanJumpInternal(edict_t *self, int direction) +{ + vec3_t forward, right; + vec3_t offset,start,end; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(self->s.angles,angles); + + if(direction == MOVE_LEFT) + angles[1] += 90; + else if(direction == MOVE_RIGHT) + angles[1] -= 90; + else if(direction == MOVE_BACK) + angles[1] -=180; + + // Set up the vectors + AngleVectors (angles, forward, right, NULL); + + VectorSet(offset, 132, 0, 24); + G_ProjectSource (self->s.origin, offset, forward, right, start); + + VectorSet(offset, 132, 0, -110); // RiEvEr reduced drop distance + G_ProjectSource (self->s.origin, offset, forward, right, end); + + //AQ2 ADDED MASK_SOLID + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID|MASK_OPAQUE); + + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + if (self->last_door_time < level.framenum) + { + if (debug_mode && (level.framenum % HZ == 0)) + debug_printf("%s: move blocked (ACEMV_CanJumpInternal)\n", self->client->pers.netname); + + Cmd_OpenDoor_f(self); // Open the door + //self->last_door_time = level.framenum + 3 * HZ; // wait! + } + return false; + } + + return true; // yup, can move +} + +qboolean ACEMV_CanJump(edict_t *self) +{ + return( (ACEMV_CanJumpInternal(self, MOVE_LEFT)) && + (ACEMV_CanJumpInternal(self, MOVE_RIGHT)) && + (ACEMV_CanJumpInternal(self, MOVE_BACK)) && + (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) ); + +} + +/////////////////////////////////////////////////////////////////////// +// Handle special cases of crouch/jump +// +// If the move is resolved here, this function returns +// true. +/////////////////////////////////////////////////////////////////////// +qboolean ACEMV_SpecialMove(edict_t *self, usercmd_t *ucmd) +{ + vec3_t dir,forward,right,start,end,offset; + vec3_t top; + trace_t tr; + + // Get current direction + VectorCopy(self->client->ps.viewangles,dir); + dir[YAW] = self->s.angles[YAW]; + AngleVectors (dir, forward, right, NULL); + + VectorSet(offset, 0, 0, 0); // changed from 18,0,0 + G_ProjectSource (self->s.origin, offset, forward, right, start); + offset[0] += 18; + G_ProjectSource (self->s.origin, offset, forward, right, end); + + // trace it + start[2] += 18; // so they are not jumping all the time + end[2] += 18; + tr = gi.trace (start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + +//RiEvEr if(tr.allsolid) + if( tr.fraction < 1.0) + { + //rekkie -- DEV_1 -- s + // Handles cases where bots get stuck on each other, for example in teamplay where they can block each other + // So lets try to make them jump over each other + if (tr.ent && tr.ent->is_bot && self->groundentity) + { + if (random() > 0.5) // try jumping + { + //if (debug_mode && level.framenum % HZ == 0) + //debug_printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); + self->velocity[2] += 400; + //ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return false; + } + else // try crouching + { + //if (debug_mode && level.framenum % HZ == 0) + //debug_printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); + ucmd->upmove = -SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return false; + } + } + //rekkie -- DEV_1 -- e + + //RiEvEr + if (tr.ent && Q_stricmp(tr.ent->classname, "func_door_rotating") == 0) + { + ucmd->forwardmove = -SPEED_RUN; // back up in case it's trying to open + return true; + } + //R + + // Check for crouching + start[2] -= 14; + end[2] -= 14; + + // Set up for crouching check + VectorCopy(self->maxs,top); + top[2] = 0.0; // crouching height + tr = gi.trace (start, self->mins, top, end, self, MASK_PLAYERSOLID); + + // Crouch +//RiEvEr if(!tr.allsolid) + if( tr.fraction == 1.0 ) + { + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = -SPEED_RUN; + return true; + } + + // Check for jump + start[2] += 32; + end[2] += 32; + tr = gi.trace (start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + +//RiEvEr if(!tr.allsolid) + if( tr.fraction == 1.0) + { + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + return true; + } + //RiEvEr + // My corner management code + if( BOTCOL_CheckBump(self, ucmd )) + { + if( BOTCOL_CanStrafeSafely(self, self->s.angles ) ) + { + ucmd->sidemove = self->bot_strafe; + return true; + } + } + //R + } + + return false; // We did not resolve a move here +} + + +/////////////////////////////////////////////////////////////////////// +// Checks for obstructions in front of bot +// +// This is a function I created origianlly for ACE that +// tries to help steer the bot around obstructions. +// +// If the move is resolved here, this function returns true. +/////////////////////////////////////////////////////////////////////// +qboolean ACEMV_CheckEyes(edict_t *self, usercmd_t *ucmd) +{ + vec3_t forward, right; + vec3_t leftstart, rightstart,focalpoint; + vec3_t /* upstart,*/upend; + vec3_t dir,offset; + + trace_t traceRight,traceLeft,/*traceUp,*/ traceFront; // for eyesight + + // Get current angle and set up "eyes" + VectorCopy(self->s.angles,dir); + AngleVectors (dir, forward, right, NULL); + + // Let them move to targets by walls + if(!self->movetarget) +// VectorSet(offset,200,0,4); // focalpoint + VectorSet(offset,64,0,4); // focalpoint + else + VectorSet(offset,36,0,4); // focalpoint + + G_ProjectSource (self->s.origin, offset, forward, right, focalpoint); + + // Check from self to focalpoint + // Ladder code + VectorSet(offset,36,0,0); // set as high as possible + G_ProjectSource (self->s.origin, offset, forward, right, upend); + //AQ2 ADDED MASK_SOLID + traceFront = gi.trace(self->s.origin, self->mins, self->maxs, upend, self, MASK_SOLID|MASK_OPAQUE); + + if(traceFront.contents & 0x8000000) // using detail brush here cuz sometimes it does not pick up ladders...?? + { + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return true; + } + + // If this check fails we need to continue on with more detailed checks + if(traceFront.fraction == 1) + { + ucmd->forwardmove = SPEED_RUN; + return true; + } + + VectorSet(offset, 0, 18, 4); + G_ProjectSource (self->s.origin, offset, forward, right, leftstart); + + offset[1] -= 36; // want to make sure this is correct + //VectorSet(offset, 0, -18, 4); + G_ProjectSource (self->s.origin, offset, forward, right, rightstart); + + traceRight = gi.trace(rightstart, NULL, NULL, focalpoint, self, MASK_OPAQUE); + traceLeft = gi.trace(leftstart, NULL, NULL, focalpoint, self, MASK_OPAQUE); + + // Wall checking code, this will degenerate progressivly so the least cost + // check will be done first. + + // If open space move ok + if(traceRight.fraction != 1 || traceLeft.fraction != 1 || strcmp(traceLeft.ent->classname,"func_door")!=0) + { +//@@ weird code... +/* // Special uppoint logic to check for slopes/stairs/jumping etc. + VectorSet(offset, 0, 18, 24); + G_ProjectSource (self->s.origin, offset, forward, right, upstart); + + VectorSet(offset,0,0,200); // scan for height above head + G_ProjectSource (self->s.origin, offset, forward, right, upend); + traceUp = gi.trace(upstart, NULL, NULL, upend, self, MASK_OPAQUE); + + VectorSet(offset,200,0,200*traceUp.fraction-5); // set as high as possible + G_ProjectSource (self->s.origin, offset, forward, right, upend); + traceUp = gi.trace(upstart, NULL, NULL, upend, self, MASK_OPAQUE); + + // If the upper trace is not open, we need to turn. + if(traceUp.fraction != 1)*/ + { + if(traceRight.fraction > traceLeft.fraction) + self->s.angles[YAW] += (1.0 - traceLeft.fraction) * 45.0; + else + self->s.angles[YAW] += -(1.0 - traceRight.fraction) * 45.0; + + ucmd->forwardmove = SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return true; + } + } + + return false; +} + +/////////////////////////////////////////////////////////////////////// +// Make the change in angles a little more gradual, not so snappy +// Subtle, but noticeable. +// +// Modified from the original id ChangeYaw code... +/////////////////////////////////////////////////////////////////////// +void ACEMV_ChangeBotAngle (edict_t *ent) +{ + float ideal_yaw; + float ideal_pitch; + float current_yaw; + float current_pitch; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float pitch_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(ent->move_vector); + + current_yaw = anglemod(ent->s.angles[YAW]); + current_pitch = anglemod(ent->s.angles[PITCH]); + + vectoangles (ent->move_vector, ideal_angle); + + ideal_yaw = anglemod(ideal_angle[YAW]); + ideal_pitch = anglemod(ideal_angle[PITCH]); + + // Raptor007: Compensate for M4 climb. + if( (ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME)) ) + ideal_pitch -= ent->client->kick_angles[PITCH]; + + // Yaw + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float) BASE_FRAMERATE; + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + + // Pitch + if (current_pitch != ideal_pitch) + { + pitch_move = ideal_pitch - current_pitch; + speed = ent->yaw_speed / (float) BASE_FRAMERATE; + if (ideal_pitch > current_pitch) + { + if (pitch_move >= 180) + pitch_move = pitch_move - 360; + } + else + { + if (pitch_move <= -180) + pitch_move = pitch_move + 360; + } + if (pitch_move > 0) + { + if (pitch_move > speed) + pitch_move = speed; + } + else + { + if (pitch_move < -speed) + pitch_move = -speed; + } + } + + // Raptor007: Interpolate towards desired changes at higher fps. + if( ! FRAMESYNC ) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float) frames_until_sync; + } + + ent->s.angles[YAW] = anglemod( current_yaw + yaw_move * move_ratio ); + ent->s.angles[PITCH] = anglemod( current_pitch + pitch_move * move_ratio ); +} + +/* +/////////////////////////////////////////////////////////////////////// +// Set bot to move to it's movetarget. (following node path) +/////////////////////////////////////////////////////////////////////// +void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) +{ + // If a rocket or grenade is around deal with it + // Simple, but effective (could be rewritten to be more accurate) + if(strcmp(self->movetarget->classname,"rocket")==0 || + strcmp(self->movetarget->classname,"grenade")==0) + { + VectorSubtract (self->movetarget->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + if(debug_mode) + debug_printf("%s: Oh crap a rocket!\n",self->client->pers.netname); + + // strafe left/right + if( (self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT) ) + ucmd->sidemove = -SPEED_RUN; + else if(ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else + ucmd->sidemove = 0; + self->bot_strafe = ucmd->sidemove; + return; + + } + else + { + if (self->current_node != INVALID && self->bot.next_node != INVALID) // rekkie -- safey check because current / next node can be INVALID + { + // Are we there yet? + vec3_t v; + trace_t tTrace; + vec3_t vStart, vDest; + + if( nodes[self->bot.current_node].type == NODE_DOOR ) + { + if( nodes[self->bot.next_node].type == NODE_DOOR ) + { + VectorCopy( nodes[self->current_node].origin, vStart ); + VectorCopy( nodes[self->next_node].origin, vDest ); + VectorSubtract( self->s.origin, nodes[self->bot.next_node].origin, v ); + } + else + { + VectorCopy( self->s.origin, vStart ); + VectorCopy( nodes[self->current_node].origin, vDest ); + VectorSubtract( self->s.origin, nodes[self->current_node].origin, v ); + } + + if(VectorLength(v) < 32) + { + // See if we have a clear shot at it + PRETRACE(); + tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID); + POSTTRACE(); + if( tTrace.fraction <1.0 ) + { + if( (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) + || (strcmp( tTrace.ent->classname, "func_door") == 0) ) + { + // The door is in the way + // See if it's moving + if( (VectorLength(tTrace.ent->avelocity) > 0.0) + || ((tTrace.ent->moveinfo.state != STATE_BOTTOM) && (tTrace.ent->moveinfo.state != STATE_TOP)) ) + { + if( self->last_door_time < level.framenum ) + self->last_door_time = level.framenum; + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_WALK; + return; + } + else + { + // Open it + if( (self->last_door_time < level.framenum - 2.0 * HZ) && + (tTrace.ent->moveinfo.state != STATE_TOP) && + (tTrace.ent->moveinfo.state != STATE_UP) ) + { + Cmd_OpenDoor_f ( self ); // Open the door + self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! + ucmd->forwardmove = 0; + VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); + ACEMV_ChangeBotAngle( self ); + return; + } + } + } + else if( tTrace.ent->client ) + { + // Back up slowly - may have a teammate in the way + if( self->last_door_time < level.framenum ) + self->last_door_time = level.framenum; + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_WALK; + return; + } + } + else + { + // If trace from bot to next node hits rotating door, it should just strafe toward the path. + VectorCopy( self->s.origin, vStart ); + VectorCopy( nodes[self->bot.next_node].origin, vDest ); + VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); + if( VectorLength(v) < 32 ) + { + PRETRACE(); + tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID ); + POSTTRACE(); + if( (tTrace.fraction < 1.) + && (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) + && (VectorLength(tTrace.ent->avelocity) > 0.) ) + { + if( self->next_node != self->current_node ) + { + // Decide if the bot should strafe to stay on course. + vec3_t dist = {0,0,0}; + VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); + v[2] = 0; + VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); + dist[2] = 0; + if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) + { + float left = 0; + VectorNormalize( v ); + VectorRotate2( v, 90 ); + left = DotProduct( v, dist ); + if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) + ucmd->sidemove = SPEED_RUN; + else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) + ucmd->sidemove = -SPEED_RUN; + } + } + else + { + ucmd->sidemove = 0; + if( (self->bot_strafe >= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) + ucmd->sidemove = SPEED_RUN; + else if( ACEMV_CanMove( self, MOVE_LEFT ) ) + ucmd->sidemove = -SPEED_RUN; + self->bot_strafe = ucmd->sidemove; + } + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_RUN; + VectorSubtract( self->movetarget->s.origin, self->s.origin, self->move_vector ); + ACEMV_ChangeBotAngle( self ); + return; + } + } + } + } + } + } + + // Set bot's movement direction + VectorSubtract (self->movetarget->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + ucmd->forwardmove = SPEED_RUN; + return; + } +} +*/ + +#if 0 +/////////////////////////////////////////////////////////////////////// +// Main movement code. (following node path) +/////////////////////////////////////////////////////////////////////// +void ACEMV_Move(edict_t *self, usercmd_t *ucmd) +{ + trace_t tr; + float distance = 0, distance_xy; // Distance XYZ and Distance XY + vec3_t dist = { 0 }, dist_xy = { 0 }; // Distance XYZ and Distance XY + vec3_t velocity; + int i = 0, current_node_type = INVALID, next_node_type = INVALID; + + // Do not follow path when teammates are still inside us. + if( OnTransparentList(self) ) + { + //rekkie -- DEV_1 -- s + + // If the bot has just spawned, then we need to wait a bit before we start moving. + if (self->just_spawned) // set by SpawnPlayers() in a_team.c + { + self->just_spawned = false; + self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. + self->state = STATE_MOVE; + if (random() < 0.3) + self->just_spawned_timeout = level.framenum + (random() * 7) * HZ; // Long wait + else if (random() < 0.7) + self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Average wait + else + self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait + return; + } + if (self->just_spawned_go && self->just_spawned_timeout > level.framenum) // Wait + { + return; // It's not time to move yet, wait! + } + else if (self->just_spawned_go) + { + // Now we can move! + self->just_spawned_go = false; + //ACEAI_PickLongRangeGoal(self); // Lets pick a long range goal + return; + } + if (0) // Don't wander + { + //rekkie -- DEV_1 -- e + + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + + //rekkie -- DEV_1 -- s + } + //rekkie -- DEV_1 -- e + } + + // Get current and next node back from nav code. + if(!BOTLIB_FollowPath(self)) + { + if(!teamplay->value || (teamplay->value && level.framenum >= self->teamPauseTime)) + { + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + else + { + // Teamplay mode - just fan out and chill + if (self->state == STATE_FLEE) + { + self->state = STATE_POSITION; + self->wander_timeout = level.framenum + 3.0 * HZ; + } + else + { + self->state = STATE_POSITION; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + } + self->goal_node = INVALID; + return; + } + + //rekkie -- DEV_1 -- s + if (self->current_node == INVALID || self->next_node == INVALID) // rekkie -- safey check because current / next node can be INVALID + { + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + //rekkie -- DEV_1 -- e + + current_node_type = nodes[self->current_node].type; + + //rekkie -- DEV_1 -- s + // + // original code + // next_node_type = nodes[self->next_node].type; + // + // modified code + // We now get the next node type from the link between current and next node + // Using the link from the current node to the next node, find the next node type + int targetNodeLink = INVALID; + //if (current_node_type == NODE_LADDER_UP && nodes[self->next_node].type == NODE_LADDER_DOWN) + if (nodes[self->next_node].type == NODE_LADDER_UP || nodes[self->next_node].type == NODE_LADDER_DOWN) + { + next_node_type = nodes[self->next_node].type; + //PrintNodeType(current_node_type, next_node_type); + } + else + for (i = 0; i < MAXLINKS; i++) + { + if (nodes[self->current_node].links[i].targetNode == self->next_node) + { + targetNodeLink = i; + next_node_type = nodes[self->current_node].links[i].targetNodeType; // Next node type + PrintNodeType(current_node_type, next_node_type); + break; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //float speed = VectorLength(nodes[self->current_node].links[i].targetVelocity); + /* + vec3_t dist, velocity; + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //dist[2] = 0; // Lose the vertical component + float distance = VectorLength(dist); + float gravity = self->gravity * sv_gravity->value; + ACEMV_ChangeBotAngle(self); + //ucmd->forwardmove = SPEED_RUN; + int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (self->groundentity && curr_node == self->current_node) + { + float jump_height = sqrt(2 * (gravity * distance)); // Calculate the jump height to get to the target + float time = distance / (self->velocity[2] + jump_height / 2.0); // Calculate the time it will take to get to the target + VectorScale(dist, 1.0 / time, velocity); // Calculate the velocity at the end of the jump + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (nodes[self->next_node].origin[2] > self->s.origin[2]) + { + z_height = (nodes[self->next_node].origin[2] - self->s.origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else + { + z_height = (self->s.origin[2] - nodes[self->next_node].origin[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + + // Limit max speed + //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER + { + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, self->velocity); + VectorCopy(velocity, self->client->oldvelocity); + VectorCopy(velocity, self->avelocity); + return; + } + } + if (self->groundentity && curr_node != self->next_node) + { + if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->goal_node); + } + else + { + Com_Printf("%s %s failed to jumppad up, finding new goal\n", __func__, self->client->pers.netname); + self->state = STATE_WANDER; // Aquire a new node + } + return; + //ucmd->forwardmove = SPEED_RUN; + //ucmd->upmove = SPEED_RUN; + } + + // Guide the bot in when close to the target + if (distance <= 48) // || (distance < 96 && (nodes[self->next_node].type == NODE_LADDER_UP || nodes[self->next_node].type == NODE_LADDER_DOWN))) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + } + + return; + */ + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + //if (next_node_type == NODE_LEARN) // && self->groundentity) + //{} + //break; + } + } + //rekkie -- DEV_1 -- e + + //rekkie -- DEV_1 -- s + //if (current_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMPPAD) + // gi.dprintf("%s %s [ct:%i] [nt:%i]\n", __func__, self->client->pers.netname, current_node_type, next_node_type); + + // If the next node is high enough, make it a jump + /* + if (next_node_type == NODE_MOVE && self->client->leg_damage == 0) + { + + // See if there is a gap / hole between the current and next node, if so we need to jump across + vec3_t pt_25, pt_50, pt_75; + VectorAdd(nodes[self->current_node].origin, nodes[self->next_node].origin, pt_25); // 25% + VectorAdd(nodes[self->current_node].origin, nodes[self->next_node].origin, pt_50); // 50% + VectorAdd(nodes[self->current_node].origin, nodes[self->next_node].origin, pt_75); // 75% + VectorScale(pt_25, 0.25, pt_25); // Scale 25% + VectorScale(pt_50, 0.50, pt_50); // Scale 50% + VectorScale(pt_75, 0.75, pt_75); // Scale 75% + // Using the points between the current and next node, trace a line down and see if one of them hits air + trace_t tr_25, tr_50, tr_75; + vec3_t end_25, end_50, end_75; + VectorCopy(pt_25, end_25); + VectorCopy(pt_50, end_50); + VectorCopy(pt_75, end_75); + end_25[2] -= NODE_MAX_JUMP_HEIGHT; + end_50[2] -= NODE_MAX_JUMP_HEIGHT; + end_75[2] -= NODE_MAX_JUMP_HEIGHT; + tr_25 = gi.trace(pt_25, vec3_origin, vec3_origin, end_25, self, MASK_SOLID); + tr_50 = gi.trace(pt_50, vec3_origin, vec3_origin, end_50, self, MASK_SOLID); + tr_75 = gi.trace(pt_75, vec3_origin, vec3_origin, end_75, self, MASK_SOLID); + // If one of our trace points hit nothing + if ( (tr_25.fraction == 1.0 && tr_25.allsolid == false) || + (tr_50.fraction == 1.0 && tr_50.allsolid == false) || + (tr_75.fraction == 1.0 && tr_75.allsolid == false) ) + { + next_node_type = NODE_JUMPPAD; + } + + // Otherwise lets see if the next_node is higher than current_node, if so jump to it + //else + { + // Distance between current and next node. + //VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + + // Calculate if nodes[self->next_node].origin[2] is higher than self->s.origin[2] + float higher = 0; float lower = 0; + if (nodes[self->next_node].origin[2] > nodes[self->current_node].origin[2]) + higher = (nodes[self->next_node].origin[2] - nodes[self->current_node].origin[2]); + else if (nodes[self->next_node].origin[2] < nodes[self->current_node].origin[2]) + lower = (nodes[self->current_node].origin[2] - nodes[self->next_node].origin[2]); + //if (distance >= 128 || higher >= NODE_MAX_JUMP_HEIGHT || lower >= NODE_MAX_JUMP_HEIGHT) + //if (higher >= NODE_MAX_JUMP_HEIGHT) // || lower >= NODE_MAX_CROUCH_FALL_HEIGHT) + if (higher) // || lower) + { + next_node_type = NODE_JUMPPAD; + } + } + } + */ + + if (self->client->leg_damage > 0) // Do we have leg damage? + next_node_type = NODE_MOVE; // Force NODE_MOVE so we can safely stumble to the next target node + + //if ((current_node_type == NODE_LADDER || next_node_type == NODE_LADDER) && OnLadder(self) == false) + // next_node_type = NODE_JUMPPAD; + //rekkie -- DEV_1 -- e + + /////////////////////////// + // Move To Goal + /////////////////////////// + if (self->movetarget) + ACEMV_MoveToGoal(self,ucmd); + + else if( (self->next_node != self->current_node) && (current_node_type != NODE_LADDER) ) + { + // Decide if the bot should strafe to stay on course. + vec3_t v = {0,0,0}; + VectorSubtract( nodes[self->next_node].origin, nodes[self->current_node].origin, v ); + v[2] = 0; + VectorSubtract( self->s.origin, nodes[self->next_node].origin, dist ); + dist[2] = 0; + if( (DotProduct( v, dist ) > 0) && (VectorLength(v) > 16) ) + { + float left = 0; + VectorNormalize( v ); + VectorRotate2( v, 90 ); + left = DotProduct( v, dist ); + if( (left > 16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_RIGHT )) ) + ucmd->sidemove = SPEED_RUN; + else if( (left < -16) && (! self->groundentity || ACEMV_CanMove( self, MOVE_LEFT )) ) + ucmd->sidemove = -SPEED_RUN; + } + } + + if(current_node_type == NODE_GRAPPLE) + { + if( self->last_door_time < level.framenum ) + { + ucmd->forwardmove = SPEED_WALK; //walk forwards a little +// Cmd_OpenDoor_f ( self ); // Open the door + self->last_door_time = level.framenum + random() * 2.0 * HZ; // wait! + } + + } + +/* //////////////////////////////////////////////////////// + // Grapple + /////////////////////////////////////////////////////// + if(next_node_type == NODE_GRAPPLE) + { + ACEMV_ChangeBotAngle(self); + ACEIT_ChangeWeapon(self,FindItem("grapple")); + ucmd->buttons = BUTTON_ATTACK; + return; + } + // Reset the grapple if hangin on a graple node + if(current_node_type == NODE_GRAPPLE) + { + CTFPlayerResetGrapple(self); + return; + }*/ + + //rekkie -- DEV_1 -- s + // Try opening the door 'remotely' before we reach it + if (next_node_type == NODE_DOOR) + { + edict_t* node_ent = node_ents[self->next_node]; + if (node_ent && (strcmp(node_ent->classname, "func_door_rotating") == 0 || strcmp(node_ent->classname, "func_door") == 0)) + { + /* + trace_t tTrace; + vec3_t vStart, vDest; + if (next_node_type == NODE_DOOR) + VectorCopy(nodes[self->current_node].origin, vStart); + else + VectorCopy(self->s.origin, vStart); + VectorCopy(nodes[self->next_node].origin, vDest); + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + dist[2] = 0; + distance = VectorLength(dist); + // See if we have a clear shot at it + PRETRACE(); + tTrace = gi.trace(vStart, tv(-16, -16, -8), tv(16, 16, 8), vDest, self, MASK_PLAYERSOLID); + POSTTRACE(); + if ((tTrace.fraction < 1.0) && (distance < 512)) + { + if (tTrace.ent->client) + { + // Back up slowly - may have a teammate in the way + if (self->last_door_time < level.framenum) + self->last_door_time = level.framenum; + if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_WALK; + else if (self->tries && self->groundentity) + ucmd->upmove = SPEED_RUN; + return; + } + } + +#define DOOR_TOGGLE 32 + if ((node_ent->spawnflags & DOOR_TOGGLE) && (node_ent->moveinfo.state == STATE_UP || node_ent->moveinfo.state == STATE_TOP)) + { + //if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -SPEED_WALK; //walk backwards a little + //else if (self->tries && self->groundentity) + // ucmd->upmove = SPEED_RUN; + //ucmd->forwardmove = SPEED_WALK; + //return; + } + */ + + /* + // The door is in the way + // See if it's moving + if ((VectorLength(node_ent->avelocity) > 0.0) || ((node_ent->moveinfo.state != STATE_BOTTOM) && (node_ent->moveinfo.state != STATE_DOWN))) + { + if (self->last_door_time < level.framenum) + self->last_door_time = level.framenum; + if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_WALK; //walk backwards a little + else if (self->tries && self->groundentity) + ucmd->upmove = SPEED_RUN; + return; + } + else*/ + { + // Open it + if ((self->last_door_time < level.framenum - 2.0 * HZ) && (node_ent->moveinfo.state != STATE_TOP) && (node_ent->moveinfo.state != STATE_UP)) + { + //Cmd_OpenDoor_f(self); // Open the door + door_use(node_ent, self, self); // Open the door + + self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! + if (self->tries && self->groundentity) + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = 0; + return; + } + } + } + } + //rekkie -- DEV_1 -- e + + //////////////////////////////////////////////////////// + // Doors - RiEvEr + // modified by Werewolf + /////////////////////////////////////////////////////// + if(current_node_type == NODE_DOOR) + { + // We are trying to go through a door + trace_t tTrace; + vec3_t vStart, vDest; + + if( next_node_type == NODE_DOOR ) + VectorCopy( nodes[self->current_node].origin, vStart ); + else + VectorCopy( self->s.origin, vStart ); + + VectorCopy( nodes[self->next_node].origin, vDest ); + VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); + dist[2] = 0; + distance = VectorLength(dist); + + // See if we have a clear shot at it + PRETRACE(); + tTrace = gi.trace( vStart, tv(-16,-16,-8), tv(16,16,8), vDest, self, MASK_PLAYERSOLID); + POSTTRACE(); + + if( (tTrace.fraction < 1.0) && (distance < 512) ) + { + if( (strcmp( tTrace.ent->classname, "func_door_rotating" ) == 0) + || (strcmp( tTrace.ent->classname, "func_door") == 0) ) + { + // The door is in the way + // See if it's moving + if( (VectorLength(tTrace.ent->avelocity) > 0.0) + || ((tTrace.ent->moveinfo.state != STATE_BOTTOM) && (tTrace.ent->moveinfo.state != STATE_TOP)) ) + { + if( self->last_door_time < level.framenum ) + self->last_door_time = level.framenum; + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_WALK; //walk backwards a little + else if( self->tries && self->groundentity ) + ucmd->upmove = SPEED_RUN; + return; + } + else + { + // Open it + if( (self->last_door_time < level.framenum - 2.0 * HZ) && + (tTrace.ent->moveinfo.state != STATE_TOP) && + (tTrace.ent->moveinfo.state != STATE_UP) ) + { + Cmd_OpenDoor_f ( self ); // Open the door + self->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! + if( self->tries && self->groundentity ) + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = 0; + return; + } + } + } + else if( tTrace.ent->client ) + { + // Back up slowly - may have a teammate in the way + if( self->last_door_time < level.framenum ) + self->last_door_time = level.framenum; + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_WALK; + else if( self->tries && self->groundentity ) + ucmd->upmove = SPEED_RUN; + return; + } + } + } + + //////////////////////////////////////////////////////// + // Platforms + /////////////////////////////////////////////////////// + if( (next_node_type == NODE_PLATFORM) && ((current_node_type != NODE_PLATFORM) || (self->current_node == self->next_node)) ) + { + // Check if the platform is directly above us. + vec3_t start, above; + trace_t tr; + VectorCopy( self->s.origin, start ); + VectorCopy( self->s.origin, above ); + start[2] += 32; + above[2] += 2048; + tr = gi.trace( start, tv(-24,-24,-8), tv(24,24,8), above, self, MASK_PLAYERSOLID ); + if( (tr.fraction < 1.0) && tr.ent && (tr.ent->use == Use_Plat) ) + { + // If it is directly above us, get out of the way. + ucmd->sidemove = 0; + if( ACEMV_CanMove( self, MOVE_BACK ) ) + ucmd->forwardmove = -SPEED_WALK; + else if( (self->bot_strafe <= 0) && ACEMV_CanMove( self, MOVE_LEFT ) ) + ucmd->sidemove = -SPEED_WALK; + else if( ACEMV_CanMove( self, MOVE_RIGHT ) ) + ucmd->sidemove = SPEED_WALK; + self->bot_strafe = ucmd->sidemove; + ACEMV_ChangeBotAngle(self); + return; + } + } + if(current_node_type != NODE_PLATFORM && next_node_type == NODE_PLATFORM) + { + // check to see if lift is down? + for(i=0;inext_node) + if(item_table[i].ent->moveinfo.state != STATE_BOTTOM) + return; // Wait for elevator + } + if( (current_node_type == NODE_PLATFORM) && self->groundentity + && ((self->groundentity->moveinfo.state == STATE_UP) || (self->groundentity->moveinfo.state == STATE_DOWN)) ) + { + // Standing on moving elevator. + ucmd->forwardmove = 0; + ucmd->sidemove = 0; + ucmd->upmove = 0; + if( VectorLength(self->groundentity->velocity) < 8 ) + ucmd->upmove = SPEED_RUN; + ACEMV_ChangeBotAngle(self); + if( debug_mode && (level.framenum % HZ == 0) ) + debug_printf( "%s: platform move state %i\n", self->client->pers.netname, self->groundentity->moveinfo.state ); + return; + } + if( (current_node_type == NODE_PLATFORM) && (next_node_type == NODE_PLATFORM) && (self->current_node != self->next_node) ) + { + // Move to the center + self->move_vector[2] = 0; // kill z movement + if(VectorLength(self->move_vector) > 10) + ucmd->forwardmove = SPEED_WALK; // walk to center + + ACEMV_ChangeBotAngle(self); + + return; // No move, riding elevator + } + + //rekkie -- DEV_1 -- s + //////////////////////////////////////////////////////// + // Jumpad Nodes + /////////////////////////////////////////////////////// + if (next_node_type == NODE_JUMPPAD) + { + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + // + // + // + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + // ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- TODO ----- + + // Move while jumping + //ucmd->forwardmove = SPEED_RUN; + //ucmd->upmove = SPEED_RUN; + + vec3_t dist; + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //dist[2] = 0; // Lose the vertical component + float next_node_distance = VectorLength(dist); + /* + // If the bot's distance is moving away from the target (failed the jump) + // Try to find another route, otherwise find a new node and go there instead + if (self->prev_next_node == self->next_node) + { + if (next_node_distance < self->next_node_distance) // Update if getting closer + self->next_node_distance = next_node_distance; + else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump + { + self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->goal_node); + } + else + { + Com_Printf("%s %s failed to jumppad up, finding new goal\n", __func__, self->client->pers.netname); + self->state = STATE_WANDER; // Aquire a new node + } + return; + } + } + if (self->prev_next_node != self->next_node) // Update previous next node and distance + { + self->prev_next_node = self->next_node; + self->next_node_distance = next_node_distance; + } + */ + + + + + + float gravity = self->gravity * sv_gravity->value; + + ACEMV_ChangeBotAngle(self); + ucmd->forwardmove = SPEED_RUN; + //if (self->groundentity || next_node_distance < NODE_Z_HEIGHT) + //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + // distance between curr_node and self->current_node + vec3_t curr_node_dist; + VectorSubtract(nodes[self->current_node].origin, self->s.origin, curr_node_dist); + float curr_node_distance = VectorLength(curr_node_dist); + //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance < 24)) // && curr_node == self->current_node)) + if (curr_node_distance <= 32 || (current_node_type == NODE_LADDER_UP && curr_node_distance < 24)) // && curr_node == self->current_node)) + { + // Recalculate the dist with the vertical component included + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + float distance = VectorLength(dist); + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + + // Calculate the time it will take to get to the target + float time = distance / (self->velocity[2] + jump_height / 2.0); + + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (nodes[self->next_node].origin[2] > self->s.origin[2]) + { + z_height = (nodes[self->next_node].origin[2] - self->s.origin[2]) + NODE_Z_HEIGHT * 2; + velocity[2] += z_height; + } + else + { + z_height = (self->s.origin[2] - nodes[self->next_node].origin[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + + // Limit max speed + //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER + { + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, self->velocity); + VectorCopy(velocity, self->client->oldvelocity); + VectorCopy(velocity, self->avelocity); + + //return; + } + } + + // Guide the bot in when close to the target + else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->next_node].type == NODE_LADDER_UP || nodes[self->next_node].type == NODE_LADDER_DOWN))) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + next_node_distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + } + } + + // Bot fell below current and next target + if (self->s.origin[2] + NODE_Z_HEIGHT * 2 < nodes[self->current_node].origin[2]) + { + self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (self->current_node == INVALID) + { + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal + { + if (debug_mode) Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->goal_node); + } + } + + + + return; + + + + + + + // Bot will jump to the jump pad + //trace_t tr; + //VectorCopy(nodes[self->next_node].origin, tr.endpos); + //tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->next_node].origin, self, MASK_SOLID | MASK_OPAQUE); //AQ2 added MASK_SOLID + //if (self->groundentity || current_node_type == NODE_LADDER && tr.fraction == 1.0) + + if (self->groundentity || current_node_type == NODE_LADDER) + { + // Calculate the distance to the target + float gravity = self->gravity * sv_gravity->value; + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + distance = VectorLength(dist); + + //if (self->s.origin[2] <= nodes[self->next_node].origin[2]) + // distance *= 1.5; // Increases the height of the parabola (jump height) - Default is 1.0 + + //distance += ent->viewheight; + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * gravity * distance); + // Calculate the time it will take to get to the target + float time = distance / (self->velocity[2] + jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = (jump_height / 2.0); + + /* + // Get the z height distance between origin and target + vec3_t zdiff, oz, tz; + // Copy origin to oz + VectorCopy(self->s.origin, oz); + // Copy target to tz + VectorCopy(nodes[self->next_node].origin, tz); + // Set X and Y on oz and tz to zero + oz[0] = 0; oz[1] = 0; + tz[0] = 0; tz[1] = 0; + // Get the z height distance between oz and tz + VectorSubtract(oz, tz, zdiff); + // Add the difference to jump velocity + float z_height = 0; + if (self->s.origin[2] <= nodes[self->next_node].origin[2]) + { + z_height = VectorLength(zdiff) + 24;// + 56; + velocity[2] += z_height; + } + else + { + velocity[2] -= VectorLength(zdiff); + } + */ + + // Calculate speed from velocity + float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + // Limit max speed + //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER + { + // Set the velocity of the player + VectorCopy(velocity, self->velocity); + // Don't take falling damage immediately from this + if (self->client) + VectorCopy(self->velocity, self->client->oldvelocity); + return; + } + } + + ucmd->forwardmove = SPEED_RUN; + + // Cannot reach target, pick new goal + //self->state = STATE_WANDER; + return; + + + + + // make sure it is visible + //trace_t tr; + //vec3_t maxs = { 1,1,1 }; + //vec3_t mins = { 1,1,1 }; + //tr = gi.trace(self->s.origin, mins, maxs, nodes[self->next_node].origin, self, MASK_SOLID | MASK_OPAQUE); //AQ2 added MASK_SOLID + //if (tr.fraction < 0.9) + // return; + + + + + + + // Distance between self and current node + //vec3_t dist_curr; + //VectorSubtract(self->s.origin, nodes[self->current_node].origin, dist); + //VectorCopy(dist, dist_curr); + //float distance_to_curr_node = VectorLength(dist_curr); // Get the absolute length + //qboolean curr_node_is_above = false; + //if (dist_curr[2] >= 0) + // curr_node_is_above = true; + + + //vec3_t dist_next; + // Distance between current node and next node + //VectorSubtract(nodes[self->current_node].origin, nodes[self->next_node].origin, dist_next); + //float distance_to_next_node = VectorLength(dist_next); // Get the absolute length + //qboolean next_node_is_above = false; + //if (dist_next[2] >= 0) + // next_node_is_above = true; + + //if (distance_to_curr_node > 128) + // return; + + + if (self->groundentity) + { + + + + ucmd->forwardmove = SPEED_RUN; + + /* + // Jump only when we are moving the correct direction. + vec3_t dist_other; + VectorCopy(self->velocity, velocity); + VectorCopy(dist, dist_other); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_other); + if (DotProduct(velocity, dist_other) < 0.8) + goto NO_JUMPPAD; + */ + + + ucmd->upmove = SPEED_RUN; + + + + self->last_door_time = level.framenum + 0.25 * HZ; // wait! + + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //dist[2] = 0; // Lose the vertical component + distance = VectorLength(dist); // Get the absolute length + + //float multi = 1; + //if (distance > 64) + // multi = (distance / 128); + //multi = (SPEED_RUN * multi) + 56; + + //vec3_t dir, angle; + //AngleVectors(angle, dir, NULL, NULL); + //VectorScale(dir, multi, self->velocity); + + float multi = 1; + //if (distance > 64) + //multi = (distance / 80); // 160 + //multi = (SPEED_RUN * multi); + multi = distance; + vec3_t vDir; + //VectorCopy(bot->s.origin, vStart); + VectorSubtract(nodes[self->next_node].origin, self->s.origin, vDir); + VectorNormalize(vDir); + VectorScale(vDir, multi, self->velocity); + + //debug_printf("NODE_JUMPPAD Velocity UP: %f\n", self->velocity[2]); + + //* + // Clip velocity + float minVel = -401; + float maxVel = 401; + if (self->velocity[0] < minVel) + self->velocity[0] = minVel; + else if (self->velocity[0] > maxVel) + self->velocity[0] = maxVel; + + if (self->velocity[1] < minVel) + self->velocity[1] = minVel; + else if (self->velocity[1] > maxVel) + self->velocity[1] = maxVel; + + if (self->velocity[2] < 0) + self->velocity[2] = 0; + else if (self->velocity[2] > maxVel) + self->velocity[2] = maxVel; + //*/ + + /* + if (0) + { + if (distance_to_curr_node < 24)// && (DotProduct(velocity, dist_other) > 0.8)) // close and facing right dir + { + //if (dist[2] < -32) // Going down + { + //self->velocity[2] = 0; + //debug_printf("NODE_JUMPPAD Velocity DOWN: %f\n", self->velocity[2]); + } + + if (dist[2] > STEPSIZE || next_node_type == NODE_JUMPPAD) // Going up + { + //VectorScale(self->s.angles, multi, self->velocity); + //self->velocity[2] = min(400, self->velocity[2]); + + self->velocity[2] = multi; + //self->velocity[2] = min(SPEED_RUN, self->velocity[2]); + debug_printf("NODE_JUMPPAD Velocity UP: %f dist[2]: %f\n", self->velocity[2], dist[2]); + //ucmd->upmove = SPEED_RUN; + } + else + { + self->velocity[2] = 0; + debug_printf("NODE_JUMPPAD Velocity DOWN: %f\n", self->velocity[2]); + } + } + else + return; + } + */ + + + //vec3_t start, end; + //vec3_t forward, right; + //vec3_t offset; + //AngleVectors(self->s.angles, forward, right, NULL); + //VectorSet(offset, 0, 0, self->viewheight - 8); + //VectorMA(self->s.origin, distance_to_next_node, forward, nodes[self->next_node].origin); + //VectorScale(self->movedir, self->speed * 10, self->velocity); + + /* + // Set up a jump move + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + */ + + ACEMV_ChangeBotAngle(self); + + return; + } + + //return; + + //if (self->speed < 1000) + // self->speed = 1000; + //VectorScale(self->movedir, self->speed * 10, self->velocity); + //return; + + //if (distance_to_next_node < 64 && curr_node_is_above && self->last_door_time < level.framenum) + //if (distance_to_next_node < 64 && self->last_door_time < level.framenum) + //if (distance_to_curr_node < 64 || self->velocity[2] < 0) + //if (distance_to_curr_node < 128 || self->s.origin[2] > nodes[self->next_node].origin[2] || self->velocity[2] < 400) + if ((self->velocity[2] <= -100 || self->velocity[2] >= 400))// && distance_to_curr_node > 32) + { + //self->last_door_time = level.framenum + 0.25 * HZ; // wait! + + // If not moving in midair + if ((self->velocity[0] > -1 && self->velocity[0] < 1) && (self->velocity[1] > -1 && self->velocity[1] < 1)) + return; + + //debug_printf("NODE_JUMPPAD\n"); + + //VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //dist[2] = 0; // Lose the vertical component + distance = VectorLength(dist); // Get the absolute length + + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, SPEED_RUN * distance, self->velocity); + if (dist[2] >= 0) + self->velocity[2] = min(400, self->velocity[2]); + else + self->velocity[2] = max(-800, self->velocity[2]); + + /* + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + + if (DotProduct(velocity, dist) > 0.8) + ucmd->upmove = SPEED_RUN; + + // Set up a jump move + if (distance < 128) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 128); + else + ucmd->forwardmove = SPEED_RUN; + + self->move_vector[2] *= 2; + */ + + ACEMV_ChangeBotAngle(self); + + //self->speed = 1000; + //VectorScale(self->movedir, distance, self->velocity); + + //self->s.angles[YAW] + //velocity = v_forward * dist + v_up * (dist); + + //VectorScale(dist, distance, self->velocity); + + + /* + vec3_t flydir = { 0.f,0.f,0.f }, kvel = { 0.f,0.f,0.f }; + float accel_scale = 100.f; // the rocket jump hack... + VectorNormalize2(self->s.angles, flydir); + flydir[2] += 0.4f; + VectorScale(flydir, accel_scale, kvel); + VectorAdd(self->velocity, kvel, self->velocity); + */ + + /* + // RiEvEr - re-instate the velocity hack + if (self->jumphack_timeout < level.framenum) + { + VectorCopy(self->move_vector,dist); + if ((440 * distance / 128) < 440) + VectorScale(dist,(440 * distance / 128),self->velocity); + else + VectorScale(dist,440,self->velocity); + self->jumphack_timeout = level.framenum + 3.0 * HZ; + } + */ + } + + return; + } + //rekkie -- DEV_1 -- e + + //////////////////////////////////////////////////////// + // Jumpto Nodes + /////////////////////////////////////////////////////// + if(next_node_type == NODE_JUMP) +// || +// ( current_node_type == NODE_JUMP +// && next_node_type != NODE_ITEM && nodes[self->next_node].origin[2] > self->s.origin[2]+16) +// ) + { + VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist); + // Lose the vertical component + dist[2] = 0; + // Get the absolute length + distance = VectorLength(dist); + + //if (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) + { + // Jump only when we are moving the correct direction. + VectorCopy( self->velocity, velocity ); + velocity[2] = 0; + VectorNormalize( velocity ); + VectorNormalize( dist ); + if( DotProduct( velocity, dist ) > 0.8 ) + ucmd->upmove = SPEED_RUN; + + //Kill running movement +// self->move_vector[0]=0; +// self->move_vector[1]=0; +// self->move_vector[2]=0; + // Set up a jump move + if( distance < 256 ) + ucmd->forwardmove = SPEED_RUN * sqrtf( distance / 256 ); + else + ucmd->forwardmove = SPEED_RUN; + + //self->move_vector[2] *= 2; + + ACEMV_ChangeBotAngle(self); + + //rekkie -- DEV_1 -- s + // Guide the bot in when close to the target + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + distance = VectorLength(dist); + if (distance <= 64) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction > 0.7) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + //rekkie -- DEV_1 -- e + + /* + // RiEvEr - re-instate the velocity hack + if (self->jumphack_timeout < level.framenum) + { + VectorCopy(self->move_vector,dist); + if ((440 * distance / 128) < 440) + VectorScale(dist,(440 * distance / 128),self->velocity); + else + VectorScale(dist,440,self->velocity); + self->jumphack_timeout = level.framenum + 3.0 * HZ; + } + */ + } + /* + else + { + self->goal_node = INVALID; + } + */ + return; + } + + //////////////////////////////////////////////////////// + // Ladder Nodes + /////////////////////////////////////////////////////// + + //rekkie -- DEV_1 -- s + if (current_node_type == NODE_LADDER_UP || current_node_type == NODE_LADDER_DOWN || + next_node_type == NODE_LADDER_UP || next_node_type == NODE_LADDER_DOWN) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); // Find the distance vector to the next node + distance = VectorLength(dist); + + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist_xy); // Find the distance vector to the next node + dist_xy[2] = 0; // Lose the vertical component + distance_xy = VectorLength(dist_xy); // Get the absolute length + + float speed = VectorLength(self->velocity); // Speed of the player + + // If we are going down the ladder, check for teammates coming up and yield to them. + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0)) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 200; + self->state = STATE_WANDER; + return; + } + + // If getting off the top of a ladder + if (current_node_type == NODE_LADDER_UP && next_node_type != NODE_LADDER_DOWN) + { + ucmd->forwardmove = SPEED_WALK; + + if ((nodes[self->next_node].origin[2] >= self->s.origin[2]) + && (nodes[self->next_node].origin[2] >= nodes[self->current_node].origin[2]) + && (((self->velocity[2] > 0) && !self->groundentity) || OnLadder(self))) + { + ucmd->upmove = SPEED_RUN; + self->velocity[2] = min(200, distance_xy * 10); // Jump higher for farther node + } + + if (1) + { + // Guide the bot in when close to the target + if (self->s.origin[2] <= nodes[self->next_node].origin[2] + 16 && distance < 64) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + } + } + + ACEMV_ChangeBotAngle(self); + return; + } + + + // If getting off the bottom of a ladder + if (current_node_type == NODE_LADDER_DOWN && next_node_type != NODE_LADDER_UP) + { + // Do nothing here, this is now handled by the next node type + } + + // Getting onto bottom of the ladder + if ( current_node_type != NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN && distance < 192 && nodes[self->next_node].origin[2] > self->s.origin[2]) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + else // Correct direction, jump up + { + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_WALK; + //Com_Printf("%s Excellent. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + + if (1) + { + // Guide the bot in when close to the target + if (self->s.origin[2] < nodes[self->next_node].origin[2] && distance < 64) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + + + if (OnLadder(self) == false) + return; + } + + // Getting onto top of the ladder + if (current_node_type != NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP)// && self->groundentity) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + else // Correct direction, jump up + { + if (distance > 64) + ucmd->forwardmove = SPEED_WALK; + else if (distance > 48) + ucmd->forwardmove = 50; + else if (distance > 24) + ucmd->forwardmove = 25; + } + + // Guide the bot in when close to the target + //VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + if (self->s.origin[2] <= nodes[self->next_node].origin[2] && distance <= 128) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 100, self->velocity); // Apply it + } + + // + //if (OnLadder(self) == false) + return; + } + + // On ladder going up + if (current_node_type == NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP) + { + if (nodes[self->next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance_xy < 64) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + if (!self->groundentity) + { + // FIXME: Dirty hack so the bots can actually use ladders. + //vec3_t origin_up; + //VectorCopy(self->s.origin, origin_up); + //origin_up[2] += 8; + //VectorSubtract(origin_up, self->s.origin, dist_xy); + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist_xy); + VectorNormalize(dist_xy); + VectorScale(dist_xy, SPEED_RUN, self->velocity); + if (dist_xy[2] > 0) + self->velocity[2] = min(200, self->velocity[2]); + } + + // Guide the bot in when close to the target + // Useful for the tall ladder found on murder.bsp - helps to deal with ladders that use playerclip + if (self->s.origin[2] <= nodes[self->next_node].origin[2] + 16 && distance < 64) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + } + + return; + } + } + + // If ladder going down + if (current_node_type == NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN) + { + if (self->s.origin[2] >= nodes[self->next_node].origin[2] && distance_xy < 64) //&& speed <= SPEED_ROAM) + { + ucmd->forwardmove = SPEED_WALK / 2; + if ((distance_xy < 200) && (nodes[self->next_node].origin[2] <= self->s.origin[2] + 16)) + ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching + + if (!self->groundentity) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + //ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + // FIXME: Dirty hack so the bots can actually use ladders. + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist_xy); + VectorNormalize(dist_xy); + VectorScale(dist_xy, SPEED_RUN, self->velocity); + if (dist_xy[2] < 0) + self->velocity[2] = max(-200, self->velocity[2]); + } + return; + } + } + } + + /* + // If getting off the top of a ladder + if ((current_node_type == NODE_LADDER_UP) && (next_node_type != NODE_LADDER_DOWN)) + { + ucmd->forwardmove = SPEED_WALK; + + if ((nodes[self->next_node].origin[2] >= self->s.origin[2]) + && (nodes[self->next_node].origin[2] >= nodes[self->current_node].origin[2]) + && (((self->velocity[2] > 0) && !self->groundentity) || OnLadder(self))) + { + ucmd->upmove = SPEED_RUN; + self->velocity[2] = min(200, distance_xy * 10); // Jump higher for farther node + } + + ACEMV_ChangeBotAngle(self); + return; + } + + // If getting onto the ladder going up + if ((next_node_type == NODE_LADDER_DOWN) && (distance_xy < 128) && (nodes[self->next_node].origin[2] >= self->s.origin[2])) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + if (DotProduct(velocity, dist_xy) > 0.8) + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_WALK / 2; + + ACEMV_ChangeBotAngle(self); + return; + } + + // On ladder going up + if (current_node_type == NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP) + { + if (nodes[self->next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance_xy < 64)// && speed <= SPEED_ROAM) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist_xy); + if (DotProduct(velocity, dist_xy) > 0.8) + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_WALK / 2; + + if (!self->groundentity) + { + // FIXME: Dirty hack so the bots can actually use ladders. + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist_xy); + VectorNormalize(dist_xy); + VectorScale(dist_xy, SPEED_RUN, self->velocity); + if (dist_xy[2] > 0) + self->velocity[2] = min(200, self->velocity[2]); + } + else + ACEMV_ChangeBotAngle(self); + + return; + } + } + + // If ladder going down + if (current_node_type == NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN) + { + if (self->s.origin[2] >= nodes[self->next_node].origin[2] && distance_xy < 64) //&& speed <= SPEED_ROAM) + { + ucmd->forwardmove = SPEED_WALK / 2; + if ((distance_xy < 200) && (nodes[self->next_node].origin[2] <= self->s.origin[2] + 16)) + ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching + + if (!self->groundentity) + { + // FIXME: Dirty hack so the bots can actually use ladders. + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist_xy); + VectorNormalize(dist_xy); + VectorScale(dist_xy, SPEED_RUN, self->velocity); + if (dist_xy[2] < 0) + self->velocity[2] = max(-200, self->velocity[2]); + } + + //ACEMV_ChangeBotAngle(self); + return; + } + } + + // Slow down when near ladder + if (current_node_type != NODE_LADDER_UP && current_node_type != NODE_LADDER_DOWN) + { + VectorNormalize(dist_xy); + if (distance_xy > 64) + VectorScale(dist_xy, SPEED_RUN, self->velocity); + else if (distance_xy > 48) + VectorScale(dist_xy, 350, self->velocity); + else if (distance_xy > 24) + VectorScale(dist_xy, 300, self->velocity); + else + VectorScale(dist_xy, 250, self->velocity); + ACEMV_ChangeBotAngle(self); + } + + return; + } + */ + //rekkie -- DEV_1 -- e + + // Find the distance vector to the next node + VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist); + // Lose the vertical component + dist[2] = 0; + // Get the absolute length + distance = VectorLength(dist); + + // If on the ladder + if( (next_node_type == NODE_LADDER) && (distance < 64) + && ((nodes[self->next_node].origin[2] > self->s.origin[2]) || ! self->groundentity) ) + { + // FIXME: Dirty hack so the bots can actually use ladders. + VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); + VectorNormalize( dist ); + VectorScale( dist, SPEED_RUN, self->velocity ); + if( dist[2] >= 0 ) + self->velocity[2] = min( 200, self->velocity[2] ); + else + self->velocity[2] = max( -200, self->velocity[2] ); + + if( self->velocity[2] < 0 ) + { + // If we are going down the ladder, check for teammates coming up and yield to them. + trace_t tr; + tr = gi.trace( self->s.origin, tv(-16,-16,-8), tv(16,16,8), nodes[self->next_node].origin, self, MASK_PLAYERSOLID ); + if( (tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0) ) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 200; + } + } + + ACEMV_ChangeBotAngle(self); + return; + } + + // If getting onto the ladder going up + if( (next_node_type == NODE_LADDER) && (distance < 200) + && (nodes[self->next_node].origin[2] >= self->s.origin[2]) ) + { + // Jump only when we are moving the correct direction. + VectorCopy( self->velocity, velocity ); + velocity[2] = 0; + VectorNormalize( velocity ); + VectorNormalize( dist ); + if( DotProduct( velocity, dist ) > 0.8 ) + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_WALK / 2; + + ACEMV_ChangeBotAngle(self); + return; + } + + // If getting off the ladder + if( (current_node_type == NODE_LADDER) && (next_node_type != NODE_LADDER) ) + { + ucmd->forwardmove = SPEED_WALK; + + if( (nodes[self->next_node].origin[2] >= self->s.origin[2]) + && (nodes[self->next_node].origin[2] >= nodes[self->current_node].origin[2]) + && ( ((self->velocity[2] > 0) && ! self->groundentity) || OnLadder(self) ) ) + { + ucmd->upmove = SPEED_RUN; + self->velocity[2] = min( 200, distance * 10 ); // Jump higher for farther node + } + + ACEMV_ChangeBotAngle(self); + return; + } + + // If getting onto the ladder going down + if( (current_node_type != NODE_LADDER) && (next_node_type == NODE_LADDER) ) + { + ucmd->forwardmove = SPEED_WALK / 2; + + if( (distance < 200) && (nodes[self->next_node].origin[2] <= self->s.origin[2] + 16) ) + ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching + + ACEMV_ChangeBotAngle(self); + return; + } + +/* //============================== + // LEDGES etc.. + // If trying to jump up a ledge + if( + (current_node_type == NODE_MOVE && + nodes[self->next_node].origin[2] > self->s.origin[2]+16 && distance < NODE_DENSITY) + ) + { + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + }*/ + //////////////////////////////////////////////////////// + // Water Nodes + /////////////////////////////////////////////////////// + if(current_node_type == NODE_WATER) + { + // We need to be pointed up/down + ACEMV_ChangeBotAngle(self); + + // If the next node is not in the water, then move up to get out. + if(next_node_type != NODE_WATER && !(gi.pointcontents(nodes[self->next_node].origin) & MASK_WATER)) // Exit water + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_RUN * 3 / 4; + return; + + } + + // Falling off ledge? +/* if(!self->groundentity) + { + ACEMV_ChangeBotAngle(self); + + self->velocity[0] = self->move_vector[0] * 360; + self->velocity[1] = self->move_vector[1] * 360; + + return; + }*/ + + /* + // Check to see if stuck, and if so try to free us + // Also handles crouching + VectorSubtract( self->s.origin, self->lastPosition, dist ); + if( (VectorLength(self->velocity) < 37) || (VectorLength(dist) < FRAMETIME) ) + { + // Keep a random factor just in case.... + if(random() > 0.5 && ACEMV_SpecialMove(self, ucmd)) + return; + + if( self->groundentity ) + { + // Check if we are obstructed by an oncoming teammate, and if so strafe right. + trace_t tr; + tr = gi.trace( self->s.origin, tv(-16,-16,-8), tv(16,16,8), nodes[self->next_node].origin, self, MASK_PLAYERSOLID ); + if( (tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client + && (DotProduct( self->velocity, tr.ent->velocity ) <= 0) && ACEMV_CanMove( self, MOVE_RIGHT ) ) + { + ucmd->sidemove = SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + } + } + + self->s.angles[YAW] += random() * 180 - 90; + + ACEMV_ChangeBotAngle(self); + + ucmd->forwardmove = SPEED_RUN; + + // Raptor007: Only jump when trying to gain altitude, not down ledges. + if( nodes[self->next_node].origin[2] > self->s.origin[2] + 16 ) + ucmd->upmove = SPEED_RUN; + + return; + } + */ + + //rekkie -- DEV_1 -- s + //////////////////////////////////////////////////////// + // NODE_STAND_DROP Nodes + if (current_node_type == NODE_STAND_DROP || current_node_type == NODE_CROUCH_DROP || current_node_type == NODE_UNSAFE_DROP) + { + // If the bot fell below the target (failed the jump down) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->next_node].origin[2]) + { + self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (self->current_node == INVALID) + { + self->state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->goal_node); + } + else + { + Com_Printf("%s %s failed to drop down, finding new goal\n", __func__, self->client->pers.netname); + self->state = STATE_WANDER; // Aquire a new node + } + return; + } + + ACEMV_ChangeBotAngle(self); + + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + dist[2] = 0; + distance = VectorLength(dist); + + if (!self->groundentity) + { + // When in air, control speed carefully to avoid overshooting the next node. + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + } + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + // If we reached a ledge, slow down. + if (distance < 512) + { + // Jump unless dropping down to the next node. + if (nodes[self->next_node].origin[2] > (self->s.origin[2] + 7) && distance < 500) + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 512); + } + else + ucmd->forwardmove = SPEED_WALK; + } + else // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + + // Guide the bot in when close to the target + if (distance <= 128) //NODE_MAX_JUMP_HEIGHT) + { + //if (current_node_type == NODE_STAND_DROP) Com_Printf("%s %s: NODE_STAND_DROP\n", __func__, self->client->pers.netname); + //if (current_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s: NODE_CROUCH_DROP\n", __func__, self->client->pers.netname); + //if (current_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s: NODE_UNSAFE_DROP\n", __func__, self->client->pers.netname); + + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + /////////////////////////////////////////////////////// + //rekkie -- DEV_1 -- e + + //////////////////////////////////////////////////////// + // Move Nodes + /////////////////////////////////////////////////////// + + //rekkie -- DEV_1 -- s + // If the bot fell below the target (failed the jump down) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->next_node].origin[2]) + { + // Upgrade target node type to jumppad + if (targetNodeLink != INVALID) + { + //if (debug_mode) Com_Printf("%s %s dropped from a move forward, upgrading move to jumppad node\n", __func__, self->client->pers.netname); + //nodes[self->current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; + self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + return; + } + else + { + if (debug_mode) Com_Printf("%s %s failed to move forward, finding new goal\n", __func__, self->client->pers.netname); + self->state = STATE_WANDER; // Aquire a new node + } + } + //rekkie -- DEV_1 -- e + + ACEMV_ChangeBotAngle(self); + + VectorSubtract( nodes[self->next_node].origin, self->s.origin, dist ); + dist[2] = 0; + distance = VectorLength(dist); + + if( ! self->groundentity ) + { + // When in air, control speed carefully to avoid overshooting the next node. + if( distance < 256 ) + ucmd->forwardmove = SPEED_RUN * sqrtf( distance / 256 ); + else + ucmd->forwardmove = SPEED_RUN; + + //* + //rekkie -- DEV_1 -- s + // Guide the bot in when close to the target + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + distance = VectorLength(dist); + if (distance <= 64) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + //rekkie -- DEV_1 -- e + //*/ + } + //rekkie -- DEV_1 -- s + else + { + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + } + //rekkie -- DEV_1 -- e + /* + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + // If we reached a ledge, slow down. + if( distance < 512 ) + { + // Jump unless dropping down to the next node. + if( (nodes[self->next_node].origin[2] > self->s.origin[2] + 7) && (distance < 500) ) + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_WALK * sqrtf( distance / 512 ); + } + else + ucmd->forwardmove = SPEED_WALK; + } + else + // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + */ +} +#endif + +/* +/////////////////////////////////////////////////////////////////////// +// Wandering code (based on old ACE movement code) +/////////////////////////////////////////////////////////////////////// +// +// RiEvEr - this routine is of a very poor standard and has a LOT of problems +// Maybe replace this with the DroneBot or ReDeMpTiOn wander code? +// +void ACEMV_Wander(edict_t *self, usercmd_t *ucmd) +{ + vec3_t temp; + int content_head = 0, content_feet = 0; + float moved = 0; + + // Do not move + if(self->next_move_time > level.framenum) + return; + + // Special check for elevators, stand still until the ride comes to a complete stop. + if( self->groundentity && (VectorLength(self->groundentity->velocity) >= 8) ) + if(self->groundentity->moveinfo.state == STATE_UP || + self->groundentity->moveinfo.state == STATE_DOWN) // only move when platform not + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 0; + self->next_move_time = level.framenum + 0.5 * HZ; + return; + } + + + // Is there a target to move to + //if (self->movetarget) + // ACEMV_MoveToGoal(self,ucmd); + + VectorSubtract( self->s.origin, self->lastPosition, temp ); + moved = VectorLength(temp); + + //////////////////////////////// + // Ladder? + //////////////////////////////// + // To avoid unnecessary falls, don't use ladders when on solid ground. + if( (! self->groundentity) && OnLadder(self) ) + { + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = 0; + ucmd->sidemove = 0; + self->s.angles[PITCH] = -15; + + // FIXME: Dirty hack so the bots can actually use ladders. + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 200; + + if( moved >= FRAMETIME ) + return; + } + + //////////////////////////////// + // Swimming? + //////////////////////////////// + VectorCopy(self->s.origin,temp); + temp[2] += 22; + content_head = gi.pointcontents(temp); + temp[2] = self->s.origin[2] - 8; + content_feet = gi.pointcontents(temp); + + // Just try to keep our head above water. + if( content_head & MASK_WATER ) + { + // If drowning and no node, move up + if(self->client->next_drown_framenum > 0) + { + ucmd->upmove = SPEED_RUN; + self->s.angles[PITCH] = -45; + } + else + ucmd->upmove = SPEED_WALK; + } + + // Don't wade in lava, try to get out! + if( content_feet & (CONTENTS_LAVA|CONTENTS_SLIME) ) + ucmd->upmove = SPEED_RUN; + + // See if we're jumping out of the water. + if( ! self->groundentity + && (content_feet & MASK_WATER) + && ACEMV_SpecialMove( self, ucmd ) ) + { + if( ucmd->upmove > 0 ) + self->velocity[2] = 270; // FIXME: Is there a cleaner way? + if( moved >= FRAMETIME ) + return; + } + +//@@ if(ACEMV_CheckEyes(self,ucmd)) +// return; + + // Check for special movement if we have a normal move (have to test) + if( (VectorLength(self->velocity) < 37) || (moved < FRAMETIME) ) + { + if(random() > 0.1 && ACEMV_SpecialMove(self,ucmd)) + return; + + self->s.angles[YAW] += random() * 180 - 90; + self->s.angles[PITCH] = 0; + + if( content_feet & MASK_WATER ) // Just keep swimming. + ucmd->forwardmove = SPEED_RUN; + else if(!M_CheckBottom(self) && !self->groundentity) // if there is ground continue otherwise wait for next move + ucmd->forwardmove = 0; + else if( ACEMV_CanMove( self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_WALK; + + return; + } + + // Otherwise move as fast as we can + // If it's safe to move forward (I can't believe ACE didn't check this! + if( ACEMV_CanMove( self, MOVE_FORWARD ) + || (content_feet & MASK_WATER) ) + { + ucmd->forwardmove = SPEED_RUN; + } + else + { + // Need a "findbestdirection" routine in here + // Forget about this route! + ucmd->forwardmove = -SPEED_WALK / 2; + self->movetarget = NULL; + } +} +*/ + +//rekkie -- DEV_1 -- s +/////////////////////////////////////////////////////////////////////// +// Choose the sniper zoom when allowed +/////////////////////////////////////////////////////////////////////// +/* +//void _SetSniper(edict_t* ent, int zoom); +void ACEAI_SetSniper( edict_t *self, int zoom ) +{ + if( (self->client->weaponstate != WEAPON_FIRING) + && (self->client->weaponstate != WEAPON_BUSY) + && ! self->client->bandaging + && ! self->client->bandage_stopped ) + _SetSniper( self, zoom ); +} +*/ +/* +// Check to make sure we're using a primary weapon where possible +void BOTLIB_CheckCurrentWeapon(edict_t* self) +{ + if (ACEIT_ChangeSniperSpecialWeapon(self, FindItem(SNIPER_NAME))) + { + ACEAI_SetSniper(self, 2); // scope in + return; + } + if (ACEIT_ChangeM4SpecialWeapon(self, FindItem(M4_NAME))) + return; + if (ACEIT_ChangeMP5SpecialWeapon(self, FindItem(MP5_NAME))) + return; + if (ACEIT_ChangeM3SpecialWeapon(self, FindItem(M3_NAME))) + return; + if (ACEIT_ChangeHCSpecialWeapon(self, FindItem(HC_NAME))) + return; + if (ACEIT_ChangeDualSpecialWeapon(self, FindItem(DUAL_NAME))) + return; + //if (ACEIT_ChangeWeapon(self, FindItem(GRENADE_NAME))) + // return; + //if (ACEIT_ChangeMK23SpecialWeapon(self, FindItem(MK23_NAME))) + // return; + //if (ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + // return; +} +*/ + +// Locate an item within a set range +edict_t* BOTLIB_LocateRangedItem(edict_t* self, char* classname, float range) +{ + edict_t* item = NULL; + + // look for a target (should make more efficient later) + item = findradius(NULL, self->s.origin, range); + + while (item) + { + if (item->classname == NULL) // end of items found + return NULL; + + //Com_Printf("%s %s items found: %s looking for: %s\n", __func__, self->client->pers.netname, item->classname, classname); + + if (Q_stricmp(item->classname, classname) == 0) + { + return item; // Found item within range + } + + // next item + item = findradius(item, self->s.origin, range); + } + + return NULL; // found nothing +} + +// Locate and pickup a primary weapon if we need one - s +void BOTLIB_GetWeaponsAndAmmo(edict_t* self) +{ + edict_t *item = NULL; + + // If no primary weapon + if (self->client->weapon != FindItem(SNIPER_NAME) + && self->client->weapon != FindItem(M4_NAME) + && self->client->weapon != FindItem(MP5_NAME) + && self->client->weapon != FindItem(M3_NAME) + && self->client->weapon != FindItem(HC_NAME)) + { + // Try find a random primary weapon + int rng_wpn = rand() % 5; + switch (rng_wpn) + { + case 0: + item = BOTLIB_LocateRangedItem(self, "weapon_Sniper", 768); + break; + case 1: + item = BOTLIB_LocateRangedItem(self, "weapon_M4", 768); + break; + case 2: + item = BOTLIB_LocateRangedItem(self, "weapon_MP5", 768); + break; + case 3: + item = BOTLIB_LocateRangedItem(self, "weapon_M3", 768); + break; + case 4: + item = BOTLIB_LocateRangedItem(self, "weapon_HC", 768); + break; + default: + break; + } + + //if (item) + // Com_Printf("%s %s located weapon: %s\n", __func__, self->client->pers.netname, item->classname); + } + + // Check ammo if we have a weapon + else if (self->client->weapon == FindItem(SNIPER_NAME)) + { + if (self->client->sniper_rds <= 6) // Rounds + item = BOTLIB_LocateRangedItem(self, "ammo_sniper", 1024); + } + else if (self->client->weapon == FindItem(M4_NAME)) + { + if (self->client->inventory[ITEM_INDEX(GET_ITEM(M4_ANUM))] < 1) // Clips + item = BOTLIB_LocateRangedItem(self, "ammo_m4", 1024); + } + else if (self->client->weapon == FindItem(MP5_NAME)) + { + if (self->client->inventory[ITEM_INDEX(GET_ITEM(MP5_ANUM))] < 1) // Mags + item = BOTLIB_LocateRangedItem(self, "ammo_mag", 1024); + } + else if (self->client->weapon == FindItem(M3_NAME)) + { + if (self->client->inventory[ITEM_INDEX(GET_ITEM(SHELL_ANUM))] <= 7) // Shells + item = BOTLIB_LocateRangedItem(self, "ammo_m3", 1024); + + //Com_Printf("%s %s got item: %s ammo: %d\n", __func__, self->client->pers.netname, M3_NAME, self->client->inventory[ITEM_INDEX(GET_ITEM(SHELL_ANUM))]); + } + else if (self->client->weapon == FindItem(HC_NAME)) + { + if (self->client->inventory[ITEM_INDEX(GET_ITEM(SHELL_ANUM))] <= 7) // Shells + item = BOTLIB_LocateRangedItem(self, "ammo_m3", 1024); + } + else if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME)) + { + if (self->client->inventory[ITEM_INDEX(GET_ITEM(MK23_ANUM))] < 1) // Clips + item = BOTLIB_LocateRangedItem(self, "ammo_clip", 1024); + } + + // Try to navigate to the item + // Check we're navigating to a different item->classname to that found in goalentity->classname + if (item) + { + // Check if we're already after this item + qboolean goalAlreadySet = false; + if (self->goalentity && Q_stricmp(item->classname, self->goalentity->classname) == 0) + goalAlreadySet = true; + + // Only grab item if we're not already after it + if (self->goalentity == NULL || goalAlreadySet == false) + { + int item_node = ACEND_FindClosestReachableNode(item, NODE_DENSITY, NODE_ALL); + if (item_node != INVALID) + { + self->goalentity = item; + + //Com_Printf("%s %s heading for item: %s\n", __func__, self->client->pers.netname, self->goalentity->classname); + + /* + if (self->bot.prev_node != self->bot.goal_node) + self->bot.prev_node = self->bot.goal_node; // Keep a copy of previous goal + self->bot.goal_node = item_node; // Make the item node our new goal + + // Try visit node near item + // Otherwise visit previous goal node + if (BOTLIB_CanGotoNode(self, self->bot.goal_node) == false) // If we cannot visit item node + { + self->bot.goal_node = self->bot.prev_node; // Restore previous goal + BOTLIB_CanGotoNode(self, self->bot.goal_node); // Try go back to previous goal + } + */ + + //BOTLIB_CanGotoNode(self, self->bot.goal_node, false); // Try go to item node + } + } + } +} +//rekkie -- DEV_1 -- e + +//rekkie -- Quake3 -- s +void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd) +{ + if (self->health != self->old_health || self->client->leg_damage) + { + //Com_Printf("%s %s is healing\n", __func__, self->client->pers.netname); + + // Handle when to bandage + // 1) Bandage immediately if critical damage (highest priority) + // 2) Bandage immediately if no nearby enemies + // 3) Delayed bandaging when no enemy in sight + + if (self->client->bandaging == 0) // Not already bandaging + { + // Set bandage delay time + if (self->bot.bot_bandage_delay_time == 0) // Timer not set yet + { + self->bot.bot_bandage_delay_time = self->bot.jumppad_last_time = level.framenum + 5 * HZ; // Base time + if (self->client->leg_damage) + self->bot.bot_bandage_delay_time += ((rand() % 15) + 1); // 1) Quicker to call Cmd_Bandage_f if leg damage + else + self->bot.bot_bandage_delay_time += ((rand() % 25) + 1); // 2) Slower to call Cmd_Bandage_f for all other damages + } + + // If nearest enemy is far enough away, just bandage. Ignore line of sight because that is checked before BOTLIB_Healing() is called. + float closest_enemy = 999999; + float dist = 0; + for (int p = 0; p < num_players; p++) + { + if (players[p] == self) continue; // Skip self + + if (ACEAI_IsEnemy(self, players[p])) // Are they our enemy + { + dist = VectorDistance(self->s.origin, players[p]->s.origin); + if (dist < closest_enemy) + closest_enemy = dist; + } + } + //if (closest_enemy > 1536) + // self->bot.bot_bandage_delay_time = 0; + + //Com_Printf("%s %s health[%d] enemy dist[%f]\n", __func__, self->client->pers.netname, self->health, closest_enemy); + + + // Bandage if out of sight + if (self->health < 7) // Critical condition, just bandage + { + //Com_Printf("%s %s critical bandaging with health:%d\n", __func__, self->client->pers.netname, self->health); + Cmd_Bandage_f(self); + self->bot.bot_bandage_delay_time = 0; // Reset timer + + if (self->client->bandaging) self->bot.radioBandaging = true; // If bandaging, flag the bot to radio this in + } + //else if ((self->bot.enemy_seen_time + self->bot.bot_bandage_delay_time) <= (level.framenum * HZ)) + else if (self->bot.bot_bandage_delay_time < level.framenum && closest_enemy > 1024) // Bot can bandage + { + //Com_Printf("%s %s is bandaging with health:%d, bandage delay:%f\n", __func__, self->client->pers.netname, self->health, self->bot_bandage_delay_time); + Cmd_Bandage_f(self); + self->bot.bot_bandage_delay_time = 0; // Reset timer + + if (self->client->bandaging) self->bot.radioBandaging = true; // If bandaging, flag the bot to radio this in + } + } + if (self->old_health != self->health) + self->old_health = self->health; + } + // Hold still while bandaging + if (self->client->bandaging == 1 || self->client->bandage_stopped == 1) + { + //self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving while bandaging + self->bot.bi.actionflags |= ACTION_CROUCH; // Crouch while bandaging + + if (self->old_health != self->health) + self->old_health = self->health; + } + + // Update old health + //self->old_health = self->health; +} +void BOTLIB_Attack(edict_t* self, usercmd_t* ucmd) +{ + if (self->client->weaponstate == WEAPON_RELOADING) return; + if (self->client->bandaging) return; + if (self->enemy == NULL) return; + if (self->enemy && self->enemy->deadflag != DEAD_NO) return; + if (teamplay->value && lights_camera_action > 0) return; + + //Com_Printf("%s [%d] %s attack enemy %s\n", __func__, level.framenum, self->client->pers.netname, self->enemy->client->pers.netname); + + //self->client->inventory[items[GRENADE_NUM].index] = 2; // Give some ammo + if (self->client->weapon == FindItemByNum(GRENADE_NUM) && self->client->weaponstate == WEAPON_READY) + { + // If pin pulled, let go of attack + if (self->client->ps.gunframe >= GRENADE_IDLE_FIRST && self->client->ps.gunframe <= GRENADE_IDLE_LAST) + return; + else // Pull pin + { + self->bot.bi.actionflags |= ACTION_ATTACK; + return; + } + } + if (self->client->weapon == FindItemByNum(KNIFE_NUM) && self->client->weaponstate == WEAPON_READY) + { + self->bot.bi.actionflags |= ACTION_ATTACK; + return; + } + + + // Always give bots basic ammo -- cheating, but at least they have something. + if (1) + { + // Knives + if (0 && (rand() % 50) == 0) + { + if ((INV_AMMO(self, KNIFE_NUM) < 2)) + self->client->inventory[items[KNIFE_NUM].index] = 1; // Give some ammo + if (self->client->weapon == FindItem(KNIFE_NAME)) + self->client->pers.knife_mode = 1; // Throwing knives + } + + // MK23 + //if ((rand() % 10) == 0) + { + gitem_t* ammo_item = FindItem(FindItem(MK23_NAME)->ammo); + int ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + self->client->inventory[ammo_index] = 1; + } + + // Grenades + if (0 && (rand() % 50) == 0) + { + if (INV_AMMO(self, BAND_NUM) && (INV_AMMO(self, GRENADE_NUM) < 2)) + self->client->inventory[items[GRENADE_NUM].index] = 1; + } + } + + // Sniper bots should zoom in before firing + if (BOTLIB_SniperZoom(self)) + return; // Wait a frame + + // Pick an appropriate weapon + if (BOTLIB_ChooseWeapon(self) == false) // If bot has no weapon + { + //Com_Printf("%s %s BOTLIB_ChooseWeapon was false\n", __func__, self->client->pers.netname); + return; // Keep moving + } + + // Only shoot if the weapon is ready and we can hit the target! + if (self->client->weaponstate == WEAPON_READY || self->client->weaponstate == WEAPON_FIRING) + { + if (self->bot.enemy_in_xhair) + { + // Check the need for crouching to increase accuracy. + // Only crouch if target is far, doesn't block the shot. + // Only allow firing of an inacurrate weapon (without a laser) if touching the ground + if (self->bot.enemy_dist > 512 && INV_AMMO(self, LASER_NUM) == false) + { + if ((self->client->m4_rds && (self->client->weapon == FindItem(M4_NAME))) + || (self->client->mp5_rds && (self->client->weapon == FindItem(MP5_NAME))) + || (self->client->mk23_rds && (self->client->weapon == FindItem(MK23_NAME))) + || (self->client->dual_rds && (self->client->weapon == FindItem(DUAL_NAME)))) + { + if (self->bot.touch_ground.fraction == 1.0) // Don't fire while to far off the ground + return; + + // Raptor007: Don't crouch if it blocks the shot. + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); + } + self->s.origin[2] += 14; + } + } + + // Jump at enemy if we have a HC + { + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME))// && self->bot.enemy_dist > 128) + { // If enemy if far enough away, we're touching the ground, and the high difference is within jumping height + if (self->bot.enemy_dist > 264 && self->bot.enemy_dist < 300 && self->bot.touch_ground.fraction < 1.0 && self->bot.enemy_height_diff <= 60) + { + //BOTLIB_Jump_Takeoff(self, NULL, self->enemy->s.origin, self->viewheight, self->velocity); + //BOTLIB_DoParabolaJump(self, self->enemy->s.origin); + // Should probably make the jump less parabola and more head on for a faster attack + } + + // Only fire the HC when close enough + if (self->bot.enemy_dist > 264) //self->bot.enemy_dist > 160 //&& (self->bot.touch_ground.fraction == 1.0 || self->bot.enemy_dist > 64)) + { + if (self->groundentity == NULL) + return; + else if (self->bot.enemy_dist > 160) + return; + } + } + } + + // Attack! + self->bot.bi.actionflags |= ACTION_ATTACK; + } + } +} +//rekkie -- Quake3 -- e + +/////////////////////////////////////////////////////////////////////// +// Attack movement routine +// +// NOTE: Very simple for now, just a basic move about avoidance. +// Change this routine for more advanced attack movement. +/////////////////////////////////////////////////////////////////////// +void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) +{ + float c; + vec3_t target; + vec3_t attackvector; + float dist; + qboolean bHasWeapon; // Needed to allow knife throwing and kick attacks + qboolean enemy_front; + + bHasWeapon = self->grenadewait || ACEAI_ChooseWeapon(self); + + //rekkie -- DEV_1 -- s + // If player has no weapon, keep moving! + if (bHasWeapon == false) + { + //Com_Printf("%s %s bHasWeapon %d self->bot.goal_node %d\n", __func__, self->client->pers.netname, bHasWeapon, self->bot.goal_node); + BOTLIB_MOV_Move(self, ucmd); // Keep moving + } + //rekkie -- DEV_1 -- e + + // Check distance to enemy + VectorSubtract( self->s.origin, self->enemy->s.origin, attackvector); + dist = VectorLength( attackvector); + + enemy_front = infront( self, self->enemy ); + + //If we're fleeing, don't bother moving randomly around the enemy and stuff... +// if (self->state != STATE_FLEE) + { + +//AQ2 CHANGE + // Don't stand around if all you have is a knife or no weapon. + if( (self->client->weapon == FindItem(KNIFE_NAME)) || !bHasWeapon/*kick attack*/ ) + { + // Don't walk off the edge + if( ACEMV_CanMove( self, MOVE_FORWARD)) + { + ucmd->forwardmove += SPEED_RUN; + } + else + { + // Can't get there! + ucmd->forwardmove = -SPEED_WALK; + self->enemy=NULL; + self->state = STATE_WANDER; + return; + } + + // Raptor007: Attempt longer knife throws when carrying many knives. + self->client->pers.knife_mode = 0; + if( (dist < 2000) && (dist < 400 + 300 * INV_AMMO(self,KNIFE_NUM)) ) + { + float kick_dist = 200; + + if( bHasWeapon ) + { + self->client->pers.knife_mode = 1; + kick_dist = 100; + } + + if( dist < kick_dist ) + { + if( dist < 64 ) // Too close + ucmd->forwardmove = -SPEED_WALK; + // Kick Attack needed! + ucmd->upmove = SPEED_RUN; + } + } + } + else //if(!(self->client->weapon == FindItem(SNIPER_NAME))) // Stop moving with sniper rifle + { + // Randomly choose a movement direction + c = random(); + if(c < 0.2 && ACEMV_CanMove(self,MOVE_LEFT)) + ucmd->sidemove -= SPEED_RUN; + else if(c < 0.4 && ACEMV_CanMove(self,MOVE_RIGHT)) + ucmd->sidemove += SPEED_RUN; + + c = random(); + if(c < 0.6 && ACEMV_CanMove(self,MOVE_FORWARD)) + ucmd->forwardmove += SPEED_RUN; + else if (c < 0.8 && ACEMV_CanMove(self,MOVE_BACK)) + ucmd->forwardmove -= SPEED_RUN; + } +//AQ2 END + if( (dist < 600) && + ( !(self->client->weapon == FindItem(KNIFE_NAME)) +)// && !(self->client->weapon == FindItem(SNIPER_NAME))) // Stop jumping with sniper rifle + &&( bHasWeapon ) // Jump already set for kick attack + ) + { + // Randomly choose a vertical movement direction + c = random(); + + if ((c < 0.10) && FRAMESYNC) //Werewolf: was 0.15 + { + if (ACEMV_CanJump(self)) + { + ucmd->upmove += SPEED_RUN; + //Com_Printf("%s %s jumping\n", __func__, self->client->pers.netname); + } + } + else if (c > 0.85) // Only crouch sometimes + { + ucmd->upmove -= SPEED_WALK; + //Com_Printf("%s %s crouching\n", __func__, self->client->pers.netname); + } + } + + } //The rest applies even for fleeing bots + + // Werewolf: Crouch if no laser light + if( (ltk_skill->value >= 3) + && self->groundentity + && ! INV_AMMO( self, LASER_NUM ) + && ( (self->client->m4_rds && (self->client->weapon == FindItem(M4_NAME))) + || (self->client->mp5_rds && (self->client->weapon == FindItem(MP5_NAME))) + || (self->client->mk23_rds && (self->client->weapon == FindItem(MK23_NAME))) + || (self->client->dual_rds && (self->client->weapon == FindItem(DUAL_NAME))) ) ) + { + //rekkie -- DEV_1 -- s + + // Try strafing around enemy + { + VectorSubtract(self->enemy->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + + + // Try strafing continually in a general direction, if possible + static int max_strafe_left = -10; + static int max_strafe_right = 10; + if (self->bot_strafe == 0) // Pick new direction + { + float strafe_choice = random() < 0.333; + if (strafe_choice < 0.33) // Left + self->bot_strafe = -1; + else if (strafe_choice < 0.66) // Right + self->bot_strafe = 1; + else + self->bot_strafe = 0; // Neither - skip strafing this turn + } + if (self->bot_strafe < 0 && random() > 0.15) // Going left 85% of the time + { + if (ACEMV_CanMove(self, MOVE_LEFT) && self->bot_strafe >= max_strafe_left) // Can go left with a limit + { + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe--; + ucmd->sidemove = -SPEED_RUN; + } + else if (ACEMV_CanMove(self, MOVE_RIGHT)) // Cannot go left anymore, so try going right + { + self->bot_strafe = 1; // Go right + ucmd->sidemove = SPEED_RUN; + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else if (self->bot_strafe > 0 && random() > 0.15) // Going right 85% of the time + { + if (ACEMV_CanMove(self, MOVE_RIGHT) && self->bot_strafe <= max_strafe_right) // Can go right with a limit + { + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe++; + ucmd->sidemove = SPEED_RUN; + } + else if (ACEMV_CanMove(self, MOVE_LEFT)) // Cannot go left anymore, so try going left + { + self->bot_strafe = -1; // Go left + ucmd->sidemove = -SPEED_RUN; + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else + self->bot_strafe = 0; // Skip strafing this turn + + // Back off if getting too close (unless we have a HC or knife) + if (dist < 256) + { + if (self->client->weapon == FindItem(HC_NAME) || self->client->weapon == FindItem(KNIFE_NAME)) + { + // Come in close for the kill + if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_RUN; + } + // Try move backwards + else if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + } + // If distance is far, consider crouching to increase accuracy + else if (dist > 1024) + { + if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_RUN; + + // Raptor007: Don't crouch if it blocks the shot. + float old_z = self->s.origin[2]; + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + ucmd->upmove = -SPEED_RUN; + //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); + } + self->s.origin[2] = old_z; + } + else + { + // Keep distance with sniper + if (dist < 1024 && self->client->weapon == FindItem(SNIPER_NAME) && ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + + // Otherwise move toward target + else if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_RUN; + } + } + + // If the bots are close to their target, don't crouch, take other measures. + //c = random(); + //if (c < 0.10) + //if (dist < 640) + if (1) + { + //if (ACEMV_CanJump(self)) + //{ + // ucmd->upmove += SPEED_RUN; + // Com_Printf("%s %s jump shooting\n", __func__, self->client->pers.netname); + //} + + /* + for (int i = 0; i < MAXLINKS; i++) + { + if (self->current_node == INVALID) + { + self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + if (self->current_node == INVALID) + break; + } + + if (nodes[self->current_node].links[i].targetNode != INVALID) + { + c = random(); + if (c < 0.01) + { + //if (BOTLIB_CanVisitNode(self, nodes[self->current_node].links[i].targetNode)) // Make sure we can visit node + { + self->state = STATE_MOVE; + self->tries = 0; // Reset the count of how many times we tried this goal + BOTLIB_SetGoal(self, nodes[self->current_node].links[i].targetNode); + self->wander_timeout = level.framenum + 1.0 * HZ; + Com_Printf("%s %s avoidance code\n", __func__, self->client->pers.netname); + return; + } + } + //else + // break; + //else + // return; + } + } + */ + } + else + { + //rekkie -- DEV_1 -- e + + + // Raptor007: Don't crouch if it blocks the shot. + float old_z = self->s.origin[2]; + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + ucmd->upmove = -SPEED_RUN; + //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); + } + self->s.origin[2] = old_z; + + + } //rekkie -- DEV_1 + } + + // Set the attack + //@@ Check this doesn't break grenades! + //Werewolf: I've checked that. Now it doesn't anymore. Behold my power! Muhahahahaha!!! + if((self->client->weaponstate == WEAPON_READY)||(self->client->weaponstate == WEAPON_FIRING)) + { + // Only shoot if the weapon is ready and we can hit the target! + //@@ Removed to try to help RobbieBoy! + //Reenabled by Werewolf + if( ACEAI_CheckShot( self )) + { + if( (ltk_skill->value >= 0) + && (self->react >= 1.f / (ltk_skill->value + 2.f)) // Reaction time. + && enemy_front + && (FRAMESYNC || (self->client->weaponstate != WEAPON_READY)) // Sync firing with random offsets. + && (self->teamPauseTime == level.framenum - 1) ) // Saw them last frame too. + { + ucmd->buttons = BUTTON_ATTACK; + + if(self->client->weapon == FindItem(GRENADE_NAME)) + { + self->grenadewait = level.framenum + 1.5 * HZ; + ucmd->forwardmove = -SPEED_RUN; //Stalk back, behold of the holy Grenade! + } + else + self->grenadewait = 0; + } + } + else if (self->state != STATE_FLEE) + { + if (ucmd->upmove == -SPEED_RUN) + ucmd->upmove = 0; + if (ltk_jumpy->value) + { + c = random(); + if(c < 0.50) //Only jump at 50% probability + { + if (ACEMV_CanJump(self)) + ucmd->upmove = SPEED_RUN; + } + else if (c < 0.75 && ACEMV_CanMove(self,MOVE_LEFT)) + ucmd->sidemove -= SPEED_WALK; + else if (ACEMV_CanMove(self,MOVE_RIGHT)) + ucmd->sidemove += SPEED_WALK; + } + } + } + + // Raptor007: Don't immediately shoot friendlies after round. + if( (team_round_countdown > 40) && ! self->grenadewait ) + { + if( self->client->weapon == FindItem(KNIFE_NAME) ) + self->client->pers.knife_mode = 1; // Throwing knives are honorable. + else if( self->client->weapon != FindItem(HC_NAME) ) // Handcannon is allowed. + ucmd->buttons &= ~BUTTON_ATTACK; + + if( dist < 128 ) + { + if( self->groundentity && (self->s.origin <= self->enemy->s.origin) ) + ucmd->upmove = SPEED_RUN; // Kicking is the most honorable form of combat. + } + else + { + ucmd->upmove = 0; + if( ACEMV_CanMove( self, MOVE_FORWARD ) ) + ucmd->forwardmove = SPEED_RUN; // We need to get closer to kick. + } + } + + // Aim + VectorCopy(self->enemy->s.origin,target); + + // Werewolf: Aim higher if using grenades + if(self->client->weapon == FindItem(GRENADE_NAME)) + target[2] += 35; + + //AQ2 ADD - RiEvEr + // Alter aiming based on skill level + // Modified by Werewolf and Raptor007 + if( + ( (ltk_skill->value >= 0) && (ltk_skill->value < 10) ) + && ( bHasWeapon ) // Kick attacks must be accurate + && (!(self->client->weapon == FindItem(KNIFE_NAME))) // Knives accurate + && (!(self->client->weapon == FindItem(GRENADE_NAME))) // Grenades accurate + ) + { + short int sign[3], iFactor = 7; + sign[0] = (random() < 0.5) ? -1 : 1; + sign[1] = (random() < 0.5) ? -1 : 1; + sign[2] = (random() < 0.5) ? -1 : 1; + + // Not that complex. We miss by 0 to 80 units based on skill value and random factor + // Unless we have a sniper rifle! + if(self->client->weapon == FindItem(SNIPER_NAME)) + iFactor = 5; + + // Shoot less accurately if we just got hit. + if( self->client->push_timeout > 45 ) + iFactor += self->client->push_timeout - 45; + + // Shoot more accurately after holding aim, but less accurately when first aiming. + if( self->react >= 4.f ) + iFactor --; + else if( (self->react < 0.5f) && (dist > 300.f) ) + iFactor ++; + + // Shoot less accurately at moving targets. + if (VectorLength(self->enemy->velocity) > 300.f) + iFactor++; + + target[0] += sign[0] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; + target[1] += sign[1] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )) * 0.7f; + target[2] += sign[2] * (10 - ltk_skill->value + ( ( iFactor*(10 - ltk_skill->value) ) * random() )); + } + //Werewolf: Snipers of skill 10 are complete lethal, so I don't use that code down there +/* else if (ltk_skill->value == 11) + if(self->client->weapon == FindItem(SNIPER_NAME)) + { + short int up, right, iFactor=1; + up = (random() < 0.5)? -1 :1; + right = (random() < 0.5)? -1 : 1; + target[0] += ( right * (10 - 5 +((iFactor*(10 - 5)) *random())) ); + target[2] += ( up * (10 - 5 +((iFactor*(10 - 5)) *random())) ); + } +*/ +//AQ2 END + + // Werewolf: release trigger after 1 second for grenades + if( (self->grenadewait == level.framenum + 1 * HZ) && (self->solid != SOLID_NOT) ) + { + ucmd->buttons = 0; + } + + //Werewolf: Wait for grenade to launch before facing elsewhere + if( level.framenum >= self->grenadewait ) + { + self->grenadewait = 0; + + // Raptor007: Interpolate angle changes, and set new desired direction at 10fps. + // Make sure imperfect angles are calculated when first spotting an enemy, too. + ACEMV_ChangeBotAngle( self ); + if( FRAMESYNC || ! enemy_front || (level.framenum - self->teamPauseTime >= FRAMEDIV) ) + VectorSubtract( target, self->s.origin, self->move_vector ); + } + + // Store time we last saw an enemy + // This value is used to decide if we initiate a long range search or not. + self->teamPauseTime = level.framenum; + +// if(debug_mode) +// debug_printf("%s attacking %s\n",self->client->pers.netname,self->enemy->client->pers.netname); +} + +//========================== +// AntPathMove +//========================== +// +qboolean AntPathMove( edict_t *self ) +{ + //node_t *temp = &nodes[self->bot.current_node]; // For checking our position + + //if( level.time == (float)((int)level.time) ) + if( level.framenum % HZ == 0 ) + { + if( + !AntLinkExists( self->bot.current_node, SLLfront(&self->pathList) ) + && ( self->bot.current_node != SLLfront(&self->pathList) ) + ) + { + // We are off the path - clear out the lists + AntInitSearch( self ); + } + } + + // Boot in our new pathing algorithm + // This will fill ai.pathList with the information we need + if( + SLLempty(&self->pathList) // We have no path and + && (self->bot.current_node != self->bot.goal_node) // we're not at our destination + ) + { + if( !AntStartSearch( self, self->bot.current_node, self->bot.goal_node, true)) // Set up our pathList + { + // Failed to find a path +// gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path \n", +// self->client->pers.netname, self->bot.goal_node, self->bot.next_node); + return false; + } + return true; + } + + // And change our pathfinding to use it + if ( !SLLempty(&self->pathList) ) // We have a path + { +// if( AtNextNode( self->s.origin, SLLfront(&self->pathList)) // we're at the nextnode + if( ACEND_FindClosestReachableNode(self,NODE_DENSITY*0.66,NODE_ALL) == SLLfront(&self->pathList) // we're at the nextnode + || self->bot.current_node == SLLfront(&self->pathList) + ) + { + self->bot.current_node = SLLfront(&self->pathList); // Show we're there + SLLpop_front(&self->pathList); // Remove the top node + } + if( !SLLempty(&self->pathList) ) + self->bot.next_node = SLLfront(&self->pathList); // Set our next destination + else + { + self->bot.next_node = INVALID; // We're at the target location + //Com_Printf("%s We're at the target location!\n", __func__); + } + return true; + } + return true; // Pathlist is emptyand we are at our destination +} + diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 8788215b9..778a984e2 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1,1394 +1,4532 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot_nodes.c 9 29/02/00 22:59 Riever $ - * - * $Log: /LTK2/src/acesrc/acebot_nodes.c $ - * - * 9 29/02/00 22:59 Riever - * Updated removelink to work with antpath. - * - * 8 27/02/00 13:08 Riever - * Enabled sensible jump linking for humans. - * - * 7 24/02/00 20:03 Riever - * Reverse link code re-enabled. - * - * 6 24/02/00 3:05 Riever - * Updated LTK nodes version to 4 - * - * User: Riever Date: 23/02/00 Time: 23:19 - * New door node creation (a node either side). - * Nodes raised to allow better linking. - * Temp nodes at routing only exist for 60 seconds. - * User: Riever Date: 23/02/00 Time: 17:24 - * Added support for 'sv shownodes on/off' - * Enabled creation of nodes for ALL doors. (Stage 1 of new method) - * User: Riever Date: 21/02/00 Time: 15:16 - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * User: Riever Date: 20/02/00 Time: 20:27 - * Added new members and definitions ready for 2nd generation of bots. - * - */ - -/////////////////////////////////////////////////////////////////////// -// -// acebot_nodes.c - This file contains all of the -// pathing routines for the ACE bot. -// -/////////////////////////////////////////////////////////////////////// - -#include "../g_local.h" - -#include "acebot.h" - -#define LTK_NODEVERSION 4 - -// flags -qboolean newmap=true; - -// Total number of nodes that are items -int numitemnodes; - -// Total number of nodes -int numnodes; - -// For debugging paths -int show_path_from = -1; -int show_path_to = -1; - -// array for node data -node_t nodes[MAX_NODES]; -short int path_table[MAX_NODES][MAX_NODES]; - -/////////////////////////////////////////////////////////////////////// -// NODE INFORMATION FUNCTIONS -/////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////// -// Determin cost of moving from one node to another -/////////////////////////////////////////////////////////////////////// -int ACEND_FindCost(int from, int to) -{ - // RiEvEr - Bug Hunting - int curnode = INVALID; - int cost=1; // Shortest possible is 1 - - // If we can not get there then return invalid - if( (from == INVALID) || (to == INVALID) || - (path_table[from][to] == INVALID) ) - return INVALID; - - // Otherwise check the path and return the cost - curnode = path_table[from][to]; - - // Find a path (linear time, very fast) - while(curnode != to) - { - curnode = path_table[curnode][to]; - if(curnode == INVALID) // something has corrupted the path abort - return INVALID; - if(cost > numnodes) // Sanity check to avoid infinite loop. - return INVALID; - cost++; - } - - return cost; -} - -/////////////////////////////////////////////////////////////////////// -// Find a close node to the player within dist. -// -// Faster than looking for the closest node, but not very -// accurate. -/////////////////////////////////////////////////////////////////////// -int ACEND_FindCloseReachableNode(edict_t *self, int range, int type) -{ - vec3_t v; - int i; - trace_t tr; - float dist; - vec3_t maxs,mins; - - VectorCopy(self->mins,mins); - mins[2] += 16; - VectorCopy(self->maxs,maxs); - maxs[2] -= 16; - - range *= range; - - for(i=0;is.origin,v); // subtract first - - dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; - - if(dist < range) // square range instead of sqrt - { - // make sure it is visible - //AQ2 ADDED MASK_SOLID - //trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, nodes[i].origin, self, MASK_SOLID|MASK_OPAQUE); - tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_ALL); - - if(tr.fraction == 1.0) - return i; - } - } - } - - return -1; -} - - -int ACEND_DistanceToTargetNode(edict_t *self) -{ - float dist; - //int node=-1; - vec3_t v; - - VectorSubtract(nodes[self->goal_node].origin, self->s.origin,v); // subtract first - dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; - - return dist; -} - - -/////////////////////////////////////////////////////////////////////// -// Find the closest node to the player within a certain range -/////////////////////////////////////////////////////////////////////// -int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) -{ - int i; - float closest = 99999; - float dist; - int node=-1; - vec3_t v; - trace_t tr; - float rng; - vec3_t maxs,mins; - - VectorCopy(self->mins,mins); - VectorCopy(self->maxs,maxs); - - // For Ladders, do not worry so much about reachability - if(type == NODE_LADDER) - { - VectorCopy(vec3_origin,maxs); - VectorCopy(vec3_origin,mins); - } - else - { - mins[2] += 18; // Stepsize - maxs[2] -= 16; // Duck a little.. - } - - rng = (float)(range * range); // square range for distance comparison (eliminate sqrt) - - for(i=0;is.origin,v); // subtract first - - dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; - - if(dist < closest && dist < rng) - { - // make sure it is visible - //AQ2 added MASK_SOLID - tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_SOLID|MASK_OPAQUE); -// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, nodes[i].origin, self, MASK_ALL); - if( (tr.fraction == 1.0) || - ( (tr.fraction > 0.9) // may be blocked by the door itself! - && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0) ) - ) - { - node = i; - closest = dist; - } - } - } - } - - return node; -} - -/////////////////////////////////////////////////////////////////////// -// Find the closest node to the player within a certain range that doesn't have LOS to an enemy -/////////////////////////////////////////////////////////////////////// -int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type) -{ - int i; - float closest = 99999; - float dist; - int node=-1; - vec3_t v; - trace_t tr; - float rng; - vec3_t maxs,mins; - int curplayer; - int is_safe=1; - - VectorCopy(self->mins,mins); - VectorCopy(self->maxs,maxs); - - // For Ladders, do not worry so much about reachability - if(type == NODE_LADDER) - { - VectorCopy(vec3_origin,maxs); - VectorCopy(vec3_origin,mins); - } - else - { - mins[2] += 18; // Stepsize - maxs[2] -= 16; // Duck a little.. - } - - rng = (float)(range * range); // square range for distance comparison (eliminate sqrt) - - for(i=0;is.origin,v); // subtract first - - dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; - - if(dist < closest && dist < rng && dist > 200) - { - // make sure it is visible - //AQ2 added MASK_SOLID - tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_SOLID|MASK_OPAQUE); -// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, nodes[i].origin, self, MASK_ALL); - if( (tr.fraction == 1.0) || - ( (tr.fraction > 0.9) // may be blocked by the door itself! - && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0) ) - ) - { - - is_safe=1; - for (curplayer=0; curplayerteam != self->team) && (players[curplayer]->solid != SOLID_NOT) ) - { - tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), players[curplayer]->s.origin, self, MASK_SOLID|MASK_OPAQUE); - if (tr.fraction == 1.0) - { - is_safe=0; - break; - } - } - - if (is_safe==1) - { - node = i; - closest = dist; - } - - } - } - } - } - - return node; -} - - -/////////////////////////////////////////////////////////////////////// -// BOT NAVIGATION ROUTINES -/////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////// -// Set up the goal -/////////////////////////////////////////////////////////////////////// -void ACEND_SetGoal(edict_t *self, int goal_node) -{ - int node; - - self->goal_node = goal_node; - node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); - - if(node == -1) - { - self->node_timeout /= 2; // If invalid, wait half the time again, then retry. - return; - } - - if(debug_mode) - debug_printf("%s new start node selected %d\n",self->client->pers.netname,node); - - self->current_node = node; - self->next_node = self->current_node; // make sure we get to the nearest node first - self->node_timeout = 0; -} - -/////////////////////////////////////////////////////////////////////// -// Move closer to goal by pointing the bot to the next node -// that is closer to the goal -/////////////////////////////////////////////////////////////////////// -qboolean ACEND_FollowPath(edict_t *self) -{ - vec3_t v; - - ////////////////////////////////////////// - // Show the path (uncomment for debugging) - show_path_from = self->current_node; - show_path_to = self->goal_node; - - if( ltk_showpath->value ) - { - ACEND_DrawPath(self); - } - ////////////////////////////////////////// - - // Try again? - if(self->node_timeout ++ > 5*BOT_FPS) - { - if(self->tries++ >= 3) - return false; - else - ACEND_SetGoal(self,self->goal_node); - } - - //RiEvEr - new path code & algorithm - // This part checks if we are off course - if( (level.framenum % HZ == 0) || (self->node_timeout == 0) ) - { - if( (self->goal_node == INVALID) || ( - !AntLinkExists( self->current_node, SLLfront(&self->pathList) ) - && ( self->current_node != SLLfront(&self->pathList) ) - )) - { - // We are off the path - clear out the lists - AntInitSearch( self ); - } - } - // Boot in our new pathing algorithm - // This will fill self->pathList with the information we need - if( - SLLempty(&self->pathList) // We have no path and - && (self->current_node != self->goal_node) // we're not at our destination - ) - { - if( !AntStartSearch( self, self->current_node, self->goal_node)) // Set up our pathList - { - // Failed to find a path - if( debug_mode ) - gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path (Next: %i)\n", - self->client->pers.netname, self->goal_node, self->next_node); - return false; - } -// return true; - } - //R - - // Are we there yet? - VectorSubtract(self->s.origin,nodes[self->next_node].origin,v); - - if(VectorLength(v) < 32) - { - // reset timeout - self->node_timeout = 0; - - if(self->next_node == self->goal_node) - { - if(debug_mode) - debug_printf("%s reached goal node %i!\n", self->client->pers.netname, self->goal_node); - - if (self->state == STATE_FLEE) - ACEAI_PickSafeGoal(self); - else - ACEAI_PickLongRangeGoal(self); // Pick a new goal - } - else - { - self->current_node = self->next_node; -// self->next_node = path_table[self->current_node][self->goal_node]; - // Removethe front entry from the list - if( self->next_node == SLLfront(&self->pathList) ) - SLLpop_front(&self->pathList); - // Get the next node - if there is one! - if( !SLLempty(&self->pathList)) - { - self->next_node = SLLfront( &self->pathList); - if(debug_mode) - debug_printf("%s reached node %i, next is %i.\n", self->client->pers.netname, self->current_node, self->next_node); - } - else - { - // We messed up... - if( debug_mode) - gi.bprintf(PRINT_HIGH, "Trying to read an empty SLL nodelist!\n"); - self->next_node = INVALID; - } - } - } - - if(self->current_node == -1 || self->next_node ==-1) - return false; - - // Set bot's movement vector - VectorSubtract (nodes[self->next_node].origin, self->s.origin , self->move_vector); - - return true; -} - - -/////////////////////////////////////////////////////////////////////// -// MAPPING CODE -/////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////// -// Capture when the grappling hook has been fired for mapping purposes. -/////////////////////////////////////////////////////////////////////// -void ACEND_GrapFired(edict_t *self) -{ -/* int closest_node; - - if(!self->owner) - return; // should not be here - - // Check to see if the grapple is in pull mode - if(self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL) - { - // Look for the closest node of type grapple - closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_GRAPPLE); - if(closest_node == -1 ) // we need to drop a node - { - closest_node = ACEND_AddNode(self,NODE_GRAPPLE); - - // Add an edge - ACEND_UpdateNodeEdge(self, self->owner->last_node,closest_node); - - self->owner->last_node = closest_node; - } - else - self->owner->last_node = closest_node; // zero out so other nodes will not be linked - }*/ -} - - -/////////////////////////////////////////////////////////////////////// -// Check for adding ladder nodes -/////////////////////////////////////////////////////////////////////// -qboolean ACEND_CheckForLadder(edict_t *self) -{ - int closest_node; - - // If there is a ladder and we are moving up, see if we should add a ladder node - if (gi.pointcontents(self->s.origin) & CONTENTS_LADDER && self->velocity[2] > 0) - { - //debug_printf("contents: %x\n",tr.contents); - - closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_LADDER); - if(closest_node == -1) - { - closest_node = ACEND_AddNode(self,NODE_LADDER); - - // Now add link - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - - // Set current to last - self->last_node = closest_node; - } - else - { - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - self->last_node = closest_node; // set visited to last - } - return true; - } - return false; -} - -//======================================================= -// LadderForward -//======================================================= -// -// The ACE code version of this doesn't work! - -qboolean ACEND_LadderForward( edict_t *self )//, vec3_t angles ) -{ - vec3_t dir, angle, dest, min, max; - trace_t trace; - int closest_node; - - - VectorClear(angle); - angle[1] = self->s.angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - VectorCopy(self->mins,min); - min[2] += 22; - VectorCopy(self->maxs,max); - VectorMA(self->s.origin, TRACE_DIST_LADDER, dir, dest); - - trace = gi.trace(self->s.origin, min, max, dest, self, MASK_ALL); - - //BOTUT_TempLaser(self->s.origin, dest); - if (trace.fraction == 1.0) - return (false); - -// gi.bprintf(PRINT_HIGH,"Contents forward are %d\n", trace.contents); - if (trace.contents & CONTENTS_LADDER || trace.contents &CONTENTS_DETAIL) - { - // Debug print -// gi.bprintf(PRINT_HIGH,"contents: %x\n",trace.contents); - - closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_LADDER); - if(closest_node == -1) - { - closest_node = ACEND_AddNode(self,NODE_LADDER); - - // Now add link - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - - // Set current to last - self->last_node = closest_node; - } - else - { - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - self->last_node = closest_node; // set visited to last - } - return (true); - } - return (false); -} - - -/////////////////////////////////////////////////////////////////////// -// This routine is called to hook in the pathing code and sets -// the current node if valid. -/////////////////////////////////////////////////////////////////////// -void ACEND_PathMap(edict_t *self) -{ - int closest_node; - // Removed last_update checks since this stopped multiple node files being built - vec3_t v; - - // Special node drawing code for debugging - if( ltk_showpath->value ) - { - if(show_path_to != -1) - ACEND_DrawPath( self ); - } - - // Just checking lightlevels - uncomment to use -// if( debug_mode && !self->is_bot) -// gi.bprintf(PRINT_HIGH,"LightLevel = %d\n", self->light_level); - - //////////////////////////////////////////////////////// - // Special check for ladder nodes - /////////////////////////////////////////////////////// - // Replace non-working ACE version with mine. -// if(ACEND_CheckForLadder(self)) // check for ladder nodes - if(ACEND_LadderForward(self)) // check for ladder nodes - return; - - // Not on ground, and not in the water, so bail - if(!self->groundentity && !self->waterlevel) - return; - - //////////////////////////////////////////////////////// - // Lava/Slime - //////////////////////////////////////////////////////// - VectorCopy(self->s.origin,v); - v[2] -= 18; - if(gi.pointcontents(v) & (CONTENTS_LAVA|CONTENTS_SLIME)) - return; // no nodes in slime - - //////////////////////////////////////////////////////// - // Jumping - /////////////////////////////////////////////////////// - if(self->is_jumping) - { - // See if there is a closeby jump landing node (prevent adding too many) - closest_node = ACEND_FindClosestReachableNode(self, 64, NODE_JUMP); - - if(closest_node == INVALID) - closest_node = ACEND_AddNode(self,NODE_JUMP); - - // Now add link - if(self->last_node != -1) - ACEND_UpdateNodeEdge(self, self->last_node, closest_node); - - self->is_jumping = false; - return; - } - - // Werewolf: - //////////////////////////////////////////////////////// - // Switches, etc. - uses the "grapple" nodetype - /////////////////////////////////////////////////////// - if(self->is_triggering) - { - // See if there is a closeby grapple node (prevent adding too many) -// closest_node = ACEND_FindClosestReachableNode(self, 64, NODE_GRAPPLE); - -// if(closest_node == INVALID) - closest_node = ACEND_AddNode(self,NODE_GRAPPLE); - - // Now add link - if(self->last_node != -1) - ACEND_UpdateNodeEdge(self, self->last_node, closest_node); - - self->is_triggering = false; - return; - } - - -/* //////////////////////////////////////////////////////////// - // Grapple - // Do not add nodes during grapple, added elsewhere manually - //////////////////////////////////////////////////////////// - if(ctf->value && self->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL) - return;*/ - - // Iterate through all nodes to make sure far enough apart - closest_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - - //////////////////////////////////////////////////////// - // Special Check for Platforms - //////////////////////////////////////////////////////// - if(self->groundentity && self->groundentity->use == Use_Plat) - { - if(closest_node == INVALID) - return; // Do not want to do anything here. - - // Here we want to add links - if(closest_node != self->last_node && self->last_node != INVALID) - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - - self->last_node = closest_node; // set visited to last - return; - } - - //////////////////////////////////////////////////////// - // Add Nodes as needed - //////////////////////////////////////////////////////// - if(closest_node == INVALID) - { - // Add nodes in the water as needed - if(self->waterlevel) - closest_node = ACEND_AddNode(self,NODE_WATER); - else - closest_node = ACEND_AddNode(self,NODE_MOVE); - - // Now add link - if(self->last_node != -1) - ACEND_UpdateNodeEdge(self, self->last_node, closest_node); - - } - else if(closest_node != self->last_node && self->last_node != INVALID) - ACEND_UpdateNodeEdge(self, self->last_node,closest_node); - - self->last_node = closest_node; // set visited to last - -} - -/////////////////////////////////////////////////////////////////////// -// Init node array (set all to INVALID) -/////////////////////////////////////////////////////////////////////// -void ACEND_InitNodes(void) -{ - numnodes = 1; - numitemnodes = 1; - memset(nodes,0,sizeof(node_t) * MAX_NODES); - memset(path_table,INVALID,sizeof(short int)*MAX_NODES*MAX_NODES); - -} - -/////////////////////////////////////////////////////////////////////// -// Show the node for debugging (utility function) -/////////////////////////////////////////////////////////////////////// -void ACEND_ShowNode(int node) -{ - edict_t *ent; - -// return; // commented out for now. uncommend to show nodes during debugging, - // but too many will cause overflows. You have been warned. - - ent = G_Spawn(); - - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - - if(nodes[node].type == NODE_MOVE) - ent->s.renderfx = RF_SHELL_BLUE; - else if (nodes[node].type == NODE_WATER) - ent->s.renderfx = RF_SHELL_RED; - else - ent->s.renderfx = RF_SHELL_GREEN; // action nodes - - ent->s.modelindex = gi.modelindex ("models/items/ammo/grenades/medium/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + 60 * HZ; // 1 minute is long enough! - ent->think = G_FreeEdict; - ent->dmg = 0; - - VectorCopy(nodes[node].origin,ent->s.origin); - gi.linkentity (ent); - -} - -/////////////////////////////////////////////////////////////////////// -// Draws the current path (utility function) -/////////////////////////////////////////////////////////////////////// -void ACEND_DrawPath(edict_t *self) -{ - int current_node, goal_node, next_node; - - current_node = show_path_from; - goal_node = show_path_to; - - // RiEvEr - rewritten to use Ant system - AntStartSearch( self, current_node, goal_node); - - next_node = SLLfront(&self->pathList); - - // Now set up and display the path - while( current_node != goal_node && current_node != INVALID) - { - gi.WriteByte (svc_temp_entity); - gi.WriteByte (TE_BFG_LASER); - gi.WritePosition (nodes[current_node].origin); - gi.WritePosition (nodes[next_node].origin); - gi.multicast (nodes[current_node].origin, MULTICAST_PVS); - current_node = next_node; - SLLpop_front( &self->pathList); - next_node = SLLfront(&self->pathList); - } - -/* - next_node = path_table[current_node][goal_node]; - - // Now set up and display the path - while(current_node != goal_node && current_node != -1) - { - gi.WriteByte (svc_temp_entity); - gi.WriteByte (TE_BFG_LASER); - gi.WritePosition (nodes[current_node].origin); - gi.WritePosition (nodes[next_node].origin); - gi.multicast (nodes[current_node].origin, MULTICAST_PVS); - current_node = next_node; - next_node = path_table[current_node][goal_node]; - }*/ -} - -/////////////////////////////////////////////////////////////////////// -// Turns on showing of the path, set goal to -1 to -// shut off. (utility function) -/////////////////////////////////////////////////////////////////////// -void ACEND_ShowPath(edict_t *self, int goal_node) -{ - show_path_from = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - show_path_to = goal_node; -} - -/////////////////////////////////////////////////////////////////////// -// Add a node of type ? -/////////////////////////////////////////////////////////////////////// -int ACEND_AddNode(edict_t *self, int type) -{ - vec3_t v1,v2; - int i; - - // Block if we exceed maximum - if (numnodes + 1 > MAX_NODES) - return false; - - // Set location - VectorCopy(self->s.origin, nodes[numnodes].origin); - nodes[numnodes].origin[2] += 8; - - // Set type - nodes[numnodes].type = type; - // Set number - RiEvEr - nodes[numnodes].nodenum = numnodes; - - // Clear out the link information - RiEvEr - for( i = 0; i< MAXLINKS; i++) - { - nodes[numnodes].links[i].targetNode = INVALID; - } - - ///////////////////////////////////////////////////// - // ITEMS - // Move the z location up just a bit. - if(type == NODE_ITEM) - { - nodes[numnodes].origin[2] += 16; - numitemnodes++; - } - - // Teleporters - if(type == NODE_TELEPORTER) - { - // Up 32 - nodes[numnodes].origin[2] += 32; - } - - // Doors - if(type == NODE_DOOR) - { - vec3_t position; - // Find mid point of door max and min and put the node there - VectorClear(position); - // find center of door - position[0] = self->absmin[0] + ((self->maxs[0] - self->mins[0]) /2); - position[1] = self->absmin[1] + ((self->maxs[1] - self->mins[1]) /2); - position[2] = self->absmin[2] + 32; - // We now have to create TWO nodes, one each side of the door - // See if the 'door' is wider in the x or y direction - if( (self->absmax[0] - self->absmin[0]) > (self->absmax[1] - self->absmin[1]) ) - { - // Create in the 'y' direction - - // First node (Duplication deliberate!) - // Set type - nodes[numnodes].type = type; - // Set number - RiEvEr - nodes[numnodes].nodenum = numnodes; - // Set position - position[1] +=48; - VectorCopy(position, nodes[numnodes].origin); - - // Second node - numnodes++; - // Set type - nodes[numnodes].type = type; - // Set number - RiEvEr - nodes[numnodes].nodenum = numnodes; - // Set position - position[1] -=82; - VectorCopy(position, nodes[numnodes].origin); - } - else - { - // Create in the 'x' direction - - // First node (Duplication deliberate!) - // Set type - nodes[numnodes].type = type; - // Set number - RiEvEr - nodes[numnodes].nodenum = numnodes; - // Set position - position[0] +=48; - VectorCopy(position, nodes[numnodes].origin); - - // Second node - numnodes++; - // Set type - nodes[numnodes].type = type; - // Set number - RiEvEr - nodes[numnodes].nodenum = numnodes; - // Set position - position[0] -=82; - VectorCopy(position, nodes[numnodes].origin); - } - numnodes++; - return numnodes-1; // return the second node added - - } - - if(type == NODE_LADDER) - { - nodes[numnodes].type = NODE_LADDER; - - if(debug_mode) - { - debug_printf("Node added %d type: Ladder\n",numnodes); - ACEND_ShowNode(numnodes); - } - - numnodes++; - return numnodes-1; // return the node added - - } - - // For platforms drop two nodes one at top, one at bottom - if(type == NODE_PLATFORM) - { - VectorCopy(self->maxs,v1); - VectorCopy(self->mins,v2); - - // To get the center - nodes[numnodes].origin[0] = (v1[0] - v2[0]) / 2 + v2[0]; - nodes[numnodes].origin[1] = (v1[1] - v2[1]) / 2 + v2[1]; - nodes[numnodes].origin[2] = self->maxs[2]; - - if(debug_mode) - ACEND_ShowNode(numnodes); - - numnodes++; - - nodes[numnodes].origin[0] = nodes[numnodes-1].origin[0]; - nodes[numnodes].origin[1] = nodes[numnodes-1].origin[1]; - nodes[numnodes].origin[2] = self->mins[2]+64; - - nodes[numnodes].type = NODE_PLATFORM; - - // Add a link - //RiEvEr modified to pass in calling entity - ACEND_UpdateNodeEdge(self, numnodes, numnodes-1); - - if(debug_mode) - { - debug_printf("Node added %d type: Platform\n",numnodes); - ACEND_ShowNode(numnodes); - } - - numnodes++; - - return numnodes -1; - } - - if(debug_mode) - { - if(nodes[numnodes].type == NODE_MOVE) - debug_printf("Node added %d type: Move\n",numnodes); - else if(nodes[numnodes].type == NODE_TELEPORTER) - debug_printf("Node added %d type: Teleporter\n",numnodes); - else if(nodes[numnodes].type == NODE_ITEM) - debug_printf("Node added %d type: Item\n",numnodes); - else if(nodes[numnodes].type == NODE_WATER) - debug_printf("Node added %d type: Water\n",numnodes); - else if(nodes[numnodes].type == NODE_GRAPPLE) - debug_printf("Node added %d type: Grapple\n",numnodes); - - ACEND_ShowNode(numnodes); - } - - numnodes++; - - return numnodes-1; // return the node added -} - -// RiEvEr -//======================================= -// ReverseLink -//======================================= -// Takes the path BACK to where we came from -// and tries to link the two nodes -// This helps make good path files -// -void ACEND_ReverseLink( edict_t *self, int from, int to ) -{ - int i; - trace_t trace; - vec3_t min,max; - - if(from == INVALID || to == INVALID || from == to) - return; // safety - - // Need to trace from -> to and check heights - // if from is much lower than to, forget it - if( (nodes[from].origin[2]+32.0) < (nodes[to].origin[2]) ) - { - // May not be able to jump that high so do not allow the return link - return; - } -// VectorCopy(self->mins, min); -// if( (nodes[from].origin[2]) < (nodes[2].origin[2]) ) -// min[2] =0; // Allow for steps etc. -// VectorCopy(self->maxs, max); -// if( (nodes[from].origin[2]) > (nodes[2].origin[2]) ) -// max[2] =0; // Could be a downward sloping feature above our head - VectorCopy( vec3_origin, min); - VectorCopy( vec3_origin, max); - - - - // This should not be necessary, but I've heard that before! - // Now trace it again - trace = gi.trace( nodes[from].origin, min, max, nodes[to].origin, self, MASK_SOLID); -// trace = gi.trace( nodes[from].origin, tv(-8,-8,0), tv(8,8,0), nodes[to].origin, self, MASK_SOLID); - if( trace.fraction < 1.0) - { - // can't get there for some reason - return; - } - // Add the link - path_table[from][to] = to; - - // Checks if the link exists and then may create a new one - RiEvEr - for( i=0; i %d\n", from, to); - break; - } - } - - // Now for the self-referencing part, linear time for each link added - for(i=0;i nodes[from].origin[2]+36) - && self->is_bot - ) - { - // If we are coming from a move or jump node - if( (nodes[from].type == NODE_MOVE) || - (nodes[from].type == NODE_JUMP) ) - { - // No if the to node is the same, it's illegal - if( (nodes[to].type == NODE_MOVE) || - (nodes[to].type == NODE_JUMP) ) - { - // Too high - not possible!! - return; - } - } - } - // Do not allow creation of nodes where the falling distance would kill you! - if( (nodes[from].origin[2]) > (nodes[to].origin[2] + 180) ) - return; - -/* VectorCopy(self->mins, min); - // If going up -// if( (nodes[from].origin[2]) < (nodes[to].origin[2]) ) - min[2] = 0; // Allow for steps up etc. - VectorCopy(self->maxs, max); - // If going down -// if( (nodes[from].origin[2]) > (nodes[to].origin[2]) ) - max[2] = 0; // door node linking*/ - VectorCopy( vec3_origin, min); - VectorCopy( vec3_origin, max); - - // Now trace it - more safety stuff! - trace = gi.trace( nodes[from].origin, min, max, nodes[to].origin, self, MASK_SOLID); - - if( trace.fraction < 1.0) - { - // can't do it - if(debug_mode) - debug_printf("Warning: (NoTrace) Failed to Link %d -> %d\n", from, to); - return; - } - // Add the link - path_table[from][to] = to; - - // Checks if the link exists and then may create a new one - RiEvEr - for( i=0; i %d\n", from, to); - break; - } - } - - // Now for the self-referencing part, linear time for each link added - for(i=0;i %d\n", self->client->pers.netname, from, to); - - path_table[from][to] = INVALID; // set to invalid - - // RiEvEr - // Now we must remove the link from the antpath system - // Get the link information - for( i=0; istring); - i += sprintf(filename + i, "\\terrain\\"); - i += sprintf(filename + i, level.mapname); - i += sprintf(filename + i, ".ltk"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/terrain/"); - strcat(filename,level.mapname); - strcat(filename,".ltk"); -#endif - - // Resolve paths - ACEND_ResolveAllPaths(); - - gi.bprintf(PRINT_MEDIUM,"Saving node table..."); - -/* strcpy(filename,"action\\nav\\"); - strcat(filename,level.mapname); - strcat(filename,".nod");*/ - - if((pOut = fopen(filename, "wb" )) == NULL) - return; // bail - - fwrite(&version,sizeof(int),1,pOut); // write version - fwrite(&numnodes,sizeof(int),1,pOut); // write count - fwrite(&num_items,sizeof(int),1,pOut); // write facts count - - fwrite(nodes,sizeof(node_t),numnodes,pOut); // write nodes - - for(i=0;istring); - i += sprintf(filename + i, "\\terrain\\"); - i += sprintf(filename + i, level.mapname); - i += sprintf(filename + i, ".ltk"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/terrain/"); - strcat(filename,level.mapname); - strcat(filename,".ltk"); -#endif -/* - strcpy(filename,"action\\nav\\"); - strcat(filename,level.mapname); - strcat(filename,".nod");*/ - - if((pIn = fopen(filename, "rb" )) == NULL) - { - // Create item table - gi.bprintf(PRINT_MEDIUM, "ACE: No node file found, creating new one..."); - ACEIT_BuildItemNodeTable(false); - gi.bprintf(PRINT_MEDIUM, "done.\n"); - return; - } - - // determin version - fread(&version,sizeof(int),1,pIn); // read version - - if(version == LTK_NODEVERSION) - { - gi.bprintf(PRINT_MEDIUM,"ACE: Loading node table..."); - - fread(&numnodes,sizeof(int),1,pIn); // read count - fread(&num_items,sizeof(int),1,pIn); // read facts count - - fread(nodes,sizeof(node_t),numnodes,pIn); - - for(i=0;i numnodes) // Sanity check to avoid infinite loop. + return INVALID; + cost++; + } + + return cost; +} + +/////////////////////////////////////////////////////////////////////// +// Find a close node to the player within dist. +// +// Faster than looking for the closest node, but not very +// accurate. +/////////////////////////////////////////////////////////////////////// +int ACEND_FindCloseReachableNode(edict_t *self, int range, int type) +{ + vec3_t v; + int i; + trace_t tr; + float dist; + vec3_t maxs,mins; + + VectorCopy(self->mins,mins); + mins[2] += 16; + VectorCopy(self->maxs,maxs); + maxs[2] -= 16; + + range *= range; + + for(i=0;ibot.goal_node) + continue; + + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + //rekkie -- DEV_1 -- e + + if(type == NODE_ALL || type == nodes[i].type) // check node type + { + + VectorSubtract(nodes[i].origin,self->s.origin,v); // subtract first + + dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; + + if(dist < range) // square range instead of sqrt + { + // make sure it is visible + //AQ2 ADDED MASK_SOLID + //trace = gi.trace (self->s.origin, vec3_origin, vec3_origin, nodes[i].origin, self, MASK_SOLID|MASK_OPAQUE); + tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_ALL); + + if(tr.fraction == 1.0) + return i; + } + } + } + + return -1; +} + + +int ACEND_DistanceToTargetNode(edict_t *self) +{ + float dist; + //int node=-1; + vec3_t v; + + VectorSubtract(nodes[self->bot.goal_node].origin, self->s.origin,v); // subtract first + dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; + + return dist; +} + + +/////////////////////////////////////////////////////////////////////// +// Find the closest node to the player within a certain range +/////////////////////////////////////////////////////////////////////// +int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) +{ + // When updating current node, only update when we're touching the ground, + // otherwise if in the air it will return INVALID node because we need to be near the ground to find a node + if (self && self->bot.current_node != INVALID && self->groundentity == NULL) + return self->bot.current_node; + + int node = INVALID; + //rekkie -- DEV_1 -- s + // Try to find node using the level geometry first + /* + if (nmesh.total_reach) + { + qboolean found; + int f = INVALID; + int t = INVALID; + GetBSPFaceTriangle(self->s.origin, &found, &f, &t); + if (found && f != INVALID && t != INVALID) //f = 0; t = 0; found = true; + { + int n = nmesh.face[f].tris[t].node; + //trace_t tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), nodes[n].origin, self, MASK_PLAYERSOLID); + trace_t tr = gi.trace(self->s.origin, tv(-16,-16,STEPSIZE), tv(16,16,32), nodes[n].origin, self, MASK_PLAYERSOLID); + //Com_Printf("%s -> [%s] f[%i] t[%i] node %i at %f %f %f\n", __func__, self->client->pers.netname, f, t, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + if (n != INVALID && tr.fraction == 1) + node = nmesh.face[f].tris[t].node; // Successfully found node + } + } + */ + //Com_Printf("%s -> [%s] failed to GetBSPFaceTriangle\n", __func__, self->client->pers.netname); + + + + // Geometry lookup failed - Look for nodes by distance + + //rekkie -- DEV_1 -- e + if (node == INVALID) + { + int i; + float closest = 99999; + float dist; + vec3_t v; + trace_t tr; + float rng; + vec3_t maxs, mins; + + VectorCopy(self->mins, mins); + VectorCopy(self->maxs, maxs); + mins[2] += 18; // Stepsize + maxs[2] -= 16; // Duck a little.. + + rng = (float)(range * range); // square range for distance comparison (eliminate sqrt) + + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (type == NODE_ALL || type == nodes[i].type) // check node type + { + // Get Height Diff + float height = fabs(nodes[i].origin[2] - self->s.origin[2]); + if (height > 60) // Height difference high + continue; + + VectorSubtract(nodes[i].origin, self->s.origin, v); // subtract first + + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + + /* + // Try to see if bot is close to the next node + if (self->bot.next_node != INVALID && self->bot.next_node == i && VectorDistance(nodes[i].origin, self->s.origin) <= 64) + { + tr = gi.trace(self->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, self, MASK_PLAYERSOLID); //rekkie + if ((tr.fraction == 1.0) || + ((tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) + ) + { + //Com_Printf("%s -> [%s] is close to next_node[%d]\n", __func__, self->client->pers.netname, node); + //if (node != INVALID) + return node; + } + } + else // Otherwise just closest node + */ + { + if (dist < closest && dist < rng) + { + // make sure it is visible + //AQ2 added MASK_SOLID + //tr = gi.trace(self->s.origin, mins, maxs, nodes[i].origin, self, MASK_SOLID | MASK_OPAQUE); + tr = gi.trace(self->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, self, MASK_PLAYERSOLID); //rekkie + if ((tr.fraction == 1.0) || + ((tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) + ) + { + node = i; + closest = dist; + } + } + } + } + } + } + + //Com_Printf("%s [%s] node %d\n", __func__, self->client->pers.netname, node); + + return node; +} + +/////////////////////////////////////////////////////////////////////// +// Find the closest node to the player within a certain range that doesn't have LOS to an enemy +/////////////////////////////////////////////////////////////////////// +int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type) +{ + int i; + float closest = 99999; + float dist; + int node=-1; + vec3_t v; + trace_t tr; + float rng; + vec3_t maxs,mins; + int curplayer; + int is_safe=1; + + VectorCopy(self->mins,mins); + VectorCopy(self->maxs,maxs); + mins[2] += 18; // Stepsize + maxs[2] -= 16; // Duck a little.. + + rng = (float)(range * range); // square range for distance comparison (eliminate sqrt) + + for(i=0;is.origin,v); // subtract first + + dist = v[0]*v[0] + v[1]*v[1] + v[2]*v[2]; + + if(dist < closest && dist < rng && dist > 200) + { + // make sure it is visible + //AQ2 added MASK_SOLID + tr = gi.trace (self->s.origin, mins, maxs, nodes[i].origin, self, MASK_SOLID|MASK_OPAQUE); +// tr = gi.trace (self->s.origin, vec3_origin, vec3_origin, nodes[i].origin, self, MASK_ALL); + if( (tr.fraction == 1.0) || + ( (tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0) ) + ) + { + + is_safe=1; + for (curplayer=0; curplayerteam != self->team) && (players[curplayer]->solid != SOLID_NOT) ) + { + tr = gi.trace (self->s.origin, tv(-8,-8,-8), tv(8,8,8), players[curplayer]->s.origin, self, MASK_SOLID|MASK_OPAQUE); + if (tr.fraction == 1.0) + { + is_safe=0; + break; + } + } + + if (is_safe==1) + { + //rekkie -- DEV_1 -- s + // Pick a node which isn't our goal node (prevent bots going in circles trying to target the same safe node) + if (i == self->bot.goal_node) + break; + //rekkie -- DEV_1 -- e + + node = i; + closest = dist; + } + + } + } + } + } + + return node; +} + + +/////////////////////////////////////////////////////////////////////// +// BOT NAVIGATION ROUTINES +/////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////// +// Set up the goal +/////////////////////////////////////////////////////////////////////// +void ACEND_SetGoal(edict_t *self, int goal_node) +{ + self->bot.goal_node = goal_node; + + int nodelist[MAX_NODELIST]; + int nodes_touched; + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) + { + self->bot.current_node = nodelist[i]; + break; + } + } + + if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // BOT_MOVE_STATE_WANDER + //self->wander_timeout = level.framenum + 0.1 * HZ; + //ACEAI_PickLongRangeGoal(self); + //Com_Printf("%s %s invalid goal[%i]\n", __func__, self->client->pers.netname, self->bot.goal_node); + return; + } + + self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + self->node_timeout = 0; + + //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); //rekkie + + return; +} + +/////////////////////////////////////////////////////////////////////// +// Advances to the selected node +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_AdvanceToSelectNode(edict_t* self, int node) +{ + qboolean bot_hit_next_node = false; + if (self->bot.next_node == node) + bot_hit_next_node = true; + + do + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = self->bot.next_node; + + // Remove the front entry from the list + if (self->bot.next_node == SLLfront(&self->pathList)) + SLLpop_front(&self->pathList); + + // Get the next node - if there is one! + if (!SLLempty(&self->pathList)) + { + self->bot.next_node = SLLfront(&self->pathList); + //Com_Printf("%s reached [n:%i t:%i] next [n:%i t:%i] goal [%i]\n", self->client->pers.netname, self->bot.current_node, nodes[self->bot.current_node].type, self->bot.next_node, nodes[self->bot.next_node].type, self->bot.goal_node); + //return true; + + if (bot_hit_next_node) return true; + } + else + { + // We messed up... + //Com_Printf("%s Trying to read an empty SLL nodelist!\n", __func__); + self->bot.next_node = INVALID; + return false; + } + } while (self->bot.next_node != node); + + return true; +} +//rekkie -- goto next node -- s +/////////////////////////////////////////////////////////////////////// +// Advance to the next node, if any +/////////////////////////////////////////////////////////////////////// +qboolean NODES_AdvanceToNextNode(edict_t* self) +{ + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = self->bot.next_node; + //self->bot.next_node = path_table[self->bot.current_node][self->bot.goal_node]; + + // Remove the front entry from the list + if (self->bot.next_node == SLLfront(&self->pathList)) + SLLpop_front(&self->pathList); + + // Get the next node - if there is one! + if (!SLLempty(&self->pathList)) + { + self->bot.next_node = SLLfront(&self->pathList); + //Com_Printf("%s reached [n:%i t:%i] next [n:%i t:%i] goal [%i]\n", self->client->pers.netname, self->bot.current_node, nodes[self->bot.current_node].type, self->bot.next_node, nodes[self->bot.next_node].type, self->bot.goal_node); + return true; + } + else + { + // We messed up... + //if (debug_mode) gi.bprintf(PRINT_HIGH, "Trying to read an empty SLL nodelist!\n"); + //Com_Printf("%s Trying to read an empty SLL nodelist!\n", __func__); + self->bot.next_node = INVALID; + return false; + } + + //Com_Printf("%s\n", self->client->pers.netname); + return false; +} +//rekkie -- goto next node -- e + +//rekkie -- walknodes -- s +// Cycle through node selection/function types +// Called via a_cmds.c Cmd_New_Reload_f() function. The user can press their reload key. +// Alternatively called via BOTLIB_MouseControlNodes. The user can hold down their attack key. +void BOTLIB_ChangeNodeFunction(edict_t* ent) +{ + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_NONE) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_SELECT; // Select nodes + Com_Printf("%s Node selection changed to: Select nodes (area is [%d]) (use nav_area to change selected node area)\n", __func__, ent->bot.walknode.selection_area); + VectorClear(ent->bot.walknode.selection_start); + VectorClear(ent->bot.walknode.selection_end); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_SELECT_SMART; // Smart node selection + Com_Printf("%s Node selection changed to: Smart node selection (area is [%d]) (use nav_area to change selected node area)\n", __func__, ent->bot.walknode.selection_area); + VectorClear(ent->bot.walknode.selection_start); + VectorClear(ent->bot.walknode.selection_end); + BOTLIB_SetAllNodeNormals(); // Set all the normals + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_ADD; // Add nodes + Com_Printf("%s Node selection changed to: Add node\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_ADD) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_LINK; // Link nodes + Com_Printf("%s Node selection changed to: Link/Unlink node\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINK) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_MOVE; // Move nodes + Com_Printf("%s Node selection changed to: Move node\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_MOVE) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_TYPE; // Change node type + Com_Printf("%s Node selection changed to: Change node type\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_TYPE) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_LINKTYPE; // Change node-to-node link type + Com_Printf("%s Node selection changed to: Change node-to-node link type\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINKTYPE) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_DEL; // Delete nodes + Com_Printf("%s Node selection changed to: Delete node\n", __func__); + } + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_DEL) + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_FLOODFILL; // Flood fill + Com_Printf("%s Node selection changed to: Flood fill area with nodes\n", __func__); + } + else + { + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_NONE; // Disable interaction with nodes + Com_Printf("%s Node selection changed to: No interaction with nodes\n", __func__); + } + + // Reset highlighted nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; +} + +// Self expanding nodes attempt to expand out from the source node. +void BOTLIB_SelfExpandingNodes(edict_t* ent, int node) +{ + trace_t tr; + int node_added; // Last node added + vec3_t exp; // Expand + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + + /* + // Keep a copy of all the nodes added - Max 4096 nodes + int node_counter = 0; + int * nodes_added = (int *)malloc(sizeof(int) * 4096); + if (nodes_added == NULL) // Failed to allocate memory + return; + nodes_added[node_counter++] = node; // Add the initial node + */ + + int curr_node = node; // Current node in the list + + botlib_sll_t openList; // SLL openlist + openList.head = openList.tail = NULL; // Nullify the list ends + + SLLpush_back(&openList, node); // Current node in the list + + int nodes_touched; // Number of nodes touched + int nodelist[MAX_NODELIST]; // Nodes touched + + // While there are nodes on the OPEN list + while (SLLempty(&openList) == false) + { + // Get next node from the list + curr_node = SLLfront(&openList); + + // Safety check + if (curr_node <= INVALID) + break; + + const byte multi = 3; // Multiplier for expanding nodes + for (int i = 0; i < 8; i++) // Cycle through all 8 directions + { + VectorCopy(nodes[curr_node].origin, exp); + if (i == 0) + exp[0] += (mins[0] * multi); // Move left + else if (i == 1) + exp[0] += (maxs[0] * multi); // Move right + else if (i == 2) + exp[1] += (mins[1] * multi); // Move forward + else if (i == 3) + exp[1] += (maxs[1] * multi); // Move back + + else if (i == 4) // Move left and forward + { + exp[0] += (mins[0] * multi); + exp[1] += (mins[1] * multi); + } + else if (i == 5) // Move right and forward + { + exp[0] += (maxs[0] * multi); + exp[1] += (mins[1] * multi); + } + else if (i == 6) // Move left and back + { + exp[0] += (mins[0] * multi); + exp[1] += (maxs[1] * multi); + } + else if (i == 7) // Move right and back + { + exp[0] += (maxs[0] * multi); + exp[1] += (maxs[1] * multi); + } + + /* + else if (i == 4) + { + exp[0] += (mins[0] * 4); + exp[1] += (maxs[0] * 4); + } + else if (i == 5) + { + exp[0] += (mins[0] * 4); + exp[1] += (maxs[1] * 4); + } + else if (i == 6) + { + exp[0] += (mins[1] * 4); + exp[1] += (maxs[0] * 4); + } + else if (i == 7) + { + exp[0] += (mins[1] * 4); + exp[1] += (maxs[1] * 4); + } + */ + + if (0) + { + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(exp, tv(0, 0, 0), 8, mins, maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched == 0) + { + // Check if we fit + tr = gi.trace(exp, mins, maxs, exp, ent, MASK_PLAYERSOLID); + if (tr.startsolid == false) + { + // Try crawling down + tr = gi.trace(exp, mins, maxs, tv(exp[0], exp[1], exp[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction == 1.0 || tr.plane.normal[2] < 0.7) // + continue; // Skip + } + else // Try crawling up + { + continue; + } + + nodes_touched = BOTLIB_NodeTouchNodes(tr.endpos, tr.plane.normal, 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched == 0) + { + // Contents check + int type = NODE_MOVE; + int contents = gi.pointcontents(tv(tr.endpos[0], tr.endpos[1], tr.endpos[2] - 23)); + if (contents & MASK_WATER) + type = NODE_WATER; + + node_added = BOTLIB_AddNode(tr.endpos, tr.plane.normal, type); + if (node_added != INVALID) + { + // Add node to the openlist + SLLpush_back(&openList, node_added); + //nodes_added[node_counter++] = node_added; + + //char typename[32] = { '\0' }; // Length of the longest node type name + //NodeTypeToString(ent, nodes[node_added].type, typename, sizeof(typename)); + //Com_Printf("%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); + } + } + } + } + + if (1) + { + exp[2] += 0.01; // Move up a little + + // Check if we fit + tr = gi.trace(exp, mins, maxs, exp, ent, MASK_PLAYERSOLID); + if (tr.startsolid == false) + { + // Try crawling down + tr = gi.trace(exp, mins, maxs, tv(exp[0], exp[1], exp[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.plane.normal[2] < 0.7 || tr.surface->flags & SURF_SKY) + continue; + } + else + { + // Try crawling up + exp[2] += 24; // 57 + tr = gi.trace(exp, mins, maxs, exp, ent, MASK_PLAYERSOLID); + if (tr.startsolid) + { + // Try crawling up more + exp[2] += 24; + tr = gi.trace(exp, mins, maxs, exp, ent, MASK_PLAYERSOLID); + if (tr.startsolid) + { + // Try crawling up once more + exp[2] += 16; + tr = gi.trace(exp, mins, maxs, exp, ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + } + } + + // And then crawl back down + tr = gi.trace(exp, mins, maxs, tv(exp[0], exp[1], exp[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.plane.normal[2] < 0.7 || tr.surface->flags & SURF_SKY) + continue; + } + + // Ignore nodes that are too far on an edge + trace_t tr_edge = gi.trace(tr.endpos, NULL, NULL, tv(tr.endpos[0], tr.endpos[1], tr.endpos[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr_edge.fraction == 1.0 || tr_edge.surface->flags & SURF_SKY) + continue; + + /* + // Check for ground -60 units + tr = gi.trace(exp, NULL, NULL, tv(exp[0], exp[1], exp[2] - (24 + NODE_MAX_JUMP_HEIGHT)), ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction == 1.0) // Inside solid, or didn't touch the ground + continue; // Skip + //tr.endpos[2] += 24; + + // If the node is on a slope, raise it up depending on the slope normal and the node's mins/maxs hit box + // This is done so the node hitbox is not inside the slope face + { + vec3_t exp_up; + VectorCopy(tr.plane.normal, exp_up); + exp_up[2] = 0; + VectorNormalize(exp_up); + exp_up[2] = 1; + VectorScale(exp_up, 24, exp_up); + VectorAdd(tr.endpos, exp_up, tr.endpos); + } + + // Check if location can fit a node + tr = gi.trace(tr.endpos, tv(-16, -16, -24), tv(16, 16, 32), tr.endpos, ent, MASK_PLAYERSOLID); + if (tr.startsolid) // Inside solid, or didn't touch the ground + continue; // Skip + */ + + /* + // Slime, lava, trigger_hurt, or skybox + //trace_t tr_avoid = gi.trace(exp, VEC_ORIGIN, VEC_ORIGIN, tv(exp[0], exp[1], exp[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID | MASK_DEADLY); + trace_t tr_avoid = gi.trace(tv(exp[0], exp[1], exp[2]), VEC_ORIGIN, VEC_ORIGIN, tv(exp[0], exp[1], exp[2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID | MASK_DEADLY); + if (tr_avoid.contents & MASK_DEADLY) + { + Com_Printf("%s %s ignoring MASK_DEADLY\n", __func__, ent->client->pers.netname); + continue; // Skip + } + if (tr_avoid.ent && (tr_avoid.ent->touch == hurt_touch)) + { + Com_Printf("%s %s ignoring hurt_touch\n", __func__, ent->client->pers.netname); + continue; // Skip + } + if (tr_avoid.surface && (tr_avoid.surface->flags & SURF_SKY)) + { + Com_Printf("%s %s ignoring SURF_SKY\n", __func__, ent->client->pers.netname); + continue; // Skip + } + */ + + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(tr.endpos, tr.plane.normal, 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); // 8 + if (nodes_touched == 0) + { + // Contents check + int type = NODE_MOVE; + int contents = gi.pointcontents(tv(tr.endpos[0], tr.endpos[1], tr.endpos[2] - 23)); + if (contents & MASK_WATER) + type = NODE_WATER; + + node_added = BOTLIB_AddNode(tr.endpos, tr.plane.normal, type); + if (node_added != INVALID) + { + // Add node to the openlist + SLLpush_back(&openList, node_added); + //nodes_added[node_counter++] = node_added; + + //char typename[32] = { '\0' }; // Length of the longest node type name + //NodeTypeToString(ent, nodes[node_added].type, typename, sizeof(typename)); + //Com_Printf("%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); + } + } + } + } + + // Remove current node from the list + SLLpop_front(&openList); + } + + // Free up the memory we allocated + SLLdelete(&openList); + + + /* + // Link all the added nodes together + int from, to; + float node_to_node_dist; + if (node_counter + 1 < 4096) + for (int i = 0; i < node_counter; i++) + { + for (int j = 0; j < node_counter; j++) + { + if (i == j) + continue; + + from = nodes_added[i]; + to = nodes_added[j]; + node_to_node_dist = VectorDistance(nodes[from].origin, nodes[to].origin); + + if (node_to_node_dist <= 128) + { + tr = gi.trace(nodes[from].origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, NODE_MOVE, true)) + { + //Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, from, to, node_type); + ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + } + } + } + + if (nodes_added) + free(nodes_added); + */ +} + +// Links +void BOTLIB_LinkNodesNearbyNode(edict_t* ent, int from) +{ + trace_t tr; + int to, type; + + for (int j = 0; j < numnodes; j++) + { + if (from == j) continue; // Skip self + + to = nodes[j].nodenum; + + if (to == INVALID) continue; // Skip invalid + //if (nodes[to].num_links >= 8) continue; // Skip checking nodes with 8+ links + + if (VectorDistance(nodes[from].origin, nodes[to].origin) >= 96) continue; + + if (fabs(nodes[from].origin[2] - nodes[to].origin[2]) <= NODE_MAX_JUMP_HEIGHT) + { + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + tr = gi.trace(nodes[from].origin, tv(-16, -16, -12), tv(16, 16, 32), nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + type = BOTLIB_Reachability(from, to); + if (type != NODE_CROUCH && type != NODE_JUMPPAD) + { + type = NODE_MOVE; + + // Contents check + int contents = gi.pointcontents(tv(nodes[from].origin[0], nodes[from].origin[1], nodes[from].origin[2])); + if (contents & MASK_WATER) + type = NODE_WATER; + } + else + continue; + + //if (type != NODE_CROUCH && type != NODE_JUMPPAD) + { + if (BOTLIB_AddNodeLink(from, to, type, true)) + { + Com_Printf("%s linking node[%d -> %d] type[%d]\n", __func__, from, to, type); + + // Add reverse link + if (BOTLIB_AddNodeLink(to, from, type, true)) + Com_Printf("%s linking node[%d -> %d] type[%d]\n", __func__, from, to, type); + } + } + } + } + } + } +} + +void BOTLIB_LinkAllNodesTogether(edict_t* ent) +{ + trace_t tr; + int from, to, type; + //float height_diff; + //float node_to_node_dist_xyz; + //float node_to_node_dist_xy; + for (int i = 0; i < numnodes; i++) + { + from = nodes[i].nodenum; + if (from == INVALID) continue; // Skip invalid + if (nodes[from].num_links >= 8) continue; // Skip checking nodes with 8+ links + + for (int j = 0; j < numnodes; j++) + { + if (i == j) continue; // Skip self + + to = nodes[j].nodenum; + if (to == INVALID) continue; // Skip invalid + if (nodes[to].num_links >= 8) continue; // Skip checking nodes with 8+ links + + //if ( (node_to_node_dist_xyz = VectorDistance(nodes[from].origin, nodes[to].origin)) > 256) continue; // Skip checking distant nodes + if (VectorDistance(nodes[from].origin, nodes[to].origin) >= 96) continue; + //node_to_node_dist_xy = VectorDistanceXY(nodes[from].origin, nodes[to].origin); + + /* + height_diff = fabs(nodes[from].origin[2] - nodes[to].origin[2]); + + qboolean target_is_above = false, target_is_below = false; + float higher = 0, lower = 0; + if (nodes[from].origin[2] > nodes[to].origin[2]) // We're above the target + { + target_is_below = true; + lower = (nodes[from].origin[2] - nodes[to].origin[2]); + } + else if (nodes[from].origin[2] < nodes[to].origin[2]) // We're below the target + { + target_is_above = true; + higher = (nodes[to].origin[2] - nodes[from].origin[2]); + } + */ + + //if (node_to_node_dist_xyz <= 128 && height_diff <= NODE_MAX_JUMP_HEIGHT) + if (fabs(nodes[from].origin[2] - nodes[to].origin[2]) <= NODE_MAX_JUMP_HEIGHT) + { + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + tr = gi.trace(nodes[from].origin, tv(-16, -16, -12), tv(16, 16, 32), nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + type = BOTLIB_Reachability(from, to); + if (type != NODE_CROUCH && type != NODE_JUMPPAD) + { + type = NODE_MOVE; + + // Contents check + int contents = gi.pointcontents(tv(nodes[from].origin[0], nodes[from].origin[1], nodes[from].origin[2])); + if (contents & MASK_WATER) + type = NODE_WATER; + } + else + continue; + + //if (type != NODE_CROUCH && type != NODE_JUMPPAD) + { + if (BOTLIB_AddNodeLink(from, to, type, true)) + { + //Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, from, to, node_type); + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + } + } + /* + // Link crouching nodes + else if (node_to_node_dist_xy <= 64 && target_is_below && height_diff <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE) // 192 + { + tr = gi.trace(nodes[from].origin, tv(-16, -16, 31), tv(16, 16, 32), nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + // Contents check + int contents = gi.pointcontents(tv(nodes[from].origin[0], nodes[from].origin[1], nodes[from].origin[2] - 32)); + if (contents & MASK_WATER) + continue; + + if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_Reachability(from, to) == NODE_CROUCH) + if (BOTLIB_AddNodeLink(from, to, NODE_CROUCH, true)) + { + //Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, from, to, node_type); + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + } + // Link jumppad nodes + else if (node_to_node_dist_xy <= 256 && target_is_above && height_diff <= NODE_MAX_JUMP_HEIGHT) + { + tr = gi.trace(nodes[from].origin, tv(-16, -16, 31), tv(16, 16, 32), nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + // Contents check + int contents = gi.pointcontents(tv(nodes[from].origin[0], nodes[from].origin[1], nodes[from].origin[2] - 32)); + if (contents & MASK_WATER) + continue; + + if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_Reachability(from, to) == NODE_JUMPPAD) + if (BOTLIB_AddNodeLink(from, to, NODE_JUMPPAD, true)) + { + //Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, from, to, node_type); + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + } + */ + } + } +} + +int SortByHeight(const void* a, const void* b) +{ + float vecA[3] = { ((float*)a)[0], ((float*)a)[1], ((float*)a)[2] }; // typecast the void pointers to vec3_t pointers + float vecB[3] = { ((float*)b)[0], ((float*)b)[1], ((float*)b)[2] }; // typecast the void pointers to vec3_t pointers + + if (vecA[2] < vecB[2]) { // If the first vector is lower than the second vector in the Z-axis, it needs to be sorted to a higher position in the array. + return 1; // Return positive value signifying we want to move vecA higher in the array. + } + else if (vecA[2] > vecB[2]) { // If the first vector is higher than the second vector in the Z-axis, it needs to be sorted to a lower position in the array. + return -1; // Return negative value signifying we want to move vecA lower in the array. + } + else { + return 0; // Return 0 if the two vectors are of the same height. + } +} + +// Self expand nodes from all spawn points +void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t *ent) +{ + trace_t tr; + int nodelist[MAX_NODELIST]; + int nodes_touched; + int node_added; + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + + int sp_counter = 0; + vec3_t sp_origin[128]; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + + // Try crawling down + sp_origin[sp_counter][2] += 0.01; + tr = gi.trace(sp_origin[sp_counter], mins, maxs, tv(sp_origin[sp_counter][0], sp_origin[sp_counter][1], sp_origin[sp_counter][2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + else + sp_origin[sp_counter][2] = tr.endpos[2]; + + sp_counter++; + } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team1")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + + // Try crawling down + sp_origin[sp_counter][2] += 0.01; + tr = gi.trace(sp_origin[sp_counter], mins, maxs, tv(sp_origin[sp_counter][0], sp_origin[sp_counter][1], sp_origin[sp_counter][2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + else + sp_origin[sp_counter][2] = tr.endpos[2]; + + sp_counter++; + } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team2")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + + // Try crawling down + sp_origin[sp_counter][2] += 0.01; + tr = gi.trace(sp_origin[sp_counter], mins, maxs, tv(sp_origin[sp_counter][0], sp_origin[sp_counter][1], sp_origin[sp_counter][2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + else + sp_origin[sp_counter][2] = tr.endpos[2]; + + sp_counter++; + } + + // Qsort in order of highest to lowest z value + qsort(sp_origin, sp_counter, sizeof(vec3_t), SortByHeight); + + // Show custom user spawn points + //if (dc_sp_count && dc_sp != NULL) + { + //for (int i = 0; i < dc_sp_count; i++) + for (int i = 0; i < sp_counter; i++) + { + //if (dc_sp[i].inuse) // Ignore deleted spawn spots + { + /* + // Add node to spawn point - ensure it touched the ground and not touching another node + trace_t tr = gi.trace(tv(sp_origin[i][0], sp_origin[i][1], sp_origin[i][2] + 1), mins, maxs, tv(sp_origin[i][0], sp_origin[i][1], sp_origin[i][2] - 2), ent, MASK_PLAYERSOLID); + if (tr.fraction == 1) // If up in the air (like one of the teamjungle spawnpoints) + { + // Find ground + tr = gi.trace(sp_origin[i], mins, maxs, tv(sp_origin[i][0], sp_origin[i][1], sp_origin[i][2] - 256), ent, MASK_PLAYERSOLID); + if (tr.fraction < 1 && tr.startsolid == false) + { + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(tr.endpos, tr.plane.normal, 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched == 0) + { + node_added = BOTLIB_AddNode(tr.endpos, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + BOTLIB_SelfExpandingNodes(NULL, node_added); + } + } + } + else // Most spawn points need no adjustment; node can use spawn origin :) + */ + { + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[i], tv(0, 0, 0), 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched == 0) + { + // Contents check + int type = NODE_MOVE; + int contents = gi.pointcontents(tv(sp_origin[i][0], sp_origin[i][1], sp_origin[i][2] - 24)); + if (contents & MASK_WATER) + type = NODE_WATER; + + node_added = BOTLIB_AddNode(sp_origin[i], tv(0, 0, 0), type); + if (node_added != INVALID) + BOTLIB_SelfExpandingNodes(ent, node_added); + } + } + } + } + } +} + +//rekkie -- debug drawing -- s +#if DEBUG_DRAWING +// Use mouse to control node: link, move, add, delete, etc +void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) +{ + const int grid_size = 2; // When adding/moving nodes, align their XY coordinates to a grid + int nodelist[MAX_NODELIST]; + int nodes_touched; + char type_name[64] = { '\0' }; // Length of the node type name + int from, to; + + int i; + int latched_buttons = ent->client->latched_buttons; + int oldbuttons = ent->client->buttons; + int buttons = ucmd->buttons; + latched_buttons |= buttons & ~oldbuttons; + + /* + // Press and hold down the attack button to change highlight functionality + // This is an alternative to pressing the 'reload' key + if (ucmd->buttons & BUTTON_ATTACK) + { + ent->bot.walknode.highlighted_counter++; // Counter + + // Reset highlighted_counter + if (ent->bot.walknode.highlighted_time < level.framenum) + { + ent->bot.walknode.highlighted_time = level.framenum + 1 * HZ; + ent->bot.walknode.highlighted_counter = 0; + } + + if (ent->bot.walknode.highlighted_counter >= 50) // Only change functionality once every 50 frames (0.5 seconds) + { + // Reset timer and counter + ent->bot.walknode.highlighted_time = level.framenum + 1 * HZ; + ent->bot.walknode.highlighted_counter = 0; + + // Reset highlighted nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + + // Change selection type + BOTLIB_ChangeNodeFunction(ent); + } + } + */ + + // Highlight type is move and we've got a node selected. Show an example of where the node will be, if successfully moved + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_MOVE && ent->bot.walknode.highlighted_node != INVALID) + { + int node = ent->bot.walknode.highlighted_node; + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = ent->client->pers.draw->DrawBox; + + + // Project trace from the player weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, ent->viewheight); + G_ProjectSource(ent->s.origin, offset, forward, right, start); + VectorMA(start, 8192, forward, end); + trace_t tr; + tr = gi.trace(start, nodes[node].mins, nodes[node].maxs, end, ent, MASK_SOLID); + + // Try to align XY to a grid, otherwise just use the hit location + int iorigin = 0; // Int based origin vec + vec3_t grid_origin; + VectorCopy(tr.endpos, grid_origin); + iorigin = (int)grid_origin[0]; // Drop the decimal + iorigin = iorigin - (iorigin % grid_size); // Align to grid + grid_origin[0] = (float)iorigin; // Convert back to float + iorigin = (int)grid_origin[1]; // Drop the decimal + iorigin = iorigin - (iorigin % grid_size); // Align to grid + grid_origin[1] = (float)iorigin; // Convert back to float + tr = gi.trace(grid_origin, nodes[node].mins, nodes[node].maxs, grid_origin, ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1.0) // Failed to find grid location, so just use hit location + VectorCopy(tr.endpos, grid_origin); + + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(grid_origin, tr.plane.normal, 0, nodes[node].mins, nodes[node].maxs, nodelist, MAX_NODELIST, node); + if (nodes_touched == 0) // Position OKAY + { + DrawBox(node, grid_origin, MakeColor(0, 255, 0, 255), nodes[node].mins, nodes[node].maxs, 200); // Draw node box + + VectorCopy(grid_origin, nodes[node].origin); // Move node + + VectorAdd(nodes[node].origin, nodes[node].mins, nodes[node].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[node].origin, nodes[node].maxs, nodes[node].absmax); // Update absolute box min/max in the world + } + else + DrawBox(node, grid_origin, MakeColor(255, 0, 0, 255), nodes[node].mins, nodes[node].maxs, 200); // Draw RED node box + + /* + if (tr.startsolid) // Position BAD + DrawBox(node, grid_origin, MakeColor(255, 0, 0, 255), nodes[node].mins, nodes[node].maxs, 200); // Draw RED node box + else if (nodes_touched == 0) // Position OKAY + { + DrawBox(node, grid_origin, MakeColor(0, 255, 0, 255), nodes[node].mins, nodes[node].maxs, 200); // Draw node box + + VectorCopy(grid_origin, nodes[node].origin); // Move node + + VectorAdd(nodes[node].origin, nodes[node].mins, nodes[node].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[node].origin, nodes[node].maxs, nodes[node].absmax); // Update absolute box min/max in the world + } + */ + } + + /* + // Select + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT) + { + if ((latched_buttons & BUTTON_ATTACK) == 0) + { + if (VectorEmpty(ent->bot.walknode.selection_start) == false && VectorEmpty(ent->bot.walknode.selection_end) == true) + { + // Project trace from the player weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, ent->viewheight); + G_ProjectSource(ent->s.origin, offset, forward, right, start); + VectorMA(start, 8192, forward, end); + trace_t tr; + tr = gi.trace(start, NULL, NULL, end, ent, MASK_PLAYERSOLID); + + VectorCopy(tr.endpos, ent->bot.walknode.selection_start); // Set the end point + } + } + } + */ + + if (ucmd->buttons & BUTTON_ATTACK) // When attack button is held down (keeps triggering while depressed) + { + // Project trace from the player weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, ent->viewheight); + G_ProjectSource(ent->s.origin, offset, forward, right, start); + VectorMA(start, 8192, forward, end); + trace_t tr; + //tr = gi.trace(start, tv(-16,-16,-24), tv(16,16,32), end, ent, MASK_PLAYERSOLID); + tr = gi.trace(start, NULL, NULL, end, ent, MASK_PLAYERSOLID); + + tr.endpos[2] += 28; // half the height of a node - [player size (32 + 24) / 2] which is [56 / 2] == 28; + + // Select + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + ent->bot.walknode.selection_node_count = 0; + + if (latched_buttons & BUTTON_ATTACK) // When attack button is initially depressed (single trigger) + { + + VectorCopy(tr.endpos, ent->bot.walknode.selection_start); // Set the selection start point + + // Reset highest and lowest points selected + ent->bot.walknode.selection_max = -99999; + ent->bot.walknode.selection_min = 99999; + + // Remember the first node selected + //ent->bot.walknode.selection_node_first = INVALID; + } + else // When attack button is held down (keeps triggering while depressed) + { + VectorCopy(tr.endpos, ent->bot.walknode.selection_end); // Set the selection end point + + // Set the min/max - the lowest and highest selection + if (tr.endpos[2] > ent->bot.walknode.selection_max) + ent->bot.walknode.selection_max = tr.endpos[2]; + else if (tr.endpos[2] < ent->bot.walknode.selection_min) + ent->bot.walknode.selection_min = tr.endpos[2]; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + if (ent->bot.walknode.selection_start[0] < ent->bot.walknode.selection_end[0]) + { + absmin[0] = ent->bot.walknode.selection_start[0]; + absmax[0] = ent->bot.walknode.selection_end[0]; + } + else + { + absmin[0] = ent->bot.walknode.selection_end[0]; + absmax[0] = ent->bot.walknode.selection_start[0]; + } + if (ent->bot.walknode.selection_start[1] < ent->bot.walknode.selection_end[1]) + { + absmin[1] = ent->bot.walknode.selection_start[1]; + absmax[1] = ent->bot.walknode.selection_end[1]; + } + else + { + absmin[1] = ent->bot.walknode.selection_end[1]; + absmax[1] = ent->bot.walknode.selection_start[1]; + } + + /* + if (ent->bot.walknode.selection_start[2] < ent->bot.walknode.selection_end[2]) + { + absmin[2] = ent->bot.walknode.selection_start[2]; + absmax[2] = ent->bot.walknode.selection_end[2]; + } + else + { + absmin[2] = ent->bot.walknode.selection_end[2]; + absmax[2] = ent->bot.walknode.selection_start[2]; + } + */ + + if (ent->bot.walknode.selection_min > 99990 || ent->bot.walknode.selection_max < -99990) + { + absmin[2] = ent->bot.walknode.selection_start[2]; + absmax[2] = ent->bot.walknode.selection_start[2]; + } + else + { + absmin[2] = ent->bot.walknode.selection_min; + absmax[2] = ent->bot.walknode.selection_max; + } + + + //absmin[2] -= NODE_SIZE; + //absmax[2] += NODE_SIZE; + + // Remember the first node selected + ent->bot.walknode.selection_node_first = INVALID; + { + vec3_t absmin_2, absmax_2; + VectorCopy(ent->bot.walknode.selection_start, absmin_2); + VectorCopy(ent->bot.walknode.selection_start, absmax_2); + absmin_2[0] -= 16; + absmin_2[1] -= 16; + //absmin_2[2] -= NODE_SIZE; + + absmax_2[0] += 16; + absmax_2[1] += 16; + //absmax_2[2] += NODE_SIZE; + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin_2, absmax_2)) // Do boxes intersect? + { + //if (nodes[i].area > 0) + { + ent->bot.walknode.selection_node_first = i; + //Com_Printf("%s %s [area: %d]\n", __func__, ent->client->pers.netname, nodes[i].area); + break; + } + } + } + } + + if (ent->bot.walknode.selection_node_first != INVALID && ent->bot.walknode.selection_min < 99998 && ent->bot.walknode.selection_max > -99998) + { + // Copy all the nodes we're selecting + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? + { + if (ent->bot.walknode.selection_node_count >= MAX_NAV_AREAS_NODES) + break; + + // Check if node already included in selection + qboolean added = false; + for (int s = 0; s < ent->bot.walknode.selection_node_count; s++) + { + if (ent->bot.walknode.selection_nodes[s] == i) + { + added = true; + break; + } + } + if (added) + continue; // Skip adding because we already have it + + ent->bot.walknode.selection_nodes[ent->bot.walknode.selection_node_count] = i; + ent->bot.walknode.selection_node_count++; + + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + BOTLIB_GroupConnectedNodeArea(ent, ent->bot.walknode.selection_nodes[ent->bot.walknode.selection_node_count - 1]); + } + } + } + } + + // Inform user of how many nodes are selected - only update upon change + if (ent->bot.walknode.selection_node_count && ent->bot.walknode.selected_node_count_prev != ent->bot.walknode.selection_node_count) + { + ent->bot.walknode.selected_node_count_prev = ent->bot.walknode.selection_node_count; // Update change + + if (ent->bot.walknode.selection_node_first == INVALID) + Com_Printf("%s %s nodes selected %d [area: INVALID]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count); + else + Com_Printf("%s %s nodes selected %d [area: %d]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count, nodes[ent->bot.walknode.selection_node_first].area); + } + } + } + } + + if (latched_buttons & BUTTON_ATTACK) + { + // Project trace from the player weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, ent->viewheight); + G_ProjectSource(ent->s.origin, offset, forward, right, start); + VectorMA(start, 8192, forward, end); + trace_t tr; + //tr = gi.trace(start, tv(-16,-16,-24), tv(16,16,32), end, ent, MASK_PLAYERSOLID); + tr = gi.trace(start, NULL, NULL, end, ent, MASK_PLAYERSOLID); + + // Check if we're touching a node at the traced endpos + int node = BOTLIB_TraceNodeBoxLine(ent->s.origin, tr.endpos, VEC_ORIGIN, VEC_ORIGIN); + if (node != INVALID) + { + // Move + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_MOVE && ent->bot.walknode.highlighted_node == INVALID) + { + ent->bot.walknode.highlighted_node = node; + return; + } + + // Delete + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_DEL && ent->bot.walknode.highlighted_node == INVALID) + { + ent->bot.walknode.highlighted_node = node; + } + + // Link type + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINKTYPE) + { + if (ent->bot.walknode.highlighted_node == INVALID) + { + ent->bot.walknode.highlighted_node = node; + return; + } + if (ent->bot.walknode.prev_highlighted_node == INVALID) + { + if (ent->bot.walknode.highlighted_node == node) // Same node selected twice + { + // Unselect nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + ent->bot.walknode.prev_highlighted_node = ent->bot.walknode.highlighted_node; + ent->bot.walknode.highlighted_node = node; + + // When first selecting the link type: inform the player of the current link type + from = ent->bot.walknode.prev_highlighted_node; + to = ent->bot.walknode.highlighted_node; + + // Check if a link exists between the two nodes + for (i = 0; i < nodes[from].num_links; i++) + { + if (nodes[from].links[i].targetNode == to) + { + // Print curent link type + NodeTypeToString(ent, nodes[from].links[i].targetNodeType, type_name, sizeof(type_name)); + Com_Printf("%s %s current link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + return; + } + } + } + } + + // Link + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINK) + { + if (ent->bot.walknode.highlighted_node == INVALID) + { + ent->bot.walknode.highlighted_node = node; + return; + } + if (ent->bot.walknode.prev_highlighted_node == INVALID) + { + if (ent->bot.walknode.highlighted_node == node) // Same node selected twice + { + // Unselect nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + ent->bot.walknode.prev_highlighted_node = ent->bot.walknode.highlighted_node; + ent->bot.walknode.highlighted_node = node; + } + } + + // When first selecting the node type: inform the player of the current node type + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_TYPE && ent->bot.walknode.highlighted_node == INVALID) + { + // Print current node type + NodeTypeToString(ent, nodes[node].type, type_name, sizeof(type_name)); + Com_Printf("%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); + + // Print all links from this node to other nodes + for (i = 0; i < nodes[node].num_links; i++) + { + NodeTypeToString(ent, nodes[node].links[i].targetNodeType, type_name, sizeof(type_name)); + Com_Printf("%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); + } + + ent->bot.walknode.highlighted_node = node; + ent->bot.walknode.prev_highlighted_node = node; + //if (ent->bot.walknode.prev_highlighted_node != ent->bot.walknode.highlighted_node) + // ent->bot.walknode.prev_highlighted_node = node; + return; + } + + //Com_Printf("%s [%d] [LINK] Node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); + } + else // No node touched + { + //Com_Printf("%s [%d] [LINK] No node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); + + // Unselect nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + if (ent->bot.walknode.highlighted_node_type != HIGHLIGHTED_NODE_ADD && ent->bot.walknode.highlighted_node_type != HIGHLIGHTED_NODE_FLOODFILL) + return; + } + + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(tr.endpos, tr.plane.normal, 0, tv(-16, -16, -24), tv(16, 16, 32), nodelist, MAX_NODELIST, node); + + // Add node + if ((ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_ADD || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_FLOODFILL) && nodes_touched == 0) // No node touched + { + tr = gi.trace(start, tv(-16, -16, -24), tv(16, 16, 32), end, ent, MASK_PLAYERSOLID); + + // Try to align XY to a grid, otherwise just use the hit location + int iorigin = 0; // Int based origin vec + vec3_t grid_origin; + VectorCopy(tr.endpos, grid_origin); + iorigin = (int)grid_origin[0]; // Drop the decimal + iorigin = iorigin - (iorigin % grid_size); // Align to grid + grid_origin[0] = (float)iorigin; // Convert back to float + iorigin = (int)grid_origin[1]; // Drop the decimal + iorigin = iorigin - (iorigin % grid_size); // Align to grid + grid_origin[1] = (float)iorigin; // Convert back to float + tr = gi.trace(grid_origin, tv(-16, -16, -24), tv(16, 16, 32), grid_origin, ent, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1.0) // Failed to find grid location, so just use hit location + VectorCopy(tr.endpos, grid_origin); + + // Check if new location touches another node + nodes_touched = BOTLIB_NodeTouchNodes(grid_origin, tr.plane.normal, 0, tv(-16, -16, -24), tv(16, 16, 32), nodelist, MAX_NODELIST, INVALID); + if (nodes_touched == 0) + { + int node_added = BOTLIB_AddNode(grid_origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + //char typename[32] = { '\0' }; // Length of the longest node type name + //NodeTypeToString(ent, nodes[node_added].type, typename, sizeof(typename)); + //Com_Printf("%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); + + // Try to self expand nodes outward from this node + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_FLOODFILL) + { + BOTLIB_SelfExpandingNodes(ent, node_added); + //BOTLIB_LinkAllNodesTogether(ent); + } + } + } + // Try to self expand nodes outward from this node + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_FLOODFILL) + { + BOTLIB_LinkAllNodesTogether(ent); + } + return; + } + + // Move + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_MOVE) + { + // Unselect node + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + // Delete + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_DEL && ent->bot.walknode.highlighted_node != INVALID) + { + BOTLIB_RemoveAllNodeLinksFrom(ent->bot.walknode.highlighted_node); // Remove all links to and from this node + nodes[ent->bot.walknode.highlighted_node].inuse = false; // Flags as unused + VectorClear(nodes[ent->bot.walknode.highlighted_node].origin); // Clear origin + + // Unselect node + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + // Change node type + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_TYPE && ent->bot.walknode.highlighted_node != INVALID)// + { + if (node == INVALID) // If unselecting, nodes will be invalid, so unhighlight and return + { + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + // Player changed to a new node from the previous node: inform the player of the current node type + if (ent->bot.walknode.highlighted_node != node) + { + // Print current node type + NodeTypeToString(ent, nodes[node].type, type_name, sizeof(type_name)); + Com_Printf("%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); + + // Print all links from this node to other nodes + for (i = 0; i < nodes[node].num_links; i++) + { + NodeTypeToString(ent, nodes[node].links[i].targetNodeType, type_name, sizeof(type_name)); + Com_Printf("%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); + } + + ent->bot.walknode.highlighted_node = node; + ent->bot.walknode.prev_highlighted_node = node; + return; + } + + // Player wishes to change type: a limited selection here so we don't go through the entire list. + int nodetype = (nodes[node].type + 1); // Increment type so the switch statement will use the next type in the nodetype_t enum + switch (nodetype) { + case NODE_JUMPPAD: + nodetype = NODE_JUMPPAD; // NODE_JUMPPAD enum == 2 + break; + case NODE_LADDER: + nodetype = NODE_LADDER; // NODE_LADDER enum == 3 + break; + case NODE_WATER: + nodetype = NODE_WATER; // NODE_WATER enum == 4 + break; + case NODE_CROUCH: + nodetype = NODE_CROUCH; // NODE_CROUCH enum == 5 + break; + case NODE_BOXJUMP: + nodetype = NODE_BOXJUMP; // NODE_BOXJUMP enum == 6 + break; + case NODE_POI: + nodetype = NODE_POI; // NODE_POI enum == 7 + break; + default: + nodetype = NODE_MOVE; // NODE_MOVE enum == 1 + break; + } + + //char typename[32] = { '\0' }; // Length of the longest node type name + if (NodeTypeToString(ent, nodetype, type_name, sizeof(type_name))) // Only change if valid + { + + if (nodetype == NODE_CROUCH) // Update crouch min/max + { + nodes[node].mins[0] = -16; + nodes[node].mins[1] = -16; + nodes[node].mins[2] = -24; + nodes[node].maxs[0] = 16; + nodes[node].maxs[1] = 16; + nodes[node].maxs[2] = CROUCHING_MAXS2; + } + else if (nodetype == NODE_BOXJUMP) // Update boxjump move min/max + { + nodes[node].mins[0] = -8; + nodes[node].mins[1] = -8; + nodes[node].mins[2] = -12; + nodes[node].maxs[0] = 8; + nodes[node].maxs[1] = 8; + nodes[node].maxs[2] = 16; + } + else // Update all other node min/max to default + { + nodes[node].mins[0] = -16; + nodes[node].mins[1] = -16; + nodes[node].mins[2] = -24; + nodes[node].maxs[0] = 16; + nodes[node].maxs[1] = 16; + nodes[node].maxs[2] = 32; + } + VectorAdd(nodes[node].origin, nodes[node].mins, nodes[node].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[node].origin, nodes[node].maxs, nodes[node].absmax); // Update absolute box min/max in the world + + BOTLIB_RemoveAllNodeLinksFrom(nodes[node].nodenum); // Remove all links to and from this node + + nodes[node].type = nodetype; // Update the node type + Com_Printf("%s Changed node to type: %s\n", __func__, type_name); + } + return; + } + + // Link type + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINKTYPE && ent->bot.walknode.highlighted_node != INVALID && ent->bot.walknode.prev_highlighted_node != INVALID) + { + from = ent->bot.walknode.prev_highlighted_node; + to = ent->bot.walknode.highlighted_node; + + if (node != to) // Changed target node, so unselect + { + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + + // Check if a link exists between the two nodes + for (i = 0; i < nodes[from].num_links; i++) + { + if (nodes[from].links[i].targetNode == to) + { + // Change link type + int linktype = (nodes[from].links[i].targetNodeType + 1); // Increment type so the switch statement will use the next type in the nodetype_t enum + switch (linktype) { + case NODE_MOVE: + linktype = NODE_MOVE; + break; + case NODE_JUMPPAD: + linktype = NODE_JUMPPAD; + break; + case NODE_LADDER: + linktype = NODE_LADDER; + break; + case NODE_WATER: + linktype = NODE_WATER; + break; + case NODE_CROUCH: + linktype = NODE_CROUCH; + break; + case NODE_BOXJUMP: + linktype = NODE_BOXJUMP; + break; + case NODE_POI: + linktype = NODE_POI; + case NODE_POI_LOOKAT: + linktype = NODE_POI_LOOKAT; // NODE_POI_LOOKAT enum == 7 + break; + default: + linktype = NODE_MOVE; // NODE_MOVE enum == 1 + break; + } + + if (NodeTypeToString(ent, linktype, type_name, sizeof(type_name))) // Only change if valid + { + Com_Printf("%s %s changing link from node[%d -> %d] to type [%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + nodes[from].links[i].targetNodeType = linktype; // Update the link type + } + return; + } + } + } + + // Link + else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINK && ent->bot.walknode.highlighted_node != INVALID && ent->bot.walknode.prev_highlighted_node != INVALID) + { + from = ent->bot.walknode.prev_highlighted_node; + to = ent->bot.walknode.highlighted_node; + + // Check if link already exists, if so remove link + for (i = 0; i < nodes[from].num_links; i++) + { + if (nodes[from].links[i].targetNode == to) + { + // Unselect node + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + + // Print removing link + NodeTypeToString(ent, nodes[from].links[i].targetNodeType, type_name, sizeof(type_name)); + Com_Printf("%s %s removing link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + // Remove link + ACEND_RemoveNodeEdge(ent, from, to); // Removes one side of the link + return; + } + } + + // Link ladder nodes + if (nodes[from].type == NODE_LADDER && nodes[to].type == NODE_LADDER) + { + int node_type = NODE_LADDER; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + + // Link water nodes + if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) + { + int node_type = NODE_WATER; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + + // Link crouch nodes + else if (nodes[from].type == NODE_CROUCH) + { + int node_type = NODE_CROUCH; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + + // Link crouch drop nodes + else if (BOTLIB_Reachability(from, to) == NODE_CROUCH) + { + int node_type = NODE_CROUCH; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + /* + // Link jump nodes + else if (BOTLIB_Reachability(from, to) == NODE_JUMP) + { + int node_type = NODE_JUMP; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + */ + // Link jumppad nodes + else if (BOTLIB_Reachability(from, to) == NODE_JUMPPAD) + { + int node_type = NODE_JUMPPAD; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + // Link move nodes + else if (nodes[from].type == NODE_MOVE || nodes[to].type == NODE_MOVE) + { + int node_type = NODE_MOVE; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + // Link small move nodes + else if (nodes[from].type == NODE_BOXJUMP || nodes[to].type == NODE_BOXJUMP) + { + int node_type = NODE_BOXJUMP; + //if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + // Print adding link + NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); + Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + + ////ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + + // Unselect nodes + ent->bot.walknode.highlighted_node = INVALID; + ent->bot.walknode.prev_highlighted_node = INVALID; + return; + } + } +} +#endif +//rekkie -- debug drawing -- e + +// Test reachability between two nodes +int BOTLIB_Reachability(int from, int to) +{ + trace_t tr; + const float max_dist = 1024; // Absolute max distance - 470 + qboolean is_gap = false; // If there's a gap between origin and target + + // Sanity checks + if (from == to || from == INVALID || to == INVALID) + return INVALID; + + vec3_t origin, target; + VectorCopy(nodes[from].origin, origin); + VectorCopy(nodes[to].origin, target); + + // Calculate the distance to the target + vec3_t dist; + vec3_t velocity; + VectorSubtract(target, origin, dist); + float xyz_distance = VectorLength(dist); // XYZ distance + dist[2] = 0; + float xy_distance = VectorLength(dist); // XY Distance + + // Max distance + if (xyz_distance > max_dist) // Ignore very far nodes + return INVALID; + + // Get direction + vec3_t dir; + VectorSubtract(target, origin, dir); // (end - start) = dir + //VectorNormalize(dir); // Normalize direction vector (must be done after VectorLength otherwise we don't get the correct distance) + + // Test for gaps/holes between the current and next node + // Each tested is conducted x units apart + { + vec3_t pos; + VectorCopy(origin, pos); + + float tested_distance = 4; + float normalized_dist = tested_distance / xyz_distance; // Normalized distance + while (tested_distance + 1 < xyz_distance) + { + tested_distance += 4; // Move next test forward + VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; + DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), nodes[from].mins, nodes[from].maxs, 500); // Draw node box + //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + } +#endif + //rekkie -- debug drawing -- e + + // Check position to see if we hit any solids along the way + //tr = gi.trace(end, tv(-12, -12, -8), tv(12, 12, 8), end, g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); + //if (tr.startsolid) is_gap = true; + //if (tr.startsolid) continue; + + tr = gi.trace(pos, tv(-16, -16, -1), tv(16, 16, 1), tv(pos[0], pos[1], -4096), g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); // Trace down until we hit the ground + if (tr.startsolid) continue; + + if ( fabs(pos[2] - tr.endpos[2]) > (61)) // Greater than max jump height //STEPSIZE + { + //Com_Printf("%s moved[%f] height[%f] xyz_distance[%f] -- FOUND GAP\n", __func__, tested_distance, fabs(pos[2] - tr.endpos[2]), xyz_distance); + is_gap = true; + break; + } + } + } + + // Calculate max jump height + // If on flat ground the max is 60 (NODE_MAX_JUMP_HEIGHT) + // If on slope up, the max is 150 depending on the angle of the slope, higher angle = higher max jump height + float z_height_max = NODE_MAX_JUMP_HEIGHT; + //float max_speed = 577; //(750 / 1.3 = 577) + /* + if (normal[0] != 1.0 && normal[1] != 1.0) + { + float z_height_max_x = 1.0; + float z_height_max_y = 1.0; + if (normal[0] >= MAX_STEEPNESS) // Player is standing on a slope going up //else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + { + z_height_max_x += (1 - normal[0]); + } + if (normal[1] >= MAX_STEEPNESS) + { + z_height_max_y += (1 - normal[1]); + } + z_height_max = (z_height_max_x + z_height_max_y) / 2; + if (z_height_max > 1.3) + z_height_max = 1.3; + else if (z_height_max < 1.0) + z_height_max = 1.0; + + z_height_max *= 116; // Max jump height is 150, and max z_height is 1.3 ... so 116 * 1.3 = 150 (150/1.3 = 116) + if (z_height_max > 150) + z_height_max = 150; + + //max_speed *= z_height_max; + } + */ + + qboolean target_is_above = false, target_is_below = false, target_is_equal = false; + float higher = 0, lower = 0; + if (origin[2] > target[2]) // We're above the target + { + target_is_below = true; + lower = (origin[2] - target[2]); + } + else if (origin[2] < target[2]) // We're below the target + { + target_is_above = true; + higher = (target[2] - origin[2]); + } + else + target_is_equal = true; + + + + // Can drop while standing + if (target_is_below && xy_distance < 128) //256, 228 + { + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower > 32 && lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH; // Can drop while crouching but will take some minor leg damage + } + } + + /* + // Dropping down to target + // ----------------------- + // Can drop while standing + if (target_is_below && xy_distance < 228) //256 + { + if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_STAND_DROP; // Can drop while standing + } + + // Crouch drop down + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH_DROP; // Can drop but need to crouch + } + + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_UNSAFE_DROP; // Can drop while crouching but will take some minor leg damage + } + } + */ + + //if (xyz_distance <= 128 && is_gap) + // return NODE_JUMP; + + + //if (xyz_distance >= 64 && is_gap) + // return NODE_JUMPPAD; + + if (xyz_distance >= 32 && is_gap) + { + float gravity = sv_gravity->value; // This doesn't take into account the ent's own gravity (ent->gravity) + float jump_height = sqrt(2 * gravity * xyz_distance); // Calculate the jump height to get to the target + float time = xyz_distance / (jump_height / 2.0); // Calculate the time it will take to get to the target + VectorScale(dist, 1.0 / time, velocity); // Calculate the velocity at the end of the jump + velocity[2] = jump_height / 2.0; + + // Test if speed is too high + qboolean speed_within_spec = false; + float required_speed = VectorLength(velocity); + if (target[2] > origin[2]) + { + //Com_Printf("%s ABOVE speed[%f] jump_height[%f]\n", __func__, required_speed, jump_height); + //if (required_speed < 550) + speed_within_spec = true; + } + else if (abs(origin[2] - target[2]) <= 8) // target is +/- 8 + { + //Com_Printf("%s EQUAL speed[%f] jump_height[%f]\n", __func__, required_speed, jump_height); + //if (required_speed < 600) + speed_within_spec = true; + } + else + { + //Com_Printf("%s BELOW speed[%f] jump_height[%f]\n", __func__, required_speed, jump_height); + //if (required_speed < 750) + speed_within_spec = true; + } + + // Result + if (speed_within_spec) + return NODE_JUMPPAD; + else + return INVALID; + } + + + // Large jump up to target + // -------------------- + // Jumppad nodes + //if (target_is_above && distance > STEPSIZE) + if (xyz_distance >= 64 && is_gap) + { + float gravity = sv_gravity->value; // This doesn't take into account the ent's own gravity (ent->gravity) + + //distance *= 1.0; // Increasing this increases the jump height + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * gravity * xyz_distance); + float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point + + // Move the middle point up to the jump height (maximum parabola height) + //end_50[2] += NODE_Z_HEIGHT_PLUS_STEPSIZE + jump_height; + // Next test from start to mid, then mid to end + //tr_25 = gi.trace(origin, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + //tr_75 = gi.trace(target, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + // If the path from [start -> mid jump -> end] is clear of obsticles, then allow the jump + //if (tr_25.fraction == 1.0 && tr_75.fraction == 1.0) + + // Do we have room to jump without hitting our head? + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -6), tv(16, 16, 48), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT) , g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), NULL, NULL, tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -24), tv(16, 16, 32), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(origin, tv(-16, -16, -24), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(origin, tv(-16, -16, 0), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //if (tr.fraction == 1.0 && !tr.startsolid) + { + //Com_Printf("jump_height[%f] jump_height_headroom[%f]\n", jump_height, jump_height_headroom); + + // Calculate the time it will take to get to the target + float time = xyz_distance / (jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = jump_height / 2.0; + + // + // If the target is above the player, increase the velocity to get to the target + float z_height = 0; + + // Above + if (target[2] > origin[2]) + { + z_height = (target[2] - origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + + float speed = VectorLength(velocity); + + Com_Printf("%s ABOVE speed[%f] z_height[%f] z_height_max[%f] jump_height[%f]\n", __func__, speed, z_height, (z_height_max + NODE_Z_HALF_HEIGHT), jump_height); + + //if (speed < 550 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) + if (speed < 550 && z_height <= z_height_max + sqrtf(xy_distance) && jump_height <= 450) + return NODE_JUMPPAD; + } + // Roughly equal + else if (abs(origin[2] - target[2]) <= 8) // target node is +/- 8 + { + float speed = VectorLength(velocity); + + Com_Printf("%s EQUAL speed[%f] jump_height[%f]\n", __func__, speed, jump_height); + + if (speed < 600 && z_height < z_height_max + NODE_Z_HEIGHT && jump_height <= 750) + return NODE_JUMPPAD; + } + // Below + else + { + z_height = (origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + + float speed = VectorLength(velocity); + + Com_Printf("%s BELOW speed[%f] z_height[%f] z_height_max[%f] jump_height[%f]\n", __func__, speed, z_height, (z_height_max + NODE_Z_HALF_HEIGHT), jump_height); + + if (speed < 750 && z_height < z_height_max + NODE_Z_HEIGHT && jump_height <= 965) + return NODE_JUMPPAD; + } + } + } + + return INVALID; +} +//rekkie -- Walknodes -- e + + +// Move closer to goal by pointing the bot to the nearest next node that is closer to the goal +qboolean BOTLIB_FollowPath(edict_t *self) +{ + //if (self->groundentity && (self->bot.current_node == INVALID || self->bot.next_node == INVALID || self->bot.goal_node == INVALID)) // Invalid pathing + if (self->bot.current_node == INVALID || self->bot.next_node == INVALID || self->bot.goal_node == INVALID) // Invalid pathing + { + //Com_Printf("%s [%s] invalid pathing current_node[%d] next_node[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + self->bot.node_travel_time++; + return false; + } + + // Start holding the POI + if (nodes[self->bot.next_node].type == NODE_POI && self->bot.node_poi_holding == false) + { + // See if we touched the goal (POI) node + float distance = 4; + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) + { + int node = self->bot.node_list[i]; // Next node in the list + if (node != INVALID) + { + vec3_t bmins = { nodes[node].absmin[0] + -(distance), nodes[node].absmin[1] + -(distance), nodes[node].absmin[2] + -(distance) }; + vec3_t bmaxs = { nodes[node].absmax[0] + distance, nodes[node].absmax[1] + distance, nodes[node].absmax[2] + distance }; + if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) + { + if (node == self->bot.goal_node) // Reached POI + { + // If we're not holding the POI, then hold it + if (self->bot.node_poi_holding == false && self->bot.node_poi_time < level.framenum) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = self->bot.goal_node; + self->bot.next_node = self->bot.goal_node; + + self->bot.node_poi_holding = true; + + if (teamplay->value && lights_camera_action) // Hold spot longer if LCA is active + self->bot.node_poi_time = level.framenum + 30 * HZ; // Set the time to spend at the POI + else + self->bot.node_poi_time = level.framenum + 7 * HZ; // Set the time to spend at the POI + + //Com_Printf("%s [%s] reached POI, holding pos. current_node[%d] next_node[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + break; + } + } + } + } + } + } + // Continue holding the POI + if (self->bot.node_poi_holding && self->bot.node_poi_time > level.framenum && self->bot.see_enemies == false && self->bot.enemy_in_xhair == false) + { + //Com_Printf("%s [%s] at POI, hold pos\n", __func__, self->client->pers.netname); + self->bot.bi.actionflags = ACTION_HOLDPOS; // Hold + //self->bot.bi.actionflags |= ACTION_CROUCH; // Crouch + return true; + } + // Stop holding the POI + else if (self->bot.node_poi_holding) + { + self->bot.node_poi_holding = false; + //Com_Printf("%s [%s] at POI, free to leave\n", __func__, self->client->pers.netname); + + // Kill velocity, give the bot a chance to adjust to the next goal path + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = -200; + + self->bot.bi.speed = 10; + + //Com_Printf("%s %s GOAL\n", __func__, self->client->pers.netname); + self->bot.node_travel_time = 0; + self->bot.state = BOT_MOVE_STATE_NAV; + self->bot.goal_node = INVALID; + return false; + } + + if (ctf->value) + { + // Allow flag carrying bots to idle around their home flag + if (BOTLIB_Carrying_Flag(self)) + { + if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) <= 128) + { + self->bot.node_travel_time = 0; + } + else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) <= 128) + { + self->bot.node_travel_time = 0; + } + } + + // Allow flag bots to idle around enemy flag home + if (BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) <= 128) + { + self->bot.node_travel_time = 0; + } + else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) <= 128) + { + self->bot.node_travel_time = 0; + } + } + } + + //if (teamplay->value && lights_camera_action > 0) + // return true; + + //rekkie -- Quake3 -- s + // Count travel time between nodes, only if bot is not holding position or LCA countdown is active + if ((self->bot.bi.actionflags & ACTION_HOLDPOS) == 0) + { + // Set node_travel_time to zero if the round isn't active + if (teamplay->value && (team_round_going == false || lights_camera_action > 0)) + self->bot.node_travel_time = 0; + + // If we stopped moving + //if (VectorDistance(self->s.origin, self->lastPosition) < FRAMETIME) + self->bot.node_travel_time++; + + // Check bot direction vs node direction + vec3_t node_vec = { 0 }; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, node_vec); + vec3_t move_vec = { 0 }; + VectorSubtract(self->s.origin, self->lastPosition, move_vec); + VectorNormalize(node_vec); + VectorNormalize(move_vec); + float dot = DotProduct(node_vec, move_vec); + //Com_Printf("%s [%s] dot[%f] \n", __func__, self->client->pers.netname, dot); + if (dot < 0.3f) + { + //Com_Printf("%s [%s] [%f] node_travel_time: %d \n", __func__, self->client->pers.netname, dot, self->bot.node_travel_time); + self->bot.node_travel_time++; + } + + //Com_Printf("%s [%s] [%f] node_travel_time: %d \n", __func__, self->client->pers.netname, dot, self->bot.node_travel_time); + } + //rekkie -- Quake3 -- e + + + + + + // Boot in our new pathing algorithm - this will fill self->pathList with the information we need + // We have no path (SLLempty) and we're not at our destination (current_node != goal_node) + if (SLLempty(&self->pathList)) + { + //Com_Printf("%s %s: SLLempty curr[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + + self->bot.node_list_count = 0; + + /* + if (!AntStartSearch(self, self->bot.current_node, self->bot.goal_node)) // Set up our pathList + { + // Failed to find a path + Com_Printf("%s %s: Target at(%i) - No Path \n", __func__, self->client->pers.netname, self->bot.goal_node, self->bot.next_node); + return false; + } + */ + } + + + + // Check if bot is touching a node that isn't on the path + int nodes_touched; // Number of nodes touched + int nodelist[MAX_NODELIST]; // Nodes touched + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 0, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + qboolean external_node_touched = true; + for (int i = 0; i < nodes_touched; i++) + { + if (self->groundentity == NULL) + { + external_node_touched = false; + break; + } + /* + // Check if touching any of the connected links + int node = self->bot.node_list[i]; + for (int j = 0; j < nodes[node].num_links; j++) + { + if (nodes[j].nodenum == curr_node || self->bot.node_list[i] == self->bot.next_node) + { + external_node_touched = false; + break; + } + } + */ + + // Check if node very close + if (VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64 || VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64) + { + external_node_touched = false; + break; + } + + for (int j = self->bot.node_list_current; j < self->bot.node_list_count; j++) + { + if (nodelist[i] == self->bot.node_list[j]) + { + external_node_touched = false; + break; + } + } + } + if (self->groundentity == NULL) + { + external_node_touched = false; + } + if (external_node_touched) + { + //Com_Printf("%s %s touching ext node\n", __func__, self->client->pers.netname); + + // Check how far we've moved + qboolean moved = true; + vec3_t lastdir; + VectorSubtract(self->s.origin, self->lastPosition, lastdir); + float move_dist = VectorLength(lastdir); + if (move_dist < FRAMETIME) + moved = false; // We've not moved + + if (moved == false && self->bot.node_list_count && self->bot.goal_node) + { + //Com_Printf("%s %s touched a node that isn't on its path\n", __func__, self->client->pers.netname); + //if (BOTLIB_CanGotoNode(self, self->bot.goal_node, false)) + if (BOTLIB_CanVisitNode(self, self->bot.goal_node, false, INVALID, false)) + { + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, self->bot.goal_node); + //Com_Printf("%s %s redirecting bot to its goal node %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + return true; + } + } + if (self->bot.node_list_count == 0 || self->bot.goal_node == INVALID) + { + //Com_Printf("%s %s redirecting bot to a new goal\n", __func__, self->client->pers.netname); + //self->bot.state = BOT_MOVE_STATE_NAV; + //return true; + } + } + + + + if (self->bot.node_list_count == 0) + { + //Com_Printf("%s %s node_list_count is empty. Wandering!\n", __func__, self->client->pers.netname); + + ////if (nav_area.total_areas > 0) + { + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + } + ////else + { + self->bot.state = BOT_MOVE_STATE_NAV; + self->bot.goal_node = INVALID; + } + + return false; + } + + if (bot_showpath->value && dedicated->value == 0) + { + BOTLIB_DrawPath(self); + } + + + /* + // Check if bot is touching a node that isn't on the path + int nodes_touched; // Number of nodes touched + int nodelist[MAX_NODELIST]; // Nodes touched + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0,0,0), 0, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + qboolean external_node_touched = false; + for (int i = 0; i < nodes_touched; i++) + { + for (int j = self->bot.node_list_current; j < self->bot.node_list_count; j++) + { + external_node_touched = true; + if (nodelist[i] == self->bot.node_list[j]) + { + external_node_touched = false; + break; + } + } + } + if (external_node_touched) + { + //Com_Printf("%s %s touched a node that isn't on our path\n", __func__, self->client->pers.netname); + } + */ + + + // Check if bot is at last node + if (self->bot.node_list_current == self->bot.node_list_count - 1) + { + int node = self->bot.node_list[self->bot.node_list_count - 1]; // Last node (goal node) + if (node != INVALID && node == self->bot.goal_node) + { + float dist = VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin); + + //float distance = 32; // extend node box size by + //vec3_t bmins = { nodes[node].absmin[0] + -(distance), nodes[node].absmin[1] + -(distance), nodes[node].absmin[2] + -(distance) }; + //vec3_t bmaxs = { nodes[node].absmax[0] + distance, nodes[node].absmax[1] + distance, nodes[node].absmax[2] + distance }; + //if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) + if (dist < 128) + { + // Reached goal + //if (node == self->bot.goal_node) + { + // Kill velocity, give the bot a chance to adjust to the next goal path + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = -200; + + self->bot.bi.speed = 100; + //self->bot.bi.actionflags = 0; + + Com_Printf("%s %s +++++++++ SUCCESSFULLY REACHED GOAL +++++++++ curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + self->bot.node_travel_time = 0; + + ////if (nav_area.total_areas > 0) + { + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + } + ////else + { + self->bot.state = BOT_MOVE_STATE_NAV; // Flag bot to get a new goal + self->bot.goal_node = INVALID; + } + + return false; + } + } + } + } + + + // See if we touched any of the nodes in our path, if so, update our current node and next node + float distance = 4; // extend node box size by + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) + { + int node = self->bot.node_list[i]; // Next node in the list + if (node != INVALID) + { + vec3_t bmins = { nodes[node].absmin[0] + -(distance), nodes[node].absmin[1] + -(distance), nodes[node].absmin[2] + -(distance) }; + vec3_t bmaxs = { nodes[node].absmax[0] + distance, nodes[node].absmax[1] + distance, nodes[node].absmax[2] + distance }; + if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) + { + // Reached goal + if (node == self->bot.goal_node) + { + // Kill velocity, give the bot a chance to adjust to the next goal path + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = -200; + + self->bot.bi.speed = 100; + //self->bot.bi.actionflags = 0; + + Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + self->bot.node_travel_time = 0; + + ////if (nav_area.total_areas > 0) + { + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + } + ////else + { + self->bot.state = BOT_MOVE_STATE_NAV; // Flag bot to get a new goal + self->bot.goal_node = INVALID; + } + + return false; + } + else // Update current and next node + { + self->bot.node_travel_time = 0; + + self->bot.node_list_current = i + 1; + + self->bot.prev_node = self->bot.current_node; + + self->bot.current_node = self->bot.node_list[i]; + self->bot.next_node = self->bot.node_list[i + 1]; + } + } + } + } + // Backup check: if our list is at the end, then we're at the goal + //Com_Printf("%s %s count[%d] curr[%d]\n", __func__, self->client->pers.netname, self->bot.node_list_count, self->bot.node_list_current); + if (self->bot.node_list_current > self->bot.node_list_count) + { + // Kill velocity, give the bot a chance to adjust to the next goal path + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = -200; + + self->bot.bi.speed = 10; + //self->bot.bi.actionflags = 0; + + Com_Printf("%s %s GOAL [FIX]\n", __func__, self->client->pers.netname); + self->bot.node_travel_time = 0; + + ////if (nav_area.total_areas > 0) + { + ////Com_Printf("%s %s GOAL [FIX]\n", __func__, self->client->pers.netname); + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + } + ////else + { + self->bot.state = BOT_MOVE_STATE_NAV; // Flag bot to get a new goal + self->bot.goal_node = INVALID; + } + + return false; + } + + return true; +} + + +/////////////////////////////////////////////////////////////////////// +// MAPPING CODE +/////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////////////////////// +// Capture when the grappling hook has been fired for mapping purposes. +/////////////////////////////////////////////////////////////////////// +void ACEND_GrapFired(edict_t *self) +{ +/* int closest_node; + + if(!self->owner) + return; // should not be here + + // Check to see if the grapple is in pull mode + if(self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL) + { + // Look for the closest node of type grapple + closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_GRAPPLE); + if(closest_node == -1 ) // we need to drop a node + { + closest_node = ACEND_AddNode(self,NODE_GRAPPLE); + + // Add an edge + ACEND_UpdateNodeEdge(self, self->owner->last_node,closest_node); + + self->owner->last_node = closest_node; + } + else + self->owner->last_node = closest_node; // zero out so other nodes will not be linked + }*/ +} + +/* +/////////////////////////////////////////////////////////////////////// +// Check for adding ladder nodes +/////////////////////////////////////////////////////////////////////// +qboolean ACEND_CheckForLadder(edict_t *self) +{ + int closest_node; + + // If there is a ladder and we are moving up, see if we should add a ladder node + if (gi.pointcontents(self->s.origin) & CONTENTS_LADDER && self->velocity[2] > 0) + { + //debug_printf("contents: %x\n",tr.contents); + + closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_LADDER); + if(closest_node == -1) + { + closest_node = ACEND_AddNode(self,NODE_LADDER); + + // Now add link + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + + // Set current to last + self->last_node = closest_node; + } + else + { + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + self->last_node = closest_node; // set visited to last + } + return true; + } + return false; +} + +//======================================================= +// LadderForward +//======================================================= +// +// The ACE code version of this doesn't work! + +qboolean ACEND_LadderForward( edict_t *self )//, vec3_t angles ) +{ + vec3_t dir, angle, dest, min, max; + trace_t trace; + int closest_node; + + + VectorClear(angle); + angle[1] = self->s.angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + VectorCopy(self->mins,min); + min[2] += 22; + VectorCopy(self->maxs,max); + VectorMA(self->s.origin, TRACE_DIST_LADDER, dir, dest); + + trace = gi.trace(self->s.origin, min, max, dest, self, MASK_ALL); + + //BOTUT_TempLaser(self->s.origin, dest); + if (trace.fraction == 1.0) + return (false); + +// gi.bprintf(PRINT_HIGH,"Contents forward are %d\n", trace.contents); + if (trace.contents & CONTENTS_LADDER || trace.contents &CONTENTS_DETAIL) + { + // Debug print +// gi.bprintf(PRINT_HIGH,"contents: %x\n",trace.contents); + + closest_node = ACEND_FindClosestReachableNode(self,NODE_DENSITY,NODE_LADDER); + if(closest_node == -1) + { + closest_node = ACEND_AddNode(self,NODE_LADDER); + + // Now add link + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + + // Set current to last + self->last_node = closest_node; + } + else + { + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + self->last_node = closest_node; // set visited to last + } + return (true); + } + return (false); +} + + +/////////////////////////////////////////////////////////////////////// +// This routine is called to hook in the pathing code and sets +// the current node if valid. +/////////////////////////////////////////////////////////////////////// +void ACEND_PathMap(edict_t *self) +{ + int closest_node; + // Removed last_update checks since this stopped multiple node files being built + vec3_t v; + + // Special node drawing code for debugging + if( ltk_showpath->value ) + { + if(show_path_to != -1) + ACEND_DrawPath( self ); + } + + // Just checking lightlevels - uncomment to use +// if( debug_mode && !self->is_bot) +// gi.bprintf(PRINT_HIGH,"LightLevel = %d\n", self->light_level); + + //////////////////////////////////////////////////////// + // Special check for ladder nodes + /////////////////////////////////////////////////////// + // Replace non-working ACE version with mine. +// if(ACEND_CheckForLadder(self)) // check for ladder nodes + if(ACEND_LadderForward(self)) // check for ladder nodes + return; + + // Not on ground, and not in the water, so bail + if(!self->groundentity && !self->waterlevel) + return; + + //////////////////////////////////////////////////////// + // Lava/Slime + //////////////////////////////////////////////////////// + VectorCopy(self->s.origin,v); + v[2] -= 18; + if(gi.pointcontents(v) & (CONTENTS_LAVA|CONTENTS_SLIME)) + return; // no nodes in slime + + //////////////////////////////////////////////////////// + // Jumping + /////////////////////////////////////////////////////// + if(self->is_jumping) + { + // See if there is a closeby jump landing node (prevent adding too many) + closest_node = ACEND_FindClosestReachableNode(self, 64, NODE_JUMP); + + if(closest_node == INVALID) + closest_node = ACEND_AddNode(self,NODE_JUMP); + + // Now add link + if(self->last_node != -1) + ACEND_UpdateNodeEdge(self, self->last_node, closest_node); + + self->is_jumping = false; + return; + } + + // Werewolf: + //////////////////////////////////////////////////////// + // Switches, etc. - uses the "grapple" nodetype + /////////////////////////////////////////////////////// + if(self->is_triggering) + { + // See if there is a closeby grapple node (prevent adding too many) +// closest_node = ACEND_FindClosestReachableNode(self, 64, NODE_GRAPPLE); + +// if(closest_node == INVALID) + closest_node = ACEND_AddNode(self,NODE_GRAPPLE); + + // Now add link + if(self->last_node != -1) + ACEND_UpdateNodeEdge(self, self->last_node, closest_node); + + self->is_triggering = false; + return; + } + + + //////////////////////////////////////////////////////////// + // Grapple + // Do not add nodes during grapple, added elsewhere manually + //////////////////////////////////////////////////////////// +// if(ctf->value && self->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL) +// return; + + // Iterate through all nodes to make sure far enough apart + closest_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + //////////////////////////////////////////////////////// + // Special Check for Platforms + //////////////////////////////////////////////////////// + if(self->groundentity && self->groundentity->use == Use_Plat) + { + if(closest_node == INVALID) + return; // Do not want to do anything here. + + // Here we want to add links + if(closest_node != self->last_node && self->last_node != INVALID) + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + + self->last_node = closest_node; // set visited to last + return; + } + + //////////////////////////////////////////////////////// + // Add Nodes as needed + //////////////////////////////////////////////////////// + if(closest_node == INVALID) + { + // Add nodes in the water as needed + if(self->waterlevel) + closest_node = ACEND_AddNode(self,NODE_WATER); + else + closest_node = ACEND_AddNode(self,NODE_MOVE); + + // Now add link + if(self->last_node != -1) + ACEND_UpdateNodeEdge(self, self->last_node, closest_node); + + } + else if(closest_node != self->last_node && self->last_node != INVALID) + ACEND_UpdateNodeEdge(self, self->last_node,closest_node); + + self->last_node = closest_node; // set visited to last + +} +*/ + +/////////////////////////////////////////////////////////////////////// +// Init node array (set all to INVALID) +/////////////////////////////////////////////////////////////////////// +void BOTLIB_InitNodes(void) +{ + numnodes = 0; // Nodes start at 0 + numitemnodes = 1; + + //memset(&nmesh, 0, sizeof(nmesh_t)); + + //BOTLIB_FreeNodes(); // Free any existing node data + + //memset(nodes,0,sizeof(node_t) * MAX_NODES); + //memset(path_table,INVALID,sizeof(short int)*MAX_NODES*MAX_NODES); + + //rekkie -- DEV_1 -- s + memset(node_ents, 0, sizeof(node_ents)); + //rekkie -- DEV_1 -- e + + // ------------- + // Alloc memory + // ------------- + /* + // Main nodes + for (int n = 0; n < MAX_PNODES; n++) + { + nodes[n] = (node_t*)malloc(sizeof(node_t)); + if (nodes[n] == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + memset(nodes[n], 0, sizeof(node_t)); + //nodes[n]->nodenum = INVALID; + } + //nodes = (node_t*)malloc(sizeof(node_t) * MAX_PNODES); + //if (nodes == NULL) + //{ + // Com_Printf("%s failed to malloc nodes\n", __func__); + // return; + //} + //memset(nodes, 0, sizeof(node_t) * MAX_PNODES); + + // Unsorted duplicate copy nodes + unsorted_nodes = (node_t*)malloc(sizeof(node_t) * MAX_PNODES); + if (unsorted_nodes == NULL) + { + Com_Printf("%s failed to malloc unsorted_nodes\n", __func__); + return; + } + */ + + /* + // Path table + // Uses a 2D array of dynamic memory consisting of an array of pointers -- https://www.techiedelight.com/dynamically-allocate-memory-for-2d-array/ + // This doubles the memory requirements versus a traditional 2D array, but it allows for larger array sizes + path_table = (short int**)malloc(sizeof(short int*) * MAX_PNODES); + if (path_table == NULL) + { + Com_Printf("%s failed to malloc path_table[]\n", __func__); + return; + } + else + { + // Dynamically allocate memory of size MAX_PNODES for each row + for (int r = 0; r < MAX_PNODES; r++) + { + path_table[r] = (short int*)malloc(sizeof(short int) * MAX_PNODES); + if (path_table[r] == NULL) + { + Com_Printf("%s failed to malloc path_table[][]\n", __func__); + return; + } + else + { + for (int c = 0; c < MAX_PNODES; c++) + { + path_table[r][c] = INVALID; + } + } + } + } + + Com_Printf("%s allocing MBytes[%ld] for path_table\n", __func__, ((sizeof(short int) * MAX_PNODES * MAX_PNODES) / 1024000)); + */ +} + +//rekkie -- DEV_1 -- s +/* +/////////////////////////////////////////////////////////////////////// +// Show spawn NODE_SPAWNPOINT nodes +/////////////////////////////////////////////////////////////////////// +edict_t* ACEND_ShowSpawnPointNode(int node, qboolean isGoodNode) +{ + edict_t* ent; + + ent = G_Spawn(); + + ent->movetype = MOVETYPE_NONE; + + //ent->solid = SOLID_NOT; + ent->solid = SOLID_TEST_NOT; // Used in gi.TraceAll (but not gi.Trace) - ensures non-solid ents are findable + ent->svflags |= SVF_ENT; // Ensures other entities (inc players) cannot clip against this entity + ent->svflags &= ~SVF_NOCLIENT; // Always send to client + + ent->classname = ent->node_name; //botdebug + ent->node_num = node; + + VectorSet(ent->mins, -4, -4, -4); + VectorSet(ent->maxs, 4, 4, 4); + + ent->owner = ent; + ent->dmg = 0; + + // Deathmatch spawn point + ent->model = "models/objects/dmspot/tris.md2"; + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + if (isGoodNode) + ent->s.renderfx = RF_SHELL_BLUE; + else + ent->s.renderfx = RF_SHELL_RED; + Q_strncpyz(ent->node_name, va("node spawn point [%d]", node), sizeof(ent->node_name)); + + if (debug_mode) + { + ent->nextthink = level.framenum + 6000 * HZ; // Extra long + ent->think = G_FreeEdict; + } + + VectorCopy(nodes[node].origin, ent->s.origin); + gi.linkentity(ent); + return ent; +} +*/ +/////////////////////////////////////////////////////////////////////// +// Ensures that spawn points all have NODE_SPAWNPOINT nodes on them +// Bad nodes occur because they were either moved or the map was changed +/////////////////////////////////////////////////////////////////////// +void ACEND_BuildSpawnPointNodes(void) +{ + // Counter, Spot, Node + int i, s, n; + + // Save + qboolean requiresSave = false; // If ACEND_SaveNodes() needs to be called + + // Spawn Point (SP) + vec3_t spots_mod[MAX_SP_NODES]; // Spawn points (SP) (info_player_deathmatch) - modified from a downward endpos trace + short sp_counter = 0; // Total spawn points found + qboolean sp_has_spn[MAX_SP_NODES]; // If the SP has an SPN + edict_t* spot = NULL; + trace_t tr; + vec3_t maxs = { 1,1,1 }; + vec3_t mins = { 1,1,1 }; + + // Spawn Point Node (SPN) + short spn_nodes[MAX_SP_NODES]; // Track spawn point nodes (SPN) + short spn_total = 0; // Total spawn point nodes (SPN) found on map + vec3_t spn_loc[MAX_SP_NODES]; // All SPN locations + qboolean spn_inuse[MAX_SP_NODES]; // If the SPN is in use + + edict_t* ent; + + // Init + for (i = 0; i < MAX_SP_NODES; i++) + { + spn_nodes[i] = INVALID; + sp_has_spn[i] = false; + spn_inuse[i] = false; + } + + // Find all SPs + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + if (sp_counter >= MAX_SP_NODES) // Security check for array bounds - what map would need more than 256 spawns? + { + Com_Printf("%s Failed build spawn point nodes. Increase MAX_SPAWN_SPOTS beyond %i.\n", __func__, MAX_SP_NODES); + return; + } + + // Trace down and find the ground + VectorCopy(spot->s.origin, spots_mod[sp_counter]); + spots_mod[sp_counter][2] -= 1024; + tr = gi.trace(spot->s.origin, mins, maxs, spots_mod[sp_counter], spot, MASK_SOLID | MASK_OPAQUE); + spots_mod[sp_counter][2] = tr.endpos[2] + 32; + //gi.bprintf(PRINT_MEDIUM, "%s node %i at %f vs %f\n", __func__, i, spot->s.origin[2], spots_mod[sp_counter][2]); + + sp_counter++; + } + + // Exit if no DM SPs found + if (!sp_counter) + { + Com_Printf("%s Failed build spawn point nodes. No deathmatch spawn points found.\n", __func__, MAX_SP_NODES); + return; + } + + // Find all SPNs + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (nodes[i].type == NODE_SPAWNPOINT) + { + VectorCopy(nodes[i].origin, spn_loc[spn_total]); // Locatiom + spn_nodes[spn_total] = i; // Node num + spn_total++; // Total + } + } + + // Find any non-SPN nodes sitting on a SP and move them up so they're out of the way and can be used elsewhere on the map + for (s = 0; s < sp_counter; s++) + { + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (nodes[i].type != NODE_SPAWNPOINT) + { + if (nodes[i].origin[0] == spots_mod[s][0] && nodes[i].origin[1] == spots_mod[s][1] && nodes[i].origin[2] == spots_mod[s][2]) + { + nodes[i].origin[2] += 32; // Move node up a little + //Com_Printf("%s Moving node %i of type %i to %f %f %f\n", __func__, i, nodes[i].type, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + requiresSave = true; + } + } + } + } + + // Map has SPNs - A) Find if a SP has an SPN - B) Flag used SPNs - C) Move unused SPNs to 0,0,0 + if (spn_total) + { + for (s = 0; s < sp_counter; s++) + { + for (n = 0; n < spn_total; n++) + { + if (spn_loc[n][0] == spots_mod[s][0] && spn_loc[n][1] == spots_mod[s][1] && spn_loc[n][2] == spots_mod[s][2]) + { + //Com_Printf("%s Found good node %i at %f %f %f\n", __func__, spn_nodes[n], spots_mod[s][0], spots_mod[s][1], spots_mod[s][2]); + sp_has_spn[s] = true; + spn_inuse[n] = true; + } + } + } + + // SP has no SPN - lets give it one + for (s = 0; s < sp_counter; s++) + { + if (sp_has_spn[s] == false) // If no SPN on SP + { + // Try using an existing SPN (if there is one free) + for (n = 0; n < spn_total; n++) + { + if (spn_inuse[n] == false) + { + sp_has_spn[s] = true; + spn_inuse[n] = true; + + i = spn_nodes[n]; + //Com_Printf("%s Moving SPN %i from %f %f %f to %f %f %f\n", __func__, spn_nodes[n], nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2], spots_mod[s][0], spots_mod[s][1], spots_mod[s][2]); + + BOTLIB_RemoveAllNodeLinksFrom(i); // Break all connections to spawn point node + VectorCopy(spots_mod[s], nodes[i].origin); // Then move node to spot location + + // Also move the entitiy location + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent && ent->node_num == i) + { + VectorCopy(spots_mod[s], ent->s.origin); + break; + } + } + + break; + } + } + + // Otherwise make a new SPN + if (sp_has_spn[s] == false) + { + if (numnodes < MAX_PNODES) + { + VectorCopy(spots_mod[s], nodes[numnodes].origin); + nodes[numnodes].type = NODE_SPAWNPOINT; + nodes[numnodes].nodenum = numnodes; + + // Init links + for (i = 0; i < MAXLINKS; i++) + { + nodes[numnodes].links[i].targetNode = INVALID; + } + + //Com_Printf("%s Adding new node %i at %f %f %f\n", __func__, numnodes, spots_mod[s][0], spots_mod[s][1], spots_mod[s][2]); + + if (debug_mode) + ACEND_ShowNode(numnodes); + + numnodes++; + } + } + } + } + + // Lastly, deal with situations where we have more SPNs than SPs - so move the unused SPN to origin 0,0,0 + for (n = 0; n < spn_total; n++) + { + if (spn_inuse[n] == false) + { + vec3_t spn_unused = { 0,0,0 }; + i = spn_nodes[n]; + //Com_Printf("%s Moving unused SPN %i from %f %f %f to %f %f %f\n", __func__, spn_nodes[n], nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2], spn_unused[0], spn_unused[1], spn_unused[2]); + + BOTLIB_RemoveAllNodeLinksFrom(i); // Break all connections to spawn point node + VectorCopy(spn_unused, nodes[i].origin); // Then move node to spot location + + // Also move the entitiy location + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent && ent->node_num == i) + { + VectorCopy(spn_unused, ent->s.origin); + break; + } + } + } + } + } + + // Map has no SPNs - so lets add them + else + { + requiresSave = true; + for (s = 0; s < sp_counter; s++) + { + // Add spawn point node + if (numnodes < MAX_PNODES) + { + VectorCopy(spots_mod[s], nodes[numnodes].origin); + nodes[numnodes].type = NODE_SPAWNPOINT; + nodes[numnodes].nodenum = numnodes; + + // Init links + for (i = 0; i < MAXLINKS; i++) + { + nodes[numnodes].links[i].targetNode = INVALID; + } + + //Com_Printf("%s [New Map] Adding new node %i at %f %f %f\n", __func__, numnodes, spots_mod[s][0], spots_mod[s][1], spots_mod[s][2]); + + if (debug_mode) + ACEND_ShowNode(numnodes); + + numnodes++; + } + } + } + + // Save changes + //if (requiresSave) + // ACEND_SaveNodes(); +} + +/////////////////////////////////////////////////////////////////////// +// Cache all the Point of Interest (PoI) nodes for faster access +// Also include Spawn Point Nodes (SPNs) so bots visit spawn locs +/////////////////////////////////////////////////////////////////////// +void ACEND_CachePointOfInterestNodes(void) +{ + num_poi_nodes = 0; + // Init + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + poi_nodes[num_poi_nodes] = INVALID; + } + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (num_poi_nodes + 1 >= MAX_POI_NODES) + { + //Com_Printf("%s Failed to cache POI nodes. Maximum POI allowed %i\n", __func__, MAX_POI_NODES); + return; + } + + if (nodes[i].type == NODE_POI) + { + //Com_Printf("%s Caching POI node %i at %f %f %f\n", __func__, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + else if (nodes[i].type == NODE_SPAWNPOINT) + { + //Com_Printf("%s Caching SPAWN POINT node %i at %f %f %f\n", __func__, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } +} + +/////////////////////////////////////////////////////////////////////// +// Builds an array of nodes that are attached to an entity +// (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +/////////////////////////////////////////////////////////////////////// +void ACEND_BuildEntNodeTable(void) +{ + // General counter, nodes + int i, n; + + // Door nodes + qboolean is_bad_door_node; // If door node is missing + vec3_t bad_door_node_pos[MAX_DOOR_NODES]; // Missing door positions + + edict_t* ent; // Entities + + // Door counters + byte doors_total = 0; // Total doors found + byte doors_good = 0; // Total good doors + byte doors_bad = 0; // How many doors are missing nodes + byte doors_fixed = 0; // Total bad doors we've fixed + + byte door_nodes[MAX_DOOR_NODES]; // Cache door nodes + //byte bad_door_nodes[MAX_DOOR_NODES]; // Cache bad door nodes + + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (nodes[i].type == NODE_DOOR) + { + if (doors_total + 1 >= MAX_DOOR_NODES) + { + Com_Printf("%s Failed build door point nodes. Increase MAX_DOOR_NODES beyond %i.\n", __func__, MAX_DOOR_NODES); + return; + } + door_nodes[doors_total] = i; + doors_total++; + } + } + + // Existing doors + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + // filter out crap + if (ent->solid == SOLID_NOT) + continue; + + if (!ent->classname) + continue; + + // Doors + if (strcmp(ent->classname, "func_door_rotating") == 0 || strcmp(ent->classname, "func_door") == 0) + { + // First lets find any existing doors nodes without entities attached + // The nodes must be found in the correct place + //============================================================== + // + vec3_t position; + // Find mid point of door max and min and put the node there + VectorClear(position); + // find center of door + position[0] = ent->absmin[0] + ((ent->maxs[0] - ent->mins[0]) / 2); + position[1] = ent->absmin[1] + ((ent->maxs[1] - ent->mins[1]) / 2); + position[2] = ent->absmin[2] + 32; + // We now have to create TWO nodes, one each side of the door + // See if the 'door' is wider in the x or y direction + // Create in the 'y' direction + if ((ent->absmax[0] - ent->absmin[0]) > (ent->absmax[1] - ent->absmin[1])) + { + is_bad_door_node = true; + position[1] += 48; + for (i = 0; i < doors_total; i++) + { + n = door_nodes[i]; + if (nodes[n].origin[0] == position[0] && nodes[n].origin[1] == position[1] && nodes[n].origin[2] == position[2]) + { + doors_good++; + is_bad_door_node = false; + node_ents[n] = ent; // Add door as an entity reference + Com_Printf("%s Attaching ent to [in place] door node %i at %f %f %f\n", __func__, n, position[0], position[1], position[2]); + } + } + if (is_bad_door_node) + { + VectorCopy(position, bad_door_node_pos[doors_bad]); + doors_bad++; + } + + is_bad_door_node = true; + position[1] -= 82; + for (i = 0; i < doors_total; i++) + { + n = door_nodes[i]; + if (nodes[n].origin[0] == position[0] && nodes[n].origin[1] == position[1] && nodes[n].origin[2] == position[2]) + { + doors_good++; + is_bad_door_node = false; + node_ents[n] = ent; // Add door as an entity reference + Com_Printf("%s Attaching ent to [in place] door node %i at %f %f %f\n", __func__, n, position[0], position[1], position[2]); + } + } + if (is_bad_door_node) + { + VectorCopy(position, bad_door_node_pos[doors_bad]); + doors_bad++; + } + } + // Create in the 'x' direction + else + { + is_bad_door_node = true; + position[0] += 48; + for (i = 0; i < doors_total; i++) + { + n = door_nodes[i]; + if (nodes[n].origin[0] == position[0] && nodes[n].origin[1] == position[1] && nodes[n].origin[2] == position[2]) + { + doors_good++; + is_bad_door_node = false; + node_ents[n] = ent; // Add door as an entity reference + Com_Printf("%s Attaching ent to [in place] door node %i at %f %f %f\n", __func__, n, position[0], position[1], position[2]); + } + } + if (is_bad_door_node) + { + VectorCopy(position, bad_door_node_pos[doors_bad]); + doors_bad++; + } + + is_bad_door_node = true; + position[0] -= 82; + for (i = 0; i < doors_total; i++) + { + n = door_nodes[i]; + if (nodes[n].origin[0] == position[0] && nodes[n].origin[1] == position[1] && nodes[n].origin[2] == position[2]) + { + doors_good++; + is_bad_door_node = false; + node_ents[n] = ent; // Add door as an entity reference + Com_Printf("%s Attaching ent to [in place] door node %i at %f %f %f\n", __func__, n, position[0], position[1], position[2]); + } + } + if (is_bad_door_node) + { + VectorCopy(position, bad_door_node_pos[doors_bad]); + doors_bad++; + } + } + // + //============================================================== + } + } + + // Door fixes + // For door nodes that are out of place and without entities attached + //=================================================================== + if (doors_bad) + { + Com_Printf("%s GD:%i BD:%i TD:%i (GD+BD:%i)\n", __func__, doors_good, doors_bad, doors_total, doors_good + doors_bad); + + // Add new door node + if (doors_good + doors_bad < doors_total) + { + return; + } + + for (i = 0; i < doors_total; i++) + { + n = door_nodes[i]; + + if (nodes[n].type == NODE_DOOR) + { + if (node_ents[n] == 0) // NULL + { + BOTLIB_RemoveAllNodeLinksFrom(n); // Break all connections to this node + + // Now move the node into place of the missing door node position + if (doors_fixed != doors_bad) + { + VectorCopy(bad_door_node_pos[doors_fixed], nodes[n].origin); + doors_fixed++; + } + + Com_Printf("%s Attaching ent to [out of place] door node %i at %f %f %f\n", __func__, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + } + } + } + + // Too many door nodes - convert to node type 0 and send to location 0,0,0 + if (doors_good + doors_bad > doors_total) + { + /* + vec3_t zero = { 0,0,0 }; + for (i = doors_fixed; i < doors_bad; i++) + { + //bad_door_nodes + //VectorCopy(zero, bad_door_node_pos[i]); + //i++; + } + */ + } + + // Run once more to attach all door nodes to door ents + //ACEND_BuildEntNodeTable(); + } + //=================================================================== +} +/////////////////////////////////////////////////////////////////////// +// Build an array of visibility nodes, i.e. If node X can see node Y +// These nodes will be calculated once, saved/loaded to/from file, +// and cached in memory for quick lookup access. +/////////////////////////////////////////////////////////////////////// +void ACEND_BuildVisibilityNodes(void) +{ + short sn, dn; // Source node, destination node + short nvl_counter; // Node vis list counter + trace_t tr; // Trace + //vec3_t down; // Looking for ground + //edict_t* ent; // Entities + + clock_t begin = clock(); + + Com_Printf("%s is working...\n", __func__); + + // Init node vis list + for (sn = 0; sn < numnodes; sn++) + { + num_vis_nodes = 0; // Reset global vis node counter + nvl_counter = 0; + for (dn = 0; dn < MAX_VIS_NODES; dn++) + { + //node_vis_list[sn][nvl_counter++] = INVALID; + node_vis_list[sn][dn] = INVALID; + } + + /* + // Moves selective nodes to eye level + // For all the move nodes lets make sure that they're at the player eye level + if (nodes[sn].type == NODE_MOVE || nodes[sn].type == NODE_POI) + { + VectorCopy(nodes[sn].origin, down); + down[2] -= 128; // Distance downward, keeping the distance shallow because a node might be up high, so leave it alone + tr = gi.trace(nodes[sn].origin, NULL, NULL, down, NULL, MASK_SOLID | MASK_OPAQUE); // Trace down and find the ground + + tr.endpos[2] += 48; // self->viewheight (Eye position) + //Com_Printf("%s Adjusting node %i [%f] to [%f]\n", __func__, sn, nodes[sn].origin[2], tr.endpos[2]); + VectorCopy(tr.endpos, nodes[sn].origin); // Adjust the node to eye pos + + // Also move the entitiy location in realtime + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent && ent->node_num == sn) + { + VectorCopy(tr.endpos, ent->s.origin); + break; + } + } + } + */ + } + + // Build it + for (sn = 0; sn < numnodes; sn++) + { + nvl_counter = 0; + for (dn = 0; dn < numnodes; dn++) + { + if (sn == dn) // Skip self + continue; + + tr = gi.trace(nodes[sn].origin, NULL, NULL, nodes[dn].origin, NULL, MASK_SOLID | MASK_OPAQUE); // Trace from source to destination node + if (tr.fraction == 1.0) + { + // Build the array + node_vis[sn][dn] = dn; + + // Build the list + if (nvl_counter + 1 < MAX_VIS_NODES) + node_vis_list[sn][nvl_counter++] = dn; + + num_vis_nodes++; // Total visibility nodes + } + else + { + node_vis[sn][dn] = INVALID; // X cannot see Y + } + } + } + + clock_t end = clock(); + Com_Printf("%s execution took %f seconds to build visibility for %i nodes [%i * %i]\n", __func__, (double)(end - begin) / CLOCKS_PER_SEC, numnodes * numnodes, numnodes, numnodes); +} +/////////////////////////////////////////////////////////////////////// +// Can node X can see node Y +// Returns true/false based on precalculated cached tracelines +/////////////////////////////////////////////////////////////////////// +qboolean ACEND_IsNodeVisibleToNodes(short x, short y) +{ + if (x < numnodes && y < numnodes) // Bounce check + { + if (node_vis[x][y] != INVALID) // Is the node visible? + return true; + } + + return false; +} +/////////////////////////////////////////////////////////////////////// +// Returns a random node that X can see +// Otherwise return INVALID if no visibility +/////////////////////////////////////////////////////////////////////// +short ACEND_GetRandomVisibleNode(short x) +{ + short i; + short dn; // Destination node + short vis_size = 0; + + if (x + 1 < numnodes) // Bounce check + { + // Work out the size of the vis list + for (i = 0; i < MAX_VIS_NODES; i++) + { + if (node_vis_list[x][i] != INVALID) + vis_size++; + } + + dn = (int)(random() * vis_size); // Pick a random destination node + return node_vis_list[x][dn]; + } + + return INVALID; +} +//rekkie -- DEV_1 -- e + +/////////////////////////////////////////////////////////////////////// +// Show the node for debugging (utility function) +/////////////////////////////////////////////////////////////////////// +void ACEND_ShowNode(int node) +{ + edict_t *ent = NULL; + +// return; // commented out for now. uncommend to show nodes during debugging, + // but too many will cause overflows. You have been warned. + + //rekkie -- adjust node placement height for visual aid - NODE_Z_HEIGHT places @ ground level -- s + float adjusted_node_height = 0; + if (0) + adjusted_node_height = NODE_Z_HEIGHT; + //rekkie -- adjust node placement height for visual aid - NODE_Z_HEIGHT places @ ground level -- e + + //rekkie -- DEV_1 -- s + // Ignore invalid nodes + if (node < 0) + return; + if (nodes[node].nodenum < 0) + return; + + // Ignore placement of nodes on top of an existing node + for (ent = g_edicts + 1; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent->inuse == false || ent->client) // skip these + continue; + + // Check if ent has same origin as node + if (nodes[node].origin[0] == ent->s.origin[0] && nodes[node].origin[1] == ent->s.origin[1] && nodes[node].origin[2] == (ent->s.origin[2] + adjusted_node_height)) + { + //Com_Printf("%s already showing node %i\n", __func__, node); + return; + } + } + //rekkie -- DEV_1 -- e + + ent = G_Spawn(); + + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + + if(nodes[node].type == NODE_MOVE) + ent->s.renderfx = RF_SHELL_BLUE; + else if (nodes[node].type == NODE_WATER) + ent->s.renderfx = RF_SHELL_RED; + else + ent->s.renderfx = RF_SHELL_GREEN; // action nodes + + ent->s.modelindex = gi.modelindex ("models/items/ammo/grenades/medium/tris.md2"); + ent->owner = ent; + //ent->nextthink = level.framenum + 60 * HZ; // 1 minute (60*hz) is long enough! + ent->think = G_FreeEdict; + ent->dmg = 0; + + //rekkie -- DEV_1 -- s + //ent->nextthink = level.framenum + 6000 * HZ; // rekkie -- extra long + ent->nextthink = level.framenum + 6 * HZ; // rekkie -- very short + + VectorSet(ent->mins, -4, -4, -4); + VectorSet(ent->maxs, 4, 4, 4); + + ent->model = "models/objects/grenades/tris.md2"; + ent->s.modelindex = gi.modelindex("models/objects/grenades/tris.md2"); + + ent->classname = ent->node_name; + ent->node_num = node; + + if (nodes[node].type == NODE_MOVE) + { + ent->s.renderfx = RF_SHELL_BLUE; + Q_strncpyz(ent->node_name, va("node move [%d]", node), sizeof(ent->node_name)); + } + else if (nodes[node].type == NODE_WATER) + { + ent->s.renderfx = RF_SHELL_RED; + Q_strncpyz(ent->node_name, va("node water [%d]", node), sizeof(ent->node_name)); + } + //rekkie -- DEV_1 -- s + else if (nodes[node].type == NODE_JUMPPAD) + { + ent->s.renderfx = RF_SHELL_GREEN; + Q_strncpyz(ent->node_name, va("node jumppad [%d]", node), sizeof(ent->node_name)); + } + else if (nodes[node].type == NODE_SPAWNPOINT) + { + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + //ent->s.renderfx = RF_SHELL_BLUE; + Q_strncpyz(ent->node_name, va("node spawn point [%d]", node), sizeof(ent->node_name)); + } + else if (nodes[node].type == NODE_POI) + { + ent->s.renderfx = RF_SHELL_RED; + Q_strncpyz(ent->node_name, va("node point of interest [%d]", node), sizeof(ent->node_name)); + } + //rekkie -- DEV_1 -- e + else + { + ent->s.renderfx = RF_SHELL_GREEN; // action nodes + Q_strncpyz(ent->node_name, va("node action [%d]", node), sizeof(ent->node_name)); + } + //rekkie -- DEV_1 -- e + + VectorCopy(nodes[node].origin, ent->s.origin); + + ent->s.origin[2] -= adjusted_node_height; //rekkie -- place node to ground level for visual aid + + gi.linkentity (ent); +} +/* +/////////////////////////////////////////////////////////////////////// +// Draws the current path (utility function) +/////////////////////////////////////////////////////////////////////// +void ACEND_DrawPath(edict_t *self) +{ + int current_node, goal_node, next_node; + + current_node = show_path_from; + goal_node = show_path_to; + + // RiEvEr - rewritten to use Ant system + AntStartSearch( self, current_node, goal_node); + + next_node = SLLfront(&self->pathList); + + // Now set up and display the path + while( current_node != goal_node && current_node != INVALID && next_node != INVALID) + { + gi.WriteByte (svc_temp_entity); + gi.WriteByte (TE_BFG_LASER); + gi.WritePosition (nodes[current_node].origin); + gi.WritePosition (nodes[next_node].origin); + gi.multicast (nodes[current_node].origin, MULTICAST_PVS); + current_node = next_node; + SLLpop_front( &self->pathList); + next_node = SLLfront(&self->pathList); + } +} +*/ +/////////////////////////////////////////////////////////////////////// +// Turns on showing of the path, set goal to -1 to +// shut off. (utility function) +/////////////////////////////////////////////////////////////////////// +void ACEND_ShowPath(edict_t *self, int goal_node) +{ + show_path_from = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + show_path_to = goal_node; +} + +/* +/////////////////////////////////////////////////////////////////////// +// Add a node of type ? +/////////////////////////////////////////////////////////////////////// +int ACEND_AddNode(edict_t *self, int type) +{ + vec3_t v1,v2; + int i; + + // Block if we exceed maximum + if (numnodes + 1 > MAX_PNODES) + return false; + + // Set location + VectorCopy(self->s.origin, nodes[numnodes].origin); + nodes[numnodes].origin[2] += 8; + + // Set type + nodes[numnodes].type = type; + // Set number - RiEvEr + nodes[numnodes].nodenum = numnodes; + + // Clear out the link information - RiEvEr + for( i = 0; i< MAXLINKS; i++) + { + nodes[numnodes].links[i].targetNode = INVALID; + } + + ///////////////////////////////////////////////////// + // ITEMS + // Move the z location up just a bit. + if(type == NODE_ITEM) + { + nodes[numnodes].origin[2] += 16; + numitemnodes++; + } + + // Teleporters + if(type == NODE_TELEPORTER) + { + // Up 32 + nodes[numnodes].origin[2] += 32; + } + + // Doors + if(type == NODE_DOOR) + { + vec3_t position; + // Find mid point of door max and min and put the node there + VectorClear(position); + // find center of door + position[0] = self->absmin[0] + ((self->maxs[0] - self->mins[0]) /2); + position[1] = self->absmin[1] + ((self->maxs[1] - self->mins[1]) /2); + position[2] = self->absmin[2] + 32; + // We now have to create TWO nodes, one each side of the door + // See if the 'door' is wider in the x or y direction + if( (self->absmax[0] - self->absmin[0]) > (self->absmax[1] - self->absmin[1]) ) + { + // Create in the 'y' direction + + // First node (Duplication deliberate!) + // Set type + nodes[numnodes].type = type; + // Set number - RiEvEr + nodes[numnodes].nodenum = numnodes; + // Set position + position[1] +=48; + VectorCopy(position, nodes[numnodes].origin); + + // Second node + numnodes++; + // Set type + nodes[numnodes].type = type; + // Set number - RiEvEr + nodes[numnodes].nodenum = numnodes; + // Set position + position[1] -=82; + VectorCopy(position, nodes[numnodes].origin); + } + else + { + // Create in the 'x' direction + + // First node (Duplication deliberate!) + // Set type + nodes[numnodes].type = type; + // Set number - RiEvEr + nodes[numnodes].nodenum = numnodes; + // Set position + position[0] +=48; + VectorCopy(position, nodes[numnodes].origin); + + // Second node + numnodes++; + // Set type + nodes[numnodes].type = type; + // Set number - RiEvEr + nodes[numnodes].nodenum = numnodes; + // Set position + position[0] -=82; + VectorCopy(position, nodes[numnodes].origin); + } + numnodes++; + return numnodes-1; // return the second node added + + } + + if(type == NODE_LADDER) + { + nodes[numnodes].type = NODE_LADDER; + + if(debug_mode) + { + debug_printf("Node added %d type: Ladder\n",numnodes); + ACEND_ShowNode(numnodes); + } + + numnodes++; + return numnodes-1; // return the node added + + } + + // For platforms drop two nodes one at top, one at bottom + if(type == NODE_PLATFORM) + { + VectorCopy(self->maxs,v1); + VectorCopy(self->mins,v2); + + // To get the center + nodes[numnodes].origin[0] = (v1[0] - v2[0]) / 2 + v2[0]; + nodes[numnodes].origin[1] = (v1[1] - v2[1]) / 2 + v2[1]; + nodes[numnodes].origin[2] = self->maxs[2]; + + if(debug_mode) + ACEND_ShowNode(numnodes); + + numnodes++; + + nodes[numnodes].origin[0] = nodes[numnodes-1].origin[0]; + nodes[numnodes].origin[1] = nodes[numnodes-1].origin[1]; + nodes[numnodes].origin[2] = self->mins[2]+64; + + nodes[numnodes].type = NODE_PLATFORM; + + // Add a link + //RiEvEr modified to pass in calling entity + ACEND_UpdateNodeEdge(self, numnodes, numnodes-1); + + if(debug_mode) + { + debug_printf("Node added %d type: Platform\n",numnodes); + ACEND_ShowNode(numnodes); + } + + numnodes++; + + return numnodes -1; + } + + if(debug_mode) + { + if(nodes[numnodes].type == NODE_MOVE) + debug_printf("Node added %d type: Move\n",numnodes); + else if(nodes[numnodes].type == NODE_TELEPORTER) + debug_printf("Node added %d type: Teleporter\n",numnodes); + else if(nodes[numnodes].type == NODE_ITEM) + debug_printf("Node added %d type: Item\n",numnodes); + else if(nodes[numnodes].type == NODE_WATER) + debug_printf("Node added %d type: Water\n",numnodes); + else if(nodes[numnodes].type == NODE_GRAPPLE) + debug_printf("Node added %d type: Grapple\n",numnodes); + + ACEND_ShowNode(numnodes); + } + + numnodes++; + + return numnodes-1; // return the node added +} +*/ + + + +// RiEvEr +//======================================= +// ReverseLink +//======================================= +// Takes the path BACK to where we came from +// and tries to link the two nodes +// This helps make good path files +// +void ACEND_ReverseLink( edict_t *self, int from, int to ) +{ + int i; + trace_t trace; + vec3_t min,max; + + if(from == INVALID || to == INVALID || from == to) + return; // safety + + // Need to trace from -> to and check heights + // if from is much lower than to, forget it + if( (nodes[from].origin[2]+32.0) < (nodes[to].origin[2]) ) + { + // May not be able to jump that high so do not allow the return link + return; + } +// VectorCopy(self->mins, min); +// if( (nodes[from].origin[2]) < (nodes[2].origin[2]) ) +// min[2] =0; // Allow for steps etc. +// VectorCopy(self->maxs, max); +// if( (nodes[from].origin[2]) > (nodes[2].origin[2]) ) +// max[2] =0; // Could be a downward sloping feature above our head + VectorCopy( vec3_origin, min); + VectorCopy( vec3_origin, max); + + + + // This should not be necessary, but I've heard that before! + // Now trace it again + trace = gi.trace( nodes[from].origin, min, max, nodes[to].origin, self, MASK_SOLID); +// trace = gi.trace( nodes[from].origin, tv(-8,-8,0), tv(8,8,0), nodes[to].origin, self, MASK_SOLID); + if( trace.fraction < 1.0) + { + // can't get there for some reason + return; + } + // Add the link + path_table[from][to] = to; + + // Checks if the link exists and then may create a new one - RiEvEr + for( i=0; i %d\n", from, to); + break; + } + } + + // Now for the self-referencing part, linear time for each link added + for(i=0;i nodes[from].origin[2]+36) + && self->is_bot + ) + { + // If we are coming from a move or jump node + if( (nodes[from].type == NODE_MOVE) || + (nodes[from].type == NODE_JUMP) ) + { + // No if the to node is the same, it's illegal + if( (nodes[to].type == NODE_MOVE) || + (nodes[to].type == NODE_JUMP) ) + { + // Too high - not possible!! + return; + } + } + } + // Do not allow creation of nodes where the falling distance would kill you! + if( (nodes[from].origin[2]) > (nodes[to].origin[2] + 180) ) + return; + + + /* + VectorCopy(self->mins, min); + // If going up +// if( (nodes[from].origin[2]) < (nodes[to].origin[2]) ) + min[2] = 0; // Allow for steps up etc. + VectorCopy(self->maxs, max); + // If going down +// if( (nodes[from].origin[2]) > (nodes[to].origin[2]) ) + max[2] = 0; // door node linking + */ + +/* + VectorCopy( vec3_origin, min); + VectorCopy( vec3_origin, max); + + // Now trace it - more safety stuff! + trace = gi.trace( nodes[from].origin, min, max, nodes[to].origin, self, MASK_SOLID); + + if( trace.fraction < 1.0) + { + // can't do it + if(debug_mode) + debug_printf("Warning: (NoTrace) Failed to Link %d -> %d\n", from, to); + return; + } + // Add the link + path_table[from][to] = to; + + // Checks if the link exists and then may create a new one - RiEvEr + for( i=0; i %d\n", from, to); + break; + } + } + + // Now for the self-referencing part, linear time for each link added + for(i=0;i %d\n", self->client->pers.netname, from, to); + + ////path_table[from][to] = INVALID; // set to invalid + + // RiEvEr + // Now we must remove the link from the antpath system + // Get the link information + for( i=0; i [LINK]-[LINK]-[LINK]-[LINK]-[INVALID] <-- terminator [INVALID]-[INVALID] <-- up to MAXLINKS + //if(nodes[from].links[i].targetNode != INVALID) + if (i + 1 < MAXLINKS) + { + nodes[from].links[i].targetNode = nodes[from].links[i + 1].targetNode; + nodes[from].links[i].targetNodeType = nodes[from].links[i + 1].targetNodeType; + nodes[from].links[i].cost = nodes[from].links[i + 1].cost; + } + } + nodes[from].num_links--; // Reduce number of links + + //R + + // Make sure this gets updated in our path array + ////for(i=0;ivalue) return; // Ignore if dedicated server //rekkie -- DEV_1 + + version = LTK_NODEVERSION; + + game_dir = gi.cvar ("game", "action", 0); + + //@@ change 'nav' to 'terrain' to line up with William +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\terrain\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".ltk"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/terrain/"); + strcat(filename,level.mapname); + strcat(filename,".ltk"); +#endif + + // Resolve paths + ACEND_ResolveAllPaths(); + + gi.bprintf(PRINT_MEDIUM,"Saving node table..."); + + //strcpy(filename,"action\\nav\\"); + //strcat(filename,level.mapname); + //strcat(filename,".nod"); + + if((pOut = fopen(filename, "wb" )) == NULL) + return; // bail + + fwrite(&version,sizeof(int),1,pOut); // write version + fwrite(&numnodes,sizeof(int),1,pOut); // write count + fwrite(&num_items,sizeof(int),1,pOut); // write facts count + + fwrite(nodes,sizeof(node_t),numnodes,pOut); // write nodes + + for(i=0;istring); + i += sprintf(filename + i, "\\terrain\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".ltk"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/terrain/"); + strcat(filename,level.mapname); + strcat(filename,".ltk"); +#endif + + //strcpy(filename,"action\\nav\\"); + //strcat(filename,level.mapname); + //strcat(filename,".nod"); + + if((pIn = fopen(filename, "rb" )) == NULL) + { + // Create item table + gi.bprintf(PRINT_MEDIUM, "ACE: No node file found, creating new one..."); + ACEIT_BuildItemNodeTable(false); + gi.bprintf(PRINT_MEDIUM, "done.\n"); + return; + } + + // determin version + fread(&version,sizeof(int),1,pIn); // read version + + if(version == LTK_NODEVERSION) + { + gi.bprintf(PRINT_MEDIUM,"ACE: Loading node table..."); + + fread(&numnodes,sizeof(int),1,pIn); // read count + fread(&num_items,sizeof(int),1,pIn); // read facts count + + fread(nodes,sizeof(node_t),numnodes,pIn); + + for(i=0;i - -//AQ2 ADD -#define CONFIG_FILE_VERSION 1 - -void ClientBeginDeathmatch( edict_t *ent ); -void AllItems( edict_t *ent ); -void AllWeapons( edict_t *ent ); -void EquipClient( edict_t *ent ); -char *TeamName(int team); -void LTKsetBotName( char *bot_name ); -void LTKsetBotNameNew(char *bot_name); -void ACEAI_Cmd_Choose( edict_t *ent, char *s); - -//========================================== -// Joining a team (hacked from AQ2 code) -//========================================== -/* defined in a_team.h -#define NOTEAM 0 -#define TEAM1 1 -#define TEAM2 2 -*/ - -//============================== -// Get the number of the next team a bot should join -//============================== -int GetNextTeamNumber(void) -{ - int i, onteam1 = 0, onteam2 = 0, onteam3 = 0; - edict_t *e; - - // only use this function during [2]team games... - if (!teamplay->value ) - return 0; - -// gi.bprintf(PRINT_HIGH, "Checking team balance..\n"); - for (i = 1; i <= game.maxclients; i++) - { - e = g_edicts + i; - if (e->inuse) - { - if (e->client->resp.team == TEAM1) - onteam1++; - else if (e->client->resp.team == TEAM2) - onteam2++; - else if (e->client->resp.team == TEAM3) - onteam3++; - } - } - - // Return the team number that needs the next bot - if (use_3teams->value && (onteam3 < onteam1) && (onteam3 < onteam2)) - return (3); - else if (onteam2 < onteam1) - return (2); - //default - return (1); -} - -//========================== -// Join a Team -//========================== -void ACESP_JoinTeam(edict_t *ent, int desired_team) -{ - JoinTeam( ent, desired_team, true ); - CheckForUnevenTeams( ent ); -} - -//====================================== -// ACESP_LoadBotConfig() -//====================================== -// Using RiEvEr's new config file -// -void ACESP_LoadBotConfig(void) -{ - FILE *pIn; - cvar_t *game_dir = NULL, *botdir = NULL; -#ifdef _WIN32 - int i; -#endif - char filename[60]; - // Scanner stuff - int fileVersion = 0; - char inString[81]; - char tokenString[81]; - char *sp, *tp; - int ttype; - - game_dir = gi.cvar ("game", "action", 0); - botdir = gi.cvar ("botdir", "bots", 0); - -if (ltk_loadbots->value){ - - // Turning off stat collection since bots are enabled - gi.cvar_forceset(stat_logs->name, "0"); - // Try to load the file for THIS level - #ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, level.mapname); - i += sprintf(filename + i, ".cfg"); - #else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/"); - strcat(filename, level.mapname); - strcat(filename, ".cfg"); - #endif - - // If there's no specific file for this level, then - // load the file name from value ltk_botfile (default is botdata.cfg) - if((pIn = fopen(filename, "rb" )) == NULL) - { - #ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, ltk_botfile->string); - i += sprintf(filename + i, ".cfg"); - #else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/"); - strcat(filename, ltk_botfile->string); - strcat(filename, ".cfg"); - #endif - // No bot file available, get out of here! - if((pIn = fopen(filename, "rb" )) == NULL) { - gi.dprintf("WARNING: No file containing bot data was found, no bots loaded.\n"); - gi.dprintf("ltk_botfile value is %s\n", ltk_botfile->string); - return; // bail - } - } - - // Now scan each line for information - // First line should be the file version number - fgets( inString, 80, pIn ); - sp = inString; - tp = tokenString; - ttype = UNDEF; - - // Scan it for the version number - scanner( &sp, tp, &ttype ); - if(ttype == BANG) - { - scanner( &sp, tp, &ttype ); - if(ttype == INTLIT) - { - fileVersion = atoi( tokenString ); - } - if( fileVersion != CONFIG_FILE_VERSION ) - { - // ERROR! - gi.bprintf(PRINT_HIGH, "Bot Config file is out of date!\n"); - fclose(pIn); - return; - } - } - - // Now process each line of the config file - while( fgets(inString, 80, pIn) ) - { - ACESP_SpawnBotFromConfig( inString ); - } - - - - /* fread(&count,sizeof (int),1,pIn); - - for(i=0;istring, "0") == 0) { - bot = ACESP_SpawnBot( team_str, name, modelskin, userinfo ); - } else { - gi.dprintf("Warning: attract mode is enabled, I am not spawning bots from config.\n"); - } - - // FIXME: This might have to happen earlier to take effect. - if( bot ) - { - bot->weaponchoice = weaponchoice; - bot->equipchoice = equipchoice; - } - - return bot; -} -//AQ2 END - - -/////////////////////////////////////////////////////////////////////// -// Called by PutClient in Server to actually release the bot into the game -// Keep from killin' each other when all spawned at once -/////////////////////////////////////////////////////////////////////// -void ACESP_HoldSpawn(edict_t *self) -{ - if (!KillBox (self)) - { // could't spawn in? - } - - gi.linkentity (self); - -#ifdef ACE_SPAWN - // Use ACE bots - self->think = ACEAI_Think; -#else - self->think = BOTAI_Think; -#endif - - self->nextthink = level.framenum + 1; - - // send effect - gi.WriteByte (svc_muzzleflash); - gi.WriteShort (self-g_edicts); - gi.WriteByte (MZ_LOGIN); - gi.multicast (self->s.origin, MULTICAST_PVS); - -/* if(ctf->value) - gi.bprintf(PRINT_MEDIUM, "%s joined the %s team.\n", - self->client->pers.netname, CTFTeamName(self->client->resp.ctf_team)); - else*/ - gi.bprintf (PRINT_MEDIUM, "%s entered the game\n", self->client->pers.netname); - -} - -/////////////////////////////////////////////////////////////////////// -// Modified version of id's code -/////////////////////////////////////////////////////////////////////// -void ACESP_PutClientInServer( edict_t *bot, qboolean respawn, int team ) -{ - bot->is_bot = true; - - // Use 'think' to pass the value of respawn to PutClientInServer. - if( ! respawn ) - { - bot->think = ACESP_HoldSpawn; - bot->nextthink = level.framenum + random() * 3 * HZ; - } - else - { - #ifdef ACE_SPAWN - bot->think = ACEAI_Think; - #else - bot->think = BOTAI_Think; - #endif - bot->nextthink = level.framenum + 1; - } - - PutClientInServer( bot ); - - JoinTeam( bot, team, true ); -} - -/////////////////////////////////////////////////////////////////////// -// Respawn the bot -/////////////////////////////////////////////////////////////////////// -void ACESP_Respawn (edict_t *self) -{ - respawn( self ); - - if( random() < 0.05) - { - // Store current enemies available - int i, counter = 0; - edict_t *myplayer[MAX_BOTS]; - - if( self->lastkilledby) - { - // Have a comeback line! - if(ltk_chat->value) // Some people don't want this *sigh* - LTK_Chat( self, self->lastkilledby, DBC_KILLED); - self->lastkilledby = NULL; - } - else - { - // Pick someone at random to insult - for(i=0;i<=num_players;i++) - { - // Find all available enemies to insult - if(players[i] == NULL || players[i] == self || - players[i]->solid == SOLID_NOT) - continue; - - if(teamplay->value && OnSameTeam( self, players[i]) ) - continue; - myplayer[counter++] = players[i]; - } - if(counter > 0) - { - // Say something insulting to them! - if(ltk_chat->value) // Some people don't want this *sigh* - LTK_Chat( self, myplayer[rand()%counter], DBC_INSULT); - } - } - } -} - -/////////////////////////////////////////////////////////////////////// -// Find a free client spot -/////////////////////////////////////////////////////////////////////// -edict_t *ACESP_FindFreeClient (void) -{ - edict_t *bot = NULL; - int i = 0; - int max_count = 0; - - // This is for the naming of the bots - for (i = game.maxclients; i > 0; i--) - { - bot = g_edicts + i + 1; - - if(bot->count > max_count) - max_count = bot->count; - } - - // Check for free spot - for (i = game.maxclients; i > 0; i--) - { - bot = g_edicts + i + 1; - - if (!bot->inuse) - break; - } - - if (bot->inuse) - return NULL; - - bot->count = max_count + 1; // Will become bot name... - - return bot; -} - -/////////////////////////////////////////////////////////////////////// -// Set the name of the bot and update the userinfo -/////////////////////////////////////////////////////////////////////// -void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team) -{ - float rnd; - char userinfo[MAX_INFO_STRING]; - char bot_skin[MAX_INFO_STRING]; - char bot_name[MAX_INFO_STRING]; - - if( (!name) || !strlen(name) ) - { - // RiEvEr - new code to get random bot names - if(!am_newnames->value) - LTKsetBotName(bot_name); - else - LTKsetBotNameNew(bot_name); - } - else - strcpy(bot_name,name); - - // skin - if( (!skin) || !strlen(skin) ) - { - // randomly choose skin - rnd = random(); - if(rnd < 0.05) - sprintf(bot_skin,"male/bluebeard"); - else if(rnd < 0.1) - sprintf(bot_skin,"female/leeloop"); - else if(rnd < 0.15) - sprintf(bot_skin,"male/blues"); - else if(rnd < 0.2) - sprintf(bot_skin,"female/sarah_ohconnor"); - else if(rnd < 0.25) - sprintf(bot_skin,"actionmale/chucky"); - else if(rnd < 0.3) - sprintf(bot_skin,"actionmale/axef"); - else if(rnd < 0.35) - sprintf(bot_skin,"sas/sasurban"); - else if(rnd < 0.4) - sprintf(bot_skin,"terror/urbanterr"); - else if(rnd < 0.45) - sprintf(bot_skin,"aqmarine/urban"); - else if(rnd < 0.5) - sprintf(bot_skin,"sydney/sydney"); - else if(rnd < 0.55) - sprintf(bot_skin,"male/cajin"); - else if(rnd < 0.6) - sprintf(bot_skin,"aqmarine/desert"); - else if(rnd < 0.65) - sprintf(bot_skin,"male/grunt"); - else if(rnd < 0.7) - sprintf(bot_skin,"male/mclaine"); - else if(rnd < 0.75) - sprintf(bot_skin,"male/robber"); - else if(rnd < 0.8) - sprintf(bot_skin,"male/snowcamo"); - else if(rnd < 0.85) - sprintf(bot_skin,"terror/swat"); - else if(rnd < 0.9) - sprintf(bot_skin,"terror/jungleterr"); - else if(rnd < 0.95) - sprintf(bot_skin,"sas/saspolice"); - else - sprintf(bot_skin,"sas/sasuc"); - } - else - strcpy(bot_skin,skin); - - // initialise userinfo - memset (userinfo, 0, sizeof(userinfo)); - - // add bot's name/skin/hand to userinfo - Info_SetValueForKey (userinfo, "name", bot_name); - Info_SetValueForKey (userinfo, "skin", bot_skin); - Info_SetValueForKey (userinfo, "hand", "2"); // bot is center handed for now! -//AQ2 ADD - Info_SetValueForKey (userinfo, "spectator", "0"); // NOT a spectator -//AQ2 END - - ClientConnect (bot, userinfo); - -// ACESP_SaveBots(); // make sure to save the bots -} -//RiEvEr - new global to enable bot self-loading of routes -extern char current_map[55]; -// - -char *LocalTeamNames[4] = { "spectator", "1", "2", "3" }; - -/////////////////////////////////////////////////////////////////////// -// Spawn the bot -/////////////////////////////////////////////////////////////////////// -edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo ) -{ - int team = 0; - edict_t *bot = ACESP_FindFreeClient(); - if( ! bot ) - { - gi.bprintf( PRINT_MEDIUM, "Server is full, increase Maxclients.\n" ); - return NULL; - } - - bot->is_bot = true; - bot->yaw_speed = 1000; // deg/sec - - // To allow bots to respawn - if( ! userinfo ) { - // Classic naming method - ACESP_SetName( bot, name, skin, team_str ); // includes ClientConnect - } else { - ClientConnect( bot, userinfo ); - } - - ClientBeginDeathmatch( bot ); - - // Balance the teams! - if( teamplay->value ) - { - if( team_str ) - team = atoi( team_str ); - if( (team < TEAM1) || (team > teamCount) ) - team = GetNextTeamNumber(); - team_str = LocalTeamNames[ team ]; - } - - if(am->value) { - if(am_team->value && !teamplay->value){ - // am_team set but not teamplay - team = 0; - } - if(am_team->value){ - team = (int)am_team->value; - if ((!use_3teams->value) && (team >= TEAM3)){ - gi.dprintf("Warning: am_team was %d, but use_3teams is not enabled! Bots will default to team 1.\n", team); - gi.cvar_forceset("am_team", "1"); - team = 1; - } - } - - - } - - ACESP_PutClientInServer( bot, true, team ); - - ACEAI_PickLongRangeGoal(bot); // pick a new goal - - // LTK chat stuff - if( random() < 0.33) - { - // Store current enemies available - int i, counter = 0; - edict_t *myplayer[MAX_BOTS]; - - for(i=0;i<=num_players;i++) - { - // Find all available enemies to insult - if(players[i] == NULL || players[i] == bot || - players[i]->solid == SOLID_NOT) - continue; - - if(teamplay->value && OnSameTeam( bot, players[i]) ) - continue; - myplayer[counter++] = players[i]; - } - if(counter > 0) - { - // Say something insulting to them! - if(ltk_chat->value) // Some people don't want this *sigh* - LTK_Chat( bot, myplayer[rand()%counter], DBC_WELCOME); - } - } - return bot; -} - -void ClientDisconnect( edict_t *ent ); - -/////////////////////////////////////////////////////////////////////// -// Remove a bot by name or all bots -/////////////////////////////////////////////////////////////////////// -void ACESP_RemoveBot(char *name) -{ - int i; - qboolean freed=false; - edict_t *bot; - qboolean remove_all = (Q_stricmp(name,"all")==0) ? true : false; - int find_team = (strlen(name)==1) ? atoi(name) : 0; - -// if (name!=NULL) - for(i=0;iinuse) - { - if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) - { - bot->health = 0; - vec3_t non_const_origin; // Convert to non-const - VectorCopy(vec3_origin, non_const_origin); - player_die (bot, bot, bot, 100000, non_const_origin); - // don't even bother waiting for death frames -// bot->deadflag = DEAD_DEAD; -// bot->inuse = false; - freed = true; - ClientDisconnect( bot ); -// ACEIT_PlayerRemoved (bot); -// gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); - if( ! remove_all ) - break; - } - } - } - -/* - //Werewolf: Remove a random bot if no name given - if(!freed) -// gi.bprintf (PRINT_MEDIUM, "%s not found\n", name); - { - do - { - i = (int)(rand()) % (int)(game.maxclients); - bot = g_edicts + i + 1; - } while ( (!bot->inuse) || (!bot->is_bot) ); - bot->health = 0; - player_die (bot, bot, bot, 100000, vec3_origin); - freed = true; - ClientDisconnect( bot ); - } -*/ - if(!freed) - gi.bprintf (PRINT_MEDIUM, "No bot removed\n"); - -// ACESP_SaveBots(); // Save them again -} - -void attract_mode_bot_check(void) -{ - int maxclientsminus2 = (int)(maxclients->value - 2); - int adj = 0; - - ACEIT_RebuildPlayerList(); - // Some sanity checking before we proceed - if (am_botcount->value < 0){ - gi.cvar_forceset("am_botcount", "0"); - } - if (am_botcount->value >= maxclients->value) { - gi.cvar_forceset("am_botcount", va("%d", maxclientsminus2)); - if (warmup_bots->value){ - gi.cvar_forceset("warmup_bots", va("%d", maxclientsminus2)); - } - } - - // Cannot have the am_botcount at a value of N-2 of the maxclients - if ((am->value == 2) && (am_botcount->value >= maxclientsminus2)) - { - // If maxclients is 10 or more, set the botcount value to 6, else disable attract mode. Increase your maxclients! - if(maxclients->value >= 10){ - gi.dprintf( "am is 2, am_botcount is %d, maxclients is too low at %d, forcing it to default (6)\n", (int)am_botcount->value, (int)maxclients->value); - gi.cvar_forceset("am_botcount", "6"); - } else { - gi.dprintf( "am is 2, am_botcount is %d, maxclients is too low at %d, reducing bot count\n", (int)am_botcount->value, (int)maxclients->value); - adj = (maxclientsminus2 - 2); - gi.cvar_forceset("am_botcount", va("%d", adj)); - } - } - - int tgt_bot_count = (int)am_botcount->value; - int real_player_count = (num_players - game.bot_count); - - /* // Debug area, uncomment for gratiuitous amounts of spam - if (teamplay->value){ - gi.dprintf("Team 1: %d - Team 2: %d, - Team 3: %d\n", team1, team2, team3); - } - gi.dprintf("tgt_bot_count is %d, real_player_count is %d, num_players is %d, game.bot_count is %d\n", tgt_bot_count, real_player_count, num_players, game.bot_count); - */ // End debug area - - // Bot Maintenance - /* Logic is as follows: - If (am_botcount - real_player_count) == game.botcount - (Current bots - real players is equal to am_botcount value) - Then do nothing, we are where we want to be - - Else - - If (am_botcount - real_player_count) > game.botcount - (Current Bots + Real Players is less than the am_botcount value) - Then add a bot until these numbers are equal - - Else - - If (am_botcount - real_player_count) < game.botcount AND if am is 1 - (Current Bots + Real Players is more than the am_botcount value) - Then remove a bot until these numbers are equal - - Else - - If (total players == (maxclients - 1)) AND if am is 2 - (Current Bots + Real Players is more than the am_botcount value) - Then remove a bot only if we're near the maxclients number - - */ - - if (tgt_bot_count - real_player_count == game.bot_count) { - return; - } else if (tgt_bot_count - real_player_count > game.bot_count) { - //gi.dprintf("I'm adding a bot because %d - %d < %d", tgt_bot_count, real_player_count, game.bot_count); - ACESP_SpawnBot(NULL, NULL, NULL, NULL); - } else if ((tgt_bot_count - real_player_count < game.bot_count) && (am->value == 1)) { - // This removes 1 bot per real player - - //gi.dprintf("I'm removing a bot because %d - %d > %d", tgt_bot_count, real_player_count, game.bot_count); - ACESP_RemoveBot(""); - } else if ((num_players == maxclientsminus2) && (am->value == 2)) { - // This removes 1 bot once we are at maxclients - 2 so we have room for a real player - gi.dprintf("Removing a bot because num_players = %d and maxclients is %d", num_players, game.maxclients); - ACESP_RemoveBot(""); - } - -} - -//==================================== -// Stuff to generate pseudo-random names -//==================================== -char *names1[NUMNAMES] = { - "Bad", "Death", "L33t", "Fast", "Real", "Lethal", "Hyper", "Hard", "Angel", "Red"}; - -char *names2[NUMNAMES] = { - "Moon", "evil", "master", "dude", "killa", "dog", "chef", "dave", "Zilch", "Amator" }; - -char *names3[NUMNAMES] = { - "Ana", "Bale", "Calen", "Cor", "Fan", "Gil", "Hali", "Line", "Male", "Pero"}; - -char *names4[NUMNAMES] = { - "ders", "rog", "born", "dor", "fing", "galad", "bon", "loss", "orch", "riel" }; - -//==================================== -// AQ2World Staff Names -- come shoot at our bots! -// TODO: Find time to implement this better -//==================================== -char *aq2names[] = { - "[BOT]bAron", "[BOT]darksaint", "[BOT]FragBait", - "[BOT]matic", "[BOT]JukS", "[BOT]TgT", "[BOT]dmc", - "[BOT]dox", "[BOT]KaniZ", "[BOT]keffo", "[BOT]QuimBy" - - "Rezet", "Royce", "vrol", "mikota", - "Reki", "ReKTeK", "Ralle", "Tech", - - "-ROBO-JukS", "-ROBO-Nevi", "-ROBO-topdeck", - "-ROBO-dmc", "-ROBO-Raptor007", "-ROBO-Ferrick", - - "Igor[ROCK].bot", "Suislide.bot", "Bartender.bot", - "Fex.bot", "Shagg.bot", "Black Angel.bot", "Rookie.bot", - - "Fireblade<<", "Cail<<", "Gooseman<<", "Ace12GA<<", - "BlackMonk<<", "hal9k<<", "Fool Killer<<", "Inghaw<<", - - "_NME_GreyDeath", "_NME_Ellusion", "_NME_Deathwatch", - "_NME_Freud", "_NME_slicer", "_NME_JBravo", "_NME_Elviz" - }; - -// END AQ2World Staff Names // - -edict_t ltknames; -// New AQ2World team bot names (am_newnames 1) -void LTKsetBotNameNew(char *bot_name) -{ - int randomname = 0; - - // Check if all names have been used - qboolean allNamesUsed = true; - for (int i = 0; i < AQ2WTEAMSIZE; i++) { - if (!ltknames.newnameused[i]) { - allNamesUsed = false; - break; - } - } - - // If all names have been used, reset the newnameused array - if (allNamesUsed) { - LTKClearBotNames(); - } - - do - { - randomname = rand() % AQ2WTEAMSIZE; - if (!ltknames.newnameused[randomname]) - { - ltknames.newnameused[randomname] = true; - break; - } - } while (ltknames.newnameused[randomname]); - - strcpy(bot_name, aq2names[randomname]); - - return; -} - -//==================================== -// Classic random bot naming routine -//==================================== -void LTKsetBotName( char *bot_name ) -{ - int part1,part2; - part1 = part2 = 0; - edict_t ltknames; - - do - { - part1 = rand()% NUMNAMES; - part2 = rand()% NUMNAMES; - }while( ltknames.nameused[part1][part2]); - - // Mark that name as used - // TODO: This is causing crashes, figure out another way to mark them as used - - ltknames.nameused[part1][part2] = true; - - // Now put the name together - if( random() < 0.5 ) - { - strcpy( bot_name, names1[part1]); - strcat( bot_name, names2[part2]); - } - else - { - strcpy( bot_name, names3[part1]); - strcat( bot_name, names4[part2]); - } -} - - -//==================================== -// LTKCLearBotNames -- Reset the bot name array -//==================================== -void LTKClearBotNames(void) { - edict_t ltknames; - int i, j; - for (i = 0; i < NUMNAMES; i++) { - for (j = 0; j < NUMNAMES; j++) { - ltknames.nameused[i][j] = false; // Reset all elements to false - } - } - for (i = 0; i < AQ2WTEAMSIZE; i++) { - ltknames.newnameused[i] = false; // Reset all elements to false - } -} \ No newline at end of file +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot_spawn.c 6 24/02/00 20:05 Riever $ + * + * $Log: /LTK2/src/acesrc/acebot_spawn.c $ + * + * 6 24/02/00 20:05 Riever + * Team Radio use fixed up. (Using ACE to test) + * + * Manual addition: Radio 'treport' support added. + * User: Riever Date: 21/02/00 Time: 23:42 + * Changed initial spawn state to BS_ROAM + * User: Riever Date: 21/02/00 Time: 15:16 + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * User: Riever Date: 20/02/00 Time: 20:27 + * Added new members and definitions ready for 2nd generation of bots. + * User: Riever Date: 17/02/00 Time: 17:10 + * Radiogender now set for bots. + * + */ + +/////////////////////////////////////////////////////////////////////// +// +// acebot_spawn.c - This file contains all of the +// spawing support routines for the ACE bot. +// +/////////////////////////////////////////////////////////////////////// + +// Define this for an ACE bot +#define ACE_SPAWN +// ---- + +#include "../g_local.h" +#include "../m_player.h" + +#include "acebot.h" +#include "botchat.h" +#include "botscan.h" + +//AQ2 ADD +#define CONFIG_FILE_VERSION 1 + +void ClientBeginDeathmatch( edict_t *ent ); +void AllItems( edict_t *ent ); +void AllWeapons( edict_t *ent ); +void EquipClient( edict_t *ent ); +char *TeamName(int team); +void LTKsetBotName( char *bot_name ); +void ACEAI_Cmd_Choose( edict_t *ent, char *s); + +//========================================== +// Joining a team (hacked from AQ2 code) +//========================================== +/* defined in a_team.h +#define NOTEAM 0 +#define TEAM1 1 +#define TEAM2 2 +*/ + +//============================== +// Get the number of the next team a bot should join +//============================== +int GetNextTeamNumber() +{ + int i, onteam1 = 0, onteam2 = 0, onteam3 = 0; + edict_t *e; + + // only use this function during [2]team games... + if (!teamplay->value ) + return 0; + +// gi.bprintf(PRINT_HIGH, "Checking team balance..\n"); + for (i = 1; i <= game.maxclients; i++) + { + e = g_edicts + i; + if (e->inuse) + { + if (e->client->resp.team == TEAM1) + onteam1++; + else if (e->client->resp.team == TEAM2) + onteam2++; + else if (e->client->resp.team == TEAM3) + onteam3++; + } + } + + // Return the team number that needs the next bot + if (use_3teams->value && (onteam3 < onteam1) && (onteam3 < onteam2)) + return (3); + else if (onteam2 < onteam1) + return (2); + //default + return (1); +} + +//========================== +// Join a Team +//========================== +void ACESP_JoinTeam(edict_t *ent, int desired_team) +{ + JoinTeam( ent, desired_team, true ); + CheckForUnevenTeams( ent ); +} + +//====================================== +// ACESP_LoadBotConfig() +//====================================== +// Using RiEvEr's new config file +// +void ACESP_LoadBotConfig() +{ + FILE *pIn; + cvar_t *game_dir = NULL, *botdir = NULL; +#ifdef _WIN32 + int i; +#endif + char filename[60]; + // Scanner stuff + int fileVersion = 0; + char inString[81]; + char tokenString[81]; + char *sp, *tp; + int ttype; + + game_dir = gi.cvar ("game", "action", 0); + botdir = gi.cvar ("botdir", "bots", 0); + +if (ltk_loadbots->value){ + + // Turning off stat collection since bots are enabled + gi.cvar_forceset(stat_logs->name, "0"); + // Try to load the file for THIS level + #ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".cfg"); + #else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, level.mapname); + strcat(filename, ".cfg"); + #endif + + // If there's no specific file for this level, then + // load the file name from value ltk_botfile (default is botdata.cfg) + if((pIn = fopen(filename, "rb" )) == NULL) + { + #ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, ltk_botfile->string); + #else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, ltk_botfile->string); + #endif + + // No bot file available, get out of here! + if((pIn = fopen(filename, "rb" )) == NULL) { + gi.dprintf("WARNING: No file containing bot data was found, no bots loaded.\n"); + gi.dprintf("ltk_botfile value is %s\n", ltk_botfile->string); + return; // bail + } + } + + // Now scan each line for information + // First line should be the file version number + fgets( inString, 80, pIn ); + sp = inString; + tp = tokenString; + ttype = UNDEF; + + // Scan it for the version number + scanner( &sp, tp, &ttype ); + if(ttype == BANG) + { + scanner( &sp, tp, &ttype ); + if(ttype == INTLIT) + { + fileVersion = atoi( tokenString ); + } + if( fileVersion != CONFIG_FILE_VERSION ) + { + // ERROR! + gi.bprintf(PRINT_HIGH, "Bot Config file is out of date!\n"); + fclose(pIn); + return; + } + } + + // Now process each line of the config file + while( fgets(inString, 80, pIn) ) + { + ACESP_SpawnBotFromConfig( inString ); + } + + + + /* fread(&count,sizeof (int),1,pIn); + + for(i=0;iweaponchoice = weaponchoice; + bot->equipchoice = equipchoice; + } + + return bot; +} +//AQ2 END + + +/////////////////////////////////////////////////////////////////////// +// Called by PutClient in Server to actually release the bot into the game +// Keep from killin' each other when all spawned at once +/////////////////////////////////////////////////////////////////////// +void ACESP_HoldSpawn(edict_t *self) +{ + if (!KillBox (self)) + { // could't spawn in? + } + + gi.linkentity (self); + +#ifdef ACE_SPAWN + // Use ACE bots + self->think = ACEAI_Think; +#else + self->think = BOTAI_Think; +#endif + + self->nextthink = level.framenum + 1; + + // send effect + gi.WriteByte (svc_muzzleflash); + gi.WriteShort (self-g_edicts); + gi.WriteByte (MZ_LOGIN); + gi.multicast (self->s.origin, MULTICAST_PVS); + +/* if(ctf->value) + gi.bprintf(PRINT_MEDIUM, "%s joined the %s team.\n", + self->client->pers.netname, CTFTeamName(self->client->resp.ctf_team)); + else*/ + gi.bprintf (PRINT_MEDIUM, "%s entered the game\n", self->client->pers.netname); + +} + +/////////////////////////////////////////////////////////////////////// +// Modified version of id's code +/////////////////////////////////////////////////////////////////////// +void ACESP_PutClientInServer( edict_t *bot, qboolean respawn, int team ) +{ + bot->is_bot = true; + + // Use 'think' to pass the value of respawn to PutClientInServer. + if( ! respawn ) + { + bot->think = ACESP_HoldSpawn; + bot->nextthink = level.framenum + random() * 3 * HZ; + } + else + { + #ifdef ACE_SPAWN + bot->think = ACEAI_Think; + #else + bot->think = BOTAI_Think; + #endif + bot->nextthink = level.framenum + 1; + } + + PutClientInServer( bot ); + + + + JoinTeam( bot, team, true ); +} + +/////////////////////////////////////////////////////////////////////// +// Respawn the bot +/////////////////////////////////////////////////////////////////////// +void ACESP_Respawn (edict_t *self) +{ + respawn( self ); + + if( random() < 0.15) + { + // Store current enemies available + int i, counter = 0; + edict_t *myplayer[MAX_BOTS]; + + if( self->lastkilledby) + { + // Have a comeback line! + if(ltk_chat->value) // Some people don't want this *sigh* + LTK_Chat( self, self->lastkilledby, DBC_KILLED); + self->lastkilledby = NULL; + } + else + { + // Pick someone at random to insult + for(i=0;i<=num_players;i++) + { + // Find all available enemies to insult + if(players[i] == NULL || players[i] == self || + players[i]->solid == SOLID_NOT) + continue; + + if(teamplay->value && OnSameTeam( self, players[i]) ) + continue; + myplayer[counter++] = players[i]; + } + if(counter > 0) + { + // Say something insulting to them! + if(ltk_chat->value) // Some people don't want this *sigh* + LTK_Chat( self, myplayer[rand()%counter], DBC_INSULT); + } + } + } +} + +/////////////////////////////////////////////////////////////////////// +// Find a free client spot +/////////////////////////////////////////////////////////////////////// +edict_t *ACESP_FindFreeClient (void) +{ + edict_t *bot = NULL; + int i = 0; + int max_count = 0; + + // This is for the naming of the bots + for (i = game.maxclients; i > 0; i--) + { + bot = g_edicts + i + 1; + + if(bot->count > max_count) + max_count = bot->count; + } + + // Check for free spot + for (i = game.maxclients; i > 0; i--) + { + bot = g_edicts + i + 1; + + if (!bot->inuse) + break; + } + + if (bot->inuse) + return NULL; + + bot->count = max_count + 1; // Will become bot name... + + return bot; +} + +/////////////////////////////////////////////////////////////////////// +// Set the name of the bot and update the userinfo +/////////////////////////////////////////////////////////////////////// +void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team) +{ + float rnd; + char userinfo[MAX_INFO_STRING]; + char bot_skin[MAX_INFO_STRING]; + + // initialise userinfo + memset(userinfo, 0, sizeof(userinfo)); + + //rekkie -- DEV_1 -- s + // + // Original code + /* + char bot_name[MAX_INFO_STRING]; + // Set the name for the bot. + // name + if( (!name) || !strlen(name) ) + { + // RiEvEr - new code to get random bot names + LTKsetBotName(bot_name); + } + else + strcpy(bot_name,name); + */ + // + // Modified code + // + DC_LoadRandomBotName(userinfo); + //rekkie -- DEV_1 -- e + + gi.cvar_forceset(stat_logs->name, "0"); // Turning off stat collection since bots are enabled + + // skin + if( (!skin) || !strlen(skin) ) + { + // randomly choose skin + rnd = random(); + if(rnd < 0.05) + sprintf(bot_skin,"male/bluebeard"); + else if(rnd < 0.1) + sprintf(bot_skin,"female/brianna"); + else if(rnd < 0.15) + sprintf(bot_skin,"male/blues"); + else if(rnd < 0.2) + sprintf(bot_skin,"female/ensign"); + else if(rnd < 0.25) + sprintf(bot_skin,"female/jezebel"); + else if(rnd < 0.3) + sprintf(bot_skin,"female/jungle"); + else if(rnd < 0.35) + sprintf(bot_skin,"sas/sasurban"); + else if(rnd < 0.4) + sprintf(bot_skin,"terror/urbanterr"); + else if(rnd < 0.45) + sprintf(bot_skin,"female/venus"); + else if(rnd < 0.5) + sprintf(bot_skin,"sydney/sydney"); + else if(rnd < 0.55) + sprintf(bot_skin,"male/cajin"); + else if(rnd < 0.6) + sprintf(bot_skin,"male/commando"); + else if(rnd < 0.65) + sprintf(bot_skin,"male/grunt"); + else if(rnd < 0.7) + sprintf(bot_skin,"male/mclaine"); + else if(rnd < 0.75) + sprintf(bot_skin,"male/robber"); + else if(rnd < 0.8) + sprintf(bot_skin,"male/snowcamo"); + else if(rnd < 0.85) + sprintf(bot_skin,"terror/swat"); + else if(rnd < 0.9) + sprintf(bot_skin,"terror/jungleterr"); + else if(rnd < 0.95) + sprintf(bot_skin,"sas/saspolice"); + else + sprintf(bot_skin,"sas/sasuc"); + } + else + strcpy(bot_skin,skin); + + + + // add bot's name/skin/hand to userinfo + //Info_SetValueForKey (userinfo, "name", bot_name); + + Info_SetValueForKey (userinfo, "skin", bot_skin); + Info_SetValueForKey (userinfo, "hand", "2"); // bot is center handed for now! +//AQ2 ADD + Info_SetValueForKey (userinfo, "spectator", "0"); // NOT a spectator +//AQ2 END + + ClientConnect (bot, userinfo); + +// ACESP_SaveBots(); // make sure to save the bots +} +//RiEvEr - new global to enable bot self-loading of routes +extern char current_map[55]; +// + +char *LocalTeamNames[4] = { "spectator", "1", "2", "3" }; + +/////////////////////////////////////////////////////////////////////// +// Spawn the bot +/////////////////////////////////////////////////////////////////////// +edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo ) +{ + int team = 0; + edict_t *bot = ACESP_FindFreeClient(); + if( ! bot ) + { + gi.bprintf( PRINT_MEDIUM, "Server is full, increase Maxclients.\n" ); + return NULL; + } + + bot->is_bot = true; + bot->yaw_speed = 1000; // deg/sec + + // To allow bots to respawn + if( ! userinfo ) + ACESP_SetName( bot, name, skin, team_str ); // includes ClientConnect + else + ClientConnect( bot, userinfo ); + + ClientBeginDeathmatch( bot ); + + // Balance the teams! + if( teamplay->value ) + { + if( team_str ) + team = atoi( team_str ); + if( (team < TEAM1) || (team > teamCount) ) + team = GetNextTeamNumber(); + team_str = LocalTeamNames[ team ]; + } + + ACESP_PutClientInServer( bot, true, team ); + + //rekkie -- Fake Bot Client -- s + // Set the average ping this bot will see + if (random() < 0.85) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 60)); // Country ping + else + bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping + gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //rekkie -- Fake Bot Client -- e + + //bot->wander_timeout = level.framenum + 60.0 * HZ; + //ACEAI_PickLongRangeGoal(bot); // pick a new goal + + // LTK chat stuff + if(0 && random() < 0.33) + { + // Store current enemies available + int i, counter = 0; + edict_t *myplayer[MAX_BOTS]; + + for(i=0;i<=num_players;i++) + { + // Find all available enemies to insult + if(players[i] == NULL || players[i] == bot || + players[i]->solid == SOLID_NOT) + continue; + + if(teamplay->value && OnSameTeam( bot, players[i]) ) + continue; + myplayer[counter++] = players[i]; + } + if(counter > 0) + { + // Say something insulting to them! + if(ltk_chat->value) // Some people don't want this *sigh* + LTK_Chat( bot, myplayer[rand()%counter], DBC_WELCOME); + } + } + + return bot; +} + +void ClientDisconnect( edict_t *ent ); + +/////////////////////////////////////////////////////////////////////// +// Remove a bot by name or all bots +/////////////////////////////////////////////////////////////////////// +void ACESP_RemoveBot(char *name) +{ + int i; + qboolean freed=false; + edict_t *bot; + qboolean remove_all = (Q_stricmp(name,"all")==0) ? true : false; + int find_team = (strlen(name)==1) ? atoi(name) : 0; + +// if (name!=NULL) + for(i=0;iinuse) + { + if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + bot->health = 0; + player_die (bot, bot, bot, 100000, vec3_origin); + // don't even bother waiting for death frames +// bot->deadflag = DEAD_DEAD; +// bot->inuse = false; + freed = true; + ClientDisconnect( bot ); +// ACEIT_PlayerRemoved (bot); +// gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + if( ! remove_all ) + break; + } + } + } + +/* + //Werewolf: Remove a random bot if no name given + if(!freed) +// gi.bprintf (PRINT_MEDIUM, "%s not found\n", name); + { + do + { + i = (int)(rand()) % (int)(game.maxclients); + bot = g_edicts + i + 1; + } while ( (!bot->inuse) || (!bot->is_bot) ); + bot->health = 0; + player_die (bot, bot, bot, 100000, vec3_origin); + freed = true; + ClientDisconnect( bot ); + } +*/ + if(!freed) + gi.bprintf (PRINT_MEDIUM, "No bot removed\n", name); + +// ACESP_SaveBots(); // Save them again +} + +//rekkie -- DEV_1 -- s +/////////////////////////////////////////////////////////////////////// +// Remove a bot by team +// Conditions: Bot must be dead or joined a team during an ongoing round +/////////////////////////////////////////////////////////////////////// +void ACESP_RemoveTeamplayBot(int team) +{ + int i; + edict_t* bot; + + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) // Ent in use + { + if (bot->is_bot) // Is a bot + { + // Only kick when the bot isn't actively in a match + //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) + if (bot->client->resp.team == team && team_round_going == 0) + { + if (random() < 0.20) // Randomly kick a bot + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + if (bot->health) + player_die(bot, bot, bot, 100000, vec3_origin); + ClientDisconnect(bot); + break; + } + } + } + } + } +} +//rekkie -- DEV_1 -- e + +//==================================== +// Stuff to generate pseudo-random names +//==================================== +#define NUMNAMES 10 +char *names1[NUMNAMES] = { + "Bad", "Death", "L33t", "Fast", "Real", "Lethal", "Hyper", "Hard", "Angel", "Red"}; + +char *names2[NUMNAMES] = { + "Moon", "evil", "master", "dude", "killa", "dog", "chef", "dave", "Zilch", "Amator" }; + +char *names3[NUMNAMES] = { + "Ana", "Bale", "Calen", "Cor", "Fan", "Gil", "Hali", "Line", "Male", "Pero"}; + +char *names4[NUMNAMES] = { + "ders", "rog", "born", "dor", "fing", "galad", "bon", "loss", "orch", "riel" }; + +qboolean nameused[NUMNAMES][NUMNAMES]; + +//==================================== +// AQ2World Staff Names -- come shoot at us! +// Find time to implement this! Or better yet, +// load names from a file rather than this array +//==================================== +// #define AQ2WORLDNUMNAMES 14 +// char *aq2names[AQ2WORLDNUMNAMES] = { +// "bAron", "darksaint", "FragBait", "matic", "stan0x", "TgT", "dmc", "dox", "KaniZ", "keffo", "QuimBy", "Rezet", "Royce", "vrol" +// }; +//qboolean adminnameused[AQ2WORLDNUMNAMES]; +// END AQ2World Staff Names // + +//==================================== +// New random bot naming routine +//==================================== +void LTKsetBotName( char *bot_name ) +{ + int part1,part2; + part1 = part2 = 0; + + do // Load random bot names from NUMNAMES lists + { + part1 = rand()% NUMNAMES; + part2 = rand()% NUMNAMES; + }while( nameused[part1][part2]); + + // Mark that name as used + nameused[part1][part2] = true; + // Now put the name together + if( random() < 0.5 ) + { + strcpy( bot_name, names1[part1]); + strcat( bot_name, names2[part2]); + } + else + { + strcpy( bot_name, names3[part1]); + strcat( bot_name, names4[part2]); + } +} + +//rekkie -- DEV_1 -- s +/* +void DC_CacheRandomBotNames(void) +{ + int i, n; + + // Cache male names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_male[n]); + n++; + } + // Cache female names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_female[n]); + n++; + } +} +void DC_GetRandomBotName(byte gender, char* bot_name) +{ + if (gender == GENDER_MALE) + { + //Q_strlcpy(bot_name, bot_male[n], name_length); + } + else if (gender == GENDER_FEMALE) + { + + } + else + { + if (random() < 0.5) + { + + } + else + { + + } + } +} +*/ + +//====================================== +// DC_GetRandomClanSymbol() +// Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc +// Returns char +//====================================== +int DC_GetRandomClanSymbol() +{ + int sym = rand() % 27; + switch (sym) + { + case 0: return '!'; + case 1: return '#'; + case 2: return '$'; + case 3: return '%'; + case 4: return '&'; + case 5: return '('; + case 6: return ')'; + case 7: return '*'; + case 8: return '+'; + case 9: return ','; + case 10: return '-'; + case 11: return '.'; + case 12: return '/'; + case 13: return ':'; + case 14: return '<'; + case 15: return '='; + case 16: return '>'; + case 17: return '?'; + case 18: return '@'; + case 19: return '['; + case 20: return ']'; + case 21: return '\\'; + case 22: return '^'; + case 23: return '_'; + case 24: return '{'; + case 25: return '|'; + case 26: return '}'; + default: return ' '; + } +} + +//====================================== +// DC_GetOpposingClanSymbol() +// If a symbol has an opposite: ie --> [ ], ( ), { }, < >, etc +// Returns opposite side, else returns the input symbol +//====================================== +int DC_GetOpposingClanSymbol(char symbol) +{ + if (symbol == '[') + return ']'; + if (symbol == ']') + return '['; + + if (symbol == '(') + return ')'; + if (symbol == ')') + return '('; + + if (symbol == '{') + return '}'; + if (symbol == '}') + return '{'; + + if (symbol == '<') + return '>'; + if (symbol == '>') + return '<'; + + if (symbol == '\\') + return '/'; + if (symbol == '/') + return '\\'; + + return symbol; +} + + +//====================================== +// DC_GetRandomClanLetter() +// Gets a random clan letter +// Returns char +//====================================== +int DC_GetRandomClanLetter() +{ + // Gets a random ASCII letter between 65 and 90, or 97 and 122 + int letter = rand() % 52; + switch (letter) + { + case 0: return 'A'; + case 1: return 'B'; + case 2: return 'C'; + case 3: return 'D'; + case 4: return 'E'; + case 5: return 'F'; + case 6: return 'G'; + case 7: return 'H'; + case 8: return 'I'; + case 9: return 'J'; + case 10: return 'K'; + case 11: return 'L'; + case 12: return 'M'; + case 13: return 'N'; + case 14: return 'O'; + case 15: return 'P'; + case 16: return 'Q'; + case 17: return 'R'; + case 18: return 'S'; + case 19: return 'T'; + case 20: return 'U'; + case 21: return 'V'; + case 22: return 'W'; + case 23: return 'X'; + case 24: return 'Y'; + case 25: return 'Z'; + case 26: return 'a'; + case 27: return 'b'; + case 28: return 'c'; + case 29: return 'd'; + case 30: return 'e'; + case 31: return 'f'; + case 32: return 'g'; + case 33: return 'h'; + case 34: return 'i'; + case 35: return 'j'; + case 36: return 'k'; + case 37: return 'l'; + case 38: return 'm'; + case 39: return 'n'; + case 40: return 'o'; + case 41: return 'p'; + case 42: return 'q'; + case 43: return 'r'; + case 44: return 's'; + case 45: return 't'; + case 46: return 'u'; + case 47: return 'v'; + case 48: return 'w'; + case 49: return 'x'; + case 50: return 'y'; + case 51: return 'z'; + default: return ' '; + } +} + + +//====================================== +// DC_LoadRandomBotName() +// Gets a random bot name from file; bot_name is limited to a maximum of 16 chars, and the size of the list can be up to random()'s maximum of 32,767 +// Genders can be: GENDER_MALE, GENDER_FEMALE, GENDER_NEUTRAL +// Returns bot_name +//====================================== +void DC_LoadRandomBotName(char *userinfo) +{ + // File stuff + FILE* f; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + char filename[MAX_QPATH]; // Filename to load from + int comments_num = 0; // Keep track of how many comments the file has + int line_num = 0; // Keep track of lines + int curr_len; // Current length of the line + char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) + curr_line[0] = '\0'; +#ifdef WIN32 + int i; // Keep track where we are in the filename array +#endif + + // Bot stuff + char bot_name[MAX_QPATH]; // Full bot name + qboolean name_has_prefix = false; // If the name gained a prefix + qboolean name_has_clan = false; // If the name gained a clan + int gender; // Bot gender + + // Try to apply either a clan tag, or a prefix to the start of the name + float prefix = random(); + if (prefix < 0.2) + { + name_has_prefix = true; + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + + if (prefix < 0.1) // Clan tag + i += sprintf(filename + i, "clans.txt"); + else // Prefix + i += sprintf(filename + i, "prefix.txt"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + if (prefix < 0.1) // Clan tag + strcat(filename, "clans.txt"); + else // Prefix + strcat(filename, "prefix.txt"); + +#endif + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + gi.dprintf("%s failed to open bot file: %s\n", __func__, filename); + return; + } + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (curr_line[0] == '/') // Skip comment lines + { + comments_num++; + continue; + } + + line_num++; + } + fclose(f); + + int random_line; + if (line_num - comments_num > 0) + random_line = rand() % (line_num - comments_num); + else + return; // File has no names + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + line_num = 0; + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (feof(f)) + { + // Could not get a prefix name + break; + } + // If we're at or above the correct line, not a comment, not empty + if (line_num >= random_line && curr_line[0] != '/' && curr_line[0]) + { + curr_len = strlen(curr_line); + Q_strlcpy(bot_name, curr_line, curr_len); + break; + } + else + line_num++; // Either we're not at random_line, or we're on a comment line + } + fclose(f); + } + } + else if (prefix < 0.3) // Random gen + { + name_has_clan = true; + + byte tag = 0; + char outer_symbol = 0; + char inner_symbol = 0; + + outer_symbol = DC_GetRandomClanSymbol(); + if (random() < 0.5) // 50% chance to add an inner symbol + inner_symbol = DC_GetRandomClanSymbol(); + + // Add the prefix tag + bot_name[tag] = outer_symbol; + if (inner_symbol) + bot_name[++tag] = inner_symbol; + + // Add the clan acronym + bot_name[++tag] = DC_GetRandomClanLetter(); + bot_name[++tag] = DC_GetRandomClanLetter(); + if (random() < 0.25) bot_name[++tag] = DC_GetRandomClanLetter(); // 50% chance to add another clan letter + + // Add the suffix tag + if (inner_symbol) + bot_name[++tag] = DC_GetOpposingClanSymbol(inner_symbol); // Add the opposing symbol, if any + bot_name[++tag] = DC_GetOpposingClanSymbol(outer_symbol); + + // print the bot name + bot_name[++tag] = '\0'; + //Com_Printf("CLAN TAG %s\n", bot_name); + } + + + + + // Pick a random name + float rnd_gender = random(); + if (rnd_gender < 0.15) + { + gender = GENDER_MALE; + Info_SetValueForKey(userinfo, "gender", "male"); + } + else if (rnd_gender < 0.30) + { + gender = GENDER_FEMALE; + Info_SetValueForKey(userinfo, "gender", "female"); + } + else + { + gender = GENDER_NEUTRAL; + Info_SetValueForKey(userinfo, "gender", "none"); + } + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + if (gender == GENDER_MALE) + i += sprintf(filename + i, "males.txt"); + else if (gender == GENDER_FEMALE) + i += sprintf(filename + i, "females.txt"); + else + i += sprintf(filename + i, "other.txt"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + if (gender == GENDER_MALE) + strcat(filename, "males.txt"); + else if (gender == GENDER_FEMALE) + strcat(filename, "females.txt"); + else + strcat(filename, "other.txt"); +#endif + + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + gi.dprintf("%s failed to open bot name file: %s\n", __func__, filename); + return; + } + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (curr_line[0] == '/') // Skip comment lines + { + comments_num++; + continue; + } + + line_num++; + } + fclose(f); + + int random_line; + if (line_num - comments_num > 0) + random_line = rand() % (line_num - comments_num); + else + return; // File has no names + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + line_num = 0; + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (feof(f)) + { + //gi.dprintf("%s could not generate a bot name, reached EOF on \n", __func__, filename); + Q_strlcpy(bot_name, "AqtionMan", 9); + break; + } + // If we're at or above the correct line and its not a comment + if (line_num >= random_line && curr_line[0] != '/') + { + curr_len = strlen(curr_line); + + if (name_has_prefix) + { + Q_strncpyz(bot_name, va("%s %s", bot_name, curr_line), sizeof(bot_name)); // Space between prefix and name + } + else if (name_has_clan) + { + if (random() < 0.5) + Q_strncpyz(bot_name, va("%s%s", bot_name, curr_line), sizeof(bot_name)); // Space between clan and name + else + Q_strncpyz(bot_name, va("%s%s", bot_name, curr_line), sizeof(bot_name)); // No space between clan and name + } + else + Q_strncpyz(bot_name, curr_line, sizeof(bot_name)); + + + break; + } + else + line_num++; // Either we're not at random_line, or we're on a comment line + } + fclose(f); + } + + + + // Try to apply a postfix to the end of the name + float postfix = random(); + if (postfix < 0.1) + { + //name_has_prefix = true; + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, "postfix.txt"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, "prefix.txt"); + +#endif + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + gi.dprintf("%s failed to open bot file: %s\n", __func__, filename); + return; + } + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (curr_line[0] == '/') // Skip comment lines + { + comments_num++; + continue; + } + + line_num++; + } + fclose(f); + + int random_line; + if (line_num - comments_num > 0) + random_line = rand() % (line_num - comments_num); + else + return; // File has no names + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + line_num = 0; + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + if (feof(f)) + { + // Could not get a prefix name + break; + } + // If we're at or above the correct line and its not a comment + if (line_num >= random_line && curr_line[0] != '/') + { + Q_strncpyz(bot_name, va("%s %s", bot_name, curr_line), sizeof(bot_name)); + //Q_strlcat(bot_name, curr_line, curr_len); + break; + } + else + line_num++; // Either we're not at random_line, or we're on a comment line + } + fclose(f); + } + } + + + + Info_SetValueForKey(userinfo, "name", bot_name); + + + /* + curr_len = strlen(curr_line); + if (curr_len > 0) // && bot_name[curr_len - 1] == '\n') + { + if (curr_len > name_length) + { + curr_line[curr_len] = '\0'; + Com_Printf("%s bot name %s is limited to 16 chars, found %i\n", __func__, curr_line, curr_len); + bot_name[name_length] = '\0'; + //Com_Printf("%s using bot name %s\n", __func__, bot_name); + } + else + { + bot_name[curr_len] = '\0'; + } + } + else + { + gi.dprintf("%s could not generate a bot name\n", __func__); + } + */ +} +//rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/acesrc/bot_ai.c b/src/action/acesrc/bot_ai.c index 145f096a6..c52e45955 100644 --- a/src/action/acesrc/bot_ai.c +++ b/src/action/acesrc/bot_ai.c @@ -1,1037 +1,1037 @@ -//--------------------------------------------- -// BOT_AI.C -//--------------------------------------------- - -#include "../g_local.h" -#include "../m_player.h" - -/* - * $Log: /LTK2/src/acesrc/bot_ai.c $ - * - * 6 2/03/00 17:47 Riever - * Removed debug prints and tidied up code - * - * 5 2/03/00 14:09 Riever - * Added NeedToBandage() - * - * 4 21/02/00 23:43 Riever - * Added goal checking code and fleshed out AI state. Now selects visible - * enemies but doesn't search for them yet. - * - * 3 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * - * 2 20/02/00 20:27 Riever - * Added new members and definitions ready for 2nd generation of bots. - */ - -//---------------------------------------------- -// Set up the goal -//---------------------------------------------- -void BOTAI_SetGoal(edict_t *self, int goal_node) -{ - self->goal_node = goal_node; - self->next_node = self->current_node; // make sure we get to the nearest node first - self->node_timeout = 0; -} - -//---------------------------------------------- -// BOTAI_PickShortRangeGoal -// -// (Modified from ACE) -// Pick best goal based on importance and range. This function -// overrides the long range goal selection for items that -// are very close to the bot and are reachable. -//---------------------------------------------- - -void BOTAI_PickShortRangeGoal(edict_t *bot) -{ - edict_t *pTarget = NULL; - float fWeight = 0.f, fBestWeight = 0.0; - edict_t *pBest = NULL; - - // look for a target (should make more efficient later) - pTarget = findradius(NULL, bot->s.origin, 200); - - while(pTarget) - { - if(pTarget->classname == NULL) - return; - - // Missle avoidance code - // Set our movetarget to be the rocket or grenade fired at us. - if( - strcmp(pTarget->classname,"rocket")==0 || - strcmp(pTarget->classname,"grenade")==0 || - strcmp(pTarget->classname,"hgrenade")==0 - ) - { - if(debug_mode) - debug_printf("ROCKET ALERT!\n"); - - bot->movetarget = pTarget; - return; - } - - if (BOTCOL_CanReachItem(bot,pTarget->s.origin)) - { - if (infront(bot, pTarget)) - { - fWeight = ACEIT_ItemNeed( bot, pTarget ); - - if(fWeight > fBestWeight) - { - fBestWeight = fWeight; - pBest = pTarget; - } - } - } - // next target - pTarget = findradius(pTarget, bot->s.origin, 200); - } - - if(fBestWeight) - { - bot->movetarget = pBest; - - if(debug_mode && bot->goalentity != bot->movetarget) - debug_printf("%s selected a %s for SR goal.\n",bot->client->pers.netname, bot->movetarget->classname); - - bot->goalentity = pBest; - } -} - -//---------------------------------------------- -// BOTAI_PickLongRangeGoal -// -// Evaluate the best long range goal of the required type -// GT_POSITION, -// GT_ENEMY, -// GT_ITEM -// Narrow this down later to give many more search options like -// GT_WEAPON -// GT_AMMO, etc. -// -//---------------------------------------------- -void BOTAI_PickLongRangeGoal(edict_t *bot, int iType) -{ - - int i = 0; - int node = 0; - float fWeight = 0.f,fBestWeight = 0.0; - int current_node = 0, goal_node = 0; - edict_t *goal_ent = NULL; - float fCost = 0.f; - - // look for a target - current_node = ACEND_FindClosestReachableNode(bot,NODE_DENSITY,NODE_ALL); - - bot->current_node = current_node; - - // Even in teamplay, we wander if no valid node - if(current_node == -1) - { - bot->nextState = BS_ROAM; - bot->wander_timeout = level.framenum + 1.0 * HZ; - bot->goal_node = -1; - return; - } - - //====================== - // Looking for a POSITION to move to - //====================== - if( iType == GT_POSITION ) - { - int counter = 0; - fCost = INVALID; - bot->goal_node = INVALID; - - // Pick a random node to go to - while( fCost == INVALID && counter < 10) // Don't look for too many - { - counter++; - i = (int)(random() * numnodes -1); // Any of the current nodes will do - fCost = ACEND_FindCost(current_node, i); - - if(fCost == INVALID || fCost < 2) // ignore invalid and very short hops - { - fCost = INVALID; - i = INVALID; - continue; - } - } - // We have a target node - just go there! - if( i != INVALID ) - { - bot->tries = 0; // Reset the count of how many times we tried this goal - ACEND_SetGoal(bot,i); - bot->wander_timeout = level.framenum + 1.0 * HZ; - return; - } - } - - //========================= - // Items - //========================= - if( iType == GT_ITEM ) - { - for(i=0;isolid == SOLID_NOT) // ignore items that are not there. - continue; - - fCost = ACEND_FindCost(current_node,item_table[i].node); - - if(fCost == INVALID || fCost < 2) // ignore invalid and very short hops - continue; - - fWeight = ACEIT_ItemNeed( bot, item_table[i].ent ); - - if( fWeight <= 0 ) // Ignore items we can't pick up. - continue; - - fWeight *= ( (rand()%5) +1 ); // Allow random variations - // weight /= cost; // Check against cost of getting there - - if(fWeight > fBestWeight && item_table[i].node != INVALID) - { - fBestWeight = fWeight; - goal_node = item_table[i].node; - goal_ent = item_table[i].ent; - } - } - } - - //======================== - // Enemies - //======================== - if( iType == GT_ENEMY ) - { - for(i=0;isolid == SOLID_NOT) ) - continue; - - // If it's dark and he's not already our enemy, ignore him - if( bot->enemy && players[i] != bot->enemy) - { - -//Disabled by Werewolf -// if( players[i]->light_level < 30) -// continue; - } - - node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY,NODE_ALL); - // RiEvEr - bug fixing - if( node == INVALID) - fCost = INVALID; - else - fCost = ACEND_FindCost(current_node, node); - if(fCost == INVALID || fCost < 3) // ignore invalid and very short hops - continue; - - // TeamMates are not enemies! - if( teamplay->value ) - { - // If not an enemy, don't choose him - if( OnSameTeam( bot, players[i])) - fWeight = 0.0; - else - fWeight = 100.0; //Werewolf: was 0.3 - } - else - fWeight = 0.8; //Werewolf: was 0.3 - - fWeight *= ( (rand()%5) +1 ); // Allow random variations - // weight /= cost; // Check against cost of getting there - - if(fWeight > fBestWeight && node != INVALID) - { - fBestWeight = fWeight; - goal_node = node; - goal_ent = players[i]; - } - } - } // End GT_ENEMY - - // If do not find a goal, go wandering.... - if(fBestWeight == 0.0 || goal_node == INVALID ) - { - bot->goal_node = INVALID; - bot->wander_timeout = level.framenum + 1.0 * HZ; - if(debug_mode) - debug_printf("%s did not find a LR goal, wandering.\n",bot->client->pers.netname); - return; // no path? - } - - // Reset the count of how many times we tried this goal - bot->tries = 0; - - if(goal_ent != NULL && debug_mode) - debug_printf("%s selected a %s at node %d for LR goal.\n",bot->client->pers.netname, goal_ent->classname, goal_node); - - BOTAI_SetGoal(bot,goal_node); - -} - -//--------------------------------------------- -// BOTAI_VisibleEnemy -// -// Used in roaming state to target only visible enemies -//--------------------------------------------- - -qboolean BOTAI_VisibleEnemy( edict_t *bot ) -{ - vec3_t v; - float fRange, fBestDist = 99999.0; - edict_t *goal_ent = NULL; - int i; - - for(i=0;isolid == SOLID_NOT) ) - continue; - - // TeamMates are not enemies! - if( teamplay->value ) - { - // If not an enemy, don't choose him - if( OnSameTeam( bot, players[i])) - continue; - } - - // Can't see it - can't shoot it! - if( !BOTCOL_Visible( bot, players[i] )) - continue; - - // Get distance to enemy - VectorSubtract (bot->s.origin, players[i]->s.origin, v); - fRange = VectorLength(v); - - // If it's dark and he's not already our enemy, ignore him - if( bot->enemy && players[i] != bot->enemy) - { - if( players[i]->light_level < 30) - continue; - } - - if(fRange < fBestDist) - { - fBestDist = fRange; - goal_ent = players[i]; - } - } - // Did we find a visible enemy? - if( goal_ent != NULL ) - { - bot->enemy = goal_ent; - return (true); - } - return (false); -} - -//--------------------------------------------- -// BOTAI_NeedToBandage -//--------------------------------------------- - -qboolean BOTAI_NeedToBandage(edict_t *bot) -{ - if ( - (bot->client->bandaging == 1) - || (bot->client->bleeding == 0 && bot->client->leg_damage == 0) - ) - return false; - else - return true; -} - -//--------------------------------------------- -// BOTAI_Think -// -// The standard entry point for the new bots -//--------------------------------------------- - -void BOTAI_Think(edict_t *bot) -{ - usercmd_t cmd; - vec3_t angles = {0,0,0}; -// vec3_t vTempDest, vTvec; // used by random roaming -// float fRoam; // -1 if no direction to roam, 0 if too soon to check again - int bs, new_bs; -// trace_t tTrace; - float rnd;//, fLen; -// gitem_t *new_weap; - - bot->bot_speed = 0; // initialise to nil to stop all unplanned movement - VectorCopy (bot->client->v_angle, angles); - VectorCopy(bot->client->ps.viewangles,bot->s.angles); - VectorSet (bot->client->ps.pmove.delta_angles, 0, 0, 0); - memset (&cmd, 0, sizeof(usercmd_t)); - - // Stop trying to think if the bot can't respawn. - if( (!esp->value) || (! IS_ALIVE(bot) && ((gameSettings & GS_ROUNDBASED) || (bot->client->respawn_framenum > level.framenum))) ) - goto LeaveThink; - - bs = bot->botState; - new_bs = bot->nextState; - - if (bs != new_bs) - { -/* if (new_bs == BS_DEAD) - gi.bprintf (PRINT_HIGH, "botState: BS_DEAD\n"); - if (new_bs == BS_WAIT) - gi.bprintf (PRINT_HIGH, "botState: BS_WAIT\n"); - if (new_bs == BS_PASSIVE) - gi.bprintf (PRINT_HIGH, "botState: BS_PASSIVE\n"); - if (new_bs == BS_ACTIVE) - gi.bprintf (PRINT_HIGH, "botState: BS_ACTIVE\n"); - if (new_bs == BS_SECURE) - gi.bprintf (PRINT_HIGH, "botState: BS_SECURE\n"); - if (new_bs == BS_RETREAT) - gi.bprintf (PRINT_HIGH, "botState: BS_RETREAT\n"); - if (new_bs == BS_HOLD) - gi.bprintf (PRINT_HIGH, "botState: BS_HOLD\n"); - if (new_bs == BS_SUPPORT) - gi.bprintf (PRINT_HIGH, "botState: BS_SUPPORT\n"); -*/ - bs = new_bs; - bot->botState = bot->nextState; - } - - if ((!bot->enemy) && (bs == BS_WAIT)) - { - bot->nextState = BS_ROAM; - bot->secondaryState = BSS_NONE; - } - if (bot->enemy) - { - if ((bot->enemy->deadflag == DEAD_DEAD) - && (bot->deadflag == DEAD_NO)) // to stop a crouch anim death bug - { - if (!bot->killchat) - { - //Taunt animation added - // can't wave when ducked or in the air :) - if( (!(bot->client->ps.pmove.pm_flags & PMF_DUCKED)) - && (bot->groundentity) ) - //FIXME: Add checks for in range and facing enemy - { - bot->bot_speed = 0; // stop moving! - //Turn to face dead enemy -// Bot_AimAt( bot, bot->lastSeen, angles); - bot->client->anim_priority = ANIM_WAVE; - rnd = random() * 4; - if (rnd < 1) - { - //Taunt them - bot->s.frame = FRAME_taunt01-1; - bot->client->anim_end = FRAME_taunt17; -// gi.bprintf (PRINT_HIGH, "bot_anim: Taunt\n"); - } - else if (rnd < 2) - { - //Sarcastic salute time - bot->s.frame = FRAME_salute01-1; - bot->client->anim_end = FRAME_salute11; -// gi.bprintf (PRINT_HIGH, "bot_anim: Salute\n"); - } - else if (rnd < 3) - { - //Point at them - bot->s.frame = FRAME_point01-1; - bot->client->anim_end = FRAME_point12; -// gi.bprintf (PRINT_HIGH, "bot_anim: Point\n"); - } - else - { - // Give them the finger - bot->s.frame = FRAME_flip01-1; - bot->client->anim_end = FRAME_flip12; -// gi.bprintf (PRINT_HIGH, "bot_anim: FlipOff\n"); - } - } - // Laugh at them! -// Bot_Chat (bot, bot->enemy, DBC_INSULT); - bot->killchat = true; - } - bot->secondaryState = BSS_NONE; - bot->enemy = NULL; - bot->movetarget = NULL; - bot->goal_node = bot->next_node = INVALID; - //Allow time to taunt and move - goto LeaveThink; - } - else - { - bot->killchat = false; - } - } - - // If I'm alive, but bot_state is dead, fix it up (when we respawn) - if ((bot->deadflag == DEAD_NO) && (bs == BS_DEAD)) - { - bot->nextState = BS_WAIT; - } - - // If I'm dead, hit fire to respawn - if (bot->deadflag == DEAD_DEAD) - { - bot->nextState = BS_DEAD; - - // Clear enemy status - bot->enemy = NULL; - bot->cansee = false; - - // Clear routing status - bot->current_node = INVALID; - bot->next_node = INVALID; - bot->goal_node = INVALID; - bot->last_node = INVALID; - - // Stop "key flooding" - if (level.framenum > bot->client->respawn_framenum) - { - bot->client->buttons = 0; // clear buttons - cmd.buttons = BUTTON_ATTACK; // hit the attack button - } - } - - // Now the state changing part - switch( bot->botState ) - { - // State when no path exists - case BS_ROAM: - BOTST_Roaming( bot, angles, &cmd); - break; - // Normal 'Active' state - case BS_ACTIVE: - BOTST_Active(bot, angles, &cmd); - break; - // 'Wait' respawn state - case BS_WAIT: - // 'Dead' state - case BS_DEAD: - // 'Passive' non-combat state - case BS_PASSIVE: - // 'Secure the area'or 'Guard' state - case BS_SECURE: - // 'Retreat/Take Cover' state - case BS_RETREAT: - // Holding or Sniping state - case BS_HOLD: - // Following or supporting state - case BS_SUPPORT: - break; - } - -LeaveThink: - - // Set movement speed - cmd.forwardmove = bot->bot_speed; - // set approximate ping - cmd.msec = 1000 / BOT_FPS; - // show random ping values in scoreboard - bot->client->ping = cmd.msec; - - // Set angles for move - cmd.angles[PITCH] = ANGLE2SHORT(angles[PITCH]); - cmd.angles[YAW]= ANGLE2SHORT(angles[YAW]); - cmd.angles[ROLL]= ANGLE2SHORT(angles[ROLL]); - // Save our current position - VectorCopy (bot->s.origin,bot->lastPosition); - // Does it need this to know where we're going? - bot->last_node = ACEND_FindClosestReachableNode( bot, NODE_DENSITY, NODE_ALL ); - - ClientThink (bot, &cmd); - - bot->nextthink = level.framenum + (game.framerate / BOT_FPS); -} - -/* - if (bot->deadflag == DEAD_NO) - { - // Show the path (uncomment for debugging) -// show_path_from = bot->client->bot_route_ai->last_node; -// show_path_to = bot->bot_ai.targetnode; -// DrawPath( bot ); - - if( (level.time >= bot->bot_ai.next_look_time)) - // Currently allow new enemy checks every frame - { - // Now resets movetarget and nodepath -// Bot_CheckEyes (bot, &bs, &new_bs, angles); - - if (!bot->enemy && !bot->movetarget ) - { - // Look for something else to collect - // sets BS_COLLECT, bot->bot_ai.targetnode & bot->movetarget -// Bot_FindItems(bot, &bs, &new_bs, angles); - } - } - - // Check for Taunt anims before roaming - if( (bs == BS_WAIT) || (bs == BS_COLLECT) || (bs == BS_HUNT) - || (bs == BS_FIND) - && !((bot->s.frame <= FRAME_point12) && (bot->s.frame >= FRAME_flip01)) ) - { - - if (bot->enemy) - { - bot->bot_ai.enemy_range = range (bot, bot->enemy); - Bot_ChooseWeapon (bot); - if (Bot_Visible (bot, bot->enemy)) - { - if( CarryingFlag(bot->enemy) && (bot->bot_ai.flagSayTime < level.time) ) - { - // Tell the team we saw their flag carrier - CTFSay_Team(bot, "Enemy flag carrier is %l\n"); - bot->bot_ai.flagSayTime = level.time + 7.0; - } - new_bs = BS_HUNT; - Bot_Attack (bot, &cmd, angles); - goto LeaveThink; - } - else if(!bot->movetarget) - { - new_bs = BS_FIND; - trace = gi.trace(bot->s.origin, VEC_ORIGIN, VEC_ORIGIN, - bot->bot_ai.last_seen, bot, MASK_PLAYERSOLID); - if (trace.fraction == 1.0) // direct route available - { - Bot_AimAt(bot, bot->bot_ai.last_seen, angles); - //drop through to roam code - } - // Don't chase if we have the flag - else if (!CarryingFlag(bot)) - { - // set our enemy up as an object we want to collect! - // only uses his last seen position - no cheating here! - bot->movetarget = bot->enemy; - bot->bot_ai.targetnode = ens_NodeProximity (bot->movetarget->s.origin); - //drop through to collect code - } - } - if (bot->bot_ai.last_seen_time < level.time - 5.0) - { - // We ain't seen him for quite a while - bot->enemy = bot->movetarget = NULL; - bot->bot_ai.targetnode = bot->bot_ai.nextnode = NOPATH; -// AntInitSearch( bot ); - new_bs = BS_WAIT; - // drop through to roam code - } - // Don't chase if we have the flag in CTF - else if (!CarryingFlag(bot)) - { - new_bs = BS_FIND; - // set our enemy up as an object we want to collect! - // only uses his last seen position - no cheating here! - bot->movetarget = bot->enemy; - bot->bot_ai.targetnode = ens_NodeProximity (bot->movetarget->s.origin); - //drop through to collect code - } - } - - -//========================================================== -// New BS_COLLECT code start -//========================================================== -//NodePaths: - if( (bot->movetarget) && (bot->bot_ai.targetnode != NOPATH) ) - { - //gi.bprintf(PRINT_HIGH,"BS_COLLECT: Target node is %i\n",(int)bot->bot_ai.targetnode); - if (AntPathMove(bot)) - { -// gi.bprintf(PRINT_HIGH,"BS_COLLECT: Routing to node %i from %i\n", -// (int)bot->bot_ai.nextnode, -// (int)bot->client->bot_route_ai->last_node); - - if( bot->bot_ai.nextnode != NOPATH) - { - if(bot->bot_ai.nextnode != bot->bot_ai.aimnode) - // not aiming for the same node again - { - // LADDER ==================== - else if( (node_array[bot->bot_ai.nextnode].method == LADDER) - && (bot->bot_ai.nextnode != bot->client->bot_route_ai->last_node) ) - { - Bot_AimAt(bot, node_array[bot->bot_ai.nextnode].position,angles); - bot->bot_ai.bot_speed = SPEED_RUN; - cmd.upmove = SPEED_WALK; - bot->bot_ai.aimnode = bot->bot_ai.nextnode; - // If no enemies, get out of here! - if( !bot->enemy ) - goto LeaveThink; - } - // End Ladder =============== - else - { - // move to nextnode - // currently just walk between them - no context information yet - Bot_AimAt(bot, node_array[bot->bot_ai.nextnode].position, angles); - bot->bot_ai.bot_speed = SPEED_WALK; - bot->bot_ai.aimnode = bot->bot_ai.nextnode; - // DEBUGGING!! Take this out later: - // goto LeaveThink; - // On next time through, roam code will take over - } - } - // else drop through to roam code - } - else if ( bot->movetarget->solid != SOLID_NOT) - { - //turn to collect it - Bot_Aim( bot, bot->movetarget, angles); - bot->bot_ai.bot_speed = SPEED_WALK; - // we're there so delete the path info - bot->bot_ai.targetnode = NOPATH; - bot->bot_ai.nextnode = NOPATH; - //DEBUG goto LeaveThink; - //variables are reset by collection in g_items.c - // continue into roam code - } - else if (bot->movetarget != bot->enemy) - { - // it's gone, forget it! - bot->movetarget = NULL; - bot->bot_ai.targetnode = NOPATH; - bot->bot_ai.nextnode = NOPATH; - new_bs = BS_WAIT; - // Continue into roam code - } - } - else if (bot->movetarget) - { - bot->bot_ai.targetnode = NOPATH; // no path, clear it - bot->bot_ai.nextnode = NOPATH; - if( Bot_Visible( bot, bot->movetarget) ) - { - Bot_Aim( bot, bot->movetarget, angles); - // FIXME: Check if we need to grapple to item - // let the roam code move us - } - else if (bot->movetarget != bot->enemy) - { - // can't see it, forget it! - bot->movetarget = NULL; - bot->bot_ai.nextnode = NOPATH; - new_bs = BS_WAIT; - // Continue into roam code - } - } - } - else if (bot->movetarget) - { - if( - (Bot_Visible( bot, bot->movetarget) ) - && (range(bot, bot->movetarget)<= RANGE_NEAR) - ) - { - if ( CanGrapple(bot, bot->movetarget->s.origin, angles)) - { - //gi.bprintf(PRINT_HIGH,"Found grapple route\n"); - // tempvector will be set - Bot_AimAt(bot,bot->bot_ai.tempvector, angles); - if (!(strcmp(bot->client->pers.weapon->classname, "weapon_grapple") == 0)) - { - // Change to grapple - //gi.bprintf(PRINT_HIGH,"Bot changing weapon to grapple...\n"); - new_weap = FindItem("Grapple"); - bot->client->newweapon = bot->client->pers.weapon = new_weap; - ChangeWeapon(bot); - goto LeaveThink; - } - else - { - // aim and shoot - Bot_AimAt(bot,bot->bot_ai.tempvector, angles); - cmd.buttons = BUTTON_ATTACK; - if( - !bot->enemy - && (bot->client->ctf_grapplestate < CTF_GRAPPLE_STATE_HANG) - ) - goto LeaveThink; - // If problems, goto leavethink after checking for - // grapplestate HANG - } - } - Bot_Aim( bot, bot->movetarget, angles); - // continue into roam code - } - else if (bot->movetarget != bot->enemy) - { - // can't see it, forget it! - bot->movetarget = NULL; - bot->bot_ai.targetnode = NOPATH; - bot->bot_ai.nextnode = NOPATH; - new_bs = BS_WAIT; - // Continue into roam code - } - } - -//===================================================== -// New Collect code end -//===================================================== -//---------------------------------------------------------------- -// NEW BS_WAIT CODE START -//---------------------------------------------------------------- -//RoamingAI: - //deal with completely stuck states first: - VectorSubtract(bot->bot_ai.last_pos, bot->s.origin, vTvec); - // Check if we have been able to move using length between points - // if we are stuck, deal with it. - len = VectorLength(vTvec); - if( (len <= 2.0) ) - { - // we are stuck! - // Stop grappling just in case - cmd.buttons = 0; - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Stuck!\n"); - roam = Bot_FindBestDirection (bot, vTempDest, angles); - if (roam > 0) - { - // Turn to face that direction - Bot_AimAt (bot, vTempDest, angles); - bot->bot_ai.crawl = false; - bot->bot_ai.last_jump = false; - bot->bot_ai.bot_speed = SPEED_ROAM; - cmd.upmove = SPEED_WALK; // jump to get out of trouble - //cmd->upmove = SPEED_WALK; // jump to get out of trouble - } - else if(roam == -1) // we are really stuck! - { - // TODO: climb, grapple or kill myself! - Cmd_Kill_f (bot); // suicide if stuck - } - else // Roam == 0 - { - // do nothing - just wait till next second :) - } - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - if( (bot->waterlevel < 2) - && (bot->client->old_waterlevel <2) - && bot->groundentity ) - { - if (bot->groundentity) - { - // Reset water timer - bot->bot_ai.watertimer = level.time; - } - if ( LadderForward (bot, angles) - && CanJumpUp (bot)) - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Ladder\n"); - bot->bot_ai.bot_speed = 0; - cmd.upmove = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - else if( CanJumpGap(bot, angles) ) // checks for safety - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: CanJumpGap\n"); - cmd.upmove = SPEED_WALK; - bot->bot_ai.bot_speed = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - else if( CanMoveSafely (bot, angles) - && CanMoveForward (bot, angles) ) - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Moving forward\n"); - bot->bot_ai.bot_speed = SPEED_WALK; - bot->bot_ai.last_jump = false; - if (CanStand (bot)) - { - //Stand up - bot->bot_ai.crawl = false; - } - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - else if( !CanMoveForward (bot, angles) - && CanCrawlForward (bot, angles) ) - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Crawling\n"); - cmd.upmove = -SPEED_WALK; - bot->client->ps.pmove.pm_flags |= PMF_DUCKED; - bot->bot_ai.bot_speed = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - // Possibly need a safety check here.. - else if( (CanMoveSafely(bot,angles)) - && (CanJumpForward (bot, angles)) - && !bot->bot_ai.last_jump)//&& (HasMoved (bot) ) - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Jumping\n"); - cmd.upmove = SPEED_WALK; - bot->bot_ai.last_jump = true; - bot->bot_ai.bot_speed = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - else if (Bot_CheckBump( bot, &cmd)) - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Checkbump\n"); - cmd.sidemove = bot->bot_ai.bot_strafe; - } - else - { - //gi.bprintf(PRINT_HIGH,"BS_WAIT: NewDirection\n"); - if (!CanMoveSafely (bot, angles)) // Lava, slime, etc. - { - roam = Bot_FindShortBestDirection( bot, vTempDest, angles); - } - else - { - roam = Bot_FindBestDirection (bot, vTempDest, angles); - } - if (roam > 0) - { - // Turn to face that direction - Bot_AimAt (bot, vTempDest, angles); - bot->bot_ai.crawl = false; - bot->bot_ai.last_jump = false; - bot->bot_ai.bot_speed = SPEED_ROAM; - } - else if(roam == -1) // we are really stuck! - { - // TODO: climb, grapple or kill myself! - } - else // Roam == 0 - { - // do nothing - just wait till next second :) - } - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - } - else if (bot->waterlevel) - { - if (bot->movetarget && bot->bot_ai.nextnode) - { - if( node_array[bot->bot_ai.nextnode].contents && IN_WATER) - { - //special handling for water nodes - if ( node_array[bot->bot_ai.nextnode].position[2] > bot->s.origin[2]) - { - cmd.upmove = SPEED_WALK; - } - bot->bot_ai.bot_speed = SPEED_WALK; - } - else - { - // getting out of the water! - cmd.upmove = SPEED_RUN; - bot->bot_ai.bot_speed = SPEED_RUN; - } - } - else if ( WaterMoveForward (bot, angles)) - { - bot->bot_ai.bot_speed = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - } -// else if (bot->waterlevel < 3) // at the surface - else if (CanLeaveWater(bot,angles)) // at the surface and edge?? - { - cmd.upmove = SPEED_RUN; - bot->bot_ai.bot_speed = SPEED_RUN; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - // Removed all this - trying for simplicity! - } - else //we hit a wall! - { - float temprand; - - temprand = random(); - if (temprand < 0.3) - cmd.upmove = SPEED_WALK; - else if (temprand < 0.6) - cmd.sidemove = SPEED_WALK; - else - bot->bot_ai.bot_speed = 0; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - if (bot->air_finished < (level.time +5)) // if drowning.. - { - cmd.upmove = SPEED_WALK; - bot->bot_ai.bot_speed = SPEED_WALK; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - goto LeaveThink; - } - else if ( !bot->groundentity) // We're in the air - { - //@@ Check for steps! - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Woohoo - we're flying!\n"); - cmd.upmove = SPEED_RUN; - bot->bot_ai.bot_speed = SPEED_RUN; - cmd.forwardmove = bot->bot_ai.bot_speed; - goto LeaveThink; - } - - -// NEW CODE END -//====================================================== - - } - else if ((bot->s.frame <= FRAME_point12) && (bot->s.frame >= FRAME_flip01)) - { - //Reset stuck time because we're taunting someone! - bot->bot_ai.last_jump = false; - } -LeaveThink: - if (VectorCompare (bot->bot_ai.last_hurt, vec3_origin) == 0) - { - if ((bs == BS_WAIT) || (bs == BS_COLLECT)) - { - // Turn to my hurt direction - // FIXME: Does this work?!? - Bot_AimAt (bot, bot->bot_ai.last_hurt, angles); - VectorClear (bot->bot_ai.last_hurt); - } - //RiEvEr - else if( ((bs == BS_HUNT) || (bs == BS_FIND)) && (bot->bot_ai.change_enemy) ) - { - // We've been shot by someone else - // and they are a better target or more of a threat - bot->bot_ai.change_enemy = false; // set in Bot_Pain - Bot_AimAt (bot, bot->bot_ai.last_hurt, angles); - VectorClear (bot->bot_ai.last_hurt); - // clear any previous information just in case! - bot->movetarget = NULL; - bot->bot_ai.targetnode = bot->bot_ai.nextnode = NOPATH; - } - //R - } - } - - - cmd.forwardmove = bot->bot_ai.bot_speed; - cmd.msec = 100; - // if (level.framenum%20 == (int)(random() * 19)) - // { - // cmd.msec = PING_AVG + (PING_STD_DEV * (random()-0.5) * 2); - // bot->client->ping = cmd.msec; -// } - - cmd.angles[PITCH] = ANGLE2SHORT(angles[PITCH]); - cmd.angles[YAW]= ANGLE2SHORT(angles[YAW]); - cmd.angles[ROLL]= ANGLE2SHORT(angles[ROLL]); - bot->client->bs = bs; - bot->client->new_bs = new_bs; - // Save our current position - VectorCopy (bot->s.origin,bot->bot_ai.last_pos); - // Does it need this to know where we're going? - bot->client->bot_route_ai->last_node = ens_NodeProximity (bot->s.origin); - - ClientThink (bot, &cmd); - - bot->nextthink = level.framenum + (game.framerate / BOT_FPS); -}*/ +//--------------------------------------------- +// BOT_AI.C +//--------------------------------------------- + +#include "../g_local.h" +#include "../m_player.h" + +/* + * $Log: /LTK2/src/acesrc/bot_ai.c $ + * + * 6 2/03/00 17:47 Riever + * Removed debug prints and tidied up code + * + * 5 2/03/00 14:09 Riever + * Added NeedToBandage() + * + * 4 21/02/00 23:43 Riever + * Added goal checking code and fleshed out AI state. Now selects visible + * enemies but doesn't search for them yet. + * + * 3 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * + * 2 20/02/00 20:27 Riever + * Added new members and definitions ready for 2nd generation of bots. + */ + +//---------------------------------------------- +// Set up the goal +//---------------------------------------------- +void BOTAI_SetGoal(edict_t *self, int goal_node) +{ + self->goal_node = goal_node; + self->next_node = self->current_node; // make sure we get to the nearest node first + self->node_timeout = 0; +} + +//---------------------------------------------- +// BOTAI_PickShortRangeGoal +// +// (Modified from ACE) +// Pick best goal based on importance and range. This function +// overrides the long range goal selection for items that +// are very close to the bot and are reachable. +//---------------------------------------------- + +void BOTAI_PickShortRangeGoal(edict_t *bot) +{ + edict_t *pTarget = NULL; + float fWeight = 0.f, fBestWeight = 0.0; + edict_t *pBest = NULL; + + // look for a target (should make more efficient later) + pTarget = findradius(NULL, bot->s.origin, 200); + + while(pTarget) + { + if(pTarget->classname == NULL) + return; + + // Missle avoidance code + // Set our movetarget to be the rocket or grenade fired at us. + if( + strcmp(pTarget->classname,"rocket")==0 || + strcmp(pTarget->classname,"grenade")==0 || + strcmp(pTarget->classname,"hgrenade")==0 + ) + { + if(debug_mode) + debug_printf("ROCKET ALERT!\n"); + + bot->movetarget = pTarget; + return; + } + + if (BOTCOL_CanReachItem(bot,pTarget->s.origin)) + { + if (infront(bot, pTarget)) + { + fWeight = ACEIT_ItemNeed( bot, pTarget ); + + if(fWeight > fBestWeight) + { + fBestWeight = fWeight; + pBest = pTarget; + } + } + } + // next target + pTarget = findradius(pTarget, bot->s.origin, 200); + } + + if(fBestWeight) + { + bot->movetarget = pBest; + + if(debug_mode && bot->goalentity != bot->movetarget) + debug_printf("%s selected a %s for SR goal.\n",bot->client->pers.netname, bot->movetarget->classname); + + bot->goalentity = pBest; + } +} + +//---------------------------------------------- +// BOTAI_PickLongRangeGoal +// +// Evaluate the best long range goal of the required type +// GT_POSITION, +// GT_ENEMY, +// GT_ITEM +// Narrow this down later to give many more search options like +// GT_WEAPON +// GT_AMMO, etc. +// +//---------------------------------------------- +void BOTAI_PickLongRangeGoal(edict_t *bot, int iType) +{ + + int i = 0; + int node = 0; + float fWeight = 0.f,fBestWeight = 0.0; + int current_node = 0, goal_node = 0; + edict_t *goal_ent = NULL; + float fCost = 0.f; + + // look for a target + current_node = ACEND_FindClosestReachableNode(bot,NODE_DENSITY,NODE_ALL); + + bot->current_node = current_node; + + // Even in teamplay, we wander if no valid node + if(current_node == -1) + { + bot->nextState = BS_ROAM; + bot->wander_timeout = level.framenum + 1.0 * HZ; + bot->goal_node = -1; + return; + } + + //====================== + // Looking for a POSITION to move to + //====================== + if( iType == GT_POSITION ) + { + int counter = 0; + fCost = INVALID; + bot->goal_node = INVALID; + + // Pick a random node to go to + while( fCost == INVALID && counter < 10) // Don't look for too many + { + counter++; + i = (int)(random() * numnodes -1); // Any of the current nodes will do + fCost = ACEND_FindCost(current_node, i); + + if(fCost == INVALID || fCost < 2) // ignore invalid and very short hops + { + fCost = INVALID; + i = INVALID; + continue; + } + } + // We have a target node - just go there! + if( i != INVALID ) + { + bot->tries = 0; // Reset the count of how many times we tried this goal + ACEND_SetGoal(bot,i); + bot->wander_timeout = level.framenum + 1.0 * HZ; + return; + } + } + + //========================= + // Items + //========================= + if( iType == GT_ITEM ) + { + for(i=0;isolid == SOLID_NOT) // ignore items that are not there. + continue; + + fCost = ACEND_FindCost(current_node,item_table[i].node); + + if(fCost == INVALID || fCost < 2) // ignore invalid and very short hops + continue; + + fWeight = ACEIT_ItemNeed( bot, item_table[i].ent ); + + if( fWeight <= 0 ) // Ignore items we can't pick up. + continue; + + fWeight *= ( (rand()%5) +1 ); // Allow random variations + // weight /= cost; // Check against cost of getting there + + if(fWeight > fBestWeight && item_table[i].node != INVALID) + { + fBestWeight = fWeight; + goal_node = item_table[i].node; + goal_ent = item_table[i].ent; + } + } + } + + //======================== + // Enemies + //======================== + if( iType == GT_ENEMY ) + { + for(i=0;isolid == SOLID_NOT) ) + continue; + + // If it's dark and he's not already our enemy, ignore him + if( bot->enemy && players[i] != bot->enemy) + { + +//Disabled by Werewolf +// if( players[i]->light_level < 30) +// continue; + } + + node = ACEND_FindClosestReachableNode(players[i],NODE_DENSITY,NODE_ALL); + // RiEvEr - bug fixing + if( node == INVALID) + fCost = INVALID; + else + fCost = ACEND_FindCost(current_node, node); + if(fCost == INVALID || fCost < 3) // ignore invalid and very short hops + continue; + + // TeamMates are not enemies! + if( teamplay->value ) + { + // If not an enemy, don't choose him + if( OnSameTeam( bot, players[i])) + fWeight = 0.0; + else + fWeight = 100.0; //Werewolf: was 0.3 + } + else + fWeight = 0.8; //Werewolf: was 0.3 + + fWeight *= ( (rand()%5) +1 ); // Allow random variations + // weight /= cost; // Check against cost of getting there + + if(fWeight > fBestWeight && node != INVALID) + { + fBestWeight = fWeight; + goal_node = node; + goal_ent = players[i]; + } + } + } // End GT_ENEMY + + // If do not find a goal, go wandering.... + if(fBestWeight == 0.0 || goal_node == INVALID ) + { + bot->goal_node = INVALID; + bot->wander_timeout = level.framenum + 1.0 * HZ; + if(debug_mode) + debug_printf("%s did not find a LR goal, wandering.\n",bot->client->pers.netname); + return; // no path? + } + + // Reset the count of how many times we tried this goal + bot->tries = 0; + + if(goal_ent != NULL && debug_mode) + debug_printf("%s selected a %s at node %d for LR goal.\n",bot->client->pers.netname, goal_ent->classname, goal_node); + + BOTAI_SetGoal(bot,goal_node); + +} + +//--------------------------------------------- +// BOTAI_VisibleEnemy +// +// Used in roaming state to target only visible enemies +//--------------------------------------------- + +qboolean BOTAI_VisibleEnemy( edict_t *bot ) +{ + vec3_t v; + float fRange, fBestDist = 99999.0; + edict_t *goal_ent = NULL; + int i; + + for(i=0;isolid == SOLID_NOT) ) + continue; + + // TeamMates are not enemies! + if( teamplay->value ) + { + // If not an enemy, don't choose him + if( OnSameTeam( bot, players[i])) + continue; + } + + // Can't see it - can't shoot it! + if( !BOTCOL_Visible( bot, players[i] )) + continue; + + // Get distance to enemy + VectorSubtract (bot->s.origin, players[i]->s.origin, v); + fRange = VectorLength(v); + + // If it's dark and he's not already our enemy, ignore him + if( bot->enemy && players[i] != bot->enemy) + { + if( players[i]->light_level < 30) + continue; + } + + if(fRange < fBestDist) + { + fBestDist = fRange; + goal_ent = players[i]; + } + } + // Did we find a visible enemy? + if( goal_ent != NULL ) + { + bot->enemy = goal_ent; + return (true); + } + return (false); +} + +//--------------------------------------------- +// BOTAI_NeedToBandage +//--------------------------------------------- + +qboolean BOTAI_NeedToBandage(edict_t *bot) +{ + if ( + (bot->client->bandaging == 1) + || (bot->client->bleeding == 0 && bot->client->leg_damage == 0) + ) + return false; + else + return true; +} + +//--------------------------------------------- +// BOTAI_Think +// +// The standard entry point for the new bots +//--------------------------------------------- + +void BOTAI_Think(edict_t *bot) +{ + usercmd_t cmd; + vec3_t angles = {0,0,0}; +// vec3_t vTempDest, vTvec; // used by random roaming +// float fRoam; // -1 if no direction to roam, 0 if too soon to check again + int bs, new_bs; +// trace_t tTrace; + float rnd;//, fLen; +// gitem_t *new_weap; + + bot->bot_speed = 0; // initialise to nil to stop all unplanned movement + VectorCopy (bot->client->v_angle, angles); + VectorCopy(bot->client->ps.viewangles,bot->s.angles); + VectorSet (bot->client->ps.pmove.delta_angles, 0, 0, 0); + memset (&cmd, 0, sizeof(usercmd_t)); + + // Stop trying to think if the bot can't respawn. + if( ! IS_ALIVE(bot) && ((gameSettings & GS_ROUNDBASED) || (bot->client->respawn_framenum > level.framenum)) ) + goto LeaveThink; + + bs = bot->botState; + new_bs = bot->nextState; + + if (bs != new_bs) + { +/* if (new_bs == BS_DEAD) + gi.bprintf (PRINT_HIGH, "botState: BS_DEAD\n"); + if (new_bs == BS_WAIT) + gi.bprintf (PRINT_HIGH, "botState: BS_WAIT\n"); + if (new_bs == BS_PASSIVE) + gi.bprintf (PRINT_HIGH, "botState: BS_PASSIVE\n"); + if (new_bs == BS_ACTIVE) + gi.bprintf (PRINT_HIGH, "botState: BS_ACTIVE\n"); + if (new_bs == BS_SECURE) + gi.bprintf (PRINT_HIGH, "botState: BS_SECURE\n"); + if (new_bs == BS_RETREAT) + gi.bprintf (PRINT_HIGH, "botState: BS_RETREAT\n"); + if (new_bs == BS_HOLD) + gi.bprintf (PRINT_HIGH, "botState: BS_HOLD\n"); + if (new_bs == BS_SUPPORT) + gi.bprintf (PRINT_HIGH, "botState: BS_SUPPORT\n"); +*/ + bs = new_bs; + bot->botState = bot->nextState; + } + + if ((!bot->enemy) && (bs == BS_WAIT)) + { + bot->nextState = BS_ROAM; + bot->secondaryState = BSS_NONE; + } + if (bot->enemy) + { + if ((bot->enemy->deadflag == DEAD_DEAD) + && (bot->deadflag == DEAD_NO)) // to stop a crouch anim death bug + { + if (!bot->killchat) + { + //Taunt animation added + // can't wave when ducked or in the air :) + if( (!(bot->client->ps.pmove.pm_flags & PMF_DUCKED)) + && (bot->groundentity) ) + //FIXME: Add checks for in range and facing enemy + { + bot->bot_speed = 0; // stop moving! + //Turn to face dead enemy +// Bot_AimAt( bot, bot->lastSeen, angles); + bot->client->anim_priority = ANIM_WAVE; + rnd = random() * 4; + if (rnd < 1) + { + //Taunt them + bot->s.frame = FRAME_taunt01-1; + bot->client->anim_end = FRAME_taunt17; +// gi.bprintf (PRINT_HIGH, "bot_anim: Taunt\n"); + } + else if (rnd < 2) + { + //Sarcastic salute time + bot->s.frame = FRAME_salute01-1; + bot->client->anim_end = FRAME_salute11; +// gi.bprintf (PRINT_HIGH, "bot_anim: Salute\n"); + } + else if (rnd < 3) + { + //Point at them + bot->s.frame = FRAME_point01-1; + bot->client->anim_end = FRAME_point12; +// gi.bprintf (PRINT_HIGH, "bot_anim: Point\n"); + } + else + { + // Give them the finger + bot->s.frame = FRAME_flip01-1; + bot->client->anim_end = FRAME_flip12; +// gi.bprintf (PRINT_HIGH, "bot_anim: FlipOff\n"); + } + } + // Laugh at them! +// Bot_Chat (bot, bot->enemy, DBC_INSULT); + bot->killchat = true; + } + bot->secondaryState = BSS_NONE; + bot->enemy = NULL; + bot->movetarget = NULL; + bot->goal_node = bot->next_node = INVALID; + //Allow time to taunt and move + goto LeaveThink; + } + else + { + bot->killchat = false; + } + } + + // If I'm alive, but bot_state is dead, fix it up (when we respawn) + if ((bot->deadflag == DEAD_NO) && (bs == BS_DEAD)) + { + bot->nextState = BS_WAIT; + } + + // If I'm dead, hit fire to respawn + if (bot->deadflag == DEAD_DEAD) + { + bot->nextState = BS_DEAD; + + // Clear enemy status + bot->enemy = NULL; + bot->cansee = false; + + // Clear routing status + bot->current_node = INVALID; + bot->next_node = INVALID; + bot->goal_node = INVALID; + bot->last_node = INVALID; + + // Stop "key flooding" + if (level.framenum > bot->client->respawn_framenum) + { + bot->client->buttons = 0; // clear buttons + cmd.buttons = BUTTON_ATTACK; // hit the attack button + } + } + + // Now the state changing part + switch( bot->botState ) + { + // State when no path exists + case BS_ROAM: + BOTST_Roaming( bot, angles, &cmd); + break; + // Normal 'Active' state + case BS_ACTIVE: + BOTST_Active(bot, angles, &cmd); + break; + // 'Wait' respawn state + case BS_WAIT: + // 'Dead' state + case BS_DEAD: + // 'Passive' non-combat state + case BS_PASSIVE: + // 'Secure the area'or 'Guard' state + case BS_SECURE: + // 'Retreat/Take Cover' state + case BS_RETREAT: + // Holding or Sniping state + case BS_HOLD: + // Following or supporting state + case BS_SUPPORT: + break; + } + +LeaveThink: + + // Set movement speed + cmd.forwardmove = bot->bot_speed; + // set approximate ping + cmd.msec = 1000 / BOT_FPS; + // show random ping values in scoreboard + bot->client->ping = cmd.msec; + + // Set angles for move + cmd.angles[PITCH] = ANGLE2SHORT(angles[PITCH]); + cmd.angles[YAW]= ANGLE2SHORT(angles[YAW]); + cmd.angles[ROLL]= ANGLE2SHORT(angles[ROLL]); + // Save our current position + VectorCopy (bot->s.origin,bot->lastPosition); + // Does it need this to know where we're going? + bot->last_node = ACEND_FindClosestReachableNode( bot, NODE_DENSITY, NODE_ALL ); + + ClientThink (bot, &cmd); + + bot->nextthink = level.framenum + (game.framerate / BOT_FPS); +} + +/* + if (bot->deadflag == DEAD_NO) + { + // Show the path (uncomment for debugging) +// show_path_from = bot->client->bot_route_ai->last_node; +// show_path_to = bot->bot_ai.targetnode; +// DrawPath( bot ); + + if( (level.time >= bot->bot_ai.next_look_time)) + // Currently allow new enemy checks every frame + { + // Now resets movetarget and nodepath +// Bot_CheckEyes (bot, &bs, &new_bs, angles); + + if (!bot->enemy && !bot->movetarget ) + { + // Look for something else to collect + // sets BS_COLLECT, bot->bot_ai.targetnode & bot->movetarget +// Bot_FindItems(bot, &bs, &new_bs, angles); + } + } + + // Check for Taunt anims before roaming + if( (bs == BS_WAIT) || (bs == BS_COLLECT) || (bs == BS_HUNT) + || (bs == BS_FIND) + && !((bot->s.frame <= FRAME_point12) && (bot->s.frame >= FRAME_flip01)) ) + { + + if (bot->enemy) + { + bot->bot_ai.enemy_range = range (bot, bot->enemy); + Bot_ChooseWeapon (bot); + if (Bot_Visible (bot, bot->enemy)) + { + if( CarryingFlag(bot->enemy) && (bot->bot_ai.flagSayTime < level.time) ) + { + // Tell the team we saw their flag carrier + CTFSay_Team(bot, "Enemy flag carrier is %l\n"); + bot->bot_ai.flagSayTime = level.time + 7.0; + } + new_bs = BS_HUNT; + Bot_Attack (bot, &cmd, angles); + goto LeaveThink; + } + else if(!bot->movetarget) + { + new_bs = BS_FIND; + trace = gi.trace(bot->s.origin, VEC_ORIGIN, VEC_ORIGIN, + bot->bot_ai.last_seen, bot, MASK_PLAYERSOLID); + if (trace.fraction == 1.0) // direct route available + { + Bot_AimAt(bot, bot->bot_ai.last_seen, angles); + //drop through to roam code + } + // Don't chase if we have the flag + else if (!CarryingFlag(bot)) + { + // set our enemy up as an object we want to collect! + // only uses his last seen position - no cheating here! + bot->movetarget = bot->enemy; + bot->bot_ai.targetnode = ens_NodeProximity (bot->movetarget->s.origin); + //drop through to collect code + } + } + if (bot->bot_ai.last_seen_time < level.time - 5.0) + { + // We ain't seen him for quite a while + bot->enemy = bot->movetarget = NULL; + bot->bot_ai.targetnode = bot->bot_ai.nextnode = NOPATH; +// AntInitSearch( bot ); + new_bs = BS_WAIT; + // drop through to roam code + } + // Don't chase if we have the flag in CTF + else if (!CarryingFlag(bot)) + { + new_bs = BS_FIND; + // set our enemy up as an object we want to collect! + // only uses his last seen position - no cheating here! + bot->movetarget = bot->enemy; + bot->bot_ai.targetnode = ens_NodeProximity (bot->movetarget->s.origin); + //drop through to collect code + } + } + + +//========================================================== +// New BS_COLLECT code start +//========================================================== +//NodePaths: + if( (bot->movetarget) && (bot->bot_ai.targetnode != NOPATH) ) + { + //gi.bprintf(PRINT_HIGH,"BS_COLLECT: Target node is %i\n",(int)bot->bot_ai.targetnode); + if (AntPathMove(bot)) + { +// gi.bprintf(PRINT_HIGH,"BS_COLLECT: Routing to node %i from %i\n", +// (int)bot->bot_ai.nextnode, +// (int)bot->client->bot_route_ai->last_node); + + if( bot->bot_ai.nextnode != NOPATH) + { + if(bot->bot_ai.nextnode != bot->bot_ai.aimnode) + // not aiming for the same node again + { + // LADDER ==================== + else if( (node_array[bot->bot_ai.nextnode].method == LADDER) + && (bot->bot_ai.nextnode != bot->client->bot_route_ai->last_node) ) + { + Bot_AimAt(bot, node_array[bot->bot_ai.nextnode].position,angles); + bot->bot_ai.bot_speed = SPEED_RUN; + cmd.upmove = SPEED_WALK; + bot->bot_ai.aimnode = bot->bot_ai.nextnode; + // If no enemies, get out of here! + if( !bot->enemy ) + goto LeaveThink; + } + // End Ladder =============== + else + { + // move to nextnode + // currently just walk between them - no context information yet + Bot_AimAt(bot, node_array[bot->bot_ai.nextnode].position, angles); + bot->bot_ai.bot_speed = SPEED_WALK; + bot->bot_ai.aimnode = bot->bot_ai.nextnode; + // DEBUGGING!! Take this out later: + // goto LeaveThink; + // On next time through, roam code will take over + } + } + // else drop through to roam code + } + else if ( bot->movetarget->solid != SOLID_NOT) + { + //turn to collect it + Bot_Aim( bot, bot->movetarget, angles); + bot->bot_ai.bot_speed = SPEED_WALK; + // we're there so delete the path info + bot->bot_ai.targetnode = NOPATH; + bot->bot_ai.nextnode = NOPATH; + //DEBUG goto LeaveThink; + //variables are reset by collection in g_items.c + // continue into roam code + } + else if (bot->movetarget != bot->enemy) + { + // it's gone, forget it! + bot->movetarget = NULL; + bot->bot_ai.targetnode = NOPATH; + bot->bot_ai.nextnode = NOPATH; + new_bs = BS_WAIT; + // Continue into roam code + } + } + else if (bot->movetarget) + { + bot->bot_ai.targetnode = NOPATH; // no path, clear it + bot->bot_ai.nextnode = NOPATH; + if( Bot_Visible( bot, bot->movetarget) ) + { + Bot_Aim( bot, bot->movetarget, angles); + // FIXME: Check if we need to grapple to item + // let the roam code move us + } + else if (bot->movetarget != bot->enemy) + { + // can't see it, forget it! + bot->movetarget = NULL; + bot->bot_ai.nextnode = NOPATH; + new_bs = BS_WAIT; + // Continue into roam code + } + } + } + else if (bot->movetarget) + { + if( + (Bot_Visible( bot, bot->movetarget) ) + && (range(bot, bot->movetarget)<= RANGE_NEAR) + ) + { + if ( CanGrapple(bot, bot->movetarget->s.origin, angles)) + { + //gi.bprintf(PRINT_HIGH,"Found grapple route\n"); + // tempvector will be set + Bot_AimAt(bot,bot->bot_ai.tempvector, angles); + if (!(strcmp(bot->client->pers.weapon->classname, "weapon_grapple") == 0)) + { + // Change to grapple + //gi.bprintf(PRINT_HIGH,"Bot changing weapon to grapple...\n"); + new_weap = FindItem("Grapple"); + bot->client->newweapon = bot->client->pers.weapon = new_weap; + ChangeWeapon(bot); + goto LeaveThink; + } + else + { + // aim and shoot + Bot_AimAt(bot,bot->bot_ai.tempvector, angles); + cmd.buttons = BUTTON_ATTACK; + if( + !bot->enemy + && (bot->client->ctf_grapplestate < CTF_GRAPPLE_STATE_HANG) + ) + goto LeaveThink; + // If problems, goto leavethink after checking for + // grapplestate HANG + } + } + Bot_Aim( bot, bot->movetarget, angles); + // continue into roam code + } + else if (bot->movetarget != bot->enemy) + { + // can't see it, forget it! + bot->movetarget = NULL; + bot->bot_ai.targetnode = NOPATH; + bot->bot_ai.nextnode = NOPATH; + new_bs = BS_WAIT; + // Continue into roam code + } + } + +//===================================================== +// New Collect code end +//===================================================== +//---------------------------------------------------------------- +// NEW BS_WAIT CODE START +//---------------------------------------------------------------- +//RoamingAI: + //deal with completely stuck states first: + VectorSubtract(bot->bot_ai.last_pos, bot->s.origin, vTvec); + // Check if we have been able to move using length between points + // if we are stuck, deal with it. + len = VectorLength(vTvec); + if( (len <= 2.0) ) + { + // we are stuck! + // Stop grappling just in case + cmd.buttons = 0; + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Stuck!\n"); + roam = Bot_FindBestDirection (bot, vTempDest, angles); + if (roam > 0) + { + // Turn to face that direction + Bot_AimAt (bot, vTempDest, angles); + bot->bot_ai.crawl = false; + bot->bot_ai.last_jump = false; + bot->bot_ai.bot_speed = SPEED_ROAM; + cmd.upmove = SPEED_WALK; // jump to get out of trouble + //cmd->upmove = SPEED_WALK; // jump to get out of trouble + } + else if(roam == -1) // we are really stuck! + { + // TODO: climb, grapple or kill myself! + Cmd_Kill_f (bot); // suicide if stuck + } + else // Roam == 0 + { + // do nothing - just wait till next second :) + } + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + if( (bot->waterlevel < 2) + && (bot->client->old_waterlevel <2) + && bot->groundentity ) + { + if (bot->groundentity) + { + // Reset water timer + bot->bot_ai.watertimer = level.time; + } + if ( LadderForward (bot, angles) + && CanJumpUp (bot)) + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Ladder\n"); + bot->bot_ai.bot_speed = 0; + cmd.upmove = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + else if( CanJumpGap(bot, angles) ) // checks for safety + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: CanJumpGap\n"); + cmd.upmove = SPEED_WALK; + bot->bot_ai.bot_speed = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + else if( CanMoveSafely (bot, angles) + && CanMoveForward (bot, angles) ) + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Moving forward\n"); + bot->bot_ai.bot_speed = SPEED_WALK; + bot->bot_ai.last_jump = false; + if (CanStand (bot)) + { + //Stand up + bot->bot_ai.crawl = false; + } + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + else if( !CanMoveForward (bot, angles) + && CanCrawlForward (bot, angles) ) + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Crawling\n"); + cmd.upmove = -SPEED_WALK; + bot->client->ps.pmove.pm_flags |= PMF_DUCKED; + bot->bot_ai.bot_speed = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + // Possibly need a safety check here.. + else if( (CanMoveSafely(bot,angles)) + && (CanJumpForward (bot, angles)) + && !bot->bot_ai.last_jump)//&& (HasMoved (bot) ) + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Jumping\n"); + cmd.upmove = SPEED_WALK; + bot->bot_ai.last_jump = true; + bot->bot_ai.bot_speed = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + else if (Bot_CheckBump( bot, &cmd)) + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Checkbump\n"); + cmd.sidemove = bot->bot_ai.bot_strafe; + } + else + { + //gi.bprintf(PRINT_HIGH,"BS_WAIT: NewDirection\n"); + if (!CanMoveSafely (bot, angles)) // Lava, slime, etc. + { + roam = Bot_FindShortBestDirection( bot, vTempDest, angles); + } + else + { + roam = Bot_FindBestDirection (bot, vTempDest, angles); + } + if (roam > 0) + { + // Turn to face that direction + Bot_AimAt (bot, vTempDest, angles); + bot->bot_ai.crawl = false; + bot->bot_ai.last_jump = false; + bot->bot_ai.bot_speed = SPEED_ROAM; + } + else if(roam == -1) // we are really stuck! + { + // TODO: climb, grapple or kill myself! + } + else // Roam == 0 + { + // do nothing - just wait till next second :) + } + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + } + else if (bot->waterlevel) + { + if (bot->movetarget && bot->bot_ai.nextnode) + { + if( node_array[bot->bot_ai.nextnode].contents && IN_WATER) + { + //special handling for water nodes + if ( node_array[bot->bot_ai.nextnode].position[2] > bot->s.origin[2]) + { + cmd.upmove = SPEED_WALK; + } + bot->bot_ai.bot_speed = SPEED_WALK; + } + else + { + // getting out of the water! + cmd.upmove = SPEED_RUN; + bot->bot_ai.bot_speed = SPEED_RUN; + } + } + else if ( WaterMoveForward (bot, angles)) + { + bot->bot_ai.bot_speed = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + } +// else if (bot->waterlevel < 3) // at the surface + else if (CanLeaveWater(bot,angles)) // at the surface and edge?? + { + cmd.upmove = SPEED_RUN; + bot->bot_ai.bot_speed = SPEED_RUN; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + // Removed all this - trying for simplicity! + } + else //we hit a wall! + { + float temprand; + + temprand = random(); + if (temprand < 0.3) + cmd.upmove = SPEED_WALK; + else if (temprand < 0.6) + cmd.sidemove = SPEED_WALK; + else + bot->bot_ai.bot_speed = 0; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + if (bot->air_finished < (level.time +5)) // if drowning.. + { + cmd.upmove = SPEED_WALK; + bot->bot_ai.bot_speed = SPEED_WALK; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + goto LeaveThink; + } + else if ( !bot->groundentity) // We're in the air + { + //@@ Check for steps! + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Woohoo - we're flying!\n"); + cmd.upmove = SPEED_RUN; + bot->bot_ai.bot_speed = SPEED_RUN; + cmd.forwardmove = bot->bot_ai.bot_speed; + goto LeaveThink; + } + + +// NEW CODE END +//====================================================== + + } + else if ((bot->s.frame <= FRAME_point12) && (bot->s.frame >= FRAME_flip01)) + { + //Reset stuck time because we're taunting someone! + bot->bot_ai.last_jump = false; + } +LeaveThink: + if (VectorCompare (bot->bot_ai.last_hurt, vec3_origin) == 0) + { + if ((bs == BS_WAIT) || (bs == BS_COLLECT)) + { + // Turn to my hurt direction + // FIXME: Does this work?!? + Bot_AimAt (bot, bot->bot_ai.last_hurt, angles); + VectorClear (bot->bot_ai.last_hurt); + } + //RiEvEr + else if( ((bs == BS_HUNT) || (bs == BS_FIND)) && (bot->bot_ai.change_enemy) ) + { + // We've been shot by someone else + // and they are a better target or more of a threat + bot->bot_ai.change_enemy = false; // set in Bot_Pain + Bot_AimAt (bot, bot->bot_ai.last_hurt, angles); + VectorClear (bot->bot_ai.last_hurt); + // clear any previous information just in case! + bot->movetarget = NULL; + bot->bot_ai.targetnode = bot->bot_ai.nextnode = NOPATH; + } + //R + } + } + + + cmd.forwardmove = bot->bot_ai.bot_speed; + cmd.msec = 100; + // if (level.framenum%20 == (int)(random() * 19)) + // { + // cmd.msec = PING_AVG + (PING_STD_DEV * (random()-0.5) * 2); + // bot->client->ping = cmd.msec; +// } + + cmd.angles[PITCH] = ANGLE2SHORT(angles[PITCH]); + cmd.angles[YAW]= ANGLE2SHORT(angles[YAW]); + cmd.angles[ROLL]= ANGLE2SHORT(angles[ROLL]); + bot->client->bs = bs; + bot->client->new_bs = new_bs; + // Save our current position + VectorCopy (bot->s.origin,bot->bot_ai.last_pos); + // Does it need this to know where we're going? + bot->client->bot_route_ai->last_node = ens_NodeProximity (bot->s.origin); + + ClientThink (bot, &cmd); + + bot->nextthink = level.framenum + (game.framerate / BOT_FPS); +}*/ diff --git a/src/action/acesrc/bot_collision.c b/src/action/acesrc/bot_collision.c index e201a22f8..67aa3ada9 100644 --- a/src/action/acesrc/bot_collision.c +++ b/src/action/acesrc/bot_collision.c @@ -1,580 +1,580 @@ -//--------------------------------------------- -// bot_collision.c -// -// Copyright (c) 2000 Connor Caple all rights reserved -// -// This file contains the bot collision tests to enable movement -//--------------------------------------------- -/* - * $Log: /LTK2/src/acesrc/bot_collision.c $ - * - * 4 2/03/00 13:55 Riever - * Added CheckShot and removed a debug TempLaser - * - * 3 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * - * 2 20/02/00 20:27 Riever - * Added new members and definitions ready for 2nd generation of bots. - */ - -#include "../g_local.h" -#include "../m_player.h" - -//---------------------------------------------- -// BOTCOL_CanJumpForward -//---------------------------------------------- - -qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, temp, dest; - trace_t trace; - - - VectorCopy (self->s.origin, temp); - temp[2]+=22; // create a point in line with bot's eyes - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - VectorMA(temp, TJUMP_DIST, dir, dest); - - trace = gi.trace(temp, tv(-16,-16,0), tv(16,16,56), dest, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - - if (trace.fraction == 1.0) - return (true); - return (false); -} - -//---------------------------------------------- -// BOTCOL_CanCrawl -// -// Checks a viewport in front of the entity and returns true -// if it can be crawled into -//---------------------------------------------- - -qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest; - trace_t trace; - - - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - VectorMA(self->s.origin, TCRAWL_DIST, dir, dest); - - // Trying to trace from FeetPos to CrawlHeightPos - trace = gi.trace(self->s.origin, tv(-16,-16,-22), tv(16,16,0), dest, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - // Complete trace possible - if (trace.fraction == 1.0) - return (true); - // Failed - return (false); -} - -//--------------------------------------------- -// BOTCOL_CanStand -// -// Returns true if it's possible to stand up. -//--------------------------------------------- -qboolean BOTCOL_CanStand(edict_t *self) -{ - static vec3_t maxs = {16, 16, 32}; - trace_t trace; - - trace = gi.trace(self->s.origin, self->mins, maxs, self->s.origin, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - else - return (true); -} - -//--------------------------------------------- -// BOTCOL_CanJumpUp -// -// Returns true if it's possible to jump up. -//--------------------------------------------- -qboolean BOTCOL_CanJumpUp(edict_t *self) -{ - static vec3_t maxs = {16, 16, 64}; - trace_t trace; - - trace = gi.trace(self->s.origin, self->mins, maxs, self->s.origin, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - else - return (true); -} - -//--------------------------------------------- -// BOTCOL_CanMoveForward -// -// Returns true if it's possible to move forward -//--------------------------------------------- - -qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, temp, dest; - trace_t trace; - - - VectorCopy (self->s.origin, temp); - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - VectorMA(temp, TMOVE_DIST, dir, dest); - // Debugging -// BOTUT_TempLaser (temp, dest); - - // Trying to trace from FeetPos to HeightPos - // Take a little off the bottom of the box from -22 to allow for slopes - trace = gi.trace(temp, tv(-16,-16,-8), tv(16,16,32), dest, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - - if (trace.fraction == 1.0) - return (true); - // Failed - return (false); -} - -//----------------------------------------------- -// BOTCOL_WaterMoveForward -// -// Returns true if botcan move forward in water -//----------------------------------------------- - -qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest; - trace_t trace; - - - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - VectorMA(self->s.origin, TWATER_DIST, dir, dest); - - // Trying to trace from FeetPos to HeightPos - // Take a little off the bottom of the box from -22 to allow for slopes - trace = gi.trace(self->s.origin, tv(-16,-16,-10), tv(16,16,32), dest, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - - if (trace.fraction == 1.0) - return (true); - // Failed - return (false); -} - -//======================================================= -// BOTCOL_CanLeaveWater -//======================================================= -// -qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, temp, dest; - trace_t trace; - - - VectorCopy (self->s.origin, temp); - temp[2]+=22; // create a point in line with bot's eyes - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - VectorMA(temp, TWEDGE_DIST, dir, dest); - - trace = gi.trace(temp, tv(-16,-16,0), tv(16,16,56), dest, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); - - if (trace.fraction == 1.0) - return (true); - - return (false); -} - - -//---------------------------------------------- -// BOTCOL_CanMoveSafely -// -// Checks for lava, slime and long drops -//---------------------------------------------- -//#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) - -qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest1, dest2; - trace_t trace; - //float this_dist; - - VectorClear(angle); - angle[1] = angles[1]; - - AngleVectors(angle, dir, NULL, NULL); - - // create a position in front of the bot - VectorMA(self->s.origin, TRACE_DIST_SHORT, dir, dest1); - - // Modified to check for crawl direction - //BOTUT_TempLaser (self->s.origin, dest1); - trace = gi.trace(self->s.origin, tv(-16,-16,0), tv(16,16,0), dest1, self, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (true); - - if (trace.fraction > 0) - { // check that destination is onground, or not above lava/slime - dest1[0] = trace.endpos[0]; - dest1[1] = trace.endpos[1]; - dest1[2] = trace.endpos[2] - 28; - //this_dist = trace.fraction * TRACE_DIST_SHORT; - - if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) - return (true); - // create a position a distance below it - VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN; - //BOTUT_TempLaser (trace.endpos, dest2); - trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); - - if( (trace.fraction == 1.0) // long drop! - || (trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA - { - return (false); - } - else - { - return (true); - } - } - //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); - return (false); -} - -//---------------------------------------------- -// BOTCOL_CanStrafeSafely -// -// Checks for lava, slime and long drops -//---------------------------------------------- -//#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) - -qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest1, dest2; - trace_t trace; - //float this_dist; - int sign; - - sign = (self->bot_strafe > 0) ? (-1) : (1); - VectorClear(angle); - angle[1] = anglemod(angles[1] + (sign * 90));//[forward][right][up] - - AngleVectors(angle, dir, NULL, NULL); - - // create a position to the side of the bot - VectorMA(self->s.origin, TRACE_DIST_STRAFE, dir, dest1); - - // Modified to check for crawl direction - //BOTUT_TempLaser (self->s.origin, dest1); - trace = gi.trace(self->s.origin, tv(-16,-16,-22), tv(16,16,0), dest1, self, MASK_SOLID); - - if (trace.fraction > 0) - { // check that destination is onground, or not above lava/slime - dest1[0] = trace.endpos[0]; - dest1[1] = trace.endpos[1]; - dest1[2] = trace.endpos[2] - 24; - //this_dist = trace.fraction * TRACE_DIST_STRAFE; - - if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) - return (true); - - // create a position a distance below it - VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN_STRAFE; - trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_SOLID | MASK_DEADLY); - - if( (trace.fraction == 1.0) // don't drop! - ||(trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA - { - return (false); - } - else - { - return (true); - } - } - //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); - return (false); -} - -//---------------------------------------------- -// BOTCOL_CanJumpGap -// -// -// Tests for a gap in the floor and if bot can cross it safely -//----------------------------------------------- -qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles) -{ - vec3_t dir, angle, dest1, dest2; - trace_t trace; - - VectorClear(angle); - angle[1] = angles[1]; - AngleVectors(angle, dir, NULL, NULL); - - // create a position in front of the bot - VectorMA(self->s.origin, TJUMP_DIST, dir, dest1); - - // Modified to check for crawl direction - //BOTUT_TempLaser (self->s.origin, dest1); - trace = gi.trace(self->s.origin, tv(-16,-16,-12), tv(16,16,32), dest1, self, MASK_SOLID); - - // Check if we are looking inside a wall! - if (trace.startsolid) - return (false); // we need a gap! - - if (trace.fraction !=1.0) - return (false); // we still need a gap! - else - { // check that destination is not onground - // create a position a distance below it - VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN_STRAFE; - //BOTUT_TempLaser (trace.endpos, dest2); - trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_SOLID);// | MASK_DEADLY); - - if(trace.fraction != 1.0) // There is no gap! - { - return (false); - } - else - { - //gi.bprintf(PRINT_HIGH,"CanJumpGap: Checking the gap distance\n"); - //Now look for a longer gap to jump over - VectorMA(self->s.origin, TRACE_DIST_JUMP, dir, dest1); - trace = gi.trace(self->s.origin, tv(-16,-16,-12), tv(16,16,32), dest1, self, MASK_SOLID); - if (trace.fraction < 0.5) - { - return (false); // we still need a longer gap! - } - dest1[0] = trace.endpos[0]; - dest1[1] = trace.endpos[1]; - dest1[2] = trace.endpos[2] - 28; - - if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) - // We have a safe landing area - return (true); - else // check if we get hurt by jumping here - { - VectorCopy( trace.endpos, dest2); - dest2[2] -= TRACE_DOWN; - trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, (MASK_SOLID | MASK_DEADLY)); - - if( (trace.fraction == 1.0) // don't drop too far! - ||(trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA - { - return (false); - } - else - { - // there is no obvious danger - return (true); - } - } - } - } - //gi.bprintf(PRINT_HIGH,"Default failure from JumpGap\n"); - return (false); -} - - -//--------------------------------------------- -// BOTCOL_CheckBump -// -// Helps steer the bot round protrusions, crate edges, etc., ... -//--------------------------------------------- - -qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd) -{ - vec3_t angles, forward, right, aim, leftarm, rightarm; - trace_t ltr, rtr; - - VectorCopy (bot->s.angles, angles); - angles[0] = 0; - AngleVectors (angles, forward, right, NULL); - - VectorScale (forward, 80, aim); - VectorAdd (bot->s.origin, aim, aim); - - VectorScale (right, 16, right); - VectorAdd (bot->s.origin, right, rightarm); - VectorInverse (right); - VectorAdd (bot->s.origin, right, leftarm); - - ltr = gi.trace (leftarm, vec3_origin, vec3_origin, aim, bot, MASK_SOLID); - rtr = gi.trace (rightarm, vec3_origin, vec3_origin, aim, bot, MASK_SOLID); - - if ((ltr.fraction < 1.00) && (rtr.fraction == 1.00)) - { - //gi.centerprintf (bot, "Strafe RIGHT, goddamit!\n"); - bot->bot_strafe = SPEED_WALK; - return true; - } - if ((ltr.fraction == 1.00) && (rtr.fraction < 1.00)) - { - //gi.centerprintf (bot, "Strafe LEFT, goddamit!\n"); - bot->bot_strafe = -SPEED_WALK; - return true; - } - if ((ltr.fraction < 1.00) && (rtr.fraction < 1.00)) - { - //gi.centerprintf (bot, "That's a WALL!\n"); - } - - // Debugging - // TempNode (aim, 0.2); - // BOTUT_TempLaser (leftarm, aim); - // BOTUT_TempLaser (rightarm, aim); - return false; -} - -//--------------------------------------------- -// BOTCOL_Visible -// -// returns 1 if any part of the entity is visible to bot -// (Yes - accurate bot vision is based on collision detection!) -//--------------------------------------------- - -qboolean BOTCOL_Visible (edict_t *bot, edict_t *other) -{ - vec3_t vEyes, vForward, actvec, offset3; - vec3_t origin, sawhim, vTemp; - vec_t offset; - trace_t tr1, tr2, tr3; // 1 = bottom right - // 2 = top left - // 3 = origin - - // We don't use the eye height since the test is for shooting. - VectorCopy (bot->s.origin, vEyes); - VectorCopy (other->s.origin, origin); - - if (other->client) - { - if (other->client->ps.pmove.pm_flags & PMF_DUCKED) - { - origin[2] -= 18; - } - } - - // May change this later to allow for glass / breakable etc.. - tr1 = gi.trace (vEyes, vec3_origin, vec3_origin, other->absmin, bot, MASK_PLAYERSOLID); - tr2 = gi.trace (vEyes, vec3_origin, vec3_origin, other->absmax, bot, MASK_PLAYERSOLID); - tr3 = gi.trace (vEyes, vec3_origin, vec3_origin, origin, bot, MASK_PLAYERSOLID); - - VectorCopy (vec3_origin, sawhim); - if (tr1.fraction == 1.0) - VectorCopy (other->absmin, sawhim); - if (tr2.fraction == 1.0) - VectorCopy (other->absmax, sawhim); - if (tr3.fraction == 1.0) - VectorCopy (origin, sawhim); - - if (VectorCompare(sawhim, vec3_origin) == 0) - { - AngleVectors (bot->s.angles, vForward, NULL, NULL); - VectorSubtract (sawhim, bot->s.origin, actvec); - VectorNormalize (actvec); - VectorSubtract (actvec, vForward, offset3); - offset = VectorLength (offset3); - - // Within Field Of View? - if (offset < ROOT2) - return true; - - // Have we already seen this enemy - if( other == bot->enemy ) - { - VectorSubtract(other->s.origin, bot->s.origin, vTemp); - // Is it too close for us to ignore? - if( VectorLength(vTemp) < 100.0) - return true; - } - } - return false; -} - -//---------------------------------------------- -// BOTCOL_CheckShot -// -// See if we can hit the target without hitting a teammate -//---------------------------------------------- - -qboolean BOTCOL_CheckShot(edict_t *bot) -{ - trace_t tTrace; - - tTrace = gi.trace (bot->s.origin, vec3_origin, vec3_origin, bot->enemy->s.origin, bot, MASK_SOLID|MASK_OPAQUE); - - // If we are in teamplay, check enemy team. - if(teamplay->value && tTrace.ent && tTrace.ent->client) - { - if( tTrace.ent->client->resp.team == bot->client->resp.team ) - return (false); - } - // Clear shot. - return true; -} - -//---------------------------------------------- -// BOTCOL_CanReachItem -// -// This _really_ needs more work! -//---------------------------------------------- - -qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal) -{ - trace_t tTrace; - vec3_t v; - - VectorCopy(bot->mins,v); - v[2] += 18; // Stepsize - - tTrace = gi.trace (bot->s.origin, v, bot->maxs, goal, bot, MASK_SOLID|MASK_OPAQUE); - - // Yes we can see it - if (tTrace.fraction == 1.0) - return true; - else - return false; -} +//--------------------------------------------- +// bot_collision.c +// +// Copyright (c) 2000 Connor Caple all rights reserved +// +// This file contains the bot collision tests to enable movement +//--------------------------------------------- +/* + * $Log: /LTK2/src/acesrc/bot_collision.c $ + * + * 4 2/03/00 13:55 Riever + * Added CheckShot and removed a debug TempLaser + * + * 3 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * + * 2 20/02/00 20:27 Riever + * Added new members and definitions ready for 2nd generation of bots. + */ + +#include "../g_local.h" +#include "../m_player.h" + +//---------------------------------------------- +// BOTCOL_CanJumpForward +//---------------------------------------------- + +qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, temp, dest; + trace_t trace; + + + VectorCopy (self->s.origin, temp); + temp[2]+=22; // create a point in line with bot's eyes + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + VectorMA(temp, TJUMP_DIST, dir, dest); + + trace = gi.trace(temp, tv(-16,-16,0), tv(16,16,56), dest, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + + if (trace.fraction == 1.0) + return (true); + return (false); +} + +//---------------------------------------------- +// BOTCOL_CanCrawl +// +// Checks a viewport in front of the entity and returns true +// if it can be crawled into +//---------------------------------------------- + +qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest; + trace_t trace; + + + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + VectorMA(self->s.origin, TCRAWL_DIST, dir, dest); + + // Trying to trace from FeetPos to CrawlHeightPos + trace = gi.trace(self->s.origin, tv(-16,-16,-22), tv(16,16,0), dest, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + // Complete trace possible + if (trace.fraction == 1.0) + return (true); + // Failed + return (false); +} + +//--------------------------------------------- +// BOTCOL_CanStand +// +// Returns true if it's possible to stand up. +//--------------------------------------------- +qboolean BOTCOL_CanStand(edict_t *self) +{ + static vec3_t maxs = {16, 16, 32}; + trace_t trace; + + trace = gi.trace(self->s.origin, self->mins, maxs, self->s.origin, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + else + return (true); +} + +//--------------------------------------------- +// BOTCOL_CanJumpUp +// +// Returns true if it's possible to jump up. +//--------------------------------------------- +qboolean BOTCOL_CanJumpUp(edict_t *self) +{ + static vec3_t maxs = {16, 16, 64}; + trace_t trace; + + trace = gi.trace(self->s.origin, self->mins, maxs, self->s.origin, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + else + return (true); +} + +//--------------------------------------------- +// BOTCOL_CanMoveForward +// +// Returns true if it's possible to move forward +//--------------------------------------------- + +qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, temp, dest; + trace_t trace; + + + VectorCopy (self->s.origin, temp); + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + VectorMA(temp, TMOVE_DIST, dir, dest); + // Debugging +// BOTUT_TempLaser (temp, dest); + + // Trying to trace from FeetPos to HeightPos + // Take a little off the bottom of the box from -22 to allow for slopes + trace = gi.trace(temp, tv(-16,-16,-8), tv(16,16,32), dest, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + + if (trace.fraction == 1.0) + return (true); + // Failed + return (false); +} + +//----------------------------------------------- +// BOTCOL_WaterMoveForward +// +// Returns true if botcan move forward in water +//----------------------------------------------- + +qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest; + trace_t trace; + + + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + VectorMA(self->s.origin, TWATER_DIST, dir, dest); + + // Trying to trace from FeetPos to HeightPos + // Take a little off the bottom of the box from -22 to allow for slopes + trace = gi.trace(self->s.origin, tv(-16,-16,-10), tv(16,16,32), dest, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + + if (trace.fraction == 1.0) + return (true); + // Failed + return (false); +} + +//======================================================= +// BOTCOL_CanLeaveWater +//======================================================= +// +qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, temp, dest; + trace_t trace; + + + VectorCopy (self->s.origin, temp); + temp[2]+=22; // create a point in line with bot's eyes + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + VectorMA(temp, TWEDGE_DIST, dir, dest); + + trace = gi.trace(temp, tv(-16,-16,0), tv(16,16,56), dest, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); + + if (trace.fraction == 1.0) + return (true); + + return (false); +} + + +//---------------------------------------------- +// BOTCOL_CanMoveSafely +// +// Checks for lava, slime and long drops +//---------------------------------------------- +//#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) + +qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest1, dest2; + trace_t trace; + //float this_dist; + + VectorClear(angle); + angle[1] = angles[1]; + + AngleVectors(angle, dir, NULL, NULL); + + // create a position in front of the bot + VectorMA(self->s.origin, TRACE_DIST_SHORT, dir, dest1); + + // Modified to check for crawl direction + //BOTUT_TempLaser (self->s.origin, dest1); + trace = gi.trace(self->s.origin, tv(-16,-16,0), tv(16,16,0), dest1, self, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (true); + + if (trace.fraction > 0) + { // check that destination is onground, or not above lava/slime + dest1[0] = trace.endpos[0]; + dest1[1] = trace.endpos[1]; + dest1[2] = trace.endpos[2] - 28; + //this_dist = trace.fraction * TRACE_DIST_SHORT; + + if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) + return (true); + // create a position a distance below it + VectorCopy( trace.endpos, dest2); + dest2[2] -= TRACE_DOWN; + //BOTUT_TempLaser (trace.endpos, dest2); + trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); + + if( (trace.fraction == 1.0) // long drop! + || (trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA + { + return (false); + } + else + { + return (true); + } + } + //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); + return (false); +} + +//---------------------------------------------- +// BOTCOL_CanStrafeSafely +// +// Checks for lava, slime and long drops +//---------------------------------------------- +//#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) + +qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest1, dest2; + trace_t trace; + //float this_dist; + int sign; + + sign = (self->bot_strafe > 0) ? (-1) : (1); + VectorClear(angle); + angle[1] = anglemod(angles[1] + (sign * 90));//[forward][right][up] + + AngleVectors(angle, dir, NULL, NULL); + + // create a position to the side of the bot + VectorMA(self->s.origin, TRACE_DIST_STRAFE, dir, dest1); + + // Modified to check for crawl direction + //BOTUT_TempLaser (self->s.origin, dest1); + trace = gi.trace(self->s.origin, tv(-16,-16,-22), tv(16,16,0), dest1, self, MASK_SOLID); + + if (trace.fraction > 0) + { // check that destination is onground, or not above lava/slime + dest1[0] = trace.endpos[0]; + dest1[1] = trace.endpos[1]; + dest1[2] = trace.endpos[2] - 24; + //this_dist = trace.fraction * TRACE_DIST_STRAFE; + + if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) + return (true); + + // create a position a distance below it + VectorCopy( trace.endpos, dest2); + dest2[2] -= TRACE_DOWN_STRAFE; + trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_SOLID | MASK_DEADLY); + + if( (trace.fraction == 1.0) // don't drop! + ||(trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA + { + return (false); + } + else + { + return (true); + } + } + //gi.bprintf(PRINT_HIGH,"Default failure from LAVACHECK\n"); + return (false); +} + +//---------------------------------------------- +// BOTCOL_CanJumpGap +// +// +// Tests for a gap in the floor and if bot can cross it safely +//----------------------------------------------- +qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles) +{ + vec3_t dir, angle, dest1, dest2; + trace_t trace; + + VectorClear(angle); + angle[1] = angles[1]; + AngleVectors(angle, dir, NULL, NULL); + + // create a position in front of the bot + VectorMA(self->s.origin, TJUMP_DIST, dir, dest1); + + // Modified to check for crawl direction + //BOTUT_TempLaser (self->s.origin, dest1); + trace = gi.trace(self->s.origin, tv(-16,-16,-12), tv(16,16,32), dest1, self, MASK_SOLID); + + // Check if we are looking inside a wall! + if (trace.startsolid) + return (false); // we need a gap! + + if (trace.fraction !=1.0) + return (false); // we still need a gap! + else + { // check that destination is not onground + // create a position a distance below it + VectorCopy( trace.endpos, dest2); + dest2[2] -= TRACE_DOWN_STRAFE; + //BOTUT_TempLaser (trace.endpos, dest2); + trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_SOLID);// | MASK_DEADLY); + + if(trace.fraction != 1.0) // There is no gap! + { + return (false); + } + else + { + //gi.bprintf(PRINT_HIGH,"CanJumpGap: Checking the gap distance\n"); + //Now look for a longer gap to jump over + VectorMA(self->s.origin, TRACE_DIST_JUMP, dir, dest1); + trace = gi.trace(self->s.origin, tv(-16,-16,-12), tv(16,16,32), dest1, self, MASK_SOLID); + if (trace.fraction < 0.5) + { + return (false); // we still need a longer gap! + } + dest1[0] = trace.endpos[0]; + dest1[1] = trace.endpos[1]; + dest1[2] = trace.endpos[2] - 28; + + if (gi.pointcontents(dest1) & MASK_PLAYERSOLID) + // We have a safe landing area + return (true); + else // check if we get hurt by jumping here + { + VectorCopy( trace.endpos, dest2); + dest2[2] -= TRACE_DOWN; + trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, (MASK_SOLID | MASK_DEADLY)); + + if( (trace.fraction == 1.0) // don't drop too far! + ||(trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA + { + return (false); + } + else + { + // there is no obvious danger + return (true); + } + } + } + } + //gi.bprintf(PRINT_HIGH,"Default failure from JumpGap\n"); + return (false); +} + + +//--------------------------------------------- +// BOTCOL_CheckBump +// +// Helps steer the bot round protrusions, crate edges, etc., ... +//--------------------------------------------- + +qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd) +{ + vec3_t angles, forward, right, aim, leftarm, rightarm; + trace_t ltr, rtr; + + VectorCopy (bot->s.angles, angles); + angles[0] = 0; + AngleVectors (angles, forward, right, NULL); + + VectorScale (forward, 80, aim); + VectorAdd (bot->s.origin, aim, aim); + + VectorScale (right, 16, right); + VectorAdd (bot->s.origin, right, rightarm); + VectorInverse (right); + VectorAdd (bot->s.origin, right, leftarm); + + ltr = gi.trace (leftarm, vec3_origin, vec3_origin, aim, bot, MASK_SOLID); + rtr = gi.trace (rightarm, vec3_origin, vec3_origin, aim, bot, MASK_SOLID); + + if ((ltr.fraction < 1.00) && (rtr.fraction == 1.00)) + { + //gi.centerprintf (bot, "Strafe RIGHT, goddamit!\n"); + bot->bot_strafe = SPEED_WALK; + return true; + } + if ((ltr.fraction == 1.00) && (rtr.fraction < 1.00)) + { + //gi.centerprintf (bot, "Strafe LEFT, goddamit!\n"); + bot->bot_strafe = -SPEED_WALK; + return true; + } + if ((ltr.fraction < 1.00) && (rtr.fraction < 1.00)) + { + //gi.centerprintf (bot, "That's a WALL!\n"); + } + + // Debugging + // TempNode (aim, 0.2); + // BOTUT_TempLaser (leftarm, aim); + // BOTUT_TempLaser (rightarm, aim); + return false; +} + +//--------------------------------------------- +// BOTCOL_Visible +// +// returns 1 if any part of the entity is visible to bot +// (Yes - accurate bot vision is based on collision detection!) +//--------------------------------------------- + +qboolean BOTCOL_Visible (edict_t *bot, edict_t *other) +{ + vec3_t vEyes, vForward, actvec, offset3; + vec3_t origin, sawhim, vTemp; + vec_t offset; + trace_t tr1, tr2, tr3; // 1 = bottom right + // 2 = top left + // 3 = origin + + // We don't use the eye height since the test is for shooting. + VectorCopy (bot->s.origin, vEyes); + VectorCopy (other->s.origin, origin); + + if (other->client) + { + if (other->client->ps.pmove.pm_flags & PMF_DUCKED) + { + origin[2] -= 18; + } + } + + // May change this later to allow for glass / breakable etc.. + tr1 = gi.trace (vEyes, vec3_origin, vec3_origin, other->absmin, bot, MASK_PLAYERSOLID); + tr2 = gi.trace (vEyes, vec3_origin, vec3_origin, other->absmax, bot, MASK_PLAYERSOLID); + tr3 = gi.trace (vEyes, vec3_origin, vec3_origin, origin, bot, MASK_PLAYERSOLID); + + VectorCopy (vec3_origin, sawhim); + if (tr1.fraction == 1.0) + VectorCopy (other->absmin, sawhim); + if (tr2.fraction == 1.0) + VectorCopy (other->absmax, sawhim); + if (tr3.fraction == 1.0) + VectorCopy (origin, sawhim); + + if (VectorCompare(sawhim, vec3_origin) == 0) + { + AngleVectors (bot->s.angles, vForward, NULL, NULL); + VectorSubtract (sawhim, bot->s.origin, actvec); + VectorNormalize (actvec); + VectorSubtract (actvec, vForward, offset3); + offset = VectorLength (offset3); + + // Within Field Of View? + if (offset < ROOT2) + return true; + + // Have we already seen this enemy + if( other == bot->enemy ) + { + VectorSubtract(other->s.origin, bot->s.origin, vTemp); + // Is it too close for us to ignore? + if( VectorLength(vTemp) < 100.0) + return true; + } + } + return false; +} + +//---------------------------------------------- +// BOTCOL_CheckShot +// +// See if we can hit the target without hitting a teammate +//---------------------------------------------- + +qboolean BOTCOL_CheckShot(edict_t *bot) +{ + trace_t tTrace; + + tTrace = gi.trace (bot->s.origin, vec3_origin, vec3_origin, bot->enemy->s.origin, bot, MASK_SOLID|MASK_OPAQUE); + + // If we are in teamplay, check enemy team. + if(teamplay->value && tTrace.ent && tTrace.ent->client) + { + if( tTrace.ent->client->resp.team == bot->client->resp.team ) + return (false); + } + // Clear shot. + return true; +} + +//---------------------------------------------- +// BOTCOL_CanReachItem +// +// This _really_ needs more work! +//---------------------------------------------- + +qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal) +{ + trace_t tTrace; + vec3_t v; + + VectorCopy(bot->mins,v); + v[2] += 18; // Stepsize + + tTrace = gi.trace (bot->s.origin, v, bot->maxs, goal, bot, MASK_SOLID|MASK_OPAQUE); + + // Yes we can see it + if (tTrace.fraction == 1.0) + return true; + else + return false; +} diff --git a/src/action/acesrc/bot_combat.c b/src/action/acesrc/bot_combat.c index 472d8062c..4051df2cb 100644 --- a/src/action/acesrc/bot_combat.c +++ b/src/action/acesrc/bot_combat.c @@ -1,230 +1,230 @@ -//--------------------------------------------- -// bot_combat.c -// -// Copyright (c) 2000 Connor Caple all rights reserved -// -// This file contains the bot combat and aiming code -//--------------------------------------------- -/* - * $Log: /LTK2/src/acesrc/bot_combat.c $ - * - * 2 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - */ - -#include "../g_local.h" - -//----------------------------------------------- -// BOTCOM_Aim -// -// Sets angles to face ae entity -//----------------------------------------------- - -void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles) -{ - vec3_t vDir, vStart, vEnd; - - VectorCopy(target->s.origin, vEnd); - if (target->client) - { - if (target->client->ps.pmove.pm_flags & PMF_DUCKED) - { - // Aim lower..... - vEnd[2] -= 18; - } - } - VectorCopy(bot->s.origin, vStart); - VectorSubtract(vEnd, vStart, vDir); - vectoangles(vDir, angles); -} - -//----------------------------------------------- -// BOTCOM_AimAt -// -// Sets angles to face a point in space -//----------------------------------------------- - -void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles) -{ - vec3_t vDir, vStart; - - if( level.framenum > bot->grenadewait ) - { - bot->grenadewait = 0; - VectorCopy(bot->s.origin, vStart); - VectorSubtract(target, vStart, vDir); - VectorNormalize (vDir); - vectoangles(vDir, angles); - } -} - -//---------------------------------------------- -// BOTCOM_BasicAttack -// -// Simple attack function just to get the bots fighting -//---------------------------------------------- - -void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget) -{ - float fRandValue; - vec3_t vAttackVector; - float vDist; - qboolean bHasWeapon; // Needed to allow knife throwing and kick attacks - - bHasWeapon = BOTWP_ChooseWeapon(bot); - - // Check distance to enemy - VectorSubtract( bot->s.origin, bot->enemy->s.origin, vAttackVector); - vDist = VectorLength( vAttackVector); - - // Don't stand around if all you have is a knife or no weapon. - if( - (bot->client->weapon == FindItem(KNIFE_NAME)) - || !bHasWeapon // kick attack - ) - { - // Don't walk off the edge - if( BOTCOL_CanMoveSafely(bot, bot->s.angles)) - { - bot->bot_speed = SPEED_RUN; - } - else - { - // Can't get there! - bot->bot_speed = -SPEED_WALK; - bot->enemy=NULL; - bot->nextState = BS_ROAM; - return; - } - - if( vDist < 200) - { - // See if we want to throw the knife - if( random() < 0.3 && bHasWeapon) - { - // Yes we do. - bot->client->pers.knife_mode = 1; - } - else - { - if( vDist < 64 ) // Too close - bot->bot_speed = -SPEED_WALK; - // Kick Attack needed! - cmd->upmove = 400; - } - } - else - { - // Outside desired throwing range - bot->client->pers.knife_mode = 0; - } - } - else if(!(bot->client->weapon == FindItem(SNIPER_NAME))) // Stop moving with sniper rifle - { - // Randomly choose a movement direction - fRandValue = random(); - if(fRandValue < 0.2) - { - bot->bot_strafe = SPEED_WALK; - } - else if(fRandValue < 0.4) - { - bot->bot_strafe = -SPEED_WALK; - } - if( fRandValue < 0.4) - { - // Check it's safe and set up the move. - if( BOTCOL_CanStrafeSafely(bot, bot->s.angles)) - { - cmd->sidemove = bot->bot_strafe; - } - } - - fRandValue = random(); - if(fRandValue < 0.6 && BOTCOL_CanMoveSafely(bot, bot->s.angles)) - bot->bot_speed = 400; - else if(fRandValue < 0.8) - bot->bot_speed = -200; - } - - if( (vDist < 600) && - ( !(bot->client->weapon == FindItem(KNIFE_NAME)) && - !(bot->client->weapon == FindItem(SNIPER_NAME))) // Stop jumping with sniper rifle - &&( bHasWeapon ) // Jump already set for kick attack - ) - { - // Randomly choose a vertical movement direction - fRandValue = random(); - - if(fRandValue < 0.10) - cmd->upmove += 200; - else if( fRandValue> 0.90 ) // Only crouch sometimes - cmd->upmove -= 200; - } - -// cmd->buttons = BUTTON_ATTACK; - // Set the attack - //@@ Check this doesn't break grenades! - if( - bHasWeapon && - ((bot->client->weaponstate == WEAPON_READY)||(bot->client->weaponstate == WEAPON_FIRING)) - ) - { - // Only shoot if the weapon is ready and we can hit the target! - if( BOTCOL_CheckShot( bot )) - { - // Raptor007: If bot skill is negative, don't fire. - if( ltk_skill->value >= 0 ) - cmd->buttons = BUTTON_ATTACK; - } - else - cmd->upmove = 200; - } - - // Aim - VectorCopy(bot->enemy->s.origin,vTarget); - - //@@ Add AimLeading here - - // Alter aiming based on skill level - if( - (ltk_skill->value < 10 ) - && ( bHasWeapon ) // Kick attacks must be accurate - && (!(bot->client->weapon == FindItem(KNIFE_NAME))) // Knives accurate - ) - { - short int up, right, iFactor=8; - up = (random() < 0.5)? -1 :1; - right = (random() < 0.5)? -1 : 1; - - // Not that complex. We miss by 0 to 80 units based on skill value and random factor - // Unless we have a sniper rifle! - if(bot->client->weapon == FindItem(SNIPER_NAME)) - iFactor = 1; - - // Raptor007: Only jiggle for skill >= 0 because negative skill doesn't fire. - if( ltk_skill->value >= 0 ) - { - vTarget[0] += ( right * (((iFactor*(10 - ltk_skill->value)) *random())) ); - vTarget[2] += ( up * (((iFactor*(10 - ltk_skill->value)) *random())) ); - } - } - - // Check the scope use for the snipers - if( (bot->client->curr_weap == SNIPER_NUM) && vDist > 250 ) - { - // Use the zoom - if( BOTWP_GetSniperMode(bot) == 0) - { - BOTWP_ChangeSniperMode(bot); - } - } - // Store time we last saw an enemy - // This value is used to decide if we initiate a long range search or not. - bot->teamPauseTime = level.framenum; - -// if(debug_mode) -// debug_printf("%s attacking %s\n",bot->client->pers.netname,bot->enemy->client->pers.netname); -} - +//--------------------------------------------- +// bot_combat.c +// +// Copyright (c) 2000 Connor Caple all rights reserved +// +// This file contains the bot combat and aiming code +//--------------------------------------------- +/* + * $Log: /LTK2/src/acesrc/bot_combat.c $ + * + * 2 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + */ + +#include "../g_local.h" + +//----------------------------------------------- +// BOTCOM_Aim +// +// Sets angles to face ae entity +//----------------------------------------------- + +void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles) +{ + vec3_t vDir, vStart, vEnd; + + VectorCopy(target->s.origin, vEnd); + if (target->client) + { + if (target->client->ps.pmove.pm_flags & PMF_DUCKED) + { + // Aim lower..... + vEnd[2] -= 18; + } + } + VectorCopy(bot->s.origin, vStart); + VectorSubtract(vEnd, vStart, vDir); + vectoangles(vDir, angles); +} + +//----------------------------------------------- +// BOTCOM_AimAt +// +// Sets angles to face a point in space +//----------------------------------------------- + +void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles) +{ + vec3_t vDir, vStart; + + if( level.framenum > bot->grenadewait ) + { + bot->grenadewait = 0; + VectorCopy(bot->s.origin, vStart); + VectorSubtract(target, vStart, vDir); + VectorNormalize (vDir); + vectoangles(vDir, angles); + } +} + +//---------------------------------------------- +// BOTCOM_BasicAttack +// +// Simple attack function just to get the bots fighting +//---------------------------------------------- + +void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget) +{ + float fRandValue; + vec3_t vAttackVector; + float vDist; + qboolean bHasWeapon; // Needed to allow knife throwing and kick attacks + + bHasWeapon = BOTWP_ChooseWeapon(bot); + + // Check distance to enemy + VectorSubtract( bot->s.origin, bot->enemy->s.origin, vAttackVector); + vDist = VectorLength( vAttackVector); + + // Don't stand around if all you have is a knife or no weapon. + if( + (bot->client->weapon == FindItem(KNIFE_NAME)) + || !bHasWeapon // kick attack + ) + { + // Don't walk off the edge + if( BOTCOL_CanMoveSafely(bot, bot->s.angles)) + { + bot->bot_speed = SPEED_RUN; + } + else + { + // Can't get there! + bot->bot_speed = -SPEED_WALK; + bot->enemy=NULL; + bot->nextState = BS_ROAM; + return; + } + + if( vDist < 200) + { + // See if we want to throw the knife + if( random() < 0.3 && bHasWeapon) + { + // Yes we do. + bot->client->pers.knife_mode = 1; + } + else + { + if( vDist < 64 ) // Too close + bot->bot_speed = -SPEED_WALK; + // Kick Attack needed! + cmd->upmove = 400; + } + } + else + { + // Outside desired throwing range + bot->client->pers.knife_mode = 0; + } + } + else if(!(bot->client->weapon == FindItem(SNIPER_NAME))) // Stop moving with sniper rifle + { + // Randomly choose a movement direction + fRandValue = random(); + if(fRandValue < 0.2) + { + bot->bot_strafe = SPEED_WALK; + } + else if(fRandValue < 0.4) + { + bot->bot_strafe = -SPEED_WALK; + } + if( fRandValue < 0.4) + { + // Check it's safe and set up the move. + if( BOTCOL_CanStrafeSafely(bot, bot->s.angles)) + { + cmd->sidemove = bot->bot_strafe; + } + } + + fRandValue = random(); + if(fRandValue < 0.6 && BOTCOL_CanMoveSafely(bot, bot->s.angles)) + bot->bot_speed = 400; + else if(fRandValue < 0.8) + bot->bot_speed = -200; + } + + if( (vDist < 600) && + ( !(bot->client->weapon == FindItem(KNIFE_NAME)) && + !(bot->client->weapon == FindItem(SNIPER_NAME))) // Stop jumping with sniper rifle + &&( bHasWeapon ) // Jump already set for kick attack + ) + { + // Randomly choose a vertical movement direction + fRandValue = random(); + + if(fRandValue < 0.10) + cmd->upmove += 200; + else if( fRandValue> 0.90 ) // Only crouch sometimes + cmd->upmove -= 200; + } + +// cmd->buttons = BUTTON_ATTACK; + // Set the attack + //@@ Check this doesn't break grenades! + if( + bHasWeapon && + ((bot->client->weaponstate == WEAPON_READY)||(bot->client->weaponstate == WEAPON_FIRING)) + ) + { + // Only shoot if the weapon is ready and we can hit the target! + if( BOTCOL_CheckShot( bot )) + { + // Raptor007: If bot skill is negative, don't fire. + if( ltk_skill->value >= 0 ) + cmd->buttons = BUTTON_ATTACK; + } + else + cmd->upmove = 200; + } + + // Aim + VectorCopy(bot->enemy->s.origin,vTarget); + + //@@ Add AimLeading here + + // Alter aiming based on skill level + if( + (ltk_skill->value < 10 ) + && ( bHasWeapon ) // Kick attacks must be accurate + && (!(bot->client->weapon == FindItem(KNIFE_NAME))) // Knives accurate + ) + { + short int up, right, iFactor=8; + up = (random() < 0.5)? -1 :1; + right = (random() < 0.5)? -1 : 1; + + // Not that complex. We miss by 0 to 80 units based on skill value and random factor + // Unless we have a sniper rifle! + if(bot->client->weapon == FindItem(SNIPER_NAME)) + iFactor = 1; + + // Raptor007: Only jiggle for skill >= 0 because negative skill doesn't fire. + if( ltk_skill->value >= 0 ) + { + vTarget[0] += ( right * (((iFactor*(10 - ltk_skill->value)) *random())) ); + vTarget[2] += ( up * (((iFactor*(10 - ltk_skill->value)) *random())) ); + } + } + + // Check the scope use for the snipers + if( (bot->client->curr_weap == SNIPER_NUM) && vDist > 250 ) + { + // Use the zoom + if( BOTWP_GetSniperMode(bot) == 0) + { + BOTWP_ChangeSniperMode(bot); + } + } + // Store time we last saw an enemy + // This value is used to decide if we initiate a long range search or not. + bot->teamPauseTime = level.framenum; + +// if(debug_mode) +// debug_printf("%s attacking %s\n",bot->client->pers.netname,bot->enemy->client->pers.netname); +} + diff --git a/src/action/acesrc/bot_env_doors.c b/src/action/acesrc/bot_env_doors.c index 90ad000aa..8fb579839 100644 --- a/src/action/acesrc/bot_env_doors.c +++ b/src/action/acesrc/bot_env_doors.c @@ -1,92 +1,92 @@ -/****************************************************************************/ -/* */ -/* project : BOT (c) 2000 Connor Caple */ -/* $Header: /LTK2/SRC/ACESRC/bot_env_doors.cpp 3 27/02/00 0:59 Riever $ */ -/* */ -/* Thanks to William for helping me to figure this stuff out */ -/* including his giving me CGF code samples to study */ -/****************************************************************************/ -/* - * $Log: /LTK2/SRC/ACESRC/bot_env_doors.cpp $ - * - * 3 27/02/00 0:59 Riever - * Added BOTENVD_IsDoorOpening and BOTENVD_IsDoorMoving - * - * 2 27/02/00 0:44 Riever - * Added BOTENVD_IsDoorOpen(edict_t* pDoor) from CGF code - */ - -#include "../g_local.h" - -//#include "bot_env_doors.h" - - -// constants - -// copied from g_func.c -#define STATE_TOP 0 -#define STATE_BOTTOM 1 -#define STATE_UP 2 -#define STATE_DOWN 3 - - -void door_use (edict_t *self, edict_t *other, edict_t *activator); - - -// types (from CGF ) -enum BOT_Door_Type -{ - BOT_DOOR_STANDARD = 0, - BOT_DOOR_ROTATING = 1, - BOT_DOOR_BUTTONOPERATED = 2 -}; -typedef enum BOT_Door_Type BOT_Door_Type_t; - -struct BOT_Env_Door -{ - edict_t* pDoor; - node_t iDoorNode; - BOT_Door_Type_t iDoorType; - vec3_t vDoorBackOffLocation; - vec3_t vDoorBackOffDirection; - vec_t fDoorBackOffTime; - edict_t* pButton; -}; -typedef struct BOT_Env_Door BOT_Env_Door_t; - -//---------------------------------------------- -// BOTENVD_IsDoorOpen (from CGF) -//---------------------------------------------- -int BOTENVD_IsDoorOpen -( -edict_t* pDoor -) -{ - return ( (pDoor->moveinfo.state == STATE_TOP) - ); -} - -//----------------------------------------------- -// BOTENVD_IsDoorOpening (from CGF) -//----------------------------------------------- -int BOTENVD_IsDoorOpening -( -edict_t* pDoor -) -{ - return ( (pDoor->moveinfo.state == STATE_UP) - || (pDoor->moveinfo.state == STATE_TOP) - ); -} - -//----------------------------------------------- -// BOTENVD_IsDoorMoving -//----------------------------------------------- -int BOTENVD_IsDoorMoving -( -edict_t* pDoor -) -{ - return ( VectorLength(pDoor->avelocity) > 0.0); -} - +/****************************************************************************/ +/* */ +/* project : BOT (c) 2000 Connor Caple */ +/* $Header: /LTK2/SRC/ACESRC/bot_env_doors.cpp 3 27/02/00 0:59 Riever $ */ +/* */ +/* Thanks to William for helping me to figure this stuff out */ +/* including his giving me CGF code samples to study */ +/****************************************************************************/ +/* + * $Log: /LTK2/SRC/ACESRC/bot_env_doors.cpp $ + * + * 3 27/02/00 0:59 Riever + * Added BOTENVD_IsDoorOpening and BOTENVD_IsDoorMoving + * + * 2 27/02/00 0:44 Riever + * Added BOTENVD_IsDoorOpen(edict_t* pDoor) from CGF code + */ + +#include "../g_local.h" + +//#include "bot_env_doors.h" + + +// constants + +// copied from g_func.c +#define STATE_TOP 0 +#define STATE_BOTTOM 1 +#define STATE_UP 2 +#define STATE_DOWN 3 + + +void door_use (edict_t *self, edict_t *other, edict_t *activator); + + +// types (from CGF ) +enum BOT_Door_Type +{ + BOT_DOOR_STANDARD = 0, + BOT_DOOR_ROTATING = 1, + BOT_DOOR_BUTTONOPERATED = 2 +}; +typedef enum BOT_Door_Type BOT_Door_Type_t; + +struct BOT_Env_Door +{ + edict_t* pDoor; + node_t iDoorNode; + BOT_Door_Type_t iDoorType; + vec3_t vDoorBackOffLocation; + vec3_t vDoorBackOffDirection; + vec_t fDoorBackOffTime; + edict_t* pButton; +}; +typedef struct BOT_Env_Door BOT_Env_Door_t; + +//---------------------------------------------- +// BOTENVD_IsDoorOpen (from CGF) +//---------------------------------------------- +int BOTENVD_IsDoorOpen +( +edict_t* pDoor +) +{ + return ( (pDoor->moveinfo.state == STATE_TOP) + ); +} + +//----------------------------------------------- +// BOTENVD_IsDoorOpening (from CGF) +//----------------------------------------------- +int BOTENVD_IsDoorOpening +( +edict_t* pDoor +) +{ + return ( (pDoor->moveinfo.state == STATE_UP) + || (pDoor->moveinfo.state == STATE_TOP) + ); +} + +//----------------------------------------------- +// BOTENVD_IsDoorMoving +//----------------------------------------------- +int BOTENVD_IsDoorMoving +( +edict_t* pDoor +) +{ + return ( VectorLength(pDoor->avelocity) > 0.0); +} + diff --git a/src/action/acesrc/bot_movement.c b/src/action/acesrc/bot_movement.c index c864cc66a..9e60f23c4 100644 --- a/src/action/acesrc/bot_movement.c +++ b/src/action/acesrc/bot_movement.c @@ -1,229 +1,229 @@ -//--------------------------------------------- -// bot_movement.c -// -// Copyright (c) 2000 Connor Caple all rights reserved -// -// This file contains the bot movement routines -//--------------------------------------------- -/* - * $Log: /LTK2/src/acesrc/bot_movement.c $ - * - * 3 2/03/00 15:37 Riever - * SetJumpVelocity, SetRandomDirection added. - * Roam movement code rewritten to be more useful. - * - * 2 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - */ - -#include "../g_local.h" - -void Cmd_Kill_f (edict_t *bot); - -//--------------------------------------------- -// BOTMV_Roaming -// -// Aimlessly wanders about the level bumping into things ;) -//--------------------------------------------- - -void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ - float fRoam, fLen; - vec3_t vTvec, vTempDest; - - //deal with completely stuck states first: - VectorSubtract(bot->lastPosition, bot->s.origin, vTvec); - // Check if we have been able to move using length between points - // if we are stuck, deal with it. - fLen = VectorLength(vTvec); - if( (fLen <= 2.0) ) - { - // we are stuck! - //gi.bprintf(PRINT_HIGH,"BS_WAIT: Stuck!\n"); - fRoam = BOTMV_FindBestDirection (bot, vTempDest, angles); - if (fRoam > 0) - { - // Level up our line of vision - angles[2] = bot->s.angles[2]; - // Turn to face that direction - BOTCOM_AimAt (bot, vTempDest, angles); - bot->bCrawl = false; - bot->bLastJump = false; - bot->bot_speed = SPEED_ROAM; - cmd->upmove = SPEED_WALK; // jump to get out of trouble - } - else if(fRoam == -1) // we are really stuck! - { - Cmd_Kill_f (bot); // suicide if stuck - } - } - else if( BOTCOL_CanMoveSafely(bot, angles)) - { - // Set probable walking speed - bot->bot_speed = SPEED_WALK; - - if ( !BOTCOL_CanMoveForward(bot, angles) ) - { - // Is it a corner edge? - if( - (BOTCOL_CheckBump(bot, cmd)) - && ( BOTCOL_CanStrafeSafely(bot, angles )) - ) - { - cmd->sidemove = bot->bot_strafe; - //Just carry on since this will set a strafe up for us - } - //Can we crawl forward? - else if( BOTCOL_CanCrawl(bot, angles)) - { - cmd->upmove = -200; - bot->bCrawl = true; - } - else if( - (BOTCOL_CanJumpForward(bot, angles)) - || (BOTCOL_CanJumpGap(bot, angles)) - ) - { - cmd->upmove = 400; - - // Make a point in front of the bot - BOTUT_MakeTargetVector(bot, angles, vTempDest); - BOTMV_SetJumpVelocity(bot, angles, vTempDest); - } - else - { - // Cannot go forward - BOTMV_SetRandomDirection(bot, angles); - } - } - } - else - { - // No safe move available - BOTMV_SetRandomDirection(bot, angles); - } -} - -//============================================================ -// BOTMV_FindBestDirection -//============================================================ -// -// -float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles) -{ - float fBestDist = -1.0, fThisDist; - int i; - vec3_t vDir, vAngle, vThisAngle, vDest, vDest2; - //vec3_t vMins; - trace_t tTrace; - -// if (bot->fLastBestDirection > (level.time - EYES_FREQ)) -// return (0); // This means called too soon -// bot->fLastBestDirection = level.time; - - //VectorAdd(bot->mins, tv(0,0,STRIDESIZE), vMins); - - // check eight compass directions - VectorClear(vAngle); - VectorClear(vThisAngle); - vAngle[1] = angles[1]; - - // start at center, then fan out in 45 degree intervals, swapping between + and - - for (i=1; i<8; i++) - { - if (i<7 && random() < 0.2) // skip random intervals - i++; - - if( ((i==3) ||(i==4)) && random() < 0.5) - i=5; // cut out 90 degree turns from time to time - - // Take alternate 45deg steps - vThisAngle[1] = anglemod(vAngle[1] + ((((i % 2)*2 - 1) * (int) (ceil(i/2))) * 45)); - AngleVectors(vThisAngle, vDir, NULL, NULL); - VectorMA(bot->s.origin, TRACE_DIST, vDir, vDest); - - // Modified to check for crawl direction - tTrace = gi.trace(bot->s.origin, tv(-16,-16,0), tv(16,16,0), vDest, bot, MASK_PLAYERSOLID); - - // Check if we are looking inside a wall! - if (tTrace.startsolid) - continue; - - if (tTrace.fraction > 0) - { // check that destination is onground, or not above lava/slime - vDest[0] = tTrace.endpos[0]; - vDest[1] = tTrace.endpos[1]; - vDest[2] = tTrace.endpos[2] - 24; - fThisDist = tTrace.fraction * TRACE_DIST; - - if (gi.pointcontents(vDest) & MASK_PLAYERSOLID) - goto nocheckground; - - - VectorCopy(tTrace.endpos, vDest2); - vDest2[2] -= 256; - tTrace = gi.trace(tTrace.endpos, VEC_ORIGIN, VEC_ORIGIN, vDest2, bot, MASK_PLAYERSOLID | MASK_WATER); - - if ( ! ((tTrace.fraction == 1) || (tTrace.contents & MASK_DEADLY))) // avoid ALL forms of liquid for now - { - //DEBUG - //gi.dprintf("Dist %i: %i\n", (int) vThisAngle[1], (int) fThisDist); - if (tTrace.fraction > 0.4) // if there is a drop in this direction, try to avoid it if possible - fThisDist *= 0.5; - } - else - { - //gi.bprintf(PRINT_HIGH,"BestDir : Ouch! Not going there\n"); - fThisDist = 0; // Do _not_ go there! - } -nocheckground: - if (fThisDist > fBestDist) - { - fBestDist = fThisDist; - VectorCopy( vDest, vBestDest); // save the best found so far - //Head for the wide open spaces :) - if(fThisDist == TRACE_DIST) - break; - } - } - } - // Send back a vec3_t to aim at in vBestDest and return a code to show what happened - // -1 == nothing found 0 == called too soon fNUM == distance safe to travel - return (fBestDist); -} - -//--------------------------------------------- -// BOTMV_SetJumpVelocity -// -// vTempDest is a point the bot wants to jump to -//--------------------------------------------- - -void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest) -{ - vec3_t vDest; - - VectorSubtract(vTempDest, bot->s.origin, vDest); - VectorNormalize(vDest); - // Velocity hack - VectorScale(vDest,440,bot->velocity); -} - -//---------------------------------------------- -// BOTMV_SetRandomDirection -//---------------------------------------------- - -void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles) -{ - vec3_t vTempDest; - - BOTMV_FindBestDirection (bot, vTempDest, angles); - // Level up our line of vision - angles[2] = bot->s.angles[2]; - // Turn to face that direction - BOTCOM_AimAt (bot, vTempDest, angles); - bot->bCrawl = false; - bot->bLastJump = false; - // Stop walking while we turn - bot->bot_speed = SPEED_ROAM; -} +//--------------------------------------------- +// bot_movement.c +// +// Copyright (c) 2000 Connor Caple all rights reserved +// +// This file contains the bot movement routines +//--------------------------------------------- +/* + * $Log: /LTK2/src/acesrc/bot_movement.c $ + * + * 3 2/03/00 15:37 Riever + * SetJumpVelocity, SetRandomDirection added. + * Roam movement code rewritten to be more useful. + * + * 2 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + */ + +#include "../g_local.h" + +void Cmd_Kill_f (edict_t *bot); + +//--------------------------------------------- +// BOTMV_Roaming +// +// Aimlessly wanders about the level bumping into things ;) +//--------------------------------------------- + +void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ + float fRoam, fLen; + vec3_t vTvec, vTempDest; + + //deal with completely stuck states first: + VectorSubtract(bot->lastPosition, bot->s.origin, vTvec); + // Check if we have been able to move using length between points + // if we are stuck, deal with it. + fLen = VectorLength(vTvec); + if( (fLen <= 2.0) ) + { + // we are stuck! + //gi.bprintf(PRINT_HIGH,"BS_WAIT: Stuck!\n"); + fRoam = BOTMV_FindBestDirection (bot, vTempDest, angles); + if (fRoam > 0) + { + // Level up our line of vision + angles[2] = bot->s.angles[2]; + // Turn to face that direction + BOTCOM_AimAt (bot, vTempDest, angles); + bot->bCrawl = false; + bot->bLastJump = false; + bot->bot_speed = SPEED_ROAM; + cmd->upmove = SPEED_WALK; // jump to get out of trouble + } + else if(fRoam == -1) // we are really stuck! + { + Cmd_Kill_f (bot); // suicide if stuck + } + } + else if( BOTCOL_CanMoveSafely(bot, angles)) + { + // Set probable walking speed + bot->bot_speed = SPEED_WALK; + + if ( !BOTCOL_CanMoveForward(bot, angles) ) + { + // Is it a corner edge? + if( + (BOTCOL_CheckBump(bot, cmd)) + && ( BOTCOL_CanStrafeSafely(bot, angles )) + ) + { + cmd->sidemove = bot->bot_strafe; + //Just carry on since this will set a strafe up for us + } + //Can we crawl forward? + else if( BOTCOL_CanCrawl(bot, angles)) + { + cmd->upmove = -200; + bot->bCrawl = true; + } + else if( + (BOTCOL_CanJumpForward(bot, angles)) + || (BOTCOL_CanJumpGap(bot, angles)) + ) + { + cmd->upmove = 400; + + // Make a point in front of the bot + BOTUT_MakeTargetVector(bot, angles, vTempDest); + BOTMV_SetJumpVelocity(bot, angles, vTempDest); + } + else + { + // Cannot go forward + BOTMV_SetRandomDirection(bot, angles); + } + } + } + else + { + // No safe move available + BOTMV_SetRandomDirection(bot, angles); + } +} + +//============================================================ +// BOTMV_FindBestDirection +//============================================================ +// +// +float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles) +{ + float fBestDist = -1.0, fThisDist; + int i; + vec3_t vDir, vAngle, vThisAngle, vDest, vDest2; + //vec3_t vMins; + trace_t tTrace; + +// if (bot->fLastBestDirection > (level.time - EYES_FREQ)) +// return (0); // This means called too soon +// bot->fLastBestDirection = level.time; + + //VectorAdd(bot->mins, tv(0,0,STRIDESIZE), vMins); + + // check eight compass directions + VectorClear(vAngle); + VectorClear(vThisAngle); + vAngle[1] = angles[1]; + + // start at center, then fan out in 45 degree intervals, swapping between + and - + for (i=1; i<8; i++) + { + if (i<7 && random() < 0.2) // skip random intervals + i++; + + if( ((i==3) ||(i==4)) && random() < 0.5) + i=5; // cut out 90 degree turns from time to time + + // Take alternate 45deg steps + vThisAngle[1] = anglemod(vAngle[1] + ((((i % 2)*2 - 1) * (int) (ceil(i/2))) * 45)); + AngleVectors(vThisAngle, vDir, NULL, NULL); + VectorMA(bot->s.origin, TRACE_DIST, vDir, vDest); + + // Modified to check for crawl direction + tTrace = gi.trace(bot->s.origin, tv(-16,-16,0), tv(16,16,0), vDest, bot, MASK_PLAYERSOLID); + + // Check if we are looking inside a wall! + if (tTrace.startsolid) + continue; + + if (tTrace.fraction > 0) + { // check that destination is onground, or not above lava/slime + vDest[0] = tTrace.endpos[0]; + vDest[1] = tTrace.endpos[1]; + vDest[2] = tTrace.endpos[2] - 24; + fThisDist = tTrace.fraction * TRACE_DIST; + + if (gi.pointcontents(vDest) & MASK_PLAYERSOLID) + goto nocheckground; + + + VectorCopy(tTrace.endpos, vDest2); + vDest2[2] -= 256; + tTrace = gi.trace(tTrace.endpos, VEC_ORIGIN, VEC_ORIGIN, vDest2, bot, MASK_PLAYERSOLID | MASK_WATER); + + if ( ! ((tTrace.fraction == 1) || (tTrace.contents & MASK_DEADLY))) // avoid ALL forms of liquid for now + { + //DEBUG + //gi.dprintf("Dist %i: %i\n", (int) vThisAngle[1], (int) fThisDist); + if (tTrace.fraction > 0.4) // if there is a drop in this direction, try to avoid it if possible + fThisDist *= 0.5; + } + else + { + //gi.bprintf(PRINT_HIGH,"BestDir : Ouch! Not going there\n"); + fThisDist = 0; // Do _not_ go there! + } +nocheckground: + if (fThisDist > fBestDist) + { + fBestDist = fThisDist; + VectorCopy( vDest, vBestDest); // save the best found so far + //Head for the wide open spaces :) + if(fThisDist == TRACE_DIST) + break; + } + } + } + // Send back a vec3_t to aim at in vBestDest and return a code to show what happened + // -1 == nothing found 0 == called too soon fNUM == distance safe to travel + return (fBestDist); +} + +//--------------------------------------------- +// BOTMV_SetJumpVelocity +// +// vTempDest is a point the bot wants to jump to +//--------------------------------------------- + +void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest) +{ + vec3_t vDest; + + VectorSubtract(vTempDest, bot->s.origin, vDest); + VectorNormalize(vDest); + // Velocity hack + VectorScale(vDest,440,bot->velocity); +} + +//---------------------------------------------- +// BOTMV_SetRandomDirection +//---------------------------------------------- + +void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles) +{ + vec3_t vTempDest; + + BOTMV_FindBestDirection (bot, vTempDest, angles); + // Level up our line of vision + angles[2] = bot->s.angles[2]; + // Turn to face that direction + BOTCOM_AimAt (bot, vTempDest, angles); + bot->bCrawl = false; + bot->bLastJump = false; + // Stop walking while we turn + bot->bot_speed = SPEED_ROAM; +} diff --git a/src/action/acesrc/bot_states.c b/src/action/acesrc/bot_states.c index 03e5f2cac..ec52872fb 100644 --- a/src/action/acesrc/bot_states.c +++ b/src/action/acesrc/bot_states.c @@ -1,167 +1,167 @@ -//--------------------------------------------- -// bot_states.c -// -// All bot states and reactions currently defined in here -//--------------------------------------------- - -#include "../g_local.h" - -//********************************************** -// BS_ACTIVE -//********************************************** - -// Positional sub-state -void ActivePosition( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ -} - -// Collecting sub-state -void ActiveCollect( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ -} - -// Attacking sub-state -void ActiveAttack( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ - if( bot->enemy ) - { - // Attack it in a nice way - } - else - { - // No enemy - get out of here - bot->secondaryState = BSS_NONE; - } -} - -// Chasing Enemy sub-state -void ActiveSeekEnemy( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ -} - -//----------------------------------------------- -// Main entry to the Active State -//----------------------------------------------- -void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ - // bot_speed and bot_strafe are set in here if needed - - switch( bot->secondaryState) - { - case BSS_POSITION: - ActivePosition( bot, angles, cmd ); - break; - case BSS_COLLECT: - ActiveCollect( bot, angles, cmd ); - break; - case BSS_SEEKENEMY: - ActiveSeekEnemy( bot, angles, cmd ); - break; - case BSS_ATTACK: - ActiveAttack( bot, angles, cmd ); - break; - case BSS_NONE: - default: - // Check for routing information - // Choose what to do - // Then do it - break; - } - - // Are any enemies visible? - // Have we taken any damage that needs us to react? - // Do we have a good enough gun? - // Do we need more ammo? - // Are there any interesting items lying about? -} - -//********************************************** -// BS_ROAM -//********************************************** - -// Attacking sub-state -void RoamingAttack( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ - vec3_t vTarget; - - if( bot->enemy && - (bot->enemy->solid != SOLID_NOT) && - BOTCOL_Visible( bot, bot->enemy) - ) - { - BOTCOM_BasicAttack (bot, cmd, vTarget); - // Aim at returned vector - BOTCOM_AimAt( bot, vTarget, angles ); - if(debug_mode) - BOTUT_TempLaser( bot->s.origin, vTarget); - } - else - { - // No visible enemy - get out of here - bot->enemy = NULL; - bot->secondaryState = BSS_NONE; - } -} - -// 'Normal' sub-state -void RoamingNone( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ -/* // Check if we have routes available - bot->current_node = ACEND_FindClosestReachableNode(bot,NODE_DENSITY, NODE_ALL); - if( bot->current_node != INVALID) - bot->nextState = BS_ACTIVE;*/ - - // Get us out of sniper zoom mode if we were in it - BOTWP_RemoveSniperZoomMode(bot); - - // Remember to move - BOTMV_Roaming( bot, angles, cmd ); - - // Have we taken any damage that needs us to react? - if( BOTAI_NeedToBandage(bot)) - Cmd_Bandage_f(bot); - - // Are any enemies visible? (This sets bot->enemy too) - if( BOTAI_VisibleEnemy(bot)) - { - bot->secondaryState = BSS_ATTACK; - } - else - { - // Do we have a good enough gun? - // Do we need more ammo? - if(!bot->movetarget) - { - // Are there any interesting items lying about? - BOTAI_PickShortRangeGoal(bot); - } - // If it's gone - if(bot->movetarget && bot->movetarget->solid == SOLID_NOT) - bot->movetarget = NULL; - // Otherwise - if(bot->movetarget) - { - BOTCOM_AimAt(bot, bot->movetarget->s.origin, angles); - } - } -} - -//----------------------------------------------- -// Main entry to the Roaming State -//----------------------------------------------- -void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd) -{ - // bot_speed and bot_strafe are set in here if needed - - switch( bot->secondaryState) - { - case BSS_ATTACK: - RoamingAttack(bot, angles, cmd); - break; - case BSS_NONE: - default: - RoamingNone(bot, angles, cmd); - break; - } - -} +//--------------------------------------------- +// bot_states.c +// +// All bot states and reactions currently defined in here +//--------------------------------------------- + +#include "../g_local.h" + +//********************************************** +// BS_ACTIVE +//********************************************** + +// Positional sub-state +void ActivePosition( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ +} + +// Collecting sub-state +void ActiveCollect( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ +} + +// Attacking sub-state +void ActiveAttack( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ + if( bot->enemy ) + { + // Attack it in a nice way + } + else + { + // No enemy - get out of here + bot->secondaryState = BSS_NONE; + } +} + +// Chasing Enemy sub-state +void ActiveSeekEnemy( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ +} + +//----------------------------------------------- +// Main entry to the Active State +//----------------------------------------------- +void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ + // bot_speed and bot_strafe are set in here if needed + + switch( bot->secondaryState) + { + case BSS_POSITION: + ActivePosition( bot, angles, cmd ); + break; + case BSS_COLLECT: + ActiveCollect( bot, angles, cmd ); + break; + case BSS_SEEKENEMY: + ActiveSeekEnemy( bot, angles, cmd ); + break; + case BSS_ATTACK: + ActiveAttack( bot, angles, cmd ); + break; + case BSS_NONE: + default: + // Check for routing information + // Choose what to do + // Then do it + break; + } + + // Are any enemies visible? + // Have we taken any damage that needs us to react? + // Do we have a good enough gun? + // Do we need more ammo? + // Are there any interesting items lying about? +} + +//********************************************** +// BS_ROAM +//********************************************** + +// Attacking sub-state +void RoamingAttack( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ + vec3_t vTarget; + + if( bot->enemy && + (bot->enemy->solid != SOLID_NOT) && + BOTCOL_Visible( bot, bot->enemy) + ) + { + BOTCOM_BasicAttack (bot, cmd, vTarget); + // Aim at returned vector + BOTCOM_AimAt( bot, vTarget, angles ); + if(debug_mode) + BOTUT_TempLaser( bot->s.origin, vTarget); + } + else + { + // No visible enemy - get out of here + bot->enemy = NULL; + bot->secondaryState = BSS_NONE; + } +} + +// 'Normal' sub-state +void RoamingNone( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ +/* // Check if we have routes available + bot->current_node = ACEND_FindClosestReachableNode(bot,NODE_DENSITY, NODE_ALL); + if( bot->current_node != INVALID) + bot->nextState = BS_ACTIVE;*/ + + // Get us out of sniper zoom mode if we were in it + BOTWP_RemoveSniperZoomMode(bot); + + // Remember to move + BOTMV_Roaming( bot, angles, cmd ); + + // Have we taken any damage that needs us to react? + if( BOTAI_NeedToBandage(bot)) + Cmd_Bandage_f(bot); + + // Are any enemies visible? (This sets bot->enemy too) + if( BOTAI_VisibleEnemy(bot)) + { + bot->secondaryState = BSS_ATTACK; + } + else + { + // Do we have a good enough gun? + // Do we need more ammo? + if(!bot->movetarget) + { + // Are there any interesting items lying about? + BOTAI_PickShortRangeGoal(bot); + } + // If it's gone + if(bot->movetarget && bot->movetarget->solid == SOLID_NOT) + bot->movetarget = NULL; + // Otherwise + if(bot->movetarget) + { + BOTCOM_AimAt(bot, bot->movetarget->s.origin, angles); + } + } +} + +//----------------------------------------------- +// Main entry to the Roaming State +//----------------------------------------------- +void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd) +{ + // bot_speed and bot_strafe are set in here if needed + + switch( bot->secondaryState) + { + case BSS_ATTACK: + RoamingAttack(bot, angles, cmd); + break; + case BSS_NONE: + default: + RoamingNone(bot, angles, cmd); + break; + } + +} diff --git a/src/action/acesrc/bot_utility.c b/src/action/acesrc/bot_utility.c index f740073ed..29ec134a4 100644 --- a/src/action/acesrc/bot_utility.c +++ b/src/action/acesrc/bot_utility.c @@ -1,223 +1,224 @@ -//--------------------------------------------- -// bot_utility.c -// -// Contains utility code used by other functions for debugging etc. -// -// Copyright (c) 2000 Connor Caple all rights reserved -//--------------------------------------------- -/* - * $Log: /LTK2/src/acesrc/bot_utility.c $ - * - * 7 2/03/00 11:34 Riever - * Added MakeTargetVector to set up fake jump targets. - * - * 6 24/02/00 20:06 Riever - * Shownodes distance added. - */ - -#include "../g_local.h" - - -#define SHOWNODES_DIST 384 // Max range for shownodes -//--------------------------------------------- -// BOTUT_TempLaser() -// start - where the laser starts from -// end - where the laser will end -// -// (Got this one from XoXus) -//--------------------------------------------- -void BOTUT_TempLaser (vec3_t start, vec3_t end) -{ - gi.WriteByte (svc_temp_entity); - gi.WriteByte (TE_BFG_LASER); - gi.WritePosition (start); - gi.WritePosition (end); - gi.multicast (start, MULTICAST_ALL); -} - - -//--------------------------------------------- -// BOTUT_NodeMarker -//--------------------------------------------- - -void BOTUT_NodeMarker (int iNode) -{ - edict_t *ent; - - ent = G_Spawn(); - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - - if(nodes[iNode].type == NODE_MOVE) - ent->s.renderfx = RF_SHELL_BLUE; - else if (nodes[iNode].type == NODE_WATER) - ent->s.renderfx = RF_SHELL_RED; - else - ent->s.renderfx = RF_SHELL_GREEN; // action nodes - - ent->s.modelindex = gi.modelindex ("models/items/ammo/grenades/medium/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + 0.2 * HZ; - ent->think = G_FreeEdict; - ent->dmg = 0; - - VectorCopy(nodes[iNode].origin,ent->s.origin); - gi.linkentity (ent); - -} - -//---------------------------------------------- -// BOTUT_ShowNodes -// -// This routine creates a temp node entity for the ent's viewport -// It only shows nodes that are currently visible. -//---------------------------------------------- - -void BOTUT_ShowNodes (edict_t *ent) -{ - int i; - vec3_t vEyes, vNodePos, vForward, vLos, vOffset, vDiff; // Los = Line Of Sight - node_t *pThisNode; - trace_t tTrace; - float fFudge; - - if ((!ent) || (!ent->client)) - return; - - VectorCopy (ent->s.origin, vEyes); - vEyes[2] += ent->viewheight; - - for (i=0; i < numnodes; i++) - { - pThisNode = &nodes[i]; - if (pThisNode->nodenum < 0) - continue; - VectorCopy (pThisNode->origin, vNodePos); - - // Distance check for visibility - VectorSubtract(vEyes, vNodePos, vDiff); - if( VectorLength(vDiff) > SHOWNODES_DIST ) - continue; - - tTrace = gi.trace (vEyes, vec3_origin, vec3_origin, - vNodePos, ent, MASK_OPAQUE); - if (tTrace.fraction < 0.9) - continue; - // Ok, can see the node - // But is it in the viewport? - AngleVectors (ent->s.angles, vForward, NULL, NULL); - //VectorNormalize (forward); - VectorAdd (vForward, vEyes, vForward); - VectorSubtract (vNodePos, vEyes, vLos); - VectorNormalize (vLos); - VectorAdd (vLos, vEyes, vLos); - VectorSubtract (vLos, vForward, vOffset); - fFudge = VectorLength (vOffset); - if ((1 - (fFudge * fFudge / 2)) > COS90) - { - BOTUT_NodeMarker(i); - } - } -} - -//---------------------------------------------- -// BOTUT_Cmd_Say_f -//---------------------------------------------- - -void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg) -{ - int j, /*i,*/ offset_of_text; - edict_t *other; - char text[2048]; - //gclient_t *cl; - - - if (!teamplay->value) - return; - - if (ent->client->resp.team == NOTEAM) - return; - - Q_snprintf (text, sizeof(text), "%s%s: ", - (teamplay->value && (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD)) ? "[DEAD] " : "", - ent->client->pers.netname); - - offset_of_text = strlen(text); //FB 5/31/99 - - strcat (text, pMsg); - - // don't let text be too long for malicious reasons - // ...doubled this limit for Axshun -FB - if (strlen(text) > 300) - text[300] = 0; - - if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) - ParseSayText(ent, text + offset_of_text, strlen(text)); //FB 5/31/99 - offset change - // this will parse the % variables, - // and again check 300 limit afterwards -FB - // (although it checks it without the name in front, oh well) - - strcat(text, "\n"); - -/* - if (flood_msgs->value) - { - cl = ent->client; - - if (level.time < cl->flood_locktill) - return; - - i = cl->flood_whenhead - flood_msgs->value + 1; - if (i < 0) - i = (sizeof(cl->flood_when)/sizeof(cl->flood_when[0])) + i; - if (cl->flood_when[i] && level.time - cl->flood_when[i] < flood_persecond->value) - { - cl->flood_locktill = level.time + flood_waitdelay->value; - return; - } - cl->flood_whenhead = (cl->flood_whenhead + 1) % (sizeof(cl->flood_when)/sizeof(cl->flood_when[0])); - cl->flood_when[cl->flood_whenhead] = level.time; - } -*/ - - if (dedicated->value) - safe_cprintf(NULL, PRINT_CHAT, "%s", text); - - for (j = 1; j <= game.maxclients; j++) - { - other = &g_edicts[j]; - if (!other->inuse) - continue; - if (!other->client) - continue; - if (!OnSameTeam(ent, other)) - continue; - if (teamplay->value && team_round_going) - { - if ((ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) - && (other->solid != SOLID_NOT && other->deadflag != DEAD_DEAD)) - continue; - } - - safe_cprintf(other, PRINT_CHAT, "%s", text); - } -} - -//---------------------------------------------- -// BOTUT_MakeTargetVector -// -// Creates a fake jump vector for the bot to aim at -//---------------------------------------------- -void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest) -{ - vec3_t vDir, vAngle, vTemp; - - VectorCopy (bot->s.origin, vTemp); - vTemp[2]+=22; // create a point in line with bot's eyes - VectorClear(vAngle); - vAngle[1] = angles[1]; - - AngleVectors(vAngle, vDir, NULL, NULL);// Forward vector - - VectorMA(vTemp, 64, vDir, vDest); -} +//--------------------------------------------- +// bot_utility.c +// +// Contains utility code used by other functions for debugging etc. +// +// Copyright (c) 2000 Connor Caple all rights reserved +//--------------------------------------------- +/* + * $Log: /LTK2/src/acesrc/bot_utility.c $ + * + * 7 2/03/00 11:34 Riever + * Added MakeTargetVector to set up fake jump targets. + * + * 6 24/02/00 20:06 Riever + * Shownodes distance added. + */ + +#include "../g_local.h" + + +#define SHOWNODES_DIST 384 // Max range for shownodes +//--------------------------------------------- +// BOTUT_TempLaser() +// start - where the laser starts from +// end - where the laser will end +// +// (Got this one from XoXus) +//--------------------------------------------- +void BOTUT_TempLaser (vec3_t start, vec3_t end) +{ + gi.WriteByte (svc_temp_entity); + gi.WriteByte (TE_BFG_LASER); + gi.WritePosition (start); + gi.WritePosition (end); + gi.multicast (start, MULTICAST_ALL); +} + + +//--------------------------------------------- +// BOTUT_NodeMarker +//--------------------------------------------- + +void BOTUT_NodeMarker (int iNode) +{ + edict_t *ent; + + ent = G_Spawn(); + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + + if(nodes[iNode].type == NODE_MOVE) + ent->s.renderfx = RF_SHELL_BLUE; + else if (nodes[iNode].type == NODE_WATER) + ent->s.renderfx = RF_SHELL_RED; + else + ent->s.renderfx = RF_SHELL_GREEN; // action nodes + + ent->s.modelindex = gi.modelindex ("models/items/ammo/grenades/medium/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + 0.2 * HZ; + ent->think = G_FreeEdict; + ent->dmg = 0; + + VectorCopy(nodes[iNode].origin,ent->s.origin); + gi.linkentity (ent); + +} + +//---------------------------------------------- +// BOTUT_ShowNodes +// +// This routine creates a temp node entity for the ent's viewport +// It only shows nodes that are currently visible. +//---------------------------------------------- + +void BOTUT_ShowNodes (edict_t *ent) +{ + int i; + vec3_t vEyes, vNodePos, vForward, vLos, vOffset, vDiff; // Los = Line Of Sight + node_t *pThisNode; + trace_t tTrace; + float fFudge; + + if ((!ent) || (!ent->client)) + return; + + VectorCopy (ent->s.origin, vEyes); + vEyes[2] += ent->viewheight; + + for (i=0; i < numnodes; i++) + { + pThisNode = &nodes[i]; + //pThisNode = nodes[i]; + if (pThisNode->nodenum < 0) + continue; + VectorCopy (pThisNode->origin, vNodePos); + + // Distance check for visibility + VectorSubtract(vEyes, vNodePos, vDiff); + if( VectorLength(vDiff) > SHOWNODES_DIST ) + continue; + + tTrace = gi.trace (vEyes, vec3_origin, vec3_origin, + vNodePos, ent, MASK_OPAQUE); + if (tTrace.fraction < 0.9) + continue; + // Ok, can see the node + // But is it in the viewport? + AngleVectors (ent->s.angles, vForward, NULL, NULL); + //VectorNormalize (forward); + VectorAdd (vForward, vEyes, vForward); + VectorSubtract (vNodePos, vEyes, vLos); + VectorNormalize (vLos); + VectorAdd (vLos, vEyes, vLos); + VectorSubtract (vLos, vForward, vOffset); + fFudge = VectorLength (vOffset); + if ((1 - (fFudge * fFudge / 2)) > COS90) + { + BOTUT_NodeMarker(i); + } + } +} + +//---------------------------------------------- +// BOTUT_Cmd_Say_f +//---------------------------------------------- + +void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg) +{ + int j, /*i,*/ offset_of_text; + edict_t *other; + char text[2048]; + //gclient_t *cl; + + + if (!teamplay->value) + return; + + if (ent->client->resp.team == NOTEAM) + return; + + Com_sprintf (text, sizeof(text), "%s%s: ", + (teamplay->value && (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD)) ? "[DEAD] " : "", + ent->client->pers.netname); + + offset_of_text = strlen(text); //FB 5/31/99 + + strcat (text, pMsg); + + // don't let text be too long for malicious reasons + // ...doubled this limit for Axshun -FB + if (strlen(text) > 300) + text[300] = 0; + + if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) + ParseSayText(ent, text + offset_of_text, strlen(text)); //FB 5/31/99 - offset change + // this will parse the % variables, + // and again check 300 limit afterwards -FB + // (although it checks it without the name in front, oh well) + + strcat(text, "\n"); + +/* + if (flood_msgs->value) + { + cl = ent->client; + + if (level.time < cl->flood_locktill) + return; + + i = cl->flood_whenhead - flood_msgs->value + 1; + if (i < 0) + i = (sizeof(cl->flood_when)/sizeof(cl->flood_when[0])) + i; + if (cl->flood_when[i] && level.time - cl->flood_when[i] < flood_persecond->value) + { + cl->flood_locktill = level.time + flood_waitdelay->value; + return; + } + cl->flood_whenhead = (cl->flood_whenhead + 1) % (sizeof(cl->flood_when)/sizeof(cl->flood_when[0])); + cl->flood_when[cl->flood_whenhead] = level.time; + } +*/ + + if (dedicated->value) + safe_cprintf(NULL, PRINT_CHAT, "%s", text); + + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + if (!OnSameTeam(ent, other)) + continue; + if (teamplay->value && team_round_going) + { + if ((ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + && (other->solid != SOLID_NOT && other->deadflag != DEAD_DEAD)) + continue; + } + + safe_cprintf(other, PRINT_CHAT, "%s", text); + } +} + +//---------------------------------------------- +// BOTUT_MakeTargetVector +// +// Creates a fake jump vector for the bot to aim at +//---------------------------------------------- +void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest) +{ + vec3_t vDir, vAngle, vTemp; + + VectorCopy (bot->s.origin, vTemp); + vTemp[2]+=22; // create a point in line with bot's eyes + VectorClear(vAngle); + vAngle[1] = angles[1]; + + AngleVectors(vAngle, vDir, NULL, NULL);// Forward vector + + VectorMA(vTemp, 64, vDir, vDest); +} diff --git a/src/action/acesrc/bot_weapon.c b/src/action/acesrc/bot_weapon.c index d51232ed4..c29d49d5e 100644 --- a/src/action/acesrc/bot_weapon.c +++ b/src/action/acesrc/bot_weapon.c @@ -1,230 +1,230 @@ -//--------------------------------------------- -// bot_weapon.c -// -// Copyright (c) 2000 Connor Caple all rights reserved -// -// This file contains the bot weapon handling code -//--------------------------------------------- -/* - * $Log:$ - * - */ -#include "../g_local.h" - -/* FOR REFERENCE ONLY -#define MK23_NUM 0 -#define MP5_NUM 1 -#define M4_NUM 2 -#define M3_NUM 3 -#define HC_NUM 4 -#define SNIPER_NUM 5 -#define DUAL_NUM 6 -#define KNIFE_NUM 7 -#define GRENADE_NUM 8 -*/ - -//----------------------------------------------- -// BOTWP_ChangeMK23Mode -//--------------------------------------------- - -int BOTWP_ChangeMK23Mode(edict_t *bot) -{ - if ( bot->client->curr_weap != MK23_NUM ) - return INVALID; // (-1) - // Change the weapon mode - Cmd_Weapon_f(bot); - return (BOTWP_GetMK23Mode(bot)); -} - -//----------------------------------------------- -// BOTWP_ChangeSniperMode -// -// SNIPER_1X 0 SNIPER_2X 1 -// SNIPER_4X 2 SNIPER_6X 3 -//--------------------------------------------- - -int BOTWP_ChangeSniperMode(edict_t *bot) -{ - int iLastMode; - - iLastMode = BOTWP_GetSniperMode(bot); - if ( bot->client->curr_weap != SNIPER_NUM ) - return INVALID; // (-1) - // Don't change modes too quickly - //if ( (bot->fLastZoomTime + 0.3) < level.time) - if( level.framenum >= bot->fLastZoomTime + 0.3 * HZ ) - { - // Change the weapon mode - Cmd_Weapon_f(bot); - if( iLastMode != BOTWP_GetSniperMode(bot)) - bot->fLastZoomTime = level.framenum; - } - return (BOTWP_GetSniperMode(bot)); -} - -//--------------------------------------------- -// BOTWP_GetMK23Mode -//--------------------------------------------- - -int BOTWP_GetMK23Mode(edict_t *bot) -{ - return (bot->client->pers.mk23_mode ); -} - -//--------------------------------------------- -// BOTWP_GetSniperMode -//--------------------------------------------- - -int BOTWP_GetSniperMode(edict_t *bot) -{ - return (bot->client->resp.sniper_mode ); -} - -//--------------------------------------------- -// BOTWP_RemoveSniperZoomMode -//--------------------------------------------- -void BOTWP_RemoveSniperZoomMode(edict_t *bot) -{ - if ( bot->client->curr_weap != SNIPER_NUM ) - return; - if( BOTWP_GetSniperMode(bot) != 0 ) - { - BOTWP_ChangeSniperMode(bot); - } -} - -//--------------------------------------------- -// BOTWP_ChooseWeapon -// -// Choose the best weapon for bot -//--------------------------------------------- - -qboolean BOTWP_ChooseWeapon(edict_t *bot) -{ - float fRange; - vec3_t vTemp; - - // if no enemy, then what are we doing here? - if(!bot->enemy) - return (true); -//AQ2 CHANGE - // Currently always favor the dual pistols! - //@@ This will become the "bot choice" weapon -// if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) -// return; -//AQ2 END - - // Base selection on distance. - VectorSubtract (bot->s.origin, bot->enemy->s.origin, vTemp); - fRange = VectorLength(vTemp); - -/* // Longer range - if(fRange > 1000) - { - if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) - return (true); - - if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) - return (true); - } - - // Longer range - if(fRange > 700) - { - if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) - return (true); - } - - // Longer range - if(fRange > 500) - { - if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) - return (true); - }*/ - - // Longer range - if(fRange > 250) - { - if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) - return (true); - - if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) - return (true); - } - - // Short range - if(ACEIT_ChangeHCSpecialWeapon(bot,FindItem(HC_NAME))) - return (true); - - if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) - return (true); - - if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) - return (true); - - if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) - return (true); - - if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) - return (true); - - if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) - return (true); - - if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) - return (true); - - if(ACEIT_ChangeWeapon(bot,FindItem(KNIFE_NAME))) - return (true); - - // We have no weapon available for use. - if(debug_mode) - gi.bprintf(PRINT_HIGH,"%s: No weapon available...\n",bot->client->pers.netname); - return (false); - -} +//--------------------------------------------- +// bot_weapon.c +// +// Copyright (c) 2000 Connor Caple all rights reserved +// +// This file contains the bot weapon handling code +//--------------------------------------------- +/* + * $Log:$ + * + */ +#include "../g_local.h" + +/* FOR REFERENCE ONLY +#define MK23_NUM 0 +#define MP5_NUM 1 +#define M4_NUM 2 +#define M3_NUM 3 +#define HC_NUM 4 +#define SNIPER_NUM 5 +#define DUAL_NUM 6 +#define KNIFE_NUM 7 +#define GRENADE_NUM 8 +*/ + +//----------------------------------------------- +// BOTWP_ChangeMK23Mode +//--------------------------------------------- + +int BOTWP_ChangeMK23Mode(edict_t *bot) +{ + if ( bot->client->curr_weap != MK23_NUM ) + return INVALID; // (-1) + // Change the weapon mode + Cmd_Weapon_f(bot); + return (BOTWP_GetMK23Mode(bot)); +} + +//----------------------------------------------- +// BOTWP_ChangeSniperMode +// +// SNIPER_1X 0 SNIPER_2X 1 +// SNIPER_4X 2 SNIPER_6X 3 +//--------------------------------------------- + +int BOTWP_ChangeSniperMode(edict_t *bot) +{ + int iLastMode; + + iLastMode = BOTWP_GetSniperMode(bot); + if ( bot->client->curr_weap != SNIPER_NUM ) + return INVALID; // (-1) + // Don't change modes too quickly + //if ( (bot->fLastZoomTime + 0.3) < level.time) + if( level.framenum >= bot->fLastZoomTime + 0.3 * HZ ) + { + // Change the weapon mode + Cmd_Weapon_f(bot); + if( iLastMode != BOTWP_GetSniperMode(bot)) + bot->fLastZoomTime = level.framenum; + } + return (BOTWP_GetSniperMode(bot)); +} + +//--------------------------------------------- +// BOTWP_GetMK23Mode +//--------------------------------------------- + +int BOTWP_GetMK23Mode(edict_t *bot) +{ + return (bot->client->pers.mk23_mode ); +} + +//--------------------------------------------- +// BOTWP_GetSniperMode +//--------------------------------------------- + +int BOTWP_GetSniperMode(edict_t *bot) +{ + return (bot->client->resp.sniper_mode ); +} + +//--------------------------------------------- +// BOTWP_RemoveSniperZoomMode +//--------------------------------------------- +void BOTWP_RemoveSniperZoomMode(edict_t *bot) +{ + if ( bot->client->curr_weap != SNIPER_NUM ) + return; + if( BOTWP_GetSniperMode(bot) != 0 ) + { + BOTWP_ChangeSniperMode(bot); + } +} + +//--------------------------------------------- +// BOTWP_ChooseWeapon +// +// Choose the best weapon for bot +//--------------------------------------------- + +qboolean BOTWP_ChooseWeapon(edict_t *bot) +{ + float fRange; + vec3_t vTemp; + + // if no enemy, then what are we doing here? + if(!bot->enemy) + return (true); +//AQ2 CHANGE + // Currently always favor the dual pistols! + //@@ This will become the "bot choice" weapon +// if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) +// return; +//AQ2 END + + // Base selection on distance. + VectorSubtract (bot->s.origin, bot->enemy->s.origin, vTemp); + fRange = VectorLength(vTemp); + +/* // Longer range + if(fRange > 1000) + { + if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) + return (true); + + if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) + return (true); + } + + // Longer range + if(fRange > 700) + { + if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) + return (true); + } + + // Longer range + if(fRange > 500) + { + if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) + return (true); + }*/ + + // Longer range + if(fRange > 250) + { + if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) + return (true); + + if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) + return (true); + } + + // Short range + if(ACEIT_ChangeHCSpecialWeapon(bot,FindItem(HC_NAME))) + return (true); + + if(ACEIT_ChangeM3SpecialWeapon(bot,FindItem(M3_NAME))) + return (true); + + if(ACEIT_ChangeM4SpecialWeapon(bot,FindItem(M4_NAME))) + return (true); + + if(ACEIT_ChangeMP5SpecialWeapon(bot,FindItem(MP5_NAME))) + return (true); + + if(ACEIT_ChangeDualSpecialWeapon(bot,FindItem(DUAL_NAME))) + return (true); + + if(ACEIT_ChangeMK23SpecialWeapon(bot,FindItem(MK23_NAME))) + return (true); + + if(ACEIT_ChangeSniperSpecialWeapon(bot,FindItem(SNIPER_NAME))) + return (true); + + if(ACEIT_ChangeWeapon(bot,FindItem(KNIFE_NAME))) + return (true); + + // We have no weapon available for use. + if(debug_mode) + gi.bprintf(PRINT_HIGH,"%s: No weapon available...\n",bot->client->pers.netname); + return (false); + +} diff --git a/src/action/acesrc/botchat.c b/src/action/acesrc/botchat.c index ec5919b25..45ba2a1c9 100644 --- a/src/action/acesrc/botchat.c +++ b/src/action/acesrc/botchat.c @@ -1,193 +1,150 @@ -//----------------------------------------------------------------------------- -// -// $Header:$ - -/* - * $History:$ - * - */ -/* - botchat.c -*/ - -#include "../g_local.h" - -#include "botchat.h" - - -/* - * In each of the following strings: - * - the first %s is the attacker/enemy which the string is - * directed to - */ - -#define DBC_WELCOMES 13 -char *ltk_welcomes[DBC_WELCOMES] = -{ - "Greetings all!", - "Hello %s! Prepare to die!!", - "%s? Who the hell is that?", - "I say, %s, have you got a license for that face?", - "%s, how do you see where you're going with those b*ll*cks in your eyes?", - "Give your arse a rest, %s. try talking through your mouth", - "Damn! I hoped Thresh would be here...", - "Hey Mom, they gave me a gun!", - "Hey, any of you guys played before?", - "Hi %s, I wondered if you'd show your face around here again.", - "Nice :-) :-) :-)", - "OK %s, let's get this over with" - "SUG BALLE SUG BALLE SUG BALLE" -}; - -#define DBC_KILLEDS 13 -char *ltk_killeds[DBC_KILLEDS] = -{ - "B*stard! %s messed up my hair.", - "All right, %s. Now I'm feeling put out!", - "Why you little!", - "Ooooh, %s, that smarts!", - "%s's mother cooks socks in hell!", - "Hey, %s, how about a match - your face and my arse!", - "Was %s talking to me, or chewing a brick?", - "Aw, %s doesn't like me...", - "It's clobberin' time, %s", - "Hey, I was tying my shoelace!", - "Oh - now I know how strawberry jam feels...", - "laaaaaaaaaaaaaaag!", - "One feels like chicken tonight...." -}; - -#define DBC_INSULTS 16 -char *ltk_insults[DBC_INSULTS] = -{ - "Hey, %s. Your mother was a hamster...!", - "%s; Eat my dust!", - "Hahaha! Hook, Line and Sinker, %s!", - "I'm sorry, %s, did I break your concentration?", - "Unlike certain other bots, %s, I can kill with an English accent..", - "Get used to disappointment, %s", - "You couldn't organise a p*ss-up in a brewery, %s", - "%s, does your mother know you're out?", - "Hey, %s, one guy wins, the other prick loses...", - "Oh %s, ever thought of taking up croquet instead?", - "Yuck! I've got some %s on my shoe!", - "Mmmmm... %s chunks!", - "Hey everyone, %s was better than the Pirates of Penzance", - "Oh - good play %s ... hehehe", - "Errm, %s, have you ever thought of taking up croquet instead?", - "Ooooooh - I'm sooooo scared %s" -}; - - - -#define AQTION_KILLDS 8 -char *aqtion_killeds[AQTION_KILLDS] = -{ - "SUG BALLE SUG BALLE" - "hahahaha", - "lol", - "Try using a real gun, %s!", - "rofl", - "you are getting better, I think", - "nice shot", - "scared me half to death, %s" -}; - -#define AQTION_INSULTS 8 -char *aqtion_insults[AQTION_INSULTS] = -{ - "Check out the Discord server! --> https://discord.aq2world.com <--", - "Have you visited the forums recently? --> https://forums.aq2world.com <--", - "no achievement for u!", - "%s is probably still using a ball mouse lol", - "well that was unforunate", - "gottem", - "peek a boooo!", - "meh!" -}; - - -void LTK_Chat (edict_t *bot, edict_t *object, int speech) -{ - char final[150]; - char *text = NULL; - - if ((!object) || (!object->client)) { - return; - } - - if(!ltk_classic->value){ - if (speech == DBC_WELCOME) { - text = ltk_welcomes[rand()%DBC_WELCOMES]; - } else if (speech == DBC_KILLED) { - text = ltk_killeds[rand()%DBC_KILLEDS]; - } else if (speech == DBC_INSULT) { - text = ltk_insults[rand()%DBC_INSULTS]; - } else { - if( debug_mode ) - gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); - return; - } - } else { - if (speech == DBC_WELCOME) - return; - else if (speech == DBC_KILLED) - text = aqtion_killeds[rand()%AQTION_KILLDS]; - else if (speech == DBC_INSULT) - text = aqtion_insults[rand()%AQTION_INSULTS]; - else - { - if( debug_mode ) - gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); - return; - } - } - - sprintf (final, text, object->client->pers.netname); - - LTK_Say (bot, final); -} - - -/* -================== -Bot_Say -================== -*/ -void LTK_Say (edict_t *ent, char *what) -{ - int j; - edict_t *other; - char text[2048]; - - Q_snprintf (text, sizeof(text), "%s: ", ent->client->pers.netname); - - if (*what == '"') - { - what++; - what[strlen(what)-1] = 0; - } - strcat(text, what); - - // don't let text be too long for malicious reasons - if (strlen(text) > 200) - text[200] = '\0'; - - strcat(text, "\n"); - - if (dedicated->value) - gi.cprintf(NULL, PRINT_CHAT, "%s", text); - - for (j = 1; j <= game.maxclients; j++) - { - other = &g_edicts[j]; - if (!other->inuse) - continue; - if (!other->client) - continue; - if (Q_stricmp(other->classname, "bot") == 0) - continue; - gi.cprintf(other, PRINT_CHAT, "%s", text); - } -} - +//----------------------------------------------------------------------------- +// +// $Header:$ + +/* + * $History:$ + * + */ +/* + botchat.c +*/ + +#include "../g_local.h" + +#include "botchat.h" + + +/* + * In each of the following strings: + * - the first %s is the attacker/enemy which the string is + * directed to + */ + +#define DBC_WELCOMES 13 +char *ltk_welcomes[DBC_WELCOMES] = +{ + "Greetings all!", + "Hello %s! Prepare to die!!", + "%s? Who the hell is that?", + "I say, %s, have you got a license for that face?", + "%s, how do you see where you're going with those b*ll*cks in your eyes?", + "Give your arse a rest, %s. try talking through your mouth", + "Damn! I hoped Thresh would be here...", + "Hey Mom, they gave me a gun!", + "I gotta find a better server...", + "Hey, any of you guys played before?", + "Hi %s, I wondered if you'd show your face around here again.", + "Nice :-) :-) :-)", + "OK %s, let's get this over with" +}; + +#define DBC_KILLEDS 13 +char *ltk_killeds[DBC_KILLEDS] = +{ + "B*stard! %s messed up my hair.", + "All right, %s. Now I'm feeling put out!", + "Hey! Go easy on me! I'm a newbie!", + "Ooooh, %s, that smarts!", + "%s's mother cooks socks in hell!", + "Hey, %s, how about a match - your face and my arse!", + "Was %s talking to me, or chewing a brick?", + "Aw, %s doesn't like me...", + "It's clobberin' time, %s", + "Hey, I was tying my shoelace!", + "Oh - now I know how strawberry jam feels...", + "laaaaaaaaaaaaaaag!", + "One feels like chicken tonight...." +}; + +#define DBC_INSULTS 16 +char *ltk_insults[DBC_INSULTS] = +{ + "Hey, %s. Your mother was a hamster...!", + "%s; Eat my dust!", + "Hahaha! Hook, Line and Sinker, %s!", + "I'm sorry, %s, did I break your concentration?", + "Unlike certain other bots, %s, I can kill with an English accent..", + "Get used to disappointment, %s", + "You couldn't organise a p*ss-up in a brewery, %s", + "%s, does your mother know you're out?", + "Hey, %s, one guy wins, the other prick loses...", + "Oh %s, ever thought of taking up croquet instead?", + "Yuck! I've got some %s on my shoe!", + "Mmmmm... %s chunks!", + "Hey everyone, %s was better than the Pirates of Penzance", + "Oh - good play %s ... hehehe", + "Errm, %s, have you ever thought of taking up croquet instead?", + "Ooooooh - I'm sooooo scared %s" +}; + + +void LTK_Chat (edict_t *bot, edict_t *object, int speech) +{ + char final[150]; + char *text = NULL; + + if ((!object) || (!object->client)) + return; + + if (speech == DBC_WELCOME) + text = ltk_welcomes[rand()%DBC_WELCOMES]; + else if (speech == DBC_KILLED) + text = ltk_killeds[rand()%DBC_KILLEDS]; + else if (speech == DBC_INSULT) + text = ltk_insults[rand()%DBC_INSULTS]; + else + { + if( debug_mode ) + gi.bprintf (PRINT_HIGH, "LTK_Chat: Unknown speech type attempted!(out of range)"); + return; + } + + sprintf (final, text, object->client->pers.netname); + + LTK_Say (bot, final); +} + + +/* +================== +Bot_Say +================== +*/ +void LTK_Say (edict_t *ent, char *what) +{ + int j; + edict_t *other; + char text[2048]; + + Com_sprintf (text, sizeof(text), "%s: ", ent->client->pers.netname); + + if (*what == '"') + { + what++; + what[strlen(what)-1] = 0; + } + strcat(text, what); + + // don't let text be too long for malicious reasons + if (strlen(text) > 200) + text[200] = '\0'; + + strcat(text, "\n"); + + if (dedicated->value) + gi.cprintf(NULL, PRINT_CHAT, "%s", text); + + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + if (Q_stricmp(other->classname, "bot") == 0) + continue; + gi.cprintf(other, PRINT_CHAT, "%s", text); + } +} + diff --git a/src/action/acesrc/botchat.h b/src/action/acesrc/botchat.h index f750e9f87..34a4f2334 100644 --- a/src/action/acesrc/botchat.h +++ b/src/action/acesrc/botchat.h @@ -1,40 +1,40 @@ -//----------------------------------------------------------------------------- -// -// $Logfile: /LicenseToKill/src/acesrc/botchat.h $ - -// $Revision: 3 $ -// $Author: Riever $ -// $Date: 28/09/99 7:30 $ -/* - * $Log: /LicenseToKill/src/acesrc/botchat.h $ - * - * 3 28/09/99 7:30 Riever - * Altered function names for LTK - * - * 2 28/09/99 7:10 Riever - * Modified for LTK - * - * 1 28/09/99 6:57 Riever - * Initial import of chat files - */ -// -/* - botchat.h -*/ - -#ifndef __BOTCHAT_H__ -#define __BOTCHAT_H__ - -void LTK_Chat (edict_t *bot, edict_t *object, int speech); -void LTK_Say (edict_t *ent, char *what); - -/* - * NOTE: DBC = DroneBot Chat - */ - -#define DBC_WELCOME 0 // When a bot is created -#define DBC_KILLED 1 // When a bot is killed ;-) -#define DBC_INSULT 2 // When a bot kills someone (player/bot) - - -#endif // __DB_CHAT_H__ +//----------------------------------------------------------------------------- +// +// $Logfile: /LicenseToKill/src/acesrc/botchat.h $ + +// $Revision: 3 $ +// $Author: Riever $ +// $Date: 28/09/99 7:30 $ +/* + * $Log: /LicenseToKill/src/acesrc/botchat.h $ + * + * 3 28/09/99 7:30 Riever + * Altered function names for LTK + * + * 2 28/09/99 7:10 Riever + * Modified for LTK + * + * 1 28/09/99 6:57 Riever + * Initial import of chat files + */ +// +/* + botchat.h +*/ + +#ifndef __BOTCHAT_H__ +#define __BOTCHAT_H__ + +void LTK_Chat (edict_t *bot, edict_t *object, int speech); +void LTK_Say (edict_t *ent, char *what); + +/* + * NOTE: DBC = DroneBot Chat + */ + +#define DBC_WELCOME 0 // When a bot is created +#define DBC_KILLED 1 // When a bot is killed ;-) +#define DBC_INSULT 2 // When a bot kills someone (player/bot) + + +#endif // __DB_CHAT_H__ diff --git a/src/action/acesrc/botnav.c b/src/action/acesrc/botnav.c index f03d1dbeb..369cba1d7 100644 --- a/src/action/acesrc/botnav.c +++ b/src/action/acesrc/botnav.c @@ -1,452 +1,513 @@ -//----------------------------------------------------------------------------- -// -// $Header:$ -// -// Copyright (C) 2000 by Connor "RiEvEr" Caple -// All rights reserved. -// -// This file contains the searchpath algorithm files for use by the bots -// The code in this file, or variants of it, may be used in any non-commercial -// FPS mod as long as you credit me as the author. -// -// Commercial permission can be obtained from me via my current e-mail -// address. (connor@botgod.org.uk as of February 2000) -// -//----------------------------------------------------------------------------- - -/* - * $History:$ - * - */ - -#include "../g_local.h" - -#include "botnav.h" - -//== GLOBAL SEMAPHORE == -int antSearch; - -// The nodes array -extern node_t nodes[MAX_NODES]; -extern short path_table[MAX_NODES][MAX_NODES]; // Quick pathsearch array [from][to] - -qboolean nodeused[MAX_NODES]; // This is used for a FAST check if the node has been used -short int nodefrom[MAX_NODES]; // Stores how we got here once the node is closed - -/* ========================================================= -The basic system works by using a single linked list and accessing information from the node array - -1) The current node is found -2) All links from it are propogated - if not already done by another node -3) If we haven't found the target node then we get the next open node and go to 1 -4) If we have the target node we return the path to it -5) If we run out of nodes then we return INVALID - -This idea is based on "Path Finding Via 'Ant Races' (a floodfill algorithm)" -by Richard Wesson. It is easy to optimise this code to make it even more efficient. - -============================================================= */ - - -//========================= -// Init the search path variables -//========================= -void AntInitSearch(edict_t *ent) -{ - //Make sure the lists and arrays used are all set to the correct values and EMPTY! - memset(nodeused, 0, sizeof(nodeused) ); - memset(nodefrom, INVALID, sizeof(nodefrom) ); - while( !SLLempty(&ent->pathList) ) - { - SLLpop_front(&ent->pathList); - } -} - -//========================= -// StartSearch -//========================= -// -// returns true if a path is found -// false otherwise -// -qboolean AntStartSearch(edict_t *ent, int from, int to ) -{ - // Safety first! - if( from==INVALID || to==INVALID) - return false; - - //@@ TESTING ONLY!! antSearch always available - antSearch = 1; - // Check we're allowed to search - if so, do it - if(1)// (antSearch > 0) && (ent->antLastCallTime < level.framenum - ANT_FREQ) ) - { - // Decrement the semaphore to limit calls to this function - //@@ If we ever get multithreading then we can increment later - antSearch--; - // make a note of when this bot last made a path call - ent->antLastCallTime = level.framenum; - // Set up the lists - AntInitSearch(ent); - // If we found a path - if( AntFindPath( ent, from, to) ) - { - // pathList now contains the links in reverse order - return true; - } - } - // We can use the quick node search method here to get our path and put it in the pathList - // the same way we do with the AntSearch mode. This will have the side effect of finding - // bad paths and removing them. - if( AntQuickPath(ent, from, to) ) - { - return true; - } - // If not allowed to search and no path - AntInitSearch(ent); // Clear out the path storage - return false; -} - -//================================= -// QuickPath -//================================= -// -// Uses the old path array to get a quick answer and removes bad paths -// - -qboolean AntQuickPath(edict_t *ent, int from, int to) -{ - int newNode = from; - int oldNode = 0; - - // Clean out the arrays, etc. - AntInitSearch(ent); - nodeused[from] = true; - // Check we can get from->to and that the path is complete - while( newNode != INVALID ) - { - oldNode = newNode; - // get next node - newNode = path_table[newNode][to]; - if( newNode == to ) - { - // We're there - store it then build the path - nodeused[newNode] = true; - nodefrom[newNode] = oldNode; - break; - } - else if( newNode == INVALID ) - { - // We have a bad path - break; - } - else if( !nodeused[newNode] ) - { - // Not been here yet - store it! - nodeused[newNode] = true; - nodefrom[newNode] = oldNode; - } - else - break; // LOOP encountered - } - - // If successful, build the pathList - if( newNode == to ) - { - SLLpush_front(&ent->pathList, to ); - while( newNode != from) - { - // Push the - SLLpush_front(&ent->pathList, nodefrom[newNode]); - newNode = nodefrom[newNode]; - } - return true; - } - // else wipe out the bad path! - else - { - newNode = oldNode; - while( newNode != from) - { - path_table[ nodefrom[newNode] ][ to ] = INVALID; - } - path_table[ from ][ to ] = INVALID; - } - return false; -} - -//======================= -// FindPath -//======================= -// -// Uses OPEN and CLOSED lists to conduct a search -// Many refinements planned -// -qboolean AntFindPath( edict_t *ent, int from, int to) -{ - int counter = 0; - int newNode = INVALID; // Stores the node being tested - node_t *tempNode = NULL; // Pointer to a real NODE - int workingNode,atNode; // Structures for search - - // Locally declared OPEN list - ltklist_t openList; - openList.head = openList.tail = NULL; // MUST do this!! - - // Safety first again - we don't want crashes! - if( from==INVALID || to==INVALID ) - return false; - - // Put startnode on the OPEN list - atNode = from; - nodefrom[atNode] = INVALID; - SLLpush_back(&openList, from ); - nodeused[from] = true; - - // While there are nodes on the OPEN list AND we are not at destNode - while( !SLLempty(&openList) && newNode != to ) - { - counter = 0; - - // Where we are - atNode = SLLfront(&openList); - - // Safety check - if( atNode <= INVALID ) - return false; - - // Get a pointer to all the node information - tempNode = &nodes[atNode]; - // Using an array for FAST access to the path ratrher than a CLOSED list - newNode = tempNode->links[counter].targetNode; - - // Process this node putting linked nodes on the OPEN list - while( newNode != INVALID) - { - // If newNode NOT on open or closed list - if( !nodeused[newNode]) - { - // Mark node as used - nodeused[newNode] = true; - // Set up working node for storage on OPEN list - workingNode = newNode; - nodefrom[newNode] = atNode; - // Store it - SLLpush_back(&openList, workingNode ); - } - // If node being linked is destNode then quit - if( newNode == to) - { - break; - } - // Check we aren't trying to read out of range - if( ++counter >= MAXLINKS ) - break; - else - newNode = tempNode->links[counter].targetNode; - } - - // ... and remove atNode from the OPEN List - SLLpop_front(&openList); - } // END While - - // Free up the memory we allocated - SLLdelete (&openList); - - // Optimise stored path with this new information - if( newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order - // so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode ); - // Set the to path in node array because this is shortest path - path_table[ nodefrom[to] ][ to ] = to; - // We earlier set our start node to INVALID to set up the termination - while( - (newNode=nodefrom[newNode])!=INVALID // there is a path and - && (newNode != from) // it's not the node we're standing on (safety check) - ) - { - // Push it onto the pathlist - SLLpush_front(&ent->pathList, newNode ); - // Set the path in the node array to match this shortest path - path_table[ nodefrom[newNode] ][ to ] = newNode; - } - return true; - } - // else - return false; -} - -//============================= -// LinkExists -//============================= -// -// Used to check we haven't wandered off path! -// -qboolean AntLinkExists( int from, int to) -{ - int counter =0; - int testnode; - node_t *tempNode = &nodes[from]; - - if( from==INVALID || to==INVALID ) - return false; - // Check if the link exists - while( counter < MAXLINKS) - { - testnode = tempNode->links[counter].targetNode; - if( testnode == to) - { - // A path exists from,to - return true; - } - else if( testnode == INVALID ) - { - // No more links and no path found - return false; - } - counter++; - } - // Didn't find it! - return false; -} - -// ****************************************************** // -//======================================================= -// SLL functions used by search code -//======================================================= - -//============================== -// SLLpush_front -//============================== -// Add to the front of the list -// -void SLLpush_front( ltklist_t *thelist, int nodedata ) -{ - slint_t *temp; - - // Store the current head pointer - temp = thelist->head; - // allocate memory for the new data (LEVEL tagged) - thelist->head = gi.TagMalloc( sizeof(slint_t), TAG_LEVEL); - // Set up the data and pointer - thelist->head->nodedata = nodedata; - thelist->head->next = temp; - // Check if there;'s a next item - if( !thelist->head->next) - { - // Set the tail pointer = head - thelist->tail = thelist->head; - } -} - -//============================== -// SLLpop_front -//============================== -// Remove the iten from the front of the list -// -void SLLpop_front( ltklist_t *thelist ) -{ - slint_t *temp; - - // Store the head pointer - temp = thelist->head; - // Check if there's a next item - if( thelist && thelist->head ) - { - if( thelist->head == thelist->tail ) - { - // List is now emptying - thelist->tail = thelist->head = NULL; - } - else - { - // Move head to point to next item - thelist->head = thelist->head->next; - } - // Free the memory (LEVEL tagged) - gi.TagFree( temp ); - } - else - { - gi.bprintf( PRINT_HIGH, "Attempting to POP an empty list!\n"); - } -} - -//============================== -// SLLfront -//============================== -// Get the integer value from the front of the list -// without removing the item (Query the list) -// -int SLLfront( ltklist_t *thelist ) -{ - if( thelist && !SLLempty( thelist) ) - return( thelist->head->nodedata); - else - return INVALID; -} - -//============================== -// SLLpush_front -//============================== -// Add to the back of the list -// -void SLLpush_back( ltklist_t *thelist, int nodedata ) -{ - slint_t *temp; - - // Allocate memory for the new item (LEVEL tagged) - temp = (slint_t *)gi.TagMalloc( sizeof(slint_t), TAG_LEVEL); - // Store the data - temp->nodedata = nodedata; - temp->next = NULL; // End of the list - // Store the new item in the list - // Is the list empty? - if( !thelist->head ) - { - // Yes - add as a new item - thelist->head = temp; - thelist->tail = temp; - } - else - { - // No make this the new tail item - thelist->tail->next = temp; - thelist->tail = temp; - } -} - -//============================== -// SLLempty -//============================== -// See if the list is empty (false if not empty) -// -qboolean SLLempty( ltklist_t *thelist ) -{ - // If there is any item in the list then it is NOT empty... - // If there is a list - if(thelist) - return (thelist->head == NULL); - else // No list so return empty - return (true); -} - -//=============================== -// Delete the list -//=============================== -// Avoids memory leaks -// -void SLLdelete( ltklist_t *thelist ) -{ - slint_t *temp; - - while( !SLLempty( thelist )) - { - temp = thelist->head; - thelist->head = thelist->head->next; - gi.TagFree( temp ); - } -} - - +//----------------------------------------------------------------------------- +// +// $Header:$ +// +// Copyright (C) 2000 by Connor "RiEvEr" Caple +// All rights reserved. +// +// This file contains the searchpath algorithm files for use by the bots +// The code in this file, or variants of it, may be used in any non-commercial +// FPS mod as long as you credit me as the author. +// +// Commercial permission can be obtained from me via my current e-mail +// address. (connor@botgod.org.uk as of February 2000) +// +//----------------------------------------------------------------------------- + +/* + * $History:$ + * + */ +/* +#include "../g_local.h" + +#include "botnav.h" + +//== GLOBAL SEMAPHORE == +int antSearch; + +// The nodes array +//rekkie -- DEV_1 -- s +#include "acebot.h" +//extern node_t *nodes; +//extern node_t nodes[MAX_PNODES]; +extern short int **path_table; +//short int path_table[MAX_PNODES][MAX_PNODES]; +//rekkie -- DEV_1 -- e +//extern node_t nodes[MAX_NODES]; +//extern short path_table[MAX_NODES][MAX_NODES]; // Quick pathsearch array [from][to] + +qboolean nodeused[MAX_PNODES]; // This is used for a FAST check if the node has been used +short int nodefrom[MAX_PNODES]; // Stores how we got here once the node is closed +*/ +/* ========================================================= +The basic system works by using a single linked list and accessing information from the node array + +1) The current node is found +2) All links from it are propogated - if not already done by another node +3) If we haven't found the target node then we get the next open node and go to 1 +4) If we have the target node we return the path to it +5) If we run out of nodes then we return INVALID + +This idea is based on "Path Finding Via 'Ant Races' (a floodfill algorithm)" +by Richard Wesson. It is easy to optimise this code to make it even more efficient. + +============================================================= */ + +/* +//========================= +// Init the search path variables +//========================= +void AntInitSearch(edict_t *ent) +{ + //Make sure the lists and arrays used are all set to the correct values and EMPTY! + memset(nodeused, 0, sizeof(nodeused) ); + memset(nodefrom, INVALID, sizeof(nodefrom) ); + while( !SLLempty(&ent->pathList) ) + { + SLLpop_front(&ent->pathList); + } + + //Botlib_FreeNextNodes(ent); //rekkie -- free next node memory +} + +//========================= +// StartSearch +//========================= +// +// returns true if a path is found +// false otherwise +// +qboolean AntStartSearch(edict_t *ent, int from, int to ) +{ + // Safety first! + if( from==INVALID || to==INVALID) + return false; + + //@@ TESTING ONLY!! antSearch always available + antSearch = 1; + // Check we're allowed to search - if so, do it + if(1)// (antSearch > 0) && (ent->antLastCallTime < level.framenum - ANT_FREQ) ) + { + // Decrement the semaphore to limit calls to this function + //@@ If we ever get multithreading then we can increment later + antSearch--; + // make a note of when this bot last made a path call + ent->antLastCallTime = level.framenum; + // Set up the lists + AntInitSearch(ent); + // If we found a path + if( AntFindPath( ent, from, to) ) + { + // pathList now contains the links in reverse order + return true; + } + } + // We can use the quick node search method here to get our path and put it in the pathList + // the same way we do with the AntSearch mode. This will have the side effect of finding + // bad paths and removing them. + if( AntQuickPath(ent, from, to) ) + { + return true; + } + // If not allowed to search and no path + AntInitSearch(ent); // Clear out the path storage + return false; +} + +//================================= +// QuickPath +//================================= +// +// Uses the old path array to get a quick answer and removes bad paths +// + +qboolean AntQuickPath(edict_t *ent, int from, int to) +{ + int newNode = from; + int oldNode = 0; + int loopProtection = 0; //rekkie -- DEV_1 + + // Clean out the arrays, etc. + AntInitSearch(ent); + nodeused[from] = true; + // Check we can get from->to and that the path is complete + while( newNode != INVALID ) + { + oldNode = newNode; + // get next node + newNode = path_table[newNode][to]; + if( newNode == to ) + { + // We're there - store it then build the path + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + break; + } + else if( newNode == INVALID ) + { + // We have a bad path + break; + } + else if( !nodeused[newNode] ) + { + // Not been here yet - store it! + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + } + else + break; // LOOP encountered + } + + // If successful, build the pathList + if( newNode == to ) + { + SLLpush_front(&ent->pathList, to ); + while( newNode != from) + { + //rekkie -- s + if (newNode >= numnodes || newNode < 0) + { + if (debug_mode) + gi.dprintf("%s newNode out of bounds!\n", __func__); + return false; + } + //rekkie -- e + + // Push the + SLLpush_front(&ent->pathList, nodefrom[newNode]); + newNode = nodefrom[newNode]; + } + return true; + } + // else wipe out the bad path! + else + { + newNode = oldNode; + while( newNode != from) + { + path_table[ nodefrom[newNode] ][ to ] = INVALID; + + //rekkie -- DEV_1 -- s + if (loopProtection < numnodes) + loopProtection++; + else + { + if (debug_mode) + debug_printf("%s ------------------- Bad while loop. Forcing exit.\n", __func__); + break; + } + //rekkie -- DEV_1 -- e + } + path_table[ from ][ to ] = INVALID; + } + return false; +} + +//======================= +// FindPath +//======================= +// +// Uses OPEN and CLOSED lists to conduct a search +// Many refinements planned +// +qboolean AntFindPath( edict_t *ent, int from, int to) +{ + int counter = 0; + int newNode = INVALID; // Stores the node being tested + //node_t *tempNode = NULL; // Pointer to a real NODE + //int workingNode,atNode; // Structures for search + int atNode; // Structures for search + + // Locally declared OPEN list + ltklist_t openList; + openList.head = openList.tail = NULL; // MUST do this!! + + // Safety first again - we don't want crashes! + if( from==INVALID || to==INVALID ) + return false; + + // Put startnode on the OPEN list + atNode = from; + nodefrom[atNode] = INVALID; + SLLpush_back(&openList, from ); + nodeused[from] = true; + + ////Com_Printf("%s NEW PATH [%d to %d]\n", __func__, from, to); + + // While there are nodes on the OPEN list AND we are not at destNode + while( !SLLempty(&openList) && newNode != to ) + { + counter = 0; + + // Where we are + atNode = SLLfront(&openList); + + ////Com_Printf("%s atNode [%d]\n", __func__, atNode); + + // Safety check + if( atNode <= INVALID ) + return false; + + // Get a pointer to all the node information + //tempNode = &nodes[atNode]; + // Using an array for FAST access to the path ratrher than a CLOSED list + //newNode = tempNode->links[counter].targetNode; + newNode = nodes[atNode].links[counter].targetNode; + + // Process this node putting linked nodes on the OPEN list + while( newNode != INVALID) + { + ////Com_Printf("%s newNode [%d]\n", __func__, newNode); + + // If newNode NOT on open or closed list + if( !nodeused[newNode]) + { + // Mark node as used + nodeused[newNode] = true; + // Set up working node for storage on OPEN list + //workingNode = newNode; + nodefrom[newNode] = atNode; + // Store it + //SLLpush_back(&openList, workingNode ); + SLLpush_back(&openList, newNode); + } + // If node being linked is destNode then quit + if( newNode == to) + { + break; + } + // Check we aren't trying to read out of range + if( ++counter >= MAXLINKS ) + break; + else + //newNode = tempNode->links[counter].targetNode; + newNode = nodes[atNode].links[counter].targetNode; + } + + // ... and remove atNode from the OPEN List + SLLpop_front(&openList); + } // END While + + // Free up the memory we allocated + SLLdelete (&openList); + + ////Com_Printf("%s PATH newNode [%d] to [%d]\n", __func__, newNode, to); + + // Optimise stored path with this new information + if( newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order + // so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode ); + + //rekkie -- DEV_1 -- s + // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID + // Check if INVALID to avoid accessing [-1] array + if (nodefrom[to] != INVALID) + //rekkie -- DEV_1 -- e + // Set the to path in node array because this is shortest path + path_table[ nodefrom[to] ][ to ] = to; + + ////Com_Printf("%s path_table[ nodefrom[%d] ][ %d ] = %d\n\n", __func__, to, to, to); + ////int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + while( + (newNode=nodefrom[newNode])!=INVALID // there is a path and + && (newNode != from) // it's not the node we're standing on (safety check) + ) + { + ////Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + ////prev_newNode = newNode; + + // Push it onto the pathlist + SLLpush_front(&ent->pathList, newNode ); + // Set the path in the node array to match this shortest path + path_table[ nodefrom[newNode] ][ to ] = newNode; + + ////Com_Printf("%s path_table[ nodefrom[%d] ][ %d ] = %d\n", __func__, newNode, to, newNode); + } + + ////Com_Printf("%s EXIT PATH\n\n", __func__); + + return true; + } + // else + return false; +} + +//============================= +// LinkExists +//============================= +// +// Used to check we haven't wandered off path! +// +qboolean AntLinkExists( int from, int to) +{ + int counter =0; + int testnode; + //node_t *tempNode = &nodes[from]; + + if( from==INVALID || to==INVALID ) + return false; + // Check if the link exists + while( counter < MAXLINKS) + { + //testnode = tempNode->links[counter].targetNode; + testnode = nodes[from].links[counter].targetNode; + if( testnode == to) + { + // A path exists from,to + return true; + } + else if( testnode == INVALID ) + { + // No more links and no path found + return false; + } + counter++; + } + // Didn't find it! + return false; +} +*/ + +// ****************************************************** // +//======================================================= +// SLL functions used by search code +//======================================================= +/* +//============================== +// SLLpush_front +//============================== +// Add to the front of the list +// +void SLLpush_front( ltklist_t *thelist, int nodedata ) +{ + slint_t *temp; + + // Store the current head pointer + temp = thelist->head; + // allocate memory for the new data (LEVEL tagged) + thelist->head = gi.TagMalloc( sizeof(slint_t), TAG_LEVEL); + // Set up the data and pointer + thelist->head->node = nodedata; + thelist->head->next = temp; + // Check if there;'s a next item + if( !thelist->head->next) + { + // Set the tail pointer = head + thelist->tail = thelist->head; + } +} + +//============================== +// SLLpop_front +//============================== +// Remove the iten from the front of the list +// +void SLLpop_front( ltklist_t *thelist ) +{ + slint_t *temp; + + // Store the head pointer + temp = thelist->head; + // Check if there's a next item + if( thelist && thelist->head ) + { + if( thelist->head == thelist->tail ) + { + // List is now emptying + thelist->tail = thelist->head = NULL; + } + else + { + // Move head to point to next item + thelist->head = thelist->head->next; + } + // Free the memory (LEVEL tagged) + gi.TagFree( temp ); + } + else + { + gi.bprintf( PRINT_HIGH, "Attempting to POP an empty list!\n"); + } +} + +//============================== +// SLLfront +//============================== +// Get the integer value from the front of the list +// without removing the item (Query the list) +// +int SLLfront( ltklist_t *thelist ) +{ + if( thelist && !SLLempty( thelist) ) + return( thelist->head->node); + else + return INVALID; +} + +//============================== +// SLLpush_front +//============================== +// Add to the back of the list +// +void SLLpush_back( ltklist_t *thelist, int nodedata ) +{ + slint_t *temp; + + // Allocate memory for the new item (LEVEL tagged) + temp = (slint_t *)gi.TagMalloc( sizeof(slint_t), TAG_LEVEL); + // Store the data + temp->node = nodedata; + temp->next = NULL; // End of the list + // Store the new item in the list + // Is the list empty? + if( !thelist->head ) + { + // Yes - add as a new item + thelist->head = temp; + thelist->tail = temp; + } + else + { + // No make this the new tail item + thelist->tail->next = temp; + thelist->tail = temp; + } +} + +//============================== +// SLLempty +//============================== +// See if the list is empty (false if not empty) +// +qboolean SLLempty( ltklist_t *thelist ) +{ + // If there is any item in the list then it is NOT empty... + // If there is a list + if(thelist) + return (thelist->head == NULL); + else // No list so return empty + return (true); +} + +//=============================== +// Delete the list +//=============================== +// Avoids memory leaks +// +void SLLdelete( ltklist_t *thelist ) +{ + slint_t *temp; + + while( !SLLempty( thelist )) + { + temp = thelist->head; + thelist->head = thelist->head->next; + gi.TagFree( temp ); + } +} +*/ \ No newline at end of file diff --git a/src/action/acesrc/botnav.h b/src/action/acesrc/botnav.h index 31dc047ba..1092b7e1d 100644 --- a/src/action/acesrc/botnav.h +++ b/src/action/acesrc/botnav.h @@ -1,81 +1,86 @@ -#ifndef BOTNAV_H -#define BOTNAV_H -//----------------------------------------------------------------------------- -// -// $Logfile:: /LicenseToKill/src/acesrc/botnav.h $ -// $Revision:: 6 $ -// $Author:: Riever $ -// $Date:: 1/10/99 18:20 $ -// -// Copyright (C) 1999 by Connor Caple -// All rights reserved. -//----------------------------------------------------------------------------- -/* - * $Log: /LicenseToKill/src/acesrc/botnav.h $ - * - * 6 1/10/99 18:20 Riever - * Reduced intensive searches to once every 0.5 secs. - * - * 5 27/09/99 14:33 Riever - * Added SLLdelete function to free up memory and help prevent any leaks. - * There may still be a necessity to free up bot pathLists at respawn - - * awaiting views from those who know how to spot memory leaks! - * - * 4 25/09/99 11:20 Riever - * Tidied up errors in declarations - * - * 3 25/09/99 9:36 Riever - * Added all SLL definitions - * - * 2 25/09/99 8:24 Riever - * - */ - -#define ANT_FREQ 0.5 // Time gap between calls to the processor intensive search - - // ----------- new Pathing Algorithm stuff ----- - qboolean AntPathMove( edict_t *ent ); // Called in item and enemy route functions - void AntInitSearch( edict_t *ent ); // Resets all the path lists etc. - qboolean AntStartSearch( edict_t *ent, int from, int to); // main entry to path algorithms - qboolean AntQuickPath( edict_t *ent, int from, int to ); // backup path system - qboolean AntFindPath( edict_t *ent, int from, int to); // Optimised path system - qboolean AntLinkExists( int from, int to); // Detects if we are off the path - -// --------- AI Tactics Values ------------- -enum{ - AIRoam, // Basic item collection AI - AIAttack, // Basic Attack Enemy AI - AIAttackCollect,// Attack Enemy while collecting Item - AICamp, // Camp at a suitable location and collect the item on respawn - AISnipe, - AIAmbush -}; - -// ** Single Linked List (SLL) implementation ** - -// ** slint_t is a list member ie: one item on the list -typedef struct slint{ - struct slint *next; // pointer to the next list member - int nodedata; // The node number we're storing -} slint_t; - -// ** ltklist_t is the actual list and contains a number of slint_t members -// It is a double-ended singly linked list (ie, we can add things to the front or back) -// We need head and tail pointers to speed up push_back operations -typedef struct{ - slint_t *head; // Front of the list - slint_t *tail; // Back of the list -} ltklist_t; - -// Now we have to define the operations that can happen on the list -// All will be prefixed with "SLL" so we know what they are working on when we read the code - -void SLLpush_front( ltklist_t *thelist, int nodedata );// Add to the front of the list -void SLLpop_front( ltklist_t *thelist ); // Remove the iten from the front of the list -int SLLfront( ltklist_t *thelist ); // Get the integer value from the front of the list.. - // ..without removing the item (Query the list) -void SLLpush_back( ltklist_t *thelist, int nodedata ); // Add to the back of the list -qboolean SLLempty( ltklist_t *thelist ); // See if the list is empty (false if not empty) -void SLLdelete( ltklist_t *thelist ); // Free all memory from a list - -#endif +#ifndef BOTNAV_H +#define BOTNAV_H +//----------------------------------------------------------------------------- +// +// $Logfile:: /LicenseToKill/src/acesrc/botnav.h $ +// $Revision:: 6 $ +// $Author:: Riever $ +// $Date:: 1/10/99 18:20 $ +// +// Copyright (C) 1999 by Connor Caple +// All rights reserved. +//----------------------------------------------------------------------------- +/* + * $Log: /LicenseToKill/src/acesrc/botnav.h $ + * + * 6 1/10/99 18:20 Riever + * Reduced intensive searches to once every 0.5 secs. + * + * 5 27/09/99 14:33 Riever + * Added SLLdelete function to free up memory and help prevent any leaks. + * There may still be a necessity to free up bot pathLists at respawn - + * awaiting views from those who know how to spot memory leaks! + * + * 4 25/09/99 11:20 Riever + * Tidied up errors in declarations + * + * 3 25/09/99 9:36 Riever + * Added all SLL definitions + * + * 2 25/09/99 8:24 Riever + * + */ +/* +#define ANT_FREQ 0.5 // Time gap between calls to the processor intensive search + + // ----------- new Pathing Algorithm stuff ----- + qboolean AntPathMove( edict_t *ent ); // Called in item and enemy route functions + void AntInitSearch( edict_t *ent ); // Resets all the path lists etc. + qboolean AntStartSearch( edict_t *ent, int from, int to); // main entry to path algorithms + qboolean AntQuickPath( edict_t *ent, int from, int to ); // backup path system + qboolean AntFindPath( edict_t *ent, int from, int to); // Optimised path system + qboolean AntLinkExists( int from, int to); // Detects if we are off the path + +// --------- AI Tactics Values ------------- +enum{ + AIRoam, // Basic item collection AI + AIAttack, // Basic Attack Enemy AI + AIAttackCollect,// Attack Enemy while collecting Item + AICamp, // Camp at a suitable location and collect the item on respawn + AISnipe, + AIAmbush +}; +*/ + +/* +// ** Single Linked List (SLL) implementation ** + +// ** slint_t is a list member ie: one item on the list +typedef struct slint{ + struct slint *next; // pointer to the next list member + int node; // The node number we're storing -=- nodedata + float cost; +} slint_t; + +// ** ltklist_t is the actual list and contains a number of slint_t members +// It is a double-ended singly linked list (ie, we can add things to the front or back) +// We need head and tail pointers to speed up push_back operations +typedef struct{ + slint_t *head; // Front of the list + slint_t *tail; // Back of the list +} ltklist_t; +*/ + +/* +// Now we have to define the operations that can happen on the list +// All will be prefixed with "SLL" so we know what they are working on when we read the code + +void SLLpush_front( ltklist_t *thelist, int nodedata );// Add to the front of the list +void SLLpop_front( ltklist_t *thelist ); // Remove the iten from the front of the list +int SLLfront( ltklist_t *thelist ); // Get the integer value from the front of the list.. + // ..without removing the item (Query the list) +void SLLpush_back( ltklist_t *thelist, int nodedata ); // Add to the back of the list +qboolean SLLempty( ltklist_t *thelist ); // See if the list is empty (false if not empty) +void SLLdelete( ltklist_t *thelist ); // Free all memory from a list +*/ +#endif diff --git a/src/action/acesrc/botscan.c b/src/action/acesrc/botscan.c index 196087bf6..c513d2f32 100644 --- a/src/action/acesrc/botscan.c +++ b/src/action/acesrc/botscan.c @@ -1,290 +1,290 @@ -/* - * $Header:$ - * - * $History:$ - * - */ -//================================================================= -// botscan.c -// -// Connor Caple 14th October 1999 -// -// A lexical scanner module to allow much more complex configuration files -// Original code idea : Lee & Mark Atkinson, "Using C", pub. Que -// -// Sample driver included at bottom of file. -// -//================================================================= - -#include "botscan.h" -#include -#include - -//============================== -// nmtoken -//============================== -// This is only used in testing. It returns a string that matches -// the type of token returned in the scanner -// Very useful when debugging new configuration files -// -// I may write a stand alone config file tester to ensure that -// people use the instructions correctly. -// -char *nmtoken( int ttype) -{ - static char *tokenNames[] = { - "LEXERR", - "SYMBOL", - "INTLIT", - "REALLIT", - "STRLIT", - "LPAREN", - "RPAREN", - "SEMIC", - "COLON", - "COMMA", - "PERIOD", - "APOST", - "PLUSOP", - "MINUSOP", - "MUXOP", - "DIVOP", - "POWOP", - "ASSIGNOP", - "HASH", - "BANG", - "EOL", - "UNDEF" - }; - return( tokenNames[ttype] ); -} - -//================================= -// scanner -//================================= -// This is the actual scanner. It searches for tokens it can -// recognise and returns them in a string with the -// tokentype defined in an integer. -// -// text is the address of the string being worked on -// token is the returned value of the token -// ttype is the integer value of the token type -// - -void scanner( char **text, char *token, int *ttype) -{ - // Skip all whitespace - for ( ; **text == ' ' || **text == '\t' || **text == '\n' || **text == '\r'; (*text)++ ); - - // If the string terminates return EOL - if( **text == '\0' ) - { - *ttype = EOL; - return; - } - - // SYMBOLS - if( (**text >='A' && **text <='Z') || (**text >='a' && **text <='z') ) - { - *ttype = SYMBOL; - - while( - (**text >='A' && **text <='Z') || (**text >='a' && **text <='z') - || (**text >='0' && **text <='9') - ) - { - *token++ = *(*text)++; - } - *token = '\0'; // Terminate the string. - return; - } - - // STRING LITERALS - if( **text == '"' ) - { - *ttype = STRLIT; - (*text)++; // Skip first quote. - while( **text != '"' && **text ) - { - *token++ = *(*text)++; - } - (*text)++; // Skip last quote. - *token = '\0'; // Terminate the string. - return; - } - - // NUMERICS - if( **text >= '0' && **text <= '9' ) - { - *ttype = INTLIT; - while( **text >= '0' && **text <= '9' ) - { - *token++ = *(*text)++; - if( **text == '.' ) - { - *ttype = REALLIT; - *token++ = *(*text)++; - } - // I left out the 'e' notation part - we don't need it. - } - *token = '\0'; // Terminate the string. - return; - } - - // PUNCTUATION SECTION - if( **text == '(' ) - { - *ttype = LPAREN; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == ')' ) - { - *ttype = RPAREN; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == ';' ) - { - *ttype = SEMIC; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == ':' ) - { - *ttype = COLON; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == ',' ) - { - *ttype = COMMA; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '.' ) - { - *ttype = PERIOD; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '\'' ) - { - *ttype = APOST; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '+' ) - { - *ttype = PLUSOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '-' ) - { - *ttype = MINUSOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '*' ) - { - *ttype = MUXOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '/' ) - { - *ttype = DIVOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '^' ) - { - *ttype = POWOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '=' ) - { - *ttype = ASSIGNOP; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '#' ) - { - *ttype = HASH; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - if( **text == '!' ) - { - *ttype = BANG; - *token++ = *(*text)++; - *token = '\0'; - return; - } - - - // Nothing matched - it must be an error - *ttype = LEXERR; - return; -} - -/* -// ************************************************************************* -// Sample driver to test this file with -// - -void parseString( char *test ) -{ - char *sp, *tp; - int ttype; - char token[81]; - - sp = test; // Set up a pointer to the string; - ttype = UNDEF; // Signal "no match found yet" - - // Now scan the string and report each token type and value found - while( ttype != EOL && ttype != LEXERR ) - { - // Set tp to point to our return string location - tp = token; - // Pass in the correct values to scanner() - scanner( &sp, tp, &ttype ); - // If we are not done, print what we found. - if( ttype != EOL ) - { - printf( "Token type = %s, token = %s\n", nmtoken(ttype), token ); - } - } -} -// ************************************************************************** -*/ +/* + * $Header:$ + * + * $History:$ + * + */ +//================================================================= +// botscan.c +// +// Connor Caple 14th October 1999 +// +// A lexical scanner module to allow much more complex configuration files +// Original code idea : Lee & Mark Atkinson, "Using C", pub. Que +// +// Sample driver included at bottom of file. +// +//================================================================= + +#include "botscan.h" +#include +#include + +//============================== +// nmtoken +//============================== +// This is only used in testing. It returns a string that matches +// the type of token returned in the scanner +// Very useful when debugging new configuration files +// +// I may write a stand alone config file tester to ensure that +// people use the instructions correctly. +// +char *nmtoken( int ttype) +{ + static char *tokenNames[] = { + "LEXERR", + "SYMBOL", + "INTLIT", + "REALLIT", + "STRLIT", + "LPAREN", + "RPAREN", + "SEMIC", + "COLON", + "COMMA", + "PERIOD", + "APOST", + "PLUSOP", + "MINUSOP", + "MUXOP", + "DIVOP", + "POWOP", + "ASSIGNOP", + "HASH", + "BANG", + "EOL", + "UNDEF" + }; + return( tokenNames[ttype] ); +} + +//================================= +// scanner +//================================= +// This is the actual scanner. It searches for tokens it can +// recognise and returns them in a string with the +// tokentype defined in an integer. +// +// text is the address of the string being worked on +// token is the returned value of the token +// ttype is the integer value of the token type +// + +void scanner( char **text, char *token, int *ttype) +{ + // Skip all whitespace + for ( ; **text == ' ' || **text == '\t' || **text == '\n' || **text == '\r'; (*text)++ ); + + // If the string terminates return EOL + if( **text == '\0' ) + { + *ttype = EOL; + return; + } + + // SYMBOLS + if( (**text >='A' && **text <='Z') || (**text >='a' && **text <='z') ) + { + *ttype = SYMBOL; + + while( + (**text >='A' && **text <='Z') || (**text >='a' && **text <='z') + || (**text >='0' && **text <='9') + ) + { + *token++ = *(*text)++; + } + *token = '\0'; // Terminate the string. + return; + } + + // STRING LITERALS + if( **text == '"' ) + { + *ttype = STRLIT; + (*text)++; // Skip first quote. + while( **text != '"' && **text ) + { + *token++ = *(*text)++; + } + (*text)++; // Skip last quote. + *token = '\0'; // Terminate the string. + return; + } + + // NUMERICS + if( **text >= '0' && **text <= '9' ) + { + *ttype = INTLIT; + while( **text >= '0' && **text <= '9' ) + { + *token++ = *(*text)++; + if( **text == '.' ) + { + *ttype = REALLIT; + *token++ = *(*text)++; + } + // I left out the 'e' notation part - we don't need it. + } + *token = '\0'; // Terminate the string. + return; + } + + // PUNCTUATION SECTION + if( **text == '(' ) + { + *ttype = LPAREN; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == ')' ) + { + *ttype = RPAREN; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == ';' ) + { + *ttype = SEMIC; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == ':' ) + { + *ttype = COLON; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == ',' ) + { + *ttype = COMMA; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '.' ) + { + *ttype = PERIOD; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '\'' ) + { + *ttype = APOST; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '+' ) + { + *ttype = PLUSOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '-' ) + { + *ttype = MINUSOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '*' ) + { + *ttype = MUXOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '/' ) + { + *ttype = DIVOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '^' ) + { + *ttype = POWOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '=' ) + { + *ttype = ASSIGNOP; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '#' ) + { + *ttype = HASH; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + if( **text == '!' ) + { + *ttype = BANG; + *token++ = *(*text)++; + *token = '\0'; + return; + } + + + // Nothing matched - it must be an error + *ttype = LEXERR; + return; +} + +/* +// ************************************************************************* +// Sample driver to test this file with +// + +void parseString( char *test ) +{ + char *sp, *tp; + int ttype; + char token[81]; + + sp = test; // Set up a pointer to the string; + ttype = UNDEF; // Signal "no match found yet" + + // Now scan the string and report each token type and value found + while( ttype != EOL && ttype != LEXERR ) + { + // Set tp to point to our return string location + tp = token; + // Pass in the correct values to scanner() + scanner( &sp, tp, &ttype ); + // If we are not done, print what we found. + if( ttype != EOL ) + { + printf( "Token type = %s, token = %s\n", nmtoken(ttype), token ); + } + } +} +// ************************************************************************** +*/ diff --git a/src/action/acesrc/botscan.h b/src/action/acesrc/botscan.h index 023ffb4d2..14693211c 100644 --- a/src/action/acesrc/botscan.h +++ b/src/action/acesrc/botscan.h @@ -1,53 +1,53 @@ -/* - * $Header: /LicenseToKill/src/acesrc/botscan.h 1 16/10/99 8:41 Riever $ - * - * $Log: /LicenseToKill/src/acesrc/botscan.h $ - * - * 1 16/10/99 8:41 Riever - * Initial import to LTK - * - * 1 14/10/99 8:26 Riever - * - * 3 14/10/99 6:53 Riever - * Changed file name to botscan.h to make it clear where it belongs in the - * project. - * - * 2 14/10/99 6:51 Riever - * First version. - * - */ -//================================================================= -// botscan.h -// -// Connor Caple 14th October 1999 -// -// A lexical scanner module to allow much more complex configuration files -// Original code idea : Lee & Mark Atkinson, "Using C", pub. Que -// -//================================================================= - -#define LEXERR 0 -#define SYMBOL 1 -#define INTLIT 2 -#define REALLIT 3 -#define STRLIT 4 -#define LPAREN 5 -#define RPAREN 6 -#define SEMIC 7 -#define COLON 8 -#define COMMA 9 -#define PERIOD 10 -#define APOST 11 -#define PLUSOP 12 -#define MINUSOP 13 -#define MUXOP 14 -#define DIVOP 15 -#define POWOP 16 -#define ASSIGNOP 17 -#define HASH 18 -#define BANG 19 -#define EOL 20 -#define UNDEF 255 - -void scanner( char **text, char *token, int *ttype); -char *nmtoken( int ttype ); +/* + * $Header: /LicenseToKill/src/acesrc/botscan.h 1 16/10/99 8:41 Riever $ + * + * $Log: /LicenseToKill/src/acesrc/botscan.h $ + * + * 1 16/10/99 8:41 Riever + * Initial import to LTK + * + * 1 14/10/99 8:26 Riever + * + * 3 14/10/99 6:53 Riever + * Changed file name to botscan.h to make it clear where it belongs in the + * project. + * + * 2 14/10/99 6:51 Riever + * First version. + * + */ +//================================================================= +// botscan.h +// +// Connor Caple 14th October 1999 +// +// A lexical scanner module to allow much more complex configuration files +// Original code idea : Lee & Mark Atkinson, "Using C", pub. Que +// +//================================================================= + +#define LEXERR 0 +#define SYMBOL 1 +#define INTLIT 2 +#define REALLIT 3 +#define STRLIT 4 +#define LPAREN 5 +#define RPAREN 6 +#define SEMIC 7 +#define COLON 8 +#define COMMA 9 +#define PERIOD 10 +#define APOST 11 +#define PLUSOP 12 +#define MINUSOP 13 +#define MUXOP 14 +#define DIVOP 15 +#define POWOP 16 +#define ASSIGNOP 17 +#define HASH 18 +#define BANG 19 +#define EOL 20 +#define UNDEF 255 + +void scanner( char **text, char *token, int *ttype); +char *nmtoken( int ttype ); diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h new file mode 100644 index 000000000..82431e4d3 --- /dev/null +++ b/src/action/botlib/botlib.h @@ -0,0 +1,465 @@ +#ifndef _BOTLIB_H +#define _BOTLIB_H + +#define BOT_AAS_VERSION 1 + +#define BOT_NAV_VERSION 2 +#define BOT_NAV_VERSION_1 1 +#define BOT_NAV_VERSION_2 2 +#define BOT_NAV_VERSION_MAX BOT_NAV_VERSION_2 + +// Bot move states +#define BOT_MOVE_STATE_NONE 0 // Bot is booting up :-) +#define BOT_MOVE_STATE_NAV 1 // Getting a navigational path +//#define BOT_MOVE_STATE_NAV_NEXT 2 // Getting next navigational path (if any) +#define BOT_MOVE_STATE_MOVE 3 // Standard movement +#define BOT_MOVE_STATE_WANDER 4 // No navigation and no movement, try wandering around +#define BOT_MOVE_STATE_STAND 5 // Stand still and hold a position +#define BOT_MOVE_STATE_FLEE 6 // Running away from enemy force +#define BOT_MOVE_STATE_COVER 7 // Under fire, take cover + + +typedef struct bot_connections_s +{ + qboolean tried_adding_prev_bots; // Flag if we tried to add any bots from a previous map. Only called once per map. + qboolean auto_balance_bots; // If the spawn manager should try to auto balance bots on teams + qboolean scale_up; // If the manager should scale up the bots + qboolean scale_dn; // If the manager should scale down the bots + + int total_bots; + int total_humans; // Total humans in server + int total_humans_playing; // Total humans in server and playing + + int total_team1; // Bots & Humans + int total_team2; + int total_team3; + + int spec_bots; // TP and DM + int team1_bots; // TP and DM + int team2_bots; // TP and 3T + int team3_bots; // TP and 3T + + + int spec_humans; // TP and DM + int team1_humans; // TP and DM + int team2_humans; // TP and 3T + int team3_humans; // TP and 3T + + int desire_bots; // How many bots we desire + int desire_team1; + int desire_team2; + int desire_team3; +} bot_connections_t; +bot_connections_t bot_connections; + +//the bot input, will be converted to a usercmd_t +typedef struct bot_input_s +{ + float thinktime; //time since last output (in seconds) + vec3_t dir; //movement direction + float speed; //speed in the range [0, 400] + vec3_t viewangles; //the view angles + int actionflags; //one of the ACTION_? flags + int radioflags; //one of the aq2 radio flags (i.e. RADIO_REPORTIN) + int weapon; //weapon to use + vec3_t look_at; //location to look at + int look_at_time; //time spent looking at location +} bot_input_t; + + + + + +// =========================================================================== +// botlib_ai.c +// =========================================================================== +void BOTLIB_Init(edict_t* self); // Initializing... HAL9000 is online. +void BOTLIB_Think(edict_t* self); // Thinking... I'm sorry Rekkie, I can't do that. +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time); // Translates bot input to actual Q2 movement calls +qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount); // I see everything... in front of me +qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount); +qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount); +qboolean BOTLIB_CanMove(edict_t* self, int direction); // Can bot move in direction +void BOTLIB_TouchingLadder(edict_t* self); +float BOTLIB_ThrowingKnifePitch(edict_t* self); // Get pitch required to reach knife throwing target +void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type); +int BOTLIB_GetEquipment(edict_t* self); // Does bot need weapons, items, ammo? If so +//void BOTLIB_CheckCurrentWeapon(edict_t* self); //rekkie -- Check to make sure we're using a primary weapon where possible +void BOTLIB_GetWeaponsAndAmmo(edict_t* self); //rekkie -- Locate and pickup a primary weapon if we need one +short BOTLIB_FindVisibleAllies(edict_t* self); +qboolean BOTLIB_FindEnemy(edict_t* self); +void BOTLIB_PickLongRangeGoal(edict_t* self); + +typedef struct botlib_noises_s +{ + edict_t* owner[MAX_CLIENTS]; // The ent who made the noise + + // Noise timers + int self_time[MAX_CLIENTS]; + int weapon_time[MAX_CLIENTS]; + int impact_time[MAX_CLIENTS]; + + // Location of noise + vec3_t self_origin[MAX_CLIENTS]; + vec3_t weapon_origin[MAX_CLIENTS]; + vec3_t impact_origin[MAX_CLIENTS]; + +} botlib_noises_t; +botlib_noises_t botlib_noises; + +// Actionable flags +#define ACTION_NONE 0x00000000 // No action taken +#define ACTION_ATTACK 0x00000001 // Tap the attack button +#define ACTION_USE 0x00000002 +#define ACTION_RESPAWN 0x00000008 +#define ACTION_JUMP 0x00000010 // Small jumps - tapping the jump button +#define ACTION_MOVEUP 0x00000020 +#define ACTION_CROUCH 0x00000080 +#define ACTION_MOVEDOWN 0x00000100 +#define ACTION_MOVEFORWARD 0x00000200 +#define ACTION_MOVEBACK 0x00000800 +#define ACTION_MOVELEFT 0x00001000 +#define ACTION_MOVERIGHT 0x00002000 +#define ACTION_BOXJUMP 0x00008000 // Higher jumps +#define ACTION_TALK 0x00010000 +#define ACTION_GESTURE 0x00020000 +#define ACTION_WALK 0x00080000 +#define ACTION_AFFIRMATIVE 0x00100000 +#define ACTION_NEGATIVE 0x00200000 +#define ACTION_GETFLAG 0x00800000 +#define ACTION_GUARDBASE 0x01000000 +#define ACTION_PATROL 0x02000000 +#define ACTION_FOLLOWME 0x08000000 +#define ACTION_HOLDJUMP 0x10000000 // Hold the jump button and release when touching ground +#define ACTION_HOLDPOS 0x20000000 // Hold position +#define ACTION_JUMPPAD 0x40000000 + + +// =========================================================================== +// botlib_ctf.c +// =========================================================================== +qboolean BOTLIB_Carrying_Flag(edict_t* self); +int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance); +qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance); +qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance); +void BOTLIB_Update_Flags_Status(void); +int BOTLIB_CTF_Get_Flag_Node(edict_t* ent); +int BOTLIB_NearestFlag(edict_t* self); +float BOTLIB_DistanceToFlag(edict_t* self, int flagType); +void BOTLIB_CTF_Goals(edict_t* self); + +typedef struct ctf_status_s +{ + edict_t* flag1; // Red flag + edict_t* flag2; // Blue flag + int flag1_home_node; // The node where the red flag initially spawns + int flag2_home_node; // The node where the blue flag initially spawns + int flag1_curr_node; // The node where the red flag currently resides + int flag2_curr_node; // The node where the blue flag currently resides + qboolean flag1_is_home; // If the red flag is home + qboolean flag2_is_home; // If the blue flag is home + qboolean flag1_is_dropped; // If the red flag was dropped + qboolean flag2_is_dropped; // If the blue flag was dropped + qboolean flag1_is_carried; // If the red flag is being carried + qboolean flag2_is_carried; // If the blue flag is being carried + edict_t* player_has_flag1; // If any ent has the red flag + edict_t* player_has_flag2; // If any ent has the blue flag + float team1_carrier_dist_to_home; // How close the red team carrier is to the home red flag node + float team2_carrier_dist_to_home; // How close the blue team carrier is to the home blue flag node +} ctf_status_t; +ctf_status_t bot_ctf_status; + +// Get flag, retrieve flag, intercept flag carrier, etc. +typedef enum +{ + BOT_CTF_STATE_NONE, // No state + BOT_CTF_STATE_GET_ENEMY_FLAG, // Get enemy flag sitting in its home location + BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG, // Get dropped enemy flag + BOT_CTF_STATE_CAPTURE_ENEMY_FLAG, // Got flag, now capture it + BOT_CTF_STATE_ATTACK_ENEMY_CARRIER, // Enemy has our flag, go after them + BOT_CTF_STATE_RETURN_TEAM_FLAG, // Our team flag is on the ground, go return it + BOT_CTF_STATE_COVER_FLAG_CARRIER, // We have the enemy flag, cover the bot who has it + BOT_CTF_STATE_FORCE_MOVE_TO_FLAG, // Special case. Bot is forced to move in direction of flag. + BOT_CTF_STATE_GET_DROPPED_ITEMS // Gets dropped weapons, items, etc from ground + +} bot_ctf_state_t; + +// =========================================================================== +// botlib_cmd.c +// =========================================================================== +qboolean BOTLIB_SV_Cmds(void); // Server commands +qboolean BOTLIB_Commands(edict_t* ent); // Client commands + + +// =========================================================================== +// botlib_communication.c +// =========================================================================== +void BOTLIB_Wave(edict_t* ent, int type); +void BOTLIB_PrecacheRadioSounds(); +void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); +void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); + +// Wave (gesture) types +#define WAVE_FLIPOFF 1 +#define WAVE_SALUTE 2 +#define WAVE_TAUNT 3 +#define WAVE_WAVE 4 +#define WAVE_POINT 5 + +// Radio flags +#define RADIO_1 0x00000001 +#define RADIO_2 0x00000002 +#define RADIO_3 0x00000003 +#define RADIO_4 0x00000004 +#define RADIO_5 0x00000005 +#define RADIO_6 0x00000006 +#define RADIO_7 0x00000007 +#define RADIO_8 0x00000008 +#define RADIO_9 0x00000009 +#define RADIO_10 0x00000010 +#define RADIO_BACK 0x00000011 +#define RADIO_COVER 0x00000012 +#define RADIO_DOWN 0x00000013 +#define RADIO_ENEMY_DOWN 0x00000014 +#define RADIO_ENEMY_SIGHTED 0x00000015 +#define RADIO_FORWARD 0x00000016 +#define RADIO_GO 0x00000017 +#define RADIO_IM_HIT 0x00000018 +#define RADIO_LEFT 0x00000019 +#define RADIO_REPORTIN 0x00000020 +#define RADIO_RIGHT 0x00000021 +#define RADIO_TAKING_FIRE 0x00000022 +#define RADIO_TEAMMATE_DOWN 0x00000023 +#define RADIO_TEAM_REPORT_IN 0x00000024 +#define RADIO_UP 0x00000025 + +// Each of the possible radio messages and their length +typedef struct bot_radio_msg_s +{ + char* msg; // the msg name + int length; // length in server frames (ie tenths of a second), rounded up + int sndIndex; +} bot_radio_msg_t; + +static bot_radio_msg_t bot_male_radio_msgs[] = { + {"1", 6, 0}, + {"2", 6, 0}, + {"3", 8, 0}, + {"4", 7, 0}, + {"5", 8, 0}, + {"6", 9, 0}, + {"7", 8, 0}, + {"8", 7, 0}, + {"9", 7, 0}, + {"10", 6, 0}, + {"back", 6, 0}, + {"cover", 7, 0}, + {"down", 13, 0}, + {"enemyd", 10, 0}, + {"enemys", 9, 0}, + {"forward", 6, 0}, + {"go", 6, 0}, + {"im_hit", 7, 0}, + {"left", 7, 0}, + {"reportin", 9, 0}, + {"right", 6, 0}, + {"taking_f", 22, 0}, + {"teamdown", 13, 0}, + {"treport", 12, 0}, + {"up", 4, 0} +}; + +static bot_radio_msg_t bot_female_radio_msgs[] = { + {"1", 5, 0}, + {"2", 5, 0}, + {"3", 5, 0}, + {"4", 5, 0}, + {"5", 5, 0}, + {"6", 8, 0}, + {"7", 7, 0}, + {"8", 5, 0}, + {"9", 5, 0}, + {"10", 5, 0}, + {"back", 6, 0}, + {"cover", 5, 0}, + {"down", 6, 0}, + {"enemyd", 9, 0}, + {"enemys", 9, 0}, + {"forward", 8, 0}, + {"go", 6, 0}, + {"im_hit", 7, 0}, + {"left", 8, 0}, + {"reportin", 9, 0}, + {"right", 5, 0}, + {"taking_f", 22, 0}, + {"teamdown", 10, 0}, + {"treport", 12, 0}, + {"up", 6, 0} +}; +static const int bot_numMaleSnds = (sizeof(bot_male_radio_msgs) / sizeof(bot_male_radio_msgs[0])); +static const int bot_numFemaleSnds = (sizeof(bot_female_radio_msgs) / sizeof(bot_female_radio_msgs[0])); + + +// =========================================================================== +// botlib_items.c +// =========================================================================== + + +// =========================================================================== +// botlib_math.c +// =========================================================================== +qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon); +bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs); +qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other); + +// =========================================================================== +// botlib_movement.c +// =========================================================================== +qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint); +void BOTLIB_Look(edict_t* self, usercmd_t* ucmd); + + +// =========================================================================== +// botlib_nav.c +// =========================================================================== +// Find all possible paths -- s + +#define MAX_NAV_AREAS 32 +#define MAX_NAV_AREAS_EDGES 64 +#define MAX_NAV_AREAS_PATHS 512 //512 +#define MAX_NAV_AREAS_NODES 4096 +int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] +int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] + + + +typedef struct { + + // Areas + int total_areas; // Total number of areas + //int area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area + int** area_nodes; + + int adjacency_matrix[MAX_NAV_AREAS][MAX_NAV_AREAS]; // Store adjacency matrix of areas + + // Depth-first Search + bool dfs_visited[MAX_NAV_AREAS]; // Visited areas + int dfs_len; // + int dfs_path[MAX_NAV_AREAS]; // Temp path + int dfs_paths[MAX_NAV_AREAS_PATHS][MAX_NAV_AREAS]; // Full path array [(512 * 32) * 4 bytes = 64k] + int dfs_path_count; // Total unique paths found + + + +} nav_area_t; +nav_area_t nav_area; + +void BOTLIB_MallocAreaNodes(); +void BOTLIB_FreeAreaNodes(); + +void BOTLIB_InitAreaNodes(void); // Init area nodes to INVALID +void BOTLIB_InitAreaConnections(); // Init and make area connections +qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization); // Supports new and old pathing methods +qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node); // Checks if possible to traverse from current node to goal_node +void BOTLIB_GetAreaPath(edict_t* self, int goal_node); // Sets a path from area-to-area +qboolean BOTLIB_GetNextAreaNode(edict_t* self); +void BOTLIB_UpdateAllAreaEdges(void); // Stores all the nodes within an area that connect to an external area (edge nodes) +int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2); // Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) +void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination); // Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm +void BOTLIB_RandomizeAreaColors(); // Randomize area colors +void BOTLIB_AutoArea(edict_t* self); +qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path); +qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization); +// Find all possible paths -- e + +// Node data for our linked list +typedef struct { + struct slint* next; // Next node + int node; // Node number + float cost; // Cost from previous node to this node + int parent_node; //rekkie -- the node we came from +} botlib_sll_nodes_t; + +// A double-ended singly linked list +// This allows data to be added to the front or back of the list. +typedef struct { + botlib_sll_nodes_t* head; // Front + botlib_sll_nodes_t* tail; // Back +} botlib_sll_t; +//} ltklist_t; + +// =========================================================================== +// botlib_nodes.c +// =========================================================================== +void BOTLIB_LinkNodesNearbyNode(edict_t* ent, int from); +qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path); + +void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node); +void BOTLIB_SetAllNodeNormals(); + +// =========================================================================== +// botlib_spawn.c +// =========================================================================== +qboolean BOTLIB_SaveBotsFromPreviousMap(void); +qboolean BOTLIB_AddBotsFromPreviousMap(float percent); +void BOTLIB_RandomizeTeamNames(edict_t* bot); // Randomize team names in teamplay +void BOTLIB_RandomizeTeamSkins(edict_t* bot); // Randomize team skins in teamplay +int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender); // Pick a random skin and return the skin's gender +edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin); // Spawn a bot +void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team); +void BOTLIB_RemoveBot(char* name); // Remove bot by name or 'ALL' to remove all bots +void BOTLIB_RemoveTeamplayBot(int team); // Remove bot from team +void BOTLIB_ChangeBotTeam(int from_team, int to_team); // Change a bot [from team] ==> [to team] +void BOTLIB_CheckBotRules(void); // Adding/Removing/Auto-balance bots + +// =========================================================================== +// botlib_spawnpoints.c +// =========================================================================== + +// Custom spawnpoints +typedef struct { + qboolean inuse; // If the spawn point is in use, or has been deleted and is now free to replace + vec3_t origin; // Spawn point location + vec3_t angles; // Spawn point direction +} dc_sp_t; +dc_sp_t* dc_sp; +int dc_sp_count; // Total spawn points +qboolean dc_sp_edit; // If the spawn points have been made visible for editing +#define DC_SP_LIMIT 255 // Maximum spawn points +#define DC_SP_VERSION 1 // Version of spawn point file +void DC_Init_Spawnpoints(void); // Initialise spawn points +void DC_Free_Spawnpoints(void); // Free the spawn points +void DC_Add_Spawnpoint(edict_t* ent); // Add a spawn point at the player location +void DC_Remove_Spawnpoint(edict_t* self); // Remove a spawn point at the player location +void DC_Get_Map_Spawnpoints(void); // Find and add all the map spawn points to the dc_sp[] array +void BOTLIB_Show_Spawnpoints(void); // Find and display (as visible ents) all spawn points (map and custom) +void DC_Save_Spawnpoints(); // Save the spawn points +void DC_Load_Spawnpoints(); // Load the spawn points + + +// =========================================================================== +// botlib_weapons.c +// =========================================================================== +int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num); +qboolean BOTLIB_ChangeSpecialWeapon(edict_t* ent, int item_num); +void BOTLIB_ReadyWeapon(edict_t* self); +qboolean BOTLIB_ChooseWeapon(edict_t* self); +void BOTLIB_Reload(edict_t* self); +qboolean BOTLIB_SniperZoom(edict_t* self); +//rekkie -- collecting weapons, items, ammo -- s +qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum); +qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon); +qboolean BOTLIB_Need_Grenades(edict_t* self); +qboolean BOTLIB_Need_Knives(edict_t* self); +qboolean BOTLIB_Need_Dual_MK23(edict_t* self); +int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon); +int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon); +int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter); +int BOTLIB_GetEquipment(edict_t* self); +//rekkie -- collecting weapons, items, ammo -- e + + + + + +#endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c new file mode 100644 index 000000000..95cb09d6d --- /dev/null +++ b/src/action/botlib/botlib_ai.c @@ -0,0 +1,1217 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +void BOTLIB_Init(edict_t* self) +{ + self->classname = "bot"; + self->suicide_timeout = level.framenum + 15.0 * HZ; + + self->nav = NULL; //rekkie -- surface data + + self->enemy = NULL; + self->movetarget = NULL; + self->last_jumppad_node = INVALID; + self->bot_strafe = 0; + self->show_node_links = INVALID; + self->show_node_links_time = 0; + + //RiEvEr - new node pathing system + memset(&(self->pathList), 0, sizeof(self->pathList)); + self->pathList.head = self->pathList.tail = NULL; + //R + + self->client->resp.radio.gender = (self->client->pers.gender == GENDER_FEMALE) ? 1 : 0; + + + // Save previous data + float prev_skill = self->bot.skill; + + memset(&self->bot, 0, sizeof(bot_t)); + + // Restore previous data + self->bot.skill = prev_skill; + + // Ping + // Set the average ping this bot will see + int rng_ping_range = rand() % 5; + if (rng_ping_range == 0) + self->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard + else if (rng_ping_range <= 3) + self->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger + else + self->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard + + + + + // Enemies + self->bot.old_enemy = NULL; + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.enemy_seen_time = 0; + self->bot.enemy_chase_time = 0; + self->bot.reaction_time = bot_reaction->value; + + //memset(&self->bot.noises, 0, sizeof(int) * MAX_CLIENTS); + //memset(&self->bot.noise_time, 0, sizeof(int) * MAX_CLIENTS); + + // Nav + self->bot.state = BOT_MOVE_STATE_NONE; // Init bot navigation + self->bot.prev_node = INVALID; + self->bot.current_node = INVALID; + self->bot.next_node = INVALID; + self->bot.goal_node = INVALID; + self->bot.stuck_node = INVALID; + self->bot.walknode.touched_node = INVALID; + self->bot.walknode.highlighted_node = INVALID; + self->bot.walknode.prev_highlighted_node = INVALID; + + + // Weapons + self->bot.last_sniper_zoom_time = 0; + self->bot.last_weapon_change_time = 0; + + + if (teamplay->value) // Reset bot radio at the start of each round + { + // Radio + BOTLIB_PrecacheRadioSounds(); + self->bot.radioTeamReportedIn = false; + self->bot.radioLastSawAllyTime = 0; + self->bot.radioTeamReportedInDelay = 75 + (rand() % 101); // Add a random delay between 75 and 175 ricks + self->bot.radioReportIn = false; + self->bot.radioReportInTime = 0; + self->bot.radioReportInDelay = 30 + (rand() % 60); // Add a random delay between 30 and 90 ticks + self->bot.radioBandaging = false; + self->bot.radioLastHumanMsg[0] = '\0'; + self->bot.radioReportKills = (rand() % 2); // RNG if this bot will report its kills or not + + // Enemies + if (ff_afterround->value) + self->bot.ff_allies_after_rnd = ((rand() % 2) == 0); // 50% chance the bot will attack allies after a TP round ends + + // Update skin to match TP skins + const char* s = Info_ValueForKey(self->client->pers.userinfo, "skin"); + AssignSkin(self, s, false /* nickChanged */); + } + +} + +//rekkie -- Quake3 -- s +//============== +// BOTLIB_BotInputToUserCommand +// Translates bot input to actual Q2 movement calls +//============== +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time) +{ + vec3_t angles, forward, right; + float f, r, u, m; + + //clear the whole structure + //memset(ucmd, 0, sizeof(usercmd_t)); + + //the duration for the user command in milli seconds + //ucmd->serverTime = time; + //ucmd->weapon = bi->weapon; + + + + //set the buttons + if (bi->actionflags & ACTION_RESPAWN) ucmd->buttons = BUTTON_ATTACK; + if (bi->actionflags & ACTION_ATTACK) ucmd->buttons |= BUTTON_ATTACK; + bi->actionflags &= ~ACTION_ATTACK; + + /* + if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK; + if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE; + if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE; + if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_WALKING; + if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE; + if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE; + if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG; + if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE; + if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL; + if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME; + */ + + + + // Radio wave animations + // Enemy interaction + if (bi->radioflags & RADIO_ENEMY_DOWN) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_ENEMY_SIGHTED) BOTLIB_Wave(ent, WAVE_FLIPOFF); + // Assistance required + else if (bi->radioflags & RADIO_TAKING_FIRE) BOTLIB_Wave(ent, WAVE_FLIPOFF); + else if (bi->radioflags & RADIO_IM_HIT) BOTLIB_Wave(ent, WAVE_TAUNT); + else if (bi->radioflags & RADIO_COVER) BOTLIB_Wave(ent, WAVE_TAUNT); + // Team reporting + else if (bi->radioflags & RADIO_TEAM_REPORT_IN) BOTLIB_Wave(ent, WAVE_WAVE); + else if (bi->radioflags & RADIO_REPORTIN) BOTLIB_Wave(ent, WAVE_SALUTE); + else if (bi->radioflags & RADIO_TEAMMATE_DOWN) BOTLIB_Wave(ent, WAVE_SALUTE); + // Directions + else if (bi->radioflags & RADIO_BACK) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_FORWARD) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_UP) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_DOWN) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_LEFT) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_RIGHT) BOTLIB_Wave(ent, WAVE_POINT); + + + + + + + + ent->bot.bi.radioflags = 0; // Clear radio flags + + + // Adjust throwing pitch if using throwing knives + //if (ent->client->weapon == FindItemByNum(KNIFE_NUM) && ent->client->pers.knife_mode == 1) // Throwing knives + // bi->viewangles[PITCH] = BOTLIB_ThrowingKnifePitch(ent); + + + //set the view angles + //NOTE: the ucmd->angles are the angles WITHOUT the delta angles + ucmd->angles[PITCH] = ANGLE2SHORT(bi->viewangles[PITCH]); + ucmd->angles[YAW] = ANGLE2SHORT(bi->viewangles[YAW]); + ucmd->angles[ROLL] = ANGLE2SHORT(bi->viewangles[ROLL]); + /* + //subtract the delta angles + for (j = 0; j < 3; j++) { + temp = ucmd->angles[j] - delta_angles[j]; + ucmd->angles[j] = temp; + } + */ + + + if (bi->dir[2]) + angles[PITCH] = bi->viewangles[PITCH]; + else + angles[PITCH] = 0; + angles[YAW] = bi->viewangles[YAW]; + angles[ROLL] = 0; + + // No movement + //if (VectorEmpty(bi->dir)) return; + if (bi->actionflags & ACTION_HOLDPOS) + { + // Kill velocity + bi->actionflags = 0; + bi->speed = 0; + ent->velocity[0] *= 0.9; + ent->velocity[1] *= 0.9; + ent->velocity[2] *= 0.9; + + return; + } + else + { + //NOTE: movement is relative to the REAL view angles + //get the horizontal forward and right vector + AngleVectors(angles, forward, right, NULL); + //bot input speed is in the range [0, 400] - SPEED_RUN + //bi->speed = bi->speed * 127 / SPEED_RUN; + + // Cap max speed + if (bi->speed > SPEED_RUN) bi->speed = SPEED_RUN; + else if (bi->speed < -SPEED_RUN) bi->speed = -SPEED_RUN; + + //set the view independent movement + f = DotProduct(forward, bi->dir); + r = DotProduct(right, bi->dir); + u = fabs(forward[2]) * bi->dir[2]; + m = fabs(f); + + if (fabs(r) > m) { + m = fabs(r); + } + + if (fabs(u) > m) { + m = fabs(u); + } + + if (m > 0) { + f *= bi->speed / m; + r *= bi->speed / m; + u *= bi->speed / m; + } + + if (ent->client->leg_damage == 0) // No jumping if legs are broken + u = 0; + + ucmd->forwardmove = f; + ucmd->sidemove = r; + ucmd->upmove = u; + } + + if (bi->actionflags & ACTION_MOVEFORWARD) ucmd->forwardmove = SPEED_RUN; // 127 + if (bi->actionflags & ACTION_MOVEBACK) ucmd->forwardmove = -SPEED_RUN; // -127 + if (bi->actionflags & ACTION_MOVELEFT) ucmd->sidemove = -SPEED_RUN; // -127 + if (bi->actionflags & ACTION_MOVERIGHT) ucmd->sidemove = SPEED_RUN; // 127 + + + //jump/moveup +//if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) ucmd->upmove = SPEED_RUN; // 127 + + if ((bi->actionflags & ACTION_HOLDJUMP) && (ent->groundentity)) // Stop holding jump if on ground + { + bi->actionflags &= ~ACTION_HOLDJUMP; + } + if ((bi->actionflags & ACTION_HOLDJUMP)) // Hold jump if in air + { + if (ent->groundentity == NULL) + ucmd->upmove = SPEED_RUN; + else + bi->actionflags &= ~ACTION_HOLDJUMP; + } + if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Slow down + //ucmd->forwardmove = 0; + //ucmd->sidemove = 0; + //ucmd->upmove = 0; + + //bi->actionflags &= ~ACTION_JUMP; + //bi->actionflags |= ACTION_HOLDJUMP; + ucmd->upmove = SPEED_RUN; + if (ent->groundentity) // Give a slight boost if on ground + ent->velocity[2] += 50; + + ucmd->forwardmove = SPEED_RUN; + } + if (bi->actionflags & ACTION_BOXJUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Slow down + //ucmd->forwardmove = 0; + //ucmd->sidemove = 0; + //ucmd->upmove = 0; + + //bi->actionflags &= ~ACTION_JUMP; + //bi->actionflags |= ACTION_HOLDJUMP; + ucmd->upmove = SPEED_RUN; + if (ent->groundentity) // Give a slight boost if on ground + ent->velocity[2] += 325; // 400 + + ucmd->forwardmove = SPEED_RUN; + } + if (bi->actionflags & ACTION_JUMPPAD && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Check the bot is touching a jump node before attempting to jump + int nodelist[MAX_NODELIST]; + int nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched && ent->bot.current_node != INVALID && ent->bot.next_node != INVALID) + { + for (int i = 0; i < nodes_touched; i++) + { + //Com_Printf("%s %s touched node %d\n", __func__, ent->client->pers.netname, nodelist[i]); + //if (nodes[nodelist[i]].type & NODE_JUMPPAD) + if (nodes[nodelist[i]].nodenum == ent->bot.current_node) + { + //Com_Printf("%s %s touched jump node %d\n", __func__, ent->client->pers.netname, nodelist[i]); + // Attempt jump + //BOTLIB_Jump_Takeoff(ent, NULL, nodes[ent->bot.next_node].origin, ent->viewheight, ent->velocity); + BOTLIB_DoParabolaJump(ent, nodes[ent->bot.next_node].origin); + bi->actionflags &= ~ACTION_JUMPPAD; + break; + } + } + } + } + //crouch/movedown + if (bi->actionflags & ACTION_CROUCH) ucmd->upmove = -SPEED_RUN; // -127 + + //ladder - movedown, moveup + if (bi->actionflags & ACTION_MOVEDOWN) + { + ucmd->forwardmove = 50; // jump/move up + ucmd->upmove = -SPEED_WALK; // crouch + + // Slow turning speed + ent->velocity[0] *= 0.25; + ent->velocity[1] *= 0.25; + + ent->velocity[2] = -100; // Slow decent + + //Com_Printf("%s %s ladder down\n", __func__, ent->client->pers.netname); + + //vec3_t dist; + //VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); + //VectorNormalize(dist); + //VectorScale(dist, 20, ent->velocity); // Apply it + } + if (bi->actionflags & ACTION_MOVEUP) + { + /* + // Check we're looking in the direction of a ladder + qboolean on_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(ent->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(ent->s.origin, 16, fwd, end); + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_PLAYERSOLID); + + on_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + + if (on_ladder) + { + ent->velocity[2] += 10; + ucmd->upmove = SPEED_WALK; + Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + } + */ + + //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + + ucmd->forwardmove = SPEED_RUN; // jump/move up + ucmd->upmove = SPEED_RUN; // jump/move up + + // Slow turning speed + ent->velocity[0] *= 0.25; + ent->velocity[1] *= 0.25; + + // Go up + ent->velocity[2] = 200; // Slow upward movement + + /* + // Check if touching ladder with an overly large hitbox + if (0) + { + vec3_t min = { -32, -32, -60 }; + vec3_t max = { 32, 32, 60 }; + trace_t tr = gi.trace(ent->s.origin, min, max, ent->s.origin, ent, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) || tr.contents & CONTENTS_SOLID) + { + ent->velocity[2] = 200; // Slow upward movement + + //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + return; + } + } + */ + + //ent->movetype == MOVETYPE_FLY; + + /* + vec3_t dist; + VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); + VectorNormalize(dist); + VectorScale(dist, 20, ent->velocity); // Apply it + + for (int i = 0; i < 3; i++) + ent->velocity[i] = 200 * bi->dir[i]; + */ + } + + + // Air control + //if ((bi->actionflags & ACTION_JUMP) || (bi->actionflags & ACTION_HOLDJUMP)) + //if (ent->groundentity == NULL) + if (bi->actionflags & ACTION_JUMP) + //if (0) + { + float wishspeed, addspeed, accelspeed, currentspeed; + + wishspeed = VectorNormalize(bi->dir); + if (wishspeed > SPEED_RUN) wishspeed = SPEED_RUN; + + currentspeed = DotProduct(ent->velocity, bi->dir); + + addspeed = wishspeed - currentspeed; + addspeed *= 10.1; + if (addspeed <= 0) { + return; + } + + //accelspeed = ent->accel * FRAMETIME * wishspeed; + accelspeed = SPEED_RUN * FRAMETIME * wishspeed; + if (accelspeed > addspeed) { + accelspeed = addspeed; + } + + + /* + float z = accelspeed * bi->dir[2]; + if (z < 0) + { + // Slow down velocity + //Com_Printf("%s %s velocity %f\n", __func__, ent->client->pers.netname, z); + ent->velocity[2] += z; + } + */ + + ent->velocity[0] += accelspeed * bi->dir[0]; + ent->velocity[1] += accelspeed * bi->dir[1]; + ent->velocity[2] += accelspeed * bi->dir[2]; + + //for (i = 0; i < 3; i++) { + // ent->velocity[i] += accelspeed * bi->dir[i]; + // //ent->velocity[i] = accelspeed * bi->dir[i]; + //} + //Com_Printf("%s %s velocity[%f %f %f]\n", __func__, ent->client->pers.netname, ent->velocity[0], ent->velocity[1], ent->velocity[2]); + } + + //bi->actionflags = 0; // Clear action flags + ent->bot.bi.actionflags = 0; +} + +// Automatic adjustment of bot skills based on their current score compared to all other bots +/* +typedef struct { + int playernum; + int score; +} PlayerScores; +// Comparison function used by qsort() +int BOTLIB_ScoreCompare(const void* a, const void* b) +{ + const PlayerScores* p1 = (PlayerScores*)a; + const PlayerScores* p2 = (PlayerScores*)b; + return p2->score - p1->score; // Sort ascendingly by score +} +*/ +int BOTLIB_AutoAdjustSkill(edict_t * self) +{ + int highest_score = 0; // Highest player score + int score_diff = 0; // Difference between player and best player's score + float score_percent_diff = 1.0; // Difference in percent between player and best player's score + + //Init the variable bot skill + if (self->bot.skill < 1) + self->bot.skill = bot_skill->value; + + if (self->client == NULL) + return 0; + + // Get highest score + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i]->client == NULL) continue; + + if (players[i]->client->resp.score > highest_score) + highest_score = players[i]->client->resp.score; + } + + // Find the difference between highest score and our score (the gap between two scores) + if (self->client->resp.score < highest_score) + { + score_diff = highest_score - self->client->resp.score; // Get score gap + + if (score_diff > (int)bot_skill_threshold->value) // If the score gap is greater than threshold + { + // Get the diff in percent: 1.0 is equal, 0.5 is half the score of the best player. Higher is better. + // > 0.75 is normal + // > 0.5 is okay + // < 0.5 is poor + score_percent_diff = (float)self->client->resp.score / (float)highest_score; + + if (score_percent_diff >= 0.75) + { + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); + return 0; // Normal + } + else if (score_percent_diff >= 0.5) + { + self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + + if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill = bot_skill->value; // Scale skill back down to base level + + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + + return 1; // Okay + } + else if (score_percent_diff < 0.5) + { + self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + + if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill = bot_skill->value; // Scale skill back down to base level + + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + + return 2; // Poor + } + } + } + + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); + return 0; // Normal + + /* + PlayerScores player_scores[MAX_CLIENTS]; // All player scores + int player_id = 0; // Our player id + + // Check if players have completed at least the first round. We only want to adjust skill after players have an established score. + qboolean completed_a_round = false; + for (int i = TEAM1; i <= teamCount; i++) + { + if (teams[i].score > 0) + { + completed_a_round = true; + break; + } + } + if (completed_a_round == false) // Ignore the first round + return 0; + + // Init player scores + for (int i = 0; i <= num_players; i++) + { + if (players[i] == self) // If this is us + player_id = i; // Store our own id + + player_scores[i].playernum = i; // Store the player id + + // Store the score + if (players[i] == NULL || players[i]->client == NULL) + player_scores[i].score = 0; // Zero invalid clients + else + player_scores[i].score = players[i]->client->resp.score; // Valid client score + + if (player_scores[i].score > highest_score) + highest_score = player_scores[i].score; + } + + // Sort scores from highest to lowest + qsort(player_scores, num_players, sizeof(PlayerScores), BOTLIB_ScoreCompare); + + // Find where we rank + int rank = 0; + for (int i = 0; i <= num_players; i++) + { + // Only rank against player's with a score + //if (player_scores[i].score > 0) + rank++; + + // Break when we find ourself + if (player_id == player_scores[i].playernum) + break; + } + + float percent = ((float)rank / (float)num_players); + + //Com_Printf("%s %s rank[%d of %d] = %f score[%d]\n", __func__, self->client->pers.netname, rank, num_players, percent, self->client->resp.score); + + // If bot score is in the bottom half (50% or greater) adjust its skill so it can aim to be in the top 10% + if (percent >= 0.75) // Higher is worse + { + self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + + if (self->bot.skill > MAX_BOTSKILL) // Max skill + self->bot.skill = MAX_BOTSKILL; + + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + } + else if (percent >= 0.5) // Higher is worse + { + self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + + if (self->bot.skill > MAX_BOTSKILL) // Max skill + self->bot.skill = MAX_BOTSKILL; + + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + } + else // Return skill back to base level + { + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%d]\n", __func__, self->client->pers.netname, (int)bot_skill->value); + } + + // Return skill bracket: low, med, high. Used to recommend a weapon and item + if (percent < 0.25) + return 0; // Low + else if (percent < 0.5) + return 1; // Med + + return 2; // High + */ +} + +/////////////////////////////////////////////////////////////////////// +// Main Think function for bot +/////////////////////////////////////////////////////////////////////// +void BOTLIB_Think(edict_t* self) +{ + usercmd_t ucmd; + + //rekkie -- Fake Bot Client -- s + if (level.framenum % 10 == 0) // Update bot info every 10th frame + { + // Set self->client->ping to random bot->bot_baseline_ping, then vary by +-3 (ping jitter) + int ping_jitter = (rand() % 7); // Positive jitter + if (ping_jitter && (rand() % 1) == 0) + ping_jitter -= (ping_jitter * 2); // Negative jitter + + //Com_Printf("%s %s [%d + %d]\n", __func__, self->client->pers.netname, self->bot.bot_baseline_ping, ping_jitter); + + self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; + if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping + self->client->ping = self->bot.bot_ping; + gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); + } + //rekkie -- Fake Bot Client -- e + + // Set up client movement + VectorCopy(self->client->ps.viewangles, self->s.angles); + VectorSet(self->client->ps.pmove.delta_angles, 0, 0, 0); + memset(&ucmd, 0, sizeof(ucmd)); + + // Make bots end intermission and enter the next map (especially useful to change maps when no 'human' players are around) + if (level.intermission_framenum) + { + ucmd.buttons = BUTTON_ANY; // This button is pressed to end the intermission + goto end_think; + } + + // Stop trying to think if the bot can't respawn. + if (!IS_ALIVE(self) && ((gameSettings & GS_ROUNDBASED) || (self->client->respawn_framenum > level.framenum))) + { + goto end_think; + } + + // Force respawn + if (self->deadflag == DEAD_DEAD) + { + self->client->buttons = 0; + ucmd.buttons = BUTTON_ATTACK; + } + + // Don't execute thinking code if not alive + if (self->deadflag != DEAD_NO || self->health <= 0) + { + goto end_think; + } + + // Make the bot brain dead for a period of time + if (self->bot.pause_time) + { + self->bot.pause_time--; // Reduce timer to zero + self->bot.bi.speed = 0; + goto end_think; // Skip bot logic + } + + if (level.framenum < 75) // Wait for a little before processing AI on a new map + goto end_think; // Skip bot logic + + if (ctf->value) // CTF Goals + { + BOTLIB_CTF_Goals(self); + } + else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) + { + /* + if (rand() % 2) // Area 1 + { + BOTLIB_CanGotoNode(self, nodes[64].nodenum, 0); + } + else // Area 9 + { + BOTLIB_CanGotoNode(self, nodes[3344].nodenum, 0); + } + */ + + /* + //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (curr_node != 2505) + if (1 && rand() % 2) // Area 0 + { + if (rand() % 2) + BOTLIB_CanGotoNode(self, nodes[2505].nodenum, 0); + else + BOTLIB_CanGotoNode(self, nodes[3346].nodenum, 0); + } + else // Area 1 + { + if (rand() % 2) + BOTLIB_CanGotoNode(self, nodes[999].nodenum, 0); + else + BOTLIB_CanGotoNode(self, nodes[38].nodenum, 0); + } + */ + + //else // DM and TP goals + { + //self->bot.pause_time = 100; + //Com_Printf("\n%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() -------------------- \n", __func__, self->client->pers.netname, level.framenum); + + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname, level.framenum); + + //nav_area.total_areas = 0; // Turn off area based nav + BOTLIB_PickLongRangeGoal(self); + + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() curr[%d] goal[%d] -------------------- \n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node, self->bot.goal_node); + } + } + /* + if (self->bot.state == BOT_MOVE_STATE_NAV_NEXT) // If map has nodes grouped into areas + { + //self->bot.pause_time = 100; + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV_NEXT BOTLIB_GetNextAreaNode() -------------------- \n", __func__, self->client->pers.netname, level.framenum); + BOTLIB_GetNextAreaNode(self); // Get next area + } + */ + + + + // Kill the bot if completely stuck somewhere + //if(VectorLength(self->velocity) > 37) // + // self->suicide_timeout = level.framenum + 10.0 * HZ; + //if( self->suicide_timeout < level.framenum && !teamplay->value ) + // killPlayer( self, true ); + + // Kill the bot if they've not moved between nodes in a timely manner, stuck! + if (self->bot.node_travel_time > 120) + killPlayer(self, true); + + // Find any short range goal + //ACEAI_PickShortRangeGoal(self); + + //BOTLIB_GetWeaponsAndAmmo(self); //rekkie -- Locate and pickup a primary weapon if we need one + + + if (0) // Always follow player -- && (level.framenum % HZ == 0)) + { + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (players[p]->is_bot == false && players[p]->bot.current_node != 0 && players[p]->solid == SOLID_BBOX && players[p]->deadflag == DEAD_NO) + { + // Get friendly node + int n = players[p]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (n == INVALID) + continue; + + if (self->bot.goal_node != n) + { + Com_Printf("%s %s following %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[p]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + //BOTLIB_SetGoal(self, n); + } + } + } + } + + + + + //if (1) + { + BOTLIB_FindVisibleAllies(self); // Find visible allies + BOTLIB_Radio(self, &ucmd); + self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies + BOTLIB_Reload(self); // Reload the weapon if needed + + if (self->enemy) + { + // Chase after the new enemy + if (self->bot.enemy_chase_time < level.framenum && self->enemy->bot.current_node != self->bot.goal_node) + { + qboolean chase_enemy = false; + if ((FindItem(HC_NAME) == self->client->weapon || + FindItem(M3_NAME) == self->client->weapon || + FindItem(DUAL_NAME) == self->client->weapon || + FindItem(KNIFE_NAME) == self->client->weapon) + && self->bot.enemy_dist > 200) + { + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + 1 * HZ; // Delay next call + } + else if (FindItem(SNIPER_NAME) == self->client->weapon)// && self->bot.enemy_dist > 1500) + { + chase_enemy = false; + self->bot.enemy_chase_time = level.framenum + (((rand() % 20) + 10) * HZ); // Delay next call + } + else if ((FindItem(M4_NAME) == self->client->weapon || FindItem(MP5_NAME) == self->client->weapon) && self->bot.enemy_dist > 1024) + { + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + (((rand() % 10) + 10) * HZ); // Delay next call + } + + if (chase_enemy) + { + // Get enemy node + if (BOTLIB_CanGotoNode(self, self->enemy->bot.current_node, false)) // Make sure we can visit the node they're at + { + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, self->enemy->bot.current_node); + //Com_Printf("%s %s visiting enemy %s node %i [delay: %i vs %i] [wep: %s]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->bot.current_node, self->bot.enemy_chase_time, level.framenum, self->client->weapon->pickup_name); + } + } + } + } + + // If the bot is on a slope, raise it up depending on the slope normal and the bot mins/maxs hit box + { + self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 48), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + + /* + self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + vec3_t exp_up; + VectorCopy(self->bot.touch_ground.plane.normal, exp_up); + exp_up[2] = 0; + VectorNormalize(exp_up); + exp_up[2] = 1; + VectorScale(exp_up, 24, exp_up); + // Feed the raised up position back into the trace + VectorAdd(self->bot.touch_ground.endpos, exp_up, self->bot.touch_ground.endpos); + self->bot.touch_ground = gi.trace(self->bot.touch_ground.endpos, NULL, NULL, self->bot.touch_ground.endpos, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + */ + + } + + if (self->bot.state == BOT_MOVE_STATE_MOVE || self->bot.state == BOT_MOVE_STATE_WANDER || self->bot.state == BOT_MOVE_STATE_STAND) + { + BOTLIB_FollowPath(self); // Get current and next node back from nav code. + BOTLIB_Wander(self, &ucmd); + } + + BOTLIB_TouchingLadder(self); + BOTLIB_Look(self, &ucmd); + + BOTLIB_ChooseWeapon(self); + + // When out of sight of enemies + if (self->bot.see_enemies == false) + { + BOTLIB_Healing(self, &ucmd); // Check if bot needs to heal + BOTLIB_ReadyWeapon(self); // Change to a better weapon + + // Sniper bots should zoom in before an encounter + if ((rand() % 10) == 0) + BOTLIB_SniperZoom(self); + + } + + //if (self->bot.see_enemies == true && self->bot.enemy_in_xhair && //(self->enemy && self->enemy->deadflag == DEAD_NO) && + // (self->client->weaponstate != WEAPON_RELOADING) && (self->client->bandaging == 0) && + // (teamplay->value && lights_camera_action <= 1) || teamplay->value == 0) + if (self->bot.see_enemies) + { + if (self->bot.enemy_in_xhair) + BOTLIB_Attack(self, &ucmd); + + else if (self->client->weapon == FindItemByNum(HC_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); + + else if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); + } + } + + + // Remember where we were, to check if we got stuck. + //VectorCopy( self->s.origin, self->lastPosition ); + + // set bot's view angle + ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); + ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); + ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); + +end_think: + + ucmd.msec = 1000 / BOT_FPS; + self->client->ps.pmove.pm_time = ucmd.msec / 8; + + BOTLIB_BotInputToUserCommand(self, &self->bot.bi, &ucmd, self->bot.bi.viewangles, self->client->ps.pmove.pm_time); + + ClientThink(self, &ucmd); + + self->nextthink = level.framenum + 1; //(game.framerate / BOT_FPS); +} + + + +// Return's the optimal pitch for throwing a knife at an enemy +float BOTLIB_ThrowingKnifePitch(edict_t* self) +{ + if (self->enemy == NULL) + return 0; + + float gravity = sv_gravity->value; // Gravity + float distanceXYZ = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XY + float distanceXY = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XYZ + float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference + float initialSpeed = 1200; + + float timeToMaxHeight = sqrtf((initialSpeed * initialSpeed) / (2 * gravity)); + float totalFlightTime = timeToMaxHeight + sqrtf((distanceXYZ / height_diff) + timeToMaxHeight * timeToMaxHeight); + + // Calculate total flight time + totalFlightTime = sqrtf((2 * distanceXYZ) / sv_gravity->value); + + float horizontalVelocity = distanceXY / totalFlightTime; + float verticalVelocity = ((horizontalVelocity * horizontalVelocity) + (initialSpeed * initialSpeed)) / (2 * height_diff); + + float launchAngleDegrees = atan2f(verticalVelocity, horizontalVelocity) * (180 / M_PI); + launchAngleDegrees += 90; + if (launchAngleDegrees) + { + launchAngleDegrees /= 2; + + if (height_diff > 0) + launchAngleDegrees -= 90; + } + if (launchAngleDegrees > 89 || launchAngleDegrees < -89) + launchAngleDegrees = 0; + + Com_Printf("%s %s pitch[%f]\n", __func__, self->enemy->client->pers.netname, launchAngleDegrees); + + return launchAngleDegrees; + + //ucmd->angles[PITCH] = launchAngleDegrees; +} + +void BOTLIB_TouchingLadder(edict_t* self) +{ + //trace_t tr; + + //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) + { + // Check if touching ladder + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + //VectorMA(self->s.origin, 1, fwd, end); + //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); + + VectorMA(self->s.origin, 1, fwd, end); + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 8, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 16, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 32, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + } + } +} + +// Record footsteps +void BOTLIB_Footsteps(edict_t *self) +{ + if (self->s.event == EV_FOOTSTEP) + PlayerNoise(self, self->s.origin, PNOISE_SELF); +} + +// Keep a record of player noises and their locations +void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type) +{ + // Each player + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (players[i] == who) // Find the right player + { + // Assign based on type + if (type == PNOISE_SELF) // Getting in and out of water, drowning, walking + { + botlib_noises.self_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.self_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.self_origin[i][0] += 512; + else + botlib_noises.self_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][1] += 512; + else + botlib_noises.self_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + else if (type == PNOISE_WEAPON) // Weapons with muzzle flash + { + botlib_noises.weapon_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.weapon_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.weapon_origin[i][0] += 512; + else + botlib_noises.weapon_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.weapon_origin[i][1] += 512; + else + botlib_noises.weapon_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + else if (type == PNOISE_IMPACT) // Knife impact, or grenade explosion + { + botlib_noises.impact_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.impact_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.impact_origin[i][0] += 512; + else + botlib_noises.impact_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.impact_origin[i][1] += 512; + else + botlib_noises.impact_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + } + } + + // Code from BASEQ2 -- s + /* + edict_t *noise; + + if (type == PNOISE_WEAPON) { + if (who->client->silencer_shots) { + who->client->silencer_shots--; + return; + } + } + + if (deathmatch->value) + return; + + if (who->flags & FL_NOTARGET) + return; + + + if (!who->mynoise) { + noise = G_Spawn(); + noise->classname = "player_noise"; + VectorSet(noise->mins, -8, -8, -8); + VectorSet(noise->maxs, 8, 8, 8); + noise->owner = who; + noise->svflags = SVF_NOCLIENT; + who->mynoise = noise; + + noise = G_Spawn(); + noise->classname = "player_noise"; + VectorSet(noise->mins, -8, -8, -8); + VectorSet(noise->maxs, 8, 8, 8); + noise->owner = who; + noise->svflags = SVF_NOCLIENT; + who->mynoise2 = noise; + } + + if (type == PNOISE_SELF || type == PNOISE_WEAPON) { + noise = who->mynoise; + level.sound_entity = noise; + level.sound_entity_framenum = level.framenum; + } else { // type == PNOISE_IMPACT + noise = who->mynoise2; + level.sound2_entity = noise; + level.sound2_entity_framenum = level.framenum; + } + + VectorCopy(where, noise->s.origin); + VectorSubtract(where, noise->maxs, noise->absmin); + VectorAdd(where, noise->maxs, noise->absmax); + noise->last_sound_framenum = level.framenum; + gi.linkentity(noise); + */ + // Code from BASEQ2 -- e +} + +/////////////////////////////////////////////////////////////////////// +// Checks if bot can move (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_CanMove(edict_t* self, int direction) +{ + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(self->s.angles, angles); + + if (direction == MOVE_LEFT) + angles[1] += 90; + else if (direction == MOVE_RIGHT) + angles[1] -= 90; + else if (direction == MOVE_BACK) + angles[1] -= 180; + + // Trace ahead to see if safe or hitting wall + { + AngleVectors(angles, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 64, forward, end); + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); + if (tr.fraction < 1.0 || tr.contents & MASK_DEADLY || (tr.ent && (tr.ent->touch == hurt_touch))) + return false; + } + + // Trace down to see if safe + { + AngleVectors(angles, forward, right, NULL); // Set up the vectors + + //VectorSet(offset, 36, 0, 24); + //G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorSet(offset, 36, 0, -110); // RiEvEr reduced drop distance + G_ProjectSource(self->s.origin, offset, forward, right, end); + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; + } + } + + + return true; // yup, can move +} \ No newline at end of file diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c new file mode 100644 index 000000000..7db0d4003 --- /dev/null +++ b/src/action/botlib/botlib_cmd.c @@ -0,0 +1,794 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Bot server commands +qboolean BOTLIB_SV_Cmds(void) +{ + char* cmd; + cmd = gi.argv(1); + + if (Q_stricmp(cmd, "bots") == 0) + { + int cc = gi.argc(); + + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added + + // sv bots + if (gi.argc() == 3) // Adding a multiple bots + { + bot_connections.desire_bots = atoi(gi.argv(2)); // How many bots + bot_connections.auto_balance_bots = true; + return true; + } + + // sv bots + else if (gi.argc() == 4) // Adding a multiple bots to a select team + { + int count = atoi(gi.argv(2)); // How many bots + int team = atoi(gi.argv(3)); // What team + + //if (count == 0) + // count = -1; // Flag to remove all bots from team + + bot_connections.auto_balance_bots = false; + + // Deathmatch + if (teamplay->value == 0) + { + if (count == 0) + { + BOTLIB_RemoveBot("ALL"); + return true; + } + else + { + bot_connections.desire_bots = count; + bot_connections.auto_balance_bots = true; + return true; + } + } + else // Teamplay + { + if (team == TEAM1) bot_connections.desire_team1 = count; + else if (team == TEAM2) bot_connections.desire_team2 = count; + else if (team == TEAM3) bot_connections.desire_team3 = count; + + return true; + } + } + + else + { + gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "Use: sets the number of bots to join a game\n"); + gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "sv bots \n"); + gi.cprintf(NULL, PRINT_HIGH, "sv bots [team]\n"); + if (teamplay->value == 0) + gi.cprintf(NULL, PRINT_HIGH, "DM bot total [%d]\n", bot_connections.total_bots); + else if (use_3teams->value) + gi.cprintf(NULL, PRINT_HIGH, "3TEAM bots on T1[%d] T2[%d] T3[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots); + else if (ctf->value) + gi.cprintf(NULL, PRINT_HIGH, "CTF bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); + else if (teamplay->value) + gi.cprintf(NULL, PRINT_HIGH, "TP bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); + gi.cprintf(NULL, PRINT_HIGH, "-------------------------------\n"); + } + + return true; + } + + if (Q_stricmp(cmd, "addbot") == 0) + { + + return true; + } + else if (Q_stricmp(cmd, "addbots") == 0) + { + if (gi.argc() >= 3) + { + int count = atoi(gi.argv(2)), i = 0; + for (i = 0; i < count; i++) + { + } + } + else + gi.cprintf(NULL, PRINT_HIGH, "Usage: sv addbots []\n"); + + return true; + } + // removebot + else if (Q_stricmp(cmd, "removebot") == 0) + { + ACESP_RemoveBot(gi.argv(2)); + return true; + } + else if (Q_stricmp(cmd, "removebots") == 0) + { + ACESP_RemoveBot("all"); + return true; + } + //rekkie -- BSP -- s + else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + { + BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + return true; + } + // Process BSP and generate AAS reachability with nodes + else if (Q_stricmp(cmd, "aas") == 0) + { + ACEND_BSP(NULL); + return true; + } + //rekkie -- BSP -- e + + //rekkie -- python chatbot -- s +#if 0 + // + // Allows Quake 2 to communicate with a LLM (Large Language Model) + // [Input] Quake 2 ==> textgen.py ==> text-generation-webui (server) + // [Output] text-generation-webui (server) ==> textgen.py ==> Quake 2 + // + // Idea was taken from: https://www.youtube.com/watch?v=ndLqg4RYcXE (Full Tutorial of Calling Python Functions(.py) in C/C++(.c or .cpp) using (python.h)) + // + // This runs the python file ./action/bots/textgen.py + // The py file allows communcation with an ai chat server called text-generation-webui (github: https://github.com/oobabooga/text-generation-webui) + // + // text-generation-webui must be run with OpenAI API turned on (port 5000) + // + // From the C end, we require a few includes and libs from Python. + // NOTE: When installing python, make sure (Download debug binaries) is ticked, + // otherwise the compiler will complain about "python310_d.lib" missing. + // https://github.com/pybind/pybind11/issues/3403 + // + // Includes + // C:\Users\...\AppData\Local\Programs\Python\Python310\include + // + // Linker (libs) + // C:\Users\...\AppData\Local\Programs\Python\Python310\libs\python310.lib + // C:\Users\...\AppData\Local\Programs\Python\Python310\libs + else if (Q_stricmp(cmd, "chat") == 0) + { + Py_Initialize(); // Init python + + PyObject* name, * load_module, * func, * callfunc, * args; + + /* + //https://groups.google.com/g/comp.lang.python/c/14Yl3C8suKI?pli=1 + // https://gist.github.com/Coderx7/509a1aaf154a8d2532a6459a57308a63 + PyObject* mainmod = PyImport_AddModule("__main__"); + Py_INCREF(mainmod); + PyObject* ns = PyModule_GetDict(mainmod); + Py_INCREF(ns); + PyObject* timeModule = PyImport_ImportModuleEx("requests", ns, ns, NULL); + */ + + //PyObject* module = PyImport_AddModule("requests"); + PyObject* pModule = PyImport_ImportModule("requests"); // The python imports (imports used in textgen.py) + + // Open our custom python file + const wchar_t* path = L".\\action\\bots\\"; // https://stackoverflow.com/questions/31314767/call-multiple-python-functions-from-different-directories + PySys_SetPath(path); // https://stackoverflow.com/questions/1796925/how-to-import-a-file-by-its-full-path-using-c-api + name = PyUnicode_FromString((char*)"textgen"); + load_module = PyImport_Import(name); + + + + if (load_module != NULL) + { + func = PyObject_GetAttrString(load_module, (char*)"func"); // Set our custom function to call + //args = PyTuple_Pack(1, PyUnicode_FromString((char*)"hello")); + args = PyTuple_Pack(1, PyUnicode_FromString(gi.argv(2))); // Set the arguments to send + + callfunc = PyObject_CallObject(func, args); // Call our custom python function + if (callfunc != NULL) + { + const char* str = _PyUnicode_AsString(callfunc); + Com_Printf("%s %s\n", __func__, str); + } +} + + Py_Finalize(); + } + else if (Q_stricmp(cmd, "chatfile") == 0) + { + char filename[128]; +#ifdef _WIN32 + sprintf(filename, ".\\action\\bots\\textchat.txt"); +#else + strcpy(filename, "./action/bots/textchat.txt"); +#endif + + // If the file exists and NOT empty, the file is considered 'in use' + FILE* file = fopen(filename, "r"); + if (file != NULL) + { + fseek(file, 0L, SEEK_END); + long size = ftell(file); + if (size > 0) + { + Com_Printf("File in use: %s\n", filename); + fclose(file); // Close the file after use + return; + } + } + + // Write to file + file = fopen(filename, "w"); // Open the file in write mode + if (file == NULL) { + Com_Printf("Could not write to file %s\n", filename); + return; + } + fprintf(file, "%s\n%s", gi.argv(2), gi.argv(3)); // Write to the file + fclose(file); // Close the file after use + + /* + srand((unsigned int)time(NULL)); // Seed the random number generator + + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + char key[7]; // Six characters + null terminator + for (int i = 0; i < 6; ++i) { + int index = rand() % (sizeof(charset) - 1); + key[i] = charset[index]; + } + key[6] = '\0'; // Null-terminate the string + + char playerName[256], chatMessage[256]; + + printf("Enter your name: "); + scanf("%s", playerName); + + printf("Enter your chat message: "); + getchar(); // Consume newline left by previous input + fgets(chatMessage, sizeof(chatMessage), stdin); // Read the whole line including spaces + + fprintf(file, "%s\n%s\n%s", key, playerName, chatMessage); // Write to the file + + fclose(file); // Close the file after use + */ + } +#endif + //rekkie -- python chatbot -- e + + return false; +} + +// Bot commands +qboolean BOTLIB_Commands(edict_t* ent) +{ + char* cmd = gi.argv(0); + + //rekkie -- BSP -- s + if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + { + BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + return true; + } + else if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS + { + ACEND_SaveAAS(true); + return true; + } + else if (Q_stricmp(cmd, "an") == 0) // add node + { + ACEND_BSP(ent); + return true; + } + else if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names + { + BOTLIB_RandomizeTeamNames(ent); + return true; + } + else if (Q_stricmp(cmd, "randomize_team_skins") == 0) // Manually randomize team skins + { + BOTLIB_RandomizeTeamSkins(ent); + return true; + } + //rekkie -- BSP -- e + /* + else if (Q_stricmp(cmd, "nav_vis") == 0) // Manually create a navigation vis network + { + if (dedicated->value) + return true; + + BOTLIB_GenerateNodeVis(ent); + + return true; + } + */ + else if (Q_stricmp(cmd, "nav_toggle") == 0) // Toggles display between: node editing, both pathing, and neither + { + if (dedicated->value) + return true; + + //bot_showpath->value = !bot_showpath->value; // Toggle path display + if (bot_showpath->value == 0 && ent->bot.walknode.enabled == false) + { + ent->bot.walknode.enabled = true; // Turn on nav editor + bot_showpath->value = 0; // Turn on path display + + } + else if (bot_showpath->value == 0 && ent->bot.walknode.enabled == true) + { + ent->bot.walknode.enabled = false; // Turn off nav editor + bot_showpath->value = 1; // Turn on path display + } + else + { + ent->bot.walknode.enabled = false; // Turn off nav editor + bot_showpath->value = 0; // Turn off path display + } + return true; + } + else if (Q_stricmp(cmd, "nav_edit") == 0) // Turn nav editor on/off + { + if (dedicated->value) + return true; + ent->bot.walknode.enabled = !ent->bot.walknode.enabled; + return true; + } + + else if (Q_stricmp(cmd, "nav_edges") == 0) // Print out all the area edges + { + if (rand() % 2) + BOTLIB_CanGotoNode(ent, nodes[2505].nodenum, 0); + else + BOTLIB_CanGotoNode(ent, nodes[999].nodenum, 0); + + /* + int goal_node = INVALID; + if (gi.argc() >= 2) + goal_node = atoi(gi.argv(1)); + + BOTLIB_CanGotoNode(ent, goal_node, false); + */ + return true; + } + + else if (Q_stricmp(cmd, "nav_go") == 0) // Test function + { + BOTLIB_GetNextAreaNode(ent); + if (ent->bot.next_area_node != INVALID) + VectorCopy(tv(nodes[ent->bot.next_area_node].origin[0], nodes[ent->bot.next_area_node].origin[1], nodes[ent->bot.next_area_node].origin[2] + 32), ent->s.origin); // Teleport to node + return true; + } + else if (Q_stricmp(cmd, "nav_aa") == 0) // Automatically generate nav areas + { + if (numnodes) + { + BOTLIB_AutoArea(ent); + } + return true; + } + else if (Q_stricmp(cmd, "nav_area") == 0) // Set the area num + { + if (dedicated->value) + return true; + + if (gi.argc() >= 2) + { + if (ent->bot.walknode.enabled) + { + ent->bot.walknode.selection_area = atoi(gi.argv(1)); + + // Force an area color + int red; + int green; + int blue; + if (gi.argc() == 5) + { + red = atoi(gi.argv(2)); + green = atoi(gi.argv(3)); + blue = atoi(gi.argv(4)); + + // Sanity check -- randomize color if it's malformed + if (red < 0 || red > 255) + red = rand() % 255; + if (green < 0 || green > 255) + green = rand() % 255; + if (blue < 0 || blue > 255) + blue = rand() % 255; + } + else // Randomize color if no color picked + { + red = rand() % 255; + green = rand() % 255; + blue = rand() % 255; + } + + ent->bot.walknode.selection_area_color = MakeColor(red, green, blue, 255); + + // Reset all area nodes to zero + if (ent->bot.walknode.selection_area < 0) + { + Com_Printf("Nav area resetting all nodes to: 0\n"); + ent->bot.walknode.selection_area = 0; + + for (int i = 0; i < numnodes; i++) + { + nodes[i].area = 0; + nodes[i].area_color = 0; + } + } + else // Try setting area node + { + // Check if selected node is disconnected from an existing area (areas must not be disconnected islands) + int node = 0; + int target_node = 0; + qboolean area_is_linked = false; + qboolean area_is_new = false; + //if (ent->bot.walknode.selection_area_used) + { + // 1) Check if our area num exists + qboolean area_exists = false; + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].area == ent->bot.walknode.selection_area) + { + area_exists = true; + break; + } + } + + // 2) If it doesn't exist, then mark as new area. No need to worry about connecting because we're making a new area. + if (area_exists == false) + area_is_new = true; + + // 3) If the area num exists, see if any of our selected nodes are connected to it + else if (area_exists) + { + for (int i = 0; i < ent->bot.walknode.selection_node_count; i++) + { + node = ent->bot.walknode.selection_nodes[i]; + + // Check if any of the selected nodes are connected to our desired selection_area + if (nodes[node].area == ent->bot.walknode.selection_area) + { + area_is_linked = true; + break; + } + + // Check if any of their links connect to our desired selection_area + for (int l = 0; l < nodes[node].num_links; l++) + { + target_node = nodes[node].links[l].targetNode; + if (nodes[target_node].area == ent->bot.walknode.selection_area) + { + area_is_linked = true; + break; + } + } + if (area_is_linked) + break; + } + } + + // 4) Optionally select surrounding nodes and include them in the set if: + // * Nodes are area: 0 + // * Can connect to the selected nodes without going through another area + // * Similar surface normal (and/or height?) + //if (area_exists && (area_is_linked || area_is_new)) + { + //if (ent->bot.walknode.selection_node_count) + // BOTLIB_GroupConnectedNodeArea(ent, ent->bot.walknode.selection_nodes[0]); + } + } + + if (area_is_linked || area_is_new || ent->bot.walknode.selection_area == 0) + { + ent->bot.walknode.selection_area_used = true; // Flag that we desire to change the area + Com_Printf("Nav area set to: %d\n", ent->bot.walknode.selection_area); + } + else + { + ent->bot.walknode.selection_area_used = false; // Don't change area + Com_Printf("Found existing area %d but the selected nodes do not connect to it.\nAreas of the same number need to be connected together, they cannot be disconnected.\n", ent->bot.walknode.selection_area); + } + } + } + } + else + { + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "Use: sets the area number of any selected nodes \n"); + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "nav_area \n"); + gi.cprintf(NULL, PRINT_HIGH, "OPTIONAL: nav_area \n"); + gi.cprintf(NULL, PRINT_HIGH, "Use '-1' to remove all areas from nodes\n"); + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + } + return true; + } + else if (Q_stricmp(cmd, "nav_load") == 0) // Load bot nav from file + { +#ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); +#else + BOTLIB_LoadNav(); +#endif + return true; + } + else if (Q_stricmp(cmd, "nav_save") == 0) // Save bot nav to file + { +#ifdef USE_ZLIB + BOTLIB_SaveNavCompressed(); +#else + BOTLIB_SaveNav(); +#endif + return true; + } + else if (Q_stricmp(cmd, "nav_autogen") == 0) // Auto generate navigation that propagates from spawn points + { + BOTLIB_SelfExpandNodesFromSpawnpoints(ent); + BOTLIB_LinkAllNodesTogether(ent); + return true; + } + //rekkie -- surface data -- s + else if (Q_stricmp(cmd, "nav") == 0) // add navigation + { + BOTLIB_InitNavigation(ent); + return true; + +#if 0 + ent->nav = gi.Nav(); // Grant access to navigation data + if (ent->nav == NULL) + { + gi.dprintf("%s failed to import NAV data\n", __func__); + } + else + { + Com_Printf("%s NAV faces %d ignored %d\n", __func__, ent->nav->faces_total, ent->nav->ignored_faces_total); + ACEND_SaveAAS(false); + } + + return true; +#endif + } + //rekkie -- surface data -- e + + /* + else if (Q_stricmp(cmd, "test") == 0) // Test func -- + { + char buffer[256]; + sprintf(buffer, "%c Bandaging [ %d%c ] %c", '\x04', ent->health, '\x05', '\x04'); + Com_Printf("%s %s\n", __func__, buffer); + } + */ + else if (Q_stricmp(cmd, "flag") == 0) // Test func -- grab a flag + { + if (ctf->value) + { + int team = atoi(gi.argv(1)); + + if (team == TEAM1) + { + gi.unlinkentity(bot_ctf_status.flag1); + VectorCopy(ent->s.origin, bot_ctf_status.flag1->s.origin); + gi.linkentity(bot_ctf_status.flag1); + } + if (team == TEAM2) + { + gi.unlinkentity(bot_ctf_status.flag2); + VectorCopy(ent->s.origin, bot_ctf_status.flag2->s.origin); + gi.linkentity(bot_ctf_status.flag2); + } + + return true; + } + } + else if (Q_stricmp(cmd, "head") == 0) // Test func -- spawn kickable head + { + ThrowGibbedHead(ent, 50); // Spawn kickable head + return true; + } + else if (Q_stricmp(cmd, "ball") == 0) // Test func -- spawn kickable ball + { + QPong_Ball(ent, 50); + return true; + } + else if (Q_stricmp(cmd, "dc_add_sp") == 0) // Add (or recycle unused) custom spawn point + { + DC_Add_Spawnpoint(ent); // This will add a custom spawn point at the player's location + return true; + } + else if (Q_stricmp(cmd, "dc_remove_sp") == 0) // Remove custom spawn point + { + DC_Remove_Spawnpoint(ent); // This will remove a custom spawn point at the player's location + return true; + } + else if (Q_stricmp(cmd, "dc_save_sp") == 0) // Save spawn points to file + { + DC_Save_Spawnpoints(); // This will save the map and user added spawn points to file + return true; + } + else if (Q_stricmp(cmd, "bot_show_spawns") == 0) // Show spawn points + { + BOTLIB_Show_Spawnpoints(); // This will show the map and user added spawn points + return true; + } + else if (Q_stricmp(cmd, "removenode") == 0 && debug_mode) + ACEND_RemoveNode(ent, atoi(gi.argv(1))); + else if (Q_stricmp(cmd, "gtnode") == 0 && debug_mode) // Goto node - forces all bots to goto node num + { + int node = atoi(gi.argv(1)); + if (node > 0 && node < numnodes) + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + if (BOTLIB_CanGotoNode(players[i], nodes[node].nodenum, false)) + { + Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, nodes[node].nodenum); + //players[i]->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(players[i], nodes[node].nodenum); + } + } + } + } + else if (ent->bot.current_node > 0) // Goto player location + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + if (BOTLIB_CanGotoNode(players[i], ent->bot.current_node, false)) + { + Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, ent->bot.current_node); + //players[i]->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(players[i], ent->bot.current_node); + } + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "ttnode") == 0 && debug_mode) // Teleport player to node location + { + int node = atoi(gi.argv(1)); + + if (node > INVALID && node < numnodes) + { + Com_Printf("%s Teleporting to node %i [%f %f %f]\n", __func__, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + VectorCopy(nodes[node].origin, ent->s.origin); + } + + return true; + } + else if (Q_stricmp(cmd, "tbtplayer") == 0 && debug_mode) // Teleport random bot to player location + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + Com_Printf("%s Teleporting %s to player %s [%f %f %f]\n", __func__, players[i]->client->pers.netname, ent->client->pers.netname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + VectorCopy(ent->s.origin, players[i]->s.origin); + return true; + } + } + + return true; + } + else if (Q_stricmp(cmd, "tbtnode") == 0 && debug_mode) // Teleport random bot to node location + { + int node = atoi(gi.argv(1)); + + if (node > INVALID && node < numnodes) + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + Com_Printf("%s Teleporting %s to node %i [%f %f %f]\n", __func__, players[i]->client->pers.netname, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + VectorCopy(nodes[node].origin, players[i]->s.origin); + return true; + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "ttloc") == 0 && debug_mode) // Teleport to location + { + float x = atof(gi.argv(1)); + float y = atof(gi.argv(2)); + float z = atof(gi.argv(3)); + + Com_Printf("%s Teleporting to location [%f %f %f]\n", __func__, x, y, z); + ent->s.origin[0] = x; + ent->s.origin[1] = y; + ent->s.origin[2] = z; + + return true; + } + else if (Q_stricmp(cmd, "bnode") == 0 && debug_mode) // Break all links to node + { + int node = atoi(gi.argv(1)); + + if (node > 0 && node < numnodes) + BOTLIB_RemoveAllNodeLinksFrom(node); + + return true; + } + else if (Q_stricmp(cmd, "cnode") == 0 && debug_mode) // Convert node + { + int node = atoi(gi.argv(1)); + int type = atoi(gi.argv(2)); + + if (node > 0 && node < numnodes) + { + if (type == NODE_MOVE || type == NODE_JUMPPAD || type == NODE_POI) + { + Com_Printf("%s Converting node %i from type %i to %i\n", __func__, node, nodes[node].type, type); + nodes[node].type = type; + + // Update node's ent details like color + edict_t* ent; + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent && ent->node_num == node) + { + if (type == NODE_MOVE) + ent->s.renderfx = RF_SHELL_BLUE; + else if (type == NODE_JUMPPAD) + ent->s.renderfx = RF_SHELL_GREEN; + else if (type == NODE_POI) + ent->s.renderfx = RF_SHELL_RED; + + break; + } + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "bvnodes") == 0 && debug_mode) // Build vis nodes + { + ACEND_BuildVisibilityNodes(); + return true; + } + else if (Q_stricmp(cmd, "xynode") == 0 && debug_mode) // Can node X see node Y + { + int x = atoi(gi.argv(1)); + int y = atoi(gi.argv(2)); + + if (x <= 0 || y <= 0 || x != y) + { + Com_Printf("Usage: xynode \n"); + return true; + } + + if (ACEND_IsNodeVisibleToNodes(x, y)) + Com_Printf("%s Node %i can see node %i\n", __func__, x, y); + else + Com_Printf("%s Node %i cannot see node %i\n", __func__, x, y); + + return true; + } + else if (Q_stricmp(cmd, "xrngnode") == 0 && debug_mode) // Random node X can see + { + int x = atoi(gi.argv(1)); + + if (x <= 0) + { + Com_Printf("Usage: xrngnode \n"); + return true; + } + + Com_Printf("%s Node %i can see random visible node %i\n", __func__, x, ACEND_GetRandomVisibleNode(x)); + + return true; + } + //rekkie -- DEV_1 -- e + //rekkie -- BSP -- e + + else + return false; + + return true; +} \ No newline at end of file diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c new file mode 100644 index 000000000..361c4ecf9 --- /dev/null +++ b/src/action/botlib/botlib_communication.c @@ -0,0 +1,550 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" +#include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 + +// Bot wave gestures +void BOTLIB_Wave(edict_t* ent, int type) +{ + if (ent->groundentity == NULL) return; // Don't wave when not on ground + if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked + if (ent->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active + + // Time limit between waves + if (ent->client->resp.lastWave + (1 * HZ) > level.framenum) + return; + ent->client->resp.lastWave = level.framenum; // Update last wave + + + switch (type) + { + case WAVE_FLIPOFF: + SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff + break; + case WAVE_SALUTE: + SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute + break; + case WAVE_TAUNT: + SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt + break; + case WAVE_WAVE: + SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave + break; + case WAVE_POINT: + default: + SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point + break; + } +} + +void BOTLIB_PrecacheRadioSounds() +{ + int i; + char path[MAX_QPATH]; + + //male + for (i = 0; i < bot_numMaleSnds; i++) + { + Com_sprintf(path, sizeof(path), "%s%s.wav", "radio/male/", bot_male_radio_msgs[i].msg); + bot_male_radio_msgs[i].sndIndex = gi.soundindex(path); + } + + //female + for (i = 0; i < bot_numFemaleSnds; i++) + { + Com_sprintf(path, sizeof(path), "%s%s.wav", "radio/female/", bot_female_radio_msgs[i].msg); + bot_female_radio_msgs[i].sndIndex = gi.soundindex(path); + } +} +void BOTLIB_RadioBroadcast(edict_t* ent, edict_t* partner, char* msg) +{ + int j, i, msg_len, numSnds; + edict_t* other; + bot_radio_msg_t* radio_msgs; + int msg_soundIndex = 0; + char msgname_num[8], filteredmsg[48]; + qboolean found = false; + radio_t* radio; + + if (!IS_ALIVE(ent)) + return; + + if (!teamplay->value) + { + if (!DMFLAGS((DF_MODELTEAMS | DF_SKINTEAMS))) + return; // don't allow in a non-team setup... + } + + /* + //TempFile END + //AQ2:TNG Slicer + if (radio_repeat->value) + { //SLIC2 Optimization + if (CheckForRepeat(ent, i) == false) + return; + } + + if (radio_max->value) + { + if (CheckForFlood(ent) == false) + return; + } + */ + + radio = &ent->client->resp.radio; + if (radio->gender) + { + radio_msgs = bot_female_radio_msgs; + numSnds = bot_numFemaleSnds; + } + else + { + radio_msgs = bot_male_radio_msgs; + numSnds = bot_numMaleSnds; + } + + i = found = 0; + msg_len = 0; + + Q_strncpyz(filteredmsg, msg, sizeof(filteredmsg)); + + for (i = 0; i < numSnds; i++) + { + if (!Q_stricmp(radio_msgs[i].msg, filteredmsg)) + { + found = true; + msg_soundIndex = radio_msgs[i].sndIndex; + msg_len = radio_msgs[i].length; + break; + } + } + if (!found) + { + Com_Printf("%s '%s' is not a valid radio message", __func__, filteredmsg); + return; + } + + //TempFile BEGIN + if (Q_stricmp(filteredmsg, "enemyd") == 0) + { + if (ent->client->radio_num_kills > 1 && ent->client->radio_num_kills <= 10) + { + // If we are reporting enemy down, add the number of kills. + sprintf(msgname_num, "%i", ent->client->radio_num_kills); + ent->client->radio_num_kills = 0; // prevent from getting into an endless loop + + BOTLIB_RadioBroadcast(ent, partner, msgname_num); // Now THAT'S recursion! =) + } + ent->client->radio_num_kills = 0; + } + + if (partner) + { + BOTLIB_AddRadioMsg(&partner->client->resp.radio, msg_soundIndex, msg_len, ent); + return; + } + + //AQ2:TNG END + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + //Com_Printf("%s [%d]%s [%d]%s\n", __func__, ent->client->resp.team, ent->client->pers.netname, other->client->resp.team, other->client->pers.netname); //ent1->client->resp.team == ent2->client->resp.team; + if (!OnSameTeam(ent, other)) + continue; + BOTLIB_AddRadioMsg(&other->client->resp.radio, msg_soundIndex, msg_len, ent); + } +} + +// Allow bots to send a chat message to players +void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) +{ + int j, /*i,*/ offset_of_text; + edict_t* other; + char text[2048]; + //gclient_t *cl; + + + if (!teamplay->value) + return; + + if (ent->client->resp.team == NOTEAM) + return; + + if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + { + if (team_message) + Com_sprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team + else + Com_sprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all + } + else if (team_message) + Com_sprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team + else + Com_sprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all + + offset_of_text = strlen(text); //FB 5/31/99 + + strcat(text, pMsg); + + // don't let text be too long for malicious reasons + // ...doubled this limit for Axshun -FB + if (strlen(text) > 300) + text[300] = 0; + + if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) + ParseSayText(ent, text + offset_of_text, strlen(text)); //FB 5/31/99 - offset change + // this will parse the % variables, + // and again check 300 limit afterwards -FB + // (although it checks it without the name in front, oh well) + + strcat(text, "\n"); + + if (dedicated->value) + safe_cprintf(NULL, PRINT_CHAT, "%s", text); + + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + if (other->is_bot || Q_stricmp(other->classname, "bot") == 0) // Don't send msg to bots + continue; + if (team_message && !OnSameTeam(ent, other)) // If team message + continue; + if (teamplay->value && team_round_going) + { + if ((ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + && (other->solid != SOLID_NOT && other->deadflag != DEAD_DEAD)) + continue; + } + + safe_cprintf(other, PRINT_CHAT, "%s", text); + } +} + +// Returns kill counter. Each time this is checked, the kill counter is reset. +static int BOTLIB_ReadKilledPlayers(edict_t* ent) +{ + int kills = 0; + edict_t* targ; + + for (int i = 0; i < MAX_LAST_KILLED; i++) + { + targ = ent->client->last_killed_target[i]; + if (!targ) + break; + + if (!targ->inuse || !targ->client || OnSameTeam(ent, targ)) // Remove disconnected or team players from list + { + for (int j = i + 1; j < MAX_LAST_KILLED; j++) + ent->client->last_killed_target[j - 1] = ent->client->last_killed_target[j]; + + ent->client->last_killed_target[MAX_LAST_KILLED - 1] = NULL; + i--; + continue; + } + + kills++; + } + + return kills; +} + +// Reports a kill counter and adds killed names to the buffer +static void BOTLIB_GetLastKilledTargets(edict_t* self, int kills, char* buf) +{ + if (kills > 1) + sprintf(buf, " %c [ %d ] %dx Enemies Down! [ %c ", '\x06', self->client->resp.score, kills, '\x07'); // Build bandage string + else + sprintf(buf, " %c [ %d ] Enemy Down! [ %c ", '\x06', self->client->resp.score, '\x07'); // Build bandage string + + Q_strncatz(buf, self->client->last_killed_target[0]->client->pers.netname, PARSE_BUFSIZE); // First kill + + // If multiple kills + for (int i = 1; i < kills; i++) // Max kills: MAX_LAST_KILLED + { + if (i == kills - 1) // Last two kills (a, b, c, d and e) + Q_strncatz(buf, " and ", PARSE_BUFSIZE); + else // Multiple kills (a, b,...) + Q_strncatz(buf, ", ", PARSE_BUFSIZE); + + // Print the additional player names + Q_strncatz(buf, self->client->last_killed_target[i]->client->pers.netname, PARSE_BUFSIZE); + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); + + self->client->last_killed_target[0] = NULL; +} + +// Builds a list of enemy names in sight +static void BOTLIB_GetEnemyList(edict_t* self, char* buf) +{ + sprintf(buf, "[ %s", players[self->bot.enemies[0]]->client->pers.netname); // Print first player + + // If multiple enemies + if (self->bot.enemies_num > 0) + { + for (int i = 1; i < self->bot.enemies_num; i++) + { + if (i == 3) // If names exceed the limit + { + char extra[20]; + sprintf(extra, ", and %d more...", self->bot.enemies_num - 3); + Q_strncatz(buf, extra, PARSE_BUFSIZE); + break; + } + + if (i == self->bot.enemies_num - 1) + Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) + else + Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) + + Q_strncatz(buf, players[self->bot.enemies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names + } + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); +} + +// Returns a list of nearby teammates +static void BOTLIB_GetNearbyTeamList(edict_t* self, char* buf) +{ + sprintf(buf, "[ %s", players[self->bot.allies[0]]->client->pers.netname); // Print first player + + for (int i = 1; i < self->bot.allies_num; i++) + { + if (i == 3) // If names exceed the limit + { + char extra[20]; + sprintf(extra, ", and %d more", self->bot.allies_num - 3); + Q_strncatz(buf, extra, PARSE_BUFSIZE); + break; + } + + if (i == self->bot.allies_num - 1) + Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) + else + Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) + + Q_strncatz(buf, players[self->bot.allies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); +} + +// Radio commands +void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) +{ + char buffer[256]; // Chat buffer + + if (teamplay->value == 0) return; // Only radio in TP games + if (team_round_going == false || lights_camera_action) return; // Only allow radio during a real match (after LCA and before win/loss announcement) + //if (team_round_going && lights_camera_action == 0) + + //if (self->groundentity == NULL) return; // Don't wave when not on ground + //if (self->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked + //if (self->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active + + // Find how many bots on our team are alive + short bots_alive = 0; + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i]->client == NULL || players[i]->solid == SOLID_NOT || players[i]->deadflag != DEAD_NO) + continue; + + if (players[i]->is_bot == false) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + bots_alive++; + } + + // CALLER: Team report in + if (self->bot.radioTeamReportedIn == false && + BOTLIB_EnemiesAlive(self) && //BOTLIB_AlliesAlive(self) && + self->bot.see_enemies == false) + { + if (self->bot.allies_num > 0) + self->bot.radioLastSawAllyTime = 0; // Reset counter because we spotted an ally + else + self->bot.radioLastSawAllyTime++; // No ally seen + + if (self->bot.radioLastSawAllyTime > self->bot.radioTeamReportedInDelay) // Haven't seen any friendly players in a while + { + self->bot.radioTeamReportedIn = true; // Only allow once per round + + sprintf(buffer, " %c Team report in %c", '\x0E', '\x0E'); // Build string + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_TEAM_REPORT_IN; // Wave + BOTLIB_RadioBroadcast(self, NULL, "treport"); // Sound + + // Find a bot on our team who is alive, request they answer the report status + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i] == self || players[i]->client == NULL || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) || players[i]->deadflag != DEAD_NO) + continue; + + if (players[i]->is_bot == false) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + // We found a bot to report its status + players[i]->bot.radioReportIn = true; // Request this bot report in + break; + } + } + } + + // CALLEE: Random chance to flag this bot to respond to a 'team report in' request from a human + if (Q_stricmp(self->bot.radioLastHumanMsg, "treport") == 0) + { + if (bots_alive) + { + if ((rand() % bots_alive) == 1) // Scale chance based on bots on our team are alive (2 bots = 50%, 4 bots = 25%, ...) + self->bot.radioReportIn = true; // Request this bot report in + } + + self->bot.radioLastHumanMsg[0] = '\0'; // Remove last radio call from bot memory + } + + // CALLEE: Bot responding in to a 'team report in' request (either bot or human) + if (self->bot.radioReportIn) + { + self->bot.radioReportInTime++; + + if (self->bot.radioReportInTime > self->bot.radioReportInDelay) // RNG delay + { + self->bot.radioReportInTime = 0; + self->bot.radioReportIn = false; + + // Reaport near enemies + if (self->bot.enemies_num) + { + char nearby_enemies[PARSE_BUFSIZE]; + BOTLIB_GetEnemyList(self, nearby_enemies); + sprintf(buffer, " %c Status [ %d%c ] near enemies ", '\x0E', self->health, '\x05'); // Build string + Q_strncatz(buffer, nearby_enemies, PARSE_BUFSIZE); + //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); + } + // Report near allies + else if (self->bot.allies_num) + { + char nearby_team[PARSE_BUFSIZE]; + BOTLIB_GetNearbyTeamList(self, nearby_team); + sprintf(buffer, " %c Status [ %d%c ] near team ", '\x0F', self->health, '\x05'); // Build string + Q_strncatz(buffer, nearby_team, PARSE_BUFSIZE); + //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); + } + // Report alone + else + { + int report_type = (rand() % 2); + + // Basic report + if (report_type == 0) + { + sprintf(buffer, " %c Reporting in! [ %d%c ]", '\x0F', self->health, '\x05'); // Build string + } + else + { + char weap_name[PARSE_BUFSIZE]; + char item_name[PARSE_BUFSIZE]; + GetWeaponName(self, weap_name); + GetItemName(self, item_name); + sprintf(buffer, " %c Reporting in! [ %d%c ] Equipped with %c %s and %s", '\x0F', self->health, '\x05', '\x07', weap_name, item_name); // Build string + } + } + + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_REPORTIN; // Wave + BOTLIB_RadioBroadcast(self, NULL, "reportin"); // Sound + } + } + + + //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text + + + // Bandaging + if (self->bot.radioBandaging) // If bandaging, flag the bot to radio this in + { + if (IS_ALIVE(self)) + { + sprintf(buffer, " %c Bandaging [ %d%c ] %c", '\x04', self->health, '\x05', '\x04'); // Build string - 0x04 bandage symbol, 0x05 heart symbol + BOTLIB_Say(self, buffer, true); // Say it + + if (self->health > 75) // Taking fire + { + self->bot.bi.radioflags |= RADIO_TAKING_FIRE; // Wave + BOTLIB_RadioBroadcast(self, NULL, "taking_f"); // Sound + } + else if (self->health > 50) // Cover me + { + self->bot.bi.radioflags |= RADIO_COVER; // Wave + BOTLIB_RadioBroadcast(self, NULL, "cover"); // Sound + } + else // I'm hit + { + self->bot.bi.radioflags |= RADIO_IM_HIT; // Wave + BOTLIB_RadioBroadcast(self, NULL, "im_hit"); // Sound + } + } + + self->bot.radioBandaging = false; + } + + //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text + + // If one or more enemies have been killed, report them as enemy down + // Each report clears the list of enemies killed. + if (self->bot.radioReportKills && self->bot.see_enemies == false) // Delay report until free of enemies + { + int kills = BOTLIB_ReadKilledPlayers(self); + //if (kills && (rand() % 100 <= kills) ) // Scale chance based kills, more kills == more chance to report + + /*if (kills && kills < 2 && (rand() % 100 > 30)) + { + self->client->last_killed_target[0] = NULL; + kills = 0; + }*/ + + if (kills) + { + BOTLIB_GetLastKilledTargets(self, kills, buffer); // Build string + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_ENEMY_DOWN; // Wave + + // Radio how many enemies killed + /*switch (kills) + { + case 1: BOTLIB_RadioBroadcast(self, NULL, "1"); break; + case 2: BOTLIB_RadioBroadcast(self, NULL, "2"); break; + case 3: BOTLIB_RadioBroadcast(self, NULL, "3"); break; + case 4: BOTLIB_RadioBroadcast(self, NULL, "4"); break; + case 5: BOTLIB_RadioBroadcast(self, NULL, "5"); break; + case 6: BOTLIB_RadioBroadcast(self, NULL, "6"); break; + case 7: BOTLIB_RadioBroadcast(self, NULL, "7"); break; + case 8: BOTLIB_RadioBroadcast(self, NULL, "8"); break; + case 9: BOTLIB_RadioBroadcast(self, NULL, "9"); break; + default: break; + }*/ + + // Radio enemy down + BOTLIB_RadioBroadcast(self, NULL, "enemyd"); // Sound + } + } +} diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c new file mode 100644 index 000000000..81020d37d --- /dev/null +++ b/src/action/botlib/botlib_ctf.c @@ -0,0 +1,1042 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Does this bot have the flag? +qboolean BOTLIB_Carrying_Flag(edict_t* self) +{ + if (!ctf->value) + return false; + + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == self) + return true; + + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == self) + return true; + + //if (self->client->inventory[items[FLAG_T1_NUM].index] || self->client->inventory[items[FLAG_T2_NUM].index]) + // return true; + return false; +} + +//#undef CTF_AUTO_FLAG_RETURN_TIMEOUT +//#define CTF_AUTO_FLAG_RETURN_TIMEOUT 30 + +// Get the +int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) +{ + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t bmins = { 0,0,0 }; + vec3_t bmaxs = { 0,0,0 }; + + if (ent == NULL) + return INVALID; + + int cloest_node_num = INVALID; + float cloest_node_dist = 99999999; + + for (int j = 0; j < numnodes; j++) + { + VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world + VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world + + float dist = VectorDistance(nodes[j].origin, ent->s.origin); + + // If ent is touching a node + //if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax) || VectorDistance(nodes[j].origin, ent->s.origin) <= 128) + if (dist <= 128) + { + trace_t tr = gi.trace(nodes[j].origin, tv(-16, -16, -8), tv(16, 16, 8), ent->s.origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + //return nodes[j].nodenum; + + if (dist < cloest_node_dist) + { + cloest_node_dist = dist; + cloest_node_num = nodes[j].nodenum; + } + } + } + } + if (cloest_node_num != INVALID) + return cloest_node_num; + + // If not touching a box, try searching via cloest distance to a node + if (1) + { + int i; + float closest = 99999; + float dist; + vec3_t v; + trace_t tr; + float rng; + int node = INVALID; + //vec3_t maxs, mins; + + //VectorCopy(self->mins, mins); + //VectorCopy(self->maxs, maxs); + //mins[2] += 18; // Stepsize + //maxs[2] -= 16; // Duck a little.. + + rng = (float)(NODE_DENSITY * NODE_DENSITY); // square range for distance comparison (eliminate sqrt) + + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + //if (type == NODE_ALL || type == nodes[i].type) // check node type + { + // Get Height Diff + float height = fabs(nodes[i].origin[2] - ent->s.origin[2]); + if (height > 60) // Height difference high + continue; + + VectorSubtract(nodes[i].origin, ent->s.origin, v); // subtract first + + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + + if (dist < closest && dist < rng) + { + tr = gi.trace(ent->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, ent, MASK_PLAYERSOLID); //rekkie + if ((tr.fraction == 1.0) || + ((tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) + ) + { + node = i; + closest = dist; + } + } + } + } + if (node != INVALID) + return nodes[node].nodenum; + } + + return INVALID; +} + +// Intercept flag carrier (good guy and bad guy) +// [OPTIONAL] distance: if bot is within distance +// Returns the node nearest to the carrier +int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance) +{ + if (distance > 0) + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + else + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + + return INVALID; +} + +// Is flag at home? +// [OPTIONAL] distance: if bot is within distance +qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance) +{ + if (team == TEAM1 && self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == true) + { + if (distance > 0) + { + if (BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + else + { + //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + if (team == TEAM2 && self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == true) + { + if (distance > 0) + { + if (BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + else + { + //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + + return false; +} + +// Is Red or Blue flag dropped to ground? +// Which team flag to check if dropped +// [OPTIONAL] if distance > 0: Checks bot is within distance +qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance) +{ + if (self->client->resp.team == TEAM1) + { + if (distance > 0) + { + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + } + else + { + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + } + } + else if (self->client->resp.team == TEAM2) + { + if (distance > 0) + { + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + } + else + { + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + } + } + + return false; +} + +// Returns the distance to flagType (FLAG_T1_NUM, FLAG_T2_NUM) +float BOTLIB_DistanceToFlag(edict_t* self, int flagType) +{ + // If carried by a player + if (bot_ctf_status.player_has_flag1) + return VectorDistance(bot_ctf_status.player_has_flag1->s.origin, self->s.origin); + if (bot_ctf_status.player_has_flag2) + return VectorDistance(bot_ctf_status.player_has_flag2->s.origin, self->s.origin); + + // If on ground + if (flagType == FLAG_T1_NUM && bot_ctf_status.flag1_curr_node > INVALID) + return VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + if (flagType == FLAG_T2_NUM && bot_ctf_status.flag2_curr_node > INVALID) + return VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + + return 9999999; // Could not find distance +} + +// Gets the nearest flag +int BOTLIB_NearestFlag(edict_t *self) +{ + if (bot_ctf_status.flag1_home_node == INVALID || bot_ctf_status.flag2_home_node == INVALID) + return 0; + + float flag1_dist = 999999999; + float flag2_dist = 999999999; + + if (bot_ctf_status.flag1_is_dropped) + flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + else + flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); + + if (bot_ctf_status.flag2_is_dropped) + flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + else + flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); + + qboolean closer_to_flag1 = flag1_dist < flag1_dist ? true : false; + if (closer_to_flag1) + return FLAG_T1_NUM; + else + return FLAG_T2_NUM; + + /* + float dist_to_flag1_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); + float dist_to_flag2_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); + qboolean closer_to_flag1 = dist_to_flag1_home < dist_to_flag2_home ? true : false; + if (closer_to_flag1) + return FLAG_T1_NUM; + else + return FLAG_T2_NUM; + */ +} + +// Get the status of flags +void BOTLIB_Update_Flags_Status(void) +{ + // Check if any players are carrying a flag + // Returns INVALID if no enemy has the flag + bot_ctf_status.player_has_flag1 = NULL; + bot_ctf_status.player_has_flag2 = NULL; + for (int p = 0; p < num_players; p++) + { + if (players[p]->client->inventory[items[FLAG_T1_NUM].index]) + bot_ctf_status.player_has_flag1 = players[p]; + + if (players[p]->client->inventory[items[FLAG_T2_NUM].index]) + bot_ctf_status.player_has_flag2 = players[p]; + } + + bot_ctf_status.flag1 = NULL; + bot_ctf_status.flag2 = NULL; + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag1_is_dropped = false; + bot_ctf_status.flag2_is_dropped = false; + bot_ctf_status.flag1_is_carried = false; + bot_ctf_status.flag2_is_carried = false; + + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + + // When at home: + // Spawned: 0 0 1 0 -- 0, 0, SOLID_TRIGGER, 0 + // Returned: 80000000 0 1 40000 -- FL_RESPAWN, 0, SOLID_TRIGGER, ITEM_TARGETS_USED + // + // When picked up: + // Picked: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // + // When dropped after being picked up: + // Dropped: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // Dropped: 0 0 1 10000 -- 0, 0, SOLID_TRIGGER, DROPPED_ITEM + + + if (ent->typeNum == FLAG_T1_NUM) + { + //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag1 = ent; + bot_ctf_status.flag1_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag1_home_node = node_added; + Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + */ + + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag1_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = true; + } + + // Picked up + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_carried = true; + if (bot_ctf_status.flag2_home_node != INVALID && bot_ctf_status.player_has_flag1 != NULL) + bot_ctf_status.team2_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, bot_ctf_status.player_has_flag1->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_dropped = true; + } + } + + if (ent->typeNum == FLAG_T2_NUM) + { + //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag2 = ent; + bot_ctf_status.flag2_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag2_home_node = node_added; + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + */ + + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = true; + } + + // Picked + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_carried = true; + if (bot_ctf_status.flag1_home_node != INVALID && bot_ctf_status.player_has_flag2 != NULL) + bot_ctf_status.team1_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, bot_ctf_status.player_has_flag2->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_dropped = true; + } + + // Flag home moved + if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_CTF_Get_Flag_Node(ent)) + { + //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); // Update home node + } + } + + + + //if (ent->solid == SOLID_NOT) continue; // picked up + } + //Com_Printf("%s\n", __func__); +} + +void BOTLIB_CTF_Goals(edict_t* self) +{ + if (team_round_going == false || lights_camera_action) return; // Only allow during a real match (after LCA and before win/loss announcement) + + // Get flag + if (self->bot.bot_ctf_state != BOT_CTF_STATE_GET_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag2_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy blue flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag1_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy red flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Take flag home, if home flag is home + if (BOTLIB_Carrying_Flag(self)) + { + if (self->bot.bot_ctf_state != BOT_CTF_STATE_CAPTURE_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_ATTACK_ENEMY_CARRIER && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag1_home_node; + else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag2_home_node; + + // If team flag is dropped and nearby, don't head to home. Allow bot to pickup the dropped team flag. + if (self->client->resp.team == TEAM1 && bot_ctf_status.flag1_is_dropped && VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, red flag was dropped nearby\n", __func__, self->client->pers.netname); + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.flag2_is_dropped && VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, blue flag was dropped nearby\n", __func__, self->client->pers.netname); + } + + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && bot_connections.total_team1 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && bot_connections.total_team2 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + //Com_Printf("%s %s goal_node[%d] flag1_home_node[%i] state[%d]\n", __func__, self->client->pers.netname, self->bot.goal_node, bot_ctf_status.flag1_home_node, self->state); + //if (n != INVALID) Com_Printf("%s %s self->bot.goal_node %i flag node %i\n", __func__, self->client->pers.netname, self->bot.goal_node, n); + + if (BOTLIB_CanGotoNode(self, n, false)) + { + if (self->client->resp.team == TEAM1) + { + //Com_Printf("%s %s Taking blue flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin)); + } + else + { + //Com_Printf("%s %s Taking red flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin)); + } + self->bot.bot_ctf_state = BOT_CTF_STATE_CAPTURE_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Bot was on its way to the enemy flag, but got picked up by a team member before the bot could reach it. Therefore, try to support the flag carrrier who got it. + if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2) + { + //Com_Printf("%s %s Red flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1) + { + //Com_Printf("%s %s Blue flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + } + + // Continue supporting flag carrier + if (self->bot.bot_ctf_state != BOT_CTF_STATE_COVER_FLAG_CARRIER && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team1_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting blue flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, bot_ctf_status.team1_carrier_dist_to_home); + + + // Support flag carrier if they're away from the home flag + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team2_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting red flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, bot_ctf_status.team2_carrier_dist_to_home); + + // Support flag carrier if they're around + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + } + + + + + // Retrieve dropped T1/T2 flags if nearby + if (self->client->resp.team == TEAM1) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Dropped team flag + //Com_Printf("%s %s [team] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + else + { + flag_to_get = FLAG_T2_NUM; // Dropped enemy flag + //Com_Printf("%s %s [enemy] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + } + else if (bot_ctf_status.flag1_is_dropped) // Dropped team flag + flag_to_get = FLAG_T1_NUM; + else if (bot_ctf_status.flag2_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T2_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T1_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T2_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + if (self->client->resp.team == TEAM2) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Dropped team flag + Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + else + { + flag_to_get = FLAG_T1_NUM; // Dropped enemy flag + Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + } + else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag + flag_to_get = FLAG_T2_NUM; + else if (bot_ctf_status.flag1_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T1_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T2_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T1_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + + + // When the bot is close to the flag from following nodes, flags are not always on a node exactly, + // therefore when the bot gets close enough to the flag, force move the bot toward the flag. + if (self->bot.goal_node == INVALID) + { + + //if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_CAPTURE_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + // TEAM 1 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 1 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag2 == false && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag1 == false && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped RED flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped BLUE flag + if (bot_ctf_status.flag2_is_dropped && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + } + + // Check if bot is closer to enemy flag carrier or the enemy flag at home + int flag_to_get = 0; + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1) + { + if (bot_ctf_status.flag2 && bot_ctf_status.flag2_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to blue flag than blue enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + } + else + flag_to_get = FLAG_T1_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T1_NUM; // If both flags gone, go after enemy carrier + } + else if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2) + { + if (bot_ctf_status.flag1 && bot_ctf_status.flag1_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to red flag than red enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + } + else + flag_to_get = FLAG_T2_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T2_NUM; // If both flags gone, go after enemy carrier + } + + // Intercept enemy flag carrier if we're closer to them than the enemy flag + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && flag_to_get == FLAG_T1_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team1 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; //bot_ctf_status.flag2_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && flag_to_get == FLAG_T2_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team2 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our blue flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; //bot_ctf_status.flag1_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + self->bot.ctf_support_time = 0; + + } + + /* + // Gather nearby weapons, ammo, and items + if (self->bot.bot_ctf_state == BOT_CTF_STATE_NONE && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ITEMS) + { + int item_node = BOTLIB_GetEquipment(self); + if (item_node != INVALID) + { + if (BOTLIB_CanVisitNode(self, nodes[item_node].nodenum, false)) + { + self->bot.state = BOT_MOVE_STATE_MOVE; + BOTLIB_SetGoal(self, nodes[item_node].nodenum); + self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ITEMS; + //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[item_node].nodenum); + return; + } + } + } + */ + + +} \ No newline at end of file diff --git a/src/action/botlib/botlib_items.c b/src/action/botlib/botlib_items.c new file mode 100644 index 000000000..6fcebe588 --- /dev/null +++ b/src/action/botlib/botlib_items.c @@ -0,0 +1,257 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Smarter weapon + item choices +void BOTLIB_SmartWeaponSelection(edict_t *self) +{ + // Let bots adjust its own skill + // Skill helps dictate the weapon and item the bot will pick + int weapon_skill_choice = 0; + + if (bot_skill_threshold->value > 0) + weapon_skill_choice = BOTLIB_AutoAdjustSkill(self); + else + self->bot.skill = bot_skill->value; + + // Allowed weapons: ALL + // Allowed items: ALL + if (weapon_skill_choice == 0) + { + int weaponchoice = 0; // Weapon choice + int highchoice = rand() % 10; // 0..9 + if (highchoice < 7) // 70% chance of ONLY primary weapon + weaponchoice = rand() % 5; + else // 30% chance of ANY weapon (including akimbos and knives) + weaponchoice = rand() % 7; + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 5; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 3) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 3: + ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); // HC + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 4: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 5: + ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); // Dual MK23 + itemchoice = rand() % 3; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 6: + ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives + itemchoice = rand() % 3; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. + break; + } + } + + + // Allowed weapons: MP5, M4, M3, Sniper + // Allowed items: ALL + else if (weapon_skill_choice == 1) + { + int weaponchoice = rand() % 4; // MP5, M4, M3, Sniper + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 5; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 3) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 3: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. + break; + } + } + + // Allowed weapons: MP5, M4, Sniper + // Allowed items: Kevlar Vest, Laser + else if (weapon_skill_choice == 2) + { + int weaponchoice = rand() % 3; // MP5, M4, M3, Sniper + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 2; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 2; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + break; + } + } + + + //Com_Printf("%s %s wep:%s item:%s\n", __func__, self->client->pers.netname, self->client->pers.chosenWeapon, self->client->pers.chosenItem); + + //self->weaponchoice = 8; // Knife + //self->equipchoice = 12; // Bandolier + + // Force bots use grenades only + //ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + + //ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + //self->client->pers.chosenItem = FindItemByNum(LASER_NUM); + //self->client->inventory[ITEM_INDEX(GET_ITEM(LASER_NUM))] = 1; + + //ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + + //ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); + + //ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); + //ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); + //ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); +} \ No newline at end of file diff --git a/src/action/botlib/botlib_math.c b/src/action/botlib/botlib_math.c new file mode 100644 index 000000000..517d02f62 --- /dev/null +++ b/src/action/botlib/botlib_math.c @@ -0,0 +1,154 @@ +#include "../g_local.h" + +// Math helper functions, utilities and other useful math based tools + + +// Functionally this works the same as #define VectorCompare(), with the exception of an epsilon +// Compare two vectors and return true if they are the same within a certain epsilon +qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon) +{ + for (int i = 0; i < 3; i++) + { + if (fabs(v1[i] - v2[i]) > epsilon) + { + return false; + } + } + return true; +} + +// Originally from Quake 2 Rerelease source code +// Returns true if two boxes intersect +bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs) +{ + return amins[0] <= bmaxs[0] && amaxs[0] >= bmins[0] && + amins[1] <= bmaxs[1] && amaxs[1] >= bmins[1] && + amins[2] <= bmaxs[2] && amaxs[2] >= bmins[2]; +} + + +qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + vec3_t forward = { 0.f,0.f,0.f }; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(other->s.origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + return true; + + return false; +} + +// Test if bot is facing toward an origin +qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + vec3_t forward = { 0.f,0.f,0.f }; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + return true; + + return false; +} + +// Test if bot is walking toward an origin +qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + //vec3_t forward = { 0.f,0.f,0.f }; + + // Get the player's direction based from their velocity + vec3_t walkdir; + VectorSubtract(self->s.origin, self->bot.last_position, walkdir); + //VectorCopy(self->s.origin, self->bot.last_position); + VectorNormalize(walkdir); + vec3_t angle, forward, right; + vectoangles(walkdir, angle); + //VectorCopy(self->s.angles, angle); // Use the player's view angles (not their walk direction) + angle[0] = 0; + AngleVectors(angle, forward, right, NULL); + + + //AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + { + //Com_Printf("%s %s moving toward [%f]\n", __func__, self->client->pers.netname, dot); + return true; + } + + return false; +} + + +// True if [self] is aiming at [other] +// This func uses box intersection to determine if the player is aiming at the other entity +qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other) +{ + // Project trace from the player's weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, self->viewheight - 8); + G_ProjectSource(self->s.origin, offset, forward, right, start); + float dist = VectorDistance(self->s.origin, other->s.origin); + VectorMA(start, dist, forward, end); + + // Adjust the mins/maxs box size based on size + //vec3_t bmins = { -24, -24, +6 }; + //vec3_t bmaxs = { 24, 24, +10 }; + vec3_t bmins = { -32, -32, -16 }; + vec3_t bmaxs = { 32, 32, +16 }; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + VectorAdd(end, bmins, absmin); + VectorAdd(end, bmaxs, absmax); + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(other->s.number + (rand() % 256 + 1), other->s.origin, blue, other->mins, other->maxs, 100, false); // Player box + DrawBox(self->s.number + (rand() % 256 + 1), end, yellow, bmins, bmaxs, 100, false); // Projectile box + } +#endif + //rekkie -- debug drawing -- e + + if (BOTLIB_BoxIntersection(absmin, absmax, other->absmin, other->absmax)) // Do boxes intersect? + { + //Com_Printf("%s %s can hit %s\n", __func__, self->client->pers.netname, other->client->pers.netname); + return true; + } + return false; +} \ No newline at end of file diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c new file mode 100644 index 000000000..3c7ed1bd6 --- /dev/null +++ b/src/action/botlib/botlib_movement.c @@ -0,0 +1,7309 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Returns the XYZ distance +float VectorDistance(vec3_t start, vec3_t end) +{ + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + return VectorLength(v); +} +// Returns the XY distance +float VectorDistanceXY(vec3_t start, vec3_t end) +{ + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + v[2] = 0; + return VectorLength(v); +} + +// Converts node type to string +// Types: NODE_MOVE, NODE_CROUCH, NODE_STEP, NODE_JUMP, NODE_JUMPPAD, NODE_STAND_DROP, NODE_CROUCH_DROP, NODE_UNSAFE_DROP, NODE_LADDER_UP, NODE_LADDER_DOWN, NODE_DOOR, NODE_PLATFORM, NODE_TELEPORTER, NODE_ITEM, NODE_WATER, NODE_GRAPPLE, NODE_SPAWNPOINT, NODE_POI, NODE_LEARN +// String: The string to copy the node type to +// Max string size: The maximum size of the string +// Returns: false if node type was not found +qboolean NodeTypeToString(edict_t* self, int type, char *string, const int max_string_size) +{ + int len; + switch (type) + { + case NODE_MOVE: + len = strlen("MOVE"); // Get the length of the string + len = len < max_string_size ? len : max_string_size - 1; // If the string is too long, truncate it + strncpy(string, "MOVE", len); // Copy the string + break; + case NODE_CROUCH: + len = strlen("CROUCH"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "CROUCH", len); + break; + case NODE_BOXJUMP: + len = strlen("NODE_BOXJUMP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "NODE_BOXJUMP", len); + break; + case NODE_JUMP: + len = strlen("JUMP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "JUMP", len); + break; + case NODE_JUMPPAD: + len = strlen("JUMPPAD"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "JUMPPAD", len); + break; + case NODE_LADDER: + len = strlen("LADDER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER", len); + break; + case NODE_POI: + len = strlen("POI"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "POI", len); + break; + case NODE_POI_LOOKAT: + len = strlen("NODE_POI_LOOKAT"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "NODE_POI_LOOKAT", len); + break; + case NODE_WATER: + len = strlen("WATER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "WATER", len); + break; + + + + case NODE_STEP: + len = strlen("STEP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "STEP", len); + break; + case NODE_STAND_DROP: + len = strlen("STAND_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "STAND_DROP", len); + break; + case NODE_CROUCH_DROP: + len = strlen("CROUCH_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "CROUCH_DROP", len); + break; + case NODE_UNSAFE_DROP: + len = strlen("UNSAFE_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "UNSAFE_DROP", len); + break; + case NODE_LADDER_UP: + len = strlen("LADDER_UP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER_UP", len); + break; + case NODE_LADDER_DOWN: + len = strlen("LADDER_DOWN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER_DOWN", len); + break; + case NODE_DOOR: + len = strlen("DOOR"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "DOOR", len); + break; + case NODE_PLATFORM: + len = strlen("PLATFORM"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "PLATFORM", len); + break; + case NODE_TELEPORTER: + len = strlen("TELEPORTER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "TELEPORTER", len); + break; + case NODE_ITEM: + len = strlen("ITEM"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "ITEM", len); + break; + case NODE_GRAPPLE: + len = strlen("GRAPPLE"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "GRAPPLE", len); + break; + case NODE_SPAWNPOINT: + len = strlen("SPAWNPOINT"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "SPAWNPOINT", len); + break; + case NODE_LEARN: + len = strlen("LEARN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LEARN", len); + break; + default: + len = strlen("UNKNOWN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "UNKNOWN", len); + string[len] = '\0'; // Terminate string + return false; // Unknown node type + } + + string[len] = '\0'; // Terminate string + return true; // Success +} + +/////////////////////////////////////////////////////////////////////// +// Bot Movement - Node Type - Utility +// Display all the node type names +// onlyPrintProblemTypes: only output if types are INVALID +/////////////////////////////////////////////////////////////////////// +void PrintAllLinkNodeTypes(edict_t *self, qboolean onlyPrintProblemTypes) +{ + qboolean foundProblem = false; + int curr_node_position = 0; // The position of the current node in the node_list array + + if (self->bot.node_list_count <= 0) // No nodes found + return; + + for (int i = 0; i < self->bot.node_list_count; i++) // If the list has nodes + { + int node = self->bot.node_list[i]; // Next node in the list + if (node != INVALID && node == self->bot.current_node) // If the node is valid + { + curr_node_position = i; // Save the position of the current node + break; + } + } + + if (0) // Debug: print out the node list + { + Com_Printf("%s: count[%d] node_list[", __func__, self->bot.node_list_count); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + Com_Printf(" %d ", self->bot.node_list[i]); + } + Com_Printf("]\n"); + } + + if (0) // Debug: print out the node types + { + int curr_node; + int next_node; + Com_Printf("%s: targetNodeTypes[", __func__); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + if (i + 1 < self->bot.node_list_count) // If there is a next node + { + curr_node = self->bot.node_list[i]; // Get current node + next_node = self->bot.node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); + //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + // foundProblem = true; + break; + } + } + } + } + Com_Printf("]\n"); + } + + char tmp_string[32] = { '\0' }; // Length of the longest node type name + char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each + int tmp_type; + int curr_node; + int next_node; + //Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + if (i + 1 < self->bot.node_list_count) // If there is a next node + { + //Com_Printf(" %d ", node_list[i]); + curr_node = self->bot.node_list[i]; // Get current node + next_node = self->bot.node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + // Add the node number + char tmp_num[9]; // Max size of a node number as a char: "[999999]\0" == (8 characters + NULL terminator = length of 9) + sprintf(tmp_num, "[%d]", self->bot.node_list[i]); + strcat(all_node_types, tmp_num); + + // Add the node type + tmp_type = nodes[curr_node].links[l].targetNodeType; + if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) + foundProblem = true; + strcat(all_node_types, tmp_string); + if (i + 2 < self->bot.node_list_count) + strcat(all_node_types, " "); + break; + } + } + } + } + //Com_Printf("]\n"); + + Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); + + /* + #define MAX_PRINT_ALL_LINK_NODE_TYPES 32 + int node_list[MAX_PRINT_ALL_LINK_NODE_TYPES]; + int node_count = BOTLIB_SLL_Query_All_Nodes(&self->pathList, node_list, MAX_PRINT_ALL_LINK_NODE_TYPES); // Retrieve the nodes in the list, if any + + if (node_count <= 0) // No nodes found + return; + + + if (0) // Debug: print out the node list + { + Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = 0; i < node_count; i++) + { + Com_Printf(" %d ", node_list[i]); + } + Com_Printf("]\n"); + } + + if (0) // Debug: print out the node types + { + int curr_node; + int next_node; + Com_Printf("%s: targetNodeTypes[", __func__); + for (int i = 0; i < node_count; i++) + { + if (i + 1 < node_count) // If there is a next node + { + curr_node = node_list[i]; // Get current node + next_node = node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); + //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + // foundProblem = true; + break; + } + } + } + } + Com_Printf("]\n"); + } + + + char tmp_string[32] = { '\0' }; // Length of the longest node type name + char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each + int tmp_type; + int curr_node; + int next_node; + //Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = 0; i < node_count; i++) + { + if (i + 1 < node_count) // If there is a next node + { + //Com_Printf(" %d ", node_list[i]); + curr_node = node_list[i]; // Get current node + next_node = node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + // Add the node number + char tmp_num[6]; // Max size of a node number as a char: 999,999 (6 chars) + sprintf(tmp_num, "[%d]", node_list[i]); + strcat(all_node_types, tmp_num); + + // Add the node type + tmp_type = nodes[curr_node].links[l].targetNodeType; + if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) + foundProblem = true; + strcat(all_node_types, tmp_string); + if (i + 2 < node_count) + strcat(all_node_types, " "); + break; + } + } + } + } + //Com_Printf("]\n"); + + Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); + */ + +#if 0 + char curr_type[64]; + char next_type[64]; + if (NodeTypeToString(self, type_from, curr_type, sizeof(next_type)) == false) + foundProblem = true; + if (NodeTypeToString(self, type_to, next_type, sizeof(next_type)) == false) + foundProblem = true; + + // -------------------------------------------- + char after_next_type[64]; + after_next_type[0] = '\0'; + //int node_after_next = BOTLIB_SLL_Query_All_Nodes(&self->pathList); // Find out what node comes after the next node, if any + if (node_after_next == INVALID) + { + strcpy(after_next_type, "INVALID"); // No next node + } + else + { + + qboolean found_link_type = false; + if (self->bot.next_node != INVALID) + { + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[self->bot.next_node].links[l].targetNode == node_after_next) + { + Com_Printf("%s: targetNodeType[%d]\n", __func__, nodes[self->bot.next_node].links[l].targetNodeType); + found_link_type = true; + if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + foundProblem = true; + break; + } + } + } + if (found_link_type == false) + strcpy(after_next_type, "INVALID"); // No next node + + + //Com_Printf("%s: cur[%d] nxt[%d] nxt_nxt[%d]\n", __func__, self->bot.current_node, self->bot.next_node, node_after_next); + if (NodeTypeToString(self, nodes[node_after_next].type, after_next_type, sizeof(after_next_type)) == false) + foundProblem = true; + } + // -------------------------------------------- + + if (onlyPrintProblemTypes) // Only print if errors found + { + if (foundProblem == false) // No error, so bail + return; + } + + if (self->groundentity) + Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [GROUND %d]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground); + else + Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [AIR %d] [ZVEL %f]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground, self->velocity[2]); +#endif +} + +/////////////////////////////////////////////////////////////////////// +// Changes the bots view angle +// +// move_vector: The move direction +// +// range: How close the current yaw needs to be to accepted as ideal +// <= 0: The bot will snap turn instantly +// > 0: Makes the change in angles a little more gradual, +// not so snappy. Subtle, but noticeable. +// Default: 2 +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_UTIL_ChangeBotAngleYaw(edict_t* ent, vec3_t move_vector, float range) +{ + float ideal_yaw; + float current_yaw; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(move_vector); + + current_yaw = anglemod(ent->s.angles[YAW]); + + vectoangles(move_vector, ideal_angle); + + ideal_yaw = anglemod(ideal_angle[YAW]); + + if (current_yaw == ideal_yaw) + return true; + + if (range <= 0) // Instant turn + { + ent->s.angles[YAW] = ideal_yaw; + return true; + } + else + { + // Turn speed based on angle of curr vs ideal + // If the turn angle is large, the bot will instantly snap + // This helps prevent circular motion when very close to a node + yaw_move = ideal_yaw - current_yaw; + if (ideal_yaw > current_yaw) + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); + if (fabs(yaw_move) >= 45.0) // Snap turn + { + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); + ent->s.angles[YAW] = ideal_yaw; + return false; + } + /* + float normal = 0; + if (current_yaw > ideal_yaw) + { + normal = (ideal_yaw + 1) / (current_yaw + 1); + } + else + { + normal = (current_yaw + 1) / (ideal_yaw + 1); + } + if (normal < 0.5) // Snap turn when angle is large + { + Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, normal); + ent->s.angles[YAW] = ideal_yaw; + return true; + } + */ + + // Yaw turn speed + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + + //rekkie -- s + const float turn_speed_factor = 1.25; // Slow down turn speed + move_ratio /= turn_speed_factor; // Slow turn speed by a factor of + //rekkie -- e + + ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + + // Check if current_yaw is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw_move < range && yaw_move > -(range)) + return true; // Ideal yaw reached + else + return false; // Ideal yaw not yet reached + + } +} + +// Change in angles can be gradual to snappy +// move_vector : The move direction +// instant : Instant 'snap' turn +// move_speed : How fast to turn, 1.0 is fast and 3.0 is slow +// yaw : Allow changing the yaw (left & right) +// pitch : Allow changing the pitch (up & down) +// Returns true if angle reached, or false if still moving toward angle +qboolean BOTLIB_ChangeBotAngleYawPitch(edict_t* ent, vec3_t move_vector, qboolean instant, float move_speed, qboolean yaw, qboolean pitch) +{ + float ideal_yaw; + float ideal_pitch; + float current_yaw; + float current_pitch; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float pitch_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(move_vector); + vectoangles(move_vector, ideal_angle); + + current_yaw = anglemod(ent->s.angles[YAW]); + current_pitch = anglemod(ent->s.angles[PITCH]); + + ideal_yaw = anglemod(ideal_angle[YAW]); + ideal_pitch = anglemod(ideal_angle[PITCH]); + + // Raptor007: Compensate for M4 climb. + if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) + ideal_pitch -= ent->client->kick_angles[PITCH]; + + // Yaw + if (instant) // Instant turn + { + ent->bot.bi.viewangles[YAW] = ideal_yaw; + } + else + { + // Turn speed based on angle of curr vs ideal + // If the turn angle is large, the bot will instantly snap + // This helps prevent circular motion when very close to a node + yaw_move = ideal_yaw - current_yaw; + if (ideal_yaw > current_yaw) + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); + //if (fabs(yaw_move) >= 45.0) // Snap turn + //{ + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); + //ent->bot.bi.viewangles[YAW] = ideal_yaw; + //return false; + //} + + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + //Com_Printf("%s %s speed:%f\n", __func__, ent->client->pers.netname, speed); + speed *= (360 / fabs(yaw_move)); // Variable speed. Far away = faster turning. Close = slower turnung. + //Com_Printf("%s %s variable speed:%f\n", __func__, ent->client->pers.netname, speed); + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + } + + + // Pitch + if (instant) // Instant turn + { + ent->bot.bi.viewangles[PITCH] = ideal_pitch; + } + + //if (fabs(pitch_move) >= 45.0) // Snap turn + //{ + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(pitch_move)); + //ent->bot.bi.viewangles[PITCH] = pitch_move; + //return false; + //} + + if (current_pitch != ideal_pitch) + { + pitch_move = ideal_pitch - current_pitch; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + speed *= (360 / fabs(pitch_move)); // Variable speed. Far away = faster turning. Close = slower turnung. + if (ideal_pitch > current_pitch) + { + if (pitch_move >= 180) + pitch_move = pitch_move - 360; + } + else + { + if (pitch_move <= -180) + pitch_move = pitch_move + 360; + } + if (pitch_move > 0) + { + if (pitch_move > speed) + pitch_move = speed; + } + else + { + if (pitch_move < -speed) + pitch_move = -speed; + } + } + + if (instant) // Instant turn + return true; + + /* + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + */ + + //move_ratio = 1; + move_ratio = 10.0 / HZ; + //Com_Printf("%s %s move_ratio[%f]\n", __func__, ent->client->pers.netname, move_ratio); + + //rekkie -- s + if (move_speed < 0.0) + move_speed = 0.0; + if (move_speed > 3.0) + move_speed = 3.0; + //float output = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + //Com_Printf("%s %s move_speed: %f\n", __func__, ent->client->pers.netname, move_speed); + float turn_speed_factor = 1 + move_speed; //2.25; // Slow down turn speed + move_ratio /= turn_speed_factor; // Slow turn speed by a factor of + //rekkie -- e + + if (yaw) + ent->bot.bi.viewangles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + if (pitch) + ent->bot.bi.viewangles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); + + // Check if yaw_move and pitch_move is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw && pitch && yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + { + if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + return true; // Ideal reached + else + return false; + } + if (yaw && !pitch) + { + if (yaw_move < 2 && yaw_move > -2) + return true; // Ideal reached + else + return false; + } + if (!yaw && pitch) + { + if (pitch_move < 2 && pitch_move > -2) + return true; // Ideal reached + else + return false; + } + + return false; +} + +/////////////////////////////////////////////////////////////////////// +// Checks if bot can move (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +// Can move safely at angle +qboolean BOTLIB_CanMoveAngle(edict_t* self, vec3_t angle) +{ + vec3_t forward, right; + vec3_t offset, start, end; + trace_t tr; + + // Set up the vectors + AngleVectors(angle, forward, right, NULL); + + VectorSet(offset, 32, 0, 0); + G_ProjectSource(self->s.origin, offset, forward, right, start); + + //VectorSet(offset, 32, 0, -32); // RiEvEr reduced drop distance to -110 NODE_MAX_FALL_HEIGHT + //G_ProjectSource(self->s.origin, offset, forward, right, end); + VectorCopy(start, end); + end[2] -= 32; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction == 1.0) + return false; + + /* + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angle))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; // can't move + } + */ + + return true; // yup, can move +} +// Can move safely in direction +qboolean BOTLIB_CanMoveInDirection(edict_t* self, vec3_t direction, float fwd_dist, float down_dist, qboolean rand_fwd_angle) +{ + trace_t tr; + vec3_t forward, right, start, end, offset, angles; + qboolean safe_forward = false; + qboolean safe_downward = false; + + vectoangles(direction, angles); // Direction to angles + + if (rand_fwd_angle) // Randomize the forward angle + { + angles[0] = 0; + angles[1] += 22.5 + (random() * 270); // Get a random angle + angles[2] = 0; + } + + // Get forward start & end + AngleVectors(angles, forward, right, NULL); // Angles to vectors + VectorSet(offset, 0, 0, self->viewheight); + G_ProjectSource(self->s.origin, offset, forward, right, start); // Get projection + VectorMA(start, fwd_dist, forward, end); // Project forward + + // Test forward + tr = gi.trace(start, tv(-16, -16, -5), tv(-16, -16, 32), end, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.fraction == 1.0 && tr.startsolid == false) // No obstruction + { + safe_forward = true; // Way is safe forward + } + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + // Test downward + VectorCopy(end, start); // Make the end the start + end[2] -= down_dist; // Move the end down + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction < 1.0 && tr.startsolid == false) // Found the ground + { + safe_downward = true; // Way is safe downward + } + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + // Convert angles back to direction + AngleVectors(angles, direction, NULL, NULL); + + if (safe_forward && safe_downward) + return true; + else + return false; +} +// Can move safely in direction +qboolean BOTLIB_CanMoveDir(edict_t* self, vec3_t direction) +{ + vec3_t forward, right; + vec3_t offset, start, end; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(direction, angles); + VectorNormalize(angles); + vectoangles(angles, angles); + + // Set up the vectors + AngleVectors(angles, forward, right, NULL); + + VectorSet(offset, 36, 0, 24); // 24 + G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorSet(offset, 36, 0, -NODE_MAX_FALL_HEIGHT); //-NODE_MAX_FALL_HEIGHT); // -110 + G_ProjectSource(self->s.origin, offset, forward, right, end); + + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); // Solid, lava, and slime + + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; // can't move + } + + return true; // yup, can move +} +//rekkie -- Quake3 -- e + +/////////////////////////////////////////////////////////////////////// +// Make the change in angles a little more gradual, not so snappy +// Subtle, but noticeable. +// +// Modified from the original id ChangeYaw code... +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_MOV_ChangeBotAngleYawPitch(edict_t* ent) +{ + float ideal_yaw; + float ideal_pitch; + float current_yaw; + float current_pitch; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float pitch_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(ent->move_vector); + + current_yaw = anglemod(ent->s.angles[YAW]); + current_pitch = anglemod(ent->s.angles[PITCH]); + + vectoangles(ent->move_vector, ideal_angle); + + ideal_yaw = anglemod(ideal_angle[YAW]); + ideal_pitch = anglemod(ideal_angle[PITCH]); + + // Raptor007: Compensate for M4 climb. + if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) + ideal_pitch -= ent->client->kick_angles[PITCH]; + + // Yaw + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + + // Pitch + if (current_pitch != ideal_pitch) + { + pitch_move = ideal_pitch - current_pitch; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_pitch > current_pitch) + { + if (pitch_move >= 180) + pitch_move = pitch_move - 360; + } + else + { + if (pitch_move <= -180) + pitch_move = pitch_move + 360; + } + if (pitch_move > 0) + { + if (pitch_move > speed) + pitch_move = speed; + } + else + { + if (pitch_move < -speed) + pitch_move = -speed; + } + } + + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + + ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + ent->s.angles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); + + // Check if yaw_move and pitch_move is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + return true; // Ideal yaw reached + else + return false; +} + +/////////////////////////////////////////////////////////////////////// +// Handle special cases of crouch/jump +// +// If the move is resolved here, this function returns true +/////////////////////////////////////////////////////////////////////// +qboolean Botlib_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd) +{ + vec3_t dir, forward, right, start, end, offset; + vec3_t top; + trace_t tr; + + // Get current direction + VectorCopy(self->client->ps.viewangles, dir); + dir[YAW] = self->s.angles[YAW]; + AngleVectors(dir, forward, right, NULL); + + VectorSet(offset, 0, 0, 0); // changed from 18,0,0 + G_ProjectSource(self->s.origin, offset, forward, right, start); + offset[0] += 18; + G_ProjectSource(self->s.origin, offset, forward, right, end); + + + start[2] += 18; // so they are not jumping all the time + end[2] += 18; + tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + + + if (tr.fraction < 1.0) + { + //rekkie -- DEV_1 -- s + // Handles cases where bots get stuck on each other, for example in teamplay where they can block each other + // So lets try to make them jump over each other + if (tr.ent && tr.ent->is_bot && self->groundentity) + { + if (random() > 0.5) // try jumping + { + if (debug_mode && level.framenum % HZ == 0) + debug_printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); + self->velocity[2] += 400; + //ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return true; + } + else // try crouching + { + if (debug_mode && level.framenum % HZ == 0) + debug_printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); + ucmd->upmove = -SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return true; + } + } + //rekkie -- DEV_1 -- e + + // Check for crouching + start[2] -= 14; + end[2] -= 14; + VectorCopy(self->maxs, top); + top[2] = 0.0; // crouching height + tr = gi.trace(start, self->mins, top, end, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + if (debug_mode) debug_printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = -SPEED_RUN; + return true; + } + + // Check for jump + start[2] += 32; + end[2] += 32; + tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + if (tr.fraction == 1.0) + { + if (debug_mode) debug_printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + return true; + } + } + + return false; // We did not resolve a move here +} + +//rekkie -- Quake3 -- s +// Special cases of crouch/jump when not using nodes +// Also resolves jumping or crouching under other players/bots in our way +// Returns the action taken: ACTION_JUMP, ACTION_CROUCH, ACTION_NONE +int BOTLIB_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd, vec3_t dir) +{ + vec3_t angle, forward, right, start, end, offset, origin; + trace_t tr; + qboolean crouch = false, head = false, jump = false; // Crouch space, head space, jump space + + // Get current direction + vectoangles(dir, angle); + AngleVectors(angle, forward, right, NULL); + VectorCopy(self->s.origin, origin); + origin[2] -= 24; // From the ground up + + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + + // Check if we hit our head + tr = gi.trace(tv(start[0], start[1], start[2] + 56), tv(-15, -15, 0), tv(15, 15, 1), tv(start[0], start[1], start[2] + 57), self, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + head = true; // Hit our head on something immediately above us + } + + //vec3_t mins = { self->mins[0], self->mins[1], self->mins[2] - 0.15 }; + //vec3_t maxs = { self->maxs[0], self->maxs[1], self->maxs[2] }; + + + tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, 56), end, self, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + + // Handle cases where bots get stuck on each other, for example in teamplay where they can block each other + // So lets try to make them jump or crouch + if (tr.ent && tr.ent->is_bot && self->groundentity) + { + if (random() > 0.5) // try jumping + { + //if (debug_mode && level.framenum % HZ == 0) + // Com_Printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); + return ACTION_JUMP; + } + else // try crouching + { + //if (debug_mode && level.framenum % HZ == 0) + // Com_Printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); + return ACTION_CROUCH; + } + } + + // Handle geometry cases + // Check for crouching + tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, STEPSIZE), end, self, MASK_PLAYERSOLID); //CROUCHING_MAXS2, 32 -- + if (tr.fraction == 1.0) + { + crouch = true; // We can crouch under something + } + + // Check for jump + tr = gi.trace(start, tv(-16,-16, 60), tv(16, 16, 61), end, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + if (ACEMV_CanMove(self, MOVE_FORWARD)) + jump = true; // We can jump over something + } + + //Com_Printf("%s %s crouch[%d] head[%d] jump[%d]\n", __func__, self->client->pers.netname, crouch, head, jump); + + if ((crouch || head) && jump == false) + { + //Com_Printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); + return ACTION_CROUCH; + } + if (jump && (crouch == false && head == false)) + { + //Com_Printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); + return ACTION_JUMP; + } + } + + return ACTION_NONE; // We did not resolve a move here +} + +// Checks the bots movement direction against the direction of the next node. +// +int BOTLIB_DirectionCheck(edict_t *ent, byte *mov_strafe) +{ + if (ent->bot.next_node == INVALID) + return 0; + + // Check velocity + if (VectorEmpty(ent->velocity)) + return 0; + + // Get the bot's direction based from their velocity + vec3_t walkdir; + VectorSubtract(ent->s.origin, ent->lastPosition, walkdir); + VectorNormalize(walkdir); + vec3_t angle, forward, right, start, end, origin, offset; + vectoangles(walkdir, angle); + //VectorCopy(ent->s.angles, angle); // Use the bots's view angles (not their walk direction) + AngleVectors(angle, forward, right, NULL); + + VectorCopy(forward, ent->bot.bot_walk_dir); // Copy the forward walk direction + + + // Calculate the direction from the bot to the node + vec3_t node_dir; + VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, node_dir); + VectorNormalize(node_dir); + + // Calculate the dot product of the forward dir and the node dir + float dot = DotProduct(forward, node_dir); + // Calculate the dot to degrees + //float degrees = acos(dot) * 180 / M_PI; + //Com_Printf("%s node %d dot %f degrees %f\n", __func__, ent->bot.next_node, dot, degrees); + //if (dot > 0.995) + // Com_Printf("%s node %d dot %f\n", __func__, ent->bot.next_node, dot); + + float dot_right = DotProduct(right, node_dir); + VectorNegate(right, right); + float dot_left = DotProduct(right, node_dir); + if (dot_right > dot_left) // + { + //Com_Printf("%s [MOVE RIGHT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + *mov_strafe = 1; + } + else + { + //Com_Printf("%s [MOVE LEFT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + *mov_strafe = -1; + } + + if (0) // Show visual debug + { + VectorCopy(ent->s.origin, origin); + origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) + + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1024; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + + // Bot walk direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(ent->s.origin, MULTICAST_PHS); + + /* + // Bot to node direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(nodes[ent->bot.next_node].origin); + gi.multicast(ent->s.origin, MULTICAST_PHS); + */ + } + + return dot; +} +//rekkie -- Quake3 -- e + +// Distance types from origin [self, current node, next node] +enum { + BOT_DIST_XYZ_SELF_CURR = 1, // [XYZ] Self -> Current node + BOT_DIST_XYZ_SELF_NEXT, // [XYZ] Self -> Next node + BOT_DIST_XYZ_CURR_NEXT, // [XYZ] Current node -> Next node + + BOT_DIST_XY_SELF_CURR, // [XY] Self -> Current node + BOT_DIST_XY_SELF_NEXT, // [XY] Self -> Next node + BOT_DIST_XY_CURR_NEXT // [XY] Current node -> Next node +}; +// Returns the distance between two points, either as XYZ or XY +// In the case of XY, the vec3_t dist is also returned with the Z value set to 0 +float BOTLIB_UTIL_Get_Distance(edict_t* self, vec3_t dist, int type) +{ + switch (type) + { + // XYZ + case BOT_DIST_XYZ_SELF_CURR: + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + return VectorLength(dist); + case BOT_DIST_XYZ_SELF_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + return VectorLength(dist); + case BOT_DIST_XYZ_CURR_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); + return VectorLength(dist); + + // XY + case BOT_DIST_XY_SELF_CURR: + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + case BOT_DIST_XY_SELF_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + case BOT_DIST_XY_CURR_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + default: + return 0; + } +} + +// Determines the closest point (between curr -> next node) the bot is nearest to +// +// [bot] +// [curr] -----------[*]---------------------------- [next] +// +// [*] is the origin nearest to the bot along the path from [curr] to [next] +// +void BOTLIB_UTIL_NEAREST_PATH_POINT(edict_t* self, usercmd_t* ucmd) +{ + if (self->bot.next_node == INVALID) return; + + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + + // Get distance from current node to next node + vec3_t curr_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); + float curr_to_next_dist = VectorLength(curr_to_next_vec); + + // Calculate the normalized distance the bot has travelled between current and next node + float bot_travelled_dist = 0; + if (bot_to_next_dist < curr_to_next_dist) + bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); + else + bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); + + //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); + + // Get the origin between the current and next node based on the normalized bot_travelled_dist + LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, self->nearest_path_point); +} + +// Determines if we're off course while traversing a path (node1 ---> node2) +// Useful for strafing to get back on course +// +// Returns: '-1' if we're off course to the right (indicating we should strafe left) +// Returns: '0' if we're on course (or close enough) +// Returns: '1' if we're off course to the left (indicating we should strafe right) +int BOTLIB_UTIL_PATH_DEVIATION(edict_t* self, usercmd_t* ucmd) +{ + if (self->bot.next_node == INVALID) return 0; + /* + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + + // Get distance from current node to next node + vec3_t curr_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); + float curr_to_next_dist = VectorLength(curr_to_next_vec); + + // Calculate the normalized distance the bot has travelled between current and next node + float bot_travelled_dist = 0; + if (bot_to_next_dist < curr_to_next_dist) + bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); + else + bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); + + + //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); + + // Get the origin between the current and next node based on the normalized bot_travelled_dist + vec3_t normalized_origin; + LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, normalized_origin); + + VectorCopy(normalized_origin, self->nearest_path_point); // Make a copy + */ + + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + + // Show a laser between next node and normalized_origin + if (1) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[self->bot.next_node].origin); + gi.WritePosition(self->nearest_path_point); + gi.multicast(nodes[self->bot.next_node].origin, MULTICAST_PVS); + } + + // Calculate the angle between the bot's origin and the normalized origin + vec3_t normalized_origin_vec; + VectorSubtract(self->nearest_path_point, self->s.origin, normalized_origin_vec); + normalized_origin_vec[2] = 0; // Flatten the vector + float bot_to_path_dist = VectorLength(normalized_origin_vec); + //Com_Printf("%s %s [d %f]\n", __func__, self->client->pers.netname, bot_to_path_dist); + + // Calculate the angle between the bot's origin and the normalized origin + float current_yaw = anglemod(self->s.angles[YAW]); + vec3_t ideal_angle; + vectoangles(normalized_origin_vec, ideal_angle); + float ideal_yaw = anglemod(ideal_angle[YAW]); + + // print current_yaw and ideal_yaw + //Com_Printf("%s %s [c %f] [i %f]\n", __func__, self->client->pers.netname, current_yaw, ideal_yaw); + + // Figure out if current yaw is closer to idea yaw if we turn left + //const float less_than_90_degrees = 87; // 90 is the optimum angle, but we want to give a little leeway (otherwise the bot's yaw movement will oscillate) + float left_yaw; + float right_yaw; + + if (bot_to_path_dist > 16) // Give a little leeway (otherwise the bot's yaw movement will oscillate) + { + if (ideal_yaw > current_yaw) + { + right_yaw = ideal_yaw - current_yaw; + left_yaw = 360 - right_yaw; + } + else + { + left_yaw = current_yaw - ideal_yaw; + right_yaw = 360 - left_yaw; + } + + //Com_Printf("%s %s [c %f] [i %f] [ry %f] [ly %f] [dist %f]\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, right_yaw, left_yaw, bot_to_path_dist); + + if (right_yaw < left_yaw) + { + //Com_Printf("%s %s need to strafe left %f\n", __func__, self->client->pers.netname, left_yaw); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> strafe left %f\n", __func__, self->client->pers.netname, left_yaw); + //ucmd->sidemove = -SPEED_WALK; + return -1; // Strafe left + } + } + else + { + //Com_Printf("%s %s need to strafe right %f\n", __func__, self->client->pers.netname, left_yaw); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> strafe right %f\n", __func__, self->client->pers.netname, right_yaw); + //ucmd->sidemove = SPEED_WALK; + return 1; // Strafe right + } + } + } + + return 0; // Strafe neither left or right +} + +// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) +// Useful for strafing around obstacles while following a path +// +// forward_distance: distance to probe forward +// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) +// +// Returns: 'true' if forward vector is clear of obstructions, +// Returns: 'false' if forward vector is obstructed +qboolean BOTLIB_UTIL_PATH_FORWARD(edict_t* self, usercmd_t* ucmd, float forward_distance, float side_distance) +{ + // Send out trace on the left or right side parallel to the current direction the bot is facing + + vec3_t groundvec, end; + vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; + float forward_back, left_or_right, up_or_down, beam_width; + + groundvec[PITCH] = 0; + groundvec[YAW] = self->client->v_angle[YAW]; + groundvec[ROLL] = 0; + + if (forward_distance == 0) + forward_distance = 64; // Distance to probe forward + + AngleVectors(groundvec, forward, NULL, NULL); // Make a forwards pointing vector out of the bot's view angles + VectorMA(self->s.origin, forward_distance, forward, end); // Make end equal to 60* the forward pointing vector + + beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall + forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward + + // +Right or -Left of the bot's current direction + if (side_distance > 0) // side_distance is greater than zero + left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) + else // side_distance is less than zero + left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) + + // +Up or -Down (from the ground) + up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 + + VectorSet(offset, forward_back, left_or_right, up_or_down); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + + if (1) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return true; // Obstructed + } + + return false; // Clear +} + +//rekkie -- Quake3 -- s +// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) +// Useful for strafing around obstacles while following a path +// +// dir: direction vector +// forward_distance: distance to probe forward +// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) +// +// Returns: 'true' if forward vector is clear of obstructions, +// Returns: 'false' if forward vector is obstructed +qboolean BOTLIB_TEST_FORWARD_VEC(edict_t* self, vec3_t dir, float forward_distance, float side_distance) +{ + // Send out trace on the left or right side parallel to the current direction the bot is facing + + vec3_t groundvec; + vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; + float forward_back, left_or_right, up_or_down, beam_width; + + if (forward_distance == 0) + forward_distance = 64; // Distance to probe forward + + beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall + forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward + + // +Right or -Left of the bot's current direction + if (side_distance > 0) // side_distance is greater than zero + left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) + else // side_distance is less than zero + left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) + // +Up or -Down (from the ground) + up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 + + VectorSet(offset, forward_back, left_or_right, up_or_down); + vectoangles(dir, groundvec); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + trace_t tr = gi.trace(start, tv(-4, -4, STEPSIZE+0.01), tv(4, 4, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return true; // Obstructed + } + + return false; // Clear +} +//rekkie -- Quake3 -- e + +/////////////////////////////////////////////////////////////////////// +// Bot Movement (based on node type) +/////////////////////////////////////////////////////////////////////// +void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) +{ +#if 0 + trace_t tr; + float distance = 0; + //float distance_xy; // Distance XYZ and Distance XY + vec3_t dist = { 0 }; + //vec3_t dist_xy = { 0 }; // Distance XYZ and Distance XY + vec3_t velocity; + int i = 0, current_node_type = INVALID, next_node_type = INVALID; + + + + + // Do not follow path when teammates are still inside us. + if (OnTransparentList(self)) + { + //rekkie -- DEV_1 -- s + + // If the bot has just spawned, then we need to wait a bit before we start moving. + if (self->just_spawned) // set by SpawnPlayers() in a_team.c + { + self->just_spawned = false; + self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. + self->bot.state = BOT_MOVE_STATE_MOVE; + if (random() < 0.3) + self->just_spawned_timeout = level.framenum + (random() * 7) * HZ; // Long wait + else if (random() < 0.7) + self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Average wait + else + self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait + return; + } + if (self->just_spawned_go && self->just_spawned_timeout > level.framenum) // Wait + { + return; // It's not time to move yet, wait! + } + else if (self->just_spawned_go) + { + // Now we can move! + self->just_spawned_go = false; + //ACEAI_PickLongRangeGoal(self); // Lets pick a long range goal + return; + } + if (0) // Don't wander + { + //rekkie -- DEV_1 -- e + + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + + //rekkie -- DEV_1 -- s + } + //rekkie -- DEV_1 -- e + } + + // Get current and next node back from nav code. + if (!BOTLIB_FollowPath(self)) + { + if (!teamplay->value || (teamplay->value && level.framenum >= self->teamPauseTime)) + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + else + { + // Teamplay mode - just fan out and chill + if (self->bot.state == STATE_FLEE) + { + self->bot.state = STATE_POSITION; + self->wander_timeout = level.framenum + 3.0 * HZ; + } + else + { + self->bot.state = STATE_POSITION; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + } + self->bot.goal_node = INVALID; + return; + } + + if (self->bot.current_node == INVALID || self->bot.next_node == INVALID) // rekkie -- safey check because current / next node can be INVALID + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + + + + // Current node type + current_node_type = nodes[self->bot.current_node].type; + + // Next node type + // We now get the next node type from the link between current and next node + // Using the link from the current node to the next node, find the next node type + int targetNodeLink = INVALID; // Used to keep track of which link to upgrade to a jumppad + if (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN) + { + //targetNodeLink = 1; + next_node_type = nodes[self->bot.next_node].type; + } + else + { + for (i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + if (nodes[self->bot.current_node].links[i].targetNode == self->bot.next_node) + { + targetNodeLink = i; + next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + + self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type + self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + break; + } + } + } + + //if (next_node_type == NODE_JUMP) + // Com_Printf("%s %s [cn:%i] JUMP_NODE [nn:%i]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node); + + + //rekkie -- DEV_1 -- s + //if (current_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMPPAD) + // gi.dprintf("%s %s [ct:%i] [nt:%i]\n", __func__, self->client->pers.netname, current_node_type, next_node_type); + + // If the next node is high enough, make it a jump + /* + if (next_node_type == NODE_MOVE && self->client->leg_damage == 0) + { + + // See if there is a gap / hole between the current and next node, if so we need to jump across + vec3_t pt_25, pt_50, pt_75; + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_25); // 25% + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_50); // 50% + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_75); // 75% + VectorScale(pt_25, 0.25, pt_25); // Scale 25% + VectorScale(pt_50, 0.50, pt_50); // Scale 50% + VectorScale(pt_75, 0.75, pt_75); // Scale 75% + // Using the points between the current and next node, trace a line down and see if one of them hits air + trace_t tr_25, tr_50, tr_75; + vec3_t end_25, end_50, end_75; + VectorCopy(pt_25, end_25); + VectorCopy(pt_50, end_50); + VectorCopy(pt_75, end_75); + end_25[2] -= NODE_MAX_JUMP_HEIGHT; + end_50[2] -= NODE_MAX_JUMP_HEIGHT; + end_75[2] -= NODE_MAX_JUMP_HEIGHT; + tr_25 = gi.trace(pt_25, vec3_origin, vec3_origin, end_25, self, MASK_SOLID); + tr_50 = gi.trace(pt_50, vec3_origin, vec3_origin, end_50, self, MASK_SOLID); + tr_75 = gi.trace(pt_75, vec3_origin, vec3_origin, end_75, self, MASK_SOLID); + // If one of our trace points hit nothing + if ( (tr_25.fraction == 1.0 && tr_25.allsolid == false) || + (tr_50.fraction == 1.0 && tr_50.allsolid == false) || + (tr_75.fraction == 1.0 && tr_75.allsolid == false) ) + { + next_node_type = NODE_JUMPPAD; + } + + // Otherwise lets see if the next_node is higher than current_node, if so jump to it + //else + { + // Distance between current and next node. + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + + // Calculate if nodes[self->bot.next_node].origin[2] is higher than self->s.origin[2] + float higher = 0; float lower = 0; + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) + higher = (nodes[self->bot.next_node].origin[2] - nodes[self->bot.current_node].origin[2]); + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + lower = (nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]); + //if (distance >= 128 || higher >= NODE_MAX_JUMP_HEIGHT || lower >= NODE_MAX_JUMP_HEIGHT) + //if (higher >= NODE_MAX_JUMP_HEIGHT) // || lower >= NODE_MAX_CROUCH_FALL_HEIGHT) + if (higher) // || lower) + { + next_node_type = NODE_JUMPPAD; + } + } + } + */ + + if (current_node_type == INVALID || next_node_type == INVALID) + { + Com_Printf("%s curr[%d]type[%d] next[%d]type[%d] Goal[%d] State[%d]\n", __func__, self->bot.current_node, current_node_type, self->bot.next_node, next_node_type, self->bot.goal_node, self->bot.state); + } + + + // DEBUG: print current and next node types + //if (1) + { + //const qboolean printOnlyErrors = true; // If only printing INVALID nodes + //PrintNodeType(self, current_node_type, next_node_type, printOnlyErrors); + } + + + + + + + // Contents check + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] += 22; + int contents_head = gi.pointcontents(temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + // If in water, force bot to wander... + if (contents_feet & MASK_WATER) + { + self->bot.state = STATE_WANDER; + return; + //self->bot.goal_node = INVALID; + //self->bot.current_node = INVALID; + //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); + } + + + + //if (next_node_type == INVALID && self->bot.goal_node == INVALID) + if (next_node_type == INVALID) //self->bot.current_node == self->bot.next_node == self->bot.goal_node) + { + //next_node_type = NODE_MOVE; + + /* + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && M_CheckBottom(self, 2)) + { + if (ACEMV_CanMove(self, MOVE_FORWARD)) + { + ucmd->forwardmove = SPEED_WALK * sqrtf(xy_dist_self_curr / 64); + return; + } + //else if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -64; + //else if (ACEMV_CanMove(self, MOVE_RIGHT)) + // ucmd->sidemove = 64; + //else if (ACEMV_CanMove(self, MOVE_LEFT)) + // ucmd->sidemove = -64; + } + */ + } + + + // + //if (current_node_type == NODE_JUMPPAD && self->last_touched_ground > 0) + //{ + //ucmd->forwardmove = 0; + //VectorClear(self->velocity); + //return; + //} + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_CURR_NEXT); + ////VectorSubtract(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, dist_xy); + ////dist_xy[2] = 0; // Drop height component + ////distance_xy = VectorLength(dist_xy); + + // Strafe jumping + // Vary it up by doing JUMPPAD 'strafe jumping' in place of MOVE to the target if far enough away + if (0) + { + if (next_node_type == NODE_MOVE && abs(nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]) < STEPSIZE && distance > 256) + { + if (self->strafe_jumps > 30) // Allow jumps to be missed + self->strafe_jumps = 0; // Reset + + if (self->strafe_jumps < 10) // Mutiple jumps in series (although not contigous) + { + next_node_type = NODE_JUMPPAD; + } + + self->strafe_jumps++; // Count strafe jumps + } + } + + //if (random() < 0.01) + //{ + //self->client->leg_damage = 1; // DEBUG: force leg damage + //Com_Printf("%s %s leg damage\n", __func__, self->client->pers.netname); + //} + + // If we have leg damage, prevent jumping + // Look for a nearby move node to take cover + if (self->client->leg_damage > 0) // Do we have leg damage? + { + next_node_type = NODE_MOVE; + + /* + if (next_node_type != NODE_MOVE) + { + qboolean foundMoveNode = false; + + // Search for a nearby move node + for (i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_MOVE) + { + // Check if any enemies can see this node + qboolean canEnemiesSeeNextNode = false; + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (ACEAI_IsEnemy(self, players[p])) // Are they our enemy + { + // trace line to see if enemy is visible from the targetNode + int targetNode = nodes[self->bot.current_node].links[i].targetNode; + tr = gi.trace(nodes[targetNode].origin, NULL, NULL, players[p]->s.origin, self, MASK_ALL); + //if (tr.ent) + // Com_Printf("%s %s found ent [%s]\n", __func__, self->client->pers.netname, tr.ent->classname); + if (tr.ent == players[p]) // Enemy can see this node + { + //Com_Printf("%s %s found MOVE node [%d] but enemy [%s] can see it\n", __func__, self->client->pers.netname, targetNode, players[p]->client->pers.netname); + canEnemiesSeeNextNode = true; + break; + } + } + } + + if (canEnemiesSeeNextNode) // Enemy can see this node, so skip it + continue; + else // No enemies can see this node, so use it + { + self->bot.next_node = nodes[self->bot.current_node].links[i].targetNode; + next_node_type = NODE_MOVE; + foundMoveNode = true; + Com_Printf("%s %s found MOVE node for cover [%d]\n", __func__, self->client->pers.netname, self->bot.next_node); + break; + } + } + } + + // Otherwise wander + if (foundMoveNode == false) + { + Com_Printf("%s %s failed to find MOVE node, wandering...\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.time + 5.0; + return; + } + } + */ + } + //return; + + + //if ((current_node_type == NODE_LADDER || next_node_type == NODE_LADDER) && OnLadder(self) == false) + // next_node_type = NODE_JUMPPAD; + //rekkie -- DEV_1 -- e + + /////////////////////// + // Grenade Avoidance // + /////////////////////// + // Try to avoid hand grenades by strafing, backing away, and crouching + edict_t* target = findradius(NULL, self->s.origin, 512); + while (target) + { + if (target->classname == NULL) + return; + + if (strcmp(target->classname, "hgrenade") == 0) + { + //Com_Printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); + if (debug_mode) debug_printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); + + VectorSubtract(target->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + + // Try strafing + if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else + ucmd->sidemove = 0; + + self->bot_strafe = ucmd->sidemove; + + // Also try move backwards + if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + + // And crouch if its very close + VectorSubtract(target->s.origin, self->s.origin, dist); + dist[2] = 0; + if (VectorLength(dist) < 192) + ucmd->upmove = -SPEED_WALK; + + return; + } + + target = findradius(target, self->s.origin, 512); + } + + //////////////////// + // Stay on course // + //////////////////// + /* + if (self->bot.next_node != self->bot.current_node && current_node_type == NODE_MOVE && next_node_type == NODE_MOVE) + { + // Decide if the bot should strafe to stay on course. + vec3_t v = { 0,0,0 }; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, v); + v[2] = 0; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + dist[2] = 0; + if ((DotProduct(v, dist) > 0) && (VectorLength(v) > 16)) + { + float left = 0; + VectorNormalize(v); + VectorRotate2(v, 90); + left = DotProduct(v, dist); + if ((left > 16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_RIGHT))) + ucmd->sidemove = SPEED_RUN; + else if ((left < -16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_LEFT))) + ucmd->sidemove = -SPEED_RUN; + } + } + */ + + + + + + ////////////////////// + // Object Avoidance // + ////////////////////// + + // Calculate the players current speed and direction + float forward_or_back_velocity = 0; + float left_or_right_velocity = 0; + if (1) + { + vec3_t velocity; + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + float speed = VectorLength(velocity); + + vec3_t angles; + VectorCopy(self->client->v_angle, angles); + angles[0] = 0; + angles[2] = 0; + + vec3_t forward, right; + AngleVectors(angles, forward, right, NULL); + + forward_or_back_velocity = DotProduct(forward, velocity); + left_or_right_velocity = DotProduct(right, velocity); + //Com_Printf("%s: speed %f forward_or_back_velocity %f left_or_right_velocity %f\n", __func__, speed, forward_or_back_velocity, left_or_right_velocity); + } + + // Check to see if stuck, and if so try to free us. Also handles crouching + VectorSubtract(self->s.origin, self->lastPosition, dist); + if (self->groundentity && forward_or_back_velocity > -30 && forward_or_back_velocity < 30) + { + if (Botlib_Crouch_Or_Jump(self, ucmd)) + { + Com_Printf("%s %s attempted to crouch or jump \n", __func__, self->client->pers.netname); + return; + } + + // Check if we are obstructed by an oncoming teammate, and if so strafe + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) + { + if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + } + } + + /* + // Check to see if stuck, and if so try to free us. Also handles crouching + VectorSubtract(self->s.origin, self->lastPosition, dist); + if ((VectorLength(self->velocity) < 37) || (VectorLength(dist) < FRAMETIME)) + { + if (self->groundentity) + { + //if (random() > 0.5 && + if (Botlib_Crouch_Or_Jump(self, ucmd)) + return; + + // Check if we are obstructed by an oncoming teammate, and if so strafe + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) + { + if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + } + } + } + */ + /* + /////////////////////// + // corner management // + /////////////////////// + //RiEvEr -- My corner management code + if (BOTCOL_CheckBump(self, ucmd)) + { + if (BOTCOL_CanStrafeSafely(self, self->s.angles)) + { + ucmd->sidemove = self->bot_strafe; + return true; + } + } + //R + */ + + /* + float botspeed = VectorLength(self->velocity); + if (botspeed == 0 && self->last_touched_ground == 0) + { + Com_Printf("%s %s is stuck: STATE_WANDER\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + */ + /* + // Check if we're standing on top of another player + // OR touching another player + if (VectorLength(self->velocity) <= 32) // && self->last_touched_ground > 0) + { + //tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_DEADSOLID); + tr = gi.trace(self->s.origin, tv(-32, -32, -16), tv(32, 32, 24), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2]), self, MASK_DEADSOLID); + if (tr.ent && tr.ent->client && tr.ent->health > 0) + { + Com_Printf("%s %s is stuck on %s: wandering\n", __func__, self->client->pers.netname, tr.ent->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + return; + } + if (tr.ent && tr.ent->classname) + { + Com_Printf("%s %s is touching: %s\n", __func__, self->client->pers.netname, tr.ent->classname); + } + } + */ + + + // Grab items along the way, if they're not already our goal item to fetch + if (self->movetarget && self->movetarget->inuse && self->movetarget != self->goalentity) + { + // Try to navigate to the item + /* + int item_node = ACEND_FindClosestReachableNode(self->goalentity, NODE_DENSITY, NODE_ALL); + if (item_node != INVALID) + { + if (self->bot.prev_node != self->bot.goal_node) + self->bot.prev_node = self->bot.goal_node; // Keep a copy of previous goal + self->bot.goal_node = item_node; // Make the item node our new goal + + // Try visit node near item + // Otherwise visit previous goal node + if (BOTLIB_CanVisitNode(self, self->bot.goal_node) == false) // If we cannot visit item node + { + self->bot.goal_node = self->bot.prev_node; // Restore previous goal + BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal + } + } + */ + + // When we're near the item we desire, jump to it + vec3_t item_vec; + VectorSubtract(self->s.origin, self->movetarget->s.origin, item_vec); + float dist = VectorLength(item_vec); + if (dist < 256) + { + //if (self->movetarget->inuse == false) self->movetarget = NULL; //ignore freed ents + + if (dist < 32) + { + //Com_Printf("%s %s successfully got short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); + self->movetarget = NULL; + //self->bot.goal_node = self->bot.prev_node; // Restore previous goal + //BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal + return; + } + else// if (ACEIT_IsReachable(self, self->movetarget->s.origin)) + { + //Com_Printf("%s %s heading for short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); + + VectorSubtract(self->movetarget->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + //ucmd->forwardmove = SPEED_RUN; + if (self->groundentity) + { + VectorClear(self->velocity); + LaunchPlayer(self, self->movetarget->s.origin); + self->movetarget = NULL; + //self->bot.state = STATE_WANDER; + return; + } + //return; + } + } + } + + + + + if (next_node_type == NODE_CROUCH) + { + // Make sure we're facing the target + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + // Crouch move forward + ucmd->upmove = -SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return; + } + + //////////////////////////////////////////////////////// + // Jumpad Nodes + /////////////////////////////////////////////////////// + if (next_node_type == NODE_JUMPPAD) + { + + //if (self->groundentity && self->velocity[2] <= 0) + //{ + // VectorClear(self->velocity); + // LaunchPlayer(self, nodes[self->bot.next_node].origin); + // return; + //} + + /* + vec3_t dist; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + float next_node_distance = VectorLength(dist); + + dist[2] = 0; // Drop height component + float next_node_distance_xy = VectorLength(dist); + + vec3_t curr_node_dist; + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, curr_node_dist); + float curr_node_distance = VectorLength(curr_node_dist); + */ + + + /* + // If the bot's distance is moving away from the target (failed the jump) + // Try to find another route, otherwise find a new node and go there instead + if (self->prev_next_node == self->bot.next_node) + { + if (next_node_distance < self->next_node_distance) // Update if getting closer + self->next_node_distance = next_node_distance; + else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump + { + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->bot.goal_node); + } + else + { + Com_Printf("%s %s failed to jumppad up, finding new goal\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + } + return; + } + } + if (self->prev_next_node != self->bot.next_node) // Update previous next node and distance + { + self->prev_next_node = self->bot.next_node; + self->next_node_distance = next_node_distance; + } + */ + /* + // If the bot fell below the target (failed the jump down) + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node by moving its origin higher + if (targetNodeLink != INVALID) + { + //if (debug_mode) + Com_Printf("%s %s dropped from a jumppad forward, moving jumppad node higher\n", __func__, self->client->pers.netname); + int n = nodes[self->bot.current_node].links[targetNodeLink].targetNode; + nodes[n].origin[2]++; + } + } + */ + + + + + + /* + // Bot fell below current and next target + //if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.current_node].origin[2]) + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); + if (self->last_touched_ground == 0 && distance > 96) + { + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMPPAD][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s (1) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + */ + + + + + + // See if we need a ladder jump boost + float speed = VectorLength(self->velocity); + if (self->bot.current_node == NODE_LADDER_UP && speed < 32 && self->last_touched_ground > 0) + { + //if (debug_mode) + // Com_Printf("%s %s is stuck [NODE_JUMPPAD]...\n", __func__, self->client->pers.netname); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: jumping\n", __func__, self->client->pers.netname); + } + else + { + ucmd->upmove = -400; // Try crouching + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: crouching\n", __func__, self->client->pers.netname); + } + } + if (speed == 0 && self->last_touched_ground == 0) // We're not moving at all + { + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]... speed: 0\n", __func__, self->client->pers.netname); + ucmd->forwardmove = -50; // Try backing up a tad, then try jumping again + } + + + + + + + + //* + // Guide the bot in when close to the target + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ///distance = VectorLength(dist); + vec3_t dist_xy; + float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + /* + if (0) + if (distance <= 50 || distance_xy < 32 || VectorLength(self->velocity) < 32)// && next_node_type != NODE_LADDER_UP) //32 + { + //self->s.angles[PITCH] = 0; // Look directly forward + + // When very close to the next node change yaw angle only + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + BOTLIB_MOV_ChangeBotAngleYawPitch(self); + + //Com_Printf("%s %s jumppad self->last_touched_ground = %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + + //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0) + { + float speed = VectorLength(self->velocity); + //if (debug_mode) + // Com_Printf("%s guiding the bot in from jump [speed %f]\n", __func__, speed); + + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + + ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. + + // Apply it + //if (!ACEMV_CanMove(self, MOVE_FORWARD)) + // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down + //else + // VectorScale(dist, 128, self->velocity); + //VectorScale(dist, 224, self->velocity); + + + //vec3_t dist_xy; + //float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); + //Com_Printf("%s %s distance_xy %f\n", __func__, self->client->pers.netname, distance_xy); + if (distance_xy > 16) + { + if (self->groundentity || ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop + { + // touching the ground but on an edge with a drop - slow down + if (ACEMV_CanMove(self, MOVE_FORWARD) == false) + { + //ucmd->forwardmove = -1; + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + Com_Printf("%s %s jumppad (1)\n", __func__, self->client->pers.netname); + } + else // touching the ground - safe to move at a normal pace + { + VectorScale(dist, SPEED_RUN, self->velocity); + ucmd->forwardmove = SPEED_RUN; + Com_Printf("%s %s jumppad (2)\n", __func__, self->client->pers.netname); + } + } + else if (distance <= 16) // Ensure we slow right down if needed + { + //VectorScale(dist, max((SPEED_WALK * sqrtf(distance / 128)), 32), self->velocity); // Slow down (min speed 32) + + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + Com_Printf("%s %s jumppad (3)\n", __func__, self->client->pers.netname); + } + else + { + //ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + //VectorScale(dist, 224, self->velocity); + Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); + } + + if (self->s.origin[2] > nodes[self->bot.next_node].origin[2]) + self->velocity[2] -= 32; + + // If we're now touching the ground, just move at a normal pace + //if (self->last_touched_ground == 0)// && self->s.origin[2] == nodes[self->bot.next_node].origin[2]) + { + //Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); + //VectorScale(dist, SPEED_RUN, self->velocity); + //ucmd->forwardmove = SPEED_RUN; + } + + } + else if (distance_xy <= 16 && self->s.origin[2] > nodes[self->bot.next_node].origin[2]) + { + Com_Printf("%s %s jumppad (5a)\n", __func__, self->client->pers.netname); + ucmd->forwardmove = 1; + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] -= 32; + //return; + + if (self->groundentity) + { + Com_Printf("%s %s jumppad (5b)\n", __func__, self->client->pers.netname); + //ucmd->forwardmove = 128 * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + } + } + //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2] && !self->groundentity) + else if (distance_xy <= 16 && (self->s.origin[2] + NODE_Z_HALF_HEIGHT) < nodes[self->bot.next_node].origin[2]) + { + Com_Printf("%s %s jumppad (6)\n", __func__, self->client->pers.netname); + + //float z_height_diff = fabs(nodes[self->bot.next_node].origin[2]) - fabs(self->s.origin[2]); + //if (z_height_diff < NODE_MAX_JUMP_HEIGHT) + { + // Try jumping + //if (debug_mode) + // Com_Printf("%s %s failed to jumppad. Trying to jump next node %d\n", __func__, self->client->pers.netname, self->bot.next_node); + //ucmd->upmove = SPEED_RUN; + //ucmd->forwardmove = SPEED_RUN; + //return; + //if (Botlib_Crouch_Or_Jump(self, ucmd)) + // return; + } + //return; + + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMPPAD][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s (2) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + else + { + //if (BOTLIB_MOV_ChangeBotAngleYawPitch(self)) + if (self->groundentity) + { + Com_Printf("%s %s jumppad (7)\n", __func__, self->client->pers.netname); + //ucmd->forwardmove = 128 * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + } + } + + + // See if we're stuck, try to unstick + if (speed < 32 && self->last_touched_ground > 0) + { + if (debug_mode) + Com_Printf("%s correcting jump, unsticking\n", __func__); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + } + else + ucmd->upmove = -400; // Try crouching + } + + return; + } + } + */ + /* + else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) + { + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + next_node_distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + + return; + } + } + */ + + + float gravity = self->gravity * sv_gravity->value; + + ACEMV_ChangeBotAngle(self); + + float boost_jump = 1; // Boost jump in rare instances + + /* + if (next_node_distance_xy > 64) + ucmd->forwardmove = SPEED_RUN; // Move forward + else if (self->groundentity && next_node_distance_xy < 64) + { + // Try alternative route + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + */ + /* + else if (nodes[self->bot.next_node].origin[2] > self->s.origin[2] + 96) // It's fairly high above the player (and not a step) + { + // Check we've reach our ideal yaw angle + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + // Check we can move backward safely + if (ACEMV_CanMove(self, MOVE_BACK)) + { + ucmd->forwardmove = -SPEED_RUN; // Move back a bit + return; + } + else + { + boost_jump = 2; // Could not move back, so cheat and boost jump + } + } + */ + + //if (self->velocity[0] || self->velocity[1] || self->velocity[2]) + //{ + // VectorClear(self->velocity); // Zero current velocity to kill any momentum + // return; + //} + + //if (self->groundentity || next_node_distance < NODE_Z_HEIGHT) + //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + // distance between curr_node and self->bot.current_node + + //Com_Printf("%s %s can jumppad? last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + + // Slightly oversized player standing box ( +2 on sides, +8 bottom, but not top ) + //vec3_t expanded_player_standing_mins = { -18, -18, -32 }; // default: -16, -16, -24 + //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 + // Figure out if player is touching against a wall or floor + //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); + + //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance < 24)) // && curr_node == self->bot.current_node)) + //if (self->groundentity || curr_node_distance < 24 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) + //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) + //if (self->last_touched_ground == 0) + if (self->last_jumppad_node != self->bot.current_node) // && self->velocity[2] > -200) // apply jumppad if we're on a different current_node + //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) + //if (self->last_touched_ground < 10) + //if (self->groundentity) + //if (self->groundentity || tr.fraction < 1.0 || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 48) // touching ground + //if (self->groundentity || tr.fraction < 1.0) // touching ground + //if (self->groundentity || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) // touching ground + //if (self->groundentity || (self->velocity[2] <= 0 && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32)) + //if ( (self->groundentity || tr.fraction < 1.0) && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_CURR) <= 32) + { + // Skip doing a jumpnode... if the node we jumped from (current_node) changed. + // This occurs when a jump is made and fails to reach the next_node; because the current node is updated just before landing (in the wrong spot) + //if (self->last_jumppad_node == self->bot.prev_node && self->last_touched_ground > 2) + //{ + //Com_Printf("%s %s jumppad failed to reach target, self->last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + // self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from + // return; + //} + + // Slightly oversized player standing box ( +2 on sides, +2 bottom, but not top ) + //vec3_t expanded_player_standing_mins = { -18, -18, -26 }; // default: -16, -16, -24 + //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 + // Figure out if player is touching against a wall or floor + //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); + //if (tr.fraction == 1.0) // is the player in the air? + // return; // bug out + + //Com_Printf("%s %s applied jumppad %f\n", __func__, self->client->pers.netname, level.time); + + VectorClear(self->velocity); // Zero current velocity to kill any momentum + self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from + + // Check we've reach our ideal yaw angle and we're not falling + //if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) // || self->velocity[2] < 0) + // return; + + if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) + { + //VectorClear(self->velocity); // Zero velocity + ucmd->forwardmove = 1; + //return; + } + + + // Check if we're off course for a jump + vec3_t chk_dist; + float chk_distance = BOTLIB_UTIL_Get_Distance(self, chk_dist, BOT_DIST_XYZ_SELF_CURR); + if (chk_distance > 128) + { + //if (debug_mode) + // Com_Printf("%s %s correcting jump that is off course\n", __func__, self->client->pers.netname); + + // Change the bot's vector to face the previous node + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + + // Face previous node + if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) + { + //VectorClear(self->velocity); // Zero velocity + ucmd->forwardmove = 1; + } + + // Return the bot's movement vector back to the target + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + } + + + + + float z_velocity = self->velocity[2]; + if (z_velocity < 0) + z_velocity = (z_velocity - (z_velocity * 2)); // Make it positive + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + + //Com_Printf("%s %s jump_height: %f\n", __func__, self->client->pers.netname, jump_height/16); + + // Calculate the time it will take to get to the target + float time = distance / (z_velocity + jump_height / 2.0); + + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2])//self->s.origin[2]) + { + z_height = ((nodes[self->bot.next_node].origin[2] - self->s.origin[2]) * boost_jump); + z_height += sqrtf(distance) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + { + z_height = (self->s.origin[2] - nodes[self->bot.next_node].origin[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // If self->velocity[2] is minus, make it positive and add it to the required velocity + if (self->velocity[2] < 0 && self->groundentity == NULL) + velocity[2] += -self->velocity[2]; + + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + + // Limit max speed + //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER + { + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, self->velocity); + VectorCopy(velocity, self->client->oldvelocity); + VectorCopy(velocity, self->avelocity); + + return; + } + } + else + { + next_node_type = NODE_MOVE; + } + + /* + // Guide the bot in when close to the target + else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + next_node_distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + } + } + */ + + + + //return; + } + + //////////////////////////////////////////////////////// + // Jump Node + /////////////////////////////////////////////////////// + if (next_node_type == NODE_JUMP) + { + + // If the bot fell below the target (failed the jump) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node type to jumppad + if (targetNodeLink != INVALID) + { + if (debug_mode) + Com_Printf("%s %s fell from a jump node, upgrading node to jumppad\n", __func__, self->client->pers.netname); + nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; + + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMP][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + return; + } + else + { + if (debug_mode) + Com_Printf("%s %s failed to jump node to goal %d because targetNodeLink was invalid, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; // Aquire a new node + return; + } + } + + // See if we're stuck, try to unstick + float speed = VectorLength(self->velocity); + if (speed < 32 && self->last_touched_ground == 0) + { + //if (debug_mode) + // Com_Printf("%s %s is stuck, trying to resolve\n", __func__, self->client->pers.netname); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + //Com_Printf("%s %s is stuck [NODE_JUMP]: jumping\n", __func__, self->client->pers.netname); + } + else + { + ucmd->upmove = -400; // Try crouching + //Com_Printf("%s %s is stuck [NODE_JUMP]: crouching\n", __func__, self->client->pers.netname); + } + } + + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + // Lose the vertical component + ////dist[2] = 0; + // Get the absolute length + ////distance = VectorLength(dist); + + //if (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + if (DotProduct(velocity, dist) > 0.8) + ucmd->upmove = SPEED_RUN; + + //Kill running movement +// self->move_vector[0]=0; +// self->move_vector[1]=0; +// self->move_vector[2]=0; + // Set up a jump move + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + + //self->move_vector[2] *= 2; + + ACEMV_ChangeBotAngle(self); + + //rekkie -- DEV_1 -- s + // Guide the bot in when close to the target + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////distance = VectorLength(dist); + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + if (distance <= 64) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.7) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + //rekkie -- DEV_1 -- e + + /* + // RiEvEr - re-instate the velocity hack + if (self->jumphack_timeout < level.framenum) + { + VectorCopy(self->move_vector,dist); + if ((440 * distance / 128) < 440) + VectorScale(dist,(440 * distance / 128),self->velocity); + else + VectorScale(dist,440,self->velocity); + self->jumphack_timeout = level.framenum + 3.0 * HZ; + } + */ + } + /* + else + { + self->bot.goal_node = INVALID; + } + */ + return; + } + + //////////////////////////////////////////////////////// + // Ladder Nodes + /////////////////////////////////////////////////////// + //rekkie -- DEV_1 -- s + if (current_node_type == NODE_LADDER_UP || current_node_type == NODE_LADDER_DOWN || + next_node_type == NODE_LADDER_UP || next_node_type == NODE_LADDER_DOWN) + { + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); // Find the distance vector to the next node + ////distance = VectorLength(dist); + + ////distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); // Find the distance vector to the next node + ////dist_xy[2] = 0; // Lose the vertical component + ////distance_xy = VectorLength(dist_xy); // Get the absolute length + + float speed = VectorLength(self->velocity); // Speed of the player + + // If we are going down the ladder, check for teammates coming up and yield to them. + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0)) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 200; + self->bot.state = STATE_WANDER; + return; + } + + // If getting off the top of a ladder + if (current_node_type == NODE_LADDER_UP && next_node_type != NODE_LADDER_DOWN) + { + ucmd->forwardmove = SPEED_WALK; + + //Com_Printf("%s %s on top ladder getting off, up vel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + + //Com_Printf("%s %s on top ladder getting off. OnLadder %d\n", __func__, self->client->pers.netname, OnLadder(self)); + if (OnLadder(self)) + { + ucmd->upmove = SPEED_RUN; + return; + } + else + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + + if (self->velocity[2] >= 0 && self->velocity[2] < 200) // do we need a boost? + { + float zdiff = 0; + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + zdiff = abs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]); + float boost = 64 + zdiff; + //float boost = 50; + //Com_Printf("%s %s on top ladder getting off, boosting jump from %f to %f\n", __func__, self->client->pers.netname, self->velocity[2], self->velocity[2] + boost); + self->velocity[2] += boost; // slight boost jump at top of ladder + } + + if (distance < 64 && self->s.origin[2] >= nodes[self->bot.next_node].origin[2]) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + VectorScale(dist, 50, self->velocity); // Apply it + //Com_Printf("%s %s on top ladder getting off, guiding velocity\n", __func__, self->client->pers.netname); + } + } + + /* + if ((nodes[self->bot.next_node].origin[2] >= self->s.origin[2]) + && (nodes[self->bot.next_node].origin[2] >= nodes[self->bot.current_node].origin[2]) + && (((self->velocity[2] > 0) && !self->groundentity) || OnLadder(self))) + { + ucmd->upmove = SPEED_RUN; + self->velocity[2] = min(200, BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) * 10); // Jump higher for farther node + } + + if (1) + { + // Guide the bot in when close to the target + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 32 && distance < 128) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + } + } + */ + + ACEMV_ChangeBotAngle(self); + return; + } + + + // If getting off the bottom of a ladder + if (current_node_type == NODE_LADDER_DOWN && next_node_type != NODE_LADDER_UP) + { + // We used to do nothing here, because it was handled by the next node type... but now we just walk forward :) + + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->forwardmove = SPEED_RUN; + } + + + // Getting onto bottom of the ladder + if (current_node_type != NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN && distance < 192 && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) + { + //Com_Printf("%s %s getting onto bottom of the ladder\n", __func__, self->client->pers.netname); + + // Jump only when we are moving the correct direction. + // Check we've reach our ideal yaw angle + //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //qboolean isOnLadder = OnLadder(self); + //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); + //if (isOnLadder == false) + if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) > 16) + { + //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //ucmd->upmove = SPEED_WALK; + ucmd->forwardmove = 96; + + //Com_Printf("%s %s moving to bottom of the ladder\n", __func__, self->client->pers.netname); + return; + } + else + { + //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); + //current_node_type = NODE_LADDER_DOWN; + //next_node_type = NODE_LADDER_UP; + } + + /* + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && OnLadder(self) == false) + //if (OnLadder(self) == false) + { + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_WALK; + } + */ + + + if (1) + { + //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 32) + // self->velocity[2] = 100; + //return; + + // Guide the bot in when close to the target + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] + 32 && distance < 128) + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 128) + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + { + /* + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 32, self->velocity); // Apply it + */ + + if (OnLadder(self) == false) + { + ucmd->forwardmove = 32; + ucmd->upmove = 32; + + if (NODES_AdvanceToNextNode(self)) + return; + } + + else + { + /* + // Get distance + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + float next_node_distance = VectorLength(dist); // Get the absolute length + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + */ + } + + /* + if (OnLadder(self) == false) + ucmd->forwardmove = 64; + else + { + // Make bot look directly up from its current pos + vec3_t above; + VectorCopy(self->s.origin, above); + above[2] + 32; + VectorSubtract(above, self->s.origin, self->move_vector); // point up + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // look up + + ucmd->upmove = SPEED_RUN; + //self->s.origin[2] = nodes[self->bot.next_node].origin[2]; + } + + // If touching ladder, advance to next node (ladder down -> ladder up) + if (OnLadder(self)) + { + if (NODES_AdvanceToNextNode(self)) + return; + } + */ + + //Com_Printf("%s %s is at bottom of the ladder, OnLadder:%d\n", __func__, self->client->pers.netname, OnLadder(self)); + } + return; + } + + + //if (OnLadder(self) == false) + //return; + } + + // Getting onto top of the ladder + if (current_node_type != NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP)// && self->groundentity) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + else // Correct direction, jump up + { + if (distance > 64) + ucmd->forwardmove = SPEED_WALK; + else if (distance > 48) + ucmd->forwardmove = 50; + else if (distance > 24) + ucmd->forwardmove = 25; + } + + // Guide the bot in when close to the target + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] && distance <= 128) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 100, self->velocity); // Apply it + } + + // + //if (OnLadder(self) == false) + return; + } + + // On ladder going up + if (current_node_type == NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP) + { + //Com_Printf("%s %s on ladder going up\n", __func__, self->client->pers.netname); + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance < 64) + ////if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance_xy < 64) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + if (!self->groundentity) + { + // FIXME: Dirty hack so the bots can actually use ladders. + //vec3_t origin_up; + //VectorCopy(self->s.origin, origin_up); + //origin_up[2] += 8; + //VectorSubtract(origin_up, self->s.origin, dist_xy); + + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); + ////VectorNormalize(dist_xy); + ////VectorScale(dist_xy, SPEED_RUN, self->velocity); + ////if (dist_xy[2] > 0) + //// self->velocity[2] = min(200, self->velocity[2]); + VectorScale(dist, SPEED_RUN, self->velocity); + if (dist[2] > 0) + self->velocity[2] = min(200, self->velocity[2]); + } + + // Guide the bot in when close to the target + // Useful for the tall ladder found on murder.bsp - helps to deal with ladders that use playerclip + //if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16 && distance < 64) + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + + return; + } + } + + // On ladder going down + if (current_node_type == NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN) + { + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance < 64) //&& speed <= SPEED_ROAM) + ////if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance_xy < 64) //&& speed <= SPEED_ROAM) + { + ucmd->forwardmove = SPEED_WALK / 2; + ////if ((distance_xy < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) + if ((distance < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) + ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching + + if (!self->groundentity) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + //ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + // FIXME: Dirty hack so the bots can actually use ladders. + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); + ////VectorNormalize(dist_xy); + ////VectorScale(dist_xy, SPEED_RUN, self->velocity); + ////if (dist_xy[2] < 0) + //// self->velocity[2] = max(-200, self->velocity[2]); + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + VectorScale(dist, SPEED_RUN, self->velocity); + if (dist[2] < 0) + self->velocity[2] = max(-200, self->velocity[2]); + } + return; + } + } + } + //rekkie -- DEV_1 -- e + + + //////////////////////////////////////////////////////// + // Water Nodes + /////////////////////////////////////////////////////// + if (current_node_type == NODE_WATER || self->waterlevel > 0) + { + ACEMV_ChangeBotAngle(self); + //ucmd->upmove = SPEED_RUN; + //ucmd->forwardmove = SPEED_RUN; + + // Ensure we have LoS + tr = gi.trace(self->s.origin, tv(-16, -16, -4), tv(16, 16, 4), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + + // Guide the bot in when close to the target + //if (next_node_type != NODE_WATER) // && distance <= 128) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + if (next_node_type != NODE_WATER) + { + VectorScale(dist, 400, self->velocity); // Jump out + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + } + else + VectorScale(dist, 200, self->velocity); // Move water speed + } + + /* + // We need to be pointed up/down + ACEMV_ChangeBotAngle(self); + + // If the next node is not in the water, then move up to get out. + if (next_node_type != NODE_WATER && !(gi.pointcontents(nodes[self->bot.next_node].origin) & MASK_WATER)) // Exit water + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_RUN * 3 / 4; + return; + */ + } + + //rekkie -- DEV_1 -- s + //////////////////////////////////////////////////////// + // NODE_STAND_DROP Nodes + if (next_node_type == NODE_STAND_DROP || next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + { + if (0) + { + if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s next_node_type [NODE_STAND_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s next_node_type [NODE_CROUCH_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s next_node_type [NODE_UNSAFE_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + } + + // If target is getting too far away, adjust our angle + float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (distance_xy > 32) + ACEMV_ChangeBotAngle(self); + + // Then keep walking + ucmd->forwardmove = SPEED_WALK; + + //Com_Printf("%s %s vel %f\n", __func__, self->client->pers.netname, VectorLength(self->velocity)); + + // If stuck, try clearing the obstacle by moving in a random dir + if (VectorLength(self->velocity) < 32) + { + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0) // blocked, try find another path to the same goal + { + //Com_Printf("%s %s is blocked [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //BOTLIB_CanVisitNode(self, self->bot.goal_node); + //ACEMV_ChangeBotAngle(self); + + self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[PITCH] = 0; + + //self->bot.state = STATE_WANDER; + return; + } + } + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + //if (self->velocity[2] < -400 || distance < 64) + //if (distance < 64) + //VectorScale(dist, 350, self->velocity); // Apply it + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0) + { + VectorScale(dist, 350, self->velocity); // Apply it + VectorCopy(self->velocity, self->client->oldvelocity); + VectorCopy(self->velocity, self->avelocity); + } + + return; + + /* + // If the bot fell below the target (failed the jump down) + // If the bot is trying an unsafe drop past the LCA timer + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2] || + (next_node_type == NODE_UNSAFE_DROP && lights_camera_action > 23)) + { + // Update the node we're near + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + //Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->bot.goal_node); + } + else + { + //Com_Printf("%s %s failed to drop down, finding new goal\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + } + return; + } + + ACEMV_ChangeBotAngle(self); + + // If stuck, randomly crouch or jump + if (VectorLength(self->velocity) < 3) + { + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + if (random() > 0.5) + { + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: crouching\n", __func__, self->client->pers.netname); + //return; + } + else + { + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: jumping\n", __func__, self->client->pers.netname); + //return; + } + } + + + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////dist[2] = 0; + ////distance = VectorLength(dist); + + if (!self->groundentity) + { + // When in air, control speed carefully to avoid overshooting the next node. + if (distance < 256) + { + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 128); //256 + // Crouch down + if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + ucmd->upmove = -SPEED_WALK; + } + else + ucmd->forwardmove = SPEED_RUN; + } + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + // If we reached a ledge, slow down. + if (distance < 512) + { + // Jump unless dropping down to the next node. + //if (nodes[self->bot.next_node].origin[2] > (self->s.origin[2] + 7) && distance < 500) + // ucmd->upmove = SPEED_RUN; + + // Crouch down + if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + ucmd->upmove = -SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 512); + } + else + ucmd->forwardmove = SPEED_WALK; + } + else // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + + + if(1) + // Guide the bot in when close to the target + //if (distance <= 128) //64 + if (self->velocity[2] < 0) + { + //if (self->velocity[2] < 32) + // ACEMV_ChangeBotAngle(self); + + //if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s: NODE_STAND_DROP\n", __func__, self->client->pers.netname); + //if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s: NODE_CROUCH_DROP\n", __func__, self->client->pers.netname); + //if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s: NODE_UNSAFE_DROP\n", __func__, self->client->pers.netname); + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + + if (self->groundentity && ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); + + VectorScale(dist, 350, self->velocity); // Apply it + + return; + } + */ + } + //rekkie -- DEV_1 -- e + + + //////////////////////////////////////////////////////// + // Move & Step Nodes + /////////////////////////////////////////////////////// + if (current_node_type == NODE_MOVE || current_node_type == NODE_STEP || current_node_type == NODE_ITEM) + { + //Com_Printf("%s %s move forward\n", __func__, self->client->pers.netname); + + + { + // Get distance from bot to next node + vec3_t bot_to_prev_vec, bot_to_curr_vec, bot_to_next_vec; // Direction vector to nodes + float bot_to_prev_dist = 0, bot_to_curr_dist = 0, bot_to_next_dist = 0; // Distance to nodes + qboolean can_see_prev = false, can_see_curr = false, can_see_next = false; // If bot can see these nodes + if (self->bot.prev_node != INVALID) + { + VectorSubtract(nodes[self->bot.prev_node].origin, self->s.origin, bot_to_prev_vec); + bot_to_prev_dist = VectorLength(bot_to_prev_vec); + //trace_t prev_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); + trace_t prev_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); + if (prev_tr.fraction == 1.0) + can_see_prev = true; + } + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_curr_vec); + bot_to_curr_dist = VectorLength(bot_to_curr_vec); + //trace_t curr_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); + trace_t curr_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (curr_tr.fraction == 1.0) + can_see_curr = true; + + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + bot_to_next_dist = VectorLength(bot_to_next_vec); + //trace_t next_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + trace_t next_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (next_tr.fraction == 1.0) + can_see_next = true; + + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) + { + //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + if (bot_to_next_dist < 192) + { + VectorClear(self->velocity); + bot_to_next_vec[2] = 0; // Flatten the vector + VectorScale(bot_to_next_vec, 400, self->velocity); + } + else + ucmd->forwardmove = SPEED_RUN; + } + else + ucmd->forwardmove = SPEED_RUN; + + //qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right + //qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left + + // Stay on course by strafing back to the path line (if not already strafing) + if (1) + { + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + if (mov_strafe == 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + } + else if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK; + + // Get distance from bot to self->nearest_path_point + vec3_t bot_to_path_vec; // Direction vector to nodes + VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); + float bot_to_path_dist = VectorLength(bot_to_path_vec); + //ucmd->sidemove = SPEED_WALK * sqrtf(bot_to_path_dist / 256); + + bot_to_path_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_path_vec); + VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = -SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK; + + // Get distance from bot to self->nearest_path_point + vec3_t bot_to_path_vec; // Direction vector to nodes + VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); + float bot_to_path_dist = VectorLength(bot_to_path_vec); + //ucmd->sidemove = -SPEED_WALK * sqrtf(bot_to_path_dist / 256); + + bot_to_path_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_path_vec); + VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it + } + } + + /* + // Check if bot can see the nodes + if (can_see_next) + { + //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + + //bot_to_next_vec[2] = 0; // Flatten the vector + //VectorNormalize(bot_to_next_vec); + //if (self->groundentity) + //{ + // ucmd->forwardmove = SPEED_RUN; + // VectorScale(bot_to_next_vec, 400, self->velocity); + //} + //else + ucmd->forwardmove = SPEED_RUN; + + //if (bot_to_next_dist < 64) + //BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2); // self->move_vector + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) + { + Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + //ucmd->forwardmove = SPEED_RUN; + bot_to_next_vec[2] = 0; // Flatten the vector + VectorScale(bot_to_next_vec, 400, self->velocity); + //VectorClear(self->velocity); + } + + } + else if (can_see_curr) + { + Com_Printf("%s %s guiding bot: can_see_curr %f\n", __func__, self->client->pers.netname, bot_to_curr_dist); + ucmd->forwardmove = SPEED_RUN; + BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_curr_vec, 2); // self->move_vector + } + else if (can_see_prev) + { + Com_Printf("%s %s guiding bot: can_see_prev %f\n", __func__, self->client->pers.netname, bot_to_prev_dist); + ucmd->forwardmove = SPEED_RUN; + BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_prev_vec, 2); // self->move_vector + } + else + { + Com_Printf("%s %s guiding bot: Wander\n", __func__, self->client->pers.netname); + // Wander or Reroute + } + */ + + self->s.angles[PITCH] = 0; // Look directly forward + return; + } + + + { + //ucmd->forwardmove = SPEED_RUN/2; + //self->s.angles[PITCH] = 0; // Look directly forward + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // Change the speed when close to node + //return; + } + + + + + + + + + + + + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + //Com_Printf("%s: %s bot_to_next_dist %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + + // Guide bot into the next_node when close + if (bot_to_next_dist < 24) + { + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + bot_to_next_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_next_vec); + VectorScale(bot_to_next_vec, 200, self->velocity); + //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); + return; + } + } + + + // Change bot angle to face next node + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 11.25) == false) + { + VectorClear(self->velocity); // Kill velocity, to give bot time to turn + return; + } + + + // Strafe if we're off course + { + // If not strafing to get back on path + // Check if path forward is blocked + qboolean did_strafe_right = false; // If the bot made a strafe move right + qboolean did_strafe_left = false; // If the bot made a strafe move left + //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + //if (mov_strafe == 0 && tr.fraction < 1.0) + if (bot_to_next_dist > 64) + { + qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right + qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left + + qboolean mid_partial_blocked = false; + qboolean mid_full_blocked = false; + + // Line trace + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + mid_partial_blocked = true; + + // Box trace -- full width of the bot + tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + mid_full_blocked = true; + } + + // All [BLOCKED] + if (mid_partial_blocked && right_blocked && left_blocked) + { + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + + // Bot -> nearest path point + tr = gi.trace(self->s.origin, NULL, NULL, self->nearest_path_point, self, MASK_DEADSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + //Com_Printf("%s %s --> heading for nearest path point\n", __func__, self->client->pers.netname); + + // Move vector between bot and self->nearest_path_point + VectorSubtract(self->nearest_path_point, self->s.origin, self->move_vector); + + self->move_vector[2] = 0; // Flatten the vector + VectorNormalize(self->move_vector); + VectorScale(self->move_vector, 400, self->velocity); + return; + } + + // Bot -> current node point + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + Com_Printf("%s %s --> heading for current node point\n", __func__, self->client->pers.netname); + + // Move vector between bot and nodes[self->bot.current_node].origin + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + + self->move_vector[2] = 0; // Flatten the vector + VectorNormalize(self->move_vector); + VectorScale(self->move_vector, 400, self->velocity); + return; + } + + // Bot -> re-route + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + if (self->bot.current_node == INVALID) + { + Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + return; + } + + return; + } + + // mid [BLOCKED] && right [CLEAR] && left [CLEAR] + if (mid_partial_blocked && !right_blocked && !left_blocked) + { + //Com_Printf("%s: %s partial middle blocked\n", __func__, self->client->pers.netname); + + if (ucmd->sidemove > 0) // Bot was strafing right, so continue + { + //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else if (ucmd->sidemove < 0) // Bot was strafing left, so continue + { + //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + else // Bot was not strafing, so pick a random direction + { + if (rand() % 2 == 0) + { + //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else + { + //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + } + + // When bot encounters a thin pole, prevent it from getting stuck + if (mid_full_blocked && forward_or_back_velocity == 0) + { + //Com_Printf("%s: %s full middle blocked\n", __func__, self->client->pers.netname); + + if (ucmd->sidemove > 0) // Bot was strafing right, so continue + { + //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else if (ucmd->sidemove < 0) // Bot was strafing left, so continue + { + //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + else // Bot was not strafing, so pick a random direction + { + if (rand() % 2 == 0) + { + //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else + { + //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + } + + // NARROW strafe search -- RIGHT or LEFT [BLOCKED] + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> small strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + else if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> small strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + + // WIDE strafe search + if (!mid_partial_blocked && left_blocked && right_blocked) + { + //Com_Printf("%s: %s small left and right blocked\n", __func__, self->client->pers.netname); + + // Try a wider search + right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 32); // Check right + left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -32); // Check left + + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> medium strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + + if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> medium strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + + if (left_blocked && right_blocked) + { + //Com_Printf("%s: %s medium left and right blocked\n", __func__, self->client->pers.netname); + + // Try the widest search + right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 48); // Check right + left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -48); // Check left + + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> large strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + + if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> large strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + } + } + + // Stay on course by strafing back to the path line (if not already strafing) + if (did_strafe_right == false && did_strafe_left == false && mid_partial_blocked == false) + { + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + if (mov_strafe == 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = 0; + } + else if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + ucmd->sidemove = SPEED_WALK; + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + ucmd->sidemove = -SPEED_WALK; + } + } + } + + ucmd->forwardmove = SPEED_RUN; + return; + } + + + + + + + + + //distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); + // If stuck, try clearing the obstacle by moving in a random dir + if (VectorLength(self->velocity) <= 8) + { + //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) > 32) + //Com_Printf("%s %s is stuck [NODE_MOVE]...\n", __func__, self->client->pers.netname); + + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0) // blocked, try find another path to the same goal + { + //Com_Printf("%s %s is blocked [NODE_MOVE]...\n", __func__, self->client->pers.netname); + + // Try strafing + if (random() > 0.5) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + + // Try strafing + if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else + ucmd->sidemove = 0; + + self->bot_strafe = ucmd->sidemove; + + if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_RUN; + // Also try move backwards + else if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + + return; + } + + /* + if (random() > 0.25) + { + //if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -SPEED_RUN; + if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + + self->s.angles[YAW] += 22.5 + (random() * 270); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + return; + } + */ + + + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //BOTLIB_CanVisitNode(self, self->bot.goal_node); + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + //ACEMV_ChangeBotAngle(self); + + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + //BOTLIB_MOV_ChangeBotAngleYawPitch(self); + /////////////////////////////////// LaunchPlayer(self, nodes[self->bot.current_node].origin); //////////////////////////// + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + //VectorNormalize(dist); + //VectorScale(dist, 400, self->velocity); // Apply it + //self->velocity[2] = 50; + + /* + //if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -SPEED_RUN; + if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + + //self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[YAW] += 180; + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + self->s.angles[PITCH] = 0; + */ + + // Make the bot look at the current node + //self->bot.next_node = self->bot.current_node; + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + //BOTLIB_MOV_ChangeBotAngleYawPitch(self); + //ucmd->forwardmove = SPEED_RUN; + + + + //self->bot.state = STATE_WANDER; + return; + } + } + + // If we cannot see the next node + tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1) + { + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + + // Run up stairs + if (current_node_type == NODE_STEP && next_node_type == NODE_STEP) + { + ucmd->forwardmove = SPEED_RUN; + self->s.angles[PITCH] = 0; // Look directly forward + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //Com_Printf("%s %s Step --> Step\n", __func__, self->client->pers.netname); + return; + } + + /* + // If stuck, crouch or jump + if (VectorLength(self->velocity) < 3) + { + Com_Printf("%s %s is stuck...\n", __func__, self->client->pers.netname); + + trace_t tr_lower_body; + trace_t tr_upper_body; + vec3_t fwd; + AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin + VectorMA(self->s.origin, 16, fwd, fwd); + tr_lower_body = gi.trace(self->s.origin, tv(-5, -5, -14), tv(5, 5, 0), fwd, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + tr_upper_body = gi.trace(self->s.origin, tv(-5, -5, 0), tv(5, 5, 32), fwd, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + // Need to crouch? + // Lower body is free, upper body is blocked + if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) + { + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + Com_Printf("%s %s is stuck [NODE_MOVE]: crouching\n", __func__, self->client->pers.netname); + return; + } + // Need to jump? + // Lower body is blocked, upper body is free + else if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) + { + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + Com_Printf("%s %s is stuck [NODE_MOVE]: jumping\n", __func__, self->client->pers.netname); + return; + } + else + { + //self->bot.state = STATE_WANDER; + //return; + } + } + */ + + + // If the bot fell below the target (failed the jump down) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node type to jumppad + if (targetNodeLink != INVALID) + { + //if (debug_mode) + //Com_Printf("%s %s dropped from a move forward, upgrading move to jumppad node\n", __func__, self->client->pers.netname); + + nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; + + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s [MOVE_NODE][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + return; + } + else + { + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + //if (debug_mode) + //Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + + //if (debug_mode) + //Com_Printf("%s %s failed to move forward to goal %d, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + + //self->bot.state = STATE_WANDER; // Aquire a new node + //return; + } + } + + self->s.angles[PITCH] = 0; // Look directly forward + + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + + // Check we've reach our ideal yaw angle + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + /* + // See if we're stuck, try to unstick + if (VectorLength(self->velocity) < 8) + { + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + } + else + ucmd->upmove = -400; // Try crouching + + // See if the way forward is clear + tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1) + { + // Try strafing left + tr = gi.trace(self->s.origin, tv(-32, -16, -14), tv(0, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction == 1) + { + ucmd->sidemove = -400; + } + else + { + // Try strafing right + tr = gi.trace(self->s.origin, tv(0, -16, -14), tv(32, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction == 1) + { + ucmd->sidemove = 400; + } + } + } + } + */ + + // Guide the bot in when close to the target + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////distance = VectorLength(dist); + if (distance <= 32) + { + //tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + tr = gi.trace(self->s.origin, tv(-14, -14, -6), tv(14, 14, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + + ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. + + // Apply it + //if (!ACEMV_CanMove(self, MOVE_FORWARD)) + // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down + //else + // VectorScale(dist, 128, self->velocity); + //VectorScale(dist, 224, self->velocity); + + if (distance <= 2) + VectorScale(dist, 128, self->velocity); + //else if (distance <= 16) + // VectorScale(dist, 224, self->velocity); + else + VectorScale(dist, 320, self->velocity); + + //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); + + return; + } + } + + /* + if (next_node_type == INVALID) + { + if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + //ACEAI_PickLongRangeGoal(self); + return; + } + } + */ + + //ACEMV_ChangeBotAngle(self); + + /* + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////dist[2] = 0; + ////distance = VectorLength(dist); + if (!self->groundentity) + { + Com_Printf("%s %s [MOVE_NODE] air guide...\n", __func__, self->client->pers.netname); + + // When in air, control speed carefully to avoid overshooting the next node. + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + } + + //rekkie -- DEV_1 -- s + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + Com_Printf("%s %s [MOVE_NODE] move guide...\n", __func__, self->client->pers.netname); + + if (distance < 128) + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); + else + ucmd->forwardmove = SPEED_WALK; + } + else + { + // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + } + */ + + ucmd->forwardmove = SPEED_RUN; + //rekkie -- DEV_1 -- e + + } +#endif +} + +qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint) +{ + /* + vec3_t forward; + + // immediately turn to where we need to go + float length = VectorDistance(self->s.origin, targetpoint); + float fwd_speed = length * 1.95f; + + vec3_t dir; + PredictAim(self, self->enemy, self->s.origin, fwd_speed, false, 0.f, &dir, nullptr); + + self->s.angles[1] = vectoyaw(dir); + AngleVectors(self->s.angles, forward, nullptr, nullptr); + self->s.origin[2] += 1; + self->velocity = forward * fwd_speed; + self->velocity[2] = 450; + */ + + + + float fordward_speed; + vec3_t forward, dir, vec, target; + + VectorCopy(targetpoint, target); + + // Get height diff from self to target point + // If > 0, target is higher than self + // If < 0, target is lower than self + //float height_diff = targetpoint[2] - (self->s.origin[2] - self->viewheight); // 56 - 22 = 34 + float height_diff = target[2] - self->s.origin[2]; // 56 - 22 = 34 + + //if (height_diff < 0) + target[2] += 32; + //else + // targetpoint[2] -= 32; + + fordward_speed = VectorDistance(self->s.origin, target); // Get forward speed + if (VectorEmpty(target)) // If Distance is zero + return false; + + //fordward_speed = fordward_speed * 1.95f; + //fordward_speed = fordward_speed * 0.2f; + + VectorSubtract(target, self->s.origin, dir); // Get direction + //dir[2] += self->viewheight; // Add eye height to direction height + float distance = VectorLength(dir); // Get distance + + float time = distance / fordward_speed; + + vec[0] = target[0] + time; + vec[1] = target[1] + time; + vec[2] = target[2] + time; + + // Adjust the z value by the height difference + //if (height_diff != 0) + // vec[2] += height_diff; + + // If vector is pointing the wrong direction (pointing backwards) + VectorNormalize(dir); + vec3_t dir2; + VectorSubtract(vec, self->s.origin, dir2); + VectorNormalize(dir2); + float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); + if (dot < 0) + { + VectorCopy(target, vec); + } + else + { + // if the shot is going to impact a nearby wall from our prediction, just fire it straight. + trace_t tr = gi.trace(self->s.origin, NULL, NULL, vec, NULL, MASK_SOLID); + if (tr.fraction < 0.9f) + { + VectorCopy(target, vec); + } + } + + //vec[2] += self->viewheight; // Add eye height to direction height + + VectorSubtract(vec, self->s.origin, dir); // Using + VectorNormalize(dir); + + vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; + angles[1] = vectoyaw(dir); + AngleVectors(angles, forward, NULL, NULL); // Get forward vector + VectorScale(forward, fordward_speed, self->velocity); // Scale the forward vector by the forward speed to get a velocity + self->s.origin[2] += 1; // Adjust player slightly off the ground + + + //self->velocity[0] = forward[0] * fordward_speed; + //self->velocity[1] = forward[1] * fordward_speed; + //self->velocity[2] = 450; + + if (height_diff > -2) + self->velocity[2] = 432 + height_diff; + else + { + if (fordward_speed + height_diff > 400) + self->velocity[2] = 450 + height_diff; + else + self->velocity[2] = fordward_speed + height_diff; + + if (self->velocity[2] < 300) + self->velocity[2] = fordward_speed; + + if (fordward_speed <= 64) + self->velocity[2] = 450; + else if (fordward_speed <= 96) + self->velocity[2] = 550; + else if (fordward_speed <= 128) + self->velocity[2] = 650; + else + self->velocity[2] = 450; + } + + self->velocity[2] = 432 + height_diff; + + + + + + + + //Com_Printf("%s %s speed:%f hdiff:%f vel[2]:%f\n", __func__, self->client->pers.netname, fordward_speed, height_diff, self->velocity[2]); + + //self->velocity[2] = fwd_speed <= 400 ? (fwd_speed + height_diff) : (400 + height_diff); + + + + /* + if (fwd_speed <= 64) + self->velocity[2] = 300 + height_diff; + else if (fwd_speed <= 96) + self->velocity[2] = 350 + height_diff; + else if (fwd_speed <= 128) + self->velocity[2] = 400 + height_diff; + else + self->velocity[2] = 432 + height_diff; + */ + + /* + // peak speeds on flat surface + velocity[0]; // 800 + velocity[1]; // 870 + velocity[2]; // 260 + speed; // 937 + */ + + self->bot.jumppad = true; // Successful jump + self->bot.node_jump_from = self->bot.current_node; + self->bot.node_jump_to = self->bot.next_node; + self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again + self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place + + return true; +} + +//rekkie -- Quake3 -- s +// predictive calculator +// target is who you want to shoot +// start is where the shot comes from +// bolt_speed is how fast the shot is (or 0 for hitscan) +// eye_height is a boolean to say whether or not to adjust to targets eye_height +// offset is how much time to miss by +// aimdir is the resulting aim direction (pass in nullptr if you don't want it) +// aimpoint is the resulting aimpoint (pass in nullptr if don't want it) +void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint) +{ + vec3_t dir, vec; + float dist, time; + + if (target) // Use entity's origin,viewheight,velocity if we can + { + VectorCopy(target->s.origin, target_point); + target_viewheight = target->viewheight; + VectorCopy(target->velocity, target_velocity); + } + + VectorSubtract(target_point, start, dir); + if (eye_height) + dir[2] += target_viewheight; + dist = VectorLength(dir); + + // [Paril-KEX] if our current attempt is blocked, try the opposite one + vec3_t end; + VectorMA(start, dist, dir, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + + //if (tr.ent != target) + { + eye_height = !eye_height; + VectorSubtract(target_point, start, dir); + if (eye_height) + dir[2] += target_viewheight; + dist = VectorLength(dir); + } + + if (bolt_speed) + time = dist / bolt_speed; + else + time = 0; + + // Calculate the jump height to get to the target + //float gravity = self->gravity * sv_gravity->value; + //float jump_height = sqrt(2 * (gravity * dist)); + //time = dist / (jump_height / 2); // Calculate the time it will take to get to the target + + //vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); + //vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); + //vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); + vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); + vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); + vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); + + // went backwards... + VectorNormalize(dir); + vec3_t dir2; + VectorSubtract(vec, start, dir2); + VectorNormalize(dir2); + float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); + if (dot < 0) + VectorCopy(target_point, vec); + else + { + // if the shot is going to impact a nearby wall from our prediction, just fire it straight. + tr = gi.trace(start, NULL, NULL, vec, NULL, MASK_SOLID); + if (tr.fraction < 0.9f) + VectorCopy(target_point, vec); + } + + if (eye_height) + vec[2] += target_viewheight; + + if (aimdir) + { + VectorSubtract(vec, start, *aimdir); + VectorNormalize(*aimdir); + } + + if (aimpoint) + { + VectorCopy(vec, *aimpoint); + } +} + +qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity) +{ + float length, fwd_speed; + vec3_t forward, dir; + + if (self->bot.jumppad_last_time > level.framenum) // Can we jump again so soon? + return false; + + if (self->client->leg_damage) // Can't jump if legs are damaged + return false; + + //Com_Printf("%s %s -- LETS GO!\n", __func__, self->client->pers.netname); + + // Zero our horizontal velocity + //target_velocity[0] = 0; + //target_velocity[1] = 0; + //target_velocity[2] = 0; + + // immediately turn to where we need to go + if (target) + { + length = VectorDistance(self->s.origin, target->s.origin); + VectorCopy(target->s.origin, target_point); + } + else + { + length = VectorDistance(self->s.origin, target_point); + } + if (VectorEmpty(target_point)) + return false; + + // Get height diff from self to target point + // If > 0, target is higher than self + // If < 0, target is lower than self + float height_diff = target_point[2] - (self->s.origin[2] - 24); + + /* + //if (height_diff < 0.1) + // height_diff = 0; + float modifier = 1.5; + if (height_diff > 0) + modifier = (height_diff / 60) + 1.5; + else if (height_diff < 0) + modifier = 1.7 - (fabs(height_diff) / 200); + + //fwd_speed = length * 1.95f; + //fwd_speed = length; + //fwd_speed = (length + 32) * modifier; + + fwd_speed = (length * 1.1) + (height_diff); + */ + //Com_Printf("%s z[%f] mod[%f] len[%f]\n", __func__, height_diff, modifier, length); + + if (self->is_bot && self->bot.current_node != INVALID && self->current_link != INVALID) + { + fwd_speed = length; // * nodes[self->bot.current_node].links[self->current_link].targetNodeJumpPower; // Self adjusting jump power + //Com_Printf("%s %s fwd_speed[%f]\n", __func__, self->client->pers.netname, fwd_speed); + } + else + { + fwd_speed = length; + } + + + + if (target) + { + vec3_t target_point, target_velocity; + BOTLIB_PredictJumpPoint(self, target, target_point, 0, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); + } + else + BOTLIB_PredictJumpPoint(self, NULL, target_point, target_viewheight, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); + + vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; + angles[1] = vectoyaw(dir); + AngleVectors(angles, forward, NULL, NULL); + self->s.origin[2] += 1; + VectorScale(forward, fwd_speed, self->velocity); + + //print big jump + //if (length < 360) + // Com_Printf("%s %s: Small Jump [%f]\n", __func__, self->client->pers.netname, length); + //else + // Com_Printf("%s %s: Big Jump [%f]\n", __func__, self->client->pers.netname, length); + + /* + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (target && target->s.origin[2] >= self->s.origin[2]) + { + z_height = (target->s.origin[2] - self->s.origin[2]); + z_height += sqrtf(length) + NODE_Z_HEIGHT; + self->velocity[2] += z_height; + } + else if (target_point[2] >= self->s.origin[2]) + { + z_height = (target_point[2] - self->s.origin[2]); + z_height += sqrtf(length) + NODE_Z_HEIGHT; + self->velocity[2] += z_height; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + { + self->velocity[2] = 375; // 450 + } + */ + + /* + if (length <= 64) + self->velocity[2] = 400 + height_diff; // 450 + else + self->velocity[2] = 432 + height_diff; + */ + + if (length <= 64) + self->velocity[2] = 300 + height_diff; + else if (length <= 96) + self->velocity[2] = 350 + height_diff; + else if (length <= 128) + self->velocity[2] = 400 + height_diff; + else + self->velocity[2] = 432 + height_diff; + + + + + /* + // peak speeds on flat surface + velocity[0]; // 800 + velocity[1]; // 870 + velocity[2]; // 260 + speed; // 937 + */ + /* + // Get the surface normal of the ground the player is standing on + trace_t tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, MASK_PLAYERSOLID); + + //if (self->velocity[0] > 800) self->velocity[0] = 800; + //if (self->velocity[1] > 870) self->velocity[1] = 870; + if (self->velocity[2] > 260) self->velocity[2] = 260; + if (tr.plane.normal[2] < 1.0) + { + float diff = 1.0 - tr.plane.normal[2]; + diff *= 2.0; + diff += tr.plane.normal[2]; + self->velocity[2] *= diff; + } + // calculate velocity speed + float speed = VectorLength(self->velocity); + Com_Printf("%s speed[%f] velocity_peak[%f %f %f]\n", __func__, speed, self->velocity[0], self->velocity[1], self->velocity[2]); + */ + + + + + + /* + // get height diff from self to target + qboolean above = false; + if (target && target->s.origin[2] >= self->s.origin[2]) + above = true; + else if (target_point[2] >= self->s.origin[2]) + above = true; + + float height_diff; + if (target) + height_diff = fabs(target->s.origin[2] - self->s.origin[2]); + else + height_diff = fabs(target_point[2] - self->s.origin[2]); + Com_Printf("%s %s: Jump [%f] Height[%f]\n", __func__, self->client->pers.netname, length, height_diff); + if (length < 256) + { + if (height_diff < 32) + self->velocity[2] = 200; + else + self->velocity[2] = 400; + } + else if (length < 384) + self->velocity[2] = 425; + else + self->velocity[2] = 450; + */ + + self->bot.jumppad = true; // Successful jump + self->bot.node_jump_from = self->bot.current_node; + self->bot.node_jump_to = self->bot.next_node; + self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again + self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place + + return true; +} + +// Predict enemy position based on their position, velocity, and our velocity +void BOTLIB_PredictEnemyOrigin(edict_t *self, vec3_t out, float multiplier) +{ + // FRAMETIME + /* + out[0] = self->enemy->s.origin[0] + ((self->enemy->velocity[0] + self->velocity[0]) * (10.0 / HZ)); + out[1] = self->enemy->s.origin[1] + ((self->enemy->velocity[1] + self->velocity[1]) * (10.0 / HZ)); + + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // Predict lower if enemy is crouching + out[2] = (self->enemy->s.origin[2] - 24) + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); + else // Predict when standing + out[2] = self->enemy->s.origin[2] + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); + */ + + if (rand() % 2 == 0) + multiplier -= (multiplier * 2); + + //Com_Printf("%s %f\n", __func__, FRAMETIME); + VectorAdd(self->velocity, tv(self->enemy->velocity[0] * multiplier, self->enemy->velocity[1] * multiplier, self->enemy->velocity[2]), out); + VectorScale(out, FRAMETIME, out); + VectorAdd(out, self->enemy->s.origin, out); + //VectorCopy(self->enemy->s.origin, out); + if (self->enemy->maxs[2] == CROUCHING_MAXS2) + out[2] -= 24; + + if (self->client->weapon == FindItemByNum(KNIFE_NUM)) + { + // Quake 2 uses x,y,z which is stored in a float array called vec3_t[] + // The Z component is for jumping and falling + // Throwing knife affected by gravity over time + //knife->velocity[2] -= sv_gravity->value * FRAMETIME; // velocity is a vec3_t[3] which is a float array + // My origin + self->s.origin; // origin is a vec3_t[3] which is a float array + // Enemy origin + self->enemy->s.origin; // origin is a vec3_t[3] which is a float array + // My pitch aim (looking directly forward is a pitch of 0) + // #define PITCH 0 + // #define YAW 1 + // #define ROLL 2 + self->s.angles[PITCH]; + + // I want to find the aiming pitch requires to reach a target location with a throwing knife that drops with gravity. + // Quake2 uses x,y,z coordinates stored in a vec3_t[3] which is a float array [0] [1] [2] + // I have a first-person 3D game. A player origin, a target, gravity (on the Z axis only), and a throwing knife that is affected by gravity. By default the player looks forward at a pitch of 0. Looking directly up is -90 and down is 90. + float g = sv_gravity->value; // Gravity + float distance = VectorDistanceXY(self->s.origin, self->enemy->s.origin); // XY Distance to enemy + float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference + float knife_speed = 1200; + // Using the distance to my enemy and the gravity of the knife what pitch would I need to set to reach my enemy? + + // Calculate total flight time + //float t = sqrtf((2 * distance) / sv_gravity->value); + + + + } + +} + + + + +///////////////// +// Look dir +///////////////// +void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) +{ + vec3_t lookdir = { 0 }; // Direction to look + trace_t tr; + //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + float turn_speed = 0.3; + qboolean reached_look_at = false; + + if (VectorEmpty(self->bot.bi.look_at) == false) + { + reached_look_at = BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.15, true, true); + + if (reached_look_at == false) + return; + } + + + + // Look at ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) + { + // Check we're looking at ladder + qboolean looking_at_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + if (looking_at_ladder) + self->bot.touching_ladder = true; + } + // Turn to find ladder + if (looking_at_ladder == false) + { + int additional_rotation = 0; + for (int i = 0; i < 16; i++) + { + vec3_t fwd = { 0 }, end = { 0 }; + + self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 + + float yaw_rad = DEG2RAD(self->s.angles[YAW]); + + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder + { + self->bot.touching_ladder = true; + VectorSubtract(end, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 10.0, true, true); + if (additional_rotation >= 1) + break; + additional_rotation++; + } + } + } + + // Look up or down when on ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = -45; //-89 + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = 45; // 89 + } + } + } + // Look at enemy + else if (self->enemy) // Track target + { + qboolean not_infront = false; // If target is in front + + // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, + // the bot will keep looking in that general direction, but not directly at the enemy's pos + if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) + { + if (BOTLIB_Infront(self, self->enemy, 0.3) == false) + not_infront = true; + + if (1) // Predicted enemy pos + { + // Predict enemy position based on their velocity and distance + vec3_t predicted_enemy_origin = { 0 }; + BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 2); + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t red = MakeColor(255, 255, 255, 255); // red + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(self->enemy->s.number, predicted_enemy_origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 200, false); + DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, red, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + } + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy + /* + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + */ + } + else // Not predicted + { + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + } + } + else // Look where enemy was last + { + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t enemy_eyes; + VectorCopy(self->enemy->s.origin, enemy_eyes); + enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); + else + goto LookAhead; // Looking at a wall, so just look at nodes + } + /* + // Adjust turn speed based on skill + //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + if (self->bot.skill >= MAX_BOTSKILL) + { + turn_speed = 0.2; + } + //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) + { + //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold + } + + if (turn_speed > 1.0) + { + if (not_infront) + turn_speed = 1.0; + + if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim + turn_speed = 0.2; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap + turn_speed = 0.1; + else if (rand() % 10 == 0) // Bots sometimes get aim snap + turn_speed = 0.5; + + turn_speed = 1.0; + } + */ + + //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); + + //if (self->client->weaponstate != WEAPON_READY) + // not_infront = rand() % 2; + + /* + if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) + { + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + else + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + */ + } + /* + // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link + else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) + { + // See if this node has any POI lookat links + int num_lookat_nodes = 0; + int lookat_nodes[MAXLINKS] = { 0 }; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) + { + lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; + num_lookat_nodes++; + } + } + if (num_lookat_nodes) // One or more POI lookat nodes found + { + // When the timer is up pick a new node to look at + if (self->bot.node_poi_look_time < level.framenum) + { + self->bot.node_poi_look_time = level.framenum + 5 * HZ; + self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node + } + + // Look at POI lookat node + VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); + } + } + */ + else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + // If no enemy, have bot look at PNOISES, if any. + else if (1) + { + int type = 0; + int player_num = INVALID; + float nearest = 9999999; + qboolean found_target = false; + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) + { + //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); + float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_SELF; + nearest = dist; + player_num = i; + } + } + } + } + if (player_num == INVALID) + { + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + } + } + } + + if (player_num != INVALID) + { + vec3_t noise_origin; + + if (type == PNOISE_WEAPON) + VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); + else if (type == PNOISE_SELF) + VectorCopy(botlib_noises.self_origin[player_num], noise_origin); + else if (type == PNOISE_IMPACT) + VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); + + + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t noise_origin_eyes; + VectorCopy(noise_origin, noise_origin_eyes); + noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level + trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + { + VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + else + goto LookAhead; + } + else + goto LookAhead; + } + // If no enemy, look at nodes in front/behind, or look at map center + else // if (self->enemy == NULL) + { + + LookAhead: + + const int look_ahead = 5; + qboolean found_viable_node = false; + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list + { + /* + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + int look_node = self->bot.node_list[(i + look_ahead)]; + if (nodes[look_node].num_links) + { + int rnd_link = rand() % nodes[look_node].num_links + 1; + int rnd_node = nodes[look_node].links[rnd_link].targetNode; + if (rnd_node != INVALID) + { + VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); + found_viable_node = true; + break; + } + } + } + */ + + // Look x nodes ahead + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + found_viable_node = true; + break; + } + // Look x nodes behind + else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + found_viable_node = true; + break; + } + else + break; + } + //if (found_viable_node) + // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + //else + // Com_Printf("%s no node [%d]\n", __func__, level.framenum); + + if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + + if (0 && found_viable_node == false) + { + // If no enemy, look at random thing + { + // If no enemy, look at random item or map center + // Search through map looking for a random item, or NULL + //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) + if (self->bot.bi.look_at_time < level.framenum) + { + self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds + + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); + //if (tr.fraction == 1.0) + { + VectorCopy(map_ents->s.origin, self->bot.bi.look_at); + break; + } + } + } + if (VectorEmpty(self->bot.bi.look_at)) + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + else + VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item + + } + + // If lookdir is empty, then look at map center + if (VectorEmpty(self->bot.bi.look_at)) + { + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + } + + BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, turn_speed, true, true); + } + } +} + +void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) +{ + //if (teamplay->value && lights_camera_action > 0) + // return; + + int next_node = INVALID; // Next node to walk towards + //vec3_t lookdir = { 0 }; // Direction to look + vec3_t walkdir; // Direction to walk + vec3_t lastdir; + float move_speed = SPEED_RUN; // Movement speed, default to running + self->bot.bi.speed = 0; // Zero speed + trace_t tr; // Trace + + // Remove flags + //self->bot.bi.actionflags = 0; + //self->enemy = NULL; + + // Prevent stuck suicide if holding position + if ((self->bot.bi.actionflags & ACTION_HOLDPOS)) + self->suicide_timeout = level.framenum + 10; + + // Check how far we've moved + qboolean moved = true; + VectorSubtract(self->s.origin, self->lastPosition, lastdir); + float move_dist = VectorLength(lastdir); + if (move_dist < FRAMETIME) + moved = false; // We've not moved + + + // If the bot is near the get_item they're after, and the item is inuse + // inuse == false if the item was picked up and waiting to respawn + // inuse == true if the item has spawned in and is ready to be picked up + if (self->bot.get_item != NULL && self->bot.get_item->inuse) + { + float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); + //Com_Printf("%s %s is looking for item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + + // item spawnflags + //#define ITEM_TRIGGER_SPAWN 0x00000001 + //#define ITEM_NO_TOUCH 0x00000002 + // 6 bits reserved for editor flags + // 8 bits used as power cube id bits for coop games + //#define DROPPED_ITEM 0x00010000 + //#define DROPPED_PLAYER_ITEM 0x00020000 + //#define ITEM_TARGETS_USED 0x00040000 + // + // Edict Flags + // #define FL_RESPAWN 0x80000000 // used for item respawning + // + // + // TP item not picked up + // spawnflags = 0 + // flags = 0 + // + // TP item picked up and in bot inventory + // spawnflags = ITEM_TARGETS_USED 0x00040000 + // flags = FL_RESPAWN 0x80000000 + // + // TP item picked up then dropped by bot + // spawnflags = DROPPED_ITEM + // flags = 0 + // + // TP item picked up, bot killed, dropping all items to ground + // spawnflags = DROPPED_PLAYER_ITEM + // flags = 0 + + trace_t tr = gi.trace(self->s.origin, NULL, NULL, self->bot.get_item->s.origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && item_dist <= 128) // Might want to do a trace line to see if bot can actually see item + { + //Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to item + VectorNormalize(walkdir); + + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_NONE) + { + //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + else if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + + if (item_dist < 128) + self->bot.bi.speed = SPEED_ROAM; // Slow down when close + + if (self->bot.get_item->solid == SOLID_NOT); // picked up + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + self->bot.get_item = NULL; + return; + } + + return; + + } + //else if (item_dist <= 64) + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + //self->bot.get_item = NULL; + } + } + + /* + // Get current and next node back from nav code. + if (!BOTLIB_FollowPath(self)) + { + //Com_Printf("%s %s BOTLIB_FollowPath == false\n", __func__, self->client->pers.netname); + //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); + + self->bot.bi.speed = 0; + self->bot.bi.actionflags = 0; + + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + return; + } + */ + + if (self->bot.touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) + { + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + } + +#if 0 + ///////////////// + // Look dir + ///////////////// + qboolean touching_ladder = false; + //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) + { + // Check if touching ladder + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + //VectorMA(self->s.origin, 1, fwd, end); + //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); + + VectorMA(self->s.origin, 1, fwd, end); + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + + if (touching_ladder == false) + { + VectorMA(self->s.origin, 8, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (touching_ladder == false) + { + VectorMA(self->s.origin, 16, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (touching_ladder == false) + { + VectorMA(self->s.origin, 32, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + } + } + //if (touching_ladder) + // Com_Printf("%s touching_ladder [%d]\n", __func__, level.framenum); + if (touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) + { + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + touching_ladder = false; + } + + // Look at ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) + { + // Check we're looking at ladder + qboolean looking_at_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + if (looking_at_ladder) + touching_ladder = true; + } + // Turn to find ladder + if (looking_at_ladder == false) + { + int additional_rotation = 0; + for (int i = 0; i < 16; i++) + { + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 + + float yaw_rad = DEG2RAD(self->s.angles[YAW]); + + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder + { + touching_ladder = true; + VectorSubtract(end, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 3.0, true, true); + if (additional_rotation >= 1) + break; + additional_rotation++; + } + } + } + + // Look up or down when on ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = -45; //-89 + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = 45; // 89 + } + } + } + // Look at enemy + else if (self->enemy) // Track target + { + qboolean not_infront = false; // If target is in front + + // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, + // the bot will keep looking in that general direction, but not directly at the enemy's pos + if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) + { + if (BOTLIB_Infront(self, self->enemy, 0.3) == false) + not_infront = true; + + if (1) // Predicted enemy pos + { + // Predict enemy position based on their velocity and distance + vec3_t predicted_enemy_origin = { 0 }; + BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 1); + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(self->enemy->s.number, predicted_enemy_origin, blue, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + } + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy + /* + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + */ + } + else // Not predicted + { + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + } + } + else // Look where enemy was last + { + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t enemy_eyes; + VectorCopy(self->enemy->s.origin, enemy_eyes); + enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); + else + goto LookAhead; // Looking at a wall, so just look at nodes + } + + // Adjust turn speed based on skill + //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + if (self->bot.skill >= MAX_BOTSKILL) + { + turn_speed = 0.2; + } + //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) + { + //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold + } + + if (turn_speed > 1.0) + { + if (not_infront) + turn_speed = 2.0; + + if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim + turn_speed = 0.2; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 10 == 0) // Bots sometimes get aim snap + turn_speed = 0.5; + } + + //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); + + //if (self->client->weaponstate != WEAPON_READY) + // not_infront = rand() % 2; + + if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) + { + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, false); + } + else + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + /* + // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link + else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) + { + // See if this node has any POI lookat links + int num_lookat_nodes = 0; + int lookat_nodes[MAXLINKS] = { 0 }; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) + { + lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; + num_lookat_nodes++; + } + } + if (num_lookat_nodes) // One or more POI lookat nodes found + { + // When the timer is up pick a new node to look at + if (self->bot.node_poi_look_time < level.framenum) + { + self->bot.node_poi_look_time = level.framenum + 5 * HZ; + self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node + } + + // Look at POI lookat node + VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); + } + } + */ + else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + // If no enemy, have bot look at PNOISES, if any. + else if (1) + { + int type = 0; + int player_num = INVALID; + float nearest = 9999999; + qboolean found_target = false; + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) + { + //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); + float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_SELF; + nearest = dist; + player_num = i; + } + } + } + } + if (player_num == INVALID) + { + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + } + } + } + + if (player_num != INVALID) + { + vec3_t noise_origin; + + if (type == PNOISE_WEAPON) + VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); + else if (type == PNOISE_SELF) + VectorCopy(botlib_noises.self_origin[player_num], noise_origin); + else if (type == PNOISE_IMPACT) + VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); + + + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t noise_origin_eyes; + VectorCopy(noise_origin, noise_origin_eyes); + noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level + trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + { + VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + else + goto LookAhead; + } + else + goto LookAhead; + } + // If no enemy, look at nodes in front/behind, or look at map center + else // if (self->enemy == NULL) + { + + LookAhead: + + const int look_ahead = 5; + qboolean found_viable_node = false; + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list + { + /* + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + int look_node = self->bot.node_list[(i + look_ahead)]; + if (nodes[look_node].num_links) + { + int rnd_link = rand() % nodes[look_node].num_links + 1; + int rnd_node = nodes[look_node].links[rnd_link].targetNode; + if (rnd_node != INVALID) + { + VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); + found_viable_node = true; + break; + } + } + } + */ + + // Look x nodes ahead + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + found_viable_node = true; + break; + } + // Look x nodes behind + else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + found_viable_node = true; + break; + } + else + break; + } + //if (found_viable_node) + // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + //else + // Com_Printf("%s no node [%d]\n", __func__, level.framenum); + + if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + + if (0 && found_viable_node == false) + { + // If no enemy, look at random thing + { + // If no enemy, look at random item or map center + // Search through map looking for a random item, or NULL + //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) + if (self->bot.bi.look_at_time < level.framenum) + { + self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds + + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); + //if (tr.fraction == 1.0) + { + VectorCopy(map_ents->s.origin, self->bot.bi.look_at); + break; + } + } + } + if (VectorEmpty(self->bot.bi.look_at)) + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + else + VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item + + } + + // If lookdir is empty, then look at map center + if (VectorEmpty(self->bot.bi.look_at)) + { + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + } + + BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, 1.0, true, true); + } + } +#endif + + // Do not follow path when teammates are still inside us. + if (OnTransparentList(self)) // Teamplay + { + // If the bot has just spawned, then we need to wait a bit before we start moving. + if (self->just_spawned) // set by SpawnPlayers() in a_team.c + { + self->just_spawned = false; + self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. + + // If enemy is in sight, don't wait too long + if (self->enemy) + self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait + else + { + // Otherwise pick from various wait times before moving out + int rnd_rng = rand() % 4; + if (rnd_rng == 0) + self->just_spawned_timeout = level.framenum + (random() * 10) * HZ; // Long wait + else if (rnd_rng == 1) + self->just_spawned_timeout = level.framenum + (random() * 5) * HZ; // Medium wait + else if (rnd_rng == 2) + self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait + else + self->just_spawned_timeout = 0; // No wait + } + + self->bot.bi.actionflags |= ACTION_HOLDPOS; + return; + } + // Wait + if (self->just_spawned_go && self->just_spawned_timeout > level.framenum && self->bot.see_enemies == false) + { + self->bot.bi.actionflags |= ACTION_HOLDPOS; + return; // It's not time to move yet, wait! + } + // Go! + if (self->just_spawned_go || self->bot.see_enemies) + { + //BOTLIB_PickLongRangeGoal(self); + self->just_spawned_go = false; // Now we can move! + } + } + + + if (self->groundentity && self->bot.next_node == INVALID) // No next node, pick new nav + { + Com_Printf("%s %s next_node is invalid; find a new path\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + // On ground and current or next node is invalid + //if (self->groundentity && (self->bot.current_node == INVALID || self->bot.next_node == INVALID)) + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node + if (self->groundentity && self->bot.current_node == INVALID) + { + //self->bot.state = BOT_MOVE_STATE_NAV; + //self->bot.bi.speed = 0; + //return; + + Com_Printf("%s %s on ground and current_node is invalid; try to wander\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.stuck_wander_time = 1; + } + if (self->groundentity && self->bot.goal_node == INVALID)// && self->bot.node_travel_time >= 15) + { + Com_Printf("%s %s on ground and goal_node is invalid; find a new path\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + + + + /* + // If next node is INVALID, assume we're stuck + //if (self->bot.next_node == INVALID || self->bot.current_node == INVALID || self->bot.goal_node == INVALID) + if (self->bot.goal_node == INVALID) + { + self->bot.stuck_wander_time = 2; + //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); + } + else */ + next_node = self->bot.next_node; + + /* + if (self->bot.node_travel_time > 30) + { + self->bot.node_travel_time = 0; + + // Wander + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + } + */ + + //if (self->bot.goal_node != INVALID && nav_area.total_areas > 0 && self->bot.node_travel_time >= 120) + { + //Com_Printf("%s %s ATTEMPTING TO FIX cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + //if (BOTLIB_CanVisitNode(self, self->bot.goal_node, false, INVALID)) + { + //self->bot.node_travel_time = 0; + //Com_Printf("%s %s FIXED node_travel_time cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + } + + // If travel time took too long, assume we're stuck + if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node + { + self->bot.stuck_wander_time = 1; + Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + + //self->bot.stuck_wander_time = 0; + + if (self->bot.stuck_wander_time)// && nav_area.total_areas <= 0) + { + self->bot.stuck_wander_time--; + self->bot.node_travel_time = 0; + + //Com_Printf("%s %s stuck_wander cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + + //Com_Printf("%s %s stuck_wander BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname); + //BOTLIB_PickLongRangeGoal(self); // pick a new long range goal + // Wander + //if (nav_area.total_areas <= 0) + { + Com_Printf("%s %s stuck_wander\n", __func__, self->client->pers.netname); + self->bot.state = BOT_MOVE_STATE_NAV; + self->bot.goal_node = INVALID; + } + //return; + + + VectorCopy(self->bot.stuck_random_dir, walkdir); + + if (self->groundentity) + { + self->bot.stuck_last_negate = 0; // Reset if we touched ground + } + + float fwd_distance = 32; + tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope + fwd_distance = 128; // Extend the distance we check for a safe direction to move toward + //Com_Printf("%s %s tr.plane.normal[%f] \n", __func__, self->client->pers.netname, tr.plane.normal[2]); + + qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false); + if (can_move == false || VectorEmpty(self->bot.stuck_random_dir)) + { + // Try to aquire a safe random direction to move toward + qboolean success = false; + for (int i = 0; i < 8; i++) + { + if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, true) == false) + continue; + else + { + success = true; + break; + } + } + if (success) + { + VectorCopy(walkdir, self->bot.stuck_random_dir); + // vect to angles + vec3_t angles; + vectoangles(walkdir, angles); + //Com_Printf("%s %s found random direction [%f %f %f]\n", __func__, self->client->pers.netname, angles[0], angles[1], angles[2]); + } + else + { + //Com_Printf("%s %s ACTION_HOLDPOS N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge + if (level.framenum > self->bot.stuck_last_negate) + { + // Only allow this to happen once every 60 seconds + // It's reset after 60 seconds, death, or touching ground again + self->bot.stuck_last_negate = level.framenum + 60 * HZ; + + //Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Reverse the direction + VectorNegate(self->velocity, self->velocity); + } + //self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + //BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + //return; + } + } + else + { + //Com_Printf("%s %s heading for random dir\n", __func__, self->client->pers.netname); + //VectorCopy(walkdir, self->bot.stuck_random_dir); + //VectorCopy(self->bot.stuck_random_dir, walkdir); + } + + vec3_t hordir; + VectorNormalize(walkdir); + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = move_speed; // Set our suggested speed + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_NONE) + { + //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + else if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + + return; + } + else + { + /* + qboolean fetch_item = false; + if (self->bot.get_item != NULL) + { + float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); + Com_Printf("%s %s is looking for item %s [%f] sf[0x%x] f[0x%x]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist, self->bot.get_item->spawnflags, self->bot.get_item->flags); + + // TP item not picked up + // spawnflags = 0 + // flags = 0 + // + // TP item picked up and in bot inventory + // spawnflags = ITEM_TARGETS_USED + // flags = FL_RESPAWN + // + // TP item picked up then dropped by bot + // spawnflags = DROPPED_ITEM + // flags = 0 + // + // TP item picked up, bot killed, dropping all items to ground + // spawnflags = DROPPED_PLAYER_ITEM + // flags = 0 + + if (item_dist <= 128) + { + Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to next node + VectorNormalize(walkdir); + fetch_item = true; + + if (item_dist <= 16) + { + Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + self->bot.get_item = NULL; + } + } + //else if (item_dist <= 64) + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + //self->bot.get_item = NULL; + } + } + */ + //if (fetch_item == false) + { + if (next_node == INVALID) + { + //self->bot.stuck_wander_time = 1; + return; + } + VectorSubtract(nodes[next_node].origin, self->s.origin, walkdir); // Head to next node + VectorNormalize(walkdir); + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, walkdir); // Head to next node + } + } + + //qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, 32, NODE_MAX_CROUCH_FALL_HEIGHT, false); + //if (can_move == false) + // self->bot.stuck_wander_time = 1; + + if (self->bot.stuck_wander_time) + return; + + + //if (self->bot.state == STATE_WANDER || self->bot.goal_node == INVALID) + // Com_Printf("%s %s [%d] INVALID\n", __func__, self->client->pers.netname, level.framenum); + + //qboolean perform = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + /* + if (perform == false && moved == false) + { + //self->bot.bi.actionflags |= (ACTION_JUMP); // Try jumping anyway + //self->bot.stuck_wander_time = 1; + } + */ + + /* + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + */ + + // Strafe to get back on track + if (0 && self->bot.next_node != INVALID) + { + // Get distance from bot to next node + //float dist = VectorDistance(self->s.origin, nodes[self->bot.next_node].origin); + // Get distanced from current node to next node + //float dist2 = VectorDistance(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin); + + byte mov_strafe = 0; + float dot = BOTLIB_DirectionCheck(self, &mov_strafe); + if (dot > 0.7 && dot < 0.99) // .995 + { + //float dist = VectorDistance(self->s.origin, nodes[self->bot.current_node].origin); + //if (dist > 64) + { + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + VectorSubtract(self->nearest_path_point, self->s.origin, walkdir); // Head to current node + VectorNormalize(walkdir); + vec3_t hordir; + hordir[0] += walkdir[0]; + hordir[1] += walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + } + + + + /* + //move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + + //BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + + if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + // Get the direction perpendicular to the dir, facing left + vec3_t right; + right[0] = walkdir[0]; + right[1] = -walkdir[0]; + right[2] = 0; + VectorNormalize(right); + VectorCopy(right, self->bot.bi.dir); + move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + // Get the direction perpendicular to the dir, facing left + vec3_t left; + left[0] = -walkdir[1]; + left[1] = walkdir[1]; + left[2] = 0; + VectorNormalize(left); + VectorCopy(left, self->bot.bi.dir); + move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + } + else + { + move_speed = SPEED_RUN; // Slow down if we're moving in the right direction + } + */ + } + } + + //self->bot.bi.speed = move_speed; // Set our suggested speed + + if (0) + { + // Get current direction + vec3_t angle, forward, right, start, end, origin, offset; + vectoangles(walkdir, angle); + AngleVectors(angle, forward, right, NULL); + VectorCopy(self->s.origin, origin); + //origin[2] -= 24; // From the ground up + origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) + + VectorSet(offset, 0, 0, 0); // changed from 18,0,0 + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1024; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + + if (1 & self->bot.node_list_count) + { + //for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) + for (int i = 1; i < self->bot.node_list_count; i++) + { + if (self->bot.next_node == self->bot.node_list[i]) + { + self->bot.current_node = self->bot.node_list[i - 1]; + break; + } + } + if (self->bot.node_list_count) + { + //self->bot.current_node = self->bot.node_list[self->bot.node_list_current]; + } + } + + // Get current and next node types + int next_node_type = INVALID; + int current_node_type = INVALID; + if (self->bot.current_node != INVALID && self->bot.next_node != INVALID) + { + current_node_type = nodes[self->bot.current_node].type; + + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + int target_node = nodes[self->bot.current_node].links[i].targetNode; + if (target_node == self->bot.next_node) + { + next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + + //self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type + //self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + break; + } + + /* + // Try search surrounding nodes to see if they contain next_node + for (int j = 0; j < nodes[target_node].num_links; j++) + { + int target_target_node = nodes[target_node].links[j].targetNode; + for (int k = 0; k < nodes[target_target_node].num_links; k++) + { + int target_target_target_node = nodes[target_target_node].links[k].targetNode; + if (target_target_target_node == self->bot.next_node) + { + next_node_type = nodes[target_target_target_node].links[i].targetNodeType; // Next node type + Com_Printf("%s %s using target_target_target_node\n", __func__, self->client->pers.netname); + break; + } + } + } + */ + } + } + + //if (current_node_type == INVALID || next_node_type == INVALID) + if (next_node_type == INVALID) + { + //next_node_type = NODE_MOVE; + //Com_Printf("%s %s invalid types node:curr/next[%d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, current_node_type, next_node_type); + + if (self->bot.next_node == INVALID) + { + Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + if (self->bot.current_node == self->bot.next_node && self->bot.current_node == self->bot.goal_node) + { + Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + ////if (nav_area.total_areas > 0) + //// self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + ////else + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + else if (self->bot.next_node != INVALID && VectorDistance(nodes[self->bot.next_node].origin, self->s.origin) <= 128) + { + /* + // Try search surrounding nodes to see if they contain next_node + qboolean resolved = false; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + if (next_node_type != INVALID) break; + int target_node = nodes[self->bot.current_node].links[i].targetNode; + if (target_node == INVALID) continue; + + for (int j = 0; j < nodes[target_node].num_links; j++) + { + if (next_node_type != INVALID) break; + int target_target_node = nodes[target_node].links[j].targetNode; + if (target_target_node == INVALID) continue; + + for (int k = 0; k < nodes[target_target_node].num_links; k++) + { + int target_target_target_node = nodes[target_target_node].links[k].targetNode; + if (target_target_target_node == INVALID) continue; + + if (target_target_target_node == self->bot.next_node) + { + if (VectorDistance(nodes[target_target_target_node].origin, self->s.origin) <= 128) + { + next_node_type = nodes[target_target_node].links[i].targetNodeType; // Next node type + Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + break; + } + } + } + } + } + */ + + //if (next_node_type != INVALID) + // Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + if (next_node_type == INVALID) + { + //Com_Printf("%s %s invalid types node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); + + // Path failed, so try again + //BOTLIB_CanVisitNode(self, self->bot.goal_node, false, nodes[self->bot.goal_node].area); + + /* + if (self->bot.goal_node != INVALID) + BOTLIB_CanGotoNode(self, self->bot.goal_node, false); + else + self->bot.state = BOT_MOVE_STATE_NAV; + */ + + return; + } + + //VectorCopy(nodes[self->bot.goal_node].origin, self->s.origin); + + //self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 1.0 * HZ; + //self->bot.goal_node = INVALID; + //return; + } + + if (current_node_type != INVALID && next_node_type != INVALID) + { + // DEBUG: print current and next node types + if (0) + { + const qboolean printOnlyErrors = false; // If only printing INVALID nodes + PrintAllLinkNodeTypes(self, printOnlyErrors); + } + + // Set the default movement: this will be overriden by the movement code below + //if (next_node_type == NODE_MOVE || next_node_type == NODE_CROUCH || next_node_type == NODE_JUMP || next_node_type == NODE_BOXJUMP || next_node_type == NODE_LADDER) + { + //* + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + //*/ + //VectorCopy(walkdir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + } + + // move, crouch, or jump + if (next_node_type == NODE_WATER) + { + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_CROUCH; + + //VectorClear(self->bot.bi.dir); + //self->bot.bi.speed = 0; + + VectorNormalize(walkdir); + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = move_speed; // Set our suggested speed + + // Check that we're in the water + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + + // Almost out of air, start heading up to the surface + if ((contents_feet & MASK_WATER) && self->air_finished_framenum < level.framenum + 5) // Move up to get air + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + self->bot.node_travel_time = 0; // Ignore node travel time while we get some air + //Com_Printf("%s %s [%d] water: get air\n", __func__, self->client->pers.netname, level.framenum); + } + else if ((contents_feet & MASK_WATER) && fabs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]) <= 33) // Roughly leveled out + { + //Com_Printf("%s %s [%d] water: level\n", __func__, self->client->pers.netname, level.framenum); + } + else if ((contents_feet & MASK_WATER) && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) // Move up + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + //Com_Printf("%s %s [%d] water: move up\n", __func__, self->client->pers.netname, level.framenum); + } + else if (nodes[self->bot.next_node].origin[2] < self->s.origin[2]) // Move down + { + if (contents_feet & MASK_WATER) + self->bot.bi.actionflags |= ACTION_MOVEDOWN; // In water moving down + else + self->bot.bi.actionflags |= ACTION_CROUCH; // Crouch drop down into water below + + //Com_Printf("%s %s [%d] water: move down\n", __func__, self->client->pers.netname, level.framenum); + } + + } + + // Pull bot in when close to the next node, help guide it in + if (0) + { + if (self->groundentity == false && level.framenum > self->bot.stuck_last_negate) + { + vec3_t bot_to_node; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node + if (xy_bot_to_next_dist > 32 && xy_bot_to_next_dist <= 128) + { + // Line of sight + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + self->bot.stuck_last_negate = level.framenum + 1 * HZ; + vec3_t dir; + dir[0] = walkdir[0]; + dir[1] = walkdir[1]; + dir[2] = walkdir[2]; + VectorNormalize(dir); + self->velocity[0] += 50 * dir[0]; + self->velocity[1] += 50 * dir[1]; + self->velocity[2] += 300 * dir[2]; + // Limit velocity + if (self->velocity[2] < -300) + self->velocity[2] = -300; + //Com_Printf("%s %s [%d] directing bot closer to next node\n", __func__, self->client->pers.netname, level.framenum); + } + } + } + } + + if (next_node_type == NODE_MOVE) + { + /* + if (BOTLIB_CanMoveDir(self, walkdir) == false) + { + // We can't move in this direction + Com_Printf("%s %s can't move safely in direction\n", __func__, self->client->pers.netname); + self->bot.stuck_wander_time = 15; + self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + return; + } + */ + /* + float fwd_distance = 32; + tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope + fwd_distance = 128; // Extend the distance we check for a safe direction to move toward + + // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge + if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false) == false); + { + if (level.framenum > self->bot.stuck_last_negate) + { + // Only allow this to happen once every 60 seconds + // It's reset after 60 seconds, death, or touching ground again + self->bot.stuck_last_negate = level.framenum + 60 * HZ; + + Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Reverse the direction + VectorNegate(self->velocity, self->velocity); + VectorClear(self->bot.bi.dir); + } + } + */ + } + + //if (next_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMP) + // BOTLIB_Crouch_Or_Jump(self, ucmd, dir); + + if (next_node_type == NODE_BOXJUMP) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + self->bot.bi.actionflags |= ACTION_BOXJUMP; + self->bot.bi.actionflags &= ~ACTION_JUMP; + self->bot.bi.actionflags &= ~ACTION_CROUCH; + } + else + { + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + self->bot.bi.actionflags &= ~ACTION_BOXJUMP; + self->bot.bi.actionflags &= ~ACTION_JUMP; + } + } + + // Slow down if we're not on the ground + if (next_node_type == NODE_MOVE) + { + if (self->groundentity == NULL) + { + //self->bot.bi.speed = SPEED_CAREFUL; // Set our speed directly + self->bot.bi.speed = SPEED_ROAM; // Set our speed directly + //Com_Printf("%s %s SPEED_CAREFUL node_travel_time[%d]\n", __func__, self->client->pers.netname, self->bot.node_travel_time); + } + //if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2] + 32) // Going up + //{ + // self->bot.bi.actionflags |= ACTION_JUMP; + //} + else + { + // Remove flags + //self->bot.bi.actionflags &= ~ACTION_CROUCH; + //self->bot.bi.actionflags &= ~ACTION_JUMP; + + //if (nodes[self->bot.next_node].normal[2] > 0.7) // If the next node is flat + //if (random() < 0.1) + // self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + } + if (next_node_type == NODE_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_JUMP; + } + + + if (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER) + { + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = 100; // Set speed slower for ladders + + if (self->bot.touching_ladder) + { + // Remove flags + self->bot.bi.actionflags &= ~ACTION_ATTACK; // Don't attack when on ladder + + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + self->bot.bi.actionflags |= ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + } + } + else if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Jump to ladder + { + self->bot.bi.actionflags |= ACTION_JUMP; + } + } + /* + //if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) + if (touching_ladder == false && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) + { + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = 400; // Set speed slower for ladders + + //if (touching_ladder) + { + self->bot.bi.actionflags |= ACTION_JUMP; + + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + } + } + */ + // Make sure the bot gets off the top and bottom of the ladder + if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 4 && self->groundentity == NULL) // + { + if (nodes[self->bot.prev_node].origin[2] < nodes[self->bot.current_node].origin[2] && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 2) // Getting off ladder at the top + { + //Com_Printf("%s %s MOVE UP\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; + self->bot.bi.actionflags |= ACTION_MOVEUP; + //self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + else if (nodes[self->bot.prev_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Getting off ladder at the bottom + { + //Com_Printf("%s %s MOVE DOWN\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; + //self->bot.bi.actionflags |= ACTION_MOVEDOWN; + //self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + } + //if (self->bot.prev_node != INVALID) + { + //Com_Printf("%s %s types[ %d %d %d ]\n", __func__, self->client->pers.netname, nodes[self->bot.prev_node].type, nodes[self->bot.current_node].type, nodes[self->bot.next_node].type); + } + + if (next_node_type == NODE_JUMP && self->groundentity) + { + //Com_Printf("%s %s ACTION_JUMP\n", __func__, self->client->pers.netname); + self->bot.bi.actionflags |= ACTION_JUMP; + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + } + + // Handle jumping + if (next_node_type == NODE_JUMPPAD) + { + //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + self->bot.bi.actionflags &= ~ACTION_JUMP; + + //self->bot.bi.actionflags = 0; + //VectorClear(self->bot.bi.dir); + + // Bot only applies direction when it's in a falling state + if (self->groundentity || self->velocity[2] > 0) + self->bot.bi.speed = 0; + else + { + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + } + self->bot.bi.actionflags |= ACTION_JUMPPAD; + //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); + + //Com_Printf("%s %s NODE_JUMPPAD\n", __func__, self->client->pers.netname); +#if 0 + // Get distance from bot to node + nodes[self->bot.current_node].origin; + vec3_t bot_to_node; + + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node + + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_curr_dist = VectorLength(bot_to_node); // Distance from bot to current node + //if (xy_bot_to_curr_dist <= 32) // If close enough to jump pad + + + float distance = 16; + vec3_t bmins = { nodes[self->bot.current_node].absmin[0] + -(distance), nodes[self->bot.current_node].absmin[1] + -(distance), nodes[self->bot.current_node].absmin[2] + -(distance) }; + vec3_t bmaxs = { nodes[self->bot.current_node].absmax[0] + distance, nodes[self->bot.current_node].absmax[1] + distance, nodes[self->bot.current_node].absmax[2] + distance }; + if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) + { + Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + self->bot.bi.actionflags |= ACTION_JUMPPAD; + //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); + + // Trace up to see if we're going to hit our head + tr = gi.trace(self->s.origin, tv(-16, -16, -0), tv(16, 16, 32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] + 60), self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Trace down to see if we hit the ground + tr = gi.trace(self->s.origin, tv(-32, -32, -0), tv(32, 32, 0), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 60), self, MASK_PLAYERSOLID); + qboolean is_player = false; + if (tr.ent && tr.ent->client) + is_player = true; + + /* + // If landing on flat ground, only allow jumping when once we touch the ground + // Otherwise if its a slope, allow jumping when we're close'ish to the ground + qboolean can_jump = true; + //if (tr.plane.normal[2] == 1 && self->groundentity == NULL && self->velocity[2] < 0) // flat ground + if (self->groundentity == NULL && self->velocity[2] < 0) // flat ground + { + can_jump = false; + self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + Com_Printf("%s %s ACTION_HOLDPOS\n", __func__, self->client->pers.netname); + } + */ + + if ((tr.fraction < 1 || tr.startsolid) && is_player == false) // && can_jump) + { + //float height_diff = nodes[self->bot.next_node].origin[2] - self->s.origin[2]; // Height difference between bot and next node + //if (xy_bot_to_next_dist <= 32 && height_diff <= 60) + { + // Bot jumped + //Com_Printf("%s %s jumped\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMP; + } + //else + { + // Trace from bot to node to see if we can jump to it + //tr = gi.trace(self->s.origin, tv(-24, -24, -STEPSIZE), tv(24, 24, 96), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); + //if (tr.fraction == 1.0) + { + //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; // Stop moving + } + } + } + } + } +#endif + //if (moved == false) + } + } + + if (ctf->value) + { + // Slow down if near flag + if (bot_ctf_status.flag1_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin) < 128) + { + self->bot.bi.speed = 200; + } + if (bot_ctf_status.flag2_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin) < 128) + { + self->bot.bi.speed = 200; + } + } + + if (self->bot.see_enemies) + { + qboolean dodging = ((rand() % 10) < 7); + + // Don't dodge if < x links on node - low link count might indicate a tight walk or narrow way + if (nodes[self->bot.current_node].num_links < 4 || nodes[self->bot.next_node].num_links < 4) + dodging = false; + + // Don't dodge if bot is taking a direct path + if (self->bot.node_random_path == false) + dodging = false; + + if (ctf->value) // Reduce dodging in CTF + { + //float f1 = BOTLIB_DistanceToFlag(self, FLAG_T1_NUM); + //float f2 = BOTLIB_DistanceToFlag(self, FLAG_T2_NUM); + + float f1 = 99999999; + float f2 = 99999999; + if (bot_ctf_status.flag1_curr_node != INVALID) + f1 = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + if (bot_ctf_status.flag2_curr_node != INVALID) + f2 = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + + if (f1 < 1500 || f2 < 1500 || BOTLIB_Carrying_Flag(self)) // || self->bot.goal_node == bot_ctf_status.flag1_curr_node || self->bot.goal_node == bot_ctf_status.flag2_curr_node) + { + dodging = false; + //Com_Printf("%s %s dodging is OFF ------ \n", __func__, self->client->pers.netname); + } + + //dodging = false; + } + + // Try strafing around enemy + trace_t tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_SHOT); + if (dodging && tr.plane.normal[2] > 0.85) // Not too steep + { + // Try strafing continually in a general direction, if possible + static int max_strafe_left = -10; + static int max_strafe_right = 10; + if (self->bot_strafe == 0) // Pick new direction + { + float strafe_choice = random() < 0.333; + if (strafe_choice < 0.33) // Left + self->bot_strafe = -1; + else if (strafe_choice < 0.66) // Right + self->bot_strafe = 1; + else + self->bot_strafe = 0; // Neither - skip strafing this turn + } + + if (self->client->weapon == FindItem(HC_NAME)) + self->bot_strafe = 0; // Don't strafe with HC, just go straight for them + + if (self->bot_strafe < 0 && random() > 0.15) // Going left 85% of the time + { + if (BOTLIB_CanMove(self, MOVE_LEFT) && self->bot_strafe >= max_strafe_left) // Can go left with a limit + { + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe--; + self->bot.bi.actionflags |= ACTION_MOVELEFT; + } + else if (BOTLIB_CanMove(self, MOVE_RIGHT)) // Cannot go left anymore, so try going right + { + self->bot_strafe = 1; // Go right + self->bot.bi.actionflags |= ACTION_MOVERIGHT; + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else if (self->bot_strafe > 0 && random() > 0.15) // Going right 85% of the time + { + if (BOTLIB_CanMove(self, MOVE_RIGHT) && self->bot_strafe <= max_strafe_right) // Can go right with a limit + { + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe++; + self->bot.bi.actionflags |= ACTION_MOVERIGHT; + } + else if (BOTLIB_CanMove(self, MOVE_LEFT)) // Cannot go right anymore, so try going left + { + self->bot_strafe = -1; // Go left + self->bot.bi.actionflags |= ACTION_MOVELEFT; + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else + self->bot_strafe = 0; // Skip strafing this turn + + // Back off if getting too close (unless we have a HC or knife) + if (self->bot.enemy_dist < 256) + { + if (self->client->weapon == FindItem(HC_NAME) && self->client->cannon_rds) + { + // Come in close for the kill + if (ACEMV_CanMove(self, MOVE_FORWARD)) + self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + else if (self->client->weapon == FindItem(KNIFE_NAME)) + { + // Come in close for the kill + if (ACEMV_CanMove(self, MOVE_FORWARD)) + self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + // Try move backwards + else if (BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + } + // If distance is far, consider crouching to increase accuracy + else if (self->bot.enemy_dist > 1024) + { + // Check if bot should be crouching based on weapon and if strafing + if (INV_AMMO(self, LASER_NUM) == false && + self->client->weapon != FindItem(SNIPER_NAME) && + self->client->weapon != FindItem(HC_NAME) && + self->client->weapon != FindItem(M3_NAME) && + (self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && + (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) + { + // Raptor007: Don't crouch if it blocks the shot. + float old_z = self->s.origin[2]; + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); + } + self->s.origin[2] = old_z; + } + } + else + { + // Keep distance with sniper + if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(SNIPER_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + // Keep distance with grenade + if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + // Otherwise move toward target + //else if (ACEMV_CanMove(self, MOVE_FORWARD)) + // self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + + // If the bot is dodging by strafing, add in some random jumps + if (self->bot_strafe != 0 && (self->bot.bi.actionflags & ACTION_CROUCH) == 0 && random() < 0.2) + self->bot.bi.actionflags |= ACTION_BOXJUMP; // Jump while dodging + } + } + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad + } + + // Stay on course by strafing back to the path line (if not already strafing) + //if (VectorLength(self->velocity) < 37) // If stuck + if (moved == false) + { + // Try strafing + qboolean right_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, 8); // Check right + qboolean left_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, -8); // Check left + if (right_blocked && left_blocked) // Both are blocked + { + self->bot.bi.actionflags |= ACTION_JUMP; // Try jumping to clear the obstacle + //Com_Printf( "%s %s blocked\n", __func__, self->client->pers.netname); + + + /* + // Bot stuck - Test of bot has LoS to next node + if (self->bot.next_node == INVALID) + { + self->bot.stuck = true; + return; + } + trace_t tr = gi.trace(self->s.origin, tv(-4, -4, 0.1), tv(4, 4, 56), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1.0) + { + if (VectorLength(self->velocity) < 5) + self->bot.stuck = true; + } + */ + //self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + //self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); + //self->bot.bi.viewangles[PITCH] = 0; + } + else if (right_blocked && BOTLIB_CanMove(self, MOVE_LEFT)) // Strafe left + { + // Get the direction perpendicular to the dir, facing left + vec3_t left; + left[0] = -walkdir[1]; + left[1] = walkdir[0]; + left[2] = 0; + VectorNormalize(left); + VectorCopy(left, self->bot.bi.dir); + + + //Com_Printf("%s %s ACTION_MOVELEFT\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_MOVELEFT; + } + else if (left_blocked && BOTLIB_CanMove(self, MOVE_RIGHT)) // Strafe right + { + // Get the direction perpendicular to the dir, facing right + vec3_t right; + right[0] = walkdir[1]; + right[1] = -walkdir[0]; + right[2] = 0; + VectorNormalize(right); + VectorCopy(right, self->bot.bi.dir); + + //Com_Printf("%s %s ACTION_MOVERIGHT\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_MOVERIGHT; + } + else + { + //Com_Printf("%s %s moved == false and not blocked on side\n", __func__, self->client->pers.netname); + //if (VectorLength(self->velocity) < 5) + // self->bot.stuck = true; + } + } + + /* + // Project forward looking for walls + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 48, forward, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); + + // If we hit an obstacle or we've not moved much, turn around + if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37) + { + self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); + self->bot.bi.viewangles[PITCH] = 0; + } + */ +} +//rekkie -- Quake3 -- e + +//////////////////// +// Wandering code // +//////////////////// +// +// Basic wandering code, simply here as a backup to help the bot find a nearby node +// +void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd) +{ +#if 0 + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + + // Do not move + if (self->next_move_time > level.framenum) + return; + + //Com_Printf("%s %s wandering...\n", __func__, self->client->pers.netname); + + // Special check for elevators, stand still until the ride comes to a complete stop. + if (self->groundentity && (VectorLength(self->groundentity->velocity) >= 8)) + { + // only move when platform not + if (self->groundentity->moveinfo.state == STATE_UP || self->groundentity->moveinfo.state == STATE_DOWN) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 0; + self->next_move_time = level.framenum + 0.5 * HZ; + return; + } + } + + // Contents check + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] += 22; + int contents_head = gi.pointcontents(temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + + // Just try to keep our head above water. + if (contents_head & MASK_WATER) + { + // If drowning and no node, move up + if (self->client->next_drown_framenum > 0) + { + ucmd->upmove = SPEED_RUN; + self->s.angles[PITCH] = -45; + } + else + ucmd->upmove = SPEED_WALK; + } + + // Don't wade in lava, try to get out! + if (contents_feet & (CONTENTS_LAVA | CONTENTS_SLIME)) + ucmd->upmove = SPEED_RUN; + + + + // Check speed + VectorSubtract(self->s.origin, self->lastPosition, temp); + float moved = VectorLength(temp); + + if (contents_feet & MASK_WATER) + { + self->bot.goal_node = INVALID; + self->bot.current_node = INVALID; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); + } + + + + // Crouch or jump + trace_t tr_lower_body; + trace_t tr_upper_body; + //vec3_t fwd; + //AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin + //VectorMA(self->s.origin, 50, fwd, fwd); + //tr_lower_body = gi.trace(self->s.origin, tv(-15, -15, -15), tv(15, 15, 0), forward, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + //tr_upper_body = gi.trace(self->s.origin, tv(-15, -15, 0), tv(15, 15, 32), forward, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 50, forward, end); + tr_lower_body = gi.trace(start, tv(-15, -15, -15), tv(15, 15, 0), end, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + tr_upper_body = gi.trace(start, tv(-15, -15, 0), tv(15, 15, 32), end, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + + // Need to crouch? + // Lower body is free, upper body is blocked + if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) + { + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: crouching\n", __func__, self->client->pers.netname); + return; + } + // Need to jump? + // Lower body is blocked, upper body is free + if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) + { + if (contents_feet & MASK_WATER) + { + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: jumping out of water\n", __func__, self->client->pers.netname); + self->s.angles[PITCH] = -45; + //VectorScale(end, 50, self->velocity); + ucmd->forwardmove = 400; + //ucmd->upmove = 400; + if (self->velocity[2] < 350) + self->velocity[2] = 350; + return; + } + + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: jumping\n", __func__, self->client->pers.netname); + + return; + } + // In water facing a ladder + //if ((contents_feet & MASK_WATER) && tr_lower_body.fraction < 1.0 && tr_upper_body.fraction < 1.0) + if ((contents_feet & MASK_WATER) && ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER))) + { + //if ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER)) + { + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: MASK_WATER -> CONTENTS_LADDER\n", __func__, self->client->pers.netname); + ucmd->forwardmove = 400; + ucmd->upmove = 400; + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY * 3, NODE_ALL); + self->s.angles[PITCH] = -45; + if (self->velocity[2] < 350) + self->velocity[2] = 350; + return; + } + } + + + + // Project forward looking for walls + //vec3_t start, end; + //vec3_t forward, right; + //vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 48, forward, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); + + // If we hit an obstacle or we've not moved much, turn around + if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37 || moved < FRAMETIME) + { + self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[PITCH] = 0; + + if (contents_feet & MASK_WATER) // Just keep swimming. + ucmd->forwardmove = SPEED_RUN; + else if (!M_CheckBottom(self) && !self->groundentity) // if there is ground continue otherwise wait for next move + ucmd->forwardmove = 0; + else if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_WALK; + + return; + } + + // Otherwise try move forward normally + if (ACEMV_CanMove(self, MOVE_FORWARD) || (contents_feet & MASK_WATER)) + { + ucmd->forwardmove = SPEED_RUN; + } + + //if (self->client->leg_damage > 0) // Do we have leg damage? + // return; + + // If no goal, try to get a new one + if (self->bot.goal_node == INVALID) + ACEAI_PickLongRangeGoal(self); + + // Try to move to our goal if we can + if (self->bot.goal_node != INVALID) + { + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s could not find FindClosestReachableNode to reach goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s %s Wander finding alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + //return; + + self->bot.next_node = self->bot.current_node; + self->bot.state = STATE_POSITION; + } + else // We couldn't visit the goal node, so lets pick a new goal + { + if (debug_mode) + Com_Printf("%s %s Wander cannot visit goal %d. Picking new goal\n", __func__, self->client->pers.netname, self->bot.goal_node); + ACEAI_PickLongRangeGoal(self); + } + } +#endif +} diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c new file mode 100644 index 000000000..e59be96f6 --- /dev/null +++ b/src/action/botlib/botlib_nav.c @@ -0,0 +1,3639 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + + + //== GLOBAL SEMAPHORE == +int antSearch; + +extern short int** path_table; +////extern short int path_table[MAX_PNODES][MAX_PNODES]; + +qboolean nodeused[MAX_PNODES]; // This is used for a FAST check if the node has been used +short int nodefrom[MAX_PNODES]; // Stores how we got here once the node is closed +float nodeweight[MAX_PNODES]; // The weight of the node based on distance + +/* ========================================================= +The basic system works by using a single linked list and accessing information from the node array + +1) The current node is found +2) All links from it are propogated - if not already done by another node +3) If we haven't found the target node then we get the next open node and go to 1 +4) If we have the target node we return the path to it +5) If we run out of nodes then we return INVALID + +This idea is based on "Path Finding Via 'Ant Races' (a floodfill algorithm)" +by Richard Wesson. It is easy to optimise this code to make it even more efficient. + +============================================================= */ + + +//========================= +// Init the search path variables +//========================= +void AntInitSearch(edict_t* ent) +{ + //Make sure the lists and arrays used are all set to the correct values and EMPTY! + memset(nodeused, 0, sizeof(nodeused)); + memset(nodefrom, INVALID, sizeof(nodefrom)); + while (!SLLempty(&ent->pathList)) + { + SLLpop_front(&ent->pathList); + } +} + +///////////////////////////////////////////////////////// +// Check if any bot is navigating to the selected node // +///////////////////////////////////////////////////////// +qboolean Botlib_Nav_NodeInUse(edict_t* self, int node) +{ + //Com_Printf("%s ===============\n", __func__); + + if (node == INVALID) + return false; + + short int i; + for (i = 0; i < num_players; i++) + { + if (players[i]->is_bot == false || players[i] == self || players[i]->solid == SOLID_NOT || ACEAI_IsEnemy(self, players[i]) == false) + continue; + + botlib_sll_t* list = &players[i]->pathList; + if (list == NULL || list->head == NULL) // No path, or no head + continue; + + botlib_sll_nodes_t* sll_node = list->head; + + // Setup nodes + int current_node = players[i]->bot.current_node; + int next_node = sll_node->node; + + // Loop through the linked list + while (sll_node != NULL) // If no next + { + // Check if we're done, also sanity check + if (current_node == players[i]->bot.goal_node || current_node == INVALID || next_node == INVALID) + break; + + if (next_node == node) + { + //if (players[i]->client) + // Com_Printf("%s node[%d] is in use by %s\n", __func__, node, players[i]->client->pers.netname); + return true; + } + + //Com_Printf("%s curr[%d] next[%d] goal[%d]\n", __func__, current_node, next_node, self->bot.goal_node); + + + // Get next node + current_node = next_node; + if ((sll_node = sll_node->next) == NULL) // No next node + break; + next_node = sll_node->node; + + } + } + + //Com_Printf("%s ===============\n\n", __func__); + + return false; +} + +//========================= +// StartSearch +//========================= +// +// returns true if a path is found +// false otherwise +// +int quick = 0; +int normal = 0; +qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization) +{ + // Safety first! + if (from == INVALID || to == INVALID) + return false; + + //rekkie -- Dijkstra pathing -- s + if (1) + { + //clock_t clock_begin = clock(); // Start performance clock + //if (AntQuickPath(ent, from, to)) + { + //Com_Printf("%s AntQuickPath()\n", __func__); + + //clock_t clock_end = clock(); + //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; + //if (res == 0) quick++; + //Com_Printf("%s execution took %f seconds. Quick %d\n", __func__, res, quick); + + //return true; + } + //else + { + //BOTLIB_THREADED_DijkstraPath(ent, from, to); + if (BOTLIB_DijkstraPath(ent, from, to, path_randomization)) + //if (BOTLIB_DijkstraHeightPath(ent, from, to, path_randomization)) + //if (BOTLIB_HighestPath(ent, from, to, path_randomization)) + { + //Com_Printf("%s BOTLIB_DijkstraPath()\n", __func__); + + //clock_t clock_end = clock(); + //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; + //if (res == 0) normal++; + //Com_Printf("%s execution took %f seconds. Normal %d\n", __func__, res, normal); + + return true; + } + } + + return false; + } + //rekkie -- Dijkstra pathing -- e + + + //@@ TESTING ONLY!! antSearch always available + antSearch = 1; + // Check we're allowed to search - if so, do it + if (1)// (antSearch > 0) && (ent->antLastCallTime < level.framenum - ANT_FREQ) ) + { + //Com_Printf("%s AntFindPath()\n", __func__); + // Decrement the semaphore to limit calls to this function + //@@ If we ever get multithreading then we can increment later + antSearch--; + // make a note of when this bot last made a path call + ent->antLastCallTime = level.framenum; + // Set up the lists + AntInitSearch(ent); + // If we found a path + if (AntFindPath(ent, from, to)) + { + // pathList now contains the links in reverse order + return true; + } + } + + // We can use the quick node search method here to get our path and put it in the pathList + // the same way we do with the AntSearch mode. This will have the side effect of finding + // bad paths and removing them. + if (AntQuickPath(ent, from, to)) + { + //Com_Printf("%s AntQuickPath()\n", __func__); + return true; + } + // If not allowed to search and no path + AntInitSearch(ent); // Clear out the path storage + return false; +} + +//================================= +// QuickPath +//================================= +// +// Uses the old path array to get a quick answer and removes bad paths +// + +qboolean AntQuickPath(edict_t* ent, int from, int to) +{ + int newNode = from; + int oldNode = 0; + int loopProtection = 0; //rekkie -- DEV_1 + + // Clean out the arrays, etc. + AntInitSearch(ent); + nodeused[from] = true; + // Check we can get from->to and that the path is complete + while (newNode != INVALID) + { + oldNode = newNode; + // get next node + newNode = path_table[newNode][to]; + if (newNode == to) + { + // We're there - store it then build the path + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + break; + } + else if (newNode == INVALID) + { + // We have a bad path + break; + } + else if (!nodeused[newNode]) + { + // Not been here yet - store it! + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + } + else + break; // LOOP encountered + } + + // If successful, build the pathList + if (newNode == to) + { + SLLpush_front(&ent->pathList, to); + while (newNode != from) + { + //rekkie -- s + if (newNode >= numnodes || newNode < 0) + { + if (debug_mode) + gi.dprintf("%s newNode out of bounds!\n", __func__); + return false; + } + //rekkie -- e + + // Push the + SLLpush_front(&ent->pathList, nodefrom[newNode]); + newNode = nodefrom[newNode]; + } + return true; + } + // else wipe out the bad path! + else + { + newNode = oldNode; + while (newNode != from) + { + path_table[nodefrom[newNode]][to] = INVALID; + + //rekkie -- DEV_1 -- s + if (loopProtection < numnodes) + loopProtection++; + else + { + if (debug_mode) + debug_printf("%s ------------------- Bad while loop. Forcing exit.\n", __func__); + break; + } + //rekkie -- DEV_1 -- e + } + path_table[from][to] = INVALID; + } + return false; +} + +//rekkie -- ZigZag pathing -- s +// The bot will zig and zag from node to node, trying to avoid enemy fire +qboolean BOTLIB_ZigZagPath(edict_t* ent, int from, int to) +{ + //Com_Printf("%s [%d]\n", __func__, level.framenum); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + + while (!SLLempty(&openList)) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + + // Update node weights, but don't add them to the list + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Add it to the open list + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order + // so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + //rekkie -- DEV_1 -- s + // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID + // Check if INVALID to avoid accessing [-1] array + if (nodefrom[to] != INVALID) + //rekkie -- DEV_1 -- e + path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // Push it onto the pathlist + SLLpush_front(&ent->pathList, newNode); + // Set the path in the node array to match this shortest path + path_table[nodefrom[newNode]][to] = newNode; + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + if (0) + { + Com_Printf("%s [%d] s[%d] g[%d] node_list[", __func__, level.framenum, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + + //Com_Printf("%s [%d] found a path\n", __func__, level.framenum); + return true; + } + + //Com_Printf("%s failed to find a path\n", __func__); + return false; +} + +//rekkie -- Dijkstra height pathing -- s +#if 0 +// Function unfinished. Wanted bots to prefer taking higher paths. +qboolean BOTLIB_DijkstraHeightPath(edict_t* ent, int from, int to, qboolean path_randomization) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + SLLpush_back(&openList, from); // Store it + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + break; // We found a path + } + + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; // Get the weight of the node we're visiting + the cost to the new node + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + } + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + return true; + } + + //Com_Printf("%s failed to find a path\n", __func__); + return false; +} +#endif +//rekkie -- Dijkstra Height Pathing -- e + +#if 0 +qboolean BOTLIB_NodeCanSeeEnemyPath(edict_t* ent, int atNode) +{ + // Check if node path can be seen by enemies + if (nodes[atNode].num_vis_nodes) // && random() <= 0.2) // && ent->bot.enemies_num && random() <= 0.2) + { + for (int v = 0; v < nodes[atNode].num_vis_nodes; v++) // each vis node + { + /* + for (int e = 0; e < num_players; e++) // each enemy + { + if (OnSameTeam(ent, players[e])) continue; + + for (int nl = 0; nl < players[e]->bot.node_list_count; nl++) // each node within the enemy's path + { + if (players[e]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + */ + + /* + for (int e = 0; e < ent->bot.enemies_num; e++) // each enemy + { + for (int nl = 0; nl < players[ent->bot.enemies[e]]->bot.node_list_count; nl++) // each node within the enemy's path + { + if (players[ent->bot.enemies[e]]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + */ + + if (ent->enemy) + { + for (int nl = 0; nl < ent->enemy->bot.node_list_count; nl++) // each node within the enemy's path + { + if (ent->enemy->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + } + } + return false; +} +#endif + + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Pathing is working, however some things to add,fix,do... +// +// 2) Also need to test ALL failure points, and try to trigger failures and test if its handled correctly. +// +// 3) Cache all nodes in each area. Leave room for an extra node (explained below what we use the extra for) +// +// 4) Currently when BOTLIB_CanVisitNode() is called, it will search all nodes. I need this to search and path only the +// areas which are involved in the search. Currently the next_node/edge node is located outside the current area of the bot, +// and just inside the next area (the border). Therefore, I should include that extra node in the search. So if we're in Area 0, +// and the next node is in Area 1 (next to Area 0), then search will be all nodes in Area 0 + the single node from Area 1. +// Once the search + path is conducted, remove the node from the search. +// +// 5) Deal with situtations where the areas are connected, but the goal node is not accessible in the last area. Perhaps after checking +// if the areas connect, run a BOTLIB_CanVisitNode() search on the last area to see if it connects from the last edge node +// to the final goal node. ALSO, there might a scenario where the random selected last edge node does not connect, but one of the +// many other edge nodes might? I guess in such a case, a connection might path through the other edge nodes anyway. +// +// 6) When selecting a path from nav_area.dfs_paths[self->bot.path_taken][i], the paths are a series of contigous areas A>B>C>D,etc +// I need an algorithm (similar to the heightmap) to see if areas are being overly used, and/or taking the higher area is preferred +// over a lower area. I could also cache area combos and check agains them. Going back to the heightmap idea, if area C (middle TJ) +// is being overly used, just check if any of the paths contain 'C' and avoid using them? +// +// 7) When picking a random edge node, perhaps I could pick the closest edge node to the current node, thereby making it a shortest path +// by direct line. However, this isn't the shortest 'path', only a direct line. +// +// 8) Check if other bots are using the edge node, if so, pick another edge node if available. +// +// 9) Move some of the arrays over to dynamic allocated memory, build the array as a single block (single malloc call) +// +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + +// Init data used when selecting nodes +void BOTLIB_InitAreaNodes(void) +{ + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + DFS_area_nodes[a][n] = INVALID; + } + } + + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) + { + DFS_area_edges[a][e] = INVALID; + } + } +} + +void BOTLIB_InitAreaConnections() +{ + // For all areas, find all their connecting neighbors. + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + nav_area.adjacency_matrix[i][a] = INVALID; // Adjacency matrix + } + } + int targetNode = INVALID; + int targetArea = INVALID; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].area == i) + { + for (int l = 0; l < nodes[n].num_links; l++) + { + targetNode = nodes[n].links[l].targetNode; + targetArea = nodes[targetNode].area; + //if (targetArea != INVALID && targetArea != i) + if (targetArea != i) + { + nav_area.adjacency_matrix[i][targetArea] = targetArea; + } + } + } + } + } + + BOTLIB_UpdateAllAreaEdges(); // Update edge connections + + // Find total areas + nav_area.total_areas = 0; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.adjacency_matrix[i][a] != INVALID) + { + nav_area.total_areas++; + break; + } + } + } + + if (nav_area.total_areas == 0) + { + Com_Printf("%s WARNING: Map has no area nodes. Bot pathing will be less optimal and large maps not supported.\n", __func__); + return; + } + + // Print connections + /* + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.adjacency_matrix[i][a] != INVALID) + { + Com_Printf("Nav area [%d] connects to [%d]\n", i, a); + } + } + } + */ + + + // Cache all the nodes in each area + BOTLIB_MallocAreaNodes(); // Alloc memory for area nodes + // Check if memory was allocated + if (nav_area.area_nodes && nav_area.area_nodes[0]) + { + // Init areas with INVALID + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + nav_area.area_nodes[i][n] = INVALID; + } + } + + // Cache all the nodes in each area -- currently a max of (MAX_NAV_AREAS_NODES - 1) nodes per area is allowed + qboolean reached_max_area_nodes; + //qboolean notified = false; + for (int i = 0; i < numnodes; i++) + { + reached_max_area_nodes = true; + for (int n = 0; n < (MAX_NAV_AREAS_NODES - 1); n++) // Cache up to (MAX_NAV_AREAS_NODES - 1) + { + if (nav_area.area_nodes[nodes[i].area][n] == INVALID) + { + reached_max_area_nodes = false; + nav_area.area_nodes[nodes[i].area][n] = nodes[i].nodenum; + break; + } + } + if (reached_max_area_nodes)// && notified == false) + { + //notified = true; + Com_Printf("%s Nav area [%d] reached_max_area_nodes [%d]\n", __func__, nodes[i].area, MAX_NAV_AREAS_NODES); + break; + } + } + + BOTLIB_RandomizeAreaColors(); + + /* + // Print out area nodes + int total_nodes; + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + if (nav_area.area_nodes[i][0] == INVALID) + continue; + + total_nodes = 0; + + Com_Printf("%s Area [%d] nodes: ", __func__, i); + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (nav_area.area_nodes[i][n] == INVALID) + break; + + Com_Printf(" %d ", nav_area.area_nodes[i][n]); + total_nodes++; + } + Com_Printf("\nTotal nodes: %d\n", total_nodes); + } + */ + } +} + +// Create area nodes mallocs +void BOTLIB_MallocAreaNodes() +{ + // Dynamically cache all the nodes in each area -- currently a max of 1024 nodes per area is allowed + //area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area + nav_area.area_nodes = (int**)malloc(sizeof(*nav_area.area_nodes) * MAX_NAV_AREAS); + if (!nav_area.area_nodes) { + Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); + nav_area.area_nodes = NULL; + } + else + { + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + nav_area.area_nodes[i] = (int*)malloc(sizeof(*(nav_area.area_nodes[i])) * MAX_NAV_AREAS_NODES); + if (!nav_area.area_nodes[i]) + { + Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); + //nav_area.area_nodes = NULL; + } + } + // Now we can use nav_area.area_nodes[i][j] to access any element :) + } +} +// Free area nodes mallocs +void BOTLIB_FreeAreaNodes() +{ + if (nav_area.area_nodes) + { + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + if (nav_area.area_nodes[i]) + { + free(nav_area.area_nodes[i]); // Freeing memory allocated for each row separately + nav_area.area_nodes[i] = NULL; + } + } + free(nav_area.area_nodes); // And then freeing the pointer to the pointers + nav_area.area_nodes = NULL; + } +} + +// This initiates bot travel from current node to goal node +// 1) The bot will use area nodes if map has areas (Efficient), +// 2) Alternatively it will fallback to doing node-to-node pathing (Less efficient) +// 3) path_randomization only applies to the fallback method +qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization) +{ + if (goal_node == INVALID) + return false; + + //Com_Printf("%s\n", __func__); + + self->bot.node_list_count = 0; + + // New pathing - if area nodes are supported + if (nav_area.total_areas > 0 && goal_node) + { + Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); + if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path + { + while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path + { + } + + // Add the goal to the end of the node list + terminate + if (self->bot.node_list_count) + { + self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + } + + return true; // Success + } + else + { + Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); + return false; // Failure + } + } + else // Fallback to old pathing + { + self->bot.goal_node = INVALID; + if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) + { + + // Add the goal to the end of the node list + terminate + if (self->bot.node_list_count) + { + self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + } + + return true; // Success + } + } + + Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); + return false; // Failure to find path +} + +// Checks if goal is reachable from current node +qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node) +{ + if (goal_node == INVALID) + { + Com_Printf("%s INVALID PATH: goal_node is invalid\n", __func__); + return false; + } + + //Com_Printf("\n\n========================================================\n", __func__); + //Com_Printf("%s Heading for goal node[%d] area[%d]\n", __func__, goal_node, self->bot.goal_area); + //Com_Printf("========================================================\n", __func__); + + //BOTLIB_InitAreaConnections(); + + // Init area + self->bot.start_area = INVALID; + //self->bot.current_area = INVALID; + //self->bot.goal_area = INVALID; + + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + + if (self->bot.current_node == INVALID) + { + //Com_Printf("%s INVALID PATH: Cannot find current_node\n", __func__); + self->bot.next_area_node = INVALID; + return false; + } + else + { + self->bot.start_area = nodes[self->bot.current_node].area; // Area we first started from + self->bot.current_area = nodes[self->bot.current_node].area; // Current area we're in + self->bot.goal_area = nodes[goal_node].area; // Goal area to reach + self->bot.next_area_counter = 0; // Keep track of which area is next, starting from the first area (current area) + self->bot.next_area_node = self->bot.current_node; + + + + + // If node is inside of the same (or final) area try to see if we can navigation there + // Otherwise continue with trying to route to the goal_node's area + if (self->bot.current_area == self->bot.goal_area) + { + //if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, false)) + { + self->bot.next_area_nodes[0] = goal_node; // Last node + self->bot.next_area_nodes_counter = 0; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[1] = INVALID; // Terminate + + self->bot.goal_node = goal_node; // Set the goal node to reach + self->bot.path_taken = 0; // Get rand path + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + Com_Printf("%s Goal node[%d] is in the same area[%d] as your area [%d]\n", __func__, goal_node, self->bot.goal_area, self->bot.current_area); + return true; // Success + } + } + + } + + + + + /* + // Pick a random area to visit + // DO NOT DELETE, it will be useful later on + //while (nav_area.goal_area == nav_area.start_area) // Pick a goal that isn't the same area as bot is in + // nav_area.goal_area = rand() % nav_area.total_areas; // Get rand goal + // Get goal node within goal area + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == nav_area.goal_area) + { + nav_area.goal_node = nodes[i].nodenum; // Get first node in area + //nav_area.goal_node = DFS_area_nodes[nav_area.goal_area][0]; // Get first node in area + Com_Printf("%s Goal node [%d]\n", __func__, nav_area.goal_node); + break; + } + } + */ + + + // Calculate all paths + for (int i = 0; i < MAX_NAV_AREAS_PATHS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + nav_area.dfs_paths[i][a] = INVALID; + } + } + nav_area.dfs_path_count = 0; + memset(nav_area.dfs_visited, false, sizeof(nav_area.dfs_visited)); + + nav_area.dfs_path[0] = 0; + nav_area.dfs_len = 0; + //nav_area.dfs_visited[0] = false; + BOTLIB_DepthFirstSearch(self, self->bot.current_area, self->bot.goal_area); // Calculate all possible paths + + //Com_Printf("%s DFS_path_count >= %d\n", __func__, nav_area.dfs_path_count); + + //Com_Printf("%s total_areas[%d] start[%d] curr[%d] goal[%d] path_taken[%d]\n", __func__, nav_area.total_areas, nav_area.start_area, nav_area.current_area, nav_area.goal_area, nav_area.path_taken); + + // Print all paths + /* + if (nav_area.dfs_path_count) + { + for (int i = 0; i < nav_area.dfs_path_count; i++) + { + Com_Printf("%s [%d]Path: ", __func__, i); + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.dfs_paths[i][a] == INVALID) + break; + + Com_Printf("%d ", nav_area.dfs_paths[i][a]); + } + Com_Printf("\n"); + } + } + */ + + // Instead of picking a random path, pick the less used path... + + // Pick a random path + if (nav_area.dfs_path_count) // Success + { + /* + self->bot.goal_node = goal_node; // Set the goal node to reach + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + // Precalculate all edge-to-edge nodes + Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); + int prev_area = self->bot.current_area; + int next_area; + int next_node; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + next_area = nav_area.dfs_paths[self->bot.path_taken][i]; + + if (next_area == INVALID) // End of path + { + Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); + self->bot.next_area_nodes[i] = goal_node; // Last node + self->bot.next_area_nodes_counter = i; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[i + 1] = INVALID; // Terminate + break; + } + else + { + next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); + self->bot.next_area_nodes[i] = next_node; + Com_Printf("[%d][%d] ", next_area, next_node); + } + + prev_area = next_area; + } + Com_Printf("\n"); + */ + + //self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node + //next_area_nodes + + BOTLIB_GetAreaPath(self, goal_node); + + return true; // Success + } + else // Failure + { + self->bot.path_taken = INVALID; + //Com_Printf("%s NO VALID PATH from area %d to %d\n", __func__, self->bot.current_area, self->bot.goal_area); + return false; // Faulure + } + + return true; // Success +} + +// +qboolean BOTLIB_MakeAreaPath(edict_t* self, int goal_node, qboolean use_heatmap) +{ + self->bot.next_area_nodes_counter = 0; + int prev_area = self->bot.current_area; + int next_area; + int next_node; + qboolean debug = false; + + if (debug) Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + next_area = nav_area.dfs_paths[self->bot.path_taken][i]; + + self->bot.area_heatmap[next_area] += 1; // Increase heatmap + if (use_heatmap) // If we're avoiding an area + { + if (self->bot.area_heatmap[next_area] >= 3) + { + //Com_Printf("\n"); + //self->bot.area_heatmap[next_area] = 0; // Reset heatmap + //return false; // Failure + } + } + + if (next_area == INVALID) // End of path + { + if (debug) Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); + self->bot.next_area_nodes[i] = goal_node; // Last node + self->bot.next_area_nodes_counter = i + 1; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[i + 1] = INVALID; // Terminate + break; + } + else + { + next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); + if (next_node == INVALID) + { + if (debug) Com_Printf("[%d][%d] ", next_area, next_node); + if (debug) Com_Printf(" -- INVALID PATH\n"); + return false; // Failure to find connected edge connection + } + self->bot.next_area_nodes[i] = next_node; + if (debug) Com_Printf("[%d][%d] ", next_area, next_node); + } + + prev_area = next_area; + } + if (debug) Com_Printf("\n"); + + return true; // Success +} + + +// Precalculate all edge-to-edge nodes +void BOTLIB_GetAreaPath(edict_t* self, int goal_node) +{ + self->bot.goal_node = goal_node; // Set the goal node to reach + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + if (BOTLIB_MakeAreaPath(self, goal_node, false) == true) // Success + break; + } + + /* + qboolean success = false; + for (int i = 0; i < nav_area.dfs_path_count; i++) + { + self->bot.path_taken = i; // Cycle through paths + success = BOTLIB_MakeAreaPath(self, goal_node, true); + if (success) + break; + } + if (success == false) + { + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + BOTLIB_MakeAreaPath(self, goal_node, false); + } + */ +} + +qboolean BOTLIB_GetNextAreaNode(edict_t *self) +{ + if (self->bot.next_area_nodes_counter == self->bot.next_area_counter) + { + Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); + return false; + } + + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node + if (self->bot.current_node == INVALID || self->bot.goal_node == INVALID) + { + Com_Printf("%s INVALID PATH\n", __func__); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return false; + } + int prev_node = self->bot.current_node; + if (self->bot.next_area_counter) + { + prev_node = self->bot.node_list[self->bot.node_list_count - 1]; + //prev_node = self->bot.next_area_nodes[self->bot.next_area_counter - 1]; + } + int next_node = self->bot.next_area_nodes[self->bot.next_area_counter]; + if (next_node != INVALID) + { + //if (BOTLIB_CanVisitNode(self, nodes[next_node].nodenum, false, INVALID, self->bot.next_area_counter)) + //if (BOTLIB_DijkstraAreaPath(self, prev_node, next_node, false, nodes[prev_node].area, self->bot.next_area_counter)) + if (BOTLIB_DijkstraPath(self, prev_node, next_node, false)) + { + self->bot.next_area_node = next_node; + self->bot.next_area_counter++; + + //Com_Printf("%s [%d] prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] curr[%d] goal[%d]\n", __func__, self->bot.next_area_counter, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.current_node, self->bot.goal_node); + + //if (self->bot.goal_node == nodes[next_node].nodenum) + // Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); + + if (next_node == self->bot.goal_node) // Reached goal node + { + self->bot.current_node = self->bot.node_list[0]; + self->bot.next_node = self->bot.node_list[1]; + self->node_timeout = 0; + self->bot.state = BOT_MOVE_STATE_MOVE; + return false; + } + + return true; + } + else + { + //Com_Printf("%s Could not visit node -- failed to reach prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] goal[%d]\n", __func__, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.goal_node); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return false; + } + } + + return false; + + /* + int last_node = self->bot.next_area_nodes[self->bot.next_area_nodes_counter]; + float dist = VectorDistance(nodes[self->bot.current_node].origin, nodes[last_node].origin); + if ( dist < 128 ) + { + //Com_Printf("%s Success, reached goal node[%d] from node[%d] dist[%f]\n", __func__, self->bot.goal_node, self->bot.current_node, dist); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return true; + } + */ + + //Com_Printf("%s Failed to reach next node [%d] curr_node[%d] goal[%d]\n", __func__, next_node, self->bot.current_node, self->bot.goal_node); + + +#if 0 + + + + if (self->bot.path_taken == INVALID) + { + Com_Printf("%s No valid paths found\n", __func__); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return; + } + + /* + float dist = 999999; + if (nav_area.next_area_node != INVALID) + { + dist = VectorDistance(nodes[nav_area.next_area_node].origin, nodes[self->bot.current_node].origin); + if (dist > 256.0f) + return; + } + */ + + // Update current area + self->bot.current_area = nodes[self->bot.current_node].area; + + // Update next area + self->bot.next_area = nav_area.dfs_paths[self->bot.path_taken][self->bot.next_area_counter]; + self->bot.next_area_counter++; + + // Check if final area reached + if (self->bot.next_area == INVALID) + { + if (self->bot.goal_node != INVALID && BOTLIB_CanVisitNode(self, nodes[self->bot.goal_node].nodenum, false, nodes[self->bot.current_node].area)) + { + Com_Printf("%s Goal area reached. Heading for goal node [%d] within the final area [%d]\n", __func__, self->bot.goal_node, self->bot.current_area); + return; + } + else + { + Com_Printf("%s Failed to reach final goal node[%d]: g_area[%d], c_area[%d], n_area[%d]\n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_area, self->bot.next_area); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return; + } + } + + // Com_Printf("%s c_area[%d] n_area[%d] g_area[%d]\n", __func__, self->bot.current_area, self->bot.next_area, self->bot.goal_area); + + /* + Com_Printf("%s Path taken: ", __func__); + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.dfs_paths[self->bot.path_taken][a] == INVALID) + break; + + Com_Printf("%d ", nav_area.dfs_paths[self->bot.path_taken][a]); + } + Com_Printf("\n"); + */ + + + //BOTLIB_UpdateAllAreaEdges(); // Update edge connections + self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node + //Com_Printf("%s c_node[%d] n_node[%d] c_area[%d] n_area[%d] g_area[%d] path_taken[%d]\n", __func__, self->bot.current_node, self->bot.next_area_node, self->bot.current_area, self->bot.next_area, self->bot.goal_area, self->bot.path_taken); + //Com_Printf("%s goal_area[%d] current_area[%d] --> next_area[%d]\n", __func__, self->bot.goal_area, self->bot.current_area, self->bot.next_area); + + if (self->bot.next_area_node != INVALID) + { + if (BOTLIB_CanVisitNode(self, nodes[self->bot.next_area_node].nodenum, false, nodes[self->bot.current_node].area)) + { + Com_Printf("%s heading to next node[%d] area[%d] goal[%d]\n", __func__, nodes[self->bot.next_area_node].nodenum, self->bot.next_area, self->bot.goal_node); + return; + } + } + + Com_Printf("%s Failed to reach NextAreaNode -- goal N[%d] A[%d] -- curr N[%d] A[%d] -- next N[%d] A[%d] \n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_node, self->bot.current_area, self->bot.next_area_node, self->bot.next_area); +#endif +} + +// Stores all the nodes within an area that connect to an external area (edge nodes) +// 1) Wipe old edge data +// 2) Find and store edge nodes +void BOTLIB_UpdateAllAreaEdges(void) +{ + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) + { + DFS_area_edges[a][e] = INVALID; + } + } + + int targetNode; + for (int i = 0; i < numnodes; i++) + { + for (int l = 0; l < nodes[i].num_links; l++) + { + targetNode = nodes[i].links[l].targetNode; + if (nodes[i].area != nodes[targetNode].area) // If node links to a node outside of its own area + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges + { + if (DFS_area_edges[nodes[i].area][e] == INVALID) // Find free spot to store a new edge + { + //Com_Printf("%s area[%d to %d] node[%d to %d]\n", __func__, nodes[i].area, nodes[targetNode].area, i, targetNode); + DFS_area_edges[nodes[i].area][e] = targetNode; // Store the edge node + break; + } + } + } + } + } +} + +// Get a random edge node from two connected areas +// Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) +int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2) +{ + int a2n; // Area 2 node + int ec = 0; // Edge counter + int edges[MAX_NAV_AREAS_EDGES] = { INVALID }; // Store the edges that connect from A1 to A2 + + // Find and store all connected nodes from A1 to A2 + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges + { + if (DFS_area_edges[area_1][e] == INVALID) // End of edges stored + break; + + a2n = DFS_area_edges[area_1][e]; + if (nodes[a2n].area == area_2) // Find all areas that connect from A1 to A2 + { + //Com_Printf("%s area[%d to %d] node[%d] counter[%d]\n", __func__, area_1, area_2, a2n, ec); + edges[ec++] = a2n; // Store the area_2 node + } + } + + if (ec) + return edges[rand() % ec]; // Return a random edge node + else + return INVALID; // No connected edge nodes found +} + +// Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm +void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination) +{ + if (current == destination) + { + if (nav_area.dfs_path_count >= MAX_NAV_AREAS_PATHS) + { + //Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_path_count >= %d\n", __func__, MAX_NAV_AREAS_PATHS); + return; + } + if (nav_area.dfs_len >= MAX_NAV_AREAS) + { + Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_len >= %d\n", __func__, MAX_NAV_AREAS); + return; + } + + if (nav_area.dfs_len) // If there's an actual path + { + // Check if path passes through itself (loops back and touches the starting area again) + // Don't cross the streams! It would be very bad! Egon. + for (int i = 0; i < nav_area.dfs_len; i++) + { + if (self->bot.start_area == nav_area.dfs_path[i]) // If path crosses start_area again + { + return; // Reject path + } + + for (int j = 0; j < nav_area.dfs_len; j++) + { + if (i == j) continue; + + if (nav_area.dfs_path[j] == nav_area.dfs_path[i]) // If path crosses same area twice + { + return; // Reject path + } + } + } + + + for (int i = 0; i < nav_area.dfs_len; i++) + { + nav_area.dfs_paths[nav_area.dfs_path_count][i] = nav_area.dfs_path[i]; // Copy from tmp path to full path array + } + nav_area.dfs_path_count++; + } + + return; + } + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + if (nav_area.adjacency_matrix[current][i] != INVALID && !nav_area.dfs_visited[i]) + { + nav_area.dfs_path[nav_area.dfs_len++] = i; // Copy to tmp path + nav_area.dfs_visited[i] = true; // Visited + BOTLIB_DepthFirstSearch(self, i, destination); // Recursive search (cycles) + nav_area.dfs_len--; // Backtrack + nav_area.dfs_visited[i] = false; // Unvisit + } + } +} + +// Check if area already exists +qboolean BOTLIB_AreaExists(int area_num) +{ + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == area_num) + { + return true; // Area exists + } + } + return false; // Area does not exist +} +// Attempts to automatically add areas to map +void BOTLIB_AutoArea(edict_t* self) +{ + int curr_area_num = 0; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == 0) + { + self->bot.walknode.selection_node_count = 0; + BOTLIB_GroupConnectedNodeArea(self, i); // Try grouping nodes from this node + + if (self->bot.walknode.selection_node_count) + { + //Check if area exists + for (; curr_area_num < MAX_NAV_AREAS; curr_area_num++) + { + if (BOTLIB_AreaExists(curr_area_num) == false) + break; + } + if (curr_area_num + 1 >= MAX_NAV_AREAS) + { + Com_Printf("%s WARNING: Cannot add more areas, maximum allowed %d\n", __func__, MAX_NAV_AREAS); + break; + } + // Found a free area + for (int s = 0; s < self->bot.walknode.selection_node_count; s++) + { + nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; + nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; + } + } + } + } + + if (curr_area_num) + { + BOTLIB_FreeAreaNodes(); + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } +} + +// Randomize area colors +void BOTLIB_RandomizeAreaColors() +{ + int color; + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + // Try to randomize colors while keeping at least one of the R,G,B values above 128+ + int rng = rand() % 3; + if (rng == 0) + color = MakeColor((rand() % 255), (rand() % 255), (rand() % 128) + 128, 255); // Randomize color + else if (rng == 1) + color = MakeColor((rand() % 255), (rand() % 128) + 128, (rand() % 255), 255); // Randomize color + else + color = MakeColor((rand() % 128) + 128, (rand() % 255), (rand() % 255), 255); // Randomize color + + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (nav_area.area_nodes[i][n] == INVALID) + break; + + nodes[nav_area.area_nodes[i][n]].area_color = color; + } + } +} + +// ==================================================================================== +// ==================================================================================== +// Create local area nodes memory alloc +int total_local_area_nodes; +qboolean* local_area_nodes; +qboolean BOTLIB_MallocLocalAreaNodes() +{ + total_local_area_nodes = 0; + local_area_nodes = NULL; + + //nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + local_area_nodes = (qboolean*)malloc(sizeof(local_area_nodes) * (numnodes + 1)); + if (local_area_nodes == NULL) + { + Com_Printf("%s Memory allocation failed for local_area_nodes\n", __func__); + local_area_nodes = NULL; + return false; + } + + for (int i = 0; i < numnodes; i++) + { + local_area_nodes[i] = false; // Init to false + } + + return true; // Success +} +// Free local area nodes memory alloc +void BOTLIB_FreeLocalAreaNodes() +{ + if (local_area_nodes) + { + free(local_area_nodes); + local_area_nodes = NULL; // Nullify dangling pointer + } +} +// A recurrsive function that finds all nodes that are connected and pathed to the initial `start_node` and are of the same area num +void BOTLIB_GroupConnectedNodeArea_r(edict_t* self, int next_node) //, vec3_t normal) +{ + for (int i = 0; i < numnodes; i++) + { + if (local_area_nodes[i] == true) + continue; + + if (nodes[i].area == nodes[next_node].area) + { + for (int l = 0; l < nodes[i].num_links; l++) + { + if (nodes[i].links[l].targetNode == next_node) + { + if (self->bot.walknode.selection_node_count >= MAX_NAV_AREAS_NODES) + break; + + // Check if node already included in selection + qboolean added = false; + for (int s = 0; s < self->bot.walknode.selection_node_count; s++) + { + if (self->bot.walknode.selection_nodes[s] == i) + { + added = true; + break; + } + } + if (added) + continue; // Skip adding because we already have it + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + // Need to test if the two nodes are on the same plane/normal/height/distance + + if (VectorDistance(nodes[i].origin, nodes[self->bot.walknode.selection_nodes[0]].origin) > 1024) + continue; + + // Check hight diff of connected node - ignore nodes with large differences + //Com_Printf("%s FABS[%f]\n", __func__, fabs(nodes[i].origin[2] - nodes[next_node].origin[2])); + if (fabs(nodes[i].origin[2] - nodes[next_node].origin[2]) > 32) + continue; + + // Only nodes on the same normal - with a small epsilon + if (BOTLIB_VectorCompare(nodes[i].normal, nodes[next_node].normal, 0.1) == false) + continue; + + + //Com_Printf("%s [%f %f %f] vs [%f %f %f]\n", __func__, nodes[next_node].normal[0], nodes[next_node].normal[1], nodes[next_node].normal[2], nodes[i].normal[0], nodes[i].normal[1], nodes[i].normal[2]); + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + local_area_nodes[next_node] = true; // Flag node as true + //total_local_area_nodes++; + + self->bot.walknode.selection_nodes[self->bot.walknode.selection_node_count] = i; + self->bot.walknode.selection_node_count++; + + BOTLIB_GroupConnectedNodeArea_r(self, i); // Scan the connected node + } + } + } + } +} + +void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node) +{ + if (BOTLIB_MallocLocalAreaNodes() == false) // Alloc memory + return; // Failure + + local_area_nodes[start_node] = true; // Initiate starting node to true + //total_local_area_nodes++; + + /* + trace_t tr = gi.trace(tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] + 32), NULL, NULL, tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] - 256), NULL, MASK_SHOT); + if (tr.fraction < 1.0) + { + VectorCopy(tr.plane.normal, nodes[start_node].normal); + } + + Com_Printf("%s [%f %f %f]\n", __func__, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); + */ + + //BOTLIB_SetAllNodeNormals(); // Set all the normals + + BOTLIB_GroupConnectedNodeArea_r(self, start_node); // Recurrsively check all nodes + + //total_local_area_nodes + //Com_Printf("%s total_local_area_nodes[%d]\n", __func__, total_local_area_nodes); + + //for (int i = 0; i < numnodes; i++) + { + //if (local_area_nodes[i] == true) + { + } + } + + BOTLIB_FreeLocalAreaNodes(); // Free memory +} +// ==================================================================================== +// ==================================================================================== + +//rekkie -- Dijkstra Area Pathing -- s +// Currently if path_randomization is enabled, the bot may not be able to reach its target, +// therefore it's only really used when heading to a random node. +// Area: specify the area we're working with; only searching nodes within this area +// build_new_path: If we're building a new path or adding to an existing path +qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID || area == INVALID) + return false; + + // Check for null pointers + if (nav_area.area_nodes == NULL || nav_area.area_nodes[0] == NULL) + return false; + + //if (build_new_path) + { + //ent->bot.node_list_count = 0; // Reset the list of nodes to zero + //ent->bot.node_list_current = 0; // Set the starting point to zero + } + + AntInitSearch(ent); // Clear out the path storage + + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + //int searched_nodes = 0; + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + //searched_nodes++; + //Com_Printf("%s NODE[%d] AREAS: ", __func__, atNode); +//#if 0 + // Find the node with the lowest weight + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (newNode == INVALID) + continue; + + // Only search in current area + if (newNode != to && nodes[newNode].area != area) + continue; + + //Com_Printf(" %d ", nodes[newNode].area); + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + /* + if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + */ + } + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] == NAV_INFINITE) + { + lowest_node = INVALID; + break; + } + + if (nodeweight[newNode] < lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + } + } + } + //Com_Printf("\n"); + + // Use the shortest path + if (lowest_node != INVALID) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[lowest_node]) + { + if (lowest_node != from && lowest_node != to) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + } + + nodeweight[lowest_node] = weight; // Update the weight + nodefrom[lowest_node] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, lowest_node); // Store it + //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + } + + if (lowest_node == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + break; // We found a path + } + } + else +//#endif + { + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (newNode == INVALID) + continue; + + // Only search in current area + if (newNode != to && nodes[newNode].area != area) + continue; + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + + } + } + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + //Com_Printf("%s searched_nodes[%d]\n", __func__, searched_nodes); + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array + ////if (nodefrom[newNode] != INVALID) + { + ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + } + + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high + if (nodes[newNode].num_links > 3) + { + nodes[newNode].weight += 0.1; + if (nodes[newNode].weight > 0.5) // Limit max weight + nodes[newNode].weight = 0; // Reset back to normal weight + if (nodes[newNode].weight < 0) // Sanity check + nodes[newNode].weight = 0; // Reset back to normal weight + } + + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + if (SLLempty(&ent->pathList)) + return false; // Failure + + return true; // Success + } + + // If we get to this point, it means our path failed + // If path_randomization was true, then try again without it + if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it + { + BOTLIB_DijkstraAreaPath(ent, from, to, false, area, build_new_path); + } + // Else: just fail it completely + { + //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); + return false; // Failure + } +} +//rekkie -- Dijkstra Area Pathing -- e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//rekkie -- Dijkstra pathing -- s +// Currently if path_randomization is enabled and the bot fails to path to its target, +// BOTLIB_DijkstraPath() is run again with path_randomization turned off +qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + /* + // Update node weights, but don't add them to the list + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + //SLLpush_back(&openList, newNode); // Add it to the open list + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + + // Find the node link with the shortest path + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] <= lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + } + } + } + + // Add the shortest path to the open list + if (lowest_node != INVALID) + { + SLLpush_back(&openList, lowest_node); // Add it to the open list + //Com_Printf("%s [%d to %d] lowest_node[%d]\n", __func__, from, to, lowest_node); + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + */ + + + + //* + // Find the node with the lowest weight + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + /* + if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + */ + } + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] == NAV_INFINITE) + { + lowest_node = INVALID; + break; + } + + if (nodeweight[newNode] < lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + } + } + } + // Use the shortest path + if (lowest_node != INVALID) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[lowest_node]) + { + if (lowest_node != from && lowest_node != to) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + } + + nodeweight[lowest_node] = weight; // Update the weight + nodefrom[lowest_node] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, lowest_node); // Store it + //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + } + + if (lowest_node == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + break; // We found a path + } + } + else + { + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + + } + } + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + //*/ + + + + /* + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode != INVALID) + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + break; // We found a path + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + */ + } + + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array + ////if (nodefrom[newNode] != INVALID) + { + ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + } + + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high + if (nodes[newNode].num_links > 3) + { + nodes[newNode].weight += 0.1; + if (nodes[newNode].weight > 0.5) // Limit max weight + nodes[newNode].weight = 0; // Reset back to normal weight + if (nodes[newNode].weight < 0) // Sanity check + nodes[newNode].weight = 0; // Reset back to normal weight + } + + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + //ent->bot.node_list_count = 0; + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + if (SLLempty(&ent->pathList)) + return false; // Failure + + return true; // Success + } + + // If we get to this point, it means our path failed + // If path_randomization was true, then try again without it + if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it + { + BOTLIB_DijkstraPath(ent, from, to, false); + } + // Else: just fail it completely + { + //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); + return false; // Failure + } +} +//rekkie -- Dijkstra pathing -- e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//======================= +// FindPath +//======================= +// +// Uses OPEN and CLOSED lists to conduct a search +// Many refinements planned +// +qboolean AntFindPath(edict_t* ent, int from, int to) +{ + int counter = 0; // Link array counter + int link_counter = 0; // Link counter + int newNode = INVALID; // Stores the node being tested + int atNode; // Structures for search + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // MUST do this!! + + // Safety first again - we don't want crashes! + if (from == INVALID || to == INVALID) + return false; + + // Put startnode on the OPEN list + atNode = from; + nodefrom[atNode] = INVALID; + SLLpush_back(&openList, from); + nodeused[from] = true; + + //Com_Printf("%s NEW PATH [%d to %d]\n", __func__, from, to); + + + // While there are nodes on the OPEN list AND we are not at destNode + while (!SLLempty(&openList) && newNode != to) + { + // Where we are + atNode = SLLfront(&openList); + + //Com_Printf("%s atNode [%d]\n", __func__, atNode); + + // Safety check + if (atNode <= INVALID) + return false; + + + // Get random start point (instead of always starting at 0) + int start_point = 0; + //if (nodes[atNode].num_links) // If we have links + // start_point = rand() % (nodes[atNode].num_links); // start at random link + //if (start_point + 1 > MAXLINKS) + // start_point = 0; + + //int start_point = 0; + counter = start_point; + + // Go to next link, check for unused node + //while (++counter != start_point) + while (counter != nodes[atNode].num_links) + { + /* + if (counter + 1 > MAXLINKS) //MAXLINKS + { + counter = 0; + if (counter == start_point) + break; + } + */ + + if (nodes[atNode].links[counter].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Using an array for FAST access to the path rather than a CLOSED list + newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + + //Com_Printf("%s counter[%d] newNode [%d]\n", __func__, counter, newNode); + counter++; + + if (newNode == INVALID) + continue; + else + { + /* + // Check for failure rates on links + // If the fail rate is > 0, then we have a chance of skipping the node + // The higher the targetFailures, the higher the chance of skipping + //Com_Printf("%s %s: n[%d] l[%d] f[%d]\n", __func__, ent->client->pers.netname, newNode, counter, nodes[atNode].links[counter].targetFailures); + if (nodes[atNode].links[counter].targetFailures > 1) + { + float percent = (1 / nodes[atNode].links[counter].targetFailures); + if (random() < percent) + { + //Com_Printf("%s %s: Skipping node %d due to %d failures\n", __func__, ent->client->pers.netname, newNode, nodes[atNode].links[counter].targetFailures); + continue; + } + } + */ + + if (0) + { + // [1] Less expensive + // Randomize paths + //if (random() < 0.1) // how often we skip a targetNode + // continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + //if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + } + + //if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) + // continue; + + // If newNode NOT on open or closed list + if (!nodeused[newNode]) + { + nodeused[newNode] = true; // Mark node as used + nodefrom[newNode] = atNode; // Add to OPEN list + SLLpush_back(&openList, newNode); // Store it + } + + if (newNode == to) // If node being linked is destNode then quit + break; + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + } + + + + /* + // While there are nodes on the OPEN list AND we are not at destNode + while (!SLLempty(&openList) && newNode != to) + { + counter = 0; + + // Where we are + atNode = SLLfront(&openList); + + ////Com_Printf("%s atNode [%d]\n", __func__, atNode); + + // Safety check + if (atNode <= INVALID) + return false; + + // Using an array for FAST access to the path rather than a CLOSED list + newNode = nodes[atNode].links[counter].targetNode; + + // Process this node putting linked nodes on the OPEN list + while (newNode != INVALID) + { + ////Com_Printf("%s newNode [%d]\n", __func__, newNode); + + // If newNode NOT on open or closed list + if (!nodeused[newNode]) + { + nodeused[newNode] = true; // Mark node as used + nodefrom[newNode] = atNode; // Add to OPEN list + SLLpush_back(&openList, newNode); // Store it + } + + if (newNode == to) // If node being linked is destNode then quit + break; + + // Go to next link, check for unused node + while (++counter < MAXLINKS) + { + newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + + if(newNode == INVALID) + break; + + // [1] Less expensive + // Randomize paths + if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) + break; + if (random() < 0.25) // how often we skip a targetNode + continue; + else + break; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + //if (random() < 0.88) + //{ + // // Check if node is free, otherwise check next link + // if (Botlib_Nav_NodeInUse(ent, newNode) == false) + // break; + //} + //else + // break; + + } + if (counter >= MAXLINKS || newNode == INVALID) + break; + + //if (++counter >= MAXLINKS) // Increase counter and check we're in bounds + // break; + //newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + } + */ + + // Free up the memory we allocated + SLLdelete(&openList); + + //Com_Printf("%s PATH newNode [%d] to [%d]\n", __func__, newNode, to); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order + // so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + //rekkie -- DEV_1 -- s + // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID + // Check if INVALID to avoid accessing [-1] array + if (nodefrom[to] != INVALID) + //rekkie -- DEV_1 -- e + path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // Push it onto the pathlist + SLLpush_front(&ent->pathList, newNode); + // Set the path in the node array to match this shortest path + path_table[nodefrom[newNode]][to] = newNode; + + //Com_Printf("%s path_table[ nodefrom[%d] ][ %d ] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + return true; + } + // else + return false; +} + +//============================= +// LinkExists +//============================= +// +// Check we haven't wandered off path! +// +qboolean AntLinkExists(int from, int to) +{ + int counter = 0; + int testnode; + + if (from == INVALID || to == INVALID) + return false; + + // Check if the link exists + while (counter < MAXLINKS) + { + testnode = nodes[from].links[counter].targetNode; // Get next linked node + if (testnode == to) + { + return true; // A path exists from -> to + } + else if (testnode == INVALID) + { + return false; // No more links and no path found + } + counter++; + } + + return false; // Didn't find it! +} + + + + +// Draw the bot path +qboolean BOTLIB_DrawPath(edict_t* self) +{ + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + int curr_node, next_node; + float line_width; + uint32_t color; + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t red = MakeColor(255, 0, 0, 255); // Red + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + void (*DrawArrow)(int number, vec3_t start, vec3_t end, const uint32_t color, float line_width, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + DrawArrow = players[0]->client->pers.draw->DrawArrow; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + players[0]->client->pers.draw->arrows_inuse = true; // Flag as being used + int jumppad_box_num = 0; // Counter to display more than one jumpbox at once + int start = self->bot.node_list_current - 1; + if (start < 0) + start = 0; + for (int i = start; (i + 1) < self->bot.node_list_count; i++) + { + curr_node = self->bot.node_list[i]; // Next node in the list + next_node = self->bot.node_list[i + 1]; // Next node in the list + + if (curr_node == INVALID || next_node == INVALID) + break; + + if (players[0]->client->pers.draw->draw_arrow_num + 1 > MAX_DRAW_ARROWS) + players[0]->client->pers.draw->draw_arrow_num = 0; + players[0]->client->pers.draw->draw_arrow_num; + + // Draw current link as thicker line + if (self->bot.current_node == curr_node) + { + line_width = 10.0; + color = MakeColor(255, 255, 0, 255); // Yellow + } + else // Draw all other links as thinner lines + { + line_width = 1.0; + color = MakeColor(0, 255, 0, 255); // Green + } + + // Show jumppad nodes as a blue box + for (int i = 0; i < nodes[curr_node].num_links; i++) + { + if (nodes[curr_node].links[i].targetNode == next_node) + { + if (nodes[curr_node].links[i].targetNodeType == NODE_JUMPPAD) + { + DrawBox(jumppad_box_num, nodes[curr_node].origin, blue, nodes[curr_node].mins, nodes[curr_node].maxs, 100, false); + jumppad_box_num++; + } + break; + } + } + + DrawArrow(players[0]->client->pers.draw->draw_arrow_num, nodes[curr_node].origin, nodes[next_node].origin, color, line_width, 100, false); + players[0]->client->pers.draw->draw_arrow_num++; + } + + // Draw goal node + DrawBox(jumppad_box_num, nodes[self->bot.goal_node].origin, red, nodes[self->bot.goal_node].mins, nodes[self->bot.goal_node].maxs, 100, false); + +#endif + //rekkie -- debug drawing -- e + + return true; +} + + + + +//============================== +// SLLpush_front +//============================== +// Add to the front of the list +// +void SLLpush_front(botlib_sll_t* list, int node) +{ + botlib_sll_nodes_t* temp; + + // Store the current head pointer + temp = list->head; + // allocate memory for the new data (LEVEL tagged) + list->head = gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); + // Set up the data and pointer + list->head->node = node; + list->head->next = temp; + // Check if there;'s a next item + if (!list->head->next) + { + // Set the tail pointer = head + list->tail = list->head; + } +} + +//============================== +// SLLpop_front +//============================== +// Remove the item from the front of the list +// +void SLLpop_front(botlib_sll_t* list) +{ + botlib_sll_nodes_t* temp; + + // Store the head pointer + temp = list->head; + // Check if there's a next item + if (list && list->head) + { + if (list->head == list->tail) + { + // List is now emptying + list->tail = list->head = NULL; + } + else + { + // Move head to point to next item + list->head = list->head->next; + } + // Free the memory (LEVEL tagged) + gi.TagFree(temp); + } + else + { + gi.bprintf(PRINT_HIGH, "Attempting to POP an empty list!\n"); + } +} + +//rekkie -- DEV_1 -- s +//============================== +// BOTLIB_SLL_Query_Next_Node +//============================== +// Query the SLL for the next node +// Returns the next node, otherwise returns INVALID if next node cannot be found (end of list, or list is NULL) +int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list) +{ + if (0) //DEBUG: Print out the entire list + { + // Check if there's a next item + if (list && list->head && list->head != list->tail && list->head->next != NULL) + { + Com_Printf("%s nodes[ ", __func__); + Com_Printf("%d ", list->head->node); // Print head node + botlib_sll_nodes_t* next = list->head->next; + while (next != NULL) + { + if (list->head == list->tail) // We're already at the last item + break; + else + { + Com_Printf("%d ", next->node); // Print next node + next = next->next; // Get the next SLL item + } + } + Com_Printf("]\n"); + } + } + + // Check if there's a next item + if (list && list->head) + { + if (list->head == list->tail || list->head->next == NULL) // We're already at the last item + { + return INVALID; // No next item + } + else + { + botlib_sll_nodes_t* next = list->head->next; // Get the next SLL item + return next->node; // return the next node + } + } + + return INVALID; // Empty list +} +//============================== +// BOTLIB_SLL_Query_All_Nodes +//============================== +// Query the SLL list and return all its nodes +// node_list is the returned list of nodes +// max_nodes is the maximum allowed nodes we can store in node_list +// Returns the node count +int BOTLIB_SLL_Query_All_Nodes(edict_t *ent, botlib_sll_t* list, int* node_list, const int max_nodes) +{ + //int node_count = 0; // Node count + + if (ent->bot.node_list_count + 1 > max_nodes) + return ent->bot.node_list_count; + + // Check if there's a next item + if (list && list->head && list->head != list->tail && list->head->next != NULL) + { + //node_list[ent->bot.node_list_count++] = ent->bot.current_node; // Save current node + node_list[ent->bot.node_list_count++] = list->head->node; // Save head node + botlib_sll_nodes_t* next = list->head->next; + while (next != NULL) + { + if (ent->bot.node_list_count + 1 > max_nodes) return ent->bot.node_list_count; + + if (list->head == list->tail) // We're already at the last item + break; + else + { + node_list[ent->bot.node_list_count++] = next->node; // Save next node + next = next->next; // Get the next SLL item + } + } + } + + return ent->bot.node_list_count; // Return node count +} +//rekkie -- DEV_1 -- e + +//============================== +// SLLfront +//============================== +// Get the integer value from the front of the list +// without removing the item (Query the list) +// +int SLLfront(botlib_sll_t* list) +{ + if (list && !SLLempty(list)) + return(list->head->node); + else + return INVALID; +} + +//============================== +// SLLpush_front +//============================== +// Add to the back of the list +// +void SLLpush_back(botlib_sll_t* list, int node) +{ + botlib_sll_nodes_t* temp; + + // Allocate memory for the new item (LEVEL tagged) + temp = (botlib_sll_nodes_t*)gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); + // Store the data + temp->node = node; + temp->next = NULL; // End of the list + // Store the new item in the list + // Is the list empty? + if (!list->head) + { + // Yes - add as a new item + list->head = temp; + list->tail = temp; + } + else + { + // No make this the new tail item + list->tail->next = temp; + list->tail = temp; + } +} + +//============================== +// SLLempty +//============================== +// See if the list is empty (false if not empty) +// +qboolean SLLempty(botlib_sll_t* list) +{ + // If there is any item in the list then it is NOT empty... + if (list) + return (list->head == NULL); + else // No list so return empty + return true; +} + +//=============================== +// Delete the list +//=============================== +// Avoids memory leaks +// +void SLLdelete(botlib_sll_t* list) +{ + botlib_sll_nodes_t* temp; + + while (!SLLempty(list)) + { + temp = list->head; + list->head = list->head->next; + gi.TagFree(temp); + } +} + +//=============================== +// Quake 3 Multithreading Code +//=============================== + +/* + ================ + I_FloatTime + ================ + */ +double I_FloatTime(void) { + time_t t; + + time(&t); + + return t; +#if 0 + // more precise, less portable + struct timeval tp; + struct timezone tzp; + static int secbase; + + gettimeofday(&tp, &tzp); + + if (!secbase) { + secbase = tp.tv_sec; + return tp.tv_usec / 1000000.0; + } + + return (tp.tv_sec - secbase) + tp.tv_usec / 1000000.0; +#endif +} + + +// Threads.c -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/threads.c +#define GDEF_OS_WINDOWS 1 +#if !GDEF_OS_WINDOWS +// The below define is necessary to use +// pthreads extensions like pthread_mutexattr_settype +#define _GNU_SOURCE +#endif // !GDEF_OS_WINDOWS + +#define MAX_THREADS 64 + +int dispatch; +int workcount; +int oldf; +qboolean pacifier; + +qboolean threaded; + +/* + ============= + GetThreadWork + + ============= + */ +int GetThreadWork(void) { + int r; + int f; + + ThreadLock(); + + if (dispatch == workcount) { + ThreadUnlock(); + return -1; + } + + f = 40 * dispatch / workcount; + if (f < oldf) { + Com_Printf("%s WARNING: progress went backwards (should never happen)\n", __func__); + oldf = f; + } + Com_Printf("%s ", __func__); + while (f > oldf) + { + ++oldf; + if (pacifier) { + if (oldf % 4 == 0) { + Com_Printf("%i", f / 4); + } + else { + Com_Printf("."); + } + fflush(stdout); /* ydnar */ + } + } + Com_Printf("\n"); + + r = dispatch; + dispatch++; + ThreadUnlock(); + + return r; +} + + +void (*workfunction)(int); + +void ThreadWorkerFunction(int threadnum) { + int work; + + while (1) + { + work = GetThreadWork(); + if (work == -1) { + break; + } + //Com_Printf("%s thread %i, work %i\n", __func__, threadnum, work); + workfunction(work); + } +} + +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)) { +void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { + if (numthreads == -1) { + ThreadSetDefault(); + } + workfunction = func; + RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); +} + + +#if GDEF_OS_WINDOWS + +/* + =================================================================== + + WIN32 + + =================================================================== + */ + +#include + +int numthreads = -1; +CRITICAL_SECTION crit; +static int enter; + +void ThreadSetDefault(void) { + SYSTEM_INFO info; + + if (numthreads == -1) { // not set manually + GetSystemInfo(&info); + numthreads = info.dwNumberOfProcessors; + if (numthreads < 1 || numthreads > 32) { + numthreads = 1; + } + } + + Com_Printf("%s %i threads\n", __func__, numthreads); +} + + +void ThreadLock(void) { + if (!threaded) { + return; + } + EnterCriticalSection(&crit); + if (enter) { + Com_Printf("%s Recursive ThreadLock\n", __func__); + } + enter = 1; +} + +void ThreadUnlock(void) { + if (!threaded) { + return; + } + if (!enter) { + Com_Printf("%s ThreadUnlock without lock\n", __func__); + } + enter = 0; + LeaveCriticalSection(&crit); +} + +// Init and and run the threaded version +void BOTLIB_THREAD_LOADAAS(qboolean force) +{ + loadaas_t loadaas; + loadaas.force = force; + //RunThreadsOnIndividual(2, true, TestThreadedFunc, &loadaas); + //if (numthreads == -1) { + // ThreadSetDefault(); + //} + numthreads = 1; + GetThreadWork(); + RunThreadsOn(1, true, BOTLIB_THREADING_LOADAAS, &loadaas); +} + +// Threaded version +void BOTLIB_THREADING_LOADAAS(void *param) +{ + // Sleep while map fully loads and is ready, otherwise we'll get a crash + while (level.framenum < 50) + { + Sleep(100); + } + + //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web + //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing + loadaas_t* params = (loadaas_t*)param; + if (params != NULL) + { + //Com_Printf("%s param:0x%x force = %d\n", __func__, ¶m, params->force); + ACEND_LoadAAS(params->force); + } +} + +// ===================================================================================== +// Init and and run the threaded version +void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to) +{ + dijkstra_path_t dpath; + dpath.ent = ent; + dpath.from = from; + dpath.to = to; + + numthreads = 1; + GetThreadWork(); + //RunThreadsOn(1, true, BOTLIB_THREADING_DijkstraPath, &dpath); + + HANDLE thdHandle = CreateThread(NULL, 0, BOTLIB_THREADING_DijkstraPath, &dpath, 0, NULL); + if (thdHandle != NULL) + WaitForSingleObject(thdHandle, INFINITE); +} + +// Threaded version +void BOTLIB_THREADING_DijkstraPath(void* param) +{ + //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web + //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing + dijkstra_path_t* dpath = (dijkstra_path_t*)param; + if (dpath != NULL) + { + //Com_Printf("%s dpath:0x%x from[%d] to[%d]\n", __func__, &dpath, dpath->from, dpath->to); + BOTLIB_DijkstraPath(dpath->ent, dpath->from, dpath->to, true); + } +} +// ===================================================================================== + +/* + ============= + RunThreadsOn + ============= + */ +//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func), void *param) { + int threadid[MAX_THREADS]; + HANDLE threadhandle[MAX_THREADS]; + int i; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + // + // run threads in parallel + // + InitializeCriticalSection(&crit); + + if (numthreads < 1) + return; + + //if (numthreads == 1) { // use same thread + // func(0); + //} + //else + { + for (i = 0; i < numthreads; i++) + { + threadhandle[i] = CreateThread( + NULL, // LPSECURITY_ATTRIBUTES lpsa, + //0, // DWORD cbStack, + + /* ydnar: cranking stack size to eliminate radiosity crash with 1MB stack on win32 */ + (4096 * 1024), + + (LPTHREAD_START_ROUTINE)func, // LPTHREAD_START_ROUTINE lpStartAddr, + (LPVOID)param, + //(LPVOID)i, // LPVOID lpvThreadParm, + 0, // DWORD fdwCreate, + &threadid[i]); + } + + //for (i = 0; i < numthreads; i++) + // WaitForSingleObject(threadhandle[i], INFINITE); + } + DeleteCriticalSection(&crit); + + threaded = qfalse; + end = I_FloatTime(); + if (pacifier) { + Com_Printf("%s (%i)\n", __func__, end - start); + } +} + + +#elif GDEF_OS_OSF1 + +/* + =================================================================== + + OSF1 + + =================================================================== + */ + +int numthreads = 4; + +void ThreadSetDefault(void) { + if (numthreads == -1) { // not set manually + numthreads = 4; + } +} + +#include + +pthread_mutex_t* my_mutex; + +void ThreadLock(void) { + if (my_mutex) { + pthread_mutex_lock(my_mutex); + } +} + +void ThreadUnlock(void) { + if (my_mutex) { + pthread_mutex_unlock(my_mutex); + } +} + + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int i; + pthread_t work_threads[MAX_THREADS]; + pthread_addr_t status; + pthread_attr_t attrib; + pthread_mutexattr_t mattrib; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + if (!my_mutex) { + my_mutex = safe_malloc(sizeof(*my_mutex)); + if (pthread_mutexattr_create(&mattrib) == -1) { + Error("pthread_mutex_attr_create failed"); + } + if (pthread_mutexattr_setkind_np(&mattrib, MUTEX_FAST_NP) == -1) { + Error("pthread_mutexattr_setkind_np failed"); + } + if (pthread_mutex_init(my_mutex, mattrib) == -1) { + Error("pthread_mutex_init failed"); + } + } + + if (pthread_attr_create(&attrib) == -1) { + Error("pthread_attr_create failed"); + } + if (pthread_attr_setstacksize(&attrib, 0x100000) == -1) { + Error("pthread_attr_setstacksize failed"); + } + + for (i = 0; i < numthreads; i++) + { + if (pthread_create(&work_threads[i], attrib + , (pthread_startroutine_t)func, (pthread_addr_t)i) == -1) { + Error("pthread_create failed"); + } + } + + for (i = 0; i < numthreads; i++) + { + if (pthread_join(work_threads[i], &status) == -1) { + Error("pthread_join failed"); + } + } + + threaded = qfalse; + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#elif GDEF_OS_IRIX + +/* + =================================================================== + + IRIX + + =================================================================== + */ + +#define USED + +#include +#include +#include +#include + +int numthreads = -1; +abilock_t lck; + +void ThreadSetDefault(void) { + if (numthreads == -1) { + numthreads = prctl(PR_MAXPPROCS); + } + Sys_Printf("%i threads\n", numthreads); + usconfig(CONF_INITUSERS, numthreads); +} + + +void ThreadLock(void) { + spin_lock(&lck); +} + +void ThreadUnlock(void) { + release_lock(&lck); +} + + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int i; + int pid[MAX_THREADS]; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + init_lock(&lck); + + for (i = 0; i < numthreads - 1; i++) + { + pid[i] = sprocsp((void (*)(void*, size_t))func, PR_SALL, (void*)i + , NULL, 0x200000); // 2 meg stacks + if (pid[i] == -1) { + perror("sproc"); + Error("sproc failed"); + } + } + + func(i); + + for (i = 0; i < numthreads - 1; i++) + wait(NULL); + + threaded = qfalse; + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#elif GDEF_OS_LINUX || GDEF_OS_BSD || GDEF_OS_MACOS + +/* + ======================================================================= + + Linux pthreads + + ======================================================================= + */ + +#include + +int numthreads = -1; + +void ThreadSetDefault(void) { + if (numthreads == -1) { // not set manually +#ifdef _SC_NPROCESSORS_ONLN + long cpus = sysconf(_SC_NPROCESSORS_ONLN); + if (cpus > 0) { + numthreads = cpus; + } + else +#endif + /* can't detect, so default to four threads */ + numthreads = 4; + } + + if (numthreads > 1) { + Sys_Printf("threads: %d\n", numthreads); + } +} + +#include + +typedef struct pt_mutex_s +{ + pthread_t* owner; + pthread_mutex_t a_mutex; + pthread_cond_t cond; + unsigned int lock; +} pt_mutex_t; + +pt_mutex_t global_lock; + +void ThreadLock(void) { + pt_mutex_t* pt_mutex = &global_lock; + + if (!threaded) { + return; + } + + pthread_mutex_lock(&pt_mutex->a_mutex); + if (pthread_equal(pthread_self(), (pthread_t)&pt_mutex->owner)) { + pt_mutex->lock++; + } + else + { + if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { + pt_mutex->owner = (pthread_t*)pthread_self(); + pt_mutex->lock = 1; + } + else + { + while (1) + { + pthread_cond_wait(&pt_mutex->cond, &pt_mutex->a_mutex); + if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { + pt_mutex->owner = (pthread_t*)pthread_self(); + pt_mutex->lock = 1; + break; + } + } + } + } + pthread_mutex_unlock(&pt_mutex->a_mutex); +} + +void ThreadUnlock(void) { + pt_mutex_t* pt_mutex = &global_lock; + + if (!threaded) { + return; + } + + pthread_mutex_lock(&pt_mutex->a_mutex); + pt_mutex->lock--; + + if (pt_mutex->lock == 0) { + pt_mutex->owner = NULL; + pthread_cond_signal(&pt_mutex->cond); + } + + pthread_mutex_unlock(&pt_mutex->a_mutex); +} + +void recursive_mutex_init(pthread_mutexattr_t attribs) { + pt_mutex_t* pt_mutex = &global_lock; + + pt_mutex->owner = NULL; + if (pthread_mutex_init(&pt_mutex->a_mutex, &attribs) != 0) { + Error("pthread_mutex_init failed\n"); + } + if (pthread_cond_init(&pt_mutex->cond, NULL) != 0) { + Error("pthread_cond_init failed\n"); + } + + pt_mutex->lock = 0; +} + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + pthread_mutexattr_t mattrib; + pthread_attr_t attr; + pthread_t work_threads[MAX_THREADS]; + size_t stacksize; + + int start, end; + int i = 0; + + start = I_FloatTime(); + pacifier = showpacifier; + + dispatch = 0; + oldf = -1; + workcount = workcnt; + + pthread_attr_init(&attr); + if (pthread_attr_setstacksize(&attr, 8388608) != 0) { + stacksize = 0; + pthread_attr_getstacksize(&attr, &stacksize); + Sys_Printf("Could not set a per-thread stack size of 8 MB, using only %.2f MB\n", stacksize / 1048576.0); + } + + if (numthreads == 1) { + func(0); + } + else + { + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + if (pthread_mutexattr_init(&mattrib) != 0) { + Error("pthread_mutexattr_init failed"); + } + if (pthread_mutexattr_settype(&mattrib, PTHREAD_MUTEX_ERRORCHECK) != 0) { + Error("pthread_mutexattr_settype failed"); + } + recursive_mutex_init(mattrib); + + for (i = 0; i < numthreads; i++) + { + /* Default pthread attributes: joinable & non-realtime scheduling */ + if (pthread_create(&work_threads[i], &attr, (void* (*)(void*)) func, (void*)(uintptr_t)i) != 0) { + Error("pthread_create failed"); + } + } + for (i = 0; i < numthreads; i++) + { + if (pthread_join(work_threads[i], NULL) != 0) { + Error("pthread_join failed"); + } + } + pthread_mutexattr_destroy(&mattrib); + threaded = qfalse; + } + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#else // UNKNOWN OS + +/* + ======================================================================= + + SINGLE THREAD + + ======================================================================= + */ + +int numthreads = 1; + +void ThreadSetDefault(void) { + numthreads = 1; +} + +void ThreadLock(void) { +} + +void ThreadUnlock(void) { +} + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int start, end; + + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + start = I_FloatTime(); + func(0); + + end = I_FloatTime(); + if (pacifier) { + Com_Printf("%s (%i)\n", __func__, end - start); + } +} + +#endif // UNKNOWN OS + + diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c new file mode 100644 index 000000000..6ab90d386 --- /dev/null +++ b/src/action/botlib/botlib_nodes.c @@ -0,0 +1,5564 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + + +//rekkie -- DEV_1 -- s + +// Free nodes +void BOTLIB_FreeNodes(void) +{ + if (nodes) + { + Com_Printf("%s --- Freeing nodes ---\n", __func__); + free(nodes); + nodes = NULL; // Nullify dangling pointer + } + + if (path_table) + { + Com_Printf("%s --- Freeing path_table ---\n", __func__); + for (int r = 0; r < MAX_PNODES; r++) { + free(path_table[r]); + path_table[r] = NULL; // Nullify dangling pointer + } + + free(path_table); + path_table = NULL; // Nullify dangling pointer + } +} + + +// Adds a new or inactive 'inuse == false' flagged node (from deleting a node) +// Returns the node that was added/reused, INVALID if node was not added +int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type) +{ + // ================================================================================= + // Avoid placing nodes on 'hurt_touch' trigger areas + // ================================================================================= + if (0) + { + solid_t* trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); + if (trigger_solid == NULL) + { + Com_Printf("%s failed to malloc trigger_solid\n", __func__); + return INVALID; + } + BOTLIB_MakeEntsSolid(trigger_solid); // Save solid state of each ent + if (BOTLIB_UTIL_CHECK_FOR_HURT(origin)) + { + Com_Printf("%s ignoring hurt_touch\n", __func__); + return INVALID; + } + BOTLIB_RestoreEntsSolidState(trigger_solid); // Restore solid state, and free memory + } + // ================================================================================= + + int node = INVALID; + // Check for any unused nodes first + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) + { + node = i; // Free to use this node + break; + } + } + + if (node == INVALID) + { + // Absolute max allowed + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); + return INVALID; + } + + // ------------- + // Alloc memory + // ------------- + node_t* prev = NULL; + + // Nodes + if (numnodes == 0) + nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = nodes; // Keep a copy + nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); + } + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because nodes is null + nodes = NULL; + prev = NULL; + } + return INVALID; + } + + node = numnodes; + } + + // ------------------------------------------------------------------------------------- + // nodes + // ------------------------------------------------------------------------------------- + nodes[node].area = 0; // Area + nodes[node].weight = 0; // Weight + + VectorCopy(origin, nodes[node].origin); // Location + + //VectorCopy(origin, nodes[node].origin); //TODO::::::::::::::::::::::: Angles (for ladders, sniper spots, etc) + + VectorCopy(normal, nodes[node].normal); // Surface Normal + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + VectorCopy(mins, nodes[node].mins); // Box mins + VectorCopy(maxs, nodes[node].maxs); // Box maxs + if (type == NODE_CROUCH) + nodes[node].maxs[2] = CROUCHING_MAXS2; + if (type == NODE_BOXJUMP) + { + nodes[node].mins[0] = -8; + nodes[node].mins[1] = -8; + nodes[node].mins[2] = -12; + nodes[node].maxs[0] = 8; + nodes[node].maxs[1] = 8; + nodes[node].maxs[2] = 16; + } + VectorAdd(origin, mins, nodes[node].absmin); // Update absolute box min/max in the world + VectorAdd(origin, maxs, nodes[node].absmax); // Update absolute box min/max in the world + nodes[node].type = type; // Node type + nodes[node].nodenum = node; // Node number + nodes[node].inuse = true; // Node is inuse + nodes[node].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + nodes[node].links[i].targetNode = INVALID; // Link + nodes[node].links[i].targetNodeType = INVALID; // Type + nodes[node].links[i].cost = INVALID; // Cost + } + + if (node == numnodes) + numnodes++; // Update the node counter + + return node; +} + +/////////////////////////////////////////////////////////////////////// +// Nodes added by the new navigation system +/////////////////////////////////////////////////////////////////////// +qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type) +{ + // Absolute max allowed + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); + return false; + } + + // ------------- + // Alloc memory + // ------------- + node_t* prev = NULL; + + // Nodes + if (numnodes == 0) + nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = nodes; // Keep a copy + nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); + } + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because nodes is null + nodes = NULL; + prev = NULL; + } + return false; + } + + // Unsorted duplicate copy nodes + if (numnodes == 0) + unsorted_nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = unsorted_nodes; // Keep a copy + unsorted_nodes = (node_t*)realloc(unsorted_nodes, sizeof(node_t) * (numnodes + 1)); + } + if (unsorted_nodes == NULL) + { + Com_Printf("%s failed to malloc unsorted_nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because unsorted_nodes is null + unsorted_nodes = NULL; + prev = NULL; + } + return false; + } + + // ------------------------------------------------------------------------------------- + // nodes + // ------------------------------------------------------------------------------------- + VectorCopy(origin, nodes[numnodes].origin); // Location + VectorCopy(normal, nodes[numnodes].normal); // Surface Normal + nodes[numnodes].type = type; // Node type + nodes[numnodes].nodenum = numnodes; // Node number + nodes[numnodes].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + nodes[numnodes].links[i].targetNode = INVALID; // Link + nodes[numnodes].links[i].targetNodeType = INVALID; // Type + nodes[numnodes].links[i].cost = INVALID; // Cost + } + + + // ------------------------------------------------------------------------------------- + // unsorted_nodes + // ------------------------------------------------------------------------------------- + // Make a duplicate copy of nodes into unsorted_nodes. We'll use this later to optimise links + VectorCopy(origin, unsorted_nodes[numnodes].origin); // Location + VectorCopy(normal, unsorted_nodes[numnodes].normal); // Surface Normal + unsorted_nodes[numnodes].type = type; // Node type + unsorted_nodes[numnodes].nodenum = numnodes; // Node number + unsorted_nodes[numnodes].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + unsorted_nodes[numnodes].links[i].targetNode = INVALID; // Link + unsorted_nodes[numnodes].links[i].targetNodeType = INVALID; // Type + unsorted_nodes[numnodes].links[i].cost = INVALID; // Cost + } + + //if ((numnodes % 1024) == 0) + //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, nodes[numnodes].nodenum, nodes[numnodes].origin[0], nodes[numnodes].origin[1], nodes[numnodes].origin[2]); + + numnodes++; // Update the node counter + + return true; +} + +/* +/////////////////////////////////////////////////////////////////////// +// Makes a duplicate copy into the unsorted_nodes[] array +/////////////////////////////////////////////////////////////////////// +void ACEND_DuplicateNodes(int node_num) +{ + node_t* unsorted_node = (unsorted_nodes + node_num); // Get our node by pointer arithmatic + + VectorCopy(nodes[node_num].origin, unsorted_node->origin); // Location + VectorCopy(nodes[node_num].normal, unsorted_node->normal); // Surface Normal + unsorted_node->type = nodes[node_num].type; // Node type + unsorted_node->nodenum = nodes[node_num].nodenum; // Node number + + for (int i = 0; i < MAXLINKS; i++) // Init links + { + unsorted_node->links[i].targetNode = INVALID; // Invalidate all links + unsorted_node->links[i].cost = INVALID; // Invalidate all costs + } +} +void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type) +{ + // Sanity check + if (numnodes + 1 > MAX_NODES) + { + debug_printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_NODES); + return; + } + + //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, numnodes, origin[0], origin[1], origin[2]); + + VectorCopy(origin, nodes[numnodes].origin); // Location + VectorCopy(normal, nodes[numnodes].normal); // Surface Normal + nodes[numnodes].type = type; // Node type + nodes[numnodes].nodenum = numnodes; // Node number + + //node_ents[numnodes] = self; // Add door as an entity reference //rekkie -- DEV_1 + + // Init links + for (int i = 0; i < MAXLINKS; i++) + { + nodes[numnodes].links[i].targetNode = INVALID; // Link + nodes[numnodes].links[i].targetNodeType = INVALID; // Type + memset(nodes[numnodes].links[i].targetVelocity, 0, sizeof(vec3_t)); // Velocity + nodes[numnodes].links[i].cost = INVALID; // Cost + } + + // Make a duplicate copy of nodes[] to *unsorted_nodes + // We'll use this later to optimise links + ACEND_DuplicateNodes(numnodes); + + // Show ever 10nth node + //if (numnodes % 10 == 0) + // ACEND_ShowNode(numnodes); + + numnodes++; // Update the node counter +} +*/ + +/////////////////////////////////////////////////////////////////////// +// Remove node by number, +// Shifts all the nodes down by one to replace the deleted node +/////////////////////////////////////////////////////////////////////// +void ACEND_RemoveNode(edict_t* self, int nodenum) +{ + int i, j, n, l; + + // Sanity + if (nodenum <= 0 || nodenum + 1 > MAX_PNODES) + return; + + // Remove links to/from this node + for (i = 0; i < MAXLINKS; i++) + { + // Remove link from other node to our node + n = nodes[nodenum].links[i].targetNode; // Links from this node to other nodes + if (n != INVALID) + { + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[n].links[j].targetNode == nodenum) // If the other node has a link back to our node + { + nodes[n].links[j].targetNode = INVALID; // Remove its link to us + } + } + } + + // Remove our link to other node + nodes[nodenum].links[i].targetNode = INVALID; + } + + // Update node ent + edict_t* ent; + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + // Free removed node ent + if (ent->node_num == nodenum) + { + G_FreeEdict(ent); + } + + // Update node name and number + if (ent->node_num > nodenum) + { + ent->node_num--; + + if (nodes[nodenum].type == NODE_MOVE) + Q_strncpyz(ent->node_name, va("node move [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_WATER) + Q_strncpyz(ent->node_name, va("node water [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_JUMPPAD) + Q_strncpyz(ent->node_name, va("node jumppad [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_SPAWNPOINT) + Q_strncpyz(ent->node_name, va("node spawn point [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_POI) + Q_strncpyz(ent->node_name, va("node point of interest [%d]", ent->node_num), sizeof(ent->node_name)); + else + Q_strncpyz(ent->node_name, va("node action [%d]", ent->node_num), sizeof(ent->node_name)); + } + } + + // Shift all nodes and links down one + for (n = 0; n < MAX_PNODES; n++) + { + if (n != nodenum) + { + // Move links down + for (l = 0; l < MAXLINKS; l++) + { + if (nodes[n].links[l].targetNode != INVALID) + { + if (nodes[n].links[l].targetNode > nodenum) + nodes[n].links[l].targetNode--; + } + } + } + } + + // Shift all nodes and links down one + for (n = 0; n + 1 < MAX_PNODES; n++) + { + // Copy links + if (n >= nodenum) + { + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = nodes[n + 1].links[l].targetNode; + } + } + + // Shift node down + copy + if (n >= nodenum) + { + nodes[n].type = nodes[n + 1].type; + VectorCopy(nodes[n + 1].origin, nodes[n].origin); + node_ents[n] = node_ents[n + 1]; + --nodes[n].nodenum; + } + } +} + +/////////////////////////////////////////////////////////////////////// +// Add/Update (one way) node link +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost) +{ + int i; + vec3_t v; + float cost; + + // Sanity Checks + if (type == INVALID) + { + if (debug_mode) debug_printf("%s cannot add link from %d to %i, targetNodeType: %d \n", __func__, from, to, type); + return false; + } + if (from == INVALID || to == INVALID || from == to) + { + if (debug_mode) debug_printf("%s error: cannot add link from %d to %i\n", __func__, from, to); + return false; + } + if (VectorEmpty(nodes[from].origin)) + { + if (debug_mode) debug_printf("%s error: zero origin from %d\n", __func__, from); + return false; + } + if (VectorEmpty(nodes[to].origin)) + { + if (debug_mode) debug_printf("%s error: zero origin to %d\n", __func__, to); + return false; + } + + // Check max links reached + if (nodes[from].num_links + 1 >= MAXLINKS) + { + if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); + return false; + } + + // Check if already added + for (i = 0; i < nodes[from].num_links; i++) // Always cycle all links so we can check for existing links + { + if (nodes[from].links[i].targetNode == to) // Do we already have a link to this target? + { + if (debug_mode) Com_Printf("%s warning: target link %d already link to %i\n", __func__, nodes[from].links[i].targetNode, to); + return false; // Failed to add link + } + } + + // Add link if we can + for (i = 0; i < MAXLINKS; i++) // Always cycle all links so we can check for existing links + { + if (i + 1 == MAXLINKS) // Max links reached + { + if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); + return false; // Failed to add link + } + + if (nodes[from].links[i].targetNode == INVALID) // Found a free spot to add a new link + { + nodes[from].num_links++; // Update link counter + nodes[from].links[i].targetNode = to; // Add the link + nodes[from].links[i].targetNodeType = type; // The type of the target node + + if (do_cost) // Add the cost of this link? + { + VectorSubtract(nodes[from].origin, nodes[to].origin, v); // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. + cost = VectorLength(v); // The cost here is distance based + nodes[from].links[i].cost = cost; // Add cost + //if (debug_mode) + // Com_Printf("Link node[%d][%d] type[%d][%d] cost[%f]\n", from, to, nodes[from].type, nodes[to].type, cost); + } + //else if (debug_mode) debug_printf("Link %d -> %d\n", from, to); + + return true; // Successfully added link + } + } + return false; // Failed to add link +} + +/////////////////////////////////////////////////////////////////////// +// Add/Update (one way) node connection/path +/////////////////////////////////////////////////////////////////////// +qboolean ACEND_UpdateNodeReach(int from, int to) +{ + int i; + //trace_t trace; + + /* + // Sanity Checks + if (from == INVALID || to == INVALID || from == to) + return qfalse; + if (nodes[from].origin[0] == 0 && nodes[from].origin[1] == 0 && nodes[from].origin[2] == 0) + { + if (debug_mode) + debug_printf("Zero origin from %d\n", from); + return qfalse; + } + if (nodes[to].origin[0] == 0 && nodes[to].origin[1] == 0 && nodes[to].origin[2] == 0) + { + if (debug_mode) + debug_printf("Zero origin to %d\n", to); + return qfalse; + } + */ + + ///////////////////// + + // Add the link + path_table[from][to] = to; + + /* + // Checks if the link exists and then may create a new one - RiEvEr + for (i = 0; i < MAXLINKS; i++) + { + if (nodes[from].links[i].targetNode == to) + break; + if (nodes[from].links[i].targetNode == INVALID) + { + // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. + vec3_t v; + float thisCost; + + VectorSubtract(nodes[from].origin, nodes[to].origin, v); // subtract first + thisCost = VectorLength(v); + nodes[from].links[i].targetNode = to; + nodes[from].links[i].cost = thisCost; + if (debug_mode) + debug_printf("Link %d -> %d\n", from, to); + break; + } + } + */ + + // Now for the self-referencing part, linear time for each link added + for (i = 0; i < numnodes; i++) + { + if (path_table[i][from] != INVALID) + { + if (i == to) + path_table[i][to] = INVALID; // make sure we terminate + else + path_table[i][to] = path_table[i][from]; + } + } + + return true; +} +//rekkie -- DEV_1 -- e + +//rekkie -- DEV_1 -- s +//////////////////////////////////////////////////////////////////////////////////////////// +// Remove all node links from a particular node, including links from and to the node +// This is used when deleting a node, so the links get destroyed properly (bi-directional) +//////////////////////////////////////////////////////////////////////////////////////////// +void BOTLIB_RemoveAllNodeLinksFrom(int from) +{ + int i, j, to; + + // Other nodes + int link_other_counter = 0; + int link_other_to[MAXLINKS]; + int link_other_from[MAXLINKS]; + + // Our node + int link_self_counter = 0; + int link_self_to[MAXLINKS]; + int link_self_from[MAXLINKS]; + + for (i = 0; i < numnodes; i++) + { + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[i].links[j].targetNode != INVALID && nodes[i].links[j].targetNode == from) + { + //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, nodes[i].nodenum, from); + link_other_to[link_other_counter] = nodes[i].nodenum; + link_other_from[link_other_counter] = from; + link_other_counter++; + } + } + } + + // Gather all the link information before doing anything with it + for (i = 0; i < MAXLINKS; i++) + { + if (nodes[from].links[i].targetNode != INVALID) + { + to = nodes[from].links[i].targetNode; + + /* + // Check other node we're connected to + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[to].links[j].targetNode == from) + { + //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, to, from); + link_other_to[link_other_counter] = to; + link_other_from[link_other_counter] = from; + link_other_counter++; + } + } + */ + + //gi.dprintf("%s: OurNode %d -> %d\n", __func__, from, to); + link_self_from[link_self_counter] = from; + link_self_to[link_self_counter] = to; + link_self_counter++; + } + } + + // Now use the link information we gathered to remove connections between nodes + // + // Remove other node links to ours + for (i = 0; i < link_other_counter; i++) + ACEND_RemoveNodeEdge(NULL, link_other_to[i], link_other_from[i]); + // Remove our node link to other nodes + for (i = 0; i < link_self_counter; i++) + ACEND_RemoveNodeEdge(NULL, link_self_from[i], link_self_to[i]); +} + +#if 0 +// TODO: This function isn't ready for use yet +// Removes a node link from all nodes +void BOTLIB_RemoveNodeLink(edict_t* self, int from, int to) +{ + path_table[from][to] = INVALID; // set to invalid + + /* + // Fill the gap caused by the removal by left shifting from the position of the removed link + for (int i = 0; i < MAXLINKS; i++) + { + // Terminate the last link by making it INVALID + if (i == MAXLINKS - 1) + { + nodes[from].links[i].targetNode = INVALID; + nodes[from].links[i].cost = INVALID; + } + + // Move all the other info down to fill the gap (left shift). + // This *MUST* be done because pathing expects links to be contiguous, + // with the first INVALID link found becoming the terminator. + // IE: First link --> [LINK]-[LINK]-[LINK]-[LINK]-[INVALID] <-- terminator [INVALID]-[INVALID] <-- up to MAXLINKS + if (nodes[from].links[i].targetNode != INVALID) + { + nodes[from].links[i].targetNode = nodes[from].links[i + 1].targetNode; + nodes[from].links[i].cost = nodes[from].links[i + 1].cost; + } + } + + // Make sure this gets updated in our path array + for (int i = 0; i < numnodes; i++) + if (path_table[from][i] == to) + path_table[from][i] = INVALID; + */ +} +#endif + +// Checks a node to see if its possible to path to +// Parameters: +// goal_node: the goal node to reach +// path_randomization: Should path be randomized (if true, this can lead to no path being taken) +// check_and_goto: If TRUE check for a valid path AND go there. If FALSE check for valid path but don't go there. +// build_new_path: If we're building a new path or adding to an existing path +qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path) +{ + // Always update current node + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + // Invalid node + if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + return false; + } + + if (nodes[goal_node].type == NODE_LADDER) // Never visit ladder nodes + return false; + + // Already at node + if (goal_node == self->bot.current_node) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); + return false; + } + + // NEW PATHING: limit search to same area + if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) + return false; + + // OLD: searches all nodes + //else if (AntStartSearch(self, self->bot.current_node, goal_node, path_randomization) == false) + else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false) + return false; + + + + // ================================================================ + // BOTLIB_SetGoal () + // ================================================================ + if (self->bot.goal_node == INVALID) + self->bot.goal_node = goal_node; + + int nodelist[MAX_NODELIST]; + int nodes_touched; + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) + { + self->bot.current_node = nodelist[i]; + break; + } + } + + if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // BOT_MOVE_STATE_WANDER + return false; + } + + self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + self->node_timeout = 0; + + self->bot.state = BOT_MOVE_STATE_MOVE; + + //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); + // ================================================================ + + + return true; +} + +/////////////////////////////////////////////////////////////////////// +// Set up the goal +/////////////////////////////////////////////////////////////////////// + +void BOTLIB_SetGoal(edict_t* self, int goal_node) +{ + // This code is now merged inside BOTLIB_CanVisitNode() + + /* + self->bot.goal_node = goal_node; + + int nodelist[MAX_NODELIST]; + int nodes_touched; + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) + { + self->bot.current_node = nodelist[i]; + break; + } + } + + if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; + //self->wander_timeout = level.framenum + 0.1 * HZ; + //ACEAI_PickLongRangeGoal(self); + //Com_Printf("%s %s invalid goal[%i]\n", __func__, self->client->pers.netname, self->bot.goal_node); + return; + } + + self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + self->node_timeout = 0; + + //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); //rekkie + + return; + */ +} + + + +// Run a trace from [start -> end] looking for nodes (hitbox) +// If [min/max] is empty -> Line trace +// If [min/max] not empty -> Box trace +// If a node was hit, return node number +// If nothing was hit, return INVALID +int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs) +{ + vec3_t pos, min, max, absmin, absmax; + vec3_t dir = { 0 }; + VectorCopy(mins, min); + VectorCopy(maxs, max); + if (VectorEmpty(min) && VectorEmpty(max)) + { + min[0] = -0.25; min[1] = -0.25; min[2] = -0.25; + max[0] = 0.25; max[1] = 0.25; max[2] = 0.25; + } + VectorSubtract(end, start, dir); + float distance = VectorLength(dir); + VectorCopy(start, pos); + float tested_distance = 1; + float normalized_dist = tested_distance / distance; // Normalized distance + while (tested_distance + 1 < distance) + { + tested_distance += 1; // Move distance forward + VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; + DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), mins, maxs, 5000); // Draw node box + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + } +#endif + //rekkie -- debug drawing -- e + + // Update absolute box min/max in the world + VectorAdd(pos, min, absmin); + VectorAdd(pos, max, absmax); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? + return i; + } + } + + return INVALID; // No intersection +} +/* +// Generate vis data for each node - test node-to-node visibility +void BOTLIB_GenerateNodeVis(edict_t* self) +{ + for (int i = 0; i < numnodes; i++) + nodes[i].num_vis_nodes = 0; // zero + + for (int i = 0; i < numnodes; i++) + { + for (int j = 0; j < numnodes; j++) + { + if (i == j) continue; // skip self + + // Check if the node was already added + for (int k = 0; k < nodes[i].num_vis_nodes; k++) + { + if (nodes[i].vis_nodes[k] == nodes[j].nodenum) + continue; + } + + if (nodes[i].num_vis_nodes + 1 >= 1024) + continue; + if (nodes[j].num_vis_nodes + 1 >= 1024) + continue; + + trace_t tr = gi.trace(nodes[i].origin, NULL, NULL, nodes[j].origin, NULL, MASK_SHOT); + + if (tr.fraction == 1.0) // Nodes can see each other + { + // Store the remote node in the local node + nodes[i].vis_nodes[nodes[i].num_vis_nodes] = nodes[j].nodenum; // Store the node we can see + nodes[i].num_vis_nodes++; // Increase the vis nodes in cache + + // Store the local node in the remote node + nodes[j].vis_nodes[nodes[j].num_vis_nodes] = nodes[i].nodenum; // Store the node we can see + nodes[j].num_vis_nodes++; // Increase the vis nodes in cache + } + } + } +} +*/ + +// Trace from node to node, checking to see if we hit other nodes along the path +// Return the node number that was hit, or INVALID if none was hit +int BOTLIB_TraceBoxNode(int from, int to) +{ + if (from == INVALID || to == INVALID) return INVALID; + + vec3_t pos, absmin, absmax; + vec3_t dir = { 0 }; + VectorSubtract(nodes[to].origin, nodes[from].origin, dir); + float distance = VectorLength(dir); + VectorCopy(nodes[from].origin, pos); + + float tested_distance = 1; + float normalized_dist = tested_distance / distance; // Normalized distance + while (tested_distance + 1 < distance) + { + tested_distance += 1; // Move distance forward + VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; + DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), nodes[from].mins, nodes[from].maxs, 5000); // Draw node box + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + } +#endif + //rekkie -- debug drawing -- e + + // Update absolute box min/max in the world + VectorAdd(pos, nodes[from].mins, absmin); + VectorAdd(pos, nodes[from].maxs, absmax); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + if (nodes[i].nodenum == from || nodes[i].nodenum == to) continue; // Ignore source and destination nodes + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? + return i; + } + } + + return INVALID; // No intersection +} + +// Test to see if a node is touching any other nodes +// Returns INVALID not touching any nodes +// Returns nodelist if one or more nodes are touching +// numnodes is the total nodes touched +// maxnodes is the maximum nodes that can be stored in nodelist +// ignore_node is optional: ignores testing this node when testing. If ignore_node is INVALID, all nodes are tested +int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int *nodelist, const int maxnodes, int ignore_node) +{ + int nodelist_count = 0; + + // Adjust the mins/maxs box size based on size + vec3_t bmins = { mins[0] + -(size), mins[1] + -(size), mins[2] + -(size) }; + vec3_t bmaxs = { maxs[0] + size, maxs[1] + size, maxs[2] + size }; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + VectorAdd(origin, bmins, absmin); + VectorAdd(origin, bmaxs, absmax); + + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + if (nodes[n].nodenum == INVALID) continue; // Ignore invalid nodes + //if (VectorDistance(nodes[n].origin, origin) > 32) continue; // Skip checking distant nodes + if (ignore_node != INVALID && nodes[n].nodenum == ignore_node) continue; // Ignore ignored node + + // Line test + //trace_t tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID | MASK_OPAQUE); + //if (tr.fraction == 1.0) + { + if (BOTLIB_BoxIntersection(absmin, absmax, nodes[n].absmin, nodes[n].absmax)) // Do boxes intersect? + { + //if (normal[2] == 1 && origin[2] != nodes[n].origin[2]) // Check flat ground + height difference + // continue; // Ignore nodes that are on a different plane (such as stairs or boxes) + + if (nodelist_count + 1 < maxnodes) // Check we have room to store + nodelist[nodelist_count++] = n; //nodes[n].nodenum; // Add to list + else + break; + } + } + } + + return nodelist_count; // Return number of nodes touched +} + +// Test location to see if taken by an existing node based on distance, return the node (if any) we're too close to +// Returns 'INVALID' if no node(s) are found +// Returns 'nodenum' if node(s) are found +int BOTLIB_TestForNodeDist(vec_t *origin, float distance, vec3_t mins, vec3_t maxs) +{ + trace_t tr; + //vec3_t v; + //float dist; + + // Check if we fit + tr = gi.trace(origin, tv(-2, -2, 0), tv(2, 2, 0), origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return -2; // Inside solid + } + + // Test to see if node hits the ceiling + //tr = gi.trace(tv(origin[0], origin[1], origin[2]), tv(-16, -16, -32), tv(16, 16, 25), tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return -2; // Hit our head on something immediately above us + } + + + vec3_t bmins = { mins[0] + -(distance), mins[1] + -(distance), mins[2] + -(distance) }; + vec3_t bmaxs = { maxs[0] + distance, maxs[1] + distance, maxs[2] + distance }; + + //for (int n = 0; n < NODE_MAX; n++) + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + + if (nodes[n].nodenum != INVALID) + { + // Line test + tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Only distance test origins that can see each other + { + // New method of box intersection search + if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[n].mins, nodes[n].maxs)) + return nodes[n].nodenum; // Return the first node found too close + + /* + // Old method of radius search + VectorSubtract(nodes[n].origin, origin, v); // other node -> our node + dist = VectorLength(v); + if (dist < distance) + { + //if (dist == 0) // If we find a node at the exact same coordinates + // Com_Printf("%s found node %d at the same origin[%f %f %f]\n", __func__, n, origin[0], origin[1], origin[2]); + + //gi.dprintf("-=-=-=-=-=------------[%i] dist[%f]\n", n, dist); + return nodes[n].nodenum; // Return the first node found too close + } + */ + } + } + } + + return INVALID; // Found no nodes within range, safe to place a node here :) +} + +// Test all nodes to see they're too close to our node +// Calculation is based on matching the exact X and Y coords, then comparing the height difference +// Returns the first offending node that is too close, otherwise INVALID if no node is found +int BOTLIB_UTIL_NearbyNodeAtHeightDist(vec_t* origin, float distance) +{ + float dist; + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + + if (nodes[n].nodenum != INVALID) + { + if (origin[0] == nodes[n].origin[0]) // Match X axis + { + if (origin[1] == nodes[n].origin[1]) // Match Y axis + { + dist = abs(nodes[n].origin[2] - origin[2]); // Get Z height distance + + if (dist <= distance) // Z axis within distance + return n; // Offending node that is too close + } + } + } + } + + return INVALID; // Found no nodes that are too close +} + +float HalfPlaneSign(vec2_t p1, vec2_t p2, vec2_t p3) +{ + return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); +} + +// Check if point is inside triangle. +// PT = (x,y,z) point location. V1,V2,V3 = 3 points of a triangle (x,y,z) +qboolean PointInTriangle(vec3_t pt, vec3_t v1, vec3_t v2, vec3_t v3) +{ + float d1, d2, d3; + qboolean has_neg, has_pos; + + d1 = HalfPlaneSign(pt, v1, v2); + d2 = HalfPlaneSign(pt, v2, v3); + d3 = HalfPlaneSign(pt, v3, v1); + + has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); + has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); + + return !(has_neg && has_pos); +} + +// Returns true if the point is inside a convex polygon +qboolean PointInPolygon(vec2_t pt, vec2_t* v, int num_verts) +{ + int i, j, c = 0; + for (i = 0, j = num_verts - 1; i < num_verts; j = i++) + { + if (((v[i][1] > pt[1]) != (v[j][1] > pt[1])) && + (pt[0] < (v[j][0] - v[i][0]) * (pt[1] - v[i][1]) / (v[j][1] - v[i][1]) + v[i][0])) + c = !c; + } + return c; +} + +// A function that makes a square polygon from a set of vertices +void BOTLIB_UTIL_MakePolySquare(int face, vec3_t out) +{ + int e; + vec3_t edge[128][2]; // Max edges per face + for (e = 0; e < nmesh.face[face].num_edges; e++) + { + // Copy all the edges into a 2D array + VectorCopy(nmesh.face[face].edge[e].v[0], edge[e][0]); // v1 + VectorCopy(nmesh.face[face].edge[e].v[1], edge[e][1]); // v2 + } + + /* + face 101 edges 6 + -------------- + v1[256.000000 -176.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] + v1[256.000000 -112.000000 -800.000000] v2[256.000000 -176.000000 -800.000000] + v1[256.000000 -33.000000 -800.000000] v2[256.000000 -112.000000 -800.000000] + v1[256.000000 0.000000 -800.000000] v2[256.000000 -33.000000 -800.000000] + v1[480.000000 0.000000 -800.000000] v2[256.000000 0.000000 -800.000000] + v1[480.000000 0.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] + -------------- + */ + + // Find each edge that goes in the same direction + // Try to find the total length of that direction + vec3_t v1, v2; + float highest = 0, lowest = 0; + float v1_x; // V1 X + float v1_y; // V1 Y + float v2_x; // V2 X + float v2_y; // V2 Y + + float highest_x = 0; + float lowest_x = 0; + float highest_y = 0; + float lowest_y = 0; + + for (e = 0; e < nmesh.face[face].num_edges; e++) + { + VectorCopy(edge[e][0], v1); // First vertex + VectorCopy(edge[e][1], v2); // Second vertex + + for (int e2 = 0; e2 < nmesh.face[face].num_edges; e2++) + { + if (e == e2) continue; // Skip self + + // Check the X axis + v1_x = v1[0]; v2_x = v2[0]; // X + v1_y = v1[1]; v2_y = v2[1]; // Y + + if (v1_x == v2_x) // Check if v1 and v2 are on the same X axis + { + // Find highest on the Y axis + if (v1_y > highest_y) + highest_y = v1_y; + else if (v2_y > highest_y) + highest_y = v2_y; + + // Find lowest on the Y axis + if (v1_y < lowest_y) + lowest_y = v1_y; + else if (v2_y < lowest_y) + lowest_y = v2_y; + } + if (v1_y == v2_y) // Check if v1 and v2 are on the same Y axis + { + // Find highest on the X axis + if (v1_x > highest_x) + highest_x = v1_x; + else if (v2_x > highest_x) + highest_x = v2_x; + + // Find lowest on the X axis + if (v1_x < lowest_x) + lowest_x = v1_x; + else if (v2_x < lowest_x) + lowest_x = v2_x; + } + } + } +} + + +// A function that finds the center of a 3D convex polygon +void BOTLIB_UTIL_PolygonCenter(vec3_t* v, int num_verts, vec3_t center) +{ + vec3_t sum = { 0,0,0 }; + for (int i = 0; i < num_verts; i++) + { + VectorAdd(sum, v[i], sum); + } + VectorScale(sum, 1.0 / num_verts, center); +} + +// Return the BSP face and triangle based on origin +void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle) +{ + vec3_t org = { 0,0,0 }; + vec3_t dist = { 0,0,0 }; + int n; + float distance; + + for (int f = 0; f < nmesh.total_faces; f++) // Current face + { + if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls + + for (int t = 0; t < nmesh.face[f].num_tris; t++) // Current triangle + { + if (PointInTriangle(origin, nmesh.face[f].tris[t].v[0], nmesh.face[f].tris[t].v[1], nmesh.face[f].tris[t].v[2])) + { + // Ignore tris with invalid nodes + n = nmesh.face[f].tris[t].node; + if (n <= 0) + continue; + + // Ensure the PointInTriangle is roughly within the same z height as origin + VectorCopy(origin, org); // We only want the height + org[0] = 0; // Remove X component + org[1] = 0; // Remove Y component + VectorSubtract(nodes[n].origin, origin, dist); + distance = VectorLength(dist); + if (distance > 64) // Height + continue; + + //Com_Printf("%s -> f[%i] t[%i] node %i at %f %f %f\n", __func__, f, t, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + + *found = true; + *floor = f; + *triangle = t; + return; + } + } + } +} + +// Return the BSP face and number of edges that is closest to origin +void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int *edges) +{ + vec3_t org = { 0,0,0 }; + vec3_t dist = { 0,0,0 }; + int e; // edge + float distance; + float closest = 999999; + + for (int f = 0; f < nmesh.total_faces; f++) // Current face + { + if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls + + vec3_t edge[128]; // Max edges per face + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Copy all the edges into a 2D array + VectorCopy(nmesh.face[f].edge[e].v[0], edge[e]); + } + + BOTLIB_UTIL_PolygonCenter(edge, nmesh.face[f].num_edges, org); // Get the center of the polygon + + // Ensure the polygon is roughly within the same z height as origin + //VectorCopy(origin, org); // We only want the height + //org[0] = 0; // Remove X component + //org[1] = 0; // Remove Y component + VectorSubtract(org, origin, dist); + distance = VectorLength(dist); + //if (distance > 64) // Height + // continue; + + if (distance < closest) + { + closest = distance; + *floor = f; + *edges = e; + } + + } +} + + +// Returns the distance from origin to target +float ACEND_Distance(vec3_t origin, vec3_t target) +{ + vec3_t dist; + VectorSubtract(target, origin, dist); + return VectorLength(dist); +} + +// Using three points of a triangle with origin being the origin calculate the angle between target_a and target_b +float ACEND_AngleBetween(vec3_t origin, vec3_t target_a, vec3_t target_b) +{ + vec3_t a, b; + VectorSubtract(target_a, origin, a); // Get length + VectorSubtract(target_b, origin, b); // Get length + return RAD2DEG(acos(DotProduct(a, b) / (VectorLength(a) * VectorLength(b)))); +} + + +void LaunchP(edict_t* ent, vec3_t origin, vec3_t target) +{ + vec3_t dist; + vec3_t velocity; + float gravity = ent->gravity * sv_gravity->value; + VectorSubtract(target, ent->s.origin, dist); + float distance = VectorLength(dist); + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + + // Calculate the time it will take to get to the target + float time = distance / (ent->velocity[2] + jump_height / 2.0); + + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height = 0; + if (target[2] > ent->s.origin[2]) + { + z_height = (target[2] - ent->s.origin[2]); + z_height += sqrtf(distance) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else if (abs(target[2] - ent->s.origin[2]) <= 4) + { + // Do nothing here + } + else + { + z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, ent->velocity); + VectorCopy(ent->velocity, ent->client->oldvelocity); + VectorCopy(ent->velocity, ent->avelocity); + + float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); +} + +qboolean LaunchPlayer(edict_t* ent, vec3_t target) +{ + + LaunchP(ent, ent->s.origin, target); + return false; + + // Calculate the distance to the target + vec3_t dist; + vec3_t velocity; + float gravity = ent->gravity * sv_gravity->value; + //float gravity = ent->gravity * sv_gravity->value * FRAMETIME; + + + vec3_t targ; + VectorCopy(target, targ); + //targ[2] += 64; + VectorSubtract(targ, ent->s.origin, dist); + //dist[2] = 0; + //dist[2] += 64; + float distance = VectorLength(dist); + VectorCopy(ent->velocity, velocity); + //velocity[2] += 64; + //VectorNormalize(velocity); + //VectorNormalize(dist); + //if (DotProduct(velocity, dist) > 0.8) + //Com_Printf("%s %f\n", __func__, DotProduct(velocity, dist)); + //distance *= 2.0; // Increasing this increases the jump height + + + //VectorSubtract(target, ent->s.origin, dist); + //float distance = VectorLength(dist); + //distance *= 1.0; // Increasing this increases the jump height + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + // Calculate the time it will take to get to the target + float time = distance / (ent->velocity[2] + jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (target[2] > ent->s.origin[2]) + { + z_height = (target[2] - ent->s.origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else + { + z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + + /* + // Get the z height distance between origin and target + vec3_t zdiff, oz, tz; + // Copy origin to oz + VectorCopy(ent->s.origin, oz); + // Copy target to tz + VectorCopy(target, tz); + // Set X and Y on oz and tz to zero + oz[0] = 0; oz[1] = 0; + tz[0] = 0; tz[1] = 0; + // Get the z height distance between oz and tz + VectorSubtract(oz, tz, zdiff); + // Add the difference to jump velocity + float z_height = 0; + if (ent->s.origin[2] < target[2]) + { + //z_height = VectorLength(zdiff); + velocity[2] += z_height + 56; + } + else + { + velocity[2] -= VectorLength(zdiff); + } + */ + + // Calculate speed from velocity + float speed = VectorLength(velocity); + gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + // Limit max speed + //if (speed < 750 && z_height < 150 && jump_height <= 965) + { + // Set the velocity of the player + VectorCopy(velocity, ent->velocity); + // Don't take falling damage immediately from this + if (ent->client) + VectorCopy(ent->velocity, ent->client->oldvelocity); + + return true; + } + return false; +} + +int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal) +{ + trace_t tr; + + //static const float max_dist = 128; // Absolute max distance -- 470 + const float max_dist = 470; // Absolute max distance -- 470 + //float curr_dist = 224; + //qboolean is_los = true; // Line of Sight + qboolean is_gap = false; // If there's a gap between origin and target + + // Sanity checks + if (from == to || from == INVALID || to == INVALID) + return INVALID; + if (origin[0] == 0 && origin[1] == 0 && origin[2] == 0) + return INVALID; + if (target[0] == 0 && target[1] == 0 && target[2] == 0) + return INVALID; + + + //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) + // Com_Printf("%s NODE_WATER\n", __func__); + + /* + // Ignore some water nodes (except ladders) + // Don't go into the water + if (nodes[from].type != NODE_WATER && nodes[to].type == NODE_WATER) + return INVALID; + // Check node is in water + if (nodes[from].type == NODE_WATER && (gi.pointcontents(nodes[from].origin) & CONTENTS_WATER) == 1) + return INVALID; + // Check node is in water + if (nodes[to].type == NODE_WATER && (gi.pointcontents(nodes[to].origin) & CONTENTS_WATER) == 1) + return INVALID; + //if (nodes[from].type == NODE_WATER) + // return INVALID; + //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) + // return INVALID; + */ + + //vec3_t vec_from, vec_to; + + // Calculate the distance to the target + vec3_t dist; + vec3_t velocity; + + VectorSubtract(target, origin, dist); + float xyz_distance = VectorLength(dist); // XYZ distance + + //VectorSubtract(target, origin, dist); + dist[2] = 0; + float xy_distance = VectorLength(dist); // XY Distance + + // Max distance + if (xyz_distance > max_dist) // Ignore very far nodes + return INVALID; + + // Min distance + //if (xyz_distance < NODE_Z_HEIGHT) // Ignore very close nodes + // return INVALID; + + /* + // Player when standing + vec3_t player_standing_mins = { -16, -16, -24 }; // With legs + vec3_t player_partial_standing_mins = { -16, -16, -0 }; // Without legs + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing smaller mins + vec3_t player_standing_smaller_mins = { -14, -14, -24 }; + vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; + // Player width only, no height component + vec3_t player_no_height_mins = { -16, -16, -0 }; + vec3_t player_no_height_maxs = { 16, 16, 0 }; + // Small box test + vec3_t small_mins = { -8, -8, -2 }; + vec3_t small_maxs = { 8, 8, 2 }; + // Tiny box test + vec3_t tiny_mins = { -1, -1, -1 }; + vec3_t tiny_maxs = { 1, 1, 1 }; + */ + + + // Do a line trace + tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); + // See if we hit any entities we wish to avoid + if (tr.ent && tr.ent != g_edicts) + { + //Com_Printf("%s %s found - \n", __func__, tr.ent->classname); + + // Avoid these + if (strcmp(tr.ent->classname, "trigger_hurt") == 0) + { + //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); + return INVALID; + } + + // Avoid rotating geometry - for now + if (strcmp(tr.ent->classname, "func_rotating") == 0) + { + //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); + return INVALID; + } + } + // See if we hit any walls or inside a solid + if (tr.fraction < 1.0 || tr.startsolid) + { + return INVALID; + } + + + + + + + // Test for gaps/holes between the current and next node + // Each tested is conducted x units apart + { + vec3_t dir, end; + float tested_distance = 0; + + // Get direction + VectorSubtract(target, origin, dir); // (end - start) = dir + + // Get XYZ distance + xyz_distance = VectorLength(dir); // VectorLength of dir (cannot be normalized) + + // Normalize direction vector (must be done after VectorLength otherwise we don't get the correct distance) + VectorNormalize(dir); + + // Safety check + if (xyz_distance >= 4096) // Max distance + { + Com_Printf("%s WARNING: gap test exceeded > 4096 units from node %d to %d\n", __func__, from, to); + } + else + { + while (tested_distance + NODE_SIZE < xyz_distance) + { + tested_distance += NODE_SIZE; // Move next test forward + VectorMA(origin, tested_distance, dir, end); // origin -> distance -> direction = end point + + // Check position to see if we hit any solids along the way + tr = gi.trace(end, tv(-12,-12,-8), tv(12, 12, 8), end, g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.startsolid) is_gap = true; + + end[2] -= NODE_Z_HEIGHT; // Move end point to ground + tr = gi.trace(end, NULL, NULL, tv(end[0], end[1], -4096), g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); // Trace down until we hit the ground + if (tr.startsolid) continue; + //is_gap = true; + + //if (tr.plane.normal[2] < 0.99) // If the ground we hit below is not flat + { + //is_gap = true; + //break; + } + + // + if (end[2] - tr.endpos[2] > (STEPSIZE + 1)) + { + //if (from == 1740 && to == 1403) + // Com_Printf("%s dist %f z-diff %f -- FOUND GAP\n", __func__, tested_distance, end[2] - tr.endpos[2]); + is_gap = true; + break; + } + } + } + } + + + + + // Use smaller node min/max for ladders + //if (nodes[to].type == NODE_LADDER_DOWN || nodes[to].type == NODE_LADDER_UP) + // //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component + // tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); // Line trace + //else + // tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height + if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) + { + tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height + //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component + // Try to trace from node->node LoS + // Failing that a second trace is performed with both nodes moved up slightly + if (tr.fraction < 1) + { + // Ignore ladders + //if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) + { + // See if target is below and if we can drop down + if (origin[2] > target[2] && xy_distance < 96) + { + //tr = gi.trace(origin, tv(-16, -16, 48), tv(16, 16, 56), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, 25), tv(16, 16, 26), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-8, -8, -0), tv(8, 8, 0), target, g_edicts, MASK_DEADSOLID); // LoS + width to the node below + if (tr.fraction == 1) + { + float lower = (origin[2] - target[2]); // Distance below + + if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_STAND_DROP; // Can drop while standing + } + + // Crouch drop down + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH_DROP; // Can drop but need to crouch + } + + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_UNSAFE_DROP; // Can drop while crouching but will take some leg damage + } + } + } + + // Check for crouching + if (nodes[to].type == NODE_MOVE && is_gap == false && xy_distance < 256) + { + tr = gi.trace(tv(origin[0], origin[1], origin[2] - 16), tv(-16, -16, -32), tv(16, 16, 0), tv(target[0], target[1], target[2] - 16), g_edicts, MASK_DEADSOLID); // Test crouch height + if (tr.fraction == 1) + { + return NODE_CROUCH; + } + } + } + + //return INVALID; + } + } + + + // Invalidate bad ladder links + // --------------------------- + // Ladder from DOWN -> UP must be on same X/Y position + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP && (origin[0] != target[0] || origin[1] != target[1])) + return INVALID; + // Ladder from UP -> DOWN must be on same X/Y position + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN && (origin[0] != target[0] || origin[1] != target[1])) + return INVALID; + // Ladder from DOWN -> DOWN + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_DOWN) + return INVALID; + // Ladder from UP -> UP + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_UP) + return INVALID; + + // Top ladder node -> non-ladder node, test if within z height range +/- + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type != NODE_LADDER_DOWN) + { + //if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 128 || xyz_distance < 48) + if (abs(origin[2] < target[2]) > 8 || xyz_distance > 128 || xyz_distance < 48) + return INVALID; + else + return NODE_MOVE; + //else + // return NODE_JUMPPAD; // Jump to next node + } + // Bottom ladder node -> non-ladder node, test if within z height range +/- + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_UP) + { + if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 96) + return INVALID; + } + + // Non-ladder node -> top ladder node - test if within z height range +/- + if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP) + { + if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) + return INVALID; + } + // Non-ladder node -> bottom ladder node - test if within z height range +/- + if (nodes[from].type != NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN) + { + //if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) + // return INVALID; + if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT && xyz_distance < 96) + return NODE_JUMPPAD; + else + return INVALID; + } + // --------------------------- + + + /* + //if (nodes[from].type == NODE_LADDER || nodes[to].type == NODE_LADDER) + // return INVALID; +// (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + if (nodes[from].type == NODE_LADDER && nodes[to].type == NODE_LADDER) + { + if (abs(origin[2] - target[2]) > 4) + { + if (abs(origin[0] - target[0]) <= 4 && abs(origin[1] - target[1]) <= 4) + return NODE_LADDER; + else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS) + return INVALID; + else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[0] == target[0] && abs(origin[1] - target[1]) <= 96) + return NODE_LADDER; + else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[1] == target[1] && abs(origin[0] - target[0]) <= 96) + return NODE_LADDER; + } + return INVALID; + } + else if (nodes[from].type == NODE_LADDER && nodes[to].type != NODE_LADDER && xyz_distance > 64) + return INVALID; + else if (nodes[from].type != NODE_LADDER && nodes[to].type == NODE_LADDER && xyz_distance > 64) + return INVALID; + */ + + + + + + + + //return INVALID; + + + + + + + + // Water nodes + if (nodes[from].type == NODE_WATER) + { + if (target[2] > origin[2] && xy_distance < 64 && nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder + return NODE_LADDER_UP; + //if (nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder + // return NODE_LADDER_DOWN; + //else if (nodes[to].type == NODE_MOVE) // Allow water -> move + // return NODE_MOVE; + else if (nodes[to].type == NODE_WATER) // Allow water -> water + return NODE_MOVE; + else + return INVALID; // Deny water to anything else + } + + + + // Steps + // ----- + // Deal with steps + if (nodes[from].type == NODE_STEP || nodes[to].type == NODE_STEP) + { + // Getting off step to a landing node + if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_MOVE && abs(origin[2] - target[2]) <= NODE_Z_HEIGHT_PLUS_STEPSIZE && xyz_distance <= 80) + { + trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_MOVE; + } + } + // Getting on step + else if (nodes[from].type == NODE_MOVE && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance <= 80) + { + trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_MOVE; + } + } + // Step to Step + else if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance < 40) + { + + //is_gap + trace_t tr_step = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_STEP; + } + } + else + return INVALID; + } + + + + + // Calculate max jump height + // If on flat ground the max is 60 (NODE_MAX_JUMP_HEIGHT) + // If on slope up, the max is 150 depending on the angle of the slope, higher angle = higher max jump height + float z_height_max = NODE_MAX_JUMP_HEIGHT; + //float max_speed = 577; //(750 / 1.3 = 577) + if (normal[0] != 1.0 && normal[1] != 1.0) + { + float z_height_max_x = 1.0; + float z_height_max_y = 1.0; + if (normal[0] >= MAX_STEEPNESS) // Player is standing on a slope going up //else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + { + z_height_max_x += (1 - normal[0]); + } + if (normal[1] >= MAX_STEEPNESS) + { + z_height_max_y += (1 - normal[1]); + } + z_height_max = (z_height_max_x + z_height_max_y) / 2; + if (z_height_max > 1.3) + z_height_max = 1.3; + else if (z_height_max < 1.0) + z_height_max = 1.0; + + z_height_max *= 116; // Max jump height is 150, and max z_height is 1.3 ... so 116 * 1.3 = 150 (150/1.3 = 116) + if (z_height_max > 150) + z_height_max = 150; + + //max_speed *= z_height_max; + } + + qboolean target_is_above = false, target_is_below = false, target_is_equal = false; + float higher = 0, lower = 0; + if (origin[2] > target[2]) // We're above the target + { + target_is_below = true; + lower = (origin[2] - target[2]); + } + else if (origin[2] < target[2]) // We're below the target + { + target_is_above = true; + higher = (target[2] - origin[2]); + } + else + target_is_equal = true; + + + + + + + + + + + + // Dropping down to target + // ----------------------- + // Can drop while standing + if (target_is_below && xy_distance < 228) //256 + { + if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_STAND_DROP; // Can drop while standing + } + + // Crouch drop down + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH_DROP; // Can drop but need to crouch + } + + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_UNSAFE_DROP; // Can drop while crouching but will take some minor leg damage + } + } + + // Small jump to target + // ----------------- + //if (target_is_above && distance > STEPSIZE && distance < NODE_Z_HEIGHT) + // return NODE_JUMP; + + // Jump nodes + //if (xyz_distance <= 96 && is_gap && fabs(origin[2] - target[2]) <= 48) + //{ + //return NODE_JUMP; + //} + + // Large jump up to target + // -------------------- + // Jumppad nodes + //if (target_is_above && distance > STEPSIZE) + if (xyz_distance >= 16 && is_gap) + { + float gravity = sv_gravity->value; // This doesn't take into account the ent's own gravity (ent->gravity) + + //distance *= 1.0; // Increasing this increases the jump height + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * gravity * xyz_distance); + float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point + + // Move the middle point up to the jump height (maximum parabola height) + //end_50[2] += NODE_Z_HEIGHT_PLUS_STEPSIZE + jump_height; + // Next test from start to mid, then mid to end + //tr_25 = gi.trace(origin, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + //tr_75 = gi.trace(target, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + // If the path from [start -> mid jump -> end] is clear of obsticles, then allow the jump + //if (tr_25.fraction == 1.0 && tr_75.fraction == 1.0) + + // Do we have room to jump without hitting our head? + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -6), tv(16, 16, 48), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT) , g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), NULL, NULL, tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -24), tv(16, 16, 32), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(origin, tv(-16, -16, -24), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + tr = gi.trace(origin, tv(-16, -16, 0), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + if (tr.fraction == 1.0 && !tr.startsolid) + { + //Com_Printf("jump_height[%f] jump_height_headroom[%f]\n", jump_height, jump_height_headroom); + + // Calculate the time it will take to get to the target + float time = xyz_distance / (jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = jump_height / 2.0; + // + // If the target is above the player, increase the velocity to get to the target + float z_height = 0; + + // Above + if (target[2] > origin[2]) + { + z_height = (target[2] - origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + + float speed = VectorLength(velocity); + //if (speed < 550 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) + if (speed < 550 && z_height <= z_height_max + sqrtf(xy_distance) && jump_height <= 450) + return NODE_JUMPPAD; + } + // Roughly equal + else if (abs(origin[2] - target[2]) <= 8) // target node is +/- 8 + { + float speed = VectorLength(velocity); + if (speed < 600 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 750) + return NODE_JUMPPAD; + } + // Below + else + { + z_height = (origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + + float speed = VectorLength(velocity); + if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) + return NODE_JUMPPAD; + } + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + //Com_Printf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + //if (distance > 64 && speed < 750 && z_height < z_height_max + 24 && jump_height <= 965) + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) // Works VERY well + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) + + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) + // return NODE_JUMPPAD; + } + } + + + // Limit max speed + //if (speed < 750 && z_height < 150 && jump_height <= 965) // Maximum + //if (speed <= 750 && z_height < 48 && jump_height <= 525) // Works well for ground based nodes + //if (speed <= 750 && z_height < NODE_MAX_JUMP_HEIGHT && jump_height <= 965) + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 600) // Works really well!! + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 700) + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 650) // Works uber well + //if (speed <= 750 && z_height <= z_height_max && (jump_height <= 650 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 64)) + //if (distance <= 224 && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32) ) + //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) + //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) + + // Small jump + //if (distance < 64 && higher > STEPSIZE && higher <= NODE_MAX_JUMP_HEIGHT) + //return NODE_JUMP; + + // Jumping up + //if (distance < 128 && higher > STEPSIZE && higher <= z_height_max) + //{ + //return NODE_JUMPPAD; + //} + + + + + // Running to target + // ----------------- + if (xy_distance < 400 && !is_gap) + { + if (origin[2] == target[2]) + { + //return INVALID; + + //if (from == 1066 && to == 1069) + // Com_Printf("%s\n", __func__); + + //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-15, -15, -NODE_Z_HEIGHT), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.fraction == 1) + return NODE_MOVE; + } + else if (abs(origin[2] - target[2]) <= STEPSIZE) // target node is +/- STEPSIZE + { + //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-15, -15, -(NODE_Z_HEIGHT - STEPSIZE)), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.fraction == 1) + return NODE_MOVE; + } + } + + /* + // Jump to target + // ----------------- + if (xy_distance < 128) + { + if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT) // target node is +/- NODE_MAX_JUMP_HEIGHT + { + // We passed a partial height test, now test players step height + // -32 is ground + 17 (one less than full step height of 18) = -14 + tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Step height + if (tr.fraction < 1) // We hit something + { + // Test if we can jump over + // mins: -32 is ground + 18 (step height) = -16 + // maxs: mins (-16) + full player size (56) = 40 + //tr = gi.trace(origin, tv(-16, -16, -16), tv(16, 16, 40), target, g_edicts, MASK_DEADSOLID); // Jump height + player height + //if (tr.fraction == 1.0) // We hit nothing, so we can jump over :) + // return NODE_JUMP; + + //VectorCopy(origin, vec_from); + //VectorCopy(target, vec_to); + //origin + + // Test if we fit inside target area + tr = gi.trace(target, tv(-16, -16, -32), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) // We fit + return NODE_JUMP; + //return NODE_JUMP; + } + else + return NODE_JUMP; + } + } + */ + + + + // Run up/down slopes and ramps + if (xy_distance < 512 && !is_gap) + { + // If we have LoS + tr = gi.trace(origin, tv(-16, -16, -14), tv(16, 16, 24), target, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + // Test for slope / stairs at the midpoint + trace_t tr_50; + vec3_t pt_50, end_50; + LerpVector(origin, target, 0.50, pt_50); // Get the two vector midpoint + VectorCopy(pt_50, end_50); + end_50[2] -= NODE_Z_HEIGHT_PLUS_STEPSIZE; + tr_50 = gi.trace(pt_50, NULL, NULL, end_50, g_edicts, MASK_DEADSOLID); + if (tr_50.fraction < 1.0) // We hit solid + { + // Slope + if (tr_50.plane.normal[0] != 0 || tr_50.plane.normal[1] != 0) + return NODE_MOVE; + } + } + } + + + return INVALID; +} + +// Check to see if triangles are coplanar +// Check to see if the triangles are adjacent +//qboolean TrianglesAreCoplanar(vec3_t t1_p1, vec3_t t1_p2, vec3_t t1_p3, vec3_t t2_p1, vec3_t t2_p2, vec3_t t2_p3) + +// Explodes all breakable glass :D +void Remove_All_Breakableglass() +{ + edict_t* glass; + for (glass = g_edicts; glass < &g_edicts[globals.num_edicts]; glass++) + { + if (!glass) + continue; + //if (glass->solid == SOLID_NOT) + // continue; + if (!glass->classname) + continue; + + if (strcmp(glass->classname, "breakableglass_trigger") == 0 || strcmp(glass->classname, "func_explosive") == 0) + { + //gi.dprintf("Found glass at location %f %f %f\n", glass->s.origin[0], glass->s.origin[1], glass->s.origin[2]); + //glass->takedamage = DAMAGE_YES; + //glass->die = kill_door; + //glass->health = 0; + //glass->max_health = 0; + + // Explode the glass + T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } + + /* + // iterate over all func_explosives + while ((glass = G_Find(glass, FOFS(classname), "func_explosive")) != NULL) + { + // glass is broken if solid != SOLID_BSP + if (glass->solid != SOLID_BSP) + { + T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } + */ +} + +void kill_door(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, vec3_t point) +{ + G_FreeEdict(self); +} +// Removes all door types +void Remove_All_Doors() +{ + edict_t* door; + for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) + { + if (!door) + continue; + if (door->solid == SOLID_NOT) + continue; + if (!door->classname) + continue; + + if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) + { + //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); + door->takedamage = DAMAGE_YES; + door->die = kill_door; + door->health = 0; + door->max_health = 0; + + // Open up the door's area portal (otherwise players will see hall-of-mirrors effect + edict_t* t = NULL; + if (door->target) + { + while ((t = G_Find(t, FOFS(targetname), door->target)) != NULL) + { + if (Q_stricmp(t->classname, "func_areaportal") == 0) { + gi.SetAreaPortalState(t->style, true); + } + } + } + + // Explode the door + T_Damage(door, door, door, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } +} + +void Open_All_Doors(edict_t* ent) +{ + if (ent != NULL) + { + if (ent->last_door_time > level.framenum - 2.0 * HZ) + return; + else + ent->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! + } + // Open all doors and keep them open + edict_t* door; + for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) + { + if (!door) + continue; + if (door->solid == SOLID_NOT) + continue; + if (!door->classname) + continue; + + if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) + { + //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); + + int DOOR_TOGGLE = 32; //#define DOOR_TOGGLE 32 + if ((door->spawnflags & DOOR_TOGGLE) == 0) + door->spawnflags |= DOOR_TOGGLE; // Bitwise add DOOR_TOGGLE to door->spawnflags + door->wait = -1; + door->speed = 9000; + door->touch_debounce_framenum = 0; + door->moveinfo.accel = 9000; + door->moveinfo.decel = 9000; + door->moveinfo.speed = 9000; + //door_use(door, ent, ent); // Open the door + } + } +} + +// Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER +#define MAX_LADDER_FACES 3072 +vec3_t surface_ladder_faces[MAX_LADDER_FACES] = { 0 }; // Origin +void ACEND_FindEdgeLadders(void) +{ + int f, e, f2, f3, i; + trace_t tr; + vec3_t zero = { 0 }; + // Player when standing + vec3_t player_standing_mins_up = { -16, -16, -1 }; + //vec3_t player_standing_mins = { -16, -16, -0 }; + vec3_t player_standing_maxs = { 16, 16, 32 }; + + + //vec3_t *ladder_faces = (vec3_t*)malloc(sizeof(vec3_t) * MAX_LADDER_FACES); + //if (ladder_faces == NULL) + { + //Com_Printf("%s [CONTENTS_LADDER] ladder_faces malloc failed\n", __func__); + //return; + } + //vec3_t* ladder_face; + + //short int ladder_index[MAX_LADDER_FACES] = { 0 }; // Face + int* ladder_index = malloc(sizeof(int) * MAX_LADDER_FACES); + if (ladder_index == NULL) + { + Com_Printf("%s [CONTENTS_LADDER] ladder_index malloc failed\n", __func__); + return; + } + + + int num_ladder_faces = 0; + qboolean foundLadder = true; + qboolean ladderLimit = false; + + /* + for (f = 0; f < nmesh.total_faces; f++) + { + if (nmesh.face[f].contents & CONTENTS_LADDER) + { + Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + } + } + */ + + // Find all the ladder faces + for (f = 0; f < nmesh.total_faces; f++) + { + if (ladderLimit) break; + + //if (nmesh.face[f].contents & CONTENTS_LADDER) + // Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + + if (nmesh.face[f].type == FACETYPE_WALL && nmesh.face[f].contents & CONTENTS_LADDER) // If wall is Ladder + { + for (e = 0; e < nmesh.face[f].num_edges; e++) // Find the bottom of the ladder using the middle of the lowest edge + { + // Look for horizontal edges - check if the two edge verts are level + if (nmesh.face[f].edge[e].v[0][2] == nmesh.face[f].edge[e].v[1][2]) + { + // Check if we already added the ladder face to ladder_faces[] + foundLadder = true; + for (f3 = 0; f3 < num_ladder_faces; f3++) + //for (f3 = 0, ladder_face = ladder_faces; f3 < num_ladder_faces; f3++, ladder_face++) + { + if (surface_ladder_faces[f3][0] == nmesh.face[f].edge[e].center[0] && surface_ladder_faces[f3][1] == nmesh.face[f].edge[e].center[1] && surface_ladder_faces[f3][2] == nmesh.face[f].edge[e].center[2]) + //if (*(ladder_face)[0] == nmesh.face[f].edge[e].center[0] && *(ladder_face)[1] == nmesh.face[f].edge[e].center[1] && *(ladder_face)[2] == nmesh.face[f].edge[e].center[2]) + { + //Com_Printf("%s f:%i e:%i\n", __func__, f, e); + foundLadder = false; + break; + } + } + + // Found new ladder edge + if (foundLadder) + { + // Check we're not over the limit + if (num_ladder_faces + 2 > MAX_LADDER_FACES) + { + Com_Printf("%s [CONTENTS_LADDER] num_ladder_faces > max_faces\n", __func__); + ladderLimit = true; + break; + } + + //Com_Printf("%s [CONTENTS_LADDER] f:%i e:%d n:%d [%f %f %f]\n", __func__, f, e, nmesh.face[f].edge[e].node, nmesh.face[f].edge[e].center[0], nmesh.face[f].edge[e].center[1], nmesh.face[f].edge[e].center[2]); + + //ladder_index[num_ladder_faces] = f; // Copy face + VectorCopy(nmesh.face[f].edge[e].center, surface_ladder_faces[num_ladder_faces]); // Copy origin + + *(ladder_index + num_ladder_faces) = f; // Copy face + + //Com_Printf("%s ladder_index:%i f:%i\n", __func__, *(ladder_index + num_ladder_faces), f); + + //for (int i = 0; i < 3; i++) + // *(ladder_faces + num_ladder_faces * sizeof(float))[i] = nmesh.face[f].edge[e].center[i]; // Copy origin + // + //ladder_face = ladder_faces + (num_ladder_faces * sizeof(vec3_t)); // Set pointer to end of array + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[0] = nmesh.face[f].edge[e].center[0]; + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[1] = nmesh.face[f].edge[e].center[1]; + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[2] = nmesh.face[f].edge[e].center[2]; + + num_ladder_faces++; + } + } + } + } + } + + + + + for (f = 0; f < num_ladder_faces; f++) + //for (f = 0, ladder_face = ladder_faces; f < num_ladder_faces; f++, ladder_face++) + { + // Com_printf ladder_faces + //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, ladder_index[f], ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2]); + //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, *(ladder_index + f), *(ladder_face)[0], *(ladder_face)[1], *(ladder_face)[2]); + } + + + + + + // Search all ladder faces looking for the lowest and highest parts of the ladder + // + // |-| <-- highest + // |-| + // |-| <-- lowest + // + qboolean tested = false; + int faces_tested_num = 0; + //short int faces_tested[MAX_LADDER_FACES]; + int* faces_tested = malloc(sizeof(int) * MAX_LADDER_FACES); + if (faces_tested == NULL) + { + Com_Printf("%s [CONTENTS_LADDER] faces_tested malloc failed\n", __func__); + return; + } + + + //vec3_t ladder_final[MAX_LADDER_FACES]; // Origin + //int ladder_final_counter = 0; + float low_z, high_z; // Height of lowest and highest parts of ladder + int low_f, high_f; // Face index of lowest and highest parts of ladder + for (f = 0; f < num_ladder_faces; f++) + { + low_z = 99999, high_z = -99999; // Reset lowest and highest height + low_f = high_f = INVALID; // Reset lowest and highest face index + + tested = false; + for (int ft = 0; ft < faces_tested_num; ft++) + { + //if (faces_tested[ft] == f) + if (*(faces_tested + ft) == f) + { + tested = true; // Skip if already tested + break; + } + } + if (tested) + continue; + + for (f2 = 0; f2 < num_ladder_faces; f2++) + { + // Check if the next ladder has the same X and Y alignment + if (surface_ladder_faces[f][0] == surface_ladder_faces[f2][0] && surface_ladder_faces[f][1] == surface_ladder_faces[f2][1]) + //if (*(ladder_faces + f)[0] == *(ladder_faces + f2)[0] && *(ladder_faces + f)[1] == *(ladder_faces + f2)[1]) + { + //Com_Printf("%s [F %i] [F %i] [%f %f %f] [%f %f %f] \n", __func__, f, f2, ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2], ladder_faces[f2][0], ladder_faces[f2][1], ladder_faces[f2][2]); + + //faces_tested[faces_tested_num++] = f2; // Add face to faces_tested + *(faces_tested + faces_tested_num++) = f2; // Add face to faces_tested + + + + if (surface_ladder_faces[f2][2] < low_z) + //if (*(ladder_faces + f2)[2] < low_z) + { + //Com_Printf("%s [F %i] [ladder_faces[f2][2] < low_z] %f < %f \n", __func__, f2, ladder_faces[f2][2], low_z); + low_z = surface_ladder_faces[f2][2]; // Update lowest height + //low_z = *(ladder_faces + f2)[2]; // Update lowest height + low_f = f2; // Update lowest face index + } + if (surface_ladder_faces[f2][2] > high_z) + //if (*(ladder_faces + f2)[2] > high_z) + { + //Com_Printf("%s [F %i] [ladder_faces[f2][2] > high_z] %f > %f \n", __func__, f2, ladder_faces[f2][2], high_z); + high_z = surface_ladder_faces[f2][2]; // Update highest height + //high_z = *(ladder_faces + f2)[2]; // Update highest height + high_f = f2; // Update highest face index + } + } + } + + foundLadder = false; + + float distance = 0; + if (surface_ladder_faces[high_f][2] > surface_ladder_faces[low_f][2]) // Make sure the top of the ladder is higher than the bottom + distance = surface_ladder_faces[high_f][2] - surface_ladder_faces[low_f][2]; + + if (distance >= 32 && low_f != INVALID && high_f != INVALID) + { + //Com_Printf("%s LO F:%i L:%f [F %i][%f %f %f]\n", __func__, low_f, low_z, ladder_index[low_f], ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + //Com_Printf("%s HI F:%i H:%f [F %i][%f %f %f]\n", __func__, high_f, high_z, ladder_index[high_f], ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); + + + if (1) // Project the top of the ladder forward, then we test to see if its in the ground, if so we need to move the top node up + { + vec3_t ladder_forward; + VectorCopy(surface_ladder_faces[high_f], ladder_forward); + //ladder_forward[2] += NODE_Z_HEIGHT; + // Move the top ladder node closer to the ladder + int f3 = *(ladder_index + high_f); + //Com_Printf("%s f3:%i\n", __func__, f3); + if (nmesh.face[f3].normal[0] != 0) // X + { + if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) + ladder_forward[0] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // -X (forward wall) + else + ladder_forward[0] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // +X (back wall) + } + if (nmesh.face[f3].normal[1] != 0) // Y + { + if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) + ladder_forward[1] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // -Y (left wall) + else + ladder_forward[1] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // +Y (right wall) + } + // Test if we're in the ground + for (i = 0; i < 100; i++) //NODE_Z_HEIGHT + { + tr = gi.trace(ladder_forward, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), ladder_forward, g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1) + { + ladder_forward[2]++; + } + else + break; + } + + //Com_Printf("%s Increased top of ladder [%f %f %f] -> [%f %f %f]\n", __func__, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2], ladder_forward[0], ladder_forward[1], ladder_forward[2]); + surface_ladder_faces[high_f][2] = ladder_forward[2]; + } + + // Move the top of the ladder up before testing, this serves two purposes: test if ladder hits roof, make sure ladder reaches top + //ladder_faces[high_f][2] += NODE_Z_HEIGHT + 8; + //ladder_faces[high_f][2] += NODE_Z_HEIGHT; + + if (1) + // Test if bottom of ladder is in the ground, move it up until its not + for (i = 0; i < 100; i++) //NODE_Z_HEIGHT + { + tr = gi.trace(surface_ladder_faces[low_f], tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), surface_ladder_faces[low_f], g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1) + { + if (surface_ladder_faces[low_f][2] + 1 < surface_ladder_faces[high_f][2]) + surface_ladder_faces[low_f][2]++; + } + else + break; + } + + //if (ladder_faces[low_f][2] + NODE_Z_HEIGHT < ladder_faces[high_f][2]) // Make sure bottom ladder doesn't go above top of ladder + // ladder_faces[low_f][2] += NODE_Z_HEIGHT; + + + + + + + // Check if ladder ends can be connected, if so add nodes and links + //tr = gi.trace(ladder_faces[low_f], player_standing_mins_up, player_standing_maxs, ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); + tr = gi.trace(surface_ladder_faces[low_f], MINS_PLAYER_STANDING, MAXS_PLAYER_STANDING, surface_ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + { + //ACEND_AutoAddNode(*(ladder_faces + low_f), nmesh.face[low_f].normal, NODE_LADDER_DOWN); + //ACEND_AutoAddNode(*(ladder_faces + high_f), nmesh.face[high_f].normal, NODE_LADDER_UP); + + int tn = INVALID; // Top node + int bn = INVALID; // Bottom node + + // Add Top ladder node + // ---------------------- + // Top ladder doesn't have to worry about existing nodes, so just make a new one + DAIC_Add_Node(surface_ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); + //ACEND_AutoAddNode(ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); + tn = numnodes - 1; + // ---------------------- + + // Add bottom ladder node + // ---------------------- + // Look for an existing node located on the triangle where the bottom of the ladder is, + // if so then move that node origin to the bottom ladder origin + qboolean found = false; + int f = INVALID; + int t = INVALID; + //vec3_t ladder_low; + //VectorCopy(ladder_faces[low_f], ladder_low); + //ladder_low[2] -= 24; + //GetBSPFaceTriangle(ladder_low, &found, &f, &t); + GetBSPFaceTriangle(surface_ladder_faces[low_f], &found, &f, &t); + if (found && f != INVALID && t != INVALID) //f = 0; t = 0; found = true; + { + found = false; + bn = nmesh.face[f].tris[t].node; + if (bn != INVALID) // Successfully found node + { + //Com_Printf("%s ladder tris found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + + found = true; + VectorCopy(surface_ladder_faces[low_f], nodes[bn].origin); // Move node origin to bottom ladder origin + nodes[bn].type = NODE_LADDER_DOWN; // Update its type + } + } + // Otherwise we add a new node for the bottom of the ladder + if (found == false) + { + //Com_Printf("%s ladder tris not found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + DAIC_Add_Node(surface_ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); + //ACEND_AutoAddNode(ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); + bn = numnodes - 1; + } + // ---------------------- + + //Com_Printf("%s NODE_LADDER_DOWN f:%i n:%d [%f %f %f]\n", __func__, ladder_index[low_f], bn, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + //Com_Printf("%s NODE_LADDER_UP f:%i n:%d [%f %f %f]\n", __func__, ladder_index[high_f], tn, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); + + if (bn != INVALID && tn != INVALID) + { + BOTLIB_AddNodeLink(bn, tn, NODE_LADDER_DOWN, true); // Bottom to top + BOTLIB_AddNodeLink(tn, bn, NODE_LADDER_UP, true); // Top to bottom + ACEND_UpdateNodeReach(bn, tn); + ACEND_UpdateNodeReach(tn, bn); + nmesh.total_reach += 2; + } + } + } + } + + // Free memory + free(faces_tested); + faces_tested = NULL; +} + +/* +// Find if origin is touching a ladder +// Can be used to see if node is touching a ladder +qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top) +{ + float yaw_rad = 0, yaw = 0, depth; + int loops; + vec3_t fwd = { 0 }, start = { 0 }, end = { 0 }, final = { 0 }, ground = { 0 }; + vec3_t mins = { -4, -4, -24 }, maxs = { 4, 4, 32 }; + trace_t tr; + qboolean is_touching = false, found_bottom = false; + VectorCopy(origin, start); + + // Player when standing + vec3_t player_standing_mins = { -16, -16, -0 }; + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing smaller mins + vec3_t player_standing_smaller_mins = { -8, -8, -0 }; + vec3_t player_standing_smaller_maxs = { 8, 8, 0 }; + + return false; //////////////////////////////////////////////////////////////////////////////////////////// TEMP DISABLE LADDERS + + depth = 0; + for (int d = 0; d < 128; d++) // depth + { + // For each yaw angle, project forward and see if touching a ladder + for (int a = 0; a < 64; a++) // Spin around in a 360 circle + { + if (yaw >= 360) // Make sure we come back around + yaw = 0; + + yaw_rad = DEG2RAD(yaw); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + VectorMA(start, depth, fwd, end); + VectorMA(start, (depth - 20), fwd, final); // Back away from the ladder until we're about the distance of the player center (give or take a little) + tr = gi.trace(start, mins, maxs, end, g_edicts, MASK_PLAYERSOLID); + //if (tr.fraction < 1 && (tr.contents & CONTENTS_LADDER)) + if (tr.contents & CONTENTS_LADDER) + { + // Find the bottom of the ladder + //-------------------- + // Now find the ground so we know not to go below this point + VectorCopy(end, ground); + ground[2] -= 8192; + tr = gi.trace(ladder_top, NULL, NULL, ground, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction < 1) + { + VectorCopy(tr.endpos, ground); + } + loops = 0; + while (loops++ < 64) + { + // Now move down testing until we no longer hit the ladder to find its bottom + if (end[2] + 32 > ground[2]) // Can we move down? + { + // Okay move down + start[2] -= 32; + end[2] -= 32; + final[2] -= 32; + + tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) == 0) + { + // Ladder reached the ground so adjust the position so we're not stuck in the ground + if (tr.startsolid) + { + start[2] = tr.endpos[2] + NODE_Z_HEIGHT; + end[2] = tr.endpos[2] + NODE_Z_HEIGHT; + final[2] = tr.endpos[2] + NODE_Z_HEIGHT; + } + else + { + // Hanging ladder, didn't reach the ground + } + VectorCopy(final, ladder_bottom); + Com_Printf("Found bottom ladder at location %f %f %f\n", ladder_bottom[0], ladder_bottom[1], ladder_bottom[2]); + found_bottom = true; + break; // end while loop + } + } + } + //-------------------- + + + // Find the top of the ladder + //-------------------- + if (found_bottom) + { + // Now move up testing until we no longer hit the ladder to find its top + loops = 0; + while (loops++ < 64) + { + // move up + start[2] += 32; + end[2] += 32; + final[2] += 32; + + tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) == 0) + { + final[2] += NODE_Z_HEIGHT; // Adjust the final height above the top of the ladder + VectorCopy(final, ladder_top); // Ladder top + + // Make sure the ladder z height length is large enough + // edge cases: where ladder handles go above the ground we're trying to reach, such as urban2 the very highest ladder + float difference = ladder_top[2] - ladder_bottom[2]; + if (difference > 96) + { + Com_Printf("Found top ladder at location %f %f %f\n", ladder_top[0], ladder_top[1], ladder_top[2]); + is_touching = true; + return is_touching; + } + } + } + } + //-------------------- + + if (is_touching) + break; + } + yaw += 5.625; // Change yaw angle + } + depth += 2; + } + + return is_touching; +} +*/ + +//rekkie -- walknodes -- s + + +void BOTLIB_SetAllNodeNormals() +{ + vec3_t mins = { -14, -14, -4, }; + vec3_t maxs = { 14, 14, 4, }; + + for (int i = 0; i < numnodes; i++) + { + // Get normal + trace_t tr = gi.trace(tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] + 30), mins, maxs, tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] - 64), NULL, MASK_SHOT); + if (tr.fraction == 1.0) + { + //Com_Printf("%s failed node[%d] normal [%f %f %f]\n", __func__, nodes[i].nodenum, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); + continue; + } + + VectorCopy(tr.plane.normal, nodes[i].normal); + } +} + +// Compression -- s +#if USE_ZLIB +#include "..\..\extern\zlib\zlib.h" +//#include +//#include +//#include + +#define CHUNK_SIZE 16384 + +void BOTLIB_DecompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) +{ + z_stream stream = { 0 }; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + int status = inflateInit(&stream); + if (status != Z_OK) { + Com_Printf("Failed to initialize decompression stream! (error %d)\n", status); + return; + } + + stream.avail_in = inputLength; + stream.next_in = (Bytef*)inputBuffer; + stream.avail_out = *outputLength; + stream.next_out = (Bytef*)outputBuffer; + + status = inflate(&stream, Z_FINISH); + if ((status != Z_OK) && (status != Z_STREAM_END)) { + Com_Printf("Failed to decompress data! (error %d)\n", status); + inflateEnd(&stream); + return; + } + + *outputLength = stream.total_out; + inflateEnd(&stream); +} +void BOTLIB_CompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) { + z_stream stream = { 0 }; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + + int status = deflateInit(&stream, Z_BEST_COMPRESSION); + if (status != Z_OK) { + Com_Printf("Failed to initialize compression stream! (error %d)\n", status); + return; + } + + stream.avail_in = inputLength; + stream.next_in = (Bytef*)inputBuffer; + stream.avail_out = inputLength; // *outputLength; + stream.next_out = (Bytef*)outputBuffer; + + status = deflate(&stream, Z_FINISH); + if ((status != Z_OK) && (status != Z_STREAM_END)) { + Com_Printf("Failed to compress data! (error %d)\n", status); + deflateEnd(&stream); + return; + } + + *outputLength = stream.total_out; + + deflateEnd(&stream); +} + +// Assume the following variables are declared on the other end: +// char* buffer; // Pointer to the outgoing buffer to add data to +// int bufferLength; // Length of the outgoing buffer +// int dataLength; // Length of the incoming data +// char* incomingData; // Pointer to the incoming data to add +void BOTLIB_AddToBuffer(char* incomingData, int dataLength, char** bufferPtr, int* bufferLengthPtr) +{ + if (*bufferPtr == NULL) + { // If the buffer is null, set the buffer to the incoming data ) + char *newBuffer = (char*)malloc(*bufferLengthPtr + dataLength); + if (newBuffer != NULL) + { // If the allocation was successful, copy the incoming data to the end of the buffer + memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); + *bufferPtr = newBuffer; + *bufferLengthPtr += dataLength; + } else { // If the allocation was not successful, handle the error + Com_Printf("Failed to allocate buffer for incoming data!\n"); + } + } + else + { + // If the buffer is not null, reallocate the buffer to the new size + char *newBuffer = (char*)realloc(*bufferPtr, *bufferLengthPtr + dataLength); + if (newBuffer != NULL) + { // If the allocation was successful, copy the incoming data to the end of the buffer + memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); + *bufferPtr = newBuffer; + *bufferLengthPtr += dataLength; + } else { // If the allocation was not successful, handle the error + Com_Printf("Failed to allocate buffer for incoming data!\n"); + } + } +} + + +// ============================================================================================ +// These group of functions sort the node links from highest to lowest based on their zheight +// difference from the node they're connected to. node -> link(s) zheight difference. We run +// this function upon saving nodes. +// +// This is done so that when pathing the highest node links are consindered first. Although, +// currently the bots use Dijkstra Pathing, so this is ignored for now. +// ============================================================================================ +// Temp link structure +typedef struct { + int targetNode; + int targetNodeType; + double cost; + double zheight; // Not part of the original link struct. Used by qsort() to order highest -> lowest +} QSLink; // Qsorted Link + +// Compare function for qsort() +static int QScompare(const void* a, const void* b) { + return ((QSLink*)b)->zheight - ((QSLink*)a)->zheight; +} + +void QsortLinks(QSLink* links, size_t length) { + qsort(links, length, sizeof(*links), QScompare); +} + +// Sorts node links by height. Highest to lowest. +void QSNodeLinksByHeight() +{ + QSLink qslinks[MAXLINKS]; // Temp links + + for (int n = 0; n < numnodes; n++) // Each node + { + for (int i = 0; i < nodes[n].num_links; ++i) // Each link + { + // Copy original link data into our temp struct + qslinks[i].targetNode = nodes[n].links[i].targetNode; + qslinks[i].targetNodeType = nodes[n].links[i].targetNodeType; + qslinks[i].cost = nodes[n].links[i].cost; + + // The original data struct doesn't use or contain the zheight of a linked node, so we create it here for qsort() to use, + int tnode = nodes[n].links[i].targetNode; // Target node + // + if ((nodes[tnode].origin[2] - nodes[n].origin[2]) >= 1) // Only sort linked nodes that are higher than the node itself and are >= 1 in height difference + qslinks[i].zheight = (nodes[tnode].origin[2] - nodes[n].origin[2]); + else + qslinks[i].zheight = 0; // Any height < 1 just consider as 0 height + } + + // Debug print + /* + qboolean hasDiffHeight = false; + for (int i = 0; i < nodes[n].num_links; ++i) + { + if (nodes[n].links[i].zheight >= 1) // Only if height diff is >= 1 + { + hasDiffHeight = true; + break; + } + } + */ + + // Debug print + /* + if (0 && hasDiffHeight) + { + Com_Printf("\nBefore sorting:\n"); + for (int i = 0; i < nodes[n].num_links; ++i) + Com_Printf("{%d %d %f %f}\n", nodes[n].links[i].targetNode, nodes[n].links[i].targetNodeType, nodes[n].links[i].cost, nodes[n].links[i].zheight); + } + */ + + // Sort links from highest to lowest + QsortLinks(qslinks, nodes[n].num_links); + + // Debug print + /* + if (0 && hasDiffHeight) // Debug print + { + Com_Printf("\nAfter sorting:\n"); + for (int i = 0; i < nodes[n].num_links; ++i) + Com_Printf("{%d %d %f %f}\n", qslinks[i].targetNode, qslinks[i].targetNodeType, qslinks[i].cost, qslinks[i].zheight); + } + */ + + // Override and store the original link data + for (int i = 0; i < nodes[n].num_links; ++i) + { + nodes[n].links[i].targetNode = qslinks[i].targetNode; + nodes[n].links[i].targetNodeType = qslinks[i].targetNodeType; + nodes[n].links[i].cost = qslinks[i].cost; + } + } +} +// ============================================== +// ============================================== + +void BOTLIB_SaveNavCompressed(void) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, n, l; // File, nodes, links + int version = BOT_NAV_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing compressed NAV to file...\n", __func__); + + QSNodeLinksByHeight(); // Sort node links by height, highest to lowest + + char* buff = NULL; + int uncompressed_buff_len = 0; + BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &uncompressed_buff_len); + for (n = 0; n < numnodes; n++) + { + // Nodes + BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &uncompressed_buff_len); + //BOTLIB_AddToBuffer((char*)&nodes[n].normal, sizeof(vec3_t), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &uncompressed_buff_len); + + // Links + BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &uncompressed_buff_len); + for (l = 0; l < nodes[n].num_links; l++) + { + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &uncompressed_buff_len); + } + + // Paths + //for (p = 0; p < numnodes; p++) + //{ + //BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &uncompressed_buff_len); // Path table + //} + } + + // Compress the buffer + int compressed_buff_len = 0; + char* compressed_buff = (char*)malloc(uncompressed_buff_len); // Make the compressed buffer as large as the uncompressed buffer + if (compressed_buff != NULL) + { + BOTLIB_CompressBuffer(buff, uncompressed_buff_len, compressed_buff, &compressed_buff_len); + + // Compare and print the size of the buffers + //Com_Printf("%s uncompressed_buff_len:%i compressed_buff_len:%i\n", __func__, uncompressed_buff_len, compressed_buff_len); + + // Write version, checksum, and buffer sizes + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // NAV Version + //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + fileSize += sizeof(int) * fwrite(&uncompressed_buff_len, sizeof(int), 1, fOut); // Uncompressed buffer size + fileSize += sizeof(int) * fwrite(&compressed_buff_len, sizeof(int), 1, fOut); // Compressed buffer size + + // Write compressed node data + fwrite(compressed_buff, compressed_buff_len, 1, fOut); + fileSize += compressed_buff_len; + } + else + { + Com_Printf("%s Failed to allocate memory for compressed nav buffer\n", __func__); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + fclose(fOut); + + if (buff != NULL) + free(buff); + + if (compressed_buff != NULL) + free(compressed_buff); + + Com_Printf("%s saved %s containing %d nodes. Nav zlib compressed [%i bytes] to [%i bytes]\n", __func__, filename, numnodes, uncompressed_buff_len, fileSize); +} +void BOTLIB_LoadNavCompressed(void) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + int f, n, l; // File, nodes, links + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + const vec3_t mins = { -16, -16, -24 }; + const vec3_t maxs = { 16, 16, 32 }; + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists + { + return; // No file + } + else + { + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version < BOT_NAV_VERSION_1 || version > BOT_NAV_VERSION) + { + Com_Printf("%s ERROR: NAV file version mismatch. Got %d, expected value between %d and %d\n", __func__, version, BOT_NAV_VERSION_1, BOT_NAV_VERSION); + fclose(fIn); // Close the file + return; + } + /* + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + fclose(fIn); // Close the file + return; + } + */ + } + + // Init Nodes + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + //memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + Remove_All_Breakableglass(); + + Com_Printf("%s Reading NAV file... detected version [%d]\n", __func__, version); + + // Read Compressed and uncompressed buffer sizes + int uncompressed_buff_len = 0; + fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size + int compressed_buff_len = 0; + fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size + + // Read compressed buffer + char* uncompressed_buff = (char*)malloc(uncompressed_buff_len); + char* compressed_buff = (char*)malloc(compressed_buff_len); + if (compressed_buff != NULL && uncompressed_buff != NULL) + { + fileSize += compressed_buff_len * fread(compressed_buff, compressed_buff_len, 1, fIn); // Compressed buffer + + BOTLIB_DecompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); + //Com_Printf("%s compressed_buff_len:%i uncompressed_buff_len:%i\n", __func__, compressed_buff_len, uncompressed_buff_len); + + // Read the uncompressed buffer + { + int buff_read = 0; + + // Total nodes + memcpy(&numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); + return; + } + + // Alloc nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + // Total areas + //nav_area.total_areas = 0; + + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, version, numnodes); + + // Read node data + for (n = 0; n < numnodes; n++) + { + if (version > BOT_NAV_VERSION_1) + { + // Area/chunk/group this node belongs to -- optimise and diversify bot pathing + memcpy(&nodes[n].area, uncompressed_buff + buff_read, sizeof(int)); // Node area + buff_read += sizeof(int); + + nodes[n].area_color = 0; + } + //VectorClear(nodes[n].normal); + //if (version > BOT_NAV_VERSION_2) + //{ + //memcpy(&nodes[n].normal, uncompressed_buff + buff_read, sizeof(vec3_t)); // Surface normal + //buff_read += sizeof(vec3_t); + //} + + nodes[n].weight = 0; // Used to help diversify bot pathing + + memcpy(&nodes[n].origin, uncompressed_buff + buff_read, sizeof(vec3_t)); // Location + buff_read += sizeof(vec3_t); + memcpy(&nodes[n].type, uncompressed_buff + buff_read, sizeof(byte)); // Node type + buff_read += sizeof(byte); + + VectorCopy(mins, nodes[n].mins); // Box mins + VectorCopy(maxs, nodes[n].maxs); // Box maxs + if (nodes[n].type == NODE_CROUCH) + nodes[n].maxs[2] = CROUCHING_MAXS2; + else if (nodes[n].type == NODE_BOXJUMP) + { + nodes[n].mins[0] = -8; + nodes[n].mins[1] = -8; + nodes[n].mins[2] = -12; + nodes[n].maxs[0] = 8; + nodes[n].maxs[1] = 8; + nodes[n].maxs[2] = 16; + } + VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world + + memcpy(&nodes[n].nodenum, uncompressed_buff + buff_read, sizeof(short int)); // Node number + buff_read += sizeof(short int); + memcpy(&nodes[n].inuse, uncompressed_buff + buff_read, sizeof(qboolean)); // Node inuse + buff_read += sizeof(qboolean); + + // Init MAXLINKS links + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Read the num_links + memcpy(&nodes[n].num_links, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + //Com_Printf("%s origin:%f %f %f type:%i nodenum:%i inuse:%i num_links:%i\n", __func__, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2], nodes[n].type, nodes[n].nodenum, nodes[n].inuse, nodes[n].num_links); + + for (l = 0; l < nodes[n].num_links; l++) // + { + memcpy(&nodes[n].links[l].targetNode, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + memcpy(&nodes[n].links[l].targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + memcpy(&nodes[n].links[l].cost, uncompressed_buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + //Com_Printf("%s targetNode:%i targetNodeType:%i cost:%f\n", __func__, nodes[n].links[l].targetNode, nodes[n].links[l].targetNodeType, nodes[n].links[l].cost); + } + + // Paths + //for (p = 0; p < numnodes; p++) + //{ + //memcpy(&path_table[n][p], uncompressed_buff + buff_read, sizeof(short int)); // Path table + //buff_read += sizeof(short int); + //Com_Printf("%s path_table[%i][%i]:%i\n", __func__, n, p, path_table[n][p]); + //} + } + } + } + + // Free memory + if (compressed_buff != NULL) + free(compressed_buff); + if (uncompressed_buff != NULL) + free(uncompressed_buff); + + // Fix any dangling or unusual links + if (numnodes) + { + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node + + if (nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node + + /* + for (int l = 0; l < nodes[i].num_links; l++) + { + int tnode = nodes[i].links[l].targetNode; + + if (nodes[tnode].origin[0] == 0 && nodes[tnode].origin[1] == 0 && nodes[tnode].origin[2] == 0) + { + BOTLIB_RemoveAllNodeLinksFrom(nodes[tnode].nodenum); // Remove all links to and from this node + } + } + */ + } + } + + fclose(fIn); + + Com_Printf("%s loaded %s containing %d nodes.\n", __func__, filename, numnodes); + + // Update old versions to new format + if (version == BOT_NAV_VERSION_1) + { + for (int i = 0; i < numnodes; i++) + { + nodes[i].area = 0; + } + } + // Save changes to file + if (version < BOT_NAV_VERSION) + { + Com_Printf("%s Updating NAV file to new version [%d]\n", __func__, BOT_NAV_VERSION); + BOTLIB_SaveNavCompressed(); + } + + //BOTLIB_SetAllNodeNormals(); // Set all the normals + + // Init Areas (must be done after nodes are loaded) + if (numnodes) + { + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + +} +#endif + +void BOTLIB_SaveNav(void) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, n, l, p; // File, nodes, links, paths + int version = BOT_NAV_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing NAV to file...\n", __func__); + +#if 0 +#ifdef USE_ZLIB + char* buff = NULL; + int buff_len = 0; + //BOTLIB_AddToBuffer((char*)&version, sizeof(int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &buff_len); + for (n = 0; n < numnodes; n++) + { + // Nodes + BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &buff_len); + + // Links + BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &buff_len); + for (l = 0; l < nodes[n].num_links; l++) // + { + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &buff_len); + } + + // Paths + for (p = 0; p < numnodes; p++) + { + BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &buff_len); // Path table + } + } + // Compress the buffer + int compressed_buff_len = 0; + char* compressed_buff = (char*)malloc(buff_len); + if (compressed_buff != NULL) + { + //memset(compressed_buff, 0, buff_len); + compressBuffer(buff, buff_len, compressed_buff, &compressed_buff_len); + // Compare and print the size of the buffers + Com_Printf("%s buff_len:%i compressed_buff_len:%i\n", __func__, buff_len, compressed_buff_len); + + int uncompressed_buff_len = buff_len; + char* uncompressed_buff = (char*)malloc(buff_len); + decompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); + Com_Printf("%s uncompressed_buff_len:%i\n", __func__, uncompressed_buff_len); + + if (uncompressed_buff != NULL) + { + // Read the uncompressed version + int buff_read = 0; + //int buff_version = 0; + //memcpy(&buff_version, uncompressed_buff, sizeof(int)); + //buff_read += sizeof(int); + // Read the buffer numnodes + unsigned short buff_numnodes = 0; + //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); + memcpy(&buff_numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); + for (n = 0; n < buff_numnodes; n++) + { + // Read the origin + vec3_t buff_origin = { 0 }; + memcpy(&buff_origin, uncompressed_buff + buff_read, sizeof(vec3_t)); + buff_read += sizeof(vec3_t); + // Read the type + byte buff_type = 0; + memcpy(&buff_type, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the nodenum + short int buff_nodenum = 0; + memcpy(&buff_nodenum, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the inuse + qboolean buff_inuse = false; + memcpy(&buff_inuse, uncompressed_buff + buff_read, sizeof(qboolean)); + buff_read += sizeof(qboolean); + // Read the num_links + byte buff_num_links = 0; + memcpy(&buff_num_links, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); + + for (l = 0; l < buff_num_links; l++) // + { + // Read the targetNode + short int buff_targetNode = 0; + memcpy(&buff_targetNode, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the targetNodeType + byte buff_targetNodeType = 0; + memcpy(&buff_targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the cost + float buff_cost = 0; + memcpy(&buff_cost, uncompressed_buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); + break; + } + for (p = 0; p < buff_numnodes; p++) + { + // Read the path_table + short int buff_path_table = 0; + memcpy(&buff_path_table, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); + break; + } + + break; + } + } + + + if (1) + { + // Read the buffer version + int buff_read = 0; + //int buff_version = 0; + //memcpy(&buff_version, buff, sizeof(int)); + //buff_read += sizeof(int); + // Read the buffer numnodes + unsigned short buff_numnodes = 0; + //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); + memcpy(&buff_numnodes, buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); + for (n = 0; n < buff_numnodes; n++) + { + // Read the origin + vec3_t buff_origin = { 0 }; + memcpy(&buff_origin, buff + buff_read, sizeof(vec3_t)); + buff_read += sizeof(vec3_t); + // Read the type + byte buff_type = 0; + memcpy(&buff_type, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the nodenum + short int buff_nodenum = 0; + memcpy(&buff_nodenum, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the inuse + qboolean buff_inuse = false; + memcpy(&buff_inuse, buff + buff_read, sizeof(qboolean)); + buff_read += sizeof(qboolean); + // Read the num_links + byte buff_num_links = 0; + memcpy(&buff_num_links, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); + + for (l = 0; l < buff_num_links; l++) // + { + // Read the targetNode + short int buff_targetNode = 0; + memcpy(&buff_targetNode, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the targetNodeType + byte buff_targetNodeType = 0; + memcpy(&buff_targetNodeType, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the cost + float buff_cost = 0; + memcpy(&buff_cost, buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); + break; + } + for (p = 0; p < buff_numnodes; p++) + { + // Read the path_table + short int buff_path_table = 0; + memcpy(&buff_path_table, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); + break; + } + + break; + } + } + + } + else + { + Com_Printf("%s Failed to allocate memory for compressed buffer\n", __func__); + } + + if (buff != NULL) + free(buff); + + if (compressed_buff != NULL) + free(compressed_buff); + +#endif +#endif + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version + //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + + // Write node data + fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Node origin + fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type + fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number + fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse + + // Links + fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); + for (l = 0; l < nodes[n].num_links; l++) // + { + fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node + fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type + fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table + } + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} + +void BOTLIB_LoadNav(void) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + int f, n, l, p; // File, nodes, links, paths + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + const vec3_t mins = { -16, -16, -24 }; + const vec3_t maxs = { 16, 16, 32 }; + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists + { + return; // No file + } + else + { + + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version != BOT_NAV_VERSION) + { + fclose(fIn); // Close the file + return; + } + /* + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + fclose(fIn); // Close the file + return; + } + */ + } + + // Init + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + //memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + Remove_All_Breakableglass(); + + Com_Printf("%s Reading NAV file...\n", __func__); + + fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); + return; + } + + // Main nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + // Read node data + for (n = 0; n < numnodes; n++) + { + nodes[n].weight = 0; // Used to help diversify bot pathing + + // Nodes + fileSize += sizeof(int) * fread(&nodes[n].area, sizeof(int), 1, fIn); // Area + fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type + + VectorClear(nodes[n].normal); // Surface normal + VectorCopy(mins, nodes[n].mins); // Box mins + VectorCopy(maxs, nodes[n].maxs); // Box maxs + if (nodes[n].type == NODE_CROUCH) + nodes[n].maxs[2] = CROUCHING_MAXS2; + else if (nodes[n].type == NODE_BOXJUMP) + { + nodes[n].mins[0] = -8; + nodes[n].mins[1] = -8; + nodes[n].mins[2] = -12; + nodes[n].maxs[0] = 8; + nodes[n].maxs[1] = 8; + nodes[n].maxs[2] = 16; + } + VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world + + nodes[n].weight = 0; // Used to help diversify bot pathing + + fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number + fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse + + // Init MAXLINKS links + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Copy num_links links + fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links + for (l = 0; l < nodes[n].num_links; l++) + { + fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node + fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type + fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table + } + } + + // Init Areas (must be done after nodes are loaded) + if (numnodes) + { + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); +} + +//rekkie -- walknodes -- e + +/////////////////////////////////////////////////////////////////////// +// Save to disk file +/////////////////////////////////////////////////////////////////////// +void ACEND_SaveAAS(qboolean update) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach + int n, l, p; // Nodes, links, paths + int version = BOT_AAS_VERSION; + cvar_t *game_dir = gi.cvar("game", "action", 0); + cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".aas"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".aas"); +#endif + + // If not updating, process from scratch + if (update == false) + { + // Proccess BSP data so we can save it to disk + ACEND_BSP(NULL); + } + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write AAS to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing AAS to file...\n", __func__); + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // AAS Version + fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_faces, sizeof(unsigned short), 1, fOut); // Total Faces + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_edges, sizeof(unsigned short), 1, fOut); // Total Edges + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_verts, sizeof(unsigned short), 1, fOut); // Total Verticies + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_tris, sizeof(unsigned short), 1, fOut); // Total Triangles + fileSize += sizeof(int) * fwrite(&nmesh.total_reach, sizeof(int), 1, fOut); // Total Reachabilities + fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes + + // Write BSP Data + for (f = 0; f < nmesh.total_faces; f++) + { + // Write face verts + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fOut); + for (v = 0; v < nmesh.face[f].num_verts; v++) + { + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fOut); + } + + // Write edges + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fOut); + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Write edge verts + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fOut); + + // Write edge center + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fOut); + + // Write edge node + fileSize += sizeof(short int) * fwrite(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fOut); + } + + // Write triangles + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fOut); + for (t = 0; t < nmesh.face[f].num_tris; t++) + { + // Write triangle verts + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fOut); + + // Write triangle center + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fOut); + + // Write triangle face + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fOut); + + // Write triangle node + fileSize += sizeof(short int) * fwrite(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fOut); + + // Write triangle reachabilities + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fOut); + for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) + { + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fOut); + } + } + + fileSize += sizeof(byte) * fwrite(&nmesh.face[f].type, sizeof(byte), 1, fOut); // Surface type + fileSize += sizeof(int) * fwrite(&nmesh.face[f].drawflags, sizeof(int), 1, fOut); // Surface flags + fileSize += sizeof(int) * fwrite(&nmesh.face[f].contents, sizeof(int), 1, fOut); // Surface contents + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].normal, sizeof(vec3_t), 1, fOut); // Surface normal + } + + + // Write node data + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Location + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].normal, sizeof(vec3_t), 1, fOut); // Surface normal + fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type + //fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number + fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse + + + // Links + fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + //for (l = 0; l < nodes[numnodes].num_links; l++) // Only include valid links + { + fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node + fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type + fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table + } + } + + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} + +/////////////////////////////////////////////////////////////////////// +// Load Area Awareness System (from file or generative) +// Parameter: force - generates a new AAS instead of loading from file +/////////////////////////////////////////////////////////////////////// +void ACEND_LoadAAS(qboolean force) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + qboolean generateAAS = false; + int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach + int n, l, p; // Nodes, links, paths + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t *game_dir = gi.cvar("game", "action", 0); + cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".aas"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".aas"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists + { + generateAAS = true; // Generate new AAS + } + else + { + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version != BOT_AAS_VERSION) + { + generateAAS = true; // Generate new AAS + fclose(fIn); // Close the file + } + + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + generateAAS = true; // Generate new AAS + fclose(fIn); // Close the file + } + + } + + if (force) + generateAAS = true; // Force: generate a new AAS instead of loading from file + + // Generate a new AAS? + if (generateAAS) + { + Com_Printf("%s Generating botnav. Please wait this might take a while\n", __func__); + ACEND_SaveAAS(false); + return; + } + + // Init + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + + Com_Printf("%s Reading AAS file...\n", __func__); + + fileSize += sizeof(unsigned short) * fread(&nmesh.total_faces, sizeof(unsigned short), 1, fIn); // Total Faces + fileSize += sizeof(unsigned short) * fread(&nmesh.total_edges, sizeof(unsigned short), 1, fIn); // Total Edges + fileSize += sizeof(unsigned short) * fread(&nmesh.total_verts, sizeof(unsigned short), 1, fIn); // Total Verticies + fileSize += sizeof(unsigned short) * fread(&nmesh.total_tris, sizeof(unsigned short), 1, fIn); // Total Triangles + fileSize += sizeof(int) * fread(&nmesh.total_reach, sizeof(int), 1, fIn); // Total Reachabilities + fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in AAS file\n", __func__); + return; + } + + // Read BSP Data + for (f = 0; f < nmesh.total_faces; f++) + { + // Read face verts + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fIn); + for (v = 0; v < nmesh.face[f].num_verts; v++) + { + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fIn); + } + + // Read edges + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fIn); + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Read edge verts + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fIn); + + // Read edge center + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fIn); + + // Read edge node + fileSize += sizeof(short int) * fread(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fIn); + } + + // Read triangles + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fIn); + for (t = 0; t < nmesh.face[f].num_tris; t++) + { + // Read triangle verts + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fIn); + + // Read triangle center + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fIn); + + // Read triangle face + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fIn); + + // Read triangle node + fileSize += sizeof(short int) * fread(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fIn); + + // Read triangle reachabilities + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fIn); + for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) + { + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fIn); + } + } + + fileSize += sizeof(byte) * fread(&nmesh.face[f].type, sizeof(byte), 1, fIn); // Surface type + fileSize += sizeof(int) * fread(&nmesh.face[f].drawflags, sizeof(int), 1, fIn); // Surface flags + fileSize += sizeof(int) * fread(&nmesh.face[f].contents, sizeof(int), 1, fIn); // Surface contents + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].normal, sizeof(vec3_t), 1, fIn); // Surface normal + } + + + // Main nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + /* + // Path table + path_table = (short int**)malloc(sizeof(short int*) * MAX_PNODES); + if (path_table == NULL) + { + Com_Printf("%s failed to malloc path_table[]\n", __func__); + return; + } + else + { + // Dynamically allocate memory of size MAX_PNODES for each row + for (int r = 0; r < numnodes; r++) + { + path_table[r] = (short int*)malloc(sizeof(short int) * MAX_PNODES); + if (path_table[r] == NULL) + { + Com_Printf("%s failed to malloc path_table[][]\n", __func__); + return; + } + } + } + + Com_Printf("%s allocing MBytes[%ld] for nodes\n", __func__, ((sizeof(node_t) * numnodes) / 1024000)); + Com_Printf("%s allocing MBytes[%ld] for path_table\n", __func__, ((sizeof(short int) * MAX_PNODES * MAX_PNODES) / 1024000)); + */ + + + // Read node data + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(vec3_t) * fread(&nodes[n].normal, sizeof(vec3_t), 1, fIn); // Surface normal + fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type + //fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number + fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse + nodes[n].nodenum = n; // Node number + + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Links + fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("%s node[%d] has more than MAXLINKS[%d] links\n", __func__, n, MAXLINKS); + //else Com_Printf("%s node[%d] has links[%d]\n", __func__, n, nodes[n].num_links); + //for (l = 0; l < nodes[numnodes].num_links; l++) // Include all links, even invalid ones + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + { + fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node + fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type + fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table + } + } + + + + /* + fread(&num_items, sizeof(int), 1, fIn); // read facts count + fread(nodes, sizeof(node_t), numnodes, fIn); + fread(item_table, sizeof(item_table_t), num_items, fIn); + // Raptor007: Do not trust saved pointers! + for (i = 0; i < MAX_EDICTS; i++) + item_table[i].ent = NULL; + */ + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); + + /* + // Try placing POI nodes on high locations + if (1) + { + float highest_origin = -9999; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > highest_origin) + highest_origin = nodes[i].origin[2]; + } + float above_height = (highest_origin - 256); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_MOVE && nodes[i].origin[2] > above_height && random() < 0.1) + { + nodes[i].type = NODE_POI; + } + } + + ACEND_CachePointOfInterestNodes(); + } + + ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies + */ +} + +// Find the reachability for each node +void BOTLIB_NODE_REACHABILITY(edict_t* ent) +{ + node_t* from_node; // From node + node_t* to_node; // To node + int s, result; + vec3_t zero = { 0 }; + //qboolean found_ladder; + for (s = 0; s < numnodes; s++) + { + if (nodes[s].inuse == false) continue; // Ignore nodes not in use + + //if (nodes[s].nodenum != INVALID) continue; + + // Get the next 'from' node + from_node = (unsorted_nodes + s); // From Node + + // Zero the link counter + from_node->num_links = 0; + + for (int o = 0; o < numnodes; o++) + { + if (nodes[o].inuse == false) continue; // Ignore nodes not in use + //if (nodes[o].nodenum != INVALID) continue; + + if (s == o) // skip self + continue; + + to_node = (unsorted_nodes + o); // Node A + + result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); + + //TEST CODE -- s + //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes + // result = INVALID; + //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) + // result = NODE_JUMPPAD; + //TEST CODE -- e + + if (result != INVALID) + { + for (int lk = 0; lk < MAXLINKS; lk++) + { + if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link + { + from_node->links[lk].targetNodeType = result; // Set target node type + + // Add link + //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); + //break; + + + //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes + if (BOTLIB_AddNodeLink(s, o, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + //from_node->num_links++; // Update number of links this node has + nmesh.total_reach++; + } + break; + } + } + } + } + } +} + +// Similar to gi.pointcontents, but returns the contents within the bounding box around the origin +// Useful when checking for multiple contents within the local area, specially when checking water +int BOTLIB_UTIL_BOXCONTENTS(vec3_t origin) +{ + trace_t tr = gi.trace(origin, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), origin, NULL, MASK_ALL); + return tr.contents; +} + +// Rotate the center of an edge around a center point, then extend the center point out by a distance +// +// (end) <--- desired result +// | +// | +// | <--- distance +// |\ +// | \ <-- rotated edge (90 degrees) +// | \ +// (pt1) -----+----- (pt2) +// (center) +// +// pt1 and pt2 are the start and end points of an edge +// float rotate the degrees of rotation +// float distance is the distance from the center point +// vec3_t end is the rotated end point +void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end) +{ + vec3_t center, angle, forward; // , right, offset; + + // print warning if distance is > 3000 + if (distance > 3000) + { + Com_Printf("%s WARNING: distance > 3000. Cannot rotate accurately.\n", __func__); + } + + // Find the middle between two points of an edge + LerpVector(pt1, pt2, 0.50, center); + + // Get direction vector + VectorSubtract(pt2, pt1, forward); + + // Normalize the direction vector + VectorNormalize(forward); + + // Get the angle of the direction vector + vectoangles(forward, angle); + + //Com_Printf("%s vectoangles()->angle[YAW] = %f\n", __func__, angle[YAW]); + + //if (direction == MOVE_LEFT) + // angles[1] += 90; + //else if (direction == MOVE_RIGHT) + // angles[1] -= 90; + //else if (direction == MOVE_BACK) + // angles[1] -= 180; + + // Rotation + //angle[PITCH] = 0; + angle[YAW] += rotate; // Rotation angle + //angle[ROLL] = 0; + + // Wrap around + //if (angle[YAW] >= 180) + // angle[YAW] = angle[YAW] - 360; + //else if (angle[YAW] <= -180) + // angle[YAW] = angle[YAW] + 360; + + //if (angle[YAW] > 360) + // angle[YAW] -= 360; + //else if (angle[YAW] < 0) + // angle[YAW] += 360; + + // Rotate the direction vector + AngleVectors(angle, forward, NULL, NULL); + + //VectorSet(offset, distance, 0, 0); + //G_ProjectSource(center, offset, forward, right, end); + + // Get the rotated end point and extend it out based on distance + VectorMA(center, distance, forward, end); + //VectorMA(center, distance, right, end); +} + +// Test for a 'trigger_hurt' entity +// Returns true if we're touching a 'trigger_hurt', otherwise false +qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin) +{ + trace_t tr; + + // Test point contents from -20 to -30 checking for lava or slime below the proposed origin + int contents; + for (int i = 0; i < 10; i++) + { + contents = gi.pointcontents(tv(origin[0], origin[1], origin[2] - (20+i) )); + if (contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + + // Trace above center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), g_edicts, MASK_ALL); + if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) + { + //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); + if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) + { + //Com_Printf("%s +SKIPPING %s\n", __func__, tr.ent->classname); + return true; + } + + if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + // Skip if we started inside of a solid + if (tr.startsolid) + { + /* + if (nmesh.face[f].drawflags) + { + if (nmesh.face[f].drawflags & SURF_SKY) + Com_Printf("%s START SOLID, drawflags: SURF_SKY\n", __func__, nmesh.face[f].drawflags); + else + Com_Printf("%s START SOLID, drawflags: %d\n", __func__, nmesh.face[f].drawflags); + } + */ + + return true; + } + // Trace below center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] - NODE_Z_HEIGHT), g_edicts, MASK_ALL); + if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) + { + //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); + if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) + { + //Com_Printf("%s -SKIPPING %s\n", __func__, tr.ent->classname); + return true; + } + if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + + return false; +} + +// Turn selective map entities into solids, so we can trace them +// Used for ents like 'trigger_hurt' so we can avoid placing nodes on those surfaces +//solid_t trigger_solid[MAX_EDICTS]; +// make trigger_solid a malloc +int BOTLIB_MakeEntsSolid(solid_t* trigger_solid) +{ + if (trigger_solid == NULL) + { + return -1; + } + + int edict_num = 1 + game.maxclients; // skip worldclass & players + //int edict_cur = edict_num; + int edict_cur = 0; + edict_t* map_ents; + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + if (map_ents->inuse == false) + { + //edict_cur++; + continue; + } + + trigger_solid[edict_cur] = map_ents->solid; // keep a copy of the solid state of each ent + /* + SOLID_NOT, // no interaction with other objects + SOLID_TRIGGER, // only touch when inside, after moving + SOLID_BBOX, // touch on edge + SOLID_BSP // bsp clip, touch on edge + */ + /* + if (map_ents->solid == SOLID_NOT) + Com_Printf("%s [%i] SOLID_NOT classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_TRIGGER) + Com_Printf("%s [%i] SOLID_TRIGGER classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_BBOX) + Com_Printf("%s [%i] SOLID_BBOX classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_BSP) + Com_Printf("%s [%i] SOLID_BSP classname: %s\n", __func__, edict_cur, map_ents->classname); + else + Com_Printf("%s [%i] SOLID_??? classname: %s\n", __func__, edict_cur, map_ents->classname); + */ + + + + //if (strlen(map_ents->classname) > 1) + // Com_Printf("%s %s found\n", __func__, map_ents->classname); + + if (strcmp(map_ents->classname, "trigger_hurt") == 0) + { + map_ents->solid = SOLID_BBOX; + gi.linkentity(map_ents); // Relink ent because solidity changed + //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); + } + + //if (strcmp(map_ents->classname, "func_rotating") == 0) + //{ + //map_ents->solid = SOLID_BBOX; + //gi.linkentity(map_ents); // Relink ent because solidity changed + //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); + //} + + edict_cur++; + } + return edict_cur; +} +void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid) +{ + if (trigger_solid == NULL) + { + Com_Printf("%s trigger_solid is NULL\n", __func__); + return; + } + // Restore solid state of each ent + int edict_cur = 0; + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + if (map_ents->inuse == false) + { + //edict_cur++; + continue; + } + + map_ents->solid = trigger_solid[edict_cur]; // Restore the original solid state + gi.linkentity(map_ents); // Relink ent because solidity changed + + edict_cur++; + } + if (trigger_solid) + free(trigger_solid); +} + +// Add nodes at item locations +void BOTLIB_AddItemNodes() +{ + // =========================== + // Add nodes at item locations + // =========================== + int foundItems = 0; // How many items were found + vec3_t item_origin = { 0,0,0 }; + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + trace_t tr; + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + // Find the ground under the item + // Some ents drop from the sky, so we need to trace down to the ground to find their landing spot (tr.endpos) + tr = gi.trace(map_ents->s.origin, NULL, NULL, tv(map_ents->s.origin[0], map_ents->s.origin[1], -4096), g_edicts, MASK_PLAYERSOLID); + + // See if the item will fit (player size) on the ground + // Some ents are placed on shelfs (urban2) so we cannot place a node on the shelf, so look for a spot to place the node nearby + tr = gi.trace(tr.endpos, tv(-12, -12, 0), tv(12, 12, 56), tr.endpos, g_edicts, MASK_PLAYERSOLID); + + VectorCopy(tr.endpos, item_origin); // Update the origin + + // If it won't fit, then look for a spot nearby + if (tr.startsolid || tr.fraction < 1.0) + { + float offset = 30; // How far to offset from the original item location (30 allows just enough room to grab the item, but not too far away) + // This number '30' was picked because of the shelves on urban2. The bot needs to walk up to the shelf to grab the item. + +// Try 24 units forward (X + 24) + tr = gi.trace(tv(item_origin[0] + offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] + offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units back (X - 24) + tr = gi.trace(tv(item_origin[0] - offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] - offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units right (Y + 24) + tr = gi.trace(tv(item_origin[0], item_origin[1] + offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] + offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units left (Y - 24) + tr = gi.trace(tv(item_origin[0], item_origin[1] - offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] - offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // If we get here, then we could not find a spot to place the node + Com_Printf("%s WARNING: could not add node for item: %s\n", __func__, map_ents->classname); + continue; // Skip adding a node for this item because we cannot reach it + + // We found a spot to place the node + VIABLE_ITEM_NODE: + VectorCopy(tr.endpos, item_origin); // Update the origin + tr = gi.trace(item_origin, NULL, NULL, tv(item_origin[0], item_origin[1], -4096), g_edicts, MASK_PLAYERSOLID); // Find the ground under the item + VectorCopy(tr.endpos, item_origin); // Update the origin again + } + + + item_origin[2] += NODE_Z_HEIGHT; // Raise the node up to standard height + + + Com_Printf("%s found node for item: %s [typeNum: %d]\n", __func__, map_ents->classname, map_ents->item->typeNum); + + // Weapons + if (strcmp(map_ents->classname, "weapon_Sniper") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_M4") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_MP5") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_M3") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_HC") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_Grenade") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + + // Ammo + else if (strcmp(map_ents->classname, "ammo_clip") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_mag") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_m4") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_m3") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_sniper") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + } + if (foundItems == 0) + { + // TODO: Get map ents without needing deathmatch enabled + + Com_Printf("%s WARNING: bot navigation was generated without item nodes. Please ensure the map is generated in deathmatch mode.\n", __func__); + } +} + +// Find the reachability for each node +void BOTLIB_ProcesssReachabilities() +{ + vec3_t zero = { 0 }; + node_t* from_node; // From node + node_t* to_node; // To node + int s, result; + //qboolean found_ladder; + for (s = 0; s < numnodes; s++) // From node + { + if (nodes[s].inuse == false) continue; // Ignore nodes not in use + + //if (nodes[s].nodenum != INVALID) continue; + + // Get the next 'from' node + from_node = (unsorted_nodes + s); // From Node + + // Zero the link counter + from_node->num_links = 0; + + for (int o = 0; o < numnodes; o++) // To node + { + if (nodes[o].inuse == false) continue; // Ignore nodes not in use + //if (nodes[o].nodenum != INVALID) continue; + + if (s == o) // skip self + continue; + + to_node = (unsorted_nodes + o); // Node A + + result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); + + //TEST CODE -- s + //if (result == NODE_JUMPPAD || result == NODE_LADDER_UP || result == NODE_LADDER_DOWN) + // result = INVALID; + //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes + // result = INVALID; + //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) + // result = NODE_JUMPPAD; + //TEST CODE -- e + + if (BOTLIB_AddNodeLink(s, o, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + nmesh.total_reach++; + } + + /* + if (result != INVALID) + { + for (int lk = 0; lk < MAXLINKS; lk++) + { + if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link + { + from_node->links[lk].targetNodeType = result; // Set target node type + + // Add link + //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); + //break; + + + //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes + if (DC_AddNodeLink(NULL, s, o, zero, false, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + //if (from_node->num_links + 1 > MAXLINKS) Com_Printf("%s too many links added to node[%d]\n", __func__, s); + //from_node->num_links++; // Update number of links this node has + nmesh.total_reach++; + } + break; + } + } + //Com_Printf("%s node[%d] added %i links\n", __func__, s, nodes[s].num_links); + } + */ + } + } +} + +void BOTLIB_BSP_SURFACES(bsp_t* bsp) +{ + int f, e; + int nv = 0; // Current number of verts + mface_t* surf; + msurfedge_t* src_surfedge; + + // Copy data from the BSP tree + for (f = 0, surf = bsp->faces; f < bsp->numfaces; f++, surf++) + { + if (surf == NULL) + { + gi.dprintf("%s null faces found\n", __func__); + return; + } + + nmesh.face[f].drawflags = surf->drawflags; // Copy draw flags (DSURF_PLANEBACK, etc) + VectorCopy(surf->plane->normal, nmesh.face[f].normal); // Copy plane normal + nmesh.face[f].num_edges = surf->numsurfedges; // Copy total edges on this face + + // Don't exclude sky because some walkable surfaces are sky + //if (surf->drawflags & SURF_SKY) + // continue; + + // If surf plane is a flat floor + if (surf->plane->normal[2] == 1) + { + if (surf->drawflags & DSURF_PLANEBACK) // Skip flat roof surfaces + continue; + + nmesh.face[f].type = FACETYPE_FLOOR; // Perfectly flat + } + else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + { + nmesh.face[f].type = FACETYPE_SLOPE; // Any climbable sloped surface + } + else //if (surf->plane->normal[2] == 0) // Wall with normal[2] == 0 means the wall is perfectly upright + nmesh.face[f].type = FACETYPE_WALL; // Anything too steep to climb becomes a wall (needs to yet be tested for CONTENTS_LADDER, this will be processed further down) + + + for (e = 0, src_surfedge = surf->firstsurfedge; e < surf->numsurfedges; e++, src_surfedge++) + { + // Copy edge verts + VectorCopy(src_surfedge->edge->v[0]->point, nmesh.face[f].edge[e].v[0]); + VectorCopy(src_surfedge->edge->v[1]->point, nmesh.face[f].edge[e].v[1]); + + + // Copy face verts + nv = nmesh.face[f].num_verts; + VectorCopy(nmesh.face[f].edge[e].v[0], nmesh.face[f].v[nv]); + VectorCopy(nmesh.face[f].edge[e].v[1], nmesh.face[f].v[nv + 1]); + nmesh.face[f].num_verts += 2; + } + + //f = ++nmesh.total_faces; // Total faces + nmesh.total_faces++; // Total faces + + + nmesh.total_verts += (surf->numsurfedges * 2); // Total verts + nmesh.total_edges += surf->numsurfedges; // Total edges + } +} + +void BOTLIB_Process_NMesh(edict_t* ent) +{ + trace_t tr; + + // Player when standing + vec3_t player_standing_mins = { -16, -16, -24 }; + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing + vec3_t player_standing_smaller_mins = { -14, -14, -24 }; + vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; + + const int MIN_STEP_WIDTH = 32; // min step width + qboolean hit_step = false; // If we hit a suspected step (not 100% sure) + qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) + + vec3_t mid_point = { 0 }; + vec3_t ladder_bottom = { 0 }, ladder_top = { 0 }; + + float tmp = 0; + int f, e; + + float normal_0, normal_1, normal_2; + int num_tris = 0; + int vert_count = 0; // Vertex count + int total_verts = 0; + vec3_t pt0 = { 0 }; // Point 1 of a triangle + vec3_t pt1 = { 0 }; // Point 2 of a triangle + vec3_t pt2 = { 0 }; // Point 3 of a triangle + vec3_t center = { 0 }; // Center point of either an edge or a triangle + + // Proccess the mesh data + for (f = 0; f < nmesh.total_faces; f++) + { + normal_0 = nmesh.face[f].normal[0]; + normal_1 = nmesh.face[f].normal[1]; + normal_2 = nmesh.face[f].normal[2]; + + num_tris = 0; + vert_count = 0; + total_verts = nmesh.face[f].num_verts; + memset(pt0, 0, sizeof(vec3_t)); + memset(pt1, 0, sizeof(vec3_t)); + memset(pt2, 0, sizeof(vec3_t)); + memset(center, 0, sizeof(vec3_t)); + + // Edges - (only the outside edges of a face) + //----------------------------------------------------- + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Copy the edge vectors + VectorCopy(nmesh.face[f].edge[e].v[0], pt1); + VectorCopy(nmesh.face[f].edge[e].v[1], pt2); + + // Get the distance between pt1 and pt2 + vec3_t pt1_to_pt2_vec; + VectorSubtract(pt2, pt1, pt1_to_pt2_vec); // Direction vector from pt1 to pt2 + float pt1_to_pt2_dist = VectorLength(pt1_to_pt2_vec); // Distance between pt1 and pt2 + + // Skip excessively small edges + if (pt1_to_pt2_vec < 4) + { + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + // Get the normalized node height based on the face type -- floor, slope, or wall + vec3_t adjusted_height = { 0 }; + if (nmesh.face[f].type == FACETYPE_FLOOR) + adjusted_height[2] += NODE_Z_HEIGHT; // Move the center so it is somewhat above the ground, instead of being stuck inside the ground + else if (nmesh.face[f].type == FACETYPE_SLOPE) + adjusted_height[2] += (NODE_Z_HEIGHT * normal_2); // Calculate the correct height based on the normal of the slope + else if (nmesh.face[f].type == FACETYPE_WALL) // Push out from wall + { + adjusted_height[2] += NODE_Z_HEIGHT; // Move up from ground (note this isn't using the normal direction, its directly up from the ground) + + // Calculate the correct wall distance based on the normal angle + if (nmesh.face[f].drawflags & DSURF_PLANEBACK) + { + if (normal_0 != 0) + adjusted_height[0] -= (NODE_Z_HEIGHT * normal_0); // -X (forward wall) + if (normal_1 != 0) + adjusted_height[1] -= (NODE_Z_HEIGHT * normal_1); // -Y (left wall) + } + else + { + if (normal_0 != 0) + adjusted_height[0] += (NODE_Z_HEIGHT * normal_0); // +X (back wall) + if (normal_1 != 0) + adjusted_height[1] += (NODE_Z_HEIGHT * normal_1); // +Y (right wall) + } + } + + // Get the center of the edge + LerpVector(pt1, pt2, 0.50, center); + + // Record it + VectorCopy(center, nmesh.face[f].edge[e].center); // Copy the center of the edge + + // Apply the adjusted height + nmesh.face[f].edge[e].center[0] += adjusted_height[0]; + nmesh.face[f].edge[e].center[1] += adjusted_height[1]; + nmesh.face[f].edge[e].center[2] += adjusted_height[2]; + + + + + + // Test for edges + // Some edges are on a floor, others from a wall, therefore we test both here + if (0) + { + qboolean hit_step_left = false, hit_step_right = false; + vec3_t end_left, end_right; + + + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1, end_left); // Turn 90 degrees left and move 1 unit + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1, end_right); // Turn 90 degrees right and move 1 unit + + // Test just above center to see if we're in a solid + trace_t tr_edge_center = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] + 1), ent, MASK_PLAYERSOLID); + if (tr_edge_center.startsolid == false && tr_edge_center.fraction == 1.0) // Not in a solid (usually a wall) + { + // Test if we're on an edge with a dropoff + trace_t tr_edge_left = gi.trace(end_left, NULL, NULL, tv(end_left[0], end_left[1], end_left[2] - 1), ent, MASK_PLAYERSOLID); + trace_t tr_edge_right = gi.trace(end_right, NULL, NULL, tv(end_right[0], end_right[1], end_right[2] - 1), ent, MASK_PLAYERSOLID); + if (tr_edge_left.startsolid == false && tr_edge_left.fraction == 1.0) + hit_step_left = true; + if (tr_edge_right.startsolid == false && tr_edge_right.fraction == 1.0) + hit_step_right = true; + + //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + + if (hit_step_left && hit_step_right) // A double edge on either side + { + //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + } + else if (hit_step_left || hit_step_right) // A single edge -- ledge / stairs + { + //if (BOTLIB_UTIL_NearbyNodeAtHeightDist(center, NODE_SIZE) == INVALID) + if (1) + { + //Com_Printf("%s f:%i e:%i node:%d hit_step_left:%i hit_step_right:%i\n", __func__, f, e, numnodes, hit_step_left, hit_step_right); + + //if (BOTLIB_TestForNodeDist(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), 2) == INVALID) + DAIC_Add_Node(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), nmesh.face[f].normal, NODE_STEP); + + // Expand nodes from the top and bottom landing steps. Try adding a node 90 degrees left and -90 degrees right. + // We try both directions because the top step is usually flat, and the landing step has a dropoff. + // This will help the bot get around corners, and connect the top and bottom of stairs to the node network + // + // [left node] [sn_1] + // -------------------------------------------- + // | | [sn_2] + // | ---------- + // | sn_x = step node | [sn_3] + // | left node = move node ---------- + // | right node = move node | [right node] + // | -------------------------------- + // The [left node] is added to the left of [sn_1] + // The [right node] is added to the right of [sn_3] + // + if (1) + { + vec3_t end; + + // Set player waist height + const int player_waist_height = fabs(player_standing_mins[2]); // 24 units + + if (1) // Left step + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 32, end); //2048 + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see if end is in a solid + trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); + if (tr_end_step.startsolid == false) + { + // Test to see if end is touching the ground + trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16,-16,-16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + + if (1) // Right step + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 32, end); //2048 + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see if end is in a solid + trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); + if (tr_end_step.startsolid == false) + { + // Test to see if end is touching the ground + trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + } + } + } + else // Floor edge (no dropoff on either side -- the floor is a flat plane) + { + // Expand additional nodes from the center of the edge. + // Heading forward, back, left, right. + if (1) + { + for (int d = 0; d < 4; d++) + { + vec3_t end; + + if (d == 0) // forwards + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 0, 1024, end); // Forward 0 degrees + //VectorCopy(pt1, end); + //end[2] += 1; + } + else if (d == 1) // backwards + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 180, 1024, end); // Backwards 180 or -180 degrees + //VectorCopy(pt2, end); + //end[2] += 1; + } + else if (d == 2) // left + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1024, end); // Left (pt1 -> pt2 -> 90 degrees) + } + else if (d == 3) // right + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1024, end); // Right (pt1 -> pt2 -> -90 degrees) + } + + // Get player waist height + const int player_waist_height = fabs(player_standing_mins[2]); // 24 units + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see where end meets a wall + // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right + trace_t tr_end_wall = gi.trace(tv(center[0], center[1], center[2] + player_waist_height), player_standing_mins, player_standing_maxs, end, ent, MASK_PLAYERSOLID); + + + // Test to see if end is touching the ground + // From the wall hit down to the ground which is -(player_waist_height + 1) + trace_t tr_end_ground = gi.trace(tr_end_wall.endpos, NULL, NULL, tv(tr_end_wall.endpos[0], tr_end_wall.endpos[1], tr_end_wall.endpos[2] - (player_waist_height + 1)), ent, MASK_PLAYERSOLID); + + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE * 4, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + } + } + } + } + + + // Skip wall edges + if (nmesh.face[f].type == FACETYPE_WALL) + { + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + + + + + // Find the middle between two points of an edge + //LerpVector(pt1, pt2, 0.50, center); // 50% (center) + + + // Ignore edges that are up in the air + // Trace just below the floor, if we didn't hit the ground, then the node is up in the air. We want to ignore these because some are 'trigger' and other ents + //if (nmesh.face[f].type == FACETYPE_FLOOR || nmesh.face[f].type == FACETYPE_SLOPE) + { + //tr = gi.trace(center, tv(-16, -16, -(NODE_Z_HEIGHT + 1)), tv(16, 16, 24), center, ent, MASK_SOLID); + // move off ground slightly, then line trace back to ground, if we don't hit ground, then we're in the air + tr = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] - 1), ent, MASK_PLAYERSOLID); + //tr = gi.trace(center, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), center, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + //Com_Printf("%s face:%i HIT failed to find ground\n", __func__, f); + nmesh.face[f].edge[e].node = INVALID; + continue; + } + } + + // Check for trigger_hurt + if (BOTLIB_UTIL_CHECK_FOR_HURT(center)) + { + //Com_Printf("%s face:%i HIT trigger_hurt\n", __func__, f); + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + // Depending on the edge tested (within the same face) we may encounter different multiple groups of contents, + // therefore we need to add them all together. This is because edge can be shared within the same face, or other faces. + int box_contents = BOTLIB_UTIL_BOXCONTENTS(center); + if (nmesh.face[f].contents > 0 && box_contents > 0 && box_contents != nmesh.face[f].contents) + { + // before + //Com_Printf("%s face:%i tris:%i contents:0x%x contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents, tr.contents); + // + // Add tr.contents to nmesh.face[f].contents + // + // 0x18000002 + // + 0x08030000 + // ---------- + // = 0x18030002 + // + nmesh.face[f].contents |= box_contents; + // + // after + //Com_Printf("%s face:%i tris:%i contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + } + // Add the first contents found + if (nmesh.face[f].contents == 0) + nmesh.face[f].contents = box_contents; + + // Copy it back + VectorCopy(nmesh.face[f].edge[e].center, center); + + // Test for steps / edges +#if 0 + if (0) + { + + // Check if trace hit a step - STEPSIZE + // + // e1 = edge 1 + // e2 = edge 2 + // c = center of bounding box + // + // roof ------------------------------- + // + // ____________ + // | | + // | player | + // | bounding | + // | box | + // | | + // -----|------ + // c + // + // floor ___________e1 (step) + // | + // |___e2 (step) + // | + // | + // ------------- floor + // + // The center of the players bounding box will fit on (e1), but might not on (e2) as it could overlap against (e1) + // We do the same floor trace as normal, except move the bounding box upward by STEPSIZE + // and check if player will fit, as if they were standing on the step while crouching. This is because + // a player can crouch walk up stairs, thereby covering normal + edge cases. + // + + hit_step = false; // If we hit a suspected step (not 100% sure) + hit_ledge = false; // If we hit a suspected ledge (not 100% sure) + + + vec3_t dist; + VectorSubtract(pt1, pt2, dist); // vector from pt1 to pt2 + dist[2] = 0; // remove height + vec3_t center_step; + VectorCopy(center, center_step); + center_step[2] -= (adjusted_height[2] - 0.1); + trace_t tr_edge = gi.trace(center_step, tv(-16, -16, -24), tv(16, 16, 32), center_step, ent, MASK_PLAYERSOLID); + if (tr_edge.startsolid && tr_edge.fraction < 1 && VectorLength(dist) > MIN_STEP_WIDTH/*calc step width*/) // inside step and we're larger than min step width + { + //* + //vec3_t center_step; + //VectorCopy(center, center_step); + //center_step[2] += STEPSIZE; + //center_step[2] += adjusted_height[2]; + center_step[2] += STEPSIZE + 0.1; + + //trace_t tr_step = gi.trace(center_step, player_crouching_mins, player_crouching_maxs, center_step, ent, MASK_DEADSOLID); + trace_t tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), center_step, ent, MASK_DEADSOLID); + //trace_t tr_step = gi.trace(center_step, tv(-0.1, -0.1, 0), tv(0.1, 0.1, 0), center_step, ent, MASK_DEADSOLID); + //trace_t tr_step = gi.trace(center_step, NULL, NULL, center_step, ent, MASK_DEADSOLID); + if (!tr_step.startsolid && tr_step.fraction == 1) + { + // Find the floor + tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), tv(center_step[0], center_step[1], center_step[2] - (STEPSIZE + 1)), ent, MASK_DEADSOLID); + tr_step.endpos[2] += adjusted_height[2]; + + if (BOTLIB_UTIL_NearbyNodeAtHeightDist(tr_step.endpos, NODE_SIZE) == INVALID) + { + DAIC_Add_Node(tr_step.endpos, nmesh.face[f].normal, NODE_STEP); + } + //hit_step = true; + //VectorCopy(center_step, center); // increase node z loc from NODE_Z_HEIGHT to (NODE_Z_HEIGHT + STEPSIZE) + //Com_Printf("%s f:%d e:%d [center_step %f %f %f] [center %f %f %f]\n", __func__, f, e, center_step[0], center_step[1], center_step[2], center[0], center[1], center[2]); + + } + //*/ + + /* + // Try again but move the bounding box slightly, this works for small edges that go along a wall (urban, urban2, etc) + // Move the bounding box to each corner, forward/back/right/left. Then trace again. + trace_t tr_edge; + vec3_t center_edge; + + // forward + if (hit_step == false) + { + VectorCopy(center, center_edge); + center_edge[0] += 16; // move node forward + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // back + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[0] -= 16; // move node back + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // right + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[1] += 16; // move node right + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // left + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[1] -= 16; // move node left + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + */ + } + } +#endif + + //if (nmesh.face[f].type == FACETYPE_WALL && (nmesh.face[f].contents & CONTENTS_LADDER) == 0) + + + if (nmesh.face[f].type == FACETYPE_SLOPE) + tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_PLAYERSOLID); + else // floor + tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_PLAYERSOLID); + + + + //tr = gi.trace(center, tv(-8, -8, -20), tv(8, 8, 0), center, ent, MASK_SOLID); + if ((!tr.startsolid && tr.fraction == 1))// || hit_step || hit_ledge) + { + //if (tr.contents < CONTENTS_AREAPORTAL || (tr.contents & CONTENTS_LADDER) == 0) // Look for visible brushes, including non-solid such as liquids, also ladders + { + if (BOTLIB_TestForNodeDist(center, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) // || hit_step || hit_ledge) // hit_step and hit_ledge ignore node size (for now) + { + // Test again, but just for MASK_WATER + if (nmesh.face[f].type == FACETYPE_SLOPE) + tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_WATER); + else + tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_WATER); + + if ((nmesh.face[f].contents & CONTENTS_LAVA) || (nmesh.face[f].contents & CONTENTS_SLIME)) + continue; + else if ((tr.contents & CONTENTS_WATER) || nmesh.face[f].contents & CONTENTS_WATER) + //continue; // Don't add edge water nodes + DAIC_Add_Node(center, nmesh.face[f].normal, NODE_WATER); + else + { + //if (hit_step) // hit_step + // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + //else if (hit_ledge) + // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + //else + DAIC_Add_Node(center, nmesh.face[f].normal, NODE_MOVE); + } + + //if (nmesh.face[f].edge[e].node) + // Com_Printf("WARNING: edge node already exists face:%d edge:%d node:%d\n", f, e, nmesh.face[f].edge[e].node); + + nmesh.face[f].edge[e].node = numnodes - 1; + + + // Expand additional nodes from the center of the edge, one heading 90 degrees left, and one heading 90 degrees right. + if (0 && nmesh.face[f].type == FACETYPE_FLOOR) + { + vec3_t end_left, end_right; + + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 2000, end_left); + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 270, 2000, end_right); + + // Lift the end points up a bit so they don't get stuck in the ground + end_left[2] += 1; + end_right[2] += 1; + + // Find the middle between two points of an edge + LerpVector(pt1, pt2, 0.50, center); + //center[2] += NODE_Z_HEIGHT; + //center[2] += 25; + + // Test to see where the left and right meet a wall + // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right + trace_t tr_left_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_left, ent, MASK_PLAYERSOLID); + trace_t tr_right_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_right, ent, MASK_PLAYERSOLID); + + //DAIC_Add_Node(tr_left_wall.endpos, tr_left_wall.plane.normal, NODE_MOVE); + //DAIC_Add_Node(tr_right_wall.endpos, tr_right_wall.plane.normal, NODE_MOVE); + + // Test to see if left and right are touching the ground + //trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); + //trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); + trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); + trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); + + + + // If the left is touching the ground, then we can add a node there + //if (tr_left_ground.fraction < 1.0) + { + tr_left_wall.endpos[2] += (NODE_Z_HEIGHT - 1); + + //if (BOTLIB_TestForNodeDist(tr_left_wall.endpos, NODE_SIZE) == INVALID) + DAIC_Add_Node(tr_left_wall.endpos, tr_left_ground.plane.normal, NODE_MOVE); + } + // If the right is touching the ground, then we can add a node there + //if (tr_right_ground.fraction < 1.0) + { + tr_right_wall.endpos[2] += (NODE_Z_HEIGHT - 1); + + //if (BOTLIB_TestForNodeDist(tr_right_wall.endpos, NODE_SIZE) == INVALID) + DAIC_Add_Node(tr_right_wall.endpos, tr_right_ground.plane.normal, NODE_MOVE); + } + } + + + + } + } + } + } + //----------------------------------------------------- + } +} + +#define BOTLIB_OrthogonalToVectors(v1, v2, res) \ + (res)[0] = ((v1)[1] * (v2)[2]) - ((v1)[2] * (v2)[1]);\ + (res)[1] = ((v1)[2] * (v2)[0]) - ((v1)[0] * (v2)[2]);\ + (res)[2] = ((v1)[0] * (v2)[1]) - ((v1)[1] * (v2)[0]); + +// Verts must be unwound and in clockwise order +qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t normal, float epsilon) +{ + vec3_t v1, v2; + vec3_t edgevec, pointvec, sepnormal; + + for (int v = 0; v < num_verts; v += 2) // edges + { + VectorCopy(verts[v + 0], v1); + VectorCopy(verts[v + 1], v2); + //edge vector + VectorSubtract(v2, v1, edgevec); + //vector from first edge point to point possible in face + VectorSubtract(point, v1, pointvec); + //get a vector pointing inside the face orthogonal to both the + //edge vector and the normal vector of the plane the face is in + //this vector defines a plane through the origin (first vertex of + //edge) and through both the edge vector and the normal vector + //of the plane + BOTLIB_OrthogonalToVectors(edgevec, normal, sepnormal); + //check on which side of the above plane the point is + //this is done by checking the sign of the dot product of the + //vector orthogonal vector from above and the vector from the + //origin (first vertex of edge) to the point + //if the dotproduct is smaller than zero the point is outside the face + if (DotProduct(pointvec, sepnormal) < -epsilon) return false; + } + + return true; // Found point +} + +void BOTLIB_InitNavigation(edict_t* ent) +{ + bsp_t* bsp = gi.Bsp(); + if (bsp == NULL) + { + gi.dprintf("%s failed to import BSP data\n", __func__); + return; + } + + if (ent == NULL) + { + ent = g_edicts; + if (ent == NULL) + return; + } + + BOTLIB_InitNodes(); + nmesh.bsp_checksum = bsp->checksum; // Save the map checksum + + Remove_All_Doors(); // Make sure all doors are open before making any nodes +} + +void ACEND_BSP(edict_t* ent) +{ + bsp_t* bsp = gi.Bsp(); + if (bsp == NULL) + { + gi.dprintf("%s failed to import BSP data\n", __func__); + return; + } + + if (ent == NULL) + { + ent = g_edicts; + if (ent == NULL) + return; + } + + clock_t clock_begin = clock(); // Start performance clock + + BOTLIB_InitNodes(); + nmesh.bsp_checksum = bsp->checksum; // Save the map checksum + + Remove_All_Doors(); // Make sure all doors are open before making any nodes + + solid_t *trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); + if (trigger_solid == NULL) + { + Com_Printf("%s failed to malloc trigger_solid\n", __func__); + return; + } + if (BOTLIB_MakeEntsSolid(trigger_solid) == -1) + return; + BOTLIB_AddItemNodes(); // Add nodes at item locations + + BOTLIB_BSP_SURFACES(bsp); + + //BOTLIB_Process_NMesh(ent); + + if (1) + { + ent->nav = gi.Nav(); // Grant access to navigation data + if (ent->nav) + { + for (int f = 0; f < ent->nav->faces_total; f++) + { + if (ent->nav->surface_data_faces[f].face_type != FACETYPE_WALK && ent->nav->surface_data_faces[f].face_type != FACETYPE_TINYWALK) continue; + + /* + for (int e = 0; e < ent->nav->surface_data_faces[f].num_verts; e += 2) // Edges + { + // Get center of edge + vec3_t center; + VectorAdd(ent->nav->surface_data_faces[f].verts[e], ent->nav->surface_data_faces[f].verts[e + 1], center); + VectorScale(center, 0.5, center); + + // Move center toward face center by 4 units + vec3_t dir; + VectorSubtract(ent->nav->surface_data_faces[f].center_poly, center, dir); + VectorNormalize(dir); + VectorMA(center, 8, dir, center); + + // Check if we're still in the same face + //BOTLIB_InsideFace + + // Move center up by 32 units + center[2] += 32; + + // Test to see if the center is valid + if (BOTLIB_TestForNodeDist(center, NODE_SIZE) == INVALID) + { + // Add the node + DAIC_Add_Node(center, ent->nav->surface_data_faces[f].normal, NODE_MOVE); + } + } + */ + + + for (int c = 0; c < ent->nav->surface_data_faces[f].snode_counter; c++) + { + vec3_t pos; + VectorCopy(ent->nav->surface_data_faces[f].snodes[c].start, pos); + pos[2] += NODE_Z_HEIGHT; + if (BOTLIB_TestForNodeDist(pos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(pos, ent->nav->surface_data_faces[f].normal, NODE_MOVE); + } + } + } + } + + // Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER + ACEND_FindEdgeLadders(); + + // Find the reachability for each node + BOTLIB_ProcesssReachabilities(); + + // Try placing POI nodes on high locations + if (0) + { + /* + float highest_origin = -9999; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > highest_origin) + highest_origin = nodes[i].origin[2]; + } + float above_height = (highest_origin - 256); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > above_height)// && random() < 0.1) + nodes[i].type = NODE_POI; + } + */ + + //ACEND_BuildSpawnPointNodes(); + //ACEND_CachePointOfInterestNodes(); + } + + // Restore solid state of each ent + BOTLIB_RestoreEntsSolidState(trigger_solid); + + + + gi.dprintf("BSP f[%i] t[%i] e[%i] v[%i] r[%i] n[%i]\n", nmesh.total_faces, nmesh.total_tris, nmesh.total_edges, nmesh.total_verts, nmesh.total_reach, numnodes); + clock_t clock_end = clock(); + Com_Printf("%s execution took %f seconds to process bsp reachability\n", __func__, (double)(clock_end - clock_begin) / CLOCKS_PER_SEC); + + //ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies + // Free the memory + if (unsorted_nodes) + { + free(unsorted_nodes); + unsorted_nodes = NULL; // Nullify dangling pointer + } +} +//rekkie -- BSP -- e \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c new file mode 100644 index 000000000..2a1a86eb1 --- /dev/null +++ b/src/action/botlib/botlib_spawn.c @@ -0,0 +1,2091 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Find a free entity for the bot to use +edict_t* BOTLIB_FindFreeEntity(void) +{ + edict_t* bot = NULL; + for (int i = game.maxclients; i > 0; i--) + { + bot = g_edicts + i + 1; + + if (!bot->inuse) + return bot; // Success + } + + return NULL; // Failure +} + +qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) +{ + FILE* f; + int comments_num = 0; // Keep track of how many comments the file has + int line_num = 0; // Keep track of lines + int curr_len; // Current length of the line + char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) + curr_line[0] = '\0'; // Current line + + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + char filename[MAX_QPATH]; // Filename to load from + int i; // Keep track where we are in the filename array +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + if (strlen(filename) <= 0) return false; + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + gi.dprintf("%s failed to open bot file: %s\n", __func__, filename); + return false; + } + + // Find how many lines the file has + int random_line; + while (fgets(curr_line, sizeof(curr_line), f)) + { + line_num++; + } + fclose(f); + random_line = rand() % line_num + 1; + //random_line = 13; + + //Com_Printf("%s %s random_line[%d]\n", __func__, filename, random_line); + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + line_num = -1; + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + line_num++; // Advance forward + + //if (feof(f)) // End of file + // break; // Could not get a prefix name + + if (curr_line[0] == '/' && curr_line[1] == '/') // Skip comment lines + continue; + + // If we're at or above the correct line, not a comment, not empty + //if (line_num >= random_line && curr_line[0] != '/' && curr_line[0]) + if (line_num >= random_line) + { + curr_len = strlen(curr_line); + if (curr_len < MAX_QPATH) // Make sure name is within acceptable length + { + //Com_Printf("%s %s curr_line[%d] = %s\n", __func__, filename, line_num, curr_line); + Q_strlcpy(buffer, curr_line, curr_len); + break; + } + } + } + fclose(f); + } + + if (strlen(buffer) > 0) + { + return true; + } + else + { + buffer[0] = '\0'; // Terminate empty string + return false; + } +} + +// Save bots from the previous map to file +#define BOTS_FILE_PREV_MAP_VERSION 1 +qboolean BOTLIB_SaveBotsFromPreviousMap(void) +{ + FILE* f; + char filename[128]; + const char* file = "prev_map_bots.txt"; + int i; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + + //if (bot_connections.total_bots <= 0) // Don't write unless we have bots + // return false; + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + //Com_Printf("%s Writing previous bots to file %s\n", __func__, filename); + + // Write to file + f = fopen(filename, "w"); // Open the file in write mode + if (f == NULL) + { + Com_Printf("Could not write to file %s\n", filename); + return false; + } + + fprintf(f, "// Bots from a previous map\n"); + fprintf(f, "%d\n", BOTS_FILE_PREV_MAP_VERSION); + fprintf(f, "%d\n", bot_connections.total_bots); + fprintf(f, "%d\n", (int)bot_maxteam->value); + if (use_3teams->value) + fprintf(f, "3teams\n"); + else if (teamplay->value) + fprintf(f, "teamplay\n"); + else + fprintf(f, "deathmatch\n"); + + for (i = 0; i < num_players; i++) + { + if (players[i]->is_bot == false) continue; // Skip humans + + // Gender + fprintf(f, "%d\n", players[i]->client->pers.gender); + + // Team + fprintf(f, "%d\n", players[i]->client->resp.team); + + // Name + fprintf(f, "%s\n", players[i]->client->pers.netname); + + // Skin + fprintf(f, "%s\n", Info_ValueForKey(players[i]->client->pers.userinfo, "skin")); + } + + fclose(f); // Close the file after use + + return true; +} + +// Add bots from the previous map from file +// Parameters: +// percent of bots to keep from previous map +qboolean BOTLIB_AddBotsFromPreviousMap(float percent) +{ + FILE* f; + qboolean print_dbg = false; // Print debug info + int line_num = 0; // Keep track of lines + + int prev_bots_skipped = 0; // How many bots we skipped adding from previous map + int prev_bot_count = 0; // How many bots from the previous map + int bots_added = 0; // How many bots we added from previous map + char gamemode[MAX_QPATH]; // Gamemode string buffer + int prev_gamemode = 0; // Previous gamemode + int prev_team = 0; // Previous Team + int prev_gender = 0; // Previous Gender + char prev_name[MAX_QPATH]; // Previous Name + char prev_skin[MAX_QPATH]; // Previous Skin + + int curr_len = 0; // Current length of the line + char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) + curr_line[0] = '\0'; // Current line + + const char* file = "prev_map_bots.txt"; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + char filename[MAX_QPATH]; // Filename to load from + int i; // Keep track where we are in the filename array +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + if (strlen(filename) <= 0) return false; + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + return false; // Ignore if file doesn't exist + } + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + // Ignore first line + fgets(curr_line, sizeof(curr_line), f); // First line is commented + if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); + + // Get file version + fgets(curr_line, sizeof(curr_line), f); + int version = atoi(curr_line); + if (print_dbg) Com_Printf("%s line[%d] version[%d]\n", __func__, ++line_num, version); + if (version != BOTS_FILE_PREV_MAP_VERSION) + { + return false; + } + + // Get previous bot count + fgets(curr_line, sizeof(curr_line), f); + prev_bot_count = atoi(curr_line); + if (print_dbg) Com_Printf("%s line[%d] bot_count[%d]\n", __func__, ++line_num, prev_bot_count); + if (prev_bot_count <= 0) + { + //gi.dprintf("%s failed to process bot count from file: %s\n", __func__, filename); + return false; + } + + // Get previous bot_maxteam num + fgets(curr_line, sizeof(curr_line), f); + //bot_maxteam->value = atoi(curr_line); + gi.cvar_set("bot_maxteam", va("%d", atoi(curr_line))); // Override if manually added + if (print_dbg) Com_Printf("%s line[%d] bot_maxteam[%f]\n", __func__, ++line_num, bot_maxteam->value); + if (bot_maxteam->value < 0 || bot_maxteam->value > MAX_CLIENTS) + { + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if manually added + gi.dprintf("%s failed to process bot_maxteam from file: %s\n", __func__, filename); + return false; + } + + // Get gamemode + fgets(curr_line, sizeof(curr_line), f); // Second line is gamemode + if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); + curr_len = strlen(curr_line); + Q_strlcpy(gamemode, curr_line, curr_len); + if (Q_strcasestr(gamemode, "deathmatch") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is DEATHMATCH\n", __func__); + prev_gamemode = 1; // DM + } + else if (Q_strcasestr(gamemode, "teamplay") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is TEAMPLAY\n", __func__); + prev_gamemode = 2; // TP + } + else if (Q_strcasestr(gamemode, "3teams") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is 3TEAMS\n", __func__); + prev_gamemode = 3; // 3TEAMS + } + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + // Get gender + prev_gender = atoi(curr_line); + if (prev_gender < GENDER_MALE || prev_gender > GENDER_NEUTRAL) + { + Com_Printf("%s failed to process bot gender from file: %s\n", __func__, filename); + return false; + } + + // Get team + fgets(curr_line, sizeof(curr_line), f); // Get next line + prev_team = atoi(curr_line); + if (prev_team < NOTEAM || prev_team > TEAM3) + { + Com_Printf("%s failed to process bot team from file: %s\n", __func__, filename); + return false; + } + + // Get name + fgets(curr_line, sizeof(curr_line), f); // Get next line + curr_len = strlen(curr_line); + if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length + continue; + Q_strlcpy(prev_name, curr_line, curr_len); + + // Get skin + fgets(curr_line, sizeof(curr_line), f); // Get next line + curr_len = strlen(curr_line); + if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length + continue; + Q_strlcpy(prev_skin, curr_line, curr_len); + + if (print_dbg) Com_Printf("%s %s curr_line[%d]. GM[%s] Gender[%d] Team[%d] Name[%s] Skin[%s]\n", __func__, filename, ++line_num, gamemode, prev_gender, prev_team, prev_name, prev_skin); + + if (random() > (percent / 100)) // Percent chance to skip adding bot from previous map + { + prev_bots_skipped++; // Bots that were not added from previous map + continue; + } + + //if (bot_maxteam->value && bots_added >= (int)bot_maxteam->value) // Limit bots to bot_maxteam, if its set + // break; + if (prev_bot_count && bots_added >= prev_bot_count) + break; + + if (use_3teams->value) + { + // Bot was previously in a different game mode joining a 3teams game, randomize the team it joins + if (prev_gamemode == 1 || prev_gamemode == 2) // DM or TP + prev_team = (rand() % TEAM3) + 1; + + if (prev_team == TEAM1) bot_connections.desire_team1++; + else if (prev_team == TEAM2) bot_connections.desire_team2++; + else if (prev_team == TEAM3) bot_connections.desire_team3++; + + if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); + BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); + bots_added++; + } + else if (teamplay->value) // TP and CTF + { + // Bot was previously in a different game mode joining a teamplay game, randomize the team it joins + if (prev_gamemode == 1 || prev_gamemode == 3) // DM or 3TEAMS + prev_team = (rand() % TEAM2) + 1; + + if (prev_team == TEAM1) bot_connections.desire_team1++; + else if (prev_team == TEAM2) bot_connections.desire_team2++; + + if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); + BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); + bots_added++; + } + else // DM + { + if (strlen(prev_skin)) + BOTLIB_SpawnBot(0, prev_gender, prev_name, prev_skin); + else + BOTLIB_SpawnBot(0, prev_gender, prev_name, NULL); + bots_added++; + } + } + fclose(f); + } + + + if (bots_added) + { + //if (bot_maxteam->value < 1) // If max team wasn't set, use it so bots can be shuffled + // bot_maxteam->value = prev_bot_count; + + bot_connections.desire_bots = bots_added; // How many bots + //bot_connections.auto_balance_bots = true; + + return true; + } + + return false; +} + +void BOTLIB_RandomizeTeamNames(edict_t* bot) +{ + char name[MAX_QPATH]; + int len = 0; + const int max_name_size = sizeof(teams[TEAM1].name); // Team names are limited to 20 characters (19 chars + terminator) + + qboolean same_name = false; + for (int i = 0; i < MAX_TEAMS; i++) // For each team + { + // Try for random team names that fit within 18 characters + for (int t = 0; t < 32; t++) // Max loops + { + BOTLIB_GetRandomBotFileLine("team_names.txt", name); // Get a random team name + + // Get name lengths + len = strlen(name); + + // Find if the name length will fit within 20 chars (name + space + terminator == 20) + if (len <= (max_name_size - 2)) // adjust max size to account for space + terminator + { + // Make sure the name is different + if (Q_strcasestr(name, teams[TEAM1].name) != NULL) // If same name as Team1 + continue; + if (Q_strcasestr(name, teams[TEAM2].name) != NULL) // If same name as Team2 + continue; + if (Q_strcasestr(name, teams[TEAM3].name) != NULL) // If same name as Team3 + continue; + + // Merge the names together + Q_strncpyz(teams[i].name, va("%s", name), len + 2); // name + space + terminator + Com_Printf("%s Setting Team Name: %s\n", __func__, teams[i].name); + break; + } + } + } +} + + +void BOTLIB_RandomizeTeamSkins(edict_t* bot) +{ + //Com_Printf("%s before T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + + // Randomize skins + BOTLIB_RandomSkin(bot, teams[TEAM1].skin, INVALID); + BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); + + // Check if any of the randomized skins are the same, if so re-roll them until they're different + qboolean same_skin = false; + for (int i = 0; i < 64; i++) + { + same_skin = false; + if (Q_strcasestr(teams[TEAM2].skin, teams[TEAM1].skin) != NULL) // If Team2 is the same skin as Team1 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); // Re-randomize skin + } + + if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM1].skin) != NULL) // If Team3 is the same skin as Team1 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin + } + + if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM2].skin) != NULL) // If Team3 is the same skin as Team2 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin + } + + if (same_skin == false) // No same skins detected, so exit loop early + break; + } + + //Com_Printf("%s after T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + + // Update skin associated indexes + Com_sprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); + Com_sprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); + Com_sprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); + + // Change team picture using the updated skin indexes + level.pic_teamskin[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); + level.pic_teamskin[TEAM2] = gi.imageindex(teams[TEAM2].skin_index); + level.pic_teamskin[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); +} + +/* +void DC_CacheRandomBotNames(void) +{ + int i, n; + + // Cache male names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_male[n]); + n++; + } + // Cache female names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_female[n]); + n++; + } +} +void DC_GetRandomBotName(byte gender, char* bot_name) +{ + if (gender == GENDER_MALE) + { + //Q_strlcpy(bot_name, bot_male[n], name_length); + } + else if (gender == GENDER_FEMALE) + { + + } + else + { + if (random() < 0.5) + { + + } + else + { + + } + } +} +*/ + +//====================================== +// BOTLIB_GetRandomClanSymbol() +// Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc +// Returns char +//====================================== +int BOTLIB_GetRandomClanSymbol() +{ + int sym = rand() % 25; + switch (sym) + { + case 0: return '!'; + case 1: return '#'; + case 2: return '$'; + case 3: return '%'; + case 4: return '&'; + case 5: return '('; + case 6: return ')'; + case 7: return '*'; + case 8: return '+'; + case 9: return ','; + case 10: return '-'; + case 11: return '.'; + //case 12: return '/'; + case 12: return ':'; + case 13: return '<'; + case 14: return '='; + case 15: return '>'; + case 16: return '?'; + case 17: return '@'; + case 18: return '['; + case 19: return ']'; + //case 21: return '\\'; + case 20: return '^'; + case 21: return '_'; + case 22: return '{'; + case 23: return '|'; + case 24: return '}'; + default: return ' '; + } +} + +//====================================== +// BOTLIB_GetOpposingClanSymbol() +// If a symbol has an opposite: ie --> [ ], ( ), { }, < >, etc +// Returns opposite side, else returns the input symbol +//====================================== +int BOTLIB_GetOpposingClanSymbol(char symbol) +{ + if (symbol == '[') + return ']'; + if (symbol == ']') + return '['; + + if (symbol == '(') + return ')'; + if (symbol == ')') + return '('; + + if (symbol == '{') + return '}'; + if (symbol == '}') + return '{'; + + if (symbol == '<') + return '>'; + if (symbol == '>') + return '<'; + + if (symbol == '\\') + return '/'; + if (symbol == '/') + return '\\'; + + return symbol; +} + + +//====================================== +// BOTLIB_GetRandomClanLetter() +// Gets a random clan letter +// Returns char +//====================================== +int BOTLIB_GetRandomClanLetter() +{ + // Gets a random ASCII letter between 65 and 90, or 97 and 122 + int letter = rand() % 52; + switch (letter) + { + case 0: return 'A'; + case 1: return 'B'; + case 2: return 'C'; + case 3: return 'D'; + case 4: return 'E'; + case 5: return 'F'; + case 6: return 'G'; + case 7: return 'H'; + case 8: return 'I'; + case 9: return 'J'; + case 10: return 'K'; + case 11: return 'L'; + case 12: return 'M'; + case 13: return 'N'; + case 14: return 'O'; + case 15: return 'P'; + case 16: return 'Q'; + case 17: return 'R'; + case 18: return 'S'; + case 19: return 'T'; + case 20: return 'U'; + case 21: return 'V'; + case 22: return 'W'; + case 23: return 'X'; + case 24: return 'Y'; + case 25: return 'Z'; + case 26: return 'a'; + case 27: return 'b'; + case 28: return 'c'; + case 29: return 'd'; + case 30: return 'e'; + case 31: return 'f'; + case 32: return 'g'; + case 33: return 'h'; + case 34: return 'i'; + case 35: return 'j'; + case 36: return 'k'; + case 37: return 'l'; + case 38: return 'm'; + case 39: return 'n'; + case 40: return 'o'; + case 41: return 'p'; + case 42: return 'q'; + case 43: return 'r'; + case 44: return 's'; + case 45: return 't'; + case 46: return 'u'; + case 47: return 'v'; + case 48: return 'w'; + case 49: return 'x'; + case 50: return 'y'; + case 51: return 'z'; + default: return ' '; + } +} + + + +void BOTLIB_GetRandomName(char* name, const int gender) +{ + if (gender == GENDER_MALE) + { + if (random() < 0.8) + BOTLIB_GetRandomBotFileLine("males.txt", name); + else + BOTLIB_GetRandomBotFileLine("other.txt", name); + } + else // GENDER_FEMALE + BOTLIB_GetRandomBotFileLine("females.txt", name); + + //if (strlen(name) <= 0) // Something went wrong. No name was set. + // Q_strlcpy(name, "AqtionMan", 9); // Set a default +} + +void BOTLIB_GetNamePrefix(char* name, const int type) +{ + if (type) + BOTLIB_GetRandomBotFileLine("clans.txt", name); // Clan tag + else + BOTLIB_GetRandomBotFileLine("prefix.txt", name); // Prefix +} + +void BOTLIB_GetNamePostfix(char* name, const int type) +{ + BOTLIB_GetRandomBotFileLine("postfix.txt", name); // Postfix +} + +void BOTLIB_GetRandomPrefix(char* name) +{ + byte tag = 0; + char outer_symbol = 0; + char inner_symbol = 0; + + outer_symbol = BOTLIB_GetRandomClanSymbol(); + if (random() < 0.5) // 50% chance to add an inner symbol + inner_symbol = BOTLIB_GetRandomClanSymbol(); + + // Add the prefix tag + name[tag] = outer_symbol; + if (inner_symbol) + name[++tag] = inner_symbol; + + // Add the clan acronym + name[++tag] = BOTLIB_GetRandomClanLetter(); + name[++tag] = BOTLIB_GetRandomClanLetter(); + if (random() < 0.25) name[++tag] = BOTLIB_GetRandomClanLetter(); // percent chance to add a third clan letter + + // Add the suffix tag + if (inner_symbol) + name[++tag] = BOTLIB_GetOpposingClanSymbol(inner_symbol); // Add the opposing symbol, if any + name[++tag] = BOTLIB_GetOpposingClanSymbol(outer_symbol); + + // print the bot name + name[++tag] = '\0'; + //Com_Printf("CLAN TAG %s\n", name); +} + + + +//====================================== +// DC_LoadRandomBotName() +// Gets a random bot name from file; bot_name is limited to a maximum of 16 chars, and the size of the list can be up to random()'s maximum of 32,767 +// Genders can be: GENDER_MALE, GENDER_FEMALE, GENDER_NEUTRAL +// Returns bot_name +//====================================== +void BOTLIB_LoadRandomBotName(edict_t* bot, char* bot_name, int gender) +{ + //for (int n = 0; n < 100; n++) + { + int rnglen = 0; // Length of rng name + int namelen = 0; // Length of name + int clanlen = 0; // Length of clan name + int prefixlen = 0; // Length of Prefix name + int postfixlen = 0; // Length of Postfix name + char name[MAX_QPATH]; // Name + char clan_name[MAX_QPATH]; // Clan name + char prefix_name[MAX_QPATH]; // Prefix name + char postfix_name[MAX_QPATH]; // Postfix name + char rng_clan_name[MAX_QPATH]; // Random Clan name + qboolean name_with_rng = false; // If name + rng clan will fit within 16 characters + qboolean name_with_clan = false; // If name + clan will fit within 16 characters + qboolean name_with_prefix = false; // If name + prefix will fit within 16 characters + qboolean name_with_postfix = false; // If name + postfix will fit within 16 characters + + // Get random names from file + //Q_strlcpy(name, "AqtionMan", 10); // Set a default name + BOTLIB_GetRandomName(name, gender); // Get name based on gender + BOTLIB_GetNamePrefix(clan_name, 0); + BOTLIB_GetNamePrefix(prefix_name, 1); + BOTLIB_GetNamePostfix(postfix_name, 1); + BOTLIB_GetRandomPrefix(rng_clan_name); + + // Decide if we're going to only take only part of a name (i.e. Arnold Schwarzenegger - we can take either Arnold or Schwarzenegger) + if (rand() % 2 == 0) + { + qboolean two_part_name = false; + char name_cpy[MAX_QPATH]; + char name_pt1[MAX_QPATH]; + char name_pt2[MAX_QPATH]; + Q_strlcpy(name_cpy, name, sizeof(name)); // Make a copy of the original string because strtok() will edit it + char* split_name = strtok(name_cpy, " "); + if (split_name != NULL) + { + Q_strlcpy(name_pt1, split_name, sizeof(split_name)); + split_name = strtok(NULL, " "); + } + if (split_name != NULL) + { + two_part_name = true; + Q_strlcpy(name_pt2, split_name, sizeof(split_name)); + } + if (two_part_name) + { + //Com_Printf("%s x1 name[%s]\n", __func__, name_pt1, strlen(name_pt1)); + //Com_Printf("%s x2 name[%s]\n", __func__, name_pt2, strlen(name_pt2)); + + if (rand() % 2 == 0) + Q_strlcpy(name, name_pt1, sizeof(name_pt1)); + else + Q_strlcpy(name, name_pt2, sizeof(name_pt2)); + } + + //Com_Printf("%s full name[%s]\n", __func__, name, strlen(name)); + } + + //Com_Printf("%s clan[%s][%d] prefix[%s][%d] postfix[%s][%d] rng_clan_name[%s][%d] name[%s][%d]\n", __func__, clan_name, strlen(clan_name), prefix_name, strlen(prefix_name), postfix_name, strlen(postfix_name), rng_clan_name, strlen(rng_clan_name), name, strlen(name)); + + // Get name lengths + namelen = strlen(name); + rnglen = strlen(rng_clan_name); + clanlen = strlen(clan_name); + prefixlen = strlen(prefix_name); + postfixlen = strlen(postfix_name); + + // Find what fits with the bot's name (14 chars for both names + space in between == 15) + if ((namelen + rnglen) < 15) + name_with_rng = true; + if ((namelen + clanlen) < 15) + name_with_clan = true; + if ((namelen + prefixlen) < 15) + name_with_prefix = true; + if ((namelen + postfixlen) < 15) + name_with_postfix = true; + + Q_strncpyz(bot_name, va("%s", name), namelen + 2); // Set a random name + + // Merge the names together + int name_type = rand() % 100; + if (name_type < 15 && name_with_rng) + Q_strncpyz(bot_name, va("%s %s", rng_clan_name, name), namelen + rnglen + 2); // RNG clan + name + else if (name_type < 20 && name_with_clan) + Q_strncpyz(bot_name, va("%s %s", clan_name, name), namelen + clanlen + 2); // clan + name + else if (name_type < 25 && name_with_prefix) + Q_strncpyz(bot_name, va("%s %s", prefix_name, name), namelen + prefixlen + 2); // prefix + name + else if (name_type < 30 && name_with_postfix) + Q_strncpyz(bot_name, va("%s %s", name, postfix_name), namelen + postfixlen + 2); // name + postfix + + //Com_Printf("%s name[%s][%d]\n", __func__, bot_name, strlen(bot_name)); + } +} + +// Sets a random skin and returns the skin's gender +// Parameters: gender - If a valid gender is passed, and not INVALID (-1) then pick a skin based on gender +int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender) +{ + int type = 0, rnd; + + strcpy(skin, "male/grunt"); // Default skin + + // Size of each collection of skins + const int actionmale = 30; + const int actionrally = 6; + const int aqmarine = 8; + const int female = 22; + const int male = 88; + const int messiah = 26; + const int sas = 25; + const int sydney = 2; + const int terror = 25; + + // When NOT forcing a gender using + if (force_gender == INVALID) + { + int rng_type = rand() % 2; // Randomize which randomizer we use. Either by [1] Collection Size, or [2] Random Type. + + // -- [1] Collection Size -- + // Since each collection varies in size, our random choice takes into account the size of the collection of skins. + // Collections with the most skins have a greater chance to be chosen. + if (rng_type == 0) + { + int sum = actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney + terror; + int rnd = (rand() % sum) + 1; // Random number from total + + if (rnd <= actionmale) + type = 0; // actionmale + else if (rnd <= (actionmale + actionrally)) + type = 1; // actionrally + else if (rnd <= (actionmale + actionrally + aqmarine)) + type = 2; // aqmarine + else if (rnd <= (actionmale + actionrally + aqmarine + female)) + type = 3; // female + else if (rnd <= (actionmale + actionrally + aqmarine + female + male)) + type = 4; // male + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah)) + type = 5; // messiah + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas)) + type = 6; // sas + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney)) + type = 7; // sydney + else + type = 8; // terror + } + // -- [2] Random Type -- + else // Alternatively we could just randomly pick from one of the collections regardless of how many skins it has + { + type = rand() % 9; // Actionmale, actionrally, aqmarine, female, male, messiah, sas, sydney, terror + } + } + else // Forcing a gender + { + // (type == 0) // Actionmale -- male + // (type == 1) // Actionrally -- female + // (type == 2) // Aqmarine -- male + // (type == 3) // Female -- female + // (type == 4) // Male -- male + // (type == 5) // Messiah -- male + // (type == 6) // Sas -- male + // (type == 7) // Sydney -- female + // (type == 8) // Terror -- male + + if (force_gender == GENDER_MALE) + { + int rnd_gen_skin = rand() % 6; // Pick one of the male skins + + if (rnd_gen_skin == 0) type = 0; // Actionmale + else if (rnd_gen_skin == 1) type = 2; // Aqmarine + else if (rnd_gen_skin == 2) type = 4; // Male + else if (rnd_gen_skin == 3) type = 5; // Messiah + else if (rnd_gen_skin == 4) type = 6; // Sas + else type = 8; // Terror + } + else // GENDER_FEMALE + { + int rnd_gen_skin = rand() % 2; // Pick one of the female skins + + if (rnd_gen_skin == 0) type = 1; // Actionrally + else type = 3; // Female + // Skipping Sydney because skin has no VWEP + } + } + + if (type == 0) // Actionmale + { + rnd = rand() % actionmale; + switch (rnd) + { + case 0: sprintf(skin, "actionmale/agent"); break; + case 1: sprintf(skin, "actionmale/aqgthugboss"); break; + case 2: sprintf(skin, "actionmale/axef"); break; + case 3: sprintf(skin, "actionmale/badmutha"); break; + case 4: sprintf(skin, "actionmale/BlackStars"); break; + case 5: sprintf(skin, "actionmale/blood"); break; + case 6: sprintf(skin, "actionmale/blucedojo"); break; + case 7: sprintf(skin, "actionmale/bluceree"); break; + case 8: sprintf(skin, "actionmale/Bruce_Wayne"); break; + case 9: sprintf(skin, "actionmale/castor"); break; + case 10: sprintf(skin, "actionmale/chellobeta"); break; + case 11: sprintf(skin, "actionmale/chucky"); break; + case 12: sprintf(skin, "actionmale/crip"); break; + case 13: sprintf(skin, "actionmale/ctf_b"); break; + case 14: sprintf(skin, "actionmale/ctf_r"); break; + case 15: sprintf(skin, "actionmale/dafist"); break; + case 16: sprintf(skin, "actionmale/dubeta"); break; + case 17: sprintf(skin, "actionmale/invince"); break; + case 18: sprintf(skin, "actionmale/jewels"); break; + case 19: sprintf(skin, "actionmale/jungseal"); break; + case 20: sprintf(skin, "actionmale/killer"); break; + case 21: sprintf(skin, "actionmale/leonreno"); break; + case 22: sprintf(skin, "actionmale/lfc"); break; + case 23: sprintf(skin, "actionmale/morpheus"); break; + case 24: sprintf(skin, "actionmale/RCMP"); break; + case 25: sprintf(skin, "actionmale/scarf"); break; + case 26: sprintf(skin, "actionmale/scarfchain"); break; + case 27: sprintf(skin, "actionmale/seagull"); break; + case 28: sprintf(skin, "actionmale/shinobi"); break; + case 29: sprintf(skin, "actionmale/swat"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 1) // Actionrally + { + rnd = rand() % actionrally; + switch (rnd) + { + case 0: sprintf(skin, "actionrally/ctf_b"); break; + case 1: sprintf(skin, "actionrally/ctf_r"); break; + case 2: sprintf(skin, "actionrally/modsquad"); break; + case 3: sprintf(skin, "actionrally/natasha"); break; + case 4: sprintf(skin, "actionrally/pulp"); break; + case 5: sprintf(skin, "actionrally/trinity"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 2) // Aqmarine + { + rnd = rand() % aqmarine; + switch (rnd) + { + case 0: sprintf(skin, "aqmarine/aquamarine"); break; + case 1: sprintf(skin, "aqmarine/centurion"); break; + case 2: sprintf(skin, "aqmarine/desert"); break; + case 3: sprintf(skin, "aqmarine/marine1"); break; + case 4: sprintf(skin, "aqmarine/marine2"); break; + case 5: sprintf(skin, "aqmarine/urban"); break; + case 6: sprintf(skin, "aqmarine/usmc"); break; + case 7: sprintf(skin, "aqmarine/woods"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 3) // Female + { + rnd = rand() % female; + switch (rnd) + { + case 0: sprintf(skin, "female/ctf_b"); break; + case 1: sprintf(skin, "female/ctf_r"); break; + case 2: sprintf(skin, "female/kw_aqua"); break; + case 3: sprintf(skin, "female/kw_black"); break; + case 4: sprintf(skin, "female/kw_blue"); break; + case 5: sprintf(skin, "female/kw_green"); break; + case 6: sprintf(skin, "female/kw_pink"); break; + case 7: sprintf(skin, "female/kw_red"); break; + case 8: sprintf(skin, "female/kw_white"); break; + case 9: sprintf(skin, "female/kw_yellow"); break; + case 10: sprintf(skin, "female/leeloop"); break; + case 11: sprintf(skin, "female/sarah_ohconnor"); break; + case 12: sprintf(skin, "female/tankgirl"); break; + + // baseq2 skins (No longer supplied with Aqtion) + case 13: sprintf(skin, "female/athena"); break; + case 14: sprintf(skin, "female/brianna"); break; + case 15: sprintf(skin, "female/cobalt"); break; + case 16: sprintf(skin, "female/doomgal"); break; + case 17: sprintf(skin, "female/ensign"); break; + case 18: sprintf(skin, "female/jezebel"); break; + case 19: sprintf(skin, "female/jungle"); break; + case 20: sprintf(skin, "female/venus"); break; + case 21: sprintf(skin, "female/voodoo"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 4) // Male + { + rnd = rand() % male; + switch (rnd) + { + case 0: sprintf(skin, "male/{oz}"); break; + case 1: sprintf(skin, "male/adidas"); break; + case 2: sprintf(skin, "male/aqgthug"); break; + case 3: sprintf(skin, "male/aqgvillain"); break; + case 4: sprintf(skin, "male/austin2"); break; + case 5: sprintf(skin, "male/babarracuda"); break; + case 6: sprintf(skin, "male/beastdl"); break; + case 7: sprintf(skin, "male/bluebeard"); break; + case 8: sprintf(skin, "male/blues"); break; + case 9: sprintf(skin, "male/boba"); break; + case 10: sprintf(skin, "male/borya"); break; + case 11: sprintf(skin, "male/bravo"); break; + case 12: sprintf(skin, "male/brucelee"); break; + case 13: sprintf(skin, "male/bruces"); break; + case 14: sprintf(skin, "male/chow"); break; + case 15: sprintf(skin, "male/chowda"); break; + case 16: sprintf(skin, "male/Clan_UNA"); break; + case 17: sprintf(skin, "male/conan"); break; + case 18: sprintf(skin, "male/coolio"); break; + case 19: sprintf(skin, "male/cop"); break; + case 20: sprintf(skin, "male/copper"); break; + case 21: sprintf(skin, "male/ctf_b"); break; + case 22: sprintf(skin, "male/ctf_g"); break; + case 23: sprintf(skin, "male/ctf_r"); break; + case 24: sprintf(skin, "male/ctf_y"); break; + case 25: sprintf(skin, "male/cyrus"); break; + case 26: sprintf(skin, "male/dashaft"); break; + case 27: sprintf(skin, "male/deadpl"); break; + case 28: sprintf(skin, "male/elway"); break; + case 29: sprintf(skin, "male/GreenBeret"); break; + case 30: sprintf(skin, "male/grunt"); break; + case 31: sprintf(skin, "male/habs"); break; + case 32: sprintf(skin, "male/hall"); break; + case 33: sprintf(skin, "male/hellspawn"); break; + case 34: sprintf(skin, "male/hollywood"); break; + case 35: sprintf(skin, "male/homer"); break; + case 36: sprintf(skin, "male/hulk2"); break; + case 37: sprintf(skin, "male/image"); break; + case 38: sprintf(skin, "male/indy"); break; + case 39: sprintf(skin, "male/invince"); break; + case 40: sprintf(skin, "male/jax-jaguar"); break; + case 41: sprintf(skin, "male/jdredd"); break; + case 42: sprintf(skin, "male/jewels"); break; + case 43: sprintf(skin, "male/jules"); break; + case 44: sprintf(skin, "male/kgb"); break; + case 45: sprintf(skin, "male/kw_aqua"); break; + case 46: sprintf(skin, "male/kw_black"); break; + case 47: sprintf(skin, "male/kw_blue"); break; + case 48: sprintf(skin, "male/kw_green"); break; + case 49: sprintf(skin, "male/kw_orange"); break; + case 50: sprintf(skin, "male/kw_pink"); break; + case 51: sprintf(skin, "male/kw_red"); break; + case 52: sprintf(skin, "male/kw_white"); break; + case 53: sprintf(skin, "male/kw_yellow"); break; + case 54: sprintf(skin, "male/leaf"); break; + case 55: sprintf(skin, "male/leservoircat"); break; + case 56: sprintf(skin, "male/mariom"); break; + case 57: sprintf(skin, "male/marsell"); break; + case 58: sprintf(skin, "male/mason"); break; + case 59: sprintf(skin, "male/MikeLowry"); break; + case 60: sprintf(skin, "male/mkrain"); break; + case 61: sprintf(skin, "male/mkrept"); break; + case 62: sprintf(skin, "male/mkscorp"); break; + case 63: sprintf(skin, "male/mksub"); break; + case 64: sprintf(skin, "male/Mohoney"); break; + case 65: sprintf(skin, "male/mr_t"); break; + case 66: sprintf(skin, "male/nut"); break; + case 67: sprintf(skin, "male/nwom"); break; + case 68: sprintf(skin, "male/optimus"); break; + case 69: sprintf(skin, "male/oz"); break; + case 70: sprintf(skin, "male/pimp"); break; + case 71: sprintf(skin, "male/police"); break; + case 72: sprintf(skin, "male/resdog"); break; + case 73: sprintf(skin, "male/rigs"); break; + case 74: sprintf(skin, "male/robber"); break; + case 75: sprintf(skin, "male/roger"); break; + case 76: sprintf(skin, "male/rredneck"); break; + case 77: sprintf(skin, "male/sabotage"); break; + case 78: sprintf(skin, "male/santa"); break; + case 79: sprintf(skin, "male/sdes"); break; + case 80: sprintf(skin, "male/shaft"); break; + case 81: sprintf(skin, "male/siris"); break; + case 82: sprintf(skin, "male/snowcamo"); break; + case 83: sprintf(skin, "male/Superman"); break; + case 84: sprintf(skin, "male/t-800m"); break; + case 85: sprintf(skin, "male/trmntr"); break; + case 86: sprintf(skin, "male/walter"); break; + case 87: sprintf(skin, "male/wcwsting3"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 5) // Messiah + { + rnd = rand() % messiah; + switch (rnd) + { + case 0: sprintf(skin, "messiah/axe"); break; + case 1: sprintf(skin, "messiah/badger"); break; + case 2: sprintf(skin, "messiah/blackman"); break; + case 3: sprintf(skin, "messiah/blade"); break; + case 4: sprintf(skin, "messiah/chow"); break; + case 5: sprintf(skin, "messiah/chowblue"); break; + case 6: sprintf(skin, "messiah/chowred"); break; + case 7: sprintf(skin, "messiah/cptahab"); break; + case 8: sprintf(skin, "messiah/crawler"); break; + case 9: sprintf(skin, "messiah/ctf_b"); break; + case 10: sprintf(skin, "messiah/ctf_r"); break; + case 11: sprintf(skin, "messiah/darkbeing"); break; + case 12: sprintf(skin, "messiah/evil_axe"); break; + case 13: sprintf(skin, "messiah/keaneo"); break; + case 14: sprintf(skin, "messiah/kindig"); break; + case 15: sprintf(skin, "messiah/leon"); break; + case 16: sprintf(skin, "messiah/mxr_neo"); break; + case 17: sprintf(skin, "messiah/neo"); break; + case 18: sprintf(skin, "messiah/outlaw"); break; + case 19: sprintf(skin, "messiah/robber"); break; + case 20: sprintf(skin, "messiah/scuba"); break; + case 21: sprintf(skin, "messiah/slug"); break; + case 22: sprintf(skin, "messiah/spectre"); break; + case 23: sprintf(skin, "messiah/suit"); break; + case 24: sprintf(skin, "messiah/TheCrow"); break; + case 25: sprintf(skin, "messiah/thedon"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 6) // Sas + { + rnd = rand() % sas; + switch (rnd) + { + case 0: sprintf(skin, "sas/aqmsas"); break; + case 1: sprintf(skin, "sas/atf"); break; + case 2: sprintf(skin, "sas/ctf_b"); break; + case 3: sprintf(skin, "sas/ctf_r"); break; + case 4: sprintf(skin, "sas/deathreat"); break; + case 5: sprintf(skin, "sas/fbi"); break; + case 6: sprintf(skin, "sas/grassland"); break; + case 7: sprintf(skin, "sas/grunt"); break; + case 8: sprintf(skin, "sas/mud"); break; + case 9: sprintf(skin, "sas/nithb"); break; + case 10: sprintf(skin, "sas/norse1"); break; + case 11: sprintf(skin, "sas/norse2"); break; + case 12: sprintf(skin, "sas/sas-urban"); break; + case 13: sprintf(skin, "sas/sas-woodland"); break; + case 14: sprintf(skin, "sas/sas"); break; + case 15: sprintf(skin, "sas/sas2"); break; + case 16: sprintf(skin, "sas/sasdc"); break; + case 17: sprintf(skin, "sas/sasdc2"); break; + case 18: sprintf(skin, "sas/sasjungle"); break; + case 19: sprintf(skin, "sas/saspolice"); break; + case 20: sprintf(skin, "sas/sasuc"); break; + case 21: sprintf(skin, "sas/sasUC2"); break; + case 22: sprintf(skin, "sas/sasurban"); break; + case 23: sprintf(skin, "sas/saswc"); break; + case 24: sprintf(skin, "sas/STARS"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 7) // Sydney + { + rnd = rand() % sydney; + switch (rnd) + { + case 0: sprintf(skin, "sydney/blonde"); break; + case 1: sprintf(skin, "sydney/sydney"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 8) // Terror + { + rnd = rand() % terror; + switch (rnd) + { + case 0: sprintf(skin, "terror/adf2"); break; + case 1: sprintf(skin, "terror/black cop"); break; + case 2: sprintf(skin, "terror/blue cop"); break; + case 3: sprintf(skin, "terror/csw black"); break; + case 4: sprintf(skin, "terror/csw blue"); break; + case 5: sprintf(skin, "terror/csw crazy"); break; + case 6: sprintf(skin, "terror/csw red"); break; + case 7: sprintf(skin, "terror/ctf_b"); break; + case 8: sprintf(skin, "terror/ctf_r"); break; + case 9: sprintf(skin, "terror/desertterr"); break; + case 10: sprintf(skin, "terror/fbiterr"); break; + case 11: sprintf(skin, "terror/j_mafia"); break; + case 12: sprintf(skin, "terror/jungleterr"); break; + case 13: sprintf(skin, "terror/kgb"); break; + case 14: sprintf(skin, "terror/mafia"); break; + case 15: sprintf(skin, "terror/mafia2"); break; + case 16: sprintf(skin, "terror/red cop"); break; + case 17: sprintf(skin, "terror/redterr2"); break; + case 18: sprintf(skin, "terror/rmafia"); break; + case 19: sprintf(skin, "terror/skyterr"); break; + case 20: sprintf(skin, "terror/stormtrooper"); break; + case 21: sprintf(skin, "terror/swat"); break; + case 22: sprintf(skin, "terror/swatsnipe"); break; + case 23: sprintf(skin, "terror/terror"); break; + case 24: sprintf(skin, "terror/urbanterr"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + + return GENDER_MALE; +} + +// Set the bot's userinfo (name, skin, hand, etc) +void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin) +{ + int gender = INVALID; + char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) + char skin[MAX_INFO_STRING]; + char userinfo[MAX_INFO_STRING]; + memset(userinfo, 0, sizeof(userinfo)); // Init userinfo + + gi.cvar_forceset(stat_logs->name, "0"); // Turning off stat collection since bots are enabled + + if (force_gender != INVALID) // Use previous gender + { + gender = force_gender; + } + if (force_skin != NULL) // Use previous skin + { + Info_SetValueForKey(userinfo, "skin", force_skin); + } + + // Teamplay and 3TEAMS + if (teamplay->value && team) // TEAM1, TEAM2, or TEAM3 + { + // Figure out the gender based on the team skins + char* femaleSkinDirs[] = { "actionrally", "female", "sydney" }; + char* maleSkinsDirs[] = { "actionmale", "aqmarine", "male", "messiah", "sas", "terror" }; + for (int i = 0; i < sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, femaleSkinDirs[i]) != NULL) + { + gender = GENDER_FEMALE; + break; + } + } + if (gender == INVALID) + { + for (int i = 0; i < sizeof(maleSkinsDirs) / sizeof(maleSkinsDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, maleSkinsDirs[i]) != NULL) + { + gender = GENDER_MALE; + break; + } + } + } + if (gender == INVALID) // Couldn't find skin gender (perhaps server is using custom skins) + { + if (rand() % 2 == 0) // So just randomize the skin gender + gender = GENDER_MALE; + else + gender = GENDER_FEMALE; + } + } + else // Deathmatch + { + if (force_skin == NULL) // Not forcing a skin, so pick one at random + { + if (gender == GENDER_MALE || gender == GENDER_FEMALE) + BOTLIB_RandomSkin(bot, skin, gender); // Set random skin based on predefined gender + else + gender = BOTLIB_RandomSkin(bot, skin, INVALID); // Set random skin and return its gender + Info_SetValueForKey(userinfo, "skin", skin); + } + } + + Q_strlcpy(name, "AqtionMan", 10); // Set a default name + if (force_name == NULL) // Not forcing a name, so pick one at random + { + BOTLIB_LoadRandomBotName(bot, name, gender); + Info_SetValueForKey(userinfo, "name", name); + } + else // Use previous name + { + Info_SetValueForKey(userinfo, "name", force_name); + } + + // Set the gender + if (gender == GENDER_MALE) + Info_SetValueForKey(userinfo, "gender", "male"); + else if (gender == GENDER_FEMALE) + Info_SetValueForKey(userinfo, "gender", "female"); + else + Info_SetValueForKey(userinfo, "gender", "none"); + + //Set userinfo: hand, spec + Info_SetValueForKey(userinfo, "hand", "2"); // bot is center handed for now! + Info_SetValueForKey(userinfo, "spectator", "0"); // NOT a spectator + + ClientConnect(bot, userinfo); +} + +/////////////////////////////////////////////////////////////////////// +// Called by PutClient in Server to actually release the bot into the game +// Keep from killin' each other when all spawned at once +/////////////////////////////////////////////////////////////////////// +void BOTLIB_HoldSpawn(edict_t* self) +{ + if (!KillBox(self)) + { // could't spawn in? + } + + gi.linkentity(self); + + self->think = BOTAI_Think; + self->nextthink = level.framenum + 1; + + // send effect + gi.WriteByte(svc_muzzleflash); + gi.WriteShort(self - g_edicts); + gi.WriteByte(MZ_LOGIN); + gi.multicast(self->s.origin, MULTICAST_PVS); + gi.bprintf(PRINT_MEDIUM, "%s entered the game\n", self->client->pers.netname); +} + +// Modified version of id's code +void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) +{ + bot->is_bot = true; + bot->bot.bot_type = BOT_TYPE_BOTLIB; + bot->think = BOTLIB_Think; + bot->nextthink = level.framenum + 1; + + PutClientInServer(bot); + JoinTeam(bot, team, true); +} + +// Spawn a bot +void ClientBeginDeathmatch(edict_t* ent); +edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin) //char* userinfo) +{ + edict_t* bot = BOTLIB_FindFreeEntity(); + if (!bot) + { + gi.bprintf(PRINT_MEDIUM, "Server is full! To allow more players set the console command 'maxclients' higher than %d\n", game.maxclients); + return NULL; + } + + bot->is_bot = true; + bot->yaw_speed = 1000; // aiming degrees per second + + if (team == TEAM1) + bot_connections.total_team1++; + else if (team == TEAM2) + bot_connections.total_team2++; + else if (team == TEAM3) + bot_connections.total_team3++; + + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); // includes ClientConnect + + ClientBeginDeathmatch(bot); + + BOTLIB_PutClientInServer(bot, true, team); + + //rekkie -- Fake Bot Client -- s + // Set the average ping this bot will see + int rng_ping_range = rand() % 5; + if (rng_ping_range == 0) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard + else if (rng_ping_range <= 3) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger + else + bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard + gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //rekkie -- Fake Bot Client -- e + + return bot; +} + + +// Remove a bot by name or all bots +void BOTLIB_RemoveBot(char* name) +{ + int i; + qboolean freed = false; + edict_t* bot; + + // =================================================== + // Kick by NAME, ALL bots, or by team num + // =================================================== + if (strlen(name)) + { + qboolean remove_all = (Q_stricmp(name, "all") == 0) ? true : false; + int find_team = (strlen(name) == 1) ? atoi(name) : 0; // Kick by team + + //if (name!=NULL) + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + bot->health = 0; + player_die(bot, bot, bot, 100000, vec3_origin); + // don't even bother waiting for death frames + //bot->deadflag = DEAD_DEAD; + //bot->inuse = false; + freed = true; + ClientDisconnect(bot); + //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + if (!remove_all) + break; + } + } + } + } + + // =================================================== + // Kick random bot + // =================================================== + int bot_to_kick = 0; + int num_bots = 0; + int bot_count = 0; + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot) + { + num_bots++; + } + } + } + if (num_bots == 0) + { + return; + } + else if (num_bots > 1) + { + bot_to_kick = (rand() % num_bots); + } + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot && bot_to_kick == bot_count) + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + bot->health = 0; + player_die(bot, bot, bot, 100000, vec3_origin); + // don't even bother waiting for death frames + //bot->deadflag = DEAD_DEAD; + //bot->inuse = false; + freed = true; + ClientDisconnect(bot); + //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + break; + } + if (bot->is_bot) + bot_count++; + } + } + + if (!freed) + gi.bprintf(PRINT_MEDIUM, "No bot removed\n", name); +} + +// Remove a bot by team +// Conditions: Bot must be dead or joined a team during an ongoing round +void BOTLIB_RemoveTeamplayBot(int team) +{ + int i; + edict_t* bot; + + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) // Ent in use + { + if (bot->is_bot) // Is a bot + { + // Only kick when the bot isn't actively in a match + //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) + if (bot->client->resp.team == team) // && team_round_going == 0) + { + //if (random() < 0.20) // Randomly kick a bot + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + if (team == TEAM1) + bot_connections.total_team1--; + else if (team == TEAM2) + bot_connections.total_team2--; + else if (team == TEAM3) + bot_connections.total_team3--; + + if (bot->health) + player_die(bot, bot, bot, 100000, vec3_origin); + ClientDisconnect(bot); + break; + } + } + } + } + } +} + +// Change bot [from team] ==> [to team] +// Conditions: Bot must be in a team. Change occurs after round ends. +void BOTLIB_ChangeBotTeam(int from_team, int to_team) +{ + int i; + edict_t* bot; + + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) // Ent in use + { + if (bot->is_bot) // Is a bot + { + // Only kick when the bot isn't actively in a match + //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) + if (bot->client->resp.team == from_team && team_round_going == 0) + { + if (from_team == TEAM1) bot_connections.total_team1--; + else if (from_team == TEAM2) bot_connections.total_team2--; + else if (from_team == TEAM3) bot_connections.total_team3--; + + if (to_team == TEAM1) bot_connections.total_team1++; + else if (to_team == TEAM2) bot_connections.total_team2++; + else if (to_team == TEAM3) bot_connections.total_team3++; + + JoinTeam(bot, to_team, true); + break; + } + } + } + } +} + +//rekkie -- DEV_1 -- s + +// Returns how many bots and players are playing (returned via struct). +void BOTLIB_GetTotalPlayers(bot_connections_t* bc) +{ + edict_t* ent; + + bc->total_bots = 0; + bc->total_humans = 0; + bc->total_humans_playing = 0; + + bc->total_team1 = 0; + bc->total_team2 = 0; + bc->total_team3 = 0; + + bc->spec_bots = 0; + bc->team1_bots = 0; + bc->team2_bots = 0; + bc->team3_bots = 0; + + bc->spec_humans = 0; + bc->team1_humans = 0; + bc->team2_humans = 0; + bc->team3_humans = 0; + + for (int i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[1 + i]; + if (ent && ent->inuse) + { + if (ent->is_bot) + { + bc->total_bots++; + + if (teamplay->value) + { + if (ent->client->resp.team == NOTEAM) + { + bc->spec_bots++; + } + else if (ent->client->resp.team == TEAM1) + { + bc->team1_bots++; + bc->total_team1++; + } + else if (ent->client->resp.team == TEAM2) + { + bc->team2_bots++; + bc->total_team2++; + } + else if (ent->client->resp.team == TEAM3) + { + bc->team3_bots++; + bc->total_team3++; + } + } + else if (deathmatch->value) + { + if (ent->solid == SOLID_NOT) + bc->spec_bots++; + else + bc->team1_bots++; + } + } + else + { + bc->total_humans++; + + if (teamplay->value) + { + if (ent->client->resp.team == NOTEAM) + bc->spec_humans++; + else if (ent->client->resp.team == TEAM1) + { + bc->team1_humans++; + bc->total_team1++; + } + else if (ent->client->resp.team == TEAM2) + { + bc->team2_humans++; + bc->total_team2++; + } + else if (ent->client->resp.team == TEAM3) + { + bc->team3_humans++; + bc->total_team3++; + } + } + else if (deathmatch->value) + { + if (ent->solid == SOLID_NOT) + bc->spec_humans++; + else + bc->team1_humans++; + } + } + } + } + bc->total_humans_playing = bc->total_humans - bc->spec_humans; +} + +// Bots auto join teams to keep them equal in teamplay. +// Empty servers have a bot join a team upon a player connecting. +int bot_teamcheckfrequency = 0; +int bot_teamchangefrequency = 0; +void BOTLIB_CheckBotRules(void) +{ + if (matchmode->value) // Bots never allowed in matchmode + return; + + if (ctf->value) + { + BOTLIB_Update_Flags_Status(); + } + + // check these rules every 1.0 seconds... (aka 10 frames) -- this will be faster if server hz is faster + if ((++bot_teamcheckfrequency % 10) != 0) + { + return; + } + else + { + bot_teamcheckfrequency = 0; + //Com_Printf("%s ltk_teamcheckfrequency\n", __func__); + } + + BOTLIB_GetTotalPlayers(&bot_connections); + //Com_Printf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots); + + if (bot_connections.tried_adding_prev_bots == false) + { + bot_connections.tried_adding_prev_bots = true; // Only called once per map + BOTLIB_AddBotsFromPreviousMap(100); // Try get bots from a previous map + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + + if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; // Turn on team balance if bot_maxteam is used + + // ======================================================================================================== + // Manually add bots to a select team + // sv bots + if (teamplay->value && bot_connections.auto_balance_bots == false) + { + // Sanity check + if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; + if (bot_connections.desire_team2 < 0) bot_connections.desire_team2 = 0; + if (bot_connections.desire_team3 < 0) bot_connections.desire_team3 = 0; + + // Remove bots + if (bot_connections.desire_team1 < bot_connections.team1_bots) + { + while (bot_connections.team1_bots != bot_connections.desire_team1) + { + BOTLIB_RemoveTeamplayBot(TEAM1); + bot_connections.team1_bots--; + } + } + if (bot_connections.desire_team2 < bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) + { + BOTLIB_RemoveTeamplayBot(TEAM2); + bot_connections.team2_bots--; + } + } + if (bot_connections.desire_team3 < bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) + { + BOTLIB_RemoveTeamplayBot(TEAM3); + bot_connections.team3_bots--; + } + } + + // Add bots + if (bot_connections.desire_team1 > bot_connections.team1_bots) + { + while (bot_connections.team1_bots != bot_connections.desire_team1) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team1 = bot_connections.team1_bots; + break; + } + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + if (bot_connections.desire_team2 > bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team2 = bot_connections.team2_bots; + break; + } + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + if (bot_connections.desire_team3 > bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team3 = bot_connections.team3_bots; + break; + } + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + + /* + // Sanity check + if (bot_connections.desire_team1 < -1) bot_connections.desire_team1 = 0; + if (bot_connections.desire_team2 < -1) bot_connections.desire_team2 = 0; + if (bot_connections.desire_team3 < -1) bot_connections.desire_team3 = 0; + + int count = (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); + if (count == -1) // Remove all bots from team + { + // Remove all bots from team + if (bot_connections.desire_team1 == -1) + { + while (bot_connections.team1_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM1); + bot_connections.team1_bots--; + } + } + else if (bot_connections.desire_team2 == -1) + { + while (bot_connections.team2_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM2); + bot_connections.team2_bots--; + } + } + else if (use_3teams->value && bot_connections.desire_team3 == -1) + { + while (bot_connections.team3_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM3); + bot_connections.team3_bots--; + } + } + } + if (count > 0) // Add bots to team + { + for (int i = 0; i < count; i++) + { + if (bot_connections.total_bots + bot_connections.total_humans + 1 < maxclients->value) // Max allowed + { + if (bot_connections.desire_team1) + { + if (bot_connections.team1_bots < bot_connections.desire_team1) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.team1_bots > bot_connections.desire_team1) + BOTLIB_RemoveTeamplayBot(TEAM1); + else + break; + } + else if (bot_connections.desire_team2) + { + if (bot_connections.team2_bots < bot_connections.desire_team2) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else if (bot_connections.team2_bots > bot_connections.desire_team2) + BOTLIB_RemoveTeamplayBot(TEAM2); + else + break; + } + else if (use_3teams->value && bot_connections.desire_team3) + { + if (bot_connections.team3_bots < bot_connections.desire_team3) + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + else if (bot_connections.team3_bots > bot_connections.desire_team3) + BOTLIB_RemoveTeamplayBot(TEAM3); + else + break; + } + } + } + bot_connections.desire_team1 = 0; + bot_connections.desire_team2 = 0; + bot_connections.desire_team3 = 0; + return; + } + */ + } + // ======================================================================================================== + + + // Auto balance bots + if (bot_connections.auto_balance_bots == false) + return; + + + //bot_connections.desire_bots += (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); + + // Percent chance to add/remove bots every 5 seconds... inc/dec = [min: 1, max: bot_maxteam] + if ((++bot_teamchangefrequency % 5) == 0) + { + bot_teamchangefrequency = 0; + + if (bot_maxteam->value) + { + //scale_up + float percent = (float)bot_connections.total_bots / bot_maxteam->value; + if (bot_connections.scale_dn == false && bot_connections.scale_up == false) + bot_connections.scale_up = true; + + float min_percent = 1 / bot_maxteam->value; + + // Reached top end of the scale, so work back down + if (bot_connections.scale_up && percent >= 1.0) + { + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } + // Reached bottom end of the scale, or randomly at 50%, so work back up + else if (bot_connections.scale_dn && (percent <= min_percent || random() < 0.05)) // Never drop below this percent + { + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } + + // Decrease bots + if (bot_connections.scale_dn && random() < 0.050) + bot_connections.desire_bots--; + + // Increase bots + if (bot_connections.scale_up && random() < 0.250) + bot_connections.desire_bots++; + + // Don't let bots be greater than bot_maxteam + if ((float)bot_connections.desire_bots > bot_maxteam->value) + bot_connections.desire_bots--; + + // Never go below 1 bot when bot_maxteam is active + if (bot_connections.desire_bots < 1) + bot_connections.desire_bots = 1; + } + } + + + // Always leave room for another player to join + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + bot_connections.desire_bots = (maxclients->value - 1); + + // Sanity check - safety limits + //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + + //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); + int bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + + // Shuffle bots around + //if (teamplay->value && bots_to_spawn == 0) + if (teamplay->value && bot_connections.auto_balance_bots) + { + if (use_3teams->value && (bot_connections.total_team1 != bot_connections.total_team2 || bot_connections.total_team1 != bot_connections.total_team3 || bot_connections.total_team2 != bot_connections.total_team3)) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1 && bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + if (abs(bot_connections.total_team1 - bot_connections.total_team3) > 1 && bot_connections.total_team1 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM1); + + if (abs(bot_connections.total_team2 - bot_connections.total_team1) > 1 && bot_connections.total_team2 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + if (abs(bot_connections.total_team2 - bot_connections.total_team3) > 1 && bot_connections.total_team2 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM2); + + if (abs(bot_connections.total_team3 - bot_connections.total_team1) > 1 && bot_connections.total_team3 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM3); + if (abs(bot_connections.total_team3 - bot_connections.total_team2) > 1 && bot_connections.total_team3 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM3); + } + else if (bot_connections.total_team1 != bot_connections.total_team2) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1) + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + else + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + } + } + + } + + // Remove ALL bots + if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) + { + BOTLIB_RemoveBot("ALL"); + bot_connections.total_team1 = 0; + bot_connections.total_team2 = 0; + bot_connections.total_team3 = 0; + } + + // Remove bots + //if (bot_connections.total_bots > bot_connections.desire_bots) + if (bots_to_spawn < 0) + { + bots_to_spawn = abs(bots_to_spawn); + for (int i = 0; i < bots_to_spawn; i++) + { + if (teamplay->value) // Remove a bot from team with the highest player count + { + if (use_3teams->value) + { + if (bot_connections.total_team1 >= bot_connections.total_team2 && bot_connections.total_team1 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team2 >= bot_connections.total_team1 && bot_connections.total_team2 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM2); + else if (bot_connections.total_team3 >= bot_connections.total_team1 && bot_connections.total_team3 >= bot_connections.total_team2) + + BOTLIB_RemoveTeamplayBot(TEAM3); + else // If all teams are equal size, randomly remove bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (choice == 1) + BOTLIB_RemoveTeamplayBot(TEAM2); + else + BOTLIB_RemoveTeamplayBot(TEAM3); + } + } + else + { + if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM2); + else // If both teams are equal size, randomly remove bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else + BOTLIB_RemoveTeamplayBot(TEAM2); + } + } + } + else // DM + BOTLIB_RemoveBot(""); + } + return; + } + //else if (bot_connections.total_bots < bots_to_spawn) + else if (bots_to_spawn > 0) + { + for (int i = 0; i < bots_to_spawn; i++) + { + if (teamplay->value) // Add a bot to a team with the lowest player count + { + if (use_3teams->value) // 3TEAMS + { + if (bot_connections.total_team1 <= bot_connections.total_team2 && bot_connections.total_team1 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team2 <= bot_connections.total_team1 && bot_connections.total_team2 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else if (bot_connections.total_team3 <= bot_connections.total_team1 && bot_connections.total_team3 <= bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + else // If all teams are equal size, randomly add bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (choice == 1) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + } + } + else // TP + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else // If both teams are equal size, randomly add bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + } + } + } + else // DM + BOTLIB_SpawnBot(0, INVALID, NULL, NULL); + } + } +} +//rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/botlib/botlib_spawnpoints.c b/src/action/botlib/botlib_spawnpoints.c new file mode 100644 index 000000000..6a00665d7 --- /dev/null +++ b/src/action/botlib/botlib_spawnpoints.c @@ -0,0 +1,483 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Free the memory +void DC_Free_Spawnpoints(void) +{ + if (dc_sp_count && dc_sp != NULL) + { + free(dc_sp); + dc_sp = NULL; // Nullify dangling pointer + } +} +void DC_Init_Spawnpoints(void) +{ + dc_sp_edit = false; // Set edit mode to false + dc_sp_count = 0; // Total spawn points + dc_sp = NULL; +} +// Add and display a new custom spawn point at the location given +void DC_Remove_Spawnpoint(edict_t* self) +{ + if (dc_sp_count && dc_sp != NULL) + { + // Try looking for a dc_spawnpoint nearby and move it to an unused state + const int search_dist = 256; + vec3_t v; + float dist; + trace_t tr; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) + { + VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player + dist = VectorLength(v); + if (dist < search_dist) // Found a spawn close enough + { + // Ensure we have LoS + tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Search the dc_sp list for this spot and update it + for (int i = 0; i < dc_sp_count; i++) + { + // Test the spot origin against the dc_sp[i].origin + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + // Edit the existing spawn point + dc_sp[i].inuse = false; // Change state to unused + + // Mark spot as deleted + spot->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; + + // Free spot + spot->nextthink = level.framenum + 5 * HZ; + spot->think = G_FreeEdict; + + return; + } + } + } + } + } + } +} +// Add and display a new custom spawn point at the location given +void DC_Add_Spawnpoint(edict_t* self) +{ + if (dc_sp_count + 1 > DC_SP_LIMIT) + { + Com_Printf("%s could not add spawnpoint, limit is %d!\n", __func__, DC_SP_LIMIT); + return; + } + + // Ensure we're in edit mode + if (dc_sp_edit == false) + BOTLIB_Show_Spawnpoints(); + + if (dc_sp_count && dc_sp != NULL) + { + // Try looking for an unused spot + for (int i = 0; i < dc_sp_count; i++) + { + if (dc_sp[i].inuse == false) // Recycle deleted spot + { + // Edit the existing spawn point + dc_sp[i].inuse = true; + VectorCopy(self->s.origin, dc_sp[i].origin); + VectorCopy(self->s.angles, dc_sp[i].angles); + + // Make a visible version + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes + ent->think = G_FreeEdict; + VectorCopy(self->s.origin, ent->s.origin); + VectorCopy(self->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + + return; + } + } + + // Otherwise, try looking for a dc_spawnpoint nearby and move it to the player + const int search_dist = 256; + vec3_t v; + float dist; + trace_t tr; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) + { + VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player + dist = VectorLength(v); + if (dist < search_dist) // Found a spawn close enough to move + { + // Ensure we have LoS + tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Search the dc_sp list for this spot and update it + for (int i = 0; i < dc_sp_count; i++) + { + // Test the spot origin against the dc_sp[i].origin + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + // Edit the existing spawn point + dc_sp[i].inuse = true; // If it was deleted, make it used again + VectorCopy(self->s.origin, dc_sp[i].origin); + VectorCopy(self->s.angles, dc_sp[i].angles); + + // Move the spot + gi.unlinkentity(spot); + VectorCopy(self->s.origin, spot->s.origin); + VectorCopy(self->s.angles, spot->s.angles); + gi.linkentity(spot); + + return; + } + } + } + } + } + } + + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes + ent->think = G_FreeEdict; + VectorCopy(self->s.origin, ent->s.origin); + VectorCopy(self->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + + // Grow memory + if (dc_sp_count == 0) + { + dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t)); + if (dc_sp == NULL) + { + Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); + return; + } + } + else + { + dc_sp_t* prev = dc_sp; + dc_sp = (dc_sp_t*)realloc(dc_sp, (dc_sp_count + 1) * sizeof(dc_sp_t)); + if (dc_sp == NULL) + { + free(prev); + prev = NULL; + Com_Printf("%s could not realloc dc_sp. Out of memory!\n", __func__); + return; + } + } + + // Add it + dc_sp[dc_sp_count].inuse = true; + VectorCopy(self->s.origin, dc_sp[dc_sp_count].origin); + VectorCopy(self->s.angles, dc_sp[dc_sp_count].angles); + + // Increment total SPs + dc_sp_count++; +} +// Find and add all the map spawn points +void DC_Get_Map_Spawnpoints(void) +{ + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + DC_Add_Spawnpoint(spot); +} +// Find and display (as visible ents) all spawn points (map and custom) +// Custom spawn points are green +// Deleted spawn points are red +void BOTLIB_Show_Spawnpoints(void) +{ + const int free_time = (60 * 1); // Free it after (60 seconds * 15) = 15 minutes + + // Only spawn them once per map + if (dc_sp_edit == false) + { + dc_sp_edit = true; + } + else + return; + + // Show map spawn points + qboolean is_custom_spot; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + // Skip adding custom spots that have been labled as "info_player_deathmatch" + // This occurs when loading custom spots from file, we set them as classname "info_player_deathmatch" + // so the spawn code can find them. However, because custom spots need editing, we don't want them 'visually' + // showing up as info_player_deathmatch, but rather as dc_spawnpoint which is handled later in this function. + is_custom_spot = false; + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + is_custom_spot = true; + break; + } + } + } + if (is_custom_spot) continue; // Skip adding this spot because its custom + + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_deathmath"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + while ((spot = G_Find(spot, FOFS(classname), "info_player_team1")) != NULL) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_team1"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + while ((spot = G_Find(spot, FOFS(classname), "info_player_team2")) != NULL) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_team2"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + // Show custom user spawn points + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(dc_sp[i].origin, ent->s.origin); + VectorCopy(dc_sp[i].angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + if (dc_sp[i].inuse == false) ent->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; // Mark as not in use + else ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + } + } +} +// Save SPs to file +void DC_Save_Spawnpoints() +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + short int i = 0; + byte version = DC_SP_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\spawnpoints\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".sp"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/spawnpoints/"); + strcat(filename, level.mapname); + strcat(filename, ".sp"); +#endif + + // Add the map spawn points + //DC_Get_Map_Spawnpoints(); + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write spawnpoint data to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing spawnpoint data to file...\n", __func__); + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version + fileSize += sizeof(int) * fwrite(&dc_sp_count, sizeof(int), 1, fOut); // Total spawnpoints + + // Write spawnpoint data + for (i = 0; i < dc_sp_count; i++) + { + fileSize += sizeof(qboolean) * fwrite(&dc_sp[i].inuse, sizeof(qboolean), 1, fOut); // In use + fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].origin, sizeof(vec3_t), 1, fOut); // Location + fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].angles, sizeof(vec3_t), 1, fOut); // Direction + } + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} +// Load SPs from file +void DC_Load_Spawnpoints() +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + short int i = 0; + byte version = 0; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\spawnpoints\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".sp"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/spawnpoints/"); + strcat(filename, level.mapname); + strcat(filename, ".sp"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists + { + Com_Printf("%s %s not found. The map has no custom spawnpoints\n", __func__, filename); + return; // Map has no spawnpoint data + } + else + { + Com_Printf("%s Reading SP file...\n", __func__); + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // AAS Version + if (version != DC_SP_VERSION) + { + Com_Printf("%s %s is from a previous version. File is incompatible.\n", __func__, filename); + fclose(fIn); + return; + } + } + + fileSize += sizeof(int) * fread(&dc_sp_count, sizeof(int), 1, fIn); // Total Nodes + + DC_Free_Spawnpoints(); //Soft map change. Free any existing spawnpoint memory used + + // Grow memory + if (dc_sp_count) + { + dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t) * dc_sp_count); + if (dc_sp == NULL) + { + Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); + fclose(fIn); + return; + } + } + + // Read SP data + for (i = 0; i < dc_sp_count; i++) + { + fileSize += sizeof(qboolean) * fread(&dc_sp[i].inuse, sizeof(qboolean), 1, fIn); // In use + fileSize += sizeof(vec3_t) * fread(&dc_sp[i].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(vec3_t) * fread(&dc_sp[i].angles, sizeof(vec3_t), 1, fIn); // Direction + } + + // Show custom user spawn points + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + if (dc_sp[i].inuse) // Ignore deleted spawn spots + { + // Add the custom spawn points to the map + edict_t* ent; + ent = G_Spawn(); + ent->classname = "info_player_deathmatch"; // Make it findable + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->owner = ent; + VectorCopy(dc_sp[i].origin, ent->s.origin); + VectorCopy(dc_sp[i].angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + } + } + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); +} \ No newline at end of file diff --git a/src/action/botlib/botlib_weapons.c b/src/action/botlib/botlib_weapons.c new file mode 100644 index 000000000..79bb790b7 --- /dev/null +++ b/src/action/botlib/botlib_weapons.c @@ -0,0 +1,1237 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + + + +// Returns how much ammunition is loaded/chambered in a weapon -- this is *NOT* spare ammo +int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num) +{ + switch (item_num) + { + case MK23_NUM: + return self->client->mk23_rds; + case DUAL_NUM: + return self->client->dual_rds; + case MP5_NUM: + return self->client->mp5_rds; + case M4_NUM: + return self->client->m4_rds; + case M3_NUM: + return self->client->shot_rds; + case HC_NUM: + return self->client->cannon_rds; + case SNIPER_NUM: + return self->client->sniper_rds; + case KNIFE_NUM: + case GRENADE_NUM: + return self->client->inventory[ITEM_INDEX(FindItemByNum(item_num))]; + default: + return 0; + } + + return 0; +} + +// +qboolean BOTLIB_ChangeSpecialWeapon(edict_t* self, int item_num) +{ + int ammo_index; + gitem_t* ammo_item; + qboolean loaded = true; + qboolean clips = true; + + gitem_t* item = FindItemByNum(item_num); + + // Check if weapon is in inventory + if (self->client->inventory[ITEM_INDEX(item)] == 0) + return false; + + // Do we have spare ammo + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + + // Check loaded ammo + if (BOTLIB_CheckWeaponLoadedAmmo(self, item_num) == 0) + loaded = false; + + + // see if we're already using it + if (item == self->client->weapon) + { + if (!loaded && !clips) // Drop weapon if we've got no ammo + { + DropSpecialWeapon(self); + return false; + } + else if (!loaded) // Reload if we've got spare ammo + { + BOTLIB_Reload(self); + //Cmd_New_Reload_f(self); + return true; + } + else + return true; // Nothing to do, weapon is good to go + } + + // No ammo + if (!loaded && !clips) + { + DropSpecialWeapon(self); + return false; + } + + // Change to this weapon + self->client->newweapon = item; + ChangeWeapon(self); + return true; +} + +// Bot checks its weapons: reload, drop empty, or change weapon... before encountering an enemy +void BOTLIB_ReadyWeapon(edict_t* self) +{ + // Limit how often this is called + if (self->bot.last_weapon_change_time > level.framenum) + return; + self->bot.last_weapon_change_time = level.framenum + 6 * HZ; + //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); + + /* + // Random chance to pull out grenades + if ((rand() % 20) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) //ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) + { + self->client->pers.grenade_mode = 2; + return; + } + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + return; + + + // Random chance to just use knives + if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return; + } + if (self->client->weapon == FindItemByNum(KNIFE_NUM)) + return; + */ + + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return; + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; + } + + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return; +} + +// Choose the best weapon for the situation +qboolean BOTLIB_ChooseWeapon(edict_t* self) +{ + // Limit how often this is called + if (self->bot.last_weapon_change_time > level.framenum) + return true; + self->bot.last_weapon_change_time = level.framenum + 6 * HZ; + //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Don't change if weaapon is busy + if (self->client->weaponstate == WEAPON_DROPPING || self->client->weaponstate == WEAPON_BUSY) + return true; + + + // If not firing weapon + if (self->client->weaponstate == WEAPON_READY && self->client->weapon != FindItemByNum(HC_NUM) && self->client->weapon != FindItemByNum(M3_NUM)) + { + // Random chance to pull out grenades + if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(GRENADE_NUM) && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + + // Random chance to just use knives + //if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + if (self->enemy && self->bot.enemy_dist <= 512) // More included to use knives when enemy is closer + { + if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + } + else // Less included to use knives when enemy is far + { + if ((rand() % 5) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + } + } + + + if (self->enemy && self->bot.enemy_dist <= 1200) + { + // Let bots use the grenades if already using + if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, GRENADE_NUM)) + { + if (self->bot.enemy_dist > 800) + self->client->pers.grenade_mode = 2; + else if (self->bot.enemy_dist > 400) + self->client->pers.grenade_mode = 1; + else + self->client->pers.grenade_mode = 0; + return true; + } + // Let bots use the knives if already using + if (self->client->weapon == FindItemByNum(KNIFE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + // Let bots use the pistol if already using + if (self->client->weapon == FindItemByNum(MK23_NUM) && self->client->mk23_rds == self->client->mk23_max) + { + return true; + } + } + + /* + // Friendly fire after round should be fought with honor. + if( team_round_countdown && ff_afterround->value ) + { + if( ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) + return true; + if( ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) + return true; + if( ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME)) ) + { + self->client->pers.grenade_mode = ( self->bot.enemy_dist > 500) ? 1 : 0; + return true; + } + } + */ + + /* + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + { + if (self->client->weaponstate == WEAPON_ACTIVATING || self->client->weaponstate == WEAPON_FIRING) + return true; + } + */ + + // Force bots use grenades only + /* + if (ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) + { + self->client->pers.grenade_mode = 2; + return true; + } + */ + + if (self->enemy == NULL) + { + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + + return true; + } + + + // Extreme range + if (self->bot.enemy_dist > 1300) + { + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + } + + // Long range + if (self->bot.enemy_dist > 1000) + { + /* + // Random chance to pull out grenades + if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 6) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + + // Allow bots that picked throwing knives to use them at a distance + if (INV_AMMO(self, KNIFE_NUM) >= 2 && self->client->pers.chosenItem == FindItemByNum(KNIFE_NUM) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (INV_AMMO(self, SIL_NUM) && BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && self->client->bleeding == 0 + && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + // Longer range + if (self->bot.enemy_dist > 700) + { + /* + // Random chance to pull out grenades + if ((rand() % 2) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + if (INV_AMMO(self, SIL_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + } + + // Long range + if (self->bot.enemy_dist > 500) + { + /* + // Random chance to pull out grenades + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + // Medium range + if (self->bot.enemy_dist) + { + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + + /* + // Random chance to pull out grenades + if ((rand() % 10) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + + // Should punch as a last resort? + } + + // We have no weapon available for use. + //if (debug_mode) gi.bprintf(PRINT_HIGH, "%s: No weapon available...\n", self->client->pers.netname); + return false; +} + +void BOTLIB_Reload(edict_t* self) +{ + int ammo_index, rounds = 0; + gitem_t* ammo_item; + qboolean loaded = true; + qboolean clips = true; + qboolean using_special = false; // If bot is using high powered weapon + + // Limit how often this is called + if (self->bot.last_weapon_reload_time > level.framenum) + return; + self->bot.last_weapon_reload_time = level.framenum + 2 * HZ; + + if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME) + || self->client->weapon == FindItem(MP5_NAME) || self->client->weapon == FindItem(M4_NAME) + || self->client->weapon == FindItem(M3_NAME) || self->client->weapon == FindItem(HC_NAME) + || self->client->weapon == FindItem(SNIPER_NAME)) + { + // Do we have spare ammo + if (self->client->weapon->ammo) + { + ammo_item = FindItem(self->client->weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + + // Check ammo + if (self->client->weapon == FindItem(MK23_NAME)) + { + rounds = self->client->mk23_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + rounds = 0; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(DUAL_NAME)) + { + rounds = self->client->dual_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + rounds = 0; + if (rounds < 6) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(MP5_NAME)) + { + using_special = true; + rounds = self->client->mp5_rds; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(M4_NAME)) + { + using_special = true; + rounds = self->client->m4_rds; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(M3_NAME)) + { + //using_special = true; + rounds = self->client->shot_rds; + if (rounds < self->client->shot_max) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(HC_NAME)) + { + //using_special = true; + rounds = self->client->cannon_rds; + if (hc_single->value && self->client->pers.hc_mode) // Single barrel fire mode + { + if (rounds < 1) + loaded = false; + } + else if (rounds < 2) // Double barrel fire mode + loaded = false; + } + else if (self->client->weapon == FindItem(SNIPER_NAME)) + { + using_special = true; + rounds = self->client->sniper_rds; + if (rounds < self->client->sniper_max) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + + //Com_Printf("%s %s inv_ammo[%d] rnds[%d] loaded[%d] clips[%d]\n", __func__, self->client->pers.netname, self->client->inventory[ammo_index], rounds, loaded, clips); + + // No ammo - drop weapon + if (!loaded && !clips) + { + DropSpecialWeapon(self); + } + // Reload + else if (!loaded) + { + // If using high powered weapon that is out of ammo and bot has a full pistol, switch to pistol + if (rounds == 0 && using_special && self->bot.enemy_dist < 512 && self->client->mk23_rds == self->client->mk23_max)// && self->bot.see_enemies) + { + // Try switch to pistol instead of reloading current weapon + BOTLIB_ChangeSpecialWeapon(self, MK23_NUM); + return; + } + + self->client->reload_attempts += 2; // Reload twice in one attempt + + //Cmd_New_Reload_f(self); + } + } +} + +// If have sniper and not zoomed in, zoom in and return true. +// Returns false otherwise +qboolean BOTLIB_SniperZoom(edict_t* self) +{ + if (self->bot.last_sniper_zoom_time > level.framenum) + { + return true; + } + + if (self->client->weapon == FindItem(SNIPER_NAME) && (self->client->resp.sniper_mode != SNIPER_2X || self->client->desired_fov != SNIPER_FOV2)) + { + if (self->client->weaponstate != WEAPON_FIRING && self->client->weaponstate != WEAPON_BUSY && self->client->weaponstate != WEAPON_RELOADING + && !self->client->bandaging && !self->client->bandage_stopped) + { + //Com_Printf("%s %s sniper zoom X2 [%d]\n", __func__, self->client->pers.netname, level.framenum); + self->bot.last_sniper_zoom_time = level.framenum + 1 * HZ; + _SetSniper(self, 2); + return true; + } + } + + return false; +} + +//rekkie -- collecting weapons, items, ammo -- s + +// Checks if the item is on the map and can be picked up by the bot +// Instances where it cannot be picked up: it's been taken by another player, or it's just not on the map +qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum) +{ + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + if (ent->solid == SOLID_NOT) continue; + //Com_Printf("%s %s typeNum[%d] dist[%f]\n", __func__, ent->classname, ent->typeNum, VectorDistance(self->s.origin, ent->s.origin)); + if (ent->typeNum == typeNum) + return true; + } + + return false; +} + +// Do we need a primary weapon? +// Pass parameters: primary_weapon and secondary_weapon to get a return on the weapons the bot is carrying +// The secondary_weapon only returns if the bot is using a bandolier +qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + int weapon_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case MP5_NUM: + if (*primary_weapon == 0) + *primary_weapon = MP5_NUM; + else + *secondary_weapon = MP5_NUM; + weapon_count++; + break; + + case M4_NUM: + if (*primary_weapon == 0) + *primary_weapon = M4_NUM; + else + *secondary_weapon = M4_NUM; + weapon_count++; + break; + + case M3_NUM: + if (*primary_weapon == 0) + *primary_weapon = M3_NUM; + else + *secondary_weapon = M3_NUM; + weapon_count++; + break; + + case HC_NUM: + if (*primary_weapon == 0) + *primary_weapon = HC_NUM; + else + *secondary_weapon = HC_NUM; + weapon_count++; + break; + + case SNIPER_NUM: + if (*primary_weapon == 0) + *primary_weapon = SNIPER_NUM; + else + *secondary_weapon = SNIPER_NUM; + weapon_count++; + break; + } + + //Com_Printf("%s inventory[%d] = %d [%s]\n", __func__, i, self->client->inventory[i], itemlist[i].classname); + } + } + + if (band && weapon_count < 2) // Bandolier: desire two primary weapons + return true; + if (weapon_count < 1) // Desire at least one primary weapon + return true; + + return false; // Don't need a primary +} + +// Do we need a grendes? +qboolean BOTLIB_Need_Grenades(edict_t* self) +{ + int grens_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case GRENADE_NUM: + grens_count = self->client->inventory[i]; + break; + } + } + } + + if (band && grens_count < 4) // Bandolier: desire up to four greandes + return true; + if (grens_count < 1) // Desire at least one grenade + return true; + + return false; // Don't need grenades +} + +// Do we need a knives? +qboolean BOTLIB_Need_Knives(edict_t* self) +{ + int knives_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case KNIFE_NUM: + knives_count = self->client->inventory[i]; + break; + } + } + } + + if (band && knives_count < 20) // Bandolier: desire up to 20 knives + return true; + if (knives_count < 4) // Desire at least a few knives + return true; + + return false; // Don't need knives +} + +// Do we need a dual pistols? +qboolean BOTLIB_Need_Dual_MK23(edict_t* self) +{ + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case DUAL_NUM: + return false; // Don't need another pistol + break; + } + } + } + + return true; // Desire a pair of mk23 pistols +} + +// Do we need ammo for weapons we carry: mk23, primary, or secondary (if any) +// Pass parameters input: primary_weapon and secondary_weapon +// Returns ammo type +int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + gitem_t* weapon; + gitem_t* ammo_item; + int ammo_index = 0; + + if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) + { + weapon = FindItem(MP5_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_mp5mags) + { + // Check there's ammo for this weapon ready for pickup on the map + if (BOTLIB_CanTouchItem(self, MP5_ANUM)) + return MP5_ANUM; + } + } + if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) + { + weapon = FindItem(M4_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_m4mags) + { + if (BOTLIB_CanTouchItem(self, M4_ANUM)) + return M4_ANUM; + } + } + if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) + { + weapon = FindItem(M3_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_shells) + { + if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) + return SHELL_ANUM; + } + } + if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) + { + weapon = FindItem(HC_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_shells) + { + if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) + return SHELL_ANUM; + } + } + if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) + { + weapon = FindItem(SNIPER_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_sniper_rnds) + { + if (BOTLIB_CanTouchItem(self, SNIPER_ANUM)) + return SNIPER_ANUM; + } + } + + // Dual MK23 + weapon = FindItem(DUAL_NAME); + if (weapon) + { + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_pistolmags) + return DUAL_NUM; + } + + // MK23 + weapon = FindItem(MK23_NAME); + if (weapon) + { + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_pistolmags) + return MK23_ANUM; + } + + return 0; +} + +// Do we need a primary weapon? +// Pass parameters input: primary_weapon and secondary_weapon +// Returns a special best fit for the weapons used +int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + int special = 0; + int random = 0; + + // Figure out what special items we have + for (int i = 0; i < game.num_items; i++) + { + if (special) + break; + + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case SIL_NUM: + special = SIL_NUM; + break; + case SLIP_NUM: + special = SLIP_NUM; + break; + case BAND_NUM: + special = BAND_NUM; + break; + case KEV_NUM: + special = KEV_NUM; + break; + case LASER_NUM: + special = LASER_NUM; + break; + case HELM_NUM: + special = HELM_NUM; + break; + } + } + } + + // No special items, so lets pick one based on our weapons in inventory + if (special == 0) + { + if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) + { + random = rand() % 5; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else if (random == 3) + return LASER_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) + { + random = rand() % 5; + if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else if (random == 3) + return LASER_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) + { + random = rand() % 4; + if (random == 0) + return SLIP_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) + { + random = rand() % 4; + if (random == 0) + return SLIP_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) + { + random = rand() % 4; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else // Just get a random item + { + random = rand() % 6; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return SLIP_NUM; + else if (random == 2) + return BAND_NUM; + else if (random == 3) + return KEV_NUM; + else if (random == 4) + return LASER_NUM; + else + return HELM_NUM; + } + } + + return 0; // Don't need a special +} + +// Locate weapons and items +int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter) +{ + qboolean found = false; + edict_t* items[32]; // Items we find + int item_nodes[32]; // Keep track of the nodes near those items + int item_count = 0; // Total items we found + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t bmins = { 0,0,0 }; + vec3_t bmaxs = { 0,0,0 }; + + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + + /* + if (strcmp(ent->classname, "info_player_deathmatch") == 0) // Skip spawn points + continue; + + if (strcmp(ent->classname, "dead_body") == 0) // Skip dead bodies + continue; + + if (ent->solid == SOLID_NOT) + continue; + + if (strcmp(ent->classname, "medkit") == 0) + { + } + + //if (ent->spawnflags != 0) continue; + //if (ent->flags != 0) continue; + + //Com_Printf("%s %s item %s [%f %f %f] - sf[0x%x] - f[0x%x]\n", __func__, self->client->pers.netname, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2], ent->spawnflags, ent->flags); + + // Ent->Spawnflags + // DROPPED_ITEM 0x00010000 Item dropped by player + // DROPPED_PLAYER_ITEM 0x00020000 Item dropped when dead + // ITEM_TARGETS_USED 0x00040000 Item picked up from spawn spot + // + // Ent->Flags + // FL_RESPAWN 0x80000000 used for item respawning + //if (ent->spawnflags == ITEM_TARGETS_USED && ent->flags == FL_RESPAWN) // Skip weapon spawn spot when weapon has been picked up + // continue; + */ + + switch (ent->typeNum) { + case MK23_NUM: + case MP5_NUM: + case M4_NUM: + case M3_NUM: + case HC_NUM: + case SNIPER_NUM: + case DUAL_NUM: + case KNIFE_NUM: + case GRENADE_NUM: + case SIL_NUM: + case SLIP_NUM: + case BAND_NUM: + case KEV_NUM: + case LASER_NUM: + case HELM_NUM: + case MK23_ANUM: + case MP5_ANUM: + case M4_ANUM: + case SHELL_ANUM: + case SNIPER_ANUM: + + // If the item is in our list + found = false; + for (int j = 0; j < items_counter; j++) + { + if (ent->typeNum == items_to_get[j]) + found = true; + } + + // Only look at items we want + if (found && item_count < 32) + { + for (int j = 0; j < numnodes; j++) + { + VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world + VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world + + // If ent is touching a node + //if (BOTLIB_BoxIntersection(ent->absmin, ent->absmax, nodes[i].absmin, nodes[i].absmax)) + if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax)) + { + //trace_t tr = gi.trace(nodes[j].origin, mins, maxs, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); + trace_t tr = gi.trace(nodes[j].origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && BOTLIB_CanGotoNode(self, nodes[j].nodenum, false)) // Check if bot can nav to item + { + items[item_count] = ent; + item_nodes[item_count] = nodes[j].nodenum; + item_count++; + break; + } + else + { + //Com_Printf("%s %s could not visit node [%d] for item %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + //Com_Printf("%s %s could not visit node [%d] for item %s dist[%f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, VectorDistance(self->s.origin, ent->s.origin)); + } + } + } + } + break; + + default: + break; + } + } + + //Com_Printf("%s %s -----\n\n", __func__, self->client->pers.netname); + + // Find the closest item + float closest = 999999; + float dist = 0; + int node = INVALID; + ent = NULL; + for (int i = 0; i < item_count; i++) + { + dist = VectorDistance(self->s.origin, items[i]->s.origin); + if (dist < closest) + { + closest = dist; + ent = items[i]; + node = item_nodes[i]; + + } + } + if (node != INVALID && ent) + { + self->bot.get_item = ent; + //Com_Printf("%s %s [%d] %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[node].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + //Com_Printf("%s %s goal item [%s] at node [%d] dist[%f]\n", __func__, self->client->pers.netname, ent->classname, nodes[node].nodenum, VectorDistance(self->s.origin, ent->s.origin)); + return nodes[node].nodenum; + } + else + { + self->bot.get_item = NULL; + return INVALID; + } + + return INVALID; +} + +// Does bot need weapons, items, ammo? If so find ground based items that are close. +// Returns the node of the item and sets the item for the bot to walk toward when its nearby +int BOTLIB_GetEquipment(edict_t* self) +{ + int primary_weapon = 0; + int secondary_weapon = 0; + qboolean need_weapon = BOTLIB_Need_Weapons(self, &primary_weapon, &secondary_weapon); + int ammo_type = BOTLIB_Need_Ammo(self, &primary_weapon, &secondary_weapon); + int special = BOTLIB_Need_Special(self, &primary_weapon, &secondary_weapon); + qboolean need_grenades = BOTLIB_Need_Grenades(self); + qboolean need_dual_mk23 = BOTLIB_Need_Dual_MK23(self); + qboolean need_knives = BOTLIB_Need_Knives(self); + //Com_Printf("%s wep[%d][%d][%d] gren[%d] knife[%d] dual[%d] ammo[%d] special[%d]\n", __func__, need_weapon, primary_weapon, secondary_weapon, need_grenades, need_knives, need_dual_mk23, ammo_type, special); + + int items_to_get[32]; + int items_counter = 0; + + // Acceptable primary weapon choices + if (primary_weapon == 0) + { + items_to_get[0] = MP5_NUM; + items_to_get[1] = M4_NUM; + items_to_get[2] = M3_NUM; + items_to_get[3] = HC_NUM; + items_to_get[4] = SNIPER_NUM; + items_counter += 5; + } + // Acceptable secondary weapon choices (if bandolier equipped) + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + if (band && primary_weapon && secondary_weapon == 0) + { + // Only add a weapon if not already in our inventory + if (primary_weapon != MP5_NUM) + { + items_to_get[items_counter] = MP5_NUM; + items_counter++; + } + if (primary_weapon != M4_NUM) + { + items_to_get[items_counter] = M4_NUM; + items_counter++; + } + if (primary_weapon != M3_NUM) + { + items_to_get[items_counter] = M3_NUM; + items_counter++; + } + if (primary_weapon != HC_NUM) + { + items_to_get[items_counter] = HC_NUM; + items_counter++; + } + if (primary_weapon != SNIPER_NUM) + { + items_to_get[items_counter] = SNIPER_NUM; + items_counter++; + } + } + if (ammo_type) + { + items_to_get[items_counter] = ammo_type; + items_counter++; + + // Because MK23 ammo can come from clips or pistols, try look for both + if (ammo_type == MK23_ANUM)// && BOTLIB_CanTouchItem(self, DUAL_NUM)) + { + items_to_get[items_counter] = DUAL_NUM; + items_counter++; + } + if (ammo_type == DUAL_NUM)// && BOTLIB_CanTouchItem(self, MK23_ANUM)) + { + items_to_get[items_counter] = MK23_ANUM; + items_counter++; + } + + } + if (special) + { + items_to_get[items_counter] = special; + items_counter++; + } + if (need_grenades) + { + items_to_get[items_counter] = GRENADE_NUM; + items_counter++; + } + if (need_dual_mk23) + { + items_to_get[items_counter] = DUAL_NUM; + items_counter++; + } + + // Debug output + if (0 && items_counter) + { + Com_Printf("%s items_to_get [", __func__); + for (int i = 0; i < items_counter; i++) + { + gitem_t* item = GET_ITEM(items_to_get[i]); + Com_Printf(" %s ", item->classname); + } + Com_Printf("]\n"); + } + + return BOTLIB_LocateFloorItem(self, items_to_get, items_counter); +} +//rekkie -- collecting weapons, items, ammo -- e diff --git a/src/client/screen.c b/src/client/screen.c index aeb71bc1d..f66250b12 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -2427,7 +2427,7 @@ static void SCR_DrawGhud(void) // float mult = 300 / link->distance; - Q_clip(mult, 0.25, 5); + Q_clipf(mult, 0.25, 5); int sizex = element->size[0] * mult; int sizey = element->size[1] * mult; From c1f0d7d48786298f850c86f65b883183749560f3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 May 2024 16:48:51 -0400 Subject: [PATCH 184/974] It builds with warnings, bots don't spawn? --- inc/common/bsp.h | 289 +++++- inc/common/cmodel.h | 6 + inc/shared/game.h | 23 +- meson.build | 18 + src/action/a_cmds.c | 6 + src/action/a_match.c | 5 - src/action/a_radio.c | 18 + src/action/a_team.c | 4 +- src/action/acesrc/acebot.h | 65 +- src/action/acesrc/acebot_ai.c | 1 + src/action/acesrc/acebot_nodes.c | 3 +- src/action/acesrc/acebot_spawn.c | 2 +- src/action/acesrc/bot_utility.c | 3 +- src/action/acesrc/botchat.c | 2 +- src/action/botlib/botlib.h | 36 +- src/action/botlib/botlib_cmd.c | 59 +- src/action/botlib/botlib_communication.c | 12 +- src/action/botlib/botlib_ctf.c | 2 + src/action/botlib/botlib_movement.c | 2 + src/action/botlib/botlib_nav.c | 22 +- src/action/botlib/botlib_nodes.c | 26 +- src/action/botlib/botlib_spawn.c | 12 +- src/action/botlib/botlib_spawnpoints.c | 4 + src/action/g_cmds.c | 4 +- src/action/g_local.h | 257 ++++- src/action/g_main.c | 14 +- src/action/g_save.c | 14 +- src/action/g_spawn.c | 35 +- src/action/g_svcmds.c | 46 +- src/action/p_client.c | 4 +- src/action/p_hud.c | 4 - src/refresh/main.c | 1199 ++++++++++++++++++++++ src/server/game.c | 24 + src/server/main.c | 98 ++ src/server/server.h | 14 + src/server/world.c | 47 + 36 files changed, 2205 insertions(+), 175 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index 45b954d3a..60e7e7190 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -58,7 +58,6 @@ typedef struct mtexinfo_s { // used internally due to name len probs //ZOID #endif } mtexinfo_t; -#if USE_REF typedef struct { vec3_t point; } mvertex_t; @@ -114,7 +113,6 @@ typedef struct mface_s { struct entity_s *entity; struct mface_s *next; } mface_t; -#endif typedef struct mnode_s { /* ======> */ @@ -202,7 +200,286 @@ typedef struct mmodel_s { #endif } mmodel_t; -#if USE_REF + +//rekkie -- surface data -- s +// Surface data structure to store information about surfaces: their faces, normals, edge verts, etc. +#define MAX_FACE_VERTS 64 +#define MAX_FACE_CONNECTIONS 1024 //(MAX_SAVED_VERTS / 2) + +// Face connection types - denotes how faces are connected to each other, and how the bot should move between them +typedef enum face_connection_type_e { + FACE_CONN_NONE, // No connection + FACE_CONN_DIRECT, // Two faces directly touch by a shared edge + FACE_CONN_INDIRECT, // Two faces are indirectly connected due to gaps between the two faces (such as the urban jump from roof to roof) + FACE_CONN_LEDGE, // The current edge is a ledge with a drop that will cause fall damage or death + FACE_CONN_WATER, // The current edge is a ledge that drops to water below + FACE_CONN_DROP, // From the current face drop to the face below + FACE_CONN_STEP, // Face is step height (up or down - bidirectional) + FACE_CONN_JUMP // Jump to face (up or down - bidirectional) +} face_connection_type_t; + +// Face movement types - gives an indication as to the type of position this leads to +typedef enum face_move_type_e { + FACE_MOVE_NONE, // No type + FACE_MOVE_SAFE, // Safe move: this interconnects with flat ground + FACE_MOVE_CAUTION, // Safe move: this interconnects to a step/stairs, drop (only down - one way), or jump (up or down - bidirectional), drop into water below + FACE_MOVE_STOP, // Caution move: this ends at dangerous ledge (damage or death), or possibly a good sniping spot :) + FACE_MOVE_GROUND, // The next interconnect is the same height + FACE_MOVE_UP, // The next interconnect is above + FACE_MOVE_DOWN // The next interconnect is below +} face_move_type_t; + +// Edge offset types - centered, offset along its length, or moved inward (i.e. away from a wall) +typedef enum edge_offset_type_e { + EDGE_OFFSET_NONE, // No type + EDGE_OFFSET_CENTER, // Offset position: center edge + EDGE_OFFSET_LENGTH, // Offset position: along its length (somewhere between the two vectors (v1 & v2) that constitute an edge) + EDGE_OFFSET_INNER // Offset position: moved inward (inside the face), possibly to move away from a wall or obstacle +} edge_offset_type_t; + +// The type of face +typedef enum facetype_e { + FACETYPE_NONE = 0, // No type + FACETYPE_IGNORED = 1, // Ignored surface (could be a tiny face, or malformed face, etc) + FACETYPE_WALK = 2, // Walkable surface + FACETYPE_TINYWALK = 4, // Walkable tiny surface + FACETYPE_WALL = 8, // Wall surface + FACETYPE_LADDER = 16, // Ladder surface + FACETYPE_SKY = 32, // Sky surface + FACETYPE_ROOF = 64, // Roof surface + FACETYPE_WATER = 128, // Water surface + FACETYPE_DAMAGE = 256 // Hurt, lava, acid, etc. +} facetype_t; + +// Nodes or locations within the face +typedef struct surface_node_s +{ + qboolean init; // + int type; // Face connection type (enum face_connection_type_t) + int move; // Face movement type (enum face_move_type_t) + int facenum; // The remote face number (if any) that is connected to this face + float dropheight; // If the edge is a ledge, this is its drop height, otherwise 0 + vec3_t start; // The start position of the connection point within the face (such as the center of the polygon) + vec3_t end; // The end position of the connection point (can be outside of the face) +} surface_node_t; + +// Ledge data +typedef struct ledge_data_s +{ + qboolean is_ledge; // Flag if the edge is a ledge + qboolean is_wall; // Flag if edge is a wall + vec3_t normal; // Surface normal that was hit + vec3_t v1; // Adjusted edge v1 + vec3_t v2; // Adjusted edge v2 + vec3_t startpos; // Start location + vec3_t endpos; // Hit location + float height; // Height difference from ledge to endpos, if zero then no ledge + + // Debug + byte hit_side; // If trace hit: none=0, left=1, right=2 + vec3_t left_start; // Start trace left + vec3_t left_end; // End trace left + vec3_t right_start; // Start trace right + vec3_t right_end; // End trace right +} ledge_data_t; + +typedef struct surface_data_s +{ + // ======================================================== + // Raw BSP Data + // Extracted from the BSP struct - Do not modify! + // ======================================================== + + int drawflags; // Drawflags -- DSURF_PLANEBACK + char texture[MAX_TEXNAME]; // Texture name + vec3_t normal; // Surface normal + int contents; // Contents of the surface + #if USE_REF + mface_t* surf; // Surface pointer to the BSP struct + #endif + // Vertex + vec3_t first_vert; // First vert - used when calculating triangles (i.e. gl_showtris "1") + int num_verts; // Number of verts (edges) + vec3_t verts[MAX_FACE_VERTS]; // Verts (inc lateral) + + // Positions + vec3_t edge_center[MAX_FACE_CONNECTIONS]; // The center of each edge + vec3_t edge_center_stepsize[MAX_FACE_CONNECTIONS]; // The edge_center moved up by 18 (STEPSIZE) units + + // ======================================================== + // End of Raw BSP Data + // ======================================================== + + + // ======================================================== + // Custom BSP Data - Safe to modify + // ======================================================== + + short int facenum; // The set of surfaces we've chosen to work with, i.e. walls are generally excluded, etc + facetype_t face_type; // The type of face, walkable, wall, ladder, water, damage... + + // Aligned verts are connected edges that are parallel (same direction) combined into a single edge to form a straight line + // Essentially we're removing all the inner verts, leaving only the outer verts, and thus forming a straight line between them + // We do this so we can calculate such things as finding the center of a polygon + // + // Normal edges: + // v1, v2, v3, v4 are vertices + // edge1 is parallel to edge2 + // + // v1 edge1 v2 v3 edge2 v4 + // | ----------------- | ----------------| + // + // Combined edges: + // v2 and v3 are removed and edge1 and edge2 are combined into a single edge + // + // v1 edge1 v2 + // | ------------------------------------| + // + int num_aligned_verts; // Number of aligned verts + vec3_t aligned_verts[MAX_FACE_VERTS]; // Aligned verts + + // Modified Edges + // This will either be the center of the edge, or failing that a position somewhere along the edge that fits the player hitbox (because sometimes the center edge cannot fit the player hitbox due to other brushes obstructing) + vec3_t edge_valid_pos[MAX_FACE_CONNECTIONS]; // Valid player hitbox position along the edge + vec3_t edge_valid_stepsize[MAX_FACE_CONNECTIONS]; // The edge_valid_pos moved up by 18 (STEPSIZE) units + edge_offset_type_t edge_offset_type[MAX_FACE_CONNECTIONS]; // Flag the type of offset + + // Detected ledges + ledge_data_t ledge[MAX_FACE_CONNECTIONS]; + + // Positions + vec3_t center_poly; // The center of the ladder's polygon face, located directly on the surface + vec3_t center_poly_32_units; // The center of the ladder face, moved away from the surface normal by 32 units (full player width) + vec3_t aligned_edge_center[MAX_FACE_CONNECTIONS]; // The center of each aligned edge + vec3_t aligned_edge_center_stepsize[MAX_FACE_CONNECTIONS]; // The aligned_edge_center moved up by 18 (STEPSIZE) units + + float volume; // Surface volume + + // The min and max lengths from edge to edge within a face + float min_length; + float max_length; + + // Connections within the face, and nearest connected faces + int snode_counter; // How many faces are connected + surface_node_t snodes[MAX_FACE_CONNECTIONS]; // Surface nodes + +} surface_data_t; +//extern surface_data_t* surface_data_faces; + +typedef struct nav_s +{ + surface_data_t* surface_data_faces; + unsigned surface_data_checksum; // Checksum of all surface data faces based on current map checksum + short int faces_total; // Current number of faces in the array + short int ignored_faces_total; // Ignored faces: FACETYPE_IGNORED, FACETYPE_NONE +} nav_t; +extern nav_t* nav_; +nav_t* CS_NAV(void); +void MoveAwayFromNormal(const int drawflags, const vec3_t normal, vec3_t out, const float distance); +//rekkie -- surface data -- e + + +//rekkie -- debug drawing -- s +#define DEBUG_DRAWING 1 +#if DEBUG_DRAWING +// Drawing functions +typedef struct debug_draw_s +{ + // Turns drawing on/off - reduce overhead when off + qboolean arrows_inuse; + qboolean boxes_inuse; + qboolean crosses_inuse; + qboolean strings_inuse; + + void* DrawSelection; // DrawSelection function pointer + void* DrawString; // DrawString function pointer + void* DrawCross; // DrawCross function pointer + void* DrawBox; // DrawCross function pointer + void* DrawArrow; // DrawArrow function pointer + int draw_arrow_num; // Current arrow being drawn: increments until MAX_DRAW_ARROWS, then resets to zero +} debug_draw_t; +debug_draw_t* CS_DebugDraw(void); + +void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, const float line_width, qboolean occluded); + +void GL_DrawStrings(void); +void GL_DrawString(vec3_t origin, const char* string, const uint32_t color, qboolean occluded); +typedef struct draw_string_s +{ + qboolean inuse; + int time; + vec3_t origin; + char string[1024]; + uint32_t color; + qboolean occluded; +} draw_string_t; +#define MAX_DRAW_STRINGS 4096 +void DrawString(int number, vec3_t origin, const char* string, const uint32_t color, const int time, qboolean occluded); + + + +void GL_DrawCross(vec3_t origin, qboolean occluded); +typedef struct draw_crosses_s +{ + qboolean inuse; + int time; + vec3_t origin; + qboolean occluded; +} draw_crosses_t; +#define MAX_DRAW_CROSSES 4096 +void DrawCross(int number, vec3_t origin, int time, qboolean occluded); + +void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded); +typedef struct draw_boxes_s +{ + qboolean inuse; + int time; + vec3_t origin; + uint32_t color; + vec3_t mins; + vec3_t maxs; + qboolean occluded; +} draw_boxes_t; +#define MAX_DRAW_BOXES 6144 +extern draw_boxes_t draw_boxes[MAX_DRAW_BOXES]; +void DrawBox(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded); + +void GL_AddDrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, qboolean occluded); +typedef struct draw_arrows_s +{ + qboolean inuse; + int time; + vec3_t start; + vec3_t end; + uint32_t color; + float line_width; + qboolean occluded; +} draw_arrows_t; +#define MAX_DRAW_ARROWS 8192 +extern draw_arrows_t draw_arrows[MAX_DRAW_ARROWS]; +void DrawArrow(int number, vec3_t start, vec3_t end, uint32_t color, float line_width, int time, qboolean occluded); + +// OpenGL selection square +void GL_DrawSelectionSquare(vec3_t start, vec3_t end, float min, float max, uint32_t color, float line_width, qboolean occluded); +typedef struct draw_selection_s +{ + qboolean inuse; + int time; + vec3_t start; + vec3_t end; + float min; + float max; + uint32_t color; + float line_width; + qboolean occluded; +} draw_selection_t; +extern draw_selection_t draw_selection; +void DrawSelection(vec3_t start, vec3_t end, float min, float max, uint32_t color, float line_width, int time, qboolean occluded); + +#endif +//rekkie -- debug drawing -- e + +//#if USE_REF typedef struct { int32_t point[3]; @@ -235,7 +512,7 @@ typedef struct { lightgrid_sample_t *samples; } lightgrid_t; -#endif +//#endif typedef struct bsp_s { list_t entry; @@ -283,7 +560,6 @@ typedef struct bsp_s { int numareaportals; // size of the array below mareaportal_t *areaportals; -#if USE_REF int numfaces; mface_t *faces; @@ -302,10 +578,11 @@ typedef struct bsp_s { int numsurfedges; msurfedge_t *surfedges; +//#if USE_REF lightgrid_t lightgrid; bool lm_decoupled; -#endif +//#endif bool extended; char name[1]; diff --git a/inc/common/cmodel.h b/inc/common/cmodel.h index b144bed44..4be91d2cb 100644 --- a/inc/common/cmodel.h +++ b/inc/common/cmodel.h @@ -32,6 +32,12 @@ typedef struct { int override_bits; int checksum; char *entitystring; + //rekkie -- surface data -- s + nav_t *nav; // Navigation the game lib can access + //rekkie -- surface data -- e + + //rekkie -- debug drawing -- s + debug_draw_t* draw; // Rendering functions the game lib can access } cm_t; void CM_Init(void); diff --git a/inc/shared/game.h b/inc/shared/game.h index 7e14fefc1..75eca2945 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "shared/list.h" +#include "common/bsp.h" // // game.h -- game dll information visible to server @@ -227,8 +228,28 @@ typedef struct { #if AQTION_EXTENSION void *(*CheckForExtension)(char *text); #endif +//rekkie -- BSP -- s + bsp_t* (*Bsp)(void); + //rekkie -- BSP -- e + //rekkie -- surface data -- s + nav_t* (*Nav)(void); + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + debug_draw_t* (*Draw)(void); +//#if USE_REF +// void (*GL_DrawArrow)(vec3_t start, vec3_t end, const uint32_t color, float line_width); +//#endif +#endif + surface_data_t* (*SurfaceData)(void); + //rekkie -- surface data -- e + + //rekkie -- Fake Bot Client -- s + void (*SV_BotUpdateInfo)(char* name, int ping, int score); + void (*SV_BotConnect)(char* name); + void (*SV_BotDisconnect)(char* name); + void (*SV_BotClearClients)(void); + //rekkie -- Fake Bot Client -- e } game_import_t; - // // functions exported by the game subsystem // diff --git a/meson.build b/meson.build index 20a2e9970..c0f945e33 100644 --- a/meson.build +++ b/meson.build @@ -231,6 +231,21 @@ ltk_src = [ 'src/action/acesrc/botscan.c' ] +botlib_src = [ + 'src/action/botlib/botlib_ai.c', + 'src/action/botlib/botlib_cmd.c', + 'src/action/botlib/botlib_communication.c', + 'src/action/botlib/botlib_ctf.c', + 'src/action/botlib/botlib_items.c', + 'src/action/botlib/botlib_math.c', + 'src/action/botlib/botlib_movement.c', + 'src/action/botlib/botlib_nav.c', + 'src/action/botlib/botlib_nodes.c', + 'src/action/botlib/botlib_spawn.c', + 'src/action/botlib/botlib_spawnpoints.c', + 'src/action/botlib/botlib_weapons.c', +] + cc = meson.get_compiler('c') win32 = host_machine.system() == 'windows' @@ -318,6 +333,8 @@ if cc.get_argument_syntax() == 'gcc' endif elif cc.get_id() == 'msvc' common_args += ['/wd4146', '/wd4244', '/wd4305'] +else + dll_link_args += '--no-relax' endif add_project_arguments(common_args, language: 'c') @@ -489,6 +506,7 @@ if get_option('aqtion-build') config.set10('AQTION_EXTENSION', true) config.set10('AQTION_HUD', true) action_src += ltk_src + action_src += botlib_src # Win32 winsock compat if win32 diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index e83113c87..007999266 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -1354,6 +1354,10 @@ void generate_uuid(void) } #endif + +//rekkie -- DEV_1 -- s +// original code -- disabled +/* #ifndef NO_BOTS void Cmd_Placenode_f (edict_t *ent) { @@ -1367,6 +1371,8 @@ void Cmd_Placenode_f (edict_t *ent) ACEND_AddNode(ent,NODE_MOVE); } #endif +*/ +//rekkie -- DEV_1 -- e void Cmd_Volunteer_f(edict_t * ent) { diff --git a/src/action/a_match.c b/src/action/a_match.c index 1e67871fa..bac84ae54 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -95,11 +95,6 @@ void SendScores(void) // Stats: Reset roundNum game.roundNum = 0; // Stats end - - #ifndef NO_BOTS - // Clear LTK bot names - LTKClearBotNames(); - #endif } void Cmd_Sub_f(edict_t * ent) diff --git a/src/action/a_radio.c b/src/action/a_radio.c index 53b402664..c445ee26d 100644 --- a/src/action/a_radio.c +++ b/src/action/a_radio.c @@ -309,6 +309,24 @@ static void AddRadioMsg( radio_t *radio, int sndIndex, int len, edict_t *from_pl } } +//rekkie -- Quake3 -- s +void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player) +{ + if (radio->queue_size == 0) + { + AppendRadioMsgToQueue(radio, globalRadio[RADIO_CLICK].sndIndex, globalRadio[RADIO_CLICK].length, 1, from_player); + AppendRadioMsgToQueue(radio, sndIndex, len, 0, from_player); + AppendRadioMsgToQueue(radio, globalRadio[RADIO_CLICK].sndIndex, globalRadio[RADIO_CLICK].length, 1, from_player); + } + else // we have some msgs in it already... + { + if (radio->queue_size < MAX_RADIO_QUEUE_SIZE) + InsertRadioMsgInQueueBeforeClick(radio, sndIndex, len, from_player); + // else ignore the message... + } +} +//rekkie -- Quake3 -- e + void RadioBroadcast (edict_t * ent, int partner, char *msg) { int j, i, msg_len, numSnds; diff --git a/src/action/a_team.c b/src/action/a_team.c index 1825f885c..982748c50 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2327,7 +2327,7 @@ void RunWarmup (void) if (warmup_bots->value){ gi.cvar_forceset("am", "1"); gi.cvar_forceset("am_botcount", warmup_bots->string); - attract_mode_bot_check(); + //attract_mode_bot_check(); } #endif } @@ -2817,7 +2817,7 @@ int CheckTeamRules (void) if (warmup_bots->value){ gi.cvar_forceset("am", "0"); gi.cvar_forceset("am_botcount", "0"); - attract_mode_bot_check(); + //attract_mode_bot_check(); ACESP_RemoveBot("all"); CenterPrintAll("All bots removed, good luck and have fun!"); diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index fa574bd05..d05ae932c 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -53,6 +53,7 @@ #ifndef _ACEBOT_H #define _ACEBOT_H +#include "../botlib/botlib.h" // Bots think at the server framerate to make sure they move smoothly. #define BOT_FPS (game.framerate) @@ -260,6 +261,7 @@ typedef struct node_s byte num_links; // Total links this node has nodelink_t links[MAXLINKS]; // store all links. - RiEvEr } node_t; +extern node_t *nodes; typedef struct item_table_s { @@ -279,13 +281,13 @@ extern node_t *unsorted_nodes; // Used to generate all links, so they can be sor #define MAX_PNODES 8096 //8096 //32768 Absolute max nodes //extern node_t *nodes; -node_t *nodes; +extern node_t *nodes; //node_t *nodes[MAX_PNODES]; //node_t nodes[MAX_PNODES]; //extern node_t nodes[MAX_PNODES]; //rekkie -- DEV_1 -- e -short int** path_table; +extern short int** path_table; ////short int path_table[MAX_PNODES][MAX_PNODES]; //extern node_t nodes[MAX_NODES]; @@ -318,7 +320,7 @@ void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); // acebot_cmds.c protos qboolean ACECM_Commands(edict_t *ent); -void ACECM_Store(); +void ACECM_Store(void); // acebot_items.c protos void ACEIT_RebuildPlayerList( void ); @@ -383,14 +385,14 @@ int BOTLIB_TestForNodeDist(vec_t* origin, float distance, vec3_t mins, vec3_t m //int ACEND_AddNode(edict_t *self, int type); //void ACEND_UpdateNodeEdge(edict_t *self, int from, int to); void ACEND_RemoveNodeEdge(edict_t *self, int from, int to); -//void ACEND_ResolveAllPaths(); -//void ACEND_SaveNodes(); -//void ACEND_LoadNodes(); +//void ACEND_ResolveAllPaths(void); +//void ACEND_SaveNodes(void); +//void ACEND_LoadNodes(void); // acebot_spawn.c protos -//void ACESP_SaveBots(); -//void ACESP_LoadBots(); -void ACESP_LoadBotConfig(); +//void ACESP_SaveBots(void); +//void ACESP_LoadBots(void); +void ACESP_LoadBotConfig(void); edict_t *ACESP_SpawnBotFromConfig( char *inString ); void ACESP_HoldSpawn(edict_t *self); void ACESP_PutClientInServer (edict_t *bot, qboolean respawn, int team); @@ -398,7 +400,7 @@ void ACESP_Respawn (edict_t *self); edict_t *ACESP_FindFreeClient (void); void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); -//void ACESP_ReAddBots(); +//void ACESP_ReAddBots(void); void ACESP_RemoveBot(char *name); void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); void safe_centerprintf (edict_t *ent, const char *fmt, ...); @@ -499,17 +501,17 @@ void BOTWP_RemoveSniperZoomMode(edict_t *bot); // // - Bots aiming through water, some water opacity should limit the bot's ability to aim through water, via water // -// - [Drg] Varekai — Today at 22:26 +// - [Drg] Varekai � Today at 22:26 // I've always wondered because we use punch to get to weird places and I imagined that how far someone went had more to do with their initial speed // as in, hitting them as they're just jumping would be more effective -// darksaint — Today at 22:27 -// ReKTeK aka Mech — Today at 22 : 31 +// darksaint � Today at 22:27 +// ReKTeK aka Mech � Today at 22 : 31 // Having another quick skim of the code, it doesn't take velocity into account. However, if the player is airborne, they wouldn't be slowed down by any initial friction(if any is applied) plus the height would give extra distance -// Drg] Varekai — Today at 22 : 32 +// Drg] Varekai � Today at 22 : 32 // so it is mostly random, pretty nuts -// ReKTeK aka Mech — Today at 22 : 33 +// ReKTeK aka Mech � Today at 22 : 33 // I suppose velocity should be taken into account -// darksaint — Today at 22 : 35 +// darksaint � Today at 22 : 35 // Kicking someone in mid - air after being kicked should result in double damage :) using their body like a football // // - Roundlimit: add a countdown at 30 seconds, 15 seconds... @@ -568,10 +570,10 @@ extern cvar_t* bot_randname; typedef struct { char name[16]; // Name }bot_names_t; -int dc_total_male_names; // Total male names -int dc_total_female_names; // Total female names -bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy -bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy +extern int dc_total_male_names; // Total male names +extern int dc_total_female_names; // Total female names +extern bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy +extern bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy @@ -673,15 +675,15 @@ typedef struct nmesh_s { unsigned bsp_checksum; // Map checksum } nmesh_t; -nmesh_t nmesh; +extern nmesh_t nmesh; -int num_poi_nodes; -int poi_nodes[MAX_POI_NODES]; -edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -int num_vis_nodes; -int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -int node_vis_list[10][10]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. +extern int num_poi_nodes; +extern int poi_nodes[MAX_POI_NODES]; +extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +extern int num_vis_nodes; +extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +extern int node_vis_list[10][10]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. //int node_vis[MAX_PNODES][MAX_PNODES]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM //int node_vis_list[MAX_PNODES][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. @@ -762,8 +764,8 @@ void BOTLIB_LoadNav(void); #endif qboolean NodeTypeToString(edict_t* self, int type, char* string, const int max_string_size); -//void ACEND_FreeUnsortedNodes(); -//qboolean ACEND_InitUnsortedNodes(); +//void ACEND_FreeUnsortedNodes(void); +//qboolean ACEND_InitUnsortedNodes(void); void ACEND_BSP(edict_t* ent); void ACEND_BuildSpawnPointNodes(void); void ACEND_CachePointOfInterestNodes(void); @@ -841,9 +843,10 @@ extern int numthreads; void ThreadSetDefault(void); int GetThreadWork(void); //void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)); -void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); //void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); -void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); +//void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); void ThreadLock(void); void ThreadUnlock(void); //rekkie -- Quake3 -- e diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 6ca40a470..be350452a 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -54,6 +54,7 @@ #include "../g_local.h" #include "acebot.h" #include "botchat.h" +#include "../botlib/botlib.h" void ACEAI_Cmd_Choose( edict_t *ent, char *s); void ACEMV_ChangeBotAngle (edict_t *ent); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 778a984e2..d5b6758c4 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -51,6 +51,7 @@ #include "../g_local.h" #include "acebot.h" +#include "botnav.h" //rekkie -- DEV_1 -- s // original code @@ -80,7 +81,7 @@ node_t *unsorted_nodes; // Used to generate all links, so they can be sorted, th //node_t nodes[MAX_PNODES]; //#define MAX_PNODES 65536 // Absolute max nodes 64k -short int **path_table = NULL; +//short int **path_table = NULL; ////short int path_table[MAX_PNODES][MAX_PNODES]; //rekkie -- DEV_1 -- e diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index f21f33101..fc3985f7d 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -330,7 +330,7 @@ edict_t *ACESP_SpawnBotFromConfig( char *inString ) Info_SetValueForKey( userinfo, "spectator", "0" ); // NOT a spectator Info_SetValueForKey( userinfo, "gender", gender ); - Com_sprintf( team_str, 2, "%i", team ); + Q_snprintf( team_str, 2, "%i", team ); bot = ACESP_SpawnBot( team_str, name, modelskin, userinfo ); diff --git a/src/action/acesrc/bot_utility.c b/src/action/acesrc/bot_utility.c index 29ec134a4..929eba2e0 100644 --- a/src/action/acesrc/bot_utility.c +++ b/src/action/acesrc/bot_utility.c @@ -16,6 +16,7 @@ */ #include "../g_local.h" +#include "../botlib/botlib.h" #define SHOWNODES_DIST 384 // Max range for shownodes @@ -139,7 +140,7 @@ void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg) if (ent->client->resp.team == NOTEAM) return; - Com_sprintf (text, sizeof(text), "%s%s: ", + Q_snprintf (text, sizeof(text), "%s%s: ", (teamplay->value && (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD)) ? "[DEAD] " : "", ent->client->pers.netname); diff --git a/src/action/acesrc/botchat.c b/src/action/acesrc/botchat.c index 45ba2a1c9..b581e05c0 100644 --- a/src/action/acesrc/botchat.c +++ b/src/action/acesrc/botchat.c @@ -117,7 +117,7 @@ void LTK_Say (edict_t *ent, char *what) edict_t *other; char text[2048]; - Com_sprintf (text, sizeof(text), "%s: ", ent->client->pers.netname); + Q_snprintf (text, sizeof(text), "%s: ", ent->client->pers.netname); if (*what == '"') { diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 82431e4d3..ff707cb66 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -50,7 +50,7 @@ typedef struct bot_connections_s int desire_team2; int desire_team3; } bot_connections_t; -bot_connections_t bot_connections; +extern bot_connections_t bot_connections; //the bot input, will be converted to a usercmd_t typedef struct bot_input_s @@ -105,7 +105,7 @@ typedef struct botlib_noises_s vec3_t impact_origin[MAX_CLIENTS]; } botlib_noises_t; -botlib_noises_t botlib_noises; +extern botlib_noises_t botlib_noises; // Actionable flags #define ACTION_NONE 0x00000000 // No action taken @@ -167,7 +167,7 @@ typedef struct ctf_status_s float team1_carrier_dist_to_home; // How close the red team carrier is to the home red flag node float team2_carrier_dist_to_home; // How close the blue team carrier is to the home blue flag node } ctf_status_t; -ctf_status_t bot_ctf_status; +extern ctf_status_t bot_ctf_status; // Get flag, retrieve flag, intercept flag carrier, etc. typedef enum @@ -195,7 +195,7 @@ qboolean BOTLIB_Commands(edict_t* ent); // Client commands // botlib_communication.c // =========================================================================== void BOTLIB_Wave(edict_t* ent, int type); -void BOTLIB_PrecacheRadioSounds(); +void BOTLIB_PrecacheRadioSounds(void); void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); @@ -328,8 +328,8 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd); #define MAX_NAV_AREAS_EDGES 64 #define MAX_NAV_AREAS_PATHS 512 //512 #define MAX_NAV_AREAS_NODES 4096 -int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] -int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] +extern int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] +extern int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] @@ -352,13 +352,13 @@ typedef struct { } nav_area_t; -nav_area_t nav_area; +extern nav_area_t nav_area; -void BOTLIB_MallocAreaNodes(); -void BOTLIB_FreeAreaNodes(); +void BOTLIB_MallocAreaNodes(void); +void BOTLIB_FreeAreaNodes(void); void BOTLIB_InitAreaNodes(void); // Init area nodes to INVALID -void BOTLIB_InitAreaConnections(); // Init and make area connections +void BOTLIB_InitAreaConnections(void); // Init and make area connections qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization); // Supports new and old pathing methods qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node); // Checks if possible to traverse from current node to goal_node void BOTLIB_GetAreaPath(edict_t* self, int goal_node); // Sets a path from area-to-area @@ -366,7 +366,7 @@ qboolean BOTLIB_GetNextAreaNode(edict_t* self); void BOTLIB_UpdateAllAreaEdges(void); // Stores all the nodes within an area that connect to an external area (edge nodes) int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2); // Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination); // Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm -void BOTLIB_RandomizeAreaColors(); // Randomize area colors +void BOTLIB_RandomizeAreaColors(void); // Randomize area colors void BOTLIB_AutoArea(edict_t* self); qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path); qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization); @@ -385,7 +385,7 @@ typedef struct { typedef struct { botlib_sll_nodes_t* head; // Front botlib_sll_nodes_t* tail; // Back -} botlib_sll_t; +} botlib_sll_t; //} ltklist_t; // =========================================================================== @@ -395,7 +395,7 @@ void BOTLIB_LinkNodesNearbyNode(edict_t* ent, int from); qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path); void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node); -void BOTLIB_SetAllNodeNormals(); +void BOTLIB_SetAllNodeNormals(void); // =========================================================================== // botlib_spawn.c @@ -422,9 +422,9 @@ typedef struct { vec3_t origin; // Spawn point location vec3_t angles; // Spawn point direction } dc_sp_t; -dc_sp_t* dc_sp; -int dc_sp_count; // Total spawn points -qboolean dc_sp_edit; // If the spawn points have been made visible for editing +extern dc_sp_t* dc_sp; +extern int dc_sp_count; // Total spawn points +extern qboolean dc_sp_edit; // If the spawn points have been made visible for editing #define DC_SP_LIMIT 255 // Maximum spawn points #define DC_SP_VERSION 1 // Version of spawn point file void DC_Init_Spawnpoints(void); // Initialise spawn points @@ -433,8 +433,8 @@ void DC_Add_Spawnpoint(edict_t* ent); // Add a spawn point at the player locatio void DC_Remove_Spawnpoint(edict_t* self); // Remove a spawn point at the player location void DC_Get_Map_Spawnpoints(void); // Find and add all the map spawn points to the dc_sp[] array void BOTLIB_Show_Spawnpoints(void); // Find and display (as visible ents) all spawn points (map and custom) -void DC_Save_Spawnpoints(); // Save the spawn points -void DC_Load_Spawnpoints(); // Load the spawn points +void DC_Save_Spawnpoints(void); // Save the spawn points +void DC_Load_Spawnpoints(void); // Load the spawn points // =========================================================================== diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 7db0d4003..3ba92aa8d 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -110,18 +110,18 @@ qboolean BOTLIB_SV_Cmds(void) return true; } //rekkie -- BSP -- s - else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - { - BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - return true; - } - // Process BSP and generate AAS reachability with nodes - else if (Q_stricmp(cmd, "aas") == 0) - { - ACEND_BSP(NULL); - return true; - } + // else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + // { + // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + // return true; + // } + // // Process BSP and generate AAS reachability with nodes + // else if (Q_stricmp(cmd, "aas") == 0) + // { + // ACEND_BSP(NULL); + // return true; + // } //rekkie -- BSP -- e //rekkie -- python chatbot -- s @@ -261,13 +261,14 @@ qboolean BOTLIB_Commands(edict_t* ent) char* cmd = gi.argv(0); //rekkie -- BSP -- s - if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - { - BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - return true; - } - else if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS + // if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + // { + // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + // return true; + // } + // else + if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS { ACEND_SaveAAS(true); return true; @@ -579,16 +580,16 @@ qboolean BOTLIB_Commands(edict_t* ent) return true; } } - else if (Q_stricmp(cmd, "head") == 0) // Test func -- spawn kickable head - { - ThrowGibbedHead(ent, 50); // Spawn kickable head - return true; - } - else if (Q_stricmp(cmd, "ball") == 0) // Test func -- spawn kickable ball - { - QPong_Ball(ent, 50); - return true; - } + // else if (Q_stricmp(cmd, "head") == 0) // Test func -- spawn kickable head + // { + // ThrowGibbedHead(ent, 50); // Spawn kickable head + // return true; + // } + // else if (Q_stricmp(cmd, "ball") == 0) // Test func -- spawn kickable ball + // { + // QPong_Ball(ent, 50); + // return true; + // } else if (Q_stricmp(cmd, "dc_add_sp") == 0) // Add (or recycle unused) custom spawn point { DC_Add_Spawnpoint(ent); // This will add a custom spawn point at the player's location diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 361c4ecf9..04d4cd67b 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -45,14 +45,14 @@ void BOTLIB_PrecacheRadioSounds() //male for (i = 0; i < bot_numMaleSnds; i++) { - Com_sprintf(path, sizeof(path), "%s%s.wav", "radio/male/", bot_male_radio_msgs[i].msg); + Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/male/", bot_male_radio_msgs[i].msg); bot_male_radio_msgs[i].sndIndex = gi.soundindex(path); } //female for (i = 0; i < bot_numFemaleSnds; i++) { - Com_sprintf(path, sizeof(path), "%s%s.wav", "radio/female/", bot_female_radio_msgs[i].msg); + Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/female/", bot_female_radio_msgs[i].msg); bot_female_radio_msgs[i].sndIndex = gi.soundindex(path); } } @@ -177,14 +177,14 @@ void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) { if (team_message) - Com_sprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team + Q_snprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team else - Com_sprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all + Q_snprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all } else if (team_message) - Com_sprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team + Q_snprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team else - Com_sprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all + Q_snprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all offset_of_text = strlen(text); //FB 5/31/99 diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 81020d37d..6027ece6f 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -2,6 +2,8 @@ #include "../acesrc/acebot.h" #include "botlib.h" +ctf_status_t bot_ctf_status; + // Does this bot have the flag? qboolean BOTLIB_Carrying_Flag(edict_t* self) { diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 3c7ed1bd6..f67b225a1 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -2,6 +2,8 @@ #include "../acesrc/acebot.h" #include "botlib.h" +botlib_noises_t botlib_noises; + // Returns the XYZ distance float VectorDistance(vec3_t start, vec3_t end) { diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index e59be96f6..4b2716a36 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -6,8 +6,8 @@ //== GLOBAL SEMAPHORE == int antSearch; -extern short int** path_table; -////extern short int path_table[MAX_PNODES][MAX_PNODES]; +short int** path_table; +nav_area_t nav_area; qboolean nodeused[MAX_PNODES]; // This is used for a FAST check if the node has been used short int nodefrom[MAX_PNODES]; // Stores how we got here once the node is closed @@ -586,6 +586,8 @@ qboolean BOTLIB_NodeCanSeeEnemyPath(edict_t* ent, int atNode) // //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] +int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] // Init data used when selecting nodes void BOTLIB_InitAreaNodes(void) @@ -3035,16 +3037,16 @@ void ThreadWorkerFunction(int threadnum) { } //void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)) { -void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { - if (numthreads == -1) { - ThreadSetDefault(); - } - workfunction = func; - RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); -} +// void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { +// if (numthreads == -1) { +// ThreadSetDefault(); +// } +// workfunction = func; +// RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); +// } -#if GDEF_OS_WINDOWS +#if _WIN32 /* =================================================================== diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 6ab90d386..6ab4559ae 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -2,8 +2,14 @@ #include "../acesrc/acebot.h" #include "botlib.h" - -//rekkie -- DEV_1 -- s +int num_poi_nodes; +int poi_nodes[MAX_POI_NODES]; +edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +int num_vis_nodes; +int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +int node_vis_list[10][10]; // Cached node +node_t *nodes; +nmesh_t nmesh; // Free nodes void BOTLIB_FreeNodes(void) @@ -2736,10 +2742,7 @@ void BOTLIB_SetAllNodeNormals() // Compression -- s #if USE_ZLIB -#include "..\..\extern\zlib\zlib.h" -//#include -//#include -//#include +#include #define CHUNK_SIZE 16384 @@ -4750,8 +4753,8 @@ void BOTLIB_BSP_SURFACES(bsp_t* bsp) for (e = 0, src_surfedge = surf->firstsurfedge; e < surf->numsurfedges; e++, src_surfedge++) { // Copy edge verts - VectorCopy(src_surfedge->edge->v[0]->point, nmesh.face[f].edge[e].v[0]); - VectorCopy(src_surfedge->edge->v[1]->point, nmesh.face[f].edge[e].v[1]); + VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[0]].point, nmesh.face[f].edge[e].v[0]); + VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[1]].point, nmesh.face[f].edge[e].v[1]); // Copy face verts @@ -5414,6 +5417,12 @@ void BOTLIB_InitNavigation(edict_t* ent) return; } + if (bsp->checksum == NULL) + { + gi.dprintf("%s bsp->checksum is null\n", __func__); + return; + } + if (ent == NULL) { ent = g_edicts; @@ -5422,6 +5431,7 @@ void BOTLIB_InitNavigation(edict_t* ent) } BOTLIB_InitNodes(); + nmesh.bsp_checksum = bsp->checksum; // Save the map checksum Remove_All_Doors(); // Make sure all doors are open before making any nodes diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 2a1a86eb1..14e244d5e 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2,6 +2,12 @@ #include "../acesrc/acebot.h" #include "botlib.h" +int dc_total_male_names; // Total male names +int dc_total_female_names; // Total female names +bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy +bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy +bot_connections_t bot_connections; + // Find a free entity for the bot to use edict_t* BOTLIB_FindFreeEntity(void) { @@ -475,9 +481,9 @@ void BOTLIB_RandomizeTeamSkins(edict_t* bot) //Com_Printf("%s after T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); // Update skin associated indexes - Com_sprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); - Com_sprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); - Com_sprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); + Q_snprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); + Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); + Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); // Change team picture using the updated skin indexes level.pic_teamskin[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); diff --git a/src/action/botlib/botlib_spawnpoints.c b/src/action/botlib/botlib_spawnpoints.c index 6a00665d7..e84f62cd7 100644 --- a/src/action/botlib/botlib_spawnpoints.c +++ b/src/action/botlib/botlib_spawnpoints.c @@ -2,6 +2,10 @@ #include "../acesrc/acebot.h" #include "botlib.h" +dc_sp_t* dc_sp; +int dc_sp_count; // Total spawn points +qboolean dc_sp_edit; // If the spawn points have been made visible for editing + // Free the memory void DC_Free_Spawnpoints(void) { diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index ad4e1fbe2..2ad8e569a 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -215,7 +215,7 @@ #include "m_player.h" #ifndef NO_BOTS -void Cmd_Placenode_f( edict_t *ent ); +//void Cmd_Placenode_f( edict_t *ent ); static void Cmd_PlaceTrigger_f( edict_t *ent ) { @@ -1975,7 +1975,7 @@ static cmdList_t commandList[] = { "gamesettings", Cmd_PrintSettings_f, 0 }, { "follow", Cmd_Follow_f, 0 }, #ifndef NO_BOTS - { "placenode", Cmd_Placenode_f, 0 }, + // { "placenode", Cmd_Placenode_f, 0 }, { "placetrigger", Cmd_PlaceTrigger_f, 0 }, #endif //vote stuff diff --git a/src/action/g_local.h b/src/action/g_local.h index 50334719a..c5efd351d 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -294,6 +294,7 @@ #ifndef NO_BOTS #include "acesrc/botnav.h" +#include "botlib/botlib.h" #endif #define getEnt(entnum) (edict_t *)((char *)globals.edicts + (globals.edict_size * entnum)) //AQ:TNG Slicer - This was missing @@ -1474,6 +1475,9 @@ char *vtos (const vec3_t v); float vectoyaw (vec3_t vec); void vectoangles (vec3_t vec, vec3_t angles); +// g_trigger.c +void hurt_touch(edict_t* self, edict_t* other, cplane_t* plane, csurface_t* surf); + // // g_combat.c // @@ -1725,6 +1729,12 @@ typedef struct qboolean connected; // a loadgame will leave valid entities that // just don't have a connection yet + //rekkie -- surface data -- s +//#if DEBUG_DRAWING + debug_draw_t* draw; // debug drawing functions +//#endif + //rekkie -- surface data -- e + qboolean silence_banned; //rekkie -- silence ban int admin; @@ -2105,6 +2115,185 @@ struct gclient_s }; +//rekkie -- s +#ifndef NO_BOTS + +typedef enum { + HIGHLIGHTED_NODE_NONE, // No interaction + HIGHLIGHTED_NODE_SELECT, // Node selection + HIGHLIGHTED_NODE_SELECT_SMART, // Smart node selection + HIGHLIGHTED_NODE_ADD, // Add nodes (either by walking around or mouse1 '+attack' on the ground) + HIGHLIGHTED_NODE_FLOODFILL, // Flood fills the area with nodes (mouse1 '+attack') + HIGHLIGHTED_NODE_LINK, // Add/del link between prev and curr highlighted nodes + HIGHLIGHTED_NODE_MOVE, // Move nodes + HIGHLIGHTED_NODE_TYPE, // Edit node types: NODE_MOVE, ladder, jump, etc (mouse1 '+attack' to change type) + HIGHLIGHTED_NODE_LINKTYPE, // Edit node-to-node link type: crouch, jumppad, boxjump, etc + HIGHLIGHTED_NODE_DEL // Delete nodes +} highlighted_type_t; +typedef struct walknodes_s +{ + int last_type; // Previous node type + int touched_node; // Last nodes touched + vec3_t last_ground_loc; // Location the player last touched the ground + vec3_t last_ground_normal; // Normal the player last touched + int last_ground_touch; + + qboolean enabled; // If we're editing nodes or not + highlighted_type_t highlighted_node_type; // Link, move, ... highlighted_type_t + int highlighted_node; // The node currently being highlighted + int prev_highlighted_node; // The node previously highlighted + int highlighted_time; // Highlight timer + int highlighted_counter; + + + vec3_t selection_start; // Starting point of the selection square + vec3_t selection_end; // Ending point of the selection square + float selection_max; // Sets the highest selection point + float selection_min; // Sets the lowest selection point + int selection_node_first; // First selected node (used to get first area selected) + int selection_node_count; // Selected nodes count + int selected_node_count_prev; // Prevous selected nodes count + int selection_nodes[MAX_NAV_AREAS_NODES]; // Selected nodes are stored here + int selection_area; // Sets the area num + unsigned int selection_area_color; // Sets the area color + qboolean selection_area_used; // Checks if area num was used + +} walknodes_t; +// Hold all the bot information in a single struct +#define MAX_NODELIST 512 + +typedef enum { + BOT_TYPE_NONE, // No bot type + BOT_TYPE_LTK, // LTK - OG bots + BOT_TYPE_BOTLIB, // BOTLIB -- New bots +} bot_type_t; + +typedef struct bot_s +{ + int bot_type; + + int state; // Bot states: STATE_MOVE, STATE_WANDER, etc + + int pause_time; // Pause bot logic, this is a countdown timer (useful for debugging) + + // Nodes + int goal_node; // The node the bot should try to reach + int current_node; // The current node the bot (or player) is at + int next_node; // The next node (if any) the bot needs to reach + int prev_node; // The previous node (if any) the bot came from + + // Area NAV + int start_area; // Starting area + int current_area; // Current area we're in + int goal_area; // Goal area to reach + int next_area; // Next area to reach + int next_area_nodes[MAX_NAV_AREAS_EDGES]; // Edge-to-edge nodes + int next_area_nodes_counter; // Keep track of next node array num + int next_area_node; // Next node to reach + int next_area_counter; // Keep track of next area array num + int path_taken; // If there are multiple paths, select one and save it here + int area_heatmap[MAX_NAV_AREAS]; // Track how often an area gets used + + vec3_t tmp_position; // + vec3_t last_position; // Remember last position + + int bot_baseline_ping; // Set the baseline ping for this bot //rekkie -- Fake Bot Client + int bot_ping; // Set the fake ping using its baseline //rekkie -- Fake Bot Client + + // Bot inputs + bot_input_t bi; + qboolean jumppad; // Jump was conducted + int node_jump_from; // Node bot jumped from + int node_jump_to; // Node bot jumped to + float jumppad_last_time; // Time until jump can be used again + float jumppad_land_time; // Time until landing measures can take place + trace_t touch_ground; // If touching the ground. (touch_ground.fraction < 1.0) is true, (touch_ground.fraction == 1.0) is false + qboolean touching_ladder; // If bot is touching a ladder + + // Stuck prevention + qboolean stuck; // If the bot is stuck true/false + int stuck_tries; // How many times the bot tried to unstick + int stuck_wander_time; // Wander until counter is zero + vec3_t stuck_random_dir; + int stuck_last_negate; // Last time we negated the bot's velocity to stop it from sliding off a steep slope + qboolean stuck_rnd_dir; // If the bot is trying a random direction to get unstuck + int stuck_node; // The node the bot trying to head to if stuck + vec3_t stuck_pos; // Current pos we're stuck at + vec3_t stuck_old_pos; // Previous pos we were stuck at + + // Nodes + int node_list[MAX_NODELIST]; // A copy of the pathList each time it's created - useful to see any nodes the list contained + int node_list_count; // Total nodes added to the current node_list + int node_list_current; // The current node + qboolean node_random_path; // If the bot is taking a direct or random path to goal + // + int node_prev; // The previous node + int node_travel_time; // The time taken to reach the next node + int highlighted_node; + vec3_t bot_walk_dir; // The current direction from the bot to the next node + // + qboolean node_poi_holding; // If the bot is currently holding + float node_poi_time; // How long the bot should stay at a point of interest node + int node_poi_lookat_node; // Which node the bot is looking at + float node_poi_look_time; // How long to look at a lookat node + + // Weapons + int last_sniper_zoom_time; // Delay time between sniper zoom attempts + int last_weapon_change_time; // Delay time between changing weapons + int last_weapon_reload_time; // Delay time between reloading weapons + + // Skill + float skill; // Variable bot skill. Allow the bot to increase or decrease its own skill based on its score (kills) and bot_skill + + // Items + edict_t *get_item; // The current item the bot wants and is located next or or inside of a node + + // CTF + bot_ctf_state_t bot_ctf_state; // Get flag, retrieve flag, intercept flag carrier, etc. + float ctf_support_time; // Time between ally support checks + + // Adding walk nodes + walknodes_t walknode; // Holds all the walk node data + + // Enemy & Damage + edict_t *old_enemy; // Previous enemy, if any + qboolean see_enemies; // If the bot has line of sight to an enemy + float reaction_time; // How quickly the bot reacts to an enemy in sight + qboolean enemy_in_xhair; // If bot's crosshair is lined up with enemy + float enemy_dist; // Distance to enemy + float enemy_height_diff; // Difference in height between bot and enemy + int enemies_num; // Number of enemies in sight + int enemies[MAX_CLIENTS]; // Store the enemies in sight + int enemies_weight[MAX_CLIENTS];// Store the enemies by weight + float enemy_seen_time; // Last time bot sighted the enemy + vec3_t enemy_seen_loc; // Last seen location + float bot_bandage_delay_time; // Delay before bandaging + int enemy_chase_time; // Delay before chasing enemy attempts + + // Allies + int allies_num; // Number of allies in sight + int allies[MAX_CLIENTS]; // Store the allies in sight + qboolean ff_allies_after_rnd; // If the bot is allowed to kill friendlies after a TP round ends + + + // Radio + qboolean radioTeamReportedIn; // Request team report in + int radioLastSawAllyTime; // Last time since bot saw an ally + int radioTeamReportedInDelay; // Add a delay before triggering the call to request a "team report in" call + // + qboolean radioReportIn; // Flag if bot needs to respond to a "team report in" request + int radioReportInTime; // Time a "team report in" request + int radioReportInDelay; // Delay when responding to a "team report in" request + // + qboolean radioBandaging; // Flag true to get the bot to radio report + // + char radioLastHumanMsg[48]; // Get the lastest radio call from a real player on our team + // + qboolean radioReportKills; // Flag if the bot reports its kills in radio and chat +} bot_t; +#endif +//rekkie -- e + struct edict_s { entity_state_t s; @@ -2267,7 +2456,56 @@ struct edict_s int classnum; int typeNum; + float friction; // If the ent has friction, default 1.0 //rekkie -- DEV_1 -- s + + //rekkie - Quake3 -- s +#ifndef NO_BOTS + bot_t bot; + //rekkie -- surface data -- s + nav_t* nav; + //rekkie -- surface data -- e + + //rekkie -- DEV_1 -- s + botlib_sll_t pathList; // Single linked list of node numbers + //rekkie -- DEV_1 -- e + + int current_link; // current link -- //rekkie + + vec3_t nearest_path_point; // between curr -> next node, this is the origin the bot is closest to along that path + + float just_spawned_timeout; // Delay before first moving after spawning + qboolean just_spawned; // If the bot has just spawned + qboolean just_spawned_go; // When the bot should move after spawning + // + int last_touched_ground; // Last time player/bot touched ground + int last_jumppad_node; // Last node we conducted a jumppad from + float next_node_distance; // Keep trace of distance + int prev_next_node; + byte strafe_jumps; // Keep track of how many strafe jumps were made + // + //int num_nextnodes; // How many nodes in the nodepath + //int *nextnode; // Node node list + // + //int tmp_curr_node; + //int tmp_prev_node; + //int prev_node; + vec3_t prev_velocity; + char node_name[32]; // Node name consisting of node num + node type + int node_num; // Node number is here so it can be linked back to nodes[nodenum].nodenum + // + float highest_velocity; + // + int show_node_links; // Display node links from the node we're looking at + int show_node_links_time; // Time to display links + +#endif + //rekkie - Quake3 -- e + #ifndef NO_BOTS + qboolean velocity_clear; + vec3_t velocity_peak; // record the player peak velocity + float speed_peak; + int old_health; int recheck_timeout; @@ -2275,7 +2513,7 @@ struct edict_s qboolean is_bot; qboolean is_jumping; - qboolean is_triggering; + qboolean is_triggering; // For movement vec3_t move_vector; @@ -2289,21 +2527,24 @@ struct edict_s float teamPauseTime; // To stop the centipede effect and seperate the team out a little qboolean teamReportedIn; // Have we reported in yet? float lastRadioTime; // Don't use the radio too often - // Path to follow - ltklist_t pathList; // Single linked list of node numbers + // Path to follow + float antLastCallTime; // Check for calling complex pathsearcher // Who killed me? edict_t *lastkilledby; // Set in ClientObituary... int grenadewait; // Raptor007: Moved here from player_state_t. //AQ2 END - // For node code - int current_node; // current node - int goal_node; // current goal node + // For node code + + int current_node; // current node int next_node; // the node that will take us one step closer to our goal + int goal_node; // current goal node int node_timeout; int last_node; - int tries; + int tries; + + // AI related stuff int weaponchoice; @@ -2327,7 +2568,7 @@ struct edict_s int bot_speed; qboolean bCrawl; qboolean bLastJump; - vec3_t lastPosition; + vec3_t lastPosition; qboolean nameused[NUMNAMES][NUMNAMES]; qboolean newnameused[AQ2WTEAMSIZE]; #if AQTION_EXTENSION diff --git a/src/action/g_main.c b/src/action/g_main.c index b711a78d2..c81fbd740 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -479,7 +479,19 @@ cvar_t *ltk_chat; cvar_t *ltk_routing; cvar_t *ltk_botfile; cvar_t *ltk_loadbots; -cvar_t *ltk_classic; +//rekkie -- DEV_1 -- s +cvar_t* bot_showpath; +cvar_t* bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! +cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +cvar_t* bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost +cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +cvar_t* bot_maxteam; // Max bots allowed in autoteam +cvar_t* bot_rush; // Bots rush players by going directly for them +cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 max: 100] +cvar_t* bot_randskill; // When random bot join a game, they pick a random skill [min: 1, max: 10]. Using 0 will turn this off. +cvar_t* bot_randname; // Allow bots to pick a random name +//cvar_t* bot_randteamskin; // Bots can randomize team skins each map +//rekkie -- DEV_1 -- e #endif cvar_t *jump; // jumping mod diff --git a/src/action/g_save.c b/src/action/g_save.c index 3444d6bc9..700a1b446 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -678,7 +678,19 @@ void InitGame( void ) ltk_routing = gi.cvar( "ltk_routing", "0", 0 ); ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); - ltk_classic = gi.cvar( "ltk_classic", "1", 0); + //rekkie -- DEV_1 -- s + bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! + bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit + bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost + bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight + bot_showpath = gi.cvar("bot_showpath", "0", 0); + bot_maxteam = gi.cvar("bot_maxteam", "0", 0); + bot_rush = gi.cvar("bot_rush", "0", 0); + bot_randvoice = gi.cvar("bot_randvoice", "33", 0); + bot_randskill = gi.cvar("bot_randskill", "10", 0); + bot_randname = gi.cvar("bot_randname", "1", 0); + //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); + //rekkie -- DEV_1 -- e #endif // items diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 9bb7062f0..ce005ce9a 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1380,24 +1380,33 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn UnBan_TeamKillers(); + +//rekkie -- s #ifndef NO_BOTS - // Reload nodes and any persistent bots. - ACEND_InitNodes(); - ACEND_LoadNodes(); - // Normal operations, load LTK bots as normal - if (Q_stricmp(am->string, "0") == 0) { - ACESP_LoadBotConfig(); - } else { - // Reset bot count, load initial bots - game.bot_count = 0; - attract_mode_bot_check(); - } + //BOTLIB_THREAD_LOADAAS(false); // Threaded version -- this will also generate AAS if it doesn't exist + BOTLIB_InitNavigation(NULL); + +#ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); +#else + BOTLIB_LoadNav(); +#endif + + //ACEND_LoadAAS(false); // This will also generate AAS if it doesn't exist + //ACEND_BSP(NULL); + + // Bot connections + memset(&bot_connections, 0, sizeof(bot_connections)); - // Clear LTK bot names - LTKClearBotNames(); + // Player noises + memset(&botlib_noises, 0, sizeof(botlib_noises)); + //rekkie -- Fake Bot Client -- s + gi.SV_BotClearClients(); // So the server can clear all fake bot clients + //rekkie -- Fake Bot Client -- e #endif +//rekkie -- e } diff --git a/src/action/g_svcmds.c b/src/action/g_svcmds.c index 7ba36e0cf..1f9c5e57f 100644 --- a/src/action/g_svcmds.c +++ b/src/action/g_svcmds.c @@ -1094,63 +1094,67 @@ void ServerCommand (void) else if (Q_stricmp(cmd, "scramble") == 0) SVCmd_Scramble_f(); #ifndef NO_BOTS - else if(Q_stricmp (cmd, "botdebug") == 0) + else if (BOTLIB_SV_Cmds() == true) + return; + else if (Q_stricmp(cmd, "botdebug") == 0) { - if (strcmp(gi.argv(2),"on")==0) + if (strcmp(gi.argv(2), "on") == 0) { - gi.bprintf (PRINT_MEDIUM, "BOT: Debug Mode On\n"); + gi.bprintf(PRINT_MEDIUM, "BOT: Debug Mode On\n"); debug_mode = true; } else { - gi.bprintf (PRINT_MEDIUM, "BOT: Debug Mode Off\n"); + gi.bprintf(PRINT_MEDIUM, "BOT: Debug Mode Off\n"); debug_mode = false; } } //RiEvEr - new node visibility method - else if(Q_stricmp (cmd, "shownodes") == 0) + else if (Q_stricmp(cmd, "shownodes") == 0) { - if (strcmp(gi.argv(2),"on")==0) + if (strcmp(gi.argv(2), "on") == 0) { - gi.bprintf (PRINT_MEDIUM, "BOT: ShowNodes On\n"); + gi.bprintf(PRINT_MEDIUM, "BOT: ShowNodes On\n"); shownodes_mode = true; } else { - gi.bprintf (PRINT_MEDIUM, "BOT: ShowNodes Off\n"); + gi.bprintf(PRINT_MEDIUM, "BOT: ShowNodes Off\n"); shownodes_mode = false; } } - else if (Q_stricmp (cmd, "addbot") == 0) + else if (Q_stricmp(cmd, "addbot") == 0) { - if(teamplay->value) // team, name, skin (ignored) - ACESP_SpawnBot (gi.argv(2), gi.argv(3), gi.argv(4), NULL); + if (teamplay->value) // team, name, skin (ignored) + ACESP_SpawnBot(gi.argv(2), gi.argv(3), gi.argv(4), NULL); else // name, skin - ACESP_SpawnBot (NULL, gi.argv(2), gi.argv(3), NULL); + ACESP_SpawnBot(NULL, gi.argv(2), gi.argv(3), NULL); } - else if (Q_stricmp (cmd, "addbots") == 0) + else if (Q_stricmp(cmd, "addbots") == 0) { - if( gi.argc() >= 3 ) + if (gi.argc() >= 3) { int count = atoi(gi.argv(2)), i = 0; - for( i = 0; i < count; i ++ ) - ACESP_SpawnBot( gi.argv(3), NULL, NULL, NULL ); + for (i = 0; i < count; i++) + ACESP_SpawnBot(gi.argv(3), NULL, NULL, NULL); } else - gi.cprintf( NULL, PRINT_HIGH, "Usage: sv addbots []\n" ); + gi.cprintf(NULL, PRINT_HIGH, "Usage: sv addbots []\n"); } // removebot - else if(Q_stricmp (cmd, "removebot") == 0) + else if (Q_stricmp(cmd, "removebot") == 0) ACESP_RemoveBot(gi.argv(2)); + /* // Node saving - else if(Q_stricmp (cmd, "savenodes") == 0) + else if (Q_stricmp(cmd, "savenodes") == 0) ACEND_SaveNodes(); // Clear all node data. - else if(Q_stricmp (cmd, "initnodes") == 0) + else if (Q_stricmp(cmd, "initnodes") == 0) ACEND_InitNodes(); // Generate map entity nodes (items/doors/etc) and load saved nodes; you should probably "initnodes" first. - else if(Q_stricmp (cmd, "loadnodes") == 0) + else if (Q_stricmp(cmd, "loadnodes") == 0) ACEND_LoadNodes(); + */ #endif else gi.cprintf (NULL, PRINT_HIGH, "Unknown server command \"%s\"\n", cmd); diff --git a/src/action/p_client.c b/src/action/p_client.c index deaf92dbe..48196dfb5 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3019,7 +3019,7 @@ void ClientBeginDeathmatch(edict_t * ent) StatBotCheck(); #if USE_AQTION if(am->value){ - attract_mode_bot_check(); + //attract_mode_bot_check(); } #endif #endif @@ -3439,7 +3439,7 @@ void ClientDisconnect(edict_t * ent) #if USE_AQTION if(am->value){ - attract_mode_bot_check(); + //attract_mode_bot_check(); } #endif #endif diff --git a/src/action/p_hud.c b/src/action/p_hud.c index ccb45a9fd..65c01a90a 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -186,10 +186,6 @@ void BeginIntermission(edict_t *targ) } InitTransparentList(); - #ifndef NO_BOTS - // Clear LTK bot names - LTKClearBotNames(); - #endif } /* diff --git a/src/refresh/main.c b/src/refresh/main.c index c13561f80..0f6d50cb5 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -34,6 +34,13 @@ refcfg_t r_config; int registration_sequence; +draw_selection_t draw_selection; +draw_crosses_t draw_crosses[MAX_DRAW_CROSSES]; +draw_boxes_t draw_boxes[MAX_DRAW_BOXES]; +draw_arrows_t draw_arrows[MAX_DRAW_ARROWS]; +draw_string_t draw_strings[MAX_DRAW_STRINGS]; + + // regular variables cvar_t *gl_partscale; cvar_t *gl_partstyle; @@ -64,6 +71,7 @@ cvar_t *gl_drawworld; cvar_t *gl_drawentities; cvar_t *gl_drawsky; cvar_t *gl_showtris; +cvar_t* gl_showedges; //rekkie -- gl_showedges cvar_t *gl_showorigins; cvar_t *gl_showtearing; #if USE_DEBUG @@ -85,6 +93,10 @@ cvar_t *gl_lightgrid; cvar_t *gl_polyblend; cvar_t *gl_showerrors; +//rekkie -- Attach model to player -- s +cvar_t* gl_vert_diff; +//rekkie -- Attach model to player -- e + // ============================================================================== static const vec_t quad_tc[8] = { 0, 1, 0, 0, 1, 1, 1, 0 }; @@ -391,15 +403,1187 @@ static void GL_DrawNullModel(void) VectorMA(e->origin, 16, glr.entaxis[1], points[3]); VectorMA(e->origin, 16, glr.entaxis[2], points[5]); + //rekkie -- allow gl_showtris to show nodes points as a cross configuration -- s + //if (gl_showtris->integer && gl_showtris->integer < 0) // gl_showtris -1, etc. + //{ + // Extend NullModel points in opposite direction + VectorMA(e->origin, -16, glr.entaxis[0], points[0]); + VectorMA(e->origin, -16, glr.entaxis[1], points[2]); + VectorMA(e->origin, -16, glr.entaxis[2], points[4]); + //} + //rekkie -- allow gl_showtris to show nodes points as a cross configuration -- e + GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); GL_ColorBytePointer(4, 0, (GLubyte *)colors); GL_VertexPointer(3, 0, &points[0][0]); + + //rekkie -- allow NullModels to be seen behind walls -- s + //if (gl_showtris->integer && gl_showtris->integer < 0) + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (NullModels can now be seen behind walls) + //rekkie -- allow NullModels to be seen behind walls -- e + qglDrawArrays(GL_LINES, 0, 6); + + //rekkie -- allow NullModels to be seen behind walls -- s + //if (gl_showtris->integer && gl_showtris->integer < 0) + GL_DepthRange(0, 1); // Set the depth buffer back to normal (NullModels are now obscured) + //rekkie -- allow NullModels to be seen behind walls -- e +} + +//rekkie -- debug drawing -- s +#if DEBUG_DRAWING +#include "../server/server.h" +// Draws a line (useful to highlight edges, normals, etc for visual debugging) +void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, const float line_width, qboolean occluded) +{ + // Copy and lift the verts up a tiny bit so they're not z-clipping the surface + const float HEIGHT = 0.03; + vec3_t v[MAX_FACE_VERTS]; + for (int i = 0; i < num_points; i++) + { + VectorCopy(verts + i * 3, v[i]); + v[i][2] += HEIGHT; + } + + /* + //static const uint32_t color_white[6] = { U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN }; + if (colors == NULL) // If no color was sent, pick a generic color for all lines + { + uint32_t color[64]; + for (int i = 0; i < num_points * 2; i++) + { + color[i] = U32_WHITE; // Make all the line colors the same + } + GL_ColorBytePointer(4, 0, (GLubyte*)color); // Set the color of the line + } + else + { + GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + } + */ + + //GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + //GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + //GL_VertexPointer(3, 0, &verts[0][0]); + GL_VertexPointer(3, 0, &v[0][0]); + glLineWidth(line_width); // Set the line width + GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + if (occluded) + GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (seen behind walls) + qglDrawArrays(GL_LINES, 0, num_points); // GL_LINES is the type of primitive to render, 0 is the starting index, 2 is the number of indices to render + GL_DepthRange(0, 1); // Set the depth buffer back to normal (edges are now obscured) + glLineWidth(1.0); // Reset the line width to default +} + +#if 0 +image_t* r_charset = NULL; +void RenderText(const char* text, float x, float y, float z, float size) +{ + if (!r_charset) { + qhandle_t tmp; + tmp = R_RegisterFont("conchars"); + if (!tmp) return; + r_charset = IMG_ForHandle(tmp); + r_charset->texnum; + } + + if (0) + { + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(-1, 1, -1, 1, -1, 1); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + } + GL_LoadMatrix(glr.viewmatrix); + // + glTranslatef(x, y, z); + glScalef(size, -size, size); + //glEnable(GL_TEXTURE_2D); + GL_BindTexture(0, r_charset->texnum); //fontTexture); + + glColor3f(1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + float texcoord_value = 0.0625f; + for (const char* c = text; *c; c++) { + float cx = (float)(*c & 15) / 16.0f; + float cy = (float)(*c >> 4) / 16.0f; + glTexCoord2f(cx, 1 - cy - texcoord_value); + glVertex3f(-0.5f, -0.5f, 0.0f); + glTexCoord2f(cx + texcoord_value, 1 - cy - texcoord_value); + glVertex3f(0.5f, -0.5f, 0.0f); + glTexCoord2f(cx + texcoord_value, 1 - cy); + glVertex3f(0.5f, 0.5f, 0.0f); + glTexCoord2f(cx, 1 - cy); + glVertex3f(-0.5f, 0.5f, 0.0f); + glTranslatef(0.5f, 0.0f, 0.0f); + } + glEnd(); + + //glDisable(GL_TEXTURE_2D); + + if (0) + { + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + } + + /* + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + glLoadIdentity(); + glOrtho(-1, 1, -1, 1, -1, 1); + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + glTranslatef(x, y, z); + glScalef(size, -size, size); + glEnable(GL_TEXTURE_2D); + //glBindTexture(GL_TEXTURE_2D, r_charset->texnum); //fontTexture); + GL_BindTexture(GL_TEXTURE_2D, r_charset->texnum); //fontTexture); + //GL_BindTexture(0, TEXNUM_WHITE); + + glColor3f(1.0f, 1.0f, 1.0f); + glBegin(GL_QUADS); + float texcoord_value = 0.0625f; + for (const char* c = text; *c; c++) { + float cx = (float)(*c & 15) / 16.0f; + float cy = (float)(*c >> 4) / 16.0f; + glTexCoord2f(cx, 1 - cy - texcoord_value); + glVertex3f(-0.5f, -0.5f, 0.0f); + glTexCoord2f(cx + texcoord_value, 1 - cy - texcoord_value); + glVertex3f(0.5f, -0.5f, 0.0f); + glTexCoord2f(cx + texcoord_value, 1 - cy); + glVertex3f(0.5f, 0.5f, 0.0f); + glTexCoord2f(cx, 1 - cy); + glVertex3f(-0.5f, 0.5f, 0.0f); + glTranslatef(0.5f, 0.0f, 0.0f); + } + glEnd(); + + glDisable(GL_TEXTURE_2D); + + + glPopMatrix(); + glMatrixMode(GL_PROJECTION); + glPopMatrix(); + */ +} +#endif +#if 0 +// This function uses the player's view angles to calculate the position and orientation of the text, +// and then calls the glDrawString function to render the actual text. +// The function also adjusts the scale and transparency of the text based on the distance from the player's viewpoint. +void DrawStringAtOrigin(char* text, float x, float y, float z, float size) { + // Calculate the position of the text relative to the player's view + float position[3] = { x, y, z }; + float distance = sqrt(x * x + y * y + z * z); + float scale = 1.0f / distance * size; + float alpha = 1.0f; + + // Set up the OpenGL states for rendering the text as 2D billboard + glPushMatrix(); + glLoadIdentity(); + glTranslatef(position[0], position[1], position[2]); + glRotatef(-r_refdef.viewangles[1], 0.0f, 1.0f, 0.0f); + glRotatef(-r_refdef.viewangles[0], 1.0f, 0.0f, 0.0f); + glScalef(scale, scale, scale); + + // Call the glDrawString function with the desired inputs + glDrawString(text, 0.0f, 0.0f, 0.0f, alpha); + + // Reset the OpenGL states + glPopMatrix(); +} +#endif + + + + +// Show a cross configuration, useful to show nodes (instead of using ent + model) +void GL_DrawCross(vec3_t origin, qboolean occluded) +{ + static const uint32_t colors[6] = { + U32_RED, U32_RED, + U32_GREEN, U32_GREEN, + U32_BLUE, U32_BLUE + }; + vec3_t points[6]; + + // Center + VectorCopy(origin, points[0]); // X center + VectorCopy(origin, points[2]); // Y center + VectorCopy(origin, points[4]); // Z center + + // Positive axis + VectorMA(origin, 16, glr.entaxis[0], points[1]); // X+ + VectorMA(origin, 16, glr.entaxis[1], points[3]); // Y+ + VectorMA(origin, 16, glr.entaxis[2], points[5]); // Z+ + + // Negative axis (extend points in the opposite direction) + VectorMA(origin, -16, glr.entaxis[0], points[0]); // X- + VectorMA(origin, -16, glr.entaxis[1], points[2]); // Y- + VectorMA(origin, -16, glr.entaxis[2], points[4]); // Z- + + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); + glLineWidth(1); // Set the line width + + if (occluded) + GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (seen behind walls) + qglDrawArrays(GL_LINES, 0, 6); + GL_DepthRange(0, 1); // Set the depth buffer back to normal (obscured behind walls) +} + +void GL_DrawString(vec3_t origin, const char* string, const uint32_t color, qboolean occluded) +{ + int x, y; + color_t color_base; + color_base.u32 = 0xFFFFFFFF; + float r_viewmatrix[16]; + + //build view and projection matricies + float modelview[16]; + float proj[16]; + + Matrix4x4_CM_ModelViewMatrix(modelview, glr.fd.viewangles, glr.fd.vieworg); + Matrix4x4_CM_Projection2(proj, glr.fd.fov_x, glr.fd.fov_y, 4); + + //build the vp matrix + Matrix4_Multiply(proj, modelview, r_viewmatrix); + + + float v[4], tempv[4], out[4]; + + // get position + v[0] = origin[0]; + v[1] = origin[1]; + v[2] = origin[2]; + v[3] = 1; + + Matrix4x4_CM_Transform4(r_viewmatrix, v, tempv); + + if (tempv[3] < 0) // the element is behind us + return; + + tempv[0] /= tempv[3]; + tempv[1] /= tempv[3]; + tempv[2] /= tempv[3]; + + out[0] = (1 + tempv[0]) / 2; + out[1] = 1 - (1 + tempv[1]) / 2; + out[2] = tempv[2]; + + int hud_width = (r_config.width / 2); + int hud_height = (r_config.height / 2); + int hud_x = 0; + int hud_y = 0; + + x = hud_x + out[0] * hud_width; + y = hud_y + out[1] * hud_height; + + vec3_t org; + VectorSubtract(origin, glr.fd.vieworg, org); + float distance = VectorLength(org); + float mult = 300 / distance; // 300 + Q_clipf(mult, 0.25, 5); + + int sizex = 8 * mult; + int sizey = 8 * mult; + x -= (sizex / 2); + y -= (sizey / 2); + + int uiflags = 0; + + // Center string on the X axis + //int length = strlen(string); + //x -= (length * CHAR_WIDTH * 0.5); + + if (1) // Fade out text as it gets further away + { + float alpha = (384 - distance) / 2; + if (distance > 384) alpha = 0; + + color_base.u8[0] = 255; + color_base.u8[1] = 255; + color_base.u8[2] = 255; + color_base.u8[3] = alpha; + R_SetColor(color_base.u32); + } + + int font_pic = R_RegisterFont("conchars"); + R_DrawString(x, y, uiflags, MAX_STRING_CHARS, string, font_pic); + R_ClearColor(); + R_SetAlpha(1); +} + +int drawstring_total = 0; // Draw string total +int drawstring_count = 0; // Draw string counter +// GL_DrawString() is called to render the text +void DrawString(int number, vec3_t origin, const char* string, const uint32_t color, const int time, qboolean occluded) +{ + if (number + 1 > MAX_DRAW_STRINGS) return; + + if (draw_strings[number].inuse == false) + { + draw_strings[number].inuse = true; + draw_strings[number].time = time; + VectorCopy(origin, draw_strings[number].origin); + draw_strings[number].color = color; + strncpy(draw_strings[number].string, string, sizeof(string)); + draw_strings[number].occluded = occluded; + } +} +void GL_DrawStrings(void) // This is called in client/screen.c +{ + qboolean strings_in_use = false; // Strings in use + if (sv.cm.draw && sv.cm.draw->strings_inuse == true) // Only run when there's at least one or more in use + { + for (int i = 0; i < MAX_DRAW_STRINGS; i++) + { + if (draw_strings[i].time < 100) // Give the gamelib some time to renew the draw request + { + draw_strings[i].inuse = false; // Unlock + } + + if (draw_strings[i].time > 0) + { + strings_in_use = true; + draw_strings[i].time--; + GL_DrawString(draw_strings[i].origin, draw_strings[i].string, draw_strings[i].color, draw_strings[i].occluded); + } + } + } + + if (sv.cm.draw && sv.cm.draw->strings_inuse && strings_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use + sv.cm.draw->strings_inuse = false; +} + +int drawbox_total = 0; // Box point total +int drawbox_count = 0; // Box point counter +vec3_t *box_points; +uint32_t* box_colors; +static qboolean ALLOC_BoxPoints(int num_boxes) +{ + void* if_null_1 = NULL; + void* if_null_2 = NULL; + if (drawbox_total == 0) + { + drawbox_total = (num_boxes * 24); + box_points = (vec3_t*)malloc(drawbox_total * sizeof(vec3_t)); + box_colors = (uint32_t*)malloc(drawbox_total * sizeof(uint32_t)); + + } + else if (num_boxes != drawbox_total) + { + if (box_points[0] == NULL || box_colors[0] == NULL) + return false; + + if_null_1 = box_points; + if_null_2 = box_colors; + drawbox_total = (num_boxes * 24); + box_points = (vec3_t*)realloc(box_points, drawbox_total * sizeof(vec3_t)); + box_colors = (uint32_t*)realloc(box_colors, drawbox_total * sizeof(uint32_t)); + } + + if (box_points == NULL) + { + Com_Printf("%s: Failed to allocate memory for box_points\n", __func__); + if (if_null_1) + { + free(if_null_1); // Free previous memory + if_null_1 = NULL; // Nullify dangling pointer + } + return false; + } + if (box_colors == NULL) + { + Com_Printf("%s: Failed to allocate memory for box_colors\n", __func__); + if (if_null_2) + { + free(if_null_2); // Free previous memory + if_null_2 = NULL; // Nullify dangling pointer + } + return false; + } + + + + return true; +} +static void FREE_BoxPoint(void) +{ + if (box_points != NULL) + { + free(box_points); + box_points = NULL; + } + if (box_colors != NULL) + { + free(box_colors); + box_colors = NULL; + } + drawbox_total = 0; + drawbox_count = 0; +} + +// Single draw call: render min/max box +void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) +{ + const vec3_t axis[3] = { 1,0,0, 0,1,0, 0,0,1 }; + + uint32_t colors[24]; + for (int i = 0; i < 24; i++) + colors[i] = color; + + vec3_t points[24]; + + // Top face + vec3_t top; + VectorMA(origin, maxs[2], axis[2], top); + + + // Top + // Start at the top right corner + VectorMA(top, mins[1], axis[1], points[0]); // + VectorMA(points[0], maxs[0], axis[0], points[0]); // + VectorMA(points[0], (mins[0] * 2), axis[0], points[1]); // + + VectorCopy(points[1], points[2]); // + VectorMA(points[2], (maxs[1] * 2), axis[1], points[3]); // + + VectorCopy(points[3], points[4]); // + VectorMA(points[4], (maxs[0] * 2), axis[0], points[5]); // + + VectorCopy(points[5], points[6]); // + VectorMA(points[6], (mins[1] * 2), axis[1], points[7]); // + + + // Bottom face + vec3_t bottom; + VectorMA(origin, mins[2], axis[2], bottom); // Down from origin + + // Bottom + VectorMA(bottom, mins[1], axis[1], points[8]); // + VectorMA(points[8], maxs[0], axis[0], points[8]); // + VectorMA(points[8], (mins[0] * 2), axis[0], points[9]); // + + VectorCopy(points[9], points[10]); // + VectorMA(points[10], (maxs[1] * 2), axis[1], points[11]); // + + VectorCopy(points[11], points[12]); // + VectorMA(points[12], (maxs[0] * 2), axis[0], points[13]); // + + VectorCopy(points[13], points[14]); // + VectorMA(points[14], (mins[1] * 2), axis[1], points[15]); // + + + // Sides + VectorCopy(points[0], points[16]); // + VectorCopy(points[8], points[17]); // + + VectorCopy(points[2], points[18]); // + VectorCopy(points[10], points[19]); // + + VectorCopy(points[4], points[20]); // + VectorCopy(points[12], points[21]); // + + VectorCopy(points[6], points[22]); // + VectorCopy(points[14], points[23]); // + + + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); + glLineWidth(2); // Set the line width + + if (occluded) + GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (seen behind walls) + qglDrawArrays(GL_LINES, 0, 24); + GL_DepthRange(0, 1); // Set the depth buffer back to normal (obscured behind walls) +} + +// Batch draw call: render min/max box +void GL_AddDrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) +{ + const vec3_t axis[3] = { 1,0,0, 0,1,0, 0,0,1 }; + + if (drawbox_total && drawbox_count < drawbox_total) + { + int nbp = drawbox_count; // Box point + drawbox_count += 24; + + for (int i = 0; i < 24; i++) + box_colors[nbp + i] = color; + + // Top face + vec3_t top; + VectorMA(origin, maxs[2], axis[2], top); + + // Top + // Start at the top right corner + VectorMA(top, mins[1], axis[1], box_points[nbp]); // + VectorMA(box_points[nbp], maxs[0], axis[0], box_points[nbp]); // + VectorMA(box_points[nbp], (mins[0] * 2), axis[0], box_points[nbp + 1]); // + + VectorCopy(box_points[nbp + 1], box_points[nbp + 2]); // + VectorMA(box_points[nbp + 2], (maxs[1] * 2), axis[1], box_points[nbp + 3]); // + + VectorCopy(box_points[nbp + 3], box_points[nbp + 4]); // + VectorMA(box_points[nbp + 4], (maxs[0] * 2), axis[0], box_points[nbp + 5]); // + + VectorCopy(box_points[nbp + 5], box_points[nbp + 6]); // + VectorMA(box_points[nbp + 6], (mins[1] * 2), axis[1], box_points[nbp + 7]); // + + + // Bottom face + vec3_t bottom; + VectorMA(origin, mins[2], axis[2], bottom); // Down from origin + + // Bottom + VectorMA(bottom, mins[1], axis[1], box_points[nbp + 8]); // + VectorMA(box_points[nbp + 8], maxs[0], axis[0], box_points[nbp + 8]); // + VectorMA(box_points[nbp + 8], (mins[0] * 2), axis[0], box_points[nbp + 9]); // + + VectorCopy(box_points[nbp + 9], box_points[nbp + 10]); // + VectorMA(box_points[nbp + 10], (maxs[1] * 2), axis[1], box_points[nbp + 11]); // + + VectorCopy(box_points[nbp + 11], box_points[nbp + 12]); // + VectorMA(box_points[nbp + 12], (maxs[0] * 2), axis[0], box_points[nbp + 13]); // + + VectorCopy(box_points[nbp + 13], box_points[nbp + 14]); // + VectorMA(box_points[nbp + 14], (mins[1] * 2), axis[1], box_points[nbp + 15]); // + + + // Sides + VectorCopy(box_points[nbp + 0], box_points[nbp + 16]); // + VectorCopy(box_points[nbp + 8], box_points[nbp + 17]); // + + VectorCopy(box_points[nbp + 2], box_points[nbp + 18]); // + VectorCopy(box_points[nbp + 10], box_points[nbp + 19]); // + + VectorCopy(box_points[nbp + 4], box_points[nbp + 20]); // + VectorCopy(box_points[nbp + 12], box_points[nbp + 21]); // + + VectorCopy(box_points[nbp + 6], box_points[nbp + 22]); // + VectorCopy(box_points[nbp + 14], box_points[nbp + 23]); // + + } +} +void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) +{ + if (drawbox_total) + { + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); + GL_VertexPointer(3, 0, &box_points[0][0]); + glLineWidth(2); // Set the line width + + if (occluded) + GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (seen behind walls) + + qglDrawArrays(GL_LINES, 0, drawbox_total); + + GL_DepthRange(0, 1); // Set the depth buffer back to normal (obscured behind walls) + } +} + +// Single draw call: render a flat 2D square +void GL_DrawSelectionSquare(vec3_t start, vec3_t end, float min, float max, uint32_t color, float line_width, qboolean occluded) +{ + // I just need the starting point and an end point. X and Y are offset from the starting point. + // Start 0,0 End 100,100 + // + // Areas: + // * Go to area selection mode in the nav editor + // * Only show nodes (no links) and a number to indicate their area num. By default all nodes are area 0. + // * Select desired nodes + // * Use a console command to assign a number nav_areanum + + //start[0] = 100; + //start[1] = 100; + //start[2] = 0; + //end[0] = 100; + //vec3_t st; + //vec3_t en; + + + uint32_t colors[12]; + for (int i = 0; i < 12; i++) + colors[i] = color; + + vec3_t points[8]; + float x_diff = (end[0] - start[0]); + float y_diff = (end[1] - start[1]); + + // Draw the lower selection box + { + start[2] = min; + end[2] = min; + + // Draw line from start to end on the X axis + VectorCopy(start, points[0]); // Start + VectorCopy(start, points[1]); // End (right 100) + points[1][0] += x_diff; + GL_DrawLine(points[0], 2, colors, line_width, occluded); + + // Draw line from start to end on the Y axis + VectorCopy(start, points[2]); // Start + VectorCopy(start, points[3]); // End (up 100) + points[3][1] += y_diff; + GL_DrawLine(points[2], 2, colors, line_width, occluded); + + // Draw line from end X to end X,Y + VectorCopy(points[1], points[4]); // Start (right 100) + VectorCopy(points[4], points[5]); // End (up 100) + points[4][1] += y_diff; + GL_DrawLine(points[4], 2, colors, line_width, occluded); + + // Draw line from end Y to end X,Y + VectorCopy(points[3], points[6]); // Start (up 100) + VectorCopy(points[1], points[7]); // End (right 100) + points[7][1] += y_diff; + GL_DrawLine(points[6], 2, colors, line_width, occluded); + } + + // Draw the sides of the selection box + { + // Draw line from point 0 going up + //VectorCopy(start, points[0]); // Start + VectorCopy(start, points[1]); // Up + points[1][2] = max; + GL_DrawLine(points[0], 2, colors, line_width, occluded); + + // Draw line from point 2 going up + VectorCopy(points[3], points[2]); // Start + //VectorCopy(start, points[3]); // End (up 100) + //points[3][1] += y_diff; + points[3][2] = max; + GL_DrawLine(points[2], 2, colors, line_width, occluded); + + // Draw line from point 5 going up + //VectorCopy(points[4], points[4]); // Start (right 100) + VectorCopy(points[4], points[5]); // End (up 100) + //points[4][1] += y_diff; + points[5][2] = max; + GL_DrawLine(points[4], 2, colors, line_width, occluded); + + // Draw line from point 7 going up + VectorCopy(start, points[6]); + points[6][0] += x_diff; + //points[6][1] += y_diff; + points[6][2] = min; + VectorCopy(points[6], points[7]); + points[7][2] = max; + GL_DrawLine(points[6], 2, colors, line_width, occluded); + } + + // Draw the upper selection box + { + start[2] = max; + end[2] = max; + + // Draw line from start to end on the X axis + VectorCopy(start, points[0]); // Start + VectorCopy(start, points[1]); // End (right 100) + points[1][0] += x_diff; + GL_DrawLine(points[0], 2, colors, line_width, occluded); + + // Draw line from start to end on the Y axis + VectorCopy(start, points[2]); // Start + VectorCopy(start, points[3]); // End (up 100) + points[3][1] += y_diff; + GL_DrawLine(points[2], 2, colors, line_width, occluded); + + // Draw line from end X to end X,Y + VectorCopy(points[1], points[4]); // Start (right 100) + VectorCopy(points[4], points[5]); // End (up 100) + points[4][1] += y_diff; + GL_DrawLine(points[4], 2, colors, line_width, occluded); + + // Draw line from end Y to end X,Y + VectorCopy(points[3], points[6]); // Start (up 100) + VectorCopy(points[1], points[7]); // End (right 100) + points[7][1] += y_diff; + GL_DrawLine(points[6], 2, colors, line_width, occluded); + } + +} + + + +int drawarrow_total = 0; // Draw arrows total +int drawarrow_count = 0; // Draw arrows counter +vec3_t* arrow_points = NULL; +uint32_t* arrow_colors = NULL; +qboolean ALLOC_Arrows(int num_arrows) +{ + void* if_null_1 = NULL; + void* if_null_2 = NULL; + + if (drawarrow_total == 0) + { + drawarrow_total = (num_arrows * 8); // 8 == 4 lines (edges) or 8 verts + arrow_points = (vec3_t*)malloc(drawarrow_total * sizeof(vec3_t)); + arrow_colors = (uint32_t*)malloc(drawarrow_total * sizeof(uint32_t)); + + } + //else if (num_arrows != drawarrow_total) + else if ((num_arrows * 8) != drawarrow_total) + { + if (arrow_points == NULL || arrow_colors == NULL) + return false; + + if_null_1 = arrow_points; + if_null_2 = arrow_colors; + drawarrow_total = (num_arrows * 8); + arrow_points = (vec3_t*)realloc(arrow_points, drawarrow_total * sizeof(vec3_t)); + arrow_colors = (uint32_t*)realloc(arrow_colors, drawarrow_total * sizeof(uint32_t)); + } + + if (arrow_points == NULL) + { + Com_Printf("%s: Failed to allocate memory for arrow_points\n", __func__); + if (if_null_1) + { + free(if_null_1); // Free previous memory + if_null_1 = NULL; // Nullify dangling pointer + } + return false; + } + if (arrow_colors == NULL) + { + Com_Printf("%s: Failed to allocate memory for arrow_colors\n", __func__); + if (if_null_2) + { + free(if_null_2); // Free previous memory + if_null_2 = NULL; // Nullify dangling pointer + } + return false; + } + + return true; +} +void FREE_Arrows(void) +{ + if (arrow_points != NULL) + { + free(arrow_points); + arrow_points = NULL; + } + if (arrow_colors != NULL) + { + free(arrow_colors); + arrow_colors = NULL; + } + drawarrow_total = 0; + drawarrow_count = 0; +} + +//=========================================================================== +// +//=========================================================================== +void GL_DrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, qboolean occluded) +{ + const uint32_t colors[2] = { color, color }; + vec3_t points[2]; + vec3_t dir, cross, p1, p2, up = { 0, 0, 1 }; + float dot; + + // Limit the width for short distance runs + //if (line_width > 4 && VectorDistance(start, end) <= 32) + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + if (line_width > 4 && VectorLength(v) <= 32) + line_width = 4; + + VectorSubtract(end, start, dir); + VectorNormalize(dir); + dot = DotProduct(dir, up); + if (dot > 0.99 || dot < -0.99) VectorSet(cross, 1, 0, 0); + else CrossProduct(dir, up, cross); + + // Draw the arrow line + VectorCopy(start, points[0]); + VectorCopy(end, points[1]); + GL_DrawLine(points[0], 2, colors, line_width, occluded); + + // Arrow at the start + { + vec3_t start2 = { start[0], start[1], start[2] }; + VectorMA(start, 16, dir, start2); // Move the start forward a bit + + VectorMA(start2, -8, dir, p1); + VectorCopy(p1, p2); + VectorMA(p1, 8, cross, p1); + VectorMA(p2, -8, cross, p2); + + // Draw the arrow head p1 - right + VectorCopy(p1, points[0]); + VectorCopy(start2, points[1]); + GL_DrawLine(points[0], 2, colors, line_width * 3, occluded); + + // Draw the arrow head p2 - left + VectorCopy(p2, points[0]); + VectorCopy(start2, points[1]); + GL_DrawLine(points[0], 2, colors, line_width * 3, occluded); + + // Draw the arrow head p3 - bottom + VectorCopy(p1, points[0]); + VectorCopy(p2, points[1]); + GL_DrawLine(points[0], 2, colors, line_width * 3, occluded); + } +} +void GL_AddDrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, qboolean occluded) +{ + if (drawarrow_total && drawarrow_count < drawarrow_total) + { + int arc = drawarrow_count; // Arrow counter + drawarrow_count += 8; + + for (int i = 0; i < 8; i++) + arrow_colors[arc + i] = color; + + vec3_t dir, cross, p1, p2, up = { 0, 0, 1 }; + float dot; + + // Limit the width for short distance runs + //if (line_width > 4 && VectorDistance(start, end) <= 32) + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + if (line_width > 4 && VectorLength(v) <= 32) + line_width = 4; + + VectorSubtract(end, start, dir); + VectorNormalize(dir); + dot = DotProduct(dir, up); + if (dot > 0.99 || dot < -0.99) VectorSet(cross, 1, 0, 0); + else CrossProduct(dir, up, cross); + + // Draw the arrow line + VectorCopy(start, arrow_points[arc]); + VectorCopy(end, arrow_points[arc + 1]); + //GL_DrawLine(arrow_points[0], 2, arrow_colors, line_width, occluded); + + // Arrow at the start + { + vec3_t start2 = { start[0], start[1], start[2] }; + VectorMA(start, 16, dir, start2); // Move the start forward a bit + + VectorMA(start2, -8, dir, p1); + VectorCopy(p1, p2); + VectorMA(p1, 8, cross, p1); + VectorMA(p2, -8, cross, p2); + + // Draw the arrow head p1 - right + VectorCopy(p1, arrow_points[arc + 2]); + VectorCopy(start2, arrow_points[arc + 3]); + //GL_DrawLine(arrow_points[0], 2, arrow_colors, line_width * 3, occluded); + + // Draw the arrow head p2 - left + VectorCopy(p2, arrow_points[arc + 4]); + VectorCopy(start2, arrow_points[arc + 5]); + //GL_DrawLine(arrow_points[0], 2, arrow_colors, line_width * 3, occluded); + + // Draw the arrow head p3 - bottom + VectorCopy(p1, arrow_points[arc + 6]); + VectorCopy(p2, arrow_points[arc + 7]); + //GL_DrawLine(arrow_points[0], 2, arrow_colors, line_width * 3, occluded); + } + } +} //end of the function GL_DrawArrow +void GL_BatchDrawArrows(qboolean occluded) +{ + if (drawarrow_total) + { + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_VertexPointer(3, 0, &arrow_points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line + //glLineWidth(line_width); // Set the line width + + if (occluded) + GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (seen behind walls) + + qglDrawArrays(GL_LINES, 0, drawarrow_total); // GL_LINES is the type of primitive to render, 0 is the starting index, 2 is the number of indices to render + + GL_DepthRange(0, 1); // Set the depth buffer back to normal (edges are now obscured) + glLineWidth(1.0); // Reset the line width to default + } +} + +int drawcross_total = 0; // Draw cross total +int drawcross_count = 0; // Draw cross counter +// GL_DrawCross() is called to render the array +void DrawCross(int number, vec3_t origin, int time, qboolean occluded) +{ + if (number + 1 > MAX_DRAW_CROSSES) return; + + if (draw_crosses[number].time < 100) // Give the gamelib some time to renew the draw request + { + draw_crosses[number].inuse = false; // Unlock + } + + // Only update the cross if it's not in use or the origin has changed + if (draw_crosses[number].inuse == false || VectorCompare(origin, draw_crosses[number].origin) == false) + { + draw_crosses[number].inuse = true; + draw_crosses[number].time = time; + VectorCopy(origin, draw_crosses[number].origin); + draw_crosses[number].occluded = occluded; + } +} +static void GL_DrawCrosses(void) +{ + qboolean crosses_in_use = false; // Crosses in use + if (sv.cm.draw && sv.cm.draw->boxes_inuse == true) // Only run when there's at least one or more in use + { + for (int i = 0; i < MAX_DRAW_CROSSES; i++) + { + if (draw_crosses[i].time > 0) + { + crosses_in_use = true; + draw_crosses[i].time--; + GL_DrawCross(draw_crosses[i].origin, draw_crosses[i].occluded); + } + } + } + if (sv.cm.draw && sv.cm.draw->crosses_inuse && crosses_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use + sv.cm.draw->crosses_inuse = false; +} + +// GL_DrawBoxes() is called to render the array +void DrawBox(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) +{ + if (number + 1 > MAX_DRAW_BOXES) return; + + if (draw_boxes[number].time < 100) // Give the gamelib some time to renew the draw request + { + draw_boxes[number].inuse = false; // Unlock + } + + // Only update the box if it's not in use or the origin has changed + if (draw_boxes[number].inuse == false || VectorCompare(origin, draw_boxes[number].origin) == false) + { + draw_boxes[number].inuse = true; + draw_boxes[number].time = time; + draw_boxes[number].color = color; + VectorCopy(mins, draw_boxes[number].mins); + VectorCopy(maxs, draw_boxes[number].maxs); + VectorCopy(origin, draw_boxes[number].origin); + draw_boxes[number].occluded = occluded; + } +} +static void GL_DrawBoxes(void) +{ + // Get number of boxes to render + qboolean boxes_in_use = false; // Boxes in use + int num_boxes = 0; // Batches boxes + if (sv.cm.draw && sv.cm.draw->boxes_inuse == true) // Only run when there's at least one or more in use + { + for (int i = 0; i < MAX_DRAW_BOXES; i++) + { + if (draw_boxes[i].time) + { + boxes_in_use = true; + draw_boxes[i].time--; + + if (draw_boxes[i].occluded) // Batched boxes + num_boxes++; + else // Just do a single draw call for non-occluded boxes + GL_DrawBox(draw_boxes[i].origin, draw_boxes[i].color, draw_boxes[i].mins, draw_boxes[i].maxs, draw_boxes[i].occluded); + } + } + } + + if (num_boxes) // Batch draw the boxes + { + ALLOC_BoxPoints(num_boxes); // Memory allocation + + // Add them to the batch + for (int i = 0; i < MAX_DRAW_BOXES; i++) + { + if (draw_boxes[i].time) + { + //draw_boxes[i].time--; + GL_AddDrawBox(draw_boxes[i].origin, draw_boxes[i].color, draw_boxes[i].mins, draw_boxes[i].maxs, draw_boxes[i].occluded); + } + } + drawbox_count = 0; // Reset the box point counter + + // Batch render them all in one pass + GL_BatchDrawBoxes(num_boxes, true); + } + else if (drawbox_total) + FREE_BoxPoint(); // Free memory + + if (sv.cm.draw && sv.cm.draw->boxes_inuse && boxes_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use + sv.cm.draw->boxes_inuse = false; } +// Adds the arrow to an array of arrows to draw. +// GL_DrawArrows() is called to render the array +// occluded: true - don't draw if occluded by a wall +void DrawArrow(int number, vec3_t start, vec3_t end, uint32_t color, float line_width, int time, qboolean occluded) +{ + if (number + 1 > MAX_DRAW_ARROWS) return; + + if (draw_arrows[number].time < 100) // Give the gamelib some time to renew the draw request + { + draw_arrows[number].inuse = false; // Unlock + } + + if (draw_arrows[number].inuse == false) + { + draw_arrows[number].inuse = true; + draw_arrows[number].time = time; + draw_arrows[number].color = color; + draw_arrows[number].line_width = line_width; + VectorCopy(start, draw_arrows[number].start); + VectorCopy(end, draw_arrows[number].end); + draw_arrows[number].occluded = occluded; + } +} +static void GL_DrawArrows(void) +{ + // Get number of arrows to render + qboolean arrows_in_use = false; // Arrows in use + int num_arrows = 0; // Batched arrows + if (sv.cm.draw && sv.cm.draw->arrows_inuse == true) // Only run when there's at least one or more in use + { + for (int i = 0; i < MAX_DRAW_ARROWS; i++) + { + if (draw_arrows[i].time) + { + arrows_in_use = true; + draw_arrows[i].time--; + + if (draw_arrows[i].occluded && draw_arrows[i].line_width == 1.0f) // Batched arrows + num_arrows++; + else // Just do a single draw call for non-occluded arrows + GL_DrawArrow(draw_arrows[i].start, draw_arrows[i].end, draw_arrows[i].color, draw_arrows[i].line_width, draw_arrows[i].occluded); + } + } + } + + if (num_arrows) // Batch draw the arrows + { + ALLOC_Arrows(num_arrows); // Memory allocation + + // Add them to the batch + for (int i = 0; i < MAX_DRAW_ARROWS; i++) + { + if (draw_arrows[i].time) + { + GL_AddDrawArrow(draw_arrows[i].start, draw_arrows[i].end, draw_arrows[i].color, draw_arrows[i].line_width, draw_arrows[i].occluded); + } + } + drawarrow_count = 0; // Reset the counter + + // Batch render them all in one pass + GL_BatchDrawArrows(true); + } + else if (drawarrow_total || num_arrows == 0) + FREE_Arrows(); // Free memory + + if (sv.cm.draw && sv.cm.draw->arrows_inuse && arrows_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use + sv.cm.draw->arrows_inuse = false; +} + +//GL_DrawSelectionSquare +void DrawSelection(vec3_t start, vec3_t end, float min, float max, uint32_t color, float line_width, int time, qboolean occluded) +{ + if (draw_selection.time < 100) // Give the gamelib some time to renew the draw request + { + draw_selection.inuse = false; // Unlock + } + + if (draw_selection.inuse == false) + { + draw_selection.inuse = true; + draw_selection.time = time; + draw_selection.color = color; + draw_selection.line_width = line_width; + VectorCopy(start, draw_selection.start); + VectorCopy(end, draw_selection.end); + draw_selection.min = min; + draw_selection.max = max; + draw_selection.occluded = occluded; + } +} +static void GL_DrawSelection(void) +{ + if (draw_selection.time) + { + //Com_Printf("%s start [%f %f %f] -- end [%f %f %f]\n", __func__, draw_selection.start[0], draw_selection.start[1], draw_selection.start[2], draw_selection.end[0], draw_selection.end[1], draw_selection.end[2]); + draw_selection.time--; + GL_DrawSelectionSquare(draw_selection.start, draw_selection.end, draw_selection.min, draw_selection.max, draw_selection.color, draw_selection.line_width, draw_selection.occluded); + } +} + +static void GL_InitDebugDraw() +{ + // Malloc nav + if (sv.cm.draw == NULL) + { + sv.cm.draw = (debug_draw_t*)Z_TagMallocz(sizeof(debug_draw_t), TAG_CMODEL); // This malloc is automatically freed in CM_FreeMap() upon every map change + if (sv.cm.draw == NULL) + { + Com_Error(ERR_DROP, "%s: malloc failed", __func__); + return; + } + + // Nothing to memset + sv.cm.draw->DrawSelection = DrawSelection; // Init the draw selection function + + memset(draw_strings, 0, sizeof(draw_strings)); // Clear the draw strings array + sv.cm.draw->DrawString = DrawString; // Init the draw string function + + memset(draw_crosses, 0, sizeof(draw_crosses)); // Clear the draw arrows array + sv.cm.draw->DrawCross = DrawCross; // Init the draw arrow function + + memset(draw_boxes, 0, sizeof(draw_boxes)); // Clear the draw arrows array + sv.cm.draw->DrawBox = DrawBox; // Init the draw arrow function + + memset(draw_arrows, 0, sizeof(draw_arrows)); // Clear the draw arrows array + sv.cm.draw->DrawArrow = DrawArrow; // Init the draw arrow function + } +} +#endif +//rekkie -- debug drawing -- e + static void make_flare_quad(const entity_t *e, float scale, vec3_t points[4]) { vec3_t up, down, left, right; @@ -709,6 +1893,11 @@ void R_RenderFrame(refdef_t *fd) GL_DrawWorld(); } +#if DEBUG_DRAWING + GL_DrawSelection(); + GL_DrawArrows(); // Draw arrows + GL_DrawBoxes(); // Draw crosses +#endif GL_DrawEntities(0, RF_TRANSLUCENT); GL_DrawBeams(); @@ -925,6 +2114,7 @@ static void GL_Register(void) gl_drawsky = Cvar_Get("gl_drawsky", "1", 0); gl_drawsky->changed = gl_drawsky_changed; gl_showtris = Cvar_Get("gl_showtris", "0", CVAR_CHEAT); + gl_showedges = Cvar_Get("gl_showedges", "0", CVAR_CHEAT); //rekkie -- gl_showedges gl_showorigins = Cvar_Get("gl_showorigins", "0", CVAR_CHEAT); gl_showtearing = Cvar_Get("gl_showtearing", "0", CVAR_CHEAT); #if USE_DEBUG @@ -949,6 +2139,10 @@ static void GL_Register(void) gl_polyblend = Cvar_Get("gl_polyblend", "1", 0); gl_showerrors = Cvar_Get("gl_showerrors", "1", 0); + //rekkie -- Attach model to player -- s + gl_vert_diff = Cvar_Get("gl_vert_diff", "0", 0); + //rekkie -- Attach model to player -- e + gl_lightmap_changed(NULL); gl_modulate_entities_changed(NULL); gl_swapinterval_changed(gl_swapinterval); @@ -1209,6 +2403,11 @@ void R_BeginRegistration(const char *name) GL_LoadWorld(fullname); } + //rekkie -- surface data -- s +#if DEBUG_DRAWING + GL_InitDebugDraw(); // Init debug drawing functions +#endif + //rekkie -- surface data -- e GL_ClearQueries(); } diff --git a/src/server/game.c b/src/server/game.c index 1e5b6cf8b..7f2e23e48 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1116,6 +1116,30 @@ void SV_InitGameProgs(void) import.CheckForExtension = G_CheckForExtension; #endif +//rekkie -- BSP -- s + //#ifdef ACTION_DLL + import.Bsp = SV_BSP; + //#endif + //rekkie -- BSP -- e + //rekkie -- surface data -- s + import.Nav = CS_NAV; + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING +//#if USE_REF + import.Draw = CS_DebugDraw; +//#endif +#endif +//rekkie -- debug drawing -- e + //import.SurfaceData = SV_SURFACE_DATA; + //rekkie -- surface data -- e + + //rekkie -- Fake Bot Client -- s + import.SV_BotUpdateInfo = SV_BotUpdateInfo; + import.SV_BotConnect = SV_BotConnect; + import.SV_BotDisconnect = SV_BotDisconnect; + import.SV_BotClearClients = SV_BotClearClients; + //rekkie -- Fake Bot Client -- e + ge = entry(&import); if (!ge) { Com_Error(ERR_DROP, "Game library returned NULL exports"); diff --git a/src/server/main.c b/src/server/main.c index 5ca98e6cc..a190ed488 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -21,6 +21,8 @@ with this program; if not, write to the Free Software Foundation, Inc., pmoveParams_t sv_pmp; +bot_client_t bot_clients[MAX_CLIENTS]; + master_t sv_masters[MAX_MASTERS]; // address of group servers LIST_DECL(sv_banlist); @@ -457,6 +459,25 @@ static size_t SV_StatusString(char *status) memcpy(status + total, entry, len); total += len; } + + //rekkie -- Fake Bot Client -- s + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (bot_clients[i].in_use) + { + len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] + + if (len >= sizeof(entry)) + continue; + + if (total + len >= SV_OUTPUTBUF_LENGTH) + break; // can't hold any more + + memcpy(status + total, entry, len); + total += len; + } + } + //rekkie -- Fake Bot Client -- e } status[total] = 0; @@ -600,6 +621,10 @@ static void SVC_GetChallenge(void) svs.challenges[i].challenge = challenge; svs.challenges[i].time = com_eventTime; } + //rekkie -- force max protocol PROTOCOL_VERSION_Q2PRO until PROTOCOL_VERSION_AQTION is compatible again -- s + Netchan_OutOfBand(NS_SERVER, &net_from, "challenge %u p=%i,%i,%i", challenge, PROTOCOL_VERSION_DEFAULT, PROTOCOL_VERSION_R1Q2, PROTOCOL_VERSION_Q2PRO); + return; + //rekkie -- force max protocol PROTOCOL_VERSION_Q2PRO until PROTOCOL_VERSION_AQTION is compatible again -- e // send it back Netchan_OutOfBand(NS_SERVER, &net_from, @@ -1112,6 +1137,79 @@ static void append_extra_userinfo(conn_params_t *params, char *userinfo) params->maxlength, params->qport, params->has_zlib); } +//rekkie -- Fake Bot Client -- s +// Init Fake Bot Client +void SV_BotInit(void) +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + bot_clients[i].in_use = false; + bot_clients[i].name[0] = 0; + bot_clients[i].ping = 0; + bot_clients[i].score = 0; + } +} +// Game DLL updates Server of bot info +void SV_BotUpdateInfo(char* name, int ping, int score) +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (bot_clients[i].in_use) + { + if (strcmp(bot_clients[i].name, name) == 0) + { + bot_clients[i].ping = ping; + bot_clients[i].score = score; + return; + } + } + } +} +// Game DLL requests Server to add fake bot client +void SV_BotConnect(char* name) +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (bot_clients[i].in_use == false) // Found a free slot + { + bot_clients[i].in_use = true; + Q_snprintf(bot_clients[i].name, sizeof(bot_clients[i].name), "%s", name); + bot_clients[i].ping = 0; + bot_clients[i].score = 0; + Com_Printf("%s Server added %s as a fake client\n", __func__, bot_clients[i].name); + break; + } + } +} +// Game DLL requests Server to remove fake bot client +void SV_BotDisconnect(char* name) +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (bot_clients[i].in_use && strcmp(bot_clients[i].name, name) == 0) + { + bot_clients[i].in_use = false; + bot_clients[i].name[0] = 0; + bot_clients[i].ping = 0; + bot_clients[i].score = 0; + Com_Printf("%s Server removed %s as a fake client\n", __func__, name); + break; + } + } +} +// Game DLL requests Server to clear bot clients (init / map change) +void SV_BotClearClients(void) +{ + for (int i = 0; i < MAX_CLIENTS; i++) + { + bot_clients[i].in_use = false; + bot_clients[i].name[0] = 0; + bot_clients[i].ping = 0; + bot_clients[i].score = 0; + } +} +//rekkie -- Fake Bot Client -- e + static void SVC_DirectConnect(void) { char userinfo[MAX_INFO_STRING * 2]; diff --git a/src/server/server.h b/src/server/server.h index 3efcb0467..12d8a2700 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -886,3 +886,17 @@ trace_t q_gameabi SV_Trace(const vec3_t start, const vec3_t mins, trace_t q_gameabi SV_Clip(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, edict_t *clip, int contentmask); + +bsp_t* SV_BSP(void); +void SV_BotInit(void); +void SV_BotUpdateInfo(char* name, int ping, int score); +void SV_BotConnect(char* name); +void SV_BotDisconnect(char* name); +void SV_BotClearClients(void); +typedef struct bot_client_s { + qboolean in_use; + char name[16]; + int ping; + short score; +} bot_client_t; +//rekkie -- Fake Bot Client -- e \ No newline at end of file diff --git a/src/server/world.c b/src/server/world.c index 7f3edd576..c75c8a826 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -594,6 +594,53 @@ trace_t q_gameabi SV_Trace(const vec3_t start, const vec3_t mins, return trace; } +//rekkie -- BSP -- s +//#ifdef ACTION_DLL +//============= +// SV_BSP +//============= +bsp_t* SV_BSP(void) +{ + bsp_t* bsp = sv.cm.cache; + if (!bsp) { + Com_Error(ERR_DROP, "%s: no map loaded", __func__); + return NULL; + } + + return bsp; +} +//#endif +//rekkie -- BSP -- e + +//rekkie -- surface data -- s +// Share surface data with game library +nav_t* CS_NAV(void) +{ + nav_t* nav = sv.cm.nav; + if (!nav) { + //Com_Error(ERR_DROP, "%s: no nav data loaded", __func__); + return NULL; + } + + return nav; +} + +//rekkie -- debug drawing -- s +#if DEBUG_DRAWING +debug_draw_t* CS_DebugDraw(void) +{ + debug_draw_t* draw = sv.cm.draw; + if (!sv.cm.draw) + { + //Com_Error(ERR_DROP, "%s: no draw func loaded", __func__); + return NULL; + } + return draw; +} +#endif +//rekkie -- debug drawing -- e +//rekkie -- surface data -- e + /* ================== SV_Clip From 353f2cd5977261c0f17da8edfcb0479df815d469 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 May 2024 17:36:59 -0400 Subject: [PATCH 185/974] Compiles, no crash yet, bots are dumb though --- src/action/a_cmds.c | 7 + src/action/a_team.c | 35 +- src/action/g_combat.c | 2 + src/action/g_local.h | 14 +- src/action/g_main.c | 34 + src/action/g_save.c | 3 + src/action/g_spawn.c | 339 +++++-- src/action/g_utils.c | 2 + src/action/g_weapon.c | 45 + src/action/p_client.c | 2258 ++++++++++++++++++++++++++++++++++++++++- 10 files changed, 2625 insertions(+), 114 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 007999266..b54d0679b 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -287,6 +287,13 @@ void LaserSightThink(edict_t * self) void Cmd_New_Reload_f(edict_t * ent) { + //rekkie -- walknodes -- s + if (ent->is_bot == false && dedicated->value == 0 && players[0] == ent && ent->bot.walknode.enabled) + { + BOTLIB_ChangeNodeFunction(ent); // Change the node function using reload key + } + //rekkie -- walknodes -- e + //FB 6/1/99 - refuse to reload during LCA if (lights_camera_action) return; diff --git a/src/action/a_team.c b/src/action/a_team.c index 982748c50..6693be76d 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2257,13 +2257,31 @@ static void SpawnPlayers(void) SelectRandomWeaponAndItem(ent, weapmenu); } + //rekkie -- s #ifndef NO_BOTS - if( !Q_stricmp( ent->classname, "bot" ) ) - ACESP_PutClientInServer( ent, true, ent->client->resp.team ); - else + if (ent->bot.bot_type == BOT_TYPE_BOTLIB) + { + BOTLIB_PutClientInServer(ent, true, ent->client->resp.team); + } #endif - PutClientInServer(ent); + else + //rekkie -- e + { + PutClientInServer(ent); + } AddToTransparentList(ent); + + //rekkie -- DEV_1 -- s +#ifndef NO_BOTS + if (ent->is_bot) + { + ent->just_spawned = true; // Flag that we've just spawned + ent->just_spawned_go = false; // Flag that the bot shouldn't move until ready + BOTLIB_PickLongRangeGoal(ent); // Lets pick a long range goal + //ACEAI_PickLongRangeGoal(ent); // Lets pick a long range goal + } +#endif + //rekkie -- DEV_1 -- e } if(matchmode->value && limchasecam->value) @@ -3767,8 +3785,13 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) else if( field == 'P' ) { #ifndef NO_BOTS - if( cl_ent->is_bot ) - strcpy( buf, " BOT" ); + //rekkie -- Fake Bot Client -- s + if (cl_ent->is_bot) + Q_snprintf(buf, sizeof(buf), "%4i", min(9999, cl_ent->bot.bot_ping)); + //if (0) + //rekkie -- Fake Bot Client -- e + //if( cl_ent->is_bot ) + // strcpy( buf, " BOT" ); else #endif Q_snprintf( buf, sizeof(buf), "%4i", min( 9999, cl->ping ) ); diff --git a/src/action/g_combat.c b/src/action/g_combat.c index e9ef01210..3c2ba4b59 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -461,6 +461,8 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve vec_t dist; float targ_maxs2; //FB 6/1/99 + if ((targ->flags & FL_GODMODE) || targ->solid == SOLID_NOT) return; //rekkie -- specators don't take damage + // do this before teamplay check if (!targ->takedamage) return; diff --git a/src/action/g_local.h b/src/action/g_local.h index c5efd351d..1a88ab788 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -293,7 +293,7 @@ #include "p_antilag.h" #ifndef NO_BOTS -#include "acesrc/botnav.h" +//#include "acesrc/botnav.h" #include "botlib/botlib.h" #endif @@ -1171,6 +1171,9 @@ extern cvar_t *radio_repeat; extern cvar_t *radio_repeat_time; extern cvar_t *hc_single; // Enable or disable the single shot handcannon +extern cvar_t *hc_boost; //rekkie -- allow HC to 'boost' the player +extern cvar_t *hc_boost_percent; //rekkie -- allow HC to 'boost' the player +extern cvar_t *hc_silencer; //rekkie -- allow HC to 'boost' the player extern cvar_t *wp_flags; // Weapon flags (bans) extern cvar_t *itm_flags; // Item flags (bans) extern cvar_t *use_classic; // Use_classic resets weapon balance to 1.52 @@ -1505,6 +1508,13 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, #define DEFAULT_SHOTGUN_COUNT 12 #define DEFAULT_SSHOTGUN_COUNT 20 +//rekkie -- DEV_1 -- s +// +// g_func.c +// +void door_use(edict_t* self, edict_t* other, edict_t* activator); +//rekkie -- DEV_1 -- e + // // g_misc.c // @@ -1626,7 +1636,7 @@ void EspionageChaseCam(edict_t *self, edict_t *attacker); // void ChangePlayerSpawns(void); void ED_CallSpawn( edict_t *ent ); -char* ED_NewString(char* string); +char* ED_NewString(const char* string); void G_UpdateSpectatorStatusbar( void ); void G_UpdatePlayerStatusbar( edict_t *ent, int force ); int Gamemodeflag(void); diff --git a/src/action/g_main.c b/src/action/g_main.c index c81fbd740..a1c695ddd 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -397,6 +397,9 @@ cvar_t *video_force_restart; cvar_t *video_check_lockpvs; cvar_t *video_check_glclear; cvar_t *hc_single; +cvar_t *hc_boost; //rekkie -- allow HC to 'boost' the player +cvar_t *hc_boost_percent; //rekkie -- allow HC to 'boost' the player +cvar_t *hc_silencer; cvar_t *wp_flags; // Weapon Banning cvar_t *itm_flags; // Item Banning cvar_t *matchmode; @@ -595,6 +598,10 @@ void ShutdownGame (void) IRC_exit (); #ifndef NO_BOTS ACECM_Store(); + BOTLIB_FreeNodes(); //rekkie -- DEV_1 -- Hard map change. Free any existing node memory used + BOTLIB_FreeAreaNodes(); //rekkie -- DEV_1 -- Soft map change. Free all area node memory used + DC_Free_Spawnpoints(); //rekkie -- DEV_1 -- Hard map change. Free any existing spawnpoint memory used + BOTLIB_SaveBotsFromPreviousMap(); //rekkie -- Hard map change. #endif //PG BUND vExitGame (); @@ -1021,6 +1028,31 @@ void CheckDMRules (void) if (level.intermission_framenum) return; +//rekkie -- DEV_1 -- s +#ifndef NO_BOTS + if (FRAMESYNC) + { + BOTLIB_CheckBotRules(); + + // Reduce noise timers + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (botlib_noises.self_time[i] > 0) + botlib_noises.self_time[i]--; + + if (botlib_noises.weapon_time[i] > 0) + { + botlib_noises.weapon_time[i]--; + //Com_Printf("%s %s counting PNOISE_WEAPON %d\n", __func__, botlib_noises.owner[i]->client->pers.netname, botlib_noises.weapon_time[i]); + } + + if (botlib_noises.impact_time[i] > 0) + botlib_noises.impact_time[i]--; + } + } +#endif + //rekkie -- DEV_1 -- e + //FIREBLADE if (teamplay->value) { @@ -1104,6 +1136,8 @@ void ExitLevel (void) edict_t *ent; char command[256]; + BOTLIB_SaveBotsFromPreviousMap(); //rekkie -- Soft map change (timelimit, fraglimit) + if(softquit) { gi.bprintf(PRINT_HIGH, "Soft quit was requested by admin. The server will now exit.\n"); /* leave clients reconnecting just in case if the server will come back */ diff --git a/src/action/g_save.c b/src/action/g_save.c index 700a1b446..83724e605 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -452,6 +452,9 @@ void InitGame( void ) video_check_glclear = gi.cvar( "video_check_glclear", "0", 0 ); video_checktime = gi.cvar( "video_checktime", "15", 0 ); hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH ); //default ON + hc_boost = gi.cvar("hc_boost", "1", CVAR_LATCH); //rekkie -- allow HC to 'boost' the player + hc_boost_percent = gi.cvar("hc_boost_percent", "100", 0); //rekkie -- allow HC to 'boost' the player + hc_silencer = gi.cvar("hc_silencer", "0", 0); //rekkie -- allow HC to 'boost' the player wp_flags = gi.cvar( "wp_flags", WPF_DEFAULT_STR, 0 ); itm_flags = gi.cvar( "itm_flags", ITF_DEFAULT_STR, 0 ); matchmode = gi.cvar( "matchmode", "0", CVAR_SERVERINFO | CVAR_LATCH ); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index ce005ce9a..f3bde728e 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -155,12 +155,22 @@ #include "g_local.h" +typedef struct { + char* name; + void (*spawn)(edict_t* ent); +} spawn_func_t; + +typedef struct { + char* name; + unsigned ofs; + fieldtype_t type; +} spawn_field_t; + typedef struct { const char *name; void (*spawn) (edict_t * ent); -} -spawn_t; +} spawn_t; void SP_item_health (edict_t * self); @@ -256,6 +266,154 @@ char ml_creator[101]; placedata_t locationbase[MAX_LOCATIONS_IN_BASE]; //AQ2:M + +static const spawn_func_t spawn_funcs[] = { + {"item_health", SP_item_health}, + {"item_health_small", SP_item_health_small}, + {"item_health_large", SP_item_health_large}, + {"item_health_mega", SP_item_health_mega}, + + {"info_player_start", SP_info_player_start}, + {"info_player_deathmatch", SP_info_player_deathmatch}, + {"info_player_intermission", SP_info_player_intermission}, + + {"info_player_team1", SP_info_player_team1}, + {"info_player_team2", SP_info_player_team2}, + + {"func_plat", SP_func_plat}, + {"func_button", SP_func_button}, + {"func_door", SP_func_door}, + {"func_door_secret", SP_func_door_secret}, + {"func_door_rotating", SP_func_door_rotating}, + {"func_rotating", SP_func_rotating}, + {"func_train", SP_func_train}, + {"func_water", SP_func_water}, + {"func_conveyor", SP_func_conveyor}, + {"func_areaportal", SP_func_areaportal}, + {"func_clock", SP_func_clock}, + {"func_wall", SP_func_wall}, + {"func_object", SP_func_object}, + {"func_timer", SP_func_timer}, + {"func_explosive", SP_func_explosive}, + {"func_killbox", SP_func_killbox}, + + {"trigger_always", SP_trigger_always}, + {"trigger_once", SP_trigger_once}, + {"trigger_multiple", SP_trigger_multiple}, + {"trigger_relay", SP_trigger_relay}, + {"trigger_push", SP_trigger_push}, + {"trigger_hurt", SP_trigger_hurt}, + {"trigger_key", SP_trigger_key}, + {"trigger_counter", SP_trigger_counter}, + {"trigger_elevator", SP_trigger_elevator}, + {"trigger_gravity", SP_trigger_gravity}, + {"trigger_monsterjump", SP_trigger_monsterjump}, + + {"target_temp_entity", SP_target_temp_entity}, + {"target_speaker", SP_target_speaker}, + {"target_explosion", SP_target_explosion}, + {"target_changelevel", SP_target_changelevel}, + // {"target_secret", SP_target_secret}, + // {"target_goal", SP_target_goal}, + {"target_splash", SP_target_splash}, + {"target_spawner", SP_target_spawner}, + {"target_blaster", SP_target_blaster}, + {"target_crosslevel_trigger", SP_target_crosslevel_trigger}, + {"target_crosslevel_target", SP_target_crosslevel_target}, + {"target_laser", SP_target_laser}, + // {"target_help", SP_target_help}, + // monster {"target_actor", SP_target_actor}, + // {"target_lightramp", SP_target_lightramp}, + {"target_earthquake", SP_target_earthquake}, + {"target_character", SP_target_character}, + {"target_string", SP_target_string}, + + {"worldspawn", SP_worldspawn}, + {"viewthing", SP_viewthing}, + + // {"light", SP_light}, + {"light_mine1", SP_light_mine1}, + {"light_mine2", SP_light_mine2}, + {"info_null", SP_info_null}, + {"func_group", SP_info_null}, + {"info_notnull", SP_info_notnull}, + {"path_corner", SP_path_corner}, + {"misc_banner", SP_misc_banner}, + {"misc_ctf_banner", SP_misc_ctf_banner}, + {"misc_ctf_small_banner", SP_misc_ctf_small_banner}, + {"misc_satellite_dish", SP_misc_satellite_dish}, + {"misc_viper", SP_misc_viper}, + {"misc_viper_bomb", SP_misc_viper_bomb}, + {"misc_bigviper", SP_misc_bigviper}, + {"misc_strogg_ship", SP_misc_strogg_ship}, + {"misc_teleporter", SP_misc_teleporter}, + {"misc_teleporter_dest", SP_misc_teleporter_dest}, + {"trigger_teleport", SP_trigger_teleport}, + {"info_teleport_destination", SP_info_teleport_destination}, + {"misc_blackhole", SP_misc_blackhole}, + + {NULL, NULL} +}; + +static const spawn_field_t spawn_fields[] = { + {"classname", FOFS(classname), F_LSTRING}, + {"model", FOFS(model), F_LSTRING}, + {"spawnflags", FOFS(spawnflags), F_INT}, + {"speed", FOFS(speed), F_FLOAT}, + {"accel", FOFS(accel), F_FLOAT}, + {"decel", FOFS(decel), F_FLOAT}, + {"target", FOFS(target), F_LSTRING}, + {"targetname", FOFS(targetname), F_LSTRING}, + {"pathtarget", FOFS(pathtarget), F_LSTRING}, + {"deathtarget", FOFS(deathtarget), F_LSTRING}, + {"killtarget", FOFS(killtarget), F_LSTRING}, + {"combattarget", FOFS(combattarget), F_LSTRING}, + {"message", FOFS(message), F_LSTRING}, + {"team", FOFS(team), F_LSTRING}, + {"wait", FOFS(wait), F_FLOAT}, + {"delay", FOFS(delay), F_FLOAT}, + {"random", FOFS(random), F_FLOAT}, + {"move_origin", FOFS(move_origin), F_VECTOR}, + {"move_angles", FOFS(move_angles), F_VECTOR}, + {"style", FOFS(style), F_INT}, + {"count", FOFS(count), F_INT}, + {"health", FOFS(health), F_INT}, + {"sounds", FOFS(sounds), F_INT}, + {"light", 0, F_IGNORE}, + {"dmg", FOFS(dmg), F_INT}, + {"mass", FOFS(mass), F_INT}, + {"volume", FOFS(volume), F_FLOAT}, + {"attenuation", FOFS(attenuation), F_FLOAT}, + {"map", FOFS(map), F_LSTRING}, + {"origin", FOFS(s.origin), F_VECTOR}, + {"angles", FOFS(s.angles), F_VECTOR}, + {"angle", FOFS(s.angles), F_ANGLEHACK}, + + {NULL} +}; + +// temp spawn vars -- only valid when the spawn function is called +static const spawn_field_t temp_fields[] = { + {"lip", STOFS(lip), F_INT}, + {"distance", STOFS(distance), F_INT}, + {"height", STOFS(height), F_INT}, + {"noise", STOFS(noise), F_LSTRING}, + {"pausetime", STOFS(pausetime), F_FLOAT}, + {"item", STOFS(item), F_LSTRING}, + + {"gravity", STOFS(gravity), F_LSTRING}, + {"sky", STOFS(sky), F_LSTRING}, + {"skyrotate", STOFS(skyrotate), F_FLOAT}, + {"skyaxis", STOFS(skyaxis), F_VECTOR}, + {"minyaw", STOFS(minyaw), F_FLOAT}, + {"maxyaw", STOFS(maxyaw), F_FLOAT}, + {"minpitch", STOFS(minpitch), F_FLOAT}, + {"maxpitch", STOFS(maxpitch), F_FLOAT}, + {"nextmap", STOFS(nextmap), F_LSTRING}, + + {NULL} +}; + static const spawn_t spawns[] = { {"item_health", SP_item_health}, {"item_health_small", SP_item_health_small}, @@ -340,7 +498,8 @@ Finds the spawn function for the entity and calls it */ void ED_CallSpawn (edict_t * ent) { - const spawn_t *s; + const spawn_func_t* s; + //const spawn_t *s; gitem_t *item; int i; @@ -396,15 +555,24 @@ void ED_CallSpawn (edict_t * ent) } } + // check normal spawn functions - for (s = spawns; s->name; s++) - { - if (!strcmp (s->name, ent->classname)) - { // found it - s->spawn (ent); + for (s = spawn_funcs; s->name; s++) { + if (!strcmp(s->name, ent->classname)) { + // found it + s->spawn(ent); return; } } + // check normal spawn functions + // for (s = spawns; s->name; s++) + // { + // if (!strcmp (s->name, ent->classname)) + // { // found it + // s->spawn (ent); + // return; + // } + // } /*if(strcmp (ent->classname, "freed") != 0) { gi.dprintf ("%s doesn't have a spawn function\n", ent->classname); @@ -467,21 +635,19 @@ void CheckItem (edict_t * ent) ED_NewString ============= */ -char *ED_NewString (char *string) +char* ED_NewString(const char* string) { - char *newb, *new_p; - int i, l; + char* newb, * new_p; + int i, l; - l = strlen (string) + 1; + l = strlen(string) + 1; - newb = gi.TagMalloc (l, TAG_LEVEL); + newb = gi.TagMalloc(l, TAG_LEVEL); new_p = newb; - for (i = 0; i < l; i++) - { - if (string[i] == '\\' && i < l - 1) - { + for (i = 0; i < l; i++) { + if (string[i] == '\\' && i < l - 1) { i++; if (string[i] == 'n') *new_p++ = '\n'; @@ -495,9 +661,6 @@ char *ED_NewString (char *string) return newb; } - - - /* =============== ED_ParseField @@ -506,58 +669,49 @@ Takes a key/value pair and sets the binary values in an edict =============== */ -void ED_ParseField (char *key, char *value, edict_t * ent) +static bool ED_ParseField(const spawn_field_t* fields, const char* key, const char* value, byte* b) { - field_t *f; - byte *b; - float v; - vec3_t vec; - - for (f = fields; f->name; f++) - { - // FFL_NOSPAWN check in the following added in 3.20. Adding here. -FB - if (!(f->flags & FFL_NOSPAWN) && !Q_stricmp (f->name, key)) - { // found it - if (f->flags & FFL_SPAWNTEMP) - b = (byte *)&st; - else - b = (byte *)ent; - - switch (f->type) - { + const spawn_field_t* f; + float v; + vec3_t vec; + + for (f = fields; f->name; f++) { + if (!Q_stricmp(f->name, key)) { + // found it + switch (f->type) { case F_LSTRING: - *(char **) (b + f->ofs) = ED_NewString (value); + *(char**)(b + f->ofs) = ED_NewString(value); break; case F_VECTOR: - if (sscanf(value, "%f %f %f", &vec[0], &vec[1], &vec[2]) != 3) { - gi.dprintf("ED_ParseField: couldn't parse '%s'\n", key); - VectorClear(vec); - } - ((float *) (b + f->ofs))[0] = vec[0]; - ((float *) (b + f->ofs))[1] = vec[1]; - ((float *) (b + f->ofs))[2] = vec[2]; - break; + if (sscanf(value, "%f %f %f", &vec[0], &vec[1], &vec[2]) != 3) { + gi.dprintf("%s: couldn't parse '%s'\n", __func__, key); + VectorClear(vec); + } + ((float*)(b + f->ofs))[0] = vec[0]; + ((float*)(b + f->ofs))[1] = vec[1]; + ((float*)(b + f->ofs))[2] = vec[2]; + break; case F_INT: - *(int *) (b + f->ofs) = atoi (value); + *(int*)(b + f->ofs) = atoi(value); break; case F_FLOAT: - *(float *) (b + f->ofs) = atof (value); + *(float*)(b + f->ofs) = atof(value); break; case F_ANGLEHACK: - v = atof (value); - ((float *) (b + f->ofs))[0] = 0; - ((float *) (b + f->ofs))[1] = v; - ((float *) (b + f->ofs))[2] = 0; + v = atof(value); + ((float*)(b + f->ofs))[0] = 0; + ((float*)(b + f->ofs))[1] = v; + ((float*)(b + f->ofs))[2] = 0; break; case F_IGNORE: break; default: break; } - return; + return true; } } - gi.dprintf("ED_ParseField: %s is not a field\n", key); + return false; } /* @@ -568,50 +722,47 @@ Parses an edict out of the given string, returning the new position ed should be a properly initialized empty edict. ==================== */ -const char * -ED_ParseEdict (const char *data, edict_t * ent) +void ED_ParseEdict(const char** data, edict_t* ent) { - qboolean init; - char keyname[256]; - char *com_token; - - init = false; - memset (&st, 0, sizeof (st)); + bool init; + char* key, * value; -// go through all the dictionary pairs - while (1) - { - // parse key - com_token = COM_Parse (&data); - if (com_token[0] == '}') - break; - if (!data) - gi.error ("ED_ParseEntity: EOF without closing brace"); + init = false; + memset(&st, 0, sizeof(st)); - Q_strncpyz(keyname, com_token, sizeof(keyname)); - - // parse value - com_token = COM_Parse (&data); - if (!data) - gi.error ("ED_ParseEntity: EOF without closing brace"); + // go through all the dictionary pairs + while (1) { + // parse key + key = COM_Parse(data); + if (key[0] == '}') + break; + if (!*data) + gi.error("%s: EOF without closing brace", __func__); - if (com_token[0] == '}') - gi.error ("ED_ParseEntity: closing brace without data"); + // parse value + value = COM_Parse(data); + if (!*data) + gi.error("%s: EOF without closing brace", __func__); - init = true; + if (value[0] == '}') + gi.error("%s: closing brace without data", __func__); - // keynames with a leading underscore are used for utility comments, - // and are immediately discarded by quake - if (keyname[0] == '_') - continue; + init = true; - ED_ParseField (keyname, com_token, ent); - } + // keynames with a leading underscore are used for utility comments, + // and are immediately discarded by quake + if (key[0] == '_') + continue; - if (!init) - memset (ent, 0, sizeof (*ent)); + if (!ED_ParseField(spawn_fields, key, value, (byte*)ent)) { + if (!ED_ParseField(temp_fields, key, value, (byte*)&st)) { + gi.dprintf("%s: %s is not a field\n", __func__, key); + } + } + } - return data; + if (!init) + memset(ent, 0, sizeof(*ent)); } @@ -1303,7 +1454,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn else ent = G_Spawn(); - entities = ED_ParseEdict(entities, ent); + ED_ParseEdict(&entities, ent); // yet another map hack if (!Q_stricmp (level.mapname, "command") @@ -1357,6 +1508,12 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn // TNG:Freud - Ghosts num_ghost_players = 0; + if (ctf->value) + { + bot_ctf_status.flag1_home_node = INVALID; + bot_ctf_status.flag2_home_node = INVALID; + } + if (!(gameSettings & GS_WEAPONCHOOSE) && !jump->value) { //zucc for special items diff --git a/src/action/g_utils.c b/src/action/g_utils.c index c82ded617..4c6fd7582 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -626,6 +626,8 @@ qboolean KillBox (edict_t * ent) if (!tr.ent) break; + if (tr.ent && tr.ent == ent) break; //rekkie -- prevent bots from killing themselves on first spawn + // nail it T_Damage (tr.ent, ent, ent, vec3_origin, ent->s.origin, vec3_origin, 100000, 0, DAMAGE_NO_PROTECTION, MOD_TELEFRAG); diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index 55b204a7b..e322825c6 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -498,6 +498,51 @@ void ProduceShotgunDamageReport (edict_t *self) total++; } + //rekkie -- allow HC to 'boost' the player -- s + if (hc_boost->value && !total && self->groundentity == NULL && self->client->curr_weap == HC_NUM) + { + //vec3_t start; + vec3_t forward; + //vec3_t offset; + AngleVectors(self->client->v_angle, forward, NULL, NULL); + //VectorSet(offset, 0, 8, self->viewheight); + //P_ProjectSource(self->client, self->s.origin, offset, forward, right, start); + //trace_t tr = gi.trace(self->s.origin, NULL, NULL, start, self, MASK_SOLID); + //if () + VectorInverse(forward); + + // Sanity check + if (hc_boost_percent->value <= 0) + gi.cvar_set("hc_boost_multiplier", va("%d", 100)); + if (hc_boost_percent->value > 1000) + gi.cvar_set("hc_boost_multiplier", va("%d", 1000)); + + float knockback = 300; // Double barrel + if (self->client->pers.hc_mode) knockback = 150; // Single barrel + float mass = max(self->mass, 50); + vec3_t flydir = { 0.f,0.f,0.f }, kvel = { 0.f,0.f,0.f }; + float accel_scale = hc_boost_percent->value; //100.f; // the rocket jump hack... + VectorNormalize2(forward, flydir); + flydir[2] += 0.4f; + VectorScale(flydir, accel_scale * knockback / mass, kvel); + VectorAdd(self->velocity, kvel, self->velocity); + VectorAdd(self->client->oldvelocity, kvel, self->client->oldvelocity); + + //if (self->is_bot == false) Com_Printf("%s %f %f %f\n", __func__, self->client->v_angle[0], self->client->v_angle[1], self->client->v_angle[2]); + + /* + if (hc_boost_multiplier->value < 0) + gi.cvar_set("hc_boost_multiplier", va("%d", 0)); + if (hc_boost_multiplier->value > 10) + gi.cvar_set("hc_boost_multiplier", va("%d", 10)); + if (self->client->pers.hc_mode == 0) // Double barrel + self->velocity[2] += (300 * hc_boost_multiplier->value); + else // self->client->pers.hc_mode == 1 // Single barrel + self->velocity[2] += (150 * hc_boost_multiplier->value); + */ + } + //rekkie -- allow HC to 'boost' the player -- e + if (!total) return; diff --git a/src/action/p_client.c b/src/action/p_client.c index 48196dfb5..66cd4192b 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -496,6 +496,277 @@ void Add_Frag(edict_t * ent, int mod) // end of changing sound dir } +//rekkie -- DEV_1 -- s + // Random bot voice sounds + if (use_voice->value && ent->is_bot && bot_randvoice->value > 0) + { + if (bot_randvoice->value > 100) bot_randvoice->value = 100; + + if (random() < (bot_randvoice->value / 100)) + { + int voicechoice = rand() % 85; + switch (voicechoice) + { + case 0: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/affirm.wav"), 1, ATTN_IDLE, 0); + break; + case 1: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/allbase.wav"), 1, ATTN_IDLE, 0); + break; + case 2: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/allbase2.wav"), 1, ATTN_IDLE, 0); + break; + case 3: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/arriba.wav"), 1, ATTN_IDLE, 0); + break; + case 4: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/audio1.wav"), 1, ATTN_IDLE, 0); + break; + case 5: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/badman.wav"), 1, ATTN_IDLE, 0); + break; + case 6: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/barf.wav"), 1, ATTN_IDLE, 0); + break; + case 7: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/becool.wav"), 1, ATTN_IDLE, 0); + break; + case 8: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/boing.wav"), 1, ATTN_IDLE, 0); + break; + case 9: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/bravo.wav"), 1, ATTN_IDLE, 0); + break; + case 10: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/cartmandontshoot.wav"), 1, ATTN_IDLE, 0); + break; + case 11: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/cdtrust.wav"), 1, ATTN_IDLE, 0); + break; + case 12: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/champion.wav"), 1, ATTN_IDLE, 0); + break; + case 13: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/chicken.wav"), 1, ATTN_IDLE, 0); + break; + case 14: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/childic.wav"), 1, ATTN_IDLE, 0); + break; + case 15: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/clint.wav"), 1, ATTN_IDLE, 0); + break; + case 16: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/comeback.wav"), 1, ATTN_IDLE, 0); + break; + case 17: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/doit.wav"), 1, ATTN_IDLE, 0); + break; + case 18: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/dontshoot.wav"), 1, ATTN_IDLE, 0); + break; + case 19: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/dum.wav"), 1, ATTN_IDLE, 0); + break; + case 20: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fap.wav"), 1, ATTN_IDLE, 0); + break; + case 21: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart1.wav"), 1, ATTN_IDLE, 0); + break; + case 22: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart2.wav"), 1, ATTN_IDLE, 0); + break; + case 23: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart3.wav"), 1, ATTN_IDLE, 0); + break; + case 24: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart4.wav"), 1, ATTN_IDLE, 0); + break; + case 25: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart5.wav"), 1, ATTN_IDLE, 0); + break; + case 26: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart6.wav"), 1, ATTN_IDLE, 0); + break; + case 27: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fart7.wav"), 1, ATTN_IDLE, 0); + break; + case 28: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fighting.wav"), 1, ATTN_IDLE, 0); + break; + case 29: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/filhands.wav"), 1, ATTN_IDLE, 0); + break; + case 30: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/fusk.wav"), 1, ATTN_IDLE, 0); + break; + case 31: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/getout.wav"), 1, ATTN_IDLE, 0); + break; + case 32: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/grimley.wav"), 1, ATTN_IDLE, 0); + break; + case 33: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/happen.wav"), 1, ATTN_IDLE, 0); + break; + case 34: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/headshot.wav"), 1, ATTN_IDLE, 0); + break; + case 35: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/iknow.wav"), 1, ATTN_IDLE, 0); + break; + case 36: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/isee.wav"), 1, ATTN_IDLE, 0); + break; + case 37: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/jabba.wav"), 1, ATTN_IDLE, 0); + break; + case 38: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/killme.wav"), 1, ATTN_IDLE, 0); + break; + case 39: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/kungfuft.wav"), 1, ATTN_IDLE, 0); + break; + case 40: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/l1.wav"), 1, ATTN_IDLE, 0); + break; + case 41: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/l2.wav"), 1, ATTN_IDLE, 0); + break; + case 42: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/laugh.wav"), 1, ATTN_IDLE, 0); + break; + case 43: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/lead.wav"), 1, ATTN_IDLE, 0); + break; + case 44: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/letsgo.wav"), 1, ATTN_IDLE, 0); + break; + case 45: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/letsrock.wav"), 1, ATTN_IDLE, 0); + break; + case 46: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/lol.wav"), 1, ATTN_IDLE, 0); + break; + case 47: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/lookwhat.wav"), 1, ATTN_IDLE, 0); + break; + case 48: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/mustbe.wav"), 1, ATTN_IDLE, 0); + break; + case 49: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/muu1.wav"), 1, ATTN_IDLE, 0); + break; + case 50: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/myvest.wav"), 1, ATTN_IDLE, 0); + break; + case 51: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/newbie.wav"), 1, ATTN_IDLE, 0); + break; + case 52: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/nice.wav"), 1, ATTN_IDLE, 0); + break; + case 53: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/no.wav"), 1, ATTN_IDLE, 0); + break; + case 54: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/nosub.wav"), 1, ATTN_IDLE, 0); + break; + case 55: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/pointy.wav"), 1, ATTN_IDLE, 0); + break; + case 56: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/pressluck.wav"), 1, ATTN_IDLE, 0); + break; + case 57: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/real.wav"), 1, ATTN_IDLE, 0); + break; + case 58: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/rofl.wav"), 1, ATTN_IDLE, 0); + break; + case 59: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/s1.wav"), 1, ATTN_IDLE, 0); + break; + case 60: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/s2.wav"), 1, ATTN_IDLE, 0); + break; + case 61: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/s3.wav"), 1, ATTN_IDLE, 0); + break; + case 62: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/shootorwhat.wav"), 1, ATTN_IDLE, 0); + break; + case 63: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/speedy.wav"), 1, ATTN_IDLE, 0); + break; + case 64: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/srikeme.wav"), 1, ATTN_IDLE, 0); + break; + case 65: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/suprisemf.wav"), 1, ATTN_IDLE, 0); + break; + case 66: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/terminat.wav"), 1, ATTN_IDLE, 0); + break; + case 67: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/thanku.wav"), 1, ATTN_IDLE, 0); + break; + case 68: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/tweetyhelp.wav"), 1, ATTN_IDLE, 0); + break; + case 69: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/unbel.wav"), 1, ATTN_IDLE, 0); + break; + case 70: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/vista.wav"), 1, ATTN_IDLE, 0); + break; + case 71: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/warchant.wav"), 1, ATTN_IDLE, 0); + break; + case 72: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/watching.wav"), 1, ATTN_IDLE, 0); + break; + case 73: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/whyuss.wav"), 1, ATTN_IDLE, 0); + break; + case 74: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/wrong.wav"), 1, ATTN_IDLE, 0); + break; + case 75: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/wtsamat.wav"), 1, ATTN_IDLE, 0); + break; + case 76: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/yeahbaby.wav"), 1, ATTN_IDLE, 0); + break; + case 77: + gi.sound(ent, CHAN_VOICE, gi.soundindex("user/yuck2.wav"), 1, ATTN_IDLE, 0); + break; + case 78: + gi.sound(ent, CHAN_VOICE, gi.soundindex("wizardext/mahadeva.wav"), 1, ATTN_IDLE, 0); + break; + case 79: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/badidea.wav"), 1, ATTN_IDLE, 0); + break; + case 80: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/bbq01.wav"), 1, ATTN_IDLE, 0); + break; + case 81: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/bbq02.wav"), 1, ATTN_IDLE, 0); + break; + case 82: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/scream.wav"), 1, ATTN_IDLE, 0); + break; + case 83: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/scream2.wav"), 1, ATTN_IDLE, 0); + break; + case 84: + gi.sound(ent, CHAN_VOICE, gi.soundindex("barbq/scream3.wav"), 1, ATTN_IDLE, 0); + break; + default: + break; + } + } + } // Announce kill streak to player if use_killcounts is enabled on server if (use_killcounts->value) { // Report only killstreak during that round @@ -2980,6 +3251,20 @@ void ClientBeginDeathmatch(edict_t * ent) ClientUserinfoChanged(ent, ent->client->pers.userinfo); } + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + ent->client->pers.draw = gi.Draw(); + //if (ent->client->pers.draw) + { + // Default all to off state + //ent->client->pers.draw->arrows_inuse = false; + //ent->client->pers.draw->boxes_inuse = false; + //ent->client->pers.draw->crosses_inuse = false; + //ent->client->pers.draw->strings_inuse = false; + } +#endif + //rekkie -- debug drawing -- e + // clear weapons and items if not auto_equipt if (!auto_equip->value || !(gameSettings & GS_WEAPONCHOOSE)) { ent->client->pers.chosenWeapon = NULL; @@ -3681,6 +3966,14 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) } #endif + #ifdef AQTION_EXTENSION + if (ent->is_bot) + { + //pm.s.pm_aq2_flags |= PMF_AQ2_FRICTION; // Increase friction + } + #endif + //rekkie -- Increase friction for bots -- e + pm.trace = PM_trace; // adds default parms pm.pointcontents = gi.pointcontents; // perform a pmove @@ -3725,6 +4018,13 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) if( ent->groundentity ) ent->groundentity_linkcount = ent->groundentity->linkcount; + //rekkie -- DEV_1 -- s + if (pm.groundentity) + ent->last_touched_ground = 0; // Reset the counter + else + ent->last_touched_ground++; // Increment the counter + //rekkie -- DEV_1 -- e + if (ent->deadflag) { client->ps.viewangles[ROLL] = 40; client->ps.viewangles[PITCH] = -15; @@ -3765,23 +4065,1951 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) } } - client->oldbuttons = client->buttons; - client->buttons = ucmd->buttons; - client->latched_buttons |= client->buttons & ~client->oldbuttons; - // save light level the player is standing on for - // monster sighting AI - ent->light_level = ucmd->lightlevel; + //rekkie -- DEV_1 -- s + // Always track what nodes human players are near, if any + if (ent->is_bot == false) + { + // Track human players + if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) + { + // Check if bot is touching a node that isn't on the path + int nodes_touched; // Number of nodes touched + int nodelist[MAX_NODELIST]; // Nodes touched + nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 32, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + for (i = 0; i < nodes_touched; i++) + { + if (nodelist[i] != INVALID && nodes[nodelist[i]].inuse) + ent->bot.current_node = nodelist[i]; // Update which node the player last touched + } - // fire weapon from final position if needed - if (client->latched_buttons & BUTTON_ATTACK) { - //TempFile - //We're gonna fire in this frame? Then abort any punching. - client->punch_framenum = level.framenum; - client->punch_desired = false; + //ent->bot.current_node = ACEND_FindClosestReachableNode(ent, NODE_DENSITY, NODE_ALL); // Update which node the player is closest to + } + } - if (ent->solid == SOLID_NOT && ent->deadflag != DEAD_DEAD && !in_warmup) { - client->latched_buttons = 0; - NextChaseMode( ent ); + // Test if walking toward middle of map + if (0 && ent->is_bot == false) + { + //if (BOTLIB_MovingToward(ent, tv(0, 0, 0), 0.3)) + { + //Com_Printf("%s %s moving toward [%d]\n", __func__, ent->client->pers.netname, level.framenum); + } + + if (BOTLIB_CanMove(ent, MOVE_BACK)) + Com_Printf("%s %s can move back [%d]\n", __func__, ent->client->pers.netname, level.framenum); + if (BOTLIB_CanMove(ent, MOVE_FORWARD)) + Com_Printf("%s %s can move forward [%d]\n", __func__, ent->client->pers.netname, level.framenum); + if (BOTLIB_CanMove(ent, MOVE_LEFT)) + Com_Printf("%s %s can move left [%d]\n", __func__, ent->client->pers.netname, level.framenum); + if (BOTLIB_CanMove(ent, MOVE_RIGHT)) + Com_Printf("%s %s can move right [%d]\n", __func__, ent->client->pers.netname, level.framenum); + } + + // Test throwing knife 'pitch' required to hit distant targets + if (0 && ent->is_bot == false) + { + vec3_t org = { 0, 0, 0 }; + if (players[1] && players[1]->client) + { + VectorCopy(players[1]->s.origin, org); + //Com_Printf("%s %s\n", __func__, players[1]->client->pers.netname); + } + + float gravity = sv_gravity->value; // Gravity + float distanceXYZ = VectorDistance(ent->s.origin, org); // Distance XY + float distanceXY = VectorDistance(ent->s.origin, org); // Distance XYZ + float height_diff = org[2] - ent->s.origin[2]; // Height difference + float initialSpeed = 1200; + + float timeToMaxHeight = sqrtf((initialSpeed * initialSpeed) / (2 * gravity)); + float totalFlightTime = timeToMaxHeight + sqrtf((distanceXYZ / height_diff) + timeToMaxHeight * timeToMaxHeight); + + // Calculate total flight time + totalFlightTime = sqrtf((2 * distanceXYZ) / sv_gravity->value); + + float horizontalVelocity = distanceXY / totalFlightTime; + float verticalVelocity = ((horizontalVelocity * horizontalVelocity) + (initialSpeed * initialSpeed)) / (2 * height_diff); + + float launchAngleDegrees = atan2f(verticalVelocity, horizontalVelocity) * (180 / M_PI); + launchAngleDegrees += 90; + if (launchAngleDegrees) + { + launchAngleDegrees /= 2; + + if (height_diff > 0) + launchAngleDegrees -= 90; + + } + Com_Printf("%s pitch %f %f launchAngleDegrees %f\n", __func__, ent->s.angles[PITCH], ent->client->ps.viewangles[PITCH], launchAngleDegrees); + + ucmd->angles[PITCH] = launchAngleDegrees; + ent->s.angles[PITCH] = launchAngleDegrees; + ent->client->ps.viewangles[PITCH] = launchAngleDegrees; + } + + if (0 && ent->is_bot == false) + { + int mk23_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, MK23_NUM); + int dual_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, DUAL_NUM); + int mp5_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, MP5_NUM); + int m4_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, M4_NUM); + int m3_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, M3_NUM); + int hc_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, HC_NUM); + int ssg_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, SNIPER_NUM); + int knife_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, KNIFE_NUM); + int gren_ammo = BOTLIB_CheckWeaponLoadedAmmo(ent, GRENADE_NUM); + Com_Printf("%s %s mk23[%d] dual[%d] mp5[%d] m4[%d] m3[%d] hc[%d] ssg[%d] k[%d] g[%d]\n", __func__, ent->client->pers.netname, + mk23_ammo, dual_ammo, mp5_ammo, m4_ammo, m3_ammo, hc_ammo, ssg_ammo, knife_ammo, gren_ammo); + } + + // Test CTF + if (0 && ctf->value && ent->is_bot == false) + { + //BOTLIB_TeamFlagIsHome(ent, ent->client->resp.team); + BOTLIB_IsFlagDropped(ent, TEAM1, 0); + BOTLIB_IsFlagDropped(ent, TEAM1, 512); + BOTLIB_IsFlagDropped(ent, TEAM2, 0); + BOTLIB_IsFlagDropped(ent, TEAM2, 512); + + //BOTLIB_TeamFlagDropped(ent, ent->client->resp.team, 512); + //BOTLIB_EnemyFlagDropped(ent, ent->client->resp.team, 512); + /* + if (ent->client->resp.team == TEAM1 && BOTLIB_TeamFlagDropped(ent, ent->client->resp.team, 512)) + Com_Printf("%s %s our dropped flag is nearby %f\n", __func__, ent->client->pers.netname, BOTLIB_DistanceToFlag(ent, FLAG_T1_NUM)); + else if (ent->client->resp.team == TEAM2 && BOTLIB_TeamFlagDropped(ent, ent->client->resp.team, 512)) + Com_Printf("%s %s our dropped flag is nearby %f\n", __func__, ent->client->pers.netname, BOTLIB_DistanceToFlag(ent, FLAG_T2_NUM)); + + if (ent->client->resp.team == TEAM1 && BOTLIB_EnemyFlagDropped(ent, ent->client->resp.team, 512)) + Com_Printf("%s %s dropped enemy flag is nearby %f\n", __func__, ent->client->pers.netname, BOTLIB_DistanceToFlag(ent, FLAG_T2_NUM)); + else if (ent->client->resp.team == TEAM2 && BOTLIB_EnemyFlagDropped(ent, ent->client->resp.team, 512)) + Com_Printf("%s %s dropped enemy flag is nearby %f\n", __func__, ent->client->pers.netname, BOTLIB_DistanceToFlag(ent, FLAG_T1_NUM)); + */ + + /* + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + Com_Printf("%s %s carrying red flag\n", __func__, bot_ctf_status.player_has_flag1->client->pers.netname); + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + Com_Printf("%s %s carrying blue flag\n", __func__, bot_ctf_status.player_has_flag2->client->pers.netname); + + if (bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) + Com_Printf("%s red flag was dropped\n", __func__); + if (bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) + Com_Printf("%s blue flag was dropped\n", __func__); + */ + } + + // Show the speed of the player, their forward/back, and left/right movement + if (0) + { + edict_t* self = ent; + + // Calculate the players current speed and direction and print the results to the console + vec3_t velocity; + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + float speed = VectorLength(velocity); + + vec3_t angles; + VectorCopy(self->client->v_angle, angles); + angles[0] = 0; + angles[2] = 0; + + vec3_t forward, right; + AngleVectors(angles, forward, right, NULL); + + float forward_back = DotProduct(forward, velocity); + float left_or_right = DotProduct(right, velocity); + + Com_Printf("%s: speed %f forward_back %f left_or_right %f\n", __func__, speed, forward_back, left_or_right); + } + + // Trace forward (left and right of player) to look for blocked paths + if (0) + { + if (ent->is_bot == false) + { + edict_t *self = ent; + + // Send out another trace on the left and right side of the bot parallel to the current direction the bot is facing + + vec3_t groundvec, end; + vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; + float forward_back, left_or_right, up_or_down, beam_width; + + groundvec[PITCH] = 0; + groundvec[YAW] = self->client->v_angle[YAW]; + groundvec[ROLL] = 0; + + const float distance = 32; + + AngleVectors(groundvec, forward, NULL, NULL); // Make a forwards pointing vector out of the bot's view angles + VectorMA(self->s.origin, 60, forward, end); // Make end equal to 60* the forward pointing vector + + beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall + forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward + + // Left (-32) of the feet from the ground + STEPSIZE -> pointing forward parallel to the forward vector + left_or_right = self->mins[YAW] - 16; // - left, + right + up_or_down = self->mins[ROLL] + STEPSIZE; // + up, - down + VectorSet(offset, forward_back, left_or_right, up_or_down); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + + VectorMA(start, (distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + trace_t tr = gi.trace(start, tv(-16,-16,0), tv(16,16,56), down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + } + else + { + Com_Printf("%s: %s left clear\n", __func__, self->client->pers.netname); + } + + // Right (+32) of the feet from the ground + STEPSIZE -> pointing forward parallel to the forward vector + left_or_right = self->maxs[YAW] + 16; // - left, + right + up_or_down = self->mins[ROLL] + STEPSIZE; // + up, - down + VectorSet(offset, forward_back, left_or_right, up_or_down); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + + VectorMA(start, (distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + tr = gi.trace(start, tv(-16, -16, 0), tv(16, 16, 56), down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + } + else + { + Com_Printf("%s: %s right clear\n", __func__, self->client->pers.netname); + } + } + } + + // Test code to see if we're to the right or left of the node + if (0) + { + if (ent->is_bot == false) + { + // Trace test to see if we can see our next node + int next_node = 1146; // urban2 --> 1151, 1146, 636 + int current_node = 1151; // urban2 --> 1113, 1151, 494 + //trace_t tr = gi.trace(ent->s.origin, NULL, NULL, nodes[next_node].origin, ent, MASK_DEADSOLID); + //if (tr.fraction < 1.0) // blocked, try find another path to the same goal + { + //Com_Printf("%s %s is blocked [NODE_MOVE]... %f\n", __func__, ent->client->pers.netname, level.time); + + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[next_node].origin, ent->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + + // Get distance from current node to next node + vec3_t curr_to_next_vec; + VectorSubtract(nodes[next_node].origin, nodes[current_node].origin, curr_to_next_vec); + float curr_to_next_dist = VectorLength(curr_to_next_vec); + + // Calculate the normalized distance the bot has travelled between current and next node + float bot_travelled_dist = 0; + if (bot_to_next_dist < curr_to_next_dist) + bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); + else + bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); + + // Normalize bot_travelled_dist + //bot_travelled_dist = bot_travelled_dist - (int)bot_travelled_dist; + + Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); + + // Get the origin between the current and next node based on the normalized bot_travelled_dist + vec3_t normalized_origin; + //VectorScale(curr_to_next_vec, bot_travelled_dist, normalized_origin); + LerpVector(nodes[current_node].origin, nodes[next_node].origin, bot_travelled_dist, normalized_origin); + + + + // Show a laser between next node and normalized_origin + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[next_node].origin); + gi.WritePosition(normalized_origin); + gi.multicast(nodes[next_node].origin, MULTICAST_PVS); + + + // Calculate the angle between the bot's origin and the normalized origin + vec3_t normalized_origin_vec; + VectorSubtract(normalized_origin, ent->s.origin, normalized_origin_vec); + float bot_to_path_dist = VectorLength(normalized_origin_vec); + //Com_Printf("%s %s [d %f]\n", __func__, ent->client->pers.netname, bot_to_path_dist); + + + //VectorNormalize(normalized_origin); + + float current_yaw = anglemod(ent->s.angles[YAW]); + + vec3_t ideal_angle; + vectoangles(normalized_origin_vec, ideal_angle); + + float ideal_yaw = anglemod(ideal_angle[YAW]);// +180; + + // print current_yaw and ideal_yaw + //Com_Printf("%s %s [c %f] [i %f]\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw); + + // Figure out if current yaw is closer to idea yaw if we turn left + float left_yaw; + float right_yaw; + // c = 301 i = 211 + // c = 213 i = 116 + //const float less_than_90_degrees = 87; // 90 is the optimum angle, but we want to give a little leeway (otherwise the bot's yaw movement will oscillate) + + if (bot_to_path_dist > 16) + { + /* + if (ideal_yaw > current_yaw) + { + right_yaw = ideal_yaw + (360 - current_yaw); + left_yaw = ideal_yaw - current_yaw; + } + else + { + right_yaw = current_yaw - ideal_yaw; + left_yaw = ideal_yaw + (360 - current_yaw); + } + */ + + if (ideal_yaw > current_yaw) + { + right_yaw = ideal_yaw - current_yaw; + left_yaw = 360 - right_yaw; + } + else + { + left_yaw = current_yaw - ideal_yaw; + right_yaw = 360 - left_yaw; + } + + /* + i > c + c 41 + i 55 + right i - c 55 - 41 = 14 + left (360 - c) - i (360 - 41 = 319) - 55 = 264 + + c > i + c 55 + i 41 + right c - i 55 - 41 = 14 + left (360 - c) - i (360 - 55 = 305) - 41 = 264 + */ + + //if (right_yaw < 0) + // right_yaw += 360; + //if (left_yaw < 0) + // left_yaw += 360; + + //if (right_yaw > 360) + // right_yaw -= 360; + //if (left_yaw > 360) + // left_yaw -= 360; + + Com_Printf("%s %s [c %f] [i %f] [ry %f] [ly %f] [dist %f]\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, right_yaw, left_yaw, bot_to_path_dist); + + if (right_yaw < left_yaw) + Com_Printf("%s %s move left\n", __func__, ent->client->pers.netname); + else + Com_Printf("%s %s move right\n", __func__, ent->client->pers.netname); + + //if (left_yaw < right_yaw) + // Com_Printf("%s %s move left %f\n", __func__, ent->client->pers.netname, left_yaw); + //else + // Com_Printf("%s %s move right %f\n", __func__, ent->client->pers.netname, right_yaw); + } + } + } + } + + if (0 && ent->is_bot == false) + { + // Test for gaps/holes between the current and next node + // Each tested is conducted 32 units apart + + vec3_t forward, end, origin, target; + VectorCopy(nodes[1740].origin, origin); + VectorCopy(nodes[1403].origin, target); + float tested_distance = 0; + + // Show a laser between next node and normalized_origin + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(origin); + gi.WritePosition(tv(target[0], target[1], target[2])); + gi.multicast(origin, MULTICAST_PVS); + + // Get direction + VectorSubtract(target, origin, forward); + + // Get XYZ distance + float xyz_distance = VectorLength(forward); + + // Normalize direction vector + VectorNormalize(forward); + + while (tested_distance + NODE_SIZE < xyz_distance) + { + tested_distance += NODE_SIZE; // Move next test forward + + VectorMA(origin, tested_distance, forward, end); // Find the end point origin of a vector of length tested_distance + + //VectorMA(origin, tested_distance, forward, end); // Find the end point origin of a vector of length tested_distance + end[2] -= NODE_Z_HEIGHT; // Move end point to ground + //end[2] -= (STEPSIZE + 1); // Move end point below ground level at the depth of step height + 1 + //end[2] = -4096; + + + + trace_t tr = gi.trace(end, NULL, NULL, tv(end[0], end[1], -4096), g_edicts, MASK_DEADSOLID); + + if (tr.startsolid) + { + //Com_Printf("%s dist %f z-diff startsolid\n", __func__, tested_distance, end[2] - tr.endpos[2]); + continue; + } + + //Com_Printf("%s [tested_distance %f] [o %f %f %f] [t %f %f %f] [e %f %f %f] [e %f %f %f]\n", __func__, tested_distance, origin[0], origin[1], origin[2], target[0], target[1], target[2], end[0], end[1], end[2], tr.endpos[0], tr.endpos[1], tr.endpos[2]); + //Com_Printf("%s dist %f z-diff %f\n", __func__, tested_distance, ((end[2] - (NODE_Z_HEIGHT + STEPSIZE)) - tr.endpos[2])); + + if (end[2] - tr.endpos[2] > (NODE_Z_HEIGHT)) + Com_Printf("%s dist %f z-diff %f -- FOUND GAP\n", __func__, tested_distance, end[2] - tr.endpos[2]); + else + Com_Printf("%s dist %f z-diff %f\n", __func__, tested_distance, end[2] - tr.endpos[2]); + + // Show a laser between next node and normalized_origin + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(end); + gi.WritePosition(tr.endpos); + gi.multicast(origin, MULTICAST_PVS); + + //if (from == 1740 && to == 1403) + { + //Com_Printf("%s tested_distance %f z-diff %f\n", __func__, tested_distance, ((origin[2] - (NODE_Z_HEIGHT + STEPSIZE)) - tr.endpos[2])); + } + + //if (tr.fraction == 1.0 && tr.startsolid == false) // Hit a gap (no floor) + if (tr.endpos[2] < (origin[2] - (NODE_Z_HEIGHT + STEPSIZE))) // Hit a gap (no floor) + { + //Com_Printf("%s gap found at %f origin %f %f %f target %f %f %f end %f %f %f\n", __func__, tested_distance, origin[0], origin[1], origin[2], target[0], target[1], target[2], end[0], end[1], end[2]); + // print gap found at tested_distance, and the difference between endpos[2] and (origin[2] - (NODE_Z_HEIGHT + STEPSIZE)) + //Com_Printf("%s gap found at tested_distance %f z-diff %f\n", __func__, tested_distance, ((origin[2] - (NODE_Z_HEIGHT + STEPSIZE)) - tr.endpos[2])); + + //is_gap = true; + //break; + } + } + Com_Printf("%s \n", __func__); + } + + if (0 && ent->is_bot == false) + { + //trace_t tr = gi.trace(ent->s.origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] - 128), ent, (MASK_PLAYERSOLID | MASK_OPAQUE)); + //Com_Printf("%s %s tr.plane.normal[%f] \n", __func__, ent->client->pers.netname, tr.plane.normal[2]); + + if (ent->bot.highlighted_node > 0) + { + // Get the player's direction based from their velocity + vec3_t walkdir; + VectorSubtract(ent->s.origin, ent->lastPosition, walkdir); + //VectorCopy(ent->s.origin, ent->lastPosition); + VectorNormalize(walkdir); + vec3_t angle, forward, right, start, end, origin, offset; + vectoangles(walkdir, angle); + VectorCopy(ent->s.angles, angle); // Use the player's view angles (not their walk direction) + angle[0] = 0; + AngleVectors(angle, forward, right, NULL); + VectorCopy(ent->s.origin, origin); + origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) + + /* + // Offset origin to the right + //VectorSet(offset, 0, 32, 0); + //G_ProjectSource(origin, offset, forward, right, start); + + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, start); + VectorNormalize(forward); + + //VectorCopy(nodes[ent->bot.highlighted_node].origin, end); + //VectorSubtract(end, right, forward); + //VectorNormalize(forward); + */ + + // Calculate the direction from the player's origin to the node origin + vec3_t node_dir; + VectorSubtract(nodes[ent->bot.highlighted_node].origin, ent->s.origin, node_dir); + VectorNormalize(node_dir); + { + // Node direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(ent->s.origin); + gi.WritePosition(nodes[ent->bot.highlighted_node].origin); + gi.multicast(ent->s.origin, MULTICAST_PHS); + } + + VectorNormalize(forward); + + // Calculate the dot product of the forward dir and the node dir + float dot = DotProduct(forward, node_dir); + //Com_Printf("%s node %d dot %f\n", __func__, ent->bot.highlighted_node, dot); + //if (dot < 0.995) + + if (1) + { + float dot_right = DotProduct(right, node_dir); + VectorNegate(right, right); + float dot_left = DotProduct(right, node_dir); + if (dot_right > dot_left) // + Com_Printf("%s [MOVE RIGHT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + else + Com_Printf("%s [MOVE LEFT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + } + + if (0) + { + // Offset origin to the right + vec3_t s_right; + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, s_right); + VectorSubtract(nodes[ent->bot.highlighted_node].origin, s_right, forward); + VectorNormalize(forward); + float dot_right = DotProduct(forward, node_dir); + { + // Player walk direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(s_right); + gi.WritePosition(nodes[ent->bot.highlighted_node].origin); + gi.multicast(s_right, MULTICAST_PHS); + } + + // Offset origin to the left + vec3_t s_left; + VectorSet(offset, 0, -32, 0); + G_ProjectSource(origin, offset, forward, right, s_left); + VectorSubtract(nodes[ent->bot.highlighted_node].origin, s_left, forward); + VectorNormalize(forward); + float dot_left = DotProduct(forward, node_dir); + { + // Player walk direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(s_left); + gi.WritePosition(nodes[ent->bot.highlighted_node].origin); + gi.multicast(ent->s.origin, MULTICAST_PHS); + } + + + if (dot_right < dot_left) // + Com_Printf("%s [MOVE RIGHT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + else + Com_Printf("%s [MOVE LEFT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + } + + + + + // Show visual + if (0) + { + VectorSet(offset, 0, 32, 0); + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1024; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + // Player walk direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(ent->s.origin, MULTICAST_PHS); + + // Node direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(ent->s.origin); + gi.WritePosition(nodes[ent->bot.highlighted_node].origin); + gi.multicast(ent->s.origin, MULTICAST_PHS); + } + } + } + + // Expand additional nodes from the center of the edge, one heading 90 degrees left, and one heading 90 degrees right. + if (0 && ent->is_bot == false) + { + //ACEND_BSP: [pt1:480.000000,640.000000,0.000000] [pt2:480.000000,752.000000,0.000000] + vec3_t pt1 = { 480.000000, 640.000000, 0.000000 }; + vec3_t pt2 = { 480.000000, 752.000000, 0.000000 }; + vec3_t center; + LerpVector(pt1, pt2, 0.50, center); + + vec3_t end_left, end_right; + + /* + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 4, end_left); + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 270, 4, end_right); + end_left[2] += 1; + end_right[2] += 1; + DAIC_Add_Node(end_left, tv(0, 0, 0), NODE_MOVE); + DAIC_Add_Node(end_right, tv(0, 0, 0), NODE_MOVE); + */ + + //ent->spin++; + //if (ent->spin > 360) + // ent->spin = 0; + + //BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, ent->spin, 512, end_left); // Anti-clockwise + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 3000, end_left); // Anti-clockwise + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 270, 3000, end_right); // Clockwise + + // Show a laser between next node and normalized_origin + /* + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(pt1); + gi.WritePosition(pt2); + gi.multicast(center, MULTICAST_PVS); + */ + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(center); + gi.WritePosition(end_left); + gi.multicast(center, MULTICAST_PVS); + + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(center); + gi.WritePosition(end_right); + gi.multicast(center, MULTICAST_PVS); + + + + /* + // Lift the end points up a bit so they don't get stuck in the ground + end_left[2] += 1; + end_right[2] += 1; + + // Test to see where the left and right meet a wall + // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right + trace_t tr_left_wall = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, end_left, ent, MASK_PLAYERSOLID); + trace_t tr_right_wall = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, end_right, ent, MASK_PLAYERSOLID); + + //tr_left_wall.endpos[1] -= 32; + //tr_right_wall.endpos[1] -= 32; + // + // Find the middle between two points of an edge + vec3_t left_mid, right_mid; + LerpVector(center, tr_left_wall.endpos, 0.50, left_mid); + LerpVector(center, tr_right_wall.endpos, 0.50, right_mid); + + //DAIC_Add_Node(left_mid, tr_left_wall.plane.normal, NODE_MOVE); + //DAIC_Add_Node(left_mid, tr_right_wall.plane.normal, NODE_MOVE); + + // Test to see if left and right are touching the ground + trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - 2), ent, MASK_PLAYERSOLID); + trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - 2), ent, MASK_PLAYERSOLID); + + vec3_t back; + + // If the left is touching the ground, then we can add a node there + if (tr_left_ground.fraction < 1.0) + { + // Back the node away from the wall a bit using the normal of the wall + //vec3_t back; + VectorMA(tr_left_wall.endpos, -NODE_Z_HALF_HEIGHT, tr_left_wall.plane.normal, back); + //DAIC_Add_Node(back, tr_left_ground.plane.normal, NODE_MOVE); + } + // If the right is touching the ground, then we can add a node there + if (tr_right_ground.fraction < 1.0) + { + // Back the node away from the wall a bit using the normal of the wall + //vec3_t back; + VectorMA(tr_right_wall.endpos, -NODE_Z_HALF_HEIGHT, tr_right_wall.plane.normal, back); + //DAIC_Add_Node(back, tr_right_ground.plane.normal, NODE_MOVE); + } + */ + } + + if (0 && ent->is_bot == false) + { + //Com_Printf("%s origin %f %f %f\n", __func__, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + /* + int f = 0, edges = 0; + BOTLIB_UTIL_NEAREST_BSP_FACE(ent->s.origin, &f, &edges); + Com_Printf("%s face %d edges %d\n--------------\n", __func__, f, edges); + for (int e = 0; e < nmesh.face[f].num_edges; e++) + { + Com_Printf("%s v1[%f %f %f] v2[%f %f %f]\n", __func__, nmesh.face[f].edge[e].v[0][0], nmesh.face[f].edge[e].v[0][1], nmesh.face[f].edge[e].v[0][2], nmesh.face[f].edge[e].v[1][0], nmesh.face[f].edge[e].v[1][1], nmesh.face[f].edge[e].v[1][2]); + } + Com_Printf("--------------\n"); + */ + } + + //rekkie -- surface data -- s + /* + if (ent->nav != NULL) + { + vec3_t start = { 0,0,0 }; vec3_t end = { 0,0,128 }; + #define U32_YELLOW MakeColor(255, 255, 0, 255) + //nav->DrawArrow(start, end, U32_YELLOW, 5.0); + //game.framerate; + + // Local client (call the function directly) + if (ent->client->clientNum == 0) + { + ent->client->pers.ip; + if (ent->nav->DrawArrow != NULL) + { + // Assign void pointer nav->DrawArrow to a function pointer + void(*DrawArrow)(int number, vec3_t start, vec3_t end, const uint32_t color, float line_width, int time) = ent->nav->DrawArrow; + DrawArrow(start, end, U32_YELLOW, 50); // Call the function + } + } + else // Networked clients (send a message) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_DRAWARROW); + gi.WriteLong(U32_YELLOW); // Color + gi.WriteByte(5); // Line width + gi.WritePosition(start); + gi.WritePosition(end); + //gi.WritePosition(ent->s.origin); + //gi.WritePosition(tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 128)); + gi.multicast(start, MULTICAST_PVS); + } + } + */ + + if (0 && ent->is_bot == false) + { + // Check if touching ladder + qboolean touching_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(ent->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(ent->s.origin, 64, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(ent->s.origin, lmins, lmaxs, end, ent, MASK_PLAYERSOLID); + + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (touching_ladder) + Com_Printf("%s touching_ladder [%d]\n", __func__, level.framenum); + } + + //if (1 && ent->is_bot == false) + { + //BOTLIB_GetEquipment(ent); + } + + //rekkie -- debug drawing -- s + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + //rekkie -- Walknodes -- s + if (ent->is_bot == false && dedicated->value == 0 && client->chase_mode) + { + if (ent->bot.walknode.enabled) // Disable chase cam if nav_edit is enabled + DisableChaseCam(ent); + + if (client->chase_target) // Disable nav_edit if chase cam is enabled + ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_NONE; // Disable interaction with nodes + + } + if (ent->is_bot == false && dedicated->value == 0 && ent->bot.walknode.enabled) // Always allow mouse control for nodes + { + BOTLIB_MouseControlNodes(ent, ucmd); // Use mouse to control node: link, move, add, delete, etc + } + // If we're allowed to interact with walknodes aka not in 'HIGHLIGHTED_TYPE_NONE' mode and ltk_showpath is off + if (ent->is_bot == false && dedicated->value == 0 && ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_ADD && bot_showpath->value == 0 && ent->bot.walknode.enabled) + { + // Test if we're looking at a ladder + qboolean on_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(ent->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(ent->s.origin, 16, fwd, end); + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_PLAYERSOLID); + + on_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + // Check we're touching a ladder - extended hitbox + qboolean touching_ladder = false; + { + vec3_t lmins = { -20, -20, -32 }; + vec3_t lmaxs = { 20, 20, 32 }; + + trace_t tr; + tr = gi.trace(ent->s.origin, lmins, lmaxs, ent->s.origin, ent, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + + const int walknode_dist = 32; + int nodes_touched = 0; + int node = INVALID; + int node_type = INVALID; + float node_to_node_dist = 99999; + int from, to; + + trace_t tr = gi.trace(ent->s.origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] - 25), ent, MASK_PLAYERSOLID); + float height_diff = ent->s.origin[2] - tr.endpos[2]; // Height difference between player and ground + float distance = VectorDistance(ent->bot.walknode.last_ground_loc, tr.endpos); // Distance between ground touches + float speed = VectorLength(ent->velocity); // Player speed + + //Com_Printf("%s height_diff %f\n", __func__, height_diff); + + //qboolean normal_jump = false; + //qboolean strafe_jump = false; + //if (ent->bot.walknode.last_ground_touch == 0 && (client->ps.pmove.pm_flags & PMF_JUMP_HELD) && ucmd->upmove > 0 && ucmd->sidemove == 0 && speed) + // normal_jump = true; + //if (ent->bot.walknode.last_ground_touch == 0 && (client->ps.pmove.pm_flags & PMF_JUMP_HELD) && ucmd->upmove > 0 && ucmd->sidemove && speed) + // strafe_jump = true; + //if (ent->bot.walknode.last_ground_touch == 0 && (client->ps.pmove.pm_flags & PMF_JUMP_HELD) && ucmd->upmove > 0) + // strafe_jump = true; + //if (normal_jump || strafe_jump) + // Com_Printf("%s normal_jump[%d] strafe_jump[%d]\n", __func__, normal_jump, strafe_jump); + + // Update last ground location + if (ent->groundentity || tr.fraction < 1) + { + ent->bot.walknode.last_ground_touch = 0; + VectorCopy(tr.endpos, ent->bot.walknode.last_ground_loc); + VectorCopy(tr.plane.normal, ent->bot.walknode.last_ground_normal); + } + else + ent->bot.walknode.last_ground_touch++; + + // Nodes currently being touched + int nodelist[MAX_NODELIST]; + nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tr.plane.normal, walknode_dist, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched > 0) + { + if (0) // print node list + { + Com_Printf("[%d] Nodes touched [%d] nodelist [", level.framenum, nodes_touched); + int count = 0; + while (count < nodes_touched) + { + if (count + 1 < nodes_touched) + Com_Printf("%d,", nodelist[count++]); + else + Com_Printf("%d", nodelist[count++]); + } + Com_Printf("]\n"); + } + + // Find the closest node + int closest_node = INVALID; + float closest_dist = 99999; + for (i = 0; i < nodes_touched; i++) + { + float dist = VectorDistance(ent->s.origin, nodes[nodelist[i]].origin); + if (dist <= walknode_dist && dist < closest_dist) + { + closest_dist = dist; + closest_node = nodelist[i]; + } + } + node = closest_node; // Assign the closest touched node + + /* + if (node != INVALID) + { + // If we're on a ladder, and the nearest node isn't a ladder node, + // pull it closer to the ladder and convert it to a ladder node. + if ((on_ladder || touching_ladder) && node != INVALID && nodes[node].type != NODE_LADDER) + { + Com_Printf("%s %s converting [%d] to NODE_LADDER\n", __func__, ent->client->pers.netname, node); + nodes[node].origin[0] = ent->s.origin[0]; // Pull node closer to ladder + nodes[node].origin[1] = ent->s.origin[1]; // Pull node closer to ladder + nodes[node].type = NODE_LADDER; // Convert node to ladder node + // Update all links connected to this node to ladder links + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + for (int l = 0; l < nodes[i].num_links; l++) + { + if (nodes[i].links[l].targetNode == node) + { + nodes[i].links[l].targetNodeType = NODE_LADDER; + Com_Printf("%s %s updating link [%d] to NODE_LADDER\n", __func__, ent->client->pers.netname, i); + } + } + } + } + + if (ent->bot.walknode.touched_node != INVALID && node != ent->bot.walknode.touched_node) + node_to_node_dist = VectorDistance(nodes[node].origin, nodes[ent->bot.walknode.touched_node].origin); + } + */ + } + + + // Assign node type + if (ent->bot.walknode.last_ground_touch == 0) + { + node_type = NODE_MOVE; + + if (on_ladder || touching_ladder) + { + //Com_Printf("%s %s is touching a ladder\n", __func__, ent->client->pers.netname); + node_type = NODE_LADDER; + } + + if (ucmd->upmove < 0) + { + node_type = NODE_CROUCH; + //node_type = INVALID; + } + } + + if (node_type != INVALID) + { + // If no nodes touched in the area, add a new node. Also check mins and maxs to make sure we're not adding a node while in noclip. + if (nodes_touched == 0 && VectorEmpty(ent->mins) == false && VectorEmpty(ent->maxs) == false) + { + // Get the surface normal + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, ent->s.origin, ent, MASK_PLAYERSOLID); + // If the node is on a slope, raise it up depending on the slope normal and the node's mins/maxs hit box + // This is done so the node hitbox is not inside the slope face + { + vec3_t exp_up; + VectorCopy(tr.plane.normal, exp_up); + exp_up[2] = 0; + VectorNormalize(exp_up); + exp_up[2] = 1; + VectorScale(exp_up, 24, exp_up); + VectorAdd(tr.endpos, exp_up, tr.endpos); + } + // Feed the raised up position back into the trace + tr = gi.trace(tr.endpos, ent->mins, ent->maxs, ent->s.origin, ent, MASK_PLAYERSOLID); + if (tr.startsolid == false) // Make sure we're not inside a wall + { + int node_added; + if (node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE) != INVALID) + { + char typename[32] = { '\0' }; // Length of the longest node type name + NodeTypeToString(ent, nodes[node_added].type, typename, sizeof(typename)); + Com_Printf("%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); + } + } + } + + nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tr.plane.normal, walknode_dist+8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + for (i = 0; i < nodes_touched; i++) + { + from = nodelist[i]; + for (int j = 0; j < nodes_touched; j++) + { + if (nodelist[i] == nodelist[j]) // Skip self + continue; + + to = nodelist[j]; + if (from != INVALID && to != INVALID) + { + //BOTLIB_Reachability(from, to); + + float height = (nodes[to].origin[2] - nodes[from].origin[2]); + if (height > 60) // Ignore if too high + continue; + if (height < -60) // Crouch when dropping down + node_type = NODE_CROUCH; + + tr = gi.trace(nodes[from].origin, ent->mins, ent->maxs, nodes[to].origin, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Don't link if we can't see the node + { + if (BOTLIB_TraceBoxNode(from, to) == INVALID) // Don't link if our trace hit other nodes + { + if (BOTLIB_AddNodeLink(from, to, node_type, true)) + { + Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, from, to, node_type); + //ACEND_UpdateNodeReach(from, to); // Update path_table + } + } + } + } + } + } + /* + else if (node_to_node_dist <= walknode_dist+16 && ent->bot.walknode.touched_node != INVALID && ent->bot.walknode.touched_node != node) + { + int from = ent->bot.walknode.touched_node; + int to = node; + int from_type = nodes[from].type; + int to_type = nodes[to].type; + //float dist = VectorDistance(nodes[from].origin, nodes[to].origin); + //if (from_type == to_type)// && dist <= 64) + trace_t tr = gi.trace(nodes[from].origin, NULL, NULL, nodes[to].origin, ent, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction == 1.0) + { + //if (BOTLIB_Reachability(from, to) == NODE_JUMPPAD) + // node_type = NODE_JUMPPAD; + + if (DC_AddNodeLink(from, to, node_type, true)) + { + Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, ent->bot.walknode.touched_node, node, node_type); + //ACEND_UpdateNodeReach(from, to); // Update path_table + } + + //if (BOTLIB_Reachability(to, from) == NODE_JUMPPAD) + // node_type = NODE_JUMPPAD; + + if (DC_AddNodeLink(to, from, node_type, true)) + { + Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, node, ent->bot.walknode.touched_node, node_type); + //ACEND_UpdateNodeReach(to, from); // Update path_table + } + } + } + */ + + // Update last touched node + ent->bot.walknode.touched_node = node; + } + +#if 0 + /* + //else if (tr.fraction < 1 && (speed || ucmd->forwardmove || ucmd->sidemove || ucmd->upmove)) + else if ((tr.fraction < 1 || ent->groundentity))// && (ucmd->forwardmove || ucmd->sidemove || ucmd->upmove)) + { + if (ent->bot.walknode.last_type == NODE_JUMP) + { + node_type = NODE_JUMP; + ent->bot.walknode.last_type = INVALID; + } + else if (ent->bot.walknode.last_type == NODE_JUMPPAD) + { + //Com_Printf("%s NODE_JUMPPAD\n", __func__); + node_type = NODE_JUMPPAD; + ent->bot.walknode.last_type = INVALID; + } + else + { + //if (BOTLIB_VectorCompare(tr.endpos, ent->bot.last_ground_loc, STEPSIZE)) + //if (fabs(tr.endpos[2] - ent->bot.last_ground_loc[2]) <= STEPSIZE || VectorCompare(tr.plane.normal, ent->bot.last_ground_normal) || ent->bot.last_ground_touch <= 1) + //if (ent->groundentity && (ucmd->forwardmove || ucmd->sidemove)) + if (tr.fraction < 1) + node_type = NODE_MOVE; + if (ucmd->upmove < 0) + node_type = NODE_CROUCH; + if (normal_jump) + { + node_type = NODE_JUMP; + ent->bot.walknode.last_type = NODE_JUMP; + } + if (strafe_jump) + { + node_type = NODE_JUMPPAD; + ent->bot.walknode.last_type = NODE_JUMPPAD; + } + } + } + */ + + + + //if (node_type > INVALID) Com_Printf("%s node_type[%d] framenum[%d]\n", __func__, node_type, level.framenum); + + + if (node_type != INVALID) + { + if (nodes_touched == 0) // If no nodes touched in the area, add a new node + { + if (BOTLIB_AddNode(ent->s.origin, tr.plane.normal, node_type)) //BOTLIB_BoxIntersection + { + //Com_Printf("%s %s added node [%d] type [%d]\n", __func__, ent->client->pers.netname, numnodes - 1, node_type); + } + } + + // Get prevous touched node, if the current node and previous node are different + // and they're the same type, and they can see each other, add a link + // Try to link up to existing nodes + //node = BOTLIB_TestForNodeDist(ent->s.origin, tr.plane.normal, 16, ent->absmin, ent->absmax); + { + int nodelist[MAX_NODELIST]; + nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tr.plane.normal, 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST); + if (nodes_touched > 0) + { + // Find the closest node + float max_distance = 64; // Maximum distance to consider + int closest_node = INVALID; + float closest_dist = 99999; + for (int i = 0; i < nodes_touched; i++) + { + float dist = VectorDistance(ent->s.origin, nodes[nodelist[i]].origin); + if (dist <= max_distance && dist < closest_dist) + { + closest_dist = dist; + closest_node = nodelist[i]; + } + } + node = closest_node; // Assign the closest touched node + //node = nodelist[0]; // Assign the touched node + + if (0) // print node link list + { + for (int i = 0; i < nodes_touched; i++) + { + if (nodes[nodelist[i]].links[0].targetNode > INVALID) + { + Com_Printf("Node [%d] is linked to nodes [", nodelist[i]); + for (int l = 0; l < nodes[nodelist[i]].num_links; l++) + { + Com_Printf(" %d ", nodes[nodelist[i]].links[l].targetNode); + } + Com_Printf("]\n"); + } + } + } + + if (0) // print node list + { + Com_Printf("Nodes touched [%d] nodelist [", nodes_touched); + int count = 0; + while (count < nodes_touched) + { + Com_Printf(" %d ", nodelist[count++]); + } + Com_Printf("]\n"); + } + + } + } + + if (ent->bot.walknode.touched_node < 0) // Init node for the first time + ent->bot.walknode.touched_node = node; + + //if (node > INVALID && node != ent->bot.touched_nodes[0]) + // Com_Printf("%s %s touched node [%d] prev [%d]\n", __func__, ent->client->pers.netname, node, ent->bot.touched_nodes[0]); + + if (nodes_touched > 0 && ent->bot.walknode.touched_node > INVALID && node != ent->bot.walknode.touched_node) + { + //if (ent->bot.touched_nodes[0] != INVALID && ent->bot.touched_nodes[1] != INVALID) + { + int from = ent->bot.walknode.touched_node; + int to = node; + int from_type = nodes[from].type; + int to_type = nodes[to].type; + //float dist = VectorDistance(nodes[from].origin, nodes[to].origin); + //if (from_type == to_type)// && dist <= 64) + trace_t tr = gi.trace(nodes[from].origin, NULL, NULL, nodes[to].origin, ent, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction == 1.0) + { + if (BOTLIB_Reachability(from, to) == NODE_JUMPPAD) + node_type = NODE_JUMPPAD; + + if (DC_AddNodeLink(NULL, from, to, tv(0, 0, 0), false, node_type, true)) + { + Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, ent->bot.walknode.touched_node, node, node_type); + //ACEND_UpdateNodeReach(from, to); // Update path_table + } + + //if (BOTLIB_Reachability(to, from) == NODE_JUMPPAD) + // node_type = NODE_JUMPPAD; + + if (DC_AddNodeLink(NULL, to, from, tv(0, 0, 0), false, node_type, true)) + { + Com_Printf("%s %s linking node[%d -> %d] type[%d]\n", __func__, ent->client->pers.netname, node, ent->bot.walknode.touched_node, node_type); + //ACEND_UpdateNodeReach(to, from); // Update path_table + } + + + } + + } + ent->bot.walknode.touched_node = node; + } + + } + #endif + } + //rekkie -- debug drawing -- e +#endif +//rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (1 && ent->is_bot == false && dedicated->value == 0 && numnodes && bot_showpath->value == 0 && ent->bot.walknode.enabled) + { + uint32_t color; + + // Selection square + //uint32_t node_color = 0; + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + if (VectorEmpty(ent->bot.walknode.selection_start) == false && VectorEmpty(ent->bot.walknode.selection_end) == false) + { + // Local client (call the function directly) + if (ent->client && ent->client->pers.draw != NULL && ent->client->clientNum == 0) + { + if (ent->client->pers.draw->DrawSelection != NULL) + { + //Com_Printf("%s %s start [%f %f %f] -- end [%f %f %f]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_start[0], ent->bot.walknode.selection_start[1], ent->bot.walknode.selection_start[2], ent->bot.walknode.selection_end[0], ent->bot.walknode.selection_end[1], ent->bot.walknode.selection_end[2]); + void(*DrawSelection)(vec3_t start, vec3_t end, float min, float max, uint32_t color, float line_width, int time, qboolean occluded) = ent->client->pers.draw->DrawSelection; + color = MakeColor(0, 255, 0, 255); // Green + + // Only draw when enough of the selection box has been sampled + if (ent->bot.walknode.selection_min < 99990 && ent->bot.walknode.selection_max > -99990) + DrawSelection(ent->bot.walknode.selection_start, ent->bot.walknode.selection_end, ent->bot.walknode.selection_min, ent->bot.walknode.selection_max, color, 3.0, 100, false); // Draw selection + } + + if (ent->client->pers.draw->DrawBox != NULL) + { + void(*DrawString)(int number, vec3_t origin, const char* string, const uint32_t color, int time, qboolean occluded) = ent->client->pers.draw->DrawString; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + players[0]->client->pers.draw->strings_inuse = true; // Flag as being used + + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = ent->client->pers.draw->DrawBox; + + int node = 0; + + + + // Show selected nodes + for (int i = 0; i < ent->bot.walknode.selection_node_count; i++) + { + node = ent->bot.walknode.selection_nodes[i]; + + //if (nodes[node].area > 0) + // node_color = nodes[node].area_color; + + // Only change node area if set by command AND it links to another area OR we're making a new area + //if (ent->bot.walknode.selection_area_used && (area_is_linked || area_is_new)) + qboolean can_add_area_node = false; // If node can be added to area array + if (ent->bot.walknode.selection_area_used) + { + // Find a free spot to add node to area array + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (DFS_area_nodes[ent->bot.walknode.selection_area][n] == INVALID) + { + DFS_area_nodes[ent->bot.walknode.selection_area][n] = node; + can_add_area_node = true; // Success! + break; + } + } + } + if (can_add_area_node) + { + nodes[node].area = ent->bot.walknode.selection_area; // Update node's area + int first_area_node = DFS_area_nodes[nodes[node].area][0]; // Get first node in area + + /* + // If one of the nodes we selected is already the area we desire + int selected_node_of_same_area = INVALID; + for (int n = 0; n < ent->bot.walknode.selection_node_count; n++) + { + if (nodes[ent->bot.walknode.selection_nodes[i]].area == ent->bot.walknode.selection_area) + { + selected_node_of_same_area = ent->bot.walknode.selection_nodes[i]; + break; + } + } + */ + + if (ent->bot.walknode.selection_area_color) // No nodes in array so use custom color + { + nodes[node].area_color = ent->bot.walknode.selection_area_color; + + // Update all other area nodes in set + // Find a free spot to add node to area array + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (DFS_area_nodes[ent->bot.walknode.selection_area][n] == INVALID) + break; + nodes[DFS_area_nodes[ent->bot.walknode.selection_area][n]].area_color = ent->bot.walknode.selection_area_color; + } + + } + else if (first_area_node > 0 && nodes[first_area_node].area_color) // Check for existing color + { + nodes[node].area_color = nodes[first_area_node].area_color; // Use existing color + } + else + { + nodes[node].area_color = MakeColor((rand() % 255), (rand() % 255), (rand() % 255), 255); // Assigned rand color + } + + /* + // Check for an existing area color + //int first_area_node = DFS_area_nodes[nodes[node].area][0]; // Get first node in area + if (first_area_node > 0 && nodes[first_area_node].area_color) // Check for existing color + { + nodes[node].area_color = nodes[first_area_node].area_color; // Use existing color + } + else if (ent->bot.walknode.selection_area_color) // No nodes in array so use custom color + nodes[node].area_color = ent->bot.walknode.selection_area_color; + else + nodes[node].area_color = MakeColor(255, 255, 128, 255); // Gold - Aassigned area nodes (area == 0) + */ + } + + //if (nodes[node].area_color) + // color = nodes[node].area_color; + + //if (node_color) + // color = node_color; + + if (ent->bot.walknode.selection_node_first != INVALID && nodes[node].area > 0 && nodes[node].area != nodes[ent->bot.walknode.selection_node_first].area) + color = MakeColor(255, 0, 0, 255); // Red - Indicates the user is selecting nodes that already have an assigned area + else + color = MakeColor(0, 255, 0, 255); // Green - Indicates the user is selecting nodes that don't have an assigned area yet + + + DrawBox(node, nodes[node].origin, color, nodes[node].mins, nodes[node].maxs, 100, true); // Draw node box + DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", nodes[node].area), color, 100, true); // Draw node number + } + + //* + // Show all area nodes of + if (ent->bot.walknode.selection_node_count == 1) + if (ent->bot.walknode.selection_node_first != INVALID && nodes[ent->bot.walknode.selection_node_first].area > 0) + { + if (nodes[node].area_color) + color = nodes[node].area_color; + + //if (node_color) + // color = node_color; + + //color = MakeColor(0, 255, 0, 255); // Green + + for (int node = 0; node < numnodes; node++) // for each node + { + // Show all nodes of same area num + if (nodes[node].area == nodes[ent->bot.walknode.selection_node_first].area) + { + DrawBox(node, nodes[node].origin, color, nodes[node].mins, nodes[node].maxs, 100, true); // Draw node box + } + } + } + //*/ + + if (ent->bot.walknode.selection_area_used) + { + ent->bot.walknode.selection_area_color = 0; + ent->bot.walknode.selection_area_used = false; // Turn off area + } + } + } + } + } + + // Local client (call the function directly) + if (ent->client && ent->client->pers.draw != NULL && ent->client->clientNum == 0) + { + if (ent->client->pers.draw->DrawBox != NULL) + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = ent->client->pers.draw->DrawBox; + void(*DrawArrow)(int number, vec3_t start, vec3_t end, const uint32_t color, float line_width, int time, qboolean occluded) = ent->client->pers.draw->DrawArrow; + void(*DrawString)(int number, vec3_t origin, const char *string, const uint32_t color, int time, qboolean occluded) = ent->client->pers.draw->DrawString; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + players[0]->client->pers.draw->arrows_inuse = true; // Flag as being used + players[0]->client->pers.draw->strings_inuse = true; // Flag as being used + #define U32_YELLOW MakeColor(255, 255, 0, 255) + int linknum = 0; + for (int node = 0; node < numnodes; node++) // for each node + { + if (nodes[node].inuse == false) + continue; // Ignore nodes not in use + + if (ent->bot.walknode.highlighted_node != node && ent->bot.walknode.prev_highlighted_node != node) + if (VectorDistance(ent->s.origin, nodes[node].origin) > 768) + continue; // Ignore nodes too far away + + // Highlighted box + if (ent->bot.walknode.highlighted_node == node) + color = MakeColor(255, 0, 0, 255); // Red + else if (nodes[node].type == NODE_LADDER) + color = MakeColor(0, 255, 0, 255); // Green + else if (nodes[node].type == NODE_POI) + color = MakeColor(0, 255, 255, 255); // Cyan + else + color = MakeColor(0, 0, 255, 255); // Blue + + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + if (nodes[node].area > 0) + { + if (nodes[node].area_color > 0) // Custom color + color = nodes[node].area_color; + else + color = MakeColor(255, 255, 128, 255); // Gold - Assigned area nodes (area > 0) + } + else + color = MakeColor(128, 128, 128, 255); // Grey - Unassigned area nodes (area == 0) + } + + // Draw the node links (arrows) + if (ent->bot.walknode.highlighted_node_type != HIGHLIGHTED_NODE_SELECT && ent->bot.walknode.highlighted_node_type != HIGHLIGHTED_NODE_SELECT_SMART) + { + for (int link = 0; link < nodes[node].num_links; link++) // for each node - MAXLINKS + { + int to = nodes[node].links[link].targetNode; + if (to != INVALID) + { + // Link nodes: only show links to and from highlighted node + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINK && ent->bot.walknode.highlighted_node != INVALID) + { + if (to != ent->bot.walknode.highlighted_node && node != ent->bot.walknode.highlighted_node) + continue; + } + // Change link type: only show links to and from highlighted node + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINKTYPE) + { + if (ent->bot.walknode.highlighted_node != INVALID && to != ent->bot.walknode.highlighted_node && node != ent->bot.walknode.highlighted_node) + continue; + if (ent->bot.walknode.prev_highlighted_node != INVALID && to != ent->bot.walknode.prev_highlighted_node && node != ent->bot.walknode.prev_highlighted_node) + continue; + } + + if (nodes[node].links[link].targetNodeType == NODE_POI_LOOKAT) + DrawArrow(linknum, nodes[node].origin, nodes[to].origin, MakeColor(0, 255, 255, 255), 1.0, 100, true); // Cyan node link + else + DrawArrow(linknum, nodes[node].origin, nodes[to].origin, U32_YELLOW, 1.0, 100, true); // Yellow node link + linknum++; + } + } + } + + DrawBox(node, nodes[node].origin, color, nodes[node].mins, nodes[node].maxs, 100, true); // Draw node box + + if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) + { + if (nodes[node].area > 0) // Only draw area num if > 0 + DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", nodes[node].area), U32_YELLOW, 100, true); // Draw area number + } + else + DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", node), U32_YELLOW, 100, true); // Draw node number + } + } + } + /* + else // Networked clients (send a message) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_DRAWARROW); + gi.WriteLong(U32_YELLOW); // Color + gi.WriteByte(5); // Line width + gi.WritePosition(start); + gi.WritePosition(end); + //gi.WritePosition(ent->s.origin); + //gi.WritePosition(tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 128)); + gi.multicast(start, MULTICAST_PVS); + } + */ + } +#endif + //rekkie -- debug drawing -- e + //rekkie -- Walknodes -- e + + //rekkie -- surface data -- e + + if (0 && ent->is_bot == false) + { + //ent->client->idle_weapon, ent->client->bandage_stopped, ent->client->bandaging + //Com_Printf("%s %s idle_weapon[%d] bandage_stopped[%d] bandaging[%d]\n", __func__, ent->client->pers.netname, ent->client->idle_weapon, ent->client->bandage_stopped, ent->client->bandaging); + //Com_Printf("%s %s viewheight[%d]\n", __func__, ent->client->pers.netname, ent->viewheight); + + if (0) + { + edict_t* self = ent; // DELETE ME !!!!!!!!!!!!!!!!!!!!!! + + int ammo_index, rounds; + gitem_t* ammo_item; + qboolean loaded = true; + qboolean clips = true; + + if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME) + || self->client->weapon == FindItem(MP5_NAME) || self->client->weapon == FindItem(M4_NAME) + || self->client->weapon == FindItem(M3_NAME) || self->client->weapon == FindItem(HC_NAME) + || self->client->weapon == FindItem(SNIPER_NAME)) + { + if (self->client->weapon->ammo) // Do we have ammo + { + ammo_item = FindItem(self->client->weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + // Check ammo + if (self->client->weapon == FindItem(MK23_NAME)) + { + rounds = self->client->mk23_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + loaded = false; + } + else if (self->client->weapon == FindItem(DUAL_NAME)) + { + rounds = self->client->dual_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + loaded = false; + } + else if (self->client->weapon == FindItem(MP5_NAME)) + { + rounds = self->client->mp5_rds; + if (rounds < 1) + loaded = false; + } + else if (self->client->weapon == FindItem(M4_NAME)) + { + rounds = self->client->m4_rds; + if (rounds < 1) + loaded = false; + } + else if (self->client->weapon == FindItem(M3_NAME)) + { + rounds = self->client->shot_rds; + if (rounds < self->client->shot_max) + loaded = false; + } + else if (self->client->weapon == FindItem(HC_NAME)) + { + rounds = self->client->cannon_rds; + if (hc_single->value && self->client->pers.hc_mode) // Single barrel fire mode + { + if (rounds < 1) + loaded = false; + } + else if (rounds < 2) // Double barrel fire mode + loaded = false; + } + else if (self->client->weapon == FindItem(SNIPER_NAME)) + { + rounds = self->client->sniper_rds; + if (rounds < self->client->sniper_max) + loaded = false; + } + + //Com_Printf("%s %s inv_ammo[%d] rnds[%d] loaded[%d] clips[%d]\n", __func__, self->client->pers.netname, self->client->inventory[ammo_index], rounds, loaded, clips); + + // No ammo - drop weapon + if (!loaded && !clips) + { + DropSpecialWeapon(self); + } + // Reload + else if (!loaded) + { + Cmd_New_Reload_f(self); + } + } + } + } + + if (0 && ent->is_bot == false) + { + if (players[1] && players[1]->is_bot) + { + // Project trace from the player's weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, ent->viewheight - 8); + G_ProjectSource(ent->s.origin, offset, forward, right, start); + float dist = VectorDistance(ent->s.origin, players[1]->s.origin); + VectorMA(start, dist, forward, end); + + // Adjust the mins/maxs box size based on size + vec3_t bmins = { -16, -16, +9}; + vec3_t bmaxs = { 16, 16, +10}; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + VectorAdd(end, bmins, absmin); + VectorAdd(end, bmaxs, absmax); + + if (BOTLIB_BoxIntersection(absmin, absmax, players[1]->absmin, players[1]->absmax)) // Do boxes intersect? + Com_Printf("%s %s can hit %s\n", __func__, ent->client->pers.netname, players[1]->client->pers.netname); + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (1) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(players[1]->s.number, players[1]->s.origin, blue, players[1]->mins, players[1]->maxs, 100, false); // Player hitbox + DrawBox(ent->s.number, end, yellow, bmins, bmaxs, 100, false); // Projectile hitbox + } +#endif + //rekkie -- debug drawing -- e + } + + } + + if (0 && ent->is_bot == false) + { + // Clear peak velocity if the player stops, then starts moving again + if (VectorEmpty(ent->velocity)) + ent->velocity_clear = true; + if (ent->velocity_clear && VectorEmpty(ent->velocity) == false) + { + ent->velocity_clear = false; + VectorClear(ent->velocity_peak); + ent->speed_peak = 0; + } + + if (ent->velocity[0] > ent->velocity_peak[0]) ent->velocity_peak[0] = ent->velocity[0]; // 800 + if (ent->velocity[1] > ent->velocity_peak[1]) ent->velocity_peak[1] = ent->velocity[1]; // 870 + if (ent->velocity[2] > ent->velocity_peak[2]) ent->velocity_peak[2] = ent->velocity[2]; // 260 + + // Get the surface normal of the ground the player is standing on + trace_t tr = gi.trace(ent->s.origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] - 128), ent, MASK_PLAYERSOLID); + + // calculate velocity speed + float speed = VectorLength(ent->velocity); + if (speed > ent->speed_peak) ent->speed_peak = speed; // 937 + + Com_Printf("%s speed[%f] peak[%f] normal[%f] velocity_peak[%f %f %f]\n", __func__, speed, ent->speed_peak, tr.plane.normal[2], ent->velocity_peak[0], ent->velocity_peak[1], ent->velocity_peak[2]); + } + + + + int show_nodes_max_dist = 29999; // max dist nodes will show from selected point in space + //show_nodes_max_dist = 99999; + // true + // false + static qboolean show_distance = false; + static qboolean show_nodes = false; + static qboolean show_MOVE_nodes_only = false; // only shows nodes that are for MOVE - requires 'show_nodes = true' + static qboolean show_step_nodes_only = false; // only shows nodes that are for STEPS - requires 'show_nodes = true' + static qboolean show_item_nodes_only = false; // only shows nodes that are for ITEMS - requires 'show_nodes = true' + static qboolean show_ladder_nodes_only = false; // only shows nodes that are for LADDER_UP / LADDER_DOWN - requires 'show_nodes = true' + // + static qboolean show_links = false; + static qboolean show_MOVE_links_only = false; // only shows links that are for MOVE - requires 'show_links = true' + // + static qboolean print_link_details = false; + static qboolean show_ladder_links = false; + static qboolean bot_visit_selected_node = true; // tell bots to go to selected node (requires show_nodes = true) + if (ent->is_bot == false && dedicated->value == 0) + { + // Force turn on nodes if links active + //if (show_links && !show_nodes) show_nodes = true; + //if (show_nodes && !show_links) show_links = true; + + // Project trace from the player's weapon POV -- s + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, ent->viewheight - 8); + offset[1] = 0; + G_ProjectSource(ent->s.origin, offset, forward, right, start); + VectorMA(start, 8192, forward, end); + trace_t tr; + tr = gi.trace(start, NULL, NULL, end, ent, MASK_SOLID); + // Project trace from the player's weapon POV -- e + + // Display nodes cloest to the player + int latched_buttons = client->latched_buttons; + int oldbuttons = client->buttons; + int buttons = ucmd->buttons; + latched_buttons |= buttons & ~oldbuttons; + if (latched_buttons & BUTTON_ATTACK) + { + //LaunchPlayer(ent, tr.endpos); + //BOTLIB_Jump_Takeoff(ent, NULL, tr.endpos, ent->viewheight, ent->velocity); + //BOTLIB_DoParabolaJump(ent, tr.endpos); + + if (show_distance) // Calculate the distance to the target + { + vec3_t dist; + //vec3_t velocity; + VectorSubtract(tr.endpos, ent->s.origin, dist); + float distance = VectorLength(dist); + Com_Printf("%s dist %f\n", __func__, distance); + } + + if (show_nodes) + { + int closest_dist = 99999; + int closest_node = INVALID; + int nodes_shown = 0; + vec3_t v; + float dist; + for (int node = 0; node < numnodes; node++) // for each node + { + if (nodes[node].inuse == false) continue; // Ignore nodes not in use + + //if (nodes_shown > 256) + // break; + + if (show_MOVE_nodes_only) // only show MOVE nodes + { + if (nodes[node].type != NODE_MOVE) // if not a move node, continue searching + continue; + } + if (show_step_nodes_only) // only show step nodes + { + //show_nodes_max_dist *= 3; // increase distance shown + if (nodes[node].type != NODE_STEP) // if not a step node, continue searching + continue; + } + if (show_item_nodes_only) + { + //show_nodes_max_dist *= 3; // increase distance shown + if (nodes[node].type != NODE_ITEM) // if not a step node, continue searching + continue; + } + if (show_ladder_nodes_only) + { + //show_nodes_max_dist *= 3; // increase distance shown + if (nodes[node].type != NODE_LADDER_DOWN && nodes[node].type != NODE_LADDER_UP) // if not a step node, continue searching + continue; + } + + VectorSubtract(nodes[node].origin, tr.endpos, v); // subtract first + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + + if (dist < show_nodes_max_dist) + { + // Find closest node + if (dist < closest_dist) + { + closest_dist = dist; + closest_node = node; + } + ACEND_ShowNode(node); + nodes_shown++; + + //ent->bot.highlighted_node = node; + + + } + } + + ent->show_node_links = closest_node; + ent->show_node_links_time = level.framenum + 10.0 * HZ; + if (closest_node >= 0) + Com_Printf("%s %s selected node %d type %d origin[%f %f %f]\n", __func__, ent->client->pers.netname, closest_node, nodes[closest_node].type, nodes[closest_node].origin[0], nodes[closest_node].origin[1], nodes[closest_node].origin[2]); + + // tell bots to go to selected node + if (bot_visit_selected_node) + if (closest_node > 0 && closest_node < numnodes) + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + //players[i]->bot.goal_node = closest_node; + //BOTLIB_SetGoal(players[i], closest_node); + if (BOTLIB_CanGotoNode(players[i], players[i]->bot.goal_node, false)) + { + Com_Printf("%s %s is heading to node %d\n", __func__, players[i]->client->pers.netname, closest_node); + } + else + { + Com_Printf("%s %s cannot reach node %d\n", __func__, players[i]->client->pers.netname, closest_node); + } + } + } + } + + if (print_link_details) + if (ent->show_node_links != INVALID && ent->show_node_links < numnodes) + { + //Com_Printf("%s Node %d type %d\n", __func__, nodes[ent->show_node_links].nodenum, nodes[ent->show_node_links].type); + + int targetNode; + for (int i = 0; i < MAXLINKS; i++) + { + // Is target node valid + if ((targetNode = nodes[ent->show_node_links].links[i].targetNode) > INVALID) + { + if (show_MOVE_links_only) // only show MOVE links + { + if (nodes[targetNode].type != NODE_MOVE) // if not a move link, continue searching + continue; + } + + float higher = 0; float lower = 99999; + if (nodes[ent->show_node_links].origin[2] > nodes[targetNode].origin[2]) + higher = (nodes[ent->show_node_links].origin[2] - nodes[targetNode].origin[2]); + else if (nodes[ent->show_node_links].origin[2] < nodes[targetNode].origin[2]) + lower = (nodes[targetNode].origin[2] - nodes[ent->show_node_links].origin[2]); + + VectorSubtract(nodes[targetNode].origin, nodes[ent->show_node_links].origin, v); + dist = VectorLength(v); + + if (higher > 0) // Node is higher than origin + Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_up: %f\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, higher); + else if (lower != 99999) // Node is lower than origin + Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_dn: %f\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, lower); + else // Node at equal dist to origin + Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_eq: %f\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, 0); + } + } + } + } + } + + // Show links from node + if (show_links) + if (ent->show_node_links != INVALID && ent->show_node_links < numnodes && ent->show_node_links_time > level.framenum) + { + int targetNode; + for (int i = 0; i < MAXLINKS; i++) + { + // Is target node valid + if ((targetNode = nodes[ent->show_node_links].links[i].targetNode) > INVALID) // Node + { + if (show_MOVE_links_only) // only show MOVE links + { + if (nodes[targetNode].type != NODE_MOVE) // if not a move link, continue searching + continue; + } + + // Show only this type + //if (nodes[ent->show_node_links].links[i].targetNodeType == NODE_MOVE) + { + // Show a visual laser link + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[ent->show_node_links].origin); // start + gi.WritePosition(nodes[targetNode].origin); // end + gi.multicast(nodes[ent->show_node_links].origin, MULTICAST_PHS); + } + } + } + } + + // Show all ladder links + if (show_ladder_links) + //if (ent->show_node_links_time > level.framenum) + { + for (int node = 0; node < numnodes; node++) // for each node + { + if (nodes[node].inuse == false) continue; // Ignore nodes not in use + /* + if ((node + 1) < numnodes && nodes[node].type == NODE_LADDER_DOWN && nodes[node + 1].type == NODE_LADDER_UP) // Bottom and top of ladder + { + vec3_t v; + float dist; + VectorSubtract(nodes[node].origin, tr.endpos, v); // Find distance from player shot trace to bottom of ladder + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + if (dist < 149999) + { + //Com_Printf("%s targetNode %i type %i\n", __func__, targetNode, nodes[targetNode].type); + // Show a visual laser link + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[node].origin); // start + gi.WritePosition(nodes[node+1].origin); // end + gi.multicast(nodes[node].origin, MULTICAST_PHS); + } + } + */ + if (nodes[node].type == NODE_LADDER_UP || nodes[node].type == NODE_LADDER_DOWN) // Node is ladder + //if (nodes[node].type == NODE_LADDER) // Node is ladder + { + int targetNode; + for (int i = 0; i < MAXLINKS; i++) + { + // Is target node valid + targetNode = nodes[node].links[i].targetNode; + if (targetNode > INVALID) // && (nodes[targetNode].type == NODE_LADDER_UP && nodes[targetNode].type == NODE_LADDER_DOWN)) // Target is also ladder + { + vec3_t v; + float dist; + VectorSubtract(nodes[node].origin, tr.endpos, v); // subtract first + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + if (dist < 9999) + { + //Com_Printf("%s targetNode %i type %i\n", __func__, targetNode, nodes[targetNode].type); + + // Show a visual laser link + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[node].origin); // start + gi.WritePosition(nodes[targetNode].origin); // end + gi.multicast(nodes[node].origin, MULTICAST_PHS); + } + } + } + } + + } + } + } + //rekkie -- DEV_1 -- e + +#ifndef NO_BOTS + //rekkie -- remember last position if it's different + //if (VectorDistance(ent->s.origin, ent->bot.last_position) > 8) + //if (VectorLength(ent->velocity) > 37) + // VectorCopy(ent->s.origin, ent->bot.last_position); + if (VectorCompare(ent->bot.tmp_position, ent->bot.last_position) == false && VectorCompare(ent->s.origin, ent->bot.tmp_position) == false) + VectorCopy(ent->bot.tmp_position, ent->bot.last_position); + if (VectorCompare(ent->s.origin, ent->bot.tmp_position) == false) + VectorCopy(ent->s.origin, ent->bot.tmp_position); +#endif + + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + client->latched_buttons |= client->buttons & ~client->oldbuttons; + // save light level the player is standing on for + // monster sighting AI + ent->light_level = ucmd->lightlevel; + + // fire weapon from final position if needed + if (client->latched_buttons & BUTTON_ATTACK) { + //TempFile + //We're gonna fire in this frame? Then abort any punching. + client->punch_framenum = level.framenum; + client->punch_desired = false; + + if (ent->solid == SOLID_NOT && ent->deadflag != DEAD_DEAD && !in_warmup) { + client->latched_buttons = 0; + +//rekkie -- DEV_1 -- prevent chase cam while show nodes is enabled -- s +#ifndef NO_BOTS + if (ent->bot.walknode.enabled == false) // Allow chase cam if nav_edit is disabled +#endif +//rekkie -- DEV_1 -- prevent chase cam while show nodes is enabled -- e + NextChaseMode( ent ); } else { ClientThinkWeaponIfReady( ent, false ); } From 87a34b54d04ebed505559f21e8d44f4a3e7409c6 Mon Sep 17 00:00:00 2001 From: Reki Date: Thu, 16 May 2024 14:03:32 -0400 Subject: [PATCH 186/974] Fix up some bad assumptions about protocol 38 packet structure --- src/client/main.c | 2 +- src/client/parse.c | 4 ++-- src/server/entities.c | 21 ++++++++++----------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/client/main.c b/src/client/main.c index ce66e7906..0837d2aed 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -301,7 +301,7 @@ void CL_UpdateRecordingSetting(void) static void CL_UpdateFlaresSetting(void) { - if (cls.netchan.protocol != PROTOCOL_VERSION_Q2PRO) { + if (cls.netchan.protocol != PROTOCOL_VERSION_Q2PRO && cls.netchan.protocol != PROTOCOL_VERSION_AQTION) { return; } if (!cl.csr.extended) { diff --git a/src/client/parse.c b/src/client/parse.c index 3e53157e0..4126df1bd 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -329,7 +329,7 @@ static void CL_ParseFrame(int extrabits) frame.areabytes = 0; } - if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { + if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol == PROTOCOL_VERSION_AQTION) { if (MSG_ReadByte() != svc_playerinfo) { Com_Error(ERR_DROP, "%s: not playerinfo, value: %i", __func__, MSG_ReadByte()); } @@ -399,7 +399,7 @@ static void CL_ParseFrame(int extrabits) } // parse packetentities - if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { + if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT || cls.serverProtocol == PROTOCOL_VERSION_AQTION) { if (MSG_ReadByte() != svc_packetentities) { Com_Error(ERR_DROP, "%s: not packetentities", __func__); } diff --git a/src/server/entities.c b/src/server/entities.c index 4ed790be9..906585378 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -434,22 +434,20 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) } clientEntityNum = 0; - if (client->protocol == PROTOCOL_VERSION_AQTION) { - if (frame->ps.pmove.pm_type < PM_DEAD && !client->settings[CLS_RECORDING]) { - clientEntityNum = frame->clientNum + 1; - } - if (client->settings[CLS_NOPREDICT]) { - psFlags |= MSG_PS_IGNORE_PREDICTION; - } - suppressed = client->frameflags; - } else { - suppressed = client->suppress_count; - } + if (frame->ps.pmove.pm_type < PM_DEAD && !client->settings[CLS_RECORDING]) { + clientEntityNum = frame->clientNum + 1; + } + if (client->settings[CLS_NOPREDICT]) { + psFlags |= MSG_PS_IGNORE_PREDICTION; + } + suppressed = client->frameflags; + if (client->csr->extended) { psFlags |= MSG_PS_EXTENSIONS; } // delta encode the playerstate + MSG_WriteByte(svc_playerinfo); extraflags = MSG_WriteDeltaPlayerstate_Aqtion(oldstate, &frame->ps, psFlags); if (client->protocol == PROTOCOL_VERSION_AQTION) { @@ -475,6 +473,7 @@ void SV_WriteFrameToClient_Aqtion(client_t *client) client->frameflags = 0; // delta encode the entities + MSG_WriteByte(svc_packetentities); SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum); #if AQTION_EXTENSION From 3bfe0878fb72bc3a4b2349ca6daa3642e78ff4fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 16 May 2024 21:43:05 +0300 Subject: [PATCH 187/974] Add generic function to check entity number. --- src/server/entities.c | 6 +----- src/server/mvd.c | 8 ++------ src/server/server.h | 10 ++++++++++ src/server/user.c | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index cdd1d1cb5..993112192 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -678,11 +678,7 @@ void SV_BuildClientFrame(client_t *client) } } - if (ent->s.number != e) { - Com_WPrintf("%s: fixing ent->s.number: %d to %d\n", - __func__, ent->s.number, e); - ent->s.number = e; - } + SV_CheckEntityNumber(ent, e); // optionally skip it if (customize && customize(clent, ent, NULL) == CE_SKIP) diff --git a/src/server/mvd.c b/src/server/mvd.c index a3fb270af..71ba2ad06 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -587,7 +587,7 @@ static void build_gamestate(void) continue; } - ent->s.number = i; + SV_CheckEntityNumber(ent, i); MSG_PackEntity(&mvd.entities[i], &ent->s, ENT_EXTENSION(&svs.csr, ent)); if (svs.csr.extended) mvd.entities[i].solid = sv.entities[i].solid32; @@ -779,11 +779,7 @@ static void emit_frame(void) continue; } - if (ent->s.number != i) { - Com_WPrintf("%s: fixing ent->s.number: %d to %d\n", - __func__, ent->s.number, i); - ent->s.number = i; - } + SV_CheckEntityNumber(ent, i); // calculate flags flags = mvd.esFlags; diff --git a/src/server/server.h b/src/server/server.h index ced9554f0..59bdc075a 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -749,6 +749,16 @@ void SV_PrintMiscInfo(void); #define HAS_EFFECTS(ent) \ ((ent)->s.modelindex || (ent)->s.effects || (ent)->s.sound || (ent)->s.event) +static inline void SV_CheckEntityNumber(edict_t *ent, int e, const char *func) +{ + if (q_unlikely(ent->s.number != e)) { + Com_WPrintf("%s: fixing ent->s.number: %d to %d\n", func, ent->s.number, e); + ent->s.number = e; + } +} + +#define SV_CheckEntityNumber(ent, e) SV_CheckEntityNumber(ent, e, __func__) + void SV_BuildClientFrame(client_t *client); bool SV_WriteFrameToClient_Default(client_t *client, unsigned maxsize); bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize); diff --git a/src/server/user.c b/src/server/user.c index e0e4e85b2..362168ac5 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -66,7 +66,7 @@ static void SV_CreateBaselines(void) continue; } - ent->s.number = i; + SV_CheckEntityNumber(ent, i); chunk = &sv_client->baselines[i >> SV_BASELINES_SHIFT]; if (*chunk == NULL) { From 0436f875ea6718a4538afdd1c431c454723b86b0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 16 May 2024 21:46:37 +0300 Subject: [PATCH 188/974] Clean up SV_BuildClientFrame() a bit. --- src/server/entities.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 993112192..6d4257353 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -547,8 +547,8 @@ void SV_BuildClientFrame(client_t *client) client_frame_t *frame; entity_packed_t *state; player_state_t *ps; - int clientarea, clientcluster; const mleaf_t *leaf; + int clientarea, clientcluster; byte clientphs[VIS_MAX_BYTES]; byte clientpvs[VIS_MAX_BYTES]; bool need_clientnum_fix; @@ -625,22 +625,19 @@ void SV_BuildClientFrame(client_t *client) ent = EDICT_NUM2(client->ge, e); // ignore entities not in use - if (!ent->inuse && (g_features->integer & GMF_PROPERINUSE)) { + if (!ent->inuse && (g_features->integer & GMF_PROPERINUSE)) continue; - } // ignore ents without visible models if (ent->svflags & SVF_NOCLIENT) continue; // ignore ents without visible models unless they have an effect - if (!HAS_EFFECTS(ent)) { + if (!HAS_EFFECTS(ent)) continue; - } - if ((ent->s.effects & EF_GIB) && client->settings[CLS_NOGIBS]) { + if ((ent->s.effects & EF_GIB) && client->settings[CLS_NOGIBS]) continue; - } if (client->csr->extended && ent->s.renderfx & RF_FLARE && client->settings[CLS_NOFLARES]) continue; From d6650ade47630088470954524c62c791eefe38b1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 16 May 2024 21:49:41 +0300 Subject: [PATCH 189/974] Consider EF_GREENGIB for entity priority too. --- src/client/entities.c | 2 +- src/server/entities.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/entities.c b/src/client/entities.c index 10e6fddf4..47dd7c491 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -602,7 +602,7 @@ static void CL_AddPacketEntities(void) ent.oldorigin[2] += autobob; } - if ((effects & EF_GIB) && !cl_gibs->integer) + if (effects & (EF_GIB | EF_GREENGIB) && !cl_gibs->integer) goto skip; // create a new entity diff --git a/src/server/entities.c b/src/server/entities.c index 6d4257353..cc5ad7b97 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -499,7 +499,7 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) (ent->s.number <= sv_client->maxclients || ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) || ent->solid == SOLID_BSP) #define LO_PRIO(ent) \ - ((ent->s.renderfx & RF_LOW_PRIORITY) || (ent->s.effects & EF_GIB) || (!ent->s.modelindex && !ent->s.effects)) + ((ent->s.renderfx & RF_LOW_PRIORITY) || (ent->s.effects & (EF_GIB | EF_GREENGIB)) || (!ent->s.modelindex && !ent->s.effects)) static int entpriocmp(const void *p1, const void *p2) { @@ -636,7 +636,7 @@ void SV_BuildClientFrame(client_t *client) if (!HAS_EFFECTS(ent)) continue; - if ((ent->s.effects & EF_GIB) && client->settings[CLS_NOGIBS]) + if (ent->s.effects & (EF_GIB | EF_GREENGIB) && client->settings[CLS_NOGIBS]) continue; if (client->csr->extended && ent->s.renderfx & RF_FLARE && client->settings[CLS_NOFLARES]) From 7c769d52dcdd28691e24c614179619104468e183 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 16 May 2024 21:50:04 +0300 Subject: [PATCH 190/974] Check for RF_LOW_PRIORITY exclusively in extended mode. --- src/server/entities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/entities.c b/src/server/entities.c index cc5ad7b97..715a1dcaf 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -499,7 +499,7 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) (ent->s.number <= sv_client->maxclients || ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) || ent->solid == SOLID_BSP) #define LO_PRIO(ent) \ - ((ent->s.renderfx & RF_LOW_PRIORITY) || (ent->s.effects & (EF_GIB | EF_GREENGIB)) || (!ent->s.modelindex && !ent->s.effects)) + ((sv_client->csr->extended ? (ent->s.renderfx & RF_LOW_PRIORITY) : (ent->s.effects & (EF_GIB | EF_GREENGIB))) || (!ent->s.modelindex && !ent->s.effects)) static int entpriocmp(const void *p1, const void *p2) { From a587865e02e68c51163be5994ccb3a1eca94a7cc Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 16 May 2024 15:53:28 -0400 Subject: [PATCH 191/974] Cleaned up a few warnings --- src/action/botlib/botlib_nav.c | 1 + src/action/botlib/botlib_nodes.c | 34 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 4b2716a36..afec7a1c5 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -825,6 +825,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz else // Fallback to old pathing { self->bot.goal_node = INVALID; + //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) { diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 6ab4559ae..7125b7d0d 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -1132,7 +1132,7 @@ void BOTLIB_UTIL_MakePolySquare(int face, vec3_t out) // Find each edge that goes in the same direction // Try to find the total length of that direction vec3_t v1, v2; - float highest = 0, lowest = 0; + //float highest = 0, lowest = 0; float v1_x; // V1 X float v1_y; // V1 Y float v2_x; // V2 X @@ -1343,7 +1343,7 @@ void LaunchP(edict_t* ent, vec3_t origin, vec3_t target) VectorCopy(ent->velocity, ent->client->oldvelocity); VectorCopy(ent->velocity, ent->avelocity); - float speed = VectorLength(velocity); + //float speed = VectorLength(velocity); //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); } @@ -1858,7 +1858,8 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma } qboolean target_is_above = false, target_is_below = false, target_is_equal = false; - float higher = 0, lower = 0; + float higher = 0; + float lower = 0; if (origin[2] > target[2]) // We're above the target { target_is_below = true; @@ -2121,7 +2122,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma //qboolean TrianglesAreCoplanar(vec3_t t1_p1, vec3_t t1_p2, vec3_t t1_p3, vec3_t t2_p1, vec3_t t2_p2, vec3_t t2_p3) // Explodes all breakable glass :D -void Remove_All_Breakableglass() +void Remove_All_Breakableglass(void) { edict_t* glass; for (glass = g_edicts; glass < &g_edicts[globals.num_edicts]; glass++) @@ -2164,7 +2165,7 @@ void kill_door(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, G_FreeEdict(self); } // Removes all door types -void Remove_All_Doors() +void Remove_All_Doors(void) { edict_t* door; for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) @@ -2247,11 +2248,11 @@ void ACEND_FindEdgeLadders(void) { int f, e, f2, f3, i; trace_t tr; - vec3_t zero = { 0 }; + //vec3_t zero = { 0 }; // Player when standing - vec3_t player_standing_mins_up = { -16, -16, -1 }; + //vec3_t player_standing_mins_up = { -16, -16, -1 }; //vec3_t player_standing_mins = { -16, -16, -0 }; - vec3_t player_standing_maxs = { 16, 16, 32 }; + //vec3_t player_standing_maxs = { 16, 16, 32 }; //vec3_t *ladder_faces = (vec3_t*)malloc(sizeof(vec3_t) * MAX_LADDER_FACES); @@ -2863,7 +2864,7 @@ void QsortLinks(QSLink* links, size_t length) { } // Sorts node links by height. Highest to lowest. -void QSNodeLinksByHeight() +void QSNodeLinksByHeight(void) { QSLink qslinks[MAXLINKS]; // Temp links @@ -3001,7 +3002,7 @@ void BOTLIB_SaveNavCompressed(void) } // Compress the buffer - int compressed_buff_len = 0; + long compressed_buff_len = 0; char* compressed_buff = (char*)malloc(uncompressed_buff_len); // Make the compressed buffer as large as the uncompressed buffer if (compressed_buff != NULL) { @@ -3118,9 +3119,9 @@ void BOTLIB_LoadNavCompressed(void) Com_Printf("%s Reading NAV file... detected version [%d]\n", __func__, version); // Read Compressed and uncompressed buffer sizes - int uncompressed_buff_len = 0; + long uncompressed_buff_len = 0; fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size - int compressed_buff_len = 0; + long compressed_buff_len = 0; fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size // Read compressed buffer @@ -4632,7 +4633,7 @@ void BOTLIB_AddItemNodes() } // Find the reachability for each node -void BOTLIB_ProcesssReachabilities() +void BOTLIB_ProcesssReachabilities(void) { vec3_t zero = { 0 }; node_t* from_node; // From node @@ -4792,7 +4793,8 @@ void BOTLIB_Process_NMesh(edict_t* ent) qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) vec3_t mid_point = { 0 }; - vec3_t ladder_bottom = { 0 }, ladder_top = { 0 }; + vec3_t ladder_bottom = { 0 }; + vec3_t ladder_top = { 0 }; float tmp = 0; int f, e; @@ -5417,9 +5419,9 @@ void BOTLIB_InitNavigation(edict_t* ent) return; } - if (bsp->checksum == NULL) + if (bsp->checksum == 0) { - gi.dprintf("%s bsp->checksum is null\n", __func__); + gi.dprintf("%s bsp->checksum is zero\n", __func__); return; } From ea27c19f7e52106def0d6f48b1fb23a589fe6ddf Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 16 May 2024 15:53:57 -0400 Subject: [PATCH 192/974] dos2unix'd the whole botlib dir --- src/action/botlib/botlib.h | 928 +- src/action/botlib/botlib_ai.c | 2432 ++-- src/action/botlib/botlib_cmd.c | 1588 +-- src/action/botlib/botlib_communication.c | 1100 +- src/action/botlib/botlib_ctf.c | 2086 +-- src/action/botlib/botlib_items.c | 512 +- src/action/botlib/botlib_math.c | 306 +- src/action/botlib/botlib_movement.c | 14622 ++++++++++----------- src/action/botlib/botlib_nav.c | 7284 +++++----- src/action/botlib/botlib_nodes.c | 11150 ++++++++-------- src/action/botlib/botlib_spawn.c | 4192 +++--- src/action/botlib/botlib_spawnpoints.c | 972 +- src/action/botlib/botlib_weapons.c | 2474 ++-- 13 files changed, 24823 insertions(+), 24823 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index ff707cb66..10eefb319 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -1,465 +1,465 @@ -#ifndef _BOTLIB_H -#define _BOTLIB_H - -#define BOT_AAS_VERSION 1 - -#define BOT_NAV_VERSION 2 -#define BOT_NAV_VERSION_1 1 -#define BOT_NAV_VERSION_2 2 -#define BOT_NAV_VERSION_MAX BOT_NAV_VERSION_2 - -// Bot move states -#define BOT_MOVE_STATE_NONE 0 // Bot is booting up :-) -#define BOT_MOVE_STATE_NAV 1 // Getting a navigational path -//#define BOT_MOVE_STATE_NAV_NEXT 2 // Getting next navigational path (if any) -#define BOT_MOVE_STATE_MOVE 3 // Standard movement -#define BOT_MOVE_STATE_WANDER 4 // No navigation and no movement, try wandering around -#define BOT_MOVE_STATE_STAND 5 // Stand still and hold a position -#define BOT_MOVE_STATE_FLEE 6 // Running away from enemy force -#define BOT_MOVE_STATE_COVER 7 // Under fire, take cover - - -typedef struct bot_connections_s -{ - qboolean tried_adding_prev_bots; // Flag if we tried to add any bots from a previous map. Only called once per map. - qboolean auto_balance_bots; // If the spawn manager should try to auto balance bots on teams - qboolean scale_up; // If the manager should scale up the bots - qboolean scale_dn; // If the manager should scale down the bots - - int total_bots; - int total_humans; // Total humans in server - int total_humans_playing; // Total humans in server and playing - - int total_team1; // Bots & Humans - int total_team2; - int total_team3; - - int spec_bots; // TP and DM - int team1_bots; // TP and DM - int team2_bots; // TP and 3T - int team3_bots; // TP and 3T - - - int spec_humans; // TP and DM - int team1_humans; // TP and DM - int team2_humans; // TP and 3T - int team3_humans; // TP and 3T - - int desire_bots; // How many bots we desire - int desire_team1; - int desire_team2; - int desire_team3; -} bot_connections_t; -extern bot_connections_t bot_connections; - -//the bot input, will be converted to a usercmd_t -typedef struct bot_input_s -{ - float thinktime; //time since last output (in seconds) - vec3_t dir; //movement direction - float speed; //speed in the range [0, 400] - vec3_t viewangles; //the view angles - int actionflags; //one of the ACTION_? flags - int radioflags; //one of the aq2 radio flags (i.e. RADIO_REPORTIN) - int weapon; //weapon to use - vec3_t look_at; //location to look at - int look_at_time; //time spent looking at location -} bot_input_t; - - - - - -// =========================================================================== -// botlib_ai.c -// =========================================================================== -void BOTLIB_Init(edict_t* self); // Initializing... HAL9000 is online. -void BOTLIB_Think(edict_t* self); // Thinking... I'm sorry Rekkie, I can't do that. -void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time); // Translates bot input to actual Q2 movement calls -qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount); // I see everything... in front of me -qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount); -qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount); -qboolean BOTLIB_CanMove(edict_t* self, int direction); // Can bot move in direction -void BOTLIB_TouchingLadder(edict_t* self); -float BOTLIB_ThrowingKnifePitch(edict_t* self); // Get pitch required to reach knife throwing target -void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type); -int BOTLIB_GetEquipment(edict_t* self); // Does bot need weapons, items, ammo? If so -//void BOTLIB_CheckCurrentWeapon(edict_t* self); //rekkie -- Check to make sure we're using a primary weapon where possible -void BOTLIB_GetWeaponsAndAmmo(edict_t* self); //rekkie -- Locate and pickup a primary weapon if we need one -short BOTLIB_FindVisibleAllies(edict_t* self); -qboolean BOTLIB_FindEnemy(edict_t* self); -void BOTLIB_PickLongRangeGoal(edict_t* self); - -typedef struct botlib_noises_s -{ - edict_t* owner[MAX_CLIENTS]; // The ent who made the noise - - // Noise timers - int self_time[MAX_CLIENTS]; - int weapon_time[MAX_CLIENTS]; - int impact_time[MAX_CLIENTS]; - - // Location of noise - vec3_t self_origin[MAX_CLIENTS]; - vec3_t weapon_origin[MAX_CLIENTS]; - vec3_t impact_origin[MAX_CLIENTS]; - -} botlib_noises_t; -extern botlib_noises_t botlib_noises; - -// Actionable flags -#define ACTION_NONE 0x00000000 // No action taken -#define ACTION_ATTACK 0x00000001 // Tap the attack button -#define ACTION_USE 0x00000002 -#define ACTION_RESPAWN 0x00000008 -#define ACTION_JUMP 0x00000010 // Small jumps - tapping the jump button -#define ACTION_MOVEUP 0x00000020 -#define ACTION_CROUCH 0x00000080 -#define ACTION_MOVEDOWN 0x00000100 -#define ACTION_MOVEFORWARD 0x00000200 -#define ACTION_MOVEBACK 0x00000800 -#define ACTION_MOVELEFT 0x00001000 -#define ACTION_MOVERIGHT 0x00002000 -#define ACTION_BOXJUMP 0x00008000 // Higher jumps -#define ACTION_TALK 0x00010000 -#define ACTION_GESTURE 0x00020000 -#define ACTION_WALK 0x00080000 -#define ACTION_AFFIRMATIVE 0x00100000 -#define ACTION_NEGATIVE 0x00200000 -#define ACTION_GETFLAG 0x00800000 -#define ACTION_GUARDBASE 0x01000000 -#define ACTION_PATROL 0x02000000 -#define ACTION_FOLLOWME 0x08000000 -#define ACTION_HOLDJUMP 0x10000000 // Hold the jump button and release when touching ground -#define ACTION_HOLDPOS 0x20000000 // Hold position -#define ACTION_JUMPPAD 0x40000000 - - -// =========================================================================== -// botlib_ctf.c -// =========================================================================== -qboolean BOTLIB_Carrying_Flag(edict_t* self); -int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance); -qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance); -qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance); -void BOTLIB_Update_Flags_Status(void); -int BOTLIB_CTF_Get_Flag_Node(edict_t* ent); -int BOTLIB_NearestFlag(edict_t* self); -float BOTLIB_DistanceToFlag(edict_t* self, int flagType); -void BOTLIB_CTF_Goals(edict_t* self); - -typedef struct ctf_status_s -{ - edict_t* flag1; // Red flag - edict_t* flag2; // Blue flag - int flag1_home_node; // The node where the red flag initially spawns - int flag2_home_node; // The node where the blue flag initially spawns - int flag1_curr_node; // The node where the red flag currently resides - int flag2_curr_node; // The node where the blue flag currently resides - qboolean flag1_is_home; // If the red flag is home - qboolean flag2_is_home; // If the blue flag is home - qboolean flag1_is_dropped; // If the red flag was dropped - qboolean flag2_is_dropped; // If the blue flag was dropped - qboolean flag1_is_carried; // If the red flag is being carried - qboolean flag2_is_carried; // If the blue flag is being carried - edict_t* player_has_flag1; // If any ent has the red flag - edict_t* player_has_flag2; // If any ent has the blue flag - float team1_carrier_dist_to_home; // How close the red team carrier is to the home red flag node - float team2_carrier_dist_to_home; // How close the blue team carrier is to the home blue flag node -} ctf_status_t; -extern ctf_status_t bot_ctf_status; - -// Get flag, retrieve flag, intercept flag carrier, etc. -typedef enum -{ - BOT_CTF_STATE_NONE, // No state - BOT_CTF_STATE_GET_ENEMY_FLAG, // Get enemy flag sitting in its home location - BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG, // Get dropped enemy flag - BOT_CTF_STATE_CAPTURE_ENEMY_FLAG, // Got flag, now capture it - BOT_CTF_STATE_ATTACK_ENEMY_CARRIER, // Enemy has our flag, go after them - BOT_CTF_STATE_RETURN_TEAM_FLAG, // Our team flag is on the ground, go return it - BOT_CTF_STATE_COVER_FLAG_CARRIER, // We have the enemy flag, cover the bot who has it - BOT_CTF_STATE_FORCE_MOVE_TO_FLAG, // Special case. Bot is forced to move in direction of flag. - BOT_CTF_STATE_GET_DROPPED_ITEMS // Gets dropped weapons, items, etc from ground - -} bot_ctf_state_t; - -// =========================================================================== -// botlib_cmd.c -// =========================================================================== -qboolean BOTLIB_SV_Cmds(void); // Server commands -qboolean BOTLIB_Commands(edict_t* ent); // Client commands - - -// =========================================================================== -// botlib_communication.c -// =========================================================================== -void BOTLIB_Wave(edict_t* ent, int type); -void BOTLIB_PrecacheRadioSounds(void); -void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); -void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); - -// Wave (gesture) types -#define WAVE_FLIPOFF 1 -#define WAVE_SALUTE 2 -#define WAVE_TAUNT 3 -#define WAVE_WAVE 4 -#define WAVE_POINT 5 - -// Radio flags -#define RADIO_1 0x00000001 -#define RADIO_2 0x00000002 -#define RADIO_3 0x00000003 -#define RADIO_4 0x00000004 -#define RADIO_5 0x00000005 -#define RADIO_6 0x00000006 -#define RADIO_7 0x00000007 -#define RADIO_8 0x00000008 -#define RADIO_9 0x00000009 -#define RADIO_10 0x00000010 -#define RADIO_BACK 0x00000011 -#define RADIO_COVER 0x00000012 -#define RADIO_DOWN 0x00000013 -#define RADIO_ENEMY_DOWN 0x00000014 -#define RADIO_ENEMY_SIGHTED 0x00000015 -#define RADIO_FORWARD 0x00000016 -#define RADIO_GO 0x00000017 -#define RADIO_IM_HIT 0x00000018 -#define RADIO_LEFT 0x00000019 -#define RADIO_REPORTIN 0x00000020 -#define RADIO_RIGHT 0x00000021 -#define RADIO_TAKING_FIRE 0x00000022 -#define RADIO_TEAMMATE_DOWN 0x00000023 -#define RADIO_TEAM_REPORT_IN 0x00000024 -#define RADIO_UP 0x00000025 - -// Each of the possible radio messages and their length -typedef struct bot_radio_msg_s -{ - char* msg; // the msg name - int length; // length in server frames (ie tenths of a second), rounded up - int sndIndex; -} bot_radio_msg_t; - -static bot_radio_msg_t bot_male_radio_msgs[] = { - {"1", 6, 0}, - {"2", 6, 0}, - {"3", 8, 0}, - {"4", 7, 0}, - {"5", 8, 0}, - {"6", 9, 0}, - {"7", 8, 0}, - {"8", 7, 0}, - {"9", 7, 0}, - {"10", 6, 0}, - {"back", 6, 0}, - {"cover", 7, 0}, - {"down", 13, 0}, - {"enemyd", 10, 0}, - {"enemys", 9, 0}, - {"forward", 6, 0}, - {"go", 6, 0}, - {"im_hit", 7, 0}, - {"left", 7, 0}, - {"reportin", 9, 0}, - {"right", 6, 0}, - {"taking_f", 22, 0}, - {"teamdown", 13, 0}, - {"treport", 12, 0}, - {"up", 4, 0} -}; - -static bot_radio_msg_t bot_female_radio_msgs[] = { - {"1", 5, 0}, - {"2", 5, 0}, - {"3", 5, 0}, - {"4", 5, 0}, - {"5", 5, 0}, - {"6", 8, 0}, - {"7", 7, 0}, - {"8", 5, 0}, - {"9", 5, 0}, - {"10", 5, 0}, - {"back", 6, 0}, - {"cover", 5, 0}, - {"down", 6, 0}, - {"enemyd", 9, 0}, - {"enemys", 9, 0}, - {"forward", 8, 0}, - {"go", 6, 0}, - {"im_hit", 7, 0}, - {"left", 8, 0}, - {"reportin", 9, 0}, - {"right", 5, 0}, - {"taking_f", 22, 0}, - {"teamdown", 10, 0}, - {"treport", 12, 0}, - {"up", 6, 0} -}; -static const int bot_numMaleSnds = (sizeof(bot_male_radio_msgs) / sizeof(bot_male_radio_msgs[0])); -static const int bot_numFemaleSnds = (sizeof(bot_female_radio_msgs) / sizeof(bot_female_radio_msgs[0])); - - -// =========================================================================== -// botlib_items.c -// =========================================================================== - - -// =========================================================================== -// botlib_math.c -// =========================================================================== -qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon); -bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs); -qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other); - -// =========================================================================== -// botlib_movement.c -// =========================================================================== -qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint); -void BOTLIB_Look(edict_t* self, usercmd_t* ucmd); - - -// =========================================================================== -// botlib_nav.c -// =========================================================================== -// Find all possible paths -- s - -#define MAX_NAV_AREAS 32 -#define MAX_NAV_AREAS_EDGES 64 -#define MAX_NAV_AREAS_PATHS 512 //512 -#define MAX_NAV_AREAS_NODES 4096 -extern int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] -extern int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] - - - -typedef struct { - - // Areas - int total_areas; // Total number of areas - //int area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area - int** area_nodes; - - int adjacency_matrix[MAX_NAV_AREAS][MAX_NAV_AREAS]; // Store adjacency matrix of areas - - // Depth-first Search - bool dfs_visited[MAX_NAV_AREAS]; // Visited areas - int dfs_len; // - int dfs_path[MAX_NAV_AREAS]; // Temp path - int dfs_paths[MAX_NAV_AREAS_PATHS][MAX_NAV_AREAS]; // Full path array [(512 * 32) * 4 bytes = 64k] - int dfs_path_count; // Total unique paths found - - - -} nav_area_t; -extern nav_area_t nav_area; - -void BOTLIB_MallocAreaNodes(void); -void BOTLIB_FreeAreaNodes(void); - -void BOTLIB_InitAreaNodes(void); // Init area nodes to INVALID -void BOTLIB_InitAreaConnections(void); // Init and make area connections -qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization); // Supports new and old pathing methods -qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node); // Checks if possible to traverse from current node to goal_node -void BOTLIB_GetAreaPath(edict_t* self, int goal_node); // Sets a path from area-to-area -qboolean BOTLIB_GetNextAreaNode(edict_t* self); -void BOTLIB_UpdateAllAreaEdges(void); // Stores all the nodes within an area that connect to an external area (edge nodes) -int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2); // Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) -void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination); // Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm -void BOTLIB_RandomizeAreaColors(void); // Randomize area colors -void BOTLIB_AutoArea(edict_t* self); -qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path); -qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization); -// Find all possible paths -- e - -// Node data for our linked list -typedef struct { - struct slint* next; // Next node - int node; // Node number - float cost; // Cost from previous node to this node - int parent_node; //rekkie -- the node we came from -} botlib_sll_nodes_t; - -// A double-ended singly linked list -// This allows data to be added to the front or back of the list. -typedef struct { - botlib_sll_nodes_t* head; // Front - botlib_sll_nodes_t* tail; // Back -} botlib_sll_t; -//} ltklist_t; - -// =========================================================================== -// botlib_nodes.c -// =========================================================================== -void BOTLIB_LinkNodesNearbyNode(edict_t* ent, int from); -qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path); - -void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node); -void BOTLIB_SetAllNodeNormals(void); - -// =========================================================================== -// botlib_spawn.c -// =========================================================================== -qboolean BOTLIB_SaveBotsFromPreviousMap(void); -qboolean BOTLIB_AddBotsFromPreviousMap(float percent); -void BOTLIB_RandomizeTeamNames(edict_t* bot); // Randomize team names in teamplay -void BOTLIB_RandomizeTeamSkins(edict_t* bot); // Randomize team skins in teamplay -int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender); // Pick a random skin and return the skin's gender -edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin); // Spawn a bot -void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team); -void BOTLIB_RemoveBot(char* name); // Remove bot by name or 'ALL' to remove all bots -void BOTLIB_RemoveTeamplayBot(int team); // Remove bot from team -void BOTLIB_ChangeBotTeam(int from_team, int to_team); // Change a bot [from team] ==> [to team] -void BOTLIB_CheckBotRules(void); // Adding/Removing/Auto-balance bots - -// =========================================================================== -// botlib_spawnpoints.c -// =========================================================================== - -// Custom spawnpoints -typedef struct { - qboolean inuse; // If the spawn point is in use, or has been deleted and is now free to replace - vec3_t origin; // Spawn point location - vec3_t angles; // Spawn point direction -} dc_sp_t; -extern dc_sp_t* dc_sp; -extern int dc_sp_count; // Total spawn points -extern qboolean dc_sp_edit; // If the spawn points have been made visible for editing -#define DC_SP_LIMIT 255 // Maximum spawn points -#define DC_SP_VERSION 1 // Version of spawn point file -void DC_Init_Spawnpoints(void); // Initialise spawn points -void DC_Free_Spawnpoints(void); // Free the spawn points -void DC_Add_Spawnpoint(edict_t* ent); // Add a spawn point at the player location -void DC_Remove_Spawnpoint(edict_t* self); // Remove a spawn point at the player location -void DC_Get_Map_Spawnpoints(void); // Find and add all the map spawn points to the dc_sp[] array -void BOTLIB_Show_Spawnpoints(void); // Find and display (as visible ents) all spawn points (map and custom) -void DC_Save_Spawnpoints(void); // Save the spawn points -void DC_Load_Spawnpoints(void); // Load the spawn points - - -// =========================================================================== -// botlib_weapons.c -// =========================================================================== -int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num); -qboolean BOTLIB_ChangeSpecialWeapon(edict_t* ent, int item_num); -void BOTLIB_ReadyWeapon(edict_t* self); -qboolean BOTLIB_ChooseWeapon(edict_t* self); -void BOTLIB_Reload(edict_t* self); -qboolean BOTLIB_SniperZoom(edict_t* self); -//rekkie -- collecting weapons, items, ammo -- s -qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum); -qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon); -qboolean BOTLIB_Need_Grenades(edict_t* self); -qboolean BOTLIB_Need_Knives(edict_t* self); -qboolean BOTLIB_Need_Dual_MK23(edict_t* self); -int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon); -int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon); -int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter); -int BOTLIB_GetEquipment(edict_t* self); -//rekkie -- collecting weapons, items, ammo -- e - - - - - +#ifndef _BOTLIB_H +#define _BOTLIB_H + +#define BOT_AAS_VERSION 1 + +#define BOT_NAV_VERSION 2 +#define BOT_NAV_VERSION_1 1 +#define BOT_NAV_VERSION_2 2 +#define BOT_NAV_VERSION_MAX BOT_NAV_VERSION_2 + +// Bot move states +#define BOT_MOVE_STATE_NONE 0 // Bot is booting up :-) +#define BOT_MOVE_STATE_NAV 1 // Getting a navigational path +//#define BOT_MOVE_STATE_NAV_NEXT 2 // Getting next navigational path (if any) +#define BOT_MOVE_STATE_MOVE 3 // Standard movement +#define BOT_MOVE_STATE_WANDER 4 // No navigation and no movement, try wandering around +#define BOT_MOVE_STATE_STAND 5 // Stand still and hold a position +#define BOT_MOVE_STATE_FLEE 6 // Running away from enemy force +#define BOT_MOVE_STATE_COVER 7 // Under fire, take cover + + +typedef struct bot_connections_s +{ + qboolean tried_adding_prev_bots; // Flag if we tried to add any bots from a previous map. Only called once per map. + qboolean auto_balance_bots; // If the spawn manager should try to auto balance bots on teams + qboolean scale_up; // If the manager should scale up the bots + qboolean scale_dn; // If the manager should scale down the bots + + int total_bots; + int total_humans; // Total humans in server + int total_humans_playing; // Total humans in server and playing + + int total_team1; // Bots & Humans + int total_team2; + int total_team3; + + int spec_bots; // TP and DM + int team1_bots; // TP and DM + int team2_bots; // TP and 3T + int team3_bots; // TP and 3T + + + int spec_humans; // TP and DM + int team1_humans; // TP and DM + int team2_humans; // TP and 3T + int team3_humans; // TP and 3T + + int desire_bots; // How many bots we desire + int desire_team1; + int desire_team2; + int desire_team3; +} bot_connections_t; +extern bot_connections_t bot_connections; + +//the bot input, will be converted to a usercmd_t +typedef struct bot_input_s +{ + float thinktime; //time since last output (in seconds) + vec3_t dir; //movement direction + float speed; //speed in the range [0, 400] + vec3_t viewangles; //the view angles + int actionflags; //one of the ACTION_? flags + int radioflags; //one of the aq2 radio flags (i.e. RADIO_REPORTIN) + int weapon; //weapon to use + vec3_t look_at; //location to look at + int look_at_time; //time spent looking at location +} bot_input_t; + + + + + +// =========================================================================== +// botlib_ai.c +// =========================================================================== +void BOTLIB_Init(edict_t* self); // Initializing... HAL9000 is online. +void BOTLIB_Think(edict_t* self); // Thinking... I'm sorry Rekkie, I can't do that. +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time); // Translates bot input to actual Q2 movement calls +qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount); // I see everything... in front of me +qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount); +qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount); +qboolean BOTLIB_CanMove(edict_t* self, int direction); // Can bot move in direction +void BOTLIB_TouchingLadder(edict_t* self); +float BOTLIB_ThrowingKnifePitch(edict_t* self); // Get pitch required to reach knife throwing target +void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type); +int BOTLIB_GetEquipment(edict_t* self); // Does bot need weapons, items, ammo? If so +//void BOTLIB_CheckCurrentWeapon(edict_t* self); //rekkie -- Check to make sure we're using a primary weapon where possible +void BOTLIB_GetWeaponsAndAmmo(edict_t* self); //rekkie -- Locate and pickup a primary weapon if we need one +short BOTLIB_FindVisibleAllies(edict_t* self); +qboolean BOTLIB_FindEnemy(edict_t* self); +void BOTLIB_PickLongRangeGoal(edict_t* self); + +typedef struct botlib_noises_s +{ + edict_t* owner[MAX_CLIENTS]; // The ent who made the noise + + // Noise timers + int self_time[MAX_CLIENTS]; + int weapon_time[MAX_CLIENTS]; + int impact_time[MAX_CLIENTS]; + + // Location of noise + vec3_t self_origin[MAX_CLIENTS]; + vec3_t weapon_origin[MAX_CLIENTS]; + vec3_t impact_origin[MAX_CLIENTS]; + +} botlib_noises_t; +extern botlib_noises_t botlib_noises; + +// Actionable flags +#define ACTION_NONE 0x00000000 // No action taken +#define ACTION_ATTACK 0x00000001 // Tap the attack button +#define ACTION_USE 0x00000002 +#define ACTION_RESPAWN 0x00000008 +#define ACTION_JUMP 0x00000010 // Small jumps - tapping the jump button +#define ACTION_MOVEUP 0x00000020 +#define ACTION_CROUCH 0x00000080 +#define ACTION_MOVEDOWN 0x00000100 +#define ACTION_MOVEFORWARD 0x00000200 +#define ACTION_MOVEBACK 0x00000800 +#define ACTION_MOVELEFT 0x00001000 +#define ACTION_MOVERIGHT 0x00002000 +#define ACTION_BOXJUMP 0x00008000 // Higher jumps +#define ACTION_TALK 0x00010000 +#define ACTION_GESTURE 0x00020000 +#define ACTION_WALK 0x00080000 +#define ACTION_AFFIRMATIVE 0x00100000 +#define ACTION_NEGATIVE 0x00200000 +#define ACTION_GETFLAG 0x00800000 +#define ACTION_GUARDBASE 0x01000000 +#define ACTION_PATROL 0x02000000 +#define ACTION_FOLLOWME 0x08000000 +#define ACTION_HOLDJUMP 0x10000000 // Hold the jump button and release when touching ground +#define ACTION_HOLDPOS 0x20000000 // Hold position +#define ACTION_JUMPPAD 0x40000000 + + +// =========================================================================== +// botlib_ctf.c +// =========================================================================== +qboolean BOTLIB_Carrying_Flag(edict_t* self); +int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance); +qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance); +qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance); +void BOTLIB_Update_Flags_Status(void); +int BOTLIB_CTF_Get_Flag_Node(edict_t* ent); +int BOTLIB_NearestFlag(edict_t* self); +float BOTLIB_DistanceToFlag(edict_t* self, int flagType); +void BOTLIB_CTF_Goals(edict_t* self); + +typedef struct ctf_status_s +{ + edict_t* flag1; // Red flag + edict_t* flag2; // Blue flag + int flag1_home_node; // The node where the red flag initially spawns + int flag2_home_node; // The node where the blue flag initially spawns + int flag1_curr_node; // The node where the red flag currently resides + int flag2_curr_node; // The node where the blue flag currently resides + qboolean flag1_is_home; // If the red flag is home + qboolean flag2_is_home; // If the blue flag is home + qboolean flag1_is_dropped; // If the red flag was dropped + qboolean flag2_is_dropped; // If the blue flag was dropped + qboolean flag1_is_carried; // If the red flag is being carried + qboolean flag2_is_carried; // If the blue flag is being carried + edict_t* player_has_flag1; // If any ent has the red flag + edict_t* player_has_flag2; // If any ent has the blue flag + float team1_carrier_dist_to_home; // How close the red team carrier is to the home red flag node + float team2_carrier_dist_to_home; // How close the blue team carrier is to the home blue flag node +} ctf_status_t; +extern ctf_status_t bot_ctf_status; + +// Get flag, retrieve flag, intercept flag carrier, etc. +typedef enum +{ + BOT_CTF_STATE_NONE, // No state + BOT_CTF_STATE_GET_ENEMY_FLAG, // Get enemy flag sitting in its home location + BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG, // Get dropped enemy flag + BOT_CTF_STATE_CAPTURE_ENEMY_FLAG, // Got flag, now capture it + BOT_CTF_STATE_ATTACK_ENEMY_CARRIER, // Enemy has our flag, go after them + BOT_CTF_STATE_RETURN_TEAM_FLAG, // Our team flag is on the ground, go return it + BOT_CTF_STATE_COVER_FLAG_CARRIER, // We have the enemy flag, cover the bot who has it + BOT_CTF_STATE_FORCE_MOVE_TO_FLAG, // Special case. Bot is forced to move in direction of flag. + BOT_CTF_STATE_GET_DROPPED_ITEMS // Gets dropped weapons, items, etc from ground + +} bot_ctf_state_t; + +// =========================================================================== +// botlib_cmd.c +// =========================================================================== +qboolean BOTLIB_SV_Cmds(void); // Server commands +qboolean BOTLIB_Commands(edict_t* ent); // Client commands + + +// =========================================================================== +// botlib_communication.c +// =========================================================================== +void BOTLIB_Wave(edict_t* ent, int type); +void BOTLIB_PrecacheRadioSounds(void); +void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); +void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); + +// Wave (gesture) types +#define WAVE_FLIPOFF 1 +#define WAVE_SALUTE 2 +#define WAVE_TAUNT 3 +#define WAVE_WAVE 4 +#define WAVE_POINT 5 + +// Radio flags +#define RADIO_1 0x00000001 +#define RADIO_2 0x00000002 +#define RADIO_3 0x00000003 +#define RADIO_4 0x00000004 +#define RADIO_5 0x00000005 +#define RADIO_6 0x00000006 +#define RADIO_7 0x00000007 +#define RADIO_8 0x00000008 +#define RADIO_9 0x00000009 +#define RADIO_10 0x00000010 +#define RADIO_BACK 0x00000011 +#define RADIO_COVER 0x00000012 +#define RADIO_DOWN 0x00000013 +#define RADIO_ENEMY_DOWN 0x00000014 +#define RADIO_ENEMY_SIGHTED 0x00000015 +#define RADIO_FORWARD 0x00000016 +#define RADIO_GO 0x00000017 +#define RADIO_IM_HIT 0x00000018 +#define RADIO_LEFT 0x00000019 +#define RADIO_REPORTIN 0x00000020 +#define RADIO_RIGHT 0x00000021 +#define RADIO_TAKING_FIRE 0x00000022 +#define RADIO_TEAMMATE_DOWN 0x00000023 +#define RADIO_TEAM_REPORT_IN 0x00000024 +#define RADIO_UP 0x00000025 + +// Each of the possible radio messages and their length +typedef struct bot_radio_msg_s +{ + char* msg; // the msg name + int length; // length in server frames (ie tenths of a second), rounded up + int sndIndex; +} bot_radio_msg_t; + +static bot_radio_msg_t bot_male_radio_msgs[] = { + {"1", 6, 0}, + {"2", 6, 0}, + {"3", 8, 0}, + {"4", 7, 0}, + {"5", 8, 0}, + {"6", 9, 0}, + {"7", 8, 0}, + {"8", 7, 0}, + {"9", 7, 0}, + {"10", 6, 0}, + {"back", 6, 0}, + {"cover", 7, 0}, + {"down", 13, 0}, + {"enemyd", 10, 0}, + {"enemys", 9, 0}, + {"forward", 6, 0}, + {"go", 6, 0}, + {"im_hit", 7, 0}, + {"left", 7, 0}, + {"reportin", 9, 0}, + {"right", 6, 0}, + {"taking_f", 22, 0}, + {"teamdown", 13, 0}, + {"treport", 12, 0}, + {"up", 4, 0} +}; + +static bot_radio_msg_t bot_female_radio_msgs[] = { + {"1", 5, 0}, + {"2", 5, 0}, + {"3", 5, 0}, + {"4", 5, 0}, + {"5", 5, 0}, + {"6", 8, 0}, + {"7", 7, 0}, + {"8", 5, 0}, + {"9", 5, 0}, + {"10", 5, 0}, + {"back", 6, 0}, + {"cover", 5, 0}, + {"down", 6, 0}, + {"enemyd", 9, 0}, + {"enemys", 9, 0}, + {"forward", 8, 0}, + {"go", 6, 0}, + {"im_hit", 7, 0}, + {"left", 8, 0}, + {"reportin", 9, 0}, + {"right", 5, 0}, + {"taking_f", 22, 0}, + {"teamdown", 10, 0}, + {"treport", 12, 0}, + {"up", 6, 0} +}; +static const int bot_numMaleSnds = (sizeof(bot_male_radio_msgs) / sizeof(bot_male_radio_msgs[0])); +static const int bot_numFemaleSnds = (sizeof(bot_female_radio_msgs) / sizeof(bot_female_radio_msgs[0])); + + +// =========================================================================== +// botlib_items.c +// =========================================================================== + + +// =========================================================================== +// botlib_math.c +// =========================================================================== +qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon); +bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs); +qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other); + +// =========================================================================== +// botlib_movement.c +// =========================================================================== +qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint); +void BOTLIB_Look(edict_t* self, usercmd_t* ucmd); + + +// =========================================================================== +// botlib_nav.c +// =========================================================================== +// Find all possible paths -- s + +#define MAX_NAV_AREAS 32 +#define MAX_NAV_AREAS_EDGES 64 +#define MAX_NAV_AREAS_PATHS 512 //512 +#define MAX_NAV_AREAS_NODES 4096 +extern int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] +extern int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] + + + +typedef struct { + + // Areas + int total_areas; // Total number of areas + //int area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area + int** area_nodes; + + int adjacency_matrix[MAX_NAV_AREAS][MAX_NAV_AREAS]; // Store adjacency matrix of areas + + // Depth-first Search + bool dfs_visited[MAX_NAV_AREAS]; // Visited areas + int dfs_len; // + int dfs_path[MAX_NAV_AREAS]; // Temp path + int dfs_paths[MAX_NAV_AREAS_PATHS][MAX_NAV_AREAS]; // Full path array [(512 * 32) * 4 bytes = 64k] + int dfs_path_count; // Total unique paths found + + + +} nav_area_t; +extern nav_area_t nav_area; + +void BOTLIB_MallocAreaNodes(void); +void BOTLIB_FreeAreaNodes(void); + +void BOTLIB_InitAreaNodes(void); // Init area nodes to INVALID +void BOTLIB_InitAreaConnections(void); // Init and make area connections +qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization); // Supports new and old pathing methods +qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node); // Checks if possible to traverse from current node to goal_node +void BOTLIB_GetAreaPath(edict_t* self, int goal_node); // Sets a path from area-to-area +qboolean BOTLIB_GetNextAreaNode(edict_t* self); +void BOTLIB_UpdateAllAreaEdges(void); // Stores all the nodes within an area that connect to an external area (edge nodes) +int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2); // Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) +void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination); // Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm +void BOTLIB_RandomizeAreaColors(void); // Randomize area colors +void BOTLIB_AutoArea(edict_t* self); +qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path); +qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization); +// Find all possible paths -- e + +// Node data for our linked list +typedef struct { + struct slint* next; // Next node + int node; // Node number + float cost; // Cost from previous node to this node + int parent_node; //rekkie -- the node we came from +} botlib_sll_nodes_t; + +// A double-ended singly linked list +// This allows data to be added to the front or back of the list. +typedef struct { + botlib_sll_nodes_t* head; // Front + botlib_sll_nodes_t* tail; // Back +} botlib_sll_t; +//} ltklist_t; + +// =========================================================================== +// botlib_nodes.c +// =========================================================================== +void BOTLIB_LinkNodesNearbyNode(edict_t* ent, int from); +qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path); + +void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node); +void BOTLIB_SetAllNodeNormals(void); + +// =========================================================================== +// botlib_spawn.c +// =========================================================================== +qboolean BOTLIB_SaveBotsFromPreviousMap(void); +qboolean BOTLIB_AddBotsFromPreviousMap(float percent); +void BOTLIB_RandomizeTeamNames(edict_t* bot); // Randomize team names in teamplay +void BOTLIB_RandomizeTeamSkins(edict_t* bot); // Randomize team skins in teamplay +int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender); // Pick a random skin and return the skin's gender +edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin); // Spawn a bot +void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team); +void BOTLIB_RemoveBot(char* name); // Remove bot by name or 'ALL' to remove all bots +void BOTLIB_RemoveTeamplayBot(int team); // Remove bot from team +void BOTLIB_ChangeBotTeam(int from_team, int to_team); // Change a bot [from team] ==> [to team] +void BOTLIB_CheckBotRules(void); // Adding/Removing/Auto-balance bots + +// =========================================================================== +// botlib_spawnpoints.c +// =========================================================================== + +// Custom spawnpoints +typedef struct { + qboolean inuse; // If the spawn point is in use, or has been deleted and is now free to replace + vec3_t origin; // Spawn point location + vec3_t angles; // Spawn point direction +} dc_sp_t; +extern dc_sp_t* dc_sp; +extern int dc_sp_count; // Total spawn points +extern qboolean dc_sp_edit; // If the spawn points have been made visible for editing +#define DC_SP_LIMIT 255 // Maximum spawn points +#define DC_SP_VERSION 1 // Version of spawn point file +void DC_Init_Spawnpoints(void); // Initialise spawn points +void DC_Free_Spawnpoints(void); // Free the spawn points +void DC_Add_Spawnpoint(edict_t* ent); // Add a spawn point at the player location +void DC_Remove_Spawnpoint(edict_t* self); // Remove a spawn point at the player location +void DC_Get_Map_Spawnpoints(void); // Find and add all the map spawn points to the dc_sp[] array +void BOTLIB_Show_Spawnpoints(void); // Find and display (as visible ents) all spawn points (map and custom) +void DC_Save_Spawnpoints(void); // Save the spawn points +void DC_Load_Spawnpoints(void); // Load the spawn points + + +// =========================================================================== +// botlib_weapons.c +// =========================================================================== +int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num); +qboolean BOTLIB_ChangeSpecialWeapon(edict_t* ent, int item_num); +void BOTLIB_ReadyWeapon(edict_t* self); +qboolean BOTLIB_ChooseWeapon(edict_t* self); +void BOTLIB_Reload(edict_t* self); +qboolean BOTLIB_SniperZoom(edict_t* self); +//rekkie -- collecting weapons, items, ammo -- s +qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum); +qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon); +qboolean BOTLIB_Need_Grenades(edict_t* self); +qboolean BOTLIB_Need_Knives(edict_t* self); +qboolean BOTLIB_Need_Dual_MK23(edict_t* self); +int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon); +int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon); +int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter); +int BOTLIB_GetEquipment(edict_t* self); +//rekkie -- collecting weapons, items, ammo -- e + + + + + #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 95cb09d6d..36b4ff9d0 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -1,1217 +1,1217 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -void BOTLIB_Init(edict_t* self) -{ - self->classname = "bot"; - self->suicide_timeout = level.framenum + 15.0 * HZ; - - self->nav = NULL; //rekkie -- surface data - - self->enemy = NULL; - self->movetarget = NULL; - self->last_jumppad_node = INVALID; - self->bot_strafe = 0; - self->show_node_links = INVALID; - self->show_node_links_time = 0; - - //RiEvEr - new node pathing system - memset(&(self->pathList), 0, sizeof(self->pathList)); - self->pathList.head = self->pathList.tail = NULL; - //R - - self->client->resp.radio.gender = (self->client->pers.gender == GENDER_FEMALE) ? 1 : 0; - - - // Save previous data - float prev_skill = self->bot.skill; - - memset(&self->bot, 0, sizeof(bot_t)); - - // Restore previous data - self->bot.skill = prev_skill; - - // Ping - // Set the average ping this bot will see - int rng_ping_range = rand() % 5; - if (rng_ping_range == 0) - self->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard - else if (rng_ping_range <= 3) - self->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger - else - self->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - - - - - // Enemies - self->bot.old_enemy = NULL; - self->bot.enemy_in_xhair = false; - self->bot.enemy_dist = 0; - self->bot.enemy_height_diff = 0; - self->bot.enemy_seen_time = 0; - self->bot.enemy_chase_time = 0; - self->bot.reaction_time = bot_reaction->value; - - //memset(&self->bot.noises, 0, sizeof(int) * MAX_CLIENTS); - //memset(&self->bot.noise_time, 0, sizeof(int) * MAX_CLIENTS); - - // Nav - self->bot.state = BOT_MOVE_STATE_NONE; // Init bot navigation - self->bot.prev_node = INVALID; - self->bot.current_node = INVALID; - self->bot.next_node = INVALID; - self->bot.goal_node = INVALID; - self->bot.stuck_node = INVALID; - self->bot.walknode.touched_node = INVALID; - self->bot.walknode.highlighted_node = INVALID; - self->bot.walknode.prev_highlighted_node = INVALID; - - - // Weapons - self->bot.last_sniper_zoom_time = 0; - self->bot.last_weapon_change_time = 0; - - - if (teamplay->value) // Reset bot radio at the start of each round - { - // Radio - BOTLIB_PrecacheRadioSounds(); - self->bot.radioTeamReportedIn = false; - self->bot.radioLastSawAllyTime = 0; - self->bot.radioTeamReportedInDelay = 75 + (rand() % 101); // Add a random delay between 75 and 175 ricks - self->bot.radioReportIn = false; - self->bot.radioReportInTime = 0; - self->bot.radioReportInDelay = 30 + (rand() % 60); // Add a random delay between 30 and 90 ticks - self->bot.radioBandaging = false; - self->bot.radioLastHumanMsg[0] = '\0'; - self->bot.radioReportKills = (rand() % 2); // RNG if this bot will report its kills or not - - // Enemies - if (ff_afterround->value) - self->bot.ff_allies_after_rnd = ((rand() % 2) == 0); // 50% chance the bot will attack allies after a TP round ends - - // Update skin to match TP skins - const char* s = Info_ValueForKey(self->client->pers.userinfo, "skin"); - AssignSkin(self, s, false /* nickChanged */); - } - -} - -//rekkie -- Quake3 -- s -//============== -// BOTLIB_BotInputToUserCommand -// Translates bot input to actual Q2 movement calls -//============== -void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time) -{ - vec3_t angles, forward, right; - float f, r, u, m; - - //clear the whole structure - //memset(ucmd, 0, sizeof(usercmd_t)); - - //the duration for the user command in milli seconds - //ucmd->serverTime = time; - //ucmd->weapon = bi->weapon; - - - - //set the buttons - if (bi->actionflags & ACTION_RESPAWN) ucmd->buttons = BUTTON_ATTACK; - if (bi->actionflags & ACTION_ATTACK) ucmd->buttons |= BUTTON_ATTACK; - bi->actionflags &= ~ACTION_ATTACK; - - /* - if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK; - if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE; - if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE; - if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_WALKING; - if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE; - if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE; - if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG; - if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE; - if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL; - if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME; - */ - - - - // Radio wave animations - // Enemy interaction - if (bi->radioflags & RADIO_ENEMY_DOWN) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_ENEMY_SIGHTED) BOTLIB_Wave(ent, WAVE_FLIPOFF); - // Assistance required - else if (bi->radioflags & RADIO_TAKING_FIRE) BOTLIB_Wave(ent, WAVE_FLIPOFF); - else if (bi->radioflags & RADIO_IM_HIT) BOTLIB_Wave(ent, WAVE_TAUNT); - else if (bi->radioflags & RADIO_COVER) BOTLIB_Wave(ent, WAVE_TAUNT); - // Team reporting - else if (bi->radioflags & RADIO_TEAM_REPORT_IN) BOTLIB_Wave(ent, WAVE_WAVE); - else if (bi->radioflags & RADIO_REPORTIN) BOTLIB_Wave(ent, WAVE_SALUTE); - else if (bi->radioflags & RADIO_TEAMMATE_DOWN) BOTLIB_Wave(ent, WAVE_SALUTE); - // Directions - else if (bi->radioflags & RADIO_BACK) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_FORWARD) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_UP) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_DOWN) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_LEFT) BOTLIB_Wave(ent, WAVE_POINT); - else if (bi->radioflags & RADIO_RIGHT) BOTLIB_Wave(ent, WAVE_POINT); - - - - - - - - ent->bot.bi.radioflags = 0; // Clear radio flags - - - // Adjust throwing pitch if using throwing knives - //if (ent->client->weapon == FindItemByNum(KNIFE_NUM) && ent->client->pers.knife_mode == 1) // Throwing knives - // bi->viewangles[PITCH] = BOTLIB_ThrowingKnifePitch(ent); - - - //set the view angles - //NOTE: the ucmd->angles are the angles WITHOUT the delta angles - ucmd->angles[PITCH] = ANGLE2SHORT(bi->viewangles[PITCH]); - ucmd->angles[YAW] = ANGLE2SHORT(bi->viewangles[YAW]); - ucmd->angles[ROLL] = ANGLE2SHORT(bi->viewangles[ROLL]); - /* - //subtract the delta angles - for (j = 0; j < 3; j++) { - temp = ucmd->angles[j] - delta_angles[j]; - ucmd->angles[j] = temp; - } - */ - - - if (bi->dir[2]) - angles[PITCH] = bi->viewangles[PITCH]; - else - angles[PITCH] = 0; - angles[YAW] = bi->viewangles[YAW]; - angles[ROLL] = 0; - - // No movement - //if (VectorEmpty(bi->dir)) return; - if (bi->actionflags & ACTION_HOLDPOS) - { - // Kill velocity - bi->actionflags = 0; - bi->speed = 0; - ent->velocity[0] *= 0.9; - ent->velocity[1] *= 0.9; - ent->velocity[2] *= 0.9; - - return; - } - else - { - //NOTE: movement is relative to the REAL view angles - //get the horizontal forward and right vector - AngleVectors(angles, forward, right, NULL); - //bot input speed is in the range [0, 400] - SPEED_RUN - //bi->speed = bi->speed * 127 / SPEED_RUN; - - // Cap max speed - if (bi->speed > SPEED_RUN) bi->speed = SPEED_RUN; - else if (bi->speed < -SPEED_RUN) bi->speed = -SPEED_RUN; - - //set the view independent movement - f = DotProduct(forward, bi->dir); - r = DotProduct(right, bi->dir); - u = fabs(forward[2]) * bi->dir[2]; - m = fabs(f); - - if (fabs(r) > m) { - m = fabs(r); - } - - if (fabs(u) > m) { - m = fabs(u); - } - - if (m > 0) { - f *= bi->speed / m; - r *= bi->speed / m; - u *= bi->speed / m; - } - - if (ent->client->leg_damage == 0) // No jumping if legs are broken - u = 0; - - ucmd->forwardmove = f; - ucmd->sidemove = r; - ucmd->upmove = u; - } - - if (bi->actionflags & ACTION_MOVEFORWARD) ucmd->forwardmove = SPEED_RUN; // 127 - if (bi->actionflags & ACTION_MOVEBACK) ucmd->forwardmove = -SPEED_RUN; // -127 - if (bi->actionflags & ACTION_MOVELEFT) ucmd->sidemove = -SPEED_RUN; // -127 - if (bi->actionflags & ACTION_MOVERIGHT) ucmd->sidemove = SPEED_RUN; // 127 - - - //jump/moveup -//if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) ucmd->upmove = SPEED_RUN; // 127 - - if ((bi->actionflags & ACTION_HOLDJUMP) && (ent->groundentity)) // Stop holding jump if on ground - { - bi->actionflags &= ~ACTION_HOLDJUMP; - } - if ((bi->actionflags & ACTION_HOLDJUMP)) // Hold jump if in air - { - if (ent->groundentity == NULL) - ucmd->upmove = SPEED_RUN; - else - bi->actionflags &= ~ACTION_HOLDJUMP; - } - if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage - { - // Slow down - //ucmd->forwardmove = 0; - //ucmd->sidemove = 0; - //ucmd->upmove = 0; - - //bi->actionflags &= ~ACTION_JUMP; - //bi->actionflags |= ACTION_HOLDJUMP; - ucmd->upmove = SPEED_RUN; - if (ent->groundentity) // Give a slight boost if on ground - ent->velocity[2] += 50; - - ucmd->forwardmove = SPEED_RUN; - } - if (bi->actionflags & ACTION_BOXJUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage - { - // Slow down - //ucmd->forwardmove = 0; - //ucmd->sidemove = 0; - //ucmd->upmove = 0; - - //bi->actionflags &= ~ACTION_JUMP; - //bi->actionflags |= ACTION_HOLDJUMP; - ucmd->upmove = SPEED_RUN; - if (ent->groundentity) // Give a slight boost if on ground - ent->velocity[2] += 325; // 400 - - ucmd->forwardmove = SPEED_RUN; - } - if (bi->actionflags & ACTION_JUMPPAD && ent->client->leg_damage == 0) // Initiate jump if no leg damage - { - // Check the bot is touching a jump node before attempting to jump - int nodelist[MAX_NODELIST]; - int nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); - if (nodes_touched && ent->bot.current_node != INVALID && ent->bot.next_node != INVALID) - { - for (int i = 0; i < nodes_touched; i++) - { - //Com_Printf("%s %s touched node %d\n", __func__, ent->client->pers.netname, nodelist[i]); - //if (nodes[nodelist[i]].type & NODE_JUMPPAD) - if (nodes[nodelist[i]].nodenum == ent->bot.current_node) - { - //Com_Printf("%s %s touched jump node %d\n", __func__, ent->client->pers.netname, nodelist[i]); - // Attempt jump - //BOTLIB_Jump_Takeoff(ent, NULL, nodes[ent->bot.next_node].origin, ent->viewheight, ent->velocity); - BOTLIB_DoParabolaJump(ent, nodes[ent->bot.next_node].origin); - bi->actionflags &= ~ACTION_JUMPPAD; - break; - } - } - } - } - //crouch/movedown - if (bi->actionflags & ACTION_CROUCH) ucmd->upmove = -SPEED_RUN; // -127 - - //ladder - movedown, moveup - if (bi->actionflags & ACTION_MOVEDOWN) - { - ucmd->forwardmove = 50; // jump/move up - ucmd->upmove = -SPEED_WALK; // crouch - - // Slow turning speed - ent->velocity[0] *= 0.25; - ent->velocity[1] *= 0.25; - - ent->velocity[2] = -100; // Slow decent - - //Com_Printf("%s %s ladder down\n", __func__, ent->client->pers.netname); - - //vec3_t dist; - //VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); - //VectorNormalize(dist); - //VectorScale(dist, 20, ent->velocity); // Apply it - } - if (bi->actionflags & ACTION_MOVEUP) - { - /* - // Check we're looking in the direction of a ladder - qboolean on_ladder = false; - { - float yaw_rad = 0; - vec3_t fwd = { 0 }, end = { 0 }; - trace_t tr; - - yaw_rad = DEG2RAD(ent->s.angles[YAW]); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - VectorMA(ent->s.origin, 16, fwd, end); - - tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_PLAYERSOLID); - - on_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - - if (on_ladder) - { - ent->velocity[2] += 10; - ucmd->upmove = SPEED_WALK; - Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); - } - */ - - //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); - - ucmd->forwardmove = SPEED_RUN; // jump/move up - ucmd->upmove = SPEED_RUN; // jump/move up - - // Slow turning speed - ent->velocity[0] *= 0.25; - ent->velocity[1] *= 0.25; - - // Go up - ent->velocity[2] = 200; // Slow upward movement - - /* - // Check if touching ladder with an overly large hitbox - if (0) - { - vec3_t min = { -32, -32, -60 }; - vec3_t max = { 32, 32, 60 }; - trace_t tr = gi.trace(ent->s.origin, min, max, ent->s.origin, ent, MASK_PLAYERSOLID); - if ((tr.contents & CONTENTS_LADDER) || tr.contents & CONTENTS_SOLID) - { - ent->velocity[2] = 200; // Slow upward movement - - //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); - return; - } - } - */ - - //ent->movetype == MOVETYPE_FLY; - - /* - vec3_t dist; - VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); - VectorNormalize(dist); - VectorScale(dist, 20, ent->velocity); // Apply it - - for (int i = 0; i < 3; i++) - ent->velocity[i] = 200 * bi->dir[i]; - */ - } - - - // Air control - //if ((bi->actionflags & ACTION_JUMP) || (bi->actionflags & ACTION_HOLDJUMP)) - //if (ent->groundentity == NULL) - if (bi->actionflags & ACTION_JUMP) - //if (0) - { - float wishspeed, addspeed, accelspeed, currentspeed; - - wishspeed = VectorNormalize(bi->dir); - if (wishspeed > SPEED_RUN) wishspeed = SPEED_RUN; - - currentspeed = DotProduct(ent->velocity, bi->dir); - - addspeed = wishspeed - currentspeed; - addspeed *= 10.1; - if (addspeed <= 0) { - return; - } - - //accelspeed = ent->accel * FRAMETIME * wishspeed; - accelspeed = SPEED_RUN * FRAMETIME * wishspeed; - if (accelspeed > addspeed) { - accelspeed = addspeed; - } - - - /* - float z = accelspeed * bi->dir[2]; - if (z < 0) - { - // Slow down velocity - //Com_Printf("%s %s velocity %f\n", __func__, ent->client->pers.netname, z); - ent->velocity[2] += z; - } - */ - - ent->velocity[0] += accelspeed * bi->dir[0]; - ent->velocity[1] += accelspeed * bi->dir[1]; - ent->velocity[2] += accelspeed * bi->dir[2]; - - //for (i = 0; i < 3; i++) { - // ent->velocity[i] += accelspeed * bi->dir[i]; - // //ent->velocity[i] = accelspeed * bi->dir[i]; - //} - //Com_Printf("%s %s velocity[%f %f %f]\n", __func__, ent->client->pers.netname, ent->velocity[0], ent->velocity[1], ent->velocity[2]); - } - - //bi->actionflags = 0; // Clear action flags - ent->bot.bi.actionflags = 0; -} - -// Automatic adjustment of bot skills based on their current score compared to all other bots -/* -typedef struct { - int playernum; - int score; -} PlayerScores; -// Comparison function used by qsort() -int BOTLIB_ScoreCompare(const void* a, const void* b) -{ - const PlayerScores* p1 = (PlayerScores*)a; - const PlayerScores* p2 = (PlayerScores*)b; - return p2->score - p1->score; // Sort ascendingly by score -} -*/ -int BOTLIB_AutoAdjustSkill(edict_t * self) -{ - int highest_score = 0; // Highest player score - int score_diff = 0; // Difference between player and best player's score - float score_percent_diff = 1.0; // Difference in percent between player and best player's score - - //Init the variable bot skill - if (self->bot.skill < 1) - self->bot.skill = bot_skill->value; - - if (self->client == NULL) - return 0; - - // Get highest score - for (int i = 0; i <= num_players; i++) - { - if (players[i] == NULL || players[i]->client == NULL) continue; - - if (players[i]->client->resp.score > highest_score) - highest_score = players[i]->client->resp.score; - } - - // Find the difference between highest score and our score (the gap between two scores) - if (self->client->resp.score < highest_score) - { - score_diff = highest_score - self->client->resp.score; // Get score gap - - if (score_diff > (int)bot_skill_threshold->value) // If the score gap is greater than threshold - { - // Get the diff in percent: 1.0 is equal, 0.5 is half the score of the best player. Higher is better. - // > 0.75 is normal - // > 0.5 is okay - // < 0.5 is poor - score_percent_diff = (float)self->client->resp.score / (float)highest_score; - - if (score_percent_diff >= 0.75) - { - self->bot.skill = bot_skill->value; - //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); - return 0; // Normal - } - else if (score_percent_diff >= 0.5) - { - self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower - - if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill - self->bot.skill = bot_skill->value; // Scale skill back down to base level - - //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); - - return 1; // Okay - } - else if (score_percent_diff < 0.5) - { - self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster - - if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill - self->bot.skill = bot_skill->value; // Scale skill back down to base level - - //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); - - return 2; // Poor - } - } - } - - self->bot.skill = bot_skill->value; - //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); - return 0; // Normal - - /* - PlayerScores player_scores[MAX_CLIENTS]; // All player scores - int player_id = 0; // Our player id - - // Check if players have completed at least the first round. We only want to adjust skill after players have an established score. - qboolean completed_a_round = false; - for (int i = TEAM1; i <= teamCount; i++) - { - if (teams[i].score > 0) - { - completed_a_round = true; - break; - } - } - if (completed_a_round == false) // Ignore the first round - return 0; - - // Init player scores - for (int i = 0; i <= num_players; i++) - { - if (players[i] == self) // If this is us - player_id = i; // Store our own id - - player_scores[i].playernum = i; // Store the player id - - // Store the score - if (players[i] == NULL || players[i]->client == NULL) - player_scores[i].score = 0; // Zero invalid clients - else - player_scores[i].score = players[i]->client->resp.score; // Valid client score - - if (player_scores[i].score > highest_score) - highest_score = player_scores[i].score; - } - - // Sort scores from highest to lowest - qsort(player_scores, num_players, sizeof(PlayerScores), BOTLIB_ScoreCompare); - - // Find where we rank - int rank = 0; - for (int i = 0; i <= num_players; i++) - { - // Only rank against player's with a score - //if (player_scores[i].score > 0) - rank++; - - // Break when we find ourself - if (player_id == player_scores[i].playernum) - break; - } - - float percent = ((float)rank / (float)num_players); - - //Com_Printf("%s %s rank[%d of %d] = %f score[%d]\n", __func__, self->client->pers.netname, rank, num_players, percent, self->client->resp.score); - - // If bot score is in the bottom half (50% or greater) adjust its skill so it can aim to be in the top 10% - if (percent >= 0.75) // Higher is worse - { - self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster - - if (self->bot.skill > MAX_BOTSKILL) // Max skill - self->bot.skill = MAX_BOTSKILL; - - //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); - } - else if (percent >= 0.5) // Higher is worse - { - self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower - - if (self->bot.skill > MAX_BOTSKILL) // Max skill - self->bot.skill = MAX_BOTSKILL; - - //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); - } - else // Return skill back to base level - { - self->bot.skill = bot_skill->value; - //Com_Printf("%s %s adjusting bot skill [-] [%d]\n", __func__, self->client->pers.netname, (int)bot_skill->value); - } - - // Return skill bracket: low, med, high. Used to recommend a weapon and item - if (percent < 0.25) - return 0; // Low - else if (percent < 0.5) - return 1; // Med - - return 2; // High - */ -} - -/////////////////////////////////////////////////////////////////////// -// Main Think function for bot -/////////////////////////////////////////////////////////////////////// -void BOTLIB_Think(edict_t* self) -{ - usercmd_t ucmd; - - //rekkie -- Fake Bot Client -- s - if (level.framenum % 10 == 0) // Update bot info every 10th frame - { - // Set self->client->ping to random bot->bot_baseline_ping, then vary by +-3 (ping jitter) - int ping_jitter = (rand() % 7); // Positive jitter - if (ping_jitter && (rand() % 1) == 0) - ping_jitter -= (ping_jitter * 2); // Negative jitter - - //Com_Printf("%s %s [%d + %d]\n", __func__, self->client->pers.netname, self->bot.bot_baseline_ping, ping_jitter); - - self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; - if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping - self->client->ping = self->bot.bot_ping; - gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); - } - //rekkie -- Fake Bot Client -- e - - // Set up client movement - VectorCopy(self->client->ps.viewangles, self->s.angles); - VectorSet(self->client->ps.pmove.delta_angles, 0, 0, 0); - memset(&ucmd, 0, sizeof(ucmd)); - - // Make bots end intermission and enter the next map (especially useful to change maps when no 'human' players are around) - if (level.intermission_framenum) - { - ucmd.buttons = BUTTON_ANY; // This button is pressed to end the intermission - goto end_think; - } - - // Stop trying to think if the bot can't respawn. - if (!IS_ALIVE(self) && ((gameSettings & GS_ROUNDBASED) || (self->client->respawn_framenum > level.framenum))) - { - goto end_think; - } - - // Force respawn - if (self->deadflag == DEAD_DEAD) - { - self->client->buttons = 0; - ucmd.buttons = BUTTON_ATTACK; - } - - // Don't execute thinking code if not alive - if (self->deadflag != DEAD_NO || self->health <= 0) - { - goto end_think; - } - - // Make the bot brain dead for a period of time - if (self->bot.pause_time) - { - self->bot.pause_time--; // Reduce timer to zero - self->bot.bi.speed = 0; - goto end_think; // Skip bot logic - } - - if (level.framenum < 75) // Wait for a little before processing AI on a new map - goto end_think; // Skip bot logic - - if (ctf->value) // CTF Goals - { - BOTLIB_CTF_Goals(self); - } - else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) - { - /* - if (rand() % 2) // Area 1 - { - BOTLIB_CanGotoNode(self, nodes[64].nodenum, 0); - } - else // Area 9 - { - BOTLIB_CanGotoNode(self, nodes[3344].nodenum, 0); - } - */ - - /* - //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (curr_node != 2505) - if (1 && rand() % 2) // Area 0 - { - if (rand() % 2) - BOTLIB_CanGotoNode(self, nodes[2505].nodenum, 0); - else - BOTLIB_CanGotoNode(self, nodes[3346].nodenum, 0); - } - else // Area 1 - { - if (rand() % 2) - BOTLIB_CanGotoNode(self, nodes[999].nodenum, 0); - else - BOTLIB_CanGotoNode(self, nodes[38].nodenum, 0); - } - */ - - //else // DM and TP goals - { - //self->bot.pause_time = 100; - //Com_Printf("\n%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() -------------------- \n", __func__, self->client->pers.netname, level.framenum); - - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname, level.framenum); - - //nav_area.total_areas = 0; // Turn off area based nav - BOTLIB_PickLongRangeGoal(self); - - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() curr[%d] goal[%d] -------------------- \n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node, self->bot.goal_node); - } - } - /* - if (self->bot.state == BOT_MOVE_STATE_NAV_NEXT) // If map has nodes grouped into areas - { - //self->bot.pause_time = 100; - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV_NEXT BOTLIB_GetNextAreaNode() -------------------- \n", __func__, self->client->pers.netname, level.framenum); - BOTLIB_GetNextAreaNode(self); // Get next area - } - */ - - - - // Kill the bot if completely stuck somewhere - //if(VectorLength(self->velocity) > 37) // - // self->suicide_timeout = level.framenum + 10.0 * HZ; - //if( self->suicide_timeout < level.framenum && !teamplay->value ) - // killPlayer( self, true ); - - // Kill the bot if they've not moved between nodes in a timely manner, stuck! - if (self->bot.node_travel_time > 120) - killPlayer(self, true); - - // Find any short range goal - //ACEAI_PickShortRangeGoal(self); - - //BOTLIB_GetWeaponsAndAmmo(self); //rekkie -- Locate and pickup a primary weapon if we need one - - - if (0) // Always follow player -- && (level.framenum % HZ == 0)) - { - for (int p = 0; p < num_players; p++) // Cycle based on how many players we find - { - if (players[p]->is_bot == false && players[p]->bot.current_node != 0 && players[p]->solid == SOLID_BBOX && players[p]->deadflag == DEAD_NO) - { - // Get friendly node - int n = players[p]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() - if (n == INVALID) - continue; - - if (self->bot.goal_node != n) - { - Com_Printf("%s %s following %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[p]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); - //BOTLIB_SetGoal(self, n); - } - } - } - } - - - - - //if (1) - { - BOTLIB_FindVisibleAllies(self); // Find visible allies - BOTLIB_Radio(self, &ucmd); - self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies - BOTLIB_Reload(self); // Reload the weapon if needed - - if (self->enemy) - { - // Chase after the new enemy - if (self->bot.enemy_chase_time < level.framenum && self->enemy->bot.current_node != self->bot.goal_node) - { - qboolean chase_enemy = false; - if ((FindItem(HC_NAME) == self->client->weapon || - FindItem(M3_NAME) == self->client->weapon || - FindItem(DUAL_NAME) == self->client->weapon || - FindItem(KNIFE_NAME) == self->client->weapon) - && self->bot.enemy_dist > 200) - { - chase_enemy = true; - self->bot.enemy_chase_time = level.framenum + 1 * HZ; // Delay next call - } - else if (FindItem(SNIPER_NAME) == self->client->weapon)// && self->bot.enemy_dist > 1500) - { - chase_enemy = false; - self->bot.enemy_chase_time = level.framenum + (((rand() % 20) + 10) * HZ); // Delay next call - } - else if ((FindItem(M4_NAME) == self->client->weapon || FindItem(MP5_NAME) == self->client->weapon) && self->bot.enemy_dist > 1024) - { - chase_enemy = true; - self->bot.enemy_chase_time = level.framenum + (((rand() % 10) + 10) * HZ); // Delay next call - } - - if (chase_enemy) - { - // Get enemy node - if (BOTLIB_CanGotoNode(self, self->enemy->bot.current_node, false)) // Make sure we can visit the node they're at - { - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, self->enemy->bot.current_node); - //Com_Printf("%s %s visiting enemy %s node %i [delay: %i vs %i] [wep: %s]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->bot.current_node, self->bot.enemy_chase_time, level.framenum, self->client->weapon->pickup_name); - } - } - } - } - - // If the bot is on a slope, raise it up depending on the slope normal and the bot mins/maxs hit box - { - self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 48), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - - /* - self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - vec3_t exp_up; - VectorCopy(self->bot.touch_ground.plane.normal, exp_up); - exp_up[2] = 0; - VectorNormalize(exp_up); - exp_up[2] = 1; - VectorScale(exp_up, 24, exp_up); - // Feed the raised up position back into the trace - VectorAdd(self->bot.touch_ground.endpos, exp_up, self->bot.touch_ground.endpos); - self->bot.touch_ground = gi.trace(self->bot.touch_ground.endpos, NULL, NULL, self->bot.touch_ground.endpos, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - */ - - } - - if (self->bot.state == BOT_MOVE_STATE_MOVE || self->bot.state == BOT_MOVE_STATE_WANDER || self->bot.state == BOT_MOVE_STATE_STAND) - { - BOTLIB_FollowPath(self); // Get current and next node back from nav code. - BOTLIB_Wander(self, &ucmd); - } - - BOTLIB_TouchingLadder(self); - BOTLIB_Look(self, &ucmd); - - BOTLIB_ChooseWeapon(self); - - // When out of sight of enemies - if (self->bot.see_enemies == false) - { - BOTLIB_Healing(self, &ucmd); // Check if bot needs to heal - BOTLIB_ReadyWeapon(self); // Change to a better weapon - - // Sniper bots should zoom in before an encounter - if ((rand() % 10) == 0) - BOTLIB_SniperZoom(self); - - } - - //if (self->bot.see_enemies == true && self->bot.enemy_in_xhair && //(self->enemy && self->enemy->deadflag == DEAD_NO) && - // (self->client->weaponstate != WEAPON_RELOADING) && (self->client->bandaging == 0) && - // (teamplay->value && lights_camera_action <= 1) || teamplay->value == 0) - if (self->bot.see_enemies) - { - if (self->bot.enemy_in_xhair) - BOTLIB_Attack(self, &ucmd); - - else if (self->client->weapon == FindItemByNum(HC_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) - BOTLIB_Attack(self, &ucmd); - - else if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) - BOTLIB_Attack(self, &ucmd); - } - } - - - // Remember where we were, to check if we got stuck. - //VectorCopy( self->s.origin, self->lastPosition ); - - // set bot's view angle - ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); - ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); - ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); - -end_think: - - ucmd.msec = 1000 / BOT_FPS; - self->client->ps.pmove.pm_time = ucmd.msec / 8; - - BOTLIB_BotInputToUserCommand(self, &self->bot.bi, &ucmd, self->bot.bi.viewangles, self->client->ps.pmove.pm_time); - - ClientThink(self, &ucmd); - - self->nextthink = level.framenum + 1; //(game.framerate / BOT_FPS); -} - - - -// Return's the optimal pitch for throwing a knife at an enemy -float BOTLIB_ThrowingKnifePitch(edict_t* self) -{ - if (self->enemy == NULL) - return 0; - - float gravity = sv_gravity->value; // Gravity - float distanceXYZ = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XY - float distanceXY = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XYZ - float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference - float initialSpeed = 1200; - - float timeToMaxHeight = sqrtf((initialSpeed * initialSpeed) / (2 * gravity)); - float totalFlightTime = timeToMaxHeight + sqrtf((distanceXYZ / height_diff) + timeToMaxHeight * timeToMaxHeight); - - // Calculate total flight time - totalFlightTime = sqrtf((2 * distanceXYZ) / sv_gravity->value); - - float horizontalVelocity = distanceXY / totalFlightTime; - float verticalVelocity = ((horizontalVelocity * horizontalVelocity) + (initialSpeed * initialSpeed)) / (2 * height_diff); - - float launchAngleDegrees = atan2f(verticalVelocity, horizontalVelocity) * (180 / M_PI); - launchAngleDegrees += 90; - if (launchAngleDegrees) - { - launchAngleDegrees /= 2; - - if (height_diff > 0) - launchAngleDegrees -= 90; - } - if (launchAngleDegrees > 89 || launchAngleDegrees < -89) - launchAngleDegrees = 0; - - Com_Printf("%s %s pitch[%f]\n", __func__, self->enemy->client->pers.netname, launchAngleDegrees); - - return launchAngleDegrees; - - //ucmd->angles[PITCH] = launchAngleDegrees; -} - -void BOTLIB_TouchingLadder(edict_t* self) -{ - //trace_t tr; - - //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) - { - // Check if touching ladder - { - float yaw_rad = 0; - vec3_t fwd = { 0 }, end = { 0 }; - trace_t tr; - - yaw_rad = DEG2RAD(self->s.angles[YAW]); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - //VectorMA(self->s.origin, 1, fwd, end); - //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); - - VectorMA(self->s.origin, 1, fwd, end); - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - - if (self->bot.touching_ladder == false) - { - VectorMA(self->s.origin, 8, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - if (self->bot.touching_ladder == false) - { - VectorMA(self->s.origin, 16, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - if (self->bot.touching_ladder == false) - { - VectorMA(self->s.origin, 32, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - } - } -} - -// Record footsteps -void BOTLIB_Footsteps(edict_t *self) -{ - if (self->s.event == EV_FOOTSTEP) - PlayerNoise(self, self->s.origin, PNOISE_SELF); -} - -// Keep a record of player noises and their locations -void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type) -{ - // Each player - for (int i = 0; i < MAX_CLIENTS; i++) - { - if (players[i] == who) // Find the right player - { - // Assign based on type - if (type == PNOISE_SELF) // Getting in and out of water, drowning, walking - { - botlib_noises.self_time[i] = 120; - botlib_noises.owner[i] = who; - VectorCopy(where, botlib_noises.self_origin[i]); - - // Randomize the noise origin a bit - if (rand() % 2 == 0) - botlib_noises.self_origin[i][0] += 512; - else - botlib_noises.self_origin[i][0] -= 512; - - if (rand() % 2 == 0) - botlib_noises.self_origin[i][1] += 512; - else - botlib_noises.self_origin[i][1] -= 512; - - if (rand() % 2 == 0) - botlib_noises.self_origin[i][2] += 64; - else - botlib_noises.self_origin[i][2] -= 32; - } - else if (type == PNOISE_WEAPON) // Weapons with muzzle flash - { - botlib_noises.weapon_time[i] = 120; - botlib_noises.owner[i] = who; - VectorCopy(where, botlib_noises.weapon_origin[i]); - - // Randomize the noise origin a bit - if (rand() % 2 == 0) - botlib_noises.weapon_origin[i][0] += 512; - else - botlib_noises.weapon_origin[i][0] -= 512; - - if (rand() % 2 == 0) - botlib_noises.weapon_origin[i][1] += 512; - else - botlib_noises.weapon_origin[i][1] -= 512; - - if (rand() % 2 == 0) - botlib_noises.self_origin[i][2] += 64; - else - botlib_noises.self_origin[i][2] -= 32; - } - else if (type == PNOISE_IMPACT) // Knife impact, or grenade explosion - { - botlib_noises.impact_time[i] = 120; - botlib_noises.owner[i] = who; - VectorCopy(where, botlib_noises.impact_origin[i]); - - // Randomize the noise origin a bit - if (rand() % 2 == 0) - botlib_noises.impact_origin[i][0] += 512; - else - botlib_noises.impact_origin[i][0] -= 512; - - if (rand() % 2 == 0) - botlib_noises.impact_origin[i][1] += 512; - else - botlib_noises.impact_origin[i][1] -= 512; - - if (rand() % 2 == 0) - botlib_noises.self_origin[i][2] += 64; - else - botlib_noises.self_origin[i][2] -= 32; - } - } - } - - // Code from BASEQ2 -- s - /* - edict_t *noise; - - if (type == PNOISE_WEAPON) { - if (who->client->silencer_shots) { - who->client->silencer_shots--; - return; - } - } - - if (deathmatch->value) - return; - - if (who->flags & FL_NOTARGET) - return; - - - if (!who->mynoise) { - noise = G_Spawn(); - noise->classname = "player_noise"; - VectorSet(noise->mins, -8, -8, -8); - VectorSet(noise->maxs, 8, 8, 8); - noise->owner = who; - noise->svflags = SVF_NOCLIENT; - who->mynoise = noise; - - noise = G_Spawn(); - noise->classname = "player_noise"; - VectorSet(noise->mins, -8, -8, -8); - VectorSet(noise->maxs, 8, 8, 8); - noise->owner = who; - noise->svflags = SVF_NOCLIENT; - who->mynoise2 = noise; - } - - if (type == PNOISE_SELF || type == PNOISE_WEAPON) { - noise = who->mynoise; - level.sound_entity = noise; - level.sound_entity_framenum = level.framenum; - } else { // type == PNOISE_IMPACT - noise = who->mynoise2; - level.sound2_entity = noise; - level.sound2_entity_framenum = level.framenum; - } - - VectorCopy(where, noise->s.origin); - VectorSubtract(where, noise->maxs, noise->absmin); - VectorAdd(where, noise->maxs, noise->absmax); - noise->last_sound_framenum = level.framenum; - gi.linkentity(noise); - */ - // Code from BASEQ2 -- e -} - -/////////////////////////////////////////////////////////////////////// -// Checks if bot can move (really just checking the ground) -// Also, this is not a real accurate check, but does a -// pretty good job and looks for lava/slime. -/////////////////////////////////////////////////////////////////////// -qboolean BOTLIB_CanMove(edict_t* self, int direction) -{ - vec3_t start, end; - vec3_t forward, right; - vec3_t offset; - vec3_t angles; - trace_t tr; - - // Now check to see if move will move us off an edge - VectorCopy(self->s.angles, angles); - - if (direction == MOVE_LEFT) - angles[1] += 90; - else if (direction == MOVE_RIGHT) - angles[1] -= 90; - else if (direction == MOVE_BACK) - angles[1] -= 180; - - // Trace ahead to see if safe or hitting wall - { - AngleVectors(angles, forward, right, NULL); - VectorSet(offset, 0, 7, self->viewheight - 8); - offset[1] = 0; - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, 64, forward, end); - tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); - if (tr.fraction < 1.0 || tr.contents & MASK_DEADLY || (tr.ent && (tr.ent->touch == hurt_touch))) - return false; - } - - // Trace down to see if safe - { - AngleVectors(angles, forward, right, NULL); // Set up the vectors - - //VectorSet(offset, 36, 0, 24); - //G_ProjectSource(self->s.origin, offset, forward, right, start); - - VectorSet(offset, 36, 0, -110); // RiEvEr reduced drop distance - G_ProjectSource(self->s.origin, offset, forward, right, end); - - tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); - if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA - || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT - { - return false; - } - } - - - return true; // yup, can move +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +void BOTLIB_Init(edict_t* self) +{ + self->classname = "bot"; + self->suicide_timeout = level.framenum + 15.0 * HZ; + + self->nav = NULL; //rekkie -- surface data + + self->enemy = NULL; + self->movetarget = NULL; + self->last_jumppad_node = INVALID; + self->bot_strafe = 0; + self->show_node_links = INVALID; + self->show_node_links_time = 0; + + //RiEvEr - new node pathing system + memset(&(self->pathList), 0, sizeof(self->pathList)); + self->pathList.head = self->pathList.tail = NULL; + //R + + self->client->resp.radio.gender = (self->client->pers.gender == GENDER_FEMALE) ? 1 : 0; + + + // Save previous data + float prev_skill = self->bot.skill; + + memset(&self->bot, 0, sizeof(bot_t)); + + // Restore previous data + self->bot.skill = prev_skill; + + // Ping + // Set the average ping this bot will see + int rng_ping_range = rand() % 5; + if (rng_ping_range == 0) + self->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard + else if (rng_ping_range <= 3) + self->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger + else + self->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard + + + + + // Enemies + self->bot.old_enemy = NULL; + self->bot.enemy_in_xhair = false; + self->bot.enemy_dist = 0; + self->bot.enemy_height_diff = 0; + self->bot.enemy_seen_time = 0; + self->bot.enemy_chase_time = 0; + self->bot.reaction_time = bot_reaction->value; + + //memset(&self->bot.noises, 0, sizeof(int) * MAX_CLIENTS); + //memset(&self->bot.noise_time, 0, sizeof(int) * MAX_CLIENTS); + + // Nav + self->bot.state = BOT_MOVE_STATE_NONE; // Init bot navigation + self->bot.prev_node = INVALID; + self->bot.current_node = INVALID; + self->bot.next_node = INVALID; + self->bot.goal_node = INVALID; + self->bot.stuck_node = INVALID; + self->bot.walknode.touched_node = INVALID; + self->bot.walknode.highlighted_node = INVALID; + self->bot.walknode.prev_highlighted_node = INVALID; + + + // Weapons + self->bot.last_sniper_zoom_time = 0; + self->bot.last_weapon_change_time = 0; + + + if (teamplay->value) // Reset bot radio at the start of each round + { + // Radio + BOTLIB_PrecacheRadioSounds(); + self->bot.radioTeamReportedIn = false; + self->bot.radioLastSawAllyTime = 0; + self->bot.radioTeamReportedInDelay = 75 + (rand() % 101); // Add a random delay between 75 and 175 ricks + self->bot.radioReportIn = false; + self->bot.radioReportInTime = 0; + self->bot.radioReportInDelay = 30 + (rand() % 60); // Add a random delay between 30 and 90 ticks + self->bot.radioBandaging = false; + self->bot.radioLastHumanMsg[0] = '\0'; + self->bot.radioReportKills = (rand() % 2); // RNG if this bot will report its kills or not + + // Enemies + if (ff_afterround->value) + self->bot.ff_allies_after_rnd = ((rand() % 2) == 0); // 50% chance the bot will attack allies after a TP round ends + + // Update skin to match TP skins + const char* s = Info_ValueForKey(self->client->pers.userinfo, "skin"); + AssignSkin(self, s, false /* nickChanged */); + } + +} + +//rekkie -- Quake3 -- s +//============== +// BOTLIB_BotInputToUserCommand +// Translates bot input to actual Q2 movement calls +//============== +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time) +{ + vec3_t angles, forward, right; + float f, r, u, m; + + //clear the whole structure + //memset(ucmd, 0, sizeof(usercmd_t)); + + //the duration for the user command in milli seconds + //ucmd->serverTime = time; + //ucmd->weapon = bi->weapon; + + + + //set the buttons + if (bi->actionflags & ACTION_RESPAWN) ucmd->buttons = BUTTON_ATTACK; + if (bi->actionflags & ACTION_ATTACK) ucmd->buttons |= BUTTON_ATTACK; + bi->actionflags &= ~ACTION_ATTACK; + + /* + if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK; + if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE; + if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE; + if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_WALKING; + if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE; + if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE; + if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG; + if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE; + if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL; + if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME; + */ + + + + // Radio wave animations + // Enemy interaction + if (bi->radioflags & RADIO_ENEMY_DOWN) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_ENEMY_SIGHTED) BOTLIB_Wave(ent, WAVE_FLIPOFF); + // Assistance required + else if (bi->radioflags & RADIO_TAKING_FIRE) BOTLIB_Wave(ent, WAVE_FLIPOFF); + else if (bi->radioflags & RADIO_IM_HIT) BOTLIB_Wave(ent, WAVE_TAUNT); + else if (bi->radioflags & RADIO_COVER) BOTLIB_Wave(ent, WAVE_TAUNT); + // Team reporting + else if (bi->radioflags & RADIO_TEAM_REPORT_IN) BOTLIB_Wave(ent, WAVE_WAVE); + else if (bi->radioflags & RADIO_REPORTIN) BOTLIB_Wave(ent, WAVE_SALUTE); + else if (bi->radioflags & RADIO_TEAMMATE_DOWN) BOTLIB_Wave(ent, WAVE_SALUTE); + // Directions + else if (bi->radioflags & RADIO_BACK) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_FORWARD) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_UP) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_DOWN) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_LEFT) BOTLIB_Wave(ent, WAVE_POINT); + else if (bi->radioflags & RADIO_RIGHT) BOTLIB_Wave(ent, WAVE_POINT); + + + + + + + + ent->bot.bi.radioflags = 0; // Clear radio flags + + + // Adjust throwing pitch if using throwing knives + //if (ent->client->weapon == FindItemByNum(KNIFE_NUM) && ent->client->pers.knife_mode == 1) // Throwing knives + // bi->viewangles[PITCH] = BOTLIB_ThrowingKnifePitch(ent); + + + //set the view angles + //NOTE: the ucmd->angles are the angles WITHOUT the delta angles + ucmd->angles[PITCH] = ANGLE2SHORT(bi->viewangles[PITCH]); + ucmd->angles[YAW] = ANGLE2SHORT(bi->viewangles[YAW]); + ucmd->angles[ROLL] = ANGLE2SHORT(bi->viewangles[ROLL]); + /* + //subtract the delta angles + for (j = 0; j < 3; j++) { + temp = ucmd->angles[j] - delta_angles[j]; + ucmd->angles[j] = temp; + } + */ + + + if (bi->dir[2]) + angles[PITCH] = bi->viewangles[PITCH]; + else + angles[PITCH] = 0; + angles[YAW] = bi->viewangles[YAW]; + angles[ROLL] = 0; + + // No movement + //if (VectorEmpty(bi->dir)) return; + if (bi->actionflags & ACTION_HOLDPOS) + { + // Kill velocity + bi->actionflags = 0; + bi->speed = 0; + ent->velocity[0] *= 0.9; + ent->velocity[1] *= 0.9; + ent->velocity[2] *= 0.9; + + return; + } + else + { + //NOTE: movement is relative to the REAL view angles + //get the horizontal forward and right vector + AngleVectors(angles, forward, right, NULL); + //bot input speed is in the range [0, 400] - SPEED_RUN + //bi->speed = bi->speed * 127 / SPEED_RUN; + + // Cap max speed + if (bi->speed > SPEED_RUN) bi->speed = SPEED_RUN; + else if (bi->speed < -SPEED_RUN) bi->speed = -SPEED_RUN; + + //set the view independent movement + f = DotProduct(forward, bi->dir); + r = DotProduct(right, bi->dir); + u = fabs(forward[2]) * bi->dir[2]; + m = fabs(f); + + if (fabs(r) > m) { + m = fabs(r); + } + + if (fabs(u) > m) { + m = fabs(u); + } + + if (m > 0) { + f *= bi->speed / m; + r *= bi->speed / m; + u *= bi->speed / m; + } + + if (ent->client->leg_damage == 0) // No jumping if legs are broken + u = 0; + + ucmd->forwardmove = f; + ucmd->sidemove = r; + ucmd->upmove = u; + } + + if (bi->actionflags & ACTION_MOVEFORWARD) ucmd->forwardmove = SPEED_RUN; // 127 + if (bi->actionflags & ACTION_MOVEBACK) ucmd->forwardmove = -SPEED_RUN; // -127 + if (bi->actionflags & ACTION_MOVELEFT) ucmd->sidemove = -SPEED_RUN; // -127 + if (bi->actionflags & ACTION_MOVERIGHT) ucmd->sidemove = SPEED_RUN; // 127 + + + //jump/moveup +//if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) ucmd->upmove = SPEED_RUN; // 127 + + if ((bi->actionflags & ACTION_HOLDJUMP) && (ent->groundentity)) // Stop holding jump if on ground + { + bi->actionflags &= ~ACTION_HOLDJUMP; + } + if ((bi->actionflags & ACTION_HOLDJUMP)) // Hold jump if in air + { + if (ent->groundentity == NULL) + ucmd->upmove = SPEED_RUN; + else + bi->actionflags &= ~ACTION_HOLDJUMP; + } + if (bi->actionflags & ACTION_JUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Slow down + //ucmd->forwardmove = 0; + //ucmd->sidemove = 0; + //ucmd->upmove = 0; + + //bi->actionflags &= ~ACTION_JUMP; + //bi->actionflags |= ACTION_HOLDJUMP; + ucmd->upmove = SPEED_RUN; + if (ent->groundentity) // Give a slight boost if on ground + ent->velocity[2] += 50; + + ucmd->forwardmove = SPEED_RUN; + } + if (bi->actionflags & ACTION_BOXJUMP && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Slow down + //ucmd->forwardmove = 0; + //ucmd->sidemove = 0; + //ucmd->upmove = 0; + + //bi->actionflags &= ~ACTION_JUMP; + //bi->actionflags |= ACTION_HOLDJUMP; + ucmd->upmove = SPEED_RUN; + if (ent->groundentity) // Give a slight boost if on ground + ent->velocity[2] += 325; // 400 + + ucmd->forwardmove = SPEED_RUN; + } + if (bi->actionflags & ACTION_JUMPPAD && ent->client->leg_damage == 0) // Initiate jump if no leg damage + { + // Check the bot is touching a jump node before attempting to jump + int nodelist[MAX_NODELIST]; + int nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + if (nodes_touched && ent->bot.current_node != INVALID && ent->bot.next_node != INVALID) + { + for (int i = 0; i < nodes_touched; i++) + { + //Com_Printf("%s %s touched node %d\n", __func__, ent->client->pers.netname, nodelist[i]); + //if (nodes[nodelist[i]].type & NODE_JUMPPAD) + if (nodes[nodelist[i]].nodenum == ent->bot.current_node) + { + //Com_Printf("%s %s touched jump node %d\n", __func__, ent->client->pers.netname, nodelist[i]); + // Attempt jump + //BOTLIB_Jump_Takeoff(ent, NULL, nodes[ent->bot.next_node].origin, ent->viewheight, ent->velocity); + BOTLIB_DoParabolaJump(ent, nodes[ent->bot.next_node].origin); + bi->actionflags &= ~ACTION_JUMPPAD; + break; + } + } + } + } + //crouch/movedown + if (bi->actionflags & ACTION_CROUCH) ucmd->upmove = -SPEED_RUN; // -127 + + //ladder - movedown, moveup + if (bi->actionflags & ACTION_MOVEDOWN) + { + ucmd->forwardmove = 50; // jump/move up + ucmd->upmove = -SPEED_WALK; // crouch + + // Slow turning speed + ent->velocity[0] *= 0.25; + ent->velocity[1] *= 0.25; + + ent->velocity[2] = -100; // Slow decent + + //Com_Printf("%s %s ladder down\n", __func__, ent->client->pers.netname); + + //vec3_t dist; + //VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); + //VectorNormalize(dist); + //VectorScale(dist, 20, ent->velocity); // Apply it + } + if (bi->actionflags & ACTION_MOVEUP) + { + /* + // Check we're looking in the direction of a ladder + qboolean on_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(ent->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(ent->s.origin, 16, fwd, end); + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_PLAYERSOLID); + + on_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + + if (on_ladder) + { + ent->velocity[2] += 10; + ucmd->upmove = SPEED_WALK; + Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + } + */ + + //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + + ucmd->forwardmove = SPEED_RUN; // jump/move up + ucmd->upmove = SPEED_RUN; // jump/move up + + // Slow turning speed + ent->velocity[0] *= 0.25; + ent->velocity[1] *= 0.25; + + // Go up + ent->velocity[2] = 200; // Slow upward movement + + /* + // Check if touching ladder with an overly large hitbox + if (0) + { + vec3_t min = { -32, -32, -60 }; + vec3_t max = { 32, 32, 60 }; + trace_t tr = gi.trace(ent->s.origin, min, max, ent->s.origin, ent, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) || tr.contents & CONTENTS_SOLID) + { + ent->velocity[2] = 200; // Slow upward movement + + //Com_Printf("%s %s ladder up\n", __func__, ent->client->pers.netname); + return; + } + } + */ + + //ent->movetype == MOVETYPE_FLY; + + /* + vec3_t dist; + VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); + VectorNormalize(dist); + VectorScale(dist, 20, ent->velocity); // Apply it + + for (int i = 0; i < 3; i++) + ent->velocity[i] = 200 * bi->dir[i]; + */ + } + + + // Air control + //if ((bi->actionflags & ACTION_JUMP) || (bi->actionflags & ACTION_HOLDJUMP)) + //if (ent->groundentity == NULL) + if (bi->actionflags & ACTION_JUMP) + //if (0) + { + float wishspeed, addspeed, accelspeed, currentspeed; + + wishspeed = VectorNormalize(bi->dir); + if (wishspeed > SPEED_RUN) wishspeed = SPEED_RUN; + + currentspeed = DotProduct(ent->velocity, bi->dir); + + addspeed = wishspeed - currentspeed; + addspeed *= 10.1; + if (addspeed <= 0) { + return; + } + + //accelspeed = ent->accel * FRAMETIME * wishspeed; + accelspeed = SPEED_RUN * FRAMETIME * wishspeed; + if (accelspeed > addspeed) { + accelspeed = addspeed; + } + + + /* + float z = accelspeed * bi->dir[2]; + if (z < 0) + { + // Slow down velocity + //Com_Printf("%s %s velocity %f\n", __func__, ent->client->pers.netname, z); + ent->velocity[2] += z; + } + */ + + ent->velocity[0] += accelspeed * bi->dir[0]; + ent->velocity[1] += accelspeed * bi->dir[1]; + ent->velocity[2] += accelspeed * bi->dir[2]; + + //for (i = 0; i < 3; i++) { + // ent->velocity[i] += accelspeed * bi->dir[i]; + // //ent->velocity[i] = accelspeed * bi->dir[i]; + //} + //Com_Printf("%s %s velocity[%f %f %f]\n", __func__, ent->client->pers.netname, ent->velocity[0], ent->velocity[1], ent->velocity[2]); + } + + //bi->actionflags = 0; // Clear action flags + ent->bot.bi.actionflags = 0; +} + +// Automatic adjustment of bot skills based on their current score compared to all other bots +/* +typedef struct { + int playernum; + int score; +} PlayerScores; +// Comparison function used by qsort() +int BOTLIB_ScoreCompare(const void* a, const void* b) +{ + const PlayerScores* p1 = (PlayerScores*)a; + const PlayerScores* p2 = (PlayerScores*)b; + return p2->score - p1->score; // Sort ascendingly by score +} +*/ +int BOTLIB_AutoAdjustSkill(edict_t * self) +{ + int highest_score = 0; // Highest player score + int score_diff = 0; // Difference between player and best player's score + float score_percent_diff = 1.0; // Difference in percent between player and best player's score + + //Init the variable bot skill + if (self->bot.skill < 1) + self->bot.skill = bot_skill->value; + + if (self->client == NULL) + return 0; + + // Get highest score + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i]->client == NULL) continue; + + if (players[i]->client->resp.score > highest_score) + highest_score = players[i]->client->resp.score; + } + + // Find the difference between highest score and our score (the gap between two scores) + if (self->client->resp.score < highest_score) + { + score_diff = highest_score - self->client->resp.score; // Get score gap + + if (score_diff > (int)bot_skill_threshold->value) // If the score gap is greater than threshold + { + // Get the diff in percent: 1.0 is equal, 0.5 is half the score of the best player. Higher is better. + // > 0.75 is normal + // > 0.5 is okay + // < 0.5 is poor + score_percent_diff = (float)self->client->resp.score / (float)highest_score; + + if (score_percent_diff >= 0.75) + { + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); + return 0; // Normal + } + else if (score_percent_diff >= 0.5) + { + self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + + if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill = bot_skill->value; // Scale skill back down to base level + + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + + return 1; // Okay + } + else if (score_percent_diff < 0.5) + { + self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + + if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill = bot_skill->value; // Scale skill back down to base level + + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + + return 2; // Poor + } + } + } + + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); + return 0; // Normal + + /* + PlayerScores player_scores[MAX_CLIENTS]; // All player scores + int player_id = 0; // Our player id + + // Check if players have completed at least the first round. We only want to adjust skill after players have an established score. + qboolean completed_a_round = false; + for (int i = TEAM1; i <= teamCount; i++) + { + if (teams[i].score > 0) + { + completed_a_round = true; + break; + } + } + if (completed_a_round == false) // Ignore the first round + return 0; + + // Init player scores + for (int i = 0; i <= num_players; i++) + { + if (players[i] == self) // If this is us + player_id = i; // Store our own id + + player_scores[i].playernum = i; // Store the player id + + // Store the score + if (players[i] == NULL || players[i]->client == NULL) + player_scores[i].score = 0; // Zero invalid clients + else + player_scores[i].score = players[i]->client->resp.score; // Valid client score + + if (player_scores[i].score > highest_score) + highest_score = player_scores[i].score; + } + + // Sort scores from highest to lowest + qsort(player_scores, num_players, sizeof(PlayerScores), BOTLIB_ScoreCompare); + + // Find where we rank + int rank = 0; + for (int i = 0; i <= num_players; i++) + { + // Only rank against player's with a score + //if (player_scores[i].score > 0) + rank++; + + // Break when we find ourself + if (player_id == player_scores[i].playernum) + break; + } + + float percent = ((float)rank / (float)num_players); + + //Com_Printf("%s %s rank[%d of %d] = %f score[%d]\n", __func__, self->client->pers.netname, rank, num_players, percent, self->client->resp.score); + + // If bot score is in the bottom half (50% or greater) adjust its skill so it can aim to be in the top 10% + if (percent >= 0.75) // Higher is worse + { + self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + + if (self->bot.skill > MAX_BOTSKILL) // Max skill + self->bot.skill = MAX_BOTSKILL; + + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + } + else if (percent >= 0.5) // Higher is worse + { + self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + + if (self->bot.skill > MAX_BOTSKILL) // Max skill + self->bot.skill = MAX_BOTSKILL; + + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + } + else // Return skill back to base level + { + self->bot.skill = bot_skill->value; + //Com_Printf("%s %s adjusting bot skill [-] [%d]\n", __func__, self->client->pers.netname, (int)bot_skill->value); + } + + // Return skill bracket: low, med, high. Used to recommend a weapon and item + if (percent < 0.25) + return 0; // Low + else if (percent < 0.5) + return 1; // Med + + return 2; // High + */ +} + +/////////////////////////////////////////////////////////////////////// +// Main Think function for bot +/////////////////////////////////////////////////////////////////////// +void BOTLIB_Think(edict_t* self) +{ + usercmd_t ucmd; + + //rekkie -- Fake Bot Client -- s + if (level.framenum % 10 == 0) // Update bot info every 10th frame + { + // Set self->client->ping to random bot->bot_baseline_ping, then vary by +-3 (ping jitter) + int ping_jitter = (rand() % 7); // Positive jitter + if (ping_jitter && (rand() % 1) == 0) + ping_jitter -= (ping_jitter * 2); // Negative jitter + + //Com_Printf("%s %s [%d + %d]\n", __func__, self->client->pers.netname, self->bot.bot_baseline_ping, ping_jitter); + + self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; + if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping + self->client->ping = self->bot.bot_ping; + gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); + } + //rekkie -- Fake Bot Client -- e + + // Set up client movement + VectorCopy(self->client->ps.viewangles, self->s.angles); + VectorSet(self->client->ps.pmove.delta_angles, 0, 0, 0); + memset(&ucmd, 0, sizeof(ucmd)); + + // Make bots end intermission and enter the next map (especially useful to change maps when no 'human' players are around) + if (level.intermission_framenum) + { + ucmd.buttons = BUTTON_ANY; // This button is pressed to end the intermission + goto end_think; + } + + // Stop trying to think if the bot can't respawn. + if (!IS_ALIVE(self) && ((gameSettings & GS_ROUNDBASED) || (self->client->respawn_framenum > level.framenum))) + { + goto end_think; + } + + // Force respawn + if (self->deadflag == DEAD_DEAD) + { + self->client->buttons = 0; + ucmd.buttons = BUTTON_ATTACK; + } + + // Don't execute thinking code if not alive + if (self->deadflag != DEAD_NO || self->health <= 0) + { + goto end_think; + } + + // Make the bot brain dead for a period of time + if (self->bot.pause_time) + { + self->bot.pause_time--; // Reduce timer to zero + self->bot.bi.speed = 0; + goto end_think; // Skip bot logic + } + + if (level.framenum < 75) // Wait for a little before processing AI on a new map + goto end_think; // Skip bot logic + + if (ctf->value) // CTF Goals + { + BOTLIB_CTF_Goals(self); + } + else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) + { + /* + if (rand() % 2) // Area 1 + { + BOTLIB_CanGotoNode(self, nodes[64].nodenum, 0); + } + else // Area 9 + { + BOTLIB_CanGotoNode(self, nodes[3344].nodenum, 0); + } + */ + + /* + //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (curr_node != 2505) + if (1 && rand() % 2) // Area 0 + { + if (rand() % 2) + BOTLIB_CanGotoNode(self, nodes[2505].nodenum, 0); + else + BOTLIB_CanGotoNode(self, nodes[3346].nodenum, 0); + } + else // Area 1 + { + if (rand() % 2) + BOTLIB_CanGotoNode(self, nodes[999].nodenum, 0); + else + BOTLIB_CanGotoNode(self, nodes[38].nodenum, 0); + } + */ + + //else // DM and TP goals + { + //self->bot.pause_time = 100; + //Com_Printf("\n%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() -------------------- \n", __func__, self->client->pers.netname, level.framenum); + + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname, level.framenum); + + //nav_area.total_areas = 0; // Turn off area based nav + BOTLIB_PickLongRangeGoal(self); + + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() curr[%d] goal[%d] -------------------- \n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node, self->bot.goal_node); + } + } + /* + if (self->bot.state == BOT_MOVE_STATE_NAV_NEXT) // If map has nodes grouped into areas + { + //self->bot.pause_time = 100; + //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV_NEXT BOTLIB_GetNextAreaNode() -------------------- \n", __func__, self->client->pers.netname, level.framenum); + BOTLIB_GetNextAreaNode(self); // Get next area + } + */ + + + + // Kill the bot if completely stuck somewhere + //if(VectorLength(self->velocity) > 37) // + // self->suicide_timeout = level.framenum + 10.0 * HZ; + //if( self->suicide_timeout < level.framenum && !teamplay->value ) + // killPlayer( self, true ); + + // Kill the bot if they've not moved between nodes in a timely manner, stuck! + if (self->bot.node_travel_time > 120) + killPlayer(self, true); + + // Find any short range goal + //ACEAI_PickShortRangeGoal(self); + + //BOTLIB_GetWeaponsAndAmmo(self); //rekkie -- Locate and pickup a primary weapon if we need one + + + if (0) // Always follow player -- && (level.framenum % HZ == 0)) + { + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (players[p]->is_bot == false && players[p]->bot.current_node != 0 && players[p]->solid == SOLID_BBOX && players[p]->deadflag == DEAD_NO) + { + // Get friendly node + int n = players[p]->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() + if (n == INVALID) + continue; + + if (self->bot.goal_node != n) + { + Com_Printf("%s %s following %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[p]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + //BOTLIB_SetGoal(self, n); + } + } + } + } + + + + + //if (1) + { + BOTLIB_FindVisibleAllies(self); // Find visible allies + BOTLIB_Radio(self, &ucmd); + self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies + BOTLIB_Reload(self); // Reload the weapon if needed + + if (self->enemy) + { + // Chase after the new enemy + if (self->bot.enemy_chase_time < level.framenum && self->enemy->bot.current_node != self->bot.goal_node) + { + qboolean chase_enemy = false; + if ((FindItem(HC_NAME) == self->client->weapon || + FindItem(M3_NAME) == self->client->weapon || + FindItem(DUAL_NAME) == self->client->weapon || + FindItem(KNIFE_NAME) == self->client->weapon) + && self->bot.enemy_dist > 200) + { + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + 1 * HZ; // Delay next call + } + else if (FindItem(SNIPER_NAME) == self->client->weapon)// && self->bot.enemy_dist > 1500) + { + chase_enemy = false; + self->bot.enemy_chase_time = level.framenum + (((rand() % 20) + 10) * HZ); // Delay next call + } + else if ((FindItem(M4_NAME) == self->client->weapon || FindItem(MP5_NAME) == self->client->weapon) && self->bot.enemy_dist > 1024) + { + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + (((rand() % 10) + 10) * HZ); // Delay next call + } + + if (chase_enemy) + { + // Get enemy node + if (BOTLIB_CanGotoNode(self, self->enemy->bot.current_node, false)) // Make sure we can visit the node they're at + { + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, self->enemy->bot.current_node); + //Com_Printf("%s %s visiting enemy %s node %i [delay: %i vs %i] [wep: %s]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->bot.current_node, self->bot.enemy_chase_time, level.framenum, self->client->weapon->pickup_name); + } + } + } + } + + // If the bot is on a slope, raise it up depending on the slope normal and the bot mins/maxs hit box + { + self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 48), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + + /* + self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + vec3_t exp_up; + VectorCopy(self->bot.touch_ground.plane.normal, exp_up); + exp_up[2] = 0; + VectorNormalize(exp_up); + exp_up[2] = 1; + VectorScale(exp_up, 24, exp_up); + // Feed the raised up position back into the trace + VectorAdd(self->bot.touch_ground.endpos, exp_up, self->bot.touch_ground.endpos); + self->bot.touch_ground = gi.trace(self->bot.touch_ground.endpos, NULL, NULL, self->bot.touch_ground.endpos, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + */ + + } + + if (self->bot.state == BOT_MOVE_STATE_MOVE || self->bot.state == BOT_MOVE_STATE_WANDER || self->bot.state == BOT_MOVE_STATE_STAND) + { + BOTLIB_FollowPath(self); // Get current and next node back from nav code. + BOTLIB_Wander(self, &ucmd); + } + + BOTLIB_TouchingLadder(self); + BOTLIB_Look(self, &ucmd); + + BOTLIB_ChooseWeapon(self); + + // When out of sight of enemies + if (self->bot.see_enemies == false) + { + BOTLIB_Healing(self, &ucmd); // Check if bot needs to heal + BOTLIB_ReadyWeapon(self); // Change to a better weapon + + // Sniper bots should zoom in before an encounter + if ((rand() % 10) == 0) + BOTLIB_SniperZoom(self); + + } + + //if (self->bot.see_enemies == true && self->bot.enemy_in_xhair && //(self->enemy && self->enemy->deadflag == DEAD_NO) && + // (self->client->weaponstate != WEAPON_RELOADING) && (self->client->bandaging == 0) && + // (teamplay->value && lights_camera_action <= 1) || teamplay->value == 0) + if (self->bot.see_enemies) + { + if (self->bot.enemy_in_xhair) + BOTLIB_Attack(self, &ucmd); + + else if (self->client->weapon == FindItemByNum(HC_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); + + else if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); + } + } + + + // Remember where we were, to check if we got stuck. + //VectorCopy( self->s.origin, self->lastPosition ); + + // set bot's view angle + ucmd.angles[PITCH] = ANGLE2SHORT(self->s.angles[PITCH]); + ucmd.angles[YAW] = ANGLE2SHORT(self->s.angles[YAW]); + ucmd.angles[ROLL] = ANGLE2SHORT(self->s.angles[ROLL]); + +end_think: + + ucmd.msec = 1000 / BOT_FPS; + self->client->ps.pmove.pm_time = ucmd.msec / 8; + + BOTLIB_BotInputToUserCommand(self, &self->bot.bi, &ucmd, self->bot.bi.viewangles, self->client->ps.pmove.pm_time); + + ClientThink(self, &ucmd); + + self->nextthink = level.framenum + 1; //(game.framerate / BOT_FPS); +} + + + +// Return's the optimal pitch for throwing a knife at an enemy +float BOTLIB_ThrowingKnifePitch(edict_t* self) +{ + if (self->enemy == NULL) + return 0; + + float gravity = sv_gravity->value; // Gravity + float distanceXYZ = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XY + float distanceXY = VectorDistance(self->s.origin, self->enemy->s.origin); // Distance XYZ + float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference + float initialSpeed = 1200; + + float timeToMaxHeight = sqrtf((initialSpeed * initialSpeed) / (2 * gravity)); + float totalFlightTime = timeToMaxHeight + sqrtf((distanceXYZ / height_diff) + timeToMaxHeight * timeToMaxHeight); + + // Calculate total flight time + totalFlightTime = sqrtf((2 * distanceXYZ) / sv_gravity->value); + + float horizontalVelocity = distanceXY / totalFlightTime; + float verticalVelocity = ((horizontalVelocity * horizontalVelocity) + (initialSpeed * initialSpeed)) / (2 * height_diff); + + float launchAngleDegrees = atan2f(verticalVelocity, horizontalVelocity) * (180 / M_PI); + launchAngleDegrees += 90; + if (launchAngleDegrees) + { + launchAngleDegrees /= 2; + + if (height_diff > 0) + launchAngleDegrees -= 90; + } + if (launchAngleDegrees > 89 || launchAngleDegrees < -89) + launchAngleDegrees = 0; + + Com_Printf("%s %s pitch[%f]\n", __func__, self->enemy->client->pers.netname, launchAngleDegrees); + + return launchAngleDegrees; + + //ucmd->angles[PITCH] = launchAngleDegrees; +} + +void BOTLIB_TouchingLadder(edict_t* self) +{ + //trace_t tr; + + //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) + { + // Check if touching ladder + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + //VectorMA(self->s.origin, 1, fwd, end); + //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); + + VectorMA(self->s.origin, 1, fwd, end); + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 8, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 16, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (self->bot.touching_ladder == false) + { + VectorMA(self->s.origin, 32, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + self->bot.touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + } + } +} + +// Record footsteps +void BOTLIB_Footsteps(edict_t *self) +{ + if (self->s.event == EV_FOOTSTEP) + PlayerNoise(self, self->s.origin, PNOISE_SELF); +} + +// Keep a record of player noises and their locations +void BOTLIB_PlayerNoise(edict_t* who, vec3_t where, int type) +{ + // Each player + for (int i = 0; i < MAX_CLIENTS; i++) + { + if (players[i] == who) // Find the right player + { + // Assign based on type + if (type == PNOISE_SELF) // Getting in and out of water, drowning, walking + { + botlib_noises.self_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.self_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.self_origin[i][0] += 512; + else + botlib_noises.self_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][1] += 512; + else + botlib_noises.self_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + else if (type == PNOISE_WEAPON) // Weapons with muzzle flash + { + botlib_noises.weapon_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.weapon_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.weapon_origin[i][0] += 512; + else + botlib_noises.weapon_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.weapon_origin[i][1] += 512; + else + botlib_noises.weapon_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + else if (type == PNOISE_IMPACT) // Knife impact, or grenade explosion + { + botlib_noises.impact_time[i] = 120; + botlib_noises.owner[i] = who; + VectorCopy(where, botlib_noises.impact_origin[i]); + + // Randomize the noise origin a bit + if (rand() % 2 == 0) + botlib_noises.impact_origin[i][0] += 512; + else + botlib_noises.impact_origin[i][0] -= 512; + + if (rand() % 2 == 0) + botlib_noises.impact_origin[i][1] += 512; + else + botlib_noises.impact_origin[i][1] -= 512; + + if (rand() % 2 == 0) + botlib_noises.self_origin[i][2] += 64; + else + botlib_noises.self_origin[i][2] -= 32; + } + } + } + + // Code from BASEQ2 -- s + /* + edict_t *noise; + + if (type == PNOISE_WEAPON) { + if (who->client->silencer_shots) { + who->client->silencer_shots--; + return; + } + } + + if (deathmatch->value) + return; + + if (who->flags & FL_NOTARGET) + return; + + + if (!who->mynoise) { + noise = G_Spawn(); + noise->classname = "player_noise"; + VectorSet(noise->mins, -8, -8, -8); + VectorSet(noise->maxs, 8, 8, 8); + noise->owner = who; + noise->svflags = SVF_NOCLIENT; + who->mynoise = noise; + + noise = G_Spawn(); + noise->classname = "player_noise"; + VectorSet(noise->mins, -8, -8, -8); + VectorSet(noise->maxs, 8, 8, 8); + noise->owner = who; + noise->svflags = SVF_NOCLIENT; + who->mynoise2 = noise; + } + + if (type == PNOISE_SELF || type == PNOISE_WEAPON) { + noise = who->mynoise; + level.sound_entity = noise; + level.sound_entity_framenum = level.framenum; + } else { // type == PNOISE_IMPACT + noise = who->mynoise2; + level.sound2_entity = noise; + level.sound2_entity_framenum = level.framenum; + } + + VectorCopy(where, noise->s.origin); + VectorSubtract(where, noise->maxs, noise->absmin); + VectorAdd(where, noise->maxs, noise->absmax); + noise->last_sound_framenum = level.framenum; + gi.linkentity(noise); + */ + // Code from BASEQ2 -- e +} + +/////////////////////////////////////////////////////////////////////// +// Checks if bot can move (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_CanMove(edict_t* self, int direction) +{ + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(self->s.angles, angles); + + if (direction == MOVE_LEFT) + angles[1] += 90; + else if (direction == MOVE_RIGHT) + angles[1] -= 90; + else if (direction == MOVE_BACK) + angles[1] -= 180; + + // Trace ahead to see if safe or hitting wall + { + AngleVectors(angles, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 64, forward, end); + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); + if (tr.fraction < 1.0 || tr.contents & MASK_DEADLY || (tr.ent && (tr.ent->touch == hurt_touch))) + return false; + } + + // Trace down to see if safe + { + AngleVectors(angles, forward, right, NULL); // Set up the vectors + + //VectorSet(offset, 36, 0, 24); + //G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorSet(offset, 36, 0, -110); // RiEvEr reduced drop distance + G_ProjectSource(self->s.origin, offset, forward, right, end); + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; + } + } + + + return true; // yup, can move } \ No newline at end of file diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 3ba92aa8d..9aecbfcab 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -1,795 +1,795 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -// Bot server commands -qboolean BOTLIB_SV_Cmds(void) -{ - char* cmd; - cmd = gi.argv(1); - - if (Q_stricmp(cmd, "bots") == 0) - { - int cc = gi.argc(); - - gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added - - // sv bots - if (gi.argc() == 3) // Adding a multiple bots - { - bot_connections.desire_bots = atoi(gi.argv(2)); // How many bots - bot_connections.auto_balance_bots = true; - return true; - } - - // sv bots - else if (gi.argc() == 4) // Adding a multiple bots to a select team - { - int count = atoi(gi.argv(2)); // How many bots - int team = atoi(gi.argv(3)); // What team - - //if (count == 0) - // count = -1; // Flag to remove all bots from team - - bot_connections.auto_balance_bots = false; - - // Deathmatch - if (teamplay->value == 0) - { - if (count == 0) - { - BOTLIB_RemoveBot("ALL"); - return true; - } - else - { - bot_connections.desire_bots = count; - bot_connections.auto_balance_bots = true; - return true; - } - } - else // Teamplay - { - if (team == TEAM1) bot_connections.desire_team1 = count; - else if (team == TEAM2) bot_connections.desire_team2 = count; - else if (team == TEAM3) bot_connections.desire_team3 = count; - - return true; - } - } - - else - { - gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); - gi.cprintf(NULL, PRINT_HIGH, "Use: sets the number of bots to join a game\n"); - gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); - gi.cprintf(NULL, PRINT_HIGH, "sv bots \n"); - gi.cprintf(NULL, PRINT_HIGH, "sv bots [team]\n"); - if (teamplay->value == 0) - gi.cprintf(NULL, PRINT_HIGH, "DM bot total [%d]\n", bot_connections.total_bots); - else if (use_3teams->value) - gi.cprintf(NULL, PRINT_HIGH, "3TEAM bots on T1[%d] T2[%d] T3[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots); - else if (ctf->value) - gi.cprintf(NULL, PRINT_HIGH, "CTF bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); - else if (teamplay->value) - gi.cprintf(NULL, PRINT_HIGH, "TP bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); - gi.cprintf(NULL, PRINT_HIGH, "-------------------------------\n"); - } - - return true; - } - - if (Q_stricmp(cmd, "addbot") == 0) - { - - return true; - } - else if (Q_stricmp(cmd, "addbots") == 0) - { - if (gi.argc() >= 3) - { - int count = atoi(gi.argv(2)), i = 0; - for (i = 0; i < count; i++) - { - } - } - else - gi.cprintf(NULL, PRINT_HIGH, "Usage: sv addbots []\n"); - - return true; - } - // removebot - else if (Q_stricmp(cmd, "removebot") == 0) - { - ACESP_RemoveBot(gi.argv(2)); - return true; - } - else if (Q_stricmp(cmd, "removebots") == 0) - { - ACESP_RemoveBot("all"); - return true; - } - //rekkie -- BSP -- s - // else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - // { - // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - // return true; - // } - // // Process BSP and generate AAS reachability with nodes - // else if (Q_stricmp(cmd, "aas") == 0) - // { - // ACEND_BSP(NULL); - // return true; - // } - //rekkie -- BSP -- e - - //rekkie -- python chatbot -- s -#if 0 - // - // Allows Quake 2 to communicate with a LLM (Large Language Model) - // [Input] Quake 2 ==> textgen.py ==> text-generation-webui (server) - // [Output] text-generation-webui (server) ==> textgen.py ==> Quake 2 - // - // Idea was taken from: https://www.youtube.com/watch?v=ndLqg4RYcXE (Full Tutorial of Calling Python Functions(.py) in C/C++(.c or .cpp) using (python.h)) - // - // This runs the python file ./action/bots/textgen.py - // The py file allows communcation with an ai chat server called text-generation-webui (github: https://github.com/oobabooga/text-generation-webui) - // - // text-generation-webui must be run with OpenAI API turned on (port 5000) - // - // From the C end, we require a few includes and libs from Python. - // NOTE: When installing python, make sure (Download debug binaries) is ticked, - // otherwise the compiler will complain about "python310_d.lib" missing. - // https://github.com/pybind/pybind11/issues/3403 - // - // Includes - // C:\Users\...\AppData\Local\Programs\Python\Python310\include - // - // Linker (libs) - // C:\Users\...\AppData\Local\Programs\Python\Python310\libs\python310.lib - // C:\Users\...\AppData\Local\Programs\Python\Python310\libs - else if (Q_stricmp(cmd, "chat") == 0) - { - Py_Initialize(); // Init python - - PyObject* name, * load_module, * func, * callfunc, * args; - - /* - //https://groups.google.com/g/comp.lang.python/c/14Yl3C8suKI?pli=1 - // https://gist.github.com/Coderx7/509a1aaf154a8d2532a6459a57308a63 - PyObject* mainmod = PyImport_AddModule("__main__"); - Py_INCREF(mainmod); - PyObject* ns = PyModule_GetDict(mainmod); - Py_INCREF(ns); - PyObject* timeModule = PyImport_ImportModuleEx("requests", ns, ns, NULL); - */ - - //PyObject* module = PyImport_AddModule("requests"); - PyObject* pModule = PyImport_ImportModule("requests"); // The python imports (imports used in textgen.py) - - // Open our custom python file - const wchar_t* path = L".\\action\\bots\\"; // https://stackoverflow.com/questions/31314767/call-multiple-python-functions-from-different-directories - PySys_SetPath(path); // https://stackoverflow.com/questions/1796925/how-to-import-a-file-by-its-full-path-using-c-api - name = PyUnicode_FromString((char*)"textgen"); - load_module = PyImport_Import(name); - - - - if (load_module != NULL) - { - func = PyObject_GetAttrString(load_module, (char*)"func"); // Set our custom function to call - //args = PyTuple_Pack(1, PyUnicode_FromString((char*)"hello")); - args = PyTuple_Pack(1, PyUnicode_FromString(gi.argv(2))); // Set the arguments to send - - callfunc = PyObject_CallObject(func, args); // Call our custom python function - if (callfunc != NULL) - { - const char* str = _PyUnicode_AsString(callfunc); - Com_Printf("%s %s\n", __func__, str); - } -} - - Py_Finalize(); - } - else if (Q_stricmp(cmd, "chatfile") == 0) - { - char filename[128]; -#ifdef _WIN32 - sprintf(filename, ".\\action\\bots\\textchat.txt"); -#else - strcpy(filename, "./action/bots/textchat.txt"); -#endif - - // If the file exists and NOT empty, the file is considered 'in use' - FILE* file = fopen(filename, "r"); - if (file != NULL) - { - fseek(file, 0L, SEEK_END); - long size = ftell(file); - if (size > 0) - { - Com_Printf("File in use: %s\n", filename); - fclose(file); // Close the file after use - return; - } - } - - // Write to file - file = fopen(filename, "w"); // Open the file in write mode - if (file == NULL) { - Com_Printf("Could not write to file %s\n", filename); - return; - } - fprintf(file, "%s\n%s", gi.argv(2), gi.argv(3)); // Write to the file - fclose(file); // Close the file after use - - /* - srand((unsigned int)time(NULL)); // Seed the random number generator - - const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - char key[7]; // Six characters + null terminator - for (int i = 0; i < 6; ++i) { - int index = rand() % (sizeof(charset) - 1); - key[i] = charset[index]; - } - key[6] = '\0'; // Null-terminate the string - - char playerName[256], chatMessage[256]; - - printf("Enter your name: "); - scanf("%s", playerName); - - printf("Enter your chat message: "); - getchar(); // Consume newline left by previous input - fgets(chatMessage, sizeof(chatMessage), stdin); // Read the whole line including spaces - - fprintf(file, "%s\n%s\n%s", key, playerName, chatMessage); // Write to the file - - fclose(file); // Close the file after use - */ - } -#endif - //rekkie -- python chatbot -- e - - return false; -} - -// Bot commands -qboolean BOTLIB_Commands(edict_t* ent) -{ - char* cmd = gi.argv(0); - - //rekkie -- BSP -- s - // if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - // { - // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - // return true; - // } - // else - if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS - { - ACEND_SaveAAS(true); - return true; - } - else if (Q_stricmp(cmd, "an") == 0) // add node - { - ACEND_BSP(ent); - return true; - } - else if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names - { - BOTLIB_RandomizeTeamNames(ent); - return true; - } - else if (Q_stricmp(cmd, "randomize_team_skins") == 0) // Manually randomize team skins - { - BOTLIB_RandomizeTeamSkins(ent); - return true; - } - //rekkie -- BSP -- e - /* - else if (Q_stricmp(cmd, "nav_vis") == 0) // Manually create a navigation vis network - { - if (dedicated->value) - return true; - - BOTLIB_GenerateNodeVis(ent); - - return true; - } - */ - else if (Q_stricmp(cmd, "nav_toggle") == 0) // Toggles display between: node editing, both pathing, and neither - { - if (dedicated->value) - return true; - - //bot_showpath->value = !bot_showpath->value; // Toggle path display - if (bot_showpath->value == 0 && ent->bot.walknode.enabled == false) - { - ent->bot.walknode.enabled = true; // Turn on nav editor - bot_showpath->value = 0; // Turn on path display - - } - else if (bot_showpath->value == 0 && ent->bot.walknode.enabled == true) - { - ent->bot.walknode.enabled = false; // Turn off nav editor - bot_showpath->value = 1; // Turn on path display - } - else - { - ent->bot.walknode.enabled = false; // Turn off nav editor - bot_showpath->value = 0; // Turn off path display - } - return true; - } - else if (Q_stricmp(cmd, "nav_edit") == 0) // Turn nav editor on/off - { - if (dedicated->value) - return true; - ent->bot.walknode.enabled = !ent->bot.walknode.enabled; - return true; - } - - else if (Q_stricmp(cmd, "nav_edges") == 0) // Print out all the area edges - { - if (rand() % 2) - BOTLIB_CanGotoNode(ent, nodes[2505].nodenum, 0); - else - BOTLIB_CanGotoNode(ent, nodes[999].nodenum, 0); - - /* - int goal_node = INVALID; - if (gi.argc() >= 2) - goal_node = atoi(gi.argv(1)); - - BOTLIB_CanGotoNode(ent, goal_node, false); - */ - return true; - } - - else if (Q_stricmp(cmd, "nav_go") == 0) // Test function - { - BOTLIB_GetNextAreaNode(ent); - if (ent->bot.next_area_node != INVALID) - VectorCopy(tv(nodes[ent->bot.next_area_node].origin[0], nodes[ent->bot.next_area_node].origin[1], nodes[ent->bot.next_area_node].origin[2] + 32), ent->s.origin); // Teleport to node - return true; - } - else if (Q_stricmp(cmd, "nav_aa") == 0) // Automatically generate nav areas - { - if (numnodes) - { - BOTLIB_AutoArea(ent); - } - return true; - } - else if (Q_stricmp(cmd, "nav_area") == 0) // Set the area num - { - if (dedicated->value) - return true; - - if (gi.argc() >= 2) - { - if (ent->bot.walknode.enabled) - { - ent->bot.walknode.selection_area = atoi(gi.argv(1)); - - // Force an area color - int red; - int green; - int blue; - if (gi.argc() == 5) - { - red = atoi(gi.argv(2)); - green = atoi(gi.argv(3)); - blue = atoi(gi.argv(4)); - - // Sanity check -- randomize color if it's malformed - if (red < 0 || red > 255) - red = rand() % 255; - if (green < 0 || green > 255) - green = rand() % 255; - if (blue < 0 || blue > 255) - blue = rand() % 255; - } - else // Randomize color if no color picked - { - red = rand() % 255; - green = rand() % 255; - blue = rand() % 255; - } - - ent->bot.walknode.selection_area_color = MakeColor(red, green, blue, 255); - - // Reset all area nodes to zero - if (ent->bot.walknode.selection_area < 0) - { - Com_Printf("Nav area resetting all nodes to: 0\n"); - ent->bot.walknode.selection_area = 0; - - for (int i = 0; i < numnodes; i++) - { - nodes[i].area = 0; - nodes[i].area_color = 0; - } - } - else // Try setting area node - { - // Check if selected node is disconnected from an existing area (areas must not be disconnected islands) - int node = 0; - int target_node = 0; - qboolean area_is_linked = false; - qboolean area_is_new = false; - //if (ent->bot.walknode.selection_area_used) - { - // 1) Check if our area num exists - qboolean area_exists = false; - for (int n = 0; n < numnodes; n++) - { - if (nodes[n].area == ent->bot.walknode.selection_area) - { - area_exists = true; - break; - } - } - - // 2) If it doesn't exist, then mark as new area. No need to worry about connecting because we're making a new area. - if (area_exists == false) - area_is_new = true; - - // 3) If the area num exists, see if any of our selected nodes are connected to it - else if (area_exists) - { - for (int i = 0; i < ent->bot.walknode.selection_node_count; i++) - { - node = ent->bot.walknode.selection_nodes[i]; - - // Check if any of the selected nodes are connected to our desired selection_area - if (nodes[node].area == ent->bot.walknode.selection_area) - { - area_is_linked = true; - break; - } - - // Check if any of their links connect to our desired selection_area - for (int l = 0; l < nodes[node].num_links; l++) - { - target_node = nodes[node].links[l].targetNode; - if (nodes[target_node].area == ent->bot.walknode.selection_area) - { - area_is_linked = true; - break; - } - } - if (area_is_linked) - break; - } - } - - // 4) Optionally select surrounding nodes and include them in the set if: - // * Nodes are area: 0 - // * Can connect to the selected nodes without going through another area - // * Similar surface normal (and/or height?) - //if (area_exists && (area_is_linked || area_is_new)) - { - //if (ent->bot.walknode.selection_node_count) - // BOTLIB_GroupConnectedNodeArea(ent, ent->bot.walknode.selection_nodes[0]); - } - } - - if (area_is_linked || area_is_new || ent->bot.walknode.selection_area == 0) - { - ent->bot.walknode.selection_area_used = true; // Flag that we desire to change the area - Com_Printf("Nav area set to: %d\n", ent->bot.walknode.selection_area); - } - else - { - ent->bot.walknode.selection_area_used = false; // Don't change area - Com_Printf("Found existing area %d but the selected nodes do not connect to it.\nAreas of the same number need to be connected together, they cannot be disconnected.\n", ent->bot.walknode.selection_area); - } - } - } - } - else - { - gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); - gi.cprintf(NULL, PRINT_HIGH, "Use: sets the area number of any selected nodes \n"); - gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); - gi.cprintf(NULL, PRINT_HIGH, "nav_area \n"); - gi.cprintf(NULL, PRINT_HIGH, "OPTIONAL: nav_area \n"); - gi.cprintf(NULL, PRINT_HIGH, "Use '-1' to remove all areas from nodes\n"); - gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); - } - return true; - } - else if (Q_stricmp(cmd, "nav_load") == 0) // Load bot nav from file - { -#ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); -#else - BOTLIB_LoadNav(); -#endif - return true; - } - else if (Q_stricmp(cmd, "nav_save") == 0) // Save bot nav to file - { -#ifdef USE_ZLIB - BOTLIB_SaveNavCompressed(); -#else - BOTLIB_SaveNav(); -#endif - return true; - } - else if (Q_stricmp(cmd, "nav_autogen") == 0) // Auto generate navigation that propagates from spawn points - { - BOTLIB_SelfExpandNodesFromSpawnpoints(ent); - BOTLIB_LinkAllNodesTogether(ent); - return true; - } - //rekkie -- surface data -- s - else if (Q_stricmp(cmd, "nav") == 0) // add navigation - { - BOTLIB_InitNavigation(ent); - return true; - -#if 0 - ent->nav = gi.Nav(); // Grant access to navigation data - if (ent->nav == NULL) - { - gi.dprintf("%s failed to import NAV data\n", __func__); - } - else - { - Com_Printf("%s NAV faces %d ignored %d\n", __func__, ent->nav->faces_total, ent->nav->ignored_faces_total); - ACEND_SaveAAS(false); - } - - return true; -#endif - } - //rekkie -- surface data -- e - - /* - else if (Q_stricmp(cmd, "test") == 0) // Test func -- - { - char buffer[256]; - sprintf(buffer, "%c Bandaging [ %d%c ] %c", '\x04', ent->health, '\x05', '\x04'); - Com_Printf("%s %s\n", __func__, buffer); - } - */ - else if (Q_stricmp(cmd, "flag") == 0) // Test func -- grab a flag - { - if (ctf->value) - { - int team = atoi(gi.argv(1)); - - if (team == TEAM1) - { - gi.unlinkentity(bot_ctf_status.flag1); - VectorCopy(ent->s.origin, bot_ctf_status.flag1->s.origin); - gi.linkentity(bot_ctf_status.flag1); - } - if (team == TEAM2) - { - gi.unlinkentity(bot_ctf_status.flag2); - VectorCopy(ent->s.origin, bot_ctf_status.flag2->s.origin); - gi.linkentity(bot_ctf_status.flag2); - } - - return true; - } - } - // else if (Q_stricmp(cmd, "head") == 0) // Test func -- spawn kickable head - // { - // ThrowGibbedHead(ent, 50); // Spawn kickable head - // return true; - // } - // else if (Q_stricmp(cmd, "ball") == 0) // Test func -- spawn kickable ball - // { - // QPong_Ball(ent, 50); - // return true; - // } - else if (Q_stricmp(cmd, "dc_add_sp") == 0) // Add (or recycle unused) custom spawn point - { - DC_Add_Spawnpoint(ent); // This will add a custom spawn point at the player's location - return true; - } - else if (Q_stricmp(cmd, "dc_remove_sp") == 0) // Remove custom spawn point - { - DC_Remove_Spawnpoint(ent); // This will remove a custom spawn point at the player's location - return true; - } - else if (Q_stricmp(cmd, "dc_save_sp") == 0) // Save spawn points to file - { - DC_Save_Spawnpoints(); // This will save the map and user added spawn points to file - return true; - } - else if (Q_stricmp(cmd, "bot_show_spawns") == 0) // Show spawn points - { - BOTLIB_Show_Spawnpoints(); // This will show the map and user added spawn points - return true; - } - else if (Q_stricmp(cmd, "removenode") == 0 && debug_mode) - ACEND_RemoveNode(ent, atoi(gi.argv(1))); - else if (Q_stricmp(cmd, "gtnode") == 0 && debug_mode) // Goto node - forces all bots to goto node num - { - int node = atoi(gi.argv(1)); - if (node > 0 && node < numnodes) - { - for (int i = 0; i <= num_players; i++) - { - if (players[i] && players[i]->is_bot && players[i]->health > 0) - { - if (BOTLIB_CanGotoNode(players[i], nodes[node].nodenum, false)) - { - Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, nodes[node].nodenum); - //players[i]->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(players[i], nodes[node].nodenum); - } - } - } - } - else if (ent->bot.current_node > 0) // Goto player location - { - for (int i = 0; i <= num_players; i++) - { - if (players[i] && players[i]->is_bot && players[i]->health > 0) - { - if (BOTLIB_CanGotoNode(players[i], ent->bot.current_node, false)) - { - Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, ent->bot.current_node); - //players[i]->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(players[i], ent->bot.current_node); - } - } - } - } - - return true; - } - else if (Q_stricmp(cmd, "ttnode") == 0 && debug_mode) // Teleport player to node location - { - int node = atoi(gi.argv(1)); - - if (node > INVALID && node < numnodes) - { - Com_Printf("%s Teleporting to node %i [%f %f %f]\n", __func__, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); - VectorCopy(nodes[node].origin, ent->s.origin); - } - - return true; - } - else if (Q_stricmp(cmd, "tbtplayer") == 0 && debug_mode) // Teleport random bot to player location - { - for (int i = 0; i <= num_players; i++) - { - if (players[i] && players[i]->is_bot && players[i]->health > 0) - { - Com_Printf("%s Teleporting %s to player %s [%f %f %f]\n", __func__, players[i]->client->pers.netname, ent->client->pers.netname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); - VectorCopy(ent->s.origin, players[i]->s.origin); - return true; - } - } - - return true; - } - else if (Q_stricmp(cmd, "tbtnode") == 0 && debug_mode) // Teleport random bot to node location - { - int node = atoi(gi.argv(1)); - - if (node > INVALID && node < numnodes) - { - for (int i = 0; i <= num_players; i++) - { - if (players[i] && players[i]->is_bot && players[i]->health > 0) - { - Com_Printf("%s Teleporting %s to node %i [%f %f %f]\n", __func__, players[i]->client->pers.netname, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); - VectorCopy(nodes[node].origin, players[i]->s.origin); - return true; - } - } - } - - return true; - } - else if (Q_stricmp(cmd, "ttloc") == 0 && debug_mode) // Teleport to location - { - float x = atof(gi.argv(1)); - float y = atof(gi.argv(2)); - float z = atof(gi.argv(3)); - - Com_Printf("%s Teleporting to location [%f %f %f]\n", __func__, x, y, z); - ent->s.origin[0] = x; - ent->s.origin[1] = y; - ent->s.origin[2] = z; - - return true; - } - else if (Q_stricmp(cmd, "bnode") == 0 && debug_mode) // Break all links to node - { - int node = atoi(gi.argv(1)); - - if (node > 0 && node < numnodes) - BOTLIB_RemoveAllNodeLinksFrom(node); - - return true; - } - else if (Q_stricmp(cmd, "cnode") == 0 && debug_mode) // Convert node - { - int node = atoi(gi.argv(1)); - int type = atoi(gi.argv(2)); - - if (node > 0 && node < numnodes) - { - if (type == NODE_MOVE || type == NODE_JUMPPAD || type == NODE_POI) - { - Com_Printf("%s Converting node %i from type %i to %i\n", __func__, node, nodes[node].type, type); - nodes[node].type = type; - - // Update node's ent details like color - edict_t* ent; - for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) - { - if (ent && ent->node_num == node) - { - if (type == NODE_MOVE) - ent->s.renderfx = RF_SHELL_BLUE; - else if (type == NODE_JUMPPAD) - ent->s.renderfx = RF_SHELL_GREEN; - else if (type == NODE_POI) - ent->s.renderfx = RF_SHELL_RED; - - break; - } - } - } - } - - return true; - } - else if (Q_stricmp(cmd, "bvnodes") == 0 && debug_mode) // Build vis nodes - { - ACEND_BuildVisibilityNodes(); - return true; - } - else if (Q_stricmp(cmd, "xynode") == 0 && debug_mode) // Can node X see node Y - { - int x = atoi(gi.argv(1)); - int y = atoi(gi.argv(2)); - - if (x <= 0 || y <= 0 || x != y) - { - Com_Printf("Usage: xynode \n"); - return true; - } - - if (ACEND_IsNodeVisibleToNodes(x, y)) - Com_Printf("%s Node %i can see node %i\n", __func__, x, y); - else - Com_Printf("%s Node %i cannot see node %i\n", __func__, x, y); - - return true; - } - else if (Q_stricmp(cmd, "xrngnode") == 0 && debug_mode) // Random node X can see - { - int x = atoi(gi.argv(1)); - - if (x <= 0) - { - Com_Printf("Usage: xrngnode \n"); - return true; - } - - Com_Printf("%s Node %i can see random visible node %i\n", __func__, x, ACEND_GetRandomVisibleNode(x)); - - return true; - } - //rekkie -- DEV_1 -- e - //rekkie -- BSP -- e - - else - return false; - - return true; +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Bot server commands +qboolean BOTLIB_SV_Cmds(void) +{ + char* cmd; + cmd = gi.argv(1); + + if (Q_stricmp(cmd, "bots") == 0) + { + int cc = gi.argc(); + + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added + + // sv bots + if (gi.argc() == 3) // Adding a multiple bots + { + bot_connections.desire_bots = atoi(gi.argv(2)); // How many bots + bot_connections.auto_balance_bots = true; + return true; + } + + // sv bots + else if (gi.argc() == 4) // Adding a multiple bots to a select team + { + int count = atoi(gi.argv(2)); // How many bots + int team = atoi(gi.argv(3)); // What team + + //if (count == 0) + // count = -1; // Flag to remove all bots from team + + bot_connections.auto_balance_bots = false; + + // Deathmatch + if (teamplay->value == 0) + { + if (count == 0) + { + BOTLIB_RemoveBot("ALL"); + return true; + } + else + { + bot_connections.desire_bots = count; + bot_connections.auto_balance_bots = true; + return true; + } + } + else // Teamplay + { + if (team == TEAM1) bot_connections.desire_team1 = count; + else if (team == TEAM2) bot_connections.desire_team2 = count; + else if (team == TEAM3) bot_connections.desire_team3 = count; + + return true; + } + } + + else + { + gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "Use: sets the number of bots to join a game\n"); + gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "sv bots \n"); + gi.cprintf(NULL, PRINT_HIGH, "sv bots [team]\n"); + if (teamplay->value == 0) + gi.cprintf(NULL, PRINT_HIGH, "DM bot total [%d]\n", bot_connections.total_bots); + else if (use_3teams->value) + gi.cprintf(NULL, PRINT_HIGH, "3TEAM bots on T1[%d] T2[%d] T3[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots); + else if (ctf->value) + gi.cprintf(NULL, PRINT_HIGH, "CTF bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); + else if (teamplay->value) + gi.cprintf(NULL, PRINT_HIGH, "TP bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); + gi.cprintf(NULL, PRINT_HIGH, "-------------------------------\n"); + } + + return true; + } + + if (Q_stricmp(cmd, "addbot") == 0) + { + + return true; + } + else if (Q_stricmp(cmd, "addbots") == 0) + { + if (gi.argc() >= 3) + { + int count = atoi(gi.argv(2)), i = 0; + for (i = 0; i < count; i++) + { + } + } + else + gi.cprintf(NULL, PRINT_HIGH, "Usage: sv addbots []\n"); + + return true; + } + // removebot + else if (Q_stricmp(cmd, "removebot") == 0) + { + ACESP_RemoveBot(gi.argv(2)); + return true; + } + else if (Q_stricmp(cmd, "removebots") == 0) + { + ACESP_RemoveBot("all"); + return true; + } + //rekkie -- BSP -- s + // else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + // { + // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + // return true; + // } + // // Process BSP and generate AAS reachability with nodes + // else if (Q_stricmp(cmd, "aas") == 0) + // { + // ACEND_BSP(NULL); + // return true; + // } + //rekkie -- BSP -- e + + //rekkie -- python chatbot -- s +#if 0 + // + // Allows Quake 2 to communicate with a LLM (Large Language Model) + // [Input] Quake 2 ==> textgen.py ==> text-generation-webui (server) + // [Output] text-generation-webui (server) ==> textgen.py ==> Quake 2 + // + // Idea was taken from: https://www.youtube.com/watch?v=ndLqg4RYcXE (Full Tutorial of Calling Python Functions(.py) in C/C++(.c or .cpp) using (python.h)) + // + // This runs the python file ./action/bots/textgen.py + // The py file allows communcation with an ai chat server called text-generation-webui (github: https://github.com/oobabooga/text-generation-webui) + // + // text-generation-webui must be run with OpenAI API turned on (port 5000) + // + // From the C end, we require a few includes and libs from Python. + // NOTE: When installing python, make sure (Download debug binaries) is ticked, + // otherwise the compiler will complain about "python310_d.lib" missing. + // https://github.com/pybind/pybind11/issues/3403 + // + // Includes + // C:\Users\...\AppData\Local\Programs\Python\Python310\include + // + // Linker (libs) + // C:\Users\...\AppData\Local\Programs\Python\Python310\libs\python310.lib + // C:\Users\...\AppData\Local\Programs\Python\Python310\libs + else if (Q_stricmp(cmd, "chat") == 0) + { + Py_Initialize(); // Init python + + PyObject* name, * load_module, * func, * callfunc, * args; + + /* + //https://groups.google.com/g/comp.lang.python/c/14Yl3C8suKI?pli=1 + // https://gist.github.com/Coderx7/509a1aaf154a8d2532a6459a57308a63 + PyObject* mainmod = PyImport_AddModule("__main__"); + Py_INCREF(mainmod); + PyObject* ns = PyModule_GetDict(mainmod); + Py_INCREF(ns); + PyObject* timeModule = PyImport_ImportModuleEx("requests", ns, ns, NULL); + */ + + //PyObject* module = PyImport_AddModule("requests"); + PyObject* pModule = PyImport_ImportModule("requests"); // The python imports (imports used in textgen.py) + + // Open our custom python file + const wchar_t* path = L".\\action\\bots\\"; // https://stackoverflow.com/questions/31314767/call-multiple-python-functions-from-different-directories + PySys_SetPath(path); // https://stackoverflow.com/questions/1796925/how-to-import-a-file-by-its-full-path-using-c-api + name = PyUnicode_FromString((char*)"textgen"); + load_module = PyImport_Import(name); + + + + if (load_module != NULL) + { + func = PyObject_GetAttrString(load_module, (char*)"func"); // Set our custom function to call + //args = PyTuple_Pack(1, PyUnicode_FromString((char*)"hello")); + args = PyTuple_Pack(1, PyUnicode_FromString(gi.argv(2))); // Set the arguments to send + + callfunc = PyObject_CallObject(func, args); // Call our custom python function + if (callfunc != NULL) + { + const char* str = _PyUnicode_AsString(callfunc); + Com_Printf("%s %s\n", __func__, str); + } +} + + Py_Finalize(); + } + else if (Q_stricmp(cmd, "chatfile") == 0) + { + char filename[128]; +#ifdef _WIN32 + sprintf(filename, ".\\action\\bots\\textchat.txt"); +#else + strcpy(filename, "./action/bots/textchat.txt"); +#endif + + // If the file exists and NOT empty, the file is considered 'in use' + FILE* file = fopen(filename, "r"); + if (file != NULL) + { + fseek(file, 0L, SEEK_END); + long size = ftell(file); + if (size > 0) + { + Com_Printf("File in use: %s\n", filename); + fclose(file); // Close the file after use + return; + } + } + + // Write to file + file = fopen(filename, "w"); // Open the file in write mode + if (file == NULL) { + Com_Printf("Could not write to file %s\n", filename); + return; + } + fprintf(file, "%s\n%s", gi.argv(2), gi.argv(3)); // Write to the file + fclose(file); // Close the file after use + + /* + srand((unsigned int)time(NULL)); // Seed the random number generator + + const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + char key[7]; // Six characters + null terminator + for (int i = 0; i < 6; ++i) { + int index = rand() % (sizeof(charset) - 1); + key[i] = charset[index]; + } + key[6] = '\0'; // Null-terminate the string + + char playerName[256], chatMessage[256]; + + printf("Enter your name: "); + scanf("%s", playerName); + + printf("Enter your chat message: "); + getchar(); // Consume newline left by previous input + fgets(chatMessage, sizeof(chatMessage), stdin); // Read the whole line including spaces + + fprintf(file, "%s\n%s\n%s", key, playerName, chatMessage); // Write to the file + + fclose(file); // Close the file after use + */ + } +#endif + //rekkie -- python chatbot -- e + + return false; +} + +// Bot commands +qboolean BOTLIB_Commands(edict_t* ent) +{ + char* cmd = gi.argv(0); + + //rekkie -- BSP -- s + // if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS + // { + // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist + // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist + // return true; + // } + // else + if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS + { + ACEND_SaveAAS(true); + return true; + } + else if (Q_stricmp(cmd, "an") == 0) // add node + { + ACEND_BSP(ent); + return true; + } + else if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names + { + BOTLIB_RandomizeTeamNames(ent); + return true; + } + else if (Q_stricmp(cmd, "randomize_team_skins") == 0) // Manually randomize team skins + { + BOTLIB_RandomizeTeamSkins(ent); + return true; + } + //rekkie -- BSP -- e + /* + else if (Q_stricmp(cmd, "nav_vis") == 0) // Manually create a navigation vis network + { + if (dedicated->value) + return true; + + BOTLIB_GenerateNodeVis(ent); + + return true; + } + */ + else if (Q_stricmp(cmd, "nav_toggle") == 0) // Toggles display between: node editing, both pathing, and neither + { + if (dedicated->value) + return true; + + //bot_showpath->value = !bot_showpath->value; // Toggle path display + if (bot_showpath->value == 0 && ent->bot.walknode.enabled == false) + { + ent->bot.walknode.enabled = true; // Turn on nav editor + bot_showpath->value = 0; // Turn on path display + + } + else if (bot_showpath->value == 0 && ent->bot.walknode.enabled == true) + { + ent->bot.walknode.enabled = false; // Turn off nav editor + bot_showpath->value = 1; // Turn on path display + } + else + { + ent->bot.walknode.enabled = false; // Turn off nav editor + bot_showpath->value = 0; // Turn off path display + } + return true; + } + else if (Q_stricmp(cmd, "nav_edit") == 0) // Turn nav editor on/off + { + if (dedicated->value) + return true; + ent->bot.walknode.enabled = !ent->bot.walknode.enabled; + return true; + } + + else if (Q_stricmp(cmd, "nav_edges") == 0) // Print out all the area edges + { + if (rand() % 2) + BOTLIB_CanGotoNode(ent, nodes[2505].nodenum, 0); + else + BOTLIB_CanGotoNode(ent, nodes[999].nodenum, 0); + + /* + int goal_node = INVALID; + if (gi.argc() >= 2) + goal_node = atoi(gi.argv(1)); + + BOTLIB_CanGotoNode(ent, goal_node, false); + */ + return true; + } + + else if (Q_stricmp(cmd, "nav_go") == 0) // Test function + { + BOTLIB_GetNextAreaNode(ent); + if (ent->bot.next_area_node != INVALID) + VectorCopy(tv(nodes[ent->bot.next_area_node].origin[0], nodes[ent->bot.next_area_node].origin[1], nodes[ent->bot.next_area_node].origin[2] + 32), ent->s.origin); // Teleport to node + return true; + } + else if (Q_stricmp(cmd, "nav_aa") == 0) // Automatically generate nav areas + { + if (numnodes) + { + BOTLIB_AutoArea(ent); + } + return true; + } + else if (Q_stricmp(cmd, "nav_area") == 0) // Set the area num + { + if (dedicated->value) + return true; + + if (gi.argc() >= 2) + { + if (ent->bot.walknode.enabled) + { + ent->bot.walknode.selection_area = atoi(gi.argv(1)); + + // Force an area color + int red; + int green; + int blue; + if (gi.argc() == 5) + { + red = atoi(gi.argv(2)); + green = atoi(gi.argv(3)); + blue = atoi(gi.argv(4)); + + // Sanity check -- randomize color if it's malformed + if (red < 0 || red > 255) + red = rand() % 255; + if (green < 0 || green > 255) + green = rand() % 255; + if (blue < 0 || blue > 255) + blue = rand() % 255; + } + else // Randomize color if no color picked + { + red = rand() % 255; + green = rand() % 255; + blue = rand() % 255; + } + + ent->bot.walknode.selection_area_color = MakeColor(red, green, blue, 255); + + // Reset all area nodes to zero + if (ent->bot.walknode.selection_area < 0) + { + Com_Printf("Nav area resetting all nodes to: 0\n"); + ent->bot.walknode.selection_area = 0; + + for (int i = 0; i < numnodes; i++) + { + nodes[i].area = 0; + nodes[i].area_color = 0; + } + } + else // Try setting area node + { + // Check if selected node is disconnected from an existing area (areas must not be disconnected islands) + int node = 0; + int target_node = 0; + qboolean area_is_linked = false; + qboolean area_is_new = false; + //if (ent->bot.walknode.selection_area_used) + { + // 1) Check if our area num exists + qboolean area_exists = false; + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].area == ent->bot.walknode.selection_area) + { + area_exists = true; + break; + } + } + + // 2) If it doesn't exist, then mark as new area. No need to worry about connecting because we're making a new area. + if (area_exists == false) + area_is_new = true; + + // 3) If the area num exists, see if any of our selected nodes are connected to it + else if (area_exists) + { + for (int i = 0; i < ent->bot.walknode.selection_node_count; i++) + { + node = ent->bot.walknode.selection_nodes[i]; + + // Check if any of the selected nodes are connected to our desired selection_area + if (nodes[node].area == ent->bot.walknode.selection_area) + { + area_is_linked = true; + break; + } + + // Check if any of their links connect to our desired selection_area + for (int l = 0; l < nodes[node].num_links; l++) + { + target_node = nodes[node].links[l].targetNode; + if (nodes[target_node].area == ent->bot.walknode.selection_area) + { + area_is_linked = true; + break; + } + } + if (area_is_linked) + break; + } + } + + // 4) Optionally select surrounding nodes and include them in the set if: + // * Nodes are area: 0 + // * Can connect to the selected nodes without going through another area + // * Similar surface normal (and/or height?) + //if (area_exists && (area_is_linked || area_is_new)) + { + //if (ent->bot.walknode.selection_node_count) + // BOTLIB_GroupConnectedNodeArea(ent, ent->bot.walknode.selection_nodes[0]); + } + } + + if (area_is_linked || area_is_new || ent->bot.walknode.selection_area == 0) + { + ent->bot.walknode.selection_area_used = true; // Flag that we desire to change the area + Com_Printf("Nav area set to: %d\n", ent->bot.walknode.selection_area); + } + else + { + ent->bot.walknode.selection_area_used = false; // Don't change area + Com_Printf("Found existing area %d but the selected nodes do not connect to it.\nAreas of the same number need to be connected together, they cannot be disconnected.\n", ent->bot.walknode.selection_area); + } + } + } + } + else + { + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "Use: sets the area number of any selected nodes \n"); + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + gi.cprintf(NULL, PRINT_HIGH, "nav_area \n"); + gi.cprintf(NULL, PRINT_HIGH, "OPTIONAL: nav_area \n"); + gi.cprintf(NULL, PRINT_HIGH, "Use '-1' to remove all areas from nodes\n"); + gi.cprintf(NULL, PRINT_HIGH, "------------------------------------------------\n"); + } + return true; + } + else if (Q_stricmp(cmd, "nav_load") == 0) // Load bot nav from file + { +#ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); +#else + BOTLIB_LoadNav(); +#endif + return true; + } + else if (Q_stricmp(cmd, "nav_save") == 0) // Save bot nav to file + { +#ifdef USE_ZLIB + BOTLIB_SaveNavCompressed(); +#else + BOTLIB_SaveNav(); +#endif + return true; + } + else if (Q_stricmp(cmd, "nav_autogen") == 0) // Auto generate navigation that propagates from spawn points + { + BOTLIB_SelfExpandNodesFromSpawnpoints(ent); + BOTLIB_LinkAllNodesTogether(ent); + return true; + } + //rekkie -- surface data -- s + else if (Q_stricmp(cmd, "nav") == 0) // add navigation + { + BOTLIB_InitNavigation(ent); + return true; + +#if 0 + ent->nav = gi.Nav(); // Grant access to navigation data + if (ent->nav == NULL) + { + gi.dprintf("%s failed to import NAV data\n", __func__); + } + else + { + Com_Printf("%s NAV faces %d ignored %d\n", __func__, ent->nav->faces_total, ent->nav->ignored_faces_total); + ACEND_SaveAAS(false); + } + + return true; +#endif + } + //rekkie -- surface data -- e + + /* + else if (Q_stricmp(cmd, "test") == 0) // Test func -- + { + char buffer[256]; + sprintf(buffer, "%c Bandaging [ %d%c ] %c", '\x04', ent->health, '\x05', '\x04'); + Com_Printf("%s %s\n", __func__, buffer); + } + */ + else if (Q_stricmp(cmd, "flag") == 0) // Test func -- grab a flag + { + if (ctf->value) + { + int team = atoi(gi.argv(1)); + + if (team == TEAM1) + { + gi.unlinkentity(bot_ctf_status.flag1); + VectorCopy(ent->s.origin, bot_ctf_status.flag1->s.origin); + gi.linkentity(bot_ctf_status.flag1); + } + if (team == TEAM2) + { + gi.unlinkentity(bot_ctf_status.flag2); + VectorCopy(ent->s.origin, bot_ctf_status.flag2->s.origin); + gi.linkentity(bot_ctf_status.flag2); + } + + return true; + } + } + // else if (Q_stricmp(cmd, "head") == 0) // Test func -- spawn kickable head + // { + // ThrowGibbedHead(ent, 50); // Spawn kickable head + // return true; + // } + // else if (Q_stricmp(cmd, "ball") == 0) // Test func -- spawn kickable ball + // { + // QPong_Ball(ent, 50); + // return true; + // } + else if (Q_stricmp(cmd, "dc_add_sp") == 0) // Add (or recycle unused) custom spawn point + { + DC_Add_Spawnpoint(ent); // This will add a custom spawn point at the player's location + return true; + } + else if (Q_stricmp(cmd, "dc_remove_sp") == 0) // Remove custom spawn point + { + DC_Remove_Spawnpoint(ent); // This will remove a custom spawn point at the player's location + return true; + } + else if (Q_stricmp(cmd, "dc_save_sp") == 0) // Save spawn points to file + { + DC_Save_Spawnpoints(); // This will save the map and user added spawn points to file + return true; + } + else if (Q_stricmp(cmd, "bot_show_spawns") == 0) // Show spawn points + { + BOTLIB_Show_Spawnpoints(); // This will show the map and user added spawn points + return true; + } + else if (Q_stricmp(cmd, "removenode") == 0 && debug_mode) + ACEND_RemoveNode(ent, atoi(gi.argv(1))); + else if (Q_stricmp(cmd, "gtnode") == 0 && debug_mode) // Goto node - forces all bots to goto node num + { + int node = atoi(gi.argv(1)); + if (node > 0 && node < numnodes) + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + if (BOTLIB_CanGotoNode(players[i], nodes[node].nodenum, false)) + { + Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, nodes[node].nodenum); + //players[i]->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(players[i], nodes[node].nodenum); + } + } + } + } + else if (ent->bot.current_node > 0) // Goto player location + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + if (BOTLIB_CanGotoNode(players[i], ent->bot.current_node, false)) + { + Com_Printf("%s %s visiting node[%i]\n", __func__, players[i]->client->pers.netname, ent->bot.current_node); + //players[i]->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(players[i], ent->bot.current_node); + } + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "ttnode") == 0 && debug_mode) // Teleport player to node location + { + int node = atoi(gi.argv(1)); + + if (node > INVALID && node < numnodes) + { + Com_Printf("%s Teleporting to node %i [%f %f %f]\n", __func__, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + VectorCopy(nodes[node].origin, ent->s.origin); + } + + return true; + } + else if (Q_stricmp(cmd, "tbtplayer") == 0 && debug_mode) // Teleport random bot to player location + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + Com_Printf("%s Teleporting %s to player %s [%f %f %f]\n", __func__, players[i]->client->pers.netname, ent->client->pers.netname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + VectorCopy(ent->s.origin, players[i]->s.origin); + return true; + } + } + + return true; + } + else if (Q_stricmp(cmd, "tbtnode") == 0 && debug_mode) // Teleport random bot to node location + { + int node = atoi(gi.argv(1)); + + if (node > INVALID && node < numnodes) + { + for (int i = 0; i <= num_players; i++) + { + if (players[i] && players[i]->is_bot && players[i]->health > 0) + { + Com_Printf("%s Teleporting %s to node %i [%f %f %f]\n", __func__, players[i]->client->pers.netname, node, nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2]); + VectorCopy(nodes[node].origin, players[i]->s.origin); + return true; + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "ttloc") == 0 && debug_mode) // Teleport to location + { + float x = atof(gi.argv(1)); + float y = atof(gi.argv(2)); + float z = atof(gi.argv(3)); + + Com_Printf("%s Teleporting to location [%f %f %f]\n", __func__, x, y, z); + ent->s.origin[0] = x; + ent->s.origin[1] = y; + ent->s.origin[2] = z; + + return true; + } + else if (Q_stricmp(cmd, "bnode") == 0 && debug_mode) // Break all links to node + { + int node = atoi(gi.argv(1)); + + if (node > 0 && node < numnodes) + BOTLIB_RemoveAllNodeLinksFrom(node); + + return true; + } + else if (Q_stricmp(cmd, "cnode") == 0 && debug_mode) // Convert node + { + int node = atoi(gi.argv(1)); + int type = atoi(gi.argv(2)); + + if (node > 0 && node < numnodes) + { + if (type == NODE_MOVE || type == NODE_JUMPPAD || type == NODE_POI) + { + Com_Printf("%s Converting node %i from type %i to %i\n", __func__, node, nodes[node].type, type); + nodes[node].type = type; + + // Update node's ent details like color + edict_t* ent; + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + if (ent && ent->node_num == node) + { + if (type == NODE_MOVE) + ent->s.renderfx = RF_SHELL_BLUE; + else if (type == NODE_JUMPPAD) + ent->s.renderfx = RF_SHELL_GREEN; + else if (type == NODE_POI) + ent->s.renderfx = RF_SHELL_RED; + + break; + } + } + } + } + + return true; + } + else if (Q_stricmp(cmd, "bvnodes") == 0 && debug_mode) // Build vis nodes + { + ACEND_BuildVisibilityNodes(); + return true; + } + else if (Q_stricmp(cmd, "xynode") == 0 && debug_mode) // Can node X see node Y + { + int x = atoi(gi.argv(1)); + int y = atoi(gi.argv(2)); + + if (x <= 0 || y <= 0 || x != y) + { + Com_Printf("Usage: xynode \n"); + return true; + } + + if (ACEND_IsNodeVisibleToNodes(x, y)) + Com_Printf("%s Node %i can see node %i\n", __func__, x, y); + else + Com_Printf("%s Node %i cannot see node %i\n", __func__, x, y); + + return true; + } + else if (Q_stricmp(cmd, "xrngnode") == 0 && debug_mode) // Random node X can see + { + int x = atoi(gi.argv(1)); + + if (x <= 0) + { + Com_Printf("Usage: xrngnode \n"); + return true; + } + + Com_Printf("%s Node %i can see random visible node %i\n", __func__, x, ACEND_GetRandomVisibleNode(x)); + + return true; + } + //rekkie -- DEV_1 -- e + //rekkie -- BSP -- e + + else + return false; + + return true; } \ No newline at end of file diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 04d4cd67b..7485bdfe1 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -1,550 +1,550 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" -#include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 - -// Bot wave gestures -void BOTLIB_Wave(edict_t* ent, int type) -{ - if (ent->groundentity == NULL) return; // Don't wave when not on ground - if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked - if (ent->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active - - // Time limit between waves - if (ent->client->resp.lastWave + (1 * HZ) > level.framenum) - return; - ent->client->resp.lastWave = level.framenum; // Update last wave - - - switch (type) - { - case WAVE_FLIPOFF: - SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff - break; - case WAVE_SALUTE: - SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute - break; - case WAVE_TAUNT: - SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt - break; - case WAVE_WAVE: - SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave - break; - case WAVE_POINT: - default: - SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point - break; - } -} - -void BOTLIB_PrecacheRadioSounds() -{ - int i; - char path[MAX_QPATH]; - - //male - for (i = 0; i < bot_numMaleSnds; i++) - { - Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/male/", bot_male_radio_msgs[i].msg); - bot_male_radio_msgs[i].sndIndex = gi.soundindex(path); - } - - //female - for (i = 0; i < bot_numFemaleSnds; i++) - { - Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/female/", bot_female_radio_msgs[i].msg); - bot_female_radio_msgs[i].sndIndex = gi.soundindex(path); - } -} -void BOTLIB_RadioBroadcast(edict_t* ent, edict_t* partner, char* msg) -{ - int j, i, msg_len, numSnds; - edict_t* other; - bot_radio_msg_t* radio_msgs; - int msg_soundIndex = 0; - char msgname_num[8], filteredmsg[48]; - qboolean found = false; - radio_t* radio; - - if (!IS_ALIVE(ent)) - return; - - if (!teamplay->value) - { - if (!DMFLAGS((DF_MODELTEAMS | DF_SKINTEAMS))) - return; // don't allow in a non-team setup... - } - - /* - //TempFile END - //AQ2:TNG Slicer - if (radio_repeat->value) - { //SLIC2 Optimization - if (CheckForRepeat(ent, i) == false) - return; - } - - if (radio_max->value) - { - if (CheckForFlood(ent) == false) - return; - } - */ - - radio = &ent->client->resp.radio; - if (radio->gender) - { - radio_msgs = bot_female_radio_msgs; - numSnds = bot_numFemaleSnds; - } - else - { - radio_msgs = bot_male_radio_msgs; - numSnds = bot_numMaleSnds; - } - - i = found = 0; - msg_len = 0; - - Q_strncpyz(filteredmsg, msg, sizeof(filteredmsg)); - - for (i = 0; i < numSnds; i++) - { - if (!Q_stricmp(radio_msgs[i].msg, filteredmsg)) - { - found = true; - msg_soundIndex = radio_msgs[i].sndIndex; - msg_len = radio_msgs[i].length; - break; - } - } - if (!found) - { - Com_Printf("%s '%s' is not a valid radio message", __func__, filteredmsg); - return; - } - - //TempFile BEGIN - if (Q_stricmp(filteredmsg, "enemyd") == 0) - { - if (ent->client->radio_num_kills > 1 && ent->client->radio_num_kills <= 10) - { - // If we are reporting enemy down, add the number of kills. - sprintf(msgname_num, "%i", ent->client->radio_num_kills); - ent->client->radio_num_kills = 0; // prevent from getting into an endless loop - - BOTLIB_RadioBroadcast(ent, partner, msgname_num); // Now THAT'S recursion! =) - } - ent->client->radio_num_kills = 0; - } - - if (partner) - { - BOTLIB_AddRadioMsg(&partner->client->resp.radio, msg_soundIndex, msg_len, ent); - return; - } - - //AQ2:TNG END - for (j = 1; j <= game.maxclients; j++) - { - other = &g_edicts[j]; - if (!other->inuse) - continue; - if (!other->client) - continue; - //Com_Printf("%s [%d]%s [%d]%s\n", __func__, ent->client->resp.team, ent->client->pers.netname, other->client->resp.team, other->client->pers.netname); //ent1->client->resp.team == ent2->client->resp.team; - if (!OnSameTeam(ent, other)) - continue; - BOTLIB_AddRadioMsg(&other->client->resp.radio, msg_soundIndex, msg_len, ent); - } -} - -// Allow bots to send a chat message to players -void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) -{ - int j, /*i,*/ offset_of_text; - edict_t* other; - char text[2048]; - //gclient_t *cl; - - - if (!teamplay->value) - return; - - if (ent->client->resp.team == NOTEAM) - return; - - if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) - { - if (team_message) - Q_snprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team - else - Q_snprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all - } - else if (team_message) - Q_snprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team - else - Q_snprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all - - offset_of_text = strlen(text); //FB 5/31/99 - - strcat(text, pMsg); - - // don't let text be too long for malicious reasons - // ...doubled this limit for Axshun -FB - if (strlen(text) > 300) - text[300] = 0; - - if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) - ParseSayText(ent, text + offset_of_text, strlen(text)); //FB 5/31/99 - offset change - // this will parse the % variables, - // and again check 300 limit afterwards -FB - // (although it checks it without the name in front, oh well) - - strcat(text, "\n"); - - if (dedicated->value) - safe_cprintf(NULL, PRINT_CHAT, "%s", text); - - for (j = 1; j <= game.maxclients; j++) - { - other = &g_edicts[j]; - if (!other->inuse) - continue; - if (!other->client) - continue; - if (other->is_bot || Q_stricmp(other->classname, "bot") == 0) // Don't send msg to bots - continue; - if (team_message && !OnSameTeam(ent, other)) // If team message - continue; - if (teamplay->value && team_round_going) - { - if ((ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) - && (other->solid != SOLID_NOT && other->deadflag != DEAD_DEAD)) - continue; - } - - safe_cprintf(other, PRINT_CHAT, "%s", text); - } -} - -// Returns kill counter. Each time this is checked, the kill counter is reset. -static int BOTLIB_ReadKilledPlayers(edict_t* ent) -{ - int kills = 0; - edict_t* targ; - - for (int i = 0; i < MAX_LAST_KILLED; i++) - { - targ = ent->client->last_killed_target[i]; - if (!targ) - break; - - if (!targ->inuse || !targ->client || OnSameTeam(ent, targ)) // Remove disconnected or team players from list - { - for (int j = i + 1; j < MAX_LAST_KILLED; j++) - ent->client->last_killed_target[j - 1] = ent->client->last_killed_target[j]; - - ent->client->last_killed_target[MAX_LAST_KILLED - 1] = NULL; - i--; - continue; - } - - kills++; - } - - return kills; -} - -// Reports a kill counter and adds killed names to the buffer -static void BOTLIB_GetLastKilledTargets(edict_t* self, int kills, char* buf) -{ - if (kills > 1) - sprintf(buf, " %c [ %d ] %dx Enemies Down! [ %c ", '\x06', self->client->resp.score, kills, '\x07'); // Build bandage string - else - sprintf(buf, " %c [ %d ] Enemy Down! [ %c ", '\x06', self->client->resp.score, '\x07'); // Build bandage string - - Q_strncatz(buf, self->client->last_killed_target[0]->client->pers.netname, PARSE_BUFSIZE); // First kill - - // If multiple kills - for (int i = 1; i < kills; i++) // Max kills: MAX_LAST_KILLED - { - if (i == kills - 1) // Last two kills (a, b, c, d and e) - Q_strncatz(buf, " and ", PARSE_BUFSIZE); - else // Multiple kills (a, b,...) - Q_strncatz(buf, ", ", PARSE_BUFSIZE); - - // Print the additional player names - Q_strncatz(buf, self->client->last_killed_target[i]->client->pers.netname, PARSE_BUFSIZE); - } - - Q_strncatz(buf, " ]", PARSE_BUFSIZE); - - self->client->last_killed_target[0] = NULL; -} - -// Builds a list of enemy names in sight -static void BOTLIB_GetEnemyList(edict_t* self, char* buf) -{ - sprintf(buf, "[ %s", players[self->bot.enemies[0]]->client->pers.netname); // Print first player - - // If multiple enemies - if (self->bot.enemies_num > 0) - { - for (int i = 1; i < self->bot.enemies_num; i++) - { - if (i == 3) // If names exceed the limit - { - char extra[20]; - sprintf(extra, ", and %d more...", self->bot.enemies_num - 3); - Q_strncatz(buf, extra, PARSE_BUFSIZE); - break; - } - - if (i == self->bot.enemies_num - 1) - Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) - else - Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) - - Q_strncatz(buf, players[self->bot.enemies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names - } - } - - Q_strncatz(buf, " ]", PARSE_BUFSIZE); -} - -// Returns a list of nearby teammates -static void BOTLIB_GetNearbyTeamList(edict_t* self, char* buf) -{ - sprintf(buf, "[ %s", players[self->bot.allies[0]]->client->pers.netname); // Print first player - - for (int i = 1; i < self->bot.allies_num; i++) - { - if (i == 3) // If names exceed the limit - { - char extra[20]; - sprintf(extra, ", and %d more", self->bot.allies_num - 3); - Q_strncatz(buf, extra, PARSE_BUFSIZE); - break; - } - - if (i == self->bot.allies_num - 1) - Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) - else - Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) - - Q_strncatz(buf, players[self->bot.allies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names - } - - Q_strncatz(buf, " ]", PARSE_BUFSIZE); -} - -// Radio commands -void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) -{ - char buffer[256]; // Chat buffer - - if (teamplay->value == 0) return; // Only radio in TP games - if (team_round_going == false || lights_camera_action) return; // Only allow radio during a real match (after LCA and before win/loss announcement) - //if (team_round_going && lights_camera_action == 0) - - //if (self->groundentity == NULL) return; // Don't wave when not on ground - //if (self->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked - //if (self->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active - - // Find how many bots on our team are alive - short bots_alive = 0; - for (int i = 0; i <= num_players; i++) - { - if (players[i] == NULL || players[i]->client == NULL || players[i]->solid == SOLID_NOT || players[i]->deadflag != DEAD_NO) - continue; - - if (players[i]->is_bot == false) - continue; - - if (OnSameTeam(self, players[i]) == false) - continue; - - bots_alive++; - } - - // CALLER: Team report in - if (self->bot.radioTeamReportedIn == false && - BOTLIB_EnemiesAlive(self) && //BOTLIB_AlliesAlive(self) && - self->bot.see_enemies == false) - { - if (self->bot.allies_num > 0) - self->bot.radioLastSawAllyTime = 0; // Reset counter because we spotted an ally - else - self->bot.radioLastSawAllyTime++; // No ally seen - - if (self->bot.radioLastSawAllyTime > self->bot.radioTeamReportedInDelay) // Haven't seen any friendly players in a while - { - self->bot.radioTeamReportedIn = true; // Only allow once per round - - sprintf(buffer, " %c Team report in %c", '\x0E', '\x0E'); // Build string - BOTLIB_Say(self, buffer, true); // Say it - - self->bot.bi.radioflags |= RADIO_TEAM_REPORT_IN; // Wave - BOTLIB_RadioBroadcast(self, NULL, "treport"); // Sound - - // Find a bot on our team who is alive, request they answer the report status - for (int i = 0; i <= num_players; i++) - { - if (players[i] == NULL || players[i] == self || players[i]->client == NULL || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) || players[i]->deadflag != DEAD_NO) - continue; - - if (players[i]->is_bot == false) - continue; - - if (OnSameTeam(self, players[i]) == false) - continue; - - // We found a bot to report its status - players[i]->bot.radioReportIn = true; // Request this bot report in - break; - } - } - } - - // CALLEE: Random chance to flag this bot to respond to a 'team report in' request from a human - if (Q_stricmp(self->bot.radioLastHumanMsg, "treport") == 0) - { - if (bots_alive) - { - if ((rand() % bots_alive) == 1) // Scale chance based on bots on our team are alive (2 bots = 50%, 4 bots = 25%, ...) - self->bot.radioReportIn = true; // Request this bot report in - } - - self->bot.radioLastHumanMsg[0] = '\0'; // Remove last radio call from bot memory - } - - // CALLEE: Bot responding in to a 'team report in' request (either bot or human) - if (self->bot.radioReportIn) - { - self->bot.radioReportInTime++; - - if (self->bot.radioReportInTime > self->bot.radioReportInDelay) // RNG delay - { - self->bot.radioReportInTime = 0; - self->bot.radioReportIn = false; - - // Reaport near enemies - if (self->bot.enemies_num) - { - char nearby_enemies[PARSE_BUFSIZE]; - BOTLIB_GetEnemyList(self, nearby_enemies); - sprintf(buffer, " %c Status [ %d%c ] near enemies ", '\x0E', self->health, '\x05'); // Build string - Q_strncatz(buffer, nearby_enemies, PARSE_BUFSIZE); - //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); - } - // Report near allies - else if (self->bot.allies_num) - { - char nearby_team[PARSE_BUFSIZE]; - BOTLIB_GetNearbyTeamList(self, nearby_team); - sprintf(buffer, " %c Status [ %d%c ] near team ", '\x0F', self->health, '\x05'); // Build string - Q_strncatz(buffer, nearby_team, PARSE_BUFSIZE); - //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); - } - // Report alone - else - { - int report_type = (rand() % 2); - - // Basic report - if (report_type == 0) - { - sprintf(buffer, " %c Reporting in! [ %d%c ]", '\x0F', self->health, '\x05'); // Build string - } - else - { - char weap_name[PARSE_BUFSIZE]; - char item_name[PARSE_BUFSIZE]; - GetWeaponName(self, weap_name); - GetItemName(self, item_name); - sprintf(buffer, " %c Reporting in! [ %d%c ] Equipped with %c %s and %s", '\x0F', self->health, '\x05', '\x07', weap_name, item_name); // Build string - } - } - - BOTLIB_Say(self, buffer, true); // Say it - - self->bot.bi.radioflags |= RADIO_REPORTIN; // Wave - BOTLIB_RadioBroadcast(self, NULL, "reportin"); // Sound - } - } - - - //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text - - - // Bandaging - if (self->bot.radioBandaging) // If bandaging, flag the bot to radio this in - { - if (IS_ALIVE(self)) - { - sprintf(buffer, " %c Bandaging [ %d%c ] %c", '\x04', self->health, '\x05', '\x04'); // Build string - 0x04 bandage symbol, 0x05 heart symbol - BOTLIB_Say(self, buffer, true); // Say it - - if (self->health > 75) // Taking fire - { - self->bot.bi.radioflags |= RADIO_TAKING_FIRE; // Wave - BOTLIB_RadioBroadcast(self, NULL, "taking_f"); // Sound - } - else if (self->health > 50) // Cover me - { - self->bot.bi.radioflags |= RADIO_COVER; // Wave - BOTLIB_RadioBroadcast(self, NULL, "cover"); // Sound - } - else // I'm hit - { - self->bot.bi.radioflags |= RADIO_IM_HIT; // Wave - BOTLIB_RadioBroadcast(self, NULL, "im_hit"); // Sound - } - } - - self->bot.radioBandaging = false; - } - - //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text - - // If one or more enemies have been killed, report them as enemy down - // Each report clears the list of enemies killed. - if (self->bot.radioReportKills && self->bot.see_enemies == false) // Delay report until free of enemies - { - int kills = BOTLIB_ReadKilledPlayers(self); - //if (kills && (rand() % 100 <= kills) ) // Scale chance based kills, more kills == more chance to report - - /*if (kills && kills < 2 && (rand() % 100 > 30)) - { - self->client->last_killed_target[0] = NULL; - kills = 0; - }*/ - - if (kills) - { - BOTLIB_GetLastKilledTargets(self, kills, buffer); // Build string - BOTLIB_Say(self, buffer, true); // Say it - - self->bot.bi.radioflags |= RADIO_ENEMY_DOWN; // Wave - - // Radio how many enemies killed - /*switch (kills) - { - case 1: BOTLIB_RadioBroadcast(self, NULL, "1"); break; - case 2: BOTLIB_RadioBroadcast(self, NULL, "2"); break; - case 3: BOTLIB_RadioBroadcast(self, NULL, "3"); break; - case 4: BOTLIB_RadioBroadcast(self, NULL, "4"); break; - case 5: BOTLIB_RadioBroadcast(self, NULL, "5"); break; - case 6: BOTLIB_RadioBroadcast(self, NULL, "6"); break; - case 7: BOTLIB_RadioBroadcast(self, NULL, "7"); break; - case 8: BOTLIB_RadioBroadcast(self, NULL, "8"); break; - case 9: BOTLIB_RadioBroadcast(self, NULL, "9"); break; - default: break; - }*/ - - // Radio enemy down - BOTLIB_RadioBroadcast(self, NULL, "enemyd"); // Sound - } - } -} +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" +#include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 + +// Bot wave gestures +void BOTLIB_Wave(edict_t* ent, int type) +{ + if (ent->groundentity == NULL) return; // Don't wave when not on ground + if (ent->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked + if (ent->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active + + // Time limit between waves + if (ent->client->resp.lastWave + (1 * HZ) > level.framenum) + return; + ent->client->resp.lastWave = level.framenum; // Update last wave + + + switch (type) + { + case WAVE_FLIPOFF: + SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff + break; + case WAVE_SALUTE: + SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute + break; + case WAVE_TAUNT: + SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt + break; + case WAVE_WAVE: + SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave + break; + case WAVE_POINT: + default: + SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point + break; + } +} + +void BOTLIB_PrecacheRadioSounds() +{ + int i; + char path[MAX_QPATH]; + + //male + for (i = 0; i < bot_numMaleSnds; i++) + { + Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/male/", bot_male_radio_msgs[i].msg); + bot_male_radio_msgs[i].sndIndex = gi.soundindex(path); + } + + //female + for (i = 0; i < bot_numFemaleSnds; i++) + { + Q_snprintf(path, sizeof(path), "%s%s.wav", "radio/female/", bot_female_radio_msgs[i].msg); + bot_female_radio_msgs[i].sndIndex = gi.soundindex(path); + } +} +void BOTLIB_RadioBroadcast(edict_t* ent, edict_t* partner, char* msg) +{ + int j, i, msg_len, numSnds; + edict_t* other; + bot_radio_msg_t* radio_msgs; + int msg_soundIndex = 0; + char msgname_num[8], filteredmsg[48]; + qboolean found = false; + radio_t* radio; + + if (!IS_ALIVE(ent)) + return; + + if (!teamplay->value) + { + if (!DMFLAGS((DF_MODELTEAMS | DF_SKINTEAMS))) + return; // don't allow in a non-team setup... + } + + /* + //TempFile END + //AQ2:TNG Slicer + if (radio_repeat->value) + { //SLIC2 Optimization + if (CheckForRepeat(ent, i) == false) + return; + } + + if (radio_max->value) + { + if (CheckForFlood(ent) == false) + return; + } + */ + + radio = &ent->client->resp.radio; + if (radio->gender) + { + radio_msgs = bot_female_radio_msgs; + numSnds = bot_numFemaleSnds; + } + else + { + radio_msgs = bot_male_radio_msgs; + numSnds = bot_numMaleSnds; + } + + i = found = 0; + msg_len = 0; + + Q_strncpyz(filteredmsg, msg, sizeof(filteredmsg)); + + for (i = 0; i < numSnds; i++) + { + if (!Q_stricmp(radio_msgs[i].msg, filteredmsg)) + { + found = true; + msg_soundIndex = radio_msgs[i].sndIndex; + msg_len = radio_msgs[i].length; + break; + } + } + if (!found) + { + Com_Printf("%s '%s' is not a valid radio message", __func__, filteredmsg); + return; + } + + //TempFile BEGIN + if (Q_stricmp(filteredmsg, "enemyd") == 0) + { + if (ent->client->radio_num_kills > 1 && ent->client->radio_num_kills <= 10) + { + // If we are reporting enemy down, add the number of kills. + sprintf(msgname_num, "%i", ent->client->radio_num_kills); + ent->client->radio_num_kills = 0; // prevent from getting into an endless loop + + BOTLIB_RadioBroadcast(ent, partner, msgname_num); // Now THAT'S recursion! =) + } + ent->client->radio_num_kills = 0; + } + + if (partner) + { + BOTLIB_AddRadioMsg(&partner->client->resp.radio, msg_soundIndex, msg_len, ent); + return; + } + + //AQ2:TNG END + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + //Com_Printf("%s [%d]%s [%d]%s\n", __func__, ent->client->resp.team, ent->client->pers.netname, other->client->resp.team, other->client->pers.netname); //ent1->client->resp.team == ent2->client->resp.team; + if (!OnSameTeam(ent, other)) + continue; + BOTLIB_AddRadioMsg(&other->client->resp.radio, msg_soundIndex, msg_len, ent); + } +} + +// Allow bots to send a chat message to players +void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) +{ + int j, /*i,*/ offset_of_text; + edict_t* other; + char text[2048]; + //gclient_t *cl; + + + if (!teamplay->value) + return; + + if (ent->client->resp.team == NOTEAM) + return; + + if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + { + if (team_message) + Q_snprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team + else + Q_snprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all + } + else if (team_message) + Q_snprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team + else + Q_snprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all + + offset_of_text = strlen(text); //FB 5/31/99 + + strcat(text, pMsg); + + // don't let text be too long for malicious reasons + // ...doubled this limit for Axshun -FB + if (strlen(text) > 300) + text[300] = 0; + + if (ent->solid != SOLID_NOT && ent->deadflag != DEAD_DEAD) + ParseSayText(ent, text + offset_of_text, strlen(text)); //FB 5/31/99 - offset change + // this will parse the % variables, + // and again check 300 limit afterwards -FB + // (although it checks it without the name in front, oh well) + + strcat(text, "\n"); + + if (dedicated->value) + safe_cprintf(NULL, PRINT_CHAT, "%s", text); + + for (j = 1; j <= game.maxclients; j++) + { + other = &g_edicts[j]; + if (!other->inuse) + continue; + if (!other->client) + continue; + if (other->is_bot || Q_stricmp(other->classname, "bot") == 0) // Don't send msg to bots + continue; + if (team_message && !OnSameTeam(ent, other)) // If team message + continue; + if (teamplay->value && team_round_going) + { + if ((ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + && (other->solid != SOLID_NOT && other->deadflag != DEAD_DEAD)) + continue; + } + + safe_cprintf(other, PRINT_CHAT, "%s", text); + } +} + +// Returns kill counter. Each time this is checked, the kill counter is reset. +static int BOTLIB_ReadKilledPlayers(edict_t* ent) +{ + int kills = 0; + edict_t* targ; + + for (int i = 0; i < MAX_LAST_KILLED; i++) + { + targ = ent->client->last_killed_target[i]; + if (!targ) + break; + + if (!targ->inuse || !targ->client || OnSameTeam(ent, targ)) // Remove disconnected or team players from list + { + for (int j = i + 1; j < MAX_LAST_KILLED; j++) + ent->client->last_killed_target[j - 1] = ent->client->last_killed_target[j]; + + ent->client->last_killed_target[MAX_LAST_KILLED - 1] = NULL; + i--; + continue; + } + + kills++; + } + + return kills; +} + +// Reports a kill counter and adds killed names to the buffer +static void BOTLIB_GetLastKilledTargets(edict_t* self, int kills, char* buf) +{ + if (kills > 1) + sprintf(buf, " %c [ %d ] %dx Enemies Down! [ %c ", '\x06', self->client->resp.score, kills, '\x07'); // Build bandage string + else + sprintf(buf, " %c [ %d ] Enemy Down! [ %c ", '\x06', self->client->resp.score, '\x07'); // Build bandage string + + Q_strncatz(buf, self->client->last_killed_target[0]->client->pers.netname, PARSE_BUFSIZE); // First kill + + // If multiple kills + for (int i = 1; i < kills; i++) // Max kills: MAX_LAST_KILLED + { + if (i == kills - 1) // Last two kills (a, b, c, d and e) + Q_strncatz(buf, " and ", PARSE_BUFSIZE); + else // Multiple kills (a, b,...) + Q_strncatz(buf, ", ", PARSE_BUFSIZE); + + // Print the additional player names + Q_strncatz(buf, self->client->last_killed_target[i]->client->pers.netname, PARSE_BUFSIZE); + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); + + self->client->last_killed_target[0] = NULL; +} + +// Builds a list of enemy names in sight +static void BOTLIB_GetEnemyList(edict_t* self, char* buf) +{ + sprintf(buf, "[ %s", players[self->bot.enemies[0]]->client->pers.netname); // Print first player + + // If multiple enemies + if (self->bot.enemies_num > 0) + { + for (int i = 1; i < self->bot.enemies_num; i++) + { + if (i == 3) // If names exceed the limit + { + char extra[20]; + sprintf(extra, ", and %d more...", self->bot.enemies_num - 3); + Q_strncatz(buf, extra, PARSE_BUFSIZE); + break; + } + + if (i == self->bot.enemies_num - 1) + Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) + else + Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) + + Q_strncatz(buf, players[self->bot.enemies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names + } + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); +} + +// Returns a list of nearby teammates +static void BOTLIB_GetNearbyTeamList(edict_t* self, char* buf) +{ + sprintf(buf, "[ %s", players[self->bot.allies[0]]->client->pers.netname); // Print first player + + for (int i = 1; i < self->bot.allies_num; i++) + { + if (i == 3) // If names exceed the limit + { + char extra[20]; + sprintf(extra, ", and %d more", self->bot.allies_num - 3); + Q_strncatz(buf, extra, PARSE_BUFSIZE); + break; + } + + if (i == self->bot.allies_num - 1) + Q_strncatz(buf, ", and ", PARSE_BUFSIZE); // Last two players (a, b, c, d and e) + else + Q_strncatz(buf, ", ", PARSE_BUFSIZE); // Multiple players (a, b,...) + + Q_strncatz(buf, players[self->bot.allies[i]]->client->pers.netname, PARSE_BUFSIZE); // Print the additional player names + } + + Q_strncatz(buf, " ]", PARSE_BUFSIZE); +} + +// Radio commands +void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) +{ + char buffer[256]; // Chat buffer + + if (teamplay->value == 0) return; // Only radio in TP games + if (team_round_going == false || lights_camera_action) return; // Only allow radio during a real match (after LCA and before win/loss announcement) + //if (team_round_going && lights_camera_action == 0) + + //if (self->groundentity == NULL) return; // Don't wave when not on ground + //if (self->client->ps.pmove.pm_flags & PMF_DUCKED) return; // Don't wave when ducked + //if (self->client->anim_priority > ANIM_WAVE) return; // Don't wave when higher animation priority are active + + // Find how many bots on our team are alive + short bots_alive = 0; + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i]->client == NULL || players[i]->solid == SOLID_NOT || players[i]->deadflag != DEAD_NO) + continue; + + if (players[i]->is_bot == false) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + bots_alive++; + } + + // CALLER: Team report in + if (self->bot.radioTeamReportedIn == false && + BOTLIB_EnemiesAlive(self) && //BOTLIB_AlliesAlive(self) && + self->bot.see_enemies == false) + { + if (self->bot.allies_num > 0) + self->bot.radioLastSawAllyTime = 0; // Reset counter because we spotted an ally + else + self->bot.radioLastSawAllyTime++; // No ally seen + + if (self->bot.radioLastSawAllyTime > self->bot.radioTeamReportedInDelay) // Haven't seen any friendly players in a while + { + self->bot.radioTeamReportedIn = true; // Only allow once per round + + sprintf(buffer, " %c Team report in %c", '\x0E', '\x0E'); // Build string + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_TEAM_REPORT_IN; // Wave + BOTLIB_RadioBroadcast(self, NULL, "treport"); // Sound + + // Find a bot on our team who is alive, request they answer the report status + for (int i = 0; i <= num_players; i++) + { + if (players[i] == NULL || players[i] == self || players[i]->client == NULL || players[i]->solid == SOLID_NOT || (players[i]->flags & FL_NOTARGET) || players[i]->deadflag != DEAD_NO) + continue; + + if (players[i]->is_bot == false) + continue; + + if (OnSameTeam(self, players[i]) == false) + continue; + + // We found a bot to report its status + players[i]->bot.radioReportIn = true; // Request this bot report in + break; + } + } + } + + // CALLEE: Random chance to flag this bot to respond to a 'team report in' request from a human + if (Q_stricmp(self->bot.radioLastHumanMsg, "treport") == 0) + { + if (bots_alive) + { + if ((rand() % bots_alive) == 1) // Scale chance based on bots on our team are alive (2 bots = 50%, 4 bots = 25%, ...) + self->bot.radioReportIn = true; // Request this bot report in + } + + self->bot.radioLastHumanMsg[0] = '\0'; // Remove last radio call from bot memory + } + + // CALLEE: Bot responding in to a 'team report in' request (either bot or human) + if (self->bot.radioReportIn) + { + self->bot.radioReportInTime++; + + if (self->bot.radioReportInTime > self->bot.radioReportInDelay) // RNG delay + { + self->bot.radioReportInTime = 0; + self->bot.radioReportIn = false; + + // Reaport near enemies + if (self->bot.enemies_num) + { + char nearby_enemies[PARSE_BUFSIZE]; + BOTLIB_GetEnemyList(self, nearby_enemies); + sprintf(buffer, " %c Status [ %d%c ] near enemies ", '\x0E', self->health, '\x05'); // Build string + Q_strncatz(buffer, nearby_enemies, PARSE_BUFSIZE); + //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); + } + // Report near allies + else if (self->bot.allies_num) + { + char nearby_team[PARSE_BUFSIZE]; + BOTLIB_GetNearbyTeamList(self, nearby_team); + sprintf(buffer, " %c Status [ %d%c ] near team ", '\x0F', self->health, '\x05'); // Build string + Q_strncatz(buffer, nearby_team, PARSE_BUFSIZE); + //Com_Printf("%s %s: %s\n", __func__, self->client->pers.netname, buffer); + } + // Report alone + else + { + int report_type = (rand() % 2); + + // Basic report + if (report_type == 0) + { + sprintf(buffer, " %c Reporting in! [ %d%c ]", '\x0F', self->health, '\x05'); // Build string + } + else + { + char weap_name[PARSE_BUFSIZE]; + char item_name[PARSE_BUFSIZE]; + GetWeaponName(self, weap_name); + GetItemName(self, item_name); + sprintf(buffer, " %c Reporting in! [ %d%c ] Equipped with %c %s and %s", '\x0F', self->health, '\x05', '\x07', weap_name, item_name); // Build string + } + } + + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_REPORTIN; // Wave + BOTLIB_RadioBroadcast(self, NULL, "reportin"); // Sound + } + } + + + //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text + + + // Bandaging + if (self->bot.radioBandaging) // If bandaging, flag the bot to radio this in + { + if (IS_ALIVE(self)) + { + sprintf(buffer, " %c Bandaging [ %d%c ] %c", '\x04', self->health, '\x05', '\x04'); // Build string - 0x04 bandage symbol, 0x05 heart symbol + BOTLIB_Say(self, buffer, true); // Say it + + if (self->health > 75) // Taking fire + { + self->bot.bi.radioflags |= RADIO_TAKING_FIRE; // Wave + BOTLIB_RadioBroadcast(self, NULL, "taking_f"); // Sound + } + else if (self->health > 50) // Cover me + { + self->bot.bi.radioflags |= RADIO_COVER; // Wave + BOTLIB_RadioBroadcast(self, NULL, "cover"); // Sound + } + else // I'm hit + { + self->bot.bi.radioflags |= RADIO_IM_HIT; // Wave + BOTLIB_RadioBroadcast(self, NULL, "im_hit"); // Sound + } + } + + self->bot.radioBandaging = false; + } + + //BOTUT_Cmd_Say_f(self, "Equipped with %W and %I. Current health %H."); // Text + + // If one or more enemies have been killed, report them as enemy down + // Each report clears the list of enemies killed. + if (self->bot.radioReportKills && self->bot.see_enemies == false) // Delay report until free of enemies + { + int kills = BOTLIB_ReadKilledPlayers(self); + //if (kills && (rand() % 100 <= kills) ) // Scale chance based kills, more kills == more chance to report + + /*if (kills && kills < 2 && (rand() % 100 > 30)) + { + self->client->last_killed_target[0] = NULL; + kills = 0; + }*/ + + if (kills) + { + BOTLIB_GetLastKilledTargets(self, kills, buffer); // Build string + BOTLIB_Say(self, buffer, true); // Say it + + self->bot.bi.radioflags |= RADIO_ENEMY_DOWN; // Wave + + // Radio how many enemies killed + /*switch (kills) + { + case 1: BOTLIB_RadioBroadcast(self, NULL, "1"); break; + case 2: BOTLIB_RadioBroadcast(self, NULL, "2"); break; + case 3: BOTLIB_RadioBroadcast(self, NULL, "3"); break; + case 4: BOTLIB_RadioBroadcast(self, NULL, "4"); break; + case 5: BOTLIB_RadioBroadcast(self, NULL, "5"); break; + case 6: BOTLIB_RadioBroadcast(self, NULL, "6"); break; + case 7: BOTLIB_RadioBroadcast(self, NULL, "7"); break; + case 8: BOTLIB_RadioBroadcast(self, NULL, "8"); break; + case 9: BOTLIB_RadioBroadcast(self, NULL, "9"); break; + default: break; + }*/ + + // Radio enemy down + BOTLIB_RadioBroadcast(self, NULL, "enemyd"); // Sound + } + } +} diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 6027ece6f..55ff41a9b 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -1,1044 +1,1044 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -ctf_status_t bot_ctf_status; - -// Does this bot have the flag? -qboolean BOTLIB_Carrying_Flag(edict_t* self) -{ - if (!ctf->value) - return false; - - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == self) - return true; - - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == self) - return true; - - //if (self->client->inventory[items[FLAG_T1_NUM].index] || self->client->inventory[items[FLAG_T2_NUM].index]) - // return true; - return false; -} - -//#undef CTF_AUTO_FLAG_RETURN_TIMEOUT -//#define CTF_AUTO_FLAG_RETURN_TIMEOUT 30 - -// Get the -int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) -{ - vec3_t mins = { -16, -16, -24 }; - vec3_t maxs = { 16, 16, 32 }; - vec3_t bmins = { 0,0,0 }; - vec3_t bmaxs = { 0,0,0 }; - - if (ent == NULL) - return INVALID; - - int cloest_node_num = INVALID; - float cloest_node_dist = 99999999; - - for (int j = 0; j < numnodes; j++) - { - VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world - VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world - - float dist = VectorDistance(nodes[j].origin, ent->s.origin); - - // If ent is touching a node - //if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax) || VectorDistance(nodes[j].origin, ent->s.origin) <= 128) - if (dist <= 128) - { - trace_t tr = gi.trace(nodes[j].origin, tv(-16, -16, -8), tv(16, 16, 8), ent->s.origin, NULL, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - //return nodes[j].nodenum; - - if (dist < cloest_node_dist) - { - cloest_node_dist = dist; - cloest_node_num = nodes[j].nodenum; - } - } - } - } - if (cloest_node_num != INVALID) - return cloest_node_num; - - // If not touching a box, try searching via cloest distance to a node - if (1) - { - int i; - float closest = 99999; - float dist; - vec3_t v; - trace_t tr; - float rng; - int node = INVALID; - //vec3_t maxs, mins; - - //VectorCopy(self->mins, mins); - //VectorCopy(self->maxs, maxs); - //mins[2] += 18; // Stepsize - //maxs[2] -= 16; // Duck a little.. - - rng = (float)(NODE_DENSITY * NODE_DENSITY); // square range for distance comparison (eliminate sqrt) - - for (i = 0; i < numnodes; i++) - { - if (nodes[i].inuse == false) continue; // Ignore nodes not in use - - //if (type == NODE_ALL || type == nodes[i].type) // check node type - { - // Get Height Diff - float height = fabs(nodes[i].origin[2] - ent->s.origin[2]); - if (height > 60) // Height difference high - continue; - - VectorSubtract(nodes[i].origin, ent->s.origin, v); // subtract first - - dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; - - if (dist < closest && dist < rng) - { - tr = gi.trace(ent->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, ent, MASK_PLAYERSOLID); //rekkie - if ((tr.fraction == 1.0) || - ((tr.fraction > 0.9) // may be blocked by the door itself! - && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) - ) - { - node = i; - closest = dist; - } - } - } - } - if (node != INVALID) - return nodes[node].nodenum; - } - - return INVALID; -} - -// Intercept flag carrier (good guy and bad guy) -// [OPTIONAL] distance: if bot is within distance -// Returns the node nearest to the carrier -int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance) -{ - if (distance > 0) - { - if (team == TEAM1) - { - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - } - - if (team == TEAM2) - { - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - } - } - else - { - if (team == TEAM1) - { - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - } - if (team == TEAM2) - { - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - } - } - - return INVALID; -} - -// Is flag at home? -// [OPTIONAL] distance: if bot is within distance -qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance) -{ - if (team == TEAM1 && self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == true) - { - if (distance > 0) - { - if (BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); - return true; - } - } - else - { - //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); - return true; - } - } - if (team == TEAM2 && self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == true) - { - if (distance > 0) - { - if (BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); - return true; - } - } - else - { - //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); - return true; - } - } - - return false; -} - -// Is Red or Blue flag dropped to ground? -// Which team flag to check if dropped -// [OPTIONAL] if distance > 0: Checks bot is within distance -qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance) -{ - if (self->client->resp.team == TEAM1) - { - if (distance > 0) - { - if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); - return true; - } - if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); - return true; - } - } - else - { - if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); - return true; - } - if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); - return true; - } - } - } - else if (self->client->resp.team == TEAM2) - { - if (distance > 0) - { - if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); - return true; - } - if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); - return true; - } - } - else - { - if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); - return true; - } - if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); - return true; - } - } - } - - return false; -} - -// Returns the distance to flagType (FLAG_T1_NUM, FLAG_T2_NUM) -float BOTLIB_DistanceToFlag(edict_t* self, int flagType) -{ - // If carried by a player - if (bot_ctf_status.player_has_flag1) - return VectorDistance(bot_ctf_status.player_has_flag1->s.origin, self->s.origin); - if (bot_ctf_status.player_has_flag2) - return VectorDistance(bot_ctf_status.player_has_flag2->s.origin, self->s.origin); - - // If on ground - if (flagType == FLAG_T1_NUM && bot_ctf_status.flag1_curr_node > INVALID) - return VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); - if (flagType == FLAG_T2_NUM && bot_ctf_status.flag2_curr_node > INVALID) - return VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); - - return 9999999; // Could not find distance -} - -// Gets the nearest flag -int BOTLIB_NearestFlag(edict_t *self) -{ - if (bot_ctf_status.flag1_home_node == INVALID || bot_ctf_status.flag2_home_node == INVALID) - return 0; - - float flag1_dist = 999999999; - float flag2_dist = 999999999; - - if (bot_ctf_status.flag1_is_dropped) - flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); - else - flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); - - if (bot_ctf_status.flag2_is_dropped) - flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); - else - flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); - - qboolean closer_to_flag1 = flag1_dist < flag1_dist ? true : false; - if (closer_to_flag1) - return FLAG_T1_NUM; - else - return FLAG_T2_NUM; - - /* - float dist_to_flag1_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); - float dist_to_flag2_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); - qboolean closer_to_flag1 = dist_to_flag1_home < dist_to_flag2_home ? true : false; - if (closer_to_flag1) - return FLAG_T1_NUM; - else - return FLAG_T2_NUM; - */ -} - -// Get the status of flags -void BOTLIB_Update_Flags_Status(void) -{ - // Check if any players are carrying a flag - // Returns INVALID if no enemy has the flag - bot_ctf_status.player_has_flag1 = NULL; - bot_ctf_status.player_has_flag2 = NULL; - for (int p = 0; p < num_players; p++) - { - if (players[p]->client->inventory[items[FLAG_T1_NUM].index]) - bot_ctf_status.player_has_flag1 = players[p]; - - if (players[p]->client->inventory[items[FLAG_T2_NUM].index]) - bot_ctf_status.player_has_flag2 = players[p]; - } - - bot_ctf_status.flag1 = NULL; - bot_ctf_status.flag2 = NULL; - bot_ctf_status.flag1_is_home = true; - bot_ctf_status.flag2_is_home = true; - bot_ctf_status.flag1_is_dropped = false; - bot_ctf_status.flag2_is_dropped = false; - bot_ctf_status.flag1_is_carried = false; - bot_ctf_status.flag2_is_carried = false; - - int base = 1 + game.maxclients + BODY_QUEUE_SIZE; - edict_t* ent = g_edicts + base; - for (int i = base; i < globals.num_edicts; i++, ent++) - { - if (ent->inuse == false) continue; - if (!ent->classname) continue; - - // When at home: - // Spawned: 0 0 1 0 -- 0, 0, SOLID_TRIGGER, 0 - // Returned: 80000000 0 1 40000 -- FL_RESPAWN, 0, SOLID_TRIGGER, ITEM_TARGETS_USED - // - // When picked up: - // Picked: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED - // - // When dropped after being picked up: - // Dropped: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED - // Dropped: 0 0 1 10000 -- 0, 0, SOLID_TRIGGER, DROPPED_ITEM - - - if (ent->typeNum == FLAG_T1_NUM) - { - //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); - - bot_ctf_status.flag1 = ent; - bot_ctf_status.flag1_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); - - // Home - never touched (set once per map) - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) - { - /* - // Add a flag node here - trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); - int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); - if (node_added != INVALID) - { - BOTLIB_LinkNodesNearbyNode(ent, node_added); - bot_ctf_status.flag1_home_node = node_added; - Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); - } - */ - - bot_ctf_status.flag1_is_home = true; - bot_ctf_status.flag1_home_node = BOTLIB_CTF_Get_Flag_Node(ent); - //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); - } - // Returned - if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag1_is_home = true; - } - - // Picked up - if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag1_is_home = false; - bot_ctf_status.flag1_is_carried = true; - if (bot_ctf_status.flag2_home_node != INVALID && bot_ctf_status.player_has_flag1 != NULL) - bot_ctf_status.team2_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, bot_ctf_status.player_has_flag1->s.origin); - } - // Dropped - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) - { - bot_ctf_status.flag1_is_home = false; - bot_ctf_status.flag1_is_dropped = true; - } - } - - if (ent->typeNum == FLAG_T2_NUM) - { - //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); - - bot_ctf_status.flag2 = ent; - bot_ctf_status.flag2_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); - - // Home - never touched (set once per map) - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) - { - /* - // Add a flag node here - trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); - int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); - if (node_added != INVALID) - { - BOTLIB_LinkNodesNearbyNode(ent, node_added); - bot_ctf_status.flag2_home_node = node_added; - Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); - } - */ - - bot_ctf_status.flag2_is_home = true; - bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); - Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); - } - // Returned - if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag2_is_home = true; - } - - // Picked - if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag2_is_home = false; - bot_ctf_status.flag2_is_carried = true; - if (bot_ctf_status.flag1_home_node != INVALID && bot_ctf_status.player_has_flag2 != NULL) - bot_ctf_status.team1_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, bot_ctf_status.player_has_flag2->s.origin); - } - // Dropped - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) - { - bot_ctf_status.flag2_is_home = false; - bot_ctf_status.flag2_is_dropped = true; - } - - // Flag home moved - if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_CTF_Get_Flag_Node(ent)) - { - //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); - bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); // Update home node - } - } - - - - //if (ent->solid == SOLID_NOT) continue; // picked up - } - //Com_Printf("%s\n", __func__); -} - -void BOTLIB_CTF_Goals(edict_t* self) -{ - if (team_round_going == false || lights_camera_action) return; // Only allow during a real match (after LCA and before win/loss announcement) - - // Get flag - if (self->bot.bot_ctf_state != BOT_CTF_STATE_GET_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home) - { - int n = bot_ctf_status.flag2_home_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s heading for enemy blue flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin)); - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag2_is_home) - { - int n = bot_ctf_status.flag1_home_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s heading for enemy red flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin)); - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - // Take flag home, if home flag is home - if (BOTLIB_Carrying_Flag(self)) - { - if (self->bot.bot_ctf_state != BOT_CTF_STATE_CAPTURE_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_ATTACK_ENEMY_CARRIER && - self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) > 128) - n = bot_ctf_status.flag1_home_node; - else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) > 128) - n = bot_ctf_status.flag2_home_node; - - // If team flag is dropped and nearby, don't head to home. Allow bot to pickup the dropped team flag. - if (self->client->resp.team == TEAM1 && bot_ctf_status.flag1_is_dropped && VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin) < 768) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, red flag was dropped nearby\n", __func__, self->client->pers.netname); - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.flag2_is_dropped && VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin) < 768) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, blue flag was dropped nearby\n", __func__, self->client->pers.netname); - } - - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && bot_connections.total_team1 <= 1) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && bot_connections.total_team2 <= 1) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - //Com_Printf("%s %s goal_node[%d] flag1_home_node[%i] state[%d]\n", __func__, self->client->pers.netname, self->bot.goal_node, bot_ctf_status.flag1_home_node, self->state); - //if (n != INVALID) Com_Printf("%s %s self->bot.goal_node %i flag node %i\n", __func__, self->client->pers.netname, self->bot.goal_node, n); - - if (BOTLIB_CanGotoNode(self, n, false)) - { - if (self->client->resp.team == TEAM1) - { - //Com_Printf("%s %s Taking blue flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin)); - } - else - { - //Com_Printf("%s %s Taking red flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin)); - } - self->bot.bot_ctf_state = BOT_CTF_STATE_CAPTURE_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - // Bot was on its way to the enemy flag, but got picked up by a team member before the bot could reach it. Therefore, try to support the flag carrrier who got it. - if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG && BOTLIB_Carrying_Flag(self) == false) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2) - { - //Com_Printf("%s %s Red flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); - - // Support ally if they're close - if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin) < 1024) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - self->bot.ctf_support_time = level.framenum + 10.0 * HZ; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - - //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - //self->bot.state = BOT_MOVE_STATE_NAV; - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1) - { - //Com_Printf("%s %s Blue flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); - - // Support ally if they're close - if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin) < 1024) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - self->bot.ctf_support_time = level.framenum + 10.0 * HZ; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - - //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - //self->bot.state = BOT_MOVE_STATE_NAV; - } - } - - // Continue supporting flag carrier - if (self->bot.bot_ctf_state != BOT_CTF_STATE_COVER_FLAG_CARRIER && BOTLIB_Carrying_Flag(self) == false) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 && self->bot.ctf_support_time < level.framenum) - { - self->bot.ctf_support_time = level.framenum + 5.0 * HZ; - - //if (bot_ctf_status.team1_carrier_dist_to_home > 512) - // Com_Printf("%s %s continue supporting blue flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, bot_ctf_status.team1_carrier_dist_to_home); - - - // Support flag carrier if they're away from the home flag - float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); - if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s continue supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 && self->bot.ctf_support_time < level.framenum) - { - self->bot.ctf_support_time = level.framenum + 5.0 * HZ; - - //if (bot_ctf_status.team2_carrier_dist_to_home > 512) - // Com_Printf("%s %s continue supporting red flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, bot_ctf_status.team2_carrier_dist_to_home); - - // Support flag carrier if they're around - float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); - if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s continue supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - } - - - - - // Retrieve dropped T1/T2 flags if nearby - if (self->client->resp.team == TEAM1) - { - int flag_to_get = 0; // Flag that is dropped - - // If both team and ememy flag is dropped, go for closest flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) - { - // If team flag is closer - if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)) - { - flag_to_get = FLAG_T1_NUM; // Dropped team flag - //Com_Printf("%s %s [team] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - } - else - { - flag_to_get = FLAG_T2_NUM; // Dropped enemy flag - //Com_Printf("%s %s [enemy] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - } - } - else if (bot_ctf_status.flag1_is_dropped) // Dropped team flag - flag_to_get = FLAG_T1_NUM; - else if (bot_ctf_status.flag2_is_dropped) // Dropped enemy flag - flag_to_get = FLAG_T2_NUM; - - if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (flag_to_get == FLAG_T1_NUM) - n = bot_ctf_status.flag1_curr_node; - if (flag_to_get == FLAG_T2_NUM) - n = bot_ctf_status.flag2_curr_node; - - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) - { - if (flag_to_get == FLAG_T1_NUM) // Dropped team flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; - //Com_Printf("%s %s retrieving [team] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - if (flag_to_get == FLAG_T2_NUM) // Dropped enemy flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; - //Com_Printf("%s %s retrieving [enemy] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, nodes[n].nodenum); - return; - } - } - } - if (self->client->resp.team == TEAM2) - { - int flag_to_get = 0; // Flag that is dropped - - // If both team and ememy flag is dropped, go for closest flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) - { - // If team flag is closer - if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) - { - flag_to_get = FLAG_T2_NUM; // Dropped team flag - Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - } - else - { - flag_to_get = FLAG_T1_NUM; // Dropped enemy flag - Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - } - } - else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag - flag_to_get = FLAG_T2_NUM; - else if (bot_ctf_status.flag1_is_dropped) // Dropped enemy flag - flag_to_get = FLAG_T1_NUM; - - if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (flag_to_get == FLAG_T2_NUM) - n = bot_ctf_status.flag2_curr_node; - if (flag_to_get == FLAG_T1_NUM) - n = bot_ctf_status.flag1_curr_node; - - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) - { - if (flag_to_get == FLAG_T2_NUM) // Dropped team flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; - //Com_Printf("%s %s retrieving [team] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - if (flag_to_get == FLAG_T1_NUM) // Dropped enemy flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; - //Com_Printf("%s %s retrieving [enemy] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, nodes[n].nodenum); - return; - } - } - } - - - // When the bot is close to the flag from following nodes, flags are not always on a node exactly, - // therefore when the bot gets close enough to the flag, force move the bot toward the flag. - if (self->bot.goal_node == INVALID) - { - - //if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_CAPTURE_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_RETURN_TEAM_FLAG) - { - // TEAM 1 - Walk to home flag (Capture) - if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag1_is_home - && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 2 - Walk to home flag (Capture) - if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag2_is_home - && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 1 - Walk to enemy flag (Take) - if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag2 == false && bot_ctf_status.flag2_is_home - && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward enemy home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 2 - Walk to enemy flag (Take) - if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag1 == false && bot_ctf_status.flag1_is_home - && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward enemy home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // Walk to dropped RED flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward dropped red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // Walk to dropped BLUE flag - if (bot_ctf_status.flag2_is_dropped && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward dropped blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - } - - // Check if bot is closer to enemy flag carrier or the enemy flag at home - int flag_to_get = 0; - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1) - { - if (bot_ctf_status.flag2 && bot_ctf_status.flag2_is_home) // If enemy flag is home - { - // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier - if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin)) - { - flag_to_get = FLAG_T2_NUM; // Go after enemy flag - //Com_Printf("%s %s closer to blue flag than blue enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); - } - else - flag_to_get = FLAG_T1_NUM; // Go after enemy carrier - } - else - flag_to_get = FLAG_T1_NUM; // If both flags gone, go after enemy carrier - } - else if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2) - { - if (bot_ctf_status.flag1 && bot_ctf_status.flag1_is_home) // If enemy flag is home - { - // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier - if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin)) - { - flag_to_get = FLAG_T1_NUM; // Go after enemy flag - //Com_Printf("%s %s closer to red flag than red enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); - } - else - flag_to_get = FLAG_T2_NUM; // Go after enemy carrier - } - else - flag_to_get = FLAG_T2_NUM; // If both flags gone, go after enemy carrier - } - - // Intercept enemy flag carrier if we're closer to them than the enemy flag - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && flag_to_get == FLAG_T1_NUM) - { - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - qboolean force_incercept = false; - if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team1 <= 1) - { - force_incercept = true; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - self->bot.state = BOT_MOVE_STATE_MOVE; - - float dist = 999999999; - dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); - if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; //bot_ctf_status.flag2_curr_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s intercepting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && flag_to_get == FLAG_T2_NUM) - { - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - qboolean force_incercept = false; - if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team2 <= 1) - { - force_incercept = true; - //Com_Printf("%s %s abort capturing flag, other team has our blue flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - self->bot.state = BOT_MOVE_STATE_MOVE; - - float dist = 999999999; - dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); - if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; //bot_ctf_status.flag1_curr_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s intercepting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - self->bot.ctf_support_time = 0; - - } - - /* - // Gather nearby weapons, ammo, and items - if (self->bot.bot_ctf_state == BOT_CTF_STATE_NONE && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ITEMS) - { - int item_node = BOTLIB_GetEquipment(self); - if (item_node != INVALID) - { - if (BOTLIB_CanVisitNode(self, nodes[item_node].nodenum, false)) - { - self->bot.state = BOT_MOVE_STATE_MOVE; - BOTLIB_SetGoal(self, nodes[item_node].nodenum); - self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ITEMS; - //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[item_node].nodenum); - return; - } - } - } - */ - - +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +ctf_status_t bot_ctf_status; + +// Does this bot have the flag? +qboolean BOTLIB_Carrying_Flag(edict_t* self) +{ + if (!ctf->value) + return false; + + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == self) + return true; + + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == self) + return true; + + //if (self->client->inventory[items[FLAG_T1_NUM].index] || self->client->inventory[items[FLAG_T2_NUM].index]) + // return true; + return false; +} + +//#undef CTF_AUTO_FLAG_RETURN_TIMEOUT +//#define CTF_AUTO_FLAG_RETURN_TIMEOUT 30 + +// Get the +int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) +{ + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t bmins = { 0,0,0 }; + vec3_t bmaxs = { 0,0,0 }; + + if (ent == NULL) + return INVALID; + + int cloest_node_num = INVALID; + float cloest_node_dist = 99999999; + + for (int j = 0; j < numnodes; j++) + { + VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world + VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world + + float dist = VectorDistance(nodes[j].origin, ent->s.origin); + + // If ent is touching a node + //if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax) || VectorDistance(nodes[j].origin, ent->s.origin) <= 128) + if (dist <= 128) + { + trace_t tr = gi.trace(nodes[j].origin, tv(-16, -16, -8), tv(16, 16, 8), ent->s.origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + //return nodes[j].nodenum; + + if (dist < cloest_node_dist) + { + cloest_node_dist = dist; + cloest_node_num = nodes[j].nodenum; + } + } + } + } + if (cloest_node_num != INVALID) + return cloest_node_num; + + // If not touching a box, try searching via cloest distance to a node + if (1) + { + int i; + float closest = 99999; + float dist; + vec3_t v; + trace_t tr; + float rng; + int node = INVALID; + //vec3_t maxs, mins; + + //VectorCopy(self->mins, mins); + //VectorCopy(self->maxs, maxs); + //mins[2] += 18; // Stepsize + //maxs[2] -= 16; // Duck a little.. + + rng = (float)(NODE_DENSITY * NODE_DENSITY); // square range for distance comparison (eliminate sqrt) + + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + //if (type == NODE_ALL || type == nodes[i].type) // check node type + { + // Get Height Diff + float height = fabs(nodes[i].origin[2] - ent->s.origin[2]); + if (height > 60) // Height difference high + continue; + + VectorSubtract(nodes[i].origin, ent->s.origin, v); // subtract first + + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + + if (dist < closest && dist < rng) + { + tr = gi.trace(ent->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, ent, MASK_PLAYERSOLID); //rekkie + if ((tr.fraction == 1.0) || + ((tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) + ) + { + node = i; + closest = dist; + } + } + } + } + if (node != INVALID) + return nodes[node].nodenum; + } + + return INVALID; +} + +// Intercept flag carrier (good guy and bad guy) +// [OPTIONAL] distance: if bot is within distance +// Returns the node nearest to the carrier +int BOTLIB_InterceptFlagCarrier(edict_t* self, int team, float distance) +{ + if (distance > 0) + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + else + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + + return INVALID; +} + +// Is flag at home? +// [OPTIONAL] distance: if bot is within distance +qboolean BOTLIB_IsFlagHome(edict_t* self, int team, float distance) +{ + if (team == TEAM1 && self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == true) + { + if (distance > 0) + { + if (BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + else + { + //Com_Printf("%s %s RED flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + if (team == TEAM2 && self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == true) + { + if (distance > 0) + { + if (BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + else + { + //Com_Printf("%s %s BLUE flag is home\n", __func__, self->client->pers.netname); + return true; + } + } + + return false; +} + +// Is Red or Blue flag dropped to ground? +// Which team flag to check if dropped +// [OPTIONAL] if distance > 0: Checks bot is within distance +qboolean BOTLIB_IsFlagDropped(edict_t* self, int team, float distance) +{ + if (self->client->resp.team == TEAM1) + { + if (distance > 0) + { + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + } + else + { + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + } + } + else if (self->client->resp.team == TEAM2) + { + if (distance > 0) + { + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s our flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s enemy flag is dropped nearby %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + } + else + { + if (team == TEAM2 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s our flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM)); + return true; + } + if (team == TEAM1 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s enemy flag is dropped %f\n", __func__, self->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM)); + return true; + } + } + } + + return false; +} + +// Returns the distance to flagType (FLAG_T1_NUM, FLAG_T2_NUM) +float BOTLIB_DistanceToFlag(edict_t* self, int flagType) +{ + // If carried by a player + if (bot_ctf_status.player_has_flag1) + return VectorDistance(bot_ctf_status.player_has_flag1->s.origin, self->s.origin); + if (bot_ctf_status.player_has_flag2) + return VectorDistance(bot_ctf_status.player_has_flag2->s.origin, self->s.origin); + + // If on ground + if (flagType == FLAG_T1_NUM && bot_ctf_status.flag1_curr_node > INVALID) + return VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + if (flagType == FLAG_T2_NUM && bot_ctf_status.flag2_curr_node > INVALID) + return VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + + return 9999999; // Could not find distance +} + +// Gets the nearest flag +int BOTLIB_NearestFlag(edict_t *self) +{ + if (bot_ctf_status.flag1_home_node == INVALID || bot_ctf_status.flag2_home_node == INVALID) + return 0; + + float flag1_dist = 999999999; + float flag2_dist = 999999999; + + if (bot_ctf_status.flag1_is_dropped) + flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + else + flag1_dist = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); + + if (bot_ctf_status.flag2_is_dropped) + flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + else + flag2_dist = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); + + qboolean closer_to_flag1 = flag1_dist < flag1_dist ? true : false; + if (closer_to_flag1) + return FLAG_T1_NUM; + else + return FLAG_T2_NUM; + + /* + float dist_to_flag1_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin); + float dist_to_flag2_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin); + qboolean closer_to_flag1 = dist_to_flag1_home < dist_to_flag2_home ? true : false; + if (closer_to_flag1) + return FLAG_T1_NUM; + else + return FLAG_T2_NUM; + */ +} + +// Get the status of flags +void BOTLIB_Update_Flags_Status(void) +{ + // Check if any players are carrying a flag + // Returns INVALID if no enemy has the flag + bot_ctf_status.player_has_flag1 = NULL; + bot_ctf_status.player_has_flag2 = NULL; + for (int p = 0; p < num_players; p++) + { + if (players[p]->client->inventory[items[FLAG_T1_NUM].index]) + bot_ctf_status.player_has_flag1 = players[p]; + + if (players[p]->client->inventory[items[FLAG_T2_NUM].index]) + bot_ctf_status.player_has_flag2 = players[p]; + } + + bot_ctf_status.flag1 = NULL; + bot_ctf_status.flag2 = NULL; + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag1_is_dropped = false; + bot_ctf_status.flag2_is_dropped = false; + bot_ctf_status.flag1_is_carried = false; + bot_ctf_status.flag2_is_carried = false; + + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + + // When at home: + // Spawned: 0 0 1 0 -- 0, 0, SOLID_TRIGGER, 0 + // Returned: 80000000 0 1 40000 -- FL_RESPAWN, 0, SOLID_TRIGGER, ITEM_TARGETS_USED + // + // When picked up: + // Picked: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // + // When dropped after being picked up: + // Dropped: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // Dropped: 0 0 1 10000 -- 0, 0, SOLID_TRIGGER, DROPPED_ITEM + + + if (ent->typeNum == FLAG_T1_NUM) + { + //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag1 = ent; + bot_ctf_status.flag1_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag1_home_node = node_added; + Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + */ + + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag1_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = true; + } + + // Picked up + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_carried = true; + if (bot_ctf_status.flag2_home_node != INVALID && bot_ctf_status.player_has_flag1 != NULL) + bot_ctf_status.team2_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, bot_ctf_status.player_has_flag1->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_dropped = true; + } + } + + if (ent->typeNum == FLAG_T2_NUM) + { + //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag2 = ent; + bot_ctf_status.flag2_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag2_home_node = node_added; + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + */ + + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = true; + } + + // Picked + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_carried = true; + if (bot_ctf_status.flag1_home_node != INVALID && bot_ctf_status.player_has_flag2 != NULL) + bot_ctf_status.team1_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, bot_ctf_status.player_has_flag2->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_dropped = true; + } + + // Flag home moved + if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_CTF_Get_Flag_Node(ent)) + { + //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); // Update home node + } + } + + + + //if (ent->solid == SOLID_NOT) continue; // picked up + } + //Com_Printf("%s\n", __func__); +} + +void BOTLIB_CTF_Goals(edict_t* self) +{ + if (team_round_going == false || lights_camera_action) return; // Only allow during a real match (after LCA and before win/loss announcement) + + // Get flag + if (self->bot.bot_ctf_state != BOT_CTF_STATE_GET_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag2_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy blue flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag1_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy red flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Take flag home, if home flag is home + if (BOTLIB_Carrying_Flag(self)) + { + if (self->bot.bot_ctf_state != BOT_CTF_STATE_CAPTURE_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_ATTACK_ENEMY_CARRIER && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag1_home_node; + else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag2_home_node; + + // If team flag is dropped and nearby, don't head to home. Allow bot to pickup the dropped team flag. + if (self->client->resp.team == TEAM1 && bot_ctf_status.flag1_is_dropped && VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, red flag was dropped nearby\n", __func__, self->client->pers.netname); + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.flag2_is_dropped && VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, blue flag was dropped nearby\n", __func__, self->client->pers.netname); + } + + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && bot_connections.total_team1 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && bot_connections.total_team2 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + //Com_Printf("%s %s goal_node[%d] flag1_home_node[%i] state[%d]\n", __func__, self->client->pers.netname, self->bot.goal_node, bot_ctf_status.flag1_home_node, self->state); + //if (n != INVALID) Com_Printf("%s %s self->bot.goal_node %i flag node %i\n", __func__, self->client->pers.netname, self->bot.goal_node, n); + + if (BOTLIB_CanGotoNode(self, n, false)) + { + if (self->client->resp.team == TEAM1) + { + //Com_Printf("%s %s Taking blue flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin)); + } + else + { + //Com_Printf("%s %s Taking red flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin)); + } + self->bot.bot_ctf_state = BOT_CTF_STATE_CAPTURE_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Bot was on its way to the enemy flag, but got picked up by a team member before the bot could reach it. Therefore, try to support the flag carrrier who got it. + if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2) + { + //Com_Printf("%s %s Red flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1) + { + //Com_Printf("%s %s Blue flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + } + + // Continue supporting flag carrier + if (self->bot.bot_ctf_state != BOT_CTF_STATE_COVER_FLAG_CARRIER && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team1_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting blue flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, bot_ctf_status.team1_carrier_dist_to_home); + + + // Support flag carrier if they're away from the home flag + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team2_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting red flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, bot_ctf_status.team2_carrier_dist_to_home); + + // Support flag carrier if they're around + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + } + + + + + // Retrieve dropped T1/T2 flags if nearby + if (self->client->resp.team == TEAM1) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Dropped team flag + //Com_Printf("%s %s [team] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + else + { + flag_to_get = FLAG_T2_NUM; // Dropped enemy flag + //Com_Printf("%s %s [enemy] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + } + else if (bot_ctf_status.flag1_is_dropped) // Dropped team flag + flag_to_get = FLAG_T1_NUM; + else if (bot_ctf_status.flag2_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T2_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T1_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T2_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + if (self->client->resp.team == TEAM2) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Dropped team flag + Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + else + { + flag_to_get = FLAG_T1_NUM; // Dropped enemy flag + Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + } + else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag + flag_to_get = FLAG_T2_NUM; + else if (bot_ctf_status.flag1_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T1_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T2_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T1_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + + + // When the bot is close to the flag from following nodes, flags are not always on a node exactly, + // therefore when the bot gets close enough to the flag, force move the bot toward the flag. + if (self->bot.goal_node == INVALID) + { + + //if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_CAPTURE_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + // TEAM 1 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 1 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag2 == false && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag1 == false && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped RED flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped BLUE flag + if (bot_ctf_status.flag2_is_dropped && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + } + + // Check if bot is closer to enemy flag carrier or the enemy flag at home + int flag_to_get = 0; + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1) + { + if (bot_ctf_status.flag2 && bot_ctf_status.flag2_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to blue flag than blue enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + } + else + flag_to_get = FLAG_T1_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T1_NUM; // If both flags gone, go after enemy carrier + } + else if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2) + { + if (bot_ctf_status.flag1 && bot_ctf_status.flag1_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to red flag than red enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + } + else + flag_to_get = FLAG_T2_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T2_NUM; // If both flags gone, go after enemy carrier + } + + // Intercept enemy flag carrier if we're closer to them than the enemy flag + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && flag_to_get == FLAG_T1_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team1 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; //bot_ctf_status.flag2_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && flag_to_get == FLAG_T2_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team2 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our blue flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; //bot_ctf_status.flag1_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + self->bot.ctf_support_time = 0; + + } + + /* + // Gather nearby weapons, ammo, and items + if (self->bot.bot_ctf_state == BOT_CTF_STATE_NONE && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ITEMS) + { + int item_node = BOTLIB_GetEquipment(self); + if (item_node != INVALID) + { + if (BOTLIB_CanVisitNode(self, nodes[item_node].nodenum, false)) + { + self->bot.state = BOT_MOVE_STATE_MOVE; + BOTLIB_SetGoal(self, nodes[item_node].nodenum); + self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ITEMS; + //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[item_node].nodenum); + return; + } + } + } + */ + + } \ No newline at end of file diff --git a/src/action/botlib/botlib_items.c b/src/action/botlib/botlib_items.c index 6fcebe588..89a17017c 100644 --- a/src/action/botlib/botlib_items.c +++ b/src/action/botlib/botlib_items.c @@ -1,257 +1,257 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -// Smarter weapon + item choices -void BOTLIB_SmartWeaponSelection(edict_t *self) -{ - // Let bots adjust its own skill - // Skill helps dictate the weapon and item the bot will pick - int weapon_skill_choice = 0; - - if (bot_skill_threshold->value > 0) - weapon_skill_choice = BOTLIB_AutoAdjustSkill(self); - else - self->bot.skill = bot_skill->value; - - // Allowed weapons: ALL - // Allowed items: ALL - if (weapon_skill_choice == 0) - { - int weaponchoice = 0; // Weapon choice - int highchoice = rand() % 10; // 0..9 - if (highchoice < 7) // 70% chance of ONLY primary weapon - weaponchoice = rand() % 5; - else // 30% chance of ANY weapon (including akimbos and knives) - weaponchoice = rand() % 7; - int itemchoice; - switch (weaponchoice) - { - case 0: - ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 - itemchoice = rand() % 5; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else if (itemchoice == 3) - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 1: - ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 2: - ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 3: - ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); // HC - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 4: - ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 5: - ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); // Dual MK23 - itemchoice = rand() % 3; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 6: - ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives - itemchoice = rand() % 3; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - default: - ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. - break; - } - } - - - // Allowed weapons: MP5, M4, M3, Sniper - // Allowed items: ALL - else if (weapon_skill_choice == 1) - { - int weaponchoice = rand() % 4; // MP5, M4, M3, Sniper - int itemchoice; - switch (weaponchoice) - { - case 0: - ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 - itemchoice = rand() % 5; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else if (itemchoice == 3) - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 1: - ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 2: - ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - case 3: - ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG - itemchoice = rand() % 4; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer - else if (itemchoice == 1) - ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - else if (itemchoice == 2) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet - break; - - default: - ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. - break; - } - } - - // Allowed weapons: MP5, M4, Sniper - // Allowed items: Kevlar Vest, Laser - else if (weapon_skill_choice == 2) - { - int weaponchoice = rand() % 3; // MP5, M4, M3, Sniper - int itemchoice; - switch (weaponchoice) - { - case 0: - ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 - itemchoice = rand() % 2; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - break; - - case 1: - ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 - itemchoice = rand() % 2; - if (itemchoice == 0) - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - else - ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - break; - - case 2: - ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - break; - - default: - ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG - ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest - break; - } - } - - - //Com_Printf("%s %s wep:%s item:%s\n", __func__, self->client->pers.netname, self->client->pers.chosenWeapon, self->client->pers.chosenItem); - - //self->weaponchoice = 8; // Knife - //self->equipchoice = 12; // Bandolier - - // Force bots use grenades only - //ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives - //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - - //ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser - //self->client->pers.chosenItem = FindItemByNum(LASER_NUM); - //self->client->inventory[ITEM_INDEX(GET_ITEM(LASER_NUM))] = 1; - - //ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); - //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - - //ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); - //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); - - //ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); - //ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); - //ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); - //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier - //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +// Smarter weapon + item choices +void BOTLIB_SmartWeaponSelection(edict_t *self) +{ + // Let bots adjust its own skill + // Skill helps dictate the weapon and item the bot will pick + int weapon_skill_choice = 0; + + if (bot_skill_threshold->value > 0) + weapon_skill_choice = BOTLIB_AutoAdjustSkill(self); + else + self->bot.skill = bot_skill->value; + + // Allowed weapons: ALL + // Allowed items: ALL + if (weapon_skill_choice == 0) + { + int weaponchoice = 0; // Weapon choice + int highchoice = rand() % 10; // 0..9 + if (highchoice < 7) // 70% chance of ONLY primary weapon + weaponchoice = rand() % 5; + else // 30% chance of ANY weapon (including akimbos and knives) + weaponchoice = rand() % 7; + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 5; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 3) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 3: + ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); // HC + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 4: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 5: + ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); // Dual MK23 + itemchoice = rand() % 3; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 6: + ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives + itemchoice = rand() % 3; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. + break; + } + } + + + // Allowed weapons: MP5, M4, M3, Sniper + // Allowed items: ALL + else if (weapon_skill_choice == 1) + { + int weaponchoice = rand() % 4; // MP5, M4, M3, Sniper + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 5; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 3) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); // M3 + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, SLIP_NUM); // Slippers + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + case 3: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + itemchoice = rand() % 4; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, SIL_NUM); // Silencer + else if (itemchoice == 1) + ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + else if (itemchoice == 2) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, HELM_NUM); // Helmet + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, 0); // Random allowed. + break; + } + } + + // Allowed weapons: MP5, M4, Sniper + // Allowed items: Kevlar Vest, Laser + else if (weapon_skill_choice == 2) + { + int weaponchoice = rand() % 3; // MP5, M4, M3, Sniper + int itemchoice; + switch (weaponchoice) + { + case 0: + ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); // MP5 + itemchoice = rand() % 2; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + break; + + case 1: + ACEAI_Cmd_Choose_Weapon_Num(self, M4_NUM); // M4 + itemchoice = rand() % 2; + if (itemchoice == 0) + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + else + ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + break; + + case 2: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + break; + + default: + ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); // SSG + ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); // Kevlar Vest + break; + } + } + + + //Com_Printf("%s %s wep:%s item:%s\n", __func__, self->client->pers.netname, self->client->pers.chosenWeapon, self->client->pers.chosenItem); + + //self->weaponchoice = 8; // Knife + //self->equipchoice = 12; // Bandolier + + // Force bots use grenades only + //ACEAI_Cmd_Choose_Weapon_Num(self, KNIFE_NUM); // Knives + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + + //ACEAI_Cmd_Choose_Item_Num(self, LASER_NUM); // Laser + //self->client->pers.chosenItem = FindItemByNum(LASER_NUM); + //self->client->inventory[ITEM_INDEX(GET_ITEM(LASER_NUM))] = 1; + + //ACEAI_Cmd_Choose_Weapon_Num(self, HC_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + + //ACEAI_Cmd_Choose_Weapon_Num(self, M3_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); + + //ACEAI_Cmd_Choose_Weapon_Num(self, MP5_NUM); + //ACEAI_Cmd_Choose_Weapon_Num(self, DUAL_NUM); + //ACEAI_Cmd_Choose_Weapon_Num(self, SNIPER_NUM); + //ACEAI_Cmd_Choose_Item_Num(self, BAND_NUM); // Bandolier + //ACEAI_Cmd_Choose_Item_Num(self, KEV_NUM); } \ No newline at end of file diff --git a/src/action/botlib/botlib_math.c b/src/action/botlib/botlib_math.c index 517d02f62..cb5700dad 100644 --- a/src/action/botlib/botlib_math.c +++ b/src/action/botlib/botlib_math.c @@ -1,154 +1,154 @@ -#include "../g_local.h" - -// Math helper functions, utilities and other useful math based tools - - -// Functionally this works the same as #define VectorCompare(), with the exception of an epsilon -// Compare two vectors and return true if they are the same within a certain epsilon -qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon) -{ - for (int i = 0; i < 3; i++) - { - if (fabs(v1[i] - v2[i]) > epsilon) - { - return false; - } - } - return true; -} - -// Originally from Quake 2 Rerelease source code -// Returns true if two boxes intersect -bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs) -{ - return amins[0] <= bmaxs[0] && amaxs[0] >= bmins[0] && - amins[1] <= bmaxs[1] && amaxs[1] >= bmins[1] && - amins[2] <= bmaxs[2] && amaxs[2] >= bmins[2]; -} - - -qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount) -{ - vec3_t vec = { 0.f,0.f,0.f }; - float dot = 0.f; - vec3_t forward = { 0.f,0.f,0.f }; - - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorSubtract(other->s.origin, self->s.origin, vec); - VectorNormalize(vec); - dot = DotProduct(vec, forward); - - if (amount == 0) - amount = 0.3f; - - if (dot > amount) - return true; - - return false; -} - -// Test if bot is facing toward an origin -qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount) -{ - vec3_t vec = { 0.f,0.f,0.f }; - float dot = 0.f; - vec3_t forward = { 0.f,0.f,0.f }; - - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorSubtract(origin, self->s.origin, vec); - VectorNormalize(vec); - dot = DotProduct(vec, forward); - - if (amount == 0) - amount = 0.3f; - - if (dot > amount) - return true; - - return false; -} - -// Test if bot is walking toward an origin -qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount) -{ - vec3_t vec = { 0.f,0.f,0.f }; - float dot = 0.f; - //vec3_t forward = { 0.f,0.f,0.f }; - - // Get the player's direction based from their velocity - vec3_t walkdir; - VectorSubtract(self->s.origin, self->bot.last_position, walkdir); - //VectorCopy(self->s.origin, self->bot.last_position); - VectorNormalize(walkdir); - vec3_t angle, forward, right; - vectoangles(walkdir, angle); - //VectorCopy(self->s.angles, angle); // Use the player's view angles (not their walk direction) - angle[0] = 0; - AngleVectors(angle, forward, right, NULL); - - - //AngleVectors(self->s.angles, forward, NULL, NULL); - VectorSubtract(origin, self->s.origin, vec); - VectorNormalize(vec); - dot = DotProduct(vec, forward); - - if (amount == 0) - amount = 0.3f; - - if (dot > amount) - { - //Com_Printf("%s %s moving toward [%f]\n", __func__, self->client->pers.netname, dot); - return true; - } - - return false; -} - - -// True if [self] is aiming at [other] -// This func uses box intersection to determine if the player is aiming at the other entity -qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other) -{ - // Project trace from the player's weapon POV - vec3_t start, end; - vec3_t forward, right; - vec3_t offset; - AngleVectors(self->client->v_angle, forward, right, NULL); - VectorSet(offset, 0, 0, self->viewheight - 8); - G_ProjectSource(self->s.origin, offset, forward, right, start); - float dist = VectorDistance(self->s.origin, other->s.origin); - VectorMA(start, dist, forward, end); - - // Adjust the mins/maxs box size based on size - //vec3_t bmins = { -24, -24, +6 }; - //vec3_t bmaxs = { 24, 24, +10 }; - vec3_t bmins = { -32, -32, -16 }; - vec3_t bmaxs = { 32, 32, +16 }; - - // Update absolute box min/max in the world - vec3_t absmin, absmax; - VectorAdd(end, bmins, absmin); - VectorAdd(end, bmaxs, absmax); - - //rekkie -- debug drawing -- s -#if DEBUG_DRAWING - if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual - { - uint32_t blue = MakeColor(0, 0, 255, 255); // Blue - uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow - void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; - DrawBox = players[0]->client->pers.draw->DrawBox; - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - DrawBox(other->s.number + (rand() % 256 + 1), other->s.origin, blue, other->mins, other->maxs, 100, false); // Player box - DrawBox(self->s.number + (rand() % 256 + 1), end, yellow, bmins, bmaxs, 100, false); // Projectile box - } -#endif - //rekkie -- debug drawing -- e - - if (BOTLIB_BoxIntersection(absmin, absmax, other->absmin, other->absmax)) // Do boxes intersect? - { - //Com_Printf("%s %s can hit %s\n", __func__, self->client->pers.netname, other->client->pers.netname); - return true; - } - return false; +#include "../g_local.h" + +// Math helper functions, utilities and other useful math based tools + + +// Functionally this works the same as #define VectorCompare(), with the exception of an epsilon +// Compare two vectors and return true if they are the same within a certain epsilon +qboolean BOTLIB_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon) +{ + for (int i = 0; i < 3; i++) + { + if (fabs(v1[i] - v2[i]) > epsilon) + { + return false; + } + } + return true; +} + +// Originally from Quake 2 Rerelease source code +// Returns true if two boxes intersect +bool BOTLIB_BoxIntersection(vec3_t amins, vec3_t amaxs, vec3_t bmins, vec3_t bmaxs) +{ + return amins[0] <= bmaxs[0] && amaxs[0] >= bmins[0] && + amins[1] <= bmaxs[1] && amaxs[1] >= bmins[1] && + amins[2] <= bmaxs[2] && amaxs[2] >= bmins[2]; +} + + +qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + vec3_t forward = { 0.f,0.f,0.f }; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(other->s.origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + return true; + + return false; +} + +// Test if bot is facing toward an origin +qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + vec3_t forward = { 0.f,0.f,0.f }; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + return true; + + return false; +} + +// Test if bot is walking toward an origin +qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount) +{ + vec3_t vec = { 0.f,0.f,0.f }; + float dot = 0.f; + //vec3_t forward = { 0.f,0.f,0.f }; + + // Get the player's direction based from their velocity + vec3_t walkdir; + VectorSubtract(self->s.origin, self->bot.last_position, walkdir); + //VectorCopy(self->s.origin, self->bot.last_position); + VectorNormalize(walkdir); + vec3_t angle, forward, right; + vectoangles(walkdir, angle); + //VectorCopy(self->s.angles, angle); // Use the player's view angles (not their walk direction) + angle[0] = 0; + AngleVectors(angle, forward, right, NULL); + + + //AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(origin, self->s.origin, vec); + VectorNormalize(vec); + dot = DotProduct(vec, forward); + + if (amount == 0) + amount = 0.3f; + + if (dot > amount) + { + //Com_Printf("%s %s moving toward [%f]\n", __func__, self->client->pers.netname, dot); + return true; + } + + return false; +} + + +// True if [self] is aiming at [other] +// This func uses box intersection to determine if the player is aiming at the other entity +qboolean BOTLIB_IsAimingAt(edict_t* self, edict_t* other) +{ + // Project trace from the player's weapon POV + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 0, self->viewheight - 8); + G_ProjectSource(self->s.origin, offset, forward, right, start); + float dist = VectorDistance(self->s.origin, other->s.origin); + VectorMA(start, dist, forward, end); + + // Adjust the mins/maxs box size based on size + //vec3_t bmins = { -24, -24, +6 }; + //vec3_t bmaxs = { 24, 24, +10 }; + vec3_t bmins = { -32, -32, -16 }; + vec3_t bmaxs = { 32, 32, +16 }; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + VectorAdd(end, bmins, absmin); + VectorAdd(end, bmaxs, absmax); + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(other->s.number + (rand() % 256 + 1), other->s.origin, blue, other->mins, other->maxs, 100, false); // Player box + DrawBox(self->s.number + (rand() % 256 + 1), end, yellow, bmins, bmaxs, 100, false); // Projectile box + } +#endif + //rekkie -- debug drawing -- e + + if (BOTLIB_BoxIntersection(absmin, absmax, other->absmin, other->absmax)) // Do boxes intersect? + { + //Com_Printf("%s %s can hit %s\n", __func__, self->client->pers.netname, other->client->pers.netname); + return true; + } + return false; } \ No newline at end of file diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index f67b225a1..2f8b1b195 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -1,7311 +1,7311 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -botlib_noises_t botlib_noises; - -// Returns the XYZ distance -float VectorDistance(vec3_t start, vec3_t end) -{ - vec3_t v = { 0 }; - VectorSubtract(end, start, v); - return VectorLength(v); -} -// Returns the XY distance -float VectorDistanceXY(vec3_t start, vec3_t end) -{ - vec3_t v = { 0 }; - VectorSubtract(end, start, v); - v[2] = 0; - return VectorLength(v); -} - -// Converts node type to string -// Types: NODE_MOVE, NODE_CROUCH, NODE_STEP, NODE_JUMP, NODE_JUMPPAD, NODE_STAND_DROP, NODE_CROUCH_DROP, NODE_UNSAFE_DROP, NODE_LADDER_UP, NODE_LADDER_DOWN, NODE_DOOR, NODE_PLATFORM, NODE_TELEPORTER, NODE_ITEM, NODE_WATER, NODE_GRAPPLE, NODE_SPAWNPOINT, NODE_POI, NODE_LEARN -// String: The string to copy the node type to -// Max string size: The maximum size of the string -// Returns: false if node type was not found -qboolean NodeTypeToString(edict_t* self, int type, char *string, const int max_string_size) -{ - int len; - switch (type) - { - case NODE_MOVE: - len = strlen("MOVE"); // Get the length of the string - len = len < max_string_size ? len : max_string_size - 1; // If the string is too long, truncate it - strncpy(string, "MOVE", len); // Copy the string - break; - case NODE_CROUCH: - len = strlen("CROUCH"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "CROUCH", len); - break; - case NODE_BOXJUMP: - len = strlen("NODE_BOXJUMP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "NODE_BOXJUMP", len); - break; - case NODE_JUMP: - len = strlen("JUMP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "JUMP", len); - break; - case NODE_JUMPPAD: - len = strlen("JUMPPAD"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "JUMPPAD", len); - break; - case NODE_LADDER: - len = strlen("LADDER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER", len); - break; - case NODE_POI: - len = strlen("POI"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "POI", len); - break; - case NODE_POI_LOOKAT: - len = strlen("NODE_POI_LOOKAT"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "NODE_POI_LOOKAT", len); - break; - case NODE_WATER: - len = strlen("WATER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "WATER", len); - break; - - - - case NODE_STEP: - len = strlen("STEP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "STEP", len); - break; - case NODE_STAND_DROP: - len = strlen("STAND_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "STAND_DROP", len); - break; - case NODE_CROUCH_DROP: - len = strlen("CROUCH_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "CROUCH_DROP", len); - break; - case NODE_UNSAFE_DROP: - len = strlen("UNSAFE_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "UNSAFE_DROP", len); - break; - case NODE_LADDER_UP: - len = strlen("LADDER_UP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER_UP", len); - break; - case NODE_LADDER_DOWN: - len = strlen("LADDER_DOWN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER_DOWN", len); - break; - case NODE_DOOR: - len = strlen("DOOR"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "DOOR", len); - break; - case NODE_PLATFORM: - len = strlen("PLATFORM"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "PLATFORM", len); - break; - case NODE_TELEPORTER: - len = strlen("TELEPORTER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "TELEPORTER", len); - break; - case NODE_ITEM: - len = strlen("ITEM"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "ITEM", len); - break; - case NODE_GRAPPLE: - len = strlen("GRAPPLE"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "GRAPPLE", len); - break; - case NODE_SPAWNPOINT: - len = strlen("SPAWNPOINT"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "SPAWNPOINT", len); - break; - case NODE_LEARN: - len = strlen("LEARN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LEARN", len); - break; - default: - len = strlen("UNKNOWN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "UNKNOWN", len); - string[len] = '\0'; // Terminate string - return false; // Unknown node type - } - - string[len] = '\0'; // Terminate string - return true; // Success -} - -/////////////////////////////////////////////////////////////////////// -// Bot Movement - Node Type - Utility -// Display all the node type names -// onlyPrintProblemTypes: only output if types are INVALID -/////////////////////////////////////////////////////////////////////// -void PrintAllLinkNodeTypes(edict_t *self, qboolean onlyPrintProblemTypes) -{ - qboolean foundProblem = false; - int curr_node_position = 0; // The position of the current node in the node_list array - - if (self->bot.node_list_count <= 0) // No nodes found - return; - - for (int i = 0; i < self->bot.node_list_count; i++) // If the list has nodes - { - int node = self->bot.node_list[i]; // Next node in the list - if (node != INVALID && node == self->bot.current_node) // If the node is valid - { - curr_node_position = i; // Save the position of the current node - break; - } - } - - if (0) // Debug: print out the node list - { - Com_Printf("%s: count[%d] node_list[", __func__, self->bot.node_list_count); - for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position - { - Com_Printf(" %d ", self->bot.node_list[i]); - } - Com_Printf("]\n"); - } - - if (0) // Debug: print out the node types - { - int curr_node; - int next_node; - Com_Printf("%s: targetNodeTypes[", __func__); - for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position - { - if (i + 1 < self->bot.node_list_count) // If there is a next node - { - curr_node = self->bot.node_list[i]; // Get current node - next_node = self->bot.node_list[i + 1]; // Get next node - - // Get the node type this links to - for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) - { - if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node - { - Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); - //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) - // foundProblem = true; - break; - } - } - } - } - Com_Printf("]\n"); - } - - char tmp_string[32] = { '\0' }; // Length of the longest node type name - char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each - int tmp_type; - int curr_node; - int next_node; - //Com_Printf("%s: count[%d] node_list[", __func__, node_count); - for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position - { - if (i + 1 < self->bot.node_list_count) // If there is a next node - { - //Com_Printf(" %d ", node_list[i]); - curr_node = self->bot.node_list[i]; // Get current node - next_node = self->bot.node_list[i + 1]; // Get next node - - // Get the node type this links to - for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) - { - if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node - { - // Add the node number - char tmp_num[9]; // Max size of a node number as a char: "[999999]\0" == (8 characters + NULL terminator = length of 9) - sprintf(tmp_num, "[%d]", self->bot.node_list[i]); - strcat(all_node_types, tmp_num); - - // Add the node type - tmp_type = nodes[curr_node].links[l].targetNodeType; - if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) - foundProblem = true; - strcat(all_node_types, tmp_string); - if (i + 2 < self->bot.node_list_count) - strcat(all_node_types, " "); - break; - } - } - } - } - //Com_Printf("]\n"); - - Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); - - /* - #define MAX_PRINT_ALL_LINK_NODE_TYPES 32 - int node_list[MAX_PRINT_ALL_LINK_NODE_TYPES]; - int node_count = BOTLIB_SLL_Query_All_Nodes(&self->pathList, node_list, MAX_PRINT_ALL_LINK_NODE_TYPES); // Retrieve the nodes in the list, if any - - if (node_count <= 0) // No nodes found - return; - - - if (0) // Debug: print out the node list - { - Com_Printf("%s: count[%d] node_list[", __func__, node_count); - for (int i = 0; i < node_count; i++) - { - Com_Printf(" %d ", node_list[i]); - } - Com_Printf("]\n"); - } - - if (0) // Debug: print out the node types - { - int curr_node; - int next_node; - Com_Printf("%s: targetNodeTypes[", __func__); - for (int i = 0; i < node_count; i++) - { - if (i + 1 < node_count) // If there is a next node - { - curr_node = node_list[i]; // Get current node - next_node = node_list[i + 1]; // Get next node - - // Get the node type this links to - for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) - { - if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node - { - Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); - //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) - // foundProblem = true; - break; - } - } - } - } - Com_Printf("]\n"); - } - - - char tmp_string[32] = { '\0' }; // Length of the longest node type name - char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each - int tmp_type; - int curr_node; - int next_node; - //Com_Printf("%s: count[%d] node_list[", __func__, node_count); - for (int i = 0; i < node_count; i++) - { - if (i + 1 < node_count) // If there is a next node - { - //Com_Printf(" %d ", node_list[i]); - curr_node = node_list[i]; // Get current node - next_node = node_list[i + 1]; // Get next node - - // Get the node type this links to - for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) - { - if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node - { - // Add the node number - char tmp_num[6]; // Max size of a node number as a char: 999,999 (6 chars) - sprintf(tmp_num, "[%d]", node_list[i]); - strcat(all_node_types, tmp_num); - - // Add the node type - tmp_type = nodes[curr_node].links[l].targetNodeType; - if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) - foundProblem = true; - strcat(all_node_types, tmp_string); - if (i + 2 < node_count) - strcat(all_node_types, " "); - break; - } - } - } - } - //Com_Printf("]\n"); - - Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); - */ - -#if 0 - char curr_type[64]; - char next_type[64]; - if (NodeTypeToString(self, type_from, curr_type, sizeof(next_type)) == false) - foundProblem = true; - if (NodeTypeToString(self, type_to, next_type, sizeof(next_type)) == false) - foundProblem = true; - - // -------------------------------------------- - char after_next_type[64]; - after_next_type[0] = '\0'; - //int node_after_next = BOTLIB_SLL_Query_All_Nodes(&self->pathList); // Find out what node comes after the next node, if any - if (node_after_next == INVALID) - { - strcpy(after_next_type, "INVALID"); // No next node - } - else - { - - qboolean found_link_type = false; - if (self->bot.next_node != INVALID) - { - for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) - { - if (nodes[self->bot.next_node].links[l].targetNode == node_after_next) - { - Com_Printf("%s: targetNodeType[%d]\n", __func__, nodes[self->bot.next_node].links[l].targetNodeType); - found_link_type = true; - if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) - foundProblem = true; - break; - } - } - } - if (found_link_type == false) - strcpy(after_next_type, "INVALID"); // No next node - - - //Com_Printf("%s: cur[%d] nxt[%d] nxt_nxt[%d]\n", __func__, self->bot.current_node, self->bot.next_node, node_after_next); - if (NodeTypeToString(self, nodes[node_after_next].type, after_next_type, sizeof(after_next_type)) == false) - foundProblem = true; - } - // -------------------------------------------- - - if (onlyPrintProblemTypes) // Only print if errors found - { - if (foundProblem == false) // No error, so bail - return; - } - - if (self->groundentity) - Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [GROUND %d]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground); - else - Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [AIR %d] [ZVEL %f]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground, self->velocity[2]); -#endif -} - -/////////////////////////////////////////////////////////////////////// -// Changes the bots view angle -// -// move_vector: The move direction -// -// range: How close the current yaw needs to be to accepted as ideal -// <= 0: The bot will snap turn instantly -// > 0: Makes the change in angles a little more gradual, -// not so snappy. Subtle, but noticeable. -// Default: 2 -/////////////////////////////////////////////////////////////////////// -qboolean BOTLIB_UTIL_ChangeBotAngleYaw(edict_t* ent, vec3_t move_vector, float range) -{ - float ideal_yaw; - float current_yaw; - float speed; - vec3_t ideal_angle; - float yaw_move = 0.f; - float move_ratio = 1.f; - - // Normalize the move angle first - VectorNormalize(move_vector); - - current_yaw = anglemod(ent->s.angles[YAW]); - - vectoangles(move_vector, ideal_angle); - - ideal_yaw = anglemod(ideal_angle[YAW]); - - if (current_yaw == ideal_yaw) - return true; - - if (range <= 0) // Instant turn - { - ent->s.angles[YAW] = ideal_yaw; - return true; - } - else - { - // Turn speed based on angle of curr vs ideal - // If the turn angle is large, the bot will instantly snap - // This helps prevent circular motion when very close to a node - yaw_move = ideal_yaw - current_yaw; - if (ideal_yaw > current_yaw) - { - //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); - if (fabs(yaw_move) >= 45.0) // Snap turn - { - //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); - ent->s.angles[YAW] = ideal_yaw; - return false; - } - /* - float normal = 0; - if (current_yaw > ideal_yaw) - { - normal = (ideal_yaw + 1) / (current_yaw + 1); - } - else - { - normal = (current_yaw + 1) / (ideal_yaw + 1); - } - if (normal < 0.5) // Snap turn when angle is large - { - Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, normal); - ent->s.angles[YAW] = ideal_yaw; - return true; - } - */ - - // Yaw turn speed - if (current_yaw != ideal_yaw) - { - yaw_move = ideal_yaw - current_yaw; - speed = ent->yaw_speed / (float)BASE_FRAMERATE; - if (ideal_yaw > current_yaw) - { - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - if (yaw_move > 0) - { - if (yaw_move > speed) - yaw_move = speed; - } - else - { - if (yaw_move < -speed) - yaw_move = -speed; - } - } - - // Raptor007: Interpolate towards desired changes at higher fps. - if (!FRAMESYNC) - { - int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; - move_ratio = 1.f / (float)frames_until_sync; - } - - //rekkie -- s - const float turn_speed_factor = 1.25; // Slow down turn speed - move_ratio /= turn_speed_factor; // Slow turn speed by a factor of - //rekkie -- e - - ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); - - // Check if current_yaw is +/- from ideal_yaw - //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); - if (yaw_move < range && yaw_move > -(range)) - return true; // Ideal yaw reached - else - return false; // Ideal yaw not yet reached - - } -} - -// Change in angles can be gradual to snappy -// move_vector : The move direction -// instant : Instant 'snap' turn -// move_speed : How fast to turn, 1.0 is fast and 3.0 is slow -// yaw : Allow changing the yaw (left & right) -// pitch : Allow changing the pitch (up & down) -// Returns true if angle reached, or false if still moving toward angle -qboolean BOTLIB_ChangeBotAngleYawPitch(edict_t* ent, vec3_t move_vector, qboolean instant, float move_speed, qboolean yaw, qboolean pitch) -{ - float ideal_yaw; - float ideal_pitch; - float current_yaw; - float current_pitch; - float speed; - vec3_t ideal_angle; - float yaw_move = 0.f; - float pitch_move = 0.f; - float move_ratio = 1.f; - - // Normalize the move angle first - VectorNormalize(move_vector); - vectoangles(move_vector, ideal_angle); - - current_yaw = anglemod(ent->s.angles[YAW]); - current_pitch = anglemod(ent->s.angles[PITCH]); - - ideal_yaw = anglemod(ideal_angle[YAW]); - ideal_pitch = anglemod(ideal_angle[PITCH]); - - // Raptor007: Compensate for M4 climb. - if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) - ideal_pitch -= ent->client->kick_angles[PITCH]; - - // Yaw - if (instant) // Instant turn - { - ent->bot.bi.viewangles[YAW] = ideal_yaw; - } - else - { - // Turn speed based on angle of curr vs ideal - // If the turn angle is large, the bot will instantly snap - // This helps prevent circular motion when very close to a node - yaw_move = ideal_yaw - current_yaw; - if (ideal_yaw > current_yaw) - { - //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); - //if (fabs(yaw_move) >= 45.0) // Snap turn - //{ - //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); - //ent->bot.bi.viewangles[YAW] = ideal_yaw; - //return false; - //} - - if (current_yaw != ideal_yaw) - { - yaw_move = ideal_yaw - current_yaw; - speed = ent->yaw_speed / (float)BASE_FRAMERATE; - //Com_Printf("%s %s speed:%f\n", __func__, ent->client->pers.netname, speed); - speed *= (360 / fabs(yaw_move)); // Variable speed. Far away = faster turning. Close = slower turnung. - //Com_Printf("%s %s variable speed:%f\n", __func__, ent->client->pers.netname, speed); - if (ideal_yaw > current_yaw) - { - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - if (yaw_move > 0) - { - if (yaw_move > speed) - yaw_move = speed; - } - else - { - if (yaw_move < -speed) - yaw_move = -speed; - } - } - } - - - // Pitch - if (instant) // Instant turn - { - ent->bot.bi.viewangles[PITCH] = ideal_pitch; - } - - //if (fabs(pitch_move) >= 45.0) // Snap turn - //{ - //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(pitch_move)); - //ent->bot.bi.viewangles[PITCH] = pitch_move; - //return false; - //} - - if (current_pitch != ideal_pitch) - { - pitch_move = ideal_pitch - current_pitch; - speed = ent->yaw_speed / (float)BASE_FRAMERATE; - speed *= (360 / fabs(pitch_move)); // Variable speed. Far away = faster turning. Close = slower turnung. - if (ideal_pitch > current_pitch) - { - if (pitch_move >= 180) - pitch_move = pitch_move - 360; - } - else - { - if (pitch_move <= -180) - pitch_move = pitch_move + 360; - } - if (pitch_move > 0) - { - if (pitch_move > speed) - pitch_move = speed; - } - else - { - if (pitch_move < -speed) - pitch_move = -speed; - } - } - - if (instant) // Instant turn - return true; - - /* - // Raptor007: Interpolate towards desired changes at higher fps. - if (!FRAMESYNC) - { - int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; - move_ratio = 1.f / (float)frames_until_sync; - } - */ - - //move_ratio = 1; - move_ratio = 10.0 / HZ; - //Com_Printf("%s %s move_ratio[%f]\n", __func__, ent->client->pers.netname, move_ratio); - - //rekkie -- s - if (move_speed < 0.0) - move_speed = 0.0; - if (move_speed > 3.0) - move_speed = 3.0; - //float output = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - //Com_Printf("%s %s move_speed: %f\n", __func__, ent->client->pers.netname, move_speed); - float turn_speed_factor = 1 + move_speed; //2.25; // Slow down turn speed - move_ratio /= turn_speed_factor; // Slow turn speed by a factor of - //rekkie -- e - - if (yaw) - ent->bot.bi.viewangles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); - if (pitch) - ent->bot.bi.viewangles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); - - // Check if yaw_move and pitch_move is +/- from ideal_yaw - //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); - if (yaw && pitch && yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) - { - if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) - return true; // Ideal reached - else - return false; - } - if (yaw && !pitch) - { - if (yaw_move < 2 && yaw_move > -2) - return true; // Ideal reached - else - return false; - } - if (!yaw && pitch) - { - if (pitch_move < 2 && pitch_move > -2) - return true; // Ideal reached - else - return false; - } - - return false; -} - -/////////////////////////////////////////////////////////////////////// -// Checks if bot can move (really just checking the ground) -// Also, this is not a real accurate check, but does a -// pretty good job and looks for lava/slime. -/////////////////////////////////////////////////////////////////////// -// Can move safely at angle -qboolean BOTLIB_CanMoveAngle(edict_t* self, vec3_t angle) -{ - vec3_t forward, right; - vec3_t offset, start, end; - trace_t tr; - - // Set up the vectors - AngleVectors(angle, forward, right, NULL); - - VectorSet(offset, 32, 0, 0); - G_ProjectSource(self->s.origin, offset, forward, right, start); - - //VectorSet(offset, 32, 0, -32); // RiEvEr reduced drop distance to -110 NODE_MAX_FALL_HEIGHT - //G_ProjectSource(self->s.origin, offset, forward, right, end); - VectorCopy(start, end); - end[2] -= 32; - - tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); - if (tr.fraction == 1.0) - return false; - - /* - if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angle))) // avoid falling after LCA - || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT - { - return false; // can't move - } - */ - - return true; // yup, can move -} -// Can move safely in direction -qboolean BOTLIB_CanMoveInDirection(edict_t* self, vec3_t direction, float fwd_dist, float down_dist, qboolean rand_fwd_angle) -{ - trace_t tr; - vec3_t forward, right, start, end, offset, angles; - qboolean safe_forward = false; - qboolean safe_downward = false; - - vectoangles(direction, angles); // Direction to angles - - if (rand_fwd_angle) // Randomize the forward angle - { - angles[0] = 0; - angles[1] += 22.5 + (random() * 270); // Get a random angle - angles[2] = 0; - } - - // Get forward start & end - AngleVectors(angles, forward, right, NULL); // Angles to vectors - VectorSet(offset, 0, 0, self->viewheight); - G_ProjectSource(self->s.origin, offset, forward, right, start); // Get projection - VectorMA(start, fwd_dist, forward, end); // Project forward - - // Test forward - tr = gi.trace(start, tv(-16, -16, -5), tv(-16, -16, 32), end, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - if (tr.fraction == 1.0 && tr.startsolid == false) // No obstruction - { - safe_forward = true; // Way is safe forward - } - - if (0) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - // Test downward - VectorCopy(end, start); // Make the end the start - end[2] -= down_dist; // Move the end down - tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); - if (tr.fraction < 1.0 && tr.startsolid == false) // Found the ground - { - safe_downward = true; // Way is safe downward - } - - if (0) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - // Convert angles back to direction - AngleVectors(angles, direction, NULL, NULL); - - if (safe_forward && safe_downward) - return true; - else - return false; -} -// Can move safely in direction -qboolean BOTLIB_CanMoveDir(edict_t* self, vec3_t direction) -{ - vec3_t forward, right; - vec3_t offset, start, end; - vec3_t angles; - trace_t tr; - - // Now check to see if move will move us off an edge - VectorCopy(direction, angles); - VectorNormalize(angles); - vectoangles(angles, angles); - - // Set up the vectors - AngleVectors(angles, forward, right, NULL); - - VectorSet(offset, 36, 0, 24); // 24 - G_ProjectSource(self->s.origin, offset, forward, right, start); - - VectorSet(offset, 36, 0, -NODE_MAX_FALL_HEIGHT); //-NODE_MAX_FALL_HEIGHT); // -110 - G_ProjectSource(self->s.origin, offset, forward, right, end); - - tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); // Solid, lava, and slime - - if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA - || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA - || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT - { - return false; // can't move - } - - return true; // yup, can move -} -//rekkie -- Quake3 -- e - -/////////////////////////////////////////////////////////////////////// -// Make the change in angles a little more gradual, not so snappy -// Subtle, but noticeable. -// -// Modified from the original id ChangeYaw code... -/////////////////////////////////////////////////////////////////////// -qboolean BOTLIB_MOV_ChangeBotAngleYawPitch(edict_t* ent) -{ - float ideal_yaw; - float ideal_pitch; - float current_yaw; - float current_pitch; - float speed; - vec3_t ideal_angle; - float yaw_move = 0.f; - float pitch_move = 0.f; - float move_ratio = 1.f; - - // Normalize the move angle first - VectorNormalize(ent->move_vector); - - current_yaw = anglemod(ent->s.angles[YAW]); - current_pitch = anglemod(ent->s.angles[PITCH]); - - vectoangles(ent->move_vector, ideal_angle); - - ideal_yaw = anglemod(ideal_angle[YAW]); - ideal_pitch = anglemod(ideal_angle[PITCH]); - - // Raptor007: Compensate for M4 climb. - if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) - ideal_pitch -= ent->client->kick_angles[PITCH]; - - // Yaw - if (current_yaw != ideal_yaw) - { - yaw_move = ideal_yaw - current_yaw; - speed = ent->yaw_speed / (float)BASE_FRAMERATE; - if (ideal_yaw > current_yaw) - { - if (yaw_move >= 180) - yaw_move = yaw_move - 360; - } - else - { - if (yaw_move <= -180) - yaw_move = yaw_move + 360; - } - if (yaw_move > 0) - { - if (yaw_move > speed) - yaw_move = speed; - } - else - { - if (yaw_move < -speed) - yaw_move = -speed; - } - } - - // Pitch - if (current_pitch != ideal_pitch) - { - pitch_move = ideal_pitch - current_pitch; - speed = ent->yaw_speed / (float)BASE_FRAMERATE; - if (ideal_pitch > current_pitch) - { - if (pitch_move >= 180) - pitch_move = pitch_move - 360; - } - else - { - if (pitch_move <= -180) - pitch_move = pitch_move + 360; - } - if (pitch_move > 0) - { - if (pitch_move > speed) - pitch_move = speed; - } - else - { - if (pitch_move < -speed) - pitch_move = -speed; - } - } - - // Raptor007: Interpolate towards desired changes at higher fps. - if (!FRAMESYNC) - { - int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; - move_ratio = 1.f / (float)frames_until_sync; - } - - ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); - ent->s.angles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); - - // Check if yaw_move and pitch_move is +/- from ideal_yaw - //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); - if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) - return true; // Ideal yaw reached - else - return false; -} - -/////////////////////////////////////////////////////////////////////// -// Handle special cases of crouch/jump -// -// If the move is resolved here, this function returns true -/////////////////////////////////////////////////////////////////////// -qboolean Botlib_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd) -{ - vec3_t dir, forward, right, start, end, offset; - vec3_t top; - trace_t tr; - - // Get current direction - VectorCopy(self->client->ps.viewangles, dir); - dir[YAW] = self->s.angles[YAW]; - AngleVectors(dir, forward, right, NULL); - - VectorSet(offset, 0, 0, 0); // changed from 18,0,0 - G_ProjectSource(self->s.origin, offset, forward, right, start); - offset[0] += 18; - G_ProjectSource(self->s.origin, offset, forward, right, end); - - - start[2] += 18; // so they are not jumping all the time - end[2] += 18; - tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); - - - if (tr.fraction < 1.0) - { - //rekkie -- DEV_1 -- s - // Handles cases where bots get stuck on each other, for example in teamplay where they can block each other - // So lets try to make them jump over each other - if (tr.ent && tr.ent->is_bot && self->groundentity) - { - if (random() > 0.5) // try jumping - { - if (debug_mode && level.framenum % HZ == 0) - debug_printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); - self->velocity[2] += 400; - //ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = SPEED_RUN; - return true; - } - else // try crouching - { - if (debug_mode && level.framenum % HZ == 0) - debug_printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); - ucmd->upmove = -SPEED_RUN; - ucmd->forwardmove = SPEED_RUN; - return true; - } - } - //rekkie -- DEV_1 -- e - - // Check for crouching - start[2] -= 14; - end[2] -= 14; - VectorCopy(self->maxs, top); - top[2] = 0.0; // crouching height - tr = gi.trace(start, self->mins, top, end, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - if (debug_mode) debug_printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = -SPEED_RUN; - return true; - } - - // Check for jump - start[2] += 32; - end[2] += 32; - tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); - if (tr.fraction == 1.0) - { - if (debug_mode) debug_printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = SPEED_RUN; - return true; - } - } - - return false; // We did not resolve a move here -} - -//rekkie -- Quake3 -- s -// Special cases of crouch/jump when not using nodes -// Also resolves jumping or crouching under other players/bots in our way -// Returns the action taken: ACTION_JUMP, ACTION_CROUCH, ACTION_NONE -int BOTLIB_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd, vec3_t dir) -{ - vec3_t angle, forward, right, start, end, offset, origin; - trace_t tr; - qboolean crouch = false, head = false, jump = false; // Crouch space, head space, jump space - - // Get current direction - vectoangles(dir, angle); - AngleVectors(angle, forward, right, NULL); - VectorCopy(self->s.origin, origin); - origin[2] -= 24; // From the ground up - - VectorSet(offset, 0, 0, 0); - G_ProjectSource(origin, offset, forward, right, start); - offset[0] += 1; // Distance forward dir - G_ProjectSource(origin, offset, forward, right, end); - - if (0) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - - // Check if we hit our head - tr = gi.trace(tv(start[0], start[1], start[2] + 56), tv(-15, -15, 0), tv(15, 15, 1), tv(start[0], start[1], start[2] + 57), self, MASK_PLAYERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - head = true; // Hit our head on something immediately above us - } - - //vec3_t mins = { self->mins[0], self->mins[1], self->mins[2] - 0.15 }; - //vec3_t maxs = { self->maxs[0], self->maxs[1], self->maxs[2] }; - - - tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, 56), end, self, MASK_PLAYERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - - // Handle cases where bots get stuck on each other, for example in teamplay where they can block each other - // So lets try to make them jump or crouch - if (tr.ent && tr.ent->is_bot && self->groundentity) - { - if (random() > 0.5) // try jumping - { - //if (debug_mode && level.framenum % HZ == 0) - // Com_Printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); - return ACTION_JUMP; - } - else // try crouching - { - //if (debug_mode && level.framenum % HZ == 0) - // Com_Printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); - return ACTION_CROUCH; - } - } - - // Handle geometry cases - // Check for crouching - tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, STEPSIZE), end, self, MASK_PLAYERSOLID); //CROUCHING_MAXS2, 32 -- - if (tr.fraction == 1.0) - { - crouch = true; // We can crouch under something - } - - // Check for jump - tr = gi.trace(start, tv(-16,-16, 60), tv(16, 16, 61), end, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0 && tr.startsolid == false) - { - if (ACEMV_CanMove(self, MOVE_FORWARD)) - jump = true; // We can jump over something - } - - //Com_Printf("%s %s crouch[%d] head[%d] jump[%d]\n", __func__, self->client->pers.netname, crouch, head, jump); - - if ((crouch || head) && jump == false) - { - //Com_Printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); - return ACTION_CROUCH; - } - if (jump && (crouch == false && head == false)) - { - //Com_Printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); - return ACTION_JUMP; - } - } - - return ACTION_NONE; // We did not resolve a move here -} - -// Checks the bots movement direction against the direction of the next node. -// -int BOTLIB_DirectionCheck(edict_t *ent, byte *mov_strafe) -{ - if (ent->bot.next_node == INVALID) - return 0; - - // Check velocity - if (VectorEmpty(ent->velocity)) - return 0; - - // Get the bot's direction based from their velocity - vec3_t walkdir; - VectorSubtract(ent->s.origin, ent->lastPosition, walkdir); - VectorNormalize(walkdir); - vec3_t angle, forward, right, start, end, origin, offset; - vectoangles(walkdir, angle); - //VectorCopy(ent->s.angles, angle); // Use the bots's view angles (not their walk direction) - AngleVectors(angle, forward, right, NULL); - - VectorCopy(forward, ent->bot.bot_walk_dir); // Copy the forward walk direction - - - // Calculate the direction from the bot to the node - vec3_t node_dir; - VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, node_dir); - VectorNormalize(node_dir); - - // Calculate the dot product of the forward dir and the node dir - float dot = DotProduct(forward, node_dir); - // Calculate the dot to degrees - //float degrees = acos(dot) * 180 / M_PI; - //Com_Printf("%s node %d dot %f degrees %f\n", __func__, ent->bot.next_node, dot, degrees); - //if (dot > 0.995) - // Com_Printf("%s node %d dot %f\n", __func__, ent->bot.next_node, dot); - - float dot_right = DotProduct(right, node_dir); - VectorNegate(right, right); - float dot_left = DotProduct(right, node_dir); - if (dot_right > dot_left) // - { - //Com_Printf("%s [MOVE RIGHT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); - *mov_strafe = 1; - } - else - { - //Com_Printf("%s [MOVE LEFT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); - *mov_strafe = -1; - } - - if (0) // Show visual debug - { - VectorCopy(ent->s.origin, origin); - origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) - - VectorSet(offset, 0, 0, 0); - G_ProjectSource(origin, offset, forward, right, start); - offset[0] += 1024; // Distance forward dir - G_ProjectSource(origin, offset, forward, right, end); - - - // Bot walk direction - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(end); - gi.multicast(ent->s.origin, MULTICAST_PHS); - - /* - // Bot to node direction - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(nodes[ent->bot.next_node].origin); - gi.multicast(ent->s.origin, MULTICAST_PHS); - */ - } - - return dot; -} -//rekkie -- Quake3 -- e - -// Distance types from origin [self, current node, next node] -enum { - BOT_DIST_XYZ_SELF_CURR = 1, // [XYZ] Self -> Current node - BOT_DIST_XYZ_SELF_NEXT, // [XYZ] Self -> Next node - BOT_DIST_XYZ_CURR_NEXT, // [XYZ] Current node -> Next node - - BOT_DIST_XY_SELF_CURR, // [XY] Self -> Current node - BOT_DIST_XY_SELF_NEXT, // [XY] Self -> Next node - BOT_DIST_XY_CURR_NEXT // [XY] Current node -> Next node -}; -// Returns the distance between two points, either as XYZ or XY -// In the case of XY, the vec3_t dist is also returned with the Z value set to 0 -float BOTLIB_UTIL_Get_Distance(edict_t* self, vec3_t dist, int type) -{ - switch (type) - { - // XYZ - case BOT_DIST_XYZ_SELF_CURR: - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); - return VectorLength(dist); - case BOT_DIST_XYZ_SELF_NEXT: - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - return VectorLength(dist); - case BOT_DIST_XYZ_CURR_NEXT: - VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); - return VectorLength(dist); - - // XY - case BOT_DIST_XY_SELF_CURR: - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); - dist[2] = 0; // Remove the Z component - return VectorLength(dist); - case BOT_DIST_XY_SELF_NEXT: - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - dist[2] = 0; // Remove the Z component - return VectorLength(dist); - case BOT_DIST_XY_CURR_NEXT: - VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); - dist[2] = 0; // Remove the Z component - return VectorLength(dist); - default: - return 0; - } -} - -// Determines the closest point (between curr -> next node) the bot is nearest to -// -// [bot] -// [curr] -----------[*]---------------------------- [next] -// -// [*] is the origin nearest to the bot along the path from [curr] to [next] -// -void BOTLIB_UTIL_NEAREST_PATH_POINT(edict_t* self, usercmd_t* ucmd) -{ - if (self->bot.next_node == INVALID) return; - - // Get distance from bot to next node - vec3_t bot_to_next_vec; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); - float bot_to_next_dist = VectorLength(bot_to_next_vec); - - // Get distance from current node to next node - vec3_t curr_to_next_vec; - VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); - float curr_to_next_dist = VectorLength(curr_to_next_vec); - - // Calculate the normalized distance the bot has travelled between current and next node - float bot_travelled_dist = 0; - if (bot_to_next_dist < curr_to_next_dist) - bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); - else - bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); - - //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); - - // Get the origin between the current and next node based on the normalized bot_travelled_dist - LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, self->nearest_path_point); -} - -// Determines if we're off course while traversing a path (node1 ---> node2) -// Useful for strafing to get back on course -// -// Returns: '-1' if we're off course to the right (indicating we should strafe left) -// Returns: '0' if we're on course (or close enough) -// Returns: '1' if we're off course to the left (indicating we should strafe right) -int BOTLIB_UTIL_PATH_DEVIATION(edict_t* self, usercmd_t* ucmd) -{ - if (self->bot.next_node == INVALID) return 0; - /* - // Get distance from bot to next node - vec3_t bot_to_next_vec; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); - float bot_to_next_dist = VectorLength(bot_to_next_vec); - - // Get distance from current node to next node - vec3_t curr_to_next_vec; - VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); - float curr_to_next_dist = VectorLength(curr_to_next_vec); - - // Calculate the normalized distance the bot has travelled between current and next node - float bot_travelled_dist = 0; - if (bot_to_next_dist < curr_to_next_dist) - bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); - else - bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); - - - //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); - - // Get the origin between the current and next node based on the normalized bot_travelled_dist - vec3_t normalized_origin; - LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, normalized_origin); - - VectorCopy(normalized_origin, self->nearest_path_point); // Make a copy - */ - - BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point - - // Show a laser between next node and normalized_origin - if (1) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(nodes[self->bot.next_node].origin); - gi.WritePosition(self->nearest_path_point); - gi.multicast(nodes[self->bot.next_node].origin, MULTICAST_PVS); - } - - // Calculate the angle between the bot's origin and the normalized origin - vec3_t normalized_origin_vec; - VectorSubtract(self->nearest_path_point, self->s.origin, normalized_origin_vec); - normalized_origin_vec[2] = 0; // Flatten the vector - float bot_to_path_dist = VectorLength(normalized_origin_vec); - //Com_Printf("%s %s [d %f]\n", __func__, self->client->pers.netname, bot_to_path_dist); - - // Calculate the angle between the bot's origin and the normalized origin - float current_yaw = anglemod(self->s.angles[YAW]); - vec3_t ideal_angle; - vectoangles(normalized_origin_vec, ideal_angle); - float ideal_yaw = anglemod(ideal_angle[YAW]); - - // print current_yaw and ideal_yaw - //Com_Printf("%s %s [c %f] [i %f]\n", __func__, self->client->pers.netname, current_yaw, ideal_yaw); - - // Figure out if current yaw is closer to idea yaw if we turn left - //const float less_than_90_degrees = 87; // 90 is the optimum angle, but we want to give a little leeway (otherwise the bot's yaw movement will oscillate) - float left_yaw; - float right_yaw; - - if (bot_to_path_dist > 16) // Give a little leeway (otherwise the bot's yaw movement will oscillate) - { - if (ideal_yaw > current_yaw) - { - right_yaw = ideal_yaw - current_yaw; - left_yaw = 360 - right_yaw; - } - else - { - left_yaw = current_yaw - ideal_yaw; - right_yaw = 360 - left_yaw; - } - - //Com_Printf("%s %s [c %f] [i %f] [ry %f] [ly %f] [dist %f]\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, right_yaw, left_yaw, bot_to_path_dist); - - if (right_yaw < left_yaw) - { - //Com_Printf("%s %s need to strafe left %f\n", __func__, self->client->pers.netname, left_yaw); - //if (ACEMV_CanMove(self, MOVE_LEFT)) - { - //Com_Printf("%s %s --> strafe left %f\n", __func__, self->client->pers.netname, left_yaw); - //ucmd->sidemove = -SPEED_WALK; - return -1; // Strafe left - } - } - else - { - //Com_Printf("%s %s need to strafe right %f\n", __func__, self->client->pers.netname, left_yaw); - //if (ACEMV_CanMove(self, MOVE_RIGHT)) - { - //Com_Printf("%s %s --> strafe right %f\n", __func__, self->client->pers.netname, right_yaw); - //ucmd->sidemove = SPEED_WALK; - return 1; // Strafe right - } - } - } - - return 0; // Strafe neither left or right -} - -// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) -// Useful for strafing around obstacles while following a path -// -// forward_distance: distance to probe forward -// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) -// -// Returns: 'true' if forward vector is clear of obstructions, -// Returns: 'false' if forward vector is obstructed -qboolean BOTLIB_UTIL_PATH_FORWARD(edict_t* self, usercmd_t* ucmd, float forward_distance, float side_distance) -{ - // Send out trace on the left or right side parallel to the current direction the bot is facing - - vec3_t groundvec, end; - vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; - float forward_back, left_or_right, up_or_down, beam_width; - - groundvec[PITCH] = 0; - groundvec[YAW] = self->client->v_angle[YAW]; - groundvec[ROLL] = 0; - - if (forward_distance == 0) - forward_distance = 64; // Distance to probe forward - - AngleVectors(groundvec, forward, NULL, NULL); // Make a forwards pointing vector out of the bot's view angles - VectorMA(self->s.origin, forward_distance, forward, end); // Make end equal to 60* the forward pointing vector - - beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall - forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward - - // +Right or -Left of the bot's current direction - if (side_distance > 0) // side_distance is greater than zero - left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) - else // side_distance is less than zero - left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) - - // +Up or -Down (from the ground) - up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 - - VectorSet(offset, forward_back, left_or_right, up_or_down); - AngleVectors(groundvec, forward, right, NULL); - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector - - if (1) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(down_left_end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) - trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); - //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - return true; // Obstructed - } - - return false; // Clear -} - -//rekkie -- Quake3 -- s -// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) -// Useful for strafing around obstacles while following a path -// -// dir: direction vector -// forward_distance: distance to probe forward -// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) -// -// Returns: 'true' if forward vector is clear of obstructions, -// Returns: 'false' if forward vector is obstructed -qboolean BOTLIB_TEST_FORWARD_VEC(edict_t* self, vec3_t dir, float forward_distance, float side_distance) -{ - // Send out trace on the left or right side parallel to the current direction the bot is facing - - vec3_t groundvec; - vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; - float forward_back, left_or_right, up_or_down, beam_width; - - if (forward_distance == 0) - forward_distance = 64; // Distance to probe forward - - beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall - forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward - - // +Right or -Left of the bot's current direction - if (side_distance > 0) // side_distance is greater than zero - left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) - else // side_distance is less than zero - left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) - // +Up or -Down (from the ground) - up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 - - VectorSet(offset, forward_back, left_or_right, up_or_down); - vectoangles(dir, groundvec); - AngleVectors(groundvec, forward, right, NULL); - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector - - if (0) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(down_left_end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) - trace_t tr = gi.trace(start, tv(-4, -4, STEPSIZE+0.01), tv(4, 4, 56), down_left_end, self, MASK_MONSTERSOLID); - //trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); - //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - return true; // Obstructed - } - - return false; // Clear -} -//rekkie -- Quake3 -- e - -/////////////////////////////////////////////////////////////////////// -// Bot Movement (based on node type) -/////////////////////////////////////////////////////////////////////// -void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) -{ -#if 0 - trace_t tr; - float distance = 0; - //float distance_xy; // Distance XYZ and Distance XY - vec3_t dist = { 0 }; - //vec3_t dist_xy = { 0 }; // Distance XYZ and Distance XY - vec3_t velocity; - int i = 0, current_node_type = INVALID, next_node_type = INVALID; - - - - - // Do not follow path when teammates are still inside us. - if (OnTransparentList(self)) - { - //rekkie -- DEV_1 -- s - - // If the bot has just spawned, then we need to wait a bit before we start moving. - if (self->just_spawned) // set by SpawnPlayers() in a_team.c - { - self->just_spawned = false; - self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. - self->bot.state = BOT_MOVE_STATE_MOVE; - if (random() < 0.3) - self->just_spawned_timeout = level.framenum + (random() * 7) * HZ; // Long wait - else if (random() < 0.7) - self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Average wait - else - self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait - return; - } - if (self->just_spawned_go && self->just_spawned_timeout > level.framenum) // Wait - { - return; // It's not time to move yet, wait! - } - else if (self->just_spawned_go) - { - // Now we can move! - self->just_spawned_go = false; - //ACEAI_PickLongRangeGoal(self); // Lets pick a long range goal - return; - } - if (0) // Don't wander - { - //rekkie -- DEV_1 -- e - - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + (random() + 0.5) * HZ; - return; - - //rekkie -- DEV_1 -- s - } - //rekkie -- DEV_1 -- e - } - - // Get current and next node back from nav code. - if (!BOTLIB_FollowPath(self)) - { - if (!teamplay->value || (teamplay->value && level.framenum >= self->teamPauseTime)) - { - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - } - else - { - // Teamplay mode - just fan out and chill - if (self->bot.state == STATE_FLEE) - { - self->bot.state = STATE_POSITION; - self->wander_timeout = level.framenum + 3.0 * HZ; - } - else - { - self->bot.state = STATE_POSITION; - self->wander_timeout = level.framenum + 1.0 * HZ; - } - } - self->bot.goal_node = INVALID; - return; - } - - if (self->bot.current_node == INVALID || self->bot.next_node == INVALID) // rekkie -- safey check because current / next node can be INVALID - { - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + (random() + 0.5) * HZ; - return; - } - - - - // Current node type - current_node_type = nodes[self->bot.current_node].type; - - // Next node type - // We now get the next node type from the link between current and next node - // Using the link from the current node to the next node, find the next node type - int targetNodeLink = INVALID; // Used to keep track of which link to upgrade to a jumppad - if (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN) - { - //targetNodeLink = 1; - next_node_type = nodes[self->bot.next_node].type; - } - else - { - for (i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) - { - if (nodes[self->bot.current_node].links[i].targetNode == self->bot.next_node) - { - targetNodeLink = i; - next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type - - self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type - self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type - break; - } - } - } - - //if (next_node_type == NODE_JUMP) - // Com_Printf("%s %s [cn:%i] JUMP_NODE [nn:%i]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node); - - - //rekkie -- DEV_1 -- s - //if (current_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMPPAD) - // gi.dprintf("%s %s [ct:%i] [nt:%i]\n", __func__, self->client->pers.netname, current_node_type, next_node_type); - - // If the next node is high enough, make it a jump - /* - if (next_node_type == NODE_MOVE && self->client->leg_damage == 0) - { - - // See if there is a gap / hole between the current and next node, if so we need to jump across - vec3_t pt_25, pt_50, pt_75; - VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_25); // 25% - VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_50); // 50% - VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_75); // 75% - VectorScale(pt_25, 0.25, pt_25); // Scale 25% - VectorScale(pt_50, 0.50, pt_50); // Scale 50% - VectorScale(pt_75, 0.75, pt_75); // Scale 75% - // Using the points between the current and next node, trace a line down and see if one of them hits air - trace_t tr_25, tr_50, tr_75; - vec3_t end_25, end_50, end_75; - VectorCopy(pt_25, end_25); - VectorCopy(pt_50, end_50); - VectorCopy(pt_75, end_75); - end_25[2] -= NODE_MAX_JUMP_HEIGHT; - end_50[2] -= NODE_MAX_JUMP_HEIGHT; - end_75[2] -= NODE_MAX_JUMP_HEIGHT; - tr_25 = gi.trace(pt_25, vec3_origin, vec3_origin, end_25, self, MASK_SOLID); - tr_50 = gi.trace(pt_50, vec3_origin, vec3_origin, end_50, self, MASK_SOLID); - tr_75 = gi.trace(pt_75, vec3_origin, vec3_origin, end_75, self, MASK_SOLID); - // If one of our trace points hit nothing - if ( (tr_25.fraction == 1.0 && tr_25.allsolid == false) || - (tr_50.fraction == 1.0 && tr_50.allsolid == false) || - (tr_75.fraction == 1.0 && tr_75.allsolid == false) ) - { - next_node_type = NODE_JUMPPAD; - } - - // Otherwise lets see if the next_node is higher than current_node, if so jump to it - //else - { - // Distance between current and next node. - //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - //distance = VectorLength(dist); - - // Calculate if nodes[self->bot.next_node].origin[2] is higher than self->s.origin[2] - float higher = 0; float lower = 0; - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) - higher = (nodes[self->bot.next_node].origin[2] - nodes[self->bot.current_node].origin[2]); - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) - lower = (nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]); - //if (distance >= 128 || higher >= NODE_MAX_JUMP_HEIGHT || lower >= NODE_MAX_JUMP_HEIGHT) - //if (higher >= NODE_MAX_JUMP_HEIGHT) // || lower >= NODE_MAX_CROUCH_FALL_HEIGHT) - if (higher) // || lower) - { - next_node_type = NODE_JUMPPAD; - } - } - } - */ - - if (current_node_type == INVALID || next_node_type == INVALID) - { - Com_Printf("%s curr[%d]type[%d] next[%d]type[%d] Goal[%d] State[%d]\n", __func__, self->bot.current_node, current_node_type, self->bot.next_node, next_node_type, self->bot.goal_node, self->bot.state); - } - - - // DEBUG: print current and next node types - //if (1) - { - //const qboolean printOnlyErrors = true; // If only printing INVALID nodes - //PrintNodeType(self, current_node_type, next_node_type, printOnlyErrors); - } - - - - - - - // Contents check - vec3_t temp = { 0,0,0 }; - VectorCopy(self->s.origin, temp); - temp[2] += 22; - int contents_head = gi.pointcontents(temp); - temp[2] = self->s.origin[2] - 8; - int contents_feet = gi.pointcontents(temp); - // If in water, force bot to wander... - if (contents_feet & MASK_WATER) - { - self->bot.state = STATE_WANDER; - return; - //self->bot.goal_node = INVALID; - //self->bot.current_node = INVALID; - //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); - } - - - - //if (next_node_type == INVALID && self->bot.goal_node == INVALID) - if (next_node_type == INVALID) //self->bot.current_node == self->bot.next_node == self->bot.goal_node) - { - //next_node_type = NODE_MOVE; - - /* - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && M_CheckBottom(self, 2)) - { - if (ACEMV_CanMove(self, MOVE_FORWARD)) - { - ucmd->forwardmove = SPEED_WALK * sqrtf(xy_dist_self_curr / 64); - return; - } - //else if (ACEMV_CanMove(self, MOVE_BACK)) - // ucmd->forwardmove = -64; - //else if (ACEMV_CanMove(self, MOVE_RIGHT)) - // ucmd->sidemove = 64; - //else if (ACEMV_CanMove(self, MOVE_LEFT)) - // ucmd->sidemove = -64; - } - */ - } - - - // - //if (current_node_type == NODE_JUMPPAD && self->last_touched_ground > 0) - //{ - //ucmd->forwardmove = 0; - //VectorClear(self->velocity); - //return; - //} - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_CURR_NEXT); - ////VectorSubtract(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, dist_xy); - ////dist_xy[2] = 0; // Drop height component - ////distance_xy = VectorLength(dist_xy); - - // Strafe jumping - // Vary it up by doing JUMPPAD 'strafe jumping' in place of MOVE to the target if far enough away - if (0) - { - if (next_node_type == NODE_MOVE && abs(nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]) < STEPSIZE && distance > 256) - { - if (self->strafe_jumps > 30) // Allow jumps to be missed - self->strafe_jumps = 0; // Reset - - if (self->strafe_jumps < 10) // Mutiple jumps in series (although not contigous) - { - next_node_type = NODE_JUMPPAD; - } - - self->strafe_jumps++; // Count strafe jumps - } - } - - //if (random() < 0.01) - //{ - //self->client->leg_damage = 1; // DEBUG: force leg damage - //Com_Printf("%s %s leg damage\n", __func__, self->client->pers.netname); - //} - - // If we have leg damage, prevent jumping - // Look for a nearby move node to take cover - if (self->client->leg_damage > 0) // Do we have leg damage? - { - next_node_type = NODE_MOVE; - - /* - if (next_node_type != NODE_MOVE) - { - qboolean foundMoveNode = false; - - // Search for a nearby move node - for (i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_MOVE) - { - // Check if any enemies can see this node - qboolean canEnemiesSeeNextNode = false; - for (int p = 0; p < num_players; p++) // Cycle based on how many players we find - { - if (ACEAI_IsEnemy(self, players[p])) // Are they our enemy - { - // trace line to see if enemy is visible from the targetNode - int targetNode = nodes[self->bot.current_node].links[i].targetNode; - tr = gi.trace(nodes[targetNode].origin, NULL, NULL, players[p]->s.origin, self, MASK_ALL); - //if (tr.ent) - // Com_Printf("%s %s found ent [%s]\n", __func__, self->client->pers.netname, tr.ent->classname); - if (tr.ent == players[p]) // Enemy can see this node - { - //Com_Printf("%s %s found MOVE node [%d] but enemy [%s] can see it\n", __func__, self->client->pers.netname, targetNode, players[p]->client->pers.netname); - canEnemiesSeeNextNode = true; - break; - } - } - } - - if (canEnemiesSeeNextNode) // Enemy can see this node, so skip it - continue; - else // No enemies can see this node, so use it - { - self->bot.next_node = nodes[self->bot.current_node].links[i].targetNode; - next_node_type = NODE_MOVE; - foundMoveNode = true; - Com_Printf("%s %s found MOVE node for cover [%d]\n", __func__, self->client->pers.netname, self->bot.next_node); - break; - } - } - } - - // Otherwise wander - if (foundMoveNode == false) - { - Com_Printf("%s %s failed to find MOVE node, wandering...\n", __func__, self->client->pers.netname); - self->bot.state = STATE_WANDER; - self->wander_timeout = level.time + 5.0; - return; - } - } - */ - } - //return; - - - //if ((current_node_type == NODE_LADDER || next_node_type == NODE_LADDER) && OnLadder(self) == false) - // next_node_type = NODE_JUMPPAD; - //rekkie -- DEV_1 -- e - - /////////////////////// - // Grenade Avoidance // - /////////////////////// - // Try to avoid hand grenades by strafing, backing away, and crouching - edict_t* target = findradius(NULL, self->s.origin, 512); - while (target) - { - if (target->classname == NULL) - return; - - if (strcmp(target->classname, "hgrenade") == 0) - { - //Com_Printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); - if (debug_mode) debug_printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); - - VectorSubtract(target->s.origin, self->s.origin, self->move_vector); - ACEMV_ChangeBotAngle(self); - - // Try strafing - if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - else - ucmd->sidemove = 0; - - self->bot_strafe = ucmd->sidemove; - - // Also try move backwards - if (ACEMV_CanMove(self, MOVE_BACK)) - ucmd->forwardmove = -SPEED_RUN; - - // And crouch if its very close - VectorSubtract(target->s.origin, self->s.origin, dist); - dist[2] = 0; - if (VectorLength(dist) < 192) - ucmd->upmove = -SPEED_WALK; - - return; - } - - target = findradius(target, self->s.origin, 512); - } - - //////////////////// - // Stay on course // - //////////////////// - /* - if (self->bot.next_node != self->bot.current_node && current_node_type == NODE_MOVE && next_node_type == NODE_MOVE) - { - // Decide if the bot should strafe to stay on course. - vec3_t v = { 0,0,0 }; - VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, v); - v[2] = 0; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - dist[2] = 0; - if ((DotProduct(v, dist) > 0) && (VectorLength(v) > 16)) - { - float left = 0; - VectorNormalize(v); - VectorRotate2(v, 90); - left = DotProduct(v, dist); - if ((left > 16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_RIGHT))) - ucmd->sidemove = SPEED_RUN; - else if ((left < -16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_LEFT))) - ucmd->sidemove = -SPEED_RUN; - } - } - */ - - - - - - ////////////////////// - // Object Avoidance // - ////////////////////// - - // Calculate the players current speed and direction - float forward_or_back_velocity = 0; - float left_or_right_velocity = 0; - if (1) - { - vec3_t velocity; - VectorCopy(self->velocity, velocity); - velocity[2] = 0; - float speed = VectorLength(velocity); - - vec3_t angles; - VectorCopy(self->client->v_angle, angles); - angles[0] = 0; - angles[2] = 0; - - vec3_t forward, right; - AngleVectors(angles, forward, right, NULL); - - forward_or_back_velocity = DotProduct(forward, velocity); - left_or_right_velocity = DotProduct(right, velocity); - //Com_Printf("%s: speed %f forward_or_back_velocity %f left_or_right_velocity %f\n", __func__, speed, forward_or_back_velocity, left_or_right_velocity); - } - - // Check to see if stuck, and if so try to free us. Also handles crouching - VectorSubtract(self->s.origin, self->lastPosition, dist); - if (self->groundentity && forward_or_back_velocity > -30 && forward_or_back_velocity < 30) - { - if (Botlib_Crouch_Or_Jump(self, ucmd)) - { - Com_Printf("%s %s attempted to crouch or jump \n", __func__, self->client->pers.netname); - return; - } - - // Check if we are obstructed by an oncoming teammate, and if so strafe - trace_t tr; - tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) - { - if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - ACEMV_ChangeBotAngle(self); - return; - } - } - - /* - // Check to see if stuck, and if so try to free us. Also handles crouching - VectorSubtract(self->s.origin, self->lastPosition, dist); - if ((VectorLength(self->velocity) < 37) || (VectorLength(dist) < FRAMETIME)) - { - if (self->groundentity) - { - //if (random() > 0.5 && - if (Botlib_Crouch_Or_Jump(self, ucmd)) - return; - - // Check if we are obstructed by an oncoming teammate, and if so strafe - trace_t tr; - tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) - { - if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - ACEMV_ChangeBotAngle(self); - return; - } - } - } - */ - /* - /////////////////////// - // corner management // - /////////////////////// - //RiEvEr -- My corner management code - if (BOTCOL_CheckBump(self, ucmd)) - { - if (BOTCOL_CanStrafeSafely(self, self->s.angles)) - { - ucmd->sidemove = self->bot_strafe; - return true; - } - } - //R - */ - - /* - float botspeed = VectorLength(self->velocity); - if (botspeed == 0 && self->last_touched_ground == 0) - { - Com_Printf("%s %s is stuck: STATE_WANDER\n", __func__, self->client->pers.netname); - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - */ - /* - // Check if we're standing on top of another player - // OR touching another player - if (VectorLength(self->velocity) <= 32) // && self->last_touched_ground > 0) - { - //tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_DEADSOLID); - tr = gi.trace(self->s.origin, tv(-32, -32, -16), tv(32, 32, 24), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2]), self, MASK_DEADSOLID); - if (tr.ent && tr.ent->client && tr.ent->health > 0) - { - Com_Printf("%s %s is stuck on %s: wandering\n", __func__, self->client->pers.netname, tr.ent->client->pers.netname); - self->bot.state = STATE_WANDER; // Aquire a new node - return; - } - if (tr.ent && tr.ent->classname) - { - Com_Printf("%s %s is touching: %s\n", __func__, self->client->pers.netname, tr.ent->classname); - } - } - */ - - - // Grab items along the way, if they're not already our goal item to fetch - if (self->movetarget && self->movetarget->inuse && self->movetarget != self->goalentity) - { - // Try to navigate to the item - /* - int item_node = ACEND_FindClosestReachableNode(self->goalentity, NODE_DENSITY, NODE_ALL); - if (item_node != INVALID) - { - if (self->bot.prev_node != self->bot.goal_node) - self->bot.prev_node = self->bot.goal_node; // Keep a copy of previous goal - self->bot.goal_node = item_node; // Make the item node our new goal - - // Try visit node near item - // Otherwise visit previous goal node - if (BOTLIB_CanVisitNode(self, self->bot.goal_node) == false) // If we cannot visit item node - { - self->bot.goal_node = self->bot.prev_node; // Restore previous goal - BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal - } - } - */ - - // When we're near the item we desire, jump to it - vec3_t item_vec; - VectorSubtract(self->s.origin, self->movetarget->s.origin, item_vec); - float dist = VectorLength(item_vec); - if (dist < 256) - { - //if (self->movetarget->inuse == false) self->movetarget = NULL; //ignore freed ents - - if (dist < 32) - { - //Com_Printf("%s %s successfully got short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); - self->movetarget = NULL; - //self->bot.goal_node = self->bot.prev_node; // Restore previous goal - //BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal - return; - } - else// if (ACEIT_IsReachable(self, self->movetarget->s.origin)) - { - //Com_Printf("%s %s heading for short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); - - VectorSubtract(self->movetarget->s.origin, self->s.origin, self->move_vector); - ACEMV_ChangeBotAngle(self); - //ucmd->forwardmove = SPEED_RUN; - if (self->groundentity) - { - VectorClear(self->velocity); - LaunchPlayer(self, self->movetarget->s.origin); - self->movetarget = NULL; - //self->bot.state = STATE_WANDER; - return; - } - //return; - } - } - } - - - - - if (next_node_type == NODE_CROUCH) - { - // Make sure we're facing the target - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) - return; - - // Crouch move forward - ucmd->upmove = -SPEED_RUN; - ucmd->forwardmove = SPEED_RUN; - return; - } - - //////////////////////////////////////////////////////// - // Jumpad Nodes - /////////////////////////////////////////////////////// - if (next_node_type == NODE_JUMPPAD) - { - - //if (self->groundentity && self->velocity[2] <= 0) - //{ - // VectorClear(self->velocity); - // LaunchPlayer(self, nodes[self->bot.next_node].origin); - // return; - //} - - /* - vec3_t dist; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - float next_node_distance = VectorLength(dist); - - dist[2] = 0; // Drop height component - float next_node_distance_xy = VectorLength(dist); - - vec3_t curr_node_dist; - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, curr_node_dist); - float curr_node_distance = VectorLength(curr_node_dist); - */ - - - /* - // If the bot's distance is moving away from the target (failed the jump) - // Try to find another route, otherwise find a new node and go there instead - if (self->prev_next_node == self->bot.next_node) - { - if (next_node_distance < self->next_node_distance) // Update if getting closer - self->next_node_distance = next_node_distance; - else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump - { - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); - BOTLIB_SetGoal(self, self->bot.goal_node); - } - else - { - Com_Printf("%s %s failed to jumppad up, finding new goal\n", __func__, self->client->pers.netname); - self->bot.state = STATE_WANDER; // Aquire a new node - } - return; - } - } - if (self->prev_next_node != self->bot.next_node) // Update previous next node and distance - { - self->prev_next_node = self->bot.next_node; - self->next_node_distance = next_node_distance; - } - */ - /* - // If the bot fell below the target (failed the jump down) - if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) - { - // Upgrade target node by moving its origin higher - if (targetNodeLink != INVALID) - { - //if (debug_mode) - Com_Printf("%s %s dropped from a jumppad forward, moving jumppad node higher\n", __func__, self->client->pers.netname); - int n = nodes[self->bot.current_node].links[targetNodeLink].targetNode; - nodes[n].origin[2]++; - } - } - */ - - - - - - /* - // Bot fell below current and next target - //if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.current_node].origin[2]) - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); - if (self->last_touched_ground == 0 && distance > 96) - { - int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - if (tmp_node != self->bot.current_node) - { - self->bot.prev_node = self->bot.current_node; - self->bot.current_node = tmp_node; - //Com_Printf("%s %s [NODE_JUMPPAD][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - } - - if (self->bot.current_node == INVALID) - { - if (debug_mode) - Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - if (debug_mode) - Com_Printf("%s (1) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - //BOTLIB_SetGoal(self, self->bot.goal_node); - return; - } - } - */ - - - - - - // See if we need a ladder jump boost - float speed = VectorLength(self->velocity); - if (self->bot.current_node == NODE_LADDER_UP && speed < 32 && self->last_touched_ground > 0) - { - //if (debug_mode) - // Com_Printf("%s %s is stuck [NODE_JUMPPAD]...\n", __func__, self->client->pers.netname); - - // project forward from origin - // Box test [feet to mid body] -> forward 16 units - vec3_t forward; - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorMA(self->s.origin, 32, forward, forward); - tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction < 1 || tr.startsolid) - { - ucmd->upmove = 400; // Try jumping - if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - self->velocity[2] = 200; - //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: jumping\n", __func__, self->client->pers.netname); - } - else - { - ucmd->upmove = -400; // Try crouching - //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: crouching\n", __func__, self->client->pers.netname); - } - } - if (speed == 0 && self->last_touched_ground == 0) // We're not moving at all - { - //Com_Printf("%s %s is stuck [NODE_JUMPPAD]... speed: 0\n", __func__, self->client->pers.netname); - ucmd->forwardmove = -50; // Try backing up a tad, then try jumping again - } - - - - - - - - //* - // Guide the bot in when close to the target - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ///distance = VectorLength(dist); - vec3_t dist_xy; - float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - /* - if (0) - if (distance <= 50 || distance_xy < 32 || VectorLength(self->velocity) < 32)// && next_node_type != NODE_LADDER_UP) //32 - { - //self->s.angles[PITCH] = 0; // Look directly forward - - // When very close to the next node change yaw angle only - //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - BOTLIB_MOV_ChangeBotAngleYawPitch(self); - - //Com_Printf("%s %s jumppad self->last_touched_ground = %d\n", __func__, self->client->pers.netname, self->last_touched_ground); - - //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction == 1.0) - { - float speed = VectorLength(self->velocity); - //if (debug_mode) - // Com_Printf("%s guiding the bot in from jump [speed %f]\n", __func__, speed); - - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - distance = VectorLength(dist); // Get the absolute length - - ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. - - // Apply it - //if (!ACEMV_CanMove(self, MOVE_FORWARD)) - // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down - //else - // VectorScale(dist, 128, self->velocity); - //VectorScale(dist, 224, self->velocity); - - - //vec3_t dist_xy; - //float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); - //Com_Printf("%s %s distance_xy %f\n", __func__, self->client->pers.netname, distance_xy); - if (distance_xy > 16) - { - if (self->groundentity || ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop - { - // touching the ground but on an edge with a drop - slow down - if (ACEMV_CanMove(self, MOVE_FORWARD) == false) - { - //ucmd->forwardmove = -1; - ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); - VectorScale(dist, 128, self->velocity); - Com_Printf("%s %s jumppad (1)\n", __func__, self->client->pers.netname); - } - else // touching the ground - safe to move at a normal pace - { - VectorScale(dist, SPEED_RUN, self->velocity); - ucmd->forwardmove = SPEED_RUN; - Com_Printf("%s %s jumppad (2)\n", __func__, self->client->pers.netname); - } - } - else if (distance <= 16) // Ensure we slow right down if needed - { - //VectorScale(dist, max((SPEED_WALK * sqrtf(distance / 128)), 32), self->velocity); // Slow down (min speed 32) - - ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); - VectorScale(dist, 128, self->velocity); - Com_Printf("%s %s jumppad (3)\n", __func__, self->client->pers.netname); - } - else - { - //ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); - //VectorScale(dist, 224, self->velocity); - Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); - } - - if (self->s.origin[2] > nodes[self->bot.next_node].origin[2]) - self->velocity[2] -= 32; - - // If we're now touching the ground, just move at a normal pace - //if (self->last_touched_ground == 0)// && self->s.origin[2] == nodes[self->bot.next_node].origin[2]) - { - //Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); - //VectorScale(dist, SPEED_RUN, self->velocity); - //ucmd->forwardmove = SPEED_RUN; - } - - } - else if (distance_xy <= 16 && self->s.origin[2] > nodes[self->bot.next_node].origin[2]) - { - Com_Printf("%s %s jumppad (5a)\n", __func__, self->client->pers.netname); - ucmd->forwardmove = 1; - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] -= 32; - //return; - - if (self->groundentity) - { - Com_Printf("%s %s jumppad (5b)\n", __func__, self->client->pers.netname); - //ucmd->forwardmove = 128 * sqrtf(distance / 256); - VectorScale(dist, 128, self->velocity); - } - } - //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2] && !self->groundentity) - else if (distance_xy <= 16 && (self->s.origin[2] + NODE_Z_HALF_HEIGHT) < nodes[self->bot.next_node].origin[2]) - { - Com_Printf("%s %s jumppad (6)\n", __func__, self->client->pers.netname); - - //float z_height_diff = fabs(nodes[self->bot.next_node].origin[2]) - fabs(self->s.origin[2]); - //if (z_height_diff < NODE_MAX_JUMP_HEIGHT) - { - // Try jumping - //if (debug_mode) - // Com_Printf("%s %s failed to jumppad. Trying to jump next node %d\n", __func__, self->client->pers.netname, self->bot.next_node); - //ucmd->upmove = SPEED_RUN; - //ucmd->forwardmove = SPEED_RUN; - //return; - //if (Botlib_Crouch_Or_Jump(self, ucmd)) - // return; - } - //return; - - int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - if (tmp_node != self->bot.current_node) - { - self->bot.prev_node = self->bot.current_node; - self->bot.current_node = tmp_node; - //Com_Printf("%s %s [NODE_JUMPPAD][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - } - - if (self->bot.current_node == INVALID) - { - if (debug_mode) - Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - if (debug_mode) - Com_Printf("%s (2) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - BOTLIB_SetGoal(self, self->bot.goal_node); - return; - } - } - else - { - //if (BOTLIB_MOV_ChangeBotAngleYawPitch(self)) - if (self->groundentity) - { - Com_Printf("%s %s jumppad (7)\n", __func__, self->client->pers.netname); - //ucmd->forwardmove = 128 * sqrtf(distance / 256); - VectorScale(dist, 128, self->velocity); - } - } - - - // See if we're stuck, try to unstick - if (speed < 32 && self->last_touched_ground > 0) - { - if (debug_mode) - Com_Printf("%s correcting jump, unsticking\n", __func__); - - // project forward from origin - // Box test [feet to mid body] -> forward 16 units - vec3_t forward; - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorMA(self->s.origin, 32, forward, forward); - tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction < 1 || tr.startsolid) - { - ucmd->upmove = 400; // Try jumping - if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - self->velocity[2] = 200; - } - else - ucmd->upmove = -400; // Try crouching - } - - return; - } - } - */ - /* - else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) - { - tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction > 0.9) - { - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - next_node_distance = VectorLength(dist); // Get the absolute length - - // Apply it - if (next_node_distance > 48) - VectorScale(dist, SPEED_RUN, self->velocity); - else if (next_node_distance > 24) - VectorScale(dist, 350, self->velocity); - else - VectorScale(dist, 300, self->velocity); - - return; - } - } - */ - - - float gravity = self->gravity * sv_gravity->value; - - ACEMV_ChangeBotAngle(self); - - float boost_jump = 1; // Boost jump in rare instances - - /* - if (next_node_distance_xy > 64) - ucmd->forwardmove = SPEED_RUN; // Move forward - else if (self->groundentity && next_node_distance_xy < 64) - { - // Try alternative route - BOTLIB_SetGoal(self, self->bot.goal_node); - return; - } - */ - /* - else if (nodes[self->bot.next_node].origin[2] > self->s.origin[2] + 96) // It's fairly high above the player (and not a step) - { - // Check we've reach our ideal yaw angle - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) - return; - - // Check we can move backward safely - if (ACEMV_CanMove(self, MOVE_BACK)) - { - ucmd->forwardmove = -SPEED_RUN; // Move back a bit - return; - } - else - { - boost_jump = 2; // Could not move back, so cheat and boost jump - } - } - */ - - //if (self->velocity[0] || self->velocity[1] || self->velocity[2]) - //{ - // VectorClear(self->velocity); // Zero current velocity to kill any momentum - // return; - //} - - //if (self->groundentity || next_node_distance < NODE_Z_HEIGHT) - //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - // distance between curr_node and self->bot.current_node - - //Com_Printf("%s %s can jumppad? last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); - - // Slightly oversized player standing box ( +2 on sides, +8 bottom, but not top ) - //vec3_t expanded_player_standing_mins = { -18, -18, -32 }; // default: -16, -16, -24 - //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 - // Figure out if player is touching against a wall or floor - //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); - - //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance < 24)) // && curr_node == self->bot.current_node)) - //if (self->groundentity || curr_node_distance < 24 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) - //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) - //if (self->last_touched_ground == 0) - if (self->last_jumppad_node != self->bot.current_node) // && self->velocity[2] > -200) // apply jumppad if we're on a different current_node - //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) - //if (self->last_touched_ground < 10) - //if (self->groundentity) - //if (self->groundentity || tr.fraction < 1.0 || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 48) // touching ground - //if (self->groundentity || tr.fraction < 1.0) // touching ground - //if (self->groundentity || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) // touching ground - //if (self->groundentity || (self->velocity[2] <= 0 && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32)) - //if ( (self->groundentity || tr.fraction < 1.0) && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_CURR) <= 32) - { - // Skip doing a jumpnode... if the node we jumped from (current_node) changed. - // This occurs when a jump is made and fails to reach the next_node; because the current node is updated just before landing (in the wrong spot) - //if (self->last_jumppad_node == self->bot.prev_node && self->last_touched_ground > 2) - //{ - //Com_Printf("%s %s jumppad failed to reach target, self->last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); - // self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from - // return; - //} - - // Slightly oversized player standing box ( +2 on sides, +2 bottom, but not top ) - //vec3_t expanded_player_standing_mins = { -18, -18, -26 }; // default: -16, -16, -24 - //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 - // Figure out if player is touching against a wall or floor - //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); - //if (tr.fraction == 1.0) // is the player in the air? - // return; // bug out - - //Com_Printf("%s %s applied jumppad %f\n", __func__, self->client->pers.netname, level.time); - - VectorClear(self->velocity); // Zero current velocity to kill any momentum - self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from - - // Check we've reach our ideal yaw angle and we're not falling - //if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) // || self->velocity[2] < 0) - // return; - - if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) - { - //VectorClear(self->velocity); // Zero velocity - ucmd->forwardmove = 1; - //return; - } - - - // Check if we're off course for a jump - vec3_t chk_dist; - float chk_distance = BOTLIB_UTIL_Get_Distance(self, chk_dist, BOT_DIST_XYZ_SELF_CURR); - if (chk_distance > 128) - { - //if (debug_mode) - // Com_Printf("%s %s correcting jump that is off course\n", __func__, self->client->pers.netname); - - // Change the bot's vector to face the previous node - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); - - // Face previous node - if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) - { - //VectorClear(self->velocity); // Zero velocity - ucmd->forwardmove = 1; - } - - // Return the bot's movement vector back to the target - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); - } - - - - - float z_velocity = self->velocity[2]; - if (z_velocity < 0) - z_velocity = (z_velocity - (z_velocity * 2)); // Make it positive - - // Calculate the jump height to get to the target - float jump_height = sqrt(2 * (gravity * distance)); - - //Com_Printf("%s %s jump_height: %f\n", __func__, self->client->pers.netname, jump_height/16); - - // Calculate the time it will take to get to the target - float time = distance / (z_velocity + jump_height / 2.0); - - // Calculate the velocity at the end of the jump - VectorScale(dist, 1.0 / time, velocity); - - velocity[2] = jump_height / 2.0; - - // If the target is above the player, increase the velocity to get to the target - float z_height; - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2])//self->s.origin[2]) - { - z_height = ((nodes[self->bot.next_node].origin[2] - self->s.origin[2]) * boost_jump); - z_height += sqrtf(distance) + NODE_Z_HEIGHT; - velocity[2] += z_height; - } - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) - { - z_height = (self->s.origin[2] - nodes[self->bot.next_node].origin[2]) - NODE_Z_HEIGHT; - velocity[2] -= z_height; - } - - // If self->velocity[2] is minus, make it positive and add it to the required velocity - if (self->velocity[2] < 0 && self->groundentity == NULL) - velocity[2] += -self->velocity[2]; - - - // Calculate speed from velocity - //float speed = VectorLength(velocity); - //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); - - // Limit max speed - //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER - { - // Set the velocity of the player to the calculated velocity - VectorCopy(velocity, self->velocity); - VectorCopy(velocity, self->client->oldvelocity); - VectorCopy(velocity, self->avelocity); - - return; - } - } - else - { - next_node_type = NODE_MOVE; - } - - /* - // Guide the bot in when close to the target - else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) - { - tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction > 0.9) - { - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - next_node_distance = VectorLength(dist); // Get the absolute length - - // Apply it - if (next_node_distance > 48) - VectorScale(dist, SPEED_RUN, self->velocity); - else if (next_node_distance > 24) - VectorScale(dist, 350, self->velocity); - else - VectorScale(dist, 300, self->velocity); - } - } - */ - - - - //return; - } - - //////////////////////////////////////////////////////// - // Jump Node - /////////////////////////////////////////////////////// - if (next_node_type == NODE_JUMP) - { - - // If the bot fell below the target (failed the jump) - // Try to find another route, otherwise find a new node and go there instead - if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) - { - // Upgrade target node type to jumppad - if (targetNodeLink != INVALID) - { - if (debug_mode) - Com_Printf("%s %s fell from a jump node, upgrading node to jumppad\n", __func__, self->client->pers.netname); - nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; - - // Update the node we're near - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (tmp_node != self->bot.current_node) - //{ - // self->bot.prev_node = self->bot.current_node; - // self->bot.current_node = tmp_node; - //Com_Printf("%s %s [NODE_JUMP][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - //} - - return; - } - else - { - if (debug_mode) - Com_Printf("%s %s failed to jump node to goal %d because targetNodeLink was invalid, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; // Aquire a new node - return; - } - } - - // See if we're stuck, try to unstick - float speed = VectorLength(self->velocity); - if (speed < 32 && self->last_touched_ground == 0) - { - //if (debug_mode) - // Com_Printf("%s %s is stuck, trying to resolve\n", __func__, self->client->pers.netname); - - // project forward from origin - // Box test [feet to mid body] -> forward 16 units - vec3_t forward; - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorMA(self->s.origin, 32, forward, forward); - tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction < 1 || tr.startsolid) - { - ucmd->upmove = 400; // Try jumping - if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - self->velocity[2] = 200; - //Com_Printf("%s %s is stuck [NODE_JUMP]: jumping\n", __func__, self->client->pers.netname); - } - else - { - ucmd->upmove = -400; // Try crouching - //Com_Printf("%s %s is stuck [NODE_JUMP]: crouching\n", __func__, self->client->pers.netname); - } - } - - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - // Lose the vertical component - ////dist[2] = 0; - // Get the absolute length - ////distance = VectorLength(dist); - - //if (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) - { - // Jump only when we are moving the correct direction. - VectorCopy(self->velocity, velocity); - velocity[2] = 0; - VectorNormalize(velocity); - VectorNormalize(dist); - if (DotProduct(velocity, dist) > 0.8) - ucmd->upmove = SPEED_RUN; - - //Kill running movement -// self->move_vector[0]=0; -// self->move_vector[1]=0; -// self->move_vector[2]=0; - // Set up a jump move - if (distance < 256) - ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); - else - ucmd->forwardmove = SPEED_RUN; - - //self->move_vector[2] *= 2; - - ACEMV_ChangeBotAngle(self); - - //rekkie -- DEV_1 -- s - // Guide the bot in when close to the target - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////distance = VectorLength(dist); - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - if (distance <= 64) - { - tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction > 0.7) - { - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 200, self->velocity); // Apply it - } - } - //rekkie -- DEV_1 -- e - - /* - // RiEvEr - re-instate the velocity hack - if (self->jumphack_timeout < level.framenum) - { - VectorCopy(self->move_vector,dist); - if ((440 * distance / 128) < 440) - VectorScale(dist,(440 * distance / 128),self->velocity); - else - VectorScale(dist,440,self->velocity); - self->jumphack_timeout = level.framenum + 3.0 * HZ; - } - */ - } - /* - else - { - self->bot.goal_node = INVALID; - } - */ - return; - } - - //////////////////////////////////////////////////////// - // Ladder Nodes - /////////////////////////////////////////////////////// - //rekkie -- DEV_1 -- s - if (current_node_type == NODE_LADDER_UP || current_node_type == NODE_LADDER_DOWN || - next_node_type == NODE_LADDER_UP || next_node_type == NODE_LADDER_DOWN) - { - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); // Find the distance vector to the next node - ////distance = VectorLength(dist); - - ////distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); // Find the distance vector to the next node - ////dist_xy[2] = 0; // Lose the vertical component - ////distance_xy = VectorLength(dist_xy); // Get the absolute length - - float speed = VectorLength(self->velocity); // Speed of the player - - // If we are going down the ladder, check for teammates coming up and yield to them. - trace_t tr; - tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0)) - { - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] = 200; - self->bot.state = STATE_WANDER; - return; - } - - // If getting off the top of a ladder - if (current_node_type == NODE_LADDER_UP && next_node_type != NODE_LADDER_DOWN) - { - ucmd->forwardmove = SPEED_WALK; - - //Com_Printf("%s %s on top ladder getting off, up vel %f\n", __func__, self->client->pers.netname, self->velocity[2]); - - //Com_Printf("%s %s on top ladder getting off. OnLadder %d\n", __func__, self->client->pers.netname, OnLadder(self)); - if (OnLadder(self)) - { - ucmd->upmove = SPEED_RUN; - return; - } - else - { - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 400, self->velocity); // Apply it - - if (self->velocity[2] >= 0 && self->velocity[2] < 200) // do we need a boost? - { - float zdiff = 0; - if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - zdiff = abs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]); - float boost = 64 + zdiff; - //float boost = 50; - //Com_Printf("%s %s on top ladder getting off, boosting jump from %f to %f\n", __func__, self->client->pers.netname, self->velocity[2], self->velocity[2] + boost); - self->velocity[2] += boost; // slight boost jump at top of ladder - } - - if (distance < 64 && self->s.origin[2] >= nodes[self->bot.next_node].origin[2]) - { - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - VectorScale(dist, 50, self->velocity); // Apply it - //Com_Printf("%s %s on top ladder getting off, guiding velocity\n", __func__, self->client->pers.netname); - } - } - - /* - if ((nodes[self->bot.next_node].origin[2] >= self->s.origin[2]) - && (nodes[self->bot.next_node].origin[2] >= nodes[self->bot.current_node].origin[2]) - && (((self->velocity[2] > 0) && !self->groundentity) || OnLadder(self))) - { - ucmd->upmove = SPEED_RUN; - self->velocity[2] = min(200, BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) * 10); // Jump higher for farther node - } - - if (1) - { - // Guide the bot in when close to the target - if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 32 && distance < 128) - { - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 400, self->velocity); // Apply it - } - } - */ - - ACEMV_ChangeBotAngle(self); - return; - } - - - // If getting off the bottom of a ladder - if (current_node_type == NODE_LADDER_DOWN && next_node_type != NODE_LADDER_UP) - { - // We used to do nothing here, because it was handled by the next node type... but now we just walk forward :) - - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - ucmd->forwardmove = SPEED_RUN; - } - - - // Getting onto bottom of the ladder - if (current_node_type != NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN && distance < 192 && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) - { - //Com_Printf("%s %s getting onto bottom of the ladder\n", __func__, self->client->pers.netname); - - // Jump only when we are moving the correct direction. - // Check we've reach our ideal yaw angle - //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - //qboolean isOnLadder = OnLadder(self); - //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); - //if (isOnLadder == false) - if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) > 16) - { - //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - //ucmd->upmove = SPEED_WALK; - ucmd->forwardmove = 96; - - //Com_Printf("%s %s moving to bottom of the ladder\n", __func__, self->client->pers.netname); - return; - } - else - { - //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); - //current_node_type = NODE_LADDER_DOWN; - //next_node_type = NODE_LADDER_UP; - } - - /* - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && OnLadder(self) == false) - //if (OnLadder(self) == false) - { - ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = SPEED_WALK; - } - */ - - - if (1) - { - //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); - //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 32) - // self->velocity[2] = 100; - //return; - - // Guide the bot in when close to the target - //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] + 32 && distance < 128) - if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 128) - //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) - { - /* - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 32, self->velocity); // Apply it - */ - - if (OnLadder(self) == false) - { - ucmd->forwardmove = 32; - ucmd->upmove = 32; - - if (NODES_AdvanceToNextNode(self)) - return; - } - - else - { - /* - // Get distance - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - float next_node_distance = VectorLength(dist); // Get the absolute length - // Apply it - if (next_node_distance > 48) - VectorScale(dist, SPEED_RUN, self->velocity); - else if (next_node_distance > 24) - VectorScale(dist, 350, self->velocity); - else - VectorScale(dist, 300, self->velocity); - */ - } - - /* - if (OnLadder(self) == false) - ucmd->forwardmove = 64; - else - { - // Make bot look directly up from its current pos - vec3_t above; - VectorCopy(self->s.origin, above); - above[2] + 32; - VectorSubtract(above, self->s.origin, self->move_vector); // point up - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // look up - - ucmd->upmove = SPEED_RUN; - //self->s.origin[2] = nodes[self->bot.next_node].origin[2]; - } - - // If touching ladder, advance to next node (ladder down -> ladder up) - if (OnLadder(self)) - { - if (NODES_AdvanceToNextNode(self)) - return; - } - */ - - //Com_Printf("%s %s is at bottom of the ladder, OnLadder:%d\n", __func__, self->client->pers.netname, OnLadder(self)); - } - return; - } - - - //if (OnLadder(self) == false) - //return; - } - - // Getting onto top of the ladder - if (current_node_type != NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP)// && self->groundentity) - { - // Jump only when we are moving the correct direction. - VectorCopy(self->velocity, velocity); - velocity[2] = 0; - VectorNormalize(velocity); - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - VectorNormalize(dist); - float dot = DotProduct(velocity, dist); - ////VectorNormalize(dist_xy); - ////float dot = DotProduct(velocity, dist_xy); - if (dot < 0.8 && dot != 0) // Make bot face ladder - { - ACEMV_ChangeBotAngle(self); - //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); - } - else // Correct direction, jump up - { - if (distance > 64) - ucmd->forwardmove = SPEED_WALK; - else if (distance > 48) - ucmd->forwardmove = 50; - else if (distance > 24) - ucmd->forwardmove = 25; - } - - // Guide the bot in when close to the target - //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - //distance = VectorLength(dist); - if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] && distance <= 128) - { - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 100, self->velocity); // Apply it - } - - // - //if (OnLadder(self) == false) - return; - } - - // On ladder going up - if (current_node_type == NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP) - { - //Com_Printf("%s %s on ladder going up\n", __func__, self->client->pers.netname); - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance < 64) - ////if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance_xy < 64) - { - VectorCopy(self->velocity, velocity); - velocity[2] = 0; - VectorNormalize(velocity); - VectorNormalize(dist); - float dot = DotProduct(velocity, dist); - ////VectorNormalize(dist_xy); - ////float dot = DotProduct(velocity, dist_xy); - if (dot < 0.8 && dot != 0) // Make bot face ladder - { - ACEMV_ChangeBotAngle(self); - //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); - } - - if (!self->groundentity) - { - // FIXME: Dirty hack so the bots can actually use ladders. - //vec3_t origin_up; - //VectorCopy(self->s.origin, origin_up); - //origin_up[2] += 8; - //VectorSubtract(origin_up, self->s.origin, dist_xy); - - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); - ////VectorNormalize(dist_xy); - ////VectorScale(dist_xy, SPEED_RUN, self->velocity); - ////if (dist_xy[2] > 0) - //// self->velocity[2] = min(200, self->velocity[2]); - VectorScale(dist, SPEED_RUN, self->velocity); - if (dist[2] > 0) - self->velocity[2] = min(200, self->velocity[2]); - } - - // Guide the bot in when close to the target - // Useful for the tall ladder found on murder.bsp - helps to deal with ladders that use playerclip - //if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16 && distance < 64) - if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16) - { - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - VectorNormalize(dist); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - VectorScale(dist, 200, self->velocity); // Apply it - } - - return; - } - } - - // On ladder going down - if (current_node_type == NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN) - { - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance < 64) //&& speed <= SPEED_ROAM) - ////if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance_xy < 64) //&& speed <= SPEED_ROAM) - { - ucmd->forwardmove = SPEED_WALK / 2; - ////if ((distance_xy < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) - if ((distance < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) - ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching - - if (!self->groundentity) - { - VectorCopy(self->velocity, velocity); - velocity[2] = 0; - VectorNormalize(velocity); - VectorNormalize(dist); - float dot = DotProduct(velocity, dist); - ////VectorNormalize(dist_xy); - ////float dot = DotProduct(velocity, dist_xy); - if (dot < 0.8 && dot != 0) // Make bot face ladder - { - //ACEMV_ChangeBotAngle(self); - //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); - } - - // FIXME: Dirty hack so the bots can actually use ladders. - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); - ////VectorNormalize(dist_xy); - ////VectorScale(dist_xy, SPEED_RUN, self->velocity); - ////if (dist_xy[2] < 0) - //// self->velocity[2] = max(-200, self->velocity[2]); - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - VectorNormalize(dist); - VectorScale(dist, SPEED_RUN, self->velocity); - if (dist[2] < 0) - self->velocity[2] = max(-200, self->velocity[2]); - } - return; - } - } - } - //rekkie -- DEV_1 -- e - - - //////////////////////////////////////////////////////// - // Water Nodes - /////////////////////////////////////////////////////// - if (current_node_type == NODE_WATER || self->waterlevel > 0) - { - ACEMV_ChangeBotAngle(self); - //ucmd->upmove = SPEED_RUN; - //ucmd->forwardmove = SPEED_RUN; - - // Ensure we have LoS - tr = gi.trace(self->s.origin, tv(-16, -16, -4), tv(16, 16, 4), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - - // Guide the bot in when close to the target - //if (next_node_type != NODE_WATER) // && distance <= 128) - { - BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - if (next_node_type != NODE_WATER) - { - VectorScale(dist, 400, self->velocity); // Jump out - ucmd->upmove = SPEED_RUN; - ucmd->forwardmove = SPEED_RUN; - } - else - VectorScale(dist, 200, self->velocity); // Move water speed - } - - /* - // We need to be pointed up/down - ACEMV_ChangeBotAngle(self); - - // If the next node is not in the water, then move up to get out. - if (next_node_type != NODE_WATER && !(gi.pointcontents(nodes[self->bot.next_node].origin) & MASK_WATER)) // Exit water - ucmd->upmove = SPEED_RUN; - - ucmd->forwardmove = SPEED_RUN * 3 / 4; - return; - */ - } - - //rekkie -- DEV_1 -- s - //////////////////////////////////////////////////////// - // NODE_STAND_DROP Nodes - if (next_node_type == NODE_STAND_DROP || next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) - { - if (0) - { - if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s next_node_type [NODE_STAND_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); - if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s next_node_type [NODE_CROUCH_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); - if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s next_node_type [NODE_UNSAFE_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); - } - - // If target is getting too far away, adjust our angle - float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - if (distance_xy > 32) - ACEMV_ChangeBotAngle(self); - - // Then keep walking - ucmd->forwardmove = SPEED_WALK; - - //Com_Printf("%s %s vel %f\n", __func__, self->client->pers.netname, VectorLength(self->velocity)); - - // If stuck, try clearing the obstacle by moving in a random dir - if (VectorLength(self->velocity) < 32) - { - //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); - - tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction < 1.0) // blocked, try find another path to the same goal - { - //Com_Printf("%s %s is blocked [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); - - //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //BOTLIB_CanVisitNode(self, self->bot.goal_node); - //ACEMV_ChangeBotAngle(self); - - self->s.angles[YAW] += 22.5 + (random() * 270); - self->s.angles[PITCH] = 0; - - //self->bot.state = STATE_WANDER; - return; - } - } - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - VectorNormalize(dist); - //if (self->velocity[2] < -400 || distance < 64) - //if (distance < 64) - //VectorScale(dist, 350, self->velocity); // Apply it - tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction == 1.0) - { - VectorScale(dist, 350, self->velocity); // Apply it - VectorCopy(self->velocity, self->client->oldvelocity); - VectorCopy(self->velocity, self->avelocity); - } - - return; - - /* - // If the bot fell below the target (failed the jump down) - // If the bot is trying an unsafe drop past the LCA timer - // Try to find another route, otherwise find a new node and go there instead - if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2] || - (next_node_type == NODE_UNSAFE_DROP && lights_camera_action > 23)) - { - // Update the node we're near - int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - if (tmp_node != self->bot.current_node) - { - self->bot.prev_node = self->bot.current_node; - self->bot.current_node = tmp_node; - //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - } - - if (self->bot.current_node == INVALID) - { - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + (random() + 0.5) * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - //Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); - BOTLIB_SetGoal(self, self->bot.goal_node); - } - else - { - //Com_Printf("%s %s failed to drop down, finding new goal\n", __func__, self->client->pers.netname); - self->bot.state = STATE_WANDER; // Aquire a new node - } - return; - } - - ACEMV_ChangeBotAngle(self); - - // If stuck, randomly crouch or jump - if (VectorLength(self->velocity) < 3) - { - //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); - if (random() > 0.5) - { - ucmd->upmove = -400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: crouching\n", __func__, self->client->pers.netname); - //return; - } - else - { - ucmd->upmove = 400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: jumping\n", __func__, self->client->pers.netname); - //return; - } - } - - - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////dist[2] = 0; - ////distance = VectorLength(dist); - - if (!self->groundentity) - { - // When in air, control speed carefully to avoid overshooting the next node. - if (distance < 256) - { - ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 128); //256 - // Crouch down - if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) - ucmd->upmove = -SPEED_WALK; - } - else - ucmd->forwardmove = SPEED_RUN; - } - else if (!ACEMV_CanMove(self, MOVE_FORWARD)) - { - // If we reached a ledge, slow down. - if (distance < 512) - { - // Jump unless dropping down to the next node. - //if (nodes[self->bot.next_node].origin[2] > (self->s.origin[2] + 7) && distance < 500) - // ucmd->upmove = SPEED_RUN; - - // Crouch down - if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) - ucmd->upmove = -SPEED_WALK; - - ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 512); - } - else - ucmd->forwardmove = SPEED_WALK; - } - else // Otherwise move as fast as we can. - ucmd->forwardmove = SPEED_RUN; - - - if(1) - // Guide the bot in when close to the target - //if (distance <= 128) //64 - if (self->velocity[2] < 0) - { - //if (self->velocity[2] < 32) - // ACEMV_ChangeBotAngle(self); - - //if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s: NODE_STAND_DROP\n", __func__, self->client->pers.netname); - //if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s: NODE_CROUCH_DROP\n", __func__, self->client->pers.netname); - //if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s: NODE_UNSAFE_DROP\n", __func__, self->client->pers.netname); - - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - VectorNormalize(dist); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////VectorNormalize(dist); - ////distance = VectorLength(dist); // Get the absolute length - - if (self->groundentity && ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop - ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); - - VectorScale(dist, 350, self->velocity); // Apply it - - return; - } - */ - } - //rekkie -- DEV_1 -- e - - - //////////////////////////////////////////////////////// - // Move & Step Nodes - /////////////////////////////////////////////////////// - if (current_node_type == NODE_MOVE || current_node_type == NODE_STEP || current_node_type == NODE_ITEM) - { - //Com_Printf("%s %s move forward\n", __func__, self->client->pers.netname); - - - { - // Get distance from bot to next node - vec3_t bot_to_prev_vec, bot_to_curr_vec, bot_to_next_vec; // Direction vector to nodes - float bot_to_prev_dist = 0, bot_to_curr_dist = 0, bot_to_next_dist = 0; // Distance to nodes - qboolean can_see_prev = false, can_see_curr = false, can_see_next = false; // If bot can see these nodes - if (self->bot.prev_node != INVALID) - { - VectorSubtract(nodes[self->bot.prev_node].origin, self->s.origin, bot_to_prev_vec); - bot_to_prev_dist = VectorLength(bot_to_prev_vec); - //trace_t prev_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); - trace_t prev_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); - if (prev_tr.fraction == 1.0) - can_see_prev = true; - } - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_curr_vec); - bot_to_curr_dist = VectorLength(bot_to_curr_vec); - //trace_t curr_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); - trace_t curr_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (curr_tr.fraction == 1.0) - can_see_curr = true; - - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); - bot_to_next_dist = VectorLength(bot_to_next_vec); - //trace_t next_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - trace_t next_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (next_tr.fraction == 1.0) - can_see_next = true; - - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) - { - //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); - if (bot_to_next_dist < 192) - { - VectorClear(self->velocity); - bot_to_next_vec[2] = 0; // Flatten the vector - VectorScale(bot_to_next_vec, 400, self->velocity); - } - else - ucmd->forwardmove = SPEED_RUN; - } - else - ucmd->forwardmove = SPEED_RUN; - - //qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right - //qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left - - // Stay on course by strafing back to the path line (if not already strafing) - if (1) - { - float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); - if (mov_strafe == 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - } - else if (mov_strafe > 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - //ucmd->sidemove = SPEED_WALK; - - ucmd->forwardmove = SPEED_WALK; - - // Get distance from bot to self->nearest_path_point - vec3_t bot_to_path_vec; // Direction vector to nodes - VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); - float bot_to_path_dist = VectorLength(bot_to_path_vec); - //ucmd->sidemove = SPEED_WALK * sqrtf(bot_to_path_dist / 256); - - bot_to_path_vec[2] = 0; // Flatten the vector - VectorNormalize(bot_to_path_vec); - VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it - } - else if (mov_strafe < 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - //ucmd->sidemove = -SPEED_WALK; - - ucmd->forwardmove = SPEED_WALK; - - // Get distance from bot to self->nearest_path_point - vec3_t bot_to_path_vec; // Direction vector to nodes - VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); - float bot_to_path_dist = VectorLength(bot_to_path_vec); - //ucmd->sidemove = -SPEED_WALK * sqrtf(bot_to_path_dist / 256); - - bot_to_path_vec[2] = 0; // Flatten the vector - VectorNormalize(bot_to_path_vec); - VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it - } - } - - /* - // Check if bot can see the nodes - if (can_see_next) - { - //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); - - //bot_to_next_vec[2] = 0; // Flatten the vector - //VectorNormalize(bot_to_next_vec); - //if (self->groundentity) - //{ - // ucmd->forwardmove = SPEED_RUN; - // VectorScale(bot_to_next_vec, 400, self->velocity); - //} - //else - ucmd->forwardmove = SPEED_RUN; - - //if (bot_to_next_dist < 64) - //BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2); // self->move_vector - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) - { - Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); - //ucmd->forwardmove = SPEED_RUN; - bot_to_next_vec[2] = 0; // Flatten the vector - VectorScale(bot_to_next_vec, 400, self->velocity); - //VectorClear(self->velocity); - } - - } - else if (can_see_curr) - { - Com_Printf("%s %s guiding bot: can_see_curr %f\n", __func__, self->client->pers.netname, bot_to_curr_dist); - ucmd->forwardmove = SPEED_RUN; - BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_curr_vec, 2); // self->move_vector - } - else if (can_see_prev) - { - Com_Printf("%s %s guiding bot: can_see_prev %f\n", __func__, self->client->pers.netname, bot_to_prev_dist); - ucmd->forwardmove = SPEED_RUN; - BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_prev_vec, 2); // self->move_vector - } - else - { - Com_Printf("%s %s guiding bot: Wander\n", __func__, self->client->pers.netname); - // Wander or Reroute - } - */ - - self->s.angles[PITCH] = 0; // Look directly forward - return; - } - - - { - //ucmd->forwardmove = SPEED_RUN/2; - //self->s.angles[PITCH] = 0; // Look directly forward - //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // Change the speed when close to node - //return; - } - - - - - - - - - - - - // Get distance from bot to next node - vec3_t bot_to_next_vec; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); - float bot_to_next_dist = VectorLength(bot_to_next_vec); - //Com_Printf("%s: %s bot_to_next_dist %f\n", __func__, self->client->pers.netname, bot_to_next_dist); - - // Guide bot into the next_node when close - if (bot_to_next_dist < 24) - { - tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - bot_to_next_vec[2] = 0; // Flatten the vector - VectorNormalize(bot_to_next_vec); - VectorScale(bot_to_next_vec, 200, self->velocity); - //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); - return; - } - } - - - // Change bot angle to face next node - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 11.25) == false) - { - VectorClear(self->velocity); // Kill velocity, to give bot time to turn - return; - } - - - // Strafe if we're off course - { - // If not strafing to get back on path - // Check if path forward is blocked - qboolean did_strafe_right = false; // If the bot made a strafe move right - qboolean did_strafe_left = false; // If the bot made a strafe move left - //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - //if (mov_strafe == 0 && tr.fraction < 1.0) - if (bot_to_next_dist > 64) - { - qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right - qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left - - qboolean mid_partial_blocked = false; - qboolean mid_full_blocked = false; - - // Line trace - tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - mid_partial_blocked = true; - - // Box trace -- full width of the bot - tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - mid_full_blocked = true; - } - - // All [BLOCKED] - if (mid_partial_blocked && right_blocked && left_blocked) - { - BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point - - // Bot -> nearest path point - tr = gi.trace(self->s.origin, NULL, NULL, self->nearest_path_point, self, MASK_DEADSOLID); - if (tr.fraction == 1.0 && tr.startsolid == false) - { - //Com_Printf("%s %s --> heading for nearest path point\n", __func__, self->client->pers.netname); - - // Move vector between bot and self->nearest_path_point - VectorSubtract(self->nearest_path_point, self->s.origin, self->move_vector); - - self->move_vector[2] = 0; // Flatten the vector - VectorNormalize(self->move_vector); - VectorScale(self->move_vector, 400, self->velocity); - return; - } - - // Bot -> current node point - tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); - if (tr.fraction == 1.0 && tr.startsolid == false) - { - Com_Printf("%s %s --> heading for current node point\n", __func__, self->client->pers.netname); - - // Move vector between bot and nodes[self->bot.current_node].origin - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); - - self->move_vector[2] = 0; // Flatten the vector - VectorNormalize(self->move_vector); - VectorScale(self->move_vector, 400, self->velocity); - return; - } - - // Bot -> re-route - // Update the node we're near - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (tmp_node != self->bot.current_node) - //{ - // self->bot.prev_node = self->bot.current_node; - // self->bot.current_node = tmp_node; - // Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - //} - if (self->bot.current_node == INVALID) - { - Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; - //self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - return; - } - - return; - } - - // mid [BLOCKED] && right [CLEAR] && left [CLEAR] - if (mid_partial_blocked && !right_blocked && !left_blocked) - { - //Com_Printf("%s: %s partial middle blocked\n", __func__, self->client->pers.netname); - - if (ucmd->sidemove > 0) // Bot was strafing right, so continue - { - //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - else if (ucmd->sidemove < 0) // Bot was strafing left, so continue - { - //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - else // Bot was not strafing, so pick a random direction - { - if (rand() % 2 == 0) - { - //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - else - { - //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - } - } - - // When bot encounters a thin pole, prevent it from getting stuck - if (mid_full_blocked && forward_or_back_velocity == 0) - { - //Com_Printf("%s: %s full middle blocked\n", __func__, self->client->pers.netname); - - if (ucmd->sidemove > 0) // Bot was strafing right, so continue - { - //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - else if (ucmd->sidemove < 0) // Bot was strafing left, so continue - { - //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - else // Bot was not strafing, so pick a random direction - { - if (rand() % 2 == 0) - { - //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - else - { - //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - } - } - - // NARROW strafe search -- RIGHT or LEFT [BLOCKED] - if (right_blocked) - { - //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); - if (ACEMV_CanMove(self, MOVE_LEFT)) - { - //Com_Printf("%s %s --> small strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - } - else if (left_blocked) - { - //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); - if (ACEMV_CanMove(self, MOVE_RIGHT)) - { - //Com_Printf("%s %s --> small strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - } - - // WIDE strafe search - if (!mid_partial_blocked && left_blocked && right_blocked) - { - //Com_Printf("%s: %s small left and right blocked\n", __func__, self->client->pers.netname); - - // Try a wider search - right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 32); // Check right - left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -32); // Check left - - if (right_blocked) - { - //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); - //if (ACEMV_CanMove(self, MOVE_LEFT)) - { - //Com_Printf("%s %s --> medium strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - } - - if (left_blocked) - { - //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); - //if (ACEMV_CanMove(self, MOVE_RIGHT)) - { - //Com_Printf("%s %s --> medium strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - } - - if (left_blocked && right_blocked) - { - //Com_Printf("%s: %s medium left and right blocked\n", __func__, self->client->pers.netname); - - // Try the widest search - right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 48); // Check right - left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -48); // Check left - - if (right_blocked) - { - //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); - //if (ACEMV_CanMove(self, MOVE_LEFT)) - { - //Com_Printf("%s %s --> large strafe left\n", __func__, self->client->pers.netname); - ucmd->sidemove = -SPEED_WALK; - did_strafe_left = true; - } - } - - if (left_blocked) - { - //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); - //if (ACEMV_CanMove(self, MOVE_RIGHT)) - { - //Com_Printf("%s %s --> large strafe right\n", __func__, self->client->pers.netname); - ucmd->sidemove = SPEED_WALK; - did_strafe_right = true; - } - } - } - } - - // Stay on course by strafing back to the path line (if not already strafing) - if (did_strafe_right == false && did_strafe_left == false && mid_partial_blocked == false) - { - float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); - if (mov_strafe == 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - //ucmd->sidemove = 0; - } - else if (mov_strafe > 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - ucmd->sidemove = SPEED_WALK; - } - else if (mov_strafe < 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - ucmd->sidemove = -SPEED_WALK; - } - } - } - - ucmd->forwardmove = SPEED_RUN; - return; - } - - - - - - - - - //distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); - // If stuck, try clearing the obstacle by moving in a random dir - if (VectorLength(self->velocity) <= 8) - { - //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) > 32) - //Com_Printf("%s %s is stuck [NODE_MOVE]...\n", __func__, self->client->pers.netname); - - tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); - if (tr.fraction < 1.0) // blocked, try find another path to the same goal - { - //Com_Printf("%s %s is blocked [NODE_MOVE]...\n", __func__, self->client->pers.netname); - - // Try strafing - if (random() > 0.5) - { - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); - ACEMV_ChangeBotAngle(self); - - // Try strafing - if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - else - ucmd->sidemove = 0; - - self->bot_strafe = ucmd->sidemove; - - if (ACEMV_CanMove(self, MOVE_FORWARD)) - ucmd->forwardmove = SPEED_RUN; - // Also try move backwards - else if (ACEMV_CanMove(self, MOVE_BACK)) - ucmd->forwardmove = -SPEED_RUN; - - return; - } - - /* - if (random() > 0.25) - { - //if (ACEMV_CanMove(self, MOVE_BACK)) - // ucmd->forwardmove = -SPEED_RUN; - if (ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - - self->s.angles[YAW] += 22.5 + (random() * 270); - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = SPEED_RUN; - return; - } - */ - - - //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //BOTLIB_CanVisitNode(self, self->bot.goal_node); - //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); - //ACEMV_ChangeBotAngle(self); - - //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); - //BOTLIB_MOV_ChangeBotAngleYawPitch(self); - /////////////////////////////////// LaunchPlayer(self, nodes[self->bot.current_node].origin); //////////////////////////// - //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); - //VectorNormalize(dist); - //VectorScale(dist, 400, self->velocity); // Apply it - //self->velocity[2] = 50; - - /* - //if (ACEMV_CanMove(self, MOVE_BACK)) - // ucmd->forwardmove = -SPEED_RUN; - if (ACEMV_CanMove(self, MOVE_LEFT)) - ucmd->sidemove = -SPEED_RUN; - else if (ACEMV_CanMove(self, MOVE_RIGHT)) - ucmd->sidemove = SPEED_RUN; - - //self->s.angles[YAW] += 22.5 + (random() * 270); - self->s.angles[YAW] += 180; - ucmd->forwardmove = SPEED_RUN; - ucmd->upmove = SPEED_RUN; - self->s.angles[PITCH] = 0; - */ - - // Make the bot look at the current node - //self->bot.next_node = self->bot.current_node; - //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); - //BOTLIB_MOV_ChangeBotAngleYawPitch(self); - //ucmd->forwardmove = SPEED_RUN; - - - - //self->bot.state = STATE_WANDER; - return; - } - } - - // If we cannot see the next node - tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); - if (tr.fraction < 1) - { - // Update the node we're near - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (tmp_node != self->bot.current_node) - //{ - // self->bot.prev_node = self->bot.current_node; - // self->bot.current_node = tmp_node; - // //Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - //} - - if (self->bot.current_node == INVALID) - { - if (debug_mode) - Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; - //self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - if (debug_mode) - Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - //BOTLIB_SetGoal(self, self->bot.goal_node); - return; - } - } - - // Run up stairs - if (current_node_type == NODE_STEP && next_node_type == NODE_STEP) - { - ucmd->forwardmove = SPEED_RUN; - self->s.angles[PITCH] = 0; // Look directly forward - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - //Com_Printf("%s %s Step --> Step\n", __func__, self->client->pers.netname); - return; - } - - /* - // If stuck, crouch or jump - if (VectorLength(self->velocity) < 3) - { - Com_Printf("%s %s is stuck...\n", __func__, self->client->pers.netname); - - trace_t tr_lower_body; - trace_t tr_upper_body; - vec3_t fwd; - AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin - VectorMA(self->s.origin, 16, fwd, fwd); - tr_lower_body = gi.trace(self->s.origin, tv(-5, -5, -14), tv(5, 5, 0), fwd, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units - tr_upper_body = gi.trace(self->s.origin, tv(-5, -5, 0), tv(5, 5, 32), fwd, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units - // Need to crouch? - // Lower body is free, upper body is blocked - if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) - { - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - ucmd->upmove = -400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - Com_Printf("%s %s is stuck [NODE_MOVE]: crouching\n", __func__, self->client->pers.netname); - return; - } - // Need to jump? - // Lower body is blocked, upper body is free - else if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) - { - BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - ucmd->upmove = 400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - Com_Printf("%s %s is stuck [NODE_MOVE]: jumping\n", __func__, self->client->pers.netname); - return; - } - else - { - //self->bot.state = STATE_WANDER; - //return; - } - } - */ - - - // If the bot fell below the target (failed the jump down) - // Try to find another route, otherwise find a new node and go there instead - if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) - { - // Upgrade target node type to jumppad - if (targetNodeLink != INVALID) - { - //if (debug_mode) - //Com_Printf("%s %s dropped from a move forward, upgrading move to jumppad node\n", __func__, self->client->pers.netname); - - nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; - - // Update the node we're near - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (tmp_node != self->bot.current_node) - //{ - // self->bot.prev_node = self->bot.current_node; - // self->bot.current_node = tmp_node; - // //Com_Printf("%s %s [MOVE_NODE][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - //} - - return; - } - else - { - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - //if (debug_mode) - //Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - BOTLIB_SetGoal(self, self->bot.goal_node); - return; - } - - //if (debug_mode) - //Com_Printf("%s %s failed to move forward to goal %d, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - - //self->bot.state = STATE_WANDER; // Aquire a new node - //return; - } - } - - self->s.angles[PITCH] = 0; // Look directly forward - - //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); - - // Check we've reach our ideal yaw angle - if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) - return; - - /* - // See if we're stuck, try to unstick - if (VectorLength(self->velocity) < 8) - { - // project forward from origin - // Box test [feet to mid body] -> forward 16 units - vec3_t forward; - AngleVectors(self->s.angles, forward, NULL, NULL); - VectorMA(self->s.origin, 32, forward, forward); - tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction < 1 || tr.startsolid) - { - ucmd->upmove = 400; // Try jumping - } - else - ucmd->upmove = -400; // Try crouching - - // See if the way forward is clear - tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction < 1) - { - // Try strafing left - tr = gi.trace(self->s.origin, tv(-32, -16, -14), tv(0, 16, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction == 1) - { - ucmd->sidemove = -400; - } - else - { - // Try strafing right - tr = gi.trace(self->s.origin, tv(0, -16, -14), tv(32, 16, 0), forward, self, MASK_DEADSOLID); - if (tr.fraction == 1) - { - ucmd->sidemove = 400; - } - } - } - } - */ - - // Guide the bot in when close to the target - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////distance = VectorLength(dist); - if (distance <= 32) - { - //tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - tr = gi.trace(self->s.origin, tv(-14, -14, -6), tv(14, 14, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - VectorNormalize(dist); - distance = VectorLength(dist); // Get the absolute length - - ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. - - // Apply it - //if (!ACEMV_CanMove(self, MOVE_FORWARD)) - // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down - //else - // VectorScale(dist, 128, self->velocity); - //VectorScale(dist, 224, self->velocity); - - if (distance <= 2) - VectorScale(dist, 128, self->velocity); - //else if (distance <= 16) - // VectorScale(dist, 224, self->velocity); - else - VectorScale(dist, 320, self->velocity); - - //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); - - return; - } - } - - /* - if (next_node_type == INVALID) - { - if (!ACEMV_CanMove(self, MOVE_FORWARD)) - { - //ACEAI_PickLongRangeGoal(self); - return; - } - } - */ - - //ACEMV_ChangeBotAngle(self); - - /* - distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); - ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); - ////dist[2] = 0; - ////distance = VectorLength(dist); - if (!self->groundentity) - { - Com_Printf("%s %s [MOVE_NODE] air guide...\n", __func__, self->client->pers.netname); - - // When in air, control speed carefully to avoid overshooting the next node. - if (distance < 256) - ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); - else - ucmd->forwardmove = SPEED_RUN; - } - - //rekkie -- DEV_1 -- s - else if (!ACEMV_CanMove(self, MOVE_FORWARD)) - { - Com_Printf("%s %s [MOVE_NODE] move guide...\n", __func__, self->client->pers.netname); - - if (distance < 128) - ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); - else - ucmd->forwardmove = SPEED_WALK; - } - else - { - // Otherwise move as fast as we can. - ucmd->forwardmove = SPEED_RUN; - } - */ - - ucmd->forwardmove = SPEED_RUN; - //rekkie -- DEV_1 -- e - - } -#endif -} - -qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint) -{ - /* - vec3_t forward; - - // immediately turn to where we need to go - float length = VectorDistance(self->s.origin, targetpoint); - float fwd_speed = length * 1.95f; - - vec3_t dir; - PredictAim(self, self->enemy, self->s.origin, fwd_speed, false, 0.f, &dir, nullptr); - - self->s.angles[1] = vectoyaw(dir); - AngleVectors(self->s.angles, forward, nullptr, nullptr); - self->s.origin[2] += 1; - self->velocity = forward * fwd_speed; - self->velocity[2] = 450; - */ - - - - float fordward_speed; - vec3_t forward, dir, vec, target; - - VectorCopy(targetpoint, target); - - // Get height diff from self to target point - // If > 0, target is higher than self - // If < 0, target is lower than self - //float height_diff = targetpoint[2] - (self->s.origin[2] - self->viewheight); // 56 - 22 = 34 - float height_diff = target[2] - self->s.origin[2]; // 56 - 22 = 34 - - //if (height_diff < 0) - target[2] += 32; - //else - // targetpoint[2] -= 32; - - fordward_speed = VectorDistance(self->s.origin, target); // Get forward speed - if (VectorEmpty(target)) // If Distance is zero - return false; - - //fordward_speed = fordward_speed * 1.95f; - //fordward_speed = fordward_speed * 0.2f; - - VectorSubtract(target, self->s.origin, dir); // Get direction - //dir[2] += self->viewheight; // Add eye height to direction height - float distance = VectorLength(dir); // Get distance - - float time = distance / fordward_speed; - - vec[0] = target[0] + time; - vec[1] = target[1] + time; - vec[2] = target[2] + time; - - // Adjust the z value by the height difference - //if (height_diff != 0) - // vec[2] += height_diff; - - // If vector is pointing the wrong direction (pointing backwards) - VectorNormalize(dir); - vec3_t dir2; - VectorSubtract(vec, self->s.origin, dir2); - VectorNormalize(dir2); - float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); - if (dot < 0) - { - VectorCopy(target, vec); - } - else - { - // if the shot is going to impact a nearby wall from our prediction, just fire it straight. - trace_t tr = gi.trace(self->s.origin, NULL, NULL, vec, NULL, MASK_SOLID); - if (tr.fraction < 0.9f) - { - VectorCopy(target, vec); - } - } - - //vec[2] += self->viewheight; // Add eye height to direction height - - VectorSubtract(vec, self->s.origin, dir); // Using - VectorNormalize(dir); - - vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; - angles[1] = vectoyaw(dir); - AngleVectors(angles, forward, NULL, NULL); // Get forward vector - VectorScale(forward, fordward_speed, self->velocity); // Scale the forward vector by the forward speed to get a velocity - self->s.origin[2] += 1; // Adjust player slightly off the ground - - - //self->velocity[0] = forward[0] * fordward_speed; - //self->velocity[1] = forward[1] * fordward_speed; - //self->velocity[2] = 450; - - if (height_diff > -2) - self->velocity[2] = 432 + height_diff; - else - { - if (fordward_speed + height_diff > 400) - self->velocity[2] = 450 + height_diff; - else - self->velocity[2] = fordward_speed + height_diff; - - if (self->velocity[2] < 300) - self->velocity[2] = fordward_speed; - - if (fordward_speed <= 64) - self->velocity[2] = 450; - else if (fordward_speed <= 96) - self->velocity[2] = 550; - else if (fordward_speed <= 128) - self->velocity[2] = 650; - else - self->velocity[2] = 450; - } - - self->velocity[2] = 432 + height_diff; - - - - - - - - //Com_Printf("%s %s speed:%f hdiff:%f vel[2]:%f\n", __func__, self->client->pers.netname, fordward_speed, height_diff, self->velocity[2]); - - //self->velocity[2] = fwd_speed <= 400 ? (fwd_speed + height_diff) : (400 + height_diff); - - - - /* - if (fwd_speed <= 64) - self->velocity[2] = 300 + height_diff; - else if (fwd_speed <= 96) - self->velocity[2] = 350 + height_diff; - else if (fwd_speed <= 128) - self->velocity[2] = 400 + height_diff; - else - self->velocity[2] = 432 + height_diff; - */ - - /* - // peak speeds on flat surface - velocity[0]; // 800 - velocity[1]; // 870 - velocity[2]; // 260 - speed; // 937 - */ - - self->bot.jumppad = true; // Successful jump - self->bot.node_jump_from = self->bot.current_node; - self->bot.node_jump_to = self->bot.next_node; - self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again - self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place - - return true; -} - -//rekkie -- Quake3 -- s -// predictive calculator -// target is who you want to shoot -// start is where the shot comes from -// bolt_speed is how fast the shot is (or 0 for hitscan) -// eye_height is a boolean to say whether or not to adjust to targets eye_height -// offset is how much time to miss by -// aimdir is the resulting aim direction (pass in nullptr if you don't want it) -// aimpoint is the resulting aimpoint (pass in nullptr if don't want it) -void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint) -{ - vec3_t dir, vec; - float dist, time; - - if (target) // Use entity's origin,viewheight,velocity if we can - { - VectorCopy(target->s.origin, target_point); - target_viewheight = target->viewheight; - VectorCopy(target->velocity, target_velocity); - } - - VectorSubtract(target_point, start, dir); - if (eye_height) - dir[2] += target_viewheight; - dist = VectorLength(dir); - - // [Paril-KEX] if our current attempt is blocked, try the opposite one - vec3_t end; - VectorMA(start, dist, dir, end); - trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); - - //if (tr.ent != target) - { - eye_height = !eye_height; - VectorSubtract(target_point, start, dir); - if (eye_height) - dir[2] += target_viewheight; - dist = VectorLength(dir); - } - - if (bolt_speed) - time = dist / bolt_speed; - else - time = 0; - - // Calculate the jump height to get to the target - //float gravity = self->gravity * sv_gravity->value; - //float jump_height = sqrt(2 * (gravity * dist)); - //time = dist / (jump_height / 2); // Calculate the time it will take to get to the target - - //vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); - //vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); - //vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); - vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); - vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); - vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); - - // went backwards... - VectorNormalize(dir); - vec3_t dir2; - VectorSubtract(vec, start, dir2); - VectorNormalize(dir2); - float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); - if (dot < 0) - VectorCopy(target_point, vec); - else - { - // if the shot is going to impact a nearby wall from our prediction, just fire it straight. - tr = gi.trace(start, NULL, NULL, vec, NULL, MASK_SOLID); - if (tr.fraction < 0.9f) - VectorCopy(target_point, vec); - } - - if (eye_height) - vec[2] += target_viewheight; - - if (aimdir) - { - VectorSubtract(vec, start, *aimdir); - VectorNormalize(*aimdir); - } - - if (aimpoint) - { - VectorCopy(vec, *aimpoint); - } -} - -qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity) -{ - float length, fwd_speed; - vec3_t forward, dir; - - if (self->bot.jumppad_last_time > level.framenum) // Can we jump again so soon? - return false; - - if (self->client->leg_damage) // Can't jump if legs are damaged - return false; - - //Com_Printf("%s %s -- LETS GO!\n", __func__, self->client->pers.netname); - - // Zero our horizontal velocity - //target_velocity[0] = 0; - //target_velocity[1] = 0; - //target_velocity[2] = 0; - - // immediately turn to where we need to go - if (target) - { - length = VectorDistance(self->s.origin, target->s.origin); - VectorCopy(target->s.origin, target_point); - } - else - { - length = VectorDistance(self->s.origin, target_point); - } - if (VectorEmpty(target_point)) - return false; - - // Get height diff from self to target point - // If > 0, target is higher than self - // If < 0, target is lower than self - float height_diff = target_point[2] - (self->s.origin[2] - 24); - - /* - //if (height_diff < 0.1) - // height_diff = 0; - float modifier = 1.5; - if (height_diff > 0) - modifier = (height_diff / 60) + 1.5; - else if (height_diff < 0) - modifier = 1.7 - (fabs(height_diff) / 200); - - //fwd_speed = length * 1.95f; - //fwd_speed = length; - //fwd_speed = (length + 32) * modifier; - - fwd_speed = (length * 1.1) + (height_diff); - */ - //Com_Printf("%s z[%f] mod[%f] len[%f]\n", __func__, height_diff, modifier, length); - - if (self->is_bot && self->bot.current_node != INVALID && self->current_link != INVALID) - { - fwd_speed = length; // * nodes[self->bot.current_node].links[self->current_link].targetNodeJumpPower; // Self adjusting jump power - //Com_Printf("%s %s fwd_speed[%f]\n", __func__, self->client->pers.netname, fwd_speed); - } - else - { - fwd_speed = length; - } - - - - if (target) - { - vec3_t target_point, target_velocity; - BOTLIB_PredictJumpPoint(self, target, target_point, 0, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); - } - else - BOTLIB_PredictJumpPoint(self, NULL, target_point, target_viewheight, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); - - vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; - angles[1] = vectoyaw(dir); - AngleVectors(angles, forward, NULL, NULL); - self->s.origin[2] += 1; - VectorScale(forward, fwd_speed, self->velocity); - - //print big jump - //if (length < 360) - // Com_Printf("%s %s: Small Jump [%f]\n", __func__, self->client->pers.netname, length); - //else - // Com_Printf("%s %s: Big Jump [%f]\n", __func__, self->client->pers.netname, length); - - /* - // If the target is above the player, increase the velocity to get to the target - float z_height; - if (target && target->s.origin[2] >= self->s.origin[2]) - { - z_height = (target->s.origin[2] - self->s.origin[2]); - z_height += sqrtf(length) + NODE_Z_HEIGHT; - self->velocity[2] += z_height; - } - else if (target_point[2] >= self->s.origin[2]) - { - z_height = (target_point[2] - self->s.origin[2]); - z_height += sqrtf(length) + NODE_Z_HEIGHT; - self->velocity[2] += z_height; - } - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) - { - self->velocity[2] = 375; // 450 - } - */ - - /* - if (length <= 64) - self->velocity[2] = 400 + height_diff; // 450 - else - self->velocity[2] = 432 + height_diff; - */ - - if (length <= 64) - self->velocity[2] = 300 + height_diff; - else if (length <= 96) - self->velocity[2] = 350 + height_diff; - else if (length <= 128) - self->velocity[2] = 400 + height_diff; - else - self->velocity[2] = 432 + height_diff; - - - - - /* - // peak speeds on flat surface - velocity[0]; // 800 - velocity[1]; // 870 - velocity[2]; // 260 - speed; // 937 - */ - /* - // Get the surface normal of the ground the player is standing on - trace_t tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, MASK_PLAYERSOLID); - - //if (self->velocity[0] > 800) self->velocity[0] = 800; - //if (self->velocity[1] > 870) self->velocity[1] = 870; - if (self->velocity[2] > 260) self->velocity[2] = 260; - if (tr.plane.normal[2] < 1.0) - { - float diff = 1.0 - tr.plane.normal[2]; - diff *= 2.0; - diff += tr.plane.normal[2]; - self->velocity[2] *= diff; - } - // calculate velocity speed - float speed = VectorLength(self->velocity); - Com_Printf("%s speed[%f] velocity_peak[%f %f %f]\n", __func__, speed, self->velocity[0], self->velocity[1], self->velocity[2]); - */ - - - - - - /* - // get height diff from self to target - qboolean above = false; - if (target && target->s.origin[2] >= self->s.origin[2]) - above = true; - else if (target_point[2] >= self->s.origin[2]) - above = true; - - float height_diff; - if (target) - height_diff = fabs(target->s.origin[2] - self->s.origin[2]); - else - height_diff = fabs(target_point[2] - self->s.origin[2]); - Com_Printf("%s %s: Jump [%f] Height[%f]\n", __func__, self->client->pers.netname, length, height_diff); - if (length < 256) - { - if (height_diff < 32) - self->velocity[2] = 200; - else - self->velocity[2] = 400; - } - else if (length < 384) - self->velocity[2] = 425; - else - self->velocity[2] = 450; - */ - - self->bot.jumppad = true; // Successful jump - self->bot.node_jump_from = self->bot.current_node; - self->bot.node_jump_to = self->bot.next_node; - self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again - self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place - - return true; -} - -// Predict enemy position based on their position, velocity, and our velocity -void BOTLIB_PredictEnemyOrigin(edict_t *self, vec3_t out, float multiplier) -{ - // FRAMETIME - /* - out[0] = self->enemy->s.origin[0] + ((self->enemy->velocity[0] + self->velocity[0]) * (10.0 / HZ)); - out[1] = self->enemy->s.origin[1] + ((self->enemy->velocity[1] + self->velocity[1]) * (10.0 / HZ)); - - if (self->enemy->maxs[2] == CROUCHING_MAXS2) // Predict lower if enemy is crouching - out[2] = (self->enemy->s.origin[2] - 24) + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); - else // Predict when standing - out[2] = self->enemy->s.origin[2] + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); - */ - - if (rand() % 2 == 0) - multiplier -= (multiplier * 2); - - //Com_Printf("%s %f\n", __func__, FRAMETIME); - VectorAdd(self->velocity, tv(self->enemy->velocity[0] * multiplier, self->enemy->velocity[1] * multiplier, self->enemy->velocity[2]), out); - VectorScale(out, FRAMETIME, out); - VectorAdd(out, self->enemy->s.origin, out); - //VectorCopy(self->enemy->s.origin, out); - if (self->enemy->maxs[2] == CROUCHING_MAXS2) - out[2] -= 24; - - if (self->client->weapon == FindItemByNum(KNIFE_NUM)) - { - // Quake 2 uses x,y,z which is stored in a float array called vec3_t[] - // The Z component is for jumping and falling - // Throwing knife affected by gravity over time - //knife->velocity[2] -= sv_gravity->value * FRAMETIME; // velocity is a vec3_t[3] which is a float array - // My origin - self->s.origin; // origin is a vec3_t[3] which is a float array - // Enemy origin - self->enemy->s.origin; // origin is a vec3_t[3] which is a float array - // My pitch aim (looking directly forward is a pitch of 0) - // #define PITCH 0 - // #define YAW 1 - // #define ROLL 2 - self->s.angles[PITCH]; - - // I want to find the aiming pitch requires to reach a target location with a throwing knife that drops with gravity. - // Quake2 uses x,y,z coordinates stored in a vec3_t[3] which is a float array [0] [1] [2] - // I have a first-person 3D game. A player origin, a target, gravity (on the Z axis only), and a throwing knife that is affected by gravity. By default the player looks forward at a pitch of 0. Looking directly up is -90 and down is 90. - float g = sv_gravity->value; // Gravity - float distance = VectorDistanceXY(self->s.origin, self->enemy->s.origin); // XY Distance to enemy - float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference - float knife_speed = 1200; - // Using the distance to my enemy and the gravity of the knife what pitch would I need to set to reach my enemy? - - // Calculate total flight time - //float t = sqrtf((2 * distance) / sv_gravity->value); - - - - } - -} - - - - -///////////////// -// Look dir -///////////////// -void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) -{ - vec3_t lookdir = { 0 }; // Direction to look - trace_t tr; - //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - float turn_speed = 0.3; - qboolean reached_look_at = false; - - if (VectorEmpty(self->bot.bi.look_at) == false) - { - reached_look_at = BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.15, true, true); - - if (reached_look_at == false) - return; - } - - - - // Look at ladder - if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) - { - // Check we're looking at ladder - qboolean looking_at_ladder = false; - { - float yaw_rad = 0; - vec3_t fwd = { 0 }, end = { 0 }; - - yaw_rad = DEG2RAD(self->s.angles[YAW]); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - VectorMA(self->s.origin, 60, fwd, end); - - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - if (looking_at_ladder) - self->bot.touching_ladder = true; - } - // Turn to find ladder - if (looking_at_ladder == false) - { - int additional_rotation = 0; - for (int i = 0; i < 16; i++) - { - vec3_t fwd = { 0 }, end = { 0 }; - - self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 - - float yaw_rad = DEG2RAD(self->s.angles[YAW]); - - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - VectorMA(self->s.origin, 60, fwd, end); - - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder - { - self->bot.touching_ladder = true; - VectorSubtract(end, self->s.origin, self->bot.bi.look_at); - ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 10.0, true, true); - if (additional_rotation >= 1) - break; - additional_rotation++; - } - } - } - - // Look up or down when on ladder - if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) - { - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up - { - //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); - - // Level out at top or bottom - if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || - VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) - self->bot.bi.viewangles[PITCH] = 0; - else - self->bot.bi.viewangles[PITCH] = -45; //-89 - } - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down - { - //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); - - // Level out at top or bottom - if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || - VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) - self->bot.bi.viewangles[PITCH] = 0; - else - self->bot.bi.viewangles[PITCH] = 45; // 89 - } - } - } - // Look at enemy - else if (self->enemy) // Track target - { - qboolean not_infront = false; // If target is in front - - // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, - // the bot will keep looking in that general direction, but not directly at the enemy's pos - if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) - { - if (BOTLIB_Infront(self, self->enemy, 0.3) == false) - not_infront = true; - - if (1) // Predicted enemy pos - { - // Predict enemy position based on their velocity and distance - vec3_t predicted_enemy_origin = { 0 }; - BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 2); - if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual - { - uint32_t red = MakeColor(255, 255, 255, 255); // red - uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow - void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; - DrawBox = players[0]->client->pers.draw->DrawBox; - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - DrawBox(self->enemy->s.number, predicted_enemy_origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 200, false); - DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, red, tv(-16, -16, -24), tv(16, 16, 32), 100, false); - } - VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy - /* - if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching - VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies - else - VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy - */ - } - else // Not predicted - { - if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching - VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies - else - VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy - } - } - else // Look where enemy was last - { - // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc - vec3_t eyes; - VectorCopy(self->s.origin, eyes); - eyes[2] += self->viewheight; // Get our eye level (standing and crouching) - vec3_t enemy_eyes; - VectorCopy(self->enemy->s.origin, enemy_eyes); - enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) - trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target - float distance = VectorDistance(self->s.origin, tr.endpos); - if (distance > 256) - VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); - else - goto LookAhead; // Looking at a wall, so just look at nodes - } - /* - // Adjust turn speed based on skill - //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - if (self->bot.skill >= MAX_BOTSKILL) - { - turn_speed = 0.2; - } - //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) - { - //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold - } - - if (turn_speed > 1.0) - { - if (not_infront) - turn_speed = 1.0; - - if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim - turn_speed = 0.2; - else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim - turn_speed = 0.2; - else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim - turn_speed = 0.2; - else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap - turn_speed = 0.5; - else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap - turn_speed = 0.1; - else if (rand() % 10 == 0) // Bots sometimes get aim snap - turn_speed = 0.5; - - turn_speed = 1.0; - } - */ - - //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); - - //if (self->client->weaponstate != WEAPON_READY) - // not_infront = rand() % 2; - - /* - if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) - { - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - } - else - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - */ - } - /* - // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link - else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) - { - // See if this node has any POI lookat links - int num_lookat_nodes = 0; - int lookat_nodes[MAXLINKS] = { 0 }; - for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) - { - lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; - num_lookat_nodes++; - } - } - if (num_lookat_nodes) // One or more POI lookat nodes found - { - // When the timer is up pick a new node to look at - if (self->bot.node_poi_look_time < level.framenum) - { - self->bot.node_poi_look_time = level.framenum + 5 * HZ; - self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node - } - - // Look at POI lookat node - VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); - } - } - */ - else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) - { - //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - } - //rand() % nodes[self->bot.current_node].num_links + 1; - Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - - VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); - ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - } - // If no enemy, have bot look at PNOISES, if any. - else if (1) - { - int type = 0; - int player_num = INVALID; - float nearest = 9999999; - qboolean found_target = false; - for (int i = 0; i < num_players; i++) - { - if (players[i] != self && OnSameTeam(self, players[i]) == false) - { - if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) - { - //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); - float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_WEAPON; - nearest = dist; - player_num = i; - } - } - if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) - { - float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_SELF; - nearest = dist; - player_num = i; - } - } - } - } - if (player_num == INVALID) - { - for (int i = 0; i < num_players; i++) - { - if (players[i] != self && OnSameTeam(self, players[i]) == false) - { - if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) - { - float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_WEAPON; - nearest = dist; - player_num = i; - } - } - } - } - } - - if (player_num != INVALID) - { - vec3_t noise_origin; - - if (type == PNOISE_WEAPON) - VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); - else if (type == PNOISE_SELF) - VectorCopy(botlib_noises.self_origin[player_num], noise_origin); - else if (type == PNOISE_IMPACT) - VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); - - - // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise - vec3_t eyes; - VectorCopy(self->s.origin, eyes); - eyes[2] += self->viewheight; // Get our eye level (standing and crouching) - vec3_t noise_origin_eyes; - VectorCopy(noise_origin, noise_origin_eyes); - noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level - trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target - float distance = VectorDistance(self->s.origin, tr.endpos); - if (distance > 256) - { - VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); - ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - } - else - goto LookAhead; - } - else - goto LookAhead; - } - // If no enemy, look at nodes in front/behind, or look at map center - else // if (self->enemy == NULL) - { - - LookAhead: - - const int look_ahead = 5; - qboolean found_viable_node = false; - for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list - { - /* - if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) - { - int look_node = self->bot.node_list[(i + look_ahead)]; - if (nodes[look_node].num_links) - { - int rnd_link = rand() % nodes[look_node].num_links + 1; - int rnd_node = nodes[look_node].links[rnd_link].targetNode; - if (rnd_node != INVALID) - { - VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); - found_viable_node = true; - break; - } - } - } - */ - - // Look x nodes ahead - if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) - { - VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); - ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - found_viable_node = true; - break; - } - // Look x nodes behind - else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) - { - VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); - ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - found_viable_node = true; - break; - } - else - break; - } - //if (found_viable_node) - // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - //else - // Com_Printf("%s no node [%d]\n", __func__, level.framenum); - - if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) - { - //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - } - //rand() % nodes[self->bot.current_node].num_links + 1; - Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - - VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - } - - if (0 && found_viable_node == false) - { - // If no enemy, look at random thing - { - // If no enemy, look at random item or map center - // Search through map looking for a random item, or NULL - //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) - if (self->bot.bi.look_at_time < level.framenum) - { - self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds - - edict_t* map_ents; - int edict_num = 1 + game.maxclients; // skip worldclass & players - for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) - { - // Skip ents that are not in used and not an item - if (map_ents->inuse == false || map_ents->item == NULL) - continue; - - //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); - //if (tr.fraction == 1.0) - { - VectorCopy(map_ents->s.origin, self->bot.bi.look_at); - break; - } - } - } - if (VectorEmpty(self->bot.bi.look_at)) - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map - else - VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item - - } - - // If lookdir is empty, then look at map center - if (VectorEmpty(self->bot.bi.look_at)) - { - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); - } - - BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, turn_speed, true, true); - } - } -} - -void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) -{ - //if (teamplay->value && lights_camera_action > 0) - // return; - - int next_node = INVALID; // Next node to walk towards - //vec3_t lookdir = { 0 }; // Direction to look - vec3_t walkdir; // Direction to walk - vec3_t lastdir; - float move_speed = SPEED_RUN; // Movement speed, default to running - self->bot.bi.speed = 0; // Zero speed - trace_t tr; // Trace - - // Remove flags - //self->bot.bi.actionflags = 0; - //self->enemy = NULL; - - // Prevent stuck suicide if holding position - if ((self->bot.bi.actionflags & ACTION_HOLDPOS)) - self->suicide_timeout = level.framenum + 10; - - // Check how far we've moved - qboolean moved = true; - VectorSubtract(self->s.origin, self->lastPosition, lastdir); - float move_dist = VectorLength(lastdir); - if (move_dist < FRAMETIME) - moved = false; // We've not moved - - - // If the bot is near the get_item they're after, and the item is inuse - // inuse == false if the item was picked up and waiting to respawn - // inuse == true if the item has spawned in and is ready to be picked up - if (self->bot.get_item != NULL && self->bot.get_item->inuse) - { - float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); - //Com_Printf("%s %s is looking for item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - - // item spawnflags - //#define ITEM_TRIGGER_SPAWN 0x00000001 - //#define ITEM_NO_TOUCH 0x00000002 - // 6 bits reserved for editor flags - // 8 bits used as power cube id bits for coop games - //#define DROPPED_ITEM 0x00010000 - //#define DROPPED_PLAYER_ITEM 0x00020000 - //#define ITEM_TARGETS_USED 0x00040000 - // - // Edict Flags - // #define FL_RESPAWN 0x80000000 // used for item respawning - // - // - // TP item not picked up - // spawnflags = 0 - // flags = 0 - // - // TP item picked up and in bot inventory - // spawnflags = ITEM_TARGETS_USED 0x00040000 - // flags = FL_RESPAWN 0x80000000 - // - // TP item picked up then dropped by bot - // spawnflags = DROPPED_ITEM - // flags = 0 - // - // TP item picked up, bot killed, dropping all items to ground - // spawnflags = DROPPED_PLAYER_ITEM - // flags = 0 - - trace_t tr = gi.trace(self->s.origin, NULL, NULL, self->bot.get_item->s.origin, NULL, MASK_PLAYERSOLID); - if (tr.fraction == 1.0 && item_dist <= 128) // Might want to do a trace line to see if bot can actually see item - { - //Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to item - VectorNormalize(walkdir); - - //horizontal direction - vec3_t hordir; - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - - self->bot.bi.speed = move_speed; // Set our suggested speed - - int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed - if (perform_action == ACTION_NONE) - { - //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching - //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping - } - else if (perform_action == ACTION_JUMP) - { - self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping - self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching - } - else if (perform_action == ACTION_CROUCH) - { - self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching - self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping - } - - if (item_dist < 128) - self->bot.bi.speed = SPEED_ROAM; // Slow down when close - - if (self->bot.get_item->solid == SOLID_NOT); // picked up - { - //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - self->bot.get_item = NULL; - return; - } - - return; - - } - //else if (item_dist <= 64) - { - //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - //self->bot.get_item = NULL; - } - } - - /* - // Get current and next node back from nav code. - if (!BOTLIB_FollowPath(self)) - { - //Com_Printf("%s %s BOTLIB_FollowPath == false\n", __func__, self->client->pers.netname); - //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); - - self->bot.bi.speed = 0; - self->bot.bi.actionflags = 0; - - self->bot.state = STATE_WANDER; - //self->wander_timeout = level.framenum + 1.0 * HZ; - self->bot.goal_node = INVALID; - return; - } - */ - - if (self->bot.touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) - { - self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; - self->bot.bi.actionflags &= ~ACTION_MOVEUP; - } - -#if 0 - ///////////////// - // Look dir - ///////////////// - qboolean touching_ladder = false; - //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) - { - // Check if touching ladder - { - float yaw_rad = 0; - vec3_t fwd = { 0 }, end = { 0 }; - trace_t tr; - - yaw_rad = DEG2RAD(self->s.angles[YAW]); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - //VectorMA(self->s.origin, 1, fwd, end); - //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); - - VectorMA(self->s.origin, 1, fwd, end); - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - - if (touching_ladder == false) - { - VectorMA(self->s.origin, 8, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - if (touching_ladder == false) - { - VectorMA(self->s.origin, 16, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - if (touching_ladder == false) - { - VectorMA(self->s.origin, 32, fwd, end); - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - } - } - } - //if (touching_ladder) - // Com_Printf("%s touching_ladder [%d]\n", __func__, level.framenum); - if (touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) - { - self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; - self->bot.bi.actionflags &= ~ACTION_MOVEUP; - touching_ladder = false; - } - - // Look at ladder - if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) - { - // Check we're looking at ladder - qboolean looking_at_ladder = false; - { - float yaw_rad = 0; - vec3_t fwd = { 0 }, end = { 0 }; - trace_t tr; - - yaw_rad = DEG2RAD(self->s.angles[YAW]); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - VectorMA(self->s.origin, 60, fwd, end); - - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); - if (looking_at_ladder) - touching_ladder = true; - } - // Turn to find ladder - if (looking_at_ladder == false) - { - int additional_rotation = 0; - for (int i = 0; i < 16; i++) - { - vec3_t fwd = { 0 }, end = { 0 }; - trace_t tr; - - self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 - - float yaw_rad = DEG2RAD(self->s.angles[YAW]); - - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - - VectorMA(self->s.origin, 60, fwd, end); - - vec3_t lmins = { -16, -16, -96 }; - vec3_t lmaxs = { 16, 16, 96 }; - - tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); - - if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder - { - touching_ladder = true; - VectorSubtract(end, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 3.0, true, true); - if (additional_rotation >= 1) - break; - additional_rotation++; - } - } - } - - // Look up or down when on ladder - if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) - { - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up - { - //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); - - // Level out at top or bottom - if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || - VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) - self->bot.bi.viewangles[PITCH] = 0; - else - self->bot.bi.viewangles[PITCH] = -45; //-89 - } - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down - { - //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); - - // Level out at top or bottom - if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || - VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) - self->bot.bi.viewangles[PITCH] = 0; - else - self->bot.bi.viewangles[PITCH] = 45; // 89 - } - } - } - // Look at enemy - else if (self->enemy) // Track target - { - qboolean not_infront = false; // If target is in front - - // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, - // the bot will keep looking in that general direction, but not directly at the enemy's pos - if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) - { - if (BOTLIB_Infront(self, self->enemy, 0.3) == false) - not_infront = true; - - if (1) // Predicted enemy pos - { - // Predict enemy position based on their velocity and distance - vec3_t predicted_enemy_origin = { 0 }; - BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 1); - if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual - { - uint32_t blue = MakeColor(0, 0, 255, 255); // Blue - uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow - void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; - DrawBox = players[0]->client->pers.draw->DrawBox; - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - DrawBox(self->enemy->s.number, predicted_enemy_origin, blue, tv(-16, -16, -24), tv(16, 16, 32), 100, false); - DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 100, false); - } - VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy - /* - if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching - VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies - else - VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy - */ - } - else // Not predicted - { - if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching - VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies - else - VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy - } - } - else // Look where enemy was last - { - // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc - vec3_t eyes; - VectorCopy(self->s.origin, eyes); - eyes[2] += self->viewheight; // Get our eye level (standing and crouching) - vec3_t enemy_eyes; - VectorCopy(self->enemy->s.origin, enemy_eyes); - enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) - trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target - float distance = VectorDistance(self->s.origin, tr.endpos); - if (distance > 256) - VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); - else - goto LookAhead; // Looking at a wall, so just look at nodes - } - - // Adjust turn speed based on skill - //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 - if (self->bot.skill >= MAX_BOTSKILL) - { - turn_speed = 0.2; - } - //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) - { - //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold - } - - if (turn_speed > 1.0) - { - if (not_infront) - turn_speed = 2.0; - - if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim - turn_speed = 0.2; - else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim - turn_speed = 0.2; - else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim - turn_speed = 0.2; - else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap - turn_speed = 0.5; - else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap - turn_speed = 0.5; - else if (rand() % 10 == 0) // Bots sometimes get aim snap - turn_speed = 0.5; - } - - //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); - - //if (self->client->weaponstate != WEAPON_READY) - // not_infront = rand() % 2; - - if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) - { - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, false); - } - else - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); - } - /* - // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link - else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) - { - // See if this node has any POI lookat links - int num_lookat_nodes = 0; - int lookat_nodes[MAXLINKS] = { 0 }; - for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) - { - lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; - num_lookat_nodes++; - } - } - if (num_lookat_nodes) // One or more POI lookat nodes found - { - // When the timer is up pick a new node to look at - if (self->bot.node_poi_look_time < level.framenum) - { - self->bot.node_poi_look_time = level.framenum + 5 * HZ; - self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node - } - - // Look at POI lookat node - VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); - } - } - */ - else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) - { - //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - } - //rand() % nodes[self->bot.current_node].num_links + 1; - Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - - VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); - } - // If no enemy, have bot look at PNOISES, if any. - else if (1) - { - int type = 0; - int player_num = INVALID; - float nearest = 9999999; - qboolean found_target = false; - for (int i = 0; i < num_players; i++) - { - if (players[i] != self && OnSameTeam(self, players[i]) == false) - { - if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) - { - //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); - float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_WEAPON; - nearest = dist; - player_num = i; - } - } - if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) - { - float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_SELF; - nearest = dist; - player_num = i; - } - } - } - } - if (player_num == INVALID) - { - for (int i = 0; i < num_players; i++) - { - if (players[i] != self && OnSameTeam(self, players[i]) == false) - { - if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) - { - float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); - if (dist < nearest && dist > 256) - { - type = PNOISE_WEAPON; - nearest = dist; - player_num = i; - } - } - } - } - } - - if (player_num != INVALID) - { - vec3_t noise_origin; - - if (type == PNOISE_WEAPON) - VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); - else if (type == PNOISE_SELF) - VectorCopy(botlib_noises.self_origin[player_num], noise_origin); - else if (type == PNOISE_IMPACT) - VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); - - - // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise - vec3_t eyes; - VectorCopy(self->s.origin, eyes); - eyes[2] += self->viewheight; // Get our eye level (standing and crouching) - vec3_t noise_origin_eyes; - VectorCopy(noise_origin, noise_origin_eyes); - noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level - trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target - float distance = VectorDistance(self->s.origin, tr.endpos); - if (distance > 256) - { - VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); - } - else - goto LookAhead; - } - else - goto LookAhead; - } - // If no enemy, look at nodes in front/behind, or look at map center - else // if (self->enemy == NULL) - { - - LookAhead: - - const int look_ahead = 5; - qboolean found_viable_node = false; - for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list - { - /* - if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) - { - int look_node = self->bot.node_list[(i + look_ahead)]; - if (nodes[look_node].num_links) - { - int rnd_link = rand() % nodes[look_node].num_links + 1; - int rnd_node = nodes[look_node].links[rnd_link].targetNode; - if (rnd_node != INVALID) - { - VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); - found_viable_node = true; - break; - } - } - } - */ - - // Look x nodes ahead - if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) - { - VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); - found_viable_node = true; - break; - } - // Look x nodes behind - else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) - { - VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); - found_viable_node = true; - break; - } - else - break; - } - //if (found_viable_node) - // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - //else - // Com_Printf("%s no node [%d]\n", __func__, level.framenum); - - if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) - { - //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) - { - } - //rand() % nodes[self->bot.current_node].num_links + 1; - Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); - - VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); - BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); - } - - if (0 && found_viable_node == false) - { - // If no enemy, look at random thing - { - // If no enemy, look at random item or map center - // Search through map looking for a random item, or NULL - //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) - if (self->bot.bi.look_at_time < level.framenum) - { - self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds - - edict_t* map_ents; - int edict_num = 1 + game.maxclients; // skip worldclass & players - for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) - { - // Skip ents that are not in used and not an item - if (map_ents->inuse == false || map_ents->item == NULL) - continue; - - //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); - //if (tr.fraction == 1.0) - { - VectorCopy(map_ents->s.origin, self->bot.bi.look_at); - break; - } - } - } - if (VectorEmpty(self->bot.bi.look_at)) - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map - else - VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item - - } - - // If lookdir is empty, then look at map center - if (VectorEmpty(self->bot.bi.look_at)) - { - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); - } - - BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, 1.0, true, true); - } - } -#endif - - // Do not follow path when teammates are still inside us. - if (OnTransparentList(self)) // Teamplay - { - // If the bot has just spawned, then we need to wait a bit before we start moving. - if (self->just_spawned) // set by SpawnPlayers() in a_team.c - { - self->just_spawned = false; - self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. - - // If enemy is in sight, don't wait too long - if (self->enemy) - self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait - else - { - // Otherwise pick from various wait times before moving out - int rnd_rng = rand() % 4; - if (rnd_rng == 0) - self->just_spawned_timeout = level.framenum + (random() * 10) * HZ; // Long wait - else if (rnd_rng == 1) - self->just_spawned_timeout = level.framenum + (random() * 5) * HZ; // Medium wait - else if (rnd_rng == 2) - self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait - else - self->just_spawned_timeout = 0; // No wait - } - - self->bot.bi.actionflags |= ACTION_HOLDPOS; - return; - } - // Wait - if (self->just_spawned_go && self->just_spawned_timeout > level.framenum && self->bot.see_enemies == false) - { - self->bot.bi.actionflags |= ACTION_HOLDPOS; - return; // It's not time to move yet, wait! - } - // Go! - if (self->just_spawned_go || self->bot.see_enemies) - { - //BOTLIB_PickLongRangeGoal(self); - self->just_spawned_go = false; // Now we can move! - } - } - - - if (self->groundentity && self->bot.next_node == INVALID) // No next node, pick new nav - { - Com_Printf("%s %s next_node is invalid; find a new path\n", __func__, self->client->pers.netname); - self->bot.bi.speed = 0; - self->bot.state = BOT_MOVE_STATE_NAV; - return; - } - // On ground and current or next node is invalid - //if (self->groundentity && (self->bot.current_node == INVALID || self->bot.next_node == INVALID)) - //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node - if (self->groundentity && self->bot.current_node == INVALID) - { - //self->bot.state = BOT_MOVE_STATE_NAV; - //self->bot.bi.speed = 0; - //return; - - Com_Printf("%s %s on ground and current_node is invalid; try to wander\n", __func__, self->client->pers.netname); - self->bot.bi.speed = 0; - self->bot.stuck_wander_time = 1; - } - if (self->groundentity && self->bot.goal_node == INVALID)// && self->bot.node_travel_time >= 15) - { - Com_Printf("%s %s on ground and goal_node is invalid; find a new path\n", __func__, self->client->pers.netname); - self->bot.bi.speed = 0; - self->bot.state = BOT_MOVE_STATE_NAV; - return; - } - - - - /* - // If next node is INVALID, assume we're stuck - //if (self->bot.next_node == INVALID || self->bot.current_node == INVALID || self->bot.goal_node == INVALID) - if (self->bot.goal_node == INVALID) - { - self->bot.stuck_wander_time = 2; - //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); - } - else */ - next_node = self->bot.next_node; - - /* - if (self->bot.node_travel_time > 30) - { - self->bot.node_travel_time = 0; - - // Wander - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 1.0 * HZ; - self->bot.goal_node = INVALID; - } - */ - - //if (self->bot.goal_node != INVALID && nav_area.total_areas > 0 && self->bot.node_travel_time >= 120) - { - //Com_Printf("%s %s ATTEMPTING TO FIX cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - //if (BOTLIB_CanVisitNode(self, self->bot.goal_node, false, INVALID)) - { - //self->bot.node_travel_time = 0; - //Com_Printf("%s %s FIXED node_travel_time cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - } - } - - // If travel time took too long, assume we're stuck - if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node - { - self->bot.stuck_wander_time = 1; - Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - } - - //self->bot.stuck_wander_time = 0; - - if (self->bot.stuck_wander_time)// && nav_area.total_areas <= 0) - { - self->bot.stuck_wander_time--; - self->bot.node_travel_time = 0; - - //Com_Printf("%s %s stuck_wander cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - - //Com_Printf("%s %s stuck_wander BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname); - //BOTLIB_PickLongRangeGoal(self); // pick a new long range goal - // Wander - //if (nav_area.total_areas <= 0) - { - Com_Printf("%s %s stuck_wander\n", __func__, self->client->pers.netname); - self->bot.state = BOT_MOVE_STATE_NAV; - self->bot.goal_node = INVALID; - } - //return; - - - VectorCopy(self->bot.stuck_random_dir, walkdir); - - if (self->groundentity) - { - self->bot.stuck_last_negate = 0; // Reset if we touched ground - } - - float fwd_distance = 32; - tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope - fwd_distance = 128; // Extend the distance we check for a safe direction to move toward - //Com_Printf("%s %s tr.plane.normal[%f] \n", __func__, self->client->pers.netname, tr.plane.normal[2]); - - qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false); - if (can_move == false || VectorEmpty(self->bot.stuck_random_dir)) - { - // Try to aquire a safe random direction to move toward - qboolean success = false; - for (int i = 0; i < 8; i++) - { - if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, true) == false) - continue; - else - { - success = true; - break; - } - } - if (success) - { - VectorCopy(walkdir, self->bot.stuck_random_dir); - // vect to angles - vec3_t angles; - vectoangles(walkdir, angles); - //Com_Printf("%s %s found random direction [%f %f %f]\n", __func__, self->client->pers.netname, angles[0], angles[1], angles[2]); - } - else - { - //Com_Printf("%s %s ACTION_HOLDPOS N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); - - // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge - if (level.framenum > self->bot.stuck_last_negate) - { - // Only allow this to happen once every 60 seconds - // It's reset after 60 seconds, death, or touching ground again - self->bot.stuck_last_negate = level.framenum + 60 * HZ; - - //Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); - - // Reverse the direction - VectorNegate(self->velocity, self->velocity); - } - //self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving - //BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed - //return; - } - } - else - { - //Com_Printf("%s %s heading for random dir\n", __func__, self->client->pers.netname); - //VectorCopy(walkdir, self->bot.stuck_random_dir); - //VectorCopy(self->bot.stuck_random_dir, walkdir); - } - - vec3_t hordir; - VectorNormalize(walkdir); - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = move_speed; // Set our suggested speed - - int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed - if (perform_action == ACTION_NONE) - { - //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching - //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping - } - else if (perform_action == ACTION_JUMP) - { - self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping - self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching - } - else if (perform_action == ACTION_CROUCH) - { - self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching - self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping - } - - return; - } - else - { - /* - qboolean fetch_item = false; - if (self->bot.get_item != NULL) - { - float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); - Com_Printf("%s %s is looking for item %s [%f] sf[0x%x] f[0x%x]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist, self->bot.get_item->spawnflags, self->bot.get_item->flags); - - // TP item not picked up - // spawnflags = 0 - // flags = 0 - // - // TP item picked up and in bot inventory - // spawnflags = ITEM_TARGETS_USED - // flags = FL_RESPAWN - // - // TP item picked up then dropped by bot - // spawnflags = DROPPED_ITEM - // flags = 0 - // - // TP item picked up, bot killed, dropping all items to ground - // spawnflags = DROPPED_PLAYER_ITEM - // flags = 0 - - if (item_dist <= 128) - { - Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to next node - VectorNormalize(walkdir); - fetch_item = true; - - if (item_dist <= 16) - { - Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - self->bot.get_item = NULL; - } - } - //else if (item_dist <= 64) - { - //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); - //self->bot.get_item = NULL; - } - } - */ - //if (fetch_item == false) - { - if (next_node == INVALID) - { - //self->bot.stuck_wander_time = 1; - return; - } - VectorSubtract(nodes[next_node].origin, self->s.origin, walkdir); // Head to next node - VectorNormalize(walkdir); - //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, walkdir); // Head to next node - } - } - - //qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, 32, NODE_MAX_CROUCH_FALL_HEIGHT, false); - //if (can_move == false) - // self->bot.stuck_wander_time = 1; - - if (self->bot.stuck_wander_time) - return; - - - //if (self->bot.state == STATE_WANDER || self->bot.goal_node == INVALID) - // Com_Printf("%s %s [%d] INVALID\n", __func__, self->client->pers.netname, level.framenum); - - //qboolean perform = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed - /* - if (perform == false && moved == false) - { - //self->bot.bi.actionflags |= (ACTION_JUMP); // Try jumping anyway - //self->bot.stuck_wander_time = 1; - } - */ - - /* - //horizontal direction - vec3_t hordir; - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - */ - - // Strafe to get back on track - if (0 && self->bot.next_node != INVALID) - { - // Get distance from bot to next node - //float dist = VectorDistance(self->s.origin, nodes[self->bot.next_node].origin); - // Get distanced from current node to next node - //float dist2 = VectorDistance(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin); - - byte mov_strafe = 0; - float dot = BOTLIB_DirectionCheck(self, &mov_strafe); - if (dot > 0.7 && dot < 0.99) // .995 - { - //float dist = VectorDistance(self->s.origin, nodes[self->bot.current_node].origin); - //if (dist > 64) - { - BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point - VectorSubtract(self->nearest_path_point, self->s.origin, walkdir); // Head to current node - VectorNormalize(walkdir); - vec3_t hordir; - hordir[0] += walkdir[0]; - hordir[1] += walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - } - - - - /* - //move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction - - //BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point - float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); - - if (mov_strafe > 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - // Get the direction perpendicular to the dir, facing left - vec3_t right; - right[0] = walkdir[0]; - right[1] = -walkdir[0]; - right[2] = 0; - VectorNormalize(right); - VectorCopy(right, self->bot.bi.dir); - move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction - } - else if (mov_strafe < 0) - { - //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); - // Get the direction perpendicular to the dir, facing left - vec3_t left; - left[0] = -walkdir[1]; - left[1] = walkdir[1]; - left[2] = 0; - VectorNormalize(left); - VectorCopy(left, self->bot.bi.dir); - move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction - } - else - { - move_speed = SPEED_RUN; // Slow down if we're moving in the right direction - } - */ - } - } - - //self->bot.bi.speed = move_speed; // Set our suggested speed - - if (0) - { - // Get current direction - vec3_t angle, forward, right, start, end, origin, offset; - vectoangles(walkdir, angle); - AngleVectors(angle, forward, right, NULL); - VectorCopy(self->s.origin, origin); - //origin[2] -= 24; // From the ground up - origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) - - VectorSet(offset, 0, 0, 0); // changed from 18,0,0 - G_ProjectSource(origin, offset, forward, right, start); - offset[0] += 1024; // Distance forward dir - G_ProjectSource(origin, offset, forward, right, end); - - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_BFG_LASER); - gi.WritePosition(start); - gi.WritePosition(end); - gi.multicast(self->s.origin, MULTICAST_PHS); - } - - - if (1 & self->bot.node_list_count) - { - //for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) - for (int i = 1; i < self->bot.node_list_count; i++) - { - if (self->bot.next_node == self->bot.node_list[i]) - { - self->bot.current_node = self->bot.node_list[i - 1]; - break; - } - } - if (self->bot.node_list_count) - { - //self->bot.current_node = self->bot.node_list[self->bot.node_list_current]; - } - } - - // Get current and next node types - int next_node_type = INVALID; - int current_node_type = INVALID; - if (self->bot.current_node != INVALID && self->bot.next_node != INVALID) - { - current_node_type = nodes[self->bot.current_node].type; - - for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) - { - int target_node = nodes[self->bot.current_node].links[i].targetNode; - if (target_node == self->bot.next_node) - { - next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type - - //self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type - //self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type - break; - } - - /* - // Try search surrounding nodes to see if they contain next_node - for (int j = 0; j < nodes[target_node].num_links; j++) - { - int target_target_node = nodes[target_node].links[j].targetNode; - for (int k = 0; k < nodes[target_target_node].num_links; k++) - { - int target_target_target_node = nodes[target_target_node].links[k].targetNode; - if (target_target_target_node == self->bot.next_node) - { - next_node_type = nodes[target_target_target_node].links[i].targetNodeType; // Next node type - Com_Printf("%s %s using target_target_target_node\n", __func__, self->client->pers.netname); - break; - } - } - } - */ - } - } - - //if (current_node_type == INVALID || next_node_type == INVALID) - if (next_node_type == INVALID) - { - //next_node_type = NODE_MOVE; - //Com_Printf("%s %s invalid types node:curr/next[%d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, current_node_type, next_node_type); - - if (self->bot.next_node == INVALID) - { - Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); - self->bot.state = BOT_MOVE_STATE_NAV; - return; - } - if (self->bot.current_node == self->bot.next_node && self->bot.current_node == self->bot.goal_node) - { - Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - ////if (nav_area.total_areas > 0) - //// self->bot.state = BOT_MOVE_STATE_NAV_NEXT; - ////else - self->bot.state = BOT_MOVE_STATE_NAV; - return; - } - else if (self->bot.next_node != INVALID && VectorDistance(nodes[self->bot.next_node].origin, self->s.origin) <= 128) - { - /* - // Try search surrounding nodes to see if they contain next_node - qboolean resolved = false; - for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) - { - if (next_node_type != INVALID) break; - int target_node = nodes[self->bot.current_node].links[i].targetNode; - if (target_node == INVALID) continue; - - for (int j = 0; j < nodes[target_node].num_links; j++) - { - if (next_node_type != INVALID) break; - int target_target_node = nodes[target_node].links[j].targetNode; - if (target_target_node == INVALID) continue; - - for (int k = 0; k < nodes[target_target_node].num_links; k++) - { - int target_target_target_node = nodes[target_target_node].links[k].targetNode; - if (target_target_target_node == INVALID) continue; - - if (target_target_target_node == self->bot.next_node) - { - if (VectorDistance(nodes[target_target_target_node].origin, self->s.origin) <= 128) - { - next_node_type = nodes[target_target_node].links[i].targetNodeType; // Next node type - Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - break; - } - } - } - } - } - */ - - //if (next_node_type != INVALID) - // Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - } - if (next_node_type == INVALID) - { - //Com_Printf("%s %s invalid types node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); - - // Path failed, so try again - //BOTLIB_CanVisitNode(self, self->bot.goal_node, false, nodes[self->bot.goal_node].area); - - /* - if (self->bot.goal_node != INVALID) - BOTLIB_CanGotoNode(self, self->bot.goal_node, false); - else - self->bot.state = BOT_MOVE_STATE_NAV; - */ - - return; - } - - //VectorCopy(nodes[self->bot.goal_node].origin, self->s.origin); - - //self->bot.state = STATE_WANDER; - //self->wander_timeout = level.framenum + 1.0 * HZ; - //self->bot.goal_node = INVALID; - //return; - } - - if (current_node_type != INVALID && next_node_type != INVALID) - { - // DEBUG: print current and next node types - if (0) - { - const qboolean printOnlyErrors = false; // If only printing INVALID nodes - PrintAllLinkNodeTypes(self, printOnlyErrors); - } - - // Set the default movement: this will be overriden by the movement code below - //if (next_node_type == NODE_MOVE || next_node_type == NODE_CROUCH || next_node_type == NODE_JUMP || next_node_type == NODE_BOXJUMP || next_node_type == NODE_LADDER) - { - //* - //horizontal direction - vec3_t hordir; - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - //*/ - //VectorCopy(walkdir, self->bot.bi.dir); - - self->bot.bi.speed = move_speed; // Set our suggested speed - } - - // move, crouch, or jump - if (next_node_type == NODE_WATER) - { - self->bot.bi.actionflags &= ~ACTION_MOVEUP; - self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; - self->bot.bi.actionflags &= ~ACTION_CROUCH; - - //VectorClear(self->bot.bi.dir); - //self->bot.bi.speed = 0; - - VectorNormalize(walkdir); - VectorCopy(walkdir, self->bot.bi.dir); - self->bot.bi.speed = move_speed; // Set our suggested speed - - // Check that we're in the water - vec3_t temp = { 0,0,0 }; - VectorCopy(self->s.origin, temp); - temp[2] = self->s.origin[2] - 8; - int contents_feet = gi.pointcontents(temp); - - // Almost out of air, start heading up to the surface - if ((contents_feet & MASK_WATER) && self->air_finished_framenum < level.framenum + 5) // Move up to get air - { - self->bot.bi.actionflags |= ACTION_MOVEUP; - self->bot.node_travel_time = 0; // Ignore node travel time while we get some air - //Com_Printf("%s %s [%d] water: get air\n", __func__, self->client->pers.netname, level.framenum); - } - else if ((contents_feet & MASK_WATER) && fabs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]) <= 33) // Roughly leveled out - { - //Com_Printf("%s %s [%d] water: level\n", __func__, self->client->pers.netname, level.framenum); - } - else if ((contents_feet & MASK_WATER) && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) // Move up - { - self->bot.bi.actionflags |= ACTION_MOVEUP; - //Com_Printf("%s %s [%d] water: move up\n", __func__, self->client->pers.netname, level.framenum); - } - else if (nodes[self->bot.next_node].origin[2] < self->s.origin[2]) // Move down - { - if (contents_feet & MASK_WATER) - self->bot.bi.actionflags |= ACTION_MOVEDOWN; // In water moving down - else - self->bot.bi.actionflags |= ACTION_CROUCH; // Crouch drop down into water below - - //Com_Printf("%s %s [%d] water: move down\n", __func__, self->client->pers.netname, level.framenum); - } - - } - - // Pull bot in when close to the next node, help guide it in - if (0) - { - if (self->groundentity == false && level.framenum > self->bot.stuck_last_negate) - { - vec3_t bot_to_node; - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); - bot_to_node[2] = 0; - float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node - if (xy_bot_to_next_dist > 32 && xy_bot_to_next_dist <= 128) - { - // Line of sight - tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - self->bot.stuck_last_negate = level.framenum + 1 * HZ; - vec3_t dir; - dir[0] = walkdir[0]; - dir[1] = walkdir[1]; - dir[2] = walkdir[2]; - VectorNormalize(dir); - self->velocity[0] += 50 * dir[0]; - self->velocity[1] += 50 * dir[1]; - self->velocity[2] += 300 * dir[2]; - // Limit velocity - if (self->velocity[2] < -300) - self->velocity[2] = -300; - //Com_Printf("%s %s [%d] directing bot closer to next node\n", __func__, self->client->pers.netname, level.framenum); - } - } - } - } - - if (next_node_type == NODE_MOVE) - { - /* - if (BOTLIB_CanMoveDir(self, walkdir) == false) - { - // We can't move in this direction - Com_Printf("%s %s can't move safely in direction\n", __func__, self->client->pers.netname); - self->bot.stuck_wander_time = 15; - self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving - return; - } - */ - /* - float fwd_distance = 32; - tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope - fwd_distance = 128; // Extend the distance we check for a safe direction to move toward - - // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge - if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false) == false); - { - if (level.framenum > self->bot.stuck_last_negate) - { - // Only allow this to happen once every 60 seconds - // It's reset after 60 seconds, death, or touching ground again - self->bot.stuck_last_negate = level.framenum + 60 * HZ; - - Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); - - // Reverse the direction - VectorNegate(self->velocity, self->velocity); - VectorClear(self->bot.bi.dir); - } - } - */ - } - - //if (next_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMP) - // BOTLIB_Crouch_Or_Jump(self, ucmd, dir); - - if (next_node_type == NODE_BOXJUMP) - { - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up - { - self->bot.bi.actionflags |= ACTION_BOXJUMP; - self->bot.bi.actionflags &= ~ACTION_JUMP; - self->bot.bi.actionflags &= ~ACTION_CROUCH; - } - else - { - // Remove flags - self->bot.bi.actionflags &= ~ACTION_CROUCH; - self->bot.bi.actionflags &= ~ACTION_BOXJUMP; - self->bot.bi.actionflags &= ~ACTION_JUMP; - } - } - - // Slow down if we're not on the ground - if (next_node_type == NODE_MOVE) - { - if (self->groundentity == NULL) - { - //self->bot.bi.speed = SPEED_CAREFUL; // Set our speed directly - self->bot.bi.speed = SPEED_ROAM; // Set our speed directly - //Com_Printf("%s %s SPEED_CAREFUL node_travel_time[%d]\n", __func__, self->client->pers.netname, self->bot.node_travel_time); - } - //if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2] + 32) // Going up - //{ - // self->bot.bi.actionflags |= ACTION_JUMP; - //} - else - { - // Remove flags - //self->bot.bi.actionflags &= ~ACTION_CROUCH; - //self->bot.bi.actionflags &= ~ACTION_JUMP; - - //if (nodes[self->bot.next_node].normal[2] > 0.7) // If the next node is flat - //if (random() < 0.1) - // self->bot.bi.actionflags |= ACTION_BOXJUMP; - } - } - if (next_node_type == NODE_CROUCH) - { - self->bot.bi.actionflags |= ACTION_CROUCH; - - // Remove flags - self->bot.bi.actionflags &= ~ACTION_JUMP; - } - - - if (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER) - { - VectorCopy(walkdir, self->bot.bi.dir); - self->bot.bi.speed = 100; // Set speed slower for ladders - - if (self->bot.touching_ladder) - { - // Remove flags - self->bot.bi.actionflags &= ~ACTION_ATTACK; // Don't attack when on ladder - - if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up - { - self->bot.bi.actionflags |= ACTION_MOVEUP; - self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; - } - else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down - { - self->bot.bi.actionflags |= ACTION_MOVEDOWN; - self->bot.bi.actionflags &= ~ACTION_MOVEUP; - } - } - else if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Jump to ladder - { - self->bot.bi.actionflags |= ACTION_JUMP; - } - } - /* - //if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) - if (touching_ladder == false && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) - { - VectorCopy(walkdir, self->bot.bi.dir); - self->bot.bi.speed = 400; // Set speed slower for ladders - - //if (touching_ladder) - { - self->bot.bi.actionflags |= ACTION_JUMP; - - self->bot.bi.actionflags &= ~ACTION_MOVEUP; - self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; - } - } - */ - // Make sure the bot gets off the top and bottom of the ladder - if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 4 && self->groundentity == NULL) // - { - if (nodes[self->bot.prev_node].origin[2] < nodes[self->bot.current_node].origin[2] && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 2) // Getting off ladder at the top - { - //Com_Printf("%s %s MOVE UP\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_JUMPPAD; - self->bot.bi.actionflags |= ACTION_MOVEUP; - //self->bot.bi.actionflags |= ACTION_BOXJUMP; - } - else if (nodes[self->bot.prev_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Getting off ladder at the bottom - { - //Com_Printf("%s %s MOVE DOWN\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_JUMPPAD; - //self->bot.bi.actionflags |= ACTION_MOVEDOWN; - //self->bot.bi.actionflags |= ACTION_BOXJUMP; - } - } - //if (self->bot.prev_node != INVALID) - { - //Com_Printf("%s %s types[ %d %d %d ]\n", __func__, self->client->pers.netname, nodes[self->bot.prev_node].type, nodes[self->bot.current_node].type, nodes[self->bot.next_node].type); - } - - if (next_node_type == NODE_JUMP && self->groundentity) - { - //Com_Printf("%s %s ACTION_JUMP\n", __func__, self->client->pers.netname); - self->bot.bi.actionflags |= ACTION_JUMP; - - // Remove flags - self->bot.bi.actionflags &= ~ACTION_CROUCH; - } - - // Handle jumping - if (next_node_type == NODE_JUMPPAD) - { - //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); - - // Remove flags - self->bot.bi.actionflags &= ~ACTION_CROUCH; - self->bot.bi.actionflags &= ~ACTION_JUMP; - - //self->bot.bi.actionflags = 0; - //VectorClear(self->bot.bi.dir); - - // Bot only applies direction when it's in a falling state - if (self->groundentity || self->velocity[2] > 0) - self->bot.bi.speed = 0; - else - { - //horizontal direction - vec3_t hordir; - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - - self->bot.bi.speed = move_speed; // Set our suggested speed - } - self->bot.bi.actionflags |= ACTION_JUMPPAD; - //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); - - //Com_Printf("%s %s NODE_JUMPPAD\n", __func__, self->client->pers.netname); -#if 0 - // Get distance from bot to node - nodes[self->bot.current_node].origin; - vec3_t bot_to_node; - - VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); - bot_to_node[2] = 0; - float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node - - VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_node); - bot_to_node[2] = 0; - float xy_bot_to_curr_dist = VectorLength(bot_to_node); // Distance from bot to current node - //if (xy_bot_to_curr_dist <= 32) // If close enough to jump pad - - - float distance = 16; - vec3_t bmins = { nodes[self->bot.current_node].absmin[0] + -(distance), nodes[self->bot.current_node].absmin[1] + -(distance), nodes[self->bot.current_node].absmin[2] + -(distance) }; - vec3_t bmaxs = { nodes[self->bot.current_node].absmax[0] + distance, nodes[self->bot.current_node].absmax[1] + distance, nodes[self->bot.current_node].absmax[2] + distance }; - if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) - { - Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); - self->bot.bi.actionflags |= ACTION_JUMPPAD; - //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); - - // Trace up to see if we're going to hit our head - tr = gi.trace(self->s.origin, tv(-16, -16, -0), tv(16, 16, 32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] + 60), self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - // Trace down to see if we hit the ground - tr = gi.trace(self->s.origin, tv(-32, -32, -0), tv(32, 32, 0), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 60), self, MASK_PLAYERSOLID); - qboolean is_player = false; - if (tr.ent && tr.ent->client) - is_player = true; - - /* - // If landing on flat ground, only allow jumping when once we touch the ground - // Otherwise if its a slope, allow jumping when we're close'ish to the ground - qboolean can_jump = true; - //if (tr.plane.normal[2] == 1 && self->groundentity == NULL && self->velocity[2] < 0) // flat ground - if (self->groundentity == NULL && self->velocity[2] < 0) // flat ground - { - can_jump = false; - self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving - Com_Printf("%s %s ACTION_HOLDPOS\n", __func__, self->client->pers.netname); - } - */ - - if ((tr.fraction < 1 || tr.startsolid) && is_player == false) // && can_jump) - { - //float height_diff = nodes[self->bot.next_node].origin[2] - self->s.origin[2]; // Height difference between bot and next node - //if (xy_bot_to_next_dist <= 32 && height_diff <= 60) - { - // Bot jumped - //Com_Printf("%s %s jumped\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_JUMP; - } - //else - { - // Trace from bot to node to see if we can jump to it - //tr = gi.trace(self->s.origin, tv(-24, -24, -STEPSIZE), tv(24, 24, 96), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); - //if (tr.fraction == 1.0) - { - //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_JUMPPAD; // Stop moving - } - } - } - } - } -#endif - //if (moved == false) - } - } - - if (ctf->value) - { - // Slow down if near flag - if (bot_ctf_status.flag1_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin) < 128) - { - self->bot.bi.speed = 200; - } - if (bot_ctf_status.flag2_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin) < 128) - { - self->bot.bi.speed = 200; - } - } - - if (self->bot.see_enemies) - { - qboolean dodging = ((rand() % 10) < 7); - - // Don't dodge if < x links on node - low link count might indicate a tight walk or narrow way - if (nodes[self->bot.current_node].num_links < 4 || nodes[self->bot.next_node].num_links < 4) - dodging = false; - - // Don't dodge if bot is taking a direct path - if (self->bot.node_random_path == false) - dodging = false; - - if (ctf->value) // Reduce dodging in CTF - { - //float f1 = BOTLIB_DistanceToFlag(self, FLAG_T1_NUM); - //float f2 = BOTLIB_DistanceToFlag(self, FLAG_T2_NUM); - - float f1 = 99999999; - float f2 = 99999999; - if (bot_ctf_status.flag1_curr_node != INVALID) - f1 = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); - if (bot_ctf_status.flag2_curr_node != INVALID) - f2 = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); - - if (f1 < 1500 || f2 < 1500 || BOTLIB_Carrying_Flag(self)) // || self->bot.goal_node == bot_ctf_status.flag1_curr_node || self->bot.goal_node == bot_ctf_status.flag2_curr_node) - { - dodging = false; - //Com_Printf("%s %s dodging is OFF ------ \n", __func__, self->client->pers.netname); - } - - //dodging = false; - } - - // Try strafing around enemy - trace_t tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_SHOT); - if (dodging && tr.plane.normal[2] > 0.85) // Not too steep - { - // Try strafing continually in a general direction, if possible - static int max_strafe_left = -10; - static int max_strafe_right = 10; - if (self->bot_strafe == 0) // Pick new direction - { - float strafe_choice = random() < 0.333; - if (strafe_choice < 0.33) // Left - self->bot_strafe = -1; - else if (strafe_choice < 0.66) // Right - self->bot_strafe = 1; - else - self->bot_strafe = 0; // Neither - skip strafing this turn - } - - if (self->client->weapon == FindItem(HC_NAME)) - self->bot_strafe = 0; // Don't strafe with HC, just go straight for them - - if (self->bot_strafe < 0 && random() > 0.15) // Going left 85% of the time - { - if (BOTLIB_CanMove(self, MOVE_LEFT) && self->bot_strafe >= max_strafe_left) // Can go left with a limit - { - //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); - self->bot_strafe--; - self->bot.bi.actionflags |= ACTION_MOVELEFT; - } - else if (BOTLIB_CanMove(self, MOVE_RIGHT)) // Cannot go left anymore, so try going right - { - self->bot_strafe = 1; // Go right - self->bot.bi.actionflags |= ACTION_MOVERIGHT; - //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); - } - else - self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice - } - else if (self->bot_strafe > 0 && random() > 0.15) // Going right 85% of the time - { - if (BOTLIB_CanMove(self, MOVE_RIGHT) && self->bot_strafe <= max_strafe_right) // Can go right with a limit - { - //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); - self->bot_strafe++; - self->bot.bi.actionflags |= ACTION_MOVERIGHT; - } - else if (BOTLIB_CanMove(self, MOVE_LEFT)) // Cannot go right anymore, so try going left - { - self->bot_strafe = -1; // Go left - self->bot.bi.actionflags |= ACTION_MOVELEFT; - //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); - } - else - self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice - } - else - self->bot_strafe = 0; // Skip strafing this turn - - // Back off if getting too close (unless we have a HC or knife) - if (self->bot.enemy_dist < 256) - { - if (self->client->weapon == FindItem(HC_NAME) && self->client->cannon_rds) - { - // Come in close for the kill - if (ACEMV_CanMove(self, MOVE_FORWARD)) - self->bot.bi.actionflags |= ACTION_MOVEFORWARD; - } - else if (self->client->weapon == FindItem(KNIFE_NAME)) - { - // Come in close for the kill - if (ACEMV_CanMove(self, MOVE_FORWARD)) - self->bot.bi.actionflags |= ACTION_MOVEFORWARD; - } - // Try move backwards - else if (BOTLIB_CanMove(self, MOVE_BACK)) - { - self->bot.bi.actionflags |= ACTION_MOVEBACK; - } - } - // If distance is far, consider crouching to increase accuracy - else if (self->bot.enemy_dist > 1024) - { - // Check if bot should be crouching based on weapon and if strafing - if (INV_AMMO(self, LASER_NUM) == false && - self->client->weapon != FindItem(SNIPER_NAME) && - self->client->weapon != FindItem(HC_NAME) && - self->client->weapon != FindItem(M3_NAME) && - (self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && - (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) - { - // Raptor007: Don't crouch if it blocks the shot. - float old_z = self->s.origin[2]; - self->s.origin[2] -= 14; - if (ACEAI_CheckShot(self)) - { - self->bot.bi.actionflags |= ACTION_CROUCH; - //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); - } - self->s.origin[2] = old_z; - } - } - else - { - // Keep distance with sniper - if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(SNIPER_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) - { - self->bot.bi.actionflags |= ACTION_MOVEBACK; - } - // Keep distance with grenade - if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) - { - self->bot.bi.actionflags |= ACTION_MOVEBACK; - } - // Otherwise move toward target - //else if (ACEMV_CanMove(self, MOVE_FORWARD)) - // self->bot.bi.actionflags |= ACTION_MOVEFORWARD; - } - - // If the bot is dodging by strafing, add in some random jumps - if (self->bot_strafe != 0 && (self->bot.bi.actionflags & ACTION_CROUCH) == 0 && random() < 0.2) - self->bot.bi.actionflags |= ACTION_BOXJUMP; // Jump while dodging - } - } - - int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed - if (perform_action == ACTION_JUMP) - { - self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping - self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching - self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad - } - else if (perform_action == ACTION_CROUCH) - { - self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching - self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping - self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad - } - - // Stay on course by strafing back to the path line (if not already strafing) - //if (VectorLength(self->velocity) < 37) // If stuck - if (moved == false) - { - // Try strafing - qboolean right_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, 8); // Check right - qboolean left_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, -8); // Check left - if (right_blocked && left_blocked) // Both are blocked - { - self->bot.bi.actionflags |= ACTION_JUMP; // Try jumping to clear the obstacle - //Com_Printf( "%s %s blocked\n", __func__, self->client->pers.netname); - - - /* - // Bot stuck - Test of bot has LoS to next node - if (self->bot.next_node == INVALID) - { - self->bot.stuck = true; - return; - } - trace_t tr = gi.trace(self->s.origin, tv(-4, -4, 0.1), tv(4, 4, 56), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); - if (tr.startsolid || tr.fraction < 1.0) - { - if (VectorLength(self->velocity) < 5) - self->bot.stuck = true; - } - */ - //self->bot.bi.actionflags |= ACTION_MOVEFORWARD; - //self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); - //self->bot.bi.viewangles[PITCH] = 0; - } - else if (right_blocked && BOTLIB_CanMove(self, MOVE_LEFT)) // Strafe left - { - // Get the direction perpendicular to the dir, facing left - vec3_t left; - left[0] = -walkdir[1]; - left[1] = walkdir[0]; - left[2] = 0; - VectorNormalize(left); - VectorCopy(left, self->bot.bi.dir); - - - //Com_Printf("%s %s ACTION_MOVELEFT\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_MOVELEFT; - } - else if (left_blocked && BOTLIB_CanMove(self, MOVE_RIGHT)) // Strafe right - { - // Get the direction perpendicular to the dir, facing right - vec3_t right; - right[0] = walkdir[1]; - right[1] = -walkdir[0]; - right[2] = 0; - VectorNormalize(right); - VectorCopy(right, self->bot.bi.dir); - - //Com_Printf("%s %s ACTION_MOVERIGHT\n", __func__, self->client->pers.netname); - //self->bot.bi.actionflags |= ACTION_MOVERIGHT; - } - else - { - //Com_Printf("%s %s moved == false and not blocked on side\n", __func__, self->client->pers.netname); - //if (VectorLength(self->velocity) < 5) - // self->bot.stuck = true; - } - } - - /* - // Project forward looking for walls - vec3_t start, end; - vec3_t forward, right; - vec3_t offset; - AngleVectors(self->client->v_angle, forward, right, NULL); - VectorSet(offset, 0, 7, self->viewheight - 8); - offset[1] = 0; - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, 48, forward, end); - trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); - - // If we hit an obstacle or we've not moved much, turn around - if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37) - { - self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); - self->bot.bi.viewangles[PITCH] = 0; - } - */ -} -//rekkie -- Quake3 -- e - -//////////////////// -// Wandering code // -//////////////////// -// -// Basic wandering code, simply here as a backup to help the bot find a nearby node -// -void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd) -{ -#if 0 - vec3_t start, end; - vec3_t forward, right; - vec3_t offset; - - // Do not move - if (self->next_move_time > level.framenum) - return; - - //Com_Printf("%s %s wandering...\n", __func__, self->client->pers.netname); - - // Special check for elevators, stand still until the ride comes to a complete stop. - if (self->groundentity && (VectorLength(self->groundentity->velocity) >= 8)) - { - // only move when platform not - if (self->groundentity->moveinfo.state == STATE_UP || self->groundentity->moveinfo.state == STATE_DOWN) - { - self->velocity[0] = 0; - self->velocity[1] = 0; - self->velocity[2] = 0; - self->next_move_time = level.framenum + 0.5 * HZ; - return; - } - } - - // Contents check - vec3_t temp = { 0,0,0 }; - VectorCopy(self->s.origin, temp); - temp[2] += 22; - int contents_head = gi.pointcontents(temp); - temp[2] = self->s.origin[2] - 8; - int contents_feet = gi.pointcontents(temp); - - // Just try to keep our head above water. - if (contents_head & MASK_WATER) - { - // If drowning and no node, move up - if (self->client->next_drown_framenum > 0) - { - ucmd->upmove = SPEED_RUN; - self->s.angles[PITCH] = -45; - } - else - ucmd->upmove = SPEED_WALK; - } - - // Don't wade in lava, try to get out! - if (contents_feet & (CONTENTS_LAVA | CONTENTS_SLIME)) - ucmd->upmove = SPEED_RUN; - - - - // Check speed - VectorSubtract(self->s.origin, self->lastPosition, temp); - float moved = VectorLength(temp); - - if (contents_feet & MASK_WATER) - { - self->bot.goal_node = INVALID; - self->bot.current_node = INVALID; - //if (debug_mode) - //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); - } - - - - // Crouch or jump - trace_t tr_lower_body; - trace_t tr_upper_body; - //vec3_t fwd; - //AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin - //VectorMA(self->s.origin, 50, fwd, fwd); - //tr_lower_body = gi.trace(self->s.origin, tv(-15, -15, -15), tv(15, 15, 0), forward, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units - //tr_upper_body = gi.trace(self->s.origin, tv(-15, -15, 0), tv(15, 15, 32), forward, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units - - AngleVectors(self->client->v_angle, forward, right, NULL); - VectorSet(offset, 0, 7, self->viewheight - 8); - offset[1] = 0; - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, 50, forward, end); - tr_lower_body = gi.trace(start, tv(-15, -15, -15), tv(15, 15, 0), end, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units - tr_upper_body = gi.trace(start, tv(-15, -15, 0), tv(15, 15, 32), end, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units - - // Need to crouch? - // Lower body is free, upper body is blocked - if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) - { - ucmd->upmove = -400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - //Com_Printf("%s %s is stuck [Wander]: crouching\n", __func__, self->client->pers.netname); - return; - } - // Need to jump? - // Lower body is blocked, upper body is free - if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) - { - if (contents_feet & MASK_WATER) - { - //if (debug_mode) - //Com_Printf("%s %s is stuck [Wander]: jumping out of water\n", __func__, self->client->pers.netname); - self->s.angles[PITCH] = -45; - //VectorScale(end, 50, self->velocity); - ucmd->forwardmove = 400; - //ucmd->upmove = 400; - if (self->velocity[2] < 350) - self->velocity[2] = 350; - return; - } - - ucmd->upmove = 400; - ucmd->forwardmove = SPEED_RUN; - //if (debug_mode) - //Com_Printf("%s %s is stuck [Wander]: jumping\n", __func__, self->client->pers.netname); - - return; - } - // In water facing a ladder - //if ((contents_feet & MASK_WATER) && tr_lower_body.fraction < 1.0 && tr_upper_body.fraction < 1.0) - if ((contents_feet & MASK_WATER) && ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER))) - { - //if ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER)) - { - //if (debug_mode) - //Com_Printf("%s %s is stuck [Wander]: MASK_WATER -> CONTENTS_LADDER\n", __func__, self->client->pers.netname); - ucmd->forwardmove = 400; - ucmd->upmove = 400; - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY * 3, NODE_ALL); - self->s.angles[PITCH] = -45; - if (self->velocity[2] < 350) - self->velocity[2] = 350; - return; - } - } - - - - // Project forward looking for walls - //vec3_t start, end; - //vec3_t forward, right; - //vec3_t offset; - AngleVectors(self->client->v_angle, forward, right, NULL); - VectorSet(offset, 0, 7, self->viewheight - 8); - offset[1] = 0; - G_ProjectSource(self->s.origin, offset, forward, right, start); - VectorMA(start, 48, forward, end); - trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); - - // If we hit an obstacle or we've not moved much, turn around - if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37 || moved < FRAMETIME) - { - self->s.angles[YAW] += 22.5 + (random() * 270); - self->s.angles[PITCH] = 0; - - if (contents_feet & MASK_WATER) // Just keep swimming. - ucmd->forwardmove = SPEED_RUN; - else if (!M_CheckBottom(self) && !self->groundentity) // if there is ground continue otherwise wait for next move - ucmd->forwardmove = 0; - else if (ACEMV_CanMove(self, MOVE_FORWARD)) - ucmd->forwardmove = SPEED_WALK; - - return; - } - - // Otherwise try move forward normally - if (ACEMV_CanMove(self, MOVE_FORWARD) || (contents_feet & MASK_WATER)) - { - ucmd->forwardmove = SPEED_RUN; - } - - //if (self->client->leg_damage > 0) // Do we have leg damage? - // return; - - // If no goal, try to get a new one - if (self->bot.goal_node == INVALID) - ACEAI_PickLongRangeGoal(self); - - // Try to move to our goal if we can - if (self->bot.goal_node != INVALID) - { - // Update the node we're near - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); - //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); - //if (tmp_node != self->bot.current_node) - //{ - // self->bot.prev_node = self->bot.current_node; - // self->bot.current_node = tmp_node; - // //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); - //} - - if (self->bot.current_node == INVALID) - { - if (debug_mode) - Com_Printf("%s %s could not find FindClosestReachableNode to reach goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); - self->bot.state = STATE_WANDER; - self->wander_timeout = level.framenum + 0.1 * HZ; - return; - } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal - { - if (debug_mode) - Com_Printf("%s %s Wander finding alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); - //BOTLIB_SetGoal(self, self->bot.goal_node); - //return; - - self->bot.next_node = self->bot.current_node; - self->bot.state = STATE_POSITION; - } - else // We couldn't visit the goal node, so lets pick a new goal - { - if (debug_mode) - Com_Printf("%s %s Wander cannot visit goal %d. Picking new goal\n", __func__, self->client->pers.netname, self->bot.goal_node); - ACEAI_PickLongRangeGoal(self); - } - } -#endif -} +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +botlib_noises_t botlib_noises; + +// Returns the XYZ distance +float VectorDistance(vec3_t start, vec3_t end) +{ + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + return VectorLength(v); +} +// Returns the XY distance +float VectorDistanceXY(vec3_t start, vec3_t end) +{ + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + v[2] = 0; + return VectorLength(v); +} + +// Converts node type to string +// Types: NODE_MOVE, NODE_CROUCH, NODE_STEP, NODE_JUMP, NODE_JUMPPAD, NODE_STAND_DROP, NODE_CROUCH_DROP, NODE_UNSAFE_DROP, NODE_LADDER_UP, NODE_LADDER_DOWN, NODE_DOOR, NODE_PLATFORM, NODE_TELEPORTER, NODE_ITEM, NODE_WATER, NODE_GRAPPLE, NODE_SPAWNPOINT, NODE_POI, NODE_LEARN +// String: The string to copy the node type to +// Max string size: The maximum size of the string +// Returns: false if node type was not found +qboolean NodeTypeToString(edict_t* self, int type, char *string, const int max_string_size) +{ + int len; + switch (type) + { + case NODE_MOVE: + len = strlen("MOVE"); // Get the length of the string + len = len < max_string_size ? len : max_string_size - 1; // If the string is too long, truncate it + strncpy(string, "MOVE", len); // Copy the string + break; + case NODE_CROUCH: + len = strlen("CROUCH"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "CROUCH", len); + break; + case NODE_BOXJUMP: + len = strlen("NODE_BOXJUMP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "NODE_BOXJUMP", len); + break; + case NODE_JUMP: + len = strlen("JUMP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "JUMP", len); + break; + case NODE_JUMPPAD: + len = strlen("JUMPPAD"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "JUMPPAD", len); + break; + case NODE_LADDER: + len = strlen("LADDER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER", len); + break; + case NODE_POI: + len = strlen("POI"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "POI", len); + break; + case NODE_POI_LOOKAT: + len = strlen("NODE_POI_LOOKAT"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "NODE_POI_LOOKAT", len); + break; + case NODE_WATER: + len = strlen("WATER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "WATER", len); + break; + + + + case NODE_STEP: + len = strlen("STEP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "STEP", len); + break; + case NODE_STAND_DROP: + len = strlen("STAND_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "STAND_DROP", len); + break; + case NODE_CROUCH_DROP: + len = strlen("CROUCH_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "CROUCH_DROP", len); + break; + case NODE_UNSAFE_DROP: + len = strlen("UNSAFE_DROP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "UNSAFE_DROP", len); + break; + case NODE_LADDER_UP: + len = strlen("LADDER_UP"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER_UP", len); + break; + case NODE_LADDER_DOWN: + len = strlen("LADDER_DOWN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LADDER_DOWN", len); + break; + case NODE_DOOR: + len = strlen("DOOR"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "DOOR", len); + break; + case NODE_PLATFORM: + len = strlen("PLATFORM"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "PLATFORM", len); + break; + case NODE_TELEPORTER: + len = strlen("TELEPORTER"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "TELEPORTER", len); + break; + case NODE_ITEM: + len = strlen("ITEM"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "ITEM", len); + break; + case NODE_GRAPPLE: + len = strlen("GRAPPLE"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "GRAPPLE", len); + break; + case NODE_SPAWNPOINT: + len = strlen("SPAWNPOINT"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "SPAWNPOINT", len); + break; + case NODE_LEARN: + len = strlen("LEARN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "LEARN", len); + break; + default: + len = strlen("UNKNOWN"); + len = len < max_string_size ? len : max_string_size - 1; + strncpy(string, "UNKNOWN", len); + string[len] = '\0'; // Terminate string + return false; // Unknown node type + } + + string[len] = '\0'; // Terminate string + return true; // Success +} + +/////////////////////////////////////////////////////////////////////// +// Bot Movement - Node Type - Utility +// Display all the node type names +// onlyPrintProblemTypes: only output if types are INVALID +/////////////////////////////////////////////////////////////////////// +void PrintAllLinkNodeTypes(edict_t *self, qboolean onlyPrintProblemTypes) +{ + qboolean foundProblem = false; + int curr_node_position = 0; // The position of the current node in the node_list array + + if (self->bot.node_list_count <= 0) // No nodes found + return; + + for (int i = 0; i < self->bot.node_list_count; i++) // If the list has nodes + { + int node = self->bot.node_list[i]; // Next node in the list + if (node != INVALID && node == self->bot.current_node) // If the node is valid + { + curr_node_position = i; // Save the position of the current node + break; + } + } + + if (0) // Debug: print out the node list + { + Com_Printf("%s: count[%d] node_list[", __func__, self->bot.node_list_count); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + Com_Printf(" %d ", self->bot.node_list[i]); + } + Com_Printf("]\n"); + } + + if (0) // Debug: print out the node types + { + int curr_node; + int next_node; + Com_Printf("%s: targetNodeTypes[", __func__); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + if (i + 1 < self->bot.node_list_count) // If there is a next node + { + curr_node = self->bot.node_list[i]; // Get current node + next_node = self->bot.node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); + //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + // foundProblem = true; + break; + } + } + } + } + Com_Printf("]\n"); + } + + char tmp_string[32] = { '\0' }; // Length of the longest node type name + char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each + int tmp_type; + int curr_node; + int next_node; + //Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = curr_node_position; i < self->bot.node_list_count; i++) // Start at the current node position + { + if (i + 1 < self->bot.node_list_count) // If there is a next node + { + //Com_Printf(" %d ", node_list[i]); + curr_node = self->bot.node_list[i]; // Get current node + next_node = self->bot.node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + // Add the node number + char tmp_num[9]; // Max size of a node number as a char: "[999999]\0" == (8 characters + NULL terminator = length of 9) + sprintf(tmp_num, "[%d]", self->bot.node_list[i]); + strcat(all_node_types, tmp_num); + + // Add the node type + tmp_type = nodes[curr_node].links[l].targetNodeType; + if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) + foundProblem = true; + strcat(all_node_types, tmp_string); + if (i + 2 < self->bot.node_list_count) + strcat(all_node_types, " "); + break; + } + } + } + } + //Com_Printf("]\n"); + + Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); + + /* + #define MAX_PRINT_ALL_LINK_NODE_TYPES 32 + int node_list[MAX_PRINT_ALL_LINK_NODE_TYPES]; + int node_count = BOTLIB_SLL_Query_All_Nodes(&self->pathList, node_list, MAX_PRINT_ALL_LINK_NODE_TYPES); // Retrieve the nodes in the list, if any + + if (node_count <= 0) // No nodes found + return; + + + if (0) // Debug: print out the node list + { + Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = 0; i < node_count; i++) + { + Com_Printf(" %d ", node_list[i]); + } + Com_Printf("]\n"); + } + + if (0) // Debug: print out the node types + { + int curr_node; + int next_node; + Com_Printf("%s: targetNodeTypes[", __func__); + for (int i = 0; i < node_count; i++) + { + if (i + 1 < node_count) // If there is a next node + { + curr_node = node_list[i]; // Get current node + next_node = node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + Com_Printf("%d ", nodes[curr_node].links[l].targetNodeType); + //if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + // foundProblem = true; + break; + } + } + } + } + Com_Printf("]\n"); + } + + + char tmp_string[32] = { '\0' }; // Length of the longest node type name + char all_node_types[32 * 32] = { '\0' }; // 32 node types, 32 chars each + int tmp_type; + int curr_node; + int next_node; + //Com_Printf("%s: count[%d] node_list[", __func__, node_count); + for (int i = 0; i < node_count; i++) + { + if (i + 1 < node_count) // If there is a next node + { + //Com_Printf(" %d ", node_list[i]); + curr_node = node_list[i]; // Get current node + next_node = node_list[i + 1]; // Get next node + + // Get the node type this links to + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[curr_node].links[l].targetNode == next_node) // If this is the link from curr to the next node + { + // Add the node number + char tmp_num[6]; // Max size of a node number as a char: 999,999 (6 chars) + sprintf(tmp_num, "[%d]", node_list[i]); + strcat(all_node_types, tmp_num); + + // Add the node type + tmp_type = nodes[curr_node].links[l].targetNodeType; + if (NodeTypeToString(self, tmp_type, tmp_string, sizeof(tmp_string)) == false) + foundProblem = true; + strcat(all_node_types, tmp_string); + if (i + 2 < node_count) + strcat(all_node_types, " "); + break; + } + } + } + } + //Com_Printf("]\n"); + + Com_Printf("%s: [Node] + Type [%s]\n", __func__, all_node_types); + */ + +#if 0 + char curr_type[64]; + char next_type[64]; + if (NodeTypeToString(self, type_from, curr_type, sizeof(next_type)) == false) + foundProblem = true; + if (NodeTypeToString(self, type_to, next_type, sizeof(next_type)) == false) + foundProblem = true; + + // -------------------------------------------- + char after_next_type[64]; + after_next_type[0] = '\0'; + //int node_after_next = BOTLIB_SLL_Query_All_Nodes(&self->pathList); // Find out what node comes after the next node, if any + if (node_after_next == INVALID) + { + strcpy(after_next_type, "INVALID"); // No next node + } + else + { + + qboolean found_link_type = false; + if (self->bot.next_node != INVALID) + { + for (int l = 0; l < nodes[self->bot.next_node].num_links; l++) + { + if (nodes[self->bot.next_node].links[l].targetNode == node_after_next) + { + Com_Printf("%s: targetNodeType[%d]\n", __func__, nodes[self->bot.next_node].links[l].targetNodeType); + found_link_type = true; + if (NodeTypeToString(self, nodes[self->bot.next_node].links[l].targetNodeType, after_next_type, sizeof(after_next_type)) == false) + foundProblem = true; + break; + } + } + } + if (found_link_type == false) + strcpy(after_next_type, "INVALID"); // No next node + + + //Com_Printf("%s: cur[%d] nxt[%d] nxt_nxt[%d]\n", __func__, self->bot.current_node, self->bot.next_node, node_after_next); + if (NodeTypeToString(self, nodes[node_after_next].type, after_next_type, sizeof(after_next_type)) == false) + foundProblem = true; + } + // -------------------------------------------- + + if (onlyPrintProblemTypes) // Only print if errors found + { + if (foundProblem == false) // No error, so bail + return; + } + + if (self->groundentity) + Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [GROUND %d]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground); + else + Com_Printf("%s: n[%d to %d] cur[%s] nxt[%s] nxt_nxt[%s] [AIR %d] [ZVEL %f]\n", __func__, self->bot.current_node, self->bot.next_node, curr_type, next_type, after_next_type, self->last_touched_ground, self->velocity[2]); +#endif +} + +/////////////////////////////////////////////////////////////////////// +// Changes the bots view angle +// +// move_vector: The move direction +// +// range: How close the current yaw needs to be to accepted as ideal +// <= 0: The bot will snap turn instantly +// > 0: Makes the change in angles a little more gradual, +// not so snappy. Subtle, but noticeable. +// Default: 2 +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_UTIL_ChangeBotAngleYaw(edict_t* ent, vec3_t move_vector, float range) +{ + float ideal_yaw; + float current_yaw; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(move_vector); + + current_yaw = anglemod(ent->s.angles[YAW]); + + vectoangles(move_vector, ideal_angle); + + ideal_yaw = anglemod(ideal_angle[YAW]); + + if (current_yaw == ideal_yaw) + return true; + + if (range <= 0) // Instant turn + { + ent->s.angles[YAW] = ideal_yaw; + return true; + } + else + { + // Turn speed based on angle of curr vs ideal + // If the turn angle is large, the bot will instantly snap + // This helps prevent circular motion when very close to a node + yaw_move = ideal_yaw - current_yaw; + if (ideal_yaw > current_yaw) + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); + if (fabs(yaw_move) >= 45.0) // Snap turn + { + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); + ent->s.angles[YAW] = ideal_yaw; + return false; + } + /* + float normal = 0; + if (current_yaw > ideal_yaw) + { + normal = (ideal_yaw + 1) / (current_yaw + 1); + } + else + { + normal = (current_yaw + 1) / (ideal_yaw + 1); + } + if (normal < 0.5) // Snap turn when angle is large + { + Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, normal); + ent->s.angles[YAW] = ideal_yaw; + return true; + } + */ + + // Yaw turn speed + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + + //rekkie -- s + const float turn_speed_factor = 1.25; // Slow down turn speed + move_ratio /= turn_speed_factor; // Slow turn speed by a factor of + //rekkie -- e + + ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + + // Check if current_yaw is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw_move < range && yaw_move > -(range)) + return true; // Ideal yaw reached + else + return false; // Ideal yaw not yet reached + + } +} + +// Change in angles can be gradual to snappy +// move_vector : The move direction +// instant : Instant 'snap' turn +// move_speed : How fast to turn, 1.0 is fast and 3.0 is slow +// yaw : Allow changing the yaw (left & right) +// pitch : Allow changing the pitch (up & down) +// Returns true if angle reached, or false if still moving toward angle +qboolean BOTLIB_ChangeBotAngleYawPitch(edict_t* ent, vec3_t move_vector, qboolean instant, float move_speed, qboolean yaw, qboolean pitch) +{ + float ideal_yaw; + float ideal_pitch; + float current_yaw; + float current_pitch; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float pitch_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(move_vector); + vectoangles(move_vector, ideal_angle); + + current_yaw = anglemod(ent->s.angles[YAW]); + current_pitch = anglemod(ent->s.angles[PITCH]); + + ideal_yaw = anglemod(ideal_angle[YAW]); + ideal_pitch = anglemod(ideal_angle[PITCH]); + + // Raptor007: Compensate for M4 climb. + if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) + ideal_pitch -= ent->client->kick_angles[PITCH]; + + // Yaw + if (instant) // Instant turn + { + ent->bot.bi.viewangles[YAW] = ideal_yaw; + } + else + { + // Turn speed based on angle of curr vs ideal + // If the turn angle is large, the bot will instantly snap + // This helps prevent circular motion when very close to a node + yaw_move = ideal_yaw - current_yaw; + if (ideal_yaw > current_yaw) + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + //Com_Printf("%s %s ym:%f\n", __func__, ent->client->pers.netname, yaw_move); + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + //Com_Printf("%s %s c:%f i:%f ym:%f\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, yaw_move); + //if (fabs(yaw_move) >= 45.0) // Snap turn + //{ + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(yaw_move)); + //ent->bot.bi.viewangles[YAW] = ideal_yaw; + //return false; + //} + + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + //Com_Printf("%s %s speed:%f\n", __func__, ent->client->pers.netname, speed); + speed *= (360 / fabs(yaw_move)); // Variable speed. Far away = faster turning. Close = slower turnung. + //Com_Printf("%s %s variable speed:%f\n", __func__, ent->client->pers.netname, speed); + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + } + + + // Pitch + if (instant) // Instant turn + { + ent->bot.bi.viewangles[PITCH] = ideal_pitch; + } + + //if (fabs(pitch_move) >= 45.0) // Snap turn + //{ + //Com_Printf("%s %s snap turn, normal: %f\n", __func__, ent->client->pers.netname, fabs(pitch_move)); + //ent->bot.bi.viewangles[PITCH] = pitch_move; + //return false; + //} + + if (current_pitch != ideal_pitch) + { + pitch_move = ideal_pitch - current_pitch; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + speed *= (360 / fabs(pitch_move)); // Variable speed. Far away = faster turning. Close = slower turnung. + if (ideal_pitch > current_pitch) + { + if (pitch_move >= 180) + pitch_move = pitch_move - 360; + } + else + { + if (pitch_move <= -180) + pitch_move = pitch_move + 360; + } + if (pitch_move > 0) + { + if (pitch_move > speed) + pitch_move = speed; + } + else + { + if (pitch_move < -speed) + pitch_move = -speed; + } + } + + if (instant) // Instant turn + return true; + + /* + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + */ + + //move_ratio = 1; + move_ratio = 10.0 / HZ; + //Com_Printf("%s %s move_ratio[%f]\n", __func__, ent->client->pers.netname, move_ratio); + + //rekkie -- s + if (move_speed < 0.0) + move_speed = 0.0; + if (move_speed > 3.0) + move_speed = 3.0; + //float output = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + //Com_Printf("%s %s move_speed: %f\n", __func__, ent->client->pers.netname, move_speed); + float turn_speed_factor = 1 + move_speed; //2.25; // Slow down turn speed + move_ratio /= turn_speed_factor; // Slow turn speed by a factor of + //rekkie -- e + + if (yaw) + ent->bot.bi.viewangles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + if (pitch) + ent->bot.bi.viewangles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); + + // Check if yaw_move and pitch_move is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw && pitch && yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + { + if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + return true; // Ideal reached + else + return false; + } + if (yaw && !pitch) + { + if (yaw_move < 2 && yaw_move > -2) + return true; // Ideal reached + else + return false; + } + if (!yaw && pitch) + { + if (pitch_move < 2 && pitch_move > -2) + return true; // Ideal reached + else + return false; + } + + return false; +} + +/////////////////////////////////////////////////////////////////////// +// Checks if bot can move (really just checking the ground) +// Also, this is not a real accurate check, but does a +// pretty good job and looks for lava/slime. +/////////////////////////////////////////////////////////////////////// +// Can move safely at angle +qboolean BOTLIB_CanMoveAngle(edict_t* self, vec3_t angle) +{ + vec3_t forward, right; + vec3_t offset, start, end; + trace_t tr; + + // Set up the vectors + AngleVectors(angle, forward, right, NULL); + + VectorSet(offset, 32, 0, 0); + G_ProjectSource(self->s.origin, offset, forward, right, start); + + //VectorSet(offset, 32, 0, -32); // RiEvEr reduced drop distance to -110 NODE_MAX_FALL_HEIGHT + //G_ProjectSource(self->s.origin, offset, forward, right, end); + VectorCopy(start, end); + end[2] -= 32; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction == 1.0) + return false; + + /* + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angle))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; // can't move + } + */ + + return true; // yup, can move +} +// Can move safely in direction +qboolean BOTLIB_CanMoveInDirection(edict_t* self, vec3_t direction, float fwd_dist, float down_dist, qboolean rand_fwd_angle) +{ + trace_t tr; + vec3_t forward, right, start, end, offset, angles; + qboolean safe_forward = false; + qboolean safe_downward = false; + + vectoangles(direction, angles); // Direction to angles + + if (rand_fwd_angle) // Randomize the forward angle + { + angles[0] = 0; + angles[1] += 22.5 + (random() * 270); // Get a random angle + angles[2] = 0; + } + + // Get forward start & end + AngleVectors(angles, forward, right, NULL); // Angles to vectors + VectorSet(offset, 0, 0, self->viewheight); + G_ProjectSource(self->s.origin, offset, forward, right, start); // Get projection + VectorMA(start, fwd_dist, forward, end); // Project forward + + // Test forward + tr = gi.trace(start, tv(-16, -16, -5), tv(-16, -16, 32), end, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.fraction == 1.0 && tr.startsolid == false) // No obstruction + { + safe_forward = true; // Way is safe forward + } + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + // Test downward + VectorCopy(end, start); // Make the end the start + end[2] -= down_dist; // Move the end down + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.fraction < 1.0 && tr.startsolid == false) // Found the ground + { + safe_downward = true; // Way is safe downward + } + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + // Convert angles back to direction + AngleVectors(angles, direction, NULL, NULL); + + if (safe_forward && safe_downward) + return true; + else + return false; +} +// Can move safely in direction +qboolean BOTLIB_CanMoveDir(edict_t* self, vec3_t direction) +{ + vec3_t forward, right; + vec3_t offset, start, end; + vec3_t angles; + trace_t tr; + + // Now check to see if move will move us off an edge + VectorCopy(direction, angles); + VectorNormalize(angles); + vectoangles(angles, angles); + + // Set up the vectors + AngleVectors(angles, forward, right, NULL); + + VectorSet(offset, 36, 0, 24); // 24 + G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorSet(offset, 36, 0, -NODE_MAX_FALL_HEIGHT); //-NODE_MAX_FALL_HEIGHT); // -110 + G_ProjectSource(self->s.origin, offset, forward, right, end); + + tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); // Solid, lava, and slime + + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT + { + return false; // can't move + } + + return true; // yup, can move +} +//rekkie -- Quake3 -- e + +/////////////////////////////////////////////////////////////////////// +// Make the change in angles a little more gradual, not so snappy +// Subtle, but noticeable. +// +// Modified from the original id ChangeYaw code... +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_MOV_ChangeBotAngleYawPitch(edict_t* ent) +{ + float ideal_yaw; + float ideal_pitch; + float current_yaw; + float current_pitch; + float speed; + vec3_t ideal_angle; + float yaw_move = 0.f; + float pitch_move = 0.f; + float move_ratio = 1.f; + + // Normalize the move angle first + VectorNormalize(ent->move_vector); + + current_yaw = anglemod(ent->s.angles[YAW]); + current_pitch = anglemod(ent->s.angles[PITCH]); + + vectoangles(ent->move_vector, ideal_angle); + + ideal_yaw = anglemod(ideal_angle[YAW]); + ideal_pitch = anglemod(ideal_angle[PITCH]); + + // Raptor007: Compensate for M4 climb. + if ((ent->client->weaponstate == WEAPON_FIRING) && (ent->client->weapon == FindItem(M4_NAME))) + ideal_pitch -= ent->client->kick_angles[PITCH]; + + // Yaw + if (current_yaw != ideal_yaw) + { + yaw_move = ideal_yaw - current_yaw; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_yaw > current_yaw) + { + if (yaw_move >= 180) + yaw_move = yaw_move - 360; + } + else + { + if (yaw_move <= -180) + yaw_move = yaw_move + 360; + } + if (yaw_move > 0) + { + if (yaw_move > speed) + yaw_move = speed; + } + else + { + if (yaw_move < -speed) + yaw_move = -speed; + } + } + + // Pitch + if (current_pitch != ideal_pitch) + { + pitch_move = ideal_pitch - current_pitch; + speed = ent->yaw_speed / (float)BASE_FRAMERATE; + if (ideal_pitch > current_pitch) + { + if (pitch_move >= 180) + pitch_move = pitch_move - 360; + } + else + { + if (pitch_move <= -180) + pitch_move = pitch_move + 360; + } + if (pitch_move > 0) + { + if (pitch_move > speed) + pitch_move = speed; + } + else + { + if (pitch_move < -speed) + pitch_move = -speed; + } + } + + // Raptor007: Interpolate towards desired changes at higher fps. + if (!FRAMESYNC) + { + int frames_until_sync = FRAMEDIV - (level.framenum - 1) % FRAMEDIV; + move_ratio = 1.f / (float)frames_until_sync; + } + + ent->s.angles[YAW] = anglemod(current_yaw + yaw_move * move_ratio); + ent->s.angles[PITCH] = anglemod(current_pitch + pitch_move * move_ratio); + + // Check if yaw_move and pitch_move is +/- from ideal_yaw + //Com_Printf("%s: current_yaw %f ideal_yaw %f yaw_move %f\n", __func__, current_yaw, ideal_yaw, yaw_move); + if (yaw_move < 2 && yaw_move > -2 && pitch_move < 2 && pitch_move > -2) + return true; // Ideal yaw reached + else + return false; +} + +/////////////////////////////////////////////////////////////////////// +// Handle special cases of crouch/jump +// +// If the move is resolved here, this function returns true +/////////////////////////////////////////////////////////////////////// +qboolean Botlib_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd) +{ + vec3_t dir, forward, right, start, end, offset; + vec3_t top; + trace_t tr; + + // Get current direction + VectorCopy(self->client->ps.viewangles, dir); + dir[YAW] = self->s.angles[YAW]; + AngleVectors(dir, forward, right, NULL); + + VectorSet(offset, 0, 0, 0); // changed from 18,0,0 + G_ProjectSource(self->s.origin, offset, forward, right, start); + offset[0] += 18; + G_ProjectSource(self->s.origin, offset, forward, right, end); + + + start[2] += 18; // so they are not jumping all the time + end[2] += 18; + tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + + + if (tr.fraction < 1.0) + { + //rekkie -- DEV_1 -- s + // Handles cases where bots get stuck on each other, for example in teamplay where they can block each other + // So lets try to make them jump over each other + if (tr.ent && tr.ent->is_bot && self->groundentity) + { + if (random() > 0.5) // try jumping + { + if (debug_mode && level.framenum % HZ == 0) + debug_printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); + self->velocity[2] += 400; + //ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return true; + } + else // try crouching + { + if (debug_mode && level.framenum % HZ == 0) + debug_printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); + ucmd->upmove = -SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return true; + } + } + //rekkie -- DEV_1 -- e + + // Check for crouching + start[2] -= 14; + end[2] -= 14; + VectorCopy(self->maxs, top); + top[2] = 0.0; // crouching height + tr = gi.trace(start, self->mins, top, end, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + if (debug_mode) debug_printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = -SPEED_RUN; + return true; + } + + // Check for jump + start[2] += 32; + end[2] += 32; + tr = gi.trace(start, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + if (tr.fraction == 1.0) + { + if (debug_mode) debug_printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + return true; + } + } + + return false; // We did not resolve a move here +} + +//rekkie -- Quake3 -- s +// Special cases of crouch/jump when not using nodes +// Also resolves jumping or crouching under other players/bots in our way +// Returns the action taken: ACTION_JUMP, ACTION_CROUCH, ACTION_NONE +int BOTLIB_Crouch_Or_Jump(edict_t* self, usercmd_t* ucmd, vec3_t dir) +{ + vec3_t angle, forward, right, start, end, offset, origin; + trace_t tr; + qboolean crouch = false, head = false, jump = false; // Crouch space, head space, jump space + + // Get current direction + vectoangles(dir, angle); + AngleVectors(angle, forward, right, NULL); + VectorCopy(self->s.origin, origin); + origin[2] -= 24; // From the ground up + + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + + // Check if we hit our head + tr = gi.trace(tv(start[0], start[1], start[2] + 56), tv(-15, -15, 0), tv(15, 15, 1), tv(start[0], start[1], start[2] + 57), self, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + head = true; // Hit our head on something immediately above us + } + + //vec3_t mins = { self->mins[0], self->mins[1], self->mins[2] - 0.15 }; + //vec3_t maxs = { self->maxs[0], self->maxs[1], self->maxs[2] }; + + + tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, 56), end, self, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + + // Handle cases where bots get stuck on each other, for example in teamplay where they can block each other + // So lets try to make them jump or crouch + if (tr.ent && tr.ent->is_bot && self->groundentity) + { + if (random() > 0.5) // try jumping + { + //if (debug_mode && level.framenum % HZ == 0) + // Com_Printf("%s %s: move blocked ----------------------- [[[[[ JUMPING ]]]]]] ---------\n", __func__, self->client->pers.netname); + return ACTION_JUMP; + } + else // try crouching + { + //if (debug_mode && level.framenum % HZ == 0) + // Com_Printf("%s %s: move blocked ----------------------- [[[[[ CROUCH ]]]]]] ---------\n", __func__, self->client->pers.netname); + return ACTION_CROUCH; + } + } + + // Handle geometry cases + // Check for crouching + tr = gi.trace(start, tv(-16, -16, STEPSIZE), tv(16, 16, STEPSIZE), end, self, MASK_PLAYERSOLID); //CROUCHING_MAXS2, 32 -- + if (tr.fraction == 1.0) + { + crouch = true; // We can crouch under something + } + + // Check for jump + tr = gi.trace(start, tv(-16,-16, 60), tv(16, 16, 61), end, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + if (ACEMV_CanMove(self, MOVE_FORWARD)) + jump = true; // We can jump over something + } + + //Com_Printf("%s %s crouch[%d] head[%d] jump[%d]\n", __func__, self->client->pers.netname, crouch, head, jump); + + if ((crouch || head) && jump == false) + { + //Com_Printf("%s %s attempted to crouch under obstacle\n", __func__, self->client->pers.netname); + return ACTION_CROUCH; + } + if (jump && (crouch == false && head == false)) + { + //Com_Printf("%s %s attempted to jump over obstacle\n", __func__, self->client->pers.netname); + return ACTION_JUMP; + } + } + + return ACTION_NONE; // We did not resolve a move here +} + +// Checks the bots movement direction against the direction of the next node. +// +int BOTLIB_DirectionCheck(edict_t *ent, byte *mov_strafe) +{ + if (ent->bot.next_node == INVALID) + return 0; + + // Check velocity + if (VectorEmpty(ent->velocity)) + return 0; + + // Get the bot's direction based from their velocity + vec3_t walkdir; + VectorSubtract(ent->s.origin, ent->lastPosition, walkdir); + VectorNormalize(walkdir); + vec3_t angle, forward, right, start, end, origin, offset; + vectoangles(walkdir, angle); + //VectorCopy(ent->s.angles, angle); // Use the bots's view angles (not their walk direction) + AngleVectors(angle, forward, right, NULL); + + VectorCopy(forward, ent->bot.bot_walk_dir); // Copy the forward walk direction + + + // Calculate the direction from the bot to the node + vec3_t node_dir; + VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, node_dir); + VectorNormalize(node_dir); + + // Calculate the dot product of the forward dir and the node dir + float dot = DotProduct(forward, node_dir); + // Calculate the dot to degrees + //float degrees = acos(dot) * 180 / M_PI; + //Com_Printf("%s node %d dot %f degrees %f\n", __func__, ent->bot.next_node, dot, degrees); + //if (dot > 0.995) + // Com_Printf("%s node %d dot %f\n", __func__, ent->bot.next_node, dot); + + float dot_right = DotProduct(right, node_dir); + VectorNegate(right, right); + float dot_left = DotProduct(right, node_dir); + if (dot_right > dot_left) // + { + //Com_Printf("%s [MOVE RIGHT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + *mov_strafe = 1; + } + else + { + //Com_Printf("%s [MOVE LEFT] node %d dot %f dot_right %f dot_left %f\n", __func__, ent->bot.highlighted_node, dot, dot_right, dot_left); + *mov_strafe = -1; + } + + if (0) // Show visual debug + { + VectorCopy(ent->s.origin, origin); + origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) + + VectorSet(offset, 0, 0, 0); + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1024; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + + // Bot walk direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(ent->s.origin, MULTICAST_PHS); + + /* + // Bot to node direction + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(nodes[ent->bot.next_node].origin); + gi.multicast(ent->s.origin, MULTICAST_PHS); + */ + } + + return dot; +} +//rekkie -- Quake3 -- e + +// Distance types from origin [self, current node, next node] +enum { + BOT_DIST_XYZ_SELF_CURR = 1, // [XYZ] Self -> Current node + BOT_DIST_XYZ_SELF_NEXT, // [XYZ] Self -> Next node + BOT_DIST_XYZ_CURR_NEXT, // [XYZ] Current node -> Next node + + BOT_DIST_XY_SELF_CURR, // [XY] Self -> Current node + BOT_DIST_XY_SELF_NEXT, // [XY] Self -> Next node + BOT_DIST_XY_CURR_NEXT // [XY] Current node -> Next node +}; +// Returns the distance between two points, either as XYZ or XY +// In the case of XY, the vec3_t dist is also returned with the Z value set to 0 +float BOTLIB_UTIL_Get_Distance(edict_t* self, vec3_t dist, int type) +{ + switch (type) + { + // XYZ + case BOT_DIST_XYZ_SELF_CURR: + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + return VectorLength(dist); + case BOT_DIST_XYZ_SELF_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + return VectorLength(dist); + case BOT_DIST_XYZ_CURR_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); + return VectorLength(dist); + + // XY + case BOT_DIST_XY_SELF_CURR: + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + case BOT_DIST_XY_SELF_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + case BOT_DIST_XY_CURR_NEXT: + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, dist); + dist[2] = 0; // Remove the Z component + return VectorLength(dist); + default: + return 0; + } +} + +// Determines the closest point (between curr -> next node) the bot is nearest to +// +// [bot] +// [curr] -----------[*]---------------------------- [next] +// +// [*] is the origin nearest to the bot along the path from [curr] to [next] +// +void BOTLIB_UTIL_NEAREST_PATH_POINT(edict_t* self, usercmd_t* ucmd) +{ + if (self->bot.next_node == INVALID) return; + + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + + // Get distance from current node to next node + vec3_t curr_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); + float curr_to_next_dist = VectorLength(curr_to_next_vec); + + // Calculate the normalized distance the bot has travelled between current and next node + float bot_travelled_dist = 0; + if (bot_to_next_dist < curr_to_next_dist) + bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); + else + bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); + + //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); + + // Get the origin between the current and next node based on the normalized bot_travelled_dist + LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, self->nearest_path_point); +} + +// Determines if we're off course while traversing a path (node1 ---> node2) +// Useful for strafing to get back on course +// +// Returns: '-1' if we're off course to the right (indicating we should strafe left) +// Returns: '0' if we're on course (or close enough) +// Returns: '1' if we're off course to the left (indicating we should strafe right) +int BOTLIB_UTIL_PATH_DEVIATION(edict_t* self, usercmd_t* ucmd) +{ + if (self->bot.next_node == INVALID) return 0; + /* + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + + // Get distance from current node to next node + vec3_t curr_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, curr_to_next_vec); + float curr_to_next_dist = VectorLength(curr_to_next_vec); + + // Calculate the normalized distance the bot has travelled between current and next node + float bot_travelled_dist = 0; + if (bot_to_next_dist < curr_to_next_dist) + bot_travelled_dist = 1 - (bot_to_next_dist / curr_to_next_dist); + else + bot_travelled_dist = 1 - (curr_to_next_dist / bot_to_next_dist); + + + //Com_Printf("%s %s [b->n %f] [c->n %f] [%f]\n", __func__, ent->client->pers.netname, bot_to_next_dist, curr_to_next_dist, bot_travelled_dist); + + // Get the origin between the current and next node based on the normalized bot_travelled_dist + vec3_t normalized_origin; + LerpVector(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, bot_travelled_dist, normalized_origin); + + VectorCopy(normalized_origin, self->nearest_path_point); // Make a copy + */ + + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + + // Show a laser between next node and normalized_origin + if (1) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(nodes[self->bot.next_node].origin); + gi.WritePosition(self->nearest_path_point); + gi.multicast(nodes[self->bot.next_node].origin, MULTICAST_PVS); + } + + // Calculate the angle between the bot's origin and the normalized origin + vec3_t normalized_origin_vec; + VectorSubtract(self->nearest_path_point, self->s.origin, normalized_origin_vec); + normalized_origin_vec[2] = 0; // Flatten the vector + float bot_to_path_dist = VectorLength(normalized_origin_vec); + //Com_Printf("%s %s [d %f]\n", __func__, self->client->pers.netname, bot_to_path_dist); + + // Calculate the angle between the bot's origin and the normalized origin + float current_yaw = anglemod(self->s.angles[YAW]); + vec3_t ideal_angle; + vectoangles(normalized_origin_vec, ideal_angle); + float ideal_yaw = anglemod(ideal_angle[YAW]); + + // print current_yaw and ideal_yaw + //Com_Printf("%s %s [c %f] [i %f]\n", __func__, self->client->pers.netname, current_yaw, ideal_yaw); + + // Figure out if current yaw is closer to idea yaw if we turn left + //const float less_than_90_degrees = 87; // 90 is the optimum angle, but we want to give a little leeway (otherwise the bot's yaw movement will oscillate) + float left_yaw; + float right_yaw; + + if (bot_to_path_dist > 16) // Give a little leeway (otherwise the bot's yaw movement will oscillate) + { + if (ideal_yaw > current_yaw) + { + right_yaw = ideal_yaw - current_yaw; + left_yaw = 360 - right_yaw; + } + else + { + left_yaw = current_yaw - ideal_yaw; + right_yaw = 360 - left_yaw; + } + + //Com_Printf("%s %s [c %f] [i %f] [ry %f] [ly %f] [dist %f]\n", __func__, ent->client->pers.netname, current_yaw, ideal_yaw, right_yaw, left_yaw, bot_to_path_dist); + + if (right_yaw < left_yaw) + { + //Com_Printf("%s %s need to strafe left %f\n", __func__, self->client->pers.netname, left_yaw); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> strafe left %f\n", __func__, self->client->pers.netname, left_yaw); + //ucmd->sidemove = -SPEED_WALK; + return -1; // Strafe left + } + } + else + { + //Com_Printf("%s %s need to strafe right %f\n", __func__, self->client->pers.netname, left_yaw); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> strafe right %f\n", __func__, self->client->pers.netname, right_yaw); + //ucmd->sidemove = SPEED_WALK; + return 1; // Strafe right + } + } + } + + return 0; // Strafe neither left or right +} + +// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) +// Useful for strafing around obstacles while following a path +// +// forward_distance: distance to probe forward +// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) +// +// Returns: 'true' if forward vector is clear of obstructions, +// Returns: 'false' if forward vector is obstructed +qboolean BOTLIB_UTIL_PATH_FORWARD(edict_t* self, usercmd_t* ucmd, float forward_distance, float side_distance) +{ + // Send out trace on the left or right side parallel to the current direction the bot is facing + + vec3_t groundvec, end; + vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; + float forward_back, left_or_right, up_or_down, beam_width; + + groundvec[PITCH] = 0; + groundvec[YAW] = self->client->v_angle[YAW]; + groundvec[ROLL] = 0; + + if (forward_distance == 0) + forward_distance = 64; // Distance to probe forward + + AngleVectors(groundvec, forward, NULL, NULL); // Make a forwards pointing vector out of the bot's view angles + VectorMA(self->s.origin, forward_distance, forward, end); // Make end equal to 60* the forward pointing vector + + beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall + forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward + + // +Right or -Left of the bot's current direction + if (side_distance > 0) // side_distance is greater than zero + left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) + else // side_distance is less than zero + left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) + + // +Up or -Down (from the ground) + up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 + + VectorSet(offset, forward_back, left_or_right, up_or_down); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + + if (1) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return true; // Obstructed + } + + return false; // Clear +} + +//rekkie -- Quake3 -- s +// Determines if forward vector is clear of obstructions while traversing a path (node1 ---> node2) +// Useful for strafing around obstacles while following a path +// +// dir: direction vector +// forward_distance: distance to probe forward +// side_distance: distance to probe left or right ( less than 0 = left, greater than 0 = right ) +// +// Returns: 'true' if forward vector is clear of obstructions, +// Returns: 'false' if forward vector is obstructed +qboolean BOTLIB_TEST_FORWARD_VEC(edict_t* self, vec3_t dir, float forward_distance, float side_distance) +{ + // Send out trace on the left or right side parallel to the current direction the bot is facing + + vec3_t groundvec; + vec3_t offset, forward, right, start, down_left_end; // , down_right_end, up_left_end, up_right_end; + float forward_back, left_or_right, up_or_down, beam_width; + + if (forward_distance == 0) + forward_distance = 64; // Distance to probe forward + + beam_width = 2; // The actual width is '4' (found in CL_ParseLaser() tent.c) - but slightly reduced it here because it's easier to see when it intersects with a wall + forward_back = self->maxs[PITCH] + beam_width; // + forward, - backward + + // +Right or -Left of the bot's current direction + if (side_distance > 0) // side_distance is greater than zero + left_or_right = self->maxs[YAW] + side_distance; // (+maxs) + (-side_distance) ( right is a positive number ) + else // side_distance is less than zero + left_or_right = self->mins[YAW] + side_distance; // (-mins) + (-side_distance) ( left is a negative number ) + // +Up or -Down (from the ground) + up_or_down = self->mins[ROLL] + STEPSIZE; // (-mins) + (+STEPSIZE) -- Player bounding box (-32 + 18) = -14 + + VectorSet(offset, forward_back, left_or_right, up_or_down); + vectoangles(dir, groundvec); + AngleVectors(groundvec, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, forward_distance, forward, down_left_end); // Make end equal to 200* the forward pointing vector + + if (0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(down_left_end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + VectorMA(start, (forward_distance - 16), forward, down_left_end); // Move the end point back -16 units (mins[0]) + trace_t tr = gi.trace(start, tv(-4, -4, STEPSIZE+0.01), tv(4, 4, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, tv(-12, -12, 0.1), tv(12, 12, 56), down_left_end, self, MASK_MONSTERSOLID); + //trace_t tr = gi.trace(start, NULL, NULL, down_left_end, self, MASK_MONSTERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return true; // Obstructed + } + + return false; // Clear +} +//rekkie -- Quake3 -- e + +/////////////////////////////////////////////////////////////////////// +// Bot Movement (based on node type) +/////////////////////////////////////////////////////////////////////// +void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) +{ +#if 0 + trace_t tr; + float distance = 0; + //float distance_xy; // Distance XYZ and Distance XY + vec3_t dist = { 0 }; + //vec3_t dist_xy = { 0 }; // Distance XYZ and Distance XY + vec3_t velocity; + int i = 0, current_node_type = INVALID, next_node_type = INVALID; + + + + + // Do not follow path when teammates are still inside us. + if (OnTransparentList(self)) + { + //rekkie -- DEV_1 -- s + + // If the bot has just spawned, then we need to wait a bit before we start moving. + if (self->just_spawned) // set by SpawnPlayers() in a_team.c + { + self->just_spawned = false; + self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. + self->bot.state = BOT_MOVE_STATE_MOVE; + if (random() < 0.3) + self->just_spawned_timeout = level.framenum + (random() * 7) * HZ; // Long wait + else if (random() < 0.7) + self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Average wait + else + self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait + return; + } + if (self->just_spawned_go && self->just_spawned_timeout > level.framenum) // Wait + { + return; // It's not time to move yet, wait! + } + else if (self->just_spawned_go) + { + // Now we can move! + self->just_spawned_go = false; + //ACEAI_PickLongRangeGoal(self); // Lets pick a long range goal + return; + } + if (0) // Don't wander + { + //rekkie -- DEV_1 -- e + + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + + //rekkie -- DEV_1 -- s + } + //rekkie -- DEV_1 -- e + } + + // Get current and next node back from nav code. + if (!BOTLIB_FollowPath(self)) + { + if (!teamplay->value || (teamplay->value && level.framenum >= self->teamPauseTime)) + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + else + { + // Teamplay mode - just fan out and chill + if (self->bot.state == STATE_FLEE) + { + self->bot.state = STATE_POSITION; + self->wander_timeout = level.framenum + 3.0 * HZ; + } + else + { + self->bot.state = STATE_POSITION; + self->wander_timeout = level.framenum + 1.0 * HZ; + } + } + self->bot.goal_node = INVALID; + return; + } + + if (self->bot.current_node == INVALID || self->bot.next_node == INVALID) // rekkie -- safey check because current / next node can be INVALID + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + + + + // Current node type + current_node_type = nodes[self->bot.current_node].type; + + // Next node type + // We now get the next node type from the link between current and next node + // Using the link from the current node to the next node, find the next node type + int targetNodeLink = INVALID; // Used to keep track of which link to upgrade to a jumppad + if (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN) + { + //targetNodeLink = 1; + next_node_type = nodes[self->bot.next_node].type; + } + else + { + for (i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + if (nodes[self->bot.current_node].links[i].targetNode == self->bot.next_node) + { + targetNodeLink = i; + next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + + self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type + self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + break; + } + } + } + + //if (next_node_type == NODE_JUMP) + // Com_Printf("%s %s [cn:%i] JUMP_NODE [nn:%i]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node); + + + //rekkie -- DEV_1 -- s + //if (current_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMPPAD) + // gi.dprintf("%s %s [ct:%i] [nt:%i]\n", __func__, self->client->pers.netname, current_node_type, next_node_type); + + // If the next node is high enough, make it a jump + /* + if (next_node_type == NODE_MOVE && self->client->leg_damage == 0) + { + + // See if there is a gap / hole between the current and next node, if so we need to jump across + vec3_t pt_25, pt_50, pt_75; + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_25); // 25% + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_50); // 50% + VectorAdd(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, pt_75); // 75% + VectorScale(pt_25, 0.25, pt_25); // Scale 25% + VectorScale(pt_50, 0.50, pt_50); // Scale 50% + VectorScale(pt_75, 0.75, pt_75); // Scale 75% + // Using the points between the current and next node, trace a line down and see if one of them hits air + trace_t tr_25, tr_50, tr_75; + vec3_t end_25, end_50, end_75; + VectorCopy(pt_25, end_25); + VectorCopy(pt_50, end_50); + VectorCopy(pt_75, end_75); + end_25[2] -= NODE_MAX_JUMP_HEIGHT; + end_50[2] -= NODE_MAX_JUMP_HEIGHT; + end_75[2] -= NODE_MAX_JUMP_HEIGHT; + tr_25 = gi.trace(pt_25, vec3_origin, vec3_origin, end_25, self, MASK_SOLID); + tr_50 = gi.trace(pt_50, vec3_origin, vec3_origin, end_50, self, MASK_SOLID); + tr_75 = gi.trace(pt_75, vec3_origin, vec3_origin, end_75, self, MASK_SOLID); + // If one of our trace points hit nothing + if ( (tr_25.fraction == 1.0 && tr_25.allsolid == false) || + (tr_50.fraction == 1.0 && tr_50.allsolid == false) || + (tr_75.fraction == 1.0 && tr_75.allsolid == false) ) + { + next_node_type = NODE_JUMPPAD; + } + + // Otherwise lets see if the next_node is higher than current_node, if so jump to it + //else + { + // Distance between current and next node. + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + + // Calculate if nodes[self->bot.next_node].origin[2] is higher than self->s.origin[2] + float higher = 0; float lower = 0; + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) + higher = (nodes[self->bot.next_node].origin[2] - nodes[self->bot.current_node].origin[2]); + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + lower = (nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]); + //if (distance >= 128 || higher >= NODE_MAX_JUMP_HEIGHT || lower >= NODE_MAX_JUMP_HEIGHT) + //if (higher >= NODE_MAX_JUMP_HEIGHT) // || lower >= NODE_MAX_CROUCH_FALL_HEIGHT) + if (higher) // || lower) + { + next_node_type = NODE_JUMPPAD; + } + } + } + */ + + if (current_node_type == INVALID || next_node_type == INVALID) + { + Com_Printf("%s curr[%d]type[%d] next[%d]type[%d] Goal[%d] State[%d]\n", __func__, self->bot.current_node, current_node_type, self->bot.next_node, next_node_type, self->bot.goal_node, self->bot.state); + } + + + // DEBUG: print current and next node types + //if (1) + { + //const qboolean printOnlyErrors = true; // If only printing INVALID nodes + //PrintNodeType(self, current_node_type, next_node_type, printOnlyErrors); + } + + + + + + + // Contents check + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] += 22; + int contents_head = gi.pointcontents(temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + // If in water, force bot to wander... + if (contents_feet & MASK_WATER) + { + self->bot.state = STATE_WANDER; + return; + //self->bot.goal_node = INVALID; + //self->bot.current_node = INVALID; + //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); + } + + + + //if (next_node_type == INVALID && self->bot.goal_node == INVALID) + if (next_node_type == INVALID) //self->bot.current_node == self->bot.next_node == self->bot.goal_node) + { + //next_node_type = NODE_MOVE; + + /* + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && M_CheckBottom(self, 2)) + { + if (ACEMV_CanMove(self, MOVE_FORWARD)) + { + ucmd->forwardmove = SPEED_WALK * sqrtf(xy_dist_self_curr / 64); + return; + } + //else if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -64; + //else if (ACEMV_CanMove(self, MOVE_RIGHT)) + // ucmd->sidemove = 64; + //else if (ACEMV_CanMove(self, MOVE_LEFT)) + // ucmd->sidemove = -64; + } + */ + } + + + // + //if (current_node_type == NODE_JUMPPAD && self->last_touched_ground > 0) + //{ + //ucmd->forwardmove = 0; + //VectorClear(self->velocity); + //return; + //} + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_CURR_NEXT); + ////VectorSubtract(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin, dist_xy); + ////dist_xy[2] = 0; // Drop height component + ////distance_xy = VectorLength(dist_xy); + + // Strafe jumping + // Vary it up by doing JUMPPAD 'strafe jumping' in place of MOVE to the target if far enough away + if (0) + { + if (next_node_type == NODE_MOVE && abs(nodes[self->bot.current_node].origin[2] - nodes[self->bot.next_node].origin[2]) < STEPSIZE && distance > 256) + { + if (self->strafe_jumps > 30) // Allow jumps to be missed + self->strafe_jumps = 0; // Reset + + if (self->strafe_jumps < 10) // Mutiple jumps in series (although not contigous) + { + next_node_type = NODE_JUMPPAD; + } + + self->strafe_jumps++; // Count strafe jumps + } + } + + //if (random() < 0.01) + //{ + //self->client->leg_damage = 1; // DEBUG: force leg damage + //Com_Printf("%s %s leg damage\n", __func__, self->client->pers.netname); + //} + + // If we have leg damage, prevent jumping + // Look for a nearby move node to take cover + if (self->client->leg_damage > 0) // Do we have leg damage? + { + next_node_type = NODE_MOVE; + + /* + if (next_node_type != NODE_MOVE) + { + qboolean foundMoveNode = false; + + // Search for a nearby move node + for (i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_MOVE) + { + // Check if any enemies can see this node + qboolean canEnemiesSeeNextNode = false; + for (int p = 0; p < num_players; p++) // Cycle based on how many players we find + { + if (ACEAI_IsEnemy(self, players[p])) // Are they our enemy + { + // trace line to see if enemy is visible from the targetNode + int targetNode = nodes[self->bot.current_node].links[i].targetNode; + tr = gi.trace(nodes[targetNode].origin, NULL, NULL, players[p]->s.origin, self, MASK_ALL); + //if (tr.ent) + // Com_Printf("%s %s found ent [%s]\n", __func__, self->client->pers.netname, tr.ent->classname); + if (tr.ent == players[p]) // Enemy can see this node + { + //Com_Printf("%s %s found MOVE node [%d] but enemy [%s] can see it\n", __func__, self->client->pers.netname, targetNode, players[p]->client->pers.netname); + canEnemiesSeeNextNode = true; + break; + } + } + } + + if (canEnemiesSeeNextNode) // Enemy can see this node, so skip it + continue; + else // No enemies can see this node, so use it + { + self->bot.next_node = nodes[self->bot.current_node].links[i].targetNode; + next_node_type = NODE_MOVE; + foundMoveNode = true; + Com_Printf("%s %s found MOVE node for cover [%d]\n", __func__, self->client->pers.netname, self->bot.next_node); + break; + } + } + } + + // Otherwise wander + if (foundMoveNode == false) + { + Com_Printf("%s %s failed to find MOVE node, wandering...\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.time + 5.0; + return; + } + } + */ + } + //return; + + + //if ((current_node_type == NODE_LADDER || next_node_type == NODE_LADDER) && OnLadder(self) == false) + // next_node_type = NODE_JUMPPAD; + //rekkie -- DEV_1 -- e + + /////////////////////// + // Grenade Avoidance // + /////////////////////// + // Try to avoid hand grenades by strafing, backing away, and crouching + edict_t* target = findradius(NULL, self->s.origin, 512); + while (target) + { + if (target->classname == NULL) + return; + + if (strcmp(target->classname, "hgrenade") == 0) + { + //Com_Printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); + if (debug_mode) debug_printf("%s %s attempted to avoid a grenade \n", __func__, self->client->pers.netname); + + VectorSubtract(target->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + + // Try strafing + if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else + ucmd->sidemove = 0; + + self->bot_strafe = ucmd->sidemove; + + // Also try move backwards + if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + + // And crouch if its very close + VectorSubtract(target->s.origin, self->s.origin, dist); + dist[2] = 0; + if (VectorLength(dist) < 192) + ucmd->upmove = -SPEED_WALK; + + return; + } + + target = findradius(target, self->s.origin, 512); + } + + //////////////////// + // Stay on course // + //////////////////// + /* + if (self->bot.next_node != self->bot.current_node && current_node_type == NODE_MOVE && next_node_type == NODE_MOVE) + { + // Decide if the bot should strafe to stay on course. + vec3_t v = { 0,0,0 }; + VectorSubtract(nodes[self->bot.next_node].origin, nodes[self->bot.current_node].origin, v); + v[2] = 0; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + dist[2] = 0; + if ((DotProduct(v, dist) > 0) && (VectorLength(v) > 16)) + { + float left = 0; + VectorNormalize(v); + VectorRotate2(v, 90); + left = DotProduct(v, dist); + if ((left > 16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_RIGHT))) + ucmd->sidemove = SPEED_RUN; + else if ((left < -16) && (!self->groundentity || ACEMV_CanMove(self, MOVE_LEFT))) + ucmd->sidemove = -SPEED_RUN; + } + } + */ + + + + + + ////////////////////// + // Object Avoidance // + ////////////////////// + + // Calculate the players current speed and direction + float forward_or_back_velocity = 0; + float left_or_right_velocity = 0; + if (1) + { + vec3_t velocity; + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + float speed = VectorLength(velocity); + + vec3_t angles; + VectorCopy(self->client->v_angle, angles); + angles[0] = 0; + angles[2] = 0; + + vec3_t forward, right; + AngleVectors(angles, forward, right, NULL); + + forward_or_back_velocity = DotProduct(forward, velocity); + left_or_right_velocity = DotProduct(right, velocity); + //Com_Printf("%s: speed %f forward_or_back_velocity %f left_or_right_velocity %f\n", __func__, speed, forward_or_back_velocity, left_or_right_velocity); + } + + // Check to see if stuck, and if so try to free us. Also handles crouching + VectorSubtract(self->s.origin, self->lastPosition, dist); + if (self->groundentity && forward_or_back_velocity > -30 && forward_or_back_velocity < 30) + { + if (Botlib_Crouch_Or_Jump(self, ucmd)) + { + Com_Printf("%s %s attempted to crouch or jump \n", __func__, self->client->pers.netname); + return; + } + + // Check if we are obstructed by an oncoming teammate, and if so strafe + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) + { + if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + } + } + + /* + // Check to see if stuck, and if so try to free us. Also handles crouching + VectorSubtract(self->s.origin, self->lastPosition, dist); + if ((VectorLength(self->velocity) < 37) || (VectorLength(dist) < FRAMETIME)) + { + if (self->groundentity) + { + //if (random() > 0.5 && + if (Botlib_Crouch_Or_Jump(self, ucmd)) + return; + + // Check if we are obstructed by an oncoming teammate, and if so strafe + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (DotProduct(self->velocity, tr.ent->velocity) <= 0)) + { + if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + ACEMV_ChangeBotAngle(self); + return; + } + } + } + */ + /* + /////////////////////// + // corner management // + /////////////////////// + //RiEvEr -- My corner management code + if (BOTCOL_CheckBump(self, ucmd)) + { + if (BOTCOL_CanStrafeSafely(self, self->s.angles)) + { + ucmd->sidemove = self->bot_strafe; + return true; + } + } + //R + */ + + /* + float botspeed = VectorLength(self->velocity); + if (botspeed == 0 && self->last_touched_ground == 0) + { + Com_Printf("%s %s is stuck: STATE_WANDER\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + */ + /* + // Check if we're standing on top of another player + // OR touching another player + if (VectorLength(self->velocity) <= 32) // && self->last_touched_ground > 0) + { + //tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_DEADSOLID); + tr = gi.trace(self->s.origin, tv(-32, -32, -16), tv(32, 32, 24), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2]), self, MASK_DEADSOLID); + if (tr.ent && tr.ent->client && tr.ent->health > 0) + { + Com_Printf("%s %s is stuck on %s: wandering\n", __func__, self->client->pers.netname, tr.ent->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + return; + } + if (tr.ent && tr.ent->classname) + { + Com_Printf("%s %s is touching: %s\n", __func__, self->client->pers.netname, tr.ent->classname); + } + } + */ + + + // Grab items along the way, if they're not already our goal item to fetch + if (self->movetarget && self->movetarget->inuse && self->movetarget != self->goalentity) + { + // Try to navigate to the item + /* + int item_node = ACEND_FindClosestReachableNode(self->goalentity, NODE_DENSITY, NODE_ALL); + if (item_node != INVALID) + { + if (self->bot.prev_node != self->bot.goal_node) + self->bot.prev_node = self->bot.goal_node; // Keep a copy of previous goal + self->bot.goal_node = item_node; // Make the item node our new goal + + // Try visit node near item + // Otherwise visit previous goal node + if (BOTLIB_CanVisitNode(self, self->bot.goal_node) == false) // If we cannot visit item node + { + self->bot.goal_node = self->bot.prev_node; // Restore previous goal + BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal + } + } + */ + + // When we're near the item we desire, jump to it + vec3_t item_vec; + VectorSubtract(self->s.origin, self->movetarget->s.origin, item_vec); + float dist = VectorLength(item_vec); + if (dist < 256) + { + //if (self->movetarget->inuse == false) self->movetarget = NULL; //ignore freed ents + + if (dist < 32) + { + //Com_Printf("%s %s successfully got short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); + self->movetarget = NULL; + //self->bot.goal_node = self->bot.prev_node; // Restore previous goal + //BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal + return; + } + else// if (ACEIT_IsReachable(self, self->movetarget->s.origin)) + { + //Com_Printf("%s %s heading for short range goal: %s\n", __func__, self->client->pers.netname, self->movetarget->classname); + + VectorSubtract(self->movetarget->s.origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + //ucmd->forwardmove = SPEED_RUN; + if (self->groundentity) + { + VectorClear(self->velocity); + LaunchPlayer(self, self->movetarget->s.origin); + self->movetarget = NULL; + //self->bot.state = STATE_WANDER; + return; + } + //return; + } + } + } + + + + + if (next_node_type == NODE_CROUCH) + { + // Make sure we're facing the target + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + // Crouch move forward + ucmd->upmove = -SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + return; + } + + //////////////////////////////////////////////////////// + // Jumpad Nodes + /////////////////////////////////////////////////////// + if (next_node_type == NODE_JUMPPAD) + { + + //if (self->groundentity && self->velocity[2] <= 0) + //{ + // VectorClear(self->velocity); + // LaunchPlayer(self, nodes[self->bot.next_node].origin); + // return; + //} + + /* + vec3_t dist; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + float next_node_distance = VectorLength(dist); + + dist[2] = 0; // Drop height component + float next_node_distance_xy = VectorLength(dist); + + vec3_t curr_node_dist; + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, curr_node_dist); + float curr_node_distance = VectorLength(curr_node_dist); + */ + + + /* + // If the bot's distance is moving away from the target (failed the jump) + // Try to find another route, otherwise find a new node and go there instead + if (self->prev_next_node == self->bot.next_node) + { + if (next_node_distance < self->next_node_distance) // Update if getting closer + self->next_node_distance = next_node_distance; + else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump + { + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->bot.goal_node); + } + else + { + Com_Printf("%s %s failed to jumppad up, finding new goal\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + } + return; + } + } + if (self->prev_next_node != self->bot.next_node) // Update previous next node and distance + { + self->prev_next_node = self->bot.next_node; + self->next_node_distance = next_node_distance; + } + */ + /* + // If the bot fell below the target (failed the jump down) + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node by moving its origin higher + if (targetNodeLink != INVALID) + { + //if (debug_mode) + Com_Printf("%s %s dropped from a jumppad forward, moving jumppad node higher\n", __func__, self->client->pers.netname); + int n = nodes[self->bot.current_node].links[targetNodeLink].targetNode; + nodes[n].origin[2]++; + } + } + */ + + + + + + /* + // Bot fell below current and next target + //if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.current_node].origin[2]) + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); + if (self->last_touched_ground == 0 && distance > 96) + { + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMPPAD][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s (1) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + */ + + + + + + // See if we need a ladder jump boost + float speed = VectorLength(self->velocity); + if (self->bot.current_node == NODE_LADDER_UP && speed < 32 && self->last_touched_ground > 0) + { + //if (debug_mode) + // Com_Printf("%s %s is stuck [NODE_JUMPPAD]...\n", __func__, self->client->pers.netname); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: jumping\n", __func__, self->client->pers.netname); + } + else + { + ucmd->upmove = -400; // Try crouching + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]: crouching\n", __func__, self->client->pers.netname); + } + } + if (speed == 0 && self->last_touched_ground == 0) // We're not moving at all + { + //Com_Printf("%s %s is stuck [NODE_JUMPPAD]... speed: 0\n", __func__, self->client->pers.netname); + ucmd->forwardmove = -50; // Try backing up a tad, then try jumping again + } + + + + + + + + //* + // Guide the bot in when close to the target + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ///distance = VectorLength(dist); + vec3_t dist_xy; + float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + /* + if (0) + if (distance <= 50 || distance_xy < 32 || VectorLength(self->velocity) < 32)// && next_node_type != NODE_LADDER_UP) //32 + { + //self->s.angles[PITCH] = 0; // Look directly forward + + // When very close to the next node change yaw angle only + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + BOTLIB_MOV_ChangeBotAngleYawPitch(self); + + //Com_Printf("%s %s jumppad self->last_touched_ground = %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + + //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0) + { + float speed = VectorLength(self->velocity); + //if (debug_mode) + // Com_Printf("%s guiding the bot in from jump [speed %f]\n", __func__, speed); + + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + + ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. + + // Apply it + //if (!ACEMV_CanMove(self, MOVE_FORWARD)) + // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down + //else + // VectorScale(dist, 128, self->velocity); + //VectorScale(dist, 224, self->velocity); + + + //vec3_t dist_xy; + //float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist_xy, BOT_DIST_XY_SELF_NEXT); + //Com_Printf("%s %s distance_xy %f\n", __func__, self->client->pers.netname, distance_xy); + if (distance_xy > 16) + { + if (self->groundentity || ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop + { + // touching the ground but on an edge with a drop - slow down + if (ACEMV_CanMove(self, MOVE_FORWARD) == false) + { + //ucmd->forwardmove = -1; + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + Com_Printf("%s %s jumppad (1)\n", __func__, self->client->pers.netname); + } + else // touching the ground - safe to move at a normal pace + { + VectorScale(dist, SPEED_RUN, self->velocity); + ucmd->forwardmove = SPEED_RUN; + Com_Printf("%s %s jumppad (2)\n", __func__, self->client->pers.netname); + } + } + else if (distance <= 16) // Ensure we slow right down if needed + { + //VectorScale(dist, max((SPEED_WALK * sqrtf(distance / 128)), 32), self->velocity); // Slow down (min speed 32) + + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + Com_Printf("%s %s jumppad (3)\n", __func__, self->client->pers.netname); + } + else + { + //ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 256); + //VectorScale(dist, 224, self->velocity); + Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); + } + + if (self->s.origin[2] > nodes[self->bot.next_node].origin[2]) + self->velocity[2] -= 32; + + // If we're now touching the ground, just move at a normal pace + //if (self->last_touched_ground == 0)// && self->s.origin[2] == nodes[self->bot.next_node].origin[2]) + { + //Com_Printf("%s %s jumppad (4)\n", __func__, self->client->pers.netname); + //VectorScale(dist, SPEED_RUN, self->velocity); + //ucmd->forwardmove = SPEED_RUN; + } + + } + else if (distance_xy <= 16 && self->s.origin[2] > nodes[self->bot.next_node].origin[2]) + { + Com_Printf("%s %s jumppad (5a)\n", __func__, self->client->pers.netname); + ucmd->forwardmove = 1; + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] -= 32; + //return; + + if (self->groundentity) + { + Com_Printf("%s %s jumppad (5b)\n", __func__, self->client->pers.netname); + //ucmd->forwardmove = 128 * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + } + } + //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + //else if (distance_xy <= 16 && self->s.origin[2] < nodes[self->bot.next_node].origin[2] && !self->groundentity) + else if (distance_xy <= 16 && (self->s.origin[2] + NODE_Z_HALF_HEIGHT) < nodes[self->bot.next_node].origin[2]) + { + Com_Printf("%s %s jumppad (6)\n", __func__, self->client->pers.netname); + + //float z_height_diff = fabs(nodes[self->bot.next_node].origin[2]) - fabs(self->s.origin[2]); + //if (z_height_diff < NODE_MAX_JUMP_HEIGHT) + { + // Try jumping + //if (debug_mode) + // Com_Printf("%s %s failed to jumppad. Trying to jump next node %d\n", __func__, self->client->pers.netname, self->bot.next_node); + //ucmd->upmove = SPEED_RUN; + //ucmd->forwardmove = SPEED_RUN; + //return; + //if (Botlib_Crouch_Or_Jump(self, ucmd)) + // return; + } + //return; + + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMPPAD][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for node %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s (2) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + else + { + //if (BOTLIB_MOV_ChangeBotAngleYawPitch(self)) + if (self->groundentity) + { + Com_Printf("%s %s jumppad (7)\n", __func__, self->client->pers.netname); + //ucmd->forwardmove = 128 * sqrtf(distance / 256); + VectorScale(dist, 128, self->velocity); + } + } + + + // See if we're stuck, try to unstick + if (speed < 32 && self->last_touched_ground > 0) + { + if (debug_mode) + Com_Printf("%s correcting jump, unsticking\n", __func__); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + } + else + ucmd->upmove = -400; // Try crouching + } + + return; + } + } + */ + /* + else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) + { + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + next_node_distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + + return; + } + } + */ + + + float gravity = self->gravity * sv_gravity->value; + + ACEMV_ChangeBotAngle(self); + + float boost_jump = 1; // Boost jump in rare instances + + /* + if (next_node_distance_xy > 64) + ucmd->forwardmove = SPEED_RUN; // Move forward + else if (self->groundentity && next_node_distance_xy < 64) + { + // Try alternative route + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + */ + /* + else if (nodes[self->bot.next_node].origin[2] > self->s.origin[2] + 96) // It's fairly high above the player (and not a step) + { + // Check we've reach our ideal yaw angle + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + // Check we can move backward safely + if (ACEMV_CanMove(self, MOVE_BACK)) + { + ucmd->forwardmove = -SPEED_RUN; // Move back a bit + return; + } + else + { + boost_jump = 2; // Could not move back, so cheat and boost jump + } + } + */ + + //if (self->velocity[0] || self->velocity[1] || self->velocity[2]) + //{ + // VectorClear(self->velocity); // Zero current velocity to kill any momentum + // return; + //} + + //if (self->groundentity || next_node_distance < NODE_Z_HEIGHT) + //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near + // distance between curr_node and self->bot.current_node + + //Com_Printf("%s %s can jumppad? last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + + // Slightly oversized player standing box ( +2 on sides, +8 bottom, but not top ) + //vec3_t expanded_player_standing_mins = { -18, -18, -32 }; // default: -16, -16, -24 + //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 + // Figure out if player is touching against a wall or floor + //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); + + //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance < 24)) // && curr_node == self->bot.current_node)) + //if (self->groundentity || curr_node_distance < 24 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) + //if (self->groundentity || curr_node_distance <= 8 || (current_node_type == NODE_LADDER_UP && curr_node_distance <= 24)) // && curr_node == self->bot.current_node)) + //if (self->last_touched_ground == 0) + if (self->last_jumppad_node != self->bot.current_node) // && self->velocity[2] > -200) // apply jumppad if we're on a different current_node + //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) + //if (self->last_touched_ground < 10) + //if (self->groundentity) + //if (self->groundentity || tr.fraction < 1.0 || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 48) // touching ground + //if (self->groundentity || tr.fraction < 1.0) // touching ground + //if (self->groundentity || BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32) // touching ground + //if (self->groundentity || (self->velocity[2] <= 0 && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) <= 32)) + //if ( (self->groundentity || tr.fraction < 1.0) && BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_CURR) <= 32) + { + // Skip doing a jumpnode... if the node we jumped from (current_node) changed. + // This occurs when a jump is made and fails to reach the next_node; because the current node is updated just before landing (in the wrong spot) + //if (self->last_jumppad_node == self->bot.prev_node && self->last_touched_ground > 2) + //{ + //Com_Printf("%s %s jumppad failed to reach target, self->last_touched_ground %d\n", __func__, self->client->pers.netname, self->last_touched_ground); + // self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from + // return; + //} + + // Slightly oversized player standing box ( +2 on sides, +2 bottom, but not top ) + //vec3_t expanded_player_standing_mins = { -18, -18, -26 }; // default: -16, -16, -24 + //vec3_t expanded_player_standing_maxs = { 18, 18, 32 }; // default: 16, 16, 32 + // Figure out if player is touching against a wall or floor + //tr = gi.trace(self->s.origin, expanded_player_standing_mins, expanded_player_standing_maxs, self->s.origin, self, MASK_DEADSOLID); + //if (tr.fraction == 1.0) // is the player in the air? + // return; // bug out + + //Com_Printf("%s %s applied jumppad %f\n", __func__, self->client->pers.netname, level.time); + + VectorClear(self->velocity); // Zero current velocity to kill any momentum + self->last_jumppad_node = self->bot.current_node; // update node we last conducted a jumppad from + + // Check we've reach our ideal yaw angle and we're not falling + //if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) // || self->velocity[2] < 0) + // return; + + if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) + { + //VectorClear(self->velocity); // Zero velocity + ucmd->forwardmove = 1; + //return; + } + + + // Check if we're off course for a jump + vec3_t chk_dist; + float chk_distance = BOTLIB_UTIL_Get_Distance(self, chk_dist, BOT_DIST_XYZ_SELF_CURR); + if (chk_distance > 128) + { + //if (debug_mode) + // Com_Printf("%s %s correcting jump that is off course\n", __func__, self->client->pers.netname); + + // Change the bot's vector to face the previous node + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + + // Face previous node + if (BOTLIB_MOV_ChangeBotAngleYawPitch(self) == false) + { + //VectorClear(self->velocity); // Zero velocity + ucmd->forwardmove = 1; + } + + // Return the bot's movement vector back to the target + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + } + + + + + float z_velocity = self->velocity[2]; + if (z_velocity < 0) + z_velocity = (z_velocity - (z_velocity * 2)); // Make it positive + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + + //Com_Printf("%s %s jump_height: %f\n", __func__, self->client->pers.netname, jump_height/16); + + // Calculate the time it will take to get to the target + float time = distance / (z_velocity + jump_height / 2.0); + + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2])//self->s.origin[2]) + { + z_height = ((nodes[self->bot.next_node].origin[2] - self->s.origin[2]) * boost_jump); + z_height += sqrtf(distance) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + { + z_height = (self->s.origin[2] - nodes[self->bot.next_node].origin[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // If self->velocity[2] is minus, make it positive and add it to the required velocity + if (self->velocity[2] < 0 && self->groundentity == NULL) + velocity[2] += -self->velocity[2]; + + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + + // Limit max speed + //if (speed < 750 && z_height < 155 && jump_height <= 965) // current_node_type == NODE_LADDER + { + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, self->velocity); + VectorCopy(velocity, self->client->oldvelocity); + VectorCopy(velocity, self->avelocity); + + return; + } + } + else + { + next_node_type = NODE_MOVE; + } + + /* + // Guide the bot in when close to the target + else if (next_node_distance <= 128) // || (next_node_distance < 96 && (nodes[self->bot.next_node].type == NODE_LADDER_UP || nodes[self->bot.next_node].type == NODE_LADDER_DOWN))) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + next_node_distance = VectorLength(dist); // Get the absolute length + + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + } + } + */ + + + + //return; + } + + //////////////////////////////////////////////////////// + // Jump Node + /////////////////////////////////////////////////////// + if (next_node_type == NODE_JUMP) + { + + // If the bot fell below the target (failed the jump) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node type to jumppad + if (targetNodeLink != INVALID) + { + if (debug_mode) + Com_Printf("%s %s fell from a jump node, upgrading node to jumppad\n", __func__, self->client->pers.netname); + nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; + + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + //Com_Printf("%s %s [NODE_JUMP][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + return; + } + else + { + if (debug_mode) + Com_Printf("%s %s failed to jump node to goal %d because targetNodeLink was invalid, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; // Aquire a new node + return; + } + } + + // See if we're stuck, try to unstick + float speed = VectorLength(self->velocity); + if (speed < 32 && self->last_touched_ground == 0) + { + //if (debug_mode) + // Com_Printf("%s %s is stuck, trying to resolve\n", __func__, self->client->pers.netname); + + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + self->velocity[2] = 200; + //Com_Printf("%s %s is stuck [NODE_JUMP]: jumping\n", __func__, self->client->pers.netname); + } + else + { + ucmd->upmove = -400; // Try crouching + //Com_Printf("%s %s is stuck [NODE_JUMP]: crouching\n", __func__, self->client->pers.netname); + } + } + + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + // Lose the vertical component + ////dist[2] = 0; + // Get the absolute length + ////distance = VectorLength(dist); + + //if (ACEMV_CanJumpInternal(self, MOVE_FORWARD)) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + if (DotProduct(velocity, dist) > 0.8) + ucmd->upmove = SPEED_RUN; + + //Kill running movement +// self->move_vector[0]=0; +// self->move_vector[1]=0; +// self->move_vector[2]=0; + // Set up a jump move + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + + //self->move_vector[2] *= 2; + + ACEMV_ChangeBotAngle(self); + + //rekkie -- DEV_1 -- s + // Guide the bot in when close to the target + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////distance = VectorLength(dist); + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + if (distance <= 64) + { + tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction > 0.7) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + } + //rekkie -- DEV_1 -- e + + /* + // RiEvEr - re-instate the velocity hack + if (self->jumphack_timeout < level.framenum) + { + VectorCopy(self->move_vector,dist); + if ((440 * distance / 128) < 440) + VectorScale(dist,(440 * distance / 128),self->velocity); + else + VectorScale(dist,440,self->velocity); + self->jumphack_timeout = level.framenum + 3.0 * HZ; + } + */ + } + /* + else + { + self->bot.goal_node = INVALID; + } + */ + return; + } + + //////////////////////////////////////////////////////// + // Ladder Nodes + /////////////////////////////////////////////////////// + //rekkie -- DEV_1 -- s + if (current_node_type == NODE_LADDER_UP || current_node_type == NODE_LADDER_DOWN || + next_node_type == NODE_LADDER_UP || next_node_type == NODE_LADDER_DOWN) + { + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); // Find the distance vector to the next node + ////distance = VectorLength(dist); + + ////distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); // Find the distance vector to the next node + ////dist_xy[2] = 0; // Lose the vertical component + ////distance_xy = VectorLength(dist_xy); // Get the absolute length + + float speed = VectorLength(self->velocity); // Speed of the player + + // If we are going down the ladder, check for teammates coming up and yield to them. + trace_t tr; + tr = gi.trace(self->s.origin, tv(-16, -16, -8), tv(16, 16, 8), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if ((tr.fraction < 1.0) && tr.ent && (tr.ent != self) && tr.ent->client && (tr.ent->velocity[2] >= 0)) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 200; + self->bot.state = STATE_WANDER; + return; + } + + // If getting off the top of a ladder + if (current_node_type == NODE_LADDER_UP && next_node_type != NODE_LADDER_DOWN) + { + ucmd->forwardmove = SPEED_WALK; + + //Com_Printf("%s %s on top ladder getting off, up vel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + + //Com_Printf("%s %s on top ladder getting off. OnLadder %d\n", __func__, self->client->pers.netname, OnLadder(self)); + if (OnLadder(self)) + { + ucmd->upmove = SPEED_RUN; + return; + } + else + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + + if (self->velocity[2] >= 0 && self->velocity[2] < 200) // do we need a boost? + { + float zdiff = 0; + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + zdiff = abs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]); + float boost = 64 + zdiff; + //float boost = 50; + //Com_Printf("%s %s on top ladder getting off, boosting jump from %f to %f\n", __func__, self->client->pers.netname, self->velocity[2], self->velocity[2] + boost); + self->velocity[2] += boost; // slight boost jump at top of ladder + } + + if (distance < 64 && self->s.origin[2] >= nodes[self->bot.next_node].origin[2]) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + VectorScale(dist, 50, self->velocity); // Apply it + //Com_Printf("%s %s on top ladder getting off, guiding velocity\n", __func__, self->client->pers.netname); + } + } + + /* + if ((nodes[self->bot.next_node].origin[2] >= self->s.origin[2]) + && (nodes[self->bot.next_node].origin[2] >= nodes[self->bot.current_node].origin[2]) + && (((self->velocity[2] > 0) && !self->groundentity) || OnLadder(self))) + { + ucmd->upmove = SPEED_RUN; + self->velocity[2] = min(200, BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) * 10); // Jump higher for farther node + } + + if (1) + { + // Guide the bot in when close to the target + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 32 && distance < 128) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 400, self->velocity); // Apply it + } + } + */ + + ACEMV_ChangeBotAngle(self); + return; + } + + + // If getting off the bottom of a ladder + if (current_node_type == NODE_LADDER_DOWN && next_node_type != NODE_LADDER_UP) + { + // We used to do nothing here, because it was handled by the next node type... but now we just walk forward :) + + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->forwardmove = SPEED_RUN; + } + + + // Getting onto bottom of the ladder + if (current_node_type != NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN && distance < 192 && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) + { + //Com_Printf("%s %s getting onto bottom of the ladder\n", __func__, self->client->pers.netname); + + // Jump only when we are moving the correct direction. + // Check we've reach our ideal yaw angle + //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //qboolean isOnLadder = OnLadder(self); + //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); + //if (isOnLadder == false) + if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT) > 16) + { + //qboolean correct_angle = BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //Com_Printf("%s %s correct_angle %d, isOnLadder %d\n", __func__, self->client->pers.netname, correct_angle, isOnLadder); + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //ucmd->upmove = SPEED_WALK; + ucmd->forwardmove = 96; + + //Com_Printf("%s %s moving to bottom of the ladder\n", __func__, self->client->pers.netname); + return; + } + else + { + //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); + //current_node_type = NODE_LADDER_DOWN; + //next_node_type = NODE_LADDER_UP; + } + + /* + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) && OnLadder(self) == false) + //if (OnLadder(self) == false) + { + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_WALK; + } + */ + + + if (1) + { + //Com_Printf("%s %s is at bottom of the ladder\n", __func__, self->client->pers.netname); + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 32) + // self->velocity[2] = 100; + //return; + + // Guide the bot in when close to the target + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] + 32 && distance < 128) + if (self->s.origin[2] < nodes[self->bot.next_node].origin[2] && distance < 128) + //if (self->s.origin[2] < nodes[self->bot.next_node].origin[2]) + { + /* + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 32, self->velocity); // Apply it + */ + + if (OnLadder(self) == false) + { + ucmd->forwardmove = 32; + ucmd->upmove = 32; + + if (NODES_AdvanceToNextNode(self)) + return; + } + + else + { + /* + // Get distance + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + float next_node_distance = VectorLength(dist); // Get the absolute length + // Apply it + if (next_node_distance > 48) + VectorScale(dist, SPEED_RUN, self->velocity); + else if (next_node_distance > 24) + VectorScale(dist, 350, self->velocity); + else + VectorScale(dist, 300, self->velocity); + */ + } + + /* + if (OnLadder(self) == false) + ucmd->forwardmove = 64; + else + { + // Make bot look directly up from its current pos + vec3_t above; + VectorCopy(self->s.origin, above); + above[2] + 32; + VectorSubtract(above, self->s.origin, self->move_vector); // point up + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // look up + + ucmd->upmove = SPEED_RUN; + //self->s.origin[2] = nodes[self->bot.next_node].origin[2]; + } + + // If touching ladder, advance to next node (ladder down -> ladder up) + if (OnLadder(self)) + { + if (NODES_AdvanceToNextNode(self)) + return; + } + */ + + //Com_Printf("%s %s is at bottom of the ladder, OnLadder:%d\n", __func__, self->client->pers.netname, OnLadder(self)); + } + return; + } + + + //if (OnLadder(self) == false) + //return; + } + + // Getting onto top of the ladder + if (current_node_type != NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP)// && self->groundentity) + { + // Jump only when we are moving the correct direction. + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + else // Correct direction, jump up + { + if (distance > 64) + ucmd->forwardmove = SPEED_WALK; + else if (distance > 48) + ucmd->forwardmove = 50; + else if (distance > 24) + ucmd->forwardmove = 25; + } + + // Guide the bot in when close to the target + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + //distance = VectorLength(dist); + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] && distance <= 128) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 100, self->velocity); // Apply it + } + + // + //if (OnLadder(self) == false) + return; + } + + // On ladder going up + if (current_node_type == NODE_LADDER_DOWN && next_node_type == NODE_LADDER_UP) + { + //Com_Printf("%s %s on ladder going up\n", __func__, self->client->pers.netname); + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance < 64) + ////if (nodes[self->bot.next_node].origin[2] + NODE_Z_HEIGHT >= self->s.origin[2] && distance_xy < 64) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + if (!self->groundentity) + { + // FIXME: Dirty hack so the bots can actually use ladders. + //vec3_t origin_up; + //VectorCopy(self->s.origin, origin_up); + //origin_up[2] += 8; + //VectorSubtract(origin_up, self->s.origin, dist_xy); + + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); + ////VectorNormalize(dist_xy); + ////VectorScale(dist_xy, SPEED_RUN, self->velocity); + ////if (dist_xy[2] > 0) + //// self->velocity[2] = min(200, self->velocity[2]); + VectorScale(dist, SPEED_RUN, self->velocity); + if (dist[2] > 0) + self->velocity[2] = min(200, self->velocity[2]); + } + + // Guide the bot in when close to the target + // Useful for the tall ladder found on murder.bsp - helps to deal with ladders that use playerclip + //if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16 && distance < 64) + if (self->s.origin[2] <= nodes[self->bot.next_node].origin[2] + 16) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + VectorScale(dist, 200, self->velocity); // Apply it + } + + return; + } + } + + // On ladder going down + if (current_node_type == NODE_LADDER_UP && next_node_type == NODE_LADDER_DOWN) + { + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance < 64) //&& speed <= SPEED_ROAM) + ////if (self->s.origin[2] >= nodes[self->bot.next_node].origin[2] && distance_xy < 64) //&& speed <= SPEED_ROAM) + { + ucmd->forwardmove = SPEED_WALK / 2; + ////if ((distance_xy < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) + if ((distance < 200) && (nodes[self->bot.next_node].origin[2] <= self->s.origin[2] + 16)) + ucmd->upmove = -SPEED_RUN; //Added by Werewolf to cause crouching + + if (!self->groundentity) + { + VectorCopy(self->velocity, velocity); + velocity[2] = 0; + VectorNormalize(velocity); + VectorNormalize(dist); + float dot = DotProduct(velocity, dist); + ////VectorNormalize(dist_xy); + ////float dot = DotProduct(velocity, dist_xy); + if (dot < 0.8 && dot != 0) // Make bot face ladder + { + //ACEMV_ChangeBotAngle(self); + //Com_Printf("%s Correcting direction. DotProduct(velocity, dist_xy) = %f\n", __func__, dot); + } + + // FIXME: Dirty hack so the bots can actually use ladders. + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist_xy); + ////VectorNormalize(dist_xy); + ////VectorScale(dist_xy, SPEED_RUN, self->velocity); + ////if (dist_xy[2] < 0) + //// self->velocity[2] = max(-200, self->velocity[2]); + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + VectorScale(dist, SPEED_RUN, self->velocity); + if (dist[2] < 0) + self->velocity[2] = max(-200, self->velocity[2]); + } + return; + } + } + } + //rekkie -- DEV_1 -- e + + + //////////////////////////////////////////////////////// + // Water Nodes + /////////////////////////////////////////////////////// + if (current_node_type == NODE_WATER || self->waterlevel > 0) + { + ACEMV_ChangeBotAngle(self); + //ucmd->upmove = SPEED_RUN; + //ucmd->forwardmove = SPEED_RUN; + + // Ensure we have LoS + tr = gi.trace(self->s.origin, tv(-16, -16, -4), tv(16, 16, 4), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + + // Guide the bot in when close to the target + //if (next_node_type != NODE_WATER) // && distance <= 128) + { + BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + if (next_node_type != NODE_WATER) + { + VectorScale(dist, 400, self->velocity); // Jump out + ucmd->upmove = SPEED_RUN; + ucmd->forwardmove = SPEED_RUN; + } + else + VectorScale(dist, 200, self->velocity); // Move water speed + } + + /* + // We need to be pointed up/down + ACEMV_ChangeBotAngle(self); + + // If the next node is not in the water, then move up to get out. + if (next_node_type != NODE_WATER && !(gi.pointcontents(nodes[self->bot.next_node].origin) & MASK_WATER)) // Exit water + ucmd->upmove = SPEED_RUN; + + ucmd->forwardmove = SPEED_RUN * 3 / 4; + return; + */ + } + + //rekkie -- DEV_1 -- s + //////////////////////////////////////////////////////// + // NODE_STAND_DROP Nodes + if (next_node_type == NODE_STAND_DROP || next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + { + if (0) + { + if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s next_node_type [NODE_STAND_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s next_node_type [NODE_CROUCH_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s next_node_type [NODE_UNSAFE_DROP]... zvel %f\n", __func__, self->client->pers.netname, self->velocity[2]); + } + + // If target is getting too far away, adjust our angle + float distance_xy = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + if (distance_xy > 32) + ACEMV_ChangeBotAngle(self); + + // Then keep walking + ucmd->forwardmove = SPEED_WALK; + + //Com_Printf("%s %s vel %f\n", __func__, self->client->pers.netname, VectorLength(self->velocity)); + + // If stuck, try clearing the obstacle by moving in a random dir + if (VectorLength(self->velocity) < 32) + { + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0) // blocked, try find another path to the same goal + { + //Com_Printf("%s %s is blocked [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //BOTLIB_CanVisitNode(self, self->bot.goal_node); + //ACEMV_ChangeBotAngle(self); + + self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[PITCH] = 0; + + //self->bot.state = STATE_WANDER; + return; + } + } + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + //if (self->velocity[2] < -400 || distance < 64) + //if (distance < 64) + //VectorScale(dist, 350, self->velocity); // Apply it + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0) + { + VectorScale(dist, 350, self->velocity); // Apply it + VectorCopy(self->velocity, self->client->oldvelocity); + VectorCopy(self->velocity, self->avelocity); + } + + return; + + /* + // If the bot fell below the target (failed the jump down) + // If the bot is trying an unsafe drop past the LCA timer + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2] || + (next_node_type == NODE_UNSAFE_DROP && lights_camera_action > 23)) + { + // Update the node we're near + int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + if (tmp_node != self->bot.current_node) + { + self->bot.prev_node = self->bot.current_node; + self->bot.current_node = tmp_node; + //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + } + + if (self->bot.current_node == INVALID) + { + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + (random() + 0.5) * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + //Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); + BOTLIB_SetGoal(self, self->bot.goal_node); + } + else + { + //Com_Printf("%s %s failed to drop down, finding new goal\n", __func__, self->client->pers.netname); + self->bot.state = STATE_WANDER; // Aquire a new node + } + return; + } + + ACEMV_ChangeBotAngle(self); + + // If stuck, randomly crouch or jump + if (VectorLength(self->velocity) < 3) + { + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]...\n", __func__, self->client->pers.netname); + if (random() > 0.5) + { + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: crouching\n", __func__, self->client->pers.netname); + //return; + } + else + { + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [NODE_STAND_DROP]: jumping\n", __func__, self->client->pers.netname); + //return; + } + } + + + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XY_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////dist[2] = 0; + ////distance = VectorLength(dist); + + if (!self->groundentity) + { + // When in air, control speed carefully to avoid overshooting the next node. + if (distance < 256) + { + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 128); //256 + // Crouch down + if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + ucmd->upmove = -SPEED_WALK; + } + else + ucmd->forwardmove = SPEED_RUN; + } + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + // If we reached a ledge, slow down. + if (distance < 512) + { + // Jump unless dropping down to the next node. + //if (nodes[self->bot.next_node].origin[2] > (self->s.origin[2] + 7) && distance < 500) + // ucmd->upmove = SPEED_RUN; + + // Crouch down + if (next_node_type == NODE_CROUCH_DROP || next_node_type == NODE_UNSAFE_DROP) + ucmd->upmove = -SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 512); + } + else + ucmd->forwardmove = SPEED_WALK; + } + else // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + + + if(1) + // Guide the bot in when close to the target + //if (distance <= 128) //64 + if (self->velocity[2] < 0) + { + //if (self->velocity[2] < 32) + // ACEMV_ChangeBotAngle(self); + + //if (next_node_type == NODE_STAND_DROP) Com_Printf("%s %s: NODE_STAND_DROP\n", __func__, self->client->pers.netname); + //if (next_node_type == NODE_CROUCH_DROP) Com_Printf("%s %s: NODE_CROUCH_DROP\n", __func__, self->client->pers.netname); + //if (next_node_type == NODE_UNSAFE_DROP) Com_Printf("%s %s: NODE_UNSAFE_DROP\n", __func__, self->client->pers.netname); + + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + VectorNormalize(dist); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////VectorNormalize(dist); + ////distance = VectorLength(dist); // Get the absolute length + + if (self->groundentity && ACEMV_CanMove(self, MOVE_FORWARD) == false) // Slow down if on edge with drop + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); + + VectorScale(dist, 350, self->velocity); // Apply it + + return; + } + */ + } + //rekkie -- DEV_1 -- e + + + //////////////////////////////////////////////////////// + // Move & Step Nodes + /////////////////////////////////////////////////////// + if (current_node_type == NODE_MOVE || current_node_type == NODE_STEP || current_node_type == NODE_ITEM) + { + //Com_Printf("%s %s move forward\n", __func__, self->client->pers.netname); + + + { + // Get distance from bot to next node + vec3_t bot_to_prev_vec, bot_to_curr_vec, bot_to_next_vec; // Direction vector to nodes + float bot_to_prev_dist = 0, bot_to_curr_dist = 0, bot_to_next_dist = 0; // Distance to nodes + qboolean can_see_prev = false, can_see_curr = false, can_see_next = false; // If bot can see these nodes + if (self->bot.prev_node != INVALID) + { + VectorSubtract(nodes[self->bot.prev_node].origin, self->s.origin, bot_to_prev_vec); + bot_to_prev_dist = VectorLength(bot_to_prev_vec); + //trace_t prev_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); + trace_t prev_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.prev_node].origin, self, MASK_PLAYERSOLID); + if (prev_tr.fraction == 1.0) + can_see_prev = true; + } + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_curr_vec); + bot_to_curr_dist = VectorLength(bot_to_curr_vec); + //trace_t curr_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); + trace_t curr_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (curr_tr.fraction == 1.0) + can_see_curr = true; + + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + bot_to_next_dist = VectorLength(bot_to_next_vec); + //trace_t next_tr = gi.trace(self->s.origin, tv(-16, -16, -24), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + trace_t next_tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (next_tr.fraction == 1.0) + can_see_next = true; + + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) + { + //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + if (bot_to_next_dist < 192) + { + VectorClear(self->velocity); + bot_to_next_vec[2] = 0; // Flatten the vector + VectorScale(bot_to_next_vec, 400, self->velocity); + } + else + ucmd->forwardmove = SPEED_RUN; + } + else + ucmd->forwardmove = SPEED_RUN; + + //qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right + //qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left + + // Stay on course by strafing back to the path line (if not already strafing) + if (1) + { + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + if (mov_strafe == 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + } + else if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK; + + // Get distance from bot to self->nearest_path_point + vec3_t bot_to_path_vec; // Direction vector to nodes + VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); + float bot_to_path_dist = VectorLength(bot_to_path_vec); + //ucmd->sidemove = SPEED_WALK * sqrtf(bot_to_path_dist / 256); + + bot_to_path_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_path_vec); + VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = -SPEED_WALK; + + ucmd->forwardmove = SPEED_WALK; + + // Get distance from bot to self->nearest_path_point + vec3_t bot_to_path_vec; // Direction vector to nodes + VectorSubtract(self->nearest_path_point, self->s.origin, bot_to_path_vec); + float bot_to_path_dist = VectorLength(bot_to_path_vec); + //ucmd->sidemove = -SPEED_WALK * sqrtf(bot_to_path_dist / 256); + + bot_to_path_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_path_vec); + VectorScale(bot_to_path_vec, 100, self->velocity); // Apply it + } + } + + /* + // Check if bot can see the nodes + if (can_see_next) + { + //Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + + //bot_to_next_vec[2] = 0; // Flatten the vector + //VectorNormalize(bot_to_next_vec); + //if (self->groundentity) + //{ + // ucmd->forwardmove = SPEED_RUN; + // VectorScale(bot_to_next_vec, 400, self->velocity); + //} + //else + ucmd->forwardmove = SPEED_RUN; + + //if (bot_to_next_dist < 64) + //BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2); // self->move_vector + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_next_vec, 2) == false) // && bot_to_next_dist < 128) + { + Com_Printf("%s %s guiding bot: can_see_next %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + //ucmd->forwardmove = SPEED_RUN; + bot_to_next_vec[2] = 0; // Flatten the vector + VectorScale(bot_to_next_vec, 400, self->velocity); + //VectorClear(self->velocity); + } + + } + else if (can_see_curr) + { + Com_Printf("%s %s guiding bot: can_see_curr %f\n", __func__, self->client->pers.netname, bot_to_curr_dist); + ucmd->forwardmove = SPEED_RUN; + BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_curr_vec, 2); // self->move_vector + } + else if (can_see_prev) + { + Com_Printf("%s %s guiding bot: can_see_prev %f\n", __func__, self->client->pers.netname, bot_to_prev_dist); + ucmd->forwardmove = SPEED_RUN; + BOTLIB_UTIL_ChangeBotAngleYaw(self, bot_to_prev_vec, 2); // self->move_vector + } + else + { + Com_Printf("%s %s guiding bot: Wander\n", __func__, self->client->pers.netname); + // Wander or Reroute + } + */ + + self->s.angles[PITCH] = 0; // Look directly forward + return; + } + + + { + //ucmd->forwardmove = SPEED_RUN/2; + //self->s.angles[PITCH] = 0; // Look directly forward + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); // Change the speed when close to node + //return; + } + + + + + + + + + + + + // Get distance from bot to next node + vec3_t bot_to_next_vec; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_next_vec); + float bot_to_next_dist = VectorLength(bot_to_next_vec); + //Com_Printf("%s: %s bot_to_next_dist %f\n", __func__, self->client->pers.netname, bot_to_next_dist); + + // Guide bot into the next_node when close + if (bot_to_next_dist < 24) + { + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + bot_to_next_vec[2] = 0; // Flatten the vector + VectorNormalize(bot_to_next_vec); + VectorScale(bot_to_next_vec, 200, self->velocity); + //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); + return; + } + } + + + // Change bot angle to face next node + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 11.25) == false) + { + VectorClear(self->velocity); // Kill velocity, to give bot time to turn + return; + } + + + // Strafe if we're off course + { + // If not strafing to get back on path + // Check if path forward is blocked + qboolean did_strafe_right = false; // If the bot made a strafe move right + qboolean did_strafe_left = false; // If the bot made a strafe move left + //tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + //if (mov_strafe == 0 && tr.fraction < 1.0) + if (bot_to_next_dist > 64) + { + qboolean right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, 16); // Check right + qboolean left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 128, -16); // Check left + + qboolean mid_partial_blocked = false; + qboolean mid_full_blocked = false; + + // Line trace + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + mid_partial_blocked = true; + + // Box trace -- full width of the bot + tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + mid_full_blocked = true; + } + + // All [BLOCKED] + if (mid_partial_blocked && right_blocked && left_blocked) + { + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + + // Bot -> nearest path point + tr = gi.trace(self->s.origin, NULL, NULL, self->nearest_path_point, self, MASK_DEADSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + //Com_Printf("%s %s --> heading for nearest path point\n", __func__, self->client->pers.netname); + + // Move vector between bot and self->nearest_path_point + VectorSubtract(self->nearest_path_point, self->s.origin, self->move_vector); + + self->move_vector[2] = 0; // Flatten the vector + VectorNormalize(self->move_vector); + VectorScale(self->move_vector, 400, self->velocity); + return; + } + + // Bot -> current node point + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); + if (tr.fraction == 1.0 && tr.startsolid == false) + { + Com_Printf("%s %s --> heading for current node point\n", __func__, self->client->pers.netname); + + // Move vector between bot and nodes[self->bot.current_node].origin + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + + self->move_vector[2] = 0; // Flatten the vector + VectorNormalize(self->move_vector); + VectorScale(self->move_vector, 400, self->velocity); + return; + } + + // Bot -> re-route + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + if (self->bot.current_node == INVALID) + { + Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + return; + } + + return; + } + + // mid [BLOCKED] && right [CLEAR] && left [CLEAR] + if (mid_partial_blocked && !right_blocked && !left_blocked) + { + //Com_Printf("%s: %s partial middle blocked\n", __func__, self->client->pers.netname); + + if (ucmd->sidemove > 0) // Bot was strafing right, so continue + { + //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else if (ucmd->sidemove < 0) // Bot was strafing left, so continue + { + //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + else // Bot was not strafing, so pick a random direction + { + if (rand() % 2 == 0) + { + //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else + { + //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + } + + // When bot encounters a thin pole, prevent it from getting stuck + if (mid_full_blocked && forward_or_back_velocity == 0) + { + //Com_Printf("%s: %s full middle blocked\n", __func__, self->client->pers.netname); + + if (ucmd->sidemove > 0) // Bot was strafing right, so continue + { + //Com_Printf("%s %s --> continue strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else if (ucmd->sidemove < 0) // Bot was strafing left, so continue + { + //Com_Printf("%s %s --> continue strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + else // Bot was not strafing, so pick a random direction + { + if (rand() % 2 == 0) + { + //Com_Printf("%s %s --> RNG strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + else + { + //Com_Printf("%s %s --> RNG strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + } + + // NARROW strafe search -- RIGHT or LEFT [BLOCKED] + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> small strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + else if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> small strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + + // WIDE strafe search + if (!mid_partial_blocked && left_blocked && right_blocked) + { + //Com_Printf("%s: %s small left and right blocked\n", __func__, self->client->pers.netname); + + // Try a wider search + right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 32); // Check right + left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -32); // Check left + + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> medium strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + + if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> medium strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + + if (left_blocked && right_blocked) + { + //Com_Printf("%s: %s medium left and right blocked\n", __func__, self->client->pers.netname); + + // Try the widest search + right_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, 48); // Check right + left_blocked = BOTLIB_UTIL_PATH_FORWARD(self, ucmd, 48, -48); // Check left + + if (right_blocked) + { + //Com_Printf("%s: %s right blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_LEFT)) + { + //Com_Printf("%s %s --> large strafe left\n", __func__, self->client->pers.netname); + ucmd->sidemove = -SPEED_WALK; + did_strafe_left = true; + } + } + + if (left_blocked) + { + //Com_Printf("%s: %s left blocked\n", __func__, self->client->pers.netname); + //if (ACEMV_CanMove(self, MOVE_RIGHT)) + { + //Com_Printf("%s %s --> large strafe right\n", __func__, self->client->pers.netname); + ucmd->sidemove = SPEED_WALK; + did_strafe_right = true; + } + } + } + } + + // Stay on course by strafing back to the path line (if not already strafing) + if (did_strafe_right == false && did_strafe_left == false && mid_partial_blocked == false) + { + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + if (mov_strafe == 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + //ucmd->sidemove = 0; + } + else if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + ucmd->sidemove = SPEED_WALK; + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + ucmd->sidemove = -SPEED_WALK; + } + } + } + + ucmd->forwardmove = SPEED_RUN; + return; + } + + + + + + + + + //distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR); + // If stuck, try clearing the obstacle by moving in a random dir + if (VectorLength(self->velocity) <= 8) + { + //if (BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_CURR) > 32) + //Com_Printf("%s %s is stuck [NODE_MOVE]...\n", __func__, self->client->pers.netname); + + tr = gi.trace(self->s.origin, tv(-16, -16, -6), tv(16, 16, 32), nodes[self->bot.next_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1.0) // blocked, try find another path to the same goal + { + //Com_Printf("%s %s is blocked [NODE_MOVE]...\n", __func__, self->client->pers.netname); + + // Try strafing + if (random() > 0.5) + { + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + ACEMV_ChangeBotAngle(self); + + // Try strafing + if ((self->bot_strafe <= 0) && ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + else + ucmd->sidemove = 0; + + self->bot_strafe = ucmd->sidemove; + + if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_RUN; + // Also try move backwards + else if (ACEMV_CanMove(self, MOVE_BACK)) + ucmd->forwardmove = -SPEED_RUN; + + return; + } + + /* + if (random() > 0.25) + { + //if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -SPEED_RUN; + if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + + self->s.angles[YAW] += 22.5 + (random() * 270); + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + return; + } + */ + + + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //BOTLIB_CanVisitNode(self, self->bot.goal_node); + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, self->move_vector); + //ACEMV_ChangeBotAngle(self); + + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + //BOTLIB_MOV_ChangeBotAngleYawPitch(self); + /////////////////////////////////// LaunchPlayer(self, nodes[self->bot.current_node].origin); //////////////////////////// + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, dist); + //VectorNormalize(dist); + //VectorScale(dist, 400, self->velocity); // Apply it + //self->velocity[2] = 50; + + /* + //if (ACEMV_CanMove(self, MOVE_BACK)) + // ucmd->forwardmove = -SPEED_RUN; + if (ACEMV_CanMove(self, MOVE_LEFT)) + ucmd->sidemove = -SPEED_RUN; + else if (ACEMV_CanMove(self, MOVE_RIGHT)) + ucmd->sidemove = SPEED_RUN; + + //self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[YAW] += 180; + ucmd->forwardmove = SPEED_RUN; + ucmd->upmove = SPEED_RUN; + self->s.angles[PITCH] = 0; + */ + + // Make the bot look at the current node + //self->bot.next_node = self->bot.current_node; + //VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, self->move_vector); + //BOTLIB_MOV_ChangeBotAngleYawPitch(self); + //ucmd->forwardmove = SPEED_RUN; + + + + //self->bot.state = STATE_WANDER; + return; + } + } + + // If we cannot see the next node + tr = gi.trace(self->s.origin, tv(-8, -8, 0), tv(8, 8, 0), nodes[self->bot.current_node].origin, self, MASK_DEADSOLID); + if (tr.fraction < 1) + { + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s [MOVE_NODE][1] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s failed to FindClosestReachableNode for goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + } + + // Run up stairs + if (current_node_type == NODE_STEP && next_node_type == NODE_STEP) + { + ucmd->forwardmove = SPEED_RUN; + self->s.angles[PITCH] = 0; // Look directly forward + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + //Com_Printf("%s %s Step --> Step\n", __func__, self->client->pers.netname); + return; + } + + /* + // If stuck, crouch or jump + if (VectorLength(self->velocity) < 3) + { + Com_Printf("%s %s is stuck...\n", __func__, self->client->pers.netname); + + trace_t tr_lower_body; + trace_t tr_upper_body; + vec3_t fwd; + AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin + VectorMA(self->s.origin, 16, fwd, fwd); + tr_lower_body = gi.trace(self->s.origin, tv(-5, -5, -14), tv(5, 5, 0), fwd, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + tr_upper_body = gi.trace(self->s.origin, tv(-5, -5, 0), tv(5, 5, 32), fwd, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + // Need to crouch? + // Lower body is free, upper body is blocked + if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) + { + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + Com_Printf("%s %s is stuck [NODE_MOVE]: crouching\n", __func__, self->client->pers.netname); + return; + } + // Need to jump? + // Lower body is blocked, upper body is free + else if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) + { + BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + Com_Printf("%s %s is stuck [NODE_MOVE]: jumping\n", __func__, self->client->pers.netname); + return; + } + else + { + //self->bot.state = STATE_WANDER; + //return; + } + } + */ + + + // If the bot fell below the target (failed the jump down) + // Try to find another route, otherwise find a new node and go there instead + if (self->s.origin[2] + NODE_Z_HEIGHT < nodes[self->bot.next_node].origin[2]) + { + // Upgrade target node type to jumppad + if (targetNodeLink != INVALID) + { + //if (debug_mode) + //Com_Printf("%s %s dropped from a move forward, upgrading move to jumppad node\n", __func__, self->client->pers.netname); + + nodes[self->bot.current_node].links[targetNodeLink].targetNodeType = NODE_JUMPPAD; + + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s [MOVE_NODE][2] prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + return; + } + else + { + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + //if (debug_mode) + //Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + BOTLIB_SetGoal(self, self->bot.goal_node); + return; + } + + //if (debug_mode) + //Com_Printf("%s %s failed to move forward to goal %d, wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + + //self->bot.state = STATE_WANDER; // Aquire a new node + //return; + } + } + + self->s.angles[PITCH] = 0; // Look directly forward + + //BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2); + + // Check we've reach our ideal yaw angle + if (BOTLIB_UTIL_ChangeBotAngleYaw(self, self->move_vector, 2) == false) + return; + + /* + // See if we're stuck, try to unstick + if (VectorLength(self->velocity) < 8) + { + // project forward from origin + // Box test [feet to mid body] -> forward 16 units + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 32, forward, forward); + tr = gi.trace(self->s.origin, tv(-8, -8, -14), tv(8, 8, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1 || tr.startsolid) + { + ucmd->upmove = 400; // Try jumping + } + else + ucmd->upmove = -400; // Try crouching + + // See if the way forward is clear + tr = gi.trace(self->s.origin, tv(-16, -16, 0), tv(16, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction < 1) + { + // Try strafing left + tr = gi.trace(self->s.origin, tv(-32, -16, -14), tv(0, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction == 1) + { + ucmd->sidemove = -400; + } + else + { + // Try strafing right + tr = gi.trace(self->s.origin, tv(0, -16, -14), tv(32, 16, 0), forward, self, MASK_DEADSOLID); + if (tr.fraction == 1) + { + ucmd->sidemove = 400; + } + } + } + } + */ + + // Guide the bot in when close to the target + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////distance = VectorLength(dist); + if (distance <= 32) + { + //tr = gi.trace(self->s.origin, tv(-14, -14, -22), tv(14, 14, 2), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + tr = gi.trace(self->s.origin, tv(-14, -14, -6), tv(14, 14, 6), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + VectorNormalize(dist); + distance = VectorLength(dist); // Get the absolute length + + ucmd->forwardmove = 1; // Fake walk, just want the legs to move. Velocity will do the actual moving. + + // Apply it + //if (!ACEMV_CanMove(self, MOVE_FORWARD)) + // VectorScale(dist, (SPEED_WALK * sqrtf(distance / 512)), self->velocity); // Slow down + //else + // VectorScale(dist, 128, self->velocity); + //VectorScale(dist, 224, self->velocity); + + if (distance <= 2) + VectorScale(dist, 128, self->velocity); + //else if (distance <= 16) + // VectorScale(dist, 224, self->velocity); + else + VectorScale(dist, 320, self->velocity); + + //Com_Printf("%s %s guiding bot from a move forward\n", __func__, self->client->pers.netname); + + return; + } + } + + /* + if (next_node_type == INVALID) + { + if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + //ACEAI_PickLongRangeGoal(self); + return; + } + } + */ + + //ACEMV_ChangeBotAngle(self); + + /* + distance = BOTLIB_UTIL_Get_Distance(self, dist, BOT_DIST_XYZ_SELF_NEXT); + ////VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, dist); + ////dist[2] = 0; + ////distance = VectorLength(dist); + if (!self->groundentity) + { + Com_Printf("%s %s [MOVE_NODE] air guide...\n", __func__, self->client->pers.netname); + + // When in air, control speed carefully to avoid overshooting the next node. + if (distance < 256) + ucmd->forwardmove = SPEED_RUN * sqrtf(distance / 256); + else + ucmd->forwardmove = SPEED_RUN; + } + + //rekkie -- DEV_1 -- s + else if (!ACEMV_CanMove(self, MOVE_FORWARD)) + { + Com_Printf("%s %s [MOVE_NODE] move guide...\n", __func__, self->client->pers.netname); + + if (distance < 128) + ucmd->forwardmove = SPEED_WALK * sqrtf(distance / 128); + else + ucmd->forwardmove = SPEED_WALK; + } + else + { + // Otherwise move as fast as we can. + ucmd->forwardmove = SPEED_RUN; + } + */ + + ucmd->forwardmove = SPEED_RUN; + //rekkie -- DEV_1 -- e + + } +#endif +} + +qboolean BOTLIB_DoParabolaJump(edict_t* self, vec3_t targetpoint) +{ + /* + vec3_t forward; + + // immediately turn to where we need to go + float length = VectorDistance(self->s.origin, targetpoint); + float fwd_speed = length * 1.95f; + + vec3_t dir; + PredictAim(self, self->enemy, self->s.origin, fwd_speed, false, 0.f, &dir, nullptr); + + self->s.angles[1] = vectoyaw(dir); + AngleVectors(self->s.angles, forward, nullptr, nullptr); + self->s.origin[2] += 1; + self->velocity = forward * fwd_speed; + self->velocity[2] = 450; + */ + + + + float fordward_speed; + vec3_t forward, dir, vec, target; + + VectorCopy(targetpoint, target); + + // Get height diff from self to target point + // If > 0, target is higher than self + // If < 0, target is lower than self + //float height_diff = targetpoint[2] - (self->s.origin[2] - self->viewheight); // 56 - 22 = 34 + float height_diff = target[2] - self->s.origin[2]; // 56 - 22 = 34 + + //if (height_diff < 0) + target[2] += 32; + //else + // targetpoint[2] -= 32; + + fordward_speed = VectorDistance(self->s.origin, target); // Get forward speed + if (VectorEmpty(target)) // If Distance is zero + return false; + + //fordward_speed = fordward_speed * 1.95f; + //fordward_speed = fordward_speed * 0.2f; + + VectorSubtract(target, self->s.origin, dir); // Get direction + //dir[2] += self->viewheight; // Add eye height to direction height + float distance = VectorLength(dir); // Get distance + + float time = distance / fordward_speed; + + vec[0] = target[0] + time; + vec[1] = target[1] + time; + vec[2] = target[2] + time; + + // Adjust the z value by the height difference + //if (height_diff != 0) + // vec[2] += height_diff; + + // If vector is pointing the wrong direction (pointing backwards) + VectorNormalize(dir); + vec3_t dir2; + VectorSubtract(vec, self->s.origin, dir2); + VectorNormalize(dir2); + float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); + if (dot < 0) + { + VectorCopy(target, vec); + } + else + { + // if the shot is going to impact a nearby wall from our prediction, just fire it straight. + trace_t tr = gi.trace(self->s.origin, NULL, NULL, vec, NULL, MASK_SOLID); + if (tr.fraction < 0.9f) + { + VectorCopy(target, vec); + } + } + + //vec[2] += self->viewheight; // Add eye height to direction height + + VectorSubtract(vec, self->s.origin, dir); // Using + VectorNormalize(dir); + + vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; + angles[1] = vectoyaw(dir); + AngleVectors(angles, forward, NULL, NULL); // Get forward vector + VectorScale(forward, fordward_speed, self->velocity); // Scale the forward vector by the forward speed to get a velocity + self->s.origin[2] += 1; // Adjust player slightly off the ground + + + //self->velocity[0] = forward[0] * fordward_speed; + //self->velocity[1] = forward[1] * fordward_speed; + //self->velocity[2] = 450; + + if (height_diff > -2) + self->velocity[2] = 432 + height_diff; + else + { + if (fordward_speed + height_diff > 400) + self->velocity[2] = 450 + height_diff; + else + self->velocity[2] = fordward_speed + height_diff; + + if (self->velocity[2] < 300) + self->velocity[2] = fordward_speed; + + if (fordward_speed <= 64) + self->velocity[2] = 450; + else if (fordward_speed <= 96) + self->velocity[2] = 550; + else if (fordward_speed <= 128) + self->velocity[2] = 650; + else + self->velocity[2] = 450; + } + + self->velocity[2] = 432 + height_diff; + + + + + + + + //Com_Printf("%s %s speed:%f hdiff:%f vel[2]:%f\n", __func__, self->client->pers.netname, fordward_speed, height_diff, self->velocity[2]); + + //self->velocity[2] = fwd_speed <= 400 ? (fwd_speed + height_diff) : (400 + height_diff); + + + + /* + if (fwd_speed <= 64) + self->velocity[2] = 300 + height_diff; + else if (fwd_speed <= 96) + self->velocity[2] = 350 + height_diff; + else if (fwd_speed <= 128) + self->velocity[2] = 400 + height_diff; + else + self->velocity[2] = 432 + height_diff; + */ + + /* + // peak speeds on flat surface + velocity[0]; // 800 + velocity[1]; // 870 + velocity[2]; // 260 + speed; // 937 + */ + + self->bot.jumppad = true; // Successful jump + self->bot.node_jump_from = self->bot.current_node; + self->bot.node_jump_to = self->bot.next_node; + self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again + self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place + + return true; +} + +//rekkie -- Quake3 -- s +// predictive calculator +// target is who you want to shoot +// start is where the shot comes from +// bolt_speed is how fast the shot is (or 0 for hitscan) +// eye_height is a boolean to say whether or not to adjust to targets eye_height +// offset is how much time to miss by +// aimdir is the resulting aim direction (pass in nullptr if you don't want it) +// aimpoint is the resulting aimpoint (pass in nullptr if don't want it) +void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint) +{ + vec3_t dir, vec; + float dist, time; + + if (target) // Use entity's origin,viewheight,velocity if we can + { + VectorCopy(target->s.origin, target_point); + target_viewheight = target->viewheight; + VectorCopy(target->velocity, target_velocity); + } + + VectorSubtract(target_point, start, dir); + if (eye_height) + dir[2] += target_viewheight; + dist = VectorLength(dir); + + // [Paril-KEX] if our current attempt is blocked, try the opposite one + vec3_t end; + VectorMA(start, dist, dir, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + + //if (tr.ent != target) + { + eye_height = !eye_height; + VectorSubtract(target_point, start, dir); + if (eye_height) + dir[2] += target_viewheight; + dist = VectorLength(dir); + } + + if (bolt_speed) + time = dist / bolt_speed; + else + time = 0; + + // Calculate the jump height to get to the target + //float gravity = self->gravity * sv_gravity->value; + //float jump_height = sqrt(2 * (gravity * dist)); + //time = dist / (jump_height / 2); // Calculate the time it will take to get to the target + + //vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); + //vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); + //vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); + vec[0] = target_point[0] + (target_velocity[0] * (time - offset)); + vec[1] = target_point[1] + (target_velocity[1] * (time - offset)); + vec[2] = target_point[2] + (target_velocity[2] * (time - offset)); + + // went backwards... + VectorNormalize(dir); + vec3_t dir2; + VectorSubtract(vec, start, dir2); + VectorNormalize(dir2); + float dot = (dir2[0] * dir[0]) + (dir2[1] * dir[1]) + (dir2[2] * dir[2]); + if (dot < 0) + VectorCopy(target_point, vec); + else + { + // if the shot is going to impact a nearby wall from our prediction, just fire it straight. + tr = gi.trace(start, NULL, NULL, vec, NULL, MASK_SOLID); + if (tr.fraction < 0.9f) + VectorCopy(target_point, vec); + } + + if (eye_height) + vec[2] += target_viewheight; + + if (aimdir) + { + VectorSubtract(vec, start, *aimdir); + VectorNormalize(*aimdir); + } + + if (aimpoint) + { + VectorCopy(vec, *aimpoint); + } +} + +qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity) +{ + float length, fwd_speed; + vec3_t forward, dir; + + if (self->bot.jumppad_last_time > level.framenum) // Can we jump again so soon? + return false; + + if (self->client->leg_damage) // Can't jump if legs are damaged + return false; + + //Com_Printf("%s %s -- LETS GO!\n", __func__, self->client->pers.netname); + + // Zero our horizontal velocity + //target_velocity[0] = 0; + //target_velocity[1] = 0; + //target_velocity[2] = 0; + + // immediately turn to where we need to go + if (target) + { + length = VectorDistance(self->s.origin, target->s.origin); + VectorCopy(target->s.origin, target_point); + } + else + { + length = VectorDistance(self->s.origin, target_point); + } + if (VectorEmpty(target_point)) + return false; + + // Get height diff from self to target point + // If > 0, target is higher than self + // If < 0, target is lower than self + float height_diff = target_point[2] - (self->s.origin[2] - 24); + + /* + //if (height_diff < 0.1) + // height_diff = 0; + float modifier = 1.5; + if (height_diff > 0) + modifier = (height_diff / 60) + 1.5; + else if (height_diff < 0) + modifier = 1.7 - (fabs(height_diff) / 200); + + //fwd_speed = length * 1.95f; + //fwd_speed = length; + //fwd_speed = (length + 32) * modifier; + + fwd_speed = (length * 1.1) + (height_diff); + */ + //Com_Printf("%s z[%f] mod[%f] len[%f]\n", __func__, height_diff, modifier, length); + + if (self->is_bot && self->bot.current_node != INVALID && self->current_link != INVALID) + { + fwd_speed = length; // * nodes[self->bot.current_node].links[self->current_link].targetNodeJumpPower; // Self adjusting jump power + //Com_Printf("%s %s fwd_speed[%f]\n", __func__, self->client->pers.netname, fwd_speed); + } + else + { + fwd_speed = length; + } + + + + if (target) + { + vec3_t target_point, target_velocity; + BOTLIB_PredictJumpPoint(self, target, target_point, 0, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); + } + else + BOTLIB_PredictJumpPoint(self, NULL, target_point, target_viewheight, target_velocity, self->s.origin, fwd_speed, false, height_diff, &dir, NULL); + + vec3_t angles = { self->s.angles[0], self->s.angles[1], self->s.angles[2] }; + angles[1] = vectoyaw(dir); + AngleVectors(angles, forward, NULL, NULL); + self->s.origin[2] += 1; + VectorScale(forward, fwd_speed, self->velocity); + + //print big jump + //if (length < 360) + // Com_Printf("%s %s: Small Jump [%f]\n", __func__, self->client->pers.netname, length); + //else + // Com_Printf("%s %s: Big Jump [%f]\n", __func__, self->client->pers.netname, length); + + /* + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (target && target->s.origin[2] >= self->s.origin[2]) + { + z_height = (target->s.origin[2] - self->s.origin[2]); + z_height += sqrtf(length) + NODE_Z_HEIGHT; + self->velocity[2] += z_height; + } + else if (target_point[2] >= self->s.origin[2]) + { + z_height = (target_point[2] - self->s.origin[2]); + z_height += sqrtf(length) + NODE_Z_HEIGHT; + self->velocity[2] += z_height; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) + { + self->velocity[2] = 375; // 450 + } + */ + + /* + if (length <= 64) + self->velocity[2] = 400 + height_diff; // 450 + else + self->velocity[2] = 432 + height_diff; + */ + + if (length <= 64) + self->velocity[2] = 300 + height_diff; + else if (length <= 96) + self->velocity[2] = 350 + height_diff; + else if (length <= 128) + self->velocity[2] = 400 + height_diff; + else + self->velocity[2] = 432 + height_diff; + + + + + /* + // peak speeds on flat surface + velocity[0]; // 800 + velocity[1]; // 870 + velocity[2]; // 260 + speed; // 937 + */ + /* + // Get the surface normal of the ground the player is standing on + trace_t tr = gi.trace(self->s.origin, tv(-16,-16,-24), tv(16,16,32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, MASK_PLAYERSOLID); + + //if (self->velocity[0] > 800) self->velocity[0] = 800; + //if (self->velocity[1] > 870) self->velocity[1] = 870; + if (self->velocity[2] > 260) self->velocity[2] = 260; + if (tr.plane.normal[2] < 1.0) + { + float diff = 1.0 - tr.plane.normal[2]; + diff *= 2.0; + diff += tr.plane.normal[2]; + self->velocity[2] *= diff; + } + // calculate velocity speed + float speed = VectorLength(self->velocity); + Com_Printf("%s speed[%f] velocity_peak[%f %f %f]\n", __func__, speed, self->velocity[0], self->velocity[1], self->velocity[2]); + */ + + + + + + /* + // get height diff from self to target + qboolean above = false; + if (target && target->s.origin[2] >= self->s.origin[2]) + above = true; + else if (target_point[2] >= self->s.origin[2]) + above = true; + + float height_diff; + if (target) + height_diff = fabs(target->s.origin[2] - self->s.origin[2]); + else + height_diff = fabs(target_point[2] - self->s.origin[2]); + Com_Printf("%s %s: Jump [%f] Height[%f]\n", __func__, self->client->pers.netname, length, height_diff); + if (length < 256) + { + if (height_diff < 32) + self->velocity[2] = 200; + else + self->velocity[2] = 400; + } + else if (length < 384) + self->velocity[2] = 425; + else + self->velocity[2] = 450; + */ + + self->bot.jumppad = true; // Successful jump + self->bot.node_jump_from = self->bot.current_node; + self->bot.node_jump_to = self->bot.next_node; + self->bot.jumppad_last_time = level.framenum + 1 * HZ; // Time until jump can be used again + self->bot.jumppad_land_time = level.framenum + 1 * HZ; // Time until landing measures can take place + + return true; +} + +// Predict enemy position based on their position, velocity, and our velocity +void BOTLIB_PredictEnemyOrigin(edict_t *self, vec3_t out, float multiplier) +{ + // FRAMETIME + /* + out[0] = self->enemy->s.origin[0] + ((self->enemy->velocity[0] + self->velocity[0]) * (10.0 / HZ)); + out[1] = self->enemy->s.origin[1] + ((self->enemy->velocity[1] + self->velocity[1]) * (10.0 / HZ)); + + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // Predict lower if enemy is crouching + out[2] = (self->enemy->s.origin[2] - 24) + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); + else // Predict when standing + out[2] = self->enemy->s.origin[2] + ((self->enemy->velocity[2] + self->velocity[2]) * (10.0 / HZ)); + */ + + if (rand() % 2 == 0) + multiplier -= (multiplier * 2); + + //Com_Printf("%s %f\n", __func__, FRAMETIME); + VectorAdd(self->velocity, tv(self->enemy->velocity[0] * multiplier, self->enemy->velocity[1] * multiplier, self->enemy->velocity[2]), out); + VectorScale(out, FRAMETIME, out); + VectorAdd(out, self->enemy->s.origin, out); + //VectorCopy(self->enemy->s.origin, out); + if (self->enemy->maxs[2] == CROUCHING_MAXS2) + out[2] -= 24; + + if (self->client->weapon == FindItemByNum(KNIFE_NUM)) + { + // Quake 2 uses x,y,z which is stored in a float array called vec3_t[] + // The Z component is for jumping and falling + // Throwing knife affected by gravity over time + //knife->velocity[2] -= sv_gravity->value * FRAMETIME; // velocity is a vec3_t[3] which is a float array + // My origin + self->s.origin; // origin is a vec3_t[3] which is a float array + // Enemy origin + self->enemy->s.origin; // origin is a vec3_t[3] which is a float array + // My pitch aim (looking directly forward is a pitch of 0) + // #define PITCH 0 + // #define YAW 1 + // #define ROLL 2 + self->s.angles[PITCH]; + + // I want to find the aiming pitch requires to reach a target location with a throwing knife that drops with gravity. + // Quake2 uses x,y,z coordinates stored in a vec3_t[3] which is a float array [0] [1] [2] + // I have a first-person 3D game. A player origin, a target, gravity (on the Z axis only), and a throwing knife that is affected by gravity. By default the player looks forward at a pitch of 0. Looking directly up is -90 and down is 90. + float g = sv_gravity->value; // Gravity + float distance = VectorDistanceXY(self->s.origin, self->enemy->s.origin); // XY Distance to enemy + float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference + float knife_speed = 1200; + // Using the distance to my enemy and the gravity of the knife what pitch would I need to set to reach my enemy? + + // Calculate total flight time + //float t = sqrtf((2 * distance) / sv_gravity->value); + + + + } + +} + + + + +///////////////// +// Look dir +///////////////// +void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) +{ + vec3_t lookdir = { 0 }; // Direction to look + trace_t tr; + //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + float turn_speed = 0.3; + qboolean reached_look_at = false; + + if (VectorEmpty(self->bot.bi.look_at) == false) + { + reached_look_at = BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.15, true, true); + + if (reached_look_at == false) + return; + } + + + + // Look at ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) + { + // Check we're looking at ladder + qboolean looking_at_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + if (looking_at_ladder) + self->bot.touching_ladder = true; + } + // Turn to find ladder + if (looking_at_ladder == false) + { + int additional_rotation = 0; + for (int i = 0; i < 16; i++) + { + vec3_t fwd = { 0 }, end = { 0 }; + + self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 + + float yaw_rad = DEG2RAD(self->s.angles[YAW]); + + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder + { + self->bot.touching_ladder = true; + VectorSubtract(end, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 10.0, true, true); + if (additional_rotation >= 1) + break; + additional_rotation++; + } + } + } + + // Look up or down when on ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = -45; //-89 + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = 45; // 89 + } + } + } + // Look at enemy + else if (self->enemy) // Track target + { + qboolean not_infront = false; // If target is in front + + // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, + // the bot will keep looking in that general direction, but not directly at the enemy's pos + if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) + { + if (BOTLIB_Infront(self, self->enemy, 0.3) == false) + not_infront = true; + + if (1) // Predicted enemy pos + { + // Predict enemy position based on their velocity and distance + vec3_t predicted_enemy_origin = { 0 }; + BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 2); + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t red = MakeColor(255, 255, 255, 255); // red + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(self->enemy->s.number, predicted_enemy_origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 200, false); + DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, red, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + } + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy + /* + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + */ + } + else // Not predicted + { + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + } + } + else // Look where enemy was last + { + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t enemy_eyes; + VectorCopy(self->enemy->s.origin, enemy_eyes); + enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); + else + goto LookAhead; // Looking at a wall, so just look at nodes + } + /* + // Adjust turn speed based on skill + //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + if (self->bot.skill >= MAX_BOTSKILL) + { + turn_speed = 0.2; + } + //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) + { + //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold + } + + if (turn_speed > 1.0) + { + if (not_infront) + turn_speed = 1.0; + + if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim + turn_speed = 0.2; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap + turn_speed = 0.1; + else if (rand() % 10 == 0) // Bots sometimes get aim snap + turn_speed = 0.5; + + turn_speed = 1.0; + } + */ + + //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); + + //if (self->client->weaponstate != WEAPON_READY) + // not_infront = rand() % 2; + + /* + if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) + { + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + else + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + */ + } + /* + // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link + else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) + { + // See if this node has any POI lookat links + int num_lookat_nodes = 0; + int lookat_nodes[MAXLINKS] = { 0 }; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) + { + lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; + num_lookat_nodes++; + } + } + if (num_lookat_nodes) // One or more POI lookat nodes found + { + // When the timer is up pick a new node to look at + if (self->bot.node_poi_look_time < level.framenum) + { + self->bot.node_poi_look_time = level.framenum + 5 * HZ; + self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node + } + + // Look at POI lookat node + VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); + } + } + */ + else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + // If no enemy, have bot look at PNOISES, if any. + else if (1) + { + int type = 0; + int player_num = INVALID; + float nearest = 9999999; + qboolean found_target = false; + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) + { + //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); + float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_SELF; + nearest = dist; + player_num = i; + } + } + } + } + if (player_num == INVALID) + { + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + } + } + } + + if (player_num != INVALID) + { + vec3_t noise_origin; + + if (type == PNOISE_WEAPON) + VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); + else if (type == PNOISE_SELF) + VectorCopy(botlib_noises.self_origin[player_num], noise_origin); + else if (type == PNOISE_IMPACT) + VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); + + + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t noise_origin_eyes; + VectorCopy(noise_origin, noise_origin_eyes); + noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level + trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + { + VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + else + goto LookAhead; + } + else + goto LookAhead; + } + // If no enemy, look at nodes in front/behind, or look at map center + else // if (self->enemy == NULL) + { + + LookAhead: + + const int look_ahead = 5; + qboolean found_viable_node = false; + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list + { + /* + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + int look_node = self->bot.node_list[(i + look_ahead)]; + if (nodes[look_node].num_links) + { + int rnd_link = rand() % nodes[look_node].num_links + 1; + int rnd_node = nodes[look_node].links[rnd_link].targetNode; + if (rnd_node != INVALID) + { + VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); + found_viable_node = true; + break; + } + } + } + */ + + // Look x nodes ahead + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + found_viable_node = true; + break; + } + // Look x nodes behind + else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + ////BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + found_viable_node = true; + break; + } + else + break; + } + //if (found_viable_node) + // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + //else + // Com_Printf("%s no node [%d]\n", __func__, level.framenum); + + if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + + if (0 && found_viable_node == false) + { + // If no enemy, look at random thing + { + // If no enemy, look at random item or map center + // Search through map looking for a random item, or NULL + //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) + if (self->bot.bi.look_at_time < level.framenum) + { + self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds + + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); + //if (tr.fraction == 1.0) + { + VectorCopy(map_ents->s.origin, self->bot.bi.look_at); + break; + } + } + } + if (VectorEmpty(self->bot.bi.look_at)) + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + else + VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item + + } + + // If lookdir is empty, then look at map center + if (VectorEmpty(self->bot.bi.look_at)) + { + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + } + + BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, turn_speed, true, true); + } + } +} + +void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) +{ + //if (teamplay->value && lights_camera_action > 0) + // return; + + int next_node = INVALID; // Next node to walk towards + //vec3_t lookdir = { 0 }; // Direction to look + vec3_t walkdir; // Direction to walk + vec3_t lastdir; + float move_speed = SPEED_RUN; // Movement speed, default to running + self->bot.bi.speed = 0; // Zero speed + trace_t tr; // Trace + + // Remove flags + //self->bot.bi.actionflags = 0; + //self->enemy = NULL; + + // Prevent stuck suicide if holding position + if ((self->bot.bi.actionflags & ACTION_HOLDPOS)) + self->suicide_timeout = level.framenum + 10; + + // Check how far we've moved + qboolean moved = true; + VectorSubtract(self->s.origin, self->lastPosition, lastdir); + float move_dist = VectorLength(lastdir); + if (move_dist < FRAMETIME) + moved = false; // We've not moved + + + // If the bot is near the get_item they're after, and the item is inuse + // inuse == false if the item was picked up and waiting to respawn + // inuse == true if the item has spawned in and is ready to be picked up + if (self->bot.get_item != NULL && self->bot.get_item->inuse) + { + float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); + //Com_Printf("%s %s is looking for item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + + // item spawnflags + //#define ITEM_TRIGGER_SPAWN 0x00000001 + //#define ITEM_NO_TOUCH 0x00000002 + // 6 bits reserved for editor flags + // 8 bits used as power cube id bits for coop games + //#define DROPPED_ITEM 0x00010000 + //#define DROPPED_PLAYER_ITEM 0x00020000 + //#define ITEM_TARGETS_USED 0x00040000 + // + // Edict Flags + // #define FL_RESPAWN 0x80000000 // used for item respawning + // + // + // TP item not picked up + // spawnflags = 0 + // flags = 0 + // + // TP item picked up and in bot inventory + // spawnflags = ITEM_TARGETS_USED 0x00040000 + // flags = FL_RESPAWN 0x80000000 + // + // TP item picked up then dropped by bot + // spawnflags = DROPPED_ITEM + // flags = 0 + // + // TP item picked up, bot killed, dropping all items to ground + // spawnflags = DROPPED_PLAYER_ITEM + // flags = 0 + + trace_t tr = gi.trace(self->s.origin, NULL, NULL, self->bot.get_item->s.origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && item_dist <= 128) // Might want to do a trace line to see if bot can actually see item + { + //Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to item + VectorNormalize(walkdir); + + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_NONE) + { + //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + else if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + + if (item_dist < 128) + self->bot.bi.speed = SPEED_ROAM; // Slow down when close + + if (self->bot.get_item->solid == SOLID_NOT); // picked up + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + self->bot.get_item = NULL; + return; + } + + return; + + } + //else if (item_dist <= 64) + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + //self->bot.get_item = NULL; + } + } + + /* + // Get current and next node back from nav code. + if (!BOTLIB_FollowPath(self)) + { + //Com_Printf("%s %s BOTLIB_FollowPath == false\n", __func__, self->client->pers.netname); + //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); + + self->bot.bi.speed = 0; + self->bot.bi.actionflags = 0; + + self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + return; + } + */ + + if (self->bot.touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) + { + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + } + +#if 0 + ///////////////// + // Look dir + ///////////////// + qboolean touching_ladder = false; + //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) + { + // Check if touching ladder + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + //VectorMA(self->s.origin, 1, fwd, end); + //tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_PLAYERSOLID); + + VectorMA(self->s.origin, 1, fwd, end); + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + + if (touching_ladder == false) + { + VectorMA(self->s.origin, 8, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (touching_ladder == false) + { + VectorMA(self->s.origin, 16, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + if (touching_ladder == false) + { + VectorMA(self->s.origin, 32, fwd, end); + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + touching_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + } + } + } + //if (touching_ladder) + // Com_Printf("%s touching_ladder [%d]\n", __func__, level.framenum); + if (touching_ladder == false)// || (nodes[self->bot.next_node].type != NODE_LADDER && self->groundentity)) + { + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + touching_ladder = false; + } + + // Look at ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER)) + { + // Check we're looking at ladder + qboolean looking_at_ladder = false; + { + float yaw_rad = 0; + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + yaw_rad = DEG2RAD(self->s.angles[YAW]); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + looking_at_ladder = ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)); + if (looking_at_ladder) + touching_ladder = true; + } + // Turn to find ladder + if (looking_at_ladder == false) + { + int additional_rotation = 0; + for (int i = 0; i < 16; i++) + { + vec3_t fwd = { 0 }, end = { 0 }; + trace_t tr; + + self->s.angles[YAW] += 22.5; // (22.5 * 16) = 360 + + float yaw_rad = DEG2RAD(self->s.angles[YAW]); + + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + + VectorMA(self->s.origin, 60, fwd, end); + + vec3_t lmins = { -16, -16, -96 }; + vec3_t lmaxs = { 16, 16, 96 }; + + tr = gi.trace(self->s.origin, lmins, lmaxs, end, self, MASK_PLAYERSOLID); + + if ((tr.fraction < 1) && (tr.contents & CONTENTS_LADDER)) // Found ladder + { + touching_ladder = true; + VectorSubtract(end, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 3.0, true, true); + if (additional_rotation >= 1) + break; + additional_rotation++; + } + } + } + + // Look up or down when on ladder + if ((self->bot.current_node != INVALID && self->bot.next_node != INVALID) && (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER)) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + //Com_Printf("%s %s ladder up [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = -45; //-89 + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + //Com_Printf("%s %s ladder down [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Level out at top or bottom + if (VectorDistance(self->s.origin, nodes[self->bot.next_node].origin) <= 64 || + VectorDistance(self->s.origin, nodes[self->bot.current_node].origin) <= 64) + self->bot.bi.viewangles[PITCH] = 0; + else + self->bot.bi.viewangles[PITCH] = 45; // 89 + } + } + } + // Look at enemy + else if (self->enemy) // Track target + { + qboolean not_infront = false; // If target is in front + + // Update bot to look at an enemy it can see. If the enemy is obstructed behind a wall, + // the bot will keep looking in that general direction, but not directly at the enemy's pos + if (self->bot.see_enemies) // || BOTLIB_Infront(self, self->enemy, 0.3) == false) + { + if (BOTLIB_Infront(self, self->enemy, 0.3) == false) + not_infront = true; + + if (1) // Predicted enemy pos + { + // Predict enemy position based on their velocity and distance + vec3_t predicted_enemy_origin = { 0 }; + BOTLIB_PredictEnemyOrigin(self, predicted_enemy_origin, 1); + if (0) // Debug draw predicted enemy origin - blue is predicted, yellow is actual + { + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t yellow = MakeColor(255, 255, 0, 255); // Yellow + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + DrawBox(self->enemy->s.number, predicted_enemy_origin, blue, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + DrawBox(self->enemy->s.number + 64, self->enemy->s.origin, yellow, tv(-16, -16, -24), tv(16, 16, 32), 100, false); + } + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at enemy + /* + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(predicted_enemy_origin[0], predicted_enemy_origin[1], predicted_enemy_origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(predicted_enemy_origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + */ + } + else // Not predicted + { + if (self->enemy->maxs[2] == CROUCHING_MAXS2) // If enemy is crouching + VectorSubtract(tv(self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] - 24), self->s.origin, self->bot.bi.look_at); // Aim lower for crouched enemies + else + VectorSubtract(self->enemy->s.origin, self->s.origin, self->bot.bi.look_at); // Aim at standing enemy + } + } + else // Look where enemy was last + { + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking enemy_seen_loc + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t enemy_eyes; + VectorCopy(self->enemy->s.origin, enemy_eyes); + enemy_eyes[2] += self->enemy->viewheight; // Get our enemy eye level (standing and crouching) + trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + VectorSubtract(self->bot.enemy_seen_loc, self->s.origin, self->bot.bi.look_at); + else + goto LookAhead; // Looking at a wall, so just look at nodes + } + + // Adjust turn speed based on skill + //float turn_speed = (10 - bot_skill->value) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 + if (self->bot.skill >= MAX_BOTSKILL) + { + turn_speed = 0.2; + } + //if (bot_skill_threshold->value > 0 && self->bot.skill >= MAX_BOTSKILL) + { + //turn_speed = 0.1; // Slightly reduce max turn speed when using bot_skill_threshold + } + + if (turn_speed > 1.0) + { + if (not_infront) + turn_speed = 2.0; + + if (self->client->weapon == FindItemByNum(HC_NUM))//FindItem(HC_NAME)) // HC bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(KNIFE_NUM)) // Knife bots get almost instant aim + turn_speed = 0.2; + else if (self->client->weapon == FindItemByNum(GRENADE_NUM)) // Grenade bots get almost instant aim + turn_speed = 0.2; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(M3_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 2 == 0 && self->client->weapon == FindItemByNum(SNIPER_NUM)) // Bots sometimes get aim snap + turn_speed = 0.5; + else if (rand() % 10 == 0) // Bots sometimes get aim snap + turn_speed = 0.5; + } + + //Com_Printf("%s %s turn_speed[%f]\n", __func__, self->client->pers.netname, turn_speed); + + //if (self->client->weaponstate != WEAPON_READY) + // not_infront = rand() % 2; + + if (not_infront) // Just 'pan' left and right (don't pitch up and down until enemy is in view) + { + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, false); + } + else + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, turn_speed, true, true); + } + /* + // When at a POI, get a direction to look at by finding a node NODE_POI_LOOKAT link + else if (self->bot.current_node != INVALID && self->bot.next_node != INVALID && nodes[self->bot.current_node].type == NODE_POI) + { + // See if this node has any POI lookat links + int num_lookat_nodes = 0; + int lookat_nodes[MAXLINKS] = { 0 }; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + if (nodes[self->bot.current_node].links[i].targetNodeType == NODE_POI_LOOKAT) + { + lookat_nodes[num_lookat_nodes] = nodes[self->bot.current_node].links[i].targetNode; + num_lookat_nodes++; + } + } + if (num_lookat_nodes) // One or more POI lookat nodes found + { + // When the timer is up pick a new node to look at + if (self->bot.node_poi_look_time < level.framenum) + { + self->bot.node_poi_look_time = level.framenum + 5 * HZ; + self->bot.node_poi_lookat_node = lookat_nodes[rand() % num_lookat_nodes]; // Pick a random POI lookat node + } + + // Look at POI lookat node + VectorSubtract(nodes[self->bot.node_poi_lookat_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 2.0); + } + } + */ + else if (0 && self->bot.current_node != INVALID && nodes[self->bot.current_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + // If no enemy, have bot look at PNOISES, if any. + else if (1) + { + int type = 0; + int player_num = INVALID; + float nearest = 9999999; + qboolean found_target = false; + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.weapon_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.weapon_origin[i], 0.3)) + { + //Com_Printf("%s %s look at weapon_origin [%d]\n", __func__, self->client->pers.netname, botlib_noises.weapon_time[i]); + float dist = VectorDistance(botlib_noises.weapon_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + if (botlib_noises.self_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.self_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.self_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_SELF; + nearest = dist; + player_num = i; + } + } + } + } + if (player_num == INVALID) + { + for (int i = 0; i < num_players; i++) + { + if (players[i] != self && OnSameTeam(self, players[i]) == false) + { + if (botlib_noises.impact_time[i] > 0 && BOTLIB_MovingToward(self, botlib_noises.impact_origin[i], 0.3)) + { + float dist = VectorDistance(botlib_noises.impact_origin[i], self->s.origin); + if (dist < nearest && dist > 256) + { + type = PNOISE_WEAPON; + nearest = dist; + player_num = i; + } + } + } + } + } + + if (player_num != INVALID) + { + vec3_t noise_origin; + + if (type == PNOISE_WEAPON) + VectorCopy(botlib_noises.weapon_origin[player_num], noise_origin); + else if (type == PNOISE_SELF) + VectorCopy(botlib_noises.self_origin[player_num], noise_origin); + else if (type == PNOISE_IMPACT) + VectorCopy(botlib_noises.impact_origin[player_num], noise_origin); + + + // Test to see if bot is looking at a wall, if so goto LookAhead, otherwise keep tracking noise + vec3_t eyes; + VectorCopy(self->s.origin, eyes); + eyes[2] += self->viewheight; // Get our eye level (standing and crouching) + vec3_t noise_origin_eyes; + VectorCopy(noise_origin, noise_origin_eyes); + noise_origin_eyes[2] += self->viewheight; // Set noise origin to eye level + trace_t tr = gi.trace(eyes, NULL, NULL, noise_origin_eyes, self, MASK_SHOT); // Trace to target + float distance = VectorDistance(self->s.origin, tr.endpos); + if (distance > 256) + { + VectorSubtract(noise_origin_eyes, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + else + goto LookAhead; + } + else + goto LookAhead; + } + // If no enemy, look at nodes in front/behind, or look at map center + else // if (self->enemy == NULL) + { + + LookAhead: + + const int look_ahead = 5; + qboolean found_viable_node = false; + for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list + { + /* + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + int look_node = self->bot.node_list[(i + look_ahead)]; + if (nodes[look_node].num_links) + { + int rnd_link = rand() % nodes[look_node].num_links + 1; + int rnd_node = nodes[look_node].links[rnd_link].targetNode; + if (rnd_node != INVALID) + { + VectorSubtract(nodes[rnd_node].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 0.1); + found_viable_node = true; + break; + } + } + } + */ + + // Look x nodes ahead + if ((i + look_ahead) < self->bot.node_list_count && self->bot.node_list[i + look_ahead] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i + look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + found_viable_node = true; + break; + } + // Look x nodes behind + else if ((i - look_ahead) < self->bot.node_list_count && self->bot.node_list[(i - look_ahead)] != INVALID) + { + VectorSubtract(nodes[self->bot.node_list[(i - look_ahead)]].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + found_viable_node = true; + break; + } + else + break; + } + //if (found_viable_node) + // Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + //else + // Com_Printf("%s no node [%d]\n", __func__, level.framenum); + + if (0 && found_viable_node == false && self->bot.next_node != INVALID && nodes[self->bot.next_node].num_links) + { + //for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) + { + } + //rand() % nodes[self->bot.current_node].num_links + 1; + Com_Printf("%s found_viable_node [%d]\n", __func__, level.framenum); + + VectorSubtract(nodes[rand() % nodes[self->bot.current_node].num_links + 1].origin, self->s.origin, self->bot.bi.look_at); + BOTLIB_ChangeBotAngleYawPitch(self, self->bot.bi.look_at, false, 1.0, true, true); + } + + if (0 && found_viable_node == false) + { + // If no enemy, look at random thing + { + // If no enemy, look at random item or map center + // Search through map looking for a random item, or NULL + //if (self->bot.bi.look_at_ent == NULL || self->bot.bi.look_at_ent_time < level.framenum) + if (self->bot.bi.look_at_time < level.framenum) + { + self->bot.bi.look_at_time = level.framenum + 1 * HZ; // Change target every 5 seconds + + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + //trace_t tr = gi.trace(self->s.origin, NULL, NULL, map_ents->s.origin, self, MASK_SOLID); + //if (tr.fraction == 1.0) + { + VectorCopy(map_ents->s.origin, self->bot.bi.look_at); + break; + } + } + } + if (VectorEmpty(self->bot.bi.look_at)) + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + else + VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item + + } + + // If lookdir is empty, then look at map center + if (VectorEmpty(self->bot.bi.look_at)) + { + VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + } + + BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, 1.0, true, true); + } + } +#endif + + // Do not follow path when teammates are still inside us. + if (OnTransparentList(self)) // Teamplay + { + // If the bot has just spawned, then we need to wait a bit before we start moving. + if (self->just_spawned) // set by SpawnPlayers() in a_team.c + { + self->just_spawned = false; + self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. + + // If enemy is in sight, don't wait too long + if (self->enemy) + self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait + else + { + // Otherwise pick from various wait times before moving out + int rnd_rng = rand() % 4; + if (rnd_rng == 0) + self->just_spawned_timeout = level.framenum + (random() * 10) * HZ; // Long wait + else if (rnd_rng == 1) + self->just_spawned_timeout = level.framenum + (random() * 5) * HZ; // Medium wait + else if (rnd_rng == 2) + self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait + else + self->just_spawned_timeout = 0; // No wait + } + + self->bot.bi.actionflags |= ACTION_HOLDPOS; + return; + } + // Wait + if (self->just_spawned_go && self->just_spawned_timeout > level.framenum && self->bot.see_enemies == false) + { + self->bot.bi.actionflags |= ACTION_HOLDPOS; + return; // It's not time to move yet, wait! + } + // Go! + if (self->just_spawned_go || self->bot.see_enemies) + { + //BOTLIB_PickLongRangeGoal(self); + self->just_spawned_go = false; // Now we can move! + } + } + + + if (self->groundentity && self->bot.next_node == INVALID) // No next node, pick new nav + { + Com_Printf("%s %s next_node is invalid; find a new path\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + // On ground and current or next node is invalid + //if (self->groundentity && (self->bot.current_node == INVALID || self->bot.next_node == INVALID)) + //self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node + if (self->groundentity && self->bot.current_node == INVALID) + { + //self->bot.state = BOT_MOVE_STATE_NAV; + //self->bot.bi.speed = 0; + //return; + + Com_Printf("%s %s on ground and current_node is invalid; try to wander\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.stuck_wander_time = 1; + } + if (self->groundentity && self->bot.goal_node == INVALID)// && self->bot.node_travel_time >= 15) + { + Com_Printf("%s %s on ground and goal_node is invalid; find a new path\n", __func__, self->client->pers.netname); + self->bot.bi.speed = 0; + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + + + + /* + // If next node is INVALID, assume we're stuck + //if (self->bot.next_node == INVALID || self->bot.current_node == INVALID || self->bot.goal_node == INVALID) + if (self->bot.goal_node == INVALID) + { + self->bot.stuck_wander_time = 2; + //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); + } + else */ + next_node = self->bot.next_node; + + /* + if (self->bot.node_travel_time > 30) + { + self->bot.node_travel_time = 0; + + // Wander + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 1.0 * HZ; + self->bot.goal_node = INVALID; + } + */ + + //if (self->bot.goal_node != INVALID && nav_area.total_areas > 0 && self->bot.node_travel_time >= 120) + { + //Com_Printf("%s %s ATTEMPTING TO FIX cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + //if (BOTLIB_CanVisitNode(self, self->bot.goal_node, false, INVALID)) + { + //self->bot.node_travel_time = 0; + //Com_Printf("%s %s FIXED node_travel_time cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + } + + // If travel time took too long, assume we're stuck + if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node + { + self->bot.stuck_wander_time = 1; + Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + + //self->bot.stuck_wander_time = 0; + + if (self->bot.stuck_wander_time)// && nav_area.total_areas <= 0) + { + self->bot.stuck_wander_time--; + self->bot.node_travel_time = 0; + + //Com_Printf("%s %s stuck_wander cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + + //Com_Printf("%s %s stuck_wander BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname); + //BOTLIB_PickLongRangeGoal(self); // pick a new long range goal + // Wander + //if (nav_area.total_areas <= 0) + { + Com_Printf("%s %s stuck_wander\n", __func__, self->client->pers.netname); + self->bot.state = BOT_MOVE_STATE_NAV; + self->bot.goal_node = INVALID; + } + //return; + + + VectorCopy(self->bot.stuck_random_dir, walkdir); + + if (self->groundentity) + { + self->bot.stuck_last_negate = 0; // Reset if we touched ground + } + + float fwd_distance = 32; + tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope + fwd_distance = 128; // Extend the distance we check for a safe direction to move toward + //Com_Printf("%s %s tr.plane.normal[%f] \n", __func__, self->client->pers.netname, tr.plane.normal[2]); + + qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false); + if (can_move == false || VectorEmpty(self->bot.stuck_random_dir)) + { + // Try to aquire a safe random direction to move toward + qboolean success = false; + for (int i = 0; i < 8; i++) + { + if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, true) == false) + continue; + else + { + success = true; + break; + } + } + if (success) + { + VectorCopy(walkdir, self->bot.stuck_random_dir); + // vect to angles + vec3_t angles; + vectoangles(walkdir, angles); + //Com_Printf("%s %s found random direction [%f %f %f]\n", __func__, self->client->pers.netname, angles[0], angles[1], angles[2]); + } + else + { + //Com_Printf("%s %s ACTION_HOLDPOS N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge + if (level.framenum > self->bot.stuck_last_negate) + { + // Only allow this to happen once every 60 seconds + // It's reset after 60 seconds, death, or touching ground again + self->bot.stuck_last_negate = level.framenum + 60 * HZ; + + //Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Reverse the direction + VectorNegate(self->velocity, self->velocity); + } + //self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + //BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + //return; + } + } + else + { + //Com_Printf("%s %s heading for random dir\n", __func__, self->client->pers.netname); + //VectorCopy(walkdir, self->bot.stuck_random_dir); + //VectorCopy(self->bot.stuck_random_dir, walkdir); + } + + vec3_t hordir; + VectorNormalize(walkdir); + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = move_speed; // Set our suggested speed + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_NONE) + { + //self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + //self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + else if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + } + + return; + } + else + { + /* + qboolean fetch_item = false; + if (self->bot.get_item != NULL) + { + float item_dist = VectorDistance(self->bot.get_item->s.origin, self->s.origin); + Com_Printf("%s %s is looking for item %s [%f] sf[0x%x] f[0x%x]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist, self->bot.get_item->spawnflags, self->bot.get_item->flags); + + // TP item not picked up + // spawnflags = 0 + // flags = 0 + // + // TP item picked up and in bot inventory + // spawnflags = ITEM_TARGETS_USED + // flags = FL_RESPAWN + // + // TP item picked up then dropped by bot + // spawnflags = DROPPED_ITEM + // flags = 0 + // + // TP item picked up, bot killed, dropping all items to ground + // spawnflags = DROPPED_PLAYER_ITEM + // flags = 0 + + if (item_dist <= 128) + { + Com_Printf("%s %s is near item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + VectorSubtract(self->bot.get_item->s.origin, self->s.origin, walkdir); // Head to next node + VectorNormalize(walkdir); + fetch_item = true; + + if (item_dist <= 16) + { + Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + self->bot.get_item = NULL; + } + } + //else if (item_dist <= 64) + { + //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); + //self->bot.get_item = NULL; + } + } + */ + //if (fetch_item == false) + { + if (next_node == INVALID) + { + //self->bot.stuck_wander_time = 1; + return; + } + VectorSubtract(nodes[next_node].origin, self->s.origin, walkdir); // Head to next node + VectorNormalize(walkdir); + //VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, walkdir); // Head to next node + } + } + + //qboolean can_move = BOTLIB_CanMoveInDirection(self, walkdir, 32, NODE_MAX_CROUCH_FALL_HEIGHT, false); + //if (can_move == false) + // self->bot.stuck_wander_time = 1; + + if (self->bot.stuck_wander_time) + return; + + + //if (self->bot.state == STATE_WANDER || self->bot.goal_node == INVALID) + // Com_Printf("%s %s [%d] INVALID\n", __func__, self->client->pers.netname, level.framenum); + + //qboolean perform = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + /* + if (perform == false && moved == false) + { + //self->bot.bi.actionflags |= (ACTION_JUMP); // Try jumping anyway + //self->bot.stuck_wander_time = 1; + } + */ + + /* + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + */ + + // Strafe to get back on track + if (0 && self->bot.next_node != INVALID) + { + // Get distance from bot to next node + //float dist = VectorDistance(self->s.origin, nodes[self->bot.next_node].origin); + // Get distanced from current node to next node + //float dist2 = VectorDistance(nodes[self->bot.current_node].origin, nodes[self->bot.next_node].origin); + + byte mov_strafe = 0; + float dot = BOTLIB_DirectionCheck(self, &mov_strafe); + if (dot > 0.7 && dot < 0.99) // .995 + { + //float dist = VectorDistance(self->s.origin, nodes[self->bot.current_node].origin); + //if (dist > 64) + { + BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + VectorSubtract(self->nearest_path_point, self->s.origin, walkdir); // Head to current node + VectorNormalize(walkdir); + vec3_t hordir; + hordir[0] += walkdir[0]; + hordir[1] += walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + } + + + + /* + //move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + + //BOTLIB_UTIL_NEAREST_PATH_POINT(self, ucmd); // Update the nearest path point + float mov_strafe = BOTLIB_UTIL_PATH_DEVIATION(self, ucmd); + + if (mov_strafe > 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + // Get the direction perpendicular to the dir, facing left + vec3_t right; + right[0] = walkdir[0]; + right[1] = -walkdir[0]; + right[2] = 0; + VectorNormalize(right); + VectorCopy(right, self->bot.bi.dir); + move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + } + else if (mov_strafe < 0) + { + //Com_Printf("%s %s [s %f]\n", __func__, self->client->pers.netname, mov_strafe); + // Get the direction perpendicular to the dir, facing left + vec3_t left; + left[0] = -walkdir[1]; + left[1] = walkdir[1]; + left[2] = 0; + VectorNormalize(left); + VectorCopy(left, self->bot.bi.dir); + move_speed = SPEED_ROAM; // Slow down if we're moving in the right direction + } + else + { + move_speed = SPEED_RUN; // Slow down if we're moving in the right direction + } + */ + } + } + + //self->bot.bi.speed = move_speed; // Set our suggested speed + + if (0) + { + // Get current direction + vec3_t angle, forward, right, start, end, origin, offset; + vectoangles(walkdir, angle); + AngleVectors(angle, forward, right, NULL); + VectorCopy(self->s.origin, origin); + //origin[2] -= 24; // From the ground up + origin[2] += 8; // [Origin 24 units] + [8 units] == 32 units heigh (same as node height) + + VectorSet(offset, 0, 0, 0); // changed from 18,0,0 + G_ProjectSource(origin, offset, forward, right, start); + offset[0] += 1024; // Distance forward dir + G_ProjectSource(origin, offset, forward, right, end); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BFG_LASER); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + + if (1 & self->bot.node_list_count) + { + //for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) + for (int i = 1; i < self->bot.node_list_count; i++) + { + if (self->bot.next_node == self->bot.node_list[i]) + { + self->bot.current_node = self->bot.node_list[i - 1]; + break; + } + } + if (self->bot.node_list_count) + { + //self->bot.current_node = self->bot.node_list[self->bot.node_list_current]; + } + } + + // Get current and next node types + int next_node_type = INVALID; + int current_node_type = INVALID; + if (self->bot.current_node != INVALID && self->bot.next_node != INVALID) + { + current_node_type = nodes[self->bot.current_node].type; + + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + int target_node = nodes[self->bot.current_node].links[i].targetNode; + if (target_node == self->bot.next_node) + { + next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + + //self->prev_to_curr_node_type = self->curr_to_next_node_type; // Previous node type + //self->curr_to_next_node_type = nodes[self->bot.current_node].links[i].targetNodeType; // Next node type + break; + } + + /* + // Try search surrounding nodes to see if they contain next_node + for (int j = 0; j < nodes[target_node].num_links; j++) + { + int target_target_node = nodes[target_node].links[j].targetNode; + for (int k = 0; k < nodes[target_target_node].num_links; k++) + { + int target_target_target_node = nodes[target_target_node].links[k].targetNode; + if (target_target_target_node == self->bot.next_node) + { + next_node_type = nodes[target_target_target_node].links[i].targetNodeType; // Next node type + Com_Printf("%s %s using target_target_target_node\n", __func__, self->client->pers.netname); + break; + } + } + } + */ + } + } + + //if (current_node_type == INVALID || next_node_type == INVALID) + if (next_node_type == INVALID) + { + //next_node_type = NODE_MOVE; + //Com_Printf("%s %s invalid types node:curr/next[%d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, current_node_type, next_node_type); + + if (self->bot.next_node == INVALID) + { + Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + if (self->bot.current_node == self->bot.next_node && self->bot.current_node == self->bot.goal_node) + { + Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + ////if (nav_area.total_areas > 0) + //// self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + ////else + self->bot.state = BOT_MOVE_STATE_NAV; + return; + } + else if (self->bot.next_node != INVALID && VectorDistance(nodes[self->bot.next_node].origin, self->s.origin) <= 128) + { + /* + // Try search surrounding nodes to see if they contain next_node + qboolean resolved = false; + for (int i = 0; i < nodes[self->bot.current_node].num_links; i++) //for (i = 0; i < MAXLINKS; i++) + { + if (next_node_type != INVALID) break; + int target_node = nodes[self->bot.current_node].links[i].targetNode; + if (target_node == INVALID) continue; + + for (int j = 0; j < nodes[target_node].num_links; j++) + { + if (next_node_type != INVALID) break; + int target_target_node = nodes[target_node].links[j].targetNode; + if (target_target_node == INVALID) continue; + + for (int k = 0; k < nodes[target_target_node].num_links; k++) + { + int target_target_target_node = nodes[target_target_node].links[k].targetNode; + if (target_target_target_node == INVALID) continue; + + if (target_target_target_node == self->bot.next_node) + { + if (VectorDistance(nodes[target_target_target_node].origin, self->s.origin) <= 128) + { + next_node_type = nodes[target_target_node].links[i].targetNodeType; // Next node type + Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + break; + } + } + } + } + } + */ + + //if (next_node_type != INVALID) + // Com_Printf("%s %s resolved next node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + } + if (next_node_type == INVALID) + { + //Com_Printf("%s %s invalid types node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); + + // Path failed, so try again + //BOTLIB_CanVisitNode(self, self->bot.goal_node, false, nodes[self->bot.goal_node].area); + + /* + if (self->bot.goal_node != INVALID) + BOTLIB_CanGotoNode(self, self->bot.goal_node, false); + else + self->bot.state = BOT_MOVE_STATE_NAV; + */ + + return; + } + + //VectorCopy(nodes[self->bot.goal_node].origin, self->s.origin); + + //self->bot.state = STATE_WANDER; + //self->wander_timeout = level.framenum + 1.0 * HZ; + //self->bot.goal_node = INVALID; + //return; + } + + if (current_node_type != INVALID && next_node_type != INVALID) + { + // DEBUG: print current and next node types + if (0) + { + const qboolean printOnlyErrors = false; // If only printing INVALID nodes + PrintAllLinkNodeTypes(self, printOnlyErrors); + } + + // Set the default movement: this will be overriden by the movement code below + //if (next_node_type == NODE_MOVE || next_node_type == NODE_CROUCH || next_node_type == NODE_JUMP || next_node_type == NODE_BOXJUMP || next_node_type == NODE_LADDER) + { + //* + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + //*/ + //VectorCopy(walkdir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + } + + // move, crouch, or jump + if (next_node_type == NODE_WATER) + { + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_CROUCH; + + //VectorClear(self->bot.bi.dir); + //self->bot.bi.speed = 0; + + VectorNormalize(walkdir); + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = move_speed; // Set our suggested speed + + // Check that we're in the water + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + + // Almost out of air, start heading up to the surface + if ((contents_feet & MASK_WATER) && self->air_finished_framenum < level.framenum + 5) // Move up to get air + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + self->bot.node_travel_time = 0; // Ignore node travel time while we get some air + //Com_Printf("%s %s [%d] water: get air\n", __func__, self->client->pers.netname, level.framenum); + } + else if ((contents_feet & MASK_WATER) && fabs(nodes[self->bot.next_node].origin[2] - self->s.origin[2]) <= 33) // Roughly leveled out + { + //Com_Printf("%s %s [%d] water: level\n", __func__, self->client->pers.netname, level.framenum); + } + else if ((contents_feet & MASK_WATER) && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) // Move up + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + //Com_Printf("%s %s [%d] water: move up\n", __func__, self->client->pers.netname, level.framenum); + } + else if (nodes[self->bot.next_node].origin[2] < self->s.origin[2]) // Move down + { + if (contents_feet & MASK_WATER) + self->bot.bi.actionflags |= ACTION_MOVEDOWN; // In water moving down + else + self->bot.bi.actionflags |= ACTION_CROUCH; // Crouch drop down into water below + + //Com_Printf("%s %s [%d] water: move down\n", __func__, self->client->pers.netname, level.framenum); + } + + } + + // Pull bot in when close to the next node, help guide it in + if (0) + { + if (self->groundentity == false && level.framenum > self->bot.stuck_last_negate) + { + vec3_t bot_to_node; + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node + if (xy_bot_to_next_dist > 32 && xy_bot_to_next_dist <= 128) + { + // Line of sight + tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + self->bot.stuck_last_negate = level.framenum + 1 * HZ; + vec3_t dir; + dir[0] = walkdir[0]; + dir[1] = walkdir[1]; + dir[2] = walkdir[2]; + VectorNormalize(dir); + self->velocity[0] += 50 * dir[0]; + self->velocity[1] += 50 * dir[1]; + self->velocity[2] += 300 * dir[2]; + // Limit velocity + if (self->velocity[2] < -300) + self->velocity[2] = -300; + //Com_Printf("%s %s [%d] directing bot closer to next node\n", __func__, self->client->pers.netname, level.framenum); + } + } + } + } + + if (next_node_type == NODE_MOVE) + { + /* + if (BOTLIB_CanMoveDir(self, walkdir) == false) + { + // We can't move in this direction + Com_Printf("%s %s can't move safely in direction\n", __func__, self->client->pers.netname); + self->bot.stuck_wander_time = 15; + self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + return; + } + */ + /* + float fwd_distance = 32; + tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope + fwd_distance = 128; // Extend the distance we check for a safe direction to move toward + + // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge + if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false) == false); + { + if (level.framenum > self->bot.stuck_last_negate) + { + // Only allow this to happen once every 60 seconds + // It's reset after 60 seconds, death, or touching ground again + self->bot.stuck_last_negate = level.framenum + 60 * HZ; + + Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // Reverse the direction + VectorNegate(self->velocity, self->velocity); + VectorClear(self->bot.bi.dir); + } + } + */ + } + + //if (next_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMP) + // BOTLIB_Crouch_Or_Jump(self, ucmd, dir); + + if (next_node_type == NODE_BOXJUMP) + { + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + self->bot.bi.actionflags |= ACTION_BOXJUMP; + self->bot.bi.actionflags &= ~ACTION_JUMP; + self->bot.bi.actionflags &= ~ACTION_CROUCH; + } + else + { + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + self->bot.bi.actionflags &= ~ACTION_BOXJUMP; + self->bot.bi.actionflags &= ~ACTION_JUMP; + } + } + + // Slow down if we're not on the ground + if (next_node_type == NODE_MOVE) + { + if (self->groundentity == NULL) + { + //self->bot.bi.speed = SPEED_CAREFUL; // Set our speed directly + self->bot.bi.speed = SPEED_ROAM; // Set our speed directly + //Com_Printf("%s %s SPEED_CAREFUL node_travel_time[%d]\n", __func__, self->client->pers.netname, self->bot.node_travel_time); + } + //if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2] + 32) // Going up + //{ + // self->bot.bi.actionflags |= ACTION_JUMP; + //} + else + { + // Remove flags + //self->bot.bi.actionflags &= ~ACTION_CROUCH; + //self->bot.bi.actionflags &= ~ACTION_JUMP; + + //if (nodes[self->bot.next_node].normal[2] > 0.7) // If the next node is flat + //if (random() < 0.1) + // self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + } + if (next_node_type == NODE_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_JUMP; + } + + + if (nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type == NODE_LADDER) + { + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = 100; // Set speed slower for ladders + + if (self->bot.touching_ladder) + { + // Remove flags + self->bot.bi.actionflags &= ~ACTION_ATTACK; // Don't attack when on ladder + + if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Going up + { + self->bot.bi.actionflags |= ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + } + else if (nodes[self->bot.next_node].origin[2] < nodes[self->bot.current_node].origin[2]) // Going down + { + self->bot.bi.actionflags |= ACTION_MOVEDOWN; + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + } + } + else if (nodes[self->bot.next_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Jump to ladder + { + self->bot.bi.actionflags |= ACTION_JUMP; + } + } + /* + //if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) + if (touching_ladder == false && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER) + { + VectorCopy(walkdir, self->bot.bi.dir); + self->bot.bi.speed = 400; // Set speed slower for ladders + + //if (touching_ladder) + { + self->bot.bi.actionflags |= ACTION_JUMP; + + self->bot.bi.actionflags &= ~ACTION_MOVEUP; + self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; + } + } + */ + // Make sure the bot gets off the top and bottom of the ladder + if (self->bot.prev_node != INVALID && nodes[self->bot.prev_node].type == NODE_LADDER && nodes[self->bot.current_node].type == NODE_LADDER && nodes[self->bot.next_node].type != NODE_LADDER && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 4 && self->groundentity == NULL) // + { + if (nodes[self->bot.prev_node].origin[2] < nodes[self->bot.current_node].origin[2] && self->s.origin[2] < nodes[self->bot.current_node].origin[2] + 2) // Getting off ladder at the top + { + //Com_Printf("%s %s MOVE UP\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; + self->bot.bi.actionflags |= ACTION_MOVEUP; + //self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + else if (nodes[self->bot.prev_node].origin[2] > nodes[self->bot.current_node].origin[2]) // Getting off ladder at the bottom + { + //Com_Printf("%s %s MOVE DOWN\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; + //self->bot.bi.actionflags |= ACTION_MOVEDOWN; + //self->bot.bi.actionflags |= ACTION_BOXJUMP; + } + } + //if (self->bot.prev_node != INVALID) + { + //Com_Printf("%s %s types[ %d %d %d ]\n", __func__, self->client->pers.netname, nodes[self->bot.prev_node].type, nodes[self->bot.current_node].type, nodes[self->bot.next_node].type); + } + + if (next_node_type == NODE_JUMP && self->groundentity) + { + //Com_Printf("%s %s ACTION_JUMP\n", __func__, self->client->pers.netname); + self->bot.bi.actionflags |= ACTION_JUMP; + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + } + + // Handle jumping + if (next_node_type == NODE_JUMPPAD) + { + //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + + // Remove flags + self->bot.bi.actionflags &= ~ACTION_CROUCH; + self->bot.bi.actionflags &= ~ACTION_JUMP; + + //self->bot.bi.actionflags = 0; + //VectorClear(self->bot.bi.dir); + + // Bot only applies direction when it's in a falling state + if (self->groundentity || self->velocity[2] > 0) + self->bot.bi.speed = 0; + else + { + //horizontal direction + vec3_t hordir; + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + + self->bot.bi.speed = move_speed; // Set our suggested speed + } + self->bot.bi.actionflags |= ACTION_JUMPPAD; + //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); + + //Com_Printf("%s %s NODE_JUMPPAD\n", __func__, self->client->pers.netname); +#if 0 + // Get distance from bot to node + nodes[self->bot.current_node].origin; + vec3_t bot_to_node; + + VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node + + VectorSubtract(nodes[self->bot.current_node].origin, self->s.origin, bot_to_node); + bot_to_node[2] = 0; + float xy_bot_to_curr_dist = VectorLength(bot_to_node); // Distance from bot to current node + //if (xy_bot_to_curr_dist <= 32) // If close enough to jump pad + + + float distance = 16; + vec3_t bmins = { nodes[self->bot.current_node].absmin[0] + -(distance), nodes[self->bot.current_node].absmin[1] + -(distance), nodes[self->bot.current_node].absmin[2] + -(distance) }; + vec3_t bmaxs = { nodes[self->bot.current_node].absmax[0] + distance, nodes[self->bot.current_node].absmax[1] + distance, nodes[self->bot.current_node].absmax[2] + distance }; + if (BOTLIB_BoxIntersection(self->absmin, self->absmax, bmins, bmaxs)) + { + Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + self->bot.bi.actionflags |= ACTION_JUMPPAD; + //BOTLIB_Jump_Takeoff(self, NULL, nodes[self->bot.next_node].origin, self->viewheight, self->velocity); + + // Trace up to see if we're going to hit our head + tr = gi.trace(self->s.origin, tv(-16, -16, -0), tv(16, 16, 32), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] + 60), self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Trace down to see if we hit the ground + tr = gi.trace(self->s.origin, tv(-32, -32, -0), tv(32, 32, 0), tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 60), self, MASK_PLAYERSOLID); + qboolean is_player = false; + if (tr.ent && tr.ent->client) + is_player = true; + + /* + // If landing on flat ground, only allow jumping when once we touch the ground + // Otherwise if its a slope, allow jumping when we're close'ish to the ground + qboolean can_jump = true; + //if (tr.plane.normal[2] == 1 && self->groundentity == NULL && self->velocity[2] < 0) // flat ground + if (self->groundentity == NULL && self->velocity[2] < 0) // flat ground + { + can_jump = false; + self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + Com_Printf("%s %s ACTION_HOLDPOS\n", __func__, self->client->pers.netname); + } + */ + + if ((tr.fraction < 1 || tr.startsolid) && is_player == false) // && can_jump) + { + //float height_diff = nodes[self->bot.next_node].origin[2] - self->s.origin[2]; // Height difference between bot and next node + //if (xy_bot_to_next_dist <= 32 && height_diff <= 60) + { + // Bot jumped + //Com_Printf("%s %s jumped\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMP; + } + //else + { + // Trace from bot to node to see if we can jump to it + //tr = gi.trace(self->s.origin, tv(-24, -24, -STEPSIZE), tv(24, 24, 96), nodes[self->bot.current_node].origin, self, MASK_PLAYERSOLID); + //if (tr.fraction == 1.0) + { + //Com_Printf("%s %s jump takeoff\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_JUMPPAD; // Stop moving + } + } + } + } + } +#endif + //if (moved == false) + } + } + + if (ctf->value) + { + // Slow down if near flag + if (bot_ctf_status.flag1_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin) < 128) + { + self->bot.bi.speed = 200; + } + if (bot_ctf_status.flag2_curr_node != INVALID && VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin) < 128) + { + self->bot.bi.speed = 200; + } + } + + if (self->bot.see_enemies) + { + qboolean dodging = ((rand() % 10) < 7); + + // Don't dodge if < x links on node - low link count might indicate a tight walk or narrow way + if (nodes[self->bot.current_node].num_links < 4 || nodes[self->bot.next_node].num_links < 4) + dodging = false; + + // Don't dodge if bot is taking a direct path + if (self->bot.node_random_path == false) + dodging = false; + + if (ctf->value) // Reduce dodging in CTF + { + //float f1 = BOTLIB_DistanceToFlag(self, FLAG_T1_NUM); + //float f2 = BOTLIB_DistanceToFlag(self, FLAG_T2_NUM); + + float f1 = 99999999; + float f2 = 99999999; + if (bot_ctf_status.flag1_curr_node != INVALID) + f1 = VectorDistance(nodes[bot_ctf_status.flag1_curr_node].origin, self->s.origin); + if (bot_ctf_status.flag2_curr_node != INVALID) + f2 = VectorDistance(nodes[bot_ctf_status.flag2_curr_node].origin, self->s.origin); + + if (f1 < 1500 || f2 < 1500 || BOTLIB_Carrying_Flag(self)) // || self->bot.goal_node == bot_ctf_status.flag1_curr_node || self->bot.goal_node == bot_ctf_status.flag2_curr_node) + { + dodging = false; + //Com_Printf("%s %s dodging is OFF ------ \n", __func__, self->client->pers.netname); + } + + //dodging = false; + } + + // Try strafing around enemy + trace_t tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_SHOT); + if (dodging && tr.plane.normal[2] > 0.85) // Not too steep + { + // Try strafing continually in a general direction, if possible + static int max_strafe_left = -10; + static int max_strafe_right = 10; + if (self->bot_strafe == 0) // Pick new direction + { + float strafe_choice = random() < 0.333; + if (strafe_choice < 0.33) // Left + self->bot_strafe = -1; + else if (strafe_choice < 0.66) // Right + self->bot_strafe = 1; + else + self->bot_strafe = 0; // Neither - skip strafing this turn + } + + if (self->client->weapon == FindItem(HC_NAME)) + self->bot_strafe = 0; // Don't strafe with HC, just go straight for them + + if (self->bot_strafe < 0 && random() > 0.15) // Going left 85% of the time + { + if (BOTLIB_CanMove(self, MOVE_LEFT) && self->bot_strafe >= max_strafe_left) // Can go left with a limit + { + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe--; + self->bot.bi.actionflags |= ACTION_MOVELEFT; + } + else if (BOTLIB_CanMove(self, MOVE_RIGHT)) // Cannot go left anymore, so try going right + { + self->bot_strafe = 1; // Go right + self->bot.bi.actionflags |= ACTION_MOVERIGHT; + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else if (self->bot_strafe > 0 && random() > 0.15) // Going right 85% of the time + { + if (BOTLIB_CanMove(self, MOVE_RIGHT) && self->bot_strafe <= max_strafe_right) // Can go right with a limit + { + //Com_Printf("%s %s strafe [RIGHT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + self->bot_strafe++; + self->bot.bi.actionflags |= ACTION_MOVERIGHT; + } + else if (BOTLIB_CanMove(self, MOVE_LEFT)) // Cannot go right anymore, so try going left + { + self->bot_strafe = -1; // Go left + self->bot.bi.actionflags |= ACTION_MOVELEFT; + //Com_Printf("%s %s strafe [LEFT] %d\n", __func__, self->client->pers.netname, self->bot_strafe); + } + else + self->bot_strafe = 0; // Could not go either direction, so skip strafing this turn and reset back to random choice + } + else + self->bot_strafe = 0; // Skip strafing this turn + + // Back off if getting too close (unless we have a HC or knife) + if (self->bot.enemy_dist < 256) + { + if (self->client->weapon == FindItem(HC_NAME) && self->client->cannon_rds) + { + // Come in close for the kill + if (ACEMV_CanMove(self, MOVE_FORWARD)) + self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + else if (self->client->weapon == FindItem(KNIFE_NAME)) + { + // Come in close for the kill + if (ACEMV_CanMove(self, MOVE_FORWARD)) + self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + // Try move backwards + else if (BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + } + // If distance is far, consider crouching to increase accuracy + else if (self->bot.enemy_dist > 1024) + { + // Check if bot should be crouching based on weapon and if strafing + if (INV_AMMO(self, LASER_NUM) == false && + self->client->weapon != FindItem(SNIPER_NAME) && + self->client->weapon != FindItem(HC_NAME) && + self->client->weapon != FindItem(M3_NAME) && + (self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && + (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) + { + // Raptor007: Don't crouch if it blocks the shot. + float old_z = self->s.origin[2]; + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + //Com_Printf("%s %s crouch shooting\n", __func__, self->client->pers.netname); + } + self->s.origin[2] = old_z; + } + } + else + { + // Keep distance with sniper + if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(SNIPER_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + // Keep distance with grenade + if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + { + self->bot.bi.actionflags |= ACTION_MOVEBACK; + } + // Otherwise move toward target + //else if (ACEMV_CanMove(self, MOVE_FORWARD)) + // self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + } + + // If the bot is dodging by strafing, add in some random jumps + if (self->bot_strafe != 0 && (self->bot.bi.actionflags & ACTION_CROUCH) == 0 && random() < 0.2) + self->bot.bi.actionflags |= ACTION_BOXJUMP; // Jump while dodging + } + } + + int perform_action = BOTLIB_Crouch_Or_Jump(self, ucmd, walkdir); // Crouch or jump if needed + if (perform_action == ACTION_JUMP) + { + self->bot.bi.actionflags |= ACTION_JUMP; // Add jumping + self->bot.bi.actionflags &= ~ACTION_CROUCH; // Remove crouching + self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad + } + else if (perform_action == ACTION_CROUCH) + { + self->bot.bi.actionflags |= ACTION_CROUCH; // Add crouching + self->bot.bi.actionflags &= ~ACTION_JUMP; // Remove jumping + self->bot.bi.actionflags &= ~ACTION_JUMPPAD; // Remove jumppad + } + + // Stay on course by strafing back to the path line (if not already strafing) + //if (VectorLength(self->velocity) < 37) // If stuck + if (moved == false) + { + // Try strafing + qboolean right_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, 8); // Check right + qboolean left_blocked = BOTLIB_TEST_FORWARD_VEC(self, walkdir, 64, -8); // Check left + if (right_blocked && left_blocked) // Both are blocked + { + self->bot.bi.actionflags |= ACTION_JUMP; // Try jumping to clear the obstacle + //Com_Printf( "%s %s blocked\n", __func__, self->client->pers.netname); + + + /* + // Bot stuck - Test of bot has LoS to next node + if (self->bot.next_node == INVALID) + { + self->bot.stuck = true; + return; + } + trace_t tr = gi.trace(self->s.origin, tv(-4, -4, 0.1), tv(4, 4, 56), nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1.0) + { + if (VectorLength(self->velocity) < 5) + self->bot.stuck = true; + } + */ + //self->bot.bi.actionflags |= ACTION_MOVEFORWARD; + //self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); + //self->bot.bi.viewangles[PITCH] = 0; + } + else if (right_blocked && BOTLIB_CanMove(self, MOVE_LEFT)) // Strafe left + { + // Get the direction perpendicular to the dir, facing left + vec3_t left; + left[0] = -walkdir[1]; + left[1] = walkdir[0]; + left[2] = 0; + VectorNormalize(left); + VectorCopy(left, self->bot.bi.dir); + + + //Com_Printf("%s %s ACTION_MOVELEFT\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_MOVELEFT; + } + else if (left_blocked && BOTLIB_CanMove(self, MOVE_RIGHT)) // Strafe right + { + // Get the direction perpendicular to the dir, facing right + vec3_t right; + right[0] = walkdir[1]; + right[1] = -walkdir[0]; + right[2] = 0; + VectorNormalize(right); + VectorCopy(right, self->bot.bi.dir); + + //Com_Printf("%s %s ACTION_MOVERIGHT\n", __func__, self->client->pers.netname); + //self->bot.bi.actionflags |= ACTION_MOVERIGHT; + } + else + { + //Com_Printf("%s %s moved == false and not blocked on side\n", __func__, self->client->pers.netname); + //if (VectorLength(self->velocity) < 5) + // self->bot.stuck = true; + } + } + + /* + // Project forward looking for walls + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 48, forward, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); + + // If we hit an obstacle or we've not moved much, turn around + if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37) + { + self->bot.bi.viewangles[YAW] += 22.5 + (random() * 270); + self->bot.bi.viewangles[PITCH] = 0; + } + */ +} +//rekkie -- Quake3 -- e + +//////////////////// +// Wandering code // +//////////////////// +// +// Basic wandering code, simply here as a backup to help the bot find a nearby node +// +void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd) +{ +#if 0 + vec3_t start, end; + vec3_t forward, right; + vec3_t offset; + + // Do not move + if (self->next_move_time > level.framenum) + return; + + //Com_Printf("%s %s wandering...\n", __func__, self->client->pers.netname); + + // Special check for elevators, stand still until the ride comes to a complete stop. + if (self->groundentity && (VectorLength(self->groundentity->velocity) >= 8)) + { + // only move when platform not + if (self->groundentity->moveinfo.state == STATE_UP || self->groundentity->moveinfo.state == STATE_DOWN) + { + self->velocity[0] = 0; + self->velocity[1] = 0; + self->velocity[2] = 0; + self->next_move_time = level.framenum + 0.5 * HZ; + return; + } + } + + // Contents check + vec3_t temp = { 0,0,0 }; + VectorCopy(self->s.origin, temp); + temp[2] += 22; + int contents_head = gi.pointcontents(temp); + temp[2] = self->s.origin[2] - 8; + int contents_feet = gi.pointcontents(temp); + + // Just try to keep our head above water. + if (contents_head & MASK_WATER) + { + // If drowning and no node, move up + if (self->client->next_drown_framenum > 0) + { + ucmd->upmove = SPEED_RUN; + self->s.angles[PITCH] = -45; + } + else + ucmd->upmove = SPEED_WALK; + } + + // Don't wade in lava, try to get out! + if (contents_feet & (CONTENTS_LAVA | CONTENTS_SLIME)) + ucmd->upmove = SPEED_RUN; + + + + // Check speed + VectorSubtract(self->s.origin, self->lastPosition, temp); + float moved = VectorLength(temp); + + if (contents_feet & MASK_WATER) + { + self->bot.goal_node = INVALID; + self->bot.current_node = INVALID; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: GOAL INVALID\n", __func__, self->client->pers.netname); + } + + + + // Crouch or jump + trace_t tr_lower_body; + trace_t tr_upper_body; + //vec3_t fwd; + //AngleVectors(self->s.angles, fwd, NULL, NULL); // project forward from origin + //VectorMA(self->s.origin, 50, fwd, fwd); + //tr_lower_body = gi.trace(self->s.origin, tv(-15, -15, -15), tv(15, 15, 0), forward, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + //tr_upper_body = gi.trace(self->s.origin, tv(-15, -15, 0), tv(15, 15, 32), forward, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 50, forward, end); + tr_lower_body = gi.trace(start, tv(-15, -15, -15), tv(15, 15, 0), end, self, MASK_DEADSOLID); // Box test [feet to mid body] -> forward 16 units + tr_upper_body = gi.trace(start, tv(-15, -15, 0), tv(15, 15, 32), end, self, MASK_DEADSOLID); // Box test [mid body to head] -> forward 16 units + + // Need to crouch? + // Lower body is free, upper body is blocked + if (tr_lower_body.fraction == 1.0 && tr_upper_body.fraction < 1.0) + { + ucmd->upmove = -400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: crouching\n", __func__, self->client->pers.netname); + return; + } + // Need to jump? + // Lower body is blocked, upper body is free + if (tr_lower_body.fraction < 1.0 && tr_upper_body.fraction == 1.0) + { + if (contents_feet & MASK_WATER) + { + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: jumping out of water\n", __func__, self->client->pers.netname); + self->s.angles[PITCH] = -45; + //VectorScale(end, 50, self->velocity); + ucmd->forwardmove = 400; + //ucmd->upmove = 400; + if (self->velocity[2] < 350) + self->velocity[2] = 350; + return; + } + + ucmd->upmove = 400; + ucmd->forwardmove = SPEED_RUN; + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: jumping\n", __func__, self->client->pers.netname); + + return; + } + // In water facing a ladder + //if ((contents_feet & MASK_WATER) && tr_lower_body.fraction < 1.0 && tr_upper_body.fraction < 1.0) + if ((contents_feet & MASK_WATER) && ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER))) + { + //if ((tr_lower_body.contents & CONTENTS_LADDER) || (tr_upper_body.contents & CONTENTS_LADDER)) + { + //if (debug_mode) + //Com_Printf("%s %s is stuck [Wander]: MASK_WATER -> CONTENTS_LADDER\n", __func__, self->client->pers.netname); + ucmd->forwardmove = 400; + ucmd->upmove = 400; + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY * 3, NODE_ALL); + self->s.angles[PITCH] = -45; + if (self->velocity[2] < 350) + self->velocity[2] = 350; + return; + } + } + + + + // Project forward looking for walls + //vec3_t start, end; + //vec3_t forward, right; + //vec3_t offset; + AngleVectors(self->client->v_angle, forward, right, NULL); + VectorSet(offset, 0, 7, self->viewheight - 8); + offset[1] = 0; + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorMA(start, 48, forward, end); + trace_t tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID); + + // If we hit an obstacle or we've not moved much, turn around + if (tr.fraction < 1.0 || VectorLength(self->velocity) < 37 || moved < FRAMETIME) + { + self->s.angles[YAW] += 22.5 + (random() * 270); + self->s.angles[PITCH] = 0; + + if (contents_feet & MASK_WATER) // Just keep swimming. + ucmd->forwardmove = SPEED_RUN; + else if (!M_CheckBottom(self) && !self->groundentity) // if there is ground continue otherwise wait for next move + ucmd->forwardmove = 0; + else if (ACEMV_CanMove(self, MOVE_FORWARD)) + ucmd->forwardmove = SPEED_WALK; + + return; + } + + // Otherwise try move forward normally + if (ACEMV_CanMove(self, MOVE_FORWARD) || (contents_feet & MASK_WATER)) + { + ucmd->forwardmove = SPEED_RUN; + } + + //if (self->client->leg_damage > 0) // Do we have leg damage? + // return; + + // If no goal, try to get a new one + if (self->bot.goal_node == INVALID) + ACEAI_PickLongRangeGoal(self); + + // Try to move to our goal if we can + if (self->bot.goal_node != INVALID) + { + // Update the node we're near + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); + //int tmp_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY*3, NODE_ALL); + //if (tmp_node != self->bot.current_node) + //{ + // self->bot.prev_node = self->bot.current_node; + // self->bot.current_node = tmp_node; + // //Com_Printf("%s %s prev_node %d -> curr node %d\n", __func__, self->client->pers.netname, self->bot.prev_node, self->bot.current_node); + //} + + if (self->bot.current_node == INVALID) + { + if (debug_mode) + Com_Printf("%s %s could not find FindClosestReachableNode to reach goal %d. Wandering...\n", __func__, self->client->pers.netname, self->bot.goal_node); + self->bot.state = STATE_WANDER; + self->wander_timeout = level.framenum + 0.1 * HZ; + return; + } + if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + { + if (debug_mode) + Com_Printf("%s %s Wander finding alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); + //BOTLIB_SetGoal(self, self->bot.goal_node); + //return; + + self->bot.next_node = self->bot.current_node; + self->bot.state = STATE_POSITION; + } + else // We couldn't visit the goal node, so lets pick a new goal + { + if (debug_mode) + Com_Printf("%s %s Wander cannot visit goal %d. Picking new goal\n", __func__, self->client->pers.netname, self->bot.goal_node); + ACEAI_PickLongRangeGoal(self); + } + } +#endif +} diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index afec7a1c5..9dd2f706b 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -1,3642 +1,3642 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - - - //== GLOBAL SEMAPHORE == -int antSearch; - -short int** path_table; -nav_area_t nav_area; - -qboolean nodeused[MAX_PNODES]; // This is used for a FAST check if the node has been used -short int nodefrom[MAX_PNODES]; // Stores how we got here once the node is closed -float nodeweight[MAX_PNODES]; // The weight of the node based on distance - -/* ========================================================= -The basic system works by using a single linked list and accessing information from the node array - -1) The current node is found -2) All links from it are propogated - if not already done by another node -3) If we haven't found the target node then we get the next open node and go to 1 -4) If we have the target node we return the path to it -5) If we run out of nodes then we return INVALID - -This idea is based on "Path Finding Via 'Ant Races' (a floodfill algorithm)" -by Richard Wesson. It is easy to optimise this code to make it even more efficient. - -============================================================= */ - - -//========================= -// Init the search path variables -//========================= -void AntInitSearch(edict_t* ent) -{ - //Make sure the lists and arrays used are all set to the correct values and EMPTY! - memset(nodeused, 0, sizeof(nodeused)); - memset(nodefrom, INVALID, sizeof(nodefrom)); - while (!SLLempty(&ent->pathList)) - { - SLLpop_front(&ent->pathList); - } -} - -///////////////////////////////////////////////////////// -// Check if any bot is navigating to the selected node // -///////////////////////////////////////////////////////// -qboolean Botlib_Nav_NodeInUse(edict_t* self, int node) -{ - //Com_Printf("%s ===============\n", __func__); - - if (node == INVALID) - return false; - - short int i; - for (i = 0; i < num_players; i++) - { - if (players[i]->is_bot == false || players[i] == self || players[i]->solid == SOLID_NOT || ACEAI_IsEnemy(self, players[i]) == false) - continue; - - botlib_sll_t* list = &players[i]->pathList; - if (list == NULL || list->head == NULL) // No path, or no head - continue; - - botlib_sll_nodes_t* sll_node = list->head; - - // Setup nodes - int current_node = players[i]->bot.current_node; - int next_node = sll_node->node; - - // Loop through the linked list - while (sll_node != NULL) // If no next - { - // Check if we're done, also sanity check - if (current_node == players[i]->bot.goal_node || current_node == INVALID || next_node == INVALID) - break; - - if (next_node == node) - { - //if (players[i]->client) - // Com_Printf("%s node[%d] is in use by %s\n", __func__, node, players[i]->client->pers.netname); - return true; - } - - //Com_Printf("%s curr[%d] next[%d] goal[%d]\n", __func__, current_node, next_node, self->bot.goal_node); - - - // Get next node - current_node = next_node; - if ((sll_node = sll_node->next) == NULL) // No next node - break; - next_node = sll_node->node; - - } - } - - //Com_Printf("%s ===============\n\n", __func__); - - return false; -} - -//========================= -// StartSearch -//========================= -// -// returns true if a path is found -// false otherwise -// -int quick = 0; -int normal = 0; -qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization) -{ - // Safety first! - if (from == INVALID || to == INVALID) - return false; - - //rekkie -- Dijkstra pathing -- s - if (1) - { - //clock_t clock_begin = clock(); // Start performance clock - //if (AntQuickPath(ent, from, to)) - { - //Com_Printf("%s AntQuickPath()\n", __func__); - - //clock_t clock_end = clock(); - //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; - //if (res == 0) quick++; - //Com_Printf("%s execution took %f seconds. Quick %d\n", __func__, res, quick); - - //return true; - } - //else - { - //BOTLIB_THREADED_DijkstraPath(ent, from, to); - if (BOTLIB_DijkstraPath(ent, from, to, path_randomization)) - //if (BOTLIB_DijkstraHeightPath(ent, from, to, path_randomization)) - //if (BOTLIB_HighestPath(ent, from, to, path_randomization)) - { - //Com_Printf("%s BOTLIB_DijkstraPath()\n", __func__); - - //clock_t clock_end = clock(); - //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; - //if (res == 0) normal++; - //Com_Printf("%s execution took %f seconds. Normal %d\n", __func__, res, normal); - - return true; - } - } - - return false; - } - //rekkie -- Dijkstra pathing -- e - - - //@@ TESTING ONLY!! antSearch always available - antSearch = 1; - // Check we're allowed to search - if so, do it - if (1)// (antSearch > 0) && (ent->antLastCallTime < level.framenum - ANT_FREQ) ) - { - //Com_Printf("%s AntFindPath()\n", __func__); - // Decrement the semaphore to limit calls to this function - //@@ If we ever get multithreading then we can increment later - antSearch--; - // make a note of when this bot last made a path call - ent->antLastCallTime = level.framenum; - // Set up the lists - AntInitSearch(ent); - // If we found a path - if (AntFindPath(ent, from, to)) - { - // pathList now contains the links in reverse order - return true; - } - } - - // We can use the quick node search method here to get our path and put it in the pathList - // the same way we do with the AntSearch mode. This will have the side effect of finding - // bad paths and removing them. - if (AntQuickPath(ent, from, to)) - { - //Com_Printf("%s AntQuickPath()\n", __func__); - return true; - } - // If not allowed to search and no path - AntInitSearch(ent); // Clear out the path storage - return false; -} - -//================================= -// QuickPath -//================================= -// -// Uses the old path array to get a quick answer and removes bad paths -// - -qboolean AntQuickPath(edict_t* ent, int from, int to) -{ - int newNode = from; - int oldNode = 0; - int loopProtection = 0; //rekkie -- DEV_1 - - // Clean out the arrays, etc. - AntInitSearch(ent); - nodeused[from] = true; - // Check we can get from->to and that the path is complete - while (newNode != INVALID) - { - oldNode = newNode; - // get next node - newNode = path_table[newNode][to]; - if (newNode == to) - { - // We're there - store it then build the path - nodeused[newNode] = true; - nodefrom[newNode] = oldNode; - break; - } - else if (newNode == INVALID) - { - // We have a bad path - break; - } - else if (!nodeused[newNode]) - { - // Not been here yet - store it! - nodeused[newNode] = true; - nodefrom[newNode] = oldNode; - } - else - break; // LOOP encountered - } - - // If successful, build the pathList - if (newNode == to) - { - SLLpush_front(&ent->pathList, to); - while (newNode != from) - { - //rekkie -- s - if (newNode >= numnodes || newNode < 0) - { - if (debug_mode) - gi.dprintf("%s newNode out of bounds!\n", __func__); - return false; - } - //rekkie -- e - - // Push the - SLLpush_front(&ent->pathList, nodefrom[newNode]); - newNode = nodefrom[newNode]; - } - return true; - } - // else wipe out the bad path! - else - { - newNode = oldNode; - while (newNode != from) - { - path_table[nodefrom[newNode]][to] = INVALID; - - //rekkie -- DEV_1 -- s - if (loopProtection < numnodes) - loopProtection++; - else - { - if (debug_mode) - debug_printf("%s ------------------- Bad while loop. Forcing exit.\n", __func__); - break; - } - //rekkie -- DEV_1 -- e - } - path_table[from][to] = INVALID; - } - return false; -} - -//rekkie -- ZigZag pathing -- s -// The bot will zig and zag from node to node, trying to avoid enemy fire -qboolean BOTLIB_ZigZagPath(edict_t* ent, int from, int to) -{ - //Com_Printf("%s [%d]\n", __func__, level.framenum); - - // Sanity check - if (from == INVALID || to == INVALID) - return false; - - AntInitSearch(ent); // Clear out the path storage - for (int i = 0; i < numnodes; i++) - nodeweight[i] = NAV_INFINITE; - - int newNode = INVALID; // Stores the node being tested - int atNode = from; // Current node we're visiting - botlib_sll_t openList; // Locally declared OPEN list - openList.head = openList.tail = NULL; // Init the list - - float weight; // Weight of the node we're visiting - nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) - nodefrom[atNode] = atNode; // Set the parent node to itself - //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list - SLLpush_back(&openList, from); // Store it - - - while (!SLLempty(&openList)) // While there are nodes on the OPEN list - { - atNode = SLLfront(&openList); // Get the next node - if (atNode == to) // If the next node is the goal node - { - //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); - break; // We found a path - } - - - // Update node weights, but don't add them to the list - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - - if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) - { - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, newNode); // Add it to the open list - } - } - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - } - - - // Free up the memory we allocated - SLLdelete(&openList); - - // Optimise stored path with this new information - if (newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order - // so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode); - - //rekkie -- DEV_1 -- s - // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID - // Check if INVALID to avoid accessing [-1] array - if (nodefrom[to] != INVALID) - //rekkie -- DEV_1 -- e - path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path - - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); - //int prev_newNode = newNode; - - // We earlier set our start node to INVALID to set up the termination - // Check if there is a path, and it's not the node we're standing on (safety check) - while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) - { - //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); - //prev_newNode = newNode; - - // Push it onto the pathlist - SLLpush_front(&ent->pathList, newNode); - // Set the path in the node array to match this shortest path - path_table[nodefrom[newNode]][to] = newNode; - - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); - } - - //Com_Printf("%s EXIT PATH\n\n", __func__); - - // Each time a path is found, make a copy - ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list - ent->bot.node_list_current = 0; - if (ent->bot.node_list_count) // Set the current and next nodes - { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; - } - - if (0) - { - Com_Printf("%s [%d] s[%d] g[%d] node_list[", __func__, level.framenum, from, to); - for (int i = 0; i < ent->bot.node_list_count; i++) - { - Com_Printf(" %d ", ent->bot.node_list[i]); - } - Com_Printf(" ]\n"); - } - - //Com_Printf("%s [%d] found a path\n", __func__, level.framenum); - return true; - } - - //Com_Printf("%s failed to find a path\n", __func__); - return false; -} - -//rekkie -- Dijkstra height pathing -- s -#if 0 -// Function unfinished. Wanted bots to prefer taking higher paths. -qboolean BOTLIB_DijkstraHeightPath(edict_t* ent, int from, int to, qboolean path_randomization) -{ - //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - - // Sanity check - if (from == INVALID || to == INVALID) - return false; - - - - AntInitSearch(ent); // Clear out the path storage - for (int i = 0; i < numnodes; i++) - nodeweight[i] = NAV_INFINITE; - - int newNode = INVALID; // Stores the node being tested - int atNode = from; // Current node we're visiting - botlib_sll_t openList; // Locally declared OPEN list - openList.head = openList.tail = NULL; // Init the list - - float weight; // Weight of the node we're visiting - nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) - nodefrom[atNode] = atNode; // Set the parent node to itself - SLLpush_back(&openList, from); // Store it - - while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list - { - atNode = SLLfront(&openList); // Get the next node - if (atNode == to) // If the next node is the goal node - { - break; // We found a path - } - - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; // Get the weight of the node we're visiting + the cost to the new node - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[newNode]) - { - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, newNode); // Store it - } - } - - if (newNode == to) // If node being linked is the goal node - { - //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); - break; // We found a path - } - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - } - - // Free up the memory we allocated - SLLdelete(&openList); - - // Optimise stored path with this new information - if (newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode); - - // We earlier set our start node to INVALID to set up the termination - // Check if there is a path, and it's not the node we're standing on (safety check) - while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) - { - SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist - } - - // Each time a path is found, make a copy - ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list - ent->bot.node_list_current = 0; - if (ent->bot.node_list_count) // Set the current and next nodes - { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; - } - - return true; - } - - //Com_Printf("%s failed to find a path\n", __func__); - return false; -} -#endif -//rekkie -- Dijkstra Height Pathing -- e - -#if 0 -qboolean BOTLIB_NodeCanSeeEnemyPath(edict_t* ent, int atNode) -{ - // Check if node path can be seen by enemies - if (nodes[atNode].num_vis_nodes) // && random() <= 0.2) // && ent->bot.enemies_num && random() <= 0.2) - { - for (int v = 0; v < nodes[atNode].num_vis_nodes; v++) // each vis node - { - /* - for (int e = 0; e < num_players; e++) // each enemy - { - if (OnSameTeam(ent, players[e])) continue; - - for (int nl = 0; nl < players[e]->bot.node_list_count; nl++) // each node within the enemy's path - { - if (players[e]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? - { - return true; - } - } - } - */ - - /* - for (int e = 0; e < ent->bot.enemies_num; e++) // each enemy - { - for (int nl = 0; nl < players[ent->bot.enemies[e]]->bot.node_list_count; nl++) // each node within the enemy's path - { - if (players[ent->bot.enemies[e]]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? - { - return true; - } - } - } - */ - - if (ent->enemy) - { - for (int nl = 0; nl < ent->enemy->bot.node_list_count; nl++) // each node within the enemy's path - { - if (ent->enemy->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? - { - return true; - } - } - } - } - } - return false; -} -#endif - - -// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -// Pathing is working, however some things to add,fix,do... -// -// 2) Also need to test ALL failure points, and try to trigger failures and test if its handled correctly. -// -// 3) Cache all nodes in each area. Leave room for an extra node (explained below what we use the extra for) -// -// 4) Currently when BOTLIB_CanVisitNode() is called, it will search all nodes. I need this to search and path only the -// areas which are involved in the search. Currently the next_node/edge node is located outside the current area of the bot, -// and just inside the next area (the border). Therefore, I should include that extra node in the search. So if we're in Area 0, -// and the next node is in Area 1 (next to Area 0), then search will be all nodes in Area 0 + the single node from Area 1. -// Once the search + path is conducted, remove the node from the search. -// -// 5) Deal with situtations where the areas are connected, but the goal node is not accessible in the last area. Perhaps after checking -// if the areas connect, run a BOTLIB_CanVisitNode() search on the last area to see if it connects from the last edge node -// to the final goal node. ALSO, there might a scenario where the random selected last edge node does not connect, but one of the -// many other edge nodes might? I guess in such a case, a connection might path through the other edge nodes anyway. -// -// 6) When selecting a path from nav_area.dfs_paths[self->bot.path_taken][i], the paths are a series of contigous areas A>B>C>D,etc -// I need an algorithm (similar to the heightmap) to see if areas are being overly used, and/or taking the higher area is preferred -// over a lower area. I could also cache area combos and check agains them. Going back to the heightmap idea, if area C (middle TJ) -// is being overly used, just check if any of the paths contain 'C' and avoid using them? -// -// 7) When picking a random edge node, perhaps I could pick the closest edge node to the current node, thereby making it a shortest path -// by direct line. However, this isn't the shortest 'path', only a direct line. -// -// 8) Check if other bots are using the edge node, if so, pick another edge node if available. -// -// 9) Move some of the arrays over to dynamic allocated memory, build the array as a single block (single malloc call) -// -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] -int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] - -// Init data used when selecting nodes -void BOTLIB_InitAreaNodes(void) -{ - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) - { - DFS_area_nodes[a][n] = INVALID; - } - } - - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) - { - DFS_area_edges[a][e] = INVALID; - } - } -} - -void BOTLIB_InitAreaConnections() -{ - // For all areas, find all their connecting neighbors. - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - nav_area.adjacency_matrix[i][a] = INVALID; // Adjacency matrix - } - } - int targetNode = INVALID; - int targetArea = INVALID; - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - for (int n = 0; n < numnodes; n++) - { - if (nodes[n].area == i) - { - for (int l = 0; l < nodes[n].num_links; l++) - { - targetNode = nodes[n].links[l].targetNode; - targetArea = nodes[targetNode].area; - //if (targetArea != INVALID && targetArea != i) - if (targetArea != i) - { - nav_area.adjacency_matrix[i][targetArea] = targetArea; - } - } - } - } - } - - BOTLIB_UpdateAllAreaEdges(); // Update edge connections - - // Find total areas - nav_area.total_areas = 0; - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - if (nav_area.adjacency_matrix[i][a] != INVALID) - { - nav_area.total_areas++; - break; - } - } - } - - if (nav_area.total_areas == 0) - { - Com_Printf("%s WARNING: Map has no area nodes. Bot pathing will be less optimal and large maps not supported.\n", __func__); - return; - } - - // Print connections - /* - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - if (nav_area.adjacency_matrix[i][a] != INVALID) - { - Com_Printf("Nav area [%d] connects to [%d]\n", i, a); - } - } - } - */ - - - // Cache all the nodes in each area - BOTLIB_MallocAreaNodes(); // Alloc memory for area nodes - // Check if memory was allocated - if (nav_area.area_nodes && nav_area.area_nodes[0]) - { - // Init areas with INVALID - for (int i = 0; i < MAX_NAV_AREAS; ++i) - { - for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) - { - nav_area.area_nodes[i][n] = INVALID; - } - } - - // Cache all the nodes in each area -- currently a max of (MAX_NAV_AREAS_NODES - 1) nodes per area is allowed - qboolean reached_max_area_nodes; - //qboolean notified = false; - for (int i = 0; i < numnodes; i++) - { - reached_max_area_nodes = true; - for (int n = 0; n < (MAX_NAV_AREAS_NODES - 1); n++) // Cache up to (MAX_NAV_AREAS_NODES - 1) - { - if (nav_area.area_nodes[nodes[i].area][n] == INVALID) - { - reached_max_area_nodes = false; - nav_area.area_nodes[nodes[i].area][n] = nodes[i].nodenum; - break; - } - } - if (reached_max_area_nodes)// && notified == false) - { - //notified = true; - Com_Printf("%s Nav area [%d] reached_max_area_nodes [%d]\n", __func__, nodes[i].area, MAX_NAV_AREAS_NODES); - break; - } - } - - BOTLIB_RandomizeAreaColors(); - - /* - // Print out area nodes - int total_nodes; - for (int i = 0; i < MAX_NAV_AREAS; ++i) - { - if (nav_area.area_nodes[i][0] == INVALID) - continue; - - total_nodes = 0; - - Com_Printf("%s Area [%d] nodes: ", __func__, i); - for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) - { - if (nav_area.area_nodes[i][n] == INVALID) - break; - - Com_Printf(" %d ", nav_area.area_nodes[i][n]); - total_nodes++; - } - Com_Printf("\nTotal nodes: %d\n", total_nodes); - } - */ - } -} - -// Create area nodes mallocs -void BOTLIB_MallocAreaNodes() -{ - // Dynamically cache all the nodes in each area -- currently a max of 1024 nodes per area is allowed - //area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area - nav_area.area_nodes = (int**)malloc(sizeof(*nav_area.area_nodes) * MAX_NAV_AREAS); - if (!nav_area.area_nodes) { - Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); - nav_area.area_nodes = NULL; - } - else - { - for (int i = 0; i < MAX_NAV_AREAS; ++i) - { - nav_area.area_nodes[i] = (int*)malloc(sizeof(*(nav_area.area_nodes[i])) * MAX_NAV_AREAS_NODES); - if (!nav_area.area_nodes[i]) - { - Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); - //nav_area.area_nodes = NULL; - } - } - // Now we can use nav_area.area_nodes[i][j] to access any element :) - } -} -// Free area nodes mallocs -void BOTLIB_FreeAreaNodes() -{ - if (nav_area.area_nodes) - { - for (int i = 0; i < MAX_NAV_AREAS; ++i) - { - if (nav_area.area_nodes[i]) - { - free(nav_area.area_nodes[i]); // Freeing memory allocated for each row separately - nav_area.area_nodes[i] = NULL; - } - } - free(nav_area.area_nodes); // And then freeing the pointer to the pointers - nav_area.area_nodes = NULL; - } -} - -// This initiates bot travel from current node to goal node -// 1) The bot will use area nodes if map has areas (Efficient), -// 2) Alternatively it will fallback to doing node-to-node pathing (Less efficient) -// 3) path_randomization only applies to the fallback method -qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization) -{ - if (goal_node == INVALID) - return false; - - //Com_Printf("%s\n", __func__); - - self->bot.node_list_count = 0; - - // New pathing - if area nodes are supported - if (nav_area.total_areas > 0 && goal_node) - { - Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); - if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path - { - while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path - { - } - - // Add the goal to the end of the node list + terminate - if (self->bot.node_list_count) - { - self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node - self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate - } - - return true; // Success - } - else - { - Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); - return false; // Failure - } - } - else // Fallback to old pathing - { - self->bot.goal_node = INVALID; - //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); - if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) - { - - // Add the goal to the end of the node list + terminate - if (self->bot.node_list_count) - { - self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node - self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate - } - - return true; // Success - } - } - - Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); - return false; // Failure to find path -} - -// Checks if goal is reachable from current node -qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node) -{ - if (goal_node == INVALID) - { - Com_Printf("%s INVALID PATH: goal_node is invalid\n", __func__); - return false; - } - - //Com_Printf("\n\n========================================================\n", __func__); - //Com_Printf("%s Heading for goal node[%d] area[%d]\n", __func__, goal_node, self->bot.goal_area); - //Com_Printf("========================================================\n", __func__); - - //BOTLIB_InitAreaConnections(); - - // Init area - self->bot.start_area = INVALID; - //self->bot.current_area = INVALID; - //self->bot.goal_area = INVALID; - - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - - - if (self->bot.current_node == INVALID) - { - //Com_Printf("%s INVALID PATH: Cannot find current_node\n", __func__); - self->bot.next_area_node = INVALID; - return false; - } - else - { - self->bot.start_area = nodes[self->bot.current_node].area; // Area we first started from - self->bot.current_area = nodes[self->bot.current_node].area; // Current area we're in - self->bot.goal_area = nodes[goal_node].area; // Goal area to reach - self->bot.next_area_counter = 0; // Keep track of which area is next, starting from the first area (current area) - self->bot.next_area_node = self->bot.current_node; - - - - - // If node is inside of the same (or final) area try to see if we can navigation there - // Otherwise continue with trying to route to the goal_node's area - if (self->bot.current_area == self->bot.goal_area) - { - //if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, false)) - { - self->bot.next_area_nodes[0] = goal_node; // Last node - self->bot.next_area_nodes_counter = 0; // Total nodes in next_area_nodes[] - self->bot.next_area_nodes[1] = INVALID; // Terminate - - self->bot.goal_node = goal_node; // Set the goal node to reach - self->bot.path_taken = 0; // Get rand path - ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; - - Com_Printf("%s Goal node[%d] is in the same area[%d] as your area [%d]\n", __func__, goal_node, self->bot.goal_area, self->bot.current_area); - return true; // Success - } - } - - } - - - - - /* - // Pick a random area to visit - // DO NOT DELETE, it will be useful later on - //while (nav_area.goal_area == nav_area.start_area) // Pick a goal that isn't the same area as bot is in - // nav_area.goal_area = rand() % nav_area.total_areas; // Get rand goal - // Get goal node within goal area - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].area == nav_area.goal_area) - { - nav_area.goal_node = nodes[i].nodenum; // Get first node in area - //nav_area.goal_node = DFS_area_nodes[nav_area.goal_area][0]; // Get first node in area - Com_Printf("%s Goal node [%d]\n", __func__, nav_area.goal_node); - break; - } - } - */ - - - // Calculate all paths - for (int i = 0; i < MAX_NAV_AREAS_PATHS; i++) - { - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - nav_area.dfs_paths[i][a] = INVALID; - } - } - nav_area.dfs_path_count = 0; - memset(nav_area.dfs_visited, false, sizeof(nav_area.dfs_visited)); - - nav_area.dfs_path[0] = 0; - nav_area.dfs_len = 0; - //nav_area.dfs_visited[0] = false; - BOTLIB_DepthFirstSearch(self, self->bot.current_area, self->bot.goal_area); // Calculate all possible paths - - //Com_Printf("%s DFS_path_count >= %d\n", __func__, nav_area.dfs_path_count); - - //Com_Printf("%s total_areas[%d] start[%d] curr[%d] goal[%d] path_taken[%d]\n", __func__, nav_area.total_areas, nav_area.start_area, nav_area.current_area, nav_area.goal_area, nav_area.path_taken); - - // Print all paths - /* - if (nav_area.dfs_path_count) - { - for (int i = 0; i < nav_area.dfs_path_count; i++) - { - Com_Printf("%s [%d]Path: ", __func__, i); - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - if (nav_area.dfs_paths[i][a] == INVALID) - break; - - Com_Printf("%d ", nav_area.dfs_paths[i][a]); - } - Com_Printf("\n"); - } - } - */ - - // Instead of picking a random path, pick the less used path... - - // Pick a random path - if (nav_area.dfs_path_count) // Success - { - /* - self->bot.goal_node = goal_node; // Set the goal node to reach - self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path - self->bot.state = BOT_MOVE_STATE_NAV_NEXT; - - // Precalculate all edge-to-edge nodes - Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); - int prev_area = self->bot.current_area; - int next_area; - int next_node; - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - next_area = nav_area.dfs_paths[self->bot.path_taken][i]; - - if (next_area == INVALID) // End of path - { - Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); - self->bot.next_area_nodes[i] = goal_node; // Last node - self->bot.next_area_nodes_counter = i; // Total nodes in next_area_nodes[] - self->bot.next_area_nodes[i + 1] = INVALID; // Terminate - break; - } - else - { - next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); - self->bot.next_area_nodes[i] = next_node; - Com_Printf("[%d][%d] ", next_area, next_node); - } - - prev_area = next_area; - } - Com_Printf("\n"); - */ - - //self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node - //next_area_nodes - - BOTLIB_GetAreaPath(self, goal_node); - - return true; // Success - } - else // Failure - { - self->bot.path_taken = INVALID; - //Com_Printf("%s NO VALID PATH from area %d to %d\n", __func__, self->bot.current_area, self->bot.goal_area); - return false; // Faulure - } - - return true; // Success -} - -// -qboolean BOTLIB_MakeAreaPath(edict_t* self, int goal_node, qboolean use_heatmap) -{ - self->bot.next_area_nodes_counter = 0; - int prev_area = self->bot.current_area; - int next_area; - int next_node; - qboolean debug = false; - - if (debug) Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - next_area = nav_area.dfs_paths[self->bot.path_taken][i]; - - self->bot.area_heatmap[next_area] += 1; // Increase heatmap - if (use_heatmap) // If we're avoiding an area - { - if (self->bot.area_heatmap[next_area] >= 3) - { - //Com_Printf("\n"); - //self->bot.area_heatmap[next_area] = 0; // Reset heatmap - //return false; // Failure - } - } - - if (next_area == INVALID) // End of path - { - if (debug) Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); - self->bot.next_area_nodes[i] = goal_node; // Last node - self->bot.next_area_nodes_counter = i + 1; // Total nodes in next_area_nodes[] - self->bot.next_area_nodes[i + 1] = INVALID; // Terminate - break; - } - else - { - next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); - if (next_node == INVALID) - { - if (debug) Com_Printf("[%d][%d] ", next_area, next_node); - if (debug) Com_Printf(" -- INVALID PATH\n"); - return false; // Failure to find connected edge connection - } - self->bot.next_area_nodes[i] = next_node; - if (debug) Com_Printf("[%d][%d] ", next_area, next_node); - } - - prev_area = next_area; - } - if (debug) Com_Printf("\n"); - - return true; // Success -} - - -// Precalculate all edge-to-edge nodes -void BOTLIB_GetAreaPath(edict_t* self, int goal_node) -{ - self->bot.goal_node = goal_node; // Set the goal node to reach - ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; - - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path - if (BOTLIB_MakeAreaPath(self, goal_node, false) == true) // Success - break; - } - - /* - qboolean success = false; - for (int i = 0; i < nav_area.dfs_path_count; i++) - { - self->bot.path_taken = i; // Cycle through paths - success = BOTLIB_MakeAreaPath(self, goal_node, true); - if (success) - break; - } - if (success == false) - { - self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path - BOTLIB_MakeAreaPath(self, goal_node, false); - } - */ -} - -qboolean BOTLIB_GetNextAreaNode(edict_t *self) -{ - if (self->bot.next_area_nodes_counter == self->bot.next_area_counter) - { - Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); - return false; - } - - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node - if (self->bot.current_node == INVALID || self->bot.goal_node == INVALID) - { - Com_Printf("%s INVALID PATH\n", __func__); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal - return false; - } - int prev_node = self->bot.current_node; - if (self->bot.next_area_counter) - { - prev_node = self->bot.node_list[self->bot.node_list_count - 1]; - //prev_node = self->bot.next_area_nodes[self->bot.next_area_counter - 1]; - } - int next_node = self->bot.next_area_nodes[self->bot.next_area_counter]; - if (next_node != INVALID) - { - //if (BOTLIB_CanVisitNode(self, nodes[next_node].nodenum, false, INVALID, self->bot.next_area_counter)) - //if (BOTLIB_DijkstraAreaPath(self, prev_node, next_node, false, nodes[prev_node].area, self->bot.next_area_counter)) - if (BOTLIB_DijkstraPath(self, prev_node, next_node, false)) - { - self->bot.next_area_node = next_node; - self->bot.next_area_counter++; - - //Com_Printf("%s [%d] prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] curr[%d] goal[%d]\n", __func__, self->bot.next_area_counter, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.current_node, self->bot.goal_node); - - //if (self->bot.goal_node == nodes[next_node].nodenum) - // Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); - - if (next_node == self->bot.goal_node) // Reached goal node - { - self->bot.current_node = self->bot.node_list[0]; - self->bot.next_node = self->bot.node_list[1]; - self->node_timeout = 0; - self->bot.state = BOT_MOVE_STATE_MOVE; - return false; - } - - return true; - } - else - { - //Com_Printf("%s Could not visit node -- failed to reach prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] goal[%d]\n", __func__, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.goal_node); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal - return false; - } - } - - return false; - - /* - int last_node = self->bot.next_area_nodes[self->bot.next_area_nodes_counter]; - float dist = VectorDistance(nodes[self->bot.current_node].origin, nodes[last_node].origin); - if ( dist < 128 ) - { - //Com_Printf("%s Success, reached goal node[%d] from node[%d] dist[%f]\n", __func__, self->bot.goal_node, self->bot.current_node, dist); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal - return true; - } - */ - - //Com_Printf("%s Failed to reach next node [%d] curr_node[%d] goal[%d]\n", __func__, next_node, self->bot.current_node, self->bot.goal_node); - - -#if 0 - - - - if (self->bot.path_taken == INVALID) - { - Com_Printf("%s No valid paths found\n", __func__); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal - return; - } - - /* - float dist = 999999; - if (nav_area.next_area_node != INVALID) - { - dist = VectorDistance(nodes[nav_area.next_area_node].origin, nodes[self->bot.current_node].origin); - if (dist > 256.0f) - return; - } - */ - - // Update current area - self->bot.current_area = nodes[self->bot.current_node].area; - - // Update next area - self->bot.next_area = nav_area.dfs_paths[self->bot.path_taken][self->bot.next_area_counter]; - self->bot.next_area_counter++; - - // Check if final area reached - if (self->bot.next_area == INVALID) - { - if (self->bot.goal_node != INVALID && BOTLIB_CanVisitNode(self, nodes[self->bot.goal_node].nodenum, false, nodes[self->bot.current_node].area)) - { - Com_Printf("%s Goal area reached. Heading for goal node [%d] within the final area [%d]\n", __func__, self->bot.goal_node, self->bot.current_area); - return; - } - else - { - Com_Printf("%s Failed to reach final goal node[%d]: g_area[%d], c_area[%d], n_area[%d]\n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_area, self->bot.next_area); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal - return; - } - } - - // Com_Printf("%s c_area[%d] n_area[%d] g_area[%d]\n", __func__, self->bot.current_area, self->bot.next_area, self->bot.goal_area); - - /* - Com_Printf("%s Path taken: ", __func__); - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - if (nav_area.dfs_paths[self->bot.path_taken][a] == INVALID) - break; - - Com_Printf("%d ", nav_area.dfs_paths[self->bot.path_taken][a]); - } - Com_Printf("\n"); - */ - - - //BOTLIB_UpdateAllAreaEdges(); // Update edge connections - self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node - //Com_Printf("%s c_node[%d] n_node[%d] c_area[%d] n_area[%d] g_area[%d] path_taken[%d]\n", __func__, self->bot.current_node, self->bot.next_area_node, self->bot.current_area, self->bot.next_area, self->bot.goal_area, self->bot.path_taken); - //Com_Printf("%s goal_area[%d] current_area[%d] --> next_area[%d]\n", __func__, self->bot.goal_area, self->bot.current_area, self->bot.next_area); - - if (self->bot.next_area_node != INVALID) - { - if (BOTLIB_CanVisitNode(self, nodes[self->bot.next_area_node].nodenum, false, nodes[self->bot.current_node].area)) - { - Com_Printf("%s heading to next node[%d] area[%d] goal[%d]\n", __func__, nodes[self->bot.next_area_node].nodenum, self->bot.next_area, self->bot.goal_node); - return; - } - } - - Com_Printf("%s Failed to reach NextAreaNode -- goal N[%d] A[%d] -- curr N[%d] A[%d] -- next N[%d] A[%d] \n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_node, self->bot.current_area, self->bot.next_area_node, self->bot.next_area); -#endif -} - -// Stores all the nodes within an area that connect to an external area (edge nodes) -// 1) Wipe old edge data -// 2) Find and store edge nodes -void BOTLIB_UpdateAllAreaEdges(void) -{ - for (int a = 0; a < MAX_NAV_AREAS; a++) - { - for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) - { - DFS_area_edges[a][e] = INVALID; - } - } - - int targetNode; - for (int i = 0; i < numnodes; i++) - { - for (int l = 0; l < nodes[i].num_links; l++) - { - targetNode = nodes[i].links[l].targetNode; - if (nodes[i].area != nodes[targetNode].area) // If node links to a node outside of its own area - { - for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges - { - if (DFS_area_edges[nodes[i].area][e] == INVALID) // Find free spot to store a new edge - { - //Com_Printf("%s area[%d to %d] node[%d to %d]\n", __func__, nodes[i].area, nodes[targetNode].area, i, targetNode); - DFS_area_edges[nodes[i].area][e] = targetNode; // Store the edge node - break; - } - } - } - } - } -} - -// Get a random edge node from two connected areas -// Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) -int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2) -{ - int a2n; // Area 2 node - int ec = 0; // Edge counter - int edges[MAX_NAV_AREAS_EDGES] = { INVALID }; // Store the edges that connect from A1 to A2 - - // Find and store all connected nodes from A1 to A2 - for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges - { - if (DFS_area_edges[area_1][e] == INVALID) // End of edges stored - break; - - a2n = DFS_area_edges[area_1][e]; - if (nodes[a2n].area == area_2) // Find all areas that connect from A1 to A2 - { - //Com_Printf("%s area[%d to %d] node[%d] counter[%d]\n", __func__, area_1, area_2, a2n, ec); - edges[ec++] = a2n; // Store the area_2 node - } - } - - if (ec) - return edges[rand() % ec]; // Return a random edge node - else - return INVALID; // No connected edge nodes found -} - -// Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm -void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination) -{ - if (current == destination) - { - if (nav_area.dfs_path_count >= MAX_NAV_AREAS_PATHS) - { - //Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_path_count >= %d\n", __func__, MAX_NAV_AREAS_PATHS); - return; - } - if (nav_area.dfs_len >= MAX_NAV_AREAS) - { - Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_len >= %d\n", __func__, MAX_NAV_AREAS); - return; - } - - if (nav_area.dfs_len) // If there's an actual path - { - // Check if path passes through itself (loops back and touches the starting area again) - // Don't cross the streams! It would be very bad! Egon. - for (int i = 0; i < nav_area.dfs_len; i++) - { - if (self->bot.start_area == nav_area.dfs_path[i]) // If path crosses start_area again - { - return; // Reject path - } - - for (int j = 0; j < nav_area.dfs_len; j++) - { - if (i == j) continue; - - if (nav_area.dfs_path[j] == nav_area.dfs_path[i]) // If path crosses same area twice - { - return; // Reject path - } - } - } - - - for (int i = 0; i < nav_area.dfs_len; i++) - { - nav_area.dfs_paths[nav_area.dfs_path_count][i] = nav_area.dfs_path[i]; // Copy from tmp path to full path array - } - nav_area.dfs_path_count++; - } - - return; - } - for (int i = 0; i < MAX_NAV_AREAS; i++) - { - if (nav_area.adjacency_matrix[current][i] != INVALID && !nav_area.dfs_visited[i]) - { - nav_area.dfs_path[nav_area.dfs_len++] = i; // Copy to tmp path - nav_area.dfs_visited[i] = true; // Visited - BOTLIB_DepthFirstSearch(self, i, destination); // Recursive search (cycles) - nav_area.dfs_len--; // Backtrack - nav_area.dfs_visited[i] = false; // Unvisit - } - } -} - -// Check if area already exists -qboolean BOTLIB_AreaExists(int area_num) -{ - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].area == area_num) - { - return true; // Area exists - } - } - return false; // Area does not exist -} -// Attempts to automatically add areas to map -void BOTLIB_AutoArea(edict_t* self) -{ - int curr_area_num = 0; - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].area == 0) - { - self->bot.walknode.selection_node_count = 0; - BOTLIB_GroupConnectedNodeArea(self, i); // Try grouping nodes from this node - - if (self->bot.walknode.selection_node_count) - { - //Check if area exists - for (; curr_area_num < MAX_NAV_AREAS; curr_area_num++) - { - if (BOTLIB_AreaExists(curr_area_num) == false) - break; - } - if (curr_area_num + 1 >= MAX_NAV_AREAS) - { - Com_Printf("%s WARNING: Cannot add more areas, maximum allowed %d\n", __func__, MAX_NAV_AREAS); - break; - } - // Found a free area - for (int s = 0; s < self->bot.walknode.selection_node_count; s++) - { - nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; - nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; - } - } - } - } - - if (curr_area_num) - { - BOTLIB_FreeAreaNodes(); - BOTLIB_InitAreaNodes(); - BOTLIB_InitAreaConnections(); - } -} - -// Randomize area colors -void BOTLIB_RandomizeAreaColors() -{ - int color; - for (int i = 0; i < MAX_NAV_AREAS; ++i) - { - // Try to randomize colors while keeping at least one of the R,G,B values above 128+ - int rng = rand() % 3; - if (rng == 0) - color = MakeColor((rand() % 255), (rand() % 255), (rand() % 128) + 128, 255); // Randomize color - else if (rng == 1) - color = MakeColor((rand() % 255), (rand() % 128) + 128, (rand() % 255), 255); // Randomize color - else - color = MakeColor((rand() % 128) + 128, (rand() % 255), (rand() % 255), 255); // Randomize color - - for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) - { - if (nav_area.area_nodes[i][n] == INVALID) - break; - - nodes[nav_area.area_nodes[i][n]].area_color = color; - } - } -} - -// ==================================================================================== -// ==================================================================================== -// Create local area nodes memory alloc -int total_local_area_nodes; -qboolean* local_area_nodes; -qboolean BOTLIB_MallocLocalAreaNodes() -{ - total_local_area_nodes = 0; - local_area_nodes = NULL; - - //nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory - local_area_nodes = (qboolean*)malloc(sizeof(local_area_nodes) * (numnodes + 1)); - if (local_area_nodes == NULL) - { - Com_Printf("%s Memory allocation failed for local_area_nodes\n", __func__); - local_area_nodes = NULL; - return false; - } - - for (int i = 0; i < numnodes; i++) - { - local_area_nodes[i] = false; // Init to false - } - - return true; // Success -} -// Free local area nodes memory alloc -void BOTLIB_FreeLocalAreaNodes() -{ - if (local_area_nodes) - { - free(local_area_nodes); - local_area_nodes = NULL; // Nullify dangling pointer - } -} -// A recurrsive function that finds all nodes that are connected and pathed to the initial `start_node` and are of the same area num -void BOTLIB_GroupConnectedNodeArea_r(edict_t* self, int next_node) //, vec3_t normal) -{ - for (int i = 0; i < numnodes; i++) - { - if (local_area_nodes[i] == true) - continue; - - if (nodes[i].area == nodes[next_node].area) - { - for (int l = 0; l < nodes[i].num_links; l++) - { - if (nodes[i].links[l].targetNode == next_node) - { - if (self->bot.walknode.selection_node_count >= MAX_NAV_AREAS_NODES) - break; - - // Check if node already included in selection - qboolean added = false; - for (int s = 0; s < self->bot.walknode.selection_node_count; s++) - { - if (self->bot.walknode.selection_nodes[s] == i) - { - added = true; - break; - } - } - if (added) - continue; // Skip adding because we already have it - - // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - // Need to test if the two nodes are on the same plane/normal/height/distance - - if (VectorDistance(nodes[i].origin, nodes[self->bot.walknode.selection_nodes[0]].origin) > 1024) - continue; - - // Check hight diff of connected node - ignore nodes with large differences - //Com_Printf("%s FABS[%f]\n", __func__, fabs(nodes[i].origin[2] - nodes[next_node].origin[2])); - if (fabs(nodes[i].origin[2] - nodes[next_node].origin[2]) > 32) - continue; - - // Only nodes on the same normal - with a small epsilon - if (BOTLIB_VectorCompare(nodes[i].normal, nodes[next_node].normal, 0.1) == false) - continue; - - - //Com_Printf("%s [%f %f %f] vs [%f %f %f]\n", __func__, nodes[next_node].normal[0], nodes[next_node].normal[1], nodes[next_node].normal[2], nodes[i].normal[0], nodes[i].normal[1], nodes[i].normal[2]); - - // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - local_area_nodes[next_node] = true; // Flag node as true - //total_local_area_nodes++; - - self->bot.walknode.selection_nodes[self->bot.walknode.selection_node_count] = i; - self->bot.walknode.selection_node_count++; - - BOTLIB_GroupConnectedNodeArea_r(self, i); // Scan the connected node - } - } - } - } -} - -void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node) -{ - if (BOTLIB_MallocLocalAreaNodes() == false) // Alloc memory - return; // Failure - - local_area_nodes[start_node] = true; // Initiate starting node to true - //total_local_area_nodes++; - - /* - trace_t tr = gi.trace(tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] + 32), NULL, NULL, tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] - 256), NULL, MASK_SHOT); - if (tr.fraction < 1.0) - { - VectorCopy(tr.plane.normal, nodes[start_node].normal); - } - - Com_Printf("%s [%f %f %f]\n", __func__, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); - */ - - //BOTLIB_SetAllNodeNormals(); // Set all the normals - - BOTLIB_GroupConnectedNodeArea_r(self, start_node); // Recurrsively check all nodes - - //total_local_area_nodes - //Com_Printf("%s total_local_area_nodes[%d]\n", __func__, total_local_area_nodes); - - //for (int i = 0; i < numnodes; i++) - { - //if (local_area_nodes[i] == true) - { - } - } - - BOTLIB_FreeLocalAreaNodes(); // Free memory -} -// ==================================================================================== -// ==================================================================================== - -//rekkie -- Dijkstra Area Pathing -- s -// Currently if path_randomization is enabled, the bot may not be able to reach its target, -// therefore it's only really used when heading to a random node. -// Area: specify the area we're working with; only searching nodes within this area -// build_new_path: If we're building a new path or adding to an existing path -qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path) -{ - //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - - // Sanity check - if (from == INVALID || to == INVALID || area == INVALID) - return false; - - // Check for null pointers - if (nav_area.area_nodes == NULL || nav_area.area_nodes[0] == NULL) - return false; - - //if (build_new_path) - { - //ent->bot.node_list_count = 0; // Reset the list of nodes to zero - //ent->bot.node_list_current = 0; // Set the starting point to zero - } - - AntInitSearch(ent); // Clear out the path storage - - for (int i = 0; i < numnodes; i++) - nodeweight[i] = NAV_INFINITE; - - int newNode = INVALID; // Stores the node being tested - int atNode = from; // Current node we're visiting - botlib_sll_t openList; // Locally declared OPEN list - openList.head = openList.tail = NULL; // Init the list - - float weight; // Weight of the node we're visiting - nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) - nodefrom[atNode] = atNode; // Set the parent node to itself - //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list - SLLpush_back(&openList, from); // Store it - - //int searched_nodes = 0; - - while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list - { - atNode = SLLfront(&openList); // Get the next node - if (atNode == to) // If the next node is the goal node - { - //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); - break; // We found a path - } - - //searched_nodes++; - //Com_Printf("%s NODE[%d] AREAS: ", __func__, atNode); -//#if 0 - // Find the node with the lowest weight - float lowest_cost = NAV_INFINITE; - int lowest_node = INVALID; - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - - if (newNode == INVALID) - continue; - - // Only search in current area - if (newNode != to && nodes[newNode].area != area) - continue; - - //Com_Printf(" %d ", nodes[newNode].area); - - if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - if (path_randomization) - { - // Consider skipping this node if it has many links and its weight is high - if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) - continue; - - // [1] Less expensive - // Randomize paths - if (newNode != to && random() <= 0.2) // how often we skip checking a linked node - continue; - - // [2] Expensive - // Try to occasionally break up paths, expensive operation - /* - if (random() < 0.01) - { - // Check if node is free, otherwise check next link - if (Botlib_Nav_NodeInUse(ent, newNode) == false) - if (random() < 0.1) - continue; - } - */ - } - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - if (nodeweight[newNode] == NAV_INFINITE) - { - lowest_node = INVALID; - break; - } - - if (nodeweight[newNode] < lowest_cost) - { - lowest_cost = nodeweight[newNode]; - lowest_node = newNode; - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - } - } - } - //Com_Printf("\n"); - - // Use the shortest path - if (lowest_node != INVALID) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[lowest_node]) - { - if (lowest_node != from && lowest_node != to) - { - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - if (path_randomization) - { - // [1] Less expensive - // Randomize paths - if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node - continue; - } - } - - nodeweight[lowest_node] = weight; // Update the weight - nodefrom[lowest_node] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, lowest_node); // Store it - //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); - } - - if (lowest_node == to) // If node being linked is the goal node - { - //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); - break; // We found a path - } - } - else -//#endif - { - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - - if (newNode == INVALID) - continue; - - // Only search in current area - if (newNode != to && nodes[newNode].area != area) - continue; - - if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - - // Get the weight of the node we're visiting + the cost to the new node - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[newNode]) - { - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - // Consider skipping this node if it has many links and its weight is high - if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) - continue; - - if (path_randomization) - { - // [1] Less expensive - // Randomize paths - if (newNode != to && random() <= 0.2) // how often we skip checking a linked node - continue; - } - - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, newNode); // Store it - } - } - - if (newNode == to) // If node being linked is the goal node - { - //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); - break; // We found a path - } - - } - } - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - } - - //Com_Printf("%s searched_nodes[%d]\n", __func__, searched_nodes); - - // Free up the memory we allocated - SLLdelete(&openList); - - // Optimise stored path with this new information - if (newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode); - - // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array - ////if (nodefrom[newNode] != INVALID) - { - ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path - //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); - } - - //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); - //int prev_newNode = newNode; - - // We earlier set our start node to INVALID to set up the termination - // Check if there is a path, and it's not the node we're standing on (safety check) - while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) - { - //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); - //prev_newNode = newNode; - - // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high - if (nodes[newNode].num_links > 3) - { - nodes[newNode].weight += 0.1; - if (nodes[newNode].weight > 0.5) // Limit max weight - nodes[newNode].weight = 0; // Reset back to normal weight - if (nodes[newNode].weight < 0) // Sanity check - nodes[newNode].weight = 0; // Reset back to normal weight - } - - SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist - ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path - - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); - } - - //Com_Printf("%s EXIT PATH\n\n", __func__); - - // Each time a path is found, make a copy - ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list - ent->bot.node_list_current = 0; - if (ent->bot.node_list_count) // Set the current and next nodes - { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; - } - ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path - - if (0) - { - if (1) - { - Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); - for (int i = 0; i < ent->bot.node_list_count; i++) - { - //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); - Com_Printf(" %d ", ent->bot.node_list[i]); - } - Com_Printf(" ]\n"); - } - else - Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - //Com_Printf("%s found a path\n", __func__); - } - - if (SLLempty(&ent->pathList)) - return false; // Failure - - return true; // Success - } - - // If we get to this point, it means our path failed - // If path_randomization was true, then try again without it - if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it - { - BOTLIB_DijkstraAreaPath(ent, from, to, false, area, build_new_path); - } - // Else: just fail it completely - { - //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); - return false; // Failure - } -} -//rekkie -- Dijkstra Area Pathing -- e - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -//rekkie -- Dijkstra pathing -- s -// Currently if path_randomization is enabled and the bot fails to path to its target, -// BOTLIB_DijkstraPath() is run again with path_randomization turned off -qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization) -{ - //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - - // Sanity check - if (from == INVALID || to == INVALID) - return false; - - AntInitSearch(ent); // Clear out the path storage - for (int i = 0; i < numnodes; i++) - nodeweight[i] = NAV_INFINITE; - - int newNode = INVALID; // Stores the node being tested - int atNode = from; // Current node we're visiting - botlib_sll_t openList; // Locally declared OPEN list - openList.head = openList.tail = NULL; // Init the list - - float weight; // Weight of the node we're visiting - nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) - nodefrom[atNode] = atNode; // Set the parent node to itself - //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list - SLLpush_back(&openList, from); // Store it - - - while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list - { - atNode = SLLfront(&openList); // Get the next node - if (atNode == to) // If the next node is the goal node - { - //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); - break; // We found a path - } - - /* - // Update node weights, but don't add them to the list - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - - if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) - { - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - //SLLpush_back(&openList, newNode); // Add it to the open list - } - } - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - - // Find the node link with the shortest path - float lowest_cost = NAV_INFINITE; - int lowest_node = INVALID; - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - if (nodeweight[newNode] <= lowest_cost) - { - lowest_cost = nodeweight[newNode]; - lowest_node = newNode; - } - } - } - - // Add the shortest path to the open list - if (lowest_node != INVALID) - { - SLLpush_back(&openList, lowest_node); // Add it to the open list - //Com_Printf("%s [%d to %d] lowest_node[%d]\n", __func__, from, to, lowest_node); - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - */ - - - - //* - // Find the node with the lowest weight - float lowest_cost = NAV_INFINITE; - int lowest_node = INVALID; - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - - if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - if (path_randomization) - { - // Consider skipping this node if it has many links and its weight is high - if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) - continue; - - // [1] Less expensive - // Randomize paths - if (newNode != to && random() <= 0.2) // how often we skip checking a linked node - continue; - - // [2] Expensive - // Try to occasionally break up paths, expensive operation - /* - if (random() < 0.01) - { - // Check if node is free, otherwise check next link - if (Botlib_Nav_NodeInUse(ent, newNode) == false) - if (random() < 0.1) - continue; - } - */ - } - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - if (nodeweight[newNode] == NAV_INFINITE) - { - lowest_node = INVALID; - break; - } - - if (nodeweight[newNode] < lowest_cost) - { - lowest_cost = nodeweight[newNode]; - lowest_node = newNode; - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - } - } - } - // Use the shortest path - if (lowest_node != INVALID) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[lowest_node]) - { - if (lowest_node != from && lowest_node != to) - { - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - if (path_randomization) - { - // [1] Less expensive - // Randomize paths - if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node - continue; - } - } - - nodeweight[lowest_node] = weight; // Update the weight - nodefrom[lowest_node] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, lowest_node); // Store it - //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); - } - - if (lowest_node == to) // If node being linked is the goal node - { - //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); - break; // We found a path - } - } - else - { - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode == INVALID) - continue; - if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - - // Get the weight of the node we're visiting + the cost to the new node - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[newNode]) - { - // Check if the node can be seen by enemy's path - //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; - - // Consider skipping this node if it has many links and its weight is high - if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) - continue; - - if (path_randomization) - { - // [1] Less expensive - // Randomize paths - if (newNode != to && random() <= 0.2) // how often we skip checking a linked node - continue; - } - - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, newNode); // Store it - } - } - - if (newNode == to) // If node being linked is the goal node - { - //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); - break; // We found a path - } - - } - } - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - //*/ - - - - /* - for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node - { - if (nodes[atNode].links[i].targetNode != INVALID) - { - newNode = nodes[atNode].links[i].targetNode; // Get the next linked node - // Get the weight of the node we're visiting + the cost to the new node - weight = nodeweight[atNode] + nodes[atNode].links[i].cost; - - if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) - { - // If weight is less than the weight of the new node - if (weight < nodeweight[newNode]) - { - nodeweight[newNode] = weight; // Update the weight - nodefrom[newNode] = atNode; // Update the parent node (open list) - SLLpush_back(&openList, newNode); // Store it - } - } - - if (newNode == to) // If node being linked is the goal node - { - break; // We found a path - } - } - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) - */ - } - - - // Free up the memory we allocated - SLLdelete(&openList); - - // Optimise stored path with this new information - if (newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode); - - // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array - ////if (nodefrom[newNode] != INVALID) - { - ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path - //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); - } - - //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); - //int prev_newNode = newNode; - - // We earlier set our start node to INVALID to set up the termination - // Check if there is a path, and it's not the node we're standing on (safety check) - while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) - { - //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); - //prev_newNode = newNode; - - // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high - if (nodes[newNode].num_links > 3) - { - nodes[newNode].weight += 0.1; - if (nodes[newNode].weight > 0.5) // Limit max weight - nodes[newNode].weight = 0; // Reset back to normal weight - if (nodes[newNode].weight < 0) // Sanity check - nodes[newNode].weight = 0; // Reset back to normal weight - } - - SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist - ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path - - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); - } - - //Com_Printf("%s EXIT PATH\n\n", __func__); - - // Each time a path is found, make a copy - //ent->bot.node_list_count = 0; - ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list - ent->bot.node_list_current = 0; - if (ent->bot.node_list_count) // Set the current and next nodes - { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; - } - ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path - - if (0) - { - if (1) - { - Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); - for (int i = 0; i < ent->bot.node_list_count; i++) - { - //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); - Com_Printf(" %d ", ent->bot.node_list[i]); - } - Com_Printf(" ]\n"); - } - else - Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - //Com_Printf("%s found a path\n", __func__); - } - - if (SLLempty(&ent->pathList)) - return false; // Failure - - return true; // Success - } - - // If we get to this point, it means our path failed - // If path_randomization was true, then try again without it - if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it - { - BOTLIB_DijkstraPath(ent, from, to, false); - } - // Else: just fail it completely - { - //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); - return false; // Failure - } -} -//rekkie -- Dijkstra pathing -- e - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -//======================= -// FindPath -//======================= -// -// Uses OPEN and CLOSED lists to conduct a search -// Many refinements planned -// -qboolean AntFindPath(edict_t* ent, int from, int to) -{ - int counter = 0; // Link array counter - int link_counter = 0; // Link counter - int newNode = INVALID; // Stores the node being tested - int atNode; // Structures for search - botlib_sll_t openList; // Locally declared OPEN list - openList.head = openList.tail = NULL; // MUST do this!! - - // Safety first again - we don't want crashes! - if (from == INVALID || to == INVALID) - return false; - - // Put startnode on the OPEN list - atNode = from; - nodefrom[atNode] = INVALID; - SLLpush_back(&openList, from); - nodeused[from] = true; - - //Com_Printf("%s NEW PATH [%d to %d]\n", __func__, from, to); - - - // While there are nodes on the OPEN list AND we are not at destNode - while (!SLLempty(&openList) && newNode != to) - { - // Where we are - atNode = SLLfront(&openList); - - //Com_Printf("%s atNode [%d]\n", __func__, atNode); - - // Safety check - if (atNode <= INVALID) - return false; - - - // Get random start point (instead of always starting at 0) - int start_point = 0; - //if (nodes[atNode].num_links) // If we have links - // start_point = rand() % (nodes[atNode].num_links); // start at random link - //if (start_point + 1 > MAXLINKS) - // start_point = 0; - - //int start_point = 0; - counter = start_point; - - // Go to next link, check for unused node - //while (++counter != start_point) - while (counter != nodes[atNode].num_links) - { - /* - if (counter + 1 > MAXLINKS) //MAXLINKS - { - counter = 0; - if (counter == start_point) - break; - } - */ - - if (nodes[atNode].links[counter].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links - continue; - - // Using an array for FAST access to the path rather than a CLOSED list - newNode = nodes[atNode].links[counter].targetNode; // Get next linked node - - //Com_Printf("%s counter[%d] newNode [%d]\n", __func__, counter, newNode); - counter++; - - if (newNode == INVALID) - continue; - else - { - /* - // Check for failure rates on links - // If the fail rate is > 0, then we have a chance of skipping the node - // The higher the targetFailures, the higher the chance of skipping - //Com_Printf("%s %s: n[%d] l[%d] f[%d]\n", __func__, ent->client->pers.netname, newNode, counter, nodes[atNode].links[counter].targetFailures); - if (nodes[atNode].links[counter].targetFailures > 1) - { - float percent = (1 / nodes[atNode].links[counter].targetFailures); - if (random() < percent) - { - //Com_Printf("%s %s: Skipping node %d due to %d failures\n", __func__, ent->client->pers.netname, newNode, nodes[atNode].links[counter].targetFailures); - continue; - } - } - */ - - if (0) - { - // [1] Less expensive - // Randomize paths - //if (random() < 0.1) // how often we skip a targetNode - // continue; - - // [2] Expensive - // Try to occasionally break up paths, expensive operation - //if (random() < 0.01) - { - // Check if node is free, otherwise check next link - if (Botlib_Nav_NodeInUse(ent, newNode) == false) - if (random() < 0.1) - continue; - } - } - - //if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) - // continue; - - // If newNode NOT on open or closed list - if (!nodeused[newNode]) - { - nodeused[newNode] = true; // Mark node as used - nodefrom[newNode] = atNode; // Add to OPEN list - SLLpush_back(&openList, newNode); // Store it - } - - if (newNode == to) // If node being linked is destNode then quit - break; - } - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - } - - - - /* - // While there are nodes on the OPEN list AND we are not at destNode - while (!SLLempty(&openList) && newNode != to) - { - counter = 0; - - // Where we are - atNode = SLLfront(&openList); - - ////Com_Printf("%s atNode [%d]\n", __func__, atNode); - - // Safety check - if (atNode <= INVALID) - return false; - - // Using an array for FAST access to the path rather than a CLOSED list - newNode = nodes[atNode].links[counter].targetNode; - - // Process this node putting linked nodes on the OPEN list - while (newNode != INVALID) - { - ////Com_Printf("%s newNode [%d]\n", __func__, newNode); - - // If newNode NOT on open or closed list - if (!nodeused[newNode]) - { - nodeused[newNode] = true; // Mark node as used - nodefrom[newNode] = atNode; // Add to OPEN list - SLLpush_back(&openList, newNode); // Store it - } - - if (newNode == to) // If node being linked is destNode then quit - break; - - // Go to next link, check for unused node - while (++counter < MAXLINKS) - { - newNode = nodes[atNode].links[counter].targetNode; // Get next linked node - - if(newNode == INVALID) - break; - - // [1] Less expensive - // Randomize paths - if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) - break; - if (random() < 0.25) // how often we skip a targetNode - continue; - else - break; - - // [2] Expensive - // Try to occasionally break up paths, expensive operation - //if (random() < 0.88) - //{ - // // Check if node is free, otherwise check next link - // if (Botlib_Nav_NodeInUse(ent, newNode) == false) - // break; - //} - //else - // break; - - } - if (counter >= MAXLINKS || newNode == INVALID) - break; - - //if (++counter >= MAXLINKS) // Increase counter and check we're in bounds - // break; - //newNode = nodes[atNode].links[counter].targetNode; // Get next linked node - } - - SLLpop_front(&openList); // Remove atNode from the OPEN List - } - */ - - // Free up the memory we allocated - SLLdelete(&openList); - - //Com_Printf("%s PATH newNode [%d] to [%d]\n", __func__, newNode, to); - - // Optimise stored path with this new information - if (newNode == to) - { - // Make the path using the fromnode array pushing node numbers on in reverse order - // so we can SLLpop_front them back later - SLLpush_front(&ent->pathList, newNode); - - //rekkie -- DEV_1 -- s - // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID - // Check if INVALID to avoid accessing [-1] array - if (nodefrom[to] != INVALID) - //rekkie -- DEV_1 -- e - path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path - - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); - //int prev_newNode = newNode; - - // We earlier set our start node to INVALID to set up the termination - // Check if there is a path, and it's not the node we're standing on (safety check) - while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) - { - //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); - //prev_newNode = newNode; - - // Push it onto the pathlist - SLLpush_front(&ent->pathList, newNode); - // Set the path in the node array to match this shortest path - path_table[nodefrom[newNode]][to] = newNode; - - //Com_Printf("%s path_table[ nodefrom[%d] ][ %d ] = %d\n", __func__, newNode, to, newNode); - } - - //Com_Printf("%s EXIT PATH\n\n", __func__); - - // Each time a path is found, make a copy - ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list - ent->bot.node_list_current = 0; - if (ent->bot.node_list_count) // Set the current and next nodes - { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; - } - - if (0) - { - if (1) - { - Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); - for (int i = 0; i < ent->bot.node_list_count; i++) - { - Com_Printf(" %d ", ent->bot.node_list[i]); - } - Com_Printf(" ]\n"); - } - else - Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - //Com_Printf("%s found a path\n", __func__); - } - - return true; - } - // else - return false; -} - -//============================= -// LinkExists -//============================= -// -// Check we haven't wandered off path! -// -qboolean AntLinkExists(int from, int to) -{ - int counter = 0; - int testnode; - - if (from == INVALID || to == INVALID) - return false; - - // Check if the link exists - while (counter < MAXLINKS) - { - testnode = nodes[from].links[counter].targetNode; // Get next linked node - if (testnode == to) - { - return true; // A path exists from -> to - } - else if (testnode == INVALID) - { - return false; // No more links and no path found - } - counter++; - } - - return false; // Didn't find it! -} - - - - -// Draw the bot path -qboolean BOTLIB_DrawPath(edict_t* self) -{ - //rekkie -- debug drawing -- s -#if DEBUG_DRAWING - int curr_node, next_node; - float line_width; - uint32_t color; - uint32_t blue = MakeColor(0, 0, 255, 255); // Blue - uint32_t red = MakeColor(255, 0, 0, 255); // Red - void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; - void (*DrawArrow)(int number, vec3_t start, vec3_t end, const uint32_t color, float line_width, int time, qboolean occluded) = NULL; - DrawBox = players[0]->client->pers.draw->DrawBox; - DrawArrow = players[0]->client->pers.draw->DrawArrow; - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - players[0]->client->pers.draw->arrows_inuse = true; // Flag as being used - int jumppad_box_num = 0; // Counter to display more than one jumpbox at once - int start = self->bot.node_list_current - 1; - if (start < 0) - start = 0; - for (int i = start; (i + 1) < self->bot.node_list_count; i++) - { - curr_node = self->bot.node_list[i]; // Next node in the list - next_node = self->bot.node_list[i + 1]; // Next node in the list - - if (curr_node == INVALID || next_node == INVALID) - break; - - if (players[0]->client->pers.draw->draw_arrow_num + 1 > MAX_DRAW_ARROWS) - players[0]->client->pers.draw->draw_arrow_num = 0; - players[0]->client->pers.draw->draw_arrow_num; - - // Draw current link as thicker line - if (self->bot.current_node == curr_node) - { - line_width = 10.0; - color = MakeColor(255, 255, 0, 255); // Yellow - } - else // Draw all other links as thinner lines - { - line_width = 1.0; - color = MakeColor(0, 255, 0, 255); // Green - } - - // Show jumppad nodes as a blue box - for (int i = 0; i < nodes[curr_node].num_links; i++) - { - if (nodes[curr_node].links[i].targetNode == next_node) - { - if (nodes[curr_node].links[i].targetNodeType == NODE_JUMPPAD) - { - DrawBox(jumppad_box_num, nodes[curr_node].origin, blue, nodes[curr_node].mins, nodes[curr_node].maxs, 100, false); - jumppad_box_num++; - } - break; - } - } - - DrawArrow(players[0]->client->pers.draw->draw_arrow_num, nodes[curr_node].origin, nodes[next_node].origin, color, line_width, 100, false); - players[0]->client->pers.draw->draw_arrow_num++; - } - - // Draw goal node - DrawBox(jumppad_box_num, nodes[self->bot.goal_node].origin, red, nodes[self->bot.goal_node].mins, nodes[self->bot.goal_node].maxs, 100, false); - -#endif - //rekkie -- debug drawing -- e - - return true; -} - - - - -//============================== -// SLLpush_front -//============================== -// Add to the front of the list -// -void SLLpush_front(botlib_sll_t* list, int node) -{ - botlib_sll_nodes_t* temp; - - // Store the current head pointer - temp = list->head; - // allocate memory for the new data (LEVEL tagged) - list->head = gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); - // Set up the data and pointer - list->head->node = node; - list->head->next = temp; - // Check if there;'s a next item - if (!list->head->next) - { - // Set the tail pointer = head - list->tail = list->head; - } -} - -//============================== -// SLLpop_front -//============================== -// Remove the item from the front of the list -// -void SLLpop_front(botlib_sll_t* list) -{ - botlib_sll_nodes_t* temp; - - // Store the head pointer - temp = list->head; - // Check if there's a next item - if (list && list->head) - { - if (list->head == list->tail) - { - // List is now emptying - list->tail = list->head = NULL; - } - else - { - // Move head to point to next item - list->head = list->head->next; - } - // Free the memory (LEVEL tagged) - gi.TagFree(temp); - } - else - { - gi.bprintf(PRINT_HIGH, "Attempting to POP an empty list!\n"); - } -} - -//rekkie -- DEV_1 -- s -//============================== -// BOTLIB_SLL_Query_Next_Node -//============================== -// Query the SLL for the next node -// Returns the next node, otherwise returns INVALID if next node cannot be found (end of list, or list is NULL) -int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list) -{ - if (0) //DEBUG: Print out the entire list - { - // Check if there's a next item - if (list && list->head && list->head != list->tail && list->head->next != NULL) - { - Com_Printf("%s nodes[ ", __func__); - Com_Printf("%d ", list->head->node); // Print head node - botlib_sll_nodes_t* next = list->head->next; - while (next != NULL) - { - if (list->head == list->tail) // We're already at the last item - break; - else - { - Com_Printf("%d ", next->node); // Print next node - next = next->next; // Get the next SLL item - } - } - Com_Printf("]\n"); - } - } - - // Check if there's a next item - if (list && list->head) - { - if (list->head == list->tail || list->head->next == NULL) // We're already at the last item - { - return INVALID; // No next item - } - else - { - botlib_sll_nodes_t* next = list->head->next; // Get the next SLL item - return next->node; // return the next node - } - } - - return INVALID; // Empty list -} -//============================== -// BOTLIB_SLL_Query_All_Nodes -//============================== -// Query the SLL list and return all its nodes -// node_list is the returned list of nodes -// max_nodes is the maximum allowed nodes we can store in node_list -// Returns the node count -int BOTLIB_SLL_Query_All_Nodes(edict_t *ent, botlib_sll_t* list, int* node_list, const int max_nodes) -{ - //int node_count = 0; // Node count - - if (ent->bot.node_list_count + 1 > max_nodes) - return ent->bot.node_list_count; - - // Check if there's a next item - if (list && list->head && list->head != list->tail && list->head->next != NULL) - { - //node_list[ent->bot.node_list_count++] = ent->bot.current_node; // Save current node - node_list[ent->bot.node_list_count++] = list->head->node; // Save head node - botlib_sll_nodes_t* next = list->head->next; - while (next != NULL) - { - if (ent->bot.node_list_count + 1 > max_nodes) return ent->bot.node_list_count; - - if (list->head == list->tail) // We're already at the last item - break; - else - { - node_list[ent->bot.node_list_count++] = next->node; // Save next node - next = next->next; // Get the next SLL item - } - } - } - - return ent->bot.node_list_count; // Return node count -} -//rekkie -- DEV_1 -- e - -//============================== -// SLLfront -//============================== -// Get the integer value from the front of the list -// without removing the item (Query the list) -// -int SLLfront(botlib_sll_t* list) -{ - if (list && !SLLempty(list)) - return(list->head->node); - else - return INVALID; -} - -//============================== -// SLLpush_front -//============================== -// Add to the back of the list -// -void SLLpush_back(botlib_sll_t* list, int node) -{ - botlib_sll_nodes_t* temp; - - // Allocate memory for the new item (LEVEL tagged) - temp = (botlib_sll_nodes_t*)gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); - // Store the data - temp->node = node; - temp->next = NULL; // End of the list - // Store the new item in the list - // Is the list empty? - if (!list->head) - { - // Yes - add as a new item - list->head = temp; - list->tail = temp; - } - else - { - // No make this the new tail item - list->tail->next = temp; - list->tail = temp; - } -} - -//============================== -// SLLempty -//============================== -// See if the list is empty (false if not empty) -// -qboolean SLLempty(botlib_sll_t* list) -{ - // If there is any item in the list then it is NOT empty... - if (list) - return (list->head == NULL); - else // No list so return empty - return true; -} - -//=============================== -// Delete the list -//=============================== -// Avoids memory leaks -// -void SLLdelete(botlib_sll_t* list) -{ - botlib_sll_nodes_t* temp; - - while (!SLLempty(list)) - { - temp = list->head; - list->head = list->head->next; - gi.TagFree(temp); - } -} - -//=============================== -// Quake 3 Multithreading Code -//=============================== - -/* - ================ - I_FloatTime - ================ - */ -double I_FloatTime(void) { - time_t t; - - time(&t); - - return t; -#if 0 - // more precise, less portable - struct timeval tp; - struct timezone tzp; - static int secbase; - - gettimeofday(&tp, &tzp); - - if (!secbase) { - secbase = tp.tv_sec; - return tp.tv_usec / 1000000.0; - } - - return (tp.tv_sec - secbase) + tp.tv_usec / 1000000.0; -#endif -} - - -// Threads.c -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/threads.c -#define GDEF_OS_WINDOWS 1 -#if !GDEF_OS_WINDOWS -// The below define is necessary to use -// pthreads extensions like pthread_mutexattr_settype -#define _GNU_SOURCE -#endif // !GDEF_OS_WINDOWS - -#define MAX_THREADS 64 - -int dispatch; -int workcount; -int oldf; -qboolean pacifier; - -qboolean threaded; - -/* - ============= - GetThreadWork - - ============= - */ -int GetThreadWork(void) { - int r; - int f; - - ThreadLock(); - - if (dispatch == workcount) { - ThreadUnlock(); - return -1; - } - - f = 40 * dispatch / workcount; - if (f < oldf) { - Com_Printf("%s WARNING: progress went backwards (should never happen)\n", __func__); - oldf = f; - } - Com_Printf("%s ", __func__); - while (f > oldf) - { - ++oldf; - if (pacifier) { - if (oldf % 4 == 0) { - Com_Printf("%i", f / 4); - } - else { - Com_Printf("."); - } - fflush(stdout); /* ydnar */ - } - } - Com_Printf("\n"); - - r = dispatch; - dispatch++; - ThreadUnlock(); - - return r; -} - - -void (*workfunction)(int); - -void ThreadWorkerFunction(int threadnum) { - int work; - - while (1) - { - work = GetThreadWork(); - if (work == -1) { - break; - } - //Com_Printf("%s thread %i, work %i\n", __func__, threadnum, work); - workfunction(work); - } -} - -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)) { -// void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { -// if (numthreads == -1) { -// ThreadSetDefault(); -// } -// workfunction = func; -// RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); -// } - - -#if _WIN32 - -/* - =================================================================== - - WIN32 - - =================================================================== - */ - -#include - -int numthreads = -1; -CRITICAL_SECTION crit; -static int enter; - -void ThreadSetDefault(void) { - SYSTEM_INFO info; - - if (numthreads == -1) { // not set manually - GetSystemInfo(&info); - numthreads = info.dwNumberOfProcessors; - if (numthreads < 1 || numthreads > 32) { - numthreads = 1; - } - } - - Com_Printf("%s %i threads\n", __func__, numthreads); -} - - -void ThreadLock(void) { - if (!threaded) { - return; - } - EnterCriticalSection(&crit); - if (enter) { - Com_Printf("%s Recursive ThreadLock\n", __func__); - } - enter = 1; -} - -void ThreadUnlock(void) { - if (!threaded) { - return; - } - if (!enter) { - Com_Printf("%s ThreadUnlock without lock\n", __func__); - } - enter = 0; - LeaveCriticalSection(&crit); -} - -// Init and and run the threaded version -void BOTLIB_THREAD_LOADAAS(qboolean force) -{ - loadaas_t loadaas; - loadaas.force = force; - //RunThreadsOnIndividual(2, true, TestThreadedFunc, &loadaas); - //if (numthreads == -1) { - // ThreadSetDefault(); - //} - numthreads = 1; - GetThreadWork(); - RunThreadsOn(1, true, BOTLIB_THREADING_LOADAAS, &loadaas); -} - -// Threaded version -void BOTLIB_THREADING_LOADAAS(void *param) -{ - // Sleep while map fully loads and is ready, otherwise we'll get a crash - while (level.framenum < 50) - { - Sleep(100); - } - - //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web - //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing - loadaas_t* params = (loadaas_t*)param; - if (params != NULL) - { - //Com_Printf("%s param:0x%x force = %d\n", __func__, ¶m, params->force); - ACEND_LoadAAS(params->force); - } -} - -// ===================================================================================== -// Init and and run the threaded version -void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to) -{ - dijkstra_path_t dpath; - dpath.ent = ent; - dpath.from = from; - dpath.to = to; - - numthreads = 1; - GetThreadWork(); - //RunThreadsOn(1, true, BOTLIB_THREADING_DijkstraPath, &dpath); - - HANDLE thdHandle = CreateThread(NULL, 0, BOTLIB_THREADING_DijkstraPath, &dpath, 0, NULL); - if (thdHandle != NULL) - WaitForSingleObject(thdHandle, INFINITE); -} - -// Threaded version -void BOTLIB_THREADING_DijkstraPath(void* param) -{ - //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web - //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing - dijkstra_path_t* dpath = (dijkstra_path_t*)param; - if (dpath != NULL) - { - //Com_Printf("%s dpath:0x%x from[%d] to[%d]\n", __func__, &dpath, dpath->from, dpath->to); - BOTLIB_DijkstraPath(dpath->ent, dpath->from, dpath->to, true); - } -} -// ===================================================================================== - -/* - ============= - RunThreadsOn - ============= - */ -//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func), void *param) { - int threadid[MAX_THREADS]; - HANDLE threadhandle[MAX_THREADS]; - int i; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - // - // run threads in parallel - // - InitializeCriticalSection(&crit); - - if (numthreads < 1) - return; - - //if (numthreads == 1) { // use same thread - // func(0); - //} - //else - { - for (i = 0; i < numthreads; i++) - { - threadhandle[i] = CreateThread( - NULL, // LPSECURITY_ATTRIBUTES lpsa, - //0, // DWORD cbStack, - - /* ydnar: cranking stack size to eliminate radiosity crash with 1MB stack on win32 */ - (4096 * 1024), - - (LPTHREAD_START_ROUTINE)func, // LPTHREAD_START_ROUTINE lpStartAddr, - (LPVOID)param, - //(LPVOID)i, // LPVOID lpvThreadParm, - 0, // DWORD fdwCreate, - &threadid[i]); - } - - //for (i = 0; i < numthreads; i++) - // WaitForSingleObject(threadhandle[i], INFINITE); - } - DeleteCriticalSection(&crit); - - threaded = qfalse; - end = I_FloatTime(); - if (pacifier) { - Com_Printf("%s (%i)\n", __func__, end - start); - } -} - - -#elif GDEF_OS_OSF1 - -/* - =================================================================== - - OSF1 - - =================================================================== - */ - -int numthreads = 4; - -void ThreadSetDefault(void) { - if (numthreads == -1) { // not set manually - numthreads = 4; - } -} - -#include - -pthread_mutex_t* my_mutex; - -void ThreadLock(void) { - if (my_mutex) { - pthread_mutex_lock(my_mutex); - } -} - -void ThreadUnlock(void) { - if (my_mutex) { - pthread_mutex_unlock(my_mutex); - } -} - - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int i; - pthread_t work_threads[MAX_THREADS]; - pthread_addr_t status; - pthread_attr_t attrib; - pthread_mutexattr_t mattrib; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - if (!my_mutex) { - my_mutex = safe_malloc(sizeof(*my_mutex)); - if (pthread_mutexattr_create(&mattrib) == -1) { - Error("pthread_mutex_attr_create failed"); - } - if (pthread_mutexattr_setkind_np(&mattrib, MUTEX_FAST_NP) == -1) { - Error("pthread_mutexattr_setkind_np failed"); - } - if (pthread_mutex_init(my_mutex, mattrib) == -1) { - Error("pthread_mutex_init failed"); - } - } - - if (pthread_attr_create(&attrib) == -1) { - Error("pthread_attr_create failed"); - } - if (pthread_attr_setstacksize(&attrib, 0x100000) == -1) { - Error("pthread_attr_setstacksize failed"); - } - - for (i = 0; i < numthreads; i++) - { - if (pthread_create(&work_threads[i], attrib - , (pthread_startroutine_t)func, (pthread_addr_t)i) == -1) { - Error("pthread_create failed"); - } - } - - for (i = 0; i < numthreads; i++) - { - if (pthread_join(work_threads[i], &status) == -1) { - Error("pthread_join failed"); - } - } - - threaded = qfalse; - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#elif GDEF_OS_IRIX - -/* - =================================================================== - - IRIX - - =================================================================== - */ - -#define USED - -#include -#include -#include -#include - -int numthreads = -1; -abilock_t lck; - -void ThreadSetDefault(void) { - if (numthreads == -1) { - numthreads = prctl(PR_MAXPPROCS); - } - Sys_Printf("%i threads\n", numthreads); - usconfig(CONF_INITUSERS, numthreads); -} - - -void ThreadLock(void) { - spin_lock(&lck); -} - -void ThreadUnlock(void) { - release_lock(&lck); -} - - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int i; - int pid[MAX_THREADS]; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - init_lock(&lck); - - for (i = 0; i < numthreads - 1; i++) - { - pid[i] = sprocsp((void (*)(void*, size_t))func, PR_SALL, (void*)i - , NULL, 0x200000); // 2 meg stacks - if (pid[i] == -1) { - perror("sproc"); - Error("sproc failed"); - } - } - - func(i); - - for (i = 0; i < numthreads - 1; i++) - wait(NULL); - - threaded = qfalse; - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#elif GDEF_OS_LINUX || GDEF_OS_BSD || GDEF_OS_MACOS - -/* - ======================================================================= - - Linux pthreads - - ======================================================================= - */ - -#include - -int numthreads = -1; - -void ThreadSetDefault(void) { - if (numthreads == -1) { // not set manually -#ifdef _SC_NPROCESSORS_ONLN - long cpus = sysconf(_SC_NPROCESSORS_ONLN); - if (cpus > 0) { - numthreads = cpus; - } - else -#endif - /* can't detect, so default to four threads */ - numthreads = 4; - } - - if (numthreads > 1) { - Sys_Printf("threads: %d\n", numthreads); - } -} - -#include - -typedef struct pt_mutex_s -{ - pthread_t* owner; - pthread_mutex_t a_mutex; - pthread_cond_t cond; - unsigned int lock; -} pt_mutex_t; - -pt_mutex_t global_lock; - -void ThreadLock(void) { - pt_mutex_t* pt_mutex = &global_lock; - - if (!threaded) { - return; - } - - pthread_mutex_lock(&pt_mutex->a_mutex); - if (pthread_equal(pthread_self(), (pthread_t)&pt_mutex->owner)) { - pt_mutex->lock++; - } - else - { - if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { - pt_mutex->owner = (pthread_t*)pthread_self(); - pt_mutex->lock = 1; - } - else - { - while (1) - { - pthread_cond_wait(&pt_mutex->cond, &pt_mutex->a_mutex); - if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { - pt_mutex->owner = (pthread_t*)pthread_self(); - pt_mutex->lock = 1; - break; - } - } - } - } - pthread_mutex_unlock(&pt_mutex->a_mutex); -} - -void ThreadUnlock(void) { - pt_mutex_t* pt_mutex = &global_lock; - - if (!threaded) { - return; - } - - pthread_mutex_lock(&pt_mutex->a_mutex); - pt_mutex->lock--; - - if (pt_mutex->lock == 0) { - pt_mutex->owner = NULL; - pthread_cond_signal(&pt_mutex->cond); - } - - pthread_mutex_unlock(&pt_mutex->a_mutex); -} - -void recursive_mutex_init(pthread_mutexattr_t attribs) { - pt_mutex_t* pt_mutex = &global_lock; - - pt_mutex->owner = NULL; - if (pthread_mutex_init(&pt_mutex->a_mutex, &attribs) != 0) { - Error("pthread_mutex_init failed\n"); - } - if (pthread_cond_init(&pt_mutex->cond, NULL) != 0) { - Error("pthread_cond_init failed\n"); - } - - pt_mutex->lock = 0; -} - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - pthread_mutexattr_t mattrib; - pthread_attr_t attr; - pthread_t work_threads[MAX_THREADS]; - size_t stacksize; - - int start, end; - int i = 0; - - start = I_FloatTime(); - pacifier = showpacifier; - - dispatch = 0; - oldf = -1; - workcount = workcnt; - - pthread_attr_init(&attr); - if (pthread_attr_setstacksize(&attr, 8388608) != 0) { - stacksize = 0; - pthread_attr_getstacksize(&attr, &stacksize); - Sys_Printf("Could not set a per-thread stack size of 8 MB, using only %.2f MB\n", stacksize / 1048576.0); - } - - if (numthreads == 1) { - func(0); - } - else - { - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - if (pthread_mutexattr_init(&mattrib) != 0) { - Error("pthread_mutexattr_init failed"); - } - if (pthread_mutexattr_settype(&mattrib, PTHREAD_MUTEX_ERRORCHECK) != 0) { - Error("pthread_mutexattr_settype failed"); - } - recursive_mutex_init(mattrib); - - for (i = 0; i < numthreads; i++) - { - /* Default pthread attributes: joinable & non-realtime scheduling */ - if (pthread_create(&work_threads[i], &attr, (void* (*)(void*)) func, (void*)(uintptr_t)i) != 0) { - Error("pthread_create failed"); - } - } - for (i = 0; i < numthreads; i++) - { - if (pthread_join(work_threads[i], NULL) != 0) { - Error("pthread_join failed"); - } - } - pthread_mutexattr_destroy(&mattrib); - threaded = qfalse; - } - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#else // UNKNOWN OS - -/* - ======================================================================= - - SINGLE THREAD - - ======================================================================= - */ - -int numthreads = 1; - -void ThreadSetDefault(void) { - numthreads = 1; -} - -void ThreadLock(void) { -} - -void ThreadUnlock(void) { -} - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int start, end; - - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - start = I_FloatTime(); - func(0); - - end = I_FloatTime(); - if (pacifier) { - Com_Printf("%s (%i)\n", __func__, end - start); - } -} - -#endif // UNKNOWN OS - - +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + + + //== GLOBAL SEMAPHORE == +int antSearch; + +short int** path_table; +nav_area_t nav_area; + +qboolean nodeused[MAX_PNODES]; // This is used for a FAST check if the node has been used +short int nodefrom[MAX_PNODES]; // Stores how we got here once the node is closed +float nodeweight[MAX_PNODES]; // The weight of the node based on distance + +/* ========================================================= +The basic system works by using a single linked list and accessing information from the node array + +1) The current node is found +2) All links from it are propogated - if not already done by another node +3) If we haven't found the target node then we get the next open node and go to 1 +4) If we have the target node we return the path to it +5) If we run out of nodes then we return INVALID + +This idea is based on "Path Finding Via 'Ant Races' (a floodfill algorithm)" +by Richard Wesson. It is easy to optimise this code to make it even more efficient. + +============================================================= */ + + +//========================= +// Init the search path variables +//========================= +void AntInitSearch(edict_t* ent) +{ + //Make sure the lists and arrays used are all set to the correct values and EMPTY! + memset(nodeused, 0, sizeof(nodeused)); + memset(nodefrom, INVALID, sizeof(nodefrom)); + while (!SLLempty(&ent->pathList)) + { + SLLpop_front(&ent->pathList); + } +} + +///////////////////////////////////////////////////////// +// Check if any bot is navigating to the selected node // +///////////////////////////////////////////////////////// +qboolean Botlib_Nav_NodeInUse(edict_t* self, int node) +{ + //Com_Printf("%s ===============\n", __func__); + + if (node == INVALID) + return false; + + short int i; + for (i = 0; i < num_players; i++) + { + if (players[i]->is_bot == false || players[i] == self || players[i]->solid == SOLID_NOT || ACEAI_IsEnemy(self, players[i]) == false) + continue; + + botlib_sll_t* list = &players[i]->pathList; + if (list == NULL || list->head == NULL) // No path, or no head + continue; + + botlib_sll_nodes_t* sll_node = list->head; + + // Setup nodes + int current_node = players[i]->bot.current_node; + int next_node = sll_node->node; + + // Loop through the linked list + while (sll_node != NULL) // If no next + { + // Check if we're done, also sanity check + if (current_node == players[i]->bot.goal_node || current_node == INVALID || next_node == INVALID) + break; + + if (next_node == node) + { + //if (players[i]->client) + // Com_Printf("%s node[%d] is in use by %s\n", __func__, node, players[i]->client->pers.netname); + return true; + } + + //Com_Printf("%s curr[%d] next[%d] goal[%d]\n", __func__, current_node, next_node, self->bot.goal_node); + + + // Get next node + current_node = next_node; + if ((sll_node = sll_node->next) == NULL) // No next node + break; + next_node = sll_node->node; + + } + } + + //Com_Printf("%s ===============\n\n", __func__); + + return false; +} + +//========================= +// StartSearch +//========================= +// +// returns true if a path is found +// false otherwise +// +int quick = 0; +int normal = 0; +qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization) +{ + // Safety first! + if (from == INVALID || to == INVALID) + return false; + + //rekkie -- Dijkstra pathing -- s + if (1) + { + //clock_t clock_begin = clock(); // Start performance clock + //if (AntQuickPath(ent, from, to)) + { + //Com_Printf("%s AntQuickPath()\n", __func__); + + //clock_t clock_end = clock(); + //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; + //if (res == 0) quick++; + //Com_Printf("%s execution took %f seconds. Quick %d\n", __func__, res, quick); + + //return true; + } + //else + { + //BOTLIB_THREADED_DijkstraPath(ent, from, to); + if (BOTLIB_DijkstraPath(ent, from, to, path_randomization)) + //if (BOTLIB_DijkstraHeightPath(ent, from, to, path_randomization)) + //if (BOTLIB_HighestPath(ent, from, to, path_randomization)) + { + //Com_Printf("%s BOTLIB_DijkstraPath()\n", __func__); + + //clock_t clock_end = clock(); + //double res = (double)(clock_end - clock_begin) / CLOCKS_PER_SEC; + //if (res == 0) normal++; + //Com_Printf("%s execution took %f seconds. Normal %d\n", __func__, res, normal); + + return true; + } + } + + return false; + } + //rekkie -- Dijkstra pathing -- e + + + //@@ TESTING ONLY!! antSearch always available + antSearch = 1; + // Check we're allowed to search - if so, do it + if (1)// (antSearch > 0) && (ent->antLastCallTime < level.framenum - ANT_FREQ) ) + { + //Com_Printf("%s AntFindPath()\n", __func__); + // Decrement the semaphore to limit calls to this function + //@@ If we ever get multithreading then we can increment later + antSearch--; + // make a note of when this bot last made a path call + ent->antLastCallTime = level.framenum; + // Set up the lists + AntInitSearch(ent); + // If we found a path + if (AntFindPath(ent, from, to)) + { + // pathList now contains the links in reverse order + return true; + } + } + + // We can use the quick node search method here to get our path and put it in the pathList + // the same way we do with the AntSearch mode. This will have the side effect of finding + // bad paths and removing them. + if (AntQuickPath(ent, from, to)) + { + //Com_Printf("%s AntQuickPath()\n", __func__); + return true; + } + // If not allowed to search and no path + AntInitSearch(ent); // Clear out the path storage + return false; +} + +//================================= +// QuickPath +//================================= +// +// Uses the old path array to get a quick answer and removes bad paths +// + +qboolean AntQuickPath(edict_t* ent, int from, int to) +{ + int newNode = from; + int oldNode = 0; + int loopProtection = 0; //rekkie -- DEV_1 + + // Clean out the arrays, etc. + AntInitSearch(ent); + nodeused[from] = true; + // Check we can get from->to and that the path is complete + while (newNode != INVALID) + { + oldNode = newNode; + // get next node + newNode = path_table[newNode][to]; + if (newNode == to) + { + // We're there - store it then build the path + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + break; + } + else if (newNode == INVALID) + { + // We have a bad path + break; + } + else if (!nodeused[newNode]) + { + // Not been here yet - store it! + nodeused[newNode] = true; + nodefrom[newNode] = oldNode; + } + else + break; // LOOP encountered + } + + // If successful, build the pathList + if (newNode == to) + { + SLLpush_front(&ent->pathList, to); + while (newNode != from) + { + //rekkie -- s + if (newNode >= numnodes || newNode < 0) + { + if (debug_mode) + gi.dprintf("%s newNode out of bounds!\n", __func__); + return false; + } + //rekkie -- e + + // Push the + SLLpush_front(&ent->pathList, nodefrom[newNode]); + newNode = nodefrom[newNode]; + } + return true; + } + // else wipe out the bad path! + else + { + newNode = oldNode; + while (newNode != from) + { + path_table[nodefrom[newNode]][to] = INVALID; + + //rekkie -- DEV_1 -- s + if (loopProtection < numnodes) + loopProtection++; + else + { + if (debug_mode) + debug_printf("%s ------------------- Bad while loop. Forcing exit.\n", __func__); + break; + } + //rekkie -- DEV_1 -- e + } + path_table[from][to] = INVALID; + } + return false; +} + +//rekkie -- ZigZag pathing -- s +// The bot will zig and zag from node to node, trying to avoid enemy fire +qboolean BOTLIB_ZigZagPath(edict_t* ent, int from, int to) +{ + //Com_Printf("%s [%d]\n", __func__, level.framenum); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + + while (!SLLempty(&openList)) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + + // Update node weights, but don't add them to the list + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Add it to the open list + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order + // so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + //rekkie -- DEV_1 -- s + // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID + // Check if INVALID to avoid accessing [-1] array + if (nodefrom[to] != INVALID) + //rekkie -- DEV_1 -- e + path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // Push it onto the pathlist + SLLpush_front(&ent->pathList, newNode); + // Set the path in the node array to match this shortest path + path_table[nodefrom[newNode]][to] = newNode; + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + if (0) + { + Com_Printf("%s [%d] s[%d] g[%d] node_list[", __func__, level.framenum, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + + //Com_Printf("%s [%d] found a path\n", __func__, level.framenum); + return true; + } + + //Com_Printf("%s failed to find a path\n", __func__); + return false; +} + +//rekkie -- Dijkstra height pathing -- s +#if 0 +// Function unfinished. Wanted bots to prefer taking higher paths. +qboolean BOTLIB_DijkstraHeightPath(edict_t* ent, int from, int to, qboolean path_randomization) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + SLLpush_back(&openList, from); // Store it + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + break; // We found a path + } + + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; // Get the weight of the node we're visiting + the cost to the new node + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + } + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + return true; + } + + //Com_Printf("%s failed to find a path\n", __func__); + return false; +} +#endif +//rekkie -- Dijkstra Height Pathing -- e + +#if 0 +qboolean BOTLIB_NodeCanSeeEnemyPath(edict_t* ent, int atNode) +{ + // Check if node path can be seen by enemies + if (nodes[atNode].num_vis_nodes) // && random() <= 0.2) // && ent->bot.enemies_num && random() <= 0.2) + { + for (int v = 0; v < nodes[atNode].num_vis_nodes; v++) // each vis node + { + /* + for (int e = 0; e < num_players; e++) // each enemy + { + if (OnSameTeam(ent, players[e])) continue; + + for (int nl = 0; nl < players[e]->bot.node_list_count; nl++) // each node within the enemy's path + { + if (players[e]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + */ + + /* + for (int e = 0; e < ent->bot.enemies_num; e++) // each enemy + { + for (int nl = 0; nl < players[ent->bot.enemies[e]]->bot.node_list_count; nl++) // each node within the enemy's path + { + if (players[ent->bot.enemies[e]]->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + */ + + if (ent->enemy) + { + for (int nl = 0; nl < ent->enemy->bot.node_list_count; nl++) // each node within the enemy's path + { + if (ent->enemy->bot.node_list[nl] == nodes[atNode].vis_nodes[v]) // does our node see the enemy path? + { + return true; + } + } + } + } + } + return false; +} +#endif + + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +// Pathing is working, however some things to add,fix,do... +// +// 2) Also need to test ALL failure points, and try to trigger failures and test if its handled correctly. +// +// 3) Cache all nodes in each area. Leave room for an extra node (explained below what we use the extra for) +// +// 4) Currently when BOTLIB_CanVisitNode() is called, it will search all nodes. I need this to search and path only the +// areas which are involved in the search. Currently the next_node/edge node is located outside the current area of the bot, +// and just inside the next area (the border). Therefore, I should include that extra node in the search. So if we're in Area 0, +// and the next node is in Area 1 (next to Area 0), then search will be all nodes in Area 0 + the single node from Area 1. +// Once the search + path is conducted, remove the node from the search. +// +// 5) Deal with situtations where the areas are connected, but the goal node is not accessible in the last area. Perhaps after checking +// if the areas connect, run a BOTLIB_CanVisitNode() search on the last area to see if it connects from the last edge node +// to the final goal node. ALSO, there might a scenario where the random selected last edge node does not connect, but one of the +// many other edge nodes might? I guess in such a case, a connection might path through the other edge nodes anyway. +// +// 6) When selecting a path from nav_area.dfs_paths[self->bot.path_taken][i], the paths are a series of contigous areas A>B>C>D,etc +// I need an algorithm (similar to the heightmap) to see if areas are being overly used, and/or taking the higher area is preferred +// over a lower area. I could also cache area combos and check agains them. Going back to the heightmap idea, if area C (middle TJ) +// is being overly used, just check if any of the paths contain 'C' and avoid using them? +// +// 7) When picking a random edge node, perhaps I could pick the closest edge node to the current node, thereby making it a shortest path +// by direct line. However, this isn't the shortest 'path', only a direct line. +// +// 8) Check if other bots are using the edge node, if so, pick another edge node if available. +// +// 9) Move some of the arrays over to dynamic allocated memory, build the array as a single block (single malloc call) +// +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +int DFS_area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // Area nodes - [(32 * 1024) * 4 bytes = 132k] +int DFS_area_edges[MAX_NAV_AREAS][MAX_NAV_AREAS_EDGES]; // Area edge nodes (area edges that connect to other areas) - [(32 * 64) * 4 bytes = 8k] + +// Init data used when selecting nodes +void BOTLIB_InitAreaNodes(void) +{ + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + DFS_area_nodes[a][n] = INVALID; + } + } + + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) + { + DFS_area_edges[a][e] = INVALID; + } + } +} + +void BOTLIB_InitAreaConnections() +{ + // For all areas, find all their connecting neighbors. + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + nav_area.adjacency_matrix[i][a] = INVALID; // Adjacency matrix + } + } + int targetNode = INVALID; + int targetArea = INVALID; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].area == i) + { + for (int l = 0; l < nodes[n].num_links; l++) + { + targetNode = nodes[n].links[l].targetNode; + targetArea = nodes[targetNode].area; + //if (targetArea != INVALID && targetArea != i) + if (targetArea != i) + { + nav_area.adjacency_matrix[i][targetArea] = targetArea; + } + } + } + } + } + + BOTLIB_UpdateAllAreaEdges(); // Update edge connections + + // Find total areas + nav_area.total_areas = 0; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.adjacency_matrix[i][a] != INVALID) + { + nav_area.total_areas++; + break; + } + } + } + + if (nav_area.total_areas == 0) + { + Com_Printf("%s WARNING: Map has no area nodes. Bot pathing will be less optimal and large maps not supported.\n", __func__); + return; + } + + // Print connections + /* + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.adjacency_matrix[i][a] != INVALID) + { + Com_Printf("Nav area [%d] connects to [%d]\n", i, a); + } + } + } + */ + + + // Cache all the nodes in each area + BOTLIB_MallocAreaNodes(); // Alloc memory for area nodes + // Check if memory was allocated + if (nav_area.area_nodes && nav_area.area_nodes[0]) + { + // Init areas with INVALID + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + nav_area.area_nodes[i][n] = INVALID; + } + } + + // Cache all the nodes in each area -- currently a max of (MAX_NAV_AREAS_NODES - 1) nodes per area is allowed + qboolean reached_max_area_nodes; + //qboolean notified = false; + for (int i = 0; i < numnodes; i++) + { + reached_max_area_nodes = true; + for (int n = 0; n < (MAX_NAV_AREAS_NODES - 1); n++) // Cache up to (MAX_NAV_AREAS_NODES - 1) + { + if (nav_area.area_nodes[nodes[i].area][n] == INVALID) + { + reached_max_area_nodes = false; + nav_area.area_nodes[nodes[i].area][n] = nodes[i].nodenum; + break; + } + } + if (reached_max_area_nodes)// && notified == false) + { + //notified = true; + Com_Printf("%s Nav area [%d] reached_max_area_nodes [%d]\n", __func__, nodes[i].area, MAX_NAV_AREAS_NODES); + break; + } + } + + BOTLIB_RandomizeAreaColors(); + + /* + // Print out area nodes + int total_nodes; + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + if (nav_area.area_nodes[i][0] == INVALID) + continue; + + total_nodes = 0; + + Com_Printf("%s Area [%d] nodes: ", __func__, i); + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (nav_area.area_nodes[i][n] == INVALID) + break; + + Com_Printf(" %d ", nav_area.area_nodes[i][n]); + total_nodes++; + } + Com_Printf("\nTotal nodes: %d\n", total_nodes); + } + */ + } +} + +// Create area nodes mallocs +void BOTLIB_MallocAreaNodes() +{ + // Dynamically cache all the nodes in each area -- currently a max of 1024 nodes per area is allowed + //area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area + nav_area.area_nodes = (int**)malloc(sizeof(*nav_area.area_nodes) * MAX_NAV_AREAS); + if (!nav_area.area_nodes) { + Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); + nav_area.area_nodes = NULL; + } + else + { + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + nav_area.area_nodes[i] = (int*)malloc(sizeof(*(nav_area.area_nodes[i])) * MAX_NAV_AREAS_NODES); + if (!nav_area.area_nodes[i]) + { + Com_Printf("%s Memory allocation failed for nav_area.area_nodes\n", __func__); + //nav_area.area_nodes = NULL; + } + } + // Now we can use nav_area.area_nodes[i][j] to access any element :) + } +} +// Free area nodes mallocs +void BOTLIB_FreeAreaNodes() +{ + if (nav_area.area_nodes) + { + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + if (nav_area.area_nodes[i]) + { + free(nav_area.area_nodes[i]); // Freeing memory allocated for each row separately + nav_area.area_nodes[i] = NULL; + } + } + free(nav_area.area_nodes); // And then freeing the pointer to the pointers + nav_area.area_nodes = NULL; + } +} + +// This initiates bot travel from current node to goal node +// 1) The bot will use area nodes if map has areas (Efficient), +// 2) Alternatively it will fallback to doing node-to-node pathing (Less efficient) +// 3) path_randomization only applies to the fallback method +qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomization) +{ + if (goal_node == INVALID) + return false; + + //Com_Printf("%s\n", __func__); + + self->bot.node_list_count = 0; + + // New pathing - if area nodes are supported + if (nav_area.total_areas > 0 && goal_node) + { + Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); + if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path + { + while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path + { + } + + // Add the goal to the end of the node list + terminate + if (self->bot.node_list_count) + { + self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + } + + return true; // Success + } + else + { + Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); + return false; // Failure + } + } + else // Fallback to old pathing + { + self->bot.goal_node = INVALID; + //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); + if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) + { + + // Add the goal to the end of the node list + terminate + if (self->bot.node_list_count) + { + self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + } + + return true; // Success + } + } + + Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); + return false; // Failure to find path +} + +// Checks if goal is reachable from current node +qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node) +{ + if (goal_node == INVALID) + { + Com_Printf("%s INVALID PATH: goal_node is invalid\n", __func__); + return false; + } + + //Com_Printf("\n\n========================================================\n", __func__); + //Com_Printf("%s Heading for goal node[%d] area[%d]\n", __func__, goal_node, self->bot.goal_area); + //Com_Printf("========================================================\n", __func__); + + //BOTLIB_InitAreaConnections(); + + // Init area + self->bot.start_area = INVALID; + //self->bot.current_area = INVALID; + //self->bot.goal_area = INVALID; + + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + + if (self->bot.current_node == INVALID) + { + //Com_Printf("%s INVALID PATH: Cannot find current_node\n", __func__); + self->bot.next_area_node = INVALID; + return false; + } + else + { + self->bot.start_area = nodes[self->bot.current_node].area; // Area we first started from + self->bot.current_area = nodes[self->bot.current_node].area; // Current area we're in + self->bot.goal_area = nodes[goal_node].area; // Goal area to reach + self->bot.next_area_counter = 0; // Keep track of which area is next, starting from the first area (current area) + self->bot.next_area_node = self->bot.current_node; + + + + + // If node is inside of the same (or final) area try to see if we can navigation there + // Otherwise continue with trying to route to the goal_node's area + if (self->bot.current_area == self->bot.goal_area) + { + //if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, false)) + { + self->bot.next_area_nodes[0] = goal_node; // Last node + self->bot.next_area_nodes_counter = 0; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[1] = INVALID; // Terminate + + self->bot.goal_node = goal_node; // Set the goal node to reach + self->bot.path_taken = 0; // Get rand path + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + Com_Printf("%s Goal node[%d] is in the same area[%d] as your area [%d]\n", __func__, goal_node, self->bot.goal_area, self->bot.current_area); + return true; // Success + } + } + + } + + + + + /* + // Pick a random area to visit + // DO NOT DELETE, it will be useful later on + //while (nav_area.goal_area == nav_area.start_area) // Pick a goal that isn't the same area as bot is in + // nav_area.goal_area = rand() % nav_area.total_areas; // Get rand goal + // Get goal node within goal area + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == nav_area.goal_area) + { + nav_area.goal_node = nodes[i].nodenum; // Get first node in area + //nav_area.goal_node = DFS_area_nodes[nav_area.goal_area][0]; // Get first node in area + Com_Printf("%s Goal node [%d]\n", __func__, nav_area.goal_node); + break; + } + } + */ + + + // Calculate all paths + for (int i = 0; i < MAX_NAV_AREAS_PATHS; i++) + { + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + nav_area.dfs_paths[i][a] = INVALID; + } + } + nav_area.dfs_path_count = 0; + memset(nav_area.dfs_visited, false, sizeof(nav_area.dfs_visited)); + + nav_area.dfs_path[0] = 0; + nav_area.dfs_len = 0; + //nav_area.dfs_visited[0] = false; + BOTLIB_DepthFirstSearch(self, self->bot.current_area, self->bot.goal_area); // Calculate all possible paths + + //Com_Printf("%s DFS_path_count >= %d\n", __func__, nav_area.dfs_path_count); + + //Com_Printf("%s total_areas[%d] start[%d] curr[%d] goal[%d] path_taken[%d]\n", __func__, nav_area.total_areas, nav_area.start_area, nav_area.current_area, nav_area.goal_area, nav_area.path_taken); + + // Print all paths + /* + if (nav_area.dfs_path_count) + { + for (int i = 0; i < nav_area.dfs_path_count; i++) + { + Com_Printf("%s [%d]Path: ", __func__, i); + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.dfs_paths[i][a] == INVALID) + break; + + Com_Printf("%d ", nav_area.dfs_paths[i][a]); + } + Com_Printf("\n"); + } + } + */ + + // Instead of picking a random path, pick the less used path... + + // Pick a random path + if (nav_area.dfs_path_count) // Success + { + /* + self->bot.goal_node = goal_node; // Set the goal node to reach + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + // Precalculate all edge-to-edge nodes + Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); + int prev_area = self->bot.current_area; + int next_area; + int next_node; + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + next_area = nav_area.dfs_paths[self->bot.path_taken][i]; + + if (next_area == INVALID) // End of path + { + Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); + self->bot.next_area_nodes[i] = goal_node; // Last node + self->bot.next_area_nodes_counter = i; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[i + 1] = INVALID; // Terminate + break; + } + else + { + next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); + self->bot.next_area_nodes[i] = next_node; + Com_Printf("[%d][%d] ", next_area, next_node); + } + + prev_area = next_area; + } + Com_Printf("\n"); + */ + + //self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node + //next_area_nodes + + BOTLIB_GetAreaPath(self, goal_node); + + return true; // Success + } + else // Failure + { + self->bot.path_taken = INVALID; + //Com_Printf("%s NO VALID PATH from area %d to %d\n", __func__, self->bot.current_area, self->bot.goal_area); + return false; // Faulure + } + + return true; // Success +} + +// +qboolean BOTLIB_MakeAreaPath(edict_t* self, int goal_node, qboolean use_heatmap) +{ + self->bot.next_area_nodes_counter = 0; + int prev_area = self->bot.current_area; + int next_area; + int next_node; + qboolean debug = false; + + if (debug) Com_Printf("%s Found a valid edge path [AREA][NODE]: [%d][%d] ", __func__, self->bot.current_area, self->bot.current_node); + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + next_area = nav_area.dfs_paths[self->bot.path_taken][i]; + + self->bot.area_heatmap[next_area] += 1; // Increase heatmap + if (use_heatmap) // If we're avoiding an area + { + if (self->bot.area_heatmap[next_area] >= 3) + { + //Com_Printf("\n"); + //self->bot.area_heatmap[next_area] = 0; // Reset heatmap + //return false; // Failure + } + } + + if (next_area == INVALID) // End of path + { + if (debug) Com_Printf("[%d][%d] ", nodes[goal_node].area, goal_node); + self->bot.next_area_nodes[i] = goal_node; // Last node + self->bot.next_area_nodes_counter = i + 1; // Total nodes in next_area_nodes[] + self->bot.next_area_nodes[i + 1] = INVALID; // Terminate + break; + } + else + { + next_node = BOTLIB_GetRandomEdgeConnection(prev_area, next_area); + if (next_node == INVALID) + { + if (debug) Com_Printf("[%d][%d] ", next_area, next_node); + if (debug) Com_Printf(" -- INVALID PATH\n"); + return false; // Failure to find connected edge connection + } + self->bot.next_area_nodes[i] = next_node; + if (debug) Com_Printf("[%d][%d] ", next_area, next_node); + } + + prev_area = next_area; + } + if (debug) Com_Printf("\n"); + + return true; // Success +} + + +// Precalculate all edge-to-edge nodes +void BOTLIB_GetAreaPath(edict_t* self, int goal_node) +{ + self->bot.goal_node = goal_node; // Set the goal node to reach + ////self->bot.state = BOT_MOVE_STATE_NAV_NEXT; + + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + if (BOTLIB_MakeAreaPath(self, goal_node, false) == true) // Success + break; + } + + /* + qboolean success = false; + for (int i = 0; i < nav_area.dfs_path_count; i++) + { + self->bot.path_taken = i; // Cycle through paths + success = BOTLIB_MakeAreaPath(self, goal_node, true); + if (success) + break; + } + if (success == false) + { + self->bot.path_taken = rand() % nav_area.dfs_path_count; // Get rand path + BOTLIB_MakeAreaPath(self, goal_node, false); + } + */ +} + +qboolean BOTLIB_GetNextAreaNode(edict_t *self) +{ + if (self->bot.next_area_nodes_counter == self->bot.next_area_counter) + { + Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); + return false; + } + + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update current node + if (self->bot.current_node == INVALID || self->bot.goal_node == INVALID) + { + Com_Printf("%s INVALID PATH\n", __func__); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return false; + } + int prev_node = self->bot.current_node; + if (self->bot.next_area_counter) + { + prev_node = self->bot.node_list[self->bot.node_list_count - 1]; + //prev_node = self->bot.next_area_nodes[self->bot.next_area_counter - 1]; + } + int next_node = self->bot.next_area_nodes[self->bot.next_area_counter]; + if (next_node != INVALID) + { + //if (BOTLIB_CanVisitNode(self, nodes[next_node].nodenum, false, INVALID, self->bot.next_area_counter)) + //if (BOTLIB_DijkstraAreaPath(self, prev_node, next_node, false, nodes[prev_node].area, self->bot.next_area_counter)) + if (BOTLIB_DijkstraPath(self, prev_node, next_node, false)) + { + self->bot.next_area_node = next_node; + self->bot.next_area_counter++; + + //Com_Printf("%s [%d] prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] curr[%d] goal[%d]\n", __func__, self->bot.next_area_counter, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.current_node, self->bot.goal_node); + + //if (self->bot.goal_node == nodes[next_node].nodenum) + // Com_Printf("%s Successfully made a path to goal node [%d]\n", __func__, self->bot.goal_node); + + if (next_node == self->bot.goal_node) // Reached goal node + { + self->bot.current_node = self->bot.node_list[0]; + self->bot.next_node = self->bot.node_list[1]; + self->node_timeout = 0; + self->bot.state = BOT_MOVE_STATE_MOVE; + return false; + } + + return true; + } + else + { + //Com_Printf("%s Could not visit node -- failed to reach prev_node[%d] prev_area[%d] next_node[%d] next_area[%d] goal[%d]\n", __func__, prev_node, nodes[prev_node].area, next_node, nodes[next_node].area, self->bot.goal_node); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return false; + } + } + + return false; + + /* + int last_node = self->bot.next_area_nodes[self->bot.next_area_nodes_counter]; + float dist = VectorDistance(nodes[self->bot.current_node].origin, nodes[last_node].origin); + if ( dist < 128 ) + { + //Com_Printf("%s Success, reached goal node[%d] from node[%d] dist[%f]\n", __func__, self->bot.goal_node, self->bot.current_node, dist); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return true; + } + */ + + //Com_Printf("%s Failed to reach next node [%d] curr_node[%d] goal[%d]\n", __func__, next_node, self->bot.current_node, self->bot.goal_node); + + +#if 0 + + + + if (self->bot.path_taken == INVALID) + { + Com_Printf("%s No valid paths found\n", __func__); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return; + } + + /* + float dist = 999999; + if (nav_area.next_area_node != INVALID) + { + dist = VectorDistance(nodes[nav_area.next_area_node].origin, nodes[self->bot.current_node].origin); + if (dist > 256.0f) + return; + } + */ + + // Update current area + self->bot.current_area = nodes[self->bot.current_node].area; + + // Update next area + self->bot.next_area = nav_area.dfs_paths[self->bot.path_taken][self->bot.next_area_counter]; + self->bot.next_area_counter++; + + // Check if final area reached + if (self->bot.next_area == INVALID) + { + if (self->bot.goal_node != INVALID && BOTLIB_CanVisitNode(self, nodes[self->bot.goal_node].nodenum, false, nodes[self->bot.current_node].area)) + { + Com_Printf("%s Goal area reached. Heading for goal node [%d] within the final area [%d]\n", __func__, self->bot.goal_node, self->bot.current_area); + return; + } + else + { + Com_Printf("%s Failed to reach final goal node[%d]: g_area[%d], c_area[%d], n_area[%d]\n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_area, self->bot.next_area); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav goal + return; + } + } + + // Com_Printf("%s c_area[%d] n_area[%d] g_area[%d]\n", __func__, self->bot.current_area, self->bot.next_area, self->bot.goal_area); + + /* + Com_Printf("%s Path taken: ", __func__); + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + if (nav_area.dfs_paths[self->bot.path_taken][a] == INVALID) + break; + + Com_Printf("%d ", nav_area.dfs_paths[self->bot.path_taken][a]); + } + Com_Printf("\n"); + */ + + + //BOTLIB_UpdateAllAreaEdges(); // Update edge connections + self->bot.next_area_node = BOTLIB_GetRandomEdgeConnection(self->bot.current_area, self->bot.next_area); // Get a random edge node + //Com_Printf("%s c_node[%d] n_node[%d] c_area[%d] n_area[%d] g_area[%d] path_taken[%d]\n", __func__, self->bot.current_node, self->bot.next_area_node, self->bot.current_area, self->bot.next_area, self->bot.goal_area, self->bot.path_taken); + //Com_Printf("%s goal_area[%d] current_area[%d] --> next_area[%d]\n", __func__, self->bot.goal_area, self->bot.current_area, self->bot.next_area); + + if (self->bot.next_area_node != INVALID) + { + if (BOTLIB_CanVisitNode(self, nodes[self->bot.next_area_node].nodenum, false, nodes[self->bot.current_node].area)) + { + Com_Printf("%s heading to next node[%d] area[%d] goal[%d]\n", __func__, nodes[self->bot.next_area_node].nodenum, self->bot.next_area, self->bot.goal_node); + return; + } + } + + Com_Printf("%s Failed to reach NextAreaNode -- goal N[%d] A[%d] -- curr N[%d] A[%d] -- next N[%d] A[%d] \n", __func__, self->bot.goal_node, self->bot.goal_area, self->bot.current_node, self->bot.current_area, self->bot.next_area_node, self->bot.next_area); +#endif +} + +// Stores all the nodes within an area that connect to an external area (edge nodes) +// 1) Wipe old edge data +// 2) Find and store edge nodes +void BOTLIB_UpdateAllAreaEdges(void) +{ + for (int a = 0; a < MAX_NAV_AREAS; a++) + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) + { + DFS_area_edges[a][e] = INVALID; + } + } + + int targetNode; + for (int i = 0; i < numnodes; i++) + { + for (int l = 0; l < nodes[i].num_links; l++) + { + targetNode = nodes[i].links[l].targetNode; + if (nodes[i].area != nodes[targetNode].area) // If node links to a node outside of its own area + { + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges + { + if (DFS_area_edges[nodes[i].area][e] == INVALID) // Find free spot to store a new edge + { + //Com_Printf("%s area[%d to %d] node[%d to %d]\n", __func__, nodes[i].area, nodes[targetNode].area, i, targetNode); + DFS_area_edges[nodes[i].area][e] = targetNode; // Store the edge node + break; + } + } + } + } + } +} + +// Get a random edge node from two connected areas +// Returns a random edge node that is inside the second area (so the bot traverses just inside the next area) +int BOTLIB_GetRandomEdgeConnection(int area_1, int area_2) +{ + int a2n; // Area 2 node + int ec = 0; // Edge counter + int edges[MAX_NAV_AREAS_EDGES] = { INVALID }; // Store the edges that connect from A1 to A2 + + // Find and store all connected nodes from A1 to A2 + for (int e = 0; e < MAX_NAV_AREAS_EDGES; e++) // Search area edges + { + if (DFS_area_edges[area_1][e] == INVALID) // End of edges stored + break; + + a2n = DFS_area_edges[area_1][e]; + if (nodes[a2n].area == area_2) // Find all areas that connect from A1 to A2 + { + //Com_Printf("%s area[%d to %d] node[%d] counter[%d]\n", __func__, area_1, area_2, a2n, ec); + edges[ec++] = a2n; // Store the area_2 node + } + } + + if (ec) + return edges[rand() % ec]; // Return a random edge node + else + return INVALID; // No connected edge nodes found +} + +// Finds ALL possible paths from current to destination using the Depth-First Search (DFS) algorithm +void BOTLIB_DepthFirstSearch(edict_t* self, int current, int destination) +{ + if (current == destination) + { + if (nav_area.dfs_path_count >= MAX_NAV_AREAS_PATHS) + { + //Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_path_count >= %d\n", __func__, MAX_NAV_AREAS_PATHS); + return; + } + if (nav_area.dfs_len >= MAX_NAV_AREAS) + { + Com_Printf("%s ERROR: Cannot add to DFS_paths[] because DFS_len >= %d\n", __func__, MAX_NAV_AREAS); + return; + } + + if (nav_area.dfs_len) // If there's an actual path + { + // Check if path passes through itself (loops back and touches the starting area again) + // Don't cross the streams! It would be very bad! Egon. + for (int i = 0; i < nav_area.dfs_len; i++) + { + if (self->bot.start_area == nav_area.dfs_path[i]) // If path crosses start_area again + { + return; // Reject path + } + + for (int j = 0; j < nav_area.dfs_len; j++) + { + if (i == j) continue; + + if (nav_area.dfs_path[j] == nav_area.dfs_path[i]) // If path crosses same area twice + { + return; // Reject path + } + } + } + + + for (int i = 0; i < nav_area.dfs_len; i++) + { + nav_area.dfs_paths[nav_area.dfs_path_count][i] = nav_area.dfs_path[i]; // Copy from tmp path to full path array + } + nav_area.dfs_path_count++; + } + + return; + } + for (int i = 0; i < MAX_NAV_AREAS; i++) + { + if (nav_area.adjacency_matrix[current][i] != INVALID && !nav_area.dfs_visited[i]) + { + nav_area.dfs_path[nav_area.dfs_len++] = i; // Copy to tmp path + nav_area.dfs_visited[i] = true; // Visited + BOTLIB_DepthFirstSearch(self, i, destination); // Recursive search (cycles) + nav_area.dfs_len--; // Backtrack + nav_area.dfs_visited[i] = false; // Unvisit + } + } +} + +// Check if area already exists +qboolean BOTLIB_AreaExists(int area_num) +{ + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == area_num) + { + return true; // Area exists + } + } + return false; // Area does not exist +} +// Attempts to automatically add areas to map +void BOTLIB_AutoArea(edict_t* self) +{ + int curr_area_num = 0; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].area == 0) + { + self->bot.walknode.selection_node_count = 0; + BOTLIB_GroupConnectedNodeArea(self, i); // Try grouping nodes from this node + + if (self->bot.walknode.selection_node_count) + { + //Check if area exists + for (; curr_area_num < MAX_NAV_AREAS; curr_area_num++) + { + if (BOTLIB_AreaExists(curr_area_num) == false) + break; + } + if (curr_area_num + 1 >= MAX_NAV_AREAS) + { + Com_Printf("%s WARNING: Cannot add more areas, maximum allowed %d\n", __func__, MAX_NAV_AREAS); + break; + } + // Found a free area + for (int s = 0; s < self->bot.walknode.selection_node_count; s++) + { + nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; + nodes[self->bot.walknode.selection_nodes[s]].area = curr_area_num; + } + } + } + } + + if (curr_area_num) + { + BOTLIB_FreeAreaNodes(); + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } +} + +// Randomize area colors +void BOTLIB_RandomizeAreaColors() +{ + int color; + for (int i = 0; i < MAX_NAV_AREAS; ++i) + { + // Try to randomize colors while keeping at least one of the R,G,B values above 128+ + int rng = rand() % 3; + if (rng == 0) + color = MakeColor((rand() % 255), (rand() % 255), (rand() % 128) + 128, 255); // Randomize color + else if (rng == 1) + color = MakeColor((rand() % 255), (rand() % 128) + 128, (rand() % 255), 255); // Randomize color + else + color = MakeColor((rand() % 128) + 128, (rand() % 255), (rand() % 255), 255); // Randomize color + + for (int n = 0; n < MAX_NAV_AREAS_NODES; n++) + { + if (nav_area.area_nodes[i][n] == INVALID) + break; + + nodes[nav_area.area_nodes[i][n]].area_color = color; + } + } +} + +// ==================================================================================== +// ==================================================================================== +// Create local area nodes memory alloc +int total_local_area_nodes; +qboolean* local_area_nodes; +qboolean BOTLIB_MallocLocalAreaNodes() +{ + total_local_area_nodes = 0; + local_area_nodes = NULL; + + //nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + local_area_nodes = (qboolean*)malloc(sizeof(local_area_nodes) * (numnodes + 1)); + if (local_area_nodes == NULL) + { + Com_Printf("%s Memory allocation failed for local_area_nodes\n", __func__); + local_area_nodes = NULL; + return false; + } + + for (int i = 0; i < numnodes; i++) + { + local_area_nodes[i] = false; // Init to false + } + + return true; // Success +} +// Free local area nodes memory alloc +void BOTLIB_FreeLocalAreaNodes() +{ + if (local_area_nodes) + { + free(local_area_nodes); + local_area_nodes = NULL; // Nullify dangling pointer + } +} +// A recurrsive function that finds all nodes that are connected and pathed to the initial `start_node` and are of the same area num +void BOTLIB_GroupConnectedNodeArea_r(edict_t* self, int next_node) //, vec3_t normal) +{ + for (int i = 0; i < numnodes; i++) + { + if (local_area_nodes[i] == true) + continue; + + if (nodes[i].area == nodes[next_node].area) + { + for (int l = 0; l < nodes[i].num_links; l++) + { + if (nodes[i].links[l].targetNode == next_node) + { + if (self->bot.walknode.selection_node_count >= MAX_NAV_AREAS_NODES) + break; + + // Check if node already included in selection + qboolean added = false; + for (int s = 0; s < self->bot.walknode.selection_node_count; s++) + { + if (self->bot.walknode.selection_nodes[s] == i) + { + added = true; + break; + } + } + if (added) + continue; // Skip adding because we already have it + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + // Need to test if the two nodes are on the same plane/normal/height/distance + + if (VectorDistance(nodes[i].origin, nodes[self->bot.walknode.selection_nodes[0]].origin) > 1024) + continue; + + // Check hight diff of connected node - ignore nodes with large differences + //Com_Printf("%s FABS[%f]\n", __func__, fabs(nodes[i].origin[2] - nodes[next_node].origin[2])); + if (fabs(nodes[i].origin[2] - nodes[next_node].origin[2]) > 32) + continue; + + // Only nodes on the same normal - with a small epsilon + if (BOTLIB_VectorCompare(nodes[i].normal, nodes[next_node].normal, 0.1) == false) + continue; + + + //Com_Printf("%s [%f %f %f] vs [%f %f %f]\n", __func__, nodes[next_node].normal[0], nodes[next_node].normal[1], nodes[next_node].normal[2], nodes[i].normal[0], nodes[i].normal[1], nodes[i].normal[2]); + + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + local_area_nodes[next_node] = true; // Flag node as true + //total_local_area_nodes++; + + self->bot.walknode.selection_nodes[self->bot.walknode.selection_node_count] = i; + self->bot.walknode.selection_node_count++; + + BOTLIB_GroupConnectedNodeArea_r(self, i); // Scan the connected node + } + } + } + } +} + +void BOTLIB_GroupConnectedNodeArea(edict_t* self, int start_node) +{ + if (BOTLIB_MallocLocalAreaNodes() == false) // Alloc memory + return; // Failure + + local_area_nodes[start_node] = true; // Initiate starting node to true + //total_local_area_nodes++; + + /* + trace_t tr = gi.trace(tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] + 32), NULL, NULL, tv(nodes[start_node].origin[0], nodes[start_node].origin[1], nodes[start_node].origin[2] - 256), NULL, MASK_SHOT); + if (tr.fraction < 1.0) + { + VectorCopy(tr.plane.normal, nodes[start_node].normal); + } + + Com_Printf("%s [%f %f %f]\n", __func__, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); + */ + + //BOTLIB_SetAllNodeNormals(); // Set all the normals + + BOTLIB_GroupConnectedNodeArea_r(self, start_node); // Recurrsively check all nodes + + //total_local_area_nodes + //Com_Printf("%s total_local_area_nodes[%d]\n", __func__, total_local_area_nodes); + + //for (int i = 0; i < numnodes; i++) + { + //if (local_area_nodes[i] == true) + { + } + } + + BOTLIB_FreeLocalAreaNodes(); // Free memory +} +// ==================================================================================== +// ==================================================================================== + +//rekkie -- Dijkstra Area Pathing -- s +// Currently if path_randomization is enabled, the bot may not be able to reach its target, +// therefore it's only really used when heading to a random node. +// Area: specify the area we're working with; only searching nodes within this area +// build_new_path: If we're building a new path or adding to an existing path +qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_randomization, int area, qboolean build_new_path) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID || area == INVALID) + return false; + + // Check for null pointers + if (nav_area.area_nodes == NULL || nav_area.area_nodes[0] == NULL) + return false; + + //if (build_new_path) + { + //ent->bot.node_list_count = 0; // Reset the list of nodes to zero + //ent->bot.node_list_current = 0; // Set the starting point to zero + } + + AntInitSearch(ent); // Clear out the path storage + + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + //int searched_nodes = 0; + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + //searched_nodes++; + //Com_Printf("%s NODE[%d] AREAS: ", __func__, atNode); +//#if 0 + // Find the node with the lowest weight + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (newNode == INVALID) + continue; + + // Only search in current area + if (newNode != to && nodes[newNode].area != area) + continue; + + //Com_Printf(" %d ", nodes[newNode].area); + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + /* + if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + */ + } + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] == NAV_INFINITE) + { + lowest_node = INVALID; + break; + } + + if (nodeweight[newNode] < lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + } + } + } + //Com_Printf("\n"); + + // Use the shortest path + if (lowest_node != INVALID) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[lowest_node]) + { + if (lowest_node != from && lowest_node != to) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + } + + nodeweight[lowest_node] = weight; // Update the weight + nodefrom[lowest_node] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, lowest_node); // Store it + //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + } + + if (lowest_node == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + break; // We found a path + } + } + else +//#endif + { + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (newNode == INVALID) + continue; + + // Only search in current area + if (newNode != to && nodes[newNode].area != area) + continue; + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + + } + } + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + } + + //Com_Printf("%s searched_nodes[%d]\n", __func__, searched_nodes); + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array + ////if (nodefrom[newNode] != INVALID) + { + ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + } + + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high + if (nodes[newNode].num_links > 3) + { + nodes[newNode].weight += 0.1; + if (nodes[newNode].weight > 0.5) // Limit max weight + nodes[newNode].weight = 0; // Reset back to normal weight + if (nodes[newNode].weight < 0) // Sanity check + nodes[newNode].weight = 0; // Reset back to normal weight + } + + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + if (SLLempty(&ent->pathList)) + return false; // Failure + + return true; // Success + } + + // If we get to this point, it means our path failed + // If path_randomization was true, then try again without it + if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it + { + BOTLIB_DijkstraAreaPath(ent, from, to, false, area, build_new_path); + } + // Else: just fail it completely + { + //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); + return false; // Failure + } +} +//rekkie -- Dijkstra Area Pathing -- e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//rekkie -- Dijkstra pathing -- s +// Currently if path_randomization is enabled and the bot fails to path to its target, +// BOTLIB_DijkstraPath() is run again with path_randomization turned off +qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization) +{ + //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + + // Sanity check + if (from == INVALID || to == INVALID) + return false; + + AntInitSearch(ent); // Clear out the path storage + for (int i = 0; i < numnodes; i++) + nodeweight[i] = NAV_INFINITE; + + int newNode = INVALID; // Stores the node being tested + int atNode = from; // Current node we're visiting + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // Init the list + + float weight; // Weight of the node we're visiting + nodeweight[atNode] = 0; // Set the weight to 0 (distance from self to self is 0) + nodefrom[atNode] = atNode; // Set the parent node to itself + //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list + SLLpush_back(&openList, from); // Store it + + + while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list + { + atNode = SLLfront(&openList); // Get the next node + if (atNode == to) // If the next node is the goal node + { + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + break; // We found a path + } + + /* + // Update node weights, but don't add them to the list + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeweight[newNode] == NAV_INFINITE || nodeweight[newNode] < weight) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + //SLLpush_back(&openList, newNode); // Add it to the open list + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + + // Find the node link with the shortest path + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] <= lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + } + } + } + + // Add the shortest path to the open list + if (lowest_node != INVALID) + { + SLLpush_back(&openList, lowest_node); // Add it to the open list + //Com_Printf("%s [%d to %d] lowest_node[%d]\n", __func__, from, to, lowest_node); + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + */ + + + + //* + // Find the node with the lowest weight + float lowest_cost = NAV_INFINITE; + int lowest_node = INVALID; + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + /* + if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + */ + } + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + if (nodeweight[newNode] == NAV_INFINITE) + { + lowest_node = INVALID; + break; + } + + if (nodeweight[newNode] < lowest_cost) + { + lowest_cost = nodeweight[newNode]; + lowest_node = newNode; + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + } + } + } + // Use the shortest path + if (lowest_node != INVALID) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[lowest_node]) + { + if (lowest_node != from && lowest_node != to) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (lowest_node != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + } + + nodeweight[lowest_node] = weight; // Update the weight + nodefrom[lowest_node] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, lowest_node); // Store it + //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + } + + if (lowest_node == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + break; // We found a path + } + } + else + { + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode == INVALID) + continue; + if (nodes[atNode].links[i].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + // Check if the node can be seen by enemy's path + //if (BOTLIB_NodeCanSeeEnemyPath(ent, atNode)) continue; + + // Consider skipping this node if it has many links and its weight is high + if (nodes[newNode].num_links > 3 && nodes[newNode].weight > random()) + continue; + + if (path_randomization) + { + // [1] Less expensive + // Randomize paths + if (newNode != to && random() <= 0.2) // how often we skip checking a linked node + continue; + } + + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + break; // We found a path + } + + } + } + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + //*/ + + + + /* + for (int i = 0; i < nodes[atNode].num_links; i++) // Look at each link from this node + { + if (nodes[atNode].links[i].targetNode != INVALID) + { + newNode = nodes[atNode].links[i].targetNode; // Get the next linked node + // Get the weight of the node we're visiting + the cost to the new node + weight = nodeweight[atNode] + nodes[atNode].links[i].cost; + + if (nodeused[newNode] == false) // If the node has not been visited (CLOSED LIST) + { + // If weight is less than the weight of the new node + if (weight < nodeweight[newNode]) + { + nodeweight[newNode] = weight; // Update the weight + nodefrom[newNode] = atNode; // Update the parent node (open list) + SLLpush_back(&openList, newNode); // Store it + } + } + + if (newNode == to) // If node being linked is the goal node + { + break; // We found a path + } + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + nodeused[atNode] = true; // Mark node as visited (CLOSED LIST) + */ + } + + + // Free up the memory we allocated + SLLdelete(&openList); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + // This happens when a bot dies and respawns, nodefrom[newNode] spits out INVALID. Check if INVALID to avoid accessing [-1] array + ////if (nodefrom[newNode] != INVALID) + { + ////path_table[nodefrom[newNode]][newNode] = newNode; // Set the 'newNode' path in node array because this is shortest path + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + } + + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // The bot has taken this node so increase its weight so other bots will consider skipping this node if its weight is high + if (nodes[newNode].num_links > 3) + { + nodes[newNode].weight += 0.1; + if (nodes[newNode].weight > 0.5) // Limit max weight + nodes[newNode].weight = 0; // Reset back to normal weight + if (nodes[newNode].weight < 0) // Sanity check + nodes[newNode].weight = 0; // Reset back to normal weight + } + + SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist + ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + //ent->bot.node_list_count = 0; + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + if (SLLempty(&ent->pathList)) + return false; // Failure + + return true; // Success + } + + // If we get to this point, it means our path failed + // If path_randomization was true, then try again without it + if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it + { + BOTLIB_DijkstraPath(ent, from, to, false); + } + // Else: just fail it completely + { + //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); + return false; // Failure + } +} +//rekkie -- Dijkstra pathing -- e + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//======================= +// FindPath +//======================= +// +// Uses OPEN and CLOSED lists to conduct a search +// Many refinements planned +// +qboolean AntFindPath(edict_t* ent, int from, int to) +{ + int counter = 0; // Link array counter + int link_counter = 0; // Link counter + int newNode = INVALID; // Stores the node being tested + int atNode; // Structures for search + botlib_sll_t openList; // Locally declared OPEN list + openList.head = openList.tail = NULL; // MUST do this!! + + // Safety first again - we don't want crashes! + if (from == INVALID || to == INVALID) + return false; + + // Put startnode on the OPEN list + atNode = from; + nodefrom[atNode] = INVALID; + SLLpush_back(&openList, from); + nodeused[from] = true; + + //Com_Printf("%s NEW PATH [%d to %d]\n", __func__, from, to); + + + // While there are nodes on the OPEN list AND we are not at destNode + while (!SLLempty(&openList) && newNode != to) + { + // Where we are + atNode = SLLfront(&openList); + + //Com_Printf("%s atNode [%d]\n", __func__, atNode); + + // Safety check + if (atNode <= INVALID) + return false; + + + // Get random start point (instead of always starting at 0) + int start_point = 0; + //if (nodes[atNode].num_links) // If we have links + // start_point = rand() % (nodes[atNode].num_links); // start at random link + //if (start_point + 1 > MAXLINKS) + // start_point = 0; + + //int start_point = 0; + counter = start_point; + + // Go to next link, check for unused node + //while (++counter != start_point) + while (counter != nodes[atNode].num_links) + { + /* + if (counter + 1 > MAXLINKS) //MAXLINKS + { + counter = 0; + if (counter == start_point) + break; + } + */ + + if (nodes[atNode].links[counter].targetNodeType == NODE_POI_LOOKAT) // POI look at nodes are not real links + continue; + + // Using an array for FAST access to the path rather than a CLOSED list + newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + + //Com_Printf("%s counter[%d] newNode [%d]\n", __func__, counter, newNode); + counter++; + + if (newNode == INVALID) + continue; + else + { + /* + // Check for failure rates on links + // If the fail rate is > 0, then we have a chance of skipping the node + // The higher the targetFailures, the higher the chance of skipping + //Com_Printf("%s %s: n[%d] l[%d] f[%d]\n", __func__, ent->client->pers.netname, newNode, counter, nodes[atNode].links[counter].targetFailures); + if (nodes[atNode].links[counter].targetFailures > 1) + { + float percent = (1 / nodes[atNode].links[counter].targetFailures); + if (random() < percent) + { + //Com_Printf("%s %s: Skipping node %d due to %d failures\n", __func__, ent->client->pers.netname, newNode, nodes[atNode].links[counter].targetFailures); + continue; + } + } + */ + + if (0) + { + // [1] Less expensive + // Randomize paths + //if (random() < 0.1) // how often we skip a targetNode + // continue; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + //if (random() < 0.01) + { + // Check if node is free, otherwise check next link + if (Botlib_Nav_NodeInUse(ent, newNode) == false) + if (random() < 0.1) + continue; + } + } + + //if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) + // continue; + + // If newNode NOT on open or closed list + if (!nodeused[newNode]) + { + nodeused[newNode] = true; // Mark node as used + nodefrom[newNode] = atNode; // Add to OPEN list + SLLpush_back(&openList, newNode); // Store it + } + + if (newNode == to) // If node being linked is destNode then quit + break; + } + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + } + + + + /* + // While there are nodes on the OPEN list AND we are not at destNode + while (!SLLempty(&openList) && newNode != to) + { + counter = 0; + + // Where we are + atNode = SLLfront(&openList); + + ////Com_Printf("%s atNode [%d]\n", __func__, atNode); + + // Safety check + if (atNode <= INVALID) + return false; + + // Using an array for FAST access to the path rather than a CLOSED list + newNode = nodes[atNode].links[counter].targetNode; + + // Process this node putting linked nodes on the OPEN list + while (newNode != INVALID) + { + ////Com_Printf("%s newNode [%d]\n", __func__, newNode); + + // If newNode NOT on open or closed list + if (!nodeused[newNode]) + { + nodeused[newNode] = true; // Mark node as used + nodefrom[newNode] = atNode; // Add to OPEN list + SLLpush_back(&openList, newNode); // Store it + } + + if (newNode == to) // If node being linked is destNode then quit + break; + + // Go to next link, check for unused node + while (++counter < MAXLINKS) + { + newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + + if(newNode == INVALID) + break; + + // [1] Less expensive + // Randomize paths + if (nodes[atNode].links[counter].targetNodeType == NODE_LADDER_UP || nodes[atNode].links[counter].targetNodeType == NODE_LADDER_DOWN) + break; + if (random() < 0.25) // how often we skip a targetNode + continue; + else + break; + + // [2] Expensive + // Try to occasionally break up paths, expensive operation + //if (random() < 0.88) + //{ + // // Check if node is free, otherwise check next link + // if (Botlib_Nav_NodeInUse(ent, newNode) == false) + // break; + //} + //else + // break; + + } + if (counter >= MAXLINKS || newNode == INVALID) + break; + + //if (++counter >= MAXLINKS) // Increase counter and check we're in bounds + // break; + //newNode = nodes[atNode].links[counter].targetNode; // Get next linked node + } + + SLLpop_front(&openList); // Remove atNode from the OPEN List + } + */ + + // Free up the memory we allocated + SLLdelete(&openList); + + //Com_Printf("%s PATH newNode [%d] to [%d]\n", __func__, newNode, to); + + // Optimise stored path with this new information + if (newNode == to) + { + // Make the path using the fromnode array pushing node numbers on in reverse order + // so we can SLLpop_front them back later + SLLpush_front(&ent->pathList, newNode); + + //rekkie -- DEV_1 -- s + // This happens when a bot dies and respawns, nodefrom[to] spits out INVALID + // Check if INVALID to avoid accessing [-1] array + if (nodefrom[to] != INVALID) + //rekkie -- DEV_1 -- e + path_table[nodefrom[to]][to] = to; // Set the to path in node array because this is shortest path + + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, to, to, to); + //int prev_newNode = newNode; + + // We earlier set our start node to INVALID to set up the termination + // Check if there is a path, and it's not the node we're standing on (safety check) + while ((newNode = nodefrom[newNode]) != INVALID && (newNode != from)) + { + //Com_Printf("%s %d = nodefrom[%d]\n", __func__, newNode, prev_newNode); + //prev_newNode = newNode; + + // Push it onto the pathlist + SLLpush_front(&ent->pathList, newNode); + // Set the path in the node array to match this shortest path + path_table[nodefrom[newNode]][to] = newNode; + + //Com_Printf("%s path_table[ nodefrom[%d] ][ %d ] = %d\n", __func__, newNode, to, newNode); + } + + //Com_Printf("%s EXIT PATH\n\n", __func__); + + // Each time a path is found, make a copy + ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list + ent->bot.node_list_current = 0; + if (ent->bot.node_list_count) // Set the current and next nodes + { + ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.next_node = ent->bot.node_list[1]; + } + + if (0) + { + if (1) + { + Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + for (int i = 0; i < ent->bot.node_list_count; i++) + { + Com_Printf(" %d ", ent->bot.node_list[i]); + } + Com_Printf(" ]\n"); + } + else + Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + //Com_Printf("%s found a path\n", __func__); + } + + return true; + } + // else + return false; +} + +//============================= +// LinkExists +//============================= +// +// Check we haven't wandered off path! +// +qboolean AntLinkExists(int from, int to) +{ + int counter = 0; + int testnode; + + if (from == INVALID || to == INVALID) + return false; + + // Check if the link exists + while (counter < MAXLINKS) + { + testnode = nodes[from].links[counter].targetNode; // Get next linked node + if (testnode == to) + { + return true; // A path exists from -> to + } + else if (testnode == INVALID) + { + return false; // No more links and no path found + } + counter++; + } + + return false; // Didn't find it! +} + + + + +// Draw the bot path +qboolean BOTLIB_DrawPath(edict_t* self) +{ + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + int curr_node, next_node; + float line_width; + uint32_t color; + uint32_t blue = MakeColor(0, 0, 255, 255); // Blue + uint32_t red = MakeColor(255, 0, 0, 255); // Red + void (*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) = NULL; + void (*DrawArrow)(int number, vec3_t start, vec3_t end, const uint32_t color, float line_width, int time, qboolean occluded) = NULL; + DrawBox = players[0]->client->pers.draw->DrawBox; + DrawArrow = players[0]->client->pers.draw->DrawArrow; + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + players[0]->client->pers.draw->arrows_inuse = true; // Flag as being used + int jumppad_box_num = 0; // Counter to display more than one jumpbox at once + int start = self->bot.node_list_current - 1; + if (start < 0) + start = 0; + for (int i = start; (i + 1) < self->bot.node_list_count; i++) + { + curr_node = self->bot.node_list[i]; // Next node in the list + next_node = self->bot.node_list[i + 1]; // Next node in the list + + if (curr_node == INVALID || next_node == INVALID) + break; + + if (players[0]->client->pers.draw->draw_arrow_num + 1 > MAX_DRAW_ARROWS) + players[0]->client->pers.draw->draw_arrow_num = 0; + players[0]->client->pers.draw->draw_arrow_num; + + // Draw current link as thicker line + if (self->bot.current_node == curr_node) + { + line_width = 10.0; + color = MakeColor(255, 255, 0, 255); // Yellow + } + else // Draw all other links as thinner lines + { + line_width = 1.0; + color = MakeColor(0, 255, 0, 255); // Green + } + + // Show jumppad nodes as a blue box + for (int i = 0; i < nodes[curr_node].num_links; i++) + { + if (nodes[curr_node].links[i].targetNode == next_node) + { + if (nodes[curr_node].links[i].targetNodeType == NODE_JUMPPAD) + { + DrawBox(jumppad_box_num, nodes[curr_node].origin, blue, nodes[curr_node].mins, nodes[curr_node].maxs, 100, false); + jumppad_box_num++; + } + break; + } + } + + DrawArrow(players[0]->client->pers.draw->draw_arrow_num, nodes[curr_node].origin, nodes[next_node].origin, color, line_width, 100, false); + players[0]->client->pers.draw->draw_arrow_num++; + } + + // Draw goal node + DrawBox(jumppad_box_num, nodes[self->bot.goal_node].origin, red, nodes[self->bot.goal_node].mins, nodes[self->bot.goal_node].maxs, 100, false); + +#endif + //rekkie -- debug drawing -- e + + return true; +} + + + + +//============================== +// SLLpush_front +//============================== +// Add to the front of the list +// +void SLLpush_front(botlib_sll_t* list, int node) +{ + botlib_sll_nodes_t* temp; + + // Store the current head pointer + temp = list->head; + // allocate memory for the new data (LEVEL tagged) + list->head = gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); + // Set up the data and pointer + list->head->node = node; + list->head->next = temp; + // Check if there;'s a next item + if (!list->head->next) + { + // Set the tail pointer = head + list->tail = list->head; + } +} + +//============================== +// SLLpop_front +//============================== +// Remove the item from the front of the list +// +void SLLpop_front(botlib_sll_t* list) +{ + botlib_sll_nodes_t* temp; + + // Store the head pointer + temp = list->head; + // Check if there's a next item + if (list && list->head) + { + if (list->head == list->tail) + { + // List is now emptying + list->tail = list->head = NULL; + } + else + { + // Move head to point to next item + list->head = list->head->next; + } + // Free the memory (LEVEL tagged) + gi.TagFree(temp); + } + else + { + gi.bprintf(PRINT_HIGH, "Attempting to POP an empty list!\n"); + } +} + +//rekkie -- DEV_1 -- s +//============================== +// BOTLIB_SLL_Query_Next_Node +//============================== +// Query the SLL for the next node +// Returns the next node, otherwise returns INVALID if next node cannot be found (end of list, or list is NULL) +int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list) +{ + if (0) //DEBUG: Print out the entire list + { + // Check if there's a next item + if (list && list->head && list->head != list->tail && list->head->next != NULL) + { + Com_Printf("%s nodes[ ", __func__); + Com_Printf("%d ", list->head->node); // Print head node + botlib_sll_nodes_t* next = list->head->next; + while (next != NULL) + { + if (list->head == list->tail) // We're already at the last item + break; + else + { + Com_Printf("%d ", next->node); // Print next node + next = next->next; // Get the next SLL item + } + } + Com_Printf("]\n"); + } + } + + // Check if there's a next item + if (list && list->head) + { + if (list->head == list->tail || list->head->next == NULL) // We're already at the last item + { + return INVALID; // No next item + } + else + { + botlib_sll_nodes_t* next = list->head->next; // Get the next SLL item + return next->node; // return the next node + } + } + + return INVALID; // Empty list +} +//============================== +// BOTLIB_SLL_Query_All_Nodes +//============================== +// Query the SLL list and return all its nodes +// node_list is the returned list of nodes +// max_nodes is the maximum allowed nodes we can store in node_list +// Returns the node count +int BOTLIB_SLL_Query_All_Nodes(edict_t *ent, botlib_sll_t* list, int* node_list, const int max_nodes) +{ + //int node_count = 0; // Node count + + if (ent->bot.node_list_count + 1 > max_nodes) + return ent->bot.node_list_count; + + // Check if there's a next item + if (list && list->head && list->head != list->tail && list->head->next != NULL) + { + //node_list[ent->bot.node_list_count++] = ent->bot.current_node; // Save current node + node_list[ent->bot.node_list_count++] = list->head->node; // Save head node + botlib_sll_nodes_t* next = list->head->next; + while (next != NULL) + { + if (ent->bot.node_list_count + 1 > max_nodes) return ent->bot.node_list_count; + + if (list->head == list->tail) // We're already at the last item + break; + else + { + node_list[ent->bot.node_list_count++] = next->node; // Save next node + next = next->next; // Get the next SLL item + } + } + } + + return ent->bot.node_list_count; // Return node count +} +//rekkie -- DEV_1 -- e + +//============================== +// SLLfront +//============================== +// Get the integer value from the front of the list +// without removing the item (Query the list) +// +int SLLfront(botlib_sll_t* list) +{ + if (list && !SLLempty(list)) + return(list->head->node); + else + return INVALID; +} + +//============================== +// SLLpush_front +//============================== +// Add to the back of the list +// +void SLLpush_back(botlib_sll_t* list, int node) +{ + botlib_sll_nodes_t* temp; + + // Allocate memory for the new item (LEVEL tagged) + temp = (botlib_sll_nodes_t*)gi.TagMalloc(sizeof(botlib_sll_nodes_t), TAG_LEVEL); + // Store the data + temp->node = node; + temp->next = NULL; // End of the list + // Store the new item in the list + // Is the list empty? + if (!list->head) + { + // Yes - add as a new item + list->head = temp; + list->tail = temp; + } + else + { + // No make this the new tail item + list->tail->next = temp; + list->tail = temp; + } +} + +//============================== +// SLLempty +//============================== +// See if the list is empty (false if not empty) +// +qboolean SLLempty(botlib_sll_t* list) +{ + // If there is any item in the list then it is NOT empty... + if (list) + return (list->head == NULL); + else // No list so return empty + return true; +} + +//=============================== +// Delete the list +//=============================== +// Avoids memory leaks +// +void SLLdelete(botlib_sll_t* list) +{ + botlib_sll_nodes_t* temp; + + while (!SLLempty(list)) + { + temp = list->head; + list->head = list->head->next; + gi.TagFree(temp); + } +} + +//=============================== +// Quake 3 Multithreading Code +//=============================== + +/* + ================ + I_FloatTime + ================ + */ +double I_FloatTime(void) { + time_t t; + + time(&t); + + return t; +#if 0 + // more precise, less portable + struct timeval tp; + struct timezone tzp; + static int secbase; + + gettimeofday(&tp, &tzp); + + if (!secbase) { + secbase = tp.tv_sec; + return tp.tv_usec / 1000000.0; + } + + return (tp.tv_sec - secbase) + tp.tv_usec / 1000000.0; +#endif +} + + +// Threads.c -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/threads.c +#define GDEF_OS_WINDOWS 1 +#if !GDEF_OS_WINDOWS +// The below define is necessary to use +// pthreads extensions like pthread_mutexattr_settype +#define _GNU_SOURCE +#endif // !GDEF_OS_WINDOWS + +#define MAX_THREADS 64 + +int dispatch; +int workcount; +int oldf; +qboolean pacifier; + +qboolean threaded; + +/* + ============= + GetThreadWork + + ============= + */ +int GetThreadWork(void) { + int r; + int f; + + ThreadLock(); + + if (dispatch == workcount) { + ThreadUnlock(); + return -1; + } + + f = 40 * dispatch / workcount; + if (f < oldf) { + Com_Printf("%s WARNING: progress went backwards (should never happen)\n", __func__); + oldf = f; + } + Com_Printf("%s ", __func__); + while (f > oldf) + { + ++oldf; + if (pacifier) { + if (oldf % 4 == 0) { + Com_Printf("%i", f / 4); + } + else { + Com_Printf("."); + } + fflush(stdout); /* ydnar */ + } + } + Com_Printf("\n"); + + r = dispatch; + dispatch++; + ThreadUnlock(); + + return r; +} + + +void (*workfunction)(int); + +void ThreadWorkerFunction(int threadnum) { + int work; + + while (1) + { + work = GetThreadWork(); + if (work == -1) { + break; + } + //Com_Printf("%s thread %i, work %i\n", __func__, threadnum, work); + workfunction(work); + } +} + +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)) { +// void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { +// if (numthreads == -1) { +// ThreadSetDefault(); +// } +// workfunction = func; +// RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); +// } + + +#if _WIN32 + +/* + =================================================================== + + WIN32 + + =================================================================== + */ + +#include + +int numthreads = -1; +CRITICAL_SECTION crit; +static int enter; + +void ThreadSetDefault(void) { + SYSTEM_INFO info; + + if (numthreads == -1) { // not set manually + GetSystemInfo(&info); + numthreads = info.dwNumberOfProcessors; + if (numthreads < 1 || numthreads > 32) { + numthreads = 1; + } + } + + Com_Printf("%s %i threads\n", __func__, numthreads); +} + + +void ThreadLock(void) { + if (!threaded) { + return; + } + EnterCriticalSection(&crit); + if (enter) { + Com_Printf("%s Recursive ThreadLock\n", __func__); + } + enter = 1; +} + +void ThreadUnlock(void) { + if (!threaded) { + return; + } + if (!enter) { + Com_Printf("%s ThreadUnlock without lock\n", __func__); + } + enter = 0; + LeaveCriticalSection(&crit); +} + +// Init and and run the threaded version +void BOTLIB_THREAD_LOADAAS(qboolean force) +{ + loadaas_t loadaas; + loadaas.force = force; + //RunThreadsOnIndividual(2, true, TestThreadedFunc, &loadaas); + //if (numthreads == -1) { + // ThreadSetDefault(); + //} + numthreads = 1; + GetThreadWork(); + RunThreadsOn(1, true, BOTLIB_THREADING_LOADAAS, &loadaas); +} + +// Threaded version +void BOTLIB_THREADING_LOADAAS(void *param) +{ + // Sleep while map fully loads and is ready, otherwise we'll get a crash + while (level.framenum < 50) + { + Sleep(100); + } + + //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web + //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing + loadaas_t* params = (loadaas_t*)param; + if (params != NULL) + { + //Com_Printf("%s param:0x%x force = %d\n", __func__, ¶m, params->force); + ACEND_LoadAAS(params->force); + } +} + +// ===================================================================================== +// Init and and run the threaded version +void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to) +{ + dijkstra_path_t dpath; + dpath.ent = ent; + dpath.from = from; + dpath.to = to; + + numthreads = 1; + GetThreadWork(); + //RunThreadsOn(1, true, BOTLIB_THREADING_DijkstraPath, &dpath); + + HANDLE thdHandle = CreateThread(NULL, 0, BOTLIB_THREADING_DijkstraPath, &dpath, 0, NULL); + if (thdHandle != NULL) + WaitForSingleObject(thdHandle, INFINITE); +} + +// Threaded version +void BOTLIB_THREADING_DijkstraPath(void* param) +{ + //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web + //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing + dijkstra_path_t* dpath = (dijkstra_path_t*)param; + if (dpath != NULL) + { + //Com_Printf("%s dpath:0x%x from[%d] to[%d]\n", __func__, &dpath, dpath->from, dpath->to); + BOTLIB_DijkstraPath(dpath->ent, dpath->from, dpath->to, true); + } +} +// ===================================================================================== + +/* + ============= + RunThreadsOn + ============= + */ +//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func), void *param) { + int threadid[MAX_THREADS]; + HANDLE threadhandle[MAX_THREADS]; + int i; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + // + // run threads in parallel + // + InitializeCriticalSection(&crit); + + if (numthreads < 1) + return; + + //if (numthreads == 1) { // use same thread + // func(0); + //} + //else + { + for (i = 0; i < numthreads; i++) + { + threadhandle[i] = CreateThread( + NULL, // LPSECURITY_ATTRIBUTES lpsa, + //0, // DWORD cbStack, + + /* ydnar: cranking stack size to eliminate radiosity crash with 1MB stack on win32 */ + (4096 * 1024), + + (LPTHREAD_START_ROUTINE)func, // LPTHREAD_START_ROUTINE lpStartAddr, + (LPVOID)param, + //(LPVOID)i, // LPVOID lpvThreadParm, + 0, // DWORD fdwCreate, + &threadid[i]); + } + + //for (i = 0; i < numthreads; i++) + // WaitForSingleObject(threadhandle[i], INFINITE); + } + DeleteCriticalSection(&crit); + + threaded = qfalse; + end = I_FloatTime(); + if (pacifier) { + Com_Printf("%s (%i)\n", __func__, end - start); + } +} + + +#elif GDEF_OS_OSF1 + +/* + =================================================================== + + OSF1 + + =================================================================== + */ + +int numthreads = 4; + +void ThreadSetDefault(void) { + if (numthreads == -1) { // not set manually + numthreads = 4; + } +} + +#include + +pthread_mutex_t* my_mutex; + +void ThreadLock(void) { + if (my_mutex) { + pthread_mutex_lock(my_mutex); + } +} + +void ThreadUnlock(void) { + if (my_mutex) { + pthread_mutex_unlock(my_mutex); + } +} + + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int i; + pthread_t work_threads[MAX_THREADS]; + pthread_addr_t status; + pthread_attr_t attrib; + pthread_mutexattr_t mattrib; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + if (!my_mutex) { + my_mutex = safe_malloc(sizeof(*my_mutex)); + if (pthread_mutexattr_create(&mattrib) == -1) { + Error("pthread_mutex_attr_create failed"); + } + if (pthread_mutexattr_setkind_np(&mattrib, MUTEX_FAST_NP) == -1) { + Error("pthread_mutexattr_setkind_np failed"); + } + if (pthread_mutex_init(my_mutex, mattrib) == -1) { + Error("pthread_mutex_init failed"); + } + } + + if (pthread_attr_create(&attrib) == -1) { + Error("pthread_attr_create failed"); + } + if (pthread_attr_setstacksize(&attrib, 0x100000) == -1) { + Error("pthread_attr_setstacksize failed"); + } + + for (i = 0; i < numthreads; i++) + { + if (pthread_create(&work_threads[i], attrib + , (pthread_startroutine_t)func, (pthread_addr_t)i) == -1) { + Error("pthread_create failed"); + } + } + + for (i = 0; i < numthreads; i++) + { + if (pthread_join(work_threads[i], &status) == -1) { + Error("pthread_join failed"); + } + } + + threaded = qfalse; + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#elif GDEF_OS_IRIX + +/* + =================================================================== + + IRIX + + =================================================================== + */ + +#define USED + +#include +#include +#include +#include + +int numthreads = -1; +abilock_t lck; + +void ThreadSetDefault(void) { + if (numthreads == -1) { + numthreads = prctl(PR_MAXPPROCS); + } + Sys_Printf("%i threads\n", numthreads); + usconfig(CONF_INITUSERS, numthreads); +} + + +void ThreadLock(void) { + spin_lock(&lck); +} + +void ThreadUnlock(void) { + release_lock(&lck); +} + + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int i; + int pid[MAX_THREADS]; + int start, end; + + start = I_FloatTime(); + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + init_lock(&lck); + + for (i = 0; i < numthreads - 1; i++) + { + pid[i] = sprocsp((void (*)(void*, size_t))func, PR_SALL, (void*)i + , NULL, 0x200000); // 2 meg stacks + if (pid[i] == -1) { + perror("sproc"); + Error("sproc failed"); + } + } + + func(i); + + for (i = 0; i < numthreads - 1; i++) + wait(NULL); + + threaded = qfalse; + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#elif GDEF_OS_LINUX || GDEF_OS_BSD || GDEF_OS_MACOS + +/* + ======================================================================= + + Linux pthreads + + ======================================================================= + */ + +#include + +int numthreads = -1; + +void ThreadSetDefault(void) { + if (numthreads == -1) { // not set manually +#ifdef _SC_NPROCESSORS_ONLN + long cpus = sysconf(_SC_NPROCESSORS_ONLN); + if (cpus > 0) { + numthreads = cpus; + } + else +#endif + /* can't detect, so default to four threads */ + numthreads = 4; + } + + if (numthreads > 1) { + Sys_Printf("threads: %d\n", numthreads); + } +} + +#include + +typedef struct pt_mutex_s +{ + pthread_t* owner; + pthread_mutex_t a_mutex; + pthread_cond_t cond; + unsigned int lock; +} pt_mutex_t; + +pt_mutex_t global_lock; + +void ThreadLock(void) { + pt_mutex_t* pt_mutex = &global_lock; + + if (!threaded) { + return; + } + + pthread_mutex_lock(&pt_mutex->a_mutex); + if (pthread_equal(pthread_self(), (pthread_t)&pt_mutex->owner)) { + pt_mutex->lock++; + } + else + { + if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { + pt_mutex->owner = (pthread_t*)pthread_self(); + pt_mutex->lock = 1; + } + else + { + while (1) + { + pthread_cond_wait(&pt_mutex->cond, &pt_mutex->a_mutex); + if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { + pt_mutex->owner = (pthread_t*)pthread_self(); + pt_mutex->lock = 1; + break; + } + } + } + } + pthread_mutex_unlock(&pt_mutex->a_mutex); +} + +void ThreadUnlock(void) { + pt_mutex_t* pt_mutex = &global_lock; + + if (!threaded) { + return; + } + + pthread_mutex_lock(&pt_mutex->a_mutex); + pt_mutex->lock--; + + if (pt_mutex->lock == 0) { + pt_mutex->owner = NULL; + pthread_cond_signal(&pt_mutex->cond); + } + + pthread_mutex_unlock(&pt_mutex->a_mutex); +} + +void recursive_mutex_init(pthread_mutexattr_t attribs) { + pt_mutex_t* pt_mutex = &global_lock; + + pt_mutex->owner = NULL; + if (pthread_mutex_init(&pt_mutex->a_mutex, &attribs) != 0) { + Error("pthread_mutex_init failed\n"); + } + if (pthread_cond_init(&pt_mutex->cond, NULL) != 0) { + Error("pthread_cond_init failed\n"); + } + + pt_mutex->lock = 0; +} + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + pthread_mutexattr_t mattrib; + pthread_attr_t attr; + pthread_t work_threads[MAX_THREADS]; + size_t stacksize; + + int start, end; + int i = 0; + + start = I_FloatTime(); + pacifier = showpacifier; + + dispatch = 0; + oldf = -1; + workcount = workcnt; + + pthread_attr_init(&attr); + if (pthread_attr_setstacksize(&attr, 8388608) != 0) { + stacksize = 0; + pthread_attr_getstacksize(&attr, &stacksize); + Sys_Printf("Could not set a per-thread stack size of 8 MB, using only %.2f MB\n", stacksize / 1048576.0); + } + + if (numthreads == 1) { + func(0); + } + else + { + threaded = qtrue; + + if (pacifier) { + setbuf(stdout, NULL); + } + + if (pthread_mutexattr_init(&mattrib) != 0) { + Error("pthread_mutexattr_init failed"); + } + if (pthread_mutexattr_settype(&mattrib, PTHREAD_MUTEX_ERRORCHECK) != 0) { + Error("pthread_mutexattr_settype failed"); + } + recursive_mutex_init(mattrib); + + for (i = 0; i < numthreads; i++) + { + /* Default pthread attributes: joinable & non-realtime scheduling */ + if (pthread_create(&work_threads[i], &attr, (void* (*)(void*)) func, (void*)(uintptr_t)i) != 0) { + Error("pthread_create failed"); + } + } + for (i = 0; i < numthreads; i++) + { + if (pthread_join(work_threads[i], NULL) != 0) { + Error("pthread_join failed"); + } + } + pthread_mutexattr_destroy(&mattrib); + threaded = qfalse; + } + + end = I_FloatTime(); + if (pacifier) { + Sys_Printf(" (%i)\n", end - start); + } +} + + +#else // UNKNOWN OS + +/* + ======================================================================= + + SINGLE THREAD + + ======================================================================= + */ + +int numthreads = 1; + +void ThreadSetDefault(void) { + numthreads = 1; +} + +void ThreadLock(void) { +} + +void ThreadUnlock(void) { +} + +/* + ============= + RunThreadsOn + ============= + */ +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { + int start, end; + + dispatch = 0; + workcount = workcnt; + oldf = -1; + pacifier = showpacifier; + start = I_FloatTime(); + func(0); + + end = I_FloatTime(); + if (pacifier) { + Com_Printf("%s (%i)\n", __func__, end - start); + } +} + +#endif // UNKNOWN OS + + diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 7125b7d0d..12c8149ec 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -1,5576 +1,5576 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -int num_poi_nodes; -int poi_nodes[MAX_POI_NODES]; -edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -int num_vis_nodes; -int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -int node_vis_list[10][10]; // Cached node -node_t *nodes; -nmesh_t nmesh; - -// Free nodes -void BOTLIB_FreeNodes(void) -{ - if (nodes) - { - Com_Printf("%s --- Freeing nodes ---\n", __func__); - free(nodes); - nodes = NULL; // Nullify dangling pointer - } - - if (path_table) - { - Com_Printf("%s --- Freeing path_table ---\n", __func__); - for (int r = 0; r < MAX_PNODES; r++) { - free(path_table[r]); - path_table[r] = NULL; // Nullify dangling pointer - } - - free(path_table); - path_table = NULL; // Nullify dangling pointer - } -} - - -// Adds a new or inactive 'inuse == false' flagged node (from deleting a node) -// Returns the node that was added/reused, INVALID if node was not added -int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type) -{ - // ================================================================================= - // Avoid placing nodes on 'hurt_touch' trigger areas - // ================================================================================= - if (0) - { - solid_t* trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); - if (trigger_solid == NULL) - { - Com_Printf("%s failed to malloc trigger_solid\n", __func__); - return INVALID; - } - BOTLIB_MakeEntsSolid(trigger_solid); // Save solid state of each ent - if (BOTLIB_UTIL_CHECK_FOR_HURT(origin)) - { - Com_Printf("%s ignoring hurt_touch\n", __func__); - return INVALID; - } - BOTLIB_RestoreEntsSolidState(trigger_solid); // Restore solid state, and free memory - } - // ================================================================================= - - int node = INVALID; - // Check for any unused nodes first - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].inuse == false) - { - node = i; // Free to use this node - break; - } - } - - if (node == INVALID) - { - // Absolute max allowed - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); - return INVALID; - } - - // ------------- - // Alloc memory - // ------------- - node_t* prev = NULL; - - // Nodes - if (numnodes == 0) - nodes = (node_t*)malloc(sizeof(node_t)); - else - { - prev = nodes; // Keep a copy - nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); - } - if (nodes == NULL) - { - Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); - if (prev) - { - free(prev); // Free using the copy, because nodes is null - nodes = NULL; - prev = NULL; - } - return INVALID; - } - - node = numnodes; - } - - // ------------------------------------------------------------------------------------- - // nodes - // ------------------------------------------------------------------------------------- - nodes[node].area = 0; // Area - nodes[node].weight = 0; // Weight - - VectorCopy(origin, nodes[node].origin); // Location - - //VectorCopy(origin, nodes[node].origin); //TODO::::::::::::::::::::::: Angles (for ladders, sniper spots, etc) - - VectorCopy(normal, nodes[node].normal); // Surface Normal - vec3_t mins = { -16, -16, -24 }; - vec3_t maxs = { 16, 16, 32 }; - VectorCopy(mins, nodes[node].mins); // Box mins - VectorCopy(maxs, nodes[node].maxs); // Box maxs - if (type == NODE_CROUCH) - nodes[node].maxs[2] = CROUCHING_MAXS2; - if (type == NODE_BOXJUMP) - { - nodes[node].mins[0] = -8; - nodes[node].mins[1] = -8; - nodes[node].mins[2] = -12; - nodes[node].maxs[0] = 8; - nodes[node].maxs[1] = 8; - nodes[node].maxs[2] = 16; - } - VectorAdd(origin, mins, nodes[node].absmin); // Update absolute box min/max in the world - VectorAdd(origin, maxs, nodes[node].absmax); // Update absolute box min/max in the world - nodes[node].type = type; // Node type - nodes[node].nodenum = node; // Node number - nodes[node].inuse = true; // Node is inuse - nodes[node].num_links = 0; // Node links - for (int i = 0; i < MAXLINKS; i++) // Init links - { - nodes[node].links[i].targetNode = INVALID; // Link - nodes[node].links[i].targetNodeType = INVALID; // Type - nodes[node].links[i].cost = INVALID; // Cost - } - - if (node == numnodes) - numnodes++; // Update the node counter - - return node; -} - -/////////////////////////////////////////////////////////////////////// -// Nodes added by the new navigation system -/////////////////////////////////////////////////////////////////////// -qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type) -{ - // Absolute max allowed - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); - return false; - } - - // ------------- - // Alloc memory - // ------------- - node_t* prev = NULL; - - // Nodes - if (numnodes == 0) - nodes = (node_t*)malloc(sizeof(node_t)); - else - { - prev = nodes; // Keep a copy - nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); - } - if (nodes == NULL) - { - Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); - if (prev) - { - free(prev); // Free using the copy, because nodes is null - nodes = NULL; - prev = NULL; - } - return false; - } - - // Unsorted duplicate copy nodes - if (numnodes == 0) - unsorted_nodes = (node_t*)malloc(sizeof(node_t)); - else - { - prev = unsorted_nodes; // Keep a copy - unsorted_nodes = (node_t*)realloc(unsorted_nodes, sizeof(node_t) * (numnodes + 1)); - } - if (unsorted_nodes == NULL) - { - Com_Printf("%s failed to malloc unsorted_nodes. Out of memory!\n", __func__); - if (prev) - { - free(prev); // Free using the copy, because unsorted_nodes is null - unsorted_nodes = NULL; - prev = NULL; - } - return false; - } - - // ------------------------------------------------------------------------------------- - // nodes - // ------------------------------------------------------------------------------------- - VectorCopy(origin, nodes[numnodes].origin); // Location - VectorCopy(normal, nodes[numnodes].normal); // Surface Normal - nodes[numnodes].type = type; // Node type - nodes[numnodes].nodenum = numnodes; // Node number - nodes[numnodes].num_links = 0; // Node links - for (int i = 0; i < MAXLINKS; i++) // Init links - { - nodes[numnodes].links[i].targetNode = INVALID; // Link - nodes[numnodes].links[i].targetNodeType = INVALID; // Type - nodes[numnodes].links[i].cost = INVALID; // Cost - } - - - // ------------------------------------------------------------------------------------- - // unsorted_nodes - // ------------------------------------------------------------------------------------- - // Make a duplicate copy of nodes into unsorted_nodes. We'll use this later to optimise links - VectorCopy(origin, unsorted_nodes[numnodes].origin); // Location - VectorCopy(normal, unsorted_nodes[numnodes].normal); // Surface Normal - unsorted_nodes[numnodes].type = type; // Node type - unsorted_nodes[numnodes].nodenum = numnodes; // Node number - unsorted_nodes[numnodes].num_links = 0; // Node links - for (int i = 0; i < MAXLINKS; i++) // Init links - { - unsorted_nodes[numnodes].links[i].targetNode = INVALID; // Link - unsorted_nodes[numnodes].links[i].targetNodeType = INVALID; // Type - unsorted_nodes[numnodes].links[i].cost = INVALID; // Cost - } - - //if ((numnodes % 1024) == 0) - //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, nodes[numnodes].nodenum, nodes[numnodes].origin[0], nodes[numnodes].origin[1], nodes[numnodes].origin[2]); - - numnodes++; // Update the node counter - - return true; -} - -/* -/////////////////////////////////////////////////////////////////////// -// Makes a duplicate copy into the unsorted_nodes[] array -/////////////////////////////////////////////////////////////////////// -void ACEND_DuplicateNodes(int node_num) -{ - node_t* unsorted_node = (unsorted_nodes + node_num); // Get our node by pointer arithmatic - - VectorCopy(nodes[node_num].origin, unsorted_node->origin); // Location - VectorCopy(nodes[node_num].normal, unsorted_node->normal); // Surface Normal - unsorted_node->type = nodes[node_num].type; // Node type - unsorted_node->nodenum = nodes[node_num].nodenum; // Node number - - for (int i = 0; i < MAXLINKS; i++) // Init links - { - unsorted_node->links[i].targetNode = INVALID; // Invalidate all links - unsorted_node->links[i].cost = INVALID; // Invalidate all costs - } -} -void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type) -{ - // Sanity check - if (numnodes + 1 > MAX_NODES) - { - debug_printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_NODES); - return; - } - - //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, numnodes, origin[0], origin[1], origin[2]); - - VectorCopy(origin, nodes[numnodes].origin); // Location - VectorCopy(normal, nodes[numnodes].normal); // Surface Normal - nodes[numnodes].type = type; // Node type - nodes[numnodes].nodenum = numnodes; // Node number - - //node_ents[numnodes] = self; // Add door as an entity reference //rekkie -- DEV_1 - - // Init links - for (int i = 0; i < MAXLINKS; i++) - { - nodes[numnodes].links[i].targetNode = INVALID; // Link - nodes[numnodes].links[i].targetNodeType = INVALID; // Type - memset(nodes[numnodes].links[i].targetVelocity, 0, sizeof(vec3_t)); // Velocity - nodes[numnodes].links[i].cost = INVALID; // Cost - } - - // Make a duplicate copy of nodes[] to *unsorted_nodes - // We'll use this later to optimise links - ACEND_DuplicateNodes(numnodes); - - // Show ever 10nth node - //if (numnodes % 10 == 0) - // ACEND_ShowNode(numnodes); - - numnodes++; // Update the node counter -} -*/ - -/////////////////////////////////////////////////////////////////////// -// Remove node by number, -// Shifts all the nodes down by one to replace the deleted node -/////////////////////////////////////////////////////////////////////// -void ACEND_RemoveNode(edict_t* self, int nodenum) -{ - int i, j, n, l; - - // Sanity - if (nodenum <= 0 || nodenum + 1 > MAX_PNODES) - return; - - // Remove links to/from this node - for (i = 0; i < MAXLINKS; i++) - { - // Remove link from other node to our node - n = nodes[nodenum].links[i].targetNode; // Links from this node to other nodes - if (n != INVALID) - { - for (j = 0; j < MAXLINKS; j++) - { - if (nodes[n].links[j].targetNode == nodenum) // If the other node has a link back to our node - { - nodes[n].links[j].targetNode = INVALID; // Remove its link to us - } - } - } - - // Remove our link to other node - nodes[nodenum].links[i].targetNode = INVALID; - } - - // Update node ent - edict_t* ent; - for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) - { - // Free removed node ent - if (ent->node_num == nodenum) - { - G_FreeEdict(ent); - } - - // Update node name and number - if (ent->node_num > nodenum) - { - ent->node_num--; - - if (nodes[nodenum].type == NODE_MOVE) - Q_strncpyz(ent->node_name, va("node move [%d]", ent->node_num), sizeof(ent->node_name)); - else if (nodes[nodenum].type == NODE_WATER) - Q_strncpyz(ent->node_name, va("node water [%d]", ent->node_num), sizeof(ent->node_name)); - else if (nodes[nodenum].type == NODE_JUMPPAD) - Q_strncpyz(ent->node_name, va("node jumppad [%d]", ent->node_num), sizeof(ent->node_name)); - else if (nodes[nodenum].type == NODE_SPAWNPOINT) - Q_strncpyz(ent->node_name, va("node spawn point [%d]", ent->node_num), sizeof(ent->node_name)); - else if (nodes[nodenum].type == NODE_POI) - Q_strncpyz(ent->node_name, va("node point of interest [%d]", ent->node_num), sizeof(ent->node_name)); - else - Q_strncpyz(ent->node_name, va("node action [%d]", ent->node_num), sizeof(ent->node_name)); - } - } - - // Shift all nodes and links down one - for (n = 0; n < MAX_PNODES; n++) - { - if (n != nodenum) - { - // Move links down - for (l = 0; l < MAXLINKS; l++) - { - if (nodes[n].links[l].targetNode != INVALID) - { - if (nodes[n].links[l].targetNode > nodenum) - nodes[n].links[l].targetNode--; - } - } - } - } - - // Shift all nodes and links down one - for (n = 0; n + 1 < MAX_PNODES; n++) - { - // Copy links - if (n >= nodenum) - { - for (l = 0; l < MAXLINKS; l++) - { - nodes[n].links[l].targetNode = nodes[n + 1].links[l].targetNode; - } - } - - // Shift node down + copy - if (n >= nodenum) - { - nodes[n].type = nodes[n + 1].type; - VectorCopy(nodes[n + 1].origin, nodes[n].origin); - node_ents[n] = node_ents[n + 1]; - --nodes[n].nodenum; - } - } -} - -/////////////////////////////////////////////////////////////////////// -// Add/Update (one way) node link -/////////////////////////////////////////////////////////////////////// -qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost) -{ - int i; - vec3_t v; - float cost; - - // Sanity Checks - if (type == INVALID) - { - if (debug_mode) debug_printf("%s cannot add link from %d to %i, targetNodeType: %d \n", __func__, from, to, type); - return false; - } - if (from == INVALID || to == INVALID || from == to) - { - if (debug_mode) debug_printf("%s error: cannot add link from %d to %i\n", __func__, from, to); - return false; - } - if (VectorEmpty(nodes[from].origin)) - { - if (debug_mode) debug_printf("%s error: zero origin from %d\n", __func__, from); - return false; - } - if (VectorEmpty(nodes[to].origin)) - { - if (debug_mode) debug_printf("%s error: zero origin to %d\n", __func__, to); - return false; - } - - // Check max links reached - if (nodes[from].num_links + 1 >= MAXLINKS) - { - if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); - return false; - } - - // Check if already added - for (i = 0; i < nodes[from].num_links; i++) // Always cycle all links so we can check for existing links - { - if (nodes[from].links[i].targetNode == to) // Do we already have a link to this target? - { - if (debug_mode) Com_Printf("%s warning: target link %d already link to %i\n", __func__, nodes[from].links[i].targetNode, to); - return false; // Failed to add link - } - } - - // Add link if we can - for (i = 0; i < MAXLINKS; i++) // Always cycle all links so we can check for existing links - { - if (i + 1 == MAXLINKS) // Max links reached - { - if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); - return false; // Failed to add link - } - - if (nodes[from].links[i].targetNode == INVALID) // Found a free spot to add a new link - { - nodes[from].num_links++; // Update link counter - nodes[from].links[i].targetNode = to; // Add the link - nodes[from].links[i].targetNodeType = type; // The type of the target node - - if (do_cost) // Add the cost of this link? - { - VectorSubtract(nodes[from].origin, nodes[to].origin, v); // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. - cost = VectorLength(v); // The cost here is distance based - nodes[from].links[i].cost = cost; // Add cost - //if (debug_mode) - // Com_Printf("Link node[%d][%d] type[%d][%d] cost[%f]\n", from, to, nodes[from].type, nodes[to].type, cost); - } - //else if (debug_mode) debug_printf("Link %d -> %d\n", from, to); - - return true; // Successfully added link - } - } - return false; // Failed to add link -} - -/////////////////////////////////////////////////////////////////////// -// Add/Update (one way) node connection/path -/////////////////////////////////////////////////////////////////////// -qboolean ACEND_UpdateNodeReach(int from, int to) -{ - int i; - //trace_t trace; - - /* - // Sanity Checks - if (from == INVALID || to == INVALID || from == to) - return qfalse; - if (nodes[from].origin[0] == 0 && nodes[from].origin[1] == 0 && nodes[from].origin[2] == 0) - { - if (debug_mode) - debug_printf("Zero origin from %d\n", from); - return qfalse; - } - if (nodes[to].origin[0] == 0 && nodes[to].origin[1] == 0 && nodes[to].origin[2] == 0) - { - if (debug_mode) - debug_printf("Zero origin to %d\n", to); - return qfalse; - } - */ - - ///////////////////// - - // Add the link - path_table[from][to] = to; - - /* - // Checks if the link exists and then may create a new one - RiEvEr - for (i = 0; i < MAXLINKS; i++) - { - if (nodes[from].links[i].targetNode == to) - break; - if (nodes[from].links[i].targetNode == INVALID) - { - // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. - vec3_t v; - float thisCost; - - VectorSubtract(nodes[from].origin, nodes[to].origin, v); // subtract first - thisCost = VectorLength(v); - nodes[from].links[i].targetNode = to; - nodes[from].links[i].cost = thisCost; - if (debug_mode) - debug_printf("Link %d -> %d\n", from, to); - break; - } - } - */ - - // Now for the self-referencing part, linear time for each link added - for (i = 0; i < numnodes; i++) - { - if (path_table[i][from] != INVALID) - { - if (i == to) - path_table[i][to] = INVALID; // make sure we terminate - else - path_table[i][to] = path_table[i][from]; - } - } - - return true; -} -//rekkie -- DEV_1 -- e - -//rekkie -- DEV_1 -- s -//////////////////////////////////////////////////////////////////////////////////////////// -// Remove all node links from a particular node, including links from and to the node -// This is used when deleting a node, so the links get destroyed properly (bi-directional) -//////////////////////////////////////////////////////////////////////////////////////////// -void BOTLIB_RemoveAllNodeLinksFrom(int from) -{ - int i, j, to; - - // Other nodes - int link_other_counter = 0; - int link_other_to[MAXLINKS]; - int link_other_from[MAXLINKS]; - - // Our node - int link_self_counter = 0; - int link_self_to[MAXLINKS]; - int link_self_from[MAXLINKS]; - - for (i = 0; i < numnodes; i++) - { - for (j = 0; j < MAXLINKS; j++) - { - if (nodes[i].links[j].targetNode != INVALID && nodes[i].links[j].targetNode == from) - { - //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, nodes[i].nodenum, from); - link_other_to[link_other_counter] = nodes[i].nodenum; - link_other_from[link_other_counter] = from; - link_other_counter++; - } - } - } - - // Gather all the link information before doing anything with it - for (i = 0; i < MAXLINKS; i++) - { - if (nodes[from].links[i].targetNode != INVALID) - { - to = nodes[from].links[i].targetNode; - - /* - // Check other node we're connected to - for (j = 0; j < MAXLINKS; j++) - { - if (nodes[to].links[j].targetNode == from) - { - //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, to, from); - link_other_to[link_other_counter] = to; - link_other_from[link_other_counter] = from; - link_other_counter++; - } - } - */ - - //gi.dprintf("%s: OurNode %d -> %d\n", __func__, from, to); - link_self_from[link_self_counter] = from; - link_self_to[link_self_counter] = to; - link_self_counter++; - } - } - - // Now use the link information we gathered to remove connections between nodes - // - // Remove other node links to ours - for (i = 0; i < link_other_counter; i++) - ACEND_RemoveNodeEdge(NULL, link_other_to[i], link_other_from[i]); - // Remove our node link to other nodes - for (i = 0; i < link_self_counter; i++) - ACEND_RemoveNodeEdge(NULL, link_self_from[i], link_self_to[i]); -} - -#if 0 -// TODO: This function isn't ready for use yet -// Removes a node link from all nodes -void BOTLIB_RemoveNodeLink(edict_t* self, int from, int to) -{ - path_table[from][to] = INVALID; // set to invalid - - /* - // Fill the gap caused by the removal by left shifting from the position of the removed link - for (int i = 0; i < MAXLINKS; i++) - { - // Terminate the last link by making it INVALID - if (i == MAXLINKS - 1) - { - nodes[from].links[i].targetNode = INVALID; - nodes[from].links[i].cost = INVALID; - } - - // Move all the other info down to fill the gap (left shift). - // This *MUST* be done because pathing expects links to be contiguous, - // with the first INVALID link found becoming the terminator. - // IE: First link --> [LINK]-[LINK]-[LINK]-[LINK]-[INVALID] <-- terminator [INVALID]-[INVALID] <-- up to MAXLINKS - if (nodes[from].links[i].targetNode != INVALID) - { - nodes[from].links[i].targetNode = nodes[from].links[i + 1].targetNode; - nodes[from].links[i].cost = nodes[from].links[i + 1].cost; - } - } - - // Make sure this gets updated in our path array - for (int i = 0; i < numnodes; i++) - if (path_table[from][i] == to) - path_table[from][i] = INVALID; - */ -} -#endif - -// Checks a node to see if its possible to path to -// Parameters: -// goal_node: the goal node to reach -// path_randomization: Should path be randomized (if true, this can lead to no path being taken) -// check_and_goto: If TRUE check for a valid path AND go there. If FALSE check for valid path but don't go there. -// build_new_path: If we're building a new path or adding to an existing path -qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path) -{ - // Always update current node - self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - - // Invalid node - if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false) - { - //Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); - return false; - } - - if (nodes[goal_node].type == NODE_LADDER) // Never visit ladder nodes - return false; - - // Already at node - if (goal_node == self->bot.current_node) - { - //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); - return false; - } - - // NEW PATHING: limit search to same area - if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) - return false; - - // OLD: searches all nodes - //else if (AntStartSearch(self, self->bot.current_node, goal_node, path_randomization) == false) - else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false) - return false; - - - - // ================================================================ - // BOTLIB_SetGoal () - // ================================================================ - if (self->bot.goal_node == INVALID) - self->bot.goal_node = goal_node; - - int nodelist[MAX_NODELIST]; - int nodes_touched; - nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); - for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched - { - if (nodelist[i] != INVALID) - { - self->bot.current_node = nodelist[i]; - break; - } - } - - if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) - { - //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; // BOT_MOVE_STATE_WANDER - return false; - } - - self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first - self->node_timeout = 0; - - self->bot.state = BOT_MOVE_STATE_MOVE; - - //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); - // ================================================================ - - - return true; -} - -/////////////////////////////////////////////////////////////////////// -// Set up the goal -/////////////////////////////////////////////////////////////////////// - -void BOTLIB_SetGoal(edict_t* self, int goal_node) -{ - // This code is now merged inside BOTLIB_CanVisitNode() - - /* - self->bot.goal_node = goal_node; - - int nodelist[MAX_NODELIST]; - int nodes_touched; - nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); - for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched - { - if (nodelist[i] != INVALID) - { - self->bot.current_node = nodelist[i]; - break; - } - } - - if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) - { - //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); - - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_NAV; - //self->wander_timeout = level.framenum + 0.1 * HZ; - //ACEAI_PickLongRangeGoal(self); - //Com_Printf("%s %s invalid goal[%i]\n", __func__, self->client->pers.netname, self->bot.goal_node); - return; - } - - self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first - self->node_timeout = 0; - - //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); //rekkie - - return; - */ -} - - - -// Run a trace from [start -> end] looking for nodes (hitbox) -// If [min/max] is empty -> Line trace -// If [min/max] not empty -> Box trace -// If a node was hit, return node number -// If nothing was hit, return INVALID -int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs) -{ - vec3_t pos, min, max, absmin, absmax; - vec3_t dir = { 0 }; - VectorCopy(mins, min); - VectorCopy(maxs, max); - if (VectorEmpty(min) && VectorEmpty(max)) - { - min[0] = -0.25; min[1] = -0.25; min[2] = -0.25; - max[0] = 0.25; max[1] = 0.25; max[2] = 0.25; - } - VectorSubtract(end, start, dir); - float distance = VectorLength(dir); - VectorCopy(start, pos); - float tested_distance = 1; - float normalized_dist = tested_distance / distance; // Normalized distance - while (tested_distance + 1 < distance) - { - tested_distance += 1; // Move distance forward - VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position - - //rekkie -- debug drawing -- s -#if DEBUG_DRAWING - if (0) // Debug draw - { - void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; - DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), mins, maxs, 5000); // Draw node box - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); - } -#endif - //rekkie -- debug drawing -- e - - // Update absolute box min/max in the world - VectorAdd(pos, min, absmin); - VectorAdd(pos, max, absmax); - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].inuse == false) continue; // Ignore nodes not in use - - if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? - return i; - } - } - - return INVALID; // No intersection -} -/* -// Generate vis data for each node - test node-to-node visibility -void BOTLIB_GenerateNodeVis(edict_t* self) -{ - for (int i = 0; i < numnodes; i++) - nodes[i].num_vis_nodes = 0; // zero - - for (int i = 0; i < numnodes; i++) - { - for (int j = 0; j < numnodes; j++) - { - if (i == j) continue; // skip self - - // Check if the node was already added - for (int k = 0; k < nodes[i].num_vis_nodes; k++) - { - if (nodes[i].vis_nodes[k] == nodes[j].nodenum) - continue; - } - - if (nodes[i].num_vis_nodes + 1 >= 1024) - continue; - if (nodes[j].num_vis_nodes + 1 >= 1024) - continue; - - trace_t tr = gi.trace(nodes[i].origin, NULL, NULL, nodes[j].origin, NULL, MASK_SHOT); - - if (tr.fraction == 1.0) // Nodes can see each other - { - // Store the remote node in the local node - nodes[i].vis_nodes[nodes[i].num_vis_nodes] = nodes[j].nodenum; // Store the node we can see - nodes[i].num_vis_nodes++; // Increase the vis nodes in cache - - // Store the local node in the remote node - nodes[j].vis_nodes[nodes[j].num_vis_nodes] = nodes[i].nodenum; // Store the node we can see - nodes[j].num_vis_nodes++; // Increase the vis nodes in cache - } - } - } -} -*/ - -// Trace from node to node, checking to see if we hit other nodes along the path -// Return the node number that was hit, or INVALID if none was hit -int BOTLIB_TraceBoxNode(int from, int to) -{ - if (from == INVALID || to == INVALID) return INVALID; - - vec3_t pos, absmin, absmax; - vec3_t dir = { 0 }; - VectorSubtract(nodes[to].origin, nodes[from].origin, dir); - float distance = VectorLength(dir); - VectorCopy(nodes[from].origin, pos); - - float tested_distance = 1; - float normalized_dist = tested_distance / distance; // Normalized distance - while (tested_distance + 1 < distance) - { - tested_distance += 1; // Move distance forward - VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position - - //rekkie -- debug drawing -- s -#if DEBUG_DRAWING - if (0) // Debug draw - { - void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; - DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), nodes[from].mins, nodes[from].maxs, 5000); // Draw node box - players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used - //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); - } -#endif - //rekkie -- debug drawing -- e - - // Update absolute box min/max in the world - VectorAdd(pos, nodes[from].mins, absmin); - VectorAdd(pos, nodes[from].maxs, absmax); - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].inuse == false) continue; // Ignore nodes not in use - if (nodes[i].nodenum == from || nodes[i].nodenum == to) continue; // Ignore source and destination nodes - - if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? - return i; - } - } - - return INVALID; // No intersection -} - -// Test to see if a node is touching any other nodes -// Returns INVALID not touching any nodes -// Returns nodelist if one or more nodes are touching -// numnodes is the total nodes touched -// maxnodes is the maximum nodes that can be stored in nodelist -// ignore_node is optional: ignores testing this node when testing. If ignore_node is INVALID, all nodes are tested -int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int *nodelist, const int maxnodes, int ignore_node) -{ - int nodelist_count = 0; - - // Adjust the mins/maxs box size based on size - vec3_t bmins = { mins[0] + -(size), mins[1] + -(size), mins[2] + -(size) }; - vec3_t bmaxs = { maxs[0] + size, maxs[1] + size, maxs[2] + size }; - - // Update absolute box min/max in the world - vec3_t absmin, absmax; - VectorAdd(origin, bmins, absmin); - VectorAdd(origin, bmaxs, absmax); - - for (int n = 0; n < numnodes; n++) - { - if (nodes[n].inuse == false) continue; // Ignore nodes not in use - if (nodes[n].nodenum == INVALID) continue; // Ignore invalid nodes - //if (VectorDistance(nodes[n].origin, origin) > 32) continue; // Skip checking distant nodes - if (ignore_node != INVALID && nodes[n].nodenum == ignore_node) continue; // Ignore ignored node - - // Line test - //trace_t tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID | MASK_OPAQUE); - //if (tr.fraction == 1.0) - { - if (BOTLIB_BoxIntersection(absmin, absmax, nodes[n].absmin, nodes[n].absmax)) // Do boxes intersect? - { - //if (normal[2] == 1 && origin[2] != nodes[n].origin[2]) // Check flat ground + height difference - // continue; // Ignore nodes that are on a different plane (such as stairs or boxes) - - if (nodelist_count + 1 < maxnodes) // Check we have room to store - nodelist[nodelist_count++] = n; //nodes[n].nodenum; // Add to list - else - break; - } - } - } - - return nodelist_count; // Return number of nodes touched -} - -// Test location to see if taken by an existing node based on distance, return the node (if any) we're too close to -// Returns 'INVALID' if no node(s) are found -// Returns 'nodenum' if node(s) are found -int BOTLIB_TestForNodeDist(vec_t *origin, float distance, vec3_t mins, vec3_t maxs) -{ - trace_t tr; - //vec3_t v; - //float dist; - - // Check if we fit - tr = gi.trace(origin, tv(-2, -2, 0), tv(2, 2, 0), origin, NULL, MASK_PLAYERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - return -2; // Inside solid - } - - // Test to see if node hits the ceiling - //tr = gi.trace(tv(origin[0], origin[1], origin[2]), tv(-16, -16, -32), tv(16, 16, 25), tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); - tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); - if (tr.fraction < 1.0 || tr.startsolid) - { - return -2; // Hit our head on something immediately above us - } - - - vec3_t bmins = { mins[0] + -(distance), mins[1] + -(distance), mins[2] + -(distance) }; - vec3_t bmaxs = { maxs[0] + distance, maxs[1] + distance, maxs[2] + distance }; - - //for (int n = 0; n < NODE_MAX; n++) - for (int n = 0; n < numnodes; n++) - { - if (nodes[n].inuse == false) continue; // Ignore nodes not in use - - if (nodes[n].nodenum != INVALID) - { - // Line test - tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) // Only distance test origins that can see each other - { - // New method of box intersection search - if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[n].mins, nodes[n].maxs)) - return nodes[n].nodenum; // Return the first node found too close - - /* - // Old method of radius search - VectorSubtract(nodes[n].origin, origin, v); // other node -> our node - dist = VectorLength(v); - if (dist < distance) - { - //if (dist == 0) // If we find a node at the exact same coordinates - // Com_Printf("%s found node %d at the same origin[%f %f %f]\n", __func__, n, origin[0], origin[1], origin[2]); - - //gi.dprintf("-=-=-=-=-=------------[%i] dist[%f]\n", n, dist); - return nodes[n].nodenum; // Return the first node found too close - } - */ - } - } - } - - return INVALID; // Found no nodes within range, safe to place a node here :) -} - -// Test all nodes to see they're too close to our node -// Calculation is based on matching the exact X and Y coords, then comparing the height difference -// Returns the first offending node that is too close, otherwise INVALID if no node is found -int BOTLIB_UTIL_NearbyNodeAtHeightDist(vec_t* origin, float distance) -{ - float dist; - for (int n = 0; n < numnodes; n++) - { - if (nodes[n].inuse == false) continue; // Ignore nodes not in use - - if (nodes[n].nodenum != INVALID) - { - if (origin[0] == nodes[n].origin[0]) // Match X axis - { - if (origin[1] == nodes[n].origin[1]) // Match Y axis - { - dist = abs(nodes[n].origin[2] - origin[2]); // Get Z height distance - - if (dist <= distance) // Z axis within distance - return n; // Offending node that is too close - } - } - } - } - - return INVALID; // Found no nodes that are too close -} - -float HalfPlaneSign(vec2_t p1, vec2_t p2, vec2_t p3) -{ - return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); -} - -// Check if point is inside triangle. -// PT = (x,y,z) point location. V1,V2,V3 = 3 points of a triangle (x,y,z) -qboolean PointInTriangle(vec3_t pt, vec3_t v1, vec3_t v2, vec3_t v3) -{ - float d1, d2, d3; - qboolean has_neg, has_pos; - - d1 = HalfPlaneSign(pt, v1, v2); - d2 = HalfPlaneSign(pt, v2, v3); - d3 = HalfPlaneSign(pt, v3, v1); - - has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); - has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); - - return !(has_neg && has_pos); -} - -// Returns true if the point is inside a convex polygon -qboolean PointInPolygon(vec2_t pt, vec2_t* v, int num_verts) -{ - int i, j, c = 0; - for (i = 0, j = num_verts - 1; i < num_verts; j = i++) - { - if (((v[i][1] > pt[1]) != (v[j][1] > pt[1])) && - (pt[0] < (v[j][0] - v[i][0]) * (pt[1] - v[i][1]) / (v[j][1] - v[i][1]) + v[i][0])) - c = !c; - } - return c; -} - -// A function that makes a square polygon from a set of vertices -void BOTLIB_UTIL_MakePolySquare(int face, vec3_t out) -{ - int e; - vec3_t edge[128][2]; // Max edges per face - for (e = 0; e < nmesh.face[face].num_edges; e++) - { - // Copy all the edges into a 2D array - VectorCopy(nmesh.face[face].edge[e].v[0], edge[e][0]); // v1 - VectorCopy(nmesh.face[face].edge[e].v[1], edge[e][1]); // v2 - } - - /* - face 101 edges 6 - -------------- - v1[256.000000 -176.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] - v1[256.000000 -112.000000 -800.000000] v2[256.000000 -176.000000 -800.000000] - v1[256.000000 -33.000000 -800.000000] v2[256.000000 -112.000000 -800.000000] - v1[256.000000 0.000000 -800.000000] v2[256.000000 -33.000000 -800.000000] - v1[480.000000 0.000000 -800.000000] v2[256.000000 0.000000 -800.000000] - v1[480.000000 0.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] - -------------- - */ - - // Find each edge that goes in the same direction - // Try to find the total length of that direction - vec3_t v1, v2; - //float highest = 0, lowest = 0; - float v1_x; // V1 X - float v1_y; // V1 Y - float v2_x; // V2 X - float v2_y; // V2 Y - - float highest_x = 0; - float lowest_x = 0; - float highest_y = 0; - float lowest_y = 0; - - for (e = 0; e < nmesh.face[face].num_edges; e++) - { - VectorCopy(edge[e][0], v1); // First vertex - VectorCopy(edge[e][1], v2); // Second vertex - - for (int e2 = 0; e2 < nmesh.face[face].num_edges; e2++) - { - if (e == e2) continue; // Skip self - - // Check the X axis - v1_x = v1[0]; v2_x = v2[0]; // X - v1_y = v1[1]; v2_y = v2[1]; // Y - - if (v1_x == v2_x) // Check if v1 and v2 are on the same X axis - { - // Find highest on the Y axis - if (v1_y > highest_y) - highest_y = v1_y; - else if (v2_y > highest_y) - highest_y = v2_y; - - // Find lowest on the Y axis - if (v1_y < lowest_y) - lowest_y = v1_y; - else if (v2_y < lowest_y) - lowest_y = v2_y; - } - if (v1_y == v2_y) // Check if v1 and v2 are on the same Y axis - { - // Find highest on the X axis - if (v1_x > highest_x) - highest_x = v1_x; - else if (v2_x > highest_x) - highest_x = v2_x; - - // Find lowest on the X axis - if (v1_x < lowest_x) - lowest_x = v1_x; - else if (v2_x < lowest_x) - lowest_x = v2_x; - } - } - } -} - - -// A function that finds the center of a 3D convex polygon -void BOTLIB_UTIL_PolygonCenter(vec3_t* v, int num_verts, vec3_t center) -{ - vec3_t sum = { 0,0,0 }; - for (int i = 0; i < num_verts; i++) - { - VectorAdd(sum, v[i], sum); - } - VectorScale(sum, 1.0 / num_verts, center); -} - -// Return the BSP face and triangle based on origin -void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle) -{ - vec3_t org = { 0,0,0 }; - vec3_t dist = { 0,0,0 }; - int n; - float distance; - - for (int f = 0; f < nmesh.total_faces; f++) // Current face - { - if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls - - for (int t = 0; t < nmesh.face[f].num_tris; t++) // Current triangle - { - if (PointInTriangle(origin, nmesh.face[f].tris[t].v[0], nmesh.face[f].tris[t].v[1], nmesh.face[f].tris[t].v[2])) - { - // Ignore tris with invalid nodes - n = nmesh.face[f].tris[t].node; - if (n <= 0) - continue; - - // Ensure the PointInTriangle is roughly within the same z height as origin - VectorCopy(origin, org); // We only want the height - org[0] = 0; // Remove X component - org[1] = 0; // Remove Y component - VectorSubtract(nodes[n].origin, origin, dist); - distance = VectorLength(dist); - if (distance > 64) // Height - continue; - - //Com_Printf("%s -> f[%i] t[%i] node %i at %f %f %f\n", __func__, f, t, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); - - *found = true; - *floor = f; - *triangle = t; - return; - } - } - } -} - -// Return the BSP face and number of edges that is closest to origin -void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int *edges) -{ - vec3_t org = { 0,0,0 }; - vec3_t dist = { 0,0,0 }; - int e; // edge - float distance; - float closest = 999999; - - for (int f = 0; f < nmesh.total_faces; f++) // Current face - { - if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls - - vec3_t edge[128]; // Max edges per face - for (e = 0; e < nmesh.face[f].num_edges; e++) - { - // Copy all the edges into a 2D array - VectorCopy(nmesh.face[f].edge[e].v[0], edge[e]); - } - - BOTLIB_UTIL_PolygonCenter(edge, nmesh.face[f].num_edges, org); // Get the center of the polygon - - // Ensure the polygon is roughly within the same z height as origin - //VectorCopy(origin, org); // We only want the height - //org[0] = 0; // Remove X component - //org[1] = 0; // Remove Y component - VectorSubtract(org, origin, dist); - distance = VectorLength(dist); - //if (distance > 64) // Height - // continue; - - if (distance < closest) - { - closest = distance; - *floor = f; - *edges = e; - } - - } -} - - -// Returns the distance from origin to target -float ACEND_Distance(vec3_t origin, vec3_t target) -{ - vec3_t dist; - VectorSubtract(target, origin, dist); - return VectorLength(dist); -} - -// Using three points of a triangle with origin being the origin calculate the angle between target_a and target_b -float ACEND_AngleBetween(vec3_t origin, vec3_t target_a, vec3_t target_b) -{ - vec3_t a, b; - VectorSubtract(target_a, origin, a); // Get length - VectorSubtract(target_b, origin, b); // Get length - return RAD2DEG(acos(DotProduct(a, b) / (VectorLength(a) * VectorLength(b)))); -} - - -void LaunchP(edict_t* ent, vec3_t origin, vec3_t target) -{ - vec3_t dist; - vec3_t velocity; - float gravity = ent->gravity * sv_gravity->value; - VectorSubtract(target, ent->s.origin, dist); - float distance = VectorLength(dist); - - // Calculate the jump height to get to the target - float jump_height = sqrt(2 * (gravity * distance)); - - // Calculate the time it will take to get to the target - float time = distance / (ent->velocity[2] + jump_height / 2.0); - - // Calculate the velocity at the end of the jump - VectorScale(dist, 1.0 / time, velocity); - - velocity[2] = jump_height / 2.0; - - // If the target is above the player, increase the velocity to get to the target - float z_height = 0; - if (target[2] > ent->s.origin[2]) - { - z_height = (target[2] - ent->s.origin[2]); - z_height += sqrtf(distance) + NODE_Z_HEIGHT; - velocity[2] += z_height; - } - else if (abs(target[2] - ent->s.origin[2]) <= 4) - { - // Do nothing here - } - else - { - z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; - velocity[2] -= z_height; - } - - // Set the velocity of the player to the calculated velocity - VectorCopy(velocity, ent->velocity); - VectorCopy(ent->velocity, ent->client->oldvelocity); - VectorCopy(ent->velocity, ent->avelocity); - - //float speed = VectorLength(velocity); - //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); -} - -qboolean LaunchPlayer(edict_t* ent, vec3_t target) -{ - - LaunchP(ent, ent->s.origin, target); - return false; - - // Calculate the distance to the target - vec3_t dist; - vec3_t velocity; - float gravity = ent->gravity * sv_gravity->value; - //float gravity = ent->gravity * sv_gravity->value * FRAMETIME; - - - vec3_t targ; - VectorCopy(target, targ); - //targ[2] += 64; - VectorSubtract(targ, ent->s.origin, dist); - //dist[2] = 0; - //dist[2] += 64; - float distance = VectorLength(dist); - VectorCopy(ent->velocity, velocity); - //velocity[2] += 64; - //VectorNormalize(velocity); - //VectorNormalize(dist); - //if (DotProduct(velocity, dist) > 0.8) - //Com_Printf("%s %f\n", __func__, DotProduct(velocity, dist)); - //distance *= 2.0; // Increasing this increases the jump height - - - //VectorSubtract(target, ent->s.origin, dist); - //float distance = VectorLength(dist); - //distance *= 1.0; // Increasing this increases the jump height - - // Calculate the jump height to get to the target - float jump_height = sqrt(2 * (gravity * distance)); - // Calculate the time it will take to get to the target - float time = distance / (ent->velocity[2] + jump_height / 2.0); - // Calculate the velocity at the end of the jump - VectorScale(dist, 1.0 / time, velocity); - velocity[2] = jump_height / 2.0; - - // If the target is above the player, increase the velocity to get to the target - float z_height; - if (target[2] > ent->s.origin[2]) - { - z_height = (target[2] - ent->s.origin[2]) + NODE_Z_HEIGHT; - velocity[2] += z_height; - } - else - { - z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; - velocity[2] -= z_height; - } - - - /* - // Get the z height distance between origin and target - vec3_t zdiff, oz, tz; - // Copy origin to oz - VectorCopy(ent->s.origin, oz); - // Copy target to tz - VectorCopy(target, tz); - // Set X and Y on oz and tz to zero - oz[0] = 0; oz[1] = 0; - tz[0] = 0; tz[1] = 0; - // Get the z height distance between oz and tz - VectorSubtract(oz, tz, zdiff); - // Add the difference to jump velocity - float z_height = 0; - if (ent->s.origin[2] < target[2]) - { - //z_height = VectorLength(zdiff); - velocity[2] += z_height + 56; - } - else - { - velocity[2] -= VectorLength(zdiff); - } - */ - - // Calculate speed from velocity - float speed = VectorLength(velocity); - gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); - // Limit max speed - //if (speed < 750 && z_height < 150 && jump_height <= 965) - { - // Set the velocity of the player - VectorCopy(velocity, ent->velocity); - // Don't take falling damage immediately from this - if (ent->client) - VectorCopy(ent->velocity, ent->client->oldvelocity); - - return true; - } - return false; -} - -int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal) -{ - trace_t tr; - - //static const float max_dist = 128; // Absolute max distance -- 470 - const float max_dist = 470; // Absolute max distance -- 470 - //float curr_dist = 224; - //qboolean is_los = true; // Line of Sight - qboolean is_gap = false; // If there's a gap between origin and target - - // Sanity checks - if (from == to || from == INVALID || to == INVALID) - return INVALID; - if (origin[0] == 0 && origin[1] == 0 && origin[2] == 0) - return INVALID; - if (target[0] == 0 && target[1] == 0 && target[2] == 0) - return INVALID; - - - //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) - // Com_Printf("%s NODE_WATER\n", __func__); - - /* - // Ignore some water nodes (except ladders) - // Don't go into the water - if (nodes[from].type != NODE_WATER && nodes[to].type == NODE_WATER) - return INVALID; - // Check node is in water - if (nodes[from].type == NODE_WATER && (gi.pointcontents(nodes[from].origin) & CONTENTS_WATER) == 1) - return INVALID; - // Check node is in water - if (nodes[to].type == NODE_WATER && (gi.pointcontents(nodes[to].origin) & CONTENTS_WATER) == 1) - return INVALID; - //if (nodes[from].type == NODE_WATER) - // return INVALID; - //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) - // return INVALID; - */ - - //vec3_t vec_from, vec_to; - - // Calculate the distance to the target - vec3_t dist; - vec3_t velocity; - - VectorSubtract(target, origin, dist); - float xyz_distance = VectorLength(dist); // XYZ distance - - //VectorSubtract(target, origin, dist); - dist[2] = 0; - float xy_distance = VectorLength(dist); // XY Distance - - // Max distance - if (xyz_distance > max_dist) // Ignore very far nodes - return INVALID; - - // Min distance - //if (xyz_distance < NODE_Z_HEIGHT) // Ignore very close nodes - // return INVALID; - - /* - // Player when standing - vec3_t player_standing_mins = { -16, -16, -24 }; // With legs - vec3_t player_partial_standing_mins = { -16, -16, -0 }; // Without legs - vec3_t player_standing_maxs = { 16, 16, 32 }; - // Player when crouching/ducking - vec3_t player_crouching_mins = { -16, -16, -24 }; - vec3_t player_crouching_maxs = { 16, 16, 4 }; - // Player when standing smaller mins - vec3_t player_standing_smaller_mins = { -14, -14, -24 }; - vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; - // Player width only, no height component - vec3_t player_no_height_mins = { -16, -16, -0 }; - vec3_t player_no_height_maxs = { 16, 16, 0 }; - // Small box test - vec3_t small_mins = { -8, -8, -2 }; - vec3_t small_maxs = { 8, 8, 2 }; - // Tiny box test - vec3_t tiny_mins = { -1, -1, -1 }; - vec3_t tiny_maxs = { 1, 1, 1 }; - */ - - - // Do a line trace - tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); - // See if we hit any entities we wish to avoid - if (tr.ent && tr.ent != g_edicts) - { - //Com_Printf("%s %s found - \n", __func__, tr.ent->classname); - - // Avoid these - if (strcmp(tr.ent->classname, "trigger_hurt") == 0) - { - //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); - return INVALID; - } - - // Avoid rotating geometry - for now - if (strcmp(tr.ent->classname, "func_rotating") == 0) - { - //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); - return INVALID; - } - } - // See if we hit any walls or inside a solid - if (tr.fraction < 1.0 || tr.startsolid) - { - return INVALID; - } - - - - - - - // Test for gaps/holes between the current and next node - // Each tested is conducted x units apart - { - vec3_t dir, end; - float tested_distance = 0; - - // Get direction - VectorSubtract(target, origin, dir); // (end - start) = dir - - // Get XYZ distance - xyz_distance = VectorLength(dir); // VectorLength of dir (cannot be normalized) - - // Normalize direction vector (must be done after VectorLength otherwise we don't get the correct distance) - VectorNormalize(dir); - - // Safety check - if (xyz_distance >= 4096) // Max distance - { - Com_Printf("%s WARNING: gap test exceeded > 4096 units from node %d to %d\n", __func__, from, to); - } - else - { - while (tested_distance + NODE_SIZE < xyz_distance) - { - tested_distance += NODE_SIZE; // Move next test forward - VectorMA(origin, tested_distance, dir, end); // origin -> distance -> direction = end point - - // Check position to see if we hit any solids along the way - tr = gi.trace(end, tv(-12,-12,-8), tv(12, 12, 8), end, g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); - if (tr.startsolid) is_gap = true; - - end[2] -= NODE_Z_HEIGHT; // Move end point to ground - tr = gi.trace(end, NULL, NULL, tv(end[0], end[1], -4096), g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); // Trace down until we hit the ground - if (tr.startsolid) continue; - //is_gap = true; - - //if (tr.plane.normal[2] < 0.99) // If the ground we hit below is not flat - { - //is_gap = true; - //break; - } - - // - if (end[2] - tr.endpos[2] > (STEPSIZE + 1)) - { - //if (from == 1740 && to == 1403) - // Com_Printf("%s dist %f z-diff %f -- FOUND GAP\n", __func__, tested_distance, end[2] - tr.endpos[2]); - is_gap = true; - break; - } - } - } - } - - - - - // Use smaller node min/max for ladders - //if (nodes[to].type == NODE_LADDER_DOWN || nodes[to].type == NODE_LADDER_UP) - // //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component - // tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); // Line trace - //else - // tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height - if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) - { - tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height - //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component - // Try to trace from node->node LoS - // Failing that a second trace is performed with both nodes moved up slightly - if (tr.fraction < 1) - { - // Ignore ladders - //if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) - { - // See if target is below and if we can drop down - if (origin[2] > target[2] && xy_distance < 96) - { - //tr = gi.trace(origin, tv(-16, -16, 48), tv(16, 16, 56), target, g_edicts, MASK_DEADSOLID); - //tr = gi.trace(origin, tv(-16, -16, 25), tv(16, 16, 26), target, g_edicts, MASK_DEADSOLID); - tr = gi.trace(origin, tv(-8, -8, -0), tv(8, 8, 0), target, g_edicts, MASK_DEADSOLID); // LoS + width to the node below - if (tr.fraction == 1) - { - float lower = (origin[2] - target[2]); // Distance below - - if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) - { - return NODE_STAND_DROP; // Can drop while standing - } - - // Crouch drop down - if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) - { - return NODE_CROUCH_DROP; // Can drop but need to crouch - } - - // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) - if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) - { - return NODE_UNSAFE_DROP; // Can drop while crouching but will take some leg damage - } - } - } - - // Check for crouching - if (nodes[to].type == NODE_MOVE && is_gap == false && xy_distance < 256) - { - tr = gi.trace(tv(origin[0], origin[1], origin[2] - 16), tv(-16, -16, -32), tv(16, 16, 0), tv(target[0], target[1], target[2] - 16), g_edicts, MASK_DEADSOLID); // Test crouch height - if (tr.fraction == 1) - { - return NODE_CROUCH; - } - } - } - - //return INVALID; - } - } - - - // Invalidate bad ladder links - // --------------------------- - // Ladder from DOWN -> UP must be on same X/Y position - if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP && (origin[0] != target[0] || origin[1] != target[1])) - return INVALID; - // Ladder from UP -> DOWN must be on same X/Y position - if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN && (origin[0] != target[0] || origin[1] != target[1])) - return INVALID; - // Ladder from DOWN -> DOWN - if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_DOWN) - return INVALID; - // Ladder from UP -> UP - if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_UP) - return INVALID; - - // Top ladder node -> non-ladder node, test if within z height range +/- - if (nodes[from].type == NODE_LADDER_UP && nodes[to].type != NODE_LADDER_DOWN) - { - //if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 128 || xyz_distance < 48) - if (abs(origin[2] < target[2]) > 8 || xyz_distance > 128 || xyz_distance < 48) - return INVALID; - else - return NODE_MOVE; - //else - // return NODE_JUMPPAD; // Jump to next node - } - // Bottom ladder node -> non-ladder node, test if within z height range +/- - if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_UP) - { - if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 96) - return INVALID; - } - - // Non-ladder node -> top ladder node - test if within z height range +/- - if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP) - { - if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) - return INVALID; - } - // Non-ladder node -> bottom ladder node - test if within z height range +/- - if (nodes[from].type != NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN) - { - //if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) - // return INVALID; - if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT && xyz_distance < 96) - return NODE_JUMPPAD; - else - return INVALID; - } - // --------------------------- - - - /* - //if (nodes[from].type == NODE_LADDER || nodes[to].type == NODE_LADDER) - // return INVALID; -// (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) - if (nodes[from].type == NODE_LADDER && nodes[to].type == NODE_LADDER) - { - if (abs(origin[2] - target[2]) > 4) - { - if (abs(origin[0] - target[0]) <= 4 && abs(origin[1] - target[1]) <= 4) - return NODE_LADDER; - else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS) - return INVALID; - else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[0] == target[0] && abs(origin[1] - target[1]) <= 96) - return NODE_LADDER; - else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[1] == target[1] && abs(origin[0] - target[0]) <= 96) - return NODE_LADDER; - } - return INVALID; - } - else if (nodes[from].type == NODE_LADDER && nodes[to].type != NODE_LADDER && xyz_distance > 64) - return INVALID; - else if (nodes[from].type != NODE_LADDER && nodes[to].type == NODE_LADDER && xyz_distance > 64) - return INVALID; - */ - - - - - - - - //return INVALID; - - - - - - - - // Water nodes - if (nodes[from].type == NODE_WATER) - { - if (target[2] > origin[2] && xy_distance < 64 && nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder - return NODE_LADDER_UP; - //if (nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder - // return NODE_LADDER_DOWN; - //else if (nodes[to].type == NODE_MOVE) // Allow water -> move - // return NODE_MOVE; - else if (nodes[to].type == NODE_WATER) // Allow water -> water - return NODE_MOVE; - else - return INVALID; // Deny water to anything else - } - - - - // Steps - // ----- - // Deal with steps - if (nodes[from].type == NODE_STEP || nodes[to].type == NODE_STEP) - { - // Getting off step to a landing node - if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_MOVE && abs(origin[2] - target[2]) <= NODE_Z_HEIGHT_PLUS_STEPSIZE && xyz_distance <= 80) - { - trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); - if (tr_step.fraction == 1) - { - return NODE_MOVE; - } - } - // Getting on step - else if (nodes[from].type == NODE_MOVE && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance <= 80) - { - trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); - if (tr_step.fraction == 1) - { - return NODE_MOVE; - } - } - // Step to Step - else if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance < 40) - { - - //is_gap - trace_t tr_step = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_PLAYERSOLID); - if (tr_step.fraction == 1) - { - return NODE_STEP; - } - } - else - return INVALID; - } - - - - - // Calculate max jump height - // If on flat ground the max is 60 (NODE_MAX_JUMP_HEIGHT) - // If on slope up, the max is 150 depending on the angle of the slope, higher angle = higher max jump height - float z_height_max = NODE_MAX_JUMP_HEIGHT; - //float max_speed = 577; //(750 / 1.3 = 577) - if (normal[0] != 1.0 && normal[1] != 1.0) - { - float z_height_max_x = 1.0; - float z_height_max_y = 1.0; - if (normal[0] >= MAX_STEEPNESS) // Player is standing on a slope going up //else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) - { - z_height_max_x += (1 - normal[0]); - } - if (normal[1] >= MAX_STEEPNESS) - { - z_height_max_y += (1 - normal[1]); - } - z_height_max = (z_height_max_x + z_height_max_y) / 2; - if (z_height_max > 1.3) - z_height_max = 1.3; - else if (z_height_max < 1.0) - z_height_max = 1.0; - - z_height_max *= 116; // Max jump height is 150, and max z_height is 1.3 ... so 116 * 1.3 = 150 (150/1.3 = 116) - if (z_height_max > 150) - z_height_max = 150; - - //max_speed *= z_height_max; - } - - qboolean target_is_above = false, target_is_below = false, target_is_equal = false; - float higher = 0; - float lower = 0; - if (origin[2] > target[2]) // We're above the target - { - target_is_below = true; - lower = (origin[2] - target[2]); - } - else if (origin[2] < target[2]) // We're below the target - { - target_is_above = true; - higher = (target[2] - origin[2]); - } - else - target_is_equal = true; - - - - - - - - - - - - // Dropping down to target - // ----------------------- - // Can drop while standing - if (target_is_below && xy_distance < 228) //256 - { - if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) - { - return NODE_STAND_DROP; // Can drop while standing - } - - // Crouch drop down - if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) - { - return NODE_CROUCH_DROP; // Can drop but need to crouch - } - - // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) - if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) - { - return NODE_UNSAFE_DROP; // Can drop while crouching but will take some minor leg damage - } - } - - // Small jump to target - // ----------------- - //if (target_is_above && distance > STEPSIZE && distance < NODE_Z_HEIGHT) - // return NODE_JUMP; - - // Jump nodes - //if (xyz_distance <= 96 && is_gap && fabs(origin[2] - target[2]) <= 48) - //{ - //return NODE_JUMP; - //} - - // Large jump up to target - // -------------------- - // Jumppad nodes - //if (target_is_above && distance > STEPSIZE) - if (xyz_distance >= 16 && is_gap) - { - float gravity = sv_gravity->value; // This doesn't take into account the ent's own gravity (ent->gravity) - - //distance *= 1.0; // Increasing this increases the jump height - // Calculate the jump height to get to the target - float jump_height = sqrt(2 * gravity * xyz_distance); - float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point - - // Move the middle point up to the jump height (maximum parabola height) - //end_50[2] += NODE_Z_HEIGHT_PLUS_STEPSIZE + jump_height; - // Next test from start to mid, then mid to end - //tr_25 = gi.trace(origin, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); - //tr_75 = gi.trace(target, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); - // If the path from [start -> mid jump -> end] is clear of obsticles, then allow the jump - //if (tr_25.fraction == 1.0 && tr_75.fraction == 1.0) - - // Do we have room to jump without hitting our head? - //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -6), tv(16, 16, 48), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT) , g_edicts, MASK_DEADSOLID); // Player height + some jumping height - //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), NULL, NULL, tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height - //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -24), tv(16, 16, 32), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height - //tr = gi.trace(origin, tv(-16, -16, -24), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height - tr = gi.trace(origin, tv(-16, -16, 0), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height - if (tr.fraction == 1.0 && !tr.startsolid) - { - //Com_Printf("jump_height[%f] jump_height_headroom[%f]\n", jump_height, jump_height_headroom); - - // Calculate the time it will take to get to the target - float time = xyz_distance / (jump_height / 2.0); - // Calculate the velocity at the end of the jump - VectorScale(dist, 1.0 / time, velocity); - velocity[2] = jump_height / 2.0; - // - // If the target is above the player, increase the velocity to get to the target - float z_height = 0; - - // Above - if (target[2] > origin[2]) - { - z_height = (target[2] - origin[2]) + NODE_Z_HEIGHT; - velocity[2] += z_height; - - float speed = VectorLength(velocity); - //if (speed < 550 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) - if (speed < 550 && z_height <= z_height_max + sqrtf(xy_distance) && jump_height <= 450) - return NODE_JUMPPAD; - } - // Roughly equal - else if (abs(origin[2] - target[2]) <= 8) // target node is +/- 8 - { - float speed = VectorLength(velocity); - if (speed < 600 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 750) - return NODE_JUMPPAD; - } - // Below - else - { - z_height = (origin[2] - target[2]) - NODE_Z_HEIGHT; - velocity[2] -= z_height; - - float speed = VectorLength(velocity); - if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) - return NODE_JUMPPAD; - } - - // Calculate speed from velocity - //float speed = VectorLength(velocity); - - //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); - //Com_Printf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); - //if (distance > 64 && speed < 750 && z_height < z_height_max + 24 && jump_height <= 965) - //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) // Works VERY well - //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) - - //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) - // return NODE_JUMPPAD; - } - } - - - // Limit max speed - //if (speed < 750 && z_height < 150 && jump_height <= 965) // Maximum - //if (speed <= 750 && z_height < 48 && jump_height <= 525) // Works well for ground based nodes - //if (speed <= 750 && z_height < NODE_MAX_JUMP_HEIGHT && jump_height <= 965) - //if (speed <= 750 && z_height <= z_height_max && jump_height <= 600) // Works really well!! - //if (speed <= 750 && z_height <= z_height_max && jump_height <= 700) - //if (speed <= 750 && z_height <= z_height_max && jump_height <= 650) // Works uber well - //if (speed <= 750 && z_height <= z_height_max && (jump_height <= 650 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 64)) - //if (distance <= 224 && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32) ) - //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) - //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) - - // Small jump - //if (distance < 64 && higher > STEPSIZE && higher <= NODE_MAX_JUMP_HEIGHT) - //return NODE_JUMP; - - // Jumping up - //if (distance < 128 && higher > STEPSIZE && higher <= z_height_max) - //{ - //return NODE_JUMPPAD; - //} - - - - - // Running to target - // ----------------- - if (xy_distance < 400 && !is_gap) - { - if (origin[2] == target[2]) - { - //return INVALID; - - //if (from == 1066 && to == 1069) - // Com_Printf("%s\n", __func__); - - //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); - tr = gi.trace(origin, tv(-15, -15, -NODE_Z_HEIGHT), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); - //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); - if (tr.fraction == 1) - return NODE_MOVE; - } - else if (abs(origin[2] - target[2]) <= STEPSIZE) // target node is +/- STEPSIZE - { - //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); - tr = gi.trace(origin, tv(-15, -15, -(NODE_Z_HEIGHT - STEPSIZE)), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); - //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); - if (tr.fraction == 1) - return NODE_MOVE; - } - } - - /* - // Jump to target - // ----------------- - if (xy_distance < 128) - { - if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT) // target node is +/- NODE_MAX_JUMP_HEIGHT - { - // We passed a partial height test, now test players step height - // -32 is ground + 17 (one less than full step height of 18) = -14 - tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Step height - if (tr.fraction < 1) // We hit something - { - // Test if we can jump over - // mins: -32 is ground + 18 (step height) = -16 - // maxs: mins (-16) + full player size (56) = 40 - //tr = gi.trace(origin, tv(-16, -16, -16), tv(16, 16, 40), target, g_edicts, MASK_DEADSOLID); // Jump height + player height - //if (tr.fraction == 1.0) // We hit nothing, so we can jump over :) - // return NODE_JUMP; - - //VectorCopy(origin, vec_from); - //VectorCopy(target, vec_to); - //origin - - // Test if we fit inside target area - tr = gi.trace(target, tv(-16, -16, -32), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) // We fit - return NODE_JUMP; - //return NODE_JUMP; - } - else - return NODE_JUMP; - } - } - */ - - - - // Run up/down slopes and ramps - if (xy_distance < 512 && !is_gap) - { - // If we have LoS - tr = gi.trace(origin, tv(-16, -16, -14), tv(16, 16, 24), target, g_edicts, MASK_PLAYERSOLID); - if (tr.fraction > 0.9) - { - // Test for slope / stairs at the midpoint - trace_t tr_50; - vec3_t pt_50, end_50; - LerpVector(origin, target, 0.50, pt_50); // Get the two vector midpoint - VectorCopy(pt_50, end_50); - end_50[2] -= NODE_Z_HEIGHT_PLUS_STEPSIZE; - tr_50 = gi.trace(pt_50, NULL, NULL, end_50, g_edicts, MASK_DEADSOLID); - if (tr_50.fraction < 1.0) // We hit solid - { - // Slope - if (tr_50.plane.normal[0] != 0 || tr_50.plane.normal[1] != 0) - return NODE_MOVE; - } - } - } - - - return INVALID; -} - -// Check to see if triangles are coplanar -// Check to see if the triangles are adjacent -//qboolean TrianglesAreCoplanar(vec3_t t1_p1, vec3_t t1_p2, vec3_t t1_p3, vec3_t t2_p1, vec3_t t2_p2, vec3_t t2_p3) - -// Explodes all breakable glass :D -void Remove_All_Breakableglass(void) -{ - edict_t* glass; - for (glass = g_edicts; glass < &g_edicts[globals.num_edicts]; glass++) - { - if (!glass) - continue; - //if (glass->solid == SOLID_NOT) - // continue; - if (!glass->classname) - continue; - - if (strcmp(glass->classname, "breakableglass_trigger") == 0 || strcmp(glass->classname, "func_explosive") == 0) - { - //gi.dprintf("Found glass at location %f %f %f\n", glass->s.origin[0], glass->s.origin[1], glass->s.origin[2]); - //glass->takedamage = DAMAGE_YES; - //glass->die = kill_door; - //glass->health = 0; - //glass->max_health = 0; - - // Explode the glass - T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); - } - } - - /* - // iterate over all func_explosives - while ((glass = G_Find(glass, FOFS(classname), "func_explosive")) != NULL) - { - // glass is broken if solid != SOLID_BSP - if (glass->solid != SOLID_BSP) - { - T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); - } - } - */ -} - -void kill_door(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, vec3_t point) -{ - G_FreeEdict(self); -} -// Removes all door types -void Remove_All_Doors(void) -{ - edict_t* door; - for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) - { - if (!door) - continue; - if (door->solid == SOLID_NOT) - continue; - if (!door->classname) - continue; - - if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) - { - //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); - door->takedamage = DAMAGE_YES; - door->die = kill_door; - door->health = 0; - door->max_health = 0; - - // Open up the door's area portal (otherwise players will see hall-of-mirrors effect - edict_t* t = NULL; - if (door->target) - { - while ((t = G_Find(t, FOFS(targetname), door->target)) != NULL) - { - if (Q_stricmp(t->classname, "func_areaportal") == 0) { - gi.SetAreaPortalState(t->style, true); - } - } - } - - // Explode the door - T_Damage(door, door, door, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); - } - } -} - -void Open_All_Doors(edict_t* ent) -{ - if (ent != NULL) - { - if (ent->last_door_time > level.framenum - 2.0 * HZ) - return; - else - ent->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! - } - // Open all doors and keep them open - edict_t* door; - for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) - { - if (!door) - continue; - if (door->solid == SOLID_NOT) - continue; - if (!door->classname) - continue; - - if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) - { - //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); - - int DOOR_TOGGLE = 32; //#define DOOR_TOGGLE 32 - if ((door->spawnflags & DOOR_TOGGLE) == 0) - door->spawnflags |= DOOR_TOGGLE; // Bitwise add DOOR_TOGGLE to door->spawnflags - door->wait = -1; - door->speed = 9000; - door->touch_debounce_framenum = 0; - door->moveinfo.accel = 9000; - door->moveinfo.decel = 9000; - door->moveinfo.speed = 9000; - //door_use(door, ent, ent); // Open the door - } - } -} - -// Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER -#define MAX_LADDER_FACES 3072 -vec3_t surface_ladder_faces[MAX_LADDER_FACES] = { 0 }; // Origin -void ACEND_FindEdgeLadders(void) -{ - int f, e, f2, f3, i; - trace_t tr; - //vec3_t zero = { 0 }; - // Player when standing - //vec3_t player_standing_mins_up = { -16, -16, -1 }; - //vec3_t player_standing_mins = { -16, -16, -0 }; - //vec3_t player_standing_maxs = { 16, 16, 32 }; - - - //vec3_t *ladder_faces = (vec3_t*)malloc(sizeof(vec3_t) * MAX_LADDER_FACES); - //if (ladder_faces == NULL) - { - //Com_Printf("%s [CONTENTS_LADDER] ladder_faces malloc failed\n", __func__); - //return; - } - //vec3_t* ladder_face; - - //short int ladder_index[MAX_LADDER_FACES] = { 0 }; // Face - int* ladder_index = malloc(sizeof(int) * MAX_LADDER_FACES); - if (ladder_index == NULL) - { - Com_Printf("%s [CONTENTS_LADDER] ladder_index malloc failed\n", __func__); - return; - } - - - int num_ladder_faces = 0; - qboolean foundLadder = true; - qboolean ladderLimit = false; - - /* - for (f = 0; f < nmesh.total_faces; f++) - { - if (nmesh.face[f].contents & CONTENTS_LADDER) - { - Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); - } - } - */ - - // Find all the ladder faces - for (f = 0; f < nmesh.total_faces; f++) - { - if (ladderLimit) break; - - //if (nmesh.face[f].contents & CONTENTS_LADDER) - // Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); - - if (nmesh.face[f].type == FACETYPE_WALL && nmesh.face[f].contents & CONTENTS_LADDER) // If wall is Ladder - { - for (e = 0; e < nmesh.face[f].num_edges; e++) // Find the bottom of the ladder using the middle of the lowest edge - { - // Look for horizontal edges - check if the two edge verts are level - if (nmesh.face[f].edge[e].v[0][2] == nmesh.face[f].edge[e].v[1][2]) - { - // Check if we already added the ladder face to ladder_faces[] - foundLadder = true; - for (f3 = 0; f3 < num_ladder_faces; f3++) - //for (f3 = 0, ladder_face = ladder_faces; f3 < num_ladder_faces; f3++, ladder_face++) - { - if (surface_ladder_faces[f3][0] == nmesh.face[f].edge[e].center[0] && surface_ladder_faces[f3][1] == nmesh.face[f].edge[e].center[1] && surface_ladder_faces[f3][2] == nmesh.face[f].edge[e].center[2]) - //if (*(ladder_face)[0] == nmesh.face[f].edge[e].center[0] && *(ladder_face)[1] == nmesh.face[f].edge[e].center[1] && *(ladder_face)[2] == nmesh.face[f].edge[e].center[2]) - { - //Com_Printf("%s f:%i e:%i\n", __func__, f, e); - foundLadder = false; - break; - } - } - - // Found new ladder edge - if (foundLadder) - { - // Check we're not over the limit - if (num_ladder_faces + 2 > MAX_LADDER_FACES) - { - Com_Printf("%s [CONTENTS_LADDER] num_ladder_faces > max_faces\n", __func__); - ladderLimit = true; - break; - } - - //Com_Printf("%s [CONTENTS_LADDER] f:%i e:%d n:%d [%f %f %f]\n", __func__, f, e, nmesh.face[f].edge[e].node, nmesh.face[f].edge[e].center[0], nmesh.face[f].edge[e].center[1], nmesh.face[f].edge[e].center[2]); - - //ladder_index[num_ladder_faces] = f; // Copy face - VectorCopy(nmesh.face[f].edge[e].center, surface_ladder_faces[num_ladder_faces]); // Copy origin - - *(ladder_index + num_ladder_faces) = f; // Copy face - - //Com_Printf("%s ladder_index:%i f:%i\n", __func__, *(ladder_index + num_ladder_faces), f); - - //for (int i = 0; i < 3; i++) - // *(ladder_faces + num_ladder_faces * sizeof(float))[i] = nmesh.face[f].edge[e].center[i]; // Copy origin - // - //ladder_face = ladder_faces + (num_ladder_faces * sizeof(vec3_t)); // Set pointer to end of array - //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[0] = nmesh.face[f].edge[e].center[0]; - //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[1] = nmesh.face[f].edge[e].center[1]; - //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[2] = nmesh.face[f].edge[e].center[2]; - - num_ladder_faces++; - } - } - } - } - } - - - - - for (f = 0; f < num_ladder_faces; f++) - //for (f = 0, ladder_face = ladder_faces; f < num_ladder_faces; f++, ladder_face++) - { - // Com_printf ladder_faces - //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, ladder_index[f], ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2]); - //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, *(ladder_index + f), *(ladder_face)[0], *(ladder_face)[1], *(ladder_face)[2]); - } - - - - - - // Search all ladder faces looking for the lowest and highest parts of the ladder - // - // |-| <-- highest - // |-| - // |-| <-- lowest - // - qboolean tested = false; - int faces_tested_num = 0; - //short int faces_tested[MAX_LADDER_FACES]; - int* faces_tested = malloc(sizeof(int) * MAX_LADDER_FACES); - if (faces_tested == NULL) - { - Com_Printf("%s [CONTENTS_LADDER] faces_tested malloc failed\n", __func__); - return; - } - - - //vec3_t ladder_final[MAX_LADDER_FACES]; // Origin - //int ladder_final_counter = 0; - float low_z, high_z; // Height of lowest and highest parts of ladder - int low_f, high_f; // Face index of lowest and highest parts of ladder - for (f = 0; f < num_ladder_faces; f++) - { - low_z = 99999, high_z = -99999; // Reset lowest and highest height - low_f = high_f = INVALID; // Reset lowest and highest face index - - tested = false; - for (int ft = 0; ft < faces_tested_num; ft++) - { - //if (faces_tested[ft] == f) - if (*(faces_tested + ft) == f) - { - tested = true; // Skip if already tested - break; - } - } - if (tested) - continue; - - for (f2 = 0; f2 < num_ladder_faces; f2++) - { - // Check if the next ladder has the same X and Y alignment - if (surface_ladder_faces[f][0] == surface_ladder_faces[f2][0] && surface_ladder_faces[f][1] == surface_ladder_faces[f2][1]) - //if (*(ladder_faces + f)[0] == *(ladder_faces + f2)[0] && *(ladder_faces + f)[1] == *(ladder_faces + f2)[1]) - { - //Com_Printf("%s [F %i] [F %i] [%f %f %f] [%f %f %f] \n", __func__, f, f2, ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2], ladder_faces[f2][0], ladder_faces[f2][1], ladder_faces[f2][2]); - - //faces_tested[faces_tested_num++] = f2; // Add face to faces_tested - *(faces_tested + faces_tested_num++) = f2; // Add face to faces_tested - - - - if (surface_ladder_faces[f2][2] < low_z) - //if (*(ladder_faces + f2)[2] < low_z) - { - //Com_Printf("%s [F %i] [ladder_faces[f2][2] < low_z] %f < %f \n", __func__, f2, ladder_faces[f2][2], low_z); - low_z = surface_ladder_faces[f2][2]; // Update lowest height - //low_z = *(ladder_faces + f2)[2]; // Update lowest height - low_f = f2; // Update lowest face index - } - if (surface_ladder_faces[f2][2] > high_z) - //if (*(ladder_faces + f2)[2] > high_z) - { - //Com_Printf("%s [F %i] [ladder_faces[f2][2] > high_z] %f > %f \n", __func__, f2, ladder_faces[f2][2], high_z); - high_z = surface_ladder_faces[f2][2]; // Update highest height - //high_z = *(ladder_faces + f2)[2]; // Update highest height - high_f = f2; // Update highest face index - } - } - } - - foundLadder = false; - - float distance = 0; - if (surface_ladder_faces[high_f][2] > surface_ladder_faces[low_f][2]) // Make sure the top of the ladder is higher than the bottom - distance = surface_ladder_faces[high_f][2] - surface_ladder_faces[low_f][2]; - - if (distance >= 32 && low_f != INVALID && high_f != INVALID) - { - //Com_Printf("%s LO F:%i L:%f [F %i][%f %f %f]\n", __func__, low_f, low_z, ladder_index[low_f], ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); - //Com_Printf("%s HI F:%i H:%f [F %i][%f %f %f]\n", __func__, high_f, high_z, ladder_index[high_f], ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); - - - if (1) // Project the top of the ladder forward, then we test to see if its in the ground, if so we need to move the top node up - { - vec3_t ladder_forward; - VectorCopy(surface_ladder_faces[high_f], ladder_forward); - //ladder_forward[2] += NODE_Z_HEIGHT; - // Move the top ladder node closer to the ladder - int f3 = *(ladder_index + high_f); - //Com_Printf("%s f3:%i\n", __func__, f3); - if (nmesh.face[f3].normal[0] != 0) // X - { - if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) - ladder_forward[0] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // -X (forward wall) - else - ladder_forward[0] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // +X (back wall) - } - if (nmesh.face[f3].normal[1] != 0) // Y - { - if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) - ladder_forward[1] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // -Y (left wall) - else - ladder_forward[1] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // +Y (right wall) - } - // Test if we're in the ground - for (i = 0; i < 100; i++) //NODE_Z_HEIGHT - { - tr = gi.trace(ladder_forward, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), ladder_forward, g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid || tr.fraction < 1) - { - ladder_forward[2]++; - } - else - break; - } - - //Com_Printf("%s Increased top of ladder [%f %f %f] -> [%f %f %f]\n", __func__, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2], ladder_forward[0], ladder_forward[1], ladder_forward[2]); - surface_ladder_faces[high_f][2] = ladder_forward[2]; - } - - // Move the top of the ladder up before testing, this serves two purposes: test if ladder hits roof, make sure ladder reaches top - //ladder_faces[high_f][2] += NODE_Z_HEIGHT + 8; - //ladder_faces[high_f][2] += NODE_Z_HEIGHT; - - if (1) - // Test if bottom of ladder is in the ground, move it up until its not - for (i = 0; i < 100; i++) //NODE_Z_HEIGHT - { - tr = gi.trace(surface_ladder_faces[low_f], tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), surface_ladder_faces[low_f], g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid || tr.fraction < 1) - { - if (surface_ladder_faces[low_f][2] + 1 < surface_ladder_faces[high_f][2]) - surface_ladder_faces[low_f][2]++; - } - else - break; - } - - //if (ladder_faces[low_f][2] + NODE_Z_HEIGHT < ladder_faces[high_f][2]) // Make sure bottom ladder doesn't go above top of ladder - // ladder_faces[low_f][2] += NODE_Z_HEIGHT; - - - - - - - // Check if ladder ends can be connected, if so add nodes and links - //tr = gi.trace(ladder_faces[low_f], player_standing_mins_up, player_standing_maxs, ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); - tr = gi.trace(surface_ladder_faces[low_f], MINS_PLAYER_STANDING, MAXS_PLAYER_STANDING, surface_ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) - { - //ACEND_AutoAddNode(*(ladder_faces + low_f), nmesh.face[low_f].normal, NODE_LADDER_DOWN); - //ACEND_AutoAddNode(*(ladder_faces + high_f), nmesh.face[high_f].normal, NODE_LADDER_UP); - - int tn = INVALID; // Top node - int bn = INVALID; // Bottom node - - // Add Top ladder node - // ---------------------- - // Top ladder doesn't have to worry about existing nodes, so just make a new one - DAIC_Add_Node(surface_ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); - //ACEND_AutoAddNode(ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); - tn = numnodes - 1; - // ---------------------- - - // Add bottom ladder node - // ---------------------- - // Look for an existing node located on the triangle where the bottom of the ladder is, - // if so then move that node origin to the bottom ladder origin - qboolean found = false; - int f = INVALID; - int t = INVALID; - //vec3_t ladder_low; - //VectorCopy(ladder_faces[low_f], ladder_low); - //ladder_low[2] -= 24; - //GetBSPFaceTriangle(ladder_low, &found, &f, &t); - GetBSPFaceTriangle(surface_ladder_faces[low_f], &found, &f, &t); - if (found && f != INVALID && t != INVALID) //f = 0; t = 0; found = true; - { - found = false; - bn = nmesh.face[f].tris[t].node; - if (bn != INVALID) // Successfully found node - { - //Com_Printf("%s ladder tris found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); - - found = true; - VectorCopy(surface_ladder_faces[low_f], nodes[bn].origin); // Move node origin to bottom ladder origin - nodes[bn].type = NODE_LADDER_DOWN; // Update its type - } - } - // Otherwise we add a new node for the bottom of the ladder - if (found == false) - { - //Com_Printf("%s ladder tris not found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); - DAIC_Add_Node(surface_ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); - //ACEND_AutoAddNode(ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); - bn = numnodes - 1; - } - // ---------------------- - - //Com_Printf("%s NODE_LADDER_DOWN f:%i n:%d [%f %f %f]\n", __func__, ladder_index[low_f], bn, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); - //Com_Printf("%s NODE_LADDER_UP f:%i n:%d [%f %f %f]\n", __func__, ladder_index[high_f], tn, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); - - if (bn != INVALID && tn != INVALID) - { - BOTLIB_AddNodeLink(bn, tn, NODE_LADDER_DOWN, true); // Bottom to top - BOTLIB_AddNodeLink(tn, bn, NODE_LADDER_UP, true); // Top to bottom - ACEND_UpdateNodeReach(bn, tn); - ACEND_UpdateNodeReach(tn, bn); - nmesh.total_reach += 2; - } - } - } - } - - // Free memory - free(faces_tested); - faces_tested = NULL; -} - -/* -// Find if origin is touching a ladder -// Can be used to see if node is touching a ladder -qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top) -{ - float yaw_rad = 0, yaw = 0, depth; - int loops; - vec3_t fwd = { 0 }, start = { 0 }, end = { 0 }, final = { 0 }, ground = { 0 }; - vec3_t mins = { -4, -4, -24 }, maxs = { 4, 4, 32 }; - trace_t tr; - qboolean is_touching = false, found_bottom = false; - VectorCopy(origin, start); - - // Player when standing - vec3_t player_standing_mins = { -16, -16, -0 }; - vec3_t player_standing_maxs = { 16, 16, 32 }; - // Player when crouching/ducking - vec3_t player_crouching_mins = { -16, -16, -24 }; - vec3_t player_crouching_maxs = { 16, 16, 4 }; - // Player when standing smaller mins - vec3_t player_standing_smaller_mins = { -8, -8, -0 }; - vec3_t player_standing_smaller_maxs = { 8, 8, 0 }; - - return false; //////////////////////////////////////////////////////////////////////////////////////////// TEMP DISABLE LADDERS - - depth = 0; - for (int d = 0; d < 128; d++) // depth - { - // For each yaw angle, project forward and see if touching a ladder - for (int a = 0; a < 64; a++) // Spin around in a 360 circle - { - if (yaw >= 360) // Make sure we come back around - yaw = 0; - - yaw_rad = DEG2RAD(yaw); - fwd[0] = cos(yaw_rad); - fwd[1] = sin(yaw_rad); - VectorMA(start, depth, fwd, end); - VectorMA(start, (depth - 20), fwd, final); // Back away from the ladder until we're about the distance of the player center (give or take a little) - tr = gi.trace(start, mins, maxs, end, g_edicts, MASK_PLAYERSOLID); - //if (tr.fraction < 1 && (tr.contents & CONTENTS_LADDER)) - if (tr.contents & CONTENTS_LADDER) - { - // Find the bottom of the ladder - //-------------------- - // Now find the ground so we know not to go below this point - VectorCopy(end, ground); - ground[2] -= 8192; - tr = gi.trace(ladder_top, NULL, NULL, ground, g_edicts, MASK_PLAYERSOLID); - if (tr.fraction < 1) - { - VectorCopy(tr.endpos, ground); - } - loops = 0; - while (loops++ < 64) - { - // Now move down testing until we no longer hit the ladder to find its bottom - if (end[2] + 32 > ground[2]) // Can we move down? - { - // Okay move down - start[2] -= 32; - end[2] -= 32; - final[2] -= 32; - - tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); - if ((tr.contents & CONTENTS_LADDER) == 0) - { - // Ladder reached the ground so adjust the position so we're not stuck in the ground - if (tr.startsolid) - { - start[2] = tr.endpos[2] + NODE_Z_HEIGHT; - end[2] = tr.endpos[2] + NODE_Z_HEIGHT; - final[2] = tr.endpos[2] + NODE_Z_HEIGHT; - } - else - { - // Hanging ladder, didn't reach the ground - } - VectorCopy(final, ladder_bottom); - Com_Printf("Found bottom ladder at location %f %f %f\n", ladder_bottom[0], ladder_bottom[1], ladder_bottom[2]); - found_bottom = true; - break; // end while loop - } - } - } - //-------------------- - - - // Find the top of the ladder - //-------------------- - if (found_bottom) - { - // Now move up testing until we no longer hit the ladder to find its top - loops = 0; - while (loops++ < 64) - { - // move up - start[2] += 32; - end[2] += 32; - final[2] += 32; - - tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); - if ((tr.contents & CONTENTS_LADDER) == 0) - { - final[2] += NODE_Z_HEIGHT; // Adjust the final height above the top of the ladder - VectorCopy(final, ladder_top); // Ladder top - - // Make sure the ladder z height length is large enough - // edge cases: where ladder handles go above the ground we're trying to reach, such as urban2 the very highest ladder - float difference = ladder_top[2] - ladder_bottom[2]; - if (difference > 96) - { - Com_Printf("Found top ladder at location %f %f %f\n", ladder_top[0], ladder_top[1], ladder_top[2]); - is_touching = true; - return is_touching; - } - } - } - } - //-------------------- - - if (is_touching) - break; - } - yaw += 5.625; // Change yaw angle - } - depth += 2; - } - - return is_touching; -} -*/ - -//rekkie -- walknodes -- s - - -void BOTLIB_SetAllNodeNormals() -{ - vec3_t mins = { -14, -14, -4, }; - vec3_t maxs = { 14, 14, 4, }; - - for (int i = 0; i < numnodes; i++) - { - // Get normal - trace_t tr = gi.trace(tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] + 30), mins, maxs, tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] - 64), NULL, MASK_SHOT); - if (tr.fraction == 1.0) - { - //Com_Printf("%s failed node[%d] normal [%f %f %f]\n", __func__, nodes[i].nodenum, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); - continue; - } - - VectorCopy(tr.plane.normal, nodes[i].normal); - } -} - -// Compression -- s -#if USE_ZLIB -#include - -#define CHUNK_SIZE 16384 - -void BOTLIB_DecompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) -{ - z_stream stream = { 0 }; - stream.zalloc = Z_NULL; - stream.zfree = Z_NULL; - stream.opaque = Z_NULL; - int status = inflateInit(&stream); - if (status != Z_OK) { - Com_Printf("Failed to initialize decompression stream! (error %d)\n", status); - return; - } - - stream.avail_in = inputLength; - stream.next_in = (Bytef*)inputBuffer; - stream.avail_out = *outputLength; - stream.next_out = (Bytef*)outputBuffer; - - status = inflate(&stream, Z_FINISH); - if ((status != Z_OK) && (status != Z_STREAM_END)) { - Com_Printf("Failed to decompress data! (error %d)\n", status); - inflateEnd(&stream); - return; - } - - *outputLength = stream.total_out; - inflateEnd(&stream); -} -void BOTLIB_CompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) { - z_stream stream = { 0 }; - stream.zalloc = Z_NULL; - stream.zfree = Z_NULL; - stream.opaque = Z_NULL; - - int status = deflateInit(&stream, Z_BEST_COMPRESSION); - if (status != Z_OK) { - Com_Printf("Failed to initialize compression stream! (error %d)\n", status); - return; - } - - stream.avail_in = inputLength; - stream.next_in = (Bytef*)inputBuffer; - stream.avail_out = inputLength; // *outputLength; - stream.next_out = (Bytef*)outputBuffer; - - status = deflate(&stream, Z_FINISH); - if ((status != Z_OK) && (status != Z_STREAM_END)) { - Com_Printf("Failed to compress data! (error %d)\n", status); - deflateEnd(&stream); - return; - } - - *outputLength = stream.total_out; - - deflateEnd(&stream); -} - -// Assume the following variables are declared on the other end: -// char* buffer; // Pointer to the outgoing buffer to add data to -// int bufferLength; // Length of the outgoing buffer -// int dataLength; // Length of the incoming data -// char* incomingData; // Pointer to the incoming data to add -void BOTLIB_AddToBuffer(char* incomingData, int dataLength, char** bufferPtr, int* bufferLengthPtr) -{ - if (*bufferPtr == NULL) - { // If the buffer is null, set the buffer to the incoming data ) - char *newBuffer = (char*)malloc(*bufferLengthPtr + dataLength); - if (newBuffer != NULL) - { // If the allocation was successful, copy the incoming data to the end of the buffer - memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); - *bufferPtr = newBuffer; - *bufferLengthPtr += dataLength; - } else { // If the allocation was not successful, handle the error - Com_Printf("Failed to allocate buffer for incoming data!\n"); - } - } - else - { - // If the buffer is not null, reallocate the buffer to the new size - char *newBuffer = (char*)realloc(*bufferPtr, *bufferLengthPtr + dataLength); - if (newBuffer != NULL) - { // If the allocation was successful, copy the incoming data to the end of the buffer - memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); - *bufferPtr = newBuffer; - *bufferLengthPtr += dataLength; - } else { // If the allocation was not successful, handle the error - Com_Printf("Failed to allocate buffer for incoming data!\n"); - } - } -} - - -// ============================================================================================ -// These group of functions sort the node links from highest to lowest based on their zheight -// difference from the node they're connected to. node -> link(s) zheight difference. We run -// this function upon saving nodes. -// -// This is done so that when pathing the highest node links are consindered first. Although, -// currently the bots use Dijkstra Pathing, so this is ignored for now. -// ============================================================================================ -// Temp link structure -typedef struct { - int targetNode; - int targetNodeType; - double cost; - double zheight; // Not part of the original link struct. Used by qsort() to order highest -> lowest -} QSLink; // Qsorted Link - -// Compare function for qsort() -static int QScompare(const void* a, const void* b) { - return ((QSLink*)b)->zheight - ((QSLink*)a)->zheight; -} - -void QsortLinks(QSLink* links, size_t length) { - qsort(links, length, sizeof(*links), QScompare); -} - -// Sorts node links by height. Highest to lowest. -void QSNodeLinksByHeight(void) -{ - QSLink qslinks[MAXLINKS]; // Temp links - - for (int n = 0; n < numnodes; n++) // Each node - { - for (int i = 0; i < nodes[n].num_links; ++i) // Each link - { - // Copy original link data into our temp struct - qslinks[i].targetNode = nodes[n].links[i].targetNode; - qslinks[i].targetNodeType = nodes[n].links[i].targetNodeType; - qslinks[i].cost = nodes[n].links[i].cost; - - // The original data struct doesn't use or contain the zheight of a linked node, so we create it here for qsort() to use, - int tnode = nodes[n].links[i].targetNode; // Target node - // - if ((nodes[tnode].origin[2] - nodes[n].origin[2]) >= 1) // Only sort linked nodes that are higher than the node itself and are >= 1 in height difference - qslinks[i].zheight = (nodes[tnode].origin[2] - nodes[n].origin[2]); - else - qslinks[i].zheight = 0; // Any height < 1 just consider as 0 height - } - - // Debug print - /* - qboolean hasDiffHeight = false; - for (int i = 0; i < nodes[n].num_links; ++i) - { - if (nodes[n].links[i].zheight >= 1) // Only if height diff is >= 1 - { - hasDiffHeight = true; - break; - } - } - */ - - // Debug print - /* - if (0 && hasDiffHeight) - { - Com_Printf("\nBefore sorting:\n"); - for (int i = 0; i < nodes[n].num_links; ++i) - Com_Printf("{%d %d %f %f}\n", nodes[n].links[i].targetNode, nodes[n].links[i].targetNodeType, nodes[n].links[i].cost, nodes[n].links[i].zheight); - } - */ - - // Sort links from highest to lowest - QsortLinks(qslinks, nodes[n].num_links); - - // Debug print - /* - if (0 && hasDiffHeight) // Debug print - { - Com_Printf("\nAfter sorting:\n"); - for (int i = 0; i < nodes[n].num_links; ++i) - Com_Printf("{%d %d %f %f}\n", qslinks[i].targetNode, qslinks[i].targetNodeType, qslinks[i].cost, qslinks[i].zheight); - } - */ - - // Override and store the original link data - for (int i = 0; i < nodes[n].num_links; ++i) - { - nodes[n].links[i].targetNode = qslinks[i].targetNode; - nodes[n].links[i].targetNodeType = qslinks[i].targetNodeType; - nodes[n].links[i].cost = qslinks[i].cost; - } - } -} -// ============================================== -// ============================================== - -void BOTLIB_SaveNavCompressed(void) -{ - FILE* fOut; - char filename[128]; - int fileSize = 0; - int f, n, l; // File, nodes, links - int version = BOT_NAV_VERSION; - cvar_t* game_dir = gi.cvar("game", "action", 0); - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".nav"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".nav"); -#endif - - if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary - { - Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); - return; - } - - Com_Printf("%s Writing compressed NAV to file...\n", __func__); - - QSNodeLinksByHeight(); // Sort node links by height, highest to lowest - - char* buff = NULL; - int uncompressed_buff_len = 0; - BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &uncompressed_buff_len); - for (n = 0; n < numnodes; n++) - { - // Nodes - BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &uncompressed_buff_len); - //BOTLIB_AddToBuffer((char*)&nodes[n].normal, sizeof(vec3_t), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &uncompressed_buff_len); - - // Links - BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &uncompressed_buff_len); - for (l = 0; l < nodes[n].num_links; l++) - { - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &uncompressed_buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &uncompressed_buff_len); - } - - // Paths - //for (p = 0; p < numnodes; p++) - //{ - //BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &uncompressed_buff_len); // Path table - //} - } - - // Compress the buffer - long compressed_buff_len = 0; - char* compressed_buff = (char*)malloc(uncompressed_buff_len); // Make the compressed buffer as large as the uncompressed buffer - if (compressed_buff != NULL) - { - BOTLIB_CompressBuffer(buff, uncompressed_buff_len, compressed_buff, &compressed_buff_len); - - // Compare and print the size of the buffers - //Com_Printf("%s uncompressed_buff_len:%i compressed_buff_len:%i\n", __func__, uncompressed_buff_len, compressed_buff_len); - - // Write version, checksum, and buffer sizes - fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // NAV Version - //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum - fileSize += sizeof(int) * fwrite(&uncompressed_buff_len, sizeof(int), 1, fOut); // Uncompressed buffer size - fileSize += sizeof(int) * fwrite(&compressed_buff_len, sizeof(int), 1, fOut); // Compressed buffer size - - // Write compressed node data - fwrite(compressed_buff, compressed_buff_len, 1, fOut); - fileSize += compressed_buff_len; - } - else - { - Com_Printf("%s Failed to allocate memory for compressed nav buffer\n", __func__); - } - - // Cache all the POI nodes - if (numnodes) - { - num_poi_nodes = 0; // Reset any previous POI nodes - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].type == NODE_POI) - { - poi_nodes[num_poi_nodes] = i; - num_poi_nodes++; - } - } - } - - fclose(fOut); - - if (buff != NULL) - free(buff); - - if (compressed_buff != NULL) - free(compressed_buff); - - Com_Printf("%s saved %s containing %d nodes. Nav zlib compressed [%i bytes] to [%i bytes]\n", __func__, filename, numnodes, uncompressed_buff_len, fileSize); -} -void BOTLIB_LoadNavCompressed(void) -{ - FILE* fIn; - char filename[128]; - int fileSize = 0; - int f, n, l; // File, nodes, links - int version = 0; // Bot nav version - unsigned bsp_checksum = 0; // Map checksum - cvar_t* game_dir = gi.cvar("game", "action", 0); - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - const vec3_t mins = { -16, -16, -24 }; - const vec3_t maxs = { 16, 16, 32 }; - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".nav"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".nav"); -#endif - - if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists - { - return; // No file - } - else - { - fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version - if (version < BOT_NAV_VERSION_1 || version > BOT_NAV_VERSION) - { - Com_Printf("%s ERROR: NAV file version mismatch. Got %d, expected value between %d and %d\n", __func__, version, BOT_NAV_VERSION_1, BOT_NAV_VERSION); - fclose(fIn); // Close the file - return; - } - /* - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - fclose(fIn); // Close the file - return; - } - */ - } - - // Init Nodes - BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used - BOTLIB_InitNodes(); - BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used - //memset(&nmesh, 0, sizeof(nmesh_t)); - - // Remove doors - Remove_All_Doors(); - Remove_All_Breakableglass(); - - Com_Printf("%s Reading NAV file... detected version [%d]\n", __func__, version); - - // Read Compressed and uncompressed buffer sizes - long uncompressed_buff_len = 0; - fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size - long compressed_buff_len = 0; - fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size - - // Read compressed buffer - char* uncompressed_buff = (char*)malloc(uncompressed_buff_len); - char* compressed_buff = (char*)malloc(compressed_buff_len); - if (compressed_buff != NULL && uncompressed_buff != NULL) - { - fileSize += compressed_buff_len * fread(compressed_buff, compressed_buff_len, 1, fIn); // Compressed buffer - - BOTLIB_DecompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); - //Com_Printf("%s compressed_buff_len:%i uncompressed_buff_len:%i\n", __func__, compressed_buff_len, uncompressed_buff_len); - - // Read the uncompressed buffer - { - int buff_read = 0; - - // Total nodes - memcpy(&numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); - buff_read += sizeof(unsigned short); - - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); - return; - } - - // Alloc nodes - nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory - if (nodes == NULL) - { - Com_Printf("%s failed to malloc nodes\n", __func__); - return; - } - - // Total areas - //nav_area.total_areas = 0; - - //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, version, numnodes); - - // Read node data - for (n = 0; n < numnodes; n++) - { - if (version > BOT_NAV_VERSION_1) - { - // Area/chunk/group this node belongs to -- optimise and diversify bot pathing - memcpy(&nodes[n].area, uncompressed_buff + buff_read, sizeof(int)); // Node area - buff_read += sizeof(int); - - nodes[n].area_color = 0; - } - //VectorClear(nodes[n].normal); - //if (version > BOT_NAV_VERSION_2) - //{ - //memcpy(&nodes[n].normal, uncompressed_buff + buff_read, sizeof(vec3_t)); // Surface normal - //buff_read += sizeof(vec3_t); - //} - - nodes[n].weight = 0; // Used to help diversify bot pathing - - memcpy(&nodes[n].origin, uncompressed_buff + buff_read, sizeof(vec3_t)); // Location - buff_read += sizeof(vec3_t); - memcpy(&nodes[n].type, uncompressed_buff + buff_read, sizeof(byte)); // Node type - buff_read += sizeof(byte); - - VectorCopy(mins, nodes[n].mins); // Box mins - VectorCopy(maxs, nodes[n].maxs); // Box maxs - if (nodes[n].type == NODE_CROUCH) - nodes[n].maxs[2] = CROUCHING_MAXS2; - else if (nodes[n].type == NODE_BOXJUMP) - { - nodes[n].mins[0] = -8; - nodes[n].mins[1] = -8; - nodes[n].mins[2] = -12; - nodes[n].maxs[0] = 8; - nodes[n].maxs[1] = 8; - nodes[n].maxs[2] = 16; - } - VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world - VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world - - memcpy(&nodes[n].nodenum, uncompressed_buff + buff_read, sizeof(short int)); // Node number - buff_read += sizeof(short int); - memcpy(&nodes[n].inuse, uncompressed_buff + buff_read, sizeof(qboolean)); // Node inuse - buff_read += sizeof(qboolean); - - // Init MAXLINKS links - for (l = 0; l < MAXLINKS; l++) - { - nodes[n].links[l].targetNode = INVALID; // Link - nodes[n].links[l].targetNodeType = INVALID; // Type - nodes[n].links[l].cost = INVALID; // Cost - } - - // Read the num_links - memcpy(&nodes[n].num_links, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - //Com_Printf("%s origin:%f %f %f type:%i nodenum:%i inuse:%i num_links:%i\n", __func__, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2], nodes[n].type, nodes[n].nodenum, nodes[n].inuse, nodes[n].num_links); - - for (l = 0; l < nodes[n].num_links; l++) // - { - memcpy(&nodes[n].links[l].targetNode, uncompressed_buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - memcpy(&nodes[n].links[l].targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - memcpy(&nodes[n].links[l].cost, uncompressed_buff + buff_read, sizeof(float)); - buff_read += sizeof(float); - //Com_Printf("%s targetNode:%i targetNodeType:%i cost:%f\n", __func__, nodes[n].links[l].targetNode, nodes[n].links[l].targetNodeType, nodes[n].links[l].cost); - } - - // Paths - //for (p = 0; p < numnodes; p++) - //{ - //memcpy(&path_table[n][p], uncompressed_buff + buff_read, sizeof(short int)); // Path table - //buff_read += sizeof(short int); - //Com_Printf("%s path_table[%i][%i]:%i\n", __func__, n, p, path_table[n][p]); - //} - } - } - } - - // Free memory - if (compressed_buff != NULL) - free(compressed_buff); - if (uncompressed_buff != NULL) - free(uncompressed_buff); - - // Fix any dangling or unusual links - if (numnodes) - { - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].inuse == false) - BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node - - if (nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) - BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node - - /* - for (int l = 0; l < nodes[i].num_links; l++) - { - int tnode = nodes[i].links[l].targetNode; - - if (nodes[tnode].origin[0] == 0 && nodes[tnode].origin[1] == 0 && nodes[tnode].origin[2] == 0) - { - BOTLIB_RemoveAllNodeLinksFrom(nodes[tnode].nodenum); // Remove all links to and from this node - } - } - */ - } - } - - fclose(fIn); - - Com_Printf("%s loaded %s containing %d nodes.\n", __func__, filename, numnodes); - - // Update old versions to new format - if (version == BOT_NAV_VERSION_1) - { - for (int i = 0; i < numnodes; i++) - { - nodes[i].area = 0; - } - } - // Save changes to file - if (version < BOT_NAV_VERSION) - { - Com_Printf("%s Updating NAV file to new version [%d]\n", __func__, BOT_NAV_VERSION); - BOTLIB_SaveNavCompressed(); - } - - //BOTLIB_SetAllNodeNormals(); // Set all the normals - - // Init Areas (must be done after nodes are loaded) - if (numnodes) - { - BOTLIB_InitAreaNodes(); - BOTLIB_InitAreaConnections(); - } - - // Cache all the POI nodes - if (numnodes) - { - num_poi_nodes = 0; // Reset any previous POI nodes - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].type == NODE_POI) - { - poi_nodes[num_poi_nodes] = i; - num_poi_nodes++; - } - } - } - -} -#endif - -void BOTLIB_SaveNav(void) -{ - FILE* fOut; - char filename[128]; - int fileSize = 0; - int f, n, l, p; // File, nodes, links, paths - int version = BOT_NAV_VERSION; - cvar_t* game_dir = gi.cvar("game", "action", 0); - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".nav"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".nav"); -#endif - - if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary - { - Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); - return; - } - - Com_Printf("%s Writing NAV to file...\n", __func__); - -#if 0 -#ifdef USE_ZLIB - char* buff = NULL; - int buff_len = 0; - //BOTLIB_AddToBuffer((char*)&version, sizeof(int), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &buff_len); - for (n = 0; n < numnodes; n++) - { - // Nodes - BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &buff_len); - - // Links - BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &buff_len); - for (l = 0; l < nodes[n].num_links; l++) // - { - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &buff_len); - BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &buff_len); - } - - // Paths - for (p = 0; p < numnodes; p++) - { - BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &buff_len); // Path table - } - } - // Compress the buffer - int compressed_buff_len = 0; - char* compressed_buff = (char*)malloc(buff_len); - if (compressed_buff != NULL) - { - //memset(compressed_buff, 0, buff_len); - compressBuffer(buff, buff_len, compressed_buff, &compressed_buff_len); - // Compare and print the size of the buffers - Com_Printf("%s buff_len:%i compressed_buff_len:%i\n", __func__, buff_len, compressed_buff_len); - - int uncompressed_buff_len = buff_len; - char* uncompressed_buff = (char*)malloc(buff_len); - decompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); - Com_Printf("%s uncompressed_buff_len:%i\n", __func__, uncompressed_buff_len); - - if (uncompressed_buff != NULL) - { - // Read the uncompressed version - int buff_read = 0; - //int buff_version = 0; - //memcpy(&buff_version, uncompressed_buff, sizeof(int)); - //buff_read += sizeof(int); - // Read the buffer numnodes - unsigned short buff_numnodes = 0; - //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); - memcpy(&buff_numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); - buff_read += sizeof(unsigned short); - //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); - for (n = 0; n < buff_numnodes; n++) - { - // Read the origin - vec3_t buff_origin = { 0 }; - memcpy(&buff_origin, uncompressed_buff + buff_read, sizeof(vec3_t)); - buff_read += sizeof(vec3_t); - // Read the type - byte buff_type = 0; - memcpy(&buff_type, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - // Read the nodenum - short int buff_nodenum = 0; - memcpy(&buff_nodenum, uncompressed_buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - // Read the inuse - qboolean buff_inuse = false; - memcpy(&buff_inuse, uncompressed_buff + buff_read, sizeof(qboolean)); - buff_read += sizeof(qboolean); - // Read the num_links - byte buff_num_links = 0; - memcpy(&buff_num_links, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); - - for (l = 0; l < buff_num_links; l++) // - { - // Read the targetNode - short int buff_targetNode = 0; - memcpy(&buff_targetNode, uncompressed_buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - // Read the targetNodeType - byte buff_targetNodeType = 0; - memcpy(&buff_targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - // Read the cost - float buff_cost = 0; - memcpy(&buff_cost, uncompressed_buff + buff_read, sizeof(float)); - buff_read += sizeof(float); - Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); - break; - } - for (p = 0; p < buff_numnodes; p++) - { - // Read the path_table - short int buff_path_table = 0; - memcpy(&buff_path_table, uncompressed_buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); - break; - } - - break; - } - } - - - if (1) - { - // Read the buffer version - int buff_read = 0; - //int buff_version = 0; - //memcpy(&buff_version, buff, sizeof(int)); - //buff_read += sizeof(int); - // Read the buffer numnodes - unsigned short buff_numnodes = 0; - //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); - memcpy(&buff_numnodes, buff + buff_read, sizeof(unsigned short)); - buff_read += sizeof(unsigned short); - //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); - for (n = 0; n < buff_numnodes; n++) - { - // Read the origin - vec3_t buff_origin = { 0 }; - memcpy(&buff_origin, buff + buff_read, sizeof(vec3_t)); - buff_read += sizeof(vec3_t); - // Read the type - byte buff_type = 0; - memcpy(&buff_type, buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - // Read the nodenum - short int buff_nodenum = 0; - memcpy(&buff_nodenum, buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - // Read the inuse - qboolean buff_inuse = false; - memcpy(&buff_inuse, buff + buff_read, sizeof(qboolean)); - buff_read += sizeof(qboolean); - // Read the num_links - byte buff_num_links = 0; - memcpy(&buff_num_links, buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); - - for (l = 0; l < buff_num_links; l++) // - { - // Read the targetNode - short int buff_targetNode = 0; - memcpy(&buff_targetNode, buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - // Read the targetNodeType - byte buff_targetNodeType = 0; - memcpy(&buff_targetNodeType, buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - // Read the cost - float buff_cost = 0; - memcpy(&buff_cost, buff + buff_read, sizeof(float)); - buff_read += sizeof(float); - Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); - break; - } - for (p = 0; p < buff_numnodes; p++) - { - // Read the path_table - short int buff_path_table = 0; - memcpy(&buff_path_table, buff + buff_read, sizeof(short int)); - buff_read += sizeof(short int); - Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); - break; - } - - break; - } - } - - } - else - { - Com_Printf("%s Failed to allocate memory for compressed buffer\n", __func__); - } - - if (buff != NULL) - free(buff); - - if (compressed_buff != NULL) - free(compressed_buff); - -#endif -#endif - - // Write general data - fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version - //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum - - // Write node data - fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes - for (n = 0; n < numnodes; n++) - { - // Nodes - fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Node origin - fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type - fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number - fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse - - // Links - fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links - if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); - for (l = 0; l < nodes[n].num_links; l++) // - { - fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node - fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type - fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost - } - - // Paths - for (p = 0; p < numnodes; p++) - { - fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table - } - } - - // Cache all the POI nodes - if (numnodes) - { - num_poi_nodes = 0; // Reset any previous POI nodes - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].type == NODE_POI) - { - poi_nodes[num_poi_nodes] = i; - num_poi_nodes++; - } - } - } - - - fclose(fOut); - - Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); -} - -void BOTLIB_LoadNav(void) -{ - FILE* fIn; - char filename[128]; - int fileSize = 0; - int f, n, l, p; // File, nodes, links, paths - int version = 0; // Bot nav version - unsigned bsp_checksum = 0; // Map checksum - cvar_t* game_dir = gi.cvar("game", "action", 0); - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - const vec3_t mins = { -16, -16, -24 }; - const vec3_t maxs = { 16, 16, 32 }; - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".nav"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".nav"); -#endif - - if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists - { - return; // No file - } - else - { - - fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version - if (version != BOT_NAV_VERSION) - { - fclose(fIn); // Close the file - return; - } - /* - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - fclose(fIn); // Close the file - return; - } - */ - } - - // Init - BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used - BOTLIB_InitNodes(); - BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used - //memset(&nmesh, 0, sizeof(nmesh_t)); - - // Remove doors - Remove_All_Doors(); - Remove_All_Breakableglass(); - - Com_Printf("%s Reading NAV file...\n", __func__); - - fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes - - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); - return; - } - - // Main nodes - nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory - if (nodes == NULL) - { - Com_Printf("%s failed to malloc nodes\n", __func__); - return; - } - - // Read node data - for (n = 0; n < numnodes; n++) - { - nodes[n].weight = 0; // Used to help diversify bot pathing - - // Nodes - fileSize += sizeof(int) * fread(&nodes[n].area, sizeof(int), 1, fIn); // Area - fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location - fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type - - VectorClear(nodes[n].normal); // Surface normal - VectorCopy(mins, nodes[n].mins); // Box mins - VectorCopy(maxs, nodes[n].maxs); // Box maxs - if (nodes[n].type == NODE_CROUCH) - nodes[n].maxs[2] = CROUCHING_MAXS2; - else if (nodes[n].type == NODE_BOXJUMP) - { - nodes[n].mins[0] = -8; - nodes[n].mins[1] = -8; - nodes[n].mins[2] = -12; - nodes[n].maxs[0] = 8; - nodes[n].maxs[1] = 8; - nodes[n].maxs[2] = 16; - } - VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world - VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world - - nodes[n].weight = 0; // Used to help diversify bot pathing - - fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number - fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse - - // Init MAXLINKS links - for (l = 0; l < MAXLINKS; l++) - { - nodes[n].links[l].targetNode = INVALID; // Link - nodes[n].links[l].targetNodeType = INVALID; // Type - nodes[n].links[l].cost = INVALID; // Cost - } - - // Copy num_links links - fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links - for (l = 0; l < nodes[n].num_links; l++) - { - fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node - fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type - fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost - } - - // Paths - for (p = 0; p < numnodes; p++) - { - fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table - } - } - - // Init Areas (must be done after nodes are loaded) - if (numnodes) - { - BOTLIB_InitAreaNodes(); - BOTLIB_InitAreaConnections(); - } - - // Cache all the POI nodes - if (numnodes) - { - num_poi_nodes = 0; // Reset any previous POI nodes - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].type == NODE_POI) - { - poi_nodes[num_poi_nodes] = i; - num_poi_nodes++; - } - } - } - - fclose(fIn); - - Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); -} - -//rekkie -- walknodes -- e - -/////////////////////////////////////////////////////////////////////// -// Save to disk file -/////////////////////////////////////////////////////////////////////// -void ACEND_SaveAAS(qboolean update) -{ - FILE* fOut; - char filename[128]; - int fileSize = 0; - int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach - int n, l, p; // Nodes, links, paths - int version = BOT_AAS_VERSION; - cvar_t *game_dir = gi.cvar("game", "action", 0); - cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".aas"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".aas"); -#endif - - // If not updating, process from scratch - if (update == false) - { - // Proccess BSP data so we can save it to disk - ACEND_BSP(NULL); - } - - if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary - { - Com_Printf("%s Failed to write AAS to file %s\n", __func__, filename); - return; - } - - Com_Printf("%s Writing AAS to file...\n", __func__); - - // Write general data - fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // AAS Version - fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum - fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_faces, sizeof(unsigned short), 1, fOut); // Total Faces - fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_edges, sizeof(unsigned short), 1, fOut); // Total Edges - fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_verts, sizeof(unsigned short), 1, fOut); // Total Verticies - fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_tris, sizeof(unsigned short), 1, fOut); // Total Triangles - fileSize += sizeof(int) * fwrite(&nmesh.total_reach, sizeof(int), 1, fOut); // Total Reachabilities - fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes - - // Write BSP Data - for (f = 0; f < nmesh.total_faces; f++) - { - // Write face verts - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fOut); - for (v = 0; v < nmesh.face[f].num_verts; v++) - { - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fOut); - } - - // Write edges - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fOut); - for (e = 0; e < nmesh.face[f].num_edges; e++) - { - // Write edge verts - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fOut); - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fOut); - - // Write edge center - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fOut); - - // Write edge node - fileSize += sizeof(short int) * fwrite(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fOut); - } - - // Write triangles - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fOut); - for (t = 0; t < nmesh.face[f].num_tris; t++) - { - // Write triangle verts - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fOut); - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fOut); - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fOut); - - // Write triangle center - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fOut); - - // Write triangle face - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fOut); - - // Write triangle node - fileSize += sizeof(short int) * fwrite(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fOut); - - // Write triangle reachabilities - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fOut); - for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) - { - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fOut); - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fOut); - fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fOut); - } - } - - fileSize += sizeof(byte) * fwrite(&nmesh.face[f].type, sizeof(byte), 1, fOut); // Surface type - fileSize += sizeof(int) * fwrite(&nmesh.face[f].drawflags, sizeof(int), 1, fOut); // Surface flags - fileSize += sizeof(int) * fwrite(&nmesh.face[f].contents, sizeof(int), 1, fOut); // Surface contents - fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].normal, sizeof(vec3_t), 1, fOut); // Surface normal - } - - - // Write node data - for (n = 0; n < numnodes; n++) - { - // Nodes - fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Location - fileSize += sizeof(vec3_t) * fwrite(&nodes[n].normal, sizeof(vec3_t), 1, fOut); // Surface normal - fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type - //fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number - fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse - - - // Links - fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links - if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); - for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones - //for (l = 0; l < nodes[numnodes].num_links; l++) // Only include valid links - { - fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node - fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type - fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost - } - - // Paths - for (p = 0; p < numnodes; p++) - { - fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table - } - } - - - fclose(fOut); - - Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); -} - -/////////////////////////////////////////////////////////////////////// -// Load Area Awareness System (from file or generative) -// Parameter: force - generates a new AAS instead of loading from file -/////////////////////////////////////////////////////////////////////// -void ACEND_LoadAAS(qboolean force) -{ - FILE* fIn; - char filename[128]; - int fileSize = 0; - qboolean generateAAS = false; - int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach - int n, l, p; // Nodes, links, paths - int version = 0; // Bot nav version - unsigned bsp_checksum = 0; // Map checksum - cvar_t *game_dir = gi.cvar("game", "action", 0); - cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - f = sprintf(filename, ".\\"); - f += sprintf(filename + f, game_dir->string); - f += sprintf(filename + f, "\\"); - f += sprintf(filename + f, botdir->string); - f += sprintf(filename + f, "\\nav\\"); - f += sprintf(filename + f, level.mapname); - f += sprintf(filename + f, ".aas"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/nav/"); - strcat(filename, level.mapname); - strcat(filename, ".aas"); -#endif - - if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists - { - generateAAS = true; // Generate new AAS - } - else - { - fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version - if (version != BOT_AAS_VERSION) - { - generateAAS = true; // Generate new AAS - fclose(fIn); // Close the file - } - - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - generateAAS = true; // Generate new AAS - fclose(fIn); // Close the file - } - - } - - if (force) - generateAAS = true; // Force: generate a new AAS instead of loading from file - - // Generate a new AAS? - if (generateAAS) - { - Com_Printf("%s Generating botnav. Please wait this might take a while\n", __func__); - ACEND_SaveAAS(false); - return; - } - - // Init - BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used - BOTLIB_InitNodes(); - BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used - memset(&nmesh, 0, sizeof(nmesh_t)); - - // Remove doors - Remove_All_Doors(); - - Com_Printf("%s Reading AAS file...\n", __func__); - - fileSize += sizeof(unsigned short) * fread(&nmesh.total_faces, sizeof(unsigned short), 1, fIn); // Total Faces - fileSize += sizeof(unsigned short) * fread(&nmesh.total_edges, sizeof(unsigned short), 1, fIn); // Total Edges - fileSize += sizeof(unsigned short) * fread(&nmesh.total_verts, sizeof(unsigned short), 1, fIn); // Total Verticies - fileSize += sizeof(unsigned short) * fread(&nmesh.total_tris, sizeof(unsigned short), 1, fIn); // Total Triangles - fileSize += sizeof(int) * fread(&nmesh.total_reach, sizeof(int), 1, fIn); // Total Reachabilities - fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes - - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s ERROR: Too many nodes in AAS file\n", __func__); - return; - } - - // Read BSP Data - for (f = 0; f < nmesh.total_faces; f++) - { - // Read face verts - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fIn); - for (v = 0; v < nmesh.face[f].num_verts; v++) - { - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fIn); - } - - // Read edges - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fIn); - for (e = 0; e < nmesh.face[f].num_edges; e++) - { - // Read edge verts - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fIn); - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fIn); - - // Read edge center - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fIn); - - // Read edge node - fileSize += sizeof(short int) * fread(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fIn); - } - - // Read triangles - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fIn); - for (t = 0; t < nmesh.face[f].num_tris; t++) - { - // Read triangle verts - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fIn); - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fIn); - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fIn); - - // Read triangle center - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fIn); - - // Read triangle face - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fIn); - - // Read triangle node - fileSize += sizeof(short int) * fread(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fIn); - - // Read triangle reachabilities - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fIn); - for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) - { - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fIn); - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fIn); - fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fIn); - } - } - - fileSize += sizeof(byte) * fread(&nmesh.face[f].type, sizeof(byte), 1, fIn); // Surface type - fileSize += sizeof(int) * fread(&nmesh.face[f].drawflags, sizeof(int), 1, fIn); // Surface flags - fileSize += sizeof(int) * fread(&nmesh.face[f].contents, sizeof(int), 1, fIn); // Surface contents - fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].normal, sizeof(vec3_t), 1, fIn); // Surface normal - } - - - // Main nodes - nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory - if (nodes == NULL) - { - Com_Printf("%s failed to malloc nodes\n", __func__); - return; - } - - /* - // Path table - path_table = (short int**)malloc(sizeof(short int*) * MAX_PNODES); - if (path_table == NULL) - { - Com_Printf("%s failed to malloc path_table[]\n", __func__); - return; - } - else - { - // Dynamically allocate memory of size MAX_PNODES for each row - for (int r = 0; r < numnodes; r++) - { - path_table[r] = (short int*)malloc(sizeof(short int) * MAX_PNODES); - if (path_table[r] == NULL) - { - Com_Printf("%s failed to malloc path_table[][]\n", __func__); - return; - } - } - } - - Com_Printf("%s allocing MBytes[%ld] for nodes\n", __func__, ((sizeof(node_t) * numnodes) / 1024000)); - Com_Printf("%s allocing MBytes[%ld] for path_table\n", __func__, ((sizeof(short int) * MAX_PNODES * MAX_PNODES) / 1024000)); - */ - - - // Read node data - for (n = 0; n < numnodes; n++) - { - // Nodes - fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location - fileSize += sizeof(vec3_t) * fread(&nodes[n].normal, sizeof(vec3_t), 1, fIn); // Surface normal - fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type - //fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number - fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse - nodes[n].nodenum = n; // Node number - - for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones - { - nodes[n].links[l].targetNode = INVALID; // Link - nodes[n].links[l].targetNodeType = INVALID; // Type - nodes[n].links[l].cost = INVALID; // Cost - } - - // Links - fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links - if (nodes[n].num_links > MAXLINKS) Com_Printf("%s node[%d] has more than MAXLINKS[%d] links\n", __func__, n, MAXLINKS); - //else Com_Printf("%s node[%d] has links[%d]\n", __func__, n, nodes[n].num_links); - //for (l = 0; l < nodes[numnodes].num_links; l++) // Include all links, even invalid ones - for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones - { - fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node - fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type - fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost - } - - // Paths - for (p = 0; p < numnodes; p++) - { - fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table - } - } - - - - /* - fread(&num_items, sizeof(int), 1, fIn); // read facts count - fread(nodes, sizeof(node_t), numnodes, fIn); - fread(item_table, sizeof(item_table_t), num_items, fIn); - // Raptor007: Do not trust saved pointers! - for (i = 0; i < MAX_EDICTS; i++) - item_table[i].ent = NULL; - */ - - fclose(fIn); - - Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); - - /* - // Try placing POI nodes on high locations - if (1) - { - float highest_origin = -9999; - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].origin[2] > highest_origin) - highest_origin = nodes[i].origin[2]; - } - float above_height = (highest_origin - 256); - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].type == NODE_MOVE && nodes[i].origin[2] > above_height && random() < 0.1) - { - nodes[i].type = NODE_POI; - } - } - - ACEND_CachePointOfInterestNodes(); - } - - ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies - */ -} - -// Find the reachability for each node -void BOTLIB_NODE_REACHABILITY(edict_t* ent) -{ - node_t* from_node; // From node - node_t* to_node; // To node - int s, result; - vec3_t zero = { 0 }; - //qboolean found_ladder; - for (s = 0; s < numnodes; s++) - { - if (nodes[s].inuse == false) continue; // Ignore nodes not in use - - //if (nodes[s].nodenum != INVALID) continue; - - // Get the next 'from' node - from_node = (unsorted_nodes + s); // From Node - - // Zero the link counter - from_node->num_links = 0; - - for (int o = 0; o < numnodes; o++) - { - if (nodes[o].inuse == false) continue; // Ignore nodes not in use - //if (nodes[o].nodenum != INVALID) continue; - - if (s == o) // skip self - continue; - - to_node = (unsorted_nodes + o); // Node A - - result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); - - //TEST CODE -- s - //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes - // result = INVALID; - //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) - // result = NODE_JUMPPAD; - //TEST CODE -- e - - if (result != INVALID) - { - for (int lk = 0; lk < MAXLINKS; lk++) - { - if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link - { - from_node->links[lk].targetNodeType = result; // Set target node type - - // Add link - //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); - //break; - - - //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes - if (BOTLIB_AddNodeLink(s, o, result, true)) - { - ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table - //from_node->num_links++; // Update number of links this node has - nmesh.total_reach++; - } - break; - } - } - } - } - } -} - -// Similar to gi.pointcontents, but returns the contents within the bounding box around the origin -// Useful when checking for multiple contents within the local area, specially when checking water -int BOTLIB_UTIL_BOXCONTENTS(vec3_t origin) -{ - trace_t tr = gi.trace(origin, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), origin, NULL, MASK_ALL); - return tr.contents; -} - -// Rotate the center of an edge around a center point, then extend the center point out by a distance -// -// (end) <--- desired result -// | -// | -// | <--- distance -// |\ -// | \ <-- rotated edge (90 degrees) -// | \ -// (pt1) -----+----- (pt2) -// (center) -// -// pt1 and pt2 are the start and end points of an edge -// float rotate the degrees of rotation -// float distance is the distance from the center point -// vec3_t end is the rotated end point -void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end) -{ - vec3_t center, angle, forward; // , right, offset; - - // print warning if distance is > 3000 - if (distance > 3000) - { - Com_Printf("%s WARNING: distance > 3000. Cannot rotate accurately.\n", __func__); - } - - // Find the middle between two points of an edge - LerpVector(pt1, pt2, 0.50, center); - - // Get direction vector - VectorSubtract(pt2, pt1, forward); - - // Normalize the direction vector - VectorNormalize(forward); - - // Get the angle of the direction vector - vectoangles(forward, angle); - - //Com_Printf("%s vectoangles()->angle[YAW] = %f\n", __func__, angle[YAW]); - - //if (direction == MOVE_LEFT) - // angles[1] += 90; - //else if (direction == MOVE_RIGHT) - // angles[1] -= 90; - //else if (direction == MOVE_BACK) - // angles[1] -= 180; - - // Rotation - //angle[PITCH] = 0; - angle[YAW] += rotate; // Rotation angle - //angle[ROLL] = 0; - - // Wrap around - //if (angle[YAW] >= 180) - // angle[YAW] = angle[YAW] - 360; - //else if (angle[YAW] <= -180) - // angle[YAW] = angle[YAW] + 360; - - //if (angle[YAW] > 360) - // angle[YAW] -= 360; - //else if (angle[YAW] < 0) - // angle[YAW] += 360; - - // Rotate the direction vector - AngleVectors(angle, forward, NULL, NULL); - - //VectorSet(offset, distance, 0, 0); - //G_ProjectSource(center, offset, forward, right, end); - - // Get the rotated end point and extend it out based on distance - VectorMA(center, distance, forward, end); - //VectorMA(center, distance, right, end); -} - -// Test for a 'trigger_hurt' entity -// Returns true if we're touching a 'trigger_hurt', otherwise false -qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin) -{ - trace_t tr; - - // Test point contents from -20 to -30 checking for lava or slime below the proposed origin - int contents; - for (int i = 0; i < 10; i++) - { - contents = gi.pointcontents(tv(origin[0], origin[1], origin[2] - (20+i) )); - if (contents & (CONTENTS_LAVA | CONTENTS_SLIME)) - return true; - } - - // Trace above center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it - tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), g_edicts, MASK_ALL); - if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) - { - //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); - if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) - { - //Com_Printf("%s +SKIPPING %s\n", __func__, tr.ent->classname); - return true; - } - - if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) - return true; - } - // Skip if we started inside of a solid - if (tr.startsolid) - { - /* - if (nmesh.face[f].drawflags) - { - if (nmesh.face[f].drawflags & SURF_SKY) - Com_Printf("%s START SOLID, drawflags: SURF_SKY\n", __func__, nmesh.face[f].drawflags); - else - Com_Printf("%s START SOLID, drawflags: %d\n", __func__, nmesh.face[f].drawflags); - } - */ - - return true; - } - // Trace below center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it - tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] - NODE_Z_HEIGHT), g_edicts, MASK_ALL); - if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) - { - //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); - if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) - { - //Com_Printf("%s -SKIPPING %s\n", __func__, tr.ent->classname); - return true; - } - if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) - return true; - } - - return false; -} - -// Turn selective map entities into solids, so we can trace them -// Used for ents like 'trigger_hurt' so we can avoid placing nodes on those surfaces -//solid_t trigger_solid[MAX_EDICTS]; -// make trigger_solid a malloc -int BOTLIB_MakeEntsSolid(solid_t* trigger_solid) -{ - if (trigger_solid == NULL) - { - return -1; - } - - int edict_num = 1 + game.maxclients; // skip worldclass & players - //int edict_cur = edict_num; - int edict_cur = 0; - edict_t* map_ents; - for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) - { - if (map_ents->inuse == false) - { - //edict_cur++; - continue; - } - - trigger_solid[edict_cur] = map_ents->solid; // keep a copy of the solid state of each ent - /* - SOLID_NOT, // no interaction with other objects - SOLID_TRIGGER, // only touch when inside, after moving - SOLID_BBOX, // touch on edge - SOLID_BSP // bsp clip, touch on edge - */ - /* - if (map_ents->solid == SOLID_NOT) - Com_Printf("%s [%i] SOLID_NOT classname: %s\n", __func__, edict_cur, map_ents->classname); - else if (map_ents->solid == SOLID_TRIGGER) - Com_Printf("%s [%i] SOLID_TRIGGER classname: %s\n", __func__, edict_cur, map_ents->classname); - else if (map_ents->solid == SOLID_BBOX) - Com_Printf("%s [%i] SOLID_BBOX classname: %s\n", __func__, edict_cur, map_ents->classname); - else if (map_ents->solid == SOLID_BSP) - Com_Printf("%s [%i] SOLID_BSP classname: %s\n", __func__, edict_cur, map_ents->classname); - else - Com_Printf("%s [%i] SOLID_??? classname: %s\n", __func__, edict_cur, map_ents->classname); - */ - - - - //if (strlen(map_ents->classname) > 1) - // Com_Printf("%s %s found\n", __func__, map_ents->classname); - - if (strcmp(map_ents->classname, "trigger_hurt") == 0) - { - map_ents->solid = SOLID_BBOX; - gi.linkentity(map_ents); // Relink ent because solidity changed - //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); - } - - //if (strcmp(map_ents->classname, "func_rotating") == 0) - //{ - //map_ents->solid = SOLID_BBOX; - //gi.linkentity(map_ents); // Relink ent because solidity changed - //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); - //} - - edict_cur++; - } - return edict_cur; -} -void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid) -{ - if (trigger_solid == NULL) - { - Com_Printf("%s trigger_solid is NULL\n", __func__); - return; - } - // Restore solid state of each ent - int edict_cur = 0; - edict_t* map_ents; - int edict_num = 1 + game.maxclients; // skip worldclass & players - for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) - { - if (map_ents->inuse == false) - { - //edict_cur++; - continue; - } - - map_ents->solid = trigger_solid[edict_cur]; // Restore the original solid state - gi.linkentity(map_ents); // Relink ent because solidity changed - - edict_cur++; - } - if (trigger_solid) - free(trigger_solid); -} - -// Add nodes at item locations -void BOTLIB_AddItemNodes() -{ - // =========================== - // Add nodes at item locations - // =========================== - int foundItems = 0; // How many items were found - vec3_t item_origin = { 0,0,0 }; - edict_t* map_ents; - int edict_num = 1 + game.maxclients; // skip worldclass & players - trace_t tr; - for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) - { - // Skip ents that are not in used and not an item - if (map_ents->inuse == false || map_ents->item == NULL) - continue; - - // Find the ground under the item - // Some ents drop from the sky, so we need to trace down to the ground to find their landing spot (tr.endpos) - tr = gi.trace(map_ents->s.origin, NULL, NULL, tv(map_ents->s.origin[0], map_ents->s.origin[1], -4096), g_edicts, MASK_PLAYERSOLID); - - // See if the item will fit (player size) on the ground - // Some ents are placed on shelfs (urban2) so we cannot place a node on the shelf, so look for a spot to place the node nearby - tr = gi.trace(tr.endpos, tv(-12, -12, 0), tv(12, 12, 56), tr.endpos, g_edicts, MASK_PLAYERSOLID); - - VectorCopy(tr.endpos, item_origin); // Update the origin - - // If it won't fit, then look for a spot nearby - if (tr.startsolid || tr.fraction < 1.0) - { - float offset = 30; // How far to offset from the original item location (30 allows just enough room to grab the item, but not too far away) - // This number '30' was picked because of the shelves on urban2. The bot needs to walk up to the shelf to grab the item. - -// Try 24 units forward (X + 24) - tr = gi.trace(tv(item_origin[0] + offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] + offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) - goto VIABLE_ITEM_NODE; // Success! - - // Try 24 units back (X - 24) - tr = gi.trace(tv(item_origin[0] - offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] - offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) - goto VIABLE_ITEM_NODE; // Success! - - // Try 24 units right (Y + 24) - tr = gi.trace(tv(item_origin[0], item_origin[1] + offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] + offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) - goto VIABLE_ITEM_NODE; // Success! - - // Try 24 units left (Y - 24) - tr = gi.trace(tv(item_origin[0], item_origin[1] - offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] - offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); - if (tr.startsolid == false && tr.fraction == 1.0) - goto VIABLE_ITEM_NODE; // Success! - - // If we get here, then we could not find a spot to place the node - Com_Printf("%s WARNING: could not add node for item: %s\n", __func__, map_ents->classname); - continue; // Skip adding a node for this item because we cannot reach it - - // We found a spot to place the node - VIABLE_ITEM_NODE: - VectorCopy(tr.endpos, item_origin); // Update the origin - tr = gi.trace(item_origin, NULL, NULL, tv(item_origin[0], item_origin[1], -4096), g_edicts, MASK_PLAYERSOLID); // Find the ground under the item - VectorCopy(tr.endpos, item_origin); // Update the origin again - } - - - item_origin[2] += NODE_Z_HEIGHT; // Raise the node up to standard height - - - Com_Printf("%s found node for item: %s [typeNum: %d]\n", __func__, map_ents->classname, map_ents->item->typeNum); - - // Weapons - if (strcmp(map_ents->classname, "weapon_Sniper") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "weapon_M4") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "weapon_MP5") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "weapon_M3") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "weapon_HC") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "weapon_Grenade") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - - // Ammo - else if (strcmp(map_ents->classname, "ammo_clip") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "ammo_mag") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "ammo_m4") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "ammo_m3") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - else if (strcmp(map_ents->classname, "ammo_sniper") == 0) - { - foundItems++; - DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); - } - } - if (foundItems == 0) - { - // TODO: Get map ents without needing deathmatch enabled - - Com_Printf("%s WARNING: bot navigation was generated without item nodes. Please ensure the map is generated in deathmatch mode.\n", __func__); - } -} - -// Find the reachability for each node -void BOTLIB_ProcesssReachabilities(void) -{ - vec3_t zero = { 0 }; - node_t* from_node; // From node - node_t* to_node; // To node - int s, result; - //qboolean found_ladder; - for (s = 0; s < numnodes; s++) // From node - { - if (nodes[s].inuse == false) continue; // Ignore nodes not in use - - //if (nodes[s].nodenum != INVALID) continue; - - // Get the next 'from' node - from_node = (unsorted_nodes + s); // From Node - - // Zero the link counter - from_node->num_links = 0; - - for (int o = 0; o < numnodes; o++) // To node - { - if (nodes[o].inuse == false) continue; // Ignore nodes not in use - //if (nodes[o].nodenum != INVALID) continue; - - if (s == o) // skip self - continue; - - to_node = (unsorted_nodes + o); // Node A - - result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); - - //TEST CODE -- s - //if (result == NODE_JUMPPAD || result == NODE_LADDER_UP || result == NODE_LADDER_DOWN) - // result = INVALID; - //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes - // result = INVALID; - //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) - // result = NODE_JUMPPAD; - //TEST CODE -- e - - if (BOTLIB_AddNodeLink(s, o, result, true)) - { - ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table - nmesh.total_reach++; - } - - /* - if (result != INVALID) - { - for (int lk = 0; lk < MAXLINKS; lk++) - { - if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link - { - from_node->links[lk].targetNodeType = result; // Set target node type - - // Add link - //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); - //break; - - - //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes - if (DC_AddNodeLink(NULL, s, o, zero, false, result, true)) - { - ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table - //if (from_node->num_links + 1 > MAXLINKS) Com_Printf("%s too many links added to node[%d]\n", __func__, s); - //from_node->num_links++; // Update number of links this node has - nmesh.total_reach++; - } - break; - } - } - //Com_Printf("%s node[%d] added %i links\n", __func__, s, nodes[s].num_links); - } - */ - } - } -} - -void BOTLIB_BSP_SURFACES(bsp_t* bsp) -{ - int f, e; - int nv = 0; // Current number of verts - mface_t* surf; - msurfedge_t* src_surfedge; - - // Copy data from the BSP tree - for (f = 0, surf = bsp->faces; f < bsp->numfaces; f++, surf++) - { - if (surf == NULL) - { - gi.dprintf("%s null faces found\n", __func__); - return; - } - - nmesh.face[f].drawflags = surf->drawflags; // Copy draw flags (DSURF_PLANEBACK, etc) - VectorCopy(surf->plane->normal, nmesh.face[f].normal); // Copy plane normal - nmesh.face[f].num_edges = surf->numsurfedges; // Copy total edges on this face - - // Don't exclude sky because some walkable surfaces are sky - //if (surf->drawflags & SURF_SKY) - // continue; - - // If surf plane is a flat floor - if (surf->plane->normal[2] == 1) - { - if (surf->drawflags & DSURF_PLANEBACK) // Skip flat roof surfaces - continue; - - nmesh.face[f].type = FACETYPE_FLOOR; // Perfectly flat - } - else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) - { - nmesh.face[f].type = FACETYPE_SLOPE; // Any climbable sloped surface - } - else //if (surf->plane->normal[2] == 0) // Wall with normal[2] == 0 means the wall is perfectly upright - nmesh.face[f].type = FACETYPE_WALL; // Anything too steep to climb becomes a wall (needs to yet be tested for CONTENTS_LADDER, this will be processed further down) - - - for (e = 0, src_surfedge = surf->firstsurfedge; e < surf->numsurfedges; e++, src_surfedge++) - { - // Copy edge verts - VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[0]].point, nmesh.face[f].edge[e].v[0]); - VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[1]].point, nmesh.face[f].edge[e].v[1]); - - - // Copy face verts - nv = nmesh.face[f].num_verts; - VectorCopy(nmesh.face[f].edge[e].v[0], nmesh.face[f].v[nv]); - VectorCopy(nmesh.face[f].edge[e].v[1], nmesh.face[f].v[nv + 1]); - nmesh.face[f].num_verts += 2; - } - - //f = ++nmesh.total_faces; // Total faces - nmesh.total_faces++; // Total faces - - - nmesh.total_verts += (surf->numsurfedges * 2); // Total verts - nmesh.total_edges += surf->numsurfedges; // Total edges - } -} - -void BOTLIB_Process_NMesh(edict_t* ent) -{ - trace_t tr; - - // Player when standing - vec3_t player_standing_mins = { -16, -16, -24 }; - vec3_t player_standing_maxs = { 16, 16, 32 }; - // Player when crouching/ducking - vec3_t player_crouching_mins = { -16, -16, -24 }; - vec3_t player_crouching_maxs = { 16, 16, 4 }; - // Player when standing - vec3_t player_standing_smaller_mins = { -14, -14, -24 }; - vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; - - const int MIN_STEP_WIDTH = 32; // min step width - qboolean hit_step = false; // If we hit a suspected step (not 100% sure) - qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) - - vec3_t mid_point = { 0 }; - vec3_t ladder_bottom = { 0 }; - vec3_t ladder_top = { 0 }; - - float tmp = 0; - int f, e; - - float normal_0, normal_1, normal_2; - int num_tris = 0; - int vert_count = 0; // Vertex count - int total_verts = 0; - vec3_t pt0 = { 0 }; // Point 1 of a triangle - vec3_t pt1 = { 0 }; // Point 2 of a triangle - vec3_t pt2 = { 0 }; // Point 3 of a triangle - vec3_t center = { 0 }; // Center point of either an edge or a triangle - - // Proccess the mesh data - for (f = 0; f < nmesh.total_faces; f++) - { - normal_0 = nmesh.face[f].normal[0]; - normal_1 = nmesh.face[f].normal[1]; - normal_2 = nmesh.face[f].normal[2]; - - num_tris = 0; - vert_count = 0; - total_verts = nmesh.face[f].num_verts; - memset(pt0, 0, sizeof(vec3_t)); - memset(pt1, 0, sizeof(vec3_t)); - memset(pt2, 0, sizeof(vec3_t)); - memset(center, 0, sizeof(vec3_t)); - - // Edges - (only the outside edges of a face) - //----------------------------------------------------- - for (e = 0; e < nmesh.face[f].num_edges; e++) - { - // Copy the edge vectors - VectorCopy(nmesh.face[f].edge[e].v[0], pt1); - VectorCopy(nmesh.face[f].edge[e].v[1], pt2); - - // Get the distance between pt1 and pt2 - vec3_t pt1_to_pt2_vec; - VectorSubtract(pt2, pt1, pt1_to_pt2_vec); // Direction vector from pt1 to pt2 - float pt1_to_pt2_dist = VectorLength(pt1_to_pt2_vec); // Distance between pt1 and pt2 - - // Skip excessively small edges - if (pt1_to_pt2_vec < 4) - { - nmesh.face[f].edge[e].node = INVALID; - continue; - } - - // Get the normalized node height based on the face type -- floor, slope, or wall - vec3_t adjusted_height = { 0 }; - if (nmesh.face[f].type == FACETYPE_FLOOR) - adjusted_height[2] += NODE_Z_HEIGHT; // Move the center so it is somewhat above the ground, instead of being stuck inside the ground - else if (nmesh.face[f].type == FACETYPE_SLOPE) - adjusted_height[2] += (NODE_Z_HEIGHT * normal_2); // Calculate the correct height based on the normal of the slope - else if (nmesh.face[f].type == FACETYPE_WALL) // Push out from wall - { - adjusted_height[2] += NODE_Z_HEIGHT; // Move up from ground (note this isn't using the normal direction, its directly up from the ground) - - // Calculate the correct wall distance based on the normal angle - if (nmesh.face[f].drawflags & DSURF_PLANEBACK) - { - if (normal_0 != 0) - adjusted_height[0] -= (NODE_Z_HEIGHT * normal_0); // -X (forward wall) - if (normal_1 != 0) - adjusted_height[1] -= (NODE_Z_HEIGHT * normal_1); // -Y (left wall) - } - else - { - if (normal_0 != 0) - adjusted_height[0] += (NODE_Z_HEIGHT * normal_0); // +X (back wall) - if (normal_1 != 0) - adjusted_height[1] += (NODE_Z_HEIGHT * normal_1); // +Y (right wall) - } - } - - // Get the center of the edge - LerpVector(pt1, pt2, 0.50, center); - - // Record it - VectorCopy(center, nmesh.face[f].edge[e].center); // Copy the center of the edge - - // Apply the adjusted height - nmesh.face[f].edge[e].center[0] += adjusted_height[0]; - nmesh.face[f].edge[e].center[1] += adjusted_height[1]; - nmesh.face[f].edge[e].center[2] += adjusted_height[2]; - - - - - - // Test for edges - // Some edges are on a floor, others from a wall, therefore we test both here - if (0) - { - qboolean hit_step_left = false, hit_step_right = false; - vec3_t end_left, end_right; - - - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1, end_left); // Turn 90 degrees left and move 1 unit - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1, end_right); // Turn 90 degrees right and move 1 unit - - // Test just above center to see if we're in a solid - trace_t tr_edge_center = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] + 1), ent, MASK_PLAYERSOLID); - if (tr_edge_center.startsolid == false && tr_edge_center.fraction == 1.0) // Not in a solid (usually a wall) - { - // Test if we're on an edge with a dropoff - trace_t tr_edge_left = gi.trace(end_left, NULL, NULL, tv(end_left[0], end_left[1], end_left[2] - 1), ent, MASK_PLAYERSOLID); - trace_t tr_edge_right = gi.trace(end_right, NULL, NULL, tv(end_right[0], end_right[1], end_right[2] - 1), ent, MASK_PLAYERSOLID); - if (tr_edge_left.startsolid == false && tr_edge_left.fraction == 1.0) - hit_step_left = true; - if (tr_edge_right.startsolid == false && tr_edge_right.fraction == 1.0) - hit_step_right = true; - - //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); - - if (hit_step_left && hit_step_right) // A double edge on either side - { - //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); - } - else if (hit_step_left || hit_step_right) // A single edge -- ledge / stairs - { - //if (BOTLIB_UTIL_NearbyNodeAtHeightDist(center, NODE_SIZE) == INVALID) - if (1) - { - //Com_Printf("%s f:%i e:%i node:%d hit_step_left:%i hit_step_right:%i\n", __func__, f, e, numnodes, hit_step_left, hit_step_right); - - //if (BOTLIB_TestForNodeDist(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), 2) == INVALID) - DAIC_Add_Node(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), nmesh.face[f].normal, NODE_STEP); - - // Expand nodes from the top and bottom landing steps. Try adding a node 90 degrees left and -90 degrees right. - // We try both directions because the top step is usually flat, and the landing step has a dropoff. - // This will help the bot get around corners, and connect the top and bottom of stairs to the node network - // - // [left node] [sn_1] - // -------------------------------------------- - // | | [sn_2] - // | ---------- - // | sn_x = step node | [sn_3] - // | left node = move node ---------- - // | right node = move node | [right node] - // | -------------------------------- - // The [left node] is added to the left of [sn_1] - // The [right node] is added to the right of [sn_3] - // - if (1) - { - vec3_t end; - - // Set player waist height - const int player_waist_height = fabs(player_standing_mins[2]); // 24 units - - if (1) // Left step - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 32, end); //2048 - - // Lift the end points up a bit so they don't get stuck in the ground - end[2] += player_waist_height + 1; - - // Test to see if end is in a solid - trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); - if (tr_end_step.startsolid == false) - { - // Test to see if end is touching the ground - trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); - - // If the end is touching the ground, then we can add a node there - if (tr_end_ground.fraction < 1.0) - { - // Bring height up to NODE_Z_HEIGHT - tr_end_ground.endpos[2] += NODE_Z_HEIGHT; - - if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16,-16,-16), tv(-16, -16, -16)) == INVALID) - DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); - } - } - } - - if (1) // Right step - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 32, end); //2048 - - // Lift the end points up a bit so they don't get stuck in the ground - end[2] += player_waist_height + 1; - - // Test to see if end is in a solid - trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); - if (tr_end_step.startsolid == false) - { - // Test to see if end is touching the ground - trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); - - // If the end is touching the ground, then we can add a node there - if (tr_end_ground.fraction < 1.0) - { - // Bring height up to NODE_Z_HEIGHT - tr_end_ground.endpos[2] += NODE_Z_HEIGHT; - - if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) - DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); - } - } - } - } - } - } - else // Floor edge (no dropoff on either side -- the floor is a flat plane) - { - // Expand additional nodes from the center of the edge. - // Heading forward, back, left, right. - if (1) - { - for (int d = 0; d < 4; d++) - { - vec3_t end; - - if (d == 0) // forwards - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 0, 1024, end); // Forward 0 degrees - //VectorCopy(pt1, end); - //end[2] += 1; - } - else if (d == 1) // backwards - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 180, 1024, end); // Backwards 180 or -180 degrees - //VectorCopy(pt2, end); - //end[2] += 1; - } - else if (d == 2) // left - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1024, end); // Left (pt1 -> pt2 -> 90 degrees) - } - else if (d == 3) // right - { - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1024, end); // Right (pt1 -> pt2 -> -90 degrees) - } - - // Get player waist height - const int player_waist_height = fabs(player_standing_mins[2]); // 24 units - - // Lift the end points up a bit so they don't get stuck in the ground - end[2] += player_waist_height + 1; - - // Test to see where end meets a wall - // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right - trace_t tr_end_wall = gi.trace(tv(center[0], center[1], center[2] + player_waist_height), player_standing_mins, player_standing_maxs, end, ent, MASK_PLAYERSOLID); - - - // Test to see if end is touching the ground - // From the wall hit down to the ground which is -(player_waist_height + 1) - trace_t tr_end_ground = gi.trace(tr_end_wall.endpos, NULL, NULL, tv(tr_end_wall.endpos[0], tr_end_wall.endpos[1], tr_end_wall.endpos[2] - (player_waist_height + 1)), ent, MASK_PLAYERSOLID); - - - // If the end is touching the ground, then we can add a node there - if (tr_end_ground.fraction < 1.0) - { - if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE * 4, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) - { - // Bring height up to NODE_Z_HEIGHT - tr_end_ground.endpos[2] += NODE_Z_HEIGHT; - - DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); - } - } - } - } - } - } - } - - - // Skip wall edges - if (nmesh.face[f].type == FACETYPE_WALL) - { - nmesh.face[f].edge[e].node = INVALID; - continue; - } - - - - - - // Find the middle between two points of an edge - //LerpVector(pt1, pt2, 0.50, center); // 50% (center) - - - // Ignore edges that are up in the air - // Trace just below the floor, if we didn't hit the ground, then the node is up in the air. We want to ignore these because some are 'trigger' and other ents - //if (nmesh.face[f].type == FACETYPE_FLOOR || nmesh.face[f].type == FACETYPE_SLOPE) - { - //tr = gi.trace(center, tv(-16, -16, -(NODE_Z_HEIGHT + 1)), tv(16, 16, 24), center, ent, MASK_SOLID); - // move off ground slightly, then line trace back to ground, if we don't hit ground, then we're in the air - tr = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] - 1), ent, MASK_PLAYERSOLID); - //tr = gi.trace(center, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), center, ent, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - //Com_Printf("%s face:%i HIT failed to find ground\n", __func__, f); - nmesh.face[f].edge[e].node = INVALID; - continue; - } - } - - // Check for trigger_hurt - if (BOTLIB_UTIL_CHECK_FOR_HURT(center)) - { - //Com_Printf("%s face:%i HIT trigger_hurt\n", __func__, f); - nmesh.face[f].edge[e].node = INVALID; - continue; - } - - // Depending on the edge tested (within the same face) we may encounter different multiple groups of contents, - // therefore we need to add them all together. This is because edge can be shared within the same face, or other faces. - int box_contents = BOTLIB_UTIL_BOXCONTENTS(center); - if (nmesh.face[f].contents > 0 && box_contents > 0 && box_contents != nmesh.face[f].contents) - { - // before - //Com_Printf("%s face:%i tris:%i contents:0x%x contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents, tr.contents); - // - // Add tr.contents to nmesh.face[f].contents - // - // 0x18000002 - // + 0x08030000 - // ---------- - // = 0x18030002 - // - nmesh.face[f].contents |= box_contents; - // - // after - //Com_Printf("%s face:%i tris:%i contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); - } - // Add the first contents found - if (nmesh.face[f].contents == 0) - nmesh.face[f].contents = box_contents; - - // Copy it back - VectorCopy(nmesh.face[f].edge[e].center, center); - - // Test for steps / edges -#if 0 - if (0) - { - - // Check if trace hit a step - STEPSIZE - // - // e1 = edge 1 - // e2 = edge 2 - // c = center of bounding box - // - // roof ------------------------------- - // - // ____________ - // | | - // | player | - // | bounding | - // | box | - // | | - // -----|------ - // c - // - // floor ___________e1 (step) - // | - // |___e2 (step) - // | - // | - // ------------- floor - // - // The center of the players bounding box will fit on (e1), but might not on (e2) as it could overlap against (e1) - // We do the same floor trace as normal, except move the bounding box upward by STEPSIZE - // and check if player will fit, as if they were standing on the step while crouching. This is because - // a player can crouch walk up stairs, thereby covering normal + edge cases. - // - - hit_step = false; // If we hit a suspected step (not 100% sure) - hit_ledge = false; // If we hit a suspected ledge (not 100% sure) - - - vec3_t dist; - VectorSubtract(pt1, pt2, dist); // vector from pt1 to pt2 - dist[2] = 0; // remove height - vec3_t center_step; - VectorCopy(center, center_step); - center_step[2] -= (adjusted_height[2] - 0.1); - trace_t tr_edge = gi.trace(center_step, tv(-16, -16, -24), tv(16, 16, 32), center_step, ent, MASK_PLAYERSOLID); - if (tr_edge.startsolid && tr_edge.fraction < 1 && VectorLength(dist) > MIN_STEP_WIDTH/*calc step width*/) // inside step and we're larger than min step width - { - //* - //vec3_t center_step; - //VectorCopy(center, center_step); - //center_step[2] += STEPSIZE; - //center_step[2] += adjusted_height[2]; - center_step[2] += STEPSIZE + 0.1; - - //trace_t tr_step = gi.trace(center_step, player_crouching_mins, player_crouching_maxs, center_step, ent, MASK_DEADSOLID); - trace_t tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), center_step, ent, MASK_DEADSOLID); - //trace_t tr_step = gi.trace(center_step, tv(-0.1, -0.1, 0), tv(0.1, 0.1, 0), center_step, ent, MASK_DEADSOLID); - //trace_t tr_step = gi.trace(center_step, NULL, NULL, center_step, ent, MASK_DEADSOLID); - if (!tr_step.startsolid && tr_step.fraction == 1) - { - // Find the floor - tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), tv(center_step[0], center_step[1], center_step[2] - (STEPSIZE + 1)), ent, MASK_DEADSOLID); - tr_step.endpos[2] += adjusted_height[2]; - - if (BOTLIB_UTIL_NearbyNodeAtHeightDist(tr_step.endpos, NODE_SIZE) == INVALID) - { - DAIC_Add_Node(tr_step.endpos, nmesh.face[f].normal, NODE_STEP); - } - //hit_step = true; - //VectorCopy(center_step, center); // increase node z loc from NODE_Z_HEIGHT to (NODE_Z_HEIGHT + STEPSIZE) - //Com_Printf("%s f:%d e:%d [center_step %f %f %f] [center %f %f %f]\n", __func__, f, e, center_step[0], center_step[1], center_step[2], center[0], center[1], center[2]); - - } - //*/ - - /* - // Try again but move the bounding box slightly, this works for small edges that go along a wall (urban, urban2, etc) - // Move the bounding box to each corner, forward/back/right/left. Then trace again. - trace_t tr_edge; - vec3_t center_edge; - - // forward - if (hit_step == false) - { - VectorCopy(center, center_edge); - center_edge[0] += 16; // move node forward - tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); - if (!tr_edge.startsolid && tr_edge.fraction == 1) - { - hit_ledge = true; - VectorCopy(center_edge, center); - } - } - - // back - if (hit_ledge == false) - { - VectorCopy(center, center_edge); - center_edge[0] -= 16; // move node back - tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); - if (!tr_edge.startsolid && tr_edge.fraction == 1) - { - hit_ledge = true; - VectorCopy(center_edge, center); - } - } - - // right - if (hit_ledge == false) - { - VectorCopy(center, center_edge); - center_edge[1] += 16; // move node right - tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); - if (!tr_edge.startsolid && tr_edge.fraction == 1) - { - hit_ledge = true; - VectorCopy(center_edge, center); - } - } - - // left - if (hit_ledge == false) - { - VectorCopy(center, center_edge); - center_edge[1] -= 16; // move node left - tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); - if (!tr_edge.startsolid && tr_edge.fraction == 1) - { - hit_ledge = true; - VectorCopy(center_edge, center); - } - } - */ - } - } -#endif - - //if (nmesh.face[f].type == FACETYPE_WALL && (nmesh.face[f].contents & CONTENTS_LADDER) == 0) - - - if (nmesh.face[f].type == FACETYPE_SLOPE) - tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_PLAYERSOLID); - else // floor - tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_PLAYERSOLID); - - - - //tr = gi.trace(center, tv(-8, -8, -20), tv(8, 8, 0), center, ent, MASK_SOLID); - if ((!tr.startsolid && tr.fraction == 1))// || hit_step || hit_ledge) - { - //if (tr.contents < CONTENTS_AREAPORTAL || (tr.contents & CONTENTS_LADDER) == 0) // Look for visible brushes, including non-solid such as liquids, also ladders - { - if (BOTLIB_TestForNodeDist(center, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) // || hit_step || hit_ledge) // hit_step and hit_ledge ignore node size (for now) - { - // Test again, but just for MASK_WATER - if (nmesh.face[f].type == FACETYPE_SLOPE) - tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_WATER); - else - tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_WATER); - - if ((nmesh.face[f].contents & CONTENTS_LAVA) || (nmesh.face[f].contents & CONTENTS_SLIME)) - continue; - else if ((tr.contents & CONTENTS_WATER) || nmesh.face[f].contents & CONTENTS_WATER) - //continue; // Don't add edge water nodes - DAIC_Add_Node(center, nmesh.face[f].normal, NODE_WATER); - else - { - //if (hit_step) // hit_step - // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); - //else if (hit_ledge) - // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); - //else - DAIC_Add_Node(center, nmesh.face[f].normal, NODE_MOVE); - } - - //if (nmesh.face[f].edge[e].node) - // Com_Printf("WARNING: edge node already exists face:%d edge:%d node:%d\n", f, e, nmesh.face[f].edge[e].node); - - nmesh.face[f].edge[e].node = numnodes - 1; - - - // Expand additional nodes from the center of the edge, one heading 90 degrees left, and one heading 90 degrees right. - if (0 && nmesh.face[f].type == FACETYPE_FLOOR) - { - vec3_t end_left, end_right; - - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 2000, end_left); - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 270, 2000, end_right); - - // Lift the end points up a bit so they don't get stuck in the ground - end_left[2] += 1; - end_right[2] += 1; - - // Find the middle between two points of an edge - LerpVector(pt1, pt2, 0.50, center); - //center[2] += NODE_Z_HEIGHT; - //center[2] += 25; - - // Test to see where the left and right meet a wall - // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right - trace_t tr_left_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_left, ent, MASK_PLAYERSOLID); - trace_t tr_right_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_right, ent, MASK_PLAYERSOLID); - - //DAIC_Add_Node(tr_left_wall.endpos, tr_left_wall.plane.normal, NODE_MOVE); - //DAIC_Add_Node(tr_right_wall.endpos, tr_right_wall.plane.normal, NODE_MOVE); - - // Test to see if left and right are touching the ground - //trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); - //trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); - trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); - trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); - - - - // If the left is touching the ground, then we can add a node there - //if (tr_left_ground.fraction < 1.0) - { - tr_left_wall.endpos[2] += (NODE_Z_HEIGHT - 1); - - //if (BOTLIB_TestForNodeDist(tr_left_wall.endpos, NODE_SIZE) == INVALID) - DAIC_Add_Node(tr_left_wall.endpos, tr_left_ground.plane.normal, NODE_MOVE); - } - // If the right is touching the ground, then we can add a node there - //if (tr_right_ground.fraction < 1.0) - { - tr_right_wall.endpos[2] += (NODE_Z_HEIGHT - 1); - - //if (BOTLIB_TestForNodeDist(tr_right_wall.endpos, NODE_SIZE) == INVALID) - DAIC_Add_Node(tr_right_wall.endpos, tr_right_ground.plane.normal, NODE_MOVE); - } - } - - - - } - } - } - } - //----------------------------------------------------- - } -} - -#define BOTLIB_OrthogonalToVectors(v1, v2, res) \ - (res)[0] = ((v1)[1] * (v2)[2]) - ((v1)[2] * (v2)[1]);\ - (res)[1] = ((v1)[2] * (v2)[0]) - ((v1)[0] * (v2)[2]);\ - (res)[2] = ((v1)[0] * (v2)[1]) - ((v1)[1] * (v2)[0]); - -// Verts must be unwound and in clockwise order -qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t normal, float epsilon) -{ - vec3_t v1, v2; - vec3_t edgevec, pointvec, sepnormal; - - for (int v = 0; v < num_verts; v += 2) // edges - { - VectorCopy(verts[v + 0], v1); - VectorCopy(verts[v + 1], v2); - //edge vector - VectorSubtract(v2, v1, edgevec); - //vector from first edge point to point possible in face - VectorSubtract(point, v1, pointvec); - //get a vector pointing inside the face orthogonal to both the - //edge vector and the normal vector of the plane the face is in - //this vector defines a plane through the origin (first vertex of - //edge) and through both the edge vector and the normal vector - //of the plane - BOTLIB_OrthogonalToVectors(edgevec, normal, sepnormal); - //check on which side of the above plane the point is - //this is done by checking the sign of the dot product of the - //vector orthogonal vector from above and the vector from the - //origin (first vertex of edge) to the point - //if the dotproduct is smaller than zero the point is outside the face - if (DotProduct(pointvec, sepnormal) < -epsilon) return false; - } - - return true; // Found point -} - -void BOTLIB_InitNavigation(edict_t* ent) -{ - bsp_t* bsp = gi.Bsp(); - if (bsp == NULL) - { - gi.dprintf("%s failed to import BSP data\n", __func__); - return; - } - - if (bsp->checksum == 0) - { - gi.dprintf("%s bsp->checksum is zero\n", __func__); - return; - } - - if (ent == NULL) - { - ent = g_edicts; - if (ent == NULL) - return; - } - - BOTLIB_InitNodes(); - - nmesh.bsp_checksum = bsp->checksum; // Save the map checksum - - Remove_All_Doors(); // Make sure all doors are open before making any nodes -} - -void ACEND_BSP(edict_t* ent) -{ - bsp_t* bsp = gi.Bsp(); - if (bsp == NULL) - { - gi.dprintf("%s failed to import BSP data\n", __func__); - return; - } - - if (ent == NULL) - { - ent = g_edicts; - if (ent == NULL) - return; - } - - clock_t clock_begin = clock(); // Start performance clock - - BOTLIB_InitNodes(); - nmesh.bsp_checksum = bsp->checksum; // Save the map checksum - - Remove_All_Doors(); // Make sure all doors are open before making any nodes - - solid_t *trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); - if (trigger_solid == NULL) - { - Com_Printf("%s failed to malloc trigger_solid\n", __func__); - return; - } - if (BOTLIB_MakeEntsSolid(trigger_solid) == -1) - return; - BOTLIB_AddItemNodes(); // Add nodes at item locations - - BOTLIB_BSP_SURFACES(bsp); - - //BOTLIB_Process_NMesh(ent); - - if (1) - { - ent->nav = gi.Nav(); // Grant access to navigation data - if (ent->nav) - { - for (int f = 0; f < ent->nav->faces_total; f++) - { - if (ent->nav->surface_data_faces[f].face_type != FACETYPE_WALK && ent->nav->surface_data_faces[f].face_type != FACETYPE_TINYWALK) continue; - - /* - for (int e = 0; e < ent->nav->surface_data_faces[f].num_verts; e += 2) // Edges - { - // Get center of edge - vec3_t center; - VectorAdd(ent->nav->surface_data_faces[f].verts[e], ent->nav->surface_data_faces[f].verts[e + 1], center); - VectorScale(center, 0.5, center); - - // Move center toward face center by 4 units - vec3_t dir; - VectorSubtract(ent->nav->surface_data_faces[f].center_poly, center, dir); - VectorNormalize(dir); - VectorMA(center, 8, dir, center); - - // Check if we're still in the same face - //BOTLIB_InsideFace - - // Move center up by 32 units - center[2] += 32; - - // Test to see if the center is valid - if (BOTLIB_TestForNodeDist(center, NODE_SIZE) == INVALID) - { - // Add the node - DAIC_Add_Node(center, ent->nav->surface_data_faces[f].normal, NODE_MOVE); - } - } - */ - - - for (int c = 0; c < ent->nav->surface_data_faces[f].snode_counter; c++) - { - vec3_t pos; - VectorCopy(ent->nav->surface_data_faces[f].snodes[c].start, pos); - pos[2] += NODE_Z_HEIGHT; - if (BOTLIB_TestForNodeDist(pos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) - DAIC_Add_Node(pos, ent->nav->surface_data_faces[f].normal, NODE_MOVE); - } - } - } - } - - // Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER - ACEND_FindEdgeLadders(); - - // Find the reachability for each node - BOTLIB_ProcesssReachabilities(); - - // Try placing POI nodes on high locations - if (0) - { - /* - float highest_origin = -9999; - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].origin[2] > highest_origin) - highest_origin = nodes[i].origin[2]; - } - float above_height = (highest_origin - 256); - - for (int i = 0; i < numnodes; i++) - { - if (nodes[i].origin[2] > above_height)// && random() < 0.1) - nodes[i].type = NODE_POI; - } - */ - - //ACEND_BuildSpawnPointNodes(); - //ACEND_CachePointOfInterestNodes(); - } - - // Restore solid state of each ent - BOTLIB_RestoreEntsSolidState(trigger_solid); - - - - gi.dprintf("BSP f[%i] t[%i] e[%i] v[%i] r[%i] n[%i]\n", nmesh.total_faces, nmesh.total_tris, nmesh.total_edges, nmesh.total_verts, nmesh.total_reach, numnodes); - clock_t clock_end = clock(); - Com_Printf("%s execution took %f seconds to process bsp reachability\n", __func__, (double)(clock_end - clock_begin) / CLOCKS_PER_SEC); - - //ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies - // Free the memory - if (unsorted_nodes) - { - free(unsorted_nodes); - unsorted_nodes = NULL; // Nullify dangling pointer - } -} +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +int num_poi_nodes; +int poi_nodes[MAX_POI_NODES]; +edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +int num_vis_nodes; +int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +int node_vis_list[10][10]; // Cached node +node_t *nodes; +nmesh_t nmesh; + +// Free nodes +void BOTLIB_FreeNodes(void) +{ + if (nodes) + { + Com_Printf("%s --- Freeing nodes ---\n", __func__); + free(nodes); + nodes = NULL; // Nullify dangling pointer + } + + if (path_table) + { + Com_Printf("%s --- Freeing path_table ---\n", __func__); + for (int r = 0; r < MAX_PNODES; r++) { + free(path_table[r]); + path_table[r] = NULL; // Nullify dangling pointer + } + + free(path_table); + path_table = NULL; // Nullify dangling pointer + } +} + + +// Adds a new or inactive 'inuse == false' flagged node (from deleting a node) +// Returns the node that was added/reused, INVALID if node was not added +int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type) +{ + // ================================================================================= + // Avoid placing nodes on 'hurt_touch' trigger areas + // ================================================================================= + if (0) + { + solid_t* trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); + if (trigger_solid == NULL) + { + Com_Printf("%s failed to malloc trigger_solid\n", __func__); + return INVALID; + } + BOTLIB_MakeEntsSolid(trigger_solid); // Save solid state of each ent + if (BOTLIB_UTIL_CHECK_FOR_HURT(origin)) + { + Com_Printf("%s ignoring hurt_touch\n", __func__); + return INVALID; + } + BOTLIB_RestoreEntsSolidState(trigger_solid); // Restore solid state, and free memory + } + // ================================================================================= + + int node = INVALID; + // Check for any unused nodes first + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) + { + node = i; // Free to use this node + break; + } + } + + if (node == INVALID) + { + // Absolute max allowed + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); + return INVALID; + } + + // ------------- + // Alloc memory + // ------------- + node_t* prev = NULL; + + // Nodes + if (numnodes == 0) + nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = nodes; // Keep a copy + nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); + } + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because nodes is null + nodes = NULL; + prev = NULL; + } + return INVALID; + } + + node = numnodes; + } + + // ------------------------------------------------------------------------------------- + // nodes + // ------------------------------------------------------------------------------------- + nodes[node].area = 0; // Area + nodes[node].weight = 0; // Weight + + VectorCopy(origin, nodes[node].origin); // Location + + //VectorCopy(origin, nodes[node].origin); //TODO::::::::::::::::::::::: Angles (for ladders, sniper spots, etc) + + VectorCopy(normal, nodes[node].normal); // Surface Normal + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + VectorCopy(mins, nodes[node].mins); // Box mins + VectorCopy(maxs, nodes[node].maxs); // Box maxs + if (type == NODE_CROUCH) + nodes[node].maxs[2] = CROUCHING_MAXS2; + if (type == NODE_BOXJUMP) + { + nodes[node].mins[0] = -8; + nodes[node].mins[1] = -8; + nodes[node].mins[2] = -12; + nodes[node].maxs[0] = 8; + nodes[node].maxs[1] = 8; + nodes[node].maxs[2] = 16; + } + VectorAdd(origin, mins, nodes[node].absmin); // Update absolute box min/max in the world + VectorAdd(origin, maxs, nodes[node].absmax); // Update absolute box min/max in the world + nodes[node].type = type; // Node type + nodes[node].nodenum = node; // Node number + nodes[node].inuse = true; // Node is inuse + nodes[node].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + nodes[node].links[i].targetNode = INVALID; // Link + nodes[node].links[i].targetNodeType = INVALID; // Type + nodes[node].links[i].cost = INVALID; // Cost + } + + if (node == numnodes) + numnodes++; // Update the node counter + + return node; +} + +/////////////////////////////////////////////////////////////////////// +// Nodes added by the new navigation system +/////////////////////////////////////////////////////////////////////// +qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type) +{ + // Absolute max allowed + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_PNODES); + return false; + } + + // ------------- + // Alloc memory + // ------------- + node_t* prev = NULL; + + // Nodes + if (numnodes == 0) + nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = nodes; // Keep a copy + nodes = (node_t*)realloc(nodes, sizeof(node_t) * (numnodes + 1)); + } + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because nodes is null + nodes = NULL; + prev = NULL; + } + return false; + } + + // Unsorted duplicate copy nodes + if (numnodes == 0) + unsorted_nodes = (node_t*)malloc(sizeof(node_t)); + else + { + prev = unsorted_nodes; // Keep a copy + unsorted_nodes = (node_t*)realloc(unsorted_nodes, sizeof(node_t) * (numnodes + 1)); + } + if (unsorted_nodes == NULL) + { + Com_Printf("%s failed to malloc unsorted_nodes. Out of memory!\n", __func__); + if (prev) + { + free(prev); // Free using the copy, because unsorted_nodes is null + unsorted_nodes = NULL; + prev = NULL; + } + return false; + } + + // ------------------------------------------------------------------------------------- + // nodes + // ------------------------------------------------------------------------------------- + VectorCopy(origin, nodes[numnodes].origin); // Location + VectorCopy(normal, nodes[numnodes].normal); // Surface Normal + nodes[numnodes].type = type; // Node type + nodes[numnodes].nodenum = numnodes; // Node number + nodes[numnodes].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + nodes[numnodes].links[i].targetNode = INVALID; // Link + nodes[numnodes].links[i].targetNodeType = INVALID; // Type + nodes[numnodes].links[i].cost = INVALID; // Cost + } + + + // ------------------------------------------------------------------------------------- + // unsorted_nodes + // ------------------------------------------------------------------------------------- + // Make a duplicate copy of nodes into unsorted_nodes. We'll use this later to optimise links + VectorCopy(origin, unsorted_nodes[numnodes].origin); // Location + VectorCopy(normal, unsorted_nodes[numnodes].normal); // Surface Normal + unsorted_nodes[numnodes].type = type; // Node type + unsorted_nodes[numnodes].nodenum = numnodes; // Node number + unsorted_nodes[numnodes].num_links = 0; // Node links + for (int i = 0; i < MAXLINKS; i++) // Init links + { + unsorted_nodes[numnodes].links[i].targetNode = INVALID; // Link + unsorted_nodes[numnodes].links[i].targetNodeType = INVALID; // Type + unsorted_nodes[numnodes].links[i].cost = INVALID; // Cost + } + + //if ((numnodes % 1024) == 0) + //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, nodes[numnodes].nodenum, nodes[numnodes].origin[0], nodes[numnodes].origin[1], nodes[numnodes].origin[2]); + + numnodes++; // Update the node counter + + return true; +} + +/* +/////////////////////////////////////////////////////////////////////// +// Makes a duplicate copy into the unsorted_nodes[] array +/////////////////////////////////////////////////////////////////////// +void ACEND_DuplicateNodes(int node_num) +{ + node_t* unsorted_node = (unsorted_nodes + node_num); // Get our node by pointer arithmatic + + VectorCopy(nodes[node_num].origin, unsorted_node->origin); // Location + VectorCopy(nodes[node_num].normal, unsorted_node->normal); // Surface Normal + unsorted_node->type = nodes[node_num].type; // Node type + unsorted_node->nodenum = nodes[node_num].nodenum; // Node number + + for (int i = 0; i < MAXLINKS; i++) // Init links + { + unsorted_node->links[i].targetNode = INVALID; // Invalidate all links + unsorted_node->links[i].cost = INVALID; // Invalidate all costs + } +} +void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type) +{ + // Sanity check + if (numnodes + 1 > MAX_NODES) + { + debug_printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_NODES); + return; + } + + //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, numnodes, origin[0], origin[1], origin[2]); + + VectorCopy(origin, nodes[numnodes].origin); // Location + VectorCopy(normal, nodes[numnodes].normal); // Surface Normal + nodes[numnodes].type = type; // Node type + nodes[numnodes].nodenum = numnodes; // Node number + + //node_ents[numnodes] = self; // Add door as an entity reference //rekkie -- DEV_1 + + // Init links + for (int i = 0; i < MAXLINKS; i++) + { + nodes[numnodes].links[i].targetNode = INVALID; // Link + nodes[numnodes].links[i].targetNodeType = INVALID; // Type + memset(nodes[numnodes].links[i].targetVelocity, 0, sizeof(vec3_t)); // Velocity + nodes[numnodes].links[i].cost = INVALID; // Cost + } + + // Make a duplicate copy of nodes[] to *unsorted_nodes + // We'll use this later to optimise links + ACEND_DuplicateNodes(numnodes); + + // Show ever 10nth node + //if (numnodes % 10 == 0) + // ACEND_ShowNode(numnodes); + + numnodes++; // Update the node counter +} +*/ + +/////////////////////////////////////////////////////////////////////// +// Remove node by number, +// Shifts all the nodes down by one to replace the deleted node +/////////////////////////////////////////////////////////////////////// +void ACEND_RemoveNode(edict_t* self, int nodenum) +{ + int i, j, n, l; + + // Sanity + if (nodenum <= 0 || nodenum + 1 > MAX_PNODES) + return; + + // Remove links to/from this node + for (i = 0; i < MAXLINKS; i++) + { + // Remove link from other node to our node + n = nodes[nodenum].links[i].targetNode; // Links from this node to other nodes + if (n != INVALID) + { + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[n].links[j].targetNode == nodenum) // If the other node has a link back to our node + { + nodes[n].links[j].targetNode = INVALID; // Remove its link to us + } + } + } + + // Remove our link to other node + nodes[nodenum].links[i].targetNode = INVALID; + } + + // Update node ent + edict_t* ent; + for (ent = g_edicts; ent < &g_edicts[globals.num_edicts]; ent++) + { + // Free removed node ent + if (ent->node_num == nodenum) + { + G_FreeEdict(ent); + } + + // Update node name and number + if (ent->node_num > nodenum) + { + ent->node_num--; + + if (nodes[nodenum].type == NODE_MOVE) + Q_strncpyz(ent->node_name, va("node move [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_WATER) + Q_strncpyz(ent->node_name, va("node water [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_JUMPPAD) + Q_strncpyz(ent->node_name, va("node jumppad [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_SPAWNPOINT) + Q_strncpyz(ent->node_name, va("node spawn point [%d]", ent->node_num), sizeof(ent->node_name)); + else if (nodes[nodenum].type == NODE_POI) + Q_strncpyz(ent->node_name, va("node point of interest [%d]", ent->node_num), sizeof(ent->node_name)); + else + Q_strncpyz(ent->node_name, va("node action [%d]", ent->node_num), sizeof(ent->node_name)); + } + } + + // Shift all nodes and links down one + for (n = 0; n < MAX_PNODES; n++) + { + if (n != nodenum) + { + // Move links down + for (l = 0; l < MAXLINKS; l++) + { + if (nodes[n].links[l].targetNode != INVALID) + { + if (nodes[n].links[l].targetNode > nodenum) + nodes[n].links[l].targetNode--; + } + } + } + } + + // Shift all nodes and links down one + for (n = 0; n + 1 < MAX_PNODES; n++) + { + // Copy links + if (n >= nodenum) + { + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = nodes[n + 1].links[l].targetNode; + } + } + + // Shift node down + copy + if (n >= nodenum) + { + nodes[n].type = nodes[n + 1].type; + VectorCopy(nodes[n + 1].origin, nodes[n].origin); + node_ents[n] = node_ents[n + 1]; + --nodes[n].nodenum; + } + } +} + +/////////////////////////////////////////////////////////////////////// +// Add/Update (one way) node link +/////////////////////////////////////////////////////////////////////// +qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost) +{ + int i; + vec3_t v; + float cost; + + // Sanity Checks + if (type == INVALID) + { + if (debug_mode) debug_printf("%s cannot add link from %d to %i, targetNodeType: %d \n", __func__, from, to, type); + return false; + } + if (from == INVALID || to == INVALID || from == to) + { + if (debug_mode) debug_printf("%s error: cannot add link from %d to %i\n", __func__, from, to); + return false; + } + if (VectorEmpty(nodes[from].origin)) + { + if (debug_mode) debug_printf("%s error: zero origin from %d\n", __func__, from); + return false; + } + if (VectorEmpty(nodes[to].origin)) + { + if (debug_mode) debug_printf("%s error: zero origin to %d\n", __func__, to); + return false; + } + + // Check max links reached + if (nodes[from].num_links + 1 >= MAXLINKS) + { + if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); + return false; + } + + // Check if already added + for (i = 0; i < nodes[from].num_links; i++) // Always cycle all links so we can check for existing links + { + if (nodes[from].links[i].targetNode == to) // Do we already have a link to this target? + { + if (debug_mode) Com_Printf("%s warning: target link %d already link to %i\n", __func__, nodes[from].links[i].targetNode, to); + return false; // Failed to add link + } + } + + // Add link if we can + for (i = 0; i < MAXLINKS; i++) // Always cycle all links so we can check for existing links + { + if (i + 1 == MAXLINKS) // Max links reached + { + if (debug_mode) Com_Printf("%s warning: MAXLINKS was reached, cannot link from %i to %i\n", __func__, from, to); + return false; // Failed to add link + } + + if (nodes[from].links[i].targetNode == INVALID) // Found a free spot to add a new link + { + nodes[from].num_links++; // Update link counter + nodes[from].links[i].targetNode = to; // Add the link + nodes[from].links[i].targetNodeType = type; // The type of the target node + + if (do_cost) // Add the cost of this link? + { + VectorSubtract(nodes[from].origin, nodes[to].origin, v); // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. + cost = VectorLength(v); // The cost here is distance based + nodes[from].links[i].cost = cost; // Add cost + //if (debug_mode) + // Com_Printf("Link node[%d][%d] type[%d][%d] cost[%f]\n", from, to, nodes[from].type, nodes[to].type, cost); + } + //else if (debug_mode) debug_printf("Link %d -> %d\n", from, to); + + return true; // Successfully added link + } + } + return false; // Failed to add link +} + +/////////////////////////////////////////////////////////////////////// +// Add/Update (one way) node connection/path +/////////////////////////////////////////////////////////////////////// +qboolean ACEND_UpdateNodeReach(int from, int to) +{ + int i; + //trace_t trace; + + /* + // Sanity Checks + if (from == INVALID || to == INVALID || from == to) + return qfalse; + if (nodes[from].origin[0] == 0 && nodes[from].origin[1] == 0 && nodes[from].origin[2] == 0) + { + if (debug_mode) + debug_printf("Zero origin from %d\n", from); + return qfalse; + } + if (nodes[to].origin[0] == 0 && nodes[to].origin[1] == 0 && nodes[to].origin[2] == 0) + { + if (debug_mode) + debug_printf("Zero origin to %d\n", to); + return qfalse; + } + */ + + ///////////////////// + + // Add the link + path_table[from][to] = to; + + /* + // Checks if the link exists and then may create a new one - RiEvEr + for (i = 0; i < MAXLINKS; i++) + { + if (nodes[from].links[i].targetNode == to) + break; + if (nodes[from].links[i].targetNode == INVALID) + { + // RiEvEr: William uses a time factor here, whereas I use distance. His is possibly more efficient. + vec3_t v; + float thisCost; + + VectorSubtract(nodes[from].origin, nodes[to].origin, v); // subtract first + thisCost = VectorLength(v); + nodes[from].links[i].targetNode = to; + nodes[from].links[i].cost = thisCost; + if (debug_mode) + debug_printf("Link %d -> %d\n", from, to); + break; + } + } + */ + + // Now for the self-referencing part, linear time for each link added + for (i = 0; i < numnodes; i++) + { + if (path_table[i][from] != INVALID) + { + if (i == to) + path_table[i][to] = INVALID; // make sure we terminate + else + path_table[i][to] = path_table[i][from]; + } + } + + return true; +} +//rekkie -- DEV_1 -- e + +//rekkie -- DEV_1 -- s +//////////////////////////////////////////////////////////////////////////////////////////// +// Remove all node links from a particular node, including links from and to the node +// This is used when deleting a node, so the links get destroyed properly (bi-directional) +//////////////////////////////////////////////////////////////////////////////////////////// +void BOTLIB_RemoveAllNodeLinksFrom(int from) +{ + int i, j, to; + + // Other nodes + int link_other_counter = 0; + int link_other_to[MAXLINKS]; + int link_other_from[MAXLINKS]; + + // Our node + int link_self_counter = 0; + int link_self_to[MAXLINKS]; + int link_self_from[MAXLINKS]; + + for (i = 0; i < numnodes; i++) + { + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[i].links[j].targetNode != INVALID && nodes[i].links[j].targetNode == from) + { + //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, nodes[i].nodenum, from); + link_other_to[link_other_counter] = nodes[i].nodenum; + link_other_from[link_other_counter] = from; + link_other_counter++; + } + } + } + + // Gather all the link information before doing anything with it + for (i = 0; i < MAXLINKS; i++) + { + if (nodes[from].links[i].targetNode != INVALID) + { + to = nodes[from].links[i].targetNode; + + /* + // Check other node we're connected to + for (j = 0; j < MAXLINKS; j++) + { + if (nodes[to].links[j].targetNode == from) + { + //gi.dprintf("%s: OtherNode %d -> %d\n", __func__, to, from); + link_other_to[link_other_counter] = to; + link_other_from[link_other_counter] = from; + link_other_counter++; + } + } + */ + + //gi.dprintf("%s: OurNode %d -> %d\n", __func__, from, to); + link_self_from[link_self_counter] = from; + link_self_to[link_self_counter] = to; + link_self_counter++; + } + } + + // Now use the link information we gathered to remove connections between nodes + // + // Remove other node links to ours + for (i = 0; i < link_other_counter; i++) + ACEND_RemoveNodeEdge(NULL, link_other_to[i], link_other_from[i]); + // Remove our node link to other nodes + for (i = 0; i < link_self_counter; i++) + ACEND_RemoveNodeEdge(NULL, link_self_from[i], link_self_to[i]); +} + +#if 0 +// TODO: This function isn't ready for use yet +// Removes a node link from all nodes +void BOTLIB_RemoveNodeLink(edict_t* self, int from, int to) +{ + path_table[from][to] = INVALID; // set to invalid + + /* + // Fill the gap caused by the removal by left shifting from the position of the removed link + for (int i = 0; i < MAXLINKS; i++) + { + // Terminate the last link by making it INVALID + if (i == MAXLINKS - 1) + { + nodes[from].links[i].targetNode = INVALID; + nodes[from].links[i].cost = INVALID; + } + + // Move all the other info down to fill the gap (left shift). + // This *MUST* be done because pathing expects links to be contiguous, + // with the first INVALID link found becoming the terminator. + // IE: First link --> [LINK]-[LINK]-[LINK]-[LINK]-[INVALID] <-- terminator [INVALID]-[INVALID] <-- up to MAXLINKS + if (nodes[from].links[i].targetNode != INVALID) + { + nodes[from].links[i].targetNode = nodes[from].links[i + 1].targetNode; + nodes[from].links[i].cost = nodes[from].links[i + 1].cost; + } + } + + // Make sure this gets updated in our path array + for (int i = 0; i < numnodes; i++) + if (path_table[from][i] == to) + path_table[from][i] = INVALID; + */ +} +#endif + +// Checks a node to see if its possible to path to +// Parameters: +// goal_node: the goal node to reach +// path_randomization: Should path be randomized (if true, this can lead to no path being taken) +// check_and_goto: If TRUE check for a valid path AND go there. If FALSE check for valid path but don't go there. +// build_new_path: If we're building a new path or adding to an existing path +qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path) +{ + // Always update current node + self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + + // Invalid node + if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + return false; + } + + if (nodes[goal_node].type == NODE_LADDER) // Never visit ladder nodes + return false; + + // Already at node + if (goal_node == self->bot.current_node) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); + return false; + } + + // NEW PATHING: limit search to same area + if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) + return false; + + // OLD: searches all nodes + //else if (AntStartSearch(self, self->bot.current_node, goal_node, path_randomization) == false) + else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false) + return false; + + + + // ================================================================ + // BOTLIB_SetGoal () + // ================================================================ + if (self->bot.goal_node == INVALID) + self->bot.goal_node = goal_node; + + int nodelist[MAX_NODELIST]; + int nodes_touched; + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) + { + self->bot.current_node = nodelist[i]; + break; + } + } + + if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; // BOT_MOVE_STATE_WANDER + return false; + } + + self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + self->node_timeout = 0; + + self->bot.state = BOT_MOVE_STATE_MOVE; + + //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); + // ================================================================ + + + return true; +} + +/////////////////////////////////////////////////////////////////////// +// Set up the goal +/////////////////////////////////////////////////////////////////////// + +void BOTLIB_SetGoal(edict_t* self, int goal_node) +{ + // This code is now merged inside BOTLIB_CanVisitNode() + + /* + self->bot.goal_node = goal_node; + + int nodelist[MAX_NODELIST]; + int nodes_touched; + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched + { + if (nodelist[i] != INVALID) + { + self->bot.current_node = nodelist[i]; + break; + } + } + + if (self->bot.current_node == INVALID || goal_node == self->bot.current_node || goal_node == INVALID || nodes[goal_node].inuse == false) + { + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + + self->bot.goal_node = INVALID; + self->bot.state = BOT_MOVE_STATE_NAV; + //self->wander_timeout = level.framenum + 0.1 * HZ; + //ACEAI_PickLongRangeGoal(self); + //Com_Printf("%s %s invalid goal[%i]\n", __func__, self->client->pers.netname, self->bot.goal_node); + return; + } + + self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + self->node_timeout = 0; + + //Com_Printf("%s curr[%d] goal[%d] state[%d]\n", __func__, self->bot.current_node, self->bot.goal_node, self->state); //rekkie + + return; + */ +} + + + +// Run a trace from [start -> end] looking for nodes (hitbox) +// If [min/max] is empty -> Line trace +// If [min/max] not empty -> Box trace +// If a node was hit, return node number +// If nothing was hit, return INVALID +int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs) +{ + vec3_t pos, min, max, absmin, absmax; + vec3_t dir = { 0 }; + VectorCopy(mins, min); + VectorCopy(maxs, max); + if (VectorEmpty(min) && VectorEmpty(max)) + { + min[0] = -0.25; min[1] = -0.25; min[2] = -0.25; + max[0] = 0.25; max[1] = 0.25; max[2] = 0.25; + } + VectorSubtract(end, start, dir); + float distance = VectorLength(dir); + VectorCopy(start, pos); + float tested_distance = 1; + float normalized_dist = tested_distance / distance; // Normalized distance + while (tested_distance + 1 < distance) + { + tested_distance += 1; // Move distance forward + VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; + DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), mins, maxs, 5000); // Draw node box + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + } +#endif + //rekkie -- debug drawing -- e + + // Update absolute box min/max in the world + VectorAdd(pos, min, absmin); + VectorAdd(pos, max, absmax); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? + return i; + } + } + + return INVALID; // No intersection +} +/* +// Generate vis data for each node - test node-to-node visibility +void BOTLIB_GenerateNodeVis(edict_t* self) +{ + for (int i = 0; i < numnodes; i++) + nodes[i].num_vis_nodes = 0; // zero + + for (int i = 0; i < numnodes; i++) + { + for (int j = 0; j < numnodes; j++) + { + if (i == j) continue; // skip self + + // Check if the node was already added + for (int k = 0; k < nodes[i].num_vis_nodes; k++) + { + if (nodes[i].vis_nodes[k] == nodes[j].nodenum) + continue; + } + + if (nodes[i].num_vis_nodes + 1 >= 1024) + continue; + if (nodes[j].num_vis_nodes + 1 >= 1024) + continue; + + trace_t tr = gi.trace(nodes[i].origin, NULL, NULL, nodes[j].origin, NULL, MASK_SHOT); + + if (tr.fraction == 1.0) // Nodes can see each other + { + // Store the remote node in the local node + nodes[i].vis_nodes[nodes[i].num_vis_nodes] = nodes[j].nodenum; // Store the node we can see + nodes[i].num_vis_nodes++; // Increase the vis nodes in cache + + // Store the local node in the remote node + nodes[j].vis_nodes[nodes[j].num_vis_nodes] = nodes[i].nodenum; // Store the node we can see + nodes[j].num_vis_nodes++; // Increase the vis nodes in cache + } + } + } +} +*/ + +// Trace from node to node, checking to see if we hit other nodes along the path +// Return the node number that was hit, or INVALID if none was hit +int BOTLIB_TraceBoxNode(int from, int to) +{ + if (from == INVALID || to == INVALID) return INVALID; + + vec3_t pos, absmin, absmax; + vec3_t dir = { 0 }; + VectorSubtract(nodes[to].origin, nodes[from].origin, dir); + float distance = VectorLength(dir); + VectorCopy(nodes[from].origin, pos); + + float tested_distance = 1; + float normalized_dist = tested_distance / distance; // Normalized distance + while (tested_distance + 1 < distance) + { + tested_distance += 1; // Move distance forward + VectorMA(pos, normalized_dist, dir, pos); // Origin -> normalized distance -> direction = new position + + //rekkie -- debug drawing -- s +#if DEBUG_DRAWING + if (0) // Debug draw + { + void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; + DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), nodes[from].mins, nodes[from].maxs, 5000); // Draw node box + players[0]->client->pers.draw->boxes_inuse = true; // Flag as being used + //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + } +#endif + //rekkie -- debug drawing -- e + + // Update absolute box min/max in the world + VectorAdd(pos, nodes[from].mins, absmin); + VectorAdd(pos, nodes[from].maxs, absmax); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + if (nodes[i].nodenum == from || nodes[i].nodenum == to) continue; // Ignore source and destination nodes + + if (BOTLIB_BoxIntersection(nodes[i].absmin, nodes[i].absmax, absmin, absmax)) // Do boxes intersect? + return i; + } + } + + return INVALID; // No intersection +} + +// Test to see if a node is touching any other nodes +// Returns INVALID not touching any nodes +// Returns nodelist if one or more nodes are touching +// numnodes is the total nodes touched +// maxnodes is the maximum nodes that can be stored in nodelist +// ignore_node is optional: ignores testing this node when testing. If ignore_node is INVALID, all nodes are tested +int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int *nodelist, const int maxnodes, int ignore_node) +{ + int nodelist_count = 0; + + // Adjust the mins/maxs box size based on size + vec3_t bmins = { mins[0] + -(size), mins[1] + -(size), mins[2] + -(size) }; + vec3_t bmaxs = { maxs[0] + size, maxs[1] + size, maxs[2] + size }; + + // Update absolute box min/max in the world + vec3_t absmin, absmax; + VectorAdd(origin, bmins, absmin); + VectorAdd(origin, bmaxs, absmax); + + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + if (nodes[n].nodenum == INVALID) continue; // Ignore invalid nodes + //if (VectorDistance(nodes[n].origin, origin) > 32) continue; // Skip checking distant nodes + if (ignore_node != INVALID && nodes[n].nodenum == ignore_node) continue; // Ignore ignored node + + // Line test + //trace_t tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID | MASK_OPAQUE); + //if (tr.fraction == 1.0) + { + if (BOTLIB_BoxIntersection(absmin, absmax, nodes[n].absmin, nodes[n].absmax)) // Do boxes intersect? + { + //if (normal[2] == 1 && origin[2] != nodes[n].origin[2]) // Check flat ground + height difference + // continue; // Ignore nodes that are on a different plane (such as stairs or boxes) + + if (nodelist_count + 1 < maxnodes) // Check we have room to store + nodelist[nodelist_count++] = n; //nodes[n].nodenum; // Add to list + else + break; + } + } + } + + return nodelist_count; // Return number of nodes touched +} + +// Test location to see if taken by an existing node based on distance, return the node (if any) we're too close to +// Returns 'INVALID' if no node(s) are found +// Returns 'nodenum' if node(s) are found +int BOTLIB_TestForNodeDist(vec_t *origin, float distance, vec3_t mins, vec3_t maxs) +{ + trace_t tr; + //vec3_t v; + //float dist; + + // Check if we fit + tr = gi.trace(origin, tv(-2, -2, 0), tv(2, 2, 0), origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return -2; // Inside solid + } + + // Test to see if node hits the ceiling + //tr = gi.trace(tv(origin[0], origin[1], origin[2]), tv(-16, -16, -32), tv(16, 16, 25), tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + 25), NULL, MASK_PLAYERSOLID); + if (tr.fraction < 1.0 || tr.startsolid) + { + return -2; // Hit our head on something immediately above us + } + + + vec3_t bmins = { mins[0] + -(distance), mins[1] + -(distance), mins[2] + -(distance) }; + vec3_t bmaxs = { maxs[0] + distance, maxs[1] + distance, maxs[2] + distance }; + + //for (int n = 0; n < NODE_MAX; n++) + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + + if (nodes[n].nodenum != INVALID) + { + // Line test + tr = gi.trace(origin, NULL, NULL, nodes[n].origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) // Only distance test origins that can see each other + { + // New method of box intersection search + if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[n].mins, nodes[n].maxs)) + return nodes[n].nodenum; // Return the first node found too close + + /* + // Old method of radius search + VectorSubtract(nodes[n].origin, origin, v); // other node -> our node + dist = VectorLength(v); + if (dist < distance) + { + //if (dist == 0) // If we find a node at the exact same coordinates + // Com_Printf("%s found node %d at the same origin[%f %f %f]\n", __func__, n, origin[0], origin[1], origin[2]); + + //gi.dprintf("-=-=-=-=-=------------[%i] dist[%f]\n", n, dist); + return nodes[n].nodenum; // Return the first node found too close + } + */ + } + } + } + + return INVALID; // Found no nodes within range, safe to place a node here :) +} + +// Test all nodes to see they're too close to our node +// Calculation is based on matching the exact X and Y coords, then comparing the height difference +// Returns the first offending node that is too close, otherwise INVALID if no node is found +int BOTLIB_UTIL_NearbyNodeAtHeightDist(vec_t* origin, float distance) +{ + float dist; + for (int n = 0; n < numnodes; n++) + { + if (nodes[n].inuse == false) continue; // Ignore nodes not in use + + if (nodes[n].nodenum != INVALID) + { + if (origin[0] == nodes[n].origin[0]) // Match X axis + { + if (origin[1] == nodes[n].origin[1]) // Match Y axis + { + dist = abs(nodes[n].origin[2] - origin[2]); // Get Z height distance + + if (dist <= distance) // Z axis within distance + return n; // Offending node that is too close + } + } + } + } + + return INVALID; // Found no nodes that are too close +} + +float HalfPlaneSign(vec2_t p1, vec2_t p2, vec2_t p3) +{ + return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); +} + +// Check if point is inside triangle. +// PT = (x,y,z) point location. V1,V2,V3 = 3 points of a triangle (x,y,z) +qboolean PointInTriangle(vec3_t pt, vec3_t v1, vec3_t v2, vec3_t v3) +{ + float d1, d2, d3; + qboolean has_neg, has_pos; + + d1 = HalfPlaneSign(pt, v1, v2); + d2 = HalfPlaneSign(pt, v2, v3); + d3 = HalfPlaneSign(pt, v3, v1); + + has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); + has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); + + return !(has_neg && has_pos); +} + +// Returns true if the point is inside a convex polygon +qboolean PointInPolygon(vec2_t pt, vec2_t* v, int num_verts) +{ + int i, j, c = 0; + for (i = 0, j = num_verts - 1; i < num_verts; j = i++) + { + if (((v[i][1] > pt[1]) != (v[j][1] > pt[1])) && + (pt[0] < (v[j][0] - v[i][0]) * (pt[1] - v[i][1]) / (v[j][1] - v[i][1]) + v[i][0])) + c = !c; + } + return c; +} + +// A function that makes a square polygon from a set of vertices +void BOTLIB_UTIL_MakePolySquare(int face, vec3_t out) +{ + int e; + vec3_t edge[128][2]; // Max edges per face + for (e = 0; e < nmesh.face[face].num_edges; e++) + { + // Copy all the edges into a 2D array + VectorCopy(nmesh.face[face].edge[e].v[0], edge[e][0]); // v1 + VectorCopy(nmesh.face[face].edge[e].v[1], edge[e][1]); // v2 + } + + /* + face 101 edges 6 + -------------- + v1[256.000000 -176.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] + v1[256.000000 -112.000000 -800.000000] v2[256.000000 -176.000000 -800.000000] + v1[256.000000 -33.000000 -800.000000] v2[256.000000 -112.000000 -800.000000] + v1[256.000000 0.000000 -800.000000] v2[256.000000 -33.000000 -800.000000] + v1[480.000000 0.000000 -800.000000] v2[256.000000 0.000000 -800.000000] + v1[480.000000 0.000000 -800.000000] v2[480.000000 -176.000000 -800.000000] + -------------- + */ + + // Find each edge that goes in the same direction + // Try to find the total length of that direction + vec3_t v1, v2; + //float highest = 0, lowest = 0; + float v1_x; // V1 X + float v1_y; // V1 Y + float v2_x; // V2 X + float v2_y; // V2 Y + + float highest_x = 0; + float lowest_x = 0; + float highest_y = 0; + float lowest_y = 0; + + for (e = 0; e < nmesh.face[face].num_edges; e++) + { + VectorCopy(edge[e][0], v1); // First vertex + VectorCopy(edge[e][1], v2); // Second vertex + + for (int e2 = 0; e2 < nmesh.face[face].num_edges; e2++) + { + if (e == e2) continue; // Skip self + + // Check the X axis + v1_x = v1[0]; v2_x = v2[0]; // X + v1_y = v1[1]; v2_y = v2[1]; // Y + + if (v1_x == v2_x) // Check if v1 and v2 are on the same X axis + { + // Find highest on the Y axis + if (v1_y > highest_y) + highest_y = v1_y; + else if (v2_y > highest_y) + highest_y = v2_y; + + // Find lowest on the Y axis + if (v1_y < lowest_y) + lowest_y = v1_y; + else if (v2_y < lowest_y) + lowest_y = v2_y; + } + if (v1_y == v2_y) // Check if v1 and v2 are on the same Y axis + { + // Find highest on the X axis + if (v1_x > highest_x) + highest_x = v1_x; + else if (v2_x > highest_x) + highest_x = v2_x; + + // Find lowest on the X axis + if (v1_x < lowest_x) + lowest_x = v1_x; + else if (v2_x < lowest_x) + lowest_x = v2_x; + } + } + } +} + + +// A function that finds the center of a 3D convex polygon +void BOTLIB_UTIL_PolygonCenter(vec3_t* v, int num_verts, vec3_t center) +{ + vec3_t sum = { 0,0,0 }; + for (int i = 0; i < num_verts; i++) + { + VectorAdd(sum, v[i], sum); + } + VectorScale(sum, 1.0 / num_verts, center); +} + +// Return the BSP face and triangle based on origin +void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle) +{ + vec3_t org = { 0,0,0 }; + vec3_t dist = { 0,0,0 }; + int n; + float distance; + + for (int f = 0; f < nmesh.total_faces; f++) // Current face + { + if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls + + for (int t = 0; t < nmesh.face[f].num_tris; t++) // Current triangle + { + if (PointInTriangle(origin, nmesh.face[f].tris[t].v[0], nmesh.face[f].tris[t].v[1], nmesh.face[f].tris[t].v[2])) + { + // Ignore tris with invalid nodes + n = nmesh.face[f].tris[t].node; + if (n <= 0) + continue; + + // Ensure the PointInTriangle is roughly within the same z height as origin + VectorCopy(origin, org); // We only want the height + org[0] = 0; // Remove X component + org[1] = 0; // Remove Y component + VectorSubtract(nodes[n].origin, origin, dist); + distance = VectorLength(dist); + if (distance > 64) // Height + continue; + + //Com_Printf("%s -> f[%i] t[%i] node %i at %f %f %f\n", __func__, f, t, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); + + *found = true; + *floor = f; + *triangle = t; + return; + } + } + } +} + +// Return the BSP face and number of edges that is closest to origin +void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int *edges) +{ + vec3_t org = { 0,0,0 }; + vec3_t dist = { 0,0,0 }; + int e; // edge + float distance; + float closest = 999999; + + for (int f = 0; f < nmesh.total_faces; f++) // Current face + { + if (nmesh.face[f].type == FACETYPE_WALL) continue; // Skip walls + + vec3_t edge[128]; // Max edges per face + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Copy all the edges into a 2D array + VectorCopy(nmesh.face[f].edge[e].v[0], edge[e]); + } + + BOTLIB_UTIL_PolygonCenter(edge, nmesh.face[f].num_edges, org); // Get the center of the polygon + + // Ensure the polygon is roughly within the same z height as origin + //VectorCopy(origin, org); // We only want the height + //org[0] = 0; // Remove X component + //org[1] = 0; // Remove Y component + VectorSubtract(org, origin, dist); + distance = VectorLength(dist); + //if (distance > 64) // Height + // continue; + + if (distance < closest) + { + closest = distance; + *floor = f; + *edges = e; + } + + } +} + + +// Returns the distance from origin to target +float ACEND_Distance(vec3_t origin, vec3_t target) +{ + vec3_t dist; + VectorSubtract(target, origin, dist); + return VectorLength(dist); +} + +// Using three points of a triangle with origin being the origin calculate the angle between target_a and target_b +float ACEND_AngleBetween(vec3_t origin, vec3_t target_a, vec3_t target_b) +{ + vec3_t a, b; + VectorSubtract(target_a, origin, a); // Get length + VectorSubtract(target_b, origin, b); // Get length + return RAD2DEG(acos(DotProduct(a, b) / (VectorLength(a) * VectorLength(b)))); +} + + +void LaunchP(edict_t* ent, vec3_t origin, vec3_t target) +{ + vec3_t dist; + vec3_t velocity; + float gravity = ent->gravity * sv_gravity->value; + VectorSubtract(target, ent->s.origin, dist); + float distance = VectorLength(dist); + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + + // Calculate the time it will take to get to the target + float time = distance / (ent->velocity[2] + jump_height / 2.0); + + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height = 0; + if (target[2] > ent->s.origin[2]) + { + z_height = (target[2] - ent->s.origin[2]); + z_height += sqrtf(distance) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else if (abs(target[2] - ent->s.origin[2]) <= 4) + { + // Do nothing here + } + else + { + z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + // Set the velocity of the player to the calculated velocity + VectorCopy(velocity, ent->velocity); + VectorCopy(ent->velocity, ent->client->oldvelocity); + VectorCopy(ent->velocity, ent->avelocity); + + //float speed = VectorLength(velocity); + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); +} + +qboolean LaunchPlayer(edict_t* ent, vec3_t target) +{ + + LaunchP(ent, ent->s.origin, target); + return false; + + // Calculate the distance to the target + vec3_t dist; + vec3_t velocity; + float gravity = ent->gravity * sv_gravity->value; + //float gravity = ent->gravity * sv_gravity->value * FRAMETIME; + + + vec3_t targ; + VectorCopy(target, targ); + //targ[2] += 64; + VectorSubtract(targ, ent->s.origin, dist); + //dist[2] = 0; + //dist[2] += 64; + float distance = VectorLength(dist); + VectorCopy(ent->velocity, velocity); + //velocity[2] += 64; + //VectorNormalize(velocity); + //VectorNormalize(dist); + //if (DotProduct(velocity, dist) > 0.8) + //Com_Printf("%s %f\n", __func__, DotProduct(velocity, dist)); + //distance *= 2.0; // Increasing this increases the jump height + + + //VectorSubtract(target, ent->s.origin, dist); + //float distance = VectorLength(dist); + //distance *= 1.0; // Increasing this increases the jump height + + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * (gravity * distance)); + // Calculate the time it will take to get to the target + float time = distance / (ent->velocity[2] + jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = jump_height / 2.0; + + // If the target is above the player, increase the velocity to get to the target + float z_height; + if (target[2] > ent->s.origin[2]) + { + z_height = (target[2] - ent->s.origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + } + else + { + z_height = (ent->s.origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + } + + + /* + // Get the z height distance between origin and target + vec3_t zdiff, oz, tz; + // Copy origin to oz + VectorCopy(ent->s.origin, oz); + // Copy target to tz + VectorCopy(target, tz); + // Set X and Y on oz and tz to zero + oz[0] = 0; oz[1] = 0; + tz[0] = 0; tz[1] = 0; + // Get the z height distance between oz and tz + VectorSubtract(oz, tz, zdiff); + // Add the difference to jump velocity + float z_height = 0; + if (ent->s.origin[2] < target[2]) + { + //z_height = VectorLength(zdiff); + velocity[2] += z_height + 56; + } + else + { + velocity[2] -= VectorLength(zdiff); + } + */ + + // Calculate speed from velocity + float speed = VectorLength(velocity); + gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + // Limit max speed + //if (speed < 750 && z_height < 150 && jump_height <= 965) + { + // Set the velocity of the player + VectorCopy(velocity, ent->velocity); + // Don't take falling damage immediately from this + if (ent->client) + VectorCopy(ent->velocity, ent->client->oldvelocity); + + return true; + } + return false; +} + +int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal) +{ + trace_t tr; + + //static const float max_dist = 128; // Absolute max distance -- 470 + const float max_dist = 470; // Absolute max distance -- 470 + //float curr_dist = 224; + //qboolean is_los = true; // Line of Sight + qboolean is_gap = false; // If there's a gap between origin and target + + // Sanity checks + if (from == to || from == INVALID || to == INVALID) + return INVALID; + if (origin[0] == 0 && origin[1] == 0 && origin[2] == 0) + return INVALID; + if (target[0] == 0 && target[1] == 0 && target[2] == 0) + return INVALID; + + + //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) + // Com_Printf("%s NODE_WATER\n", __func__); + + /* + // Ignore some water nodes (except ladders) + // Don't go into the water + if (nodes[from].type != NODE_WATER && nodes[to].type == NODE_WATER) + return INVALID; + // Check node is in water + if (nodes[from].type == NODE_WATER && (gi.pointcontents(nodes[from].origin) & CONTENTS_WATER) == 1) + return INVALID; + // Check node is in water + if (nodes[to].type == NODE_WATER && (gi.pointcontents(nodes[to].origin) & CONTENTS_WATER) == 1) + return INVALID; + //if (nodes[from].type == NODE_WATER) + // return INVALID; + //if (nodes[from].type == NODE_WATER || nodes[to].type == NODE_WATER) + // return INVALID; + */ + + //vec3_t vec_from, vec_to; + + // Calculate the distance to the target + vec3_t dist; + vec3_t velocity; + + VectorSubtract(target, origin, dist); + float xyz_distance = VectorLength(dist); // XYZ distance + + //VectorSubtract(target, origin, dist); + dist[2] = 0; + float xy_distance = VectorLength(dist); // XY Distance + + // Max distance + if (xyz_distance > max_dist) // Ignore very far nodes + return INVALID; + + // Min distance + //if (xyz_distance < NODE_Z_HEIGHT) // Ignore very close nodes + // return INVALID; + + /* + // Player when standing + vec3_t player_standing_mins = { -16, -16, -24 }; // With legs + vec3_t player_partial_standing_mins = { -16, -16, -0 }; // Without legs + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing smaller mins + vec3_t player_standing_smaller_mins = { -14, -14, -24 }; + vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; + // Player width only, no height component + vec3_t player_no_height_mins = { -16, -16, -0 }; + vec3_t player_no_height_maxs = { 16, 16, 0 }; + // Small box test + vec3_t small_mins = { -8, -8, -2 }; + vec3_t small_maxs = { 8, 8, 2 }; + // Tiny box test + vec3_t tiny_mins = { -1, -1, -1 }; + vec3_t tiny_maxs = { 1, 1, 1 }; + */ + + + // Do a line trace + tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); + // See if we hit any entities we wish to avoid + if (tr.ent && tr.ent != g_edicts) + { + //Com_Printf("%s %s found - \n", __func__, tr.ent->classname); + + // Avoid these + if (strcmp(tr.ent->classname, "trigger_hurt") == 0) + { + //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); + return INVALID; + } + + // Avoid rotating geometry - for now + if (strcmp(tr.ent->classname, "func_rotating") == 0) + { + //Com_Printf("%s %s found - invalid reachability\n", __func__, tr.ent->classname); + return INVALID; + } + } + // See if we hit any walls or inside a solid + if (tr.fraction < 1.0 || tr.startsolid) + { + return INVALID; + } + + + + + + + // Test for gaps/holes between the current and next node + // Each tested is conducted x units apart + { + vec3_t dir, end; + float tested_distance = 0; + + // Get direction + VectorSubtract(target, origin, dir); // (end - start) = dir + + // Get XYZ distance + xyz_distance = VectorLength(dir); // VectorLength of dir (cannot be normalized) + + // Normalize direction vector (must be done after VectorLength otherwise we don't get the correct distance) + VectorNormalize(dir); + + // Safety check + if (xyz_distance >= 4096) // Max distance + { + Com_Printf("%s WARNING: gap test exceeded > 4096 units from node %d to %d\n", __func__, from, to); + } + else + { + while (tested_distance + NODE_SIZE < xyz_distance) + { + tested_distance += NODE_SIZE; // Move next test forward + VectorMA(origin, tested_distance, dir, end); // origin -> distance -> direction = end point + + // Check position to see if we hit any solids along the way + tr = gi.trace(end, tv(-12,-12,-8), tv(12, 12, 8), end, g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); + if (tr.startsolid) is_gap = true; + + end[2] -= NODE_Z_HEIGHT; // Move end point to ground + tr = gi.trace(end, NULL, NULL, tv(end[0], end[1], -4096), g_edicts, MASK_PLAYERSOLID | MASK_OPAQUE); // Trace down until we hit the ground + if (tr.startsolid) continue; + //is_gap = true; + + //if (tr.plane.normal[2] < 0.99) // If the ground we hit below is not flat + { + //is_gap = true; + //break; + } + + // + if (end[2] - tr.endpos[2] > (STEPSIZE + 1)) + { + //if (from == 1740 && to == 1403) + // Com_Printf("%s dist %f z-diff %f -- FOUND GAP\n", __func__, tested_distance, end[2] - tr.endpos[2]); + is_gap = true; + break; + } + } + } + } + + + + + // Use smaller node min/max for ladders + //if (nodes[to].type == NODE_LADDER_DOWN || nodes[to].type == NODE_LADDER_UP) + // //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component + // tr = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_DEADSOLID); // Line trace + //else + // tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height + if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) + { + tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Test player partial height + //tr = gi.trace(origin, tv(-16, -16, -0), tv(16, 16, 0), target, g_edicts, MASK_DEADSOLID); // Player width only, no height component + // Try to trace from node->node LoS + // Failing that a second trace is performed with both nodes moved up slightly + if (tr.fraction < 1) + { + // Ignore ladders + //if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_DOWN && nodes[from].type != NODE_LADDER_UP && nodes[to].type != NODE_LADDER_UP) + { + // See if target is below and if we can drop down + if (origin[2] > target[2] && xy_distance < 96) + { + //tr = gi.trace(origin, tv(-16, -16, 48), tv(16, 16, 56), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, 25), tv(16, 16, 26), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-8, -8, -0), tv(8, 8, 0), target, g_edicts, MASK_DEADSOLID); // LoS + width to the node below + if (tr.fraction == 1) + { + float lower = (origin[2] - target[2]); // Distance below + + if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_STAND_DROP; // Can drop while standing + } + + // Crouch drop down + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH_DROP; // Can drop but need to crouch + } + + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_UNSAFE_DROP; // Can drop while crouching but will take some leg damage + } + } + } + + // Check for crouching + if (nodes[to].type == NODE_MOVE && is_gap == false && xy_distance < 256) + { + tr = gi.trace(tv(origin[0], origin[1], origin[2] - 16), tv(-16, -16, -32), tv(16, 16, 0), tv(target[0], target[1], target[2] - 16), g_edicts, MASK_DEADSOLID); // Test crouch height + if (tr.fraction == 1) + { + return NODE_CROUCH; + } + } + } + + //return INVALID; + } + } + + + // Invalidate bad ladder links + // --------------------------- + // Ladder from DOWN -> UP must be on same X/Y position + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP && (origin[0] != target[0] || origin[1] != target[1])) + return INVALID; + // Ladder from UP -> DOWN must be on same X/Y position + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN && (origin[0] != target[0] || origin[1] != target[1])) + return INVALID; + // Ladder from DOWN -> DOWN + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_DOWN) + return INVALID; + // Ladder from UP -> UP + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type == NODE_LADDER_UP) + return INVALID; + + // Top ladder node -> non-ladder node, test if within z height range +/- + if (nodes[from].type == NODE_LADDER_UP && nodes[to].type != NODE_LADDER_DOWN) + { + //if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 128 || xyz_distance < 48) + if (abs(origin[2] < target[2]) > 8 || xyz_distance > 128 || xyz_distance < 48) + return INVALID; + else + return NODE_MOVE; + //else + // return NODE_JUMPPAD; // Jump to next node + } + // Bottom ladder node -> non-ladder node, test if within z height range +/- + if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_UP) + { + if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 96) + return INVALID; + } + + // Non-ladder node -> top ladder node - test if within z height range +/- + if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP) + { + if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) + return INVALID; + } + // Non-ladder node -> bottom ladder node - test if within z height range +/- + if (nodes[from].type != NODE_LADDER_UP && nodes[to].type == NODE_LADDER_DOWN) + { + //if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) + // return INVALID; + if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT && xyz_distance < 96) + return NODE_JUMPPAD; + else + return INVALID; + } + // --------------------------- + + + /* + //if (nodes[from].type == NODE_LADDER || nodes[to].type == NODE_LADDER) + // return INVALID; +// (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + if (nodes[from].type == NODE_LADDER && nodes[to].type == NODE_LADDER) + { + if (abs(origin[2] - target[2]) > 4) + { + if (abs(origin[0] - target[0]) <= 4 && abs(origin[1] - target[1]) <= 4) + return NODE_LADDER; + else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS) + return INVALID; + else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[0] == target[0] && abs(origin[1] - target[1]) <= 96) + return NODE_LADDER; + else if (normal[0] != 1.0 && normal[1] != 1.0 && origin[1] == target[1] && abs(origin[0] - target[0]) <= 96) + return NODE_LADDER; + } + return INVALID; + } + else if (nodes[from].type == NODE_LADDER && nodes[to].type != NODE_LADDER && xyz_distance > 64) + return INVALID; + else if (nodes[from].type != NODE_LADDER && nodes[to].type == NODE_LADDER && xyz_distance > 64) + return INVALID; + */ + + + + + + + + //return INVALID; + + + + + + + + // Water nodes + if (nodes[from].type == NODE_WATER) + { + if (target[2] > origin[2] && xy_distance < 64 && nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder + return NODE_LADDER_UP; + //if (nodes[to].type == NODE_LADDER_UP) // Allow water -> ladder + // return NODE_LADDER_DOWN; + //else if (nodes[to].type == NODE_MOVE) // Allow water -> move + // return NODE_MOVE; + else if (nodes[to].type == NODE_WATER) // Allow water -> water + return NODE_MOVE; + else + return INVALID; // Deny water to anything else + } + + + + // Steps + // ----- + // Deal with steps + if (nodes[from].type == NODE_STEP || nodes[to].type == NODE_STEP) + { + // Getting off step to a landing node + if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_MOVE && abs(origin[2] - target[2]) <= NODE_Z_HEIGHT_PLUS_STEPSIZE && xyz_distance <= 80) + { + trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_MOVE; + } + } + // Getting on step + else if (nodes[from].type == NODE_MOVE && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance <= 80) + { + trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_MOVE; + } + } + // Step to Step + else if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance < 40) + { + + //is_gap + trace_t tr_step = gi.trace(origin, NULL, NULL, target, g_edicts, MASK_PLAYERSOLID); + if (tr_step.fraction == 1) + { + return NODE_STEP; + } + } + else + return INVALID; + } + + + + + // Calculate max jump height + // If on flat ground the max is 60 (NODE_MAX_JUMP_HEIGHT) + // If on slope up, the max is 150 depending on the angle of the slope, higher angle = higher max jump height + float z_height_max = NODE_MAX_JUMP_HEIGHT; + //float max_speed = 577; //(750 / 1.3 = 577) + if (normal[0] != 1.0 && normal[1] != 1.0) + { + float z_height_max_x = 1.0; + float z_height_max_y = 1.0; + if (normal[0] >= MAX_STEEPNESS) // Player is standing on a slope going up //else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + { + z_height_max_x += (1 - normal[0]); + } + if (normal[1] >= MAX_STEEPNESS) + { + z_height_max_y += (1 - normal[1]); + } + z_height_max = (z_height_max_x + z_height_max_y) / 2; + if (z_height_max > 1.3) + z_height_max = 1.3; + else if (z_height_max < 1.0) + z_height_max = 1.0; + + z_height_max *= 116; // Max jump height is 150, and max z_height is 1.3 ... so 116 * 1.3 = 150 (150/1.3 = 116) + if (z_height_max > 150) + z_height_max = 150; + + //max_speed *= z_height_max; + } + + qboolean target_is_above = false, target_is_below = false, target_is_equal = false; + float higher = 0; + float lower = 0; + if (origin[2] > target[2]) // We're above the target + { + target_is_below = true; + lower = (origin[2] - target[2]); + } + else if (origin[2] < target[2]) // We're below the target + { + target_is_above = true; + higher = (target[2] - origin[2]); + } + else + target_is_equal = true; + + + + + + + + + + + + // Dropping down to target + // ----------------------- + // Can drop while standing + if (target_is_below && xy_distance < 228) //256 + { + if (lower <= NODE_MAX_FALL_HEIGHT && xyz_distance <= NODE_MAX_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_STAND_DROP; // Can drop while standing + } + + // Crouch drop down + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT + NODE_Z_HALF_HEIGHT) + { + return NODE_CROUCH_DROP; // Can drop but need to crouch + } + + // Crouch drop down + leg damage (actcity2 uses this for the one spawn that can cause leg damage when dropping after LCA) + if (lower <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE && xyz_distance <= NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE + NODE_Z_HALF_HEIGHT) + { + return NODE_UNSAFE_DROP; // Can drop while crouching but will take some minor leg damage + } + } + + // Small jump to target + // ----------------- + //if (target_is_above && distance > STEPSIZE && distance < NODE_Z_HEIGHT) + // return NODE_JUMP; + + // Jump nodes + //if (xyz_distance <= 96 && is_gap && fabs(origin[2] - target[2]) <= 48) + //{ + //return NODE_JUMP; + //} + + // Large jump up to target + // -------------------- + // Jumppad nodes + //if (target_is_above && distance > STEPSIZE) + if (xyz_distance >= 16 && is_gap) + { + float gravity = sv_gravity->value; // This doesn't take into account the ent's own gravity (ent->gravity) + + //distance *= 1.0; // Increasing this increases the jump height + // Calculate the jump height to get to the target + float jump_height = sqrt(2 * gravity * xyz_distance); + float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point + + // Move the middle point up to the jump height (maximum parabola height) + //end_50[2] += NODE_Z_HEIGHT_PLUS_STEPSIZE + jump_height; + // Next test from start to mid, then mid to end + //tr_25 = gi.trace(origin, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + //tr_75 = gi.trace(target, tv(-16, -16, -30), tv(16, 16, 24), end_50, g_edicts, MASK_DEADSOLID); + // If the path from [start -> mid jump -> end] is clear of obsticles, then allow the jump + //if (tr_25.fraction == 1.0 && tr_75.fraction == 1.0) + + // Do we have room to jump without hitting our head? + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -6), tv(16, 16, 48), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT) , g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), NULL, NULL, tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), tv(-16, -16, -24), tv(16, 16, 32), tv(target[0], target[1], target[2] + NODE_Z_HEIGHT), g_edicts, MASK_DEADSOLID); // Player height + some jumping height + //tr = gi.trace(origin, tv(-16, -16, -24), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + tr = gi.trace(origin, tv(-16, -16, 0), tv(16, 16, jump_height_headroom), target, g_edicts, MASK_DEADSOLID); // Player height + some jumping height + if (tr.fraction == 1.0 && !tr.startsolid) + { + //Com_Printf("jump_height[%f] jump_height_headroom[%f]\n", jump_height, jump_height_headroom); + + // Calculate the time it will take to get to the target + float time = xyz_distance / (jump_height / 2.0); + // Calculate the velocity at the end of the jump + VectorScale(dist, 1.0 / time, velocity); + velocity[2] = jump_height / 2.0; + // + // If the target is above the player, increase the velocity to get to the target + float z_height = 0; + + // Above + if (target[2] > origin[2]) + { + z_height = (target[2] - origin[2]) + NODE_Z_HEIGHT; + velocity[2] += z_height; + + float speed = VectorLength(velocity); + //if (speed < 550 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) + if (speed < 550 && z_height <= z_height_max + sqrtf(xy_distance) && jump_height <= 450) + return NODE_JUMPPAD; + } + // Roughly equal + else if (abs(origin[2] - target[2]) <= 8) // target node is +/- 8 + { + float speed = VectorLength(velocity); + if (speed < 600 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 750) + return NODE_JUMPPAD; + } + // Below + else + { + z_height = (origin[2] - target[2]) - NODE_Z_HEIGHT; + velocity[2] -= z_height; + + float speed = VectorLength(velocity); + if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) + return NODE_JUMPPAD; + } + + // Calculate speed from velocity + //float speed = VectorLength(velocity); + + //gi.dprintf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + //Com_Printf("speed[%f] z_height[%f] jump[%f]\n", speed, z_height, jump_height); + //if (distance > 64 && speed < 750 && z_height < z_height_max + 24 && jump_height <= 965) + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) // Works VERY well + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 500) + + //if (speed < 750 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 965) + // return NODE_JUMPPAD; + } + } + + + // Limit max speed + //if (speed < 750 && z_height < 150 && jump_height <= 965) // Maximum + //if (speed <= 750 && z_height < 48 && jump_height <= 525) // Works well for ground based nodes + //if (speed <= 750 && z_height < NODE_MAX_JUMP_HEIGHT && jump_height <= 965) + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 600) // Works really well!! + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 700) + //if (speed <= 750 && z_height <= z_height_max && jump_height <= 650) // Works uber well + //if (speed <= 750 && z_height <= z_height_max && (jump_height <= 650 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 64)) + //if (distance <= 224 && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32) ) + //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) + //if (distance <= curr_dist && (higher <= NODE_MAX_JUMP_HEIGHT + 40 || lower <= NODE_MAX_CROUCH_FALL_HEIGHT + 32)) + + // Small jump + //if (distance < 64 && higher > STEPSIZE && higher <= NODE_MAX_JUMP_HEIGHT) + //return NODE_JUMP; + + // Jumping up + //if (distance < 128 && higher > STEPSIZE && higher <= z_height_max) + //{ + //return NODE_JUMPPAD; + //} + + + + + // Running to target + // ----------------- + if (xy_distance < 400 && !is_gap) + { + if (origin[2] == target[2]) + { + //return INVALID; + + //if (from == 1066 && to == 1069) + // Com_Printf("%s\n", __func__); + + //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-15, -15, -NODE_Z_HEIGHT), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.fraction == 1) + return NODE_MOVE; + } + else if (abs(origin[2] - target[2]) <= STEPSIZE) // target node is +/- STEPSIZE + { + //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + tr = gi.trace(origin, tv(-15, -15, -(NODE_Z_HEIGHT - STEPSIZE)), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); + //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.fraction == 1) + return NODE_MOVE; + } + } + + /* + // Jump to target + // ----------------- + if (xy_distance < 128) + { + if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT) // target node is +/- NODE_MAX_JUMP_HEIGHT + { + // We passed a partial height test, now test players step height + // -32 is ground + 17 (one less than full step height of 18) = -14 + tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); // Step height + if (tr.fraction < 1) // We hit something + { + // Test if we can jump over + // mins: -32 is ground + 18 (step height) = -16 + // maxs: mins (-16) + full player size (56) = 40 + //tr = gi.trace(origin, tv(-16, -16, -16), tv(16, 16, 40), target, g_edicts, MASK_DEADSOLID); // Jump height + player height + //if (tr.fraction == 1.0) // We hit nothing, so we can jump over :) + // return NODE_JUMP; + + //VectorCopy(origin, vec_from); + //VectorCopy(target, vec_to); + //origin + + // Test if we fit inside target area + tr = gi.trace(target, tv(-16, -16, -32), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) // We fit + return NODE_JUMP; + //return NODE_JUMP; + } + else + return NODE_JUMP; + } + } + */ + + + + // Run up/down slopes and ramps + if (xy_distance < 512 && !is_gap) + { + // If we have LoS + tr = gi.trace(origin, tv(-16, -16, -14), tv(16, 16, 24), target, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction > 0.9) + { + // Test for slope / stairs at the midpoint + trace_t tr_50; + vec3_t pt_50, end_50; + LerpVector(origin, target, 0.50, pt_50); // Get the two vector midpoint + VectorCopy(pt_50, end_50); + end_50[2] -= NODE_Z_HEIGHT_PLUS_STEPSIZE; + tr_50 = gi.trace(pt_50, NULL, NULL, end_50, g_edicts, MASK_DEADSOLID); + if (tr_50.fraction < 1.0) // We hit solid + { + // Slope + if (tr_50.plane.normal[0] != 0 || tr_50.plane.normal[1] != 0) + return NODE_MOVE; + } + } + } + + + return INVALID; +} + +// Check to see if triangles are coplanar +// Check to see if the triangles are adjacent +//qboolean TrianglesAreCoplanar(vec3_t t1_p1, vec3_t t1_p2, vec3_t t1_p3, vec3_t t2_p1, vec3_t t2_p2, vec3_t t2_p3) + +// Explodes all breakable glass :D +void Remove_All_Breakableglass(void) +{ + edict_t* glass; + for (glass = g_edicts; glass < &g_edicts[globals.num_edicts]; glass++) + { + if (!glass) + continue; + //if (glass->solid == SOLID_NOT) + // continue; + if (!glass->classname) + continue; + + if (strcmp(glass->classname, "breakableglass_trigger") == 0 || strcmp(glass->classname, "func_explosive") == 0) + { + //gi.dprintf("Found glass at location %f %f %f\n", glass->s.origin[0], glass->s.origin[1], glass->s.origin[2]); + //glass->takedamage = DAMAGE_YES; + //glass->die = kill_door; + //glass->health = 0; + //glass->max_health = 0; + + // Explode the glass + T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } + + /* + // iterate over all func_explosives + while ((glass = G_Find(glass, FOFS(classname), "func_explosive")) != NULL) + { + // glass is broken if solid != SOLID_BSP + if (glass->solid != SOLID_BSP) + { + T_Damage(glass, glass, glass, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } + */ +} + +void kill_door(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, vec3_t point) +{ + G_FreeEdict(self); +} +// Removes all door types +void Remove_All_Doors(void) +{ + edict_t* door; + for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) + { + if (!door) + continue; + if (door->solid == SOLID_NOT) + continue; + if (!door->classname) + continue; + + if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) + { + //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); + door->takedamage = DAMAGE_YES; + door->die = kill_door; + door->health = 0; + door->max_health = 0; + + // Open up the door's area portal (otherwise players will see hall-of-mirrors effect + edict_t* t = NULL; + if (door->target) + { + while ((t = G_Find(t, FOFS(targetname), door->target)) != NULL) + { + if (Q_stricmp(t->classname, "func_areaportal") == 0) { + gi.SetAreaPortalState(t->style, true); + } + } + } + + // Explode the door + T_Damage(door, door, door, vec3_origin, vec3_origin, vec3_origin, 100000, 1, 0, MOD_UNKNOWN); + } + } +} + +void Open_All_Doors(edict_t* ent) +{ + if (ent != NULL) + { + if (ent->last_door_time > level.framenum - 2.0 * HZ) + return; + else + ent->last_door_time = level.framenum + random() * 2.5 * HZ; // wait! + } + // Open all doors and keep them open + edict_t* door; + for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) + { + if (!door) + continue; + if (door->solid == SOLID_NOT) + continue; + if (!door->classname) + continue; + + if (strcmp(door->classname, "func_door_rotating") == 0 || strcmp(door->classname, "func_door") == 0 || strcmp(door->classname, "func_door_secret") == 0) + { + //gi.dprintf("Found door at location %f %f %f\n", door->s.origin[0], door->s.origin[1], door->s.origin[2]); + + int DOOR_TOGGLE = 32; //#define DOOR_TOGGLE 32 + if ((door->spawnflags & DOOR_TOGGLE) == 0) + door->spawnflags |= DOOR_TOGGLE; // Bitwise add DOOR_TOGGLE to door->spawnflags + door->wait = -1; + door->speed = 9000; + door->touch_debounce_framenum = 0; + door->moveinfo.accel = 9000; + door->moveinfo.decel = 9000; + door->moveinfo.speed = 9000; + //door_use(door, ent, ent); // Open the door + } + } +} + +// Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER +#define MAX_LADDER_FACES 3072 +vec3_t surface_ladder_faces[MAX_LADDER_FACES] = { 0 }; // Origin +void ACEND_FindEdgeLadders(void) +{ + int f, e, f2, f3, i; + trace_t tr; + //vec3_t zero = { 0 }; + // Player when standing + //vec3_t player_standing_mins_up = { -16, -16, -1 }; + //vec3_t player_standing_mins = { -16, -16, -0 }; + //vec3_t player_standing_maxs = { 16, 16, 32 }; + + + //vec3_t *ladder_faces = (vec3_t*)malloc(sizeof(vec3_t) * MAX_LADDER_FACES); + //if (ladder_faces == NULL) + { + //Com_Printf("%s [CONTENTS_LADDER] ladder_faces malloc failed\n", __func__); + //return; + } + //vec3_t* ladder_face; + + //short int ladder_index[MAX_LADDER_FACES] = { 0 }; // Face + int* ladder_index = malloc(sizeof(int) * MAX_LADDER_FACES); + if (ladder_index == NULL) + { + Com_Printf("%s [CONTENTS_LADDER] ladder_index malloc failed\n", __func__); + return; + } + + + int num_ladder_faces = 0; + qboolean foundLadder = true; + qboolean ladderLimit = false; + + /* + for (f = 0; f < nmesh.total_faces; f++) + { + if (nmesh.face[f].contents & CONTENTS_LADDER) + { + Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + } + } + */ + + // Find all the ladder faces + for (f = 0; f < nmesh.total_faces; f++) + { + if (ladderLimit) break; + + //if (nmesh.face[f].contents & CONTENTS_LADDER) + // Com_Printf("%s face:%i tris:%i contents:0x%x (LADDER)\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + + if (nmesh.face[f].type == FACETYPE_WALL && nmesh.face[f].contents & CONTENTS_LADDER) // If wall is Ladder + { + for (e = 0; e < nmesh.face[f].num_edges; e++) // Find the bottom of the ladder using the middle of the lowest edge + { + // Look for horizontal edges - check if the two edge verts are level + if (nmesh.face[f].edge[e].v[0][2] == nmesh.face[f].edge[e].v[1][2]) + { + // Check if we already added the ladder face to ladder_faces[] + foundLadder = true; + for (f3 = 0; f3 < num_ladder_faces; f3++) + //for (f3 = 0, ladder_face = ladder_faces; f3 < num_ladder_faces; f3++, ladder_face++) + { + if (surface_ladder_faces[f3][0] == nmesh.face[f].edge[e].center[0] && surface_ladder_faces[f3][1] == nmesh.face[f].edge[e].center[1] && surface_ladder_faces[f3][2] == nmesh.face[f].edge[e].center[2]) + //if (*(ladder_face)[0] == nmesh.face[f].edge[e].center[0] && *(ladder_face)[1] == nmesh.face[f].edge[e].center[1] && *(ladder_face)[2] == nmesh.face[f].edge[e].center[2]) + { + //Com_Printf("%s f:%i e:%i\n", __func__, f, e); + foundLadder = false; + break; + } + } + + // Found new ladder edge + if (foundLadder) + { + // Check we're not over the limit + if (num_ladder_faces + 2 > MAX_LADDER_FACES) + { + Com_Printf("%s [CONTENTS_LADDER] num_ladder_faces > max_faces\n", __func__); + ladderLimit = true; + break; + } + + //Com_Printf("%s [CONTENTS_LADDER] f:%i e:%d n:%d [%f %f %f]\n", __func__, f, e, nmesh.face[f].edge[e].node, nmesh.face[f].edge[e].center[0], nmesh.face[f].edge[e].center[1], nmesh.face[f].edge[e].center[2]); + + //ladder_index[num_ladder_faces] = f; // Copy face + VectorCopy(nmesh.face[f].edge[e].center, surface_ladder_faces[num_ladder_faces]); // Copy origin + + *(ladder_index + num_ladder_faces) = f; // Copy face + + //Com_Printf("%s ladder_index:%i f:%i\n", __func__, *(ladder_index + num_ladder_faces), f); + + //for (int i = 0; i < 3; i++) + // *(ladder_faces + num_ladder_faces * sizeof(float))[i] = nmesh.face[f].edge[e].center[i]; // Copy origin + // + //ladder_face = ladder_faces + (num_ladder_faces * sizeof(vec3_t)); // Set pointer to end of array + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[0] = nmesh.face[f].edge[e].center[0]; + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[1] = nmesh.face[f].edge[e].center[1]; + //*(ladder_faces + (num_ladder_faces + sizeof(vec3_t)))[2] = nmesh.face[f].edge[e].center[2]; + + num_ladder_faces++; + } + } + } + } + } + + + + + for (f = 0; f < num_ladder_faces; f++) + //for (f = 0, ladder_face = ladder_faces; f < num_ladder_faces; f++, ladder_face++) + { + // Com_printf ladder_faces + //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, ladder_index[f], ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2]); + //Com_Printf("%s [CONTENTS_LADDER] f:%i [%f %f %f]\n", __func__, *(ladder_index + f), *(ladder_face)[0], *(ladder_face)[1], *(ladder_face)[2]); + } + + + + + + // Search all ladder faces looking for the lowest and highest parts of the ladder + // + // |-| <-- highest + // |-| + // |-| <-- lowest + // + qboolean tested = false; + int faces_tested_num = 0; + //short int faces_tested[MAX_LADDER_FACES]; + int* faces_tested = malloc(sizeof(int) * MAX_LADDER_FACES); + if (faces_tested == NULL) + { + Com_Printf("%s [CONTENTS_LADDER] faces_tested malloc failed\n", __func__); + return; + } + + + //vec3_t ladder_final[MAX_LADDER_FACES]; // Origin + //int ladder_final_counter = 0; + float low_z, high_z; // Height of lowest and highest parts of ladder + int low_f, high_f; // Face index of lowest and highest parts of ladder + for (f = 0; f < num_ladder_faces; f++) + { + low_z = 99999, high_z = -99999; // Reset lowest and highest height + low_f = high_f = INVALID; // Reset lowest and highest face index + + tested = false; + for (int ft = 0; ft < faces_tested_num; ft++) + { + //if (faces_tested[ft] == f) + if (*(faces_tested + ft) == f) + { + tested = true; // Skip if already tested + break; + } + } + if (tested) + continue; + + for (f2 = 0; f2 < num_ladder_faces; f2++) + { + // Check if the next ladder has the same X and Y alignment + if (surface_ladder_faces[f][0] == surface_ladder_faces[f2][0] && surface_ladder_faces[f][1] == surface_ladder_faces[f2][1]) + //if (*(ladder_faces + f)[0] == *(ladder_faces + f2)[0] && *(ladder_faces + f)[1] == *(ladder_faces + f2)[1]) + { + //Com_Printf("%s [F %i] [F %i] [%f %f %f] [%f %f %f] \n", __func__, f, f2, ladder_faces[f][0], ladder_faces[f][1], ladder_faces[f][2], ladder_faces[f2][0], ladder_faces[f2][1], ladder_faces[f2][2]); + + //faces_tested[faces_tested_num++] = f2; // Add face to faces_tested + *(faces_tested + faces_tested_num++) = f2; // Add face to faces_tested + + + + if (surface_ladder_faces[f2][2] < low_z) + //if (*(ladder_faces + f2)[2] < low_z) + { + //Com_Printf("%s [F %i] [ladder_faces[f2][2] < low_z] %f < %f \n", __func__, f2, ladder_faces[f2][2], low_z); + low_z = surface_ladder_faces[f2][2]; // Update lowest height + //low_z = *(ladder_faces + f2)[2]; // Update lowest height + low_f = f2; // Update lowest face index + } + if (surface_ladder_faces[f2][2] > high_z) + //if (*(ladder_faces + f2)[2] > high_z) + { + //Com_Printf("%s [F %i] [ladder_faces[f2][2] > high_z] %f > %f \n", __func__, f2, ladder_faces[f2][2], high_z); + high_z = surface_ladder_faces[f2][2]; // Update highest height + //high_z = *(ladder_faces + f2)[2]; // Update highest height + high_f = f2; // Update highest face index + } + } + } + + foundLadder = false; + + float distance = 0; + if (surface_ladder_faces[high_f][2] > surface_ladder_faces[low_f][2]) // Make sure the top of the ladder is higher than the bottom + distance = surface_ladder_faces[high_f][2] - surface_ladder_faces[low_f][2]; + + if (distance >= 32 && low_f != INVALID && high_f != INVALID) + { + //Com_Printf("%s LO F:%i L:%f [F %i][%f %f %f]\n", __func__, low_f, low_z, ladder_index[low_f], ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + //Com_Printf("%s HI F:%i H:%f [F %i][%f %f %f]\n", __func__, high_f, high_z, ladder_index[high_f], ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); + + + if (1) // Project the top of the ladder forward, then we test to see if its in the ground, if so we need to move the top node up + { + vec3_t ladder_forward; + VectorCopy(surface_ladder_faces[high_f], ladder_forward); + //ladder_forward[2] += NODE_Z_HEIGHT; + // Move the top ladder node closer to the ladder + int f3 = *(ladder_index + high_f); + //Com_Printf("%s f3:%i\n", __func__, f3); + if (nmesh.face[f3].normal[0] != 0) // X + { + if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) + ladder_forward[0] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // -X (forward wall) + else + ladder_forward[0] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[0]); // +X (back wall) + } + if (nmesh.face[f3].normal[1] != 0) // Y + { + if (nmesh.face[f3].drawflags & DSURF_PLANEBACK) + ladder_forward[1] += (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // -Y (left wall) + else + ladder_forward[1] -= (NODE_Z_HEIGHT * nmesh.face[f3].normal[1]); // +Y (right wall) + } + // Test if we're in the ground + for (i = 0; i < 100; i++) //NODE_Z_HEIGHT + { + tr = gi.trace(ladder_forward, tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), ladder_forward, g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1) + { + ladder_forward[2]++; + } + else + break; + } + + //Com_Printf("%s Increased top of ladder [%f %f %f] -> [%f %f %f]\n", __func__, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2], ladder_forward[0], ladder_forward[1], ladder_forward[2]); + surface_ladder_faces[high_f][2] = ladder_forward[2]; + } + + // Move the top of the ladder up before testing, this serves two purposes: test if ladder hits roof, make sure ladder reaches top + //ladder_faces[high_f][2] += NODE_Z_HEIGHT + 8; + //ladder_faces[high_f][2] += NODE_Z_HEIGHT; + + if (1) + // Test if bottom of ladder is in the ground, move it up until its not + for (i = 0; i < 100; i++) //NODE_Z_HEIGHT + { + tr = gi.trace(surface_ladder_faces[low_f], tv(-16, -16, -NODE_Z_HEIGHT), tv(16, 16, 0), surface_ladder_faces[low_f], g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid || tr.fraction < 1) + { + if (surface_ladder_faces[low_f][2] + 1 < surface_ladder_faces[high_f][2]) + surface_ladder_faces[low_f][2]++; + } + else + break; + } + + //if (ladder_faces[low_f][2] + NODE_Z_HEIGHT < ladder_faces[high_f][2]) // Make sure bottom ladder doesn't go above top of ladder + // ladder_faces[low_f][2] += NODE_Z_HEIGHT; + + + + + + + // Check if ladder ends can be connected, if so add nodes and links + //tr = gi.trace(ladder_faces[low_f], player_standing_mins_up, player_standing_maxs, ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); + tr = gi.trace(surface_ladder_faces[low_f], MINS_PLAYER_STANDING, MAXS_PLAYER_STANDING, surface_ladder_faces[high_f], g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + { + //ACEND_AutoAddNode(*(ladder_faces + low_f), nmesh.face[low_f].normal, NODE_LADDER_DOWN); + //ACEND_AutoAddNode(*(ladder_faces + high_f), nmesh.face[high_f].normal, NODE_LADDER_UP); + + int tn = INVALID; // Top node + int bn = INVALID; // Bottom node + + // Add Top ladder node + // ---------------------- + // Top ladder doesn't have to worry about existing nodes, so just make a new one + DAIC_Add_Node(surface_ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); + //ACEND_AutoAddNode(ladder_faces[high_f], nmesh.face[high_f].normal, NODE_LADDER_UP); + tn = numnodes - 1; + // ---------------------- + + // Add bottom ladder node + // ---------------------- + // Look for an existing node located on the triangle where the bottom of the ladder is, + // if so then move that node origin to the bottom ladder origin + qboolean found = false; + int f = INVALID; + int t = INVALID; + //vec3_t ladder_low; + //VectorCopy(ladder_faces[low_f], ladder_low); + //ladder_low[2] -= 24; + //GetBSPFaceTriangle(ladder_low, &found, &f, &t); + GetBSPFaceTriangle(surface_ladder_faces[low_f], &found, &f, &t); + if (found && f != INVALID && t != INVALID) //f = 0; t = 0; found = true; + { + found = false; + bn = nmesh.face[f].tris[t].node; + if (bn != INVALID) // Successfully found node + { + //Com_Printf("%s ladder tris found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + + found = true; + VectorCopy(surface_ladder_faces[low_f], nodes[bn].origin); // Move node origin to bottom ladder origin + nodes[bn].type = NODE_LADDER_DOWN; // Update its type + } + } + // Otherwise we add a new node for the bottom of the ladder + if (found == false) + { + //Com_Printf("%s ladder tris not found [%f %f %f]\n", __func__, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + DAIC_Add_Node(surface_ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); + //ACEND_AutoAddNode(ladder_faces[low_f], nmesh.face[low_f].normal, NODE_LADDER_DOWN); + bn = numnodes - 1; + } + // ---------------------- + + //Com_Printf("%s NODE_LADDER_DOWN f:%i n:%d [%f %f %f]\n", __func__, ladder_index[low_f], bn, ladder_faces[low_f][0], ladder_faces[low_f][1], ladder_faces[low_f][2]); + //Com_Printf("%s NODE_LADDER_UP f:%i n:%d [%f %f %f]\n", __func__, ladder_index[high_f], tn, ladder_faces[high_f][0], ladder_faces[high_f][1], ladder_faces[high_f][2]); + + if (bn != INVALID && tn != INVALID) + { + BOTLIB_AddNodeLink(bn, tn, NODE_LADDER_DOWN, true); // Bottom to top + BOTLIB_AddNodeLink(tn, bn, NODE_LADDER_UP, true); // Top to bottom + ACEND_UpdateNodeReach(bn, tn); + ACEND_UpdateNodeReach(tn, bn); + nmesh.total_reach += 2; + } + } + } + } + + // Free memory + free(faces_tested); + faces_tested = NULL; +} + +/* +// Find if origin is touching a ladder +// Can be used to see if node is touching a ladder +qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top) +{ + float yaw_rad = 0, yaw = 0, depth; + int loops; + vec3_t fwd = { 0 }, start = { 0 }, end = { 0 }, final = { 0 }, ground = { 0 }; + vec3_t mins = { -4, -4, -24 }, maxs = { 4, 4, 32 }; + trace_t tr; + qboolean is_touching = false, found_bottom = false; + VectorCopy(origin, start); + + // Player when standing + vec3_t player_standing_mins = { -16, -16, -0 }; + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing smaller mins + vec3_t player_standing_smaller_mins = { -8, -8, -0 }; + vec3_t player_standing_smaller_maxs = { 8, 8, 0 }; + + return false; //////////////////////////////////////////////////////////////////////////////////////////// TEMP DISABLE LADDERS + + depth = 0; + for (int d = 0; d < 128; d++) // depth + { + // For each yaw angle, project forward and see if touching a ladder + for (int a = 0; a < 64; a++) // Spin around in a 360 circle + { + if (yaw >= 360) // Make sure we come back around + yaw = 0; + + yaw_rad = DEG2RAD(yaw); + fwd[0] = cos(yaw_rad); + fwd[1] = sin(yaw_rad); + VectorMA(start, depth, fwd, end); + VectorMA(start, (depth - 20), fwd, final); // Back away from the ladder until we're about the distance of the player center (give or take a little) + tr = gi.trace(start, mins, maxs, end, g_edicts, MASK_PLAYERSOLID); + //if (tr.fraction < 1 && (tr.contents & CONTENTS_LADDER)) + if (tr.contents & CONTENTS_LADDER) + { + // Find the bottom of the ladder + //-------------------- + // Now find the ground so we know not to go below this point + VectorCopy(end, ground); + ground[2] -= 8192; + tr = gi.trace(ladder_top, NULL, NULL, ground, g_edicts, MASK_PLAYERSOLID); + if (tr.fraction < 1) + { + VectorCopy(tr.endpos, ground); + } + loops = 0; + while (loops++ < 64) + { + // Now move down testing until we no longer hit the ladder to find its bottom + if (end[2] + 32 > ground[2]) // Can we move down? + { + // Okay move down + start[2] -= 32; + end[2] -= 32; + final[2] -= 32; + + tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) == 0) + { + // Ladder reached the ground so adjust the position so we're not stuck in the ground + if (tr.startsolid) + { + start[2] = tr.endpos[2] + NODE_Z_HEIGHT; + end[2] = tr.endpos[2] + NODE_Z_HEIGHT; + final[2] = tr.endpos[2] + NODE_Z_HEIGHT; + } + else + { + // Hanging ladder, didn't reach the ground + } + VectorCopy(final, ladder_bottom); + Com_Printf("Found bottom ladder at location %f %f %f\n", ladder_bottom[0], ladder_bottom[1], ladder_bottom[2]); + found_bottom = true; + break; // end while loop + } + } + } + //-------------------- + + + // Find the top of the ladder + //-------------------- + if (found_bottom) + { + // Now move up testing until we no longer hit the ladder to find its top + loops = 0; + while (loops++ < 64) + { + // move up + start[2] += 32; + end[2] += 32; + final[2] += 32; + + tr = gi.trace(start, player_standing_smaller_mins, player_standing_smaller_maxs, end, g_edicts, MASK_PLAYERSOLID); + if ((tr.contents & CONTENTS_LADDER) == 0) + { + final[2] += NODE_Z_HEIGHT; // Adjust the final height above the top of the ladder + VectorCopy(final, ladder_top); // Ladder top + + // Make sure the ladder z height length is large enough + // edge cases: where ladder handles go above the ground we're trying to reach, such as urban2 the very highest ladder + float difference = ladder_top[2] - ladder_bottom[2]; + if (difference > 96) + { + Com_Printf("Found top ladder at location %f %f %f\n", ladder_top[0], ladder_top[1], ladder_top[2]); + is_touching = true; + return is_touching; + } + } + } + } + //-------------------- + + if (is_touching) + break; + } + yaw += 5.625; // Change yaw angle + } + depth += 2; + } + + return is_touching; +} +*/ + +//rekkie -- walknodes -- s + + +void BOTLIB_SetAllNodeNormals() +{ + vec3_t mins = { -14, -14, -4, }; + vec3_t maxs = { 14, 14, 4, }; + + for (int i = 0; i < numnodes; i++) + { + // Get normal + trace_t tr = gi.trace(tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] + 30), mins, maxs, tv(nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2] - 64), NULL, MASK_SHOT); + if (tr.fraction == 1.0) + { + //Com_Printf("%s failed node[%d] normal [%f %f %f]\n", __func__, nodes[i].nodenum, tr.plane.normal[0], tr.plane.normal[1], tr.plane.normal[2]); + continue; + } + + VectorCopy(tr.plane.normal, nodes[i].normal); + } +} + +// Compression -- s +#if USE_ZLIB +#include + +#define CHUNK_SIZE 16384 + +void BOTLIB_DecompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) +{ + z_stream stream = { 0 }; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + int status = inflateInit(&stream); + if (status != Z_OK) { + Com_Printf("Failed to initialize decompression stream! (error %d)\n", status); + return; + } + + stream.avail_in = inputLength; + stream.next_in = (Bytef*)inputBuffer; + stream.avail_out = *outputLength; + stream.next_out = (Bytef*)outputBuffer; + + status = inflate(&stream, Z_FINISH); + if ((status != Z_OK) && (status != Z_STREAM_END)) { + Com_Printf("Failed to decompress data! (error %d)\n", status); + inflateEnd(&stream); + return; + } + + *outputLength = stream.total_out; + inflateEnd(&stream); +} +void BOTLIB_CompressBuffer(char* inputBuffer, long inputLength, char* outputBuffer, long* outputLength) { + z_stream stream = { 0 }; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + + int status = deflateInit(&stream, Z_BEST_COMPRESSION); + if (status != Z_OK) { + Com_Printf("Failed to initialize compression stream! (error %d)\n", status); + return; + } + + stream.avail_in = inputLength; + stream.next_in = (Bytef*)inputBuffer; + stream.avail_out = inputLength; // *outputLength; + stream.next_out = (Bytef*)outputBuffer; + + status = deflate(&stream, Z_FINISH); + if ((status != Z_OK) && (status != Z_STREAM_END)) { + Com_Printf("Failed to compress data! (error %d)\n", status); + deflateEnd(&stream); + return; + } + + *outputLength = stream.total_out; + + deflateEnd(&stream); +} + +// Assume the following variables are declared on the other end: +// char* buffer; // Pointer to the outgoing buffer to add data to +// int bufferLength; // Length of the outgoing buffer +// int dataLength; // Length of the incoming data +// char* incomingData; // Pointer to the incoming data to add +void BOTLIB_AddToBuffer(char* incomingData, int dataLength, char** bufferPtr, int* bufferLengthPtr) +{ + if (*bufferPtr == NULL) + { // If the buffer is null, set the buffer to the incoming data ) + char *newBuffer = (char*)malloc(*bufferLengthPtr + dataLength); + if (newBuffer != NULL) + { // If the allocation was successful, copy the incoming data to the end of the buffer + memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); + *bufferPtr = newBuffer; + *bufferLengthPtr += dataLength; + } else { // If the allocation was not successful, handle the error + Com_Printf("Failed to allocate buffer for incoming data!\n"); + } + } + else + { + // If the buffer is not null, reallocate the buffer to the new size + char *newBuffer = (char*)realloc(*bufferPtr, *bufferLengthPtr + dataLength); + if (newBuffer != NULL) + { // If the allocation was successful, copy the incoming data to the end of the buffer + memcpy(newBuffer + (*bufferLengthPtr), incomingData, dataLength); + *bufferPtr = newBuffer; + *bufferLengthPtr += dataLength; + } else { // If the allocation was not successful, handle the error + Com_Printf("Failed to allocate buffer for incoming data!\n"); + } + } +} + + +// ============================================================================================ +// These group of functions sort the node links from highest to lowest based on their zheight +// difference from the node they're connected to. node -> link(s) zheight difference. We run +// this function upon saving nodes. +// +// This is done so that when pathing the highest node links are consindered first. Although, +// currently the bots use Dijkstra Pathing, so this is ignored for now. +// ============================================================================================ +// Temp link structure +typedef struct { + int targetNode; + int targetNodeType; + double cost; + double zheight; // Not part of the original link struct. Used by qsort() to order highest -> lowest +} QSLink; // Qsorted Link + +// Compare function for qsort() +static int QScompare(const void* a, const void* b) { + return ((QSLink*)b)->zheight - ((QSLink*)a)->zheight; +} + +void QsortLinks(QSLink* links, size_t length) { + qsort(links, length, sizeof(*links), QScompare); +} + +// Sorts node links by height. Highest to lowest. +void QSNodeLinksByHeight(void) +{ + QSLink qslinks[MAXLINKS]; // Temp links + + for (int n = 0; n < numnodes; n++) // Each node + { + for (int i = 0; i < nodes[n].num_links; ++i) // Each link + { + // Copy original link data into our temp struct + qslinks[i].targetNode = nodes[n].links[i].targetNode; + qslinks[i].targetNodeType = nodes[n].links[i].targetNodeType; + qslinks[i].cost = nodes[n].links[i].cost; + + // The original data struct doesn't use or contain the zheight of a linked node, so we create it here for qsort() to use, + int tnode = nodes[n].links[i].targetNode; // Target node + // + if ((nodes[tnode].origin[2] - nodes[n].origin[2]) >= 1) // Only sort linked nodes that are higher than the node itself and are >= 1 in height difference + qslinks[i].zheight = (nodes[tnode].origin[2] - nodes[n].origin[2]); + else + qslinks[i].zheight = 0; // Any height < 1 just consider as 0 height + } + + // Debug print + /* + qboolean hasDiffHeight = false; + for (int i = 0; i < nodes[n].num_links; ++i) + { + if (nodes[n].links[i].zheight >= 1) // Only if height diff is >= 1 + { + hasDiffHeight = true; + break; + } + } + */ + + // Debug print + /* + if (0 && hasDiffHeight) + { + Com_Printf("\nBefore sorting:\n"); + for (int i = 0; i < nodes[n].num_links; ++i) + Com_Printf("{%d %d %f %f}\n", nodes[n].links[i].targetNode, nodes[n].links[i].targetNodeType, nodes[n].links[i].cost, nodes[n].links[i].zheight); + } + */ + + // Sort links from highest to lowest + QsortLinks(qslinks, nodes[n].num_links); + + // Debug print + /* + if (0 && hasDiffHeight) // Debug print + { + Com_Printf("\nAfter sorting:\n"); + for (int i = 0; i < nodes[n].num_links; ++i) + Com_Printf("{%d %d %f %f}\n", qslinks[i].targetNode, qslinks[i].targetNodeType, qslinks[i].cost, qslinks[i].zheight); + } + */ + + // Override and store the original link data + for (int i = 0; i < nodes[n].num_links; ++i) + { + nodes[n].links[i].targetNode = qslinks[i].targetNode; + nodes[n].links[i].targetNodeType = qslinks[i].targetNodeType; + nodes[n].links[i].cost = qslinks[i].cost; + } + } +} +// ============================================== +// ============================================== + +void BOTLIB_SaveNavCompressed(void) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, n, l; // File, nodes, links + int version = BOT_NAV_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing compressed NAV to file...\n", __func__); + + QSNodeLinksByHeight(); // Sort node links by height, highest to lowest + + char* buff = NULL; + int uncompressed_buff_len = 0; + BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &uncompressed_buff_len); + for (n = 0; n < numnodes; n++) + { + // Nodes + BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &uncompressed_buff_len); + //BOTLIB_AddToBuffer((char*)&nodes[n].normal, sizeof(vec3_t), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &uncompressed_buff_len); + + // Links + BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &uncompressed_buff_len); + for (l = 0; l < nodes[n].num_links; l++) + { + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &uncompressed_buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &uncompressed_buff_len); + } + + // Paths + //for (p = 0; p < numnodes; p++) + //{ + //BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &uncompressed_buff_len); // Path table + //} + } + + // Compress the buffer + long compressed_buff_len = 0; + char* compressed_buff = (char*)malloc(uncompressed_buff_len); // Make the compressed buffer as large as the uncompressed buffer + if (compressed_buff != NULL) + { + BOTLIB_CompressBuffer(buff, uncompressed_buff_len, compressed_buff, &compressed_buff_len); + + // Compare and print the size of the buffers + //Com_Printf("%s uncompressed_buff_len:%i compressed_buff_len:%i\n", __func__, uncompressed_buff_len, compressed_buff_len); + + // Write version, checksum, and buffer sizes + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // NAV Version + //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + fileSize += sizeof(int) * fwrite(&uncompressed_buff_len, sizeof(int), 1, fOut); // Uncompressed buffer size + fileSize += sizeof(int) * fwrite(&compressed_buff_len, sizeof(int), 1, fOut); // Compressed buffer size + + // Write compressed node data + fwrite(compressed_buff, compressed_buff_len, 1, fOut); + fileSize += compressed_buff_len; + } + else + { + Com_Printf("%s Failed to allocate memory for compressed nav buffer\n", __func__); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + fclose(fOut); + + if (buff != NULL) + free(buff); + + if (compressed_buff != NULL) + free(compressed_buff); + + Com_Printf("%s saved %s containing %d nodes. Nav zlib compressed [%i bytes] to [%i bytes]\n", __func__, filename, numnodes, uncompressed_buff_len, fileSize); +} +void BOTLIB_LoadNavCompressed(void) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + int f, n, l; // File, nodes, links + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + const vec3_t mins = { -16, -16, -24 }; + const vec3_t maxs = { 16, 16, 32 }; + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists + { + return; // No file + } + else + { + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version < BOT_NAV_VERSION_1 || version > BOT_NAV_VERSION) + { + Com_Printf("%s ERROR: NAV file version mismatch. Got %d, expected value between %d and %d\n", __func__, version, BOT_NAV_VERSION_1, BOT_NAV_VERSION); + fclose(fIn); // Close the file + return; + } + /* + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + fclose(fIn); // Close the file + return; + } + */ + } + + // Init Nodes + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + //memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + Remove_All_Breakableglass(); + + Com_Printf("%s Reading NAV file... detected version [%d]\n", __func__, version); + + // Read Compressed and uncompressed buffer sizes + long uncompressed_buff_len = 0; + fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size + long compressed_buff_len = 0; + fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size + + // Read compressed buffer + char* uncompressed_buff = (char*)malloc(uncompressed_buff_len); + char* compressed_buff = (char*)malloc(compressed_buff_len); + if (compressed_buff != NULL && uncompressed_buff != NULL) + { + fileSize += compressed_buff_len * fread(compressed_buff, compressed_buff_len, 1, fIn); // Compressed buffer + + BOTLIB_DecompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); + //Com_Printf("%s compressed_buff_len:%i uncompressed_buff_len:%i\n", __func__, compressed_buff_len, uncompressed_buff_len); + + // Read the uncompressed buffer + { + int buff_read = 0; + + // Total nodes + memcpy(&numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); + return; + } + + // Alloc nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + // Total areas + //nav_area.total_areas = 0; + + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, version, numnodes); + + // Read node data + for (n = 0; n < numnodes; n++) + { + if (version > BOT_NAV_VERSION_1) + { + // Area/chunk/group this node belongs to -- optimise and diversify bot pathing + memcpy(&nodes[n].area, uncompressed_buff + buff_read, sizeof(int)); // Node area + buff_read += sizeof(int); + + nodes[n].area_color = 0; + } + //VectorClear(nodes[n].normal); + //if (version > BOT_NAV_VERSION_2) + //{ + //memcpy(&nodes[n].normal, uncompressed_buff + buff_read, sizeof(vec3_t)); // Surface normal + //buff_read += sizeof(vec3_t); + //} + + nodes[n].weight = 0; // Used to help diversify bot pathing + + memcpy(&nodes[n].origin, uncompressed_buff + buff_read, sizeof(vec3_t)); // Location + buff_read += sizeof(vec3_t); + memcpy(&nodes[n].type, uncompressed_buff + buff_read, sizeof(byte)); // Node type + buff_read += sizeof(byte); + + VectorCopy(mins, nodes[n].mins); // Box mins + VectorCopy(maxs, nodes[n].maxs); // Box maxs + if (nodes[n].type == NODE_CROUCH) + nodes[n].maxs[2] = CROUCHING_MAXS2; + else if (nodes[n].type == NODE_BOXJUMP) + { + nodes[n].mins[0] = -8; + nodes[n].mins[1] = -8; + nodes[n].mins[2] = -12; + nodes[n].maxs[0] = 8; + nodes[n].maxs[1] = 8; + nodes[n].maxs[2] = 16; + } + VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world + + memcpy(&nodes[n].nodenum, uncompressed_buff + buff_read, sizeof(short int)); // Node number + buff_read += sizeof(short int); + memcpy(&nodes[n].inuse, uncompressed_buff + buff_read, sizeof(qboolean)); // Node inuse + buff_read += sizeof(qboolean); + + // Init MAXLINKS links + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Read the num_links + memcpy(&nodes[n].num_links, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + //Com_Printf("%s origin:%f %f %f type:%i nodenum:%i inuse:%i num_links:%i\n", __func__, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2], nodes[n].type, nodes[n].nodenum, nodes[n].inuse, nodes[n].num_links); + + for (l = 0; l < nodes[n].num_links; l++) // + { + memcpy(&nodes[n].links[l].targetNode, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + memcpy(&nodes[n].links[l].targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + memcpy(&nodes[n].links[l].cost, uncompressed_buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + //Com_Printf("%s targetNode:%i targetNodeType:%i cost:%f\n", __func__, nodes[n].links[l].targetNode, nodes[n].links[l].targetNodeType, nodes[n].links[l].cost); + } + + // Paths + //for (p = 0; p < numnodes; p++) + //{ + //memcpy(&path_table[n][p], uncompressed_buff + buff_read, sizeof(short int)); // Path table + //buff_read += sizeof(short int); + //Com_Printf("%s path_table[%i][%i]:%i\n", __func__, n, p, path_table[n][p]); + //} + } + } + } + + // Free memory + if (compressed_buff != NULL) + free(compressed_buff); + if (uncompressed_buff != NULL) + free(uncompressed_buff); + + // Fix any dangling or unusual links + if (numnodes) + { + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node + + if (nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node + + /* + for (int l = 0; l < nodes[i].num_links; l++) + { + int tnode = nodes[i].links[l].targetNode; + + if (nodes[tnode].origin[0] == 0 && nodes[tnode].origin[1] == 0 && nodes[tnode].origin[2] == 0) + { + BOTLIB_RemoveAllNodeLinksFrom(nodes[tnode].nodenum); // Remove all links to and from this node + } + } + */ + } + } + + fclose(fIn); + + Com_Printf("%s loaded %s containing %d nodes.\n", __func__, filename, numnodes); + + // Update old versions to new format + if (version == BOT_NAV_VERSION_1) + { + for (int i = 0; i < numnodes; i++) + { + nodes[i].area = 0; + } + } + // Save changes to file + if (version < BOT_NAV_VERSION) + { + Com_Printf("%s Updating NAV file to new version [%d]\n", __func__, BOT_NAV_VERSION); + BOTLIB_SaveNavCompressed(); + } + + //BOTLIB_SetAllNodeNormals(); // Set all the normals + + // Init Areas (must be done after nodes are loaded) + if (numnodes) + { + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + +} +#endif + +void BOTLIB_SaveNav(void) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, n, l, p; // File, nodes, links, paths + int version = BOT_NAV_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write NAV to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing NAV to file...\n", __func__); + +#if 0 +#ifdef USE_ZLIB + char* buff = NULL; + int buff_len = 0; + //BOTLIB_AddToBuffer((char*)&version, sizeof(int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&numnodes, sizeof(unsigned short), &buff, &buff_len); + for (n = 0; n < numnodes; n++) + { + // Nodes + BOTLIB_AddToBuffer((char*)&nodes[n].area, sizeof(int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].origin, sizeof(vec3_t), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].type, sizeof(byte), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].nodenum, sizeof(short int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].inuse, sizeof(qboolean), &buff, &buff_len); + + // Links + BOTLIB_AddToBuffer((char*)&nodes[n].num_links, sizeof(byte), &buff, &buff_len); + for (l = 0; l < nodes[n].num_links; l++) // + { + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNode, sizeof(short int), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].targetNodeType, sizeof(byte), &buff, &buff_len); + BOTLIB_AddToBuffer((char*)&nodes[n].links[l].cost, sizeof(float), &buff, &buff_len); + } + + // Paths + for (p = 0; p < numnodes; p++) + { + BOTLIB_AddToBuffer((char*)&path_table[n][p], sizeof(short int), &buff, &buff_len); // Path table + } + } + // Compress the buffer + int compressed_buff_len = 0; + char* compressed_buff = (char*)malloc(buff_len); + if (compressed_buff != NULL) + { + //memset(compressed_buff, 0, buff_len); + compressBuffer(buff, buff_len, compressed_buff, &compressed_buff_len); + // Compare and print the size of the buffers + Com_Printf("%s buff_len:%i compressed_buff_len:%i\n", __func__, buff_len, compressed_buff_len); + + int uncompressed_buff_len = buff_len; + char* uncompressed_buff = (char*)malloc(buff_len); + decompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); + Com_Printf("%s uncompressed_buff_len:%i\n", __func__, uncompressed_buff_len); + + if (uncompressed_buff != NULL) + { + // Read the uncompressed version + int buff_read = 0; + //int buff_version = 0; + //memcpy(&buff_version, uncompressed_buff, sizeof(int)); + //buff_read += sizeof(int); + // Read the buffer numnodes + unsigned short buff_numnodes = 0; + //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); + memcpy(&buff_numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); + for (n = 0; n < buff_numnodes; n++) + { + // Read the origin + vec3_t buff_origin = { 0 }; + memcpy(&buff_origin, uncompressed_buff + buff_read, sizeof(vec3_t)); + buff_read += sizeof(vec3_t); + // Read the type + byte buff_type = 0; + memcpy(&buff_type, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the nodenum + short int buff_nodenum = 0; + memcpy(&buff_nodenum, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the inuse + qboolean buff_inuse = false; + memcpy(&buff_inuse, uncompressed_buff + buff_read, sizeof(qboolean)); + buff_read += sizeof(qboolean); + // Read the num_links + byte buff_num_links = 0; + memcpy(&buff_num_links, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); + + for (l = 0; l < buff_num_links; l++) // + { + // Read the targetNode + short int buff_targetNode = 0; + memcpy(&buff_targetNode, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the targetNodeType + byte buff_targetNodeType = 0; + memcpy(&buff_targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the cost + float buff_cost = 0; + memcpy(&buff_cost, uncompressed_buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); + break; + } + for (p = 0; p < buff_numnodes; p++) + { + // Read the path_table + short int buff_path_table = 0; + memcpy(&buff_path_table, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); + break; + } + + break; + } + } + + + if (1) + { + // Read the buffer version + int buff_read = 0; + //int buff_version = 0; + //memcpy(&buff_version, buff, sizeof(int)); + //buff_read += sizeof(int); + // Read the buffer numnodes + unsigned short buff_numnodes = 0; + //memcpy(&buff_numnodes, buff + sizeof(int), sizeof(unsigned short)); + memcpy(&buff_numnodes, buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, buff_version, buff_numnodes); + for (n = 0; n < buff_numnodes; n++) + { + // Read the origin + vec3_t buff_origin = { 0 }; + memcpy(&buff_origin, buff + buff_read, sizeof(vec3_t)); + buff_read += sizeof(vec3_t); + // Read the type + byte buff_type = 0; + memcpy(&buff_type, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the nodenum + short int buff_nodenum = 0; + memcpy(&buff_nodenum, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the inuse + qboolean buff_inuse = false; + memcpy(&buff_inuse, buff + buff_read, sizeof(qboolean)); + buff_read += sizeof(qboolean); + // Read the num_links + byte buff_num_links = 0; + memcpy(&buff_num_links, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + Com_Printf("%s buff_origin:%f %f %f buff_type:%i buff_nodenum:%i buff_inuse:%i buff_num_links:%i\n", __func__, buff_origin[0], buff_origin[1], buff_origin[2], buff_type, buff_nodenum, buff_inuse, buff_num_links); + + for (l = 0; l < buff_num_links; l++) // + { + // Read the targetNode + short int buff_targetNode = 0; + memcpy(&buff_targetNode, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + // Read the targetNodeType + byte buff_targetNodeType = 0; + memcpy(&buff_targetNodeType, buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + // Read the cost + float buff_cost = 0; + memcpy(&buff_cost, buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + Com_Printf("%s buff_targetNode:%i buff_targetNodeType:%i buff_cost:%f\n", __func__, buff_targetNode, buff_targetNodeType, buff_cost); + break; + } + for (p = 0; p < buff_numnodes; p++) + { + // Read the path_table + short int buff_path_table = 0; + memcpy(&buff_path_table, buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + Com_Printf("%s buff_path_table:%i\n", __func__, buff_path_table); + break; + } + + break; + } + } + + } + else + { + Com_Printf("%s Failed to allocate memory for compressed buffer\n", __func__); + } + + if (buff != NULL) + free(buff); + + if (compressed_buff != NULL) + free(compressed_buff); + +#endif +#endif + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version + //fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + + // Write node data + fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Node origin + fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type + fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number + fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse + + // Links + fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); + for (l = 0; l < nodes[n].num_links; l++) // + { + fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node + fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type + fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table + } + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} + +void BOTLIB_LoadNav(void) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + int f, n, l, p; // File, nodes, links, paths + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + const vec3_t mins = { -16, -16, -24 }; + const vec3_t maxs = { 16, 16, 32 }; + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".nav"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".nav"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists + { + return; // No file + } + else + { + + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version != BOT_NAV_VERSION) + { + fclose(fIn); // Close the file + return; + } + /* + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + fclose(fIn); // Close the file + return; + } + */ + } + + // Init + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + //memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + Remove_All_Breakableglass(); + + Com_Printf("%s Reading NAV file...\n", __func__); + + fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); + return; + } + + // Main nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + // Read node data + for (n = 0; n < numnodes; n++) + { + nodes[n].weight = 0; // Used to help diversify bot pathing + + // Nodes + fileSize += sizeof(int) * fread(&nodes[n].area, sizeof(int), 1, fIn); // Area + fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type + + VectorClear(nodes[n].normal); // Surface normal + VectorCopy(mins, nodes[n].mins); // Box mins + VectorCopy(maxs, nodes[n].maxs); // Box maxs + if (nodes[n].type == NODE_CROUCH) + nodes[n].maxs[2] = CROUCHING_MAXS2; + else if (nodes[n].type == NODE_BOXJUMP) + { + nodes[n].mins[0] = -8; + nodes[n].mins[1] = -8; + nodes[n].mins[2] = -12; + nodes[n].maxs[0] = 8; + nodes[n].maxs[1] = 8; + nodes[n].maxs[2] = 16; + } + VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world + + nodes[n].weight = 0; // Used to help diversify bot pathing + + fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number + fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse + + // Init MAXLINKS links + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Copy num_links links + fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links + for (l = 0; l < nodes[n].num_links; l++) + { + fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node + fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type + fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table + } + } + + // Init Areas (must be done after nodes are loaded) + if (numnodes) + { + BOTLIB_InitAreaNodes(); + BOTLIB_InitAreaConnections(); + } + + // Cache all the POI nodes + if (numnodes) + { + num_poi_nodes = 0; // Reset any previous POI nodes + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_POI) + { + poi_nodes[num_poi_nodes] = i; + num_poi_nodes++; + } + } + } + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); +} + +//rekkie -- walknodes -- e + +/////////////////////////////////////////////////////////////////////// +// Save to disk file +/////////////////////////////////////////////////////////////////////// +void ACEND_SaveAAS(qboolean update) +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach + int n, l, p; // Nodes, links, paths + int version = BOT_AAS_VERSION; + cvar_t *game_dir = gi.cvar("game", "action", 0); + cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".aas"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".aas"); +#endif + + // If not updating, process from scratch + if (update == false) + { + // Proccess BSP data so we can save it to disk + ACEND_BSP(NULL); + } + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write AAS to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing AAS to file...\n", __func__); + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // AAS Version + fileSize += sizeof(unsigned) * fwrite(&nmesh.bsp_checksum, sizeof(unsigned), 1, fOut); // Map checksum + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_faces, sizeof(unsigned short), 1, fOut); // Total Faces + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_edges, sizeof(unsigned short), 1, fOut); // Total Edges + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_verts, sizeof(unsigned short), 1, fOut); // Total Verticies + fileSize += sizeof(unsigned short) * fwrite(&nmesh.total_tris, sizeof(unsigned short), 1, fOut); // Total Triangles + fileSize += sizeof(int) * fwrite(&nmesh.total_reach, sizeof(int), 1, fOut); // Total Reachabilities + fileSize += sizeof(unsigned short) * fwrite(&numnodes, sizeof(unsigned short), 1, fOut); // Total Nodes + + // Write BSP Data + for (f = 0; f < nmesh.total_faces; f++) + { + // Write face verts + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fOut); + for (v = 0; v < nmesh.face[f].num_verts; v++) + { + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fOut); + } + + // Write edges + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fOut); + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Write edge verts + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fOut); + + // Write edge center + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fOut); + + // Write edge node + fileSize += sizeof(short int) * fwrite(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fOut); + } + + // Write triangles + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fOut); + for (t = 0; t < nmesh.face[f].num_tris; t++) + { + // Write triangle verts + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fOut); + + // Write triangle center + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fOut); + + // Write triangle face + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fOut); + + // Write triangle node + fileSize += sizeof(short int) * fwrite(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fOut); + + // Write triangle reachabilities + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fOut); + for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) + { + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fOut); + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fOut); + fileSize += sizeof(unsigned short) * fwrite(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fOut); + } + } + + fileSize += sizeof(byte) * fwrite(&nmesh.face[f].type, sizeof(byte), 1, fOut); // Surface type + fileSize += sizeof(int) * fwrite(&nmesh.face[f].drawflags, sizeof(int), 1, fOut); // Surface flags + fileSize += sizeof(int) * fwrite(&nmesh.face[f].contents, sizeof(int), 1, fOut); // Surface contents + fileSize += sizeof(vec3_t) * fwrite(&nmesh.face[f].normal, sizeof(vec3_t), 1, fOut); // Surface normal + } + + + // Write node data + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].origin, sizeof(vec3_t), 1, fOut); // Location + fileSize += sizeof(vec3_t) * fwrite(&nodes[n].normal, sizeof(vec3_t), 1, fOut); // Surface normal + fileSize += sizeof(byte) * fwrite(&nodes[n].type, sizeof(byte), 1, fOut); // Node type + //fileSize += sizeof(short int) * fwrite(&nodes[n].nodenum, sizeof(short int), 1, fOut); // Node number + fileSize += sizeof(qboolean) * fwrite(&nodes[n].inuse, sizeof(qboolean), 1, fOut); // Node inuse + + + // Links + fileSize += sizeof(byte) * fwrite(&nodes[n].num_links, sizeof(byte), 1, fOut); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("WARNING: Node %d has %d links, only %d will be saved\n", n, nodes[n].num_links, MAXLINKS); + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + //for (l = 0; l < nodes[numnodes].num_links; l++) // Only include valid links + { + fileSize += sizeof(short int) * fwrite(&nodes[n].links[l].targetNode, sizeof(short int), 1, fOut); // Link target node + fileSize += sizeof(byte) * fwrite(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fOut); // Link target type + fileSize += sizeof(float) * fwrite(&nodes[n].links[l].cost, sizeof(float), 1, fOut); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fwrite(&path_table[n][p], sizeof(short int), 1, fOut); // Path table + } + } + + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} + +/////////////////////////////////////////////////////////////////////// +// Load Area Awareness System (from file or generative) +// Parameter: force - generates a new AAS instead of loading from file +/////////////////////////////////////////////////////////////////////// +void ACEND_LoadAAS(qboolean force) +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + qboolean generateAAS = false; + int f, v, e, t, r; // BSP: Faces, verts, edges, tris, reach + int n, l, p; // Nodes, links, paths + int version = 0; // Bot nav version + unsigned bsp_checksum = 0; // Map checksum + cvar_t *game_dir = gi.cvar("game", "action", 0); + cvar_t *botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\nav\\"); + f += sprintf(filename + f, level.mapname); + f += sprintf(filename + f, ".aas"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/nav/"); + strcat(filename, level.mapname); + strcat(filename, ".aas"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists + { + generateAAS = true; // Generate new AAS + } + else + { + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // Bot nav version + if (version != BOT_AAS_VERSION) + { + generateAAS = true; // Generate new AAS + fclose(fIn); // Close the file + } + + bsp_t* bsp = gi.Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum + if (bsp_checksum != bsp->checksum) + { + generateAAS = true; // Generate new AAS + fclose(fIn); // Close the file + } + + } + + if (force) + generateAAS = true; // Force: generate a new AAS instead of loading from file + + // Generate a new AAS? + if (generateAAS) + { + Com_Printf("%s Generating botnav. Please wait this might take a while\n", __func__); + ACEND_SaveAAS(false); + return; + } + + // Init + BOTLIB_FreeNodes(); //Soft map change. Free any existing node memory used + BOTLIB_InitNodes(); + BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used + memset(&nmesh, 0, sizeof(nmesh_t)); + + // Remove doors + Remove_All_Doors(); + + Com_Printf("%s Reading AAS file...\n", __func__); + + fileSize += sizeof(unsigned short) * fread(&nmesh.total_faces, sizeof(unsigned short), 1, fIn); // Total Faces + fileSize += sizeof(unsigned short) * fread(&nmesh.total_edges, sizeof(unsigned short), 1, fIn); // Total Edges + fileSize += sizeof(unsigned short) * fread(&nmesh.total_verts, sizeof(unsigned short), 1, fIn); // Total Verticies + fileSize += sizeof(unsigned short) * fread(&nmesh.total_tris, sizeof(unsigned short), 1, fIn); // Total Triangles + fileSize += sizeof(int) * fread(&nmesh.total_reach, sizeof(int), 1, fIn); // Total Reachabilities + fileSize += sizeof(unsigned short) * fread(&numnodes, sizeof(unsigned short), 1, fIn); // Total Nodes + + if (numnodes + 1 > MAX_PNODES) + { + Com_Printf("%s ERROR: Too many nodes in AAS file\n", __func__); + return; + } + + // Read BSP Data + for (f = 0; f < nmesh.total_faces; f++) + { + // Read face verts + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_verts, sizeof(unsigned short), 1, fIn); + for (v = 0; v < nmesh.face[f].num_verts; v++) + { + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].v[v], sizeof(vec3_t), 1, fIn); + } + + // Read edges + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_edges, sizeof(unsigned short), 1, fIn); + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Read edge verts + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[0], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].v[1], sizeof(vec3_t), 1, fIn); + + // Read edge center + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].edge[e].center, sizeof(vec3_t), 1, fIn); + + // Read edge node + fileSize += sizeof(short int) * fread(&nmesh.face[f].edge[e].node, sizeof(short int), 1, fIn); + } + + // Read triangles + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].num_tris, sizeof(unsigned short), 1, fIn); + for (t = 0; t < nmesh.face[f].num_tris; t++) + { + // Read triangle verts + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[0], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[1], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].v[2], sizeof(vec3_t), 1, fIn); + + // Read triangle center + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].center, sizeof(vec3_t), 1, fIn); + + // Read triangle face + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].face, sizeof(unsigned short), 1, fIn); + + // Read triangle node + fileSize += sizeof(short int) * fread(&nmesh.face[f].tris[t].node, sizeof(short int), 1, fIn); + + // Read triangle reachabilities + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_num, sizeof(unsigned short), 1, fIn); + for (r = 0; r < nmesh.face[f].tris[t].reach_num; r++) + { + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_triangle[r], sizeof(unsigned short), 1, fIn); + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].tris[t].reach_origin[r], sizeof(vec3_t), 1, fIn); + fileSize += sizeof(unsigned short) * fread(&nmesh.face[f].tris[t].reach_face[r], sizeof(unsigned short), 1, fIn); + } + } + + fileSize += sizeof(byte) * fread(&nmesh.face[f].type, sizeof(byte), 1, fIn); // Surface type + fileSize += sizeof(int) * fread(&nmesh.face[f].drawflags, sizeof(int), 1, fIn); // Surface flags + fileSize += sizeof(int) * fread(&nmesh.face[f].contents, sizeof(int), 1, fIn); // Surface contents + fileSize += sizeof(vec3_t) * fread(&nmesh.face[f].normal, sizeof(vec3_t), 1, fIn); // Surface normal + } + + + // Main nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) + { + Com_Printf("%s failed to malloc nodes\n", __func__); + return; + } + + /* + // Path table + path_table = (short int**)malloc(sizeof(short int*) * MAX_PNODES); + if (path_table == NULL) + { + Com_Printf("%s failed to malloc path_table[]\n", __func__); + return; + } + else + { + // Dynamically allocate memory of size MAX_PNODES for each row + for (int r = 0; r < numnodes; r++) + { + path_table[r] = (short int*)malloc(sizeof(short int) * MAX_PNODES); + if (path_table[r] == NULL) + { + Com_Printf("%s failed to malloc path_table[][]\n", __func__); + return; + } + } + } + + Com_Printf("%s allocing MBytes[%ld] for nodes\n", __func__, ((sizeof(node_t) * numnodes) / 1024000)); + Com_Printf("%s allocing MBytes[%ld] for path_table\n", __func__, ((sizeof(short int) * MAX_PNODES * MAX_PNODES) / 1024000)); + */ + + + // Read node data + for (n = 0; n < numnodes; n++) + { + // Nodes + fileSize += sizeof(vec3_t) * fread(&nodes[n].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(vec3_t) * fread(&nodes[n].normal, sizeof(vec3_t), 1, fIn); // Surface normal + fileSize += sizeof(byte) * fread(&nodes[n].type, sizeof(byte), 1, fIn); // Node type + //fileSize += sizeof(unsigned short) * fread(&nodes[n].nodenum, sizeof(unsigned short), 1, fIn); // Node number + fileSize += sizeof(qboolean) * fread(&nodes[n].inuse, sizeof(qboolean), 1, fIn); // Node inuse + nodes[n].nodenum = n; // Node number + + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Links + fileSize += sizeof(byte) * fread(&nodes[n].num_links, sizeof(byte), 1, fIn); // Total node links + if (nodes[n].num_links > MAXLINKS) Com_Printf("%s node[%d] has more than MAXLINKS[%d] links\n", __func__, n, MAXLINKS); + //else Com_Printf("%s node[%d] has links[%d]\n", __func__, n, nodes[n].num_links); + //for (l = 0; l < nodes[numnodes].num_links; l++) // Include all links, even invalid ones + for (l = 0; l < MAXLINKS; l++) // Include all links, even invalid ones + { + fileSize += sizeof(short int) * fread(&nodes[n].links[l].targetNode, sizeof(short int), 1, fIn); // Link target node + fileSize += sizeof(byte) * fread(&nodes[n].links[l].targetNodeType, sizeof(byte), 1, fIn); // Link target type + fileSize += sizeof(float) * fread(&nodes[n].links[l].cost, sizeof(float), 1, fIn); // Link cost + } + + // Paths + for (p = 0; p < numnodes; p++) + { + fileSize += sizeof(short int) * fread(&path_table[n][p], sizeof(short int), 1, fIn); // Path table + } + } + + + + /* + fread(&num_items, sizeof(int), 1, fIn); // read facts count + fread(nodes, sizeof(node_t), numnodes, fIn); + fread(item_table, sizeof(item_table_t), num_items, fIn); + // Raptor007: Do not trust saved pointers! + for (i = 0; i < MAX_EDICTS; i++) + item_table[i].ent = NULL; + */ + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); + + /* + // Try placing POI nodes on high locations + if (1) + { + float highest_origin = -9999; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > highest_origin) + highest_origin = nodes[i].origin[2]; + } + float above_height = (highest_origin - 256); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].type == NODE_MOVE && nodes[i].origin[2] > above_height && random() < 0.1) + { + nodes[i].type = NODE_POI; + } + } + + ACEND_CachePointOfInterestNodes(); + } + + ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies + */ +} + +// Find the reachability for each node +void BOTLIB_NODE_REACHABILITY(edict_t* ent) +{ + node_t* from_node; // From node + node_t* to_node; // To node + int s, result; + vec3_t zero = { 0 }; + //qboolean found_ladder; + for (s = 0; s < numnodes; s++) + { + if (nodes[s].inuse == false) continue; // Ignore nodes not in use + + //if (nodes[s].nodenum != INVALID) continue; + + // Get the next 'from' node + from_node = (unsorted_nodes + s); // From Node + + // Zero the link counter + from_node->num_links = 0; + + for (int o = 0; o < numnodes; o++) + { + if (nodes[o].inuse == false) continue; // Ignore nodes not in use + //if (nodes[o].nodenum != INVALID) continue; + + if (s == o) // skip self + continue; + + to_node = (unsorted_nodes + o); // Node A + + result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); + + //TEST CODE -- s + //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes + // result = INVALID; + //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) + // result = NODE_JUMPPAD; + //TEST CODE -- e + + if (result != INVALID) + { + for (int lk = 0; lk < MAXLINKS; lk++) + { + if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link + { + from_node->links[lk].targetNodeType = result; // Set target node type + + // Add link + //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); + //break; + + + //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes + if (BOTLIB_AddNodeLink(s, o, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + //from_node->num_links++; // Update number of links this node has + nmesh.total_reach++; + } + break; + } + } + } + } + } +} + +// Similar to gi.pointcontents, but returns the contents within the bounding box around the origin +// Useful when checking for multiple contents within the local area, specially when checking water +int BOTLIB_UTIL_BOXCONTENTS(vec3_t origin) +{ + trace_t tr = gi.trace(origin, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), origin, NULL, MASK_ALL); + return tr.contents; +} + +// Rotate the center of an edge around a center point, then extend the center point out by a distance +// +// (end) <--- desired result +// | +// | +// | <--- distance +// |\ +// | \ <-- rotated edge (90 degrees) +// | \ +// (pt1) -----+----- (pt2) +// (center) +// +// pt1 and pt2 are the start and end points of an edge +// float rotate the degrees of rotation +// float distance is the distance from the center point +// vec3_t end is the rotated end point +void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end) +{ + vec3_t center, angle, forward; // , right, offset; + + // print warning if distance is > 3000 + if (distance > 3000) + { + Com_Printf("%s WARNING: distance > 3000. Cannot rotate accurately.\n", __func__); + } + + // Find the middle between two points of an edge + LerpVector(pt1, pt2, 0.50, center); + + // Get direction vector + VectorSubtract(pt2, pt1, forward); + + // Normalize the direction vector + VectorNormalize(forward); + + // Get the angle of the direction vector + vectoangles(forward, angle); + + //Com_Printf("%s vectoangles()->angle[YAW] = %f\n", __func__, angle[YAW]); + + //if (direction == MOVE_LEFT) + // angles[1] += 90; + //else if (direction == MOVE_RIGHT) + // angles[1] -= 90; + //else if (direction == MOVE_BACK) + // angles[1] -= 180; + + // Rotation + //angle[PITCH] = 0; + angle[YAW] += rotate; // Rotation angle + //angle[ROLL] = 0; + + // Wrap around + //if (angle[YAW] >= 180) + // angle[YAW] = angle[YAW] - 360; + //else if (angle[YAW] <= -180) + // angle[YAW] = angle[YAW] + 360; + + //if (angle[YAW] > 360) + // angle[YAW] -= 360; + //else if (angle[YAW] < 0) + // angle[YAW] += 360; + + // Rotate the direction vector + AngleVectors(angle, forward, NULL, NULL); + + //VectorSet(offset, distance, 0, 0); + //G_ProjectSource(center, offset, forward, right, end); + + // Get the rotated end point and extend it out based on distance + VectorMA(center, distance, forward, end); + //VectorMA(center, distance, right, end); +} + +// Test for a 'trigger_hurt' entity +// Returns true if we're touching a 'trigger_hurt', otherwise false +qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin) +{ + trace_t tr; + + // Test point contents from -20 to -30 checking for lava or slime below the proposed origin + int contents; + for (int i = 0; i < 10; i++) + { + contents = gi.pointcontents(tv(origin[0], origin[1], origin[2] - (20+i) )); + if (contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + + // Trace above center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] + NODE_Z_HEIGHT), g_edicts, MASK_ALL); + if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) + { + //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); + if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) + { + //Com_Printf("%s +SKIPPING %s\n", __func__, tr.ent->classname); + return true; + } + + if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + // Skip if we started inside of a solid + if (tr.startsolid) + { + /* + if (nmesh.face[f].drawflags) + { + if (nmesh.face[f].drawflags & SURF_SKY) + Com_Printf("%s START SOLID, drawflags: SURF_SKY\n", __func__, nmesh.face[f].drawflags); + else + Com_Printf("%s START SOLID, drawflags: %d\n", __func__, nmesh.face[f].drawflags); + } + */ + + return true; + } + // Trace below center to see if we're touching a 'trigger_hurt' designed to kill a player who touches it + tr = gi.trace(tv(origin[0], origin[1], origin[2]), NULL, NULL, tv(origin[0], origin[1], origin[2] - NODE_Z_HEIGHT), g_edicts, MASK_ALL); + if (tr.ent && tr.ent != g_edicts) // is ent and not worldspawn (g_edicts[0] = worldspawn) + { + //Com_Printf("%s face:%i tris:%i ent:%s\n", __func__, f, nmesh.face[f].num_tris, tr.ent->classname); + if (strcmp(tr.ent->classname, "trigger_hurt") == 0 || tr.ent->dmg) + { + //Com_Printf("%s -SKIPPING %s\n", __func__, tr.ent->classname); + return true; + } + if (tr.contents & (CONTENTS_LAVA | CONTENTS_SLIME)) + return true; + } + + return false; +} + +// Turn selective map entities into solids, so we can trace them +// Used for ents like 'trigger_hurt' so we can avoid placing nodes on those surfaces +//solid_t trigger_solid[MAX_EDICTS]; +// make trigger_solid a malloc +int BOTLIB_MakeEntsSolid(solid_t* trigger_solid) +{ + if (trigger_solid == NULL) + { + return -1; + } + + int edict_num = 1 + game.maxclients; // skip worldclass & players + //int edict_cur = edict_num; + int edict_cur = 0; + edict_t* map_ents; + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + if (map_ents->inuse == false) + { + //edict_cur++; + continue; + } + + trigger_solid[edict_cur] = map_ents->solid; // keep a copy of the solid state of each ent + /* + SOLID_NOT, // no interaction with other objects + SOLID_TRIGGER, // only touch when inside, after moving + SOLID_BBOX, // touch on edge + SOLID_BSP // bsp clip, touch on edge + */ + /* + if (map_ents->solid == SOLID_NOT) + Com_Printf("%s [%i] SOLID_NOT classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_TRIGGER) + Com_Printf("%s [%i] SOLID_TRIGGER classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_BBOX) + Com_Printf("%s [%i] SOLID_BBOX classname: %s\n", __func__, edict_cur, map_ents->classname); + else if (map_ents->solid == SOLID_BSP) + Com_Printf("%s [%i] SOLID_BSP classname: %s\n", __func__, edict_cur, map_ents->classname); + else + Com_Printf("%s [%i] SOLID_??? classname: %s\n", __func__, edict_cur, map_ents->classname); + */ + + + + //if (strlen(map_ents->classname) > 1) + // Com_Printf("%s %s found\n", __func__, map_ents->classname); + + if (strcmp(map_ents->classname, "trigger_hurt") == 0) + { + map_ents->solid = SOLID_BBOX; + gi.linkentity(map_ents); // Relink ent because solidity changed + //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); + } + + //if (strcmp(map_ents->classname, "func_rotating") == 0) + //{ + //map_ents->solid = SOLID_BBOX; + //gi.linkentity(map_ents); // Relink ent because solidity changed + //Com_Printf("%s %s found\n", __func__, map_ents->classname, map_ents->s.solid); + //} + + edict_cur++; + } + return edict_cur; +} +void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid) +{ + if (trigger_solid == NULL) + { + Com_Printf("%s trigger_solid is NULL\n", __func__); + return; + } + // Restore solid state of each ent + int edict_cur = 0; + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + if (map_ents->inuse == false) + { + //edict_cur++; + continue; + } + + map_ents->solid = trigger_solid[edict_cur]; // Restore the original solid state + gi.linkentity(map_ents); // Relink ent because solidity changed + + edict_cur++; + } + if (trigger_solid) + free(trigger_solid); +} + +// Add nodes at item locations +void BOTLIB_AddItemNodes() +{ + // =========================== + // Add nodes at item locations + // =========================== + int foundItems = 0; // How many items were found + vec3_t item_origin = { 0,0,0 }; + edict_t* map_ents; + int edict_num = 1 + game.maxclients; // skip worldclass & players + trace_t tr; + for (map_ents = g_edicts + edict_num; map_ents < &g_edicts[globals.num_edicts]; map_ents++) + { + // Skip ents that are not in used and not an item + if (map_ents->inuse == false || map_ents->item == NULL) + continue; + + // Find the ground under the item + // Some ents drop from the sky, so we need to trace down to the ground to find their landing spot (tr.endpos) + tr = gi.trace(map_ents->s.origin, NULL, NULL, tv(map_ents->s.origin[0], map_ents->s.origin[1], -4096), g_edicts, MASK_PLAYERSOLID); + + // See if the item will fit (player size) on the ground + // Some ents are placed on shelfs (urban2) so we cannot place a node on the shelf, so look for a spot to place the node nearby + tr = gi.trace(tr.endpos, tv(-12, -12, 0), tv(12, 12, 56), tr.endpos, g_edicts, MASK_PLAYERSOLID); + + VectorCopy(tr.endpos, item_origin); // Update the origin + + // If it won't fit, then look for a spot nearby + if (tr.startsolid || tr.fraction < 1.0) + { + float offset = 30; // How far to offset from the original item location (30 allows just enough room to grab the item, but not too far away) + // This number '30' was picked because of the shelves on urban2. The bot needs to walk up to the shelf to grab the item. + +// Try 24 units forward (X + 24) + tr = gi.trace(tv(item_origin[0] + offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] + offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units back (X - 24) + tr = gi.trace(tv(item_origin[0] - offset, item_origin[1], item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0] - offset, item_origin[1], item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units right (Y + 24) + tr = gi.trace(tv(item_origin[0], item_origin[1] + offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] + offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // Try 24 units left (Y - 24) + tr = gi.trace(tv(item_origin[0], item_origin[1] - offset, item_origin[2]), tv(-12, -12, 0), tv(12, 12, 56), tv(item_origin[0], item_origin[1] - offset, item_origin[2]), g_edicts, MASK_PLAYERSOLID); + if (tr.startsolid == false && tr.fraction == 1.0) + goto VIABLE_ITEM_NODE; // Success! + + // If we get here, then we could not find a spot to place the node + Com_Printf("%s WARNING: could not add node for item: %s\n", __func__, map_ents->classname); + continue; // Skip adding a node for this item because we cannot reach it + + // We found a spot to place the node + VIABLE_ITEM_NODE: + VectorCopy(tr.endpos, item_origin); // Update the origin + tr = gi.trace(item_origin, NULL, NULL, tv(item_origin[0], item_origin[1], -4096), g_edicts, MASK_PLAYERSOLID); // Find the ground under the item + VectorCopy(tr.endpos, item_origin); // Update the origin again + } + + + item_origin[2] += NODE_Z_HEIGHT; // Raise the node up to standard height + + + Com_Printf("%s found node for item: %s [typeNum: %d]\n", __func__, map_ents->classname, map_ents->item->typeNum); + + // Weapons + if (strcmp(map_ents->classname, "weapon_Sniper") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_M4") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_MP5") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_M3") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_HC") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "weapon_Grenade") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + + // Ammo + else if (strcmp(map_ents->classname, "ammo_clip") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_mag") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_m4") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_m3") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + else if (strcmp(map_ents->classname, "ammo_sniper") == 0) + { + foundItems++; + DAIC_Add_Node(item_origin, tr.plane.normal, NODE_ITEM); + } + } + if (foundItems == 0) + { + // TODO: Get map ents without needing deathmatch enabled + + Com_Printf("%s WARNING: bot navigation was generated without item nodes. Please ensure the map is generated in deathmatch mode.\n", __func__); + } +} + +// Find the reachability for each node +void BOTLIB_ProcesssReachabilities(void) +{ + vec3_t zero = { 0 }; + node_t* from_node; // From node + node_t* to_node; // To node + int s, result; + //qboolean found_ladder; + for (s = 0; s < numnodes; s++) // From node + { + if (nodes[s].inuse == false) continue; // Ignore nodes not in use + + //if (nodes[s].nodenum != INVALID) continue; + + // Get the next 'from' node + from_node = (unsorted_nodes + s); // From Node + + // Zero the link counter + from_node->num_links = 0; + + for (int o = 0; o < numnodes; o++) // To node + { + if (nodes[o].inuse == false) continue; // Ignore nodes not in use + //if (nodes[o].nodenum != INVALID) continue; + + if (s == o) // skip self + continue; + + to_node = (unsorted_nodes + o); // Node A + + result = DC_Reachability(from_node->nodenum, to_node->nodenum, from_node->origin, to_node->origin, from_node->normal); + + //TEST CODE -- s + //if (result == NODE_JUMPPAD || result == NODE_LADDER_UP || result == NODE_LADDER_DOWN) + // result = INVALID; + //if (result != NODE_MOVE && result != NODE_STEP) // Force only MOVE nodes + // result = INVALID; + //if (from_node->nodenum == 1740 && to_node->nodenum == 1403) + // result = NODE_JUMPPAD; + //TEST CODE -- e + + if (BOTLIB_AddNodeLink(s, o, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + nmesh.total_reach++; + } + + /* + if (result != INVALID) + { + for (int lk = 0; lk < MAXLINKS; lk++) + { + if (from_node->links[lk].targetNode != o && from_node->links[lk].targetNode == INVALID) // Find a spare target link + { + from_node->links[lk].targetNodeType = result; // Set target node type + + // Add link + //DC_AddNodeLink(unsorted_nodes, s, o, zero, false, result, true); + //break; + + + //DC_AddNodeLink(NULL, s_node->nodenum, a_node->nodenum, zero, false, s_node->links[lk1].targetNodeType, true); // Copy the sorted nodes + if (DC_AddNodeLink(NULL, s, o, zero, false, result, true)) + { + ACEND_UpdateNodeReach(from_node->nodenum, to_node->nodenum); // Update path_table + //if (from_node->num_links + 1 > MAXLINKS) Com_Printf("%s too many links added to node[%d]\n", __func__, s); + //from_node->num_links++; // Update number of links this node has + nmesh.total_reach++; + } + break; + } + } + //Com_Printf("%s node[%d] added %i links\n", __func__, s, nodes[s].num_links); + } + */ + } + } +} + +void BOTLIB_BSP_SURFACES(bsp_t* bsp) +{ + int f, e; + int nv = 0; // Current number of verts + mface_t* surf; + msurfedge_t* src_surfedge; + + // Copy data from the BSP tree + for (f = 0, surf = bsp->faces; f < bsp->numfaces; f++, surf++) + { + if (surf == NULL) + { + gi.dprintf("%s null faces found\n", __func__); + return; + } + + nmesh.face[f].drawflags = surf->drawflags; // Copy draw flags (DSURF_PLANEBACK, etc) + VectorCopy(surf->plane->normal, nmesh.face[f].normal); // Copy plane normal + nmesh.face[f].num_edges = surf->numsurfedges; // Copy total edges on this face + + // Don't exclude sky because some walkable surfaces are sky + //if (surf->drawflags & SURF_SKY) + // continue; + + // If surf plane is a flat floor + if (surf->plane->normal[2] == 1) + { + if (surf->drawflags & DSURF_PLANEBACK) // Skip flat roof surfaces + continue; + + nmesh.face[f].type = FACETYPE_FLOOR; // Perfectly flat + } + else if (surf->plane->normal[0] >= -MAX_STEEPNESS && surf->plane->normal[0] <= MAX_STEEPNESS && surf->plane->normal[1] >= -MAX_STEEPNESS && surf->plane->normal[1] <= MAX_STEEPNESS && surf->plane->normal[2] >= MAX_STEEPNESS) + { + nmesh.face[f].type = FACETYPE_SLOPE; // Any climbable sloped surface + } + else //if (surf->plane->normal[2] == 0) // Wall with normal[2] == 0 means the wall is perfectly upright + nmesh.face[f].type = FACETYPE_WALL; // Anything too steep to climb becomes a wall (needs to yet be tested for CONTENTS_LADDER, this will be processed further down) + + + for (e = 0, src_surfedge = surf->firstsurfedge; e < surf->numsurfedges; e++, src_surfedge++) + { + // Copy edge verts + VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[0]].point, nmesh.face[f].edge[e].v[0]); + VectorCopy(bsp->vertices[bsp->edges[src_surfedge->edge].v[1]].point, nmesh.face[f].edge[e].v[1]); + + + // Copy face verts + nv = nmesh.face[f].num_verts; + VectorCopy(nmesh.face[f].edge[e].v[0], nmesh.face[f].v[nv]); + VectorCopy(nmesh.face[f].edge[e].v[1], nmesh.face[f].v[nv + 1]); + nmesh.face[f].num_verts += 2; + } + + //f = ++nmesh.total_faces; // Total faces + nmesh.total_faces++; // Total faces + + + nmesh.total_verts += (surf->numsurfedges * 2); // Total verts + nmesh.total_edges += surf->numsurfedges; // Total edges + } +} + +void BOTLIB_Process_NMesh(edict_t* ent) +{ + trace_t tr; + + // Player when standing + vec3_t player_standing_mins = { -16, -16, -24 }; + vec3_t player_standing_maxs = { 16, 16, 32 }; + // Player when crouching/ducking + vec3_t player_crouching_mins = { -16, -16, -24 }; + vec3_t player_crouching_maxs = { 16, 16, 4 }; + // Player when standing + vec3_t player_standing_smaller_mins = { -14, -14, -24 }; + vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; + + const int MIN_STEP_WIDTH = 32; // min step width + qboolean hit_step = false; // If we hit a suspected step (not 100% sure) + qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) + + vec3_t mid_point = { 0 }; + vec3_t ladder_bottom = { 0 }; + vec3_t ladder_top = { 0 }; + + float tmp = 0; + int f, e; + + float normal_0, normal_1, normal_2; + int num_tris = 0; + int vert_count = 0; // Vertex count + int total_verts = 0; + vec3_t pt0 = { 0 }; // Point 1 of a triangle + vec3_t pt1 = { 0 }; // Point 2 of a triangle + vec3_t pt2 = { 0 }; // Point 3 of a triangle + vec3_t center = { 0 }; // Center point of either an edge or a triangle + + // Proccess the mesh data + for (f = 0; f < nmesh.total_faces; f++) + { + normal_0 = nmesh.face[f].normal[0]; + normal_1 = nmesh.face[f].normal[1]; + normal_2 = nmesh.face[f].normal[2]; + + num_tris = 0; + vert_count = 0; + total_verts = nmesh.face[f].num_verts; + memset(pt0, 0, sizeof(vec3_t)); + memset(pt1, 0, sizeof(vec3_t)); + memset(pt2, 0, sizeof(vec3_t)); + memset(center, 0, sizeof(vec3_t)); + + // Edges - (only the outside edges of a face) + //----------------------------------------------------- + for (e = 0; e < nmesh.face[f].num_edges; e++) + { + // Copy the edge vectors + VectorCopy(nmesh.face[f].edge[e].v[0], pt1); + VectorCopy(nmesh.face[f].edge[e].v[1], pt2); + + // Get the distance between pt1 and pt2 + vec3_t pt1_to_pt2_vec; + VectorSubtract(pt2, pt1, pt1_to_pt2_vec); // Direction vector from pt1 to pt2 + float pt1_to_pt2_dist = VectorLength(pt1_to_pt2_vec); // Distance between pt1 and pt2 + + // Skip excessively small edges + if (pt1_to_pt2_vec < 4) + { + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + // Get the normalized node height based on the face type -- floor, slope, or wall + vec3_t adjusted_height = { 0 }; + if (nmesh.face[f].type == FACETYPE_FLOOR) + adjusted_height[2] += NODE_Z_HEIGHT; // Move the center so it is somewhat above the ground, instead of being stuck inside the ground + else if (nmesh.face[f].type == FACETYPE_SLOPE) + adjusted_height[2] += (NODE_Z_HEIGHT * normal_2); // Calculate the correct height based on the normal of the slope + else if (nmesh.face[f].type == FACETYPE_WALL) // Push out from wall + { + adjusted_height[2] += NODE_Z_HEIGHT; // Move up from ground (note this isn't using the normal direction, its directly up from the ground) + + // Calculate the correct wall distance based on the normal angle + if (nmesh.face[f].drawflags & DSURF_PLANEBACK) + { + if (normal_0 != 0) + adjusted_height[0] -= (NODE_Z_HEIGHT * normal_0); // -X (forward wall) + if (normal_1 != 0) + adjusted_height[1] -= (NODE_Z_HEIGHT * normal_1); // -Y (left wall) + } + else + { + if (normal_0 != 0) + adjusted_height[0] += (NODE_Z_HEIGHT * normal_0); // +X (back wall) + if (normal_1 != 0) + adjusted_height[1] += (NODE_Z_HEIGHT * normal_1); // +Y (right wall) + } + } + + // Get the center of the edge + LerpVector(pt1, pt2, 0.50, center); + + // Record it + VectorCopy(center, nmesh.face[f].edge[e].center); // Copy the center of the edge + + // Apply the adjusted height + nmesh.face[f].edge[e].center[0] += adjusted_height[0]; + nmesh.face[f].edge[e].center[1] += adjusted_height[1]; + nmesh.face[f].edge[e].center[2] += adjusted_height[2]; + + + + + + // Test for edges + // Some edges are on a floor, others from a wall, therefore we test both here + if (0) + { + qboolean hit_step_left = false, hit_step_right = false; + vec3_t end_left, end_right; + + + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1, end_left); // Turn 90 degrees left and move 1 unit + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1, end_right); // Turn 90 degrees right and move 1 unit + + // Test just above center to see if we're in a solid + trace_t tr_edge_center = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] + 1), ent, MASK_PLAYERSOLID); + if (tr_edge_center.startsolid == false && tr_edge_center.fraction == 1.0) // Not in a solid (usually a wall) + { + // Test if we're on an edge with a dropoff + trace_t tr_edge_left = gi.trace(end_left, NULL, NULL, tv(end_left[0], end_left[1], end_left[2] - 1), ent, MASK_PLAYERSOLID); + trace_t tr_edge_right = gi.trace(end_right, NULL, NULL, tv(end_right[0], end_right[1], end_right[2] - 1), ent, MASK_PLAYERSOLID); + if (tr_edge_left.startsolid == false && tr_edge_left.fraction == 1.0) + hit_step_left = true; + if (tr_edge_right.startsolid == false && tr_edge_right.fraction == 1.0) + hit_step_right = true; + + //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + + if (hit_step_left && hit_step_right) // A double edge on either side + { + //DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + } + else if (hit_step_left || hit_step_right) // A single edge -- ledge / stairs + { + //if (BOTLIB_UTIL_NearbyNodeAtHeightDist(center, NODE_SIZE) == INVALID) + if (1) + { + //Com_Printf("%s f:%i e:%i node:%d hit_step_left:%i hit_step_right:%i\n", __func__, f, e, numnodes, hit_step_left, hit_step_right); + + //if (BOTLIB_TestForNodeDist(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), 2) == INVALID) + DAIC_Add_Node(tv(center[0], center[1], center[2] + NODE_Z_HEIGHT), nmesh.face[f].normal, NODE_STEP); + + // Expand nodes from the top and bottom landing steps. Try adding a node 90 degrees left and -90 degrees right. + // We try both directions because the top step is usually flat, and the landing step has a dropoff. + // This will help the bot get around corners, and connect the top and bottom of stairs to the node network + // + // [left node] [sn_1] + // -------------------------------------------- + // | | [sn_2] + // | ---------- + // | sn_x = step node | [sn_3] + // | left node = move node ---------- + // | right node = move node | [right node] + // | -------------------------------- + // The [left node] is added to the left of [sn_1] + // The [right node] is added to the right of [sn_3] + // + if (1) + { + vec3_t end; + + // Set player waist height + const int player_waist_height = fabs(player_standing_mins[2]); // 24 units + + if (1) // Left step + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 32, end); //2048 + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see if end is in a solid + trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); + if (tr_end_step.startsolid == false) + { + // Test to see if end is touching the ground + trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16,-16,-16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + + if (1) // Right step + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 32, end); //2048 + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see if end is in a solid + trace_t tr_end_step = gi.trace(end, tv(-15, -15, -24), tv(15, 15, 32), end, ent, MASK_PLAYERSOLID); + if (tr_end_step.startsolid == false) + { + // Test to see if end is touching the ground + trace_t tr_end_ground = gi.trace(end, NULL, NULL, tv(end[0], end[1], end[2] - (player_waist_height + 2 + STEPSIZE)), ent, MASK_PLAYERSOLID); + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + } + } + } + else // Floor edge (no dropoff on either side -- the floor is a flat plane) + { + // Expand additional nodes from the center of the edge. + // Heading forward, back, left, right. + if (1) + { + for (int d = 0; d < 4; d++) + { + vec3_t end; + + if (d == 0) // forwards + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 0, 1024, end); // Forward 0 degrees + //VectorCopy(pt1, end); + //end[2] += 1; + } + else if (d == 1) // backwards + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 180, 1024, end); // Backwards 180 or -180 degrees + //VectorCopy(pt2, end); + //end[2] += 1; + } + else if (d == 2) // left + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1024, end); // Left (pt1 -> pt2 -> 90 degrees) + } + else if (d == 3) // right + { + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1024, end); // Right (pt1 -> pt2 -> -90 degrees) + } + + // Get player waist height + const int player_waist_height = fabs(player_standing_mins[2]); // 24 units + + // Lift the end points up a bit so they don't get stuck in the ground + end[2] += player_waist_height + 1; + + // Test to see where end meets a wall + // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right + trace_t tr_end_wall = gi.trace(tv(center[0], center[1], center[2] + player_waist_height), player_standing_mins, player_standing_maxs, end, ent, MASK_PLAYERSOLID); + + + // Test to see if end is touching the ground + // From the wall hit down to the ground which is -(player_waist_height + 1) + trace_t tr_end_ground = gi.trace(tr_end_wall.endpos, NULL, NULL, tv(tr_end_wall.endpos[0], tr_end_wall.endpos[1], tr_end_wall.endpos[2] - (player_waist_height + 1)), ent, MASK_PLAYERSOLID); + + + // If the end is touching the ground, then we can add a node there + if (tr_end_ground.fraction < 1.0) + { + if (BOTLIB_TestForNodeDist(tr_end_ground.endpos, NODE_SIZE * 4, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + { + // Bring height up to NODE_Z_HEIGHT + tr_end_ground.endpos[2] += NODE_Z_HEIGHT; + + DAIC_Add_Node(tr_end_ground.endpos, tr_end_ground.plane.normal, NODE_MOVE); + } + } + } + } + } + } + } + + + // Skip wall edges + if (nmesh.face[f].type == FACETYPE_WALL) + { + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + + + + + // Find the middle between two points of an edge + //LerpVector(pt1, pt2, 0.50, center); // 50% (center) + + + // Ignore edges that are up in the air + // Trace just below the floor, if we didn't hit the ground, then the node is up in the air. We want to ignore these because some are 'trigger' and other ents + //if (nmesh.face[f].type == FACETYPE_FLOOR || nmesh.face[f].type == FACETYPE_SLOPE) + { + //tr = gi.trace(center, tv(-16, -16, -(NODE_Z_HEIGHT + 1)), tv(16, 16, 24), center, ent, MASK_SOLID); + // move off ground slightly, then line trace back to ground, if we don't hit ground, then we're in the air + tr = gi.trace(tv(center[0], center[1], center[2] + 1), NULL, NULL, tv(center[0], center[1], center[2] - 1), ent, MASK_PLAYERSOLID); + //tr = gi.trace(center, tv(-0.1, -0.1, -0.1), tv(0.1, 0.1, 0.1), center, ent, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + //Com_Printf("%s face:%i HIT failed to find ground\n", __func__, f); + nmesh.face[f].edge[e].node = INVALID; + continue; + } + } + + // Check for trigger_hurt + if (BOTLIB_UTIL_CHECK_FOR_HURT(center)) + { + //Com_Printf("%s face:%i HIT trigger_hurt\n", __func__, f); + nmesh.face[f].edge[e].node = INVALID; + continue; + } + + // Depending on the edge tested (within the same face) we may encounter different multiple groups of contents, + // therefore we need to add them all together. This is because edge can be shared within the same face, or other faces. + int box_contents = BOTLIB_UTIL_BOXCONTENTS(center); + if (nmesh.face[f].contents > 0 && box_contents > 0 && box_contents != nmesh.face[f].contents) + { + // before + //Com_Printf("%s face:%i tris:%i contents:0x%x contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents, tr.contents); + // + // Add tr.contents to nmesh.face[f].contents + // + // 0x18000002 + // + 0x08030000 + // ---------- + // = 0x18030002 + // + nmesh.face[f].contents |= box_contents; + // + // after + //Com_Printf("%s face:%i tris:%i contents:0x%x\n", __func__, f, nmesh.face[f].num_tris, nmesh.face[f].contents); + } + // Add the first contents found + if (nmesh.face[f].contents == 0) + nmesh.face[f].contents = box_contents; + + // Copy it back + VectorCopy(nmesh.face[f].edge[e].center, center); + + // Test for steps / edges +#if 0 + if (0) + { + + // Check if trace hit a step - STEPSIZE + // + // e1 = edge 1 + // e2 = edge 2 + // c = center of bounding box + // + // roof ------------------------------- + // + // ____________ + // | | + // | player | + // | bounding | + // | box | + // | | + // -----|------ + // c + // + // floor ___________e1 (step) + // | + // |___e2 (step) + // | + // | + // ------------- floor + // + // The center of the players bounding box will fit on (e1), but might not on (e2) as it could overlap against (e1) + // We do the same floor trace as normal, except move the bounding box upward by STEPSIZE + // and check if player will fit, as if they were standing on the step while crouching. This is because + // a player can crouch walk up stairs, thereby covering normal + edge cases. + // + + hit_step = false; // If we hit a suspected step (not 100% sure) + hit_ledge = false; // If we hit a suspected ledge (not 100% sure) + + + vec3_t dist; + VectorSubtract(pt1, pt2, dist); // vector from pt1 to pt2 + dist[2] = 0; // remove height + vec3_t center_step; + VectorCopy(center, center_step); + center_step[2] -= (adjusted_height[2] - 0.1); + trace_t tr_edge = gi.trace(center_step, tv(-16, -16, -24), tv(16, 16, 32), center_step, ent, MASK_PLAYERSOLID); + if (tr_edge.startsolid && tr_edge.fraction < 1 && VectorLength(dist) > MIN_STEP_WIDTH/*calc step width*/) // inside step and we're larger than min step width + { + //* + //vec3_t center_step; + //VectorCopy(center, center_step); + //center_step[2] += STEPSIZE; + //center_step[2] += adjusted_height[2]; + center_step[2] += STEPSIZE + 0.1; + + //trace_t tr_step = gi.trace(center_step, player_crouching_mins, player_crouching_maxs, center_step, ent, MASK_DEADSOLID); + trace_t tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), center_step, ent, MASK_DEADSOLID); + //trace_t tr_step = gi.trace(center_step, tv(-0.1, -0.1, 0), tv(0.1, 0.1, 0), center_step, ent, MASK_DEADSOLID); + //trace_t tr_step = gi.trace(center_step, NULL, NULL, center_step, ent, MASK_DEADSOLID); + if (!tr_step.startsolid && tr_step.fraction == 1) + { + // Find the floor + tr_step = gi.trace(center_step, tv(-15.9, -15.9, 0), tv(15.9, 15.9, 0), tv(center_step[0], center_step[1], center_step[2] - (STEPSIZE + 1)), ent, MASK_DEADSOLID); + tr_step.endpos[2] += adjusted_height[2]; + + if (BOTLIB_UTIL_NearbyNodeAtHeightDist(tr_step.endpos, NODE_SIZE) == INVALID) + { + DAIC_Add_Node(tr_step.endpos, nmesh.face[f].normal, NODE_STEP); + } + //hit_step = true; + //VectorCopy(center_step, center); // increase node z loc from NODE_Z_HEIGHT to (NODE_Z_HEIGHT + STEPSIZE) + //Com_Printf("%s f:%d e:%d [center_step %f %f %f] [center %f %f %f]\n", __func__, f, e, center_step[0], center_step[1], center_step[2], center[0], center[1], center[2]); + + } + //*/ + + /* + // Try again but move the bounding box slightly, this works for small edges that go along a wall (urban, urban2, etc) + // Move the bounding box to each corner, forward/back/right/left. Then trace again. + trace_t tr_edge; + vec3_t center_edge; + + // forward + if (hit_step == false) + { + VectorCopy(center, center_edge); + center_edge[0] += 16; // move node forward + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // back + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[0] -= 16; // move node back + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // right + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[1] += 16; // move node right + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + + // left + if (hit_ledge == false) + { + VectorCopy(center, center_edge); + center_edge[1] -= 16; // move node left + tr_edge = gi.trace(center_edge, player_crouching_mins, player_crouching_maxs, center_edge, ent, MASK_DEADSOLID); + if (!tr_edge.startsolid && tr_edge.fraction == 1) + { + hit_ledge = true; + VectorCopy(center_edge, center); + } + } + */ + } + } +#endif + + //if (nmesh.face[f].type == FACETYPE_WALL && (nmesh.face[f].contents & CONTENTS_LADDER) == 0) + + + if (nmesh.face[f].type == FACETYPE_SLOPE) + tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_PLAYERSOLID); + else // floor + tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_PLAYERSOLID); + + + + //tr = gi.trace(center, tv(-8, -8, -20), tv(8, 8, 0), center, ent, MASK_SOLID); + if ((!tr.startsolid && tr.fraction == 1))// || hit_step || hit_ledge) + { + //if (tr.contents < CONTENTS_AREAPORTAL || (tr.contents & CONTENTS_LADDER) == 0) // Look for visible brushes, including non-solid such as liquids, also ladders + { + if (BOTLIB_TestForNodeDist(center, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) // || hit_step || hit_ledge) // hit_step and hit_ledge ignore node size (for now) + { + // Test again, but just for MASK_WATER + if (nmesh.face[f].type == FACETYPE_SLOPE) + tr = gi.trace(center, tv(-16, -16, -6), player_crouching_maxs, center, ent, MASK_WATER); + else + tr = gi.trace(center, player_crouching_mins, player_crouching_maxs, center, ent, MASK_WATER); + + if ((nmesh.face[f].contents & CONTENTS_LAVA) || (nmesh.face[f].contents & CONTENTS_SLIME)) + continue; + else if ((tr.contents & CONTENTS_WATER) || nmesh.face[f].contents & CONTENTS_WATER) + //continue; // Don't add edge water nodes + DAIC_Add_Node(center, nmesh.face[f].normal, NODE_WATER); + else + { + //if (hit_step) // hit_step + // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + //else if (hit_ledge) + // DAIC_Add_Node(center, nmesh.face[f].normal, NODE_STEP); + //else + DAIC_Add_Node(center, nmesh.face[f].normal, NODE_MOVE); + } + + //if (nmesh.face[f].edge[e].node) + // Com_Printf("WARNING: edge node already exists face:%d edge:%d node:%d\n", f, e, nmesh.face[f].edge[e].node); + + nmesh.face[f].edge[e].node = numnodes - 1; + + + // Expand additional nodes from the center of the edge, one heading 90 degrees left, and one heading 90 degrees right. + if (0 && nmesh.face[f].type == FACETYPE_FLOOR) + { + vec3_t end_left, end_right; + + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 2000, end_left); + BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 270, 2000, end_right); + + // Lift the end points up a bit so they don't get stuck in the ground + end_left[2] += 1; + end_right[2] += 1; + + // Find the middle between two points of an edge + LerpVector(pt1, pt2, 0.50, center); + //center[2] += NODE_Z_HEIGHT; + //center[2] += 25; + + // Test to see where the left and right meet a wall + // Center is lifted up a bit so it doesn't get stuck in the ground, for the same reason as end_left and end_right + trace_t tr_left_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_left, ent, MASK_PLAYERSOLID); + trace_t tr_right_wall = gi.trace(tv(center[0], center[1], center[2] + 1), player_standing_mins, player_standing_maxs, end_right, ent, MASK_PLAYERSOLID); + + //DAIC_Add_Node(tr_left_wall.endpos, tr_left_wall.plane.normal, NODE_MOVE); + //DAIC_Add_Node(tr_right_wall.endpos, tr_right_wall.plane.normal, NODE_MOVE); + + // Test to see if left and right are touching the ground + //trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); + //trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - NODE_Z_HEIGHT), ent, MASK_PLAYERSOLID); + trace_t tr_left_ground = gi.trace(tr_left_wall.endpos, NULL, NULL, tv(tr_left_wall.endpos[0], tr_left_wall.endpos[1], tr_left_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); + trace_t tr_right_ground = gi.trace(tr_right_wall.endpos, NULL, NULL, tv(tr_right_wall.endpos[0], tr_right_wall.endpos[1], tr_right_wall.endpos[2] - 1.2), ent, MASK_PLAYERSOLID); + + + + // If the left is touching the ground, then we can add a node there + //if (tr_left_ground.fraction < 1.0) + { + tr_left_wall.endpos[2] += (NODE_Z_HEIGHT - 1); + + //if (BOTLIB_TestForNodeDist(tr_left_wall.endpos, NODE_SIZE) == INVALID) + DAIC_Add_Node(tr_left_wall.endpos, tr_left_ground.plane.normal, NODE_MOVE); + } + // If the right is touching the ground, then we can add a node there + //if (tr_right_ground.fraction < 1.0) + { + tr_right_wall.endpos[2] += (NODE_Z_HEIGHT - 1); + + //if (BOTLIB_TestForNodeDist(tr_right_wall.endpos, NODE_SIZE) == INVALID) + DAIC_Add_Node(tr_right_wall.endpos, tr_right_ground.plane.normal, NODE_MOVE); + } + } + + + + } + } + } + } + //----------------------------------------------------- + } +} + +#define BOTLIB_OrthogonalToVectors(v1, v2, res) \ + (res)[0] = ((v1)[1] * (v2)[2]) - ((v1)[2] * (v2)[1]);\ + (res)[1] = ((v1)[2] * (v2)[0]) - ((v1)[0] * (v2)[2]);\ + (res)[2] = ((v1)[0] * (v2)[1]) - ((v1)[1] * (v2)[0]); + +// Verts must be unwound and in clockwise order +qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t normal, float epsilon) +{ + vec3_t v1, v2; + vec3_t edgevec, pointvec, sepnormal; + + for (int v = 0; v < num_verts; v += 2) // edges + { + VectorCopy(verts[v + 0], v1); + VectorCopy(verts[v + 1], v2); + //edge vector + VectorSubtract(v2, v1, edgevec); + //vector from first edge point to point possible in face + VectorSubtract(point, v1, pointvec); + //get a vector pointing inside the face orthogonal to both the + //edge vector and the normal vector of the plane the face is in + //this vector defines a plane through the origin (first vertex of + //edge) and through both the edge vector and the normal vector + //of the plane + BOTLIB_OrthogonalToVectors(edgevec, normal, sepnormal); + //check on which side of the above plane the point is + //this is done by checking the sign of the dot product of the + //vector orthogonal vector from above and the vector from the + //origin (first vertex of edge) to the point + //if the dotproduct is smaller than zero the point is outside the face + if (DotProduct(pointvec, sepnormal) < -epsilon) return false; + } + + return true; // Found point +} + +void BOTLIB_InitNavigation(edict_t* ent) +{ + bsp_t* bsp = gi.Bsp(); + if (bsp == NULL) + { + gi.dprintf("%s failed to import BSP data\n", __func__); + return; + } + + if (bsp->checksum == 0) + { + gi.dprintf("%s bsp->checksum is zero\n", __func__); + return; + } + + if (ent == NULL) + { + ent = g_edicts; + if (ent == NULL) + return; + } + + BOTLIB_InitNodes(); + + nmesh.bsp_checksum = bsp->checksum; // Save the map checksum + + Remove_All_Doors(); // Make sure all doors are open before making any nodes +} + +void ACEND_BSP(edict_t* ent) +{ + bsp_t* bsp = gi.Bsp(); + if (bsp == NULL) + { + gi.dprintf("%s failed to import BSP data\n", __func__); + return; + } + + if (ent == NULL) + { + ent = g_edicts; + if (ent == NULL) + return; + } + + clock_t clock_begin = clock(); // Start performance clock + + BOTLIB_InitNodes(); + nmesh.bsp_checksum = bsp->checksum; // Save the map checksum + + Remove_All_Doors(); // Make sure all doors are open before making any nodes + + solid_t *trigger_solid = (solid_t*)malloc(sizeof(solid_t) * MAX_EDICTS); + if (trigger_solid == NULL) + { + Com_Printf("%s failed to malloc trigger_solid\n", __func__); + return; + } + if (BOTLIB_MakeEntsSolid(trigger_solid) == -1) + return; + BOTLIB_AddItemNodes(); // Add nodes at item locations + + BOTLIB_BSP_SURFACES(bsp); + + //BOTLIB_Process_NMesh(ent); + + if (1) + { + ent->nav = gi.Nav(); // Grant access to navigation data + if (ent->nav) + { + for (int f = 0; f < ent->nav->faces_total; f++) + { + if (ent->nav->surface_data_faces[f].face_type != FACETYPE_WALK && ent->nav->surface_data_faces[f].face_type != FACETYPE_TINYWALK) continue; + + /* + for (int e = 0; e < ent->nav->surface_data_faces[f].num_verts; e += 2) // Edges + { + // Get center of edge + vec3_t center; + VectorAdd(ent->nav->surface_data_faces[f].verts[e], ent->nav->surface_data_faces[f].verts[e + 1], center); + VectorScale(center, 0.5, center); + + // Move center toward face center by 4 units + vec3_t dir; + VectorSubtract(ent->nav->surface_data_faces[f].center_poly, center, dir); + VectorNormalize(dir); + VectorMA(center, 8, dir, center); + + // Check if we're still in the same face + //BOTLIB_InsideFace + + // Move center up by 32 units + center[2] += 32; + + // Test to see if the center is valid + if (BOTLIB_TestForNodeDist(center, NODE_SIZE) == INVALID) + { + // Add the node + DAIC_Add_Node(center, ent->nav->surface_data_faces[f].normal, NODE_MOVE); + } + } + */ + + + for (int c = 0; c < ent->nav->surface_data_faces[f].snode_counter; c++) + { + vec3_t pos; + VectorCopy(ent->nav->surface_data_faces[f].snodes[c].start, pos); + pos[2] += NODE_Z_HEIGHT; + if (BOTLIB_TestForNodeDist(pos, NODE_SIZE, tv(-16, -16, -16), tv(-16, -16, -16)) == INVALID) + DAIC_Add_Node(pos, ent->nav->surface_data_faces[f].normal, NODE_MOVE); + } + } + } + } + + // Find ladders by searching the center of bottom edges of walls that are CONTENTS_LADDER + ACEND_FindEdgeLadders(); + + // Find the reachability for each node + BOTLIB_ProcesssReachabilities(); + + // Try placing POI nodes on high locations + if (0) + { + /* + float highest_origin = -9999; + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > highest_origin) + highest_origin = nodes[i].origin[2]; + } + float above_height = (highest_origin - 256); + + for (int i = 0; i < numnodes; i++) + { + if (nodes[i].origin[2] > above_height)// && random() < 0.1) + nodes[i].type = NODE_POI; + } + */ + + //ACEND_BuildSpawnPointNodes(); + //ACEND_CachePointOfInterestNodes(); + } + + // Restore solid state of each ent + BOTLIB_RestoreEntsSolidState(trigger_solid); + + + + gi.dprintf("BSP f[%i] t[%i] e[%i] v[%i] r[%i] n[%i]\n", nmesh.total_faces, nmesh.total_tris, nmesh.total_edges, nmesh.total_verts, nmesh.total_reach, numnodes); + clock_t clock_end = clock(); + Com_Printf("%s execution took %f seconds to process bsp reachability\n", __func__, (double)(clock_end - clock_begin) / CLOCKS_PER_SEC); + + //ACEND_BuildVisibilityNodes(); // Rebuild node vis -- generate LOS nodes to target enemies + // Free the memory + if (unsorted_nodes) + { + free(unsorted_nodes); + unsorted_nodes = NULL; // Nullify dangling pointer + } +} //rekkie -- BSP -- e \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 14e244d5e..56e522bfe 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1,2097 +1,2097 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -int dc_total_male_names; // Total male names -int dc_total_female_names; // Total female names -bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy -bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy -bot_connections_t bot_connections; - -// Find a free entity for the bot to use -edict_t* BOTLIB_FindFreeEntity(void) -{ - edict_t* bot = NULL; - for (int i = game.maxclients; i > 0; i--) - { - bot = g_edicts + i + 1; - - if (!bot->inuse) - return bot; // Success - } - - return NULL; // Failure -} - -qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) -{ - FILE* f; - int comments_num = 0; // Keep track of how many comments the file has - int line_num = 0; // Keep track of lines - int curr_len; // Current length of the line - char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) - curr_line[0] = '\0'; // Current line - - cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - char filename[MAX_QPATH]; // Filename to load from - int i; // Keep track where we are in the filename array -#ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, file); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/"); - strcat(filename, file); -#endif - - if (strlen(filename) <= 0) return false; - - // Open and check for success - if ((f = fopen(filename, "r")) == NULL) // Read text file - { - gi.dprintf("%s failed to open bot file: %s\n", __func__, filename); - return false; - } - - // Find how many lines the file has - int random_line; - while (fgets(curr_line, sizeof(curr_line), f)) - { - line_num++; - } - fclose(f); - random_line = rand() % line_num + 1; - //random_line = 13; - - //Com_Printf("%s %s random_line[%d]\n", __func__, filename, random_line); - - // Open and check for success - if ((f = fopen(filename, "r")) != NULL) // Read text file - { - line_num = -1; - - // Read each line - while (fgets(curr_line, sizeof(curr_line), f)) - { - line_num++; // Advance forward - - //if (feof(f)) // End of file - // break; // Could not get a prefix name - - if (curr_line[0] == '/' && curr_line[1] == '/') // Skip comment lines - continue; - - // If we're at or above the correct line, not a comment, not empty - //if (line_num >= random_line && curr_line[0] != '/' && curr_line[0]) - if (line_num >= random_line) - { - curr_len = strlen(curr_line); - if (curr_len < MAX_QPATH) // Make sure name is within acceptable length - { - //Com_Printf("%s %s curr_line[%d] = %s\n", __func__, filename, line_num, curr_line); - Q_strlcpy(buffer, curr_line, curr_len); - break; - } - } - } - fclose(f); - } - - if (strlen(buffer) > 0) - { - return true; - } - else - { - buffer[0] = '\0'; // Terminate empty string - return false; - } -} - -// Save bots from the previous map to file -#define BOTS_FILE_PREV_MAP_VERSION 1 -qboolean BOTLIB_SaveBotsFromPreviousMap(void) -{ - FILE* f; - char filename[128]; - const char* file = "prev_map_bots.txt"; - int i; - cvar_t* game_dir = gi.cvar("game", "action", 0); - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - - //if (bot_connections.total_bots <= 0) // Don't write unless we have bots - // return false; - -#ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, file); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/"); - strcat(filename, file); -#endif - - //Com_Printf("%s Writing previous bots to file %s\n", __func__, filename); - - // Write to file - f = fopen(filename, "w"); // Open the file in write mode - if (f == NULL) - { - Com_Printf("Could not write to file %s\n", filename); - return false; - } - - fprintf(f, "// Bots from a previous map\n"); - fprintf(f, "%d\n", BOTS_FILE_PREV_MAP_VERSION); - fprintf(f, "%d\n", bot_connections.total_bots); - fprintf(f, "%d\n", (int)bot_maxteam->value); - if (use_3teams->value) - fprintf(f, "3teams\n"); - else if (teamplay->value) - fprintf(f, "teamplay\n"); - else - fprintf(f, "deathmatch\n"); - - for (i = 0; i < num_players; i++) - { - if (players[i]->is_bot == false) continue; // Skip humans - - // Gender - fprintf(f, "%d\n", players[i]->client->pers.gender); - - // Team - fprintf(f, "%d\n", players[i]->client->resp.team); - - // Name - fprintf(f, "%s\n", players[i]->client->pers.netname); - - // Skin - fprintf(f, "%s\n", Info_ValueForKey(players[i]->client->pers.userinfo, "skin")); - } - - fclose(f); // Close the file after use - - return true; -} - -// Add bots from the previous map from file -// Parameters: -// percent of bots to keep from previous map -qboolean BOTLIB_AddBotsFromPreviousMap(float percent) -{ - FILE* f; - qboolean print_dbg = false; // Print debug info - int line_num = 0; // Keep track of lines - - int prev_bots_skipped = 0; // How many bots we skipped adding from previous map - int prev_bot_count = 0; // How many bots from the previous map - int bots_added = 0; // How many bots we added from previous map - char gamemode[MAX_QPATH]; // Gamemode string buffer - int prev_gamemode = 0; // Previous gamemode - int prev_team = 0; // Previous Team - int prev_gender = 0; // Previous Gender - char prev_name[MAX_QPATH]; // Previous Name - char prev_skin[MAX_QPATH]; // Previous Skin - - int curr_len = 0; // Current length of the line - char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) - curr_line[0] = '\0'; // Current line - - const char* file = "prev_map_bots.txt"; - cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - char filename[MAX_QPATH]; // Filename to load from - int i; // Keep track where we are in the filename array -#ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, file); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/"); - strcat(filename, file); -#endif - - if (strlen(filename) <= 0) return false; - - // Open and check for success - if ((f = fopen(filename, "r")) == NULL) // Read text file - { - return false; // Ignore if file doesn't exist - } - - // Open and check for success - if ((f = fopen(filename, "r")) != NULL) // Read text file - { - // Ignore first line - fgets(curr_line, sizeof(curr_line), f); // First line is commented - if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); - - // Get file version - fgets(curr_line, sizeof(curr_line), f); - int version = atoi(curr_line); - if (print_dbg) Com_Printf("%s line[%d] version[%d]\n", __func__, ++line_num, version); - if (version != BOTS_FILE_PREV_MAP_VERSION) - { - return false; - } - - // Get previous bot count - fgets(curr_line, sizeof(curr_line), f); - prev_bot_count = atoi(curr_line); - if (print_dbg) Com_Printf("%s line[%d] bot_count[%d]\n", __func__, ++line_num, prev_bot_count); - if (prev_bot_count <= 0) - { - //gi.dprintf("%s failed to process bot count from file: %s\n", __func__, filename); - return false; - } - - // Get previous bot_maxteam num - fgets(curr_line, sizeof(curr_line), f); - //bot_maxteam->value = atoi(curr_line); - gi.cvar_set("bot_maxteam", va("%d", atoi(curr_line))); // Override if manually added - if (print_dbg) Com_Printf("%s line[%d] bot_maxteam[%f]\n", __func__, ++line_num, bot_maxteam->value); - if (bot_maxteam->value < 0 || bot_maxteam->value > MAX_CLIENTS) - { - gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if manually added - gi.dprintf("%s failed to process bot_maxteam from file: %s\n", __func__, filename); - return false; - } - - // Get gamemode - fgets(curr_line, sizeof(curr_line), f); // Second line is gamemode - if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); - curr_len = strlen(curr_line); - Q_strlcpy(gamemode, curr_line, curr_len); - if (Q_strcasestr(gamemode, "deathmatch") != NULL) - { - if (print_dbg) Com_Printf("%s Gamemode is DEATHMATCH\n", __func__); - prev_gamemode = 1; // DM - } - else if (Q_strcasestr(gamemode, "teamplay") != NULL) - { - if (print_dbg) Com_Printf("%s Gamemode is TEAMPLAY\n", __func__); - prev_gamemode = 2; // TP - } - else if (Q_strcasestr(gamemode, "3teams") != NULL) - { - if (print_dbg) Com_Printf("%s Gamemode is 3TEAMS\n", __func__); - prev_gamemode = 3; // 3TEAMS - } - - // Read each line - while (fgets(curr_line, sizeof(curr_line), f)) - { - // Get gender - prev_gender = atoi(curr_line); - if (prev_gender < GENDER_MALE || prev_gender > GENDER_NEUTRAL) - { - Com_Printf("%s failed to process bot gender from file: %s\n", __func__, filename); - return false; - } - - // Get team - fgets(curr_line, sizeof(curr_line), f); // Get next line - prev_team = atoi(curr_line); - if (prev_team < NOTEAM || prev_team > TEAM3) - { - Com_Printf("%s failed to process bot team from file: %s\n", __func__, filename); - return false; - } - - // Get name - fgets(curr_line, sizeof(curr_line), f); // Get next line - curr_len = strlen(curr_line); - if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length - continue; - Q_strlcpy(prev_name, curr_line, curr_len); - - // Get skin - fgets(curr_line, sizeof(curr_line), f); // Get next line - curr_len = strlen(curr_line); - if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length - continue; - Q_strlcpy(prev_skin, curr_line, curr_len); - - if (print_dbg) Com_Printf("%s %s curr_line[%d]. GM[%s] Gender[%d] Team[%d] Name[%s] Skin[%s]\n", __func__, filename, ++line_num, gamemode, prev_gender, prev_team, prev_name, prev_skin); - - if (random() > (percent / 100)) // Percent chance to skip adding bot from previous map - { - prev_bots_skipped++; // Bots that were not added from previous map - continue; - } - - //if (bot_maxteam->value && bots_added >= (int)bot_maxteam->value) // Limit bots to bot_maxteam, if its set - // break; - if (prev_bot_count && bots_added >= prev_bot_count) - break; - - if (use_3teams->value) - { - // Bot was previously in a different game mode joining a 3teams game, randomize the team it joins - if (prev_gamemode == 1 || prev_gamemode == 2) // DM or TP - prev_team = (rand() % TEAM3) + 1; - - if (prev_team == TEAM1) bot_connections.desire_team1++; - else if (prev_team == TEAM2) bot_connections.desire_team2++; - else if (prev_team == TEAM3) bot_connections.desire_team3++; - - if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); - BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); - bots_added++; - } - else if (teamplay->value) // TP and CTF - { - // Bot was previously in a different game mode joining a teamplay game, randomize the team it joins - if (prev_gamemode == 1 || prev_gamemode == 3) // DM or 3TEAMS - prev_team = (rand() % TEAM2) + 1; - - if (prev_team == TEAM1) bot_connections.desire_team1++; - else if (prev_team == TEAM2) bot_connections.desire_team2++; - - if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); - BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); - bots_added++; - } - else // DM - { - if (strlen(prev_skin)) - BOTLIB_SpawnBot(0, prev_gender, prev_name, prev_skin); - else - BOTLIB_SpawnBot(0, prev_gender, prev_name, NULL); - bots_added++; - } - } - fclose(f); - } - - - if (bots_added) - { - //if (bot_maxteam->value < 1) // If max team wasn't set, use it so bots can be shuffled - // bot_maxteam->value = prev_bot_count; - - bot_connections.desire_bots = bots_added; // How many bots - //bot_connections.auto_balance_bots = true; - - return true; - } - - return false; -} - -void BOTLIB_RandomizeTeamNames(edict_t* bot) -{ - char name[MAX_QPATH]; - int len = 0; - const int max_name_size = sizeof(teams[TEAM1].name); // Team names are limited to 20 characters (19 chars + terminator) - - qboolean same_name = false; - for (int i = 0; i < MAX_TEAMS; i++) // For each team - { - // Try for random team names that fit within 18 characters - for (int t = 0; t < 32; t++) // Max loops - { - BOTLIB_GetRandomBotFileLine("team_names.txt", name); // Get a random team name - - // Get name lengths - len = strlen(name); - - // Find if the name length will fit within 20 chars (name + space + terminator == 20) - if (len <= (max_name_size - 2)) // adjust max size to account for space + terminator - { - // Make sure the name is different - if (Q_strcasestr(name, teams[TEAM1].name) != NULL) // If same name as Team1 - continue; - if (Q_strcasestr(name, teams[TEAM2].name) != NULL) // If same name as Team2 - continue; - if (Q_strcasestr(name, teams[TEAM3].name) != NULL) // If same name as Team3 - continue; - - // Merge the names together - Q_strncpyz(teams[i].name, va("%s", name), len + 2); // name + space + terminator - Com_Printf("%s Setting Team Name: %s\n", __func__, teams[i].name); - break; - } - } - } -} - - -void BOTLIB_RandomizeTeamSkins(edict_t* bot) -{ - //Com_Printf("%s before T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); - - // Randomize skins - BOTLIB_RandomSkin(bot, teams[TEAM1].skin, INVALID); - BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); - BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); - - // Check if any of the randomized skins are the same, if so re-roll them until they're different - qboolean same_skin = false; - for (int i = 0; i < 64; i++) - { - same_skin = false; - if (Q_strcasestr(teams[TEAM2].skin, teams[TEAM1].skin) != NULL) // If Team2 is the same skin as Team1 - { - same_skin = true; - //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); - BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); // Re-randomize skin - } - - if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM1].skin) != NULL) // If Team3 is the same skin as Team1 - { - same_skin = true; - //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); - BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin - } - - if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM2].skin) != NULL) // If Team3 is the same skin as Team2 - { - same_skin = true; - //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); - BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin - } - - if (same_skin == false) // No same skins detected, so exit loop early - break; - } - - //Com_Printf("%s after T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); - - // Update skin associated indexes - Q_snprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); - Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); - Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); - - // Change team picture using the updated skin indexes - level.pic_teamskin[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); - level.pic_teamskin[TEAM2] = gi.imageindex(teams[TEAM2].skin_index); - level.pic_teamskin[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); -} - -/* -void DC_CacheRandomBotNames(void) -{ - int i, n; - - // Cache male names - n = 0; - for (i = 0; i < MAX_BOT_NAMES; i++) - { - DC_LoadRandomBotName(GENDER_MALE, &bot_male[n]); - n++; - } - // Cache female names - n = 0; - for (i = 0; i < MAX_BOT_NAMES; i++) - { - DC_LoadRandomBotName(GENDER_MALE, &bot_female[n]); - n++; - } -} -void DC_GetRandomBotName(byte gender, char* bot_name) -{ - if (gender == GENDER_MALE) - { - //Q_strlcpy(bot_name, bot_male[n], name_length); - } - else if (gender == GENDER_FEMALE) - { - - } - else - { - if (random() < 0.5) - { - - } - else - { - - } - } -} -*/ - -//====================================== -// BOTLIB_GetRandomClanSymbol() -// Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc -// Returns char -//====================================== -int BOTLIB_GetRandomClanSymbol() -{ - int sym = rand() % 25; - switch (sym) - { - case 0: return '!'; - case 1: return '#'; - case 2: return '$'; - case 3: return '%'; - case 4: return '&'; - case 5: return '('; - case 6: return ')'; - case 7: return '*'; - case 8: return '+'; - case 9: return ','; - case 10: return '-'; - case 11: return '.'; - //case 12: return '/'; - case 12: return ':'; - case 13: return '<'; - case 14: return '='; - case 15: return '>'; - case 16: return '?'; - case 17: return '@'; - case 18: return '['; - case 19: return ']'; - //case 21: return '\\'; - case 20: return '^'; - case 21: return '_'; - case 22: return '{'; - case 23: return '|'; - case 24: return '}'; - default: return ' '; - } -} - -//====================================== -// BOTLIB_GetOpposingClanSymbol() -// If a symbol has an opposite: ie --> [ ], ( ), { }, < >, etc -// Returns opposite side, else returns the input symbol -//====================================== -int BOTLIB_GetOpposingClanSymbol(char symbol) -{ - if (symbol == '[') - return ']'; - if (symbol == ']') - return '['; - - if (symbol == '(') - return ')'; - if (symbol == ')') - return '('; - - if (symbol == '{') - return '}'; - if (symbol == '}') - return '{'; - - if (symbol == '<') - return '>'; - if (symbol == '>') - return '<'; - - if (symbol == '\\') - return '/'; - if (symbol == '/') - return '\\'; - - return symbol; -} - - -//====================================== -// BOTLIB_GetRandomClanLetter() -// Gets a random clan letter -// Returns char -//====================================== -int BOTLIB_GetRandomClanLetter() -{ - // Gets a random ASCII letter between 65 and 90, or 97 and 122 - int letter = rand() % 52; - switch (letter) - { - case 0: return 'A'; - case 1: return 'B'; - case 2: return 'C'; - case 3: return 'D'; - case 4: return 'E'; - case 5: return 'F'; - case 6: return 'G'; - case 7: return 'H'; - case 8: return 'I'; - case 9: return 'J'; - case 10: return 'K'; - case 11: return 'L'; - case 12: return 'M'; - case 13: return 'N'; - case 14: return 'O'; - case 15: return 'P'; - case 16: return 'Q'; - case 17: return 'R'; - case 18: return 'S'; - case 19: return 'T'; - case 20: return 'U'; - case 21: return 'V'; - case 22: return 'W'; - case 23: return 'X'; - case 24: return 'Y'; - case 25: return 'Z'; - case 26: return 'a'; - case 27: return 'b'; - case 28: return 'c'; - case 29: return 'd'; - case 30: return 'e'; - case 31: return 'f'; - case 32: return 'g'; - case 33: return 'h'; - case 34: return 'i'; - case 35: return 'j'; - case 36: return 'k'; - case 37: return 'l'; - case 38: return 'm'; - case 39: return 'n'; - case 40: return 'o'; - case 41: return 'p'; - case 42: return 'q'; - case 43: return 'r'; - case 44: return 's'; - case 45: return 't'; - case 46: return 'u'; - case 47: return 'v'; - case 48: return 'w'; - case 49: return 'x'; - case 50: return 'y'; - case 51: return 'z'; - default: return ' '; - } -} - - - -void BOTLIB_GetRandomName(char* name, const int gender) -{ - if (gender == GENDER_MALE) - { - if (random() < 0.8) - BOTLIB_GetRandomBotFileLine("males.txt", name); - else - BOTLIB_GetRandomBotFileLine("other.txt", name); - } - else // GENDER_FEMALE - BOTLIB_GetRandomBotFileLine("females.txt", name); - - //if (strlen(name) <= 0) // Something went wrong. No name was set. - // Q_strlcpy(name, "AqtionMan", 9); // Set a default -} - -void BOTLIB_GetNamePrefix(char* name, const int type) -{ - if (type) - BOTLIB_GetRandomBotFileLine("clans.txt", name); // Clan tag - else - BOTLIB_GetRandomBotFileLine("prefix.txt", name); // Prefix -} - -void BOTLIB_GetNamePostfix(char* name, const int type) -{ - BOTLIB_GetRandomBotFileLine("postfix.txt", name); // Postfix -} - -void BOTLIB_GetRandomPrefix(char* name) -{ - byte tag = 0; - char outer_symbol = 0; - char inner_symbol = 0; - - outer_symbol = BOTLIB_GetRandomClanSymbol(); - if (random() < 0.5) // 50% chance to add an inner symbol - inner_symbol = BOTLIB_GetRandomClanSymbol(); - - // Add the prefix tag - name[tag] = outer_symbol; - if (inner_symbol) - name[++tag] = inner_symbol; - - // Add the clan acronym - name[++tag] = BOTLIB_GetRandomClanLetter(); - name[++tag] = BOTLIB_GetRandomClanLetter(); - if (random() < 0.25) name[++tag] = BOTLIB_GetRandomClanLetter(); // percent chance to add a third clan letter - - // Add the suffix tag - if (inner_symbol) - name[++tag] = BOTLIB_GetOpposingClanSymbol(inner_symbol); // Add the opposing symbol, if any - name[++tag] = BOTLIB_GetOpposingClanSymbol(outer_symbol); - - // print the bot name - name[++tag] = '\0'; - //Com_Printf("CLAN TAG %s\n", name); -} - - - -//====================================== -// DC_LoadRandomBotName() -// Gets a random bot name from file; bot_name is limited to a maximum of 16 chars, and the size of the list can be up to random()'s maximum of 32,767 -// Genders can be: GENDER_MALE, GENDER_FEMALE, GENDER_NEUTRAL -// Returns bot_name -//====================================== -void BOTLIB_LoadRandomBotName(edict_t* bot, char* bot_name, int gender) -{ - //for (int n = 0; n < 100; n++) - { - int rnglen = 0; // Length of rng name - int namelen = 0; // Length of name - int clanlen = 0; // Length of clan name - int prefixlen = 0; // Length of Prefix name - int postfixlen = 0; // Length of Postfix name - char name[MAX_QPATH]; // Name - char clan_name[MAX_QPATH]; // Clan name - char prefix_name[MAX_QPATH]; // Prefix name - char postfix_name[MAX_QPATH]; // Postfix name - char rng_clan_name[MAX_QPATH]; // Random Clan name - qboolean name_with_rng = false; // If name + rng clan will fit within 16 characters - qboolean name_with_clan = false; // If name + clan will fit within 16 characters - qboolean name_with_prefix = false; // If name + prefix will fit within 16 characters - qboolean name_with_postfix = false; // If name + postfix will fit within 16 characters - - // Get random names from file - //Q_strlcpy(name, "AqtionMan", 10); // Set a default name - BOTLIB_GetRandomName(name, gender); // Get name based on gender - BOTLIB_GetNamePrefix(clan_name, 0); - BOTLIB_GetNamePrefix(prefix_name, 1); - BOTLIB_GetNamePostfix(postfix_name, 1); - BOTLIB_GetRandomPrefix(rng_clan_name); - - // Decide if we're going to only take only part of a name (i.e. Arnold Schwarzenegger - we can take either Arnold or Schwarzenegger) - if (rand() % 2 == 0) - { - qboolean two_part_name = false; - char name_cpy[MAX_QPATH]; - char name_pt1[MAX_QPATH]; - char name_pt2[MAX_QPATH]; - Q_strlcpy(name_cpy, name, sizeof(name)); // Make a copy of the original string because strtok() will edit it - char* split_name = strtok(name_cpy, " "); - if (split_name != NULL) - { - Q_strlcpy(name_pt1, split_name, sizeof(split_name)); - split_name = strtok(NULL, " "); - } - if (split_name != NULL) - { - two_part_name = true; - Q_strlcpy(name_pt2, split_name, sizeof(split_name)); - } - if (two_part_name) - { - //Com_Printf("%s x1 name[%s]\n", __func__, name_pt1, strlen(name_pt1)); - //Com_Printf("%s x2 name[%s]\n", __func__, name_pt2, strlen(name_pt2)); - - if (rand() % 2 == 0) - Q_strlcpy(name, name_pt1, sizeof(name_pt1)); - else - Q_strlcpy(name, name_pt2, sizeof(name_pt2)); - } - - //Com_Printf("%s full name[%s]\n", __func__, name, strlen(name)); - } - - //Com_Printf("%s clan[%s][%d] prefix[%s][%d] postfix[%s][%d] rng_clan_name[%s][%d] name[%s][%d]\n", __func__, clan_name, strlen(clan_name), prefix_name, strlen(prefix_name), postfix_name, strlen(postfix_name), rng_clan_name, strlen(rng_clan_name), name, strlen(name)); - - // Get name lengths - namelen = strlen(name); - rnglen = strlen(rng_clan_name); - clanlen = strlen(clan_name); - prefixlen = strlen(prefix_name); - postfixlen = strlen(postfix_name); - - // Find what fits with the bot's name (14 chars for both names + space in between == 15) - if ((namelen + rnglen) < 15) - name_with_rng = true; - if ((namelen + clanlen) < 15) - name_with_clan = true; - if ((namelen + prefixlen) < 15) - name_with_prefix = true; - if ((namelen + postfixlen) < 15) - name_with_postfix = true; - - Q_strncpyz(bot_name, va("%s", name), namelen + 2); // Set a random name - - // Merge the names together - int name_type = rand() % 100; - if (name_type < 15 && name_with_rng) - Q_strncpyz(bot_name, va("%s %s", rng_clan_name, name), namelen + rnglen + 2); // RNG clan + name - else if (name_type < 20 && name_with_clan) - Q_strncpyz(bot_name, va("%s %s", clan_name, name), namelen + clanlen + 2); // clan + name - else if (name_type < 25 && name_with_prefix) - Q_strncpyz(bot_name, va("%s %s", prefix_name, name), namelen + prefixlen + 2); // prefix + name - else if (name_type < 30 && name_with_postfix) - Q_strncpyz(bot_name, va("%s %s", name, postfix_name), namelen + postfixlen + 2); // name + postfix - - //Com_Printf("%s name[%s][%d]\n", __func__, bot_name, strlen(bot_name)); - } -} - -// Sets a random skin and returns the skin's gender -// Parameters: gender - If a valid gender is passed, and not INVALID (-1) then pick a skin based on gender -int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender) -{ - int type = 0, rnd; - - strcpy(skin, "male/grunt"); // Default skin - - // Size of each collection of skins - const int actionmale = 30; - const int actionrally = 6; - const int aqmarine = 8; - const int female = 22; - const int male = 88; - const int messiah = 26; - const int sas = 25; - const int sydney = 2; - const int terror = 25; - - // When NOT forcing a gender using - if (force_gender == INVALID) - { - int rng_type = rand() % 2; // Randomize which randomizer we use. Either by [1] Collection Size, or [2] Random Type. - - // -- [1] Collection Size -- - // Since each collection varies in size, our random choice takes into account the size of the collection of skins. - // Collections with the most skins have a greater chance to be chosen. - if (rng_type == 0) - { - int sum = actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney + terror; - int rnd = (rand() % sum) + 1; // Random number from total - - if (rnd <= actionmale) - type = 0; // actionmale - else if (rnd <= (actionmale + actionrally)) - type = 1; // actionrally - else if (rnd <= (actionmale + actionrally + aqmarine)) - type = 2; // aqmarine - else if (rnd <= (actionmale + actionrally + aqmarine + female)) - type = 3; // female - else if (rnd <= (actionmale + actionrally + aqmarine + female + male)) - type = 4; // male - else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah)) - type = 5; // messiah - else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas)) - type = 6; // sas - else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney)) - type = 7; // sydney - else - type = 8; // terror - } - // -- [2] Random Type -- - else // Alternatively we could just randomly pick from one of the collections regardless of how many skins it has - { - type = rand() % 9; // Actionmale, actionrally, aqmarine, female, male, messiah, sas, sydney, terror - } - } - else // Forcing a gender - { - // (type == 0) // Actionmale -- male - // (type == 1) // Actionrally -- female - // (type == 2) // Aqmarine -- male - // (type == 3) // Female -- female - // (type == 4) // Male -- male - // (type == 5) // Messiah -- male - // (type == 6) // Sas -- male - // (type == 7) // Sydney -- female - // (type == 8) // Terror -- male - - if (force_gender == GENDER_MALE) - { - int rnd_gen_skin = rand() % 6; // Pick one of the male skins - - if (rnd_gen_skin == 0) type = 0; // Actionmale - else if (rnd_gen_skin == 1) type = 2; // Aqmarine - else if (rnd_gen_skin == 2) type = 4; // Male - else if (rnd_gen_skin == 3) type = 5; // Messiah - else if (rnd_gen_skin == 4) type = 6; // Sas - else type = 8; // Terror - } - else // GENDER_FEMALE - { - int rnd_gen_skin = rand() % 2; // Pick one of the female skins - - if (rnd_gen_skin == 0) type = 1; // Actionrally - else type = 3; // Female - // Skipping Sydney because skin has no VWEP - } - } - - if (type == 0) // Actionmale - { - rnd = rand() % actionmale; - switch (rnd) - { - case 0: sprintf(skin, "actionmale/agent"); break; - case 1: sprintf(skin, "actionmale/aqgthugboss"); break; - case 2: sprintf(skin, "actionmale/axef"); break; - case 3: sprintf(skin, "actionmale/badmutha"); break; - case 4: sprintf(skin, "actionmale/BlackStars"); break; - case 5: sprintf(skin, "actionmale/blood"); break; - case 6: sprintf(skin, "actionmale/blucedojo"); break; - case 7: sprintf(skin, "actionmale/bluceree"); break; - case 8: sprintf(skin, "actionmale/Bruce_Wayne"); break; - case 9: sprintf(skin, "actionmale/castor"); break; - case 10: sprintf(skin, "actionmale/chellobeta"); break; - case 11: sprintf(skin, "actionmale/chucky"); break; - case 12: sprintf(skin, "actionmale/crip"); break; - case 13: sprintf(skin, "actionmale/ctf_b"); break; - case 14: sprintf(skin, "actionmale/ctf_r"); break; - case 15: sprintf(skin, "actionmale/dafist"); break; - case 16: sprintf(skin, "actionmale/dubeta"); break; - case 17: sprintf(skin, "actionmale/invince"); break; - case 18: sprintf(skin, "actionmale/jewels"); break; - case 19: sprintf(skin, "actionmale/jungseal"); break; - case 20: sprintf(skin, "actionmale/killer"); break; - case 21: sprintf(skin, "actionmale/leonreno"); break; - case 22: sprintf(skin, "actionmale/lfc"); break; - case 23: sprintf(skin, "actionmale/morpheus"); break; - case 24: sprintf(skin, "actionmale/RCMP"); break; - case 25: sprintf(skin, "actionmale/scarf"); break; - case 26: sprintf(skin, "actionmale/scarfchain"); break; - case 27: sprintf(skin, "actionmale/seagull"); break; - case 28: sprintf(skin, "actionmale/shinobi"); break; - case 29: sprintf(skin, "actionmale/swat"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - else if (type == 1) // Actionrally - { - rnd = rand() % actionrally; - switch (rnd) - { - case 0: sprintf(skin, "actionrally/ctf_b"); break; - case 1: sprintf(skin, "actionrally/ctf_r"); break; - case 2: sprintf(skin, "actionrally/modsquad"); break; - case 3: sprintf(skin, "actionrally/natasha"); break; - case 4: sprintf(skin, "actionrally/pulp"); break; - case 5: sprintf(skin, "actionrally/trinity"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_FEMALE; - } - else if (type == 2) // Aqmarine - { - rnd = rand() % aqmarine; - switch (rnd) - { - case 0: sprintf(skin, "aqmarine/aquamarine"); break; - case 1: sprintf(skin, "aqmarine/centurion"); break; - case 2: sprintf(skin, "aqmarine/desert"); break; - case 3: sprintf(skin, "aqmarine/marine1"); break; - case 4: sprintf(skin, "aqmarine/marine2"); break; - case 5: sprintf(skin, "aqmarine/urban"); break; - case 6: sprintf(skin, "aqmarine/usmc"); break; - case 7: sprintf(skin, "aqmarine/woods"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - else if (type == 3) // Female - { - rnd = rand() % female; - switch (rnd) - { - case 0: sprintf(skin, "female/ctf_b"); break; - case 1: sprintf(skin, "female/ctf_r"); break; - case 2: sprintf(skin, "female/kw_aqua"); break; - case 3: sprintf(skin, "female/kw_black"); break; - case 4: sprintf(skin, "female/kw_blue"); break; - case 5: sprintf(skin, "female/kw_green"); break; - case 6: sprintf(skin, "female/kw_pink"); break; - case 7: sprintf(skin, "female/kw_red"); break; - case 8: sprintf(skin, "female/kw_white"); break; - case 9: sprintf(skin, "female/kw_yellow"); break; - case 10: sprintf(skin, "female/leeloop"); break; - case 11: sprintf(skin, "female/sarah_ohconnor"); break; - case 12: sprintf(skin, "female/tankgirl"); break; - - // baseq2 skins (No longer supplied with Aqtion) - case 13: sprintf(skin, "female/athena"); break; - case 14: sprintf(skin, "female/brianna"); break; - case 15: sprintf(skin, "female/cobalt"); break; - case 16: sprintf(skin, "female/doomgal"); break; - case 17: sprintf(skin, "female/ensign"); break; - case 18: sprintf(skin, "female/jezebel"); break; - case 19: sprintf(skin, "female/jungle"); break; - case 20: sprintf(skin, "female/venus"); break; - case 21: sprintf(skin, "female/voodoo"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_FEMALE; - } - else if (type == 4) // Male - { - rnd = rand() % male; - switch (rnd) - { - case 0: sprintf(skin, "male/{oz}"); break; - case 1: sprintf(skin, "male/adidas"); break; - case 2: sprintf(skin, "male/aqgthug"); break; - case 3: sprintf(skin, "male/aqgvillain"); break; - case 4: sprintf(skin, "male/austin2"); break; - case 5: sprintf(skin, "male/babarracuda"); break; - case 6: sprintf(skin, "male/beastdl"); break; - case 7: sprintf(skin, "male/bluebeard"); break; - case 8: sprintf(skin, "male/blues"); break; - case 9: sprintf(skin, "male/boba"); break; - case 10: sprintf(skin, "male/borya"); break; - case 11: sprintf(skin, "male/bravo"); break; - case 12: sprintf(skin, "male/brucelee"); break; - case 13: sprintf(skin, "male/bruces"); break; - case 14: sprintf(skin, "male/chow"); break; - case 15: sprintf(skin, "male/chowda"); break; - case 16: sprintf(skin, "male/Clan_UNA"); break; - case 17: sprintf(skin, "male/conan"); break; - case 18: sprintf(skin, "male/coolio"); break; - case 19: sprintf(skin, "male/cop"); break; - case 20: sprintf(skin, "male/copper"); break; - case 21: sprintf(skin, "male/ctf_b"); break; - case 22: sprintf(skin, "male/ctf_g"); break; - case 23: sprintf(skin, "male/ctf_r"); break; - case 24: sprintf(skin, "male/ctf_y"); break; - case 25: sprintf(skin, "male/cyrus"); break; - case 26: sprintf(skin, "male/dashaft"); break; - case 27: sprintf(skin, "male/deadpl"); break; - case 28: sprintf(skin, "male/elway"); break; - case 29: sprintf(skin, "male/GreenBeret"); break; - case 30: sprintf(skin, "male/grunt"); break; - case 31: sprintf(skin, "male/habs"); break; - case 32: sprintf(skin, "male/hall"); break; - case 33: sprintf(skin, "male/hellspawn"); break; - case 34: sprintf(skin, "male/hollywood"); break; - case 35: sprintf(skin, "male/homer"); break; - case 36: sprintf(skin, "male/hulk2"); break; - case 37: sprintf(skin, "male/image"); break; - case 38: sprintf(skin, "male/indy"); break; - case 39: sprintf(skin, "male/invince"); break; - case 40: sprintf(skin, "male/jax-jaguar"); break; - case 41: sprintf(skin, "male/jdredd"); break; - case 42: sprintf(skin, "male/jewels"); break; - case 43: sprintf(skin, "male/jules"); break; - case 44: sprintf(skin, "male/kgb"); break; - case 45: sprintf(skin, "male/kw_aqua"); break; - case 46: sprintf(skin, "male/kw_black"); break; - case 47: sprintf(skin, "male/kw_blue"); break; - case 48: sprintf(skin, "male/kw_green"); break; - case 49: sprintf(skin, "male/kw_orange"); break; - case 50: sprintf(skin, "male/kw_pink"); break; - case 51: sprintf(skin, "male/kw_red"); break; - case 52: sprintf(skin, "male/kw_white"); break; - case 53: sprintf(skin, "male/kw_yellow"); break; - case 54: sprintf(skin, "male/leaf"); break; - case 55: sprintf(skin, "male/leservoircat"); break; - case 56: sprintf(skin, "male/mariom"); break; - case 57: sprintf(skin, "male/marsell"); break; - case 58: sprintf(skin, "male/mason"); break; - case 59: sprintf(skin, "male/MikeLowry"); break; - case 60: sprintf(skin, "male/mkrain"); break; - case 61: sprintf(skin, "male/mkrept"); break; - case 62: sprintf(skin, "male/mkscorp"); break; - case 63: sprintf(skin, "male/mksub"); break; - case 64: sprintf(skin, "male/Mohoney"); break; - case 65: sprintf(skin, "male/mr_t"); break; - case 66: sprintf(skin, "male/nut"); break; - case 67: sprintf(skin, "male/nwom"); break; - case 68: sprintf(skin, "male/optimus"); break; - case 69: sprintf(skin, "male/oz"); break; - case 70: sprintf(skin, "male/pimp"); break; - case 71: sprintf(skin, "male/police"); break; - case 72: sprintf(skin, "male/resdog"); break; - case 73: sprintf(skin, "male/rigs"); break; - case 74: sprintf(skin, "male/robber"); break; - case 75: sprintf(skin, "male/roger"); break; - case 76: sprintf(skin, "male/rredneck"); break; - case 77: sprintf(skin, "male/sabotage"); break; - case 78: sprintf(skin, "male/santa"); break; - case 79: sprintf(skin, "male/sdes"); break; - case 80: sprintf(skin, "male/shaft"); break; - case 81: sprintf(skin, "male/siris"); break; - case 82: sprintf(skin, "male/snowcamo"); break; - case 83: sprintf(skin, "male/Superman"); break; - case 84: sprintf(skin, "male/t-800m"); break; - case 85: sprintf(skin, "male/trmntr"); break; - case 86: sprintf(skin, "male/walter"); break; - case 87: sprintf(skin, "male/wcwsting3"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - else if (type == 5) // Messiah - { - rnd = rand() % messiah; - switch (rnd) - { - case 0: sprintf(skin, "messiah/axe"); break; - case 1: sprintf(skin, "messiah/badger"); break; - case 2: sprintf(skin, "messiah/blackman"); break; - case 3: sprintf(skin, "messiah/blade"); break; - case 4: sprintf(skin, "messiah/chow"); break; - case 5: sprintf(skin, "messiah/chowblue"); break; - case 6: sprintf(skin, "messiah/chowred"); break; - case 7: sprintf(skin, "messiah/cptahab"); break; - case 8: sprintf(skin, "messiah/crawler"); break; - case 9: sprintf(skin, "messiah/ctf_b"); break; - case 10: sprintf(skin, "messiah/ctf_r"); break; - case 11: sprintf(skin, "messiah/darkbeing"); break; - case 12: sprintf(skin, "messiah/evil_axe"); break; - case 13: sprintf(skin, "messiah/keaneo"); break; - case 14: sprintf(skin, "messiah/kindig"); break; - case 15: sprintf(skin, "messiah/leon"); break; - case 16: sprintf(skin, "messiah/mxr_neo"); break; - case 17: sprintf(skin, "messiah/neo"); break; - case 18: sprintf(skin, "messiah/outlaw"); break; - case 19: sprintf(skin, "messiah/robber"); break; - case 20: sprintf(skin, "messiah/scuba"); break; - case 21: sprintf(skin, "messiah/slug"); break; - case 22: sprintf(skin, "messiah/spectre"); break; - case 23: sprintf(skin, "messiah/suit"); break; - case 24: sprintf(skin, "messiah/TheCrow"); break; - case 25: sprintf(skin, "messiah/thedon"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - else if (type == 6) // Sas - { - rnd = rand() % sas; - switch (rnd) - { - case 0: sprintf(skin, "sas/aqmsas"); break; - case 1: sprintf(skin, "sas/atf"); break; - case 2: sprintf(skin, "sas/ctf_b"); break; - case 3: sprintf(skin, "sas/ctf_r"); break; - case 4: sprintf(skin, "sas/deathreat"); break; - case 5: sprintf(skin, "sas/fbi"); break; - case 6: sprintf(skin, "sas/grassland"); break; - case 7: sprintf(skin, "sas/grunt"); break; - case 8: sprintf(skin, "sas/mud"); break; - case 9: sprintf(skin, "sas/nithb"); break; - case 10: sprintf(skin, "sas/norse1"); break; - case 11: sprintf(skin, "sas/norse2"); break; - case 12: sprintf(skin, "sas/sas-urban"); break; - case 13: sprintf(skin, "sas/sas-woodland"); break; - case 14: sprintf(skin, "sas/sas"); break; - case 15: sprintf(skin, "sas/sas2"); break; - case 16: sprintf(skin, "sas/sasdc"); break; - case 17: sprintf(skin, "sas/sasdc2"); break; - case 18: sprintf(skin, "sas/sasjungle"); break; - case 19: sprintf(skin, "sas/saspolice"); break; - case 20: sprintf(skin, "sas/sasuc"); break; - case 21: sprintf(skin, "sas/sasUC2"); break; - case 22: sprintf(skin, "sas/sasurban"); break; - case 23: sprintf(skin, "sas/saswc"); break; - case 24: sprintf(skin, "sas/STARS"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - else if (type == 7) // Sydney - { - rnd = rand() % sydney; - switch (rnd) - { - case 0: sprintf(skin, "sydney/blonde"); break; - case 1: sprintf(skin, "sydney/sydney"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_FEMALE; - } - else if (type == 8) // Terror - { - rnd = rand() % terror; - switch (rnd) - { - case 0: sprintf(skin, "terror/adf2"); break; - case 1: sprintf(skin, "terror/black cop"); break; - case 2: sprintf(skin, "terror/blue cop"); break; - case 3: sprintf(skin, "terror/csw black"); break; - case 4: sprintf(skin, "terror/csw blue"); break; - case 5: sprintf(skin, "terror/csw crazy"); break; - case 6: sprintf(skin, "terror/csw red"); break; - case 7: sprintf(skin, "terror/ctf_b"); break; - case 8: sprintf(skin, "terror/ctf_r"); break; - case 9: sprintf(skin, "terror/desertterr"); break; - case 10: sprintf(skin, "terror/fbiterr"); break; - case 11: sprintf(skin, "terror/j_mafia"); break; - case 12: sprintf(skin, "terror/jungleterr"); break; - case 13: sprintf(skin, "terror/kgb"); break; - case 14: sprintf(skin, "terror/mafia"); break; - case 15: sprintf(skin, "terror/mafia2"); break; - case 16: sprintf(skin, "terror/red cop"); break; - case 17: sprintf(skin, "terror/redterr2"); break; - case 18: sprintf(skin, "terror/rmafia"); break; - case 19: sprintf(skin, "terror/skyterr"); break; - case 20: sprintf(skin, "terror/stormtrooper"); break; - case 21: sprintf(skin, "terror/swat"); break; - case 22: sprintf(skin, "terror/swatsnipe"); break; - case 23: sprintf(skin, "terror/terror"); break; - case 24: sprintf(skin, "terror/urbanterr"); break; - } - //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); - return GENDER_MALE; - } - - return GENDER_MALE; -} - -// Set the bot's userinfo (name, skin, hand, etc) -void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin) -{ - int gender = INVALID; - char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) - char skin[MAX_INFO_STRING]; - char userinfo[MAX_INFO_STRING]; - memset(userinfo, 0, sizeof(userinfo)); // Init userinfo - - gi.cvar_forceset(stat_logs->name, "0"); // Turning off stat collection since bots are enabled - - if (force_gender != INVALID) // Use previous gender - { - gender = force_gender; - } - if (force_skin != NULL) // Use previous skin - { - Info_SetValueForKey(userinfo, "skin", force_skin); - } - - // Teamplay and 3TEAMS - if (teamplay->value && team) // TEAM1, TEAM2, or TEAM3 - { - // Figure out the gender based on the team skins - char* femaleSkinDirs[] = { "actionrally", "female", "sydney" }; - char* maleSkinsDirs[] = { "actionmale", "aqmarine", "male", "messiah", "sas", "terror" }; - for (int i = 0; i < sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); ++i) - { - if (Q_strcasestr(teams[team].skin, femaleSkinDirs[i]) != NULL) - { - gender = GENDER_FEMALE; - break; - } - } - if (gender == INVALID) - { - for (int i = 0; i < sizeof(maleSkinsDirs) / sizeof(maleSkinsDirs[0]); ++i) - { - if (Q_strcasestr(teams[team].skin, maleSkinsDirs[i]) != NULL) - { - gender = GENDER_MALE; - break; - } - } - } - if (gender == INVALID) // Couldn't find skin gender (perhaps server is using custom skins) - { - if (rand() % 2 == 0) // So just randomize the skin gender - gender = GENDER_MALE; - else - gender = GENDER_FEMALE; - } - } - else // Deathmatch - { - if (force_skin == NULL) // Not forcing a skin, so pick one at random - { - if (gender == GENDER_MALE || gender == GENDER_FEMALE) - BOTLIB_RandomSkin(bot, skin, gender); // Set random skin based on predefined gender - else - gender = BOTLIB_RandomSkin(bot, skin, INVALID); // Set random skin and return its gender - Info_SetValueForKey(userinfo, "skin", skin); - } - } - - Q_strlcpy(name, "AqtionMan", 10); // Set a default name - if (force_name == NULL) // Not forcing a name, so pick one at random - { - BOTLIB_LoadRandomBotName(bot, name, gender); - Info_SetValueForKey(userinfo, "name", name); - } - else // Use previous name - { - Info_SetValueForKey(userinfo, "name", force_name); - } - - // Set the gender - if (gender == GENDER_MALE) - Info_SetValueForKey(userinfo, "gender", "male"); - else if (gender == GENDER_FEMALE) - Info_SetValueForKey(userinfo, "gender", "female"); - else - Info_SetValueForKey(userinfo, "gender", "none"); - - //Set userinfo: hand, spec - Info_SetValueForKey(userinfo, "hand", "2"); // bot is center handed for now! - Info_SetValueForKey(userinfo, "spectator", "0"); // NOT a spectator - - ClientConnect(bot, userinfo); -} - -/////////////////////////////////////////////////////////////////////// -// Called by PutClient in Server to actually release the bot into the game -// Keep from killin' each other when all spawned at once -/////////////////////////////////////////////////////////////////////// -void BOTLIB_HoldSpawn(edict_t* self) -{ - if (!KillBox(self)) - { // could't spawn in? - } - - gi.linkentity(self); - - self->think = BOTAI_Think; - self->nextthink = level.framenum + 1; - - // send effect - gi.WriteByte(svc_muzzleflash); - gi.WriteShort(self - g_edicts); - gi.WriteByte(MZ_LOGIN); - gi.multicast(self->s.origin, MULTICAST_PVS); - gi.bprintf(PRINT_MEDIUM, "%s entered the game\n", self->client->pers.netname); -} - -// Modified version of id's code -void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) -{ - bot->is_bot = true; - bot->bot.bot_type = BOT_TYPE_BOTLIB; - bot->think = BOTLIB_Think; - bot->nextthink = level.framenum + 1; - - PutClientInServer(bot); - JoinTeam(bot, team, true); -} - -// Spawn a bot -void ClientBeginDeathmatch(edict_t* ent); -edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin) //char* userinfo) -{ - edict_t* bot = BOTLIB_FindFreeEntity(); - if (!bot) - { - gi.bprintf(PRINT_MEDIUM, "Server is full! To allow more players set the console command 'maxclients' higher than %d\n", game.maxclients); - return NULL; - } - - bot->is_bot = true; - bot->yaw_speed = 1000; // aiming degrees per second - - if (team == TEAM1) - bot_connections.total_team1++; - else if (team == TEAM2) - bot_connections.total_team2++; - else if (team == TEAM3) - bot_connections.total_team3++; - - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); // includes ClientConnect - - ClientBeginDeathmatch(bot); - - BOTLIB_PutClientInServer(bot, true, team); - - //rekkie -- Fake Bot Client -- s - // Set the average ping this bot will see - int rng_ping_range = rand() % 5; - if (rng_ping_range == 0) - bot->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard - else if (rng_ping_range <= 3) - bot->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger - else - bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' - //rekkie -- Fake Bot Client -- e - - return bot; -} - - -// Remove a bot by name or all bots -void BOTLIB_RemoveBot(char* name) -{ - int i; - qboolean freed = false; - edict_t* bot; - - // =================================================== - // Kick by NAME, ALL bots, or by team num - // =================================================== - if (strlen(name)) - { - qboolean remove_all = (Q_stricmp(name, "all") == 0) ? true : false; - int find_team = (strlen(name) == 1) ? atoi(name) : 0; // Kick by team - - //if (name!=NULL) - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) - { - if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) - { - //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client - //rekkie -- Fake Bot Client -- e - - bot->health = 0; - player_die(bot, bot, bot, 100000, vec3_origin); - // don't even bother waiting for death frames - //bot->deadflag = DEAD_DEAD; - //bot->inuse = false; - freed = true; - ClientDisconnect(bot); - //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); - if (!remove_all) - break; - } - } - } - } - - // =================================================== - // Kick random bot - // =================================================== - int bot_to_kick = 0; - int num_bots = 0; - int bot_count = 0; - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) - { - if (bot->is_bot) - { - num_bots++; - } - } - } - if (num_bots == 0) - { - return; - } - else if (num_bots > 1) - { - bot_to_kick = (rand() % num_bots); - } - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) - { - if (bot->is_bot && bot_to_kick == bot_count) - { - //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client - //rekkie -- Fake Bot Client -- e - - bot->health = 0; - player_die(bot, bot, bot, 100000, vec3_origin); - // don't even bother waiting for death frames - //bot->deadflag = DEAD_DEAD; - //bot->inuse = false; - freed = true; - ClientDisconnect(bot); - //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); - break; - } - if (bot->is_bot) - bot_count++; - } - } - - if (!freed) - gi.bprintf(PRINT_MEDIUM, "No bot removed\n", name); -} - -// Remove a bot by team -// Conditions: Bot must be dead or joined a team during an ongoing round -void BOTLIB_RemoveTeamplayBot(int team) -{ - int i; - edict_t* bot; - - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) // Ent in use - { - if (bot->is_bot) // Is a bot - { - // Only kick when the bot isn't actively in a match - //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) - if (bot->client->resp.team == team) // && team_round_going == 0) - { - //if (random() < 0.20) // Randomly kick a bot - { - //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client - //rekkie -- Fake Bot Client -- e - - if (team == TEAM1) - bot_connections.total_team1--; - else if (team == TEAM2) - bot_connections.total_team2--; - else if (team == TEAM3) - bot_connections.total_team3--; - - if (bot->health) - player_die(bot, bot, bot, 100000, vec3_origin); - ClientDisconnect(bot); - break; - } - } - } - } - } -} - -// Change bot [from team] ==> [to team] -// Conditions: Bot must be in a team. Change occurs after round ends. -void BOTLIB_ChangeBotTeam(int from_team, int to_team) -{ - int i; - edict_t* bot; - - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) // Ent in use - { - if (bot->is_bot) // Is a bot - { - // Only kick when the bot isn't actively in a match - //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) - if (bot->client->resp.team == from_team && team_round_going == 0) - { - if (from_team == TEAM1) bot_connections.total_team1--; - else if (from_team == TEAM2) bot_connections.total_team2--; - else if (from_team == TEAM3) bot_connections.total_team3--; - - if (to_team == TEAM1) bot_connections.total_team1++; - else if (to_team == TEAM2) bot_connections.total_team2++; - else if (to_team == TEAM3) bot_connections.total_team3++; - - JoinTeam(bot, to_team, true); - break; - } - } - } - } -} - -//rekkie -- DEV_1 -- s - -// Returns how many bots and players are playing (returned via struct). -void BOTLIB_GetTotalPlayers(bot_connections_t* bc) -{ - edict_t* ent; - - bc->total_bots = 0; - bc->total_humans = 0; - bc->total_humans_playing = 0; - - bc->total_team1 = 0; - bc->total_team2 = 0; - bc->total_team3 = 0; - - bc->spec_bots = 0; - bc->team1_bots = 0; - bc->team2_bots = 0; - bc->team3_bots = 0; - - bc->spec_humans = 0; - bc->team1_humans = 0; - bc->team2_humans = 0; - bc->team3_humans = 0; - - for (int i = 0; i < game.maxclients; i++) - { - ent = &g_edicts[1 + i]; - if (ent && ent->inuse) - { - if (ent->is_bot) - { - bc->total_bots++; - - if (teamplay->value) - { - if (ent->client->resp.team == NOTEAM) - { - bc->spec_bots++; - } - else if (ent->client->resp.team == TEAM1) - { - bc->team1_bots++; - bc->total_team1++; - } - else if (ent->client->resp.team == TEAM2) - { - bc->team2_bots++; - bc->total_team2++; - } - else if (ent->client->resp.team == TEAM3) - { - bc->team3_bots++; - bc->total_team3++; - } - } - else if (deathmatch->value) - { - if (ent->solid == SOLID_NOT) - bc->spec_bots++; - else - bc->team1_bots++; - } - } - else - { - bc->total_humans++; - - if (teamplay->value) - { - if (ent->client->resp.team == NOTEAM) - bc->spec_humans++; - else if (ent->client->resp.team == TEAM1) - { - bc->team1_humans++; - bc->total_team1++; - } - else if (ent->client->resp.team == TEAM2) - { - bc->team2_humans++; - bc->total_team2++; - } - else if (ent->client->resp.team == TEAM3) - { - bc->team3_humans++; - bc->total_team3++; - } - } - else if (deathmatch->value) - { - if (ent->solid == SOLID_NOT) - bc->spec_humans++; - else - bc->team1_humans++; - } - } - } - } - bc->total_humans_playing = bc->total_humans - bc->spec_humans; -} - -// Bots auto join teams to keep them equal in teamplay. -// Empty servers have a bot join a team upon a player connecting. -int bot_teamcheckfrequency = 0; -int bot_teamchangefrequency = 0; -void BOTLIB_CheckBotRules(void) -{ - if (matchmode->value) // Bots never allowed in matchmode - return; - - if (ctf->value) - { - BOTLIB_Update_Flags_Status(); - } - - // check these rules every 1.0 seconds... (aka 10 frames) -- this will be faster if server hz is faster - if ((++bot_teamcheckfrequency % 10) != 0) - { - return; - } - else - { - bot_teamcheckfrequency = 0; - //Com_Printf("%s ltk_teamcheckfrequency\n", __func__); - } - - BOTLIB_GetTotalPlayers(&bot_connections); - //Com_Printf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots); - - if (bot_connections.tried_adding_prev_bots == false) - { - bot_connections.tried_adding_prev_bots = true; // Only called once per map - BOTLIB_AddBotsFromPreviousMap(100); // Try get bots from a previous map - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats - } - - if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; // Turn on team balance if bot_maxteam is used - - // ======================================================================================================== - // Manually add bots to a select team - // sv bots - if (teamplay->value && bot_connections.auto_balance_bots == false) - { - // Sanity check - if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; - if (bot_connections.desire_team2 < 0) bot_connections.desire_team2 = 0; - if (bot_connections.desire_team3 < 0) bot_connections.desire_team3 = 0; - - // Remove bots - if (bot_connections.desire_team1 < bot_connections.team1_bots) - { - while (bot_connections.team1_bots != bot_connections.desire_team1) - { - BOTLIB_RemoveTeamplayBot(TEAM1); - bot_connections.team1_bots--; - } - } - if (bot_connections.desire_team2 < bot_connections.team2_bots) - { - while (bot_connections.team2_bots != bot_connections.desire_team2) - { - BOTLIB_RemoveTeamplayBot(TEAM2); - bot_connections.team2_bots--; - } - } - if (bot_connections.desire_team3 < bot_connections.team3_bots) - { - while (bot_connections.team3_bots != bot_connections.desire_team3) - { - BOTLIB_RemoveTeamplayBot(TEAM3); - bot_connections.team3_bots--; - } - } - - // Add bots - if (bot_connections.desire_team1 > bot_connections.team1_bots) - { - while (bot_connections.team1_bots != bot_connections.desire_team1) - { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team1 = bot_connections.team1_bots; - break; - } - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats - } - } - if (bot_connections.desire_team2 > bot_connections.team2_bots) - { - while (bot_connections.team2_bots != bot_connections.desire_team2) - { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team2 = bot_connections.team2_bots; - break; - } - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats - } - } - if (bot_connections.desire_team3 > bot_connections.team3_bots) - { - while (bot_connections.team3_bots != bot_connections.desire_team3) - { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team3 = bot_connections.team3_bots; - break; - } - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats - } - } - - /* - // Sanity check - if (bot_connections.desire_team1 < -1) bot_connections.desire_team1 = 0; - if (bot_connections.desire_team2 < -1) bot_connections.desire_team2 = 0; - if (bot_connections.desire_team3 < -1) bot_connections.desire_team3 = 0; - - int count = (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); - if (count == -1) // Remove all bots from team - { - // Remove all bots from team - if (bot_connections.desire_team1 == -1) - { - while (bot_connections.team1_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM1); - bot_connections.team1_bots--; - } - } - else if (bot_connections.desire_team2 == -1) - { - while (bot_connections.team2_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM2); - bot_connections.team2_bots--; - } - } - else if (use_3teams->value && bot_connections.desire_team3 == -1) - { - while (bot_connections.team3_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM3); - bot_connections.team3_bots--; - } - } - } - if (count > 0) // Add bots to team - { - for (int i = 0; i < count; i++) - { - if (bot_connections.total_bots + bot_connections.total_humans + 1 < maxclients->value) // Max allowed - { - if (bot_connections.desire_team1) - { - if (bot_connections.team1_bots < bot_connections.desire_team1) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.team1_bots > bot_connections.desire_team1) - BOTLIB_RemoveTeamplayBot(TEAM1); - else - break; - } - else if (bot_connections.desire_team2) - { - if (bot_connections.team2_bots < bot_connections.desire_team2) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else if (bot_connections.team2_bots > bot_connections.desire_team2) - BOTLIB_RemoveTeamplayBot(TEAM2); - else - break; - } - else if (use_3teams->value && bot_connections.desire_team3) - { - if (bot_connections.team3_bots < bot_connections.desire_team3) - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - else if (bot_connections.team3_bots > bot_connections.desire_team3) - BOTLIB_RemoveTeamplayBot(TEAM3); - else - break; - } - } - } - bot_connections.desire_team1 = 0; - bot_connections.desire_team2 = 0; - bot_connections.desire_team3 = 0; - return; - } - */ - } - // ======================================================================================================== - - - // Auto balance bots - if (bot_connections.auto_balance_bots == false) - return; - - - //bot_connections.desire_bots += (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); - - // Percent chance to add/remove bots every 5 seconds... inc/dec = [min: 1, max: bot_maxteam] - if ((++bot_teamchangefrequency % 5) == 0) - { - bot_teamchangefrequency = 0; - - if (bot_maxteam->value) - { - //scale_up - float percent = (float)bot_connections.total_bots / bot_maxteam->value; - if (bot_connections.scale_dn == false && bot_connections.scale_up == false) - bot_connections.scale_up = true; - - float min_percent = 1 / bot_maxteam->value; - - // Reached top end of the scale, so work back down - if (bot_connections.scale_up && percent >= 1.0) - { - bot_connections.scale_up = false; - bot_connections.scale_dn = true; - } - // Reached bottom end of the scale, or randomly at 50%, so work back up - else if (bot_connections.scale_dn && (percent <= min_percent || random() < 0.05)) // Never drop below this percent - { - bot_connections.scale_up = true; - bot_connections.scale_dn = false; - } - - // Decrease bots - if (bot_connections.scale_dn && random() < 0.050) - bot_connections.desire_bots--; - - // Increase bots - if (bot_connections.scale_up && random() < 0.250) - bot_connections.desire_bots++; - - // Don't let bots be greater than bot_maxteam - if ((float)bot_connections.desire_bots > bot_maxteam->value) - bot_connections.desire_bots--; - - // Never go below 1 bot when bot_maxteam is active - if (bot_connections.desire_bots < 1) - bot_connections.desire_bots = 1; - } - } - - - // Always leave room for another player to join - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - bot_connections.desire_bots = (maxclients->value - 1); - - // Sanity check - safety limits - //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; - - //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); - int bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; - - // Shuffle bots around - //if (teamplay->value && bots_to_spawn == 0) - if (teamplay->value && bot_connections.auto_balance_bots) - { - if (use_3teams->value && (bot_connections.total_team1 != bot_connections.total_team2 || bot_connections.total_team1 != bot_connections.total_team3 || bot_connections.total_team2 != bot_connections.total_team3)) - { - if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1 && bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM1); - if (abs(bot_connections.total_team1 - bot_connections.total_team3) > 1 && bot_connections.total_team1 < bot_connections.total_team3) - BOTLIB_ChangeBotTeam(TEAM3, TEAM1); - - if (abs(bot_connections.total_team2 - bot_connections.total_team1) > 1 && bot_connections.total_team2 < bot_connections.total_team1) - BOTLIB_ChangeBotTeam(TEAM1, TEAM2); - if (abs(bot_connections.total_team2 - bot_connections.total_team3) > 1 && bot_connections.total_team2 < bot_connections.total_team3) - BOTLIB_ChangeBotTeam(TEAM3, TEAM2); - - if (abs(bot_connections.total_team3 - bot_connections.total_team1) > 1 && bot_connections.total_team3 < bot_connections.total_team1) - BOTLIB_ChangeBotTeam(TEAM1, TEAM3); - if (abs(bot_connections.total_team3 - bot_connections.total_team2) > 1 && bot_connections.total_team3 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM3); - } - else if (bot_connections.total_team1 != bot_connections.total_team2) - { - if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1) - { - if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM1); - else - BOTLIB_ChangeBotTeam(TEAM1, TEAM2); - } - } - - } - - // Remove ALL bots - if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) - { - BOTLIB_RemoveBot("ALL"); - bot_connections.total_team1 = 0; - bot_connections.total_team2 = 0; - bot_connections.total_team3 = 0; - } - - // Remove bots - //if (bot_connections.total_bots > bot_connections.desire_bots) - if (bots_to_spawn < 0) - { - bots_to_spawn = abs(bots_to_spawn); - for (int i = 0; i < bots_to_spawn; i++) - { - if (teamplay->value) // Remove a bot from team with the highest player count - { - if (use_3teams->value) - { - if (bot_connections.total_team1 >= bot_connections.total_team2 && bot_connections.total_team1 >= bot_connections.total_team3) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (bot_connections.total_team2 >= bot_connections.total_team1 && bot_connections.total_team2 >= bot_connections.total_team3) - BOTLIB_RemoveTeamplayBot(TEAM2); - else if (bot_connections.total_team3 >= bot_connections.total_team1 && bot_connections.total_team3 >= bot_connections.total_team2) - - BOTLIB_RemoveTeamplayBot(TEAM3); - else // If all teams are equal size, randomly remove bot to one of the teams - { - int choice = rand() % 3; - if (choice == 0) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (choice == 1) - BOTLIB_RemoveTeamplayBot(TEAM2); - else - BOTLIB_RemoveTeamplayBot(TEAM3); - } - } - else - { - if (bot_connections.total_team1 > bot_connections.total_team2) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_RemoveTeamplayBot(TEAM2); - else // If both teams are equal size, randomly remove bot to one of the teams - { - if (rand() % 2 == 0) - BOTLIB_RemoveTeamplayBot(TEAM1); - else - BOTLIB_RemoveTeamplayBot(TEAM2); - } - } - } - else // DM - BOTLIB_RemoveBot(""); - } - return; - } - //else if (bot_connections.total_bots < bots_to_spawn) - else if (bots_to_spawn > 0) - { - for (int i = 0; i < bots_to_spawn; i++) - { - if (teamplay->value) // Add a bot to a team with the lowest player count - { - if (use_3teams->value) // 3TEAMS - { - if (bot_connections.total_team1 <= bot_connections.total_team2 && bot_connections.total_team1 <= bot_connections.total_team3) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.total_team2 <= bot_connections.total_team1 && bot_connections.total_team2 <= bot_connections.total_team3) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else if (bot_connections.total_team3 <= bot_connections.total_team1 && bot_connections.total_team3 <= bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - else // If all teams are equal size, randomly add bot to one of the teams - { - int choice = rand() % 3; - if (choice == 0) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (choice == 1) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - } - } - else // TP - { - if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.total_team1 > bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else // If both teams are equal size, randomly add bot to one of the teams - { - if (rand() % 2 == 0) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - } - } - } - else // DM - BOTLIB_SpawnBot(0, INVALID, NULL, NULL); - } - } -} +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +int dc_total_male_names; // Total male names +int dc_total_female_names; // Total female names +bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy +bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy +bot_connections_t bot_connections; + +// Find a free entity for the bot to use +edict_t* BOTLIB_FindFreeEntity(void) +{ + edict_t* bot = NULL; + for (int i = game.maxclients; i > 0; i--) + { + bot = g_edicts + i + 1; + + if (!bot->inuse) + return bot; // Success + } + + return NULL; // Failure +} + +qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) +{ + FILE* f; + int comments_num = 0; // Keep track of how many comments the file has + int line_num = 0; // Keep track of lines + int curr_len; // Current length of the line + char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) + curr_line[0] = '\0'; // Current line + + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + char filename[MAX_QPATH]; // Filename to load from + int i; // Keep track where we are in the filename array +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + if (strlen(filename) <= 0) return false; + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + gi.dprintf("%s failed to open bot file: %s\n", __func__, filename); + return false; + } + + // Find how many lines the file has + int random_line; + while (fgets(curr_line, sizeof(curr_line), f)) + { + line_num++; + } + fclose(f); + random_line = rand() % line_num + 1; + //random_line = 13; + + //Com_Printf("%s %s random_line[%d]\n", __func__, filename, random_line); + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + line_num = -1; + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + line_num++; // Advance forward + + //if (feof(f)) // End of file + // break; // Could not get a prefix name + + if (curr_line[0] == '/' && curr_line[1] == '/') // Skip comment lines + continue; + + // If we're at or above the correct line, not a comment, not empty + //if (line_num >= random_line && curr_line[0] != '/' && curr_line[0]) + if (line_num >= random_line) + { + curr_len = strlen(curr_line); + if (curr_len < MAX_QPATH) // Make sure name is within acceptable length + { + //Com_Printf("%s %s curr_line[%d] = %s\n", __func__, filename, line_num, curr_line); + Q_strlcpy(buffer, curr_line, curr_len); + break; + } + } + } + fclose(f); + } + + if (strlen(buffer) > 0) + { + return true; + } + else + { + buffer[0] = '\0'; // Terminate empty string + return false; + } +} + +// Save bots from the previous map to file +#define BOTS_FILE_PREV_MAP_VERSION 1 +qboolean BOTLIB_SaveBotsFromPreviousMap(void) +{ + FILE* f; + char filename[128]; + const char* file = "prev_map_bots.txt"; + int i; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + + //if (bot_connections.total_bots <= 0) // Don't write unless we have bots + // return false; + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + //Com_Printf("%s Writing previous bots to file %s\n", __func__, filename); + + // Write to file + f = fopen(filename, "w"); // Open the file in write mode + if (f == NULL) + { + Com_Printf("Could not write to file %s\n", filename); + return false; + } + + fprintf(f, "// Bots from a previous map\n"); + fprintf(f, "%d\n", BOTS_FILE_PREV_MAP_VERSION); + fprintf(f, "%d\n", bot_connections.total_bots); + fprintf(f, "%d\n", (int)bot_maxteam->value); + if (use_3teams->value) + fprintf(f, "3teams\n"); + else if (teamplay->value) + fprintf(f, "teamplay\n"); + else + fprintf(f, "deathmatch\n"); + + for (i = 0; i < num_players; i++) + { + if (players[i]->is_bot == false) continue; // Skip humans + + // Gender + fprintf(f, "%d\n", players[i]->client->pers.gender); + + // Team + fprintf(f, "%d\n", players[i]->client->resp.team); + + // Name + fprintf(f, "%s\n", players[i]->client->pers.netname); + + // Skin + fprintf(f, "%s\n", Info_ValueForKey(players[i]->client->pers.userinfo, "skin")); + } + + fclose(f); // Close the file after use + + return true; +} + +// Add bots from the previous map from file +// Parameters: +// percent of bots to keep from previous map +qboolean BOTLIB_AddBotsFromPreviousMap(float percent) +{ + FILE* f; + qboolean print_dbg = false; // Print debug info + int line_num = 0; // Keep track of lines + + int prev_bots_skipped = 0; // How many bots we skipped adding from previous map + int prev_bot_count = 0; // How many bots from the previous map + int bots_added = 0; // How many bots we added from previous map + char gamemode[MAX_QPATH]; // Gamemode string buffer + int prev_gamemode = 0; // Previous gamemode + int prev_team = 0; // Previous Team + int prev_gender = 0; // Previous Gender + char prev_name[MAX_QPATH]; // Previous Name + char prev_skin[MAX_QPATH]; // Previous Skin + + int curr_len = 0; // Current length of the line + char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) + curr_line[0] = '\0'; // Current line + + const char* file = "prev_map_bots.txt"; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + char filename[MAX_QPATH]; // Filename to load from + int i; // Keep track where we are in the filename array +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, file); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/"); + strcat(filename, file); +#endif + + if (strlen(filename) <= 0) return false; + + // Open and check for success + if ((f = fopen(filename, "r")) == NULL) // Read text file + { + return false; // Ignore if file doesn't exist + } + + // Open and check for success + if ((f = fopen(filename, "r")) != NULL) // Read text file + { + // Ignore first line + fgets(curr_line, sizeof(curr_line), f); // First line is commented + if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); + + // Get file version + fgets(curr_line, sizeof(curr_line), f); + int version = atoi(curr_line); + if (print_dbg) Com_Printf("%s line[%d] version[%d]\n", __func__, ++line_num, version); + if (version != BOTS_FILE_PREV_MAP_VERSION) + { + return false; + } + + // Get previous bot count + fgets(curr_line, sizeof(curr_line), f); + prev_bot_count = atoi(curr_line); + if (print_dbg) Com_Printf("%s line[%d] bot_count[%d]\n", __func__, ++line_num, prev_bot_count); + if (prev_bot_count <= 0) + { + //gi.dprintf("%s failed to process bot count from file: %s\n", __func__, filename); + return false; + } + + // Get previous bot_maxteam num + fgets(curr_line, sizeof(curr_line), f); + //bot_maxteam->value = atoi(curr_line); + gi.cvar_set("bot_maxteam", va("%d", atoi(curr_line))); // Override if manually added + if (print_dbg) Com_Printf("%s line[%d] bot_maxteam[%f]\n", __func__, ++line_num, bot_maxteam->value); + if (bot_maxteam->value < 0 || bot_maxteam->value > MAX_CLIENTS) + { + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if manually added + gi.dprintf("%s failed to process bot_maxteam from file: %s\n", __func__, filename); + return false; + } + + // Get gamemode + fgets(curr_line, sizeof(curr_line), f); // Second line is gamemode + if (print_dbg) Com_Printf("%s line[%d] %s", __func__, ++line_num, curr_line); + curr_len = strlen(curr_line); + Q_strlcpy(gamemode, curr_line, curr_len); + if (Q_strcasestr(gamemode, "deathmatch") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is DEATHMATCH\n", __func__); + prev_gamemode = 1; // DM + } + else if (Q_strcasestr(gamemode, "teamplay") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is TEAMPLAY\n", __func__); + prev_gamemode = 2; // TP + } + else if (Q_strcasestr(gamemode, "3teams") != NULL) + { + if (print_dbg) Com_Printf("%s Gamemode is 3TEAMS\n", __func__); + prev_gamemode = 3; // 3TEAMS + } + + // Read each line + while (fgets(curr_line, sizeof(curr_line), f)) + { + // Get gender + prev_gender = atoi(curr_line); + if (prev_gender < GENDER_MALE || prev_gender > GENDER_NEUTRAL) + { + Com_Printf("%s failed to process bot gender from file: %s\n", __func__, filename); + return false; + } + + // Get team + fgets(curr_line, sizeof(curr_line), f); // Get next line + prev_team = atoi(curr_line); + if (prev_team < NOTEAM || prev_team > TEAM3) + { + Com_Printf("%s failed to process bot team from file: %s\n", __func__, filename); + return false; + } + + // Get name + fgets(curr_line, sizeof(curr_line), f); // Get next line + curr_len = strlen(curr_line); + if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length + continue; + Q_strlcpy(prev_name, curr_line, curr_len); + + // Get skin + fgets(curr_line, sizeof(curr_line), f); // Get next line + curr_len = strlen(curr_line); + if (curr_len >= MAX_QPATH) // Make sure name is within acceptable length + continue; + Q_strlcpy(prev_skin, curr_line, curr_len); + + if (print_dbg) Com_Printf("%s %s curr_line[%d]. GM[%s] Gender[%d] Team[%d] Name[%s] Skin[%s]\n", __func__, filename, ++line_num, gamemode, prev_gender, prev_team, prev_name, prev_skin); + + if (random() > (percent / 100)) // Percent chance to skip adding bot from previous map + { + prev_bots_skipped++; // Bots that were not added from previous map + continue; + } + + //if (bot_maxteam->value && bots_added >= (int)bot_maxteam->value) // Limit bots to bot_maxteam, if its set + // break; + if (prev_bot_count && bots_added >= prev_bot_count) + break; + + if (use_3teams->value) + { + // Bot was previously in a different game mode joining a 3teams game, randomize the team it joins + if (prev_gamemode == 1 || prev_gamemode == 2) // DM or TP + prev_team = (rand() % TEAM3) + 1; + + if (prev_team == TEAM1) bot_connections.desire_team1++; + else if (prev_team == TEAM2) bot_connections.desire_team2++; + else if (prev_team == TEAM3) bot_connections.desire_team3++; + + if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); + BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); + bots_added++; + } + else if (teamplay->value) // TP and CTF + { + // Bot was previously in a different game mode joining a teamplay game, randomize the team it joins + if (prev_gamemode == 1 || prev_gamemode == 3) // DM or 3TEAMS + prev_team = (rand() % TEAM2) + 1; + + if (prev_team == TEAM1) bot_connections.desire_team1++; + else if (prev_team == TEAM2) bot_connections.desire_team2++; + + if (print_dbg) Com_Printf("%s Gender[%d] Team[%d] Name[%s]\n", __func__, prev_gender, prev_team, prev_name); + BOTLIB_SpawnBot(prev_team, prev_gender, prev_name, NULL); + bots_added++; + } + else // DM + { + if (strlen(prev_skin)) + BOTLIB_SpawnBot(0, prev_gender, prev_name, prev_skin); + else + BOTLIB_SpawnBot(0, prev_gender, prev_name, NULL); + bots_added++; + } + } + fclose(f); + } + + + if (bots_added) + { + //if (bot_maxteam->value < 1) // If max team wasn't set, use it so bots can be shuffled + // bot_maxteam->value = prev_bot_count; + + bot_connections.desire_bots = bots_added; // How many bots + //bot_connections.auto_balance_bots = true; + + return true; + } + + return false; +} + +void BOTLIB_RandomizeTeamNames(edict_t* bot) +{ + char name[MAX_QPATH]; + int len = 0; + const int max_name_size = sizeof(teams[TEAM1].name); // Team names are limited to 20 characters (19 chars + terminator) + + qboolean same_name = false; + for (int i = 0; i < MAX_TEAMS; i++) // For each team + { + // Try for random team names that fit within 18 characters + for (int t = 0; t < 32; t++) // Max loops + { + BOTLIB_GetRandomBotFileLine("team_names.txt", name); // Get a random team name + + // Get name lengths + len = strlen(name); + + // Find if the name length will fit within 20 chars (name + space + terminator == 20) + if (len <= (max_name_size - 2)) // adjust max size to account for space + terminator + { + // Make sure the name is different + if (Q_strcasestr(name, teams[TEAM1].name) != NULL) // If same name as Team1 + continue; + if (Q_strcasestr(name, teams[TEAM2].name) != NULL) // If same name as Team2 + continue; + if (Q_strcasestr(name, teams[TEAM3].name) != NULL) // If same name as Team3 + continue; + + // Merge the names together + Q_strncpyz(teams[i].name, va("%s", name), len + 2); // name + space + terminator + Com_Printf("%s Setting Team Name: %s\n", __func__, teams[i].name); + break; + } + } + } +} + + +void BOTLIB_RandomizeTeamSkins(edict_t* bot) +{ + //Com_Printf("%s before T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + + // Randomize skins + BOTLIB_RandomSkin(bot, teams[TEAM1].skin, INVALID); + BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); + + // Check if any of the randomized skins are the same, if so re-roll them until they're different + qboolean same_skin = false; + for (int i = 0; i < 64; i++) + { + same_skin = false; + if (Q_strcasestr(teams[TEAM2].skin, teams[TEAM1].skin) != NULL) // If Team2 is the same skin as Team1 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM2].skin, INVALID); // Re-randomize skin + } + + if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM1].skin) != NULL) // If Team3 is the same skin as Team1 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin + } + + if (Q_strcasestr(teams[TEAM3].skin, teams[TEAM2].skin) != NULL) // If Team3 is the same skin as Team2 + { + same_skin = true; + //Com_Printf("%s same skin detected T1[%s] T2[%s] T3[%s] +++++++++\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + BOTLIB_RandomSkin(bot, teams[TEAM3].skin, INVALID); // Re-randomize skin + } + + if (same_skin == false) // No same skins detected, so exit loop early + break; + } + + //Com_Printf("%s after T1[%s] T2[%s] T3[%s]\n", __func__, teams[TEAM1].skin, teams[TEAM2].skin, teams[TEAM3].skin); + + // Update skin associated indexes + Q_snprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); + Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); + Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); + + // Change team picture using the updated skin indexes + level.pic_teamskin[TEAM1] = gi.imageindex(teams[TEAM1].skin_index); + level.pic_teamskin[TEAM2] = gi.imageindex(teams[TEAM2].skin_index); + level.pic_teamskin[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); +} + +/* +void DC_CacheRandomBotNames(void) +{ + int i, n; + + // Cache male names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_male[n]); + n++; + } + // Cache female names + n = 0; + for (i = 0; i < MAX_BOT_NAMES; i++) + { + DC_LoadRandomBotName(GENDER_MALE, &bot_female[n]); + n++; + } +} +void DC_GetRandomBotName(byte gender, char* bot_name) +{ + if (gender == GENDER_MALE) + { + //Q_strlcpy(bot_name, bot_male[n], name_length); + } + else if (gender == GENDER_FEMALE) + { + + } + else + { + if (random() < 0.5) + { + + } + else + { + + } + } +} +*/ + +//====================================== +// BOTLIB_GetRandomClanSymbol() +// Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc +// Returns char +//====================================== +int BOTLIB_GetRandomClanSymbol() +{ + int sym = rand() % 25; + switch (sym) + { + case 0: return '!'; + case 1: return '#'; + case 2: return '$'; + case 3: return '%'; + case 4: return '&'; + case 5: return '('; + case 6: return ')'; + case 7: return '*'; + case 8: return '+'; + case 9: return ','; + case 10: return '-'; + case 11: return '.'; + //case 12: return '/'; + case 12: return ':'; + case 13: return '<'; + case 14: return '='; + case 15: return '>'; + case 16: return '?'; + case 17: return '@'; + case 18: return '['; + case 19: return ']'; + //case 21: return '\\'; + case 20: return '^'; + case 21: return '_'; + case 22: return '{'; + case 23: return '|'; + case 24: return '}'; + default: return ' '; + } +} + +//====================================== +// BOTLIB_GetOpposingClanSymbol() +// If a symbol has an opposite: ie --> [ ], ( ), { }, < >, etc +// Returns opposite side, else returns the input symbol +//====================================== +int BOTLIB_GetOpposingClanSymbol(char symbol) +{ + if (symbol == '[') + return ']'; + if (symbol == ']') + return '['; + + if (symbol == '(') + return ')'; + if (symbol == ')') + return '('; + + if (symbol == '{') + return '}'; + if (symbol == '}') + return '{'; + + if (symbol == '<') + return '>'; + if (symbol == '>') + return '<'; + + if (symbol == '\\') + return '/'; + if (symbol == '/') + return '\\'; + + return symbol; +} + + +//====================================== +// BOTLIB_GetRandomClanLetter() +// Gets a random clan letter +// Returns char +//====================================== +int BOTLIB_GetRandomClanLetter() +{ + // Gets a random ASCII letter between 65 and 90, or 97 and 122 + int letter = rand() % 52; + switch (letter) + { + case 0: return 'A'; + case 1: return 'B'; + case 2: return 'C'; + case 3: return 'D'; + case 4: return 'E'; + case 5: return 'F'; + case 6: return 'G'; + case 7: return 'H'; + case 8: return 'I'; + case 9: return 'J'; + case 10: return 'K'; + case 11: return 'L'; + case 12: return 'M'; + case 13: return 'N'; + case 14: return 'O'; + case 15: return 'P'; + case 16: return 'Q'; + case 17: return 'R'; + case 18: return 'S'; + case 19: return 'T'; + case 20: return 'U'; + case 21: return 'V'; + case 22: return 'W'; + case 23: return 'X'; + case 24: return 'Y'; + case 25: return 'Z'; + case 26: return 'a'; + case 27: return 'b'; + case 28: return 'c'; + case 29: return 'd'; + case 30: return 'e'; + case 31: return 'f'; + case 32: return 'g'; + case 33: return 'h'; + case 34: return 'i'; + case 35: return 'j'; + case 36: return 'k'; + case 37: return 'l'; + case 38: return 'm'; + case 39: return 'n'; + case 40: return 'o'; + case 41: return 'p'; + case 42: return 'q'; + case 43: return 'r'; + case 44: return 's'; + case 45: return 't'; + case 46: return 'u'; + case 47: return 'v'; + case 48: return 'w'; + case 49: return 'x'; + case 50: return 'y'; + case 51: return 'z'; + default: return ' '; + } +} + + + +void BOTLIB_GetRandomName(char* name, const int gender) +{ + if (gender == GENDER_MALE) + { + if (random() < 0.8) + BOTLIB_GetRandomBotFileLine("males.txt", name); + else + BOTLIB_GetRandomBotFileLine("other.txt", name); + } + else // GENDER_FEMALE + BOTLIB_GetRandomBotFileLine("females.txt", name); + + //if (strlen(name) <= 0) // Something went wrong. No name was set. + // Q_strlcpy(name, "AqtionMan", 9); // Set a default +} + +void BOTLIB_GetNamePrefix(char* name, const int type) +{ + if (type) + BOTLIB_GetRandomBotFileLine("clans.txt", name); // Clan tag + else + BOTLIB_GetRandomBotFileLine("prefix.txt", name); // Prefix +} + +void BOTLIB_GetNamePostfix(char* name, const int type) +{ + BOTLIB_GetRandomBotFileLine("postfix.txt", name); // Postfix +} + +void BOTLIB_GetRandomPrefix(char* name) +{ + byte tag = 0; + char outer_symbol = 0; + char inner_symbol = 0; + + outer_symbol = BOTLIB_GetRandomClanSymbol(); + if (random() < 0.5) // 50% chance to add an inner symbol + inner_symbol = BOTLIB_GetRandomClanSymbol(); + + // Add the prefix tag + name[tag] = outer_symbol; + if (inner_symbol) + name[++tag] = inner_symbol; + + // Add the clan acronym + name[++tag] = BOTLIB_GetRandomClanLetter(); + name[++tag] = BOTLIB_GetRandomClanLetter(); + if (random() < 0.25) name[++tag] = BOTLIB_GetRandomClanLetter(); // percent chance to add a third clan letter + + // Add the suffix tag + if (inner_symbol) + name[++tag] = BOTLIB_GetOpposingClanSymbol(inner_symbol); // Add the opposing symbol, if any + name[++tag] = BOTLIB_GetOpposingClanSymbol(outer_symbol); + + // print the bot name + name[++tag] = '\0'; + //Com_Printf("CLAN TAG %s\n", name); +} + + + +//====================================== +// DC_LoadRandomBotName() +// Gets a random bot name from file; bot_name is limited to a maximum of 16 chars, and the size of the list can be up to random()'s maximum of 32,767 +// Genders can be: GENDER_MALE, GENDER_FEMALE, GENDER_NEUTRAL +// Returns bot_name +//====================================== +void BOTLIB_LoadRandomBotName(edict_t* bot, char* bot_name, int gender) +{ + //for (int n = 0; n < 100; n++) + { + int rnglen = 0; // Length of rng name + int namelen = 0; // Length of name + int clanlen = 0; // Length of clan name + int prefixlen = 0; // Length of Prefix name + int postfixlen = 0; // Length of Postfix name + char name[MAX_QPATH]; // Name + char clan_name[MAX_QPATH]; // Clan name + char prefix_name[MAX_QPATH]; // Prefix name + char postfix_name[MAX_QPATH]; // Postfix name + char rng_clan_name[MAX_QPATH]; // Random Clan name + qboolean name_with_rng = false; // If name + rng clan will fit within 16 characters + qboolean name_with_clan = false; // If name + clan will fit within 16 characters + qboolean name_with_prefix = false; // If name + prefix will fit within 16 characters + qboolean name_with_postfix = false; // If name + postfix will fit within 16 characters + + // Get random names from file + //Q_strlcpy(name, "AqtionMan", 10); // Set a default name + BOTLIB_GetRandomName(name, gender); // Get name based on gender + BOTLIB_GetNamePrefix(clan_name, 0); + BOTLIB_GetNamePrefix(prefix_name, 1); + BOTLIB_GetNamePostfix(postfix_name, 1); + BOTLIB_GetRandomPrefix(rng_clan_name); + + // Decide if we're going to only take only part of a name (i.e. Arnold Schwarzenegger - we can take either Arnold or Schwarzenegger) + if (rand() % 2 == 0) + { + qboolean two_part_name = false; + char name_cpy[MAX_QPATH]; + char name_pt1[MAX_QPATH]; + char name_pt2[MAX_QPATH]; + Q_strlcpy(name_cpy, name, sizeof(name)); // Make a copy of the original string because strtok() will edit it + char* split_name = strtok(name_cpy, " "); + if (split_name != NULL) + { + Q_strlcpy(name_pt1, split_name, sizeof(split_name)); + split_name = strtok(NULL, " "); + } + if (split_name != NULL) + { + two_part_name = true; + Q_strlcpy(name_pt2, split_name, sizeof(split_name)); + } + if (two_part_name) + { + //Com_Printf("%s x1 name[%s]\n", __func__, name_pt1, strlen(name_pt1)); + //Com_Printf("%s x2 name[%s]\n", __func__, name_pt2, strlen(name_pt2)); + + if (rand() % 2 == 0) + Q_strlcpy(name, name_pt1, sizeof(name_pt1)); + else + Q_strlcpy(name, name_pt2, sizeof(name_pt2)); + } + + //Com_Printf("%s full name[%s]\n", __func__, name, strlen(name)); + } + + //Com_Printf("%s clan[%s][%d] prefix[%s][%d] postfix[%s][%d] rng_clan_name[%s][%d] name[%s][%d]\n", __func__, clan_name, strlen(clan_name), prefix_name, strlen(prefix_name), postfix_name, strlen(postfix_name), rng_clan_name, strlen(rng_clan_name), name, strlen(name)); + + // Get name lengths + namelen = strlen(name); + rnglen = strlen(rng_clan_name); + clanlen = strlen(clan_name); + prefixlen = strlen(prefix_name); + postfixlen = strlen(postfix_name); + + // Find what fits with the bot's name (14 chars for both names + space in between == 15) + if ((namelen + rnglen) < 15) + name_with_rng = true; + if ((namelen + clanlen) < 15) + name_with_clan = true; + if ((namelen + prefixlen) < 15) + name_with_prefix = true; + if ((namelen + postfixlen) < 15) + name_with_postfix = true; + + Q_strncpyz(bot_name, va("%s", name), namelen + 2); // Set a random name + + // Merge the names together + int name_type = rand() % 100; + if (name_type < 15 && name_with_rng) + Q_strncpyz(bot_name, va("%s %s", rng_clan_name, name), namelen + rnglen + 2); // RNG clan + name + else if (name_type < 20 && name_with_clan) + Q_strncpyz(bot_name, va("%s %s", clan_name, name), namelen + clanlen + 2); // clan + name + else if (name_type < 25 && name_with_prefix) + Q_strncpyz(bot_name, va("%s %s", prefix_name, name), namelen + prefixlen + 2); // prefix + name + else if (name_type < 30 && name_with_postfix) + Q_strncpyz(bot_name, va("%s %s", name, postfix_name), namelen + postfixlen + 2); // name + postfix + + //Com_Printf("%s name[%s][%d]\n", __func__, bot_name, strlen(bot_name)); + } +} + +// Sets a random skin and returns the skin's gender +// Parameters: gender - If a valid gender is passed, and not INVALID (-1) then pick a skin based on gender +int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender) +{ + int type = 0, rnd; + + strcpy(skin, "male/grunt"); // Default skin + + // Size of each collection of skins + const int actionmale = 30; + const int actionrally = 6; + const int aqmarine = 8; + const int female = 22; + const int male = 88; + const int messiah = 26; + const int sas = 25; + const int sydney = 2; + const int terror = 25; + + // When NOT forcing a gender using + if (force_gender == INVALID) + { + int rng_type = rand() % 2; // Randomize which randomizer we use. Either by [1] Collection Size, or [2] Random Type. + + // -- [1] Collection Size -- + // Since each collection varies in size, our random choice takes into account the size of the collection of skins. + // Collections with the most skins have a greater chance to be chosen. + if (rng_type == 0) + { + int sum = actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney + terror; + int rnd = (rand() % sum) + 1; // Random number from total + + if (rnd <= actionmale) + type = 0; // actionmale + else if (rnd <= (actionmale + actionrally)) + type = 1; // actionrally + else if (rnd <= (actionmale + actionrally + aqmarine)) + type = 2; // aqmarine + else if (rnd <= (actionmale + actionrally + aqmarine + female)) + type = 3; // female + else if (rnd <= (actionmale + actionrally + aqmarine + female + male)) + type = 4; // male + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah)) + type = 5; // messiah + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas)) + type = 6; // sas + else if (rnd <= (actionmale + actionrally + aqmarine + female + male + messiah + sas + sydney)) + type = 7; // sydney + else + type = 8; // terror + } + // -- [2] Random Type -- + else // Alternatively we could just randomly pick from one of the collections regardless of how many skins it has + { + type = rand() % 9; // Actionmale, actionrally, aqmarine, female, male, messiah, sas, sydney, terror + } + } + else // Forcing a gender + { + // (type == 0) // Actionmale -- male + // (type == 1) // Actionrally -- female + // (type == 2) // Aqmarine -- male + // (type == 3) // Female -- female + // (type == 4) // Male -- male + // (type == 5) // Messiah -- male + // (type == 6) // Sas -- male + // (type == 7) // Sydney -- female + // (type == 8) // Terror -- male + + if (force_gender == GENDER_MALE) + { + int rnd_gen_skin = rand() % 6; // Pick one of the male skins + + if (rnd_gen_skin == 0) type = 0; // Actionmale + else if (rnd_gen_skin == 1) type = 2; // Aqmarine + else if (rnd_gen_skin == 2) type = 4; // Male + else if (rnd_gen_skin == 3) type = 5; // Messiah + else if (rnd_gen_skin == 4) type = 6; // Sas + else type = 8; // Terror + } + else // GENDER_FEMALE + { + int rnd_gen_skin = rand() % 2; // Pick one of the female skins + + if (rnd_gen_skin == 0) type = 1; // Actionrally + else type = 3; // Female + // Skipping Sydney because skin has no VWEP + } + } + + if (type == 0) // Actionmale + { + rnd = rand() % actionmale; + switch (rnd) + { + case 0: sprintf(skin, "actionmale/agent"); break; + case 1: sprintf(skin, "actionmale/aqgthugboss"); break; + case 2: sprintf(skin, "actionmale/axef"); break; + case 3: sprintf(skin, "actionmale/badmutha"); break; + case 4: sprintf(skin, "actionmale/BlackStars"); break; + case 5: sprintf(skin, "actionmale/blood"); break; + case 6: sprintf(skin, "actionmale/blucedojo"); break; + case 7: sprintf(skin, "actionmale/bluceree"); break; + case 8: sprintf(skin, "actionmale/Bruce_Wayne"); break; + case 9: sprintf(skin, "actionmale/castor"); break; + case 10: sprintf(skin, "actionmale/chellobeta"); break; + case 11: sprintf(skin, "actionmale/chucky"); break; + case 12: sprintf(skin, "actionmale/crip"); break; + case 13: sprintf(skin, "actionmale/ctf_b"); break; + case 14: sprintf(skin, "actionmale/ctf_r"); break; + case 15: sprintf(skin, "actionmale/dafist"); break; + case 16: sprintf(skin, "actionmale/dubeta"); break; + case 17: sprintf(skin, "actionmale/invince"); break; + case 18: sprintf(skin, "actionmale/jewels"); break; + case 19: sprintf(skin, "actionmale/jungseal"); break; + case 20: sprintf(skin, "actionmale/killer"); break; + case 21: sprintf(skin, "actionmale/leonreno"); break; + case 22: sprintf(skin, "actionmale/lfc"); break; + case 23: sprintf(skin, "actionmale/morpheus"); break; + case 24: sprintf(skin, "actionmale/RCMP"); break; + case 25: sprintf(skin, "actionmale/scarf"); break; + case 26: sprintf(skin, "actionmale/scarfchain"); break; + case 27: sprintf(skin, "actionmale/seagull"); break; + case 28: sprintf(skin, "actionmale/shinobi"); break; + case 29: sprintf(skin, "actionmale/swat"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 1) // Actionrally + { + rnd = rand() % actionrally; + switch (rnd) + { + case 0: sprintf(skin, "actionrally/ctf_b"); break; + case 1: sprintf(skin, "actionrally/ctf_r"); break; + case 2: sprintf(skin, "actionrally/modsquad"); break; + case 3: sprintf(skin, "actionrally/natasha"); break; + case 4: sprintf(skin, "actionrally/pulp"); break; + case 5: sprintf(skin, "actionrally/trinity"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 2) // Aqmarine + { + rnd = rand() % aqmarine; + switch (rnd) + { + case 0: sprintf(skin, "aqmarine/aquamarine"); break; + case 1: sprintf(skin, "aqmarine/centurion"); break; + case 2: sprintf(skin, "aqmarine/desert"); break; + case 3: sprintf(skin, "aqmarine/marine1"); break; + case 4: sprintf(skin, "aqmarine/marine2"); break; + case 5: sprintf(skin, "aqmarine/urban"); break; + case 6: sprintf(skin, "aqmarine/usmc"); break; + case 7: sprintf(skin, "aqmarine/woods"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 3) // Female + { + rnd = rand() % female; + switch (rnd) + { + case 0: sprintf(skin, "female/ctf_b"); break; + case 1: sprintf(skin, "female/ctf_r"); break; + case 2: sprintf(skin, "female/kw_aqua"); break; + case 3: sprintf(skin, "female/kw_black"); break; + case 4: sprintf(skin, "female/kw_blue"); break; + case 5: sprintf(skin, "female/kw_green"); break; + case 6: sprintf(skin, "female/kw_pink"); break; + case 7: sprintf(skin, "female/kw_red"); break; + case 8: sprintf(skin, "female/kw_white"); break; + case 9: sprintf(skin, "female/kw_yellow"); break; + case 10: sprintf(skin, "female/leeloop"); break; + case 11: sprintf(skin, "female/sarah_ohconnor"); break; + case 12: sprintf(skin, "female/tankgirl"); break; + + // baseq2 skins (No longer supplied with Aqtion) + case 13: sprintf(skin, "female/athena"); break; + case 14: sprintf(skin, "female/brianna"); break; + case 15: sprintf(skin, "female/cobalt"); break; + case 16: sprintf(skin, "female/doomgal"); break; + case 17: sprintf(skin, "female/ensign"); break; + case 18: sprintf(skin, "female/jezebel"); break; + case 19: sprintf(skin, "female/jungle"); break; + case 20: sprintf(skin, "female/venus"); break; + case 21: sprintf(skin, "female/voodoo"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 4) // Male + { + rnd = rand() % male; + switch (rnd) + { + case 0: sprintf(skin, "male/{oz}"); break; + case 1: sprintf(skin, "male/adidas"); break; + case 2: sprintf(skin, "male/aqgthug"); break; + case 3: sprintf(skin, "male/aqgvillain"); break; + case 4: sprintf(skin, "male/austin2"); break; + case 5: sprintf(skin, "male/babarracuda"); break; + case 6: sprintf(skin, "male/beastdl"); break; + case 7: sprintf(skin, "male/bluebeard"); break; + case 8: sprintf(skin, "male/blues"); break; + case 9: sprintf(skin, "male/boba"); break; + case 10: sprintf(skin, "male/borya"); break; + case 11: sprintf(skin, "male/bravo"); break; + case 12: sprintf(skin, "male/brucelee"); break; + case 13: sprintf(skin, "male/bruces"); break; + case 14: sprintf(skin, "male/chow"); break; + case 15: sprintf(skin, "male/chowda"); break; + case 16: sprintf(skin, "male/Clan_UNA"); break; + case 17: sprintf(skin, "male/conan"); break; + case 18: sprintf(skin, "male/coolio"); break; + case 19: sprintf(skin, "male/cop"); break; + case 20: sprintf(skin, "male/copper"); break; + case 21: sprintf(skin, "male/ctf_b"); break; + case 22: sprintf(skin, "male/ctf_g"); break; + case 23: sprintf(skin, "male/ctf_r"); break; + case 24: sprintf(skin, "male/ctf_y"); break; + case 25: sprintf(skin, "male/cyrus"); break; + case 26: sprintf(skin, "male/dashaft"); break; + case 27: sprintf(skin, "male/deadpl"); break; + case 28: sprintf(skin, "male/elway"); break; + case 29: sprintf(skin, "male/GreenBeret"); break; + case 30: sprintf(skin, "male/grunt"); break; + case 31: sprintf(skin, "male/habs"); break; + case 32: sprintf(skin, "male/hall"); break; + case 33: sprintf(skin, "male/hellspawn"); break; + case 34: sprintf(skin, "male/hollywood"); break; + case 35: sprintf(skin, "male/homer"); break; + case 36: sprintf(skin, "male/hulk2"); break; + case 37: sprintf(skin, "male/image"); break; + case 38: sprintf(skin, "male/indy"); break; + case 39: sprintf(skin, "male/invince"); break; + case 40: sprintf(skin, "male/jax-jaguar"); break; + case 41: sprintf(skin, "male/jdredd"); break; + case 42: sprintf(skin, "male/jewels"); break; + case 43: sprintf(skin, "male/jules"); break; + case 44: sprintf(skin, "male/kgb"); break; + case 45: sprintf(skin, "male/kw_aqua"); break; + case 46: sprintf(skin, "male/kw_black"); break; + case 47: sprintf(skin, "male/kw_blue"); break; + case 48: sprintf(skin, "male/kw_green"); break; + case 49: sprintf(skin, "male/kw_orange"); break; + case 50: sprintf(skin, "male/kw_pink"); break; + case 51: sprintf(skin, "male/kw_red"); break; + case 52: sprintf(skin, "male/kw_white"); break; + case 53: sprintf(skin, "male/kw_yellow"); break; + case 54: sprintf(skin, "male/leaf"); break; + case 55: sprintf(skin, "male/leservoircat"); break; + case 56: sprintf(skin, "male/mariom"); break; + case 57: sprintf(skin, "male/marsell"); break; + case 58: sprintf(skin, "male/mason"); break; + case 59: sprintf(skin, "male/MikeLowry"); break; + case 60: sprintf(skin, "male/mkrain"); break; + case 61: sprintf(skin, "male/mkrept"); break; + case 62: sprintf(skin, "male/mkscorp"); break; + case 63: sprintf(skin, "male/mksub"); break; + case 64: sprintf(skin, "male/Mohoney"); break; + case 65: sprintf(skin, "male/mr_t"); break; + case 66: sprintf(skin, "male/nut"); break; + case 67: sprintf(skin, "male/nwom"); break; + case 68: sprintf(skin, "male/optimus"); break; + case 69: sprintf(skin, "male/oz"); break; + case 70: sprintf(skin, "male/pimp"); break; + case 71: sprintf(skin, "male/police"); break; + case 72: sprintf(skin, "male/resdog"); break; + case 73: sprintf(skin, "male/rigs"); break; + case 74: sprintf(skin, "male/robber"); break; + case 75: sprintf(skin, "male/roger"); break; + case 76: sprintf(skin, "male/rredneck"); break; + case 77: sprintf(skin, "male/sabotage"); break; + case 78: sprintf(skin, "male/santa"); break; + case 79: sprintf(skin, "male/sdes"); break; + case 80: sprintf(skin, "male/shaft"); break; + case 81: sprintf(skin, "male/siris"); break; + case 82: sprintf(skin, "male/snowcamo"); break; + case 83: sprintf(skin, "male/Superman"); break; + case 84: sprintf(skin, "male/t-800m"); break; + case 85: sprintf(skin, "male/trmntr"); break; + case 86: sprintf(skin, "male/walter"); break; + case 87: sprintf(skin, "male/wcwsting3"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 5) // Messiah + { + rnd = rand() % messiah; + switch (rnd) + { + case 0: sprintf(skin, "messiah/axe"); break; + case 1: sprintf(skin, "messiah/badger"); break; + case 2: sprintf(skin, "messiah/blackman"); break; + case 3: sprintf(skin, "messiah/blade"); break; + case 4: sprintf(skin, "messiah/chow"); break; + case 5: sprintf(skin, "messiah/chowblue"); break; + case 6: sprintf(skin, "messiah/chowred"); break; + case 7: sprintf(skin, "messiah/cptahab"); break; + case 8: sprintf(skin, "messiah/crawler"); break; + case 9: sprintf(skin, "messiah/ctf_b"); break; + case 10: sprintf(skin, "messiah/ctf_r"); break; + case 11: sprintf(skin, "messiah/darkbeing"); break; + case 12: sprintf(skin, "messiah/evil_axe"); break; + case 13: sprintf(skin, "messiah/keaneo"); break; + case 14: sprintf(skin, "messiah/kindig"); break; + case 15: sprintf(skin, "messiah/leon"); break; + case 16: sprintf(skin, "messiah/mxr_neo"); break; + case 17: sprintf(skin, "messiah/neo"); break; + case 18: sprintf(skin, "messiah/outlaw"); break; + case 19: sprintf(skin, "messiah/robber"); break; + case 20: sprintf(skin, "messiah/scuba"); break; + case 21: sprintf(skin, "messiah/slug"); break; + case 22: sprintf(skin, "messiah/spectre"); break; + case 23: sprintf(skin, "messiah/suit"); break; + case 24: sprintf(skin, "messiah/TheCrow"); break; + case 25: sprintf(skin, "messiah/thedon"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 6) // Sas + { + rnd = rand() % sas; + switch (rnd) + { + case 0: sprintf(skin, "sas/aqmsas"); break; + case 1: sprintf(skin, "sas/atf"); break; + case 2: sprintf(skin, "sas/ctf_b"); break; + case 3: sprintf(skin, "sas/ctf_r"); break; + case 4: sprintf(skin, "sas/deathreat"); break; + case 5: sprintf(skin, "sas/fbi"); break; + case 6: sprintf(skin, "sas/grassland"); break; + case 7: sprintf(skin, "sas/grunt"); break; + case 8: sprintf(skin, "sas/mud"); break; + case 9: sprintf(skin, "sas/nithb"); break; + case 10: sprintf(skin, "sas/norse1"); break; + case 11: sprintf(skin, "sas/norse2"); break; + case 12: sprintf(skin, "sas/sas-urban"); break; + case 13: sprintf(skin, "sas/sas-woodland"); break; + case 14: sprintf(skin, "sas/sas"); break; + case 15: sprintf(skin, "sas/sas2"); break; + case 16: sprintf(skin, "sas/sasdc"); break; + case 17: sprintf(skin, "sas/sasdc2"); break; + case 18: sprintf(skin, "sas/sasjungle"); break; + case 19: sprintf(skin, "sas/saspolice"); break; + case 20: sprintf(skin, "sas/sasuc"); break; + case 21: sprintf(skin, "sas/sasUC2"); break; + case 22: sprintf(skin, "sas/sasurban"); break; + case 23: sprintf(skin, "sas/saswc"); break; + case 24: sprintf(skin, "sas/STARS"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + else if (type == 7) // Sydney + { + rnd = rand() % sydney; + switch (rnd) + { + case 0: sprintf(skin, "sydney/blonde"); break; + case 1: sprintf(skin, "sydney/sydney"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_FEMALE; + } + else if (type == 8) // Terror + { + rnd = rand() % terror; + switch (rnd) + { + case 0: sprintf(skin, "terror/adf2"); break; + case 1: sprintf(skin, "terror/black cop"); break; + case 2: sprintf(skin, "terror/blue cop"); break; + case 3: sprintf(skin, "terror/csw black"); break; + case 4: sprintf(skin, "terror/csw blue"); break; + case 5: sprintf(skin, "terror/csw crazy"); break; + case 6: sprintf(skin, "terror/csw red"); break; + case 7: sprintf(skin, "terror/ctf_b"); break; + case 8: sprintf(skin, "terror/ctf_r"); break; + case 9: sprintf(skin, "terror/desertterr"); break; + case 10: sprintf(skin, "terror/fbiterr"); break; + case 11: sprintf(skin, "terror/j_mafia"); break; + case 12: sprintf(skin, "terror/jungleterr"); break; + case 13: sprintf(skin, "terror/kgb"); break; + case 14: sprintf(skin, "terror/mafia"); break; + case 15: sprintf(skin, "terror/mafia2"); break; + case 16: sprintf(skin, "terror/red cop"); break; + case 17: sprintf(skin, "terror/redterr2"); break; + case 18: sprintf(skin, "terror/rmafia"); break; + case 19: sprintf(skin, "terror/skyterr"); break; + case 20: sprintf(skin, "terror/stormtrooper"); break; + case 21: sprintf(skin, "terror/swat"); break; + case 22: sprintf(skin, "terror/swatsnipe"); break; + case 23: sprintf(skin, "terror/terror"); break; + case 24: sprintf(skin, "terror/urbanterr"); break; + } + //Com_Printf("%s %s type[%d] rnd[%d]\n", __func__, bot->client->pers.netname, type, rnd); + return GENDER_MALE; + } + + return GENDER_MALE; +} + +// Set the bot's userinfo (name, skin, hand, etc) +void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin) +{ + int gender = INVALID; + char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) + char skin[MAX_INFO_STRING]; + char userinfo[MAX_INFO_STRING]; + memset(userinfo, 0, sizeof(userinfo)); // Init userinfo + + gi.cvar_forceset(stat_logs->name, "0"); // Turning off stat collection since bots are enabled + + if (force_gender != INVALID) // Use previous gender + { + gender = force_gender; + } + if (force_skin != NULL) // Use previous skin + { + Info_SetValueForKey(userinfo, "skin", force_skin); + } + + // Teamplay and 3TEAMS + if (teamplay->value && team) // TEAM1, TEAM2, or TEAM3 + { + // Figure out the gender based on the team skins + char* femaleSkinDirs[] = { "actionrally", "female", "sydney" }; + char* maleSkinsDirs[] = { "actionmale", "aqmarine", "male", "messiah", "sas", "terror" }; + for (int i = 0; i < sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, femaleSkinDirs[i]) != NULL) + { + gender = GENDER_FEMALE; + break; + } + } + if (gender == INVALID) + { + for (int i = 0; i < sizeof(maleSkinsDirs) / sizeof(maleSkinsDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, maleSkinsDirs[i]) != NULL) + { + gender = GENDER_MALE; + break; + } + } + } + if (gender == INVALID) // Couldn't find skin gender (perhaps server is using custom skins) + { + if (rand() % 2 == 0) // So just randomize the skin gender + gender = GENDER_MALE; + else + gender = GENDER_FEMALE; + } + } + else // Deathmatch + { + if (force_skin == NULL) // Not forcing a skin, so pick one at random + { + if (gender == GENDER_MALE || gender == GENDER_FEMALE) + BOTLIB_RandomSkin(bot, skin, gender); // Set random skin based on predefined gender + else + gender = BOTLIB_RandomSkin(bot, skin, INVALID); // Set random skin and return its gender + Info_SetValueForKey(userinfo, "skin", skin); + } + } + + Q_strlcpy(name, "AqtionMan", 10); // Set a default name + if (force_name == NULL) // Not forcing a name, so pick one at random + { + BOTLIB_LoadRandomBotName(bot, name, gender); + Info_SetValueForKey(userinfo, "name", name); + } + else // Use previous name + { + Info_SetValueForKey(userinfo, "name", force_name); + } + + // Set the gender + if (gender == GENDER_MALE) + Info_SetValueForKey(userinfo, "gender", "male"); + else if (gender == GENDER_FEMALE) + Info_SetValueForKey(userinfo, "gender", "female"); + else + Info_SetValueForKey(userinfo, "gender", "none"); + + //Set userinfo: hand, spec + Info_SetValueForKey(userinfo, "hand", "2"); // bot is center handed for now! + Info_SetValueForKey(userinfo, "spectator", "0"); // NOT a spectator + + ClientConnect(bot, userinfo); +} + +/////////////////////////////////////////////////////////////////////// +// Called by PutClient in Server to actually release the bot into the game +// Keep from killin' each other when all spawned at once +/////////////////////////////////////////////////////////////////////// +void BOTLIB_HoldSpawn(edict_t* self) +{ + if (!KillBox(self)) + { // could't spawn in? + } + + gi.linkentity(self); + + self->think = BOTAI_Think; + self->nextthink = level.framenum + 1; + + // send effect + gi.WriteByte(svc_muzzleflash); + gi.WriteShort(self - g_edicts); + gi.WriteByte(MZ_LOGIN); + gi.multicast(self->s.origin, MULTICAST_PVS); + gi.bprintf(PRINT_MEDIUM, "%s entered the game\n", self->client->pers.netname); +} + +// Modified version of id's code +void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) +{ + bot->is_bot = true; + bot->bot.bot_type = BOT_TYPE_BOTLIB; + bot->think = BOTLIB_Think; + bot->nextthink = level.framenum + 1; + + PutClientInServer(bot); + JoinTeam(bot, team, true); +} + +// Spawn a bot +void ClientBeginDeathmatch(edict_t* ent); +edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin) //char* userinfo) +{ + edict_t* bot = BOTLIB_FindFreeEntity(); + if (!bot) + { + gi.bprintf(PRINT_MEDIUM, "Server is full! To allow more players set the console command 'maxclients' higher than %d\n", game.maxclients); + return NULL; + } + + bot->is_bot = true; + bot->yaw_speed = 1000; // aiming degrees per second + + if (team == TEAM1) + bot_connections.total_team1++; + else if (team == TEAM2) + bot_connections.total_team2++; + else if (team == TEAM3) + bot_connections.total_team3++; + + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); // includes ClientConnect + + ClientBeginDeathmatch(bot); + + BOTLIB_PutClientInServer(bot, true, team); + + //rekkie -- Fake Bot Client -- s + // Set the average ping this bot will see + int rng_ping_range = rand() % 5; + if (rng_ping_range == 0) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 33)); // Low ping bastard + else if (rng_ping_range <= 3) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger + else + bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard + gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //rekkie -- Fake Bot Client -- e + + return bot; +} + + +// Remove a bot by name or all bots +void BOTLIB_RemoveBot(char* name) +{ + int i; + qboolean freed = false; + edict_t* bot; + + // =================================================== + // Kick by NAME, ALL bots, or by team num + // =================================================== + if (strlen(name)) + { + qboolean remove_all = (Q_stricmp(name, "all") == 0) ? true : false; + int find_team = (strlen(name) == 1) ? atoi(name) : 0; // Kick by team + + //if (name!=NULL) + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + bot->health = 0; + player_die(bot, bot, bot, 100000, vec3_origin); + // don't even bother waiting for death frames + //bot->deadflag = DEAD_DEAD; + //bot->inuse = false; + freed = true; + ClientDisconnect(bot); + //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + if (!remove_all) + break; + } + } + } + } + + // =================================================== + // Kick random bot + // =================================================== + int bot_to_kick = 0; + int num_bots = 0; + int bot_count = 0; + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot) + { + num_bots++; + } + } + } + if (num_bots == 0) + { + return; + } + else if (num_bots > 1) + { + bot_to_kick = (rand() % num_bots); + } + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) + { + if (bot->is_bot && bot_to_kick == bot_count) + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + bot->health = 0; + player_die(bot, bot, bot, 100000, vec3_origin); + // don't even bother waiting for death frames + //bot->deadflag = DEAD_DEAD; + //bot->inuse = false; + freed = true; + ClientDisconnect(bot); + //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + break; + } + if (bot->is_bot) + bot_count++; + } + } + + if (!freed) + gi.bprintf(PRINT_MEDIUM, "No bot removed\n", name); +} + +// Remove a bot by team +// Conditions: Bot must be dead or joined a team during an ongoing round +void BOTLIB_RemoveTeamplayBot(int team) +{ + int i; + edict_t* bot; + + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) // Ent in use + { + if (bot->is_bot) // Is a bot + { + // Only kick when the bot isn't actively in a match + //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) + if (bot->client->resp.team == team) // && team_round_going == 0) + { + //if (random() < 0.20) // Randomly kick a bot + { + //rekkie -- Fake Bot Client -- s + gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + if (team == TEAM1) + bot_connections.total_team1--; + else if (team == TEAM2) + bot_connections.total_team2--; + else if (team == TEAM3) + bot_connections.total_team3--; + + if (bot->health) + player_die(bot, bot, bot, 100000, vec3_origin); + ClientDisconnect(bot); + break; + } + } + } + } + } +} + +// Change bot [from team] ==> [to team] +// Conditions: Bot must be in a team. Change occurs after round ends. +void BOTLIB_ChangeBotTeam(int from_team, int to_team) +{ + int i; + edict_t* bot; + + for (i = 0; i < game.maxclients; i++) + { + bot = g_edicts + i + 1; + if (bot->inuse) // Ent in use + { + if (bot->is_bot) // Is a bot + { + // Only kick when the bot isn't actively in a match + //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) + if (bot->client->resp.team == from_team && team_round_going == 0) + { + if (from_team == TEAM1) bot_connections.total_team1--; + else if (from_team == TEAM2) bot_connections.total_team2--; + else if (from_team == TEAM3) bot_connections.total_team3--; + + if (to_team == TEAM1) bot_connections.total_team1++; + else if (to_team == TEAM2) bot_connections.total_team2++; + else if (to_team == TEAM3) bot_connections.total_team3++; + + JoinTeam(bot, to_team, true); + break; + } + } + } + } +} + +//rekkie -- DEV_1 -- s + +// Returns how many bots and players are playing (returned via struct). +void BOTLIB_GetTotalPlayers(bot_connections_t* bc) +{ + edict_t* ent; + + bc->total_bots = 0; + bc->total_humans = 0; + bc->total_humans_playing = 0; + + bc->total_team1 = 0; + bc->total_team2 = 0; + bc->total_team3 = 0; + + bc->spec_bots = 0; + bc->team1_bots = 0; + bc->team2_bots = 0; + bc->team3_bots = 0; + + bc->spec_humans = 0; + bc->team1_humans = 0; + bc->team2_humans = 0; + bc->team3_humans = 0; + + for (int i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[1 + i]; + if (ent && ent->inuse) + { + if (ent->is_bot) + { + bc->total_bots++; + + if (teamplay->value) + { + if (ent->client->resp.team == NOTEAM) + { + bc->spec_bots++; + } + else if (ent->client->resp.team == TEAM1) + { + bc->team1_bots++; + bc->total_team1++; + } + else if (ent->client->resp.team == TEAM2) + { + bc->team2_bots++; + bc->total_team2++; + } + else if (ent->client->resp.team == TEAM3) + { + bc->team3_bots++; + bc->total_team3++; + } + } + else if (deathmatch->value) + { + if (ent->solid == SOLID_NOT) + bc->spec_bots++; + else + bc->team1_bots++; + } + } + else + { + bc->total_humans++; + + if (teamplay->value) + { + if (ent->client->resp.team == NOTEAM) + bc->spec_humans++; + else if (ent->client->resp.team == TEAM1) + { + bc->team1_humans++; + bc->total_team1++; + } + else if (ent->client->resp.team == TEAM2) + { + bc->team2_humans++; + bc->total_team2++; + } + else if (ent->client->resp.team == TEAM3) + { + bc->team3_humans++; + bc->total_team3++; + } + } + else if (deathmatch->value) + { + if (ent->solid == SOLID_NOT) + bc->spec_humans++; + else + bc->team1_humans++; + } + } + } + } + bc->total_humans_playing = bc->total_humans - bc->spec_humans; +} + +// Bots auto join teams to keep them equal in teamplay. +// Empty servers have a bot join a team upon a player connecting. +int bot_teamcheckfrequency = 0; +int bot_teamchangefrequency = 0; +void BOTLIB_CheckBotRules(void) +{ + if (matchmode->value) // Bots never allowed in matchmode + return; + + if (ctf->value) + { + BOTLIB_Update_Flags_Status(); + } + + // check these rules every 1.0 seconds... (aka 10 frames) -- this will be faster if server hz is faster + if ((++bot_teamcheckfrequency % 10) != 0) + { + return; + } + else + { + bot_teamcheckfrequency = 0; + //Com_Printf("%s ltk_teamcheckfrequency\n", __func__); + } + + BOTLIB_GetTotalPlayers(&bot_connections); + //Com_Printf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots); + + if (bot_connections.tried_adding_prev_bots == false) + { + bot_connections.tried_adding_prev_bots = true; // Only called once per map + BOTLIB_AddBotsFromPreviousMap(100); // Try get bots from a previous map + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + + if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; // Turn on team balance if bot_maxteam is used + + // ======================================================================================================== + // Manually add bots to a select team + // sv bots + if (teamplay->value && bot_connections.auto_balance_bots == false) + { + // Sanity check + if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; + if (bot_connections.desire_team2 < 0) bot_connections.desire_team2 = 0; + if (bot_connections.desire_team3 < 0) bot_connections.desire_team3 = 0; + + // Remove bots + if (bot_connections.desire_team1 < bot_connections.team1_bots) + { + while (bot_connections.team1_bots != bot_connections.desire_team1) + { + BOTLIB_RemoveTeamplayBot(TEAM1); + bot_connections.team1_bots--; + } + } + if (bot_connections.desire_team2 < bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) + { + BOTLIB_RemoveTeamplayBot(TEAM2); + bot_connections.team2_bots--; + } + } + if (bot_connections.desire_team3 < bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) + { + BOTLIB_RemoveTeamplayBot(TEAM3); + bot_connections.team3_bots--; + } + } + + // Add bots + if (bot_connections.desire_team1 > bot_connections.team1_bots) + { + while (bot_connections.team1_bots != bot_connections.desire_team1) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team1 = bot_connections.team1_bots; + break; + } + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + if (bot_connections.desire_team2 > bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team2 = bot_connections.team2_bots; + break; + } + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + if (bot_connections.desire_team3 > bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) + { + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + { + bot_connections.desire_team3 = bot_connections.team3_bots; + break; + } + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + } + } + + /* + // Sanity check + if (bot_connections.desire_team1 < -1) bot_connections.desire_team1 = 0; + if (bot_connections.desire_team2 < -1) bot_connections.desire_team2 = 0; + if (bot_connections.desire_team3 < -1) bot_connections.desire_team3 = 0; + + int count = (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); + if (count == -1) // Remove all bots from team + { + // Remove all bots from team + if (bot_connections.desire_team1 == -1) + { + while (bot_connections.team1_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM1); + bot_connections.team1_bots--; + } + } + else if (bot_connections.desire_team2 == -1) + { + while (bot_connections.team2_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM2); + bot_connections.team2_bots--; + } + } + else if (use_3teams->value && bot_connections.desire_team3 == -1) + { + while (bot_connections.team3_bots) + { + BOTLIB_RemoveTeamplayBot(TEAM3); + bot_connections.team3_bots--; + } + } + } + if (count > 0) // Add bots to team + { + for (int i = 0; i < count; i++) + { + if (bot_connections.total_bots + bot_connections.total_humans + 1 < maxclients->value) // Max allowed + { + if (bot_connections.desire_team1) + { + if (bot_connections.team1_bots < bot_connections.desire_team1) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.team1_bots > bot_connections.desire_team1) + BOTLIB_RemoveTeamplayBot(TEAM1); + else + break; + } + else if (bot_connections.desire_team2) + { + if (bot_connections.team2_bots < bot_connections.desire_team2) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else if (bot_connections.team2_bots > bot_connections.desire_team2) + BOTLIB_RemoveTeamplayBot(TEAM2); + else + break; + } + else if (use_3teams->value && bot_connections.desire_team3) + { + if (bot_connections.team3_bots < bot_connections.desire_team3) + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + else if (bot_connections.team3_bots > bot_connections.desire_team3) + BOTLIB_RemoveTeamplayBot(TEAM3); + else + break; + } + } + } + bot_connections.desire_team1 = 0; + bot_connections.desire_team2 = 0; + bot_connections.desire_team3 = 0; + return; + } + */ + } + // ======================================================================================================== + + + // Auto balance bots + if (bot_connections.auto_balance_bots == false) + return; + + + //bot_connections.desire_bots += (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); + + // Percent chance to add/remove bots every 5 seconds... inc/dec = [min: 1, max: bot_maxteam] + if ((++bot_teamchangefrequency % 5) == 0) + { + bot_teamchangefrequency = 0; + + if (bot_maxteam->value) + { + //scale_up + float percent = (float)bot_connections.total_bots / bot_maxteam->value; + if (bot_connections.scale_dn == false && bot_connections.scale_up == false) + bot_connections.scale_up = true; + + float min_percent = 1 / bot_maxteam->value; + + // Reached top end of the scale, so work back down + if (bot_connections.scale_up && percent >= 1.0) + { + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } + // Reached bottom end of the scale, or randomly at 50%, so work back up + else if (bot_connections.scale_dn && (percent <= min_percent || random() < 0.05)) // Never drop below this percent + { + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } + + // Decrease bots + if (bot_connections.scale_dn && random() < 0.050) + bot_connections.desire_bots--; + + // Increase bots + if (bot_connections.scale_up && random() < 0.250) + bot_connections.desire_bots++; + + // Don't let bots be greater than bot_maxteam + if ((float)bot_connections.desire_bots > bot_maxteam->value) + bot_connections.desire_bots--; + + // Never go below 1 bot when bot_maxteam is active + if (bot_connections.desire_bots < 1) + bot_connections.desire_bots = 1; + } + } + + + // Always leave room for another player to join + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) + bot_connections.desire_bots = (maxclients->value - 1); + + // Sanity check - safety limits + //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + + //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); + int bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + + // Shuffle bots around + //if (teamplay->value && bots_to_spawn == 0) + if (teamplay->value && bot_connections.auto_balance_bots) + { + if (use_3teams->value && (bot_connections.total_team1 != bot_connections.total_team2 || bot_connections.total_team1 != bot_connections.total_team3 || bot_connections.total_team2 != bot_connections.total_team3)) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1 && bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + if (abs(bot_connections.total_team1 - bot_connections.total_team3) > 1 && bot_connections.total_team1 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM1); + + if (abs(bot_connections.total_team2 - bot_connections.total_team1) > 1 && bot_connections.total_team2 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + if (abs(bot_connections.total_team2 - bot_connections.total_team3) > 1 && bot_connections.total_team2 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM2); + + if (abs(bot_connections.total_team3 - bot_connections.total_team1) > 1 && bot_connections.total_team3 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM3); + if (abs(bot_connections.total_team3 - bot_connections.total_team2) > 1 && bot_connections.total_team3 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM3); + } + else if (bot_connections.total_team1 != bot_connections.total_team2) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1) + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + else + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + } + } + + } + + // Remove ALL bots + if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) + { + BOTLIB_RemoveBot("ALL"); + bot_connections.total_team1 = 0; + bot_connections.total_team2 = 0; + bot_connections.total_team3 = 0; + } + + // Remove bots + //if (bot_connections.total_bots > bot_connections.desire_bots) + if (bots_to_spawn < 0) + { + bots_to_spawn = abs(bots_to_spawn); + for (int i = 0; i < bots_to_spawn; i++) + { + if (teamplay->value) // Remove a bot from team with the highest player count + { + if (use_3teams->value) + { + if (bot_connections.total_team1 >= bot_connections.total_team2 && bot_connections.total_team1 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team2 >= bot_connections.total_team1 && bot_connections.total_team2 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM2); + else if (bot_connections.total_team3 >= bot_connections.total_team1 && bot_connections.total_team3 >= bot_connections.total_team2) + + BOTLIB_RemoveTeamplayBot(TEAM3); + else // If all teams are equal size, randomly remove bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (choice == 1) + BOTLIB_RemoveTeamplayBot(TEAM2); + else + BOTLIB_RemoveTeamplayBot(TEAM3); + } + } + else + { + if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM2); + else // If both teams are equal size, randomly remove bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else + BOTLIB_RemoveTeamplayBot(TEAM2); + } + } + } + else // DM + BOTLIB_RemoveBot(""); + } + return; + } + //else if (bot_connections.total_bots < bots_to_spawn) + else if (bots_to_spawn > 0) + { + for (int i = 0; i < bots_to_spawn; i++) + { + if (teamplay->value) // Add a bot to a team with the lowest player count + { + if (use_3teams->value) // 3TEAMS + { + if (bot_connections.total_team1 <= bot_connections.total_team2 && bot_connections.total_team1 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team2 <= bot_connections.total_team1 && bot_connections.total_team2 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else if (bot_connections.total_team3 <= bot_connections.total_team1 && bot_connections.total_team3 <= bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + else // If all teams are equal size, randomly add bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (choice == 1) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + } + } + else // TP + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else // If both teams are equal size, randomly add bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + } + } + } + else // DM + BOTLIB_SpawnBot(0, INVALID, NULL, NULL); + } + } +} //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/botlib/botlib_spawnpoints.c b/src/action/botlib/botlib_spawnpoints.c index e84f62cd7..16f513d3f 100644 --- a/src/action/botlib/botlib_spawnpoints.c +++ b/src/action/botlib/botlib_spawnpoints.c @@ -1,487 +1,487 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - -dc_sp_t* dc_sp; -int dc_sp_count; // Total spawn points -qboolean dc_sp_edit; // If the spawn points have been made visible for editing - -// Free the memory -void DC_Free_Spawnpoints(void) -{ - if (dc_sp_count && dc_sp != NULL) - { - free(dc_sp); - dc_sp = NULL; // Nullify dangling pointer - } -} -void DC_Init_Spawnpoints(void) -{ - dc_sp_edit = false; // Set edit mode to false - dc_sp_count = 0; // Total spawn points - dc_sp = NULL; -} -// Add and display a new custom spawn point at the location given -void DC_Remove_Spawnpoint(edict_t* self) -{ - if (dc_sp_count && dc_sp != NULL) - { - // Try looking for a dc_spawnpoint nearby and move it to an unused state - const int search_dist = 256; - vec3_t v; - float dist; - trace_t tr; - edict_t* spot = NULL; - while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) - { - VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player - dist = VectorLength(v); - if (dist < search_dist) // Found a spawn close enough - { - // Ensure we have LoS - tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - // Search the dc_sp list for this spot and update it - for (int i = 0; i < dc_sp_count; i++) - { - // Test the spot origin against the dc_sp[i].origin - if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location - { - // Edit the existing spawn point - dc_sp[i].inuse = false; // Change state to unused - - // Mark spot as deleted - spot->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; - - // Free spot - spot->nextthink = level.framenum + 5 * HZ; - spot->think = G_FreeEdict; - - return; - } - } - } - } - } - } -} -// Add and display a new custom spawn point at the location given -void DC_Add_Spawnpoint(edict_t* self) -{ - if (dc_sp_count + 1 > DC_SP_LIMIT) - { - Com_Printf("%s could not add spawnpoint, limit is %d!\n", __func__, DC_SP_LIMIT); - return; - } - - // Ensure we're in edit mode - if (dc_sp_edit == false) - BOTLIB_Show_Spawnpoints(); - - if (dc_sp_count && dc_sp != NULL) - { - // Try looking for an unused spot - for (int i = 0; i < dc_sp_count; i++) - { - if (dc_sp[i].inuse == false) // Recycle deleted spot - { - // Edit the existing spawn point - dc_sp[i].inuse = true; - VectorCopy(self->s.origin, dc_sp[i].origin); - VectorCopy(self->s.angles, dc_sp[i].angles); - - // Make a visible version - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes - ent->think = G_FreeEdict; - VectorCopy(self->s.origin, ent->s.origin); - VectorCopy(self->s.angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom - gi.linkentity(ent); - - return; - } - } - - // Otherwise, try looking for a dc_spawnpoint nearby and move it to the player - const int search_dist = 256; - vec3_t v; - float dist; - trace_t tr; - edict_t* spot = NULL; - while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) - { - VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player - dist = VectorLength(v); - if (dist < search_dist) // Found a spawn close enough to move - { - // Ensure we have LoS - tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); - if (tr.fraction == 1.0) - { - // Search the dc_sp list for this spot and update it - for (int i = 0; i < dc_sp_count; i++) - { - // Test the spot origin against the dc_sp[i].origin - if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location - { - // Edit the existing spawn point - dc_sp[i].inuse = true; // If it was deleted, make it used again - VectorCopy(self->s.origin, dc_sp[i].origin); - VectorCopy(self->s.angles, dc_sp[i].angles); - - // Move the spot - gi.unlinkentity(spot); - VectorCopy(self->s.origin, spot->s.origin); - VectorCopy(self->s.angles, spot->s.angles); - gi.linkentity(spot); - - return; - } - } - } - } - } - } - - // Show a visible spawn point - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes - ent->think = G_FreeEdict; - VectorCopy(self->s.origin, ent->s.origin); - VectorCopy(self->s.angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom - gi.linkentity(ent); - - // Grow memory - if (dc_sp_count == 0) - { - dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t)); - if (dc_sp == NULL) - { - Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); - return; - } - } - else - { - dc_sp_t* prev = dc_sp; - dc_sp = (dc_sp_t*)realloc(dc_sp, (dc_sp_count + 1) * sizeof(dc_sp_t)); - if (dc_sp == NULL) - { - free(prev); - prev = NULL; - Com_Printf("%s could not realloc dc_sp. Out of memory!\n", __func__); - return; - } - } - - // Add it - dc_sp[dc_sp_count].inuse = true; - VectorCopy(self->s.origin, dc_sp[dc_sp_count].origin); - VectorCopy(self->s.angles, dc_sp[dc_sp_count].angles); - - // Increment total SPs - dc_sp_count++; -} -// Find and add all the map spawn points -void DC_Get_Map_Spawnpoints(void) -{ - edict_t* spot = NULL; - while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) - DC_Add_Spawnpoint(spot); -} -// Find and display (as visible ents) all spawn points (map and custom) -// Custom spawn points are green -// Deleted spawn points are red -void BOTLIB_Show_Spawnpoints(void) -{ - const int free_time = (60 * 1); // Free it after (60 seconds * 15) = 15 minutes - - // Only spawn them once per map - if (dc_sp_edit == false) - { - dc_sp_edit = true; - } - else - return; - - // Show map spawn points - qboolean is_custom_spot; - edict_t* spot = NULL; - while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) - { - // Skip adding custom spots that have been labled as "info_player_deathmatch" - // This occurs when loading custom spots from file, we set them as classname "info_player_deathmatch" - // so the spawn code can find them. However, because custom spots need editing, we don't want them 'visually' - // showing up as info_player_deathmatch, but rather as dc_spawnpoint which is handled later in this function. - is_custom_spot = false; - if (dc_sp_count && dc_sp != NULL) - { - for (int i = 0; i < dc_sp_count; i++) - { - if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location - { - is_custom_spot = true; - break; - } - } - } - if (is_custom_spot) continue; // Skip adding this spot because its custom - - // Show a visible spawn point - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint_deathmath"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + free_time * HZ; - ent->think = G_FreeEdict; - VectorCopy(spot->s.origin, ent->s.origin); - VectorCopy(spot->s.angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - gi.linkentity(ent); - } - - while ((spot = G_Find(spot, FOFS(classname), "info_player_team1")) != NULL) - { - // Show a visible spawn point - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint_team1"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + free_time * HZ; - ent->think = G_FreeEdict; - VectorCopy(spot->s.origin, ent->s.origin); - VectorCopy(spot->s.angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - gi.linkentity(ent); - } - - while ((spot = G_Find(spot, FOFS(classname), "info_player_team2")) != NULL) - { - // Show a visible spawn point - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint_team2"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + free_time * HZ; - ent->think = G_FreeEdict; - VectorCopy(spot->s.origin, ent->s.origin); - VectorCopy(spot->s.angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - gi.linkentity(ent); - } - - // Show custom user spawn points - if (dc_sp_count && dc_sp != NULL) - { - for (int i = 0; i < dc_sp_count; i++) - { - // Show a visible spawn point - edict_t* ent; - ent = G_Spawn(); - ent->classname = "dc_spawnpoint"; - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point - ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); - ent->owner = ent; - ent->nextthink = level.framenum + free_time * HZ; - ent->think = G_FreeEdict; - VectorCopy(dc_sp[i].origin, ent->s.origin); - VectorCopy(dc_sp[i].angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - if (dc_sp[i].inuse == false) ent->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; // Mark as not in use - else ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom - gi.linkentity(ent); - } - } -} -// Save SPs to file -void DC_Save_Spawnpoints() -{ - FILE* fOut; - char filename[128]; - int fileSize = 0; - short int i = 0; - byte version = DC_SP_VERSION; - cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\spawnpoints\\"); - i += sprintf(filename + i, level.mapname); - i += sprintf(filename + i, ".sp"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/spawnpoints/"); - strcat(filename, level.mapname); - strcat(filename, ".sp"); -#endif - - // Add the map spawn points - //DC_Get_Map_Spawnpoints(); - - if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary - { - Com_Printf("%s Failed to write spawnpoint data to file %s\n", __func__, filename); - return; - } - - Com_Printf("%s Writing spawnpoint data to file...\n", __func__); - - // Write general data - fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version - fileSize += sizeof(int) * fwrite(&dc_sp_count, sizeof(int), 1, fOut); // Total spawnpoints - - // Write spawnpoint data - for (i = 0; i < dc_sp_count; i++) - { - fileSize += sizeof(qboolean) * fwrite(&dc_sp[i].inuse, sizeof(qboolean), 1, fOut); // In use - fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].origin, sizeof(vec3_t), 1, fOut); // Location - fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].angles, sizeof(vec3_t), 1, fOut); // Direction - } - - fclose(fOut); - - Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); -} -// Load SPs from file -void DC_Load_Spawnpoints() -{ - FILE* fIn; - char filename[128]; - int fileSize = 0; - short int i = 0; - byte version = 0; - cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib - cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib - -#ifdef _WIN32 - i = sprintf(filename, ".\\"); - i += sprintf(filename + i, game_dir->string); - i += sprintf(filename + i, "\\"); - i += sprintf(filename + i, botdir->string); - i += sprintf(filename + i, "\\spawnpoints\\"); - i += sprintf(filename + i, level.mapname); - i += sprintf(filename + i, ".sp"); -#else - strcpy(filename, "./"); - strcat(filename, game_dir->string); - strcat(filename, "/"); - strcat(filename, botdir->string); - strcat(filename, "/spawnpoints/"); - strcat(filename, level.mapname); - strcat(filename, ".sp"); -#endif - - if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists - { - Com_Printf("%s %s not found. The map has no custom spawnpoints\n", __func__, filename); - return; // Map has no spawnpoint data - } - else - { - Com_Printf("%s Reading SP file...\n", __func__); - fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // AAS Version - if (version != DC_SP_VERSION) - { - Com_Printf("%s %s is from a previous version. File is incompatible.\n", __func__, filename); - fclose(fIn); - return; - } - } - - fileSize += sizeof(int) * fread(&dc_sp_count, sizeof(int), 1, fIn); // Total Nodes - - DC_Free_Spawnpoints(); //Soft map change. Free any existing spawnpoint memory used - - // Grow memory - if (dc_sp_count) - { - dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t) * dc_sp_count); - if (dc_sp == NULL) - { - Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); - fclose(fIn); - return; - } - } - - // Read SP data - for (i = 0; i < dc_sp_count; i++) - { - fileSize += sizeof(qboolean) * fread(&dc_sp[i].inuse, sizeof(qboolean), 1, fIn); // In use - fileSize += sizeof(vec3_t) * fread(&dc_sp[i].origin, sizeof(vec3_t), 1, fIn); // Location - fileSize += sizeof(vec3_t) * fread(&dc_sp[i].angles, sizeof(vec3_t), 1, fIn); // Direction - } - - // Show custom user spawn points - if (dc_sp_count && dc_sp != NULL) - { - for (int i = 0; i < dc_sp_count; i++) - { - if (dc_sp[i].inuse) // Ignore deleted spawn spots - { - // Add the custom spawn points to the map - edict_t* ent; - ent = G_Spawn(); - ent->classname = "info_player_deathmatch"; // Make it findable - ent->movetype = MOVETYPE_NONE; - ent->solid = SOLID_NOT; - ent->owner = ent; - VectorCopy(dc_sp[i].origin, ent->s.origin); - VectorCopy(dc_sp[i].angles, ent->s.angles); - VectorSet(ent->mins, -16, -16, -24); - VectorSet(ent->maxs, 16, 16, 32); - gi.linkentity(ent); - } - } - } - - fclose(fIn); - - Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +dc_sp_t* dc_sp; +int dc_sp_count; // Total spawn points +qboolean dc_sp_edit; // If the spawn points have been made visible for editing + +// Free the memory +void DC_Free_Spawnpoints(void) +{ + if (dc_sp_count && dc_sp != NULL) + { + free(dc_sp); + dc_sp = NULL; // Nullify dangling pointer + } +} +void DC_Init_Spawnpoints(void) +{ + dc_sp_edit = false; // Set edit mode to false + dc_sp_count = 0; // Total spawn points + dc_sp = NULL; +} +// Add and display a new custom spawn point at the location given +void DC_Remove_Spawnpoint(edict_t* self) +{ + if (dc_sp_count && dc_sp != NULL) + { + // Try looking for a dc_spawnpoint nearby and move it to an unused state + const int search_dist = 256; + vec3_t v; + float dist; + trace_t tr; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) + { + VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player + dist = VectorLength(v); + if (dist < search_dist) // Found a spawn close enough + { + // Ensure we have LoS + tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Search the dc_sp list for this spot and update it + for (int i = 0; i < dc_sp_count; i++) + { + // Test the spot origin against the dc_sp[i].origin + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + // Edit the existing spawn point + dc_sp[i].inuse = false; // Change state to unused + + // Mark spot as deleted + spot->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; + + // Free spot + spot->nextthink = level.framenum + 5 * HZ; + spot->think = G_FreeEdict; + + return; + } + } + } + } + } + } +} +// Add and display a new custom spawn point at the location given +void DC_Add_Spawnpoint(edict_t* self) +{ + if (dc_sp_count + 1 > DC_SP_LIMIT) + { + Com_Printf("%s could not add spawnpoint, limit is %d!\n", __func__, DC_SP_LIMIT); + return; + } + + // Ensure we're in edit mode + if (dc_sp_edit == false) + BOTLIB_Show_Spawnpoints(); + + if (dc_sp_count && dc_sp != NULL) + { + // Try looking for an unused spot + for (int i = 0; i < dc_sp_count; i++) + { + if (dc_sp[i].inuse == false) // Recycle deleted spot + { + // Edit the existing spawn point + dc_sp[i].inuse = true; + VectorCopy(self->s.origin, dc_sp[i].origin); + VectorCopy(self->s.angles, dc_sp[i].angles); + + // Make a visible version + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes + ent->think = G_FreeEdict; + VectorCopy(self->s.origin, ent->s.origin); + VectorCopy(self->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + + return; + } + } + + // Otherwise, try looking for a dc_spawnpoint nearby and move it to the player + const int search_dist = 256; + vec3_t v; + float dist; + trace_t tr; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "dc_spawnpoint")) != NULL) + { + VectorSubtract(spot->s.origin, self->s.origin, v); // Distance from player + dist = VectorLength(v); + if (dist < search_dist) // Found a spawn close enough to move + { + // Ensure we have LoS + tr = gi.trace(self->s.origin, NULL, NULL, spot->s.origin, self, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + // Search the dc_sp list for this spot and update it + for (int i = 0; i < dc_sp_count; i++) + { + // Test the spot origin against the dc_sp[i].origin + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + // Edit the existing spawn point + dc_sp[i].inuse = true; // If it was deleted, make it used again + VectorCopy(self->s.origin, dc_sp[i].origin); + VectorCopy(self->s.angles, dc_sp[i].angles); + + // Move the spot + gi.unlinkentity(spot); + VectorCopy(self->s.origin, spot->s.origin); + VectorCopy(self->s.angles, spot->s.angles); + gi.linkentity(spot); + + return; + } + } + } + } + } + } + + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + (60 * 15) * HZ; // Free it after (60 sec * 15) 15 minutes + ent->think = G_FreeEdict; + VectorCopy(self->s.origin, ent->s.origin); + VectorCopy(self->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + + // Grow memory + if (dc_sp_count == 0) + { + dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t)); + if (dc_sp == NULL) + { + Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); + return; + } + } + else + { + dc_sp_t* prev = dc_sp; + dc_sp = (dc_sp_t*)realloc(dc_sp, (dc_sp_count + 1) * sizeof(dc_sp_t)); + if (dc_sp == NULL) + { + free(prev); + prev = NULL; + Com_Printf("%s could not realloc dc_sp. Out of memory!\n", __func__); + return; + } + } + + // Add it + dc_sp[dc_sp_count].inuse = true; + VectorCopy(self->s.origin, dc_sp[dc_sp_count].origin); + VectorCopy(self->s.angles, dc_sp[dc_sp_count].angles); + + // Increment total SPs + dc_sp_count++; +} +// Find and add all the map spawn points +void DC_Get_Map_Spawnpoints(void) +{ + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + DC_Add_Spawnpoint(spot); +} +// Find and display (as visible ents) all spawn points (map and custom) +// Custom spawn points are green +// Deleted spawn points are red +void BOTLIB_Show_Spawnpoints(void) +{ + const int free_time = (60 * 1); // Free it after (60 seconds * 15) = 15 minutes + + // Only spawn them once per map + if (dc_sp_edit == false) + { + dc_sp_edit = true; + } + else + return; + + // Show map spawn points + qboolean is_custom_spot; + edict_t* spot = NULL; + while ((spot = G_Find(spot, FOFS(classname), "info_player_deathmatch")) != NULL) + { + // Skip adding custom spots that have been labled as "info_player_deathmatch" + // This occurs when loading custom spots from file, we set them as classname "info_player_deathmatch" + // so the spawn code can find them. However, because custom spots need editing, we don't want them 'visually' + // showing up as info_player_deathmatch, but rather as dc_spawnpoint which is handled later in this function. + is_custom_spot = false; + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + if (VectorCompare(spot->s.origin, dc_sp[i].origin)) // Same location + { + is_custom_spot = true; + break; + } + } + } + if (is_custom_spot) continue; // Skip adding this spot because its custom + + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_deathmath"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + while ((spot = G_Find(spot, FOFS(classname), "info_player_team1")) != NULL) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_team1"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + while ((spot = G_Find(spot, FOFS(classname), "info_player_team2")) != NULL) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_team2"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + + // Show custom user spawn points + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(dc_sp[i].origin, ent->s.origin); + VectorCopy(dc_sp[i].angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + if (dc_sp[i].inuse == false) ent->s.renderfx = RF_SHELL_RED | RF_TRANSLUCENT; // Mark as not in use + else ent->s.renderfx = RF_SHELL_GREEN | RF_TRANSLUCENT; // Mark as custom + gi.linkentity(ent); + } + } +} +// Save SPs to file +void DC_Save_Spawnpoints() +{ + FILE* fOut; + char filename[128]; + int fileSize = 0; + short int i = 0; + byte version = DC_SP_VERSION; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\spawnpoints\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".sp"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/spawnpoints/"); + strcat(filename, level.mapname); + strcat(filename, ".sp"); +#endif + + // Add the map spawn points + //DC_Get_Map_Spawnpoints(); + + if ((fOut = fopen(filename, "wb")) == NULL) // Open file to write binary + { + Com_Printf("%s Failed to write spawnpoint data to file %s\n", __func__, filename); + return; + } + + Com_Printf("%s Writing spawnpoint data to file...\n", __func__); + + // Write general data + fileSize += sizeof(byte) * fwrite(&version, sizeof(byte), 1, fOut); // Version + fileSize += sizeof(int) * fwrite(&dc_sp_count, sizeof(int), 1, fOut); // Total spawnpoints + + // Write spawnpoint data + for (i = 0; i < dc_sp_count; i++) + { + fileSize += sizeof(qboolean) * fwrite(&dc_sp[i].inuse, sizeof(qboolean), 1, fOut); // In use + fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].origin, sizeof(vec3_t), 1, fOut); // Location + fileSize += sizeof(vec3_t) * fwrite(&dc_sp[i].angles, sizeof(vec3_t), 1, fOut); // Direction + } + + fclose(fOut); + + Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); +} +// Load SPs from file +void DC_Load_Spawnpoints() +{ + FILE* fIn; + char filename[128]; + int fileSize = 0; + short int i = 0; + byte version = 0; + cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib + cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib + +#ifdef _WIN32 + i = sprintf(filename, ".\\"); + i += sprintf(filename + i, game_dir->string); + i += sprintf(filename + i, "\\"); + i += sprintf(filename + i, botdir->string); + i += sprintf(filename + i, "\\spawnpoints\\"); + i += sprintf(filename + i, level.mapname); + i += sprintf(filename + i, ".sp"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/spawnpoints/"); + strcat(filename, level.mapname); + strcat(filename, ".sp"); +#endif + + if ((fIn = fopen(filename, "rb")) == NULL) // See if AAS file exists + { + Com_Printf("%s %s not found. The map has no custom spawnpoints\n", __func__, filename); + return; // Map has no spawnpoint data + } + else + { + Com_Printf("%s Reading SP file...\n", __func__); + fileSize += sizeof(byte) * fread(&version, sizeof(byte), 1, fIn); // AAS Version + if (version != DC_SP_VERSION) + { + Com_Printf("%s %s is from a previous version. File is incompatible.\n", __func__, filename); + fclose(fIn); + return; + } + } + + fileSize += sizeof(int) * fread(&dc_sp_count, sizeof(int), 1, fIn); // Total Nodes + + DC_Free_Spawnpoints(); //Soft map change. Free any existing spawnpoint memory used + + // Grow memory + if (dc_sp_count) + { + dc_sp = (dc_sp_t*)malloc(sizeof(dc_sp_t) * dc_sp_count); + if (dc_sp == NULL) + { + Com_Printf("%s could not alloc dc_sp. Out of memory!\n", __func__); + fclose(fIn); + return; + } + } + + // Read SP data + for (i = 0; i < dc_sp_count; i++) + { + fileSize += sizeof(qboolean) * fread(&dc_sp[i].inuse, sizeof(qboolean), 1, fIn); // In use + fileSize += sizeof(vec3_t) * fread(&dc_sp[i].origin, sizeof(vec3_t), 1, fIn); // Location + fileSize += sizeof(vec3_t) * fread(&dc_sp[i].angles, sizeof(vec3_t), 1, fIn); // Direction + } + + // Show custom user spawn points + if (dc_sp_count && dc_sp != NULL) + { + for (int i = 0; i < dc_sp_count; i++) + { + if (dc_sp[i].inuse) // Ignore deleted spawn spots + { + // Add the custom spawn points to the map + edict_t* ent; + ent = G_Spawn(); + ent->classname = "info_player_deathmatch"; // Make it findable + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->owner = ent; + VectorCopy(dc_sp[i].origin, ent->s.origin); + VectorCopy(dc_sp[i].angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + } + } + + fclose(fIn); + + Com_Printf("%s Loaded %s [%i bytes] from disk\n", __func__, filename, fileSize); } \ No newline at end of file diff --git a/src/action/botlib/botlib_weapons.c b/src/action/botlib/botlib_weapons.c index 79bb790b7..462b66ff2 100644 --- a/src/action/botlib/botlib_weapons.c +++ b/src/action/botlib/botlib_weapons.c @@ -1,1237 +1,1237 @@ -#include "../g_local.h" -#include "../acesrc/acebot.h" -#include "botlib.h" - - - -// Returns how much ammunition is loaded/chambered in a weapon -- this is *NOT* spare ammo -int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num) -{ - switch (item_num) - { - case MK23_NUM: - return self->client->mk23_rds; - case DUAL_NUM: - return self->client->dual_rds; - case MP5_NUM: - return self->client->mp5_rds; - case M4_NUM: - return self->client->m4_rds; - case M3_NUM: - return self->client->shot_rds; - case HC_NUM: - return self->client->cannon_rds; - case SNIPER_NUM: - return self->client->sniper_rds; - case KNIFE_NUM: - case GRENADE_NUM: - return self->client->inventory[ITEM_INDEX(FindItemByNum(item_num))]; - default: - return 0; - } - - return 0; -} - -// -qboolean BOTLIB_ChangeSpecialWeapon(edict_t* self, int item_num) -{ - int ammo_index; - gitem_t* ammo_item; - qboolean loaded = true; - qboolean clips = true; - - gitem_t* item = FindItemByNum(item_num); - - // Check if weapon is in inventory - if (self->client->inventory[ITEM_INDEX(item)] == 0) - return false; - - // Do we have spare ammo - if (item->ammo) - { - ammo_item = FindItem(item->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - - // Check loaded ammo - if (BOTLIB_CheckWeaponLoadedAmmo(self, item_num) == 0) - loaded = false; - - - // see if we're already using it - if (item == self->client->weapon) - { - if (!loaded && !clips) // Drop weapon if we've got no ammo - { - DropSpecialWeapon(self); - return false; - } - else if (!loaded) // Reload if we've got spare ammo - { - BOTLIB_Reload(self); - //Cmd_New_Reload_f(self); - return true; - } - else - return true; // Nothing to do, weapon is good to go - } - - // No ammo - if (!loaded && !clips) - { - DropSpecialWeapon(self); - return false; - } - - // Change to this weapon - self->client->newweapon = item; - ChangeWeapon(self); - return true; -} - -// Bot checks its weapons: reload, drop empty, or change weapon... before encountering an enemy -void BOTLIB_ReadyWeapon(edict_t* self) -{ - // Limit how often this is called - if (self->bot.last_weapon_change_time > level.framenum) - return; - self->bot.last_weapon_change_time = level.framenum + 6 * HZ; - //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); - - /* - // Random chance to pull out grenades - if ((rand() % 20) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) //ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) - { - self->client->pers.grenade_mode = 2; - return; - } - if (self->client->weapon == FindItemByNum(GRENADE_NUM)) - return; - - - // Random chance to just use knives - if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return; - } - if (self->client->weapon == FindItemByNum(KNIFE_NUM)) - return; - */ - - - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return; - - if (INV_AMMO(self, LASER_NUM)) - { - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; - } - - if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return; -} - -// Choose the best weapon for the situation -qboolean BOTLIB_ChooseWeapon(edict_t* self) -{ - // Limit how often this is called - if (self->bot.last_weapon_change_time > level.framenum) - return true; - self->bot.last_weapon_change_time = level.framenum + 6 * HZ; - //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); - - // Don't change if weaapon is busy - if (self->client->weaponstate == WEAPON_DROPPING || self->client->weaponstate == WEAPON_BUSY) - return true; - - - // If not firing weapon - if (self->client->weaponstate == WEAPON_READY && self->client->weapon != FindItemByNum(HC_NUM) && self->client->weapon != FindItemByNum(M3_NUM)) - { - // Random chance to pull out grenades - if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(GRENADE_NUM) && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) - { - self->client->pers.grenade_mode = 2; - return true; - } - - // Random chance to just use knives - //if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - if (self->enemy && self->bot.enemy_dist <= 512) // More included to use knives when enemy is closer - { - if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - } - else // Less included to use knives when enemy is far - { - if ((rand() % 5) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - } - } - - - if (self->enemy && self->bot.enemy_dist <= 1200) - { - // Let bots use the grenades if already using - if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, GRENADE_NUM)) - { - if (self->bot.enemy_dist > 800) - self->client->pers.grenade_mode = 2; - else if (self->bot.enemy_dist > 400) - self->client->pers.grenade_mode = 1; - else - self->client->pers.grenade_mode = 0; - return true; - } - // Let bots use the knives if already using - if (self->client->weapon == FindItemByNum(KNIFE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - - // Let bots use the pistol if already using - if (self->client->weapon == FindItemByNum(MK23_NUM) && self->client->mk23_rds == self->client->mk23_max) - { - return true; - } - } - - /* - // Friendly fire after round should be fought with honor. - if( team_round_countdown && ff_afterround->value ) - { - if( ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) - return true; - if( ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) - return true; - if( ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME)) ) - { - self->client->pers.grenade_mode = ( self->bot.enemy_dist > 500) ? 1 : 0; - return true; - } - } - */ - - /* - if (self->client->weapon == FindItemByNum(GRENADE_NUM)) - { - if (self->client->weaponstate == WEAPON_ACTIVATING || self->client->weaponstate == WEAPON_FIRING) - return true; - } - */ - - // Force bots use grenades only - /* - if (ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) - { - self->client->pers.grenade_mode = 2; - return true; - } - */ - - if (self->enemy == NULL) - { - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - - if (INV_AMMO(self, LASER_NUM)) - { - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - - return true; - } - - - // Extreme range - if (self->bot.enemy_dist > 1300) - { - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - } - - // Long range - if (self->bot.enemy_dist > 1000) - { - /* - // Random chance to pull out grenades - if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) - { - self->client->pers.grenade_mode = 2; - return true; - } - // Random chance to just use knives - if ((rand() % 6) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - */ - - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - - // Allow bots that picked throwing knives to use them at a distance - if (INV_AMMO(self, KNIFE_NUM) >= 2 && self->client->pers.chosenItem == FindItemByNum(KNIFE_NUM) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - - if (INV_AMMO(self, SIL_NUM) && BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; - - // If bot has a HC and healthy: - // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol - if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) - && self->client->leg_damage == 0 && self->client->bleeding == 0 - && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) - { - return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - } - - // Longer range - if (self->bot.enemy_dist > 700) - { - /* - // Random chance to pull out grenades - if ((rand() % 2) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) - { - self->client->pers.grenade_mode = 2; - return true; - } - // Random chance to just use knives - if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - */ - - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - - if (INV_AMMO(self, SIL_NUM)) - { - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; - - // If bot has a HC and healthy: - // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol - if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) - && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) - { - return true; - } - - if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; - } - - // Long range - if (self->bot.enemy_dist > 500) - { - /* - // Random chance to pull out grenades - if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) - { - self->client->pers.grenade_mode = 2; - return true; - } - // Random chance to just use knives - if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - */ - - if (INV_AMMO(self, LASER_NUM)) - { - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - - // If bot has a HC and healthy: - // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol - if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) - && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) - { - return true; - } - - if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - } - - // Medium range - if (self->bot.enemy_dist) - { - if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; - - /* - // Random chance to pull out grenades - if ((rand() % 10) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) - { - self->client->pers.grenade_mode = 2; - return true; - } - // Random chance to just use knives - if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) - { - self->client->pers.knife_mode = 1; // Throwing knives - return true; - } - */ - - if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; - if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; - - // Should punch as a last resort? - } - - // We have no weapon available for use. - //if (debug_mode) gi.bprintf(PRINT_HIGH, "%s: No weapon available...\n", self->client->pers.netname); - return false; -} - -void BOTLIB_Reload(edict_t* self) -{ - int ammo_index, rounds = 0; - gitem_t* ammo_item; - qboolean loaded = true; - qboolean clips = true; - qboolean using_special = false; // If bot is using high powered weapon - - // Limit how often this is called - if (self->bot.last_weapon_reload_time > level.framenum) - return; - self->bot.last_weapon_reload_time = level.framenum + 2 * HZ; - - if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME) - || self->client->weapon == FindItem(MP5_NAME) || self->client->weapon == FindItem(M4_NAME) - || self->client->weapon == FindItem(M3_NAME) || self->client->weapon == FindItem(HC_NAME) - || self->client->weapon == FindItem(SNIPER_NAME)) - { - // Do we have spare ammo - if (self->client->weapon->ammo) - { - ammo_item = FindItem(self->client->weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < 1) - clips = false; - else - clips = true; - } - - // Check ammo - if (self->client->weapon == FindItem(MK23_NAME)) - { - rounds = self->client->mk23_rds; - if (self->client->weaponstate == WEAPON_END_MAG) - rounds = 0; - if (rounds < 5) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - else if (self->client->weapon == FindItem(DUAL_NAME)) - { - rounds = self->client->dual_rds; - if (self->client->weaponstate == WEAPON_END_MAG) - rounds = 0; - if (rounds < 6) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - else if (self->client->weapon == FindItem(MP5_NAME)) - { - using_special = true; - rounds = self->client->mp5_rds; - if (rounds < 5) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - else if (self->client->weapon == FindItem(M4_NAME)) - { - using_special = true; - rounds = self->client->m4_rds; - if (rounds < 5) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - else if (self->client->weapon == FindItem(M3_NAME)) - { - //using_special = true; - rounds = self->client->shot_rds; - if (rounds < self->client->shot_max) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - else if (self->client->weapon == FindItem(HC_NAME)) - { - //using_special = true; - rounds = self->client->cannon_rds; - if (hc_single->value && self->client->pers.hc_mode) // Single barrel fire mode - { - if (rounds < 1) - loaded = false; - } - else if (rounds < 2) // Double barrel fire mode - loaded = false; - } - else if (self->client->weapon == FindItem(SNIPER_NAME)) - { - using_special = true; - rounds = self->client->sniper_rds; - if (rounds < self->client->sniper_max) - loaded = false; - if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy - loaded = true; - } - - //Com_Printf("%s %s inv_ammo[%d] rnds[%d] loaded[%d] clips[%d]\n", __func__, self->client->pers.netname, self->client->inventory[ammo_index], rounds, loaded, clips); - - // No ammo - drop weapon - if (!loaded && !clips) - { - DropSpecialWeapon(self); - } - // Reload - else if (!loaded) - { - // If using high powered weapon that is out of ammo and bot has a full pistol, switch to pistol - if (rounds == 0 && using_special && self->bot.enemy_dist < 512 && self->client->mk23_rds == self->client->mk23_max)// && self->bot.see_enemies) - { - // Try switch to pistol instead of reloading current weapon - BOTLIB_ChangeSpecialWeapon(self, MK23_NUM); - return; - } - - self->client->reload_attempts += 2; // Reload twice in one attempt - - //Cmd_New_Reload_f(self); - } - } -} - -// If have sniper and not zoomed in, zoom in and return true. -// Returns false otherwise -qboolean BOTLIB_SniperZoom(edict_t* self) -{ - if (self->bot.last_sniper_zoom_time > level.framenum) - { - return true; - } - - if (self->client->weapon == FindItem(SNIPER_NAME) && (self->client->resp.sniper_mode != SNIPER_2X || self->client->desired_fov != SNIPER_FOV2)) - { - if (self->client->weaponstate != WEAPON_FIRING && self->client->weaponstate != WEAPON_BUSY && self->client->weaponstate != WEAPON_RELOADING - && !self->client->bandaging && !self->client->bandage_stopped) - { - //Com_Printf("%s %s sniper zoom X2 [%d]\n", __func__, self->client->pers.netname, level.framenum); - self->bot.last_sniper_zoom_time = level.framenum + 1 * HZ; - _SetSniper(self, 2); - return true; - } - } - - return false; -} - -//rekkie -- collecting weapons, items, ammo -- s - -// Checks if the item is on the map and can be picked up by the bot -// Instances where it cannot be picked up: it's been taken by another player, or it's just not on the map -qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum) -{ - int base = 1 + game.maxclients + BODY_QUEUE_SIZE; - edict_t* ent = g_edicts + base; - - for (int i = base; i < globals.num_edicts; i++, ent++) - { - if (ent->inuse == false) continue; - if (!ent->classname) continue; - if (ent->solid == SOLID_NOT) continue; - //Com_Printf("%s %s typeNum[%d] dist[%f]\n", __func__, ent->classname, ent->typeNum, VectorDistance(self->s.origin, ent->s.origin)); - if (ent->typeNum == typeNum) - return true; - } - - return false; -} - -// Do we need a primary weapon? -// Pass parameters: primary_weapon and secondary_weapon to get a return on the weapons the bot is carrying -// The secondary_weapon only returns if the bot is using a bandolier -qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon) -{ - int weapon_count = 0; - qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; - - // Figure out what weapons we have - for (int i = 0; i < game.num_items; i++) - { - if (self->client->inventory[i] > 0) - { - switch (itemlist[i].typeNum) - { - case MP5_NUM: - if (*primary_weapon == 0) - *primary_weapon = MP5_NUM; - else - *secondary_weapon = MP5_NUM; - weapon_count++; - break; - - case M4_NUM: - if (*primary_weapon == 0) - *primary_weapon = M4_NUM; - else - *secondary_weapon = M4_NUM; - weapon_count++; - break; - - case M3_NUM: - if (*primary_weapon == 0) - *primary_weapon = M3_NUM; - else - *secondary_weapon = M3_NUM; - weapon_count++; - break; - - case HC_NUM: - if (*primary_weapon == 0) - *primary_weapon = HC_NUM; - else - *secondary_weapon = HC_NUM; - weapon_count++; - break; - - case SNIPER_NUM: - if (*primary_weapon == 0) - *primary_weapon = SNIPER_NUM; - else - *secondary_weapon = SNIPER_NUM; - weapon_count++; - break; - } - - //Com_Printf("%s inventory[%d] = %d [%s]\n", __func__, i, self->client->inventory[i], itemlist[i].classname); - } - } - - if (band && weapon_count < 2) // Bandolier: desire two primary weapons - return true; - if (weapon_count < 1) // Desire at least one primary weapon - return true; - - return false; // Don't need a primary -} - -// Do we need a grendes? -qboolean BOTLIB_Need_Grenades(edict_t* self) -{ - int grens_count = 0; - qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; - - // Figure out what weapons we have - for (int i = 0; i < game.num_items; i++) - { - if (self->client->inventory[i] > 0) - { - switch (itemlist[i].typeNum) - { - case GRENADE_NUM: - grens_count = self->client->inventory[i]; - break; - } - } - } - - if (band && grens_count < 4) // Bandolier: desire up to four greandes - return true; - if (grens_count < 1) // Desire at least one grenade - return true; - - return false; // Don't need grenades -} - -// Do we need a knives? -qboolean BOTLIB_Need_Knives(edict_t* self) -{ - int knives_count = 0; - qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; - - // Figure out what weapons we have - for (int i = 0; i < game.num_items; i++) - { - if (self->client->inventory[i] > 0) - { - switch (itemlist[i].typeNum) - { - case KNIFE_NUM: - knives_count = self->client->inventory[i]; - break; - } - } - } - - if (band && knives_count < 20) // Bandolier: desire up to 20 knives - return true; - if (knives_count < 4) // Desire at least a few knives - return true; - - return false; // Don't need knives -} - -// Do we need a dual pistols? -qboolean BOTLIB_Need_Dual_MK23(edict_t* self) -{ - // Figure out what weapons we have - for (int i = 0; i < game.num_items; i++) - { - if (self->client->inventory[i] > 0) - { - switch (itemlist[i].typeNum) - { - case DUAL_NUM: - return false; // Don't need another pistol - break; - } - } - } - - return true; // Desire a pair of mk23 pistols -} - -// Do we need ammo for weapons we carry: mk23, primary, or secondary (if any) -// Pass parameters input: primary_weapon and secondary_weapon -// Returns ammo type -int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon) -{ - gitem_t* weapon; - gitem_t* ammo_item; - int ammo_index = 0; - - if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) - { - weapon = FindItem(MP5_NAME); - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_mp5mags) - { - // Check there's ammo for this weapon ready for pickup on the map - if (BOTLIB_CanTouchItem(self, MP5_ANUM)) - return MP5_ANUM; - } - } - if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) - { - weapon = FindItem(M4_NAME); - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_m4mags) - { - if (BOTLIB_CanTouchItem(self, M4_ANUM)) - return M4_ANUM; - } - } - if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) - { - weapon = FindItem(M3_NAME); - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_shells) - { - if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) - return SHELL_ANUM; - } - } - if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) - { - weapon = FindItem(HC_NAME); - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_shells) - { - if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) - return SHELL_ANUM; - } - } - if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) - { - weapon = FindItem(SNIPER_NAME); - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_sniper_rnds) - { - if (BOTLIB_CanTouchItem(self, SNIPER_ANUM)) - return SNIPER_ANUM; - } - } - - // Dual MK23 - weapon = FindItem(DUAL_NAME); - if (weapon) - { - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_pistolmags) - return DUAL_NUM; - } - - // MK23 - weapon = FindItem(MK23_NAME); - if (weapon) - { - ammo_item = FindItem(weapon->ammo); - ammo_index = ITEM_INDEX(ammo_item); - if (self->client->inventory[ammo_index] < self->client->max_pistolmags) - return MK23_ANUM; - } - - return 0; -} - -// Do we need a primary weapon? -// Pass parameters input: primary_weapon and secondary_weapon -// Returns a special best fit for the weapons used -int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon) -{ - int special = 0; - int random = 0; - - // Figure out what special items we have - for (int i = 0; i < game.num_items; i++) - { - if (special) - break; - - if (self->client->inventory[i] > 0) - { - switch (itemlist[i].typeNum) - { - case SIL_NUM: - special = SIL_NUM; - break; - case SLIP_NUM: - special = SLIP_NUM; - break; - case BAND_NUM: - special = BAND_NUM; - break; - case KEV_NUM: - special = KEV_NUM; - break; - case LASER_NUM: - special = LASER_NUM; - break; - case HELM_NUM: - special = HELM_NUM; - break; - } - } - } - - // No special items, so lets pick one based on our weapons in inventory - if (special == 0) - { - if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) - { - random = rand() % 5; - if (random == 0) - return SIL_NUM; - else if (random == 1) - return BAND_NUM; - else if (random == 2) - return KEV_NUM; - else if (random == 3) - return LASER_NUM; - else - return HELM_NUM; - } - else if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) - { - random = rand() % 5; - if (random == 1) - return BAND_NUM; - else if (random == 2) - return KEV_NUM; - else if (random == 3) - return LASER_NUM; - else - return HELM_NUM; - } - else if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) - { - random = rand() % 4; - if (random == 0) - return SLIP_NUM; - else if (random == 1) - return BAND_NUM; - else if (random == 2) - return KEV_NUM; - else - return HELM_NUM; - } - else if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) - { - random = rand() % 4; - if (random == 0) - return SLIP_NUM; - else if (random == 1) - return BAND_NUM; - else if (random == 2) - return KEV_NUM; - else - return HELM_NUM; - } - else if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) - { - random = rand() % 4; - if (random == 0) - return SIL_NUM; - else if (random == 1) - return BAND_NUM; - else if (random == 2) - return KEV_NUM; - else - return HELM_NUM; - } - else // Just get a random item - { - random = rand() % 6; - if (random == 0) - return SIL_NUM; - else if (random == 1) - return SLIP_NUM; - else if (random == 2) - return BAND_NUM; - else if (random == 3) - return KEV_NUM; - else if (random == 4) - return LASER_NUM; - else - return HELM_NUM; - } - } - - return 0; // Don't need a special -} - -// Locate weapons and items -int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter) -{ - qboolean found = false; - edict_t* items[32]; // Items we find - int item_nodes[32]; // Keep track of the nodes near those items - int item_count = 0; // Total items we found - int base = 1 + game.maxclients + BODY_QUEUE_SIZE; - edict_t* ent = g_edicts + base; - - vec3_t mins = { -16, -16, -24 }; - vec3_t maxs = { 16, 16, 32 }; - vec3_t bmins = { 0,0,0 }; - vec3_t bmaxs = { 0,0,0 }; - - for (int i = base; i < globals.num_edicts; i++, ent++) - { - if (ent->inuse == false) continue; - if (!ent->classname) continue; - - /* - if (strcmp(ent->classname, "info_player_deathmatch") == 0) // Skip spawn points - continue; - - if (strcmp(ent->classname, "dead_body") == 0) // Skip dead bodies - continue; - - if (ent->solid == SOLID_NOT) - continue; - - if (strcmp(ent->classname, "medkit") == 0) - { - } - - //if (ent->spawnflags != 0) continue; - //if (ent->flags != 0) continue; - - //Com_Printf("%s %s item %s [%f %f %f] - sf[0x%x] - f[0x%x]\n", __func__, self->client->pers.netname, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2], ent->spawnflags, ent->flags); - - // Ent->Spawnflags - // DROPPED_ITEM 0x00010000 Item dropped by player - // DROPPED_PLAYER_ITEM 0x00020000 Item dropped when dead - // ITEM_TARGETS_USED 0x00040000 Item picked up from spawn spot - // - // Ent->Flags - // FL_RESPAWN 0x80000000 used for item respawning - //if (ent->spawnflags == ITEM_TARGETS_USED && ent->flags == FL_RESPAWN) // Skip weapon spawn spot when weapon has been picked up - // continue; - */ - - switch (ent->typeNum) { - case MK23_NUM: - case MP5_NUM: - case M4_NUM: - case M3_NUM: - case HC_NUM: - case SNIPER_NUM: - case DUAL_NUM: - case KNIFE_NUM: - case GRENADE_NUM: - case SIL_NUM: - case SLIP_NUM: - case BAND_NUM: - case KEV_NUM: - case LASER_NUM: - case HELM_NUM: - case MK23_ANUM: - case MP5_ANUM: - case M4_ANUM: - case SHELL_ANUM: - case SNIPER_ANUM: - - // If the item is in our list - found = false; - for (int j = 0; j < items_counter; j++) - { - if (ent->typeNum == items_to_get[j]) - found = true; - } - - // Only look at items we want - if (found && item_count < 32) - { - for (int j = 0; j < numnodes; j++) - { - VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world - VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world - - // If ent is touching a node - //if (BOTLIB_BoxIntersection(ent->absmin, ent->absmax, nodes[i].absmin, nodes[i].absmax)) - if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax)) - { - //trace_t tr = gi.trace(nodes[j].origin, mins, maxs, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); - trace_t tr = gi.trace(nodes[j].origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); - if (tr.fraction == 1.0 && BOTLIB_CanGotoNode(self, nodes[j].nodenum, false)) // Check if bot can nav to item - { - items[item_count] = ent; - item_nodes[item_count] = nodes[j].nodenum; - item_count++; - break; - } - else - { - //Com_Printf("%s %s could not visit node [%d] for item %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); - //Com_Printf("%s %s could not visit node [%d] for item %s dist[%f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, VectorDistance(self->s.origin, ent->s.origin)); - } - } - } - } - break; - - default: - break; - } - } - - //Com_Printf("%s %s -----\n\n", __func__, self->client->pers.netname); - - // Find the closest item - float closest = 999999; - float dist = 0; - int node = INVALID; - ent = NULL; - for (int i = 0; i < item_count; i++) - { - dist = VectorDistance(self->s.origin, items[i]->s.origin); - if (dist < closest) - { - closest = dist; - ent = items[i]; - node = item_nodes[i]; - - } - } - if (node != INVALID && ent) - { - self->bot.get_item = ent; - //Com_Printf("%s %s [%d] %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[node].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); - //Com_Printf("%s %s goal item [%s] at node [%d] dist[%f]\n", __func__, self->client->pers.netname, ent->classname, nodes[node].nodenum, VectorDistance(self->s.origin, ent->s.origin)); - return nodes[node].nodenum; - } - else - { - self->bot.get_item = NULL; - return INVALID; - } - - return INVALID; -} - -// Does bot need weapons, items, ammo? If so find ground based items that are close. -// Returns the node of the item and sets the item for the bot to walk toward when its nearby -int BOTLIB_GetEquipment(edict_t* self) -{ - int primary_weapon = 0; - int secondary_weapon = 0; - qboolean need_weapon = BOTLIB_Need_Weapons(self, &primary_weapon, &secondary_weapon); - int ammo_type = BOTLIB_Need_Ammo(self, &primary_weapon, &secondary_weapon); - int special = BOTLIB_Need_Special(self, &primary_weapon, &secondary_weapon); - qboolean need_grenades = BOTLIB_Need_Grenades(self); - qboolean need_dual_mk23 = BOTLIB_Need_Dual_MK23(self); - qboolean need_knives = BOTLIB_Need_Knives(self); - //Com_Printf("%s wep[%d][%d][%d] gren[%d] knife[%d] dual[%d] ammo[%d] special[%d]\n", __func__, need_weapon, primary_weapon, secondary_weapon, need_grenades, need_knives, need_dual_mk23, ammo_type, special); - - int items_to_get[32]; - int items_counter = 0; - - // Acceptable primary weapon choices - if (primary_weapon == 0) - { - items_to_get[0] = MP5_NUM; - items_to_get[1] = M4_NUM; - items_to_get[2] = M3_NUM; - items_to_get[3] = HC_NUM; - items_to_get[4] = SNIPER_NUM; - items_counter += 5; - } - // Acceptable secondary weapon choices (if bandolier equipped) - qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; - if (band && primary_weapon && secondary_weapon == 0) - { - // Only add a weapon if not already in our inventory - if (primary_weapon != MP5_NUM) - { - items_to_get[items_counter] = MP5_NUM; - items_counter++; - } - if (primary_weapon != M4_NUM) - { - items_to_get[items_counter] = M4_NUM; - items_counter++; - } - if (primary_weapon != M3_NUM) - { - items_to_get[items_counter] = M3_NUM; - items_counter++; - } - if (primary_weapon != HC_NUM) - { - items_to_get[items_counter] = HC_NUM; - items_counter++; - } - if (primary_weapon != SNIPER_NUM) - { - items_to_get[items_counter] = SNIPER_NUM; - items_counter++; - } - } - if (ammo_type) - { - items_to_get[items_counter] = ammo_type; - items_counter++; - - // Because MK23 ammo can come from clips or pistols, try look for both - if (ammo_type == MK23_ANUM)// && BOTLIB_CanTouchItem(self, DUAL_NUM)) - { - items_to_get[items_counter] = DUAL_NUM; - items_counter++; - } - if (ammo_type == DUAL_NUM)// && BOTLIB_CanTouchItem(self, MK23_ANUM)) - { - items_to_get[items_counter] = MK23_ANUM; - items_counter++; - } - - } - if (special) - { - items_to_get[items_counter] = special; - items_counter++; - } - if (need_grenades) - { - items_to_get[items_counter] = GRENADE_NUM; - items_counter++; - } - if (need_dual_mk23) - { - items_to_get[items_counter] = DUAL_NUM; - items_counter++; - } - - // Debug output - if (0 && items_counter) - { - Com_Printf("%s items_to_get [", __func__); - for (int i = 0; i < items_counter; i++) - { - gitem_t* item = GET_ITEM(items_to_get[i]); - Com_Printf(" %s ", item->classname); - } - Com_Printf("]\n"); - } - - return BOTLIB_LocateFloorItem(self, items_to_get, items_counter); -} -//rekkie -- collecting weapons, items, ammo -- e +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + + + +// Returns how much ammunition is loaded/chambered in a weapon -- this is *NOT* spare ammo +int BOTLIB_CheckWeaponLoadedAmmo(edict_t* self, int item_num) +{ + switch (item_num) + { + case MK23_NUM: + return self->client->mk23_rds; + case DUAL_NUM: + return self->client->dual_rds; + case MP5_NUM: + return self->client->mp5_rds; + case M4_NUM: + return self->client->m4_rds; + case M3_NUM: + return self->client->shot_rds; + case HC_NUM: + return self->client->cannon_rds; + case SNIPER_NUM: + return self->client->sniper_rds; + case KNIFE_NUM: + case GRENADE_NUM: + return self->client->inventory[ITEM_INDEX(FindItemByNum(item_num))]; + default: + return 0; + } + + return 0; +} + +// +qboolean BOTLIB_ChangeSpecialWeapon(edict_t* self, int item_num) +{ + int ammo_index; + gitem_t* ammo_item; + qboolean loaded = true; + qboolean clips = true; + + gitem_t* item = FindItemByNum(item_num); + + // Check if weapon is in inventory + if (self->client->inventory[ITEM_INDEX(item)] == 0) + return false; + + // Do we have spare ammo + if (item->ammo) + { + ammo_item = FindItem(item->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + + // Check loaded ammo + if (BOTLIB_CheckWeaponLoadedAmmo(self, item_num) == 0) + loaded = false; + + + // see if we're already using it + if (item == self->client->weapon) + { + if (!loaded && !clips) // Drop weapon if we've got no ammo + { + DropSpecialWeapon(self); + return false; + } + else if (!loaded) // Reload if we've got spare ammo + { + BOTLIB_Reload(self); + //Cmd_New_Reload_f(self); + return true; + } + else + return true; // Nothing to do, weapon is good to go + } + + // No ammo + if (!loaded && !clips) + { + DropSpecialWeapon(self); + return false; + } + + // Change to this weapon + self->client->newweapon = item; + ChangeWeapon(self); + return true; +} + +// Bot checks its weapons: reload, drop empty, or change weapon... before encountering an enemy +void BOTLIB_ReadyWeapon(edict_t* self) +{ + // Limit how often this is called + if (self->bot.last_weapon_change_time > level.framenum) + return; + self->bot.last_weapon_change_time = level.framenum + 6 * HZ; + //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); + + /* + // Random chance to pull out grenades + if ((rand() % 20) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) //ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) + { + self->client->pers.grenade_mode = 2; + return; + } + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + return; + + + // Random chance to just use knives + if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return; + } + if (self->client->weapon == FindItemByNum(KNIFE_NUM)) + return; + */ + + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return; + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; + } + + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return; +} + +// Choose the best weapon for the situation +qboolean BOTLIB_ChooseWeapon(edict_t* self) +{ + // Limit how often this is called + if (self->bot.last_weapon_change_time > level.framenum) + return true; + self->bot.last_weapon_change_time = level.framenum + 6 * HZ; + //Com_Printf("%s %s [%d]\n", __func__, self->client->pers.netname, level.framenum); + + // Don't change if weaapon is busy + if (self->client->weaponstate == WEAPON_DROPPING || self->client->weaponstate == WEAPON_BUSY) + return true; + + + // If not firing weapon + if (self->client->weaponstate == WEAPON_READY && self->client->weapon != FindItemByNum(HC_NUM) && self->client->weapon != FindItemByNum(M3_NUM)) + { + // Random chance to pull out grenades + if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(GRENADE_NUM) && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + + // Random chance to just use knives + //if ((rand() % 20) == 0 && (INV_AMMO(self, KNIFE_NUM) >= 2) && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + if (self->enemy && self->bot.enemy_dist <= 512) // More included to use knives when enemy is closer + { + if ((rand() % 2) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + } + else // Less included to use knives when enemy is far + { + if ((rand() % 5) == 0 && self->client->weapon != FindItemByNum(KNIFE_NUM) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + } + } + + + if (self->enemy && self->bot.enemy_dist <= 1200) + { + // Let bots use the grenades if already using + if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, GRENADE_NUM)) + { + if (self->bot.enemy_dist > 800) + self->client->pers.grenade_mode = 2; + else if (self->bot.enemy_dist > 400) + self->client->pers.grenade_mode = 1; + else + self->client->pers.grenade_mode = 0; + return true; + } + // Let bots use the knives if already using + if (self->client->weapon == FindItemByNum(KNIFE_NUM) && BOTLIB_CheckWeaponLoadedAmmo(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + // Let bots use the pistol if already using + if (self->client->weapon == FindItemByNum(MK23_NUM) && self->client->mk23_rds == self->client->mk23_max) + { + return true; + } + } + + /* + // Friendly fire after round should be fought with honor. + if( team_round_countdown && ff_afterround->value ) + { + if( ACEIT_ChangeWeapon(self,FindItem(KNIFE_NAME)) ) + return true; + if( ACEIT_ChangeHCSpecialWeapon(self,FindItem(HC_NAME)) ) + return true; + if( ACEIT_ChangeWeapon(self,FindItem(GRENADE_NAME)) ) + { + self->client->pers.grenade_mode = ( self->bot.enemy_dist > 500) ? 1 : 0; + return true; + } + } + */ + + /* + if (self->client->weapon == FindItemByNum(GRENADE_NUM)) + { + if (self->client->weaponstate == WEAPON_ACTIVATING || self->client->weaponstate == WEAPON_FIRING) + return true; + } + */ + + // Force bots use grenades only + /* + if (ACEIT_ChangeWeapon(self, FindItemByNum(GRENADE_NUM))) + { + self->client->pers.grenade_mode = 2; + return true; + } + */ + + if (self->enemy == NULL) + { + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + + return true; + } + + + // Extreme range + if (self->bot.enemy_dist > 1300) + { + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + } + + // Long range + if (self->bot.enemy_dist > 1000) + { + /* + // Random chance to pull out grenades + if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 6) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + + // Allow bots that picked throwing knives to use them at a distance + if (INV_AMMO(self, KNIFE_NUM) >= 2 && self->client->pers.chosenItem == FindItemByNum(KNIFE_NUM) && ACEIT_ChangeWeapon(self, FindItem(KNIFE_NAME))) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (INV_AMMO(self, SIL_NUM) && BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && self->client->bleeding == 0 + && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + // Longer range + if (self->bot.enemy_dist > 700) + { + /* + // Random chance to pull out grenades + if ((rand() % 2) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 3) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + if (INV_AMMO(self, SIL_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + } + + // Long range + if (self->bot.enemy_dist > 500) + { + /* + // Random chance to pull out grenades + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (INV_AMMO(self, LASER_NUM)) + { + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + + // If bot has a HC and healthy: + // keep HC equipped so the bot can try get closer to its target, instead of pulling out the pistol + if (self->client->cannon_rds && self->client->weapon == FindItem(HC_NAME) + && self->client->leg_damage == 0 && BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) + { + return true; + } + + if ((INV_AMMO(self, KNIFE_NUM) >= 2) && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + } + + // Medium range + if (self->bot.enemy_dist) + { + if (BOTLIB_ChangeSpecialWeapon(self, HC_NUM)) return true; + + /* + // Random chance to pull out grenades + if ((rand() % 10) == 0 && BOTLIB_ChangeSpecialWeapon(self, GRENADE_NUM)) + { + self->client->pers.grenade_mode = 2; + return true; + } + // Random chance to just use knives + if ((rand() % 5) == 0 && BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) + { + self->client->pers.knife_mode = 1; // Throwing knives + return true; + } + */ + + if (BOTLIB_ChangeSpecialWeapon(self, M3_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, M4_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MP5_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, SNIPER_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, KNIFE_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, DUAL_NUM)) return true; + if (BOTLIB_ChangeSpecialWeapon(self, MK23_NUM)) return true; + + // Should punch as a last resort? + } + + // We have no weapon available for use. + //if (debug_mode) gi.bprintf(PRINT_HIGH, "%s: No weapon available...\n", self->client->pers.netname); + return false; +} + +void BOTLIB_Reload(edict_t* self) +{ + int ammo_index, rounds = 0; + gitem_t* ammo_item; + qboolean loaded = true; + qboolean clips = true; + qboolean using_special = false; // If bot is using high powered weapon + + // Limit how often this is called + if (self->bot.last_weapon_reload_time > level.framenum) + return; + self->bot.last_weapon_reload_time = level.framenum + 2 * HZ; + + if (self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME) + || self->client->weapon == FindItem(MP5_NAME) || self->client->weapon == FindItem(M4_NAME) + || self->client->weapon == FindItem(M3_NAME) || self->client->weapon == FindItem(HC_NAME) + || self->client->weapon == FindItem(SNIPER_NAME)) + { + // Do we have spare ammo + if (self->client->weapon->ammo) + { + ammo_item = FindItem(self->client->weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < 1) + clips = false; + else + clips = true; + } + + // Check ammo + if (self->client->weapon == FindItem(MK23_NAME)) + { + rounds = self->client->mk23_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + rounds = 0; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(DUAL_NAME)) + { + rounds = self->client->dual_rds; + if (self->client->weaponstate == WEAPON_END_MAG) + rounds = 0; + if (rounds < 6) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(MP5_NAME)) + { + using_special = true; + rounds = self->client->mp5_rds; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(M4_NAME)) + { + using_special = true; + rounds = self->client->m4_rds; + if (rounds < 5) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(M3_NAME)) + { + //using_special = true; + rounds = self->client->shot_rds; + if (rounds < self->client->shot_max) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + else if (self->client->weapon == FindItem(HC_NAME)) + { + //using_special = true; + rounds = self->client->cannon_rds; + if (hc_single->value && self->client->pers.hc_mode) // Single barrel fire mode + { + if (rounds < 1) + loaded = false; + } + else if (rounds < 2) // Double barrel fire mode + loaded = false; + } + else if (self->client->weapon == FindItem(SNIPER_NAME)) + { + using_special = true; + rounds = self->client->sniper_rds; + if (rounds < self->client->sniper_max) + loaded = false; + if (self->bot.see_enemies && rounds) // Don't reload until empty if facing enemy + loaded = true; + } + + //Com_Printf("%s %s inv_ammo[%d] rnds[%d] loaded[%d] clips[%d]\n", __func__, self->client->pers.netname, self->client->inventory[ammo_index], rounds, loaded, clips); + + // No ammo - drop weapon + if (!loaded && !clips) + { + DropSpecialWeapon(self); + } + // Reload + else if (!loaded) + { + // If using high powered weapon that is out of ammo and bot has a full pistol, switch to pistol + if (rounds == 0 && using_special && self->bot.enemy_dist < 512 && self->client->mk23_rds == self->client->mk23_max)// && self->bot.see_enemies) + { + // Try switch to pistol instead of reloading current weapon + BOTLIB_ChangeSpecialWeapon(self, MK23_NUM); + return; + } + + self->client->reload_attempts += 2; // Reload twice in one attempt + + //Cmd_New_Reload_f(self); + } + } +} + +// If have sniper and not zoomed in, zoom in and return true. +// Returns false otherwise +qboolean BOTLIB_SniperZoom(edict_t* self) +{ + if (self->bot.last_sniper_zoom_time > level.framenum) + { + return true; + } + + if (self->client->weapon == FindItem(SNIPER_NAME) && (self->client->resp.sniper_mode != SNIPER_2X || self->client->desired_fov != SNIPER_FOV2)) + { + if (self->client->weaponstate != WEAPON_FIRING && self->client->weaponstate != WEAPON_BUSY && self->client->weaponstate != WEAPON_RELOADING + && !self->client->bandaging && !self->client->bandage_stopped) + { + //Com_Printf("%s %s sniper zoom X2 [%d]\n", __func__, self->client->pers.netname, level.framenum); + self->bot.last_sniper_zoom_time = level.framenum + 1 * HZ; + _SetSniper(self, 2); + return true; + } + } + + return false; +} + +//rekkie -- collecting weapons, items, ammo -- s + +// Checks if the item is on the map and can be picked up by the bot +// Instances where it cannot be picked up: it's been taken by another player, or it's just not on the map +qboolean BOTLIB_CanTouchItem(edict_t* self, int typeNum) +{ + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + if (ent->solid == SOLID_NOT) continue; + //Com_Printf("%s %s typeNum[%d] dist[%f]\n", __func__, ent->classname, ent->typeNum, VectorDistance(self->s.origin, ent->s.origin)); + if (ent->typeNum == typeNum) + return true; + } + + return false; +} + +// Do we need a primary weapon? +// Pass parameters: primary_weapon and secondary_weapon to get a return on the weapons the bot is carrying +// The secondary_weapon only returns if the bot is using a bandolier +qboolean BOTLIB_Need_Weapons(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + int weapon_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case MP5_NUM: + if (*primary_weapon == 0) + *primary_weapon = MP5_NUM; + else + *secondary_weapon = MP5_NUM; + weapon_count++; + break; + + case M4_NUM: + if (*primary_weapon == 0) + *primary_weapon = M4_NUM; + else + *secondary_weapon = M4_NUM; + weapon_count++; + break; + + case M3_NUM: + if (*primary_weapon == 0) + *primary_weapon = M3_NUM; + else + *secondary_weapon = M3_NUM; + weapon_count++; + break; + + case HC_NUM: + if (*primary_weapon == 0) + *primary_weapon = HC_NUM; + else + *secondary_weapon = HC_NUM; + weapon_count++; + break; + + case SNIPER_NUM: + if (*primary_weapon == 0) + *primary_weapon = SNIPER_NUM; + else + *secondary_weapon = SNIPER_NUM; + weapon_count++; + break; + } + + //Com_Printf("%s inventory[%d] = %d [%s]\n", __func__, i, self->client->inventory[i], itemlist[i].classname); + } + } + + if (band && weapon_count < 2) // Bandolier: desire two primary weapons + return true; + if (weapon_count < 1) // Desire at least one primary weapon + return true; + + return false; // Don't need a primary +} + +// Do we need a grendes? +qboolean BOTLIB_Need_Grenades(edict_t* self) +{ + int grens_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case GRENADE_NUM: + grens_count = self->client->inventory[i]; + break; + } + } + } + + if (band && grens_count < 4) // Bandolier: desire up to four greandes + return true; + if (grens_count < 1) // Desire at least one grenade + return true; + + return false; // Don't need grenades +} + +// Do we need a knives? +qboolean BOTLIB_Need_Knives(edict_t* self) +{ + int knives_count = 0; + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case KNIFE_NUM: + knives_count = self->client->inventory[i]; + break; + } + } + } + + if (band && knives_count < 20) // Bandolier: desire up to 20 knives + return true; + if (knives_count < 4) // Desire at least a few knives + return true; + + return false; // Don't need knives +} + +// Do we need a dual pistols? +qboolean BOTLIB_Need_Dual_MK23(edict_t* self) +{ + // Figure out what weapons we have + for (int i = 0; i < game.num_items; i++) + { + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case DUAL_NUM: + return false; // Don't need another pistol + break; + } + } + } + + return true; // Desire a pair of mk23 pistols +} + +// Do we need ammo for weapons we carry: mk23, primary, or secondary (if any) +// Pass parameters input: primary_weapon and secondary_weapon +// Returns ammo type +int BOTLIB_Need_Ammo(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + gitem_t* weapon; + gitem_t* ammo_item; + int ammo_index = 0; + + if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) + { + weapon = FindItem(MP5_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_mp5mags) + { + // Check there's ammo for this weapon ready for pickup on the map + if (BOTLIB_CanTouchItem(self, MP5_ANUM)) + return MP5_ANUM; + } + } + if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) + { + weapon = FindItem(M4_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_m4mags) + { + if (BOTLIB_CanTouchItem(self, M4_ANUM)) + return M4_ANUM; + } + } + if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) + { + weapon = FindItem(M3_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_shells) + { + if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) + return SHELL_ANUM; + } + } + if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) + { + weapon = FindItem(HC_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_shells) + { + if (BOTLIB_CanTouchItem(self, SHELL_ANUM)) + return SHELL_ANUM; + } + } + if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) + { + weapon = FindItem(SNIPER_NAME); + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_sniper_rnds) + { + if (BOTLIB_CanTouchItem(self, SNIPER_ANUM)) + return SNIPER_ANUM; + } + } + + // Dual MK23 + weapon = FindItem(DUAL_NAME); + if (weapon) + { + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_pistolmags) + return DUAL_NUM; + } + + // MK23 + weapon = FindItem(MK23_NAME); + if (weapon) + { + ammo_item = FindItem(weapon->ammo); + ammo_index = ITEM_INDEX(ammo_item); + if (self->client->inventory[ammo_index] < self->client->max_pistolmags) + return MK23_ANUM; + } + + return 0; +} + +// Do we need a primary weapon? +// Pass parameters input: primary_weapon and secondary_weapon +// Returns a special best fit for the weapons used +int BOTLIB_Need_Special(edict_t* self, int* primary_weapon, int* secondary_weapon) +{ + int special = 0; + int random = 0; + + // Figure out what special items we have + for (int i = 0; i < game.num_items; i++) + { + if (special) + break; + + if (self->client->inventory[i] > 0) + { + switch (itemlist[i].typeNum) + { + case SIL_NUM: + special = SIL_NUM; + break; + case SLIP_NUM: + special = SLIP_NUM; + break; + case BAND_NUM: + special = BAND_NUM; + break; + case KEV_NUM: + special = KEV_NUM; + break; + case LASER_NUM: + special = LASER_NUM; + break; + case HELM_NUM: + special = HELM_NUM; + break; + } + } + } + + // No special items, so lets pick one based on our weapons in inventory + if (special == 0) + { + if (*primary_weapon == MP5_NUM || *secondary_weapon == MP5_NUM) + { + random = rand() % 5; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else if (random == 3) + return LASER_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == M4_NUM || *secondary_weapon == M4_NUM) + { + random = rand() % 5; + if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else if (random == 3) + return LASER_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == M3_NUM || *secondary_weapon == M3_NUM) + { + random = rand() % 4; + if (random == 0) + return SLIP_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == HC_NUM || *secondary_weapon == HC_NUM) + { + random = rand() % 4; + if (random == 0) + return SLIP_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else if (*primary_weapon == SNIPER_NUM || *secondary_weapon == SNIPER_NUM) + { + random = rand() % 4; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return BAND_NUM; + else if (random == 2) + return KEV_NUM; + else + return HELM_NUM; + } + else // Just get a random item + { + random = rand() % 6; + if (random == 0) + return SIL_NUM; + else if (random == 1) + return SLIP_NUM; + else if (random == 2) + return BAND_NUM; + else if (random == 3) + return KEV_NUM; + else if (random == 4) + return LASER_NUM; + else + return HELM_NUM; + } + } + + return 0; // Don't need a special +} + +// Locate weapons and items +int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter) +{ + qboolean found = false; + edict_t* items[32]; // Items we find + int item_nodes[32]; // Keep track of the nodes near those items + int item_count = 0; // Total items we found + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t bmins = { 0,0,0 }; + vec3_t bmaxs = { 0,0,0 }; + + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + + /* + if (strcmp(ent->classname, "info_player_deathmatch") == 0) // Skip spawn points + continue; + + if (strcmp(ent->classname, "dead_body") == 0) // Skip dead bodies + continue; + + if (ent->solid == SOLID_NOT) + continue; + + if (strcmp(ent->classname, "medkit") == 0) + { + } + + //if (ent->spawnflags != 0) continue; + //if (ent->flags != 0) continue; + + //Com_Printf("%s %s item %s [%f %f %f] - sf[0x%x] - f[0x%x]\n", __func__, self->client->pers.netname, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2], ent->spawnflags, ent->flags); + + // Ent->Spawnflags + // DROPPED_ITEM 0x00010000 Item dropped by player + // DROPPED_PLAYER_ITEM 0x00020000 Item dropped when dead + // ITEM_TARGETS_USED 0x00040000 Item picked up from spawn spot + // + // Ent->Flags + // FL_RESPAWN 0x80000000 used for item respawning + //if (ent->spawnflags == ITEM_TARGETS_USED && ent->flags == FL_RESPAWN) // Skip weapon spawn spot when weapon has been picked up + // continue; + */ + + switch (ent->typeNum) { + case MK23_NUM: + case MP5_NUM: + case M4_NUM: + case M3_NUM: + case HC_NUM: + case SNIPER_NUM: + case DUAL_NUM: + case KNIFE_NUM: + case GRENADE_NUM: + case SIL_NUM: + case SLIP_NUM: + case BAND_NUM: + case KEV_NUM: + case LASER_NUM: + case HELM_NUM: + case MK23_ANUM: + case MP5_ANUM: + case M4_ANUM: + case SHELL_ANUM: + case SNIPER_ANUM: + + // If the item is in our list + found = false; + for (int j = 0; j < items_counter; j++) + { + if (ent->typeNum == items_to_get[j]) + found = true; + } + + // Only look at items we want + if (found && item_count < 32) + { + for (int j = 0; j < numnodes; j++) + { + VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world + VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world + + // If ent is touching a node + //if (BOTLIB_BoxIntersection(ent->absmin, ent->absmax, nodes[i].absmin, nodes[i].absmax)) + if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax)) + { + //trace_t tr = gi.trace(nodes[j].origin, mins, maxs, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); + trace_t tr = gi.trace(nodes[j].origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0 && BOTLIB_CanGotoNode(self, nodes[j].nodenum, false)) // Check if bot can nav to item + { + items[item_count] = ent; + item_nodes[item_count] = nodes[j].nodenum; + item_count++; + break; + } + else + { + //Com_Printf("%s %s could not visit node [%d] for item %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + //Com_Printf("%s %s could not visit node [%d] for item %s dist[%f]\n", __func__, self->client->pers.netname, nodes[j].nodenum, ent->classname, VectorDistance(self->s.origin, ent->s.origin)); + } + } + } + } + break; + + default: + break; + } + } + + //Com_Printf("%s %s -----\n\n", __func__, self->client->pers.netname); + + // Find the closest item + float closest = 999999; + float dist = 0; + int node = INVALID; + ent = NULL; + for (int i = 0; i < item_count; i++) + { + dist = VectorDistance(self->s.origin, items[i]->s.origin); + if (dist < closest) + { + closest = dist; + ent = items[i]; + node = item_nodes[i]; + + } + } + if (node != INVALID && ent) + { + self->bot.get_item = ent; + //Com_Printf("%s %s [%d] %s [%f %f %f]\n", __func__, self->client->pers.netname, nodes[node].nodenum, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2]); + //Com_Printf("%s %s goal item [%s] at node [%d] dist[%f]\n", __func__, self->client->pers.netname, ent->classname, nodes[node].nodenum, VectorDistance(self->s.origin, ent->s.origin)); + return nodes[node].nodenum; + } + else + { + self->bot.get_item = NULL; + return INVALID; + } + + return INVALID; +} + +// Does bot need weapons, items, ammo? If so find ground based items that are close. +// Returns the node of the item and sets the item for the bot to walk toward when its nearby +int BOTLIB_GetEquipment(edict_t* self) +{ + int primary_weapon = 0; + int secondary_weapon = 0; + qboolean need_weapon = BOTLIB_Need_Weapons(self, &primary_weapon, &secondary_weapon); + int ammo_type = BOTLIB_Need_Ammo(self, &primary_weapon, &secondary_weapon); + int special = BOTLIB_Need_Special(self, &primary_weapon, &secondary_weapon); + qboolean need_grenades = BOTLIB_Need_Grenades(self); + qboolean need_dual_mk23 = BOTLIB_Need_Dual_MK23(self); + qboolean need_knives = BOTLIB_Need_Knives(self); + //Com_Printf("%s wep[%d][%d][%d] gren[%d] knife[%d] dual[%d] ammo[%d] special[%d]\n", __func__, need_weapon, primary_weapon, secondary_weapon, need_grenades, need_knives, need_dual_mk23, ammo_type, special); + + int items_to_get[32]; + int items_counter = 0; + + // Acceptable primary weapon choices + if (primary_weapon == 0) + { + items_to_get[0] = MP5_NUM; + items_to_get[1] = M4_NUM; + items_to_get[2] = M3_NUM; + items_to_get[3] = HC_NUM; + items_to_get[4] = SNIPER_NUM; + items_counter += 5; + } + // Acceptable secondary weapon choices (if bandolier equipped) + qboolean band = INV_AMMO(self, BAND_NUM) ? 1 : 0; + if (band && primary_weapon && secondary_weapon == 0) + { + // Only add a weapon if not already in our inventory + if (primary_weapon != MP5_NUM) + { + items_to_get[items_counter] = MP5_NUM; + items_counter++; + } + if (primary_weapon != M4_NUM) + { + items_to_get[items_counter] = M4_NUM; + items_counter++; + } + if (primary_weapon != M3_NUM) + { + items_to_get[items_counter] = M3_NUM; + items_counter++; + } + if (primary_weapon != HC_NUM) + { + items_to_get[items_counter] = HC_NUM; + items_counter++; + } + if (primary_weapon != SNIPER_NUM) + { + items_to_get[items_counter] = SNIPER_NUM; + items_counter++; + } + } + if (ammo_type) + { + items_to_get[items_counter] = ammo_type; + items_counter++; + + // Because MK23 ammo can come from clips or pistols, try look for both + if (ammo_type == MK23_ANUM)// && BOTLIB_CanTouchItem(self, DUAL_NUM)) + { + items_to_get[items_counter] = DUAL_NUM; + items_counter++; + } + if (ammo_type == DUAL_NUM)// && BOTLIB_CanTouchItem(self, MK23_ANUM)) + { + items_to_get[items_counter] = MK23_ANUM; + items_counter++; + } + + } + if (special) + { + items_to_get[items_counter] = special; + items_counter++; + } + if (need_grenades) + { + items_to_get[items_counter] = GRENADE_NUM; + items_counter++; + } + if (need_dual_mk23) + { + items_to_get[items_counter] = DUAL_NUM; + items_counter++; + } + + // Debug output + if (0 && items_counter) + { + Com_Printf("%s items_to_get [", __func__); + for (int i = 0; i < items_counter; i++) + { + gitem_t* item = GET_ITEM(items_to_get[i]); + Com_Printf(" %s ", item->classname); + } + Com_Printf("]\n"); + } + + return BOTLIB_LocateFloorItem(self, items_to_get, items_counter); +} +//rekkie -- collecting weapons, items, ammo -- e From 0b2ef475deb58264555760f273771b17b6143710 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 16 May 2024 22:08:17 +0300 Subject: [PATCH 193/974] Add EntityVisibleToClient() game API. Since the server now walks entity list twice, first to build list of visible entities and then to actually send them, using CustomizeEntity() to determine entity visibility became awkward. Add new EntityVisibleToClient() API for this purpose, rename CustomizeEntity() to CustomizeEntityToClient() and make it return a boolean value. Since this is an API break, don't call CustomizeEntityToClient() unless game advertises updated API version. --- inc/shared/game.h | 14 ++++++-------- src/server/entities.c | 13 ++++++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index e660b2c17..23af00081 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -280,11 +280,14 @@ typedef game_export_t *(*game_entry_t)(game_import_t *); * API version history: * 1 - Initial release. * 2 - Added CustomizeEntity(). + * 3 - Added EntityVisibleToClient(), renamed CustomizeEntity() to + * CustomizeEntityToClient() and changed the meaning of return value. */ #define GAME_API_VERSION_EX_MINIMUM 1 #define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 -#define GAME_API_VERSION_EX 2 +#define GAME_API_VERSION_EX_ENTITY_VISIBLE 3 +#define GAME_API_VERSION_EX 3 typedef enum { VIS_PVS = 0, @@ -292,12 +295,6 @@ typedef enum { VIS_NOAREAS = 2 // can be OR'ed with one of above } vis_t; -typedef enum { - CE_SKIP, // don't send this entity to client - CE_PASS, // pass unmodified - CE_CUSTOMIZE // customize (game must fill `temp') -} customize_entity_result_t; - typedef struct { entity_state_t s; #if USE_PROTOCOL_EXTENSIONS @@ -326,7 +323,8 @@ typedef struct { qboolean (*CanSave)(void); void (*PrepFrame)(void); void (*RestartFilesystem)(void); // called when fs_restart is issued - customize_entity_result_t (*CustomizeEntity)(edict_t *client, edict_t *ent, customize_entity_t *temp); + qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); + qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); } game_export_ex_t; typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); diff --git a/src/server/entities.c b/src/server/entities.c index 715a1dcaf..7cca3882e 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -555,7 +555,8 @@ void SV_BuildClientFrame(client_t *client) int max_packet_entities; edict_t *edicts[MAX_EDICTS]; int num_edicts; - customize_entity_result_t (*customize)(edict_t *, edict_t *, customize_entity_t *) = NULL; + qboolean (*visible)(edict_t *, edict_t *) = NULL; + qboolean (*customize)(edict_t *, edict_t *, customize_entity_t *) = NULL; customize_entity_t temp; clent = client->edict; @@ -610,8 +611,10 @@ void SV_BuildClientFrame(client_t *client) sv_max_packet_entities->integer > 0 ? sv_max_packet_entities->integer : client->csr->extended ? MAX_PACKET_ENTITIES : MAX_PACKET_ENTITIES_OLD; - if (gex && gex->apiversion >= GAME_API_VERSION_EX_CUSTOMIZE_ENTITY) - customize = gex->CustomizeEntity; + if (gex && gex->apiversion >= GAME_API_VERSION_EX_ENTITY_VISIBLE) { + visible = gex->EntityVisibleToClient; + customize = gex->CustomizeEntityToClient; + } CM_FatPVS(client->cm, clientpvs, org); BSP_ClusterVis(client->cm->cache, clientphs, clientcluster, DVIS_PHS); @@ -678,7 +681,7 @@ void SV_BuildClientFrame(client_t *client) SV_CheckEntityNumber(ent, e); // optionally skip it - if (customize && customize(clent, ent, NULL) == CE_SKIP) + if (visible && !visible(clent, ent)) continue; edicts[num_edicts++] = ent; @@ -706,7 +709,7 @@ void SV_BuildClientFrame(client_t *client) state = &svs.entities[svs.next_entity % svs.num_entities]; // optionally customize it - if (customize && customize(clent, ent, &temp) == CE_CUSTOMIZE) { + if (customize && customize(clent, ent, &temp)) { Q_assert(temp.s.number == e); MSG_PackEntity(state, &temp.s, ENT_EXTENSION(client->csr, &temp)); } else { From 4e8d6458668aadacfade200ddcd57e8239702490 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 16 May 2024 15:57:20 -0400 Subject: [PATCH 194/974] replaced slint with botlib_sll_t in botlib_sll_nodes_t --- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_nodes.c | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 10eefb319..d26cb2094 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -374,7 +374,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando // Node data for our linked list typedef struct { - struct slint* next; // Next node + struct botlib_sll_t* next; // Next node int node; // Node number float cost; // Cost from previous node to this node int parent_node; //rekkie -- the node we came from diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 12c8149ec..ee5ae3f4b 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -1697,7 +1697,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma if (nodes[from].type == NODE_LADDER_UP && nodes[to].type != NODE_LADDER_DOWN) { //if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 128 || xyz_distance < 48) - if (abs(origin[2] < target[2]) > 8 || xyz_distance > 128 || xyz_distance < 48) + if (fabsf(origin[2] < target[2]) > 8 || xyz_distance > 128 || xyz_distance < 48) return INVALID; else return NODE_MOVE; @@ -1707,14 +1707,14 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma // Bottom ladder node -> non-ladder node, test if within z height range +/- if (nodes[from].type == NODE_LADDER_DOWN && nodes[to].type != NODE_LADDER_UP) { - if (abs(origin[2] - target[2]) > STEPSIZE || xyz_distance > 96) + if (fabsf(origin[2] - target[2]) > STEPSIZE || xyz_distance > 96) return INVALID; } // Non-ladder node -> top ladder node - test if within z height range +/- if (nodes[from].type != NODE_LADDER_DOWN && nodes[to].type == NODE_LADDER_UP) { - if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) + if (fabsf(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) return INVALID; } // Non-ladder node -> bottom ladder node - test if within z height range +/- @@ -1722,7 +1722,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma { //if (abs(origin[2] - target[2]) > NODE_Z_HEIGHT || xyz_distance > 96) // return INVALID; - if (abs(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT && xyz_distance < 96) + if (fabsf(origin[2] - target[2]) <= NODE_MAX_JUMP_HEIGHT && xyz_distance < 96) return NODE_JUMPPAD; else return INVALID; @@ -1792,7 +1792,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma if (nodes[from].type == NODE_STEP || nodes[to].type == NODE_STEP) { // Getting off step to a landing node - if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_MOVE && abs(origin[2] - target[2]) <= NODE_Z_HEIGHT_PLUS_STEPSIZE && xyz_distance <= 80) + if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_MOVE && fabsf(origin[2] - target[2]) <= NODE_Z_HEIGHT_PLUS_STEPSIZE && xyz_distance <= 80) { trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); if (tr_step.fraction == 1) @@ -1801,7 +1801,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma } } // Getting on step - else if (nodes[from].type == NODE_MOVE && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance <= 80) + else if (nodes[from].type == NODE_MOVE && nodes[to].type == NODE_STEP && fabsf(origin[2] - target[2]) <= STEPSIZE && xyz_distance <= 80) { trace_t tr_step = gi.trace(origin, tv(-15,-15,-18), tv(15,15,32), target, g_edicts, MASK_PLAYERSOLID); if (tr_step.fraction == 1) @@ -1810,7 +1810,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma } } // Step to Step - else if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_STEP && abs(origin[2] - target[2]) <= STEPSIZE && xyz_distance < 40) + else if (nodes[from].type == NODE_STEP && nodes[to].type == NODE_STEP && fabsf(origin[2] - target[2]) <= STEPSIZE && xyz_distance < 40) { //is_gap @@ -1969,7 +1969,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma return NODE_JUMPPAD; } // Roughly equal - else if (abs(origin[2] - target[2]) <= 8) // target node is +/- 8 + else if (fabsf(origin[2] - target[2]) <= 8) // target node is +/- 8 { float speed = VectorLength(velocity); if (speed < 600 && z_height < z_height_max + NODE_Z_HALF_HEIGHT && jump_height <= 750) @@ -2043,7 +2043,7 @@ int DC_Reachability(int from, int to, vec3_t origin, vec3_t target, vec3_t norma if (tr.fraction == 1) return NODE_MOVE; } - else if (abs(origin[2] - target[2]) <= STEPSIZE) // target node is +/- STEPSIZE + else if (fabsf(origin[2] - target[2]) <= STEPSIZE) // target node is +/- STEPSIZE { //tr = gi.trace(origin, tv(-16, -16, -(NODE_Z_HEIGHT - STEPSIZE)), tv(16, 16, 24), target, g_edicts, MASK_DEADSOLID); tr = gi.trace(origin, tv(-15, -15, -(NODE_Z_HEIGHT - STEPSIZE)), tv(15, 15, 24), target, g_edicts, MASK_DEADSOLID); @@ -4496,7 +4496,7 @@ void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid) } // Add nodes at item locations -void BOTLIB_AddItemNodes() +void BOTLIB_AddItemNodes(void) { // =========================== // Add nodes at item locations From a86590fa87c118544d80881f35d344b16b633696 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 12:08:38 -0400 Subject: [PATCH 195/974] Typo on gamearm->gamearm64 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe628f88d..f59762482 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -238,7 +238,7 @@ jobs: path: | builddir/q2pro builddir/q2proded - builddir/gamearm.so + builddir/gamearm64.so darwin: runs-on: macos-13 From caf6315a097e9439c820eb98b54019f308ee91f3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 12:10:26 -0400 Subject: [PATCH 196/974] Typo on Mac arm64 dylib name --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f59762482..6b4cb790d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -300,4 +300,4 @@ jobs: path: | builddir/q2pro builddir/q2proded - builddir/gamex86_64.dylib + builddir/gamearm64.dylib From 1f890475b3babf22f78f6af3ba5e59c7b513a706 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 14:04:03 -0400 Subject: [PATCH 197/974] Cleaned a few things up --- src/action/a_team.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 0f0e9744a..bc69aac4a 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2501,8 +2501,9 @@ qboolean CheckTimelimit( void ) // Otherwise, use_warnings should warn about 3 minutes and 1 minute left, but only if there aren't round ending warnings. // CTF and Espionage warnings - if( use_warnings->value && (ctf->value || ! roundtimelimit->value) && !(gameSettings & GS_DEATHMATCH) ) + if( use_warnings->value && (ctf->value || esp->value || ! roundtimelimit->value) && !(gameSettings & GS_DEATHMATCH) ) { + // CTF countdown when the match is ending. Espionage is round-based so there's no countdown. if( timewarning < 3 && ((ctf->value) && level.matchTime >= timelimit->value * 60 - 10 )) { gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("world/10_0.wav"), 1.0, ATTN_NONE, 0.0 ); @@ -2516,6 +2517,7 @@ qboolean CheckTimelimit( void ) if (esp->value){ if (esp_debug->value) gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); + // Warns the players that the round is ending in 1 minute EspAnnounceDetails(true); } } @@ -2539,11 +2541,6 @@ qboolean CheckTimelimit( void ) CenterPrintAll( "1 MINUTE LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/1_minute.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 2; - if (esp->value){ - if (esp_debug->value) - gi.dprintf("%s: level.matchTime = %f\n", __FUNCTION__, level.matchTime); - EspAnnounceDetails(true); - } } else if( timewarning < 1 && timelimit->value > 3 && level.matchTime >= ((timelimit->value - 3) * 60) && !during_countdown) { From 358fe3270e119ab3277cc64e00a501c874c66a2f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 14:08:00 -0400 Subject: [PATCH 198/974] Set during_countdown to qboolean rather than int --- src/action/a_team.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index bc69aac4a..56d982468 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -308,8 +308,8 @@ qboolean team_game_going = false; // is a team game going right now? qboolean team_round_going = false; // is an actual round of a team game going right now? +qboolean during_countdown = false; // This is set to 1 when the 10..9..8.. countdown is going on -int during_countdown = 0; // This is set to 1 when the 10..9..8.. countdown is going on int team_round_countdown = 0; // countdown variable for start of a round int rulecheckfrequency = 0; // accumulator variable for checking rules every 1.5 secs int lights_camera_action = 0; // countdown variable for "lights...camera...action!" @@ -2341,7 +2341,7 @@ void StartRound (void) static void StartLCA(void) { - during_countdown = 0; + during_countdown = false; if ((gameSettings & (GS_WEAPONCHOOSE|GS_ROUNDBASED))) CleanLevel(); @@ -2836,7 +2836,7 @@ int CheckTeamRules (void) { if (team_round_countdown == 101) { - during_countdown = 1; + during_countdown = true; gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); From 1cc8a83e5a402441dec5884055931c285329a633 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 17 May 2024 21:17:47 +0300 Subject: [PATCH 199/974] Clean up entity priority macros. --- src/server/entities.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 7cca3882e..60b1e59ab 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -495,24 +495,30 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) return (dist - SOUND_FULLVOLUME) * dist_mult > 1.0f; } -#define HI_PRIO(ent) \ - (ent->s.number <= sv_client->maxclients || ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) || ent->solid == SOLID_BSP) +#define IS_MONSTER(ent) \ + ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) -#define LO_PRIO(ent) \ - ((sv_client->csr->extended ? (ent->s.renderfx & RF_LOW_PRIORITY) : (ent->s.effects & (EF_GIB | EF_GREENGIB))) || (!ent->s.modelindex && !ent->s.effects)) +#define IS_HI_PRIO(ent) \ + (ent->s.number <= sv_client->maxclients || IS_MONSTER(ent) || ent->solid == SOLID_BSP) + +#define IS_GIB(ent) \ + (sv_client->csr->extended ? (ent->s.renderfx & RF_LOW_PRIORITY) : (ent->s.effects & (EF_GIB | EF_GREENGIB))) + +#define IS_LO_PRIO(ent) \ + (IS_GIB(ent) || (!ent->s.modelindex && !ent->s.effects)) static int entpriocmp(const void *p1, const void *p2) { const edict_t *a = *(const edict_t **)p1; const edict_t *b = *(const edict_t **)p2; - bool hi_a = HI_PRIO(a); - bool hi_b = HI_PRIO(b); + bool hi_a = IS_HI_PRIO(a); + bool hi_b = IS_HI_PRIO(b); if (hi_a != hi_b) return hi_b - hi_a; - bool lo_a = LO_PRIO(a); - bool lo_b = LO_PRIO(b); + bool lo_a = IS_LO_PRIO(a); + bool lo_b = IS_LO_PRIO(b); if (lo_a != lo_b) return lo_a - lo_b; From 0d33b74bd0ffdccf4e123d748c95ec25555ccea5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 17 May 2024 21:18:16 +0300 Subject: [PATCH 200/974] Use BIT() macro for override flags. --- src/common/cmodel.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/cmodel.c b/src/common/cmodel.c index abb66a90f..37be5f0ee 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -45,10 +45,10 @@ static void FloodAreaConnections(const cm_t *cm); //======================================================================= enum { - OVERRIDE_NAME = 1, - OVERRIDE_CSUM = 2, - OVERRIDE_ENTS = 4, - OVERRIDE_ALL = 7 + OVERRIDE_NAME = BIT(0), + OVERRIDE_CSUM = BIT(1), + OVERRIDE_ENTS = BIT(2), + OVERRIDE_ALL = MASK(3) }; static void load_entstring_override(cm_t *cm, const char *server) From 49e78fd8e4d28d575e986536d99c4545d1ea4dae Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 14:37:51 -0400 Subject: [PATCH 201/974] Updated build to specify ARCH in linux builds --- .github/workflows/release.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f44563498..a8ed8a523 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,28 +35,28 @@ jobs: uses: dawidd6/action-download-artifact@v2 with: workflow: build.yml - name: q2pro-lin-clang + name: q2pro-lin-clang-x86_64 skip_unpack: true - name: Download Linux x64 gcc build artifacts uses: dawidd6/action-download-artifact@v2 with: workflow: build.yml - name: q2pro-lin-gcc + name: q2pro-lin-gcc-x86_64 skip_unpack: true - name: Download Linux Wayland x64 clang build artifacts uses: dawidd6/action-download-artifact@v2 with: workflow: build.yml - name: q2pro-lin-wayland-clang + name: q2pro-lin-wayland-clang-x86_64 skip_unpack: true - name: Download Linux Wayland x64 gcc build artifacts uses: dawidd6/action-download-artifact@v2 with: workflow: build.yml - name: q2pro-lin-wayland-gcc + name: q2pro-lin-wayland-gcc-x86_64 skip_unpack: true - name: Download Linux ARM x64 build artifacts @@ -119,9 +119,9 @@ jobs: q2pro-mingw-x86_64.zip q2pro-msvc-x64.zip q2pro-lin-arm64.zip - q2pro-lin-gcc.zip - q2pro-lin-clang.zip - q2pro-lin-wayland-gcc.zip - q2pro-lin-wayland-clang.zip + q2pro-lin-gcc-x86_64.zip + q2pro-lin-clang-x86_64.zip + q2pro-lin-wayland-gcc-x86_64.zip + q2pro-lin-wayland-clang-x86_64.zip q2pro-darwin-x86_64.zip q2pro-darwin-arm64.zip From f32fbab4f41b28188e291042411fdfb3ffe29240 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 14:47:24 -0400 Subject: [PATCH 202/974] Missed one --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b4cb790d..2ffe2fd44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -153,7 +153,7 @@ jobs: - name: Generate Linux x64 archives uses: actions/upload-artifact@v4 with: - name: q2pro-lin-${{ matrix.cc }} + name: q2pro-lin-${{ matrix.cc }}-x86_64 path: | builddir/q2pro builddir/q2proded From 8649dc840b03e6b403d3727c6f52155546fa1fcb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 17 May 2024 21:49:58 +0300 Subject: [PATCH 203/974] Comment some game.h stuff. --- inc/shared/game.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 23af00081..421d17d3e 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -33,14 +33,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SVF_MONSTER BIT(2) // treat as CONTENTS_MONSTER for collision #if USE_PROTOCOL_EXTENSIONS -#define SVF_PLAYER BIT(3) +#define SVF_PLAYER BIT(3) // treat as CONTENTS_PLAYER for collision #define SVF_BOT BIT(4) #define SVF_NOBOTS BIT(5) #define SVF_RESPAWNING BIT(6) -#define SVF_PROJECTILE BIT(7) +#define SVF_PROJECTILE BIT(7) // treat as CONTENTS_PROJECTILE for collision #define SVF_INSTANCED BIT(8) #define SVF_DOOR BIT(9) -#define SVF_NOCULL BIT(10) +#define SVF_NOCULL BIT(10) // always send entity to clients (no PVS checks) #define SVF_HULL BIT(11) #endif @@ -115,9 +115,11 @@ struct edict_s { //================================ +#if USE_PROTOCOL_EXTENSIONS // extra entity state communicated to clients // only valid if g_features has GMF_PROTOCOL_EXTENSIONS bit entity_state_extension_t x; +#endif // the game dll can add anything it wants after // this point in the structure @@ -323,7 +325,7 @@ typedef struct { qboolean (*CanSave)(void); void (*PrepFrame)(void); void (*RestartFilesystem)(void); // called when fs_restart is issued - qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); + qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // must initialize `temp' qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); } game_export_ex_t; From 7086671d7b1ba996a4558c4e0aba31aeafd29deb Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 17 May 2024 15:02:46 -0400 Subject: [PATCH 204/974] Missed.. another --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ffe2fd44..374987c53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -197,7 +197,7 @@ jobs: - name: Generate Linux x64 archives uses: actions/upload-artifact@v4 with: - name: q2pro-lin-wayland-${{ matrix.cc }} + name: q2pro-lin-wayland-${{ matrix.cc }}-x86_64 path: | builddir/q2pro builddir/q2proded From 63e4ab923996245d9060e1efa2673d76e3974b37 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 17 May 2024 23:30:03 +0300 Subject: [PATCH 205/974] Check for VIS_NOAREAS on separate line. --- src/server/game.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/game.c b/src/server/game.c index b2df24797..380ced1a2 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -453,7 +453,9 @@ static qboolean PF_inVIS(const vec3_t p1, const vec3_t p2, vis_t vis) return false; if (!Q_IsBitSet(mask, leaf2->cluster)) return false; - if (!(vis & VIS_NOAREAS) && !CM_AreasConnected(&sv.cm, leaf1->area, leaf2->area)) + if (vis & VIS_NOAREAS) + return true; + if (!CM_AreasConnected(&sv.cm, leaf1->area, leaf2->area)) return false; // a door blocks it return true; } From 8673e7491c8b144e534c9059a3e0e7be3f28f647 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 18 May 2024 16:32:32 +0300 Subject: [PATCH 206/974] Use dummy flag to mark MVD entity as seen. --- src/server/mvd/client.c | 4 ++-- src/server/mvd/client.h | 3 +++ src/server/mvd/parse.c | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index af04b65ad..423cf3ec4 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1866,7 +1866,7 @@ static void emit_base_frame(mvd_t *mvd) // send base entity states for (i = 1; i < mvd->csr->max_edicts; i++) { ent = &mvd->edicts[i]; - if (!(ent->svflags & SVF_MONSTER)) + if (!(ent->svflags & SVF_MVD_SEEN)) continue; // entity never seen ent->s.number = i; MSG_PackEntity(&es, &ent->s, &ent->x); @@ -2409,7 +2409,7 @@ static void MVD_Seek_f(void) for (i = 1; i < mvd->csr->max_edicts; i++) { ent = &mvd->edicts[i]; - if (ent->svflags & SVF_MONSTER) + if (ent->svflags & SVF_MVD_SEEN) MVD_LinkEdict(mvd, ent); if (!ent->inuse) diff --git a/src/server/mvd/client.h b/src/server/mvd/client.h index 1ce9d7e3f..9fff4024e 100644 --- a/src/server/mvd/client.h +++ b/src/server/mvd/client.h @@ -39,6 +39,9 @@ with this program; if not, write to the Free Software Foundation, Inc., // game features MVD client supports #define MVD_FEATURES (GMF_CLIENTNUM | GMF_PROPERINUSE | GMF_WANT_ALL_DISCONNECTS) +// dummy flag to mark entity as seen +#define SVF_MVD_SEEN BIT(31) + #define LAYOUT_MSEC 3000 typedef enum { diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index fd285fda8..6660e74c5 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -682,7 +682,7 @@ static void MVD_ParsePacketEntities(mvd_t *mvd) } // mark this entity as seen even if removed - ent->svflags |= SVF_MONSTER; + ent->svflags |= SVF_MVD_SEEN; // shuffle current origin to old if removed if (bits & U_REMOVE) { From e6188c1a82942a736e86c5b15dc67a317d63f880 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 18 May 2024 16:34:26 +0300 Subject: [PATCH 207/974] Fix incorrect comment. --- inc/shared/game.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 421d17d3e..85fc5b490 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SVF_NOCLIENT BIT(0) // don't send entity to clients, even if it has effects #define SVF_DEADMONSTER BIT(1) // treat as CONTENTS_DEADMONSTER for collision -#define SVF_MONSTER BIT(2) // treat as CONTENTS_MONSTER for collision +#define SVF_MONSTER BIT(2) // only used by server as entity priority hint #if USE_PROTOCOL_EXTENSIONS #define SVF_PLAYER BIT(3) // treat as CONTENTS_PLAYER for collision From 29a56d0f6cb98c5d198faabd8fdc5b16345af485 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 19 May 2024 22:06:39 -0400 Subject: [PATCH 208/974] Gracefully handling non-field values for ARM --- src/action/g_save.c | 3 ++- src/action/g_spawn.c | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index 3444d6bc9..da464da7b 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -307,7 +307,8 @@ field_t fields[] = { {"maxyaw", STOFS (maxyaw), F_FLOAT, FFL_SPAWNTEMP}, {"minpitch", STOFS (minpitch), F_FLOAT, FFL_SPAWNTEMP}, {"maxpitch", STOFS (maxpitch), F_FLOAT, FFL_SPAWNTEMP}, - {"nextmap", STOFS (nextmap), F_LSTRING, FFL_SPAWNTEMP} + {"nextmap", STOFS (nextmap), F_LSTRING, FFL_SPAWNTEMP}, + {NULL} }; /* diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 9bb7062f0..f95ecaa68 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -495,8 +495,23 @@ char *ED_NewString (char *string) return newb; } +/* +=============== +FindField +Finds valid fields in the fields array +=============== +*/ +field_t* FindField(const char* key) { + field_t* f; + for (f = fields; f->name; f++) { + if (!Q_stricmp(f->name, key)) { + return f; + } + } + return NULL; +} /* =============== @@ -513,6 +528,11 @@ void ED_ParseField (char *key, char *value, edict_t * ent) float v; vec3_t vec; + if (FindField(key) == NULL) { + gi.dprintf("ED_ParseField: %s is not a valid field\n", key); + return; + } + for (f = fields; f->name; f++) { // FFL_NOSPAWN check in the following added in 3.20. Adding here. -FB From e9eb927ee2b7b59abeabebdd35bc3ff57a3260c5 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 19 May 2024 22:24:37 -0400 Subject: [PATCH 209/974] Few comment edits --- src/action/g_save.c | 2 +- src/action/g_spawn.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index da464da7b..50acceb7e 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -308,7 +308,7 @@ field_t fields[] = { {"minpitch", STOFS (minpitch), F_FLOAT, FFL_SPAWNTEMP}, {"maxpitch", STOFS (maxpitch), F_FLOAT, FFL_SPAWNTEMP}, {"nextmap", STOFS (nextmap), F_LSTRING, FFL_SPAWNTEMP}, - {NULL} + {NULL} // Added NULL terminator for safety -- darksaint }; /* diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index f95ecaa68..e8dc50f11 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -500,6 +500,7 @@ char *ED_NewString (char *string) FindField Finds valid fields in the fields array +Returns NULL if not found =============== */ From 400261824f23309b9fdc1b75d3c09090a7adb16c Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 19 May 2024 22:42:08 -0400 Subject: [PATCH 210/974] Made it a little C99 friendlier --- src/action/acesrc/acebot.h | 3 +++ src/action/acesrc/acebot_ai.c | 7 +++++-- src/action/acesrc/acebot_cmds.c | 4 +++- src/action/acesrc/bot_ai.c | 1 + src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_movement.c | 2 +- src/action/g_local.h | 4 ++++ 7 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index d05ae932c..368a449eb 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -309,6 +309,7 @@ qboolean ClientConnect (edict_t *ent, char *userinfo); void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); // acebot_ai.c protos +qboolean ACEAI_CheckShot(edict_t *self); void ACEAI_Think (edict_t *self); void ACEAI_PickLongRangeGoal(edict_t *self); void ACEAI_PickShortRangeGoal(edict_t *self); @@ -342,6 +343,7 @@ int ACEIT_ClassnameToIndex( char *classname ); void ACEIT_BuildItemNodeTable (qboolean rebuild); // acebot_movement.c protos +qboolean ACEMV_CanMove(edict_t *self, int direction); qboolean ACEMV_SpecialMove(edict_t *self,usercmd_t *ucmd); //void ACEMV_Move(edict_t *self, usercmd_t *ucmd); void ACEMV_Attack (edict_t *self, usercmd_t *ucmd); @@ -362,6 +364,7 @@ qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point //rekkie -- Quake3 -- e // acebot_nodes.c protos +void ACEND_SetGoal(edict_t *self, int goal_node); int ACEND_FindCost(int from, int to); int ACEND_FindCloseReachableNode(edict_t *self, int dist, int type); int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index be350452a..4cc1a3e18 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -913,7 +913,9 @@ void ACEAI_PickSafeGoal(edict_t *self) { self->state = STATE_FLEE; self->tries = 0; // Reset the count of how many times we tried this goal - BOTLIB_SetGoal(self,i); + + // This func has been deprecated in favor of BOTLIB_CanVisitNode, but it's unusable here + //BOTLIB_SetGoal(self,i); self->wander_timeout = level.framenum + 2.0 * HZ; // LTK_Say (self, "Under fire, extracting!"); @@ -940,7 +942,8 @@ void ACEAI_PickSafeGoal(edict_t *self) // if(goal_ent != NULL && debug_mode) // debug_printf("%s selected a %s at node %d for LR goal.\n",self->client->pers.netname, goal_ent->classname, goal_node); - BOTLIB_SetGoal(self,goal_node); + // This func has been deprecated in favor of BOTLIB_CanVisitNode, but it's unusable here + //BOTLIB_SetGoal(self,goal_node); } //----------------------------------------------------------- diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 2dd155603..54cb68997 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -123,7 +123,9 @@ qboolean ACECM_Commands(edict_t *ent) if( ent->inuse && ent->is_bot && IS_ALIVE(ent) ) { AntInitSearch( ent ); - BOTLIB_SetGoal( ent, node ); + + // This func has been deprecated in favor of BOTLIB_CanVisitNode, but it's unusable here + //BOTLIB_SetGoal( ent, node ); ent->state = STATE_MOVE; ent->node_timeout = 0; } diff --git a/src/action/acesrc/bot_ai.c b/src/action/acesrc/bot_ai.c index c52e45955..bbe62c42c 100644 --- a/src/action/acesrc/bot_ai.c +++ b/src/action/acesrc/bot_ai.c @@ -4,6 +4,7 @@ #include "../g_local.h" #include "../m_player.h" +#include "acebot.h" /* * $Log: /LTK2/src/acesrc/bot_ai.c $ diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 36b4ff9d0..9a3c9f646 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -1204,7 +1204,7 @@ qboolean BOTLIB_CanMove(edict_t* self, int direction) G_ProjectSource(self->s.origin, offset, forward, right, end); tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID | MASK_OPAQUE); - if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && BOTCOL_CanMoveSafely(self, angles))) // avoid falling after LCA || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT { diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 2f8b1b195..9751484b8 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -854,7 +854,7 @@ qboolean BOTLIB_CanMoveDir(edict_t* self, vec3_t direction) tr = gi.trace(start, NULL, NULL, end, self, MASK_PLAYERSOLID | MASK_OPAQUE); // Solid, lava, and slime - if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && CanMoveSafely(self, angles))) // avoid falling after LCA + if (((tr.fraction == 1.0) && !((lights_camera_action || self->client->uvTime) && BOTCOL_CanMoveSafely(self, angles))) // avoid falling after LCA || (tr.contents & MASK_DEADLY) // avoid SLIME or LAVA || (tr.ent && (tr.ent->touch == hurt_touch))) // avoid MOD_TRIGGER_HURT { diff --git a/src/action/g_local.h b/src/action/g_local.h index 1a88ab788..4954e105b 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1513,6 +1513,9 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, // g_func.c // void door_use(edict_t* self, edict_t* other, edict_t* activator); + +// from a_cmds.c +void _SetSniper(edict_t * ent, int zoom); //rekkie -- DEV_1 -- e // @@ -1620,6 +1623,7 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot); // // g_chase.c // +void DisableChaseCam( edict_t *ent ); void UpdateChaseCam (edict_t * ent); int ChaseTargetGone (edict_t * ent); void NextChaseMode( edict_t *ent ); From ee236920c721839d7ae346914f5314bbbb6a538e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 19 May 2024 23:00:41 -0400 Subject: [PATCH 211/974] Cleanup, adjusted botlib_sll_t per Copilot suggestion --- src/action/acesrc/acebot_nodes.c | 2 +- src/action/acesrc/acebot_spawn.c | 8 ++++---- src/action/botlib/botlib.h | 23 +++++++++++++++++------ src/action/botlib/botlib_communication.c | 2 +- src/action/botlib/botlib_nav.c | 4 +--- src/action/botlib/botlib_spawn.c | 6 +++--- src/action/botlib/botlib_spawnpoints.c | 4 ++-- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index d5b6758c4..da401b58f 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -3154,7 +3154,7 @@ void ACEND_BuildSpawnPointNodes(void) // Exit if no DM SPs found if (!sp_counter) { - Com_Printf("%s Failed build spawn point nodes. No deathmatch spawn points found.\n", __func__, MAX_SP_NODES); + Com_Printf("%s Failed build spawn point nodes. No deathmatch spawn points found.\n", __func__); return; } diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index fc3985f7d..561ac3498 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -71,7 +71,7 @@ void ACEAI_Cmd_Choose( edict_t *ent, char *s); //============================== // Get the number of the next team a bot should join //============================== -int GetNextTeamNumber() +int GetNextTeamNumber(void) { int i, onteam1 = 0, onteam2 = 0, onteam3 = 0; edict_t *e; @@ -118,7 +118,7 @@ void ACESP_JoinTeam(edict_t *ent, int desired_team) //====================================== // Using RiEvEr's new config file // -void ACESP_LoadBotConfig() +void ACESP_LoadBotConfig(void) { FILE *pIn; cvar_t *game_dir = NULL, *botdir = NULL; @@ -882,7 +882,7 @@ void DC_GetRandomBotName(byte gender, char* bot_name) // Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc // Returns char //====================================== -int DC_GetRandomClanSymbol() +int DC_GetRandomClanSymbol(void) { int sym = rand() % 27; switch (sym) @@ -959,7 +959,7 @@ int DC_GetOpposingClanSymbol(char symbol) // Gets a random clan letter // Returns char //====================================== -int DC_GetRandomClanLetter() +int DC_GetRandomClanLetter(void) { // Gets a random ASCII letter between 65 and 90, or 97 and 122 int letter = rand() % 52; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index d26cb2094..de62f7e8b 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -372,12 +372,23 @@ qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_r qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization); // Find all possible paths -- e -// Node data for our linked list -typedef struct { - struct botlib_sll_t* next; // Next node - int node; // Node number - float cost; // Cost from previous node to this node - int parent_node; //rekkie -- the node we came from +// Forward declaration +struct botlib_sll_t; + +// // Node data for our linked list +// typedef struct { +// struct botlib_sll_t* next; // Next node +// int node; // Node number +// float cost; // Cost from previous node to this node +// int parent_node; //rekkie -- the node we came from +// } botlib_sll_nodes_t; + +// Copilot suggestion +typedef struct botlib_sll_nodes_t { + struct botlib_sll_nodes_t* next; // Next node + int node; // Node number + float cost; // Cost from previous node to this node + int parent_node; //rekkie -- the node we came from } botlib_sll_nodes_t; // A double-ended singly linked list diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 7485bdfe1..d921ea0d3 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -37,7 +37,7 @@ void BOTLIB_Wave(edict_t* ent, int type) } } -void BOTLIB_PrecacheRadioSounds() +void BOTLIB_PrecacheRadioSounds(void) { int i; char path[MAX_QPATH]; diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 237889fae..006d78610 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -743,7 +743,7 @@ void BOTLIB_InitAreaConnections(void) } // Create area nodes mallocs -void BOTLIB_MallocAreaNodes() +void BOTLIB_MallocAreaNodes(void) { // Dynamically cache all the nodes in each area -- currently a max of 1024 nodes per area is allowed //area_nodes[MAX_NAV_AREAS][MAX_NAV_AREAS_NODES]; // A collection of all nodes in an area @@ -793,8 +793,6 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz if (goal_node == INVALID) return false; - //Com_Printf("%s\n", __func__); - self->bot.node_list_count = 0; // New pathing - if area nodes are supported diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 56e522bfe..85d44af13 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -540,7 +540,7 @@ void DC_GetRandomBotName(byte gender, char* bot_name) // Gets a random clan tag char symbol: ie --> [ ], ( ), { }, < >, etc // Returns char //====================================== -int BOTLIB_GetRandomClanSymbol() +int BOTLIB_GetRandomClanSymbol(void) { int sym = rand() % 25; switch (sym) @@ -617,7 +617,7 @@ int BOTLIB_GetOpposingClanSymbol(char symbol) // Gets a random clan letter // Returns char //====================================== -int BOTLIB_GetRandomClanLetter() +int BOTLIB_GetRandomClanLetter(void) { // Gets a random ASCII letter between 65 and 90, or 97 and 122 int letter = rand() % 52; @@ -1516,7 +1516,7 @@ void BOTLIB_RemoveBot(char* name) } if (!freed) - gi.bprintf(PRINT_MEDIUM, "No bot removed\n", name); + gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); } // Remove a bot by team diff --git a/src/action/botlib/botlib_spawnpoints.c b/src/action/botlib/botlib_spawnpoints.c index 16f513d3f..6e7ad2f02 100644 --- a/src/action/botlib/botlib_spawnpoints.c +++ b/src/action/botlib/botlib_spawnpoints.c @@ -333,7 +333,7 @@ void BOTLIB_Show_Spawnpoints(void) } } // Save SPs to file -void DC_Save_Spawnpoints() +void DC_Save_Spawnpoints(void) { FILE* fOut; char filename[128]; @@ -389,7 +389,7 @@ void DC_Save_Spawnpoints() Com_Printf("%s Saved %s [%i bytes] to disk\n", __func__, filename, fileSize); } // Load SPs from file -void DC_Load_Spawnpoints() +void DC_Load_Spawnpoints(void) { FILE* fIn; char filename[128]; From 4671578cee15088fb6fe6c1bf8506a84626d7d8a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 19 May 2024 23:35:49 -0400 Subject: [PATCH 212/974] Missed adding this function, but it didn't make a difference? --- src/action/g_cmds.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 2ad8e569a..9770ba741 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -2051,8 +2051,8 @@ void ClientCommand (edict_t * ent) return; // not fully in game yet #ifndef NO_BOTS - if( ACECM_Commands(ent) ) - return; + if (ACECM_Commands(ent)) return; // LTK commands + if (BOTLIB_Commands(ent)) return; // Botlib commands #endif // if (level.intermission_framenum) From 9ebf9e4e63c9a88c66a742026e6b6e6f3a1da9b7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 18 May 2024 20:10:34 +0300 Subject: [PATCH 213/974] Clip attenuation after converting to integer. --- src/server/game.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/game.c b/src/server/game.c index 380ced1a2..27276061f 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -534,9 +534,12 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, Com_Error(ERR_DROP, "%s: soundindex = %d", __func__, soundindex); vol = volume * 255; - att = min(attenuation * 64, 255); // need to clip due to check above + att = attenuation * 64; ofs = timeofs * 1000; + // need to clip due to faulty range check above + att = min(att, 255); + ent = NUM_FOR_EDICT(edict); sendchan = (ent << 3) | (channel & 7); From aee13b89ea64cac1a6737d182cb0bc850740d4b4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 18 May 2024 20:11:39 +0300 Subject: [PATCH 214/974] Simplify sound packet multicasting. --- src/server/game.c | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/server/game.c b/src/server/game.c index 27276061f..7d93f105f 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -595,19 +595,12 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, // multicast if force sending origin if (force_pos) { - if (channel & CHAN_NO_PHS_ADD) { - if (channel & CHAN_RELIABLE) { - SV_Multicast(NULL, MULTICAST_ALL_R); - } else { - SV_Multicast(NULL, MULTICAST_ALL); - } - } else { - if (channel & CHAN_RELIABLE) { - SV_Multicast(origin, MULTICAST_PHS_R); - } else { - SV_Multicast(origin, MULTICAST_PHS); - } - } + multicast_t to = MULTICAST_PHS; + if (channel & CHAN_NO_PHS_ADD) + to = MULTICAST_ALL; + if (channel & CHAN_RELIABLE) + to += MULTICAST_ALL_R; + SV_Multicast(origin, to); return; } From 52074f3f08a48afff4a928273109f1d4ed53b52a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 18 May 2024 20:13:05 +0300 Subject: [PATCH 215/974] Improve sound packet handling. Fix MAX_SOUND_PACKET definition. Add symbolic define for sound packet cursize. More accurately count msg_unreliable_bytes for sound packets. --- src/server/game.c | 4 ++-- src/server/mvd/parse.c | 4 ++-- src/server/send.c | 14 +++++++------- src/server/server.h | 3 ++- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/server/game.c b/src/server/game.c index 7d93f105f..07d9b11e0 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -649,7 +649,7 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, msg = LIST_FIRST(message_packet_t, &client->msg_free_list, entry); - msg->cursize = 0; + msg->cursize = SOUND_PACKET; msg->flags = flags; msg->index = soundindex; msg->volume = vol; @@ -662,7 +662,7 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, List_Remove(&msg->entry); List_Append(&client->msg_unreliable_list, &msg->entry); - client->msg_unreliable_bytes += MAX_SOUND_PACKET; + client->msg_unreliable_bytes += msg_write.cursize; } // clear multicast buffer diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index 6660e74c5..1c047f231 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -537,7 +537,7 @@ static void MVD_ParseSound(mvd_t *mvd, int extrabits) msg = LIST_FIRST(message_packet_t, &cl->msg_free_list, entry); - msg->cursize = 0; + msg->cursize = SOUND_PACKET; msg->flags = flags; msg->index = index; msg->volume = volume; @@ -550,7 +550,7 @@ static void MVD_ParseSound(mvd_t *mvd, int extrabits) List_Remove(&msg->entry); List_Append(&cl->msg_unreliable_list, &msg->entry); - cl->msg_unreliable_bytes += MAX_SOUND_PACKET; + cl->msg_unreliable_bytes += msg_write.cursize; } // clear multicast buffer diff --git a/src/server/send.c b/src/server/send.c index 048be6843..b9075b944 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -579,10 +579,10 @@ static inline void write_unreliables(client_t *client, unsigned maxsize) message_packet_t *msg, *next; FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { - if (msg->cursize) { - write_msg(client, msg, maxsize); - } else { + if (msg->cursize == SOUND_PACKET) { write_snd(client, msg, maxsize); + } else { + write_msg(client, msg, maxsize); } } } @@ -654,7 +654,7 @@ static void repack_unreliables(client_t *client, unsigned maxsize) // temp entities first FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { - if (!msg->cursize || msg->data[0] != svc_temp_entity) { + if (msg->cursize == SOUND_PACKET || msg->data[0] != svc_temp_entity) { continue; } // ignore some low-priority effects, these checks come from r1q2 @@ -672,7 +672,7 @@ static void repack_unreliables(client_t *client, unsigned maxsize) // then entity sounds FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { - if (!msg->cursize) { + if (msg->cursize == SOUND_PACKET) { write_snd(client, msg, maxsize); } } @@ -683,7 +683,7 @@ static void repack_unreliables(client_t *client, unsigned maxsize) // then positioned sounds FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { - if (msg->cursize && msg->data[0] == svc_sound) { + if (msg->cursize != SOUND_PACKET && msg->data[0] == svc_sound) { write_msg(client, msg, maxsize); } } @@ -694,7 +694,7 @@ static void repack_unreliables(client_t *client, unsigned maxsize) // then everything else left FOR_EACH_MSG_SAFE(&client->msg_unreliable_list) { - if (msg->cursize) { + if (msg->cursize != SOUND_PACKET) { write_msg(client, msg, maxsize); } } diff --git a/src/server/server.h b/src/server/server.h index 59bdc075a..627c05fec 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -220,7 +220,8 @@ typedef enum { #define ZPACKET_HEADER 5 -#define MAX_SOUND_PACKET 14 +#define MAX_SOUND_PACKET 15 +#define SOUND_PACKET 0 // special value for cursize typedef struct { list_t entry; From 6bfaf8248c6a9961be46c1281f894fb91ac58a3e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 19 May 2024 15:12:58 +0300 Subject: [PATCH 216/974] Inline wrapper CM_* functions. --- inc/common/cmodel.h | 31 +++++++++++++++++++++++++++---- src/common/bsp.c | 5 +---- src/common/cmodel.c | 36 ++++-------------------------------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/inc/common/cmodel.h b/inc/common/cmodel.h index eb0e18a1b..f97433b53 100644 --- a/inc/common/cmodel.h +++ b/inc/common/cmodel.h @@ -34,6 +34,8 @@ typedef struct { char *entitystring; } cm_t; +extern const mleaf_t nullleaf; + void CM_Init(void); void CM_FreeMap(cm_t *cm); @@ -52,7 +54,13 @@ const mleaf_t *CM_LeafNum(const cm_t *cm, int number); const mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs); // returns an ORed contents mask -int CM_PointContents(const vec3_t p, const mnode_t *headnode); +static inline int CM_PointContents(const vec3_t p, const mnode_t *headnode) +{ + if (!headnode) + return 0; // map not loaded + return BSP_PointLeaf(headnode, p)->contents; +} + int CM_TransformedPointContents(const vec3_t p, const mnode_t *headnode, const vec3_t origin, const vec3_t angles); @@ -69,9 +77,24 @@ void CM_ClipEntity(trace_t *dst, const trace_t *src, struct edict_s *ent) // call with topnode set to the headnode, returns with topnode // set to the first node that splits the box -int CM_BoxLeafs(const cm_t *cm, const vec3_t mins, const vec3_t maxs, - const mleaf_t **list, int listsize, const mnode_t **topnode); -const mleaf_t *CM_PointLeaf(const cm_t *cm, const vec3_t p); +int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, + const mleaf_t **list, int listsize, + const mnode_t *headnode, const mnode_t **topnode); + +static inline int CM_BoxLeafs(const cm_t *cm, const vec3_t mins, const vec3_t maxs, + const mleaf_t **list, int listsize, const mnode_t **topnode) +{ + if (!cm->cache) + return 0; // map not loaded + return CM_BoxLeafs_headnode(mins, maxs, list, listsize, cm->cache->nodes, topnode); +} + +static inline const mleaf_t *CM_PointLeaf(const cm_t *cm, const vec3_t p) +{ + if (!cm->cache) + return &nullleaf; // map not loaded + return BSP_PointLeaf(cm->cache->nodes, p); +} byte *CM_FatPVS(const cm_t *cm, byte *mask, const vec3_t org); diff --git a/src/common/bsp.c b/src/common/bsp.c index addd7b361..2bf7b3f6d 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -1766,10 +1766,7 @@ const mleaf_t *BSP_PointLeaf(const mnode_t *node, const vec3_t p) while (node->plane) { d = PlaneDiffFast(p, node->plane); - if (d < 0) - node = node->children[1]; - else - node = node->children[0]; + node = node->children[d < 0]; } return (const mleaf_t *)node; diff --git a/src/common/cmodel.c b/src/common/cmodel.c index 37be5f0ee..8f45552fd 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., mtexinfo_t nulltexinfo; -static const mleaf_t nullleaf = { .cluster = -1 }; +const mleaf_t nullleaf = { .cluster = -1 }; static unsigned floodvalid; static unsigned checkcount; @@ -332,14 +332,6 @@ const mnode_t *CM_HeadnodeForBox(const vec3_t mins, const vec3_t maxs) return box_headnode; } -const mleaf_t *CM_PointLeaf(const cm_t *cm, const vec3_t p) -{ - if (!cm->cache) { - return &nullleaf; // server may call this without map loaded - } - return BSP_PointLeaf(cm->cache->nodes, p); -} - /* ============= CM_BoxLeafnums @@ -377,9 +369,9 @@ static void CM_BoxLeafs_r(const mnode_t *node) } } -static int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, - const mleaf_t **list, int listsize, - const mnode_t *headnode, const mnode_t **topnode) +int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, + const mleaf_t **list, int listsize, + const mnode_t *headnode, const mnode_t **topnode) { leaf_list = list; leaf_count = 0; @@ -397,26 +389,6 @@ static int CM_BoxLeafs_headnode(const vec3_t mins, const vec3_t maxs, return leaf_count; } -int CM_BoxLeafs(const cm_t *cm, const vec3_t mins, const vec3_t maxs, - const mleaf_t **list, int listsize, const mnode_t **topnode) -{ - if (!cm->cache) // map not loaded - return 0; - return CM_BoxLeafs_headnode(mins, maxs, list, listsize, cm->cache->nodes, topnode); -} - -/* -================== -CM_PointContents -================== -*/ -int CM_PointContents(const vec3_t p, const mnode_t *headnode) -{ - if (!headnode) - return 0; - return BSP_PointLeaf(headnode, p)->contents; -} - /* ================== CM_TransformedPointContents From 0e0d34ea244fb17931c4398a5dc119c333158684 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 19 May 2024 15:51:20 +0300 Subject: [PATCH 217/974] Use consistent argument order for trace functions. --- src/client/client.h | 2 +- src/client/entities.c | 2 +- src/client/predict.c | 8 ++++---- src/client/tent.c | 4 ++-- src/server/world.c | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 808c2159e..4c035c890 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -819,7 +819,7 @@ void CL_InitTEnts(void); void CL_PredictAngles(void); void CL_PredictMovement(void); void CL_CheckPredictionError(void); -void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int contentmask); +void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, int contentmask); // diff --git a/src/client/entities.c b/src/client/entities.c index 47dd7c491..ab1bd4c0c 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -745,7 +745,7 @@ static void CL_AddPacketEntities(void) VectorCopy(ent.origin, start); } - CL_Trace(&trace, start, vec3_origin, vec3_origin, end, mask); + CL_Trace(&trace, start, end, vec3_origin, vec3_origin, mask); LerpVector(start, end, cent->flashlightfrac, end); V_AddLight(end, 256, 1, 1, 1); diff --git a/src/client/predict.c b/src/client/predict.c index 6123e07e5..5e9684cac 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -75,7 +75,7 @@ void CL_CheckPredictionError(void) CL_ClipMoveToEntities ==================== */ -static void CL_ClipMoveToEntities(trace_t *tr, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int contentmask) +static void CL_ClipMoveToEntities(trace_t *tr, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, int contentmask) { int i; trace_t trace; @@ -115,7 +115,7 @@ static void CL_ClipMoveToEntities(trace_t *tr, const vec3_t start, const vec3_t CL_Trace ================ */ -void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int contentmask) +void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t end, const vec3_t mins, const vec3_t maxs, int contentmask) { // check against world CM_BoxTrace(tr, start, end, mins, maxs, cl.bsp->nodes, contentmask); @@ -123,7 +123,7 @@ void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t mins, const vec3_t m tr->ent = (struct edict_s *)cl_entities; // check all other solid models - CL_ClipMoveToEntities(tr, start, mins, maxs, end, contentmask); + CL_ClipMoveToEntities(tr, start, end, mins, maxs, contentmask); } static int pm_clipmask; @@ -131,7 +131,7 @@ static int pm_clipmask; static trace_t q_gameabi CL_PMTrace(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end) { trace_t t; - CL_Trace(&t, start, mins, maxs, end, pm_clipmask); + CL_Trace(&t, start, end, mins, maxs, pm_clipmask); return t; } diff --git a/src/client/tent.c b/src/client/tent.c index 608d30554..6f9b9bf6e 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -112,7 +112,7 @@ static int CL_FindFootstepSurface(int entnum) // first, a trace done solely against MASK_SOLID trace_t tr; - CL_Trace(&tr, trace_start, trace_mins, trace_maxs, trace_end, MASK_SOLID); + CL_Trace(&tr, trace_start, trace_end, trace_mins, trace_maxs, MASK_SOLID); if (tr.fraction == 1.0f) { // if we didn't hit anything, use default step ID @@ -128,7 +128,7 @@ static int CL_FindFootstepSurface(int entnum) VectorCopy(tr.endpos, new_end); new_end[2] += 1; - CL_Trace(&tr, trace_start, trace_mins, trace_maxs, new_end, MASK_SOLID | MASK_WATER); + CL_Trace(&tr, trace_start, new_end, trace_mins, trace_maxs, MASK_SOLID | MASK_WATER); // if we hit something else, use that new footstep id instead of the first traces' value if (tr.surface != &(nulltexinfo.c)) footstep_id = ((mtexinfo_t *)tr.surface)->step_id; diff --git a/src/server/world.c b/src/server/world.c index 28930398a..33ad4144e 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -500,12 +500,12 @@ int SV_PointContents(const vec3_t p) /* ==================== SV_ClipMoveToEntities - ==================== */ -static void SV_ClipMoveToEntities(const vec3_t start, const vec3_t mins, - const vec3_t maxs, const vec3_t end, - edict_t *passedict, int contentmask, trace_t *tr) +static void SV_ClipMoveToEntities(trace_t *tr, + const vec3_t start, const vec3_t end, + const vec3_t mins, const vec3_t maxs, + edict_t *passedict, int contentmask) { vec3_t boxmins, boxmaxs; int i, num; @@ -590,7 +590,7 @@ trace_t q_gameabi SV_Trace(const vec3_t start, const vec3_t mins, return trace; // blocked by the world // clip to other solid entities - SV_ClipMoveToEntities(start, mins, maxs, end, passedict, contentmask, &trace); + SV_ClipMoveToEntities(&trace, start, end, mins, maxs, passedict, contentmask); return trace; } From 359349eacc6245d264ae900fa50048db38ead838 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 19 May 2024 18:49:42 +0300 Subject: [PATCH 218/974] Make CL_Trace() behave like SV_Trace(). --- src/client/predict.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/predict.c b/src/client/predict.c index 5e9684cac..b87a2a8e3 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -119,8 +119,9 @@ void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t end, const vec3_t mi { // check against world CM_BoxTrace(tr, start, end, mins, maxs, cl.bsp->nodes, contentmask); - if (tr->fraction < 1.0f) - tr->ent = (struct edict_s *)cl_entities; + tr->ent = (struct edict_s *)cl_entities; + if (tr->fraction == 0) + return; // blocked by the world // check all other solid models CL_ClipMoveToEntities(tr, start, end, mins, maxs, contentmask); From c265f0ac30a405cf6feb36ae275082f35c895547 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 20 May 2024 00:10:52 +0300 Subject: [PATCH 219/974] Use view origin for entity sorting by distance. --- src/server/entities.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 60b1e59ab..22d2d0543 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -507,6 +507,8 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) #define IS_LO_PRIO(ent) \ (IS_GIB(ent) || (!ent->s.modelindex && !ent->s.effects)) +static vec3_t clientorg; + static int entpriocmp(const void *p1, const void *p2) { const edict_t *a = *(const edict_t **)p1; @@ -522,8 +524,8 @@ static int entpriocmp(const void *p1, const void *p2) if (lo_a != lo_b) return lo_a - lo_b; - float dist_a = DistanceSquared(a->s.origin, sv_player->s.origin); - float dist_b = DistanceSquared(b->s.origin, sv_player->s.origin); + float dist_a = DistanceSquared(a->s.origin, clientorg); + float dist_b = DistanceSquared(b->s.origin, clientorg); if (dist_a > dist_b) return 1; return -1; @@ -698,6 +700,7 @@ void SV_BuildClientFrame(client_t *client) // prioritize entities on overflow if (num_edicts > max_packet_entities) { + VectorCopy(org, clientorg); sv_client = client; sv_player = client->edict; qsort(edicts, num_edicts, sizeof(edicts[0]), entpriocmp); From 09a42a599428200ca441aa25f3e61e9f734e2238 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 20 May 2024 00:41:03 +0300 Subject: [PATCH 220/974] Consider RF_FRAMELERP entities to be monsters. --- src/server/entities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/entities.c b/src/server/entities.c index 22d2d0543..5788b6c7c 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -496,7 +496,7 @@ static bool SV_EntityAttenuatedAway(const vec3_t org, const edict_t *ent) } #define IS_MONSTER(ent) \ - ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER) + ((ent->svflags & (SVF_MONSTER | SVF_DEADMONSTER)) == SVF_MONSTER || (ent->s.renderfx & RF_FRAMELERP)) #define IS_HI_PRIO(ent) \ (ent->s.number <= sv_client->maxclients || IS_MONSTER(ent) || ent->solid == SOLID_BSP) From 5f62413466f70de8d0a9c0eb4599210935f50ae3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 20 May 2024 14:32:33 -0400 Subject: [PATCH 221/974] Changes to ED_ParseField to support new spawn arrays --- src/action/g_spawn.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index c5369a274..5c0bd8ec0 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -164,6 +164,7 @@ typedef struct { char* name; unsigned ofs; fieldtype_t type; + int flags; } spawn_field_t; typedef struct @@ -699,15 +700,17 @@ static bool ED_ParseField(const spawn_field_t* fields, const char* key, const ch return; } - for (f = fields; f->name; f++) - { - // FFL_NOSPAWN check in the following added in 3.20. Adding here. -FB - if (!(f->flags & FFL_NOSPAWN) && !Q_stricmp (f->name, key)) - { // found it - if (f->flags & FFL_SPAWNTEMP) - b = (byte *)&st; - else - b = (byte *)ent; + // for (f = fields; f->name; f++) + // { + // // FFL_NOSPAWN check in the following added in 3.20. Adding here. -FB + // if (!(f->flags & FFL_NOSPAWN) && !Q_stricmp (f->name, key)) + // { // found it + // if (f->flags & FFL_SPAWNTEMP) + // b = (byte *)&st; + // else + // b = (byte *)ent; + for (f = fields; f->name; f++) { + if (!Q_stricmp(f->name, key)) { switch (f->type) { From 11471de8257b38bb7c830e5002c9224731d0b3b3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 21 May 2024 13:40:12 -0400 Subject: [PATCH 222/974] bool return false for ED_ParseField --- src/action/g_spawn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 5c0bd8ec0..af1e5eb29 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -697,7 +697,7 @@ static bool ED_ParseField(const spawn_field_t* fields, const char* key, const ch if (FindField(key) == NULL) { gi.dprintf("ED_ParseField: %s is not a valid field\n", key); - return; + return false; } // for (f = fields; f->name; f++) From 46be30a7c2caa9c79fd215146cf3567745997d74 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 23 May 2024 16:08:11 +0300 Subject: [PATCH 223/974] Update libcurl to 8.8.0. --- subprojects/libcurl.wrap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index 4cb52d2a4..36f5a3d0e 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,11 @@ [wrap-file] -directory = curl-8.7.1 -source_url = https://curl.se/download/curl-8.7.1.tar.xz -source_filename = curl-8.7.1.tar.xz -source_hash = 6fea2aac6a4610fbd0400afb0bcddbe7258a64c63f1f68e5855ebc0c659710cd -patch_url = https://skuller.net/meson/libcurl_8.7.1-1_patch.zip -patch_filename = libcurl_8.7.1-1_patch.zip -patch_hash = 10bae69337beb371526ff85aba2492eddf77b39278a8fba2128226b598fa3687 +directory = curl-8.8.0 +source_url = https://curl.se/download/curl-8.8.0.tar.xz +source_filename = curl-8.8.0.tar.xz +source_hash = 0f58bb95fc330c8a46eeb3df5701b0d90c9d9bfcc42bd1cd08791d12551d4400 +patch_url = https://skuller.net/meson/libcurl_8.8.0-1_patch.zip +patch_filename = libcurl_8.8.0-1_patch.zip +patch_hash = 07a6459d786c62ca248811889157ee537262e7248efef26beb2dc765b20ea07c [provide] libcurl = libcurl_dep From b6fb736682c7362558b6969a0f90f856b1aea564 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 23 May 2024 16:21:25 +0300 Subject: [PATCH 224/974] Update libjpeg-turbo to 3.0.3. --- subprojects/libjpeg-turbo.wrap | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/subprojects/libjpeg-turbo.wrap b/subprojects/libjpeg-turbo.wrap index 77d491fc7..71f38a08c 100644 --- a/subprojects/libjpeg-turbo.wrap +++ b/subprojects/libjpeg-turbo.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = libjpeg-turbo-3.0.2 -source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.2/libjpeg-turbo-3.0.2.tar.gz -source_filename = libjpeg-turbo-3.0.2.tar.gz -source_hash = c2ce515a78d91b09023773ef2770d6b0df77d674e144de80d63e0389b3a15ca6 -patch_filename = libjpeg-turbo_3.0.2-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.2-1/get_patch -patch_hash = 271788065332bcb52d504edc86466dc802a705c64b825e018ed927371ac53cf1 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.2-1/libjpeg-turbo-3.0.2.tar.gz -wrapdb_version = 3.0.2-1 +directory = libjpeg-turbo-3.0.3 +source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.3/libjpeg-turbo-3.0.3.tar.gz +source_filename = libjpeg-turbo-3.0.3.tar.gz +source_hash = 343e789069fc7afbcdfe44dbba7dbbf45afa98a15150e079a38e60e44578865d +patch_filename = libjpeg-turbo_3.0.3-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.3-1/get_patch +patch_hash = 8bbf4f205e54e73c48c14dae7fcc71f152cdc8fea6d55a1af1e3108d10c6a2e4 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.3-1/libjpeg-turbo-3.0.3.tar.gz +wrapdb_version = 3.0.3-1 [provide] dependency_names = libjpeg, libturbojpeg From d683e3d83b83ed0c40a46a891a813c5f1d750ca9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 24 May 2024 19:40:06 +0300 Subject: [PATCH 225/974] Clean up server world code a bit. --- src/server/world.c | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/server/world.c b/src/server/world.c index 33ad4144e..be4a09959 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -100,21 +100,17 @@ SV_ClearWorld */ void SV_ClearWorld(void) { - mmodel_t *cm; - edict_t *ent; - int i; - memset(sv_areanodes, 0, sizeof(sv_areanodes)); sv_numareanodes = 0; if (sv.cm.cache) { - cm = &sv.cm.cache->models[0]; + const mmodel_t *cm = &sv.cm.cache->models[0]; SV_CreateAreaNode(0, cm->mins, cm->maxs); } // make sure all entities are unlinked - for (i = 0; i < ge->max_edicts; i++) { - ent = EDICT_NUM(i); + for (int i = 0; i < ge->max_edicts; i++) { + edict_t *ent = EDICT_NUM(i); ent->area.next = ent->area.prev = NULL; } } @@ -175,9 +171,9 @@ void SV_LinkEdict(const cm_t *cm, edict_t *ent) ent->areanum = 0; ent->areanum2 = 0; - //get all leafs, including solids + // get all leafs, including solids num_leafs = CM_BoxLeafs(cm, ent->absmin, ent->absmax, - leafs, MAX_TOTAL_ENT_LEAFS, &topnode); + leafs, q_countof(leafs), &topnode); // set areas for (i = 0; i < num_leafs; i++) { @@ -187,16 +183,15 @@ void SV_LinkEdict(const cm_t *cm, edict_t *ent) // doors may legally straggle two areas, // but nothing should evern need more than that if (ent->areanum && ent->areanum != area) { - if (ent->areanum2 && ent->areanum2 != area && sv.state == ss_loading) { + if (ent->areanum2 && ent->areanum2 != area && sv.state == ss_loading) Com_DPrintf("Object touching 3 areas at %s\n", vtos(ent->absmin)); - } ent->areanum2 = area; } else ent->areanum = area; } } - if (num_leafs >= MAX_TOTAL_ENT_LEAFS) { + if (num_leafs == q_countof(leafs)) { // assume we missed some leafs, and mark by headnode ent->num_clusters = -1; ent->headnode = CM_NumNode(cm, topnode); @@ -285,9 +280,8 @@ void PF_LinkEdict(edict_t *ent) return; } - if (!sv.cm.cache) { + if (!sv.cm.cache) return; - } entnum = NUM_FOR_EDICT(ent); sent = &sv.entities[entnum]; @@ -523,7 +517,7 @@ static void SV_ClipMoveToEntities(trace_t *tr, } } - num = SV_AreaEdicts(boxmins, boxmaxs, touchlist, MAX_EDICTS, AREA_SOLID); + num = SV_AreaEdicts(boxmins, boxmaxs, touchlist, q_countof(touchlist), AREA_SOLID); // be careful, it is possible to have an entity in this // list removed before we get to it (killtriggered) From d0b6f75547bfcc099c9637fa086b2e3a9e118c27 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 24 May 2024 19:40:20 +0300 Subject: [PATCH 226/974] Skip passedict comparsion if NULL. --- src/server/world.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/world.c b/src/server/world.c index be4a09959..573652a73 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -525,11 +525,11 @@ static void SV_ClipMoveToEntities(trace_t *tr, touch = touchlist[i]; if (touch->solid == SOLID_NOT) continue; - if (touch == passedict) - continue; if (tr->allsolid) return; if (passedict) { + if (touch == passedict) + continue; if (touch->owner == passedict) continue; // don't clip against own missiles if (passedict->owner == touch) From 5f78066ef2bc8f9bf62323c7c1f1dcaeb30d00d1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 24 May 2024 19:59:56 +0300 Subject: [PATCH 227/974] =?UTF-8?q?Rename=20ambiguous=20=E2=80=98slot?= =?UTF-8?q?=E2=80=99=20to=20=E2=80=98infonum=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/entities.c | 2 +- src/server/main.c | 2 +- src/server/mvd.c | 2 +- src/server/mvd/game.c | 2 +- src/server/server.h | 2 +- src/server/user.c | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 5788b6c7c..05667fc4c 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -764,5 +764,5 @@ void SV_BuildClientFrame(client_t *client) } if (need_clientnum_fix) - frame->clientNum = client->slot; + frame->clientNum = client->infonum; } diff --git a/src/server/main.c b/src/server/main.c index dd668049d..d8172f473 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1092,7 +1092,7 @@ static void SVC_DirectConnect(void) // accept the new client // this is the only place a client_t is ever initialized memset(newcl, 0, sizeof(*newcl)); - newcl->number = newcl->slot = number; + newcl->number = newcl->infonum = number; newcl->challenge = params.challenge; // save challenge for checksumming newcl->protocol = params.protocol; newcl->version = params.version; diff --git a/src/server/mvd.c b/src/server/mvd.c index 71ba2ad06..bbef429ee 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -375,7 +375,7 @@ static int dummy_create(void) memset(newcl, 0, sizeof(*newcl)); number = newcl - svs.client_pool; - newcl->number = newcl->slot = number; + newcl->number = newcl->infonum = number; newcl->protocol = -1; newcl->state = cs_connected; newcl->AddMessage = dummy_add_message; diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index db3553245..81a19ef7f 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -778,7 +778,7 @@ static void MVD_SetServerState(client_t *cl, mvd_t *mvd) cl->mapname = mvd->mapname; cl->configstrings = mvd->configstrings; cl->csr = mvd->csr; - cl->slot = mvd->clientNum; + cl->infonum = mvd->clientNum; cl->cm = &mvd->cm; cl->ge = &mvd->ge; cl->spawncount = mvd->servercount; diff --git a/src/server/server.h b/src/server/server.h index 627c05fec..e7938988e 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -356,7 +356,7 @@ typedef struct client_s { char *gamedir, *mapname; const game_export_t *ge; cm_t *cm; - int slot; + int infonum; // slot number visible to client int spawncount; int maxclients; diff --git a/src/server/user.c b/src/server/user.c index 362168ac5..7d9d53c6b 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -389,7 +389,7 @@ void SV_New_f(void) if (sv.state == ss_pic || sv.state == ss_cinematic) MSG_WriteShort(-1); else - MSG_WriteShort(sv_client->slot); + MSG_WriteShort(sv_client->infonum); MSG_WriteString(sv_client->configstrings[CS_NAME]); // send protocol specific stuff @@ -420,7 +420,7 @@ void SV_New_f(void) if (sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version < PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT && - sv_client->slot == CLIENTNUM_NONE && oldstate == cs_assigned) + sv_client->infonum == CLIENTNUM_NONE && oldstate == cs_assigned) { SV_ClientPrintf(sv_client, PRINT_HIGH, "WARNING: Server has allocated client slot number 255. " From cfe9ca24b2c9b916c87ce7a6ebc7dc8934d5410e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 21:57:40 +0300 Subject: [PATCH 228/974] Use defines instead of magic constants. --- src/client/sound/main.c | 2 +- src/server/init.c | 2 +- src/server/main.c | 4 ++-- src/server/mvd/client.c | 2 +- src/shared/shared.c | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/sound/main.c b/src/client/sound/main.c index 026299906..1bce4d4f2 100644 --- a/src/client/sound/main.c +++ b/src/client/sound/main.c @@ -536,7 +536,7 @@ channel_t *S_PickChannel(int entnum, int entchannel) // Check for replacement sound, or find the best one to replace first_to_die = -1; - life_left = 0x7fffffff; + life_left = INT_MAX; for (ch_idx = 0; ch_idx < s_numchannels; ch_idx++) { ch = &s_channels[ch_idx]; // channel 0 never overrides unless out of channels diff --git a/src/server/init.c b/src/server/init.c index 923aab45c..60bf9b1b4 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -125,7 +125,7 @@ void SV_SpawnServer(const mapcmd_t *cmd) // wipe the entire per-level structure memset(&sv, 0, sizeof(sv)); - sv.spawncount = Q_rand() & 0x7fffffff; + sv.spawncount = Q_rand() & INT_MAX; // set legacy spawncounts FOR_EACH_CLIENT(client) { diff --git a/src/server/main.c b/src/server/main.c index d8172f473..ee3fcec29 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -574,7 +574,7 @@ static void SVC_GetChallenge(void) unsigned oldestTime; oldest = 0; - oldestTime = 0xffffffff; + oldestTime = UINT_MAX; // see if we already have a challenge for this ip for (i = 0; i < MAX_CHALLENGES; i++) { @@ -589,7 +589,7 @@ static void SVC_GetChallenge(void) } } - challenge = Q_rand() & 0x7fffffff; + challenge = Q_rand() & INT_MAX; if (i == MAX_CHALLENGES) { // overwrite the oldest svs.challenges[oldest].challenge = challenge; diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index 423cf3ec4..d0f403518 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1690,7 +1690,7 @@ void MVD_Spawn(void) #endif // generate spawncount for Waiting Room - sv.spawncount = Q_rand() & 0x7fffffff; + sv.spawncount = Q_rand() & INT_MAX; #if USE_FPS // just fixed base FPS diff --git a/src/shared/shared.c b/src/shared/shared.c index 0f907e5e3..8a49900dc 100644 --- a/src/shared/shared.c +++ b/src/shared/shared.c @@ -921,8 +921,8 @@ uint32_t Q_rand(void) mt_index = 0; #define STEP(j, k) do { \ - x = mt_state[i] & 0x80000000; \ - x |= mt_state[j] & 0x7FFFFFFF; \ + x = mt_state[i] & BIT(31); \ + x |= mt_state[j] & MASK(31); \ y = x >> 1; \ y ^= 0x9908B0DF & -(x & 1); \ mt_state[i] = mt_state[k] ^ y; \ From 3a7ec145445a2f083d6498770be2a3ad16375d39 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 21:58:10 +0300 Subject: [PATCH 229/974] Simplify Q_IsBitSet(). --- inc/shared/shared.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index aa13a5626..f5fcca77b 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -411,7 +411,7 @@ static inline uint16_t Q_clip_uint16(int a) #define Q_rint(x) ((x) < 0 ? ((int)((x) - 0.5f)) : ((int)((x) + 0.5f))) -#define Q_IsBitSet(data, bit) (((data)[(bit) >> 3] & (1 << ((bit) & 7))) != 0) +#define Q_IsBitSet(data, bit) (((data)[(bit) >> 3] >> ((bit) & 7)) & 1) #define Q_SetBit(data, bit) ((data)[(bit) >> 3] |= (1 << ((bit) & 7))) #define Q_ClearBit(data, bit) ((data)[(bit) >> 3] &= ~(1 << ((bit) & 7))) From ee711f1f98f0ec4d2f89b8c319bcef526ec65629 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 21:58:54 +0300 Subject: [PATCH 230/974] Fix sign extension bug in Com_MakePrintable(). --- src/common/utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils.c b/src/common/utils.c index eca5e6811..10b9ac33f 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -585,7 +585,7 @@ char *Com_MakePrintable(const char *s) char *end = buffer + sizeof(buffer); while (*s && o < end - 1) { - int c = *s++; + byte c = *s++; int e = get_escape_char(c); if (e) From 9d275e556d40966c87b69de342e1f191eb85139d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 22:00:37 +0300 Subject: [PATCH 231/974] Use 64-bit integer math for GTV stream compression ratio. --- src/server/mvd/client.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index d0f403518..43ac697ba 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1786,8 +1786,7 @@ static void MVD_ListServers_f(void) ratio = 100; #if USE_ZLIB if (gtv->z_act && gtv->z_str.total_out) { - ratio = 100 * ((double)gtv->z_str.total_in / - gtv->z_str.total_out); + ratio = gtv->z_str.total_in * 100ULL / gtv->z_str.total_out; } #endif Com_Printf("%2d %-12.12s %-12.12s %4u%% %7u %s\n", From fef4bebaff3c43e366c528fb7e696d6dce73c89a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 22:01:25 +0300 Subject: [PATCH 232/974] =?UTF-8?q?Show=20MVD=20playback=20progress=20in?= =?UTF-8?q?=20=E2=80=98mvdchannels=E2=80=99=20command=20output.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/mvd/client.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index 43ac697ba..b660d3a8b 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1721,12 +1721,19 @@ static void list_generic(void) "-- ------------ -------- --- --- ---- --- ---- --------------\n"); FOR_EACH_MVD(mvd) { + gtv_t *gtv = mvd->gtv; + int percent; + + if (gtv && gtv->demoplayback) + percent = gtv->demoprogress * 100; + else + percent = FIFO_Percent(&mvd->delay); + Com_Printf("%2d %-12.12s %-8.8s %3d %3d %-4.4s %3d %4u %s\n", mvd->id, mvd->name, mvd->mapname, List_Count(&mvd->clients), mvd->numplayers, - mvd_states[mvd->state], - FIFO_Percent(&mvd->delay), mvd->num_packets, - mvd->gtv ? mvd->gtv->address : ""); + mvd_states[mvd->state], percent, mvd->num_packets, + gtv ? gtv->address : ""); } } From 519dc33aed98c2e77f58a810aa15a48d61f1f0f6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 25 May 2024 22:02:01 +0300 Subject: [PATCH 233/974] Optimize baselines transmission. --- src/server/user.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/server/user.c b/src/server/user.c index 7d9d53c6b..66ceaae14 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -76,6 +76,16 @@ static void SV_CreateBaselines(void) base = *chunk + (i & SV_BASELINES_MASK); MSG_PackEntity(base, &ent->s, ENT_EXTENSION(sv_client->csr, ent)); + // no need to transmit data that will change anyway + if (i <= sv_client->maxclients) { + VectorClear(base->origin); + VectorClear(base->angles); + base->frame = 0; + } + + // don't ever transmit event + base->event = 0; + #if USE_MVD_CLIENT if (sv.state == ss_broadcast) { // spectators only need to know about inline BSP models From cb917750fc99e3413b26f1547b01a691da5ca93f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 26 May 2024 00:14:48 +0300 Subject: [PATCH 234/974] Check for NULL multicast origin, empty message. --- src/server/send.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/server/send.c b/src/server/send.c index b9075b944..6b2906e9e 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -268,6 +268,14 @@ void SV_Multicast(const vec3_t origin, multicast_t to) to -= MULTICAST_ALL_R; } + if (to && !origin) + Com_Error(ERR_DROP, "%s: NULL origin", __func__); + + if (!msg_write.cursize) { + Com_DPrintf("%s with empty data\n", __func__); + return; + } + if (to) { leaf1 = CM_PointLeaf(&sv.cm, origin); BSP_ClusterVis(sv.cm.cache, mask, leaf1->cluster, MULTICAST_PVS - to); From 5a3cd614b6d4d3f32451c4eb0f82e38ab238ebb1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 26 May 2024 18:46:10 +0300 Subject: [PATCH 235/974] Const-ify more stuff. --- inc/client/client.h | 2 +- inc/client/ui.h | 2 +- inc/client/video.h | 2 +- inc/common/hash_map.h | 8 ++++---- inc/server/server.h | 2 +- src/client/ascii.c | 4 ++-- src/client/client.h | 2 +- src/client/crc.c | 2 +- src/client/demo.c | 4 ++-- src/client/effects.c | 2 +- src/client/input.c | 2 +- src/client/main.c | 2 +- src/client/refresh.c | 2 +- src/client/sound/dma.c | 22 +++++++++++----------- src/client/ui/servers.c | 2 +- src/client/ui/ui.c | 2 +- src/client/ui/ui.h | 2 +- src/common/files.c | 8 ++++---- src/common/hash_map.c | 8 ++++---- src/common/net/net.c | 6 +++--- src/common/prompt.c | 10 +++------- src/refresh/images.c | 34 +++++++++++++++++----------------- src/refresh/images.h | 2 +- src/refresh/surf.c | 6 +++--- src/refresh/texture.c | 6 +++--- src/server/ac.c | 2 +- src/server/entities.c | 20 ++++++++++---------- src/server/main.c | 16 ++++++++-------- src/server/mvd.c | 6 +++--- src/server/send.c | 10 +++++----- src/server/server.h | 6 +++--- 31 files changed, 100 insertions(+), 104 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index a11d3ec5e..00e793c2b 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -63,7 +63,7 @@ typedef enum { bool CL_ProcessEvents(void); #if USE_ICMP -void CL_ErrorEvent(netadr_t *from); +void CL_ErrorEvent(const netadr_t *from); #endif void CL_Init(void); void CL_Disconnect(error_type_t type); diff --git a/inc/client/ui.h b/inc/client/ui.h index 400f7ded2..024a5b145 100644 --- a/inc/client/ui.h +++ b/inc/client/ui.h @@ -38,7 +38,7 @@ void UI_Draw(unsigned realtime); void UI_OpenMenu(uiMenu_t menu); void UI_Frame(int msec); void UI_StatusEvent(const serverStatus_t *status); -void UI_ErrorEvent(netadr_t *from); +void UI_ErrorEvent(const netadr_t *from); void UI_MouseEvent(int x, int y); bool UI_IsTransparent(void); #else diff --git a/inc/client/video.h b/inc/client/video.h index 07ed655f1..a9b058443 100644 --- a/inc/client/video.h +++ b/inc/client/video.h @@ -56,5 +56,5 @@ extern vid_driver_t vid; bool VID_GetFullscreen(vrect_t *rc, int *freq_p, int *depth_p); bool VID_GetGeometry(vrect_t *rc); -void VID_SetGeometry(vrect_t *rc); +void VID_SetGeometry(const vrect_t *rc); void VID_ToggleFullscreen(void); diff --git a/inc/common/hash_map.h b/inc/common/hash_map.h index bd6961253..0d91b87bf 100644 --- a/inc/common/hash_map.h +++ b/inc/common/hash_map.h @@ -27,10 +27,10 @@ void HashMap_Destroy(hash_map_t *map); void HashMap_Reserve(hash_map_t *map, uint32_t capacity); bool HashMap_InsertImpl(hash_map_t *map, const uint32_t key_size, const uint32_t value_size, const void *const key, const void *const value); bool HashMap_EraseImpl(hash_map_t *map, const uint32_t key_size, const void *const key); -void *HashMap_LookupImpl(hash_map_t *map, const uint32_t key_size, const void *const key); -uint32_t HashMap_Size(hash_map_t *map); -void *HashMap_GetKeyImpl(hash_map_t *map, uint32_t index); -void *HashMap_GetValueImpl(hash_map_t *map, uint32_t index); +void *HashMap_LookupImpl(const hash_map_t *map, const uint32_t key_size, const void *const key); +uint32_t HashMap_Size(const hash_map_t *map); +void *HashMap_GetKeyImpl(const hash_map_t *map, uint32_t index); +void *HashMap_GetValueImpl(const hash_map_t *map, uint32_t index); #define HashMap_Create(key_type, value_type, hasher, comp) HashMap_CreateImpl(sizeof(key_type), sizeof(value_type), hasher, comp) #define HashMap_Insert(map, key, value) HashMap_InsertImpl(map, sizeof(*key), sizeof(*value), key, value) diff --git a/inc/server/server.h b/inc/server/server.h index 5facefa5a..55cc8b6a9 100644 --- a/inc/server/server.h +++ b/inc/server/server.h @@ -31,7 +31,7 @@ typedef enum { } server_state_t; #if USE_ICMP -void SV_ErrorEvent(netadr_t *from, int ee_errno, int ee_info); +void SV_ErrorEvent(const netadr_t *from, int ee_errno, int ee_info); #endif void SV_Init(void); void SV_Shutdown(const char *finalmsg, error_type_t type); diff --git a/src/client/ascii.c b/src/client/ascii.c index d586e4a8b..dce044930 100644 --- a/src/client/ascii.c +++ b/src/client/ascii.c @@ -29,7 +29,7 @@ STAT PROGRAMS TO TEXT #define TH_WIDTH 80 #define TH_HEIGHT 40 -static void TH_DrawString(char *dst, int x, int y, char *src, size_t len) +static void TH_DrawString(char *dst, int x, int y, const char *src, size_t len) { int c; @@ -58,7 +58,7 @@ static void TH_DrawString(char *dst, int x, int y, char *src, size_t len) } } -static void TH_DrawCenterString(char *dst, int x, int y, char *src, size_t len) +static void TH_DrawCenterString(char *dst, int x, int y, const char *src, size_t len) { x -= len / 2; if (x < 0) { diff --git a/src/client/client.h b/src/client/client.h index 4c035c890..1b6f74914 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -855,7 +855,7 @@ void CL_BigTeleportParticles(const vec3_t org); void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old); void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, int flags); void CL_FlyEffect(centity_t *ent, const vec3_t origin); -void CL_BfgParticles(entity_t *ent); +void CL_BfgParticles(const entity_t *ent); void CL_ItemRespawnParticles(const vec3_t org); void CL_InitEffects(void); void CL_ClearEffects(void); diff --git a/src/client/crc.c b/src/client/crc.c index 44ac3bce8..b661f0a08 100644 --- a/src/client/crc.c +++ b/src/client/crc.c @@ -61,7 +61,7 @@ static const uint16_t crctable[256] = { 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 }; -static uint16_t CRC_Block(byte *start, size_t count) +static uint16_t CRC_Block(const byte *start, size_t count) { uint16_t crc = CRC_INIT_VALUE; diff --git a/src/client/demo.c b/src/client/demo.c index c5bc1b44b..fc3cf4f82 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -86,7 +86,7 @@ void CL_PackEntity(entity_packed_t *out, const centity_state_t *in) } // writes a delta update of an entity_state_t list to the message. -static void emit_packet_entities(server_frame_t *from, server_frame_t *to) +static void emit_packet_entities(const server_frame_t *from, const server_frame_t *to) { entity_packed_t oldpack, newpack; centity_state_t *oldent, *newent; @@ -157,7 +157,7 @@ static void emit_packet_entities(server_frame_t *from, server_frame_t *to) MSG_WriteShort(0); // end of packetentities } -static void emit_delta_frame(server_frame_t *from, server_frame_t *to, +static void emit_delta_frame(const server_frame_t *from, const server_frame_t *to, int fromnum, int tonum) { player_packed_t oldpack, newpack; diff --git a/src/client/effects.c b/src/client/effects.c index ee7f6885b..315b66185 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -1630,7 +1630,7 @@ void CL_FlyEffect(centity_t *ent, const vec3_t origin) CL_BfgParticles =============== */ -void CL_BfgParticles(entity_t *ent) +void CL_BfgParticles(const entity_t *ent) { int i; cparticle_t *p; diff --git a/src/client/input.c b/src/client/input.c index b6ae5200e..04ba48425 100644 --- a/src/client/input.c +++ b/src/client/input.c @@ -414,7 +414,7 @@ CL_KeyState Returns the fraction of the frame that the key was down =============== */ -static float CL_KeyState(kbutton_t *key) +static float CL_KeyState(const kbutton_t *key) { unsigned msec = key->msec; diff --git a/src/client/main.c b/src/client/main.c index d14908afe..4cfca7bed 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -1512,7 +1512,7 @@ static void CL_PacketEvent(void) } #if USE_ICMP -void CL_ErrorEvent(netadr_t *from) +void CL_ErrorEvent(const netadr_t *from) { UI_ErrorEvent(from); diff --git a/src/client/refresh.c b/src/client/refresh.c index 007826bb7..4cfff3c4f 100644 --- a/src/client/refresh.c +++ b/src/client/refresh.c @@ -184,7 +184,7 @@ bool VID_GetGeometry(vrect_t *rc) return true; } -void VID_SetGeometry(vrect_t *rc) +void VID_SetGeometry(const vrect_t *rc) { char buffer[MAX_QPATH]; diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index 41888a491..fccea572f 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -176,7 +176,7 @@ PAINTBUFFER TRANSFER =============================================================================== */ -static void TransferStereo16(samplepair_t *samp, int endtime) +static void TransferStereo16(const samplepair_t *samp, int endtime) { int ltime = s_paintedtime; int size = dma.samples >> 1; @@ -197,9 +197,9 @@ static void TransferStereo16(samplepair_t *samp, int endtime) } } -static void TransferStereo(samplepair_t *samp, int endtime) +static void TransferStereo(const samplepair_t *samp, int endtime) { - float *p = (float *)samp; + const float *p = (const float *)samp; int count = (endtime - s_paintedtime) * dma.channels; int out_mask = dma.samples - 1; int out_idx = s_paintedtime * dma.channels & out_mask; @@ -325,16 +325,16 @@ CHANNEL MIXING =============================================================================== */ -typedef void (*paintfunc_t)(channel_t *, sfxcache_t *, int, samplepair_t *); +typedef void (*paintfunc_t)(const channel_t *, const sfxcache_t *, int, samplepair_t *); #define PAINTFUNC(name) \ - static void name(channel_t *ch, sfxcache_t *sc, int count, samplepair_t *samp) + static void name(const channel_t *ch, const sfxcache_t *sc, int count, samplepair_t *samp) PAINTFUNC(PaintMono8) { float leftvol = ch->leftvol * snd_vol * 256; float rightvol = ch->rightvol * snd_vol * 256; - uint8_t *sfx = sc->data + ch->pos; + const uint8_t *sfx = sc->data + ch->pos; for (int i = 0; i < count; i++, samp++, sfx++) { samp->left += (*sfx - 128) * leftvol; @@ -346,7 +346,7 @@ PAINTFUNC(PaintStereoDmix8) { float leftvol = ch->leftvol * snd_vol * (256 * M_SQRT1_2); float rightvol = ch->rightvol * snd_vol * (256 * M_SQRT1_2); - uint8_t *sfx = sc->data + ch->pos * 2; + const uint8_t *sfx = sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { int sum = (sfx[0] - 128) + (sfx[1] - 128); @@ -358,7 +358,7 @@ PAINTFUNC(PaintStereoDmix8) PAINTFUNC(PaintStereoFull8) { float vol = ch->leftvol * snd_vol * 256; - uint8_t *sfx = sc->data + ch->pos * 2; + const uint8_t *sfx = sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { samp->left += (sfx[0] - 128) * vol; @@ -370,7 +370,7 @@ PAINTFUNC(PaintMono16) { float leftvol = ch->leftvol * snd_vol; float rightvol = ch->rightvol * snd_vol; - int16_t *sfx = (int16_t *)sc->data + ch->pos; + const int16_t *sfx = (const int16_t *)sc->data + ch->pos; for (int i = 0; i < count; i++, samp++, sfx++) { samp->left += *sfx * leftvol; @@ -382,7 +382,7 @@ PAINTFUNC(PaintStereoDmix16) { float leftvol = ch->leftvol * snd_vol * M_SQRT1_2; float rightvol = ch->rightvol * snd_vol * M_SQRT1_2; - int16_t *sfx = (int16_t *)sc->data + ch->pos * 2; + const int16_t *sfx = (const int16_t *)sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { int sum = sfx[0] + sfx[1]; @@ -394,7 +394,7 @@ PAINTFUNC(PaintStereoDmix16) PAINTFUNC(PaintStereoFull16) { float vol = ch->leftvol * snd_vol; - int16_t *sfx = (int16_t *)sc->data + ch->pos * 2; + const int16_t *sfx = (const int16_t *)sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { samp->left += sfx[0] * vol; diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index 87e6fe2de..98279cfa8 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -324,7 +324,7 @@ UI_ErrorEvent An ICMP destination-unreachable error has been received. ================= */ -void UI_ErrorEvent(netadr_t *from) +void UI_ErrorEvent(const netadr_t *from) { serverslot_t *slot; netadr_t address; diff --git a/src/client/ui/ui.c b/src/client/ui/ui.c index 2c7155f58..b80b1bee9 100644 --- a/src/client/ui/ui.c +++ b/src/client/ui/ui.c @@ -285,7 +285,7 @@ char *UI_GetColumn(char *s, int n) UI_CursorInRect ================= */ -bool UI_CursorInRect(vrect_t *rect) +bool UI_CursorInRect(const vrect_t *rect) { if (uis.mouseCoords[0] < rect->x) { return false; diff --git a/src/client/ui/ui.h b/src/client/ui/ui.h index 6c1220570..c3be79296 100644 --- a/src/client/ui/ui.h +++ b/src/client/ui/ui.h @@ -317,7 +317,7 @@ void UI_ForceMenuOff(void); void UI_PopMenu(void); void UI_StartSound(menuSound_t sound); bool UI_DoHitTest(void); -bool UI_CursorInRect(vrect_t *rect); +bool UI_CursorInRect(const vrect_t *rect); void *UI_FormatColumns(int extrasize, ...) q_sentinel; char *UI_GetColumn(char *s, int n); void UI_DrawString(int x, int y, int flags, const char *string); diff --git a/src/common/files.c b/src/common/files.c index 6a671546a..4890f6d6e 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -421,7 +421,7 @@ static file_t *file_for_handle(qhandle_t f) } // expects a buffer of at least MAX_OSPATH bytes! -static symlink_t *expand_links(list_t *list, char *buffer, size_t *len_p) +static symlink_t *expand_links(const list_t *list, char *buffer, size_t *len_p) { symlink_t *link; size_t namelen = *len_p; @@ -503,7 +503,7 @@ int64_t FS_Tell(qhandle_t f) } } -static int64_t get_seek_offset(file_t *file, int64_t offset, int whence) +static int64_t get_seek_offset(const file_t *file, int64_t offset, int whence) { switch (whence) { case SEEK_SET: @@ -2273,7 +2273,7 @@ static bool parse_zip64_extra_data(packfile_t *file, const byte *buf, int size) return true; } -static bool parse_extra_data(pack_t *pack, packfile_t *file, int xtra_size) +static bool parse_extra_data(const pack_t *pack, packfile_t *file, int xtra_size) { byte buf[0xffff]; int pos = 0; @@ -2294,7 +2294,7 @@ static bool parse_extra_data(pack_t *pack, packfile_t *file, int xtra_size) return false; } -static bool get_file_info(pack_t *pack, packfile_t *file, char *name, size_t *len, bool zip64) +static bool get_file_info(const pack_t *pack, packfile_t *file, char *name, size_t *len, bool zip64) { unsigned comp_mtd, comp_len, file_len, name_size, xtra_size, comm_size, file_pos; byte header[ZIP_SIZECENTRALDIRITEM]; // we can't use a struct here because of packing diff --git a/src/common/hash_map.c b/src/common/hash_map.c index 226039375..7d311acc1 100644 --- a/src/common/hash_map.c +++ b/src/common/hash_map.c @@ -44,7 +44,7 @@ typedef struct hash_map_s { HashMap_GetKeyImpl ================= */ -void *HashMap_GetKeyImpl(hash_map_t *map, uint32_t index) +void *HashMap_GetKeyImpl(const hash_map_t *map, uint32_t index) { return (byte *)map->keys + (map->key_size * index); } @@ -54,7 +54,7 @@ void *HashMap_GetKeyImpl(hash_map_t *map, uint32_t index) HashMap_GetValueImpl ================= */ -void *HashMap_GetValueImpl(hash_map_t *map, uint32_t index) +void *HashMap_GetValueImpl(const hash_map_t *map, uint32_t index) { return (byte *)map->values + (map->value_size * index); } @@ -253,7 +253,7 @@ bool HashMap_EraseImpl(hash_map_t *map, const uint32_t key_size, const void *con HashMap_LookupImpl ================= */ -void *HashMap_LookupImpl(hash_map_t *map, const uint32_t key_size, const void *const key) +void *HashMap_LookupImpl(const hash_map_t *map, const uint32_t key_size, const void *const key) { Q_assert(map->key_size == key_size); if (map->num_entries == 0) @@ -277,7 +277,7 @@ void *HashMap_LookupImpl(hash_map_t *map, const uint32_t key_size, const void *c HashMap_Size ================= */ -uint32_t HashMap_Size(hash_map_t *map) +uint32_t HashMap_Size(const hash_map_t *map) { return map->num_entries; } diff --git a/src/common/net/net.c b/src/common/net/net.c index 1c5de4686..1db8b523f 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -601,7 +601,7 @@ static bool NET_SendLoopPacket(netsrc_t sock, const void *data, static const char *os_error_string(int err); -static void NET_ErrorEvent(qsocket_t sock, netadr_t *from, +static void NET_ErrorEvent(qsocket_t sock, const netadr_t *from, int ee_errno, int ee_info) { struct pollfd *s; @@ -1635,7 +1635,7 @@ neterr_t NET_RunStream(netstream_t *s) //=================================================================== -static void dump_addrinfo(struct addrinfo *ai) +static void dump_addrinfo(const struct addrinfo *ai) { char buf1[MAX_QPATH], buf2[MAX_STRING_CHARS]; const char *fa = ai->ai_addr->sa_family == AF_INET6 ? "6" : ""; @@ -1649,7 +1649,7 @@ static void dump_addrinfo(struct addrinfo *ai) Com_Printf("IP%1s : %s\n", fa, buf1); } -static void dump_socket(struct pollfd *s, const char *s1, const char *s2) +static void dump_socket(const struct pollfd *s, const char *s1, const char *s2) { netadr_t adr; diff --git a/src/common/prompt.c b/src/common/prompt.c index 63ddfa142..3e3ed11b7 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., static cvar_t *com_completion_mode; static cvar_t *com_completion_treshold; -static void Prompt_ShowMatches(commandPrompt_t *prompt, char **matches, int count) +static void Prompt_ShowMatches(const commandPrompt_t *prompt, char **matches, int count) { int numCols = 7, numLines; int i, j, k; @@ -71,12 +71,8 @@ static void Prompt_ShowMatches(commandPrompt_t *prompt, char **matches, int coun } } -static void Prompt_ShowIndividualMatches( - commandPrompt_t *prompt, - char **matches, - int numCommands, - int numAliases, - int numCvars) +static void Prompt_ShowIndividualMatches(const commandPrompt_t *prompt, char **matches, + int numCommands, int numAliases, int numCvars) { if (numCommands) { qsort(matches, numCommands, sizeof(matches[0]), SortStrcmp); diff --git a/src/refresh/images.c b/src/refresh/images.c index 6394f5c63..635e17671 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -46,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define R_COLORMAP_PCX "pics/colormap.pcx" #define IMG_LOAD(x) \ - static int IMG_Load##x(byte *rawdata, size_t rawlen, \ + static int IMG_Load##x(const byte *rawdata, size_t rawlen, \ image_t *image, byte **pic) static bool check_image_size(unsigned w, unsigned h) @@ -213,11 +213,11 @@ static int uncompress_pcx(const byte *raw, const byte *end, return Q_ERR_SUCCESS; } -static int load_pcx(byte *rawdata, size_t rawlen, byte **pixels_p, +static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, byte *palette, int *width, int *height) { - dpcx_t *pcx; - int w, h, scan; + const dpcx_t *pcx; + int w, h, scan; // // parse the PCX file @@ -226,7 +226,7 @@ static int load_pcx(byte *rawdata, size_t rawlen, byte **pixels_p, return Q_ERR_FILE_TOO_SMALL; } - pcx = (dpcx_t *)rawdata; + pcx = (const dpcx_t *)rawdata; if (pcx->manufacturer != 10 || pcx->version != 5) { return Q_ERR_UNKNOWN_FORMAT; @@ -320,14 +320,14 @@ WAL LOADING IMG_LOAD(WAL) { - miptex_t *mt; - unsigned w, h, offset; + const miptex_t *mt; + unsigned w, h, offset; if (rawlen < sizeof(miptex_t)) { return Q_ERR_FILE_TOO_SMALL; } - mt = (miptex_t *)rawdata; + mt = (const miptex_t *)rawdata; w = LittleLong(mt->width); h = LittleLong(mt->height); @@ -346,7 +346,7 @@ IMG_LOAD(WAL) image->upload_width = image->width = w; image->upload_height = image->height = h; - image->flags |= IMG_Unpack8((uint32_t *)*pic, (uint8_t *)mt + offset, w, h); + image->flags |= IMG_Unpack8((uint32_t *)*pic, rawdata + offset, w, h); return Q_ERR_SUCCESS; } @@ -561,7 +561,7 @@ IMG_LOAD(TGA) return Q_ERR_SUCCESS; } -static int IMG_SaveTGA(screenshot_t *restrict s) +static int IMG_SaveTGA(const screenshot_t *restrict s) { byte header[TARGA_HEADER_SIZE] = { 0 }; @@ -646,7 +646,7 @@ static void my_error_exit(j_common_ptr cinfo) longjmp(jerr->setjmp_buffer, 1); } -static int my_jpeg_start_decompress(j_decompress_ptr cinfo, byte *rawdata, size_t rawlen) +static int my_jpeg_start_decompress(j_decompress_ptr cinfo, const byte *rawdata, size_t rawlen) { my_error_ptr jerr = (my_error_ptr)cinfo->err; @@ -733,7 +733,7 @@ IMG_LOAD(JPG) return ret; } -static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, screenshot_t *s) +static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, const screenshot_t *s) { my_error_ptr jerr = (my_error_ptr)cinfo->err; @@ -759,7 +759,7 @@ static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, scree return 0; } -static int IMG_SaveJPG(screenshot_t *restrict s) +static int IMG_SaveJPG(const screenshot_t *restrict s) { struct jpeg_compress_struct cinfo; struct my_error_mgr jerr; @@ -798,7 +798,7 @@ PNG IMAGES #if USE_PNG typedef struct { - png_bytep next_in; + png_const_bytep next_in; png_size_t avail_in; } my_png_io; @@ -977,7 +977,7 @@ IMG_LOAD(PNG) } static int my_png_write_image(png_structp png_ptr, png_infop info_ptr, - png_bytepp row_pointers, screenshot_t *s) + png_bytepp row_pointers, const screenshot_t *s) { my_png_error *err = png_get_error_ptr(png_ptr); @@ -994,7 +994,7 @@ static int my_png_write_image(png_structp png_ptr, png_infop info_ptr, return 0; } -static int IMG_SavePNG(screenshot_t *restrict s) +static int IMG_SavePNG(const screenshot_t *restrict s) { png_structp png_ptr; png_infop info_ptr; @@ -1345,7 +1345,7 @@ uint32_t d_8to24table[256]; static const struct { char ext[4]; - int (*load)(byte *, size_t, image_t *, byte **); + int (*load)(const byte *, size_t, image_t *, byte **); } img_loaders[IM_MAX] = { { "pcx", IMG_LoadPCX }, { "wal", IMG_LoadWAL }, diff --git a/src/refresh/images.h b/src/refresh/images.h index cc63beea9..5b9b48a2e 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -94,7 +94,7 @@ void IMG_Load(image_t *image, byte *pic); struct screenshot_s; -typedef int (*save_cb_t)(struct screenshot_s *restrict); +typedef int (*save_cb_t)(const struct screenshot_s *restrict); typedef struct screenshot_s { save_cb_t save_cb; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 86b284a80..b753eca02 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -538,7 +538,7 @@ POLYGONS BUILDING (double)(x)[1]*(y)[1]+\ (double)(x)[2]*(y)[2]) -static uint32_t color_for_surface(mface_t *surf) +static uint32_t color_for_surface(const mface_t *surf) { if (surf->drawflags & SURF_TRANS33) return gl_static.inverse_intensity_33; @@ -763,7 +763,7 @@ static void build_surface_light(mface_t *surf, vec_t *vbo) } // normalizes and stores lightmap texture coordinates in vertices -static void normalize_surface_lmtc(mface_t *surf, vec_t *vbo) +static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) { float s, t; int i; @@ -783,7 +783,7 @@ static void normalize_surface_lmtc(mface_t *surf, vec_t *vbo) // duplicates normalized texture0 coordinates for non-lit surfaces in texture1 // to make them render properly when gl_lightmap hack is used -static void duplicate_surface_lmtc(mface_t *surf, vec_t *vbo) +static void duplicate_surface_lmtc(const mface_t *surf, vec_t *vbo) { int i; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index f0423ac7f..d14c9e054 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -233,7 +233,7 @@ static void IMG_ResampleTexture(const byte *in, int inwidth, int inheight, } } -static void IMG_MipMap(byte *out, byte *in, int width, int height) +static void IMG_MipMap(byte *out, const byte *in, int width, int height) { int i, j; @@ -419,10 +419,10 @@ static void GL_ColorInvertTexture(byte *in, int inwidth, int inheight, imagetype } } -static bool GL_TextureHasAlpha(byte *data, int width, int height) +static bool GL_TextureHasAlpha(const byte *data, int width, int height) { int i, c; - byte *scan; + const byte *scan; c = width * height; scan = data + 3; diff --git a/src/server/ac.c b/src/server/ac.c index e55cf6cbe..7dc704bc0 100644 --- a/src/server/ac.c +++ b/src/server/ac.c @@ -1094,7 +1094,7 @@ void AC_ClientAnnounce(client_t *cl) } } -char *AC_ClientConnect(client_t *cl) +const char *AC_ClientConnect(client_t *cl) { if (!ac_required->integer) { return ""; // anticheat is not in use diff --git a/src/server/entities.c b/src/server/entities.c index 05667fc4c..9a7635e14 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -38,11 +38,11 @@ Truncates remainder of entity_packed_t list, patching current frame to make delta compression happy. ============= */ -static bool SV_TruncPacketEntities(client_t *client, - client_frame_t *from, - client_frame_t *to, - int oldindex, - int newindex) +static bool SV_TruncPacketEntities(const client_t *client, + const client_frame_t *from, + client_frame_t *to, + int oldindex, + int newindex) { entity_packed_t *newent; const entity_packed_t *oldent; @@ -134,11 +134,11 @@ SV_EmitPacketEntities Writes a delta update of an entity_packed_t list to the message. ============= */ -static bool SV_EmitPacketEntities(client_t *client, - client_frame_t *from, - client_frame_t *to, - int clientEntityNum, - unsigned maxsize) +static bool SV_EmitPacketEntities(const client_t *client, + const client_frame_t *from, + client_frame_t *to, + int clientEntityNum, + unsigned maxsize) { entity_packed_t *newent; const entity_packed_t *oldent; diff --git a/src/server/main.c b/src/server/main.c index ee3fcec29..37736523b 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -371,7 +371,7 @@ void SV_RateInit(ratelimit_t *r, const char *s) r->cost = rate2credits(rate); } -addrmatch_t *SV_MatchAddress(list_t *list, netadr_t *addr) +addrmatch_t *SV_MatchAddress(const list_t *list, const netadr_t *addr) { addrmatch_t *match; @@ -1341,14 +1341,14 @@ int SV_CountClients(void) return count; } -static int ping_nop(client_t *cl) +static int ping_nop(const client_t *cl) { return 0; } -static int ping_min(client_t *cl) +static int ping_min(const client_t *cl) { - client_frame_t *frame; + const client_frame_t *frame; int i, j, count = INT_MAX; for (i = 0; i < UPDATE_BACKUP; i++) { @@ -1365,9 +1365,9 @@ static int ping_min(client_t *cl) return count == INT_MAX ? 0 : count; } -static int ping_avg(client_t *cl) +static int ping_avg(const client_t *cl) { - client_frame_t *frame; + const client_frame_t *frame; int i, j, total = 0, count = 0; for (i = 0; i < UPDATE_BACKUP; i++) { @@ -1394,7 +1394,7 @@ Updates the cl->ping and cl->fps variables static void SV_CalcPings(void) { client_t *cl; - int (*calc)(client_t *); + int (*calc)(const client_t *); int res; switch (sv_calcpings_method->integer) { @@ -1597,7 +1597,7 @@ static void update_client_mtu(client_t *client, int ee_info) SV_ErrorEvent ================= */ -void SV_ErrorEvent(netadr_t *from, int ee_errno, int ee_info) +void SV_ErrorEvent(const netadr_t *from, int ee_errno, int ee_info) { client_t *client; netchan_t *netchan; diff --git a/src/server/mvd.c b/src/server/mvd.c index bbef429ee..19b38c2cd 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -1154,7 +1154,7 @@ void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable) // Performs some basic filtering of the unicast data that would be // otherwise discarded by the MVD client. -static bool filter_unicast_data(edict_t *ent) +static bool filter_unicast_data(const edict_t *ent) { int cmd = msg_write.data[0]; @@ -1189,7 +1189,7 @@ static bool filter_unicast_data(edict_t *ent) SV_MvdUnicast ============== */ -void SV_MvdUnicast(edict_t *ent, int clientNum, bool reliable) +void SV_MvdUnicast(const edict_t *ent, int clientNum, bool reliable) { mvd_ops_t op; sizebuf_t *buf; @@ -1446,7 +1446,7 @@ static void write_message(gtv_client_t *client, gtv_serverop_t op) write_stream(client, msg_write.data, msg_write.cursize); } -static bool auth_client(gtv_client_t *client, const char *password) +static bool auth_client(const gtv_client_t *client, const char *password) { if (SV_MatchAddress(>v_white_list, &client->stream.address)) return true; // ALLOW whitelisted hosts without password diff --git a/src/server/send.c b/src/server/send.c index 6b2906e9e..7cdd2e66e 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -312,7 +312,7 @@ void SV_Multicast(const vec3_t origin, multicast_t to) } #if USE_ZLIB -static bool can_auto_compress(client_t *client) +static bool can_auto_compress(const client_t *client) { if (!client->has_zlib) return false; @@ -332,7 +332,7 @@ static bool can_auto_compress(client_t *client) return true; } -static int compress_message(client_t *client) +static int compress_message(const client_t *client) { int ret, len; byte *hdr; @@ -501,7 +501,7 @@ static void add_msg_packet(client_t *client, const byte *data, } // check if this entity is present in current client frame -static bool check_entity(client_t *client, int entnum) +static bool check_entity(const client_t *client, int entnum) { const client_frame_t *frame; int left, right; @@ -528,7 +528,7 @@ static bool check_entity(client_t *client, int entnum) } // sounds relative to entities are handled specially -static void emit_snd(client_t *client, const message_packet_t *msg) +static void emit_snd(const client_t *client, const message_packet_t *msg) { int entnum = msg->sendchan >> 3; int flags = msg->flags; @@ -855,7 +855,7 @@ static void finish_frame(client_t *client) } #if USE_DEBUG && USE_FPS -static void check_key_sync(client_t *client) +static void check_key_sync(const client_t *client) { int div = sv.frametime.div / client->framediv; int key1 = !(sv.framenum % sv.frametime.div); diff --git a/src/server/server.h b/src/server/server.h index e7938988e..d73649d7e 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -583,7 +583,7 @@ bool SV_RateLimited(ratelimit_t *r); void SV_RateRecharge(ratelimit_t *r); void SV_RateInit(ratelimit_t *r, const char *s); -addrmatch_t *SV_MatchAddress(list_t *list, netadr_t *address); +addrmatch_t *SV_MatchAddress(const list_t *list, const netadr_t *address); int SV_CountClients(void); @@ -647,7 +647,7 @@ void SV_MvdStatus_f(void); void SV_MvdMapChanged(void); void SV_MvdClientDropped(client_t *client); -void SV_MvdUnicast(edict_t *ent, int clientNum, bool reliable); +void SV_MvdUnicast(const edict_t *ent, int clientNum, bool reliable); void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable); void SV_MvdConfigstring(int index, const char *string, size_t len); void SV_MvdBroadcastPrint(int level, const char *string); @@ -685,7 +685,7 @@ void SV_MvdStop_f(void); // sv_ac.c // #if USE_AC_SERVER -char *AC_ClientConnect(client_t *cl); +const char *AC_ClientConnect(client_t *cl); void AC_ClientDisconnect(client_t *cl); bool AC_ClientBegin(client_t *cl); void AC_ClientAnnounce(client_t *cl); From b562918ed248724bdc44315c04c13122bfd7c5d6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 26 May 2024 23:24:53 +0300 Subject: [PATCH 236/974] Simplify IMG_Save*() prototype. --- src/refresh/images.c | 6 +++--- src/refresh/images.h | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 635e17671..d5f9a5549 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -561,7 +561,7 @@ IMG_LOAD(TGA) return Q_ERR_SUCCESS; } -static int IMG_SaveTGA(const screenshot_t *restrict s) +static int IMG_SaveTGA(const screenshot_t *s) { byte header[TARGA_HEADER_SIZE] = { 0 }; @@ -759,7 +759,7 @@ static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, const return 0; } -static int IMG_SaveJPG(const screenshot_t *restrict s) +static int IMG_SaveJPG(const screenshot_t *s) { struct jpeg_compress_struct cinfo; struct my_error_mgr jerr; @@ -994,7 +994,7 @@ static int my_png_write_image(png_structp png_ptr, png_infop info_ptr, return 0; } -static int IMG_SavePNG(const screenshot_t *restrict s) +static int IMG_SavePNG(const screenshot_t *s) { png_structp png_ptr; png_infop info_ptr; diff --git a/src/refresh/images.h b/src/refresh/images.h index 5b9b48a2e..22cd4fba1 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -92,17 +92,17 @@ image_t *IMG_ForHandle(qhandle_t h); void IMG_Unload(image_t *image); void IMG_Load(image_t *image, byte *pic); -struct screenshot_s; +typedef struct screenshot_s screenshot_t; -typedef int (*save_cb_t)(const struct screenshot_s *restrict); +typedef int (*save_cb_t)(const screenshot_t *); -typedef struct screenshot_s { +struct screenshot_s { save_cb_t save_cb; byte *pixels; FILE *fp; char *filename; int width, height, rowbytes, bpp, status, param; bool async; -} screenshot_t; +}; void IMG_ReadPixels(screenshot_t *s); From 6afea9f9273b42d72cb678f42c7c3742900bc7f6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 27 May 2024 01:08:12 +0300 Subject: [PATCH 237/974] Make wave sound buffer counters unsigned. --- src/windows/wave.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/wave.c b/src/windows/wave.c index dabf31977..b1537783c 100644 --- a/src/windows/wave.c +++ b/src/windows/wave.c @@ -29,7 +29,7 @@ static bool wav_init; // starts at 0 for disabled static int sample16; -static int snd_sent, snd_completed; +static unsigned snd_sent, snd_completed; static HANDLE hData; static HPSTR lpData; From f2e84ea39aa79202e5c0e953bf82f43ba6b3aac7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 28 May 2024 16:23:57 +0300 Subject: [PATCH 238/974] Fix indentation. --- src/server/mvd.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index 19b38c2cd..e786f2c26 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -1429,10 +1429,9 @@ static void write_stream(gtv_client_t *client, void *data, size_t len) } while (z->avail_in); } else #endif - - if (FIFO_Write(fifo, data, len) != len) { - drop_client(client, "overflowed"); - } + if (FIFO_Write(fifo, data, len) != len) { + drop_client(client, "overflowed"); + } } static void write_message(gtv_client_t *client, gtv_serverop_t op) From ff46c2ddff97b0c379c56319e1dcbd15108a48da Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 30 May 2024 15:04:47 +0300 Subject: [PATCH 239/974] Use Q_rand_uniform() in stuff_junk(). --- src/server/user.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/user.c b/src/server/user.c index 66ceaae14..c5dff1d98 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -297,14 +297,12 @@ static void stuff_junk(void) static const char junkchars[] = "!#&'()*+,-./0123456789:<=>?@[\\]^_``````````abcdefghijklmnopqrstuvwxyz|~~~~~~~~~~"; char junk[8][16]; - int i, j, k; + int i, j; for (i = 0; i < 8; i++) { - for (j = 0; j < 15; j++) { - k = Q_rand() % (sizeof(junkchars) - 1); - junk[i][j] = junkchars[k]; - } - junk[i][15] = 0; + for (j = 0; j < 15; j++) + junk[i][j] = junkchars[Q_rand_uniform(sizeof(junkchars) - 1)]; + junk[i][j] = 0; } Q_strlcpy(sv_client->reconnect_var, junk[2], sizeof(sv_client->reconnect_var)); From d05662b6354c2f82892a11e4fee1767d5b6fb82e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 31 May 2024 12:13:40 +0300 Subject: [PATCH 240/974] Define USE_PROTOCOL_EXTENSIONS in config.h. --- meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index c1ff9b937..3665d080f 100644 --- a/meson.build +++ b/meson.build @@ -194,7 +194,7 @@ if system_wide homedir = get_option('homedir') endif -common_args = ['-DHAVE_CONFIG_H', '-DUSE_PROTOCOL_EXTENSIONS=1'] +common_args = ['-DHAVE_CONFIG_H'] if win32 common_args += '-D_USE_MATH_DEFINES' else @@ -463,6 +463,7 @@ config.set10('USE_MD5', get_option('md5')) config.set10('USE_PACKETDUP', get_option('packetdup-hack')) config.set10('USE_TGA', get_option('tga')) config.set10('USE_' + host_machine.endian().to_upper() + '_ENDIAN', true) +config.set10('USE_PROTOCOL_EXTENSIONS', true) configure_file(output: 'config.h', configuration: config) From 075465330e077b6770e45852aa0248c64bfc678c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 1 Jun 2024 17:47:21 +0300 Subject: [PATCH 241/974] Clean up and fix net loopback code. Use 32-bit types. Fix off-by-one error in overrun check. Correctly handle get/send counters wraparound. --- src/common/net/net.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/common/net/net.c b/src/common/net/net.c index 1db8b523f..f97adbebc 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -73,14 +73,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_LOOPBACK 4 typedef struct { - byte data[MAX_PACKETLEN]; - size_t datalen; + byte data[MAX_PACKETLEN]; + unsigned datalen; } loopmsg_t; typedef struct { - loopmsg_t msgs[MAX_LOOPBACK]; - unsigned long get; - unsigned long send; + loopmsg_t msgs[MAX_LOOPBACK]; + unsigned get; + unsigned send; } loopback_t; static loopback_t loopbacks[NS_COUNT]; @@ -543,11 +543,11 @@ static void NET_GetLoopPackets(netsrc_t sock, void (*packet_cb)(void)) loop = &loopbacks[sock]; - if (loop->send - loop->get > MAX_LOOPBACK - 1) { - loop->get = loop->send - MAX_LOOPBACK + 1; + if (loop->send - loop->get > MAX_LOOPBACK) { + loop->get = loop->send - MAX_LOOPBACK; } - while (loop->get < loop->send) { + while (loop->get != loop->send) { loopmsg = &loop->msgs[loop->get & (MAX_LOOPBACK - 1)]; loop->get++; From 5f47fa703587c2c52ea7a61792b079bd36d17d90 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 1 Jun 2024 17:57:34 +0300 Subject: [PATCH 242/974] Rename variable for consistency. --- src/common/net/net.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/net/net.c b/src/common/net/net.c index f97adbebc..4dc6fde78 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -539,7 +539,7 @@ static size_t NET_DnRate_m(char *buffer, size_t size) static void NET_GetLoopPackets(netsrc_t sock, void (*packet_cb)(void)) { loopback_t *loop; - loopmsg_t *loopmsg; + loopmsg_t *msg; loop = &loopbacks[sock]; @@ -548,19 +548,19 @@ static void NET_GetLoopPackets(netsrc_t sock, void (*packet_cb)(void)) } while (loop->get != loop->send) { - loopmsg = &loop->msgs[loop->get & (MAX_LOOPBACK - 1)]; + msg = &loop->msgs[loop->get & (MAX_LOOPBACK - 1)]; loop->get++; - memcpy(msg_read_buffer, loopmsg->data, loopmsg->datalen); + memcpy(msg_read_buffer, msg->data, msg->datalen); - NET_LogPacket(&net_from, "LP recv", loopmsg->data, loopmsg->datalen); + NET_LogPacket(&net_from, "LP recv", msg->data, msg->datalen); if (sock == NS_CLIENT) { - net_rate_rcvd += loopmsg->datalen; + net_rate_rcvd += msg->datalen; } SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = loopmsg->datalen; + msg_read.cursize = msg->datalen; (*packet_cb)(); } From b007439d132d961bd5de99f085e44717f6783f02 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 1 Jun 2024 19:53:26 +0300 Subject: [PATCH 243/974] Update libpng to 1.6.43-2. --- subprojects/libpng.wrap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index ca8a32400..d32e859a4 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -3,10 +3,10 @@ directory = libpng-1.6.43 source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz source_filename = libpng-1.6.43.tar.xz source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c -patch_filename = libpng_1.6.43-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-1/get_patch -patch_hash = 0e995446c607ef2e618fb561929acf91e4bdd8017d2e18a7a3b68ba41da345e6 -wrapdb_version = 1.6.43-1 +patch_filename = libpng_1.6.43-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-2/get_patch +patch_hash = 49951297edf03e81d925ab03726555f09994ad1ed78fb539a269216430eef3da +wrapdb_version = 1.6.43-2 [provide] libpng = libpng_dep From 42e77caf98a98aa66fc5c3a2fb05ec793eede792 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 1 Jun 2024 19:56:50 +0300 Subject: [PATCH 244/974] Const-ify Netchan_ShouldUpdate() argument. --- inc/common/net/chan.h | 2 +- src/common/net/chan.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/common/net/chan.h b/inc/common/net/chan.h index 09d6ede1a..ae291e37e 100644 --- a/inc/common/net/chan.h +++ b/inc/common/net/chan.h @@ -81,7 +81,7 @@ void Netchan_Setup(netchan_t *chan, netsrc_t sock, netchan_type_t type, int Netchan_Transmit(netchan_t *chan, size_t length, const void *data, int numpackets); int Netchan_TransmitNextFragment(netchan_t *chan); bool Netchan_Process(netchan_t *chan); -bool Netchan_ShouldUpdate(netchan_t *chan); +bool Netchan_ShouldUpdate(const netchan_t *chan); void Netchan_Close(netchan_t *chan); #define OOB_PRINT(sock, addr, data) \ diff --git a/src/common/net/chan.c b/src/common/net/chan.c index ef25f947c..b2b8e5e2d 100644 --- a/src/common/net/chan.c +++ b/src/common/net/chan.c @@ -712,7 +712,7 @@ int Netchan_Transmit(netchan_t *chan, size_t length, const void *data, int numpa Netchan_ShouldUpdate ============== */ -bool Netchan_ShouldUpdate(netchan_t *chan) +bool Netchan_ShouldUpdate(const netchan_t *chan) { return chan->message.cursize || chan->reliable_ack_pending From 0d5b451f3f853d610a1414a1bdc6db7bee61c38b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 2 Jun 2024 17:03:03 +0300 Subject: [PATCH 245/974] Rewrite MVD_LinkEdict() to allow non-solid BSP models. Fixes disapperaing trigger_teleport models in remaster. --- src/server/mvd/game.c | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 81a19ef7f..13c2c685d 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -1686,26 +1686,25 @@ MISC GAME FUNCTIONS void MVD_LinkEdict(mvd_t *mvd, edict_t *ent) { - int index; - mmodel_t *cm; - bsp_t *cache = mvd->cm.cache; + int index; + const mmodel_t *mod; + const bsp_t *bsp = mvd->cm.cache; - if (!cache) { + if (!bsp) return; - } - if (ent->s.solid == PACKED_BSP) { - index = ent->s.modelindex; - if (index < 1 || index > cache->nummodels) { - Com_WPrintf("%s: entity %d: bad inline model index: %d\n", - __func__, ent->s.number, index); - return; - } - cm = &cache->models[index - 1]; - VectorCopy(cm->mins, ent->mins); - VectorCopy(cm->maxs, ent->maxs); - ent->solid = SOLID_BSP; - } else if (ent->s.solid) { + index = ent->s.modelindex - 1; + if (index >= MODELINDEX_PLAYER && bsp->nummodels >= MODELINDEX_PLAYER) + index--; + if (index > 0 && index < bsp->nummodels) { + mod = &bsp->models[index]; + VectorCopy(mod->mins, ent->mins); + VectorCopy(mod->maxs, ent->maxs); + if (ent->s.solid == PACKED_BSP) + ent->solid = SOLID_BSP; + else + ent->solid = SOLID_TRIGGER; + } else if (ent->s.solid && ent->s.solid != PACKED_BSP) { if (mvd->csr->extended) MSG_UnpackSolid32_Ver2(ent->s.solid, ent->mins, ent->maxs); else From 8faf28cc55925f33b9bc9037a32353762acf49dc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 4 Jun 2024 17:54:10 +0300 Subject: [PATCH 246/974] Improve FPS rounding message. Don't print message if value is outside allowed range for a cvar. Also reword the message to make it more clear (see #350). --- src/client/main.c | 88 ++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/client/main.c b/src/client/main.c index 4cfca7bed..84dfbe834 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2594,51 +2594,11 @@ static void cl_flares_changed(cvar_t *self) CL_UpdateFlaresSetting(); } -static inline int fps_to_msec(int fps) -{ -#if 0 - return (1000 + fps / 2) / fps; -#else - return 1000 / fps; -#endif -} - -static void warn_on_fps_rounding(cvar_t *cvar) -{ - static bool warned = false; - int msec, real_maxfps; - - if (cvar->integer <= 0 || cl_warn_on_fps_rounding->integer <= 0) - return; - - msec = fps_to_msec(cvar->integer); - if (!msec) - return; - - real_maxfps = 1000 / msec; - if (cvar->integer == real_maxfps) - return; - - Com_WPrintf("%s value `%d' is inexact, using `%d' instead.\n", - cvar->name, cvar->integer, real_maxfps); - if (!warned) { - Com_Printf("(Set `%s' to `0' to disable this warning.)\n", - cl_warn_on_fps_rounding->name); - warned = true; - } -} - static void cl_sync_changed(cvar_t *self) { CL_UpdateFrameTimes(); } -static void cl_maxfps_changed(cvar_t *self) -{ - CL_UpdateFrameTimes(); - warn_on_fps_rounding(self); -} - // allow downloads to be permanently disabled as a // protection measure from malicious (or just stupid) servers // that force downloads by stuffing commands @@ -2762,11 +2722,11 @@ static void CL_InitLocal(void) cl_kickangles = Cvar_Get("cl_kickangles", "1", CVAR_CHEAT); cl_warn_on_fps_rounding = Cvar_Get("cl_warn_on_fps_rounding", "1", 0); cl_maxfps = Cvar_Get("cl_maxfps", "62", 0); - cl_maxfps->changed = cl_maxfps_changed; + cl_maxfps->changed = cl_sync_changed; cl_async = Cvar_Get("cl_async", "1", 0); cl_async->changed = cl_sync_changed; r_maxfps = Cvar_Get("r_maxfps", "0", 0); - r_maxfps->changed = cl_maxfps_changed; + r_maxfps->changed = cl_sync_changed; cl_autopause = Cvar_Get("cl_autopause", "1", 0); cl_rollhack = Cvar_Get("cl_rollhack", "1", 0); cl_noglow = Cvar_Get("cl_noglow", "0", 0); @@ -2777,8 +2737,6 @@ static void CL_InitLocal(void) com_timedemo->changed = cl_sync_changed; CL_UpdateFrameTimes(); - warn_on_fps_rounding(cl_maxfps); - warn_on_fps_rounding(r_maxfps); #if USE_DEBUG cl_shownet = Cvar_Get("cl_shownet", "0", 0); @@ -3121,12 +3079,50 @@ static sync_mode_t sync_mode; #define MIN_REF_HZ MIN_PHYS_HZ #define MAX_REF_HZ 1000 +static inline int fps_to_msec(int fps) +{ + return 1000 / fps; +} + +static void warn_on_fps_rounding(const cvar_t *cvar, int msec) +{ + static bool warned = false; + int real_maxfps; + + if (cl_warn_on_fps_rounding->integer <= 0) + return; + + if (!msec) + return; + + real_maxfps = 1000 / msec; + if (cvar->integer == real_maxfps) + return; + + Com_WPrintf("%s value `%d' is inexact and will be rounded to `%d'.\n", + cvar->name, cvar->integer, real_maxfps); + if (!warned) { + Com_Printf("(Set `%s' to `0' to disable this warning.)\n", + cl_warn_on_fps_rounding->name); + warned = true; + } +} + static int fps_to_clamped_msec(cvar_t *cvar, int min, int max) { + int msec; + if (cvar->integer == 0) return fps_to_msec(max); - else - return fps_to_msec(Cvar_ClampInteger(cvar, min, max)); + + msec = fps_to_msec(Cvar_ClampInteger(cvar, min, max)); + + if (cvar->modified) { + warn_on_fps_rounding(cvar, msec); + cvar->modified = false; + } + + return msec; } /* From 8f1fbb67ca729a25d3dc107df1ec23dcb2344885 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 4 Jun 2024 18:23:59 +0300 Subject: [PATCH 247/974] Move some checks inside allow_stufftext(). --- src/client/main.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client/main.c b/src/client/main.c index 84dfbe834..d1bca4281 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2497,10 +2497,16 @@ static bool allow_stufftext(const char *text) { string_entry_t *entry; + if (cl_ignore_stufftext->integer <= 0) + return true; + for (entry = cls.stufftextwhitelist; entry; entry = entry->next) if (Com_WildCmp(entry->string, text)) return true; + if (cl_ignore_stufftext->integer >= 2) + Com_WPrintf("Ignored stufftext: %s\n", text); + return false; } @@ -2547,9 +2553,8 @@ static void exec_server_string(cmdbuf_t *buf, const char *text) return; } - if (cl_ignore_stufftext->integer >= 1 && !allow_stufftext(text)) { - if (cl_ignore_stufftext->integer >= 2) - Com_WPrintf("Ignored stufftext: %s\n", text); + // optional whitelist filtering + if (!allow_stufftext(text)) { return; } From 6173f7e239897cfd4aa06fd681dad4c6288f4849 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 5 Jun 2024 18:55:44 +0300 Subject: [PATCH 248/974] Allow saving system console command history. --- doc/server.asciidoc | 5 +++++ inc/common/common.h | 1 + inc/system/system.h | 4 ++++ src/common/common.c | 3 +++ src/common/prompt.c | 4 ++++ src/unix/tty.c | 19 +++++++++++++++++++ src/windows/system.c | 19 +++++++++++++++++++ 7 files changed, 55 insertions(+) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index a53d14745..bdcc05a7b 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -515,6 +515,11 @@ sys_console:: is a terminal - 2 — enable command line editing and colored text output +sys_history:: + Specifies how many lines to save into system console history file before + exiting Q2PRO, to be reloaded on next startup. Maximum number of history + lines is 128. Default value is 0. + .System console key bindings **************************** The following key bindings are available in Windows console and in TTY console diff --git a/inc/common/common.h b/inc/common/common.h index 681c20e22..bcae8e390 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -47,6 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc., // FIXME: rename these #define COM_HISTORYFILE_NAME ".conhistory" #define COM_DEMOCACHE_NAME ".democache" +#define SYS_HISTORYFILE_NAME ".syshistory" #define MAXPRINTMSG 4096 #define MAXERRORMSG 1024 diff --git a/inc/system/system.h b/inc/system/system.h index 6d021d721..dc662f264 100644 --- a/inc/system/system.h +++ b/inc/system/system.h @@ -47,12 +47,16 @@ void Sys_ConsoleOutput(const char *text, size_t len); void Sys_SetConsoleTitle(const char *title); void Sys_SetConsoleColor(color_index_t color); void Sys_Printf(const char *fmt, ...) q_printf(1, 2); +void Sys_LoadHistory(void); +void Sys_SaveHistory(void); #else #define Sys_RunConsole() (void)0 #define Sys_ConsoleOutput(text, len) (void)0 #define Sys_SetConsoleTitle(title) (void)0 #define Sys_SetConsoleColor(color) (void)0 #define Sys_Printf(...) (void)0 +#define Sys_LoadHistory() (void)0 +#define Sys_SaveHistory() (void)0 #endif q_noreturn q_printf(1, 2) diff --git a/src/common/common.c b/src/common/common.c index 08ba88270..ce7c28208 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -554,6 +554,7 @@ void Com_Error(error_type_t code, const char *fmt, ...) SV_Shutdown(va("Server fatal crashed: %s\n", com_errorMsg), ERR_FATAL); CL_Shutdown(); NET_Shutdown(); + Sys_SaveHistory(); logfile_close(); FS_Shutdown(); @@ -598,6 +599,7 @@ void Com_Quit(const char *reason, error_type_t type) SV_Shutdown(buffer, type); CL_Shutdown(); NET_Shutdown(); + Sys_SaveHistory(); logfile_close(); FS_Shutdown(); Com_ShutdownAsyncWork(); @@ -969,6 +971,7 @@ void Qcommon_Init(int argc, char **argv) SV_Init(); CL_Init(); TST_Init(); + Sys_LoadHistory(); Sys_RunConsole(); diff --git a/src/common/prompt.c b/src/common/prompt.c index 3e3ed11b7..703dd25f6 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -510,6 +510,10 @@ void Prompt_SaveHistory(commandPrompt_t *prompt, const char *filename, int lines char *s; int i; + if (!prompt->inputLineNum) { + return; + } + FS_OpenFile(filename, &f, FS_MODE_WRITE | FS_PATH_BASE); if (!f) { return; diff --git a/src/unix/tty.c b/src/unix/tty.c index ee3a9fba1..5ee689127 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -44,6 +44,7 @@ enum { }; static cvar_t *sys_console; +static cvar_t *sys_history; static bool tty_enabled; static struct termios tty_orig; @@ -583,6 +584,24 @@ void tty_shutdown_input(void) Cvar_Set("sys_console", "0"); } +void Sys_LoadHistory(void) +{ + if (!tty_enabled) { + return; + } + sys_history = Cvar_Get("sys_history", "0", 0); + if (sys_history->integer > 0) { + Prompt_LoadHistory(&tty_prompt, SYS_HISTORYFILE_NAME); + } +} + +void Sys_SaveHistory(void) +{ + if (sys_history && sys_history->integer > 0) { + Prompt_SaveHistory(&tty_prompt, SYS_HISTORYFILE_NAME, sys_history->integer); + } +} + void Sys_RunConsole(void) { char text[MAX_STRING_CHARS]; diff --git a/src/windows/system.c b/src/windows/system.c index 0423f1d7e..e0de87265 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -64,6 +64,7 @@ static HANDLE houtput = INVALID_HANDLE_VALUE; static commandPrompt_t sys_con; static int sys_hidden; static bool gotConsole; +static cvar_t *sys_history; static void write_console_data(const char *data, size_t len) { @@ -490,6 +491,24 @@ void Sys_RunConsole(void) } } +void Sys_LoadHistory(void) +{ + if (!gotConsole) { + return; + } + sys_history = Cvar_Get("sys_history", "0", 0); + if (sys_history->integer > 0) { + Prompt_LoadHistory(&sys_con, SYS_HISTORYFILE_NAME); + } +} + +void Sys_SaveHistory(void) +{ + if (sys_history && sys_history->integer > 0) { + Prompt_SaveHistory(&sys_con, SYS_HISTORYFILE_NAME, sys_history->integer); + } +} + #define FOREGROUND_BLACK 0 #define FOREGROUND_WHITE (FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_RED) From 3eefdd487ca7a563449770a83de4bf5ed7b38c43 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 5 Jun 2024 20:43:13 +0300 Subject: [PATCH 249/974] Avoid signed integer overflow in prompt history code. --- inc/common/prompt.h | 6 ++--- src/common/prompt.c | 63 ++++++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/inc/common/prompt.h b/inc/common/prompt.h index dc2f23454..1e27608ba 100644 --- a/inc/common/prompt.h +++ b/inc/common/prompt.h @@ -28,8 +28,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_MATCHES 250000000 typedef struct { - int inputLineNum; - int historyLineNum; + unsigned inputLineNum; + unsigned historyLineNum; inputField_t inputLine; char *history[HISTORY_SIZE]; @@ -50,5 +50,5 @@ char *Prompt_Action(commandPrompt_t *prompt); void Prompt_HistoryUp(commandPrompt_t *prompt); void Prompt_HistoryDown(commandPrompt_t *prompt); void Prompt_Clear(commandPrompt_t *prompt); -void Prompt_SaveHistory(commandPrompt_t *prompt, const char *filename, int lines); +void Prompt_SaveHistory(const commandPrompt_t *prompt, const char *filename, int lines); void Prompt_LoadHistory(commandPrompt_t *prompt, const char *filename); diff --git a/src/common/prompt.c b/src/common/prompt.c index 703dd25f6..18c7990aa 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -344,8 +344,8 @@ void Prompt_CompleteCommand(commandPrompt_t *prompt, bool backslash) void Prompt_CompleteHistory(commandPrompt_t *prompt, bool forward) { - char *s, *m = NULL; - int i, j; + const char *s, *m = NULL; + unsigned i, j; if (!prompt->search) { s = prompt->inputLine.text; @@ -359,7 +359,11 @@ void Prompt_CompleteHistory(commandPrompt_t *prompt, bool forward) } if (forward) { - for (i = prompt->historyLineNum + 1; i < prompt->inputLineNum; i++) { + j = prompt->inputLineNum; + if (prompt->historyLineNum == j) { + return; + } + for (i = prompt->historyLineNum + 1; i != j; i++) { s = prompt->history[i & HISTORY_MASK]; if (s && strstr(s, prompt->search)) { if (strcmp(s, prompt->inputLine.text)) { @@ -370,10 +374,10 @@ void Prompt_CompleteHistory(commandPrompt_t *prompt, bool forward) } } else { j = prompt->inputLineNum - HISTORY_SIZE; - if (j < 0) { - j = 0; + if (prompt->historyLineNum == j) { + return; } - for (i = prompt->historyLineNum - 1; i >= j; i--) { + for (i = prompt->historyLineNum - 1; i != j; i--) { s = prompt->history[i & HISTORY_MASK]; if (s && strstr(s, prompt->search)) { if (strcmp(s, prompt->inputLine.text)) { @@ -389,7 +393,7 @@ void Prompt_CompleteHistory(commandPrompt_t *prompt, bool forward) } prompt->historyLineNum = i; - IF_Replace(&prompt->inputLine, prompt->history[i & HISTORY_MASK]); + IF_Replace(&prompt->inputLine, m); } void Prompt_ClearState(commandPrompt_t *prompt) @@ -407,7 +411,7 @@ User just pressed enter */ char *Prompt_Action(commandPrompt_t *prompt) { - char *s = prompt->inputLine.text; + const char *s = prompt->inputLine.text; int i, j; Prompt_ClearState(prompt); @@ -454,7 +458,7 @@ void Prompt_HistoryUp(commandPrompt_t *prompt) } if (prompt->inputLineNum - prompt->historyLineNum < HISTORY_SIZE && - prompt->historyLineNum > 0) { + prompt->history[(prompt->historyLineNum - 1) & HISTORY_MASK]) { prompt->historyLineNum--; } @@ -504,16 +508,20 @@ void Prompt_Clear(commandPrompt_t *prompt) IF_Clear(&prompt->inputLine); } -void Prompt_SaveHistory(commandPrompt_t *prompt, const char *filename, int lines) +void Prompt_SaveHistory(const commandPrompt_t *prompt, const char *filename, int lines) { qhandle_t f; - char *s; - int i; + const char *s; + unsigned i; if (!prompt->inputLineNum) { return; } + if (lines < 1) { + return; + } + FS_OpenFile(filename, &f, FS_MODE_WRITE | FS_PATH_BASE); if (!f) { return; @@ -523,11 +531,7 @@ void Prompt_SaveHistory(commandPrompt_t *prompt, const char *filename, int lines lines = HISTORY_SIZE; } - i = prompt->inputLineNum - lines; - if (i < 0) { - i = 0; - } - for (; i < prompt->inputLineNum; i++) { + for (i = prompt->inputLineNum - lines; i != prompt->inputLineNum; i++) { s = prompt->history[i & HISTORY_MASK]; if (s && *s) { FS_FPrintf(f, "%s\n", s); @@ -541,28 +545,27 @@ void Prompt_LoadHistory(commandPrompt_t *prompt, const char *filename) { char buffer[MAX_FIELD_TEXT]; qhandle_t f; - int i; + unsigned i; FS_OpenFile(filename, &f, FS_MODE_READ | FS_TYPE_REAL | FS_PATH_BASE); if (!f) { return; } - for (i = 0; i < HISTORY_SIZE; i++) { - while (1) { - int len = FS_ReadLine(f, buffer, sizeof(buffer)); - if (len <= 0) - goto out; - if (buffer[len - 1] == '\n') - buffer[len - 1] = 0; - if (buffer[0]) - break; + i = 0; + while (1) { + int len = FS_ReadLine(f, buffer, sizeof(buffer)); + if (len <= 0) + break; + if (buffer[len - 1] == '\n') + buffer[len - 1] = 0; + if (buffer[0]) { + Z_Free(prompt->history[i & HISTORY_MASK]); + prompt->history[i & HISTORY_MASK] = Z_CopyString(buffer); + i++; } - Z_Free(prompt->history[i]); - prompt->history[i] = Z_CopyString(buffer); } -out: FS_CloseFile(f); prompt->historyLineNum = i; From 335861024e4e4cc21cff659a85e42c34c6edf792 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 7 Jun 2024 20:06:30 +0300 Subject: [PATCH 250/974] =?UTF-8?q?Forbid=20=E2=80=98begin=E2=80=99=20if?= =?UTF-8?q?=20there=20is=20no=20map=20loaded.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Otherwise game will crash with ‘Couldn't find spawn point’ error on ClientBegin(). --- src/server/user.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/user.c b/src/server/user.c index c5dff1d98..233649101 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -502,6 +502,10 @@ void SV_Begin_f(void) Com_DPrintf("Begin not valid -- already spawned\n"); return; } + if (sv.state == ss_pic || sv.state == ss_cinematic) { + Com_DPrintf("Begin not valid -- map not loaded\n"); + return; + } if (!sv_client->version_string) { SV_DropClient(sv_client, "!failed version probe"); From 5ebce0191dd31f8dbeb4f844737ce241987a99d5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 7 Jun 2024 22:54:29 +0300 Subject: [PATCH 251/974] Allocate packet entities per client. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Size of packet entities circular buffer must be power of two to handle entity counters wraparound correctly. With globally allocated buffer this is only the case when ‘maxclients’ is power of two. To make it work with any ‘maxclients’, switch to per-client packet entities allocation. This also minimizes server memory usage if not all client slots are in use. --- src/server/entities.c | 55 ++++++++++++++++++++----------------------- src/server/init.c | 11 ++------- src/server/main.c | 5 +++- src/server/mvd/game.c | 4 ++++ src/server/send.c | 4 ++-- src/server/server.h | 9 +++---- src/server/user.c | 7 ++++++ 7 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 9a7635e14..242452e83 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -38,15 +38,12 @@ Truncates remainder of entity_packed_t list, patching current frame to make delta compression happy. ============= */ -static bool SV_TruncPacketEntities(const client_t *client, - const client_frame_t *from, - client_frame_t *to, - int oldindex, - int newindex) +static bool SV_TruncPacketEntities(client_t *client, const client_frame_t *from, + client_frame_t *to, int oldindex, int newindex) { entity_packed_t *newent; const entity_packed_t *oldent; - int i, oldnum, newnum, from_num_entities, to_num_entities; + int i, oldnum, newnum, entities_mask, from_num_entities, to_num_entities; bool ret = true; if (!sv_trunc_packet_entities->integer || client->netchan.type) @@ -61,21 +58,22 @@ static bool SV_TruncPacketEntities(const client_t *client, from_num_entities = from->num_entities; to_num_entities = to->num_entities; + entities_mask = client->num_entities - 1; oldent = newent = NULL; while (newindex < to->num_entities || oldindex < from_num_entities) { if (newindex >= to->num_entities) { newnum = MAX_EDICTS; } else { - i = (to->first_entity + newindex) % svs.num_entities; - newent = &svs.entities[i]; + i = (to->first_entity + newindex) & entities_mask; + newent = &client->entities[i]; newnum = newent->number; } if (oldindex >= from_num_entities) { oldnum = MAX_EDICTS; } else { - i = (from->first_entity + oldindex) % svs.num_entities; - oldent = &svs.entities[i]; + i = (from->first_entity + oldindex) & entities_mask; + oldent = &client->entities[i]; oldnum = oldent->number; } @@ -91,8 +89,8 @@ static bool SV_TruncPacketEntities(const client_t *client, // remove new entity from frame to->num_entities--; for (i = newindex; i < to->num_entities; i++) { - svs.entities[(to->first_entity + i ) % svs.num_entities] = - svs.entities[(to->first_entity + i + 1) % svs.num_entities]; + client->entities[(to->first_entity + i ) & entities_mask] = + client->entities[(to->first_entity + i + 1) & entities_mask]; } continue; } @@ -107,11 +105,11 @@ static bool SV_TruncPacketEntities(const client_t *client, // insert old entity into frame for (i = to->num_entities - 1; i >= newindex; i--) { - svs.entities[(to->first_entity + i + 1) % svs.num_entities] = - svs.entities[(to->first_entity + i ) % svs.num_entities]; + client->entities[(to->first_entity + i + 1) & entities_mask] = + client->entities[(to->first_entity + i ) & entities_mask]; } - svs.entities[(to->first_entity + newindex) % svs.num_entities] = *oldent; + client->entities[(to->first_entity + newindex) & entities_mask] = *oldent; to->num_entities++; // should never go backwards @@ -123,7 +121,7 @@ static bool SV_TruncPacketEntities(const client_t *client, } } - svs.next_entity = to->first_entity + to_num_entities; + client->next_entity = to->first_entity + to_num_entities; return ret; } @@ -134,11 +132,8 @@ SV_EmitPacketEntities Writes a delta update of an entity_packed_t list to the message. ============= */ -static bool SV_EmitPacketEntities(const client_t *client, - const client_frame_t *from, - client_frame_t *to, - int clientEntityNum, - unsigned maxsize) +static bool SV_EmitPacketEntities(client_t *client, const client_frame_t *from, + client_frame_t *to, int clientEntityNum, unsigned maxsize) { entity_packed_t *newent; const entity_packed_t *oldent; @@ -166,16 +161,16 @@ static bool SV_EmitPacketEntities(const client_t *client, if (newindex >= to->num_entities) { newnum = MAX_EDICTS; } else { - i = (to->first_entity + newindex) % svs.num_entities; - newent = &svs.entities[i]; + i = (to->first_entity + newindex) & (client->num_entities - 1); + newent = &client->entities[i]; newnum = newent->number; } if (oldindex >= from_num_entities) { oldnum = MAX_EDICTS; } else { - i = (from->first_entity + oldindex) % svs.num_entities; - oldent = &svs.entities[i]; + i = (from->first_entity + oldindex) & (client->num_entities - 1); + oldent = &client->entities[i]; oldnum = oldent->number; } @@ -257,7 +252,7 @@ static client_frame_t *get_last_frame(client_t *client) return NULL; } - if (svs.next_entity - frame->first_entity > svs.num_entities) { + if (client->next_entity - frame->first_entity > client->num_entities) { // but entities are too old Com_DPrintf("%s: delta request from out-of-date entities.\n", client->name); return NULL; @@ -571,6 +566,8 @@ void SV_BuildClientFrame(client_t *client) if (!clent->client) return; // not in game yet + Q_assert(client->entities); + // this is the frame we are creating frame = &client->frames[client->framenum & UPDATE_MASK]; frame->number = client->framenum; @@ -629,7 +626,7 @@ void SV_BuildClientFrame(client_t *client) // build up the list of visible entities frame->num_entities = 0; - frame->first_entity = svs.next_entity; + frame->first_entity = client->next_entity; num_edicts = 0; for (e = 1; e < client->ge->num_edicts; e++) { @@ -715,7 +712,7 @@ void SV_BuildClientFrame(client_t *client) e = ent->s.number; // add it to the circular client_entities array - state = &svs.entities[svs.next_entity % svs.num_entities]; + state = &client->entities[client->next_entity & (client->num_entities - 1)]; // optionally customize it if (customize && customize(clent, ent, &temp)) { @@ -760,7 +757,7 @@ void SV_BuildClientFrame(client_t *client) } frame->num_entities++; - svs.next_entity++; + client->next_entity++; } if (need_clientnum_fix) diff --git a/src/server/init.c b/src/server/init.c index 60bf9b1b4..83ecc63af 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -34,6 +34,7 @@ void SV_ClientReset(client_t *client) client->frames_nodelta = 0; client->send_delta = 0; client->suppress_count = 0; + client->next_entity = 0; memset(&client->lastcmd, 0, sizeof(client->lastcmd)); } @@ -132,9 +133,6 @@ void SV_SpawnServer(const mapcmd_t *cmd) client->spawncount = sv.spawncount; } - // reset entity counter - svs.next_entity = 0; - // set framerate parameters set_frame_time(); @@ -344,7 +342,7 @@ If mvd_spawn is non-zero, load the built-in MVD game module. */ void SV_InitGame(unsigned mvd_spawn) { - int i, entnum, max_packet_entities; + int i, entnum; edict_t *ent; client_t *client; @@ -443,11 +441,6 @@ void SV_InitGame(unsigned mvd_spawn) SV_MvdPostInit(); } - // allocate packet entities - max_packet_entities = svs.csr.extended ? MAX_PACKET_ENTITIES : MAX_PACKET_ENTITIES_OLD; - svs.num_entities = sv_maxclients->integer * max_packet_entities * UPDATE_BACKUP; - svs.entities = SV_Mallocz(sizeof(svs.entities[0]) * svs.num_entities); - // send heartbeat very soon svs.last_heartbeat = -(HEARTBEAT_SECONDS - 5) * 1000; svs.heartbeat_index = 0; diff --git a/src/server/main.c b/src/server/main.c index 37736523b..6f86ab2ae 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -160,6 +160,10 @@ void SV_CleanClient(client_t *client) for (i = 0; i < SV_BASELINES_CHUNKS; i++) { Z_Freep(&client->baselines[i]); } + + // free packet entities + Z_Freep(&client->entities); + client->num_entities = 0; } static void print_drop_reason(client_t *client, const char *reason, clstate_t oldstate) @@ -2377,7 +2381,6 @@ void SV_Shutdown(const char *finalmsg, error_type_t type) // free server static data Z_Free(svs.client_pool); - Z_Free(svs.entities); #if USE_ZLIB deflateEnd(&svs.z); Z_Free(svs.z_buffer); diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 13c2c685d..25c730f74 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -774,6 +774,10 @@ void MVD_BroadcastPrintf(mvd_t *mvd, int level, int mask, const char *fmt, ...) static void MVD_SetServerState(client_t *cl, mvd_t *mvd) { + if (cl->csr != mvd->csr) { + Z_Freep(&cl->entities); + cl->num_entities = 0; + } cl->gamedir = mvd->gamedir; cl->mapname = mvd->mapname; cl->configstrings = mvd->configstrings; diff --git a/src/server/send.c b/src/server/send.c index 7cdd2e66e..64e50441e 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -514,8 +514,8 @@ static bool check_entity(const client_t *client, int entnum) int i, j; i = (left + right) / 2; - j = (frame->first_entity + i) % svs.num_entities; - j = svs.entities[j].number; + j = (frame->first_entity + i) & (client->num_entities - 1); + j = client->entities[j].number; if (j < entnum) left = i + 1; else if (j > entnum) diff --git a/src/server/server.h b/src/server/server.h index d73649d7e..b2a2e6a0a 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -350,6 +350,11 @@ typedef struct client_s { // per-client baseline chunks entity_packed_t *baselines[SV_BASELINES_CHUNKS]; + // per-client packet entities + unsigned num_entities; // UPDATE_BACKUP*MAX_PACKET_ENTITIES(_OLD) + unsigned next_entity; // next state to use + entity_packed_t *entities; // [num_entities] + // server state pointers (hack for MVD channels implementation) configstring_t *configstrings; const cs_remap_t *csr; @@ -468,10 +473,6 @@ typedef struct { client_t *client_pool; // [maxclients] - unsigned num_entities; // maxclients*UPDATE_BACKUP*MAX_PACKET_ENTITIES - unsigned next_entity; // next state to use - entity_packed_t *entities; // [num_entities] - #if USE_ZLIB z_stream z; // for compressing messages at once byte *z_buffer; diff --git a/src/server/user.c b/src/server/user.c index 233649101..093cdf343 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -534,6 +534,13 @@ void SV_Begin_f(void) stuff_cmds(&sv_cmdlist_begin); + // allocate packet entities if not done yet + if (!sv_client->entities) { + int max_packet_entities = sv_client->csr->extended ? MAX_PACKET_ENTITIES : MAX_PACKET_ENTITIES_OLD; + sv_client->num_entities = max_packet_entities * UPDATE_BACKUP; + sv_client->entities = SV_Mallocz(sizeof(sv_client->entities[0]) * sv_client->num_entities); + } + // call the game begin function ge->ClientBegin(sv_player); From 7876a8dbcbba08e53d48c57ae50ab6ba222498fa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 8 Jun 2024 17:45:09 +0300 Subject: [PATCH 252/974] Remove unneeded check. --- src/common/prompt.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/common/prompt.c b/src/common/prompt.c index 18c7990aa..ce3ca2e80 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -514,10 +514,6 @@ void Prompt_SaveHistory(const commandPrompt_t *prompt, const char *filename, int const char *s; unsigned i; - if (!prompt->inputLineNum) { - return; - } - if (lines < 1) { return; } From c4c1da3a877ec0fce65536049d73ccaaa08fe793 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 9 Jun 2024 18:37:42 +0300 Subject: [PATCH 253/974] Strip DOS line endings from history file. --- src/common/prompt.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/common/prompt.c b/src/common/prompt.c index ce3ca2e80..55948101a 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -553,13 +553,14 @@ void Prompt_LoadHistory(commandPrompt_t *prompt, const char *filename) int len = FS_ReadLine(f, buffer, sizeof(buffer)); if (len <= 0) break; - if (buffer[len - 1] == '\n') - buffer[len - 1] = 0; - if (buffer[0]) { - Z_Free(prompt->history[i & HISTORY_MASK]); - prompt->history[i & HISTORY_MASK] = Z_CopyString(buffer); - i++; - } + while (len > 0 && (buffer[len - 1] == '\n' || buffer[len - 1] == '\r')) + len--; + if (!len) + continue; + buffer[len] = 0; + Z_Free(prompt->history[i & HISTORY_MASK]); + prompt->history[i & HISTORY_MASK] = Z_CopyString(buffer); + i++; } FS_CloseFile(f); From eab40bc3d43d1feb0cb35e46ddd994ef873f83ad Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 10 Jun 2024 15:30:24 +0300 Subject: [PATCH 254/974] Clean up network address functions. Only define NA_LOOPBACK and NA_BROADCAST when building a client. Ignore NA_BROADCAST when comparing addresses. Always use 64-bit bitops in NET_IsEqualBaseAdrMask(). Make NET_AdrToString() return const char *. --- inc/common/net/net.h | 48 +++++++++++++++++++++----------------------- src/client/main.c | 10 +++++---- src/common/net/net.c | 26 +++++++++++++++--------- src/common/net/win.h | 2 ++ src/server/main.c | 2 +- src/server/mvd.c | 1 - 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/inc/common/net/net.h b/inc/common/net/net.h index 9844cf524..10571beff 100644 --- a/inc/common/net/net.h +++ b/inc/common/net/net.h @@ -51,8 +51,10 @@ struct pollfd; typedef enum { NA_UNSPECIFIED, +#if USE_CLIENT NA_LOOPBACK, NA_BROADCAST, +#endif NA_IP, NA_IP6 } netadrtype_t; @@ -83,7 +85,7 @@ typedef struct { uint32_t scope_id; // IPv6 crap } netadr_t; -typedef enum netstate_e { +typedef enum { NS_DISCONNECTED,// no socket opened NS_CONNECTING, // connect() not yet completed NS_CONNECTED, // may transmit data @@ -99,17 +101,24 @@ typedef struct { fifo_t send; } netstream_t; +#if USE_CLIENT +#define NET_IsLocalAddress(adr) ((adr)->type == NA_LOOPBACK) +#else +#define NET_IsLocalAddress(adr) false +#endif + static inline bool NET_IsEqualAdr(const netadr_t *a, const netadr_t *b) { if (a->type != b->type) { return false; } - switch (a->type) { - case NA_LOOPBACK: + if (NET_IsLocalAddress(a)) { return true; + } + + switch (a->type) { case NA_IP: - case NA_BROADCAST: return a->ip.u32[0] == b->ip.u32[0] && a->port == b->port; case NA_IP6: return !memcmp(a->ip.u8, b->ip.u8, 16) && a->port == b->port; @@ -124,11 +133,12 @@ static inline bool NET_IsEqualBaseAdr(const netadr_t *a, const netadr_t *b) return false; } - switch (a->type) { - case NA_LOOPBACK: + if (NET_IsLocalAddress(a)) { return true; + } + + switch (a->type) { case NA_IP: - case NA_BROADCAST: return a->ip.u32[0] == b->ip.u32[0]; case NA_IP6: return !memcmp(a->ip.u8, b->ip.u8, 16); @@ -149,15 +159,8 @@ static inline bool NET_IsEqualBaseAdrMask(const netadr_t *a, case NA_IP: return !((a->ip.u32[0] ^ b->ip.u32[0]) & m->ip.u32[0]); case NA_IP6: -#if (defined __amd64__) || (defined _M_AMD64) return !(((a->ip.u64[0] ^ b->ip.u64[0]) & m->ip.u64[0]) | ((a->ip.u64[1] ^ b->ip.u64[1]) & m->ip.u64[1])); -#else - return !(((a->ip.u32[0] ^ b->ip.u32[0]) & m->ip.u32[0]) | - ((a->ip.u32[1] ^ b->ip.u32[1]) & m->ip.u32[1]) | - ((a->ip.u32[2] ^ b->ip.u32[2]) & m->ip.u32[2]) | - ((a->ip.u32[3] ^ b->ip.u32[3]) & m->ip.u32[3])); -#endif default: return false; } @@ -165,11 +168,12 @@ static inline bool NET_IsEqualBaseAdrMask(const netadr_t *a, static inline bool NET_IsLanAddress(const netadr_t *adr) { - switch (adr->type) { - case NA_LOOPBACK: + if (NET_IsLocalAddress(adr)) { return true; + } + + switch (adr->type) { case NA_IP: - case NA_BROADCAST: return adr->ip.u8[0] == 127 || adr->ip.u8[0] == 10 || adr->ip.u16[0] == MakeRawShort(192, 168) || adr->ip.u16[0] == MakeRawShort(172, 16); @@ -180,12 +184,6 @@ static inline bool NET_IsLanAddress(const netadr_t *adr) } } -#if USE_CLIENT -#define NET_IsLocalAddress(adr) ((adr)->type == NA_LOOPBACK) -#else -#define NET_IsLocalAddress(adr) false -#endif - void NET_Init(void); void NET_Shutdown(void); void NET_Config(netflag_t flag); @@ -196,11 +194,11 @@ void NET_GetPackets(netsrc_t sock, void (*packet_cb)(void)); bool NET_SendPacket(netsrc_t sock, const void *data, size_t len, const netadr_t *to); -char *NET_AdrToString(const netadr_t *a); +const char *NET_AdrToString(const netadr_t *a); bool NET_StringToAdr(const char *s, netadr_t *a, int default_port); bool NET_StringPairToAdr(const char *host, const char *port, netadr_t *a); -char *NET_BaseAdrToString(const netadr_t *a); +const char *NET_BaseAdrToString(const netadr_t *a); #define NET_StringToBaseAdr(s, a) NET_StringPairToAdr(s, NULL, a) const char *NET_ErrorString(void); diff --git a/src/client/main.c b/src/client/main.c index d1bca4281..d5cbf2f50 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -581,7 +581,7 @@ static void CL_FollowIP_f(void) a = &cls.recent_addr[i & RECENT_MASK]; if (a->type) { - char *s = NET_AdrToString(a); + const char *s = NET_AdrToString(a); Com_Printf("Following %s...\n", s); Cbuf_InsertText(cmd_current, va("connect %s\n", s)); } @@ -1415,16 +1415,18 @@ static void CL_ConnectionlessPacket(void) } if (!strcmp(c, "passive_connect")) { + const char *adr; + if (!cls.passive) { Com_DPrintf("Passive connect received while not connecting. Ignored.\n"); return; } - s = NET_AdrToString(&net_from); - Com_Printf("Received passive connect from %s.\n", s); + adr = NET_AdrToString(&net_from); + Com_Printf("Received passive connect from %s.\n", adr); cls.serverAddress = net_from; cls.serverProtocol = cl_protocol->integer; - Q_strlcpy(cls.servername, s, sizeof(cls.servername)); + Q_strlcpy(cls.servername, adr, sizeof(cls.servername)); cls.passive = false; cls.state = ca_challenging; diff --git a/src/common/net/net.c b/src/common/net/net.c index 4dc6fde78..f01a2038b 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -156,11 +156,13 @@ static size_t NET_NetadrToSockadr(const netadr_t *a, struct sockaddr_storage *s) memset(s, 0, sizeof(*s)); switch (a->type) { +#if USE_CLIENT case NA_BROADCAST: s4->sin_family = AF_INET; s4->sin_addr.s_addr = INADDR_BROADCAST; s4->sin_port = a->port; return sizeof(*s4); +#endif case NA_IP: s4->sin_family = AF_INET; memcpy(&s4->sin_addr, &a->ip, 4); @@ -208,21 +210,23 @@ static void NET_SockadrToNetadr(const struct sockaddr_storage *s, netadr_t *a) } } -char *NET_BaseAdrToString(const netadr_t *a) +const char *NET_BaseAdrToString(const netadr_t *a) { static char s[MAX_QPATH]; switch (a->type) { case NA_UNSPECIFIED: - return strcpy(s, ""); + return ""; +#if USE_CLIENT case NA_LOOPBACK: - return strcpy(s, "loopback"); - case NA_IP: + return "loopback"; case NA_BROADCAST: +#endif + case NA_IP: if (inet_ntop(AF_INET, &a->ip, s, sizeof(s))) return s; else - return strcpy(s, ""); + return ""; case NA_IP6: if (a->scope_id) { struct sockaddr_storage addr; @@ -236,7 +240,7 @@ char *NET_BaseAdrToString(const netadr_t *a) if (inet_ntop(AF_INET6, &a->ip, s, sizeof(s))) return s; else - return strcpy(s, ""); + return ""; default: Q_assert(!"bad address type"); } @@ -249,15 +253,17 @@ char *NET_BaseAdrToString(const netadr_t *a) NET_AdrToString =================== */ -char *NET_AdrToString(const netadr_t *a) +const char *NET_AdrToString(const netadr_t *a) { static char s[MAX_QPATH]; switch (a->type) { case NA_UNSPECIFIED: - return strcpy(s, ""); + return ""; +#if USE_CLIENT case NA_LOOPBACK: - return strcpy(s, "loopback"); + return "loopback"; +#endif default: Q_snprintf(s, sizeof(s), (a->type == NA_IP6) ? "[%s]:%u" : "%s:%u", NET_BaseAdrToString(a), BigShort(a->port)); @@ -837,9 +843,9 @@ bool NET_SendPacket(netsrc_t sock, const void *data, #if USE_CLIENT case NA_LOOPBACK: return NET_SendLoopPacket(sock, data, len, to); + case NA_BROADCAST: #endif case NA_IP: - case NA_BROADCAST: s = udp_sockets[sock]; break; case NA_IP6: diff --git a/src/common/net/win.h b/src/common/net/win.h index ee456ec43..8ed3afddb 100644 --- a/src/common/net/win.h +++ b/src/common/net/win.h @@ -165,9 +165,11 @@ static int os_udp_send(qsocket_t sock, const void *data, if (net_error == WSAEWOULDBLOCK || net_error == WSAEINTR) return NET_AGAIN; +#if USE_CLIENT // some PPP links do not allow broadcasts if (net_error == WSAEADDRNOTAVAIL && to->type == NA_BROADCAST) return NET_AGAIN; +#endif return NET_ERROR; } diff --git a/src/server/main.c b/src/server/main.c index 6f86ab2ae..59fcdc87b 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -818,7 +818,7 @@ static bool parse_enhanced_params(conn_params_t *p) return true; } -static char *userinfo_ip_string(void) +static const char *userinfo_ip_string(void) { // fake up reserved IPv4 address to prevent IPv6 unaware mods from exploding if (net_from.type == NA_IP6 && !(g_features->integer & GMF_IPV6_ADDRESS_AWARE)) { diff --git a/src/server/mvd.c b/src/server/mvd.c index e786f2c26..101fdb782 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -380,7 +380,6 @@ static int dummy_create(void) newcl->state = cs_connected; newcl->AddMessage = dummy_add_message; newcl->edict = EDICT_NUM(number + 1); - newcl->netchan.remote_address.type = NA_LOOPBACK; List_Init(&newcl->entry); From d24ae66a9551e33e05cf1d2971394e9931648aeb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 10 Jun 2024 16:22:25 +0300 Subject: [PATCH 255/974] Print LIGHTGRID_OCTREE as separate BSP feature. --- src/common/bsp.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/bsp.c b/src/common/bsp.c index 2bf7b3f6d..004ae8cd9 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -840,6 +840,7 @@ static void BSP_PrintStats(const bsp_t *bsp) "%8u : lightgrid leafs\n" "%8u : lightgrid samples\n", grid->numstyles, grid->numnodes, grid->numleafs, grid->numsamples); + extended = true; } extended |= bsp->lm_decoupled; #endif @@ -851,6 +852,8 @@ static void BSP_PrintStats(const bsp_t *bsp) #if USE_REF if (bsp->lm_decoupled) Com_Printf(" DECOUPLED_LM"); + if (bsp->lightgrid.numleafs) + Com_Printf(" LIGHTGRID_OCTREE"); #endif Com_Printf("\n"); } From cb4b35c90bbb29e9bc341ac706d0468bf0f565fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 10 Jun 2024 16:32:43 +0300 Subject: [PATCH 256/974] Fix type of variable. --- src/server/mvd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index 101fdb782..e80cf52a0 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -349,7 +349,7 @@ static int dummy_create(void) client_t *newcl; char userinfo[MAX_INFO_STRING * 2]; const char *s; - int allow; + qboolean allow; int number; // do nothing if already created From 47d306c8b3e28868ec52487a1c9a7ff1c1a0213a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 10 Jun 2024 17:15:39 +0300 Subject: [PATCH 257/974] Spawn dummy MVD client as spectator. Hopefully this shouldn't make things worse for MVD-unaware mods. --- src/server/mvd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index e80cf52a0..d05ff20f5 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -339,7 +339,7 @@ static client_t *dummy_find_slot(void) } #define MVD_USERINFO1 \ - "\\name\\[MVDSPEC]\\skin\\male/grunt" + "\\name\\[MVDSPEC]\\skin\\male/grunt\\spectator\\1" #define MVD_USERINFO2 \ "\\mvdspec\\" STRINGIFY(PROTOCOL_VERSION_MVD_CURRENT) "\\ip\\loopback" From 3568c1236132cab149dcf76e0b7accb531ed178b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 11 Jun 2024 22:54:16 +0300 Subject: [PATCH 258/974] Bump default minimum/maximum allowed rate again. 15000 bytes/sec is more appropriate for minimum rate nowadays. Bump maximum allowed rate to 60000. --- doc/server.asciidoc | 4 ++-- src/server/main.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index bdcc05a7b..b392890a4 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -150,12 +150,12 @@ sv_lan_force_rate:: sv_min_rate:: Server clamps minimum value of ‘rate’ userinfo parameter to this value. - Default value is 1500 bytes/sec. This parameter can't be greater than + Default value is 15000 bytes/sec. This parameter can't be greater than ‘sv_max_rate’ value or less than 1500 bytes/sec. sv_max_rate:: Server clamps maximum value of ‘rate’ userinfo parameter to this value. - Default value is 15000 bytes/sec. + Default value is 60000 bytes/sec. sv_calcpings_method:: Specifies the way client pings are calculated. Default ping calculation diff --git a/src/server/main.c b/src/server/main.c index 59fcdc87b..4911ac2aa 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2215,8 +2215,8 @@ void SV_Init(void) sv_pad_packets = Cvar_Get("sv_pad_packets", "0", 0); #endif sv_lan_force_rate = Cvar_Get("sv_lan_force_rate", "0", CVAR_LATCH); - sv_min_rate = Cvar_Get("sv_min_rate", "1500", CVAR_LATCH); - sv_max_rate = Cvar_Get("sv_max_rate", "15000", CVAR_LATCH); + sv_min_rate = Cvar_Get("sv_min_rate", "15000", CVAR_LATCH); + sv_max_rate = Cvar_Get("sv_max_rate", "60000", CVAR_LATCH); sv_max_rate->changed = sv_min_rate->changed = sv_rate_changed; sv_max_rate->changed(sv_max_rate); sv_calcpings_method = Cvar_Get("sv_calcpings_method", "2", 0); From 1d06a052f4fa45a58da3eda176e890d75fce4612 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 11 Jun 2024 23:01:47 +0300 Subject: [PATCH 259/974] Limit maximum command buffer wait count. --- src/common/cmd.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/cmd.c b/src/common/cmd.c index 7751fe832..6bdfc9f0b 100644 --- a/src/common/cmd.c +++ b/src/common/cmd.c @@ -57,7 +57,13 @@ bind g "impulse 5 ; +attack ; wait ; -attack ; impulse 2" static void Cmd_Wait_f(void) { int count = Q_atoi(Cmd_Argv(1)); - cmd_current->waitCount += max(count, 1); + + if (cmd_current->waitCount >= 1000) { + Com_WPrintf("Runaway wait count\n"); + return; + } + + cmd_current->waitCount += Q_clip(count, 1, 1000 - cmd_current->waitCount); } /* From a8bc5fc7df70a5925315e6d18483eabcce04b481 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 11 Jun 2024 23:04:57 +0300 Subject: [PATCH 260/974] Ignore stubbed commands if running dedicated server. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't use them for command completion, or print in ‘cmdlist’ output. Also disable ‘complete’ command if running dedicated server. --- src/common/cmd.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/cmd.c b/src/common/cmd.c index 6bdfc9f0b..d5c5244fe 100644 --- a/src/common/cmd.c +++ b/src/common/cmd.c @@ -1536,8 +1536,11 @@ void Cmd_Command_g(genctx_t *ctx) { cmd_function_t *cmd; - FOR_EACH_CMD(cmd) + FOR_EACH_CMD(cmd) { + if (COM_DEDICATED && !cmd->function) + continue; Prompt_AddMatch(ctx, cmd->name); + } } void Cmd_ExecuteCommand(cmdbuf_t *buf) @@ -1559,7 +1562,7 @@ void Cmd_ExecuteCommand(cmdbuf_t *buf) if (cmd) { if (cmd->function) { cmd->function(); - } else if (!CL_ForwardToServer()) { + } else if (!COM_DEDICATED && !CL_ForwardToServer()) { Com_Printf("Can't \"%s\", not connected\n", cmd_argv[0]); } return; @@ -1847,6 +1850,9 @@ static void Cmd_List_f(void) i = total = 0; FOR_EACH_CMD(cmd) { total++; + if (COM_DEDICATED && !cmd->function) { + continue; + } if (filter && !Com_WildCmp(filter, cmd->name)) { continue; } @@ -1895,6 +1901,9 @@ static void Cmd_Complete_f(void) char *name; size_t len; + if (COM_DEDICATED) + return; + if (cmd_argc < 2) { Com_Printf("Usage: %s ", cmd_argv[0]); return; From 6786aa1d07ff99d3a385e861b7d045ac249fd1e9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 12 Jun 2024 01:02:24 +0300 Subject: [PATCH 261/974] =?UTF-8?q?Allow=20=E2=80=98ignoretext=E2=80=99,?= =?UTF-8?q?=20etc=20commands=20in=20autoexec.cfg.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also stub them when running dedicated server. --- inc/client/client.h | 2 ++ src/client/main.c | 33 ++++++++++++++++++++++++++------- src/client/null.c | 17 ++++++++--------- src/common/common.c | 3 +-- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index 00e793c2b..7b1891d46 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -32,6 +32,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SOUND_LOOPATTENUATE_MULT 0.0006f +void CL_PreInit(void); + #if USE_CLIENT #define MAX_LOCAL_SERVERS 16 diff --git a/src/client/main.c b/src/client/main.c index d5cbf2f50..76d4c2d05 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2649,10 +2649,6 @@ static const cmdreg_t c_client[] = { { "reconnect", CL_Reconnect_f }, { "rcon", CL_Rcon_f, CL_Rcon_c }, { "serverstatus", CL_ServerStatus_f, CL_ServerStatus_c }, - { "ignoretext", CL_IgnoreText_f }, - { "unignoretext", CL_UnIgnoreText_f }, - { "ignorenick", CL_IgnoreNick_f, CL_IgnoreNick_c }, - { "unignorenick", CL_UnIgnoreNick_f, CL_UnIgnoreNick_c }, { "dumpclients", CL_DumpClients_f }, { "dumpstatusbar", CL_DumpStatusbar_f }, { "dumplayout", CL_DumpLayout_f }, @@ -2700,9 +2696,6 @@ static void CL_InitLocal(void) CL_InitDownloads(); CL_GTV_Init(); - List_Init(&cl_ignore_text); - List_Init(&cl_ignore_nick); - Cmd_Register(c_client); for (i = 0; i < MAX_LOCAL_SERVERS; i++) { @@ -2838,6 +2831,32 @@ static void CL_InitLocal(void) Cmd_AddMacro("cl_numentities", CL_NumEntities_m); } +static const cmdreg_t c_ignores[] = { + { "ignoretext", CL_IgnoreText_f }, + { "unignoretext", CL_UnIgnoreText_f }, + { "ignorenick", CL_IgnoreNick_f, CL_IgnoreNick_c }, + { "unignorenick", CL_UnIgnoreNick_f, CL_UnIgnoreNick_c }, + { NULL } +}; + +/* +================= +CL_PreInit + +Called before executing configs to register commands such as `bind' or +`ignoretext'. +================= +*/ +void CL_PreInit(void) +{ + Key_Init(); + + List_Init(&cl_ignore_text); + List_Init(&cl_ignore_nick); + + Cmd_Register(c_ignores); +} + /* ================== CL_CheatsOK diff --git a/src/client/null.c b/src/client/null.c index eb2c4f546..81c2f1705 100644 --- a/src/client/null.c +++ b/src/client/null.c @@ -20,17 +20,16 @@ with this program; if not, write to the Free Software Foundation, Inc., // for pure dedicated servers #include "shared/shared.h" -#include "common/cvar.h" #include "client/client.h" -#include "client/keys.h" -static void Key_Bind_Null_f(void) -{ -} +static const char *const nullcmds[] = { + "bind", "unbind", "unbindall", + "ignoretext", "unignoretext", + "ignorenick", "unignorenick" +}; -void Key_Init(void) +void CL_PreInit(void) { - Cmd_AddCommand("bind", Key_Bind_Null_f); - Cmd_AddCommand("unbind", Key_Bind_Null_f); - Cmd_AddCommand("unbindall", Key_Bind_Null_f); + for (int i = 0; i < q_countof(nullcmds); i++) + Cmd_AddCommand(nullcmds[i], NULL); } diff --git a/src/common/common.c b/src/common/common.c index ce7c28208..a0b43651a 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -45,7 +45,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/zone.h" #include "client/client.h" -#include "client/keys.h" #include "server/server.h" #include "system/system.h" #include "system/hunk.h" @@ -860,7 +859,7 @@ void Qcommon_Init(int argc, char **argv) Cbuf_Init(); Cmd_Init(); Cvar_Init(); - Key_Init(); + CL_PreInit(); Prompt_Init(); Con_Init(); From 5e2be23e9256c4837a1bf57b1b05d0085432834e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 12 Jun 2024 18:41:30 +0300 Subject: [PATCH 262/974] Remove unused enum name. --- inc/client/keys.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/client/keys.h b/inc/client/keys.h index 4f1a3b143..f76a5c431 100644 --- a/inc/client/keys.h +++ b/inc/client/keys.h @@ -112,7 +112,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define K_MWHEELLEFT 213 #define K_MOUSELAST 213 -typedef enum keydest_e { +typedef enum { KEY_GAME = 0, KEY_CONSOLE = BIT(0), KEY_MESSAGE = BIT(1), From 1b5efb7b30de0ae7c1e178f5c98d5737554ea87e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 12 Jun 2024 22:05:59 +0300 Subject: [PATCH 263/974] Convert PATH_ defines into enums. --- inc/common/files.h | 13 ++++++++----- src/client/download.c | 2 +- src/client/http.c | 10 +++++----- src/common/files.c | 13 ++++++------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/inc/common/files.h b/inc/common/files.h index 27edbdba2..1debc7019 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -107,11 +107,14 @@ void FS_FreeList(void **list); size_t FS_NormalizePathBuffer(char *out, const char *in, size_t size); #define FS_NormalizePath(path) FS_NormalizePathBuffer(path, path, SIZE_MAX) -#define PATH_INVALID 0 -#define PATH_VALID 1 -#define PATH_MIXED_CASE 2 - -int FS_ValidatePath(const char *s); +typedef enum { + PATH_NOT_CHECKED = -1, // never returned by FS_ValidatePath() + PATH_INVALID = 0, + PATH_VALID, + PATH_MIXED_CASE +} path_valid_t; + +path_valid_t FS_ValidatePath(const char *s); void FS_CleanupPath(char *s); #ifdef _WIN32 diff --git a/src/client/download.c b/src/client/download.c index 101da45b2..0f53da011 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -453,8 +453,8 @@ bool CL_CheckDownloadExtension(const char *ext) static int check_file_len(const char *path, size_t len, dltype_t type) { char buffer[MAX_QPATH], *ext; + path_valid_t valid; int ret; - int valid; // check for oversize path if (len >= MAX_QPATH) diff --git a/src/client/http.c b/src/client/http.c index f2ad2b25e..0c8c6e017 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -602,11 +602,11 @@ int HTTP_QueueDownload(const char *path, dltype_t type) // Validate a path supplied by a filelist. static void check_and_queue_download(char *path) { - size_t len; - char *ext; - dltype_t type; - unsigned flags; - int valid; + size_t len; + char *ext; + dltype_t type; + unsigned flags; + path_valid_t valid; len = strlen(path); if (len >= MAX_QPATH) diff --git a/src/common/files.c b/src/common/files.c index 4890f6d6e..416d067cb 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -79,8 +79,6 @@ QUAKE FILESYSTEM #define FS_ERR_READ(fp) \ (ferror(fp) ? Q_ERR_FAILURE : Q_ERR_UNEXPECTED_EOF) -#define PATH_NOT_CHECKED -1 - #define FOR_EACH_SYMLINK(link, list) \ LIST_FOR_EACH(symlink_t, link, list, entry) @@ -271,9 +269,9 @@ FS_ValidatePath Checks for bad (OS specific) and mixed case characters in path. ================ */ -int FS_ValidatePath(const char *s) +path_valid_t FS_ValidatePath(const char *s) { - int res = PATH_VALID; + path_valid_t res = PATH_VALID; if (!*s) return PATH_INVALID; @@ -1317,7 +1315,7 @@ static int64_t open_file_read(file_t *file, const char *normalized, size_t namel unsigned hash; packfile_t *entry; int64_t ret; - int valid; + path_valid_t valid; FS_COUNT_READ; @@ -2738,7 +2736,7 @@ void **FS_ListFiles(const char *path, const char *filter, unsigned flags, int *c listfiles_t list; size_t len, pathlen; char *s, *p; - int valid; + path_valid_t valid; memset(&list, 0, sizeof(list)); valid = PATH_NOT_CHECKED; @@ -3051,7 +3049,8 @@ static void FS_WhereIs_f(void) symlink_t *link; unsigned hash; file_info_t info; - int ret, total, valid; + int ret, total; + path_valid_t valid; size_t len, namelen; bool report_all; From 84a0407f821660e6ab59ac1dd1ecd6a848043b04 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 12 Jun 2024 22:22:05 +0300 Subject: [PATCH 264/974] Convert network error defines into enums. --- inc/common/net/net.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/inc/common/net/net.h b/inc/common/net/net.h index 10571beff..f51bb5b97 100644 --- a/inc/common/net/net.h +++ b/inc/common/net/net.h @@ -33,14 +33,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_PACKETLEN_WRITABLE (MAX_PACKETLEN - PACKET_HEADER) #define MAX_PACKETLEN_WRITABLE_DEFAULT (MAX_PACKETLEN_DEFAULT - PACKET_HEADER) -// portable network error codes -#define NET_OK 0 // success -#define NET_ERROR -1 // failure (NET_ErrorString returns error message) -#define NET_AGAIN -2 // operation would block, try again -#define NET_CLOSED -3 // peer has closed connection - -typedef int neterr_t; - #ifdef _WIN32 typedef intptr_t qsocket_t; #else @@ -49,6 +41,14 @@ typedef int qsocket_t; struct pollfd; +// portable network error codes +typedef enum { + NET_OK = 0, // success + NET_ERROR = -1, // failure (NET_ErrorString returns error message) + NET_AGAIN = -2, // operation would block, try again + NET_CLOSED = -3, // peer has closed connection +} neterr_t; + typedef enum { NA_UNSPECIFIED, #if USE_CLIENT From 0c86849c131fb769c8bdec0fa3326b96bce01713 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 13 Jun 2024 15:23:39 +0300 Subject: [PATCH 265/974] Force convert paths in packfiles to lower case. --- src/common/files.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/files.c b/src/common/files.c index 416d067cb..ff971a0fa 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -2064,7 +2064,13 @@ static void pack_calc_hashes(pack_t *pack) pack->file_hash = FS_Mallocz(pack->hash_size * sizeof(pack->file_hash[0])); for (i = 0, file = pack->files; i < pack->num_files; i++, file++) { - unsigned hash = FS_HashPath(pack->names + file->nameofs, pack->hash_size); + char *name = pack->names + file->nameofs; + unsigned hash; + + // force conversion to lower case. mixed case paths are annoying. + Q_strlwr(name); + + hash = Com_HashString(name, pack->hash_size); file->hash_next = pack->file_hash[hash]; pack->file_hash[hash] = file; } From 4bf49a6aa5bf0749d14a9767540016114cee5ff1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 13 Jun 2024 16:11:21 +0300 Subject: [PATCH 266/974] Fix sign extension bug in escape_path(). --- src/client/http.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/http.c b/src/client/http.c index 0c8c6e017..eba4b681b 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -151,7 +151,7 @@ static size_t recv_func(void *ptr, size_t size, size_t nmemb, void *stream) static void escape_path(char *escaped, const char *path) { while (*path) { - int c = *path++; + byte c = *path++; if (!Q_isalnum(c) && !strchr("/-_.~", c)) { sprintf(escaped, "%%%02x", c); escaped += 3; From 03e9d01778e1d7a6395b75a384cd9beab22b660e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 13 Jun 2024 17:40:37 -0400 Subject: [PATCH 267/974] NULL check on T_RadiusDamage for grenade messaging --- src/action/g_combat.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index dce27760c..207018dd7 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1024,16 +1024,17 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, } } + if (attacker){ // Messaging addition - if (ent_count > 3) - gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade, nicely done!\n"); - else if (selfharm && ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, along with %s\n", ent_name_list); - else if (selfharm) - gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); - else if (ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "%s were blasted by your grenade.\n", ent_name_list); - + if (ent_count > 3) + gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade, nicely done!\n"); + else if (selfharm && ent_count > 0) + gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, along with %s\n", ent_name_list); + else if (selfharm) + gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); + else if (ent_count > 0) + gi.cprintf(attacker, PRINT_HIGH, "%s were blasted by your grenade.\n", ent_name_list); + } // Stats for fun, tracks the highest amount of players hit by a single grenade // if (ent_count > attacker->client->resp.grenSplash) // attacker->client->resp.grenSplash = ent_count; From f2acacee49d4178fcd6399124aaffb25c947942e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 13 Jun 2024 17:44:36 -0400 Subject: [PATCH 268/974] Fixed messaging wording --- src/action/g_combat.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 207018dd7..41c3cbb86 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1033,7 +1033,7 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, else if (selfharm) gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); else if (ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "%s were blasted by your grenade.\n", ent_name_list); + gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); } // Stats for fun, tracks the highest amount of players hit by a single grenade // if (ent_count > attacker->client->resp.grenSplash) From 03578135620b51c5e6fcc05901fbcdf8b5585b0c Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 13 Jun 2024 22:40:56 -0400 Subject: [PATCH 269/974] Removed grenSplash stat --- src/action/g_combat.c | 3 --- src/action/g_local.h | 1 - 2 files changed, 4 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 41c3cbb86..bff1d560a 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -1035,7 +1035,4 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, else if (ent_count > 0) gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); } - // Stats for fun, tracks the highest amount of players hit by a single grenade - // if (ent_count > attacker->client->resp.grenSplash) - // attacker->client->resp.grenSplash = ent_count; } diff --git a/src/action/g_local.h b/src/action/g_local.h index 50334719a..2e5610ac9 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1836,7 +1836,6 @@ typedef struct int streakHS; //Headshots in a Row int streakKillsHighest; //Highest kills in a row int streakHSHighest; //Highest headshots in a Row - int grenSplash; //Tracks latest amount of players harmed by a single grenade int hitsLocations[LOC_MAX]; //Number of hits for different locations gunStats_t gunstats[MOD_TOTAL]; //Number of shots/hits for different guns, adjusted to MOD_TOTAL to allow grenade, kick and punch stats From 89aab148f28242bacf850a457e70cb584bde45db Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 15 Jun 2024 10:07:38 -0400 Subject: [PATCH 270/974] Additional checks around grenade damaging messaging --- src/action/g_combat.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index bff1d560a..b249c8cff 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -950,8 +950,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve T_RadiusDamage ============ */ -void -T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, +void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, edict_t * ignore, float radius, int mod) { float points; @@ -1024,8 +1023,11 @@ T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, } } - if (attacker){ - // Messaging addition + // Grenade splash damage messaging + // Checks for attacker being NULL (this causes a cprintf segfault if NULL), and if the attacker is a client + // Also checks if the mod is a grenade splash. The game uses T_RadiusDamage for a variety of damaging effects + // so we only want to print the grenade splash messages if the mod is a grenade splash + if (attacker && attacker->client && mod == MOD_HG_SPLASH){ if (ent_count > 3) gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade, nicely done!\n"); else if (selfharm && ent_count > 0) From 5e7249b887aade231fb4a85212d92491f61b8a5d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 16 Jun 2024 19:11:42 +0300 Subject: [PATCH 271/974] Prefer Xft.dpi to physical screen DPI. --- src/unix/video/x11.c | 64 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/src/unix/video/x11.c b/src/unix/video/x11.c index 0f8fe44c4..297e7c795 100644 --- a/src/unix/video/x11.c +++ b/src/unix/video/x11.c @@ -33,6 +33,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include #include +#include #include #include @@ -219,6 +220,53 @@ static bool choose_fb_config(const r_opengl_config_t *cfg, GLXFBConfig *fbc) return true; } +static bool get_dpi_scale_xft(void) +{ + char *resman = XResourceManagerString(x11.dpy); + if (!resman) + return false; + + XrmInitialize(); + + XrmDatabase db = XrmGetStringDatabase(resman); + if (!db) + return false; + + bool ret = false; + char *type, *end; + XrmValue value; + if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) && + value.addr && !strcmp(type, "String") && *value.addr) { + unsigned long dpi = strtoul(value.addr, &end, 10); + if (dpi && !*end) { + x11.dpi_scale = Q_clip((dpi + 48) / 96, 1, 10); + Com_DPrintf("Using Xft DPI scale: %d\n", x11.dpi_scale); + ret = true; + } + } + XrmDestroyDatabase(db); + return ret; +} + +static void get_dpi_scale_physical(void) +{ + int width = DisplayWidth(x11.dpy, x11.screen); + int height = DisplayHeight(x11.dpy, x11.screen); + int mm_width = DisplayWidthMM(x11.dpy, x11.screen); + int mm_height = DisplayHeightMM(x11.dpy, x11.screen); + + if (mm_width > 0 && mm_height > 0) { + float dpi_x = width * 25.4f / mm_width; + float dpi_y = height * 25.4f / mm_height; + int scale_x = Q_rint(dpi_x / 96.0f); + int scale_y = Q_rint(dpi_y / 96.0f); + if (scale_x == scale_y) { + x11.dpi_scale = Q_clip(scale_x, 1, 10); + Com_DPrintf("Using physical DPI scale: %d\n", x11.dpi_scale); + } + } +} + static bool init(void) { if (!(x11.dpy = XOpenDisplay(NULL))) { @@ -291,20 +339,8 @@ static bool init(void) } x11.dpi_scale = 1; - - int width = DisplayWidth(x11.dpy, x11.screen); - int height = DisplayHeight(x11.dpy, x11.screen); - int mm_width = DisplayWidthMM(x11.dpy, x11.screen); - int mm_height = DisplayHeightMM(x11.dpy, x11.screen); - - if (mm_width > 0 && mm_height > 0) { - float dpi_x = width * 25.4f / mm_width; - float dpi_y = height * 25.4f / mm_height; - int scale_x = Q_rint(dpi_x / 96.0f); - int scale_y = Q_rint(dpi_y / 96.0f); - if (scale_x == scale_y) - x11.dpi_scale = Q_clip(scale_x, 1, 10); - } + if (!get_dpi_scale_xft()) + get_dpi_scale_physical(); XSizeHints hints = { .flags = PMinSize, From 49595b6caded5813f54d033c6bd40a16418ccec7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 16 Jun 2024 19:15:22 +0300 Subject: [PATCH 272/974] Use separate SZ_Init*() funcs for read/write. --- inc/common/sizebuf.h | 3 ++- src/client/demo.c | 17 ++++++----------- src/client/gtv.c | 2 +- src/client/parse.c | 3 +-- src/client/sound/mem.c | 3 +-- src/common/bsp.c | 6 ++---- src/common/cmodel.c | 3 +-- src/common/fifo.c | 5 ++--- src/common/net/chan.c | 8 ++++---- src/common/net/net.c | 6 ++---- src/common/sizebuf.c | 14 ++++++++------ src/server/mvd.c | 4 ++-- src/server/mvd/client.c | 11 +++-------- src/server/save.c | 6 ++---- 14 files changed, 37 insertions(+), 54 deletions(-) diff --git a/inc/common/sizebuf.h b/inc/common/sizebuf.h index 91348c15f..1c49941a6 100644 --- a/inc/common/sizebuf.h +++ b/inc/common/sizebuf.h @@ -31,8 +31,9 @@ typedef struct { const char *tag; // for debugging } sizebuf_t; -void SZ_Init(sizebuf_t *buf, void *data, size_t size); void SZ_TagInit(sizebuf_t *buf, void *data, size_t size, const char *tag); +void SZ_InitWrite(sizebuf_t *buf, void *data, size_t size); +void SZ_InitRead(sizebuf_t *buf, const void *data, size_t size); void SZ_Clear(sizebuf_t *buf); void *SZ_GetSpace(sizebuf_t *buf, size_t len); void SZ_WriteByte(sizebuf_t *sb, int c); diff --git a/src/client/demo.c b/src/client/demo.c index fc3cf4f82..3074f4e63 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -403,7 +403,7 @@ static void CL_Record_f(void) if (cl.csr.extended) size = MAX_MSGLEN; - SZ_Init(&cls.demo.buffer, demo_buffer, size); + SZ_InitWrite(&cls.demo.buffer, demo_buffer, size); // clear dirty configstrings memset(cl.dcs, 0, sizeof(cl.dcs)); @@ -598,15 +598,13 @@ static int read_first_message(qhandle_t f) return Q_ERR_INVALID_FORMAT; } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = msglen; - // read packet data - read = FS_Read(msg_read.data, msglen, f); + read = FS_Read(msg_read_buffer, msglen, f); if (read != msglen) { return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; } + SZ_InitRead(&msg_read, msg_read_buffer, msglen); return type; } @@ -631,15 +629,13 @@ static int read_next_message(qhandle_t f) return Q_ERR_INVALID_FORMAT; } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = msglen; - // read packet data - read = FS_Read(msg_read.data, msglen, f); + read = FS_Read(msg_read_buffer, msglen, f); if (read != msglen) { return read < 0 ? read : Q_ERR_UNEXPECTED_EOF; } + SZ_InitRead(&msg_read, msg_read_buffer, msglen); return 1; } @@ -1061,8 +1057,7 @@ static void CL_Seek_f(void) strcpy(to, from); } - SZ_Init(&msg_read, snap->data, snap->msglen); - msg_read.cursize = snap->msglen; + SZ_InitRead(&msg_read, snap->data, snap->msglen); CL_SeekDemoMessage(); cls.demo.frames_read = snap->framenum; diff --git a/src/client/gtv.c b/src/client/gtv.c index 1c8b6539a..c8c6b1d91 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -257,7 +257,7 @@ void CL_GTV_Resume(void) if (cls.gtv.state != ca_active) return; - SZ_Init(&cls.gtv.message, gtv_message_buffer, sizeof(gtv_message_buffer)); + SZ_InitWrite(&cls.gtv.message, gtv_message_buffer, sizeof(gtv_message_buffer)); build_gamestate(); emit_gamestate(); diff --git a/src/client/parse.c b/src/client/parse.c index 23720dff2..a83f66827 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -1164,8 +1164,7 @@ static void CL_ParseZPacket(void) } temp = msg_read; - SZ_Init(&msg_read, buffer, sizeof(buffer)); - msg_read.cursize = outlen; + SZ_InitRead(&msg_read, buffer, outlen); CL_ParseServerMessage(); diff --git a/src/client/sound/mem.c b/src/client/sound/mem.c index 3a82f71bb..a2d42b016 100644 --- a/src/client/sound/mem.c +++ b/src/client/sound/mem.c @@ -245,8 +245,7 @@ sfxcache_t *S_LoadSound(sfx_t *s) memset(&s_info, 0, sizeof(s_info)); s_info.name = name; - SZ_Init(&sz, data, len); - sz.cursize = len; + SZ_InitRead(&sz, data, len); if (!GetWavinfo(&sz)) { s->error = Q_ERR_INVALID_FORMAT; diff --git a/src/common/bsp.c b/src/common/bsp.c index 004ae8cd9..693854be1 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -1204,8 +1204,7 @@ static size_t BSP_ParseLightgridHeader(bsp_t *bsp, const byte *in, size_t filele lightgrid_t *grid = &bsp->lightgrid; sizebuf_t s; - SZ_Init(&s, (void *)in, filelen); - s.cursize = filelen; + SZ_InitRead(&s, in, filelen); if (!BSP_ParseLightgridHeader_(grid, &s)) { Com_WPrintf("Bad LIGHTGRID_OCTREE header\n"); @@ -1266,8 +1265,7 @@ static void BSP_ParseLightgrid(bsp_t *bsp, const byte *in, size_t filelen) return; } - SZ_Init(&s, (void *)in, filelen); - s.cursize = filelen; + SZ_InitRead(&s, in, filelen); grid->nodes = ALLOC(sizeof(grid->nodes[0]) * grid->numnodes); diff --git a/src/common/cmodel.c b/src/common/cmodel.c index 8f45552fd..31e97f075 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -98,8 +98,7 @@ static void load_binary_override(cm_t *cm, char *server, size_t server_size) goto fail; } - SZ_Init(&sz, data, ret); - sz.cursize = ret; + SZ_InitRead(&sz, data, ret); ret = Q_ERR_INVALID_FORMAT; diff --git a/src/common/fifo.c b/src/common/fifo.c index 63d498fc3..5865df266 100644 --- a/src/common/fifo.c +++ b/src/common/fifo.c @@ -97,13 +97,12 @@ bool FIFO_ReadMessage(fifo_t *fifo, size_t msglen) if (!FIFO_TryRead(fifo, msg_read_buffer, msglen)) { return false; // not yet available } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); + SZ_InitRead(&msg_read, msg_read_buffer, msglen); } else { // read in a single block without copying any memory - SZ_Init(&msg_read, data, msglen); + SZ_InitRead(&msg_read, data, msglen); FIFO_Decommit(fifo, msglen); } - msg_read.cursize = msglen; return true; } diff --git a/src/common/net/chan.c b/src/common/net/chan.c index b2b8e5e2d..6201b2240 100644 --- a/src/common/net/chan.c +++ b/src/common/net/chan.c @@ -658,8 +658,8 @@ static bool NetchanNew_Process(netchan_t *chan) } // message has been sucessfully assembled - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - SZ_Write(&msg_read, chan->fragment_in.data, chan->fragment_in.cursize); + memcpy(msg_read_buffer, chan->fragment_in.data, chan->fragment_in.cursize); + SZ_InitRead(&msg_read, msg_read_buffer, chan->fragment_in.cursize); SZ_Clear(&chan->fragment_in); } @@ -751,12 +751,12 @@ void Netchan_Setup(netchan_t *chan, netsrc_t sock, netchan_type_t type, switch (type) { case NETCHAN_OLD: chan->reliable_buf = buf = Z_TagMalloc(maxpacketlen * 2, tag); - SZ_Init(&chan->message, buf + maxpacketlen, maxpacketlen); + SZ_InitWrite(&chan->message, buf + maxpacketlen, maxpacketlen); break; case NETCHAN_NEW: chan->reliable_buf = buf = Z_TagMalloc(MAX_MSGLEN * 4, tag); - SZ_Init(&chan->message, buf + MAX_MSGLEN, MAX_MSGLEN); + SZ_InitWrite(&chan->message, buf + MAX_MSGLEN, MAX_MSGLEN); SZ_TagInit(&chan->fragment_in, buf + MAX_MSGLEN * 2, MAX_MSGLEN, "nc_frg_in"); SZ_TagInit(&chan->fragment_out, buf + MAX_MSGLEN * 3, MAX_MSGLEN, "nc_frg_out"); break; diff --git a/src/common/net/net.c b/src/common/net/net.c index f01a2038b..ac20887d7 100644 --- a/src/common/net/net.c +++ b/src/common/net/net.c @@ -565,8 +565,7 @@ static void NET_GetLoopPackets(netsrc_t sock, void (*packet_cb)(void)) net_rate_rcvd += msg->datalen; } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = msg->datalen; + SZ_InitRead(&msg_read, msg_read_buffer, msg->datalen); (*packet_cb)(); } @@ -784,8 +783,7 @@ static void NET_GetUdpPackets(struct pollfd *sock, void (*packet_cb)(void)) net_bytes_rcvd += ret; net_packets_rcvd++; - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = ret; + SZ_InitRead(&msg_read, msg_read_buffer, ret); (*packet_cb)(); } diff --git a/src/common/sizebuf.c b/src/common/sizebuf.c index 686c57e40..fde4d2043 100644 --- a/src/common/sizebuf.c +++ b/src/common/sizebuf.c @@ -30,15 +30,17 @@ void SZ_TagInit(sizebuf_t *buf, void *data, size_t size, const char *tag) buf->tag = tag; } -void SZ_Init(sizebuf_t *buf, void *data, size_t size) +void SZ_InitWrite(sizebuf_t *buf, void *data, size_t size) { - Q_assert(size <= INT32_MAX); - memset(buf, 0, sizeof(*buf)); - buf->data = data; - buf->maxsize = size; + SZ_TagInit(buf, data, size, "none"); buf->allowoverflow = true; +} + +void SZ_InitRead(sizebuf_t *buf, const void *data, size_t size) +{ + SZ_TagInit(buf, (void *)data, size, "none"); + buf->cursize = size; buf->allowunderflow = true; - buf->tag = "none"; } void SZ_Clear(sizebuf_t *buf) diff --git a/src/server/mvd.c b/src/server/mvd.c index d05ff20f5..3970268a9 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -2113,8 +2113,8 @@ void SV_MvdPostInit(void) } // allocate buffers - SZ_Init(&mvd.message, SV_Malloc(MAX_MSGLEN), MAX_MSGLEN); - SZ_Init(&mvd.datagram, SV_Malloc(MAX_MSGLEN), MAX_MSGLEN); + SZ_InitWrite(&mvd.message, SV_Malloc(MAX_MSGLEN), MAX_MSGLEN); + SZ_InitWrite(&mvd.datagram, SV_Malloc(MAX_MSGLEN), MAX_MSGLEN); mvd.players = SV_Malloc(sizeof(mvd.players[0]) * sv_maxclients->integer); mvd.entities = SV_Malloc(sizeof(mvd.entities[0]) * svs.csr.max_edicts); diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index b660d3a8b..5386332a4 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -505,9 +505,7 @@ static int demo_skip_map(qhandle_t f) } } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = msglen; - + SZ_InitRead(&msg_read, msg_read_buffer, msglen); return msglen; } @@ -519,9 +517,7 @@ static int demo_read_message(qhandle_t f) return msglen; } - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = msglen; - + SZ_InitRead(&msg_read, msg_read_buffer, msglen); return msglen; } @@ -2341,8 +2337,7 @@ static void MVD_Seek_f(void) // set player names MVD_SetPlayerNames(mvd); - SZ_Init(&msg_read, snap->data, snap->msglen); - msg_read.cursize = snap->msglen; + SZ_InitRead(&msg_read, snap->data, snap->msglen); MVD_ParseMessage(mvd); mvd->framenum = snap->framenum; diff --git a/src/server/save.c b/src/server/save.c index f95ff8472..f304bd164 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -249,8 +249,7 @@ static int read_binary_file(const char *name) if (FS_Read(msg_read_buffer, len, f) != len) goto fail; - SZ_Init(&msg_read, msg_read_buffer, sizeof(msg_read_buffer)); - msg_read.cursize = len; + SZ_InitRead(&msg_read, msg_read_buffer, len); FS_CloseFile(f); return 0; @@ -409,8 +408,7 @@ static int read_level_file(void) if (!data) return -1; - SZ_Init(&msg_read, data, len); - msg_read.cursize = len; + SZ_InitRead(&msg_read, data, len); if (MSG_ReadLong() != SAVE_MAGIC2) { FS_FreeFile(data); From 4f1ec4965a0c4cbfef3ada24e04347ee48900570 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 17 Jun 2024 19:26:32 +0300 Subject: [PATCH 273/974] Wrap error in Q_ERR() macro. --- src/client/download.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/download.c b/src/client/download.c index 0f53da011..031d551b3 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -201,7 +201,7 @@ static bool start_udp_download(dlqueue_t *q) ret = FS_OpenFile(cls.download.temp, &f, FS_MODE_RDWR); if (ret > INT_MAX) { FS_CloseFile(f); - ret = -EFBIG; + ret = Q_ERR(EFBIG); } if (ret >= 0) { // it exists cls.download.file = f; From 586a789c791ba0b56f5164bad52882db92ccbf99 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 17 Jun 2024 23:04:46 +0300 Subject: [PATCH 274/974] Rename SZ_TagInit() to SZ_Init(). --- inc/common/sizebuf.h | 2 +- src/common/msg.c | 4 ++-- src/common/net/chan.c | 10 +++++----- src/common/sizebuf.c | 7 +++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/inc/common/sizebuf.h b/inc/common/sizebuf.h index 1c49941a6..f62330c7a 100644 --- a/inc/common/sizebuf.h +++ b/inc/common/sizebuf.h @@ -31,7 +31,7 @@ typedef struct { const char *tag; // for debugging } sizebuf_t; -void SZ_TagInit(sizebuf_t *buf, void *data, size_t size, const char *tag); +void SZ_Init(sizebuf_t *buf, void *data, size_t size, const char *tag); void SZ_InitWrite(sizebuf_t *buf, void *data, size_t size); void SZ_InitRead(sizebuf_t *buf, const void *data, size_t size); void SZ_Clear(sizebuf_t *buf); diff --git a/src/common/msg.c b/src/common/msg.c index 47f7af860..c85b3363f 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -57,8 +57,8 @@ the allow underflow flag as appropriate. */ void MSG_Init(void) { - SZ_TagInit(&msg_read, msg_read_buffer, MAX_MSGLEN, "msg_read"); - SZ_TagInit(&msg_write, msg_write_buffer, MAX_MSGLEN, "msg_write"); + SZ_Init(&msg_read, msg_read_buffer, MAX_MSGLEN, "msg_read"); + SZ_Init(&msg_write, msg_write_buffer, MAX_MSGLEN, "msg_write"); } diff --git a/src/common/net/chan.c b/src/common/net/chan.c index 6201b2240..5e4ce8ec3 100644 --- a/src/common/net/chan.c +++ b/src/common/net/chan.c @@ -206,7 +206,7 @@ static int NetchanOld_Transmit(netchan_t *chan, size_t length, const void *data, if (chan->incoming_reliable_sequence) w2 |= REL_BIT; - SZ_TagInit(&send, send_buf, sizeof(send_buf), "nc_send_old"); + SZ_Init(&send, send_buf, sizeof(send_buf), "nc_send_old"); SZ_WriteLong(&send, w1); SZ_WriteLong(&send, w2); @@ -382,7 +382,7 @@ int Netchan_TransmitNextFragment(netchan_t *chan) if (chan->incoming_reliable_sequence) w2 |= REL_BIT; - SZ_TagInit(&send, send_buf, sizeof(send_buf), "nc_send_frg"); + SZ_Init(&send, send_buf, sizeof(send_buf), "nc_send_frg"); SZ_WriteLong(&send, w1); SZ_WriteLong(&send, w2); @@ -498,7 +498,7 @@ static int NetchanNew_Transmit(netchan_t *chan, size_t length, const void *data, if (chan->incoming_reliable_sequence) w2 |= REL_BIT; - SZ_TagInit(&send, send_buf, sizeof(send_buf), "nc_send_new"); + SZ_Init(&send, send_buf, sizeof(send_buf), "nc_send_new"); SZ_WriteLong(&send, w1); SZ_WriteLong(&send, w2); @@ -757,8 +757,8 @@ void Netchan_Setup(netchan_t *chan, netsrc_t sock, netchan_type_t type, case NETCHAN_NEW: chan->reliable_buf = buf = Z_TagMalloc(MAX_MSGLEN * 4, tag); SZ_InitWrite(&chan->message, buf + MAX_MSGLEN, MAX_MSGLEN); - SZ_TagInit(&chan->fragment_in, buf + MAX_MSGLEN * 2, MAX_MSGLEN, "nc_frg_in"); - SZ_TagInit(&chan->fragment_out, buf + MAX_MSGLEN * 3, MAX_MSGLEN, "nc_frg_out"); + SZ_Init(&chan->fragment_in, buf + MAX_MSGLEN * 2, MAX_MSGLEN, "nc_frg_in"); + SZ_Init(&chan->fragment_out, buf + MAX_MSGLEN * 3, MAX_MSGLEN, "nc_frg_out"); break; default: diff --git a/src/common/sizebuf.c b/src/common/sizebuf.c index fde4d2043..6ac116693 100644 --- a/src/common/sizebuf.c +++ b/src/common/sizebuf.c @@ -17,11 +17,10 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "shared/shared.h" -#include "common/protocol.h" #include "common/sizebuf.h" #include "common/intreadwrite.h" -void SZ_TagInit(sizebuf_t *buf, void *data, size_t size, const char *tag) +void SZ_Init(sizebuf_t *buf, void *data, size_t size, const char *tag) { Q_assert(size <= INT32_MAX); memset(buf, 0, sizeof(*buf)); @@ -32,13 +31,13 @@ void SZ_TagInit(sizebuf_t *buf, void *data, size_t size, const char *tag) void SZ_InitWrite(sizebuf_t *buf, void *data, size_t size) { - SZ_TagInit(buf, data, size, "none"); + SZ_Init(buf, data, size, "none"); buf->allowoverflow = true; } void SZ_InitRead(sizebuf_t *buf, const void *data, size_t size) { - SZ_TagInit(buf, (void *)data, size, "none"); + SZ_Init(buf, (void *)data, size, "none"); buf->cursize = size; buf->allowunderflow = true; } From 70ba0d17ed7d38e76a767c0147e24833286415d8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 18 Jun 2024 11:31:19 +0300 Subject: [PATCH 275/974] Make netchan sequence numbers unsigned. --- inc/common/net/chan.h | 15 +++++++------- src/client/client.h | 2 +- src/common/net/chan.c | 48 +++++++++++++++++++++---------------------- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/inc/common/net/chan.h b/inc/common/net/chan.h index ae291e37e..0fdff220a 100644 --- a/inc/common/net/chan.h +++ b/inc/common/net/chan.h @@ -34,7 +34,7 @@ typedef struct { netsrc_t sock; - int dropped; // between last packet and previous + unsigned dropped; // between last packet and previous unsigned total_dropped; // for statistics unsigned total_received; @@ -46,23 +46,22 @@ typedef struct { sizebuf_t message; // writing buffer for reliable data - unsigned reliable_length; - bool reliable_ack_pending; // set to true each time reliable is received bool fragment_pending; // sequencing variables - int incoming_sequence; - int incoming_acknowledged; - int outgoing_sequence; + unsigned incoming_sequence; + unsigned incoming_acknowledged; + unsigned outgoing_sequence; bool incoming_reliable_acknowledged; // single bit bool incoming_reliable_sequence; // single bit, maintained local bool reliable_sequence; // single bit - int last_reliable_sequence; // sequence number of last send - int fragment_sequence; + unsigned last_reliable_sequence; // sequence number of last send + unsigned fragment_sequence; // message is copied to this buffer when it is first transfered + unsigned reliable_length; byte *reliable_buf; // unacked reliable message sizebuf_t fragment_in; diff --git a/src/client/client.h b/src/client/client.h index 1b6f74914..eb0a70cc3 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -173,7 +173,7 @@ typedef struct { unsigned cmdNumber; short predicted_origins[CMD_BACKUP][3]; // for debug comparing against server client_history_t history[CMD_BACKUP]; - int initialSeq; + unsigned initialSeq; float predicted_step; // for stair up smoothing unsigned predicted_step_time; diff --git a/src/common/net/chan.c b/src/common/net/chan.c index 5e4ce8ec3..5dc819c8d 100644 --- a/src/common/net/chan.c +++ b/src/common/net/chan.c @@ -178,7 +178,7 @@ static int NetchanOld_Transmit(netchan_t *chan, size_t length, const void *data, sizebuf_t send; byte send_buf[MAX_PACKETLEN]; bool send_reliable; - int i, w1, w2; + unsigned w1, w2; send_reliable = false; @@ -234,18 +234,18 @@ static int NetchanOld_Transmit(netchan_t *chan, size_t length, const void *data, else Com_WPrintf("%s: dumped unreliable\n", NET_AdrToString(&chan->remote_address)); - SHOWPACKET("send %4u : s=%d ack=%d rack=%d", + SHOWPACKET("send %4u : s=%u ack=%u rack=%d", send.cursize, chan->outgoing_sequence, chan->incoming_sequence, chan->incoming_reliable_sequence); if (send_reliable) { - SHOWPACKET(" reliable=%i", chan->reliable_sequence); + SHOWPACKET(" reliable=%d", chan->reliable_sequence); } SHOWPACKET("\n"); // send the datagram - for (i = 0; i < numpackets; i++) { + for (int i = 0; i < numpackets; i++) { NET_SendPacket(chan->sock, send.data, send.cursize, &chan->remote_address); } @@ -266,8 +266,8 @@ modifies net_message so that it points to the packet payload */ static bool NetchanOld_Process(netchan_t *chan) { - int sequence, sequence_ack; - bool reliable_ack, reliable_message; + unsigned sequence, sequence_ack; + bool reliable_ack, reliable_message; // get sequence numbers MSG_BeginReading(); @@ -295,7 +295,7 @@ static bool NetchanOld_Process(netchan_t *chan) sequence &= OLD_MASK; sequence_ack &= OLD_MASK; - SHOWPACKET("recv %4u : s=%d ack=%d rack=%d", + SHOWPACKET("recv %4u : s=%u ack=%u rack=%d", msg_read.cursize, sequence, sequence_ack, @@ -309,7 +309,7 @@ static bool NetchanOld_Process(netchan_t *chan) // discard stale or duplicated packets // if (sequence <= chan->incoming_sequence) { - SHOWDROP("%s: out of order packet %i at %i\n", + SHOWDROP("%s: out of order packet %u at %u\n", NET_AdrToString(&chan->remote_address), sequence, chan->incoming_sequence); return false; @@ -320,7 +320,7 @@ static bool NetchanOld_Process(netchan_t *chan) // chan->dropped = sequence - (chan->incoming_sequence + 1); if (chan->dropped > 0) { - SHOWDROP("%s: dropped %i packets at %i\n", + SHOWDROP("%s: dropped %u packets at %u\n", NET_AdrToString(&chan->remote_address), chan->dropped, sequence); } @@ -366,8 +366,7 @@ int Netchan_TransmitNextFragment(netchan_t *chan) sizebuf_t send; byte send_buf[MAX_PACKETLEN]; bool send_reliable, more_fragments; - int w1, w2, offset; - unsigned fragment_length; + unsigned w1, w2, offset, fragment_length; Q_assert(chan->type); @@ -413,7 +412,7 @@ int Netchan_TransmitNextFragment(netchan_t *chan) // write fragment contents SZ_Write(&send, chan->fragment_out.data + chan->fragment_out.readcount, fragment_length); - SHOWPACKET("send %4u : s=%d ack=%d rack=%d " + SHOWPACKET("send %4u : s=%u ack=%u rack=%d " "fragment_offset=%u more_fragments=%d", send.cursize, chan->outgoing_sequence, @@ -422,7 +421,7 @@ int Netchan_TransmitNextFragment(netchan_t *chan) chan->fragment_out.readcount, more_fragments); if (send_reliable) { - SHOWPACKET(" reliable=%i ", chan->reliable_sequence); + SHOWPACKET(" reliable=%d", chan->reliable_sequence); } SHOWPACKET("\n"); @@ -452,7 +451,7 @@ static int NetchanNew_Transmit(netchan_t *chan, size_t length, const void *data, sizebuf_t send; byte send_buf[MAX_PACKETLEN]; bool send_reliable; - int i, w1, w2; + unsigned w1, w2; if (chan->fragment_pending) { return Netchan_TransmitNextFragment(chan); @@ -519,7 +518,7 @@ static int NetchanNew_Transmit(netchan_t *chan, size_t length, const void *data, // add the unreliable part SZ_Write(&send, data, length); - SHOWPACKET("send %4u : s=%d ack=%d rack=%d", + SHOWPACKET("send %4u : s=%u ack=%u rack=%d", send.cursize, chan->outgoing_sequence, chan->incoming_sequence, @@ -530,7 +529,7 @@ static int NetchanNew_Transmit(netchan_t *chan, size_t length, const void *data, SHOWPACKET("\n"); // send the datagram - for (i = 0; i < numpackets; i++) { + for (int i = 0; i < numpackets; i++) { NET_SendPacket(chan->sock, send.data, send.cursize, &chan->remote_address); } @@ -548,9 +547,8 @@ NetchanNew_Process */ static bool NetchanNew_Process(netchan_t *chan) { - int sequence, sequence_ack, fragment_offset; + unsigned sequence, sequence_ack, fragment_offset, length; bool reliable_message, reliable_ack, fragmented_message, more_fragments; - unsigned length; // get sequence numbers MSG_BeginReading(); @@ -583,10 +581,10 @@ static bool NetchanNew_Process(netchan_t *chan) return false; } - SHOWPACKET("recv %4u : s=%d ack=%d rack=%d", + SHOWPACKET("recv %4u : s=%u ack=%u rack=%d", msg_read.cursize, sequence, sequence_ack, reliable_ack); if (fragmented_message) { - SHOWPACKET(" fragment_offset=%d more_fragments=%d", + SHOWPACKET(" fragment_offset=%u more_fragments=%d", fragment_offset, more_fragments); } if (reliable_message) { @@ -598,7 +596,7 @@ static bool NetchanNew_Process(netchan_t *chan) // discard stale or duplicated packets // if (sequence <= chan->incoming_sequence) { - SHOWDROP("%s: out of order packet %i at %i\n", + SHOWDROP("%s: out of order packet %u at %u\n", NET_AdrToString(&chan->remote_address), sequence, chan->incoming_sequence); return false; @@ -609,7 +607,7 @@ static bool NetchanNew_Process(netchan_t *chan) // chan->dropped = sequence - (chan->incoming_sequence + 1); if (chan->dropped > 0) { - SHOWDROP("%s: dropped %i packets at %i\n", + SHOWDROP("%s: dropped %u packets at %u\n", NET_AdrToString(&chan->remote_address), chan->dropped, sequence); } @@ -634,20 +632,20 @@ static bool NetchanNew_Process(netchan_t *chan) } if (fragment_offset < chan->fragment_in.cursize) { - SHOWDROP("%s: out of order fragment at %i\n", + SHOWDROP("%s: out of order fragment at %u\n", NET_AdrToString(&chan->remote_address), sequence); return false; } if (fragment_offset > chan->fragment_in.cursize) { - SHOWDROP("%s: dropped fragment(s) at %i\n", + SHOWDROP("%s: dropped fragment(s) at %u\n", NET_AdrToString(&chan->remote_address), sequence); return false; } length = msg_read.cursize - msg_read.readcount; if (length > chan->fragment_in.maxsize - chan->fragment_in.cursize) { - SHOWDROP("%s: oversize fragment at %i\n", + SHOWDROP("%s: oversize fragment at %u\n", NET_AdrToString(&chan->remote_address), sequence); return false; } From eda54bedef8d6592644e1f0cb118f69d14b62459 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 19 Jun 2024 19:26:30 +0300 Subject: [PATCH 276/974] Drop connection if outgoing sequence is too big. --- inc/common/net/chan.h | 5 +++++ src/client/input.c | 4 ++++ src/server/main.c | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/inc/common/net/chan.h b/inc/common/net/chan.h index 0fdff220a..944b80806 100644 --- a/inc/common/net/chan.h +++ b/inc/common/net/chan.h @@ -83,5 +83,10 @@ bool Netchan_Process(netchan_t *chan); bool Netchan_ShouldUpdate(const netchan_t *chan); void Netchan_Close(netchan_t *chan); +static inline bool Netchan_SeqTooBig(const netchan_t *chan) +{ + return chan->outgoing_sequence >= BIT(31 - chan->type) - 256; +} + #define OOB_PRINT(sock, addr, data) \ NET_SendPacket(sock, CONST_STR_LEN("\xff\xff\xff\xff" data), addr) diff --git a/src/client/input.c b/src/client/input.c index 04ba48425..67a61062a 100644 --- a/src/client/input.c +++ b/src/client/input.c @@ -1111,6 +1111,10 @@ static void CL_SendUserinfo(void) static void CL_SendReliable(void) { + if (Netchan_SeqTooBig(&cls.netchan)) { + Com_Error(ERR_DROP, "Outgoing sequence too big"); + } + if (cls.userinfo_modified) { CL_SendUserinfo(); cls.userinfo_modified = 0; diff --git a/src/server/main.c b/src/server/main.c index 4911ac2aa..ff8918bc4 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1653,6 +1653,11 @@ static void SV_CheckTimeouts(void) unsigned delta; FOR_EACH_CLIENT(client) { + if (Netchan_SeqTooBig(&client->netchan)) { + SV_DropClient(client, "outgoing sequence too big"); + continue; + } + // never timeout local clients if (NET_IsLocalAddress(&client->netchan.remote_address)) { continue; From 2b1f0af445ed0df4bd3af9b42077c12329a6b15b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 22 Jun 2024 20:05:25 +0300 Subject: [PATCH 277/974] Reduce number of Com_Printf() calls to print client list. --- src/server/commands.c | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/server/commands.c b/src/server/commands.c index 53a44bd8d..93b464a85 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -503,41 +503,39 @@ static void dump_clients(void) Com_Printf( "num score ping name lastmsg address rate pr fps\n" "--- ----- ---- --------------- ------- --------------------- ----- -- ---\n"); + FOR_EACH_CLIENT(client) { - Com_Printf("%3i %5i ", client->number, - client->edict->client->ps.stats[STAT_FRAGS]); + const char *ping; switch (client->state) { case cs_zombie: - Com_Printf("ZMBI "); + ping = "ZMBI"; break; case cs_assigned: - Com_Printf("ASGN "); + ping = "ASGN"; break; case cs_connected: case cs_primed: if (client->download) { - Com_Printf("DNLD "); + ping = "DNLD"; } else if (client->http_download) { - Com_Printf("HTTP "); + ping = "HTTP"; } else if (client->state == cs_connected) { - Com_Printf("CNCT "); + ping = "CNCT"; } else { - Com_Printf("PRIM "); + ping = "PRIM"; } break; default: - Com_Printf("%4i ", client->ping < 9999 ? client->ping : 9999); + ping = va("%4i", min(client->ping, 9999)); break; } - Com_Printf("%-15.15s ", client->name); - Com_Printf("%7u ", svs.realtime - client->lastmessage); - Com_Printf("%-21s ", NET_AdrToString(&client->netchan.remote_address)); - Com_Printf("%5i ", client->rate); - Com_Printf("%2i ", client->protocol); - Com_Printf("%3i ", client->moves_per_sec); - Com_Printf("\n"); + Com_Printf("%3i %5i %s %-15.15s %7u %-21s %5i %2i %3i\n", client->number, + client->edict->client->ps.stats[STAT_FRAGS], + ping, client->name, svs.realtime - client->lastmessage, + NET_AdrToString(&client->netchan.remote_address), + client->rate, client->protocol, client->moves_per_sec); } } From 47028ebcf95bce1a9739ac6622137f2a78bd3d91 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 22 Jun 2024 20:06:36 +0300 Subject: [PATCH 278/974] Print CLS_NOFLARES setting too. --- src/server/commands.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/commands.c b/src/server/commands.c index 93b464a85..7e7aceff5 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -646,7 +646,6 @@ static void dump_settings(void) "num name proto options upd fps\n" "--- --------------- ----- ------- --- ---\n"); - opt[6] = ' '; opt[7] = 0; FOR_EACH_CLIENT(cl) { opt[0] = cl->settings[CLS_NOGUN] ? 'G' : ' '; @@ -655,6 +654,7 @@ static void dump_settings(void) opt[3] = cl->settings[CLS_NOGIBS] ? 'I' : ' '; opt[4] = cl->settings[CLS_NOFOOTSTEPS] ? 'F' : ' '; opt[5] = cl->settings[CLS_NOPREDICT] ? 'P' : ' '; + opt[6] = cl->settings[CLS_NOFLARES] ? 'L' : ' '; Com_Printf("%3i %-15.15s %5d %s %3d %3d\n", cl->number, cl->name, cl->protocol, opt, cl->settings[CLS_PLAYERUPDATES], cl->settings[CLS_FPS]); From c8929b4b7210a766c1a54593e63263270d425dcc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 23 Jun 2024 15:47:27 +0300 Subject: [PATCH 279/974] Use INT32_MAX for packfile offset checks. --- src/common/files.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index ff971a0fa..14c75afbe 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -2123,7 +2123,7 @@ static pack_t *load_pak_file(const char *packfile) } header.dirofs = LittleLong(header.dirofs); - if (header.dirofs > INT_MAX) { + if (header.dirofs > INT32_MAX) { Com_SetLastError("bad directory offset"); goto fail1; } @@ -2141,7 +2141,7 @@ static pack_t *load_pak_file(const char *packfile) for (i = 0, dfile = info; i < num_files; i++, dfile++) { dfile->filepos = LittleLong(dfile->filepos); dfile->filelen = LittleLong(dfile->filelen); - if (dfile->filelen > INT_MAX || dfile->filepos > INT_MAX - dfile->filelen) { + if (dfile->filelen > INT32_MAX || dfile->filepos > INT32_MAX - dfile->filelen) { Com_SetLastError("file length or position too big"); goto fail2; } From 08a790e3acd8400ebeccc6a83aac56f2aa5843a5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 23 Jun 2024 15:47:50 +0300 Subject: [PATCH 280/974] Make filesystem debug counters unsigned. --- src/common/files.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index 14c75afbe..2c23202c4 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -181,10 +181,10 @@ static int fs_num_files; static bool fs_non_uniq_open; #if USE_DEBUG -static int fs_count_read; -static int fs_count_open; -static int fs_count_strcmp; -static int fs_count_strlwr; +static unsigned fs_count_read; +static unsigned fs_count_open; +static unsigned fs_count_strcmp; +static unsigned fs_count_strlwr; #define FS_COUNT_READ fs_count_read++ #define FS_COUNT_OPEN fs_count_open++ #define FS_COUNT_STRCMP fs_count_strcmp++ @@ -3287,10 +3287,10 @@ static void FS_Stats_f(void) } Com_Printf("File slots allocated: %d\n", fs_num_files); - Com_Printf("Total calls to open_file_read: %d\n", fs_count_read); - Com_Printf("Total path comparsions: %d\n", fs_count_strcmp); - Com_Printf("Total calls to open_from_disk: %d\n", fs_count_open); - Com_Printf("Total mixed-case reopens: %d\n", fs_count_strlwr); + Com_Printf("Total calls to open_file_read: %u\n", fs_count_read); + Com_Printf("Total path comparsions: %u\n", fs_count_strcmp); + Com_Printf("Total calls to open_from_disk: %u\n", fs_count_open); + Com_Printf("Total mixed-case reopens: %u\n", fs_count_strlwr); if (!totalHashSize) { Com_Printf("No stats to display\n"); From 7aeb131190e4cde05edb7354137048e8d299c0b4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 03:21:34 +0300 Subject: [PATCH 281/974] Make client entity state counters unsigned. --- src/client/client.h | 4 ++-- src/client/parse.c | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index eb0a70cc3..38a274616 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -127,7 +127,7 @@ typedef struct { int clientNum; int numEntities; - int firstEntity; + unsigned firstEntity; } server_frame_t; // locally calculated frame flags for debug display @@ -191,7 +191,7 @@ typedef struct { centity_state_t baselines[MAX_EDICTS]; centity_state_t entityStates[MAX_PARSE_ENTITIES]; - int numEntityStates; + unsigned numEntityStates; msgEsFlags_t esFlags; msgPsFlags_t psFlags; diff --git a/src/client/parse.c b/src/client/parse.c index a83f66827..d5a36fd6b 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -92,8 +92,8 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, if (oldindex >= oldframe->numEntities) { oldnum = MAX_EDICTS; } else { - i = oldframe->firstEntity + oldindex; - oldstate = &cl.entityStates[i & PARSE_ENTITIES_MASK]; + i = (oldframe->firstEntity + oldindex) & PARSE_ENTITIES_MASK; + oldstate = &cl.entityStates[i]; oldnum = oldstate->number; } } @@ -118,8 +118,8 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, if (oldindex >= oldframe->numEntities) { oldnum = MAX_EDICTS; } else { - i = oldframe->firstEntity + oldindex; - oldstate = &cl.entityStates[i & PARSE_ENTITIES_MASK]; + i = (oldframe->firstEntity + oldindex) & PARSE_ENTITIES_MASK; + oldstate = &cl.entityStates[i]; oldnum = oldstate->number; } } @@ -139,8 +139,8 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, if (oldindex >= oldframe->numEntities) { oldnum = MAX_EDICTS; } else { - i = oldframe->firstEntity + oldindex; - oldstate = &cl.entityStates[i & PARSE_ENTITIES_MASK]; + i = (oldframe->firstEntity + oldindex) & PARSE_ENTITIES_MASK; + oldstate = &cl.entityStates[i]; oldnum = oldstate->number; } continue; @@ -159,8 +159,8 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, if (oldindex >= oldframe->numEntities) { oldnum = MAX_EDICTS; } else { - i = oldframe->firstEntity + oldindex; - oldstate = &cl.entityStates[i & PARSE_ENTITIES_MASK]; + i = (oldframe->firstEntity + oldindex) & PARSE_ENTITIES_MASK; + oldstate = &cl.entityStates[i]; oldnum = oldstate->number; } continue; @@ -189,8 +189,8 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, if (oldindex >= oldframe->numEntities) { oldnum = MAX_EDICTS; } else { - i = oldframe->firstEntity + oldindex; - oldstate = &cl.entityStates[i & PARSE_ENTITIES_MASK]; + i = (oldframe->firstEntity + oldindex) & PARSE_ENTITIES_MASK; + oldstate = &cl.entityStates[i]; oldnum = oldstate->number; } } From d0defdc8eb2d079dd5f6cd10d502abea596c945e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 04:08:54 +0300 Subject: [PATCH 282/974] Make recent IP counter unsigned. --- src/client/client.h | 2 +- src/client/main.c | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 38a274616..ddc4f4de9 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -439,7 +439,7 @@ typedef struct { #define RECENT_MASK (RECENT_ADDR - 1) netadr_t recent_addr[RECENT_ADDR]; - int recent_head; + unsigned recent_head; string_entry_t *stufftextwhitelist; diff --git a/src/client/main.c b/src/client/main.c index 76d4c2d05..4793845a6 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -478,15 +478,8 @@ void CL_CheckForResend(void) static void CL_RecentIP_g(genctx_t *ctx) { - netadr_t *a; - int i, j; - - j = cls.recent_head - RECENT_ADDR; - if (j < 0) { - j = 0; - } - for (i = cls.recent_head - 1; i >= j; i--) { - a = &cls.recent_addr[i & RECENT_MASK]; + for (int i = 0; i < RECENT_ADDR; i++) { + const netadr_t *a = &cls.recent_addr[(cls.recent_head - i - 1) & RECENT_MASK]; if (a->type) { Prompt_AddMatch(ctx, NET_AdrToString(a)); } @@ -563,27 +556,21 @@ static void CL_Connect_f(void) static void CL_FollowIP_f(void) { - netadr_t *a; - int i, j; + const netadr_t *a; + int i = 0; if (Cmd_Argc() > 1) { // optional second argument references less recent address - j = Q_clip(Q_atoi(Cmd_Argv(1)), 0, RECENT_ADDR - 1) + 1; - } else { - j = 1; + i = Q_clip(Q_atoi(Cmd_Argv(1)), 0, RECENT_ADDR - 1); } - i = cls.recent_head - j; - if (i < 0) { - Com_Printf("No IP address to follow.\n"); - return; - } - - a = &cls.recent_addr[i & RECENT_MASK]; + a = &cls.recent_addr[(cls.recent_head - i - 1) & RECENT_MASK]; if (a->type) { const char *s = NET_AdrToString(a); Com_Printf("Following %s...\n", s); Cbuf_InsertText(cmd_current, va("connect %s\n", s)); + } else { + Com_Printf("No IP address to follow.\n"); } } From 616f15ff48cc5d661b14b45e67991a8dc20606b5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 04:20:20 +0300 Subject: [PATCH 283/974] Remove unused cls.framecount variable. --- src/client/client.h | 1 - src/client/main.c | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index ddc4f4de9..1f12228e6 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -392,7 +392,6 @@ typedef struct { // this is set each time a CVAR_USERINFO variable is changed // so that the client knows to send it to the server - int framecount; unsigned realtime; // always increasing, no clamping, etc float frametime; // seconds since last frame diff --git a/src/client/main.c b/src/client/main.c index 4793845a6..b9ade40b4 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -3334,8 +3334,6 @@ unsigned CL_Frame(unsigned msec) CL_MeasureStats(); - cls.framecount++; - main_extra = 0; return 0; } From 287fcaf420ef504ddf5401a5522e37fdc12a23b3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 04:33:08 +0300 Subject: [PATCH 284/974] Make CL_GetDemoInfo() return a boolean. --- inc/client/client.h | 2 +- src/client/client.h | 2 +- src/client/demo.c | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index 7b1891d46..b267f13eb 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -75,7 +75,7 @@ void CL_RestartFilesystem(bool total); void CL_Activate(active_t active); void CL_UpdateUserinfo(cvar_t *var, from_t from); void CL_SendStatusRequest(const netadr_t *address); -demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info); +bool CL_GetDemoInfo(const char *path, demoInfo_t *info); bool CL_CheatsOK(void); void CL_SetSky(void); diff --git a/src/client/client.h b/src/client/client.h index 1f12228e6..bce4d67e7 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -924,7 +924,7 @@ void CL_EmitDemoSnapshot(void); void CL_FreeDemoSnapshots(void); void CL_FirstDemoFrame(void); void CL_Stop_f(void); -demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info); +bool CL_GetDemoInfo(const char *path, demoInfo_t *info); // diff --git a/src/client/demo.c b/src/client/demo.c index 3074f4e63..b3e8ac757 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -1154,15 +1154,16 @@ static void parse_info_string(demoInfo_t *info, int clientNum, int index, const CL_GetDemoInfo ==================== */ -demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info) +bool CL_GetDemoInfo(const char *path, demoInfo_t *info) { qhandle_t f; int c, index, clientNum, type; const cs_remap_t *csr = &cs_remap_old; + bool res = false; FS_OpenFile(path, &f, FS_MODE_READ | FS_FLAG_GZIP); if (!f) { - return NULL; + return false; } type = read_first_message(f); @@ -1232,13 +1233,11 @@ demoInfo_t *CL_GetDemoInfo(const char *path, demoInfo_t *info) parse_info_string(info, clientNum, index, csr); } } - - FS_CloseFile(f); - return info; + res = true; fail: FS_CloseFile(f); - return NULL; + return res; } // ========================================================================= From cc437b09b62d9020ec561d62dd9274092a84df26 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 07:13:51 +0300 Subject: [PATCH 285/974] Fix initializing and clearing console. Initialize first console line properly by calling Con_CarriageRet(). Reset line counters to zero to remove the scrollback buffer when clearing. --- src/client/console.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/console.c b/src/client/console.c index cb930b23c..a87ca0eb5 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -209,7 +209,8 @@ Con_Clear_f static void Con_Clear_f(void) { memset(con.text, 0, sizeof(con.text)); - con.display = con.current; + con.display = con.current = 0; + con.newline = '\r'; } static void Con_Dump_c(genctx_t *ctx, int argnum) @@ -486,7 +487,7 @@ void Con_Init(void) con.linewidth = -1; con.scale = 1; con.color = COLOR_NONE; - con.text[0].color = COLOR_NONE; + con.newline = '\r'; Con_CheckResize(); From fdc0c5917386dcd2d9b1e9bee9e055e7bb9e3034 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 07:16:40 +0300 Subject: [PATCH 286/974] Wrap console line counters to avoid overflow. --- src/client/console.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/console.c b/src/client/console.c index a87ca0eb5..324274b2b 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -548,6 +548,12 @@ static void Con_Linefeed(void) } else { Con_CheckTop(); } + + // wrap to avoid integer overflow + if (con.current >= CON_TOTALLINES * 2) { + con.current -= CON_TOTALLINES; + con.display -= CON_TOTALLINES; + } } void Con_SetColor(color_index_t color) From b44477c1ce3a24540504d059d9704b782af1cea7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 08:18:22 +0300 Subject: [PATCH 287/974] Better align con.text array in memory. --- src/client/console.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/console.c b/src/client/console.c index 324274b2b..fb26a96be 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -46,9 +46,8 @@ typedef struct { } consoleLine_t; typedef struct { - bool initialized; - consoleLine_t text[CON_TOTALLINES]; + int current; // line where next message will be printed int x; // offset in current line for next print int display; // bottom of console displays this line @@ -63,6 +62,7 @@ typedef struct { unsigned times[CON_TIMES]; // cls.realtime time the line was generated // for transparent notify lines bool skipNotify; + bool initialized; qhandle_t backImage; qhandle_t charsetImage; From b192037739bb9ad52bc1ff20cf192d793c5bdf45 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 25 Jun 2024 16:04:38 +0300 Subject: [PATCH 288/974] Clean up and fix configstring bitmap code. Properly handle MAX_CONFIGSTRINGS not evenly divisible by 8. Optimize for 64-bit. --- inc/common/protocol.h | 4 ---- inc/common/utils.h | 4 ++++ inc/shared/shared.h | 6 +++--- src/client/client.h | 2 +- src/client/demo.c | 16 ++++++++-------- src/server/mvd/client.c | 8 ++++---- src/server/mvd/client.h | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index ec2013ab4..50e7c053d 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -83,7 +83,6 @@ with this program; if not, write to the Free Software Foundation, Inc., // increased from 64 #define CMD_MASK (CMD_BACKUP - 1) - #define SVCMD_BITS 5 #define SVCMD_MASK MASK(SVCMD_BITS) @@ -105,9 +104,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_PACKET_STRINGCMDS 8 #define MAX_PACKET_USERINFOS 8 -#define CS_BITMAP_BYTES (MAX_CONFIGSTRINGS / 8) // 260 -#define CS_BITMAP_LONGS (CS_BITMAP_BYTES / 4) - #define MVD_MAGIC MakeRawLong('M','V','D','2') // diff --git a/inc/common/utils.h b/inc/common/utils.h index 7fe7b295c..c1fb1c20b 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -74,6 +74,10 @@ unsigned Com_ParseExtensionString(const char *s, const char *const extnames[]); char *Com_MakePrintable(const char *s); +// Bitmap chunks (for sparse bitmaps) +#define BC_BITS (sizeof(size_t) * CHAR_BIT) +#define BC_COUNT(n) (((n) + BC_BITS - 1) / BC_BITS) + // Some mods actually exploit CS_STATUSBAR to take space up to CS_AIRACCEL static inline size_t CS_SIZE(const cs_remap_t *csr, int cs) { diff --git a/inc/shared/shared.h b/inc/shared/shared.h index f5fcca77b..5c0ec623d 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -411,9 +411,9 @@ static inline uint16_t Q_clip_uint16(int a) #define Q_rint(x) ((x) < 0 ? ((int)((x) - 0.5f)) : ((int)((x) + 0.5f))) -#define Q_IsBitSet(data, bit) (((data)[(bit) >> 3] >> ((bit) & 7)) & 1) -#define Q_SetBit(data, bit) ((data)[(bit) >> 3] |= (1 << ((bit) & 7))) -#define Q_ClearBit(data, bit) ((data)[(bit) >> 3] &= ~(1 << ((bit) & 7))) +#define Q_IsBitSet(data, bit) ((((const byte *)(data))[(bit) >> 3] >> ((bit) & 7)) & 1) +#define Q_SetBit(data, bit) (((byte *)(data))[(bit) >> 3] |= (1 << ((bit) & 7))) +#define Q_ClearBit(data, bit) (((byte *)(data))[(bit) >> 3] &= ~(1 << ((bit) & 7))) //============================================= diff --git a/src/client/client.h b/src/client/client.h index bce4d67e7..eb23207d5 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -210,7 +210,7 @@ typedef struct { int keyservertime; #endif - byte dcs[CS_BITMAP_BYTES]; + size_t dcs[BC_COUNT(MAX_CONFIGSTRINGS)]; // the client maintains its own idea of view angles, which are // sent to the server each frame. It is cleared to 0 upon entering each level. diff --git a/src/client/demo.c b/src/client/demo.c index b3e8ac757..effaded8f 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -479,12 +479,12 @@ static void resume_record(void) char *s; // write dirty configstrings - for (i = 0; i < CS_BITMAP_LONGS; i++) { - if (((uint32_t *)cl.dcs)[i] == 0) + for (i = 0; i < q_countof(cl.dcs); i++) { + if (cl.dcs[i] == 0) continue; - index = i << 5; - for (j = 0; j < 32; j++, index++) { + index = i * BC_BITS; + for (j = 0; j < BC_BITS; j++, index++) { if (!Q_IsBitSet(cl.dcs, index)) continue; @@ -1092,12 +1092,12 @@ static void CL_Seek_f(void) Com_DPrintf("[%d] after skip %d\n", cls.demo.frames_read, cl.frame.number); // update dirty configstrings - for (i = 0; i < CS_BITMAP_LONGS; i++) { - if (((uint32_t *)cl.dcs)[i] == 0) + for (i = 0; i < q_countof(cl.dcs); i++) { + if (cl.dcs[i] == 0) continue; - index = i << 5; - for (j = 0; j < 32; j++, index++) { + index = i * BC_BITS; + for (j = 0; j < BC_BITS; j++, index++) { if (Q_IsBitSet(cl.dcs, index)) CL_UpdateConfigstring(index); } diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index 5386332a4..e5c592191 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -2373,12 +2373,12 @@ static void MVD_Seek_f(void) Com_DPrintf("[%d] after skip\n", mvd->framenum); // update dirty configstrings - for (i = 0; i < CS_BITMAP_LONGS; i++) { - if (((uint32_t *)mvd->dcs)[i] == 0) + for (i = 0; i < q_countof(mvd->dcs); i++) { + if (mvd->dcs[i] == 0) continue; - index = i << 5; - for (j = 0; j < 32; j++, index++) { + index = i * BC_BITS; + for (j = 0; j < BC_BITS; j++, index++) { if (Q_IsBitSet(mvd->dcs, index)) MVD_UpdateConfigstring(mvd, index); } diff --git a/src/server/mvd/client.h b/src/server/mvd/client.h index 9fff4024e..db9af304f 100644 --- a/src/server/mvd/client.h +++ b/src/server/mvd/client.h @@ -163,7 +163,7 @@ typedef struct mvd_s { vec3_t spawnOrigin; vec3_t spawnAngles; int pm_type; - byte dcs[CS_BITMAP_BYTES]; + size_t dcs[BC_COUNT(MAX_CONFIGSTRINGS)]; configstring_t baseconfigstrings[MAX_CONFIGSTRINGS]; configstring_t configstrings[MAX_CONFIGSTRINGS]; const cs_remap_t *csr; From 7693d406e736a58ace3395c63948f320b938a5f5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 26 Jun 2024 11:41:34 +0300 Subject: [PATCH 289/974] Rename CS_SIZE() to Com_ConfigstringSize(). --- inc/common/utils.h | 3 +-- src/client/parse.c | 2 +- src/server/game.c | 2 +- src/server/mvd/parse.c | 4 ++-- src/server/save.c | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/inc/common/utils.h b/inc/common/utils.h index c1fb1c20b..a7f0275aa 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -79,7 +79,7 @@ char *Com_MakePrintable(const char *s); #define BC_COUNT(n) (((n) + BC_BITS - 1) / BC_BITS) // Some mods actually exploit CS_STATUSBAR to take space up to CS_AIRACCEL -static inline size_t CS_SIZE(const cs_remap_t *csr, int cs) +static inline size_t Com_ConfigstringSize(const cs_remap_t *csr, int cs) { if (cs >= CS_STATUSBAR && cs < csr->airaccel) return MAX_QPATH * (csr->airaccel - cs); @@ -102,5 +102,4 @@ static inline frametime_t Com_ComputeFrametime(int rate) int framediv = Q_clip(rate / BASE_FRAMERATE, 1, MAX_FRAMEDIV); return (frametime_t){ .time = BASE_FRAMETIME / framediv, .div = framediv }; } - #endif // USE_FPS diff --git a/src/client/parse.c b/src/client/parse.c index d5a36fd6b..605657fff 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -427,7 +427,7 @@ static void CL_ParseConfigstring(int index) } s = cl.configstrings[index]; - maxlen = CS_SIZE(&cl.csr, index); + maxlen = Com_ConfigstringSize(&cl.csr, index); len = MSG_ReadString(s, maxlen); SHOWNET(2, " %d \"%s\"\n", index, Com_MakePrintable(s)); diff --git a/src/server/game.c b/src/server/game.c index 07d9b11e0..2889ac42a 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -388,7 +388,7 @@ static void PF_configstring(int index, const char *val) } // print a warning and truncate everything else - maxlen = CS_SIZE(&svs.csr, index); + maxlen = Com_ConfigstringSize(&svs.csr, index); if (len >= maxlen) { Com_WPrintf( "%s: index %d overflowed: %zu > %zu\n", diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index 1c047f231..d0ece0ff1 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -569,7 +569,7 @@ static void MVD_ParseConfigstring(mvd_t *mvd) } s = mvd->configstrings[index]; - maxlen = CS_SIZE(mvd->csr, index); + maxlen = Com_ConfigstringSize(mvd->csr, index); if (MSG_ReadString(s, maxlen) >= maxlen) { MVD_Destroyf(mvd, "%s: index %d overflowed", __func__, index); } @@ -927,7 +927,7 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) } string = mvd->configstrings[index]; - maxlen = CS_SIZE(mvd->csr, index); + maxlen = Com_ConfigstringSize(mvd->csr, index); if (MSG_ReadString(string, maxlen) >= maxlen) { MVD_Destroyf(mvd, "Configstring %d overflowed", index); } diff --git a/src/server/save.c b/src/server/save.c index f304bd164..52bda1d3d 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -435,7 +435,7 @@ static int read_level_file(void) if (index < 0 || index >= svs.csr.end) Com_Error(ERR_DROP, "Bad savegame configstring index"); - maxlen = CS_SIZE(&svs.csr, index); + maxlen = Com_ConfigstringSize(&svs.csr, index); if (MSG_ReadString(sv.configstrings[index], maxlen) >= maxlen) Com_Error(ERR_DROP, "Savegame configstring too long"); } From 668a12f05ecba9ecc461e87e1abc6a937a87ba97 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 26 Jun 2024 12:29:54 +0300 Subject: [PATCH 290/974] Clean up client frame parsing code a bit. --- src/client/parse.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/client/parse.c b/src/client/parse.c index 605657fff..8370aa72a 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -73,12 +73,11 @@ static void CL_ParseDeltaEntity(server_frame_t *frame, } } -static void CL_ParsePacketEntities(server_frame_t *oldframe, - server_frame_t *frame) +static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_t *frame) { - uint64_t bits; - centity_state_t *oldstate; - int i, oldindex, oldnum, newnum; + uint64_t bits; + const centity_state_t *oldstate; + int i, oldindex, oldnum, newnum; frame->firstEntity = cl.numEntityStates; frame->numEntities = 0; @@ -198,12 +197,11 @@ static void CL_ParsePacketEntities(server_frame_t *oldframe, static void CL_ParseFrame(int extrabits) { - uint32_t bits, extraflags; - int currentframe, deltaframe, - delta, suppressed; - server_frame_t frame, *oldframe; - player_state_t *from; - int length; + uint32_t bits, extraflags; + int currentframe, deltaframe, delta, suppressed, length; + server_frame_t frame; + const server_frame_t *oldframe; + const player_state_t *from; memset(&frame, 0, sizeof(frame)); @@ -868,7 +866,7 @@ static void CL_ParseStartSoundPacket(void) { int flags, channel, entity; - flags = MSG_ReadByte(); + snd.flags = flags = MSG_ReadByte(); if (cl.csr.extended && flags & SND_INDEX16) snd.index = MSG_ReadWord(); @@ -910,8 +908,6 @@ static void CL_ParseStartSoundPacket(void) if (flags & SND_POS) MSG_ReadPos(snd.pos); - snd.flags = flags; - SHOWNET(2, " %s\n", cl.configstrings[cl.csr.sounds + snd.index]); } From 0ee74be01ef6f38368b4a0aac28dc15f94a999f4 Mon Sep 17 00:00:00 2001 From: mikota Date: Thu, 27 Jun 2024 07:03:55 +0200 Subject: [PATCH 291/974] Antilag fix Add lerping of enemy hitbox inbetween latest known position and latest svframe position in cases where antilag is seeking for a position after newest svframe --- src/action/p_antilag.c | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/action/p_antilag.c b/src/action/p_antilag.c index 9a3a8521c..d53250c04 100644 --- a/src/action/p_antilag.c +++ b/src/action/p_antilag.c @@ -38,8 +38,23 @@ float antilag_findseek(edict_t *ent, float time_stamp) { if (state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK] && state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK] <= time_stamp) { - if ((offs - 1) < 0) // never return a timestamp from the future aka erroneous crap - return -1; + if (offs == 0) { + float corrected_leveltime = level.time - FRAMETIME; + //need to do this because level.time gets updated after antilag_update during sv frame + //Com_Printf("###SHOT REWIND###\n"); + //Com_Printf("hitbox curr_timestamp %f\n", state->curr_timestamp); + //Com_Printf("antilag curr_timestamp %f\n", time_stamp); + //Com_Printf("level time %f\n", corrected_leveltime); + float advanced_since_svframe = state->curr_timestamp - corrected_leveltime; + //Com_Printf("advanced_since_svframe %f ms\n", advanced_since_svframe*1000); + if (advanced_since_svframe <= 0) return 0; + + float timestamp_since_svframe = time_stamp - corrected_leveltime; + //Com_Printf("antilag_timestamp_since_svframe %f ms\n", timestamp_since_svframe*1000); + if (timestamp_since_svframe <= 0) return state->seek; + if (timestamp_since_svframe >= advanced_since_svframe) return state->seek; + return - ((float)(state->seek) + (timestamp_since_svframe / advanced_since_svframe)); + } float frac = 1; float stamp_last = state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK]; @@ -90,9 +105,9 @@ void antilag_rewind_all(edict_t *ent) if (who->deadflag != DEAD_NO) continue; - float rewind_seek = antilag_findseek(who, time_to_seek); + float rewind_seek = antilag_findseek(who, time_to_seek); //Com_Printf("rewind seek %f\n", rewind_seek); - if (rewind_seek < 0) + if (rewind_seek == 0) continue; state->rewound = true; @@ -101,8 +116,23 @@ void antilag_rewind_all(edict_t *ent) VectorCopy(who->maxs, state->hold_maxs); //Com_Printf("seek diff %f\n", (float)state->seek - rewind_seek); - LerpVector(state->hist_origin[((int)rewind_seek) & ANTILAG_MASK], state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], rewind_seek - ((float)(int)rewind_seek), who->s.origin); - + int lerp_latest = 0; + if (rewind_seek < 0) { + lerp_latest = 1; + rewind_seek = -rewind_seek; + } + float lerpfrac = rewind_seek - (int)rewind_seek; + //Com_Printf("lerpfrac %f\n", lerpfrac); + vec3_t prev, next; + VectorCopy(state->hist_origin[(int)rewind_seek & ANTILAG_MASK], prev); + if (lerp_latest) { + VectorCopy(who->s.origin, next); + //Com_Printf("Using current origin as next\n"); + } else { + VectorCopy(state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], next); + //Com_Printf("Using hist_origin as next\n"); + } + LerpVector(prev, next, lerpfrac, who->s.origin); VectorCopy(state->hist_mins[(int)rewind_seek & ANTILAG_MASK], who->mins); VectorCopy(state->hist_maxs[(int)rewind_seek & ANTILAG_MASK], who->maxs); From 6a28ab81971c94d8f3bc997f21a2616ad8602354 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 27 Jun 2024 18:30:17 +0300 Subject: [PATCH 292/974] Always align cl.serverdelta down. To prevent cl.servertime from becoming negative. --- inc/shared/shared.h | 8 +++++++- src/client/entities.c | 2 +- src/client/parse.c | 2 +- src/server/user.c | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 5c0ec623d..a85a89486 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -317,7 +317,13 @@ static inline float anglemod(float a) return a; } -static inline int Q_align(int value, int align) +static inline int Q_align_down(int value, int align) +{ + int mod = value % align; + return value - mod; +} + +static inline int Q_align_up(int value, int align) { int mod = value % align; return mod ? value + align - mod : value; diff --git a/src/client/entities.c b/src/client/entities.c index ab1bd4c0c..ae83f5e1a 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -247,7 +247,7 @@ static void set_active_state(void) { cls.state = ca_active; - cl.serverdelta = Q_align(cl.frame.number, CL_FRAMEDIV); + cl.serverdelta = Q_align_down(cl.frame.number, CL_FRAMEDIV); cl.time = cl.servertime = 0; // set time, needed for demos #if USE_FPS cl.keytime = cl.keyservertime = 0; diff --git a/src/client/parse.c b/src/client/parse.c index 8370aa72a..4bbd14097 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -1180,7 +1180,7 @@ static void set_server_fps(int value) // fix time delta if (cls.state == ca_active) { int delta = cl.frame.number - cl.servertime / cl.frametime.time; - cl.serverdelta = Q_align(delta, cl.frametime.div); + cl.serverdelta = Q_align_down(delta, cl.frametime.div); } Com_DPrintf("client framediv=%d time=%d delta=%d\n", diff --git a/src/server/user.c b/src/server/user.c index 093cdf343..25a5dfff9 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -1495,7 +1495,7 @@ void SV_AlignKeyFrames(client_t *client) int framediv = sv.frametime.div / client->framediv; int framenum = sv.framenum / client->framediv; int frameofs = framenum % framediv; - int newnum = frameofs + Q_align(client->framenum, framediv); + int newnum = frameofs + Q_align_up(client->framenum, framediv); Com_DDPrintf("[%d] align %d --> %d (num = %d, div = %d, ofs = %d)\n", sv.framenum, client->framenum, newnum, framenum, framediv, frameofs); From 394bed59f58a352bf26940842f607ac66e9d32e9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 02:20:15 +0300 Subject: [PATCH 293/974] Fix broken SV_AlignKeyFrames(). --- src/server/user.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/user.c b/src/server/user.c index 25a5dfff9..4ca3735c8 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -1493,7 +1493,7 @@ static void SV_ParseDeltaUserinfo(void) void SV_AlignKeyFrames(client_t *client) { int framediv = sv.frametime.div / client->framediv; - int framenum = sv.framenum / client->framediv; + int framenum = (sv.framenum + client->framediv - 1) / client->framediv; int frameofs = framenum % framediv; int newnum = frameofs + Q_align_up(client->framenum, framediv); From dde9f6061244da032defafc84ff42ae992816aca Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 02:21:06 +0300 Subject: [PATCH 294/974] Error out if cl.servertime under- or overflows. --- src/client/entities.c | 7 +++++++ src/client/parse.c | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/client/entities.c b/src/client/entities.c index ae83f5e1a..5e926d818 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -379,6 +379,13 @@ void CL_DeltaFrame(void) // set server time framenum = cl.frame.number - cl.serverdelta; + + if (framenum < 0) + Com_Error(ERR_DROP, "%s: server time went backwards", __func__); + + if (framenum > INT_MAX / CL_FRAMETIME) + Com_Error(ERR_DROP, "%s: server time overflowed", __func__); + cl.servertime = framenum * CL_FRAMETIME; #if USE_FPS cl.keyservertime = (framenum / cl.frametime.div) * BASE_FRAMETIME; diff --git a/src/client/parse.c b/src/client/parse.c index 4bbd14097..1ddb2981d 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -237,6 +237,10 @@ static void CL_ParseFrame(int extrabits) currentframe = MSG_ReadLong(); deltaframe = MSG_ReadLong(); + if (currentframe < 0) { + Com_Error(ERR_DROP, "%s: currentframe < 0", __func__); + } + // BIG HACK to let old demos continue to work if (cls.serverProtocol != PROTOCOL_VERSION_OLD) { suppressed = MSG_ReadByte(); From 574e3cb8d4bc1939c38311740ea58d527044f721 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 08:32:09 +0300 Subject: [PATCH 295/974] Add more comments. --- inc/common/files.h | 1 + inc/common/intreadwrite.h | 9 +++++++++ inc/shared/shared.h | 1 + src/server/user.c | 2 ++ 4 files changed, 13 insertions(+) diff --git a/inc/common/files.h b/inc/common/files.h index 1debc7019..9f2778314 100644 --- a/inc/common/files.h +++ b/inc/common/files.h @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_LISTED_FILES 250000000 #define MAX_LISTED_DEPTH 8 +// prevents integer overflows #define MAX_LOADFILE 0x4001000 // 64 MiB + some slop #define FS_Malloc(size) Z_TagMalloc(size, TAG_FILESYSTEM) diff --git a/inc/common/intreadwrite.h b/inc/common/intreadwrite.h index 79d7018c6..320e559f2 100644 --- a/inc/common/intreadwrite.h +++ b/inc/common/intreadwrite.h @@ -18,7 +18,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once +// +// intreadwrite.h -- macros for fast unaligned integer R/W. +// + #if (defined __GNUC__) +// For GCC/Clang use a trick (stolen from FFmpeg) with packed structure. struct unaligned16 { uint16_t u; } __attribute__((packed, may_alias)); struct unaligned32 { uint32_t u; } __attribute__((packed, may_alias)); @@ -33,6 +38,7 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #define WN64(p, v) (((struct unaligned64 *)(p))->u = (v)) #elif (defined _MSC_VER) +// MSVC doesn't have strict aliasing, and allows unaligned access. #define RN16(p) (*(const uint16_t *)(p)) #define RN32(p) (*(const uint32_t *)(p)) @@ -45,6 +51,7 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #endif #if USE_LITTLE_ENDIAN +// We only optimize for little-endian arches here. #ifdef RN16 #define RL16(p) RN16(p) @@ -60,6 +67,8 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #endif // USE_LITTLE_ENDIAN +// Slow (but portable) macros for big-endian arches (or unsupported compilers). + #ifndef RL16 #define RL16(p) ((((const uint8_t *)(p))[1] << 8) | ((const uint8_t *)(p))[0]) #endif diff --git a/inc/shared/shared.h b/inc/shared/shared.h index a85a89486..0b9403244 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -114,6 +114,7 @@ void Com_Error(error_type_t code, const char *fmt, ...); #define Com_EPrintf(...) Com_LPrintf(PRINT_ERROR, __VA_ARGS__) #define Com_NPrintf(...) Com_LPrintf(PRINT_NOTICE, __VA_ARGS__) +// an assertion that's ALWAYS enabled. `expr' may have side effects. #define Q_assert(expr) \ do { if (!(expr)) Com_Error(ERR_FATAL, "%s: assertion `%s' failed", __func__, #expr); } while (0) diff --git a/src/server/user.c b/src/server/user.c index 4ca3735c8..6dcf88f03 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -1490,6 +1490,8 @@ static void SV_ParseDeltaUserinfo(void) } #if USE_FPS +// key frames must be aligned for all clients (and game) to ensure there isn't +// additional frame of latency for clients with framediv > 1. void SV_AlignKeyFrames(client_t *client) { int framediv = sv.frametime.div / client->framediv; From a6dcb6875b8ace80c62058c1b6245561dc445d58 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 11:44:28 +0300 Subject: [PATCH 296/974] Use BASE_FRAMERATE instead of hard-coded literals. --- src/client/demo.c | 4 ++-- src/client/main.c | 2 +- src/client/screen.c | 4 ++-- src/server/mvd/client.c | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/demo.c b/src/client/demo.c index effaded8f..b73b4ab46 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -254,7 +254,7 @@ static size_t format_demo_status(char *buffer, size_t size) size_t len = format_demo_size(buffer, size); int min, sec, frames = cls.demo.frames_written; - sec = frames / 10; frames %= 10; + sec = frames / BASE_FRAMERATE; frames %= BASE_FRAMERATE; min = sec / 60; sec %= 60; len += Q_scnprintf(buffer + len, size - len, ", %d:%02d.%d", @@ -797,7 +797,7 @@ void CL_EmitDemoSnapshot(void) if (cl_demosnaps->integer <= 0) return; - if (cls.demo.frames_read < cls.demo.last_snapshot + cl_demosnaps->integer * 10) + if (cls.demo.frames_read < cls.demo.last_snapshot + cl_demosnaps->integer * BASE_FRAMERATE) return; if (cls.demo.numsnapshots >= MAX_SNAPSHOTS) diff --git a/src/client/main.c b/src/client/main.c index b9ade40b4..741724c68 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2220,7 +2220,7 @@ static size_t CL_DemoPos_m(char *buffer, size_t size) else if (!MVD_GetDemoStatus(NULL, NULL, &framenum)) framenum = 0; - sec = framenum / 10; framenum %= 10; + sec = framenum / BASE_FRAMERATE; framenum %= BASE_FRAMERATE; min = sec / 60; sec %= 60; return Q_snprintf(buffer, size, "%d:%02d.%d", min, sec, framenum); diff --git a/src/client/screen.c b/src/client/screen.c index bd1fe56aa..28ce87e38 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -304,10 +304,10 @@ static void draw_progress_bar(float progress, bool paused, int framenum) R_DrawString(x, h, 0, MAX_STRING_CHARS, buffer, scr.font_pic); if (scr_demobar->integer > 1) { - int sec = framenum / 10; + int sec = framenum / BASE_FRAMERATE; int min = sec / 60; sec %= 60; - Q_scnprintf(buffer, sizeof(buffer), "%d:%02d.%d", min, sec, framenum % 10); + Q_scnprintf(buffer, sizeof(buffer), "%d:%02d.%d", min, sec, framenum % BASE_FRAMERATE); R_DrawString(0, h, 0, MAX_STRING_CHARS, buffer, scr.font_pic); } diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index e5c592191..a3ffdd686 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -558,7 +558,7 @@ static void demo_emit_snapshot(mvd_t *mvd) if (mvd_snaps->integer <= 0) return; - if (mvd->framenum < mvd->last_snapshot + mvd_snaps->integer * 10) + if (mvd->framenum < mvd->last_snapshot + mvd_snaps->integer * BASE_FRAMERATE) return; if (mvd->numsnapshots >= MAX_SNAPSHOTS) @@ -2677,7 +2677,7 @@ static const cmdreg_t c_mvd[] = { static void mvd_wait_delay_changed(cvar_t *self) { - self->integer = 10 * Cvar_ClampValue(self, 0, 60 * 60); + self->integer = BASE_FRAMERATE * Cvar_ClampValue(self, 0, 60 * 60); } /* From a14c527ef53ea6a2feb8f29c82d182d07e08b946 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 11:49:29 +0300 Subject: [PATCH 297/974] Skip timestamp when drawing notify lines. --- src/client/console.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/console.c b/src/client/console.c index fb26a96be..d42f6ebf9 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -684,7 +684,7 @@ DRAWING ============================================================================== */ -static int Con_DrawLine(int v, int row, float alpha) +static int Con_DrawLine(int v, int row, float alpha, bool notify) { consoleLine_t *line = &con.text[row & CON_TOTALLINES_MASK]; char *s = line->text; @@ -692,7 +692,9 @@ static int Con_DrawLine(int v, int row, float alpha) int x = CHAR_WIDTH; int w = con.linewidth; - if (line->ts_len) { + if (notify) { + s += line->ts_len; + } else if (line->ts_len) { R_SetColor(con.ts_color.u32); R_SetAlpha(alpha); x = R_DrawString(x, v, 0, line->ts_len, s, con.charsetImage); @@ -767,7 +769,7 @@ static void Con_DrawNotify(void) alpha = 1; // don't fade } - Con_DrawLine(v, i, alpha); + Con_DrawLine(v, i, alpha, true); v += CHAR_HEIGHT; } @@ -853,7 +855,7 @@ static void Con_DrawSolidConsole(void) if (con.current - row > CON_TOTALLINES - 1) break; // past scrollback wrap point - x = Con_DrawLine(y, row, 1); + x = Con_DrawLine(y, row, 1, false); if (i < 2) { widths[i] = x; } From befe9f85a4bc42c6f22551f9be56630e86a6f82b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 28 Jun 2024 11:58:33 +0300 Subject: [PATCH 298/974] More const in console code. --- src/client/console.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/client/console.c b/src/client/console.c index d42f6ebf9..80a0c7b2c 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -254,7 +254,7 @@ static void Con_Dump_f(void) // write the remaining lines for (; l <= con.current; l++) { char buffer[CON_LINEWIDTH + 1]; - char *p = con.text[l & CON_TOTALLINES_MASK].text; + const char *p = con.text[l & CON_TOTALLINES_MASK].text; int i; for (i = 0; i < CON_LINEWIDTH && p[i]; i++) @@ -686,8 +686,8 @@ DRAWING static int Con_DrawLine(int v, int row, float alpha, bool notify) { - consoleLine_t *line = &con.text[row & CON_TOTALLINES_MASK]; - char *s = line->text; + const consoleLine_t *line = &con.text[row & CON_TOTALLINES_MASK]; + const char *s = line->text; int flags = 0; int x = CHAR_WIDTH; int w = con.linewidth; @@ -1057,7 +1057,7 @@ void Con_DrawConsole(void) ============================================================================== */ -static void Con_Say(char *msg) +static void Con_Say(const char *msg) { CL_ClientCommand(va("say%s \"%s\"", con.chat == CHAT_TEAM ? "_team" : "", msg)); } @@ -1072,7 +1072,7 @@ static void Con_InteractiveMode(void) static void Con_Action(void) { - char *cmd = Prompt_Action(&con.prompt); + const char *cmd = Prompt_Action(&con.prompt); Con_InteractiveMode(); @@ -1147,8 +1147,8 @@ static void Con_Paste(char *(*func)(void)) // console lines are not necessarily NUL-terminated static void Con_ClearLine(char *buf, int row) { - consoleLine_t *line = &con.text[row & CON_TOTALLINES_MASK]; - char *s = line->text + line->ts_len; + const consoleLine_t *line = &con.text[row & CON_TOTALLINES_MASK]; + const char *s = line->text + line->ts_len; int w = con.linewidth - line->ts_len; while (w-- > 0 && *s) @@ -1159,7 +1159,7 @@ static void Con_ClearLine(char *buf, int row) static void Con_SearchUp(void) { char buf[CON_LINEWIDTH + 1]; - char *s = con.prompt.inputLine.text; + const char *s = con.prompt.inputLine.text; int top = con.current - CON_TOTALLINES + 1; if (top < 0) @@ -1180,7 +1180,7 @@ static void Con_SearchUp(void) static void Con_SearchDown(void) { char buf[CON_LINEWIDTH + 1]; - char *s = con.prompt.inputLine.text; + const char *s = con.prompt.inputLine.text; if (!*s) return; @@ -1329,7 +1329,7 @@ void Key_Message(int key) } if (key == K_ENTER || key == K_KP_ENTER) { - char *cmd = Prompt_Action(&con.chatPrompt); + const char *cmd = Prompt_Action(&con.chatPrompt); if (cmd) { Con_Say(cmd); From 91d9d5d300c8929f82c0a1b23f009e8cde2948ba Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 29 Jun 2024 10:10:47 +0300 Subject: [PATCH 299/974] Don't allow flare hash map grow too big. --- src/refresh/main.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index e27342b65..82a96a20d 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -446,9 +446,12 @@ static void GL_OccludeFlares(void) if (!q) { glquery_t new = { 0 }; + uint32_t map_size = HashMap_Size(gl_static.queries); + if (map_size >= MAX_EDICTS) + continue; qglGenQueries(1, &new.query); - HashMap_Insert(gl_static.queries, &e->skinnum, &new); - q = HashMap_GetValue(glquery_t, gl_static.queries, HashMap_Size(gl_static.queries) - 1); + Q_assert(!HashMap_Insert(gl_static.queries, &e->skinnum, &new)); + q = HashMap_GetValue(glquery_t, gl_static.queries, map_size); } make_flare_quad(e, 2.5f, points); From 12604fd77b7981be5b0d6c701efc7adcbb0107c0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 29 Jun 2024 12:42:29 +0300 Subject: [PATCH 300/974] Avoid double precision floating point math. --- inc/shared/shared.h | 10 ++++++++-- src/client/effects.c | 40 ++++++++++++++++++++-------------------- src/client/entities.c | 10 +++++----- src/client/locs.c | 2 +- src/client/newfx.c | 10 +++++----- src/client/sound/dma.c | 18 +++++++++--------- src/client/tent.c | 10 +++++----- src/client/view.c | 6 +++--- src/common/math.c | 8 ++++---- src/game/g_combat.c | 4 ++-- src/game/g_turret.c | 6 +++--- src/game/g_utils.c | 6 +++--- src/game/m_move.c | 8 ++++---- src/game/p_client.c | 2 +- src/game/p_view.c | 2 +- src/game/p_weapon.c | 6 +++--- src/refresh/main.c | 18 +++++++++--------- src/refresh/mesh.c | 10 +++++----- src/refresh/models.c | 2 +- src/refresh/shader.c | 4 ++-- src/refresh/state.c | 4 ++-- src/refresh/tess.c | 2 +- src/refresh/texture.c | 2 +- src/shared/shared.c | 12 ++++++------ 24 files changed, 104 insertions(+), 98 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 0b9403244..764307e00 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -169,8 +169,14 @@ typedef struct { int x, y, width, height; } vrect_t; -#define DEG2RAD(a) ((a) * (M_PI / 180)) -#define RAD2DEG(a) ((a) * (180 / M_PI)) +#ifndef M_PIf +#define M_PIf 3.14159265358979323846f +#define M_SQRT2f 1.41421356237309504880f +#define M_SQRT1_2f 0.70710678118654752440f +#endif + +#define DEG2RAD(a) ((a) * (M_PIf / 180)) +#define RAD2DEG(a) ((a) * (180 / M_PIf)) #define Q_ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) diff --git a/src/client/effects.c b/src/client/effects.c index 315b66185..63bc8fd9c 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -1134,15 +1134,15 @@ void CL_BigTeleportParticles(const vec3_t org) p->color = colortable[Q_rand() & 3]; - angle = (Q_rand() & 1023) * (M_PI * 2 / 1023); + angle = (Q_rand() & 1023) * (M_PIf * 2 / 1023); dist = Q_rand() & 31; - p->org[0] = org[0] + cos(angle) * dist; - p->vel[0] = cos(angle) * (70 + (Q_rand() & 63)); - p->accel[0] = -cos(angle) * 100; + p->org[0] = org[0] + cosf(angle) * dist; + p->vel[0] = cosf(angle) * (70 + (Q_rand() & 63)); + p->accel[0] = -cosf(angle) * 100; - p->org[1] = org[1] + sin(angle) * dist; - p->vel[1] = sin(angle) * (70 + (Q_rand() & 63)); - p->accel[1] = -sin(angle) * 100; + p->org[1] = org[1] + sinf(angle) * dist; + p->vel[1] = sinf(angle) * (70 + (Q_rand() & 63)); + p->accel[1] = -sinf(angle) * 100; p->org[2] = org[2] + 8 + (Q_rand() % 90); p->vel[2] = -100 + (int)(Q_rand() & 31); @@ -1452,8 +1452,8 @@ void CL_OldRailTrail(void) VectorClear(p->accel); d = i * 0.1f; - c = cos(d); - s = sin(d); + c = cosf(d); + s = sinf(d); VectorScale(right, c, dir); VectorMA(dir, s, up, dir); @@ -1566,11 +1566,11 @@ static void CL_FlyParticles(const vec3_t origin, int count) ltime = cl.time * 0.001f; for (i = 0; i < count; i += 2) { angle = ltime * avelocities[i][0]; - sy = sin(angle); - cy = cos(angle); + sy = sinf(angle); + cy = cosf(angle); angle = ltime * avelocities[i][1]; - sp = sin(angle); - cp = cos(angle); + sp = sinf(angle); + cp = cosf(angle); forward[0] = cp * cy; forward[1] = cp * sy; @@ -1582,7 +1582,7 @@ static void CL_FlyParticles(const vec3_t origin, int count) p->time = cl.time; - dist = sin(ltime + i) * 64; + dist = sinf(ltime + i) * 64; p->org[0] = origin[0] + bytedirs[i][0] * dist + forward[0] * BEAMLENGTH; p->org[1] = origin[1] + bytedirs[i][1] * dist + forward[1] * BEAMLENGTH; p->org[2] = origin[2] + bytedirs[i][2] * dist + forward[2] * BEAMLENGTH; @@ -1643,11 +1643,11 @@ void CL_BfgParticles(const entity_t *ent) ltime = cl.time * 0.001f; for (i = 0; i < NUMVERTEXNORMALS; i++) { angle = ltime * avelocities[i][0]; - sy = sin(angle); - cy = cos(angle); + sy = sinf(angle); + cy = cosf(angle); angle = ltime * avelocities[i][1]; - sp = sin(angle); - cp = cos(angle); + sp = sinf(angle); + cp = cosf(angle); forward[0] = cp * cy; forward[1] = cp * sy; @@ -1659,7 +1659,7 @@ void CL_BfgParticles(const entity_t *ent) p->time = cl.time; - dist = sin(ltime + i) * 64; + dist = sinf(ltime + i) * 64; p->org[0] = ent->origin[0] + bytedirs[i][0] * dist + forward[0] * BEAMLENGTH; p->org[1] = ent->origin[1] + bytedirs[i][1] * dist + forward[1] * BEAMLENGTH; p->org[2] = ent->origin[2] + bytedirs[i][2] * dist + forward[2] * BEAMLENGTH; @@ -1668,7 +1668,7 @@ void CL_BfgParticles(const entity_t *ent) VectorClear(p->accel); dist = Distance(p->org, ent->origin) / 90.0f; - p->color = floor(0xd0 + dist * 7); + p->color = floorf(0xd0 + dist * 7); p->alpha = 1.0f - dist; p->alphavel = INSTANT_PARTICLE; diff --git a/src/client/entities.c b/src/client/entities.c index 5e926d818..566d55ee2 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -504,7 +504,7 @@ static void CL_AddPacketEntities(void) // brush models can auto animate their frames autoanim = 2 * cl.time / 1000; - autobob = 5 * sin(cl.time / 400.0f); + autobob = 5 * sinf(cl.time / 400.0f); memset(&ent, 0, sizeof(ent)); @@ -990,7 +990,7 @@ static void CL_AddPacketEntities(void) V_AddLight(ent.origin, 225, 1.0f, 1.0f, 0.0f); } else if (effects & EF_TRACKERTRAIL) { if (effects & EF_TRACKER) { - float intensity = 50 + (500 * (sin(cl.time / 500.0f) + 1.0f)); + float intensity = 50 + (500 * (sinf(cl.time / 500.0f) + 1.0f)); V_AddLight(ent.origin, intensity, -1.0f, -1.0f, -1.0f); } else { CL_Tracker_Shell(cent, ent.origin); @@ -1222,8 +1222,8 @@ static void CL_SetupThirdPersionView(void) angle = DEG2RAD(cl_thirdperson_angle->value); range = cl_thirdperson_range->value; - fscale = cos(angle); - rscale = sin(angle); + fscale = cosf(angle); + rscale = sinf(angle); VectorMA(cl.refdef.vieworg, -range * fscale, cl.v_forward, cl.refdef.vieworg); VectorMA(cl.refdef.vieworg, -range * rscale, cl.v_right, cl.refdef.vieworg); @@ -1236,7 +1236,7 @@ static void CL_SetupThirdPersionView(void) VectorSubtract(focus, cl.refdef.vieworg, focus); dist = sqrtf(focus[0] * focus[0] + focus[1] * focus[1]); - cl.refdef.viewangles[PITCH] = -RAD2DEG(atan2(focus[2], dist)); + cl.refdef.viewangles[PITCH] = -RAD2DEG(atan2f(focus[2], dist)); cl.refdef.viewangles[YAW] -= cl_thirdperson_angle->value; cl.thirdPersonView = true; diff --git a/src/client/locs.c b/src/client/locs.c index 446851631..21193a21f 100644 --- a/src/client/locs.c +++ b/src/client/locs.c @@ -200,7 +200,7 @@ void LOC_AddLocationsToScene(void) VectorCopy(loc->origin, ent.origin); if (loc == nearest) { - ent.origin[2] += 10.0f * sin(cl.time * 0.01f); + ent.origin[2] += 10.0f * sinf(cl.time * 0.01f); V_AddLight(loc->origin, 200, 1, 1, 1); } diff --git a/src/client/newfx.c b/src/client/newfx.c index 08d03bd8a..127879959 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -204,12 +204,12 @@ void CL_Heatbeam(const vec3_t start, const vec3_t forward) VectorScale(vec, step, vec); - rstep = M_PI / 10.0f; + rstep = M_PIf / 10.0f; for (i = start_pt; i < len; i += step) { if (i > step * 5) // don't bother after the 5th ring break; - for (rot = 0; rot < M_PI * 2; rot += rstep) { + for (rot = 0; rot < M_PIf * 2; rot += rstep) { p = CL_AllocParticle(); if (!p) return; @@ -217,8 +217,8 @@ void CL_Heatbeam(const vec3_t start, const vec3_t forward) p->time = cl.time; VectorClear(p->accel); variance = 0.5f; - c = cos(rot) * variance; - s = sin(rot) * variance; + c = cosf(rot) * variance; + s = sinf(rot) * variance; // trim it so it looks like it's starting at the origin if (i < 10) { @@ -332,7 +332,7 @@ void CL_TrackerTrail(const vec3_t start, const vec3_t end, int particleColor) p->alphavel = -2.0f; p->color = particleColor; dist = DotProduct(move, forward); - VectorMA(move, 8 * cos(dist), up, p->org); + VectorMA(move, 8 * cosf(dist), up, p->org); for (j = 0; j < 3; j++) { p->vel[j] = 0; p->accel[j] = 0; diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index fccea572f..965fdc727 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -232,7 +232,7 @@ static void TransferPaintBuffer(samplepair_t *samp, int endtime) if (s_testsound->integer) { // write a fixed sine wave for (i = 0; i < endtime - s_paintedtime; i++) { - samp[i].left = samp[i].right = sin((s_paintedtime + i) * 0.1f) * 20000; + samp[i].left = samp[i].right = sinf((s_paintedtime + i) * 0.1f) * 20000; } } @@ -276,10 +276,10 @@ static void s_underwater_gain_hf_changed(cvar_t *self) // Limit to -60dB gain = max(gain, 0.001f); - float w0 = M_PI * 2.0f * f0norm; - float sin_w0 = sin(w0); - float cos_w0 = cos(w0); - float alpha = sin_w0 / 2.0f * M_SQRT2; + float w0 = M_PIf * 2.0f * f0norm; + float sin_w0 = sinf(w0); + float cos_w0 = cosf(w0); + float alpha = sin_w0 / 2.0f * M_SQRT2f; float sqrtgain_alpha_2 = 2.0f * sqrtf(gain) * alpha; float a0; @@ -344,8 +344,8 @@ PAINTFUNC(PaintMono8) PAINTFUNC(PaintStereoDmix8) { - float leftvol = ch->leftvol * snd_vol * (256 * M_SQRT1_2); - float rightvol = ch->rightvol * snd_vol * (256 * M_SQRT1_2); + float leftvol = ch->leftvol * snd_vol * (256 * M_SQRT1_2f); + float rightvol = ch->rightvol * snd_vol * (256 * M_SQRT1_2f); const uint8_t *sfx = sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { @@ -380,8 +380,8 @@ PAINTFUNC(PaintMono16) PAINTFUNC(PaintStereoDmix16) { - float leftvol = ch->leftvol * snd_vol * M_SQRT1_2; - float rightvol = ch->rightvol * snd_vol * M_SQRT1_2; + float leftvol = ch->leftvol * snd_vol * M_SQRT1_2f; + float rightvol = ch->rightvol * snd_vol * M_SQRT1_2f; const int16_t *sfx = (const int16_t *)sc->data + ch->pos * 2; for (int i = 0; i < count; i++, samp++, sfx += 2) { diff --git a/src/client/tent.c b/src/client/tent.c index 6f9b9bf6e..c6de5cfaf 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -504,7 +504,7 @@ static void CL_AddExplosions(void) } frac = (cl.time - ex->start) * BASE_1_FRAMETIME; - f = floor(frac); + f = floorf(frac); ent = &ex->ent; @@ -1175,8 +1175,8 @@ static void CL_RailSpiral(void) VectorClear(p->accel); d = i * 0.1f; - c = cos(d); - s = sin(d); + c = cosf(d); + s = sinf(d); VectorScale(right, c, dir); VectorMA(dir, s, up, dir); @@ -1210,9 +1210,9 @@ static void CL_RailTrail(void) static void dirtoangles(vec3_t angles) { - angles[0] = RAD2DEG(acos(te.dir[2])); + angles[0] = RAD2DEG(acosf(te.dir[2])); if (te.dir[0]) - angles[1] = RAD2DEG(atan2(te.dir[1], te.dir[0])); + angles[1] = RAD2DEG(atan2f(te.dir[1], te.dir[0])); else if (te.dir[1] > 0) angles[1] = 90; else if (te.dir[1] < 0) diff --git a/src/client/view.c b/src/client/view.c index 00229afb3..0caad6b31 100644 --- a/src/client/view.c +++ b/src/client/view.c @@ -342,10 +342,10 @@ float V_CalcFov(float fov_x, float width, float height) if (fov_x < 0.75f || fov_x > 179) Com_Error(ERR_DROP, "%s: bad fov: %f", __func__, fov_x); - x = width / tan(fov_x * (M_PI / 360)); + x = width / tanf(fov_x * (M_PIf / 360)); - a = atan(height / x); - a = a * (360 / M_PI); + a = atanf(height / x); + a = a * (360 / M_PIf); return a; } diff --git a/src/common/math.c b/src/common/math.c index 00ee3c2e8..c2ad40c01 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -39,7 +39,7 @@ void vectoangles2(const vec3_t value1, vec3_t angles) pitch = 270; } else { if (value1[0]) - yaw = RAD2DEG(atan2(value1[1], value1[0])); + yaw = RAD2DEG(atan2f(value1[1], value1[0])); else if (value1[1] > 0) yaw = 90; else @@ -49,7 +49,7 @@ void vectoangles2(const vec3_t value1, vec3_t angles) yaw += 360; forward = sqrtf(value1[0] * value1[0] + value1[1] * value1[1]); - pitch = RAD2DEG(atan2(value1[2], forward)); + pitch = RAD2DEG(atan2f(value1[2], forward)); if (pitch < 0) pitch += 360; } @@ -360,8 +360,8 @@ void SetupRotationMatrix(vec3_t matrix[3], const vec3_t dir, float degrees) vec_t angle, s, c, one_c, xx, yy, zz, xy, yz, zx, xs, ys, zs; angle = DEG2RAD(degrees); - s = sin(angle); - c = cos(angle); + s = sinf(angle); + c = cosf(angle); one_c = 1.0F - c; xx = dir[0] * dir[0]; diff --git a/src/game/g_combat.c b/src/game/g_combat.c index 6ff9e31d8..a46df3984 100644 --- a/src/game/g_combat.c +++ b/src/game/g_combat.c @@ -260,9 +260,9 @@ static int CheckArmor(edict_t *ent, const vec3_t point, const vec3_t normal, int armor = GetItemByIndex(index); if (dflags & DAMAGE_ENERGY) - save = ceil(((const gitem_armor_t *)armor->info)->energy_protection * damage); + save = ceilf(((const gitem_armor_t *)armor->info)->energy_protection * damage); else - save = ceil(((const gitem_armor_t *)armor->info)->normal_protection * damage); + save = ceilf(((const gitem_armor_t *)armor->info)->normal_protection * damage); if (save >= client->pers.inventory[index]) save = client->pers.inventory[index]; diff --git a/src/game/g_turret.c b/src/game/g_turret.c index 015741296..d92a157ec 100644 --- a/src/game/g_turret.c +++ b/src/game/g_turret.c @@ -168,8 +168,8 @@ void turret_breach_think(edict_t *self) // x & y angle = self->s.angles[1] + self->owner->move_origin[1]; angle = DEG2RAD(angle); - target[0] = SnapToEights(self->s.origin[0] + cos(angle) * self->owner->move_origin[0]); - target[1] = SnapToEights(self->s.origin[1] + sin(angle) * self->owner->move_origin[0]); + target[0] = SnapToEights(self->s.origin[0] + cosf(angle) * self->owner->move_origin[0]); + target[1] = SnapToEights(self->s.origin[1] + sinf(angle) * self->owner->move_origin[0]); target[2] = self->owner->s.origin[2]; VectorSubtract(target, self->owner->s.origin, dir); @@ -178,7 +178,7 @@ void turret_breach_think(edict_t *self) // z angle = DEG2RAD(self->s.angles[PITCH]); - target_z = SnapToEights(self->s.origin[2] + self->owner->move_origin[0] * tan(angle) + self->owner->move_origin[2]); + target_z = SnapToEights(self->s.origin[2] + self->owner->move_origin[0] * tanf(angle) + self->owner->move_origin[2]); diff = target_z - self->owner->s.origin[2]; self->owner->velocity[2] = diff * 1.0f / FRAMETIME; diff --git a/src/game/g_utils.c b/src/game/g_utils.c index 4bd19f6ba..9a06cd585 100644 --- a/src/game/g_utils.c +++ b/src/game/g_utils.c @@ -257,7 +257,7 @@ float vectoyaw(vec3_t vec) else if (vec[YAW] < 0) yaw = -90; } else { - yaw = (int)RAD2DEG(atan2(vec[YAW], vec[PITCH])); + yaw = (int)RAD2DEG(atan2f(vec[YAW], vec[PITCH])); if (yaw < 0) yaw += 360; } @@ -278,7 +278,7 @@ void vectoangles(vec3_t value1, vec3_t angles) pitch = 270; } else { if (value1[0]) - yaw = (int)RAD2DEG(atan2(value1[1], value1[0])); + yaw = (int)RAD2DEG(atan2f(value1[1], value1[0])); else if (value1[1] > 0) yaw = 90; else @@ -287,7 +287,7 @@ void vectoangles(vec3_t value1, vec3_t angles) yaw += 360; forward = sqrtf(value1[0] * value1[0] + value1[1] * value1[1]); - pitch = (int)RAD2DEG(atan2(value1[2], forward)); + pitch = (int)RAD2DEG(atan2f(value1[2], forward)); if (pitch < 0) pitch += 360; } diff --git a/src/game/m_move.c b/src/game/m_move.c index c46d77b64..d6012e644 100644 --- a/src/game/m_move.c +++ b/src/game/m_move.c @@ -319,8 +319,8 @@ bool SV_StepDirection(edict_t *ent, float yaw, float dist) M_ChangeYaw(ent); yaw = DEG2RAD(yaw); - move[0] = cos(yaw) * dist; - move[1] = sin(yaw) * dist; + move[0] = cosf(yaw) * dist; + move[1] = sinf(yaw) * dist; move[2] = 0; VectorCopy(ent->s.origin, oldorigin); @@ -490,8 +490,8 @@ bool M_walkmove(edict_t *ent, float yaw, float dist) return false; yaw = DEG2RAD(yaw); - move[0] = cos(yaw) * dist; - move[1] = sin(yaw) * dist; + move[0] = cosf(yaw) * dist; + move[1] = sinf(yaw) * dist; move[2] = 0; return SV_movestep(ent, move, true); diff --git a/src/game/p_client.c b/src/game/p_client.c index 2c3163c47..43c204863 100644 --- a/src/game/p_client.c +++ b/src/game/p_client.c @@ -446,7 +446,7 @@ void LookAtKiller(edict_t *self, edict_t *inflictor, edict_t *attacker) } if (dir[0]) - self->client->killer_yaw = RAD2DEG(atan2(dir[1], dir[0])); + self->client->killer_yaw = RAD2DEG(atan2f(dir[1], dir[0])); else { self->client->killer_yaw = 0; if (dir[1] > 0) diff --git a/src/game/p_view.c b/src/game/p_view.c index 123a72660..389de42d7 100644 --- a/src/game/p_view.c +++ b/src/game/p_view.c @@ -927,7 +927,7 @@ void ClientEndServerFrame(edict_t *ent) bobtime *= 4; bobcycle = (int)bobtime; - bobfracsin = fabs(sin(bobtime * M_PI)); + bobfracsin = fabsf(sinf(bobtime * M_PIf)); // detect hitting the floor P_FallingDamage(ent); diff --git a/src/game/p_weapon.c b/src/game/p_weapon.c index 23410bf65..3a101ff49 100644 --- a/src/game/p_weapon.c +++ b/src/game/p_weapon.c @@ -784,10 +784,10 @@ void Weapon_HyperBlaster_Fire(edict_t *ent) } NoAmmoWeaponChange(ent); } else { - rotation = (ent->client->ps.gunframe - 5) * (M_PI / 3); - offset[0] = -4 * sin(rotation); + rotation = (ent->client->ps.gunframe - 5) * (M_PIf / 3); + offset[0] = -4 * sinf(rotation); offset[1] = 0; - offset[2] = 4 * cos(rotation); + offset[2] = 4 * cosf(rotation); if ((ent->client->ps.gunframe == 6) || (ent->client->ps.gunframe == 9)) effect = EF_HYPERBLASTER; diff --git a/src/refresh/main.c b/src/refresh/main.c index 82a96a20d..0551c36f4 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -98,8 +98,8 @@ static void GL_SetupFrustum(void) // right/left angle = DEG2RAD(glr.fd.fov_x / 2); - sf = sin(angle); - cf = cos(angle); + sf = sinf(angle); + cf = cosf(angle); VectorScale(glr.viewaxis[0], sf, forward); VectorScale(glr.viewaxis[1], cf, left); @@ -109,8 +109,8 @@ static void GL_SetupFrustum(void) // top/bottom angle = DEG2RAD(glr.fd.fov_y / 2); - sf = sin(angle); - cf = cos(angle); + sf = sinf(angle); + cf = cosf(angle); VectorScale(glr.viewaxis[0], sf, forward); VectorScale(glr.viewaxis[2], cf, up); @@ -1012,14 +1012,14 @@ static void GL_InitTables(void) for (i = 0; i < NUMVERTEXNORMALS; i++) { v = bytedirs[i]; - lat = acos(v[2]); - lng = atan2(v[1], v[0]); - gl_static.latlngtab[i][0] = (int)(lat * (float)(255 / (2 * M_PI))) & 255; - gl_static.latlngtab[i][1] = (int)(lng * (float)(255 / (2 * M_PI))) & 255; + lat = acosf(v[2]); + lng = atan2f(v[1], v[0]); + gl_static.latlngtab[i][0] = (int)(lat * (255 / (2 * M_PIf))) & 255; + gl_static.latlngtab[i][1] = (int)(lng * (255 / (2 * M_PIf))) & 255; } for (i = 0; i < 256; i++) { - gl_static.sintab[i] = sin(i * (2 * M_PI / 255)); + gl_static.sintab[i] = sinf(i * (2 * M_PIf / 255)); } } diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 0564ec9fb..9635f751b 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -57,10 +57,10 @@ static void setup_dotshading(void) // matches the anormtab.h precalculations yaw = -DEG2RAD(glr.ent->angles[YAW]); - cy = cos(yaw); - sy = sin(yaw); - cp = cos(-M_PI / 4); - sp = sin(-M_PI / 4); + cy = cosf(yaw); + sy = sinf(yaw); + cp = cosf(-M_PIf / 4); + sp = sinf(-M_PIf / 4); shadedir[0] = cp * cy; shadedir[1] = cp * sy; shadedir[2] = -sp; @@ -377,7 +377,7 @@ static void setup_color(void) } if (flags & RF_GLOW) { - f = 0.1f * sin(glr.fd.time * 7); + f = 0.1f * sinf(glr.fd.time * 7); for (i = 0; i < 3; i++) { m = color[i] * 0.8f; color[i] += f; diff --git a/src/refresh/models.c b/src/refresh/models.c index df6abb3ba..486d555c3 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -819,7 +819,7 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto CrossProduct(d1, d2, norm); VectorNormalize(norm); - float angle = acos(DotProduct(d1, d2)); + float angle = acosf(DotProduct(d1, d2)); VectorScale(norm, angle, norm); for (j = 0; j < 3; j++) { diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 158349d4f..752ef3870 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -385,8 +385,8 @@ static void shader_setup_2d(void) gls.u_block.w_amp[0] = 0.00666f; gls.u_block.w_amp[1] = 0.00666f; - gls.u_block.w_phase[0] = M_PI * 10; - gls.u_block.w_phase[1] = M_PI * 10; + gls.u_block.w_phase[0] = M_PIf * 10; + gls.u_block.w_phase[1] = M_PIf * 10; } static void shader_setup_3d(void) diff --git a/src/refresh/state.c b/src/refresh/state.c index 320a2b3fd..dc12a832b 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -187,10 +187,10 @@ void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) else zfar = gl_static.world.size * 2; - xmax = znear * tan(fov_x * (M_PI / 360)); + xmax = znear * tanf(fov_x * (M_PIf / 360)); xmin = -xmax; - ymax = znear * tan(fov_y * (M_PI / 360)); + ymax = znear * tanf(fov_y * (M_PIf / 360)); ymin = -ymax; width = xmax - xmin; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index a502e9687..e4c0ea7f4 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -71,7 +71,7 @@ void GL_Flush2D(void) tess.flags = 0; } -#define PARTICLE_SIZE (1 + (float)M_SQRT1_2) +#define PARTICLE_SIZE (1 + M_SQRT1_2f) #define PARTICLE_SCALE (1 / (2 * PARTICLE_SIZE)) void GL_DrawParticles(void) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index d14c9e054..27c8e732d 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -613,7 +613,7 @@ static void GL_Upscale32(byte *data, int width, int height, int maxlevel, imaget if (upload_width != width || upload_height != height) { float du = upload_width / (float)width; float dv = upload_height / (float)height; - float bias = -log(max(du, dv)) / M_LN2; + float bias = -log2f(max(du, dv)); if (gl_config.caps & QGL_CAP_TEXTURE_LOD_BIAS) qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, bias); diff --git a/src/shared/shared.c b/src/shared/shared.c index 8a49900dc..cb9f8b6a7 100644 --- a/src/shared/shared.c +++ b/src/shared/shared.c @@ -26,14 +26,14 @@ void AngleVectors(const vec3_t angles, vec3_t forward, vec3_t right, vec3_t up) float sr, sp, sy, cr, cp, cy; angle = DEG2RAD(angles[YAW]); - sy = sin(angle); - cy = cos(angle); + sy = sinf(angle); + cy = cosf(angle); angle = DEG2RAD(angles[PITCH]); - sp = sin(angle); - cp = cos(angle); + sp = sinf(angle); + cp = cosf(angle); angle = DEG2RAD(angles[ROLL]); - sr = sin(angle); - cr = cos(angle); + sr = sinf(angle); + cr = cosf(angle); if (forward) { forward[0] = cp * cy; From 5dee69ff0f2c43337c0648d9ae7d2fc0f64ed823 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 29 Jun 2024 13:09:21 +0300 Subject: [PATCH 301/974] =?UTF-8?q?Clamp=20=E2=80=98s=5Funderwater=5Fgain?= =?UTF-8?q?=5Fhf=E2=80=99=20cvar=20to=200.001-1.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/sound/al.c | 2 +- src/client/sound/dma.c | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/client/sound/al.c b/src/client/sound/al.c index c1717a1e1..351e5ff26 100644 --- a/src/client/sound/al.c +++ b/src/client/sound/al.c @@ -55,7 +55,7 @@ static void s_underwater_gain_hf_changed(cvar_t *self) s_underwater_flag = false; } - qalFilterf(s_underwater_filter, AL_LOWPASS_GAINHF, Cvar_ClampValue(self, 0, 1)); + qalFilterf(s_underwater_filter, AL_LOWPASS_GAINHF, Cvar_ClampValue(self, 0.001f, 1)); } static bool AL_Init(void) diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index 965fdc727..444bae89a 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -96,7 +96,6 @@ static void DMA_PageInSfx(sfx_t *sfx) Com_PageInMemory(sc->data, sc->size); } - /* =============================================================================== @@ -167,7 +166,6 @@ static void DMA_DropRawSamples(void) s_rawend = s_paintedtime; } - /* =============================================================================== @@ -266,16 +264,11 @@ typedef struct { static hist_t hist[2]; static float a1, a2, b0, b1, b2; -// Implements "high shelf" biquad filter. This is what OpenAL Soft uses for -// AL_FILTER_LOWPASS. +// Implementation of "high shelf" biquad filter from OpenAL Soft. static void s_underwater_gain_hf_changed(cvar_t *self) { + float gain = Cvar_ClampValue(self, 0.001f, 1); float f0norm = 5000.0f / dma.speed; - float gain = Cvar_ClampValue(self, 0, 1); - - // Limit to -60dB - gain = max(gain, 0.001f); - float w0 = M_PIf * 2.0f * f0norm; float sin_w0 = sinf(w0); float cos_w0 = cosf(w0); @@ -291,7 +284,7 @@ static void s_underwater_gain_hf_changed(cvar_t *self) a1 = ((gain-1.0f) - (gain+1.0f) * cos_w0) * 2.0f; a2 = (gain+1.0f) - (gain-1.0f) * cos_w0 - sqrtgain_alpha_2; - a1 /= a0; a2 /= a0; b0 /= a0; b1 /= a0; b2 /= a0; + a1 /= a0, a2 /= a0, b0 /= a0, b1 /= a0, b2 /= a0; } static void filter_ch(hist_t *hist, float *samp, int count) From f362483f02831d998348fddb017cf6c948b83004 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 29 Jun 2024 14:49:34 +0300 Subject: [PATCH 302/974] =?UTF-8?q?Build=20with=20=E2=80=98-fno-trapping-m?= =?UTF-8?q?ath=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index 3665d080f..040523078 100644 --- a/meson.build +++ b/meson.build @@ -219,6 +219,7 @@ if cc.get_argument_syntax() == 'gcc' '-fsigned-char', '-fms-extensions', '-fno-math-errno', + '-fno-trapping-math', '-Wpointer-arith', '-Wformat-security', '-Werror=vla', From 9413c117c7a6ca214f1a455d03dc02c57311ce99 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 30 Jun 2024 12:35:04 +0300 Subject: [PATCH 303/974] =?UTF-8?q?Move=20=E2=80=98sys=5Fforcegamelib?= =?UTF-8?q?=E2=80=99=20cvar=20to=20common=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/common/common.h | 2 ++ inc/system/system.h | 1 - src/common/common.c | 4 ++++ src/unix/system.c | 2 -- src/windows/system.c | 3 --- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index bcae8e390..219c916a2 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -170,6 +170,8 @@ extern cvar_t *allow_download_others; extern cvar_t *rcon_password; +extern cvar_t *sys_forcegamelib; + #if USE_CLIENT // host_speeds times extern unsigned time_before_game; diff --git a/inc/system/system.h b/inc/system/system.h index dc662f264..bf079af38 100644 --- a/inc/system/system.h +++ b/inc/system/system.h @@ -80,4 +80,3 @@ bool Sys_SetNonBlock(int fd, bool nb); extern cvar_t *sys_basedir; extern cvar_t *sys_libdir; extern cvar_t *sys_homedir; -extern cvar_t *sys_forcegamelib; diff --git a/src/common/common.c b/src/common/common.c index a0b43651a..e98c83860 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -115,6 +115,8 @@ cvar_t *allow_download_others; cvar_t *rcon_password; +cvar_t *sys_forcegamelib; + const char com_version_string[] = APPLICATION " " VERSION " " __DATE__ " " BUILDSTRING " " CPUSTRING; @@ -915,6 +917,8 @@ void Qcommon_Init(int argc, char **argv) rcon_password = Cvar_Get("rcon_password", "", CVAR_PRIVATE); + sys_forcegamelib = Cvar_Get("sys_forcegamelib", "", CVAR_NOSET); + Cmd_AddCommand("z_stats", Z_Stats_f); //Cmd_AddCommand("setenv", Com_Setenv_f); diff --git a/src/unix/system.c b/src/unix/system.c index 9e52c2ed6..4a1f19383 100644 --- a/src/unix/system.c +++ b/src/unix/system.c @@ -45,7 +45,6 @@ with this program; if not, write to the Free Software Foundation, Inc., cvar_t *sys_basedir; cvar_t *sys_libdir; cvar_t *sys_homedir; -cvar_t *sys_forcegamelib; extern cvar_t *console_prefix; @@ -207,7 +206,6 @@ void Sys_Init(void) } sys_homedir = Cvar_Get("homedir", homedir, CVAR_NOSET); sys_libdir = Cvar_Get("libdir", LIBDIR, CVAR_NOSET); - sys_forcegamelib = Cvar_Get("sys_forcegamelib", "", CVAR_NOSET); sys_parachute = Cvar_Get("sys_parachute", "1", CVAR_NOSET); if (sys_parachute->integer) { diff --git a/src/windows/system.c b/src/windows/system.c index e0de87265..fd2c388fe 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -44,7 +44,6 @@ static cvar_t *sys_exitonerror; cvar_t *sys_basedir; cvar_t *sys_libdir; cvar_t *sys_homedir; -cvar_t *sys_forcegamelib; /* =============================================================================== @@ -934,8 +933,6 @@ void Sys_Init(void) // specifies per-user writable directory for demos, screenshots, etc sys_homedir = Cvar_Get("homedir", "", CVAR_NOSET); - sys_forcegamelib = Cvar_Get("sys_forcegamelib", "", CVAR_NOSET); - sys_exitonerror = Cvar_Get("sys_exitonerror", "0", 0); #if USE_WINSVC From 91f4cc98d721d1dbbf784b59399110c963c39d3a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 30 Jun 2024 12:45:32 +0300 Subject: [PATCH 304/974] =?UTF-8?q?Add=20=E2=80=98sys=5Fallow=5Funsafe=5Fs?= =?UTF-8?q?avegames=E2=80=99=20cvar.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/common/common.h | 4 ++++ src/common/common.c | 8 ++++++++ src/server/save.c | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/inc/common/common.h b/inc/common/common.h index 219c916a2..0687495e2 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -172,6 +172,10 @@ extern cvar_t *rcon_password; extern cvar_t *sys_forcegamelib; +#if USE_SAVEGAMES +extern cvar_t *sys_allow_unsafe_savegames; +#endif + #if USE_CLIENT // host_speeds times extern unsigned time_before_game; diff --git a/src/common/common.c b/src/common/common.c index e98c83860..b80a8d9dd 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -117,6 +117,10 @@ cvar_t *rcon_password; cvar_t *sys_forcegamelib; +#if USE_SAVEGAMES +cvar_t *sys_allow_unsafe_savegames; +#endif + const char com_version_string[] = APPLICATION " " VERSION " " __DATE__ " " BUILDSTRING " " CPUSTRING; @@ -919,6 +923,10 @@ void Qcommon_Init(int argc, char **argv) sys_forcegamelib = Cvar_Get("sys_forcegamelib", "", CVAR_NOSET); +#if USE_SAVEGAMES + sys_allow_unsafe_savegames = Cvar_Get("sys_allow_unsafe_savegames", "0", CVAR_NOSET); +#endif + Cmd_AddCommand("z_stats", Z_Stats_f); //Cmd_AddCommand("setenv", Com_Setenv_f); diff --git a/src/server/save.c b/src/server/save.c index 52bda1d3d..b47df8f05 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -581,6 +581,12 @@ void SV_CheckForEnhancedSavegames(void) return; } + if (sys_allow_unsafe_savegames->integer) { + Com_WPrintf("Use of unsafe savegames forced from command line.\n"); + Cvar_SetInteger(g_features, g_features->integer | GMF_ENHANCED_SAVEGAMES, FROM_CODE); + return; + } + Com_WPrintf("Game does not support enhanced savegames. Savegames will not work.\n"); } From 46a13e9cb4c03e5087e089968d178422b5bc9e9d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 30 Jun 2024 12:54:54 +0300 Subject: [PATCH 305/974] Advance sv.framenum in SV_CheckForSavegame(). --- src/server/init.c | 4 ++-- src/server/save.c | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/server/init.c b/src/server/init.c index 83ecc63af..eddd9b5e1 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -187,8 +187,8 @@ void SV_SpawnServer(const mapcmd_t *cmd) ge->SpawnEntities(sv.name, sv.cm.entitystring, cmd->spawnpoint); // run two frames to allow everything to settle - ge->RunFrame(); sv.framenum++; - ge->RunFrame(); sv.framenum++; + for (i = 0; i < 2; i++, sv.framenum++) + ge->RunFrame(); // make sure maxclients string is correct sprintf(sv.configstrings[svs.csr.maxclients], "%d", sv_maxclients->integer); diff --git a/src/server/save.c b/src/server/save.c index b47df8f05..fb6118904 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -538,6 +538,8 @@ void SV_AutoSaveEnd(void) void SV_CheckForSavegame(const mapcmd_t *cmd) { + int frames; + if (no_save_games()) return; if (sv_noreload->integer) @@ -553,16 +555,15 @@ void SV_CheckForSavegame(const mapcmd_t *cmd) if (cmd->loadgame == LOAD_NORMAL) { // called from SV_Loadgame_f - ge->RunFrame(); - ge->RunFrame(); + frames = 2; } else { - int i; - // coming back to a level after being in a different // level, so run it for ten seconds - for (i = 0; i < 100; i++) - ge->RunFrame(); + frames = 10 * SV_FRAMERATE; } + + for (int i = 0; i < frames; i++, sv.framenum++) + ge->RunFrame(); } void SV_CheckForEnhancedSavegames(void) From 0088d618f5b2ee9371d43b24d546636eb10947dc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 30 Jun 2024 20:11:53 +0300 Subject: [PATCH 306/974] Remove unused macros. --- src/server/server.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/server.h b/src/server/server.h index b2a2e6a0a..3931e56be 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -56,8 +56,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SV_Malloc(size) Z_TagMalloc(size, TAG_SERVER) #define SV_Mallocz(size) Z_TagMallocz(size, TAG_SERVER) #define SV_CopyString(s) Z_TagCopyString(s, TAG_SERVER) -#define SV_LoadFile(path, buf) FS_LoadFileEx(path, buf, 0, TAG_SERVER) -#define SV_FreeFile(buf) Z_Free(buf) #if USE_DEBUG #define SV_DPrintf(level,...) \ From 7b76c13c82132c307f396c65624ce77c3468ca08 Mon Sep 17 00:00:00 2001 From: mikota Date: Mon, 1 Jul 2024 07:06:58 +0200 Subject: [PATCH 307/974] dont let corpses take damage (mebbe?) potential useful hack for weird hit dead people bug --- src/action/p_client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/p_client.c b/src/action/p_client.c index deaf92dbe..6fbf44148 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1473,7 +1473,7 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage VectorClear(self->avelocity); - self->takedamage = DAMAGE_YES; + self->takedamage = DAMAGE_NO; self->movetype = MOVETYPE_TOSS; self->s.modelindex2 = 0; // remove linked weapon model From 2b0413a9114c350f0d9dbe9e5bf2d3f15d3684df Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 11:54:57 +0300 Subject: [PATCH 308/974] Remove unneeded check. --- src/client/main.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/client/main.c b/src/client/main.c index 741724c68..046ce8a72 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -146,14 +146,10 @@ static request_t *CL_AddRequest(const netadr_t *adr, requestType_t type) static request_t *CL_FindRequest(void) { request_t *r; - int i, count; - - count = MAX_REQUESTS; - if (count > nextRequest) - count = nextRequest; + int i; // find the most recent request sent to this address - for (i = 0; i < count; i++) { + for (i = 0; i < MAX_REQUESTS; i++) { r = &clientRequests[(nextRequest - i - 1) & REQUEST_MASK]; if (!r->type) { continue; From 487344d09f9972c9a52b7ce72fb2808a66182a3c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 14:13:20 +0300 Subject: [PATCH 309/974] Reduce size of sfx_t. --- src/client/sound/sound.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/sound/sound.h b/src/client/sound/sound.h index 5f19dc341..a3d91dde8 100644 --- a/src/client/sound/sound.h +++ b/src/client/sound/sound.h @@ -45,9 +45,9 @@ typedef struct { typedef struct { char name[MAX_QPATH]; - int registration_sequence; - sfxcache_t *cache; char *truename; + sfxcache_t *cache; + int registration_sequence; int error; } sfx_t; From 232a5b945dda2833ea8e4c35080f7866422d5cae Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 14:21:48 +0300 Subject: [PATCH 310/974] Reduce size of image_t. --- src/refresh/images.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/images.h b/src/refresh/images.h index 22cd4fba1..5a538f6ba 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -60,7 +60,7 @@ typedef struct image_s { char name[MAX_QPATH]; // game path uint8_t baselen; // without extension uint8_t type; - imageflags_t flags; + uint16_t flags; uint16_t width, height; // source image uint16_t upload_width, upload_height; // after power of two and picmip int registration_sequence; // 0 = free From 5d4fa95b86219115a8fdbd75e0c091c07a90f1ed Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 1 Jul 2024 18:47:23 -0400 Subject: [PATCH 311/974] Added mikota's patch to this branch --- src/action/p_antilag.c | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/action/p_antilag.c b/src/action/p_antilag.c index 9a3a8521c..21bd8f8dd 100644 --- a/src/action/p_antilag.c +++ b/src/action/p_antilag.c @@ -38,8 +38,23 @@ float antilag_findseek(edict_t *ent, float time_stamp) { if (state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK] && state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK] <= time_stamp) { - if ((offs - 1) < 0) // never return a timestamp from the future aka erroneous crap - return -1; + if (offs == 0) { + float corrected_leveltime = level.time - FRAMETIME; + //need to do this because level.time gets updated after antilag_update during sv frame + //Com_Printf("###SHOT REWIND###\n"); + //Com_Printf("hitbox curr_timestamp %f\n", state->curr_timestamp); + //Com_Printf("antilag curr_timestamp %f\n", time_stamp); + //Com_Printf("level time %f\n", corrected_leveltime); + float advanced_since_svframe = state->curr_timestamp - corrected_leveltime; + //Com_Printf("advanced_since_svframe %f ms\n", advanced_since_svframe*1000); + if (advanced_since_svframe <= 0) return 0; + + float timestamp_since_svframe = time_stamp - corrected_leveltime; + //Com_Printf("antilag_timestamp_since_svframe %f ms\n", timestamp_since_svframe*1000); + if (timestamp_since_svframe <= 0) return state->seek; + if (timestamp_since_svframe >= advanced_since_svframe) return state->seek; + return - ((float)(state->seek) + (timestamp_since_svframe / advanced_since_svframe)); + } float frac = 1; float stamp_last = state->hist_timestamp[(state->seek - offs) & ANTILAG_MASK]; @@ -92,7 +107,7 @@ void antilag_rewind_all(edict_t *ent) float rewind_seek = antilag_findseek(who, time_to_seek); //Com_Printf("rewind seek %f\n", rewind_seek); - if (rewind_seek < 0) + if (rewind_seek == 0) continue; state->rewound = true; @@ -101,8 +116,26 @@ void antilag_rewind_all(edict_t *ent) VectorCopy(who->maxs, state->hold_maxs); //Com_Printf("seek diff %f\n", (float)state->seek - rewind_seek); - LerpVector(state->hist_origin[((int)rewind_seek) & ANTILAG_MASK], state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], rewind_seek - ((float)(int)rewind_seek), who->s.origin); + //LerpVector(state->hist_origin[((int)rewind_seek) & ANTILAG_MASK], state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], rewind_seek - ((float)(int)rewind_seek), who->s.origin); + int lerp_latest = 0; + if (rewind_seek < 0) { + lerp_latest = 1; + rewind_seek = -rewind_seek; + } + float lerpfrac = rewind_seek - (int)rewind_seek; + //Com_Printf("lerpfrac %f\n", lerpfrac); + vec3_t prev, next; + VectorCopy(state->hist_origin[(int)rewind_seek & ANTILAG_MASK], prev); + if (lerp_latest) { + VectorCopy(who->s.origin, next); + //Com_Printf("Using current origin as next\n"); + } else { + VectorCopy(state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], next); + //Com_Printf("Using hist_origin as next\n"); + } + LerpVector(prev, next, lerpfrac, who->s.origin); + VectorCopy(state->hist_mins[(int)rewind_seek & ANTILAG_MASK], who->mins); VectorCopy(state->hist_maxs[(int)rewind_seek & ANTILAG_MASK], who->maxs); From 36551de1345f19a659788761a9a67ac1c844d4bd Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 1 Jul 2024 18:50:49 -0400 Subject: [PATCH 312/974] Added workflow_dispatch --- .github/workflows/build.yml | 1 + src/action/p_antilag.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 374987c53..95ac4c791 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: [master, aqtion, aqtion-alpha, ci] pull_request: branches: [master, aqtion, aqtion-alpha] + workflow_dispatch: env: MESON_ARGS: >- diff --git a/src/action/p_antilag.c b/src/action/p_antilag.c index f8a2ac193..894734d6a 100644 --- a/src/action/p_antilag.c +++ b/src/action/p_antilag.c @@ -118,7 +118,7 @@ void antilag_rewind_all(edict_t *ent) //Com_Printf("seek diff %f\n", (float)state->seek - rewind_seek); //LerpVector(state->hist_origin[((int)rewind_seek) & ANTILAG_MASK], state->hist_origin[((int)(rewind_seek+1)) & ANTILAG_MASK], rewind_seek - ((float)(int)rewind_seek), who->s.origin); - int lerp_latest = 0; + int lerp_latest = 0; if (rewind_seek < 0) { lerp_latest = 1; rewind_seek = -rewind_seek; From d551ae4393be45f3ca4fa6bc749bec6f231694f0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 14:27:24 +0300 Subject: [PATCH 313/974] Remove unneeded local variables. --- src/refresh/images.c | 15 +++++++-------- src/refresh/models.c | 17 ++++++----------- src/unix/tty.c | 5 ++--- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index d5f9a5549..3267fb525 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1524,16 +1524,16 @@ static image_t *lookup_image(const char *name, static int try_image_format_(imageformat_t fmt, image_t *image, byte **pic) { byte *data; - int len, ret; + int ret; // load the file - len = FS_LoadFile(image->name, (void **)&data); + ret = FS_LoadFile(image->name, (void **)&data); if (!data) { - return len; + return ret; } // decompress the image - ret = img_loaders[fmt].load(data, len, image, pic); + ret = img_loaders[fmt].load(data, ret, image, pic); FS_FreeFile(data); @@ -2043,16 +2043,15 @@ R_GetPalette void IMG_GetPalette(void) { byte pal[768], *src, *data; - int i, ret, len; + int i, ret; // get the palette - len = FS_LoadFile(R_COLORMAP_PCX, (void **)&data); + ret = FS_LoadFile(R_COLORMAP_PCX, (void **)&data); if (!data) { - ret = len; goto fail; } - ret = load_pcx(data, len, NULL, pal, NULL, NULL); + ret = load_pcx(data, ret, NULL, pal, NULL, NULL); FS_FreeFile(data); diff --git a/src/refresh/models.c b/src/refresh/models.c index 486d555c3..10b04aaca 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1402,10 +1402,8 @@ qhandle_t R_RegisterModel(const char *name) char normalized[MAX_QPATH]; qhandle_t index; size_t namelen; - int filelen; model_t *model; byte *rawdata; - uint32_t ident; int (*load)(model_t *, const void *, size_t); int ret; @@ -1443,25 +1441,22 @@ qhandle_t R_RegisterModel(const char *name) goto done; } - filelen = FS_LoadFile(normalized, (void **)&rawdata); + ret = FS_LoadFile(normalized, (void **)&rawdata); if (!rawdata) { // don't spam about missing models - if (filelen == Q_ERR(ENOENT)) { + if (ret == Q_ERR(ENOENT)) { return 0; } - - ret = filelen; goto fail1; } - if (filelen < 4) { + if (ret < 4) { ret = Q_ERR_FILE_TOO_SMALL; goto fail2; } // check ident - ident = LittleLong(*(uint32_t *)rawdata); - switch (ident) { + switch (LittleLong(*(uint32_t *)rawdata)) { case MD2_IDENT: load = MOD_LoadMD2; break; @@ -1487,11 +1482,11 @@ qhandle_t R_RegisterModel(const char *name) memcpy(model->name, normalized, namelen + 1); model->registration_sequence = registration_sequence; - ret = load(model, rawdata, filelen); + ret = load(model, rawdata, ret); FS_FreeFile(rawdata); - if (ret) { + if (ret < 0) { memset(model, 0, sizeof(*model)); goto fail1; } diff --git a/src/unix/tty.c b/src/unix/tty.c index 5ee689127..2abf763e8 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -543,13 +543,12 @@ void tty_init_input(void) #endif // determine terminal width - int width = tty_get_width(); - tty_prompt.widthInChars = width; + tty_prompt.widthInChars = tty_get_width(); tty_prompt.printf = Sys_Printf; tty_enabled = true; // figure out input line width - IF_Init(&tty_prompt.inputLine, width - 1, MAX_FIELD_TEXT - 1); + IF_Init(&tty_prompt.inputLine, tty_prompt.widthInChars - 1, MAX_FIELD_TEXT - 1); // display command prompt tty_stdout_write("]", 1); From 7af0a1a90ee83102ef9daf38bdeaf6f8ed9c0ed8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 14:39:22 +0300 Subject: [PATCH 314/974] Make registration sequences unsigned. --- src/client/sound/main.c | 2 +- src/client/sound/sound.h | 2 +- src/refresh/gl.h | 4 ++-- src/refresh/images.c | 17 ++++++++--------- src/refresh/images.h | 4 ++-- src/refresh/main.c | 6 +++--- src/refresh/models.c | 12 ++++++------ src/refresh/surf.c | 2 +- 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/client/sound/main.c b/src/client/sound/main.c index 1bce4d4f2..12d4ff3b3 100644 --- a/src/client/sound/main.c +++ b/src/client/sound/main.c @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., // Internal sound data & structures // ======================================================================= -int s_registration_sequence; +unsigned s_registration_sequence; channel_t s_channels[MAX_CHANNELS]; int s_numchannels; diff --git a/src/client/sound/sound.h b/src/client/sound/sound.h index a3d91dde8..3dd237d6d 100644 --- a/src/client/sound/sound.h +++ b/src/client/sound/sound.h @@ -47,7 +47,7 @@ typedef struct { char name[MAX_QPATH]; char *truename; sfxcache_t *cache; - int registration_sequence; + unsigned registration_sequence; int error; } sfx_t; diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 43a17641d..92e6177f8 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -167,7 +167,7 @@ extern glRefdef_t glr; extern entity_t gl_world; -extern int registration_sequence; +extern unsigned r_registration_sequence; typedef struct { int nodesVisible; @@ -369,7 +369,7 @@ typedef struct { } type; char name[MAX_QPATH]; - int registration_sequence; + unsigned registration_sequence; memhunk_t hunk; int nummeshes; diff --git a/src/refresh/images.c b/src/refresh/images.c index 3267fb525..c982cdd59 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1442,7 +1442,7 @@ static void IMG_List_f(void) texels = count = 0; for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { - if (!image->registration_sequence) + if (!image->name[0]) continue; if (mask && !(mask & (1 << image->type))) continue; @@ -1476,7 +1476,7 @@ static image_t *alloc_image(void) // find a free image_t slot for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { - if (!image->registration_sequence) + if (!image->name[0]) return image; if (!image->upload_width && !image->upload_height && !placeholder) placeholder = image; @@ -1816,7 +1816,7 @@ static image_t *find_or_load_image(const char *name, size_t len, // look for it if ((image = lookup_image(name, type, hash, len - 4)) != NULL) { - image->registration_sequence = registration_sequence; + image->registration_sequence = r_registration_sequence; if (image->upload_width && image->upload_height) { image->flags |= flags & IF_PERMANENT; return image; @@ -1836,7 +1836,7 @@ static image_t *find_or_load_image(const char *name, size_t len, image->baselen = len - 4; image->type = type; image->flags = flags; - image->registration_sequence = registration_sequence; + image->registration_sequence = r_registration_sequence; // find out original extension for (fmt = 0; fmt < IM_MAX; fmt++) { @@ -1984,11 +1984,10 @@ void IMG_FreeUnused(void) int i, count = 0; for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { - if (image->registration_sequence == registration_sequence) { - continue; // used this sequence - } - if (!image->registration_sequence) + if (!image->name[0]) continue; // free image_t slot + if (image->registration_sequence == r_registration_sequence) + continue; // used this sequence if (image->flags & (IF_PERMANENT | IF_SCRAP)) continue; // don't free pics @@ -2013,7 +2012,7 @@ void IMG_FreeAll(void) int i, count = 0; for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { - if (!image->registration_sequence) + if (!image->name[0]) continue; // free image_t slot // free it IMG_Unload(image); diff --git a/src/refresh/images.h b/src/refresh/images.h index 5a538f6ba..a1bdd938a 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -63,7 +63,7 @@ typedef struct image_s { uint16_t flags; uint16_t width, height; // source image uint16_t upload_width, upload_height; // after power of two and picmip - int registration_sequence; // 0 = free + unsigned registration_sequence; unsigned texnum, glow_texnum; // gl texture binding float sl, sh, tl, th; float aspect; @@ -74,7 +74,7 @@ typedef struct image_s { extern image_t r_images[MAX_RIMAGES]; extern int r_numImages; -extern int registration_sequence; +extern unsigned r_registration_sequence; #define R_NOTEXTURE &r_images[0] diff --git a/src/refresh/main.c b/src/refresh/main.c index 0551c36f4..a7e90f414 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -32,7 +32,7 @@ entity_t gl_world; refcfg_t r_config; -int registration_sequence; +unsigned r_registration_sequence; // regular variables cvar_t *gl_partscale; @@ -1025,7 +1025,7 @@ static void GL_InitTables(void) static void GL_PostInit(void) { - registration_sequence = 1; + r_registration_sequence = 1; GL_ClearState(); GL_InitImages(); @@ -1201,7 +1201,7 @@ void R_BeginRegistration(const char *name) char fullname[MAX_QPATH]; gl_static.registering = true; - registration_sequence++; + r_registration_sequence++; memset(&glr, 0, sizeof(glr)); glr.viewcluster1 = glr.viewcluster2 = -2; diff --git a/src/refresh/models.c b/src/refresh/models.c index 10b04aaca..4bae1a597 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -114,7 +114,7 @@ void MOD_FreeUnused(void) if (!model->type) { continue; } - if (model->registration_sequence == registration_sequence) { + if (model->registration_sequence == r_registration_sequence) { // make sure it is paged in Com_PageInMemory(model->hunk.base, model->hunk.cursize); #if USE_MD5 @@ -1372,20 +1372,20 @@ static void MOD_Reference(model_t *model) for (i = 0; i < model->nummeshes; i++) { maliasmesh_t *mesh = &model->meshes[i]; for (j = 0; j < mesh->numskins; j++) { - mesh->skins[j]->registration_sequence = registration_sequence; + mesh->skins[j]->registration_sequence = r_registration_sequence; } } #if USE_MD5 if (model->skeleton) { for (j = 0; j < model->skeleton->num_skins; j++) { - model->skeleton->skins[j]->registration_sequence = registration_sequence; + model->skeleton->skins[j]->registration_sequence = r_registration_sequence; } } #endif break; case MOD_SPRITE: for (i = 0; i < model->numframes; i++) { - model->spriteframes[i].image->registration_sequence = registration_sequence; + model->spriteframes[i].image->registration_sequence = r_registration_sequence; } break; case MOD_EMPTY: @@ -1394,7 +1394,7 @@ static void MOD_Reference(model_t *model) Q_assert(!"bad model type"); } - model->registration_sequence = registration_sequence; + model->registration_sequence = r_registration_sequence; } qhandle_t R_RegisterModel(const char *name) @@ -1480,7 +1480,7 @@ qhandle_t R_RegisterModel(const char *name) } memcpy(model->name, normalized, namelen + 1); - model->registration_sequence = registration_sequence; + model->registration_sequence = r_registration_sequence; ret = load(model, rawdata, ret); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index b753eca02..692360de1 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -998,7 +998,7 @@ void GL_LoadWorld(const char *name) // check if the required world model was already loaded if (gl_static.world.cache == bsp) { for (i = 0; i < bsp->numtexinfo; i++) { - bsp->texinfo[i].image->registration_sequence = registration_sequence; + bsp->texinfo[i].image->registration_sequence = r_registration_sequence; } for (i = 0; i < bsp->numnodes; i++) { bsp->nodes[i].visframe = 0; From 0ca7b41d581d6d611a979609f4770ddac2b013fb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 18:02:50 +0300 Subject: [PATCH 315/974] =?UTF-8?q?Clean=20up=20=E2=80=98r=5Ftexture=5Ffor?= =?UTF-8?q?mats=E2=80=99=20definition.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use space separated list of filename extensions instead of string composed of initial characters. Define default value in config.h. --- doc/client.asciidoc | 3 +- meson.build | 11 +++++++ src/refresh/images.c | 68 ++++++++++++++++++++++---------------------- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index bbedf0705..f093c206d 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -861,8 +861,7 @@ r_override_textures:: r_texture_formats:: Specifies the order in which truecolor texture replacements are searched. - Default value is "pjt", which means to try ‘.png’ extension first, then - ‘.jpg’, then ‘.tga’. + Default value is "png jpg tga". r_texture_overrides:: Specifies what types of textures are affected by ‘r_override_textures’. diff --git a/meson.build b/meson.build index 040523078..ea05b51b0 100644 --- a/meson.build +++ b/meson.build @@ -253,6 +253,8 @@ add_project_link_arguments(common_link_args, language: 'c') config = configuration_data() +texture_formats = [] + fallback_opt = ['default_library=static'] zlib = dependency('zlib', @@ -269,6 +271,9 @@ png = dependency('libpng', required: get_option('libpng'), default_options: fallback_opt, ) +if png.found() + texture_formats += 'png' +endif config.set10('USE_ZLIB', zlib.found()) config.set10('USE_PNG', png.found()) @@ -310,10 +315,15 @@ if jpeg.found() endif if has_rgba client_deps += jpeg + texture_formats += 'jpg' config.set10('USE_JPG', true) endif endif +if get_option('tga') + texture_formats += 'tga' +endif + openal = dependency('openal', version: '>= 1.18.0', required: get_option('openal'), @@ -452,6 +462,7 @@ if not win32 config.set_quoted('LIBDIR', libdir) config.set_quoted('HOMEDIR', homedir) endif +config.set_quoted('R_TEXTURE_FORMATS', ' '.join(texture_formats)) config.set_quoted('VID_GEOMETRY', get_option('vid-geometry')) config.set_quoted('VID_MODELIST', get_option('vid-modelist')) diff --git a/src/refresh/images.c b/src/refresh/images.c index c982cdd59..8293a4516 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1617,39 +1617,49 @@ static void get_image_dimensions(imageformat_t fmt, image_t *image) image->height = h; } +static void add_texture_format(imageformat_t fmt) +{ + // don't let format to be specified more than once + for (int i = 0; i < img_total; i++) + if (img_search[i] == fmt) + return; + + Q_assert(img_total < IM_MAX); + img_search[img_total++] = fmt; +} + static void r_texture_formats_changed(cvar_t *self) { - char *s; - int i, j; + const char *s; // reset the search order img_total = 0; // parse the string - for (s = self->string; *s; s++) { - switch (*s) { -#if USE_TGA - case 't': case 'T': i = IM_TGA; break; -#endif -#if USE_JPG - case 'j': case 'J': i = IM_JPG; break; -#endif -#if USE_PNG - case 'p': case 'P': i = IM_PNG; break; -#endif - default: continue; - } - - // don't let format to be specified more than once - for (j = 0; j < img_total; j++) - if (img_search[j] == i) + s = self->string; + while (s) { + char *tok = COM_Parse(&s); + int i; + + // handle "png jpg tga" format + for (i = IM_WAL + 1; i < IM_MAX; i++) { + if (!Q_stricmp(tok, img_loaders[i].ext)) { + add_texture_format(i); break; - if (j != img_total) + } + } + if (i != IM_MAX) continue; - img_search[img_total++] = i; - if (img_total == IM_MAX) { - break; + // handle legacy "pjt" format + while (*tok) { + for (i = IM_WAL + 1; i < IM_MAX; i++) { + if (Q_tolower(*tok) == img_loaders[i].ext[0]) { + add_texture_format(i); + break; + } + } + tok++; } } } @@ -2094,17 +2104,7 @@ void IMG_Init(void) #if USE_PNG || USE_JPG || USE_TGA r_override_textures = Cvar_Get("r_override_textures", "1", CVAR_FILES); - r_texture_formats = Cvar_Get("r_texture_formats", -#if USE_PNG - "p" -#endif -#if USE_JPG - "j" -#endif -#if USE_TGA - "t" -#endif - , 0); + r_texture_formats = Cvar_Get("r_texture_formats", R_TEXTURE_FORMATS, 0); r_texture_formats->changed = r_texture_formats_changed; r_texture_formats_changed(r_texture_formats); r_texture_overrides = Cvar_Get("r_texture_overrides", "-1", CVAR_FILES); From 5231bdcdd252956efac8ff097d3a139543a080f5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 19:24:42 +0300 Subject: [PATCH 316/974] Clean up MD5 parsing code a bit. --- src/refresh/models.c | 87 +++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index 4bae1a597..6aaf0086e 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -772,9 +772,10 @@ static bool MD5_ParseVector(const char **buffer, vec3_t output) if (!(x)) { Com_SetLastError(e); ret = Q_ERR_INVALID_FORMAT; goto fail; } #define MD5_EXPECT(x) MD5_CHECK(MD5_ParseExpect(&s, x)) -#define MD5_UINT(x) MD5_CHECK(MD5_ParseUint(&s, x)) -#define MD5_FLOAT(x) MD5_CHECK(MD5_ParseFloat(&s, x)) +#define MD5_UINT(x) MD5_CHECK(MD5_ParseUint(&s, &x)) +#define MD5_FLOAT(x) MD5_CHECK(MD5_ParseFloat(&s, &x)) #define MD5_VECTOR(x) MD5_CHECK(MD5_ParseVector(&s, x)) +#define MD5_SKIP() COM_Parse(&s) static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleton) { @@ -882,18 +883,18 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) OOM_CHECK(model->skeleton = mdl = MD5_Malloc(sizeof(*mdl))); MD5_EXPECT("commandline"); - COM_Parse(&s); + MD5_SKIP(); MD5_EXPECT("numJoints"); - MD5_UINT(&num_joints); - MD5_ENSURE(num_joints, "no joints"); + MD5_UINT(num_joints); + MD5_ENSURE(num_joints > 0, "no joints"); MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "too many joints"); OOM_CHECK(mdl->base_skeleton = MD5_Malloc(num_joints * sizeof(mdl->base_skeleton[0]))); mdl->num_joints = num_joints; MD5_EXPECT("numMeshes"); - MD5_UINT(&num_meshes); - MD5_ENSURE(num_meshes, "no meshes"); + MD5_UINT(num_meshes); + MD5_ENSURE(num_meshes > 0, "no meshes"); MD5_ENSURE(num_meshes <= MD5_MAX_MESHES, "too many meshes"); OOM_CHECK(mdl->meshes = MD5_Malloc(num_meshes * sizeof(mdl->meshes[0]))); mdl->num_meshes = num_meshes; @@ -904,10 +905,11 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) for (i = 0; i < num_joints; i++) { md5_joint_t *joint = &mdl->base_skeleton[i]; - COM_Parse(&s); // ignore name + // skip name + MD5_SKIP(); uint32_t parent; - MD5_UINT(&parent); + MD5_UINT(parent); MD5_ENSURE(parent == -1 || (parent < num_joints && parent != i), "bad parent joint"); joint->parent = parent; @@ -928,10 +930,10 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("{"); MD5_EXPECT("shader"); - COM_Parse(&s); + MD5_SKIP(); MD5_EXPECT("numverts"); - MD5_UINT(&num_verts); + MD5_UINT(num_verts); MD5_ENSURE(num_verts <= TESS_MAX_VERTICES, "too many verts"); OOM_CHECK(mesh->vertices = MD5_Malloc(num_verts * sizeof(mesh->vertices[0]))); OOM_CHECK(mesh->tcoords = MD5_Malloc(num_verts * sizeof(mesh->tcoords[0]))); @@ -941,22 +943,22 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("vert"); uint32_t vert_index; - MD5_UINT(&vert_index); + MD5_UINT(vert_index); MD5_ENSURE(vert_index < num_verts, "bad vert index"); maliastc_t *tc = &mesh->tcoords[vert_index]; MD5_EXPECT("("); - MD5_FLOAT(&tc->st[0]); - MD5_FLOAT(&tc->st[1]); + MD5_FLOAT(tc->st[0]); + MD5_FLOAT(tc->st[1]); MD5_EXPECT(")"); md5_vertex_t *vert = &mesh->vertices[vert_index]; - MD5_UINT(&vert->start); - MD5_UINT(&vert->count); + MD5_UINT(vert->start); + MD5_UINT(vert->count); } MD5_EXPECT("numtris"); - MD5_UINT(&num_tris); + MD5_UINT(num_tris); MD5_ENSURE(num_tris <= TESS_MAX_INDICES / 3, "too many tris"); OOM_CHECK(mesh->indices = MD5_Malloc(num_tris * 3 * sizeof(mesh->indices[0]))); mesh->num_indices = num_tris * 3; @@ -965,19 +967,19 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("tri"); uint32_t tri_index; - MD5_UINT(&tri_index); + MD5_UINT(tri_index); MD5_ENSURE(tri_index < num_tris, "bad tri index"); for (k = 0; k < 3; k++) { uint32_t vert_index; - MD5_UINT(&vert_index); + MD5_UINT(vert_index); MD5_ENSURE(vert_index < mesh->num_verts, "bad tri vert"); mesh->indices[tri_index * 3 + k] = vert_index; } } MD5_EXPECT("numweights"); - MD5_UINT(&num_weights); + MD5_UINT(num_weights); MD5_ENSURE(num_weights <= MD5_MAX_WEIGHTS, "too many weights"); OOM_CHECK(mesh->weights = MD5_Malloc(num_weights * sizeof(mesh->weights[0]))); mesh->num_weights = num_weights; @@ -986,17 +988,17 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("weight"); uint32_t weight_index; - MD5_UINT(&weight_index); + MD5_UINT(weight_index); MD5_ENSURE(weight_index < num_weights, "bad weight index"); md5_weight_t *weight = &mesh->weights[weight_index]; uint32_t joint; - MD5_UINT(&joint); + MD5_UINT(joint); MD5_ENSURE(joint < mdl->num_joints, "bad weight joint"); weight->joint = joint; - MD5_FLOAT(&weight->bias); + MD5_FLOAT(weight->bias); MD5_VECTOR(weight->pos); } @@ -1055,11 +1057,9 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, VectorCopy(baseJoint->pos, animated_position); VectorCopy(baseJoint->orient, animated_quat); // W will be re-calculated below - for (int c = 0, j = 0; c < MD5_NUM_ANIMATED_COMPONENT_BITS; c++) { - if (joint_infos[i].flags & BIT(c)) { + for (int c = 0, j = 0; c < MD5_NUM_ANIMATED_COMPONENT_BITS; c++) + if (joint_infos[i].flags & BIT(c)) components[c] = anim_frame_data[joint_infos[i].start_index + j++]; - } - } Quat_ComputeW(animated_quat); @@ -1184,13 +1184,13 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) MD5_EXPECT("10"); MD5_EXPECT("commandline"); - COM_Parse(&s); + MD5_SKIP(); MD5_EXPECT("numFrames"); - MD5_UINT(&num_frames); + MD5_UINT(num_frames); // md5 replacements need at least 1 frame, because the // pose frame isn't used - MD5_ENSURE(num_frames, "no frames"); + MD5_ENSURE(num_frames > 0, "no frames"); MD5_ENSURE(num_frames <= MD5_MAX_FRAMES, "too many frames"); mdl->num_frames = num_frames; @@ -1201,38 +1201,36 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) } MD5_EXPECT("numJoints"); - MD5_UINT(&num_joints); + MD5_UINT(num_joints); MD5_ENSURE(num_joints == mdl->num_joints, "bad numJoints"); MD5_EXPECT("frameRate"); - COM_Parse(&s); + MD5_SKIP(); MD5_EXPECT("numAnimatedComponents"); - MD5_UINT(&num_animated_components); + MD5_UINT(num_animated_components); MD5_ENSURE(num_animated_components <= q_countof(anim_frame_data), "bad numAnimatedComponents"); MD5_EXPECT("hierarchy"); MD5_EXPECT("{"); - for (i = 0; i < mdl->num_joints; ++i) { + for (i = 0; i < mdl->num_joints; i++) { joint_info_t *joint_info = &joint_infos[i]; Q_strlcpy(joint_info->name, COM_Parse(&s), sizeof(joint_info->name)); - MD5_UINT(&joint_info->parent); - MD5_UINT(&joint_info->flags); - MD5_UINT(&joint_info->start_index); + MD5_UINT(joint_info->parent); + MD5_UINT(joint_info->flags); + MD5_UINT(joint_info->start_index); joint_info->scale_pos = false; // validate animated components int num_components = 0; - for (j = 0; j < MD5_NUM_ANIMATED_COMPONENT_BITS; j++) { - if (joint_info->flags & BIT(j)) { + for (j = 0; j < MD5_NUM_ANIMATED_COMPONENT_BITS; j++) + if (joint_info->flags & BIT(j)) num_components++; - } - } MD5_ENSURE((uint64_t)joint_info->start_index + num_components <= num_animated_components, "bad joint info"); @@ -1287,13 +1285,12 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) MD5_EXPECT("frame"); uint32_t frame_index; - MD5_UINT(&frame_index); + MD5_UINT(frame_index); MD5_ENSURE(frame_index < mdl->num_frames, "bad frame index"); MD5_EXPECT("{"); - for (j = 0; j < num_animated_components; j++) { - MD5_FLOAT(&anim_frame_data[j]); - } + for (j = 0; j < num_animated_components; j++) + MD5_FLOAT(anim_frame_data[j]); MD5_EXPECT("}"); /* Build frame skeleton from the collected data */ From a5a6a19e164c613744f805dd67752d13741d691e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 1 Jul 2024 19:49:44 +0300 Subject: [PATCH 317/974] Add and use Q_atof() macro. --- inc/shared/shared.h | 2 ++ src/client/locs.c | 6 +++--- src/client/precache.c | 2 +- src/client/screen.c | 8 ++++---- src/client/ui/script.c | 6 +++--- src/common/cmd.c | 12 ++++++------ src/common/cvar.c | 4 ++-- src/common/tests.c | 2 +- src/game/g_spawn.c | 4 ++-- src/refresh/models.c | 2 +- src/server/mvd/parse.c | 2 +- src/server/user.c | 6 +++--- 12 files changed, 29 insertions(+), 27 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 764307e00..167d2eaf6 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -553,6 +553,8 @@ size_t Q_strnlen(const char *s, size_t maxlen); int Q_atoi(const char *s); #endif +#define Q_atof(s) strtof(s, NULL) + char *COM_SkipPath(const char *pathname); size_t COM_StripExtension(char *out, const char *in, size_t size); size_t COM_DefaultExtension(char *path, const char *ext, size_t size); diff --git a/src/client/locs.c b/src/client/locs.c index 21193a21f..c5f1c9e97 100644 --- a/src/client/locs.c +++ b/src/client/locs.c @@ -93,9 +93,9 @@ void LOC_LoadLocations(void) Com_WPrintf("Line %d is incomplete in %s\n", line, path); } else { loc = LOC_Alloc(Cmd_RawArgsFrom(3)); - loc->origin[0] = atof(Cmd_Argv(0)) * 0.125f; - loc->origin[1] = atof(Cmd_Argv(1)) * 0.125f; - loc->origin[2] = atof(Cmd_Argv(2)) * 0.125f; + loc->origin[0] = Q_atof(Cmd_Argv(0)) * 0.125f; + loc->origin[1] = Q_atof(Cmd_Argv(1)) * 0.125f; + loc->origin[2] = Q_atof(Cmd_Argv(2)) * 0.125f; List_Append(&cl_locations, &loc->entry); count++; } diff --git a/src/client/precache.c b/src/client/precache.c index 4ba971531..4bd64794e 100644 --- a/src/client/precache.c +++ b/src/client/precache.c @@ -305,7 +305,7 @@ void CL_SetSky(void) if (cl.csr.extended) sscanf(cl.configstrings[CS_SKYROTATE], "%f %d", &rotate, &autorotate); else - rotate = atof(cl.configstrings[CS_SKYROTATE]); + rotate = Q_atof(cl.configstrings[CS_SKYROTATE]); if (sscanf(cl.configstrings[CS_SKYAXIS], "%f %f %f", &axis[0], &axis[1], &axis[2]) != 3) { diff --git a/src/client/screen.c b/src/client/screen.c index 28ce87e38..b75dbd55f 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1100,14 +1100,14 @@ static void SCR_Sky_f(void) } if (argc > 2) - rotate = atof(Cmd_Argv(2)); + rotate = Q_atof(Cmd_Argv(2)); else rotate = 0; if (argc == 6) { - axis[0] = atof(Cmd_Argv(3)); - axis[1] = atof(Cmd_Argv(4)); - axis[2] = atof(Cmd_Argv(5)); + axis[0] = Q_atof(Cmd_Argv(3)); + axis[1] = Q_atof(Cmd_Argv(4)); + axis[2] = Q_atof(Cmd_Argv(5)); } else VectorSet(axis, 0, 0, 1); diff --git a/src/client/ui/script.c b/src/client/ui/script.c index 2f491dead..9ea683ea8 100644 --- a/src/client/ui/script.c +++ b/src/client/ui/script.c @@ -220,10 +220,10 @@ static void Parse_Range(menuFrameWork_t *menu) s->generic.name = UI_CopyString(Cmd_Argv(cmd_optind)); s->generic.status = UI_CopyString(status); s->cvar = Cvar_WeakGet(Cmd_Argv(cmd_optind + 1)); - s->minvalue = atof(Cmd_Argv(cmd_optind + 2)); - s->maxvalue = atof(Cmd_Argv(cmd_optind + 3)); + s->minvalue = Q_atof(Cmd_Argv(cmd_optind + 2)); + s->maxvalue = Q_atof(Cmd_Argv(cmd_optind + 3)); if (Cmd_Argc() - cmd_optind > 4) { - s->step = atof(Cmd_Argv(cmd_optind + 4)); + s->step = Q_atof(Cmd_Argv(cmd_optind + 4)); } else { s->step = (s->maxvalue - s->minvalue) / SLIDER_RANGE; } diff --git a/src/common/cmd.c b/src/common/cmd.c index d5c5244fe..7a25c7692 100644 --- a/src/common/cmd.c +++ b/src/common/cmd.c @@ -615,28 +615,28 @@ static void Cmd_If_f(void) numeric = COM_IsFloat(a) && COM_IsFloat(b); if (!strcmp(op, "==")) { - matched = numeric ? atof(a) == atof(b) : !strcmp(a, b); + matched = numeric ? Q_atof(a) == Q_atof(b) : !strcmp(a, b); } else if (!strcmp(op, "!=") || !strcmp(op, "<>")) { - matched = numeric ? atof(a) != atof(b) : strcmp(a, b); + matched = numeric ? Q_atof(a) != Q_atof(b) : strcmp(a, b); } else if (!strcmp(op, "<")) { if (!numeric) { error: Com_Printf("Can't use '%s' with non-numeric expression(s)\n", op); return; } - matched = atof(a) < atof(b); + matched = Q_atof(a) < Q_atof(b); } else if (!strcmp(op, "<=")) { if (!numeric) goto error; - matched = atof(a) <= atof(b); + matched = Q_atof(a) <= Q_atof(b); } else if (!strcmp(op, ">")) { if (!numeric) goto error; - matched = atof(a) > atof(b); + matched = Q_atof(a) > Q_atof(b); } else if (!strcmp(op, ">=")) { if (!numeric) goto error; - matched = atof(a) >= atof(b); + matched = Q_atof(a) >= Q_atof(b); } else if (!Q_stricmp(op, "isin")) { matched = strstr(b, a) != NULL; } else if (!Q_stricmp(op, "!isin")) { diff --git a/src/common/cvar.c b/src/common/cvar.c index b5cf09f85..6fcd0bb11 100644 --- a/src/common/cvar.c +++ b/src/common/cvar.c @@ -162,7 +162,7 @@ static void parse_string_value(cvar_t *var) var->value = (float)var->integer; } else { var->integer = Q_atoi(s); - var->value = atof(s); + var->value = Q_atof(s); if (var->value != 0.0f && !isnormal(var->value)) var->value = 0.0f; } @@ -1022,7 +1022,7 @@ static void Cvar_Inc_f(void) value = 1; if (Cmd_Argc() > 2) { - value = atof(Cmd_Argv(2)); + value = Q_atof(Cmd_Argv(2)); } if (!strcmp(Cmd_Argv(0), "dec")) { value = -value; diff --git a/src/common/tests.c b/src/common/tests.c index 83c0c91b5..11d8e668e 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -69,7 +69,7 @@ static void Com_Freeze_f(void) return; } - seconds = atof(Cmd_Argv(1)); + seconds = Q_atof(Cmd_Argv(1)); if (seconds < 0) { return; } diff --git a/src/game/g_spawn.c b/src/game/g_spawn.c index a886f3719..d1afb4744 100644 --- a/src/game/g_spawn.c +++ b/src/game/g_spawn.c @@ -438,10 +438,10 @@ static bool ED_ParseField(const spawn_field_t *fields, const char *key, const ch *(int *)(b + f->ofs) = Q_atoi(value); break; case F_FLOAT: - *(float *)(b + f->ofs) = atof(value); + *(float *)(b + f->ofs) = Q_atof(value); break; case F_ANGLEHACK: - v = atof(value); + v = Q_atof(value); ((float *)(b + f->ofs))[0] = 0; ((float *)(b + f->ofs))[1] = v; ((float *)(b + f->ofs))[2] = 0; diff --git a/src/refresh/models.c b/src/refresh/models.c index 6aaf0086e..c0bc531a5 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1147,7 +1147,7 @@ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t continue; } - model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = strtof(tok2, NULL); + model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = Q_atof(tok2); } } diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index d0ece0ff1..b3153c171 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -100,7 +100,7 @@ void MVD_ParseEntityString(mvd_t *mvd, const char *data) } else if (!strcmp(key, "angles")) { sscanf(value, "%f %f", &angles[0], &angles[1]); } else if (!strcmp(key, "angle")) { - angles[1] = atof(value); + angles[1] = Q_atof(value); } } diff --git a/src/server/user.c b/src/server/user.c index 6dcf88f03..ca58f6609 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -871,11 +871,11 @@ static bool match_cvar_val(const char *s, const char *v) case '*': return *v; case '=': - return atof(v) == atof(s); + return Q_atof(v) == Q_atof(s); case '<': - return atof(v) < atof(s); + return Q_atof(v) < Q_atof(s); case '>': - return atof(v) > atof(s); + return Q_atof(v) > Q_atof(s); case '~': return Q_stristr(v, s); case '#': From 3d2d0615704622f56d29a7d08a755a8a2128ca1d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 10:26:19 +0300 Subject: [PATCH 318/974] Remove redundant checks. --- src/unix/tty.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/unix/tty.c b/src/unix/tty.c index 2abf763e8..482932d3f 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -606,10 +606,6 @@ void Sys_RunConsole(void) char text[MAX_STRING_CHARS]; int ret; - if (!sys_console || !sys_console->integer) { - return; - } - if (!tty_input || !(tty_input->revents & (POLLIN | POLLERR | POLLHUP))) { return; } @@ -675,10 +671,6 @@ void Sys_SetConsoleTitle(const char *title) char buf[MAX_STRING_CHARS]; size_t len; - if (!sys_console || !sys_console->integer) { - return; - } - if (!tty_enabled) { return; } @@ -706,10 +698,6 @@ void Sys_SetConsoleColor(color_index_t color) char buf[5]; size_t len; - if (!sys_console || !sys_console->integer) { - return; - } - if (!tty_enabled) { return; } From b06c869f18feda4c38517bc7dee6c2b0ca8a2441 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 10:35:33 +0300 Subject: [PATCH 319/974] Shorten some TTY function names. --- src/unix/tty.c | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/unix/tty.c b/src/unix/tty.c index 482932d3f..2915890cc 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -65,7 +65,7 @@ static void tty_fatal_error(const char *what) // handles partial writes correctly, but never spins too much // blocks for 100 ms before giving up and losing data -static void tty_stdout_write(const char *buf, size_t len) +static void tty_write(const char *buf, size_t len) { int ret = write(STDOUT_FILENO, buf, len); if (ret == len) @@ -113,7 +113,7 @@ static void tty_stdout_write(const char *buf, size_t len) } q_printf(1, 2) -static void tty_stdout_writef(const char *fmt, ...) +static void tty_printf(const char *fmt, ...) { char buf[MAX_STRING_CHARS]; va_list ap; @@ -123,7 +123,7 @@ static void tty_stdout_writef(const char *fmt, ...) len = Q_vscnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); - tty_stdout_write(buf, len); + tty_write(buf, len); } static int tty_get_width(void) @@ -142,7 +142,7 @@ static void tty_hide_input(void) { if (!tty_hidden) { // move to start of line, erase from cursor to end of line - tty_stdout_write(CONST_STR_LEN("\r\033[K")); + tty_write(CONST_STR_LEN("\r\033[K")); } tty_hidden++; } @@ -168,7 +168,7 @@ static void tty_show_input(void) // move to start of line, print prompt and text, // move to start of line, forward N chars - tty_stdout_writef("\r]%.*s\r\033[%zuC", (int)f->visibleChars, text, pos + 1); + tty_printf("\r]%.*s\r\033[%zuC", (int)f->visibleChars, text, pos + 1); } } @@ -189,13 +189,13 @@ static void tty_move_cursor(inputField_t *f, size_t pos) if (oldpos < f->visibleChars && pos < f->visibleChars) { if (oldpos == pos - 1) { // forward one char - tty_stdout_write("\033[C", 3); + tty_write("\033[C", 3); } else if (oldpos == pos + 1) { // backward one char - tty_stdout_write("\033[D", 3); + tty_write("\033[D", 3); } else { // move to start of line, forward N chars - tty_stdout_writef("\r\033[%zuC", pos + 1); + tty_printf("\r\033[%zuC", pos + 1); } } else { tty_hide_input(); @@ -265,7 +265,7 @@ static void tty_parse_input(const char *text) if (f->cursorPos > 0) { if (f->text[f->cursorPos] == 0 && f->cursorPos < f->visibleChars) { f->text[--f->cursorPos] = 0; - tty_stdout_write("\b \b", 3); + tty_write("\b \b", 3); } else { tty_hide_input(); memmove(f->text + f->cursorPos - 1, f->text + f->cursorPos, sizeof(f->text) - f->cursorPos); @@ -304,14 +304,14 @@ static void tty_parse_input(const char *text) if (f->text[f->cursorPos]) { f->text[f->cursorPos] = 0; // erase from cursor to end of line - tty_stdout_write("\033[K", 3); + tty_write("\033[K", 3); } break; case CTRL_L: tty_hide_input(); // move cursor to top left corner, erase screen - tty_stdout_write(CONST_STR_LEN("\033[H\033[2J")); + tty_write(CONST_STR_LEN("\033[H\033[2J")); tty_show_input(); break; @@ -340,11 +340,11 @@ static void tty_parse_input(const char *text) // when cursor is at the rightmost column, terminal may or may // not advance it. force absolute position to keep it in the // same place. - tty_stdout_writef("%c\r\033[%zuC", key, f->cursorPos + 1); + tty_printf("%c\r\033[%zuC", key, f->cursorPos + 1); f->text[f->cursorPos + 0] = key; f->text[f->cursorPos + 1] = 0; } else if (f->text[f->cursorPos] == 0 && f->cursorPos + 1 < f->visibleChars) { - tty_stdout_write(&(char){ key }, 1); + tty_write(&(char){ key }, 1); f->text[f->cursorPos + 0] = key; f->text[f->cursorPos + 1] = 0; f->cursorPos++; @@ -368,7 +368,7 @@ static void tty_parse_input(const char *text) Cbuf_AddText(&cmd_buffer, s); Cbuf_AddText(&cmd_buffer, "\n"); } else { - tty_stdout_write("]\n", 2); + tty_write("]\n", 2); } tty_show_input(); break; @@ -551,7 +551,7 @@ void tty_init_input(void) IF_Init(&tty_prompt.inputLine, tty_prompt.widthInChars - 1, MAX_FIELD_TEXT - 1); // display command prompt - tty_stdout_write("]", 1); + tty_write("]", 1); return; no_tty: @@ -648,7 +648,7 @@ void Sys_ConsoleOutput(const char *text, size_t len) } if (!tty_enabled) { - tty_stdout_write(text, len); + tty_write(text, len); } else { static bool hack = false; @@ -657,7 +657,7 @@ void Sys_ConsoleOutput(const char *text, size_t len) hack = true; } - tty_stdout_write(text, len); + tty_write(text, len); if (text[len - 1] == '\n') { tty_show_input(); @@ -690,7 +690,7 @@ void Sys_SetConsoleTitle(const char *title) buf[len++] = '\007'; - tty_stdout_write(buf, len); + tty_write(buf, len); } void Sys_SetConsoleColor(color_index_t color) @@ -727,7 +727,7 @@ void Sys_SetConsoleColor(color_index_t color) if (color != COLOR_NONE) { tty_hide_input(); } - tty_stdout_write(buf, len); + tty_write(buf, len); if (color == COLOR_NONE) { tty_show_input(); } From d9665a20ff175fe5c15556e6b2349a9137653b23 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 10:37:44 +0300 Subject: [PATCH 320/974] Don't compile winch_handler() without TIOCGWINSZ. --- src/unix/tty.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/unix/tty.c b/src/unix/tty.c index 2915890cc..d5bf70112 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -492,10 +492,12 @@ static void tty_parse_input(const char *text) } } -static void q_unused winch_handler(int signum) +#ifdef TIOCGWINSZ +static void winch_handler(int signum) { tty_prompt.inputLine.visibleChars = 0; // force refresh } +#endif void tty_init_input(void) { From cc478742ed5cd8110da915ddbec3c1d13a366ae0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 2 Jul 2024 16:50:39 -0400 Subject: [PATCH 321/974] Removed grenade messaging, it causes too many problems --- src/action/g_combat.c | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index b249c8cff..ceed11e23 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -957,10 +957,6 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, edict_t *ent = NULL; vec3_t v; vec3_t dir; - char ent_name_list[1024] = ""; // Buffer to hold the names - int ent_count = 0; // Counter for the number of entities - qboolean selfharm = false; - while ((ent = findradius (ent, inflictor->s.origin, radius)) != NULL) { @@ -968,29 +964,6 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, continue; if (!ent->takedamage) continue; - if (ent == attacker) - selfharm = true; - - // Messaging addition - if ((ent->client || ent->is_bot) && IS_ALIVE(ent) && ent != attacker){ - // Only add the name to the list if there are less than 4 names - if (ent_count < 4) - { - // Calculate the length of the new string - int new_length = strlen(ent_name_list) + strlen(" and ") + strlen(ent->client->pers.netname); - - // Check if the new string would fit in ent_name_list - if (new_length < sizeof(ent_name_list)) - { - if (ent_count > 0) - strncat(ent_name_list, " and ", sizeof(ent_name_list) - strlen(ent_name_list) - 1); - strncat(ent_name_list, ent->client->pers.netname, sizeof(ent_name_list) - strlen(ent_name_list) - 1); - } - } - ent_count++; - } - - // End messaging addition VectorAdd (ent->mins, ent->maxs, v); VectorMA (ent->s.origin, 0.5, v, v); @@ -1022,19 +995,4 @@ void T_RadiusDamage (edict_t * inflictor, edict_t * attacker, float damage, } } } - - // Grenade splash damage messaging - // Checks for attacker being NULL (this causes a cprintf segfault if NULL), and if the attacker is a client - // Also checks if the mod is a grenade splash. The game uses T_RadiusDamage for a variety of damaging effects - // so we only want to print the grenade splash messages if the mod is a grenade splash - if (attacker && attacker->client && mod == MOD_HG_SPLASH){ - if (ent_count > 3) - gi.cprintf(attacker, PRINT_HIGH, "You nailed several players with that grenade, nicely done!\n"); - else if (selfharm && ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, along with %s\n", ent_name_list); - else if (selfharm) - gi.cprintf(attacker, PRINT_HIGH, "You were blasted by your own grenade, throw farther next time?\n"); - else if (ent_count > 0) - gi.cprintf(attacker, PRINT_HIGH, "%s was blasted by your grenade.\n", ent_name_list); - } } From bd60ade33ce5d513c9385a8013dd044e5648f7c2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 15:48:31 +0300 Subject: [PATCH 322/974] Allow write() failure in tty_kill_stdin(). --- src/unix/tty.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/unix/tty.c b/src/unix/tty.c index d5bf70112..809dc2566 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -567,22 +567,26 @@ static void tty_kill_stdin(void) NET_FreePollFd(tty_input); tty_input = NULL; } + if (tty_enabled) { - tty_hide_input(); + if (!tty_hidden) + if (write(STDOUT_FILENO, CONST_STR_LEN("\r\033[K")) < 0) + (void)"Shut up GCC"; tcsetattr(STDIN_FILENO, TCSADRAIN, &tty_orig); tty_enabled = false; } - Cvar_Set("sys_console", "1"); } void tty_shutdown_input(void) { tty_kill_stdin(); + if (sys_console && sys_console->integer) { Sys_SetNonBlock(STDIN_FILENO, false); Sys_SetNonBlock(STDOUT_FILENO, false); } - Cvar_Set("sys_console", "0"); + + sys_console = NULL; } void Sys_LoadHistory(void) From 2d43dc82f8658d6631f5ab994f19e1b97b2488ea Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 15:49:12 +0300 Subject: [PATCH 323/974] Fix async signal safety issues. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't call async signal unsafe functions from signal handlers. Remove ‘sys_parachute’ cvar because it's practically impossible to safely handle it. --- doc/server.asciidoc | 7 ------- src/unix/system.c | 36 +++++++----------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index b392890a4..66617ebeb 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -552,13 +552,6 @@ In Windows console additional key bindings are supported: * Ctrl+PGDN — scroll to console bottom **************************** -sys_parachute:: - On UNIX-like systems, specifies if a fatal termination handler is - installed. Default value is 1, which means Q2PRO will do some cleanup when - it crashes, like restoring terminal settings. However, this will prevent - core dump from being generated. To enable core dumps, set this variable to - 0. - sys_forcegamelib:: Specifies the full path to the game library server should attempt to load first, before normal search paths are tried. Useful mainly for debugging or diff --git a/src/unix/system.c b/src/unix/system.c index 4a1f19383..5da04571f 100644 --- a/src/unix/system.c +++ b/src/unix/system.c @@ -48,7 +48,7 @@ cvar_t *sys_homedir; extern cvar_t *console_prefix; -static bool terminate; +static int terminate; static bool flush_logs; /* @@ -149,23 +149,7 @@ static void usr1_handler(int signum) static void term_handler(int signum) { - Com_Printf("%s\n", strsignal(signum)); - - terminate = true; -} - -static void kill_handler(int signum) -{ - tty_shutdown_input(); - -#if USE_REF - if (vid.fatal_shutdown) - vid.fatal_shutdown(); -#endif - - fprintf(stderr, "%s\n", strsignal(signum)); - - exit(EXIT_FAILURE); + terminate = signum; } /* @@ -175,8 +159,7 @@ Sys_Init */ void Sys_Init(void) { - const char *homedir; - cvar_t *sys_parachute; + const char *homedir; signal(SIGTERM, term_handler); signal(SIGINT, term_handler); @@ -204,17 +187,9 @@ void Sys_Init(void) } else { homedir = HOMEDIR; } + sys_homedir = Cvar_Get("homedir", homedir, CVAR_NOSET); sys_libdir = Cvar_Get("libdir", LIBDIR, CVAR_NOSET); - sys_parachute = Cvar_Get("sys_parachute", "1", CVAR_NOSET); - - if (sys_parachute->integer) { - // perform some cleanup when crashing - signal(SIGSEGV, kill_handler); - signal(SIGILL, kill_handler); - signal(SIGFPE, kill_handler); - signal(SIGTRAP, kill_handler); - } tty_init_input(); } @@ -462,6 +437,7 @@ int main(int argc, char **argv) } Qcommon_Init(argc, argv); + while (!terminate) { if (flush_logs) { Com_FlushLogs(); @@ -470,6 +446,8 @@ int main(int argc, char **argv) Qcommon_Frame(); } + Com_Printf("%s\n", strsignal(terminate)); Com_Quit(NULL, ERR_DISCONNECT); + return EXIT_FAILURE; // never gets here } From 693f1a15f4ba727e291dc5cb2810504ae89f861a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 16:02:05 +0300 Subject: [PATCH 324/974] Assert POLLNVAL isn't set for stdin. --- src/unix/tty.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/unix/tty.c b/src/unix/tty.c index 809dc2566..788de20f2 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -612,7 +612,13 @@ void Sys_RunConsole(void) char text[MAX_STRING_CHARS]; int ret; - if (!tty_input || !(tty_input->revents & (POLLIN | POLLERR | POLLHUP))) { + if (!tty_input) { + return; + } + + Q_assert(!(tty_input->revents & POLLNVAL)); + + if (!(tty_input->revents & (POLLIN | POLLERR | POLLHUP))) { return; } From 3d602756e40c89d698fbf73ec4b3a5b34abcf2f2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 21:01:52 +0300 Subject: [PATCH 325/974] Fix possible Wayland video driver crash. --- src/unix/video/wayland.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/unix/video/wayland.c b/src/unix/video/wayland.c index 0b7dff895..a87e04312 100644 --- a/src/unix/video/wayland.c +++ b/src/unix/video/wayland.c @@ -666,10 +666,12 @@ static void init_clipboard(void); static void shutdown(void) { - struct output *output, *next; - wl_list_for_each_safe(output, next, &wl.outputs, link) { - wl_output_destroy(output->wl_output); - Z_Free(output); + if (wl.outputs.next) { + struct output *output, *next; + wl_list_for_each_safe(output, next, &wl.outputs, link) { + wl_output_destroy(output->wl_output); + Z_Free(output); + } } if (wl.egl_display) From 0b8a67ccbb631a20d501334bf7f0814ae94e6631 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 21:42:23 +0300 Subject: [PATCH 326/974] =?UTF-8?q?Move=20=E2=80=98sys=5Fhistory=E2=80=99?= =?UTF-8?q?=20cvar=20to=20common=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/common/common.h | 4 ++++ src/common/common.c | 8 ++++++++ src/unix/tty.c | 9 ++------- src/windows/system.c | 9 ++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index 0687495e2..cf559ff52 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -176,6 +176,10 @@ extern cvar_t *sys_forcegamelib; extern cvar_t *sys_allow_unsafe_savegames; #endif +#if USE_SYSCON +extern cvar_t *sys_history; +#endif + #if USE_CLIENT // host_speeds times extern unsigned time_before_game; diff --git a/src/common/common.c b/src/common/common.c index b80a8d9dd..2417d15f3 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -121,6 +121,10 @@ cvar_t *sys_forcegamelib; cvar_t *sys_allow_unsafe_savegames; #endif +#if USE_SYSCON +cvar_t *sys_history; +#endif + const char com_version_string[] = APPLICATION " " VERSION " " __DATE__ " " BUILDSTRING " " CPUSTRING; @@ -927,6 +931,10 @@ void Qcommon_Init(int argc, char **argv) sys_allow_unsafe_savegames = Cvar_Get("sys_allow_unsafe_savegames", "0", CVAR_NOSET); #endif +#if USE_SYSCON + sys_history = Cvar_Get("sys_history", "0", 0); +#endif + Cmd_AddCommand("z_stats", Z_Stats_f); //Cmd_AddCommand("setenv", Com_Setenv_f); diff --git a/src/unix/tty.c b/src/unix/tty.c index 788de20f2..d4e925671 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -44,7 +44,6 @@ enum { }; static cvar_t *sys_console; -static cvar_t *sys_history; static bool tty_enabled; static struct termios tty_orig; @@ -591,18 +590,14 @@ void tty_shutdown_input(void) void Sys_LoadHistory(void) { - if (!tty_enabled) { - return; - } - sys_history = Cvar_Get("sys_history", "0", 0); - if (sys_history->integer > 0) { + if (tty_enabled && sys_history && sys_history->integer > 0) { Prompt_LoadHistory(&tty_prompt, SYS_HISTORYFILE_NAME); } } void Sys_SaveHistory(void) { - if (sys_history && sys_history->integer > 0) { + if (tty_enabled && sys_history && sys_history->integer > 0) { Prompt_SaveHistory(&tty_prompt, SYS_HISTORYFILE_NAME, sys_history->integer); } } diff --git a/src/windows/system.c b/src/windows/system.c index fd2c388fe..2b9189088 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -63,7 +63,6 @@ static HANDLE houtput = INVALID_HANDLE_VALUE; static commandPrompt_t sys_con; static int sys_hidden; static bool gotConsole; -static cvar_t *sys_history; static void write_console_data(const char *data, size_t len) { @@ -492,18 +491,14 @@ void Sys_RunConsole(void) void Sys_LoadHistory(void) { - if (!gotConsole) { - return; - } - sys_history = Cvar_Get("sys_history", "0", 0); - if (sys_history->integer > 0) { + if (gotConsole && sys_history && sys_history->integer > 0) { Prompt_LoadHistory(&sys_con, SYS_HISTORYFILE_NAME); } } void Sys_SaveHistory(void) { - if (sys_history && sys_history->integer > 0) { + if (gotConsole && sys_history && sys_history->integer > 0) { Prompt_SaveHistory(&sys_con, SYS_HISTORYFILE_NAME, sys_history->integer); } } From 1c40b30c902323a3caf3ba48358afe1008edb42a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 2 Jul 2024 21:47:25 +0300 Subject: [PATCH 327/974] Enable console history by default. --- doc/client.asciidoc | 2 +- doc/server.asciidoc | 2 +- src/client/console.c | 2 +- src/common/common.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index f093c206d..7f87f809c 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -380,7 +380,7 @@ con_notifylines:: con_history:: Specifies how many lines to save into console history file before exiting Q2PRO, to be reloaded on next startup. Maximum number of history lines is 128. - Default value is 0. + Default value is 128. con_scroll:: Controls automatic scrolling of console text when some event occurs. This diff --git a/doc/server.asciidoc b/doc/server.asciidoc index 66617ebeb..947638d00 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -518,7 +518,7 @@ sys_console:: sys_history:: Specifies how many lines to save into system console history file before exiting Q2PRO, to be reloaded on next startup. Maximum number of history - lines is 128. Default value is 0. + lines is 128. Default value is 128. .System console key bindings **************************** diff --git a/src/client/console.c b/src/client/console.c index 80a0c7b2c..2cb570919 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -466,7 +466,7 @@ void Con_Init(void) con_background = Cvar_Get("con_background", "conback", 0); con_background->changed = con_media_changed; con_scroll = Cvar_Get("con_scroll", "0", 0); - con_history = Cvar_Get("con_history", "0", 0); + con_history = Cvar_Get("con_history", STRINGIFY(HISTORY_SIZE), 0); con_timestamps = Cvar_Get("con_timestamps", "0", 0); con_timestamps->changed = con_width_changed; con_timestampsformat = Cvar_Get("con_timestampsformat", "%H:%M:%S ", 0); diff --git a/src/common/common.c b/src/common/common.c index 2417d15f3..d3c69d412 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -932,7 +932,7 @@ void Qcommon_Init(int argc, char **argv) #endif #if USE_SYSCON - sys_history = Cvar_Get("sys_history", "0", 0); + sys_history = Cvar_Get("sys_history", STRINGIFY(HISTORY_SIZE), 0); #endif Cmd_AddCommand("z_stats", Z_Stats_f); From bd54e2b476896879325416532b366181f7f006af Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 3 Jul 2024 14:18:05 +0300 Subject: [PATCH 328/974] Fix indentation. --- inc/system/system.h | 6 +++--- src/common/common.c | 5 ++--- src/common/pmove.c | 6 +++--- src/common/utils.c | 4 ++-- src/refresh/main.c | 6 +++--- src/windows/system.c | 7 +++---- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/inc/system/system.h b/inc/system/system.h index bf079af38..f5bd7fd13 100644 --- a/inc/system/system.h +++ b/inc/system/system.h @@ -34,7 +34,7 @@ void Sys_FreeLibrary(void *handle); void *Sys_GetProcAddress(void *handle, const char *sym); unsigned Sys_Milliseconds(void); -void Sys_Sleep(int msec); +void Sys_Sleep(int msec); void Sys_Init(void); void Sys_AddDefaultConfig(void); @@ -70,11 +70,11 @@ void Sys_ListFiles_r(listfiles_t *list, const char *path, int depth); void Sys_DebugBreak(void); #if USE_AC_CLIENT -bool Sys_GetAntiCheatAPI(void); +bool Sys_GetAntiCheatAPI(void); #endif #ifndef _WIN32 -bool Sys_SetNonBlock(int fd, bool nb); +bool Sys_SetNonBlock(int fd, bool nb); #endif extern cvar_t *sys_basedir; diff --git a/src/common/common.c b/src/common/common.c index d3c69d412..df92af1f8 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -1044,7 +1044,7 @@ void Qcommon_Frame(void) static float frac; if (setjmp(com_abortframe)) { - return; // an ERR_DROP was thrown + return; // an ERR_DROP was thrown } Com_CompleteAsyncWork(); @@ -1083,8 +1083,7 @@ void Qcommon_Frame(void) if (msec > 250) { Com_DPrintf("Hitch warning: %u msec frame time\n", msec); - msec = 100; // time was unreasonable, - // host OS was hibernated or something + msec = 100; // time was unreasonable } if (fixedtime->integer) { diff --git a/src/common/pmove.c b/src/common/pmove.c index 17d9abf13..3a8076b10 100644 --- a/src/common/pmove.c +++ b/src/common/pmove.c @@ -105,7 +105,7 @@ static void PM_StepSlideMove_(void) vec3_t planes[MAX_CLIP_PLANES]; vec3_t primal_velocity; int i, j; - trace_t trace; + trace_t trace; vec3_t end; float time_left; @@ -751,8 +751,8 @@ PM_FlyMove */ static void PM_FlyMove(void) { - float speed, drop, friction, control, newspeed; - float currentspeed, addspeed, accelspeed; + float speed, drop, friction, control, newspeed; + float currentspeed, addspeed, accelspeed; int i; vec3_t wishvel; float fmove, smove; diff --git a/src/common/utils.c b/src/common/utils.c index 10b9ac33f..388a71a4c 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -373,7 +373,7 @@ unsigned Com_HashString(const char *s, unsigned size) hash = 127 * hash + c; } - hash = (hash >> 20) ^(hash >> 10) ^ hash; + hash = (hash >> 20) ^ (hash >> 10) ^ hash; return hash & (size - 1); } @@ -395,7 +395,7 @@ unsigned Com_HashStringLen(const char *s, size_t len, unsigned size) hash = 127 * hash + c; } - hash = (hash >> 20) ^(hash >> 10) ^ hash; + hash = (hash >> 20) ^ (hash >> 10) ^ hash; return hash & (size - 1); } diff --git a/src/refresh/main.c b/src/refresh/main.c index a7e90f414..dd4ed923c 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -971,9 +971,9 @@ static void APIENTRY myDebugProc(GLenum source, GLenum type, GLuint id, GLenum s int level = PRINT_DEVELOPER; switch (severity) { - case GL_DEBUG_SEVERITY_HIGH: level = PRINT_ERROR; break; - case GL_DEBUG_SEVERITY_MEDIUM: level = PRINT_WARNING; break; - case GL_DEBUG_SEVERITY_LOW: level = PRINT_ALL; break; + case GL_DEBUG_SEVERITY_HIGH: level = PRINT_ERROR; break; + case GL_DEBUG_SEVERITY_MEDIUM: level = PRINT_WARNING; break; + case GL_DEBUG_SEVERITY_LOW: level = PRINT_ALL; break; } Com_LPrintf(level, "%s\n", message); diff --git a/src/windows/system.c b/src/windows/system.c index 2b9189088..2fc0df7be 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -202,11 +202,10 @@ Sys_ConsoleInput void Sys_RunConsole(void) { INPUT_RECORD recs[MAX_CONSOLE_INPUT_EVENTS]; - int ch; - DWORD numread, numevents; - int i; + int i, ch; + DWORD numread, numevents; inputField_t *f = &sys_con.inputLine; - char *s; + char *s; if (hinput == INVALID_HANDLE_VALUE) { return; From 85f0efe618aab3e14227dff334e1a9d2df179117 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 3 Jul 2024 14:18:36 +0300 Subject: [PATCH 329/974] Move Sys_Printf() to common code. --- inc/common/common.h | 6 ++++++ inc/system/system.h | 2 -- src/common/common.c | 15 +++++++++++++++ src/unix/tty.c | 13 ------------- src/windows/system.c | 20 -------------------- 5 files changed, 21 insertions(+), 35 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index cf559ff52..f5d7fbc4b 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -111,6 +111,12 @@ void Com_FlushLogs(void); void Com_AddConfigFile(const char *name, unsigned flags); +#if USE_SYSCON +void Sys_Printf(const char *fmt, ...) q_printf(1, 2); +#else +#define Sys_Printf(...) (void)0 +#endif + #if USE_CLIENT #define COM_DEDICATED (dedicated->integer != 0) #else diff --git a/inc/system/system.h b/inc/system/system.h index f5bd7fd13..f14c8384b 100644 --- a/inc/system/system.h +++ b/inc/system/system.h @@ -46,7 +46,6 @@ void Sys_RunConsole(void); void Sys_ConsoleOutput(const char *text, size_t len); void Sys_SetConsoleTitle(const char *title); void Sys_SetConsoleColor(color_index_t color); -void Sys_Printf(const char *fmt, ...) q_printf(1, 2); void Sys_LoadHistory(void); void Sys_SaveHistory(void); #else @@ -54,7 +53,6 @@ void Sys_SaveHistory(void); #define Sys_ConsoleOutput(text, len) (void)0 #define Sys_SetConsoleTitle(title) (void)0 #define Sys_SetConsoleColor(color) (void)0 -#define Sys_Printf(...) (void)0 #define Sys_LoadHistory() (void)0 #define Sys_SaveHistory() (void)0 #endif diff --git a/src/common/common.c b/src/common/common.c index df92af1f8..6d96bbd44 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -353,6 +353,21 @@ static void console_write(print_type_t type, const char *text) Sys_ConsoleOutput(buf, len); } +#if USE_SYSCON +void Sys_Printf(const char *fmt, ...) +{ + va_list argptr; + char msg[MAXPRINTMSG]; + size_t len; + + va_start(argptr, fmt); + len = Q_vscnprintf(msg, sizeof(msg), fmt, argptr); + va_end(argptr); + + Sys_ConsoleOutput(msg, len); +} +#endif + #ifndef _WIN32 /* ============= diff --git a/src/unix/tty.c b/src/unix/tty.c index d4e925671..71238782e 100644 --- a/src/unix/tty.c +++ b/src/unix/tty.c @@ -739,16 +739,3 @@ void Sys_SetConsoleColor(color_index_t color) tty_show_input(); } } - -void Sys_Printf(const char *fmt, ...) -{ - va_list argptr; - char msg[MAXPRINTMSG]; - size_t len; - - va_start(argptr, fmt); - len = Q_vscnprintf(msg, sizeof(msg), fmt, argptr); - va_end(argptr); - - Sys_ConsoleOutput(msg, len); -} diff --git a/src/windows/system.c b/src/windows/system.c index 2fc0df7be..80370b752 100644 --- a/src/windows/system.c +++ b/src/windows/system.c @@ -781,26 +781,6 @@ MISC =============================================================================== */ -#if USE_SYSCON -/* -================ -Sys_Printf -================ -*/ -void Sys_Printf(const char *fmt, ...) -{ - va_list argptr; - char msg[MAXPRINTMSG]; - size_t len; - - va_start(argptr, fmt); - len = Q_vscnprintf(msg, sizeof(msg), fmt, argptr); - va_end(argptr); - - Sys_ConsoleOutput(msg, len); -} -#endif - /* ================ Sys_Error From c420674c5c8bbd828e0e9c39840bdf46066163d2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 6 Jul 2024 11:49:36 +0300 Subject: [PATCH 330/974] Replace fmod() with fmodf(). --- src/client/newfx.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/newfx.c b/src/client/newfx.c index 127879959..15993575d 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -199,7 +199,7 @@ void CL_Heatbeam(const vec3_t start, const vec3_t forward) len = VectorNormalize(vec); ltime = cl.time * 0.001f; - start_pt = fmod(ltime * 96.0f, step); + start_pt = fmodf(ltime * 96.0f, step); VectorMA(move, start_pt, vec, move); VectorScale(vec, step, vec); From e578d7af94c9f1d273bbee61f97d3fd75a33beeb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 6 Jul 2024 11:50:04 +0300 Subject: [PATCH 331/974] Clean up image loading code a bit. --- src/refresh/images.c | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 8293a4516..7ac61237f 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -63,24 +63,24 @@ IMAGE FLOOD FILLING */ typedef struct { - short x, y; + uint16_t x, y; } floodfill_t; // must be a power of 2 #define FLOODFILL_FIFO_SIZE 0x1000 #define FLOODFILL_FIFO_MASK (FLOODFILL_FIFO_SIZE - 1) -#define FLOODFILL_STEP(off, dx, dy) \ - do { \ - if (pos[off] == fillcolor) { \ - pos[off] = 255; \ - fifo[inpt].x = x + (dx); \ - fifo[inpt].y = y + (dy); \ - inpt = (inpt + 1) & FLOODFILL_FIFO_MASK; \ - } else if (pos[off] != 255) { \ - fdc = pos[off]; \ - } \ - } while(0) +#define FLOODFILL_STEP(off, dx, dy) \ + do { \ + if (pos[off] == fillcolor) { \ + pos[off] = 255; \ + fifo[inpt].x = x + (dx); \ + fifo[inpt].y = y + (dy); \ + inpt = (inpt + 1) & FLOODFILL_FIFO_MASK; \ + } else if (pos[off] != 255) { \ + fdc = pos[off]; \ + } \ + } while (0) /* ================= @@ -89,7 +89,7 @@ IMG_FloodFill Fill background pixels so mipmapping doesn't have haloes ================= */ -static q_noinline void IMG_FloodFill(byte *skin, int skinwidth, int skinheight) +static void IMG_FloodFill(byte *skin, int skinwidth, int skinheight) { byte fillcolor = *skin; // assume this is the pixel to fill floodfill_t fifo[FLOODFILL_FIFO_SIZE]; @@ -98,9 +98,8 @@ static q_noinline void IMG_FloodFill(byte *skin, int skinwidth, int skinheight) // can't fill to filled color or to transparent color // (used as visited marker) - if (fillcolor == filledcolor || fillcolor == 255) { + if (fillcolor == filledcolor || fillcolor == 255) return; - } fifo[inpt].x = 0, fifo[inpt].y = 0; inpt = (inpt + 1) & FLOODFILL_FIFO_MASK; @@ -1791,7 +1790,7 @@ static void check_for_glow_map(image_t *image) byte *dst = glow_pic; for (int i = 0; i < size; i++, dst += 4) { - float alpha = dst[3] / 255.f; + float alpha = dst[3] / 255.0f; dst[0] *= alpha; dst[1] *= alpha; dst[2] *= alpha; From 8287cbaf4ef702911cb139c1e2f76283cfb89bde Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 6 Jul 2024 11:50:57 +0300 Subject: [PATCH 332/974] Clean up enhanced savegames detection. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rather than manipulating ‘g_features’, use have_enhanced_savegames() function. Also move ‘gamedetecthack’ to server_static_t where it belongs. --- src/server/game.c | 8 ++++---- src/server/save.c | 34 ++++++++++++++++------------------ src/server/server.h | 8 ++++---- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/server/game.c b/src/server/game.c index 2889ac42a..19dec1100 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -202,10 +202,10 @@ static void PF_dprintf(const char *fmt, ...) #if USE_SAVEGAMES // detect YQ2 game lib by unique first two messages - if (!sv.gamedetecthack) - sv.gamedetecthack = 1 + !strcmp(fmt, "Game is starting up.\n"); - else if (sv.gamedetecthack == 2) - sv.gamedetecthack = 3 + !strcmp(fmt, "Game is %s built on %s.\n"); + if (!svs.gamedetecthack) + svs.gamedetecthack = 1 + !strcmp(fmt, "Game is starting up.\n"); + else if (svs.gamedetecthack == 2) + svs.gamedetecthack = 3 + !strcmp(fmt, "Game is %s built on %s.\n"); #endif va_start(argptr, fmt); diff --git a/src/server/save.c b/src/server/save.c index fb6118904..d87c6baba 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -39,6 +39,8 @@ typedef enum { static cvar_t *sv_noreload; +static bool have_enhanced_savegames(void); + static int write_server_file(savetype_t autosave) { char name[MAX_OSPATH]; @@ -377,7 +379,7 @@ static int read_server_file(void) SV_InitGame(MVD_SPAWN_DISABLED); // error out immediately if game doesn't support safe savegames - if (!(g_features->integer & GMF_ENHANCED_SAVEGAMES)) + if (!have_enhanced_savegames()) Com_Error(ERR_DROP, "Game does not support enhanced savegames"); // read game state @@ -458,7 +460,7 @@ static int read_level_file(void) static bool no_save_games(void) { - if (!(g_features->integer & GMF_ENHANCED_SAVEGAMES)) + if (!have_enhanced_savegames()) return true; if (Cvar_VariableInteger("deathmatch")) @@ -566,29 +568,25 @@ void SV_CheckForSavegame(const mapcmd_t *cmd) ge->RunFrame(); } +static bool have_enhanced_savegames(void) +{ + return (g_features->integer & GMF_ENHANCED_SAVEGAMES) + || (svs.gamedetecthack == 4) || sys_allow_unsafe_savegames->integer; +} + void SV_CheckForEnhancedSavegames(void) { if (Cvar_VariableInteger("deathmatch")) return; - if (g_features->integer & GMF_ENHANCED_SAVEGAMES) { + if (g_features->integer & GMF_ENHANCED_SAVEGAMES) Com_Printf("Game supports Q2PRO enhanced savegames.\n"); - return; - } - - if (sv.gamedetecthack == 4) { + else if (svs.gamedetecthack == 4) Com_Printf("Game supports YQ2 enhanced savegames.\n"); - Cvar_SetInteger(g_features, g_features->integer | GMF_ENHANCED_SAVEGAMES, FROM_CODE); - return; - } - - if (sys_allow_unsafe_savegames->integer) { + else if (sys_allow_unsafe_savegames->integer) Com_WPrintf("Use of unsafe savegames forced from command line.\n"); - Cvar_SetInteger(g_features, g_features->integer | GMF_ENHANCED_SAVEGAMES, FROM_CODE); - return; - } - - Com_WPrintf("Game does not support enhanced savegames. Savegames will not work.\n"); + else + Com_WPrintf("Game does not support enhanced savegames. Savegames will not work.\n"); } static void SV_Savegame_c(genctx_t *ctx, int argnum) @@ -650,7 +648,7 @@ static void SV_Savegame_f(void) } // don't bother saving if we can't read them back! - if (!(g_features->integer & GMF_ENHANCED_SAVEGAMES)) { + if (!have_enhanced_savegames()) { Com_Printf("Game does not support enhanced savegames.\n"); return; } diff --git a/src/server/server.h b/src/server/server.h index 3931e56be..23f59b7a9 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -148,10 +148,6 @@ typedef struct { server_state_t state; // precache commands are only valid during load int spawncount; // random number generated each server spawn -#if USE_SAVEGAMES - int gamedetecthack; -#endif - #if USE_FPS int framerate; frametime_t frametime; @@ -477,6 +473,10 @@ typedef struct { unsigned z_buffer_size; #endif +#if USE_SAVEGAMES + int gamedetecthack; +#endif + cs_remap_t csr; unsigned last_heartbeat; From 5c60549330d4e959b0196a88123fe8117b059bda Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 6 Jul 2024 13:18:06 +0300 Subject: [PATCH 333/974] Fix possible integer overflow. --- src/client/entities.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/entities.c b/src/client/entities.c index 566d55ee2..ebcb2b1dc 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -502,7 +502,7 @@ static void CL_AddPacketEntities(void) autorotate = anglemod(cl.time * 0.1f); // brush models can auto animate their frames - autoanim = 2 * cl.time / 1000; + autoanim = cl.time / 500; autobob = 5 * sinf(cl.time / 400.0f); From 5fbc88a7043228757fa89fbf35393c868044b666 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 9 Jul 2024 17:38:57 +0300 Subject: [PATCH 334/974] Always update console overlay time in Con_Print(). --- src/client/console.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/console.c b/src/client/console.c index 2cb570919..ba4b46d3d 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -627,6 +627,10 @@ void Con_Print(const char *txt) txt++; } + + // update time for transparent overlay + if (!con.skipNotify) + con.times[con.current & CON_TIMES_MASK] = cls.realtime; } /* From 961bdee625a54c6459ef0fb381a46f6d5020c165 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 10 Jul 2024 20:52:25 -0400 Subject: [PATCH 335/974] Cleaned up all of the multithreading code --- src/action/acesrc/acebot.h | 24 - src/action/botlib/botlib_cmd.c | 23 - src/action/botlib/botlib_nav.c | 756 +------------------------------ src/action/botlib/botlib_nodes.c | 19 +- src/action/g_spawn.c | 3 - 5 files changed, 31 insertions(+), 794 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 368a449eb..1adec4e67 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -826,32 +826,8 @@ void DC_LoadRandomBotName(char* userinfo); // Quake 3 Multithreading Code //=============================== // qthreads.h -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/qthreads.h -void BOTLIB_THREAD_LOADAAS(qboolean force); // Init and run this function threaded -void BOTLIB_THREADING_LOADAAS(void* param); // Running in a thread -typedef struct loadaas_s // Struct used to send parameters to the threaded function -{ - qboolean force; -} loadaas_t; -void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to); -void BOTLIB_THREADING_DijkstraPath(void* param); -typedef struct dijkstra_path_s // Struct used to send parameters to the threaded function -{ - edict_t *ent; - int from; - int to; -} dijkstra_path_t; // extern int numthreads; -void ThreadSetDefault(void); -int GetThreadWork(void); -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)); -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); -//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); -//void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); -void ThreadLock(void); -void ThreadUnlock(void); -//rekkie -- Quake3 -- e #endif diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 9aecbfcab..d27dfd47d 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -109,20 +109,6 @@ qboolean BOTLIB_SV_Cmds(void) ACESP_RemoveBot("all"); return true; } - //rekkie -- BSP -- s - // else if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - // { - // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - // return true; - // } - // // Process BSP and generate AAS reachability with nodes - // else if (Q_stricmp(cmd, "aas") == 0) - // { - // ACEND_BSP(NULL); - // return true; - // } - //rekkie -- BSP -- e //rekkie -- python chatbot -- s #if 0 @@ -259,15 +245,6 @@ qboolean BOTLIB_SV_Cmds(void) qboolean BOTLIB_Commands(edict_t* ent) { char* cmd = gi.argv(0); - - //rekkie -- BSP -- s - // if (Q_stricmp(cmd, "loadaas") == 0) // Load AAS - // { - // BOTLIB_THREAD_LOADAAS(true); // Threaded version -- this will also generate AAS if it doesn't exist - // //ACEND_LoadAAS(true); // This will also generate AAS if it doesn't exist - // return true; - // } - // else if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS { ACEND_SaveAAS(true); diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 006d78610..7e7180a67 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -131,7 +131,6 @@ qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomizat } //else { - //BOTLIB_THREADED_DijkstraPath(ent, from, to); if (BOTLIB_DijkstraPath(ent, from, to, path_randomization)) //if (BOTLIB_DijkstraHeightPath(ent, from, to, path_randomization)) //if (BOTLIB_HighestPath(ent, from, to, path_randomization)) @@ -823,7 +822,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz else // Fallback to old pathing { self->bot.goal_node = INVALID; - //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); + gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) { @@ -1936,10 +1935,11 @@ qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_r // BOTLIB_DijkstraPath() is run again with path_randomization turned off qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization) { - //Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); + Com_Printf("%s from[%d] to[%d] for %s, random: %d\n", __func__, from, to, ent->client->pers.netname, path_randomization); // Sanity check if (from == INVALID || to == INVALID) + gi.dprintf("both from and to nodes were invalid\n"); return false; AntInitSearch(ent); // Clear out the path storage @@ -1957,13 +1957,12 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando //nodeused[atNode] = true; // Add the starting node to the visited CLOSED list SLLpush_back(&openList, from); // Store it - while (!SLLempty(&openList) && newNode != to) // While there are nodes on the OPEN list { atNode = SLLfront(&openList); // Get the next node if (atNode == to) // If the next node is the goal node { - //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); break; // We found a path } @@ -2103,12 +2102,12 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando nodeweight[lowest_node] = weight; // Update the weight nodefrom[lowest_node] = atNode; // Update the parent node (open list) SLLpush_back(&openList, lowest_node); // Store it - //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); } if (lowest_node == to) // If node being linked is the goal node { - //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); break; // We found a path } } @@ -2154,7 +2153,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando if (newNode == to) // If node being linked is the goal node { - //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); break; // We found a path } @@ -2215,7 +2214,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); } - //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); //int prev_newNode = newNode; // We earlier set our start node to INVALID to set up the termination @@ -2238,10 +2237,10 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path - //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); } - //Com_Printf("%s EXIT PATH\n\n", __func__); + Com_Printf("%s EXIT PATH\n\n", __func__); // Each time a path is found, make a copy //ent->bot.node_list_count = 0; @@ -2258,21 +2257,23 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando { if (1) { - Com_Printf("%s s[%d] g[%d] node_list[", __func__, from, to); + gi.dprintf("%s s[%d] g[%d] node_list[", __func__, from, to); for (int i = 0; i < ent->bot.node_list_count; i++) { - //Com_Printf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); - Com_Printf(" %d ", ent->bot.node_list[i]); + //gi.dprintf(" %d[%.1f] ", ent->bot.node_list[i], nodes[ent->bot.node_list[i]].weight); + gi.dprintf(" %d ", ent->bot.node_list[i]); } - Com_Printf(" ]\n"); + gi.dprintf(" ]\n"); } else - Com_Printf("%s from[%d] to[%d]\n", __func__, from, to); - //Com_Printf("%s found a path\n", __func__); + gi.dprintf("%s from[%d] to[%d]\n", __func__, from, to); + //gi.dprintf("%s found a path\n", __func__); } - if (SLLempty(&ent->pathList)) + if (SLLempty(&ent->pathList)){ + gi.dprintf("SLLempty: %s failed to find a path from %d to %d\n", __func__, from, to); return false; // Failure + } return true; // Success } @@ -2281,11 +2282,12 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando // If path_randomization was true, then try again without it if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it { + gi.dprintf("Trying again without path randomization\n"); BOTLIB_DijkstraPath(ent, from, to, false); } // Else: just fail it completely { - //Com_Printf("%s failed to find a path from %d to %d\n", __func__, from, to); + gi.dprintf("Complete failure: %s failed to find a path from %d to %d\n", __func__, from, to); return false; // Failure } } @@ -2922,719 +2924,3 @@ void SLLdelete(botlib_sll_t* list) gi.TagFree(temp); } } - -//=============================== -// Quake 3 Multithreading Code -//=============================== - -/* - ================ - I_FloatTime - ================ - */ -double I_FloatTime(void) { - time_t t; - - time(&t); - - return t; -#if 0 - // more precise, less portable - struct timeval tp; - struct timezone tzp; - static int secbase; - - gettimeofday(&tp, &tzp); - - if (!secbase) { - secbase = tp.tv_sec; - return tp.tv_usec / 1000000.0; - } - - return (tp.tv_sec - secbase) + tp.tv_usec / 1000000.0; -#endif -} - - -// Threads.c -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/threads.c -#define GDEF_OS_WINDOWS 1 -#if !GDEF_OS_WINDOWS -// The below define is necessary to use -// pthreads extensions like pthread_mutexattr_settype -#define _GNU_SOURCE -#endif // !GDEF_OS_WINDOWS - -#define MAX_THREADS 64 - -int dispatch; -int workcount; -int oldf; -qboolean pacifier; - -qboolean threaded; - -/* - ============= - GetThreadWork - - ============= - */ -int GetThreadWork(void) { - int r; - int f; - - ThreadLock(); - - if (dispatch == workcount) { - ThreadUnlock(); - return -1; - } - - f = 40 * dispatch / workcount; - if (f < oldf) { - Com_Printf("%s WARNING: progress went backwards (should never happen)\n", __func__); - oldf = f; - } - Com_Printf("%s ", __func__); - while (f > oldf) - { - ++oldf; - if (pacifier) { - if (oldf % 4 == 0) { - Com_Printf("%i", f / 4); - } - else { - Com_Printf("."); - } - fflush(stdout); /* ydnar */ - } - } - Com_Printf("\n"); - - r = dispatch; - dispatch++; - ThreadUnlock(); - - return r; -} - - -void (*workfunction)(int); - -void ThreadWorkerFunction(int threadnum) { - int work; - - while (1) - { - work = GetThreadWork(); - if (work == -1) { - break; - } - //Com_Printf("%s thread %i, work %i\n", __func__, threadnum, work); - workfunction(work); - } -} - -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)) { -// void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void(*func), void* param) { -// if (numthreads == -1) { -// ThreadSetDefault(); -// } -// workfunction = func; -// RunThreadsOn(workcnt, showpacifier, ThreadWorkerFunction, param); -// } - - -#if _WIN32 - -/* - =================================================================== - - WIN32 - - =================================================================== - */ - -#include - -int numthreads = -1; -CRITICAL_SECTION crit; -static int enter; - -void ThreadSetDefault(void) { - SYSTEM_INFO info; - - if (numthreads == -1) { // not set manually - GetSystemInfo(&info); - numthreads = info.dwNumberOfProcessors; - if (numthreads < 1 || numthreads > 32) { - numthreads = 1; - } - } - - Com_Printf("%s %i threads\n", __func__, numthreads); -} - - -void ThreadLock(void) { - if (!threaded) { - return; - } - EnterCriticalSection(&crit); - if (enter) { - Com_Printf("%s Recursive ThreadLock\n", __func__); - } - enter = 1; -} - -void ThreadUnlock(void) { - if (!threaded) { - return; - } - if (!enter) { - Com_Printf("%s ThreadUnlock without lock\n", __func__); - } - enter = 0; - LeaveCriticalSection(&crit); -} - -// Init and and run the threaded version -void BOTLIB_THREAD_LOADAAS(qboolean force) -{ - loadaas_t loadaas; - loadaas.force = force; - //RunThreadsOnIndividual(2, true, TestThreadedFunc, &loadaas); - //if (numthreads == -1) { - // ThreadSetDefault(); - //} - numthreads = 1; - GetThreadWork(); - RunThreadsOn(1, true, BOTLIB_THREADING_LOADAAS, &loadaas); -} - -// Threaded version -void BOTLIB_THREADING_LOADAAS(void *param) -{ - // Sleep while map fully loads and is ready, otherwise we'll get a crash - while (level.framenum < 50) - { - Sleep(100); - } - - //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web - //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing - loadaas_t* params = (loadaas_t*)param; - if (params != NULL) - { - //Com_Printf("%s param:0x%x force = %d\n", __func__, ¶m, params->force); - ACEND_LoadAAS(params->force); - } -} - -// ===================================================================================== -// Init and and run the threaded version -void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to) -{ - dijkstra_path_t dpath; - dpath.ent = ent; - dpath.from = from; - dpath.to = to; - - numthreads = 1; - GetThreadWork(); - //RunThreadsOn(1, true, BOTLIB_THREADING_DijkstraPath, &dpath); - - HANDLE thdHandle = CreateThread(NULL, 0, BOTLIB_THREADING_DijkstraPath, &dpath, 0, NULL); - if (thdHandle != NULL) - WaitForSingleObject(thdHandle, INFINITE); -} - -// Threaded version -void BOTLIB_THREADING_DijkstraPath(void* param) -{ - //https://search.brave.com/search?q=windows+CreateThread+passing+parameter&source=web - //https://stackoverflow.com/questions/5654015/windows-c-thread-parameter-passing - dijkstra_path_t* dpath = (dijkstra_path_t*)param; - if (dpath != NULL) - { - //Com_Printf("%s dpath:0x%x from[%d] to[%d]\n", __func__, &dpath, dpath->from, dpath->to); - BOTLIB_DijkstraPath(dpath->ent, dpath->from, dpath->to, true); - } -} -// ===================================================================================== - -/* - ============= - RunThreadsOn - ============= - */ -//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func), void *param) { - int threadid[MAX_THREADS]; - HANDLE threadhandle[MAX_THREADS]; - int i; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - // - // run threads in parallel - // - InitializeCriticalSection(&crit); - - if (numthreads < 1) - return; - - //if (numthreads == 1) { // use same thread - // func(0); - //} - //else - { - for (i = 0; i < numthreads; i++) - { - threadhandle[i] = CreateThread( - NULL, // LPSECURITY_ATTRIBUTES lpsa, - //0, // DWORD cbStack, - - /* ydnar: cranking stack size to eliminate radiosity crash with 1MB stack on win32 */ - (4096 * 1024), - - (LPTHREAD_START_ROUTINE)func, // LPTHREAD_START_ROUTINE lpStartAddr, - (LPVOID)param, - //(LPVOID)i, // LPVOID lpvThreadParm, - 0, // DWORD fdwCreate, - &threadid[i]); - } - - //for (i = 0; i < numthreads; i++) - // WaitForSingleObject(threadhandle[i], INFINITE); - } - DeleteCriticalSection(&crit); - - threaded = qfalse; - end = I_FloatTime(); - if (pacifier) { - Com_Printf("%s (%i)\n", __func__, end - start); - } -} - - -#elif GDEF_OS_OSF1 - -/* - =================================================================== - - OSF1 - - =================================================================== - */ - -int numthreads = 4; - -void ThreadSetDefault(void) { - if (numthreads == -1) { // not set manually - numthreads = 4; - } -} - -#include - -pthread_mutex_t* my_mutex; - -void ThreadLock(void) { - if (my_mutex) { - pthread_mutex_lock(my_mutex); - } -} - -void ThreadUnlock(void) { - if (my_mutex) { - pthread_mutex_unlock(my_mutex); - } -} - - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int i; - pthread_t work_threads[MAX_THREADS]; - pthread_addr_t status; - pthread_attr_t attrib; - pthread_mutexattr_t mattrib; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - if (!my_mutex) { - my_mutex = safe_malloc(sizeof(*my_mutex)); - if (pthread_mutexattr_create(&mattrib) == -1) { - Error("pthread_mutex_attr_create failed"); - } - if (pthread_mutexattr_setkind_np(&mattrib, MUTEX_FAST_NP) == -1) { - Error("pthread_mutexattr_setkind_np failed"); - } - if (pthread_mutex_init(my_mutex, mattrib) == -1) { - Error("pthread_mutex_init failed"); - } - } - - if (pthread_attr_create(&attrib) == -1) { - Error("pthread_attr_create failed"); - } - if (pthread_attr_setstacksize(&attrib, 0x100000) == -1) { - Error("pthread_attr_setstacksize failed"); - } - - for (i = 0; i < numthreads; i++) - { - if (pthread_create(&work_threads[i], attrib - , (pthread_startroutine_t)func, (pthread_addr_t)i) == -1) { - Error("pthread_create failed"); - } - } - - for (i = 0; i < numthreads; i++) - { - if (pthread_join(work_threads[i], &status) == -1) { - Error("pthread_join failed"); - } - } - - threaded = qfalse; - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#elif GDEF_OS_IRIX - -/* - =================================================================== - - IRIX - - =================================================================== - */ - -#define USED - -#include -#include -#include -#include - -int numthreads = -1; -abilock_t lck; - -void ThreadSetDefault(void) { - if (numthreads == -1) { - numthreads = prctl(PR_MAXPPROCS); - } - Sys_Printf("%i threads\n", numthreads); - usconfig(CONF_INITUSERS, numthreads); -} - - -void ThreadLock(void) { - spin_lock(&lck); -} - -void ThreadUnlock(void) { - release_lock(&lck); -} - - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int i; - int pid[MAX_THREADS]; - int start, end; - - start = I_FloatTime(); - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - init_lock(&lck); - - for (i = 0; i < numthreads - 1; i++) - { - pid[i] = sprocsp((void (*)(void*, size_t))func, PR_SALL, (void*)i - , NULL, 0x200000); // 2 meg stacks - if (pid[i] == -1) { - perror("sproc"); - Error("sproc failed"); - } - } - - func(i); - - for (i = 0; i < numthreads - 1; i++) - wait(NULL); - - threaded = qfalse; - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#elif GDEF_OS_LINUX || GDEF_OS_BSD || GDEF_OS_MACOS - -/* - ======================================================================= - - Linux pthreads - - ======================================================================= - */ - -#include - -int numthreads = -1; - -void ThreadSetDefault(void) { - if (numthreads == -1) { // not set manually -#ifdef _SC_NPROCESSORS_ONLN - long cpus = sysconf(_SC_NPROCESSORS_ONLN); - if (cpus > 0) { - numthreads = cpus; - } - else -#endif - /* can't detect, so default to four threads */ - numthreads = 4; - } - - if (numthreads > 1) { - Sys_Printf("threads: %d\n", numthreads); - } -} - -#include - -typedef struct pt_mutex_s -{ - pthread_t* owner; - pthread_mutex_t a_mutex; - pthread_cond_t cond; - unsigned int lock; -} pt_mutex_t; - -pt_mutex_t global_lock; - -void ThreadLock(void) { - pt_mutex_t* pt_mutex = &global_lock; - - if (!threaded) { - return; - } - - pthread_mutex_lock(&pt_mutex->a_mutex); - if (pthread_equal(pthread_self(), (pthread_t)&pt_mutex->owner)) { - pt_mutex->lock++; - } - else - { - if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { - pt_mutex->owner = (pthread_t*)pthread_self(); - pt_mutex->lock = 1; - } - else - { - while (1) - { - pthread_cond_wait(&pt_mutex->cond, &pt_mutex->a_mutex); - if ((!pt_mutex->owner) && (pt_mutex->lock == 0)) { - pt_mutex->owner = (pthread_t*)pthread_self(); - pt_mutex->lock = 1; - break; - } - } - } - } - pthread_mutex_unlock(&pt_mutex->a_mutex); -} - -void ThreadUnlock(void) { - pt_mutex_t* pt_mutex = &global_lock; - - if (!threaded) { - return; - } - - pthread_mutex_lock(&pt_mutex->a_mutex); - pt_mutex->lock--; - - if (pt_mutex->lock == 0) { - pt_mutex->owner = NULL; - pthread_cond_signal(&pt_mutex->cond); - } - - pthread_mutex_unlock(&pt_mutex->a_mutex); -} - -void recursive_mutex_init(pthread_mutexattr_t attribs) { - pt_mutex_t* pt_mutex = &global_lock; - - pt_mutex->owner = NULL; - if (pthread_mutex_init(&pt_mutex->a_mutex, &attribs) != 0) { - Error("pthread_mutex_init failed\n"); - } - if (pthread_cond_init(&pt_mutex->cond, NULL) != 0) { - Error("pthread_cond_init failed\n"); - } - - pt_mutex->lock = 0; -} - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - pthread_mutexattr_t mattrib; - pthread_attr_t attr; - pthread_t work_threads[MAX_THREADS]; - size_t stacksize; - - int start, end; - int i = 0; - - start = I_FloatTime(); - pacifier = showpacifier; - - dispatch = 0; - oldf = -1; - workcount = workcnt; - - pthread_attr_init(&attr); - if (pthread_attr_setstacksize(&attr, 8388608) != 0) { - stacksize = 0; - pthread_attr_getstacksize(&attr, &stacksize); - Sys_Printf("Could not set a per-thread stack size of 8 MB, using only %.2f MB\n", stacksize / 1048576.0); - } - - if (numthreads == 1) { - func(0); - } - else - { - threaded = qtrue; - - if (pacifier) { - setbuf(stdout, NULL); - } - - if (pthread_mutexattr_init(&mattrib) != 0) { - Error("pthread_mutexattr_init failed"); - } - if (pthread_mutexattr_settype(&mattrib, PTHREAD_MUTEX_ERRORCHECK) != 0) { - Error("pthread_mutexattr_settype failed"); - } - recursive_mutex_init(mattrib); - - for (i = 0; i < numthreads; i++) - { - /* Default pthread attributes: joinable & non-realtime scheduling */ - if (pthread_create(&work_threads[i], &attr, (void* (*)(void*)) func, (void*)(uintptr_t)i) != 0) { - Error("pthread_create failed"); - } - } - for (i = 0; i < numthreads; i++) - { - if (pthread_join(work_threads[i], NULL) != 0) { - Error("pthread_join failed"); - } - } - pthread_mutexattr_destroy(&mattrib); - threaded = qfalse; - } - - end = I_FloatTime(); - if (pacifier) { - Sys_Printf(" (%i)\n", end - start); - } -} - - -#else // UNKNOWN OS - -/* - ======================================================================= - - SINGLE THREAD - - ======================================================================= - */ - -int numthreads = 1; - -void ThreadSetDefault(void) { - numthreads = 1; -} - -void ThreadLock(void) { -} - -void ThreadUnlock(void) { -} - -/* - ============= - RunThreadsOn - ============= - */ -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)) { - int start, end; - - dispatch = 0; - workcount = workcnt; - oldf = -1; - pacifier = showpacifier; - start = I_FloatTime(); - func(0); - - end = I_FloatTime(); - if (pacifier) { - Com_Printf("%s (%i)\n", __func__, end - start); - } -} - -#endif // UNKNOWN OS - - diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index cee4e79d2..b0dfbfdcc 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -679,9 +679,8 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Invalid node - if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false) - { - //Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false){ + Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); return false; } @@ -689,20 +688,22 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi return false; // Already at node - if (goal_node == self->bot.current_node) - { - //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); + if (goal_node == self->bot.current_node){ + Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); return false; } // NEW PATHING: limit search to same area - if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) + if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) { + gi.dprintf("NEW: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); return false; - + } // OLD: searches all nodes //else if (AntStartSearch(self, self->bot.current_node, goal_node, path_randomization) == false) - else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false) + else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false){ + gi.dprintf("OLD: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); return false; + } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index af1e5eb29..0bd9315ca 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1575,10 +1575,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- s #ifndef NO_BOTS - - //BOTLIB_THREAD_LOADAAS(false); // Threaded version -- this will also generate AAS if it doesn't exist BOTLIB_InitNavigation(NULL); - #ifdef USE_ZLIB BOTLIB_LoadNavCompressed(); #else From 251f9f0b3d412406298052555e9707d43806ca46 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 9 Jul 2024 17:46:26 +0300 Subject: [PATCH 336/974] Update comment. --- inc/shared/game.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 85fc5b490..6d0efbb10 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -325,7 +325,7 @@ typedef struct { qboolean (*CanSave)(void); void (*PrepFrame)(void); void (*RestartFilesystem)(void); // called when fs_restart is issued - qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // must initialize `temp' + qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // if true is returned, `temp' must be initialized qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); } game_export_ex_t; From 3f909a9cd5b98399afa1af2f3b8ae4c40614c475 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 11 Jul 2024 15:15:24 +0300 Subject: [PATCH 337/974] Simplify packing entity and player states. --- src/common/msg.c | 59 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/common/msg.c b/src/common/msg.c index c85b3363f..8091ff655 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -450,20 +450,26 @@ void MSG_WriteDir(const vec3_t dir) MSG_WriteByte(best); } +#define PACK_COORDS(out, in) \ + (out[0] = COORD2SHORT(in[0]), \ + out[1] = COORD2SHORT(in[1]), \ + out[2] = COORD2SHORT(in[2])) + +#define PACK_ANGLES(out, in) \ + (out[0] = ANGLE2SHORT(in[0]), \ + out[1] = ANGLE2SHORT(in[1]), \ + out[2] = ANGLE2SHORT(in[2])) + void MSG_PackEntity(entity_packed_t *out, const entity_state_t *in, const entity_state_extension_t *ext) { // allow 0 to accomodate empty baselines Q_assert(in->number >= 0 && in->number < MAX_EDICTS); out->number = in->number; - out->origin[0] = COORD2SHORT(in->origin[0]); - out->origin[1] = COORD2SHORT(in->origin[1]); - out->origin[2] = COORD2SHORT(in->origin[2]); - out->angles[0] = ANGLE2SHORT(in->angles[0]); - out->angles[1] = ANGLE2SHORT(in->angles[1]); - out->angles[2] = ANGLE2SHORT(in->angles[2]); - out->old_origin[0] = COORD2SHORT(in->old_origin[0]); - out->old_origin[1] = COORD2SHORT(in->old_origin[1]); - out->old_origin[2] = COORD2SHORT(in->old_origin[2]); + + PACK_COORDS(out->origin, in->origin); + PACK_COORDS(out->old_origin, in->old_origin); + PACK_ANGLES(out->angles, in->angles); + out->modelindex = in->modelindex; out->modelindex2 = in->modelindex2; out->modelindex3 = in->modelindex3; @@ -475,6 +481,7 @@ void MSG_PackEntity(entity_packed_t *out, const entity_state_t *in, const entity out->frame = in->frame; out->sound = in->sound; out->event = in->event; + if (ext) { out->morefx = ext->morefx; out->alpha = Q_clip_uint8(ext->alpha * 255.0f); @@ -782,24 +789,36 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, } #define OFFSET2CHAR(x) Q_clip_int8((x) * 4) +#define BLEND2BYTE(x) Q_clip_uint8((x) * 255) + +#define PACK_OFFSET(out, in) \ + (out[0] = OFFSET2CHAR(in[0]), \ + out[1] = OFFSET2CHAR(in[1]), \ + out[2] = OFFSET2CHAR(in[2])) + +#define PACK_BLEND(out, in) \ + (out[0] = BLEND2BYTE(in[0]), \ + out[1] = BLEND2BYTE(in[1]), \ + out[2] = BLEND2BYTE(in[2]), \ + out[3] = BLEND2BYTE(in[3])) void MSG_PackPlayer(player_packed_t *out, const player_state_t *in) { - int i; - out->pmove = in->pmove; - for (i = 0; i < 3; i++) out->viewangles[i] = ANGLE2SHORT(in->viewangles[i]); - for (i = 0; i < 3; i++) out->viewoffset[i] = OFFSET2CHAR(in->viewoffset[i]); - for (i = 0; i < 3; i++) out->kick_angles[i] = OFFSET2CHAR(in->kick_angles[i]); - for (i = 0; i < 3; i++) out->gunoffset[i] = OFFSET2CHAR(in->gunoffset[i]); - for (i = 0; i < 3; i++) out->gunangles[i] = OFFSET2CHAR(in->gunangles[i]); + + PACK_ANGLES(out->viewangles, in->viewangles); + PACK_OFFSET(out->viewoffset, in->viewoffset); + PACK_OFFSET(out->kick_angles, in->kick_angles); + PACK_OFFSET(out->gunoffset, in->gunoffset); + PACK_OFFSET(out->gunangles, in->gunangles); + out->gunindex = in->gunindex; out->gunframe = in->gunframe; - for (i = 0; i < 4; i++) - out->blend[i] = Q_clip_uint8(in->blend[i] * 255); - out->fov = (int)in->fov; + PACK_BLEND(out->blend, in->blend); + out->fov = Q_clip_uint8(in->fov); out->rdflags = in->rdflags; - for (i = 0; i < MAX_STATS; i++) + + for (int i = 0; i < MAX_STATS; i++) out->stats[i] = in->stats[i]; } From 0f2b2356a3e3028c3274c1b020a839d43af907f7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 11 Jul 2024 15:17:57 +0300 Subject: [PATCH 338/974] Write packed player state byte arrays directly. --- src/common/msg.c | 102 +++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 74 deletions(-) diff --git a/src/common/msg.c b/src/common/msg.c index 8091ff655..1c6fe128b 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -926,11 +926,8 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player // // write the rest of the player_state_t // - if (pflags & PS_VIEWOFFSET) { - MSG_WriteChar(to->viewoffset[0]); - MSG_WriteChar(to->viewoffset[1]); - MSG_WriteChar(to->viewoffset[2]); - } + if (pflags & PS_VIEWOFFSET) + MSG_WriteData(to->viewoffset, sizeof(to->viewoffset)); if (pflags & PS_VIEWANGLES) { MSG_WriteShort(to->viewangles[0]); @@ -938,11 +935,8 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteShort(to->viewangles[2]); } - if (pflags & PS_KICKANGLES) { - MSG_WriteChar(to->kick_angles[0]); - MSG_WriteChar(to->kick_angles[1]); - MSG_WriteChar(to->kick_angles[2]); - } + if (pflags & PS_KICKANGLES) + MSG_WriteData(to->kick_angles, sizeof(to->kick_angles)); if (pflags & PS_WEAPONINDEX) { if (flags & MSG_PS_EXTENSIONS) @@ -953,20 +947,12 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (pflags & PS_WEAPONFRAME) { MSG_WriteByte(to->gunframe); - MSG_WriteChar(to->gunoffset[0]); - MSG_WriteChar(to->gunoffset[1]); - MSG_WriteChar(to->gunoffset[2]); - MSG_WriteChar(to->gunangles[0]); - MSG_WriteChar(to->gunangles[1]); - MSG_WriteChar(to->gunangles[2]); + MSG_WriteData(to->gunoffset, sizeof(to->gunoffset)); + MSG_WriteData(to->gunangles, sizeof(to->gunangles)); } - if (pflags & PS_BLEND) { - MSG_WriteByte(to->blend[0]); - MSG_WriteByte(to->blend[1]); - MSG_WriteByte(to->blend[2]); - MSG_WriteByte(to->blend[3]); - } + if (pflags & PS_BLEND) + MSG_WriteData(to->blend, sizeof(to->blend)); if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1156,11 +1142,8 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, // // write the rest of the player_state_t // - if (pflags & PS_VIEWOFFSET) { - MSG_WriteChar(to->viewoffset[0]); - MSG_WriteChar(to->viewoffset[1]); - MSG_WriteChar(to->viewoffset[2]); - } + if (pflags & PS_VIEWOFFSET) + MSG_WriteData(to->viewoffset, sizeof(to->viewoffset)); if (pflags & PS_VIEWANGLES) { MSG_WriteShort(to->viewangles[0]); @@ -1170,11 +1153,8 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (eflags & EPS_VIEWANGLE2) MSG_WriteShort(to->viewangles[2]); - if (pflags & PS_KICKANGLES) { - MSG_WriteChar(to->kick_angles[0]); - MSG_WriteChar(to->kick_angles[1]); - MSG_WriteChar(to->kick_angles[2]); - } + if (pflags & PS_KICKANGLES) + MSG_WriteData(to->kick_angles, sizeof(to->kick_angles)); if (pflags & PS_WEAPONINDEX) { if (flags & MSG_PS_EXTENSIONS) @@ -1186,24 +1166,14 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (pflags & PS_WEAPONFRAME) MSG_WriteByte(to->gunframe); - if (eflags & EPS_GUNOFFSET) { - MSG_WriteChar(to->gunoffset[0]); - MSG_WriteChar(to->gunoffset[1]); - MSG_WriteChar(to->gunoffset[2]); - } + if (eflags & EPS_GUNOFFSET) + MSG_WriteData(to->gunoffset, sizeof(to->gunoffset)); - if (eflags & EPS_GUNANGLES) { - MSG_WriteChar(to->gunangles[0]); - MSG_WriteChar(to->gunangles[1]); - MSG_WriteChar(to->gunangles[2]); - } + if (eflags & EPS_GUNANGLES) + MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PS_BLEND) { - MSG_WriteByte(to->blend[0]); - MSG_WriteByte(to->blend[1]); - MSG_WriteByte(to->blend[2]); - MSG_WriteByte(to->blend[3]); - } + if (pflags & PS_BLEND) + MSG_WriteData(to->blend, sizeof(to->blend)); if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1342,11 +1312,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, // // write the rest of the player_state_t // - if (pflags & PPS_VIEWOFFSET) { - MSG_WriteChar(to->viewoffset[0]); - MSG_WriteChar(to->viewoffset[1]); - MSG_WriteChar(to->viewoffset[2]); - } + if (pflags & PPS_VIEWOFFSET) + MSG_WriteData(to->viewoffset, sizeof(to->viewoffset)); if (pflags & PPS_VIEWANGLES) { MSG_WriteShort(to->viewangles[0]); @@ -1356,11 +1323,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_VIEWANGLE2) MSG_WriteShort(to->viewangles[2]); - if (pflags & PPS_KICKANGLES) { - MSG_WriteChar(to->kick_angles[0]); - MSG_WriteChar(to->kick_angles[1]); - MSG_WriteChar(to->kick_angles[2]); - } + if (pflags & PPS_KICKANGLES) + MSG_WriteData(to->kick_angles, sizeof(to->kick_angles)); if (pflags & PPS_WEAPONINDEX) { if (flags & MSG_PS_EXTENSIONS) @@ -1372,24 +1336,14 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_WEAPONFRAME) MSG_WriteByte(to->gunframe); - if (pflags & PPS_GUNOFFSET) { - MSG_WriteChar(to->gunoffset[0]); - MSG_WriteChar(to->gunoffset[1]); - MSG_WriteChar(to->gunoffset[2]); - } + if (pflags & PPS_GUNOFFSET) + MSG_WriteData(to->gunoffset, sizeof(to->gunoffset)); - if (pflags & PPS_GUNANGLES) { - MSG_WriteChar(to->gunangles[0]); - MSG_WriteChar(to->gunangles[1]); - MSG_WriteChar(to->gunangles[2]); - } + if (pflags & PPS_GUNANGLES) + MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PPS_BLEND) { - MSG_WriteByte(to->blend[0]); - MSG_WriteByte(to->blend[1]); - MSG_WriteByte(to->blend[2]); - MSG_WriteByte(to->blend[3]); - } + if (pflags & PPS_BLEND) + MSG_WriteData(to->blend, sizeof(to->blend)); if (pflags & PPS_FOV) MSG_WriteByte(to->fov); From 136c1d762e451aeef9adfd6e485f4a4a3e934f5e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 11 Jul 2024 15:18:41 +0300 Subject: [PATCH 339/974] Use vec4_t type for blend. --- inc/shared/shared.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 167d2eaf6..9ba07740a 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1439,7 +1439,7 @@ typedef struct { int gunindex; int gunframe; - float blend[4]; // rgba full screen effect + vec4_t blend; // rgba full screen effect float fov; // horizontal field of view From 9b15c2294cb930731200460f5b36cae765e301dc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 9 Jul 2024 17:43:37 +0300 Subject: [PATCH 340/974] Support more protocol extensions. Add new game API version that supports player_state_t extensions for larger map coordinates, more stats and separate screen/damage blend factors. --- inc/common/intreadwrite.h | 17 + inc/common/msg.h | 47 +- inc/common/pmove.h | 6 +- inc/common/protocol.h | 45 +- inc/shared/game.h | 36 +- inc/shared/shared.h | 112 +++- meson.build | 12 +- meson_options.txt | 5 + src/client/client.h | 6 +- src/client/demo.c | 10 +- src/client/gtv.c | 23 +- src/client/parse.c | 68 ++- src/client/predict.c | 4 +- src/common/msg.c | 680 ++++++++++++++++------- src/common/pmove/common.c | 52 ++ src/common/pmove/new.c | 27 + src/common/pmove/old.c | 27 + src/common/{pmove.c => pmove/template.c} | 59 +- src/game/g_local.h | 8 + src/game/g_misc.c | 2 +- src/game/g_save.c | 11 + src/game/p_client.c | 8 +- src/server/commands.c | 2 +- src/server/entities.c | 16 +- src/server/game.c | 28 +- src/server/init.c | 6 + src/server/main.c | 29 +- src/server/mvd.c | 69 ++- src/server/mvd/client.c | 20 +- src/server/mvd/game.c | 38 +- src/server/mvd/parse.c | 17 +- src/server/save.c | 2 +- src/server/send.c | 7 +- src/server/server.h | 55 +- src/server/user.c | 3 + 35 files changed, 1133 insertions(+), 424 deletions(-) create mode 100644 src/common/pmove/common.c create mode 100644 src/common/pmove/new.c create mode 100644 src/common/pmove/old.c rename src/common/{pmove.c => pmove/template.c} (96%) diff --git a/inc/common/intreadwrite.h b/inc/common/intreadwrite.h index 320e559f2..bbe4eb124 100644 --- a/inc/common/intreadwrite.h +++ b/inc/common/intreadwrite.h @@ -73,6 +73,13 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #define RL16(p) ((((const uint8_t *)(p))[1] << 8) | ((const uint8_t *)(p))[0]) #endif +#ifndef RL24 +#define RL24(p) \ + (((uint32_t)((const uint8_t *)(p))[2] << 16) | \ + ((uint32_t)((const uint8_t *)(p))[1] << 8) | \ + ((uint32_t)((const uint8_t *)(p))[0])) +#endif + #ifndef RL32 #define RL32(p) \ (((uint32_t)((const uint8_t *)(p))[3] << 24) | \ @@ -102,6 +109,16 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); } while (0) #endif +#ifndef WL24 +#define WL24(p, v) \ + do { \ + uint32_t _v = (v); \ + ((uint8_t *)p)[0] = _v & 0xff; \ + ((uint8_t *)p)[1] = (_v >> 8) & 0xff; \ + ((uint8_t *)p)[2] = (_v >> 16) & 0xff; \ + } while (0) +#endif + #ifndef WL32 #define WL32(p, v) \ do { \ diff --git a/inc/common/msg.h b/inc/common/msg.h index 899bd549b..5d5a7538d 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -21,15 +21,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/protocol.h" #include "common/sizebuf.h" -#define MAX_PACKETENTITY_BYTES 64 // 62 bytes worst case + margin +#define MAX_PACKETENTITY_BYTES 70 // 68 bytes worst case + 2 byte eof // entity and player states are pre-quantized before sending to make delta // comparsion easier typedef struct { uint16_t number; - int16_t origin[3]; int16_t angles[3]; - int16_t old_origin[3]; + int32_t origin[3]; + int32_t old_origin[3]; uint16_t modelindex; uint16_t modelindex2; uint16_t modelindex3; @@ -49,18 +49,19 @@ typedef struct { } entity_packed_t; typedef struct { - pmove_state_t pmove; - int16_t viewangles[3]; - int8_t viewoffset[3]; - int8_t kick_angles[3]; - int8_t gunangles[3]; - int8_t gunoffset[3]; - uint16_t gunindex; - uint8_t gunframe; - uint8_t blend[4]; - uint8_t fov; - uint8_t rdflags; - int16_t stats[MAX_STATS]; + pmove_state_new_t pmove; + int16_t viewangles[3]; + int8_t viewoffset[3]; + int8_t kick_angles[3]; + int8_t gunangles[3]; + int8_t gunoffset[3]; + uint16_t gunindex; + uint8_t gunframe; + uint8_t blend[4]; + uint8_t damage_blend[4]; + uint8_t fov; + uint8_t rdflags; + int16_t stats[MAX_STATS_NEW]; } player_packed_t; typedef enum { @@ -71,8 +72,9 @@ typedef enum { MSG_PS_IGNORE_DELTAANGLES = BIT(4), // ignore delta_angles MSG_PS_IGNORE_PREDICTION = BIT(5), // mutually exclusive with IGNORE_VIEWANGLES MSG_PS_EXTENSIONS = BIT(6), // enable protocol extensions - MSG_PS_FORCE = BIT(7), // send even if unchanged (MVD stream only) - MSG_PS_REMOVE = BIT(8), // player is removed (MVD stream only) + MSG_PS_EXTENSIONS_2 = BIT(7), // enable more protocol extensions + MSG_PS_FORCE = BIT(8), // send even if unchanged (MVD stream only) + MSG_PS_REMOVE = BIT(9), // player is removed (MVD stream only) } msgPsFlags_t; typedef enum { @@ -84,7 +86,8 @@ typedef enum { MSG_ES_BEAMORIGIN = BIT(5), // client has RF_BEAM old_origin fix MSG_ES_SHORTANGLES = BIT(6), // higher precision angles encoding MSG_ES_EXTENSIONS = BIT(7), // enable protocol extensions - MSG_ES_REMOVE = BIT(8), // entity is removed (MVD stream only) + MSG_ES_EXTENSIONS_2 = BIT(8), // enable more protocol extensions + MSG_ES_REMOVE = BIT(9), // entity is removed (MVD stream only) } msgEsFlags_t; extern sizebuf_t msg_write; @@ -106,7 +109,8 @@ void MSG_WriteShort(int c); void MSG_WriteLong(int c); void MSG_WriteLong64(int64_t c); void MSG_WriteString(const char *s); -void MSG_WritePos(const vec3_t pos); +void MSG_WritePos(const vec3_t pos, bool extended); +void MSG_WriteIntPos(const int32_t pos[3], bool extended); void MSG_WriteAngle(float f); #if USE_CLIENT void MSG_FlushBits(void); @@ -117,7 +121,8 @@ int MSG_WriteDeltaUsercmd_Enhanced(const usercmd_t *from, const usercmd_t *c void MSG_WriteDir(const vec3_t vector); void MSG_PackEntity(entity_packed_t *out, const entity_state_t *in, const entity_state_extension_t *ext); void MSG_WriteDeltaEntity(const entity_packed_t *from, const entity_packed_t *to, msgEsFlags_t flags); -void MSG_PackPlayer(player_packed_t *out, const player_state_t *in); +void MSG_PackPlayerOld(player_packed_t *out, const player_state_old_t *in); +void MSG_PackPlayerNew(player_packed_t *out, const player_state_new_t *in); void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags); int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags); void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, const player_packed_t *to, int number, msgPsFlags_t flags); @@ -144,7 +149,7 @@ int64_t MSG_ReadLong64(void); size_t MSG_ReadString(char *dest, size_t size); size_t MSG_ReadStringLine(char *dest, size_t size); #if USE_CLIENT -void MSG_ReadPos(vec3_t pos); +void MSG_ReadPos(vec3_t pos, bool extended); void MSG_ReadDir(vec3_t vector); #endif int MSG_ReadBits(int bits); diff --git a/inc/common/pmove.h b/inc/common/pmove.h index e9071ddca..e55d6b153 100644 --- a/inc/common/pmove.h +++ b/inc/common/pmove.h @@ -34,6 +34,8 @@ typedef struct { bool strafehack; bool flyhack; bool waterhack; + byte time_shift; + byte coord_bits; float speedmult; float watermult; float maxspeed; @@ -42,7 +44,9 @@ typedef struct { float flyfriction; } pmoveParams_t; -void Pmove(pmove_t *pmove, const pmoveParams_t *params); +void PmoveOld(pmove_old_t *pmove, const pmoveParams_t *params); +void PmoveNew(pmove_new_t *pmove, const pmoveParams_t *params); void PmoveInit(pmoveParams_t *pmp); void PmoveEnableQW(pmoveParams_t *pmp); +void PmoveEnableExt(pmoveParams_t *pmp); diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 50e7c053d..eb941e05c 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -24,34 +24,37 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_MSGLEN 0x8000 // max length of a message, 32 KiB -#define PROTOCOL_VERSION_OLD 26 -#define PROTOCOL_VERSION_DEFAULT 34 -#define PROTOCOL_VERSION_R1Q2 35 -#define PROTOCOL_VERSION_Q2PRO 36 -#define PROTOCOL_VERSION_MVD 37 // not used for UDP connections -#define PROTOCOL_VERSION_EXTENDED 3434 +#define PROTOCOL_VERSION_OLD 26 +#define PROTOCOL_VERSION_DEFAULT 34 +#define PROTOCOL_VERSION_R1Q2 35 +#define PROTOCOL_VERSION_Q2PRO 36 +#define PROTOCOL_VERSION_MVD 37 // not used for UDP connections +#define PROTOCOL_VERSION_EXTENDED_OLD 3434 +#define PROTOCOL_VERSION_EXTENDED 3435 #define PROTOCOL_VERSION_R1Q2_MINIMUM 1903 // b6377 #define PROTOCOL_VERSION_R1Q2_UCMD 1904 // b7387 #define PROTOCOL_VERSION_R1Q2_LONG_SOLID 1905 // b7759 #define PROTOCOL_VERSION_R1Q2_CURRENT 1905 // b7759 -#define PROTOCOL_VERSION_Q2PRO_MINIMUM 1015 // r335 -#define PROTOCOL_VERSION_Q2PRO_RESERVED 1016 // r364 -#define PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN 1017 // r1037-8 -#define PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES 1018 // r1037-44 -#define PROTOCOL_VERSION_Q2PRO_SERVER_STATE 1019 // r1302 -#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LAYOUT 1020 // r1354 -#define PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS 1021 // r1358 -#define PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT 1022 // r2161 -#define PROTOCOL_VERSION_Q2PRO_CINEMATICS 1023 // r2263 -#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS 1024 // r2894 -#define PROTOCOL_VERSION_Q2PRO_CURRENT 1024 // r2894 +#define PROTOCOL_VERSION_Q2PRO_MINIMUM 1015 // r335 +#define PROTOCOL_VERSION_Q2PRO_RESERVED 1016 // r364 +#define PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN 1017 // r1037-8 +#define PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES 1018 // r1037-44 +#define PROTOCOL_VERSION_Q2PRO_SERVER_STATE 1019 // r1302 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LAYOUT 1020 // r1354 +#define PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS 1021 // r1358 +#define PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT 1022 // r2161 +#define PROTOCOL_VERSION_Q2PRO_CINEMATICS 1023 // r2263 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS 1024 // r2894 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 1025 // r3300 +#define PROTOCOL_VERSION_Q2PRO_CURRENT 1025 // r3300 #define PROTOCOL_VERSION_MVD_MINIMUM 2009 // r168 #define PROTOCOL_VERSION_MVD_DEFAULT 2010 // r177 #define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS 2011 // r2894 -#define PROTOCOL_VERSION_MVD_CURRENT 2011 // r2894 +#define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2 2012 // r3300 +#define PROTOCOL_VERSION_MVD_CURRENT 2012 // r3300 #define R1Q2_SUPPORTED(x) \ ((x) >= PROTOCOL_VERSION_R1Q2_MINIMUM && \ @@ -72,6 +75,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define Q2PRO_PF_QW_MODE BIT(1) #define Q2PRO_PF_WATERJUMP_HACK BIT(2) #define Q2PRO_PF_EXTENSIONS BIT(3) +#define Q2PRO_PF_EXTENSIONS_2 BIT(4) //========================================= @@ -178,11 +182,12 @@ typedef enum { mvd_num_types } mvd_ops_t; -// MVD stream flags (only 3 bits can be used) +// MVD stream flags typedef enum { MVF_NOMSGS = BIT(0), MVF_SINGLEPOV = BIT(1), - MVF_EXTLIMITS = BIT(2) + MVF_EXTLIMITS = BIT(2), + MVF_EXTLIMITS_2 = BIT(3), } mvd_flags_t; //============================================== diff --git a/inc/shared/game.h b/inc/shared/game.h index 6d0efbb10..a13c19aa8 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -24,7 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc., // game.h -- game dll information visible to server // -#define GAME_API_VERSION 3 +#define GAME_API_VERSION_OLD 3 // game uses gclient_old_t +#define GAME_API_VERSION_NEW 3300 // game uses gclient_new_t + +#if USE_NEW_GAME_API +#define GAME_API_VERSION GAME_API_VERSION_NEW +#else +#define GAME_API_VERSION GAME_API_VERSION_OLD +#endif // edict->svflags @@ -78,9 +85,24 @@ typedef struct gclient_s gclient_t; #ifndef GAME_INCLUDE -struct gclient_s { - player_state_t ps; // communicated by server to clients - int ping; +typedef struct gclient_old_s gclient_old_t; +typedef struct gclient_new_s gclient_new_t; + +struct gclient_old_s { + player_state_old_t ps; // communicated by server to clients + int ping; + + // set to (client POV entity number) - 1 by game, + // only valid if g_features has GMF_CLIENTNUM bit + int clientNum; + + // the game dll can add anything it wants after + // this point in the structure +}; + +struct gclient_new_s { + player_state_new_t ps; // communicated by server to clients + int ping; // set to (client POV entity number) - 1 by game, // only valid if g_features has GMF_CLIENTNUM bit @@ -92,7 +114,7 @@ struct gclient_s { struct edict_s { entity_state_t s; - struct gclient_s *client; + void *client; qboolean inuse; int linkcount; @@ -170,7 +192,11 @@ typedef struct { void (*linkentity)(edict_t *ent); void (*unlinkentity)(edict_t *ent); // call before removing an interactive edict int (*BoxEdicts)(const vec3_t mins, const vec3_t maxs, edict_t **list, int maxcount, int areatype); +#ifdef GAME_INCLUDE void (*Pmove)(pmove_t *pmove); // player movement code common with client prediction +#else + void (*Pmove)(void *pmove); +#endif // network messaging void (*multicast)(const vec3_t origin, multicast_t to); diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 9ba07740a..3dde2d8fb 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -877,6 +877,7 @@ typedef enum { //KEX #define PMF_IGNORE_PLAYER_COLLISION BIT(7) +#define PMF_ON_LADDER BIT(8) //KEX // this structure needs to be communicated bit-accurate @@ -894,7 +895,21 @@ typedef struct { short gravity; short delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters -} pmove_state_t; +} pmove_state_old_t; + +#if USE_NEW_GAME_API +typedef struct { + pmtype_t pm_type; + + int32_t origin[3]; // 19.3 + int32_t velocity[3]; // 19.3 + uint16_t pm_flags; // ducked, jump_held, etc + uint16_t pm_time; // in msec + int16_t gravity; + int16_t delta_angles[3]; // add to command angles to get view direction + // changed by spawns, rotating objects, and teleporters +} pmove_state_new_t; +#endif // // button bits @@ -914,16 +929,17 @@ typedef struct { } usercmd_t; #define MAXTOUCH 32 + typedef struct { // state (in / out) - pmove_state_t s; + pmove_state_old_t s; // command (in) usercmd_t cmd; qboolean snapinitial; // if s has been changed outside pmove // results (out) - int numtouch; + int numtouch; struct edict_s *touchents[MAXTOUCH]; vec3_t viewangles; // clamped @@ -932,13 +948,41 @@ typedef struct { vec3_t mins, maxs; // bounding box size struct edict_s *groundentity; - int watertype; - int waterlevel; + int watertype; + int waterlevel; // callbacks to test the world trace_t (* q_gameabi trace)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end); int (*pointcontents)(const vec3_t point); -} pmove_t; +} pmove_old_t; + +#if USE_NEW_GAME_API +typedef struct { + // state (in / out) + pmove_state_new_t s; + + // command (in) + usercmd_t cmd; + qboolean snapinitial; // if s has been changed outside pmove + + // results (out) + int numtouch; + struct edict_s *touchents[MAXTOUCH]; + + vec3_t viewangles; // clamped + float viewheight; + + vec3_t mins, maxs; // bounding box size + + struct edict_s *groundentity; + int watertype; + int waterlevel; + + // callbacks to test the world + trace_t (* q_gameabi trace)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end); + int (*pointcontents)(const vec3_t point); +} pmove_new_t; +#endif // entity_state_t->effects // Effects are things handled on the client side (lights, particles, frame animations) @@ -1222,10 +1266,11 @@ enum { STAT_FLASHES, // cleared each frame, 1 = health, 2 = armor STAT_CHASE, STAT_SPECTATOR, - - MAX_STATS = 32 }; +#define MAX_STATS_OLD 32 +#define MAX_STATS_NEW 64 + // STAT_LAYOUTS flags #define LAYOUTS_LAYOUT BIT(0) #define LAYOUTS_INVENTORY BIT(1) @@ -1425,7 +1470,7 @@ typedef struct { // but the number of pmove_state_t changes will be reletive to client // frame rates typedef struct { - pmove_state_t pmove; // for prediction + pmove_state_old_t pmove; // for prediction // these fields do not need to be communicated bit-precise @@ -1445,8 +1490,37 @@ typedef struct { int rdflags; // refdef flags - short stats[MAX_STATS]; // fast status bar updates -} player_state_t; + short stats[MAX_STATS_OLD]; // fast status bar updates +} player_state_old_t; + +#if USE_NEW_GAME_API +typedef struct { + pmove_state_new_t pmove; // for prediction + + // these fields do not need to be communicated bit-precise + + vec3_t viewangles; // for fixed views + vec3_t viewoffset; // add to pmovestate->origin + vec3_t kick_angles; // add to view direction to get render angles + // set by weapon kicks, pain effects, etc + + vec3_t gunangles; + vec3_t gunoffset; + int gunindex; + int gunframe; + + vec4_t blend; // rgba full screen effect + vec4_t damage_blend; + + float fov; // horizontal field of view + + int rdflags; // refdef flags + + int reserved[4]; + + int16_t stats[MAX_STATS_NEW]; // fast status bar updates +} player_state_new_t; +#endif //============================================== @@ -1467,3 +1541,19 @@ typedef struct { } entity_state_extension_t; #endif + +#if USE_NEW_GAME_API + +#define MAX_STATS MAX_STATS_NEW +typedef pmove_new_t pmove_t; +typedef pmove_state_new_t pmove_state_t; +typedef player_state_new_t player_state_t; + +#else + +#define MAX_STATS MAX_STATS_OLD +typedef pmove_old_t pmove_t; +typedef pmove_state_old_t pmove_state_t; +typedef player_state_old_t player_state_t; + +#endif diff --git a/meson.build b/meson.build index ea05b51b0..5517108b0 100644 --- a/meson.build +++ b/meson.build @@ -24,7 +24,9 @@ common_src = [ 'src/common/msg.c', 'src/common/net/chan.c', 'src/common/net/net.c', - 'src/common/pmove.c', + 'src/common/pmove/common.c', + 'src/common/pmove/new.c', + 'src/common/pmove/old.c', 'src/common/prompt.c', 'src/common/sizebuf.c', 'src/common/utils.c', @@ -201,11 +203,16 @@ else common_args += '-D_GNU_SOURCE' endif -engine_args = [] +engine_args = ['-DUSE_NEW_GAME_API=1'] if win32 engine_args += '-D_WIN32_WINNT=0x0601' endif +game_args = [] +if get_option('game-new-api') + game_args += '-DUSE_NEW_GAME_API=1' +endif + common_link_args = [] exe_link_args = [] dll_link_args = [] @@ -446,6 +453,7 @@ shared_library('game' + cpu, game_src, include_directories: 'inc', gnu_symbol_visibility: 'hidden', link_args: dll_link_args, + c_args: game_args, install: system_wide, install_dir: libdir / get_option('base-game'), override_options: get_option('game-build-options'), diff --git a/meson_options.txt b/meson_options.txt index dba718cb2..ec8367a95 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -44,6 +44,11 @@ option('game-build-options', value: [], description: 'Custom game build options for LTO, etc') +option('game-new-api', + type: 'boolean', + value: false, + description: 'Build game using new API for big maps support') + option('homedir', type: 'string', value: '~/.q2pro', diff --git a/src/client/client.h b/src/client/client.h index eb23207d5..20814ca3e 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -171,7 +171,7 @@ typedef struct { usercmd_t cmd; usercmd_t cmds[CMD_BACKUP]; // each mesage will send several old cmds unsigned cmdNumber; - short predicted_origins[CMD_BACKUP][3]; // for debug comparing against server + int predicted_origins[CMD_BACKUP][3]; // for debug comparing against server client_history_t history[CMD_BACKUP]; unsigned initialSeq; @@ -478,6 +478,7 @@ typedef struct { bool seeking; bool eof; msgEsFlags_t esFlags; // for snapshots/recording + msgPsFlags_t psFlags; } demo; #if USE_CLIENT_GTV @@ -687,6 +688,9 @@ void CL_SendCmd(void); #define CL_ES_EXTENDED_MASK \ (MSG_ES_LONGSOLID | MSG_ES_UMASK | MSG_ES_BEAMORIGIN | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS) +#define CL_ES_EXTENDED_MASK_2 (CL_ES_EXTENDED_MASK | MSG_ES_EXTENSIONS_2) +#define CL_PS_EXTENDED_MASK_2 (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) + typedef struct { int type; vec3_t pos1; diff --git a/src/client/demo.c b/src/client/demo.c index b73b4ab46..e33940898 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -174,12 +174,12 @@ static void emit_delta_frame(const server_frame_t *from, const server_frame_t *t // delta encode the playerstate MSG_WriteByte(svc_playerinfo); - MSG_PackPlayer(&newpack, &to->ps); + MSG_PackPlayerNew(&newpack, &to->ps); if (from) { - MSG_PackPlayer(&oldpack, &from->ps); - MSG_WriteDeltaPlayerstate_Default(&oldpack, &newpack, cl.psFlags); + MSG_PackPlayerNew(&oldpack, &from->ps); + MSG_WriteDeltaPlayerstate_Default(&oldpack, &newpack, cls.demo.psFlags); } else { - MSG_WriteDeltaPlayerstate_Default(NULL, &newpack, cl.psFlags); + MSG_WriteDeltaPlayerstate_Default(NULL, &newpack, cls.demo.psFlags); } // delta encode the entities @@ -1178,7 +1178,7 @@ bool CL_GetDemoInfo(const char *path, demoInfo_t *info) goto fail; } c = MSG_ReadLong(); - if (c == PROTOCOL_VERSION_EXTENDED) { + if (c == PROTOCOL_VERSION_EXTENDED || c == PROTOCOL_VERSION_EXTENDED_OLD) { csr = &cs_remap_new; } else if (c < PROTOCOL_VERSION_OLD || c > PROTOCOL_VERSION_DEFAULT) { goto fail; diff --git a/src/client/gtv.c b/src/client/gtv.c index c8c6b1d91..1c18f7c39 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -37,7 +37,7 @@ static void build_gamestate(void) memset(cls.gtv.entities, 0, sizeof(cls.gtv.entities)); // set base player states - MSG_PackPlayer(&cls.gtv.ps, &cl.frame.ps); + MSG_PackPlayerNew(&cls.gtv.ps, &cl.frame.ps); // set base entity states for (i = 1; i < cl.csr.max_edicts; i++) { @@ -51,9 +51,7 @@ static void build_gamestate(void) } // set protocol flags - cls.gtv.esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN; - if (cl.csr.extended) - cls.gtv.esFlags |= MSG_ES_LONGSOLID | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; + cls.gtv.esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN | (cl.esFlags & CL_ES_EXTENDED_MASK_2); } static void emit_gamestate(void) @@ -65,14 +63,19 @@ static void emit_gamestate(void) // send the serverdata flags = MVF_SINGLEPOV; - if (cl.csr.extended) + if (cl.csr.extended) { flags |= MVF_EXTLIMITS; - MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - if (cl.csr.extended) + if (cl.esFlags & MSG_ES_EXTENSIONS_2) + flags |= MVF_EXTLIMITS_2; + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_CURRENT); - else + MSG_WriteShort(flags); + } else { + MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_DEFAULT); + } MSG_WriteLong(cl.servercount); MSG_WriteString(cl.gamedir); MSG_WriteShort(-1); @@ -131,7 +134,7 @@ void CL_GTV_EmitFrame(void) MSG_WriteByte(0); // send player state - MSG_PackPlayer(&newps, &cl.frame.ps); + MSG_PackPlayerNew(&newps, &cl.frame.ps); MSG_WriteDeltaPlayerstate_Packet(&cls.gtv.ps, &newps, cl.clientNum, cl.psFlags | MSG_PS_FORCE); diff --git a/src/client/parse.c b/src/client/parse.c index 1ddb2981d..bd2fe4b06 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -530,13 +530,14 @@ static void CL_ParseServerData(void) cls.serverProtocol, protocol); } // BIG HACK to let demos from release work with the 3.0x patch!!! - if (protocol == PROTOCOL_VERSION_EXTENDED) { + if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_EXTENDED_OLD) { cl.csr = cs_remap_new; - protocol = PROTOCOL_VERSION_DEFAULT; + cls.serverProtocol = PROTOCOL_VERSION_DEFAULT; } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_DEFAULT) { Com_Error(ERR_DROP, "Demo uses unsupported protocol version %d.", protocol); + } else { + cls.serverProtocol = protocol; } - cls.serverProtocol = protocol; } // game directory @@ -638,6 +639,16 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO protocol extensions enabled\n"); cl.csr = cs_remap_new; } + if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 && + i & Q2PRO_PF_EXTENSIONS_2) { + if (!cl.csr.extended) { + Com_Error(ERR_DROP, "Q2PRO_PF_EXTENSIONS_2 without Q2PRO_PF_EXTENSIONS"); + } + Com_DPrintf("Q2PRO protocol extensions v2 enabled\n"); + cl.esFlags |= MSG_ES_EXTENSIONS_2; + cl.psFlags |= MSG_PS_EXTENSIONS_2; + PmoveEnableExt(&cl.pmp); + } } else { if (MSG_ReadByte()) { Com_DPrintf("Q2PRO strafejump hack enabled\n"); @@ -669,9 +680,17 @@ static void CL_ParseServerData(void) if (cl.csr.extended) { cl.esFlags |= CL_ES_EXTENDED_MASK; cl.psFlags |= MSG_PS_EXTENSIONS; + + // hack for demo playback + if (protocol == PROTOCOL_VERSION_EXTENDED) { + cl.esFlags |= MSG_ES_EXTENSIONS_2; + cl.psFlags |= MSG_PS_EXTENSIONS_2; + } } - cls.demo.esFlags = cl.csr.extended ? CL_ES_EXTENDED_MASK : 0; + // use full extended flags unless writing backward compatible demo + cls.demo.esFlags = cl.csr.extended ? CL_ES_EXTENDED_MASK_2 : 0; + cls.demo.psFlags = cl.csr.extended ? CL_PS_EXTENDED_MASK_2 : 0; if (cinematic) { SCR_PlayCinematic(levelname); @@ -708,6 +727,11 @@ tent_params_t te; mz_params_t mz; snd_params_t snd; +static void CL_ReadPos(vec3_t pos) +{ + MSG_ReadPos(pos, cl.esFlags & MSG_ES_EXTENSIONS_2); +} + static void CL_ParseTEntPacket(void) { te.type = MSG_ReadByte(); @@ -730,7 +754,7 @@ static void CL_ParseTEntPacket(void) case TE_ELECTRIC_SPARKS: case TE_BLUEHYPERBLASTER_2: case TE_BERSERK_SLAM: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); break; @@ -739,7 +763,7 @@ static void CL_ParseTEntPacket(void) case TE_WELDING_SPARKS: case TE_TUNNEL_SPARKS: te.count = MSG_ReadByte(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); te.color = MSG_ReadByte(); break; @@ -752,8 +776,8 @@ static void CL_ParseTEntPacket(void) case TE_BUBBLETRAIL2: case TE_BFG_LASER: case TE_BFG_ZAP: - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_GRENADE_EXPLOSION: @@ -777,7 +801,7 @@ static void CL_ParseTEntPacket(void) case TE_NUKEBLAST: case TE_EXPLOSION1_NL: case TE_EXPLOSION2_NL: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); break; case TE_PARASITE_ATTACK: @@ -787,39 +811,39 @@ static void CL_ParseTEntPacket(void) case TE_GRAPPLE_CABLE_2: case TE_LIGHTNING_BEAM: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_GRAPPLE_CABLE: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); - MSG_ReadPos(te.offset); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); + CL_ReadPos(te.offset); break; case TE_LIGHTNING: te.entity1 = MSG_ReadShort(); te.entity2 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_FLASHLIGHT: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); te.entity1 = MSG_ReadShort(); break; case TE_FORCEWALL: - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); te.color = MSG_ReadByte(); break; case TE_STEAM: te.entity1 = MSG_ReadShort(); te.count = MSG_ReadByte(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); te.color = MSG_ReadByte(); te.entity2 = MSG_ReadShort(); @@ -830,7 +854,7 @@ static void CL_ParseTEntPacket(void) case TE_WIDOWBEAMOUT: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); break; case TE_POWER_SPLASH: @@ -910,7 +934,7 @@ static void CL_ParseStartSoundPacket(void) // positioned in space if (flags & SND_POS) - MSG_ReadPos(snd.pos); + CL_ReadPos(snd.pos); SHOWNET(2, " %s\n", cl.configstrings[cl.csr.sounds + snd.index]); } diff --git a/src/client/predict.c b/src/client/predict.c index b87a2a8e3..aac6ff5fa 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -235,7 +235,7 @@ void CL_PredictMovement(void) // run frames while (++ack <= current) { pm.cmd = cl.cmds[ack & CMD_MASK]; - Pmove(&pm, &cl.pmp); + PmoveNew(&pm, &cl.pmp); // save for debug checking VectorCopy(pm.s.origin, cl.predicted_origins[ack & CMD_MASK]); @@ -247,7 +247,7 @@ void CL_PredictMovement(void) pm.cmd.forwardmove = cl.localmove[0]; pm.cmd.sidemove = cl.localmove[1]; pm.cmd.upmove = cl.localmove[2]; - Pmove(&pm, &cl.pmp); + PmoveNew(&pm, &cl.pmp); frame = current; // save for debug checking diff --git a/src/common/msg.c b/src/common/msg.c index 1c6fe128b..b70bf2476 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -172,13 +172,18 @@ void MSG_WriteString(const char *string) /* ============= -MSG_WriteCoord +MSG_WriteDeltaInt23 ============= */ - -static inline void MSG_WriteCoord(float f) +static void MSG_WriteDeltaInt23(int32_t from, int32_t to) { - MSG_WriteShort(COORD2SHORT(f)); + int32_t delta = to - from; + if (delta >= -0x4000 && delta <= 0x3fff) { + MSG_WriteShort((uint32_t)delta << 1); + } else { + byte *buf = SZ_GetSpace(&msg_write, 3); + WL24(buf, ((uint32_t)to << 1) | 1); + } } /* @@ -186,11 +191,35 @@ static inline void MSG_WriteCoord(float f) MSG_WritePos ============= */ -void MSG_WritePos(const vec3_t pos) +void MSG_WritePos(const vec3_t pos, bool extended) +{ + if (extended) { + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[0])); + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[1])); + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[2])); + } else { + MSG_WriteShort(COORD2SHORT(pos[0])); + MSG_WriteShort(COORD2SHORT(pos[1])); + MSG_WriteShort(COORD2SHORT(pos[2])); + } +} + +/* +============= +MSG_WriteIntPos +============= +*/ +void MSG_WriteIntPos(const int32_t pos[3], bool extended) { - MSG_WriteCoord(pos[0]); - MSG_WriteCoord(pos[1]); - MSG_WriteCoord(pos[2]); + if (extended) { + MSG_WriteDeltaInt23(0, pos[0]); + MSG_WriteDeltaInt23(0, pos[1]); + MSG_WriteDeltaInt23(0, pos[2]); + } else { + MSG_WriteShort(pos[0]); + MSG_WriteShort(pos[1]); + MSG_WriteShort(pos[2]); + } } /* @@ -725,9 +754,15 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, else if (bits & U_RENDERFX16) MSG_WriteShort(to->renderfx); - if (bits & U_ORIGIN1) MSG_WriteShort(to->origin[0]); - if (bits & U_ORIGIN2) MSG_WriteShort(to->origin[1]); - if (bits & U_ORIGIN3) MSG_WriteShort(to->origin[2]); + if (flags & MSG_ES_EXTENSIONS_2) { + if (bits & U_ORIGIN1) MSG_WriteDeltaInt23(from->origin[0], to->origin[0]); + if (bits & U_ORIGIN2) MSG_WriteDeltaInt23(from->origin[1], to->origin[1]); + if (bits & U_ORIGIN3) MSG_WriteDeltaInt23(from->origin[2], to->origin[2]); + } else { + if (bits & U_ORIGIN1) MSG_WriteShort(to->origin[0]); + if (bits & U_ORIGIN2) MSG_WriteShort(to->origin[1]); + if (bits & U_ORIGIN3) MSG_WriteShort(to->origin[2]); + } if (bits & U_ANGLE16) { if (bits & U_ANGLE1) MSG_WriteShort(to->angles[0]); @@ -739,11 +774,8 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, if (bits & U_ANGLE3) MSG_WriteChar(to->angles[2] >> 8); } - if (bits & U_OLDORIGIN) { - MSG_WriteShort(to->old_origin[0]); - MSG_WriteShort(to->old_origin[1]); - MSG_WriteShort(to->old_origin[2]); - } + if (bits & U_OLDORIGIN) + MSG_WriteIntPos(to->old_origin, flags & MSG_ES_EXTENSIONS_2); if (bits & U_SOUND) { if (flags & MSG_ES_EXTENSIONS) { @@ -802,7 +834,33 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, out[2] = BLEND2BYTE(in[2]), \ out[3] = BLEND2BYTE(in[3])) -void MSG_PackPlayer(player_packed_t *out, const player_state_t *in) +void MSG_PackPlayerOld(player_packed_t *out, const player_state_old_t *in) +{ + out->pmove.pm_type = in->pmove.pm_type; + VectorCopy(in->pmove.origin, out->pmove.origin); + VectorCopy(in->pmove.velocity, out->pmove.velocity); + out->pmove.pm_flags = in->pmove.pm_flags; + out->pmove.pm_time = in->pmove.pm_time; + out->pmove.gravity = in->pmove.gravity; + VectorCopy(in->pmove.delta_angles, out->pmove.delta_angles); + + PACK_ANGLES(out->viewangles, in->viewangles); + PACK_OFFSET(out->viewoffset, in->viewoffset); + PACK_OFFSET(out->kick_angles, in->kick_angles); + PACK_OFFSET(out->gunoffset, in->gunoffset); + PACK_OFFSET(out->gunangles, in->gunangles); + + out->gunindex = in->gunindex; + out->gunframe = in->gunframe; + PACK_BLEND(out->blend, in->blend); + out->fov = Q_clip_uint8(in->fov); + out->rdflags = in->rdflags; + + for (int i = 0; i < MAX_STATS_OLD; i++) + out->stats[i] = in->stats[i]; +} + +void MSG_PackPlayerNew(player_packed_t *out, const player_state_new_t *in) { out->pmove = in->pmove; @@ -815,18 +873,80 @@ void MSG_PackPlayer(player_packed_t *out, const player_state_t *in) out->gunindex = in->gunindex; out->gunframe = in->gunframe; PACK_BLEND(out->blend, in->blend); + PACK_BLEND(out->damage_blend, in->damage_blend); out->fov = Q_clip_uint8(in->fov); out->rdflags = in->rdflags; - for (int i = 0; i < MAX_STATS; i++) + for (int i = 0; i < MAX_STATS_NEW; i++) out->stats[i] = in->stats[i]; } +static uint64_t MSG_CalcStatBits(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags) +{ + int numstats = (flags & MSG_PS_EXTENSIONS_2) ? MAX_STATS_NEW : MAX_STATS_OLD; + uint64_t statbits = 0; + + for (int i = 0; i < numstats; i++) + if (to->stats[i] != from->stats[i]) + statbits |= BIT_ULL(i); + + return statbits; +} + +static void MSG_WriteVarInt64(uint64_t v) +{ + do { + int c = v & 0x7f; + v >>= 7; + if (v) + c |= 0x80; + MSG_WriteByte(c); + } while (v); +} + +static void MSG_WriteStats(const player_packed_t *to, uint64_t statbits, msgPsFlags_t flags) +{ + int numstats; + + if (flags & MSG_PS_EXTENSIONS_2) { + MSG_WriteVarInt64(statbits); + numstats = MAX_STATS_NEW; + } else { + MSG_WriteLong(statbits); + numstats = MAX_STATS_OLD; + } + + for (int i = 0; i < numstats; i++) + if (statbits & BIT_ULL(i)) + MSG_WriteShort(to->stats[i]); +} + +static void MSG_WriteDeltaBlend(const player_packed_t *from, const player_packed_t *to) +{ + int i, bflags = 0; + + for (i = 0; i < 4; i++) { + if (to->blend[i] != from->blend[i]) + bflags |= BIT(i); + if (to->damage_blend[i] != from->damage_blend[i]) + bflags |= BIT(4 + i); + } + + MSG_WriteByte(bflags); + + for (i = 0; i < 4; i++) + if (bflags & BIT(i)) + MSG_WriteByte(to->blend[i]); + + for (i = 0; i < 4; i++) + if (bflags & BIT(4 + i)) + MSG_WriteByte(to->damage_blend[i]); +} + void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags) { - int i; - int pflags; - int statbits; + int pflags; + uint64_t statbits; Q_assert(to); @@ -870,6 +990,9 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (!Vector4Compare(to->blend, from->blend)) pflags |= PS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PS_BLEND; if (to->fov != from->fov) pflags |= PS_FOV; @@ -896,23 +1019,43 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (pflags & PS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - MSG_WriteShort(to->pmove.origin[2]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); + } - if (pflags & PS_M_VELOCITY) { - MSG_WriteShort(to->pmove.velocity[0]); - MSG_WriteShort(to->pmove.velocity[1]); - MSG_WriteShort(to->pmove.velocity[2]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteDeltaInt23(from->pmove.velocity[0], to->pmove.velocity[0]); + MSG_WriteDeltaInt23(from->pmove.velocity[1], to->pmove.velocity[1]); + MSG_WriteDeltaInt23(from->pmove.velocity[2], to->pmove.velocity[2]); + } + + if (pflags & PS_M_TIME) + MSG_WriteShort(to->pmove.pm_time); + + if (pflags & PS_M_FLAGS) + MSG_WriteShort(to->pmove.pm_flags); + } else { + if (pflags & PS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + MSG_WriteShort(to->pmove.origin[2]); + } - if (pflags & PS_M_TIME) - MSG_WriteByte(to->pmove.pm_time); + if (pflags & PS_M_VELOCITY) { + MSG_WriteShort(to->pmove.velocity[0]); + MSG_WriteShort(to->pmove.velocity[1]); + MSG_WriteShort(to->pmove.velocity[2]); + } + + if (pflags & PS_M_TIME) + MSG_WriteByte(to->pmove.pm_time); - if (pflags & PS_M_FLAGS) - MSG_WriteByte(to->pmove.pm_flags); + if (pflags & PS_M_FLAGS) + MSG_WriteByte(to->pmove.pm_flags); + } if (pflags & PS_M_GRAVITY) MSG_WriteShort(to->pmove.gravity); @@ -951,8 +1094,12 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteData(to->gunangles, sizeof(to->gunangles)); } - if (pflags & PS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -961,24 +1108,16 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteByte(to->rdflags); // send stats - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); + statbits = MSG_CalcStatBits(from, to, flags); + MSG_WriteStats(to, statbits, flags); } int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags) { - int i; - int pflags, eflags; - int statbits; + int pflags, eflags; + uint64_t statbits; Q_assert(to); @@ -1054,9 +1193,13 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (!(flags & MSG_PS_IGNORE_BLEND)) { if (!Vector4Compare(from->blend, to->blend)) pflags |= PS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PS_BLEND; } else { // save previous state Vector4Copy(from->blend, to->blend); + Vector4Copy(from->damage_blend, to->damage_blend); } if (from->fov != to->fov) @@ -1089,11 +1232,7 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, VectorCopy(from->gunangles, to->gunangles); } - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - + statbits = MSG_CalcStatBits(from, to, flags); if (statbits) eflags |= EPS_STATS; @@ -1108,27 +1247,51 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (pflags & PS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + } - if (eflags & EPS_M_ORIGIN2) - MSG_WriteShort(to->pmove.origin[2]); + if (eflags & EPS_M_ORIGIN2) + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); - if (pflags & PS_M_VELOCITY) { - MSG_WriteShort(to->pmove.velocity[0]); - MSG_WriteShort(to->pmove.velocity[1]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteDeltaInt23(from->pmove.velocity[0], to->pmove.velocity[0]); + MSG_WriteDeltaInt23(from->pmove.velocity[1], to->pmove.velocity[1]); + } + + if (eflags & EPS_M_VELOCITY2) + MSG_WriteDeltaInt23(from->pmove.velocity[2], to->pmove.velocity[2]); + + if (pflags & PS_M_TIME) + MSG_WriteShort(to->pmove.pm_time); + + if (pflags & PS_M_FLAGS) + MSG_WriteShort(to->pmove.pm_flags); + } else { + if (pflags & PS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + } + + if (eflags & EPS_M_ORIGIN2) + MSG_WriteShort(to->pmove.origin[2]); - if (eflags & EPS_M_VELOCITY2) - MSG_WriteShort(to->pmove.velocity[2]); + if (pflags & PS_M_VELOCITY) { + MSG_WriteShort(to->pmove.velocity[0]); + MSG_WriteShort(to->pmove.velocity[1]); + } + + if (eflags & EPS_M_VELOCITY2) + MSG_WriteShort(to->pmove.velocity[2]); - if (pflags & PS_M_TIME) - MSG_WriteByte(to->pmove.pm_time); + if (pflags & PS_M_TIME) + MSG_WriteByte(to->pmove.pm_time); - if (pflags & PS_M_FLAGS) - MSG_WriteByte(to->pmove.pm_flags); + if (pflags & PS_M_FLAGS) + MSG_WriteByte(to->pmove.pm_flags); + } if (pflags & PS_M_GRAVITY) MSG_WriteShort(to->pmove.gravity); @@ -1172,8 +1335,12 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (eflags & EPS_GUNANGLES) MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1182,12 +1349,8 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, MSG_WriteByte(to->rdflags); // send stats - if (eflags & EPS_STATS) { - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); - } + if (eflags & EPS_STATS) + MSG_WriteStats(to, statbits, flags); return eflags; } @@ -1207,9 +1370,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, int number, msgPsFlags_t flags) { - int i; - int pflags; - int statbits; + int pflags; + uint64_t statbits; // this can happen with client GTV if (number < 0 || number >= CLIENTNUM_NONE) @@ -1252,8 +1414,13 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (!VectorCompare(from->kick_angles, to->kick_angles)) pflags |= PPS_KICKANGLES; - if (!(flags & MSG_PS_IGNORE_BLEND) && !Vector4Compare(from->blend, to->blend)) - pflags |= PPS_BLEND; + if (!(flags & MSG_PS_IGNORE_BLEND)) { + if (!Vector4Compare(from->blend, to->blend)) + pflags |= PPS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PPS_BLEND; + } if (from->fov != to->fov) pflags |= PPS_FOV; @@ -1275,11 +1442,7 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, pflags |= PPS_GUNANGLES; } - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - + statbits = MSG_CalcStatBits(from, to, flags); if (statbits) pflags |= PPS_STATS; @@ -1301,13 +1464,23 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PPS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PPS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + } - if (pflags & PPS_M_ORIGIN2) - MSG_WriteShort(to->pmove.origin[2]); + if (pflags & PPS_M_ORIGIN2) + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); + } else { + if (pflags & PPS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + } + + if (pflags & PPS_M_ORIGIN2) + MSG_WriteShort(to->pmove.origin[2]); + } // // write the rest of the player_state_t @@ -1342,8 +1515,12 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_GUNANGLES) MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PPS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PPS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PPS_FOV) MSG_WriteByte(to->fov); @@ -1352,12 +1529,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, MSG_WriteByte(to->rdflags); // send stats - if (pflags & PPS_STATS) { - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); - } + if (pflags & PPS_STATS) + MSG_WriteStats(to, statbits, flags); } #endif // USE_MVD_SERVER || USE_MVD_CLIENT || USE_CLIENT_GTV @@ -1519,16 +1692,6 @@ static inline float MSG_ReadCoord(void) return SHORT2COORD(MSG_ReadShort()); } -#if USE_SERVER -static inline -#endif -void MSG_ReadPos(vec3_t pos) -{ - pos[0] = MSG_ReadCoord(); - pos[1] = MSG_ReadCoord(); - pos[2] = MSG_ReadCoord(); -} - static inline float MSG_ReadAngle(void) { return BYTE2ANGLE(MSG_ReadChar()); @@ -1539,7 +1702,46 @@ static inline float MSG_ReadAngle16(void) return SHORT2ANGLE(MSG_ReadShort()); } +static void MSG_ReadDeltaInt23(int32_t *to) +{ + uint32_t v = MSG_ReadWord(); + if (v & 1) { + v |= (uint32_t)MSG_ReadByte() << 16; + *to = SignExtend(v >> 1, 23); + } else { + *to += SignExtend(v >> 1, 15); + } +} + +static void MSG_ReadDeltaCoord(float *to) +{ + uint32_t v = MSG_ReadWord(); + if (v & 1) { + v |= (uint32_t)MSG_ReadByte() << 16; + *to = SHORT2COORD(SignExtend(v >> 1, 23)); + } else { + *to += SHORT2COORD(SignExtend(v >> 1, 15)); + } +} + +#endif + +#if USE_SERVER +static inline #endif +void MSG_ReadPos(vec3_t pos, bool extended) +{ + if (extended) { + VectorClear(pos); + MSG_ReadDeltaCoord(&pos[0]); + MSG_ReadDeltaCoord(&pos[1]); + MSG_ReadDeltaCoord(&pos[2]); + } else { + pos[0] = MSG_ReadCoord(); + pos[1] = MSG_ReadCoord(); + pos[2] = MSG_ReadCoord(); + } +} #if USE_CLIENT void MSG_ReadDir(vec3_t dir) @@ -1862,9 +2064,15 @@ void MSG_ParseDeltaEntity(entity_state_t *to, else if (bits & U_RENDERFX16) to->renderfx = MSG_ReadWord(); - if (bits & U_ORIGIN1) to->origin[0] = MSG_ReadCoord(); - if (bits & U_ORIGIN2) to->origin[1] = MSG_ReadCoord(); - if (bits & U_ORIGIN3) to->origin[2] = MSG_ReadCoord(); + if (flags & MSG_ES_EXTENSIONS_2) { + if (bits & U_ORIGIN1) MSG_ReadDeltaCoord(&to->origin[0]); + if (bits & U_ORIGIN2) MSG_ReadDeltaCoord(&to->origin[1]); + if (bits & U_ORIGIN3) MSG_ReadDeltaCoord(&to->origin[2]); + } else { + if (bits & U_ORIGIN1) to->origin[0] = MSG_ReadCoord(); + if (bits & U_ORIGIN2) to->origin[1] = MSG_ReadCoord(); + if (bits & U_ORIGIN3) to->origin[2] = MSG_ReadCoord(); + } if (flags & MSG_ES_SHORTANGLES && bits & U_ANGLE16) { if (bits & U_ANGLE1) to->angles[0] = MSG_ReadAngle16(); @@ -1877,7 +2085,7 @@ void MSG_ParseDeltaEntity(entity_state_t *to, } if (bits & U_OLDORIGIN) - MSG_ReadPos(to->old_origin); + MSG_ReadPos(to->old_origin, flags & MSG_ES_EXTENSIONS_2); if (bits & U_SOUND) { if (flags & MSG_ES_EXTENSIONS) { @@ -1925,6 +2133,63 @@ void MSG_ParseDeltaEntity(entity_state_t *to, #endif // USE_CLIENT || USE_MVD_CLIENT +static uint64_t MSG_ReadVarInt64(void) +{ + uint64_t v = 0; + int c, bits = 0; + + do { + c = MSG_ReadByte(); + if (c == -1) + break; + v |= (c & UINT64_C(0x7f)) << bits; + bits += 7; + } while (c & 0x80 && bits < 64); + + return v; +} + +static void MSG_ReadStats(player_state_t *to, msgPsFlags_t flags) +{ + uint64_t statbits; + int numstats; + + if (flags & MSG_PS_EXTENSIONS_2) { + statbits = MSG_ReadVarInt64(); + numstats = MAX_STATS_NEW; + } else { + statbits = MSG_ReadLong(); + numstats = MAX_STATS_OLD; + } + + if (!statbits) + return; + + for (int i = 0; i < numstats; i++) + if (statbits & BIT_ULL(i)) + to->stats[i] = MSG_ReadShort(); +} + +static void MSG_ReadBlend(player_state_t *to, msgPsFlags_t psflags) +{ + if (psflags & MSG_PS_EXTENSIONS_2) { + int bflags = MSG_ReadByte(); + + for (int i = 0; i < 4; i++) + if (bflags & BIT(i)) + to->blend[i] = MSG_ReadByte() / 255.0f; + + for (int i = 0; i < 4; i++) + if (bflags & BIT(4 + i)) + to->damage_blend[i] = MSG_ReadByte() / 255.0f; + } else { + to->blend[0] = MSG_ReadByte() / 255.0f; + to->blend[1] = MSG_ReadByte() / 255.0f; + to->blend[2] = MSG_ReadByte() / 255.0f; + to->blend[3] = MSG_ReadByte() / 255.0f; + } +} + #if USE_CLIENT /* @@ -1937,9 +2202,6 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, int flags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -1955,23 +2217,43 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, if (flags & PS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - to->pmove.origin[2] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + MSG_ReadDeltaInt23(&to->pmove.origin[2]); + } - if (flags & PS_M_VELOCITY) { - to->pmove.velocity[0] = MSG_ReadShort(); - to->pmove.velocity[1] = MSG_ReadShort(); - to->pmove.velocity[2] = MSG_ReadShort(); - } + if (flags & PS_M_VELOCITY) { + MSG_ReadDeltaInt23(&to->pmove.velocity[0]); + MSG_ReadDeltaInt23(&to->pmove.velocity[1]); + MSG_ReadDeltaInt23(&to->pmove.velocity[2]); + } + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadWord(); - if (flags & PS_M_TIME) - to->pmove.pm_time = MSG_ReadByte(); + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadWord(); + } else { + if (flags & PS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + to->pmove.origin[2] = MSG_ReadShort(); + } + + if (flags & PS_M_VELOCITY) { + to->pmove.velocity[0] = MSG_ReadShort(); + to->pmove.velocity[1] = MSG_ReadShort(); + to->pmove.velocity[2] = MSG_ReadShort(); + } - if (flags & PS_M_FLAGS) - to->pmove.pm_flags = MSG_ReadByte(); + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadByte(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadByte(); + } if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); @@ -2020,12 +2302,8 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2034,15 +2312,9 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - statbits = MSG_ReadLong(); - if (statbits) { - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + MSG_ReadStats(to, psflags); } - /* =================== MSG_ParseDeltaPlayerstate_Default @@ -2054,9 +2326,6 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, int extraflags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -2072,27 +2341,51 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, if (flags & PS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + } - if (extraflags & EPS_M_ORIGIN2) - to->pmove.origin[2] = MSG_ReadShort(); + if (extraflags & EPS_M_ORIGIN2) + MSG_ReadDeltaInt23(&to->pmove.origin[2]); - if (flags & PS_M_VELOCITY) { - to->pmove.velocity[0] = MSG_ReadShort(); - to->pmove.velocity[1] = MSG_ReadShort(); - } + if (flags & PS_M_VELOCITY) { + MSG_ReadDeltaInt23(&to->pmove.velocity[0]); + MSG_ReadDeltaInt23(&to->pmove.velocity[1]); + } - if (extraflags & EPS_M_VELOCITY2) - to->pmove.velocity[2] = MSG_ReadShort(); + if (extraflags & EPS_M_VELOCITY2) + MSG_ReadDeltaInt23(&to->pmove.velocity[2]); - if (flags & PS_M_TIME) - to->pmove.pm_time = MSG_ReadByte(); + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadWord(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadWord(); + } else { + if (flags & PS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + } - if (flags & PS_M_FLAGS) - to->pmove.pm_flags = MSG_ReadByte(); + if (extraflags & EPS_M_ORIGIN2) + to->pmove.origin[2] = MSG_ReadShort(); + + if (flags & PS_M_VELOCITY) { + to->pmove.velocity[0] = MSG_ReadShort(); + to->pmove.velocity[1] = MSG_ReadShort(); + } + + if (extraflags & EPS_M_VELOCITY2) + to->pmove.velocity[2] = MSG_ReadShort(); + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadByte(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadByte(); + } if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); @@ -2148,12 +2441,8 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2162,12 +2451,8 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - if (extraflags & EPS_STATS) { - statbits = MSG_ReadLong(); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + if (extraflags & EPS_STATS) + MSG_ReadStats(to, psflags); } #endif // USE_CLIENT @@ -2184,9 +2469,6 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, int flags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -2202,13 +2484,23 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, if (flags & PPS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PPS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PPS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + } - if (flags & PPS_M_ORIGIN2) - to->pmove.origin[2] = MSG_ReadShort(); + if (flags & PPS_M_ORIGIN2) + MSG_ReadDeltaInt23(&to->pmove.origin[2]); + } else { + if (flags & PPS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + } + + if (flags & PPS_M_ORIGIN2) + to->pmove.origin[2] = MSG_ReadShort(); + } // // parse the rest of the player_state_t @@ -2255,12 +2547,8 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PPS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PPS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PPS_FOV) to->fov = MSG_ReadByte(); @@ -2269,12 +2557,8 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - if (flags & PPS_STATS) { - statbits = MSG_ReadLong(); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + if (flags & PPS_STATS) + MSG_ReadStats(to, psflags); } #endif // USE_MVD_CLIENT diff --git a/src/common/pmove/common.c b/src/common/pmove/common.c new file mode 100644 index 000000000..e2d09cd63 --- /dev/null +++ b/src/common/pmove/common.c @@ -0,0 +1,52 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +void PmoveInit(pmoveParams_t *pmp) +{ + // set up default pmove parameters + memset(pmp, 0, sizeof(*pmp)); + + pmp->speedmult = 1; + pmp->watermult = 0.5f; + pmp->maxspeed = 300; + pmp->friction = 6; + pmp->waterfriction = 1; + pmp->flyfriction = 9; + pmp->time_shift = 3; + pmp->coord_bits = 16; +} + +void PmoveEnableQW(pmoveParams_t *pmp) +{ + pmp->qwmode = true; + pmp->watermult = 0.7f; + pmp->maxspeed = 320; + //pmp->upspeed = (sv_qwmod->integer > 1) ? 310 : 350; + pmp->friction = 4; + pmp->waterfriction = 4; + pmp->airaccelerate = true; +} + +void PmoveEnableExt(pmoveParams_t *pmp) +{ + pmp->time_shift = 0; + pmp->coord_bits = 23; +} diff --git a/src/common/pmove/new.c b/src/common/pmove/new.c new file mode 100644 index 000000000..a47f3f4c2 --- /dev/null +++ b/src/common/pmove/new.c @@ -0,0 +1,27 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +#define PMOVE_NEW 1 +#define PMOVE_TYPE pmove_new_t +#define PMOVE_FUNC PmoveNew +#define PMOVE_TIME_SHIFT pmp->time_shift +#define PMOVE_C2S(x) SignExtend(COORD2SHORT(x), pmp->coord_bits) +#include "template.c" diff --git a/src/common/pmove/old.c b/src/common/pmove/old.c new file mode 100644 index 000000000..6060e7ef1 --- /dev/null +++ b/src/common/pmove/old.c @@ -0,0 +1,27 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +#define PMOVE_OLD 1 +#define PMOVE_TYPE pmove_old_t +#define PMOVE_FUNC PmoveOld +#define PMOVE_TIME_SHIFT 3 +#define PMOVE_C2S(x) COORD2SHORT(x) +#include "template.c" diff --git a/src/common/pmove.c b/src/common/pmove/template.c similarity index 96% rename from src/common/pmove.c rename to src/common/pmove/template.c index 3a8076b10..1f785135b 100644 --- a/src/common/pmove.c +++ b/src/common/pmove/template.c @@ -16,9 +16,6 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "shared/shared.h" -#include "common/pmove.h" - #define STEPSIZE 18 // all of the locals will be zeroed before each @@ -36,11 +33,11 @@ typedef struct { cplane_t groundplane; int groundcontents; - short previous_origin[3]; + int previous_origin[3]; bool ladder; } pml_t; -static pmove_t *pm; +static PMOVE_TYPE *pm; static pml_t pml; static const pmoveParams_t *pmp; @@ -596,9 +593,9 @@ static void PM_CategorizePosition(void) pm->s.pm_flags |= PMF_TIME_LAND; // don't allow another jump for a little while if (pml.velocity[2] < -400) - pm->s.pm_time = 25; + pm->s.pm_time = 200 >> PMOVE_TIME_SHIFT; else - pm->s.pm_time = 18; + pm->s.pm_time = 144 >> PMOVE_TIME_SHIFT; } } } @@ -741,7 +738,7 @@ static void PM_CheckSpecialMovement(void) pml.velocity[2] = 350; pm->s.pm_flags |= PMF_TIME_WATERJUMP; - pm->s.pm_time = 255; + pm->s.pm_time = 2040 >> PMOVE_TIME_SHIFT; } /* @@ -924,20 +921,20 @@ static void PM_SnapPosition(void) { int sign[3]; int i, j, bits; - short base[3]; + int base[3]; // try all single bits first - static const byte jitterbits[8] = {0, 4, 1, 2, 3, 5, 6, 7}; + static const byte jitterbits[8] = { 0, 4, 1, 2, 3, 5, 6, 7 }; // snap velocity to eigths for (i = 0; i < 3; i++) - pm->s.velocity[i] = (int)(pml.velocity[i] * 8); + pm->s.velocity[i] = PMOVE_C2S(pml.velocity[i]); for (i = 0; i < 3; i++) { if (pml.origin[i] >= 0) sign[i] = 1; else sign[i] = -1; - pm->s.origin[i] = (int)(pml.origin[i] * 8); + pm->s.origin[i] = PMOVE_C2S(pml.origin[i]); if (pm->s.origin[i] * 0.125f == pml.origin[i]) sign[i] = 0; } @@ -968,8 +965,8 @@ PM_InitialSnapPosition static void PM_InitialSnapPosition(void) { int x, y, z; - short base[3]; - static const short offset[3] = { 0, -1, 1 }; + int base[3]; + static const int offset[3] = { 0, -1, 1 }; VectorCopy(pm->s.origin, base); @@ -1024,7 +1021,7 @@ Pmove Can be called by either the server or the client ================ */ -void Pmove(pmove_t *pmove, const pmoveParams_t *params) +void PMOVE_FUNC(PMOVE_TYPE *pmove, const pmoveParams_t *params) { pm = pmove; pmp = params; @@ -1085,7 +1082,7 @@ void Pmove(pmove_t *pmove, const pmoveParams_t *params) if (pm->s.pm_time) { int msec; - msec = pm->cmd.msec >> 3; + msec = pm->cmd.msec >> PMOVE_TIME_SHIFT; if (!msec) msec = 1; if (msec >= pm->s.pm_time) { @@ -1132,28 +1129,12 @@ void Pmove(pmove_t *pmove, const pmoveParams_t *params) PM_CategorizePosition(); PM_SnapPosition(); -} -void PmoveInit(pmoveParams_t *pmp) -{ - // set up default pmove parameters - memset(pmp, 0, sizeof(*pmp)); - - pmp->speedmult = 1; - pmp->watermult = 0.5f; - pmp->maxspeed = 300; - pmp->friction = 6; - pmp->waterfriction = 1; - pmp->flyfriction = 9; -} - -void PmoveEnableQW(pmoveParams_t *pmp) -{ - pmp->qwmode = true; - pmp->watermult = 0.7f; - pmp->maxspeed = 320; - //pmp->upspeed = (sv_qwmod->integer > 1) ? 310 : 350; - pmp->friction = 4; - pmp->waterfriction = 4; - pmp->airaccelerate = true; +#ifdef PMOVE_NEW + // export "on ladder" flag to game + if (pml.ladder) + pm->s.pm_flags |= PMF_ON_LADDER; + else + pm->s.pm_flags &= ~PMF_ON_LADDER; +#endif } diff --git a/src/game/g_local.h b/src/game/g_local.h index fdc7c5ea2..b858f30c1 100644 --- a/src/game/g_local.h +++ b/src/game/g_local.h @@ -252,6 +252,14 @@ typedef struct precache_s { void (*func)(void); } precache_t; +// new game API can be used w/o protocol extensions, +// so this needs to be dynamic +#if USE_NEW_GAME_API +#define PM_TIME_SHIFT (game.csr.extended ? 0 : 3) +#else +#define PM_TIME_SHIFT 3 +#endif + // // this structure is left intact through an entire game // it should be initialized at dll load time, and read/written to diff --git a/src/game/g_misc.c b/src/game/g_misc.c index ee3c9aec4..bcfbc1778 100644 --- a/src/game/g_misc.c +++ b/src/game/g_misc.c @@ -1635,7 +1635,7 @@ void teleporter_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t // clear the velocity and hold them in place briefly VectorClear(other->velocity); - other->client->ps.pmove.pm_time = 160 >> 3; // hold time + other->client->ps.pmove.pm_time = 160 >> PM_TIME_SHIFT; // hold time other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT; // draw the teleport splash at source and on the player diff --git a/src/game/g_save.c b/src/game/g_save.c index dfa4053c3..e6af7e1b5 100644 --- a/src/game/g_save.c +++ b/src/game/g_save.c @@ -305,10 +305,17 @@ static const save_field_t clientfields[] = { #define _OFS CLOFS I(ps.pmove.pm_type), +#if USE_NEW_GAME_API + IA(ps.pmove.origin, 3), + IA(ps.pmove.velocity, 3), + S(ps.pmove.pm_flags), + S(ps.pmove.pm_time), +#else SA(ps.pmove.origin, 3), SA(ps.pmove.velocity, 3), B(ps.pmove.pm_flags), B(ps.pmove.pm_time), +#endif S(ps.pmove.gravity), SA(ps.pmove.delta_angles, 3), @@ -799,7 +806,11 @@ static void read_fields(gzFile f, const save_field_t *fields, void *base) #define SAVE_MAGIC1 MakeLittleLong('S','S','V','1') #define SAVE_MAGIC2 MakeLittleLong('S','A','V','1') +#if USE_NEW_GAME_API +#define SAVE_VERSION 0x100 +#else #define SAVE_VERSION 8 +#endif static void check_gzip(int magic) { diff --git a/src/game/p_client.c b/src/game/p_client.c index 43c204863..a36f9db2d 100644 --- a/src/game/p_client.c +++ b/src/game/p_client.c @@ -938,7 +938,7 @@ void respawn(edict_t *self) // hold in place briefly self->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - self->client->ps.pmove.pm_time = 14; + self->client->ps.pmove.pm_time = 112 >> PM_TIME_SHIFT; self->client->respawn_framenum = level.framenum; @@ -1018,7 +1018,7 @@ void spectator_respawn(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 14; + ent->client->ps.pmove.pm_time = 112 >> PM_TIME_SHIFT; } ent->client->respawn_framenum = level.framenum; @@ -1236,7 +1236,7 @@ void ClientBeginDeathmatch(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 200 >> 3; + ent->client->ps.pmove.pm_time = 200 >> PM_TIME_SHIFT; } gi.bprintf(PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname); @@ -1284,7 +1284,7 @@ void ClientBegin(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 200 >> 3; + ent->client->ps.pmove.pm_time = 200 >> PM_TIME_SHIFT; } if (level.intermission_framenum) { diff --git a/src/server/commands.c b/src/server/commands.c index 7e7aceff5..fc2724da0 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -532,7 +532,7 @@ static void dump_clients(void) } Com_Printf("%3i %5i %s %-15.15s %7u %-21s %5i %2i %3i\n", client->number, - client->edict->client->ps.stats[STAT_FRAGS], + SV_GetClient_Stat(client, STAT_FRAGS), ping, client->name, svs.realtime - client->lastmessage, NET_AdrToString(&client->netchan.remote_address), client->rate, client->protocol, client->moves_per_sec); diff --git a/src/server/entities.c b/src/server/entities.c index 242452e83..c67019ade 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -346,7 +346,7 @@ bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) MSG_WriteData(frame->areabits, frame->areabytes); // ignore some parts of playerstate if not recording demo - psFlags = 0; + psFlags = client->psFlags; if (!client->settings[CLS_RECORDING]) { if (client->settings[CLS_NOGUN]) { psFlags |= MSG_PS_IGNORE_GUNFRAMES; @@ -379,9 +379,6 @@ bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) } else { suppressed = client->suppress_count; } - if (client->csr->extended) { - psFlags |= MSG_PS_EXTENSIONS; - } // delta encode the playerstate extraflags = MSG_WriteDeltaPlayerstate_Enhanced(oldstate, &frame->ps, psFlags); @@ -549,7 +546,6 @@ void SV_BuildClientFrame(client_t *client) edict_t *clent; client_frame_t *frame; entity_packed_t *state; - player_state_t *ps; const mleaf_t *leaf; int clientarea, clientcluster; byte clientphs[VIS_MAX_BYTES]; @@ -577,8 +573,7 @@ void SV_BuildClientFrame(client_t *client) client->frames_sent++; // find the client's PVS - ps = &clent->client->ps; - VectorMA(ps->viewoffset, 0.125f, ps->pmove.origin, org); + SV_GetClient_ViewOrg(client, org); leaf = CM_PointLeaf(client->cm, org); clientarea = leaf->area; @@ -592,11 +587,14 @@ void SV_BuildClientFrame(client_t *client) } // grab the current player_state_t - MSG_PackPlayer(&frame->ps, ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&frame->ps, clent->client); + else + MSG_PackPlayerOld(&frame->ps, clent->client); // grab the current clientNum if (g_features->integer & GMF_CLIENTNUM) { - frame->clientNum = clent->client->clientNum; + frame->clientNum = SV_GetClient_ClientNum(client); if (!VALIDATE_CLIENTNUM(client->csr, frame->clientNum)) { Com_WPrintf("%s: bad clientNum %d for client %d\n", __func__, frame->clientNum, client->number); diff --git a/src/server/game.c b/src/server/game.c index 19dec1100..1fb493f7b 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -440,6 +440,11 @@ static void PF_WriteFloat(float f) Com_Error(ERR_DROP, "PF_WriteFloat not implemented"); } +static void PF_WritePos(const vec3_t pos) +{ + MSG_WritePos(pos, svs.csr.extended && IS_NEW_GAME_API); +} + static qboolean PF_inVIS(const vec3_t p1, const vec3_t p2, vis_t vis) { const mleaf_t *leaf1, *leaf2; @@ -586,7 +591,7 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, MSG_WriteByte(ofs); MSG_WriteShort(sendchan); - MSG_WritePos(origin); + PF_WritePos(origin); // if the sound doesn't attenuate, send it to everyone // (global radio chatter, voiceovers, etc) @@ -704,13 +709,14 @@ static void PF_LocalSound(edict_t *target, const vec3_t origin, PF_Unicast(target, !!(channel & CHAN_RELIABLE)); } -void PF_Pmove(pmove_t *pm) +void PF_Pmove(void *pm) { - if (sv_client) { - Pmove(pm, &sv_client->pmp); - } else { - Pmove(pm, &sv_pmp); - } + const pmoveParams_t *pmp = sv_client ? &sv_client->pmp : &svs.pmp; + + if (IS_NEW_GAME_API) + PmoveNew(pm, pmp); + else + PmoveOld(pm, pmp); } static cvar_t *PF_cvar(const char *name, const char *value, int flags) @@ -813,7 +819,7 @@ static const game_import_t game_import = { .WriteLong = MSG_WriteLong, .WriteFloat = PF_WriteFloat, .WriteString = MSG_WriteString, - .WritePosition = MSG_WritePos, + .WritePosition = PF_WritePos, .WriteDir = MSG_WriteDir, .WriteAngle = MSG_WriteAngle, @@ -980,9 +986,11 @@ void SV_InitGameProgs(void) Com_Error(ERR_DROP, "Game library returned NULL exports"); } - if (ge->apiversion != GAME_API_VERSION) { + Com_DPrintf("Game API version: %d\n", ge->apiversion); + + if (ge->apiversion != GAME_API_VERSION_OLD && ge->apiversion != GAME_API_VERSION_NEW) { Com_Error(ERR_DROP, "Game library is version %d, expected %d", - ge->apiversion, GAME_API_VERSION); + ge->apiversion, GAME_API_VERSION_OLD); } // get extended api if present diff --git a/src/server/init.c b/src/server/init.c index eddd9b5e1..8975925fe 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -425,6 +425,9 @@ void SV_InitGame(unsigned mvd_spawn) svs.csr = cs_remap_old; + // set up default pmove parameters + PmoveInit(&svs.pmp); + // init game #if USE_MVD_CLIENT if (mvd_spawn) { @@ -441,6 +444,9 @@ void SV_InitGame(unsigned mvd_spawn) SV_MvdPostInit(); } + if (svs.csr.extended && IS_NEW_GAME_API) + PmoveEnableExt(&svs.pmp); + // send heartbeat very soon svs.last_heartbeat = -(HEARTBEAT_SECONDS - 5) * 1000; svs.heartbeat_index = 0; diff --git a/src/server/main.c b/src/server/main.c index ff8918bc4..c15adec85 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -19,8 +19,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" #include "client/input.h" -pmoveParams_t sv_pmp; - master_t sv_masters[MAX_MASTERS]; // address of group servers LIST_DECL(sv_banlist); @@ -449,7 +447,7 @@ static size_t SV_StatusString(char *status) } len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", - cl->edict->client->ps.stats[STAT_FRAGS], + SV_GetClient_Stat(cl, STAT_FRAGS), cl->ping, cl->name); if (len >= sizeof(entry)) { continue; @@ -809,10 +807,18 @@ static bool parse_enhanced_params(conn_params_t *p) } } - if (!CLIENT_COMPATIBLE(&svs.csr, p)) { + // verify protocol extensions compatibility + if (svs.csr.extended) { + int minimal = IS_NEW_GAME_API ? + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 : + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS; + + if (p->protocol == PROTOCOL_VERSION_Q2PRO && p->version >= minimal) + return true; + return reject("This is a protocol limit removing enhanced server.\n" "Your client version is not compatible. Make sure you are " - "running latest Q2PRO client version.\n"); + "running the latest Q2PRO client version.\n"); } return true; @@ -975,7 +981,7 @@ static void init_pmove_and_es_flags(client_t *newcl) int force; // copy default pmove parameters - newcl->pmp = sv_pmp; + newcl->pmp = svs.pmp; newcl->pmp.airaccelerate = sv_airaccelerate->integer; // common extensions @@ -1011,6 +1017,12 @@ static void init_pmove_and_es_flags(client_t *newcl) } if (svs.csr.extended) { newcl->esFlags |= MSG_ES_EXTENSIONS; + newcl->psFlags |= MSG_PS_EXTENSIONS; + + if (IS_NEW_GAME_API) { + newcl->esFlags |= MSG_ES_EXTENSIONS_2; + newcl->psFlags |= MSG_PS_EXTENSIONS_2; + } } force = 1; } @@ -1435,7 +1447,7 @@ static void SV_CalcPings(void) } // let the game dll know about the ping - cl->edict->client->ping = cl->ping; + SV_SetClient_Ping(cl, cl->ping); } } @@ -2280,9 +2292,6 @@ void SV_Init(void) sv.frametime = Com_ComputeFrametime(sv.framerate); #endif - // set up default pmove parameters - PmoveInit(&sv_pmp); - #if USE_SYSCON SV_SetConsoleTitle(); #endif diff --git a/src/server/mvd.c b/src/server/mvd.c index 3970268a9..d61bb75e5 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -479,7 +479,8 @@ should do it for us by providing some SVF_* flag or something. */ static bool player_is_active(const edict_t *ent) { - int num; + int num, pm_type, pm_flags; + float fov; if ((g_features->integer & GMF_PROPERINUSE) && !ent->inuse) { return false; @@ -503,8 +504,20 @@ static bool player_is_active(const edict_t *ent) } } + if (IS_NEW_GAME_API) { + const gclient_new_t *cl = ent->client; + pm_type = cl->ps.pmove.pm_type; + pm_flags = cl->ps.pmove.pm_flags; + fov = cl->ps.fov; + } else { + const gclient_old_t *cl = ent->client; + pm_type = cl->ps.pmove.pm_type; + pm_flags = cl->ps.pmove.pm_flags; + fov = cl->ps.fov; + } + // first of all, make sure player_state_t is valid - if (!ent->client->ps.fov) { + if (!fov) { return false; } @@ -514,7 +527,7 @@ static bool player_is_active(const edict_t *ent) } // never capture spectators - if (ent->client->ps.pmove.pm_type == PM_SPECTATOR) { + if (pm_type == PM_SPECTATOR) { return false; } @@ -532,12 +545,12 @@ static bool player_is_active(const edict_t *ent) } // they are likely following someone in case of PM_FREEZE - if (ent->client->ps.pmove.pm_type == PM_FREEZE) { + if (pm_type == PM_FREEZE) { return false; } // they are likely following someone if PMF_NO_PREDICTION is set - if (ent->client->ps.pmove.pm_flags & PMF_NO_PREDICTION) { + if (pm_flags & PMF_NO_PREDICTION) { return false; } @@ -574,7 +587,10 @@ static void build_gamestate(void) continue; } - MSG_PackPlayer(&mvd.players[i], &ent->client->ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&mvd.players[i], ent->client); + else + MSG_PackPlayerOld(&mvd.players[i], ent->client); PPS_INUSE(&mvd.players[i]) = true; } @@ -602,7 +618,7 @@ static void emit_gamestate(void) player_packed_t *ps; entity_packed_t *es; size_t length; - int flags, extra, portalbytes; + int flags, portalbytes; byte portalbits[MAX_MAP_PORTAL_BYTES]; // don't bother writing if there are no active MVD clients @@ -610,22 +626,25 @@ static void emit_gamestate(void) return; } - // pack MVD stream flags into extra bits - extra = 0; - if (sv_mvd_nomsgs->integer && mvd.dummy) { - extra |= MVF_NOMSGS << SVCMD_BITS; - } - if (svs.csr.extended) { - extra |= MVF_EXTLIMITS << SVCMD_BITS; - } + // setup MVD stream flags + flags = 0; + if (sv_mvd_nomsgs->integer && mvd.dummy) + flags |= MVF_NOMSGS; // send the serverdata - MSG_WriteByte(mvd_serverdata | extra); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - if (svs.csr.extended) + if (svs.csr.extended) { + flags |= MVF_EXTLIMITS; + if (IS_NEW_GAME_API) + flags |= MVF_EXTLIMITS_2; + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_CURRENT); - else + MSG_WriteShort(flags); + } else { + MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_DEFAULT); + } MSG_WriteLong(sv.spawncount); MSG_WriteString(fs_game->string); if (mvd.dummy) @@ -744,7 +763,10 @@ static void emit_frame(void) } // quantize - MSG_PackPlayer(&newps, &ent->client->ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&newps, ent->client); + else + MSG_PackPlayerOld(&newps, ent->client); if (PPS_INUSE(oldps)) { // delta update from old position @@ -2125,12 +2147,19 @@ void SV_MvdPostInit(void) if (sv_mvd_noblend->integer) { mvd.psFlags |= MSG_PS_IGNORE_BLEND; } + if (sv_mvd_nogun->integer) { mvd.psFlags |= MSG_PS_IGNORE_GUNINDEX | MSG_PS_IGNORE_GUNFRAMES; } + if (svs.csr.extended) { mvd.esFlags |= MSG_ES_LONGSOLID | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; mvd.psFlags |= MSG_PS_EXTENSIONS; + + if (IS_NEW_GAME_API) { + mvd.esFlags |= MSG_ES_EXTENSIONS_2; + mvd.psFlags |= MSG_PS_EXTENSIONS_2; + } } } diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index a3ffdd686..e876bd2d5 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1860,7 +1860,7 @@ static void emit_base_frame(mvd_t *mvd) // send base player states for (i = 0; i < mvd->maxclients; i++) { player = &mvd->players[i]; - MSG_PackPlayer(&ps, &player->ps); + MSG_PackPlayerNew(&ps, &player->ps); MSG_WriteDeltaPlayerstate_Packet(NULL, &ps, i, player_flags(mvd, player)); } MSG_WriteByte(CLIENTNUM_NONE); @@ -1879,17 +1879,21 @@ static void emit_base_frame(mvd_t *mvd) static void emit_gamestate(mvd_t *mvd) { - int i, extra; + int i; char *s; size_t len; - // pack MVD stream flags into extra bits - extra = mvd->flags << SVCMD_BITS; - // send the serverdata - MSG_WriteByte(mvd_serverdata | extra); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - MSG_WriteLong(mvd->version); + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2) { + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); + MSG_WriteLong(mvd->version); + MSG_WriteShort(mvd->flags); + } else { + MSG_WriteByte(mvd_serverdata | (mvd->flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); + MSG_WriteLong(mvd->version); + } MSG_WriteLong(mvd->servercount); MSG_WriteString(mvd->gamedir); MSG_WriteShort(mvd->clientNum); diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 25c730f74..92bbcb4c3 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -681,6 +681,8 @@ static void MVD_UpdateClient(mvd_client_t *client) Vector4Set(client->ps.blend, 0.5f, 0.3f, 0.2f, 0.4f); else Vector4Clear(client->ps.blend); + + Vector4Clear(client->ps.damage_blend); } else { // copy entire player state client->ps = target->ps; @@ -695,7 +697,7 @@ static void MVD_UpdateClient(mvd_client_t *client) if (target != mvd->dummy) { if (mvd_stats_hack->integer && mvd->dummy) { // copy stats of the dummy MVD observer - for (i = 0; i < MAX_STATS; i++) { + for (i = 0; i < MAX_STATS_OLD; i++) { if (mvd_stats_hack->integer & BIT(i)) { client->ps.stats[i] = mvd->dummy->ps.stats[i]; } @@ -772,12 +774,16 @@ void MVD_BroadcastPrintf(mvd_t *mvd, int level, int mask, const char *fmt, ...) SZ_Clear(&msg_write); } +#define ES_MASK (MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS | MSG_ES_EXTENSIONS_2) +#define PS_MASK (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) + static void MVD_SetServerState(client_t *cl, mvd_t *mvd) { if (cl->csr != mvd->csr) { Z_Freep(&cl->entities); cl->num_entities = 0; } + cl->gamedir = mvd->gamedir; cl->mapname = mvd->mapname; cl->configstrings = mvd->configstrings; @@ -787,10 +793,11 @@ static void MVD_SetServerState(client_t *cl, mvd_t *mvd) cl->ge = &mvd->ge; cl->spawncount = mvd->servercount; cl->maxclients = mvd->maxclients; - if (cl->csr->extended) - cl->esFlags |= MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; - else - cl->esFlags &= ~(MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS); + + cl->esFlags &= ~ES_MASK; + cl->psFlags &= ~PS_MASK; + cl->esFlags |= mvd->esFlags & ES_MASK; + cl->psFlags |= mvd->psFlags & PS_MASK; } void MVD_SwitchChannel(mvd_client_t *client, mvd_t *mvd) @@ -845,6 +852,21 @@ static bool MVD_PartFilter(mvd_client_t *client) return delta < treshold; } +static bool MVD_ClientCompatible(client_t *cl, mvd_t *mvd) +{ + int minimal; + + if (!(mvd->flags & (MVF_EXTLIMITS | MVF_EXTLIMITS_2))) + return true; + if (cl->protocol != PROTOCOL_VERSION_Q2PRO) + return false; + + minimal = (mvd->flags & MVF_EXTLIMITS_2) ? + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 : + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS; + return cl->version >= minimal; +} + static void MVD_TrySwitchChannel(mvd_client_t *client, mvd_t *mvd) { if (mvd == client->mvd) { @@ -853,7 +875,7 @@ static void MVD_TrySwitchChannel(mvd_client_t *client, mvd_t *mvd) "in the Waiting Room" : "on this channel"); return; // nothing to do } - if (!CLIENT_COMPATIBLE(mvd->csr, client->cl)) { + if (!MVD_ClientCompatible(client->cl, mvd)) { SV_ClientPrintf(client->cl, PRINT_HIGH, "[MVD] This channel is not compatible with your client version.\n"); return; @@ -1770,7 +1792,7 @@ static void MVD_GameInit(void) for (i = 0; i < sv_maxclients->integer; i++) { mvd_clients[i].cl = &svs.client_pool[i]; - edicts[i + 1].client = (gclient_t *)&mvd_clients[i]; + edicts[i + 1].client = &mvd_clients[i]; } mvd_ge.edicts = edicts; @@ -1864,7 +1886,7 @@ static qboolean MVD_GameClientConnect(edict_t *ent, char *userinfo) if (LIST_SINGLE(&mvd_channel_list)) { mvd = LIST_FIRST(mvd_t, &mvd_channel_list, entry); } - if (!mvd || !CLIENT_COMPATIBLE(mvd->csr, client->cl)) { + if (!mvd || !MVD_ClientCompatible(client->cl, mvd)) { mvd = &mvd_waitingRoom; } List_SeqAdd(&mvd->clients, &client->entry); diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index b3153c171..bc6ac2662 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -488,7 +488,7 @@ static void MVD_ParseSound(mvd_t *mvd, int extrabits) MSG_WriteByte(offset); MSG_WriteShort(sendchan); - MSG_WritePos(origin); + MSG_WritePos(origin, mvd->esFlags & MSG_ES_EXTENSIONS_2); leaf1 = NULL; if (!(extrabits & 1)) { @@ -894,12 +894,18 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) "Current version is %d.\n", mvd->version, PROTOCOL_VERSION_MVD_CURRENT); } + // parse MVD stream flags + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2) { + mvd->flags = MSG_ReadWord(); + } else { + mvd->flags = extrabits; + } + mvd->servercount = MSG_ReadLong(); if (MSG_ReadString(mvd->gamedir, sizeof(mvd->gamedir)) >= sizeof(mvd->gamedir)) { MVD_Destroyf(mvd, "Oversize gamedir string"); } mvd->clientNum = MSG_ReadShort(); - mvd->flags = extrabits; mvd->esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN; mvd->psFlags = 0; mvd->csr = &cs_remap_old; @@ -909,6 +915,13 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) mvd->psFlags |= MSG_PS_EXTENSIONS; mvd->csr = &cs_remap_new; } + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2 && mvd->flags & MVF_EXTLIMITS_2) { + mvd->esFlags |= MSG_ES_EXTENSIONS_2; + mvd->psFlags |= MSG_PS_EXTENSIONS_2; + if (!(mvd->flags & MVF_EXTLIMITS)) { + MVD_Destroyf(mvd, "MVF_EXTLIMITS_2 without MVF_EXTLIMITS"); + } + } #if 0 // change gamedir unless playing a demo diff --git a/src/server/save.c b/src/server/save.c index d87c6baba..d7757d063 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -662,7 +662,7 @@ static void SV_Savegame_f(void) if (!gex->CanSave()) return; } else { - if (sv_maxclients->integer == 1 && svs.client_pool[0].edict->client->ps.stats[STAT_HEALTH] <= 0) { + if (sv_maxclients->integer == 1 && SV_GetClient_Stat(&svs.client_pool[0], STAT_HEALTH) <= 0) { Com_Printf("Can't savegame while dead!\n"); return; } diff --git a/src/server/send.c b/src/server/send.c index 64e50441e..191289047 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -556,11 +556,8 @@ static void emit_snd(const client_t *client, const message_packet_t *msg) MSG_WriteShort(msg->sendchan); - if (flags & SND_POS) { - for (int i = 0; i < 3; i++) { - MSG_WriteShort(msg->pos[i]); - } - } + if (flags & SND_POS) + MSG_WriteIntPos(msg->pos, client->esFlags & MSG_ES_EXTENSIONS_2); } static inline void write_snd(client_t *client, message_packet_t *msg, unsigned maxsize) diff --git a/src/server/server.h b/src/server/server.h index 23f59b7a9..25c858122 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -92,6 +92,10 @@ with this program; if not, write to the Free Software Foundation, Inc., GMF_IPV6_ADDRESS_AWARE | GMF_ALLOW_INDEX_OVERFLOW | \ GMF_PROTOCOL_EXTENSIONS) +// flag indicating if game uses gclient_new_t or gclient_old_t. +// doesn't enable protocol extensions by itself. +#define IS_NEW_GAME_API (ge->apiversion == GAME_API_VERSION_NEW) + // ugly hack for SV_Shutdown #define MVD_SPAWN_DISABLED 0 #define MVD_SPAWN_ENABLED BIT(30) @@ -172,10 +176,6 @@ typedef struct { #define MAX_TOTAL_ENT_LEAFS 128 -#define CLIENT_COMPATIBLE(csr, c) \ - (!(csr)->extended || ((c)->protocol == PROTOCOL_VERSION_Q2PRO && \ - (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS)) - #define ENT_EXTENSION(csr, ent) ((csr)->extended ? &(ent)->x : NULL) typedef enum { @@ -229,7 +229,7 @@ typedef struct { uint8_t volume; uint8_t attenuation; uint8_t timeofs; - int16_t pos[3]; // saved in case entity is freed + int32_t pos[3]; // saved in case entity is freed }; }; } message_packet_t; @@ -332,6 +332,7 @@ typedef struct client_s { pmoveParams_t pmp; // spectator speed, etc msgEsFlags_t esFlags; // entity protocol flags + msgPsFlags_t psFlags; // packetized messages list_t msg_free_list; @@ -478,6 +479,7 @@ typedef struct { #endif cs_remap_t csr; + pmoveParams_t pmp; unsigned last_heartbeat; unsigned last_timescale_check; @@ -508,8 +510,6 @@ extern list_t sv_clientlist; // linked list of non-free clients extern server_static_t svs; // persistant server info extern server_t sv; // local server -extern pmoveParams_t sv_pmp; - extern cvar_t *sv_hostname; extern cvar_t *sv_maxclients; extern cvar_t *sv_password; @@ -772,7 +772,7 @@ extern const game_export_ex_t *gex; void SV_InitGameProgs(void); void SV_ShutdownGameProgs(void); -void PF_Pmove(pmove_t *pm); +void PF_Pmove(void *pm); // // sv_save.c @@ -791,6 +791,45 @@ void SV_RegisterSavegames(void); #define SV_RegisterSavegames() (void)0 #endif +// +// ugly gclient_(old|new)_t accessors +// + +static inline void SV_GetClient_ViewOrg(const client_t *client, vec3_t org) +{ + if (IS_NEW_GAME_API) { + const gclient_new_t *cl = client->edict->client; + VectorMA(cl->ps.viewoffset, 0.125f, cl->ps.pmove.origin, org); + } else { + const gclient_old_t *cl = client->edict->client; + VectorMA(cl->ps.viewoffset, 0.125f, cl->ps.pmove.origin, org); + } +} + +static inline int SV_GetClient_ClientNum(const client_t *client) +{ + if (IS_NEW_GAME_API) + return ((const gclient_new_t *)client->edict->client)->clientNum; + else + return ((const gclient_old_t *)client->edict->client)->clientNum; +} + +static inline int SV_GetClient_Stat(const client_t *client, int stat) +{ + if (IS_NEW_GAME_API) + return ((const gclient_new_t *)client->edict->client)->ps.stats[stat]; + else + return ((const gclient_old_t *)client->edict->client)->ps.stats[stat]; +} + +static inline void SV_SetClient_Ping(const client_t *client, int ping) +{ + if (IS_NEW_GAME_API) + ((gclient_new_t *)client->edict->client)->ping = ping; + else + ((gclient_old_t *)client->edict->client)->ping = ping; +} + //============================================================ // diff --git a/src/server/user.c b/src/server/user.c index ca58f6609..e33c35bef 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -341,6 +341,9 @@ static int q2pro_protocol_flags(void) if (sv_client->csr->extended) flags |= Q2PRO_PF_EXTENSIONS; + if (sv_client->esFlags & MSG_ES_EXTENSIONS_2) + flags |= Q2PRO_PF_EXTENSIONS_2; + return flags; } From 9179c2d5616484774786a5a2a22397fe2d7b6479 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 9 Jul 2024 17:45:16 +0300 Subject: [PATCH 341/974] Support drawing damage vignette. From: Jonathan --- inc/refresh/refresh.h | 3 +- src/client/entities.c | 3 +- src/client/view.c | 16 +++--- src/refresh/draw.c | 124 +++++++++++++++++++++++++++++++++++++++--- src/refresh/gl.h | 1 + src/refresh/main.c | 4 +- 6 files changed, 133 insertions(+), 18 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 560b06c2c..0309b8741 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -106,7 +106,8 @@ typedef struct { float fov_x, fov_y; vec3_t vieworg; vec3_t viewangles; - vec4_t blend; // rgba 0-1 full screen blend + vec4_t screen_blend; // rgba 0-1 full screen blend + vec4_t damage_blend; // rgba 0-1 damage blend float time; // time is uesed to auto animate int rdflags; // RDF_UNDERWATER, etc diff --git a/src/client/entities.c b/src/client/entities.c index ebcb2b1dc..9c9d15ff3 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1358,7 +1358,8 @@ void CL_CalcViewValues(void) } // don't interpolate blend color - Vector4Copy(ps->blend, cl.refdef.blend); + Vector4Copy(ps->blend, cl.refdef.screen_blend); + Vector4Copy(ps->damage_blend, cl.refdef.damage_blend); #if USE_FPS ps = &cl.keyframe.ps; diff --git a/src/client/view.c b/src/client/view.c index 0caad6b31..d57a7bd09 100644 --- a/src/client/view.c +++ b/src/client/view.c @@ -376,12 +376,10 @@ void V_RenderView(void) V_TestEntities(); if (cl_testlights->integer) V_TestLights(); - if (cl_testblend->integer) { - cl.refdef.blend[0] = 1; - cl.refdef.blend[1] = 0.5f; - cl.refdef.blend[2] = 0.25f; - cl.refdef.blend[3] = 0.5f; - } + if (cl_testblend->integer & 1) + Vector4Set(cl.refdef.screen_blend, 1, 0.5f, 0.25f, 0.5f); + if (cl_testblend->integer & 2) + Vector4Set(cl.refdef.damage_blend, 0.25f, 0.5f, 0.7f, 0.5f); #endif // never let it sit exactly on a node line, because a water plane can @@ -419,8 +417,10 @@ void V_RenderView(void) r_numparticles = 0; if (!cl_add_lights->integer) r_numdlights = 0; - if (!cl_add_blend->integer) - Vector4Clear(cl.refdef.blend); + if (!cl_add_blend->integer) { + Vector4Clear(cl.refdef.screen_blend); + Vector4Clear(cl.refdef.damage_blend); + } cl.refdef.num_entities = r_numentities; cl.refdef.entities = r_entities; diff --git a/src/refresh/draw.c b/src/refresh/draw.c index e2d98c22f..396539387 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -76,17 +76,127 @@ static inline void GL_StretchPic_( #define GL_StretchPic(x,y,w,h,s1,t1,s2,t2,color,image) \ GL_StretchPic_(x,y,w,h,s1,t1,s2,t2,color,(image)->texnum,(image)->flags) +static void GL_DrawVignette(int distance, color_t outer, color_t inner) +{ + vec_t *dst_vert; + uint32_t *dst_color; + QGL_INDEX_TYPE *dst_indices; + + if (tess.numverts + 8 > TESS_MAX_VERTICES || + tess.numindices + 24 > TESS_MAX_INDICES || + (tess.numverts && tess.texnum[0] != TEXNUM_WHITE)) { + GL_Flush2D(); + } + + tess.texnum[0] = TEXNUM_WHITE; + + int x = 0, y = 0; + int w = glr.fd.width, h = glr.fd.height; + + // outer vertices + dst_vert = tess.vertices + tess.numverts * 4; + Vector4Set(dst_vert, x, y, 0, 0); + Vector4Set(dst_vert + 4, x + w, y, 0, 0); + Vector4Set(dst_vert + 8, x + w, y + h, 0, 0); + Vector4Set(dst_vert + 12, x, y + h, 0, 0); + + dst_color = (uint32_t *)tess.colors + tess.numverts; + dst_color[0] = outer.u32; + dst_color[1] = outer.u32; + dst_color[2] = outer.u32; + dst_color[3] = outer.u32; + + // inner vertices + x += distance; + y += distance; + w -= distance * 2; + h -= distance * 2; + + dst_vert += 16; + Vector4Set(dst_vert, x, y, 0, 0); + Vector4Set(dst_vert + 4, x + w, y, 0, 0); + Vector4Set(dst_vert + 8, x + w, y + h, 0, 0); + Vector4Set(dst_vert + 12, x, y + h, 0, 0); + + dst_color += 4; + dst_color[0] = inner.u32; + dst_color[1] = inner.u32; + dst_color[2] = inner.u32; + dst_color[3] = inner.u32; + + /* + 0 1 + 4 5 + + 7 6 + 3 2 + */ + + dst_indices = tess.indices + tess.numindices; + dst_indices[0] = tess.numverts + 0; + dst_indices[1] = tess.numverts + 5; + dst_indices[2] = tess.numverts + 4; + dst_indices[3] = tess.numverts + 0; + dst_indices[4] = tess.numverts + 1; + dst_indices[5] = tess.numverts + 5; + + dst_indices[6] = tess.numverts + 1; + dst_indices[7] = tess.numverts + 6; + dst_indices[8] = tess.numverts + 5; + dst_indices[9] = tess.numverts + 1; + dst_indices[10] = tess.numverts + 2; + dst_indices[11] = tess.numverts + 6; + + dst_indices[12] = tess.numverts + 6; + dst_indices[13] = tess.numverts + 2; + dst_indices[14] = tess.numverts + 3; + dst_indices[15] = tess.numverts + 6; + dst_indices[16] = tess.numverts + 3; + dst_indices[17] = tess.numverts + 7; + + dst_indices[18] = tess.numverts + 0; + dst_indices[19] = tess.numverts + 7; + dst_indices[20] = tess.numverts + 3; + dst_indices[21] = tess.numverts + 0; + dst_indices[22] = tess.numverts + 4; + dst_indices[23] = tess.numverts + 7; + + tess.flags |= 2; + + tess.numverts += 8; + tess.numindices += 24; +} + void GL_Blend(void) { - color_t color; + if (glr.fd.screen_blend[3]) { + color_t color; - color.u8[0] = glr.fd.blend[0] * 255; - color.u8[1] = glr.fd.blend[1] * 255; - color.u8[2] = glr.fd.blend[2] * 255; - color.u8[3] = glr.fd.blend[3] * 255; + color.u8[0] = glr.fd.screen_blend[0] * 255; + color.u8[1] = glr.fd.screen_blend[1] * 255; + color.u8[2] = glr.fd.screen_blend[2] * 255; + color.u8[3] = glr.fd.screen_blend[3] * 255; - GL_StretchPic_(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, - color.u32, TEXNUM_WHITE, 0); + GL_StretchPic_(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, + color.u32, TEXNUM_WHITE, 0); + } + + if (glr.fd.damage_blend[3]) { + color_t outer, inner; + + outer.u8[0] = glr.fd.damage_blend[0] * 255; + outer.u8[1] = glr.fd.damage_blend[1] * 255; + outer.u8[2] = glr.fd.damage_blend[2] * 255; + outer.u8[3] = glr.fd.damage_blend[3] * 255; + + inner.u32 = outer.u32 & U32_RGB; + + if (gl_damageblend_frac->value > 0) + GL_DrawVignette(min(glr.fd.width, glr.fd.height) * gl_damageblend_frac->value, outer, inner); + else + GL_StretchPic_(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, + outer.u32, TEXNUM_WHITE, 0); + } } void R_ClearColor(void) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 92e6177f8..e8c89136d 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -213,6 +213,7 @@ extern cvar_t *gl_shaders; extern cvar_t *gl_md5_load; extern cvar_t *gl_md5_use; #endif +extern cvar_t *gl_damageblend_frac; // development variables extern cvar_t *gl_znear; diff --git a/src/refresh/main.c b/src/refresh/main.c index dd4ed923c..4e253862b 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -55,6 +55,7 @@ cvar_t *gl_shaders; cvar_t *gl_md5_load; cvar_t *gl_md5_use; #endif +cvar_t *gl_damageblend_frac; cvar_t *gl_waterwarp; cvar_t *gl_swapinterval; @@ -739,7 +740,7 @@ void R_RenderFrame(const refdef_t *fd) GL_WaterWarp(); } - if (gl_polyblend->integer && glr.fd.blend[3] != 0) { + if (gl_polyblend->integer) { GL_Blend(); } @@ -917,6 +918,7 @@ static void GL_Register(void) gl_md5_load = Cvar_Get("gl_md5_load", "1", CVAR_FILES); gl_md5_use = Cvar_Get("gl_md5_use", "1", 0); #endif + gl_damageblend_frac = Cvar_Get("gl_damageblend_frac", "0.2", 0); gl_waterwarp = Cvar_Get("gl_waterwarp", "0", 0); gl_swapinterval = Cvar_Get("gl_swapinterval", "1", CVAR_ARCHIVE); gl_swapinterval->changed = gl_swapinterval_changed; From cb4f6f8daa19dd8413ff2f0da7b79f4097e9e86e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 14 Jul 2024 20:00:26 +0300 Subject: [PATCH 342/974] =?UTF-8?q?Fix=20broken=20viewangles=20with=20?= =?UTF-8?q?=E2=80=98spectator=201=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Broken by commit 65061f5b. --- src/game/p_client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/p_client.c b/src/game/p_client.c index a36f9db2d..b55a4e95c 100644 --- a/src/game/p_client.c +++ b/src/game/p_client.c @@ -1227,7 +1227,7 @@ void ClientBeginDeathmatch(edict_t *ent) if (level.intermission_framenum) { MoveClientToIntermission(ent); - } else { + } else if (!ent->client->pers.spectator) { // send effect gi.WriteByte(svc_muzzleflash); gi.WriteShort(ent - g_edicts); From 9c5067567cb5da42303cae550ed145551a1fc3f0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 17 Jul 2024 15:42:27 +0300 Subject: [PATCH 343/974] Smooth flares visibility transitions. --- src/refresh/gl.h | 15 +++++++++++++++ src/refresh/main.c | 15 ++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index e8c89136d..ea0f80786 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -79,6 +79,7 @@ typedef struct { typedef struct { GLuint query; + float frac; bool pending; bool visible; } glquery_t; @@ -121,6 +122,8 @@ typedef struct { unsigned drawframe; unsigned dlightframe; unsigned rand_seed; + unsigned timestamp; + float frametime; int viewcluster1; int viewcluster2; cplane_t frustumPlanes[4]; @@ -254,6 +257,18 @@ void GL_RotateForEntity(void); void GL_ClearErrors(void); bool GL_ShowErrors(const char *func); +static inline void GL_AdvanceValue(float *restrict val, float target, float speed) +{ + if (*val < target) { + *val += speed * glr.frametime; + if (*val > target) + *val = target; + } else if (*val > target) { + *val -= speed * glr.frametime; + if (*val < target) + *val = target; + } +} /* * gl_model.c diff --git a/src/refresh/main.c b/src/refresh/main.c index 4e253862b..4d1d18392 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -427,8 +427,8 @@ static void GL_OccludeFlares(void) if (!glr.num_flares) return; - if (!gl_static.queries) - return; + + Q_assert(gl_static.queries); GL_LoadMatrix(glr.viewmatrix); GL_StateBits(GLS_DEPTHMASK_FALSE); @@ -494,7 +494,8 @@ static void GL_DrawFlare(const entity_t *e) if (!q->pending) glr.num_flares++; - if (!q->visible) + GL_AdvanceValue(&q->frac, q->visible, 8); + if (!q->frac) return; GL_LoadMatrix(glr.viewmatrix); @@ -504,9 +505,9 @@ static void GL_DrawFlare(const entity_t *e) GL_Color(e->rgba.u8[0] / 255.0f, e->rgba.u8[1] / 255.0f, e->rgba.u8[2] / 255.0f, - e->alpha * 0.5f); + e->alpha * 0.5f * q->frac); - make_flare_quad(e, 25.0f, points); + make_flare_quad(e, 25.0f * q->frac, points); GL_TexCoordPointer(2, 0, quad_tc); GL_VertexPointer(3, 0, &points[0][0]); @@ -672,6 +673,9 @@ void R_RenderFrame(const refdef_t *fd) Q_assert(gl_static.world.cache || (fd->rdflags & RDF_NOWORLDMODEL)); + glr.frametime = (com_eventTime - glr.timestamp) * 0.001f; + glr.timestamp = com_eventTime; + glr.drawframe++; glr.rand_seed = fd->time * 20; @@ -1070,6 +1074,7 @@ static void GL_ClearQueries(void) for (int i = 0; i < map_size; i++) { glquery_t *q = HashMap_GetValue(glquery_t, gl_static.queries, i); q->pending = q->visible = false; + q->frac = 0; } } From e102a5f599f6335859d68818ff3bcf3c6c0696b5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 17 Jul 2024 20:28:15 +0300 Subject: [PATCH 344/974] Print offsets of entities in client frame. --- src/client/parse.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/client/parse.c b/src/client/parse.c index bd2fe4b06..7821e1378 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -98,6 +98,9 @@ static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_ } while (1) { +#if USE_DEBUG + uint32_t readcount = msg_read.readcount; +#endif newnum = MSG_ParseEntityBits(&bits, cl.esFlags); if (newnum < 0 || newnum >= cl.csr.max_edicts) { Com_Error(ERR_DROP, "%s: bad number: %d", __func__, newnum); @@ -109,7 +112,7 @@ static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_ while (oldnum < newnum) { // one or more entities from the old packet are unchanged - SHOWNET(3, " unchanged: %i\n", oldnum); + SHOWNET(3, " unchanged:%i\n", oldnum); CL_ParseDeltaEntity(frame, oldnum, oldstate, 0); oldindex++; @@ -125,7 +128,7 @@ static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_ if (bits & U_REMOVE) { // the entity present in oldframe is not in the current frame - SHOWNET(2, " remove: %i\n", newnum); + SHOWNET(2, "%3u:remove:%i\n", readcount, newnum); if (oldnum != newnum) { Com_DPrintf("U_REMOVE: oldnum != newnum\n"); } @@ -147,7 +150,7 @@ static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_ if (oldnum == newnum) { // delta from previous state - SHOWNET(2, " delta: %i ", newnum); + SHOWNET(2, "%3u:delta:%i ", readcount, newnum); CL_ParseDeltaEntity(frame, newnum, oldstate, bits); if (!bits) { SHOWNET(2, "\n"); @@ -167,20 +170,19 @@ static void CL_ParsePacketEntities(const server_frame_t *oldframe, server_frame_ if (oldnum > newnum) { // delta from baseline - SHOWNET(2, " baseline: %i ", newnum); + SHOWNET(2, "%3u:baseline:%i ", readcount, newnum); CL_ParseDeltaEntity(frame, newnum, &cl.baselines[newnum], bits); if (!bits) { SHOWNET(2, "\n"); } continue; } - } // any remaining entities in the old frame are copied over while (oldnum != MAX_EDICTS) { // one or more entities from the old packet are unchanged - SHOWNET(3, " unchanged: %i\n", oldnum); + SHOWNET(3, " unchanged:%i\n", oldnum); CL_ParseDeltaEntity(frame, oldnum, oldstate, 0); oldindex++; @@ -308,14 +310,14 @@ static void CL_ParseFrame(int extrabits) frame.areabytes = 0; } + SHOWNET(2, "%3u:playerinfo\n", msg_read.readcount); + if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT) { if (MSG_ReadByte() != svc_playerinfo) { Com_Error(ERR_DROP, "%s: not playerinfo", __func__); } } - SHOWNET(2, "%3u:playerinfo\n", msg_read.readcount - 1); - // parse playerstate bits = MSG_ReadWord(); if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { @@ -356,6 +358,8 @@ static void CL_ParseFrame(int extrabits) frame.clientNum = cl.clientNum; } + SHOWNET(2, "%3u:packetentities\n", msg_read.readcount); + // parse packetentities if (cls.serverProtocol <= PROTOCOL_VERSION_DEFAULT) { if (MSG_ReadByte() != svc_packetentities) { @@ -363,8 +367,6 @@ static void CL_ParseFrame(int extrabits) } } - SHOWNET(2, "%3u:packetentities\n", msg_read.readcount - 1); - CL_ParsePacketEntities(oldframe, &frame); // save the frame off in the backup array for later delta comparisons @@ -375,7 +377,7 @@ static void CL_ParseFrame(int extrabits) int seq = cls.netchan.incoming_acknowledged & CMD_MASK; int rtt = cls.demo.playback ? 0 : cls.realtime - cl.history[seq].sent; Com_LPrintf(PRINT_DEVELOPER, "%3u:frame:%d delta:%d rtt:%d\n", - msg_read.readcount - 1, frame.number, frame.delta, rtt); + msg_read.readcount, frame.number, frame.delta, rtt); } #endif @@ -463,7 +465,7 @@ static void CL_ParseBaseline(int index, uint64_t bits) #if USE_DEBUG if (cl_shownet->integer > 2) { - Com_LPrintf(PRINT_DEVELOPER, " baseline: %i ", index); + Com_LPrintf(PRINT_DEVELOPER, " baseline:%i ", index); MSG_ShowDeltaEntityBits(bits); Com_LPrintf(PRINT_DEVELOPER, "\n"); } From dd60913ac4815748e9c2b6db579009a4e7af3f99 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 17 Jul 2024 20:38:19 +0300 Subject: [PATCH 345/974] Clean up setting USE_NEW_GAME_API define. --- inc/shared/shared.h | 8 ++++++++ meson.build | 17 ++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 3dde2d8fb..c0a49e004 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -26,6 +26,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "config.h" #endif +#ifndef USE_PROTOCOL_EXTENSIONS +#define USE_PROTOCOL_EXTENSIONS (USE_CLIENT || USE_SERVER) +#endif + +#ifndef USE_NEW_GAME_API +#define USE_NEW_GAME_API (USE_CLIENT || USE_SERVER) +#endif + #include #include #include diff --git a/meson.build b/meson.build index 5517108b0..63ee3c046 100644 --- a/meson.build +++ b/meson.build @@ -203,16 +203,11 @@ else common_args += '-D_GNU_SOURCE' endif -engine_args = ['-DUSE_NEW_GAME_API=1'] +engine_args = [] if win32 engine_args += '-D_WIN32_WINNT=0x0601' endif -game_args = [] -if get_option('game-new-api') - game_args += '-DUSE_NEW_GAME_API=1' -endif - common_link_args = [] exe_link_args = [] dll_link_args = [] @@ -453,7 +448,6 @@ shared_library('game' + cpu, game_src, include_directories: 'inc', gnu_symbol_visibility: 'hidden', link_args: dll_link_args, - c_args: game_args, install: system_wide, install_dir: libdir / get_option('base-game'), override_options: get_option('game-build-options'), @@ -483,8 +477,16 @@ config.set10('USE_MD5', get_option('md5')) config.set10('USE_PACKETDUP', get_option('packetdup-hack')) config.set10('USE_TGA', get_option('tga')) config.set10('USE_' + host_machine.endian().to_upper() + '_ENDIAN', true) + +# protocol extensions are always on config.set10('USE_PROTOCOL_EXTENSIONS', true) +# new game API flag is *always on* for engine, +# and can be enabled here for game as well +if get_option('game-new-api') + config.set10('USE_NEW_GAME_API', true) +endif + configure_file(output: 'config.h', configuration: config) if system_wide @@ -499,6 +501,7 @@ summary({ 'client-ui' : config.get('USE_UI', 0) != 0, 'debug' : config.get('USE_DEBUG', 0) != 0, 'game-abi-hack' : config.get('USE_GAME_ABI_HACK', 0) != 0, + 'game-new-api' : config.get('USE_NEW_GAME_API', 0) != 0, 'icmp-errors' : config.get('USE_ICMP', 0) != 0, 'libcurl' : config.get('USE_CURL', 0) != 0, 'libjpeg' : config.get('USE_JPG', 0) != 0, From 1201e4951ccf27488f0693dcad50d1d07bb778fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 17 Jul 2024 20:46:18 +0300 Subject: [PATCH 346/974] Expose more cvar_t members if building for new API. --- inc/shared/shared.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index c0a49e004..1cd3b127c 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -737,9 +737,11 @@ typedef struct cvar_s { struct cvar_s *next; // ------ new stuff ------ -#if USE_CLIENT || USE_SERVER +#if USE_NEW_GAME_API int integer; char *default_string; +#endif +#if USE_CLIENT || USE_SERVER xchanged_t changed; xgenerator_t generator; struct cvar_s *hashNext; From 6f02a9867009618c7b2484ad64a2dad0f016dbd5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 18 Jul 2024 13:50:57 +0300 Subject: [PATCH 347/974] Print offsets of players/entities in MVD frame. --- src/server/mvd/parse.c | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index bc6ac2662..289f3c001 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -650,6 +650,9 @@ static void MVD_ParsePacketEntities(mvd_t *mvd) edict_t *ent; while (1) { +#if USE_DEBUG + uint32_t readcount = msg_read.readcount; +#endif if (msg_read.readcount > msg_read.cursize) { MVD_Destroyf(mvd, "%s: read past end of message", __func__); } @@ -667,10 +670,10 @@ static void MVD_ParsePacketEntities(mvd_t *mvd) #if USE_DEBUG if (mvd_shownet->integer > 2) { - Com_Printf(" %s: %d ", ent->inuse ? - "delta" : "baseline", number); + Com_LPrintf(PRINT_DEVELOPER, "%3u:%s:%d ", readcount, + ent->inuse ? "delta" : "baseline", number); MSG_ShowDeltaEntityBits(bits); - Com_Printf("\n"); + Com_LPrintf(PRINT_DEVELOPER, "\n"); } #endif @@ -686,7 +689,7 @@ static void MVD_ParsePacketEntities(mvd_t *mvd) // shuffle current origin to old if removed if (bits & U_REMOVE) { - SHOWNET(2, " remove: %d\n", number); + SHOWNET(2, "%3u:remove:%d\n", readcount, number); if (!(ent->s.renderfx & RF_BEAM)) { VectorCopy(ent->s.origin, ent->s.old_origin); } @@ -713,6 +716,9 @@ static void MVD_ParsePacketPlayers(mvd_t *mvd) mvd_player_t *player; while (1) { +#if USE_DEBUG + uint32_t readcount = msg_read.readcount; +#endif if (msg_read.readcount > msg_read.cursize) { MVD_Destroyf(mvd, "%s: read past end of message", __func__); } @@ -732,17 +738,17 @@ static void MVD_ParsePacketPlayers(mvd_t *mvd) #if USE_DEBUG if (mvd_shownet->integer > 2) { - Com_Printf(" %s: %d ", player->inuse ? - "delta" : "baseline", number); + Com_LPrintf(PRINT_DEVELOPER, "%3u:%s:%d ", readcount, + player->inuse ? "delta" : "baseline", number); MSG_ShowDeltaPlayerstateBits_Packet(bits); - Com_Printf("\n"); + Com_LPrintf(PRINT_DEVELOPER, "\n"); } #endif MSG_ParseDeltaPlayerstate_Packet(&player->ps, &player->ps, bits, mvd->psFlags); if (bits & PPS_REMOVE) { - SHOWNET(2, " remove: %d\n", number); + SHOWNET(2, "%3u:remove:%d\n", readcount, number); player->inuse = false; continue; } From f78d9eb9b171445841777f8d5f4fa80047025dd0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 18 Jul 2024 16:19:09 +0300 Subject: [PATCH 348/974] Fix MVD_LinkEdict() bug with MODELINDEX_PLAYER. --- src/server/mvd/game.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 92bbcb4c3..1fdb46aba 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -1720,7 +1720,9 @@ void MVD_LinkEdict(mvd_t *mvd, edict_t *ent) return; index = ent->s.modelindex - 1; - if (index >= MODELINDEX_PLAYER && bsp->nummodels >= MODELINDEX_PLAYER) + if (index == MODELINDEX_PLAYER - 1) + index = 0; + else if (index >= MODELINDEX_PLAYER) index--; if (index > 0 && index < bsp->nummodels) { mod = &bsp->models[index]; From 916a6f621d69440c76da8f069c10394acd9dca44 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 19 Jul 2024 13:28:27 +0300 Subject: [PATCH 349/974] Encode 32-bit bbox into s.solid in extended mode. --- src/server/entities.c | 2 +- src/server/mvd.c | 4 ---- src/server/user.c | 2 +- src/server/world.c | 2 ++ 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index c67019ade..e4780e192 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -750,7 +750,7 @@ void SV_BuildClientFrame(client_t *client) if (ent->owner == clent) { // don't mark players missiles as solid state->solid = 0; - } else if (client->esFlags & MSG_ES_LONGSOLID) { + } else if (client->esFlags & MSG_ES_LONGSOLID && !client->csr->extended) { state->solid = sv.entities[e].solid32; } diff --git a/src/server/mvd.c b/src/server/mvd.c index d61bb75e5..a28dca9ca 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -604,8 +604,6 @@ static void build_gamestate(void) SV_CheckEntityNumber(ent, i); MSG_PackEntity(&mvd.entities[i], &ent->s, ENT_EXTENSION(&svs.csr, ent)); - if (svs.csr.extended) - mvd.entities[i].solid = sv.entities[i].solid32; } } @@ -820,8 +818,6 @@ static void emit_frame(void) // quantize MSG_PackEntity(&newes, &ent->s, ENT_EXTENSION(&svs.csr, ent)); - if (svs.csr.extended) - newes.solid = sv.entities[i].solid32; MSG_WriteDeltaEntity(oldes, &newes, flags); diff --git a/src/server/user.c b/src/server/user.c index e33c35bef..6e3986207 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -93,7 +93,7 @@ static void SV_CreateBaselines(void) base->solid = 0; } else #endif - if (sv_client->esFlags & MSG_ES_LONGSOLID) { + if (sv_client->esFlags & MSG_ES_LONGSOLID && !sv_client->csr->extended) { base->solid = sv.entities[i].solid32; } } diff --git a/src/server/world.c b/src/server/world.c index 573652a73..2c97c9f55 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -292,6 +292,8 @@ void PF_LinkEdict(edict_t *ent) if ((ent->svflags & SVF_DEADMONSTER) || VectorCompare(ent->mins, ent->maxs)) { ent->s.solid = 0; sent->solid32 = 0; + } else if (svs.csr.extended) { + sent->solid32 = ent->s.solid = SV_PackSolid32(ent); } else { ent->s.solid = MSG_PackSolid16(ent->mins, ent->maxs); sent->solid32 = SV_PackSolid32(ent); From f80041b7bcf23f2e695ebf3e67fc180f872145f0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 19 Jul 2024 13:29:46 +0300 Subject: [PATCH 350/974] Fix delta encoding of player entity origins in MVDs. Existing code was all wrong. With relative origin updates this bug has become more visible. --- src/server/mvd.c | 35 ++++++----------------------------- src/server/mvd/parse.c | 2 +- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index a28dca9ca..cd3f10dbc 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -698,33 +698,6 @@ static void emit_gamestate(void) MSG_WriteShort(0); } -static void copy_entity_state(entity_packed_t *dst, const entity_packed_t *src, int flags) -{ - if (!(flags & MSG_ES_FIRSTPERSON)) { - VectorCopy(src->origin, dst->origin); - VectorCopy(src->angles, dst->angles); - VectorCopy(src->old_origin, dst->old_origin); - } - dst->modelindex = src->modelindex; - dst->modelindex2 = src->modelindex2; - dst->modelindex3 = src->modelindex3; - dst->modelindex4 = src->modelindex4; - dst->frame = src->frame; - dst->skinnum = src->skinnum; - dst->effects = src->effects; - dst->renderfx = src->renderfx; - dst->solid = src->solid; - dst->sound = src->sound; - dst->event = 0; - if (svs.csr.extended) { - dst->morefx = src->morefx; - dst->alpha = src->alpha; - dst->scale = src->scale; - dst->loop_volume = src->loop_volume; - dst->loop_attenuation = src->loop_attenuation; - } -} - /* Builds a new delta compressed MVD frame by capturing all entity and player states and calculating portalbits. The same frame is used for all MVD clients, @@ -802,6 +775,7 @@ static void emit_frame(void) // calculate flags flags = mvd.esFlags; + oldps = NULL; // shut up compiler if (i <= sv_maxclients->integer) { oldps = &mvd.players[i - 1]; if (PPS_INUSE(oldps) && oldps->pmove.pm_type == PM_NORMAL) { @@ -822,8 +796,11 @@ static void emit_frame(void) MSG_WriteDeltaEntity(oldes, &newes, flags); // shuffle current state to previous - copy_entity_state(oldes, &newes, flags); - oldes->number = i; + *oldes = newes; + + // fixup origin for next delta + if (flags & MSG_ES_FIRSTPERSON) + VectorCopy(oldps->pmove.origin, oldes->origin); } MSG_WriteShort(0); // end of packetentities diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index 289f3c001..d66f9bff2 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -622,7 +622,7 @@ static void MVD_PlayerToEntityStates(mvd_t *mvd) mvd->numplayers++; if (player->ps.pmove.pm_type != PM_NORMAL) { continue; // can be out of sync, in this case - // server should provide valid data + // server should provide valid data } edict = &mvd->edicts[i]; From 6daaf8c73b172652abf59d6c77376585a8413eb7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 19 Jul 2024 14:44:17 +0300 Subject: [PATCH 351/974] Initialize newps/newes to zero. Better to avoid copying uninitialized data, even if unused. --- src/server/mvd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index cd3f10dbc..b547035ee 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -705,8 +705,8 @@ as well as local recorder. */ static void emit_frame(void) { - player_packed_t *oldps, newps; - entity_packed_t *oldes, newes; + player_packed_t *oldps, newps = { 0 }; + entity_packed_t *oldes, newes = { 0 }; edict_t *ent; int flags, portalbytes; byte portalbits[MAX_MAP_PORTAL_BYTES]; From ec5cc5776b3439ec2de0f03f3a660ad1cc901284 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Jul 2024 10:49:03 -0400 Subject: [PATCH 352/974] Save point -- still not 100% --- src/action/acesrc/acebot.h | 1714 +++++++++++++-------------- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_movement.c | 128 +- src/action/botlib/botlib_nav.c | 66 +- src/action/botlib/botlib_nodes.c | 71 +- src/action/p_client.c | 164 +-- 7 files changed, 1004 insertions(+), 1143 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 368a449eb..e97e2d2f4 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -1,857 +1,857 @@ -/////////////////////////////////////////////////////////////////////// -// -// ACE - Quake II Bot Base Code -// -// Version 1.0 -// -// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved -// -// -// All other files are Copyright(c) Id Software, Inc. -//////////////////////////////////////////////////////////////////////// -/* - * $Header: /LTK2/src/acesrc/acebot.h 9 29/02/00 11:20 Riever $ - * - * $Log: /LTK2/src/acesrc/acebot.h $ - * - * 9 29/02/00 11:20 Riever - * ChooseWeapon changed to qboolean - * - * 8 27/02/00 13:07 Riever - * Changed enums to defines for better compatibility. - * - * 7 24/02/00 3:07 Riever - * BOTUT_Cmd_Say_f proto added - * - * 6 23/02/00 17:24 Riever - * Added support for 'sv shownodes on/off' - * Enabled creation of nodes for ALL doors. (Stage 1 of new method) - * - * 5 21/02/00 23:45 Riever - * Added GT_ goal selection support and ROAM code protos. Altered movement - * trace lengths to be shorter. - * - * 4 21/02/00 15:16 Riever - * Bot now has the ability to roam on dry land. Basic collision functions - * written. Active state skeletal implementation. - * - * 3 20/02/00 20:27 Riever - * Added new members and definitions ready for 2nd generation of bots. - * - * 2 17/02/00 17:53 Riever - * Fixed item list to be in the right order! - * - */ - -/////////////////////////////////////////////////////////////////////// -// -// acebot.h - Main header file for ACEBOT -// -// -/////////////////////////////////////////////////////////////////////// - -#ifndef _ACEBOT_H -#define _ACEBOT_H - -#include "../botlib/botlib.h" -// Bots think at the server framerate to make sure they move smoothly. -#define BOT_FPS (game.framerate) - -// Only 100 allowed for now (probably never be enough edicts for 'em) -#define MAX_BOTS 100 - -// Platform states -#define STATE_TOP 0 -#define STATE_BOTTOM 1 -#define STATE_UP 2 -#define STATE_DOWN 3 - -// Maximum nodes -//#define MAX_NODES 1200 - -// Node types -#define INVALID -1 // Invalid / Terminating node/links -#define NAV_INFINITE 99999 //rekkie -- for navigation: Dijkstra's algorithm -// -typedef enum -{ - NODE_NONE, - NODE_MOVE, // Move (forward, left, right, back) - NODE_JUMPPAD, // For large jumps across any distance - NODE_LADDER, // Ladder - NODE_WATER, // Water - NODE_CROUCH, // Crouching with a reduced hitbox/collision height - NODE_BOXJUMP, // Move jump + half size hitbox (for jumping into windows, on boxes, etc) - NODE_POI, // Point of Interest (POI) specific locations that you wish bots to visit from time to time - NODE_POI_LOOKAT, // When using a Point of Interest (POI), this is the node to look at - - NODE_STEP, // For steps / stairs - NODE_JUMP, // Small jumps (+moveup) - NODE_STAND_DROP, // For dropping down while standing - NODE_CROUCH_DROP, // For dropping down while crouching - NODE_UNSAFE_DROP, // For dropping down while crouching, but causing minimal leg damage - NODE_LADDER_UP, // Ladder going up - NODE_LADDER_DOWN, // Ladder going down - NODE_DOOR, // Door node - NODE_PLATFORM, // Moving platforms - NODE_TELEPORTER, // Teleporters - NODE_ITEM, // Items - NODE_GRAPPLE, // CTF grapple hook - NODE_SPAWNPOINT, // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes - NODE_LEARN, // Special node - Human learning - NODE_CTF_FLAG, // CTF Flag - NODE_ALL = 99 // For selecting all nodes -} nodetype_t; -/* -#define NODE_MOVE (1) // Move (forward, left, right, back) -#define NODE_CROUCH (NODE_MOVE + 1) // Crouching (-moveup) -#define NODE_STEP (NODE_CROUCH + 1) // For steps / stairs -#define NODE_JUMP (NODE_STEP + 1) // Small jumps (+moveup) -#define NODE_JUMPPAD (NODE_JUMP + 1) // For large jumps across any distance -#define NODE_STAND_DROP (NODE_JUMPPAD + 1) // For dropping down while standing -#define NODE_CROUCH_DROP (NODE_STAND_DROP + 1) // For dropping down while crouching -#define NODE_UNSAFE_DROP (NODE_CROUCH_DROP + 1) // For dropping down while crouching, but causing minimal leg damage -#define NODE_LADDER (NODE_UNSAFE_DROP + 1) // Ladder -#define NODE_LADDER_UP (NODE_LADDER + 1) // Ladder going up -#define NODE_LADDER_DOWN (NODE_LADDER_UP + 1) // Ladder going down -#define NODE_DOOR (NODE_LADDER_DOWN + 1) // Door node -#define NODE_PLATFORM (NODE_DOOR + 1) // Moving platforms -#define NODE_TELEPORTER (NODE_PLATFORM + 1) // Teleporters -#define NODE_ITEM (NODE_TELEPORTER + 1) // Items -#define NODE_WATER (NODE_ITEM + 1) // Water -#define NODE_GRAPPLE (NODE_WATER + 1) // CTF grapple hook -#define NODE_SPAWNPOINT (NODE_GRAPPLE + 1) // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes -#define NODE_POI (NODE_SPAWNPOINT + 1) // Point of Interest (POI) specific locations that you wish bots to visit from time to time -#define NODE_LEARN (NODE_POI + 1) // Special node - Human learning -// -#define NODE_ALL 99 // For selecting all nodes -*/ - -// Density setting for nodes -#define NODE_DENSITY 96 - -// Maximum links per node -//#define MAXLINKS 12 - -//rekkie -- BSP -- s -//#undef MAX_NODES -//#define MAX_NODES 12288 //7168 //6144 -//#undef MAXLINKS -#define MAXLINKS 32 -//rekkie -- BSP -- e - -#define MAX_BOTSKILL 10 -extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! -extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit -extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost -extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight -extern cvar_t *bot_showpath; // Show bot paths - -//AQ2 ADD -extern cvar_t *ltk_skill; // Skill setting for bots, range 0-10 -extern cvar_t *ltk_showpath; // Toggles display of bot paths in debug mode -extern cvar_t *ltk_chat; // Chat setting for bots, off or on (0,1) -extern cvar_t *ltk_routing; // Set to 1 to drop nodes, otherwise you won't do it! -extern cvar_t *ltk_botfile; // Set this to adjust which botdata file to load, default is botdata -extern cvar_t *ltk_loadbots; // Set to 0 to not load bots from ltk_botfile value, 1 for normal operation - -extern int lights_camera_action; - -//AQ2 END - -// Bot state types -#define STATE_STAND 0 -#define STATE_MOVE 1 -#define STATE_ATTACK 2 -#define STATE_WANDER 3 -#define STATE_FLEE 4 -#define STATE_POSITION 5 -#define STATE_COVER 6 -#define STATE_ITEM 7 // Bot is grabbing an item - -// New state definitions -#define BS_WAIT 0 -#define BS_DEAD 1 -#define BS_ROAM 2 -#define BS_PASSIVE 3 -#define BS_ACTIVE 4 -#define BS_SECURE 5 -#define BS_RETREAT 6 -#define BS_HOLD 7 -#define BS_SUPPORT 8 - -// Secondary states -#define BSS_NONE 0 -#define BSS_POSITION 1 -#define BSS_COLLECT 2 -#define BSS_SEEKENEMY 4 -#define BSS_ATTACK 8 - - -// Goal Types (Extensible) -#define GT_NONE 0 -#define GT_POSITION 1 -#define GT_ENEMY 2 -#define GT_ITEM 3 - - -#define MOVE_LEFT 0 -#define MOVE_RIGHT 1 -#define MOVE_FORWARD 2 -#define MOVE_BACK 3 - -// Used in the BOTCOL functions -#define TRACE_DOWN 128 -#define TRACE_DOWN_STRAFE 32 // don't go off ledges when strafing! -#define TRACE_DIST 48 //dropped from 256 -#define TRACE_DIST_STRAFE 24 // watch that edge! -#define TRACE_DIST_SHORT 32 // for forwards motion -#define TRACE_DIST_LADDER 24 -#define TRACE_DIST_JUMP 128 // to jump over gaps -#define TJUMP_DIST 40 //just enough to stand on -#define TWATER_DIST 8 // For getting to edge in water -#define TWEDGE_DIST 16 // ledge width required to leave water -#define TCRAWL_DIST 32 // not worth crawling otherwise -#define TMOVE_DIST 16 // wall detection -#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) // ouch! - -// Movement speeds -#define SPEED_CAREFUL 10 -#define SPEED_ROAM 100 -#define SPEED_WALK 200 -#define SPEED_RUN 400 - -#define EYES_FREQ 0.2 // Every n seconds the bot's eyes will be checked -#define ROOT2 1.41421 // Square root of 2 (i.e. ROOT2^2 = 2) -#define COS90 -0.34202 -#define STRIDESIZE 24 - -#define VEC_ORIGIN tv(0,0,0) - - -typedef struct nodelink_s -{ - short int targetNode; - //rekkie -- DEV_1 -- s - byte targetNodeType; // From our node, what type of node do we need the target node to be to reach it - //rekkie -- DEV_1 -- e - float cost; // updated for pathsearch algorithm - -}nodelink_t; // RiEvEr - - -// Node structure -typedef struct node_s -{ - int area; // Area/chunk/group this node belongs to -- optimise and diversify bot pathing - unsigned int area_color; // Sets the area color - vec3_t origin; // The origin of the node - byte type; // Type of node (MOVE, JUMP, LADDER, etc) - short int nodenum; // Node number - qboolean inuse; // If the node is used, deleted nodes are set to 'unused' - vec3_t normal; // The surface normal the node sits on - vec3_t mins; - vec3_t maxs; - vec3_t absmin; - vec3_t absmax; - float weight; // Used to help diversify bot pathing - //int num_vis_nodes; // Total nodes visible to this node - //int vis_nodes[1024]; // Store all vis nodes - - byte num_links; // Total links this node has - nodelink_t links[MAXLINKS]; // store all links. - RiEvEr -} node_t; -extern node_t *nodes; - -typedef struct item_table_s -{ - int item; - float weight; - edict_t *ent; - int node; - -} item_table_t; - -extern int num_players; -extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game - -// extern decs -//rekkie -- DEV_1 -- s -extern node_t *unsorted_nodes; // Used to generate all links, so they can be sorted, then copied to nodes -#define MAX_PNODES 8096 //8096 -//32768 Absolute max nodes -//extern node_t *nodes; -extern node_t *nodes; -//node_t *nodes[MAX_PNODES]; -//node_t nodes[MAX_PNODES]; -//extern node_t nodes[MAX_PNODES]; -//rekkie -- DEV_1 -- e - -extern short int** path_table; -////short int path_table[MAX_PNODES][MAX_PNODES]; - -//extern node_t nodes[MAX_NODES]; -extern item_table_t item_table[MAX_EDICTS]; -extern qboolean debug_mode; -extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" -extern int numnodes; -extern int num_items; - -// id Function Protos I need -void LookAtKiller (edict_t *self, edict_t *inflictor, edict_t *attacker); -void ClientObituary (edict_t *self, edict_t *inflictor, edict_t *attacker); -void TossClientWeapon (edict_t *self); -void ClientThink (edict_t *ent, usercmd_t *ucmd); -void SelectSpawnPoint (edict_t *ent, vec3_t origin, vec3_t angles); -void ClientUserinfoChanged (edict_t *ent, char *userinfo); -void CopyToBodyQue (edict_t *ent); -qboolean ClientConnect (edict_t *ent, char *userinfo); -void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); - -// acebot_ai.c protos -qboolean ACEAI_CheckShot(edict_t *self); -void ACEAI_Think (edict_t *self); -void ACEAI_PickLongRangeGoal(edict_t *self); -void ACEAI_PickShortRangeGoal(edict_t *self); -void ACEAI_PickSafeGoal(edict_t *self); -qboolean ACEAI_ChooseWeapon(edict_t *self); -void ACEAI_Cmd_Choose( edict_t *ent, char *s ); -void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ); -void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); - -// acebot_cmds.c protos -qboolean ACECM_Commands(edict_t *ent); -void ACECM_Store(void); - -// acebot_items.c protos -void ACEIT_RebuildPlayerList( void ); -qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal); -qboolean ACEIT_IsReachable(edict_t *self,vec3_t goal); -qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item); -//AQ2 ADD -qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item); -qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item); -//AQ2 END -qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other); -float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ); -int ACEIT_ClassnameToIndex( char *classname ); -void ACEIT_BuildItemNodeTable (qboolean rebuild); - -// acebot_movement.c protos -qboolean ACEMV_CanMove(edict_t *self, int direction); -qboolean ACEMV_SpecialMove(edict_t *self,usercmd_t *ucmd); -//void ACEMV_Move(edict_t *self, usercmd_t *ucmd); -void ACEMV_Attack (edict_t *self, usercmd_t *ucmd); -//void ACEMV_Wander (edict_t *self, usercmd_t *ucmd); -//rekkie -- Quake3 -- s -// botlib_items.c -void BOTLIB_SmartWeaponSelection(edict_t* self); -// botlib_ai.c -int BOTLIB_AutoAdjustSkill(edict_t* self); - - -void BOTLIB_Attack(edict_t* self, usercmd_t* ucmd); -void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd); -void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint); -qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity); -// BOT RADIO - -//rekkie -- Quake3 -- e - -// acebot_nodes.c protos -void ACEND_SetGoal(edict_t *self, int goal_node); -int ACEND_FindCost(int from, int to); -int ACEND_FindCloseReachableNode(edict_t *self, int dist, int type); -int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); -int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type); -qboolean BOTLIB_FollowPath(edict_t *self); -int BOTLIB_MakeEntsSolid(solid_t* trigger_solid); -void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid); -qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin); -void ACEND_GrapFired(edict_t *self); -//qboolean ACEND_CheckForLadder(edict_t *self); -//void ACEND_PathMap(edict_t *self); -void BOTLIB_InitNodes(void); -void ACEND_ShowNode(int node); -void ACEND_DrawPath(edict_t *self); -void ACEND_ShowPath(edict_t *self, int goal_node); -//void BOTLIB_GenerateNodeVis(edict_t* self); -int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs); -int BOTLIB_TraceBoxNode(int from, int to); -int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int* nodelist, const int maxnodes, int ignore_node); -int BOTLIB_TestForNodeDist(vec_t* origin, float distance, vec3_t mins, vec3_t maxs); -//int ACEND_AddNode(edict_t *self, int type); -//void ACEND_UpdateNodeEdge(edict_t *self, int from, int to); -void ACEND_RemoveNodeEdge(edict_t *self, int from, int to); -//void ACEND_ResolveAllPaths(void); -//void ACEND_SaveNodes(void); -//void ACEND_LoadNodes(void); - -// acebot_spawn.c protos -//void ACESP_SaveBots(void); -//void ACESP_LoadBots(void); -void ACESP_LoadBotConfig(void); -edict_t *ACESP_SpawnBotFromConfig( char *inString ); -void ACESP_HoldSpawn(edict_t *self); -void ACESP_PutClientInServer (edict_t *bot, qboolean respawn, int team); -void ACESP_Respawn (edict_t *self); -edict_t *ACESP_FindFreeClient (void); -void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); -edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); -//void ACESP_ReAddBots(void); -void ACESP_RemoveBot(char *name); -void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); -void safe_centerprintf (edict_t *ent, const char *fmt, ...); -void debug_printf (char *fmt, ...); - -// bot_ai.c protos -qboolean BOTAI_NeedToBandage(edict_t *bot); -void BOTAI_PickLongRangeGoal(edict_t *self, int iType); -void BOTAI_PickShortRangeGoal(edict_t *bot); -void BOTAI_SetGoal(edict_t *self, int goal_node); -void BOTAI_Think(edict_t *bot); -qboolean BOTAI_VisibleEnemy( edict_t *self ); - -// bot_collision.c protos -qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanStand(edict_t *self); -qboolean BOTCOL_CanJumpUp(edict_t *self); -qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal); -qboolean BOTCOL_CheckShot(edict_t *bot); -qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles); -qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles); -qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd); -qboolean BOTCOL_Visible (edict_t *bot, edict_t *other); - -// bot_combat.c protos -void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles); -void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles); -void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget); - -// bot_movement.c protos -void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); -float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles); -void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest); -void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles); - -// bot_states.c protos -void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd); -void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); - -//bot_utility.c protos -void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg); -void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest); -void BOTUT_ShowNodes (edict_t *ent); -void BOTUT_TempLaser( vec3_t start, vec3_t end); - -// bot_weapon.c protos -int BOTWP_ChangeMK23Mode(edict_t *bot); -int BOTWP_ChangeSniperMode(edict_t *bot); -qboolean BOTWP_ChooseWeapon(edict_t *bot); -int BOTWP_GetMK23Mode(edict_t *bot); -int BOTWP_GetSniperMode(edict_t *bot); -void BOTWP_RemoveSniperZoomMode(edict_t *bot); - -//rekkie -- BSP -- s - -//////////////////////////////////////////////////////////////////////// -// DAIC - Data's 'AI' Child ( taken from S3E16 of TNG ) -// https://en.wikipedia.org/wiki/The_Offspring_(Star_Trek%3A_The_Next_Generation) -// "Data invites Deanna Troi, Wesley Crusher and Geordi La Forge to his lab and surprises them by introducing a featureless humanoid android, whom he created based on his own structural design and recent advances in Federation cybernetics technology, describing it as his child. He names the android Lal (after the Hindi word for "beloved") and encourages it to select a gender and appearance. With Troi's assistance and considering many of the on-board species as well as the databanks, Lal narrows down to four possibilities, including a Klingon male, which, as Troi points out, would make it "a friend for Worf", but in the end selects the appearance of a young female human becoming a Gynoid.Data first aids Lal with cognitiveand standard behavioral algorithms, as well encourages her to interact with other members of the crew to learn behavioraland social customs." -// Notice that Data is attempting to make her in his own image, as we are attempting to make a bot like a human player. His child can also pick its own genderand appearance, like LTK bots do in DM.Additionally, he trains his child with "with cognitive and standard behavioral algorithms". -//////////////////////////////////////////////////////////////////////// -// -// TODO: -// - Make bot aim worst upon first few rounds by taking skill, minus 5 from it, then each shot adds +1 to skill until skill is back to normal. -// copy skill to default_skill, make a tmpskill and set it to skill and subtract -5, subtract tmpskill from skill in the code doing the aiming -// (check if skill is less than 0, set it to 0), then add +1 to tmpskill. Repeat this until skill is back to normal. -// If the bot finds a new enemy or stops firing, reset skill and default_skill. -// -// - When first adding reachabilities for the first time, set them to only use nodes on flat/sloped ground + ladders. -// For jumppads, record player's jumps from node to node and let the bot use these as jumppads so the bot can better pick jumps. -// -// - Make bot aim and strafe -// -// - Make bot be able to move while looking at any direction -// -// - in acebot_movement, when the bot is moving and NOT on ground, check if bot touches another player/bot while in air, if so then set bot to wander or find a new path? -// -// - Antsearch pathing could check if there is something blocking the path and if so, try to find a new path. -// -// - Try to find a way to add a variety of paths: Add varience, randomness, and more path_table's to pick randomly from! -// -// - Upon spawning, head to random spawn point. -// -// - For the top of ladders with rungs (like the ladders in urban2 that have left/right poles and rungs going horizontally in the middle), the rungs don't always -// come up high enough to meet the ground above (small ladder in ground level room that has a hole in roof going up). -// -// - Add random 'action hero' bot names -// -// - Make bot remember last enemy (or enemies) they encountered each round, attack then until they're dead or we gain a new enemy. Perhaps cycle through list of enemies -// we encounter when we're not attacking. -// -// - Punch grenades (allies or enemies), or catch grenade and add to inventory -// -// - Bots aiming through water, some water opacity should limit the bot's ability to aim through water, via water -// -// - [Drg] Varekai � Today at 22:26 -// I've always wondered because we use punch to get to weird places and I imagined that how far someone went had more to do with their initial speed -// as in, hitting them as they're just jumping would be more effective -// darksaint � Today at 22:27 -// ReKTeK aka Mech � Today at 22 : 31 -// Having another quick skim of the code, it doesn't take velocity into account. However, if the player is airborne, they wouldn't be slowed down by any initial friction(if any is applied) plus the height would give extra distance -// Drg] Varekai � Today at 22 : 32 -// so it is mostly random, pretty nuts -// ReKTeK aka Mech � Today at 22 : 33 -// I suppose velocity should be taken into account -// darksaint � Today at 22 : 35 -// Kicking someone in mid - air after being kicked should result in double damage :) using their body like a football -// -// - Roundlimit: add a countdown at 30 seconds, 15 seconds... -// - Donations: name on bill board, name in credits (txt/menu), -// - Bots attack and continue to move up/down ladders... -// - Make a beep/ding/windows sound when minimised and a round has ended. -// - Bot names, Diablo 2 pre, post fix names -// - Chance to shoot weapon out of hand -// - Make maps snow + fog for xmas -// - Make maps have a snow/fog/rain/etc. effect on the bot's view -// -// Yogomaster, Shadowking, Dirty, Psy, MOG, and Rekkie are the best bots. Lord Slice [web] -// - -// Node Stuff -#define MAX_SP_NODES 256 // Maximum Spawn Spots (SP), and Spawn Spot Nodes (SPN) -#define MAX_POI_NODES 512 // Maximum POI nodes -#define MAX_DOOR_NODES 64 // Maximum Door nodes -#define MAX_VIS_NODES 320 // Maximum visibility nodes -// - - - - -// -//#define MOVE_FORWARD_LEFT 4 // +45 degrees left -//#define MOVE_FORWARD_RIGHT 5 // -45 degrees right -//#define MOVE_BACK_LEFT 6 // +135 degrees left -//#define MOVE_BACK_RIGHT 7 // -135 degrees right -// Acebot Node Stuff -- e - -#define STEPSIZE 18 - -#define MINS_PLAYER_STANDING tv(-16, -16, -24) // Player standing -#define MAXS_PLAYER_STANDING tv(16, 16, 32) - -#define MINS_PLAYER_CROUCHING tv(-16, -16, -24) // Player crouching -#define MAXS_PLAYER_SCROUCHING tv(16, 16, 4) - -//rekkie -- DEV_1 -- Gib Players -- s -extern cvar_t* gib_bodies; -extern cvar_t* gib_heads; -//rekkie -- DEV_1 -- Gib Players -- e - -extern cvar_t* bot_maxteam; -extern cvar_t* bot_rush; -extern cvar_t* bot_randvoice; -extern cvar_t* bot_randskill; -extern cvar_t* bot_randname; -//extern cvar_t* bot_randteamskin; - - - - -#define MAX_BOT_NAMES 64 -typedef struct { - char name[16]; // Name -}bot_names_t; -extern int dc_total_male_names; // Total male names -extern int dc_total_female_names; // Total female names -extern bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy -extern bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy - - - - - - - - - -// Maximum nodes -//#define NODE_MAX 15000 // Max nodes -- 25,000 -//#define NODE_MAXLINKS 16 // Maximum links per node -#define NODE_Z_HEIGHT 32 // Z height of node placement -#define NODE_Z_HALF_HEIGHT 24 // 1/2 of NODE_Z_HEIGHT -#define NODE_Z_HEIGHT_PLUS_STEPSIZE 50 // NODE_Z_HEIGHT + STEPSIZE -#define NODE_MAX_DISTANCE 999999 // Max distance for a node -#define NODE_SIZE 24 // Physical size of the node -#define NODE_GRID_SIZE 16 //(NODE_SIZE*2) // Fit nodes on grid alignment -#define NODE_MAX_JUMP_HEIGHT 60 // Maximum height that a player can jump to reach a higher level surface, such as a box -#define NODE_MAX_FALL_HEIGHT 210 // Maximum height that a player can fall from without damage -#define NODE_MAX_SAFE_CROUCH_FALL_HEIGHT 214 // Maximum height that a player can crouch fall from without damage -#define NODE_MAX_CROUCH_FALL_HEIGHT 224 // Maximum height that a player can crouch fall from without damage -#define NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE 256 // Maximum height that a player can crouch fall + minor leg damage -#define NODE_CROUCH_HEIGHT 29 // Crouch height that a player can fit under -#define MAX_STEEPNESS 0.7 // Maximum normal value - steepness of the surface (0.7) - -// NMesh constraints -#define MAX_NMESH_FACES 18432 //18432 //16384 //12192 //8096 -#define MAX_REACHABILITY 64 -#define FACETYPE_FLOOR 1 -#define FACETYPE_SLOPE 2 -#define FACETYPE_WALL 3 -//#define FACETYPE_LADDER 4 -// Triangles -typedef struct { - vec3_t v[3]; // Verts of the triangle - vec3_t center; // Centoid of the triangle - int face; // The face this triangle belongs to - int node; // Triangle node - - // Reachability of nearby triangles - int reach_triangle[MAX_REACHABILITY]; // Nearby triangles reached - vec3_t reach_origin[MAX_REACHABILITY][3]; // Nearby triangle coordinates - int reach_face[MAX_REACHABILITY]; // The face that the nearby triangle resides in - int reach_num; // The number of nearby triangles reached - - // Reachability of target triangles (jumping) - //float reach_velocity[MAX_REACHABILITY]; // The velocity required to reach the target triangle - //float reach_distance[MAX_REACHABILITY]; // The distance to the target triangle - //float reach_time[MAX_REACHABILITY]; // The time it takes to reach the target triangle - //vec3_t reach_direction[MAX_REACHABILITY]; // The direction to the target triangle - -} ntris_t; -// Verts -typedef struct { - vec3_t v; - int node; // Vert node -} nvert_t; -// Edges -typedef struct { - vec3_t v[2]; // Verts of current edge - vec3_t center; // Center of the edge - int node; // Edge node -} nedge_t; -// Faces -typedef struct nface_s { - - // Verts - int num_verts; // Verts on current face - vec3_t v[64]; // All verts of current floor surface - - // Edges - nedge_t edge[128]; // Max edges per face - int num_edges; // Edges on current face - - // Triangles - ntris_t tris[32]; // Max triangles per face - int num_tris; // Triangles on current face - - int type; // Surface type - FACETYPE_FLOOR, FACETYPE_SLOPE, FACETYPE_WALL - int drawflags; // Surface flags - SURF_SKY, SURF_TRANS33, SURF_TRANS66, ... - int contents; // Surface contents - CONTENTS_SOLID, CONTENTS_WINDOW, ... - vec3_t normal; // Surface normals - -} nface_t; -typedef struct nmesh_s { - - // Totals - int total_faces; // Faces - int total_walls; // Walls - int total_edges; // Edges - int total_verts; // Verts - int total_tris; // Triangles - int total_reach; // Triangles reached - - // Floors & Walls - nface_t face[MAX_NMESH_FACES]; // Max faces (flat, sloped, walls) - - unsigned bsp_checksum; // Map checksum - -} nmesh_t; -extern nmesh_t nmesh; - - -extern int num_poi_nodes; -extern int poi_nodes[MAX_POI_NODES]; -extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -extern int num_vis_nodes; -extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -extern int node_vis_list[10][10]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. -//int node_vis[MAX_PNODES][MAX_PNODES]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -//int node_vis_list[MAX_PNODES][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. - - -// Botlib A_TEAM -//void CheckBotRules(void); - - -// acebot_ai.c -qboolean ACEAI_IsEnemy(edict_t* self, edict_t* other); -short BOTLIB_EnemiesAlive(edict_t* self); -short BOTLIB_AlliesAlive(edict_t* self); - -// DC Movement -void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd); -void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd); -void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd); -float VectorDistance(vec3_t start, vec3_t end); -float VectorDistanceXY(vec3_t start, vec3_t end); - - - -// Navigation -#define ANT_FREQ 0.5 // Time gap between calls to the processor intensive search -// ----------- new Pathing Algorithm stuff ----- -qboolean AntPathMove(edict_t* ent); // Called in item and enemy route functions -void AntInitSearch(edict_t* ent); // Resets all the path lists etc. -qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization); // main entry to path algorithms -qboolean AntQuickPath(edict_t* ent, int from, int to); // backup path system -qboolean AntFindPath(edict_t* ent, int from, int to); // Optimised path system -qboolean AntLinkExists(int from, int to); // Detects if we are off the path -// --------- AI Tactics Values ------------- -enum { - AIRoam, // Basic item collection AI - AIAttack, // Basic Attack Enemy AI - AIAttackCollect,// Attack Enemy while collecting Item - AICamp, // Camp at a suitable location and collect the item on respawn - AISnipe, - AIAmbush -}; - -qboolean BOTLIB_DrawPath(edict_t* self); // Draw bot path -void SLLpush_front(botlib_sll_t* thelist, int nodedata); // Add to the front of the list -void SLLpop_front(botlib_sll_t* thelist); // Remove the iten from the front of the list -int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list); //rekkie -- Query the SLL for the next node -int BOTLIB_SLL_Query_All_Nodes(edict_t* ent, botlib_sll_t* list, int* node_list, const int max_nodes); //rekkie -- Query and return all the nodes in the list -int SLLfront(botlib_sll_t* thelist); // Get the integer value from the front of the list, without removing the item (Query the list) -void SLLpush_back(botlib_sll_t* thelist, int nodedata); // Add to the back of the list -qboolean SLLempty(botlib_sll_t* thelist); // See if the list is empty (false if not empty) -void SLLdelete(botlib_sll_t* thelist); // Free all memory from a list - - - - - -// BOTLIB Nodes -void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end); // Rotate around a center, move the vector out and return the new point -void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int* edges); // Gets the nearest BSP face and number of edges to the origin -void BOTLIB_ChangeNodeFunction(edict_t* ent); -void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd); -int BOTLIB_Reachability(int from, int to); -qboolean BOTLIB_AdvanceToSelectNode(edict_t* self, int node); -qboolean NODES_AdvanceToNextNode(edict_t* self); -void BOTLIB_InitNavigation(edict_t* ent); -int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type); -qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost); -void BOTLIB_SelfExpandingNodes(edict_t* ent, int node); -void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t* ent); -void BOTLIB_LinkAllNodesTogether(edict_t* ent); -void BOTLIB_RemoveAllNodeLinksFrom(int from); - -#ifdef USE_ZLIB -void BOTLIB_SaveNavCompressed(void); -void BOTLIB_LoadNavCompressed(void); -#else -void BOTLIB_SaveNav(void); -void BOTLIB_LoadNav(void); -#endif -qboolean NodeTypeToString(edict_t* self, int type, char* string, const int max_string_size); - -//void ACEND_FreeUnsortedNodes(void); -//qboolean ACEND_InitUnsortedNodes(void); -void ACEND_BSP(edict_t* ent); -void ACEND_BuildSpawnPointNodes(void); -void ACEND_CachePointOfInterestNodes(void); -void ACEND_BuildEntNodeTable(void); -void ACEND_BuildVisibilityNodes(void); -qboolean ACEND_IsNodeVisibleToNodes(short x, short y); -short ACEND_GetRandomVisibleNode(short x); -//void ACEND_ShowAllNodes(void); -void BOTLIB_FreeNodes(void); // Free all nodes from memory -qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type); // Add node -//void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type); -void ACEND_RemoveNode(edict_t* self, int nodenum); - -qboolean ACEND_UpdateNodeReach(int from, int to); -//qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top); -void ACEND_SaveAAS(qboolean update); -void ACEND_LoadAAS(qboolean force); // Load Area Awareness System. Force: generate a new AAS instead of loading from file -void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle); -qboolean LaunchPlayer(edict_t* ent, vec3_t target); -//int ACEND_JumpReachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal); - - -// DAIC Spawn -void ACESP_RemoveTeamplayBot(int team); -void DC_LoadRandomBotName(char* userinfo); -//void DC_LoadRandomBotName(byte gender, char* bot_name); - -//rekkie -- BSP -- e - - -//rekkie -- Quake3 -- s -//=============================== -// Quake 3 Botlib.h Code -//=============================== -/* -* case 0: - SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff - break; - case 1: - SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute - break; - case 2: - SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt - break; - case 3: - SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave - break; - case 4: - default: - SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point -*/ - - -//=============================== -// Quake 3 Multithreading Code -//=============================== -// qthreads.h -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/qthreads.h -void BOTLIB_THREAD_LOADAAS(qboolean force); // Init and run this function threaded -void BOTLIB_THREADING_LOADAAS(void* param); // Running in a thread -typedef struct loadaas_s // Struct used to send parameters to the threaded function -{ - qboolean force; -} loadaas_t; - -void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to); -void BOTLIB_THREADING_DijkstraPath(void* param); -typedef struct dijkstra_path_s // Struct used to send parameters to the threaded function -{ - edict_t *ent; - int from; - int to; -} dijkstra_path_t; -// -extern int numthreads; -void ThreadSetDefault(void); -int GetThreadWork(void); -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)); -//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); -//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); -void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); -//void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); -void ThreadLock(void); -void ThreadUnlock(void); -//rekkie -- Quake3 -- e - -#endif +/////////////////////////////////////////////////////////////////////// +// +// ACE - Quake II Bot Base Code +// +// Version 1.0 +// +// Original file is Copyright(c), Steve Yeager 1998, All Rights Reserved +// +// +// All other files are Copyright(c) Id Software, Inc. +//////////////////////////////////////////////////////////////////////// +/* + * $Header: /LTK2/src/acesrc/acebot.h 9 29/02/00 11:20 Riever $ + * + * $Log: /LTK2/src/acesrc/acebot.h $ + * + * 9 29/02/00 11:20 Riever + * ChooseWeapon changed to qboolean + * + * 8 27/02/00 13:07 Riever + * Changed enums to defines for better compatibility. + * + * 7 24/02/00 3:07 Riever + * BOTUT_Cmd_Say_f proto added + * + * 6 23/02/00 17:24 Riever + * Added support for 'sv shownodes on/off' + * Enabled creation of nodes for ALL doors. (Stage 1 of new method) + * + * 5 21/02/00 23:45 Riever + * Added GT_ goal selection support and ROAM code protos. Altered movement + * trace lengths to be shorter. + * + * 4 21/02/00 15:16 Riever + * Bot now has the ability to roam on dry land. Basic collision functions + * written. Active state skeletal implementation. + * + * 3 20/02/00 20:27 Riever + * Added new members and definitions ready for 2nd generation of bots. + * + * 2 17/02/00 17:53 Riever + * Fixed item list to be in the right order! + * + */ + +/////////////////////////////////////////////////////////////////////// +// +// acebot.h - Main header file for ACEBOT +// +// +/////////////////////////////////////////////////////////////////////// + +#ifndef _ACEBOT_H +#define _ACEBOT_H + +#include "../botlib/botlib.h" +// Bots think at the server framerate to make sure they move smoothly. +#define BOT_FPS (game.framerate) + +// Only 100 allowed for now (probably never be enough edicts for 'em) +#define MAX_BOTS 100 + +// Platform states +#define STATE_TOP 0 +#define STATE_BOTTOM 1 +#define STATE_UP 2 +#define STATE_DOWN 3 + +// Maximum nodes +//#define MAX_NODES 1200 + +// Node types +#define INVALID -1 // Invalid / Terminating node/links +#define NAV_INFINITE 99999 //rekkie -- for navigation: Dijkstra's algorithm +// +typedef enum +{ + NODE_NONE, + NODE_MOVE, // Move (forward, left, right, back) + NODE_JUMPPAD, // For large jumps across any distance + NODE_LADDER, // Ladder + NODE_WATER, // Water + NODE_CROUCH, // Crouching with a reduced hitbox/collision height + NODE_BOXJUMP, // Move jump + half size hitbox (for jumping into windows, on boxes, etc) + NODE_POI, // Point of Interest (POI) specific locations that you wish bots to visit from time to time + NODE_POI_LOOKAT, // When using a Point of Interest (POI), this is the node to look at + + NODE_STEP, // For steps / stairs + NODE_JUMP, // Small jumps (+moveup) + NODE_STAND_DROP, // For dropping down while standing + NODE_CROUCH_DROP, // For dropping down while crouching + NODE_UNSAFE_DROP, // For dropping down while crouching, but causing minimal leg damage + NODE_LADDER_UP, // Ladder going up + NODE_LADDER_DOWN, // Ladder going down + NODE_DOOR, // Door node + NODE_PLATFORM, // Moving platforms + NODE_TELEPORTER, // Teleporters + NODE_ITEM, // Items + NODE_GRAPPLE, // CTF grapple hook + NODE_SPAWNPOINT, // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes + NODE_LEARN, // Special node - Human learning + NODE_CTF_FLAG, // CTF Flag + NODE_ALL = 99 // For selecting all nodes +} nodetype_t; +/* +#define NODE_MOVE (1) // Move (forward, left, right, back) +#define NODE_CROUCH (NODE_MOVE + 1) // Crouching (-moveup) +#define NODE_STEP (NODE_CROUCH + 1) // For steps / stairs +#define NODE_JUMP (NODE_STEP + 1) // Small jumps (+moveup) +#define NODE_JUMPPAD (NODE_JUMP + 1) // For large jumps across any distance +#define NODE_STAND_DROP (NODE_JUMPPAD + 1) // For dropping down while standing +#define NODE_CROUCH_DROP (NODE_STAND_DROP + 1) // For dropping down while crouching +#define NODE_UNSAFE_DROP (NODE_CROUCH_DROP + 1) // For dropping down while crouching, but causing minimal leg damage +#define NODE_LADDER (NODE_UNSAFE_DROP + 1) // Ladder +#define NODE_LADDER_UP (NODE_LADDER + 1) // Ladder going up +#define NODE_LADDER_DOWN (NODE_LADDER_UP + 1) // Ladder going down +#define NODE_DOOR (NODE_LADDER_DOWN + 1) // Door node +#define NODE_PLATFORM (NODE_DOOR + 1) // Moving platforms +#define NODE_TELEPORTER (NODE_PLATFORM + 1) // Teleporters +#define NODE_ITEM (NODE_TELEPORTER + 1) // Items +#define NODE_WATER (NODE_ITEM + 1) // Water +#define NODE_GRAPPLE (NODE_WATER + 1) // CTF grapple hook +#define NODE_SPAWNPOINT (NODE_GRAPPLE + 1) // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes +#define NODE_POI (NODE_SPAWNPOINT + 1) // Point of Interest (POI) specific locations that you wish bots to visit from time to time +#define NODE_LEARN (NODE_POI + 1) // Special node - Human learning +// +#define NODE_ALL 99 // For selecting all nodes +*/ + +// Density setting for nodes +#define NODE_DENSITY 96 + +// Maximum links per node +//#define MAXLINKS 12 + +//rekkie -- BSP -- s +//#undef MAX_NODES +//#define MAX_NODES 12288 //7168 //6144 +//#undef MAXLINKS +#define MAXLINKS 32 +//rekkie -- BSP -- e + +#define MAX_BOTSKILL 10 +extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! +extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost +extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +extern cvar_t *bot_showpath; // Show bot paths + +//AQ2 ADD +extern cvar_t *ltk_skill; // Skill setting for bots, range 0-10 +extern cvar_t *ltk_showpath; // Toggles display of bot paths in debug mode +extern cvar_t *ltk_chat; // Chat setting for bots, off or on (0,1) +extern cvar_t *ltk_routing; // Set to 1 to drop nodes, otherwise you won't do it! +extern cvar_t *ltk_botfile; // Set this to adjust which botdata file to load, default is botdata +extern cvar_t *ltk_loadbots; // Set to 0 to not load bots from ltk_botfile value, 1 for normal operation + +extern int lights_camera_action; + +//AQ2 END + +// Bot state types +#define STATE_STAND 0 +#define STATE_MOVE 1 +#define STATE_ATTACK 2 +#define STATE_WANDER 3 +#define STATE_FLEE 4 +#define STATE_POSITION 5 +#define STATE_COVER 6 +#define STATE_ITEM 7 // Bot is grabbing an item + +// New state definitions +#define BS_WAIT 0 +#define BS_DEAD 1 +#define BS_ROAM 2 +#define BS_PASSIVE 3 +#define BS_ACTIVE 4 +#define BS_SECURE 5 +#define BS_RETREAT 6 +#define BS_HOLD 7 +#define BS_SUPPORT 8 + +// Secondary states +#define BSS_NONE 0 +#define BSS_POSITION 1 +#define BSS_COLLECT 2 +#define BSS_SEEKENEMY 4 +#define BSS_ATTACK 8 + + +// Goal Types (Extensible) +#define GT_NONE 0 +#define GT_POSITION 1 +#define GT_ENEMY 2 +#define GT_ITEM 3 + + +#define MOVE_LEFT 0 +#define MOVE_RIGHT 1 +#define MOVE_FORWARD 2 +#define MOVE_BACK 3 + +// Used in the BOTCOL functions +#define TRACE_DOWN 128 +#define TRACE_DOWN_STRAFE 32 // don't go off ledges when strafing! +#define TRACE_DIST 48 //dropped from 256 +#define TRACE_DIST_STRAFE 24 // watch that edge! +#define TRACE_DIST_SHORT 32 // for forwards motion +#define TRACE_DIST_LADDER 24 +#define TRACE_DIST_JUMP 128 // to jump over gaps +#define TJUMP_DIST 40 //just enough to stand on +#define TWATER_DIST 8 // For getting to edge in water +#define TWEDGE_DIST 16 // ledge width required to leave water +#define TCRAWL_DIST 32 // not worth crawling otherwise +#define TMOVE_DIST 16 // wall detection +#define MASK_DEADLY (CONTENTS_LAVA|CONTENTS_SLIME) // ouch! + +// Movement speeds +#define SPEED_CAREFUL 10 +#define SPEED_ROAM 100 +#define SPEED_WALK 200 +#define SPEED_RUN 400 + +#define EYES_FREQ 0.2 // Every n seconds the bot's eyes will be checked +#define ROOT2 1.41421 // Square root of 2 (i.e. ROOT2^2 = 2) +#define COS90 -0.34202 +#define STRIDESIZE 24 + +#define VEC_ORIGIN tv(0,0,0) + + +typedef struct nodelink_s +{ + short int targetNode; + //rekkie -- DEV_1 -- s + byte targetNodeType; // From our node, what type of node do we need the target node to be to reach it + //rekkie -- DEV_1 -- e + float cost; // updated for pathsearch algorithm + +}nodelink_t; // RiEvEr + + +// Node structure +typedef struct node_s +{ + int area; // Area/chunk/group this node belongs to -- optimise and diversify bot pathing + unsigned int area_color; // Sets the area color + vec3_t origin; // The origin of the node + byte type; // Type of node (MOVE, JUMP, LADDER, etc) + short int nodenum; // Node number + qboolean inuse; // If the node is used, deleted nodes are set to 'unused' + vec3_t normal; // The surface normal the node sits on + vec3_t mins; + vec3_t maxs; + vec3_t absmin; + vec3_t absmax; + float weight; // Used to help diversify bot pathing + //int num_vis_nodes; // Total nodes visible to this node + //int vis_nodes[1024]; // Store all vis nodes + + byte num_links; // Total links this node has + nodelink_t links[MAXLINKS]; // store all links. - RiEvEr +} node_t; +extern node_t *nodes; + +typedef struct item_table_s +{ + int item; + float weight; + edict_t *ent; + int node; + +} item_table_t; + +extern int num_players; +extern edict_t *players[MAX_CLIENTS]; // pointers to all players in the game + +// extern decs +//rekkie -- DEV_1 -- s +extern node_t *unsorted_nodes; // Used to generate all links, so they can be sorted, then copied to nodes +#define MAX_PNODES 8096 //8096 +//32768 Absolute max nodes +//extern node_t *nodes; +extern node_t *nodes; +//node_t *nodes[MAX_PNODES]; +//node_t nodes[MAX_PNODES]; +//extern node_t nodes[MAX_PNODES]; +//rekkie -- DEV_1 -- e + +extern short int** path_table; +////short int path_table[MAX_PNODES][MAX_PNODES]; + +//extern node_t nodes[MAX_NODES]; +extern item_table_t item_table[MAX_EDICTS]; +extern qboolean debug_mode; +extern qboolean shownodes_mode; // RiEvEr - for the new command "sv shownodes on/off" +extern int numnodes; +extern int num_items; + +// id Function Protos I need +void LookAtKiller (edict_t *self, edict_t *inflictor, edict_t *attacker); +void ClientObituary (edict_t *self, edict_t *inflictor, edict_t *attacker); +void TossClientWeapon (edict_t *self); +void ClientThink (edict_t *ent, usercmd_t *ucmd); +void SelectSpawnPoint (edict_t *ent, vec3_t origin, vec3_t angles); +void ClientUserinfoChanged (edict_t *ent, char *userinfo); +void CopyToBodyQue (edict_t *ent); +qboolean ClientConnect (edict_t *ent, char *userinfo); +void Use_Plat (edict_t *ent, edict_t *other, edict_t *activator); + +// acebot_ai.c protos +qboolean ACEAI_CheckShot(edict_t *self); +void ACEAI_Think (edict_t *self); +void ACEAI_PickLongRangeGoal(edict_t *self); +void ACEAI_PickShortRangeGoal(edict_t *self); +void ACEAI_PickSafeGoal(edict_t *self); +qboolean ACEAI_ChooseWeapon(edict_t *self); +void ACEAI_Cmd_Choose( edict_t *ent, char *s ); +void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ); +void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); + +// acebot_cmds.c protos +qboolean ACECM_Commands(edict_t *ent); +void ACECM_Store(void); + +// acebot_items.c protos +void ACEIT_RebuildPlayerList( void ); +qboolean ACEIT_IsVisible(edict_t *self, vec3_t goal); +qboolean ACEIT_IsReachable(edict_t *self,vec3_t goal); +qboolean ACEIT_ChangeWeapon (edict_t *ent, gitem_t *item); +//AQ2 ADD +qboolean ACEIT_ChangeMK23SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeHCSpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeSniperSpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeM4SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeM3SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeMP5SpecialWeapon (edict_t *ent, gitem_t *item); +qboolean ACEIT_ChangeDualSpecialWeapon (edict_t *ent, gitem_t *item); +//AQ2 END +qboolean ACEIT_CanUseArmor (gitem_t *item, edict_t *other); +float ACEIT_ItemNeed( edict_t *self, edict_t *item_ent ); +int ACEIT_ClassnameToIndex( char *classname ); +void ACEIT_BuildItemNodeTable (qboolean rebuild); + +// acebot_movement.c protos +qboolean ACEMV_CanMove(edict_t *self, int direction); +qboolean ACEMV_SpecialMove(edict_t *self,usercmd_t *ucmd); +//void ACEMV_Move(edict_t *self, usercmd_t *ucmd); +void ACEMV_Attack (edict_t *self, usercmd_t *ucmd); +//void ACEMV_Wander (edict_t *self, usercmd_t *ucmd); +//rekkie -- Quake3 -- s +// botlib_items.c +void BOTLIB_SmartWeaponSelection(edict_t* self); +// botlib_ai.c +int BOTLIB_AutoAdjustSkill(edict_t* self); + + +void BOTLIB_Attack(edict_t* self, usercmd_t* ucmd); +void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd); +void BOTLIB_PredictJumpPoint(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity, vec3_t start, float bolt_speed, bool eye_height, float offset, vec3_t* aimdir, vec3_t* aimpoint); +qboolean BOTLIB_Jump_Takeoff(edict_t* self, edict_t* target, vec3_t target_point, float target_viewheight, vec3_t target_velocity); +// BOT RADIO + +//rekkie -- Quake3 -- e + +// acebot_nodes.c protos +void ACEND_SetGoal(edict_t *self, int goal_node); +int ACEND_FindCost(int from, int to); +int ACEND_FindCloseReachableNode(edict_t *self, int dist, int type); +int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); +int ACEND_FindClosestReachableSafeNode(edict_t *self, int range, int type); +qboolean BOTLIB_FollowPath(edict_t *self); +int BOTLIB_MakeEntsSolid(solid_t* trigger_solid); +void BOTLIB_RestoreEntsSolidState(solid_t* trigger_solid); +qboolean BOTLIB_UTIL_CHECK_FOR_HURT(vec3_t origin); +void ACEND_GrapFired(edict_t *self); +//qboolean ACEND_CheckForLadder(edict_t *self); +//void ACEND_PathMap(edict_t *self); +void BOTLIB_InitNodes(void); +void ACEND_ShowNode(int node); +void ACEND_DrawPath(edict_t *self); +void ACEND_ShowPath(edict_t *self, int goal_node); +//void BOTLIB_GenerateNodeVis(edict_t* self); +int BOTLIB_TraceNodeBoxLine(vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs); +int BOTLIB_TraceBoxNode(int from, int to); +int BOTLIB_NodeTouchNodes(vec_t *origin, vec3_t normal, float size, vec3_t mins, vec3_t maxs, int* nodelist, const int maxnodes, int ignore_node); +int BOTLIB_TestForNodeDist(vec_t* origin, float distance, vec3_t mins, vec3_t maxs); +//int ACEND_AddNode(edict_t *self, int type); +//void ACEND_UpdateNodeEdge(edict_t *self, int from, int to); +void ACEND_RemoveNodeEdge(edict_t *self, int from, int to); +//void ACEND_ResolveAllPaths(void); +//void ACEND_SaveNodes(void); +//void ACEND_LoadNodes(void); + +// acebot_spawn.c protos +//void ACESP_SaveBots(void); +//void ACESP_LoadBots(void); +void ACESP_LoadBotConfig(void); +edict_t *ACESP_SpawnBotFromConfig( char *inString ); +void ACESP_HoldSpawn(edict_t *self); +void ACESP_PutClientInServer (edict_t *bot, qboolean respawn, int team); +void ACESP_Respawn (edict_t *self); +edict_t *ACESP_FindFreeClient (void); +void ACESP_SetName(edict_t *bot, char *name, char *skin, char *team); +edict_t *ACESP_SpawnBot (char *team, char *name, char *skin, char *userinfo); +//void ACESP_ReAddBots(void); +void ACESP_RemoveBot(char *name); +void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); +void safe_centerprintf (edict_t *ent, const char *fmt, ...); +void debug_printf (char *fmt, ...); + +// bot_ai.c protos +qboolean BOTAI_NeedToBandage(edict_t *bot); +void BOTAI_PickLongRangeGoal(edict_t *self, int iType); +void BOTAI_PickShortRangeGoal(edict_t *bot); +void BOTAI_SetGoal(edict_t *self, int goal_node); +void BOTAI_Think(edict_t *bot); +qboolean BOTAI_VisibleEnemy( edict_t *self ); + +// bot_collision.c protos +qboolean BOTCOL_CanJumpForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanCrawl(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanStand(edict_t *self); +qboolean BOTCOL_CanJumpUp(edict_t *self); +qboolean BOTCOL_CanMoveForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanReachItem(edict_t *bot, vec3_t goal); +qboolean BOTCOL_CheckShot(edict_t *bot); +qboolean BOTCOL_WaterMoveForward(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanLeaveWater(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanStrafeSafely(edict_t *self, vec3_t angles); +qboolean BOTCOL_CanJumpGap(edict_t *self, vec3_t angles); +qboolean BOTCOL_CheckBump (edict_t *bot, usercmd_t *ucmd); +qboolean BOTCOL_Visible (edict_t *bot, edict_t *other); + +// bot_combat.c protos +void BOTCOM_Aim (edict_t *bot, edict_t *target, vec3_t angles); +void BOTCOM_AimAt (edict_t *bot, vec3_t target, vec3_t angles); +void BOTCOM_BasicAttack (edict_t *bot, usercmd_t *cmd, vec3_t vTarget); + +// bot_movement.c protos +void BOTMV_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); +float BOTMV_FindBestDirection(edict_t *bot, vec3_t vBestDest, vec3_t angles); +void BOTMV_SetJumpVelocity(edict_t *bot, vec3_t angles, vec3_t vTempDest); +void BOTMV_SetRandomDirection(edict_t *bot, vec3_t angles); + +// bot_states.c protos +void BOTST_Active( edict_t *bot, vec3_t angles, usercmd_t *cmd); +void BOTST_Roaming( edict_t *bot, vec3_t angles, usercmd_t *cmd); + +//bot_utility.c protos +void BOTUT_Cmd_Say_f (edict_t *ent, char *pMsg); +void BOTUT_MakeTargetVector(edict_t *bot, vec3_t angles, vec3_t vDest); +void BOTUT_ShowNodes (edict_t *ent); +void BOTUT_TempLaser( vec3_t start, vec3_t end); + +// bot_weapon.c protos +int BOTWP_ChangeMK23Mode(edict_t *bot); +int BOTWP_ChangeSniperMode(edict_t *bot); +qboolean BOTWP_ChooseWeapon(edict_t *bot); +int BOTWP_GetMK23Mode(edict_t *bot); +int BOTWP_GetSniperMode(edict_t *bot); +void BOTWP_RemoveSniperZoomMode(edict_t *bot); + +//rekkie -- BSP -- s + +//////////////////////////////////////////////////////////////////////// +// DAIC - Data's 'AI' Child ( taken from S3E16 of TNG ) +// https://en.wikipedia.org/wiki/The_Offspring_(Star_Trek%3A_The_Next_Generation) +// "Data invites Deanna Troi, Wesley Crusher and Geordi La Forge to his lab and surprises them by introducing a featureless humanoid android, whom he created based on his own structural design and recent advances in Federation cybernetics technology, describing it as his child. He names the android Lal (after the Hindi word for "beloved") and encourages it to select a gender and appearance. With Troi's assistance and considering many of the on-board species as well as the databanks, Lal narrows down to four possibilities, including a Klingon male, which, as Troi points out, would make it "a friend for Worf", but in the end selects the appearance of a young female human becoming a Gynoid.Data first aids Lal with cognitiveand standard behavioral algorithms, as well encourages her to interact with other members of the crew to learn behavioraland social customs." +// Notice that Data is attempting to make her in his own image, as we are attempting to make a bot like a human player. His child can also pick its own genderand appearance, like LTK bots do in DM.Additionally, he trains his child with "with cognitive and standard behavioral algorithms". +//////////////////////////////////////////////////////////////////////// +// +// TODO: +// - Make bot aim worst upon first few rounds by taking skill, minus 5 from it, then each shot adds +1 to skill until skill is back to normal. +// copy skill to default_skill, make a tmpskill and set it to skill and subtract -5, subtract tmpskill from skill in the code doing the aiming +// (check if skill is less than 0, set it to 0), then add +1 to tmpskill. Repeat this until skill is back to normal. +// If the bot finds a new enemy or stops firing, reset skill and default_skill. +// +// - When first adding reachabilities for the first time, set them to only use nodes on flat/sloped ground + ladders. +// For jumppads, record player's jumps from node to node and let the bot use these as jumppads so the bot can better pick jumps. +// +// - Make bot aim and strafe +// +// - Make bot be able to move while looking at any direction +// +// - in acebot_movement, when the bot is moving and NOT on ground, check if bot touches another player/bot while in air, if so then set bot to wander or find a new path? +// +// - Antsearch pathing could check if there is something blocking the path and if so, try to find a new path. +// +// - Try to find a way to add a variety of paths: Add varience, randomness, and more path_table's to pick randomly from! +// +// - Upon spawning, head to random spawn point. +// +// - For the top of ladders with rungs (like the ladders in urban2 that have left/right poles and rungs going horizontally in the middle), the rungs don't always +// come up high enough to meet the ground above (small ladder in ground level room that has a hole in roof going up). +// +// - Add random 'action hero' bot names +// +// - Make bot remember last enemy (or enemies) they encountered each round, attack then until they're dead or we gain a new enemy. Perhaps cycle through list of enemies +// we encounter when we're not attacking. +// +// - Punch grenades (allies or enemies), or catch grenade and add to inventory +// +// - Bots aiming through water, some water opacity should limit the bot's ability to aim through water, via water +// +// - [Drg] Varekai � Today at 22:26 +// I've always wondered because we use punch to get to weird places and I imagined that how far someone went had more to do with their initial speed +// as in, hitting them as they're just jumping would be more effective +// darksaint � Today at 22:27 +// ReKTeK aka Mech � Today at 22 : 31 +// Having another quick skim of the code, it doesn't take velocity into account. However, if the player is airborne, they wouldn't be slowed down by any initial friction(if any is applied) plus the height would give extra distance +// Drg] Varekai � Today at 22 : 32 +// so it is mostly random, pretty nuts +// ReKTeK aka Mech � Today at 22 : 33 +// I suppose velocity should be taken into account +// darksaint � Today at 22 : 35 +// Kicking someone in mid - air after being kicked should result in double damage :) using their body like a football +// +// - Roundlimit: add a countdown at 30 seconds, 15 seconds... +// - Donations: name on bill board, name in credits (txt/menu), +// - Bots attack and continue to move up/down ladders... +// - Make a beep/ding/windows sound when minimised and a round has ended. +// - Bot names, Diablo 2 pre, post fix names +// - Chance to shoot weapon out of hand +// - Make maps snow + fog for xmas +// - Make maps have a snow/fog/rain/etc. effect on the bot's view +// +// Yogomaster, Shadowking, Dirty, Psy, MOG, and Rekkie are the best bots. Lord Slice [web] +// + +// Node Stuff +#define MAX_SP_NODES 256 // Maximum Spawn Spots (SP), and Spawn Spot Nodes (SPN) +#define MAX_POI_NODES 512 // Maximum POI nodes +#define MAX_DOOR_NODES 64 // Maximum Door nodes +#define MAX_VIS_NODES 320 // Maximum visibility nodes +// + + + + +// +//#define MOVE_FORWARD_LEFT 4 // +45 degrees left +//#define MOVE_FORWARD_RIGHT 5 // -45 degrees right +//#define MOVE_BACK_LEFT 6 // +135 degrees left +//#define MOVE_BACK_RIGHT 7 // -135 degrees right +// Acebot Node Stuff -- e + +#define STEPSIZE 18 + +#define MINS_PLAYER_STANDING tv(-16, -16, -24) // Player standing +#define MAXS_PLAYER_STANDING tv(16, 16, 32) + +#define MINS_PLAYER_CROUCHING tv(-16, -16, -24) // Player crouching +#define MAXS_PLAYER_SCROUCHING tv(16, 16, 4) + +//rekkie -- DEV_1 -- Gib Players -- s +extern cvar_t* gib_bodies; +extern cvar_t* gib_heads; +//rekkie -- DEV_1 -- Gib Players -- e + +extern cvar_t* bot_maxteam; +extern cvar_t* bot_rush; +extern cvar_t* bot_randvoice; +extern cvar_t* bot_randskill; +extern cvar_t* bot_randname; +//extern cvar_t* bot_randteamskin; + + + + +#define MAX_BOT_NAMES 64 +typedef struct { + char name[16]; // Name +}bot_names_t; +extern int dc_total_male_names; // Total male names +extern int dc_total_female_names; // Total female names +extern bot_names_t bot_male[MAX_BOT_NAMES]; // Cached copy +extern bot_names_t bot_female[MAX_BOT_NAMES]; // Cached copy + + + + + + + + + +// Maximum nodes +//#define NODE_MAX 15000 // Max nodes -- 25,000 +//#define NODE_MAXLINKS 16 // Maximum links per node +#define NODE_Z_HEIGHT 32 // Z height of node placement +#define NODE_Z_HALF_HEIGHT 24 // 1/2 of NODE_Z_HEIGHT +#define NODE_Z_HEIGHT_PLUS_STEPSIZE 50 // NODE_Z_HEIGHT + STEPSIZE +#define NODE_MAX_DISTANCE 999999 // Max distance for a node +#define NODE_SIZE 24 // Physical size of the node +#define NODE_GRID_SIZE 16 //(NODE_SIZE*2) // Fit nodes on grid alignment +#define NODE_MAX_JUMP_HEIGHT 60 // Maximum height that a player can jump to reach a higher level surface, such as a box +#define NODE_MAX_FALL_HEIGHT 210 // Maximum height that a player can fall from without damage +#define NODE_MAX_SAFE_CROUCH_FALL_HEIGHT 214 // Maximum height that a player can crouch fall from without damage +#define NODE_MAX_CROUCH_FALL_HEIGHT 224 // Maximum height that a player can crouch fall from without damage +#define NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE 256 // Maximum height that a player can crouch fall + minor leg damage +#define NODE_CROUCH_HEIGHT 29 // Crouch height that a player can fit under +#define MAX_STEEPNESS 0.7 // Maximum normal value - steepness of the surface (0.7) + +// NMesh constraints +#define MAX_NMESH_FACES 18432 //18432 //16384 //12192 //8096 +#define MAX_REACHABILITY 64 +#define FACETYPE_FLOOR 1 +#define FACETYPE_SLOPE 2 +#define FACETYPE_WALL 3 +//#define FACETYPE_LADDER 4 +// Triangles +typedef struct { + vec3_t v[3]; // Verts of the triangle + vec3_t center; // Centoid of the triangle + int face; // The face this triangle belongs to + int node; // Triangle node + + // Reachability of nearby triangles + int reach_triangle[MAX_REACHABILITY]; // Nearby triangles reached + vec3_t reach_origin[MAX_REACHABILITY][3]; // Nearby triangle coordinates + int reach_face[MAX_REACHABILITY]; // The face that the nearby triangle resides in + int reach_num; // The number of nearby triangles reached + + // Reachability of target triangles (jumping) + //float reach_velocity[MAX_REACHABILITY]; // The velocity required to reach the target triangle + //float reach_distance[MAX_REACHABILITY]; // The distance to the target triangle + //float reach_time[MAX_REACHABILITY]; // The time it takes to reach the target triangle + //vec3_t reach_direction[MAX_REACHABILITY]; // The direction to the target triangle + +} ntris_t; +// Verts +typedef struct { + vec3_t v; + int node; // Vert node +} nvert_t; +// Edges +typedef struct { + vec3_t v[2]; // Verts of current edge + vec3_t center; // Center of the edge + int node; // Edge node +} nedge_t; +// Faces +typedef struct nface_s { + + // Verts + int num_verts; // Verts on current face + vec3_t v[64]; // All verts of current floor surface + + // Edges + nedge_t edge[128]; // Max edges per face + int num_edges; // Edges on current face + + // Triangles + ntris_t tris[32]; // Max triangles per face + int num_tris; // Triangles on current face + + int type; // Surface type - FACETYPE_FLOOR, FACETYPE_SLOPE, FACETYPE_WALL + int drawflags; // Surface flags - SURF_SKY, SURF_TRANS33, SURF_TRANS66, ... + int contents; // Surface contents - CONTENTS_SOLID, CONTENTS_WINDOW, ... + vec3_t normal; // Surface normals + +} nface_t; +typedef struct nmesh_s { + + // Totals + int total_faces; // Faces + int total_walls; // Walls + int total_edges; // Edges + int total_verts; // Verts + int total_tris; // Triangles + int total_reach; // Triangles reached + + // Floors & Walls + nface_t face[MAX_NMESH_FACES]; // Max faces (flat, sloped, walls) + + unsigned bsp_checksum; // Map checksum + +} nmesh_t; +extern nmesh_t nmesh; + + +extern int num_poi_nodes; +extern int poi_nodes[MAX_POI_NODES]; +extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +extern int num_vis_nodes; +extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +extern int node_vis_list[10][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. +//int node_vis[MAX_PNODES][MAX_PNODES]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +//int node_vis_list[MAX_PNODES][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. + + +// Botlib A_TEAM +//void CheckBotRules(void); + + +// acebot_ai.c +qboolean ACEAI_IsEnemy(edict_t* self, edict_t* other); +short BOTLIB_EnemiesAlive(edict_t* self); +short BOTLIB_AlliesAlive(edict_t* self); + +// DC Movement +void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd); +void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd); +void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd); +float VectorDistance(vec3_t start, vec3_t end); +float VectorDistanceXY(vec3_t start, vec3_t end); + + + +// Navigation +#define ANT_FREQ 0.5 // Time gap between calls to the processor intensive search +// ----------- new Pathing Algorithm stuff ----- +qboolean AntPathMove(edict_t* ent); // Called in item and enemy route functions +void AntInitSearch(edict_t* ent); // Resets all the path lists etc. +qboolean AntStartSearch(edict_t* ent, int from, int to, qboolean path_randomization); // main entry to path algorithms +qboolean AntQuickPath(edict_t* ent, int from, int to); // backup path system +qboolean AntFindPath(edict_t* ent, int from, int to); // Optimised path system +qboolean AntLinkExists(int from, int to); // Detects if we are off the path +// --------- AI Tactics Values ------------- +enum { + AIRoam, // Basic item collection AI + AIAttack, // Basic Attack Enemy AI + AIAttackCollect,// Attack Enemy while collecting Item + AICamp, // Camp at a suitable location and collect the item on respawn + AISnipe, + AIAmbush +}; + +qboolean BOTLIB_DrawPath(edict_t* self); // Draw bot path +void SLLpush_front(botlib_sll_t* thelist, int nodedata); // Add to the front of the list +void SLLpop_front(botlib_sll_t* thelist); // Remove the iten from the front of the list +int BOTLIB_SLL_Query_Next_Node(botlib_sll_t* list); //rekkie -- Query the SLL for the next node +int BOTLIB_SLL_Query_All_Nodes(edict_t* ent, botlib_sll_t* list, int* node_list, const int max_nodes); //rekkie -- Query and return all the nodes in the list +int SLLfront(botlib_sll_t* thelist); // Get the integer value from the front of the list, without removing the item (Query the list) +void SLLpush_back(botlib_sll_t* thelist, int nodedata); // Add to the back of the list +qboolean SLLempty(botlib_sll_t* thelist); // See if the list is empty (false if not empty) +void SLLdelete(botlib_sll_t* thelist); // Free all memory from a list + + + + + +// BOTLIB Nodes +void BOTLIB_UTIL_ROTATE_CENTER(vec3_t pt1, vec3_t pt2, float rotate, float distance, vec3_t end); // Rotate around a center, move the vector out and return the new point +void BOTLIB_UTIL_NEAREST_BSP_FACE(vec3_t origin, int* floor, int* edges); // Gets the nearest BSP face and number of edges to the origin +void BOTLIB_ChangeNodeFunction(edict_t* ent); +void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd); +int BOTLIB_Reachability(int from, int to); +qboolean BOTLIB_AdvanceToSelectNode(edict_t* self, int node); +qboolean NODES_AdvanceToNextNode(edict_t* self); +void BOTLIB_InitNavigation(edict_t* ent); +int BOTLIB_AddNode(vec3_t origin, vec3_t normal, byte type); +qboolean BOTLIB_AddNodeLink(int from, int to, byte type, qboolean do_cost); +void BOTLIB_SelfExpandingNodes(edict_t* ent, int node); +void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t* ent); +void BOTLIB_LinkAllNodesTogether(edict_t* ent); +void BOTLIB_RemoveAllNodeLinksFrom(int from); + +#ifdef USE_ZLIB +void BOTLIB_SaveNavCompressed(void); +void BOTLIB_LoadNavCompressed(void); +#else +void BOTLIB_SaveNav(void); +void BOTLIB_LoadNav(void); +#endif +qboolean NodeTypeToString(edict_t* self, int type, char* string, const int max_string_size); + +//void ACEND_FreeUnsortedNodes(void); +//qboolean ACEND_InitUnsortedNodes(void); +void ACEND_BSP(edict_t* ent); +void ACEND_BuildSpawnPointNodes(void); +void ACEND_CachePointOfInterestNodes(void); +void ACEND_BuildEntNodeTable(void); +void ACEND_BuildVisibilityNodes(void); +qboolean ACEND_IsNodeVisibleToNodes(short x, short y); +short ACEND_GetRandomVisibleNode(short x); +//void ACEND_ShowAllNodes(void); +void BOTLIB_FreeNodes(void); // Free all nodes from memory +qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type); // Add node +//void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type); +void ACEND_RemoveNode(edict_t* self, int nodenum); + +qboolean ACEND_UpdateNodeReach(int from, int to); +//qboolean ACEND_FindLadder(vec3_t origin, vec3_t ladder_bottom, vec3_t ladder_top); +void ACEND_SaveAAS(qboolean update); +void ACEND_LoadAAS(qboolean force); // Load Area Awareness System. Force: generate a new AAS instead of loading from file +void GetBSPFaceTriangle(vec3_t origin, qboolean* found, int* floor, int* triangle); +qboolean LaunchPlayer(edict_t* ent, vec3_t target); +//int ACEND_JumpReachability(int from, int to, vec3_t origin, vec3_t target, vec3_t normal); + + +// DAIC Spawn +void ACESP_RemoveTeamplayBot(int team); +void DC_LoadRandomBotName(char* userinfo); +//void DC_LoadRandomBotName(byte gender, char* bot_name); + +//rekkie -- BSP -- e + + +//rekkie -- Quake3 -- s +//=============================== +// Quake 3 Botlib.h Code +//=============================== +/* +* case 0: + SetAnimation(ent, FRAME_flip01 - 1, FRAME_flip12, ANIM_WAVE); //flipoff + break; + case 1: + SetAnimation(ent, FRAME_salute01 - 1, FRAME_salute11, ANIM_WAVE); //salute + break; + case 2: + SetAnimation(ent, FRAME_taunt01 - 1, FRAME_taunt17, ANIM_WAVE); //taunt + break; + case 3: + SetAnimation(ent, FRAME_wave01 - 1, FRAME_wave11, ANIM_WAVE); //wave + break; + case 4: + default: + SetAnimation(ent, FRAME_point01 - 1, FRAME_point12, ANIM_WAVE); //point +*/ + + +//=============================== +// Quake 3 Multithreading Code +//=============================== +// qthreads.h -- https://github.com/DaemonEngine/daemonmap/blob/d91a0d828e27f75c74e5c3398b2778036e8544f6/tools/quake3/common/qthreads.h +void BOTLIB_THREAD_LOADAAS(qboolean force); // Init and run this function threaded +void BOTLIB_THREADING_LOADAAS(void* param); // Running in a thread +typedef struct loadaas_s // Struct used to send parameters to the threaded function +{ + qboolean force; +} loadaas_t; + +void BOTLIB_THREADED_DijkstraPath(edict_t* ent, int from, int to); +void BOTLIB_THREADING_DijkstraPath(void* param); +typedef struct dijkstra_path_s // Struct used to send parameters to the threaded function +{ + edict_t *ent; + int from; + int to; +} dijkstra_path_t; +// +extern int numthreads; +void ThreadSetDefault(void); +int GetThreadWork(void); +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func)(int)); +//void RunThreadsOnIndividual(int workcnt, qboolean showpacifier, void (*func), void *param); +//void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); +void RunThreadsOn(int workcnt, qboolean showpacifier, void (*func)(int)); +//void RunThreadsOn(int workcnt, qboolean showpacifier, void(*func), void* param); +void ThreadLock(void); +void ThreadUnlock(void); +//rekkie -- Quake3 -- e + +#endif diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index de62f7e8b..65cce45fd 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -75,7 +75,7 @@ typedef struct bot_input_s // =========================================================================== void BOTLIB_Init(edict_t* self); // Initializing... HAL9000 is online. void BOTLIB_Think(edict_t* self); // Thinking... I'm sorry Rekkie, I can't do that. -void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time); // Translates bot input to actual Q2 movement calls +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, vec3_t delta_angles, int time); // Translates bot input to actual Q2 movement calls qboolean BOTLIB_Infront(edict_t* self, edict_t* other, float amount); // I see everything... in front of me qboolean BOTLIB_OriginInfront(edict_t* self, vec3_t origin, float amount); qboolean BOTLIB_MovingToward(edict_t* self, vec3_t origin, float amount); diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 9a3c9f646..9a40fca7b 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -104,7 +104,7 @@ void BOTLIB_Init(edict_t* self) // BOTLIB_BotInputToUserCommand // Translates bot input to actual Q2 movement calls //============== -void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, int delta_angles[3], int time) +void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd, vec3_t delta_angles, int time) { vec3_t angles, forward, right; float f, r, u, m; diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 9751484b8..c95deb9e5 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -31,123 +31,99 @@ qboolean NodeTypeToString(edict_t* self, int type, char *string, const int max_s switch (type) { case NODE_MOVE: - len = strlen("MOVE"); // Get the length of the string - len = len < max_string_size ? len : max_string_size - 1; // If the string is too long, truncate it - strncpy(string, "MOVE", len); // Copy the string + strncpy(string, "MOVE", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_CROUCH: - len = strlen("CROUCH"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "CROUCH", len); + strncpy(string, "CROUCH", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_BOXJUMP: - len = strlen("NODE_BOXJUMP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "NODE_BOXJUMP", len); + strncpy(string, "NODE_BOXJUMP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_JUMP: - len = strlen("JUMP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "JUMP", len); + strncpy(string, "JUMP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_JUMPPAD: - len = strlen("JUMPPAD"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "JUMPPAD", len); + strncpy(string, "JUMPPAD", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_LADDER: - len = strlen("LADDER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER", len); + strncpy(string, "LADDER", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_POI: - len = strlen("POI"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "POI", len); + strncpy(string, "POI", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_POI_LOOKAT: - len = strlen("NODE_POI_LOOKAT"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "NODE_POI_LOOKAT", len); + strncpy(string, "NODE_POI_LOOKAT", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_WATER: - len = strlen("WATER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "WATER", len); + strncpy(string, "WATER", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_STEP: - len = strlen("STEP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "STEP", len); + strncpy(string, "STEP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_STAND_DROP: - len = strlen("STAND_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "STAND_DROP", len); + strncpy(string, "STAND_DROP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_CROUCH_DROP: - len = strlen("CROUCH_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "CROUCH_DROP", len); + strncpy(string, "CROUCH_DROP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_UNSAFE_DROP: - len = strlen("UNSAFE_DROP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "UNSAFE_DROP", len); + strncpy(string, "UNSAFE_DROP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_LADDER_UP: - len = strlen("LADDER_UP"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER_UP", len); + strncpy(string, "LADDER_UP", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_LADDER_DOWN: - len = strlen("LADDER_DOWN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LADDER_DOWN", len); + strncpy(string, "LADDER_DOWN", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_DOOR: - len = strlen("DOOR"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "DOOR", len); + strncpy(string, "DOOR", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_PLATFORM: - len = strlen("PLATFORM"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "PLATFORM", len); + strncpy(string, "PLATFORM", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_TELEPORTER: - len = strlen("TELEPORTER"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "TELEPORTER", len); + strncpy(string, "TELEPORTER", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_ITEM: - len = strlen("ITEM"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "ITEM", len); + strncpy(string, "ITEM", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_GRAPPLE: - len = strlen("GRAPPLE"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "GRAPPLE", len); + strncpy(string, "GRAPPLE", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_SPAWNPOINT: - len = strlen("SPAWNPOINT"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "SPAWNPOINT", len); + strncpy(string, "SPAWNPOINT", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; case NODE_LEARN: - len = strlen("LEARN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "LEARN", len); + strncpy(string, "LEARN", max_string_size - 1); + string[max_string_size - 1] = '\0'; break; default: - len = strlen("UNKNOWN"); - len = len < max_string_size ? len : max_string_size - 1; - strncpy(string, "UNKNOWN", len); - string[len] = '\0'; // Terminate string + strncpy(string, "UNKNOWN", max_string_size - 1); + string[max_string_size - 1] = '\0'; // Ensure null-termination for all paths return false; // Unknown node type } @@ -5366,7 +5342,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (item_dist < 128) self->bot.bi.speed = SPEED_ROAM; // Slow down when close - if (self->bot.get_item->solid == SOLID_NOT); // picked up + if (self->bot.get_item->solid == SOLID_NOT) // picked up { //Com_Printf("%s %s grabbed item %s [%f]\n", __func__, self->client->pers.netname, self->bot.get_item->classname, item_dist); self->bot.get_item = NULL; @@ -5906,9 +5882,9 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Otherwise pick from various wait times before moving out int rnd_rng = rand() % 4; if (rnd_rng == 0) - self->just_spawned_timeout = level.framenum + (random() * 10) * HZ; // Long wait + self->just_spawned_timeout = level.framenum + (random() * 6) * HZ; // Long wait else if (rnd_rng == 1) - self->just_spawned_timeout = level.framenum + (random() * 5) * HZ; // Medium wait + self->just_spawned_timeout = level.framenum + (random() * 4) * HZ; // Medium wait else if (rnd_rng == 2) self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait else @@ -5949,13 +5925,13 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) //self->bot.bi.speed = 0; //return; - Com_Printf("%s %s on ground and current_node is invalid; try to wander\n", __func__, self->client->pers.netname); + Com_Printf("%s %s on ground and current_node (%i) is invalid; try to wander\n", __func__, self->client->pers.netname, self->bot.current_node); self->bot.bi.speed = 0; self->bot.stuck_wander_time = 1; } if (self->groundentity && self->bot.goal_node == INVALID)// && self->bot.node_travel_time >= 15) { - Com_Printf("%s %s on ground and goal_node is invalid; find a new path\n", __func__, self->client->pers.netname); + Com_Printf("%s %s on ground and goal_node (%i) is invalid; find a new path\n", __func__, self->client->pers.netname, self->bot.goal_node); self->bot.bi.speed = 0; self->bot.state = BOT_MOVE_STATE_NAV; return; @@ -5997,7 +5973,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) } // If travel time took too long, assume we're stuck - if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node + if (self->bot.node_travel_time >= 60) //60 // Bot failure to reach next node { self->bot.stuck_wander_time = 1; Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 006d78610..e47a3d81f 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -631,11 +631,14 @@ void BOTLIB_InitAreaConnections(void) { targetNode = nodes[n].links[l].targetNode; targetArea = nodes[targetNode].area; + + // ## darksaint: removed area check for creating adjacency matrix + //gi.dprintf("Node[%d] Area[%d] connects to Node[%d] Area[%d]\n", n, i, targetNode, targetArea); //if (targetArea != INVALID && targetArea != i) - if (targetArea != i) - { - nav_area.adjacency_matrix[i][targetArea] = targetArea; - } + // if (targetArea != i) + // { + nav_area.adjacency_matrix[i][targetArea] = targetArea; + //} } } } @@ -796,37 +799,36 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz self->bot.node_list_count = 0; // New pathing - if area nodes are supported - if (nav_area.total_areas > 0 && goal_node) - { - Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); - if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path - { - while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path - { - } - - // Add the goal to the end of the node list + terminate - if (self->bot.node_list_count) - { - self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node - self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate - } - - return true; // Success - } - else - { - Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); - return false; // Failure - } - } - else // Fallback to old pathing - { + // if (nav_area.total_areas > 0 && goal_node) + // { + // Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); + // if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path + // { + // while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path + // { + // } + + // // Add the goal to the end of the node list + terminate + // if (self->bot.node_list_count) + // { + // self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + // self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + // } + + // return true; // Success + // } + // else + // { + // Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); + // return false; // Failure + // } + // } + // else // Fallback to old pathing + // { self->bot.goal_node = INVALID; //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) { - // Add the goal to the end of the node list + terminate if (self->bot.node_list_count) { @@ -836,7 +838,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz return true; // Success } - } + //} Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); return false; // Failure to find path diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index cee4e79d2..ee2c2b57d 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -7,7 +7,7 @@ int poi_nodes[MAX_POI_NODES]; edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) int num_vis_nodes; int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -int node_vis_list[10][10]; // Cached node +int node_vis_list[10][MAX_VIS_NODES]; // Cached node node_t *nodes; nmesh_t nmesh; @@ -250,63 +250,6 @@ qboolean DAIC_Add_Node(vec3_t origin, vec3_t normal, byte type) return true; } -/* -/////////////////////////////////////////////////////////////////////// -// Makes a duplicate copy into the unsorted_nodes[] array -/////////////////////////////////////////////////////////////////////// -void ACEND_DuplicateNodes(int node_num) -{ - node_t* unsorted_node = (unsorted_nodes + node_num); // Get our node by pointer arithmatic - - VectorCopy(nodes[node_num].origin, unsorted_node->origin); // Location - VectorCopy(nodes[node_num].normal, unsorted_node->normal); // Surface Normal - unsorted_node->type = nodes[node_num].type; // Node type - unsorted_node->nodenum = nodes[node_num].nodenum; // Node number - - for (int i = 0; i < MAXLINKS; i++) // Init links - { - unsorted_node->links[i].targetNode = INVALID; // Invalidate all links - unsorted_node->links[i].cost = INVALID; // Invalidate all costs - } -} -void ACEND_AutoAddNode(vec3_t origin, vec3_t normal, byte type) -{ - // Sanity check - if (numnodes + 1 > MAX_NODES) - { - debug_printf("%s exceeded max nodes, the limit is %d\n", __func__, MAX_NODES); - return; - } - - //Com_Printf("%s Adding node %i at [%f %f %f]\n", __func__, numnodes, origin[0], origin[1], origin[2]); - - VectorCopy(origin, nodes[numnodes].origin); // Location - VectorCopy(normal, nodes[numnodes].normal); // Surface Normal - nodes[numnodes].type = type; // Node type - nodes[numnodes].nodenum = numnodes; // Node number - - //node_ents[numnodes] = self; // Add door as an entity reference //rekkie -- DEV_1 - - // Init links - for (int i = 0; i < MAXLINKS; i++) - { - nodes[numnodes].links[i].targetNode = INVALID; // Link - nodes[numnodes].links[i].targetNodeType = INVALID; // Type - memset(nodes[numnodes].links[i].targetVelocity, 0, sizeof(vec3_t)); // Velocity - nodes[numnodes].links[i].cost = INVALID; // Cost - } - - // Make a duplicate copy of nodes[] to *unsorted_nodes - // We'll use this later to optimise links - ACEND_DuplicateNodes(numnodes); - - // Show ever 10nth node - //if (numnodes % 10 == 0) - // ACEND_ShowNode(numnodes); - - numnodes++; // Update the node counter -} -*/ /////////////////////////////////////////////////////////////////////// // Remove node by number, @@ -675,6 +618,7 @@ void BOTLIB_RemoveNodeLink(edict_t* self, int from, int to) // build_new_path: If we're building a new path or adding to an existing path qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomization, int area, qboolean build_new_path) { + //gi.dprintf("All parameters: goal_node[%d] path_randomization[%d] area[%d] build_new_path[%d]\n", goal_node, path_randomization, area, build_new_path); // Always update current node self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); @@ -4837,7 +4781,7 @@ void BOTLIB_Process_NMesh(edict_t* ent) float pt1_to_pt2_dist = VectorLength(pt1_to_pt2_vec); // Distance between pt1 and pt2 // Skip excessively small edges - if (pt1_to_pt2_vec < 4) + if (pt1_to_pt2_dist < 4) { nmesh.face[f].edge[e].node = INVALID; continue; @@ -4881,10 +4825,6 @@ void BOTLIB_Process_NMesh(edict_t* ent) nmesh.face[f].edge[e].center[1] += adjusted_height[1]; nmesh.face[f].edge[e].center[2] += adjusted_height[2]; - - - - // Test for edges // Some edges are on a floor, others from a wall, therefore we test both here if (0) @@ -4892,7 +4832,6 @@ void BOTLIB_Process_NMesh(edict_t* ent) qboolean hit_step_left = false, hit_step_right = false; vec3_t end_left, end_right; - BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, 90, 1, end_left); // Turn 90 degrees left and move 1 unit BOTLIB_UTIL_ROTATE_CENTER(pt1, pt2, -90, 1, end_right); // Turn 90 degrees right and move 1 unit @@ -5072,10 +5011,6 @@ void BOTLIB_Process_NMesh(edict_t* ent) continue; } - - - - // Find the middle between two points of an edge //LerpVector(pt1, pt2, 0.50, center); // 50% (center) diff --git a/src/action/p_client.c b/src/action/p_client.c index f9b43f052..cd7f93b1a 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -2984,119 +2984,67 @@ void PutClientInServer(edict_t * ent) for (i = 0; i < 3; i++) client->ps.pmove.delta_angles[i] = ANGLE2SHORT(ent->s.angles[i] - client->resp.cmd_angles[i]); + #ifndef NO_BOTS - ent->last_node = -1; - ent->is_jumping = false; - ent->is_triggering = false; - ent->grenadewait = 0; - ent->react = 0.f; - - if( ent->is_bot ) + + //rekkie -- s + if (ent->is_bot) { - ent->classname = "bot"; - - ent->enemy = NULL; - ent->movetarget = NULL; - if( ! teamplay->value ) - { - ent->state = STATE_MOVE; - ent->botState = BS_ROAM; - ent->nextState = BS_ROAM; - ent->secondaryState = BSS_NONE; - } - else + if (ent->bot.bot_type == BOT_TYPE_BOTLIB) // BOTLIB { - ent->state = STATE_POSITION; - ent->botState = BS_ROAM; - ent->nextState = BS_ROAM; - ent->secondaryState = BSS_POSITION; - } - - // Set the current node - ent->current_node = ACEND_FindClosestReachableNode( ent, NODE_DENSITY, NODE_ALL ); - ent->goal_node = ent->current_node; - ent->next_node = ent->current_node; - ent->next_move_time = level.framenum; - ent->suicide_timeout = level.framenum + 15.0 * HZ; - - ent->killchat = false; - VectorClear( ent->lastSeen ); - ent->cansee = false; - - ent->bot_strafe = SPEED_WALK; - ent->bot_speed = 0; - VectorClear( ent->lastPosition ); - - // Choose Teamplay weapon - switch( ent->weaponchoice - 1 ) // Range is 1..5 - { - case 0: - ACEAI_Cmd_Choose_Weapon_Num( ent, MP5_NUM ); - break; - case 1: - ACEAI_Cmd_Choose_Weapon_Num( ent, M4_NUM ); - break; - case 2: - ACEAI_Cmd_Choose_Weapon_Num( ent, M3_NUM ); - break; - case 3: - ACEAI_Cmd_Choose_Weapon_Num( ent, HC_NUM ); - break; - case 4: - ACEAI_Cmd_Choose_Weapon_Num( ent, SNIPER_NUM ); - break; - default: - ACEAI_Cmd_Choose_Weapon_Num( ent, 0 ); // Random allowed. - break; - } - - // Choose Teamplay equipment - switch( ent->equipchoice - 1 ) // Range is 1..5 - { - case 0: - ACEAI_Cmd_Choose_Item_Num( ent, SIL_NUM ); - break; - case 1: - ACEAI_Cmd_Choose_Item_Num( ent, SLIP_NUM ); - break; - case 2: - ACEAI_Cmd_Choose_Item_Num( ent, BAND_NUM ); - break; - case 3: - ACEAI_Cmd_Choose_Item_Num( ent, KEV_NUM ); - break; - case 4: - ACEAI_Cmd_Choose_Item_Num( ent, LASER_NUM ); - break; - default: - ACEAI_Cmd_Choose_Item_Num( ent, 0 ); // Random allowed. - break; + BOTLIB_Init(ent); // Initialize all the bot variables + BOTLIB_SmartWeaponSelection(ent); } - - if( teamplay->value ) + else // LTK bots { - int randomnode = 0; - const char *s = Info_ValueForKey( ent->client->pers.userinfo, "skin" ); - AssignSkin( ent, s, false /* nickChanged */ ); - // Anti centipede timer - ent->teamPauseTime = level.framenum + (3.0 + (rand() % 7)) * HZ; - // Radio setup - ent->teamReportedIn = true; - ent->lastRadioTime = level.framenum; - // Change facing angle for each bot - randomnode = (int)( num_players * random() ); - VectorSubtract( nodes[randomnode].origin, ent->s.origin, ent->move_vector ); - ent->move_vector[2] = 0; + ent->bot.bot_type = BOT_TYPE_LTK; // Set LTK + //rekkie -- e + + ent->classname = "bot"; + + ent->last_node = -1; + ent->is_jumping = false; + ent->is_triggering = false; + ent->grenadewait = 0; + ent->react = 0.f; + + ent->next_move_time = level.framenum; + ent->suicide_timeout = level.framenum + 15.0 * HZ; + + ent->killchat = false; + VectorClear(ent->lastSeen); + ent->cansee = false; + + ent->bot_speed = 0; + VectorClear(ent->lastPosition); + + if (teamplay->value) + { + const char* s = Info_ValueForKey(ent->client->pers.userinfo, "skin"); + AssignSkin(ent, s, false /* nickChanged */); + // Anti centipede timer + ent->teamPauseTime = level.framenum + (3.0 + (rand() % 7)) * HZ; + // Radio setup + ent->teamReportedIn = true; + ent->lastRadioTime = level.framenum; + // Change facing angle for each bot + if (numnodes) + { + int randomnode = (int)(num_players * random()); + VectorSubtract(nodes[randomnode].origin, ent->s.origin, ent->move_vector); + } + ent->move_vector[2] = 0; + } + else + ent->teamPauseTime = level.framenum; + + //RiEvEr - new node pathing system + memset(&(ent->pathList), 0, sizeof(ent->pathList)); + ent->pathList.head = ent->pathList.tail = NULL; + //R + + ent->client->resp.radio.gender = (ent->client->pers.gender == GENDER_FEMALE) ? 1 : 0; } - else - ent->teamPauseTime = level.framenum; - - //RiEvEr - new node pathing system - memset( &(ent->pathList), 0, sizeof(ent->pathList) ); - ent->pathList.head = ent->pathList.tail = NULL; - //R - - ent->client->resp.radio.gender = (ent->client->pers.gender == GENDER_FEMALE) ? 1 : 0; } if( (! ent->is_bot) || (ent->think != ACESP_HoldSpawn) ) // if( respawn ) @@ -5879,7 +5827,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) else if (lower != 99999) // Node is lower than origin Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_dn: %f\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, lower); else // Node at equal dist to origin - Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_eq: %f\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, 0); + Com_Printf("%s n[%i to %i] - t[%i to %i] - dist %f z_eq: %d\n", __func__, ent->show_node_links, targetNode, nodes[ent->show_node_links].type, nodes[targetNode].type, dist, 0); } } } From 00103c3e403008eaa519e5821412e1d285c36344 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 19 Jul 2024 17:53:00 +0300 Subject: [PATCH 353/974] Don't relink player entities when seeking. --- src/server/mvd/parse.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index d66f9bff2..c880114e8 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -632,7 +632,9 @@ static void MVD_PlayerToEntityStates(mvd_t *mvd) Com_PlayerToEntityState(&player->ps, &edict->s); - MVD_LinkEdict(mvd, edict); + if (!mvd->demoseeking) { + MVD_LinkEdict(mvd, edict); + } } } From c88248d1327d746fcfb3a0880b32b9a3a8c53dfa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 19 Jul 2024 18:58:42 +0300 Subject: [PATCH 354/974] Simplify MSG_ParseDeltaPlayerstate_Packet() signature. --- inc/common/msg.h | 2 +- src/common/msg.c | 10 +--------- src/server/mvd/parse.c | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/inc/common/msg.h b/inc/common/msg.h index 5d5a7538d..4fb524a58 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -162,7 +162,7 @@ void MSG_ParseDeltaEntity(entity_state_t *to, entity_state_extension_t *ext, void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, player_state_t *to, int flags, msgPsFlags_t psflags); void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, player_state_t *to, int flags, int extraflags, msgPsFlags_t psflags); #endif -void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, player_state_t *to, int flags, msgPsFlags_t psflags); +void MSG_ParseDeltaPlayerstate_Packet(player_state_t *to, int flags, msgPsFlags_t psflags); #if USE_DEBUG #if USE_CLIENT diff --git a/src/common/msg.c b/src/common/msg.c index b70bf2476..dd63216f9 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -2464,20 +2464,12 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, MSG_ParseDeltaPlayerstate_Packet =================== */ -void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, - player_state_t *to, +void MSG_ParseDeltaPlayerstate_Packet(player_state_t *to, int flags, msgPsFlags_t psflags) { Q_assert(to); - // clear to old value before delta parsing - if (!from) { - memset(to, 0, sizeof(*to)); - } else if (to != from) { - memcpy(to, from, sizeof(*to)); - } - // // parse the pmove_state_t // diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index c880114e8..cc5e4395e 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -747,7 +747,7 @@ static void MVD_ParsePacketPlayers(mvd_t *mvd) } #endif - MSG_ParseDeltaPlayerstate_Packet(&player->ps, &player->ps, bits, mvd->psFlags); + MSG_ParseDeltaPlayerstate_Packet(&player->ps, bits, mvd->psFlags); if (bits & PPS_REMOVE) { SHOWNET(2, "%3u:remove:%d\n", readcount, number); From 34e89db5262989532a2004e5fb4ffbd3ff7bf243 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 20 Jul 2024 19:28:47 +0300 Subject: [PATCH 355/974] Reduce tearing of some client effects. --- inc/shared/shared.h | 11 +++++++++++ src/client/client.h | 4 ++++ src/client/entities.c | 4 ++-- src/client/main.c | 2 ++ src/client/tent.c | 12 ++++++------ src/refresh/gl.h | 3 +++ src/refresh/tess.c | 16 ---------------- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 1cd3b127c..11c486d98 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -358,6 +358,17 @@ void Q_srand(uint32_t seed); uint32_t Q_rand(void); uint32_t Q_rand_uniform(uint32_t n); +static inline uint32_t Q_rand_state(uint32_t *seed) +{ + uint32_t x = *seed; + + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + + return *seed = x; +} + static inline int Q_clip(int a, int b, int c) { if (a < b) diff --git a/src/client/client.h b/src/client/client.h index 20814ca3e..40ece22c1 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -236,6 +236,8 @@ typedef struct { float keylerpfrac; #endif + uint32_t rand_seed; + refdef_t refdef; float fov_x; // interpolated float fov_y; // derived from fov_x assuming 4/3 aspect ratio @@ -609,6 +611,8 @@ static inline void CL_AdvanceValue(float *restrict val, float target, float spee // main.c // +#define CL_rand() Q_rand_state(&cl.rand_seed) + void CL_Init(void); void CL_Quit_f(void); void CL_Disconnect(error_type_t type); diff --git a/src/client/entities.c b/src/client/entities.c index 9c9d15ff3..804ebd73c 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -666,7 +666,7 @@ static void CL_AddPacketEntities(void) if (renderfx & RF_BEAM) { // the four beam colors are encoded in 32 bits of skinnum (hack) ent.alpha = 0.30f; - ent.skinnum = (s1->skinnum >> ((Q_rand() % 4) * 8)) & 0xff; + ent.skinnum = (s1->skinnum >> ((CL_rand() % 4) * 8)) & 0xff; ent.model = 0; } else { // set skin @@ -977,7 +977,7 @@ static void CL_AddPacketEntities(void) } else if (effects & EF_TRAP) { ent.origin[2] += 32; CL_TrapParticles(cent, ent.origin); - i = (Q_rand() % 100) + 100; + i = (CL_rand() % 100) + 100; V_AddLight(ent.origin, i, 1, 0.8f, 0.1f); } else if (effects & EF_FLAG1) { CL_FlagTrail(cent->lerp_origin, ent.origin, 242); diff --git a/src/client/main.c b/src/client/main.c index 046ce8a72..8c6c3fac6 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -3265,6 +3265,8 @@ unsigned CL_Frame(unsigned msec) if (cls.state == ca_active && !sv_paused->integer) CL_SetClientTime(); + cl.rand_seed = cl.time / 16; + #if USE_AUTOREPLY // check for version reply CL_CheckForReply(); diff --git a/src/client/tent.c b/src/client/tent.c index c6de5cfaf..26b1b7453 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -785,7 +785,7 @@ void CL_DrawBeam(const vec3_t start, const vec3_t end, qhandle_t model) ent.flags = RF_FULLBRIGHT; ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; V_AddEntity(&ent); return; } @@ -801,11 +801,11 @@ void CL_DrawBeam(const vec3_t start, const vec3_t end, qhandle_t model) ent.flags = RF_FULLBRIGHT; ent.angles[0] = -angles[0]; ent.angles[1] = angles[1] + 180.0f; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; } else { ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; } V_AddEntity(&ent); @@ -967,7 +967,7 @@ static void CL_AddPlayerBeams(void) ent.flags = RF_FULLBRIGHT; ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; V_AddEntity(&ent); continue; } @@ -989,11 +989,11 @@ static void CL_AddPlayerBeams(void) ent.flags = RF_FULLBRIGHT; ent.angles[0] = -angles[0]; ent.angles[1] = angles[1] + 180.0f; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; } else { ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = Q_rand() % 360; + ent.angles[2] = CL_rand() % 360; } V_AddEntity(&ent); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index ea0f80786..4e2ae9830 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -236,6 +236,9 @@ extern cvar_t *gl_vertexlight; extern cvar_t *gl_lightgrid; extern cvar_t *gl_showerrors; +#define GL_rand() Q_rand_state(&glr.rand_seed) +#define GL_frand() ((int32_t)GL_rand() * 0x1p-32f + 0.5f) + typedef enum { CULL_OUT, CULL_IN, diff --git a/src/refresh/tess.c b/src/refresh/tess.c index e4c0ea7f4..a044102fe 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -211,22 +211,6 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col #define MAX_LIGHTNING_SEGMENTS 7 #define MIN_SEGMENT_LENGTH 10 -static uint32_t GL_rand(void) -{ - uint32_t x = glr.rand_seed; - - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - - return glr.rand_seed = x; -} - -static float GL_frand(void) -{ - return (int32_t)GL_rand() * 0x1p-32f + 0.5f; -} - static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t color, float width) { vec3_t d1, segments[MAX_LIGHTNING_SEGMENTS - 1]; From 3fd588f7aab4be4c9f09118a4160092672bf63ac Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 20 Jul 2024 20:39:49 +0300 Subject: [PATCH 356/974] Remove legacy color shell palette defines. --- inc/refresh/refresh.h | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 0309b8741..a501ab7b6 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -29,23 +29,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define POWERSUIT_SCALE 4.0f #define WEAPONSHELL_SCALE 0.5f -#define SHELL_RED_COLOR 0xF2 -#define SHELL_GREEN_COLOR 0xD0 -#define SHELL_BLUE_COLOR 0xF3 - -#define SHELL_RG_COLOR 0xDC -//#define SHELL_RB_COLOR 0x86 -#define SHELL_RB_COLOR 0x68 -#define SHELL_BG_COLOR 0x78 - -//ROGUE -#define SHELL_DOUBLE_COLOR 0xDF // 223 -#define SHELL_HALF_DAM_COLOR 0x90 -#define SHELL_CYAN_COLOR 0x72 -//ROGUE - -#define SHELL_WHITE_COLOR 0xD7 - #define RF_SHELL_MASK (RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE | \ RF_SHELL_DOUBLE | RF_SHELL_HALF_DAM | RF_SHELL_LITE_GREEN) From f2661cc7380f5ab9f487526e36a3c22daa47b1f9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 20 Jul 2024 21:19:18 +0300 Subject: [PATCH 357/974] Read entity frame numbers as unsigned. For backward compatibility allow negative RF_BEAM width. --- inc/refresh/refresh.h | 4 ++-- src/common/msg.c | 2 +- src/refresh/main.c | 2 +- src/refresh/mesh.c | 12 ++++++------ src/refresh/tess.c | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index a501ab7b6..87113fe2f 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -42,13 +42,13 @@ typedef struct entity_s { ** most recent data */ vec3_t origin; // also used as RF_BEAM's "from" - int frame; // also used as RF_BEAM's diameter + unsigned frame; // also used as RF_BEAM's diameter /* ** previous data for lerping */ vec3_t oldorigin; // also used as RF_BEAM's "to" - int oldframe; + unsigned oldframe; /* ** misc diff --git a/src/common/msg.c b/src/common/msg.c index dd63216f9..a0fe813e6 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -2041,7 +2041,7 @@ void MSG_ParseDeltaEntity(entity_state_t *to, if (bits & U_FRAME8) to->frame = MSG_ReadByte(); if (bits & U_FRAME16) - to->frame = MSG_ReadShort(); + to->frame = MSG_ReadWord(); if ((bits & U_SKIN32) == U_SKIN32) to->skinnum = MSG_ReadLong(); diff --git a/src/refresh/main.c b/src/refresh/main.c index 4d1d18392..3a4e5b8c5 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -334,7 +334,7 @@ void GL_RotateForEntity(void) static void GL_DrawSpriteModel(const model_t *model) { const entity_t *e = glr.ent; - const mspriteframe_t *frame = &model->spriteframes[(unsigned)e->frame % model->numframes]; + const mspriteframe_t *frame = &model->spriteframes[e->frame % model->numframes]; const image_t *image = frame->image; const float alpha = (e->flags & RF_TRANSLUCENT) ? e->alpha : 1; int bits = GLS_DEPTHMASK_FALSE; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 9635f751b..4f0520de5 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -18,8 +18,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gl.h" -static int oldframenum; -static int newframenum; +static unsigned oldframenum; +static unsigned newframenum; static float frontlerp; static float backlerp; static vec3_t origin; @@ -760,14 +760,14 @@ void GL_DrawAliasModel(const model_t *model) int i; newframenum = ent->frame; - if (newframenum < 0 || newframenum >= model->numframes) { - Com_DPrintf("%s: no such frame %d\n", __func__, newframenum); + if (newframenum >= model->numframes) { + Com_DPrintf("%s: no such frame %u\n", __func__, newframenum); newframenum = 0; } oldframenum = ent->oldframe; - if (oldframenum < 0 || oldframenum >= model->numframes) { - Com_DPrintf("%s: no such oldframe %d\n", __func__, oldframenum); + if (oldframenum >= model->numframes) { + Com_DPrintf("%s: no such oldframe %u\n", __func__, oldframenum); oldframenum = 0; } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index a044102fe..74e57595a 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -278,7 +278,7 @@ void GL_DrawBeams(void) } color.u8[3] *= ent->alpha; - width = ent->frame * 1.2f; + width = abs((int16_t)ent->frame) * 1.2f; if (ent->flags & RF_GLOW) { GL_DrawLightningBeam(start, end, color, width); @@ -395,7 +395,7 @@ static int GL_CopyVerts(const mface_t *surf) static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) { if (q_unlikely(tex->next)) { - unsigned c = (unsigned)glr.ent->frame % tex->numframes; + unsigned c = glr.ent->frame % tex->numframes; while (c) { tex = tex->next; From cc9825034ae654b5d4674295cc924aeabbb75eda Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 20 Jul 2024 21:43:28 +0300 Subject: [PATCH 358/974] Wrap alias model frame numbers in extended mode. So that EF_ANIM* flags can be used for alias models too. --- inc/refresh/refresh.h | 1 + src/client/view.c | 1 + src/refresh/mesh.c | 23 ++++++++++++++--------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 87113fe2f..a25311725 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -93,6 +93,7 @@ typedef struct { vec4_t damage_blend; // rgba 0-1 damage blend float time; // time is uesed to auto animate int rdflags; // RDF_UNDERWATER, etc + bool extended; byte *areabits; // if not NULL, only areas with set bits will be drawn diff --git a/src/client/view.c b/src/client/view.c index d57a7bd09..b4d8436e3 100644 --- a/src/client/view.c +++ b/src/client/view.c @@ -430,6 +430,7 @@ void V_RenderView(void) cl.refdef.dlights = r_dlights; cl.refdef.lightstyles = r_lightstyles; cl.refdef.rdflags = cl.frame.ps.rdflags; + cl.refdef.extended = cl.csr.extended; // sort entities for better cache locality qsort(cl.refdef.entities, cl.refdef.num_entities, sizeof(cl.refdef.entities[0]), entitycmpfnc); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 4f0520de5..73b632e61 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -759,16 +759,21 @@ void GL_DrawAliasModel(const model_t *model) void (*tessfunc)(const maliasmesh_t *); int i; - newframenum = ent->frame; - if (newframenum >= model->numframes) { - Com_DPrintf("%s: no such frame %u\n", __func__, newframenum); - newframenum = 0; - } + if (glr.fd.extended) { + newframenum = ent->frame % model->numframes; + oldframenum = ent->oldframe % model->numframes; + } else { + newframenum = ent->frame; + if (newframenum >= model->numframes) { + Com_DPrintf("%s: no such frame %u\n", __func__, newframenum); + newframenum = 0; + } - oldframenum = ent->oldframe; - if (oldframenum >= model->numframes) { - Com_DPrintf("%s: no such oldframe %u\n", __func__, oldframenum); - oldframenum = 0; + oldframenum = ent->oldframe; + if (oldframenum >= model->numframes) { + Com_DPrintf("%s: no such oldframe %u\n", __func__, oldframenum); + oldframenum = 0; + } } backlerp = ent->backlerp; From 5893b81b9d320e5213788a2381a88b2130f0aeff Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 21 Jul 2024 13:04:38 +0300 Subject: [PATCH 359/974] Add command to extract file from packfile. --- src/common/tests.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/common/tests.c b/src/common/tests.c index 11d8e668e..596720e4d 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -826,6 +826,35 @@ static void Com_NextPathTest_f(void) } } +static void Com_Extract_f(void) +{ + char *path; + void *data; + int len, ret; + + if (Cmd_Argc() < 2) { + Com_Printf("Usage: %s \n", Cmd_Argv(0)); + return; + } + + path = Cmd_Argv(1); + len = FS_LoadFileEx(path, &data, FS_TYPE_PAK, TAG_FILESYSTEM); + if (!data) { + if (len == Q_ERR(ENOENT) && FS_FileExistsEx(path, FS_TYPE_REAL)) + Com_Printf("%s is not in a pack file\n", path); + else + Com_Printf("Couldn't extract %s: %s\n", path, Q_ErrorString(len)); + return; + } + + ret = FS_WriteFile(path, data, len); + FS_FreeFile(data); + if (ret) + Com_Printf("Couldn't write %s: %s\n", path, Q_ErrorString(ret)); + else + Com_Printf("Extracted %s (%d bytes)\n", path, len); +} + static const cmdreg_t c_test[] = { { "error", Com_Error_f }, { "errordrop", Com_ErrorDrop_f }, @@ -850,6 +879,7 @@ static const cmdreg_t c_test[] = { { "mdfoursum", Com_MdfourSum_f }, { "extcmptest", Com_ExtCmpTest_f }, { "nextpathtest", Com_NextPathTest_f }, + { "extract", Com_Extract_f }, { NULL } }; From 0dc71b823ff8c2881df9f701b594c3a0a2617532 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 21 Jul 2024 16:29:03 +0300 Subject: [PATCH 360/974] Allow MD5 models with more vertices. --- src/refresh/gl.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 4e2ae9830..5651585d4 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -324,7 +324,7 @@ typedef struct { #define MD5_MAX_JOINTS 256 #define MD5_MAX_JOINTNAME 32 #define MD5_MAX_MESHES 32 -#define MD5_MAX_WEIGHTS 4096 +#define MD5_MAX_WEIGHTS 8192 #define MD5_MAX_FRAMES 1024 /* Joint */ @@ -668,7 +668,7 @@ extern cvar_t *gl_intensity; * gl_tess.c * */ -#define TESS_MAX_VERTICES 4096 +#define TESS_MAX_VERTICES 6144 #define TESS_MAX_INDICES (3 * TESS_MAX_VERTICES) typedef struct { From 182b9db71c18f60be1a74bbe1e070248f885a935 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 21 Jul 2024 16:30:25 +0300 Subject: [PATCH 361/974] Support drawing remaster health bars. --- src/client/screen.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/client/screen.c b/src/client/screen.c index b75dbd55f..b803a1ebb 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1634,6 +1634,21 @@ static void SCR_SkipToEndif(const char **s) } } +static void SCR_DrawHealthBar(int x, int y, int value) +{ + if (!value) + return; + + int bar_width = scr.hud_width / 3; + float percent = (value - 1) / 254.0f; + int w = bar_width * percent + 0.5f; + int h = CHAR_HEIGHT / 2; + + x -= bar_width / 2; + R_DrawFill8(x, y, w, h, 240); + R_DrawFill8(x + w, y, bar_width - w, h, 4); +} + static void SCR_ExecuteLayoutString(const char *s) { char buffer[MAX_QPATH]; @@ -1973,6 +1988,26 @@ static void SCR_ExecuteLayoutString(const char *s) } continue; } + + if (!strcmp(token, "health_bars")) { + token = COM_Parse(&s); + value = Q_atoi(token); + if (value < 0 || value >= MAX_STATS) { + Com_Error(ERR_DROP, "%s: invalid stat index", __func__); + } + value = cl.frame.ps.stats[value]; + + token = COM_Parse(&s); + index = Q_atoi(token); + if (index < 0 || index >= cl.csr.end) { + Com_Error(ERR_DROP, "%s: invalid string index", __func__); + } + + HUD_DrawCenterString(x + 320 / 2, y, cl.configstrings[index]); + SCR_DrawHealthBar(x + 320 / 2, y + CHAR_HEIGHT + 4, value & 0xff); + SCR_DrawHealthBar(x + 320 / 2, y + CHAR_HEIGHT + 12, (value >> 8) & 0xff); + continue; + } } R_ClearColor(); From 6393be0a5bb83893814eda1b536fe88fcfc0835e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 22 Jul 2024 15:23:45 +0300 Subject: [PATCH 362/974] =?UTF-8?q?Fix=20skipping=20=E2=80=98health=5Fbars?= =?UTF-8?q?=E2=80=99=20command.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/screen.c b/src/client/screen.c index b803a1ebb..61676cc92 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1610,7 +1610,7 @@ static void SCR_SkipToEndif(const char **s) continue; } - if (!strcmp(token, "num")) { + if (!strcmp(token, "num") || !strcmp(token, "health_bars")) { COM_Parse(s); COM_Parse(s); continue; From 3b9c4f6623b352ad3cfb120b7fca1afa4acc129e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 23 Jul 2024 13:56:05 -0400 Subject: [PATCH 363/974] Some minor warning cleanup --- src/action/acesrc/acebot.h | 26 +----- src/action/acesrc/acebot_cmds.c | 2 +- src/action/acesrc/acebot_nodes.c | 2 +- src/action/acesrc/acebot_spawn.c | 2 +- src/action/botlib/botlib.h | 110 ++++++++++++----------- src/action/botlib/botlib_communication.c | 93 ++++++++++++++++++- src/action/botlib/botlib_movement.c | 7 +- src/action/botlib/botlib_nav.c | 33 +++---- src/action/botlib/botlib_nodes.c | 8 +- src/action/botlib/botlib_spawn.c | 10 +++ src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/refresh/main.c | 6 +- 14 files changed, 195 insertions(+), 107 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index e97e2d2f4..258174219 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -100,32 +100,9 @@ typedef enum NODE_SPAWNPOINT, // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes NODE_LEARN, // Special node - Human learning NODE_CTF_FLAG, // CTF Flag + NODE_ESP_TARGET, // ESP Target NODE_ALL = 99 // For selecting all nodes } nodetype_t; -/* -#define NODE_MOVE (1) // Move (forward, left, right, back) -#define NODE_CROUCH (NODE_MOVE + 1) // Crouching (-moveup) -#define NODE_STEP (NODE_CROUCH + 1) // For steps / stairs -#define NODE_JUMP (NODE_STEP + 1) // Small jumps (+moveup) -#define NODE_JUMPPAD (NODE_JUMP + 1) // For large jumps across any distance -#define NODE_STAND_DROP (NODE_JUMPPAD + 1) // For dropping down while standing -#define NODE_CROUCH_DROP (NODE_STAND_DROP + 1) // For dropping down while crouching -#define NODE_UNSAFE_DROP (NODE_CROUCH_DROP + 1) // For dropping down while crouching, but causing minimal leg damage -#define NODE_LADDER (NODE_UNSAFE_DROP + 1) // Ladder -#define NODE_LADDER_UP (NODE_LADDER + 1) // Ladder going up -#define NODE_LADDER_DOWN (NODE_LADDER_UP + 1) // Ladder going down -#define NODE_DOOR (NODE_LADDER_DOWN + 1) // Door node -#define NODE_PLATFORM (NODE_DOOR + 1) // Moving platforms -#define NODE_TELEPORTER (NODE_PLATFORM + 1) // Teleporters -#define NODE_ITEM (NODE_TELEPORTER + 1) // Items -#define NODE_WATER (NODE_ITEM + 1) // Water -#define NODE_GRAPPLE (NODE_WATER + 1) // CTF grapple hook -#define NODE_SPAWNPOINT (NODE_GRAPPLE + 1) // Nodes tied to spawnpoint locations - these are auto generated for each map ready to be connected to user placed nodes -#define NODE_POI (NODE_SPAWNPOINT + 1) // Point of Interest (POI) specific locations that you wish bots to visit from time to time -#define NODE_LEARN (NODE_POI + 1) // Special node - Human learning -// -#define NODE_ALL 99 // For selecting all nodes -*/ // Density setting for nodes #define NODE_DENSITY 96 @@ -564,6 +541,7 @@ extern cvar_t* bot_rush; extern cvar_t* bot_randvoice; extern cvar_t* bot_randskill; extern cvar_t* bot_randname; +extern cvar_t* bot_chat; //extern cvar_t* bot_randteamskin; diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 54cb68997..b9f6d3c04 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -31,7 +31,7 @@ #include "../g_local.h" -qboolean debug_mode=false; +qboolean debug_mode=true; qboolean shownodes_mode=false; //RiEvEr - new node showing method /////////////////////////////////////////////////////////////////////// diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index da401b58f..e3696c3ef 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -299,7 +299,7 @@ int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) } } - //Com_Printf("%s [%s] node %d\n", __func__, self->client->pers.netname, node); + Com_Printf("%s [%s] node %d\n", __func__, self->client->pers.netname, node); return node; } diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 561ac3498..dcfb15c9f 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -729,7 +729,7 @@ void ACESP_RemoveBot(char *name) } */ if(!freed) - gi.bprintf (PRINT_MEDIUM, "No bot removed\n", name); + gi.bprintf (PRINT_MEDIUM, "No bot removed\n"); // ACESP_SaveBots(); // Save them again } diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 65cce45fd..30907c98b 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -108,31 +108,31 @@ typedef struct botlib_noises_s extern botlib_noises_t botlib_noises; // Actionable flags -#define ACTION_NONE 0x00000000 // No action taken -#define ACTION_ATTACK 0x00000001 // Tap the attack button -#define ACTION_USE 0x00000002 -#define ACTION_RESPAWN 0x00000008 -#define ACTION_JUMP 0x00000010 // Small jumps - tapping the jump button -#define ACTION_MOVEUP 0x00000020 -#define ACTION_CROUCH 0x00000080 -#define ACTION_MOVEDOWN 0x00000100 -#define ACTION_MOVEFORWARD 0x00000200 -#define ACTION_MOVEBACK 0x00000800 -#define ACTION_MOVELEFT 0x00001000 -#define ACTION_MOVERIGHT 0x00002000 -#define ACTION_BOXJUMP 0x00008000 // Higher jumps -#define ACTION_TALK 0x00010000 -#define ACTION_GESTURE 0x00020000 -#define ACTION_WALK 0x00080000 -#define ACTION_AFFIRMATIVE 0x00100000 -#define ACTION_NEGATIVE 0x00200000 -#define ACTION_GETFLAG 0x00800000 -#define ACTION_GUARDBASE 0x01000000 -#define ACTION_PATROL 0x02000000 -#define ACTION_FOLLOWME 0x08000000 -#define ACTION_HOLDJUMP 0x10000000 // Hold the jump button and release when touching ground -#define ACTION_HOLDPOS 0x20000000 // Hold position -#define ACTION_JUMPPAD 0x40000000 +#define ACTION_NONE BIT(0) // No action taken +#define ACTION_ATTACK BIT(1) // Tap the attack button +#define ACTION_USE BIT(2) +#define ACTION_RESPAWN BIT(3) +#define ACTION_JUMP BIT(4) // Small jumps - tapping the jump button +#define ACTION_MOVEUP BIT(5) +#define ACTION_CROUCH BIT(7) +#define ACTION_MOVEDOWN BIT(8) +#define ACTION_MOVEFORWARD BIT(9) +#define ACTION_MOVEBACK BIT(11) +#define ACTION_MOVELEFT BIT(12) +#define ACTION_MOVERIGHT BIT(13) +#define ACTION_BOXJUMP BIT(15) // Higher jumps +#define ACTION_TALK BIT(16) +#define ACTION_GESTURE BIT(17) +#define ACTION_WALK BIT(19) +#define ACTION_AFFIRMATIVE BIT(20) +#define ACTION_NEGATIVE BIT(21) +#define ACTION_GETFLAG BIT(23) +#define ACTION_GUARDBASE BIT(24) +#define ACTION_PATROL BIT(25) +#define ACTION_FOLLOWME BIT(27) +#define ACTION_HOLDJUMP BIT(28) // Hold the jump button and release when touching ground +#define ACTION_HOLDPOS BIT(29) // Hold position +#define ACTION_JUMPPAD BIT(30) // =========================================================================== @@ -194,10 +194,20 @@ qboolean BOTLIB_Commands(edict_t* ent); // Client commands // =========================================================================== // botlib_communication.c // =========================================================================== +typedef enum +{ + CHAT_WELCOME, + CHAT_KILLED, + CHAT_INSULTS, + CHAT_GOODBYE +} bot_chat_types_t; + void BOTLIB_Wave(edict_t* ent, int type); void BOTLIB_PrecacheRadioSounds(void); void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); +void BOTLIB_Chat(edict_t *bot, bot_chat_types_t chattype); +void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message); // Wave (gesture) types #define WAVE_FLIPOFF 1 @@ -207,31 +217,31 @@ void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd); #define WAVE_POINT 5 // Radio flags -#define RADIO_1 0x00000001 -#define RADIO_2 0x00000002 -#define RADIO_3 0x00000003 -#define RADIO_4 0x00000004 -#define RADIO_5 0x00000005 -#define RADIO_6 0x00000006 -#define RADIO_7 0x00000007 -#define RADIO_8 0x00000008 -#define RADIO_9 0x00000009 -#define RADIO_10 0x00000010 -#define RADIO_BACK 0x00000011 -#define RADIO_COVER 0x00000012 -#define RADIO_DOWN 0x00000013 -#define RADIO_ENEMY_DOWN 0x00000014 -#define RADIO_ENEMY_SIGHTED 0x00000015 -#define RADIO_FORWARD 0x00000016 -#define RADIO_GO 0x00000017 -#define RADIO_IM_HIT 0x00000018 -#define RADIO_LEFT 0x00000019 -#define RADIO_REPORTIN 0x00000020 -#define RADIO_RIGHT 0x00000021 -#define RADIO_TAKING_FIRE 0x00000022 -#define RADIO_TEAMMATE_DOWN 0x00000023 -#define RADIO_TEAM_REPORT_IN 0x00000024 -#define RADIO_UP 0x00000025 +#define RADIO_1 BIT(0) +#define RADIO_2 BIT(1) +#define RADIO_3 BIT(2) +#define RADIO_4 BIT(3) +#define RADIO_5 BIT(4) +#define RADIO_6 BIT(5) +#define RADIO_7 BIT(6) +#define RADIO_8 BIT(7) +#define RADIO_9 BIT(8) +#define RADIO_10 BIT(9) +#define RADIO_BACK BIT(10) +#define RADIO_COVER BIT(11) +#define RADIO_DOWN BIT(12) +#define RADIO_ENEMY_DOWN BIT(13) +#define RADIO_ENEMY_SIGHTED BIT(14) +#define RADIO_FORWARD BIT(15) +#define RADIO_GO BIT(16) +#define RADIO_IM_HIT BIT(17) +#define RADIO_LEFT BIT(18) +#define RADIO_REPORTIN BIT(19) +#define RADIO_RIGHT BIT(20) +#define RADIO_TAKING_FIRE BIT(21) +#define RADIO_TEAMMATE_DOWN BIT(22) +#define RADIO_TEAM_REPORT_IN BIT(23) +#define RADIO_UP BIT(24) // Each of the possible radio messages and their length typedef struct bot_radio_msg_s diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index d921ea0d3..a04df3d52 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -3,6 +3,87 @@ #include "botlib.h" #include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 +// Borrowed from LTK bots +#define DBC_WELCOMES 4 +char *botchat_welcomes[DBC_WELCOMES] = +{ + "Hey %s, how's it going?", + "hey %s whats up", + "yo %s", + "aw yeah %s is here" +}; + +#define DBC_GOODBYES 5 +char *botchat_goodbyes[DBC_GOODBYES] = +{ + "welp gotta go", + "food time brb", + "ggs all", + "time for a j", + "sauna time" +}; + +#define DBC_KILLEDS 8 +char *botchat_killeds[DBC_KILLEDS] = +{ + "lol", + "welp..", + "aw come on", + "nice one", + "prkl", + "jajajaja", + "ffffffffff", + "woowwwww" +}; + +#define DBC_INSULTS 11 +char *botchat_insults[DBC_INSULTS] = +{ + "lol gottem", + "pow!", + "rip", + "can't hide from me", + "goteem", + "he he heee", + ":D", + "heh", + "AHAHAHAHA", + ">:)", + ":>" +}; + +void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { + char* text = NULL; + + switch (chattype) { + case CHAT_WELCOME: + text = botchat_welcomes[rand() % DBC_WELCOMES]; + break; + case CHAT_KILLED: + text = botchat_killeds[rand() % DBC_KILLEDS]; + break; + case CHAT_INSULTS: + text = botchat_insults[rand() % DBC_INSULTS]; + break; + case CHAT_GOODBYE: + text = botchat_goodbyes[rand() % DBC_GOODBYES]; + break; + default: + if (debug_mode) + gi.bprintf(PRINT_HIGH, "%s: Unknown chat type %d", __func__, chattype); + return; // Exit if chattype is unknown + } + + // Ensure text is not NULL before proceeding + if (text == NULL) { + if (debug_mode) + gi.bprintf(PRINT_HIGH, "%s: text is NULL, cannot proceed with BOTLIB_Say.", __func__); + return; // Optionally, set text to a default value before calling BOTLIB_Say + } + + BOTLIB_Say(bot, text, false); +} + // Bot wave gestures void BOTLIB_Wave(edict_t* ent, int type) { @@ -168,8 +249,12 @@ void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) //gclient_t *cl; - if (!teamplay->value) - return; + if (!teamplay->value) { + if (!bot_chat->value) + return; + else + Q_snprintf(text, sizeof(text), pMsg); // Say all + } if (ent->client->resp.team == NOTEAM) return; @@ -459,8 +544,8 @@ void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) } else { - char weap_name[PARSE_BUFSIZE]; - char item_name[PARSE_BUFSIZE]; + char weap_name[WEAP_ITM_NAME_LEN]; + char item_name[WEAP_ITM_NAME_LEN]; GetWeaponName(self, weap_name); GetItemName(self, item_name); sprintf(buffer, " %c Reporting in! [ %d%c ] Equipped with %c %s and %s", '\x0F', self->health, '\x05', '\x07', weap_name, item_name); // Build string diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index c95deb9e5..21667177a 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -127,7 +127,6 @@ qboolean NodeTypeToString(edict_t* self, int type, char *string, const int max_s return false; // Unknown node type } - string[len] = '\0'; // Terminate string return true; // Success } @@ -5973,7 +5972,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) } // If travel time took too long, assume we're stuck - if (self->bot.node_travel_time >= 60) //60 // Bot failure to reach next node + if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node { self->bot.stuck_wander_time = 1; Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); @@ -6471,7 +6470,9 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) } else if ((contents_feet & MASK_WATER) && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) // Move up { - self->bot.bi.actionflags |= ACTION_MOVEUP; + //self->bot.bi.actionflags |= ACTION_MOVEUP; + // darksaint: changed this to MOVEUP and MOVEFORWARD simultanously to get out of water + self->bot.bi.actionflags |= (ACTION_MOVEUP | ACTION_MOVEFORWARD); //Com_Printf("%s %s [%d] water: move up\n", __func__, self->client->pers.netname, level.framenum); } else if (nodes[self->bot.next_node].origin[2] < self->s.origin[2]) // Move down diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index ae9d9258d..11dcb796d 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -825,7 +825,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz // else // Fallback to old pathing // { self->bot.goal_node = INVALID; - gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); + //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) { // Add the goal to the end of the node list + terminate @@ -852,9 +852,9 @@ qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node) return false; } - //Com_Printf("\n\n========================================================\n", __func__); - //Com_Printf("%s Heading for goal node[%d] area[%d]\n", __func__, goal_node, self->bot.goal_area); - //Com_Printf("========================================================\n", __func__); + // Com_Printf("\n\n========================================================\n", __func__); + // Com_Printf("%s Heading for goal node[%d] area[%d]\n", __func__, goal_node, self->bot.goal_area); + // Com_Printf("========================================================\n", __func__); //BOTLIB_InitAreaConnections(); @@ -1937,12 +1937,13 @@ qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_r // BOTLIB_DijkstraPath() is run again with path_randomization turned off qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_randomization) { - Com_Printf("%s from[%d] to[%d] for %s, random: %d\n", __func__, from, to, ent->client->pers.netname, path_randomization); + //Com_Printf("%s from[%d] to[%d] for %s, random: %d\n", __func__, from, to, ent->client->pers.netname, path_randomization); // Sanity check - if (from == INVALID || to == INVALID) + if (from == INVALID || to == INVALID) { gi.dprintf("both from and to nodes were invalid\n"); return false; + } AntInitSearch(ent); // Clear out the path storage for (int i = 0; i < numnodes; i++) @@ -1964,7 +1965,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando atNode = SLLfront(&openList); // Get the next node if (atNode == to) // If the next node is the goal node { - Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); + //Com_Printf("%s [%d] next node found a path\n", __func__, level.framenum); break; // We found a path } @@ -2104,12 +2105,12 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando nodeweight[lowest_node] = weight; // Update the weight nodefrom[lowest_node] = atNode; // Update the parent node (open list) SLLpush_back(&openList, lowest_node); // Store it - Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); + //Com_Printf("%s lowest_node[%d]\n", __func__, lowest_node); } if (lowest_node == to) // If node being linked is the goal node { - Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); + //Com_Printf("%s [%d] lowest_node found a path\n", __func__, level.framenum); break; // We found a path } } @@ -2155,7 +2156,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando if (newNode == to) // If node being linked is the goal node { - Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); + //Com_Printf("%s [%d] normal found a path\n", __func__, level.framenum); break; // We found a path } @@ -2216,7 +2217,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); } - Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); + //Com_Printf("%s path_table[ nodefrom[%d] ] [%d] = %d\n", __func__, newNode, newNode, newNode); //int prev_newNode = newNode; // We earlier set our start node to INVALID to set up the termination @@ -2239,10 +2240,10 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando SLLpush_front(&ent->pathList, newNode); // Push it onto the pathlist ////path_table[nodefrom[newNode]][to] = newNode; // Set the path in the node array to match this shortest path - Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); + //Com_Printf("%s path_table[ nodefrom[%d][%d] = %d\n", __func__, newNode, to, newNode); } - Com_Printf("%s EXIT PATH\n\n", __func__); + //Com_Printf("%s EXIT PATH\n\n", __func__); // Each time a path is found, make a copy //ent->bot.node_list_count = 0; @@ -2273,7 +2274,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando } if (SLLempty(&ent->pathList)){ - gi.dprintf("SLLempty: %s failed to find a path from %d to %d\n", __func__, from, to); + //gi.dprintf("SLLempty: %s failed to find a path from %d to %d\n", __func__, from, to); return false; // Failure } @@ -2284,12 +2285,12 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando // If path_randomization was true, then try again without it if (path_randomization) //If using rand pathing, and it fails, try again *ONCE* without it { - gi.dprintf("Trying again without path randomization\n"); + //gi.dprintf("Trying again without path randomization\n"); BOTLIB_DijkstraPath(ent, from, to, false); } // Else: just fail it completely { - gi.dprintf("Complete failure: %s failed to find a path from %d to %d\n", __func__, from, to); + //gi.dprintf("Complete failure: %s failed to find a path from %d to %d\n", __func__, from, to); return false; // Failure } } diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index d806fe1a7..361d3bce9 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -624,7 +624,7 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi // Invalid node if (goal_node == INVALID || self->bot.current_node == INVALID || nodes[goal_node].inuse == false){ - Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); + //Com_Printf("%s %s invalid node set: current_node[%d] goal_node[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node); return false; } @@ -633,19 +633,19 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi // Already at node if (goal_node == self->bot.current_node){ - Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); + //Com_Printf("%s %s invalid goal set: current_node[%d] goal_node[%d] inuse[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, nodes[goal_node].inuse); return false; } // NEW PATHING: limit search to same area if (area != INVALID && BOTLIB_DijkstraAreaPath(self, self->bot.current_node, goal_node, path_randomization, area, build_new_path) == false) { - gi.dprintf("NEW: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); + //gi.dprintf("NEW: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); return false; } // OLD: searches all nodes //else if (AntStartSearch(self, self->bot.current_node, goal_node, path_randomization) == false) else if (BOTLIB_DijkstraPath(self, self->bot.current_node, goal_node, path_randomization) == false){ - gi.dprintf("OLD: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); + //gi.dprintf("OLD: %s %s failed to find path to node %d\n", __func__, self->client->pers.netname, goal_node); return false; } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 85d44af13..7b86e1b22 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1375,6 +1375,14 @@ void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) bot->think = BOTLIB_Think; bot->nextthink = level.framenum + 1; + //darksaint -- Bot Chat -- s + // Generates chat message if respawning (insults) + if (bot_chat->value && respawn) { + gi.dprintf("Firing off chat message---------------\n"); + BOTLIB_Chat(bot, CHAT_INSULTS); + } + //darksaint -- Bot Chat -- e + PutClientInServer(bot); JoinTeam(bot, team, true); } @@ -1456,6 +1464,8 @@ void BOTLIB_RemoveBot(char* name) //bot->inuse = false; freed = true; ClientDisconnect(bot); + if (bot_chat->value && !remove_all) + BOTLIB_Chat(bot, CHAT_GOODBYE); //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); if (!remove_all) break; diff --git a/src/action/g_local.h b/src/action/g_local.h index ba87d953f..7ca7d5227 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2748,6 +2748,7 @@ typedef struct team_s }team_t; extern team_t teams[TEAM_TOP]; +#define WEAP_ITM_NAME_LEN 32 #define PARSE_BUFSIZE 256 #define IS_ALIVE(ent) ((ent)->solid != SOLID_NOT && (ent)->deadflag != DEAD_DEAD) diff --git a/src/action/g_main.c b/src/action/g_main.c index a1c695ddd..d8dc39e0a 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -493,6 +493,7 @@ cvar_t* bot_rush; // Bots rush players by going directly for them cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 max: 100] cvar_t* bot_randskill; // When random bot join a game, they pick a random skill [min: 1, max: 10]. Using 0 will turn this off. cvar_t* bot_randname; // Allow bots to pick a random name +cvar_t* bot_chat; // Bots chat //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index f459a2e4c..4f5050a21 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -693,6 +693,7 @@ void InitGame( void ) bot_randvoice = gi.cvar("bot_randvoice", "33", 0); bot_randskill = gi.cvar("bot_randskill", "10", 0); bot_randname = gi.cvar("bot_randname", "1", 0); + bot_chat = gi.cvar("bot_chat", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif diff --git a/src/refresh/main.c b/src/refresh/main.c index 0f6d50cb5..ce82ec142 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -848,7 +848,7 @@ static void FREE_BoxPoint(void) // Single draw call: render min/max box void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) { - const vec3_t axis[3] = { 1,0,0, 0,1,0, 0,0,1 }; + const vec3_t axis[3] = { {1,0,0}, {0,1,0}, {0,0,1} }; uint32_t colors[24]; for (int i = 0; i < 24; i++) @@ -929,7 +929,7 @@ void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolea // Batch draw call: render min/max box void GL_AddDrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) { - const vec3_t axis[3] = { 1,0,0, 0,1,0, 0,0,1 }; + const vec3_t axis[3] = { {1,0,0}, {0,1,0}, {0,0,1} }; if (drawbox_total && drawbox_count < drawbox_total) { @@ -1553,7 +1553,7 @@ static void GL_DrawSelection(void) } } -static void GL_InitDebugDraw() +static void GL_InitDebugDraw(void) { // Malloc nav if (sv.cm.draw == NULL) From e8007cf75b888f193b0cdcdbbd7573336d3a5b7d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 23 Jul 2024 16:52:43 +0300 Subject: [PATCH 364/974] Support drawing player hit markers. Based on code from: Jonathan --- doc/client.asciidoc | 12 +++++++++++- inc/shared/shared.h | 2 ++ src/client/client.h | 3 +++ src/client/parse.c | 4 ++++ src/client/screen.c | 36 ++++++++++++++++++++++++++++++++++++ src/client/tent.c | 15 +++++++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 7f87f809c..78df01808 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -269,7 +269,13 @@ cl_gun_z:: Specifies custom gun model offset. Default value is 0. cl_muzzleflashes:: - Specifies if muzzleflash effects are enabled. Default value is 1. + Specifies if re-release muzzleflash effects are enabled. Default value is 1. + +cl_hit_markers:: + Specifies if re-release hit marker effects are enabled. Default value is 2. + - 0 — hit marker effects disabled + - 1 — draw hit marker + - 2 — draw hit marker and play hit sound Sound Subsystem ~~~~~~~~~~~~~~~ @@ -533,6 +539,10 @@ ch_y:: the default position in center of the game screen. Default values are 0 (draw in center). +scr_hit_marker_time:: + Hit marker visibility time in milliseconds. Hit marker is only visible if + crosshair is visible. Default value is 500. + Video Modes ~~~~~~~~~~~ diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 11c486d98..abaa60ff0 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1232,6 +1232,8 @@ typedef enum { TE_EXPLOSION2_NL, //[Paril-KEX] + TE_DAMAGE_DEALT = 128, + TE_NUM_ENTITIES } temp_event_t; diff --git a/src/client/client.h b/src/client/client.h index 40ece22c1..9dadb9a6f 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -311,6 +311,9 @@ typedef struct { vec3_t offset; } muzzle; } weapon; + + unsigned hit_marker_time; + int hit_marker_count; } client_state_t; extern client_state_t cl; diff --git a/src/client/parse.c b/src/client/parse.c index 7821e1378..62f420a53 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -864,6 +864,10 @@ static void CL_ParseTEntPacket(void) te.count = MSG_ReadByte(); break; + case TE_DAMAGE_DEALT: + te.count = MSG_ReadShort(); + break; + default: Com_Error(ERR_DROP, "%s: bad type", __func__); } diff --git a/src/client/screen.c b/src/client/screen.c index 61676cc92..1bfd15717 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -36,6 +36,9 @@ static struct { int crosshair_width, crosshair_height; color_t crosshair_color; + qhandle_t hit_marker_pic; + int hit_marker_width, hit_marker_height; + qhandle_t pause_pic; int pause_width, pause_height; @@ -96,6 +99,8 @@ static cvar_t *ch_scale; static cvar_t *ch_x; static cvar_t *ch_y; +static cvar_t *scr_hit_marker_time; + vrect_t scr_vrect; // position of render window on screen const uint32_t colorTable[8] = { @@ -1268,6 +1273,9 @@ void SCR_RegisterMedia(void) scr.net_pic = R_RegisterPic("net"); scr.font_pic = R_RegisterFont(scr_font->string); + scr.hit_marker_pic = R_RegisterPic("marker"); + R_GetPicSize(&scr.hit_marker_width, &scr.hit_marker_height, scr.hit_marker_pic); + scr_crosshair_changed(scr_crosshair); } @@ -1350,6 +1358,8 @@ void SCR_Init(void) scr_showpmove = Cvar_Get("scr_showpmove", "0", 0); #endif + scr_hit_marker_time = Cvar_Get("scr_hit_marker_time", "500", 0); + Cmd_Register(scr_cmds); scr_scale_changed(scr_scale); @@ -2052,6 +2062,30 @@ static void SCR_DrawLoading(void) R_SetScale(1.0f); } +static void SCR_DrawHitMarker(void) +{ + if (!cl.hit_marker_count) + return; + if (scr_hit_marker_time->integer <= 0 || + cls.realtime - cl.hit_marker_time > scr_hit_marker_time->integer) { + cl.hit_marker_count = 0; + return; + } + + float frac = (float)(cls.realtime - cl.hit_marker_time) / scr_hit_marker_time->integer; + float alpha = 1.0f - (frac * frac); + + int w = scr.hit_marker_width * ch_scale->value; + int h = scr.hit_marker_height * ch_scale->value; + + int x = (scr.hud_width - w) / 2; + int y = (scr.hud_height - h) / 2; + + R_SetColor(MakeColor(255, 0, 0, alpha * 255)); + + R_DrawStretchPic(x + ch_x->integer, y + ch_y->integer, w, h, scr.hit_marker_pic); +} + static void SCR_DrawCrosshair(void) { int x, y; @@ -2071,6 +2105,8 @@ static void SCR_DrawCrosshair(void) scr.crosshair_width, scr.crosshair_height, scr.crosshair_pic); + + SCR_DrawHitMarker(); } // The status bar is a small layout program that is based on the stats array diff --git a/src/client/tent.c b/src/client/tent.c index 26b1b7453..fee073abc 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -35,6 +35,8 @@ qhandle_t cl_sfx_watrexp; qhandle_t cl_sfx_lightning; qhandle_t cl_sfx_disrexp; +qhandle_t cl_sfx_hit_marker; + qhandle_t cl_mod_explode; qhandle_t cl_mod_smoke; qhandle_t cl_mod_flash; @@ -55,6 +57,7 @@ qhandle_t cl_mod_muzzles[MFLASH_TOTAL]; qhandle_t cl_img_flare; static cvar_t *cl_muzzleflashes; +static cvar_t *cl_hit_markers; #define MAX_FOOTSTEP_SFX 9 @@ -263,6 +266,8 @@ void CL_RegisterTEntSounds(void) cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); + + cl_sfx_hit_marker = S_RegisterSound("weapons/marker.wav"); } /* @@ -1596,6 +1601,15 @@ void CL_ParseTEnt(void) CL_PowerSplash(); break; + case TE_DAMAGE_DEALT: + if (te.count > 0 && cl_hit_markers->integer > 0) { + cl.hit_marker_time = cls.realtime; + cl.hit_marker_count = te.count; + if (cl_hit_markers->integer > 1) + S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + } + break; + default: Com_Error(ERR_DROP, "%s: bad type", __func__); } @@ -1631,6 +1645,7 @@ void CL_ClearTEnts(void) void CL_InitTEnts(void) { cl_muzzleflashes = Cvar_Get("cl_muzzleflashes", "1", 0); + cl_hit_markers = Cvar_Get("cl_hit_markers", "2", 0); cl_railtrail_type = Cvar_Get("cl_railtrail_type", "0", 0); cl_railtrail_time = Cvar_Get("cl_railtrail_time", "1.0", 0); cl_railtrail_time->changed = cl_timeout_changed; From 449b903b2756144a078b4d5bf2ded4854b82758f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 23 Jul 2024 18:57:07 +0300 Subject: [PATCH 365/974] Clean up Make*Long() macros. Explicitly cast all arguments to uint32_t, so that e.g. MakeColor() can be used with floats. --- inc/shared/shared.h | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index abaa60ff0..ef19e0e3e 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -660,28 +660,29 @@ static inline int32_t SignExtend(uint32_t v, int bits) } #if USE_LITTLE_ENDIAN -#define BigShort ShortSwap -#define BigLong LongSwap -#define BigFloat FloatSwap -#define LittleShort(x) ((uint16_t)(x)) -#define LittleLong(x) ((uint32_t)(x)) -#define LittleFloat(x) ((float)(x)) -#define MakeRawLong(b1,b2,b3,b4) (((unsigned)(b4)<<24)|((b3)<<16)|((b2)<<8)|(b1)) +#define BigShort(x) ShortSwap(x) +#define BigLong(x) LongSwap(x) +#define BigFloat(x) FloatSwap(x) +#define LittleShort(x) ((uint16_t)(x)) +#define LittleLong(x) ((uint32_t)(x)) +#define LittleFloat(x) ((float)(x)) +#define MakeRawLong(b1,b2,b3,b4) MakeLittleLong(b1,b2,b3,b4) #define MakeRawShort(b1,b2) (((b2)<<8)|(b1)) #elif USE_BIG_ENDIAN #define BigShort(x) ((uint16_t)(x)) #define BigLong(x) ((uint32_t)(x)) #define BigFloat(x) ((float)(x)) -#define LittleShort ShortSwap -#define LittleLong LongSwap -#define LittleFloat FloatSwap -#define MakeRawLong(b1,b2,b3,b4) (((unsigned)(b1)<<24)|((b2)<<16)|((b3)<<8)|(b4)) +#define LittleShort(x) ShortSwap(x) +#define LittleLong(x) LongSwap(x) +#define LittleFloat(x) FloatSwap(x) +#define MakeRawLong(b1,b2,b3,b4) MakeBigLong(b1,b2,b3,b4) #define MakeRawShort(b1,b2) (((b1)<<8)|(b2)) #else #error Unknown byte order #endif -#define MakeLittleLong(b1,b2,b3,b4) (((unsigned)(b4)<<24)|((b3)<<16)|((b2)<<8)|(b1)) +#define MakeLittleLong(b1,b2,b3,b4) (((uint32_t)(b4)<<24)|((uint32_t)(b3)<<16)|((uint32_t)(b2)<<8)|(uint32_t)(b1)) +#define MakeBigLong(b1,b2,b3,b4) (((uint32_t)(b1)<<24)|((uint32_t)(b2)<<16)|((uint32_t)(b3)<<8)|(uint32_t)(b4)) #define LittleVector(a,b) \ ((b)[0]=LittleFloat((a)[0]),\ From b06127ee3a5bcfdf6c6dc1d84acf1a14dcb38d47 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 23 Jul 2024 19:15:20 +0300 Subject: [PATCH 366/974] Use GCC builtins for byteswapping if available. --- inc/shared/platform.h | 6 ++++++ inc/shared/shared.h | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/inc/shared/platform.h b/inc/shared/platform.h index f5043cb38..9dc07fe5a 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -86,6 +86,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #define R_OK 4 #endif +#ifdef __has_builtin +#define q_has_builtin(x) __has_builtin(x) +#else +#define q_has_builtin(x) 0 +#endif + #ifdef __GNUC__ #if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index ef19e0e3e..a65084c44 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -620,15 +620,23 @@ char *vtos(const vec3_t v); static inline uint16_t ShortSwap(uint16_t s) { +#if q_has_builtin(__builtin_bswap16) + return __builtin_bswap16(s); +#else s = (s >> 8) | (s << 8); return s; +#endif } static inline uint32_t LongSwap(uint32_t l) { +#if q_has_builtin(__builtin_bswap32) + return __builtin_bswap32(l); +#else l = ((l >> 8) & 0x00ff00ff) | ((l << 8) & 0xff00ff00); l = (l >> 16) | (l << 16); return l; +#endif } static inline float FloatSwap(float f) From e793103085647ab704166ed11b52750d9e27d215 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 24 Jul 2024 15:07:25 +0300 Subject: [PATCH 367/974] Clean up crosshair and other screen code. Don't cache loading/paused pics width/height. Prescale hit marker. Separate scr_crosshair_changed() into smaller functions. --- src/client/screen.c | 104 ++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 1bfd15717..71b1a69c1 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -40,10 +40,8 @@ static struct { int hit_marker_width, hit_marker_height; qhandle_t pause_pic; - int pause_width, pause_height; qhandle_t loading_pic; - int loading_width, loading_height; bool draw_loading; qhandle_t sb_pics[2][STAT_PICS]; @@ -1163,34 +1161,40 @@ static void SCR_TimeRefresh_f(void) //============================================================================ -static void scr_crosshair_changed(cvar_t *self) +static void ch_scale_changed(cvar_t *self) { - char buffer[16]; int w, h; float scale; - if (scr_crosshair->integer > 0) { - Q_snprintf(buffer, sizeof(buffer), "ch%i", scr_crosshair->integer); - scr.crosshair_pic = R_RegisterPic(buffer); - R_GetPicSize(&w, &h, scr.crosshair_pic); - - // prescale - scale = Cvar_ClampValue(ch_scale, 0.1f, 9.0f); - scr.crosshair_width = w * scale; - scr.crosshair_height = h * scale; - if (scr.crosshair_width < 1) - scr.crosshair_width = 1; - if (scr.crosshair_height < 1) - scr.crosshair_height = 1; - - if (ch_health->integer) { - SCR_SetCrosshairColor(); - } else { - scr.crosshair_color.u8[0] = Cvar_ClampValue(ch_red, 0, 1) * 255; - scr.crosshair_color.u8[1] = Cvar_ClampValue(ch_green, 0, 1) * 255; - scr.crosshair_color.u8[2] = Cvar_ClampValue(ch_blue, 0, 1) * 255; - } - scr.crosshair_color.u8[3] = Cvar_ClampValue(ch_alpha, 0, 1) * 255; + scale = Cvar_ClampValue(self, 0.1f, 9.0f); + + // prescale + R_GetPicSize(&w, &h, scr.crosshair_pic); + scr.crosshair_width = Q_rint(w * scale); + scr.crosshair_height = Q_rint(h * scale); + + R_GetPicSize(&w, &h, scr.hit_marker_pic); + scr.hit_marker_width = Q_rint(w * scale); + scr.hit_marker_height = Q_rint(h * scale); +} + +static void ch_color_changed(cvar_t *self) +{ + if (ch_health->integer) { + SCR_SetCrosshairColor(); + } else { + scr.crosshair_color.u8[0] = Cvar_ClampValue(ch_red, 0, 1) * 255; + scr.crosshair_color.u8[1] = Cvar_ClampValue(ch_green, 0, 1) * 255; + scr.crosshair_color.u8[2] = Cvar_ClampValue(ch_blue, 0, 1) * 255; + } + scr.crosshair_color.u8[3] = Cvar_ClampValue(ch_alpha, 0, 1) * 255; +} + +static void scr_crosshair_changed(cvar_t *self) +{ + if (self->integer > 0) { + scr.crosshair_pic = R_RegisterPic(va("ch%i", self->integer)); + ch_scale_changed(ch_scale); } else { scr.crosshair_pic = 0; } @@ -1261,20 +1265,12 @@ void SCR_RegisterMedia(void) scr.inven_pic = R_RegisterPic("inventory"); scr.field_pic = R_RegisterPic("field_3"); - scr.backtile_pic = R_RegisterImage("backtile", IT_PIC, IF_PERMANENT | IF_REPEAT); - scr.pause_pic = R_RegisterPic("pause"); - R_GetPicSize(&scr.pause_width, &scr.pause_height, scr.pause_pic); - scr.loading_pic = R_RegisterPic("loading"); - R_GetPicSize(&scr.loading_width, &scr.loading_height, scr.loading_pic); - scr.net_pic = R_RegisterPic("net"); scr.font_pic = R_RegisterFont(scr_font->string); - scr.hit_marker_pic = R_RegisterPic("marker"); - R_GetPicSize(&scr.hit_marker_width, &scr.hit_marker_height, scr.hit_marker_pic); scr_crosshair_changed(scr_crosshair); } @@ -1330,18 +1326,18 @@ void SCR_Init(void) scr_chathud_y = Cvar_Get("scr_chathud_y", "-64", 0); ch_health = Cvar_Get("ch_health", "0", 0); - ch_health->changed = scr_crosshair_changed; + ch_health->changed = ch_color_changed; ch_red = Cvar_Get("ch_red", "1", 0); - ch_red->changed = scr_crosshair_changed; + ch_red->changed = ch_color_changed; ch_green = Cvar_Get("ch_green", "1", 0); - ch_green->changed = scr_crosshair_changed; + ch_green->changed = ch_color_changed; ch_blue = Cvar_Get("ch_blue", "1", 0); - ch_blue->changed = scr_crosshair_changed; + ch_blue->changed = ch_color_changed; ch_alpha = Cvar_Get("ch_alpha", "1", 0); - ch_alpha->changed = scr_crosshair_changed; + ch_alpha->changed = ch_color_changed; ch_scale = Cvar_Get("ch_scale", "1", 0); - ch_scale->changed = scr_crosshair_changed; + ch_scale->changed = ch_scale_changed; ch_x = Cvar_Get("ch_x", "0", 0); ch_y = Cvar_Get("ch_y", "0", 0); @@ -1363,6 +1359,7 @@ void SCR_Init(void) Cmd_Register(scr_cmds); scr_scale_changed(scr_scale); + ch_color_changed(NULL); scr.initialized = true; } @@ -2028,7 +2025,7 @@ static void SCR_ExecuteLayoutString(const char *s) static void SCR_DrawPause(void) { - int x, y; + int x, y, w, h; if (!sv_paused->integer) return; @@ -2037,15 +2034,16 @@ static void SCR_DrawPause(void) if (scr_showpause->integer != 1) return; - x = (scr.hud_width - scr.pause_width) / 2; - y = (scr.hud_height - scr.pause_height) / 2; + R_GetPicSize(&w, &h, scr.pause_pic); + x = (scr.hud_width - w) / 2; + y = (scr.hud_height - h) / 2; R_DrawPic(x, y, scr.pause_pic); } static void SCR_DrawLoading(void) { - int x, y; + int x, y, w, h; if (!scr.draw_loading) return; @@ -2054,8 +2052,9 @@ static void SCR_DrawLoading(void) R_SetScale(scr.hud_scale); - x = (r_config.width * scr.hud_scale - scr.loading_width) / 2; - y = (r_config.height * scr.hud_scale - scr.loading_height) / 2; + R_GetPicSize(&w, &h, scr.loading_pic); + x = (r_config.width * scr.hud_scale - w) / 2; + y = (r_config.height * scr.hud_scale - h) / 2; R_DrawPic(x, y, scr.loading_pic); @@ -2075,15 +2074,16 @@ static void SCR_DrawHitMarker(void) float frac = (float)(cls.realtime - cl.hit_marker_time) / scr_hit_marker_time->integer; float alpha = 1.0f - (frac * frac); - int w = scr.hit_marker_width * ch_scale->value; - int h = scr.hit_marker_height * ch_scale->value; - - int x = (scr.hud_width - w) / 2; - int y = (scr.hud_height - h) / 2; + int x = (scr.hud_width - scr.hit_marker_width) / 2; + int y = (scr.hud_height - scr.hit_marker_height) / 2; R_SetColor(MakeColor(255, 0, 0, alpha * 255)); - R_DrawStretchPic(x + ch_x->integer, y + ch_y->integer, w, h, scr.hit_marker_pic); + R_DrawStretchPic(x + ch_x->integer, + y + ch_y->integer, + scr.hit_marker_width, + scr.hit_marker_height, + scr.hit_marker_pic); } static void SCR_DrawCrosshair(void) From 267658fce5017350193e4a04ddb4621ddce3d735 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 24 Jul 2024 15:12:05 +0300 Subject: [PATCH 368/974] Make hit marker image optional. Don't print a warning if it is missing and don't draw placeholder pic. --- inc/refresh/refresh.h | 3 +++ src/client/screen.c | 4 ++-- src/refresh/images.c | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index a25311725..2418477ff 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -145,6 +145,9 @@ typedef enum { IF_REPEAT = BIT(6), IF_NEAREST = BIT(7), IF_OPAQUE = BIT(8), + + // not stored in image + IF_OPTIONAL = BIT(16), } imageflags_t; typedef enum { diff --git a/src/client/screen.c b/src/client/screen.c index 71b1a69c1..9629e0306 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1270,7 +1270,7 @@ void SCR_RegisterMedia(void) scr.loading_pic = R_RegisterPic("loading"); scr.net_pic = R_RegisterPic("net"); scr.font_pic = R_RegisterFont(scr_font->string); - scr.hit_marker_pic = R_RegisterPic("marker"); + scr.hit_marker_pic = R_RegisterImage("marker", IT_PIC, IF_PERMANENT | IF_OPTIONAL); scr_crosshair_changed(scr_crosshair); } @@ -2065,7 +2065,7 @@ static void SCR_DrawHitMarker(void) { if (!cl.hit_marker_count) return; - if (scr_hit_marker_time->integer <= 0 || + if (!scr.hit_marker_pic || scr_hit_marker_time->integer <= 0 || cls.realtime - cl.hit_marker_time > scr_hit_marker_time->integer) { cl.hit_marker_count = 0; return; diff --git a/src/refresh/images.c b/src/refresh/images.c index 7ac61237f..64d410b4f 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1687,7 +1687,7 @@ static void print_error(const char *name, imageflags_t flags, int err) case Q_ERR(ENOENT): if (flags == -1) { return; - } else if (flags & IF_PERMANENT) { + } else if ((flags & (IF_PERMANENT | IF_OPTIONAL)) == IF_PERMANENT) { // ugly hack for console code if (strcmp(name, "pics/conchars.pcx")) level = PRINT_WARNING; From 4ec477ae4a4ced308d112929e8bf4a71aed1bf50 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 24 Jul 2024 15:24:09 +0300 Subject: [PATCH 369/974] =?UTF-8?q?Reset=20=E2=80=98scr=5Ffont=E2=80=99=20?= =?UTF-8?q?to=20default=20if=20it=20fails=20to=20register.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/screen.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 9629e0306..019b65dbf 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1246,6 +1246,15 @@ void SCR_ModeChanged(void) scr.hud_scale = R_ClampScale(scr_scale); } +static void scr_font_changed(cvar_t *self) +{ + scr.font_pic = R_RegisterFont(self->string); + if (!scr.font_pic && strcmp(self->string, self->default_string)) { + Cvar_Reset(self); + scr.font_pic = R_RegisterFont(self->default_string); + } +} + /* ================== SCR_RegisterMedia @@ -1269,15 +1278,10 @@ void SCR_RegisterMedia(void) scr.pause_pic = R_RegisterPic("pause"); scr.loading_pic = R_RegisterPic("loading"); scr.net_pic = R_RegisterPic("net"); - scr.font_pic = R_RegisterFont(scr_font->string); scr.hit_marker_pic = R_RegisterImage("marker", IT_PIC, IF_PERMANENT | IF_OPTIONAL); scr_crosshair_changed(scr_crosshair); -} - -static void scr_font_changed(cvar_t *self) -{ - scr.font_pic = R_RegisterFont(self->string); + scr_font_changed(scr_font); } static void scr_scale_changed(cvar_t *self) From 2c913647896c38c32b9ce92446c534d32c83c2d5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Jul 2024 13:58:39 +0300 Subject: [PATCH 370/974] Add q_unreachable() macro. --- inc/shared/platform.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/inc/shared/platform.h b/inc/shared/platform.h index 9dc07fe5a..184c87337 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -133,6 +133,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_unused __attribute__((unused)) +#if q_has_builtin(__builtin_unreachable) +#define q_unreachable() __builtin_unreachable() +#else +#define q_unreachable() abort() +#endif + #else /* __GNUC__ */ #ifdef _MSC_VER @@ -140,11 +146,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_noinline __declspec(noinline) #define q_malloc __declspec(restrict) #define q_alignof(t) __alignof(t) +#define q_unreachable() __assume(0) #else #define q_noreturn #define q_noinline #define q_malloc #define q_alignof(t) 1 +#define q_unreachable() abort() #endif #define q_printf(f, a) From 1ba2309b5327797946199fcdbb28859959d10374 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Jul 2024 14:00:12 +0300 Subject: [PATCH 371/974] Add support for 16-bit and colormapped TGA images. --- inc/common/sizebuf.h | 1 + src/common/sizebuf.c | 6 + src/refresh/images.c | 357 ++++++++++++++++++++++++++----------------- 3 files changed, 223 insertions(+), 141 deletions(-) diff --git a/inc/common/sizebuf.h b/inc/common/sizebuf.h index f62330c7a..ce907d5cb 100644 --- a/inc/common/sizebuf.h +++ b/inc/common/sizebuf.h @@ -57,5 +57,6 @@ static inline uint32_t SZ_Remaining(const sizebuf_t *buf) void *SZ_ReadData(sizebuf_t *buf, size_t len); int SZ_ReadByte(sizebuf_t *sb); int SZ_ReadShort(sizebuf_t *sb); +int SZ_ReadWord(sizebuf_t *sb); int SZ_ReadLong(sizebuf_t *sb); float SZ_ReadFloat(sizebuf_t *sb); diff --git a/src/common/sizebuf.c b/src/common/sizebuf.c index 6ac116693..247ca4cb4 100644 --- a/src/common/sizebuf.c +++ b/src/common/sizebuf.c @@ -154,6 +154,12 @@ int SZ_ReadShort(sizebuf_t *sb) return buf ? (int16_t)RL16(buf) : -1; } +int SZ_ReadWord(sizebuf_t *sb) +{ + byte *buf = SZ_ReadData(sb, 2); + return buf ? (uint16_t)RL16(buf) : -1; +} + int SZ_ReadLong(sizebuf_t *sb) { byte *buf = SZ_ReadData(sb, 4); diff --git a/src/refresh/images.c b/src/refresh/images.c index 64d410b4f..e219e6eaf 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -27,6 +27,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/cvar.h" #include "common/files.h" #include "common/intreadwrite.h" +#include "common/sizebuf.h" #include "system/system.h" #include "format/pcx.h" #include "format/wal.h" @@ -362,186 +363,255 @@ TARGA IMAGES #define TARGA_HEADER_SIZE 18 -#define TGA_DECODE_RAW(x, bpp, a, b, c, d) \ -static int tga_decode_##x##_raw(const byte *in, byte **row_pointers, \ - int cols, int rows, const byte *max_in) \ -{ \ - int col, row; \ - byte *out_row; \ - \ - for (row = 0; row < rows; row++) { \ - out_row = row_pointers[row]; \ - for (col = 0; col < cols; col++, out_row += 4, in += bpp) { \ - out_row[0] = a; \ - out_row[1] = b; \ - out_row[2] = c; \ - out_row[3] = d; \ - } \ - } \ - \ - return Q_ERR_SUCCESS; \ +enum { + TGA_Colormap = 1, + TGA_RGB = 2, + TGA_Mono = 3, + TGA_RLE = 8 +}; + +static uint32_t tga_unpack_pixel(const byte *in, int bpp) +{ + int r, g, b; + + switch (bpp) { + case 1: + return MakeColor(255, 255, 255, in[0]); + case 2: + r = (in[1] & 0x7C) >> 2; + g = ((in[1] & 0x03) << 3) | ((in[0] & 0xE0) >> 5); + b = (in[0] & 0x1F); + return MakeColor(r << 3, g << 3, b << 3, 255); + case 3: + return MakeColor(in[2], in[1], in[0], 255); + case 4: + return MakeColor(in[2], in[1], in[0], in[3]); + default: + q_unreachable(); + } } -#define TGA_DECODE_RLE(x, bpp, a, b, c, d) \ -static int tga_decode_##x##_rle(const byte *in, byte **row_pointers, \ - int cols, int rows, const byte *max_in) \ -{ \ - int col, row; \ - byte *out_row; \ - uint32_t color; \ - int j, packet_header, packet_size; \ - \ - for (row = 0; row < rows; row++) { \ - out_row = row_pointers[row]; \ - \ - for (col = 0; col < cols;) { \ - packet_header = *in++; \ - packet_size = 1 + (packet_header & 0x7f); \ - \ - if (packet_header & 0x80) { \ - if (max_in - in < bpp) \ - return Q_ERR_OVERRUN; \ - \ - color = MakeColor(a, b, c, d); \ - in += bpp; \ - for (j = 0; j < packet_size; j++) { \ - *(uint32_t *)out_row = color; \ - out_row += 4; \ - \ - if (++col == cols) { \ - col = 0; \ - if (++row == rows) \ - return Q_ERR_SUCCESS; \ - out_row = row_pointers[row]; \ - } \ - } \ - } else { \ - if (max_in - in < bpp * packet_size) \ - return Q_ERR_OVERRUN; \ - \ - for (j = 0; j < packet_size; j++) { \ - out_row[0] = a; \ - out_row[1] = b; \ - out_row[2] = c; \ - out_row[3] = d; \ - out_row += 4; \ - in += bpp; \ - \ - if (++col == cols) { \ - col = 0; \ - if (++row == rows) \ - return Q_ERR_SUCCESS; \ - out_row = row_pointers[row]; \ - } \ - } \ - } \ - } \ - } \ - \ - return Q_ERR_SUCCESS; \ +static int tga_decode_raw(sizebuf_t *s, uint32_t **row_pointers, + int cols, int rows, int bpp, + const uint32_t *palette) +{ + int col, row; + uint32_t *out_row; + const byte *in; + + in = SZ_ReadData(s, cols * rows * bpp); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + + for (row = 0; row < rows; row++) { + out_row = row_pointers[row]; + for (col = 0; col < cols; col++) { + if (palette) + *out_row++ = palette[*in]; + else + *out_row++ = tga_unpack_pixel(in, bpp); + in += bpp; + } + } + + return Q_ERR_SUCCESS; } -TGA_DECODE_RAW(mono, 1, 255, 255, 255, in[0]) -TGA_DECODE_RAW(bgr, 3, in[2], in[1], in[0], 255) -TGA_DECODE_RAW(bgra, 4, in[2], in[1], in[0], in[3]) +static int tga_decode_rle(sizebuf_t *s, uint32_t **row_pointers, + int cols, int rows, int bpp, + const uint32_t *palette) +{ + int col, row; + uint32_t *out_row; + uint32_t color; + const byte *in; + int j, packet_header, packet_size; + + for (row = 0; row < rows; row++) { + out_row = row_pointers[row]; + + for (col = 0; col < cols;) { + packet_header = SZ_ReadByte(s); + packet_size = 1 + (packet_header & 0x7f); + + if (packet_header & 0x80) { + in = SZ_ReadData(s, bpp); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + + if (palette) + color = palette[*in]; + else + color = tga_unpack_pixel(in, bpp); + for (j = 0; j < packet_size; j++) { + *out_row++ = color; + + if (++col == cols) { + col = 0; + if (++row == rows) + return Q_ERR_SUCCESS; + out_row = row_pointers[row]; + } + } + } else { + in = SZ_ReadData(s, bpp * packet_size); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + + for (j = 0; j < packet_size; j++) { + if (palette) + color = palette[*in]; + else + color = tga_unpack_pixel(in, bpp); + *out_row++ = color; + in += bpp; + + if (++col == cols) { + col = 0; + if (++row == rows) + return Q_ERR_SUCCESS; + out_row = row_pointers[row]; + } + } + } + } + } -TGA_DECODE_RLE(mono, 1, 255, 255, 255, in[0]) -TGA_DECODE_RLE(bgr, 3, in[2], in[1], in[0], 255) -TGA_DECODE_RLE(bgra, 4, in[2], in[1], in[0], in[3]) + return Q_ERR_SUCCESS; +} IMG_LOAD(TGA) { byte *pixels; - byte *row_pointers[MAX_TEXTURE_SIZE]; - unsigned i, bpp, id_length, colormap_type, image_type, w, h, pixel_size, attributes, offset; - int (*decode)(const byte *, byte **, int, int, const byte *) = NULL; - int ret; + uint32_t *row_pointers[MAX_TEXTURE_SIZE]; + uint32_t colormap[256]; + sizebuf_t s; + unsigned id_length, colormap_type, image_type, colormap_start, + colormap_length, colormap_size, w, h, pixel_size, attributes; + bool rle; + int i, ret; - if (rawlen < TARGA_HEADER_SIZE) { + if (rawlen < TARGA_HEADER_SIZE) return Q_ERR_FILE_TOO_SMALL; - } - - id_length = rawdata[0]; - colormap_type = rawdata[1]; - image_type = rawdata[2]; - w = RL16(&rawdata[12]); - h = RL16(&rawdata[14]); - pixel_size = rawdata[16]; - attributes = rawdata[17]; - // skip TARGA image comment - offset = TARGA_HEADER_SIZE + id_length; - if (offset + 4 > rawlen) { - Com_SetLastError("bad offset"); + SZ_InitRead(&s, rawdata, rawlen); + + id_length = SZ_ReadByte(&s); + colormap_type = SZ_ReadByte(&s); + image_type = SZ_ReadByte(&s); + colormap_start = SZ_ReadWord(&s); + colormap_length = SZ_ReadWord(&s); + colormap_size = SZ_ReadByte(&s); + s.readcount += 4; + w = SZ_ReadWord(&s); + h = SZ_ReadWord(&s); + pixel_size = SZ_ReadByte(&s); + attributes = SZ_ReadByte(&s); + + rle = image_type & TGA_RLE; + image_type &= ~TGA_RLE; + + switch (image_type) { + case TGA_Colormap: + case TGA_RGB: + case TGA_Mono: + break; + default: + Com_SetLastError("unsupported targa image type"); return Q_ERR_INVALID_FORMAT; } - if (colormap_type) { - Com_SetLastError("color mapped targa images are not supported"); + if (check_image_size(w, h)) { + Com_SetLastError("invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } - if (pixel_size == 32) { - bpp = 4; - } else if (pixel_size == 24) { - bpp = 3; - } else if (pixel_size == 8) { - bpp = 1; - } else { + switch (pixel_size) { + case 8: + case 15: + case 16: + case 24: + case 32: + break; + default: Com_SetLastError("unsupported number of bits per pixel"); return Q_ERR_INVALID_FORMAT; } - if (check_image_size(w, h)) { - Com_SetLastError("invalid image dimensions"); + if (attributes & 0xC0) { + Com_SetLastError("interleaved targa images are not supported"); return Q_ERR_INVALID_FORMAT; } - if (image_type == 2) { - if (pixel_size == 32) { - decode = tga_decode_bgra_raw; - } else if (pixel_size == 24) { - decode = tga_decode_bgr_raw; - } - } else if (image_type == 3) { - if (pixel_size == 8) { - decode = tga_decode_mono_raw; - } - } else if (image_type == 10) { - if (pixel_size == 32) { - decode = tga_decode_bgra_rle; - } else if (pixel_size == 24) { - decode = tga_decode_bgr_rle; + if (image_type == TGA_Colormap) { + if (!colormap_type) { + Com_SetLastError("colormapped image but no colormap present"); + return Q_ERR_INVALID_FORMAT; } - } else if (image_type == 11) { - if (pixel_size == 8) { - decode = tga_decode_mono_rle; + if (pixel_size != 8) { + Com_SetLastError("only 8-bit colormaps are supported"); + return Q_ERR_INVALID_FORMAT; } } - if (!decode) { - Com_SetLastError("unsupported targa image type"); - return Q_ERR_INVALID_FORMAT; - } - if (image_type == 2 || image_type == 3) { - if (offset + w * h * bpp > rawlen) { - Com_SetLastError("data out of bounds"); + // skip TARGA image comment + if (!SZ_ReadData(&s, id_length)) + return Q_ERR_UNEXPECTED_EOF; + + // read the colormap + if (colormap_type) { + int colormap_bpp = (colormap_size + 1) / 8; + const byte *in; + + switch (colormap_size) { + case 15: + case 16: + case 24: + case 32: + break; + default: + Com_SetLastError("unsupported number of bits per colormap pixel"); return Q_ERR_INVALID_FORMAT; } + + if (colormap_start + colormap_length > 256) { + Com_SetLastError("too many colormap entries"); + return Q_ERR_INVALID_FORMAT; + } + + in = SZ_ReadData(&s, colormap_length * colormap_bpp); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + + // don't bother unpacking palette unless we need it + if (image_type == TGA_Colormap) { + if (colormap_length != 256) + memset(colormap, 0, sizeof(colormap)); + + for (i = 0; i < colormap_length; i++) { + colormap[colormap_start + i] = tga_unpack_pixel(in, colormap_bpp); + in += colormap_bpp; + } + } } pixels = IMG_AllocPixels(w * h * 4); if (attributes & 32) { for (i = 0; i < h; i++) { - row_pointers[i] = pixels + i * w * 4; + row_pointers[i] = (uint32_t *)(pixels + i * w * 4); } } else { for (i = 0; i < h; i++) { - row_pointers[i] = pixels + (h - i - 1) * w * 4; + row_pointers[i] = (uint32_t *)(pixels + (h - i - 1) * w * 4); } } - ret = decode(rawdata + offset, row_pointers, w, h, rawdata + rawlen); + if (rle) + ret = tga_decode_rle(&s, row_pointers, w, h, (pixel_size + 1) / 8, + (image_type == TGA_Colormap) ? colormap : NULL); + else + ret = tga_decode_raw(&s, row_pointers, w, h, (pixel_size + 1) / 8, + (image_type == TGA_Colormap) ? colormap : NULL); if (ret < 0) { IMG_FreePixels(pixels); return ret; @@ -552,10 +622,15 @@ IMG_LOAD(TGA) image->upload_width = image->width = w; image->upload_height = image->height = h; - if (pixel_size == 24) - image->flags |= IF_OPAQUE; - else if (pixel_size == 8) + if (colormap_type) { + image->flags |= IF_PALETTED; + pixel_size = colormap_size; + } + + if (pixel_size == 8) image->flags |= IF_TRANSPARENT; + else if (pixel_size != 32) + image->flags |= IF_OPAQUE; return Q_ERR_SUCCESS; } From 96f9ab2c7685f22ac62cb659a0616495cd366c50 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 25 Jul 2024 21:22:17 +0300 Subject: [PATCH 372/974] Add support for interleaved and right-to-left TGA images. --- src/refresh/images.c | 58 ++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index e219e6eaf..ad5f46ebd 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -370,6 +370,13 @@ enum { TGA_RLE = 8 }; +enum { + TGA_RIGHTTOLEFT = BIT(4), + TGA_TOPTOBOTTOM = BIT(5), + TGA_INTERLEAVE_2 = BIT(6), + TGA_INTERLEAVE_4 = BIT(7), +}; + static uint32_t tga_unpack_pixel(const byte *in, int bpp) { int r, g, b; @@ -482,14 +489,14 @@ static int tga_decode_rle(sizebuf_t *s, uint32_t **row_pointers, IMG_LOAD(TGA) { - byte *pixels; + byte *pixels, *start; uint32_t *row_pointers[MAX_TEXTURE_SIZE]; uint32_t colormap[256]; sizebuf_t s; unsigned id_length, colormap_type, image_type, colormap_start, colormap_length, colormap_size, w, h, pixel_size, attributes; bool rle; - int i, ret; + int i, j, ret, stride, interleave; if (rawlen < TARGA_HEADER_SIZE) return Q_ERR_FILE_TOO_SMALL; @@ -538,8 +545,18 @@ IMG_LOAD(TGA) return Q_ERR_INVALID_FORMAT; } - if (attributes & 0xC0) { - Com_SetLastError("interleaved targa images are not supported"); + switch (attributes & (TGA_INTERLEAVE_2 | TGA_INTERLEAVE_4)) { + case 0: + interleave = 1; + break; + case TGA_INTERLEAVE_2: + interleave = 2; + break; + case TGA_INTERLEAVE_4: + interleave = 4; + break; + default: + Com_SetLastError("unsupported interleaving flag"); return Q_ERR_INVALID_FORMAT; } @@ -595,34 +612,43 @@ IMG_LOAD(TGA) } } - pixels = IMG_AllocPixels(w * h * 4); - if (attributes & 32) { - for (i = 0; i < h; i++) { - row_pointers[i] = (uint32_t *)(pixels + i * w * 4); - } - } else { - for (i = 0; i < h; i++) { - row_pointers[i] = (uint32_t *)(pixels + (h - i - 1) * w * 4); - } + stride = w * 4; + start = pixels = IMG_AllocPixels(h * stride); + + if (!(attributes & TGA_TOPTOBOTTOM)) { + start += (h - 1) * stride; + stride = -stride; + } + + for (i = j = 0; i < h; i++) { + row_pointers[i] = (uint32_t *)(start + j * stride); + j += interleave; + if (j >= h) + j = (j + 1) & (interleave - 1); } if (rle) ret = tga_decode_rle(&s, row_pointers, w, h, (pixel_size + 1) / 8, - (image_type == TGA_Colormap) ? colormap : NULL); + image_type == TGA_Colormap ? colormap : NULL); else ret = tga_decode_raw(&s, row_pointers, w, h, (pixel_size + 1) / 8, - (image_type == TGA_Colormap) ? colormap : NULL); + image_type == TGA_Colormap ? colormap : NULL); if (ret < 0) { IMG_FreePixels(pixels); return ret; } + if (attributes & TGA_RIGHTTOLEFT) + for (i = 0; i < h; i++) + for (j = 0; j < w / 2; j++) + SWAP(uint32_t, row_pointers[i][j], row_pointers[i][w - j - 1]); + *pic = pixels; image->upload_width = image->width = w; image->upload_height = image->height = h; - if (colormap_type) { + if (image_type == TGA_Colormap) { image->flags |= IF_PALETTED; pixel_size = colormap_size; } From 41f6f77807f507f9daa1606e51aa68c6cf38ecc4 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 25 Jul 2024 20:45:09 -0400 Subject: [PATCH 373/974] Moved some externs around, nav_edit works in Linux! --- src/action/acesrc/acebot.h | 9 + src/action/acesrc/acebot_ai.c | 3 +- src/action/acesrc/acebot_nodes.c | 7 +- src/action/botlib/botlib.h | 3 - src/action/botlib/botlib_movement.c | 14 +- src/action/botlib/botlib_nav.c | 13 +- src/action/botlib/botlib_nodes.c | 13 +- src/client/client.h | 4 + src/common/cmodel.c | 17 + src/refresh/main.c | 4281 +++++++++++++++++++++++++++ 10 files changed, 4339 insertions(+), 25 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 258174219..b015957e2 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -832,4 +832,13 @@ void ThreadLock(void); void ThreadUnlock(void); //rekkie -- Quake3 -- e + +extern int num_poi_nodes; +extern int poi_nodes[MAX_POI_NODES]; +extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +extern int num_vis_nodes; +extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +extern int node_vis_list[10][MAX_VIS_NODES]; // Cached node +extern node_t *nodes; +extern nmesh_t nmesh; #endif diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 4cc1a3e18..e4d6e15a1 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -914,7 +914,8 @@ void ACEAI_PickSafeGoal(edict_t *self) self->state = STATE_FLEE; self->tries = 0; // Reset the count of how many times we tried this goal - // This func has been deprecated in favor of BOTLIB_CanVisitNode, but it's unusable here + // This func has been deprecated in favor of BOTLIB_CanVisitNode + BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, false, INVALID, false); //BOTLIB_SetGoal(self,i); self->wander_timeout = level.framenum + 2.0 * HZ; // LTK_Say (self, "Under fire, extracting!"); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index e3696c3ef..98c462efa 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -77,6 +77,11 @@ int show_path_to = INVALID; // array for node data //rekkie -- DEV_1 -- s node_t *unsorted_nodes; // Used to generate all links, so they can be sorted, then copied to nodes +edict_t *node_ents[MAX_EDICTS]; // Define the node_ents variable +int num_vis_nodes = 0; // Number of visible nodes +int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM +int node_vis_list[10][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. + //node_t *nodes = NULL; // Memory allocated nodes //node_t nodes[MAX_PNODES]; //#define MAX_PNODES 65536 // Absolute max nodes 64k @@ -299,7 +304,7 @@ int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) } } - Com_Printf("%s [%s] node %d\n", __func__, self->client->pers.netname, node); + //Com_Printf("%s [%s] node %d\n", __func__, self->client->pers.netname, node); return node; } diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 30907c98b..66702fa54 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -480,7 +480,4 @@ int BOTLIB_GetEquipment(edict_t* self); //rekkie -- collecting weapons, items, ammo -- e - - - #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 21667177a..9bd86ff48 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5881,11 +5881,11 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Otherwise pick from various wait times before moving out int rnd_rng = rand() % 4; if (rnd_rng == 0) - self->just_spawned_timeout = level.framenum + (random() * 6) * HZ; // Long wait + self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Long wait else if (rnd_rng == 1) - self->just_spawned_timeout = level.framenum + (random() * 4) * HZ; // Medium wait + self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Medium wait else if (rnd_rng == 2) - self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait + self->just_spawned_timeout = level.framenum + (random() * 1) * HZ; // Short wait else self->just_spawned_timeout = 0; // No wait } @@ -6391,7 +6391,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) //Com_Printf("%s %s invalid types node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); // Path failed, so try again - //BOTLIB_CanVisitNode(self, self->bot.goal_node, false, nodes[self->bot.goal_node].area); + //BOTLIB_CanVisitNode(self, self->bot.goal_node, false, nodes[self->bot.goal_node].area, true); /* if (self->bot.goal_node != INVALID) @@ -6444,6 +6444,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; self->bot.bi.actionflags &= ~ACTION_CROUCH; + gi.dprintf("I'm in the water %s and my air is %d\n", self->client->pers.netname, self->air_finished_framenum - level.framenum); //VectorClear(self->bot.bi.dir); //self->bot.bi.speed = 0; @@ -6458,7 +6459,8 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) int contents_feet = gi.pointcontents(temp); // Almost out of air, start heading up to the surface - if ((contents_feet & MASK_WATER) && self->air_finished_framenum < level.framenum + 5) // Move up to get air + //if ((contents_feet & MASK_WATER) && self->air_finished_framenum < level.framenum + 5) // Move up to get air + if (self->air_finished_framenum < level.framenum + 5) // Move up to get air { self->bot.bi.actionflags |= ACTION_MOVEUP; self->bot.node_travel_time = 0; // Ignore node travel time while we get some air @@ -6496,7 +6498,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); bot_to_node[2] = 0; float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node - if (xy_bot_to_next_dist > 32 && xy_bot_to_next_dist <= 128) + if (xy_bot_to_next_dist > 16 && xy_bot_to_next_dist <= 150) { // Line of sight tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 11dcb796d..d851b29ec 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -631,13 +631,14 @@ void BOTLIB_InitAreaConnections(void) targetNode = nodes[n].links[l].targetNode; targetArea = nodes[targetNode].area; - // ## darksaint: removed area check for creating adjacency matrix + //nav_area.adjacency_matrix[i][targetArea] = targetArea; //gi.dprintf("Node[%d] Area[%d] connects to Node[%d] Area[%d]\n", n, i, targetNode, targetArea); - //if (targetArea != INVALID && targetArea != i) - // if (targetArea != i) - // { - nav_area.adjacency_matrix[i][targetArea] = targetArea; - //} + if (targetArea != INVALID && targetArea != i) { + if (targetArea != i) + { + nav_area.adjacency_matrix[i][targetArea] = targetArea; + } + } } } } diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 361d3bce9..100252823 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -2,14 +2,11 @@ #include "../acesrc/acebot.h" #include "botlib.h" -int num_poi_nodes; -int poi_nodes[MAX_POI_NODES]; -edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -int num_vis_nodes; -int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -int node_vis_list[10][MAX_VIS_NODES]; // Cached node -node_t *nodes; -nmesh_t nmesh; +int num_poi_nodes = 0; +int poi_nodes[MAX_POI_NODES] = { INVALID }; +// edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) +node_t *nodes = NULL; +nmesh_t nmesh = { 0 }; // Free nodes void BOTLIB_FreeNodes(void) diff --git a/src/client/client.h b/src/client/client.h index 5ae5e2d07..b1295b70db 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -729,6 +729,10 @@ typedef struct { int entity1; int entity2; int time; + //rekkie -- surface data -- s + unsigned int acolor; // arrow color + byte width; // line width + //rekkie -- surface data -- e } tent_params_t; typedef struct { diff --git a/src/common/cmodel.c b/src/common/cmodel.c index dc386cb7a..70b5bb1bf 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -180,6 +180,23 @@ void CM_FreeMap(cm_t *cm) BSP_Free(cm->cache); + //rekkie -- surface data -- s + //nav_t* tmpnav = cm->nav; // Save nav pointer + //if (cm->nav != NULL) cm->nav = NULL; // Unhook the nav pointer so it doesn't get cleared by memset + if (cm->nav) + { + if (cm->nav->surface_data_faces) + { + Z_Free(cm->nav->surface_data_faces); + //free(cm->nav->surface_data_faces); + //cm->nav->surface_data_faces = NULL; + } + Z_Free(cm->nav); + //free(cm->nav); + //cm->nav = NULL; + } + //rekkie -- surface data -- e + memset(cm, 0, sizeof(*cm)); } diff --git a/src/refresh/main.c b/src/refresh/main.c index ce82ec142..11b80c035 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -433,6 +433,4287 @@ static void GL_DrawNullModel(void) //rekkie -- allow NullModels to be seen behind walls -- e } + +#if 0 +//rekkie -- gl_showedges -- s +#include "../server/server.h" + +#define STEPSIZE 18 +#define MAX_JUMP_HEIGHT 60 // Maximum height that a player can jump to reach a higher level surface, such as a box +#define MAX_FALL_HEIGHT 210 // Maximum height that a player can fall from without damage +#define MAX_SAFE_CROUCH_FALL_HEIGHT 214 // Maximum height that a player can crouch fall from without damage +#define MAX_CROUCH_FALL_HEIGHT 224 // Maximum height that a player can crouch fall from without damage +#define MAX_CROUCH_FALL_HEIGHT_UNSAFE 256 // Maximum height that a player can crouch fall + minor leg damage +#define MAX_STEEPNESS 0.71 +#define MAX_FACE_VERTS 64 +#define INVALID -1 +#define VectorLength(v) (sqrtf(DotProduct((v),(v)))) + +const qboolean IgnoreSmallSurfaces = false; + +void CM_BoxTrace(trace_t* trace, vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs, mnode_t* headnode, int brushmask); +int CM_PointContents(vec3_t p, mnode_t* headnode); +trace_t q_gameabi SV_Trace(vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end, edict_t* passedict, int contentmask); + +// Returns the distance from origin to target +float UTIL_Distance(vec3_t origin, vec3_t target) +{ + vec3_t dist; + VectorSubtract(target, origin, dist); + return VectorLength(dist); +} + +/* +============= +TempVector + +This is just a convenience function +for making temporary vectors for function calls +============= +*/ +/* +float* tv(float x, float y, float z) +{ + static int index; + static vec3_t vecs[8]; + float* v; + + // use an array so that multiple tempvectors won't collide + // for a while + v = vecs[index]; + index = (index + 1) & 7; + + v[0] = x; + v[1] = y; + v[2] = z; + + return v; +} +*/ + +void G_ProjectSource(vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result) +{ + result[0] = point[0] + forward[0] * distance[0] + right[0] * distance[1]; + result[1] = point[1] + forward[1] * distance[0] + right[1] * distance[1]; + result[2] = point[2] + forward[2] * distance[0] + right[2] * distance[1] + distance[2]; +} + +float VectorDistance(vec3_t start, vec3_t end) +{ + vec3_t v = { 0 }; + VectorSubtract(end, start, v); + return VectorLength(v); +} + + + +// TODO: Add GL_ColorPolygon() to the draw chain of rendered faces, this will fix the issue of entities being drawn over highlighted faces, +// such as the func_wall entities found in Actctiy3, the ladder faces and one of the walkways +// +// Blends an color + alpha over a texture (useful to highlight faces for visual debugging) +void GL_ColorPolygon(vec3_t verts, int num_points, surface_data_t* face_data, float red, float green, float blue, float alpha) +{ + int i; + vec3_t v[MAX_FACE_VERTS]; + const float HEIGHT = 0.03; + + if (num_points > MAX_FACE_VERTS) + { + Com_Printf("%s - num_points > MAX_FACE_VERTS\n", __func__); + return; + } + + vec3_t vec, tmp; + MoveAwayFromNormal(face_data->drawflags, face_data->normal, vec, HEIGHT); // Move vec away from the surface normal + + //if (face_data->normal[0] == 0 && face_data->normal[1] == 0 && face_data->normal[2] == 0) + { + //Com_Printf("%s - face_data->normal == 0\n", __func__); + //return; + } + + // Copy and lift the verts up a tiny bit so they're not z-clipping the surface + for (i = 0; i < num_points; i++) + { + //VectorCopy(verts + i * 3, v[i]); + //v[i][2] += 0.03; + + VectorCopy(verts + i * 3, tmp); + VectorAdd(tmp, vec, v[i]); // Move tmp away from the surface normal + } + + // Draw solid shade + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_Color(red, green, blue, alpha); + + glBegin(GL_POLYGON); + for (i = 0; i < num_points; i++) + glVertex3fv((float*)v + i * 3); + glEnd(); +} + +float HalfPlaneSign(vec2_t p1, vec2_t p2, vec2_t p3) +{ + return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); +} + +// Check if point is inside triangle. +// PT = (x,y,z) point location. V1,V2,V3 = 3 points of a triangle (x,y,z) +qboolean PointInTriangle(vec3_t pt, vec3_t v1, vec3_t v2, vec3_t v3) +{ + float d1, d2, d3; + qboolean has_neg, has_pos; + + d1 = HalfPlaneSign(pt, v1, v2); + d2 = HalfPlaneSign(pt, v2, v3); + d3 = HalfPlaneSign(pt, v3, v1); + + has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0); + has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0); + + return !(has_neg && has_pos); +} + +// A function that finds the center of a 3D convex polygon +void BOTLIB_UTIL_PolygonCenter2D(vec3_t* v, int num_verts, vec3_t center) +{ + vec3_t sum = { 0,0,0 }; + for (int i = 0; i < num_verts; i++) + { + VectorAdd(sum, v[i], sum); + } + VectorScale(sum, 1.0 / num_verts, center); +} + +// Cycles through RGB colors (0.0 to 1.0) +// Speed: 0.001 works well +// Example use: +/* + float cycle_red; + float cycle_green; + float cycle_blue; + UTIL_CycleThroughColors(&cycle_red, &cycle_green, &cycle_blue, 0.001); +*/ +void UTIL_CycleThroughColors(float* red, float* green, float* blue, float speed) +{ + static int phase = 0; + + float frequency = speed; + float center = 0.5f; + float width = 0.5f; + float redValue = sin(frequency * phase + 0) * width + center; + float greenValue = sin(frequency * phase + 2) * width + center; + float blueValue = sin(frequency * phase + 4) * width + center; + + *red = redValue; + *green = greenValue; + *blue = blueValue; + + phase++; + if (phase > 100000) + phase = 0; +} + +//=========================================================================== +// returns the surface area of the given face +// +// Parameter: - +// Returns: - +// Changes Globals: - +//=========================================================================== +float AAS_FaceArea(surface_data_t* surface) +{ + //int i, edgenum, side; + float total; + //vec_t* v; + vec3_t d1, d2, cross; + //aas_edge_t* edge; + + //edgenum = aasworld.edgeindex[face->firstedge]; + //side = edgenum < 0; + //edge = &aasworld.edges[abs(edgenum)]; + //v = aasworld.vertexes[edge->v[side]]; + + total = 0; + //for (int i = 1; i < face->numedges - 1; i++) + for (int i = 2; i < surface->num_aligned_verts; i += 2) // edges + { + //edgenum = aasworld.edgeindex[face->firstedge + i]; + //side = edgenum < 0; + //edge = &aasworld.edges[abs(edgenum)]; + VectorSubtract(surface->aligned_verts[i + 0], surface->first_vert, d1); + VectorSubtract(surface->aligned_verts[i + 1], surface->first_vert, d2); + CrossProduct(d1, d2, cross); + total += 0.5 * VectorLength(cross); + } //end for + return total; +} //end of the function AAS_FaceArea +//=========================================================================== +// a simple cross product +// +// Parameter: - +// Returns: - +// Changes Globals: - +//=========================================================================== +// void AAS_OrthogonalToVectors(vec3_t v1, vec3_t v2, vec3_t res) +#define AAS_OrthogonalToVectors(v1, v2, res) \ + (res)[0] = ((v1)[1] * (v2)[2]) - ((v1)[2] * (v2)[1]);\ + (res)[1] = ((v1)[2] * (v2)[0]) - ((v1)[0] * (v2)[2]);\ + (res)[2] = ((v1)[0] * (v2)[1]) - ((v1)[1] * (v2)[0]); +//=========================================================================== +// Tests if the given point is within the face boundaries +// +// Parameter: face : face to test if the point is in it +// pnormal : normal of the plane to use for the face +// point : point to test if inside face boundaries +// Returns: true if the point is within the face boundaries +// Changes Globals: - +//=========================================================================== +qboolean AAS_InsideFace(surface_data_t* surface, vec3_t point, float epsilon, qboolean line_trace) +{ + vec3_t v1, v2; + vec3_t edgevec, pointvec, sepnormal; + + // Scan center point only + if (line_trace) + { + for (int v = 0; v < surface->num_aligned_verts; v += 2) // edges + { + VectorCopy(surface->aligned_verts[v + 0], v1); + VectorCopy(surface->aligned_verts[v + 1], v2); + //edge vector + VectorSubtract(v2, v1, edgevec); + //vector from first edge point to point possible in face + VectorSubtract(point, v1, pointvec); + //get a vector pointing inside the face orthogonal to both the + //edge vector and the normal vector of the plane the face is in + //this vector defines a plane through the origin (first vertex of + //edge) and through both the edge vector and the normal vector + //of the plane + AAS_OrthogonalToVectors(edgevec, surface->normal, sepnormal); + //check on which side of the above plane the point is + //this is done by checking the sign of the dot product of the + //vector orthogonal vector from above and the vector from the + //origin (first vertex of edge) to the point + //if the dotproduct is smaller than zero the point is outside the face + if (DotProduct(pointvec, sepnormal) < -epsilon) return qfalse; + } + + return true; // Found point + } + + // Scan center + forward,back,left,right of center with an offset + else + { + qboolean found; + vec3_t pnt; + const float offset = 16; // Offset from center point + + for (int p = 0; p < 5; p++) + { + found = true; // Reset + + VectorCopy(point, pnt); // Reset pnt + + + if (p == 0) // p == 0 is the center point + ; + else if (p == 1) + pnt[0] += offset; // +X + else if (p == 2) + pnt[0] -= offset; // -X + else if (p == 3) + pnt[1] += offset; // +Y + else + pnt[1] -= offset; // -Y + + for (int v = 0; v < surface->num_aligned_verts; v += 2) // edges + { + VectorCopy(surface->aligned_verts[v + 0], v1); + VectorCopy(surface->aligned_verts[v + 1], v2); + //edge vector + VectorSubtract(v2, v1, edgevec); + //vector from first edge point to point possible in face + VectorSubtract(pnt, v1, pointvec); + //get a vector pointing inside the face orthogonal to both the + //edge vector and the normal vector of the plane the face is in + //this vector defines a plane through the origin (first vertex of + //edge) and through both the edge vector and the normal vector + //of the plane + AAS_OrthogonalToVectors(edgevec, surface->normal, sepnormal); + //check on which side of the above plane the point is + //this is done by checking the sign of the dot product of the + //vector orthogonal vector from above and the vector from the + //origin (first vertex of edge) to the point + //if the dotproduct is smaller than zero the point is outside the face + if (DotProduct(pointvec, sepnormal) < -epsilon) + { + found = false; // Flag as failure + break; + } + } //end for + + if (found) // If no failures found + return true; // We found a point + } + + return false; // Failed to find a point + } + +} //end of the function AAS_InsideFace + +qboolean UTIL_InsideFace(surface_data_t* face_data, vec3_t point, float epsilon) +{ + vec3_t v1, v2; + vec3_t edgevec, pointvec, sepnormal; + + for (int v = 0; v < face_data->num_aligned_verts; v += 2) // edges + { + VectorCopy(face_data->aligned_verts[v + 0], v1); + VectorCopy(face_data->aligned_verts[v + 1], v2); + //edge vector + VectorSubtract(v2, v1, edgevec); + //vector from first edge point to point possible in face + VectorSubtract(point, v1, pointvec); + //get a vector pointing inside the face orthogonal to both the + //edge vector and the normal vector of the plane the face is in + //this vector defines a plane through the origin (first vertex of + //edge) and through both the edge vector and the normal vector + //of the plane + AAS_OrthogonalToVectors(edgevec, face_data->normal, sepnormal); + //check on which side of the above plane the point is + //this is done by checking the sign of the dot product of the + //vector orthogonal vector from above and the vector from the + //origin (first vertex of edge) to the point + //if the dotproduct is smaller than zero the point is outside the face + if (DotProduct(pointvec, sepnormal) < -epsilon) return qfalse; + } + + return true; // Found point +} + +//=========================================================================== +// Tests if the given point is within the face boundaries +// +// Parameter: face : face to test if the point is in it +// pnormal : normal of the plane to use for the face +// point : point to test if inside face boundaries +// Returns: true if the point is within the face boundaries +// Changes Globals: - +//=========================================================================== +qboolean AAS_InsideFaceBoxScan(surface_data_t* surface, vec3_t point, float epsilon, qboolean line_trace) +{ + // Try line trace first + if (UTIL_InsideFace(surface, point, epsilon)) + return true; + + // Try to scan in a grid pattern + vec3_t pnt = { 0, 0, 0 }; // Point to test + float offset = 0; // Offset from center point + const float grid_size = 16; // Offset maximum from center point + while (offset < grid_size) // Scan until grid_size is reached + { + offset += 4; // Increase offset - check every nth steps + + for (int p = 0; p < 8; p++) // Scan: forward, back, left, right + diagonals + { + VectorCopy(point, pnt); // Reset pnt to point + + // Forward, back, left, right scans + if (p == 0) + pnt[0] += offset; // +X + else if (p == 1) + pnt[0] -= offset; // -X + else if (p == 2) + pnt[1] += offset; // +Y + else if (p == 3) + pnt[1] -= offset; // -Y + // Diagonal scans + else if (p == 4) + { + pnt[0] += offset; // +X + pnt[1] += offset; // +Y + } + else if (p == 5) + { + pnt[0] += offset; // +X + pnt[1] -= offset; // -Y + } + else if (p == 6) + { + pnt[0] -= offset; // -X + pnt[1] += offset; // +Y + } + else if (p == 7) + { + pnt[0] -= offset; // -X + pnt[1] -= offset; // -Y + } + + // This is expensive, but rarely used. More often than not this occurs on a slope + // when a line trace from the center of the player to the ground does not hit the ground; + // Instances like this occur if the player collision box is on the edge of a slope. + // Therefore, we need to find the Z height at each offset adjustment. + if (line_trace) + { + vec3_t start, end; + VectorCopy(pnt, start); + VectorCopy(pnt, end); + start[2] += 16; // Start 16 units above the point + end[2] -= 106; // End (90 + 16) units below the point + trace_t tr = SV_Trace(start, NULL, NULL, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored + if (tr.fraction == 1.0) + continue; // Skip this point if it doesn't hit anything + else + pnt[2] = tr.endpos[2]; // Set the Z height to the trace endpos + } + + if (UTIL_InsideFace(surface, pnt, epsilon)) // Try with the new offset + return true; // Success: found a face! + } + } + return false; // Fail: could not find a face +} + +// Moves a vector away from a face based on the normal of the surface and distance +// drawflags = DSURF_PLANEBACK if the surface is facing away from the camera +// normal = The normal of the surface +// out = The vector to move +// distance = The distance to move the vector +void MoveAwayFromNormal(const int drawflags, const vec3_t normal, vec3_t out, const float distance) +{ + VectorClear(out); + + vec3_t norm; + VectorCopy(normal, norm); + if (drawflags & DSURF_PLANEBACK) + { + //VectorInverse(norm); + } + + // Floors + if (norm[2] == 1) + out[2] = distance; + // Slopes + else if (norm[0] >= -MAX_STEEPNESS && norm[0] <= MAX_STEEPNESS && norm[1] >= -MAX_STEEPNESS && norm[1] <= MAX_STEEPNESS && norm[2] >= MAX_STEEPNESS) + { + out[2] = (distance * norm[2]); + + if (norm[0] != 0) + out[0] = (distance * norm[0]); + if (norm[1] != 0) + out[1] = (distance * norm[1]); + } + // Walls and steep slopes + else + { + out[2] = (distance * norm[2]); + + if (norm[0] != 0) + out[0] = (distance * norm[0]); + if (norm[1] != 0) + out[1] = (distance * norm[1]); + } + + /* + // Floors + if (normal[2] == 1) + out[2] += distance; + // Slopes + else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= MAX_STEEPNESS) + { + out[2] += (distance * normal[2]); + + if (drawflags & DSURF_PLANEBACK) + { + if (normal[0] != 0) + out[0] -= (distance * normal[0]); + if (normal[1] != 0) + out[1] -= (distance * normal[1]); + } + else + { + if (normal[0] != 0) + out[0] += (distance * normal[0]); + if (normal[1] != 0) + out[1] += (distance * normal[1]); + } + } + // Walls and steep slopes + else + { + out[2] += (distance * normal[2]); + + if (drawflags & DSURF_PLANEBACK) + { + if (normal[0] != 0) + out[0] -= (distance * normal[0]); + if (normal[1] != 0) + out[1] -= (distance * normal[1]); + } + else + { + if (normal[0] != 0) + out[0] += (distance * normal[0]); + if (normal[1] != 0) + out[1] += (distance * normal[1]); + } + } + */ +} + +// Project trace from the camera view (center screen) to the world +// parameters: distance (absolute max: 8192) +trace_t UTIL_CameraTrace(float distance) +{ + //const float distance = 64; // Max ray cast distance (absolute max: 8192) + //const float min_max = 0.01; // Min/max value for the mins/maxs of the trace + //vec3_t mins = { -min_max,-min_max,-min_max }; + //vec3_t maxs = { min_max,min_max,min_max }; + vec3_t start, end; + vec3_t forward, right; + vec3_t view_offset = { 0 }; + AngleVectors(glr.fd.viewangles, forward, right, NULL); + //VectorSet(view_offset, 0, 0, 0); + //view_offset[1] = 0; + G_ProjectSource(glr.fd.vieworg, view_offset, forward, right, start); + VectorMA(start, distance, forward, end); + return SV_Trace(start, NULL, NULL, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored + //return SV_Trace(start, mins, maxs, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored +} +// Project trace from the player's feet (from camera origin) to the world below +trace_t UTIL_FeetLineTrace(void) +{ + vec3_t start, end; + vec3_t forward, right; + vec3_t view_offset = { 0, 0, 0 }; + AngleVectors(glr.fd.viewangles, forward, right, NULL); + G_ProjectSource(glr.fd.vieworg, view_offset, forward, right, start); + VectorCopy(start, end); + end[2] -= 90; + return SV_Trace(start, NULL, NULL, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored +} +// Project trace from the player's feet (from camera origin) to the world below +trace_t UTIL_FeetBoxTrace(void) +{ + const float min_max = 16; // Min/max value for the mins/maxs of the trace + vec3_t mins = { -min_max,-min_max, 0 }; + vec3_t maxs = { min_max,min_max, 1 }; + //vec3_t mins = { -min_max,-min_max,-min_max }; + //vec3_t maxs = { min_max,min_max,min_max }; + vec3_t start, end; + vec3_t forward, right; + vec3_t view_offset = { 0, 0, 0 }; + AngleVectors(glr.fd.viewangles, forward, right, NULL); + G_ProjectSource(glr.fd.vieworg, view_offset, forward, right, start); + VectorCopy(start, end); + end[2] -= 90; + return SV_Trace(start, mins, maxs, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored +} +// Project trace of a player's box size (from camera origin) +trace_t UTIL_PlayerBoxTraceStanding(qboolean crouching) +{ + vec3_t mins = { -16, -16, 24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t start, end; + vec3_t forward, right; + vec3_t view_offset = { 0, 0, 0 }; + AngleVectors(glr.fd.viewangles, forward, right, NULL); + G_ProjectSource(glr.fd.vieworg, view_offset, forward, right, start); + VectorCopy(start, end); + if (crouching) maxs[2] = 8; + return SV_Trace(start, mins, maxs, end, NULL, MASK_SOLID); // Picked MASK_SOLID over MASK_PLAYERSOLID so player clip on ladders is ignored +} + +// Checks if two face normals are similar, with a leeway +qboolean UTIL_FaceNormalsMatch(const vec3_t normal_1, const vec3_t normal_2) +{ + const float leeway = 0.03; + vec3_t normal, n1, n2; + VectorCopy(normal_1, n1); + VectorCopy(normal_2, n2); + + // Normalize range between 0 and 1 + if (n1[0] == -1) + n1[0] = 1; + if (n1[1] == -1) + n1[1] = 1; + if (n1[2] == -1) + n1[2] = 1; + // Normalize range between 0 and 1 + if (n2[0] == -1) + n2[0] = 1; + if (n2[1] == -1) + n2[1] = 1; + if (n2[2] == -1) + n2[2] = 1; + + // Get the difference between the two normals + normal[0] = fabs(n1[0] - n2[0]); + normal[1] = fabs(n1[1] - n2[1]); + normal[2] = fabs(n1[2] - n2[2]); + + // Check if the difference is within the leeway + if (normal[0] > leeway || normal[1] > leeway || normal[2] > leeway) + return false; + + return true; +} + + + +// Compares two normals to see if they're roughly equal (within leeway percent, 0.03 is 3%) +qboolean UTIL_CompareNormal(vec3_t n1, vec3_t n2, float leeway) +{ + vec3_t norm, norm_1, norm_2; + VectorCopy(n1, norm_1); + VectorCopy(n2, norm_2); + + norm[0] = fabs(norm_1[0] - norm_2[0]); + norm[1] = fabs(norm_1[1] - norm_2[1]); + norm[2] = fabs(norm_1[2] - norm_2[2]); + + if (norm[0] > leeway || norm[1] > leeway || norm[2] > leeway) + return false; // Normal does not match + else + return true; // Normal within range +} + +// Compares two vect_3 verts, checking if they're on the same x or y or z plane, allowing for a leeway, returning the result as true/false. The function definition: qboolean UTIL_CompareVert(vec3_t v1, vec3_t v2, vec3_t normal, float leeway) +qboolean UTIL_CompareVertPlanes(vec3_t v1, vec3_t v2, vec3_t normal, float leeway) +{ + vec3_t pNormal; + VectorSubtract(v2, v1, pNormal); + VectorNormalize(pNormal); + + if (fabs(DotProduct(pNormal, normal)) > leeway) + return false; + else + return true; +} + +qboolean UTIL_CompareVertPlanesSlope(vec3_t verts, int num_verts, vec3_t v2, vec3_t normal, float leeway) +{ + vec3_t pNormal = { 0 }; + for (int i = 0; i < num_verts; i++) + { + VectorSubtract(v2, verts+i, pNormal); + VectorNormalize(pNormal); + if (fabs(DotProduct(pNormal, normal)) <= leeway) + return true; + } + return false; +} + +// Checks if a point is inside/touching a face, this is done by splitting up a face into triangles and checking if the point is inside any of the triangles +//static qboolean UTIL_PointInsideFace(vec3_t pos, vec3_t* verts, int total_verts, vec3_t normal, int drawflags) +static qboolean UTIL_PointInsideFace(vec3_t pos, mface_t* surf, int face, int contents) +{ + int e = 0, v = 0; // Edge and vert index + msurfedge_t* surfedge; // Surface edge + int total_verts = 0; + vec3_t verts[MAX_FACE_VERTS]; // All verts + for (surfedge = surf->firstsurfedge; e < surf->numsurfedges; surfedge++) + { + VectorCopy(surfedge->edge->v[0]->point, verts[v]); + v++; + VectorCopy(surfedge->edge->v[1]->point, verts[v]); + v++; + total_verts += 2; + e++; + } + vec3_t normal; + VectorCopy(surf->plane->normal, normal); + int drawflags = surf->drawflags; + + if (total_verts <= 2) // If there are less than 3 verts, then it's not a face + return false; + + //Com_Printf("%s f:%d normal[%f %f %f]\n", __func__, face, normal[0], normal[1], normal[2]); + + const float adjust_dist = 0.01; + vec3_t center = { 0 }; // Center point of a triangle + vec3_t pt0 = { 0 }; // Point 1 of a triangle + vec3_t pt1 = { 0 }; // Point 2 of a triangle + vec3_t pt2 = { 0 }; // Point 3 of a triangle + + + + // Get the starting point of all the triangles + if (VectorCompare(verts[0], verts[total_verts - 1]) || VectorCompare(verts[0], verts[total_verts - 2])) + { + VectorCopy(verts[0], pt0); // Starting point - Vertex 1 + } + else + { + VectorCopy(verts[1], pt0); // Starting point - Vertex 2 + } + + + // Testing starting vector direction & angle + qboolean follows_starting_vec = true; // If the point follows the starting vector direction + vec3_t dir = { 0 }; // Dir vector + vec3_t angle = { 0 }; // Angle vector + // Get the direction of the edge to test against + vec3_t dir_forward = { 0 }; + vec3_t dir_backward = { 0 }; + // Get vector angle of dir_forward + vec3_t angle_forward = { 0 }; + vec3_t angle_backward = { 0 }; + // Init the forward and backward angles + VectorSubtract(verts[1], verts[0], dir_forward); // Forward vector + VectorSubtract(verts[0], verts[1], dir_backward); // Backward vector + vectoangles2(dir_forward, angle_forward); // Forward angle + vectoangles2(dir_backward, angle_backward); // Backward angle + + + // Get the centoid of a face triangle + qboolean inside = false; + for (int vert_count = 0; vert_count < total_verts; vert_count += 2) + { + //if (vert_count + 2 == total_verts) // ignore the last edge + // continue; + + //if (normal[2] == 1 || (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= MAX_STEEPNESS)) + // ; + //else + // continue; // ignore walls and steep slopes + + // An edge is comprised of two verts, it needs a third vert to make a triangle. + // pt0 (the third vert) will be using either the very first vertex verts[0] or verts[1] + // In this example verts[1] becomes the third vert for [pt2] and [pt3], + // together they form a triangle as per the command gl_showtris "1" + // + // + // verts[3] --> [pt3] -------------- [pt0] <-- verts[0] + // |\ | + // | \ | + // | \ | + // | \ | + // | \ | + // | \ | + // verts[2] --> [pt2] -------------- [pt1] <-- verts[1] <=== pick this for pt0 + // + /* + if (vert_count == 2) + { + // Point 1 == 3 + if (verts[0][0] == verts[vert_count][0] && verts[0][1] == verts[vert_count][1] && verts[0][2] == verts[vert_count][2]) + VectorCopy(verts[1], pt0); + + // Point 1 == 4 + else if (verts[0][0] == verts[vert_count + 1][0] && verts[0][1] == verts[vert_count + 1][1] && verts[0][2] == verts[vert_count + 1][2]) + VectorCopy(verts[1], pt0); + + // Point 2 == 3 + else if (verts[1][0] == verts[vert_count][0] && verts[1][1] == verts[vert_count][1] && verts[1][2] == verts[vert_count][2]) + VectorCopy(verts[0], pt0); + + // Point 2 == 4 + else + VectorCopy(verts[0], pt0); + } + */ + /* + // Ignore if three verts form a line instead of a triangle + // Floors, Slopes + if (normal[2] == 1 || (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= MAX_STEEPNESS)) + { + // [pt0] ------ [pt2] ------ [pt3] ----- [] [line != triangle] + // [pt1] ------ [pt2] ------ [pt3] ----- [] [line != triangle] + // + // If three verts on the X axis are the same, ignore because they form a line and not a valid triangle + if (pt0[0] == verts[vert_count][0] && pt0[0] == verts[vert_count + 1][0]) + continue; + + // If three verts on the Y axis are the same, ignore because they form a line and not a valid triangle + if (pt0[1] == verts[vert_count][1] && pt0[1] == verts[vert_count + 1][1]) + continue; + } + else // Walls + { + // If three verts on the Z axis are the same + if (pt0[2] == verts[vert_count][2] && pt0[2] == verts[vert_count + 1][2]) + continue; + } + */ + + // If the next edge after the starting edge is heading in the same direction, skip it + // We check this by comparing the angle of the next edge to the angle of the starting edge + // This is done until the next edge is heading in a different direction, whereby we make follows_starting_vec false + // Get the direction of the current edge + VectorSubtract(verts[vert_count + 1], verts[vert_count], dir); // Current edge vector + vectoangles2(dir, angle); // Current edge angle + // Check if the angle is the same as angle_forward or angle_backward + if (follows_starting_vec && (VectorCompare(angle, angle_forward) || VectorCompare(angle, angle_backward))) + { + // The current edge is in the same direction as the previous edge, skip it + continue; + } + else + { + follows_starting_vec = false; + } + + + // Copy the next two verts of the current edge + VectorCopy(verts[vert_count], pt1); + VectorCopy(verts[vert_count + 1], pt2); + + // Calculate the centroid of a triangle ((A+B+C)/3) + float x, y, z; + x = (pt0[0] + pt1[0] + pt2[0]) / 3; + y = (pt0[1] + pt1[1] + pt2[1]) / 3; + z = (pt0[2] + pt1[2] + pt2[2]) / 3; + + // Copy the centoid + center[0] = x; + center[1] = y; + center[2] = z; + + if (0) // Look for surface normals that point down, we wish to ignore these triangle faces + { + vec3_t normal_end = { 0 }; + vec3_t center_vec = { 0 }; + const float offset = 32; + MoveAwayFromNormal(drawflags, normal, center_vec, offset); + VectorAdd(center, center_vec, normal_end); // Move away from surface + if (normal_end[2] < center[2]) // Normal points down + return false; // Ignore surface + } + + if (0) // DEBUG: draw triangles + { + vec3_t points[6]; + + VectorCopy(pt0, points[0]); + VectorCopy(pt1, points[1]); + + VectorCopy(pt1, points[2]); + VectorCopy(pt2, points[3]); + + VectorCopy(pt2, points[4]); + VectorCopy(pt0, points[5]); + + static const uint32_t color_white[6] = { U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN }; + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_VertexPointer(3, 0, &points[0][0]); + glLineWidth(1.0); + GL_ColorBytePointer(4, 0, (GLubyte*)color_white); + if (gl_showedges && gl_showedges->integer < 0) // -1 + GL_DepthRange(0, 1); // Set the far clipping plane to 0 (edges are now obscured) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (edges can now be seen behind walls) + qglDrawArrays(GL_LINES, 0, 6); // GL_LINES is the type of primitive to render, 0 is the starting index, 2 is the number of indices to render + GL_DepthRange(0, 1); // Set the depth buffer back to normal (edges are now obscured) + glLineWidth(1.0); + } + + if (0) // DEBUG: draws triangle centoids + { + vec3_t points[2]; + vec3_t center_vec = { 0 }; + + const float offset = 32; // Player model width + MoveAwayFromNormal(drawflags, normal, center_vec, offset); + + VectorCopy(center, points[0]); + VectorAdd(center, center_vec, points[1]); // Move away from surface + + static const uint32_t color_white[2] = { U32_RED, U32_RED }; + GL_DrawLine(points[0], 2, color_white, 1, false); + } + + vec3_t pt; + vec3_t v1; v1[0] = pt0[0]; v1[1] = pt0[1]; v1[2] = pt0[2]; + vec3_t v2; v2[0] = pt1[0]; v2[1] = pt1[1]; v2[2] = pt1[2]; + vec3_t v3; v3[0] = pt2[0]; v3[1] = pt2[1]; v3[2] = pt2[2]; + + + + + + + + + + + + + + // Center point + // Check to ensure the centoid (x,y) coordinates are truly inside of the triangle + // vec3_t pt; pt[0] = center[0]; pt[1] = center[1]; pt[2] = center[2]; + //if (PointInTriangle(pt, v1, v2, v3)) // Is the point inside the triangle? + //{ + // is_point_inside = true; + //} + + // Check if origin is a ladder + qboolean is_ladder = false; + if (1) + { + const float min_max = 0.01; // Min/max value for the mins/maxs of the trace + vec3_t mins = { -min_max,-min_max,-min_max }; + vec3_t maxs = { min_max,min_max,min_max }; + vec3_t start = { 0 }; + vec3_t pos_vec = { 0 }; + const float offset = 32; + MoveAwayFromNormal(drawflags, normal, pos_vec, offset); + VectorAdd(pos, pos_vec, start); // Move away from surface + trace_t tr = SV_Trace(start, mins, maxs, pos, NULL, MASK_PLAYERSOLID); // From origin outside to inside + if (tr.contents & CONTENTS_LADDER) + is_ladder = true; + } + if (contents & CONTENTS_LADDER) + is_ladder = true; + + + + + + //is_ladder = true; // Debug + + + + + + + // Target point + pt[0] = pos[0]; pt[1] = pos[1]; pt[2] = pos[2]; + if (is_ladder || PointInTriangle(pt, v1, v2, v3)) // Is the point inside the triangle? + { + float distance = 1; // within 2 units from the surface + vec3_t org = { 0,0,0 }; + vec3_t vec = { 0,0,0 }; + VectorCopy(pos, org); // We only want the height + + // Floors + if (normal[2] == 1 && normal[0] == 0 && normal[1] == 0) + { + org[0] = 0; // Remove X component + org[1] = 0; // Remove Y component + center[0] = 0; // Remove X component + center[1] = 0; // Remove Y component + + VectorSubtract(center, org, vec); + if (VectorLength(vec) <= distance) + return true; + //inside = true; + } + // Slopes + //else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= MAX_STEEPNESS) + else + { + // Look for surface normals that point down, we wish to ignore these faces + //vec3_t normal_end = { 0 }; + //vec3_t center_vec = { 0 }; + //const float offset = 32; + //MoveAwayFromNormal(drawflags, normal, center_vec, offset); + //VectorAdd(center, center_vec, normal_end); // Move away from surface + //if (normal_end[2] < center[2]) // Normal points down + // return false; // Ignore surface + + // Flatten the center triangle by averaging the height between all 3 points, and save the difference to a float + //float avg_height = (pt0[2] + pt1[2] + pt2[2]) / 3; + //float diff = avg_height - center[2]; + + /* + // Get the min height of the triangle + float min_height = pt0[2]; + if (pt1[2] < min_height) + min_height = pt1[2]; + if (pt2[2] < min_height) + min_height = pt2[2]; + + // Get the max height of the triangle + float max_height = pt0[2]; + if (pt1[2] > max_height) + max_height = pt1[2]; + if (pt2[2] > max_height) + max_height = pt2[2]; + */ + + /* + // Flatten the center by averaging the height between all vertex points, and save the difference + float avg_height = 0; + float diff = 0; + for (int v = 0; v < total_verts; v++) + { + avg_height += verts[v][2]; + } + avg_height /= total_verts; + diff = avg_height - center[2]; + */ + + // Min and max vertex height + float min_x = 999999; + float max_x = -999999; + float min_y = 999999; + float max_y = -999999; + float min_z = 999999; + float max_z = -999999; + for (int v = 0; v < total_verts; v++) + { + if (verts[v][0] < min_x) + min_x = verts[v][0]; + if (verts[v][0] > max_x) + max_x = verts[v][0]; + if (verts[v][1] < min_y) + min_y = verts[v][1]; + if (verts[v][1] > max_y) + max_y = verts[v][1]; + if (verts[v][2] < min_z) + min_z = verts[v][2]; + if (verts[v][2] > max_z) + max_z = verts[v][2]; + } + + // Add leeway to the min/max + if (0) + { + const float leeway = 0.3125; + if (min_x > 0) min_x -= leeway; + else min_x += -(leeway); + max_x += leeway; + + if (min_y > 0) min_y -= leeway; + else min_y += -(leeway); + max_y += leeway; + + if (min_z > 0) min_z -= leeway; + else min_z += -(leeway); + max_z += leeway; + } + + + + + // height diff between center and origin in absolute + //float abs_diff = fabs(org[2] - center[2]); + //float abs_diff = fabs(org[2] - avg_height); + +/* +face:59 org[256.031250 -79.877510 -718.222961] -x[256.000000] +x[256.000000] -y[-112.000000] +y[-33.000000] -z[-800.000000] +z[-608.000000] +face:59 org[256.031250 -79.877510 -718.222961] -x[256.000000] +x[256.000000] -y[-112.000000] +y[-33.000000] -z[-800.000000] +z[-608.000000] +face:59 org[256.031250 -79.877510 -718.222961] -x[256.000000] +x[256.000000] -y[-112.000000] +y[-33.000000] -z[-800.000000] +z[-608.000000] + +face:1 org[-285.238037 -0.031258 -706.308044] -x[-287.899994] +x[-128.100006] -y[0.100000] +y[-0.100000] -z[-800.000000] +z[-704.000000] +*/ + + //if (org[2] >= min_z && org[2] <= max_z) + if (org[0] >= min_x && org[0] <= max_x && org[1] >= min_y && org[1] <= max_y && org[2] >= min_z && org[2] <= max_z) + { + //Com_Printf("--- %s face:%d org[%f %f %f] -x[%f] +x[%f] -y[%f] +y[%f] -z[%f] +z[%f]\n", __func__, face, org[0], org[1], org[2], min_x, max_x, min_y, max_y, min_z, max_z); + //Com_Printf("--- %s diff:%f avg_height:%f abs_diff:%f center[%f] org[%f] mm[%f %f]\n", __func__, diff, avg_height, abs_diff, center[2], org[2], min_height, max_height); + return true; + } + + /* + if (face == 1) + { + Com_Printf("%s face:%d org[%f %f %f] -x[%f] +x[%f] -y[%f] +y[%f] -z[%f] +z[%f]\n", __func__, face, org[0], org[1], org[2], min_x, max_x, min_y, max_y, min_z, max_z); + return true; + } + else + return false; + */ + + //Com_Printf("%s face:%d org[%f %f %f] -x[%f] +x[%f] -y[%f] +y[%f] -z[%f] +z[%f]\n", __func__, face, org[0], org[1], org[2], min_x, max_x, min_y, max_y, min_z, max_z); + + //if (abs_diff < 32) + // Com_Printf("%s diff:%f avg_height:%f abs_diff:%f center[%f] org[%f] mm[%f %f]\n", __func__, diff, avg_height, abs_diff, center[2], org[2], min_z, max_z); + + //center[2] = avg_height; // Flatten the center + + // Add the difference to the origin + //org[2] += diff; + } + + /* + if (0) // DEBUG: draws triangle centoids + { + vec3_t points[2]; + vec3_t center_vec = { 0 }; + + const float offset = 32; // Player model width + MoveAwayFromNormal(drawflags, normal, center_vec, offset); + + VectorCopy(center, points[0]); + VectorAdd(center, center_vec, points[1]); // Move away from surface + + static const uint32_t color_white[2] = { U32_RED, U32_RED }; + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + GL_VertexPointer(3, 0, &points[0][0]); + glLineWidth(2.5); + GL_ColorBytePointer(4, 0, (GLubyte*)color_white); + if (gl_showedges && gl_showedges->integer < 0) // -1 + GL_DepthRange(0, 1); // Set the far clipping plane to 0 (edges are now obscured) + else + GL_DepthRange(0, 0); // Set the far clipping plane to 0 (edges can now be seen behind walls) + qglDrawArrays(GL_LINES, 0, 2); // GL_LINES is the type of primitive to render, 0 is the starting index, 2 is the number of indices to render + GL_DepthRange(0, 1); // Set the depth buffer back to normal (edges are now obscured) + glLineWidth(1.0); + } + + org[0] = 0; // Remove X component + org[1] = 0; // Remove Y component + center[0] = 0; // Remove X component + center[1] = 0; // Remove Y component + + VectorSubtract(center, org, vec); + if (VectorLength(vec) <= distance) + inside = true; + */ + + + /* + // Ensure the PointInTriangle is roughly within the same z height as origin + vec3_t org = { 0,0,0 }; + vec3_t vec = { 0,0,0 }; + VectorCopy(pos, org); // We only want the height + org[0] = 0; // Remove X component + org[1] = 0; // Remove Y component + center[0] = 0; // Remove X component + center[1] = 0; // Remove Y component + VectorSubtract(center, org, vec); + float distance = VectorLength(vec); + if (distance < 2) // Height + inside = true; + */ + } + + + + + // Check if floor + if (normal[0] == 0 && normal[1] == 0 && normal[2] == 1) + { + //Com_Printf("--- %s\n", __func__); + } + // Slopes + else if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= MAX_STEEPNESS) + { + //Com_Printf("--- %s\n", __func__); + } + else // Wall + { + // Min and max vertex height + float min_x = 999999; + float max_x = -999999; + float min_y = 999999; + float max_y = -999999; + float min_z = 999999; + float max_z = -999999; + for (int v = 0; v < total_verts; v++) + { + if (verts[v][0] < min_x) + min_x = verts[v][0]; + if (verts[v][0] > max_x) + max_x = verts[v][0]; + if (verts[v][1] < min_y) + min_y = verts[v][1]; + if (verts[v][1] > max_y) + max_y = verts[v][1]; + if (verts[v][2] < min_z) + min_z = verts[v][2]; + if (verts[v][2] > max_z) + max_z = verts[v][2]; + } + + // Add leeway to the min/max + if (1) + { + //const float leeway = 0.3125; + const float leeway = 0.03125; + if (min_x > 0) min_x -= leeway; + else min_x += -(leeway); + max_x += leeway; + + if (min_y > 0) min_y -= leeway; + else min_y += -(leeway); + max_y += leeway; + + if (min_z > 0) min_z -= leeway; + else min_z += -(leeway); + max_z += leeway; + } + + if (pos[0] >= min_x && pos[0] <= max_x && pos[1] >= min_y && pos[1] <= max_y && pos[2] >= min_z && pos[2] <= max_z) + { + //Com_Printf("--- %s face:%d org[%f %f %f] -x[%f] +x[%f] -y[%f] +y[%f] -z[%f] +z[%f]\n", __func__, face, pos[0], pos[1], pos[2], min_x, max_x, min_y, max_y, min_z, max_z); + //Com_Printf("--- %s diff:%f avg_height:%f abs_diff:%f center[%f] org[%f] mm[%f %f]\n", __func__, diff, avg_height, abs_diff, center[2], pos[2], min_height, max_height); + return true; + } + } + + + } + + return inside; +} + + + + + +// Functionally this works similar to the #define VectorCompare(), with the exception of an epsilon +// Compare two vectors and return true if they are the same within a certain epsilon +qboolean UTIL_VectorCompare(vec3_t v1, vec3_t v2, const float epsilon) +{ + for (int i = 0; i < 3; i++) + { + if (fabs(v1[i] - v2[i]) > epsilon) + { + return false; + } + } + return true; +} + +// Checks the direction of v1 and v2 against v3 and v4 +// Also add a qboolean to allow checking for the opposite direction +// Include an epsilon to allow for floating point errors +qboolean UTIL_VectorDirectionCompare(vec3_t v1, vec3_t v2, vec3_t v3, vec3_t v4, qboolean opposite, const float epsilon) +{ + vec3_t dir_forward[2], dir_backward[2]; // Forward and backward vectors + vec3_t angle_forward[2], angle_backward[2]; // Forward and backward angles + + VectorSubtract(v2, v1, dir_forward[0]); // Forward vector + VectorSubtract(v4, v3, dir_forward[1]); // Forward vector + + vectoangles2(dir_forward[0], angle_forward[0]); // Forward angle + vectoangles2(dir_forward[1], angle_forward[1]); // Forward angle + + if (opposite) + { + VectorSubtract(v1, v2, dir_backward[0]); // Backward vector + VectorSubtract(v3, v4, dir_backward[1]); // Backward vector + + vectoangles2(dir_backward[0], angle_backward[0]); // Backward angle + vectoangles2(dir_backward[1], angle_backward[1]); // Backward angle + + if (UTIL_VectorCompare(angle_forward[0], angle_forward[1], epsilon) || UTIL_VectorCompare(angle_backward[0], angle_backward[1], epsilon) + || UTIL_VectorCompare(angle_forward[0], angle_backward[1], epsilon) || UTIL_VectorCompare(angle_backward[0], angle_forward[1], epsilon)) + { + return true; // Return true if either forward or backward angles are the same + } + } + else + { + // Vector compare the forward angles + if (UTIL_VectorCompare(angle_forward[0], angle_forward[1], epsilon)) + { + return true; // Return true if forward angles are the same + } + } + + return false; // Default to false +} + +// Gets the raw vertex data from a surface and returns the result to face_data +void InitVertexData(mface_t* surf, surface_data_t* face_data) +{ + int e = 0, v = 0; // Edge and vert index + msurfedge_t* surfedge; // Surface edge + face_data->num_verts = 0; // Init num verts + + for (surfedge = surf->firstsurfedge; e < surf->numsurfedges; surfedge++) + { + VectorCopy(surfedge->edge->v[0]->point, face_data->verts[v]); + v++; + VectorCopy(surfedge->edge->v[1]->point, face_data->verts[v]); + v++; + face_data->num_verts += 2; + e++; + } +} + +// Repair any malformed vertex data +void FixMalformedVertexData(surface_data_t* face_data) +{ + /* + * MAP: Deepcanyon + GL_DrawEdges face:988 verts:10 norm[1.000000 0.000000 0.000000] tr.norm[1.000000 0.000000 0.000000] vol:7202.355957 + -- + GL_DrawEdges face:988 edge:0 [16.000000 1641.672607 912.000000] -- [16.000000 1641.170410 911.749451] + GL_DrawEdges face:988 edge:1 [16.000000 1641.170410 911.749451] -- [16.000000 1472.000000 827.353455] + GL_DrawEdges face:988 edge:2 [16.000000 1472.000000 912.000000] -- [16.000000 1472.000000 827.353455] + GL_DrawEdges face:988 edge:3 [16.000000 1641.170410 911.749451] -- [16.000000 1472.000000 912.000000] + GL_DrawEdges face:988 edge:4 [16.000000 1641.672607 912.000000] -- [16.000000 1641.170410 911.749451] + */ + // Fix a bug in the vertex data where the first and second vertex are the same as the last and second last vertex + if ((UTIL_VectorCompare(face_data->verts[0], face_data->verts[face_data->num_verts - 2], 0.01) || UTIL_VectorCompare(face_data->verts[0], face_data->verts[face_data->num_verts - 1], 0.01)) + && (UTIL_VectorCompare(face_data->verts[1], face_data->verts[face_data->num_verts - 1], 0.01) || UTIL_VectorCompare(face_data->verts[1], face_data->verts[0], 0.01))) + { + // Move all the verts down by two + for (int i = 0; i < face_data->num_verts; i++) + { + VectorCopy(face_data->verts[i + 2], face_data->verts[i]); + } + + // Remove the first two and last two verts + face_data->num_verts -= 4; + + Com_Printf("%s BUGFIX: removed first and last two verts of face: %d\n", __func__, face_data->facenum); + } + + // Look for an uneven amount of verts + if (face_data->num_verts % 2 != 0) + { + // Remove the last vert + face_data->num_verts -= 1; + + Com_Printf("%s BUGFIX: removed last vert of face: %d\n", __func__, face_data->facenum); + } +} + +// Using the raw vertex data output the result of aligning parallel edges so they form a straight line +// 1) Nearest edges that are parallel are aligned into a single edge, +// 2) Remove the inner vertex points leaving only the outer vertex points +// 3) The result is a straight line with a start and end point +void AlignVertexPoints(surface_data_t* face_data) +{ + vec3_t v1 = { 0 }; // Current vert 1 (current edge) + vec3_t v2 = { 0 }; // Current vert 2 (current edge) + vec3_t p1 = { 0 }; // Previous vert 1 (prev edge) + vec3_t p2 = { 0 }; // Previous vert 2 (prev edge) + vec3_t dir = { 0 }; // Dir vector + vec3_t angle = { 0 }; // Angle vector + + // Get the direction of the edge to test against + vec3_t dir_forward = { 0 }; + vec3_t dir_backward = { 0 }; + + // Get vector angle of dir_forward + vec3_t angle_forward = { 0 }; + vec3_t angle_backward = { 0 }; + + if (face_data->num_verts >= 6) // Min 3 edges (triangle), 6 verts + { + // Get the first vert + if (VectorCompare(face_data->verts[0], face_data->verts[face_data->num_verts - 2]) || VectorCompare(face_data->verts[0], face_data->verts[face_data->num_verts - 1])) + { + VectorCopy(face_data->verts[0], face_data->first_vert); + } + else if (VectorCompare(face_data->verts[1], face_data->verts[face_data->num_verts - 2]) || VectorCompare(face_data->verts[1], face_data->verts[face_data->num_verts - 1])) + { + VectorCopy(face_data->verts[1], face_data->first_vert); + } + + // Save the first vert + VectorCopy(face_data->first_vert, face_data->aligned_verts[0]); + face_data->num_aligned_verts = 1; // Init the first aligned vert + + // Init the forward and backward angles + VectorCopy(face_data->verts[0], v1); // Current vert v1 + VectorCopy(face_data->verts[1], v2); // Current vert v2 + VectorSubtract(v2, v1, dir_forward); // Forward vector + VectorSubtract(v1, v2, dir_backward); // Backward vector + vectoangles2(dir_forward, angle_forward); // Forward angle + vectoangles2(dir_backward, angle_backward); // Backward angle + + // Align the raw verts + for (int v = 2; v < face_data->num_verts; v += 2) // Edges + { + // Copy the current verts + VectorCopy(face_data->verts[v + 0], v1); // Current vert v1 + VectorCopy(face_data->verts[v + 1], v2); // Current vert v2 + + // Copy the previous verts + VectorCopy(face_data->verts[v - 2], p1); // Previous vert p1 + VectorCopy(face_data->verts[v - 1], p2); // Previous vert p2 + + // Get the direction of the current verts + VectorSubtract(v2, v1, dir); // Current edge vector + vectoangles2(dir, angle); // Current edge angle + + // Check if the angle is the same as angle_forward or angle_backward + //if (UTIL_VectorCompare(angle, angle_forward, angle_tolerance) || UTIL_VectorCompare(angle, angle_backward, angle_tolerance)) + if (VectorCompare(angle, angle_forward) || VectorCompare(angle, angle_backward)) + { + if (v == face_data->num_verts - 2) // Last edge + { + VectorCopy(face_data->first_vert, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + break; // Finished + } + // The current edge is in the same direction as the previous edge, skip it + continue; + } + else + { + if (VectorCompare(v1, p1) || VectorCompare(v1, p2)) + { + VectorCopy(v1, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + VectorCopy(v1, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + } + else if (VectorCompare(v2, p1) || VectorCompare(v2, p2)) + { + VectorCopy(v2, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + VectorCopy(v2, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + } + + if (v == face_data->num_verts - 2) + { + VectorCopy(face_data->first_vert, face_data->aligned_verts[face_data->num_aligned_verts]); + face_data->num_aligned_verts++; + break; // Finished + } + + // Set the new forward and backward angles + VectorCopy(face_data->verts[v + 0], v1); // Current vert v1 + VectorCopy(face_data->verts[v + 1], v2); // Current vert v2 + VectorSubtract(v2, v1, dir_forward); // Forward vector + VectorSubtract(v1, v2, dir_backward); // Backward vector + vectoangles2(dir_forward, angle_forward); // Forward angle + vectoangles2(dir_backward, angle_backward); // Backward angle + } + } + } +} + +// Checks if face is liquid: water/lava/slime +qboolean CheckFaceLiquid(surface_data_t* face_data) +{ + const float min_max = 0.1; // Min/max value for the mins/maxs of the trace + vec3_t mins = { -min_max,-min_max,-min_max }; + vec3_t maxs = { min_max,min_max,min_max }; + trace_t tr = SV_Trace(face_data->center_poly, mins, maxs, face_data->center_poly, NULL, MASK_WATER); + if (tr.startsolid == false) + return false; + else + return true; +} + +// Checks if face is solid +qboolean CheckFaceSolid(surface_data_t* face_data) +{ + const float min_max = 0.1; // Min/max value for the mins/maxs of the trace + vec3_t mins = { -min_max,-min_max,-min_max }; + vec3_t maxs = { min_max,min_max,min_max }; + trace_t tr = SV_Trace(face_data->center_poly, mins, maxs, face_data->center_poly, NULL, MASK_PLAYERSOLID); + if (tr.startsolid == false) + return false; + else + return true; +} + +// Checks if player fits above face +qboolean CheckSkyAboveFace(surface_data_t* face_data) +{ + vec3_t start = { face_data->center_poly[0], face_data->center_poly[1], face_data->center_poly[2] + 0.1 }; + vec3_t end = { face_data->center_poly[0], face_data->center_poly[1], face_data->center_poly[2] + 56 }; + trace_t tr = SV_Trace(start, NULL, NULL, end, NULL, MASK_PLAYERSOLID); + //if (tr.startsolid || tr.fraction < 1) + { + + //vec3_t start_higher = { face_data->center_poly[0], face_data->center_poly[1], face_data->center_poly[2] + 16 }; + //tr = SV_Trace(start_higher, NULL, NULL, end, NULL, MASK_PLAYERSOLID); + //if (tr.startsolid) + // Com_Printf("%s eeeep!!!\n", __func__); + } + if (tr.surface->flags & SURF_SKY) + return false; + else + return true; +} + +void GetFaceContents(surface_data_t* face_data) +{ + face_data->contents = 0; // Surface contents + trace_t tr_center; // Trace to the center of the polygon + const float min_max = 0.01; // Min/max value for the mins/maxs of the trace + vec3_t mins = { -min_max,-min_max,-min_max }; + vec3_t maxs = { min_max,min_max,min_max }; + qboolean foundLadder = true; // Normalize ladder to be true, until found otherwise + + // Check if our starting point is not inside a solid + tr_center = SV_Trace(face_data->center_poly_32_units, mins, maxs, face_data->center_poly_32_units, NULL, MASK_PLAYERSOLID); // From polygon centoid outside + if (tr_center.startsolid == false) + { + // Test from starting point to center of polygon + tr_center = SV_Trace(face_data->center_poly_32_units, mins, maxs, face_data->center_poly, NULL, MASK_PLAYERSOLID); // From polygon centoid outside to inside MASK_UNLIMITED + + face_data->contents = tr_center.contents; + + // Get distances - If the difference is greater than MAX_STEEPNESS, then the trace is not accurate enough + float target_dist = VectorDistance(face_data->center_poly_32_units, face_data->center_poly); // Distance between starting point and center of polygon + float trace_dist = VectorDistance(face_data->center_poly_32_units, tr_center.endpos); // Distance between starting point and end of a trace + float diff_dist = fabs(target_dist - trace_dist); // Get the difference between the two distances in absolute value + + // Test surface is ladder and (the difference is less than MAX_STEEPNESS OR if the trace hit a playerclip) + // NOTE: Some ladders have a playerclip around them, so the trace will hit the playerclip and not the intended ladder surface, + // but because the playerclip is CONTENTS_LADDER, this will be our ladder surface + if ((tr_center.contents & CONTENTS_LADDER) && (diff_dist <= MAX_STEEPNESS || (tr_center.contents & CONTENTS_PLAYERCLIP))) + { + } + else + foundLadder = false; // Not a ladder + + // Look for liquids: water, lava, slime + tr_center = SV_Trace(face_data->center_poly_32_units, mins, maxs, face_data->center_poly, NULL, MASK_WATER); + if (tr_center.contents & CONTENTS_WATER) + face_data->contents |= CONTENTS_WATER; + else if (tr_center.contents & CONTENTS_LAVA) + face_data->contents |= CONTENTS_LAVA; + else if (tr_center.contents & CONTENTS_SLIME) + face_data->contents |= CONTENTS_SLIME; + + + // Look for trigger edicts (trigger_hurt) + //tr_center = SV_Trace(face_data->center_poly_32_units, mins, maxs, face_data->center_poly, NULL, MASK_ALL); + //if (tr_center.ent && tr_center.ent->s.number > 0) + // Com_Printf("%s: ENT num[%d]\n", __func__, tr_center.ent->s.number); + //if (tr_center.ent->solid == SOLID_TRIGGER) + // Com_Printf("%s: ENT solid[%d] num[%d]\n", __func__, tr_center.ent->solid, tr_center.ent->s.number); + } + + // Remove ladder contents from floors + if (face_data->normal[2] == 1 && (face_data->contents & CONTENTS_LADDER)) + { + //contents &= ~CONTENTS_LADDER; + } + + // Remove ladders from invalid faces (surfaces that does not have its contents flagged as a CONTENTS_LADDER) + if (foundLadder == false) + face_data->contents &= ~CONTENTS_LADDER; +} + +void InverseNormals(surface_data_t* face_data) +{ + VectorCopy(face_data->surf->plane->normal, face_data->normal); + if (face_data->drawflags & DSURF_PLANEBACK) + { + VectorInverse(face_data->normal); + } +} + +facetype_t SetSurfaceType(surface_data_t *face_data) +{ + unsigned int surface_type = 0; + + //#define MASK_SURF_IGNORE (SURF_LIGHT|SURF_SLICK|SURF_WARP|SURF_TRANS33|SURF_TRANS66|SURF_FLOWING|SURF_NODRAW) + //if (face_data->drawflags & (SURF_LIGHT | SURF_SLICK | SURF_WARP | SURF_TRANS33 | SURF_TRANS66 | SURF_FLOWING | SURF_NODRAW)) + if (face_data->drawflags & SURF_NODRAW) + { + //Com_Printf("%s: face[%d] drawflags:[0x%x]\n", __func__, face_data->facenum, face_data->drawflags); + surface_type |= FACETYPE_IGNORED; + } + + // Sky surfaces + if (face_data->drawflags & SURF_SKY) + { + surface_type |= FACETYPE_SKY; + } + // Roof surfaces + //if (face_data->surf->plane->normal[2] == 1) // Floor + //{ + //if (face_data->surf->drawflags & DSURF_PLANEBACK) // Roof + // surface_type |= FACETYPE_ROOF; + //} + // Floors - perfectly flat + if (face_data->normal[0] == 0 && face_data->normal[1] == 0 && face_data->normal[2] == 1) + { + if (face_data->surf->drawflags & DSURF_PLANEBACK) // Roof + surface_type |= FACETYPE_ROOF; + else // Floor + surface_type |= FACETYPE_WALK; + } + // Any climbable sloped surface + else if (face_data->normal[0] >= -MAX_STEEPNESS && face_data->normal[0] <= MAX_STEEPNESS && face_data->normal[1] >= -MAX_STEEPNESS && face_data->normal[1] <= MAX_STEEPNESS && face_data->normal[2] >= (MAX_STEEPNESS - 0.10)) // && face_data->normal[2] >= MAX_STEEPNESS) + surface_type |= FACETYPE_WALK; // Allow variable steep surfaces, except overly steep ones + // Anything too steep to climb (except ladders) becomes a wall -- Wall with normal[2] == 0 means the wall is perfectly upright + else + surface_type |= FACETYPE_WALL; // Ignore walls + // Angled slope with normal facing down + //if (face_data->normal[2] < 0) return true; + + if (face_data->contents & CONTENTS_LADDER) + surface_type |= FACETYPE_LADDER; + + if (face_data->contents & CONTENTS_WATER) + surface_type |= FACETYPE_WATER; + + if ( (face_data->contents & CONTENTS_LAVA) || (face_data->contents & CONTENTS_SLIME)) + surface_type |= FACETYPE_DAMAGE; + + if (IgnoreSmallSurfaces) + { + // Ignore small volumes + if (face_data->volume < 8) // 128 + surface_type |= FACETYPE_IGNORED; + + // Ignore small surfaces + if (face_data->max_length < 16) + surface_type |= FACETYPE_IGNORED; + if (face_data->min_length < 4) + surface_type |= FACETYPE_IGNORED; + } + else if (surface_type & FACETYPE_WALK) + { + // Flag small volumes + if (face_data->volume < 8) // 128 + surface_type |= FACETYPE_TINYWALK; + + // Ignore small surfaces + if (face_data->max_length < 16) + surface_type |= FACETYPE_TINYWALK; + if (face_data->min_length < 4) + surface_type |= FACETYPE_TINYWALK; + } + + return surface_type; +} + +// Gets the min / max length of the face +void UTIL_GetFaceLengthMinMax(surface_data_t* face_data) +{ + int i, j = 0; + float length = 0; + + // Init min/max + face_data->max_length = 0.0; + face_data->min_length = 9999.0; + + if (face_data->facenum == 593 || face_data->facenum == 594) + { + int a = 0; + } + + // Poly center -> edge + // For each aligned edge + for (i = 0; i < face_data->num_aligned_verts / 2; i++) + { + // Get the distance from poly center to edge + length = VectorDistance(face_data->center_poly, face_data->aligned_edge_center[i]); + + // If the length is greater than the current greatest length, set it as the greatest length + if (length > face_data->max_length) + face_data->max_length = length; + + // If the length is less than the current smallest length, set it as the smallest length + if (length < face_data->min_length) + face_data->min_length = length; + } + + // Edge -> edge + // For each aligned edge + for (i = 0; i < face_data->num_aligned_verts / 2; i++) + { + for (j = 0; j < face_data->num_aligned_verts / 2; j++) + { + if (i == j) continue; // Skip self + + // Get the distance from edge to edge + length = VectorDistance(face_data->aligned_edge_center[i], face_data->aligned_edge_center[j]); + + // If the length is greater than the current greatest length, set it as the greatest length + if (length > face_data->max_length) + face_data->max_length = length; + + // If the length is less than the current smallest length, set it as the smallest length + if (length < face_data->min_length) + face_data->min_length = length; + } + } +} + + +void UTIL_GetCenterOfVertEdges(surface_data_t* face_data) +{ + // For each vert edge + for (int i = 0; i < face_data->num_verts; i += 2) + { + // Get the center of the edge + VectorAdd(face_data->verts[i], face_data->verts[i + 1], face_data->edge_center[i / 2]); + VectorScale(face_data->edge_center[i / 2], 0.5, face_data->edge_center[i / 2]); + + // Center edge moved up by STEPSIZE + vec3_t center_vec = { 0 }; // Center point vector + MoveAwayFromNormal(face_data->drawflags, face_data->normal, center_vec, STEPSIZE); // Get the vector + VectorAdd(face_data->edge_center[i / 2], center_vec, face_data->edge_center_stepsize[i / 2]); // Move away from the surface normal by STEPSIZE units + } +} + +void UTIL_GetCenterOfAlignedEdges(surface_data_t* face_data) +{ + // For each aligned edge + for(int i = 0; i < face_data->num_aligned_verts; i += 2) + { + // Get the center of the edge + VectorAdd(face_data->aligned_verts[i], face_data->aligned_verts[i + 1], face_data->aligned_edge_center[i / 2]); + VectorScale(face_data->aligned_edge_center[i / 2], 0.5, face_data->aligned_edge_center[i / 2]); + + // Center edge moved up by STEPSIZE + vec3_t center_vec = { 0 }; // Center point vector + MoveAwayFromNormal(face_data->drawflags, face_data->normal, center_vec, STEPSIZE); // Get the vector + VectorAdd(face_data->aligned_edge_center[i / 2], center_vec, face_data->aligned_edge_center_stepsize[i / 2]); // Move away from the surface normal by STEPSIZE units + } +} + +// Similar to VectorCompare, except with a height tolerance +qboolean UTIL_VectorCompareHeight(vec3_t v1, vec3_t v2, float height_difference) +{ + if (v1[0] == v2[0] && v1[1] == v2[1] && v1[2] >= (v2[2] - height_difference) && v1[2] <= (v2[2] + height_difference)) + return true; + return false; +} +// Compare two edges to see if they are the same +qboolean UTIL_EdgeCompare(vec3_t edge1_v1, vec3_t edge1_v2, vec3_t edge2_v1, vec3_t edge2_v2) //, vec3_t center_edge) +{ + if ( VectorCompare(edge1_v1, edge2_v1) || VectorCompare(edge1_v1, edge2_v2) ) // Edge 1 (vertex 1) == Edge 2 (vertex 1 or 2) + { + if ( VectorCompare(edge1_v2, edge2_v1) || VectorCompare(edge1_v2, edge2_v2) ) // Edge 1 (vertex 2) == Edge 2 (vertex 1 or 2) + { + return true; + } + } + return false; +} +// Compares two edges to see if they share the same edge +// v1 & v2 is edge 1 +// v3 & v4 is edge 2 +qboolean UTIL_EdgeCompare2(vec3_t v1, vec3_t v2, vec3_t v3, vec3_t v4) +{ + if (VectorCompare(v1, v3) && VectorCompare(v2, v4)) return true; + if (VectorCompare(v1, v4) && VectorCompare(v2, v3)) return true; + return false; +} +//#define EdgeCompare(v1, v2, v3, v4) ( ( VectorCompare(v1, v3) && VectorCompare(v2, v4) ) || ( VectorCompare(v1, v4) && VectorCompare(v2, v3) ) ) +//VectorCompare(v1, v3) ((v1)[0]==(v3)[0]&&(v1)[1]==(v3)[1]&&(v1)[2]==(v3)[2]) +//VectorCompare(v2, v4) ((v2)[0]==(v4)[0]&&(v2)[1]==(v4)[1]&&(v2)[2]==(v4)[2]) +//VectorCompare(v1, v4) ((v1)[0]==(v4)[0]&&(v1)[1]==(v4)[1]&&(v1)[2]==(v4)[2]) +//VectorCompare(v2, v3) ((v2)[0]==(v3)[0]&&(v2)[1]==(v3)[1]&&(v2)[2]==(v3)[2]) +#define EdgeCompare(v1, v2, v3, v4) ( ( ((v1)[0]==(v3)[0]&&(v1)[1]==(v3)[1]&&(v1)[2]==(v3)[2]) && ((v2)[0]==(v4)[0]&&(v2)[1]==(v4)[1]&&(v2)[2]==(v4)[2]) ) || ( ((v1)[0]==(v4)[0]&&(v1)[1]==(v4)[1]&&(v1)[2]==(v4)[2]) && ((v2)[0]==(v3)[0]&&(v2)[1]==(v3)[1]&&(v2)[2]==(v3)[2]) ) ) + +// Runs a player sized box test from the position of the feet to the position of the head +qboolean UTIL_PlayerBoxTestCouched(vec3_t pos) +{ + // Height test lifted from STEPSIZE 18 to 28 to accommodate for box test edges and slopes + vec3_t start = { pos[0], pos[1], pos[2] + STEPSIZE }; + const float min_height = 10; + + vec3_t min = { -15.99, -15.99, 0 }; + vec3_t max = { 15.99, 15.99, min_height }; + + trace_t tr = SV_Trace(start, min, max, start, NULL, MASK_PLAYERSOLID); + if (tr.startsolid || tr.allsolid) + return false; + if (tr.fraction == 1.0) + return true; + else + return false; +} +qboolean UTIL_PlayerBoxTestStanding(vec3_t pos, vec3_t normal) +{ + // Adjust the start position to be away from the surface normal + vec3_t start = { pos[0], pos[1], pos[2] }; + vec3_t center_vec; + const float normal_height = 8; // Move distance + MoveAwayFromNormal(0, normal, center_vec, normal_height); // Get the vector + VectorAdd(start, center_vec, start); // Move away from the surface normal + + //const float min_height = 20; // crouched min/max (-24 + 4) = 28. (28 - normal_height = 20) + const float max_height = 48; // standing min/max (-24 + 32) = 56 (56 - normal_height = 48) + + vec3_t min = { -15.99, -15.99, 0 }; + vec3_t max = { 15.99, 15.99, max_height }; + + trace_t tr = SV_Trace(start, min, max, start, NULL, MASK_PLAYERSOLID); + if (tr.startsolid || tr.allsolid) + return false; + if (tr.fraction == 1.0) + return true; + else + return false; +} + +qboolean UTIL_LineOfSight(vec3_t start, vec3_t end, vec3_t normal) +{ + // Calculate distance + if (VectorDistance(start, end) > 256) // Ignore long distances + return false; + + // Caculate the height difference + //if (fabs(start[2] - end[2]) > 60) //max jump height //STEPSIZE + // return false; + + // Box test the start position + //if (normal[0] == 0 && normal[1] == 0 && normal[2] == 1) // Flat floors + // if (UTIL_PlayerBoxTestCouched(start) == false) // Test if the start position fits + // return false; + + // LOS + PLAYER WIDTH + CROUCH HEIGHT test start to end + vec3_t min = { -15.99, -15.99, 28 }; + //if (normal[0] == 0 && normal[1] == 0 && normal[2] == 1) // Flat floors + min[2] = 17.99; + vec3_t max = { 15.99, 15.99, 28 }; + trace_t tr = SV_Trace(start, min, max, end, NULL, MASK_PLAYERSOLID); + + if (tr.startsolid || tr.allsolid) + return false; + + if (tr.fraction == 1.0) + { + // Test if the end position fits + return UTIL_PlayerBoxTestCouched(tr.endpos); + //return true; + } + + return false; +} + +// Accepts two vectors and a percent, returns a point between the two vectors based on the percent +// Percent is from 0.0 to 1.0 +qboolean BOTLIB_UTIL_PositionBetweenTwoPoints(vec3_t v1, vec3_t v2, float percent, vec3_t out) +{ + if (percent < 0 || percent > 1) + return false; + + // Calculate the distance between the two vectors + float distance = VectorDistance(v1, v2); + + // Ensure we have a valid distance + if (distance > 32) + { + // Get the position between v1 and v2 based on the percent, putting the result in out + VectorAdd(v1, v2, out); + VectorScale(out, percent, out); + + return true; + } + + return false; +} + +// Rotate the center of an edge around a center point, then extend the center point out by a distance +// +// (end) <--- desired result +// | <--- distance +// | +// | +// |\ +// | \ <-- rotated edge (rotation here is 90 degrees left) +// | \ +// (v1) -----+----- (v2) ( 0 degrees --> ) +// (center) +// +// v1 and v2 are the start and end points of an edge +// float rotate the degrees of rotation +// float distance is the distance from the center point +// vec3_t end is the rotated end point +void BOTLIB_UTIL_ROTATE_CENTER(vec3_t v1, vec3_t v2, float rotate, float distance, vec3_t out, vec3_t normal) +{ + vec3_t center, angle, forward; // , right, offset; + + // print warning if distance is > 3000 + if (distance > 3000) + { + Com_Printf("%s WARNING: distance > 3000. Cannot rotate accurately.\n", __func__); + } + + // Find the middle between two points of an edge + LerpVector(v1, v2, 0.50, center); + + // Get direction vector + VectorSubtract(v2, v1, forward); + + // Normalize the direction vector + VectorNormalize(forward); + + // Get the angle of the direction vector + vectoangles2(forward, angle); + + // Rotation + angle[YAW] += rotate; // Rotation angle + + // Rotate the direction vector + AngleVectors(angle, forward, NULL, NULL); + + // Get the rotated end point and extend it out based on distance + VectorMA(center, distance, forward, out); + + // Calculate the new normal of the edge + if (normal != NULL) + { + CrossProduct(center, out, normal); + VectorNormalize(normal); + } +} +// v1 -> v2 is the edge (use to get direction +// point is what we want to move against the direction of the edge (90 degrees rotatation is perpendicular to the edge) +// rotation of the point, with the direction of the edge at 0 degrees +// distance to move away from the edge +// out is the result +void BOTLIB_UTIL_ROTATE_EDGE_POINT(vec3_t v1, vec3_t v2, vec3_t point, float rotate, float distance, vec3_t out) +{ + vec3_t angle, forward; // , right, offset; + + // print warning if distance is > 3000 + if (distance > 3000) + { + Com_Printf("%s WARNING: distance > 3000. Cannot rotate accurately.\n", __func__); + } + + // Get direction vector + VectorSubtract(v2, v1, forward); + + // Normalize the direction vector + VectorNormalize(forward); + + // Get the angle of the direction vector + vectoangles2(forward, angle); + + // Rotation + angle[YAW] += rotate; // Rotation angle + + // Rotate the direction vector + AngleVectors(angle, forward, NULL, NULL); + + // Get the rotated end point and extend it out based on distance + VectorMA(point, distance, forward, out); +} + +// Returns true for any climbable sloped surface +qboolean UTIL_IsClimableSlope(vec3_t normal) +{ + // Floor + if (normal[0] == 0 && normal[1] == 0 && normal[2] == 1) + return false; + // Climable slope + if (normal[0] >= -MAX_STEEPNESS && normal[0] <= MAX_STEEPNESS && normal[1] >= -MAX_STEEPNESS && normal[1] <= MAX_STEEPNESS && normal[2] >= (MAX_STEEPNESS - 0.10)) // && tr_right.plane.normal[2] >= MAX_STEEPNESS) + return true; + else + return false; +} +qboolean UTIL_IsFlatFloor(vec3_t normal) +{ + if (normal[0] == 0 && normal[1] == 0 && normal[2] == 1) + return true; + else + return false; +} + +// Finds if the edge is a ledge +// Returns the ledge data struct: normal, endpos, ledge height +ledge_data_t UTIL_FindLedge(nav_t* nav, surface_data_t* face_data, vec3_t v1, vec3_t v2) +{ + ledge_data_t ledge; + memset(&ledge, 0, sizeof(ledge_data_t)); + + int facenum; + vec3_t start_left, start_right; + //vec3_t normal = { 0, 0, 1 }; // perfectly flat + + if (0) + { + // Debug + LerpVector(v1, v2, 0.50, start_left); + LerpVector(v2, v1, 0.50, start_right); + start_left[2] += 8; + start_right[2] += 8; + vec3_t end_left = { start_left[0], start_left[1], -4096 }; + vec3_t end_right = { start_right[0], start_right[1], -4096 }; + VectorCopy(start_left, ledge.left_start); + VectorCopy(start_right, ledge.right_start); + VectorCopy(end_left, ledge.left_end); + VectorCopy(end_right, ledge.right_end); + return ledge; + } + + // Ignore all non-walkable faces + if ( (face_data->face_type & FACETYPE_WALK) == 0 && (face_data->face_type & FACETYPE_TINYWALK) == 0) + return ledge; + + // Ignore liquid faces + //if ((face_data->contents & CONTENTS_WATER) || (face_data->contents & CONTENTS_LAVA) || (face_data->contents & CONTENTS_SLIME)) + // return ledge; + + for (int f = 0; f < nav->faces_total; f++) + { + for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[f].verts[i], nav->surface_data_faces[f].verts[i + 1])) + { + // Ignore all walkable faces + if ((nav->surface_data_faces[f].face_type & FACETYPE_WALK) == 0 && (nav->surface_data_faces[f].face_type & FACETYPE_TINYWALK) == 0) + { + // Test if 'ledge' is a non-solid face + if (1) + { + vec3_t mins = { -1, -1, -0 }; + vec3_t maxs = { 1, 1, 1 }; + trace_t tr = SV_Trace(v1, mins, maxs, v2, NULL, MASK_ALL); // MASK_PLAYERSOLID | MASK_WATER); + if (tr.startsolid == false && tr.fraction == 1.0) + return ledge; // Not a ledge + + // Ignore liquid faces + //if ((face_data->contents & CONTENTS_WATER) || (face_data->contents & CONTENTS_LAVA) || (face_data->contents & CONTENTS_SLIME)) + // return ledge; + } + + // Find a face which is not our own face + + /* + // Left + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, 4.01, start_left, NULL); // Turn 90 degrees left and move x units + start_left[2] += 8.1; + vec3_t end_left = { start_left[0], start_left[1], -4096 }; + trace_t tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID | MASK_WATER); + short facenum_left = UTIL_FindFace(nav, tr_left.endpos, tr_left.plane.normal); // Get facenum of the face that was hit + if (facenum_left == f) // same face + { + VectorCopy(start_left, end_left); + end_left[2] += 32; + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, 4.01, start_left, NULL); // Turn 90 degrees left and move x units + BOTLIB_BSP_AddAreaConnections(nav, f, f, start_left, end_left, FACE_CONN_DIRECT, FACE_MOVE_GROUND, 0); + } + */ + + // Right + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, 4.01, start_right, NULL); // Turn 90 degrees right and move x units + start_right[2] += 8.1; + vec3_t end_right = { start_right[0], start_right[1], -4096 }; + trace_t tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID | MASK_WATER); + short facenum_right = UTIL_FindFace(nav, tr_right.endpos, tr_right.plane.normal); // Get facenum of the face that was hit + + /* + // Left + if (facenum_left != INVALID && facenum_left != face_data->facenum) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(start_left, ledge.startpos); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = abs(tr_left.endpos[2] - start_left[2]); + //return ledge; + + // Debug + ledge.hit_side = 1; // none = 0, left = 1, right = 2 + } + */ + + // Right + //if (facenum_right != INVALID && facenum_right != face_data->facenum) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + + + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(start_right, ledge.startpos); + VectorCopy(tr_right.endpos, ledge.endpos); + VectorCopy(v1, ledge.v1); + VectorCopy(v2, ledge.v2); + ledge.height = abs(tr_right.endpos[2] - start_right[2]); + //return ledge; + + + + + // Test if ledge is a wall + vec3_t v1_wall, v2_wall; + const float MOVE_AWAY = 0.5; + vec3_t forward; + vec3_t center_start, center_end; + LerpVector(v1, v2, 0.50, center_start); + VectorCopy(center_start, center_end); + center_start[2] += 0.25; + center_end[2] += 32; + vec3_t mins = { -0.1, -0.1, 0 }; + vec3_t maxs = { 0.1, 0.1, 32 }; + //trace_t tr_point = SV_Trace(center_start, NULL, NULL, center_start, NULL, MASK_PLAYERSOLID | MASK_WATER); + //trace_t tr_line = SV_Trace(center_start, NULL, NULL, center_end, NULL, MASK_PLAYERSOLID | MASK_WATER); + trace_t tr_box = SV_Trace(center_start, mins, maxs, center_start, NULL, MASK_SOLID); + //if (tr_point.startsolid || tr_line.fraction < 1.0) + if (tr_box.startsolid || tr_box.fraction < 1.0) + { + ledge.is_ledge = false; + ledge.is_wall = true; + + if (0) // Adjust wall edge to be away from the wall + { + // Move away from surface normal + //nav->surface_data_faces[f].normal; + + // Try heading 90 degrees right + BOTLIB_UTIL_ROTATE_EDGE_POINT(v2, v1, v1, 90, MOVE_AWAY, v1_wall); // Move v1 away from the wall + BOTLIB_UTIL_ROTATE_EDGE_POINT(v2, v1, v2, 90, MOVE_AWAY, v2_wall); // Move v2 away from the wall + + // Move v1_wall and v2_wall slightly above the ground + v1_wall[2] += 1; + v2_wall[2] += 1; + + // Test new edge to see if it's free from the wall + LerpVector(v1_wall, v2_wall, 0.50, center_start); + //center_start[2] += 1; + trace_t tr_point = SV_Trace(center_start, NULL, NULL, center_start, NULL, MASK_SOLID); + if (tr_point.startsolid) // Still inside wall, switch sides + { + // Try heading 90 degrees left + BOTLIB_UTIL_ROTATE_EDGE_POINT(v1, v2, v1, 90, MOVE_AWAY, v1_wall); // Move v1 away from the wall + BOTLIB_UTIL_ROTATE_EDGE_POINT(v1, v2, v2, 90, MOVE_AWAY, v2_wall); // Move v2 away from the wall + + // Move v1_wall and v2_wall slightly above the ground + v1_wall[2] += 1; + v2_wall[2] += 1; + } + + // Test if v1_wall or v2_wall are inside a solid + VectorSubtract(v2_wall, v1_wall, forward); + VectorNormalize(forward); + tr_point = SV_Trace(v1_wall, NULL, NULL, v1_wall, NULL, MASK_PLAYERSOLID | MASK_WATER); + if (tr_point.startsolid) + { + VectorMA(v1_wall, MOVE_AWAY, forward, v1_wall); // Bring v1_wall closer to v2_wall by MOVE_AWAY units + } + else + { + VectorMA(v1_wall, -MOVE_AWAY, forward, v1_wall); // Take v1_wall further from v2_wall by MOVE_AWAY units + } + + tr_point = SV_Trace(v2_wall, NULL, NULL, v2_wall, NULL, MASK_PLAYERSOLID | MASK_WATER); + if (tr_point.startsolid) + { + VectorMA(v2_wall, -MOVE_AWAY, forward, v2_wall); // Bring v2_wall closer to v1_wall by MOVE_AWAY units + } + else + { + VectorMA(v2_wall, MOVE_AWAY, forward, v2_wall); // Take v2_wall further from v1_wall by MOVE_AWAY units + } + + + + + + /* + // Move v1_wall and v2_wall x units apart from each other + // Get direction vector + vec3_t forward; + VectorSubtract(v2_wall, v1_wall, forward); + VectorNormalize(forward); + VectorMA(v2_wall, 0.25, forward, v2_wall); + VectorMA(v1_wall, -0.25, forward, v1_wall); + */ + + // Move v1_wall and v2_wall slightly above the ground + //v1_wall[2] += 4; + //v2_wall[2] += 4; + //trace_t tr_v1 = SV_Trace(v1_wall, NULL, NULL, v1, NULL, MASK_PLAYERSOLID | MASK_WATER); + //v1_wall[2] = tr_v1.endpos[2]; + //trace_t tr_v2 = SV_Trace(v2_wall, NULL, NULL, v2, NULL, MASK_PLAYERSOLID | MASK_WATER); + //v2_wall[2] = tr_v2.endpos[2]; + + //v1_wall[2] -= 0.9; + //v2_wall[2] -= 0.9; + + // Copy the final result + VectorCopy(v1_wall, ledge.v1); + VectorCopy(v2_wall, ledge.v2); + } + } + + + + // Debug + ledge.hit_side = 2; // none = 0, left = 1, right = 2 + } + + /* + // Debug + //ledge.hit_side = 2; // none = 0, left = 1, right = 2 + VectorCopy(start_left, ledge.left_start); + VectorCopy(tr_left.endpos, ledge.left_end); + VectorCopy(start_right, ledge.right_start); + VectorCopy(tr_right.endpos, ledge.right_end); + // + if (0) + { + LerpVector(v1, v2, 0.50, start_left); + LerpVector(v2, v1, 0.50, start_right); + start_left[2] += 8; + start_right[2] += 8; + VectorCopy(start_left, ledge.left_start); + VectorCopy(start_right, ledge.right_start); + VectorCopy(end_left, ledge.left_end); + VectorCopy(end_right, ledge.right_end); + } + */ + + //Com_Printf("%s could not find edge at face %d\n", __func__, f); + //ledge.is_ledge = true; + return ledge; + } + } + } + } + + + + + + + + + return ledge; + + + + + + + + const float DROP_DIST = 1.0; // 1.0 + const float MOVE_DIST = 1.0; // 16.0 + vec3_t mins = { -15.99, -15.99, 0 }; + vec3_t maxs = { 15.99, 15.99, 1 }; + + // Trace sideways to find if two faces are connected + { + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, 0.1, start_left, NULL); // Turn 90 degrees left and move 0.1 units + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, 0.1, start_right, NULL); // Turn 90 degrees right and move 0.1 units + vec3_t end_left = { start_left[0], start_left[1], -4096 }; + vec3_t end_right = { start_right[0], start_right[1], -4096 }; + trace_t tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID); + trace_t tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID); + if (tr_left.startsolid && tr_right.startsolid) // Not a ledge + return ledge; // is_ledge == false; + } + + //if (UTIL_IsClimableSlope(face_data->normal)) + // return ledge; + + // Do another trace, hitting the face on the left and right of the edge, find a face which is not our own face + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, 0.25, start_left, NULL); // Turn 90 degrees left and move 0.25 units + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, 0.25, start_right, NULL); // Turn 90 degrees right and move 0.25 units + start_left[2] += 0.1; + start_right[2] += 0.1; + vec3_t end_left = { start_left[0], start_left[1], -4096 }; + vec3_t end_right = { start_right[0], start_right[1], -4096 }; + trace_t tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID); + trace_t tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID); + + + + + // If we went left and hit the same face as our own, then trace right + facenum = UTIL_FindFace(nav, tr_left.endpos, tr_left.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + //if (UTIL_IsClimableSlope(tr_left.plane.normal)) return ledge; + + if (facenum == face_data->facenum) + { + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, MOVE_DIST, start_right, NULL); // Turn 90 degrees right and move MOVE_DIST units + end_right[0] = start_right[0]; + end_right[1] = start_right[1]; + end_right[2] = -4096; + //tr_right = SV_Trace(start_right, mins, maxs, end_right, NULL, MASK_PLAYERSOLID); + tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID); + if (tr_right.startsolid) + return ledge; + + facenum = UTIL_FindFace(nav, tr_right.endpos, tr_right.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + if (facenum == face_data->facenum) + Com_Printf("%s facenum == face_data->facenum %d\n", __func__, facenum); + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + //if (nav->surface_data_faces[facenum].face_type == FACETYPE_WALK || nav->surface_data_faces[facenum].face_type == FACETYPE_TINYWALK) + if ((face_data->face_type & FACETYPE_WALK) || (face_data->face_type & FACETYPE_TINYWALK)) + return ledge; + } + } + } + + if (abs(tr_right.endpos[2] - start_right[2]) > DROP_DIST) + { + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(start_right, ledge.startpos); + VectorCopy(tr_right.endpos, ledge.endpos); + ledge.height = abs(tr_right.endpos[2] - start_right[2]); + return ledge; + } + } + + // Facenum was different + //if (0) + //else if (UTIL_IsClimableSlope(tr_left.plane.normal)) + //else if (UTIL_IsClimableSlope(face_data->normal)) + else + { + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, 1, start_left, NULL); // Turn 90 degrees right and move MOVE_DIST units + start_left[2] += 4; + end_left[0] = start_left[0]; + end_left[1] = start_left[1]; + end_left[2] = -4096; + //tr_left = SV_Trace(start_left, mins, maxs, end_left, NULL, MASK_PLAYERSOLID); + tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID); + if (tr_left.startsolid) + return ledge; + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + if (nav->surface_data_faces[facenum].face_type == FACETYPE_WALK || nav->surface_data_faces[facenum].face_type == FACETYPE_TINYWALK) + return ledge; + } + } + + //if (nav->surface_data_faces[facenum].face_type != FACETYPE_WALK && nav->surface_data_faces[facenum].face_type != FACETYPE_TINYWALK) + if (abs(tr_left.endpos[2] - start_left[2]) > 4) // DROP_DIST + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(start_left, ledge.startpos); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = abs(tr_left.endpos[2] - start_left[2]); + return ledge; + } + } + } + + // If we went right and hit the same face as our own, then trace left + facenum = UTIL_FindFace(nav, tr_right.endpos, tr_right.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + //if (UTIL_IsClimableSlope(tr_right.plane.normal)) return ledge; + + // Facenum was the same as our own, so trace left + if (facenum == face_data->facenum) + { + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, MOVE_DIST, start_left, NULL); // Turn 90 degrees left and move MOVE_DIST units + end_left[0] = start_left[0]; + end_left[1] = start_left[1]; + end_left[2] = -4096; + //tr_left = SV_Trace(start_left, mins, maxs, end_left, NULL, MASK_PLAYERSOLID); + tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID); + if (tr_left.startsolid) + return ledge; + + facenum = UTIL_FindFace(nav, tr_left.endpos, tr_left.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + if (facenum == face_data->facenum) + Com_Printf("%s facenum == face_data->facenum %d\n", __func__, facenum); + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + if (nav->surface_data_faces[facenum].face_type == FACETYPE_WALK || nav->surface_data_faces[facenum].face_type == FACETYPE_TINYWALK) + return ledge; + } + } + } + + if (abs(tr_left.endpos[2] - start_left[2]) > DROP_DIST) + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(start_left, ledge.startpos); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = abs(tr_left.endpos[2] - start_left[2]); + return ledge; + } + } + + // Facenum was different + //if (0) + //else if (UTIL_IsClimableSlope(tr_right.plane.normal)) + else + { + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, 1, start_right, NULL); // Turn 90 degrees right and move MOVE_DIST units + start_right[2] += 4; + end_right[0] = start_right[0]; + end_right[1] = start_right[1]; + end_right[2] = -4096; + //tr_right = SV_Trace(start_right, mins, maxs, end_right, NULL, MASK_PLAYERSOLID); + tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID); + if (tr_right.startsolid) + return ledge; + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + if (nav->surface_data_faces[facenum].face_type == FACETYPE_WALK || nav->surface_data_faces[facenum].face_type == FACETYPE_TINYWALK) + return ledge; + } + } + + //if (nav->surface_data_faces[facenum].face_type != FACETYPE_WALK && nav->surface_data_faces[facenum].face_type != FACETYPE_TINYWALK) + if (abs(tr_right.endpos[2] - start_right[2]) > 4) // DROP_DIST + { + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(start_right, ledge.startpos); + VectorCopy(tr_right.endpos, ledge.endpos); + ledge.height = abs(tr_right.endpos[2] - start_right[2]); + return ledge; + } + } + } + + + + + + if (0) + { + const float left_drop = abs(tr_left.endpos[2] - start_left[2]); + const float right_drop = abs(tr_right.endpos[2] - start_right[2]); + + if (left_drop > right_drop) + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(start_left, ledge.startpos); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = abs(tr_left.endpos[2] - start_left[2]); + return ledge; + } + else + { + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(start_right, ledge.startpos); + VectorCopy(tr_right.endpos, ledge.endpos); + ledge.height = abs(tr_right.endpos[2] - start_right[2]); + return ledge; + } + } + + + + + return ledge; + + + + + + + + + + + + + + + if (tr_left.startsolid && tr_right.startsolid) // Not a ledge + { + return ledge; // this will be: ledge.is_ledge = false; + } + //else if (abs(tr_left.endpos[2] - start_left[2]) <= 0.01 || abs(tr_right.endpos[2] - start_right[2]) <= 0.01) + //{ + // return ledge; // this will be: ledge.is_ledge = false; + //} + + if (tr_left.startsolid == 0 && tr_right.startsolid) // Ledge is left side + { + //if (tr_left.plane.normal[0] < -MAX_STEEPNESS && tr_left.plane.normal[0] > MAX_STEEPNESS && tr_left.plane.normal[1] < -MAX_STEEPNESS && tr_left.plane.normal[1] > MAX_STEEPNESS && tr_left.plane.normal[2] < (MAX_STEEPNESS - 0.10)) + //if (abs(tr_left.endpos[2] - start_left[2]) > 0.01 || (tr_left.plane.normal[0] < -MAX_STEEPNESS && tr_left.plane.normal[0] > MAX_STEEPNESS && tr_left.plane.normal[1] < -MAX_STEEPNESS && tr_left.plane.normal[1] > MAX_STEEPNESS && tr_left.plane.normal[2] < (MAX_STEEPNESS - 0.10))) + //if (abs(tr_left.endpos[2] - start_left[2]) > 0.01) + { + if (1) + { + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, MOVE_DIST, start_left, NULL); // Turn 90 degrees left and move MOVE_DIST units + end_left[0] = start_left[0]; + end_left[1] = start_left[1]; + end_left[2] = -4096; + //tr_left = SV_Trace(start_left, mins, maxs, end_left, NULL, MASK_PLAYERSOLID); + tr_left = SV_Trace(start_left, NULL, NULL, end_left, NULL, MASK_PLAYERSOLID); + + //if (abs(tr_left.endpos[2] - start_left[2]) < 4) // Ignore tiny ledges + // return ledge; + } + + // Check if face that was hit shares an edge with the current face + int facenum = UTIL_FindFace(nav, tr_left.endpos, tr_left.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + //if (facenum == face_data->facenum) + // Com_Printf("%s facenum == face_data->facenum %d\n", __func__, facenum); + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + //if (tr_left.plane.normal[0] < -MAX_STEEPNESS && tr_left.plane.normal[0] > MAX_STEEPNESS && tr_left.plane.normal[1] < -MAX_STEEPNESS && tr_left.plane.normal[1] > MAX_STEEPNESS && tr_left.plane.normal[2] < (MAX_STEEPNESS - 0.10)) + // Any climbable sloped surface + //if (tr_left.plane.normal[0] >= -MAX_STEEPNESS && tr_left.plane.normal[0] <= MAX_STEEPNESS && tr_left.plane.normal[1] >= -MAX_STEEPNESS && tr_left.plane.normal[1] <= MAX_STEEPNESS && tr_left.plane.normal[2] >= (MAX_STEEPNESS - 0.10)) // && tr_right.plane.normal[2] >= MAX_STEEPNESS) + return ledge; + } + } + } + + //if (tr_left.startsolid == 0) + if (abs(tr_left.endpos[2] - start_left[2]) > DROP_DIST) + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(start_left, ledge.startpos); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = abs(tr_left.endpos[2] - start_left[2]); + } + } + } + else if (tr_left.startsolid && tr_right.startsolid == 0) // Ledge is right side + { + //if (tr_right.plane.normal[0] < -MAX_STEEPNESS && tr_right.plane.normal[0] > MAX_STEEPNESS && tr_right.plane.normal[1] < -MAX_STEEPNESS && tr_right.plane.normal[1] > MAX_STEEPNESS && tr_right.plane.normal[2] < (MAX_STEEPNESS - 0.10)) + //if (abs(tr_right.endpos[2] - start_right[2]) > 0.01 || (tr_right.plane.normal[0] < -MAX_STEEPNESS && tr_right.plane.normal[0] > MAX_STEEPNESS && tr_right.plane.normal[1] < -MAX_STEEPNESS && tr_right.plane.normal[1] > MAX_STEEPNESS && tr_right.plane.normal[2] < (MAX_STEEPNESS - 0.10))) + //if (abs(tr_right.endpos[2] - start_right[2]) > 0.01) + { + if (1) + { + BOTLIB_UTIL_ROTATE_CENTER(v2, v1, 90, MOVE_DIST, start_right, NULL); // Turn 90 degrees right and move MOVE_DIST units + end_right[0] = start_right[0]; + end_right[1] = start_right[1]; + end_right[2] = -4096; + //tr_right = SV_Trace(start_right, mins, maxs, end_right, NULL, MASK_PLAYERSOLID); + tr_right = SV_Trace(start_right, NULL, NULL, end_right, NULL, MASK_PLAYERSOLID); + + //if (abs(tr_right.endpos[2] - start_right[2]) < 4) // Ignore tiny ledges + // return ledge; + } + + // Check if face that was hit shares an edge with the current face + int facenum = UTIL_FindFace(nav, tr_right.endpos, tr_right.plane.normal); // Get facenum of the face that was hit + if (facenum != INVALID) // If valid face (i.e. any surface that was included in the surface_data_t array) + { + //if (facenum == face_data->facenum) + // Com_Printf("%s facenum == face_data->facenum %d\n", __func__, facenum); + + for (int i = 0; i < nav->surface_data_faces[facenum].num_verts; i += 2) + { + // Compare the two edges to see if they share the same edge + if (UTIL_EdgeCompare(v1, v2, nav->surface_data_faces[facenum].verts[i], nav->surface_data_faces[facenum].verts[i + 1])) + { + //if (tr_right.plane.normal[0] < -MAX_STEEPNESS && tr_right.plane.normal[0] > MAX_STEEPNESS && tr_right.plane.normal[1] < -MAX_STEEPNESS && tr_right.plane.normal[1] > MAX_STEEPNESS && tr_right.plane.normal[2] < (MAX_STEEPNESS - 0.10)) + //if (tr_right.plane.normal[0] >= -MAX_STEEPNESS && tr_right.plane.normal[0] <= MAX_STEEPNESS && tr_right.plane.normal[1] >= -MAX_STEEPNESS && tr_right.plane.normal[1] <= MAX_STEEPNESS && tr_right.plane.normal[2] >= (MAX_STEEPNESS - 0.10)) // && tr_right.plane.normal[2] >= MAX_STEEPNESS) + return ledge; + } + } + } + + //if (tr_right.startsolid == 0) + if (abs(tr_right.endpos[2] - start_right[2]) > DROP_DIST) + { + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(start_right, ledge.startpos); + VectorCopy(tr_right.endpos, ledge.endpos); + ledge.height = abs(tr_right.endpos[2] - start_right[2]); + } + } + } + + /* + // Ledge is both left and right, this might occur on very thin ledges less than 0.01 width + // Perhaps we should ignore these ledges? Even if very rare... + else + { + const float left_drop = abs(tr_left.endpos[2] - start_left[2]); + const float right_drop = abs(tr_right.endpos[2] - start_right[2]); + + if (left_drop > right_drop) + { + ledge.is_ledge = true; + VectorCopy(tr_left.plane.normal, ledge.normal); + VectorCopy(tr_left.endpos, ledge.endpos); + ledge.height = left_drop; + } + else + { + ledge.is_ledge = true; + VectorCopy(tr_right.plane.normal, ledge.normal); + VectorCopy(tr_right.endpos, ledge.endpos); + ledge.height = right_drop; + } + } + */ + + return ledge; +} + +// Using the given position and normal, find the nearest face +// Returns the face number, or INVALID (-1) if none found +// INVALID surfaces are is surface that wasn't included in the surface_data_t array, or the position is outside the map +int UTIL_FindFace(nav_t* nav, vec3_t pos, vec3_t normal) +{ + for (int f = 0; f < nav->faces_total; f++) + { + // Skip far away faces + if (VectorDistance(nav->surface_data_faces[f].center_poly, pos) > 512) // Distance from face center to pos + continue; + + // Skip if not same normal + if (UTIL_CompareNormal(nav->surface_data_faces[f].normal, normal, 0.05) == false) // 0.05 + continue; + + // Skip if not same plane + if (UTIL_CompareVertPlanes(nav->surface_data_faces[f].verts[0], pos, normal, 0.01) == false) // 0.01 + continue; + + // Inside face + if (UTIL_InsideFace(&nav->surface_data_faces[f], pos, 0.01)) // Point inside face + return f; + + //if (AAS_InsideFaceBoxScan(&nav->surface_data_faces[f], pos, 0.01, true)) // Boxscan point inside face + // return f; + } + return INVALID; +} + +// Takes an edge (v1 to v2) and finds an position that is perpendicular to the edge and is inside the face +// If the edge is in a corner, the function will try to move the position further inside the face +// Returns true if a valid position was found, and out is set to the valid inner position +qboolean UTIL_FindValidInnerPosition(surface_data_t* face_data, int facenum, vec3_t normal, float distance, vec3_t v1, vec3_t v2, vec3_t out) +{ + vec3_t left, right; + vec3_t offset, center; + + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, distance, left, NULL); // Turn 90 degrees left and move 'distance' units + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, -90, distance, right, NULL); // Turn -90 degrees right and move 'distance' units + + //if (UTIL_FindFace(nav, left, normal) == facenum) // Check we're still inside the same face + if (UTIL_InsideFace(face_data, left, 0.01)) // Point inside face + { + // See if the new position is valid, if not then readjust and try new position + // Reasons this might fail: edge might be in a corner, or the face is too small, etc + if (UTIL_PlayerBoxTestCouched(left) == false) + { + // Print facenum + //Com_Printf("%s facenum: %d\n", __func__, facenum); + + // Same as the initial left rotation, except double the distance. + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, 90, distance * 2, left, NULL); // Turn 90 degrees left and move 'distance * 2' units + + // Find the middle + LerpVector(v1, v2, 0.50, center); + + // Try left + BOTLIB_UTIL_ROTATE_CENTER(center, left, 90, distance, offset, NULL); // Turn 90 degrees left and move 'distance' units + //if (UTIL_FindFace(nav, offset, normal) == facenum) // Check we're still inside the same face + if (UTIL_InsideFace(face_data, offset, 0.01)) // Point inside face + { + if (UTIL_PlayerBoxTestCouched(offset)) + { + VectorCopy(offset, out); + return true; + } + } + + // Try right + BOTLIB_UTIL_ROTATE_CENTER(center, left, -90, distance, offset, NULL); // Turn -90 degrees right and move 'distance' units + //if (UTIL_FindFace(nav, offset, normal) == facenum) // Check we're still inside the same face + if (UTIL_InsideFace(face_data, offset, 0.01)) // Point inside face + { + if (UTIL_PlayerBoxTestCouched(offset)) + { + VectorCopy(offset, out); + return true; + } + } + + // Failed to find a valid position + return false; + } + else + { + VectorCopy(left, out); + return true; + } + } + //else if (UTIL_FindFace(nav, right, normal) == facenum) // Check we're still inside the same face + else if (UTIL_InsideFace(face_data, right, 0.01)) // Point inside face + { + // See if the new position is valid, if not then readjust and try new position + // Reasons this might fail: edge might be in a corner, or the face is too small, etc + if (UTIL_PlayerBoxTestCouched(right) == false) + { + // Same as the initial right rotation, except double the distance. + BOTLIB_UTIL_ROTATE_CENTER(v1, v2, -90, distance * 2, right, NULL); // Turn 90 degrees right and move 'distance * 2' units + + // Find the middle + LerpVector(v1, v2, 0.50, center); + + // Try left + BOTLIB_UTIL_ROTATE_CENTER(center, right, 90, distance, offset, NULL); // Turn 90 degrees left and move 'distance' units + //if (UTIL_FindFace(nav, offset, normal) == facenum) // Check we're still inside the same face + if (UTIL_InsideFace(face_data, offset, 0.01)) // Point inside face + { + if (UTIL_PlayerBoxTestCouched(offset)) + { + VectorCopy(offset, out); + return true; + } + } + + // Try right + BOTLIB_UTIL_ROTATE_CENTER(center, right, -90, distance, offset, NULL); // Turn -90 degrees right and move 'distance' units + //if (UTIL_FindFace(nav, offset, normal) == facenum) // Check we're still inside the same face + if (UTIL_InsideFace(face_data, offset, 0.01)) // Point inside face + { + if (UTIL_PlayerBoxTestCouched(offset)) + { + VectorCopy(offset, out); + return true; + } + } + + // Failed to find a valid position + return false; + } + else + { + VectorCopy(right, out); + return true; + } + } + else + { + return false; + } +} + +qboolean surface_data_added = false; // True if surface data has been added to the map +qboolean surface_faces_connected = false; // True if the surface faces have been connected +const int SURFACE_MEM_CHUNK_SIZE = 8192; // How many surface faces to allocate at a time +qboolean Alloc_Surface_Memory(nav_t* nav) +{ + // ------------------------------ + // Alloc memory for surface faces + // ------------------------------ + surface_data_t* prev_surface_data_faces = NULL; // Used to free memory if realloc fails + + // Malloc first surface added + if (nav->surface_data_faces == NULL) + { + // This malloc is purposely freed in CM_FreeMap() -- this should occur every map change + nav->surface_data_faces = (surface_data_t*)Z_TagMallocz(sizeof(surface_data_t) * SURFACE_MEM_CHUNK_SIZE, TAG_CMODEL); + //nav->surface_data_faces = (surface_data_t*)malloc(sizeof(surface_data_t) * SURFACE_MEM_CHUNK_SIZE); + //if (nav->surface_data_faces != NULL) + // memset(nav->surface_data_faces, 0, sizeof(surface_data_t) * SURFACE_MEM_CHUNK_SIZE); + } + // Realloc all other surfaces added + else if (nav->faces_total > 0 && nav->faces_total >= SURFACE_MEM_CHUNK_SIZE) + { + Com_Printf("%s failed to malloc surface_data_faces. Increase SURFACE_MEM_CHUNK_SIZE > %d\n", __func__, SURFACE_MEM_CHUNK_SIZE); + + //prev_surface_data_faces = nav->surface_data_faces; // Keep a copy + //nav->surface_data_faces = (surface_data_t*)realloc(nav->surface_data_faces, sizeof(surface_data_t) * (nav->faces_total + 1)); + } + + // Deal with malloc/realloc failure + if (nav->surface_data_faces == NULL) + { + Com_Printf("%s failed to malloc prev_surface_data_faces. Out of memory!\n", __func__); + if (prev_surface_data_faces) + { + free(prev_surface_data_faces); // Free using the copy, because nodes is null + nav->surface_data_faces = NULL; + prev_surface_data_faces = NULL; + } + nav->faces_total--; + return false; // Failed to allocate memory + } + + return true; // Success +} +qboolean Save_Surface_Data(nav_t* nav, surface_data_t* face_data) +{ + int i; + + if (face_data == NULL) + return false; + + // Add the data + if (nav->surface_data_faces) + { + int num = nav->faces_total; // Current total surfaces + + // ======================================================== + // Save raw BSP Data + // ======================================================== + nav->surface_data_faces[num].surf = face_data->surf; // Surface pointer + for (int c = 0; c < MAX_TEXNAME; c++) // Copy the texture name + { + nav->surface_data_faces[num].texture[c] = face_data->texture[c]; // Surface texture name + } + VectorCopy(face_data->normal, nav->surface_data_faces[num].normal); // Surface normal + nav->surface_data_faces[num].contents = face_data->contents; // Surface contents + nav->surface_data_faces[num].drawflags = face_data->drawflags; // Surface drawflags + + VectorCopy(face_data->first_vert, nav->surface_data_faces[num].first_vert); // First vert - used when calculating triangles (i.e. gl_showtris "1") + + // Raw verts + nav->surface_data_faces[num].num_verts = face_data->num_verts; + for (i = 0; i < face_data->num_verts; i++) + { + VectorCopy(face_data->verts[i], nav->surface_data_faces[num].verts[i]); // Verts + } + + // Raw Edges + for (i = 0; i < face_data->num_verts; i += 2) + { + VectorCopy(face_data->edge_center[i / 2], nav->surface_data_faces[num].edge_center[i / 2]); // Center edge + VectorCopy(face_data->edge_center_stepsize[i / 2], nav->surface_data_faces[num].edge_center_stepsize[i / 2]); // Center[2] edge + 32 + } + + // ======================================================== + // Save Custom BSP Data - Safe to modify + // ======================================================== + nav->surface_data_faces[num].facenum = nav->faces_total; // Assigned surface number + + nav->surface_data_faces[num].face_type = face_data->face_type; // Surface type: none, ignore, walkable, wall, ladder, water, damage... + + // Aligned verts + nav->surface_data_faces[num].num_aligned_verts = face_data->num_aligned_verts; + for (i = 0; i < face_data->num_aligned_verts; i++) + { + VectorCopy(face_data->aligned_verts[i], nav->surface_data_faces[num].aligned_verts[i]); // Aligned verts + } + + // Aligned edges + for (i = 0; i < face_data->num_aligned_verts; i += 2) + { + VectorCopy(face_data->aligned_edge_center[i / 2], nav->surface_data_faces[num].aligned_edge_center[i / 2]); // Center edge + VectorCopy(face_data->aligned_edge_center_stepsize[i / 2], nav->surface_data_faces[num].aligned_edge_center_stepsize[i / 2]); // Center[2] edge + 18 + } + + // Offset edges + for (i = 0; i < face_data->num_verts; i += 2) + { + nav->surface_data_faces[num].edge_offset_type[i / 2] = face_data->edge_offset_type[i / 2]; // Offset type + VectorCopy(face_data->edge_valid_pos[i / 2], nav->surface_data_faces[num].edge_valid_pos[i / 2]); // Offset edge + VectorCopy(face_data->edge_valid_stepsize[i / 2], nav->surface_data_faces[num].edge_valid_stepsize[i / 2]); // Offset edge_valid_pos[2] + 18 + } + + // Ledges + for (i = 0; i < face_data->num_verts; i += 2) + { + nav->surface_data_faces[num].ledge[i / 2].is_ledge = face_data->ledge[i / 2].is_ledge; // Is ledge + nav->surface_data_faces[num].ledge[i / 2].height = face_data->ledge[i / 2].height; // Ledge height + VectorCopy(face_data->ledge[i / 2].startpos, nav->surface_data_faces[num].ledge[i / 2].startpos); // Start of ledge + VectorCopy(face_data->ledge[i / 2].endpos, nav->surface_data_faces[num].ledge[i / 2].endpos); // End of ledge + VectorCopy(face_data->ledge[i / 2].normal, nav->surface_data_faces[num].ledge[i / 2].normal); // Normal + + // Debug + nav->surface_data_faces[num].ledge[i / 2].hit_side = face_data->ledge[i / 2].hit_side; // Hit side: none, left, right + VectorCopy(face_data->ledge[i / 2].left_start, nav->surface_data_faces[num].ledge[i / 2].left_start); // Trace left start + VectorCopy(face_data->ledge[i / 2].left_end, nav->surface_data_faces[num].ledge[i / 2].left_end); // Trace left end + VectorCopy(face_data->ledge[i / 2].right_start, nav->surface_data_faces[num].ledge[i / 2].right_start); // Trace right start + VectorCopy(face_data->ledge[i / 2].right_end, nav->surface_data_faces[num].ledge[i / 2].right_end); // Trace right end + } + + // Polygon Center + VectorCopy(face_data->center_poly, nav->surface_data_faces[num].center_poly); // Center + VectorCopy(face_data->center_poly_32_units, nav->surface_data_faces[num].center_poly_32_units); // Center[2] + 32 + + nav->surface_data_faces[num].volume = face_data->volume; + + // Min/max surface lengths (from poly center to edge) + nav->surface_data_faces[num].min_length = face_data->min_length; + nav->surface_data_faces[num].max_length = face_data->max_length; + + nav->faces_total++; // Increment face counter + + if (face_data->face_type & FACETYPE_NONE) + nav->ignored_faces_total++; // Increment ignored face counter + if ( (face_data->face_type & FACETYPE_IGNORED) || (face_data->face_type & FACETYPE_WALL)) + nav->ignored_faces_total++; // Increment ignored face counter + + return true; // Successfully added surface data + } + + return false; // Failure to added surface data +} + +// This will attempt to locate the most valid position on the edge, and store it in edge_valid_pos +// The first pick will be the center of the edge, +// and if that fails, it will try a position closest to the center to maximize the chance of LoS with other edges +void UTIL_FindValidEdgePositions(surface_data_t* face_data) +{ + const int MIN_EDGE_WIDTH = 8; // Minimum edge width to consider + const float EDGE_OFFSET_STEP = 0.25; // How far to offset the edge each time + + float edge_length = 0; // Length of the edge + float edge_pos = 0; // Position along the edge + vec3_t curr_pos = { 0 }; // Current position along the edge + int total_positions = 0; // Total valid positions found along the edge + #define MAX_EDGE_POSITIONS 512 // Maximum number of valid positions to find along the edge + vec3_t positions[MAX_EDGE_POSITIONS]; // All the valid positions found along the edge + vec3_t vec = { 0 }; // Vector used for calculating a direction away from the edge + + // Test player hitbox against the edge to see if it fits + // (some edges are against a wall, so move it inside the face a bit) + for (int e = 0; e < face_data->num_verts; e += 2) + { + face_data->edge_offset_type[e / 2] = EDGE_OFFSET_NONE; // Set null offset flag + + // Skip very small edges + if (VectorDistance(face_data->verts[e], face_data->verts[e + 1]) < MIN_EDGE_WIDTH) + { + VectorCopy(face_data->edge_center[e / 2], face_data->edge_valid_pos[e / 2]); // Copy center edge + face_data->edge_offset_type[e / 2] = EDGE_OFFSET_CENTER; + + MoveAwayFromNormal(face_data->drawflags, face_data->normal, vec, STEPSIZE); // Get the vector + VectorAdd(face_data->edge_valid_pos[e / 2], vec, face_data->edge_valid_stepsize[e / 2]); // Move away from the surface normal by STEPSIZE + + continue; + } + + total_positions = 0; // Reset number of positions + + // Test if center edge fits player hitbox + // + // Does it for on the center edge? + if (UTIL_PlayerBoxTestCouched(face_data->edge_center[e / 2])) + { + VectorCopy(face_data->edge_center[e / 2], face_data->edge_valid_pos[e / 2]); // Copy center edge + face_data->edge_offset_type[e / 2] = EDGE_OFFSET_CENTER; + + } + // Does it fit somewhere else along the edge? + else // It didn't fit, so lets try different positions along the edge to see if it will fit. Keeping the most centered position + { + edge_length = VectorDistance(face_data->verts[e], face_data->verts[e + 1]); // Get the length of the edge + + //if (face_data->facenum == 2450) + // int a = 0; + + // While loop to find the most centered position that fits the player hitbox along the edge + edge_pos = 1; // Reset edge position + while (edge_pos < edge_length) + { + if (total_positions + 1 > MAX_EDGE_POSITIONS) + { + Com_Printf("%s total_positions > MAX_EDGE_POSITIONS overflow error. Increase MAX_EDGE_POSITIONS\n", __func__); + break; + } + // LerpVector (vert -> vert, position along edge, output) + LerpVector(face_data->verts[e], face_data->verts[e + 1], edge_pos / edge_length, curr_pos); // Get position along the edge + if (UTIL_PlayerBoxTestCouched(curr_pos)) // Yes, it fits + { + VectorCopy(curr_pos, positions[total_positions]); + total_positions++; + //break; // Stop searching + } + edge_pos += EDGE_OFFSET_STEP; // Increment edge position by EDGE_OFFSET_STEP + } + } + + if (total_positions) + { + // Find the position closest to the center of the edge to maximize the chance of LoS with other edges + int closest = 0; + float closest_dist = 99999; + float dist = 0; + for (int i = 0; i < total_positions; i++) + { + dist = VectorDistance(face_data->edge_center[e / 2], positions[i]); + if (dist < closest_dist) + { + closest_dist = dist; + closest = i; + } + } + VectorCopy(positions[closest], face_data->edge_valid_pos[e / 2]); // Copy modified edge pos + face_data->edge_offset_type[e / 2] = EDGE_OFFSET_LENGTH; + } + else // All else failed, so just use the center edge + { + VectorCopy(face_data->edge_center[e / 2], face_data->edge_valid_pos[e / 2]); // Copy center edge pos + face_data->edge_offset_type[e / 2] = EDGE_OFFSET_CENTER; + } + + MoveAwayFromNormal(face_data->drawflags, face_data->normal, vec, STEPSIZE); // Get the vector + VectorAdd(face_data->edge_valid_pos[e / 2], vec, face_data->edge_valid_stepsize[e / 2]); // Move away from the surface normal by STEPSIZE + } +} + +// If the edge is too close to an obstacle, move it away from the obstacle +// Test if the new position is valid (fits the player hitbox) +void UTIL_FindValidInnerEdgePositions(surface_data_t* face_data) +{ + const int MIN_EDGE_WIDTH = 8; // Minimum edge width to consider + vec3_t edge2_inside; // The same edge but moved further inside the face + + // Test player hitbox against the edge to see if it fits + // (some edges are against a wall, so move it inside the face a bit) + for (int e = 0; e < face_data->num_verts; e += 2) + { + // Skip very small edges + if (VectorDistance(face_data->verts[e], face_data->verts[e + 1]) < MIN_EDGE_WIDTH) + continue; + + // Test if the pos fits + if (UTIL_PlayerBoxTestCouched(face_data->edge_valid_pos[e / 2])) + continue; + + if (UTIL_FindValidInnerPosition(face_data, face_data->facenum, face_data->normal, 16, face_data->verts[e], face_data->verts[e + 1], edge2_inside)) + { + VectorCopy(edge2_inside, face_data->edge_valid_pos[e / 2]); // Success: update edge with the new position + } + + } +} + +void UTIL_FindLedges(nav_t *nav, surface_data_t* face_data) +{ + for (int e = 0; e < face_data->num_verts; e += 2) + { + face_data->ledge[e/2] = UTIL_FindLedge(nav, face_data, face_data->verts[e], face_data->verts[e + 1]); + } +} + +// Gets the surface data +qboolean GetSurfaceData(nav_t* nav, mface_t* surf, int face) +{ + //surface_data_t face_data; // Surface data + vec3_t center_vec = { 0 }; // Center point vector + + if (nav->faces_total + 1 > SURFACE_MEM_CHUNK_SIZE) + { + Com_Printf("%s SURFACE_MEM_CHUNK_SIZE overflow error. Increase SURFACE_MEM_CHUNK_SIZE\n", __func__); + return false; + } + + Alloc_Surface_Memory(nav); // Alloc memory for surface face + int num = nav->faces_total; + surface_data_t* face_data = nav->surface_data_faces + num; + face_data->facenum = num; // WARNING: facenum might not a be trustworthy number yet: some faces haven't been added to Save_Surface_Data(), or some might get rejected + + face_data->surf = surf; // Copy surface reference + face_data->facenum = face; // Init face num + + face_data->drawflags = surf->drawflags; // Init draw flags + for (int c = 0; c < MAX_TEXNAME; c++) // Copy the texture name + { + face_data->texture[c] = surf->texinfo->name[c]; + } + //if (strlen(face_data->texture) == 0) // Check if the texture name is empty + //{ + //Com_Printf("%s: WARNING: texture name is empty. Len:%d [%s]\n", __func__, strlen(face_data->texture), face_data->texture); + //return false; + //} + + if (Q_strcasestr(surf->texinfo->name, "trigger") != NULL) // Look for "trigger" textures, and ignore them + { + //Com_Printf("%s: removing face with texture [%s]\n", __func__, surf->texinfo->name); + return false; + } + //Com_Printf("%s: texture [%s]\n", __func__, surf->texinfo->name); + + + InverseNormals(face_data); // Inverse the surface normal if needed + InitVertexData(surf, face_data); // Init the vertex data + FixMalformedVertexData(face_data); // Fix any malformed vertex data + AlignVertexPoints(face_data); // Align verts + BOTLIB_UTIL_PolygonCenter2D(face_data->aligned_verts, face_data->num_aligned_verts, face_data->center_poly); // Get the center of the face polygon + + // 32 - Player model width + MoveAwayFromNormal(face_data->drawflags, face_data->normal, center_vec, 32); // Get the vector + VectorAdd(face_data->center_poly, center_vec, face_data->center_poly_32_units); // Move away from the surface normal by 32 units (full player width) + + // Check for liquids, solids, and space + if (CheckFaceLiquid(face_data) == false && CheckFaceSolid(face_data) == false) return false; + if (CheckSkyAboveFace(face_data) == false) return false; + + // Edges + UTIL_GetCenterOfVertEdges(face_data); // Get the center each VERT edge + UTIL_GetCenterOfAlignedEdges(face_data); // Get the center each ALIGNED edge + UTIL_FindValidEdgePositions(face_data); // Find a valid edge position + UTIL_FindValidInnerEdgePositions(face_data); // Find a valid inner edge position + + face_data->volume = AAS_FaceArea(face_data); // Surface area + UTIL_GetFaceLengthMinMax(face_data); // Get the min/max length of the face + GetFaceContents(face_data); // Get the face contents + + face_data->face_type = SetSurfaceType(face_data); + + // Rejecting faces here might mean that a valid face_data->facenum might not be detected when searching the faces array + if (face_data->face_type & (FACETYPE_IGNORED | FACETYPE_SKY)) + return false; // Ignore face + + // Save all the data relating this surface + Save_Surface_Data(nav, face_data); + + return true; +} + +// Cache the surface data & save it +static void BOTLIB_SaveSurfaceData(nav_t* nav, bsp_t *bsp) +{ + int face = 0; + short int processed = 0; + mface_t* surf; + + // Clear out data (either from a previous map or init a new instance) + if (bsp->checksum != nav->surface_data_checksum) + { + surface_data_added = false; + surface_faces_connected = false; // Reset nearest neighbor faces + } + // Add the surface data + if (surface_data_added == false) + { + for (face = 0, surf = bsp->faces; face < bsp->numfaces; face++, surf++) + { + processed++; + + // Ignore sky surfaces + //if (surf->drawflags & SURF_SKY) + // continue; + + // Ignore roof surfaces + //if (surf->plane->normal[2] == 1) // Floor + //{ + //if (surf->drawflags & DSURF_PLANEBACK) // Roof + // continue; + //} + + if (GetSurfaceData(nav, surf, face) == false) + continue; + } + surface_data_added = true; + nav->surface_data_checksum = bsp->checksum; + Com_Printf("%s [map surfaces %d] [%d processed] [%d ignored] [%d usable]\n", __func__, processed, nav->faces_total, nav->ignored_faces_total, nav->faces_total - nav->ignored_faces_total); + } +} + +// Add a connection between two faces +// f1 = Face 1, f2 = Face 2 +// e1 = Edge 1, e2 = Edge 2 +// end_pos = The position where the touch was made +// Return: 1 = connected, 0 = error +int BOTLIB_BSP_AddAreaConnections(nav_t* nav, int f1, int f2, vec3_t start_pos, vec3_t end_pos, face_connection_type_t ftype, face_move_type_t mtype, float drop_height) +{ + const int fcc = nav->surface_data_faces[f1].snode_counter; // Face Connection Counter (code readability) + + if (nav->surface_data_faces[f1].snode_counter >= MAX_FACE_CONNECTIONS) + { + Com_Printf("%s - MAX_FACE_CONNECTIONS: %d overflow\n", __func__, MAX_FACE_CONNECTIONS); + return 1; // Error + } + + //if (ftype == FACE_CONN_NONE) return 0; + + // Check to make we've not already found this face + for (int c = 0; c < fcc; c++) + { + // Same face + if (nav->surface_data_faces[f1].snodes[c].facenum == nav->surface_data_faces[f2].facenum) + { + // Same start & end points + if (VectorCompare(start_pos, nav->surface_data_faces[f1].snodes[c].start) && VectorCompare(end_pos, nav->surface_data_faces[f1].snodes[c].end)) + { + //Com_Printf("%s alreaded added connection from face[%d to %d] s[%f %f %f] e[%f %f %f]\n", __func__, f1, f2, start_pos[0], start_pos[1], start_pos[2], end_pos[0], end_pos[1], end_pos[2]); + return 0; // Success (already added) + } + } + } + + + + // Add it to the list + nav->surface_data_faces[f1].snodes[fcc].type = ftype; // Connection type + nav->surface_data_faces[f1].snodes[fcc].move = mtype; // Movement type + nav->surface_data_faces[f1].snodes[fcc].facenum = nav->surface_data_faces[f2].facenum; // The face we touched + nav->surface_data_faces[f1].snodes[fcc].dropheight = drop_height; // Drop height + VectorCopy(start_pos, nav->surface_data_faces[f1].snodes[fcc].start); // + VectorCopy(end_pos, nav->surface_data_faces[f1].snodes[fcc].end); // + //VectorCopy(nav->surface_data_faces[f2].edge_center[e2 / 2], nav->surface_data_faces[f1].snodes[fcc].end); // The edge center + nav->surface_data_faces[f1].snode_counter++; // Increment how many faces we touched + + return 0; // Success +} + +// Searches all faces to find the nearest connected edges - faces that share the same edge +void FindNearestFacesByConnectedEdges(nav_t* nav) +{ + const int MIN_EDGE_WIDTH = 8; // Minimum edge width to consider + int facenum = 0; // Face number + vec3_t start_pos; // The start position + vec3_t end_pos; // The end position + int area_connections = 0; // Total connections found + face_connection_type_t face_conn_type = FACE_CONN_NONE; + face_move_type_t move_type = FACE_MOVE_NONE; + vec3_t edge1, edge2; // Edges + + // Test and add local face edges + if (1) + for (int f1 = 0; f1 < nav->faces_total; f1++) // Face 1 + { + for (int e1 = 0; e1 < nav->surface_data_faces[f1].num_verts; e1 += 2) // Edge 1 (face1) + { + if (IgnoreSmallSurfaces) + { + // Skip very small edges + if (VectorDistance(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1]) < MIN_EDGE_WIDTH) + continue; + } + + // For each edge, test the start and end positions + for (int e2 = 0; e2 < nav->surface_data_faces[f1].num_verts; e2 += 2) + { + if (e1 == e2) continue; // Skip self edge + + if (IgnoreSmallSurfaces) + { + // Skip very small edges + if (VectorDistance(nav->surface_data_faces[f1].verts[e2], nav->surface_data_faces[f1].verts[e2 + 1]) < MIN_EDGE_WIDTH) + continue; + } + + // Skip edges going in the same direction + //if (UTIL_VectorDirectionCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f1].verts[e3], nav->surface_data_faces[f1].verts[e3 + 1], true, 0.1) == true) + // continue; + + VectorCopy(nav->surface_data_faces[f1].edge_valid_pos[e1 / 2], edge1); + VectorCopy(nav->surface_data_faces[f1].edge_valid_pos[e2 / 2], edge2); + + //VectorCopy(nav->surface_data_faces[f1].verts[e1 / 2], edge1); + //VectorCopy(nav->surface_data_faces[f1].verts[e2 / 2], edge2); + + // Skip if not LoS + if (UTIL_LineOfSight(edge1, edge2, nav->surface_data_faces[f1].normal) == false) + { + //vec3_t empty = { 0, 0, 0 }; + //BOTLIB_BSP_AddAreaConnections(nav, INVALID, INVALID, empty, empty, 0, 0, 0); + continue; + } + + // Starting position away from the ledge (pointing towards a ledge) + face_conn_type = FACE_CONN_NONE; + ledge_data_t *ledge = &nav->surface_data_faces[f1].ledge[e2 / 2]; // Get reference to ledge data + facenum = INVALID; // Reset facenum + if (ledge->height) // Only run UTIL_FindFace() if we have a ledge height + facenum = UTIL_FindFace(nav, ledge->endpos, ledge->normal); // Find the face we're touching + + //if (ledge->height && facenum == INVALID) + //Com_Printf("%s - invalid facenum at face:%d edge:%d -> edge:%d\n", __func__, f1, e1/2, e2/2); + + // Drops into water + if (facenum != INVALID && nav->surface_data_faces[facenum].contents & CONTENTS_WATER) + { + face_conn_type = FACE_CONN_WATER; + move_type = FACE_MOVE_DOWN; + } + // Flat surface (no drop - faces are on the same plane) + else if (ledge->height == 0) + { + face_conn_type = FACE_CONN_DIRECT; + move_type = FACE_MOVE_GROUND; + } + // Stepsize + else if (ledge->height <= STEPSIZE) + { + face_conn_type = FACE_CONN_STEP; + move_type = FACE_MOVE_DOWN; + } + // Drop down height #1 (can jump back up) + else if (ledge->height <= MAX_JUMP_HEIGHT) + { + face_conn_type = FACE_CONN_JUMP; + move_type = FACE_MOVE_DOWN; + } + // Drop down height #2 (safe drop, cannot jump back up) + else if (ledge->height <= MAX_CROUCH_FALL_HEIGHT) + { + face_conn_type = FACE_CONN_DROP; + move_type = FACE_MOVE_DOWN; + } + // Falling height (injury or fall death) + else if (ledge->height > MAX_CROUCH_FALL_HEIGHT) + { + face_conn_type = FACE_CONN_LEDGE; + move_type = FACE_MOVE_STOP; + } + else + { + //vec3_t empty = { 0, 0, 0 }; + //BOTLIB_BSP_AddAreaConnections(nav, INVALID, INVALID, empty, empty, 0, 0, 0); + Com_Printf("%s WARNING: could not connect face %d to edge %d\n", __func__, f1, e2); + continue; + } + + // Check if edge is shared with another face + int face2 = f1; // Face2 defaults to face1 + for (int f2 = 0; f2 < nav->faces_total; f2++) // Face 1 + { + if (nav->surface_data_faces[f2].face_type == FACETYPE_WALL) continue; // Skip walls + if (f1 == f2) continue; // skip self + for (int e3 = 0; e3 < nav->surface_data_faces[f2].num_verts; e3 += 2) // Edge 2 (face2) + { + if (IgnoreSmallSurfaces) // Skip very small edges + { + if (VectorDistance(nav->surface_data_faces[f2].verts[e3], nav->surface_data_faces[f2].verts[e3 + 1]) < MIN_EDGE_WIDTH) + continue; + } + + // Skip unconnected edges + if (UTIL_EdgeCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f2].verts[e3], nav->surface_data_faces[f2].verts[e3 + 1]) == false) + continue; + + face2 = f2; // Update face2 + break; + } + if (face2 != f1) break; // Break if face was changed + } + + // Add connection from EDGE TO EDGE within the same face + if (BOTLIB_BSP_AddAreaConnections(nav, f1, face2, edge1, edge2, face_conn_type, move_type, ledge->height) == 0) + area_connections++; + + // Add connection from LEDGE to the ENDPOS of face below + if (1) + { + if (face_conn_type != FACE_CONN_DIRECT && face_conn_type != FACE_CONN_LEDGE) + { + if (facenum == INVALID) + continue; + + //if (ledge.endpos[2] > edge2[2]) + // continue; + + // Add connection from LEDGE to the ENDPOS of face below + if (BOTLIB_BSP_AddAreaConnections(nav, f1, facenum, edge2, ledge->endpos, face_conn_type, FACE_MOVE_DOWN, ledge->height) == 0) + //if (BOTLIB_BSP_AddAreaConnections(nav, f1, facenum, ledge.startpos, ledge.endpos, face_conn_type, ledge.height) == 0) + area_connections++; + + // Add reverse connection from ENDPOS of face below to the LEDGE above + if (1 && face_conn_type == FACE_CONN_STEP || face_conn_type == FACE_CONN_JUMP) + { + if (BOTLIB_BSP_AddAreaConnections(nav, facenum, f1, ledge->endpos, edge2, face_conn_type, FACE_MOVE_UP, ledge->height) == 0) + //if (BOTLIB_BSP_AddAreaConnections(nav, facenum, f1, ledge.endpos, ledge.startpos, face_conn_type, ledge.height) == 0) + area_connections++; + + /* + // Add EDGE connections to the ENDPOS of face below + for (int e3 = 0; e3 < nav->surface_data_faces[f1].num_verts; e3 += 2) + { + // Test if the start pos fits + if (UTIL_PlayerBoxTestCouched(nav->surface_data_faces[f1].edge_center[e3 / 2]) == false) + continue; + + if (BOTLIB_BSP_AddAreaConnections(nav, facenum, f1, nav->surface_data_faces[f1].edge_center[e3 / 2], ledge->endpos, face_conn_type, ledge->height) == 0) + area_connections++; + } + */ + } + } + } + } + } + } + + + + return; + + + + // Test and add connected faces by an edge connection + for (int f1 = 0; f1 < nav->faces_total; f1++) // Face 1 + { + if (nav->surface_data_faces[f1].face_type == FACETYPE_WALL) // Skip walls + continue; + + for (int f2 = 0; f2 < nav->faces_total; f2++) // Face 2 + { + if (f1 == f2) continue; // skip self + + if (nav->surface_data_faces[f2].face_type == FACETYPE_WALL) // Skip walls + continue; + + // Ignore faces far away + //if (VectorDistance(nav->surface_data_faces[f1].center_poly, nav->surface_data_faces[f2].center_poly) > 256) + // continue; + + //qboolean connected = false; + for (int e1 = 0; e1 < nav->surface_data_faces[f1].num_verts; e1 += 2) // Edge 1 (face1) + { + if (IgnoreSmallSurfaces) // Skip very small edges + { + if (VectorDistance(nav->surface_data_faces[f2].verts[e1], nav->surface_data_faces[f2].verts[e1 + 1]) < MIN_EDGE_WIDTH) + continue; + } + + for (int e2 = 0; e2 < nav->surface_data_faces[f2].num_verts; e2 += 2) // Edge 2 (face2) + { + if (IgnoreSmallSurfaces) // Skip very small edges + { + if (VectorDistance(nav->surface_data_faces[f2].verts[e2], nav->surface_data_faces[f2].verts[e2 + 1]) < MIN_EDGE_WIDTH) + continue; + } + + // Skip unconnected edges + if (UTIL_EdgeCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f2].verts[e2], nav->surface_data_faces[f2].verts[e2 + 1]) == false) + continue; + + if (0) + { + for (int c = 0; c < nav->surface_data_faces[f2].snode_counter; c++) + { + //VectorCompare( nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f2].snodes[c].start) + } + } + + if (0) // Add connection from EDGE to EDGE of nearby face + { + //for (int e3 = 0; e3 < nav->surface_data_faces[f1].num_verts; e3) + //for (int c1 = 0; c1 < nav->surface_data_faces[f1].snode_counter; c1++) + { + for (int c = 0; c < nav->surface_data_faces[f2].snode_counter; c++) + { + VectorCopy(nav->surface_data_faces[f1].verts[e1], start_pos); + //VectorCopy(nav->surface_data_faces[f1].snodes[c1].start, start_pos); + VectorCopy(nav->surface_data_faces[f2].snodes[c].start, end_pos); + if (BOTLIB_BSP_AddAreaConnections(nav, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT, FACE_MOVE_SAFE, 0) == 0) + area_connections++; + + VectorCopy(nav->surface_data_faces[f1].verts[e1 + 1], start_pos); + if (BOTLIB_BSP_AddAreaConnections(nav, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT, FACE_MOVE_SAFE, 0) == 0) + area_connections++; + } + } + } + + // Connected face edges + if (0) + { + //VectorCopy(nav->surface_data_faces[f1].edge_center[e1 / 2], start_pos); + //VectorCopy(nav->surface_data_faces[f2].edge_center[e2 / 2], end_pos); + //if (BOTLIB_BSP_AddAreaConnections(nav, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT, FACE_MOVE_SAFE, 0) == 0) + // area_connections++; + + //connected = true; // We connected + + for (int c = 0; c < nav->surface_data_faces[f2].snode_counter; c++) + { + //if (nav->surface_data_faces[f2].snodes[c].type == FACETYPE_WALK) + { + //if (nav->surface_data_faces[f2].snodes[c].start) + { + // Add connection from EDGE TO EDGE within the same face + VectorCopy(nav->surface_data_faces[f1].edge_center[e1 / 2], start_pos); + VectorCopy(nav->surface_data_faces[f2].snodes[c].start, end_pos); + if (BOTLIB_BSP_AddAreaConnections(nav, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT, FACE_MOVE_SAFE, 0) == 0) + area_connections++; + + //break; + } + } + } + } + + //break; // Finished connecting + } + //if (connected) break; // Finished connecting, search another face + } + } + } + + + + + + return; + + + + + // Test and add connected face edges + for (int f1 = 0; f1 < nav->faces_total; f1++) // Face 1 + { + //if (IgnoreSurface(&nav->surface_data_faces[f1])) // Ignore surface + // continue; + + if (nav->surface_data_faces[f1].face_type == FACETYPE_WALL) + continue; + + for (int f2 = 0; f2 < nav->faces_total; f2++) // Face 2 + { + if (f1 == f2) continue; // skip self + + if (nav->surface_data_faces[f2].face_type == FACETYPE_WALL) + continue; + + //if (IgnoreSurface(&nav->surface_data_faces[f2])) // Ignore surface + // continue; + + // Ignore faces far away + //if (VectorDistance(nav->surface_data_faces[f1].center_poly, nav->surface_data_faces[f2].center_poly) > 256) + // continue; + + for (int e1 = 0; e1 < nav->surface_data_faces[f1].num_verts; e1 += 2) // Edge 1 (face1) + { + if (IgnoreSmallSurfaces) // Skip very small edges + { + //if (VectorDistance(nav->surface_data_faces[f2].verts[e1], nav->surface_data_faces[f2].verts[e1 + 1]) < MIN_EDGE_WIDTH) + // continue; + } + + for (int e2 = 0; e2 < nav->surface_data_faces[f2].num_verts; e2 += 2) // Edge 2 (face2) + { + if (IgnoreSmallSurfaces) // Skip very small edges + { + //if (VectorDistance(nav->surface_data_faces[f2].verts[e2], nav->surface_data_faces[f2].verts[e2 + 1]) < MIN_EDGE_WIDTH) + // continue; + } + + // Do edges connect? + //if (UTIL_EdgeCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f2].verts[e2], nav->surface_data_faces[f2].verts[e2 + 1], nav->surface_data_faces[f2].edge_center[e2 / 2]) == false) + if (UTIL_EdgeCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f2].verts[e2], nav->surface_data_faces[f2].verts[e2 + 1]) == false) + { + //if (UTIL_FindLedge == false) + continue; + } + + // For each edge, test the start and end positions + for (int e3 = 0; e3 < nav->surface_data_faces[f1].num_verts; e3 += 2) + { + if (e1 == e3) continue; // Skip same edge + + // Skip edges going in the same direction + if (UTIL_VectorDirectionCompare(nav->surface_data_faces[f1].verts[e1], nav->surface_data_faces[f1].verts[e1 + 1], nav->surface_data_faces[f1].verts[e3], nav->surface_data_faces[f1].verts[e3 + 1], true, 0.1) == true) + continue; + + // Test if the start pos fits + if (UTIL_PlayerBoxTestCouched(nav->surface_data_faces[f1].edge_center[e3 / 2]) == false) + continue; + + // Test LoS + if (UTIL_LineOfSight(nav->surface_data_faces[f1].edge_center[e1 / 2], nav->surface_data_faces[f1].edge_center[e3 / 2], nav->surface_data_faces[f1].normal)) + { + VectorCopy(nav->surface_data_faces[f1].edge_center[e3 / 2], start_pos); + VectorCopy(nav->surface_data_faces[f1].edge_center[e1 / 2], end_pos); + + if (BOTLIB_BSP_AddAreaConnections(nav, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT, FACE_MOVE_SAFE, 0) == 0) + { + area_connections++; + //goto NEXT_FACE; // Success (either added or already added) + } + //else + // return; // Error + } + } + + /* + // For each vertex, test the start and end positions + for (int e3 = 0; e3 < bsp->surface_data_faces[f1].num_verts; e3 += 2) + { + if (e1 == e2) continue; // Skip same edge + + // Test if the start pos fits + if (UTIL_PlayerBoxTestCouched(bsp->surface_data_faces[f1].edge_center[e3 / 2]) == false) + continue; + + // Test LoS + if (UTIL_LineOfSight(bsp->surface_data_faces[f2].edge_center[e2 / 2], bsp->surface_data_faces[f1].edge_center[e3 / 2], bsp->surface_data_faces[f1].normal)) + { + VectorCopy(bsp->surface_data_faces[f1].edge_center[e3 / 2], start_pos); + VectorCopy(bsp->surface_data_faces[f1].edge_center[e1 / 2], end_pos); + + if (BOTLIB_BSP_AddAreaConnections(bsp, f1, f2, start_pos, end_pos, FACE_CONN_DIRECT) == 0) + { + area_connections++; + //goto NEXT_FACE; // Success (either added or already added) + } + else + return; // Error + } + } + */ + + } // End Edge 2 + } // End Edge 1 + + + } // End Face 2 + } //End Face 1 + + Com_Printf("%s - Found %d area connections\n", __func__, area_connections); +} + +// Init face connections +void InitFaceConnections(nav_t* nav) +{ + if (surface_faces_connected) + return; + surface_faces_connected = true; // Surface faces are now connected + + if (nav->surface_data_faces == NULL) + { + Com_Printf("%s - surface data is NULL\n", __func__); + return; + } + + // Init the nearest face edges + for (int f = 0; f < nav->faces_total; f++) // Face + { + nav->surface_data_faces[f].snode_counter = 0; // Init the nearest counter + + for (int fc = 0; fc < MAX_FACE_CONNECTIONS; fc++) + { + nav->surface_data_faces[f].snodes[fc].type = FACE_CONN_NONE; // (face_connection_type_t) + nav->surface_data_faces[f].snodes[fc].facenum = INVALID; + VectorClear(nav->surface_data_faces[f].snodes[fc].end); + } + } + + // Find ledges + for (int f = 0; f < nav->faces_total; f++) // Face + { + UTIL_FindLedges(nav, &nav->surface_data_faces[f]); // Find ledges + } + + FindNearestFacesByConnectedEdges(nav); // Find the nearest connected face edges +} + + + +// Recursively checks for connected polygons +void ShowConnectedPolygons_r(nav_t* nav, int face, int *facelist, int *facelist_count) +{ + qboolean found = false; + int new_face = INVALID; + + if ((*facelist_count) + 1 >= MAX_FACE_CONNECTIONS) + { + Com_Printf("%s - MAX_FACE_CONNECTIONS reached\n", __func__); + return; + } + + // Check if face has any connections + for (int c = 0; c < nav->surface_data_faces[face].snode_counter; c++) + { + if ((*facelist_count) + 1 >= MAX_FACE_CONNECTIONS) + { + Com_Printf("%s - MAX_FACE_CONNECTIONS reached\n", __func__); + return; + } + + if (nav->surface_data_faces[face].snodes[c].type == FACE_CONN_NONE) + continue; + + new_face = nav->surface_data_faces[face].snodes[c].facenum; // get face number + + if (new_face == face) // Skip self face + continue; + + // Check if face is already in the list + found = false; + for (int f = 0; f < *facelist_count; f++) + { + if (new_face == facelist[f]) + { + found = true; + break; + } + } + if (found) continue; + + // Add face to list + facelist[*facelist_count] = new_face; + (*facelist_count)++; + + // Recursively check for other connected faces + ShowConnectedPolygons_r(nav, new_face, facelist, facelist_count); + } + + return; +} +void ShowConnectedPolygons(nav_t* nav, int face) +{ + int facelist[MAX_FACE_CONNECTIONS]; + int facelist_count = 0; + memset( facelist, 0, sizeof(facelist) ); + + facelist[0] = face; // Add the initial face to array + facelist_count++; + + ShowConnectedPolygons_r(nav, face, facelist, &facelist_count); // From the initial face, recursively find all the connected faces + + // Draw the faces + for (int f = 0; f < facelist_count; f++) + { + face = facelist[f]; // Get the next face in the array + + // Draw all polygon faces + //GL_ColorPolygon(nav->surface_data_faces[face].aligned_verts[0], nav->surface_data_faces[face].num_aligned_verts, &nav->surface_data_faces[face], 0, 0, 1, 0.4); + + // Draw all arrows / lines + if (1) + { + for (int c = 0; c < nav->surface_data_faces[face].snode_counter; c++) + { + //GL_DrawArrow(nav->surface_data_faces[face].snodes[nf].start, nav->surface_data_faces[face].snodes[nf].end, U32_CYAN, 10); + + const uint32_t red[24] = { U32_RED, U32_RED }; + vec3_t points[2]; + VectorCopy(nav->surface_data_faces[face].snodes[c].start, points[0]); + VectorCopy(nav->surface_data_faces[face].snodes[c].end, points[1]); + GL_DrawLine(points[0], 2, red, 10, false); + } + } + } + + /* + for (int nf = 0; nf < nav->surface_data_faces[face].snode_counter; nf++) + { + int nearest_face = nav->surface_data_faces[face].snodes[nf].facenum; // get face number + if (nearest_face == INVALID) continue; // skip invalid + //if (nearest_face == face) continue; // skip self + GL_ColorPolygon(nav->surface_data_faces[nearest_face].aligned_verts[0], nav->surface_data_faces[nearest_face].num_aligned_verts, &nav->surface_data_faces[nearest_face], 0, 0, 1, 0.4); + } + */ +} + +// Draws the current face the player it looking at +//nav_t* nav_ = NULL; +static void BOTLIB_DrawFaces(void) +{ + int f; + const uint32_t color_magenta[24] = { U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA, U32_MAGENTA }; + const uint32_t color_cyan[24] = { U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN, U32_CYAN }; + const uint32_t color_green[24] = { U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN, U32_GREEN }; + #define U32_LIGHT_BLUE MakeColor(0, 128, 255, 255) + const uint32_t color_light_blue[24] = { U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE, U32_LIGHT_BLUE }; + const uint32_t color_red[24] = { U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED, U32_RED }; + const uint32_t color_yellow[24] = { U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW, U32_YELLOW }; + #define U32_ORANGE MakeColor(255, 165, 0, 255) + const uint32_t color_orange[24] = { U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, + U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, + U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, + U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE, U32_ORANGE }; + const uint32_t all_colors[24] = { U32_RED, U32_RED, U32_GREEN, U32_GREEN, U32_BLUE, U32_BLUE, U32_YELLOW, U32_YELLOW, U32_CYAN, U32_CYAN, U32_MAGENTA, U32_MAGENTA, U32_RED, U32_RED, U32_GREEN, U32_GREEN, U32_BLUE, U32_BLUE, U32_YELLOW, U32_YELLOW, U32_CYAN, U32_CYAN, U32_MAGENTA, U32_MAGENTA }; + + //if (!gl_static.world.cache) // Client side + if (!sv.cm.cache) // Server side + { + Com_Error(ERR_DROP, "%s: no map loaded", __func__); + return; + } + //bsp_t* bsp = gl_static.world.cache; // Client side + bsp_t* bsp = sv.cm.cache; // Server side + + // Malloc nav + if (sv.cm.nav == NULL) + { + sv.cm.nav = (nav_t*) Z_TagMallocz(sizeof(nav_t), TAG_CMODEL); // This malloc is purposely freed in CM_FreeMap() -- this should occur every map change + if (sv.cm.nav == NULL) + { + Com_Error(ERR_DROP, "%s: malloc failed", __func__); + return; + } + //memset(sv.cm.nav, 0, sizeof(nav_t)); + } + nav_t* nav = sv.cm.nav; + + + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + + if (0) return; // Disable nav + + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + + + // Save the surface data + BOTLIB_SaveSurfaceData(nav, bsp); + + if (nav->surface_data_faces == NULL) + { + Com_Printf("%s: nav->surface_data_faces == NULL\n", __func__); + return; + } + + //surface_data_t* sdf = &nav->surface_data_faces[255]; + //nav->surface_data_faces[255].facenum; + + InitFaceConnections(nav); // Init the faces + + + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + + if (1) return; // Disable realtime nav view + + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////// + + // Feet trace + float vert_plane_epsilon = 0.01; + qboolean line_trace = false; + trace_t tr; + tr = UTIL_FeetBoxTrace(); // Box trace from player feet to the world below + //if (tr.fraction == 1.0 && tr.plane.normal[2] != 1) // Found nothing, try a box trace + if (tr.plane.normal[2] != 1) // Not on flat ground + { + line_trace = true; + //tr = UTIL_FeetLineTrace(); // Line trace from player feet to the world below + } + + // Camera trace + trace_t tr_cam = UTIL_CameraTrace(64); // 64 - Trace from the camera to the world + qboolean ladder = (tr_cam.contents & CONTENTS_LADDER); + //if (tr_cam.contents & CONTENTS_LADDER) Com_Printf("%s: tr_cam.contents & CONTENTS_LADDER\n", __func__); + + if (0) + { + // Cycles through RGB colors (0.0 to 1.0) + float cycle_red; + float cycle_green; + float cycle_blue; + UTIL_CycleThroughColors(&cycle_red, &cycle_green, &cycle_blue, 0.001); + } + + for (f = 0; f < nav->faces_total; f++) + { + // Skip checking these faces + //if (nav->surface_data_faces[f].face_type != FACETYPE_WALK && nav->surface_data_faces[f].face_type != FACETYPE_TINYWALK && nav->surface_data_faces[f].face_type != FACETYPE_LADDER) // || nav->surface_data_faces[f].face_type == FACETYPE_WATER) + if ((nav->surface_data_faces[f].face_type & FACETYPE_WALK) || (nav->surface_data_faces[f].face_type & FACETYPE_TINYWALK) || (nav->surface_data_faces[f].face_type & FACETYPE_LADDER) || (nav->surface_data_faces[f].face_type & FACETYPE_WATER)) + ; + else + continue; + + //if (nav->surface_data_faces[f].face_type & FACETYPE_ROOF) + // continue; + + if (1) //if (ladder == false) + { + if (1) // Set to false to show all faces + { + // Ignore faces that are too far away + if (VectorDistance(nav->surface_data_faces[f].center_poly, tr.endpos) > 512) // Distance from the face center to the player + continue; + //GL_ColorPolygon(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, 0, 255, 0, 0.25); // debug to show distance + + if (UTIL_CompareNormal(nav->surface_data_faces[f].normal, tr.plane.normal, 0.05) == false) // Same normals + continue; + + if (line_trace) vert_plane_epsilon = 0.31; // Slopes + else vert_plane_epsilon = 0.01; // Flat ground + + // Slope planes + if (line_trace && UTIL_CompareVertPlanesSlope(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, tr.endpos, tr.plane.normal, vert_plane_epsilon) == false) + continue; // Not on the same plane + // Flat planes + else if (UTIL_CompareVertPlanes(nav->surface_data_faces[f].verts[0], tr.endpos, tr.plane.normal, vert_plane_epsilon) == false) + continue; // Not on the same plane + + //if (AAS_InsideFace(&nav->surface_data_faces[f], tr.endpos, 0.01, line_trace) == false) // Point inside face + // continue; + + if (AAS_InsideFaceBoxScan(&nav->surface_data_faces[f], tr.endpos, 0.01, line_trace) == false) // Box inside face + continue; + + ShowConnectedPolygons(nav, f); // Show connected polygons + } + + + //trace_t tr_test = UTIL_FeetBoxTrace(); // Box trace from player feet to the world below + //Com_Printf("%s line_trace_height:%f box_trace_height:%f\n", __func__, tr.endpos[2], tr_test.endpos[2]); + + + if (0) // Show colored face + { + GL_ColorPolygon(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, &nav->surface_data_faces[f], 0, 0.0, 1, 0.4); + //nav->surface_data_faces[f].surf->texnum[1] = TEXNUM_WHITE; + } + + if (0) // DEBUG: Show edges + { + if (0 && nav->surface_data_faces[f].contents & CONTENTS_LADDER) // Draw ladder edges + { + // Get the distance from the camera to poly center + vec3_t vec; + VectorSubtract(nav->surface_data_faces[f].center_poly, glr.fd.vieworg, vec); + float distance = VectorLength(vec); + float line_width; + // Change line width to be thinner the further away the distance is + if (distance < 1024) line_width = 12; + else if (distance < 2048) line_width = 4; + else line_width = 2; + + //Com_Printf("%s [LADDER] face:%d verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + GL_DrawLine(nav->surface_data_faces[f].verts[0], nav->surface_data_faces[f].num_verts, color_cyan, line_width, false); + } + else + { + //Com_Printf("%s face:%d verts:%d vol:%f frac:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_verts, volume, tr.fraction, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + //for (int i = 0; i < surface_data_faces[f].num_verts; i += 2) + { + //int edge = i / 2; // edge num + //Com_Printf("%s face:%d edge:%d [%f %f %f] -- [%f %f %f]\n", __func__, f, edge, nav->surface_data_faces[f].verts[i][0], nav->surface_data_faces[f].verts[i][1], nav->surface_data_faces[f].verts[i][2], nav->surface_data_faces[f].verts[i + 1][0], nav->surface_data_faces[f].verts[i + 1][1], nav->surface_data_faces[f].verts[i + 1][2]); + } + + + if (0) // Draw ledge lines + { + int num_ledge_verts = 0; + vec3_t ledge_verts[MAX_FACE_VERTS]; + for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + if (nav->surface_data_faces[f].ledge[i / 2].is_ledge) // Copy only ledge verts + { + //VectorCopy(nav->surface_data_faces[f].verts[i], ledge_verts[num_ledge_verts]); + //VectorCopy(nav->surface_data_faces[f].verts[i + 1], ledge_verts[num_ledge_verts + 1]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].v1, ledge_verts[num_ledge_verts]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].v2, ledge_verts[num_ledge_verts + 1]); + num_ledge_verts += 2; + } + } + + GL_DrawLine(ledge_verts[0], num_ledge_verts, color_orange, 4.0, false); + } + if (0) // Draw wall lines + { + int num_ledge_verts = 0; + vec3_t ledge_verts[MAX_FACE_VERTS]; + for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + if (nav->surface_data_faces[f].ledge[i / 2].is_wall) // Copy only wall verts + { + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].v1, ledge_verts[num_ledge_verts]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].v2, ledge_verts[num_ledge_verts + 1]); + num_ledge_verts += 2; + } + } + + GL_DrawLine(ledge_verts[0], num_ledge_verts, color_red, 4.0, false); + } + + + // Draw the edges of face player is standing on + //GL_DrawLine((const vec3_t*)&nav->surface_data_faces[f].verts, nav->surface_data_faces[f].num_verts, all_colors, 3.0); + + //Draw Arrows + if (1) + { + // Find the closest starting point + float closest = 999999; + int nearest_start_num = INVALID; + //vec3_t nearest_start_vec = { 0 }; + for (int nf = 0; nf < nav->surface_data_faces[f].snode_counter; nf++) + { + int nearest_face = nav->surface_data_faces[f].snodes[nf].facenum; + if (nearest_face != -1) + { + //volume = AAS_FaceArea(&nav->surface_data_faces[nearest_face]); // Surface area + //Com_Printf("%s nearest face:%d verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, nearest_face, nav->surface_data_faces[nearest_face].num_verts, volume, nav->surface_data_faces[nearest_face].normal[0], nav->surface_data_faces[nearest_face].normal[1], nav->surface_data_faces[nearest_face].normal[2], nav->surface_data_faces[nearest_face].texture); + + // Draw the connected faces to the face player is standing on (except the face player is standing on) + if (0) + { + if (nearest_face != f) + GL_DrawLine(nav->surface_data_faces[nearest_face].verts[0], nav->surface_data_faces[nearest_face].num_verts, all_colors, 3.0, false); + } + + // Draw arrows to connected faces + //GL_DrawArrow(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nf].end); + + // Get the distance from the player to the nearest connected face + float nearest_end_point = VectorDistance(nav->surface_data_faces[f].snodes[nf].start, tr.endpos); + if (nearest_end_point < closest) + { + closest = nearest_end_point; + //VectorCopy(nav->surface_data_faces[f].snodes[nf].start, nearest_start_vec); + nearest_start_num = nf; + } + } + } + + // Draw nearest connection within face + for (int nf = 0; nf < nav->surface_data_faces[f].snode_counter; nf++) + { + // Ledge arrows down to connected faces (have a different starting point) + if (nav->surface_data_faces[f].snodes[nf].dropheight > 0) + { + // If the x,y coords are the same (with an EPSILON of 0.1) then it's a ledge + // We use EPSILON here because a trace endpos is not 100% accurately located below and we don't want to draw a ledge arrow if it's not a ledge + if (fabs(nav->surface_data_faces[f].snodes[nf].start[0] - nav->surface_data_faces[f].snodes[nf].end[0]) < 0.1 + && fabs(nav->surface_data_faces[f].snodes[nf].start[1] - nav->surface_data_faces[f].snodes[nf].end[1]) < 0.1) + //if (nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_DROP + // || nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_STEP + // || nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_JUMP) + { + // Only draw drop arrows if the drop height is less than the max crouch fall height + if (nav->surface_data_faces[f].snodes[nf].dropheight < MAX_CROUCH_FALL_HEIGHT) + { + //GL_DrawArrow(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nf].end, U32_YELLOW, 10, false); // Caution + continue; + } + } + } + + if (nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_DROP + || nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_STEP + || nav->surface_data_faces[f].snodes[nf].type == FACE_CONN_JUMP) + continue; + + // Using the closest starting point, ensure it is valid AND face_connection_start[nf] is the same location as the closest starting point + if (nearest_start_num == INVALID) + continue; + if (VectorCompare(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nearest_start_num].start) == false) + continue; + + /* + // Draw arrows to connected faces + if (nav->surface_data_faces[f].snodes[nf].dropheight == 0) + GL_DrawArrow(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nf].end, U32_CYAN, 10, false); // Go + else if (nav->surface_data_faces[f].snodes[nf].dropheight < MAX_CROUCH_FALL_HEIGHT) + GL_DrawArrow(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nf].end, U32_YELLOW, 10, false); // Caution + else + GL_DrawArrow(nav->surface_data_faces[f].snodes[nf].start, nav->surface_data_faces[f].snodes[nf].end, U32_RED, 10, false); // Stop + */ + + //if (nav->surface_data_faces[f].snodes[nf].dropheight) + // Com_Printf("%s [%d] dropheight: %f\n", __func__, nf, nav->surface_data_faces[f].snodes[nf].dropheight); + } + + /* + // Draw nearest connection from connected face + if (0) + for (int nf = 0; nf < nav->surface_data_faces[f].snode_counter; nf++) + { + int nearest_face = nav->surface_data_faces[f].face_connection_facenum[nf]; + if (nearest_face != -1) + { + for (int ncf = 0; ncf < nav->surface_data_faces[nearest_face].snode_counter; ncf++) + { + if (VectorCompare(nav->surface_data_faces[nearest_face].face_connection_start[ncf], nearest_start_vec)) + { + // Draw arrows to connected faces + GL_DrawArrow(nearest_start_vec, nav->surface_data_faces[nearest_face].face_connection_end[ncf], U32_WHITE, 10); + } + } + } + } + */ + } + + } + } + if (0) // DEBUG: Show vert edges + { + if (nav->surface_data_faces[f].contents & CONTENTS_LADDER) + { + //Com_Printf("%s [LADDER] face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + GL_DrawLine(nav->surface_data_faces[f].verts[0], nav->surface_data_faces[f].num_verts, all_colors, 7.0, false); + } + else + { + //Com_Printf("%s face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + //int edge = i / 2; // edge num + //Com_Printf("%s face:%d edge:%d [%f %f %f] -- [%f %f %f]\n", __func__, f, edge, nav->surface_data_faces[f].aligned_verts[i][0], nav->surface_data_faces[f].aligned_verts[i][1], nav->surface_data_faces[f].aligned_verts[i][2], nav->surface_data_faces[f].aligned_verts[i + 1][0], nav->surface_data_faces[f].aligned_verts[i + 1][1], nav->surface_data_faces[f].aligned_verts[i + 1][2]); + } + GL_DrawLine(nav->surface_data_faces[f].verts[0], nav->surface_data_faces[f].num_verts, all_colors, 3.0, false); + } + } + if (0) // DEBUG: Show aligned edges + { + if (nav->surface_data_faces[f].contents & CONTENTS_LADDER) + { + Com_Printf("%s [LADDER] face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + GL_DrawLine(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, all_colors, 7.0, false); + } + else + { + Com_Printf("%s face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + for (int i = 0; i < nav->surface_data_faces[f].num_aligned_verts; i += 2) + { + //int edge = i / 2; // edge num + //Com_Printf("%s face:%d edge:%d [%f %f %f] -- [%f %f %f]\n", __func__, f, edge, nav->surface_data_faces[f].aligned_verts[i][0], nav->surface_data_faces[f].aligned_verts[i][1], nav->surface_data_faces[f].aligned_verts[i][2], nav->surface_data_faces[f].aligned_verts[i + 1][0], nav->surface_data_faces[f].aligned_verts[i + 1][1], nav->surface_data_faces[f].aligned_verts[i + 1][2]); + } + GL_DrawLine(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, all_colors, 3.0, false); + } + } + if (0) // DEBUG: Show face normals (center_poly -> center_poly_32_units) + { + vec3_t points[2]; + VectorCopy(nav->surface_data_faces[f].center_poly, points[0]); + VectorCopy(nav->surface_data_faces[f].center_poly_32_units, points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); + // Print the min and max length of the face + Com_Printf("%s face:%d min_length:%f max_length:%f volume:%f\n", __func__, f, nav->surface_data_faces[f].min_length, nav->surface_data_faces[f].max_length, nav->surface_data_faces[f].volume); + } + if (0) // DEBUG: Show vert center edges + { + vec3_t points[2]; + for (int i = 0; i < nav->surface_data_faces[f].num_verts / 2; i++) + { + VectorCopy(nav->surface_data_faces[f].edge_center[i], points[0]); + VectorCopy(nav->surface_data_faces[f].edge_center_stepsize[i], points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); + } + } + if (0) // DEBUG: Show aligned center edges + { + vec3_t points[2]; + for (int i = 0; i < nav->surface_data_faces[f].num_aligned_verts / 2; i++) + { + VectorCopy(nav->surface_data_faces[f].aligned_edge_center[i], points[0]); + VectorCopy(nav->surface_data_faces[f].aligned_edge_center_stepsize[i], points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); + } + } + if (0) // DEBUG: Show valid edge positions + { + vec3_t points[2]; + for (int i = 0; i < nav->surface_data_faces[f].num_verts / 2; i++) + { + VectorCopy(nav->surface_data_faces[f].edge_valid_pos[i], points[0]); + //VectorCopy(nav->surface_data_faces[f].edge_valid_stepsize[i], points[1]); + VectorCopy(nav->surface_data_faces[f].edge_center_stepsize[i], points[1]); + if (nav->surface_data_faces[f].edge_offset_type[i] == EDGE_OFFSET_LENGTH) + GL_DrawLine(points[0], 2, color_magenta, 3.0, false); // Edge was offset + //else + // GL_DrawLine(points[0], 2, color_red, 3.0, false); // Edge is centered + } + } + if (0) // DEBUG: Show ledge start -> end + { + vec3_t points[2]; + for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + if (0) + { + if (nav->surface_data_faces[f].ledge->is_ledge) + { + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].startpos, points[0]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].endpos, points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); // Edge was offset + } + } + + if (0) + { + if (nav->surface_data_faces[f].ledge[i / 2].hit_side == 1) // none = 0, left = 1, right = 2) + { + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].left_start, points[0]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].left_end, points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); // Edge was offset + } + + if (nav->surface_data_faces[f].ledge[i / 2].hit_side == 2) // none = 0, left = 1, right = 2) + { + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].right_start, points[0]); + VectorCopy(nav->surface_data_faces[f].ledge[i / 2].right_end, points[1]); + GL_DrawLine(points[0], 2, color_green, 3.0, false); // Edge was offset + } + } + } + } + + + } // End feet trace + + + // Camera trace + if (0 && ladder) // Found ladder + if (UTIL_CompareNormal(nav->surface_data_faces[f].normal, tr_cam.plane.normal, 0.05)) // Same normals + { + if (UTIL_CompareVertPlanes(nav->surface_data_faces[f].verts[0], tr_cam.endpos, tr_cam.plane.normal, 0.01)) // Same planes + { + if (AAS_InsideFace(&nav->surface_data_faces[f], tr_cam.endpos, 0.01, tr.fraction)) // Point inside face + { + //float volume = AAS_FaceArea(&nav->surface_data_faces[f]); // Surface area + //if (volume) + { + if (0) // DEBUG: Show edges + { + if (nav->surface_data_faces[f].contents & CONTENTS_LADDER) + { + Com_Printf("%s [LADDER] face:%d verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + GL_DrawLine(nav->surface_data_faces[f].verts[0], nav->surface_data_faces[f].num_verts, all_colors, 3.0, false); + } + else + { + Com_Printf("%s face:%d verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + //for (int i = 0; i < nav->surface_data_faces[f].num_verts; i += 2) + { + //int edge = i / 2; // edge num + //Com_Printf("%s face:%d edge:%d [%f %f %f] -- [%f %f %f]\n", __func__, f, edge, nav->surface_data_faces[f].verts[i][0], nav->surface_data_faces[f].verts[i][1], nav->surface_data_faces[f].verts[i][2], nav->surface_data_faces[f].verts[i + 1][0], nav->surface_data_faces[f].verts[i + 1][1], nav->surface_data_faces[f].verts[i + 1][2]); + } + GL_DrawLine(nav->surface_data_faces[f].verts[0], nav->surface_data_faces[f].num_verts, all_colors, 3.0, false); + } + } + if (1) // DEBUG: Show aligned edges + { + if (nav->surface_data_faces[f].contents & CONTENTS_LADDER) + { + Com_Printf("%s [LADDER] face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + GL_DrawLine(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, all_colors, 7.0, false); + } + else + { + Com_Printf("%s face:%d nl_verts:%d volume:%f normal[%f %f %f] texture:%s\n", __func__, f, nav->surface_data_faces[f].num_aligned_verts, nav->surface_data_faces[f].volume, nav->surface_data_faces[f].normal[0], nav->surface_data_faces[f].normal[1], nav->surface_data_faces[f].normal[2], nav->surface_data_faces[f].texture); + for (int i = 0; i < nav->surface_data_faces[f].num_aligned_verts; i += 2) + { + //int edge = i / 2; // edge num + //Com_Printf("%s face:%d edge:%d [%f %f %f] -- [%f %f %f]\n", __func__, f, edge, nav->surface_data_faces[f].aligned_verts[i][0], nav->surface_data_faces[f].aligned_verts[i][1], nav->surface_data_faces[f].aligned_verts[i][2], nav->surface_data_faces[f].aligned_verts[i + 1][0], nav->surface_data_faces[f].aligned_verts[i + 1][1], nav->surface_data_faces[f].aligned_verts[i + 1][2]); + } + GL_DrawLine(nav->surface_data_faces[f].aligned_verts[0], nav->surface_data_faces[f].num_aligned_verts, all_colors, 3.0, false); + } + } + if (1) // DEBUG: Show face normals (center_poly -> center_poly_32_units) + { + vec3_t points[2]; + VectorCopy(nav->surface_data_faces[f].center_poly, points[0]); + VectorCopy(nav->surface_data_faces[f].center_poly_32_units, points[1]); + GL_DrawLine(points[0], 2, color_red, 3.0, false); + } + } + } + } + } // End feet trace + } +} + +static void GL_DrawEdges(void) +{ + BOTLIB_DrawFaces(); +} +#endif +//rekkie -- gl_showedges -- e + //rekkie -- debug drawing -- s #if DEBUG_DRAWING #include "../server/server.h" From 9109a5af62d179769ea2b6b12ef9d28df83a3321 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 26 Jul 2024 13:08:39 +0300 Subject: [PATCH 374/974] Simplify tga_decode_rle(). Drop useless outer for () loop. Replace inner for () loop with while (1). Also check for packet overrun condition. --- src/refresh/images.c | 85 +++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index ad5f46ebd..725100578 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -432,58 +432,63 @@ static int tga_decode_rle(sizebuf_t *s, uint32_t **row_pointers, uint32_t *out_row; uint32_t color; const byte *in; - int j, packet_header, packet_size; + int packet_header, packet_size; - for (row = 0; row < rows; row++) { - out_row = row_pointers[row]; + col = row = 0; + out_row = row_pointers[row]; - for (col = 0; col < cols;) { - packet_header = SZ_ReadByte(s); - packet_size = 1 + (packet_header & 0x7f); + while (1) { + packet_header = SZ_ReadByte(s); + packet_size = 1 + (packet_header & 0x7f); - if (packet_header & 0x80) { - in = SZ_ReadData(s, bpp); - if (!in) - return Q_ERR_UNEXPECTED_EOF; + if (packet_header & 0x80) { + in = SZ_ReadData(s, bpp); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + if (palette) + color = palette[*in]; + else + color = tga_unpack_pixel(in, bpp); + do { + *out_row++ = color; + packet_size--; + + if (++col == cols) { + col = 0; + if (++row == rows) + goto done; + out_row = row_pointers[row]; + } + } while (packet_size); + } else { + in = SZ_ReadData(s, bpp * packet_size); + if (!in) + return Q_ERR_UNEXPECTED_EOF; + + do { if (palette) color = palette[*in]; else color = tga_unpack_pixel(in, bpp); - for (j = 0; j < packet_size; j++) { - *out_row++ = color; - - if (++col == cols) { - col = 0; - if (++row == rows) - return Q_ERR_SUCCESS; - out_row = row_pointers[row]; - } - } - } else { - in = SZ_ReadData(s, bpp * packet_size); - if (!in) - return Q_ERR_UNEXPECTED_EOF; - - for (j = 0; j < packet_size; j++) { - if (palette) - color = palette[*in]; - else - color = tga_unpack_pixel(in, bpp); - *out_row++ = color; - in += bpp; - - if (++col == cols) { - col = 0; - if (++row == rows) - return Q_ERR_SUCCESS; - out_row = row_pointers[row]; - } + *out_row++ = color; + in += bpp; + packet_size--; + + if (++col == cols) { + col = 0; + if (++row == rows) + goto done; + out_row = row_pointers[row]; } - } + } while (packet_size); } } +done: + if (packet_size) + return Q_ERR_OVERRUN; + return Q_ERR_SUCCESS; } From 2b76657a1cf4a291f0d8993d916f39ffbbc0488d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 26 Jul 2024 14:24:59 +0300 Subject: [PATCH 375/974] Optimize tga_decode_raw() a bit. --- src/refresh/images.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 725100578..36fdea447 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -412,12 +412,12 @@ static int tga_decode_raw(sizebuf_t *s, uint32_t **row_pointers, for (row = 0; row < rows; row++) { out_row = row_pointers[row]; - for (col = 0; col < cols; col++) { - if (palette) + if (palette) { + for (col = 0; col < cols; col++, in++) *out_row++ = palette[*in]; - else + } else { + for (col = 0; col < cols; col++, in += bpp) *out_row++ = tga_unpack_pixel(in, bpp); - in += bpp; } } From 8a4dfeecf9df41e1e095b8bc2175cafab0bcc2e3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 09:13:13 -0400 Subject: [PATCH 376/974] Added Espionage structs --- src/action/botlib/botlib.h | 23 ++++++++ src/action/botlib/botlib_movement.c | 82 +++++++++++++++++------------ 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 66702fa54..46f895086 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -169,6 +169,29 @@ typedef struct ctf_status_s } ctf_status_t; extern ctf_status_t bot_ctf_status; +typedef struct esp_status_s +{ + edict_t* esp_target; // ETV: target edict + int esp_target_node; // ETV: the closest node where the target resides + qboolean esp_target_captured; // ETV: if we've already captured the target this round + float esp_leader_dist_to_target; // ETV: how close team1 leader is from the target + int esp_team1_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) + int esp_team2_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) + int esp_team3_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) + +} esp_status_t; +extern esp_status_t bot_esp_status; + +// Get flag, retrieve flag, intercept flag carrier, etc. +typedef enum +{ + BOT_ESP_STATE_NONE, // No / Normal state (both teams actively attacking) + BOT_ESP_COVER_TEAM_LEADER, // Cover the team leader + BOT_ESP_ATTACK_TARGET, // Attack the target (enemy leader or target area) + BOT_ESP_DEFEND_TARGET, // Defend the target (team leader or target area) + BOT_ESP_RETREAT, // Retreat from the target (enemy leader or target area) +} bot_esp_state_t; + // Get flag, retrieve flag, intercept flag carrier, etc. typedef enum { diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 9bd86ff48..08021630f 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -6437,6 +6437,21 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->bot.bi.speed = move_speed; // Set our suggested speed } + // Water! + /// Borrowed from P_WorldEffects + int waterlevel = self->waterlevel; + int old_waterlevel = self->client->old_waterlevel; + self->client->old_waterlevel = waterlevel; + + if (current_node_type == NODE_WATER || waterlevel == 3){ + gi.dprintf("I'm in the water %s and my air is %d\n", self->client->pers.netname, self->air_finished_framenum - level.framenum); + // Get out before you start drowning! + if (self->air_finished_framenum < level.framenum + 10) { + self->bot.bi.actionflags |= ACTION_MOVEUP; + self->bot.node_travel_time = 0; + } + } + // move, crouch, or jump if (next_node_type == NODE_WATER) { @@ -6444,7 +6459,6 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->bot.bi.actionflags &= ~ACTION_MOVEDOWN; self->bot.bi.actionflags &= ~ACTION_CROUCH; - gi.dprintf("I'm in the water %s and my air is %d\n", self->client->pers.netname, self->air_finished_framenum - level.framenum); //VectorClear(self->bot.bi.dir); //self->bot.bi.speed = 0; @@ -6473,7 +6487,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) else if ((contents_feet & MASK_WATER) && nodes[self->bot.next_node].origin[2] > self->s.origin[2]) // Move up { //self->bot.bi.actionflags |= ACTION_MOVEUP; - // darksaint: changed this to MOVEUP and MOVEFORWARD simultanously to get out of water + // darksaint: changed this to MOVEUP and MOVEFORWARD simultanously to get out of water? self->bot.bi.actionflags |= (ACTION_MOVEUP | ACTION_MOVEFORWARD); //Com_Printf("%s %s [%d] water: move up\n", __func__, self->client->pers.netname, level.framenum); } @@ -6524,39 +6538,37 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (next_node_type == NODE_MOVE) { - /* - if (BOTLIB_CanMoveDir(self, walkdir) == false) - { - // We can't move in this direction - Com_Printf("%s %s can't move safely in direction\n", __func__, self->client->pers.netname); - self->bot.stuck_wander_time = 15; - self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving - return; - } - */ - /* - float fwd_distance = 32; - tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope - fwd_distance = 128; // Extend the distance we check for a safe direction to move toward - - // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge - if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false) == false); - { - if (level.framenum > self->bot.stuck_last_negate) - { - // Only allow this to happen once every 60 seconds - // It's reset after 60 seconds, death, or touching ground again - self->bot.stuck_last_negate = level.framenum + 60 * HZ; - - Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); - - // Reverse the direction - VectorNegate(self->velocity, self->velocity); - VectorClear(self->bot.bi.dir); - } - } - */ + + // if (BOTLIB_CanMoveDir(self, walkdir) == false) + // { + // // We can't move in this direction + // Com_Printf("%s %s can't move safely in direction\n", __func__, self->client->pers.netname); + // self->bot.stuck_wander_time = 15; + // self->bot.bi.actionflags |= ACTION_HOLDPOS; // Stop moving + // return; + // } + + // float fwd_distance = 32; + // tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + // if (tr.plane.normal[2] < 0.99) // If on a slope that makes the player 'bounce' when moving down the slope + // fwd_distance = 128; // Extend the distance we check for a safe direction to move toward + + // // Prevent bot from falling off a ledge by reversing its velocity, pulling it away from the ledge + // if (BOTLIB_CanMoveInDirection(self, walkdir, fwd_distance, NODE_MAX_CROUCH_FALL_HEIGHT, false) == false) + // { + // if (level.framenum > self->bot.stuck_last_negate) + // { + // // Only allow this to happen once every 60 seconds + // // It's reset after 60 seconds, death, or touching ground again + // self->bot.stuck_last_negate = level.framenum + 60 * HZ; + + // Com_Printf("%s %s stuck_last_negate N[%d] L[%d] \n", __func__, self->client->pers.netname, self->bot.stuck_last_negate, level.framenum); + + // // Reverse the direction + // VectorNegate(self->velocity, self->velocity); + // VectorClear(self->bot.bi.dir); + // } + // } } //if (next_node_type == NODE_JUMPPAD || next_node_type == NODE_JUMP) From 9e100735426b0dd991b4658f4f866055f1f67b4b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 13:25:45 -0400 Subject: [PATCH 377/974] Retrying random nodes, dump_bot info for status command draft --- inc/shared/list.h | 5 +++ src/action/acesrc/acebot_ai.c | 34 +++++++++++++---- src/action/botlib/botlib.h | 20 +++++----- src/action/botlib/botlib_ai.c | 4 +- src/action/botlib/botlib_movement.c | 11 +++--- src/action/botlib/botlib_nav.c | 58 ++++++++++++++--------------- src/server/commands.c | 53 ++++++++++++++++++++++++++ src/server/main.c | 2 + src/server/server.h | 6 +++ 9 files changed, 141 insertions(+), 52 deletions(-) diff --git a/inc/shared/list.h b/inc/shared/list.h index 7664529a8..8e46f3a63 100644 --- a/inc/shared/list.h +++ b/inc/shared/list.h @@ -121,6 +121,11 @@ static inline void List_Remove(list_t *elem) !LIST_TERM(cursor, list, member); \ cursor = LIST_NEXT(type, cursor, member)) +#define LIST_FOR_EACH_BOT_CLIENT(var, list) \ + for (bot_client_t *var = (bot_client_t *)((char *)(list)->next - offsetof(bot_client_t, next)); \ + (void *)&var->next != (void *)(list); \ + var = (bot_client_t *)((char *)var->next - offsetof(bot_client_t, next))) + #define LIST_FOR_EACH_SAFE(type, cursor, next, list, member) \ for (cursor = LIST_FIRST(type, list, member); \ next = LIST_NEXT(type, cursor, member), \ diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index e4d6e15a1..c1b4b905a 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -71,6 +71,24 @@ void ACEAI_Think(edict_t* self) //Com_Printf("%s\n", __func__); } // hehe I think not +qboolean BOTLIB_ChooseRandomNode(edict_t* self, int iter) +{ + // Random node + for (int i = 0; i < iter; i++) + { + int n = rand() % numnodes; // pick a random node + //Com_Printf("%s %s RNG Node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, rand() % 2)) + { + Com_Printf("%s %s visiting [RNG] node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); + return true; + } else { + Com_Printf("%s %s visiting [RNG] node[%i] failed\n", __func__, self->client->pers.netname, nodes[n].nodenum); + return false; + } + } + return false; +} void BOTLIB_PickLongRangeGoal(edict_t* self) { @@ -81,6 +99,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) edict_t* goal_ent = NULL; float cost = INVALID; int counter = 0; + int max_random_retries = 10; // Clear old Node -> Node movement types //self->prev_to_curr_node_type = INVALID; @@ -354,16 +373,15 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } // Random node - for (int i = 0; i < 128; i++) - { - int n = rand() % numnodes; // pick a random node - //Com_Printf("%s %s RNG Node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, rand() % 2)) + int retries = 0; + while (retries < max_random_retries) { - //Com_Printf("%s %s visiting [RNG] node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); - return; + if (BOTLIB_ChooseRandomNode(self, 128)) + { + return true; + } + retries++; } - } } Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 46f895086..d5bd76bd2 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -9,14 +9,17 @@ #define BOT_NAV_VERSION_MAX BOT_NAV_VERSION_2 // Bot move states -#define BOT_MOVE_STATE_NONE 0 // Bot is booting up :-) -#define BOT_MOVE_STATE_NAV 1 // Getting a navigational path -//#define BOT_MOVE_STATE_NAV_NEXT 2 // Getting next navigational path (if any) -#define BOT_MOVE_STATE_MOVE 3 // Standard movement -#define BOT_MOVE_STATE_WANDER 4 // No navigation and no movement, try wandering around -#define BOT_MOVE_STATE_STAND 5 // Stand still and hold a position -#define BOT_MOVE_STATE_FLEE 6 // Running away from enemy force -#define BOT_MOVE_STATE_COVER 7 // Under fire, take cover +typedef enum +{ + BOT_MOVE_STATE_NONE = 0, // Bot is booting up :-) + BOT_MOVE_STATE_NAV, // Getting a navigational path + //BOT_MOVE_STATE_NAV_NEXT, // Getting next navigational path (if any) + BOT_MOVE_STATE_MOVE, // Standard movement + BOT_MOVE_STATE_WANDER, // No navigation and no movement, try wandering around + BOT_MOVE_STATE_STAND, // Stand still and hold a position + BOT_MOVE_STATE_FLEE, // Running away from enemy force + BOT_MOVE_STATE_COVER // Under fire, take cover +} bot_move_state_t; typedef struct bot_connections_s @@ -178,7 +181,6 @@ typedef struct esp_status_s int esp_team1_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) int esp_team2_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) int esp_team3_leader_area; // ATL: Approximate area where team1 leader is (helps bots find the bad guy) - } esp_status_t; extern esp_status_t bot_esp_status; diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 9a40fca7b..388229cbb 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -701,7 +701,9 @@ void BOTLIB_Think(edict_t* self) goto end_think; // Skip bot logic } - if (level.framenum < 75) // Wait for a little before processing AI on a new map + // darksaint: reduced this to 15 because bots aren't moving quickly enough on spawn and telefrags are happening + // Original value was 75 + if (level.framenum < 15) // Wait for a little before processing AI on a new map goto end_think; // Skip bot logic if (ctf->value) // CTF Goals diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 08021630f..70b04835a 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5863,10 +5863,11 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) } } #endif - + + // This only applies for teamplay + // Do not follow path when teammates are still inside us. - if (OnTransparentList(self)) // Teamplay - { + if (OnTransparentList(self)) { // Teamplay // If the bot has just spawned, then we need to wait a bit before we start moving. if (self->just_spawned) // set by SpawnPlayers() in a_team.c { @@ -5885,7 +5886,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) else if (rnd_rng == 1) self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Medium wait else if (rnd_rng == 2) - self->just_spawned_timeout = level.framenum + (random() * 1) * HZ; // Short wait + self->just_spawned_timeout = level.framenum + (random() * HZ); // Short wait else self->just_spawned_timeout = 0; // No wait } @@ -5902,7 +5903,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Go! if (self->just_spawned_go || self->bot.see_enemies) { - //BOTLIB_PickLongRangeGoal(self); + BOTLIB_PickLongRangeGoal(self); self->just_spawned_go = false; // Now we can move! } } diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index d851b29ec..e678b5e6c 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -799,32 +799,32 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz self->bot.node_list_count = 0; // New pathing - if area nodes are supported - // if (nav_area.total_areas > 0 && goal_node) - // { - // Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); - // if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path - // { - // while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path - // { - // } - - // // Add the goal to the end of the node list + terminate - // if (self->bot.node_list_count) - // { - // self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node - // self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate - // } - - // return true; // Success - // } - // else - // { - // Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); - // return false; // Failure - // } - // } - // else // Fallback to old pathing - // { + if (nav_area.total_areas > 0 && goal_node) + { + Com_Printf("%s %s goal_node[%i]\n", __func__, self->client->pers.netname, goal_node); + if (BOTLIB_CanVisitAreaNode(self, goal_node)) // Get area-to-area (low resolution) path + { + while (BOTLIB_GetNextAreaNode(self)) // Get node-to-node (full resolution) path using area-to-area path + { + } + + // Add the goal to the end of the node list + terminate + if (self->bot.node_list_count) + { + self->bot.node_list[self->bot.node_list_count++] = self->bot.goal_node; // Save goal node + self->bot.node_list[self->bot.node_list_count] = INVALID; // Terminate + } + + return true; // Success + } + else + { + Com_Printf("%s %s failed #1 \n", __func__, self->client->pers.netname); + return false; // Failure + } + } + else // Fallback to old pathing + { self->bot.goal_node = INVALID; //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) @@ -838,9 +838,9 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz return true; // Success } - //} + } - Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); + //Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); return false; // Failure to find path } @@ -1942,7 +1942,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando // Sanity check if (from == INVALID || to == INVALID) { - gi.dprintf("both from and to nodes were invalid\n"); + gi.dprintf("both from and to nodes were invalid, to: %i, from: %i\n", to, from); return false; } diff --git a/src/server/commands.c b/src/server/commands.c index 057b9c561..947fd04b1 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -541,6 +541,53 @@ static void dump_clients(void) } } +list_t sv_botclientlist; + +static void dump_bot_clients(void) +{ + bot_client_t *bot_client; + + Com_Printf( + "num score ping name lastmsg address rate pr fps\n" + "--- ----- ---- --------------- ------- --------------------- ----- -- ---\n"); + + LIST_FOR_EACH_BOT_CLIENT(bot_client, &sv_botclientlist) { + + Com_Printf("%3i %5i ", bot_client->number, bot_client->score); + Com_Printf("%4i ", bot_client->ping); + Com_Printf("%-15.15s ", bot_client->name); + Com_Printf("N/A "); // Bots don't have lastmsg, address, rate, protocol, or fps + Com_Printf("N/A "); + Com_Printf("N/A "); + Com_Printf("N/A "); + Com_Printf("N/A "); + Com_Printf("\n"); + } +} + +// static void dump_bot_clients(void) +// { +// bot_client_t *client; + +// Com_Printf( +// "num score ping name lastmsg address rate pr fps\n" +// "--- ----- ---- --------------- ------- --------------------- ----- -- ---\n"); +// FOR_EACH_CLIENT(client) { +// Com_Printf("%3i %5i ", client->number, +// client->score); + +// Com_Printf("BOT "); +// Com_Printf("%4i ", client->ping < 9999 ? client->ping : 9999); +// Com_Printf("%-15.15s ", client->name); +// Com_Printf("%7u ", 0); +// Com_Printf("%-21s ", "localhost"); +// Com_Printf("%5i ", 0); +// Com_Printf("%2i ", 0); +// Com_Printf("%3i ", 0); +// Com_Printf("\n"); +// } +// } + static void dump_versions(void) { client_t *client; @@ -702,6 +749,12 @@ static void SV_Status_f(void) } Com_Printf("\n"); + // darksaint: revisit someday, get bots popuate in 'status' command + // if (!LIST_EMPTY(&sv_botclientlist)) { + // dump_bot_clients(); + // } + // Com_Printf("\n"); + SV_MvdStatus_f(); } diff --git a/src/server/main.c b/src/server/main.c index a190ed488..44649543e 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1147,6 +1147,7 @@ void SV_BotInit(void) bot_clients[i].name[0] = 0; bot_clients[i].ping = 0; bot_clients[i].score = 0; + bot_clients[i].number = i; } } // Game DLL updates Server of bot info @@ -1176,6 +1177,7 @@ void SV_BotConnect(char* name) Q_snprintf(bot_clients[i].name, sizeof(bot_clients[i].name), "%s", name); bot_clients[i].ping = 0; bot_clients[i].score = 0; + bot_clients[i].number = i; Com_Printf("%s Server added %s as a fake client\n", __func__, bot_clients[i].name); break; } diff --git a/src/server/server.h b/src/server/server.h index 12d8a2700..9cd3fa046 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -255,6 +255,9 @@ typedef struct { #define FOR_EACH_CLIENT(client) \ LIST_FOR_EACH(client_t, client, &sv_clientlist, entry) +#define FOR_EACH_BOT_CLIENT(bot_client) \ + LIST_FOR_EACH_BOT_CLIENT(bot_client_t, bot_client, &sv_botclientlist) + #define CLIENT_ACTIVE(cl) \ ((cl)->state == cs_spawned && !(cl)->download && !(cl)->nodata) @@ -898,5 +901,8 @@ typedef struct bot_client_s { char name[16]; int ping; short score; + int number; + list_t *next; // Assuming this is the field used for list linkage + } bot_client_t; //rekkie -- Fake Bot Client -- e \ No newline at end of file From 075155f1ea95145d71a20ffe9847f6cb0d5442ac Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 13:51:17 -0400 Subject: [PATCH 378/974] Moved bot_client_t to server.h, bots now display on 'status' command --- inc/shared/list.h | 5 ---- src/server/commands.c | 64 ++++++++++++++----------------------------- src/server/main.c | 4 +-- src/server/server.h | 8 ++---- 4 files changed, 24 insertions(+), 57 deletions(-) diff --git a/inc/shared/list.h b/inc/shared/list.h index 8e46f3a63..7664529a8 100644 --- a/inc/shared/list.h +++ b/inc/shared/list.h @@ -121,11 +121,6 @@ static inline void List_Remove(list_t *elem) !LIST_TERM(cursor, list, member); \ cursor = LIST_NEXT(type, cursor, member)) -#define LIST_FOR_EACH_BOT_CLIENT(var, list) \ - for (bot_client_t *var = (bot_client_t *)((char *)(list)->next - offsetof(bot_client_t, next)); \ - (void *)&var->next != (void *)(list); \ - var = (bot_client_t *)((char *)var->next - offsetof(bot_client_t, next))) - #define LIST_FOR_EACH_SAFE(type, cursor, next, list, member) \ for (cursor = LIST_FIRST(type, list, member); \ next = LIST_NEXT(type, cursor, member), \ diff --git a/src/server/commands.c b/src/server/commands.c index 947fd04b1..eedfabcde 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -541,53 +541,30 @@ static void dump_clients(void) } } -list_t sv_botclientlist; static void dump_bot_clients(void) { - bot_client_t *bot_client; + // Display nothing if no bots + if (!bot_clients[0].in_use) { + return; + } Com_Printf( - "num score ping name lastmsg address rate pr fps\n" - "--- ----- ---- --------------- ------- --------------------- ----- -- ---\n"); - - LIST_FOR_EACH_BOT_CLIENT(bot_client, &sv_botclientlist) { - - Com_Printf("%3i %5i ", bot_client->number, bot_client->score); - Com_Printf("%4i ", bot_client->ping); - Com_Printf("%-15.15s ", bot_client->name); - Com_Printf("N/A "); // Bots don't have lastmsg, address, rate, protocol, or fps - Com_Printf("N/A "); - Com_Printf("N/A "); - Com_Printf("N/A "); - Com_Printf("N/A "); - Com_Printf("\n"); + "\n" + "Bot Clients.\n" + "num score ping name\n" + "--- ----- ---- ---------------\n"); + + for (int i = 0; i < 32; i++) { + if(bot_clients[i].in_use) { + Com_Printf("%3i %5i ", bot_clients[i].number, bot_clients[i].score); + Com_Printf("%4i ", bot_clients[i].ping); + Com_Printf("%-15.15s ", bot_clients[i].name); + Com_Printf("\n"); + } } } -// static void dump_bot_clients(void) -// { -// bot_client_t *client; - -// Com_Printf( -// "num score ping name lastmsg address rate pr fps\n" -// "--- ----- ---- --------------- ------- --------------------- ----- -- ---\n"); -// FOR_EACH_CLIENT(client) { -// Com_Printf("%3i %5i ", client->number, -// client->score); - -// Com_Printf("BOT "); -// Com_Printf("%4i ", client->ping < 9999 ? client->ping : 9999); -// Com_Printf("%-15.15s ", client->name); -// Com_Printf("%7u ", 0); -// Com_Printf("%-21s ", "localhost"); -// Com_Printf("%5i ", 0); -// Com_Printf("%2i ", 0); -// Com_Printf("%3i ", 0); -// Com_Printf("\n"); -// } -// } - static void dump_versions(void) { client_t *client; @@ -749,11 +726,10 @@ static void SV_Status_f(void) } Com_Printf("\n"); - // darksaint: revisit someday, get bots popuate in 'status' command - // if (!LIST_EMPTY(&sv_botclientlist)) { - // dump_bot_clients(); - // } - // Com_Printf("\n"); + #ifndef NO_BOTS + dump_bot_clients(); + Com_Printf("\n"); + #endif SV_MvdStatus_f(); } diff --git a/src/server/main.c b/src/server/main.c index 44649543e..3f30909eb 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -19,10 +19,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" #include "client/input.h" -pmoveParams_t sv_pmp; - bot_client_t bot_clients[MAX_CLIENTS]; +pmoveParams_t sv_pmp; + master_t sv_masters[MAX_MASTERS]; // address of group servers LIST_DECL(sv_banlist); diff --git a/src/server/server.h b/src/server/server.h index 9cd3fa046..456119a3e 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -255,9 +255,6 @@ typedef struct { #define FOR_EACH_CLIENT(client) \ LIST_FOR_EACH(client_t, client, &sv_clientlist, entry) -#define FOR_EACH_BOT_CLIENT(bot_client) \ - LIST_FOR_EACH_BOT_CLIENT(bot_client_t, bot_client, &sv_botclientlist) - #define CLIENT_ACTIVE(cl) \ ((cl)->state == cs_spawned && !(cl)->download && !(cl)->nodata) @@ -589,7 +586,6 @@ extern cvar_t *sv_ghostime; extern client_t *sv_client; extern edict_t *sv_player; - //=========================================================== // @@ -902,7 +898,7 @@ typedef struct bot_client_s { int ping; short score; int number; - list_t *next; // Assuming this is the field used for list linkage - } bot_client_t; +extern bot_client_t bot_clients[MAX_CLIENTS]; + //rekkie -- Fake Bot Client -- e \ No newline at end of file From ccb56a9c62c364f0acfe040af418b252b2a82f7c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 14:19:52 -0400 Subject: [PATCH 379/974] Set iter to MAX_CLIENTS --- src/action/acesrc/acebot_spawn.c | 3 +++ src/action/botlib/botlib_spawn.c | 7 ++++++- src/server/commands.c | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index dcfb15c9f..47ef32b73 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -638,6 +638,7 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + game.bot_count++; //rekkie -- Fake Bot Client -- e //bot->wander_timeout = level.framenum + 60.0 * HZ; @@ -704,6 +705,7 @@ void ACESP_RemoveBot(char *name) // bot->inuse = false; freed = true; ClientDisconnect( bot ); + game.bot_count--; // ACEIT_PlayerRemoved (bot); // gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); if( ! remove_all ) @@ -760,6 +762,7 @@ void ACESP_RemoveTeamplayBot(int team) //rekkie -- Fake Bot Client -- s gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e + game.bot_count--; if (bot->health) player_die(bot, bot, bot, 100000, vec3_origin); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 7b86e1b22..afa3ddc69 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1424,6 +1424,7 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + game.bot_count++; //rekkie -- Fake Bot Client -- e return bot; @@ -1464,6 +1465,7 @@ void BOTLIB_RemoveBot(char* name) //bot->inuse = false; freed = true; ClientDisconnect(bot); + game.bot_count--; if (bot_chat->value && !remove_all) BOTLIB_Chat(bot, CHAT_GOODBYE); //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); @@ -1517,11 +1519,12 @@ void BOTLIB_RemoveBot(char* name) //bot->inuse = false; freed = true; ClientDisconnect(bot); + game.bot_count--; //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); break; } if (bot->is_bot) - bot_count++; + bot_count--; } } @@ -1560,6 +1563,8 @@ void BOTLIB_RemoveTeamplayBot(int team) else if (team == TEAM3) bot_connections.total_team3--; + game.bot_count--; + if (bot->health) player_die(bot, bot, bot, 100000, vec3_origin); ClientDisconnect(bot); diff --git a/src/server/commands.c b/src/server/commands.c index eedfabcde..6963c8788 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -555,7 +555,7 @@ static void dump_bot_clients(void) "num score ping name\n" "--- ----- ---- ---------------\n"); - for (int i = 0; i < 32; i++) { + for (int i = 0; i < MAX_CLIENTS; i++) { if(bot_clients[i].in_use) { Com_Printf("%3i %5i ", bot_clients[i].number, bot_clients[i].score); Com_Printf("%4i ", bot_clients[i].ping); From a6208c42eaa288ee7cb2d0e8c6a97c05f945bfd7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 14:37:01 -0400 Subject: [PATCH 380/974] Commented some debug statements --- src/action/acesrc/acebot_ai.c | 4 ++-- src/action/acesrc/acebot_nodes.c | 2 +- src/action/botlib/botlib_movement.c | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index c1b4b905a..ae45c0714 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -80,10 +80,10 @@ qboolean BOTLIB_ChooseRandomNode(edict_t* self, int iter) //Com_Printf("%s %s RNG Node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, rand() % 2)) { - Com_Printf("%s %s visiting [RNG] node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); + //Com_Printf("%s %s visiting [RNG] node[%i]\n", __func__, self->client->pers.netname, nodes[n].nodenum); return true; } else { - Com_Printf("%s %s visiting [RNG] node[%i] failed\n", __func__, self->client->pers.netname, nodes[n].nodenum); + //Com_Printf("%s %s visiting [RNG] node[%i] failed\n", __func__, self->client->pers.netname, nodes[n].nodenum); return false; } } diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 98c462efa..c1352aff0 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -2650,7 +2650,7 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.bi.speed = 100; //self->bot.bi.actionflags = 0; - Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + //Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); self->bot.node_travel_time = 0; ////if (nav_area.total_areas > 0) diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 70b04835a..89e0fb335 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5911,7 +5911,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->groundentity && self->bot.next_node == INVALID) // No next node, pick new nav { - Com_Printf("%s %s next_node is invalid; find a new path\n", __func__, self->client->pers.netname); + //Com_Printf("%s %s next_node is invalid; find a new path\n", __func__, self->client->pers.netname); self->bot.bi.speed = 0; self->bot.state = BOT_MOVE_STATE_NAV; return; @@ -5925,13 +5925,13 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) //self->bot.bi.speed = 0; //return; - Com_Printf("%s %s on ground and current_node (%i) is invalid; try to wander\n", __func__, self->client->pers.netname, self->bot.current_node); + //Com_Printf("%s %s on ground and current_node (%i) is invalid; try to wander\n", __func__, self->client->pers.netname, self->bot.current_node); self->bot.bi.speed = 0; self->bot.stuck_wander_time = 1; } if (self->groundentity && self->bot.goal_node == INVALID)// && self->bot.node_travel_time >= 15) { - Com_Printf("%s %s on ground and goal_node (%i) is invalid; find a new path\n", __func__, self->client->pers.netname, self->bot.goal_node); + //Com_Printf("%s %s on ground and goal_node (%i) is invalid; find a new path\n", __func__, self->client->pers.netname, self->bot.goal_node); self->bot.bi.speed = 0; self->bot.state = BOT_MOVE_STATE_NAV; return; @@ -5976,7 +5976,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node { self->bot.stuck_wander_time = 1; - Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + //Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); } //self->bot.stuck_wander_time = 0; @@ -5993,7 +5993,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Wander //if (nav_area.total_areas <= 0) { - Com_Printf("%s %s stuck_wander\n", __func__, self->client->pers.netname); + //Com_Printf("%s %s stuck_wander\n", __func__, self->client->pers.netname); self->bot.state = BOT_MOVE_STATE_NAV; self->bot.goal_node = INVALID; } From 26918dff1578659221285dcb7bc2750240df6b0c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 14:41:27 -0400 Subject: [PATCH 381/974] Added workflow_dispatch --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 374987c53..95ac4c791 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: [master, aqtion, aqtion-alpha, ci] pull_request: branches: [master, aqtion, aqtion-alpha] + workflow_dispatch: env: MESON_ARGS: >- From 66e5bf80f19e309b4c2d8e0e4d92d763ce4c5383 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 14:47:03 -0400 Subject: [PATCH 382/974] ifdef condition fix? --- src/action/acesrc/acebot_spawn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 47ef32b73..7b2f884f9 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -1043,7 +1043,7 @@ void DC_LoadRandomBotName(char *userinfo) int curr_len; // Current length of the line char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) curr_line[0] = '\0'; -#ifdef WIN32 +#ifdef _WIN32 int i; // Keep track where we are in the filename array #endif From 63dee72be41976a69550034730e0234e02aca74c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 14:51:16 -0400 Subject: [PATCH 383/974] Removal of returning true in void function --- src/action/acesrc/acebot_ai.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index ae45c0714..825d29cb4 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -376,11 +376,8 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) int retries = 0; while (retries < max_random_retries) { - if (BOTLIB_ChooseRandomNode(self, 128)) - { - return true; - } - retries++; + if (!BOTLIB_ChooseRandomNode(self, 128)) + retries++; } } From 3b9542ce6a20c3ac083072d1d90935e4f2bb8d6e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 26 Jul 2024 21:57:58 +0300 Subject: [PATCH 384/974] Update libcurl to 8.9.0. --- subprojects/libcurl.wrap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index 36f5a3d0e..4de7bbc10 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,11 @@ [wrap-file] -directory = curl-8.8.0 -source_url = https://curl.se/download/curl-8.8.0.tar.xz -source_filename = curl-8.8.0.tar.xz -source_hash = 0f58bb95fc330c8a46eeb3df5701b0d90c9d9bfcc42bd1cd08791d12551d4400 -patch_url = https://skuller.net/meson/libcurl_8.8.0-1_patch.zip -patch_filename = libcurl_8.8.0-1_patch.zip -patch_hash = 07a6459d786c62ca248811889157ee537262e7248efef26beb2dc765b20ea07c +directory = curl-8.9.0 +source_url = https://curl.se/download/curl-8.9.0.tar.xz +source_filename = curl-8.9.0.tar.xz +source_hash = ff09b2791ca56d25fd5c3f3a4927dce7c8a9dc4182200c487ca889fba1fdd412 +patch_url = https://skuller.net/meson/libcurl_8.9.0-1_patch.zip +patch_filename = libcurl_8.9.0-1_patch.zip +patch_hash = 584a4887b7940f806a48df6d5ef8af4518da374b0e6327298fb53d180002e4c7 [provide] libcurl = libcurl_dep From 203c3fd3f848c3b9ebab701b5e3e00b2ebcbe814 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 15:00:59 -0400 Subject: [PATCH 385/974] Fixed a problem in BOTLIB_Look? --- src/action/botlib/botlib_movement.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 89e0fb335..648a69a73 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -4797,6 +4797,9 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) //float turn_speed = (MAX_BOTSKILL - self->bot.skill) * 3 / 9; // [Min:0 Max:10] skill "10" == 0, Skill "0" == 3 float turn_speed = 0.3; qboolean reached_look_at = false; + // Declarations moved outside the labeled block + const int look_ahead = 5; + qboolean found_viable_node = false; if (VectorEmpty(self->bot.bi.look_at) == false) { @@ -5131,11 +5134,7 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) // If no enemy, look at nodes in front/behind, or look at map center else // if (self->enemy == NULL) { - LookAhead: - - const int look_ahead = 5; - qboolean found_viable_node = false; for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) // Loop from current node until end of list { /* From 61e6c7bcf654ad2ee2058b23a27141d1a0b5a844 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 26 Jul 2024 16:58:10 -0400 Subject: [PATCH 386/974] One more debug statement commented --- src/action/acesrc/acebot_ai.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 825d29cb4..2f095954e 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -381,7 +381,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } } - Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); + //Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); self->bot.goal_node = INVALID; self->bot.state = BOT_MOVE_STATE_NAV; // Get new nav return; // no path? From d65aabeb281799adaaefc3c2481dc0b559bc47bf Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 15:56:04 +0300 Subject: [PATCH 387/974] Capitalize error messages. --- src/common/files.c | 50 +++++++-------- src/refresh/images.c | 36 +++++------ src/refresh/models.c | 144 +++++++++++++++++++++---------------------- 3 files changed, 115 insertions(+), 115 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index 2c23202c4..73b20e5ec 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -2097,43 +2097,43 @@ static pack_t *load_pak_file(const char *packfile) } if (!fread(&header, sizeof(header), 1, fp)) { - Com_SetLastError("reading header failed"); + Com_SetLastError("Reading header failed"); goto fail1; } if (LittleLong(header.ident) != IDPAKHEADER) { - Com_SetLastError("bad header ident"); + Com_SetLastError("Bad header ident"); goto fail1; } header.dirlen = LittleLong(header.dirlen); if (header.dirlen % sizeof(dpackfile_t)) { - Com_SetLastError("bad directory length"); + Com_SetLastError("Bad directory length"); goto fail1; } num_files = header.dirlen / sizeof(dpackfile_t); if (num_files < 1) { - Com_SetLastError("no files"); + Com_SetLastError("No files"); goto fail1; } if (num_files > MAX_FILES_IN_PACK) { - Com_SetLastError("too many files"); + Com_SetLastError("Too many files"); goto fail1; } header.dirofs = LittleLong(header.dirofs); if (header.dirofs > INT32_MAX) { - Com_SetLastError("bad directory offset"); + Com_SetLastError("Bad directory offset"); goto fail1; } if (os_fseek(fp, header.dirofs, SEEK_SET)) { - Com_SetLastError("seeking to directory failed"); + Com_SetLastError("Seeking to directory failed"); goto fail1; } info = FS_AllocTempMem(header.dirlen); if (!fread(info, header.dirlen, 1, fp)) { - Com_SetLastError("reading directory failed"); + Com_SetLastError("Reading directory failed"); goto fail2; } @@ -2142,7 +2142,7 @@ static pack_t *load_pak_file(const char *packfile) dfile->filepos = LittleLong(dfile->filepos); dfile->filelen = LittleLong(dfile->filelen); if (dfile->filelen > INT32_MAX || dfile->filepos > INT32_MAX - dfile->filelen) { - Com_SetLastError("file length or position too big"); + Com_SetLastError("File length or position too big"); goto fail2; } names_len += Q_strnlen(dfile->name, sizeof(dfile->name)) + 1; @@ -2306,13 +2306,13 @@ static bool get_file_info(const pack_t *pack, packfile_t *file, char *name, size *len = 0; if (!fread(header, sizeof(header), 1, pack->fp)) { - Com_SetLastError("reading central directory failed"); + Com_SetLastError("Reading central directory failed"); return false; } // check the magic if (RL32(&header[0]) != ZIP_CENTRALHEADERMAGIC) { - Com_SetLastError("bad central directory magic"); + Com_SetLastError("Bad central directory magic"); return false; } @@ -2334,7 +2334,7 @@ static bool get_file_info(const pack_t *pack, packfile_t *file, char *name, size file->filelen = file_len; file->filepos = file_pos; if (!fread(name, name_size, 1, pack->fp)) { - Com_SetLastError("reading central directory failed"); + Com_SetLastError("Reading central directory failed"); return false; } name[name_size] = 0; @@ -2342,11 +2342,11 @@ static bool get_file_info(const pack_t *pack, packfile_t *file, char *name, size if (file_pos == UINT32_MAX || file_len == UINT32_MAX || comp_len == UINT32_MAX) { if (!zip64) { - Com_SetLastError("file length or position too big"); + Com_SetLastError("File length or position too big"); return false; } if (!parse_extra_data(pack, file, xtra_size)) { - Com_SetLastError("parsing zip64 extra data failed"); + Com_SetLastError("Parsing zip64 extra data failed"); return false; } xtra_size = 0; @@ -2357,7 +2357,7 @@ static bool get_file_info(const pack_t *pack, packfile_t *file, char *name, size skip: if (os_fseek(pack->fp, name_size + xtra_size + comm_size, SEEK_CUR)) { - Com_SetLastError("seeking to central directory failed"); + Com_SetLastError("Seeking to central directory failed"); return false; } @@ -2385,7 +2385,7 @@ static pack_t *load_zip_file(const char *packfile) header_pos = search_central_header(fp); if (!header_pos) { - Com_SetLastError("no central header found"); + Com_SetLastError("No central header found"); goto fail2; } @@ -2398,11 +2398,11 @@ static pack_t *load_zip_file(const char *packfile) } if (os_fseek(fp, header_pos, SEEK_SET)) { - Com_SetLastError("seeking to central header failed"); + Com_SetLastError("Seeking to central header failed"); goto fail2; } if (!fread(header, header_size, 1, fp)) { - Com_SetLastError("reading central header failed"); + Com_SetLastError("Reading central header failed"); goto fail2; } @@ -2423,21 +2423,21 @@ static pack_t *load_zip_file(const char *packfile) } if (num_files_cd != num_files || num_disk_cd != 0 || num_disk != 0) { - Com_SetLastError("unsupported multi-part archive"); + Com_SetLastError("Unsupported multi-part archive"); goto fail2; } if (num_files_cd < 1) { - Com_SetLastError("no files"); + Com_SetLastError("No files"); goto fail2; } if (num_files_cd > ZIP_MAXFILES) { - Com_SetLastError("too many files"); + Com_SetLastError("Too many files"); goto fail2; } central_end = central_ofs + central_size; if (central_end > header_pos || central_end < central_ofs) { - Com_SetLastError("bad central directory offset"); + Com_SetLastError("Bad central directory offset"); goto fail2; } @@ -2448,7 +2448,7 @@ static pack_t *load_zip_file(const char *packfile) } if (os_fseek(fp, central_ofs + extra_bytes, SEEK_SET)) { - Com_SetLastError("seeking to central directory failed"); + Com_SetLastError("Seeking to central directory failed"); goto fail2; } @@ -2465,7 +2465,7 @@ static pack_t *load_zip_file(const char *packfile) if (len) { // fix absolute position if (file->filepos > INT64_MAX - extra_bytes) { - Com_SetLastError("bad file position"); + Com_SetLastError("Bad file position"); goto fail1; } file->filepos += extra_bytes; @@ -2482,7 +2482,7 @@ static pack_t *load_zip_file(const char *packfile) names_len = name - pack->names; if (!num_files) { - Com_SetLastError("no valid files"); + Com_SetLastError("No valid files"); goto fail1; } diff --git a/src/refresh/images.c b/src/refresh/images.c index 36fdea447..680ee211f 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -233,25 +233,25 @@ static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, } if (pcx->encoding != 1 || pcx->bits_per_pixel != 8) { - Com_SetLastError("invalid encoding or bits per pixel"); + Com_SetLastError("Invalid encoding or bits per pixel"); return Q_ERR_INVALID_FORMAT; } w = (LittleShort(pcx->xmax) - LittleShort(pcx->xmin)) + 1; h = (LittleShort(pcx->ymax) - LittleShort(pcx->ymin)) + 1; if (check_image_size(w, h)) { - Com_SetLastError("invalid image dimensions"); + Com_SetLastError("Invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } if (pcx->color_planes != 1) { - Com_SetLastError("invalid number of color planes"); + Com_SetLastError("Invalid number of color planes"); return Q_ERR_INVALID_FORMAT; } scan = LittleShort(pcx->bytes_per_line); if (scan < w) { - Com_SetLastError("invalid number of bytes per line"); + Com_SetLastError("Invalid number of bytes per line"); return Q_ERR_INVALID_FORMAT; } @@ -332,13 +332,13 @@ IMG_LOAD(WAL) w = LittleLong(mt->width); h = LittleLong(mt->height); if (check_image_size(w, h)) { - Com_SetLastError("invalid image dimensions"); + Com_SetLastError("Invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } offset = LittleLong(mt->offsets[0]); if ((uint64_t)offset + w * h > rawlen) { - Com_SetLastError("data out of bounds"); + Com_SetLastError("Data out of bounds"); return Q_ERR_INVALID_FORMAT; } @@ -529,12 +529,12 @@ IMG_LOAD(TGA) case TGA_Mono: break; default: - Com_SetLastError("unsupported targa image type"); + Com_SetLastError("Unsupported targa image type"); return Q_ERR_INVALID_FORMAT; } if (check_image_size(w, h)) { - Com_SetLastError("invalid image dimensions"); + Com_SetLastError("Invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -546,7 +546,7 @@ IMG_LOAD(TGA) case 32: break; default: - Com_SetLastError("unsupported number of bits per pixel"); + Com_SetLastError("Unsupported number of bits per pixel"); return Q_ERR_INVALID_FORMAT; } @@ -561,17 +561,17 @@ IMG_LOAD(TGA) interleave = 4; break; default: - Com_SetLastError("unsupported interleaving flag"); + Com_SetLastError("Unsupported interleaving flag"); return Q_ERR_INVALID_FORMAT; } if (image_type == TGA_Colormap) { if (!colormap_type) { - Com_SetLastError("colormapped image but no colormap present"); + Com_SetLastError("Colormapped image but no colormap present"); return Q_ERR_INVALID_FORMAT; } if (pixel_size != 8) { - Com_SetLastError("only 8-bit colormaps are supported"); + Com_SetLastError("Only 8-bit colormaps are supported"); return Q_ERR_INVALID_FORMAT; } } @@ -592,12 +592,12 @@ IMG_LOAD(TGA) case 32: break; default: - Com_SetLastError("unsupported number of bits per colormap pixel"); + Com_SetLastError("Unsupported number of bits per colormap pixel"); return Q_ERR_INVALID_FORMAT; } if (colormap_start + colormap_length > 256) { - Com_SetLastError("too many colormap entries"); + Com_SetLastError("Too many colormap entries"); return Q_ERR_INVALID_FORMAT; } @@ -764,7 +764,7 @@ static int my_jpeg_start_decompress(j_decompress_ptr cinfo, const byte *rawdata, jpeg_read_header(cinfo, TRUE); if (cinfo->out_color_space != JCS_RGB && cinfo->out_color_space != JCS_GRAYSCALE) { - Com_SetLastError("invalid image color space"); + Com_SetLastError("Invalid image color space"); return Q_ERR_INVALID_FORMAT; } @@ -773,12 +773,12 @@ static int my_jpeg_start_decompress(j_decompress_ptr cinfo, const byte *rawdata, jpeg_start_decompress(cinfo); if (cinfo->output_components != 4) { - Com_SetLastError("invalid number of color components"); + Com_SetLastError("Invalid number of color components"); return Q_ERR_INVALID_FORMAT; } if (check_image_size(cinfo->output_width, cinfo->output_height)) { - Com_SetLastError("invalid image dimensions"); + Com_SetLastError("Invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } @@ -963,7 +963,7 @@ static int my_png_read_header(png_structp png_ptr, png_infop info_ptr, } if (check_image_size(w, h)) { - Com_SetLastError("invalid image dimensions"); + Com_SetLastError("Invalid image dimensions"); return Q_ERR_INVALID_FORMAT; } diff --git a/src/refresh/models.c b/src/refresh/models.c index c0bc531a5..eca09f046 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -185,11 +185,11 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) return Q_ERR_SUCCESS; } if (header.numframes > SP2_MAX_FRAMES) { - Com_SetLastError("too many frames"); + Com_SetLastError("Too many frames"); return Q_ERR_INVALID_FORMAT; } if (sizeof(dsp2header_t) + sizeof(dsp2frame_t) * header.numframes > length) { - Com_SetLastError("frames out of bounds"); + Com_SetLastError("Frames out of bounds"); return Q_ERR_INVALID_FORMAT; } @@ -227,28 +227,28 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) static const char *MOD_ValidateMD2(const dmd2header_t *header, size_t length) { - ENSURE(header->num_tris <= TESS_MAX_INDICES / 3, "too many tris"); - ENSURE(header->num_st <= INT_MAX / sizeof(dmd2stvert_t), "too many st"); - ENSURE(header->num_xyz <= MD2_MAX_VERTS, "too many xyz"); - ENSURE(header->num_frames <= MD2_MAX_FRAMES, "too many frames"); - ENSURE(header->num_skins <= MD2_MAX_SKINS, "too many skins"); + ENSURE(header->num_tris <= TESS_MAX_INDICES / 3, "Too many tris"); + ENSURE(header->num_st <= INT_MAX / sizeof(dmd2stvert_t), "Too many st"); + ENSURE(header->num_xyz <= MD2_MAX_VERTS, "Too many xyz"); + ENSURE(header->num_frames <= MD2_MAX_FRAMES, "Too many frames"); + ENSURE(header->num_skins <= MD2_MAX_SKINS, "Too many skins"); Q_assert(header->num_xyz); - ENSURE(header->framesize >= sizeof(dmd2frame_t) + (header->num_xyz - 1) * sizeof(dmd2trivertx_t), "too small frame size"); - ENSURE(header->framesize <= MD2_MAX_FRAMESIZE, "too big frame size"); + ENSURE(header->framesize >= sizeof(dmd2frame_t) + (header->num_xyz - 1) * sizeof(dmd2trivertx_t), "Too small frame size"); + ENSURE(header->framesize <= MD2_MAX_FRAMESIZE, "Too big frame size"); - ENSURE((uint64_t)header->ofs_tris + header->num_tris * sizeof(dmd2triangle_t) <= length, "bad tris offset"); - ENSURE((uint64_t)header->ofs_st + header->num_st * sizeof(dmd2stvert_t) <= length, "bad st offset"); - ENSURE((uint64_t)header->ofs_frames + header->num_frames * header->framesize <= length, "bad frames offset"); - ENSURE((uint64_t)header->ofs_skins + MD2_MAX_SKINNAME * header->num_skins <= length, "bad skins offset"); + ENSURE((uint64_t)header->ofs_tris + header->num_tris * sizeof(dmd2triangle_t) <= length, "Bad tris offset"); + ENSURE((uint64_t)header->ofs_st + header->num_st * sizeof(dmd2stvert_t) <= length, "Bad st offset"); + ENSURE((uint64_t)header->ofs_frames + header->num_frames * header->framesize <= length, "Bad frames offset"); + ENSURE((uint64_t)header->ofs_skins + MD2_MAX_SKINNAME * header->num_skins <= length, "Bad skins offset"); - ENSURE(!(header->ofs_tris % q_alignof(dmd2triangle_t)), "odd tris offset"); - ENSURE(!(header->ofs_st % q_alignof(dmd2stvert_t)), "odd st offset"); - ENSURE(!(header->ofs_frames % q_alignof(dmd2frame_t)), "odd frames offset"); - ENSURE(!(header->framesize % q_alignof(dmd2frame_t)), "odd frame size"); + ENSURE(!(header->ofs_tris % q_alignof(dmd2triangle_t)), "Odd tris offset"); + ENSURE(!(header->ofs_st % q_alignof(dmd2stvert_t)), "Odd st offset"); + ENSURE(!(header->ofs_frames % q_alignof(dmd2frame_t)), "Odd frames offset"); + ENSURE(!(header->framesize % q_alignof(dmd2frame_t)), "Odd frame size"); - ENSURE(header->skinwidth >= 1 && header->skinwidth <= MAX_TEXTURE_SIZE, "bad skin width"); - ENSURE(header->skinheight >= 1 && header->skinheight <= MAX_TEXTURE_SIZE, "bad skin height"); + ENSURE(header->skinwidth >= 1 && header->skinwidth <= MAX_TEXTURE_SIZE, "Bad skin width"); + ENSURE(header->skinheight >= 1 && header->skinheight <= MAX_TEXTURE_SIZE, "Bad skin height"); return NULL; } @@ -324,7 +324,7 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) } if (numindices < 3) { - Com_SetLastError("too few valid indices"); + Com_SetLastError("Too few valid indices"); return Q_ERR_INVALID_FORMAT; } @@ -356,7 +356,7 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) } if (numverts > TESS_MAX_VERTICES) { - Com_SetLastError("too many verts"); + Com_SetLastError("Too many verts"); return Q_ERR_INVALID_FORMAT; } @@ -482,24 +482,24 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) #if USE_MD3 static const char *MOD_ValidateMD3Mesh(const model_t *model, const dmd3mesh_t *header, size_t length) { - ENSURE(header->meshsize >= sizeof(*header) && header->meshsize <= length, "bad mesh size"); - ENSURE(!(header->meshsize % q_alignof(dmd3mesh_t)), "odd mesh size"); - - ENSURE(header->num_verts >= 3, "too few verts"); - ENSURE(header->num_verts <= TESS_MAX_VERTICES, "too many verts"); - ENSURE(header->num_tris >= 1, "too few tris"); - ENSURE(header->num_tris <= TESS_MAX_INDICES / 3, "too many tris"); - ENSURE(header->num_skins <= MD3_MAX_SKINS, "too many skins"); - - ENSURE((uint64_t)header->ofs_skins + header->num_skins * sizeof(dmd3skin_t) <= length, "bad skins offset"); - ENSURE((uint64_t)header->ofs_verts + header->num_verts * model->numframes * sizeof(dmd3vertex_t) <= length, "bad verts offset"); - ENSURE((uint64_t)header->ofs_tcs + header->num_verts * sizeof(dmd3coord_t) <= length, "bad tcs offset"); - ENSURE((uint64_t)header->ofs_indexes + header->num_tris * 3 * sizeof(uint32_t) <= length, "bad indexes offset"); - - ENSURE(!(header->ofs_skins % q_alignof(dmd3skin_t)), "odd skins offset"); - ENSURE(!(header->ofs_verts % q_alignof(dmd3vertex_t)), "odd verts offset"); - ENSURE(!(header->ofs_tcs % q_alignof(dmd3coord_t)), "odd tcs offset"); - ENSURE(!(header->ofs_indexes & 3), "odd indexes offset"); + ENSURE(header->meshsize >= sizeof(*header) && header->meshsize <= length, "Bad mesh size"); + ENSURE(!(header->meshsize % q_alignof(dmd3mesh_t)), "Odd mesh size"); + + ENSURE(header->num_verts >= 3, "Too few verts"); + ENSURE(header->num_verts <= TESS_MAX_VERTICES, "Too many verts"); + ENSURE(header->num_tris >= 1, "Too few tris"); + ENSURE(header->num_tris <= TESS_MAX_INDICES / 3, "Too many tris"); + ENSURE(header->num_skins <= MD3_MAX_SKINS, "Too many skins"); + + ENSURE((uint64_t)header->ofs_skins + header->num_skins * sizeof(dmd3skin_t) <= length, "Bad skins offset"); + ENSURE((uint64_t)header->ofs_verts + header->num_verts * model->numframes * sizeof(dmd3vertex_t) <= length, "Bad verts offset"); + ENSURE((uint64_t)header->ofs_tcs + header->num_verts * sizeof(dmd3coord_t) <= length, "Bad tcs offset"); + ENSURE((uint64_t)header->ofs_indexes + header->num_tris * 3 * sizeof(uint32_t) <= length, "Bad indexes offset"); + + ENSURE(!(header->ofs_skins % q_alignof(dmd3skin_t)), "Odd skins offset"); + ENSURE(!(header->ofs_verts % q_alignof(dmd3vertex_t)), "Odd verts offset"); + ENSURE(!(header->ofs_tcs % q_alignof(dmd3coord_t)), "Odd tcs offset"); + ENSURE(!(header->ofs_indexes & 3), "Odd indexes offset"); return NULL; } @@ -596,7 +596,7 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, for (i = 0; i < header.num_tris * 3; i++) { index = LittleLong(*src_idx++); if (index >= header.num_verts) { - Com_SetLastError("bad triangle index"); + Com_SetLastError("Bad triangle index"); return Q_ERR_INVALID_FORMAT; } *dst_idx++ = index; @@ -611,14 +611,14 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, static const char *MOD_ValidateMD3(const dmd3header_t *header, size_t length) { - ENSURE(header->num_frames >= 1, "too few frames"); - ENSURE(header->num_frames <= MD3_MAX_FRAMES, "too many frames"); - ENSURE((uint64_t)header->ofs_frames + header->num_frames * sizeof(dmd3frame_t) <= length, "bad frames offset"); - ENSURE(!(header->ofs_frames % q_alignof(dmd3frame_t)), "odd frames offset"); - ENSURE(header->num_meshes >= 1, "too few meshes"); - ENSURE(header->num_meshes <= MD3_MAX_MESHES, "too many meshes"); - ENSURE(header->ofs_meshes <= length, "bad meshes offset"); - ENSURE(!(header->ofs_meshes % q_alignof(dmd3mesh_t)), "odd meshes offset"); + ENSURE(header->num_frames >= 1, "Too few frames"); + ENSURE(header->num_frames <= MD3_MAX_FRAMES, "Too many frames"); + ENSURE((uint64_t)header->ofs_frames + header->num_frames * sizeof(dmd3frame_t) <= length, "Bad frames offset"); + ENSURE(!(header->ofs_frames % q_alignof(dmd3frame_t)), "Odd frames offset"); + ENSURE(header->num_meshes >= 1, "Too few meshes"); + ENSURE(header->num_meshes <= MD3_MAX_MESHES, "Too many meshes"); + ENSURE(header->ofs_meshes <= length, "Bad meshes offset"); + ENSURE(!(header->ofs_meshes % q_alignof(dmd3mesh_t)), "Odd meshes offset"); return NULL; } @@ -720,7 +720,7 @@ static bool MD5_ParseExpect(const char **buffer, const char *expect) char *token = COM_Parse(buffer); if (strcmp(token, expect)) { - Com_SetLastError(va("expected %s, got %s", expect, token)); + Com_SetLastError(va("Expected %s, got %s", expect, token)); return false; } @@ -734,7 +734,7 @@ static bool MD5_ParseFloat(const char **buffer, float *output) *output = strtof(token, &endptr); if (endptr == token || *endptr) { - Com_SetLastError(va("expected float, got %s", token)); + Com_SetLastError(va("Expected float, got %s", token)); return false; } @@ -748,7 +748,7 @@ static bool MD5_ParseUint(const char **buffer, uint32_t *output) *output = strtoul(token, &endptr, 10); if (endptr == token || *endptr) { - Com_SetLastError(va("expected int, got %s", token)); + Com_SetLastError(va("Expected int, got %s", token)); return false; } @@ -887,15 +887,15 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("numJoints"); MD5_UINT(num_joints); - MD5_ENSURE(num_joints > 0, "no joints"); - MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "too many joints"); + MD5_ENSURE(num_joints > 0, "No joints"); + MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "Too many joints"); OOM_CHECK(mdl->base_skeleton = MD5_Malloc(num_joints * sizeof(mdl->base_skeleton[0]))); mdl->num_joints = num_joints; MD5_EXPECT("numMeshes"); MD5_UINT(num_meshes); - MD5_ENSURE(num_meshes > 0, "no meshes"); - MD5_ENSURE(num_meshes <= MD5_MAX_MESHES, "too many meshes"); + MD5_ENSURE(num_meshes > 0, "No meshes"); + MD5_ENSURE(num_meshes <= MD5_MAX_MESHES, "Too many meshes"); OOM_CHECK(mdl->meshes = MD5_Malloc(num_meshes * sizeof(mdl->meshes[0]))); mdl->num_meshes = num_meshes; @@ -910,7 +910,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) uint32_t parent; MD5_UINT(parent); - MD5_ENSURE(parent == -1 || (parent < num_joints && parent != i), "bad parent joint"); + MD5_ENSURE(parent == -1 || (parent < num_joints && parent != i), "Bad parent joint"); joint->parent = parent; MD5_VECTOR(joint->pos); @@ -934,7 +934,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("numverts"); MD5_UINT(num_verts); - MD5_ENSURE(num_verts <= TESS_MAX_VERTICES, "too many verts"); + MD5_ENSURE(num_verts <= TESS_MAX_VERTICES, "Too many verts"); OOM_CHECK(mesh->vertices = MD5_Malloc(num_verts * sizeof(mesh->vertices[0]))); OOM_CHECK(mesh->tcoords = MD5_Malloc(num_verts * sizeof(mesh->tcoords[0]))); mesh->num_verts = num_verts; @@ -944,7 +944,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) uint32_t vert_index; MD5_UINT(vert_index); - MD5_ENSURE(vert_index < num_verts, "bad vert index"); + MD5_ENSURE(vert_index < num_verts, "Bad vert index"); maliastc_t *tc = &mesh->tcoords[vert_index]; MD5_EXPECT("("); @@ -959,7 +959,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) MD5_EXPECT("numtris"); MD5_UINT(num_tris); - MD5_ENSURE(num_tris <= TESS_MAX_INDICES / 3, "too many tris"); + MD5_ENSURE(num_tris <= TESS_MAX_INDICES / 3, "Too many tris"); OOM_CHECK(mesh->indices = MD5_Malloc(num_tris * 3 * sizeof(mesh->indices[0]))); mesh->num_indices = num_tris * 3; @@ -968,19 +968,19 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) uint32_t tri_index; MD5_UINT(tri_index); - MD5_ENSURE(tri_index < num_tris, "bad tri index"); + MD5_ENSURE(tri_index < num_tris, "Bad tri index"); for (k = 0; k < 3; k++) { uint32_t vert_index; MD5_UINT(vert_index); - MD5_ENSURE(vert_index < mesh->num_verts, "bad tri vert"); + MD5_ENSURE(vert_index < mesh->num_verts, "Bad tri vert"); mesh->indices[tri_index * 3 + k] = vert_index; } } MD5_EXPECT("numweights"); MD5_UINT(num_weights); - MD5_ENSURE(num_weights <= MD5_MAX_WEIGHTS, "too many weights"); + MD5_ENSURE(num_weights <= MD5_MAX_WEIGHTS, "Too many weights"); OOM_CHECK(mesh->weights = MD5_Malloc(num_weights * sizeof(mesh->weights[0]))); mesh->num_weights = num_weights; @@ -989,13 +989,13 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) uint32_t weight_index; MD5_UINT(weight_index); - MD5_ENSURE(weight_index < num_weights, "bad weight index"); + MD5_ENSURE(weight_index < num_weights, "Bad weight index"); md5_weight_t *weight = &mesh->weights[weight_index]; uint32_t joint; MD5_UINT(joint); - MD5_ENSURE(joint < mdl->num_joints, "bad weight joint"); + MD5_ENSURE(joint < mdl->num_joints, "Bad weight joint"); weight->joint = joint; MD5_FLOAT(weight->bias); @@ -1008,7 +1008,7 @@ static bool MOD_LoadMD5Mesh(model_t *model, const char *path) // because of circular data dependencies for (j = 0; j < num_verts; j++) { md5_vertex_t *vert = &mesh->vertices[j]; - MD5_ENSURE((uint64_t)vert->start + vert->count <= num_weights, "bad start/count"); + MD5_ENSURE((uint64_t)vert->start + vert->count <= num_weights, "Bad vert start/count"); } MD5_ComputeNormals(mesh, mdl->base_skeleton); @@ -1190,8 +1190,8 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) MD5_UINT(num_frames); // md5 replacements need at least 1 frame, because the // pose frame isn't used - MD5_ENSURE(num_frames > 0, "no frames"); - MD5_ENSURE(num_frames <= MD5_MAX_FRAMES, "too many frames"); + MD5_ENSURE(num_frames > 0, "No frames"); + MD5_ENSURE(num_frames <= MD5_MAX_FRAMES, "Too many frames"); mdl->num_frames = num_frames; // warn on mismatched frame counts (not fatal) @@ -1202,14 +1202,14 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) MD5_EXPECT("numJoints"); MD5_UINT(num_joints); - MD5_ENSURE(num_joints == mdl->num_joints, "bad numJoints"); + MD5_ENSURE(num_joints == mdl->num_joints, "Bad numJoints"); MD5_EXPECT("frameRate"); MD5_SKIP(); MD5_EXPECT("numAnimatedComponents"); MD5_UINT(num_animated_components); - MD5_ENSURE(num_animated_components <= q_countof(anim_frame_data), "bad numAnimatedComponents"); + MD5_ENSURE(num_animated_components <= q_countof(anim_frame_data), "Bad numAnimatedComponents"); MD5_EXPECT("hierarchy"); MD5_EXPECT("{"); @@ -1232,10 +1232,10 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) if (joint_info->flags & BIT(j)) num_components++; - MD5_ENSURE((uint64_t)joint_info->start_index + num_components <= num_animated_components, "bad joint info"); + MD5_ENSURE((uint64_t)joint_info->start_index + num_components <= num_animated_components, "Bad joint info"); // validate parents; they need to match the base skeleton - MD5_ENSURE(joint_info->parent == mdl->base_skeleton[i].parent, "bad parent"); + MD5_ENSURE(joint_info->parent == mdl->base_skeleton[i].parent, "Bad parent joint"); } MD5_EXPECT("}"); @@ -1286,7 +1286,7 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) uint32_t frame_index; MD5_UINT(frame_index); - MD5_ENSURE(frame_index < mdl->num_frames, "bad frame index"); + MD5_ENSURE(frame_index < mdl->num_frames, "Bad frame index"); MD5_EXPECT("{"); for (j = 0; j < num_animated_components; j++) From 01de0ad94357102ffe43d59338a47b1217f1ac66 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 17:16:46 +0300 Subject: [PATCH 388/974] Shorten muzzleflash registration code. --- src/client/tent.c | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/client/tent.c b/src/client/tent.c index fee073abc..9639a48be 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -270,6 +270,21 @@ void CL_RegisterTEntSounds(void) cl_sfx_hit_marker = S_RegisterSound("weapons/marker.wav"); } +static const char *const muzzlenames[MFLASH_TOTAL] = { + [MFLASH_MACHN] = "v_machn", + [MFLASH_SHOTG2] = "v_shotg2", + [MFLASH_SHOTG] = "v_shotg", + [MFLASH_ROCKET] = "v_rocket", + [MFLASH_RAIL] = "v_rail", + [MFLASH_LAUNCH] = "v_launch", + [MFLASH_ETF_RIFLE] = "v_etf_rifle", + [MFLASH_DIST] = "v_dist", + [MFLASH_BOOMER] = "v_boomer", + [MFLASH_BLAST] = "v_blast", + [MFLASH_BFG] = "v_bfg", + [MFLASH_BEAMER] = "v_beamer", +}; + /* ================= CL_RegisterTEntModels @@ -295,18 +310,8 @@ void CL_RegisterTEntModels(void) cl_mod_heatbeam = R_RegisterModel("models/proj/beam/tris.md2"); cl_mod_explo4_big = R_RegisterModel("models/objects/r_explode2/tris.md2"); - cl_mod_muzzles[MFLASH_MACHN] = R_RegisterModel("models/weapons/v_machn/flash/tris.md2"); - cl_mod_muzzles[MFLASH_SHOTG2] = R_RegisterModel("models/weapons/v_shotg2/flash/tris.md2"); - cl_mod_muzzles[MFLASH_SHOTG] = R_RegisterModel("models/weapons/v_shotg/flash/tris.md2"); - cl_mod_muzzles[MFLASH_ROCKET] = R_RegisterModel("models/weapons/v_rocket/flash/tris.md2"); - cl_mod_muzzles[MFLASH_RAIL] = R_RegisterModel("models/weapons/v_rail/flash/tris.md2"); - cl_mod_muzzles[MFLASH_LAUNCH] = R_RegisterModel("models/weapons/v_launch/flash/tris.md2"); - cl_mod_muzzles[MFLASH_ETF_RIFLE] = R_RegisterModel("models/weapons/v_etf_rifle/flash/tris.md2"); - cl_mod_muzzles[MFLASH_DIST] = R_RegisterModel("models/weapons/v_dist/flash/tris.md2"); - cl_mod_muzzles[MFLASH_BOOMER] = R_RegisterModel("models/weapons/v_boomer/flash/tris.md2"); - cl_mod_muzzles[MFLASH_BLAST] = R_RegisterModel("models/weapons/v_blast/flash/tris.md2"); - cl_mod_muzzles[MFLASH_BFG] = R_RegisterModel("models/weapons/v_bfg/flash/tris.md2"); - cl_mod_muzzles[MFLASH_BEAMER] = R_RegisterModel("models/weapons/v_beamer/flash/tris.md2"); + for (int i = 0; i < MFLASH_TOTAL; i++) + cl_mod_muzzles[i] = R_RegisterModel(va("models/weapons/%s/flash/tris.md2", muzzlenames[i])); cl_img_flare = R_RegisterSprite("misc/flare.tga"); From 2d46fd265698946b6528d1a477c019bb80b09974 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 19:01:14 +0300 Subject: [PATCH 389/974] Avoid adding stray semicolons by macros. --- src/refresh/models.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index eca09f046..a02d2d434 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MOD_Malloc(size) Hunk_TryAlloc(&model->hunk, size) -#define OOM_CHECK(x) if (!(x)) { ret = Q_ERR(ENOMEM); goto fail; } +#define OOM_CHECK(x) do { if (!(x)) { ret = Q_ERR(ENOMEM); goto fail; } } while (0) #define ENSURE(x, e) if (!(x)) return e // this used to be MAX_MODELS * 2, but not anymore. MAX_MODELS is 8192 now and @@ -766,10 +766,10 @@ static bool MD5_ParseVector(const char **buffer, vec3_t output) } #define MD5_CHECK(x) \ - if (!(x)) { ret = Q_ERR_INVALID_FORMAT; goto fail; } + do { if (!(x)) { ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) #define MD5_ENSURE(x, e) \ - if (!(x)) { Com_SetLastError(e); ret = Q_ERR_INVALID_FORMAT; goto fail; } + do { if (!(x)) { Com_SetLastError(e); ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) #define MD5_EXPECT(x) MD5_CHECK(MD5_ParseExpect(&s, x)) #define MD5_UINT(x) MD5_CHECK(MD5_ParseUint(&s, &x)) From dd180cb98ec80281189b15001df626f39d86da9a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 19:03:52 +0300 Subject: [PATCH 390/974] Simplify BSP error checking. --- src/common/bsp.c | 204 +++++++++++------------------------------------ 1 file changed, 46 insertions(+), 158 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 693854be1..abb7032b8 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -54,6 +54,9 @@ static cvar_t *map_visibility_patch; #define DEBUG(msg) \ Com_SetLastError(va("%s: %s", __func__, msg)) +#define ENSURE(cond, msg) \ + do { if (!(cond)) { DEBUG(msg); return Q_ERR_INVALID_FORMAT; } } while (0) + #define BSP_Short() (in += 2, RL16(in - 2)) #define BSP_Long() (in += 4, RL32(in - 4)) #define BSP_Float() LongToFloat(BSP_Long()) @@ -71,22 +74,13 @@ LOAD(Visibility) return Q_ERR_SUCCESS; } - if (count < 4) { - DEBUG("too small header"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count >= 4, "Too small header"); numclusters = BSP_Long(); - if (numclusters > MAX_MAP_CLUSTERS) { - DEBUG("too many clusters"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); hdrsize = 4 + numclusters * 8; - if (count < hdrsize) { - DEBUG("too small header"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count >= hdrsize, "Too small header"); bsp->numvisibility = count; bsp->vis = ALLOC(count); @@ -97,10 +91,7 @@ LOAD(Visibility) for (i = 0; i < numclusters; i++) { for (j = 0; j < 2; j++) { bitofs = BSP_Long(); - if (bitofs < hdrsize || bitofs >= count) { - DEBUG("bad bitofs"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); bsp->vis->bitofs[i][j] = bitofs; } } @@ -145,10 +136,7 @@ LOAD(Texinfo) #if USE_REF next = (int32_t)BSP_Long(); if (next > 0) { - if (next >= count) { - DEBUG("bad anim chain"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(next < count, "Bad anim chain"); out->next = bsp->texinfo + next; } else { out->next = NULL; @@ -165,7 +153,7 @@ LOAD(Texinfo) out->numframes = 1; for (step = out->next; step && step != out; step = step->next) { if (out->numframes == count) { - DEBUG("infinite anim chain"); + DEBUG("Infinite anim chain"); return Q_ERR_INFINITE_LOOP; } out->numframes++; @@ -209,19 +197,13 @@ LOAD(BrushSides) out = bsp->brushsides; for (i = 0; i < count; i++, out++) { planenum = BSP_ExtLong(); - if (planenum >= bsp->numplanes) { - DEBUG("bad planenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; texinfo = BSP_ExtLong(); if (texinfo == BSP_ExtNull) { out->texinfo = &nulltexinfo; } else { - if (texinfo >= bsp->numtexinfo) { - DEBUG("bad texinfo"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); out->texinfo = bsp->texinfo + texinfo; } } @@ -242,10 +224,7 @@ LOAD(Brushes) for (i = 0; i < count; i++, out++) { firstside = BSP_Long(); numsides = BSP_Long(); - if ((uint64_t)firstside + numsides > bsp->numbrushsides) { - DEBUG("bad brushsides"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); out->firstbrushside = bsp->brushsides + firstside; out->numsides = numsides; out->contents = BSP_Long(); @@ -267,10 +246,7 @@ LOAD(LeafBrushes) out = bsp->leafbrushes; for (i = 0; i < count; i++, out++) { brushnum = BSP_ExtLong(); - if (brushnum >= bsp->numbrushes) { - DEBUG("bad brushnum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); *out = bsp->brushes + brushnum; } @@ -321,10 +297,7 @@ LOAD(Edges) for (i = 0; i < count; i++, out++) { for (j = 0; j < 2; j++) { vertnum = BSP_ExtLong(); - if (vertnum >= bsp->numvertices) { - DEBUG("bad vertnum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); out->v[j] = vertnum; } } @@ -349,10 +322,7 @@ LOAD(SurfEdges) if (vert) index = -index; - if (index >= bsp->numedges) { - DEBUG("bad edgenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(index < bsp->numedges, "Bad edgenum"); out->edge = index; out->vert = vert; @@ -375,10 +345,7 @@ LOAD(Faces) out = bsp->faces; for (i = 0; i < count; i++, out++) { planenum = BSP_ExtLong(); - if (planenum >= bsp->numplanes) { - DEBUG("bad planenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; side = BSP_ExtLong(); @@ -386,26 +353,13 @@ LOAD(Faces) firstedge = BSP_Long(); numedges = BSP_ExtLong(); - if (numedges < 3) { - DEBUG("too few surfedges"); - return Q_ERR_INVALID_FORMAT; - } - if (numedges > 4096) { - DEBUG("too many surfedges"); - return Q_ERR_INVALID_FORMAT; - } - if ((uint64_t)firstedge + numedges > bsp->numsurfedges) { - DEBUG("bad surfedges"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(numedges >= 3 && numedges <= 4096 && + (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); out->firstsurfedge = bsp->surfedges + firstedge; out->numsurfedges = numedges; texinfo = BSP_ExtLong(); - if (texinfo >= bsp->numtexinfo) { - DEBUG("bad texinfo"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); out->texinfo = bsp->texinfo + texinfo; for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) { @@ -420,10 +374,7 @@ LOAD(Faces) if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { out->lightmap = NULL; } else { - if (lightofs >= bsp->numlightmapbytes) { - DEBUG("bad lightofs"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(lightofs < bsp->numlightmapbytes, "Bad lightofs"); out->lightmap = bsp->lightmap + lightofs; } } @@ -443,10 +394,7 @@ LOAD(LeafFaces) out = bsp->leaffaces; for (i = 0; i < count; i++, out++) { facenum = BSP_ExtLong(); - if (facenum >= bsp->numfaces) { - DEBUG("bad facenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(facenum < bsp->numfaces, "Bad facenum"); *out = bsp->faces + facenum; } @@ -465,10 +413,7 @@ LOAD(Leafs) uint32_t firstleafface, numleaffaces; #endif - if (!count) { - DEBUG("map with no leafs"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count > 0, "Map with no leafs"); bsp->numleafs = count; bsp->leafs = ALLOC(sizeof(*out) * count); @@ -486,18 +431,12 @@ LOAD(Leafs) out->cluster = 0; } else { // validate cluster - if (cluster >= bsp->vis->numclusters) { - DEBUG("bad cluster"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(cluster < bsp->vis->numclusters, "Bad cluster"); out->cluster = cluster; } area = BSP_ExtLong(); - if (area >= bsp->numareas) { - DEBUG("bad area"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(area < bsp->numareas, "Bad area"); out->area = area; #if USE_REF @@ -508,10 +447,7 @@ LOAD(Leafs) firstleafface = BSP_ExtLong(); numleaffaces = BSP_ExtLong(); - if ((uint64_t)firstleafface + numleaffaces > bsp->numleaffaces) { - DEBUG("bad leaffaces"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); out->firstleafface = bsp->leaffaces + firstleafface; out->numleaffaces = numleaffaces; @@ -523,18 +459,12 @@ LOAD(Leafs) firstleafbrush = BSP_ExtLong(); numleafbrushes = BSP_ExtLong(); - if ((uint64_t)firstleafbrush + numleafbrushes > bsp->numleafbrushes) { - DEBUG("bad leafbrushes"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); out->firstleafbrush = bsp->leafbrushes + firstleafbrush; out->numleafbrushes = numleafbrushes; } - if (bsp->leafs[0].contents != CONTENTS_SOLID) { - DEBUG("map leaf 0 is not CONTENTS_SOLID"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(bsp->leafs[0].contents == CONTENTS_SOLID, "Map leaf 0 is not CONTENTS_SOLID"); return Q_ERR_SUCCESS; } @@ -548,10 +478,7 @@ LOAD(Nodes) uint32_t firstface, numfaces; #endif - if (!count) { - DEBUG("map with no nodes"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count > 0, "Map with no nodes"); bsp->numnodes = count; bsp->nodes = ALLOC(sizeof(*out) * count); @@ -559,26 +486,17 @@ LOAD(Nodes) out = bsp->nodes; for (i = 0; i < count; i++, out++) { planenum = BSP_Long(); - if (planenum >= bsp->numplanes) { - DEBUG("bad planenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; for (j = 0; j < 2; j++) { child = BSP_Long(); if (child & BIT(31)) { child = ~child; - if (child >= bsp->numleafs) { - DEBUG("bad leafnum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(child < bsp->numleafs, "Bad leafnum"); out->children[j] = (mnode_t *)(bsp->leafs + child); } else { - if (child >= count) { - DEBUG("bad nodenum"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(child < count, "Bad nodenum"); out->children[j] = bsp->nodes + child; } } @@ -591,10 +509,7 @@ LOAD(Nodes) firstface = BSP_ExtLong(); numfaces = BSP_ExtLong(); - if ((uint64_t)firstface + numfaces > bsp->numfaces) { - DEBUG("bad faces"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); out->firstface = bsp->faces + firstface; out->numfaces = numfaces; @@ -617,14 +532,8 @@ LOAD(SubModels) uint32_t firstface, numfaces; #endif - if (!count) { - DEBUG("map with no models"); - return Q_ERR_INVALID_FORMAT; - } - if (count > MAX_MODELS - 2) { - DEBUG("too many models"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count > 0, "Map with no models"); + ENSURE(count <= MAX_MODELS - 2, "Too many models"); bsp->nummodels = count; bsp->models = ALLOC(sizeof(*out) * count); @@ -643,16 +552,10 @@ LOAD(SubModels) if (headnode & BIT(31)) { // be careful, some models have no nodes, just a leaf headnode = ~headnode; - if (headnode >= bsp->numleafs) { - DEBUG("bad headleaf"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(headnode < bsp->numleafs, "Bad headleaf"); out->headnode = (mnode_t *)(bsp->leafs + headnode); } else { - if (headnode >= bsp->numnodes) { - DEBUG("bad headnode"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(headnode < bsp->numnodes, "Bad headnode"); out->headnode = bsp->nodes + headnode; } #if USE_REF @@ -662,10 +565,7 @@ LOAD(SubModels) } firstface = BSP_Long(); numfaces = BSP_Long(); - if ((uint64_t)firstface + numfaces > bsp->numfaces) { - DEBUG("bad faces"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); out->firstface = bsp->faces + firstface; out->numfaces = numfaces; @@ -702,10 +602,7 @@ LOAD(Areas) int i; uint32_t numareaportals, firstareaportal; - if (count > MAX_MAP_AREAS) { - DEBUG("too many areas"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); bsp->numareas = count; bsp->areas = ALLOC(sizeof(*out) * count); @@ -714,10 +611,7 @@ LOAD(Areas) for (i = 0; i < count; i++, out++) { numareaportals = BSP_Long(); firstareaportal = BSP_Long(); - if ((uint64_t)firstareaportal + numareaportals > bsp->numareaportals) { - DEBUG("bad areaportals"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); out->numareaportals = numareaportals; out->firstareaportal = bsp->areaportals + firstareaportal; out->floodvalid = 0; @@ -911,7 +805,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) // a face may never belong to more than one node for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { if (face->drawframe) { - DEBUG("duplicate face"); + DEBUG("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = key; @@ -920,7 +814,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[0]; if (child->parent) { - DEBUG("cycle encountered"); + DEBUG("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -930,7 +824,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[1]; if (child->parent) { - DEBUG("cycle encountered"); + DEBUG("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -951,7 +845,7 @@ static int BSP_ValidateTree(bsp_t *bsp) for (i = 0, mod = bsp->models; i < bsp->nummodels; i++, mod++) { if (i == 0 && mod->headnode != bsp->nodes) { - DEBUG("map model 0 headnode is not the first node"); + DEBUG("Map model 0 headnode is not the first node"); return Q_ERR_INVALID_FORMAT; } @@ -964,7 +858,7 @@ static int BSP_ValidateTree(bsp_t *bsp) // a face may never belong to more than one model for (j = 0, face = mod->firstface; j < mod->numfaces; j++, face++) { if (face->drawframe && face->drawframe != ~i) { - DEBUG("duplicate face"); + DEBUG("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = ~i; @@ -984,14 +878,8 @@ static int BSP_ValidateAreaPortals(bsp_t *bsp) bsp->numportals = 0; for (i = 0, p = bsp->areaportals; i < bsp->numareaportals; i++, p++) { - if (p->portalnum >= bsp->numareaportals) { - DEBUG("bad portalnum"); - return Q_ERR_INVALID_FORMAT; - } - if (p->otherarea >= bsp->numareas) { - DEBUG("bad otherarea"); - return Q_ERR_INVALID_FORMAT; - } + ENSURE(p->portalnum < bsp->numareaportals, "Bad portalnum"); + ENSURE(p->otherarea < bsp->numareas, "Bad otherarea"); bsp->numportals = max(bsp->numportals, p->portalnum + 1); } From d633760d1dead21731d1a2ba9dc31196e4ba28cc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 23:18:58 +0300 Subject: [PATCH 391/974] Further reformat/simplify BSP loading code. --- src/common/bsp.c | 229 +++++++++++++++++------------------------------ 1 file changed, 82 insertions(+), 147 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index abb7032b8..859bf8e8b 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -67,30 +67,25 @@ static cvar_t *map_visibility_patch; LOAD(Visibility) { - uint32_t numclusters, bitofs, hdrsize; - int i, j; - - if (!count) { + if (!count) return Q_ERR_SUCCESS; - } ENSURE(count >= 4, "Too small header"); - numclusters = BSP_Long(); + uint32_t numclusters = BSP_Long(); ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); - hdrsize = 4 + numclusters * 8; + uint32_t hdrsize = 4 + numclusters * 8; ENSURE(count >= hdrsize, "Too small header"); bsp->numvisibility = count; bsp->vis = ALLOC(count); bsp->vis->numclusters = numclusters; bsp->visrowsize = (numclusters + 7) >> 3; - Q_assert(bsp->visrowsize <= VIS_MAX_BYTES); - for (i = 0; i < numclusters; i++) { - for (j = 0; j < 2; j++) { - bitofs = BSP_Long(); + for (int i = 0; i < numclusters; i++) { + for (int j = 0; j < 2; j++) { + uint32_t bitofs = BSP_Long(); ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); bsp->vis->bitofs[i][j] = bitofs; } @@ -104,20 +99,13 @@ LOAD(Visibility) LOAD(Texinfo) { mtexinfo_t *out; - int i; -#if USE_REF - int j; - int32_t next; - mtexinfo_t *step; -#endif bsp->numtexinfo = count; - bsp->texinfo = ALLOC(sizeof(*out) * count); + bsp->texinfo = out = ALLOC(sizeof(*out) * count); - out = bsp->texinfo; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { #if USE_REF - for (j = 0; j < 2; j++) { + for (int j = 0; j < 2; j++) { out->axis[j][0] = BSP_Float(); out->axis[j][1] = BSP_Float(); out->axis[j][2] = BSP_Float(); @@ -134,7 +122,7 @@ LOAD(Texinfo) in += MAX_TEXNAME; #if USE_REF - next = (int32_t)BSP_Long(); + int32_t next = (int32_t)BSP_Long(); if (next > 0) { ENSURE(next < count, "Bad anim chain"); out->next = bsp->texinfo + next; @@ -149,9 +137,9 @@ LOAD(Texinfo) #if USE_REF // count animation frames out = bsp->texinfo; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { out->numframes = 1; - for (step = out->next; step && step != out; step = step->next) { + for (mtexinfo_t *step = out->next; step && step != out; step = step->next) { if (out->numframes == count) { DEBUG("Infinite anim chain"); return Q_ERR_INFINITE_LOOP; @@ -167,13 +155,11 @@ LOAD(Texinfo) LOAD(Planes) { cplane_t *out; - int i; bsp->numplanes = count; - bsp->planes = ALLOC(sizeof(*out) * count); + bsp->planes = out = ALLOC(sizeof(*out) * count); - out = bsp->planes; - for (i = 0; i < count; i++, in += 4, out++) { + for (int i = 0; i < count; i++, in += 4, out++) { out->normal[0] = BSP_Float(); out->normal[1] = BSP_Float(); out->normal[2] = BSP_Float(); @@ -188,18 +174,16 @@ LOAD(Planes) LOAD(BrushSides) { mbrushside_t *out; - int i; - uint32_t planenum, texinfo; bsp->numbrushsides = count; - bsp->brushsides = ALLOC(sizeof(*out) * count); + bsp->brushsides = out = ALLOC(sizeof(*out) * count); - out = bsp->brushsides; - for (i = 0; i < count; i++, out++) { - planenum = BSP_ExtLong(); + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; - texinfo = BSP_ExtLong(); + + uint32_t texinfo = BSP_ExtLong(); if (texinfo == BSP_ExtNull) { out->texinfo = &nulltexinfo; } else { @@ -214,16 +198,13 @@ LOAD(BrushSides) LOAD(Brushes) { mbrush_t *out; - int i; - uint32_t firstside, numsides; bsp->numbrushes = count; - bsp->brushes = ALLOC(sizeof(*out) * count); + bsp->brushes = out = ALLOC(sizeof(*out) * count); - out = bsp->brushes; - for (i = 0; i < count; i++, out++) { - firstside = BSP_Long(); - numsides = BSP_Long(); + for (int i = 0; i < count; i++, out++) { + uint32_t firstside = BSP_Long(); + uint32_t numsides = BSP_Long(); ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); out->firstbrushside = bsp->brushsides + firstside; out->numsides = numsides; @@ -237,15 +218,12 @@ LOAD(Brushes) LOAD(LeafBrushes) { mbrush_t **out; - int i; - uint32_t brushnum; bsp->numleafbrushes = count; - bsp->leafbrushes = ALLOC(sizeof(*out) * count); + bsp->leafbrushes = out = ALLOC(sizeof(*out) * count); - out = bsp->leafbrushes; - for (i = 0; i < count; i++, out++) { - brushnum = BSP_ExtLong(); + for (int i = 0; i < count; i++, out++) { + uint32_t brushnum = BSP_ExtLong(); ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); *out = bsp->brushes + brushnum; } @@ -269,13 +247,11 @@ LOAD(Lightmap) LOAD(Vertices) { mvertex_t *out; - int i; bsp->numvertices = count; - bsp->vertices = ALLOC(sizeof(*out) * count); + bsp->vertices = out = ALLOC(sizeof(*out) * count); - out = bsp->vertices; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { out->point[0] = BSP_Float(); out->point[1] = BSP_Float(); out->point[2] = BSP_Float(); @@ -287,16 +263,13 @@ LOAD(Vertices) LOAD(Edges) { medge_t *out; - int i, j; - uint32_t vertnum; bsp->numedges = count; - bsp->edges = ALLOC(sizeof(*out) * count); + bsp->edges = out = ALLOC(sizeof(*out) * count); - out = bsp->edges; - for (i = 0; i < count; i++, out++) { - for (j = 0; j < 2; j++) { - vertnum = BSP_ExtLong(); + for (int i = 0; i < count; i++, out++) { + for (int j = 0; j < 2; j++) { + uint32_t vertnum = BSP_ExtLong(); ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); out->v[j] = vertnum; } @@ -308,22 +281,16 @@ LOAD(Edges) LOAD(SurfEdges) { msurfedge_t *out; - int i; - uint32_t index, vert; bsp->numsurfedges = count; - bsp->surfedges = ALLOC(sizeof(*out) * count); + bsp->surfedges = out = ALLOC(sizeof(*out) * count); - out = bsp->surfedges; - for (i = 0; i < count; i++, out++) { - index = BSP_Long(); - - vert = index >> 31; + for (int i = 0; i < count; i++, out++) { + uint32_t index = BSP_Long(); + uint32_t vert = index >> 31; if (vert) index = -index; - ENSURE(index < bsp->numedges, "Bad edgenum"); - out->edge = index; out->vert = vert; } @@ -334,34 +301,29 @@ LOAD(SurfEdges) LOAD(Faces) { mface_t *out; - int i, j; - uint32_t firstedge, numedges; - uint32_t planenum, texinfo, side; - uint32_t lightofs; bsp->numfaces = count; - bsp->faces = ALLOC(sizeof(*out) * count); + bsp->faces = out = ALLOC(sizeof(*out) * count); - out = bsp->faces; - for (i = 0; i < count; i++, out++) { - planenum = BSP_ExtLong(); + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; - side = BSP_ExtLong(); - out->drawflags = side & DSURF_PLANEBACK; + out->drawflags = BSP_ExtLong() & DSURF_PLANEBACK; - firstedge = BSP_Long(); - numedges = BSP_ExtLong(); + uint32_t firstedge = BSP_Long(); + uint32_t numedges = BSP_ExtLong(); ENSURE(numedges >= 3 && numedges <= 4096 && (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); out->firstsurfedge = bsp->surfedges + firstedge; out->numsurfedges = numedges; - texinfo = BSP_ExtLong(); + uint32_t texinfo = BSP_ExtLong(); ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); out->texinfo = bsp->texinfo + texinfo; + int j; for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) { out->styles[j] = in[j]; } @@ -370,7 +332,7 @@ LOAD(Faces) } in += MAX_LIGHTMAPS; - lightofs = BSP_Long(); + uint32_t lightofs = BSP_Long(); if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { out->lightmap = NULL; } else { @@ -385,15 +347,13 @@ LOAD(Faces) LOAD(LeafFaces) { mface_t **out; - int i; - uint32_t facenum; bsp->numleaffaces = count; bsp->leaffaces = ALLOC(sizeof(*out) * count); out = bsp->leaffaces; - for (i = 0; i < count; i++, out++) { - facenum = BSP_ExtLong(); + for (int i = 0; i < count; i++, out++) { + uint32_t facenum = BSP_ExtLong(); ENSURE(facenum < bsp->numfaces, "Bad facenum"); *out = bsp->faces + facenum; } @@ -405,24 +365,17 @@ LOAD(LeafFaces) LOAD(Leafs) { mleaf_t *out; - int i; - uint32_t cluster, area; - uint32_t firstleafbrush, numleafbrushes; -#if USE_REF - int j; - uint32_t firstleafface, numleaffaces; -#endif ENSURE(count > 0, "Map with no leafs"); bsp->numleafs = count; - bsp->leafs = ALLOC(sizeof(*out) * count); + bsp->leafs = out = ALLOC(sizeof(*out) * count); - out = bsp->leafs; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { out->plane = NULL; out->contents = BSP_Long(); - cluster = BSP_ExtLong(); + + uint32_t cluster = BSP_ExtLong(); if (cluster == BSP_ExtNull) { // solid leafs use special -1 cluster out->cluster = -1; @@ -435,18 +388,18 @@ LOAD(Leafs) out->cluster = cluster; } - area = BSP_ExtLong(); + uint32_t area = BSP_ExtLong(); ENSURE(area < bsp->numareas, "Bad area"); out->area = area; #if USE_REF - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->mins[j] = BSP_ExtFloat(); - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->maxs[j] = BSP_ExtFloat(); - firstleafface = BSP_ExtLong(); - numleaffaces = BSP_ExtLong(); + uint32_t firstleafface = BSP_ExtLong(); + uint32_t numleaffaces = BSP_ExtLong(); ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); out->firstleafface = bsp->leaffaces + firstleafface; out->numleaffaces = numleaffaces; @@ -457,8 +410,8 @@ LOAD(Leafs) in += 16 * (bsp->extended + 1); #endif - firstleafbrush = BSP_ExtLong(); - numleafbrushes = BSP_ExtLong(); + uint32_t firstleafbrush = BSP_ExtLong(); + uint32_t numleafbrushes = BSP_ExtLong(); ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); out->firstleafbrush = bsp->leafbrushes + firstleafbrush; out->numleafbrushes = numleafbrushes; @@ -472,25 +425,19 @@ LOAD(Leafs) LOAD(Nodes) { mnode_t *out; - int i, j; - uint32_t planenum, child; -#if USE_REF - uint32_t firstface, numfaces; -#endif ENSURE(count > 0, "Map with no nodes"); bsp->numnodes = count; - bsp->nodes = ALLOC(sizeof(*out) * count); + bsp->nodes = out = ALLOC(sizeof(*out) * count); - out = bsp->nodes; - for (i = 0; i < count; i++, out++) { - planenum = BSP_Long(); + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_Long(); ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; - for (j = 0; j < 2; j++) { - child = BSP_Long(); + for (int j = 0; j < 2; j++) { + uint32_t child = BSP_Long(); if (child & BIT(31)) { child = ~child; ENSURE(child < bsp->numleafs, "Bad leafnum"); @@ -502,13 +449,13 @@ LOAD(Nodes) } #if USE_REF - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->mins[j] = BSP_ExtFloat(); - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->maxs[j] = BSP_ExtFloat(); - firstface = BSP_ExtLong(); - numfaces = BSP_ExtLong(); + uint32_t firstface = BSP_ExtLong(); + uint32_t numfaces = BSP_ExtLong(); ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); out->firstface = bsp->faces + firstface; out->numfaces = numfaces; @@ -526,29 +473,23 @@ LOAD(Nodes) LOAD(SubModels) { mmodel_t *out; - int i, j; - uint32_t headnode; -#if USE_REF - uint32_t firstface, numfaces; -#endif ENSURE(count > 0, "Map with no models"); ENSURE(count <= MAX_MODELS - 2, "Too many models"); bsp->nummodels = count; - bsp->models = ALLOC(sizeof(*out) * count); + bsp->models = out = ALLOC(sizeof(*out) * count); - out = bsp->models; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { // spread the mins / maxs by a pixel - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->mins[j] = BSP_Float() - 1; - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->maxs[j] = BSP_Float() + 1; - for (j = 0; j < 3; j++) + for (int j = 0; j < 3; j++) out->origin[j] = BSP_Float(); - headnode = BSP_Long(); + uint32_t headnode = BSP_Long(); if (headnode & BIT(31)) { // be careful, some models have no nodes, just a leaf headnode = ~headnode; @@ -563,8 +504,8 @@ LOAD(SubModels) in += 8; continue; } - firstface = BSP_Long(); - numfaces = BSP_Long(); + uint32_t firstface = BSP_Long(); + uint32_t numfaces = BSP_Long(); ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); out->firstface = bsp->faces + firstface; out->numfaces = numfaces; @@ -582,13 +523,11 @@ LOAD(SubModels) LOAD(AreaPortals) { mareaportal_t *out; - int i; bsp->numareaportals = count; - bsp->areaportals = ALLOC(sizeof(*out) * count); + bsp->areaportals = out = ALLOC(sizeof(*out) * count); - out = bsp->areaportals; - for (i = 0; i < count; i++, out++) { + for (int i = 0; i < count; i++, out++) { out->portalnum = BSP_Long(); out->otherarea = BSP_Long(); } @@ -599,18 +538,15 @@ LOAD(AreaPortals) LOAD(Areas) { marea_t *out; - int i; - uint32_t numareaportals, firstareaportal; ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); bsp->numareas = count; - bsp->areas = ALLOC(sizeof(*out) * count); + bsp->areas = out = ALLOC(sizeof(*out) * count); - out = bsp->areas; - for (i = 0; i < count; i++, out++) { - numareaportals = BSP_Long(); - firstareaportal = BSP_Long(); + for (int i = 0; i < count; i++, out++) { + uint32_t numareaportals = BSP_Long(); + uint32_t firstareaportal = BSP_Long(); ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); out->numareaportals = numareaportals; out->firstareaportal = bsp->areaportals + firstareaportal; @@ -969,7 +905,6 @@ int BSP_LoadMaterials(bsp_t *bsp) static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) { mface_t *out; - uint32_t offset; if (filelen % 40) { Com_WPrintf("DECOUPLED_LM lump has odd size\n"); @@ -986,7 +921,7 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) out->lm_width = BSP_Short(); out->lm_height = BSP_Short(); - offset = BSP_Long(); + uint32_t offset = BSP_Long(); if (offset < bsp->numlightmapbytes) out->lightmap = bsp->lightmap + offset; From 64395b080b50da0eddb09a0aa51ece282cf32184 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 27 Jul 2024 23:30:31 +0300 Subject: [PATCH 392/974] Invalidate lightmap if DECOUPLED_LM offset is out of range. --- src/common/bsp.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 859bf8e8b..ef38b0d71 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -902,16 +902,18 @@ int BSP_LoadMaterials(bsp_t *bsp) #if USE_REF +#define DECOUPLED_LM_BYTES 40 + static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) { mface_t *out; - if (filelen % 40) { + if (filelen % DECOUPLED_LM_BYTES) { Com_WPrintf("DECOUPLED_LM lump has odd size\n"); return; } - if (bsp->numfaces > filelen / 40) { + if (bsp->numfaces > filelen / DECOUPLED_LM_BYTES) { Com_WPrintf("DECOUPLED_LM lump too short\n"); return; } @@ -924,6 +926,8 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) uint32_t offset = BSP_Long(); if (offset < bsp->numlightmapbytes) out->lightmap = bsp->lightmap + offset; + else + out->lightmap = NULL; for (int j = 0; j < 2; j++) { out->lm_axis[j][0] = BSP_Float(); From 165f45222f1ef342a4081617e3294a0244237bfc Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 29 Jul 2024 15:47:14 -0400 Subject: [PATCH 393/974] Added bot chat queue system --- src/action/botlib/botlib.h | 1 + src/action/botlib/botlib_communication.c | 118 +++++++++++++++++++---- src/action/botlib/botlib_spawn.c | 16 +-- src/action/g_local.h | 6 ++ src/action/g_main.c | 3 + src/action/p_client.c | 34 +++++++ 6 files changed, 151 insertions(+), 27 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index d5bd76bd2..27eee687a 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -227,6 +227,7 @@ typedef enum CHAT_GOODBYE } bot_chat_types_t; +void UpdateBotChat(void); void BOTLIB_Wave(edict_t* ent, int type); void BOTLIB_PrecacheRadioSounds(void); void BOTLIB_AddRadioMsg(radio_t* radio, int sndIndex, int len, edict_t* from_player); diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index a04df3d52..072d26f31 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -3,6 +3,56 @@ #include "botlib.h" #include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 + +// Delayed chat, somewhat more realistic +#define MAX_MESSAGES 100 + +typedef struct { + edict_t* bot; + char text[256]; + int frameReceived; +} ChatMessage; + +typedef struct { + ChatMessage messages[MAX_MESSAGES]; + int count; +} MessageQueue; + +MessageQueue chatQueue = { .count = 0 }; + +void AddMessageToQueue(edict_t* bot, const char* text, int frameReceived) { + if (chatQueue.count < MAX_MESSAGES) { + chatQueue.messages[chatQueue.count].bot = bot; + strncpy(chatQueue.messages[chatQueue.count].text, text, sizeof(chatQueue.messages[chatQueue.count].text) - 1); + chatQueue.messages[chatQueue.count].frameReceived = frameReceived; + chatQueue.count++; + } else { + gi.dprintf("Chat queue is full, message dropped\n"); + } +} + +void ProcessChatQueue(int currentFrame) { + for (int i = 0; i < chatQueue.count; i++) { + if (currentFrame > chatQueue.messages[i].frameReceived + 40) { + // Send the chat message from the correct bot + BOTLIB_Say(chatQueue.messages[i].bot, chatQueue.messages[i].text, false); + //gi.dprintf("Sending delayed chat message from bot: %s\n", chatQueue.messages[i].text); + + // Remove the message from the queue + for (int j = i; j < chatQueue.count - 1; j++) { + chatQueue.messages[j] = chatQueue.messages[j + 1]; + } + chatQueue.count--; + i--; // Adjust index to account for removed message + } + } +} + +// Call this function periodically, e.g., in the main game loop +void UpdateBotChat() { + ProcessChatQueue(level.framenum); +} + // Borrowed from LTK bots #define DBC_WELCOMES 4 char *botchat_welcomes[DBC_WELCOMES] = @@ -54,6 +104,7 @@ char *botchat_insults[DBC_INSULTS] = void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { char* text = NULL; + qboolean delayed = true; switch (chattype) { case CHAT_WELCOME: @@ -81,7 +132,37 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { return; // Optionally, set text to a default value before calling BOTLIB_Say } - BOTLIB_Say(bot, text, false); + // Define the chat interval (e.g., once per minute) + int chatInterval = 60 * HZ; // 60 seconds * HZ (game ticks per second) + float randval = random(); + + //Check if the bot should chat + // Only applies to insults and killed messages + if (chattype == CHAT_INSULTS || chattype == CHAT_KILLED) { + if (bot->bot.lastChatTime > level.framenum - chatInterval) { + //gi.dprintf("Skipping chat due to interval limit (%i) needs to be 0 or smaller\n", (bot->bot.lastChatTime - (level.framenum - chatInterval))); + return; + } + } + if (chattype == CHAT_GOODBYE) + randval = randval - 0.3; // Increase the chance of a goodbye message + delayed = false; + if (randval > 0.2) { + //gi.dprintf("Skipping chat due to random chance (%f)\n", randval); + return; // Don't chat too often + } + + // Add the message to the queue if delayed + if (delayed) + AddMessageToQueue(bot, text, level.framenum); + else // instant message, such as a goodbye before leaving + BOTLIB_Say(bot, text, false); + + // Sets the current level time as the last chat time so the bot doesn't spam chat + bot->bot.lastChatTime = level.framenum; + + //gi.dprintf("new LastChatTime: %d\n", bot->bot.lastChatTime); + } // Bot wave gestures @@ -246,30 +327,29 @@ void BOTLIB_Say(edict_t* ent, char* pMsg, qboolean team_message) int j, /*i,*/ offset_of_text; edict_t* other; char text[2048]; - //gclient_t *cl; - if (!teamplay->value) { - if (!bot_chat->value) - return; - else - Q_snprintf(text, sizeof(text), pMsg); // Say all - } + if (teamplay->value) { - if (ent->client->resp.team == NOTEAM) - return; + if (ent->client->resp.team == NOTEAM) + return; - if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) - { - if (team_message) - Q_snprintf(text, sizeof(text), "[DEAD] (%s):", ent->client->pers.netname); // Dead, say team + if (ent->solid == SOLID_NOT || ent->deadflag == DEAD_DEAD) + { + if (team_message) + Q_snprintf(text, sizeof(text), "[DEAD] (%s): ", ent->client->pers.netname); // Dead, say team + else + Q_snprintf(text, sizeof(text), "[DEAD] %s: ", ent->client->pers.netname); // Dead, say all + } + else if (team_message) + Q_snprintf(text, sizeof(text), "(%s): ", ent->client->pers.netname); // Alive, say team else - Q_snprintf(text, sizeof(text), "[DEAD] %s:", ent->client->pers.netname); // Dead, say all + Q_snprintf(text, sizeof(text), "%s: ", ent->client->pers.netname); // Alive, say all + } else { // non-teamplay + if (bot_chat->value) { + Q_snprintf(text, sizeof(text), "%s: ", ent->client->pers.netname); + } } - else if (team_message) - Q_snprintf(text, sizeof(text), "(%s):", ent->client->pers.netname); // Alive, say team - else - Q_snprintf(text, sizeof(text), "%s:", ent->client->pers.netname); // Alive, say all offset_of_text = strlen(text); //FB 5/31/99 diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index afa3ddc69..72c22a51d 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1375,14 +1375,6 @@ void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) bot->think = BOTLIB_Think; bot->nextthink = level.framenum + 1; - //darksaint -- Bot Chat -- s - // Generates chat message if respawning (insults) - if (bot_chat->value && respawn) { - gi.dprintf("Firing off chat message---------------\n"); - BOTLIB_Chat(bot, CHAT_INSULTS); - } - //darksaint -- Bot Chat -- e - PutClientInServer(bot); JoinTeam(bot, team, true); } @@ -1508,6 +1500,13 @@ void BOTLIB_RemoveBot(char* name) { if (bot->is_bot && bot_to_kick == bot_count) { + //darksaint -- Bot Chat -- s + // Generates chat message if respawning (goodbyes) + if (bot_chat->value) { + BOTLIB_Chat(bot, CHAT_GOODBYE); + } + //darksaint -- Bot Chat -- e + //rekkie -- Fake Bot Client -- s gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -1520,6 +1519,7 @@ void BOTLIB_RemoveBot(char* name) freed = true; ClientDisconnect(bot); game.bot_count--; + //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); break; } diff --git a/src/action/g_local.h b/src/action/g_local.h index 7ca7d5227..c15e0f959 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2265,6 +2265,9 @@ typedef struct bot_s bot_ctf_state_t bot_ctf_state; // Get flag, retrieve flag, intercept flag carrier, etc. float ctf_support_time; // Time between ally support checks + // Espionage + bot_esp_state_t bot_esp_state; + // Adding walk nodes walknodes_t walknode; // Holds all the walk node data @@ -2303,6 +2306,9 @@ typedef struct bot_s char radioLastHumanMsg[48]; // Get the lastest radio call from a real player on our team // qboolean radioReportKills; // Flag if the bot reports its kills in radio and chat + // + int lastChatTime; // Last time the bot chatted + } bot_t; #endif //rekkie -- e diff --git a/src/action/g_main.c b/src/action/g_main.c index d8dc39e0a..6e2075ee5 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -823,6 +823,9 @@ void ClientEndServerFrames (void) if (timedmsgs->value) FireTimedMessages(); + + // Botlib chat + UpdateBotChat(); } /* diff --git a/src/action/p_client.c b/src/action/p_client.c index cd7f93b1a..5cddcfcf0 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -766,7 +766,31 @@ void Add_Frag(edict_t * ent, int mod) break; } } + + // A little taunting... + int wavechoice = rand() % 5; + switch (wavechoice) + { + case 0: + BOTLIB_Wave(ent, WAVE_FLIPOFF); + break; + case 1: + BOTLIB_Wave(ent, WAVE_SALUTE); + break; + case 2: + BOTLIB_Wave(ent, WAVE_TAUNT); + break; + case 3: + BOTLIB_Wave(ent, WAVE_WAVE); + break; + case 4: + BOTLIB_Wave(ent, WAVE_POINT); + break; + default: + break; + } } + // Announce kill streak to player if use_killcounts is enabled on server if (use_killcounts->value) { // Report only killstreak during that round @@ -1543,6 +1567,16 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) } } + #ifndef NO_BOTS + //darksaint -- Bot Chat -- s + // Generates chat message if respawning (killed) + if (bot_chat->value && self->is_bot) { + BOTLIB_Chat(self, CHAT_KILLED); + } + #endif + //darksaint -- Bot Chat -- e + + return; } // if(message) } From 584672619ec05bee2963caa4420fc8e9e7aba76a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 28 Jul 2024 16:25:40 +0300 Subject: [PATCH 394/974] Init texture size limits in GL_SetupConfig(). Also add max_texture_size_log2 which will be useful in the next commit. --- inc/shared/shared.h | 16 ++++++++++++++++ src/refresh/gl.h | 2 ++ src/refresh/main.c | 4 ++++ src/refresh/texture.c | 17 +++-------------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index a65084c44..060216569 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -317,6 +317,22 @@ static inline uint32_t Q_npot32(uint32_t k) return k + 1; } +static inline int Q_log2(uint32_t k) +{ +#if q_has_builtin(__builtin_clz) + return 31 - __builtin_clz(k | 1); +#elif (defined _MSC_VER) + unsigned long index; + _BitScanReverse(&index, k | 1); + return index; +#else + for (int i = 31; i > 0; i--) + if (k & BIT(i)) + return i; + return 0; +#endif +} + static inline float LerpAngle(float a2, float a1, float frac) { if (a1 - a2 > 180) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 5651585d4..c5a8d201f 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -162,6 +162,8 @@ typedef struct { int colorbits; int depthbits; int stencilbits; + int max_texture_size_log2; + int max_texture_size; } glConfig_t; extern glStatic_t gl_static; diff --git a/src/refresh/main.c b/src/refresh/main.c index 3a4e5b8c5..d876f83f4 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -989,6 +989,10 @@ static void GL_SetupConfig(void) { GLint integer = 0; + qglGetIntegerv(GL_MAX_TEXTURE_SIZE, &integer); + gl_config.max_texture_size_log2 = Q_log2(min(integer, MAX_TEXTURE_SIZE)); + gl_config.max_texture_size = 1U << gl_config.max_texture_size_log2; + gl_config.colorbits = 0; qglGetIntegerv(GL_RED_BITS, &integer); gl_config.colorbits += integer; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 27c8e732d..fbc273b33 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -30,8 +30,6 @@ static int upload_width; static int upload_height; static bool upload_alpha; -static int max_texture_size; - static cvar_t *gl_noscrap; static cvar_t *gl_round_down; static cvar_t *gl_picmip; @@ -479,7 +477,8 @@ static void GL_Upload32(byte *data, int width, int height, int baselevel, imaget } // don't ever bother with >256 textures - while (scaled_width > max_texture_size || scaled_height > max_texture_size) { + while (scaled_width > gl_config.max_texture_size || + scaled_height > gl_config.max_texture_size) { scaled_width >>= 1; scaled_height >>= 1; } @@ -574,7 +573,7 @@ static int GL_UpscaleLevel(int width, int height, imagetype_t type, imageflags_t maxlevel = Cvar_ClampInteger(gl_upscale_pcx, 0, 2); while (maxlevel) { - int maxsize = max_texture_size >> maxlevel; + int maxsize = gl_config.max_texture_size >> maxlevel; // don't bother upscaling larger than max texture size if (width <= maxsize && height <= maxsize) @@ -1020,8 +1019,6 @@ GL_InitImages */ void GL_InitImages(void) { - GLint integer = 0; - gl_bilerp_chars = Cvar_Get("gl_bilerp_chars", "0", 0); gl_bilerp_chars->changed = gl_bilerp_chars_changed; gl_bilerp_pics = Cvar_Get("gl_bilerp_pics", "1", 0); @@ -1058,14 +1055,6 @@ void GL_InitImages(void) else gl_intensity->flags |= CVAR_FILES; - qglGetIntegerv(GL_MAX_TEXTURE_SIZE, &integer); - - if (integer & (integer - 1)) { - integer = Q_npot32(integer) >> 1; - } - - max_texture_size = min(integer, MAX_TEXTURE_SIZE); - IMG_Init(); IMG_GetPalette(); From 5e46cd1f7958a04930dea405032402f21ffc0172 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 28 Jul 2024 17:08:00 +0300 Subject: [PATCH 395/974] Clamp lightmap block size to maximum texture size. --- src/refresh/surf.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 692360de1..eb93cf45c 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -415,7 +415,7 @@ static void LM_BeginBuilding(void) return; // use larger lightmaps for DECOUPLED_LM maps - bits = 8 + bsp->lm_decoupled * 2; + bits = min(8 + bsp->lm_decoupled * 2, gl_config.max_texture_size_log2); lm.block_size = 1 << bits; lm.block_shift = bits + 2; @@ -426,7 +426,7 @@ static void LM_BeginBuilding(void) for (int i = 0; i < lm.maxmaps; i++) lm.lightmaps[i].buffer = lm.buffer + (i << size_shift); - Com_DDPrintf("%s: %d lightmaps, %d block size\n", __func__, lm.maxmaps, lm.block_size); + Com_DPrintf("%s: %d lightmaps max, %d block size\n", __func__, lm.maxmaps, lm.block_size); LM_InitBlock(); } From 9eaf4556422c699ffe0584988e2a10fe9db64854 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 29 Jul 2024 02:17:25 +0300 Subject: [PATCH 396/974] Allocate world vertices on heap if not using VBO. --- src/refresh/gl.h | 1 - src/refresh/surf.c | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index c5a8d201f..b5aa0ae25 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -90,7 +90,6 @@ typedef struct { glbackend_t backend; struct { bsp_t *cache; - memhunk_t hunk; vec_t *vertices; GLuint bufnum; vec_t size; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index eb93cf45c..5e8c74c53 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -972,7 +972,7 @@ void GL_FreeWorld(void) BSP_Free(gl_static.world.cache); if (gl_static.world.vertices) { - Hunk_Free(&gl_static.world.hunk); + Z_Free(gl_static.world.vertices); } else if (qglDeleteBuffers) { qglDeleteBuffers(1, &gl_static.world.bufnum); } @@ -1042,15 +1042,12 @@ void GL_LoadWorld(const char *name) size += surf->numsurfedges * VERTEX_SIZE * sizeof(vec_t); } - // try VBO first, then allocate on hunk + // try VBO first, then allocate on heap if (create_surface_vbo(size)) { Com_DPrintf("%s: %zu bytes of vertex data as VBO\n", __func__, size); } else { - Hunk_Begin(&gl_static.world.hunk, size); - gl_static.world.vertices = Hunk_Alloc(&gl_static.world.hunk, size); - Hunk_End(&gl_static.world.hunk); - - Com_DPrintf("%s: %zu bytes of vertex data on hunk\n", __func__, size); + gl_static.world.vertices = Z_TagMalloc(size, TAG_RENDERER); + Com_DPrintf("%s: %zu bytes of vertex data on heap\n", __func__, size); } gl_static.nolm_mask = SURF_NOLM_MASK_DEFAULT; From cb0dd70d8db188b53f7b6c89dfe4975de42a7356 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 29 Jul 2024 15:21:03 +0300 Subject: [PATCH 397/974] Change LM_MAX_LIGHTMAPS back to 128. If maximum texture size is 512 or lower then 32 lightmaps may not be enough for DECOUPLED_LM maps. Set it back to 128 like it was before commit b3286ea5e. --- src/refresh/gl.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index b5aa0ae25..610bef1d4 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -425,12 +425,12 @@ qhandle_t R_RegisterModel(const char *name); #define LIGHT_STYLE(i) \ &glr.fd.lightstyles[gl_static.lightstylemap[(i)]] -#define LM_MAX_LIGHTMAPS 32 -#define LM_BLOCK_WIDTH (1 << 10) +#define LM_MAX_LIGHTMAPS 128 +#define LM_MAX_BLOCK_WIDTH (1 << 10) typedef struct lightmap_s { - int mins[2]; - int maxs[2]; + uint16_t mins[2]; + uint16_t maxs[2]; byte *buffer; } lightmap_t; @@ -439,7 +439,7 @@ typedef struct { int comp, block_size, block_shift; float add, modulate, scale; int nummaps, maxmaps; - int inuse[LM_BLOCK_WIDTH]; + int inuse[LM_MAX_BLOCK_WIDTH]; GLuint texnums[LM_MAX_LIGHTMAPS]; lightmap_t lightmaps[LM_MAX_LIGHTMAPS]; byte buffer[0x4000000]; From 21b51d9f4e60c172e589c9c3cb080abd5dbf55c5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 29 Jul 2024 20:37:02 +0300 Subject: [PATCH 398/974] Avoid using magic constants for 2D drawing flags. --- src/refresh/draw.c | 8 ++++---- src/refresh/tess.c | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 396539387..b48b011d7 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -59,14 +59,14 @@ static inline void GL_StretchPic_( if (flags & IF_TRANSPARENT) { if ((flags & IF_PALETTED) && draw.scale == 1) { - tess.flags |= 1; + tess.flags |= GLS_ALPHATEST_ENABLE; } else { - tess.flags |= 2; + tess.flags |= GLS_BLEND_BLEND; } } if ((color & U32_ALPHA) != U32_ALPHA) { - tess.flags |= 2; + tess.flags |= GLS_BLEND_BLEND; } tess.numverts += 4; @@ -161,7 +161,7 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) dst_indices[22] = tess.numverts + 4; dst_indices[23] = tess.numverts + 7; - tess.flags |= 2; + tess.flags |= GLS_BLEND_BLEND; tess.numverts += 8; tess.numindices += 24; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 74e57595a..4799ae07d 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -36,11 +36,9 @@ void GL_Flush2D(void) return; } - bits = GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE; - if (tess.flags & 2) { - bits |= GLS_BLEND_BLEND; - } else if (tess.flags & 1) { - bits |= GLS_ALPHATEST_ENABLE; + bits = GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | tess.flags; + if (bits & GLS_BLEND_BLEND) { + bits &= ~GLS_ALPHATEST_ENABLE; } Scrap_Upload(); From 08baae315e382b3430edf8656f6042d28ebf7e12 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 01:03:24 +0300 Subject: [PATCH 399/974] Reduce scope of some variables. --- src/refresh/main.c | 13 +++++-------- src/refresh/tess.c | 15 ++++----------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index d876f83f4..7177bc9d6 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1016,19 +1016,16 @@ static void GL_SetupConfig(void) static void GL_InitTables(void) { - vec_t lat, lng; - const vec_t *v; - int i; + for (int i = 0; i < NUMVERTEXNORMALS; i++) { + const vec_t *v = bytedirs[i]; + float lat = acosf(v[2]); + float lng = atan2f(v[1], v[0]); - for (i = 0; i < NUMVERTEXNORMALS; i++) { - v = bytedirs[i]; - lat = acosf(v[2]); - lng = atan2f(v[1], v[0]); gl_static.latlngtab[i][0] = (int)(lat * (255 / (2 * M_PIf))) & 255; gl_static.latlngtab[i][1] = (int)(lng * (255 / (2 * M_PIf))) & 255; } - for (i = 0; i < 256; i++) { + for (int i = 0; i < 256; i++) { gl_static.sintab[i] = sinf(i * (2 * M_PIf / 255)); } } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 4799ae07d..a5e6cdda6 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -457,20 +457,15 @@ static void GL_DrawFace(const mface_t *surf) void GL_ClearSolidFaces(void) { - int i; - - for (i = 0; i < FACE_HASH_SIZE; i++) { + for (int i = 0; i < FACE_HASH_SIZE; i++) { faces_next[i] = &faces_head[i]; } } void GL_DrawSolidFaces(void) { - mface_t *face; - int i; - - for (i = 0; i < FACE_HASH_SIZE; i++) { - for (face = faces_head[i]; face; face = face->next) { + for (int i = 0; i < FACE_HASH_SIZE; i++) { + for (const mface_t *face = faces_head[i]; face; face = face->next) { GL_DrawFace(face); } faces_head[i] = NULL; @@ -479,8 +474,6 @@ void GL_DrawSolidFaces(void) void GL_DrawAlphaFaces(void) { - mface_t *face; - if (!faces_alpha) { return; } @@ -489,7 +482,7 @@ void GL_DrawAlphaFaces(void) GL_BindArrays(); - for (face = faces_alpha; face; face = face->next) { + for (const mface_t *face = faces_alpha; face; face = face->next) { if (glr.ent != face->entity) { glr.ent = face->entity; GL_Flush3D(); From 1538dcbf52ce4b55fd57702fc72436690818045f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 01:10:41 +0300 Subject: [PATCH 400/974] =?UTF-8?q?Use=20uint16=5Ft=20for=20block=20alloca?= =?UTF-8?q?tor=20=E2=80=98inuse=E2=80=99=20array.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/refresh/gl.h | 4 ++-- src/refresh/main.c | 2 +- src/refresh/texture.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 610bef1d4..9b8504950 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -250,7 +250,7 @@ glCullResult_t GL_CullBox(const vec3_t bounds[2]); glCullResult_t GL_CullSphere(const vec3_t origin, float radius); glCullResult_t GL_CullLocalBox(const vec3_t origin, const vec3_t bounds[2]); -bool GL_AllocBlock(int width, int height, int *inuse, +bool GL_AllocBlock(int width, int height, uint16_t *inuse, int w, int h, int *s, int *t); void GL_MultMatrix(GLfloat *restrict out, const GLfloat *restrict a, const GLfloat *restrict b); @@ -439,7 +439,7 @@ typedef struct { int comp, block_size, block_shift; float add, modulate, scale; int nummaps, maxmaps; - int inuse[LM_MAX_BLOCK_WIDTH]; + uint16_t inuse[LM_MAX_BLOCK_WIDTH]; GLuint texnums[LM_MAX_LIGHTMAPS]; lightmap_t lightmaps[LM_MAX_LIGHTMAPS]; byte buffer[0x4000000]; diff --git a/src/refresh/main.c b/src/refresh/main.c index 7177bc9d6..1ad8dcaf9 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -221,7 +221,7 @@ glCullResult_t GL_CullLocalBox(const vec3_t origin, const vec3_t bounds[2]) } // shared between lightmap and scrap allocators -bool GL_AllocBlock(int width, int height, int *inuse, +bool GL_AllocBlock(int width, int height, uint16_t *inuse, int w, int h, int *s, int *t) { int i, j, k, x, y, max_inuse, min_inuse; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index fbc273b33..2b669816c 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -261,7 +261,7 @@ static void IMG_MipMap(byte *out, const byte *in, int width, int height) #define SCRAP_BLOCK_WIDTH 256 #define SCRAP_BLOCK_HEIGHT 256 -static int scrap_inuse[SCRAP_BLOCK_WIDTH]; +static uint16_t scrap_inuse[SCRAP_BLOCK_WIDTH]; static byte scrap_data[SCRAP_BLOCK_WIDTH * SCRAP_BLOCK_HEIGHT * 4]; static bool scrap_dirty; From c7f4db6f30f8e14a3b64fbf4c173067e5a3e5015 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 11:58:15 +0300 Subject: [PATCH 401/974] Use fixed width integer types for maliasvert_t. --- src/refresh/gl.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 9b8504950..89a9bb2e4 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -284,8 +284,8 @@ typedef struct { } maliastc_t; typedef struct { - short pos[3]; - byte norm[2]; // lat, lng + int16_t pos[3]; + uint8_t norm[2]; // lat, lng } maliasvert_t; typedef struct { From 34885f4f3b039bae7d05499ba9ec195e79e5f138 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 22:13:31 +0300 Subject: [PATCH 402/974] Rename some image loading functions for clarity. --- src/refresh/images.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 680ee211f..0b4500139 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1626,7 +1626,7 @@ static image_t *lookup_image(const char *name, return NULL; } -static int try_image_format_(imageformat_t fmt, image_t *image, byte **pic) +static int try_image_format(imageformat_t fmt, image_t *image, byte **pic) { byte *data; int ret; @@ -1645,16 +1645,15 @@ static int try_image_format_(imageformat_t fmt, image_t *image, byte **pic) return ret < 0 ? ret : fmt; } -static int try_image_format(imageformat_t fmt, image_t *image, byte **pic) +#if USE_PNG || USE_JPG || USE_TGA + +static int try_replace_ext(imageformat_t fmt, image_t *image, byte **pic) { // replace the extension memcpy(image->name + image->baselen + 1, img_loaders[fmt].ext, 4); - return try_image_format_(fmt, image, pic); + return try_image_format(fmt, image, pic); } - -#if USE_PNG || USE_JPG || USE_TGA - // tries to load the image with a different extension static int try_other_formats(imageformat_t orig, image_t *image, byte **pic) { @@ -1668,7 +1667,7 @@ static int try_other_formats(imageformat_t orig, image_t *image, byte **pic) continue; // don't retry twice } - ret = try_image_format(fmt, image, pic); + ret = try_replace_ext(fmt, image, pic); if (ret != Q_ERR(ENOENT)) { return ret; // found something } @@ -1680,7 +1679,7 @@ static int try_other_formats(imageformat_t orig, image_t *image, byte **pic) return Q_ERR(ENOENT); // don't retry twice } - return try_image_format(fmt, image, pic); + return try_replace_ext(fmt, image, pic); } static void get_image_dimensions(imageformat_t fmt, image_t *image) @@ -1813,7 +1812,7 @@ static void print_error(const char *name, imageflags_t flags, int err) Com_LPrintf(level, "Couldn't load %s: %s\n", name, msg); } -static int load_image_data(image_t *image, imageformat_t fmt, bool check_dimensions, byte **pic) +static int load_image_data(image_t *image, imageformat_t fmt, bool need_dimensions, byte **pic) { int ret; @@ -1830,7 +1829,7 @@ static int load_image_data(image_t *image, imageformat_t fmt, bool check_dimensi ret = try_other_formats(IM_MAX, image, pic); } else { // first try with original extension - ret = try_image_format_(fmt, image, pic); + ret = try_image_format(fmt, image, pic); if (ret == Q_ERR(ENOENT)) { // retry with remaining extensions ret = try_other_formats(fmt, image, pic); @@ -1839,14 +1838,14 @@ static int load_image_data(image_t *image, imageformat_t fmt, bool check_dimensi // if we are replacing 8-bit texture with a higher resolution 32-bit // texture, we need to recover original image dimensions - if (check_dimensions && fmt <= IM_WAL && ret > IM_WAL) { + if (need_dimensions && fmt <= IM_WAL && ret > IM_WAL) { get_image_dimensions(fmt, image); } #else if (fmt == IM_MAX) { ret = Q_ERR_INVALID_PATH; } else { - ret = try_image_format_(fmt, image, pic); + ret = try_image_format(fmt, image, pic); } #endif From 3e37b76803f4507e2eeb987fe2d94ef3afb20204 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 1 Aug 2024 00:20:35 -0400 Subject: [PATCH 403/974] Node list clearing --- src/action/acesrc/acebot_nodes.c | 2 +- src/action/botlib/botlib_nav.c | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index c1352aff0..98c462efa 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -2650,7 +2650,7 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.bi.speed = 100; //self->bot.bi.actionflags = 0; - //Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); self->bot.node_travel_time = 0; ////if (nav_area.total_areas > 0) diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index e678b5e6c..dd26c7a16 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -826,7 +826,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz else // Fallback to old pathing { self->bot.goal_node = INVALID; - //gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); + gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) { // Add the goal to the end of the node list + terminate @@ -840,7 +840,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz } } - //Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); + Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); return false; // Failure to find path } @@ -2247,7 +2247,8 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando //Com_Printf("%s EXIT PATH\n\n", __func__); // Each time a path is found, make a copy - //ent->bot.node_list_count = 0; + ent->bot.node_list_count = 0; + memset(ent->bot.node_list, 0, sizeof(ent->bot.node_list)); // Ensure the node list is cleared ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list ent->bot.node_list_current = 0; if (ent->bot.node_list_count) // Set the current and next nodes @@ -2257,7 +2258,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando } ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path - if (0) + if (1) { if (1) { From 85046b9ffaeb36c91fc4536ebc2f1a44bdbf3bbf Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 22:16:32 +0300 Subject: [PATCH 404/974] Escape unprintable characters in error messages. --- src/refresh/images.c | 2 +- src/refresh/models.c | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index 0b4500139..c93a03a8b 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1809,7 +1809,7 @@ static void print_error(const char *name, imageflags_t flags, int err) break; } - Com_LPrintf(level, "Couldn't load %s: %s\n", name, msg); + Com_LPrintf(level, "Couldn't load %s: %s\n", Com_MakePrintable(name), msg); } static int load_image_data(image_t *image, imageformat_t fmt, bool need_dimensions, byte **pic) diff --git a/src/refresh/models.c b/src/refresh/models.c index a02d2d434..383676ebe 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -706,7 +706,7 @@ static int MOD_LoadMD3(model_t *model, const void *rawdata, size_t length) static void MOD_PrintError(const char *path, int err) { - Com_EPrintf("Couldn't load %s: %s\n", path, + Com_EPrintf("Couldn't load %s: %s\n", Com_MakePrintable(path), err == Q_ERR_INVALID_FORMAT ? Com_GetLastError() : Q_ErrorString(err)); } @@ -720,7 +720,7 @@ static bool MD5_ParseExpect(const char **buffer, const char *expect) char *token = COM_Parse(buffer); if (strcmp(token, expect)) { - Com_SetLastError(va("Expected %s, got %s", expect, token)); + Com_SetLastError(va("Expected %s, got %s", expect, Com_MakePrintable(token))); return false; } @@ -734,7 +734,7 @@ static bool MD5_ParseFloat(const char **buffer, float *output) *output = strtof(token, &endptr); if (endptr == token || *endptr) { - Com_SetLastError(va("Expected float, got %s", token)); + Com_SetLastError(va("Expected float, got %s", Com_MakePrintable(token))); return false; } @@ -748,7 +748,7 @@ static bool MD5_ParseUint(const char **buffer, uint32_t *output) *output = strtoul(token, &endptr, 10); if (endptr == token || *endptr) { - Com_SetLastError(va("Expected int, got %s", token)); + Com_SetLastError(va("Expected int, got %s", Com_MakePrintable(token))); return false; } @@ -1121,7 +1121,7 @@ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t } if (joint_id == -1) - Com_WPrintf("No such joint %s in %s\n", tok, path); + Com_WPrintf("No such joint %s in %s\n", Com_MakePrintable(tok), path); MD5_EXPECT(":"); MD5_EXPECT("{"); From eee5a0dcb4840d88f02ddd136639a2a009c0025a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 22:19:47 +0300 Subject: [PATCH 405/974] Add direct image loading flag for testing. --- inc/refresh/refresh.h | 1 + src/common/tests.c | 2 +- src/refresh/images.c | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 2418477ff..83336dd44 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -148,6 +148,7 @@ typedef enum { // not stored in image IF_OPTIONAL = BIT(16), + IF_DIRECT = BIT(17), } imageflags_t; typedef enum { diff --git a/src/common/tests.c b/src/common/tests.c index 596720e4d..b6166e01d 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -598,7 +598,7 @@ static void Com_TestImages_f(void) R_EndRegistration(); R_BeginRegistration(NULL); } - if (!R_RegisterTempPic(va("/%s", (char *)list[i]))) { + if (!R_RegisterImage(va("/%s", (char *)list[i]), IT_PIC, IF_DIRECT)) { errors++; continue; } diff --git a/src/refresh/images.c b/src/refresh/images.c index c93a03a8b..a8ea172cf 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1962,7 +1962,15 @@ static image_t *find_or_load_image(const char *name, size_t len, // load the pic from disk pic = NULL; - ret = load_image_data(image, fmt, true, &pic); + if (flags & IF_DIRECT) { + // direct load requested (for testing code) + if (fmt == IM_MAX) + ret = Q_ERR_INVALID_PATH; + else + ret = try_image_format(fmt, image, &pic); + } else { + ret = load_image_data(image, fmt, true, &pic); + } if (ret < 0) { print_error(image->name, flags, ret); From 38ce3afdd093aae34efbc80e652bfa067bc9c911 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 22:20:26 +0300 Subject: [PATCH 406/974] Document supported image flags. --- inc/refresh/refresh.h | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 83336dd44..d4b8bd046 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -136,19 +136,19 @@ typedef struct { typedef enum { IF_NONE = 0, - IF_PERMANENT = BIT(0), - IF_TRANSPARENT = BIT(1), - IF_PALETTED = BIT(2), - IF_UPSCALED = BIT(3), - IF_SCRAP = BIT(4), - IF_TURBULENT = BIT(5), - IF_REPEAT = BIT(6), - IF_NEAREST = BIT(7), - IF_OPAQUE = BIT(8), + IF_PERMANENT = BIT(0), // not freed by R_EndRegistration() + IF_TRANSPARENT = BIT(1), // known to be transparent + IF_PALETTED = BIT(2), // loaded from 8-bit paletted format + IF_UPSCALED = BIT(3), // upscaled + IF_SCRAP = BIT(4), // put in scrap texture + IF_TURBULENT = BIT(5), // turbulent surface (don't desaturate, etc) + IF_REPEAT = BIT(6), // tiling image + IF_NEAREST = BIT(7), // don't bilerp + IF_OPAQUE = BIT(8), // known to be opaque // not stored in image - IF_OPTIONAL = BIT(16), - IF_DIRECT = BIT(17), + IF_OPTIONAL = BIT(16), // don't warn if not found + IF_DIRECT = BIT(17), // don't override extension } imageflags_t; typedef enum { From ec3b9445dae7271a589bd0d9600d5b071ed2ab22 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 30 Jul 2024 22:21:40 +0300 Subject: [PATCH 407/974] Strip trailing slashes from paths when normalizing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes ‘dir some/dir/’ command work as expected in packfiles. --- src/common/files.c | 13 +++++++++---- src/common/tests.c | 11 ++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index 73b20e5ec..076169a91 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -300,12 +300,13 @@ void FS_CleanupPath(char *s) FS_NormalizePathBuffer Simplifies the path, converting backslashes to slashes and removing ./ and ../ -components, as well as duplicated slashes. Any leading slashes are also skipped. -Return value == size signifies overflow. +components, as well as duplicated slashes. Any leading/trailing slashes are +also stripped. Return value == size signifies overflow. May operate in place if in == out. ///foo -> foo + foo/ -> foo foo\bar -> foo/bar foo/.. -> foo/../bar -> bar @@ -339,7 +340,7 @@ size_t FS_NormalizePathBuffer(char *out, const char *in, size_t size) if (c == 0) break; if (out > start) - // save the slash + // keep the slash out++; } pre = '/'; @@ -360,8 +361,12 @@ size_t FS_NormalizePathBuffer(char *out, const char *in, size_t size) } if ((pre & 0xff) == '/') { - if (c == 0) + if (c == 0) { + if (out > start) + // eat the slash + out--; break; + } continue; } diff --git a/src/common/tests.c b/src/common/tests.c index b6166e01d..ae714bcdb 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -252,7 +252,7 @@ typedef struct { static const normtest_t normtests[] = { { "", "", }, { "///", "", }, - { "foo///", "foo/", }, + { "foo///", "foo", }, { "\\/\\", "", }, { "///foo", "foo" }, { "\\/foo", "foo" }, @@ -272,9 +272,10 @@ static const normtest_t normtests[] = { { "./../../foo", "foo" }, { "../bar/../foo", "foo" }, { "foo/bar/..", "foo" }, - { "foo/bar/../", "foo/" }, + { "foo/bar/../", "foo" }, + { "foo/bar//..//", "foo" }, { "foo/bar/.", "foo/bar" }, - { "foo/bar/./", "foo/bar/" }, + { "foo/bar/./", "foo/bar" }, { "..", "" }, { ".", "" }, { "/..", "" }, @@ -285,8 +286,8 @@ static const normtest_t normtests[] = { { "/./", "" }, { "../..", "" }, { "../../../../", "" }, - { "../foo..bar/", "foo..bar/" }, - { "......./", "......./" }, + { "../foo..bar/", "foo..bar" }, + { "......./", "......." }, { "foo/bar/baz/abc/../def", "foo/bar/baz/def" }, { "foo/bar/baz/abc/../../def", "foo/bar/def" }, From 08201437d0ba81feeb378ef64341b1e01910c119 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 1 Aug 2024 17:11:24 -0400 Subject: [PATCH 408/974] More cleanup, next_node and current_node are messed up --- meson.build | 4 +- src/action/acesrc/acebot.h | 20 +-------- src/action/acesrc/acebot_ai.c | 70 ++++------------------------- src/action/acesrc/acebot_movement.c | 36 +-------------- src/action/acesrc/acebot_nodes.c | 66 +++++++++++++-------------- src/action/botlib/botlib_ai.c | 8 ++-- src/action/botlib/botlib_ctf.c | 1 - src/action/botlib/botlib_movement.c | 22 +++------ src/action/botlib/botlib_nav.c | 12 ++--- src/action/botlib/botlib_nodes.c | 11 +++-- src/action/p_client.c | 5 +-- 11 files changed, 65 insertions(+), 190 deletions(-) diff --git a/meson.build b/meson.build index c0f945e33..0944e76db 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=debugoptimized', + 'buildtype=debug', ], ) @@ -339,6 +339,8 @@ endif add_project_arguments(common_args, language: 'c') add_project_link_arguments(common_link_args, language: 'c') +# add_project_arguments('-fsanitize=address', language: 'c') +# add_project_link_arguments('-fsanitize=address', language: 'c') config = configuration_data() diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index b015957e2..359570c6e 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -258,7 +258,7 @@ extern node_t *unsorted_nodes; // Used to generate all links, so they can be sor #define MAX_PNODES 8096 //8096 //32768 Absolute max nodes //extern node_t *nodes; -extern node_t *nodes; +//extern node_t *nodes; //node_t *nodes[MAX_PNODES]; //node_t nodes[MAX_PNODES]; //extern node_t nodes[MAX_PNODES]; @@ -656,18 +656,7 @@ typedef struct nmesh_s { unsigned bsp_checksum; // Map checksum } nmesh_t; -extern nmesh_t nmesh; - - -extern int num_poi_nodes; -extern int poi_nodes[MAX_POI_NODES]; -extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -extern int num_vis_nodes; -extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -extern int node_vis_list[10][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. -//int node_vis[MAX_PNODES][MAX_PNODES]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -//int node_vis_list[MAX_PNODES][MAX_VIS_NODES]; // Cached node visibility list. node_vis_list[X][list-of-nodes-x-can-see] <-- All the nodes that X can see. - +//extern nmesh_t nmesh; // Botlib A_TEAM //void CheckBotRules(void); @@ -836,9 +825,4 @@ void ThreadUnlock(void); extern int num_poi_nodes; extern int poi_nodes[MAX_POI_NODES]; extern edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -extern int num_vis_nodes; -extern int node_vis[10][10]; // Cached node visibily. node_vis[X][Y] <-- can X see Y? If Y == INVALID, then false. Otherwise Y == NODE NUM -extern int node_vis_list[10][MAX_VIS_NODES]; // Cached node -extern node_t *nodes; -extern nmesh_t nmesh; #endif diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 2f095954e..b35983853 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -122,12 +122,10 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) if (nodelist[i] != INVALID) // Make sure we can visit the spawn point node { current_node = nodelist[i]; - self->bot.current_node = nodelist[i]; break; } } */ - //Com_Printf("%s %s [%d] self->bot.current_node[%d]\n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node); //current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); @@ -152,55 +150,11 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) return; } - /* - if (self->bot.state == STATE_ATTACK) - { - if (self->enemy && ACEAI_IsEnemy(self, self->enemy)) // Are they our enemy - { - // Get enemy node - int n = self->enemy->bot.current_node; // This is for bots and humans. For humans their current node is now updated in p_client.c ClientThink() - if (BOTLIB_CanVisitNode(self, n)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s visiting enemy %s node %i at %f %f %f\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); - BOTLIB_SetGoal(self, n); - } - } - - self->bot.state = BOT_MOVE_STATE_MOVE; - return; - } - */ - //======================= // Get navigation //======================= //if (self->bot.state == BOT_MOVE_STATE_NAV) { - //Com_Printf("%s %s [%d] self->bot.state == BOT_MOVE_STATE_NAV\n", __func__, self->client->pers.netname, level.framenum); - - /* - if (0 && random() < 0.1 || (teamplay->value && lights_camera_action)) // Greater than LCA! - { - i = rand() % numnodes; // pick a random node - if (BOTLIB_CanVisitNode(self, nodes[i].nodenum)) - { - //Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); - self->bot.state = BOT_MOVE_STATE_MOVE; - //self->tries = 0; // Reset the count of how many times we tried this goal - BOTLIB_SetGoal(self, nodes[i].nodenum); - //self->wander_timeout = level.framenum + 1.0 * HZ; - return; - } - - { - //Com_Printf("%s %s BOT_MOVE_STATE_NAV [%d]\n", __func__, self->client->pers.netname, level.framenum); - self->bot.goal_node = INVALID; - self->bot.state = BOT_MOVE_STATE_WANDER; // BOT_MOVE_STATE_WANDER - //self->wander_timeout = level.framenum + 1.0 * HZ; - return; // no path? - } - } - */ //self->bot.get_item = NULL; //if (BOTLIB_NeedWeaponOrAmmo(self)) @@ -245,7 +199,6 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) { if (players[p]->is_bot && players[p]->bot.goal_node == poi_nodes[i] && players[p]->solid == SOLID_BBOX) { - //self->bot.goal_node = INVALID; //self->bot.state = BOT_MOVE_STATE_NAV; } } @@ -304,7 +257,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) byte spot_picked = rand() % sp_counter; // Pick a random spot // Check if spawn point location touches any nodes - nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[spot_picked], tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[spot_picked], vec3_origin, 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); for (i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched { if (BOTLIB_CanGotoNode(self, nodelist[i], true)) // Make sure we can visit the spawn point node @@ -358,7 +311,6 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) if (n == INVALID) continue; - //if (BOTLIB_CanVisitNode(self, n, rand() % 2)) // Make sure we can visit the node they're at if (BOTLIB_CanGotoNode(self, n, true)) { //if (debug_mode) @@ -372,13 +324,13 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } } - // Random node - int retries = 0; - while (retries < max_random_retries) - { - if (!BOTLIB_ChooseRandomNode(self, 128)) - retries++; - } + // // Random node + // int retries = 0; + // while (retries < max_random_retries) + // { + // if (!BOTLIB_ChooseRandomNode(self, 128)) + // retries++; + // } } //Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); @@ -477,7 +429,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) if (spot != NULL) { i = ACEND_FindClosestReachableNode(spot, NODE_DENSITY, NODE_ALL); - if (BOTLIB_CanVisitNode(self, i)) // Make sure we can visit the node they're at { if (debug_mode) Com_Printf("%s %s visiting spawn spot node[%i] [%f %f %f]\n", __func__, self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); @@ -505,7 +456,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) if (n == INVALID) continue; - if (BOTLIB_CanVisitNode(self, n)) // Make sure we can visit the node they're at { if (debug_mode) Com_Printf("%s %s visiting friendly %s node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2]); @@ -546,7 +496,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) else if (num_vis_nodes) // Otherwise try to find a visibility node that has sight on the player node { int sn = ACEND_GetRandomVisibleNode(n); - if (BOTLIB_CanVisitNode(self, sn)) { if (debug_mode) Com_Printf("%s %s visiting enemy %s [LOS] to node %i from node %i at %f %f %f\n", __func__, self->client->pers.netname, players[i]->client->pers.netname, n, sn, nodes[sn].origin[0], nodes[sn].origin[1], nodes[sn].origin[2]); @@ -569,7 +518,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) i = (int)(random() * num_poi_nodes - 1); // Any of the POI nodes will do if (poi_nodes[i] != INVALID) { - if (BOTLIB_CanVisitNode(self, i)) { if (debug_mode) debug_printf("%s %s visiting [POI] node %i at %f %f %f\n", __func__, self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); @@ -596,7 +544,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) if (nodes[i].inuse == false) continue; // Don't go to nodes not in use //rekkie -- DEV_1 -- s - if (BOTLIB_CanVisitNode(self, i) == false) { cost = INVALID; i = INVALID; @@ -797,7 +744,6 @@ void ACEAI_PickLongRangeGoal(edict_t *self) while (tries < 256) { i = (int)(random() * numnodes - 1); // Any of the POI nodes will do - if (BOTLIB_CanVisitNode(self, i)) { if (debug_mode) debug_printf("%s did not find a LR goal, visiting random node %i at %f %f %f\n", self->client->pers.netname, i, nodes[i].origin[0], nodes[i].origin[1], nodes[i].origin[2]); diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index 4e1251932..1539e1b28 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -643,7 +643,6 @@ void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) } else { - if (self->current_node != INVALID && self->bot.next_node != INVALID) // rekkie -- safey check because current / next node can be INVALID { // Are we there yet? vec3_t v; @@ -652,12 +651,7 @@ void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) if( nodes[self->bot.current_node].type == NODE_DOOR ) { - if( nodes[self->bot.next_node].type == NODE_DOOR ) - { - VectorCopy( nodes[self->current_node].origin, vStart ); - VectorCopy( nodes[self->next_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->bot.next_node].origin, v ); - } + else { VectorCopy( self->s.origin, vStart ); @@ -716,9 +710,6 @@ void ACEMV_MoveToGoal(edict_t *self, usercmd_t *ucmd) else { // If trace from bot to next node hits rotating door, it should just strafe toward the path. - VectorCopy( self->s.origin, vStart ); - VectorCopy( nodes[self->bot.next_node].origin, vDest ); - VectorSubtract( self->s.origin, nodes[self->next_node].origin, v ); if( VectorLength(v) < 32 ) { PRETRACE(); @@ -945,7 +936,6 @@ void ACEMV_Move(edict_t *self, usercmd_t *ucmd) } if (self->groundentity && curr_node != self->next_node) { - if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal { Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->goal_node); @@ -1362,7 +1352,6 @@ void ACEMV_Move(edict_t *self, usercmd_t *ucmd) else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump { self->current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal { Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->goal_node); @@ -1473,7 +1462,6 @@ void ACEMV_Move(edict_t *self, usercmd_t *ucmd) self->wander_timeout = level.framenum + (random() + 0.5) * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal { if (debug_mode) Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->goal_node); @@ -2403,7 +2391,6 @@ void ACEMV_Move(edict_t *self, usercmd_t *ucmd) self->wander_timeout = level.framenum + (random() + 0.5) * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->goal_node)) // Try to find another way to our goal { Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->goal_node); @@ -2845,24 +2832,6 @@ void BOTLIB_GetWeaponsAndAmmo(edict_t* self) if (item_node != INVALID) { self->goalentity = item; - - //Com_Printf("%s %s heading for item: %s\n", __func__, self->client->pers.netname, self->goalentity->classname); - - /* - if (self->bot.prev_node != self->bot.goal_node) - self->bot.prev_node = self->bot.goal_node; // Keep a copy of previous goal - self->bot.goal_node = item_node; // Make the item node our new goal - - // Try visit node near item - // Otherwise visit previous goal node - if (BOTLIB_CanGotoNode(self, self->bot.goal_node) == false) // If we cannot visit item node - { - self->bot.goal_node = self->bot.prev_node; // Restore previous goal - BOTLIB_CanGotoNode(self, self->bot.goal_node); // Try go back to previous goal - } - */ - - //BOTLIB_CanGotoNode(self, self->bot.goal_node, false); // Try go to item node } } } @@ -3096,7 +3065,6 @@ void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) // If player has no weapon, keep moving! if (bHasWeapon == false) { - //Com_Printf("%s %s bHasWeapon %d self->bot.goal_node %d\n", __func__, self->client->pers.netname, bHasWeapon, self->bot.goal_node); BOTLIB_MOV_Move(self, ucmd); // Keep moving } //rekkie -- DEV_1 -- e @@ -3327,7 +3295,6 @@ void ACEMV_Attack (edict_t *self, usercmd_t *ucmd) c = random(); if (c < 0.01) { - //if (BOTLIB_CanVisitNode(self, nodes[self->current_node].links[i].targetNode)) // Make sure we can visit node { self->state = STATE_MOVE; self->tries = 0; // Reset the count of how many times we tried this goal @@ -3548,7 +3515,6 @@ qboolean AntPathMove( edict_t *self ) { // Failed to find a path // gi.bprintf(PRINT_HIGH,"%s: Target at(%i) - No Path \n", -// self->client->pers.netname, self->bot.goal_node, self->bot.next_node); return false; } return true; diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 98c462efa..64247948a 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -268,7 +268,6 @@ int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) /* // Try to see if bot is close to the next node - if (self->bot.next_node != INVALID && self->bot.next_node == i && VectorDistance(nodes[i].origin, self->s.origin) <= 64) { tr = gi.trace(self->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, self, MASK_PLAYERSOLID); //rekkie if ((tr.fraction == 1.0) || @@ -276,7 +275,6 @@ int ACEND_FindClosestReachableNode(edict_t *self, int range, int type) && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) ) { - //Com_Printf("%s -> [%s] is close to next_node[%d]\n", __func__, self->client->pers.netname, node); //if (node != INVALID) return node; } @@ -400,7 +398,7 @@ void ACEND_SetGoal(edict_t *self, int goal_node) int nodelist[MAX_NODELIST]; int nodes_touched; - nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, vec3_origin, 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched { if (nodelist[i] != INVALID) @@ -452,7 +450,6 @@ qboolean BOTLIB_AdvanceToSelectNode(edict_t* self, int node) if (!SLLempty(&self->pathList)) { self->bot.next_node = SLLfront(&self->pathList); - //Com_Printf("%s reached [n:%i t:%i] next [n:%i t:%i] goal [%i]\n", self->client->pers.netname, self->bot.current_node, nodes[self->bot.current_node].type, self->bot.next_node, nodes[self->bot.next_node].type, self->bot.goal_node); //return true; if (bot_hit_next_node) return true; @@ -476,7 +473,6 @@ qboolean NODES_AdvanceToNextNode(edict_t* self) { self->bot.prev_node = self->bot.current_node; self->bot.current_node = self->bot.next_node; - //self->bot.next_node = path_table[self->bot.current_node][self->bot.goal_node]; // Remove the front entry from the list if (self->bot.next_node == SLLfront(&self->pathList)) @@ -486,7 +482,6 @@ qboolean NODES_AdvanceToNextNode(edict_t* self) if (!SLLempty(&self->pathList)) { self->bot.next_node = SLLfront(&self->pathList); - //Com_Printf("%s reached [n:%i t:%i] next [n:%i t:%i] goal [%i]\n", self->client->pers.netname, self->bot.current_node, nodes[self->bot.current_node].type, self->bot.next_node, nodes[self->bot.next_node].type, self->bot.goal_node); return true; } else @@ -668,7 +663,7 @@ void BOTLIB_SelfExpandingNodes(edict_t* ent, int node) if (0) { // Check if new location touches another node - nodes_touched = BOTLIB_NodeTouchNodes(exp, tv(0, 0, 0), 8, mins, maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(exp, vec3_origin, 8, mins, maxs, nodelist, MAX_NODELIST, INVALID); if (nodes_touched == 0) { // Check if we fit @@ -1155,7 +1150,7 @@ void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t *ent) */ { // Check if new location touches another node - nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[i], tv(0, 0, 0), 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(sp_origin[i], vec3_origin, 0, mins, maxs, nodelist, MAX_NODELIST, INVALID); if (nodes_touched == 0) { // Contents check @@ -1164,7 +1159,7 @@ void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t *ent) if (contents & MASK_WATER) type = NODE_WATER; - node_added = BOTLIB_AddNode(sp_origin[i], tv(0, 0, 0), type); + node_added = BOTLIB_AddNode(sp_origin[i], vec3_origin, type); if (node_added != INVALID) BOTLIB_SelfExpandingNodes(ent, node_added); } @@ -2295,10 +2290,8 @@ int BOTLIB_Reachability(int from, int to) // Move closer to goal by pointing the bot to the nearest next node that is closer to the goal qboolean BOTLIB_FollowPath(edict_t *self) { - //if (self->groundentity && (self->bot.current_node == INVALID || self->bot.next_node == INVALID || self->bot.goal_node == INVALID)) // Invalid pathing if (self->bot.current_node == INVALID || self->bot.next_node == INVALID || self->bot.goal_node == INVALID) // Invalid pathing { - //Com_Printf("%s [%s] invalid pathing current_node[%d] next_node[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); self->bot.node_travel_time++; return false; } @@ -2333,7 +2326,6 @@ qboolean BOTLIB_FollowPath(edict_t *self) else self->bot.node_poi_time = level.framenum + 7 * HZ; // Set the time to spend at the POI - //Com_Printf("%s [%s] reached POI, holding pos. current_node[%d] next_node[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); break; } } @@ -2448,18 +2440,15 @@ qboolean BOTLIB_FollowPath(edict_t *self) if (!AntStartSearch(self, self->bot.current_node, self->bot.goal_node)) // Set up our pathList { // Failed to find a path - Com_Printf("%s %s: Target at(%i) - No Path \n", __func__, self->client->pers.netname, self->bot.goal_node, self->bot.next_node); return false; } */ } - - - + // Check if bot is touching a node that isn't on the path - int nodes_touched; // Number of nodes touched int nodelist[MAX_NODELIST]; // Nodes touched - nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 0, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + int nodes_touched; // Number of nodes touched + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, vec3_origin, 0, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); qboolean external_node_touched = true; for (int i = 0; i < nodes_touched; i++) { @@ -2473,7 +2462,6 @@ qboolean BOTLIB_FollowPath(edict_t *self) int node = self->bot.node_list[i]; for (int j = 0; j < nodes[node].num_links; j++) { - if (nodes[j].nodenum == curr_node || self->bot.node_list[i] == self->bot.next_node) { external_node_touched = false; break; @@ -2665,7 +2653,7 @@ qboolean BOTLIB_FollowPath(edict_t *self) return false; } - else // Update current and next node + else // Update current and next node // THIS IS IT HERE!!! { self->bot.node_travel_time = 0; @@ -2674,7 +2662,8 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.prev_node = self->bot.current_node; self->bot.current_node = self->bot.node_list[i]; - self->bot.next_node = self->bot.node_list[i + 1]; + + self->bot.next_node = self->bot.node_list[0]; } } } @@ -3680,24 +3669,29 @@ qboolean ACEND_IsNodeVisibleToNodes(short x, short y) /////////////////////////////////////////////////////////////////////// short ACEND_GetRandomVisibleNode(short x) { - short i; - short dn; // Destination node - short vis_size = 0; + short i; + short dn; // Destination node + short vis_size = 0; - if (x + 1 < numnodes) // Bounce check - { - // Work out the size of the vis list - for (i = 0; i < MAX_VIS_NODES; i++) - { - if (node_vis_list[x][i] != INVALID) - vis_size++; - } + if (x + 1 < numnodes) // Bounds check + { + // Work out the size of the vis list + for (i = 0; i < MAX_VIS_NODES; i++) + { + if (node_vis_list[x][i] != INVALID) + vis_size++; + } - dn = (int)(random() * vis_size); // Pick a random destination node - return node_vis_list[x][dn]; - } + if (vis_size == 0) // No visible nodes + return INVALID; - return INVALID; + dn = (short)(random() * vis_size); // Pick a random destination node + + if (dn >= 0 && dn < vis_size) // Ensure dn is within bounds + return node_vis_list[x][dn]; + } + + return INVALID; } //rekkie -- DEV_1 -- e diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 388229cbb..5ca527284 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -300,7 +300,7 @@ void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd { // Check the bot is touching a jump node before attempting to jump int nodelist[MAX_NODELIST]; - int nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + int nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, vec3_origin, 8, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); if (nodes_touched && ent->bot.current_node != INVALID && ent->bot.next_node != INVALID) { for (int i = 0; i < nodes_touched; i++) @@ -311,7 +311,6 @@ void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd { //Com_Printf("%s %s touched jump node %d\n", __func__, ent->client->pers.netname, nodelist[i]); // Attempt jump - //BOTLIB_Jump_Takeoff(ent, NULL, nodes[ent->bot.next_node].origin, ent->viewheight, ent->velocity); BOTLIB_DoParabolaJump(ent, nodes[ent->bot.next_node].origin); bi->actionflags &= ~ACTION_JUMPPAD; break; @@ -337,7 +336,6 @@ void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd //Com_Printf("%s %s ladder down\n", __func__, ent->client->pers.netname); //vec3_t dist; - //VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); //VectorNormalize(dist); //VectorScale(dist, 20, ent->velocity); // Apply it } @@ -403,7 +401,6 @@ void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd /* vec3_t dist; - VectorSubtract(nodes[ent->bot.next_node].origin, ent->s.origin, dist); VectorNormalize(dist); VectorScale(dist, 20, ent->velocity); // Apply it @@ -750,6 +747,8 @@ void BOTLIB_Think(edict_t* self) //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname, level.framenum); //nav_area.total_areas = 0; // Turn off area based nav + + // THIS IS WHERE THE bot.current_node is set // BOTLIB_PickLongRangeGoal(self); //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() curr[%d] goal[%d] -------------------- \n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node, self->bot.goal_node); @@ -976,7 +975,6 @@ void BOTLIB_TouchingLadder(edict_t* self) { //trace_t tr; - //if (nodes[self->bot.current_node].type == NODE_LADDER || nodes[self->bot.next_node].type == NODE_LADDER) { // Check if touching ladder { diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 55ff41a9b..9a43776ad 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -1028,7 +1028,6 @@ void BOTLIB_CTF_Goals(edict_t* self) int item_node = BOTLIB_GetEquipment(self); if (item_node != INVALID) { - if (BOTLIB_CanVisitNode(self, nodes[item_node].nodenum, false)) { self->bot.state = BOT_MOVE_STATE_MOVE; BOTLIB_SetGoal(self, nodes[item_node].nodenum); diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 648a69a73..7fb57b8ad 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -1581,7 +1581,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) } // Get current and next node back from nav code. - if (!BOTLIB_FollowPath(self)) { if (!teamplay->value || (teamplay->value && level.framenum >= self->teamPauseTime)) { @@ -2084,7 +2083,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) // Try visit node near item // Otherwise visit previous goal node - if (BOTLIB_CanVisitNode(self, self->bot.goal_node) == false) // If we cannot visit item node { self->bot.goal_node = self->bot.prev_node; // Restore previous goal BOTLIB_CanVisitNode(self, self->bot.goal_node); // Try go back to previous goal @@ -2180,7 +2178,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) else if (next_node_distance > 64 && next_node_distance >= self->next_node_distance + NODE_Z_HALF_HEIGHT) // If getting further away we failed the jump { self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); // Update the node we're near - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { Com_Printf("%s %s failed to jumppad up, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->bot.goal_node); @@ -2240,7 +2237,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) self->wander_timeout = level.framenum + 0.1 * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { if (debug_mode) Com_Printf("%s (1) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); @@ -2434,7 +2430,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) self->wander_timeout = level.framenum + 0.1 * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { if (debug_mode) Com_Printf("%s (2) %s failed to jumppad. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); @@ -3348,7 +3343,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) self->wander_timeout = level.framenum + (random() + 0.5) * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { //Com_Printf("%s %s failed to drop down, trying alternative path to goal\n", __func__, self->client->pers.netname); BOTLIB_SetGoal(self, self->bot.goal_node); @@ -3733,7 +3727,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) //self->wander_timeout = level.framenum + 0.1 * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); return; @@ -4046,7 +4039,7 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) //self->wander_timeout = level.framenum + 0.1 * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal + if ((self, self->bot.goal_node)) // Try to find another way to our goal { if (debug_mode) Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); @@ -4135,7 +4128,6 @@ void BOTLIB_MOV_Move(edict_t* self, usercmd_t* ucmd) } else { - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { //if (debug_mode) //Com_Printf("%s %s failed to move. Trying alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); @@ -5220,7 +5212,7 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) } } if (VectorEmpty(self->bot.bi.look_at)) - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + VectorSubtract(vec3_origin, self->s.origin, lookdir); // Look at center of map else VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item @@ -5229,7 +5221,7 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) // If lookdir is empty, then look at map center if (VectorEmpty(self->bot.bi.look_at)) { - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + VectorSubtract(vec3_origin, self->s.origin, lookdir); } BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, turn_speed, true, true); @@ -5359,9 +5351,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) /* // Get current and next node back from nav code. - if (!BOTLIB_FollowPath(self)) { - //Com_Printf("%s %s BOTLIB_FollowPath == false\n", __func__, self->client->pers.netname); //Com_Printf("%s %s next_node:%d current_node:%d goal_node:%d\n", __func__, self->client->pers.netname, self->bot.next_node, self->bot.current_node, self->bot.goal_node); self->bot.bi.speed = 0; @@ -5846,7 +5836,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) } } if (VectorEmpty(self->bot.bi.look_at)) - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); // Look at center of map + VectorSubtract(vec3_origin, self->s.origin, lookdir); // Look at center of map else VectorSubtract(self->bot.bi.look_at, self->s.origin, lookdir); // Look at random map item @@ -5855,7 +5845,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // If lookdir is empty, then look at map center if (VectorEmpty(self->bot.bi.look_at)) { - VectorSubtract(tv(0, 0, 0), self->s.origin, lookdir); + VectorSubtract(vec3_origin, self->s.origin, lookdir); } BOTLIB_ChangeBotAngleYawPitch(self, lookdir, false, 1.0, true, true); @@ -5964,7 +5954,6 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) //if (self->bot.goal_node != INVALID && nav_area.total_areas > 0 && self->bot.node_travel_time >= 120) { //Com_Printf("%s %s ATTEMPTING TO FIX cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); - //if (BOTLIB_CanVisitNode(self, self->bot.goal_node, false, INVALID)) { //self->bot.node_travel_time = 0; //Com_Printf("%s %s FIXED node_travel_time cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); @@ -7281,7 +7270,6 @@ void BOTLIB_MOV_Wander(edict_t* self, usercmd_t* ucmd) self->wander_timeout = level.framenum + 0.1 * HZ; return; } - if (BOTLIB_CanVisitNode(self, self->bot.goal_node)) // Try to find another way to our goal { if (debug_mode) Com_Printf("%s %s Wander finding alternative path to goal %d\n", __func__, self->client->pers.netname, self->bot.goal_node); diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index dd26c7a16..50f891c57 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -888,7 +888,6 @@ qboolean BOTLIB_CanVisitAreaNode(edict_t* self, int goal_node) // Otherwise continue with trying to route to the goal_node's area if (self->bot.current_area == self->bot.goal_area) { - //if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, false)) { self->bot.next_area_nodes[0] = goal_node; // Last node self->bot.next_area_nodes_counter = 0; // Total nodes in next_area_nodes[] @@ -1131,7 +1130,6 @@ qboolean BOTLIB_GetNextAreaNode(edict_t *self) int next_node = self->bot.next_area_nodes[self->bot.next_area_counter]; if (next_node != INVALID) { - //if (BOTLIB_CanVisitNode(self, nodes[next_node].nodenum, false, INVALID, self->bot.next_area_counter)) //if (BOTLIB_DijkstraAreaPath(self, prev_node, next_node, false, nodes[prev_node].area, self->bot.next_area_counter)) if (BOTLIB_DijkstraPath(self, prev_node, next_node, false)) { @@ -1248,7 +1246,6 @@ qboolean BOTLIB_GetNextAreaNode(edict_t *self) if (self->bot.next_area_node != INVALID) { - if (BOTLIB_CanVisitNode(self, nodes[self->bot.next_area_node].nodenum, false, nodes[self->bot.current_node].area)) { Com_Printf("%s heading to next node[%d] area[%d] goal[%d]\n", __func__, nodes[self->bot.next_area_node].nodenum, self->bot.next_area, self->bot.goal_node); return; @@ -1947,8 +1944,9 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando } AntInitSearch(ent); // Clear out the path storage - for (int i = 0; i < numnodes; i++) + for (int i = 0; i < numnodes; i++){ nodeweight[i] = NAV_INFINITE; + } int newNode = INVALID; // Stores the node being tested int atNode = from; // Current node we're visiting @@ -2253,8 +2251,9 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando ent->bot.node_list_current = 0; if (ent->bot.node_list_count) // Set the current and next nodes { - ent->bot.current_node = ent->bot.node_list[0]; - ent->bot.next_node = ent->bot.node_list[1]; + //ent->bot.current_node = ent->bot.node_list[0]; + ent->bot.current_node = from; + ent->bot.next_node = ent->bot.node_list[0]; } ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path @@ -2262,6 +2261,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando { if (1) { + gi.dprintf("from: %d, next: %d, to: %d\n", from, ent->bot.next_node, to); gi.dprintf("%s s[%d] g[%d] node_list[", __func__, from, to); for (int i = 0; i < ent->bot.node_list_count; i++) { diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 100252823..18f7065ee 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -2,11 +2,10 @@ #include "../acesrc/acebot.h" #include "botlib.h" -int num_poi_nodes = 0; -int poi_nodes[MAX_POI_NODES] = { INVALID }; -// edict_t* node_ents[MAX_EDICTS]; // If the node is attached to an entity (such as a NODE_DOOR being attached to a func_door_rotating or func_door entity) -node_t *nodes = NULL; -nmesh_t nmesh = { 0 }; +int num_poi_nodes; +int poi_nodes[MAX_POI_NODES]; +node_t *nodes; +static nmesh_t nmesh; // Free nodes void BOTLIB_FreeNodes(void) @@ -656,7 +655,7 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi int nodelist[MAX_NODELIST]; int nodes_touched; - nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, tv(0, 0, 0), 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(self->s.origin, vec3_origin, 32, self->mins, self->maxs, nodelist, MAX_NODELIST, INVALID); for (int i = 0; i < nodes_touched; i++) // Cycle through all the nodes we touched { if (nodelist[i] != INVALID) diff --git a/src/action/p_client.c b/src/action/p_client.c index 5cddcfcf0..87dc5d1ce 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -4057,7 +4057,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) // Check if bot is touching a node that isn't on the path int nodes_touched; // Number of nodes touched int nodelist[MAX_NODELIST]; // Nodes touched - nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tv(0, 0, 0), 32, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); + nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, vec3_origin, 32, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); for (i = 0; i < nodes_touched; i++) { if (nodelist[i] != INVALID && nodes[nodelist[i]].inuse) @@ -4903,7 +4903,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) nodes_touched = BOTLIB_NodeTouchNodes(ent->s.origin, tr.plane.normal, walknode_dist, ent->mins, ent->maxs, nodelist, MAX_NODELIST, INVALID); if (nodes_touched > 0) { - if (0) // print node list + if (1) // print node list { Com_Printf("[%d] Nodes touched [%d] nodelist [", level.framenum, nodes_touched); int count = 0; @@ -5816,7 +5816,6 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) { if (players[i] && players[i]->is_bot && players[i]->health > 0) { - //players[i]->bot.goal_node = closest_node; //BOTLIB_SetGoal(players[i], closest_node); if (BOTLIB_CanGotoNode(players[i], players[i]->bot.goal_node, false)) { From 8e537c4ae22c38a1b86a0c049ec15902e4182951 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 2 Aug 2024 14:07:30 -0400 Subject: [PATCH 409/974] I think it worksgit status! --- src/action/acesrc/acebot_ai.c | 28 ++++++++-------- src/action/acesrc/acebot_nodes.c | 3 +- src/action/botlib/botlib.h | 4 +-- src/action/botlib/botlib_ai.c | 3 ++ src/action/botlib/botlib_movement.c | 14 ++++---- src/action/botlib/botlib_nav.c | 50 +++++++---------------------- src/action/botlib/botlib_nodes.c | 4 +-- src/action/botlib/botlib_weapons.c | 35 +++----------------- 8 files changed, 44 insertions(+), 97 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index b35983853..9da210929 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -129,6 +129,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) //current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); + gi.dprintf("%s is currently at node %d\n", self->client->pers.netname, self->bot.current_node); if (ctf->value) // CTF has it's own goals return; @@ -153,9 +154,8 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) //======================= // Get navigation //======================= - //if (self->bot.state == BOT_MOVE_STATE_NAV) + if (self->bot.state == BOT_MOVE_STATE_NAV) { - //self->bot.get_item = NULL; //if (BOTLIB_NeedWeaponOrAmmo(self)) //if (random() < 0.75 && self->client->weapon == FindItem(MK23_NAME) || self->client->weapon == FindItem(DUAL_NAME)) @@ -166,13 +166,11 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) node = BOTLIB_GetEquipment(self); if (node != INVALID) { - if (BOTLIB_CanGotoNode(self, nodes[node].nodenum, rand() % 2)) - { - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, nodes[node].nodenum); - //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[node].nodenum); + //if (BOTLIB_CanGotoNode(self, nodes[node].nodenum, rand() % 2)) + //{ + Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[node].nodenum); return; - } + //} } } @@ -187,7 +185,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } if (BOTLIB_CanGotoNode(self, nodes[i].nodenum, false)) { - //Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); + Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); //self->bot.state = BOT_MOVE_STATE_MOVE; //BOTLIB_SetGoal(self, nodes[i].nodenum); return; @@ -325,12 +323,12 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } // // Random node - // int retries = 0; - // while (retries < max_random_retries) - // { - // if (!BOTLIB_ChooseRandomNode(self, 128)) - // retries++; - // } + int retries = 0; + while (retries < max_random_retries) + { + if (!BOTLIB_ChooseRandomNode(self, 128)) + retries++; + } } //Com_Printf("%s %s BOT_MOVE_STATE_NAV couldn't find a good path [%d]\n", __func__, self->client->pers.netname, level.framenum); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 64247948a..092198b42 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -2662,8 +2662,7 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.prev_node = self->bot.current_node; self->bot.current_node = self->bot.node_list[i]; - - self->bot.next_node = self->bot.node_list[0]; + self->bot.next_node = self->bot.node_list[i + 1]; } } } diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 27eee687a..aa5b39031 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -12,9 +12,9 @@ typedef enum { BOT_MOVE_STATE_NONE = 0, // Bot is booting up :-) - BOT_MOVE_STATE_NAV, // Getting a navigational path + BOT_MOVE_STATE_NAV = 1, // Getting a navigational path //BOT_MOVE_STATE_NAV_NEXT, // Getting next navigational path (if any) - BOT_MOVE_STATE_MOVE, // Standard movement + BOT_MOVE_STATE_MOVE = 3, // Standard movement BOT_MOVE_STATE_WANDER, // No navigation and no movement, try wandering around BOT_MOVE_STATE_STAND, // Stand still and hold a position BOT_MOVE_STATE_FLEE, // Running away from enemy force diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 5ca527284..92c1e2b13 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -703,10 +703,12 @@ void BOTLIB_Think(edict_t* self) if (level.framenum < 15) // Wait for a little before processing AI on a new map goto end_think; // Skip bot logic + //gi.dprintf("%s: My bot state is %d\n", __func__, self->bot.state); if (ctf->value) // CTF Goals { BOTLIB_CTF_Goals(self); } + // Check if the bot is in a NAV state (I need a nav) or if NONE else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) { /* @@ -772,6 +774,7 @@ void BOTLIB_Think(edict_t* self) // killPlayer( self, true ); // Kill the bot if they've not moved between nodes in a timely manner, stuck! + gi.dprintf("%s is currently at node %i\n", self->client->pers.netname, self->bot.current_node); if (self->bot.node_travel_time > 120) killPlayer(self, true); diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 7fb57b8ad..6185a529d 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5871,11 +5871,11 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Otherwise pick from various wait times before moving out int rnd_rng = rand() % 4; if (rnd_rng == 0) - self->just_spawned_timeout = level.framenum + (random() * 3) * HZ; // Long wait + self->just_spawned_timeout = level.framenum + (random() * 10) * HZ; // Long wait else if (rnd_rng == 1) - self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Medium wait + self->just_spawned_timeout = level.framenum + (random() * 5) * HZ; // Medium wait else if (rnd_rng == 2) - self->just_spawned_timeout = level.framenum + (random() * HZ); // Short wait + self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait else self->just_spawned_timeout = 0; // No wait } @@ -5892,7 +5892,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Go! if (self->just_spawned_go || self->bot.see_enemies) { - BOTLIB_PickLongRangeGoal(self); + //BOTLIB_PickLongRangeGoal(self); self->just_spawned_go = false; // Now we can move! } } @@ -5964,7 +5964,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node { self->bot.stuck_wander_time = 1; - //Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); } //self->bot.stuck_wander_time = 0; @@ -6260,8 +6260,8 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (1 & self->bot.node_list_count) + //if (self->bot.node_list_count > 0) { - //for (int i = self->bot.node_list_current; i < self->bot.node_list_count; i++) for (int i = 1; i < self->bot.node_list_count; i++) { if (self->bot.next_node == self->bot.node_list[i]) @@ -6501,7 +6501,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) VectorSubtract(nodes[self->bot.next_node].origin, self->s.origin, bot_to_node); bot_to_node[2] = 0; float xy_bot_to_next_dist = VectorLength(bot_to_node); // Distance from bot to next node - if (xy_bot_to_next_dist > 16 && xy_bot_to_next_dist <= 150) + if (xy_bot_to_next_dist > 32 && xy_bot_to_next_dist <= 150) { // Line of sight tr = gi.trace(self->s.origin, NULL, NULL, nodes[self->bot.next_node].origin, self, MASK_PLAYERSOLID); diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 50f891c57..1e5193dc7 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -826,8 +826,11 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz else // Fallback to old pathing { self->bot.goal_node = INVALID; - gi.dprintf("BOTLIB_CanVisitNode? %d\n", BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)); - if (BOTLIB_CanVisitNode(self, nodes[goal_node].nodenum, path_randomization, INVALID, false)) + //self->bot.goal_node = goal_node; + qboolean canVisit = BOTLIB_CanVisitNode(self, goal_node, path_randomization, INVALID, false); + + gi.dprintf("BOTLIB_CanVisitNode? %d\n", canVisit); + if (canVisit) { // Add the goal to the end of the node list + terminate if (self->bot.node_list_count) @@ -1898,38 +1901,6 @@ qboolean BOTLIB_DijkstraAreaPath(edict_t* ent, int from, int to, qboolean path_r } //rekkie -- Dijkstra Area Pathing -- e - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //rekkie -- Dijkstra pathing -- s // Currently if path_randomization is enabled and the bot fails to path to its target, // BOTLIB_DijkstraPath() is run again with path_randomization turned off @@ -1944,9 +1915,9 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando } AntInitSearch(ent); // Clear out the path storage - for (int i = 0; i < numnodes; i++){ + for (int i = 0; i < numnodes; i++) nodeweight[i] = NAV_INFINITE; - } + int newNode = INVALID; // Stores the node being tested int atNode = from; // Current node we're visiting @@ -2245,8 +2216,8 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando //Com_Printf("%s EXIT PATH\n\n", __func__); // Each time a path is found, make a copy - ent->bot.node_list_count = 0; - memset(ent->bot.node_list, 0, sizeof(ent->bot.node_list)); // Ensure the node list is cleared + // ent->bot.node_list_count = 0; + // memset(ent->bot.node_list, 0, sizeof(ent->bot.node_list)); // Ensure the node list is cleared ent->bot.node_list_count = BOTLIB_SLL_Query_All_Nodes(ent, &ent->pathList, ent->bot.node_list, MAX_NODELIST); // Retrieve the nodes in the list ent->bot.node_list_current = 0; if (ent->bot.node_list_count) // Set the current and next nodes @@ -2255,6 +2226,9 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando ent->bot.current_node = from; ent->bot.next_node = ent->bot.node_list[0]; } + if(from != ent->bot.node_list[0]) + gi.dprintf("Deviation in provided `from` parameter (%i) and node_list[0] value (%i)!\n", from, ent->bot.node_list[0]); + ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path if (1) diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 18f7065ee..4c5b39076 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -660,7 +660,7 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi { if (nodelist[i] != INVALID) { - self->bot.current_node = nodelist[i]; + //self->bot.current_node = nodelist[i]; break; } } @@ -673,7 +673,7 @@ qboolean BOTLIB_CanVisitNode(edict_t* self, int goal_node, qboolean path_randomi return false; } - self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first + //self->bot.next_node = self->bot.current_node; // make sure we get to the nearest node first self->node_timeout = 0; self->bot.state = BOT_MOVE_STATE_MOVE; diff --git a/src/action/botlib/botlib_weapons.c b/src/action/botlib/botlib_weapons.c index 462b66ff2..2d25d1925 100644 --- a/src/action/botlib/botlib_weapons.c +++ b/src/action/botlib/botlib_weapons.c @@ -992,42 +992,14 @@ int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter) vec3_t maxs = { 16, 16, 32 }; vec3_t bmins = { 0,0,0 }; vec3_t bmaxs = { 0,0,0 }; + //qboolean outer_break = false; // Flag to break the outer loop + //for (int i = base; i < globals.num_edicts && !outer_break; i++, ent++) for (int i = base; i < globals.num_edicts; i++, ent++) { if (ent->inuse == false) continue; if (!ent->classname) continue; - /* - if (strcmp(ent->classname, "info_player_deathmatch") == 0) // Skip spawn points - continue; - - if (strcmp(ent->classname, "dead_body") == 0) // Skip dead bodies - continue; - - if (ent->solid == SOLID_NOT) - continue; - - if (strcmp(ent->classname, "medkit") == 0) - { - } - - //if (ent->spawnflags != 0) continue; - //if (ent->flags != 0) continue; - - //Com_Printf("%s %s item %s [%f %f %f] - sf[0x%x] - f[0x%x]\n", __func__, self->client->pers.netname, ent->classname, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2], ent->spawnflags, ent->flags); - - // Ent->Spawnflags - // DROPPED_ITEM 0x00010000 Item dropped by player - // DROPPED_PLAYER_ITEM 0x00020000 Item dropped when dead - // ITEM_TARGETS_USED 0x00040000 Item picked up from spawn spot - // - // Ent->Flags - // FL_RESPAWN 0x80000000 used for item respawning - //if (ent->spawnflags == ITEM_TARGETS_USED && ent->flags == FL_RESPAWN) // Skip weapon spawn spot when weapon has been picked up - // continue; - */ - switch (ent->typeNum) { case MK23_NUM: case MP5_NUM: @@ -1072,11 +1044,12 @@ int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter) { //trace_t tr = gi.trace(nodes[j].origin, mins, maxs, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); trace_t tr = gi.trace(nodes[j].origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] + 0), NULL, MASK_PLAYERSOLID); - if (tr.fraction == 1.0 && BOTLIB_CanGotoNode(self, nodes[j].nodenum, false)) // Check if bot can nav to item + if (tr.fraction == 1.0 && (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) && BOTLIB_CanGotoNode(self, nodes[j].nodenum, false)) // Check if bot can nav to item { items[item_count] = ent; item_nodes[item_count] = nodes[j].nodenum; item_count++; + //outer_break = true; // Set flag to break the outer loop break; } else From 1a5aa64b3fe60b87fc93ac8723bda2e2da7b6c96 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 2 Aug 2024 14:45:17 -0400 Subject: [PATCH 410/974] Removed debug messaging from prints --- src/action/acesrc/acebot_ai.c | 6 +++--- src/action/acesrc/acebot_nodes.c | 5 +++-- src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_movement.c | 2 +- src/action/botlib/botlib_nav.c | 13 ++++++++----- src/action/p_client.c | 2 ++ 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 9da210929..d197c1683 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -129,7 +129,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) //current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); self->bot.current_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - gi.dprintf("%s is currently at node %d\n", self->client->pers.netname, self->bot.current_node); + //gi.dprintf("%s is currently at node %d\n", self->client->pers.netname, self->bot.current_node); if (ctf->value) // CTF has it's own goals return; @@ -168,7 +168,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) { //if (BOTLIB_CanGotoNode(self, nodes[node].nodenum, rand() % 2)) //{ - Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[node].nodenum); + //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[node].nodenum); return; //} } @@ -185,7 +185,7 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } if (BOTLIB_CanGotoNode(self, nodes[i].nodenum, false)) { - Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); + //Com_Printf("%s %s visiting [RNG] node[%i] counter[%d]\n", __func__, self->client->pers.netname, nodes[i].nodenum, counter); //self->bot.state = BOT_MOVE_STATE_MOVE; //BOTLIB_SetGoal(self, nodes[i].nodenum); return; diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 092198b42..9eb36cb70 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -2596,7 +2596,7 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.bi.speed = 100; //self->bot.bi.actionflags = 0; - Com_Printf("%s %s +++++++++ SUCCESSFULLY REACHED GOAL +++++++++ curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + //Com_Printf("%s %s +++++++++ SUCCESSFULLY REACHED GOAL +++++++++ curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); self->bot.node_travel_time = 0; ////if (nav_area.total_areas > 0) @@ -2638,7 +2638,8 @@ qboolean BOTLIB_FollowPath(edict_t *self) self->bot.bi.speed = 100; //self->bot.bi.actionflags = 0; - Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); + // This is a complete success, well done + //Com_Printf("%s %s SUCCESSFULLY REACHED GOAL - curr_node[%d] goal_node[%d] dist[%f]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.goal_node, VectorDistance(nodes[self->bot.current_node].origin, nodes[node].origin)); self->bot.node_travel_time = 0; ////if (nav_area.total_areas > 0) diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 92c1e2b13..8df69435a 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -774,7 +774,7 @@ void BOTLIB_Think(edict_t* self) // killPlayer( self, true ); // Kill the bot if they've not moved between nodes in a timely manner, stuck! - gi.dprintf("%s is currently at node %i\n", self->client->pers.netname, self->bot.current_node); + //gi.dprintf("%s is currently at node %i\n", self->client->pers.netname, self->bot.current_node); if (self->bot.node_travel_time > 120) killPlayer(self, true); diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 6185a529d..081bb835b 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5964,7 +5964,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->bot.node_travel_time >= 120) //60 // Bot failure to reach next node { self->bot.stuck_wander_time = 1; - Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + //Com_Printf("%s %s node_travel_time was hit! cur[%d] nxt[%d] goal[%d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); } //self->bot.stuck_wander_time = 0; diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index 1e5193dc7..d063b252c 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -829,7 +829,7 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz //self->bot.goal_node = goal_node; qboolean canVisit = BOTLIB_CanVisitNode(self, goal_node, path_randomization, INVALID, false); - gi.dprintf("BOTLIB_CanVisitNode? %d\n", canVisit); + //gi.dprintf("BOTLIB_CanVisitNode? %d\n", canVisit); if (canVisit) { // Add the goal to the end of the node list + terminate @@ -843,7 +843,9 @@ qboolean BOTLIB_CanGotoNode(edict_t* self, int goal_node, qboolean path_randomiz } } - Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); + // Failed #2 is super common, it just means that the bot cannot reach the goal node within the + // bounds of how long distances can be calculated. It's not a failure, just a limitation. + //Com_Printf("%s %s failed #2 \n", __func__, self->client->pers.netname); return false; // Failure to find path } @@ -2226,12 +2228,13 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando ent->bot.current_node = from; ent->bot.next_node = ent->bot.node_list[0]; } - if(from != ent->bot.node_list[0]) - gi.dprintf("Deviation in provided `from` parameter (%i) and node_list[0] value (%i)!\n", from, ent->bot.node_list[0]); + // if(from != ent->bot.node_list[0]) + // gi.dprintf("Deviation in provided `from` parameter (%i) and node_list[0] value (%i)!\n", from, ent->bot.node_list[0]); ent->bot.node_random_path = path_randomization; // Note down if the bot was taking a random or direct path - if (1) + // Huge help in debugging, change this to 1 + if (0) { if (1) { diff --git a/src/action/p_client.c b/src/action/p_client.c index 87dc5d1ce..08fef2569 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -498,6 +498,8 @@ void Add_Frag(edict_t * ent, int mod) //rekkie -- DEV_1 -- s // Random bot voice sounds + + // Debug this, sometimes bots will repeat this over and over again if (use_voice->value && ent->is_bot && bot_randvoice->value > 0) { if (bot_randvoice->value > 100) bot_randvoice->value = 100; From 7d86e187af645011c708e242588840f4aab327a5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 2 Aug 2024 16:00:19 -0400 Subject: [PATCH 411/974] Added debugmode check on some messages --- meson.build | 2 +- src/action/acesrc/acebot_cmds.c | 2 +- src/action/acesrc/acebot_spawn.c | 7 +++++-- src/action/botlib/botlib_spawn.c | 7 +++++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index 0944e76db..c539a58cd 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=debug', + 'buildtype=release', ], ) diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index b9f6d3c04..54cb68997 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -31,7 +31,7 @@ #include "../g_local.h" -qboolean debug_mode=true; +qboolean debug_mode=false; qboolean shownodes_mode=false; //RiEvEr - new node showing method /////////////////////////////////////////////////////////////////////// diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 7b2f884f9..715bb9a72 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -730,8 +730,11 @@ void ACESP_RemoveBot(char *name) ClientDisconnect( bot ); } */ - if(!freed) - gi.bprintf (PRINT_MEDIUM, "No bot removed\n"); + if (!freed) { + if (debug_mode) { + gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); + } + } // ACESP_SaveBots(); // Save them again } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 72c22a51d..0c8c4097b 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1528,8 +1528,11 @@ void BOTLIB_RemoveBot(char* name) } } - if (!freed) - gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); + if (!freed) { + if (debug_mode) { + gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); + } + } } // Remove a bot by team From 60ed01773d59b979904e46e4649549eecd53a2ab Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 2 Aug 2024 16:09:30 -0400 Subject: [PATCH 412/974] Changed ifdef to if --- src/client/parse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/parse.c b/src/client/parse.c index 4126df1bd..080dc3982 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -341,7 +341,7 @@ static void CL_ParseFrame(int extrabits) bits = MSG_ReadWord(); if (cls.serverProtocol == PROTOCOL_VERSION_AQTION) { MSG_ParseDeltaPlayerstate_Aqtion(from, &frame.ps, bits, extraflags, cl.psFlags); -#ifdef USE_DEBUG +#if USE_DEBUG if (cl_shownet->integer > 2 && (bits || extraflags)) { MSG_ShowDeltaPlayerstateBits_Enhanced(bits, extraflags); Com_LPrintf(PRINT_DEVELOPER, "\n"); From 09c922a1b33ea2a0e43bf92d2a8e620ec85009ab Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 2 Aug 2024 16:11:29 -0400 Subject: [PATCH 413/974] Removed drowning debug statement --- src/action/botlib/botlib_movement.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 081bb835b..c156b496f 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -6433,7 +6433,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->client->old_waterlevel = waterlevel; if (current_node_type == NODE_WATER || waterlevel == 3){ - gi.dprintf("I'm in the water %s and my air is %d\n", self->client->pers.netname, self->air_finished_framenum - level.framenum); + //gi.dprintf("I'm in the water %s and my air is %d\n", self->client->pers.netname, self->air_finished_framenum - level.framenum); // Get out before you start drowning! if (self->air_finished_framenum < level.framenum + 10) { self->bot.bi.actionflags |= ACTION_MOVEUP; From 4afb09aa48bf2f1acfaa8363b7c4e134f73cb921 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Aug 2024 17:56:46 -0400 Subject: [PATCH 414/974] Delayed bots based on HZ rather than straight frames --- src/action/botlib/botlib_communication.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 072d26f31..2327e5c70 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -33,7 +33,7 @@ void AddMessageToQueue(edict_t* bot, const char* text, int frameReceived) { void ProcessChatQueue(int currentFrame) { for (int i = 0; i < chatQueue.count; i++) { - if (currentFrame > chatQueue.messages[i].frameReceived + 40) { + if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ) { // Send the chat message from the correct bot BOTLIB_Say(chatQueue.messages[i].bot, chatQueue.messages[i].text, false); //gi.dprintf("Sending delayed chat message from bot: %s\n", chatQueue.messages[i].text); From 4a301eb8a2fadc525c409a045687cd664299a544 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 2 Aug 2024 19:44:46 -0400 Subject: [PATCH 415/974] Added example bot_personality files --- bot.json | 9 + src/action/acesrc/acebot.h | 2 + src/action/botlib/bot_personality.c | 257 ++++++++++++++++++++++++++++ src/action/g_local.h | 15 ++ src/action/g_main.c | 3 +- src/action/g_save.c | 1 + 6 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 bot.json create mode 100644 src/action/botlib/bot_personality.c diff --git a/bot.json b/bot.json new file mode 100644 index 000000000..fe1c92af8 --- /dev/null +++ b/bot.json @@ -0,0 +1,9 @@ +{ + "name": "bbbbbaskdimasoidmasiodmob", + "skin": "male/terrorbbbbbaskdimasoidmasiodmobbbbbbaskdimasoidmasiodmobbbbbbaskdimasoidmasiodmob", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": 0.75, + "combat_demeanor": 0.45, + "chat_demeanor": 50 +} diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 359570c6e..1b7760e8f 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -542,6 +542,8 @@ extern cvar_t* bot_randvoice; extern cvar_t* bot_randskill; extern cvar_t* bot_randname; extern cvar_t* bot_chat; +extern cvar_t* bot_personality; + //extern cvar_t* bot_randteamskin; diff --git a/src/action/botlib/bot_personality.c b/src/action/botlib/bot_personality.c new file mode 100644 index 000000000..4b107ce51 --- /dev/null +++ b/src/action/botlib/bot_personality.c @@ -0,0 +1,257 @@ +//#include "../g_local.h" +#include "/opt/homebrew/include/jansson.h" +#include +#include +#include + +int warningCount = 0; + +#define WEAPON_COUNT 9 +#define ITEM_COUNT 6 + +typedef struct bot_personality_s +{ + // These are +1 because we're ignoring the first index [0] + // So that MK23_NUM (1) stays at 1 here as well + float weapon_prefs[WEAPON_COUNT + 1]; //-1 = Will never choose, 1 = Will always choose + float item_prefs[ITEM_COUNT +1]; //-1 = Will never choose, 1 = Will always choose + + float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love + float combat_demeanor; //-1 = Timid | 1 = Aggressive + float chat_demeanor; //-1 = Quiet | 1 = Chatty + int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. + char* skin; + char* name; +} bot_personality_t; + +#define MK23_NUM 1 +#define MP5_NUM 2 +#define M4_NUM 3 +#define M3_NUM 4 +#define HC_NUM 5 +#define SNIPER_NUM 6 +#define DUAL_NUM 7 +#define KNIFE_NUM 8 +#define GRENADE_NUM 9 + +#define SIL_NUM 10 +#define SLIP_NUM 11 +#define BAND_NUM 12 +#define KEV_NUM 13 +#define LASER_NUM 14 +#define HELM_NUM 15 + +#define MK23_NAME "MK23 Pistol" +#define MP5_NAME "MP5/10 Submachinegun" +#define M4_NAME "M4 Assault Rifle" +#define M3_NAME "M3 Super 90 Assault Shotgun" +#define HC_NAME "Handcannon" +#define SNIPER_NAME "Sniper Rifle" +#define DUAL_NAME "Dual MK23 Pistols" +#define KNIFE_NAME "Combat Knife" +#define GRENADE_NAME "M26 Fragmentation Grenade" + +#define SIL_NAME "Silencer" +#define SLIP_NAME "Stealth Slippers" +#define BAND_NAME "Bandolier" +#define KEV_NAME "Kevlar Vest" +#define HELM_NAME "Kevlar Helmet" +#define LASER_NAME "Lasersight" + +/////* +// This file focuses on loading bot personality traits +/////* + +// Data Validation +float validate_pref_numeric(json_t* value, const char* prefName) { + float numValue = 0.0f; + + if (value) { + if (json_is_real(value)) { + numValue = (float)json_real_value(value); + } else if (json_is_integer(value)) { + numValue = (float)json_integer_value(value); + } + + // Check if the value is within the bounds [-1, 1] + if (numValue < -1.0f || numValue > 1.0f) { + warningCount++; + printf("Warning: Value %f for '%s' is out of bounds. It must be between -1 and 1.\n", numValue, prefName); + return 0.0f; // Value is out of bounds + } + + return numValue; // Value is valid and within bounds + } + + warningCount++; + printf("Warning: Invalid or missing value for '%s'.\n", prefName); + return 0.0f; // Return 0 if validation fails +} +char* validate_pref_string(json_t* value, int stringtype) { + const char* str = json_string_value(value); + size_t len = strlen(str); + int maxlen = 0; + char* valuename; + if (value && json_is_string(value)) { + if (stringtype == 0) { + maxlen = 16; + valuename = "name"; + } else { + maxlen = 64; + valuename = "skin"; + } + if (len > 0 && len <= maxlen) { + return strdup(str); // Return a copy of the string if valid + } + } + warningCount++; + printf("Warning: Invalid %s (%s). Using default value.\n", valuename, str); + return NULL; // Return NULL if validation fails +} + +// Function to load a bot personality from a JSON file using libjansson +bot_personality_t* load_bot_personality(const char* filename) { + // Open the file + FILE* file = fopen(filename, "r"); + if (!file) { + perror("Failed to open file"); + return NULL; + } + + // Use jansson's json_loadf to parse the file directly into a json_t object + json_error_t error; + json_t* root = json_loadf(file, 0, &error); + fclose(file); // Close the file as soon as it's no longer needed + + if (!root) { + fprintf(stderr, "Error parsing JSON: %s\n", error.text); + return NULL; + } + + // Allocate memory for the bot_personality_t struct + bot_personality_t* personality = (bot_personality_t*)malloc(sizeof(bot_personality_t)); + if (!personality) { + json_decref(root); // Ensure to release the json_t object to prevent memory leaks + return NULL; + } + + // Extract and assign values from the JSON object + // Define an array of weapon names corresponding to their preferences + const char* weaponNames[] = { + MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, + SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME + }; + + json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); + for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { + json_t* wpref_value = json_array_get(weapon_prefs, i); + // Ensure the index is within the bounds of weaponNames array + const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; + float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name + + // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) + if (value != 0.0f) { + personality->weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered + } + } + + // Assuming you have an array of preference names corresponding to each index + // Define an array of item names corresponding to their preferences + const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; + + json_t* item_prefs = json_object_get(root, "item_prefs"); + for (size_t i = 0; i < json_array_size(item_prefs); i++) { + json_t* ipref_value = json_array_get(item_prefs, i); + // Ensure the index is within the bounds of itemNames array + const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; + float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings + + // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases + if (value != 0.0f) { + personality->item_prefs[i] = value; + } + } + + // Extract single float values, if they exist, else default to 0 + json_t *checkval; + + personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); + + personality->combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); + + personality->chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); + + // Extract integer value, every fresh personality load sets this to 0 + personality->leave_percent = 0; + + // Set the skin, if provided + char* defaultSkin = "male/grunt"; + + checkval = json_object_get(root, "skin"); + char* validatedSkin = validate_pref_string(checkval, 1); + if (validatedSkin != NULL) { + // If skin value is valid, use it + personality->skin = validatedSkin; + } else { + // If skin value is invalid, use defaultSkin and print a warning + personality->skin = strdup(defaultSkin); // Use strdup to copy defaultSkin + } + + // Set the name, if provided + char* defaultName = "AqtionBot"; + + checkval = json_object_get(root, "name"); + char* validatedName = validate_pref_string(checkval, 0); + if (validatedName != NULL) { + // If name value is valid, use it + personality->name = validatedName; + } else { + // If name value is invalid, use defaultName and print a warning + personality->name = strdup(defaultName); // Use strdup to copy defaultName + } + + // Clean up + json_decref(root); + + return personality; +} + +int main() { + // Example usage of load_bot_personality + bot_personality_t* personality = load_bot_personality("bot.json"); + if (personality != NULL) { + printf("\n"); + printf("Weapon Preferences:"); + for (int i = 1; i < WEAPON_COUNT; i++) { // Assuming WEAPON_PREFS_SIZE is defined + printf(" %f", personality->weapon_prefs[i]); + } + printf("\nItem Preferences:"); + for (int i = 1; i < ITEM_COUNT; i++) { // Assuming ITEM_PREFS_SIZE is defined + printf(" %f", personality->item_prefs[i]); + } + printf("\n"); + // Print Map Preferences + printf("Map Preferences: %f\n", personality->map_prefs); + // Print Chat Demeanor + printf("Chat Demeanor: %f\n", personality->chat_demeanor); + + // Print Combat Demeanor + printf("Combat Demeanor: %f\n", personality->combat_demeanor); + + // Print Skin + printf("Skin: %s\n", personality->skin); + + printf("\n"); + // + if (!warningCount) + printf("Bot personality loaded successfully.\n"); + else + printf("Loaded with %i warnings\n", warningCount); + + // Remember to free the allocated memory for personality + free(personality); + } else { + printf("Failed to load bot personality.\n"); + } + return 0; +} diff --git a/src/action/g_local.h b/src/action/g_local.h index c15e0f959..b4a2f9235 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2181,6 +2181,21 @@ typedef enum { BOT_TYPE_BOTLIB, // BOTLIB -- New bots } bot_type_t; +// Most float values here are between -1 and 1 +typedef struct bot_personality_s +{ + // These are +1 because we're ignoring the first index [0] + // So that MK23_NUM (1) stays at 1 here as well + float weapon_prefs[WEAPON_COUNT + 1]; //-1 = Will never choose, 1 = Will always choose + float item_prefs[ITEM_COUNT +1]; //-1 = Will never choose, 1 = Will always choose + + float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love + float combat_demeanor; //-1 = Timid | 1 = Aggressive + float chat_demeanor; //-1 = Quiet | 1 = Chatty + int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. + +} bot_personality_t; + typedef struct bot_s { int bot_type; diff --git a/src/action/g_main.c b/src/action/g_main.c index 6e2075ee5..ad08b587a 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -493,7 +493,8 @@ cvar_t* bot_rush; // Bots rush players by going directly for them cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 max: 100] cvar_t* bot_randskill; // When random bot join a game, they pick a random skill [min: 1, max: 10]. Using 0 will turn this off. cvar_t* bot_randname; // Allow bots to pick a random name -cvar_t* bot_chat; // Bots chat +cvar_t* bot_chat; // Enable generic bot chat +cvar_t* bot_personality; // Enable bot personality functionality //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 4f5050a21..ed0757823 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -694,6 +694,7 @@ void InitGame( void ) bot_randskill = gi.cvar("bot_randskill", "10", 0); bot_randname = gi.cvar("bot_randname", "1", 0); bot_chat = gi.cvar("bot_chat", "0", 0); + bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From 3902401dfa614a309764b25a3bd31916edb46e49 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 31 Jul 2024 13:14:08 +0300 Subject: [PATCH 416/974] Support decoding 24-bit PCX images. These images already exist in the wild, so we might as well support them. Most other Q2 engines will render them corrupted, so print a warning whenever such file is encountered. --- src/refresh/images.c | 145 ++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index a8ea172cf..c7e704c91 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -180,44 +180,38 @@ PCX LOADING ================================================================= */ -static int uncompress_pcx(const byte *raw, const byte *end, - int w, int h, int scan, byte *pixels) +#define PCX_PALETTE_SIZE 768 + +static int uncompress_pcx(sizebuf_t *s, int scan, byte *pix) { - int dataByte, runLength; + for (int x = 0; x < scan;) { + int dataByte, runLength; - for (int y = 0; y < h; y++, pixels += w) { - for (int x = 0; x < scan;) { - if (raw >= end) - return Q_ERR_OVERRUN; - dataByte = *raw++; - - if ((dataByte & 0xC0) == 0xC0) { - runLength = dataByte & 0x3F; - if (x + runLength > scan) - return Q_ERR_OVERRUN; - if (raw >= end) - return Q_ERR_OVERRUN; - dataByte = *raw++; - } else { - runLength = 1; - } + if ((dataByte = SZ_ReadByte(s)) == -1) + return Q_ERR_UNEXPECTED_EOF; - while (runLength--) { - if (x < w) - pixels[x] = dataByte; - x++; - } + if ((dataByte & 0xC0) == 0xC0) { + runLength = dataByte & 0x3F; + if (x + runLength > scan) + return Q_ERR_OVERRUN; + if ((dataByte = SZ_ReadByte(s)) == -1) + return Q_ERR_UNEXPECTED_EOF; + } else { + runLength = 1; } + + while (runLength--) + pix[x++] = dataByte; } return Q_ERR_SUCCESS; } -static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, - byte *palette, int *width, int *height) +static int load_pcx(const byte *rawdata, size_t rawlen, + image_t *image, byte *palette, byte **pic) { const dpcx_t *pcx; - int w, h, scan; + int w, h, bytes_per_line; // // parse the PCX file @@ -233,7 +227,7 @@ static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, } if (pcx->encoding != 1 || pcx->bits_per_pixel != 8) { - Com_SetLastError("Invalid encoding or bits per pixel"); + Com_SetLastError("Unsupported encoding or bits per pixel"); return Q_ERR_INVALID_FORMAT; } @@ -244,13 +238,13 @@ static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, return Q_ERR_INVALID_FORMAT; } - if (pcx->color_planes != 1) { - Com_SetLastError("Invalid number of color planes"); + if (pcx->color_planes != 1 && (palette || pcx->color_planes != 3)) { + Com_SetLastError("Unsupported number of color planes"); return Q_ERR_INVALID_FORMAT; } - scan = LittleShort(pcx->bytes_per_line); - if (scan < w) { + bytes_per_line = LittleShort(pcx->bytes_per_line); + if (bytes_per_line < w) { Com_SetLastError("Invalid number of bytes per line"); return Q_ERR_INVALID_FORMAT; } @@ -259,56 +253,81 @@ static int load_pcx(const byte *rawdata, size_t rawlen, byte **pixels_p, // get palette // if (palette) { - if (rawlen < 768) { + if (rawlen < PCX_PALETTE_SIZE) { return Q_ERR_FILE_TOO_SMALL; } - memcpy(palette, rawdata + rawlen - 768, 768); + memcpy(palette, rawdata + rawlen - PCX_PALETTE_SIZE, PCX_PALETTE_SIZE); } // // get pixels // - if (pixels_p) { - byte *pixels = IMG_AllocPixels(w * h); - int ret = uncompress_pcx(pcx->data, rawdata + rawlen, w, h, scan, pixels); + if (image) { + int bytes_per_scanline = bytes_per_line * pcx->color_planes; + bool is_pal = pcx->color_planes == 1; + byte *scanline, *pixels, *out; + sizebuf_t s; + int ret = 0; + + SZ_InitRead(&s, rawdata, rawlen); + s.readcount = q_offsetof(dpcx_t, data); + + out = pixels = IMG_AllocPixels(w * h * (is_pal ? 1 : 4)); + scanline = IMG_AllocPixels(bytes_per_scanline); + + for (int y = 0; y < h; y++) { + ret = uncompress_pcx(&s, bytes_per_scanline, scanline); + if (ret < 0) + break; + if (is_pal) { + memcpy(out, scanline, w); + out += w; + } else { + for (int x = 0; x < w; x++, out += 4) { + out[0] = scanline[x]; + out[1] = scanline[x + bytes_per_line]; + out[2] = scanline[x + bytes_per_line * 2]; + out[3] = 255; + } + } + } + + IMG_FreePixels(scanline); + if (ret < 0) { IMG_FreePixels(pixels); return ret; } - *pixels_p = pixels; - } - if (width) - *width = w; - if (height) - *height = h; + if (is_pal) { + if (SZ_Remaining(&s) < PCX_PALETTE_SIZE) + Com_WPrintf("PCX file %s possibly corrupted\n", image->name); - return Q_ERR_SUCCESS; -} + if (image->type == IT_SKIN) + IMG_FloodFill(pixels, w, h); -IMG_LOAD(PCX) -{ - byte *pixels; - int w, h, ret; + *pic = IMG_AllocPixels(w * h * 4); - ret = load_pcx(rawdata, rawlen, &pixels, NULL, &w, &h); - if (ret < 0) - return ret; - - if (image->type == IT_SKIN) - IMG_FloodFill(pixels, w, h); - - *pic = IMG_AllocPixels(w * h * 4); + image->flags |= IMG_Unpack8((uint32_t *)*pic, pixels, w, h); - image->upload_width = image->width = w; - image->upload_height = image->height = h; - image->flags |= IMG_Unpack8((uint32_t *)*pic, pixels, w, h); + IMG_FreePixels(pixels); + } else { + Com_WPrintf("%s is a 24-bit PCX file. This is not portable.\n", image->name); + *pic = pixels; + image->flags |= IF_OPAQUE; + } - IMG_FreePixels(pixels); + image->upload_width = image->width = w; + image->upload_height = image->height = h; + } return Q_ERR_SUCCESS; } +IMG_LOAD(PCX) +{ + return load_pcx(rawdata, rawlen, image, NULL, pic); +} /* ================================================================= @@ -2163,7 +2182,7 @@ R_GetPalette */ void IMG_GetPalette(void) { - byte pal[768], *src, *data; + byte pal[PCX_PALETTE_SIZE], *src, *data; int i, ret; // get the palette @@ -2172,7 +2191,7 @@ void IMG_GetPalette(void) goto fail; } - ret = load_pcx(data, ret, NULL, pal, NULL, NULL); + ret = load_pcx(data, ret, NULL, pal, NULL); FS_FreeFile(data); From 9f889834b0f30a3f43206d04faa83b2564fdf81b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 31 Jul 2024 21:23:31 +0300 Subject: [PATCH 417/974] Clear stufftext buffer when demo playback stops. Prevents demos from executing commands by stuffing them right before demo playback stops. --- inc/common/cmd.h | 3 +++ src/client/demo.c | 3 +++ src/common/cmd.c | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/inc/common/cmd.h b/inc/common/cmd.h index c88422b24..f74c5570a 100644 --- a/inc/common/cmd.h +++ b/inc/common/cmd.h @@ -79,6 +79,9 @@ void Cbuf_Execute(cmdbuf_t *buf); void Cbuf_Frame(cmdbuf_t *buf); // Called once per frame. Decrements waitCount, resets aliasCount. +void Cbuf_Clear(cmdbuf_t *buf); +// Clears entire buffer text. + //=========================================================================== /* diff --git a/src/client/demo.c b/src/client/demo.c index e33940898..bac994fa1 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -1262,6 +1262,9 @@ void CL_CleanupDemos(void) cls.demo.time_frames, sec, fps); } } + + // clear whatever stufftext remains + Cbuf_Clear(&cl_cmdbuf); } CL_FreeDemoSnapshots(); diff --git a/src/common/cmd.c b/src/common/cmd.c index 7a25c7692..816ed8b38 100644 --- a/src/common/cmd.c +++ b/src/common/cmd.c @@ -203,6 +203,16 @@ void Cbuf_Frame(cmdbuf_t *buf) buf->aliasCount = 0; // don't allow infinite alias loops } +/* +============ +Cbuf_Clear +============ +*/ +void Cbuf_Clear(cmdbuf_t *buf) +{ + buf->cursize = buf->waitCount = buf->aliasCount = 0; +} + /* ============================================================================== From 68c933704a7cad69194348ed012546fb73a6906b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 1 Aug 2024 00:38:26 +0300 Subject: [PATCH 418/974] Make sound file loading errors more visible. --- src/client/sound/al.c | 5 ++++- src/client/sound/dma.c | 2 +- src/client/sound/mem.c | 35 ++++++++++++++++++++--------------- src/client/sound/ogg.c | 36 ++++++++++++++++++------------------ 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/client/sound/al.c b/src/client/sound/al.c index 351e5ff26..8f98b3bd7 100644 --- a/src/client/sound/al.c +++ b/src/client/sound/al.c @@ -166,11 +166,14 @@ static sfxcache_t *AL_UploadSfx(sfx_t *s) qalGetError(); qalGenBuffers(1, &buffer); - if (qalGetError()) + if (qalGetError()) { + Com_SetLastError("Failed to generate buffer"); goto fail; + } qalBufferData(buffer, format, s_info.data, size, s_info.rate); if (qalGetError()) { + Com_SetLastError("Failed to upload samples"); qalDeleteBuffers(1, &buffer); goto fail; } diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index 444bae89a..ddad75f27 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -58,7 +58,7 @@ static sfxcache_t *DMA_UploadSfx(sfx_t *sfx) int outcount = s_info.samples / stepscale; if (!outcount) { - Com_DPrintf("%s resampled to zero length\n", s_info.name); + Com_SetLastError("Resampled to zero length"); sfx->error = Q_ERR_INVALID_FORMAT; return NULL; } diff --git a/src/client/sound/mem.c b/src/client/sound/mem.c index a2d42b016..28d1eb5f4 100644 --- a/src/client/sound/mem.c +++ b/src/client/sound/mem.c @@ -71,13 +71,13 @@ static bool GetWavinfo(sizebuf_t *sz) // find "RIFF" chunk if (tag != TAG_RIFF) { - Com_DPrintf("%s has missing/invalid RIFF chunk\n", s_info.name); + Com_SetLastError("Missing RIFF chunk"); return false; } sz->readcount += 4; if (SZ_ReadLong(sz) != TAG_WAVE) { - Com_DPrintf("%s has missing/invalid WAVE chunk\n", s_info.name); + Com_SetLastError("Missing WAVE chunk"); return false; } @@ -86,25 +86,25 @@ static bool GetWavinfo(sizebuf_t *sz) // find "fmt " chunk if (!FindChunk(sz, TAG_fmt)) { - Com_DPrintf("%s has missing/invalid fmt chunk\n", s_info.name); + Com_SetLastError("Missing fmt chunk"); return false; } s_info.format = SZ_ReadShort(sz); if (s_info.format != FORMAT_PCM) { - Com_DPrintf("%s has unsupported format\n", s_info.name); + Com_SetLastError("Unsupported PCM format"); return false; } s_info.channels = SZ_ReadShort(sz); if (s_info.channels < 1 || s_info.channels > 2) { - Com_DPrintf("%s has bad number of channels\n", s_info.name); + Com_SetLastError("Unsupported number of channels"); return false; } s_info.rate = SZ_ReadLong(sz); if (s_info.rate < 8000 || s_info.rate > 48000) { - Com_DPrintf("%s has bad rate\n", s_info.name); + Com_SetLastError("Unsupported sample rate"); return false; } @@ -112,16 +112,12 @@ static bool GetWavinfo(sizebuf_t *sz) width = SZ_ReadShort(sz); switch (width) { case 8: - s_info.width = 1; - break; case 16: - s_info.width = 2; - break; case 24: - s_info.width = 3; + s_info.width = width / 8; break; default: - Com_DPrintf("%s has bad width\n", s_info.name); + Com_SetLastError("Unsupported number of bits per sample"); return false; } @@ -129,17 +125,22 @@ static bool GetWavinfo(sizebuf_t *sz) sz->readcount = next_chunk; chunk_len = FindChunk(sz, TAG_data); if (!chunk_len) { - Com_DPrintf("%s has missing/invalid data chunk\n", s_info.name); + Com_SetLastError("Missing data chunk"); return false; } // calculate length in samples s_info.samples = chunk_len / (s_info.width * s_info.channels); - if (s_info.samples < 1 || s_info.samples > MAX_SFX_SAMPLES) { - Com_DPrintf("%s has bad number of samples\n", s_info.name); + if (s_info.samples < 1) { + Com_SetLastError("No samples"); + return false; + } + if (s_info.samples > MAX_SFX_SAMPLES) { + Com_SetLastError("Too many samples"); return false; } +// any errors are non-fatal from this point s_info.data = sz->data + sz->readcount; s_info.loopstart = -1; @@ -238,6 +239,8 @@ sfxcache_t *S_LoadSound(sfx_t *s) len = FS_LoadFile(name, (void **)&data); if (!data) { + if (len != Q_ERR(ENOENT)) + Com_EPrintf("Couldn't load %s: %s\n", Com_MakePrintable(name), Q_ErrorString(len)); s->error = len; return NULL; } @@ -263,6 +266,8 @@ sfxcache_t *S_LoadSound(sfx_t *s) #endif fail: + if (!sc) + Com_EPrintf("Couldn't load %s: %s\n", Com_MakePrintable(name), Com_GetLastError()); FS_FreeFile(data); return sc; } diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index 1d1fc3a2e..1f8f6a5b7 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -472,32 +472,32 @@ bool OGG_Load(sizebuf_t *sz) const AVInputFormat *fmt = av_find_input_format("ogg"); if (!fmt) { - Com_DPrintf("Ogg input format not found\n"); + Com_SetLastError("Ogg input format not found"); return false; } const AVCodec *dec = avcodec_find_decoder(AV_CODEC_ID_VORBIS); if (!dec) { - Com_DPrintf("Vorbis decoder not found\n"); + Com_SetLastError("Vorbis decoder not found"); return false; } fmt_ctx = avformat_alloc_context(); if (!fmt_ctx) { - Com_DPrintf("Failed to allocate format context\n"); + Com_SetLastError("Failed to allocate format context"); return false; } avio_ctx_buffer = av_malloc(avio_ctx_buffer_size); if (!avio_ctx_buffer) { - Com_DPrintf("Failed to allocate avio buffer\n"); + Com_SetLastError("Failed to allocate avio buffer"); goto fail; } avio_ctx = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size, 0, sz, sz_read_packet, NULL, sz_seek); if (!avio_ctx) { - Com_DPrintf("Failed to allocate avio context\n"); + Com_SetLastError("Failed to allocate avio context"); goto fail; } @@ -505,51 +505,51 @@ bool OGG_Load(sizebuf_t *sz) ret = avformat_open_input(&fmt_ctx, NULL, fmt, NULL); if (ret < 0) { - Com_DPrintf("Couldn't open %s: %s\n", s_info.name, av_err2str(ret)); + Com_SetLastError(av_err2str(ret)); goto fail; } if (fmt_ctx->nb_streams != 1) { - Com_DPrintf("Multiple streams in %s\n", s_info.name); + Com_SetLastError("Multiple Ogg streams are not supported"); goto fail; } st = fmt_ctx->streams[0]; if (st->codecpar->codec_id != AV_CODEC_ID_VORBIS) { - Com_DPrintf("First stream is not Vorbis in %s\n", s_info.name); + Com_SetLastError("First stream is not Vorbis"); goto fail; } if (st->codecpar->ch_layout.nb_channels < 1 || st->codecpar->ch_layout.nb_channels > 2) { - Com_DPrintf("%s has bad number of channels\n", s_info.name); + Com_SetLastError("Unsupported number of channels"); goto fail; } if (st->codecpar->sample_rate < 8000 || st->codecpar->sample_rate > 48000) { - Com_DPrintf("%s has bad rate\n", s_info.name); + Com_SetLastError("Unsupported sample rate"); goto fail; } if (st->duration < 1 || st->duration > MAX_SFX_SAMPLES) { - Com_DPrintf("%s has bad number of samples\n", s_info.name); + Com_SetLastError("Unsupported duration"); goto fail; } dec_ctx = avcodec_alloc_context3(dec); if (!dec_ctx) { - Com_DPrintf("Failed to allocate codec context\n"); + Com_SetLastError("Failed to allocate codec context"); goto fail; } ret = avcodec_parameters_to_context(dec_ctx, st->codecpar); if (ret < 0) { - Com_DPrintf("Failed to copy codec parameters to decoder context\n"); + Com_SetLastError("Failed to copy codec parameters to decoder context"); goto fail; } ret = avcodec_open2(dec_ctx, dec, NULL); if (ret < 0) { - Com_DPrintf("Failed to open codec\n"); + Com_SetLastError("Failed to open codec"); goto fail; } @@ -560,7 +560,7 @@ bool OGG_Load(sizebuf_t *sz) out = av_frame_alloc(); swr_ctx = swr_alloc(); if (!pkt || !frame || !out || !swr_ctx) { - Com_DPrintf("Failed to allocate memory\n"); + Com_SetLastError("Failed to allocate memory"); goto fail; } @@ -570,7 +570,7 @@ bool OGG_Load(sizebuf_t *sz) ret = av_channel_layout_copy(&out->ch_layout, &dec_ctx->ch_layout); if (ret < 0) { - Com_DPrintf("Failed to copy channel layout\n"); + Com_SetLastError("Failed to copy channel layout"); goto fail; } out->format = AV_SAMPLE_FMT_S16; @@ -579,7 +579,7 @@ bool OGG_Load(sizebuf_t *sz) ret = av_frame_get_buffer(out, 0); if (ret < 0) { - Com_DPrintf("Failed to allocate audio buffer\n"); + Com_SetLastError("Failed to allocate audio buffer"); goto fail; } @@ -637,7 +637,7 @@ bool OGG_Load(sizebuf_t *sz) } if (ret < 0) { - Com_DPrintf("Error decoding %s: %s\n", s_info.name, av_err2str(ret)); + Com_SetLastError(av_err2str(ret)); Z_Freep(&s_info.data); goto fail; } From cd90691735d80f43d910798cf400ac87f29a2acd Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 1 Aug 2024 00:39:27 +0300 Subject: [PATCH 419/974] Allow sample rate as low as 6000 Hz. --- src/client/sound/mem.c | 2 +- src/client/sound/ogg.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/sound/mem.c b/src/client/sound/mem.c index 28d1eb5f4..1eca02fed 100644 --- a/src/client/sound/mem.c +++ b/src/client/sound/mem.c @@ -103,7 +103,7 @@ static bool GetWavinfo(sizebuf_t *sz) } s_info.rate = SZ_ReadLong(sz); - if (s_info.rate < 8000 || s_info.rate > 48000) { + if (s_info.rate < 6000 || s_info.rate > 48000) { Com_SetLastError("Unsupported sample rate"); return false; } diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index 1f8f6a5b7..fdd0abce1 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -525,7 +525,7 @@ bool OGG_Load(sizebuf_t *sz) goto fail; } - if (st->codecpar->sample_rate < 8000 || st->codecpar->sample_rate > 48000) { + if (st->codecpar->sample_rate < 6000 || st->codecpar->sample_rate > 48000) { Com_SetLastError("Unsupported sample rate"); goto fail; } From 8d0bc711cd57895a49d7995871b55c24b70666e7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 1 Aug 2024 10:27:09 +0300 Subject: [PATCH 420/974] Drop unneeded asserts. --- src/client/sound/ogg.c | 4 +--- src/refresh/models.c | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index fdd0abce1..380d194d8 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -585,10 +585,8 @@ bool OGG_Load(sizebuf_t *sz) int64_t nb_samples = st->duration; - if (out->sample_rate != dec_ctx->sample_rate) { + if (out->sample_rate != dec_ctx->sample_rate) nb_samples = av_rescale_rnd(st->duration + 2, out->sample_rate, dec_ctx->sample_rate, AV_ROUND_UP) + 2; - Q_assert(nb_samples <= INT_MAX >> out->ch_layout.nb_channels); - } int bufsize = nb_samples << out->ch_layout.nb_channels; int offset = 0; diff --git a/src/refresh/models.c b/src/refresh/models.c index 383676ebe..515edd7a8 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -233,7 +233,6 @@ static const char *MOD_ValidateMD2(const dmd2header_t *header, size_t length) ENSURE(header->num_frames <= MD2_MAX_FRAMES, "Too many frames"); ENSURE(header->num_skins <= MD2_MAX_SKINS, "Too many skins"); - Q_assert(header->num_xyz); ENSURE(header->framesize >= sizeof(dmd2frame_t) + (header->num_xyz - 1) * sizeof(dmd2trivertx_t), "Too small frame size"); ENSURE(header->framesize <= MD2_MAX_FRAMESIZE, "Too big frame size"); From a2c3ceb2a19caa89e49f60814a8beb4cad7b9414 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 1 Aug 2024 10:27:32 +0300 Subject: [PATCH 421/974] Allow listing images filtered by paletted flag. Also rename --placeholder option to --missing. --- src/refresh/images.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index c7e704c91..e77cda49d 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1496,13 +1496,15 @@ static cvar_t *r_texture_overrides; static cvar_t *r_glowmaps; static const cmd_option_t o_imagelist[] = { + { "8", "pal", "list paletted images" }, { "f", "fonts", "list fonts" }, { "h", "help", "display this help message" }, { "m", "skins", "list skins" }, { "p", "pics", "list pics" }, - { "P", "placeholder", "list placeholder images" }, + { "r", "rgb", "list rgb images" }, { "s", "sprites", "list sprites" }, { "w", "walls", "list walls" }, + { "x", "missing", "list missing images" }, { "y", "skies", "list skies" }, { NULL } }; @@ -1522,7 +1524,8 @@ static void IMG_List_f(void) static const char types[8] = "PFMSWY??"; image_t *image; const char *wildcard = NULL; - bool placeholder = false; + bool missing = false; + int paletted = 0; int i, c, mask = 0, count; size_t texels; @@ -1534,7 +1537,9 @@ static void IMG_List_f(void) case 's': mask |= 1 << IT_SPRITE; break; case 'w': mask |= 1 << IT_WALL; break; case 'y': mask |= 1 << IT_SKY; break; - case 'P': placeholder = true; break; + case '8': paletted = 1; break; + case 'r': paletted = -1; break; + case 'x': missing = true; break; case 'h': Cmd_PrintUsage(o_imagelist, "[wildcard]"); Com_Printf("List registered images.\n"); @@ -1572,7 +1577,11 @@ static void IMG_List_f(void) continue; if (wildcard && !Com_WildCmp(wildcard, image->name)) continue; - if ((image->width && image->height) == placeholder) + if ((image->width && image->height) == missing) + continue; + if (paletted == 1 && !(image->flags & IF_PALETTED)) + continue; + if (paletted == -1 && (image->flags & IF_PALETTED)) continue; Com_Printf("%c%c%c%c %4i %4i %s: %s\n", From 3cb9af375fd25f53cf7dc20e8223f85d7c5030f5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 1 Aug 2024 16:36:16 +0300 Subject: [PATCH 422/974] Access internal APIs via pointer to const vtable. --- inc/client/video.h | 2 +- src/client/console.c | 10 +++++---- src/client/input.c | 18 ++++++++-------- src/client/refresh.c | 20 +++++++++--------- src/client/sound/dma.c | 25 ++++++++++++----------- src/client/sound/main.c | 44 ++++++++++++++++++++-------------------- src/client/sound/mem.c | 2 +- src/client/sound/ogg.c | 16 +++++++-------- src/client/sound/sound.h | 6 +++--- src/client/ui/servers.c | 4 ++-- src/common/field.c | 4 ++-- src/refresh/draw.c | 4 ++-- src/refresh/main.c | 14 ++++++------- src/refresh/qgl.c | 10 ++++----- src/refresh/texture.c | 4 ++-- src/unix/system.c | 4 ++-- 16 files changed, 95 insertions(+), 92 deletions(-) diff --git a/inc/client/video.h b/inc/client/video.h index a9b058443..aecbd7e04 100644 --- a/inc/client/video.h +++ b/inc/client/video.h @@ -52,7 +52,7 @@ extern cvar_t *vid_modelist; extern cvar_t *vid_fullscreen; extern cvar_t *_vid_fullscreen; -extern vid_driver_t vid; +extern const vid_driver_t *vid; bool VID_GetFullscreen(vrect_t *rc, int *freq_p, int *depth_p); bool VID_GetGeometry(vrect_t *rc); diff --git a/src/client/console.c b/src/client/console.c index ba4b46d3d..b019a5fb1 100644 --- a/src/client/console.c +++ b/src/client/console.c @@ -570,8 +570,8 @@ void CL_LoadState(load_state_t state) { con.loadstate = state; SCR_UpdateScreen(); - if (vid.pump_events) - vid.pump_events(); + if (vid) + vid->pump_events(); } /* @@ -1223,12 +1223,14 @@ void Key_Console(int key) } if (key == 'v' && Key_IsDown(K_CTRL)) { - Con_Paste(vid.get_clipboard_data); + if (vid) + Con_Paste(vid->get_clipboard_data); goto scroll; } if ((key == K_INS && Key_IsDown(K_SHIFT)) || key == K_MOUSE3) { - Con_Paste(vid.get_selection_data); + if (vid) + Con_Paste(vid->get_selection_data); goto scroll; } diff --git a/src/client/input.c b/src/client/input.c index 67a61062a..fb5586615 100644 --- a/src/client/input.c +++ b/src/client/input.c @@ -108,8 +108,8 @@ IN_Activate */ void IN_Activate(void) { - if (vid.grab_mouse) { - vid.grab_mouse(IN_GetCurrentGrab()); + if (vid && vid->grab_mouse) { + vid->grab_mouse(IN_GetCurrentGrab()); } } @@ -143,8 +143,8 @@ IN_WarpMouse */ void IN_WarpMouse(int x, int y) { - if (vid.warp_mouse) { - vid.warp_mouse(x, y); + if (vid && vid->warp_mouse) { + vid->warp_mouse(x, y); } } @@ -159,8 +159,8 @@ void IN_Shutdown(void) in_grab->changed = NULL; } - if (vid.shutdown_mouse) { - vid.shutdown_mouse(); + if (vid && vid->shutdown_mouse) { + vid->shutdown_mouse(); } memset(&input, 0, sizeof(input)); @@ -190,7 +190,7 @@ void IN_Init(void) return; } - if (!vid.init_mouse()) { + if (!vid || !vid->init_mouse || !vid->init_mouse()) { Cvar_Set("in_enable", "0"); return; } @@ -449,13 +449,13 @@ static void CL_MouseMove(void) float mx, my; float speed; - if (!vid.get_mouse_motion) { + if (!vid || !vid->get_mouse_motion) { return; } if (cls.key_dest & (KEY_MENU | KEY_CONSOLE)) { return; } - if (!vid.get_mouse_motion(&dx, &dy)) { + if (!vid->get_mouse_motion(&dx, &dy)) { return; } diff --git a/src/client/refresh.c b/src/client/refresh.c index 4cfff3c4f..67df2d2b4 100644 --- a/src/client/refresh.c +++ b/src/client/refresh.c @@ -29,7 +29,7 @@ cvar_t *vid_modelist; cvar_t *vid_fullscreen; cvar_t *_vid_fullscreen; -vid_driver_t vid; +const vid_driver_t *vid; #define MODE_GEOMETRY 1 #define MODE_FULLSCREEN 2 @@ -269,22 +269,22 @@ void CL_RunRefresh(void) return; } - vid.pump_events(); + vid->pump_events(); if (mode_changed) { if (mode_changed & MODE_FULLSCREEN) { - vid.set_mode(); + vid->set_mode(); if (vid_fullscreen->integer) { Cvar_Set("_vid_fullscreen", vid_fullscreen->string); } } else { if (vid_fullscreen->integer) { if (mode_changed & MODE_MODELIST) { - vid.set_mode(); + vid->set_mode(); } } else { if (mode_changed & MODE_GEOMETRY) { - vid.set_mode(); + vid->set_mode(); } } } @@ -356,7 +356,7 @@ void CL_InitRefresh(void) bool ok = false; for (i = 0; vid_drivers[i]; i++) { if (!strcmp(vid_drivers[i]->name, vid_driver->string)) { - vid = *vid_drivers[i]; + vid = vid_drivers[i]; ok = R_Init(true); break; } @@ -379,7 +379,7 @@ void CL_InitRefresh(void) for (i = 0; vid_drivers[i]; i++) { if (i == tried || !vid_drivers[i]->probe || !vid_drivers[i]->probe()) continue; - vid = *vid_drivers[i]; + vid = vid_drivers[i]; if ((ok = R_Init(true))) break; } @@ -389,11 +389,11 @@ void CL_InitRefresh(void) if (!ok) Com_Error(ERR_FATAL, "Couldn't initialize refresh: %s", Com_GetLastError()); - modelist = vid.get_mode_list(); + modelist = vid->get_mode_list(); vid_modelist = Cvar_Get("vid_modelist", modelist, 0); Z_Free(modelist); - vid.set_mode(); + vid->set_mode(); cls.ref_initialized = true; @@ -436,7 +436,7 @@ void CL_ShutdownRefresh(void) R_Shutdown(true); - memset(&vid, 0, sizeof(vid)); + vid = NULL; cls.ref_initialized = false; diff --git a/src/client/sound/dma.c b/src/client/sound/dma.c index ddad75f27..a9ce742eb 100644 --- a/src/client/sound/dma.c +++ b/src/client/sound/dma.c @@ -523,7 +523,7 @@ static const snddma_driver_t *const s_drivers[] = { NULL }; -static snddma_driver_t snddma; +static const snddma_driver_t *snddma; static void DMA_SoundInfo(void) { @@ -549,8 +549,8 @@ static bool DMA_Init(void) for (i = 0; s_drivers[i]; i++) { if (!strcmp(s_drivers[i]->name, s_driver->string)) { - snddma = *s_drivers[i]; - ret = snddma.init(); + snddma = s_drivers[i]; + ret = snddma->init(); break; } } @@ -560,8 +560,8 @@ static bool DMA_Init(void) for (i = 0; s_drivers[i]; i++) { if (i == tried) continue; - snddma = *s_drivers[i]; - if ((ret = snddma.init()) == SIS_SUCCESS) + snddma = s_drivers[i]; + if ((ret = snddma->init()) == SIS_SUCCESS) break; } Cvar_Reset(s_driver); @@ -585,7 +585,8 @@ static bool DMA_Init(void) static void DMA_Shutdown(void) { - snddma.shutdown(); + snddma->shutdown(); + snddma = NULL; s_numchannels = 0; s_underwater_gain_hf->changed = NULL; @@ -594,9 +595,9 @@ static void DMA_Shutdown(void) static void DMA_Activate(void) { - if (snddma.activate) { + if (snddma->activate) { S_StopAllSounds(); - snddma.activate(s_active); + snddma->activate(s_active); } } @@ -630,10 +631,10 @@ static int DMA_DriftBeginofs(float timeofs) static void DMA_ClearBuffer(void) { - snddma.begin_painting(); + snddma->begin_painting(); if (dma.buffer) memset(dma.buffer, dma.samplebits == 8 ? 0x80 : 0, dma.samples * dma.samplebits / 8); - snddma.submit(); + snddma->submit(); } /* @@ -851,7 +852,7 @@ static void DMA_Update(void) } #endif - snddma.begin_painting(); + snddma->begin_painting(); if (!dma.buffer) return; @@ -878,7 +879,7 @@ static void DMA_Update(void) PaintChannels(endtime); - snddma.submit(); + snddma->submit(); } static int DMA_GetSampleRate(void) diff --git a/src/client/sound/main.c b/src/client/sound/main.c index 12d4ff3b3..9d227b79a 100644 --- a/src/client/sound/main.c +++ b/src/client/sound/main.c @@ -30,7 +30,7 @@ int s_numchannels; sndstarted_t s_started; bool s_active; -sndapi_t s_api; +const sndapi_t *s_api; vec3_t listener_origin; vec3_t listener_forward; @@ -77,7 +77,7 @@ static void S_SoundInfo_f(void) return; } - s_api.sound_info(); + s_api->sound_info(); } static void S_SoundList_f(void) @@ -159,14 +159,14 @@ void S_Init(void) #if USE_OPENAL if (s_started == SS_NOT && s_enable->integer >= SS_OAL && snd_openal.init()) { s_started = SS_OAL; - s_api = snd_openal; + s_api = &snd_openal; } #endif #if USE_SNDDMA if (s_started == SS_NOT && s_enable->integer >= SS_DMA && snd_dma.init()) { s_started = SS_DMA; - s_api = snd_dma; + s_api = &snd_dma; } #endif @@ -206,8 +206,8 @@ void S_Init(void) static void S_FreeSound(sfx_t *sfx) { - if (s_api.delete_sfx) - s_api.delete_sfx(sfx); + if (s_api->delete_sfx) + s_api->delete_sfx(sfx); Z_Free(sfx->cache); Z_Free(sfx->truename); memset(sfx, 0, sizeof(*sfx)); @@ -237,8 +237,8 @@ void S_Shutdown(void) S_FreeAllSounds(); OGG_Stop(); - s_api.shutdown(); - memset(&s_api, 0, sizeof(s_api)); + s_api->shutdown(); + s_api = NULL; s_started = SS_NOT; s_active = false; @@ -269,9 +269,9 @@ void S_Activate(void) s_active = active; if (!active) - s_api.drop_raw_samples(); + s_api->drop_raw_samples(); - s_api.activate(); + s_api->activate(); } // ======================================================================= @@ -503,8 +503,8 @@ void S_EndRegistration(void) continue; } // make sure it is paged in - if (s_api.page_in_sfx) - s_api.page_in_sfx(sfx); + if (s_api->page_in_sfx) + s_api->page_in_sfx(sfx); } // load everything in @@ -562,8 +562,8 @@ channel_t *S_PickChannel(int entnum, int entchannel) return NULL; ch = &s_channels[first_to_die]; - if (s_api.stop_channel) - s_api.stop_channel(ch); + if (s_api->stop_channel) + s_api->stop_channel(ch); memset(ch, 0, sizeof(*ch)); return ch; @@ -647,7 +647,7 @@ void S_IssuePlaysound(playsound_t *ps) ch->pos = 0; ch->end = s_paintedtime + sc->length; - s_api.play_channel(ch); + s_api->play_channel(ch); // free the playsound S_FreePlaysound(ps); @@ -707,7 +707,7 @@ void S_StartSound(const vec3_t origin, int entnum, int entchannel, qhandle_t hSf ps->attenuation = attenuation; ps->volume = vol; ps->sfx = sfx; - ps->begin = s_api.get_begin_ofs(timeofs); + ps->begin = s_api->get_begin_ofs(timeofs); // sort into the pending sound list LIST_FOR_EACH(playsound_t, sort, &s_pendingplays, entry) @@ -776,7 +776,7 @@ void S_StopAllSounds(void) for (i = 0; i < MAX_PLAYSOUNDS; i++) List_Append(&s_freeplays, &s_playsounds[i].entry); - s_api.stop_all_sounds(); + s_api->stop_all_sounds(); // clear all the channels memset(s_channels, 0, sizeof(s_channels)); @@ -784,14 +784,14 @@ void S_StopAllSounds(void) void S_RawSamples(int samples, int rate, int width, int channels, const byte *data) { - if (s_started) - s_api.raw_samples(samples, rate, width, channels, data, 1.0f); + if (s_api) + s_api->raw_samples(samples, rate, width, channels, data, 1.0f); } int S_GetSampleRate(void) { - if (s_started && s_api.get_sample_rate) - return s_api.get_sample_rate(); + if (s_api && s_api->get_sample_rate) + return s_api->get_sample_rate(); return 0; } @@ -874,5 +874,5 @@ void S_Update(void) OGG_Update(); - s_api.update(); + s_api->update(); } diff --git a/src/client/sound/mem.c b/src/client/sound/mem.c index 1eca02fed..53aed24c9 100644 --- a/src/client/sound/mem.c +++ b/src/client/sound/mem.c @@ -258,7 +258,7 @@ sfxcache_t *S_LoadSound(sfx_t *s) if (s_info.format == FORMAT_PCM) ConvertSamples(); - sc = s_api.upload_sfx(s); + sc = s_api->upload_sfx(s); #if USE_AVCODEC if (s_info.format != FORMAT_PCM) diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index 380d194d8..d07cb2d17 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -271,8 +271,8 @@ void OGG_Stop(void) { ogg_stop(); - if (s_started) - s_api.drop_raw_samples(); + if (s_api) + s_api->drop_raw_samples(); } static int read_packet(AVPacket *pkt) @@ -339,7 +339,7 @@ static int convert_samples(AVFrame *in) return 0; // get available free space - out->nb_samples = s_api.need_raw_samples(); + out->nb_samples = s_api->need_raw_samples(); Q_assert((unsigned)out->nb_samples <= MAX_RAW_SAMPLES); ret = swr_convert_frame(ogg.swr_ctx, out, in); @@ -350,10 +350,10 @@ static int convert_samples(AVFrame *in) Com_DDDPrintf("%d raw samples\n", out->nb_samples); - if (!s_api.raw_samples(out->nb_samples, out->sample_rate, 2, - out->ch_layout.nb_channels, - out->data[0], ogg_volume->value)) - s_api.drop_raw_samples(); + if (!s_api->raw_samples(out->nb_samples, out->sample_rate, 2, + out->ch_layout.nb_channels, + out->data[0], ogg_volume->value)) + s_api->drop_raw_samples(); return 1; } @@ -385,7 +385,7 @@ void OGG_Update(void) if (!s_started || !s_active || !ogg.dec_ctx) return; - while (s_api.need_raw_samples() > 0) { + while (s_api->need_raw_samples() > 0) { int ret = convert_samples(NULL); // if swr buffer is empty, decode more input diff --git a/src/client/sound/sound.h b/src/client/sound/sound.h index 3dd237d6d..8b46dcf58 100644 --- a/src/client/sound/sound.h +++ b/src/client/sound/sound.h @@ -146,9 +146,9 @@ typedef enum { #endif } sndstarted_t; -extern sndstarted_t s_started; -extern bool s_active; -extern sndapi_t s_api; +extern sndstarted_t s_started; +extern bool s_active; +extern const sndapi_t *s_api; #define MAX_CHANNELS 32 extern channel_t s_channels[MAX_CHANNELS]; diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index 98279cfa8..94c48787a 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -397,8 +397,8 @@ static menuSound_t CopyAddress(void) slot = m_servers.list.items[m_servers.list.curvalue]; - if (vid.set_clipboard_data) - vid.set_clipboard_data(slot->hostname); + if (vid && vid->set_clipboard_data) + vid->set_clipboard_data(slot->hostname); return QMS_OUT; } diff --git a/src/common/field.c b/src/common/field.c index cdc62d205..657c708dc 100644 --- a/src/common/field.c +++ b/src/common/field.c @@ -130,8 +130,8 @@ bool IF_KeyEvent(inputField_t *field, int key) } if (key == 'c' && Key_IsDown(K_CTRL)) { - if (vid.set_clipboard_data) - vid.set_clipboard_data(field->text); + if (vid && vid->set_clipboard_data) + vid->set_clipboard_data(field->text); return true; } diff --git a/src/refresh/draw.c b/src/refresh/draw.c index b48b011d7..23d060643 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -275,8 +275,8 @@ static int get_auto_scale(void) scale = 2; } - if (vid.get_dpi_scale) { - int min_scale = vid.get_dpi_scale(); + if (vid && vid->get_dpi_scale) { + int min_scale = vid->get_dpi_scale(); return max(scale, min_scale); } diff --git a/src/refresh/main.c b/src/refresh/main.c index 1ad8dcaf9..d906daaa1 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -793,7 +793,7 @@ void R_EndFrame(void) GL_ShowErrors(__func__); - vid.swap_buffers(); + vid->swap_buffers(); } // ============================================================================== @@ -889,8 +889,8 @@ static void gl_novis_changed(cvar_t *self) static void gl_swapinterval_changed(cvar_t *self) { - if (vid.swap_interval) - vid.swap_interval(self->integer); + if (vid && vid->swap_interval) + vid->swap_interval(self->integer); } static void GL_Register(void) @@ -1096,11 +1096,11 @@ bool R_Init(bool total) } Com_Printf("------- R_Init -------\n"); - Com_Printf("Using video driver: %s\n", vid.name); + Com_Printf("Using video driver: %s\n", vid->name); // initialize OS-specific parts of OpenGL // create the window and set up the context - if (!vid.init()) { + if (!vid->init()) { return false; } @@ -1133,7 +1133,7 @@ bool R_Init(bool total) memset(&gl_static, 0, sizeof(gl_static)); memset(&gl_config, 0, sizeof(gl_config)); QGL_Shutdown(); - vid.shutdown(); + vid->shutdown(); return false; } @@ -1162,7 +1162,7 @@ void R_Shutdown(bool total) QGL_Shutdown(); // shut down OS specific OpenGL stuff like contexts, etc. - vid.shutdown(); + vid->shutdown(); GL_Unregister(); diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 008225779..21648465c 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -341,7 +341,7 @@ static bool parse_version(void) int major, minor, ver; bool gl_es = false; - qglGetString = vid.get_proc_addr("glGetString"); + qglGetString = vid->get_proc_addr("glGetString"); if (!qglGetString) return false; @@ -487,8 +487,8 @@ bool QGL_Init(void) } if (gl_config.ver_gl >= QGL_VER(3, 0) || gl_config.ver_es >= QGL_VER(3, 0)) { - qglGetStringi = vid.get_proc_addr("glGetStringi"); - qglGetIntegerv = vid.get_proc_addr("glGetIntegerv"); + qglGetStringi = vid->get_proc_addr("glGetStringi"); + qglGetIntegerv = vid->get_proc_addr("glGetIntegerv"); if (!qglGetStringi || !qglGetIntegerv) { Com_EPrintf("Required OpenGL entry points not found\n"); return false; @@ -543,7 +543,7 @@ bool QGL_Init(void) if (sec->functions) { for (func = sec->functions; func->name; func++) { const char *name = func->name; - void *addr = vid.get_proc_addr(name); + void *addr = vid->get_proc_addr(name); // try with XYZ suffix if this is a GL_XYZ_extension if (!addr && !core) { @@ -552,7 +552,7 @@ bool QGL_Init(void) suf[3] = 0; if (!strstr(name, suf)) { name = va("%s%s", name, suf); - addr = vid.get_proc_addr(name); + addr = vid->get_proc_addr(name); } } diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 2b669816c..e8d7c46d3 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -821,8 +821,8 @@ static void GL_BuildGammaTables(void) static void gl_gamma_changed(cvar_t *self) { GL_BuildGammaTables(); - if (vid.update_gamma) - vid.update_gamma(gammatable); + if (vid && vid->update_gamma) + vid->update_gamma(gammatable); } static const byte dottexture[8][8] = { diff --git a/src/unix/system.c b/src/unix/system.c index 5da04571f..2d60f360e 100644 --- a/src/unix/system.c +++ b/src/unix/system.c @@ -208,8 +208,8 @@ void Sys_Error(const char *error, ...) tty_shutdown_input(); #if USE_REF - if (vid.fatal_shutdown) - vid.fatal_shutdown(); + if (vid && vid->fatal_shutdown) + vid->fatal_shutdown(); #endif va_start(argptr, error); From 46ace8121ed929c62b9e5629fd75afefd6e7ec21 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 2 Aug 2024 00:19:52 +0300 Subject: [PATCH 423/974] =?UTF-8?q?Add=20=E2=80=98demomap=E2=80=99=20compa?= =?UTF-8?q?tibility=20mode=20to=20=E2=80=98demo=E2=80=99=20command.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intended to bring demo playback closer to original Q2. Allows executing stufftext commands, which can be used for playing scripted/chained demos. Also changes FOV behavior to match original. The next commit will use this for ‘demomap’ command emulation. --- src/client/client.h | 1 + src/client/demo.c | 4 +++- src/client/entities.c | 13 ++++++++++--- src/client/main.c | 2 +- src/client/screen.c | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 9dadb9a6f..18ef57227 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -482,6 +482,7 @@ typedef struct { bool paused; bool seeking; bool eof; + bool compat; // demomap compatibility mode msgEsFlags_t esFlags; // for snapshots/recording msgPsFlags_t psFlags; } demo; diff --git a/src/client/demo.c b/src/client/demo.c index bac994fa1..d19189b54 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -749,6 +749,7 @@ static void CL_PlayDemo_f(void) CL_Disconnect(ERR_RECONNECT); cls.demo.playback = f; + cls.demo.compat = !strcmp(Cmd_Argv(2), "compat"); cls.state = ca_connected; Q_strlcpy(cls.servername, COM_SkipPath(name), sizeof(cls.servername)); cls.serverAddress.type = NA_LOOPBACK; @@ -1264,7 +1265,8 @@ void CL_CleanupDemos(void) } // clear whatever stufftext remains - Cbuf_Clear(&cl_cmdbuf); + if (!cls.demo.compat) + Cbuf_Clear(&cl_cmdbuf); } CL_FreeDemoSnapshots(); diff --git a/src/client/entities.c b/src/client/entities.c index 804ebd73c..94d3bfa42 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1073,8 +1073,15 @@ static void CL_AddViewWeapon(void) return; } - if (info_hand->integer == 2 && cl_gun->integer == 1) { - return; + if (cl_gun->integer == 1) { + // don't draw gun if in wide angle view + if (cls.demo.playback && cls.demo.compat && cl.frame.ps.fov > 90) { + return; + } + // don't draw gun if center handed + if (info_hand->integer == 2) { + return; + } } // find states to interpolate between @@ -1265,7 +1272,7 @@ static void CL_FinishViewValues(void) static inline float lerp_client_fov(float ofov, float nfov, float lerp) { - if (cls.demo.playback) { + if (cls.demo.playback && !cls.demo.compat) { int fov = info_fov->integer; if (fov < 1) diff --git a/src/client/main.c b/src/client/main.c index 8c6c3fac6..b3e6940e7 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2522,7 +2522,7 @@ static void exec_server_string(cmdbuf_t *buf, const char *text) } // forbid nearly every command from demos - if (cls.demo.playback) { + if (cls.demo.playback && !cls.demo.compat) { if (strcmp(s, "play")) { return; } diff --git a/src/client/screen.c b/src/client/screen.c index 019b65dbf..06ebd08e1 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -334,7 +334,7 @@ static void SCR_DrawDemo(void) } if (cls.demo.playback) { - if (cls.demo.file_size) { + if (cls.demo.file_size && !cls.demo.compat) { draw_progress_bar( cls.demo.file_progress, sv_paused->integer && From 6547c411914b52760d7b169d8247f35b239d1361 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 2 Aug 2024 00:58:08 +0300 Subject: [PATCH 424/974] =?UTF-8?q?Add=20more=20complete=20=E2=80=98demoma?= =?UTF-8?q?p=E2=80=99=20command=20emulation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detect ‘.dm2’ extension in SV_ParseMapCmd() and turn ‘demomap demo.dm2’ command into ‘demo demo.dm2 compat’, while setting ‘nextserver’ properly. Allows mods that utilize demos as cinematics to work. --- doc/client.asciidoc | 14 +++++++++++++- src/server/commands.c | 37 +++++++++++++++++++++++-------------- src/server/init.c | 13 +++++++++++++ src/server/server.h | 4 ++++ 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 78df01808..ff6b68ead 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -1244,6 +1244,16 @@ NOTE: By default, during demo playback, Q2PRO overrides FOV value stored in demo file with value of local ‘fov’ variable, unless stored FOV value is less than 90. This behavior can be changed with ‘uf’ variable (see above). +NOTE: Recommened way to play demos in Q2PRO is the ‘demo’ command, but +‘demomap’ is still supported for compatibility. Playing demos using ‘demomap’ +enables some compatibility options, such as hiding player gun in wide angle +view. + +WARNING: Unlike ‘demo’ command, ‘demomap’ allows executing stufftext commands +from demos. Demos may change client settings, trick the client into connecting +to random servers, and have other security implications. Only play demos from +trusted sources using ‘demomap’! + seek [+-][%]:: Seeks the given amount of time during demo playback. Prepend with ‘+’ to seek forward relative to current position, prepend with ‘-’ to seek @@ -1604,7 +1614,9 @@ it used to be you know where to look. The following list may be incomplete. - ‘clientport’ variable has been renamed to ‘net_clientport’, and ‘ip_clientport’ alias is no longer supported. -- ‘demomap’ command has been removed in favor of ‘demo’ and ‘mvdplay’. +- ‘demomap’ command is emulated: if filename with ‘.dm2’ extension is specified, + client side demo playback is started. Otherwise ‘demomap’ command is equivalent + to ‘gamemap’ (attract loop mode is not supported). - Q2PRO works only with virtual paths constrained to the quake file system. All paths are normalized before use so that it is impossible to go past virtual diff --git a/src/server/commands.c b/src/server/commands.c index fc2724da0..58a597bef 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -263,6 +263,14 @@ static void SV_Map(bool restart) if (!SV_ParseMapCmd(&cmd)) return; +#if USE_CLIENT + // hack for demomap + if (cmd.state == ss_demo) { + Cbuf_InsertText(&cmd_buffer, va("demo \"%s\" compat\n", cmd.server)); + return; + } +#endif + // save pending CM to be freed later if ERR_DROP is thrown Com_AbortFunc(abort_func, &cmd.cm); @@ -290,21 +298,15 @@ SV_DemoMap_f Puts the server in demo mode on a specific map/cinematic ================== */ -#if USE_CLIENT static void SV_DemoMap_f(void) { - char *s = Cmd_Argv(1); + if (Cmd_Argc() != 2) { + Com_Printf("Usage: %s \n", Cmd_Argv(0)); + return; + } - if (!COM_CompareExtension(s, ".dm2")) - Cbuf_InsertText(&cmd_buffer, va("demo \"%s\"\n", s)); - else if (!COM_CompareExtension(s, ".cin")) - Cbuf_InsertText(&cmd_buffer, va("map \"%s\" force\n", s)); - else if (*s) - Com_Printf("\"%s\" only supports .dm2 and .cin files\n", Cmd_Argv(0)); - else - Com_Printf("Usage: %s \n", Cmd_Argv(0)); + SV_Map(false); } -#endif /* ================== @@ -423,6 +425,15 @@ static void SV_Map_c(genctx_t *ctx, int argnum) } } +static void SV_DemoMap_c(genctx_t *ctx, int argnum) +{ +#if USE_CLIENT + if (argnum == 1) { + FS_File_g("demos", ".dm2", FS_SEARCH_RECURSIVE, ctx); + } +#endif +} + static void SV_DumpEnts_f(void) { bsp_t *c = sv.cm.cache; @@ -1741,9 +1752,7 @@ static const cmdreg_t c_server[] = { { "stuffcvar", SV_StuffCvar_f, SV_SetPlayer_c }, { "printall", SV_PrintAll_f }, { "map", SV_Map_f, SV_Map_c }, -#if USE_CLIENT - { "demomap", SV_DemoMap_f }, -#endif + { "demomap", SV_DemoMap_f, SV_DemoMap_c }, { "gamemap", SV_GameMap_f, SV_Map_c }, { "dumpents", SV_DumpEnts_f }, { "setmaster", SV_SetMaster_f }, diff --git a/src/server/init.c b/src/server/init.c index 8975925fe..07615bca1 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -253,6 +253,19 @@ static bool check_server(mapcmd_t *cmd, const char *server, bool nextserver) ret = SCR_CheckForCinematic(expanded); } cmd->state = ss_cinematic; + } else if (!COM_CompareExtension(s, ".dm2")) { + if (!sv_cinematics->integer && nextserver) + return false; // skip it + if (Q_concat(expanded, sizeof(expanded), "demos/", s) < sizeof(expanded)) { +#if USE_CLIENT + ret = cmd->loadgame ? Q_ERR(ENOSYS) : FS_LoadFile(expanded, NULL); + if (ret == Q_ERR(EFBIG)) + ret = Q_ERR_SUCCESS; +#else + ret = Q_ERR(ENOSYS); +#endif + } + cmd->state = ss_demo; } else { CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); if (Q_concat(expanded, sizeof(expanded), "maps/", s, ".bsp") < sizeof(expanded)) { diff --git a/src/server/server.h b/src/server/server.h index 25c858122..2862d1fab 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -597,6 +597,10 @@ void sv_min_timeout_changed(cvar_t *self); // // sv_init.c // + +// not a real server state! hack for demomap command. +#define ss_demo -1 + void SV_ClientReset(client_t *client); void SV_SetState(server_state_t state); void SV_SpawnServer(const mapcmd_t *cmd); From 50da5ec6f0cbe8f8cf2d930ad763bef702d900dc Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Aug 2024 13:20:00 -0400 Subject: [PATCH 425/974] Initial try for personality traits --- bot.json | 44 +- meson.build | 30 +- meson_options.txt | 5 + src/action/botlib/bot_personality.c | 257 ------------ src/action/botlib/botlib.h | 23 ++ src/action/botlib/botlib_ai.c | 2 + src/action/botlib/botlib_communication.c | 15 +- src/action/botlib/botlib_personality.c | 498 +++++++++++++++++++++++ src/action/botlib/botlib_spawn.c | 17 +- src/action/g_local.h | 9 + src/action/g_spawn.c | 4 + src/action/p_client.c | 12 +- 12 files changed, 636 insertions(+), 280 deletions(-) delete mode 100644 src/action/botlib/bot_personality.c create mode 100644 src/action/botlib/botlib_personality.c diff --git a/bot.json b/bot.json index fe1c92af8..293f30ed4 100644 --- a/bot.json +++ b/bot.json @@ -1,9 +1,37 @@ { - "name": "bbbbbaskdimasoidmasiodmob", - "skin": "male/terrorbbbbbaskdimasoidmasiodmobbbbbbaskdimasoidmasiodmobbbbbbaskdimasoidmasiodmob", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": 0.75, - "combat_demeanor": 0.45, - "chat_demeanor": 50 -} + "bots": { + "bob": { + "skin": "male/terror", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67 + }, + "combat_demeanor": 0.45, + "chat_demeanor": 50 + }, + "bill": { + "skin": "male/terror", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67 + }, + "combat_demeanor": 0.45, + "chat_demeanor": 50 + }, + "sam": { + "skin": "male/terror", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67 + }, + "combat_demeanor": 0.45, + "chat_demeanor": 50 + } + } +} \ No newline at end of file diff --git a/meson.build b/meson.build index c539a58cd..f93286aa2 100644 --- a/meson.build +++ b/meson.build @@ -244,6 +244,7 @@ botlib_src = [ 'src/action/botlib/botlib_spawn.c', 'src/action/botlib/botlib_spawnpoints.c', 'src/action/botlib/botlib_weapons.c', + 'src/action/botlib/botlib_personality.c' ] cc = meson.get_compiler('c') @@ -256,6 +257,8 @@ cpuremap = { 'aarch64': 'arm64', } +inc_dirs = ['inc'] + cpu = host_machine.cpu_family() if cpu in cpuremap cpu = cpuremap[cpu] @@ -422,7 +425,25 @@ if not win32 allow_fallback: win32, default_options: fallback_opt, ) - game_deps += uuid + + if uuid.found() + client_deps += uuid + config.set10('USE_UUID', true) + dll_link_args += '-luuid' + endif + + jansson = dependency('jansson', + required: get_option('jansson'), + allow_fallback: win32, + default_options: fallback_opt, + ) + + if jansson.found() + client_deps += jansson + config.set10('USE_JANSSON', true) + dll_link_args += '-L/opt/homebrew/lib' + dll_link_args += '-ljansson' + endif endif if host_machine.system() == 'darwin' @@ -433,6 +454,7 @@ if host_machine.system() == 'darwin' default_options: fallback_opt, ) client_deps += opengl + inc_dirs += '/opt/homebrew/include' endif # require FFmpeg >= 5.1.3 @@ -553,7 +575,7 @@ endif executable('q2pro', common_src, client_src, refresh_src, dependencies: common_deps + client_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', win_subsystem: 'windows,6.0', link_args: exe_link_args, @@ -563,7 +585,7 @@ executable('q2pro', common_src, client_src, refresh_src, executable('q2proded', common_src, server_src, dependencies: common_deps + server_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', win_subsystem: 'console,6.0', link_args: exe_link_args, @@ -574,7 +596,7 @@ executable('q2proded', common_src, server_src, shared_library('game' + cpu, action_src, name_prefix: '', dependencies: game_deps, - include_directories: 'inc', + include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', link_args: dll_link_args, install: system_wide, diff --git a/meson_options.txt b/meson_options.txt index 729b33e83..433ad7908 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -74,6 +74,11 @@ option('libcurl', value: 'auto', description: 'libcurl support') +option('jansson', + type: 'feature', + value: 'auto', + description: 'AQtion jansson support') + option('libjpeg', type: 'feature', value: 'auto', diff --git a/src/action/botlib/bot_personality.c b/src/action/botlib/bot_personality.c deleted file mode 100644 index 4b107ce51..000000000 --- a/src/action/botlib/bot_personality.c +++ /dev/null @@ -1,257 +0,0 @@ -//#include "../g_local.h" -#include "/opt/homebrew/include/jansson.h" -#include -#include -#include - -int warningCount = 0; - -#define WEAPON_COUNT 9 -#define ITEM_COUNT 6 - -typedef struct bot_personality_s -{ - // These are +1 because we're ignoring the first index [0] - // So that MK23_NUM (1) stays at 1 here as well - float weapon_prefs[WEAPON_COUNT + 1]; //-1 = Will never choose, 1 = Will always choose - float item_prefs[ITEM_COUNT +1]; //-1 = Will never choose, 1 = Will always choose - - float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love - float combat_demeanor; //-1 = Timid | 1 = Aggressive - float chat_demeanor; //-1 = Quiet | 1 = Chatty - int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. - char* skin; - char* name; -} bot_personality_t; - -#define MK23_NUM 1 -#define MP5_NUM 2 -#define M4_NUM 3 -#define M3_NUM 4 -#define HC_NUM 5 -#define SNIPER_NUM 6 -#define DUAL_NUM 7 -#define KNIFE_NUM 8 -#define GRENADE_NUM 9 - -#define SIL_NUM 10 -#define SLIP_NUM 11 -#define BAND_NUM 12 -#define KEV_NUM 13 -#define LASER_NUM 14 -#define HELM_NUM 15 - -#define MK23_NAME "MK23 Pistol" -#define MP5_NAME "MP5/10 Submachinegun" -#define M4_NAME "M4 Assault Rifle" -#define M3_NAME "M3 Super 90 Assault Shotgun" -#define HC_NAME "Handcannon" -#define SNIPER_NAME "Sniper Rifle" -#define DUAL_NAME "Dual MK23 Pistols" -#define KNIFE_NAME "Combat Knife" -#define GRENADE_NAME "M26 Fragmentation Grenade" - -#define SIL_NAME "Silencer" -#define SLIP_NAME "Stealth Slippers" -#define BAND_NAME "Bandolier" -#define KEV_NAME "Kevlar Vest" -#define HELM_NAME "Kevlar Helmet" -#define LASER_NAME "Lasersight" - -/////* -// This file focuses on loading bot personality traits -/////* - -// Data Validation -float validate_pref_numeric(json_t* value, const char* prefName) { - float numValue = 0.0f; - - if (value) { - if (json_is_real(value)) { - numValue = (float)json_real_value(value); - } else if (json_is_integer(value)) { - numValue = (float)json_integer_value(value); - } - - // Check if the value is within the bounds [-1, 1] - if (numValue < -1.0f || numValue > 1.0f) { - warningCount++; - printf("Warning: Value %f for '%s' is out of bounds. It must be between -1 and 1.\n", numValue, prefName); - return 0.0f; // Value is out of bounds - } - - return numValue; // Value is valid and within bounds - } - - warningCount++; - printf("Warning: Invalid or missing value for '%s'.\n", prefName); - return 0.0f; // Return 0 if validation fails -} -char* validate_pref_string(json_t* value, int stringtype) { - const char* str = json_string_value(value); - size_t len = strlen(str); - int maxlen = 0; - char* valuename; - if (value && json_is_string(value)) { - if (stringtype == 0) { - maxlen = 16; - valuename = "name"; - } else { - maxlen = 64; - valuename = "skin"; - } - if (len > 0 && len <= maxlen) { - return strdup(str); // Return a copy of the string if valid - } - } - warningCount++; - printf("Warning: Invalid %s (%s). Using default value.\n", valuename, str); - return NULL; // Return NULL if validation fails -} - -// Function to load a bot personality from a JSON file using libjansson -bot_personality_t* load_bot_personality(const char* filename) { - // Open the file - FILE* file = fopen(filename, "r"); - if (!file) { - perror("Failed to open file"); - return NULL; - } - - // Use jansson's json_loadf to parse the file directly into a json_t object - json_error_t error; - json_t* root = json_loadf(file, 0, &error); - fclose(file); // Close the file as soon as it's no longer needed - - if (!root) { - fprintf(stderr, "Error parsing JSON: %s\n", error.text); - return NULL; - } - - // Allocate memory for the bot_personality_t struct - bot_personality_t* personality = (bot_personality_t*)malloc(sizeof(bot_personality_t)); - if (!personality) { - json_decref(root); // Ensure to release the json_t object to prevent memory leaks - return NULL; - } - - // Extract and assign values from the JSON object - // Define an array of weapon names corresponding to their preferences - const char* weaponNames[] = { - MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, - SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME - }; - - json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); - for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { - json_t* wpref_value = json_array_get(weapon_prefs, i); - // Ensure the index is within the bounds of weaponNames array - const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; - float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name - - // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) - if (value != 0.0f) { - personality->weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered - } - } - - // Assuming you have an array of preference names corresponding to each index - // Define an array of item names corresponding to their preferences - const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; - - json_t* item_prefs = json_object_get(root, "item_prefs"); - for (size_t i = 0; i < json_array_size(item_prefs); i++) { - json_t* ipref_value = json_array_get(item_prefs, i); - // Ensure the index is within the bounds of itemNames array - const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; - float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings - - // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases - if (value != 0.0f) { - personality->item_prefs[i] = value; - } - } - - // Extract single float values, if they exist, else default to 0 - json_t *checkval; - - personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); - - personality->combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); - - personality->chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); - - // Extract integer value, every fresh personality load sets this to 0 - personality->leave_percent = 0; - - // Set the skin, if provided - char* defaultSkin = "male/grunt"; - - checkval = json_object_get(root, "skin"); - char* validatedSkin = validate_pref_string(checkval, 1); - if (validatedSkin != NULL) { - // If skin value is valid, use it - personality->skin = validatedSkin; - } else { - // If skin value is invalid, use defaultSkin and print a warning - personality->skin = strdup(defaultSkin); // Use strdup to copy defaultSkin - } - - // Set the name, if provided - char* defaultName = "AqtionBot"; - - checkval = json_object_get(root, "name"); - char* validatedName = validate_pref_string(checkval, 0); - if (validatedName != NULL) { - // If name value is valid, use it - personality->name = validatedName; - } else { - // If name value is invalid, use defaultName and print a warning - personality->name = strdup(defaultName); // Use strdup to copy defaultName - } - - // Clean up - json_decref(root); - - return personality; -} - -int main() { - // Example usage of load_bot_personality - bot_personality_t* personality = load_bot_personality("bot.json"); - if (personality != NULL) { - printf("\n"); - printf("Weapon Preferences:"); - for (int i = 1; i < WEAPON_COUNT; i++) { // Assuming WEAPON_PREFS_SIZE is defined - printf(" %f", personality->weapon_prefs[i]); - } - printf("\nItem Preferences:"); - for (int i = 1; i < ITEM_COUNT; i++) { // Assuming ITEM_PREFS_SIZE is defined - printf(" %f", personality->item_prefs[i]); - } - printf("\n"); - // Print Map Preferences - printf("Map Preferences: %f\n", personality->map_prefs); - // Print Chat Demeanor - printf("Chat Demeanor: %f\n", personality->chat_demeanor); - - // Print Combat Demeanor - printf("Combat Demeanor: %f\n", personality->combat_demeanor); - - // Print Skin - printf("Skin: %s\n", personality->skin); - - printf("\n"); - // - if (!warningCount) - printf("Bot personality loaded successfully.\n"); - else - printf("Loaded with %i warnings\n", warningCount); - - // Remember to free the allocated memory for personality - free(personality); - } else { - printf("Failed to load bot personality.\n"); - } - return 0; -} diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index aa5b39031..f3b5673d8 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -505,5 +505,28 @@ int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter); int BOTLIB_GetEquipment(edict_t* self); //rekkie -- collecting weapons, items, ammo -- e +// =========================================================================== +// botlib_personality.c +// =========================================================================== +// Copy of bot_personality_t struct +// Most float values here are between -1 and 1 +typedef struct temp_bot_personality_s +{ + float weapon_prefs[10]; //-1 = Will never choose, 1 = Will always choose + float item_prefs[6]; //-1 = Will never choose, 1 = Will always choose + float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love + float combat_demeanor; //-1 = Timid | 1 = Aggressive + float chat_demeanor; //-1 = Quiet | 1 = Chatty + int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. + char* skin_pref; // Skin preference, if DM mode +} temp_bot_personality_t; + +typedef struct { + char* name; + temp_bot_personality_t personality; +} bot_mapping_t; + +size_t BOTLIB_PersonalityCount(void); +bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 8df69435a..6ae5a2c43 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -814,6 +814,8 @@ void BOTLIB_Think(edict_t* self) self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies BOTLIB_Reload(self); // Reload the weapon if needed + // This doesn't mean that the bot sees itself as an enemy + // self->enemy is which bot the current self->bot is targeting if (self->enemy) { // Chase after the new enemy diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 2327e5c70..58bc6b138 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -102,7 +102,20 @@ char *botchat_insults[DBC_INSULTS] = ":>" }; -void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { +#define DBC_VICTORY 3 +char *botchat_victory[DBC_VICTORY] = +{ + "smoked!", + "whew", + "too ez" +}; + +void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) +{ + // Do nothing if bot_chat is disabled + if(!bot_chat->value) + return; + char* text = NULL; qboolean delayed = true; diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c new file mode 100644 index 000000000..1ca53aafe --- /dev/null +++ b/src/action/botlib/botlib_personality.c @@ -0,0 +1,498 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" +#include "/opt/homebrew/include/jansson.h" + +// Count of bot personalities loaded +char** botNames = NULL; // Global array of bot names +size_t loadedBotCount = 0; + +bot_mapping_t bot_mappings[MAX_BOTS]; + +// #define WEAPON_COUNT 9 +// #define ITEM_COUNT 6 + +// typedef struct bot_personality_s +// { +// // These are +1 because we're ignoring the first index [0] +// // So that MK23_NUM (1) stays at 1 here as well +// float weapon_prefs[WEAPON_COUNT + 1]; //-1 = Will never choose, 1 = Will always choose +// float item_prefs[ITEM_COUNT +1]; //-1 = Will never choose, 1 = Will always choose + +// float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love +// float combat_demeanor; //-1 = Timid | 1 = Aggressive +// float chat_demeanor; //-1 = Quiet | 1 = Chatty +// int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. +// float shooting_skill; // -1 = Poor Skill | 1 = Excellent Skill (higher value = harder bot) +// float movement_skill; // -1 = Poor Skill | 1 = Excellent Skill (higher value = harder bot + +// // Will use the edict's values here, these are only here for testing +// char* skin; +// char* name; +// } bot_personality_t; + +// #define MK23_NUM 1 +// #define MP5_NUM 2 +// #define M4_NUM 3 +// #define M3_NUM 4 +// #define HC_NUM 5 +// #define SNIPER_NUM 6 +// #define DUAL_NUM 7 +// #define KNIFE_NUM 8 +// #define GRENADE_NUM 9 + +// #define SIL_NUM 10 +// #define SLIP_NUM 11 +// #define BAND_NUM 12 +// #define KEV_NUM 13 +// #define LASER_NUM 14 +// #define HELM_NUM 15 + +// #define MK23_NAME "MK23 Pistol" +// #define MP5_NAME "MP5/10 Submachinegun" +// #define M4_NAME "M4 Assault Rifle" +// #define M3_NAME "M3 Super 90 Assault Shotgun" +// #define HC_NAME "Handcannon" +// #define SNIPER_NAME "Sniper Rifle" +// #define DUAL_NAME "Dual MK23 Pistols" +// #define KNIFE_NAME "Combat Knife" +// #define GRENADE_NAME "M26 Fragmentation Grenade" + +// #define SIL_NAME "Silencer" +// #define SLIP_NAME "Stealth Slippers" +// #define BAND_NAME "Bandolier" +// #define KEV_NAME "Kevlar Vest" +// #define HELM_NAME "Kevlar Helmet" +// #define LASER_NAME "Lasersight" + +/////* +// This file focuses on loading bot personality traits +/////* + +// Data Validation +float validate_pref_numeric(json_t* value, const char* prefName) { + float numValue = 0.0f; + + if (value) { + if (json_is_real(value)) { + numValue = (float)json_real_value(value); + } else if (json_is_integer(value)) { + numValue = (float)json_integer_value(value); + } + + // Check if the value is within the bounds [-1, 1] + if (numValue < -1.0f || numValue > 1.0f) { + + gi.dprintf("Warning: Value %f for '%s' is out of bounds. It must be between -1 and 1. Setting to 0.\n", numValue, prefName); + return 0.0f; // Value is out of bounds + } + + return numValue; // Value is valid and within bounds + } + + gi.dprintf("Warning: Invalid or missing value for '%s'.\n", prefName); + return 0.0f; // Return 0 if validation fails +} +char* validate_pref_string(json_t* value, int stringtype) { + const char* str = json_string_value(value); + size_t len = strlen(str); + int maxlen = 0; + char* valuename; + if (value && json_is_string(value)) { + if (stringtype == 0) { + maxlen = 16; + valuename = "name"; + } else { + maxlen = 64; + valuename = "skin"; + } + if (len > 0 && len <= maxlen) { + return strdup(str); // Return a copy of the string if valid + } + } + gi.dprintf("Warning: Invalid %s (%s). Using default value.\n", valuename, str); + gi.dprintf("Future improvement: run it through the randomizer instead\n"); + return NULL; // Return NULL if validation fails +} + +// qboolean json_load_check(const char* filename) +// { +// json_error_t error; +// json_t* root = json_loadf(filename, 0, &error); +// fclose(filename); + +// if (!root) { +// gi.dprintf("bot_personality: Error parsing JSON: %s\n", error.text); +// return NULL; +// } + +// json_t* bots = json_object_get(root, "bots"); +// if (!bots) { +// gi.dprintf("bot_personality: 'bots' object not found\n"); +// json_decref(root); +// return NULL; +// } +// } + + +// Evaluate leave percentage +/* + We will call this on every bot death and kill if + map_prefs is < 0 +*/ + +qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) +{ + // Don't do anything if the map_prefs are 0 to positive + if (self->bot.personality.map_prefs >= 0) + return false; + + // Deaths are worth more than frags when you don't like the map you're on + float fragplus = 0.05; + float deathplus = 0.07; + + // Death is true, Frag is false + if (frag_or_death) { + gi.dprintf("%s received a death, they're mad!\n", self->client->pers.netname); + self->bot.personality.map_prefs -= deathplus; + } else { + gi.dprintf("%s received a frag, they're happy!\n", self->client->pers.netname); + self->bot.personality.map_prefs += fragplus; + } + + float rage_quit_threshold = -1.0; // Example threshold value + if (self->bot.personality.map_prefs <= rage_quit_threshold) { + gi.dprintf("%s is rage quitting due to low map preference!\n", self->client->pers.netname); + return true; + } + + //Default return false, bot sticks around + return false; +} + + +// Dynamically update the map preferences of the bot +// Not all bots will have a preference, so we default to 0 for those +void update_map_pref(json_t* root, char* map_name, temp_bot_personality_t* personality) +{ + // Get the "map_prefs" object from the root + json_t* map_prefs = json_object_get(root, "map_prefs"); + if (!map_prefs) { + gi.dprintf("Map preferences not found.\n"); + return; + } + + // Fetch the value for the specified map name + json_t* value = json_object_get(map_prefs, map_name); + if (value && json_is_real(value)) { + // Update the personality struct with the fetched value + personality->map_prefs = (float)json_real_value(value); + gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, personality->map_prefs); + } else { + // If the map name is not found or the value is not a real number, default to 0.0f + personality->map_prefs = 0.0f; + gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, personality->map_prefs); + } +} + +// Function to load a bot personality from a JSON file using libjansson +bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) { + + // Open the file + FILE* file = fopen(filename, "r"); + if (!file) { + perror("Failed to open file"); + return NULL; + } + + json_error_t error; + json_t* root = json_loadf(file, 0, &error); + fclose(file); + + if (!root) { + gi.dprintf("bot_personality: Error parsing JSON: %s\n", error.text); + return NULL; + } + + json_t* bots = json_object_get(root, "bots"); + if (!bots) { + gi.dprintf("bot_personality: 'bots' object not found\n"); + json_decref(root); + return NULL; + } + + // json_t* bot_data = json_object_get(bots, bot_name); + // if (!bot_data) { + // gi.dprintf("bot_personality: Bot '%s' not found\n", bot_name); + // json_decref(root); + // return NULL; + // } + + bot_mapping_t* temp_bot = (bot_mapping_t*)malloc(sizeof(bot_mapping_t)); + if (!temp_bot) { + json_decref(root); + return NULL; + } + + // Extract and assign values from the JSON object + // Define an array of weapon names corresponding to their preferences + const char* weaponNames[] = { + MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, + SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME + }; + + json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); + for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { + json_t* wpref_value = json_array_get(weapon_prefs, i); + // Ensure the index is within the bounds of weaponNames array + const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; + float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name + + // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) + if (value != 0.0f) { + temp_bot->personality.weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered + } + } + + // Assuming you have an array of preference names corresponding to each index + // Define an array of item names corresponding to their preferences + const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; + + json_t* item_prefs = json_object_get(root, "item_prefs"); + for (size_t i = 0; i < json_array_size(item_prefs); i++) { + json_t* ipref_value = json_array_get(item_prefs, i); + // Ensure the index is within the bounds of itemNames array + const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; + float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings + + // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases + if (value != 0.0f) { + temp_bot->personality.item_prefs[i] = value; + } + } + + // Extract single float values, if they exist, else default to 0 + json_t *checkval; + + char* current_map_name = level.mapname; + //personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); + update_map_pref(root, current_map_name, temp_bot); + + temp_bot->personality.combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); + + temp_bot->personality.chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); + + // Extract integer value, every fresh personality load sets this to 0 + temp_bot->personality.leave_percent = 0; + + // Set the skin, if provided + char* defaultSkin = "male/grunt"; + + checkval = json_object_get(root, "skin"); + char* validatedSkin = validate_pref_string(checkval, 1); + if (validatedSkin != NULL) { + // If skin value is valid, use it + temp_bot->personality.skin_pref = validatedSkin; + } else { + // If skin value is invalid, use defaultSkin and print a warning + temp_bot->personality.skin_pref = strdup(defaultSkin); // Use strdup to copy defaultSkin + } + + // Set the name, if provided + char* defaultName = "AqtionBot"; + + checkval = json_object_get(root, "name"); + char* validatedName = validate_pref_string(checkval, 0); + if (validatedName != NULL) { + // If name value is valid, use it + temp_bot->name = validatedName; + } else { + // If name value is invalid, use defaultName and print a warning + temp_bot->name = strdup(defaultName); // Use strdup to copy defaultName + } + + // Clean up + json_decref(root); + + return temp_bot; +} + +// int main() { +// // Example usage of load_bot_personality +// bot_personality_t* personality = load_bot_personality("bot.json"); +// if (personality != NULL) { +// printf("\n"); +// printf("Weapon Preferences:"); +// for (int i = 1; i < WEAPON_COUNT; i++) { // Assuming WEAPON_PREFS_SIZE is defined +// printf(" %f", personality->weapon_prefs[i]); +// } +// printf("\nItem Preferences:"); +// for (int i = 1; i < ITEM_COUNT; i++) { // Assuming ITEM_PREFS_SIZE is defined +// printf(" %f", personality->item_prefs[i]); +// } +// printf("\n"); +// // Print Map Preferences +// printf("Map Preferences: %f\n", personality->map_prefs); +// // Print Chat Demeanor +// printf("Chat Demeanor: %f\n", personality->chat_demeanor); + +// // Print Combat Demeanor +// printf("Combat Demeanor: %f\n", personality->combat_demeanor); + +// // Print Skin +// printf("Skin: %s\n", personality->skin); + +// printf("\n"); +// // +// if (!warningCount) +// printf("Bot personality loaded successfully.\n"); +// else +// printf("Loaded with %i warnings\n", warningCount); + +// // Remember to free the allocated memory for personality +// free(personality); +// } else { +// printf("Failed to load bot personality.\n"); +// } +// return 0; +// } + +// Function to load bot names from file +// int loadBotNames(const char* filename) { +// json_error_t error; +// json_t* root = json_load_file(filename, 0, &error); + +// if (!root) { +// return 0; // Failed to load file +// } + +// json_t* bots = json_object_get(root, "bots"); +// if (!bots) { +// json_decref(root); +// return 0; // No "bots" object +// } + +// size_t fileBotCount = json_array_size(bots); +// // Adjust the number of bots to load based on game.maxclients +// loadedBotCount = (fileBotCount < game.maxclients) ? fileBotCount : game.maxclients; +// botNames = malloc(loadedBotCount * sizeof(char*)); // Allocate array for bot names + +// for (size_t i = 0; i < loadedBotCount; i++) { +// json_t* bot = json_array_get(bots, i); +// const char* name = json_string_value(json_object_get(bot, "name")); +// botNames[i] = strdup(name); // Copy name into array +// } + +// json_decref(root); +// return 1; // Success +// } +// // Function to get a random bot name +// char* getRandomBotName() { +// if (loadedBotCount == 0) return NULL; // No bots loaded +// srand(time(NULL)); // Seed the random number generator +// size_t index = rand() % loadedBotCount; +// return botNames[index]; // Return random bot name +// } + +// void freeBotNames() { +// for (size_t i = 0; i < loadedBotCount; i++) { +// free(botNames[i]); // Free each name +// } +// free(botNames); // Free the array +// botNames = NULL; +// loadedBotCount = 0; +// } + +// Function to count bots in the JSON file +size_t BOTLIB_PersonalityCount(void) { + json_t* root; + json_error_t error; + json_t* bots; + size_t bot_count = 0; + FILE* fIn; + char filename[128]; + cvar_t* game_dir = gi.cvar("game", "action", 0); + cvar_t* botdir = gi.cvar("botdir", "bots", 0); + +#ifdef _WIN32 + f = sprintf(filename, ".\\"); + f += sprintf(filename + f, game_dir->string); + f += sprintf(filename + f, "\\"); + f += sprintf(filename + f, botdir->string); + f += sprintf(filename + f, "\\bots.json"); +#else + strcpy(filename, "./"); + strcat(filename, game_dir->string); + strcat(filename, "/"); + strcat(filename, botdir->string); + strcat(filename, "/bots.json"); +#endif + + // Save file path for later references + strncpy(game.bot_file_path, filename, MAX_QPATH); + + if ((fIn = fopen(filename, "rb")) == NULL) // See if .json file exists + { + return 0; // No file + } + + // Rewind the file pointer to the beginning of the file + rewind(fIn); + + // Load JSON from file + root = json_loadf(fIn, 0, &error); + + if (!root) { + // Handle JSON parsing error + fprintf(stderr, "JSON error on line %d: %s\n", error.line, error.text); + json_decref(root); + return 0; // Return 0 to indicate failure + } + + // Get the "bots" object from the root of the JSON structure + bots = json_object_get(root, "bots"); + if (!json_is_object(bots)) { + // Handle error: "bots" is not an object or not found + json_decref(root); + return 0; // Return 0 to indicate failure + } + + // Count the number of keys (bots) in the "bots" object + bot_count = json_object_size(bots); + + // Clean up + json_decref(root); + fclose(fIn); + + return bot_count; // Return the count of bots +} + +// qboolean BOTLIB_LoadPersonality(char* botname) +// { +// int maxclients = game.maxclients; +// FILE* fIn; +// char tempfilename[128]; +// int fileSize = 0; +// int f, n, l, p; // File, nodes, links, paths +// qboolean randomized; + +// // Const-ify the filename +// const char* filename = strdup(game.bot_file_path); + +// if ((fIn = fopen(filename, "rb")) == NULL) // See if .json file exists +// { +// return false; // No file +// } + +// fclose(fIn); + +// Com_Printf("%s Loaded %s from disk\n", __func__, filename); + +// // If no botname provided, let's re-run with a random bot +// if(botname == NULL) +// BOTLIB_LoadPersonality(getRandomBotName()); + +// if(load_bot_personality()){ + +// } +// } \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 0c8c4097b..90d8b32a3 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1254,7 +1254,7 @@ int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender) } // Set the bot's userinfo (name, skin, hand, etc) -void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin) +void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin, qboolean personality) { int gender = INVALID; char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) @@ -1400,7 +1400,11 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else if (team == TEAM3) bot_connections.total_team3++; - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); // includes ClientConnect + if(bot_personality->value) {// Load personality data + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, true); // includes ClientConnect + } else { // Use random data + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, false); + } ClientBeginDeathmatch(bot); @@ -1458,9 +1462,7 @@ void BOTLIB_RemoveBot(char* name) freed = true; ClientDisconnect(bot); game.bot_count--; - if (bot_chat->value && !remove_all) - BOTLIB_Chat(bot, CHAT_GOODBYE); - //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + if (!remove_all) break; } @@ -1501,10 +1503,9 @@ void BOTLIB_RemoveBot(char* name) if (bot->is_bot && bot_to_kick == bot_count) { //darksaint -- Bot Chat -- s - // Generates chat message if respawning (goodbyes) - if (bot_chat->value) { + // Generates chat message (goodbyes) + if (bot_chat->value) BOTLIB_Chat(bot, CHAT_GOODBYE); - } //darksaint -- Bot Chat -- e //rekkie -- Fake Bot Client -- s diff --git a/src/action/g_local.h b/src/action/g_local.h index b4a2f9235..76a95f0fe 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -777,6 +777,11 @@ typedef struct // High Scores support from OpenFFA char dir[MAX_OSPATH]; // where variable data is stored + + // Bot personalities loaded + #ifndef NO_BOTS + const char* bot_file_path; + #endif } game_locals_t; @@ -2194,6 +2199,8 @@ typedef struct bot_personality_s float chat_demeanor; //-1 = Quiet | 1 = Chatty int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. + char name_pref; // Name preference + char skin_pref; // Skin preference, if DM mode } bot_personality_t; typedef struct bot_s @@ -2324,6 +2331,8 @@ typedef struct bot_s // int lastChatTime; // Last time the bot chatted + bot_personality_t personality; // Personality struct + } bot_t; #endif //rekkie -- e diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 0bd9315ca..ee3bcae39 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1594,6 +1594,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- Fake Bot Client -- s gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e + + if(bot_personality->value) + BOTLIB_PersonalityCount(); + BOTLIB_LoadPersonalities(game.bot_file_path); #endif //rekkie -- e } diff --git a/src/action/p_client.c b/src/action/p_client.c index 08fef2569..ddbd1d31c 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -500,6 +500,7 @@ void Add_Frag(edict_t * ent, int mod) // Random bot voice sounds // Debug this, sometimes bots will repeat this over and over again + // TODO: Disable this if we get too close to the max sound limit if (use_voice->value && ent->is_bot && bot_randvoice->value > 0) { if (bot_randvoice->value > 100) bot_randvoice->value = 100; @@ -793,6 +794,14 @@ void Add_Frag(edict_t * ent, int mod) } } + #ifndef NO_BOTS + //darksaint -- Bot Chat -- s + // Generates chat message if respawning (killed) + if (ent->is_bot) + BOTLIB_Chat(ent, CHAT_INSULTS); + #endif + //darksaint -- Bot Chat -- e + // Announce kill streak to player if use_killcounts is enabled on server if (use_killcounts->value) { // Report only killstreak during that round @@ -1572,9 +1581,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) #ifndef NO_BOTS //darksaint -- Bot Chat -- s // Generates chat message if respawning (killed) - if (bot_chat->value && self->is_bot) { + if (self->is_bot) BOTLIB_Chat(self, CHAT_KILLED); - } #endif //darksaint -- Bot Chat -- e From d5000bf0f1ccf850fb5f00686a0ac79801bacb5d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Aug 2024 21:51:14 -0400 Subject: [PATCH 426/974] Bot personalities appear to be loaded in memory --- bot.json | 37 ---- src/action/botlib/botlib_personality.c | 251 +++++++++++++++++-------- src/action/g_local.h | 2 +- src/action/g_spawn.c | 1 - 4 files changed, 172 insertions(+), 119 deletions(-) delete mode 100644 bot.json diff --git a/bot.json b/bot.json deleted file mode 100644 index 293f30ed4..000000000 --- a/bot.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "bots": { - "bob": { - "skin": "male/terror", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67 - }, - "combat_demeanor": 0.45, - "chat_demeanor": 50 - }, - "bill": { - "skin": "male/terror", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67 - }, - "combat_demeanor": 0.45, - "chat_demeanor": 50 - }, - "sam": { - "skin": "male/terror", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67 - }, - "combat_demeanor": 0.45, - "chat_demeanor": 50 - } - } -} \ No newline at end of file diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 1ca53aafe..05f23fe7c 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -94,10 +94,17 @@ float validate_pref_numeric(json_t* value, const char* prefName) { return 0.0f; // Return 0 if validation fails } char* validate_pref_string(json_t* value, int stringtype) { + if (!value) { // Check if value is NULL + return NULL; // Early return if value is NULL + } const char* str = json_string_value(value); + if (!str) { // Check if str is NULL + return NULL; // Early return if str is NULL + } size_t len = strlen(str); int maxlen = 0; - char* valuename; + char* valuename = NULL; + if (value && json_is_string(value)) { if (stringtype == 0) { maxlen = 16; @@ -141,6 +148,14 @@ char* validate_pref_string(json_t* value, int stringtype) { map_prefs is < 0 */ +// Function to create a new bot_mapping_t instance +bot_mapping_t* create_new_bot(char* name) { + bot_mapping_t* newBot = (bot_mapping_t*)malloc(sizeof(bot_mapping_t)); + newBot->name = strdup(name); // Duplicate the name + newBot->personality.skin_pref = NULL; // Initialize to NULL + return newBot; +} + qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) { // Don't do anything if the map_prefs are 0 to positive @@ -173,7 +188,7 @@ qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) // Dynamically update the map preferences of the bot // Not all bots will have a preference, so we default to 0 for those -void update_map_pref(json_t* root, char* map_name, temp_bot_personality_t* personality) +void update_map_pref(json_t* root, char* map_name, bot_mapping_t* newBot) { // Get the "map_prefs" object from the root json_t* map_prefs = json_object_get(root, "map_prefs"); @@ -185,19 +200,19 @@ void update_map_pref(json_t* root, char* map_name, temp_bot_personality_t* perso // Fetch the value for the specified map name json_t* value = json_object_get(map_prefs, map_name); if (value && json_is_real(value)) { - // Update the personality struct with the fetched value - personality->map_prefs = (float)json_real_value(value); - gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, personality->map_prefs); + // Update the newBot struct with the fetched value + newBot->personality.map_prefs = (float)json_real_value(value); + gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, newBot->personality.map_prefs); } else { // If the map name is not found or the value is not a real number, default to 0.0f - personality->map_prefs = 0.0f; - gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, personality->map_prefs); + newBot->personality.map_prefs = 0.0f; + gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, newBot->personality.map_prefs); } } // Function to load a bot personality from a JSON file using libjansson -bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) { - +bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) +{ // Open the file FILE* file = fopen(filename, "r"); if (!file) { @@ -228,93 +243,167 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) { // return NULL; // } - bot_mapping_t* temp_bot = (bot_mapping_t*)malloc(sizeof(bot_mapping_t)); - if (!temp_bot) { - json_decref(root); - return NULL; - } + const char* botName; + json_t* botDetails; - // Extract and assign values from the JSON object - // Define an array of weapon names corresponding to their preferences - const char* weaponNames[] = { - MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, - SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME - }; - - json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); - for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { - json_t* wpref_value = json_array_get(weapon_prefs, i); - // Ensure the index is within the bounds of weaponNames array - const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; - float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name - - // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) - if (value != 0.0f) { - temp_bot->personality.weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered - } - } + json_object_foreach(bots, botName, botDetails) { + bot_mapping_t* newBot = create_new_bot((char*)botName); - // Assuming you have an array of preference names corresponding to each index - // Define an array of item names corresponding to their preferences - const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; + if (!newBot) { + json_decref(root); + return NULL; + } - json_t* item_prefs = json_object_get(root, "item_prefs"); - for (size_t i = 0; i < json_array_size(item_prefs); i++) { - json_t* ipref_value = json_array_get(item_prefs, i); - // Ensure the index is within the bounds of itemNames array - const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; - float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings + // Extract weapon preferences + json_t* weapon_prefs = json_object_get(botDetails, "weapon_prefs"); + for (size_t i = 0; i < WEAPON_COUNT; i++) { + json_t* wpref_value = json_array_get(weapon_prefs, i); + newBot->personality.weapon_prefs[i] = validate_pref_numeric(wpref_value, "weapon_prefs"); + } - // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases - if (value != 0.0f) { - temp_bot->personality.item_prefs[i] = value; + // Extract item preferences + json_t* item_prefs = json_object_get(botDetails, "item_prefs"); + for (size_t i = 0; i < ITEM_COUNT; i++) { + json_t* ipref_value = json_array_get(item_prefs, i); + newBot->personality.item_prefs[i] = validate_pref_numeric(ipref_value, "item_prefs"); } - } - // Extract single float values, if they exist, else default to 0 - json_t *checkval; + // Extract map preferences + json_t* map_prefs = json_object_get(botDetails, "map_prefs"); + newBot->personality.map_prefs = validate_pref_numeric(map_prefs, "map_prefs"); + update_map_pref(botDetails, level.mapname, newBot); + + // Extract combat demeanor + json_t* combat_demeanor = json_object_get(botDetails, "combat_demeanor"); + newBot->personality.combat_demeanor = validate_pref_numeric(combat_demeanor, "combat_demeanor"); + + // Extract chat demeanor + json_t* chat_demeanor = json_object_get(botDetails, "chat_demeanor"); + newBot->personality.chat_demeanor = validate_pref_numeric(chat_demeanor, "chat_demeanor"); + + // Extract skin preference + json_t* skin_pref = json_object_get(botDetails, "skin"); + if (skin_pref) { + char* validatedSkinPref = validate_pref_string(skin_pref, MAX_QPATH); // Assuming STRING_TYPE_SKIN is a defined constant for skin strings + if (validatedSkinPref) { + newBot->personality.skin_pref = validatedSkinPref; + } else { // Default skin + newBot->personality.skin_pref = "male/grunt"; + } + } - char* current_map_name = level.mapname; - //personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); - update_map_pref(root, current_map_name, temp_bot); + // Add the newBot to your bot collection or array here + gi.dprintf("Loaded bot %s\n", newBot->name); + gi.dprintf("Weapon Preferences:\n"); + for (size_t i = 0; i < WEAPON_COUNT; i++) { + gi.dprintf(" %.2f\n", newBot->personality.weapon_prefs[i]); + } + gi.dprintf("Item Preferences:\n"); + for (size_t i = 0; i < ITEM_COUNT; i++) { + gi.dprintf(" %.2f\n", newBot->personality.item_prefs[i]); + } + gi.dprintf("Map Preferences: %.2f\n", newBot->personality.map_prefs); + gi.dprintf("Combat Demeanor: %.2f\n", newBot->personality.combat_demeanor); + gi.dprintf("Chat Demeanor: %.2f\n", newBot->personality.chat_demeanor); + gi.dprintf("Skin Preference: %s\n", newBot->personality.skin_pref); + } + + + + + + + + + + + + + + + + // Extract and assign values from the JSON object + // Define an array of weapon names corresponding to their preferences + // const char* weaponNames[] = { + // MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, + // SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME + // }; + + // json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); + // for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { + // json_t* wpref_value = json_array_get(weapon_prefs, i); + // // Ensure the index is within the bounds of weaponNames array + // const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; + // float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name + + // // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) + // if (value != 0.0f) { + // temp_bot->personality.weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered + // } + // } - temp_bot->personality.combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); + // // Assuming you have an array of preference names corresponding to each index + // // Define an array of item names corresponding to their preferences + // const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; + + // json_t* item_prefs = json_object_get(root, "item_prefs"); + // for (size_t i = 0; i < json_array_size(item_prefs); i++) { + // json_t* ipref_value = json_array_get(item_prefs, i); + // // Ensure the index is within the bounds of itemNames array + // const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; + // float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings + + // // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases + // if (value != 0.0f) { + // temp_bot->personality.item_prefs[i] = value; + // } + // } - temp_bot->personality.chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); - - // Extract integer value, every fresh personality load sets this to 0 - temp_bot->personality.leave_percent = 0; + // // Extract single float values, if they exist, else default to 0 + // json_t *checkval; - // Set the skin, if provided - char* defaultSkin = "male/grunt"; + // char* current_map_name = level.mapname; + // //personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); + // update_map_pref(root, current_map_name, temp_bot); - checkval = json_object_get(root, "skin"); - char* validatedSkin = validate_pref_string(checkval, 1); - if (validatedSkin != NULL) { - // If skin value is valid, use it - temp_bot->personality.skin_pref = validatedSkin; - } else { - // If skin value is invalid, use defaultSkin and print a warning - temp_bot->personality.skin_pref = strdup(defaultSkin); // Use strdup to copy defaultSkin - } + // temp_bot->personality.combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); - // Set the name, if provided - char* defaultName = "AqtionBot"; + // temp_bot->personality.chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); + + // // Extract integer value, every fresh personality load sets this to 0 + // temp_bot->personality.leave_percent = 0; + + // // Set the skin, if provided + // char* defaultSkin = "male/grunt"; + + // checkval = json_object_get(root, "skin"); + // char* validatedSkin = validate_pref_string(checkval, 1); + // if (validatedSkin != NULL) { + // // If skin value is valid, use it + // temp_bot->personality.skin_pref = validatedSkin; + // } else { + // // If skin value is invalid, use defaultSkin and print a warning + // temp_bot->personality.skin_pref = strdup(defaultSkin); // Use strdup to copy defaultSkin + // } - checkval = json_object_get(root, "name"); - char* validatedName = validate_pref_string(checkval, 0); - if (validatedName != NULL) { - // If name value is valid, use it - temp_bot->name = validatedName; - } else { - // If name value is invalid, use defaultName and print a warning - temp_bot->name = strdup(defaultName); // Use strdup to copy defaultName - } + // // Set the name, if provided + // char* defaultName = "AqtionBot"; + + // checkval = json_object_get(root, "bots"); + // char* validatedName = validate_pref_string(checkval, 0); + // if (validatedName != NULL) { + // // If name value is valid, use it + // temp_bot->name = validatedName; + // } else { + // // If name value is invalid, use defaultName and print a warning + // temp_bot->name = strdup(defaultName); // Use strdup to copy defaultName + // } + // Clean up json_decref(root); - return temp_bot; + return 0; } // int main() { @@ -410,7 +499,7 @@ size_t BOTLIB_PersonalityCount(void) { json_t* bots; size_t bot_count = 0; FILE* fIn; - char filename[128]; + char filename[MAX_QPATH]; cvar_t* game_dir = gi.cvar("game", "action", 0); cvar_t* botdir = gi.cvar("botdir", "bots", 0); @@ -436,6 +525,8 @@ size_t BOTLIB_PersonalityCount(void) { return 0; // No file } + BOTLIB_LoadPersonalities(filename); + // Rewind the file pointer to the beginning of the file rewind(fIn); diff --git a/src/action/g_local.h b/src/action/g_local.h index 76a95f0fe..7998b3717 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -780,7 +780,7 @@ typedef struct // Bot personalities loaded #ifndef NO_BOTS - const char* bot_file_path; + char* bot_file_path[MAX_QPATH]; #endif } game_locals_t; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index ee3bcae39..bb977cbb1 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1597,7 +1597,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if(bot_personality->value) BOTLIB_PersonalityCount(); - BOTLIB_LoadPersonalities(game.bot_file_path); #endif //rekkie -- e } From 09521356db1f557dca1765b0ba7a0303daa0a866 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Aug 2024 21:51:27 -0400 Subject: [PATCH 427/974] Added bots.json --- bots.json | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 bots.json diff --git a/bots.json b/bots.json new file mode 100644 index 000000000..dd47c19f2 --- /dev/null +++ b/bots.json @@ -0,0 +1,38 @@ +{ + "bots": { + "bob": { + "skin": "male/babarracuda", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67 + }, + "combat_demeanor": 0.35, + "chat_demeanor": -0.89 + }, + "bill": { + "skin": "male/terror", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67 + }, + "combat_demeanor": -0.45, + "chat_demeanor": 0.41 + }, + "sam": { + "skin": "male/mr_t", + "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], + "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], + "map_prefs": { + "teamjungle": 0.11, + "plaza": 0.67, + "kingslanding": 0.9 + }, + "combat_demeanor": 0.45, + "chat_demeanor": -0.3 + } + } +} From 33f123abc0b458189e4d9dd9af170dc942e6f195 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 3 Aug 2024 23:01:07 -0400 Subject: [PATCH 428/974] Some enhanced error checking --- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_personality.c | 148 ++++++++++--------------- src/action/botlib/botlib_spawn.c | 6 +- src/action/g_local.h | 2 + src/action/g_spawn.c | 2 +- 5 files changed, 68 insertions(+), 92 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index f3b5673d8..9c926b0fd 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -527,6 +527,6 @@ typedef struct { } bot_mapping_t; -size_t BOTLIB_PersonalityCount(void); +void BOTLIB_Personality(void); bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 05f23fe7c..c6d443ba3 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -5,10 +5,10 @@ // Count of bot personalities loaded char** botNames = NULL; // Global array of bot names -size_t loadedBotCount = 0; - bot_mapping_t bot_mappings[MAX_BOTS]; +qboolean pers_debug_mode = false; + // #define WEAPON_COUNT 9 // #define ITEM_COUNT 6 @@ -104,7 +104,6 @@ char* validate_pref_string(json_t* value, int stringtype) { size_t len = strlen(str); int maxlen = 0; char* valuename = NULL; - if (value && json_is_string(value)) { if (stringtype == 0) { maxlen = 16; @@ -117,11 +116,18 @@ char* validate_pref_string(json_t* value, int stringtype) { return strdup(str); // Return a copy of the string if valid } } + gi.dprintf("Value for skin was: %s %s\n", valuename, str); gi.dprintf("Warning: Invalid %s (%s). Using default value.\n", valuename, str); gi.dprintf("Future improvement: run it through the randomizer instead\n"); return NULL; // Return NULL if validation fails } +void DeactivateBotPersonality() +{ + gi.dprintf("Deactivating bot_personality\n"); + gi.cvar_forceset("bot_personality", 0); +} + // qboolean json_load_check(const char* filename) // { // json_error_t error; @@ -202,11 +208,13 @@ void update_map_pref(json_t* root, char* map_name, bot_mapping_t* newBot) if (value && json_is_real(value)) { // Update the newBot struct with the fetched value newBot->personality.map_prefs = (float)json_real_value(value); - gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, newBot->personality.map_prefs); + if(pers_debug_mode) + gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, newBot->personality.map_prefs); } else { // If the map name is not found or the value is not a real number, default to 0.0f newBot->personality.map_prefs = 0.0f; - gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, newBot->personality.map_prefs); + if(pers_debug_mode) + gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, newBot->personality.map_prefs); } } @@ -217,6 +225,7 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) FILE* file = fopen(filename, "r"); if (!file) { perror("Failed to open file"); + DeactivateBotPersonality(); return NULL; } @@ -226,12 +235,14 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) if (!root) { gi.dprintf("bot_personality: Error parsing JSON: %s\n", error.text); + DeactivateBotPersonality(); return NULL; } json_t* bots = json_object_get(root, "bots"); if (!bots) { gi.dprintf("bot_personality: 'bots' object not found\n"); + DeactivateBotPersonality(); json_decref(root); return NULL; } @@ -243,6 +254,17 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) // return NULL; // } + // Get the "bots" object from the root of the JSON structure + bots = json_object_get(root, "bots"); + if (!json_is_object(bots)) { + // Handle error: "bots" is not an object or not found + json_decref(root); + return 0; // Return 0 to indicate failure + } + + // Count the number of keys (bots) in the "bots" object and store it + game.loaded_bot_personalities = json_object_size(bots); + const char* botName; json_t* botDetails; @@ -282,30 +304,40 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) newBot->personality.chat_demeanor = validate_pref_numeric(chat_demeanor, "chat_demeanor"); // Extract skin preference + + // Setting safe default, only override if exists json_t* skin_pref = json_object_get(botDetails, "skin"); if (skin_pref) { char* validatedSkinPref = validate_pref_string(skin_pref, MAX_QPATH); // Assuming STRING_TYPE_SKIN is a defined constant for skin strings - if (validatedSkinPref) { + if (validatedSkinPref != NULL) { newBot->personality.skin_pref = validatedSkinPref; - } else { // Default skin + } else { // Default skin if validation fails + if(pers_debug_mode) + gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); newBot->personality.skin_pref = "male/grunt"; } + } else { // Default skin if 'skin' key is missing + if(pers_debug_mode) + gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); + newBot->personality.skin_pref = "male/grunt"; } // Add the newBot to your bot collection or array here - gi.dprintf("Loaded bot %s\n", newBot->name); - gi.dprintf("Weapon Preferences:\n"); - for (size_t i = 0; i < WEAPON_COUNT; i++) { - gi.dprintf(" %.2f\n", newBot->personality.weapon_prefs[i]); - } - gi.dprintf("Item Preferences:\n"); - for (size_t i = 0; i < ITEM_COUNT; i++) { - gi.dprintf(" %.2f\n", newBot->personality.item_prefs[i]); + if(pers_debug_mode){ + gi.dprintf("Loaded bot %s\n", newBot->name); + gi.dprintf("Weapon Preferences:\n"); + for (size_t i = 0; i < WEAPON_COUNT; i++) { + gi.dprintf(" %.2f\n", newBot->personality.weapon_prefs[i]); + } + gi.dprintf("Item Preferences:\n"); + for (size_t i = 0; i < ITEM_COUNT; i++) { + gi.dprintf(" %.2f\n", newBot->personality.item_prefs[i]); + } + gi.dprintf("Map Preferences: %.2f\n", newBot->personality.map_prefs); + gi.dprintf("Combat Demeanor: %.2f\n", newBot->personality.combat_demeanor); + gi.dprintf("Chat Demeanor: %.2f\n", newBot->personality.chat_demeanor); + gi.dprintf("Skin Preference: %s\n", newBot->personality.skin_pref); } - gi.dprintf("Map Preferences: %.2f\n", newBot->personality.map_prefs); - gi.dprintf("Combat Demeanor: %.2f\n", newBot->personality.combat_demeanor); - gi.dprintf("Chat Demeanor: %.2f\n", newBot->personality.chat_demeanor); - gi.dprintf("Skin Preference: %s\n", newBot->personality.skin_pref); } @@ -492,12 +524,8 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) // loadedBotCount = 0; // } -// Function to count bots in the JSON file -size_t BOTLIB_PersonalityCount(void) { - json_t* root; - json_error_t error; - json_t* bots; - size_t bot_count = 0; +// Main function that will load other personality methods +void BOTLIB_Personality(void) { FILE* fIn; char filename[MAX_QPATH]; cvar_t* game_dir = gi.cvar("game", "action", 0); @@ -520,70 +548,12 @@ size_t BOTLIB_PersonalityCount(void) { // Save file path for later references strncpy(game.bot_file_path, filename, MAX_QPATH); - if ((fIn = fopen(filename, "rb")) == NULL) // See if .json file exists - { - return 0; // No file - } - - BOTLIB_LoadPersonalities(filename); - - // Rewind the file pointer to the beginning of the file - rewind(fIn); - - // Load JSON from file - root = json_loadf(fIn, 0, &error); - - if (!root) { - // Handle JSON parsing error - fprintf(stderr, "JSON error on line %d: %s\n", error.line, error.text); - json_decref(root); - return 0; // Return 0 to indicate failure + // Validate file exists + if ((fIn = fopen(filename, "rb")) == NULL) {// See if .json file exists + DeactivateBotPersonality(); + return; // No file } - // Get the "bots" object from the root of the JSON structure - bots = json_object_get(root, "bots"); - if (!json_is_object(bots)) { - // Handle error: "bots" is not an object or not found - json_decref(root); - return 0; // Return 0 to indicate failure - } - - // Count the number of keys (bots) in the "bots" object - bot_count = json_object_size(bots); - - // Clean up - json_decref(root); - fclose(fIn); - - return bot_count; // Return the count of bots + // Now that we have the file, let's load the bots + BOTLIB_LoadPersonalities(filename); } - -// qboolean BOTLIB_LoadPersonality(char* botname) -// { -// int maxclients = game.maxclients; -// FILE* fIn; -// char tempfilename[128]; -// int fileSize = 0; -// int f, n, l, p; // File, nodes, links, paths -// qboolean randomized; - -// // Const-ify the filename -// const char* filename = strdup(game.bot_file_path); - -// if ((fIn = fopen(filename, "rb")) == NULL) // See if .json file exists -// { -// return false; // No file -// } - -// fclose(fIn); - -// Com_Printf("%s Loaded %s from disk\n", __func__, filename); - -// // If no botname provided, let's re-run with a random bot -// if(botname == NULL) -// BOTLIB_LoadPersonality(getRandomBotName()); - -// if(load_bot_personality()){ - -// } -// } \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 90d8b32a3..1377fc869 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1400,7 +1400,11 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else if (team == TEAM3) bot_connections.total_team3++; - if(bot_personality->value) {// Load personality data + if(bot_personality->value && (game.loaded_bot_personalities == game.used_bot_personalities)){ + gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n"); + } + // Load bots up, hard set and random alike + if(bot_personality->value && (game.loaded_bot_personalities < game.used_bot_personalities)) {// Load personality data BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, true); // includes ClientConnect } else { // Use random data BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, false); diff --git a/src/action/g_local.h b/src/action/g_local.h index 7998b3717..1e0704f07 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -781,6 +781,8 @@ typedef struct // Bot personalities loaded #ifndef NO_BOTS char* bot_file_path[MAX_QPATH]; + int loaded_bot_personalities; + int used_bot_personalities; #endif } game_locals_t; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index bb977cbb1..9885727fd 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1596,7 +1596,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- Fake Bot Client -- e if(bot_personality->value) - BOTLIB_PersonalityCount(); + BOTLIB_Personality(); #endif //rekkie -- e } From 62b49226042fb646ce21ff9d6286ed4457e71953 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 4 Aug 2024 17:33:43 +0300 Subject: [PATCH 429/974] Shorten code. --- src/server/commands.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/server/commands.c b/src/server/commands.c index 58a597bef..663101948 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -250,9 +250,9 @@ static void abort_func(void *arg) static void SV_Map(bool restart) { - mapcmd_t cmd; - - memset(&cmd, 0, sizeof(cmd)); + mapcmd_t cmd = { + .endofunit = restart, // wipe savegames + }; // save the mapcmd if (Cmd_ArgvBuffer(1, cmd.buffer, sizeof(cmd.buffer)) >= sizeof(cmd.buffer)) { @@ -274,9 +274,6 @@ static void SV_Map(bool restart) // save pending CM to be freed later if ERR_DROP is thrown Com_AbortFunc(abort_func, &cmd.cm); - // wipe savegames - cmd.endofunit |= restart; - SV_AutoSaveBegin(&cmd); // any error will drop from this point From 092f73f9d230de58ff0008a876c001e5004a90df Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 4 Aug 2024 17:37:32 +0300 Subject: [PATCH 430/974] Update README.md and client docs. --- README.md | 30 +++++++++++++++++++++--------- doc/client.asciidoc | 13 ++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b8dfcf74e..1f0d56775 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,32 @@ Q2PRO ===== -Q2PRO is an enhanced, multiplayer oriented Quake 2 client and server. +Q2PRO is an enhanced Quake 2 client and server for Windows and Linux. Supported +features include: -Features include: - -* rewritten OpenGL renderer optimized for stable FPS -* enhanced client console with persistent history -* ZIP packfiles (.pkz), JPEG and PNG textures, MD3 models -* fast HTTP downloads +* unified OpenGL renderer with support for GL 1.1–1.4, 3.0+, GL ES 3.0+ +* enhanced console command completion +* persistent and searchable console command history +* rendering / physics / packet rate separation +* ZIP packfiles (.pkz) +* JPEG/PNG textures +* MD3 and MD5 (re-release) models +* Ogg Vorbis music and Ogg Theora cinematics +* fast and secure HTTP downloads * multichannel sound using OpenAL -* recording from demos, forward and backward seeking -* server side multiview demos and GTV capabilities +* stereo WAV files support +* forward and backward seeking in demos +* recording from demos +* server side multiview demos +* live game broadcasting capabilities +* network protocol extensions for larger maps +* won't crash if game data is corrupted For building Q2PRO, consult the INSTALL.md file. +Q2PRO doesn't have releases. It is always recommended to use the git master +version. + Nightly Windows builds are available at https://skuller.net/q2pro/ For information on using and configuring Q2PRO, refer to client and server diff --git a/doc/client.asciidoc b/doc/client.asciidoc index ff6b68ead..f98818a86 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -1601,13 +1601,12 @@ it used to be you know where to look. The following list may be incomplete. separate modulation factors for world lightmaps and entities please use ‘gl_modulate_world’ and ‘gl_modulate_entities’ variables. -- Default value of R1GL-specific ‘gl_dlight_falloff’ variable has been changed - from 0 to 1. +- R1GL-specific ‘gl_dlight_falloff’ variable is supported, but its default has + been changed from 0 to 1. - ‘gl_particle_*’ series of variables are gone, as well as - ‘gl_ext_pointparameters’ and R1GL-specific ‘gl_ext_point_sprite’. For - controlling size of particles, which are always drawn as textured triangles, - Q2PRO supports its own ‘gl_partscale’ variable. + ‘gl_ext_pointparameters’. For controlling size of particles, which are always + drawn as textured triangles, Q2PRO supports its own ‘gl_partscale’ variable. - ‘ip’ variable has been renamed to ‘net_ip’. @@ -1634,5 +1633,5 @@ it used to be you know where to look. The following list may be incomplete. - Single player savegame format has been rewritten from scratch for better robustness and portability. Only the ‘baseq2’ game library included in Q2PRO distribution has been converted to use the new improved savegame format. Q2PRO - will refuse to load and save games in old format for security reasons. - + will refuse to load and save games in old format for security reasons, unless + ‘sys_allow_unsafe_savegames’ is set to 1 from the command line (not recommended). From 850791b6da69fa12032d3c2c6b659216909e3e5f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 4 Aug 2024 17:58:34 +0300 Subject: [PATCH 431/974] Update libcurl to 8.9.1. --- subprojects/libcurl.wrap | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index 4de7bbc10..df7c7745c 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,11 @@ [wrap-file] -directory = curl-8.9.0 -source_url = https://curl.se/download/curl-8.9.0.tar.xz -source_filename = curl-8.9.0.tar.xz -source_hash = ff09b2791ca56d25fd5c3f3a4927dce7c8a9dc4182200c487ca889fba1fdd412 -patch_url = https://skuller.net/meson/libcurl_8.9.0-1_patch.zip -patch_filename = libcurl_8.9.0-1_patch.zip -patch_hash = 584a4887b7940f806a48df6d5ef8af4518da374b0e6327298fb53d180002e4c7 +directory = curl-8.9.1 +source_url = https://curl.se/download/curl-8.9.1.tar.xz +source_filename = curl-8.9.1.tar.xz +source_hash = f292f6cc051d5bbabf725ef85d432dfeacc8711dd717ea97612ae590643801e5 +patch_url = https://skuller.net/meson/libcurl_8.9.1-1_patch.zip +patch_filename = libcurl_8.9.1-1_patch.zip +patch_hash = b7f4ef775d912da335387b222dc1caf421fb434cc36aeee84a032fb28cac5c77 [provide] libcurl = libcurl_dep From b8271082096eaed3b657732b75ae0bddb51636d3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 5 Aug 2024 00:02:13 +0300 Subject: [PATCH 432/974] Limit initial demo parsing loop iteration count. A huge file full of zeros could hang the client. --- src/client/demo.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/demo.c b/src/client/demo.c index d19189b54..bfbd1aac2 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -762,7 +762,7 @@ static void CL_PlayDemo_f(void) CL_ParseServerMessage(); // read and parse messages util `precache' command - while (cls.state == ca_connected) { + for (int i = 0; cls.state == ca_connected && i < 1000; i++) { Cbuf_Execute(&cl_cmdbuf); parse_next_message(0); } From 3635745780ed8af631fa09c9a765e4f33c4bdea9 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 4 Aug 2024 19:29:35 -0400 Subject: [PATCH 433/974] Getting closer, need to avoid duplicates now --- meson.build | 2 +- src/action/botlib/botlib.h | 28 ++- src/action/botlib/botlib_ai.c | 1 - src/action/botlib/botlib_communication.c | 22 +- src/action/botlib/botlib_personality.c | 308 +++++++++++++++++++---- src/action/botlib/botlib_spawn.c | 29 ++- src/action/g_local.h | 4 +- 7 files changed, 316 insertions(+), 78 deletions(-) diff --git a/meson.build b/meson.build index f93286aa2..ddc5360ef 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=release', + 'buildtype=debug', ], ) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 9c926b0fd..1285ba0f4 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -224,7 +224,8 @@ typedef enum CHAT_WELCOME, CHAT_KILLED, CHAT_INSULTS, - CHAT_GOODBYE + CHAT_GOODBYE, + CHAT_RAGE, } bot_chat_types_t; void UpdateBotChat(void); @@ -458,6 +459,7 @@ void BOTLIB_RemoveBot(char* name); // Remove bot by name or 'ALL' to remove all void BOTLIB_RemoveTeamplayBot(int team); // Remove bot from team void BOTLIB_ChangeBotTeam(int from_team, int to_team); // Change a bot [from team] ==> [to team] void BOTLIB_CheckBotRules(void); // Adding/Removing/Auto-balance bots +void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin); // =========================================================================== // botlib_spawnpoints.c @@ -512,21 +514,25 @@ int BOTLIB_GetEquipment(edict_t* self); // Most float values here are between -1 and 1 typedef struct temp_bot_personality_s { - float weapon_prefs[10]; //-1 = Will never choose, 1 = Will always choose - float item_prefs[6]; //-1 = Will never choose, 1 = Will always choose - float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love - float combat_demeanor; //-1 = Timid | 1 = Aggressive - float chat_demeanor; //-1 = Quiet | 1 = Chatty - int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. - char* skin_pref; // Skin preference, if DM mode + float weapon_prefs[10]; //-1 = Will never choose, 1 = Will always choose + float item_prefs[6]; //-1 = Will never choose, 1 = Will always choose + float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love + float combat_demeanor; //-1 = Timid | 1 = Aggressive + float chat_demeanor; //-1 = Quiet | 1 = Chatty + int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. + char* skin_pref; // Skin preference, if DM mode + int pId; // Personality id (used as an index) + qboolean isActive; // Determines if bot is active in game or not (avoid dupes) } temp_bot_personality_t; typedef struct { char* name; temp_bot_personality_t personality; -} bot_mapping_t; - +} temp_bot_mapping_t; +temp_bot_mapping_t bot_mappings[100]; void BOTLIB_Personality(void); -bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); +temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); +void DeactivateBotPersonality(void); +qboolean LoadBotPersonality(edict_t* bot, int team, int force_gender); #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 6ae5a2c43..f5bc9c70f 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -73,7 +73,6 @@ void BOTLIB_Init(edict_t* self) self->bot.last_sniper_zoom_time = 0; self->bot.last_weapon_change_time = 0; - if (teamplay->value) // Reset bot radio at the start of each round { // Radio diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 58bc6b138..0c512d2bb 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -49,7 +49,7 @@ void ProcessChatQueue(int currentFrame) { } // Call this function periodically, e.g., in the main game loop -void UpdateBotChat() { +void UpdateBotChat(void) { ProcessChatQueue(level.framenum); } @@ -110,6 +110,19 @@ char *botchat_victory[DBC_VICTORY] = "too ez" }; +#define DBC_RAGE 8 +char *botchat_rage[DBC_RAGE] = +{ + "f this map", + "i never liked this map anyway gg", + "nope not today", + "I'm terrible at this map anyway", + "yeah gg", + "that's enough for me today", + "ASDKDDKJFJK", + "PERKELEEeeeee" +}; + void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { // Do nothing if bot_chat is disabled @@ -157,10 +170,11 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) return; } } - if (chattype == CHAT_GOODBYE) - randval = randval - 0.3; // Increase the chance of a goodbye message + // Goodbyes and rages happen without delay + if (chattype == CHAT_GOODBYE || chattype == CHAT_RAGE) + randval = randval - 0.3; // Increase the chance of this type of message delayed = false; - if (randval > 0.2) { + if (randval > 0.2) { // 80% do not chat //gi.dprintf("Skipping chat due to random chance (%f)\n", randval); return; // Don't chat too often } diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index c6d443ba3..2a44dfd77 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -5,9 +5,8 @@ // Count of bot personalities loaded char** botNames = NULL; // Global array of bot names -bot_mapping_t bot_mappings[MAX_BOTS]; - -qboolean pers_debug_mode = false; +int personality_count = 0; +qboolean pers_debug_mode = true; // #define WEAPON_COUNT 9 // #define ITEM_COUNT 6 @@ -122,9 +121,9 @@ char* validate_pref_string(json_t* value, int stringtype) { return NULL; // Return NULL if validation fails } -void DeactivateBotPersonality() +void DeactivateBotPersonality(void) { - gi.dprintf("Deactivating bot_personality\n"); + gi.dprintf("INFO: Deactivating bot_personality.\n"); gi.cvar_forceset("bot_personality", 0); } @@ -155,35 +154,47 @@ void DeactivateBotPersonality() */ // Function to create a new bot_mapping_t instance -bot_mapping_t* create_new_bot(char* name) { - bot_mapping_t* newBot = (bot_mapping_t*)malloc(sizeof(bot_mapping_t)); +temp_bot_mapping_t* create_new_bot(char* name) { + temp_bot_mapping_t* newBot = (temp_bot_mapping_t*)malloc(sizeof(temp_bot_mapping_t)); + if (newBot == NULL) { + // Handle memory allocation failure + return NULL; + } newBot->name = strdup(name); // Duplicate the name + if (newBot->name == NULL) { + // Handle strdup failure + free(newBot); + return NULL; + } newBot->personality.skin_pref = NULL; // Initialize to NULL + newBot->personality.pId = personality_count; // Initialize ID for indexing, start at -1 because we're incrementing to 0 + personality_count++; return newBot; } qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) { - // Don't do anything if the map_prefs are 0 to positive + // Don't do anything if the map_prefs are 0 or positive if (self->bot.personality.map_prefs >= 0) return false; // Deaths are worth more than frags when you don't like the map you're on float fragplus = 0.05; - float deathplus = 0.07; + float deathminus = 0.07; // Death is true, Frag is false if (frag_or_death) { gi.dprintf("%s received a death, they're mad!\n", self->client->pers.netname); - self->bot.personality.map_prefs -= deathplus; + self->bot.personality.map_prefs -= deathminus; } else { gi.dprintf("%s received a frag, they're happy!\n", self->client->pers.netname); self->bot.personality.map_prefs += fragplus; } - float rage_quit_threshold = -1.0; // Example threshold value + float rage_quit_threshold = -1.0; // Max threshold value if (self->bot.personality.map_prefs <= rage_quit_threshold) { gi.dprintf("%s is rage quitting due to low map preference!\n", self->client->pers.netname); + BOTLIB_Chat(self, CHAT_RAGE); return true; } @@ -194,7 +205,7 @@ qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) // Dynamically update the map preferences of the bot // Not all bots will have a preference, so we default to 0 for those -void update_map_pref(json_t* root, char* map_name, bot_mapping_t* newBot) +void update_map_pref(json_t* root, char* map_name, temp_bot_mapping_t* newBot) { // Get the "map_prefs" object from the root json_t* map_prefs = json_object_get(root, "map_prefs"); @@ -219,9 +230,8 @@ void update_map_pref(json_t* root, char* map_name, bot_mapping_t* newBot) } // Function to load a bot personality from a JSON file using libjansson -bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) +temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) { - // Open the file FILE* file = fopen(filename, "r"); if (!file) { perror("Failed to open file"); @@ -240,90 +250,73 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) } json_t* bots = json_object_get(root, "bots"); - if (!bots) { - gi.dprintf("bot_personality: 'bots' object not found\n"); + if (!bots || !json_is_object(bots)) { + gi.dprintf("bot_personality: 'bots' object not found or is not an object\n"); DeactivateBotPersonality(); json_decref(root); return NULL; } - // json_t* bot_data = json_object_get(bots, bot_name); - // if (!bot_data) { - // gi.dprintf("bot_personality: Bot '%s' not found\n", bot_name); - // json_decref(root); - // return NULL; - // } - - // Get the "bots" object from the root of the JSON structure - bots = json_object_get(root, "bots"); - if (!json_is_object(bots)) { - // Handle error: "bots" is not an object or not found + game.loaded_bot_personalities = json_object_size(bots); + if (game.loaded_bot_personalities == 0) { + gi.dprintf("bot_personality: No bots found in JSON\n"); + DeactivateBotPersonality(); json_decref(root); - return 0; // Return 0 to indicate failure + return NULL; } - // Count the number of keys (bots) in the "bots" object and store it - game.loaded_bot_personalities = json_object_size(bots); - const char* botName; json_t* botDetails; + int botIndex = 0; json_object_foreach(bots, botName, botDetails) { - bot_mapping_t* newBot = create_new_bot((char*)botName); - + temp_bot_mapping_t* newBot = create_new_bot((char*)botName); if (!newBot) { json_decref(root); return NULL; } - // Extract weapon preferences json_t* weapon_prefs = json_object_get(botDetails, "weapon_prefs"); for (size_t i = 0; i < WEAPON_COUNT; i++) { json_t* wpref_value = json_array_get(weapon_prefs, i); newBot->personality.weapon_prefs[i] = validate_pref_numeric(wpref_value, "weapon_prefs"); } - // Extract item preferences json_t* item_prefs = json_object_get(botDetails, "item_prefs"); for (size_t i = 0; i < ITEM_COUNT; i++) { json_t* ipref_value = json_array_get(item_prefs, i); newBot->personality.item_prefs[i] = validate_pref_numeric(ipref_value, "item_prefs"); } - // Extract map preferences json_t* map_prefs = json_object_get(botDetails, "map_prefs"); newBot->personality.map_prefs = validate_pref_numeric(map_prefs, "map_prefs"); update_map_pref(botDetails, level.mapname, newBot); - // Extract combat demeanor json_t* combat_demeanor = json_object_get(botDetails, "combat_demeanor"); newBot->personality.combat_demeanor = validate_pref_numeric(combat_demeanor, "combat_demeanor"); - // Extract chat demeanor json_t* chat_demeanor = json_object_get(botDetails, "chat_demeanor"); newBot->personality.chat_demeanor = validate_pref_numeric(chat_demeanor, "chat_demeanor"); - // Extract skin preference - - // Setting safe default, only override if exists json_t* skin_pref = json_object_get(botDetails, "skin"); if (skin_pref) { - char* validatedSkinPref = validate_pref_string(skin_pref, MAX_QPATH); // Assuming STRING_TYPE_SKIN is a defined constant for skin strings + char* validatedSkinPref = validate_pref_string(skin_pref, MAX_QPATH); if (validatedSkinPref != NULL) { newBot->personality.skin_pref = validatedSkinPref; - } else { // Default skin if validation fails - if(pers_debug_mode) + } else { + if (pers_debug_mode) gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); newBot->personality.skin_pref = "male/grunt"; } - } else { // Default skin if 'skin' key is missing - if(pers_debug_mode) + } else { + if (pers_debug_mode) gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); newBot->personality.skin_pref = "male/grunt"; } - // Add the newBot to your bot collection or array here - if(pers_debug_mode){ + bot_mappings[botIndex++] = *newBot; + + if (pers_debug_mode) { gi.dprintf("Loaded bot %s\n", newBot->name); gi.dprintf("Weapon Preferences:\n"); for (size_t i = 0; i < WEAPON_COUNT; i++) { @@ -338,10 +331,14 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) gi.dprintf("Chat Demeanor: %.2f\n", newBot->personality.chat_demeanor); gi.dprintf("Skin Preference: %s\n", newBot->personality.skin_pref); } + + free(newBot); // Free the temporary newBot since it's now copied to bot_mappings } - - - + + json_decref(root); + return bot_mappings; +} + @@ -433,10 +430,10 @@ bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) // Clean up - json_decref(root); +// json_decref(root); - return 0; -} +// return 0; +// } // int main() { // // Example usage of load_bot_personality @@ -553,7 +550,208 @@ void BOTLIB_Personality(void) { DeactivateBotPersonality(); return; // No file } - // Now that we have the file, let's load the bots BOTLIB_LoadPersonalities(filename); } + +// void copy_temp_to_real(edict_t* bot, temp_bot_mapping_t *roster, bot_personality_t *personality) { +// // Copying array fields +// memcpy(personality->weapon_prefs, roster->personality.weapon_prefs, sizeof(personality->weapon_prefs)); +// memcpy(personality->item_prefs, roster->personality.item_prefs, sizeof(personality->item_prefs)); + +// // Copying simple fields +// personality->map_prefs = roster->personality.map_prefs; +// personality->combat_demeanor = roster->personality.combat_demeanor; +// personality->chat_demeanor = roster->personality.chat_demeanor; +// personality->leave_percent = roster->personality.leave_percent; + +// // Copying string fields, assuming they should be char* in bot_personality_t +// if (roster->name != NULL) { +// personality->name_pref = malloc(strlen(roster->name) + 1); +// Info_SetValueForKey(userinfo, "name", roster->name); +// } else { +// // Safe default +// personality->name_pref = "AqtionBot"; +// } + +// if (roster->personality.skin_pref != NULL) { +// personality->skin_pref = malloc(strlen(roster->personality.skin_pref) + 1); +// strcpy(personality->skin_pref, roster->personality.skin_pref); +// } else { +// // Safe default +// personality->skin_pref = "male/grunt"; +// } + + +// } + +// Return skin if false, return model if true +char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { + char *skinpath; + char *saveptr = NULL; + + // Ensure skinpath is modifiable by duplicating the input + if(skinpathInput == NULL || skinpathInput[0] == '\0') { + skinpath = strdup("male/grunt"); + } else { + skinpath = strdup(skinpathInput); + } + + if(skinpath == NULL) { + // Handle memory allocation failure + return NULL; + } + + // Use strtok_r for a safer tokenization + char *token = strtok_r(skinpath, "/", &saveptr); + if (token == NULL) { + gi.dprintf("%s skin path provided is invalid\n", skinpath); + free(skinpath); // Clean up + return NULL; + } + + if (!returnSkin) { + // Return model + return token; + } else { + // Get the second token (skin) + token = strtok(NULL, "/"); + if (token == NULL) { + gi.dprintf("No skin part in the path: %s\n", skinpath); + return NULL; + } + // Return skin + return token; + } +} + +qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) +{ + if (game.loaded_bot_personalities <= 0) { + // Handle error: No bot personalities loaded + return false; + } + + srand(time(NULL)); + int randomIndex = rand() % game.loaded_bot_personalities; + int attempts = 0; + qboolean foundInactiveBot = false; + + while (attempts < game.loaded_bot_personalities) { + randomIndex = rand() % game.loaded_bot_personalities; + if (!bot_mappings[randomIndex].personality.isActive) { + foundInactiveBot = true; + break; + } + attempts++; + } + + if (!foundInactiveBot) { + // Unable to find an inactive bot + return false; + } + + // Mark the selected bot as active + bot_mappings[randomIndex].personality.isActive = true; + + temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; + + gi.dprintf("bot mappings name %s\n", bot_mappings[randomIndex].name); + gi.dprintf("bot name: %s\n", selectedBot->name); + + // Copying array fields + memcpy(self->bot.personality.weapon_prefs, selectedBot->personality.weapon_prefs, sizeof(self->bot.personality.weapon_prefs)); + memcpy(self->bot.personality.item_prefs, selectedBot->personality.item_prefs, sizeof(self->bot.personality.item_prefs)); + + // Copying simple fields + self->bot.personality.map_prefs = selectedBot->personality.map_prefs; + self->bot.personality.combat_demeanor = selectedBot->personality.combat_demeanor; + self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; + self->bot.personality.leave_percent = selectedBot->personality.leave_percent; + + gi.dprintf("Trying to random bot %i add bot %s with skin %s\n", randomIndex, selectedBot->name, selectedBot->personality.skin_pref); + + int gender = INVALID; + char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) + char skin[MAX_INFO_STRING]; + char userinfo[MAX_INFO_STRING]; + memset(userinfo, 0, sizeof(userinfo)); // Init userinfo + + gi.cvar_forceset(stat_logs->name, "0"); // Turning off stat collection since bots are enabled + + // Set bot name + Info_SetValueForKey(userinfo, "name", selectedBot->name); + + + char* femaleSkinDirs[] = { "actionrally", "female", "sydney" }; + char* maleSkinsDirs[] = { "actionmale", "aqmarine", "male", "messiah", "sas", "terror" }; + int femaleSkinDirsSize = sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); + int maleSkinsDirsSize = sizeof(maleSkinsDirs) / sizeof(maleSkinsDirs[0]); + + // Teamplay and 3TEAMS + if (teamplay->value && team) // TEAM1, TEAM2, or TEAM3 + { + // Figure out the gender based on the team skins + for (int i = 0; i < sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, femaleSkinDirs[i]) != NULL) + { + gender = GENDER_FEMALE; + break; + } + } + if (gender == INVALID) + { + for (int i = 0; i < sizeof(maleSkinsDirs) / sizeof(maleSkinsDirs[0]); ++i) + { + if (Q_strcasestr(teams[team].skin, maleSkinsDirs[i]) != NULL) + { + gender = GENDER_MALE; + break; + } + } + } + if (gender == INVALID) // Couldn't find skin gender (perhaps server is using custom skins) + { + if (rand() % 2 == 0) // So just randomize the skin gender + gender = GENDER_MALE; + else + gender = GENDER_FEMALE; + } + } + else // Deathmatch + { + // Set the skin + char* selectedSkin = selectedBot->personality.skin_pref; + // Assuming selectedBot is correctly initialized and contains the desired data + if (selectedSkin != NULL || selectedSkin == "") { + // Directly use the skin_pref from selectedBot + Info_SetValueForKey(userinfo, "skin", selectedSkin); + } else { + // Safe default + Info_SetValueForKey(userinfo, "skin", "male/grunt"); + } + + // Set the gender + char* modelName = _splitSkinChar(selectedSkin, false); + for (int i = 0; i < maleSkinsDirsSize; i++) { + if (strcmp(maleSkinsDirs[i], modelName) == 0) { + Info_SetValueForKey(userinfo, "gender", "male"); + } + } + for (int i = 0; i < femaleSkinDirsSize; i++) { + if (strcmp(femaleSkinDirs[i], modelName) == 0) { + Info_SetValueForKey(userinfo, "gender", "female"); + } + } + } + + //Set userinfo: hand, spec + Info_SetValueForKey(userinfo, "hand", "2"); // bot is center handed for now! + Info_SetValueForKey(userinfo, "spectator", "0"); // NOT a spectator + + ClientConnect(self, userinfo); + + game.used_bot_personalities++; + return true; +} \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 1377fc869..39a41d2f0 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1254,7 +1254,7 @@ int BOTLIB_RandomSkin(edict_t* bot, char* skin, int force_gender) } // Set the bot's userinfo (name, skin, hand, etc) -void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin, qboolean personality) +void BOTLIB_SetUserinfo(edict_t* bot, const int team, int force_gender, char* force_name, char* force_skin) { int gender = INVALID; char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) @@ -1400,14 +1400,20 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else if (team == TEAM3) bot_connections.total_team3++; - if(bot_personality->value && (game.loaded_bot_personalities == game.used_bot_personalities)){ + if(bot_personality->value && (game.loaded_bot_personalities == game.used_bot_personalities)) { gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n"); + DeactivateBotPersonality(); } + + gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, game.loaded_bot_personalities, game.used_bot_personalities); + // Load bots up, hard set and random alike - if(bot_personality->value && (game.loaded_bot_personalities < game.used_bot_personalities)) {// Load personality data - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, true); // includes ClientConnect + if(bot_personality->value && (game.loaded_bot_personalities > game.used_bot_personalities)) {// Load personality data + if (!LoadBotPersonality(bot, team, force_gender)) + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } else { // Use random data - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin, false); + gi.dprintf("%s: trying to load random bots\n", __func__); + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } ClientBeginDeathmatch(bot); @@ -1467,6 +1473,19 @@ void BOTLIB_RemoveBot(char* name) ClientDisconnect(bot); game.bot_count--; + if (bot_personality->value) { + // If this is a bot with personality, free it up + int bot_pId = bot->bot.personality.pId; + gi.dprintf("Bot pid: %i\n", bot_pId); + for (int i = 0; i < MAX_BOTS; i++) { + if (bot_mappings[i].personality.pId == bot_pId) { + bot_mappings[i].personality.isActive = false; + break; // Exit the loop once the bot is found and deactivated + } + } + game.used_bot_personalities--; + } + if (!remove_all) break; } diff --git a/src/action/g_local.h b/src/action/g_local.h index 1e0704f07..27722683f 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2201,8 +2201,10 @@ typedef struct bot_personality_s float chat_demeanor; //-1 = Quiet | 1 = Chatty int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. - char name_pref; // Name preference char skin_pref; // Skin preference, if DM mode + int pId; // Personality id (used as an index) + qboolean isActive; // Determines if bot is active in game or not (avoid dupes) + } bot_personality_t; typedef struct bot_s From 7fc5ed29fed458beee3aec643ac5d828a540071b Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 4 Aug 2024 21:28:29 -0400 Subject: [PATCH 434/974] Math isnt jiving here --- src/action/acesrc/acebot_spawn.c | 3 ++ src/action/botlib/botlib.h | 3 ++ src/action/botlib/botlib_personality.c | 65 +++++++++++--------------- src/action/botlib/botlib_spawn.c | 39 +++++++--------- 4 files changed, 50 insertions(+), 60 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 715bb9a72..f6a58c61e 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -708,6 +708,9 @@ void ACESP_RemoveBot(char *name) game.bot_count--; // ACEIT_PlayerRemoved (bot); // gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + + if (bot_personality->value) + BOTLIB_FreeBotPersonality(bot); if( ! remove_all ) break; } diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 1285ba0f4..4f3f7ea52 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -535,4 +535,7 @@ void BOTLIB_Personality(void); temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); void DeactivateBotPersonality(void); qboolean LoadBotPersonality(edict_t* bot, int team, int force_gender); +void BOTLIB_FreeBotPersonality(edict_t* bot); + +extern int loaded_bot_personalities; #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 2a44dfd77..fa4637158 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -5,8 +5,8 @@ // Count of bot personalities loaded char** botNames = NULL; // Global array of bot names -int personality_count = 0; qboolean pers_debug_mode = true; +int loaded_bot_personalities = 0; // #define WEAPON_COUNT 9 // #define ITEM_COUNT 6 @@ -127,32 +127,6 @@ void DeactivateBotPersonality(void) gi.cvar_forceset("bot_personality", 0); } -// qboolean json_load_check(const char* filename) -// { -// json_error_t error; -// json_t* root = json_loadf(filename, 0, &error); -// fclose(filename); - -// if (!root) { -// gi.dprintf("bot_personality: Error parsing JSON: %s\n", error.text); -// return NULL; -// } - -// json_t* bots = json_object_get(root, "bots"); -// if (!bots) { -// gi.dprintf("bot_personality: 'bots' object not found\n"); -// json_decref(root); -// return NULL; -// } -// } - - -// Evaluate leave percentage -/* - We will call this on every bot death and kill if - map_prefs is < 0 -*/ - // Function to create a new bot_mapping_t instance temp_bot_mapping_t* create_new_bot(char* name) { temp_bot_mapping_t* newBot = (temp_bot_mapping_t*)malloc(sizeof(temp_bot_mapping_t)); @@ -162,13 +136,13 @@ temp_bot_mapping_t* create_new_bot(char* name) { } newBot->name = strdup(name); // Duplicate the name if (newBot->name == NULL) { - // Handle strdup failure + gi.dprintf("%s: error, missing bot name in file\n", __func__); free(newBot); return NULL; } newBot->personality.skin_pref = NULL; // Initialize to NULL - newBot->personality.pId = personality_count; // Initialize ID for indexing, start at -1 because we're incrementing to 0 - personality_count++; + newBot->personality.pId = loaded_bot_personalities; // Initialize ID for indexing + //loaded_bot_personalities++; return newBot; } @@ -257,8 +231,8 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) return NULL; } - game.loaded_bot_personalities = json_object_size(bots); - if (game.loaded_bot_personalities == 0) { + loaded_bot_personalities = json_object_size(bots); + if (loaded_bot_personalities == 0) { gi.dprintf("bot_personality: No bots found in JSON\n"); DeactivateBotPersonality(); json_decref(root); @@ -627,18 +601,18 @@ char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) { - if (game.loaded_bot_personalities <= 0) { + if (loaded_bot_personalities <= 0) { // Handle error: No bot personalities loaded return false; } srand(time(NULL)); - int randomIndex = rand() % game.loaded_bot_personalities; + int randomIndex = rand() % loaded_bot_personalities; int attempts = 0; qboolean foundInactiveBot = false; - while (attempts < game.loaded_bot_personalities) { - randomIndex = rand() % game.loaded_bot_personalities; + while (attempts < loaded_bot_personalities) { + randomIndex = rand() % loaded_bot_personalities; if (!bot_mappings[randomIndex].personality.isActive) { foundInactiveBot = true; break; @@ -656,8 +630,7 @@ qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; - gi.dprintf("bot mappings name %s\n", bot_mappings[randomIndex].name); - gi.dprintf("bot name: %s\n", selectedBot->name); + gi.dprintf("Selected Bot %i - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", randomIndex, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); // Copying array fields memcpy(self->bot.personality.weapon_prefs, selectedBot->personality.weapon_prefs, sizeof(self->bot.personality.weapon_prefs)); @@ -669,6 +642,8 @@ qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; self->bot.personality.leave_percent = selectedBot->personality.leave_percent; + gi.dprintf("Selected Bot %i - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", randomIndex, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); + gi.dprintf("Trying to random bot %i add bot %s with skin %s\n", randomIndex, selectedBot->name, selectedBot->personality.skin_pref); int gender = INVALID; @@ -682,7 +657,6 @@ qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) // Set bot name Info_SetValueForKey(userinfo, "name", selectedBot->name); - char* femaleSkinDirs[] = { "actionrally", "female", "sydney" }; char* maleSkinsDirs[] = { "actionmale", "aqmarine", "male", "messiah", "sas", "terror" }; int femaleSkinDirsSize = sizeof(femaleSkinDirs) / sizeof(femaleSkinDirs[0]); @@ -754,4 +728,17 @@ qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) game.used_bot_personalities++; return true; +} + +void BOTLIB_FreeBotPersonality(edict_t* bot) +{ + // If this is a bot with personality, free it up + int bot_pId = bot->bot.personality.pId; + for (int i = 0; i < MAX_BOTS; i++) { + if (bot_mappings[i].personality.pId == bot_pId) { + bot_mappings[i].personality.isActive = false; + break; // Exit the loop once the bot is found and deactivated + } + } + game.used_bot_personalities--; } \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 39a41d2f0..de1ccbb51 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1400,18 +1400,24 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else if (team == TEAM3) bot_connections.total_team3++; - if(bot_personality->value && (game.loaded_bot_personalities == game.used_bot_personalities)) { + if(bot_personality->value && (loaded_bot_personalities == game.used_bot_personalities)) { gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n"); DeactivateBotPersonality(); } - - gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, game.loaded_bot_personalities, game.used_bot_personalities); + gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); - // Load bots up, hard set and random alike - if(bot_personality->value && (game.loaded_bot_personalities > game.used_bot_personalities)) {// Load personality data - if (!LoadBotPersonality(bot, team, force_gender)) - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); - } else { // Use random data + if (bot_personality->value) { + if (loaded_bot_personalities > game.used_bot_personalities) { + if (!LoadBotPersonality(bot, team, force_gender)) { + gi.dprintf("Failed to load bot personality, using default userinfo.\n"); + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else { + gi.dprintf("Ran out of bot personalities, loading random bots now.\n"); + // Potentially deactivate a bot personality here, but need an ID or criteria + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else { // Use random data gi.dprintf("%s: trying to load random bots\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } @@ -1473,18 +1479,8 @@ void BOTLIB_RemoveBot(char* name) ClientDisconnect(bot); game.bot_count--; - if (bot_personality->value) { - // If this is a bot with personality, free it up - int bot_pId = bot->bot.personality.pId; - gi.dprintf("Bot pid: %i\n", bot_pId); - for (int i = 0; i < MAX_BOTS; i++) { - if (bot_mappings[i].personality.pId == bot_pId) { - bot_mappings[i].personality.isActive = false; - break; // Exit the loop once the bot is found and deactivated - } - } - game.used_bot_personalities--; - } + if (bot_personality->value) + BOTLIB_FreeBotPersonality(bot); if (!remove_all) break; @@ -1543,7 +1539,8 @@ void BOTLIB_RemoveBot(char* name) freed = true; ClientDisconnect(bot); game.bot_count--; - + if (bot_personality->value) + BOTLIB_FreeBotPersonality(bot); //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); break; } From eeb639bd6c9e25d64ed269887ca69c4160e0adfe Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 5 Aug 2024 16:38:46 +0300 Subject: [PATCH 435/974] Add and use HTTP_FreeFile() macro. --- inc/client/client.h | 1 + src/client/http.c | 2 +- src/client/ui/servers.c | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index b267f13eb..d39307b80 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -81,6 +81,7 @@ void CL_SetSky(void); #if USE_CURL int HTTP_FetchFile(const char *url, void **data); +#define HTTP_FreeFile(data) free(data) #endif bool CL_ForwardToServer(void); diff --git a/src/client/http.c b/src/client/http.c index eba4b681b..551d8049c 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -373,7 +373,7 @@ int HTTP_FetchFile(const char *url, void **data) Com_EPrintf("[HTTP] Failed to fetch '%s': %s\n", url, ret == CURLE_HTTP_RETURNED_ERROR ? http_strerror(response) : curl_easy_strerror(ret)); - free(tmp.buffer); + HTTP_FreeFile(tmp.buffer); return -1; } diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index 94c48787a..0be614446 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -602,7 +602,7 @@ static void ParseMasterArgs(netadr_t *broadcast) if (len < 0) continue; (*parse)(data, len, chunk); - free(data); + HTTP_FreeFile(data); #else Com_Printf("Can't fetch '%s', no HTTP support compiled in.\n", s); #endif From cf1920f73e5b508f24c7b110a147ece73de6c8d4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 5 Aug 2024 18:15:00 +0300 Subject: [PATCH 436/974] Add and use Com_EscapeString(). --- inc/common/utils.h | 4 ++++ src/client/http.c | 4 +++- src/common/utils.c | 53 +++++++++++++++++++++++++++++++++------------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/inc/common/utils.h b/inc/common/utils.h index a7f0275aa..f6e84a474 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -72,6 +72,10 @@ color_index_t Com_ParseColor(const char *s); unsigned Com_ParseExtensionString(const char *s, const char *const extnames[]); #endif +extern const char com_hexchars[16]; + +size_t Com_EscapeString(char *dst, const char *src, size_t size); + char *Com_MakePrintable(const char *s); // Bitmap chunks (for sparse bitmaps) diff --git a/src/client/http.c b/src/client/http.c index 551d8049c..1b15c71bc 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -153,7 +153,9 @@ static void escape_path(char *escaped, const char *path) while (*path) { byte c = *path++; if (!Q_isalnum(c) && !strchr("/-_.~", c)) { - sprintf(escaped, "%%%02x", c); + escaped[0] = '%'; + escaped[1] = com_hexchars[c >> 4]; + escaped[2] = com_hexchars[c & 15]; escaped += 3; } else { *escaped++ = c; diff --git a/src/common/utils.c b/src/common/utils.c index 388a71a4c..f419ad74b 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -562,7 +562,7 @@ size_t Com_FormatSizeLong(char *dest, size_t destsize, int64_t bytes) return Q_scnprintf(dest, destsize, "unknown size"); } -static int get_escape_char(int c) +static int escape_char(int c) { switch (c) { case '\a': return 'a'; @@ -578,24 +578,47 @@ static int get_escape_char(int c) return 0; } -char *Com_MakePrintable(const char *s) +const char com_hexchars[16] = "0123456789ABCDEF"; + +size_t Com_EscapeString(char *dst, const char *src, size_t size) { - static char buffer[4096]; - char *o = buffer; - char *end = buffer + sizeof(buffer); + char *p, *end; + + if (!size) + return 0; - while (*s && o < end - 1) { - byte c = *s++; - int e = get_escape_char(c); + p = dst; + end = dst + size; + while (*src) { + byte c = *src++; + int e = escape_char(c); - if (e) - o += Q_scnprintf(o, end - o, "\\%c", e); - else if (!Q_isprint(c)) - o += Q_scnprintf(o, end - o, "\\x%02X", c); - else - *o++ = c; + if (e) { + if (end - p <= 2) + break; + *p++ = '\\'; + *p++ = e; + } else if (Q_isprint(c)) { + if (end - p <= 1) + break; + *p++ = c; + } else { + if (end - p <= 4) + break; + *p++ = '\\'; + *p++ = 'x'; + *p++ = com_hexchars[c >> 4]; + *p++ = com_hexchars[c & 15]; + } } - *o = 0; + *p = 0; + return p - dst; +} + +char *Com_MakePrintable(const char *s) +{ + static char buffer[4096]; + Com_EscapeString(buffer, s, sizeof(buffer)); return buffer; } From f825f054149d7b80df243308ef8d8833e29ffd5e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 5 Aug 2024 12:55:32 -0400 Subject: [PATCH 437/974] Fixes bot indexes --- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib.h | 8 ++-- src/action/botlib/botlib_ai.c | 4 ++ src/action/botlib/botlib_personality.c | 62 +++++++++++++++----------- src/action/botlib/botlib_spawn.c | 8 ++-- src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_spawn.c | 2 +- src/action/p_client.c | 12 ++++- 9 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 1b7760e8f..2912bc3e4 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -543,6 +543,7 @@ extern cvar_t* bot_randskill; extern cvar_t* bot_randname; extern cvar_t* bot_chat; extern cvar_t* bot_personality; +extern cvar_t* bot_ragequit; //extern cvar_t* bot_randteamskin; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 4f3f7ea52..fdc0257dd 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -531,11 +531,13 @@ typedef struct { } temp_bot_mapping_t; temp_bot_mapping_t bot_mappings[100]; -void BOTLIB_Personality(void); +void BOTLIB_PersonalityFile(void); temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); void DeactivateBotPersonality(void); -qboolean LoadBotPersonality(edict_t* bot, int team, int force_gender); +qboolean BOTLIB_SetPersonality(edict_t* bot, int team, int force_gender); +qboolean BOTLIB_LoadBotPersonality(edict_t* self); void BOTLIB_FreeBotPersonality(edict_t* bot); - +qboolean BotRageQuit(edict_t* self, qboolean frag_or_death); extern int loaded_bot_personalities; +extern int bot_personality_index; #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index f5bc9c70f..bd6229e3e 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -26,11 +26,13 @@ void BOTLIB_Init(edict_t* self) // Save previous data float prev_skill = self->bot.skill; + int pId = self->bot.personality.pId; memset(&self->bot, 0, sizeof(bot_t)); // Restore previous data self->bot.skill = prev_skill; + self->bot.personality.pId = pId; // Ping // Set the average ping this bot will see @@ -96,6 +98,8 @@ void BOTLIB_Init(edict_t* self) AssignSkin(self, s, false /* nickChanged */); } + if (bot_personality->value) + BOTLIB_LoadBotPersonality(self); } //rekkie -- Quake3 -- s diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index fa4637158..6be44694e 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -7,6 +7,7 @@ char** botNames = NULL; // Global array of bot names qboolean pers_debug_mode = true; int loaded_bot_personalities = 0; +int bot_personality_index = 0; // #define WEAPON_COUNT 9 // #define ITEM_COUNT 6 @@ -141,11 +142,12 @@ temp_bot_mapping_t* create_new_bot(char* name) { return NULL; } newBot->personality.skin_pref = NULL; // Initialize to NULL - newBot->personality.pId = loaded_bot_personalities; // Initialize ID for indexing - //loaded_bot_personalities++; + newBot->personality.pId = bot_personality_index; // Initialize ID for indexing + bot_personality_index++; return newBot; } +// Death = True, Frag = False qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) { // Don't do anything if the map_prefs are 0 or positive @@ -496,7 +498,7 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) // } // Main function that will load other personality methods -void BOTLIB_Personality(void) { +void BOTLIB_PersonalityFile(void) { FILE* fIn; char filename[MAX_QPATH]; cvar_t* game_dir = gi.cvar("game", "action", 0); @@ -599,20 +601,40 @@ char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { } } -qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) +// At this point we have the bot ready, we just need to copy over the right loaded personality +qboolean BOTLIB_LoadBotPersonality(edict_t* self) { - if (loaded_bot_personalities <= 0) { + temp_bot_mapping_t* selectedBot = &bot_mappings[self->bot.personality.pId]; + + // Copying array fields + memcpy(self->bot.personality.weapon_prefs, selectedBot->personality.weapon_prefs, sizeof(self->bot.personality.weapon_prefs)); + memcpy(self->bot.personality.item_prefs, selectedBot->personality.item_prefs, sizeof(self->bot.personality.item_prefs)); + + // Copying simple fields + self->bot.personality.map_prefs = selectedBot->personality.map_prefs; + self->bot.personality.combat_demeanor = selectedBot->personality.combat_demeanor; + self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; + self->bot.personality.leave_percent = selectedBot->personality.leave_percent; + + if(pers_debug_mode) + gi.dprintf("Selected Bot %s(indexes: %i/%i) - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", selectedBot->name, self->bot.personality.pId, selectedBot->personality.pId, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); +} + +qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) +{ + if (bot_personality_index <= 0) { // Handle error: No bot personalities loaded return false; } srand(time(NULL)); - int randomIndex = rand() % loaded_bot_personalities; + int randomIndex = rand() % bot_personality_index; int attempts = 0; + temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; qboolean foundInactiveBot = false; - while (attempts < loaded_bot_personalities) { - randomIndex = rand() % loaded_bot_personalities; + while (attempts < bot_personality_index) { + randomIndex = rand() % bot_personality_index; if (!bot_mappings[randomIndex].personality.isActive) { foundInactiveBot = true; break; @@ -625,27 +647,13 @@ qboolean LoadBotPersonality(edict_t* self, int team, int force_gender) return false; } - // Mark the selected bot as active + // Mark the selected bot as active in both loaded and live structs bot_mappings[randomIndex].personality.isActive = true; + self->bot.personality.isActive = true; + self->bot.personality.pId = randomIndex; - temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; - - gi.dprintf("Selected Bot %i - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", randomIndex, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); - - // Copying array fields - memcpy(self->bot.personality.weapon_prefs, selectedBot->personality.weapon_prefs, sizeof(self->bot.personality.weapon_prefs)); - memcpy(self->bot.personality.item_prefs, selectedBot->personality.item_prefs, sizeof(self->bot.personality.item_prefs)); - - // Copying simple fields - self->bot.personality.map_prefs = selectedBot->personality.map_prefs; - self->bot.personality.combat_demeanor = selectedBot->personality.combat_demeanor; - self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; - self->bot.personality.leave_percent = selectedBot->personality.leave_percent; - - gi.dprintf("Selected Bot %i - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", randomIndex, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); - - gi.dprintf("Trying to random bot %i add bot %s with skin %s\n", randomIndex, selectedBot->name, selectedBot->personality.skin_pref); - + gi.dprintf("Random index %i\n", randomIndex); + int gender = INVALID; char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) char skin[MAX_INFO_STRING]; diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index de1ccbb51..f0ca1dc70 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1400,15 +1400,15 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else if (team == TEAM3) bot_connections.total_team3++; - if(bot_personality->value && (loaded_bot_personalities == game.used_bot_personalities)) { + if(bot_personality->value && (bot_personality_index == game.used_bot_personalities)) { gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n"); DeactivateBotPersonality(); } - gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); + gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, bot_personality_index, game.used_bot_personalities); if (bot_personality->value) { - if (loaded_bot_personalities > game.used_bot_personalities) { - if (!LoadBotPersonality(bot, team, force_gender)) { + if (bot_personality_index > game.used_bot_personalities) { + if (!BOTLIB_SetPersonality(bot, team, force_gender)) { gi.dprintf("Failed to load bot personality, using default userinfo.\n"); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } diff --git a/src/action/g_main.c b/src/action/g_main.c index ad08b587a..764f7c3b1 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -495,6 +495,7 @@ cvar_t* bot_randskill; // When random bot join a game, they pick a random skill cvar_t* bot_randname; // Allow bots to pick a random name cvar_t* bot_chat; // Enable generic bot chat cvar_t* bot_personality; // Enable bot personality functionality +cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index ed0757823..3021ff0dd 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -695,6 +695,7 @@ void InitGame( void ) bot_randname = gi.cvar("bot_randname", "1", 0); bot_chat = gi.cvar("bot_chat", "0", 0); bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); + bot_ragequit = gi.cvar("bot_ragequit", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 9885727fd..fced5a9e9 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1596,7 +1596,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- Fake Bot Client -- e if(bot_personality->value) - BOTLIB_Personality(); + BOTLIB_PersonalityFile(); #endif //rekkie -- e } diff --git a/src/action/p_client.c b/src/action/p_client.c index ddbd1d31c..bafbe9504 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -797,8 +797,12 @@ void Add_Frag(edict_t * ent, int mod) #ifndef NO_BOTS //darksaint -- Bot Chat -- s // Generates chat message if respawning (killed) - if (ent->is_bot) + if (ent->is_bot) { BOTLIB_Chat(ent, CHAT_INSULTS); + if(bot_personality->value && bot_ragequit->value) { + BotRageQuit(ent, false); + } + } #endif //darksaint -- Bot Chat -- e @@ -1581,8 +1585,12 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) #ifndef NO_BOTS //darksaint -- Bot Chat -- s // Generates chat message if respawning (killed) - if (self->is_bot) + if (self->is_bot) { BOTLIB_Chat(self, CHAT_KILLED); + if(bot_personality->value && bot_ragequit->value) { + BotRageQuit(self, true); + } + } #endif //darksaint -- Bot Chat -- e From 3032019e72b6b5821db654a0383d356899adfc84 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 5 Aug 2024 13:23:06 -0400 Subject: [PATCH 438/974] Fixes bot indexes --- src/action/botlib/botlib_personality.c | 9 +++++---- src/action/botlib/botlib_spawn.c | 4 ++-- src/action/g_local.h | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 6be44694e..ccfa2e22a 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -7,7 +7,7 @@ char** botNames = NULL; // Global array of bot names qboolean pers_debug_mode = true; int loaded_bot_personalities = 0; -int bot_personality_index = 0; +int bot_personality_index = 0; // We're incrementing as we create // #define WEAPON_COUNT 9 // #define ITEM_COUNT 6 @@ -142,8 +142,7 @@ temp_bot_mapping_t* create_new_bot(char* name) { return NULL; } newBot->personality.skin_pref = NULL; // Initialize to NULL - newBot->personality.pId = bot_personality_index; // Initialize ID for indexing - bot_personality_index++; + newBot->personality.pId = bot_personality_index++; // Initialize ID for indexing return newBot; } @@ -631,12 +630,14 @@ qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) int randomIndex = rand() % bot_personality_index; int attempts = 0; temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; + qboolean foundInactiveBot = false; - while (attempts < bot_personality_index) { + while (!foundInactiveBot && attempts < bot_personality_index) { randomIndex = rand() % bot_personality_index; if (!bot_mappings[randomIndex].personality.isActive) { foundInactiveBot = true; + selectedBot = &bot_mappings[randomIndex]; break; } attempts++; diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index f0ca1dc70..7aa2a6bd4 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1401,10 +1401,10 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for bot_connections.total_team3++; if(bot_personality->value && (bot_personality_index == game.used_bot_personalities)) { - gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n"); + gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); DeactivateBotPersonality(); } - gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, bot_personality_index, game.used_bot_personalities); + gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); if (bot_personality->value) { if (bot_personality_index > game.used_bot_personalities) { diff --git a/src/action/g_local.h b/src/action/g_local.h index 27722683f..114a4adb7 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -781,7 +781,6 @@ typedef struct // Bot personalities loaded #ifndef NO_BOTS char* bot_file_path[MAX_QPATH]; - int loaded_bot_personalities; int used_bot_personalities; #endif } From e473e4289ef50b76528ec056b11a48d80bdbb0e9 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 5 Aug 2024 13:26:32 -0400 Subject: [PATCH 439/974] Setting isActive true --- src/action/botlib/botlib_personality.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index ccfa2e22a..a376eaf9e 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -614,6 +614,7 @@ qboolean BOTLIB_LoadBotPersonality(edict_t* self) self->bot.personality.combat_demeanor = selectedBot->personality.combat_demeanor; self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; self->bot.personality.leave_percent = selectedBot->personality.leave_percent; + self->bot.personality.isActive = true; if(pers_debug_mode) gi.dprintf("Selected Bot %s(indexes: %i/%i) - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", selectedBot->name, self->bot.personality.pId, selectedBot->personality.pId, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); From d6e1056802a6178e69e97a29983321cc657190d8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 5 Aug 2024 13:31:25 -0400 Subject: [PATCH 440/974] Cleaned up some debug calls --- src/action/botlib/botlib.h | 1 + src/action/botlib/botlib_spawn.c | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index fdc0257dd..bbda5739a 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -540,4 +540,5 @@ void BOTLIB_FreeBotPersonality(edict_t* bot); qboolean BotRageQuit(edict_t* self, qboolean frag_or_death); extern int loaded_bot_personalities; extern int bot_personality_index; +extern qboolean pers_debug_mode; #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 7aa2a6bd4..9580ffe5d 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1401,20 +1401,23 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for bot_connections.total_team3++; if(bot_personality->value && (bot_personality_index == game.used_bot_personalities)) { - gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); + if(pers_debug_mode) + gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); DeactivateBotPersonality(); } - gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); + if (pers_debug_mode) + gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); if (bot_personality->value) { if (bot_personality_index > game.used_bot_personalities) { if (!BOTLIB_SetPersonality(bot, team, force_gender)) { - gi.dprintf("Failed to load bot personality, using default userinfo.\n"); + if(pers_debug_mode) + gi.dprintf("Failed to load bot personality, using default userinfo.\n"); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { - gi.dprintf("Ran out of bot personalities, loading random bots now.\n"); - // Potentially deactivate a bot personality here, but need an ID or criteria + if(pers_debug_mode) + gi.dprintf("Ran out of bot personalities, loading random bots now.\n"); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { // Use random data From 7db5a469d5e90a2350bd2986487404e1cafa0b0b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 6 Aug 2024 16:26:19 +0300 Subject: [PATCH 441/974] Minor code style fix. --- src/common/bsp.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index ef38b0d71..3e234c5e9 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -349,9 +349,8 @@ LOAD(LeafFaces) mface_t **out; bsp->numleaffaces = count; - bsp->leaffaces = ALLOC(sizeof(*out) * count); + bsp->leaffaces = out = ALLOC(sizeof(*out) * count); - out = bsp->leaffaces; for (int i = 0; i < count; i++, out++) { uint32_t facenum = BSP_ExtLong(); ENSURE(facenum < bsp->numfaces, "Bad facenum"); From 30e839bbde5d40afbbfce6bbb6aa82ae35f9d31b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 6 Aug 2024 17:47:40 +0300 Subject: [PATCH 442/974] Fix capitalization of client names in comments. --- inc/common/protocol.h | 24 ++++++++++++------------ inc/shared/shared.h | 2 +- src/client/http.c | 2 +- src/server/main.c | 4 ++-- src/server/send.c | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index eb941e05c..54b9e1e16 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -141,13 +141,13 @@ typedef enum { svc_deltapacketentities, // [...] svc_frame, - // r1q2 specific operations + // R1Q2 specific operations svc_zpacket, svc_zdownload, - svc_gamestate, // q2pro specific, means svc_playerupdate in r1q2 + svc_gamestate, // Q2PRO specific, means svc_playerupdate in R1Q2 svc_setting, - // q2pro specific operations + // Q2PRO specific operations svc_configstringstream, svc_baselinestream, @@ -202,10 +202,10 @@ typedef enum { clc_userinfo, // [userinfo string] clc_stringcmd, // [string] message - // r1q2 specific operations + // R1Q2 specific operations clc_setting, - // q2pro specific operations + // Q2PRO specific operations clc_move_nodelta = 10, clc_move_batched, clc_userinfo_delta @@ -233,7 +233,7 @@ typedef enum { #define PS_RDFLAGS BIT(14) #define PS_RESERVED BIT(15) -// r1q2 protocol specific extra flags +// R1Q2 protocol specific extra flags #define EPS_GUNOFFSET BIT(0) #define EPS_GUNANGLES BIT(1) #define EPS_M_VELOCITY2 BIT(2) @@ -241,7 +241,7 @@ typedef enum { #define EPS_VIEWANGLE2 BIT(4) #define EPS_STATS BIT(5) -// q2pro protocol specific extra flags +// Q2PRO protocol specific extra flags #define EPS_CLIENTNUM BIT(6) //============================================== @@ -284,7 +284,7 @@ typedef enum { #define CM_BUTTONS BIT(6) #define CM_IMPULSE BIT(7) -// r1q2 button byte hacks +// R1Q2 button byte hacks #define BUTTON_MASK (BUTTON_ATTACK|BUTTON_USE|BUTTON_ANY) #define BUTTON_FORWARD BIT(2) #define BUTTON_SIDE BIT(3) @@ -367,14 +367,14 @@ typedef enum { #define PACKED_BSP 31 typedef enum { - // r1q2 specific + // R1Q2 specific CLS_NOGUN, CLS_NOBLEND, CLS_RECORDING, CLS_PLAYERUPDATES, CLS_FPS, - // q2pro specific + // Q2PRO specific CLS_NOGIBS = 10, CLS_NOFOOTSTEPS, CLS_NOPREDICT, @@ -384,14 +384,14 @@ typedef enum { } clientSetting_t; typedef enum { - // r1q2 specific + // R1Q2 specific SVS_PLAYERUPDATES, SVS_FPS, SVS_MAX } serverSetting_t; -// q2pro frame flags sent by the server +// Q2PRO frame flags sent by the server // only SUPPRESSCOUNT_BITS can be used #define FF_SUPPRESSED BIT(0) #define FF_CLIENTDROP BIT(1) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 060216569..d7ddf565f 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -841,7 +841,7 @@ COLLISION DETECTION #define SURF_FLOWING BIT(6) // scroll towards angle #define SURF_NODRAW BIT(7) // don't bother referencing the texture -#define SURF_ALPHATEST BIT(25) // used by kmquake2 +#define SURF_ALPHATEST BIT(25) // used by KMQuake2 //KEX #define SURF_N64_UV BIT(28) diff --git a/src/client/http.c b/src/client/http.c index 1b15c71bc..6c1eb3dcf 100644 --- a/src/client/http.c +++ b/src/client/http.c @@ -584,7 +584,7 @@ int HTTP_QueueDownload(const char *path, dltype_t type) //this is a nasty hack to let the server know what we're doing so admins don't //get confused by a ton of people stuck in CNCT state. it's assumed the server - //is running r1q2 if we're even able to do http downloading so hopefully this + //is running R1Q2 if we're even able to do http downloading so hopefully this //won't spew an error msg. if (!download_default_repo) CL_ClientCommand("download http\n"); diff --git a/src/server/main.c b/src/server/main.c index c15adec85..548578e8a 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -992,7 +992,7 @@ static void init_pmove_and_es_flags(client_t *newcl) } newcl->pmp.strafehack = sv_strafejump_hack->integer >= force; - // r1q2 extensions + // R1Q2 extensions if (newcl->protocol == PROTOCOL_VERSION_R1Q2) { newcl->esFlags |= MSG_ES_BEAMORIGIN; if (newcl->version >= PROTOCOL_VERSION_R1Q2_LONG_SOLID) { @@ -1000,7 +1000,7 @@ static void init_pmove_and_es_flags(client_t *newcl) } } - // q2pro extensions + // Q2PRO extensions force = 2; if (newcl->protocol == PROTOCOL_VERSION_Q2PRO) { if (sv_qwmod->integer) { diff --git a/src/server/send.c b/src/server/send.c index 191289047..0afb0e639 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -662,7 +662,7 @@ static void repack_unreliables(client_t *client, unsigned maxsize) if (msg->cursize == SOUND_PACKET || msg->data[0] != svc_temp_entity) { continue; } - // ignore some low-priority effects, these checks come from r1q2 + // ignore some low-priority effects, these checks come from R1Q2 if (msg->data[1] == TE_BLOOD || msg->data[1] == TE_SPLASH || msg->data[1] == TE_GUNSHOT || msg->data[1] == TE_BULLET_SPARKS || msg->data[1] == TE_SHOTGUN) { From 3bb1065600465c5d15c4fe85c6e07944d608a54c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 6 Aug 2024 22:45:01 +0300 Subject: [PATCH 443/974] Clean up mapcmd parsing. Also check if not running dedicated server before allowing demo playback. --- inc/server/server.h | 1 + src/server/init.c | 94 +++++++++++++++++++++++++++------------------ src/server/server.h | 3 -- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/inc/server/server.h b/inc/server/server.h index 55cc8b6a9..15a86ba58 100644 --- a/inc/server/server.h +++ b/inc/server/server.h @@ -28,6 +28,7 @@ typedef enum { ss_pic, // showing static picture ss_broadcast, // running MVD client ss_cinematic, // playing a cinematic + ss_demo = -1, // hack for demomap command, not a real server state } server_state_t; #if USE_ICMP diff --git a/src/server/init.c b/src/server/init.c index 07615bca1..0eacf8b4f 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -112,6 +112,8 @@ void SV_SpawnServer(const mapcmd_t *cmd) Com_Printf("------- Server Initialization -------\n"); Com_Printf("SpawnServer: %s\n", cmd->server); + Q_assert(cmd->state >= ss_game); + // everyone needs to reconnect FOR_EACH_CLIENT(client) { SV_ClientReset(client); @@ -221,65 +223,81 @@ void SV_SpawnServer(const mapcmd_t *cmd) Com_Printf("-------------------------------------\n"); } -static bool check_server(mapcmd_t *cmd, const char *server, bool nextserver) +static server_state_t get_server_state(const char *s) { - char expanded[MAX_QPATH]; - char *s, *ch; - int ret = Q_ERR(ENAMETOOLONG); + s = COM_FileExtension(s); - // copy it off to keep original mapcmd intact + if (!Q_stricmp(s, ".pcx")) + return ss_pic; + + if (!Q_stricmp(s, ".cin")) + return ss_cinematic; + + if (!Q_stricmp(s, ".dm2")) + return ss_demo; + + return ss_game; +} + +static bool parse_and_check_server(mapcmd_t *cmd, const char *server, bool nextserver) +{ + char expanded[MAX_QPATH], *ch; + int ret = Q_ERR(ENAMETOOLONG); + + // copy it off Q_strlcpy(cmd->server, server, sizeof(cmd->server)); - s = cmd->server; // if there is a $, use the remainder as a spawnpoint - ch = strchr(s, '$'); - if (ch) { - *ch = 0; - cmd->spawnpoint = ch + 1; - } else { - cmd->spawnpoint = s + strlen(s); - } + ch = Q_strchrnul(cmd->server, '$'); + if (*ch) + *ch++ = 0; + cmd->spawnpoint = ch; // now expand and try to load the map - if (!COM_CompareExtension(s, ".pcx")) { - if (Q_concat(expanded, sizeof(expanded), "pics/", s) < sizeof(expanded)) { + server_state_t state = get_server_state(cmd->server); + switch (state) { + case ss_pic: + if (Q_concat(expanded, sizeof(expanded), "pics/", cmd->server) < sizeof(expanded)) ret = COM_DEDICATED ? Q_ERR_SUCCESS : FS_LoadFile(expanded, NULL); - } - cmd->state = ss_pic; - } else if (!COM_CompareExtension(s, ".cin")) { + break; + + case ss_cinematic: if (!sv_cinematics->integer && nextserver) return false; // skip it - if (Q_concat(expanded, sizeof(expanded), "video/", s) < sizeof(expanded)) { + if (Q_concat(expanded, sizeof(expanded), "video/", cmd->server) < sizeof(expanded)) ret = SCR_CheckForCinematic(expanded); - } - cmd->state = ss_cinematic; - } else if (!COM_CompareExtension(s, ".dm2")) { + break; + + case ss_demo: if (!sv_cinematics->integer && nextserver) - return false; // skip it - if (Q_concat(expanded, sizeof(expanded), "demos/", s) < sizeof(expanded)) { + return false; // skip it + if (Q_concat(expanded, sizeof(expanded), "demos/", cmd->server) >= sizeof(expanded)) + break; + ret = Q_ERR(ENOSYS); // only works if running a client #if USE_CLIENT - ret = cmd->loadgame ? Q_ERR(ENOSYS) : FS_LoadFile(expanded, NULL); - if (ret == Q_ERR(EFBIG)) - ret = Q_ERR_SUCCESS; -#else - ret = Q_ERR(ENOSYS); + if (dedicated->integer || cmd->loadgame) + break; // not supported + ret = FS_LoadFile(expanded, NULL); + if (ret == Q_ERR(EFBIG)) + ret = Q_ERR_SUCCESS; #endif - } - cmd->state = ss_demo; - } else { - CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); - if (Q_concat(expanded, sizeof(expanded), "maps/", s, ".bsp") < sizeof(expanded)) { + break; + + default: + CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); // may override server! + if (Q_concat(expanded, sizeof(expanded), "maps/", cmd->server, ".bsp") < sizeof(expanded)) ret = CM_LoadMap(&cmd->cm, expanded); - } - cmd->state = ss_game; + if (ret < 0) + CM_FreeMap(&cmd->cm); // free entstring if overridden + break; } if (ret < 0) { Com_Printf("Couldn't load %s: %s\n", expanded, BSP_ErrorString(ret)); - CM_FreeMap(&cmd->cm); // free entstring if overridden return false; } + cmd->state = state; return true; } @@ -320,7 +338,7 @@ bool SV_ParseMapCmd(mapcmd_t *cmd) *ch = 0; // see if map exists and can be loaded - if (check_server(cmd, s, ch)) { + if (parse_and_check_server(cmd, s, ch)) { if (ch) Cvar_Set("nextserver", va("gamemap \"!%s\"", ch + 1)); else diff --git a/src/server/server.h b/src/server/server.h index 2862d1fab..e3638419c 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -598,9 +598,6 @@ void sv_min_timeout_changed(cvar_t *self); // sv_init.c // -// not a real server state! hack for demomap command. -#define ss_demo -1 - void SV_ClientReset(client_t *client); void SV_SetState(server_state_t state); void SV_SpawnServer(const mapcmd_t *cmd); From 8a2e2c6faaab28b0c46826ada60484131d07a4af Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 7 Aug 2024 17:07:33 +0300 Subject: [PATCH 444/974] =?UTF-8?q?Complete=20cinematic=20names=20for=20?= =?UTF-8?q?=E2=80=98demomap=E2=80=99=20command.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/client/client.h | 4 ++++ src/client/cin.c | 10 ++++++++++ src/client/ffcin.c | 22 ++++++++++++++++++++++ src/server/commands.c | 1 + 4 files changed, 37 insertions(+) diff --git a/inc/client/client.h b/inc/client/client.h index d39307b80..1d4375958 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -98,7 +98,9 @@ void Con_Close(bool force); void SCR_BeginLoadingPlaque(void); void SCR_EndLoadingPlaque(void); + int SCR_CheckForCinematic(const char *name); +void SCR_Cinematic_g(genctx_t *ctx); void SCR_ModeChanged(void); void SCR_UpdateScreen(void); @@ -150,6 +152,8 @@ float V_CalcFov(float fov_x, float width, float height); #define SCR_BeginLoadingPlaque() (void)0 #define SCR_EndLoadingPlaque() (void)0 + #define SCR_CheckForCinematic(name) Q_ERR_SUCCESS +#define SCR_Cinematic_g(ctx) (void)0 #endif // !USE_CLIENT diff --git a/src/client/cin.c b/src/client/cin.c index 82fe354d5..63195092d 100644 --- a/src/client/cin.c +++ b/src/client/cin.c @@ -476,3 +476,13 @@ int SCR_CheckForCinematic(const char *name) return ret; } + +/* +================== +SCR_Cinematic_g +================== +*/ +void SCR_Cinematic_g(genctx_t *ctx) +{ + FS_File_g("video", ".cin", FS_SEARCH_RECURSIVE | FS_TYPE_REAL, ctx); +} diff --git a/src/client/ffcin.c b/src/client/ffcin.c index 85c81e0e8..62a5d55c6 100644 --- a/src/client/ffcin.c +++ b/src/client/ffcin.c @@ -73,6 +73,7 @@ static const avformat_t formats[] = { { ".cin", "idcin", AV_CODEC_ID_IDCIN }, }; +static char extensions[MAX_QPATH]; static int supported; /* @@ -90,6 +91,9 @@ void SCR_InitCinematics(void) !avcodec_find_decoder(f->codec_id)) continue; supported |= BIT(i); + if (*extensions) + Q_strlcat(extensions, ";", sizeof(extensions)); + Q_strlcat(extensions, f->ext, sizeof(extensions)); } Com_DPrintf("Supported cinematic formats: %#x\n", supported); @@ -723,3 +727,21 @@ int SCR_CheckForCinematic(const char *name) return ret; } + +/* +================== +SCR_Cinematic_g +================== +*/ +void SCR_Cinematic_g(genctx_t *ctx) +{ + const unsigned flags = FS_SEARCH_RECURSIVE | FS_SEARCH_STRIPEXT | FS_TYPE_REAL; + int count; + void **list; + + ctx->ignoredups = true; + list = FS_ListFiles("video", extensions, flags, &count); + for (int i = 0; i < count; i++) + Prompt_AddMatch(ctx, va("%s.cin", (char *)list[i])); + FS_FreeList(list); +} diff --git a/src/server/commands.c b/src/server/commands.c index 663101948..531d5876e 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -427,6 +427,7 @@ static void SV_DemoMap_c(genctx_t *ctx, int argnum) #if USE_CLIENT if (argnum == 1) { FS_File_g("demos", ".dm2", FS_SEARCH_RECURSIVE, ctx); + SCR_Cinematic_g(ctx); } #endif } From fc1dd3194359fb69bccb7f4bfed3ca32854e99fd Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 7 Aug 2024 20:34:42 +0300 Subject: [PATCH 445/974] Ignore duplicates when completing overrides. --- src/server/commands.c | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/server/commands.c b/src/server/commands.c index 531d5876e..223bea323 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -409,17 +409,33 @@ static void SV_Map_f(void) static void SV_Map_c(genctx_t *ctx, int argnum) { - unsigned flags = FS_SEARCH_RECURSIVE | FS_SEARCH_STRIPEXT; - if (argnum == 1) { - FS_File_g("maps", ".bsp", flags, ctx); - const char *s = Cvar_VariableString("map_override_path"); - if (*s) { - int pos = ctx->count; - FS_File_g(s, ".bsp.override", flags, ctx); - for (int i = pos; i < ctx->count; i++) - *COM_FileExtension(ctx->matches[i]) = 0; - } + const char *path; + void **list; + int count; + + if (argnum != 1) + return; + + // complete regular maps + FS_File_g("maps", ".bsp", FS_SEARCH_RECURSIVE | FS_SEARCH_STRIPEXT, ctx); + + // complete overrides + path = Cvar_VariableString("map_override_path"); + if (!*path) + return; + + list = FS_ListFiles(path, ".bsp.override", FS_SEARCH_RECURSIVE, &count); + if (!list) + return; + + ctx->ignoredups = true; + for (int i = 0; i < count; i++) { + const char *s = list[i]; + const int len = strlen(s) - strlen(".bsp.override"); + Prompt_AddMatch(ctx, va("%.*s", len, s)); } + + FS_FreeList(list); } static void SV_DemoMap_c(genctx_t *ctx, int argnum) From d394a365fcae57bed097524f807b897219a7cc4d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 7 Aug 2024 22:45:49 -0400 Subject: [PATCH 446/974] Bot chat tied to personality trait --- src/action/botlib/botlib.h | 2 ++ src/action/botlib/botlib_communication.c | 8 +++++++- src/action/botlib/botlib_personality.c | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index bbda5739a..957b830f3 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -538,6 +538,8 @@ qboolean BOTLIB_SetPersonality(edict_t* bot, int team, int force_gender); qboolean BOTLIB_LoadBotPersonality(edict_t* self); void BOTLIB_FreeBotPersonality(edict_t* bot); qboolean BotRageQuit(edict_t* self, qboolean frag_or_death); +qboolean BOTLIB_DoIChat(edict_t* bot); + extern int loaded_bot_personalities; extern int bot_personality_index; extern qboolean pers_debug_mode; diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 0c512d2bb..3e3c3a2e4 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -174,7 +174,13 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) if (chattype == CHAT_GOODBYE || chattype == CHAT_RAGE) randval = randval - 0.3; // Increase the chance of this type of message delayed = false; - if (randval > 0.2) { // 80% do not chat + + // Extra randomization + if (bot_personality->value) { + if (!BOTLIB_DoIChat(bot)){ + return; + } + } else if (randval > 0.2) { // 80% do not chat //gi.dprintf("Skipping chat due to random chance (%f)\n", randval); return; // Don't chat too often } diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index a376eaf9e..a22afe686 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -751,4 +751,21 @@ void BOTLIB_FreeBotPersonality(edict_t* bot) } } game.used_bot_personalities--; +} + +qboolean BOTLIB_DoIChat(edict_t* bot) { + float chatty = bot->bot.personality.chat_demeanor; + srand(time(NULL)); + size_t multi = rand() % 100; + + // Normalize chatty value to range 0 to 1 + float normalizedChatty = (chatty + 1) / 2; + + // Determine if the bot will chat + if (multi <= normalizedChatty * 100) { + return true; // Bot will chat + } else { + return false; // Bot will not chat + } + return false; // Default false } \ No newline at end of file From 5b731eaa02773cc10a4f858d749f0685a3d680ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 8 Aug 2024 13:11:56 +0300 Subject: [PATCH 447/974] Don't scale flares when occluding. --- src/refresh/main.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index d906daaa1..de014769e 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -405,8 +405,6 @@ static void make_flare_quad(const entity_t *e, float scale, vec3_t points[4]) { vec3_t up, down, left, right; - scale *= e->scale; - VectorScale(glr.viewaxis[1], scale, left); VectorScale(glr.viewaxis[1], -scale, right); VectorScale(glr.viewaxis[2], -scale, down); @@ -507,7 +505,7 @@ static void GL_DrawFlare(const entity_t *e) e->rgba.u8[2] / 255.0f, e->alpha * 0.5f * q->frac); - make_flare_quad(e, 25.0f * q->frac, points); + make_flare_quad(e, e->scale * 25.0f * q->frac, points); GL_TexCoordPointer(2, 0, quad_tc); GL_VertexPointer(3, 0, &points[0][0]); From c4a5d65be6b7e58c8986139e35806806a11aa7b8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 8 Aug 2024 17:05:13 +0300 Subject: [PATCH 448/974] Access GL backend API via pointer to const vtable. --- src/refresh/gl.h | 23 ++++++++++++----------- src/refresh/state.c | 27 +++++++++++++++------------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 89a9bb2e4..5403d80fc 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -87,7 +87,6 @@ typedef struct { typedef struct { bool registering; bool use_shaders; - glbackend_t backend; struct { bsp_t *cache; vec_t *vertices; @@ -519,6 +518,8 @@ typedef struct { extern glState_t gls; +extern const glbackend_t *gl_backend; + static inline void GL_ActiveTexture(GLuint tmu) { if (gls.server_tmu != tmu) { @@ -538,7 +539,7 @@ static inline void GL_ClientActiveTexture(GLuint tmu) static inline void GL_StateBits(GLbitfield bits) { if (gls.state_bits != bits) { - gl_static.backend.state_bits(bits); + gl_backend->state_bits(bits); gls.state_bits = bits; } } @@ -546,7 +547,7 @@ static inline void GL_StateBits(GLbitfield bits) static inline void GL_ArrayBits(GLbitfield bits) { if (gls.array_bits != bits) { - gl_static.backend.array_bits(bits); + gl_backend->array_bits(bits); gls.array_bits = bits; } } @@ -567,14 +568,14 @@ static inline void GL_UnlockArrays(void) static inline void GL_ForceMatrix(const GLfloat *matrix) { - gl_static.backend.load_view_matrix(matrix); + gl_backend->load_view_matrix(matrix); gls.currentmatrix = matrix; } static inline void GL_LoadMatrix(const GLfloat *matrix) { if (gls.currentmatrix != matrix) { - gl_static.backend.load_view_matrix(matrix); + gl_backend->load_view_matrix(matrix); gls.currentmatrix = matrix; } } @@ -595,12 +596,12 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) qglDepthRange(n, f); } -#define GL_VertexPointer gl_static.backend.vertex_pointer -#define GL_TexCoordPointer gl_static.backend.tex_coord_pointer -#define GL_LightCoordPointer gl_static.backend.light_coord_pointer -#define GL_ColorBytePointer gl_static.backend.color_byte_pointer -#define GL_ColorFloatPointer gl_static.backend.color_float_pointer -#define GL_Color gl_static.backend.color +#define GL_VertexPointer gl_backend->vertex_pointer +#define GL_TexCoordPointer gl_backend->tex_coord_pointer +#define GL_LightCoordPointer gl_backend->light_coord_pointer +#define GL_ColorBytePointer gl_backend->color_byte_pointer +#define GL_ColorFloatPointer gl_backend->color_float_pointer +#define GL_Color gl_backend->color void GL_ForceTexture(GLuint tmu, GLuint texnum); void GL_BindTexture(GLuint tmu, GLuint texnum); diff --git a/src/refresh/state.c b/src/refresh/state.c index dc12a832b..0a00a9079 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -20,6 +20,8 @@ with this program; if not, write to the Free Software Foundation, Inc., glState_t gls; +const glbackend_t *gl_backend; + // for uploading void GL_ForceTexture(GLuint tmu, GLuint texnum) { @@ -150,7 +152,7 @@ void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat zn matrix[11] = 0; matrix[15] = 1; - gl_static.backend.load_proj_matrix(matrix); + gl_backend->load_proj_matrix(matrix); } void GL_Setup2D(void) @@ -168,10 +170,10 @@ void GL_Setup2D(void) draw.scissor = false; } - if (gl_static.backend.setup_2d) - gl_static.backend.setup_2d(); + if (gl_backend->setup_2d) + gl_backend->setup_2d(); - gl_static.backend.load_view_matrix(NULL); + gl_backend->load_view_matrix(NULL); } void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) @@ -217,7 +219,7 @@ void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) matrix[11] = -1; matrix[15] = 0; - gl_static.backend.load_proj_matrix(matrix); + gl_backend->load_proj_matrix(matrix); } static void GL_RotateForViewer(void) @@ -257,8 +259,8 @@ void GL_Setup3D(bool waterwarp) qglViewport(glr.fd.x, r_config.height - (glr.fd.y + glr.fd.height), glr.fd.width, glr.fd.height); - if (gl_static.backend.setup_3d) - gl_static.backend.setup_3d(); + if (gl_backend->setup_3d) + gl_backend->setup_3d(); GL_Frustum(glr.fd.fov_x, glr.fd.fov_y, 1.0f); @@ -316,7 +318,7 @@ void GL_ClearState(void) qglCullFace(GL_BACK); qglEnable(GL_CULL_FACE); - gl_static.backend.clear_state(); + gl_backend->clear_state(); qglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | gl_static.stencil_buffer_bit); @@ -342,13 +344,14 @@ void GL_InitState(void) } } - gl_static.backend = gl_static.use_shaders ? backend_shader : backend_legacy; - gl_static.backend.init(); + gl_backend = gl_static.use_shaders ? &backend_shader : &backend_legacy; + gl_backend->init(); - Com_Printf("Using %s rendering backend.\n", gl_static.backend.name); + Com_Printf("Using %s rendering backend.\n", gl_backend->name); } void GL_ShutdownState(void) { - gl_static.backend.shutdown(); + gl_backend->shutdown(); + gl_backend = NULL; } From febef38996ba2888dc7bf35438b61b30ad7965c7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 01:51:19 -0400 Subject: [PATCH 449/974] Added some weapon/item selection, chat options, spawnrush funcs.. --- src/action/acesrc/acebot_ai.c | 3 +- src/action/botlib/botlib.h | 5 + src/action/botlib/botlib_communication.c | 64 +++++- src/action/botlib/botlib_personality.c | 248 ++++++++++++++++++++++- src/action/g_local.h | 1 + src/action/g_utils.c | 4 + src/action/p_client.c | 11 +- 7 files changed, 320 insertions(+), 16 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index d197c1683..7723d677c 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -229,7 +229,8 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) } // At LCA, randomly pick a spawn spot. This breaks up the bots at LCA, so they spread out. - if (random() < 0.2 || (teamplay->value && lights_camera_action)) // Greater than LCA! + // BOTLIB_Spawnrush will take into account bot personality and current weaponry + if (random() < 0.2 || (teamplay->value && lights_camera_action) || BOTLIB_SpawnRush(self)) // Greater than LCA! { // Find all the spawn points int sp_counter = 0; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 957b830f3..d8af89b37 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -225,6 +225,7 @@ typedef enum CHAT_KILLED, CHAT_INSULTS, CHAT_GOODBYE, + CHAT_VICTORY, CHAT_RAGE, } bot_chat_types_t; @@ -539,6 +540,10 @@ qboolean BOTLIB_LoadBotPersonality(edict_t* self); void BOTLIB_FreeBotPersonality(edict_t* bot); qboolean BotRageQuit(edict_t* self, qboolean frag_or_death); qboolean BOTLIB_DoIChat(edict_t* bot); +void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot); +void BOTLIB_BotPersonalityChooseItem(edict_t* bot); +void BOTLIB_BotPersonalityChooseItemKit(edict_t* bot); +qboolean BOTLIB_SpawnRush(edict_t* bot); extern int loaded_bot_personalities; extern int bot_personality_index; diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 3e3c3a2e4..97329351f 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -4,6 +4,7 @@ #include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 +// // Delayed chat, somewhat more realistic #define MAX_MESSAGES 100 @@ -20,7 +21,7 @@ typedef struct { MessageQueue chatQueue = { .count = 0 }; -void AddMessageToQueue(edict_t* bot, const char* text, int frameReceived) { +void AddMessageToChatQueue(edict_t* bot, const char* text, int frameReceived) { if (chatQueue.count < MAX_MESSAGES) { chatQueue.messages[chatQueue.count].bot = bot; strncpy(chatQueue.messages[chatQueue.count].text, text, sizeof(chatQueue.messages[chatQueue.count].text) - 1); @@ -33,7 +34,7 @@ void AddMessageToQueue(edict_t* bot, const char* text, int frameReceived) { void ProcessChatQueue(int currentFrame) { for (int i = 0; i < chatQueue.count; i++) { - if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ) { + if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ/HZ) { // Send the chat message from the correct bot BOTLIB_Say(chatQueue.messages[i].bot, chatQueue.messages[i].text, false); //gi.dprintf("Sending delayed chat message from bot: %s\n", chatQueue.messages[i].text); @@ -53,6 +54,32 @@ void UpdateBotChat(void) { ProcessChatQueue(level.framenum); } +// Function to get a random bot +edict_t* getRandomBot() +{ + edict_t* bots[MAX_CLIENTS]; + int botCount = 0; + + // Populate the bots array with pointers to bots + for (int i = 0; i < num_players; i++) + { + if (players[i]->is_bot) + { + bots[botCount++] = players[i]; + } + } + + // If we found any bots, return a random one + if (botCount > 0) + { + int randomIndex = rand() % botCount; // Generate a random index + return bots[randomIndex]; + } + + // If no bots were found, return NULL + return NULL; +} + // Borrowed from LTK bots #define DBC_WELCOMES 4 char *botchat_welcomes[DBC_WELCOMES] = @@ -131,11 +158,14 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) char* text = NULL; qboolean delayed = true; + // bot_is_target means the bot edict provided to this function is not the talker, but the bot the other + // bots will talk to, such as 'Welcome !'. Bot chosen to speak will be random from the list + // of current/inuse bots + qboolean bot_is_target = false; switch (chattype) { - case CHAT_WELCOME: - text = botchat_welcomes[rand() % DBC_WELCOMES]; - break; + //case CHAT_WELCOME: + // CHAT_WELCOME is handled below outside of this switch statement case CHAT_KILLED: text = botchat_killeds[rand() % DBC_KILLEDS]; break; @@ -145,6 +175,9 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) case CHAT_GOODBYE: text = botchat_goodbyes[rand() % DBC_GOODBYES]; break; + case CHAT_RAGE: + text = botchat_rage[rand() % DBC_RAGE]; + break; default: if (debug_mode) gi.bprintf(PRINT_HIGH, "%s: Unknown chat type %d", __func__, chattype); @@ -171,11 +204,26 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) } } // Goodbyes and rages happen without delay - if (chattype == CHAT_GOODBYE || chattype == CHAT_RAGE) + if (chattype == CHAT_GOODBYE || chattype == CHAT_RAGE) { randval = randval - 0.3; // Increase the chance of this type of message delayed = false; + } + + // In this one, the bot edict is not the talker, but the 'target' of the talker + // Then we switch the bot after we get the target's name, since the talker + // will be random + if (chattype == CHAT_WELCOME) { + int index = rand() % DBC_WELCOMES; // Choose a random message index + char message[256]; // Assuming 256 bytes is enough for the message + snprintf(message, sizeof(message), botchat_welcomes[index], bot->client->pers.netname); + // Copy back to 'text' + snprintf(text, sizeof(text), "%s", message); + if (getRandomBot() != NULL) + bot = getRandomBot(); + } - // Extra randomization + // bot_personality + // This will let the bot chat based on their propensity to do so if (bot_personality->value) { if (!BOTLIB_DoIChat(bot)){ return; @@ -187,7 +235,7 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) // Add the message to the queue if delayed if (delayed) - AddMessageToQueue(bot, text, level.framenum); + AddMessageToChatQueue(bot, text, level.framenum); else // instant message, such as a goodbye before leaving BOTLIB_Say(bot, text, false); diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index a22afe686..390f3f80f 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -4,7 +4,6 @@ #include "/opt/homebrew/include/jansson.h" // Count of bot personalities loaded -char** botNames = NULL; // Global array of bot names qboolean pers_debug_mode = true; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create @@ -177,10 +176,9 @@ qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) return false; } - // Dynamically update the map preferences of the bot // Not all bots will have a preference, so we default to 0 for those -void update_map_pref(json_t* root, char* map_name, temp_bot_mapping_t* newBot) +void UpdateMapPref(json_t* root, char* map_name, temp_bot_mapping_t* newBot) { // Get the "map_prefs" object from the root json_t* map_prefs = json_object_get(root, "map_prefs"); @@ -265,7 +263,7 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) json_t* map_prefs = json_object_get(botDetails, "map_prefs"); newBot->personality.map_prefs = validate_pref_numeric(map_prefs, "map_prefs"); - update_map_pref(botDetails, level.mapname, newBot); + UpdateMapPref(botDetails, level.mapname, newBot); json_t* combat_demeanor = json_object_get(botDetails, "combat_demeanor"); newBot->personality.combat_demeanor = validate_pref_numeric(combat_demeanor, "combat_demeanor"); @@ -627,7 +625,6 @@ qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) return false; } - srand(time(NULL)); int randomIndex = rand() % bot_personality_index; int attempts = 0; temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; @@ -755,7 +752,6 @@ void BOTLIB_FreeBotPersonality(edict_t* bot) qboolean BOTLIB_DoIChat(edict_t* bot) { float chatty = bot->bot.personality.chat_demeanor; - srand(time(NULL)); size_t multi = rand() % 100; // Normalize chatty value to range 0 to 1 @@ -768,4 +764,244 @@ qboolean BOTLIB_DoIChat(edict_t* bot) { return false; // Bot will not chat } return false; // Default false +} + + +bool isArrayAllZeros(const float* array, int size) { + for (int i = 0; i < size; i++) { + if (array[i] != 0.0f) { + return false; // Found a non-zero element + } + } + return true; // All elements are zero +} + +// +/* +MK23_NUM (1) maps to weapon_prefs[0] +MP5_NUM (2) maps to weapon_prefs[1] +M4_NUM (3) maps to weapon_prefs[2] +M3_NUM (4) maps to weapon_prefs[3] +HC_NUM (5) maps to weapon_prefs[4] +SNIPER_NUM (6) maps to weapon_prefs[5] +DUAL_NUM (7) maps to weapon_prefs[6] +KNIFE_NUM (8) maps to weapon_prefs[7] +GRENADE_NUM (9) maps to weapon_prefs[8] +*/ +// "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3] +void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { + float weapon_prefs[10]; + memcpy(weapon_prefs, bot->bot.personality.weapon_prefs, sizeof(bot->bot.personality.weapon_prefs)); + + int chosen_weapon_index = 0; // Initialize with the first index + float highest_pref = weapon_prefs[0]; // Initialize with the first weapon's preference + + int top3_indices[3] = {0, 0, 0}; + float top3_prefs[3] = {-1.0f, -1.0f, -1.0f}; // Initialize with low values + + qboolean all_zeros = isArrayAllZeros(weapon_prefs, sizeof(weapon_prefs) / sizeof(weapon_prefs[0])); + + // All zeroes somehow? Still let's pick a good weapon + if (all_zeros) + BOTLIB_SmartWeaponSelection(bot); + return; + + for (int i = 0; i < (WEAPON_COUNT - 1); i++) { // Adjust loop to exclude grenades + float current_pref = weapon_prefs[i]; + for (int j = 0; j < 3; j++) { + if (current_pref > top3_prefs[j]) { + // Shift lower preferences down + for (int k = 2; k > j; k--) { + top3_prefs[k] = top3_prefs[k - 1]; + top3_indices[k] = top3_indices[k - 1]; + } + // Insert the new preference + top3_prefs[j] = current_pref; + top3_indices[j] = i; + break; // Break since we've inserted the current preference + } + } + } + + // Will choose between always picking the top weapon, from choosing from the top 3 + int randomIndex = rand() % 2; + + // Case 1: Always pick the top weapon + if (randomIndex == 0) { + ACEAI_Cmd_Choose_Weapon_Num(bot, top3_indices[0]); + return; + } + // Case 2: Randomly choose from the top 3 + else { + int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 + ACEAI_Cmd_Choose_Weapon_Num(bot, top3_indices[randomChoice]); + return; + } + + // If for whatever reason this doesn't work, safely return BOTLIB_SmartWeaponSelection + return BOTLIB_SmartWeaponSelection(bot); +} + +/* +#define SIL_NUM 10 +#define SLIP_NUM 11 +#define BAND_NUM 12 +#define KEV_NUM 13 +#define LASER_NUM 14 +#define HELM_NUM 15 +*/ +//"item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0] + +void BOTLIB_BotPersonalityChooseItem(edict_t* bot) +{ + float item_prefs[6]; + memcpy(item_prefs, bot->bot.personality.item_prefs, sizeof(item_prefs)); + + int chosen_item_index = 0; // Initialize with the first index + float highest_pref = item_prefs[0]; // Initialize with the first item's preference + + int top3_indices[3] = {0, 0, 0}; // Initialize with the first index + float top3_prefs[3] = {-1.0f, -1.0f, -1.0f}; // Initialize with low values + + qboolean all_zeros = isArrayAllZeros(item_prefs, sizeof(item_prefs) / sizeof(item_prefs[0])); + + // Will choose between always picking the top item, from choosing from the top 3 + int randomIndex = rand() % 2; + + // If this is all zeroes, we're picking a random if we haven't selected an item yet + if (all_zeros && bot->client->selected_item < 1) + chosen_item_index = rand() % 6; // Randomly choose one of the 6 items + ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index); + return; + + for (int i = 0; i < ITEM_COUNT; i++) { // Adjust loop for 6 items + float current_pref = item_prefs[i]; + for (int j = 0; j < 3; j++) { + if (current_pref > top3_prefs[j]) { + // Shift lower preferences down + for (int k = 2; k > j; k--) { + top3_prefs[k] = top3_prefs[k - 1]; + top3_indices[k] = top3_indices[k - 1]; + } + // Insert the new preference + top3_prefs[j] = current_pref; + top3_indices[j] = i; + break; // Break since we've inserted the current preference + } + } + } + + // Case 1: Always pick the top item + if (randomIndex == 0) { + ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[0]); + } + // Case 2: Randomly choose from the top 3 + else { + int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 + ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[randomChoice]); + } +} + +/* +#define KEV_NUM 13 +#define C_KIT_NUM 25 +#define S_KIT_NUM 26 +#define A_KIT_NUM 27 + +Commando, Stealth and Assassin Kits +*/ + + +void BOTLIB_BotPersonalityChooseItemKit(edict_t* bot) { + float item_kit_prefs[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // Initialize preferences to 0 + + int weaponNum = bot->client->weapon ? bot->client->weapon->typeNum : 0; + + // Adjust preferences based on weapon + if (weaponNum == SNIPER_NUM || weaponNum == M4_NUM) { + item_kit_prefs[0] += 0.25; + } + if (weaponNum == MP5_NUM || weaponNum == M4_NUM) { + item_kit_prefs[1] += 0.25; + } + if (weaponNum == HC_NUM || weaponNum == M3_NUM) { + item_kit_prefs[2] += 0.25; + } + if (weaponNum == MP5_NUM || weaponNum == DUAL_NUM) { + item_kit_prefs[3] += 0.25; + } + + int top3_indices[3] = {0, 0, 0}; + float top3_prefs[3] = {-1.0f, -1.0f, -1.0f}; + + // Determine top 3 preferences + for (int i = 0; i < 4; i++) { // Adjusted loop condition + float current_pref = item_kit_prefs[i]; + for (int j = 0; j < 3; j++) { + if (current_pref > top3_prefs[j]) { + for (int k = 2; k > j; k--) { + top3_prefs[k] = top3_prefs[k - 1]; + top3_indices[k] = top3_indices[k - 1]; + } + top3_prefs[j] = current_pref; + top3_indices[j] = i; + break; + } + } + } + + // Choose item + if (rand() % 2 == 0) { + ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[0]); + } else { + ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[rand() % 3]); + } +} + +//"combat_demeanor": 0.45 +qboolean BOTLIB_SpawnRush(edict_t* bot) { + int weaponNum = bot->client->weapon ? bot->client->weapon->typeNum : 0; + int itemNum = bot->client->selected_item ? bot->client->selected_item : 0; + + // Normalize combat_index from [-1, 1] to [0, 1] + float normalized_combat_index = (bot->bot.personality.combat_demeanor + 1) / 2.0f; + + float probabilityThreshold; + if (normalized_combat_index < 0.5) { + // Scale for values below 0.5 to be between 0% and 25% + probabilityThreshold = 0.5f * normalized_combat_index; + } else { + // Adjust the scale for values above 0.5 to be between 25% and 100% + probabilityThreshold = 0.25f + ((normalized_combat_index - 0.5f) * 1.5f); + } + + // Adjusting probabilityThreshold based on weapon possession + if (weaponNum == SNIPER_NUM) { + probabilityThreshold -= 0.1; // Decrease chance of rush for sniper rifle + } else if ((weaponNum == M4_NUM || weaponNum == HC_NUM || weaponNum == M3_NUM)) { + probabilityThreshold += 0.1; // Increase chance of rush for M4, handcannon, and shotgun + } + + // Adjusting probabilityThreshold based on item possession + if ((itemNum == SIL_NUM || itemNum == SLIP_NUM)) { + probabilityThreshold -= 0.1; // Decrease chance of rush for silencer and slipper users + } else if ((itemNum == KEV_NUM || itemNum == HELM_NUM || itemNum == BAND_NUM || itemNum == LASER_NUM)) { + probabilityThreshold += 0.1; // Increase chance of rush for vest, helm, laser and bandolier users + } + + // Generate a random number between 0 and 1 + float randomValue = rand() / (float)RAND_MAX; + + bool isRushing = randomValue <= probabilityThreshold; + + // Decide based on the probability threshold + if (pers_debug_mode) { + gi.dprintf("%s: %s's probabilityThreshold = %f, randomValue = %f, isRushing = %s\n", + __func__, + bot->client->pers.netname, + probabilityThreshold, + randomValue, + isRushing ? "YES" : "NO"); + } + return isRushing; } \ No newline at end of file diff --git a/src/action/g_local.h b/src/action/g_local.h index 114a4adb7..058e345d0 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1476,6 +1476,7 @@ qboolean infront( edict_t *self, edict_t *other ); #endif void disablecvar(cvar_t *cvar, char *msg); int eztimer(int seconds); +float sigmoid(float x); // Re-enabled for bots float *tv (float x, float y, float z); diff --git a/src/action/g_utils.c b/src/action/g_utils.c index 4c6fd7582..44553061a 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -762,4 +762,8 @@ based on variable FPS (HZ) */ int eztimer(int seconds){ return (level.framenum + seconds * HZ); +} + +float sigmoid(float x) { + return 1 / (1 + exp(-x)); } \ No newline at end of file diff --git a/src/action/p_client.c b/src/action/p_client.c index bafbe9504..9c3d50700 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3045,7 +3045,16 @@ void PutClientInServer(edict_t * ent) if (ent->bot.bot_type == BOT_TYPE_BOTLIB) // BOTLIB { BOTLIB_Init(ent); // Initialize all the bot variables - BOTLIB_SmartWeaponSelection(ent); + if (!bot_personality->value) { + BOTLIB_SmartWeaponSelection(ent); // This is an excellent way to choose good weapon variety + } else { + // If using personalities, they have their own weapon/item preferences + BOTLIB_BotPersonalityChooseWeapon(ent); + if (item_kit_mode->value) + BOTLIB_BotPersonalityChooseItemKit(ent); + else + BOTLIB_BotPersonalityChooseItem(ent); + } } else // LTK bots { From cb4eeb7141ee049075984c11e9b14b157006111f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 13:52:53 +0300 Subject: [PATCH 450/974] Don't use big explosion model. Remaster doesn't have it. Draw regular model at 2x scale, which is identical. --- src/client/tent.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/tent.c b/src/client/tent.c index 9639a48be..4249b176b 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -50,7 +50,6 @@ qhandle_t cl_mod_dmspot; qhandle_t cl_mod_lightning; qhandle_t cl_mod_heatbeam; -qhandle_t cl_mod_explo4_big; qhandle_t cl_mod_muzzles[MFLASH_TOTAL]; @@ -308,7 +307,6 @@ void CL_RegisterTEntModels(void) cl_mod_lightning = R_RegisterModel("models/proj/lightning/tris.md2"); cl_mod_heatbeam = R_RegisterModel("models/proj/beam/tris.md2"); - cl_mod_explo4_big = R_RegisterModel("models/objects/r_explode2/tris.md2"); for (int i = 0; i < MFLASH_TOTAL; i++) cl_mod_muzzles[i] = R_RegisterModel(va("models/weapons/%s/flash/tris.md2", muzzlenames[i])); @@ -1420,7 +1418,8 @@ void CL_ParseTEnt(void) case TE_EXPLOSION1_BIG: ex = CL_PlainExplosion(); - ex->ent.model = cl_mod_explo4_big; + ex->ent.model = cl_mod_explo4; + ex->ent.scale = 2.0f; S_StartSound(te.pos1, 0, 0, cl_sfx_rockexp, 1, ATTN_NORM, 0); break; From db1f4e84b749087c0b197bdd4bc5244733622157 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 15:18:29 +0300 Subject: [PATCH 451/974] Assert surface has at least 3 edges. --- src/refresh/surf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 5e8c74c53..c294b3839 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -875,7 +875,7 @@ static void upload_world_surfaces(void) if (surf->drawflags & (SURF_SKY | SURF_NODRAW)) continue; - Q_assert(surf->numsurfedges <= TESS_MAX_VERTICES); + Q_assert(surf->numsurfedges >= 3 && surf->numsurfedges <= TESS_MAX_VERTICES); if (gl_static.world.vertices) { vbo = gl_static.world.vertices + currvert * VERTEX_SIZE; From 62b5ab7a01104d7c187fbe59a9cfc59f45f8dbd8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 12:17:17 -0400 Subject: [PATCH 452/974] Added ItemKit selection for bots --- src/action/acesrc/acebot.h | 1 + src/action/acesrc/acebot_ai.c | 21 +++++++++++++++++++++ src/action/botlib/botlib_personality.c | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 2912bc3e4..1416e9147 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -295,6 +295,7 @@ qboolean ACEAI_ChooseWeapon(edict_t *self); void ACEAI_Cmd_Choose( edict_t *ent, char *s ); void ACEAI_Cmd_Choose_Weapon_Num( edict_t *ent, int num ); void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ); +void ACEAI_Cmd_Choose_ItemKit_Num( edict_t *ent, int num ); // acebot_cmds.c protos qboolean ACECM_Commands(edict_t *ent); diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 7723d677c..cb8642b43 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -2046,3 +2046,24 @@ void ACEAI_Cmd_Choose_Item_Num( edict_t *ent, int num ) if( num ) ent->client->pers.chosenItem = FindItemByNum(num); } + +void ACEAI_Cmd_Choose_ItemKit_Num( edict_t *ent, int num ) +{ + // Item Kits ignore item bans, for simplicity + + if( num == KEV_NUM ) { + ent->client->pers.chosenItem = FindItemByNum(KEV_NUM); + } else if ( num == C_KIT_NUM ) { + ent->client->pers.chosenItem = FindItemByNum(BAND_NUM); + ent->client->pers.chosenItem2 = FindItemByNum(HELM_NUM); + } else if ( num == S_KIT_NUM ) { + ent->client->pers.chosenItem = FindItemByNum(SLIP_NUM); + ent->client->pers.chosenItem2 = FindItemByNum(SIL_NUM); + } else if ( num == A_KIT_NUM ) { + ent->client->pers.chosenItem = FindItemByNum(LASER_NUM); + ent->client->pers.chosenItem2 = FindItemByNum(SIL_NUM); + } else { // For safety + gi.dprintf("%s: num value here is not an item kit or kevlar vest (%i)\n", __func__, num); + ent->client->pers.chosenItem = FindItemByNum(KEV_NUM); + } +} diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 390f3f80f..10955dfef 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -952,9 +952,9 @@ void BOTLIB_BotPersonalityChooseItemKit(edict_t* bot) { // Choose item if (rand() % 2 == 0) { - ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[0]); + ACEAI_Cmd_Choose_ItemKit_Num(bot, top3_indices[0]); } else { - ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[rand() % 3]); + ACEAI_Cmd_Choose_ItemKit_Num(bot, top3_indices[rand() % 3]); } } From 75c06f65979c603154a4c9716ce7c05a0d7464ce Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 12:23:34 -0400 Subject: [PATCH 453/974] Fixed some warnings --- src/action/botlib/botlib_nodes.c | 35 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 4c5b39076..b15bddb7f 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -2880,12 +2880,13 @@ void BOTLIB_SaveNavCompressed(void) FILE* fOut; char filename[128]; int fileSize = 0; - int f, n, l; // File, nodes, links + int n, l; // File, nodes, links int version = BOT_NAV_VERSION; cvar_t* game_dir = gi.cvar("game", "action", 0); cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib #ifdef _WIN32 + int f; f = sprintf(filename, ".\\"); f += sprintf(filename + f, game_dir->string); f += sprintf(filename + f, "\\"); @@ -2997,7 +2998,7 @@ void BOTLIB_LoadNavCompressed(void) FILE* fIn; char filename[128]; int fileSize = 0; - int f, n, l; // File, nodes, links + int n, l; // File, nodes, links int version = 0; // Bot nav version unsigned bsp_checksum = 0; // Map checksum cvar_t* game_dir = gi.cvar("game", "action", 0); @@ -3006,6 +3007,7 @@ void BOTLIB_LoadNavCompressed(void) const vec3_t maxs = { 16, 16, 32 }; #ifdef _WIN32 + int f; f = sprintf(filename, ".\\"); f += sprintf(filename + f, game_dir->string); f += sprintf(filename + f, "\\"); @@ -4726,24 +4728,23 @@ void BOTLIB_Process_NMesh(edict_t* ent) vec3_t player_crouching_mins = { -16, -16, -24 }; vec3_t player_crouching_maxs = { 16, 16, 4 }; // Player when standing - vec3_t player_standing_smaller_mins = { -14, -14, -24 }; - vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; + // vec3_t player_standing_smaller_mins = { -14, -14, -24 }; + // vec3_t player_standing_smaller_maxs = { 14, 14, 32 }; - const int MIN_STEP_WIDTH = 32; // min step width - qboolean hit_step = false; // If we hit a suspected step (not 100% sure) - qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) + //const int MIN_STEP_WIDTH = 32; // min step width + //qboolean hit_step = false; // If we hit a suspected step (not 100% sure) + //qboolean hit_ledge = false; // If we hit a suspected ledge (not 100% sure) - vec3_t mid_point = { 0 }; - vec3_t ladder_bottom = { 0 }; - vec3_t ladder_top = { 0 }; + // vec3_t mid_point = { 0 }; + // vec3_t ladder_bottom = { 0 }; + // vec3_t ladder_top = { 0 }; - float tmp = 0; int f, e; float normal_0, normal_1, normal_2; - int num_tris = 0; - int vert_count = 0; // Vertex count - int total_verts = 0; + //int num_tris = 0; + //int vert_count = 0; // Vertex count + //int total_verts = 0; vec3_t pt0 = { 0 }; // Point 1 of a triangle vec3_t pt1 = { 0 }; // Point 2 of a triangle vec3_t pt2 = { 0 }; // Point 3 of a triangle @@ -4756,9 +4757,9 @@ void BOTLIB_Process_NMesh(edict_t* ent) normal_1 = nmesh.face[f].normal[1]; normal_2 = nmesh.face[f].normal[2]; - num_tris = 0; - vert_count = 0; - total_verts = nmesh.face[f].num_verts; + int num_tris = 0; + int vert_count = 0; + int total_verts = nmesh.face[f].num_verts; memset(pt0, 0, sizeof(vec3_t)); memset(pt1, 0, sizeof(vec3_t)); memset(pt2, 0, sizeof(vec3_t)); From b6e338da3fbbdb7c8970b1b6413e40d32f34bc03 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 13:21:43 -0400 Subject: [PATCH 454/974] Added some debugging, fixed a few bugs --- src/action/botlib/botlib_personality.c | 42 ++++++++++++++++++-------- src/action/botlib/botlib_spawn.c | 3 ++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 10955dfef..b52809a38 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -767,7 +767,7 @@ qboolean BOTLIB_DoIChat(edict_t* bot) { } -bool isArrayAllZeros(const float* array, int size) { +qboolean isArrayAllZeros(const float* array, int size) { for (int i = 0; i < size; i++) { if (array[i] != 0.0f) { return false; // Found a non-zero element @@ -790,8 +790,9 @@ GRENADE_NUM (9) maps to weapon_prefs[8] */ // "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3] void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { - float weapon_prefs[10]; - memcpy(weapon_prefs, bot->bot.personality.weapon_prefs, sizeof(bot->bot.personality.weapon_prefs)); + float weapon_prefs[10] = {0.0f}; // All elements will be set to 0.0f + + memcpy(weapon_prefs, bot->bot.personality.weapon_prefs, sizeof(weapon_prefs)); int chosen_weapon_index = 0; // Initialize with the first index float highest_pref = weapon_prefs[0]; // Initialize with the first weapon's preference @@ -802,9 +803,10 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { qboolean all_zeros = isArrayAllZeros(weapon_prefs, sizeof(weapon_prefs) / sizeof(weapon_prefs[0])); // All zeroes somehow? Still let's pick a good weapon - if (all_zeros) + if (all_zeros) { BOTLIB_SmartWeaponSelection(bot); return; + } for (int i = 0; i < (WEAPON_COUNT - 1); i++) { // Adjust loop to exclude grenades float current_pref = weapon_prefs[i]; @@ -825,16 +827,23 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { // Will choose between always picking the top weapon, from choosing from the top 3 int randomIndex = rand() % 2; + int chosenWeapon = 0; // Case 1: Always pick the top weapon if (randomIndex == 0) { - ACEAI_Cmd_Choose_Weapon_Num(bot, top3_indices[0]); + chosenWeapon = top3_indices[0]; + ACEAI_Cmd_Choose_Weapon_Num(bot, chosenWeapon); + if (pers_debug_mode) + gi.dprintf("%s: %s chose %i weapon\n", __func__, bot->client->pers.netname, chosenWeapon); return; } // Case 2: Randomly choose from the top 3 else { int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 - ACEAI_Cmd_Choose_Weapon_Num(bot, top3_indices[randomChoice]); + chosenWeapon = top3_indices[randomChoice]; + ACEAI_Cmd_Choose_Weapon_Num(bot, chosenWeapon); + if (pers_debug_mode) + gi.dprintf("%s: %s chose %i weapon\n", __func__, bot->client->pers.netname, chosenWeapon); return; } @@ -865,14 +874,12 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) qboolean all_zeros = isArrayAllZeros(item_prefs, sizeof(item_prefs) / sizeof(item_prefs[0])); - // Will choose between always picking the top item, from choosing from the top 3 - int randomIndex = rand() % 2; - // If this is all zeroes, we're picking a random if we haven't selected an item yet - if (all_zeros && bot->client->selected_item < 1) + if (all_zeros && bot->client->selected_item < 1) { chosen_item_index = rand() % 6; // Randomly choose one of the 6 items ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index); return; + } for (int i = 0; i < ITEM_COUNT; i++) { // Adjust loop for 6 items float current_pref = item_prefs[i]; @@ -890,15 +897,26 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) } } } + // Will choose between always picking the top item, from choosing from the top 3 + int randomIndex = rand() % 2; + int chosenItem = 0; // Case 1: Always pick the top item if (randomIndex == 0) { - ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[0]); + chosenItem = top3_indices[0]; + ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); + if (pers_debug_mode) + gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); + return; } // Case 2: Randomly choose from the top 3 else { int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 - ACEAI_Cmd_Choose_Item_Num(bot, top3_indices[randomChoice]); + chosenItem = top3_indices[randomChoice]; + ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); + if (pers_debug_mode) + gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); + return; } } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 9580ffe5d..5bb2350f9 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1425,6 +1425,9 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } + // Need to set this here for ClientBeginDeathmatch + bot->bot.bot_type = BOT_TYPE_BOTLIB; + ClientBeginDeathmatch(bot); BOTLIB_PutClientInServer(bot, true, team); From eec03165b3ed5a456f7aeb8e91f22f8e0cd7c1b6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 15:29:48 +0300 Subject: [PATCH 455/974] Const-ify more stuff in refresh code. --- src/refresh/draw.c | 8 ++++---- src/refresh/main.c | 6 +++--- src/refresh/sky.c | 14 +++++++------- src/refresh/surf.c | 18 +++++++++--------- src/refresh/tess.c | 6 +++--- src/refresh/texture.c | 3 +-- src/refresh/world.c | 24 +++++++++++------------- 7 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 23d060643..7fdd352cc 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -310,7 +310,7 @@ void R_SetScale(float scale) void R_DrawStretchPic(int x, int y, int w, int h, qhandle_t pic) { - image_t *image = IMG_ForHandle(pic); + const image_t *image = IMG_ForHandle(pic); GL_StretchPic(x, y, w, h, image->sl, image->tl, image->sh, image->th, draw.colors[0].u32, image); @@ -318,7 +318,7 @@ void R_DrawStretchPic(int x, int y, int w, int h, qhandle_t pic) void R_DrawKeepAspectPic(int x, int y, int w, int h, qhandle_t pic) { - image_t *image = IMG_ForHandle(pic); + const image_t *image = IMG_ForHandle(pic); if (image->flags & IF_SCRAP) { R_DrawStretchPic(x, y, w, h, pic); @@ -337,7 +337,7 @@ void R_DrawKeepAspectPic(int x, int y, int w, int h, qhandle_t pic) void R_DrawPic(int x, int y, qhandle_t pic) { - image_t *image = IMG_ForHandle(pic); + const image_t *image = IMG_ForHandle(pic); GL_StretchPic(x, y, image->width, image->height, image->sl, image->tl, image->sh, image->th, draw.colors[0].u32, image); @@ -416,7 +416,7 @@ void R_DrawChar(int x, int y, int flags, int c, qhandle_t font) int R_DrawString(int x, int y, int flags, size_t maxlen, const char *s, qhandle_t font) { - image_t *image = IMG_ForHandle(font); + const image_t *image = IMG_ForHandle(font); while (maxlen-- && *s) { byte c = *s++; diff --git a/src/refresh/main.c b/src/refresh/main.c index de014769e..3bac541c0 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -276,7 +276,7 @@ void GL_MultMatrix(GLfloat *restrict p, const GLfloat *restrict a, const GLfloat void GL_SetEntityAxis(void) { - entity_t *e = glr.ent; + const entity_t *e = glr.ent; glr.entrotated = false; glr.entscale = 1; @@ -419,7 +419,7 @@ static void make_flare_quad(const entity_t *e, float scale, vec3_t points[4]) static void GL_OccludeFlares(void) { vec3_t points[4]; - entity_t *e; + const entity_t *e; glquery_t *q; int i; @@ -543,7 +543,7 @@ static void GL_DrawEntities(int musthave, int canthave) // inline BSP model if (ent->model & BIT(31)) { - bsp_t *bsp = gl_static.world.cache; + const bsp_t *bsp = gl_static.world.cache; int index = ~ent->model; if (glr.fd.rdflags & RDF_NOWORLDMODEL) { diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 19f32ef94..e80fc2d7b 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -234,13 +234,13 @@ R_AddSkySurface */ void R_AddSkySurface(const mface_t *fa) { - int i; - vec3_t verts[MAX_CLIP_VERTS]; - vec3_t temp; - msurfedge_t *surfedge; - mvertex_t *vert; - medge_t *edge; - bsp_t *bsp = gl_static.world.cache; + int i; + vec3_t verts[MAX_CLIP_VERTS]; + vec3_t temp; + const msurfedge_t *surfedge; + const mvertex_t *vert; + const medge_t *edge; + const bsp_t *bsp = gl_static.world.cache; if (fa->numsurfedges > MAX_CLIP_VERTS) { Com_DPrintf("%s: too many verts\n", __func__); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index c294b3839..07477e948 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -400,7 +400,7 @@ static bool no_lightmaps(void) static void LM_BeginBuilding(void) { - bsp_t *bsp = gl_static.world.cache; + const bsp_t *bsp = gl_static.world.cache; int size_shift, bits; // start up with fullbright styles @@ -496,7 +496,7 @@ static void LM_BuildSurface(mface_t *surf) static void LM_RebuildSurfaces(void) { - bsp_t *bsp = gl_static.world.cache; + const bsp_t *bsp = gl_static.world.cache; mface_t *surf; lightmap_t *m; int i; @@ -554,11 +554,11 @@ static uint32_t color_for_surface(const mface_t *surf) static void build_surface_poly(mface_t *surf, vec_t *vbo) { - bsp_t *bsp = gl_static.world.cache; - msurfedge_t *src_surfedge; - mvertex_t *src_vert; - medge_t *src_edge; - mtexinfo_t *texinfo = surf->texinfo; + const bsp_t *bsp = gl_static.world.cache; + const msurfedge_t *src_surfedge; + const mvertex_t *src_vert; + const medge_t *src_edge; + const mtexinfo_t *texinfo = surf->texinfo; vec2_t scale, tc, mins, maxs; int i, bmins[2], bmaxs[2]; uint32_t color; @@ -725,8 +725,8 @@ static void sample_surface_verts(mface_t *surf, vec_t *vbo) // validates and processes surface lightmap static void build_surface_light(mface_t *surf, vec_t *vbo) { + const bsp_t *bsp = gl_static.world.cache; int smax, tmax, size, ofs; - bsp_t *bsp = gl_static.world.cache; if (gl_fullbright->integer) return; @@ -854,7 +854,7 @@ static void upload_surface_vbo(int lastvert) static void upload_world_surfaces(void) { - bsp_t *bsp = gl_static.world.cache; + const bsp_t *bsp = gl_static.world.cache; vec_t *vbo; mface_t *surf; int i, currvert, lastvert; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index a5e6cdda6..a97ed269a 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -74,7 +74,7 @@ void GL_Flush2D(void) void GL_DrawParticles(void) { - particle_t *p; + const particle_t *p; int total, count; vec3_t transformed; vec_t scale, scale2, dist; @@ -242,10 +242,10 @@ static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t c void GL_DrawBeams(void) { - vec_t *start, *end; + const vec_t *start, *end; color_t color; float width; - entity_t *ent; + const entity_t *ent; int i; if (!glr.num_beams) { diff --git a/src/refresh/texture.c b/src/refresh/texture.c index e8d7c46d3..8df3b80e8 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -1023,8 +1023,7 @@ void GL_InitImages(void) gl_bilerp_chars->changed = gl_bilerp_chars_changed; gl_bilerp_pics = Cvar_Get("gl_bilerp_pics", "1", 0); gl_bilerp_pics->changed = gl_bilerp_pics_changed; - gl_texturemode = Cvar_Get("gl_texturemode", - "GL_LINEAR_MIPMAP_LINEAR", CVAR_ARCHIVE); + gl_texturemode = Cvar_Get("gl_texturemode", "GL_LINEAR_MIPMAP_LINEAR", CVAR_ARCHIVE); gl_texturemode->changed = gl_texturemode_changed; gl_texturemode->generator = gl_texturemode_g; gl_texturebits = Cvar_Get("gl_texturebits", "0", CVAR_FILES); diff --git a/src/refresh/world.c b/src/refresh/world.c index 99719bad5..1fb66c93d 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -20,15 +20,14 @@ with this program; if not, write to the Free Software Foundation, Inc., void GL_SampleLightPoint(vec3_t color) { - mface_t *surf = glr.lightpoint.surf; - int s, t, i; - byte *lightmap; - byte *b1, *b2, *b3, *b4; - float fracu, fracv; - float w1, w2, w3, w4; - vec3_t temp; - int smax, tmax, size; - lightstyle_t *style; + const mface_t *surf = glr.lightpoint.surf; + const byte *lightmap; + const byte *b1, *b2, *b3, *b4; + const lightstyle_t *style; + float fracu, fracv; + float w1, w2, w3, w4; + vec3_t temp; + int s, t, smax, tmax, size; s = glr.lightpoint.s; t = glr.lightpoint.t; @@ -50,7 +49,7 @@ void GL_SampleLightPoint(vec3_t color) // add all the lightmaps with bilinear filtering lightmap = surf->lightmap; - for (i = 0; i < surf->numstyles; i++) { + for (int i = 0; i < surf->numstyles; i++) { b1 = &lightmap[3 * ((t + 0) * smax + (s + 0))]; b2 = &lightmap[3 * ((t + 0) * smax + (s + 1))]; b3 = &lightmap[3 * ((t + 1) * smax + (s + 1))]; @@ -98,7 +97,7 @@ static bool GL_LightGridPoint(const lightgrid_t *grid, const vec3_t start, vec3_ VectorClear(samples[i]); for (j = 0; j < grid->numstyles && s->style != 255; j++, s++) { - lightstyle_t *style = LIGHT_STYLE(s->style); + const lightstyle_t *style = LIGHT_STYLE(s->style); VectorMA(samples[i], style->white, s->rgb, samples[i]); } @@ -152,7 +151,7 @@ static bool GL_LightGridPoint(const lightgrid_t *grid, const vec3_t start, vec3_ static bool GL_LightPoint_(const vec3_t start, vec3_t color) { - const bsp_t *bsp; + const bsp_t *bsp = gl_static.world.cache; int i, index; lightpoint_t pt; vec3_t end, mins, maxs; @@ -160,7 +159,6 @@ static bool GL_LightPoint_(const vec3_t start, vec3_t color) const mmodel_t *model; const vec_t *angles; - bsp = gl_static.world.cache; if (!bsp || !bsp->lightmap) return false; From 567ca5c8ce18e2072a594dfa929ad15053037e26 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 16:14:00 +0300 Subject: [PATCH 456/974] Cull BSP faces after testing for sky/nodraw. --- src/refresh/world.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/refresh/world.c b/src/refresh/world.c index 1fb66c93d..7bd214757 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -415,10 +415,6 @@ static void GL_MarkLeaves(void) #define BACKFACE_EPSILON 0.01f -#define BSP_CullFace(face, dot) \ - (((dot) < -BACKFACE_EPSILON && !((face)->drawflags & DSURF_PLANEBACK)) || \ - ((dot) > BACKFACE_EPSILON && ((face)->drawflags & DSURF_PLANEBACK))) - void GL_DrawBspModel(mmodel_t *model) { mface_t *face, *last; @@ -472,14 +468,14 @@ void GL_DrawBspModel(mmodel_t *model) // draw visible faces last = model->firstface + model->numfaces; for (face = model->firstface; face < last; face++) { - dot = PlaneDiffFast(transformed, face->plane); - if (BSP_CullFace(face, dot)) { - c.facesCulled++; + // sky faces don't have their polygon built + if (face->drawflags & (SURF_SKY | SURF_NODRAW)) { continue; } - // sky faces don't have their polygon built - if (face->drawflags & (SURF_SKY | SURF_NODRAW)) { + dot = PlaneDiffFast(transformed, face->plane); + if ((face->drawflags & DSURF_PLANEBACK) ? (dot > BACKFACE_EPSILON) : (dot < -BACKFACE_EPSILON)) { + c.facesCulled++; continue; } From 56e41800ad34b0a5bf853ab2f551093789c65f1e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 21:45:49 +0300 Subject: [PATCH 457/974] Reduce code duplication. --- src/refresh/images.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index e77cda49d..75f1b251d 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -516,11 +516,12 @@ IMG_LOAD(TGA) byte *pixels, *start; uint32_t *row_pointers[MAX_TEXTURE_SIZE]; uint32_t colormap[256]; + const uint32_t *pal; sizebuf_t s; unsigned id_length, colormap_type, image_type, colormap_start, colormap_length, colormap_size, w, h, pixel_size, attributes; bool rle; - int i, j, ret, stride, interleave; + int i, j, ret, bpp, stride, interleave; if (rawlen < TARGA_HEADER_SIZE) return Q_ERR_FILE_TOO_SMALL; @@ -651,12 +652,13 @@ IMG_LOAD(TGA) j = (j + 1) & (interleave - 1); } + bpp = (pixel_size + 1) / 8; + pal = image_type == TGA_Colormap ? colormap : NULL; + if (rle) - ret = tga_decode_rle(&s, row_pointers, w, h, (pixel_size + 1) / 8, - image_type == TGA_Colormap ? colormap : NULL); + ret = tga_decode_rle(&s, row_pointers, w, h, bpp, pal); else - ret = tga_decode_raw(&s, row_pointers, w, h, (pixel_size + 1) / 8, - image_type == TGA_Colormap ? colormap : NULL); + ret = tga_decode_raw(&s, row_pointers, w, h, bpp, pal); if (ret < 0) { IMG_FreePixels(pixels); return ret; From 666bbee73ce4e267395ddf940f45c4a57ef1f1bc Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 16:41:32 -0400 Subject: [PATCH 458/974] bot_personality 2 will add both types of bots --- src/action/acesrc/acebot_spawn.c | 2 +- src/action/botlib/botlib_personality.c | 6 ++-- src/action/botlib/botlib_spawn.c | 38 +++++++++++++++++--------- src/action/g_local.h | 13 +++++++++ src/action/g_main.c | 2 +- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index f6a58c61e..57311e70c 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -709,7 +709,7 @@ void ACESP_RemoveBot(char *name) // ACEIT_PlayerRemoved (bot); // gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); - if (bot_personality->value) + if (bot_personality->value && bot->bot.personality.isActive) BOTLIB_FreeBotPersonality(bot); if( ! remove_all ) break; diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index b52809a38..7059e7ce4 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -149,6 +149,7 @@ temp_bot_mapping_t* create_new_bot(char* name) { qboolean BotRageQuit(edict_t* self, qboolean frag_or_death) { // Don't do anything if the map_prefs are 0 or positive + // This includes all bots that do not have personalities if (self->bot.personality.map_prefs >= 0) return false; @@ -742,12 +743,13 @@ void BOTLIB_FreeBotPersonality(edict_t* bot) // If this is a bot with personality, free it up int bot_pId = bot->bot.personality.pId; for (int i = 0; i < MAX_BOTS; i++) { - if (bot_mappings[i].personality.pId == bot_pId) { + if (bot_mappings[i].personality.pId == bot_pId && bot_mappings[i].personality.isActive) { bot_mappings[i].personality.isActive = false; + game.used_bot_personalities--; + gi.dprintf("Freed up %s\n", bot->client->pers.netname); break; // Exit the loop once the bot is found and deactivated } } - game.used_bot_personalities--; } qboolean BOTLIB_DoIChat(edict_t* bot) { diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 5bb2350f9..319da8af2 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1408,19 +1408,31 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for if (pers_debug_mode) gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); - if (bot_personality->value) { - if (bot_personality_index > game.used_bot_personalities) { - if (!BOTLIB_SetPersonality(bot, team, force_gender)) { - if(pers_debug_mode) + + // + if (bot_personality->value == 2) { + if (rand() % 2) { // Randomly choose between 0 and 1 + if (!BOTLIB_SetPersonality(bot, team, force_gender)) { + if (pers_debug_mode) + gi.dprintf("Failed to load bot personality, using default userinfo.\n"); + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else { + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else if (bot_personality->value == 1) { + if (bot_personality_index > game.used_bot_personalities) { + if (!BOTLIB_SetPersonality(bot, team, force_gender)) { + if (pers_debug_mode) gi.dprintf("Failed to load bot personality, using default userinfo.\n"); - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); - } - } else { - if(pers_debug_mode) + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else { + if (pers_debug_mode) gi.dprintf("Ran out of bot personalities, loading random bots now.\n"); - BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); - } - } else { // Use random data + BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); + } + } else { // Use random data gi.dprintf("%s: trying to load random bots\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } @@ -1485,7 +1497,7 @@ void BOTLIB_RemoveBot(char* name) ClientDisconnect(bot); game.bot_count--; - if (bot_personality->value) + if (bot_personality->value && bot->bot.personality.isActive) BOTLIB_FreeBotPersonality(bot); if (!remove_all) @@ -1545,7 +1557,7 @@ void BOTLIB_RemoveBot(char* name) freed = true; ClientDisconnect(bot); game.bot_count--; - if (bot_personality->value) + if (bot_personality->value && bot->bot.personality.isActive) BOTLIB_FreeBotPersonality(bot); //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); break; diff --git a/src/action/g_local.h b/src/action/g_local.h index 058e345d0..df997d631 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -786,6 +786,16 @@ typedef struct } game_locals_t; +// Map features, utilized by / requires bot_personality to be enabled +typedef struct map_features_s +{ + float volume; + float openness; + float water_amt; + float lava_amt; + float slime_amt; + +} map_features_t; // // this structure is cleared as each map is entered @@ -877,6 +887,9 @@ typedef struct // Point of interest vec3_t poi_origin; vec3_t poi_angle; + + // Map features + map_features_t map_features; } level_locals_t; diff --git a/src/action/g_main.c b/src/action/g_main.c index 764f7c3b1..a7907588b 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -494,7 +494,7 @@ cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 m cvar_t* bot_randskill; // When random bot join a game, they pick a random skill [min: 1, max: 10]. Using 0 will turn this off. cvar_t* bot_randname; // Allow bots to pick a random name cvar_t* bot_chat; // Enable generic bot chat -cvar_t* bot_personality; // Enable bot personality functionality +cvar_t* bot_personality; // Enable bot personality functionality [0 disable, 1 enable, 2 mixed with random bots] cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e From 7da5574a8c5b4c0554e788f603e7da48944309d1 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 19:42:46 -0400 Subject: [PATCH 459/974] Removed old code, adjusted item kit selections --- src/action/botlib/botlib_personality.c | 292 ++----------------------- src/server/main.c | 13 +- src/server/server.h | 4 + 3 files changed, 31 insertions(+), 278 deletions(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 7059e7ce4..be4800152 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -4,66 +4,10 @@ #include "/opt/homebrew/include/jansson.h" // Count of bot personalities loaded -qboolean pers_debug_mode = true; +qboolean pers_debug_mode = false; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create -// #define WEAPON_COUNT 9 -// #define ITEM_COUNT 6 - -// typedef struct bot_personality_s -// { -// // These are +1 because we're ignoring the first index [0] -// // So that MK23_NUM (1) stays at 1 here as well -// float weapon_prefs[WEAPON_COUNT + 1]; //-1 = Will never choose, 1 = Will always choose -// float item_prefs[ITEM_COUNT +1]; //-1 = Will never choose, 1 = Will always choose - -// float map_prefs; //-1 = Hate, 0 = Neutral, 1 = Love -// float combat_demeanor; //-1 = Timid | 1 = Aggressive -// float chat_demeanor; //-1 = Quiet | 1 = Chatty -// int leave_percent; // Percentage calculated that the bot will leave the map. Recalculated/increases every time the bot dies. -// float shooting_skill; // -1 = Poor Skill | 1 = Excellent Skill (higher value = harder bot) -// float movement_skill; // -1 = Poor Skill | 1 = Excellent Skill (higher value = harder bot - -// // Will use the edict's values here, these are only here for testing -// char* skin; -// char* name; -// } bot_personality_t; - -// #define MK23_NUM 1 -// #define MP5_NUM 2 -// #define M4_NUM 3 -// #define M3_NUM 4 -// #define HC_NUM 5 -// #define SNIPER_NUM 6 -// #define DUAL_NUM 7 -// #define KNIFE_NUM 8 -// #define GRENADE_NUM 9 - -// #define SIL_NUM 10 -// #define SLIP_NUM 11 -// #define BAND_NUM 12 -// #define KEV_NUM 13 -// #define LASER_NUM 14 -// #define HELM_NUM 15 - -// #define MK23_NAME "MK23 Pistol" -// #define MP5_NAME "MP5/10 Submachinegun" -// #define M4_NAME "M4 Assault Rifle" -// #define M3_NAME "M3 Super 90 Assault Shotgun" -// #define HC_NAME "Handcannon" -// #define SNIPER_NAME "Sniper Rifle" -// #define DUAL_NAME "Dual MK23 Pistols" -// #define KNIFE_NAME "Combat Knife" -// #define GRENADE_NAME "M26 Fragmentation Grenade" - -// #define SIL_NAME "Silencer" -// #define SLIP_NAME "Stealth Slippers" -// #define BAND_NAME "Bandolier" -// #define KEV_NAME "Kevlar Vest" -// #define HELM_NAME "Kevlar Helmet" -// #define LASER_NAME "Lasersight" - /////* // This file focuses on loading bot personality traits /////* @@ -313,187 +257,6 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) return bot_mappings; } - - - - - - - - - - - - - // Extract and assign values from the JSON object - // Define an array of weapon names corresponding to their preferences - // const char* weaponNames[] = { - // MK23_NAME, MP5_NAME, M4_NAME, M3_NAME, HC_NAME, - // SNIPER_NAME, DUAL_NAME, KNIFE_NAME, GRENADE_NAME - // }; - - // json_t* weapon_prefs = json_object_get(root, "weapon_prefs"); - // for (size_t i = 0; i < json_array_size(weapon_prefs); i++) { - // json_t* wpref_value = json_array_get(weapon_prefs, i); - // // Ensure the index is within the bounds of weaponNames array - // const char* weaponName = (i < sizeof(weaponNames)/sizeof(weaponNames[0])) ? weaponNames[i] : "Unknown Weapon"; - // float value = validate_pref_numeric(wpref_value, weaponName); // Updated function call with weapon name - - // // Proceed with assignment only if value is not 0.0 (assuming 0.0 indicates invalid or not set) - // if (value != 0.0f) { - // temp_bot->personality.weapon_prefs[i] = value; // Direct assignment using index, assuming array is properly sized and ordered - // } - // } - - // // Assuming you have an array of preference names corresponding to each index - // // Define an array of item names corresponding to their preferences - // const char* itemNames[] = {SIL_NAME, SLIP_NAME, BAND_NAME, KEV_NAME, HELM_NAME, LASER_NAME}; - - // json_t* item_prefs = json_object_get(root, "item_prefs"); - // for (size_t i = 0; i < json_array_size(item_prefs); i++) { - // json_t* ipref_value = json_array_get(item_prefs, i); - // // Ensure the index is within the bounds of itemNames array - // const char* itemName = (i < sizeof(itemNames)/sizeof(itemNames[0])) ? itemNames[i] : "Unknown Item"; - // float value = validate_pref_numeric(ipref_value, itemName); // Pass the item name for detailed warnings - - // // Assuming 0.0f indicates an invalid or out-of-bounds value, skip assignment in such cases - // if (value != 0.0f) { - // temp_bot->personality.item_prefs[i] = value; - // } - // } - - // // Extract single float values, if they exist, else default to 0 - // json_t *checkval; - - // char* current_map_name = level.mapname; - // //personality->map_prefs = validate_pref_numeric(json_object_get(root, "map_prefs"), "map_prefs"); - // update_map_pref(root, current_map_name, temp_bot); - - // temp_bot->personality.combat_demeanor = validate_pref_numeric(json_object_get(root, "combat_demeanor"), "combat_demeanor"); - - // temp_bot->personality.chat_demeanor = validate_pref_numeric(json_object_get(root, "chat_demeanor"), "chat_demeanor"); - - // // Extract integer value, every fresh personality load sets this to 0 - // temp_bot->personality.leave_percent = 0; - - // // Set the skin, if provided - // char* defaultSkin = "male/grunt"; - - // checkval = json_object_get(root, "skin"); - // char* validatedSkin = validate_pref_string(checkval, 1); - // if (validatedSkin != NULL) { - // // If skin value is valid, use it - // temp_bot->personality.skin_pref = validatedSkin; - // } else { - // // If skin value is invalid, use defaultSkin and print a warning - // temp_bot->personality.skin_pref = strdup(defaultSkin); // Use strdup to copy defaultSkin - // } - - // // Set the name, if provided - // char* defaultName = "AqtionBot"; - - // checkval = json_object_get(root, "bots"); - // char* validatedName = validate_pref_string(checkval, 0); - // if (validatedName != NULL) { - // // If name value is valid, use it - // temp_bot->name = validatedName; - // } else { - // // If name value is invalid, use defaultName and print a warning - // temp_bot->name = strdup(defaultName); // Use strdup to copy defaultName - // } - - - // Clean up -// json_decref(root); - -// return 0; -// } - -// int main() { -// // Example usage of load_bot_personality -// bot_personality_t* personality = load_bot_personality("bot.json"); -// if (personality != NULL) { -// printf("\n"); -// printf("Weapon Preferences:"); -// for (int i = 1; i < WEAPON_COUNT; i++) { // Assuming WEAPON_PREFS_SIZE is defined -// printf(" %f", personality->weapon_prefs[i]); -// } -// printf("\nItem Preferences:"); -// for (int i = 1; i < ITEM_COUNT; i++) { // Assuming ITEM_PREFS_SIZE is defined -// printf(" %f", personality->item_prefs[i]); -// } -// printf("\n"); -// // Print Map Preferences -// printf("Map Preferences: %f\n", personality->map_prefs); -// // Print Chat Demeanor -// printf("Chat Demeanor: %f\n", personality->chat_demeanor); - -// // Print Combat Demeanor -// printf("Combat Demeanor: %f\n", personality->combat_demeanor); - -// // Print Skin -// printf("Skin: %s\n", personality->skin); - -// printf("\n"); -// // -// if (!warningCount) -// printf("Bot personality loaded successfully.\n"); -// else -// printf("Loaded with %i warnings\n", warningCount); - -// // Remember to free the allocated memory for personality -// free(personality); -// } else { -// printf("Failed to load bot personality.\n"); -// } -// return 0; -// } - -// Function to load bot names from file -// int loadBotNames(const char* filename) { -// json_error_t error; -// json_t* root = json_load_file(filename, 0, &error); - -// if (!root) { -// return 0; // Failed to load file -// } - -// json_t* bots = json_object_get(root, "bots"); -// if (!bots) { -// json_decref(root); -// return 0; // No "bots" object -// } - -// size_t fileBotCount = json_array_size(bots); -// // Adjust the number of bots to load based on game.maxclients -// loadedBotCount = (fileBotCount < game.maxclients) ? fileBotCount : game.maxclients; -// botNames = malloc(loadedBotCount * sizeof(char*)); // Allocate array for bot names - -// for (size_t i = 0; i < loadedBotCount; i++) { -// json_t* bot = json_array_get(bots, i); -// const char* name = json_string_value(json_object_get(bot, "name")); -// botNames[i] = strdup(name); // Copy name into array -// } - -// json_decref(root); -// return 1; // Success -// } -// // Function to get a random bot name -// char* getRandomBotName() { -// if (loadedBotCount == 0) return NULL; // No bots loaded -// srand(time(NULL)); // Seed the random number generator -// size_t index = rand() % loadedBotCount; -// return botNames[index]; // Return random bot name -// } - -// void freeBotNames() { -// for (size_t i = 0; i < loadedBotCount; i++) { -// free(botNames[i]); // Free each name -// } -// free(botNames); // Free the array -// botNames = NULL; -// loadedBotCount = 0; -// } // Main function that will load other personality methods void BOTLIB_PersonalityFile(void) { @@ -528,37 +291,6 @@ void BOTLIB_PersonalityFile(void) { BOTLIB_LoadPersonalities(filename); } -// void copy_temp_to_real(edict_t* bot, temp_bot_mapping_t *roster, bot_personality_t *personality) { -// // Copying array fields -// memcpy(personality->weapon_prefs, roster->personality.weapon_prefs, sizeof(personality->weapon_prefs)); -// memcpy(personality->item_prefs, roster->personality.item_prefs, sizeof(personality->item_prefs)); - -// // Copying simple fields -// personality->map_prefs = roster->personality.map_prefs; -// personality->combat_demeanor = roster->personality.combat_demeanor; -// personality->chat_demeanor = roster->personality.chat_demeanor; -// personality->leave_percent = roster->personality.leave_percent; - -// // Copying string fields, assuming they should be char* in bot_personality_t -// if (roster->name != NULL) { -// personality->name_pref = malloc(strlen(roster->name) + 1); -// Info_SetValueForKey(userinfo, "name", roster->name); -// } else { -// // Safe default -// personality->name_pref = "AqtionBot"; -// } - -// if (roster->personality.skin_pref != NULL) { -// personality->skin_pref = malloc(strlen(roster->personality.skin_pref) + 1); -// strcpy(personality->skin_pref, roster->personality.skin_pref); -// } else { -// // Safe default -// personality->skin_pref = "male/grunt"; -// } - - -// } - // Return skin if false, return model if true char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { char *skinpath; @@ -746,7 +478,8 @@ void BOTLIB_FreeBotPersonality(edict_t* bot) if (bot_mappings[i].personality.pId == bot_pId && bot_mappings[i].personality.isActive) { bot_mappings[i].personality.isActive = false; game.used_bot_personalities--; - gi.dprintf("Freed up %s\n", bot->client->pers.netname); + if(pers_debug_mode) + gi.dprintf("%s: Freed up %s\n", __func__, bot->client->pers.netname); break; // Exit the loop once the bot is found and deactivated } } @@ -804,9 +537,11 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { qboolean all_zeros = isArrayAllZeros(weapon_prefs, sizeof(weapon_prefs) / sizeof(weapon_prefs[0])); - // All zeroes somehow? Still let's pick a good weapon + // All zeroes somehow? Still, let's pick a good weapon if (all_zeros) { BOTLIB_SmartWeaponSelection(bot); + if(pers_debug_mode) + gi.dprintf("%s: chose BOTLIB_SmartWeaponSelection() because weapon_prefs were all zeroes"); return; } @@ -880,6 +615,8 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) if (all_zeros && bot->client->selected_item < 1) { chosen_item_index = rand() % 6; // Randomly choose one of the 6 items ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index); + if(pers_debug_mode) + gi.dprintf("%s: chose random item because item_prefs were all zeroes"); return; } @@ -938,16 +675,17 @@ void BOTLIB_BotPersonalityChooseItemKit(edict_t* bot) { int weaponNum = bot->client->weapon ? bot->client->weapon->typeNum : 0; // Adjust preferences based on weapon - if (weaponNum == SNIPER_NUM || weaponNum == M4_NUM) { - item_kit_prefs[0] += 0.25; - } - if (weaponNum == MP5_NUM || weaponNum == M4_NUM) { + + // Everyone likes Kevlar + item_kit_prefs[0] += 0.50; + + if (weaponNum == MP5_NUM || weaponNum == M4_NUM || weaponNum == M3_NUM) { item_kit_prefs[1] += 0.25; } - if (weaponNum == HC_NUM || weaponNum == M3_NUM) { + if (weaponNum == HC_NUM || weaponNum == M3_NUM || weaponNum == KNIFE_NUM || weaponNum == SNIPER_NUM) { item_kit_prefs[2] += 0.25; } - if (weaponNum == MP5_NUM || weaponNum == DUAL_NUM) { + if (weaponNum == MP5_NUM || weaponNum == DUAL_NUM || weaponNum == SNIPER_NUM) { item_kit_prefs[3] += 0.25; } diff --git a/src/server/main.c b/src/server/main.c index 3f30909eb..57c865d53 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -112,6 +112,10 @@ cvar_t *g_view_predict; cvar_t *g_view_low; cvar_t *g_view_high; +#ifndef NO_BOTS +cvar_t *sv_bot_ping; // Report bot ping as fake ping values or simply as BOT (0 = ping, 1 = BOT) +#endif + static bool sv_registered; //============================================================================ @@ -465,7 +469,10 @@ static size_t SV_StatusString(char *status) { if (bot_clients[i].in_use) { - len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] + if(!sv_bot_ping->integer) + len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] + else + len = Q_snprintf(entry, sizeof(entry), "%i \"%s\" \"%s\"\n", bot_clients[i].score, "BOT", bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] if (len >= sizeof(entry)) continue; @@ -2428,6 +2435,10 @@ void SV_Init(void) sv.frametime = Com_ComputeFrametime(sv.framerate); #endif +#ifndef NO_BOTS + sv_bot_ping = Cvar_Get("sv_bot_ping", "0", 0); +#endif + // set up default pmove parameters PmoveInit(&sv_pmp); diff --git a/src/server/server.h b/src/server/server.h index 456119a3e..8097b7bfc 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -586,6 +586,10 @@ extern cvar_t *sv_ghostime; extern client_t *sv_client; extern edict_t *sv_player; +#ifndef NO_BOTS +extern cvar_t *sv_bot_ping; +#endif + //=========================================================== // From 781d3184797e0b9973f3f58081d89fff0b48dac4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 20:31:55 -0400 Subject: [PATCH 460/974] Initial espionage ideas, enable bot teamplay without humans --- src/action/a_vote.c | 7 +- src/action/acesrc/acebot.h | 2 +- src/action/botlib/botlib_esp.c | 924 ++++++++++++++++++++++++++++ src/action/botlib/botlib_movement.c | 8 +- src/action/g_main.c | 1 + src/action/g_save.c | 1 + 6 files changed, 937 insertions(+), 6 deletions(-) create mode 100644 src/action/botlib/botlib_esp.c diff --git a/src/action/a_vote.c b/src/action/a_vote.c index 8dd45b424..e16024d07 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -114,8 +114,11 @@ int _numclients (void) if (!other->inuse || !other->client || !other->client->pers.connected || other->client->pers.mvdspec) continue; #ifndef NO_BOTS - if (other->is_bot) - continue; + // If bot_teamplay is enabled, then do not continue/ignore bots + if(!bot_teamplay->value) { + if (other->is_bot) + continue; + } #endif count++; diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 1416e9147..4bb5573f0 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -545,7 +545,7 @@ extern cvar_t* bot_randname; extern cvar_t* bot_chat; extern cvar_t* bot_personality; extern cvar_t* bot_ragequit; - +extern cvar_t* bot_teamplay; //extern cvar_t* bot_randteamskin; diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c new file mode 100644 index 000000000..75e918041 --- /dev/null +++ b/src/action/botlib/botlib_esp.c @@ -0,0 +1,924 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +esp_status_t bot_esp_status; + +// Get the +int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) +{ + vec3_t mins = { -16, -16, -24 }; + vec3_t maxs = { 16, 16, 32 }; + vec3_t bmins = { 0,0,0 }; + vec3_t bmaxs = { 0,0,0 }; + + if (ent == NULL) + return INVALID; + + int cloest_node_num = INVALID; + float cloest_node_dist = 99999999; + + for (int j = 0; j < numnodes; j++) + { + VectorAdd(ent->s.origin, mins, bmins); // Update absolute box min/max in the world + VectorAdd(ent->s.origin, maxs, bmaxs); // Update absolute box min/max in the world + + float dist = VectorDistance(nodes[j].origin, ent->s.origin); + + // If ent is touching a node + //if (BOTLIB_BoxIntersection(bmins, bmaxs, nodes[j].absmin, nodes[j].absmax) || VectorDistance(nodes[j].origin, ent->s.origin) <= 128) + if (dist <= 128) + { + trace_t tr = gi.trace(nodes[j].origin, tv(-16, -16, -8), tv(16, 16, 8), ent->s.origin, NULL, MASK_PLAYERSOLID); + if (tr.fraction == 1.0) + { + //return nodes[j].nodenum; + + if (dist < cloest_node_dist) + { + cloest_node_dist = dist; + cloest_node_num = nodes[j].nodenum; + } + } + } + } + if (cloest_node_num != INVALID) + return cloest_node_num; + + // If not touching a box, try searching via cloest distance to a node + if (1) + { + int i; + float closest = 99999; + float dist; + vec3_t v; + trace_t tr; + float rng; + int node = INVALID; + //vec3_t maxs, mins; + + //VectorCopy(self->mins, mins); + //VectorCopy(self->maxs, maxs); + //mins[2] += 18; // Stepsize + //maxs[2] -= 16; // Duck a little.. + + rng = (float)(NODE_DENSITY * NODE_DENSITY); // square range for distance comparison (eliminate sqrt) + + for (i = 0; i < numnodes; i++) + { + if (nodes[i].inuse == false) continue; // Ignore nodes not in use + + //if (type == NODE_ALL || type == nodes[i].type) // check node type + { + // Get Height Diff + float height = fabs(nodes[i].origin[2] - ent->s.origin[2]); + if (height > 60) // Height difference high + continue; + + VectorSubtract(nodes[i].origin, ent->s.origin, v); // subtract first + + dist = v[0] * v[0] + v[1] * v[1] + v[2] * v[2]; + + if (dist < closest && dist < rng) + { + tr = gi.trace(ent->s.origin, tv(-16, -16, STEPSIZE), tv(16, 16, 32), nodes[i].origin, ent, MASK_PLAYERSOLID); //rekkie + if ((tr.fraction == 1.0) || + ((tr.fraction > 0.9) // may be blocked by the door itself! + && (Q_stricmp(tr.ent->classname, "func_door_rotating") == 0)) + ) + { + node = i; + closest = dist; + } + } + } + } + if (node != INVALID) + return nodes[node].nodenum; + } + + return INVALID; +} + +// Intercept flag carrier (good guy and bad guy) +// [OPTIONAL] distance: if bot is within distance +// Returns the node nearest to the carrier +int BOTLIB_InterceptLeader(edict_t* self, int team, float distance) +{ + if (distance > 0) + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + else + { + if (team == TEAM1) + { + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + } + if (team == TEAM2) + { + if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) + { + //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); + return bot_ctf_status.player_has_flag1->bot.current_node; + } + if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) + { + //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); + return bot_ctf_status.player_has_flag2->bot.current_node; + } + } + } + + return INVALID; +} + +float BOTLIB_DistanceToEnemyLeader(edict_t* self, int flagType) +{ + // If target is leader + edict_t* team1leader = teams[TEAM1].leader; + edict_t* team2leader = teams[TEAM2].leader; + edict_t* team3leader = teams[TEAM3].leader; + + if (use_3teams->value) { + // Array of team leaders + edict_t *teamLeaders[3] = {team1leader, team2leader, team3leader}; + float minDistance = 999999; + edict_t *targetLeader = NULL; + + // Iterate over each team leader + for (int i = 0; i < 3; i++) { + // Skip if the leader is null or if it's the same team as self + if (!teamLeaders[i] || self->client->resp.team == i + 1) { + continue; + } + // Calculate the distance to the team leader + float distance = VectorDistance(teamLeaders[i]->s.origin, self->s.origin); + if (distance < minDistance) { + minDistance = distance; + targetLeader = teamLeaders[i]; + } + } + + if (targetLeader) { + // Perform the attack or any other logic here + // attack(targetLeader); + return minDistance; + } + } else { // Teamplay + // Array of team leaders + edict_t *teamLeaders[2] = {team1leader, team2leader}; + float minDistance = 999999; + edict_t *targetLeader = NULL; + + // Iterate over each team leader + for (int i = 0; i < 2; i++) { + // Skip if the leader is null or if it's the same team as self + if (!teamLeaders[i] || self->client->resp.team == i + 1) { + continue; + } + // Calculate the distance to the team leader + float distance = VectorDistance(teamLeaders[i]->s.origin, self->s.origin); + if (distance < minDistance) { + minDistance = distance; + targetLeader = teamLeaders[i]; + } + } + + if (targetLeader) { + // Perform the attack or any other logic here + // attack(targetLeader); + return minDistance; + } + } + + return 9999999; // Could not find distance +} + +// Get the status of flags +void BOTLIB_Update_Flags_Status(void) +{ + // Check if any players are carrying a flag + // Returns INVALID if no enemy has the flag + bot_ctf_status.player_has_flag1 = NULL; + bot_ctf_status.player_has_flag2 = NULL; + for (int p = 0; p < num_players; p++) + { + if (players[p]->client->inventory[items[FLAG_T1_NUM].index]) + bot_ctf_status.player_has_flag1 = players[p]; + + if (players[p]->client->inventory[items[FLAG_T2_NUM].index]) + bot_ctf_status.player_has_flag2 = players[p]; + } + + bot_ctf_status.flag1 = NULL; + bot_ctf_status.flag2 = NULL; + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag1_is_dropped = false; + bot_ctf_status.flag2_is_dropped = false; + bot_ctf_status.flag1_is_carried = false; + bot_ctf_status.flag2_is_carried = false; + + int base = 1 + game.maxclients + BODY_QUEUE_SIZE; + edict_t* ent = g_edicts + base; + for (int i = base; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse == false) continue; + if (!ent->classname) continue; + + // When at home: + // Spawned: 0 0 1 0 -- 0, 0, SOLID_TRIGGER, 0 + // Returned: 80000000 0 1 40000 -- FL_RESPAWN, 0, SOLID_TRIGGER, ITEM_TARGETS_USED + // + // When picked up: + // Picked: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // + // When dropped after being picked up: + // Dropped: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED + // Dropped: 0 0 1 10000 -- 0, 0, SOLID_TRIGGER, DROPPED_ITEM + + + if (ent->typeNum == FLAG_T1_NUM) + { + //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag1 = ent; + bot_ctf_status.flag1_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag1_home_node = node_added; + Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + */ + + bot_ctf_status.flag1_is_home = true; + bot_ctf_status.flag1_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = true; + } + + // Picked up + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_carried = true; + if (bot_ctf_status.flag2_home_node != INVALID && bot_ctf_status.player_has_flag1 != NULL) + bot_ctf_status.team2_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, bot_ctf_status.player_has_flag1->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag1_is_home = false; + bot_ctf_status.flag1_is_dropped = true; + } + } + + if (ent->typeNum == FLAG_T2_NUM) + { + //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); + + bot_ctf_status.flag2 = ent; + bot_ctf_status.flag2_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + + // Home - never touched (set once per map) + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) + { + /* + // Add a flag node here + trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); + int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); + if (node_added != INVALID) + { + BOTLIB_LinkNodesNearbyNode(ent, node_added); + bot_ctf_status.flag2_home_node = node_added; + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + */ + + bot_ctf_status.flag2_is_home = true; + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); + } + // Returned + if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = true; + } + + // Picked + if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_carried = true; + if (bot_ctf_status.flag1_home_node != INVALID && bot_ctf_status.player_has_flag2 != NULL) + bot_ctf_status.team1_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, bot_ctf_status.player_has_flag2->s.origin); + } + // Dropped + if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) + { + bot_ctf_status.flag2_is_home = false; + bot_ctf_status.flag2_is_dropped = true; + } + + // Flag home moved + if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_CTF_Get_Flag_Node(ent)) + { + //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); + bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); // Update home node + } + } + + + + //if (ent->solid == SOLID_NOT) continue; // picked up + } + //Com_Printf("%s\n", __func__); +} + +void BOTLIB_CTF_Goals(edict_t* self) +{ + if (team_round_going == false || lights_camera_action) return; // Only allow during a real match (after LCA and before win/loss announcement) + + // Get flag + if (self->bot.bot_ctf_state != BOT_CTF_STATE_GET_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag2_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy blue flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag2_is_home) + { + int n = bot_ctf_status.flag1_home_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s heading for enemy red flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin)); + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Take flag home, if home flag is home + if (BOTLIB_Carrying_Flag(self)) + { + if (self->bot.bot_ctf_state != BOT_CTF_STATE_CAPTURE_ENEMY_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_ATTACK_ENEMY_CARRIER && + self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG && + self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag1_home_node; + else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) > 128) + n = bot_ctf_status.flag2_home_node; + + // If team flag is dropped and nearby, don't head to home. Allow bot to pickup the dropped team flag. + if (self->client->resp.team == TEAM1 && bot_ctf_status.flag1_is_dropped && VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, red flag was dropped nearby\n", __func__, self->client->pers.netname); + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.flag2_is_dropped && VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin) < 768) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, blue flag was dropped nearby\n", __func__, self->client->pers.netname); + } + + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && bot_connections.total_team1 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && bot_connections.total_team2 <= 1) + { + n = INVALID; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + //Com_Printf("%s %s goal_node[%d] flag1_home_node[%i] state[%d]\n", __func__, self->client->pers.netname, self->bot.goal_node, bot_ctf_status.flag1_home_node, self->state); + //if (n != INVALID) Com_Printf("%s %s self->bot.goal_node %i flag node %i\n", __func__, self->client->pers.netname, self->bot.goal_node, n); + + if (BOTLIB_CanGotoNode(self, n, false)) + { + if (self->client->resp.team == TEAM1) + { + //Com_Printf("%s %s Taking blue flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin)); + } + else + { + //Com_Printf("%s %s Taking red flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin)); + } + self->bot.bot_ctf_state = BOT_CTF_STATE_CAPTURE_ENEMY_FLAG; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + // Bot was on its way to the enemy flag, but got picked up by a team member before the bot could reach it. Therefore, try to support the flag carrrier who got it. + if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2) + { + //Com_Printf("%s %s Red flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1) + { + //Com_Printf("%s %s Blue flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + + // Support ally if they're close + if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin) < 1024) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + self->bot.ctf_support_time = level.framenum + 10.0 * HZ; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + + //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + //self->bot.state = BOT_MOVE_STATE_NAV; + } + } + + // Continue supporting flag carrier + if (self->bot.bot_ctf_state != BOT_CTF_STATE_COVER_FLAG_CARRIER && BOTLIB_Carrying_Flag(self) == false) + { + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team1_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting blue flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, bot_ctf_status.team1_carrier_dist_to_home); + + + // Support flag carrier if they're away from the home flag + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 && self->bot.ctf_support_time < level.framenum) + { + self->bot.ctf_support_time = level.framenum + 5.0 * HZ; + + //if (bot_ctf_status.team2_carrier_dist_to_home > 512) + // Com_Printf("%s %s continue supporting red flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, bot_ctf_status.team2_carrier_dist_to_home); + + // Support flag carrier if they're around + float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; + if (BOTLIB_CanGotoNode(self, n, false)) + { + //Com_Printf("%s %s continue supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + } + + + + + // Retrieve dropped T1/T2 flags if nearby + if (self->client->resp.team == TEAM1) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Dropped team flag + //Com_Printf("%s %s [team] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + else + { + flag_to_get = FLAG_T2_NUM; // Dropped enemy flag + //Com_Printf("%s %s [enemy] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + } + else if (bot_ctf_status.flag1_is_dropped) // Dropped team flag + flag_to_get = FLAG_T1_NUM; + else if (bot_ctf_status.flag2_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T2_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T1_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T2_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + if (self->client->resp.team == TEAM2) + { + int flag_to_get = 0; // Flag that is dropped + + // If both team and ememy flag is dropped, go for closest flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) + { + // If team flag is closer + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Dropped team flag + Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + } + else + { + flag_to_get = FLAG_T1_NUM; // Dropped enemy flag + Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + } + } + else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag + flag_to_get = FLAG_T2_NUM; + else if (bot_ctf_status.flag1_is_dropped) // Dropped enemy flag + flag_to_get = FLAG_T1_NUM; + + if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG + && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) + { + int n = INVALID; + if (flag_to_get == FLAG_T2_NUM) + n = bot_ctf_status.flag2_curr_node; + if (flag_to_get == FLAG_T1_NUM) + n = bot_ctf_status.flag1_curr_node; + + if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + { + if (flag_to_get == FLAG_T2_NUM) // Dropped team flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; + //Com_Printf("%s %s retrieving [team] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + if (flag_to_get == FLAG_T1_NUM) // Dropped enemy flag + { + self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; + //Com_Printf("%s %s retrieving [enemy] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } + + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, nodes[n].nodenum); + return; + } + } + } + + + // When the bot is close to the flag from following nodes, flags are not always on a node exactly, + // therefore when the bot gets close enough to the flag, force move the bot toward the flag. + if (self->bot.goal_node == INVALID) + { + + //if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_CAPTURE_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_RETURN_TEAM_FLAG) + { + // TEAM 1 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to home flag (Capture) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 1 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag2 == false && bot_ctf_status.flag2_is_home + && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // TEAM 2 - Walk to enemy flag (Take) + if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag1 == false && bot_ctf_status.flag1_is_home + && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward enemy home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped RED flag + if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + // Walk to dropped BLUE flag + if (bot_ctf_status.flag2_is_dropped && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) + { + //Com_Printf("%s %s is being forced toward dropped blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + vec3_t walkdir; + VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag + VectorNormalize(walkdir); + vec3_t hordir; //horizontal direction + hordir[0] = walkdir[0]; + hordir[1] = walkdir[1]; + hordir[2] = 0; + VectorNormalize(hordir); + VectorCopy(hordir, self->bot.bi.dir); + self->bot.bi.speed = 400; + self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + return; + } + } + + // Check if bot is closer to enemy flag carrier or the enemy flag at home + int flag_to_get = 0; + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1) + { + if (bot_ctf_status.flag2 && bot_ctf_status.flag2_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin)) + { + flag_to_get = FLAG_T2_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to blue flag than blue enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); + } + else + flag_to_get = FLAG_T1_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T1_NUM; // If both flags gone, go after enemy carrier + } + else if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2) + { + if (bot_ctf_status.flag1 && bot_ctf_status.flag1_is_home) // If enemy flag is home + { + // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier + if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin)) + { + flag_to_get = FLAG_T1_NUM; // Go after enemy flag + //Com_Printf("%s %s closer to red flag than red enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + } + else + flag_to_get = FLAG_T2_NUM; // Go after enemy carrier + } + else + flag_to_get = FLAG_T2_NUM; // If both flags gone, go after enemy carrier + } + + // Intercept enemy flag carrier if we're closer to them than the enemy flag + if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && flag_to_get == FLAG_T1_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team1 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag1->bot.current_node; //bot_ctf_status.flag2_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && flag_to_get == FLAG_T2_NUM) + { + // If both teams have the flag, and there's no other players/bots to go attack the flag carrier + qboolean force_incercept = false; + if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team2 <= 1) + { + force_incercept = true; + //Com_Printf("%s %s abort capturing flag, other team has our blue flag and there's no support to go get it back\n", __func__, self->client->pers.netname); + } + + self->bot.state = BOT_MOVE_STATE_MOVE; + + float dist = 999999999; + dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); + if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) + { + int n = bot_ctf_status.player_has_flag2->bot.current_node; //bot_ctf_status.flag1_curr_node; + if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at + { + //Com_Printf("%s %s intercepting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); + self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, n); + return; + } + } + } + + self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; + self->bot.ctf_support_time = 0; + + } + + /* + // Gather nearby weapons, ammo, and items + if (self->bot.bot_ctf_state == BOT_CTF_STATE_NONE && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ITEMS) + { + int item_node = BOTLIB_GetEquipment(self); + if (item_node != INVALID) + { + { + self->bot.state = BOT_MOVE_STATE_MOVE; + BOTLIB_SetGoal(self, nodes[item_node].nodenum); + self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ITEMS; + //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[item_node].nodenum); + return; + } + } + } + */ + + +} \ No newline at end of file diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index c156b496f..4e567a0a3 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -6323,17 +6323,19 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->bot.next_node == INVALID) { - Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); + if(pers_debug_mode) + Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); self->bot.state = BOT_MOVE_STATE_NAV; return; } if (self->bot.current_node == self->bot.next_node && self->bot.current_node == self->bot.goal_node) { - Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); + if(pers_debug_mode) + Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); ////if (nav_area.total_areas > 0) //// self->bot.state = BOT_MOVE_STATE_NAV_NEXT; ////else - self->bot.state = BOT_MOVE_STATE_NAV; + self->bot.state = BOT_MOVE_STATE_NAV; return; } else if (self->bot.next_node != INVALID && VectorDistance(nodes[self->bot.next_node].origin, self->s.origin) <= 128) diff --git a/src/action/g_main.c b/src/action/g_main.c index a7907588b..78d56f7a9 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -496,6 +496,7 @@ cvar_t* bot_randname; // Allow bots to pick a random name cvar_t* bot_chat; // Enable generic bot chat cvar_t* bot_personality; // Enable bot personality functionality [0 disable, 1 enable, 2 mixed with random bots] cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) +cvar_t* bot_teamplay; // Allows bots to play teamplay games without humans (see a_vote.c _numclients() ) //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 3021ff0dd..d7c84e641 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -696,6 +696,7 @@ void InitGame( void ) bot_chat = gi.cvar("bot_chat", "0", 0); bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); bot_ragequit = gi.cvar("bot_ragequit", "0", 0); + bot_teamplay = gi.cvar("bot_teamplay", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From 3881a1248aecf1488d81e248a0f0e9e8f0915efc Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 21:27:23 -0400 Subject: [PATCH 461/974] Some espionage additions, some more chat messages --- src/action/botlib/botlib.h | 6 ++++++ src/action/botlib/botlib_communication.c | 16 +++++++++++++-- src/action/botlib/botlib_esp.c | 25 +++++++++++++++++------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index d8af89b37..f222dd842 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -172,6 +172,12 @@ typedef struct ctf_status_s } ctf_status_t; extern ctf_status_t bot_ctf_status; +// =========================================================================== +// botlib_esp.c +// =========================================================================== +int BOTLIB_ESP_Get_Target_Node(edict_t* ent); +int BOTLIB_InterceptLeader(edict_t* self, int team, float distance); + typedef struct esp_status_s { edict_t* esp_target; // ETV: target edict diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 97329351f..93bf5f252 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -100,7 +100,7 @@ char *botchat_goodbyes[DBC_GOODBYES] = "sauna time" }; -#define DBC_KILLEDS 8 +#define DBC_KILLEDS 20 char *botchat_killeds[DBC_KILLEDS] = { "lol", @@ -110,7 +110,19 @@ char *botchat_killeds[DBC_KILLEDS] = "prkl", "jajajaja", "ffffffffff", - "woowwwww" + "woowwwww", + "ran outta ammo", + "ahhhhhhhh", + "damn", + "heh", + "rip", + ":D", + ":)))", + ">:)", + "noooooo", + ":<", + "oops", + "rofl", }; #define DBC_INSULTS 11 diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index 75e918041..9ee802a35 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -5,7 +5,7 @@ esp_status_t bot_esp_status; // Get the -int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) +int BOTLIB_ESP_Get_Target_Node(edict_t *ent) { vec3_t mins = { -16, -16, -24 }; vec3_t maxs = { 16, 16, 32 }; @@ -15,6 +15,17 @@ int BOTLIB_CTF_Get_Flag_Node(edict_t *ent) if (ent == NULL) return INVALID; + edict_t *tmp = NULL; + edict_t *target = NULL; + + // Reset escortcap value + espsettings.escortcap = false; + + // Find the target + while ((tmp = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { + target = tmp; + } + int cloest_node_num = INVALID; float cloest_node_dist = 99999999; @@ -279,7 +290,7 @@ void BOTLIB_Update_Flags_Status(void) //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); bot_ctf_status.flag1 = ent; - bot_ctf_status.flag1_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + bot_ctf_status.flag1_curr_node = BOTLIB_ESP_Get_Target_Node(ent); // Home - never touched (set once per map) if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) @@ -297,7 +308,7 @@ void BOTLIB_Update_Flags_Status(void) */ bot_ctf_status.flag1_is_home = true; - bot_ctf_status.flag1_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + bot_ctf_status.flag1_home_node = BOTLIB_ESP_Get_Target_Node(ent); //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); } // Returned @@ -327,7 +338,7 @@ void BOTLIB_Update_Flags_Status(void) //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); bot_ctf_status.flag2 = ent; - bot_ctf_status.flag2_curr_node = BOTLIB_CTF_Get_Flag_Node(ent); + bot_ctf_status.flag2_curr_node = BOTLIB_ESP_Get_Target_Node(ent); // Home - never touched (set once per map) if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) @@ -345,7 +356,7 @@ void BOTLIB_Update_Flags_Status(void) */ bot_ctf_status.flag2_is_home = true; - bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); + bot_ctf_status.flag2_home_node = BOTLIB_ESP_Get_Target_Node(ent); Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); } // Returned @@ -370,10 +381,10 @@ void BOTLIB_Update_Flags_Status(void) } // Flag home moved - if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_CTF_Get_Flag_Node(ent)) + if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_ESP_Get_Target_Node(ent)) { //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); - bot_ctf_status.flag2_home_node = BOTLIB_CTF_Get_Flag_Node(ent); // Update home node + bot_ctf_status.flag2_home_node = BOTLIB_ESP_Get_Target_Node(ent); // Update home node } } From 2d6bc0f19036fdbf3209d165b790e5a8ecada4f6 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 21:35:40 -0400 Subject: [PATCH 462/974] Adjusted jansson library stuff --- .github/workflows/build.yml | 6 +++--- src/action/botlib/botlib_personality.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95ac4c791..0db7f9888 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -178,7 +178,7 @@ jobs: libwayland-dev wayland-protocols libdecor-0-dev \ libavcodec-dev libavformat-dev libavutil-dev \ libswresample-dev libswscale-dev \ - uuid-dev patchelf + uuid-dev patchelf libjansson-dev - name: Build run: | @@ -248,7 +248,7 @@ jobs: - name: Install dependencies run: | - brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo + brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson - name: Build run: | @@ -279,7 +279,7 @@ jobs: - name: Install dependencies run: | - brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo + brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson - name: Build run: | diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index be4800152..045f27fc9 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -1,7 +1,7 @@ #include "../g_local.h" #include "../acesrc/acebot.h" #include "botlib.h" -#include "/opt/homebrew/include/jansson.h" +#include "jansson.h" // Count of bot personalities loaded qboolean pers_debug_mode = false; From 843827638ca0e2729595e47fd9730d2c7c00ee56 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 21:44:15 -0400 Subject: [PATCH 463/974] More gcc can't find anything nonsense --- meson.build | 2 +- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_personality.c | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index ddc5360ef..39bc04ade 100644 --- a/meson.build +++ b/meson.build @@ -441,7 +441,6 @@ if not win32 if jansson.found() client_deps += jansson config.set10('USE_JANSSON', true) - dll_link_args += '-L/opt/homebrew/lib' dll_link_args += '-ljansson' endif endif @@ -455,6 +454,7 @@ if host_machine.system() == 'darwin' ) client_deps += opengl inc_dirs += '/opt/homebrew/include' + dll_link_args += '-L/opt/homebrew/lib' endif # require FFmpeg >= 5.1.3 diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index f222dd842..13cab9457 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -536,7 +536,7 @@ typedef struct { char* name; temp_bot_personality_t personality; } temp_bot_mapping_t; -temp_bot_mapping_t bot_mappings[100]; +extern temp_bot_mapping_t bot_mappings[100]; void BOTLIB_PersonalityFile(void); temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 045f27fc9..96781fc27 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -7,6 +7,7 @@ qboolean pers_debug_mode = false; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create +temp_bot_mapping_t bot_mappings[MAX_BOTS]; /////* // This file focuses on loading bot personality traits From 06b03b392436b5d9ee253499e0377ef01f2b1b31 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 21:55:16 -0400 Subject: [PATCH 464/974] Added libjansson wrapper --- .github/workflows/build.yml | 6 +++--- meson.build | 15 +++++++++++++-- subprojects/libjansson.wrap | 9 +++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 subprojects/libjansson.wrap diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0db7f9888..f4acf2358 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,7 +133,7 @@ jobs: libcurl4-openssl-dev libx11-dev libxi-dev \ libavcodec-dev libavformat-dev libavutil-dev \ libswresample-dev libswscale-dev \ - uuid-dev patchelf + uuid-dev patchelf libjansson-dev pip3 install meson ninja - name: Build @@ -248,7 +248,7 @@ jobs: - name: Install dependencies run: | - brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson + brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson ossp-uuid - name: Build run: | @@ -279,7 +279,7 @@ jobs: - name: Install dependencies run: | - brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson + brew install pkg-config meson libpng sdl2 openal-soft zlib curl ffmpeg jpeg-turbo jansson ossp-uuid - name: Build run: | diff --git a/meson.build b/meson.build index 39bc04ade..c3cccbbfa 100644 --- a/meson.build +++ b/meson.build @@ -419,6 +419,12 @@ if openal.found() config.set10('USE_OPENAL', true) endif +jansson = dependency('libjansson', + version: '>= 2.13', + required: get_option('libjansson'), + default_options: fallback_opt, +) + if not win32 uuid = dependency('uuid', required: get_option('uuid'), @@ -453,8 +459,13 @@ if host_machine.system() == 'darwin' default_options: fallback_opt, ) client_deps += opengl - inc_dirs += '/opt/homebrew/include' - dll_link_args += '-L/opt/homebrew/lib' + if host_machine.cpu_family() == 'x86_64' # Intel + inc_dirs += '/usr/local/include' + dll_link_args += '-L/usr/local/lib' + else # Apple Silicon + inc_dirs += '/opt/homebrew/include' + dll_link_args += '-L/opt/homebrew/lib' + endif endif # require FFmpeg >= 5.1.3 diff --git a/subprojects/libjansson.wrap b/subprojects/libjansson.wrap new file mode 100644 index 000000000..d0416ea51 --- /dev/null +++ b/subprojects/libjansson.wrap @@ -0,0 +1,9 @@ +[wrap-file] +directory = jansson-2.13 +source_url = https://digip.org/jansson/releases/jansson-2.13.tar.gz +source_filename = jansson-2.13.tar.gz +source_hash = 02c31bc16e702b30feb06d18bbfe086c0d8c938e906950980af7adcdb324541b +wrapdb_version = 2.13-1 + +[provide] +jansson = jansson_dep From f950368f70f674b6a5e790417021b116c54e8ae8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:03:24 -0400 Subject: [PATCH 465/974] ah the jansson vs libjansson --- meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index c3cccbbfa..90b8eadb3 100644 --- a/meson.build +++ b/meson.build @@ -419,9 +419,9 @@ if openal.found() config.set10('USE_OPENAL', true) endif -jansson = dependency('libjansson', +jansson = dependency('jansson', version: '>= 2.13', - required: get_option('libjansson'), + required: get_option('jansson'), default_options: fallback_opt, ) From 318826a3409abe37939ca1e9ec79f92167f91346 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:05:38 -0400 Subject: [PATCH 466/974] Reduced jansson version requirement --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 90b8eadb3..c95f74d71 100644 --- a/meson.build +++ b/meson.build @@ -420,7 +420,7 @@ if openal.found() endif jansson = dependency('jansson', - version: '>= 2.13', + version: '>= 2.8', required: get_option('jansson'), default_options: fallback_opt, ) From e1737cacab726b44b7af786ce63ff4994e770ac8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:25:21 -0400 Subject: [PATCH 467/974] Trying a libjansson wrapper --- subprojects/libjansson.wrap | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/subprojects/libjansson.wrap b/subprojects/libjansson.wrap index d0416ea51..a1ae48618 100644 --- a/subprojects/libjansson.wrap +++ b/subprojects/libjansson.wrap @@ -1,9 +1,13 @@ [wrap-file] -directory = jansson-2.13 -source_url = https://digip.org/jansson/releases/jansson-2.13.tar.gz -source_filename = jansson-2.13.tar.gz -source_hash = 02c31bc16e702b30feb06d18bbfe086c0d8c938e906950980af7adcdb324541b -wrapdb_version = 2.13-1 +directory = jansson-2.14 +source_url = https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 +source_filename = jansson-2.14.tar.bz2 +source_hash = fba956f27c6ae56ce6dfd52fbf9d20254aad42821f74fa52f83957625294afb9 +patch_filename = jansson_2.14-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/jansson_2.14-2/get_patch +patch_hash = eb5d70d761313509cfd2d384869b292aa54f5a1fc01fb7958f084ebc7ad394f6 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/jansson_2.14-2/jansson-2.14.tar.bz2 +wrapdb_version = 2.14-2 [provide] jansson = jansson_dep From 5fef6806fc3e7f21f5173d72821aed5ce272f0dd Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:47:47 -0400 Subject: [PATCH 468/974] A few minor fixes for building Windows: --- inc/shared/shared.h | 2 +- src/action/botlib/botlib_personality.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index c261e2ecc..9a9ee4b8c 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., //rekkie -- CMAKE -- s #if _MSC_VER >= 1920 && !__INTEL_COMPILER //#define NDEBUG 1 -#define VERSION "ReKTeK" +//#define VERSION "ReKTeK" //#define BUILDSTRING "1" //#define CPUSTRING "1" //#define BASEGAME "baseq2" diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 96781fc27..26e41e848 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -1,7 +1,7 @@ #include "../g_local.h" #include "../acesrc/acebot.h" #include "botlib.h" -#include "jansson.h" +#include // Count of bot personalities loaded qboolean pers_debug_mode = false; From f4f588eda07469bb0f6484783cbdb42162faf7d5 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:55:54 -0400 Subject: [PATCH 469/974] Adjustments to meson.build, come on Windows --- meson.build | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/meson.build b/meson.build index c95f74d71..3f752a24c 100644 --- a/meson.build +++ b/meson.build @@ -425,6 +425,13 @@ jansson = dependency('jansson', default_options: fallback_opt, ) +if jansson.found() + client_deps += jansson + config.set10('USE_JANSSON', true) + allow_fallback: jansson_dep + dll_link_args += '-ljansson' +endif + if not win32 uuid = dependency('uuid', required: get_option('uuid'), @@ -437,18 +444,6 @@ if not win32 config.set10('USE_UUID', true) dll_link_args += '-luuid' endif - - jansson = dependency('jansson', - required: get_option('jansson'), - allow_fallback: win32, - default_options: fallback_opt, - ) - - if jansson.found() - client_deps += jansson - config.set10('USE_JANSSON', true) - dll_link_args += '-ljansson' - endif endif if host_machine.system() == 'darwin' From 1bde7b00e975f02ee15142113ab9631b96a8d3b4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 22:58:08 -0400 Subject: [PATCH 470/974] Adjustments to meson.build, come on Windows --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 3f752a24c..df92f1d2e 100644 --- a/meson.build +++ b/meson.build @@ -423,12 +423,12 @@ jansson = dependency('jansson', version: '>= 2.8', required: get_option('jansson'), default_options: fallback_opt, + allow_fallback: jansson_dep ) if jansson.found() client_deps += jansson config.set10('USE_JANSSON', true) - allow_fallback: jansson_dep dll_link_args += '-ljansson' endif From 17e3cb618a827da6e259e216682e087e3a6a28f7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:01:31 -0400 Subject: [PATCH 471/974] Adjustments to meson.build, come on Windows --- meson.build | 1 - 1 file changed, 1 deletion(-) diff --git a/meson.build b/meson.build index df92f1d2e..24ae0eb9f 100644 --- a/meson.build +++ b/meson.build @@ -423,7 +423,6 @@ jansson = dependency('jansson', version: '>= 2.8', required: get_option('jansson'), default_options: fallback_opt, - allow_fallback: jansson_dep ) if jansson.found() From 30e02d406f80def1d86b78d594d00fb5674709f4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:14:17 -0400 Subject: [PATCH 472/974] Adjustments to meson.build, come on Windows --- meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meson.build b/meson.build index 24ae0eb9f..9b4660991 100644 --- a/meson.build +++ b/meson.build @@ -427,8 +427,12 @@ jansson = dependency('jansson', if jansson.found() client_deps += jansson + server_deps += jansson config.set10('USE_JANSSON', true) dll_link_args += '-ljansson' + if win32 + inc_dirs += 'subprojects/jansson-2.14' + endif endif if not win32 From 77f01cd2a295de9837d6d8ae73b78ef40f3bdcb2 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:26:49 -0400 Subject: [PATCH 473/974] Trying another method to get jansson to work with Windows, jesus christ --- meson.build | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index 9b4660991..de6be2f5d 100644 --- a/meson.build +++ b/meson.build @@ -419,20 +419,17 @@ if openal.found() config.set10('USE_OPENAL', true) endif -jansson = dependency('jansson', +jansson_dep = dependency('jansson', version: '>= 2.8', required: get_option('jansson'), default_options: fallback_opt, + fallback: ['jansson', 'jansson_dep'] ) - -if jansson.found() +if jansson_dep.found() client_deps += jansson server_deps += jansson config.set10('USE_JANSSON', true) dll_link_args += '-ljansson' - if win32 - inc_dirs += 'subprojects/jansson-2.14' - endif endif if not win32 From 306628534fe864fc6dff7cff510e79573308a5e5 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:30:20 -0400 Subject: [PATCH 474/974] Trying another method to get jansson to work with Windows, jesus christ --- meson.build | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index de6be2f5d..a1832f92f 100644 --- a/meson.build +++ b/meson.build @@ -421,14 +421,13 @@ endif jansson_dep = dependency('jansson', version: '>= 2.8', - required: get_option('jansson'), - default_options: fallback_opt, - fallback: ['jansson', 'jansson_dep'] + fallback: ['jansson', 'jansson_dep'], + required: true # Or set to true/false as needed ) + if jansson_dep.found() - client_deps += jansson - server_deps += jansson - config.set10('USE_JANSSON', true) + client_deps += jansson_dep + server_deps += jansson_dep dll_link_args += '-ljansson' endif From 0a79372b17d34f0ea91743cfd4cd9769e3156979 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:32:05 -0400 Subject: [PATCH 475/974] omfg libjansson vs jansson just pick one --- subprojects/{libjansson.wrap => jansson.wrap} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename subprojects/{libjansson.wrap => jansson.wrap} (100%) diff --git a/subprojects/libjansson.wrap b/subprojects/jansson.wrap similarity index 100% rename from subprojects/libjansson.wrap rename to subprojects/jansson.wrap From 1b4d06bcc9a65c7a83de162fc55b45d3ec252167 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 9 Aug 2024 23:45:53 -0400 Subject: [PATCH 476/974] Trying another method to get jansson to work with Windows, jesus christ --- .github/workflows/build.yml | 3 +++ meson.build | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4acf2358..2bb1f30df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,6 +107,9 @@ jobs: meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_WIN }} builddir meson compile -C builddir + - name: List Jansson directory contents + run: dir subprojects\jansson-2.14 + - name: Generate Win MSVC archives uses: actions/upload-artifact@v4 with: diff --git a/meson.build b/meson.build index a1832f92f..cbdd8b0e0 100644 --- a/meson.build +++ b/meson.build @@ -428,6 +428,9 @@ jansson_dep = dependency('jansson', if jansson_dep.found() client_deps += jansson_dep server_deps += jansson_dep + if host_machine.system() == 'windows' and cc.get_id() == 'msvc' + jansson_lib = 'subprojects/jansson-2.14/jansson.lib' + endif dll_link_args += '-ljansson' endif From cb112290de85a0969563792e911ba0640fb57cf9 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 11:54:50 -0400 Subject: [PATCH 477/974] Trying a homemade meson.build file for jansson --- extern/jansson_patch.zip | Bin 0 -> 1559 bytes meson.build | 3 --- 2 files changed, 3 deletions(-) create mode 100644 extern/jansson_patch.zip diff --git a/extern/jansson_patch.zip b/extern/jansson_patch.zip new file mode 100644 index 0000000000000000000000000000000000000000..ffc20cc0716ad596d73e3a73f326738630ee5dbf GIT binary patch literal 1559 zcmZ|Pc|4SP9Ki8s4%>u5jz&g|Mw9y(=AL9m3y*HMymGN`%qnL! z$*^Qbk@=jx+gOg7UC*ZHXp^61J=TFH$%(!Kcp;H;5wVR*ohl*03w) z3SNc3wk2Rl*Ae4djQZ95+f1t*%8asa?o)4HbiJ9`CjwG~%Gy z3w;78!Sn{X4vdO_NX{K`142u>#m1)Sr1KM0f86-3GKqLMOKNM|{+h*}%!yF3-Y8bD zR||UTtx%AFp5x?@+Rf5{zyqIm$2#%FP(Q5tOK<@?!(1@9@r9DNs-xWl0?TcS0 z+V$&yCS2;gd_N6k0y}9e!ZD3VA!};dlz7bb&sK!#K;S;S{FVdwk{i>g*o96N%0MhQ zhUiymlW>))3YWmfL>g@jn9dIrbUvxXJ*k{@9*ozYQ!!GOP%w83)>h28L2BnkWnv~| zAYrQVimp`$6Ie(ONVp0vfFaHS5UD(qL zY_I@d!U)B&3)QJMlDgnM;`EwokkPCsZG~%4AeZIzIGxjdh{$~u@C)};uA?)@d~IIM zxchnc2B>E>uiKFx%-J&CZ+>l7eyCyZp~E#pQuuCsgEv;l$ZfMO*$@>^?z-5>Ek}Zo zuc(1~`NlPLI$kru!hKAFd7Q!DGE+}W)dl`kiob_OcxU9blOt`Vli{zEK`!ARxx1i*v|2rz6yNl>vwJ4 pA%9fYFC@RiKv|0Qx8(m?%hz{W`<*WE)6TDe`Okg+bP?G3`VZhgn63Z- literal 0 HcmV?d00001 diff --git a/meson.build b/meson.build index cbdd8b0e0..a1832f92f 100644 --- a/meson.build +++ b/meson.build @@ -428,9 +428,6 @@ jansson_dep = dependency('jansson', if jansson_dep.found() client_deps += jansson_dep server_deps += jansson_dep - if host_machine.system() == 'windows' and cc.get_id() == 'msvc' - jansson_lib = 'subprojects/jansson-2.14/jansson.lib' - endif dll_link_args += '-ljansson' endif From d76b4f80610480623c33215a33d4a6f12e71c094 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 11:57:15 -0400 Subject: [PATCH 478/974] Trying a homemade meson.build file for jansson --- extern/jansson_patch.zip | Bin 1559 -> 1579 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/extern/jansson_patch.zip b/extern/jansson_patch.zip index ffc20cc0716ad596d73e3a73f326738630ee5dbf..2379c56b28944d47ae9f8434e6a206df92e412d5 100644 GIT binary patch delta 1319 zcmV+?1=#wR466)~Q4J$s3R$CWBOc2H007gGUL=3gjkjjgjkjiXcnbgl1oi;{00a~O z006aC-*4MC5PsKR!66TluvAUjASkkBs2jVr2FEUJrCk>k1Wi#96Nyww%BeT(f8UX^ zY$?B%KJ3NT9q&HgeY`ueQu3Nn6ZqFeXf1`mk9-P0E2cD;0-YfLRdjd+nu1d%G_#uX zIPrf&&!ZHbqLP7Ebj1v&vaEQ)Q~&^>SEa-dSf^5C{Hj*O035c;AoOU?=nc*pNnsl> zy7Un8nMgSiQSR?sDWk#^tx!lx3f4**+|#YF)-NlfNZD0rLrhmTyktFjT`}D%YQeQ> z@V!?IsrAvLon z2**+Jbn|7Cmsv{IR+bfWXAFLB-EoQ6oE448OvAMkj54w)eDw7{a?hTVdsYi-YaoBf z?quI4VVWc$yVtMm_I2;;@VNbY^JqXg*@k4E6Ot{1z@VRzj}4$#ie>ye)b}12w5U^t zer?(Q&4293+5=m}e~w`PKDMtl|BF2ezNZgr%d>A%CJ&l8QmjcK#g*Sr@M1B- z7xTe%I6DW6Txx?$V3rFO0WXG?L5O}jMgA=psl3&FbnE;%2H(#Md^Z2`F_{fM#*f0B z_Fz2v)MV=Q|0UYBTf^#haP;ShD@gi3O^05SLl6ZdX9K`^_wCyNcxI6{V_1I&nJsLz zk~~GW_kf7M|AG7w>V>)~oJl;jCzUnpOW5%SzU)ZOdcoZHOkCAla*mH&72GvhB=ckj zcHNN@dQGslJ!BSa3dEk&(as-#tEFM-Zl}?Q(Hy^@&Cdr8tbmyXR27>2fRA%=X?c6- zef-Dy=maG>gNtoN6emky4gr4-$`~=V0>MbMWQcvQ-~!~PSfi9{YnDdV4;iM7M!*li z8HYm@LnVjK7gs1#vb5^WRnpEtjg~NGZxC6^6*!30Igvuk&75EU>K-!iW|_5rA=*R4RopF60tom8E#+0M zx7-3z&0LXoZGVO>LxJrdQPqD2CNwR{4cn4gDZV0x?MQXY?&$#BH6D-II;&;`w774bt~Yy5 zn!9Y@`+=yQ6U~oCLAOz~2nAolpoCvV)VlLVbnhrAxU?>-i0)*jlvG^>jJ4d@gMUzo zHG?+ltQ3;;`wsfeqV-E91en$Pd4I;lGnm1z!y#UkX{H dZX+Jc1ONcjle`5o0@96>@C6|TWCZ{K006>zcmMzZ delta 1299 zcmV+u1?>8(43`X$Q4MBZ3Rx>$9_zOR006a-UL=2SjJIZ&jJIZWcnbgl1oi;{00a~O z006aCZExE)5dN-T!J!`{VVRn=MNxF?P&aWK4T)XYO1my72%0)YOe9hxDW~4B|Gp#T z7b$ORKJ1IFJKjCKdw6$bqtzvF8-s-k~T(p{5griOw#Q`sYU(pg+j@zKo zc|b@AG8arHWw__0f=gR%gi3K%!A3IZo^OSfdD<||>b}BQX8W=gH4Nlc17@RWr7*U` z`x*p=(!h90N=$SRCYt)35(7xr@#xc-zTJO+8z$&bGaHpjE1elvS`@1&POPGB(;yvj zyhk`sN@mtjQ5nYpX1m@h2)4Kf`)lNRc9c#sdK}(#4KY+9U2=%A4Yt5w&+*_YWx$;v z9!Dec^_NRt6**fuSvBC#82$XZ=aQ}jRGkR6WTUAvd`T8HZ7vGb6(Ms-WAS!=u zWWPzmc1d7%&!4&N+riht%gxuTdjq1`HYEE!A-OV$Ecuay)FOJLp%CA&zMlxmt2PJn z%ZBY=!~2eGb71rIk1_5)#14%Sf5E-ryGr!6n#I>>EZ0l$80+KnBEKX zW&jiLhbGgY|If*;-8xpcgJU>Hd_nU6X*vv=9D}GZITrw>yYJ8iAPR@H9>aeoDqLZc zr4l)Dy@y2n{SW3($RN~p;hdxscTz>SzQ7%?@XL+lVi3%2&&*f7Bj@D{Uj;W!=GiQp zqFsNa#6cIVYY$aK>jH5n^KCR#h zd7u7%I(|*E67XVMGA-BwnInIQLkeKF)hJj+7C_)zgBPH%)r#c8II}!)e#C;Yh9Ex( zXFU$nKx%=VFE2=;RqfR4t7J0+4O!rry&`O(mgpdJ@5Cyj)^h@Kgj=jgc!{?w4HGhQ zYdiebK$gJj1*LOJWDmy6?AwyDNF(inVmBQd{jV9z817v?IA0$z&C3w#p zRRd`%W?jJ&(*PHR-~=nwSFq$jMBQi%x0co<{X<6H9JB5(CQk_S6?e_FL;`*$3w6<& zEw@M%HUbe6yRWVRJL^`t_})309*$1NhttU?gxQ0Dv{*6=J5v3!e>xy{oyUE)-l`h`8{D^EH<&%I zySr>Jctlj+6U}3z;M*uR2$fjipu}Iz^s4tp^zSGvxV){Kgzsd~TIs$5$69IK!QZK* z2C$8KE0tmcxrSQlRbSIEY@=4XzrGsh%Azd_&^MHr;j*u4ZP8a3m#k_xp>fLeg>lc@ zpN@RdVD-IEKj0gz&qxjpGLi$YunnDrvjikxxs$?Sd_}K?byr7^!99P7{?4bl(D>TM zO90$X=Xa Date: Sat, 10 Aug 2024 12:00:05 -0400 Subject: [PATCH 479/974] Update patch url and shasum for jansson --- subprojects/jansson.wrap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subprojects/jansson.wrap b/subprojects/jansson.wrap index a1ae48618..4de6d2c17 100644 --- a/subprojects/jansson.wrap +++ b/subprojects/jansson.wrap @@ -4,8 +4,8 @@ source_url = https://github.com/akheron/jansson/releases/download/v2.14/jansson- source_filename = jansson-2.14.tar.bz2 source_hash = fba956f27c6ae56ce6dfd52fbf9d20254aad42821f74fa52f83957625294afb9 patch_filename = jansson_2.14-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/jansson_2.14-2/get_patch -patch_hash = eb5d70d761313509cfd2d384869b292aa54f5a1fc01fb7958f084ebc7ad394f6 +patch_url = https://raw.githubusercontent.com/actionquake/q2pro/rektek-bots-personality/extern/jansson_patch.zip +patch_hash = d3d72f357402287ed07f6831921c1a4771647392e8efad13084040abcd83b7fa source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/jansson_2.14-2/jansson-2.14.tar.bz2 wrapdb_version = 2.14-2 From 75706684fda1591719c332f4596c328262b20531 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:03:53 -0400 Subject: [PATCH 480/974] Updating libjansson build wrap file --- extern/jansson_patch.zip | Bin 1579 -> 1603 bytes subprojects/jansson.wrap | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/jansson_patch.zip b/extern/jansson_patch.zip index 2379c56b28944d47ae9f8434e6a206df92e412d5..4f2e4b9def3f0f736cb8d6bcfeca73856935f853 100644 GIT binary patch delta 1344 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 Date: Sat, 10 Aug 2024 12:16:32 -0400 Subject: [PATCH 481/974] Trying to build it in mingw --- .github/workflows/build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bb1f30df..5d7c26872 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,14 @@ jobs: sudo apt-get install -y gcc-mingw-w64 nasm python3-pip ninja-build sudo python3 -m pip install meson + - name: Build jansson + run: | + wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 + ./configure + make + make check + make install + - name: Build run: | meson setup --cross-file=.ci/${{ matrix.arch }}-w64-mingw32.txt \ From a59a50aa26f5ebd311fea32824147a8e0a36310b Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:17:17 -0400 Subject: [PATCH 482/974] Trying to build it in mingw --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d7c26872..09a260ac6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,6 +67,8 @@ jobs: - name: Build jansson run: | wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 + tar -xvf jansson-2.14.tar.bz2 + cd jansson-2.14 ./configure make make check From 14451a75442206cde929c255e8f605c799ee2a2d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:21:00 -0400 Subject: [PATCH 483/974] Trying to build it in mingw --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 09a260ac6..e9e071c4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,7 @@ jobs: ./configure make make check + cat test-suite.log make install - name: Build From 543d00476cd43c5edf647772a07dd7fefbce2c70 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:22:20 -0400 Subject: [PATCH 484/974] Trying to build it in mingw --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9e071c4b..ab79268cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,7 @@ jobs: cd jansson-2.14 ./configure make - make check + make check | true cat test-suite.log make install From 3aee8da70c07dcb18be54dd777bbd56545788bdd Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:27:01 -0400 Subject: [PATCH 485/974] Nevermind --- .github/workflows/build.yml | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab79268cf..1423617a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,16 +64,16 @@ jobs: sudo apt-get install -y gcc-mingw-w64 nasm python3-pip ninja-build sudo python3 -m pip install meson - - name: Build jansson - run: | - wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 - tar -xvf jansson-2.14.tar.bz2 - cd jansson-2.14 - ./configure - make - make check | true - cat test-suite.log - make install + # - name: Build jansson + # run: | + # wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 + # tar -xvf jansson-2.14.tar.bz2 + # cd jansson-2.14 + # ./configure + # make + # make check | true + # cat test-suite.log + # make install - name: Build run: | @@ -118,9 +118,6 @@ jobs: meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_WIN }} builddir meson compile -C builddir - - name: List Jansson directory contents - run: dir subprojects\jansson-2.14 - - name: Generate Win MSVC archives uses: actions/upload-artifact@v4 with: From 303c7b69f0601396caedfc86133cf5352a73a17e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:31:21 -0400 Subject: [PATCH 486/974] Trying only game_deps --- meson.build | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meson.build b/meson.build index a1832f92f..fa3ad63ac 100644 --- a/meson.build +++ b/meson.build @@ -426,8 +426,7 @@ jansson_dep = dependency('jansson', ) if jansson_dep.found() - client_deps += jansson_dep - server_deps += jansson_dep + game_deps += jansson_dep dll_link_args += '-ljansson' endif From 49119e62ec73466434baf996a87ad0566578ca89 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:34:04 -0400 Subject: [PATCH 487/974] Missed an 'f' --- meson.build | 2 +- src/action/botlib/botlib_personality.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index fa3ad63ac..bd4e3be1c 100644 --- a/meson.build +++ b/meson.build @@ -419,12 +419,12 @@ if openal.found() config.set10('USE_OPENAL', true) endif +# libjansson required for botlib jansson_dep = dependency('jansson', version: '>= 2.8', fallback: ['jansson', 'jansson_dep'], required: true # Or set to true/false as needed ) - if jansson_dep.found() game_deps += jansson_dep dll_link_args += '-ljansson' diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 26e41e848..3be5d49c8 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -267,6 +267,7 @@ void BOTLIB_PersonalityFile(void) { cvar_t* botdir = gi.cvar("botdir", "bots", 0); #ifdef _WIN32 + int f; f = sprintf(filename, ".\\"); f += sprintf(filename + f, game_dir->string); f += sprintf(filename + f, "\\"); From a1c5bce4822d5e519f1eea1de3398804b6b472b0 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 12:54:34 -0400 Subject: [PATCH 488/974] Trying without specifying dll link args --- meson.build | 2 +- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_personality.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index bd4e3be1c..8f4ffe77c 100644 --- a/meson.build +++ b/meson.build @@ -427,7 +427,7 @@ jansson_dep = dependency('jansson', ) if jansson_dep.found() game_deps += jansson_dep - dll_link_args += '-ljansson' + #dll_link_args += '-ljansson' endif if not win32 diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 13cab9457..89fef1a2d 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -542,7 +542,7 @@ void BOTLIB_PersonalityFile(void); temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename); void DeactivateBotPersonality(void); qboolean BOTLIB_SetPersonality(edict_t* bot, int team, int force_gender); -qboolean BOTLIB_LoadBotPersonality(edict_t* self); +void BOTLIB_LoadBotPersonality(edict_t* self); void BOTLIB_FreeBotPersonality(edict_t* bot); qboolean BotRageQuit(edict_t* self, qboolean frag_or_death); qboolean BOTLIB_DoIChat(edict_t* bot); diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 3be5d49c8..5f39ce9fc 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -334,7 +334,7 @@ char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { } // At this point we have the bot ready, we just need to copy over the right loaded personality -qboolean BOTLIB_LoadBotPersonality(edict_t* self) +void BOTLIB_LoadBotPersonality(edict_t* self) { temp_bot_mapping_t* selectedBot = &bot_mappings[self->bot.personality.pId]; From 4731cc90bc52e6d4cf625f72cec619ffdfda6163 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 10 Aug 2024 13:00:15 -0400 Subject: [PATCH 489/974] Updated jansson_patch zip file --- extern/jansson_patch.zip | Bin 1603 -> 1631 bytes subprojects/jansson.wrap | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/jansson_patch.zip b/extern/jansson_patch.zip index 4f2e4b9def3f0f736cb8d6bcfeca73856935f853..e889f5ce65557a2b69fda47f99efea50e0ee27ed 100644 GIT binary patch delta 1371 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 Date: Sat, 10 Aug 2024 13:05:13 -0400 Subject: [PATCH 490/974] strtok_s and _r compatibility because Windows is weird --- src/action/botlib/botlib_personality.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 5f39ce9fc..aa94d2db4 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -310,8 +310,14 @@ char* _splitSkinChar(char *skinpathInput, qboolean returnSkin) { return NULL; } - // Use strtok_r for a safer tokenization + // Use strtok_s for a safer tokenization on Windows +#ifdef _WIN32 + char *context; + char *token = strtok_s(skinpath, "/", &context); +#else char *token = strtok_r(skinpath, "/", &saveptr); +#endif + if (token == NULL) { gi.dprintf("%s skin path provided is invalid\n", skinpath); free(skinpath); // Clean up From 1c60845eaad08af65ca2c81965f4a7c73f5ff126 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 9 Aug 2024 22:23:06 +0300 Subject: [PATCH 491/974] Implement distance LOD for MD5 models. --- doc/client.asciidoc | 9 +++++++-- src/refresh/gl.h | 1 + src/refresh/main.c | 2 ++ src/refresh/mesh.c | 4 +++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index f98818a86..c5f13bfbb 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -853,6 +853,11 @@ gl_md5_use:: Enables use of MD5 replacement models as found in re-release. Only effective if ‘gl_md5_load’ is enabled. Default value is 1. +gl_md5_distance:: + Only use MD5 replacement model if entity is closer than this distance to + the viewer, otherwise use original model. Default value is 2048. Setting + this to 0 disables distance LOD. + gl_glowmap_intensity:: Intensity factor for entity glowmaps. Default value is 0.75. @@ -889,8 +894,8 @@ When Q2PRO attempts to load an alias model from disk, it determines actual model format by file contents, rather than by filename extension. Therefore, if you wish to override MD2 model with MD3 replacement, simply rename the MD3 model to ‘tris.md2’ and place it in appropriate packfile to make sure it gets -loaded first. Note that MD5 models can't be loaded this way, see ‘gl_md5_’ -variables. +loaded first. MD5 overrides don't work this way, use ‘gl_md5_’ variables to +control that. ******************** diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 5403d80fc..44d1da066 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -215,6 +215,7 @@ extern cvar_t *gl_shaders; #if USE_MD5 extern cvar_t *gl_md5_load; extern cvar_t *gl_md5_use; +extern cvar_t *gl_md5_distance; #endif extern cvar_t *gl_damageblend_frac; diff --git a/src/refresh/main.c b/src/refresh/main.c index 3bac541c0..cb34ae522 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -54,6 +54,7 @@ cvar_t *gl_shaders; #if USE_MD5 cvar_t *gl_md5_load; cvar_t *gl_md5_use; +cvar_t *gl_md5_distance; #endif cvar_t *gl_damageblend_frac; cvar_t *gl_waterwarp; @@ -919,6 +920,7 @@ static void GL_Register(void) #if USE_MD5 gl_md5_load = Cvar_Get("gl_md5_load", "1", CVAR_FILES); gl_md5_use = Cvar_Get("gl_md5_use", "1", 0); + gl_md5_distance = Cvar_Get("gl_md5_distance", "2048", 0); #endif gl_damageblend_frac = Cvar_Get("gl_damageblend_frac", "0.2", 0); gl_waterwarp = Cvar_Get("gl_waterwarp", "0", 0); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 73b632e61..8819d6582 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -823,7 +823,9 @@ void GL_DrawAliasModel(const model_t *model) // draw all the meshes #if USE_MD5 - if (model->skeleton && gl_md5_use->integer) + if (model->skeleton && gl_md5_use->integer && + (ent->flags & RF_NO_LOD || gl_md5_distance->value <= 0 || + Distance(origin, glr.fd.vieworg) <= gl_md5_distance->value)) draw_alias_skeleton(model->skeleton); else #endif From 31fa1c17b1b164b8e5d5b4ad8902db3e7f439dae Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 10 Aug 2024 15:37:55 +0300 Subject: [PATCH 492/974] Implement frustum culling for flares. --- src/refresh/draw.c | 3 ++- src/refresh/gl.h | 1 + src/refresh/main.c | 51 +++++++++++++++++++++++++++++----------------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 7fdd352cc..f28212017 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -448,7 +448,7 @@ void Draw_Stats(void) int x = 10, y = 10; R_SetScale(1.0f / get_auto_scale()); - R_DrawFill8(8, 8, 25*8, 21*10+2, 4); + R_DrawFill8(8, 8, 25*8, 22*10+2, 4); Draw_Stringf(x, y, "Nodes visible : %i", c.nodesVisible); y += 10; Draw_Stringf(x, y, "Nodes culled : %i", c.nodesCulled); y += 10; @@ -471,6 +471,7 @@ void Draw_Stats(void) Draw_Stringf(x, y, "Total dlights : %i", glr.fd.num_dlights); y += 10; Draw_Stringf(x, y, "Total particles: %i", glr.fd.num_particles); y += 10; Draw_Stringf(x, y, "Uniform uploads: %i", c.uniformUploads); y += 10; + Draw_Stringf(x, y, "Occl. queries : %i", c.occlusionQueries); y += 10; R_SetScale(1.0f); } diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 44d1da066..9e8be1104 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -191,6 +191,7 @@ typedef struct { int rotatedBoxesCulled; int batchesDrawn2D; int uniformUploads; + int occlusionQueries; } statCounters_t; extern statCounters_t c; diff --git a/src/refresh/main.c b/src/refresh/main.c index cb34ae522..f8d99d585 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -422,38 +422,53 @@ static void GL_OccludeFlares(void) vec3_t points[4]; const entity_t *e; glquery_t *q; - int i; + int i, j; + bool set = false; if (!glr.num_flares) return; Q_assert(gl_static.queries); - GL_LoadMatrix(glr.viewmatrix); - GL_StateBits(GLS_DEPTHMASK_FALSE); - GL_ArrayBits(GLA_VERTEX); - qglColorMask(0, 0, 0, 0); - GL_BindTexture(0, TEXNUM_WHITE); - GL_VertexPointer(3, 0, &points[0][0]); - for (i = 0, e = glr.fd.entities; i < glr.fd.num_entities; i++, e++) { if (!(e->flags & RF_FLARE)) continue; q = HashMap_Lookup(glquery_t, gl_static.queries, &e->skinnum); - if (q && q->pending) - continue; + + for (j = 0; j < 4; j++) + if (PlaneDiff(e->origin, &glr.frustumPlanes[j]) < -2.5f) + break; + if (j != 4) { + if (q) + q->pending = q->visible = false; + continue; // not visible + } + + c.occlusionQueries++; if (!q) { glquery_t new = { 0 }; uint32_t map_size = HashMap_Size(gl_static.queries); - if (map_size >= MAX_EDICTS) - continue; + Q_assert(map_size < MAX_EDICTS); qglGenQueries(1, &new.query); Q_assert(!HashMap_Insert(gl_static.queries, &e->skinnum, &new)); q = HashMap_GetValue(glquery_t, gl_static.queries, map_size); } + if (q->pending) + continue; + + if (!set) { + GL_LoadMatrix(glr.viewmatrix); + GL_StateBits(GLS_DEPTHMASK_FALSE); + GL_ArrayBits(GLA_VERTEX); + qglColorMask(0, 0, 0, 0); + GL_BindTexture(0, TEXNUM_WHITE); + GL_VertexPointer(3, 0, &points[0][0]); + set = true; + } + make_flare_quad(e, 2.5f, points); qglBeginQuery(gl_static.samples_passed, q->query); @@ -463,7 +478,8 @@ static void GL_OccludeFlares(void) q->pending = true; } - qglColorMask(1, 1, 1, 1); + if (set) + qglColorMask(1, 1, 1, 1); } static void GL_DrawFlare(const entity_t *e) @@ -475,11 +491,11 @@ static void GL_DrawFlare(const entity_t *e) if (!gl_static.queries) return; + glr.num_flares++; + q = HashMap_Lookup(glquery_t, gl_static.queries, &e->skinnum); - if (!q) { - glr.num_flares++; + if (!q) return; - } if (q->pending) { qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_AVAILABLE, &result); @@ -490,9 +506,6 @@ static void GL_DrawFlare(const entity_t *e) } } - if (!q->pending) - glr.num_flares++; - GL_AdvanceValue(&q->frac, q->visible, 8); if (!q->frac) return; From 1cc68e91dcb40107fc1461427d2bfcd1c22f0bfb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 10 Aug 2024 15:41:06 +0300 Subject: [PATCH 493/974] Delete occlusion queries on map change. --- src/refresh/gl.h | 3 +++ src/refresh/main.c | 29 ++++++++--------------------- src/refresh/surf.c | 4 ++++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 9e8be1104..0382eda71 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -262,6 +262,9 @@ void GL_RotateForEntity(void); void GL_ClearErrors(void); bool GL_ShowErrors(const char *func); +void GL_InitQueries(void); +void GL_DeleteQueries(void); + static inline void GL_AdvanceValue(float *restrict val, float target, float speed) { if (*val < target) { diff --git a/src/refresh/main.c b/src/refresh/main.c index f8d99d585..68d04e335 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1049,10 +1049,11 @@ static void GL_PostInit(void) GL_ClearState(); GL_InitImages(); + GL_InitQueries(); MOD_Init(); } -static void GL_InitQueries(void) +void GL_InitQueries(void) { if (!qglBeginQuery) return; @@ -1061,10 +1062,11 @@ static void GL_InitQueries(void) if (gl_config.ver_gl >= QGL_VER(3, 3) || gl_config.ver_es >= QGL_VER(3, 0)) gl_static.samples_passed = GL_ANY_SAMPLES_PASSED; + Q_assert(!gl_static.queries); gl_static.queries = HashMap_Create(int, glquery_t, HashInt32, NULL); } -static void GL_ShutdownQueries(void) +void GL_DeleteQueries(void) { if (!gl_static.queries) return; @@ -1075,23 +1077,13 @@ static void GL_ShutdownQueries(void) qglDeleteQueries(1, &q->query); } + if (map_size) + Com_DPrintf("%s: %u queries deleted\n", __func__, map_size); + HashMap_Destroy(gl_static.queries); gl_static.queries = NULL; } -static void GL_ClearQueries(void) -{ - if (!gl_static.queries) - return; - - uint32_t map_size = HashMap_Size(gl_static.queries); - for (int i = 0; i < map_size; i++) { - glquery_t *q = HashMap_GetValue(glquery_t, gl_static.queries, i); - q->pending = q->visible = false; - q->frac = 0; - } -} - // ============================================================================== /* @@ -1130,8 +1122,6 @@ bool R_Init(bool total) GL_InitState(); - GL_InitQueries(); - GL_InitTables(); GL_PostInit(); @@ -1160,6 +1150,7 @@ void R_Shutdown(bool total) Com_DPrintf("GL_Shutdown( %i )\n", total); GL_FreeWorld(); + GL_DeleteQueries(); GL_ShutdownImages(); MOD_Shutdown(); @@ -1167,8 +1158,6 @@ void R_Shutdown(bool total) return; } - GL_ShutdownQueries(); - GL_ShutdownState(); // shutdown our QGL subsystem @@ -1231,8 +1220,6 @@ void R_BeginRegistration(const char *name) Q_concat(fullname, sizeof(fullname), "maps/", name, ".bsp"); GL_LoadWorld(fullname); } - - GL_ClearQueries(); } /* diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 07477e948..90e7aa5bf 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -1014,6 +1014,10 @@ void GL_LoadWorld(const char *name) // free previous model, if any GL_FreeWorld(); + // delete occlusion queries + GL_DeleteQueries(); + GL_InitQueries(); + gl_static.world.cache = bsp; // calculate world size for far clip plane and sky box From e03626678669121267e0593277ae55b079b68b1c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 10 Aug 2024 21:48:17 +0300 Subject: [PATCH 494/974] Make flare fading speed configurable. --- doc/client.asciidoc | 4 ++++ src/refresh/gl.h | 4 +++- src/refresh/main.c | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index c5f13bfbb..067c2a9de 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -681,6 +681,10 @@ gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using GLSL backend. Default value is 0 (disabled). +gl_flarespeed:: + Specifies flare fading effect speed. Default value is 8. Set this to 0 + to disable fading. + gl_fontshadow:: Specifies font shadow width, in pixels, ranging from 0 to 2. Default value is 0 (no shadow). diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 0382eda71..0d16c372b 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -267,7 +267,9 @@ void GL_DeleteQueries(void); static inline void GL_AdvanceValue(float *restrict val, float target, float speed) { - if (*val < target) { + if (speed <= 0) { + *val = target; + } else if (*val < target) { *val += speed * glr.frametime; if (*val > target) *val = target; diff --git a/src/refresh/main.c b/src/refresh/main.c index 68d04e335..278cdb216 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -49,6 +49,7 @@ cvar_t *gl_dlight_falloff; cvar_t *gl_modulate_entities; cvar_t *gl_doublelight_entities; cvar_t *gl_glowmap_intensity; +cvar_t *gl_flarespeed; cvar_t *gl_fontshadow; cvar_t *gl_shaders; #if USE_MD5 @@ -506,7 +507,7 @@ static void GL_DrawFlare(const entity_t *e) } } - GL_AdvanceValue(&q->frac, q->visible, 8); + GL_AdvanceValue(&q->frac, q->visible, gl_flarespeed->value); if (!q->frac) return; @@ -928,6 +929,7 @@ static void GL_Register(void) gl_modulate_entities->changed = gl_modulate_entities_changed; gl_doublelight_entities = Cvar_Get("gl_doublelight_entities", "1", 0); gl_glowmap_intensity = Cvar_Get("gl_glowmap_intensity", "0.75", 0); + gl_flarespeed = Cvar_Get("gl_flarespeed", "8", 0); gl_fontshadow = Cvar_Get("gl_fontshadow", "0", 0); gl_shaders = Cvar_Get("gl_shaders", (gl_config.caps & QGL_CAP_SHADER) ? "1" : "0", CVAR_REFRESH); #if USE_MD5 From db9d5fe9f1a495add163ab668fcafc0ec632de6c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Aug 2024 14:10:11 +0300 Subject: [PATCH 495/974] Implement flare batching and outline drawing. --- src/refresh/gl.h | 2 + src/refresh/main.c | 59 ++++-------------------- src/refresh/tess.c | 112 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 0d16c372b..cc6012c0f 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -211,6 +211,7 @@ extern cvar_t *gl_dlight_falloff; extern cvar_t *gl_modulate_entities; extern cvar_t *gl_doublelight_entities; extern cvar_t *gl_glowmap_intensity; +extern cvar_t *gl_flarespeed; extern cvar_t *gl_fontshadow; extern cvar_t *gl_shaders; #if USE_MD5 @@ -695,6 +696,7 @@ extern tesselator_t tess; void GL_Flush2D(void); void GL_DrawParticles(void); void GL_DrawBeams(void); +void GL_DrawFlares(void); void GL_BindArrays(void); void GL_Flush3D(void); diff --git a/src/refresh/main.c b/src/refresh/main.c index 278cdb216..1e0269929 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -428,8 +428,8 @@ static void GL_OccludeFlares(void) if (!glr.num_flares) return; - - Q_assert(gl_static.queries); + if (!gl_static.queries) + return; for (i = 0, e = glr.fd.entities; i < glr.fd.num_entities; i++, e++) { if (!(e->flags & RF_FLARE)) @@ -483,50 +483,6 @@ static void GL_OccludeFlares(void) qglColorMask(1, 1, 1, 1); } -static void GL_DrawFlare(const entity_t *e) -{ - vec3_t points[4]; - GLuint result; - glquery_t *q; - - if (!gl_static.queries) - return; - - glr.num_flares++; - - q = HashMap_Lookup(glquery_t, gl_static.queries, &e->skinnum); - if (!q) - return; - - if (q->pending) { - qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_AVAILABLE, &result); - if (result) { - qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT, &result); - q->visible = result; - q->pending = false; - } - } - - GL_AdvanceValue(&q->frac, q->visible, gl_flarespeed->value); - if (!q->frac) - return; - - GL_LoadMatrix(glr.viewmatrix); - GL_BindTexture(0, IMG_ForHandle(e->skin)->texnum); - GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); - GL_ArrayBits(GLA_VERTEX | GLA_TC); - GL_Color(e->rgba.u8[0] / 255.0f, - e->rgba.u8[1] / 255.0f, - e->rgba.u8[2] / 255.0f, - e->alpha * 0.5f * q->frac); - - make_flare_quad(e, e->scale * 25.0f * q->frac, points); - - GL_TexCoordPointer(2, 0, quad_tc); - GL_VertexPointer(3, 0, &points[0][0]); - qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); -} - static void GL_DrawEntities(int musthave, int canthave) { entity_t *ent, *last; @@ -543,11 +499,14 @@ static void GL_DrawEntities(int musthave, int canthave) glr.num_beams++; continue; } - if ((ent->flags & musthave) != musthave || (ent->flags & canthave)) { + + if (ent->flags & RF_FLARE) { + // flares are drawn elsewhere in single batch + glr.num_flares++; continue; } - if (ent->flags & RF_FLARE) { - GL_DrawFlare(ent); + + if ((ent->flags & musthave) != musthave || (ent->flags & canthave)) { continue; } @@ -740,6 +699,8 @@ void R_RenderFrame(const refdef_t *fd) GL_OccludeFlares(); + GL_DrawFlares(); + if (!(glr.fd.rdflags & RDF_NOWORLDMODEL)) { GL_DrawAlphaFaces(); } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index a97ed269a..406b5feb1 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -290,6 +290,118 @@ void GL_DrawBeams(void) tess.numverts = tess.numindices = 0; } +static void GL_FlushFlares(void) +{ + GL_BindTexture(0, tess.texnum[0]); + GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); + GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + + qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + + if (gl_showtris->integer & BIT(2)) + GL_DrawOutlines(tess.numindices, tess.indices); + + tess.numverts = tess.numindices = 0; +} + +void GL_DrawFlares(void) +{ + vec3_t up, down, left, right; + color_t color; + vec_t *dst_vert; + uint32_t *dst_color; + QGL_INDEX_TYPE *dst_indices; + GLuint result, texnum; + const entity_t *ent; + glquery_t *q; + float scale; + int i; + + if (!glr.num_flares) + return; + if (!gl_static.queries) + return; + + GL_LoadMatrix(glr.viewmatrix); + + GL_VertexPointer(3, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 3); + GL_ColorBytePointer(4, 0, tess.colors); + + for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { + if (!(ent->flags & RF_FLARE)) + continue; + + q = HashMap_Lookup(glquery_t, gl_static.queries, &ent->skinnum); + if (!q) + continue; + + if (q->pending) { + qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_AVAILABLE, &result); + if (result) { + qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT, &result); + q->visible = result; + q->pending = false; + } + } + + GL_AdvanceValue(&q->frac, q->visible, gl_flarespeed->value); + if (!q->frac) + continue; + + texnum = IMG_ForHandle(ent->skin)->texnum; + + if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || + tess.numindices + 6 > TESS_MAX_INDICES) || + (tess.numindices && tess.texnum[0] != texnum)) + GL_FlushFlares(); + + tess.texnum[0] = texnum; + + scale = 25.0f * (ent->scale * q->frac); + + VectorScale(glr.viewaxis[1], scale, left); + VectorScale(glr.viewaxis[1], -scale, right); + VectorScale(glr.viewaxis[2], -scale, down); + VectorScale(glr.viewaxis[2], scale, up); + + dst_vert = tess.vertices + tess.numverts * 5; + + VectorAdd3(ent->origin, down, left, dst_vert); + VectorAdd3(ent->origin, up, left, dst_vert + 5); + VectorAdd3(ent->origin, up, right, dst_vert + 10); + VectorAdd3(ent->origin, down, right, dst_vert + 15); + + dst_vert[ 3] = 0; dst_vert[ 4] = 1; + dst_vert[ 8] = 0; dst_vert[ 9] = 0; + dst_vert[13] = 1; dst_vert[14] = 0; + dst_vert[18] = 1; dst_vert[19] = 1; + + color.u32 = ent->rgba.u32; + color.u8[3] = 128 * (ent->alpha * q->frac); + + dst_color = (uint32_t *)tess.colors + tess.numverts; + dst_color[0] = color.u32; + dst_color[1] = color.u32; + dst_color[2] = color.u32; + dst_color[3] = color.u32; + + dst_indices = tess.indices + tess.numindices; + dst_indices[0] = tess.numverts + 0; + dst_indices[1] = tess.numverts + 2; + dst_indices[2] = tess.numverts + 3; + dst_indices[3] = tess.numverts + 0; + dst_indices[4] = tess.numverts + 1; + dst_indices[5] = tess.numverts + 2; + + tess.numverts += 4; + tess.numindices += 6; + } + + if (tess.numindices) + GL_FlushFlares(); +} + void GL_BindArrays(void) { if (gl_static.world.vertices) { From 6351be0efbd8fed516247ebc8f7838a213e49c4a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Aug 2024 14:10:11 +0300 Subject: [PATCH 496/974] Support drawing beam segment outlines. --- src/refresh/tess.c | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 406b5feb1..d88c79f06 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -151,6 +151,20 @@ void GL_DrawParticles(void) } while (total); } +static void GL_FlushBeamSegments(void) +{ + GL_BindTexture(0, TEXNUM_BEAM); + GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); + GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + + qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + + if (gl_showtris->integer & BIT(2)) + GL_DrawOutlines(tess.numindices, tess.indices); + + tess.numverts = tess.numindices = 0; +} + static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t color, float width) { vec3_t d1, d2, d3; @@ -170,11 +184,8 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col return; if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || - tess.numindices + 6 > TESS_MAX_INDICES)) { - qglDrawElements(GL_TRIANGLES, tess.numindices, - QGL_INDEX_ENUM, tess.indices); - tess.numverts = tess.numindices = 0; - } + tess.numindices + 6 > TESS_MAX_INDICES)) + GL_FlushBeamSegments(); dst_vert = tess.vertices + tess.numverts * 5; VectorAdd(start, d3, dst_vert); @@ -248,46 +259,37 @@ void GL_DrawBeams(void) const entity_t *ent; int i; - if (!glr.num_beams) { + if (!glr.num_beams) return; - } GL_LoadMatrix(glr.viewmatrix); - GL_BindTexture(0, TEXNUM_BEAM); - GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); - GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); GL_VertexPointer(3, 5, tess.vertices); GL_TexCoordPointer(2, 5, tess.vertices + 3); GL_ColorBytePointer(4, 0, tess.colors); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { - if (!(ent->flags & RF_BEAM)) { + if (!(ent->flags & RF_BEAM)) continue; - } start = ent->origin; end = ent->oldorigin; - if (ent->skinnum == -1) { + if (ent->skinnum == -1) color.u32 = ent->rgba.u32; - } else { + else color.u32 = d_8to24table[ent->skinnum & 0xff]; - } color.u8[3] *= ent->alpha; width = abs((int16_t)ent->frame) * 1.2f; - if (ent->flags & RF_GLOW) { + if (ent->flags & RF_GLOW) GL_DrawLightningBeam(start, end, color, width); - } else { + else GL_DrawBeamSegment(start, end, color, width); - } } - qglDrawElements(GL_TRIANGLES, tess.numindices, - QGL_INDEX_ENUM, tess.indices); - tess.numverts = tess.numindices = 0; + GL_FlushBeamSegments(); } static void GL_FlushFlares(void) From ea202a606b133036456f9583c14866b59505cf40 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Aug 2024 14:32:51 +0300 Subject: [PATCH 497/974] Add sybmolic names for showtris constants. --- src/refresh/gl.h | 7 +++++++ src/refresh/mesh.c | 2 +- src/refresh/tess.c | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index cc6012c0f..5c5597687 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -611,6 +611,13 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) #define GL_ColorFloatPointer gl_backend->color_float_pointer #define GL_Color gl_backend->color +enum { + SHOWTRIS_WORLD = BIT(0), + SHOWTRIS_MESH = BIT(1), + SHOWTRIS_PIC = BIT(2), + SHOWTRIS_FX = BIT(3), +}; + void GL_ForceTexture(GLuint tmu, GLuint texnum); void GL_BindTexture(GLuint tmu, GLuint texnum); void GL_CommonStateBits(GLbitfield bits); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 8819d6582..95362349e 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -599,7 +599,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, draw_celshading(indices, num_indices); - if (gl_showtris->integer & BIT(1)) { + if (gl_showtris->integer & SHOWTRIS_MESH) { GL_DrawOutlines(num_indices, indices); } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index d88c79f06..5945acde3 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -55,7 +55,7 @@ void GL_Flush2D(void) qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); - if (gl_showtris->integer & BIT(2)) { + if (gl_showtris->integer & SHOWTRIS_PIC) { GL_DrawOutlines(tess.numindices, tess.indices); } @@ -145,7 +145,7 @@ void GL_DrawParticles(void) qglDrawArrays(GL_TRIANGLES, 0, numverts); - if (gl_showtris->integer & BIT(2)) { + if (gl_showtris->integer & SHOWTRIS_FX) { GL_DrawOutlines(numverts, NULL); } } while (total); @@ -159,7 +159,7 @@ static void GL_FlushBeamSegments(void) qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); - if (gl_showtris->integer & BIT(2)) + if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); tess.numverts = tess.numindices = 0; @@ -300,7 +300,7 @@ static void GL_FlushFlares(void) qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); - if (gl_showtris->integer & BIT(2)) + if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); tess.numverts = tess.numindices = 0; @@ -470,7 +470,7 @@ void GL_Flush3D(void) qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); - if (gl_showtris->integer & BIT(0)) { + if (gl_showtris->integer & SHOWTRIS_WORLD) { GL_DrawOutlines(tess.numindices, tess.indices); } From 102b04b5ca29177e34d127d8f2f93bfa4abdfa54 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 11 Aug 2024 15:58:20 +0300 Subject: [PATCH 498/974] Add typedef for GL capabilities. --- src/refresh/gl.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 5c5597687..ec8f5e714 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -138,7 +138,7 @@ typedef struct { bool framebuffer_ok; } glRefdef_t; -enum { +typedef enum { QGL_CAP_LEGACY = BIT(0), QGL_CAP_SHADER = BIT(1), QGL_CAP_TEXTURE_BITS = BIT(2), @@ -147,7 +147,7 @@ enum { QGL_CAP_TEXTURE_LOD_BIAS = BIT(5), QGL_CAP_TEXTURE_NON_POWER_OF_TWO = BIT(6), QGL_CAP_TEXTURE_ANISOTROPY = BIT(7), -}; +} glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) #define QGL_UNPACK_VER(ver) (ver) / 100, (ver) % 100 @@ -156,7 +156,7 @@ typedef struct { int ver_gl; int ver_es; int ver_sl; - int caps; + glcap_t caps; int colorbits; int depthbits; int stencilbits; From b9f0febbf26102e5d275e64fa6d54faaaecfca20 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 11 Aug 2024 18:10:56 -0400 Subject: [PATCH 499/974] Cleaning up debug statements, adding another check for bot bandaging, bot.skill calculations on Wander.. --- src/action/acesrc/acebot_movement.c | 2 +- src/action/botlib/botlib_communication.c | 45 ++++++++++++++++++++++-- src/action/botlib/botlib_movement.c | 17 +++++++-- src/action/botlib/botlib_spawn.c | 17 ++++----- 4 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index 1539e1b28..d9ab7eccf 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -2841,7 +2841,7 @@ void BOTLIB_GetWeaponsAndAmmo(edict_t* self) //rekkie -- Quake3 -- s void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd) { - if (self->health != self->old_health || self->client->leg_damage) + if ((self->health != self->old_health || self->client->leg_damage) && self->health < 100) { //Com_Printf("%s %s is healing\n", __func__, self->client->pers.netname); diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 93bf5f252..67d8398d0 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -34,7 +34,7 @@ void AddMessageToChatQueue(edict_t* bot, const char* text, int frameReceived) { void ProcessChatQueue(int currentFrame) { for (int i = 0; i < chatQueue.count; i++) { - if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ/HZ) { + if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ) { // Send the chat message from the correct bot BOTLIB_Say(chatQueue.messages[i].bot, chatQueue.messages[i].text, false); //gi.dprintf("Sending delayed chat message from bot: %s\n", chatQueue.messages[i].text); @@ -162,6 +162,42 @@ char *botchat_rage[DBC_RAGE] = "PERKELEEeeeee" }; +// This function is used to mix up chat so that multiple +// phrases can be combined to create a more unique message +// Currently onlt doing this for CHAT_KILLED +char* _chatMix(char* msg, bot_chat_types_t chattype, int randval) +{ + char* text = NULL; + int newrandval = 0; + switch (chattype) { + case CHAT_KILLED: + do { + newrandval = rand() % DBC_KILLEDS; + } while (newrandval == randval); + text = botchat_killeds[newrandval]; + break; + default: + if (debug_mode) + gi.bprintf(PRINT_HIGH, "%s: Unknown chat type %d", __func__, chattype); + return msg; // Just return what was given to us + } + + // Randomize whether to append or prepend + if (rand() % 2 == 0) { + // Prepend + char temp[256]; + snprintf(temp, sizeof(temp), "%s %s", text, msg); + strncpy(msg, temp, sizeof(temp) - 1); + msg[sizeof(temp) - 1] = '\0'; // Ensure null-termination + } else { + // Append + strncat(msg, " ", sizeof(msg) - strlen(msg) - 1); // Add a space before appending the new text + strncat(msg, text, sizeof(msg) - strlen(msg) - 1); + } + + return msg; +} + void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) { // Do nothing if bot_chat is disabled @@ -179,7 +215,8 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) //case CHAT_WELCOME: // CHAT_WELCOME is handled below outside of this switch statement case CHAT_KILLED: - text = botchat_killeds[rand() % DBC_KILLEDS]; + int randval = rand() % DBC_KILLEDS; + text = botchat_killeds[randval]; break; case CHAT_INSULTS: text = botchat_insults[rand() % DBC_INSULTS]; @@ -213,6 +250,10 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) if (bot->bot.lastChatTime > level.framenum - chatInterval) { //gi.dprintf("Skipping chat due to interval limit (%i) needs to be 0 or smaller\n", (bot->bot.lastChatTime - (level.framenum - chatInterval))); return; + } else if (chattype == CHAT_KILLED) { // Mix up the killed messages + if (rand() % 2 == 0) { + text = _chatMix(text, chattype, randval); + } } } // Goodbyes and rages happen without delay diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 4e567a0a3..0e36dd9b8 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5864,10 +5864,9 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->just_spawned_go = true; // Bot is ready, when wander_timeout is reached. // If enemy is in sight, don't wait too long - if (self->enemy) + if (self->enemy) { self->just_spawned_timeout = level.framenum + random() * HZ; // Short wait - else - { + } else if (!bot_personality->value) { // Otherwise pick from various wait times before moving out int rnd_rng = rand() % 4; if (rnd_rng == 0) @@ -5878,6 +5877,18 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->just_spawned_timeout = level.framenum + (random() * 2) * HZ; // Short wait else self->just_spawned_timeout = 0; // No wait + } else { // bot_personality is enabled, let's make it more realistic + int rnd_rng = rand() % 4; + float skill_factor = 1.0f - (self->bot.skill / 10.0f); // Scale factor based on skill (0 to 1) + + if (rnd_rng == 0) + self->just_spawned_timeout = level.framenum + (random() * 10 * skill_factor) * HZ; // Long wait + else if (rnd_rng == 1) + self->just_spawned_timeout = level.framenum + (random() * 5 * skill_factor) * HZ; // Medium wait + else if (rnd_rng == 2) + self->just_spawned_timeout = level.framenum + (random() * 2 * skill_factor) * HZ; // Short wait + else + self->just_spawned_timeout = 0; // No wait } self->bot.bi.actionflags |= ACTION_HOLDPOS; diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 319da8af2..b47b210b3 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1409,31 +1409,32 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); - // + // Loading bot personalities and/or random bots if (bot_personality->value == 2) { - if (rand() % 2) { // Randomly choose between 0 and 1 + if (rand() % 2) { // Randomly choose between 0 and 1, picking a personality or a random bot if (!BOTLIB_SetPersonality(bot, team, force_gender)) { if (pers_debug_mode) - gi.dprintf("Failed to load bot personality, using default userinfo.\n"); + gi.dprintf("%s: bot_personality value %f BOTLIB_SetPersonality() failed to load bot personality.\n", __func__, bot_personality->value); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } - } else if (bot_personality->value == 1) { + } else if (bot_personality->value == 1) { // We want to prioritize bot personalities, then go random if we run out if (bot_personality_index > game.used_bot_personalities) { if (!BOTLIB_SetPersonality(bot, team, force_gender)) { if (pers_debug_mode) - gi.dprintf("Failed to load bot personality, using default userinfo.\n"); + gi.dprintf("%s: bot_personality value %f BOTLIB_SetPersonality() failed to load bot personality.\n", __func__, bot_personality->value); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { if (pers_debug_mode) - gi.dprintf("Ran out of bot personalities, loading random bots now.\n"); + gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } - } else { // Use random data - gi.dprintf("%s: trying to load random bots\n", __func__); + } else { // Use random bot data, no personalities + if (pers_debug_mode) + gi.dprintf("%s: trying to load random bots\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } From 31a78ca15a28a756d058cf1279120384c2da10c0 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 11 Aug 2024 19:37:39 -0400 Subject: [PATCH 500/974] Skill adjustments, crouching behavior, trying to expose bot.skill in status command --- inc/shared/game.h | 2 +- src/action/botlib/botlib_ai.c | 14 ++++++- src/action/botlib/botlib_communication.c | 13 +++--- src/action/botlib/botlib_movement.c | 53 +++++++++++++++++++----- src/action/botlib/botlib_personality.c | 6 +-- src/action/g_save.c | 2 + src/server/commands.c | 5 ++- src/server/main.c | 20 ++++----- src/server/server.h | 7 +--- 9 files changed, 78 insertions(+), 44 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 75eca2945..a248f4f04 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -244,7 +244,7 @@ typedef struct { //rekkie -- surface data -- e //rekkie -- Fake Bot Client -- s - void (*SV_BotUpdateInfo)(char* name, int ping, int score); + void (*SV_BotUpdateInfo)(char* name, int ping, int score, float skill); void (*SV_BotConnect)(char* name); void (*SV_BotDisconnect)(char* name); void (*SV_BotClearClients)(void); diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index bd6229e3e..8c6bade18 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -98,8 +98,18 @@ void BOTLIB_Init(edict_t* self) AssignSkin(self, s, false /* nickChanged */); } - if (bot_personality->value) + // Bot personalities + if (bot_personality->value) { BOTLIB_LoadBotPersonality(self); + + // Bot skill adjustment based on map and other prefs + // Simple adjustments for now + int skillPlus = 0; + skillPlus += self->bot.personality.map_prefs; + self->bot.skill += skillPlus; // This will decrease skill if map_prefs is negative, max -1 + if (self->bot.skill > MAX_BOTSKILL) + self->bot.skill = MAX_BOTSKILL; + } } //rekkie -- Quake3 -- s @@ -658,7 +668,7 @@ void BOTLIB_Think(edict_t* self) self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); + gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score, self->bot.skill); } //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 67d8398d0..995869ff3 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -168,6 +168,8 @@ char *botchat_rage[DBC_RAGE] = char* _chatMix(char* msg, bot_chat_types_t chattype, int randval) { char* text = NULL; + char buffer[1024]; + buffer[0] = '\0'; int newrandval = 0; switch (chattype) { case CHAT_KILLED: @@ -185,17 +187,14 @@ char* _chatMix(char* msg, bot_chat_types_t chattype, int randval) // Randomize whether to append or prepend if (rand() % 2 == 0) { // Prepend - char temp[256]; - snprintf(temp, sizeof(temp), "%s %s", text, msg); - strncpy(msg, temp, sizeof(temp) - 1); - msg[sizeof(temp) - 1] = '\0'; // Ensure null-termination + snprintf(buffer, sizeof(buffer), "%s %s", text, msg); } else { // Append - strncat(msg, " ", sizeof(msg) - strlen(msg) - 1); // Add a space before appending the new text - strncat(msg, text, sizeof(msg) - strlen(msg) - 1); + snprintf(buffer, sizeof(buffer), "%s %s", msg, text); } - return msg; + // Return a dynamically allocated copy of the result + return strdup(buffer); } void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 0e36dd9b8..d7956c65b 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5229,6 +5229,30 @@ void BOTLIB_Look(edict_t* self, usercmd_t* ucmd) } } +void BOTLIB_CrouchFire(edict_t* self) +{ + if (self == NULL || self->client == NULL || self->client->weapon == NULL) { + return; // Early exit if self, client, or weapon is NULL + } + gitem_t* clweapon = self->client->weapon; + + // More skillful bots will crouch when firing + if (self->bot.skill >= 5) { + if ((self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && + (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) + { + // Raptor007: Don't crouch if it blocks the shot. + float old_z = self->s.origin[2]; + self->s.origin[2] -= 14; + if (ACEAI_CheckShot(self)) + { + self->bot.bi.actionflags |= ACTION_CROUCH; + } + self->s.origin[2] = old_z; + } + } +} + void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) { //if (teamplay->value && lights_camera_action > 0) @@ -6853,6 +6877,8 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Try strafing around enemy trace_t tr = gi.trace(self->s.origin, NULL, NULL, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 32), self, MASK_SHOT); + gitem_t* clweapon = self->client->weapon; + if (dodging && tr.plane.normal[2] > 0.85) // Not too steep { // Try strafing continually in a general direction, if possible @@ -6869,7 +6895,7 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->bot_strafe = 0; // Neither - skip strafing this turn } - if (self->client->weapon == FindItem(HC_NAME)) + if (clweapon == FindItem(HC_NAME)) self->bot_strafe = 0; // Don't strafe with HC, just go straight for them if (self->bot_strafe < 0 && random() > 0.15) // Going left 85% of the time @@ -6912,32 +6938,39 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) // Back off if getting too close (unless we have a HC or knife) if (self->bot.enemy_dist < 256) { - if (self->client->weapon == FindItem(HC_NAME) && self->client->cannon_rds) + if (clweapon == FindItem(HC_NAME) && self->client->cannon_rds) { // Come in close for the kill if (ACEMV_CanMove(self, MOVE_FORWARD)) self->bot.bi.actionflags |= ACTION_MOVEFORWARD; } - else if (self->client->weapon == FindItem(KNIFE_NAME)) + else if (clweapon == FindItem(KNIFE_NAME)) { // Come in close for the kill if (ACEMV_CanMove(self, MOVE_FORWARD)) self->bot.bi.actionflags |= ACTION_MOVEFORWARD; - } // Try move backwards - else if (BOTLIB_CanMove(self, MOVE_BACK)) + } else if (BOTLIB_CanMove(self, MOVE_BACK)) { self->bot.bi.actionflags |= ACTION_MOVEBACK; } + + if ((clweapon == FindItem(M4_NAME) || + clweapon == FindItem(MP5_NAME) || + clweapon == FindItem(DUAL_NAME) || + clweapon == FindItem(MK23_NAME)) && + INV_AMMO(self, LASER_NUM) == false) { + BOTLIB_CrouchFire(self); // Utilize crouching for better accuracy + } } // If distance is far, consider crouching to increase accuracy else if (self->bot.enemy_dist > 1024) { // Check if bot should be crouching based on weapon and if strafing if (INV_AMMO(self, LASER_NUM) == false && - self->client->weapon != FindItem(SNIPER_NAME) && - self->client->weapon != FindItem(HC_NAME) && - self->client->weapon != FindItem(M3_NAME) && + clweapon != FindItem(SNIPER_NAME) && + clweapon != FindItem(HC_NAME) && + clweapon != FindItem(M3_NAME) && (self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) { @@ -6955,12 +6988,12 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) else { // Keep distance with sniper - if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(SNIPER_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + if (self->bot.enemy_dist < 1024 && clweapon == FindItemByNum(SNIPER_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) { self->bot.bi.actionflags |= ACTION_MOVEBACK; } // Keep distance with grenade - if (self->bot.enemy_dist < 1024 && self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) + if (self->bot.enemy_dist < 1024 && clweapon == FindItemByNum(GRENADE_NUM) && BOTLIB_CanMove(self, MOVE_BACK)) { self->bot.bi.actionflags |= ACTION_MOVEBACK; } diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index aa94d2db4..8f48b72f4 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -365,7 +365,7 @@ qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) // Handle error: No bot personalities loaded return false; } - + int randomIndex = rand() % bot_personality_index; int attempts = 0; temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; @@ -391,8 +391,6 @@ qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) bot_mappings[randomIndex].personality.isActive = true; self->bot.personality.isActive = true; self->bot.personality.pId = randomIndex; - - gi.dprintf("Random index %i\n", randomIndex); int gender = INVALID; char name[MAX_QPATH]; // Full bot name ( [prefix/clan/rng] and/or [name] and/or [postfix] ) @@ -549,7 +547,7 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { if (all_zeros) { BOTLIB_SmartWeaponSelection(bot); if(pers_debug_mode) - gi.dprintf("%s: chose BOTLIB_SmartWeaponSelection() because weapon_prefs were all zeroes"); + gi.dprintf("%s: chose BOTLIB_SmartWeaponSelection() because weapon_prefs were all zeroes\n", __func__); return; } diff --git a/src/action/g_save.c b/src/action/g_save.c index d7c84e641..3fdeec173 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -328,6 +328,8 @@ void InitGame( void ) InitCommandList(); + Q_srand(time(NULL)); + IRC_init(); gi.dprintf( "==== InitGame ====\n" ); diff --git a/src/server/commands.c b/src/server/commands.c index 6963c8788..5b9fbf50e 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -552,13 +552,14 @@ static void dump_bot_clients(void) Com_Printf( "\n" "Bot Clients.\n" - "num score ping name\n" - "--- ----- ---- ---------------\n"); + "num score ping skill name\n" + "--- ----- ---- ----- ---------------\n"); for (int i = 0; i < MAX_CLIENTS; i++) { if(bot_clients[i].in_use) { Com_Printf("%3i %5i ", bot_clients[i].number, bot_clients[i].score); Com_Printf("%4i ", bot_clients[i].ping); + Com_Printf("%5i ", bot_clients[i].skill); Com_Printf("%-15.15s ", bot_clients[i].name); Com_Printf("\n"); } diff --git a/src/server/main.c b/src/server/main.c index 57c865d53..dd8208cd5 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -112,10 +112,6 @@ cvar_t *g_view_predict; cvar_t *g_view_low; cvar_t *g_view_high; -#ifndef NO_BOTS -cvar_t *sv_bot_ping; // Report bot ping as fake ping values or simply as BOT (0 = ping, 1 = BOT) -#endif - static bool sv_registered; //============================================================================ @@ -469,10 +465,7 @@ static size_t SV_StatusString(char *status) { if (bot_clients[i].in_use) { - if(!sv_bot_ping->integer) - len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] - else - len = Q_snprintf(entry, sizeof(entry), "%i \"%s\" \"%s\"\n", bot_clients[i].score, "BOT", bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] + len = Q_snprintf(entry, sizeof(entry), "%i %i %2f \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].skill, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] if (len >= sizeof(entry)) continue; @@ -1155,10 +1148,11 @@ void SV_BotInit(void) bot_clients[i].ping = 0; bot_clients[i].score = 0; bot_clients[i].number = i; + bot_clients[i].skill = 0; } } // Game DLL updates Server of bot info -void SV_BotUpdateInfo(char* name, int ping, int score) +void SV_BotUpdateInfo(char* name, int ping, int score, float skill) { for (int i = 0; i < MAX_CLIENTS; i++) { @@ -1168,6 +1162,7 @@ void SV_BotUpdateInfo(char* name, int ping, int score) { bot_clients[i].ping = ping; bot_clients[i].score = score; + bot_clients[i].skill = skill; return; } } @@ -1185,6 +1180,7 @@ void SV_BotConnect(char* name) bot_clients[i].ping = 0; bot_clients[i].score = 0; bot_clients[i].number = i; + bot_clients[i].skill = 0; Com_Printf("%s Server added %s as a fake client\n", __func__, bot_clients[i].name); break; } @@ -1201,6 +1197,7 @@ void SV_BotDisconnect(char* name) bot_clients[i].name[0] = 0; bot_clients[i].ping = 0; bot_clients[i].score = 0; + bot_clients[i].skill = 0; Com_Printf("%s Server removed %s as a fake client\n", __func__, name); break; } @@ -1215,6 +1212,7 @@ void SV_BotClearClients(void) bot_clients[i].name[0] = 0; bot_clients[i].ping = 0; bot_clients[i].score = 0; + bot_clients[i].skill = 0; } } //rekkie -- Fake Bot Client -- e @@ -2435,10 +2433,6 @@ void SV_Init(void) sv.frametime = Com_ComputeFrametime(sv.framerate); #endif -#ifndef NO_BOTS - sv_bot_ping = Cvar_Get("sv_bot_ping", "0", 0); -#endif - // set up default pmove parameters PmoveInit(&sv_pmp); diff --git a/src/server/server.h b/src/server/server.h index 8097b7bfc..87f0fcfba 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -586,10 +586,6 @@ extern cvar_t *sv_ghostime; extern client_t *sv_client; extern edict_t *sv_player; -#ifndef NO_BOTS -extern cvar_t *sv_bot_ping; -#endif - //=========================================================== // @@ -892,7 +888,7 @@ trace_t q_gameabi SV_Clip(const vec3_t start, const vec3_t mins, bsp_t* SV_BSP(void); void SV_BotInit(void); -void SV_BotUpdateInfo(char* name, int ping, int score); +void SV_BotUpdateInfo(char* name, int ping, int score, float skill); void SV_BotConnect(char* name); void SV_BotDisconnect(char* name); void SV_BotClearClients(void); @@ -902,6 +898,7 @@ typedef struct bot_client_s { int ping; short score; int number; + float skill; } bot_client_t; extern bot_client_t bot_clients[MAX_CLIENTS]; From b004b0a75547e20dc8b2ce4895880baac1f79c84 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 12 Aug 2024 16:37:38 +0300 Subject: [PATCH 501/974] Fix calculating number of lightning beam segments. --- src/refresh/tess.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 5945acde3..feb5510de 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -224,17 +224,24 @@ static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t c { vec3_t d1, segments[MAX_LIGHTNING_SEGMENTS - 1]; vec_t length; - int i, num_segments = MIN_LIGHTNING_SEGMENTS + GL_rand() % (MAX_LIGHTNING_SEGMENTS - MIN_LIGHTNING_SEGMENTS); + int i, num_segments, max_segments; VectorSubtract(end, start, d1); length = VectorNormalize(d1); - num_segments = min(num_segments, (int)(length / MIN_SEGMENT_LENGTH)); - if (num_segments <= 1) { + max_segments = length / MIN_SEGMENT_LENGTH; + if (max_segments <= 1) { GL_DrawBeamSegment(start, end, color, width); return; } + if (max_segments <= MIN_LIGHTNING_SEGMENTS) { + num_segments = max_segments; + } else { + max_segments = min(max_segments, MAX_LIGHTNING_SEGMENTS); + num_segments = MIN_LIGHTNING_SEGMENTS + GL_rand() % (max_segments - MIN_LIGHTNING_SEGMENTS + 1); + } + for (i = 0; i < num_segments - 1; i++) { int dir = GL_rand() % q_countof(bytedirs); float dist = GL_frand() * 20; From 6cf3e9af2be081653bc1a0fd82dc2b196646ec77 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 12 Aug 2024 20:22:36 +0300 Subject: [PATCH 502/974] Always use tess.verices for temp arrays. --- src/refresh/main.c | 78 +++++++++++++++++++++++++--------------------- src/refresh/sky.c | 13 ++++---- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 1e0269929..0850b2c75 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -90,8 +90,6 @@ cvar_t *gl_showerrors; // ============================================================================== -static const vec_t quad_tc[8] = { 0, 1, 0, 0, 1, 1, 1, 0 }; - static void GL_SetupFrustum(void) { vec_t angle, sf, cf; @@ -339,9 +337,8 @@ static void GL_DrawSpriteModel(const model_t *model) const mspriteframe_t *frame = &model->spriteframes[e->frame % model->numframes]; const image_t *image = frame->image; const float alpha = (e->flags & RF_TRANSLUCENT) ? e->alpha : 1; - int bits = GLS_DEPTHMASK_FALSE; + glStateBits_t bits = GLS_DEPTHMASK_FALSE; vec3_t up, down, left, right; - vec3_t points[4]; if (alpha == 1) { if (image->flags & IF_TRANSPARENT) { @@ -366,13 +363,18 @@ static void GL_DrawSpriteModel(const model_t *model) VectorScale(glr.viewaxis[2], -frame->origin_y, down); VectorScale(glr.viewaxis[2], frame->height - frame->origin_y, up); - VectorAdd3(e->origin, down, left, points[0]); - VectorAdd3(e->origin, up, left, points[1]); - VectorAdd3(e->origin, down, right, points[2]); - VectorAdd3(e->origin, up, right, points[3]); + VectorAdd3(e->origin, down, left, tess.vertices); + VectorAdd3(e->origin, up, left, tess.vertices + 5); + VectorAdd3(e->origin, down, right, tess.vertices + 10); + VectorAdd3(e->origin, up, right, tess.vertices + 15); + + tess.vertices[ 3] = 0; tess.vertices[ 4] = 1; + tess.vertices[ 8] = 0; tess.vertices[ 9] = 0; + tess.vertices[13] = 1; tess.vertices[14] = 1; + tess.vertices[18] = 1; tess.vertices[19] = 0; - GL_TexCoordPointer(2, 0, quad_tc); - GL_VertexPointer(3, 0, &points[0][0]); + GL_VertexPointer(3, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 3); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } @@ -384,43 +386,41 @@ static void GL_DrawNullModel(void) U32_BLUE, U32_BLUE }; const entity_t *e = glr.ent; - vec3_t points[6]; - VectorCopy(e->origin, points[0]); - VectorCopy(e->origin, points[2]); - VectorCopy(e->origin, points[4]); + VectorCopy(e->origin, tess.vertices + 0); + VectorCopy(e->origin, tess.vertices + 6); + VectorCopy(e->origin, tess.vertices + 12); - VectorMA(e->origin, 16, glr.entaxis[0], points[1]); - VectorMA(e->origin, 16, glr.entaxis[1], points[3]); - VectorMA(e->origin, 16, glr.entaxis[2], points[5]); + VectorMA(e->origin, 16, glr.entaxis[0], tess.vertices + 3); + VectorMA(e->origin, 16, glr.entaxis[1], tess.vertices + 9); + VectorMA(e->origin, 16, glr.entaxis[2], tess.vertices + 15); GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); GL_ColorBytePointer(4, 0, (GLubyte *)colors); - GL_VertexPointer(3, 0, &points[0][0]); + GL_VertexPointer(3, 0, tess.vertices); qglDrawArrays(GL_LINES, 0, 6); } -static void make_flare_quad(const entity_t *e, float scale, vec3_t points[4]) +static void make_flare_quad(const entity_t *e, float scale) { vec3_t up, down, left, right; - VectorScale(glr.viewaxis[1], scale, left); + VectorScale(glr.viewaxis[1], scale, left); VectorScale(glr.viewaxis[1], -scale, right); VectorScale(glr.viewaxis[2], -scale, down); - VectorScale(glr.viewaxis[2], scale, up); + VectorScale(glr.viewaxis[2], scale, up); - VectorAdd3(e->origin, down, left, points[0]); - VectorAdd3(e->origin, up, left, points[1]); - VectorAdd3(e->origin, down, right, points[2]); - VectorAdd3(e->origin, up, right, points[3]); + VectorAdd3(e->origin, down, left, tess.vertices + 0); + VectorAdd3(e->origin, up, left, tess.vertices + 3); + VectorAdd3(e->origin, down, right, tess.vertices + 6); + VectorAdd3(e->origin, up, right, tess.vertices + 9); } static void GL_OccludeFlares(void) { - vec3_t points[4]; const entity_t *e; glquery_t *q; int i, j; @@ -466,11 +466,11 @@ static void GL_OccludeFlares(void) GL_ArrayBits(GLA_VERTEX); qglColorMask(0, 0, 0, 0); GL_BindTexture(0, TEXNUM_WHITE); - GL_VertexPointer(3, 0, &points[0][0]); + GL_VertexPointer(3, 0, tess.vertices); set = true; } - make_flare_quad(e, 2.5f, points); + make_flare_quad(e, 2.5f); qglBeginQuery(gl_static.samples_passed, q->query); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); @@ -622,20 +622,26 @@ bool GL_ShowErrors(const char *func) static void GL_WaterWarp(void) { + float x0, x1, y0, y1; + GL_ForceTexture(0, gl_static.warp_texture); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | GLS_TEXTURE_REPLACE | GLS_WARP_ENABLE); GL_ArrayBits(GLA_VERTEX | GLA_TC); - vec_t points[8] = { - glr.fd.x, glr.fd.y, - glr.fd.x, glr.fd.y + glr.fd.height, - glr.fd.x + glr.fd.width, glr.fd.y, - glr.fd.x + glr.fd.width, glr.fd.y + glr.fd.height, - }; + x0 = glr.fd.x; + x1 = glr.fd.x + glr.fd.width; + + y0 = glr.fd.y; + y1 = glr.fd.y + glr.fd.height; + + Vector4Set(tess.vertices, x0, y0, 0, 1); + Vector4Set(tess.vertices + 4, x0, y1, 0, 0); + Vector4Set(tess.vertices + 8, x1, y0, 1, 1); + Vector4Set(tess.vertices + 12, x1, y1, 1, 0); - GL_TexCoordPointer(2, 0, quad_tc); - GL_VertexPointer(2, 0, points); + GL_VertexPointer(2, 4, tess.vertices); + GL_TexCoordPointer(2, 4, tess.vertices + 2); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } diff --git a/src/refresh/sky.c b/src/refresh/sky.c index e80fc2d7b..9976a1234 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -328,7 +328,6 @@ R_DrawSkyBox */ void R_DrawSkyBox(void) { - vec5_t verts[4]; int i; // check for no sky at all @@ -337,8 +336,8 @@ void R_DrawSkyBox(void) GL_StateBits(GLS_TEXTURE_REPLACE); GL_ArrayBits(GLA_VERTEX | GLA_TC); - GL_VertexPointer(3, 5, &verts[0][0]); - GL_TexCoordPointer(2, 5, &verts[0][3]); + GL_VertexPointer(3, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 3); for (i = 0; i < 6; i++) { if (skymins[0][i] >= skymaxs[0][i] || @@ -347,10 +346,10 @@ void R_DrawSkyBox(void) GL_BindTexture(0, sky_images[i]); - MakeSkyVec(skymaxs[0][i], skymins[1][i], i, verts[0]); - MakeSkyVec(skymins[0][i], skymins[1][i], i, verts[1]); - MakeSkyVec(skymaxs[0][i], skymaxs[1][i], i, verts[2]); - MakeSkyVec(skymins[0][i], skymaxs[1][i], i, verts[3]); + MakeSkyVec(skymaxs[0][i], skymins[1][i], i, tess.vertices); + MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); + MakeSkyVec(skymaxs[0][i], skymaxs[1][i], i, tess.vertices + 10); + MakeSkyVec(skymins[0][i], skymaxs[1][i], i, tess.vertices + 15); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } } From f062d3c07f6fc3f6ce3b4929079016fea7171a8f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 12 Aug 2024 13:28:00 -0400 Subject: [PATCH 503/974] Avoided double-dipping PutClientInServer, randomization in loading personalitise --- src/action/botlib/botlib_personality.c | 42 ++++++++++++++------------ src/action/botlib/botlib_spawn.c | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 8f48b72f4..6019a8c90 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -87,6 +87,7 @@ temp_bot_mapping_t* create_new_bot(char* name) { } newBot->personality.skin_pref = NULL; // Initialize to NULL newBot->personality.pId = bot_personality_index++; // Initialize ID for indexing + newBot->personality.isActive = false; // Initialize as inactive return newBot; } @@ -352,11 +353,11 @@ void BOTLIB_LoadBotPersonality(edict_t* self) self->bot.personality.map_prefs = selectedBot->personality.map_prefs; self->bot.personality.combat_demeanor = selectedBot->personality.combat_demeanor; self->bot.personality.chat_demeanor = selectedBot->personality.chat_demeanor; - self->bot.personality.leave_percent = selectedBot->personality.leave_percent; - self->bot.personality.isActive = true; + self->bot.personality.leave_percent = 0; // Initialize to 0, will get updated in RageQuit + self->bot.personality.isActive = true; // Mark as active if(pers_debug_mode) - gi.dprintf("Selected Bot %s(indexes: %i/%i) - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f, Leave Percent: %d\n", selectedBot->name, self->bot.personality.pId, selectedBot->personality.pId, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor, selectedBot->personality.leave_percent); + gi.dprintf("Selected Bot %s(indexes: %i/%i) - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f Now ACTIVE\n", selectedBot->name, self->bot.personality.pId, selectedBot->personality.pId, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor); } qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) @@ -369,23 +370,24 @@ qboolean BOTLIB_SetPersonality(edict_t* self, int team, int force_gender) int randomIndex = rand() % bot_personality_index; int attempts = 0; temp_bot_mapping_t* selectedBot = &bot_mappings[randomIndex]; - - qboolean foundInactiveBot = false; - - while (!foundInactiveBot && attempts < bot_personality_index) { - randomIndex = rand() % bot_personality_index; - if (!bot_mappings[randomIndex].personality.isActive) { - foundInactiveBot = true; - selectedBot = &bot_mappings[randomIndex]; - break; - } - attempts++; - } - - if (!foundInactiveBot) { - // Unable to find an inactive bot - return false; - } + selectedBot = &bot_mappings[randomIndex]; + + // qboolean foundInactiveBot = false; + + // while (!foundInactiveBot && attempts < bot_personality_index) { + // randomIndex = rand() % bot_personality_index; + // if (!bot_mappings[randomIndex].personality.isActive) { + // foundInactiveBot = true; + // selectedBot = &bot_mappings[randomIndex]; + // break; + // } + // attempts++; + // } + + // if (!foundInactiveBot) { + // // Unable to find an inactive bot + // return false; + // } // Mark the selected bot as active in both loaded and live structs bot_mappings[randomIndex].personality.isActive = true; diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index b47b210b3..97af023fd 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1375,7 +1375,7 @@ void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) bot->think = BOTLIB_Think; bot->nextthink = level.framenum + 1; - PutClientInServer(bot); + //PutClientInServer(bot); JoinTeam(bot, team, true); } From b547feeacd2dbb5c79961c4e715e8b135c0403af Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 12 Aug 2024 15:07:07 -0400 Subject: [PATCH 504/974] Printing weapon/item names when bots spawn, testing CHAT_WELCOME --- src/action/a_game.c | 54 ++++++++++++++++++++++++ src/action/botlib/botlib_communication.c | 10 +++-- src/action/botlib/botlib_personality.c | 10 ++--- src/action/g_local.h | 2 + src/action/p_client.c | 11 +++++ 5 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 07eedd1ee..e44bbb6b1 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1264,6 +1264,60 @@ void AddSplat(edict_t * self, vec3_t point, trace_t * tr) gi.linkentity(splat); } +const char* PrintWeaponName( int weapon ) +{ + switch( weapon ) + { + case MK23_NUM: + return MK23_NAME; + case MP5_NUM: + return MP5_NAME; + case M4_NUM: + return M4_NAME; + case M3_NUM: + return M3_NAME; + case HC_NUM: + return HC_NAME; + case SNIPER_NUM: + return SNIPER_NAME; + case DUAL_NUM: + return DUAL_NAME; + case KNIFE_NUM: + return KNIFE_NAME; + case GRENADE_NUM: + return GRENADE_NAME; + default: + return "unknown"; + } +} + +const char* PrintItemName( int item ) +{ + switch( item ) + { + case KEV_NUM: + return KEV_NAME; + case LASER_NUM: + return LASER_NAME; + case SLIP_NUM: + return SLIP_NAME; + case SIL_NUM: + return SIL_NAME; + case BAND_NUM: + return BAND_NAME; + case HELM_NUM: + return HELM_NAME; + case C_KIT_NUM: + return C_KIT_NAME; + case S_KIT_NUM: + return S_KIT_NAME; + case A_KIT_NUM: + return A_KIT_NAME; + default: + return "unknown"; + } +} + /* %-variables for chat msgs */ void GetWeaponName( edict_t *ent, char *buf ) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 995869ff3..586c72e3c 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -34,7 +34,7 @@ void AddMessageToChatQueue(edict_t* bot, const char* text, int frameReceived) { void ProcessChatQueue(int currentFrame) { for (int i = 0; i < chatQueue.count; i++) { - if (currentFrame > chatQueue.messages[i].frameReceived + 4 * HZ) { + if (currentFrame > chatQueue.messages[i].frameReceived + (rand() % HZ) + 4 * HZ) { // Send the chat message from the correct bot BOTLIB_Say(chatQueue.messages[i].bot, chatQueue.messages[i].text, false); //gi.dprintf("Sending delayed chat message from bot: %s\n", chatQueue.messages[i].text); @@ -55,7 +55,7 @@ void UpdateBotChat(void) { } // Function to get a random bot -edict_t* getRandomBot() +edict_t* getRandomBot(void) { edict_t* bots[MAX_CLIENTS]; int botCount = 0; @@ -208,7 +208,6 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) // bot_is_target means the bot edict provided to this function is not the talker, but the bot the other // bots will talk to, such as 'Welcome !'. Bot chosen to speak will be random from the list // of current/inuse bots - qboolean bot_is_target = false; switch (chattype) { //case CHAT_WELCOME: @@ -268,8 +267,13 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) int index = rand() % DBC_WELCOMES; // Choose a random message index char message[256]; // Assuming 256 bytes is enough for the message snprintf(message, sizeof(message), botchat_welcomes[index], bot->client->pers.netname); + + // Ensure 'text' buffer is large enough + char text[256]; // Increase the size of 'text' to match 'message' + // Copy back to 'text' snprintf(text, sizeof(text), "%s", message); + if (getRandomBot() != NULL) bot = getRandomBot(); } diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 6019a8c90..2f42ac8ee 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -622,13 +622,13 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) // If this is all zeroes, we're picking a random if we haven't selected an item yet if (all_zeros && bot->client->selected_item < 1) { chosen_item_index = rand() % 6; // Randomly choose one of the 6 items - ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index); + ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index + 10); // Map index to item number if(pers_debug_mode) - gi.dprintf("%s: chose random item because item_prefs were all zeroes"); + gi.dprintf("%s: chose random item because item_prefs were all zeroes", bot->client->pers.netname); return; } - for (int i = 0; i < ITEM_COUNT; i++) { // Adjust loop for 6 items + for (int i = 0; i < 6; i++) { // Adjust loop for 6 items float current_pref = item_prefs[i]; for (int j = 0; j < 3; j++) { if (current_pref > top3_prefs[j]) { @@ -650,7 +650,7 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) // Case 1: Always pick the top item if (randomIndex == 0) { - chosenItem = top3_indices[0]; + chosenItem = top3_indices[0] + 10; // Map index to item number ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); if (pers_debug_mode) gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); @@ -659,7 +659,7 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) // Case 2: Randomly choose from the top 3 else { int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 - chosenItem = top3_indices[randomChoice]; + chosenItem = top3_indices[randomChoice] + 10; // Map index to item number ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); if (pers_debug_mode) gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); diff --git a/src/action/g_local.h b/src/action/g_local.h index df997d631..da8da91c8 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2702,6 +2702,8 @@ void Bandage (edict_t * ent); void ShowGun (edict_t * ent); // hentai's vwep function added by zucc void FL_think (edict_t * self); // TNG Flashlight void FL_make (edict_t * self); // TNG Flashlight +const char* PrintWeaponName( int weapon ); +const char* PrintItemName( int item ); // spec functions void SetupSpecSpawn (void); diff --git a/src/action/p_client.c b/src/action/p_client.c index 9c3d50700..9c24607e4 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3055,6 +3055,11 @@ void PutClientInServer(edict_t * ent) else BOTLIB_BotPersonalityChooseItem(ent); } + gi.dprintf("Bot %s has chosen %s and %s\n", + ent->client->pers.netname, + PrintWeaponName(ent->client->pers.chosenWeapon->typeNum), + PrintItemName(ent->client->pers.chosenItem->typeNum) + ); } else // LTK bots { @@ -3632,6 +3637,12 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) //set connected on ClientBeginDeathmatch as clientconnect doesn't always //guarantee a client is actually making it all the way into the game. //ent->client->pers.connected = true; + + #ifndef NO_BOTS + if(bot_chat->value) + BOTLIB_Chat(ent, CHAT_WELCOME); + #endif + return true; } From 8a12ba251244d82ccc5cae4938426e2652d0c429 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 12 Aug 2024 15:32:40 -0400 Subject: [PATCH 505/974] Fixing potential memory leak --- src/action/botlib/botlib_personality.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 2f42ac8ee..b1d75fc76 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -253,6 +253,8 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) } free(newBot); // Free the temporary newBot since it's now copied to bot_mappings + json_decref(botDetails); + json_decref(root); } json_decref(root); From 47733cfadd8008fc3e129d86f922d828e882de0d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 12 Aug 2024 17:27:37 -0400 Subject: [PATCH 506/974] Dont count bots in voting --- src/action/a_vote.c | 33 +++++++++++++++++++++----- src/action/botlib/botlib_personality.c | 2 -- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/action/a_vote.c b/src/action/a_vote.c index e16024d07..ea58f348c 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -126,6 +126,27 @@ int _numclients (void) return count; } +// The difference between this one and _numclients is that bots don't vote, so don't count them +int _numvoteclients (void) +{ + int count, i; + edict_t *other; + + count = 0; + for (i = 0, other = g_edicts + 1; i < game.maxclients; i++, other++) + { + if (!other->inuse || !other->client || !other->client->pers.connected || other->client->pers.mvdspec) + continue; +#ifndef NO_BOTS + if (other->is_bot) + continue; +#endif + + count++; + } + return count; +} + /* Kicks the given (client) edict out of the server, reason will be printed before */ @@ -553,7 +574,7 @@ votelist_t *MapWithMostVotes (float *p) return (NULL); //find map_num_clients - map_num_clients = _numclients(); + map_num_clients = _numvoteclients(); if (map_num_clients == 0) return (NULL); @@ -586,7 +607,7 @@ votelist_t *MapWithMostAllVotes( void ) votelist_t *search = NULL, *most = NULL; int highest_total = 0; - if( ! _numclients() ) + if( ! _numvoteclients() ) return NULL; for( search = map_votes; search != NULL; search = search->next ) @@ -990,7 +1011,7 @@ void _CheckKickVote (void) return; kickvotechanged = false; - playernum = _numclients (); + playernum = _numvoteclients (); maxvotes = 0; mtarget = NULL; @@ -1191,7 +1212,7 @@ void Cmd_Kicklist_f(edict_t *ent) "kicked players %s be temporarily banned.\n\n", (int) (kickvote_min->value), (kickvote_min->value == 1) ? "" : "s", - _numclients(), + _numvoteclients(), kickvote_need->value, Allkickvotes, kickvote_pass->value, Mostkickpercent, Mostkickvotes == NULL ? "nobody" : Mostkickvotes->client->pers.netname, @@ -1524,7 +1545,7 @@ configlist_t *ConfigWithMostVotes (float *p) return (NULL); //find config_num_clients - config_num_clients = _numclients(); + config_num_clients = _numvoteclients(); if (config_num_clients == 0) return (NULL); @@ -2093,7 +2114,7 @@ void _CalcScrambleVotes (int *numclients, int *numvotes, float *percent) int i; edict_t *ent; - *numclients = _numclients (); + *numclients = _numvoteclients(); *numvotes = 0; *percent = 0.00f; diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index b1d75fc76..2f42ac8ee 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -253,8 +253,6 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) } free(newBot); // Free the temporary newBot since it's now copied to bot_mappings - json_decref(botDetails); - json_decref(root); } json_decref(root); From bf0cf7f3dec0d7531d77006ae3b65f07796899bc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 11:40:37 +0300 Subject: [PATCH 507/974] Don't flush empty beam segments. --- src/refresh/tess.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index feb5510de..ac7f407e3 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -153,6 +153,9 @@ void GL_DrawParticles(void) static void GL_FlushBeamSegments(void) { + if (!tess.numindices) + return; + GL_BindTexture(0, TEXNUM_BEAM); GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); From baba75d3469431278e2dbb48c7e7167b641fc71f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 11:40:39 +0300 Subject: [PATCH 508/974] Add missing array locking. --- src/refresh/main.c | 8 ++++++++ src/refresh/mesh.c | 2 ++ src/refresh/sky.c | 2 ++ src/refresh/tess.c | 11 +++++++++++ 4 files changed, 23 insertions(+) diff --git a/src/refresh/main.c b/src/refresh/main.c index 0850b2c75..b9d2dd7a9 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -375,7 +375,9 @@ static void GL_DrawSpriteModel(const model_t *model) GL_VertexPointer(3, 5, tess.vertices); GL_TexCoordPointer(2, 5, tess.vertices + 3); + GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + GL_UnlockArrays(); } static void GL_DrawNullModel(void) @@ -401,7 +403,9 @@ static void GL_DrawNullModel(void) GL_ArrayBits(GLA_VERTEX | GLA_COLOR); GL_ColorBytePointer(4, 0, (GLubyte *)colors); GL_VertexPointer(3, 0, tess.vertices); + GL_LockArrays(6); qglDrawArrays(GL_LINES, 0, 6); + GL_UnlockArrays(); } static void make_flare_quad(const entity_t *e, float scale) @@ -472,9 +476,11 @@ static void GL_OccludeFlares(void) make_flare_quad(e, 2.5f); + GL_LockArrays(4); qglBeginQuery(gl_static.samples_passed, q->query); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); qglEndQuery(gl_static.samples_passed); + GL_UnlockArrays(); q->pending = true; } @@ -642,7 +648,9 @@ static void GL_WaterWarp(void) GL_VertexPointer(2, 4, tess.vertices); GL_TexCoordPointer(2, 4, tess.vertices + 2); + GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + GL_UnlockArrays(); } void R_RenderFrame(const refdef_t *fd) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 95362349e..f89209e36 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -559,9 +559,11 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_ArrayBits(GLA_VERTEX); GL_BindTexture(0, TEXNUM_WHITE); GL_VertexPointer(3, dotshading ? VERTEX_SIZE : 4, tess.vertices); + GL_LockArrays(num_verts); qglColorMask(0, 0, 0, 0); qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices); qglColorMask(1, 1, 1, 1); + GL_UnlockArrays(); } if (dotshading) diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 9976a1234..fa5a7e609 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -350,7 +350,9 @@ void R_DrawSkyBox(void) MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); MakeSkyVec(skymaxs[0][i], skymaxs[1][i], i, tess.vertices + 10); MakeSkyVec(skymins[0][i], skymaxs[1][i], i, tess.vertices + 15); + GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + GL_UnlockArrays(); } } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index ac7f407e3..9b0985584 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -143,11 +143,14 @@ void GL_DrawParticles(void) p++; } while (--count); + GL_LockArrays(numverts); qglDrawArrays(GL_TRIANGLES, 0, numverts); if (gl_showtris->integer & SHOWTRIS_FX) { GL_DrawOutlines(numverts, NULL); } + + GL_UnlockArrays(); } while (total); } @@ -160,11 +163,15 @@ static void GL_FlushBeamSegments(void) GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + GL_LockArrays(tess.numverts); + qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); + GL_UnlockArrays(); + tess.numverts = tess.numindices = 0; } @@ -308,11 +315,15 @@ static void GL_FlushFlares(void) GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + GL_LockArrays(tess.numverts); + qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); + GL_UnlockArrays(); + tess.numverts = tess.numindices = 0; } From 8f447127ba80f5d4efe82e93faa263b6569b73c0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 11:40:39 +0300 Subject: [PATCH 509/974] Pre-multiply stride in GL_*Pointer() macros. --- src/refresh/gl.h | 22 ++++++++++++++++------ src/refresh/legacy.c | 10 +++++----- src/refresh/shader.c | 10 +++++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index ec8f5e714..44996f6dd 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -604,12 +604,22 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) qglDepthRange(n, f); } -#define GL_VertexPointer gl_backend->vertex_pointer -#define GL_TexCoordPointer gl_backend->tex_coord_pointer -#define GL_LightCoordPointer gl_backend->light_coord_pointer -#define GL_ColorBytePointer gl_backend->color_byte_pointer -#define GL_ColorFloatPointer gl_backend->color_float_pointer -#define GL_Color gl_backend->color +#define GL_VertexPointer(size, stride, ptr) \ + gl_backend->vertex_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + +#define GL_TexCoordPointer(size, stride, ptr) \ + gl_backend->tex_coord_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + +#define GL_LightCoordPointer(size, stride, ptr) \ + gl_backend->light_coord_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + +#define GL_ColorBytePointer(size, stride, ptr) \ + gl_backend->color_byte_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + +#define GL_ColorFloatPointer(size, stride, ptr) \ + gl_backend->color_float_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + +#define GL_Color(r, g, b, a) gl_backend->color(r, g, b, a) enum { SHOWTRIS_WORLD = BIT(0), diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index bcc30c6da..ba5556d2b 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -127,29 +127,29 @@ static void legacy_array_bits(GLbitfield bits) static void legacy_vertex_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglVertexPointer(size, GL_FLOAT, sizeof(GLfloat) * stride, pointer); + qglVertexPointer(size, GL_FLOAT, stride, pointer); } static void legacy_tex_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { GL_ClientActiveTexture(0); - qglTexCoordPointer(size, GL_FLOAT, sizeof(GLfloat) * stride, pointer); + qglTexCoordPointer(size, GL_FLOAT, stride, pointer); } static void legacy_light_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { GL_ClientActiveTexture(1); - qglTexCoordPointer(size, GL_FLOAT, sizeof(GLfloat) * stride, pointer); + qglTexCoordPointer(size, GL_FLOAT, stride, pointer); } static void legacy_color_byte_pointer(GLint size, GLsizei stride, const GLubyte *pointer) { - qglColorPointer(size, GL_UNSIGNED_BYTE, sizeof(GLfloat) * stride, pointer); + qglColorPointer(size, GL_UNSIGNED_BYTE, stride, pointer); } static void legacy_color_float_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglColorPointer(size, GL_FLOAT, sizeof(GLfloat) * stride, pointer); + qglColorPointer(size, GL_FLOAT, stride, pointer); } static void legacy_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 752ef3870..26b7e929b 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -324,27 +324,27 @@ static void shader_array_bits(GLbitfield bits) static void shader_vertex_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglVertexAttribPointer(VERT_ATTR_POS, size, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * stride, pointer); + qglVertexAttribPointer(VERT_ATTR_POS, size, GL_FLOAT, GL_FALSE, stride, pointer); } static void shader_tex_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglVertexAttribPointer(VERT_ATTR_TC, size, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * stride, pointer); + qglVertexAttribPointer(VERT_ATTR_TC, size, GL_FLOAT, GL_FALSE, stride, pointer); } static void shader_light_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglVertexAttribPointer(VERT_ATTR_LMTC, size, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * stride, pointer); + qglVertexAttribPointer(VERT_ATTR_LMTC, size, GL_FLOAT, GL_FALSE, stride, pointer); } static void shader_color_byte_pointer(GLint size, GLsizei stride, const GLubyte *pointer) { - qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(GLfloat) * stride, pointer); + qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_UNSIGNED_BYTE, GL_TRUE, stride, pointer); } static void shader_color_float_pointer(GLint size, GLsizei stride, const GLfloat *pointer) { - qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * stride, pointer); + qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_FLOAT, GL_FALSE, stride, pointer); } static void shader_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) From b868a70c50f07533d90ee0432b2b8209ba6f7d99 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 07:55:57 -0400 Subject: [PATCH 510/974] Fixed label issue with botlib comms --- meson.build | 2 +- src/action/botlib/botlib_communication.c | 3 ++- src/action/botlib/botlib_personality.c | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index 8f4ffe77c..adf362ddb 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=debug', + 'buildtype=release', ], ) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 586c72e3c..c7b496224 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -213,7 +213,8 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) //case CHAT_WELCOME: // CHAT_WELCOME is handled below outside of this switch statement case CHAT_KILLED: - int randval = rand() % DBC_KILLEDS; + int randval = 0; + randval = rand() % DBC_KILLEDS; text = botchat_killeds[randval]; break; case CHAT_INSULTS: diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 2f42ac8ee..9562f76f6 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -4,7 +4,7 @@ #include // Count of bot personalities loaded -qboolean pers_debug_mode = false; +qboolean pers_debug_mode = true; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create temp_bot_mapping_t bot_mappings[MAX_BOTS]; From 0ac63e9b6e045935abd2ec3ecf56e0a274064d7d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 07:58:39 -0400 Subject: [PATCH 511/974] Fixed label issue with botlib comms --- meson.build | 2 +- src/action/botlib/botlib_communication.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index adf362ddb..8f4ffe77c 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=release', + 'buildtype=debug', ], ) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index c7b496224..9e5e6af10 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -209,11 +209,11 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) // bots will talk to, such as 'Welcome !'. Bot chosen to speak will be random from the list // of current/inuse bots + int randval = 0; switch (chattype) { //case CHAT_WELCOME: // CHAT_WELCOME is handled below outside of this switch statement case CHAT_KILLED: - int randval = 0; randval = rand() % DBC_KILLEDS; text = botchat_killeds[randval]; break; From 5cb5dd0e18f996fa88e72f6d6765acd4c956df83 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 08:01:09 -0400 Subject: [PATCH 512/974] Fixed label issue with botlib comms --- src/action/botlib/botlib_communication.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 9e5e6af10..b0b319b78 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -209,13 +209,13 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) // bots will talk to, such as 'Welcome !'. Bot chosen to speak will be random from the list // of current/inuse bots - int randval = 0; + int killedsrandval = 0; switch (chattype) { //case CHAT_WELCOME: // CHAT_WELCOME is handled below outside of this switch statement case CHAT_KILLED: - randval = rand() % DBC_KILLEDS; - text = botchat_killeds[randval]; + killedsrandval = rand() % DBC_KILLEDS; + text = botchat_killeds[killedsrandval]; break; case CHAT_INSULTS: text = botchat_insults[rand() % DBC_INSULTS]; From eae3205d7469f56548fd39ffd07fb725343dffc8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 08:18:03 -0400 Subject: [PATCH 513/974] Turned debug mode off --- src/action/botlib/botlib_personality.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 9562f76f6..2f42ac8ee 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -4,7 +4,7 @@ #include // Count of bot personalities loaded -qboolean pers_debug_mode = true; +qboolean pers_debug_mode = false; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create temp_bot_mapping_t bot_mappings[MAX_BOTS]; From 420905cab55acc8439676f40013ba27875d3af69 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 09:14:47 -0400 Subject: [PATCH 514/974] Added bot_debug as a cvar --- meson.build | 2 +- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib.h | 1 - src/action/botlib/botlib_movement.c | 4 ++-- src/action/botlib/botlib_personality.c | 29 +++++++++++++------------- src/action/botlib/botlib_spawn.c | 12 +++++------ src/action/g_main.c | 1 + src/action/g_save.c | 1 + 8 files changed, 26 insertions(+), 25 deletions(-) diff --git a/meson.build b/meson.build index 8f4ffe77c..adf362ddb 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=debug', + 'buildtype=release', ], ) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 4bb5573f0..acace1d59 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -546,6 +546,7 @@ extern cvar_t* bot_chat; extern cvar_t* bot_personality; extern cvar_t* bot_ragequit; extern cvar_t* bot_teamplay; +extern cvar_t* bot_debug; //extern cvar_t* bot_randteamskin; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 89fef1a2d..460cf43ee 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -553,5 +553,4 @@ qboolean BOTLIB_SpawnRush(edict_t* bot); extern int loaded_bot_personalities; extern int bot_personality_index; -extern qboolean pers_debug_mode; #endif // _BOTLIB_H \ No newline at end of file diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index d7956c65b..bf6fb81ed 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -6358,14 +6358,14 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) if (self->bot.next_node == INVALID) { - if(pers_debug_mode) + if(bot_debug->value) Com_Printf("%s %s invalid next_node node:curr/next/goal[%d %d %d] type:curr/next[%d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node, current_node_type, next_node_type); self->bot.state = BOT_MOVE_STATE_NAV; return; } if (self->bot.current_node == self->bot.next_node && self->bot.current_node == self->bot.goal_node) { - if(pers_debug_mode) + if(bot_debug->value) Com_Printf("%s %s reached goal node:curr/next/goal[%d %d %d]\n", __func__, self->client->pers.netname, self->bot.current_node, self->bot.next_node, self->bot.goal_node); ////if (nav_area.total_areas > 0) //// self->bot.state = BOT_MOVE_STATE_NAV_NEXT; diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 2f42ac8ee..7a4972054 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -4,7 +4,6 @@ #include // Count of bot personalities loaded -qboolean pers_debug_mode = false; int loaded_bot_personalities = 0; int bot_personality_index = 0; // We're incrementing as we create temp_bot_mapping_t bot_mappings[MAX_BOTS]; @@ -139,12 +138,12 @@ void UpdateMapPref(json_t* root, char* map_name, temp_bot_mapping_t* newBot) if (value && json_is_real(value)) { // Update the newBot struct with the fetched value newBot->personality.map_prefs = (float)json_real_value(value); - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("Updated map preference for '%s' to %f.\n", map_name, newBot->personality.map_prefs); } else { // If the map name is not found or the value is not a real number, default to 0.0f newBot->personality.map_prefs = 0.0f; - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("Map '%s' not found or invalid. Defaulting to %f.\n", map_name, newBot->personality.map_prefs); } } @@ -224,19 +223,19 @@ temp_bot_mapping_t* BOTLIB_LoadPersonalities(const char* filename) if (validatedSkinPref != NULL) { newBot->personality.skin_pref = validatedSkinPref; } else { - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); newBot->personality.skin_pref = "male/grunt"; } } else { - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: warning: skin object missing from %s\n", __func__, botName); newBot->personality.skin_pref = "male/grunt"; } bot_mappings[botIndex++] = *newBot; - if (pers_debug_mode) { + if (bot_debug->value) { gi.dprintf("Loaded bot %s\n", newBot->name); gi.dprintf("Weapon Preferences:\n"); for (size_t i = 0; i < WEAPON_COUNT; i++) { @@ -356,7 +355,7 @@ void BOTLIB_LoadBotPersonality(edict_t* self) self->bot.personality.leave_percent = 0; // Initialize to 0, will get updated in RageQuit self->bot.personality.isActive = true; // Mark as active - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("Selected Bot %s(indexes: %i/%i) - Weapon Pref[0]: %f, Item Pref[0]: %f, Map Pref: %f, Combat Demeanor: %f, Chat Demeanor: %f Now ACTIVE\n", selectedBot->name, self->bot.personality.pId, selectedBot->personality.pId, selectedBot->personality.weapon_prefs[0], selectedBot->personality.item_prefs[0], selectedBot->personality.map_prefs, selectedBot->personality.combat_demeanor, selectedBot->personality.chat_demeanor); } @@ -486,7 +485,7 @@ void BOTLIB_FreeBotPersonality(edict_t* bot) if (bot_mappings[i].personality.pId == bot_pId && bot_mappings[i].personality.isActive) { bot_mappings[i].personality.isActive = false; game.used_bot_personalities--; - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("%s: Freed up %s\n", __func__, bot->client->pers.netname); break; // Exit the loop once the bot is found and deactivated } @@ -548,7 +547,7 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { // All zeroes somehow? Still, let's pick a good weapon if (all_zeros) { BOTLIB_SmartWeaponSelection(bot); - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("%s: chose BOTLIB_SmartWeaponSelection() because weapon_prefs were all zeroes\n", __func__); return; } @@ -578,7 +577,7 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { if (randomIndex == 0) { chosenWeapon = top3_indices[0]; ACEAI_Cmd_Choose_Weapon_Num(bot, chosenWeapon); - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: %s chose %i weapon\n", __func__, bot->client->pers.netname, chosenWeapon); return; } @@ -587,7 +586,7 @@ void BOTLIB_BotPersonalityChooseWeapon(edict_t* bot) { int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 chosenWeapon = top3_indices[randomChoice]; ACEAI_Cmd_Choose_Weapon_Num(bot, chosenWeapon); - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: %s chose %i weapon\n", __func__, bot->client->pers.netname, chosenWeapon); return; } @@ -623,7 +622,7 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) if (all_zeros && bot->client->selected_item < 1) { chosen_item_index = rand() % 6; // Randomly choose one of the 6 items ACEAI_Cmd_Choose_Item_Num(bot, chosen_item_index + 10); // Map index to item number - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("%s: chose random item because item_prefs were all zeroes", bot->client->pers.netname); return; } @@ -652,7 +651,7 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) if (randomIndex == 0) { chosenItem = top3_indices[0] + 10; // Map index to item number ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); return; } @@ -661,7 +660,7 @@ void BOTLIB_BotPersonalityChooseItem(edict_t* bot) int randomChoice = rand() % 3; // Randomly choose 0, 1, or 2 chosenItem = top3_indices[randomChoice] + 10; // Map index to item number ACEAI_Cmd_Choose_Item_Num(bot, chosenItem); - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: %s chose %i item\n", __func__, bot->client->pers.netname, chosenItem); return; } @@ -761,7 +760,7 @@ qboolean BOTLIB_SpawnRush(edict_t* bot) { bool isRushing = randomValue <= probabilityThreshold; // Decide based on the probability threshold - if (pers_debug_mode) { + if (bot_debug->value) { gi.dprintf("%s: %s's probabilityThreshold = %f, randomValue = %f, isRushing = %s\n", __func__, bot->client->pers.netname, diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 97af023fd..2ad712b51 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1401,11 +1401,11 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for bot_connections.total_team3++; if(bot_personality->value && (bot_personality_index == game.used_bot_personalities)) { - if(pers_debug_mode) + if(bot_debug->value) gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); DeactivateBotPersonality(); } - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: trying to load personalities, loaded: %i used: %i\n", __func__, loaded_bot_personalities, game.used_bot_personalities); @@ -1413,7 +1413,7 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for if (bot_personality->value == 2) { if (rand() % 2) { // Randomly choose between 0 and 1, picking a personality or a random bot if (!BOTLIB_SetPersonality(bot, team, force_gender)) { - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: bot_personality value %f BOTLIB_SetPersonality() failed to load bot personality.\n", __func__, bot_personality->value); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } @@ -1423,17 +1423,17 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for } else if (bot_personality->value == 1) { // We want to prioritize bot personalities, then go random if we run out if (bot_personality_index > game.used_bot_personalities) { if (!BOTLIB_SetPersonality(bot, team, force_gender)) { - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: bot_personality value %f BOTLIB_SetPersonality() failed to load bot personality.\n", __func__, bot_personality->value); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: Ran out of bot personalities, loading random bots now.\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } } else { // Use random bot data, no personalities - if (pers_debug_mode) + if (bot_debug->value) gi.dprintf("%s: trying to load random bots\n", __func__); BOTLIB_SetUserinfo(bot, team, force_gender, force_name, force_skin); } diff --git a/src/action/g_main.c b/src/action/g_main.c index 78d56f7a9..3cc610977 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -497,6 +497,7 @@ cvar_t* bot_chat; // Enable generic bot chat cvar_t* bot_personality; // Enable bot personality functionality [0 disable, 1 enable, 2 mixed with random bots] cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) cvar_t* bot_teamplay; // Allows bots to play teamplay games without humans (see a_vote.c _numclients() ) +cvar_t* bot_debug; // Enable bot debug mode //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 3fdeec173..15663c9e2 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -699,6 +699,7 @@ void InitGame( void ) bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); bot_ragequit = gi.cvar("bot_ragequit", "0", 0); bot_teamplay = gi.cvar("bot_teamplay", "0", 0); + bot_debug = gi.cvar("bot_debug", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From f468f06a1f140c56148ab12476e44817fe495f96 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 11:26:07 -0400 Subject: [PATCH 515/974] Fixed bot skill status values, reduced randvoice default --- src/action/g_save.c | 2 +- src/server/commands.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index 15663c9e2..84b59c72b 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -692,7 +692,7 @@ void InitGame( void ) bot_showpath = gi.cvar("bot_showpath", "0", 0); bot_maxteam = gi.cvar("bot_maxteam", "0", 0); bot_rush = gi.cvar("bot_rush", "0", 0); - bot_randvoice = gi.cvar("bot_randvoice", "33", 0); + bot_randvoice = gi.cvar("bot_randvoice", "5", 0); bot_randskill = gi.cvar("bot_randskill", "10", 0); bot_randname = gi.cvar("bot_randname", "1", 0); bot_chat = gi.cvar("bot_chat", "0", 0); diff --git a/src/server/commands.c b/src/server/commands.c index 5b9fbf50e..0ae2341f4 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -559,7 +559,7 @@ static void dump_bot_clients(void) if(bot_clients[i].in_use) { Com_Printf("%3i %5i ", bot_clients[i].number, bot_clients[i].score); Com_Printf("%4i ", bot_clients[i].ping); - Com_Printf("%5i ", bot_clients[i].skill); + Com_Printf(" %.2f ", bot_clients[i].skill); Com_Printf("%-15.15s ", bot_clients[i].name); Com_Printf("\n"); } From bab58f418dbce46a6767898cca4ab55bed2b37b5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 13:07:54 -0400 Subject: [PATCH 516/974] Moved ChageNodeFunction prints to gi.cprintf --- src/action/acesrc/acebot_nodes.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 9eb36cb70..01e28e5de 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -507,14 +507,14 @@ void BOTLIB_ChangeNodeFunction(edict_t* ent) if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_NONE) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_SELECT; // Select nodes - Com_Printf("%s Node selection changed to: Select nodes (area is [%d]) (use nav_area to change selected node area)\n", __func__, ent->bot.walknode.selection_area); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Select nodes (area is [%d]) (use nav_area to change selected node area)\n", "ChangeNodeFunction:", ent->bot.walknode.selection_area); VectorClear(ent->bot.walknode.selection_start); VectorClear(ent->bot.walknode.selection_end); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_SELECT_SMART; // Smart node selection - Com_Printf("%s Node selection changed to: Smart node selection (area is [%d]) (use nav_area to change selected node area)\n", __func__, ent->bot.walknode.selection_area); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Smart node selection (area is [%d]) (use nav_area to change selected node area)\n", "ChangeNodeFunction:", ent->bot.walknode.selection_area); VectorClear(ent->bot.walknode.selection_start); VectorClear(ent->bot.walknode.selection_end); BOTLIB_SetAllNodeNormals(); // Set all the normals @@ -522,42 +522,42 @@ void BOTLIB_ChangeNodeFunction(edict_t* ent) else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_ADD; // Add nodes - Com_Printf("%s Node selection changed to: Add node\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Add node\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_ADD) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_LINK; // Link nodes - Com_Printf("%s Node selection changed to: Link/Unlink node\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Link/Unlink node\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINK) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_MOVE; // Move nodes - Com_Printf("%s Node selection changed to: Move node\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Move node\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_MOVE) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_TYPE; // Change node type - Com_Printf("%s Node selection changed to: Change node type\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Change node type\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_TYPE) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_LINKTYPE; // Change node-to-node link type - Com_Printf("%s Node selection changed to: Change node-to-node link type\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Change node-to-node link type\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_LINKTYPE) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_DEL; // Delete nodes - Com_Printf("%s Node selection changed to: Delete node\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Delete node\n", "ChangeNodeFunction:"); } else if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_DEL) { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_FLOODFILL; // Flood fill - Com_Printf("%s Node selection changed to: Flood fill area with nodes\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: Flood fill area with nodes\n", "ChangeNodeFunction:"); } else { ent->bot.walknode.highlighted_node_type = HIGHLIGHTED_NODE_NONE; // Disable interaction with nodes - Com_Printf("%s Node selection changed to: No interaction with nodes\n", __func__); + gi.cprintf(ent, PRINT_MEDIUM, "%s Node selection changed to: No interaction with nodes\n", "ChangeNodeFunction:"); } // Reset highlighted nodes From ff3fe9e5b7fee65080065cb24a3524daafb50144 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:17:07 +0300 Subject: [PATCH 517/974] Simplify GL_DrawBeamSegment(). --- src/refresh/tess.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 9b0985584..732288c84 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -181,17 +181,13 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col vec_t *dst_vert; uint32_t *dst_color; QGL_INDEX_TYPE *dst_indices; - vec_t length; VectorSubtract(end, start, d1); VectorSubtract(glr.fd.vieworg, start, d2); CrossProduct(d1, d2, d3); - VectorNormalize(d3); - VectorScale(d3, width, d3); - - length = VectorLength(d1); - if (length < 0.1f) + if (VectorNormalize(d3) < 0.1f) return; + VectorScale(d3, width, d3); if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || tess.numindices + 6 > TESS_MAX_INDICES)) @@ -205,8 +201,8 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col dst_vert[3] = 0; dst_vert[4] = 0; dst_vert[8] = 1; dst_vert[9] = 0; - dst_vert[13] = 1; dst_vert[14] = length; - dst_vert[18] = 0; dst_vert[19] = length; + dst_vert[13] = 1; dst_vert[14] = 1; + dst_vert[18] = 0; dst_vert[19] = 1; dst_color = (uint32_t *)tess.colors + tess.numverts; dst_color[0] = color.u32; From da599188b06e29d1f92ca3e5ebe74ed27016d8d8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:21:22 +0300 Subject: [PATCH 518/974] Simplify counting visible nodes. --- src/refresh/draw.c | 2 +- src/refresh/gl.h | 2 +- src/refresh/world.c | 16 ++++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index f28212017..6a951e18e 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -450,7 +450,7 @@ void Draw_Stats(void) R_SetScale(1.0f / get_auto_scale()); R_DrawFill8(8, 8, 25*8, 22*10+2, 4); - Draw_Stringf(x, y, "Nodes visible : %i", c.nodesVisible); y += 10; + Draw_Stringf(x, y, "Nodes visible : %i", glr.nodes_visible); y += 10; Draw_Stringf(x, y, "Nodes culled : %i", c.nodesCulled); y += 10; Draw_Stringf(x, y, "Nodes drawn : %i", c.nodesDrawn); y += 10; Draw_Stringf(x, y, "Leaves drawn : %i", c.leavesDrawn); y += 10; diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 44996f6dd..690a464ac 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -124,6 +124,7 @@ typedef struct { float frametime; int viewcluster1; int viewcluster2; + int nodes_visible; cplane_t frustumPlanes[4]; entity_t *ent; bool entrotated; @@ -173,7 +174,6 @@ extern entity_t gl_world; extern unsigned r_registration_sequence; typedef struct { - int nodesVisible; int nodesDrawn; int leavesDrawn; int facesMarked; diff --git a/src/refresh/world.c b/src/refresh/world.c index 7bd214757..693d466cc 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -335,7 +335,6 @@ void R_LightPoint(const vec3_t origin, vec3_t color) static void GL_MarkLeaves(void) { - static int lastNodesVisible; byte vis1[VIS_MAX_BYTES]; byte vis2[VIS_MAX_BYTES]; const mleaf_t *leaf; @@ -359,11 +358,11 @@ static void GL_MarkLeaves(void) } if (cluster1 == glr.viewcluster1 && cluster2 == glr.viewcluster2) { - goto finish; + return; } if (gl_lockpvs->integer) { - goto finish; + return; } glr.visframe++; @@ -378,8 +377,8 @@ static void GL_MarkLeaves(void) for (i = 0; i < bsp->numleafs; i++) { bsp->leafs[i].visframe = glr.visframe; } - lastNodesVisible = bsp->numnodes; - goto finish; + glr.nodes_visible = bsp->numnodes; + return; } BSP_ClusterVis(bsp, vis1, cluster1, DVIS_PVS); @@ -393,7 +392,7 @@ static void GL_MarkLeaves(void) } } - lastNodesVisible = 0; + glr.nodes_visible = 0; for (i = 0, leaf = bsp->leafs; i < bsp->numleafs; i++, leaf++) { cluster1 = leaf->cluster; if (cluster1 == -1) { @@ -405,12 +404,9 @@ static void GL_MarkLeaves(void) // mark parent nodes visible for (node = (mnode_t *)leaf; node && node->visframe != glr.visframe; node = node->parent) { node->visframe = glr.visframe; - lastNodesVisible++; + glr.nodes_visible++; } } - -finish: - c.nodesVisible = lastNodesVisible; } #define BACKFACE_EPSILON 0.01f From 33064abfe487691099bbc967db5293ea08e3f96d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 11:40:52 +0300 Subject: [PATCH 519/974] Add and use GL_DrawTriangles() macro. --- src/refresh/gl.h | 3 +++ src/refresh/mesh.c | 8 ++++---- src/refresh/state.c | 2 +- src/refresh/tess.c | 8 ++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 690a464ac..6057407d1 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -621,6 +621,9 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) #define GL_Color(r, g, b, a) gl_backend->color(r, g, b, a) +#define GL_DrawTriangles(num_indices, indices) \ + qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices) + enum { SHOWTRIS_WORLD = BIT(0), SHOWTRIS_MESH = BIT(1), diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index f89209e36..afe07983d 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -421,7 +421,7 @@ static void draw_celshading(const QGL_INDEX_TYPE *indices, int num_indices) qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); qglCullFace(GL_FRONT); GL_Color(0, 0, 0, color[3] * celscale); - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices); + GL_DrawTriangles(num_indices, indices); qglCullFace(GL_BACK); qglPolygonMode(GL_FRONT_AND_BACK, GL_FILL); qglLineWidth(1); @@ -503,7 +503,7 @@ static void draw_shadow(const QGL_INDEX_TYPE *indices, int num_indices) qglEnable(GL_POLYGON_OFFSET_FILL); qglPolygonOffset(-1.0f, -2.0f); GL_Color(0, 0, 0, color[3] * 0.5f); - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices); + GL_DrawTriangles(num_indices, indices); qglDisable(GL_POLYGON_OFFSET_FILL); // once we have drawn something to stencil buffer, continue to clear it for @@ -561,7 +561,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_VertexPointer(3, dotshading ? VERTEX_SIZE : 4, tess.vertices); GL_LockArrays(num_verts); qglColorMask(0, 0, 0, 0); - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices); + GL_DrawTriangles(num_indices, indices); qglColorMask(1, 1, 1, 1); GL_UnlockArrays(); } @@ -596,7 +596,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_LockArrays(num_verts); - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices); + GL_DrawTriangles(num_indices, indices); c.trisDrawn += num_indices / 3; draw_celshading(indices, num_indices); diff --git a/src/refresh/state.c b/src/refresh/state.c index 0a00a9079..663d96c4f 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -283,7 +283,7 @@ void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices) qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); if (indices) - qglDrawElements(GL_TRIANGLES, count, QGL_INDEX_ENUM, indices); + GL_DrawTriangles(count, indices); else qglDrawArrays(GL_TRIANGLES, 0, count); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 732288c84..cb9d75f88 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -53,7 +53,7 @@ void GL_Flush2D(void) GL_LockArrays(tess.numverts); - qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + GL_DrawTriangles(tess.numindices, tess.indices); if (gl_showtris->integer & SHOWTRIS_PIC) { GL_DrawOutlines(tess.numindices, tess.indices); @@ -165,7 +165,7 @@ static void GL_FlushBeamSegments(void) GL_LockArrays(tess.numverts); - qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + GL_DrawTriangles(tess.numindices, tess.indices); if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); @@ -313,7 +313,7 @@ static void GL_FlushFlares(void) GL_LockArrays(tess.numverts); - qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + GL_DrawTriangles(tess.numindices, tess.indices); if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(tess.numindices, tess.indices); @@ -485,7 +485,7 @@ void GL_Flush3D(void) GL_LockArrays(tess.numverts); } - qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_ENUM, tess.indices); + GL_DrawTriangles(tess.numindices, tess.indices); if (gl_showtris->integer & SHOWTRIS_WORLD) { GL_DrawOutlines(tess.numindices, tess.indices); From 54553ade7ea55d26d833a3a3d7c366ac5216087d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 12:16:48 +0300 Subject: [PATCH 520/974] Always setup GL frustum. --- src/refresh/main.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index b9d2dd7a9..de121c921 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -695,9 +695,7 @@ void R_RenderFrame(const refdef_t *fd) GL_Setup3D(waterwarp); - if (gl_cull_nodes->integer) { - GL_SetupFrustum(); - } + GL_SetupFrustum(); if (!(glr.fd.rdflags & RDF_NOWORLDMODEL) && gl_drawworld->integer) { GL_DrawWorld(); From 3e952ba97e772bd157e2c077af62e1a52a210769 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:33:04 +0300 Subject: [PATCH 521/974] Move empty buffer check into GL_FlushFlares(). --- src/refresh/tess.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index cb9d75f88..bc5f87364 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -307,6 +307,9 @@ void GL_DrawBeams(void) static void GL_FlushFlares(void) { + if (!tess.numindices) + return; + GL_BindTexture(0, tess.texnum[0]); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); @@ -321,6 +324,7 @@ static void GL_FlushFlares(void) GL_UnlockArrays(); tess.numverts = tess.numindices = 0; + tess.texnum[0] = 0; } void GL_DrawFlares(void) @@ -417,8 +421,7 @@ void GL_DrawFlares(void) tess.numindices += 6; } - if (tess.numindices) - GL_FlushFlares(); + GL_FlushFlares(); } void GL_BindArrays(void) From b15529b9a6af4603ae65fde81629c627e91b3035 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:37:20 +0300 Subject: [PATCH 522/974] Add and use VBO_OFS() macro. --- src/refresh/gl.h | 2 ++ src/refresh/tess.c | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 6057407d1..a528977ed 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -604,6 +604,8 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) qglDepthRange(n, f); } +#define VBO_OFS(n) ((void *)(sizeof(GLfloat) * (n))) + #define GL_VertexPointer(size, stride, ptr) \ gl_backend->vertex_pointer((size), (stride) * sizeof(GLfloat), (ptr)) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index bc5f87364..e684be473 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -436,12 +436,12 @@ void GL_BindArrays(void) } else { qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); - GL_VertexPointer(3, VERTEX_SIZE, (GLfloat *)0); - GL_TexCoordPointer(2, VERTEX_SIZE, (GLfloat *)(sizeof(GLfloat) * 4)); + GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); + GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); if (lm.nummaps) { - GL_LightCoordPointer(2, VERTEX_SIZE, (GLfloat *)(sizeof(GLfloat) * 6)); + GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); } - GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(sizeof(GLfloat) * 3)); + GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); qglBindBuffer(GL_ARRAY_BUFFER, 0); } From c9b52075c2608f39a83ae714ecfb328028186c81 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:56:39 +0300 Subject: [PATCH 523/974] Reorder GL_BindArrays() calls. --- src/refresh/tess.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index e684be473..0434ea0a0 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -428,20 +428,20 @@ void GL_BindArrays(void) { if (gl_static.world.vertices) { GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); + GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); GL_TexCoordPointer(2, VERTEX_SIZE, tess.vertices + 4); if (lm.nummaps) { GL_LightCoordPointer(2, VERTEX_SIZE, tess.vertices + 6); } - GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); } else { qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); + GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); if (lm.nummaps) { GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); } - GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); qglBindBuffer(GL_ARRAY_BUFFER, 0); } From 20135867ecd8e977595297719bb48285fedc7821 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:57:47 +0300 Subject: [PATCH 524/974] Simplify GL_CopyVerts(). --- src/refresh/tess.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 0434ea0a0..365512d97 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -508,16 +508,15 @@ void GL_Flush3D(void) static int GL_CopyVerts(const mface_t *surf) { - void *src, *dst; int firstvert; if (tess.numverts + surf->numsurfedges > TESS_MAX_VERTICES) { GL_Flush3D(); } - src = gl_static.world.vertices + surf->firstvert * VERTEX_SIZE; - dst = tess.vertices + tess.numverts * VERTEX_SIZE; - memcpy(dst, src, surf->numsurfedges * VERTEX_SIZE * sizeof(vec_t)); + memcpy(tess.vertices + tess.numverts * VERTEX_SIZE, + gl_static.world.vertices + surf->firstvert * VERTEX_SIZE, + surf->numsurfedges * VERTEX_SIZE * sizeof(GLfloat)); firstvert = tess.numverts; tess.numverts += surf->numsurfedges; From 0c0761b92a27a0aee338a43d12762dbeecf324f2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 21:58:32 +0300 Subject: [PATCH 525/974] Simplify binding textures. --- src/refresh/tess.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 365512d97..52753194a 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -476,13 +476,9 @@ void GL_Flush3D(void) GL_StateBits(state); GL_ArrayBits(array); - GL_BindTexture(0, tess.texnum[0]); - if (q_likely(tess.texnum[1])) { - GL_BindTexture(1, tess.texnum[1]); - } - if (tess.texnum[2]) { - GL_BindTexture(2, tess.texnum[2]); - } + for (int i = 0; i < MAX_TMUS; i++) + if (tess.texnum[i]) + GL_BindTexture(i, tess.texnum[i]); if (gl_static.world.vertices) { GL_LockArrays(tess.numverts); From b18a067a2772e202afbf8ff6eef5f5334072d272 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:04:50 +0300 Subject: [PATCH 526/974] Simplify loops. --- src/refresh/main.c | 6 +++--- src/refresh/world.c | 45 ++++++++++++++++++--------------------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index de121c921..7806e72f0 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -491,15 +491,15 @@ static void GL_OccludeFlares(void) static void GL_DrawEntities(int musthave, int canthave) { - entity_t *ent, *last; + entity_t *ent; model_t *model; + int i; if (!gl_drawentities->integer) { return; } - last = glr.fd.entities + glr.fd.num_entities; - for (ent = glr.fd.entities; ent != last; ent++) { + for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (ent->flags & RF_BEAM) { // beams are drawn elsewhere in single batch glr.num_beams++; diff --git a/src/refresh/world.c b/src/refresh/world.c index 693d466cc..21b535186 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -170,8 +170,7 @@ static bool GL_LightPoint_(const vec3_t start, vec3_t color) BSP_LightPoint(&glr.lightpoint, start, end, bsp->nodes, gl_static.nolm_mask); // trace to other BSP models - for (i = 0; i < glr.fd.num_entities; i++) { - ent = &glr.fd.entities[i]; + for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { index = ent->model; if (!(index & BIT(31))) break; // BSP models are at the start of entity array @@ -223,9 +222,9 @@ static bool GL_LightPoint_(const vec3_t start, vec3_t color) static void GL_MarkLights_r(const mnode_t *node, const dlight_t *light, uint64_t lightbit) { - vec_t dot; - int count; mface_t *face; + vec_t dot; + int i; while (node->plane) { dot = PlaneDiffFast(light->transformed, node->plane); @@ -238,19 +237,14 @@ static void GL_MarkLights_r(const mnode_t *node, const dlight_t *light, uint64_t continue; } - face = node->firstface; - count = node->numfaces; - while (count--) { - if (!(face->drawflags & gl_static.nolm_mask)) { - if (face->dlightframe != glr.dlightframe) { - face->dlightframe = glr.dlightframe; - face->dlightbits = 0; - } - - face->dlightbits |= lightbit; + for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { + if (face->drawflags & gl_static.nolm_mask) + continue; + if (face->dlightframe != glr.dlightframe) { + face->dlightframe = glr.dlightframe; + face->dlightbits = 0; } - - face++; + face->dlightbits |= lightbit; } GL_MarkLights_r(node->children[0], light, lightbit); @@ -413,12 +407,13 @@ static void GL_MarkLeaves(void) void GL_DrawBspModel(mmodel_t *model) { - mface_t *face, *last; + mface_t *face; vec3_t bounds[2]; vec_t dot; vec3_t transformed, temp; entity_t *ent = glr.ent; glCullResult_t cull; + int i; if (!model->numfaces) return; @@ -462,8 +457,7 @@ void GL_DrawBspModel(mmodel_t *model) GL_ClearSolidFaces(); // draw visible faces - last = model->firstface + model->numfaces; - for (face = model->firstface; face < last; face++) { + for (i = 0, face = model->firstface; i < model->numfaces; i++, face++) { // sky faces don't have their polygon built if (face->drawflags & (SURF_SKY | SURF_NODRAW)) { continue; @@ -533,8 +527,6 @@ static inline bool GL_ClipNode(const mnode_t *node, int *clipflags) static inline void GL_DrawLeaf(const mleaf_t *leaf) { - mface_t **face, **last; - if (leaf->contents == CONTENTS_SOLID) { return; // solid leaf } @@ -542,19 +534,18 @@ static inline void GL_DrawLeaf(const mleaf_t *leaf) return; // door blocks sight } - last = leaf->firstleafface + leaf->numleaffaces; - for (face = leaf->firstleafface; face < last; face++) { - (*face)->drawframe = glr.drawframe; - } + for (int i = 0; i < leaf->numleaffaces; i++) + leaf->firstleafface[i]->drawframe = glr.drawframe; c.leavesDrawn++; } static inline void GL_DrawNode(const mnode_t *node) { - mface_t *face, *last = node->firstface + node->numfaces; + mface_t *face; + int i; - for (face = node->firstface; face < last; face++) { + for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { if (face->drawframe != glr.drawframe) { continue; } From 5e9498ff09be905d579042268f6a0973ec8c8fc2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:07:03 +0300 Subject: [PATCH 527/974] Make skin_for_mesh() return const image_t *. --- src/refresh/mesh.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index afe07983d..daea53a4b 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -515,7 +515,7 @@ static void draw_shadow(const QGL_INDEX_TYPE *indices, int num_indices) } } -static image_t *skin_for_mesh(image_t **skins, int num_skins) +static const image_t *skin_for_mesh(image_t **skins, int num_skins) { const entity_t *ent = glr.ent; @@ -547,7 +547,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, image_t **skins, int num_skins) { glStateBits_t state = GLS_INTENSITY_ENABLE; - image_t *skin = skin_for_mesh(skins, num_skins); + const image_t *skin = skin_for_mesh(skins, num_skins); // fall back to entity matrix GL_LoadMatrix(glr.entmatrix); From e2ad962527f87d42996bdbcf76d8c5198406ccb5 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:07:46 +0300 Subject: [PATCH 528/974] Make MD5 frame numbers unsigned. --- src/refresh/mesh.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index daea53a4b..5782c7d1e 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -679,8 +679,8 @@ static void tess_shell_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) static void lerp_alias_skeleton(const md5_model_t *model) { - int frame_a = oldframenum % model->num_frames; - int frame_b = newframenum % model->num_frames; + unsigned frame_a = oldframenum % model->num_frames; + unsigned frame_b = newframenum % model->num_frames; const md5_joint_t *skel_a = &model->skeleton_frames[frame_a * model->num_joints]; const md5_joint_t *skel_b = &model->skeleton_frames[frame_b * model->num_joints]; From 6f5a09bf2402e1cb670381660a5957d9f41135da Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:08:52 +0300 Subject: [PATCH 529/974] More consistent developer message style. --- src/refresh/mesh.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 5782c7d1e..1be40a3c6 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -532,7 +532,7 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) return R_NOTEXTURE; if (ent->skinnum < 0 || ent->skinnum >= num_skins) { - Com_DPrintf("%s: no such skin: %d\n", "GL_DrawAliasModel", ent->skinnum); + Com_DPrintf("GL_DrawAliasModel: no such skin: %d\n", ent->skinnum); return skins[0]; } @@ -767,13 +767,13 @@ void GL_DrawAliasModel(const model_t *model) } else { newframenum = ent->frame; if (newframenum >= model->numframes) { - Com_DPrintf("%s: no such frame %u\n", __func__, newframenum); + Com_DPrintf("%s: no such frame: %u\n", __func__, newframenum); newframenum = 0; } oldframenum = ent->oldframe; if (oldframenum >= model->numframes) { - Com_DPrintf("%s: no such oldframe %u\n", __func__, oldframenum); + Com_DPrintf("%s: no such oldframe: %u\n", __func__, oldframenum); oldframenum = 0; } } From 22e560e1eed8e7d0a7e54d84e7d7f92e90bfce48 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:14:59 +0300 Subject: [PATCH 530/974] Exit GL_MarkLeaves() early if PVS is locked. --- src/refresh/world.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/refresh/world.c b/src/refresh/world.c index 21b535186..260ce10b1 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -338,6 +338,10 @@ static void GL_MarkLeaves(void) vec3_t tmp; const bsp_t *bsp = gl_static.world.cache; + if (gl_lockpvs->integer) { + return; + } + leaf = BSP_PointLeaf(bsp->nodes, glr.fd.vieworg); cluster1 = cluster2 = leaf->cluster; VectorCopy(glr.fd.vieworg, tmp); @@ -355,10 +359,6 @@ static void GL_MarkLeaves(void) return; } - if (gl_lockpvs->integer) { - return; - } - glr.visframe++; glr.viewcluster1 = cluster1; glr.viewcluster2 = cluster2; From f117333a8838c904a6bf90027484bdad81f20f24 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:24:57 +0300 Subject: [PATCH 531/974] Use typed value for state bits. --- src/refresh/tess.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 52753194a..1a3e8e62e 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -82,7 +82,7 @@ void GL_DrawParticles(void) int numverts; vec_t *dst_vert; uint32_t *dst_color; - int bits; + glStateBits_t bits; if (!glr.fd.num_particles) return; From 2ea322762fcd67437ff626414902564cf678f0b0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:25:31 +0300 Subject: [PATCH 532/974] More refresh code style cleanup. --- src/refresh/draw.c | 27 +++++-------- src/refresh/mesh.c | 3 +- src/refresh/surf.c | 99 ++++++++++++++++++--------------------------- src/refresh/tess.c | 68 +++++++++++-------------------- src/refresh/world.c | 98 +++++++++++++++++--------------------------- 5 files changed, 111 insertions(+), 184 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 6a951e18e..66c223a07 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -31,9 +31,8 @@ static inline void GL_StretchPic_( if (tess.numverts + 4 > TESS_MAX_VERTICES || tess.numindices + 6 > TESS_MAX_INDICES || - (tess.numverts && tess.texnum[0] != texnum)) { + (tess.numverts && tess.texnum[0] != texnum)) GL_Flush2D(); - } tess.texnum[0] = texnum; @@ -58,16 +57,14 @@ static inline void GL_StretchPic_( dst_indices[5] = tess.numverts + 2; if (flags & IF_TRANSPARENT) { - if ((flags & IF_PALETTED) && draw.scale == 1) { + if ((flags & IF_PALETTED) && draw.scale == 1) tess.flags |= GLS_ALPHATEST_ENABLE; - } else { + else tess.flags |= GLS_BLEND_BLEND; - } } - if ((color & U32_ALPHA) != U32_ALPHA) { + if ((color & U32_ALPHA) != U32_ALPHA) tess.flags |= GLS_BLEND_BLEND; - } tess.numverts += 4; tess.numindices += 6; @@ -84,9 +81,8 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) if (tess.numverts + 8 > TESS_MAX_VERTICES || tess.numindices + 24 > TESS_MAX_INDICES || - (tess.numverts && tess.texnum[0] != TEXNUM_WHITE)) { + (tess.numverts && tess.texnum[0] != TEXNUM_WHITE)) GL_Flush2D(); - } tess.texnum[0] = TEXNUM_WHITE; @@ -296,9 +292,8 @@ float R_ClampScale(cvar_t *var) void R_SetScale(float scale) { - if (draw.scale == scale) { + if (draw.scale == scale) return; - } GL_Flush2D(); @@ -380,16 +375,14 @@ static inline void draw_char(int x, int y, int flags, int c, const image_t *imag { float s, t; - if ((c & 127) == 32) { + if ((c & 127) == 32) return; - } - if (flags & UI_ALTCOLOR) { + if (flags & UI_ALTCOLOR) c |= 0x80; - } - if (flags & UI_XORCOLOR) { + + if (flags & UI_XORCOLOR) c ^= 0x80; - } s = (c & 15) * 0.0625f; t = (c >> 4) * 0.0625f; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 1be40a3c6..8edc4c77a 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -601,9 +601,8 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, draw_celshading(indices, num_indices); - if (gl_showtris->integer & SHOWTRIS_MESH) { + if (gl_showtris->integer & SHOWTRIS_MESH) GL_DrawOutlines(num_indices, indices); - } // FIXME: unlock arrays before changing matrix? draw_shadow(indices, num_indices); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 90e7aa5bf..4ae277708 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -50,12 +50,10 @@ adjust_color_f(vec_t *out, const vec_t *in, float add, float modulate, float sca // determine the brightest of the three color components max = g; - if (r > max) { + if (r > max) max = r; - } - if (b > max) { + if (b > max) max = b; - } // rescale all the color components if the intensity of the greatest // channel exceeds 1.0 @@ -238,11 +236,10 @@ static void update_dynamic_lightmap(mface_t *surf) add_light_styles(surf); // add all the dynamic lights - if (surf->dlightframe == glr.dlightframe) { + if (surf->dlightframe == glr.dlightframe) add_dynamic_lights(surf); - } else { + else surf->dlightframe = 0; - } // put into texture format put_blocklights(surf); @@ -269,9 +266,8 @@ void GL_PushLights(mface_t *surf) lightstyle_t *style; int i; - if (!surf->light_m) { + if (!surf->light_m) return; - } // dynamic this frame or dynamic previously if (surf->dlightframe) { @@ -351,9 +347,8 @@ static void LM_InitBlock(void) static void LM_UploadBlock(void) { - if (!lm.dirty) { + if (!lm.dirty) return; - } Q_assert(lm.nummaps < lm.maxmaps); @@ -381,15 +376,13 @@ static void build_style_map(int dynamic) return; } - for (i = 0; i < MAX_LIGHTSTYLES; i++) { + for (i = 0; i < MAX_LIGHTSTYLES; i++) gl_static.lightstylemap[i] = i; - } if (dynamic != 1) { // make dynamic styles fullbright - for (i = 1; i < 32; i++) { + for (i = 1; i < 32; i++) gl_static.lightstylemap[i] = 0; - } } } @@ -503,15 +496,12 @@ static void LM_RebuildSurfaces(void) build_style_map(gl_dynamic->integer); - if (!lm.nummaps) { + if (!lm.nummaps) return; - } - for (i = 0, surf = bsp->faces; i < bsp->numfaces; i++, surf++) { - if (surf->light_m) { + for (i = 0, surf = bsp->faces; i < bsp->numfaces; i++, surf++) + if (surf->light_m) build_primary_lightmap(surf); - } - } // upload all lightmaps for (i = 0, m = lm.lightmaps; i < lm.nummaps; i++, m++) { @@ -524,7 +514,6 @@ static void LM_RebuildSurfaces(void) } } - /* ============================================================================= @@ -572,36 +561,32 @@ static void build_surface_poly(mface_t *surf, vec_t *vbo) // convert surface flags to state bits surf->statebits = GLS_DEFAULT; if (gl_static.use_shaders) { - if (!(surf->drawflags & SURF_TRANS_MASK)) { + // no inverse intensity + if (!(surf->drawflags & SURF_TRANS_MASK)) surf->statebits |= GLS_TEXTURE_REPLACE; - } + // always use intensity on lightmapped surfaces if ((surf->lightmap && bsp->lm_decoupled) || !(surf->drawflags & SURF_COLOR_MASK) || - (!(surf->drawflags & SURF_TRANS_MASK) && strstr(texinfo->name, "lava"))) { + (!(surf->drawflags & SURF_TRANS_MASK) && strstr(texinfo->name, "lava"))) surf->statebits |= GLS_INTENSITY_ENABLE; - } } else { - if (!(surf->drawflags & SURF_COLOR_MASK)) { + if (!(surf->drawflags & SURF_COLOR_MASK)) surf->statebits |= GLS_TEXTURE_REPLACE; - } } - if (surf->drawflags & SURF_WARP) { + if (surf->drawflags & SURF_WARP) surf->statebits |= GLS_WARP_ENABLE; - } - if (surf->drawflags & SURF_TRANS_MASK) { + if (surf->drawflags & SURF_TRANS_MASK) surf->statebits |= GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE; - } else if (surf->drawflags & SURF_ALPHATEST) { + else if (surf->drawflags & SURF_ALPHATEST) surf->statebits |= GLS_ALPHATEST_ENABLE; - } if (surf->drawflags & SURF_FLOWING) { surf->statebits |= GLS_SCROLL_ENABLE; - if (surf->drawflags & SURF_WARP) { + if (surf->drawflags & SURF_WARP) surf->statebits |= GLS_SCROLL_SLOW; - } } // normalize texture coordinates @@ -612,15 +597,15 @@ static void build_surface_poly(mface_t *surf, vec_t *vbo) scale[0] *= 0.5f; scale[1] *= 0.5f; } - if (surf->drawflags & SURF_N64_SCROLL_X) { + + if (surf->drawflags & SURF_N64_SCROLL_X) surf->statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_X; - } - if (surf->drawflags & SURF_N64_SCROLL_Y) { + + if (surf->drawflags & SURF_N64_SCROLL_Y) surf->statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_Y; - } - if (surf->drawflags & SURF_N64_SCROLL_FLIP) { + + if (surf->drawflags & SURF_N64_SCROLL_FLIP) surf->statebits |= GLS_SCROLL_FLIP; - } mins[0] = mins[1] = 99999; maxs[0] = maxs[1] = -99999; @@ -814,14 +799,12 @@ static bool create_surface_vbo(size_t size) { GLuint buf = 0; - if (!qglGenBuffers) { + if (!qglGenBuffers) return false; - } #if USE_GLES - if (size > 65536 * VERTEX_SIZE * sizeof(vec_t)) { + if (size > 65536 * VERTEX_SIZE * sizeof(vec_t)) return false; - } #endif GL_ClearErrors(); @@ -965,17 +948,15 @@ void GL_RebuildLighting(void) void GL_FreeWorld(void) { - if (!gl_static.world.cache) { + if (!gl_static.world.cache) return; - } BSP_Free(gl_static.world.cache); - if (gl_static.world.vertices) { + if (gl_static.world.vertices) Z_Free(gl_static.world.vertices); - } else if (qglDeleteBuffers) { + else if (qglDeleteBuffers) qglDeleteBuffers(1, &gl_static.world.bufnum); - } memset(&gl_static.world, 0, sizeof(gl_static.world)); } @@ -990,22 +971,21 @@ void GL_LoadWorld(const char *name) int i, n64surfs, ret; ret = BSP_Load(name, &bsp); - if (!bsp) { + if (!bsp) Com_Error(ERR_DROP, "%s: couldn't load %s: %s", __func__, name, BSP_ErrorString(ret)); - } // check if the required world model was already loaded if (gl_static.world.cache == bsp) { - for (i = 0; i < bsp->numtexinfo; i++) { + for (i = 0; i < bsp->numtexinfo; i++) bsp->texinfo[i].image->registration_sequence = r_registration_sequence; - } - for (i = 0; i < bsp->numnodes; i++) { + + for (i = 0; i < bsp->numnodes; i++) bsp->nodes[i].visframe = 0; - } - for (i = 0; i < bsp->numleafs; i++) { + + for (i = 0; i < bsp->numleafs; i++) bsp->leafs[i].visframe = 0; - } + Com_DPrintf("%s: reused old world model\n", __func__); bsp->refcount--; return; @@ -1059,9 +1039,8 @@ void GL_LoadWorld(const char *name) // only supported in DECOUPLED_LM maps because vanilla maps have broken // lightofs for liquids/alphas. legacy renderer doesn't support lightmapped // liquids too. - if ((bsp->lm_decoupled || n64surfs > 100) && gl_static.use_shaders) { + if ((bsp->lm_decoupled || n64surfs > 100) && gl_static.use_shaders) gl_static.nolm_mask = SURF_NOLM_MASK_REMASTER; - } glr.fd.lightstyles = &(lightstyle_t){ 1 }; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 1a3e8e62e..2283ae9d4 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -32,14 +32,12 @@ void GL_Flush2D(void) { glStateBits_t bits; - if (!tess.numverts) { + if (!tess.numverts) return; - } bits = GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | tess.flags; - if (bits & GLS_BLEND_BLEND) { + if (bits & GLS_BLEND_BLEND) bits &= ~GLS_ALPHATEST_ENABLE; - } Scrap_Upload(); @@ -55,9 +53,8 @@ void GL_Flush2D(void) GL_DrawTriangles(tess.numindices, tess.indices); - if (gl_showtris->integer & SHOWTRIS_PIC) { + if (gl_showtris->integer & SHOWTRIS_PIC) GL_DrawOutlines(tess.numindices, tess.indices); - } GL_UnlockArrays(); @@ -128,11 +125,10 @@ void GL_DrawParticles(void) dst_vert += 15; - if (p->color == -1) { + if (p->color == -1) color.u32 = p->rgba.u32; - } else { + else color.u32 = d_8to24table[p->color & 0xff]; - } color.u8[3] *= p->alpha; dst_color[0] = color.u32; @@ -146,9 +142,8 @@ void GL_DrawParticles(void) GL_LockArrays(numverts); qglDrawArrays(GL_TRIANGLES, 0, numverts); - if (gl_showtris->integer & SHOWTRIS_FX) { + if (gl_showtris->integer & SHOWTRIS_FX) GL_DrawOutlines(numverts, NULL); - } GL_UnlockArrays(); } while (total); @@ -199,8 +194,8 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col VectorSubtract(end, d3, dst_vert + 10); VectorAdd(end, d3, dst_vert + 15); - dst_vert[3] = 0; dst_vert[4] = 0; - dst_vert[8] = 1; dst_vert[9] = 0; + dst_vert[ 3] = 0; dst_vert[ 4] = 0; + dst_vert[ 8] = 1; dst_vert[ 9] = 0; dst_vert[13] = 1; dst_vert[14] = 1; dst_vert[18] = 0; dst_vert[19] = 1; @@ -430,18 +425,16 @@ void GL_BindArrays(void) GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); GL_TexCoordPointer(2, VERTEX_SIZE, tess.vertices + 4); - if (lm.nummaps) { + if (lm.nummaps) GL_LightCoordPointer(2, VERTEX_SIZE, tess.vertices + 6); - } } else { qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); - if (lm.nummaps) { + if (lm.nummaps) GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); - } qglBindBuffer(GL_ARRAY_BUFFER, 0); } @@ -452,26 +445,22 @@ void GL_Flush3D(void) glStateBits_t state = tess.flags; glArrayBits_t array = GLA_VERTEX | GLA_TC; - if (!tess.numindices) { + if (!tess.numindices) return; - } if (q_likely(tess.texnum[1])) { state |= GLS_LIGHTMAP_ENABLE; array |= GLA_LMTC; - if (q_unlikely(gl_lightmap->integer)) { + if (q_unlikely(gl_lightmap->integer)) state &= ~GLS_INTENSITY_ENABLE; - } } - if (tess.texnum[2]) { + if (tess.texnum[2]) state |= GLS_GLOWMAP_ENABLE; - } - if (!(state & GLS_TEXTURE_REPLACE)) { + if (!(state & GLS_TEXTURE_REPLACE)) array |= GLA_COLOR; - } GL_StateBits(state); GL_ArrayBits(array); @@ -480,19 +469,16 @@ void GL_Flush3D(void) if (tess.texnum[i]) GL_BindTexture(i, tess.texnum[i]); - if (gl_static.world.vertices) { + if (gl_static.world.vertices) GL_LockArrays(tess.numverts); - } GL_DrawTriangles(tess.numindices, tess.indices); - if (gl_showtris->integer & SHOWTRIS_WORLD) { + if (gl_showtris->integer & SHOWTRIS_WORLD) GL_DrawOutlines(tess.numindices, tess.indices); - } - if (gl_static.world.vertices) { + if (gl_static.world.vertices) GL_UnlockArrays(); - } c.batchesDrawn++; @@ -506,9 +492,8 @@ static int GL_CopyVerts(const mface_t *surf) { int firstvert; - if (tess.numverts + surf->numsurfedges > TESS_MAX_VERTICES) { + if (tess.numverts + surf->numsurfedges > TESS_MAX_VERTICES) GL_Flush3D(); - } memcpy(tess.vertices + tess.numverts * VERTEX_SIZE, gl_static.world.vertices + surf->firstvert * VERTEX_SIZE, @@ -555,20 +540,18 @@ static void GL_DrawFace(const mface_t *surf) tess.texnum[1] != texnum[1] || tess.texnum[2] != texnum[2] || tess.flags != surf->statebits || - tess.numindices + numindices > TESS_MAX_INDICES) { + tess.numindices + numindices > TESS_MAX_INDICES) GL_Flush3D(); - } tess.texnum[0] = texnum[0]; tess.texnum[1] = texnum[1]; tess.texnum[2] = texnum[2]; tess.flags = surf->statebits; - if (q_unlikely(gl_static.world.vertices)) { + if (q_unlikely(gl_static.world.vertices)) j = GL_CopyVerts(surf); - } else { + else j = surf->firstvert; - } dst_indices = tess.indices + tess.numindices; for (i = 0; i < numtris; i++) { @@ -586,26 +569,23 @@ static void GL_DrawFace(const mface_t *surf) void GL_ClearSolidFaces(void) { - for (int i = 0; i < FACE_HASH_SIZE; i++) { + for (int i = 0; i < FACE_HASH_SIZE; i++) faces_next[i] = &faces_head[i]; - } } void GL_DrawSolidFaces(void) { for (int i = 0; i < FACE_HASH_SIZE; i++) { - for (const mface_t *face = faces_head[i]; face; face = face->next) { + for (const mface_t *face = faces_head[i]; face; face = face->next) GL_DrawFace(face); - } faces_head[i] = NULL; } } void GL_DrawAlphaFaces(void) { - if (!faces_alpha) { + if (!faces_alpha) return; - } glr.ent = NULL; diff --git a/src/refresh/world.c b/src/refresh/world.c index 260ce10b1..e21906170 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -305,9 +305,8 @@ void GL_LightPoint(const vec3_t origin, vec3_t color) } // get lighting from world - if (!GL_LightPoint_(origin, color)) { + if (!GL_LightPoint_(origin, color)) VectorSet(color, 1, 1, 1); - } // add dynamic lights GL_AddLights(origin, color); @@ -329,35 +328,29 @@ void R_LightPoint(const vec3_t origin, vec3_t color) static void GL_MarkLeaves(void) { + const bsp_t *bsp = gl_static.world.cache; byte vis1[VIS_MAX_BYTES]; byte vis2[VIS_MAX_BYTES]; const mleaf_t *leaf; - mnode_t *node; - size_t *src1, *src2; - int i, cluster1, cluster2, longs; + int i, cluster1, cluster2; vec3_t tmp; - const bsp_t *bsp = gl_static.world.cache; - if (gl_lockpvs->integer) { + if (gl_lockpvs->integer) return; - } leaf = BSP_PointLeaf(bsp->nodes, glr.fd.vieworg); cluster1 = cluster2 = leaf->cluster; VectorCopy(glr.fd.vieworg, tmp); - if (!leaf->contents) { + if (!leaf->contents) tmp[2] -= 16; - } else { + else tmp[2] += 16; - } leaf = BSP_PointLeaf(bsp->nodes, tmp); - if (!(leaf->contents & CONTENTS_SOLID)) { + if (!(leaf->contents & CONTENTS_SOLID)) cluster2 = leaf->cluster; - } - if (cluster1 == glr.viewcluster1 && cluster2 == glr.viewcluster2) { + if (cluster1 == glr.viewcluster1 && cluster2 == glr.viewcluster2) return; - } glr.visframe++; glr.viewcluster1 = cluster1; @@ -365,12 +358,12 @@ static void GL_MarkLeaves(void) if (!bsp->vis || gl_novis->integer || cluster1 == -1) { // mark everything visible - for (i = 0; i < bsp->numnodes; i++) { + for (i = 0; i < bsp->numnodes; i++) bsp->nodes[i].visframe = glr.visframe; - } - for (i = 0; i < bsp->numleafs; i++) { + + for (i = 0; i < bsp->numleafs; i++) bsp->leafs[i].visframe = glr.visframe; - } + glr.nodes_visible = bsp->numnodes; return; } @@ -378,25 +371,22 @@ static void GL_MarkLeaves(void) BSP_ClusterVis(bsp, vis1, cluster1, DVIS_PVS); if (cluster1 != cluster2) { BSP_ClusterVis(bsp, vis2, cluster2, DVIS_PVS); - longs = VIS_FAST_LONGS(bsp); - src1 = (size_t *)vis1; - src2 = (size_t *)vis2; - while (longs--) { + int longs = VIS_FAST_LONGS(bsp); + size_t *src1 = (size_t *)vis1; + size_t *src2 = (size_t *)vis2; + while (longs--) *src1++ |= *src2++; - } } glr.nodes_visible = 0; for (i = 0, leaf = bsp->leafs; i < bsp->numleafs; i++, leaf++) { cluster1 = leaf->cluster; - if (cluster1 == -1) { + if (cluster1 == -1) continue; - } - if (!Q_IsBitSet(vis1, cluster1)) { + if (!Q_IsBitSet(vis1, cluster1)) continue; - } // mark parent nodes visible - for (node = (mnode_t *)leaf; node && node->visframe != glr.visframe; node = node->parent) { + for (mnode_t *node = (mnode_t *)leaf; node && node->visframe != glr.visframe; node = node->parent) { node->visframe = glr.visframe; glr.nodes_visible++; } @@ -459,9 +449,8 @@ void GL_DrawBspModel(mmodel_t *model) // draw visible faces for (i = 0, face = model->firstface; i < model->numfaces; i++, face++) { // sky faces don't have their polygon built - if (face->drawflags & (SURF_SKY | SURF_NODRAW)) { + if (face->drawflags & (SURF_SKY | SURF_NODRAW)) continue; - } dot = PlaneDiffFast(transformed, face->plane); if ((face->drawflags & DSURF_PLANEBACK) ? (dot > BACKFACE_EPSILON) : (dot < -BACKFACE_EPSILON)) { @@ -469,9 +458,8 @@ void GL_DrawBspModel(mmodel_t *model) continue; } - if (gl_dynamic->integer) { + if (gl_dynamic->integer) GL_PushLights(face); - } if (face->drawflags & SURF_TRANS_MASK) { if (model->drawframe != glr.drawframe) @@ -482,9 +470,8 @@ void GL_DrawBspModel(mmodel_t *model) GL_AddSolidFace(face); } - if (gl_dynamic->integer) { + if (gl_dynamic->integer) GL_UploadLightmaps(); - } GL_DrawSolidFaces(); @@ -503,36 +490,31 @@ static inline bool GL_ClipNode(const mnode_t *node, int *clipflags) int flags = *clipflags; int i, bits, mask; - if (flags == NODE_UNCLIPPED) { + if (flags == NODE_UNCLIPPED) return true; - } + for (i = 0, mask = 1; i < 4; i++, mask <<= 1) { - if (flags & mask) { + if (flags & mask) continue; - } bits = BoxOnPlaneSide(node->mins, node->maxs, &glr.frustumPlanes[i]); - if (bits == BOX_BEHIND) { + if (bits == BOX_BEHIND) return false; - } - if (bits == BOX_INFRONT) { + if (bits == BOX_INFRONT) flags |= mask; - } } *clipflags = flags; - return true; } static inline void GL_DrawLeaf(const mleaf_t *leaf) { - if (leaf->contents == CONTENTS_SOLID) { + if (leaf->contents == CONTENTS_SOLID) return; // solid leaf - } - if (glr.fd.areabits && !Q_IsBitSet(glr.fd.areabits, leaf->area)) { + + if (glr.fd.areabits && !Q_IsBitSet(glr.fd.areabits, leaf->area)) return; // door blocks sight - } for (int i = 0; i < leaf->numleaffaces; i++) leaf->firstleafface[i]->drawframe = glr.drawframe; @@ -546,29 +528,24 @@ static inline void GL_DrawNode(const mnode_t *node) int i; for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { - if (face->drawframe != glr.drawframe) { + if (face->drawframe != glr.drawframe) continue; - } if (face->drawflags & SURF_SKY) { R_AddSkySurface(face); continue; } - if (face->drawflags & SURF_NODRAW) { + if (face->drawflags & SURF_NODRAW) continue; - } - if (gl_dynamic->integer) { + if (gl_dynamic->integer) GL_PushLights(face); - } - if (face->drawflags & SURF_TRANS_MASK) { + if (face->drawflags & SURF_TRANS_MASK) GL_AddAlphaFace(face, &gl_world); - continue; - } - - GL_AddSolidFace(face); + else + GL_AddSolidFace(face); } c.nodesDrawn++; @@ -623,9 +600,8 @@ void GL_DrawWorld(void) GL_WorldNode_r(gl_static.world.cache->nodes, gl_cull_nodes->integer ? NODE_CLIPPED : NODE_UNCLIPPED); - if (gl_dynamic->integer) { + if (gl_dynamic->integer) GL_UploadLightmaps(); - } GL_DrawSolidFaces(); From 704d747e403302f6275f42f693020b4af0dcc4b2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:31:41 +0300 Subject: [PATCH 533/974] Simplify GL_TextureAnimation(). --- src/refresh/tess.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 2283ae9d4..7adb7feae 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -506,14 +506,9 @@ static int GL_CopyVerts(const mface_t *surf) static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) { - if (q_unlikely(tex->next)) { - unsigned c = glr.ent->frame % tex->numframes; - - while (c) { + if (q_unlikely(tex->next)) + for (int i = 0; i < glr.ent->frame % tex->numframes; i++) tex = tex->next; - c--; - } - } return tex->image; } From c3239a70cc16a6d71747f59c97ea11f5037d6374 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 13 Aug 2024 22:59:00 +0300 Subject: [PATCH 534/974] Fix damage vignette in legacy mode. --- src/refresh/draw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 66c223a07..d73a78ef7 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -157,7 +157,7 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) dst_indices[22] = tess.numverts + 4; dst_indices[23] = tess.numverts + 7; - tess.flags |= GLS_BLEND_BLEND; + tess.flags |= GLS_BLEND_BLEND | GLS_SHADE_SMOOTH; tess.numverts += 8; tess.numindices += 24; From 77b395168ef7efce5b7357ccef350f7476ed085c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 17:15:04 -0400 Subject: [PATCH 535/974] Changed MouseControlNodes to cprintf as well --- src/action/acesrc/acebot_nodes.c | 44 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 01e28e5de..263cca511 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1468,9 +1468,9 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) ent->bot.walknode.selected_node_count_prev = ent->bot.walknode.selection_node_count; // Update change if (ent->bot.walknode.selection_node_first == INVALID) - Com_Printf("%s %s nodes selected %d [area: INVALID]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s nodes selected %d [area: INVALID]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count); else - Com_Printf("%s %s nodes selected %d [area: %d]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count, nodes[ent->bot.walknode.selection_node_first].area); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s nodes selected %d [area: %d]\n", __func__, ent->client->pers.netname, ent->bot.walknode.selection_node_count, nodes[ent->bot.walknode.selection_node_first].area); } } } @@ -1539,7 +1539,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print curent link type NodeTypeToString(ent, nodes[from].links[i].targetNodeType, type_name, sizeof(type_name)); - Com_Printf("%s %s current link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s current link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); return; } } @@ -1574,13 +1574,13 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print current node type NodeTypeToString(ent, nodes[node].type, type_name, sizeof(type_name)); - Com_Printf("%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); // Print all links from this node to other nodes for (i = 0; i < nodes[node].num_links; i++) { NodeTypeToString(ent, nodes[node].links[i].targetNodeType, type_name, sizeof(type_name)); - Com_Printf("%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); } ent->bot.walknode.highlighted_node = node; @@ -1590,11 +1590,11 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) return; } - //Com_Printf("%s [%d] [LINK] Node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); + //gi.cprintf(ent, PRINT_MEDIUM, "%s [%d] [LINK] Node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); } else // No node touched { - //Com_Printf("%s [%d] [LINK] No node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); + //gi.cprintf(ent, PRINT_MEDIUM, "%s [%d] [LINK] No node touched prev_and_curr[%d %d] type[%d %d] \n", __func__, level.framenum, ent->bot.walknode.prev_highlighted_node, node, nodes[ent->bot.walknode.prev_highlighted_node].type, nodes[node].type); // Unselect nodes ent->bot.walknode.highlighted_node = INVALID; @@ -1634,7 +1634,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { //char typename[32] = { '\0' }; // Length of the longest node type name //NodeTypeToString(ent, nodes[node_added].type, typename, sizeof(typename)); - //Com_Printf("%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); + //gi.cprintf(ent, PRINT_MEDIUM, "%s %s added node [%d] type [%s]\n", __func__, ent->client->pers.netname, node_added, typename); // Try to self expand nodes outward from this node if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_FLOODFILL) @@ -1689,13 +1689,13 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print current node type NodeTypeToString(ent, nodes[node].type, type_name, sizeof(type_name)); - Com_Printf("%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s Current node [%d] of type: %s\n", __func__, nodes[node].nodenum, type_name); // Print all links from this node to other nodes for (i = 0; i < nodes[node].num_links; i++) { NodeTypeToString(ent, nodes[node].links[i].targetNodeType, type_name, sizeof(type_name)); - Com_Printf("%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s node-to-node[%d -> %d] link type: %s\n", __func__, nodes[node].nodenum, nodes[node].links[i].targetNode, type_name); } ent->bot.walknode.highlighted_node = node; @@ -1766,7 +1766,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) BOTLIB_RemoveAllNodeLinksFrom(nodes[node].nodenum); // Remove all links to and from this node nodes[node].type = nodetype; // Update the node type - Com_Printf("%s Changed node to type: %s\n", __func__, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s Changed node to type: %s\n", __func__, type_name); } return; } @@ -1822,7 +1822,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) if (NodeTypeToString(ent, linktype, type_name, sizeof(type_name))) // Only change if valid { - Com_Printf("%s %s changing link from node[%d -> %d] to type [%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s changing link from node[%d -> %d] to type [%s]\n", __func__, ent->client->pers.netname, from, to, type_name); nodes[from].links[i].targetNodeType = linktype; // Update the link type } return; @@ -1847,7 +1847,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) // Print removing link NodeTypeToString(ent, nodes[from].links[i].targetNodeType, type_name, sizeof(type_name)); - Com_Printf("%s %s removing link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s removing link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); // Remove link ACEND_RemoveNodeEdge(ent, from, to); // Removes one side of the link @@ -1865,7 +1865,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1882,7 +1882,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1899,7 +1899,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1916,7 +1916,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1933,7 +1933,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1950,7 +1950,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1966,7 +1966,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -1982,7 +1982,7 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) { // Print adding link NodeTypeToString(ent, node_type, type_name, sizeof(type_name)); - Com_Printf("%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); + gi.cprintf(ent, PRINT_MEDIUM, "%s %s adding link from node[%d -> %d] type[%s]\n", __func__, ent->client->pers.netname, from, to, type_name); ////ACEND_UpdateNodeReach(from, to); // Update path_table } @@ -2050,7 +2050,7 @@ int BOTLIB_Reachability(int from, int to) { void(*DrawBox)(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time) = players[0]->client->pers.draw->DrawBox; DrawBox(500 + tested_distance, pos, MakeColor(255, 255, 0, 255), nodes[from].mins, nodes[from].maxs, 500); // Draw node box - //Com_Printf("%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); + //gi.cprintf(ent, PRINT_MEDIUM, "%s [%d to %d] dist[%f of %f] pos[%f %f %f]\n", __func__, from, to, distance, tested_distance, pos[0], pos[1], pos[2]); } #endif //rekkie -- debug drawing -- e From 0e4963d27b7575adce1254c1d0422bc2b3ee7e41 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 17:41:44 -0400 Subject: [PATCH 536/974] Fixing missing players list --- src/action/a_vote.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/action/a_vote.c b/src/action/a_vote.c index ea58f348c..8dc33937c 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -574,7 +574,7 @@ votelist_t *MapWithMostVotes (float *p) return (NULL); //find map_num_clients - map_num_clients = _numvoteclients(); + map_num_clients = _numclients(); if (map_num_clients == 0) return (NULL); @@ -607,7 +607,7 @@ votelist_t *MapWithMostAllVotes( void ) votelist_t *search = NULL, *most = NULL; int highest_total = 0; - if( ! _numvoteclients() ) + if( ! _numclients() ) return NULL; for( search = map_votes; search != NULL; search = search->next ) @@ -1011,7 +1011,7 @@ void _CheckKickVote (void) return; kickvotechanged = false; - playernum = _numvoteclients (); + playernum = _numclients (); maxvotes = 0; mtarget = NULL; @@ -1212,7 +1212,7 @@ void Cmd_Kicklist_f(edict_t *ent) "kicked players %s be temporarily banned.\n\n", (int) (kickvote_min->value), (kickvote_min->value == 1) ? "" : "s", - _numvoteclients(), + _numclients(), kickvote_need->value, Allkickvotes, kickvote_pass->value, Mostkickpercent, Mostkickvotes == NULL ? "nobody" : Mostkickvotes->client->pers.netname, @@ -1545,7 +1545,7 @@ configlist_t *ConfigWithMostVotes (float *p) return (NULL); //find config_num_clients - config_num_clients = _numvoteclients(); + config_num_clients = _numclients(); if (config_num_clients == 0) return (NULL); @@ -2114,7 +2114,7 @@ void _CalcScrambleVotes (int *numclients, int *numvotes, float *percent) int i; edict_t *ent; - *numclients = _numvoteclients(); + *numclients = _numclients(); *numvotes = 0; *percent = 0.00f; From 94a77a1922b59be401c20b9196acdefecd55c161 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 17:48:56 -0400 Subject: [PATCH 537/974] Reverted adding bot skill to status, players did not show up in q2servers.com --- src/action/a_vote.c | 12 ++++++------ src/server/commands.c | 5 ++--- src/server/main.c | 9 ++------- src/server/server.h | 3 +-- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/action/a_vote.c b/src/action/a_vote.c index 8dc33937c..ea58f348c 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -574,7 +574,7 @@ votelist_t *MapWithMostVotes (float *p) return (NULL); //find map_num_clients - map_num_clients = _numclients(); + map_num_clients = _numvoteclients(); if (map_num_clients == 0) return (NULL); @@ -607,7 +607,7 @@ votelist_t *MapWithMostAllVotes( void ) votelist_t *search = NULL, *most = NULL; int highest_total = 0; - if( ! _numclients() ) + if( ! _numvoteclients() ) return NULL; for( search = map_votes; search != NULL; search = search->next ) @@ -1011,7 +1011,7 @@ void _CheckKickVote (void) return; kickvotechanged = false; - playernum = _numclients (); + playernum = _numvoteclients (); maxvotes = 0; mtarget = NULL; @@ -1212,7 +1212,7 @@ void Cmd_Kicklist_f(edict_t *ent) "kicked players %s be temporarily banned.\n\n", (int) (kickvote_min->value), (kickvote_min->value == 1) ? "" : "s", - _numclients(), + _numvoteclients(), kickvote_need->value, Allkickvotes, kickvote_pass->value, Mostkickpercent, Mostkickvotes == NULL ? "nobody" : Mostkickvotes->client->pers.netname, @@ -1545,7 +1545,7 @@ configlist_t *ConfigWithMostVotes (float *p) return (NULL); //find config_num_clients - config_num_clients = _numclients(); + config_num_clients = _numvoteclients(); if (config_num_clients == 0) return (NULL); @@ -2114,7 +2114,7 @@ void _CalcScrambleVotes (int *numclients, int *numvotes, float *percent) int i; edict_t *ent; - *numclients = _numclients(); + *numclients = _numvoteclients(); *numvotes = 0; *percent = 0.00f; diff --git a/src/server/commands.c b/src/server/commands.c index 0ae2341f4..6963c8788 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -552,14 +552,13 @@ static void dump_bot_clients(void) Com_Printf( "\n" "Bot Clients.\n" - "num score ping skill name\n" - "--- ----- ---- ----- ---------------\n"); + "num score ping name\n" + "--- ----- ---- ---------------\n"); for (int i = 0; i < MAX_CLIENTS; i++) { if(bot_clients[i].in_use) { Com_Printf("%3i %5i ", bot_clients[i].number, bot_clients[i].score); Com_Printf("%4i ", bot_clients[i].ping); - Com_Printf(" %.2f ", bot_clients[i].skill); Com_Printf("%-15.15s ", bot_clients[i].name); Com_Printf("\n"); } diff --git a/src/server/main.c b/src/server/main.c index dd8208cd5..3f30909eb 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -465,7 +465,7 @@ static size_t SV_StatusString(char *status) { if (bot_clients[i].in_use) { - len = Q_snprintf(entry, sizeof(entry), "%i %i %2f \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].skill, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] + len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", bot_clients[i].score, bot_clients[i].ping, bot_clients[i].name); //entry example = "0 1 \"[AIR]-Mech\"\n" char[1024] if (len >= sizeof(entry)) continue; @@ -1148,11 +1148,10 @@ void SV_BotInit(void) bot_clients[i].ping = 0; bot_clients[i].score = 0; bot_clients[i].number = i; - bot_clients[i].skill = 0; } } // Game DLL updates Server of bot info -void SV_BotUpdateInfo(char* name, int ping, int score, float skill) +void SV_BotUpdateInfo(char* name, int ping, int score) { for (int i = 0; i < MAX_CLIENTS; i++) { @@ -1162,7 +1161,6 @@ void SV_BotUpdateInfo(char* name, int ping, int score, float skill) { bot_clients[i].ping = ping; bot_clients[i].score = score; - bot_clients[i].skill = skill; return; } } @@ -1180,7 +1178,6 @@ void SV_BotConnect(char* name) bot_clients[i].ping = 0; bot_clients[i].score = 0; bot_clients[i].number = i; - bot_clients[i].skill = 0; Com_Printf("%s Server added %s as a fake client\n", __func__, bot_clients[i].name); break; } @@ -1197,7 +1194,6 @@ void SV_BotDisconnect(char* name) bot_clients[i].name[0] = 0; bot_clients[i].ping = 0; bot_clients[i].score = 0; - bot_clients[i].skill = 0; Com_Printf("%s Server removed %s as a fake client\n", __func__, name); break; } @@ -1212,7 +1208,6 @@ void SV_BotClearClients(void) bot_clients[i].name[0] = 0; bot_clients[i].ping = 0; bot_clients[i].score = 0; - bot_clients[i].skill = 0; } } //rekkie -- Fake Bot Client -- e diff --git a/src/server/server.h b/src/server/server.h index 87f0fcfba..456119a3e 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -888,7 +888,7 @@ trace_t q_gameabi SV_Clip(const vec3_t start, const vec3_t mins, bsp_t* SV_BSP(void); void SV_BotInit(void); -void SV_BotUpdateInfo(char* name, int ping, int score, float skill); +void SV_BotUpdateInfo(char* name, int ping, int score); void SV_BotConnect(char* name); void SV_BotDisconnect(char* name); void SV_BotClearClients(void); @@ -898,7 +898,6 @@ typedef struct bot_client_s { int ping; short score; int number; - float skill; } bot_client_t; extern bot_client_t bot_clients[MAX_CLIENTS]; From 940b32ca4ad77e12830cb246c94992cc596a5bd7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 13 Aug 2024 17:53:08 -0400 Subject: [PATCH 538/974] Reverted adding bot skill to status, players did not show up in q2servers.com --- inc/shared/game.h | 2 +- src/action/botlib/botlib_ai.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index a248f4f04..75eca2945 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -244,7 +244,7 @@ typedef struct { //rekkie -- surface data -- e //rekkie -- Fake Bot Client -- s - void (*SV_BotUpdateInfo)(char* name, int ping, int score, float skill); + void (*SV_BotUpdateInfo)(char* name, int ping, int score); void (*SV_BotConnect)(char* name); void (*SV_BotDisconnect)(char* name); void (*SV_BotClearClients)(void); diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 8c6bade18..11b2bc67d 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -668,7 +668,7 @@ void BOTLIB_Think(edict_t* self) self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score, self->bot.skill); + gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } //rekkie -- Fake Bot Client -- e From c299bac8613ab717dd25417ada41f288a95106d6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 14 Aug 2024 11:18:34 -0400 Subject: [PATCH 539/974] Fixing bot spawning bug in teamplay --- src/action/botlib/botlib_spawn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 2ad712b51..d23aa8968 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1375,7 +1375,7 @@ void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) bot->think = BOTLIB_Think; bot->nextthink = level.framenum + 1; - //PutClientInServer(bot); + PutClientInServer(bot); JoinTeam(bot, team, true); } From 84c557d0c89312e58db8ef96bdaa1542505e2a93 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 14 Aug 2024 17:17:32 +0300 Subject: [PATCH 540/974] Tune lightning beam effect more. --- src/refresh/tess.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 7adb7feae..a7372ee34 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -219,12 +219,12 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col #define MIN_LIGHTNING_SEGMENTS 3 #define MAX_LIGHTNING_SEGMENTS 7 -#define MIN_SEGMENT_LENGTH 10 +#define MIN_SEGMENT_LENGTH 16 static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t color, float width) { vec3_t d1, segments[MAX_LIGHTNING_SEGMENTS - 1]; - vec_t length; + vec_t length, segment_length; int i, num_segments, max_segments; VectorSubtract(end, start, d1); @@ -243,12 +243,13 @@ static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t c num_segments = MIN_LIGHTNING_SEGMENTS + GL_rand() % (max_segments - MIN_LIGHTNING_SEGMENTS + 1); } + segment_length = length / num_segments; for (i = 0; i < num_segments - 1; i++) { int dir = GL_rand() % q_countof(bytedirs); - float dist = GL_frand() * 20; - float frac = (float)(i + 1) / num_segments; - VectorMA(start, frac * length, d1, segments[i]); - VectorMA(segments[i], dist, bytedirs[dir], segments[i]); + float offs = GL_frand() * (segment_length * 0.5f); + float dist = (i + 1) * segment_length; + VectorMA(start, dist, d1, segments[i]); + VectorMA(segments[i], offs, bytedirs[dir], segments[i]); } for (i = 0; i < num_segments; i++) { From 2f3663af92c4a594d67d9553f71644f601956918 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 14 Aug 2024 16:40:39 -0400 Subject: [PATCH 541/974] Node/arrow/line colorations, stop bandaging at 100 health, teamplay suicide delay --- src/action/acesrc/acebot_movement.c | 4 +- src/action/acesrc/acebot_nodes.c | 53 +++++++++--------- src/action/botlib/botlib_ai.c | 16 +++++- src/action/botlib/botlib_spawn.c | 2 +- src/action/p_client.c | 84 +++++++++++++++++++++++++---- 5 files changed, 120 insertions(+), 39 deletions(-) diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index d9ab7eccf..5fef11690 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -2892,7 +2892,9 @@ void BOTLIB_Healing(edict_t* self, usercmd_t* ucmd) if (self->client->bandaging) self->bot.radioBandaging = true; // If bandaging, flag the bot to radio this in } //else if ((self->bot.enemy_seen_time + self->bot.bot_bandage_delay_time) <= (level.framenum * HZ)) - else if (self->bot.bot_bandage_delay_time < level.framenum && closest_enemy > 1024) // Bot can bandage + else if (self->bot.bot_bandage_delay_time < level.framenum && + closest_enemy > 1024 && + (self->health < 100 || self->client->bleeding)) // Bot can bandage { //Com_Printf("%s %s is bandaging with health:%d, bandage delay:%f\n", __func__, self->client->pers.netname, self->health, self->bot_bandage_delay_time); Cmd_Bandage_f(self); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 263cca511..5d2241558 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1792,32 +1792,33 @@ void BOTLIB_MouseControlNodes(edict_t* ent, usercmd_t* ucmd) // Change link type int linktype = (nodes[from].links[i].targetNodeType + 1); // Increment type so the switch statement will use the next type in the nodetype_t enum switch (linktype) { - case NODE_MOVE: - linktype = NODE_MOVE; - break; - case NODE_JUMPPAD: - linktype = NODE_JUMPPAD; - break; - case NODE_LADDER: - linktype = NODE_LADDER; - break; - case NODE_WATER: - linktype = NODE_WATER; - break; - case NODE_CROUCH: - linktype = NODE_CROUCH; - break; - case NODE_BOXJUMP: - linktype = NODE_BOXJUMP; - break; - case NODE_POI: - linktype = NODE_POI; - case NODE_POI_LOOKAT: - linktype = NODE_POI_LOOKAT; // NODE_POI_LOOKAT enum == 7 - break; - default: - linktype = NODE_MOVE; // NODE_MOVE enum == 1 - break; + case NODE_MOVE: + linktype = NODE_MOVE; + break; + case NODE_JUMPPAD: + linktype = NODE_JUMPPAD; + break; + case NODE_LADDER: + linktype = NODE_LADDER; + break; + case NODE_WATER: + linktype = NODE_WATER; + break; + case NODE_CROUCH: + linktype = NODE_CROUCH; + break; + case NODE_BOXJUMP: + linktype = NODE_BOXJUMP; + break; + case NODE_POI: + linktype = NODE_POI; + break; + case NODE_POI_LOOKAT: + linktype = NODE_POI_LOOKAT; // NODE_POI_LOOKAT enum == 7 + break; + default: + linktype = NODE_MOVE; // NODE_MOVE enum == 1 + break; } if (NodeTypeToString(ent, linktype, type_name, sizeof(type_name))) // Only change if valid diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 11b2bc67d..40332b4c4 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -788,8 +788,20 @@ void BOTLIB_Think(edict_t* self) // Kill the bot if they've not moved between nodes in a timely manner, stuck! //gi.dprintf("%s is currently at node %i\n", self->client->pers.netname, self->bot.current_node); - if (self->bot.node_travel_time > 120) - killPlayer(self, true); + + // Non-teamplay stuck suicide + if (!teamplay->value) { + if (self->bot.node_travel_time > 120) { + killPlayer(self, true); + } + // Too often teamplay bots will suicide because there's a bit of waiting around + } else if (self->bot.node_travel_time > 160 && + current_round_length > 60 && + !lights_camera_action && + !holding_on_tie_check) { + BOTLIB_PickLongRangeGoal(self); + //killPlayer(self, true); + } // Find any short range goal //ACEAI_PickShortRangeGoal(self); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index d23aa8968..968f9ca4d 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1379,7 +1379,7 @@ void BOTLIB_PutClientInServer(edict_t* bot, qboolean respawn, int team) JoinTeam(bot, team, true); } -// Spawn a bot +// Spawn a bot*SelectTeamplaySpawnPoint void ClientBeginDeathmatch(edict_t* ent); edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* force_skin) //char* userinfo) { diff --git a/src/action/p_client.c b/src/action/p_client.c index 9c24607e4..ef619c309 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1585,7 +1585,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) #ifndef NO_BOTS //darksaint -- Bot Chat -- s // Generates chat message if respawning (killed) - if (self->is_bot) { + if (self->is_bot && mod < MOD_TOTAL) { // Don't count 'killed him/herself' messages BOTLIB_Chat(self, CHAT_KILLED); if(bot_personality->value && bot_ragequit->value) { BotRageQuit(self, true); @@ -5287,7 +5287,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) #if DEBUG_DRAWING if (1 && ent->is_bot == false && dedicated->value == 0 && numnodes && bot_showpath->value == 0 && ent->bot.walknode.enabled) { - uint32_t color; + uint32_t color, link_color, arrow_color; // Selection square //uint32_t node_color = 0; @@ -5476,11 +5476,21 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) // Highlighted box if (ent->bot.walknode.highlighted_node == node) color = MakeColor(255, 0, 0, 255); // Red + else if (nodes[node].type == NODE_JUMPPAD) + color = MakeColor(128, 0, 128, 255); // Purple else if (nodes[node].type == NODE_LADDER) color = MakeColor(0, 255, 0, 255); // Green + else if (nodes[node].type == NODE_WATER) + color = MakeColor(255, 255, 255, 255); // White + else if (nodes[node].type == NODE_CROUCH) + color = MakeColor(255, 165, 0, 255); // Orange + else if (nodes[node].type == NODE_BOXJUMP) + color = MakeColor(128, 128, 128, 255); // Gray else if (nodes[node].type == NODE_POI) color = MakeColor(0, 255, 255, 255); // Cyan - else + else if (nodes[node].type == NODE_POI_LOOKAT) + color = MakeColor(0, 192, 192, 192); // Cyan-ish + else // NODE_MOVE color = MakeColor(0, 0, 255, 255); // Blue if (ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT || ent->bot.walknode.highlighted_node_type == HIGHLIGHTED_NODE_SELECT_SMART) @@ -5518,11 +5528,37 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) if (ent->bot.walknode.prev_highlighted_node != INVALID && to != ent->bot.walknode.prev_highlighted_node && node != ent->bot.walknode.prev_highlighted_node) continue; } + switch (nodes[node].links[link].targetNodeType) { + case NODE_MOVE: + arrow_color = MakeColor(255, 255, 0, 255); // Yellow + break; + case NODE_JUMPPAD: + arrow_color = MakeColor(128, 0, 128, 255); // Purple + break; + case NODE_LADDER: + arrow_color = MakeColor(0, 255, 0, 255); // Green + break; + case NODE_WATER: + arrow_color = MakeColor(255, 255, 255, 255); // White + break; + case NODE_CROUCH: + arrow_color = MakeColor(255, 165, 0, 255); // Orange + break; + case NODE_BOXJUMP: + arrow_color = MakeColor(128, 128, 128, 255); // Gray + break; + case NODE_POI: + arrow_color = MakeColor(0, 255, 255, 255); // Cyan + break; + case NODE_POI_LOOKAT: + arrow_color = MakeColor(0, 192, 192, 192); // Cyan-ish + break; + default: + arrow_color = MakeColor(255, 255, 0, 255); // Yellow + break; + } - if (nodes[node].links[link].targetNodeType == NODE_POI_LOOKAT) - DrawArrow(linknum, nodes[node].origin, nodes[to].origin, MakeColor(0, 255, 255, 255), 1.0, 100, true); // Cyan node link - else - DrawArrow(linknum, nodes[node].origin, nodes[to].origin, U32_YELLOW, 1.0, 100, true); // Yellow node link + DrawArrow(linknum, nodes[node].origin, nodes[to].origin, arrow_color, 1.0, 100, true); // Draw node link linknum++; } } @@ -5535,8 +5571,38 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) if (nodes[node].area > 0) // Only draw area num if > 0 DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", nodes[node].area), U32_YELLOW, 100, true); // Draw area number } - else - DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", node), U32_YELLOW, 100, true); // Draw node number + else { + switch (nodes[node].links->targetNodeType) { + case NODE_MOVE: + link_color = MakeColor(255, 255, 0, 255); // Yellow + break; + case NODE_JUMPPAD: + link_color = MakeColor(128, 0, 128, 255); // Purple + break; + case NODE_LADDER: + link_color = MakeColor(0, 255, 0, 255); // Green + break; + case NODE_WATER: + link_color = MakeColor(255, 255, 255, 255); // White + break; + case NODE_CROUCH: + link_color = MakeColor(255, 165, 0, 255); // Orange + break; + case NODE_BOXJUMP: + link_color = MakeColor(128, 128, 128, 255); // Gray + break; + case NODE_POI: + link_color = MakeColor(0, 255, 255, 255); // Cyan + break; + case NODE_POI_LOOKAT: + link_color = MakeColor(0, 192, 192, 192); // Cyan-ish + break; + default: + link_color = MakeColor(255, 255, 0, 255); // Yellow + break; + } + DrawString(node, tv(nodes[node].origin[0], nodes[node].origin[1], nodes[node].origin[2] + 20), va("%d", node), link_color, 100, true); // Draw node number + } } } } From 9a8b886d65e84227fb770772098af542589ee114 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 14 Aug 2024 20:12:51 -0400 Subject: [PATCH 542/974] Disabled two broken commands that crashed the game in nav_edit mode --- src/action/botlib/botlib_cmd.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index d27dfd47d..f93cba9fb 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -245,17 +245,17 @@ qboolean BOTLIB_SV_Cmds(void) qboolean BOTLIB_Commands(edict_t* ent) { char* cmd = gi.argv(0); - if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS - { - ACEND_SaveAAS(true); - return true; - } - else if (Q_stricmp(cmd, "an") == 0) // add node - { - ACEND_BSP(ent); - return true; - } - else if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names + // if (Q_stricmp(cmd, "dc_save_aas") == 0) // Save AAS + // { + // ACEND_SaveAAS(true); + // return true; + // } + // else if (Q_stricmp(cmd, "an") == 0) // add node + // { + // ACEND_BSP(ent); + // return true; + // } + if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names { BOTLIB_RandomizeTeamNames(ent); return true; From f48d0fb9194c580736234e8e2784aefb52d38d20 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 17:19:57 +0300 Subject: [PATCH 543/974] =?UTF-8?q?Allow=20=E2=80=98map=E2=80=99=20by=20de?= =?UTF-8?q?fault=20for=20non-dedicated=20servers.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/server.asciidoc | 3 ++- src/server/commands.c | 2 +- src/server/main.c | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/server.asciidoc b/doc/server.asciidoc index 947638d00..b6846cce6 100644 --- a/doc/server.asciidoc +++ b/doc/server.asciidoc @@ -244,7 +244,8 @@ WARNING: Be sure to read ‘recycle’ command description below before enabling sv_allow_map:: Controls the ‘map’ command behavior. ‘map’ is often mistakingly used by server operators to change maps instead of the more lightweight ‘gamemap’. - Thus, this variable exists to prevent misuse of ‘map’. Default value is 0. + Thus, this variable exists to prevent misuse of ‘map’. Default value is 0 + for dedicated servers, 1 otherwise. - 0 — disallow ‘map’ and print a warning unless there are pending latched cvars - 1 — handle ‘map’ command normally diff --git a/src/server/commands.c b/src/server/commands.c index 223bea323..d2ffea9da 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -365,7 +365,7 @@ static int should_really_restart(void) if (sv_allow_map->integer == 1) return 1; // `map' warning disabled - if (sv_allow_map->integer != 0) + if (sv_allow_map->integer >= 2) return 0; // turn `map' into `gamemap' Com_Printf( diff --git a/src/server/main.c b/src/server/main.c index 548578e8a..c10407b5f 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2250,7 +2250,7 @@ void SV_Init(void) sv_packetdup_hack = Cvar_Get("sv_packetdup_hack", "0", 0); #endif - sv_allow_map = Cvar_Get("sv_allow_map", "0", 0); + sv_allow_map = Cvar_Get("sv_allow_map", COM_DEDICATED ? "0" : "1", 0); sv_cinematics = Cvar_Get("sv_cinematics", "1", 0); #if USE_SERVER From 31a2c21bb8fb7fb523a86075dc94e9d595a9de7a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 15 Aug 2024 11:22:35 -0400 Subject: [PATCH 544/974] Removed jansson build step --- .github/workflows/build.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1423617a8..f4acf2358 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,17 +64,6 @@ jobs: sudo apt-get install -y gcc-mingw-w64 nasm python3-pip ninja-build sudo python3 -m pip install meson - # - name: Build jansson - # run: | - # wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.bz2 - # tar -xvf jansson-2.14.tar.bz2 - # cd jansson-2.14 - # ./configure - # make - # make check | true - # cat test-suite.log - # make install - - name: Build run: | meson setup --cross-file=.ci/${{ matrix.arch }}-w64-mingw32.txt \ From f0c1a914910204ff31aff8728e0d3296ac094015 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 15 Aug 2024 11:23:17 -0400 Subject: [PATCH 545/974] Removed bots.json --- bots.json | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 bots.json diff --git a/bots.json b/bots.json deleted file mode 100644 index dd47c19f2..000000000 --- a/bots.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "bots": { - "bob": { - "skin": "male/babarracuda", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67 - }, - "combat_demeanor": 0.35, - "chat_demeanor": -0.89 - }, - "bill": { - "skin": "male/terror", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67 - }, - "combat_demeanor": -0.45, - "chat_demeanor": 0.41 - }, - "sam": { - "skin": "male/mr_t", - "weapon_prefs": [-0.6, 0.2, 0.5, -0.7, 0.1, -0.8, 0.4, -0.9, 0.3], - "item_prefs": [0.6, 0.7, 0.8, 0.9, 1.0, 1.0], - "map_prefs": { - "teamjungle": 0.11, - "plaza": 0.67, - "kingslanding": 0.9 - }, - "combat_demeanor": 0.45, - "chat_demeanor": -0.3 - } - } -} From 247885745dd97a87c25d3ef327e9e8653a739650 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 15 Aug 2024 11:31:11 -0400 Subject: [PATCH 546/974] Updated link to patch file to use aqtion branch --- subprojects/jansson.wrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subprojects/jansson.wrap b/subprojects/jansson.wrap index 75b991f90..408f9a262 100644 --- a/subprojects/jansson.wrap +++ b/subprojects/jansson.wrap @@ -4,7 +4,7 @@ source_url = https://github.com/akheron/jansson/releases/download/v2.14/jansson- source_filename = jansson-2.14.tar.bz2 source_hash = fba956f27c6ae56ce6dfd52fbf9d20254aad42821f74fa52f83957625294afb9 patch_filename = jansson_2.14-2_patch.zip -patch_url = https://raw.githubusercontent.com/actionquake/q2pro/rektek-bots-personality/extern/jansson_patch.zip +patch_url = https://raw.githubusercontent.com/actionquake/q2pro/aqtion/extern/jansson_patch.zip patch_hash = 8221c3c382e908137d7f7e008c592af9e7bba40e26752af11d52cb4d33744e78 source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/jansson_2.14-2/jansson-2.14.tar.bz2 wrapdb_version = 2.14-2 From 75f9b47f353f9ac1678b054f099d16a26b1352ba Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 15 Aug 2024 14:56:38 -0400 Subject: [PATCH 547/974] Moved SV_Ghud_SendUpdateToClient to SV_EmitPacketEntities --- src/server/entities.c | 142 ++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 67 deletions(-) diff --git a/src/server/entities.c b/src/server/entities.c index 0c2378b3b..7f11d30f9 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -125,6 +125,75 @@ static bool SV_TruncPacketEntities(client_t *client, const client_frame_t *from, return ret; } +#if AQTION_EXTENSION +static void SV_Ghud_SendUpdateToClient(client_t *client, const client_frame_t *oldframe, client_frame_t *frame) +{ + if (client->protocol == PROTOCOL_VERSION_AQTION && client->version >= PROTOCOL_VERSION_AQTION_GHUD) + { + int protocolflags, maxelements; + + maxelements = 0; + protocolflags = 0; + if (client->version >= PROTOCOL_VERSION_AQTION_GHUD2) + { + maxelements = MAX_GHUDS; + protocolflags |= 1; + } + else + { + maxelements = 64; // we have to obey old kinda broken ghud for older clients + } + + if (oldframe != NULL) { + memcpy(frame->ghud, oldframe->ghud, sizeof(frame->ghud)); // use oldframe as baseline in case we can't fit all the updates in one package + } else { + memset(frame->ghud, 0, sizeof(frame->ghud)); // initialize to zero if oldframe is NULL + } + MSG_WriteByte(svc_ghudupdate); + qboolean written = false; + int i; + for (i = 0; i < maxelements; i++) + { + ghud_element_t *element = &client->ghud[i]; + size_t old_size; + int uflags = 0; + + if (oldframe != NULL) { + uflags = MSG_DeltaGhud(&oldframe->ghud[i], element, protocolflags); + if (oldframe->ghud[i].flags & GHF_FORCE || element->flags & GHF_FORCE) + uflags |= 0x7F; + } else { + if (element->flags & GHF_FORCE) + uflags |= 0x7F; + } + + if (!uflags) + continue; + + old_size = msg_write.cursize; + + MSG_WriteByte(i); + MSG_WriteGhud(element, uflags); + + if (msg_write.cursize >= client->netchan.maxpacketlen) + { + msg_write.cursize = old_size; + break; + } + + written = true; + memcpy(&frame->ghud[i], element, sizeof(ghud_element_t)); // update the ghud since it made it into the frame + element->flags &= ~GHF_FORCE; + } + + if (written) + MSG_WriteByte(255); + else + msg_write.cursize--; + } +} +#endif + /* ============= SV_EmitPacketEntities @@ -223,6 +292,11 @@ static bool SV_EmitPacketEntities(client_t *client, const client_frame_t *from, } MSG_WriteShort(0); // end of packetentities + +#if AQTION_EXTENSION + if(from) + SV_Ghud_SendUpdateToClient(client, from, to); +#endif return ret; } @@ -412,67 +486,6 @@ bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) return SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum, maxsize); } -#if AQTION_EXTENSION -static void SV_Ghud_SendUpdateToClient(client_t *client, client_frame_t *oldframe, client_frame_t *frame) -{ - if (client->protocol == PROTOCOL_VERSION_AQTION && client->version >= PROTOCOL_VERSION_AQTION_GHUD) - { - int protocolflags, maxelements; - - maxelements = 0; - protocolflags = 0; - if (client->version >= PROTOCOL_VERSION_AQTION_GHUD2) - { - maxelements = MAX_GHUDS; - protocolflags |= 1; - } - else - { - maxelements = 64; // we have to obey old kinda broken ghud for older clients - } - - memcpy(frame->ghud, oldframe->ghud, sizeof(frame->ghud)); // use oldframe as baseline in case we can't fit all the updates in one package - - MSG_WriteByte(svc_ghudupdate); - qboolean written = false; - int i; - for (i = 0; i < maxelements; i++) - { - ghud_element_t *element = &client->ghud[i]; - size_t old_size; - int uflags; - - uflags = MSG_DeltaGhud(&oldframe->ghud[i], element, protocolflags); - if (oldframe->ghud[i].flags & GHF_FORCE || element->flags & GHF_FORCE) - uflags |= 0x7F; - - if (!uflags) - continue; - - old_size = msg_write.cursize; - - MSG_WriteByte(i); - MSG_WriteGhud(element, uflags); - - if (msg_write.cursize >= client->netchan.maxpacketlen) - { - msg_write.cursize = old_size; - break; - } - - written = true; - memcpy(&frame->ghud[i], element, sizeof(ghud_element_t)); // update the ghud since it made it into the frame - element->flags &= ~GHF_FORCE; - } - - if (written) - MSG_WriteByte(255); - else - msg_write.cursize--; - } -} -#endif - bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) { client_frame_t *frame, *oldframe; @@ -572,13 +585,8 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) // delta encode the entities MSG_WriteByte(svc_packetentities); -#if AQTION_EXTENSION - if (oldframe) - SV_Ghud_SendUpdateToClient(client, oldframe, frame); -#endif - return SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum, maxsize); - + return SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum, maxsize); } From 2953486f3dd7d4c5aecabecfa290930d7fbc9bac Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 15 Aug 2024 17:15:09 -0400 Subject: [PATCH 548/974] Comment update --- src/server/entities.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/entities.c b/src/server/entities.c index 7f11d30f9..44546a1b4 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -293,6 +293,9 @@ static bool SV_EmitPacketEntities(client_t *client, const client_frame_t *from, MSG_WriteShort(0); // end of packetentities +// This was moved from SV_WriteFrameToClient_Aqtion() because its +// return type changed from void to bool and it's now returning SV_EmitPacketEntities() +// rather than just calling it #if AQTION_EXTENSION if(from) SV_Ghud_SendUpdateToClient(client, from, to); From f85f3db7dcba670767606eac079d538c7d52af55 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 17:21:54 +0300 Subject: [PATCH 549/974] Add comment about NPOT textures. --- src/refresh/qgl.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 21648465c..3728224d5 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -257,6 +257,8 @@ static const glsection_t sections[] = { { .ver_gl = QGL_VER(3, 0), .ver_es = QGL_VER(3, 0), + // NPOT textures are technically GL 2.0, but only enable them on 3.0 to + // ensure full hardware support, including mipmaps. .caps = QGL_CAP_TEXTURE_MAX_LEVEL | QGL_CAP_TEXTURE_NON_POWER_OF_TWO, .functions = (const glfunction_t []) { QGL_FN(GetStringi), @@ -586,7 +588,7 @@ bool QGL_Init(void) } if (gl_config.ver_es) { - // don't ever attempt to use shaders with GL ES < 3.0 + // don't ever attempt to use shaders with GL ES < 3.0, or GLSL ES < 3.0 if (gl_config.ver_es < QGL_VER(3, 0) || gl_config.ver_sl < QGL_VER(3, 0)) gl_config.caps &= ~QGL_CAP_SHADER; @@ -597,7 +599,7 @@ bool QGL_Init(void) return false; } } else { - // don't ever attempt to use shaders with GL < 3.0 + // don't ever attempt to use shaders with GL < 3.0, or GLSL < 1.30 if (gl_config.ver_gl < QGL_VER(3, 0) || gl_config.ver_sl < QGL_VER(1, 30)) gl_config.caps &= ~QGL_CAP_SHADER; From d95bd704e0e2ceb6884e54d66c5d9367728e45b3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 19:39:00 +0300 Subject: [PATCH 550/974] Update glow texture parameters if present. --- src/refresh/texture.c | 59 ++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 8df3b80e8..1b021ca2d 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -71,10 +71,28 @@ static const glmode_t filterModes[] = { static const int numFilterModes = q_countof(filterModes); +static void update_image_params(unsigned mask) +{ + int i; + const image_t *image; + + for (i = 0, image = r_images; i < r_numImages; i++, image++) { + if (!(mask & BIT(image->type))) + continue; + + GL_ForceTexture(0, image->texnum); + GL_SetFilterAndRepeat(image->type, image->flags); + + if (image->glow_texnum) { + GL_ForceTexture(0, image->glow_texnum); + GL_SetFilterAndRepeat(image->type, image->flags); + } + } +} + static void gl_texturemode_changed(cvar_t *self) { - int i; - image_t *image; + int i; for (i = 0; i < numFilterModes; i++) { if (!Q_stricmp(filterModes[i].name, self->string)) @@ -92,12 +110,7 @@ static void gl_texturemode_changed(cvar_t *self) } // change all the existing mipmap texture objects - for (i = 0, image = r_images; i < r_numImages; i++, image++) { - if (image->type == IT_WALL || image->type == IT_SKIN || image->type == IT_SKY) { - GL_ForceTexture(0, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - } - } + update_image_params(BIT(IT_WALL) | BIT(IT_SKIN) | BIT(IT_SKY)); } static void gl_texturemode_g(genctx_t *ctx) @@ -111,8 +124,6 @@ static void gl_texturemode_g(genctx_t *ctx) static void gl_anisotropy_changed(cvar_t *self) { - int i; - image_t *image; GLfloat value = 1; if (!(gl_config.caps & QGL_CAP_TEXTURE_ANISOTROPY)) @@ -122,41 +133,19 @@ static void gl_anisotropy_changed(cvar_t *self) gl_filter_anisotropy = Cvar_ClampValue(self, 1, value); // change all the existing mipmap texture objects - for (i = 0, image = r_images; i < r_numImages; i++, image++) { - if (image->type == IT_WALL || image->type == IT_SKIN) { - GL_ForceTexture(0, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - } - } + update_image_params(BIT(IT_WALL) | BIT(IT_SKIN)); } static void gl_bilerp_chars_changed(cvar_t *self) { - int i; - image_t *image; - // change all the existing charset texture objects - for (i = 0, image = r_images; i < r_numImages; i++, image++) { - if (image->type == IT_FONT) { - GL_ForceTexture(0, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - } - } + update_image_params(BIT(IT_FONT)); } static void gl_bilerp_pics_changed(cvar_t *self) { - int i; - image_t *image; - // change all the existing pic texture objects - for (i = 0, image = r_images; i < r_numImages; i++, image++) { - if (image->type == IT_PIC) { - GL_ForceTexture(0, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - } - } - + update_image_params(BIT(IT_PIC)); GL_InitRawTexture(); } From 209e9cf1472a4a69fcf54a33e49aa33fd67a2661 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 19:50:47 +0300 Subject: [PATCH 551/974] Remove Scrap_Shutdown(). --- src/refresh/texture.c | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 1b021ca2d..4a1b75b76 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -260,17 +260,8 @@ static bool scrap_dirty; static void Scrap_Init(void) { // make scrap texture initially transparent + memset(scrap_inuse, 0, sizeof(scrap_inuse)); memset(scrap_data, 0, sizeof(scrap_data)); -} - -static void Scrap_Shutdown(void) -{ - int i; - - for (i = 0; i < SCRAP_BLOCK_WIDTH; i++) { - scrap_inuse[i] = 0; - } - scrap_dirty = false; } @@ -1117,8 +1108,8 @@ void GL_ShutdownImages(void) r_charset = 0; #endif + scrap_dirty = false; + IMG_FreeAll(); IMG_Shutdown(); - - Scrap_Shutdown(); } From b20321f2c60ecc2765d4dfa47e5095e33ce122d3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 19:52:44 +0300 Subject: [PATCH 552/974] Clean up building gamma/intensity tables. --- src/refresh/texture.c | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 4a1b75b76..2a25c88e0 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -757,20 +757,18 @@ void IMG_ReadPixels(screenshot_t *s) static void GL_BuildIntensityTable(void) { int i, j; - float f; + float f = Cvar_ClampValue(gl_intensity, 1, 5); - f = Cvar_ClampValue(gl_intensity, 1, 5); - if (gl_static.use_shaders) - f = 1; - for (i = 0; i < 256; i++) { - j = i * f; - if (j > 255) { - j = 255; - } - intensitytable[i] = j; + if (gl_static.use_shaders || f == 1.0f) { + for (i = 0; i < 256; i++) + intensitytable[i] = i; + j = 255; + } else { + for (i = 0; i < 256; i++) + intensitytable[i] = min(i * f, 255); + j = 255.0f / f; } - j = 255.0f / f; gl_static.inverse_intensity_33 = MakeColor(j, j, j, 85); gl_static.inverse_intensity_66 = MakeColor(j, j, j, 170); gl_static.inverse_intensity_100 = MakeColor(j, j, j, 255); @@ -789,10 +787,7 @@ static void GL_BuildGammaTables(void) } else { for (i = 0; i < 256; i++) { inf = 255 * pow((i + 0.5) / 255.5, g) + 0.5; - if (inf > 255) { - inf = 255; - } - gammatable[i] = inf; + gammatable[i] = min(inf, 255); gammaintensitytable[i] = intensitytable[gammatable[i]]; } } From 4b126b18eedd6d4fe060c627aaeeb5f3b988f045 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 19:54:42 +0300 Subject: [PATCH 553/974] Skip GL_LightScaleTexture() in identity case. --- src/refresh/texture.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 2a25c88e0..92f47aede 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -300,6 +300,7 @@ static byte gammatable[256]; static byte intensitytable[256]; static byte gammaintensitytable[256]; static float colorscale; +static bool lightscale; /* ================ @@ -356,6 +357,8 @@ static void GL_LightScaleTexture(byte *in, int inwidth, int inheight, imagetype_ if (r_config.flags & QVF_GAMMARAMP) return; + if (!lightscale) + return; p = in; c = inwidth * inheight; @@ -1047,6 +1050,7 @@ void GL_InitImages(void) // FIXME: the name 'saturation' is misleading in this context colorscale = Cvar_ClampValue(gl_saturation, 0, 1); + lightscale = !(gl_gamma->value == 1.0f && (gl_static.use_shaders || gl_intensity->value == 1.0f)); gl_texturemode_changed(gl_texturemode); gl_texturebits_changed(gl_texturebits); From adeacf11d321b25e328b2a2b80d7257de17e3527 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 16 Aug 2024 11:11:52 -0400 Subject: [PATCH 554/974] Added dir from aq2-tng --- action/aq2-server.sh | 10 + action/aq2rtx.pkz | Bin 0 -> 672632 bytes action/bots/botdata.cfg | 9 + action/config/action.ini | 52 ++ action/config/aq2_2on2.cfg | 78 +++ action/config/aq2_3teams.cfg | 78 +++ action/config/aq2_dm.cfg | 77 +++ action/config/aq2_tp.cfg | 81 +++ action/config/configlist.ini | 5 + action/config/maplist.ini | 24 + action/config/sndlist.ini | 61 ++ action/config/tourney.ini | 87 +++ action/default-original.esp | 30 + action/doc/adf.txt | 52 ++ action/doc/aq2-configs.html | 682 +++++++++++++++++++ action/doc/config-generator.html | 31 + action/doc/create_aq2-config.php | 126 ++++ action/doc/create_mode-config.php | 170 +++++ action/doc/manual.txt | 513 ++++++++++++++ action/doc/mode-configs.html | 899 +++++++++++++++++++++++++ action/doc/readme.txt | 163 +++++ action/doc/tngcvar.txt | 100 +++ action/doc/variables.txt | 38 ++ action/map.ctf | 36 + action/models/ctf/banner/skin.pcx | Bin 0 -> 53667 bytes action/models/ctf/banner/skin2.pcx | Bin 0 -> 51955 bytes action/models/ctf/banner/small.md2 | Bin 0 -> 13492 bytes action/models/ctf/banner/tris.md2 | Bin 0 -> 13492 bytes action/models/flags/flag1.md2 | Bin 0 -> 29136 bytes action/models/flags/flag2.md2 | Bin 0 -> 29136 bytes action/models/items/breather/skin.pcx | Bin 0 -> 27687 bytes action/models/items/breather/tris.md2 | Bin 0 -> 4372 bytes action/pics/ctfsb1.pcx | Bin 0 -> 2375 bytes action/pics/ctfsb2.pcx | Bin 0 -> 2353 bytes action/pics/i_ctf1.pcx | Bin 0 -> 1293 bytes action/pics/i_ctf1d.pcx | Bin 0 -> 1124 bytes action/pics/i_ctf1t.pcx | Bin 0 -> 1121 bytes action/pics/i_ctf2.pcx | Bin 0 -> 1294 bytes action/pics/i_ctf2d.pcx | Bin 0 -> 1294 bytes action/pics/i_ctf2t.pcx | Bin 0 -> 1032 bytes action/pics/p_rebreather.pcx | Bin 0 -> 1279 bytes action/pics/sbfctf1.pcx | Bin 0 -> 967 bytes action/pics/sbfctf2.pcx | Bin 0 -> 949 bytes action/players/actionmale/ctf_b.pcx | Bin 0 -> 39455 bytes action/players/actionmale/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/actionmale/ctf_r.pcx | Bin 0 -> 39455 bytes action/players/actionmale/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/actionrally/ctf_b.pcx | Bin 0 -> 35634 bytes action/players/actionrally/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/actionrally/ctf_r.pcx | Bin 0 -> 35634 bytes action/players/actionrally/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/female/ctf_b.pcx | Bin 0 -> 36362 bytes action/players/female/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/female/ctf_r.pcx | Bin 0 -> 38382 bytes action/players/female/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/gijoe/ctf_b.pcx | Bin 0 -> 65287 bytes action/players/gijoe/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/gijoe/ctf_r.pcx | Bin 0 -> 72043 bytes action/players/gijoe/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/m/ctf_b.pcx | Bin 0 -> 35596 bytes action/players/m/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/m/ctf_r.pcx | Bin 0 -> 35563 bytes action/players/m/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/male/ctf_b.pcx | Bin 0 -> 27596 bytes action/players/male/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/male/ctf_r.pcx | Bin 0 -> 27596 bytes action/players/male/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/male/flag1.pcx | Bin 0 -> 9263 bytes action/players/male/flag2.pcx | Bin 0 -> 11487 bytes action/players/messiah/ctf_b.pcx | Bin 0 -> 52016 bytes action/players/messiah/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/messiah/ctf_r.pcx | Bin 0 -> 54402 bytes action/players/messiah/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/sas/ctf_b.pcx | Bin 0 -> 32907 bytes action/players/sas/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/sas/ctf_r.pcx | Bin 0 -> 32907 bytes action/players/sas/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/suit/ctf_b.pcx | Bin 0 -> 30515 bytes action/players/suit/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/suit/ctf_r.pcx | Bin 0 -> 31729 bytes action/players/suit/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/sydney/ctf_b.pcx | Bin 0 -> 30444 bytes action/players/sydney/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/sydney/ctf_r.pcx | Bin 0 -> 31397 bytes action/players/sydney/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/players/terror/ctf_b.pcx | Bin 0 -> 38709 bytes action/players/terror/ctf_b_i.pcx | Bin 0 -> 1294 bytes action/players/terror/ctf_r.pcx | Bin 0 -> 38709 bytes action/players/terror/ctf_r_i.pcx | Bin 0 -> 1293 bytes action/prules.ini | 381 +++++++++++ action/sound/misc/flashlight.wav | Bin 0 -> 866 bytes action/sound/tng/1_frag.wav | Bin 0 -> 9460 bytes action/sound/tng/1_minute.wav | Bin 0 -> 8766 bytes action/sound/tng/2_frags.wav | Bin 0 -> 9592 bytes action/sound/tng/3_frags.wav | Bin 0 -> 8706 bytes action/sound/tng/3_minutes.wav | Bin 0 -> 15268 bytes action/sound/tng/accuracy.wav | Bin 0 -> 7864 bytes action/sound/tng/clanwar.wav | Bin 0 -> 10608 bytes action/sound/tng/disabled.wav | Bin 0 -> 9234 bytes action/sound/tng/enabled.wav | Bin 0 -> 7778 bytes action/sound/tng/excellent.wav | Bin 0 -> 7304 bytes action/sound/tng/flagcap.wav | Bin 0 -> 55290 bytes action/sound/tng/flagret.wav | Bin 0 -> 13524 bytes action/sound/tng/flagtk.wav | Bin 0 -> 50598 bytes action/sound/tng/impressive.wav | Bin 0 -> 9954 bytes action/sound/tng/no_team_wins.wav | Bin 0 -> 15304 bytes action/sound/tng/team1_wins.wav | Bin 0 -> 15248 bytes action/sound/tng/team2_wins.wav | Bin 0 -> 13732 bytes action/sound/tng/team3_wins.wav | Bin 0 -> 16660 bytes action/sound/user/affirm.wav | Bin 0 -> 18498 bytes action/sound/user/cdtrust.wav | Bin 0 -> 34184 bytes action/sound/user/childic.wav | Bin 0 -> 50094 bytes action/sound/user/clint.wav | Bin 0 -> 32300 bytes action/sound/user/comeback.wav | Bin 0 -> 24078 bytes action/sound/user/filhands.wav | Bin 0 -> 31738 bytes action/sound/user/grimley.wav | Bin 0 -> 18784 bytes action/sound/user/iknow.wav | Bin 0 -> 44240 bytes action/sound/user/isee.wav | Bin 0 -> 28156 bytes action/sound/user/killme.wav | Bin 0 -> 168588 bytes action/sound/user/l1.wav | Bin 0 -> 52062 bytes action/sound/user/l2.wav | Bin 0 -> 45430 bytes action/sound/user/letsgo.wav | Bin 0 -> 4770 bytes action/sound/user/letsrock.wav | Bin 0 -> 9514 bytes action/sound/user/lookwhat.wav | Bin 0 -> 18882 bytes action/sound/user/mustbe.wav | Bin 0 -> 24492 bytes action/sound/user/newbie.wav | Bin 0 -> 16348 bytes action/sound/user/pressluck.wav | Bin 0 -> 31948 bytes action/sound/user/s1.wav | Bin 0 -> 88848 bytes action/sound/user/s2.wav | Bin 0 -> 133744 bytes action/sound/user/s3.wav | Bin 0 -> 87638 bytes action/sound/user/terminat.wav | Bin 0 -> 18220 bytes action/sound/user/thanku.wav | Bin 0 -> 10350 bytes action/sound/user/vista.wav | Bin 0 -> 55396 bytes action/sound/user/whyuss.wav | Bin 0 -> 28606 bytes action/sound/user/wrong.wav | Bin 0 -> 32892 bytes action/sound/user/wtsamat.wav | Bin 0 -> 16550 bytes action/sound/user/yuck2.wav | Bin 0 -> 29994 bytes action/sound/weapons/blastf1a.wav | Bin 0 -> 18606 bytes action/sound/weapons/hyprbf1a.wav | Bin 0 -> 40524 bytes action/sound/weapons/machgf1b.wav | Bin 0 -> 16692 bytes action/sound/weapons/machgf2b.wav | Bin 0 -> 16692 bytes action/sound/weapons/machgf3b.wav | Bin 0 -> 16692 bytes action/sound/weapons/machgf4b.wav | Bin 0 -> 16692 bytes action/sound/weapons/machgf5b.wav | Bin 0 -> 16692 bytes action/sound/weapons/rocklf1a.wav | Bin 0 -> 37634 bytes action/sound/weapons/rocklr1b.wav | Bin 0 -> 44 bytes action/sound/weapons/shotgf1b.wav | Bin 0 -> 34902 bytes action/sound/weapons/shotgr1b.wav | Bin 0 -> 44 bytes action/sound/weapons/sshotf1b.wav | Bin 0 -> 33086 bytes action/terrain/act2r2.ltk | Bin 0 -> 32652 bytes action/terrain/actcity3.ltk | Bin 0 -> 34780 bytes action/terrain/actdam.ltk | Bin 0 -> 91674 bytes action/terrain/asylum.ltk | Bin 0 -> 19138 bytes action/terrain/beer.ltk | Bin 0 -> 209580 bytes action/terrain/bwcity2.ltk | Bin 0 -> 84858 bytes action/terrain/country.ltk | Bin 0 -> 17148 bytes action/terrain/country2.ltk | Bin 0 -> 23418 bytes action/terrain/countryside.ltk | Bin 0 -> 22530 bytes action/terrain/gangsta.ltk | Bin 0 -> 264780 bytes action/terrain/ground3.ltk | Bin 0 -> 64578 bytes action/terrain/matrix2.ltk | Bin 0 -> 15628 bytes action/terrain/terminal.ltk | Bin 0 -> 168858 bytes action/terrain/tokyo.ltk | Bin 0 -> 154012 bytes action/terrain/urban.ltk | Bin 0 -> 95130 bytes action/terrain/urban2.ltk | Bin 0 -> 82380 bytes action/terrain/urban3.ltk | Bin 0 -> 76738 bytes action/terrain/urban4.ltk | Bin 0 -> 44538 bytes action/terrain/village2.ltk | Bin 0 -> 30588 bytes action/terrain/wfall.ltk | Bin 0 -> 99580 bytes action/tng/act2r2.esp | 28 + action/tng/actcity2.aqg | 54 ++ action/tng/actcity2.esp | 37 + action/tng/actcity2.flg | 4 + action/tng/actcity3.aqg | 29 + action/tng/actcity3.dom | 2 + action/tng/actcity3.flg | 3 + action/tng/aggression.aqg | 42 ++ action/tng/aggression.flg | 3 + action/tng/aq2ctf1.ctf | 16 + action/tng/arabtown.ctf | 16 + action/tng/arabtown2.ctf | 16 + action/tng/armyterr.flg | 3 + action/tng/artmuseum.ctf | 16 + action/tng/assassin2.ctf | 16 + action/tng/asylum.aqg | 49 ++ action/tng/asylum.flg | 3 + action/tng/avenue1.aqg | 51 ++ action/tng/avenue1.flg | 3 + action/tng/backyard.ctf | 16 + action/tng/bank.aqg | 11 + action/tng/barnyard.esp | 28 + action/tng/beach.ctf | 16 + action/tng/beer.aqg | 40 ++ action/tng/beer.flg | 3 + action/tng/blockwar.aqg | 63 ++ action/tng/border.flg | 3 + action/tng/bwcity2.ctf | 16 + action/tng/bxtrain2.flg | 3 + action/tng/chasm.aqg | 38 ++ action/tng/city.aqg | 54 ++ action/tng/city.flg | 3 + action/tng/cliff.aqg | 44 ++ action/tng/cliff.dom | 2 + action/tng/cliff2.flg | 3 + action/tng/cloud.aqg | 37 + action/tng/colt.aqg | 80 +++ action/tng/colt.flg | 3 + action/tng/country.ctf | 17 + action/tng/country2.ctf | 16 + action/tng/ctbcity.aqg | 60 ++ action/tng/ctbcity.flg | 3 + action/tng/ctfsludge.ctf | 16 + action/tng/deepcanyon.aqg | 22 + action/tng/default.esp | 33 + action/tng/desert.aqg | 44 ++ action/tng/desert.flg | 3 + action/tng/ffrontal.aqg | 34 + action/tng/ffrontal.flg | 3 + action/tng/flooded.ctf | 16 + action/tng/hidden3.ctf | 16 + action/tng/jkduel2.ctf | 16 + action/tng/jungle1.flg | 3 + action/tng/kumanru.aqg | 30 + action/tng/lavatube.flg | 3 + action/tng/leaf.esp | 29 + action/tng/lighthouse.aqg | 53 ++ action/tng/lighthouse.dom | 2 + action/tng/lighthouse.esp | 28 + action/tng/lighthouse.flg | 3 + action/tng/mesto.esp | 28 + action/tng/metropol.ctf | 16 + action/tng/monastery.aqg | 53 ++ action/tng/murder.aqg | 51 ++ action/tng/museum.aqg | 26 + action/tng/museum.flg | 3 + action/tng/nine2.esp | 28 + action/tng/ninja.aqg | 11 + action/tng/p1_lightbeam.esp | 28 + action/tng/pier.ctf | 19 + action/tng/rhcity1.aqg | 57 ++ action/tng/rhcity1.flg | 3 + action/tng/ricochet.ctf | 16 + action/tng/ricochet.esp | 28 + action/tng/riot.aqg | 48 ++ action/tng/riot.flg | 3 + action/tng/riot2.aqg | 53 ++ action/tng/riotx.aqg | 51 ++ action/tng/rok.ctf | 16 + action/tng/rok.esp | 35 + action/tng/rooftops.ctf | 16 + action/tng/ruins3.flg | 3 + action/tng/santa.esp | 28 + action/tng/siege2.aqg | 51 ++ action/tng/siege2.ctf | 16 + action/tng/siege2.flg | 3 + action/tng/sludge1.flg | 3 + action/tng/soho.aqg | 53 ++ action/tng/spring.esp | 28 + action/tng/subway2.flg | 3 + action/tng/teacher.aqg | 50 ++ action/tng/teamdepo.aqg | 58 ++ action/tng/teamdepo.ctf | 16 + action/tng/teamjungle.aqg | 32 + action/tng/teamjungle.flg | 3 + action/tng/temhole.aqg | 8 + action/tng/tjt.ctf | 16 + action/tng/tokyo.flg | 3 + action/tng/torg.esp | 28 + action/tng/urban.aqg | 40 ++ action/tng/urban.ctf | 16 + action/tng/urban2.aqg | 50 ++ action/tng/urban2.flg | 3 + action/tng/urban3.aqg | 54 ++ action/tng/urban3.flg | 3 + action/tng/urban4.aqg | 55 ++ action/tng/urban4.ctf | 16 + action/tng/urban4.dom | 2 + action/tng/urban4.esp | 36 + action/tng/urbanjungle.ctf | 16 + action/tng/waldoworldarena.dom | 2 + action/tng/wfall.ctf | 15 + action/tng/winter.aqg | 36 + action/tng/winter.ctf | 16 + action/tng/winter.flg | 3 + action/tng/zoo.ctf | 16 + 285 files changed, 6457 insertions(+) create mode 100755 action/aq2-server.sh create mode 100644 action/aq2rtx.pkz create mode 100644 action/bots/botdata.cfg create mode 100644 action/config/action.ini create mode 100644 action/config/aq2_2on2.cfg create mode 100644 action/config/aq2_3teams.cfg create mode 100644 action/config/aq2_dm.cfg create mode 100644 action/config/aq2_tp.cfg create mode 100644 action/config/configlist.ini create mode 100644 action/config/maplist.ini create mode 100644 action/config/sndlist.ini create mode 100644 action/config/tourney.ini create mode 100644 action/default-original.esp create mode 100644 action/doc/adf.txt create mode 100644 action/doc/aq2-configs.html create mode 100644 action/doc/config-generator.html create mode 100644 action/doc/create_aq2-config.php create mode 100644 action/doc/create_mode-config.php create mode 100644 action/doc/manual.txt create mode 100644 action/doc/mode-configs.html create mode 100644 action/doc/readme.txt create mode 100644 action/doc/tngcvar.txt create mode 100644 action/doc/variables.txt create mode 100644 action/map.ctf create mode 100644 action/models/ctf/banner/skin.pcx create mode 100644 action/models/ctf/banner/skin2.pcx create mode 100644 action/models/ctf/banner/small.md2 create mode 100644 action/models/ctf/banner/tris.md2 create mode 100644 action/models/flags/flag1.md2 create mode 100644 action/models/flags/flag2.md2 create mode 100644 action/models/items/breather/skin.pcx create mode 100644 action/models/items/breather/tris.md2 create mode 100644 action/pics/ctfsb1.pcx create mode 100644 action/pics/ctfsb2.pcx create mode 100644 action/pics/i_ctf1.pcx create mode 100755 action/pics/i_ctf1d.pcx create mode 100644 action/pics/i_ctf1t.pcx create mode 100644 action/pics/i_ctf2.pcx create mode 100755 action/pics/i_ctf2d.pcx create mode 100644 action/pics/i_ctf2t.pcx create mode 100755 action/pics/p_rebreather.pcx create mode 100644 action/pics/sbfctf1.pcx create mode 100644 action/pics/sbfctf2.pcx create mode 100755 action/players/actionmale/ctf_b.pcx create mode 100644 action/players/actionmale/ctf_b_i.pcx create mode 100755 action/players/actionmale/ctf_r.pcx create mode 100644 action/players/actionmale/ctf_r_i.pcx create mode 100755 action/players/actionrally/ctf_b.pcx create mode 100644 action/players/actionrally/ctf_b_i.pcx create mode 100755 action/players/actionrally/ctf_r.pcx create mode 100644 action/players/actionrally/ctf_r_i.pcx create mode 100644 action/players/female/ctf_b.pcx create mode 100644 action/players/female/ctf_b_i.pcx create mode 100644 action/players/female/ctf_r.pcx create mode 100644 action/players/female/ctf_r_i.pcx create mode 100755 action/players/gijoe/ctf_b.pcx create mode 100644 action/players/gijoe/ctf_b_i.pcx create mode 100755 action/players/gijoe/ctf_r.pcx create mode 100644 action/players/gijoe/ctf_r_i.pcx create mode 100755 action/players/m/ctf_b.pcx create mode 100644 action/players/m/ctf_b_i.pcx create mode 100755 action/players/m/ctf_r.pcx create mode 100644 action/players/m/ctf_r_i.pcx create mode 100644 action/players/male/ctf_b.pcx create mode 100644 action/players/male/ctf_b_i.pcx create mode 100644 action/players/male/ctf_r.pcx create mode 100644 action/players/male/ctf_r_i.pcx create mode 100644 action/players/male/flag1.pcx create mode 100644 action/players/male/flag2.pcx create mode 100755 action/players/messiah/ctf_b.pcx create mode 100644 action/players/messiah/ctf_b_i.pcx create mode 100755 action/players/messiah/ctf_r.pcx create mode 100644 action/players/messiah/ctf_r_i.pcx create mode 100755 action/players/sas/ctf_b.pcx create mode 100644 action/players/sas/ctf_b_i.pcx create mode 100755 action/players/sas/ctf_r.pcx create mode 100644 action/players/sas/ctf_r_i.pcx create mode 100755 action/players/suit/ctf_b.pcx create mode 100644 action/players/suit/ctf_b_i.pcx create mode 100755 action/players/suit/ctf_r.pcx create mode 100644 action/players/suit/ctf_r_i.pcx create mode 100755 action/players/sydney/ctf_b.pcx create mode 100644 action/players/sydney/ctf_b_i.pcx create mode 100755 action/players/sydney/ctf_r.pcx create mode 100644 action/players/sydney/ctf_r_i.pcx create mode 100755 action/players/terror/ctf_b.pcx create mode 100644 action/players/terror/ctf_b_i.pcx create mode 100755 action/players/terror/ctf_r.pcx create mode 100644 action/players/terror/ctf_r_i.pcx create mode 100644 action/prules.ini create mode 100644 action/sound/misc/flashlight.wav create mode 100644 action/sound/tng/1_frag.wav create mode 100644 action/sound/tng/1_minute.wav create mode 100644 action/sound/tng/2_frags.wav create mode 100644 action/sound/tng/3_frags.wav create mode 100644 action/sound/tng/3_minutes.wav create mode 100644 action/sound/tng/accuracy.wav create mode 100644 action/sound/tng/clanwar.wav create mode 100644 action/sound/tng/disabled.wav create mode 100644 action/sound/tng/enabled.wav create mode 100644 action/sound/tng/excellent.wav create mode 100644 action/sound/tng/flagcap.wav create mode 100644 action/sound/tng/flagret.wav create mode 100644 action/sound/tng/flagtk.wav create mode 100644 action/sound/tng/impressive.wav create mode 100644 action/sound/tng/no_team_wins.wav create mode 100644 action/sound/tng/team1_wins.wav create mode 100644 action/sound/tng/team2_wins.wav create mode 100644 action/sound/tng/team3_wins.wav create mode 100644 action/sound/user/affirm.wav create mode 100644 action/sound/user/cdtrust.wav create mode 100644 action/sound/user/childic.wav create mode 100644 action/sound/user/clint.wav create mode 100644 action/sound/user/comeback.wav create mode 100644 action/sound/user/filhands.wav create mode 100644 action/sound/user/grimley.wav create mode 100644 action/sound/user/iknow.wav create mode 100644 action/sound/user/isee.wav create mode 100644 action/sound/user/killme.wav create mode 100644 action/sound/user/l1.wav create mode 100644 action/sound/user/l2.wav create mode 100644 action/sound/user/letsgo.wav create mode 100644 action/sound/user/letsrock.wav create mode 100644 action/sound/user/lookwhat.wav create mode 100644 action/sound/user/mustbe.wav create mode 100644 action/sound/user/newbie.wav create mode 100644 action/sound/user/pressluck.wav create mode 100644 action/sound/user/s1.wav create mode 100644 action/sound/user/s2.wav create mode 100644 action/sound/user/s3.wav create mode 100644 action/sound/user/terminat.wav create mode 100644 action/sound/user/thanku.wav create mode 100644 action/sound/user/vista.wav create mode 100644 action/sound/user/whyuss.wav create mode 100644 action/sound/user/wrong.wav create mode 100644 action/sound/user/wtsamat.wav create mode 100644 action/sound/user/yuck2.wav create mode 100644 action/sound/weapons/blastf1a.wav create mode 100644 action/sound/weapons/hyprbf1a.wav create mode 100644 action/sound/weapons/machgf1b.wav create mode 100644 action/sound/weapons/machgf2b.wav create mode 100644 action/sound/weapons/machgf3b.wav create mode 100644 action/sound/weapons/machgf4b.wav create mode 100644 action/sound/weapons/machgf5b.wav create mode 100644 action/sound/weapons/rocklf1a.wav create mode 100644 action/sound/weapons/rocklr1b.wav create mode 100644 action/sound/weapons/shotgf1b.wav create mode 100644 action/sound/weapons/shotgr1b.wav create mode 100644 action/sound/weapons/sshotf1b.wav create mode 100644 action/terrain/act2r2.ltk create mode 100644 action/terrain/actcity3.ltk create mode 100644 action/terrain/actdam.ltk create mode 100644 action/terrain/asylum.ltk create mode 100644 action/terrain/beer.ltk create mode 100644 action/terrain/bwcity2.ltk create mode 100644 action/terrain/country.ltk create mode 100644 action/terrain/country2.ltk create mode 100644 action/terrain/countryside.ltk create mode 100644 action/terrain/gangsta.ltk create mode 100755 action/terrain/ground3.ltk create mode 100644 action/terrain/matrix2.ltk create mode 100644 action/terrain/terminal.ltk create mode 100644 action/terrain/tokyo.ltk create mode 100644 action/terrain/urban.ltk create mode 100644 action/terrain/urban2.ltk create mode 100644 action/terrain/urban3.ltk create mode 100644 action/terrain/urban4.ltk create mode 100644 action/terrain/village2.ltk create mode 100644 action/terrain/wfall.ltk create mode 100644 action/tng/act2r2.esp create mode 100644 action/tng/actcity2.aqg create mode 100644 action/tng/actcity2.esp create mode 100644 action/tng/actcity2.flg create mode 100644 action/tng/actcity3.aqg create mode 100644 action/tng/actcity3.dom create mode 100644 action/tng/actcity3.flg create mode 100644 action/tng/aggression.aqg create mode 100644 action/tng/aggression.flg create mode 100644 action/tng/aq2ctf1.ctf create mode 100644 action/tng/arabtown.ctf create mode 100644 action/tng/arabtown2.ctf create mode 100644 action/tng/armyterr.flg create mode 100644 action/tng/artmuseum.ctf create mode 100644 action/tng/assassin2.ctf create mode 100644 action/tng/asylum.aqg create mode 100644 action/tng/asylum.flg create mode 100644 action/tng/avenue1.aqg create mode 100644 action/tng/avenue1.flg create mode 100644 action/tng/backyard.ctf create mode 100644 action/tng/bank.aqg create mode 100644 action/tng/barnyard.esp create mode 100644 action/tng/beach.ctf create mode 100644 action/tng/beer.aqg create mode 100644 action/tng/beer.flg create mode 100644 action/tng/blockwar.aqg create mode 100644 action/tng/border.flg create mode 100644 action/tng/bwcity2.ctf create mode 100644 action/tng/bxtrain2.flg create mode 100644 action/tng/chasm.aqg create mode 100644 action/tng/city.aqg create mode 100644 action/tng/city.flg create mode 100644 action/tng/cliff.aqg create mode 100644 action/tng/cliff.dom create mode 100644 action/tng/cliff2.flg create mode 100644 action/tng/cloud.aqg create mode 100644 action/tng/colt.aqg create mode 100644 action/tng/colt.flg create mode 100644 action/tng/country.ctf create mode 100644 action/tng/country2.ctf create mode 100644 action/tng/ctbcity.aqg create mode 100644 action/tng/ctbcity.flg create mode 100644 action/tng/ctfsludge.ctf create mode 100644 action/tng/deepcanyon.aqg create mode 100644 action/tng/default.esp create mode 100644 action/tng/desert.aqg create mode 100644 action/tng/desert.flg create mode 100644 action/tng/ffrontal.aqg create mode 100644 action/tng/ffrontal.flg create mode 100644 action/tng/flooded.ctf create mode 100644 action/tng/hidden3.ctf create mode 100644 action/tng/jkduel2.ctf create mode 100644 action/tng/jungle1.flg create mode 100644 action/tng/kumanru.aqg create mode 100644 action/tng/lavatube.flg create mode 100644 action/tng/leaf.esp create mode 100644 action/tng/lighthouse.aqg create mode 100644 action/tng/lighthouse.dom create mode 100644 action/tng/lighthouse.esp create mode 100644 action/tng/lighthouse.flg create mode 100644 action/tng/mesto.esp create mode 100644 action/tng/metropol.ctf create mode 100644 action/tng/monastery.aqg create mode 100644 action/tng/murder.aqg create mode 100644 action/tng/museum.aqg create mode 100644 action/tng/museum.flg create mode 100644 action/tng/nine2.esp create mode 100644 action/tng/ninja.aqg create mode 100644 action/tng/p1_lightbeam.esp create mode 100644 action/tng/pier.ctf create mode 100644 action/tng/rhcity1.aqg create mode 100644 action/tng/rhcity1.flg create mode 100644 action/tng/ricochet.ctf create mode 100644 action/tng/ricochet.esp create mode 100644 action/tng/riot.aqg create mode 100644 action/tng/riot.flg create mode 100644 action/tng/riot2.aqg create mode 100644 action/tng/riotx.aqg create mode 100644 action/tng/rok.ctf create mode 100644 action/tng/rok.esp create mode 100644 action/tng/rooftops.ctf create mode 100644 action/tng/ruins3.flg create mode 100644 action/tng/santa.esp create mode 100644 action/tng/siege2.aqg create mode 100644 action/tng/siege2.ctf create mode 100644 action/tng/siege2.flg create mode 100644 action/tng/sludge1.flg create mode 100644 action/tng/soho.aqg create mode 100644 action/tng/spring.esp create mode 100644 action/tng/subway2.flg create mode 100644 action/tng/teacher.aqg create mode 100644 action/tng/teamdepo.aqg create mode 100644 action/tng/teamdepo.ctf create mode 100644 action/tng/teamjungle.aqg create mode 100644 action/tng/teamjungle.flg create mode 100644 action/tng/temhole.aqg create mode 100644 action/tng/tjt.ctf create mode 100644 action/tng/tokyo.flg create mode 100644 action/tng/torg.esp create mode 100644 action/tng/urban.aqg create mode 100644 action/tng/urban.ctf create mode 100644 action/tng/urban2.aqg create mode 100644 action/tng/urban2.flg create mode 100644 action/tng/urban3.aqg create mode 100644 action/tng/urban3.flg create mode 100644 action/tng/urban4.aqg create mode 100644 action/tng/urban4.ctf create mode 100644 action/tng/urban4.dom create mode 100644 action/tng/urban4.esp create mode 100644 action/tng/urbanjungle.ctf create mode 100644 action/tng/waldoworldarena.dom create mode 100644 action/tng/wfall.ctf create mode 100644 action/tng/winter.aqg create mode 100644 action/tng/winter.ctf create mode 100644 action/tng/winter.flg create mode 100644 action/tng/zoo.ctf diff --git a/action/aq2-server.sh b/action/aq2-server.sh new file mode 100755 index 000000000..e0c9b63f3 --- /dev/null +++ b/action/aq2-server.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +cd .. +while true +do + ./quake2 +set dedicated 1 +set game action +set port 27910 +set ininame config/action.ini +set maplistname config/maplist.ini +set configlistname config/configlist.ini +exec config/aq2_tp.cfg + sleep 10 +done + + diff --git a/action/aq2rtx.pkz b/action/aq2rtx.pkz new file mode 100644 index 0000000000000000000000000000000000000000..daf39bfe2e1020115ef58e26daa266f0f5f5dbf8 GIT binary patch literal 672632 zcma&N2Uru)wl=KN1O-&2H|YWb0s_)Oq=|sid!+Z?OM+5GKzeUV5hpK$7@%S&r90B%y$MBahEmop;+LvJ7N_&D zMG~iT<||e8So9UVEn3QKt`~W|84Q#5gk=by9OkgJCr-?zQY38fCm5eHkMm+Z>*zZ^ z7P*t0V;1Q4ZvY$^;IU>rvR+ZowQYXV7s}7)~sO6nV3Xe!~Y(+%!$H!WxQ*Qu( zE?pXI?-P{=3j76$JI>tEH=p!@w7VopA5RA=ddC+jSr|>~6Iz8Th~NP~P#oPRKw6|) zOEY!|nLgDxt}c_LwEl>LS(Wcnj&&3f);cJb$a9fc;M>!T(~Rw*l7e-y_mBc0xAKN3mVBGH z%)GqwqpBxq(n>`^;+xu)%~SR5`HB4Hw+MAnnEGa5Qs!bZ$|oG5yLlx5Z6z^p_dEK8nb#H_5?q}-9ytqA2OIo*#~Eok^%A7H-+@;6HKOzYF4WE ztKU;{GwuH>{g6@2Gic<5un3I9G2IwVB}2G@yjZ_^>SVn81fzr|vpj3ZgTzZhS49pW-f(@wP?tC@w;*!fb z+bjNsy#Vd_yWY{>Zp)pm0mTeWV|IAI#oWDScC~G;cT!+saMEx*uaI`dctt0=4kpBS zV|AJ|s`*Bi`S#>UJS{aAimrPR#K+moI`_ti6Vx}}n{E(0CZ@z3jl;)MY{#u}5z2a_ zwaH~8(b`}0-dId?+C1gUY$A&C|J;yw?J$vVH#Wn--iKFrGb|S2?rnc|L{cQ?D-FJ$=y%*EY&s_Fk-AxrcYWBevEo^dL>Yk`6OSVWT5`Lk*PPK+9Q9Z868=!ai`{bAxE0$0xbj}0eFZ&t8$O<2W4m1HQZh3c)gX@r^ zqQ3mvVLZOTBbiu zo~h9z9N_FFvknF)CUt-l!)eE-u0ujmRsh}=y!XLhrT15} zo{gHn{_Gkfr|9-0`AhVX_I#s~YgAO#46n}=jQ!Er7#xksjz>mwOO^A11)CLohe1;FzM)0!bq&-kc z8F+Ok0Om$n_~2Mdi?W6+jGwOJo0ls8$@h}Mhl6tZr1}ai?_(?8$RCO97K=aJ$!z=j zw0Ah<$6iDkTl;eX7G2xajUQ>Gk!~~aoJS>CXV%|hJzIS~^ zpwoFI@Gz3KU4sL3m;2(O?gfGO{f)vS!acv}X8o|>D6i;Cy%6NvmOl)<-Ox8Jf7(hr z8WUUu(@KvR3ys$Sq&V5XD+a<_x*aNiy~yBDujqxYmBx6N1Wo*F<3eMr4|tHj$7r-< zJRh4s{@(UrDg>E;r2Q@*pnS;s=ZGInaI@rQS}5Bec&z-K1f;nn#t?hTguKH1RJp)| z3879>!Ex8Nm+>@bXgbn%%ci^IksjnFcg-$^p2&MD6vnMvS{x}x$o7_P2ByMccIUdWh<^sz3cL7)9ut(WnFyHOOvU+V|qi-gJcm2E1iV#7;j!9p@oP<1JD1Q?KHGAr0^c&~sI-}5 z+&5bl8j_hwaV)#skgaI!G43xH^SZ4e3?jHdw)5rs^CVd%_(w3EOWdg|>>cur^K#_h zXjD5aTZO0}sYh{dFTOWgDX%LZQwU0|*7KcqxN#Ncy(X9WJl7c)^`lw%iL&LH>u>4u zNrkb6yQ&QjSn!4#%_)M3Kn@J!?9Nh9LTFkQzzw8lZ2l&SWhBq%pqEuK)rJis(G)Yyz z@>MMT@kZCsh0*18vOy+a;RoHv16|R^JWqI4IM5pJC zqpZ~^E$k1!>*(rgQ|kCyaCp=bi)6E7Qz;^n!_q1iEO@%43%N8)%iMHT&)D^zh@}li zXvcTy;%kvqx43JUS~O8>k?LyeZYWJKR;a!hk&bsLRVifh+u={>DeYBlpwe90NUL6NvJ5{VYChr@aegI%bk-rG|?;zZo8e`mjRyiANh3DG&s61SsOo$=<@op z12EI2u?{^u)>Scy4Lln4Yo|gaB^eHJh%W{)>#3t3z2C?_a*4QwWz#6{XcLHz&`2f+ zJE!2pNa#Ajr35%-bLpptF|RTM&02bRZz`E50Tw!pI8v_KhdcRClCzSAUTWZ{*66Y} z$I3g9&SrZ>c)A>vw)|#y>Sb;I$)6p9b%*TyI)y&A>q}Co*@czZzqZXhbR+MzVe{6N zuEsoSWG*gzI`!a?i{iKc!`qr4peL5EBQI<2>9A92u)%enn~^@#lHIMejNNJzx_8^2 z;YvnTP(5~%*)zkS0glxv%^V&kQUy%W*$cfQb}7WVy`vx8;e8AQXjH8kw1WJc`UBLz ztm(*hvweLOiCM~SDz2is8S8VmV2qI=-6h>(Eh@pOGz-Nx;Z4eksHiK*>JpaTb$BV2 z(S$ekNF3diqOvgu!p5JF#Ggu?!p%3QDS|wBxqnE13y4h$ z9y$G%EnXcfA9BhF?@^uIzVd{Pyp_}#`Lg9<@m=5W+GleY`c4k*zy$5pp{=Y*Q67oh z8o>8!Gr9v$WAFdQ(sFJraWXDTJaJgHM@&R*l}wK7|cGf{k`*2 z0=Bm^X%%5Kpb?NsAEY3n1`g35bn@fO{`M-%P(-Js=9_kd@`#@2B%MwAAYZVQ=CoXy zIXU*O)#WRHjZXMOr(d4)d6iTj@@OZ`A(_QgTDO-f(a&-}-V)S&;M0mWj3!V~`=L1L zOeJDip8C6JGMHst$nNh0tsb6QYV9EISbg7~pzb|i#8$ud5s6Do^oreCozcLEyWrHf zkb(@!*?yjy49&&dUmj&sboEqMMJi*XbS^)J(y!?1jlc!Dyb03^WhHPA8>j|YpMJFR z6J&2nsa_SV)M_I7(=_gON@_O7hYefYbmgR#HAS@vA+(3gmFleo6JM5p#KuB&MLqTB zel?Ua?xs7*&z1PFIB&qJX>TJ|H&chn+e{=>~J0VxGWJ(%6osMUleuu6TbWM zJaQRt_mRnc<}}ueJLlm?AYFXd&%XVS{h)JEY4sjD2OQJHb@LlEbD`Us9m-Sgx%(jr zD+RkReW8pVT8y2$_?TM8nY^&*Wr_uuW~t>2b!YJuw;x-Z8myz3QiN^Vr)~cO*Vy5@ z4d|Y|u&>bZ2iq0CBb!+QSAAtGHodJ$VS}7?SmT2$&j9=&Em*g0wJZrrYiq^(h-wxO zrR84}8F)~;8xo+AvkGgDG`>|VlsXIw9F-+UMVzcBwv<;?@$Xd+>J#kpoD;1KmH*M5 zMnBVQT^X8T{wz@KlNRMI=R@Jx*iv||a<`iQw_}gQh;*}@*#`mEgB1d?oazJPn26e*1IfUwBIN1+*=9a4ihN7H>+IkxGPZ&>jxF4 zO%ACLEWpoy1`{tmpze=85}CGU(<`2&Er(T6T~*5x>}8$NPNSdd1sb*r|7{eY;_hG5Ibuix*Y00R;X!}tEiJ^ye*(OJT`MNMV%2FYFh4{GJ>I$bQ|U93Dedm! zLoe=G92s_xhz-fAt6CqSDxlV&R7D%Y?ZKeb-)mQCj)YlC-4nFlZ zFg^c(p``pC$2AEmwQ74=7w= zQd!qS%ZumCyBo8}HQay?7yKC}^Z#|39dBz}&z)9VYzc(J zIvtR^=4WJ)FfhUbahT4{M>fzOJEVJBMIW0a1&wNKVTj_BT&?G9*g_jKO5UETzPXYG z@<=10^e-<8q8O6^xm5AV`o80OMoE3l^LK9pgnxF4F?*9?mruUcA5p6&`7V*bf^RD~ z94#oK8L|L@PR(MKgogp#CBA7R^v8(stj*+x9n~2BTfJ!3;+c6=c-iJ9NuWC?-J1Kf8 z8Ky27ne=t6eioy@s9s_Et8%jlb@to+KiMI{?SrIWcOFaR2M-<8PgGRyCBG8#aM8F( zZR8yZZsXhRIlkKHb0(*0^Y@(U(JP>*^s*nj`E>OCZlJaP(&mLE<2m_FfYqdczNxyO z22G-Tl%I)doA2>4%dU1tY@{}6ia@OF6PQ2jX)1PRJyx;$#m(0 zxDmy-uv4*|7KhMJ9w^ACO|m=I`)=m>y8v*eNLdzAfH3J+1}rHv-zB&u0zd>`wK4$Z zares-3lI#;+iR;fhAE8gn;l%@;Hpw7G_-4SN7hxhn=?i5E2sri-)%;xo4ItM?s? z#er&xk_&(Jo`33FU}tsTFkz)yGvRi9)WV^4FthP|sr`N;&6>wF_w0HJ;udz0%Ly^_ zj4N(jxo~Ao=9yAZI*!=%6!P&S0&7v{_0$b+4b1b_S7^+Js&;Mc~Kpk_dOyov&ng6<}hN7 zp&bS;DnKj@q6@aBCjYS*j%wYgH`QZ{TwC=qh|O83gXDG_0d9jQxyCgio|3(8F@Scd zT$S$no86+bOzrF4BEw408bJH2J?WHfeSs09yk-5$kf28N#6YDP>j0;VB)6#m7_ne< zy|*E{VDb*3*4J)aH}x%L{A|4ZjC37};2MuYZ+vEuRBCf9YOY9{Sj?~XaYk2wH=ipr z`?#d%tZs{7Jg&bM&JKLf`=XDXUCc3o#vNF4(Pe$t8*JBUEgNWU9@`7>mr{>ia&F4~ z8+btXYR75cG0Whfr`0BNUNRuZ*ipH%Id)AMUevs%>>17FUOZE+vA{OrdBCZ8oNXcI zc07rVI7aSK>~IB2sp`q&-p;qms_lAj`FvYZ`Hu=H{5|!nv26CDy`tu4&@}$Pbgq)&L#Wl#SfwH>cCn~TfU*M%dwS%P;XuCOteG7TY%x3)e%m5}hiHyR^oU91<8hw?ix&f~6l z8|>imH9A_f+>^IIPSnRf|F+D=@AH~*A!TLQ4op~p{Ky;}{0XAqlGOd|WY{_A;VX9u zbdKNrJ7IT;BfiVdLBVG46)ByOTq}=;1BtShnWR5gBcmb=4!xWnpl|ic@6d+D>|(ds z-7v+O(Adi7-3l@4x-fjk{g10Tm758iK2MRqohZ7EJ&}SOpfegU^R^i7XNGGO`3Tlm zWOznbL~0?b(pl+Z8sq50EFv(M4I=|hYQ!7t+05N^Pn=~U>JTRXY`BnH6USR3U zuWjvplWy>VqB-sPQ%gXN%u@XWzeMO}tD>L{@RW%=HyQyG`;``nMMHHO^$xOgK&BXdZ|qCAH5D%yeg$gZO3C2NMai5l z<}IO}-;eZV*qXtPS3t0~Z-_46SkN{3nFjJWVPjE%bojcD9yl;3D+Dz~+HzPPE}VNb ztQo^~w{rN3_SOMeoI95L=GBR+X&$s7PIPw z2c>-f8H^Dwx}cvNzdpCUls;ixyVCIcbgmq7d9#f&kOzf;q3uhq25lu1QU!61i!2oT zm*359A=K$KIRoNDW_C^zD`FF7>_x$4@*w!(!l7O7i}TtOKM6XhS~i0kB%D!wznGCe zXJU}+#M#-QPUjsI?*l!kTR;l935UNaK-lEp=V&=B13~H*0|82-a9|g0{f_%Rk@p8nsJY zwb_E1)YusARN%oY^0wooSI@O}JfGfo6mC?Or3b9eBhoOg z`X3ik=;s_6r)3)GB&mIeB+hNtu8bQ&07i-%Bqb(gd-t5G+i{V_*VTY1=H`R1JgBN& z!|MjAF}v1GEP`buM)rv|!s%cZ`_HbvR@<*mW14Ov8k^4oEef9A=9IH@ zC6Fx0e>a}L_TcFco(7}Nh;BSuJX=MZnd&tzH+mLd%Og190$eB*0->+k*3=~1a)zWs z&_g9jx32y?-}?)lIIpeJ(J%40=&@`b)=zBnx}$a+^IoDsuA30GZy0rHHKhIbtBqCq zZbT&p6+^7qyL|;h3!{KSy-VgEOgMHMc^rdUUOQP*5&lNCDM9zI|L zeEZ!nx-0(x*t=O+1P!$v$<{EyW*0i{-egTZ-Rt!g*JvO2^O+d{)?Hq8u|Q3va}!c= z5meZXU35(CDQJE<%~2jVkt)gFzqFTDrjlEPXy_0?gj}<#1#^jBZqQMu!EZd`>igCW zKY2)6J53v5Q3C<7%$ct8s?P5au2Dr$Kx<5;^K{PqjtvSsJ$!Y;JNp0v1&;@==)3r; zXkHZ#v|9-zwpT~A z^p?!{3`Fx=lCrPqgOR?VPcLcqZpxu`XRr&=GL z3q@s>J_mo}dHXS0>GuY+5%)(U%0f|R%KUy(Pt zdFa<&#<#Lz-{sbrz286x#7Qzd<{)JKf+}MBF8Dca6xRMxy<`Wpbd2N-egSq)QjZwv zC7;qDWnYoNXGU}JlzZ>3u@R9?Gc4UOVk2ayPM=$4YrM$|tN8W#C@0JY89mz2RJ5+p zcJv$S!hQKjAg8#mghoi==9K?z%sL>3e~c<1OVrXyI43mwi!{M4doQv>ifgmgj;Lsq z|6Rdz>(QjT&pz5@?98j-2LZB)K~G7#Sh;c)c+m@fn)I!?+f&ydzOP(>DJ24*7@h?gs;r zP2mfVWQ^01a=rBHR6?P(>T*jRmz>qN+}WsyFi)~A3G(Lq&G=VBFbsdMts8{4Ts9O4 z4ZQ*Miu12XxvrUPv1~oF{iN9`8-*qw&U&=v`3+{Mw)a)Cmtc`VGDpkZglIa! zU*^Pqp~dlwZJcF{#fg3*9?3Drv9DpIN&iUU$lJ2Me$>7S(KR27waJHx+Wa7Uz;2^W z6+~}OaVA&0sL)<@@|@bcQ7}^KlPHy()D!9Lh46dUK$ zFG)oa7}(!+{`0PV)CrS+)xfGxrQ|dmAqL1NY8^^U=WwF;tTN)%n`1qP`;a?_w#SpW z@7>&cFs4od+|~+L+UK2hZ{Ac~_~tGgSyB3Df5#wX4r} zCrcg;bNTU|%FL0MD~<$xQVb#biJ7?j>CqYQJ!5vi``A0AcnZHaSgR~L_kKj$;~PY_ zP=%)4HzSvRQ_vAG*@W*@y-Ye<@QTn$uAMqGg=Fczv=Y&m*xx|znJ1n4^2~(_9lA}C z9G|R1B7c&PnSY}3)gsj6_oFy94ytoyfF#L-` zBB~FZ%my8kmWC%PssCbL=ae}T8z*^E2W3GUXI;xfNlQl<*e0#Az`9ay4$J{~F73M> zj+uyA(>GlibA&B#dzVUG1RgU(G!!+>JOlFeO*6(Y6Iu<>Eua0S3FSQ*TDQ}MrQ90y zG;BClj7YkOSuy(C?W41_pr>Gy0MD1(t@XnDt?i2{w^q+qArpwq%h^_G%Dr=g+AN`wz;!4JVp_JFf|97lx;=+amT_DZ2pA znvFu?XSdVCW9O?0CVzrCEd6e-3k9Vk;6>;CCI@PH@Vyhzq_{u4ldzS1@aS;qm@pRPUh6Bt;>{S6F79vLYfsmbg4fm5p!L!RQj%z93k0kMrnI%qo|^zj1=FftFnp*`-# zJ;r74q4(CZvZqrxp*Ar&w=xZ>j+8 zc~>*kTUx0108M}byliVwWw5x?zjApZ6(+!tno!r*8B9$pu;gv{>r}Y05YP@dFT)H4 zuAA)U?CtG1Zu_5^(5haZis0#=KVVXwTO*S0BZbYA;6fXSEf=VT=L-Qq*_lcRGv<%v zZNpE9Fg5^77hoo>ucRdd91#Gtd~!ROPdcY@NEf{TNwDi9SvPmD{+IwO52MArJ3k=B zfzrU(^)?=A%**-RtD?#h)ZXBta0J%O{b6MRL=kJ?(LU7}Y1w46+nOJ@BRD+e8)kU{ zn-Y7&JYm@cm=ybB*_1XJi0Dg1CI=uHx?SJ4n*!g*jUjn%FPZ<9dg=UI>h*v17PByS zvHGu6?LRtgeeQJM82OuM1##cGL-+5=)<5n4Grfwtc0 zQQHIvLKk2RU|9iPU1Jivq5DSg@}!dkAV%C41w0qW5I%_1JgR@3aMzsmZyk~E@RJYf zZu1qNwLPJoBrSF>+ED!ZB9_9xx}d)o#5$z+;yIv&#}mDKY+WX_mBI&zZ*~}1-Jm5O z2Jqfi-m(_GYw3Td8ozOz{61Ns#zd!U3``MWe2-r;T zzt-)SIMmpem`L>Zsbmln)lTP?7x$*GbhoT^H~711Gjl z%D??u<5I)NF$+1Wn=jd)Vg}1b-rQMa8xggCc~6-v|`7wvnPzjg0Dp^d0QHYlxQEveqSQ67)hf>FuYE zrDx^JQj2%;ln{5sIt0d`VL|uiDNZX{p$=F&4;}Tgl!;+qBhgyGyd4i>tU>ti7uqma z0Ys)jO#dv(e7-2pzI&Z3-&qpmk|to6mjvd-_E+KWv$AuSw_1hQ7b(7U#c%FsbDT^3 zDiSj6qTrxBEGPvC)pRuU#cRvGU^^0fWA+8m37_aioo!E#i}pvK3S@#9o!JB{*>LrB zXY2N)&N(K_>_JXOf#RV6TZ03>Ie`_5ajHZ9t14ikdtinmvn676i0AxQrd}UES46nl zJ+#k*J=N5@je&x?8xJBdWngV?eZ6y{E$z(4nH084Qux;M48VW_d7G4S9{PR1+ubTQs_^KXna36k&Q9u!`XtlmMO}EvoF6- zQ2*o=I;&UT9NPnr4rVXE+-;+ThI)jFPpq6K^qVj}`GdL-QdV&Ubrq3%IG(h{Cgihc z=1s2`ivXWy(y-ODB#qV4oEX>p%K56ikSqgB%O03;!}{H`SH5R&kbMY*xp1*-Qp0I= zn?uE`(T!fFU!k?t2PEFjMcUk*Gd$ki6ECNyv#Rj+xOYt6#H^Hqt9|9>$x85wMTg%q z_o1GvzNL?R`atoqAv>E0k*b~KIMNJ#Q`N_ ztkv%_bR!tu+&Z9bn^gUj!tf#RTNXkiJK2gto2&4t3&gHgn6W8MOPx&OnLU}-h(<%P zy%*y{dssl$!yeW%LE1RQ@oiRvR(f|#N~5!8K?_MsA*ZS3NIwfnF0k zeieU33&kqPU<2cU3t=~gmCWOEc6ZtrB`T%NZ-0&ZQ+3h{l9`f>KeL<) zr9rJS3O(XxeAmqy5-3Jo8F18qg4e$K%}jNvVj9IiFqvY*M_eMSH}G~T(w6OlHmZU* zQE&e_D~xl9pe;eUTc-TM4_1R((>t+(myA7Gj@dDhM34U%*xgHh5VyrA^+hHv=KW`` zv8}{<)gvpv`?MMO>OYy^lxr_r%xL!hlG9$fv#0)rWE;;<=MMEaQ9po1iqxlHj^$O( z!}Z#(hf50-X!GY%YBnd%g2OsP3>mydRlU+gJeIJ#{U@XFFOdnM{-Z(DBO%n!xE*b% z(}LTxyqvzvA@GC-z`E8)@ehS&) zx9aqz^-=lJU?uE$4S8Ro8V~j{G-%*{Aa-6Pkx<6N4GzcHGAF{V>p@=(dl@ZjuzvWo*hlK|SEG`p>KnHXUBy|bw2?1@)Q zbpTb|$%y9@e%oR9?Wa0~dF)-GrASV8S5H z6%T_HxcJiL>Pw)Jrh@GH`(1x&HrYhxZZ+`?+aHLXG>-@ntL;YdshYU))+kgSnp}H9 zcrA=A%KnqrX*nEaHqzvI%-NPHjFC8{Dw7EtaXj`Eg5n#Iz^ylQw-Iu(X-a;io+II) zyI|{BC5SaL;!MUa?60;l+#ZW#^kXb*Ck4MO_7*us9rGHvhWb7`lhZ_3jzS-}cH{x% zp`u&<;cZ-7QO9t4xer%xYbl219BZ)Q{UTA{ma(#~{753?L;P#6;g6LR3EWHvg6~rcULj{8CsK%Bg zBFFY_(2^Iz2B<8gd|uX7(RZBiC3zE>fJ~%}5auJt^rne>+Ntr; zq1pvqTgGF+o8Vu_;ThR(+ifw5S=tNbC%|g$8lVbT1-Z#p} zzkO^eBo1*rj#jPAn4R?3uW^H-JN{U~;K7viucs5}yW&;6JewuEpJNPYW_`}y1-Cs_ zJ{t}^#_7;9=Ezt%Sp_Iys&YvwvXeElSW5SUssD2Hw-)o`zdsW?21z4p?bKEg!T5H4&V(BsTpk*CxV)+3qXFB~;|N?m=dN>OfB z5w4oO2FL<#-r93hFdsdr-ruZKSww_`nR@DJj^>i+P5ZCNK1jgL%+HBg0eDNU$?MyI z0P``&*^zo?_TXz3&tqK9$j;Rj;04zE{7)u;yjd^MD>U1)9mL(XjE~ZT+ zaw8+vxKkZ6rry`GRove8vTUny3&V^#vH~2OcN5oJPf%nIk||j~m7$!fJ#q`0h=QT( zxypnc1tx7`TQBVVdT~uhxmV2ZP?wP-3KN;k;%+xKlSv9$V)|xKT2t{lf8~v{wKek5 zT!sKAy^T@JlPs5|w%JxUUo$Psp9S8l)i~+IV7EBgMo9k{RJPJ`Yha~c7f% z`_S`7_XAxd)pVO5+`^=F0ed7z@7QKrEF;EnmXZFzSDEJ4(zLa=^e!i8Bo9;GPG9`p zeybN?A~hXT?P=ck#Cb!n&kjcaeKV^WXLbt=?jF>f*(vsFX>IYXq9ABsz{msE#VpY*afS?&s`i#3m)wY3F! z7s>Pv9(bE0)RJ`d!G%Q?T^r|q2U2BDhzL_*F%#%iv7MNh8R?AseD%(P50E?1$A`2e zXH(uXjyP%&Hb7eDQ)kvTHzsHlS&G`PY2=b<#JphXfmL)r_Z9F0i+Wr^2!Z#fn@ouY z%@}9B3mgN^eg*zK+gZf(VJypmfv=FdZPJ4eylR7*Ck8EViYmUm9YRSiW}CqbU3#}o ztJZXsJ(*gKeM}dd1hyDR*qfW7*(ZiSiv4VNjFy?w_5>J-7l6^Nnf?dBWc$@y_5DPDTd=lp^}y6L`DOXr zpqfn|@Y%}JC>-JLA40b@JyfL3MglIY966idXqyVoGEptCE$z4#xf$sWWY37MKp&6W zBaqbI+20?!k}IHjeq7{Jig6fSQgDsoik$PWcR_^u3hbCp>buBN{IvamDU74X9BZ~d z({!)nl%BSAg>Bmy^ljhxx%V^Ukt)OSgN`mAQ{OvFV{)rmzFoo)W5>QY>1%Ew0L}Jd zq>`zi{Gz;dtP|Y2a)tgP?en2=aN3m3XGxp)`y$Xmyo8jgLm9t=bOT{fe3bZX%s_KW z7M1v<#?8Qn{#s;xuT7O?Z>n=Es#h1k4iwpuVf&pXONtnx?>yTu@4j1T9;}kPf8i~= zA2cVf!_U9821S1BJK4JaiZ5*Q8y^`}3UBl)m&Bw%+8G26k4S+fYf(1Cl4~fRwq}=f z;S z9Pu>`^N)t`Z?j|ce&u<&DA>T@P#pO3E|Z^B=UAI#ryb*r$M2keR@?l#Hi78qH1Q;k zi$m?!D^tnEz`l(XRbB>ZI2JeBRzg0@V!7Wusf5U2H7Xdhsjx`%E{B^UE5xocqlbkcy8I%5mg`E&&6B!d7lZ}K)taR;V-7#+GEnXIB06Twm0|=VFud4o65?A|0Y<~`qr1)KTP3%e z%Gb8Ku$a%eBSYfaIYPwK&?RQ!KV=whjgleNSYd`Uu?te6KeOyh$h3bdIZ^{9o`z@h zJy%!miQ1&H%u|21(^?&Or05r4d-0#S=hp3i!IlO7D9728?t)(YrT+75uTMCdA{!RP zViD^cUo?6gc`NG-tbc4SP)H1|6 zkrU3pm-G9qz_)3aw{SxSVKk2UQXw)F%j@OrDn0KTJH%uQ2M3%vCg7u}ryNO@fZf82 zaer8RuuEFN*r8H{(G=?C#>gQS(Sqq~ki&>`Dl|diHtKJ8B#G`1av;E?;=uvYmmYw(acc^8L+Ytn zpAAgN%M4WNLrGufnwu)128P7X{CU|8WIKIT3jU5xf%dPQLL4fQ_ zU*(i->0|khKpS{F-rabZVuN;LN z!U+>;c0QzyMB{YxE~2R%d3RTBGedMUTele$0o#ae#BlOh$>b>Xm6K*@A~B@^EGy?+lMxGfSF*3)jw3J{{#rH2lAg1@rDN)9oZ$8D=1tFx?D=hQ$=f&o2P5Wfj znDpNbwKf>J1f%9qHegp^URgP&9~uA;G~wyI#gYFDH1+s5(3JdNAgHT_gSDmCe_-?|2F=^?ewK>V5pK^p~7s zEMMa9u*AQ#Zo;U&`TUaTei#d$stV29w{kz8e*-@Y4H@Rt`0-4IlG6H#`{%|w&6Ro| zKd-Z?1_Mb4KpNQlxz-XYJFxr4w2$_EEqSjs(=4S~X z7}lQieUTg|yz$U19X{_F2=a@v{Ty?48lRAh->WziyEgxNouaTynBa<7s8?7W*Htc) zCU~r@$`<+dQU%)|IgaNq;FfT$&N+zWP{#4#Db|-Q#QO+k)BMThPuvSS~aVzylQjn>9jA%dv*%#7-yQl=nG6YK|5kaOJahUpe z)CkVfJXA}6r7$WPgG^OSB^!9MUVmh??tFItQnz3BhvvrDYhoNrTdiIdp>pA{HjAf} za^X{tj?fj_qsJdun?_IgV^8=`fN7cSyo?>e_cRHq?R;rVKCaI)X5%rl?ksx^6|n2p z{yfw|?eIrSf2<)a#aJb|`aX}$XgARBz7)&WAHWv`R<534 z)6(G_9)W4;2^Voa0Co`DhRpQo-2lfT&dw`ArWcN0=?fJKMR(giGnGv(jelM8h)t=V z``GZKm0;2F$nl6z;*rdwTmpEAZ8z!J=N93^yWT{7oomace}(a{9Q>6rHwEHl{LA+9 zM9tBwckRbS#}_(816vo+yNdpV?_#(V=llay+1oEjK{&Fh0hN-p$69=bT6lc;0-q~W zv}?#(jLnh*f1vv9XYT-5{Xg)(GCEAIt`8A!m=Y3dP z5Aa*M<{Qm;z`f-A{$u{%81lk$KS2g&&cd~hFt0%5f{hBp!FSxpqtZ04E!W*+E}4?F za=5%xsX|B>Vb~@9dG89T9zXGGvI}l$0jM0#^T;~qixa7YqZO}LYi^1&mc)zdM&F=# z;+Xy>mtoXFytHqF7)WzTMjLnNwEKz?MWsb?)hH9GP9$+8WnL7Sf6ltR~!>bHObnPvB9|ibH zFHC}1#6{yh@z!|!`1}#wdw-t=g1oVb!+ZPH#@SnU z<_c5*Ki2Y;PXtvrUJS1rCpx7u4>9;B)ou$>xvrs_9;U*)tTkaJK3=PjEb=0m*^0Zp z(BwOV2rJ=Y^Q5}S+qZ{FOQ#};m%h%->Z5OU5AoyJOZBhZSa}-XKN+`j!!KZ8kJ?QT zH8`Dmsy-#!^=FuGOyKLhO0Y=qK_TC3bs{^7V;V@I9ifqPd?OM0q&!`yQlQJ%r*6tS z&#HwCguU-`y;I&{&yX;v?a0X(`oQ?hevVkyl-=0VtEqwLPyaK`}1Vi)@7G__jY^0$x*lbcD{GEgmyVUj_V#zUMq&=vl}K|?5Wte zCsq>duCd3a_@T=R!q=(bcF+4)>JFYS7O9-n)8rh+1pNrom{6IvjLC#*s01b{M19}z zLiK}Xsier}Wn#X0Nnwe5Vhe}fdEa}TIKdUQ%eBUp@UiBP+2(viPMVT)`;X^t65`4f z+=s0@l({rk)&(xVSPj^4F&`LgkZ7rk$|8(j8 zNha*mA*Lc~;emOS+^Q}{oZ1ADuk4VP$PKvdOQ)dF%%-aJU>BUpA)IK|l;VQQp@3}L zB1>UiOV&atN`uWKSGNF$l~Mc@cl(E2$atvBFI-p(d|>CiALC0Rv@_qYvEs}IL_Cp@ z8LIpF>jtSWDB^x|r)^r(axwKPtJ<$DDf8v@|9z&FcX<{c_~YEH3?8Qf+SG|HCOy`=+WQnjW1jxqUy#K5DE(~;G{K-x zi5uIf{Ics{8593Y?#w#Y$&a@VcNAyh>O_T##IR55pU9jh^}OP--t1XZH#ictG~*bJ zt&$JFr;aa72_miGw_=2n?kMXa0|z#+|USlStb1e)Jo@1A5b{5 z$HEl&pPIBBk-(RSR7eK0Tr$BbTpUvyz4krWT53sAu^a0<-oliGyPd_2g=1sp+!2~Ad6;r3`5X5PdjEb4->42YJ_<^&j zNts^U`zzkKwsCz{Nc#?li4rO&6WHEd*R=ok0pqAP01 zPxHh~@Q+&_^88+KoUw1FzJI^lqCz_vR)=sP??raN|44dffi@Wy8$&{SLEZeP2~Ji% zCFe@`>@v6~!N(%hA??|AGP%+$$8PZYWBxa0zfe# z5nX?_z0Pj$cRt7IwVYRn&2=)#a_w`=zFV##jO#$4!Q_ndTVH8?_%$Jv&6T6wvm%E6 zM7VyxPT__A_~zlV3x4&k5{ZUACiwQiJmsJ8x`k*l^z2Pa|Gu_ppVcR>;o?CMiW;I? z;*_`SST$#;6`yrYrc?mQ9hR+Av|D=UEaDmY)lgUAYav=MTM}!pP*$N^NjyF*uU)px z0LW+2iU9)iCXl+4UtF$RuNUtol%^C2oU(Yp>cvy@-^3 zTC_35xvJ=aiPX`>nd}#;#JmXNC10PQ zwiujG$Fz>m=GKpfaijwzL^Dueb%9B;NPtDU@8j6Wr?TT5Ik|k!Wy6XX`^*gI_wFkH zvi#6~Cs|%x`0V?u6y3!j%0!9%yWFEyjY;+4?y+vY?&5=e$>4?uSQgl~@w@jg@B3x8yiWLN2t`kB7G}u4{bMger5UkTgn{Rw}Kx!x z?itcq38<0!TMrdc>v*21d$B|3Eku z#R7__JM3ilP*yB;j&GmsROYV5l-d09x8vemn4g_sIUeo5TJLVU=+~B8y=tnsyTgUg z(K@^^K}}S)RmkTv;9l;A@(FH%H_N7%GTvw`hlbu&IyB_9+>PvavdK|Pkfqf-2ZzJM z^y}x*-Pk`{b@SV^%iDF{_D3&$hn7yR;|=nOjMWTx9D%vU^Mf6dr~fPsln13JIJ#6y?utdOlUg}kU z*QvQwl%MWh)p*ifd|{c{%pP5Fno<2VgIlil^_an7$MI|i{QLMJnZ`OrNqmf&Cs6qN z2G?=A-264>l_i3Q+lIrWca$OYNqrX%e)(Do*)i98o)LSWTocYceu!J(zILZ_Dr?yG zw(x54i=l3a3aD2h3mVm$TNhJ(aY8W9h!aQ}*-gU_wt%7}JS#afBRdq|>-5O0Iy+^6$IAgs>j zVCBpW6RNGGvf>6`9JR*V8y`E;KV3D`nf^SrN{`gOQ|g9Qb2^p3gF|6@>n}V1xgiR!eE080E@(w;m&VqC?VDw~@yXA+ zzcp_EYA-|vNNWbkUpWw-GAzutf4j8tOdm$}JC^e(8=TJUS32r)##Hk!n0QA;<(93b ze~r#=`4%6QK{?GWxU-Uas&1IOG5LUnB}S=oEGw^s`@`nsnFJW%fpgNx9N>bs6E*{D z9%12ihlOt&7|{i#k{|t<<#X_oO0N4G->Gl%%>WN%_|kNYl52K`m!vW@Uk8T31dhNwWyk?pZ)4y&!nTduJ(oo+z&kjUt+9wUAZvxdyBUFwhTt<1jFe;-Xy zfml_FF?^sQ>XqgCHh{7U6qKOc8oPShuWW9DSyxV+D-4kn+Ft&}e&F}?#7pyzj7@ZT zvJF_}NK?ltx0h`1c5#7Q9SaAyKTqCdPjJs){kzVJ(jN$RJdn}5FEXsVA08D{uNga9 zcJLhnY%d+Nf5T;Y@Bkj2-i6HHk%6y8o+JM;74|+nFM~%*6}ewVA4a{mGO9=%&-;wp zwz(P}YWzl2j;W^Zju7jewCVzPNMZZ8D2UpJ-0Fs=HP<9 z7T?|U_oxk+Lkf8zVkf;CFL#ycscFEQclL`X5aPtCgdy~8@7@l&4~6OJ+9(D5e|nrT zjzA4i_15(CmvcWMzl@IYhZWoN+DbC}?hCg$eim?GU%sgg@fKGb|3r$_PR3)HuCh~! zeJf9xHs9%K_QIZ>Qz6ZD;Lcq&%y|RZU^u+1Ehy;~3b@GUh_#Q}eAgOqA?` zSb#+mK3l$5kGoA<0%@^4@dh=uF5Fm^*8*v87n|pJ&V;$lw{Bn+9WCJH_BS2nl76!W zT;`6?Pya3FvFSe$x&H>=d03cyb9DIc@Ll|WD3ktQAnw1E!617RGb@MxK<{FXXbBss z5D-3!At2EHzdQXGc$eXY=1HPGbwlMXiBBXISK&%8E-v-A1~0W%{HLp=%A0ILEd%uu zI>cg0f;Zm_7-I48-qN*66MZb=zAykHqkqBrC9es1enoICad98#JdA0ZEMMvD3NjXI zz#gSPv7X``idfQ2F z7ok5BGNQ#$3R?!hR0idu;Zg(o1G)$IxvvMXGo)XZIzp#GnoRrHX6elFVlk@-GQr(^ zGQS)cWXADgg>MGPGC!;4Y-F8RT2QDuPRfsyt;c0?y9Vo68N4w-NJUG{Jtz2wCP@1Y z(8bQnfluAV8ICg{JI+!|8jsqF(#%n*h;*E9{tyYsF(%V|k7)LWI^SZPgYe#L*iU&W zupxX!qe^Z3=Ex%*HTWS=HrQ06MuLZzMqw$a0m+e61TO_1Xqg+WH!g4RjwqriY`<(A zC=|X3K+2G6Uy)>Od}h@O#E@zT1NPjJ>>H1Zcq2AqxN7ffAIho}lk!rtA~+)?m zvLTwTy+o`DE-J5OZ8}=P*9zXxL@4@#E;e44ii*UR!7?52ZGxAvql)9m)RiL7U zaZS{z7wC>qDAhoH+mAY~ItZsf&7-^Gdz%4OhGi?|-T&mJ^}o!vMOjwtpP5f%;iX2T zM##4nqHF%T{(5!2HcT|TQUoo>WmRQ;7XKsRXe*+#z12Ao6w`AfU}rzv+#vDN(LCvt zZ)byhm{Tn3_M?I)H0Z$wgV30zM0E{w2GxCVCPU*%0{^x9R8ieHWtP0Uz;V~I_c+ab zd6?1rHH>kQ@i*fH7&`3YBAf^#xu-Me4~oYq`RetH^12cY)BNLr=HMpGLmnZhyNPm8 zYsAGK46VB@lZ%A+&$IfIIHN>WR*EbB9mBU`U%3NNU%af$MZSb;A%Dro5tL)p3}nBG ze@MDP6&Na7D^}Hq3Qt;czk;Yjn(z<3Fc1^|HIUZ^ofMB`VgH;%3y3moK1Asg?0>*Q_L}r5%rWcnig;eX*79ge+_iiFiEyW3!P-trz}9HkkL8eof>= zN#@d1W4VTt>&L`yUy&zG07=!^(U;o;ntKUJJDEe81mv5kcAQE_b?t{@6~AZqSVM6P!lX^1Ylx3uDp z?wAA{#?{YfGy~9;T5*`?7csPQuCteMsr^PAk+y^{V~S7((JXi}7uEs&Q-~nws`}$u zM=S>7z12yGKj~yE-9GsUro`4+wVo~Fh|*VLE-N60P*_r*-j~Y@fj!7XD_v3gF3QKm znO3TztlbaVay8$QLq(9Hmm~+sC~(n)tq{% ztbL&&qlKtV>^7bIrlj&h<2uOZHOznkM zlKv!KXw<|Gmq{p20KRdh7j+~j^v6FtAZcz7QZYT>;8xVDU-A13Qa|@iwQJBgMo#QT zaq~~LlJg-}dl5#C@HwhN5dw5qKlQCazv@-DX-DU!DD9Vu$+GA*8EZ z!kV}nD|jOtZQ>b4)!q^b15-lCLTMg3naS)>w&H)D!5)&|6)WF|u^8bF$7>REPa!Sh zXyGqbmmsx8r&O1`u@qWw+jFb-BI}>y#g#ZxACb}k{oFO?cAtCXi{w1TQA_iX;%n;? z^byMfdO9AX7t^Y%mHc93-zX2|(+*W7kQb*`^8{&8!}c!4!#`!m_0{ClA945M6oFhN-+cA2k#@O_?9e0* zxm^5YK1&}`BJ+|Z+oPFKeQ%PeJY=6#&yq-{MJ7TVPP8LMgd%n#(aG#vAqD1a`;2Br zNe(~fP+|$bNqhCyC%qcf6rN#U4kSx`7t%HN!}L|Czotpo`S_w zBu~DTHU^0Xg_YoR*euA?9l4!=Jmv@Q8<3`1;Jhv z;9Pnc6M(^XoGEz}bWtx=5j(!41iJczH-VxAHMnq+X#Jri(;LkZrdbqc^WGDq-Ix+_ z=F8Lp^*X}LTTfIKG>5V`$Q|JxkNx6&Geo{YZ3@>~_VSu%GG`=bL$Sk0#$CVG zr$>)oP|5ua;zjz&0nUPRlL>ZXX^9{T$O?%ePO3LCxF>29`TiOOkQ3Zy?8FtNgB!aQ zVr8v{@b$)+>;>_^)YeD#r?S`#HPB(tB|fHg8ZKU_$oB;1$_Qi4*)Q!`9A$W%a3GGPH+n^!`GAssxEcix!dtaDg=@!jy@pipaC0A(;bBl9yLUM&V#EB*Lk{9P&zBAYi}%i1^4E z*!q?i*ONO><_eJ4SBrP&r}qhneBh^^KFk^4B#?DdSCIj$SHVQ(a0H;0>D{RN6pB;kR(yEMj?1oj7`DDSdh9@PoO$tu=O@`Pobs zQ&*WGVx@A;+9KAnLE8_mC6&%{ZOV0(EADTUGr+U3*u|pySU@}|cCp00 z6NYEav&k&gK$CL(0lm{mq58@hiK(T;b6jX18Z;tDCPvbM>lT$}Zcj<)7+j109^Jnz zoHW3&N1wo?O2cwCqR(bfEQL1(FFnLgD(&wdh6=WRm9ooi*vWWuzg!WB((kBHH4wCga%p zZZ2b=W0b{$w)zR2!E|uU_5wYAJyQOgnpcu3^!QX=;#ao0AHKr$#|VmDgVo1<33=Ag z6>*5Ts!X|2_;H_(a9l&kVNvr&>5JxibP3^EVUazT+ZSV$OA4Xl(OCEFcGtr)N#xTX z)B0{fSLDZqes^1Z)Hxxp2mxin-<`zTD!R2i>TvfFLf?cIKV7hI06i;LB-Z`NaNklh z)z{|x5q!@0wEvd6fRF(Gkvh~@eLL3`>+Yka8bSt9%T{oVlu+>f76XK6XzNNaBd5mH zgW~RkABu14sJIX67gRs=10p{}R-{YNFJD<+5JpDa+p;Hui4Ft+(?Ul)-?#Xw*oP88 zNtjSD8Mr9yu>7HbG zPb>Ybz$^Cli3wj1j+FC)cE|qrImC!E`hqc!-lh^mcMori1w%Jv8t=MBnhJwg-YN4m z5Un`qu;Yu$cM7&UVJ!+oUU3Whu>cF1bZ#Y`OYGTiG?NXlmK*9n%?)}2Esr^!v_UY7)N<@dLFuBH}0l|ahg4b{wOtJQ#3|smW~tendVAv!-8R& z-m;&eL?(UrfA{*HFbQtK&5Cme6vkb5l+P7r!-&R+9XR*8aV~)s2zzvk*bf=c-v`{9 z(XY_X-2M@#V;%_Yy2w0BeQ?TCBl5yPWRO1gBJU0f$${T+W2{NVJgWss{aC>P(up0LHpRf-| zp5D0{&VGwHA$aNLSM4i0Kg2mRroa?YeL^|*(_0SO<;q&>^ys_fw0xE>6W)9x-F)PT zd;W2Sw%uWIQnCHjmzKb1rGL$Duzn6LG}Zg&Rt>!*mZ!8YW=1nQ56^S852-;rx@@E>hj+)fK!u58kLBn6a1Ajyd1#^@pfbM%^x z)TctXyfIh5hLmcMjo6cJh~>;`)(111DzkQded94m4yS|E^Xyypa@jU_lm2Vfz9?i> zC+ znX_HrvQoI23zrP;-?rIim6PN3=q6kS*e*ct00N(T{?RlCu+2>4$@M=OaZqtSRiRAt z;i69sHO8w_=m0TIxTPr}u8|TbfB2 zyzSL9(DMXDTM7sp?X2e*rr%+qTZX#F?MAVi8nt)92`en5xy6vVp{>$Qc0RP+$z-nK zA#8bZbLwU`aj%EdHOkH>+tzu>kiJyPA#}Q^!AxzT5@sxlZ|&o|>3+oeJGOq{v(x#i zrIX9eNJMhY=}M({SZbKI7%%gnkbJhwjVNKWtE1iC%mGhYnJPrvQEosR-t$xNwOz8UfPFA=eK)Hqa z^!>f{1omvSY-xW!f8^3M&&Ja};QTzQ`;S#TBLz4KbuX)X=k!qXpdZ3L=;uC|L%4+Y|FA !jLq6q%T-=W%M~8P-P7_T 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

R&RLd2^) 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�pQBJr|`$RMq7ZEyp94<4Kp} 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�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 s@E=_ZNF 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�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

    ;;6Lnk~L4dJ+JTV?_u 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�_N%%~|V`nsYZuHlJKJ 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

    t!n*KXSy@2aCx-Tu7KW=^8GTnb8~ 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 Unix) +// +// Revision 1.1.1.1 2001/05/06 17:37:05 igor_rock +// This is the PG Bund Edition V1.25 with all stuff laying around here... +// +//----------------------------------------------------------------------------- + +// These settings control time flow and events at initial round start +// and general round start at section [start], game start (time gap +// between beeing respawned and the game starts really) at section +// [spawn], and round end (time between recognizing game end and +// starting a new round) at [end]. +// +// There are currently three supported commands, all in the format +// +// at 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 literal 0 HcmV?d00001 diff --git a/action/models/ctf/banner/small.md2 b/action/models/ctf/banner/small.md2 new file mode 100644 index 0000000000000000000000000000000000000000..1ffe1de695a2142f1aa98e09cc605e2e1a8b313d GIT binary patch literal 13492 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 literal 0 HcmV?d00001 diff --git a/action/models/flags/flag1.md2 b/action/models/flags/flag1.md2 new file mode 100644 index 0000000000000000000000000000000000000000..5bb46e6f0206c06830e6cc70b954ecbd270e51ee GIT binary patch literal 29136 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 literal 0 HcmV?d00001 diff --git a/action/models/items/breather/tris.md2 b/action/models/items/breather/tris.md2 new file mode 100644 index 0000000000000000000000000000000000000000..cca12b0fed03876131f100c20e6206a590d2997f GIT binary patch literal 4372 zcmZ8j3s98T6+Zt8yDYHFF0ee7$MSx|@_z2#8xX;O4+NsoR*g+)#A@q^t)>DAgN}We zmvyXCv6wNFW~^yzjBg$7WKye1tg*2sqGPNWr!i)Xoz`dQcdpsa9a-)@-*^9e&+DFh z|NrsEw$czHx?&JL4w(Xpe-da!Nb0~x+Y=qV$O z78>a^)$mZ6;ih=QMP?%sPCIEGMba8_Vtowlrv&8Q%BO!*9t}_dJqNjoifK3b=`=Oa7&XyXXjiC_uG1vC3Ve#{@!Utd(0)xN6ls*u zH&jUfMO%&5i?$i#??6YCQA}RLPsM1husOwOp!r54{BNRXjY)JCzMdlooj^NCHpCi6 zuOZe!=-5gjl!Uy*8zD3fo@0bjG;CEFHss7fGZ1eF&vEk2cFu%BJqU(CV+F#JOVNbXaasg;M_-Hy#q2DC=%Fk|mg5%^iaxsOpAEEG#|h{}S9qE&RUN<#?6<4Fj9R`3u+6t6T`3AF{r zxrl)b2ag2`MP0d7^kLxPh$sPiV=*HU$fjn-LE6FXcoI%Y3WXyFcKjm1xCbbbGJsr4g9kDOJOxjpD3h|_FKb9q5oIci(UgrRu}VWKWIQ-qbWsjaywZ>c z=>}(uF_a7BhKvPwBZgdMF&5`57mRly12PfZ4HSnnmMFz}XWw&SW7l??NVI z5jcAiPhOxR$OP~rFt73?0cV#nYtDfz0cTGVsT8P0Y0ib;X;h{>NuqL~G}uc9PlK=J z%9CWO07}QG2Rt1fR={39SS3}#FAuU)0GSD12$VwAK$(!K;F<8L8eSEG)c|EHi$zpR zb+|LUc#?*@!3V|`y^sQ&Jx?bIB$UNs$j895@FW9wjs(jqmV<0nK$e160_9ROP$^^{ zcqvT=uL8=$o1+;l4>_rVtOBn_pHD5+igo$0Sq(V_oHI~BQ-P*H7J^TqY2dX$g)|*# z8YE|D8geon*{B6;1M)#~c6`W5o647$W&rt>=R7LGOYx+b+JPiw7Pv%q+969YV{L%7GgZZwF8X@Fc|80aOXxgt$6@s(_mjRR>Tta4Y=p1gZgUfzO>lwZK#1 zXD3h{@O1dr`G0?B0Q=yDAD*=XoBd7x`Qgj`mc*}oce0FQAAeqZY~-^=$M~(OuEKA- zS?`P)eTT=aeSPD>&08*$ulEu)&%N;ctjVJTeRA~Y+b55%-8=u{nq&Q+h3(z?Puupr zWA@7P!*TuBN6hYBLk`!fjjwnnuJ5f+czLit;>AS=lV1A8ndVn($9gyTKHa^!_~a*B z3iGdrPVFDJHCqpvhpFEfvip)ZRXP%W+fQ+G-i?fZ`iq97#_<7a`1+`&{_kH{TZ^yS z=k?!?oa;PjZLq!&RPU;HKUN##n%QrS>Npk`({ZIKuKho&O>NhYgbtm$YJYO(?lbfH z_FkOZ{F}cu_*Q*b-}=-C^)s7KwDyS;&D)z#OxrW>{o$j1r#9~yK0UO3_nDFY-dmla zh`%hSLhe~STNuF0PiM;Xrf@&M(>8>b$+x>&`4ffq{i#6yewJ78+wG;UV!7hRsNi>q z`Smn;V|2gZ_sXV~9_h;Kl!?HfraENHud-wk@SDX>d2(Z+%msdBi&-9j*(3S$#EPct z)os82=M1^6Ay@=q&fPglvZDJVKfi6CzY;CKe{!S1=Ma7Hk5GB(=zzc%2+cMJ%cga4 zG8uTV-H>ZSUD5-5;F?L<%Ll$QK0f}XBTi;d9rByOb-lXnX}4Tb_V@bn=|%H*SmfcK z{#Zl->-O}u97oHVzluQK>Vn=9Y=gh|zB&G;2yG zL9Y|N=J>p%QI-25aQ(dLF>^gR_c>+Yj4oW8A_Mz%%ATq)uRk)=t7_8= z%=LGm=UGt)TrgOw?nNK4ttC_4cYfgeua>DfrNDgu>3Xv+dS#$KbiL>PRU3m+7oWYrd3R+A8OXK1p7Wy5)9X%O!{?mu2#%9u(D&}Zwae;mEy1|Hcc!Yd zYJM5U^}QQ?@;&i~d45rjaeeRjEd8!6Ra9VH-@DhF(v4e0ofJ9J=qizi^TM2XW=lZ2$lO literal 0 HcmV?d00001 diff --git a/action/pics/ctfsb1.pcx b/action/pics/ctfsb1.pcx new file mode 100644 index 0000000000000000000000000000000000000000..802e88b4771f510a4b7ba822b20a81526bf2bb2a GIT binary patch literal 2375 zcmb7EEo|dR6rM`G_3=9kc3=9km3<3fI0s;a8406(ff`Ed8 zfPlb&fCCE(3RaGjrfqurbKTnodrtS7O1Ij8T^Zd!bR{L^MKm~dF~qmfG&XIx}dn?Ij*FxC_qsj zbpn!3c?P+*@sy_Wv4@3VBemVcVX zb138-o)G^zZ?=`#-1KkgPr#k&7Q-at_D#V)*nP$Ekonx3%`*EZe)}BL1s& zIKe5j+FcbU8B{->gSPp7l85d<0#UvsiGSx;0P@K@`Oep0ik9O&TQq-uo9-2+(7VwD zMY#dGKll@hl4zF zJ&K}Er_<~8hQr~>$;oUsd;9k7a=EO!YUn7wt$3DPHf5UeMFWU3NPmK1^^1<>EHNW~;RC5}!^l_v<< z!S^Sj_cUtEdPi?gj%RPKpG>d%$H~FqxY-?q2fcEuLu+l~Hzc=)olvxW!E^;`3z~rx z9T5d#2?-*Y-wA@po)?wNO~FR!N4VQH2s-YKmB literal 0 HcmV?d00001 diff --git a/action/pics/ctfsb2.pcx b/action/pics/ctfsb2.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fea2f475245829898f2e2fe3e6e3237e5fa33116 GIT binary patch literal 2353 zcmb7EA#CGT7=D$yxT&vwz`(%3Cm9&{BwGqFFfcF(FbFU(FbD_;h&nrd&GV{&Ogc~y zP*6}15ExJ}aMF5H!RhRzX`60$TW>8mX#<-yP;hi7ZSwx-q-l1#PMQ@t&rk3F{_p$$ z@BN!!tZg88dPqjk)}F1g_5XU-zDEB)=pn;6&r2Um<2=pNQcnM*St-q5=dk>nmR`?S zG%aVwFU9*E+bm`DrXAnV^0{#ayHg{h<(WatV`EZ$j@c6SGI*`S;r30k_cMiaXS952 zr1yv6*o9G?g)J6PoJ}rhOp*=xc;9wN@^E>AkkkOkx!gv?n% z#26xq_#!lewDV*W+KD#f?f7XJo!zHg#7|gmCaK7@ioka60mhVUl7tu}rY}P>28?Sa zYe8~>0f^5R z;WOF+xMc6A6&|Y?ke`t$sxe!}r?lB*>NN}?^C8MwW3lF;3_9d|%Rfp9P5@t(|#=|T!kHaffQ zEgU6yJN+^`NumT`-Y`opm=`_yjF`-z(k&HTJ_S7z)U&O3jxP%@?8 z#$=ns3A^r)o=p)JdC7*;q8nrQ0upR|O^o}%3-ER^mI@QkEpQHff}n!%1S-Cm6%Rp| zbUw;*`g@T-zDf3qby`a4SGRjtG&kM?NV>ilx$s}C{wlVXGkHDDVNS1qTp#C8V02@Q zWm(pB-L`Glb%P*iwOZY7x8Ls%27}|{ITAml60n855Cj zikvSX7b6EF3!@5;G#)8Dk~k!As9Sf5@3cI-S+nX@(|0PKt-Ge?R8(76OjXhqQI!N) z#2^9qhn843MNbzUO*9oI5Y)l`i=g(Z8_ouYx2Gqw+h1QyuZJhu{^*1pje`9_HRw+r&=5+a71SoNjY!1<4u`WrPL9@lgD}??12Ay45OioR(!ZDwROjeNA%}#g^rY zBx!;mV=VAI&v9`6XxKeCBK!ST?|CEX)(P=jty-f|4FacDvmB>lnwqY&ib6Rt=7+9* z=$L&=@0n_%D=kfGD55V5H3>T+ZwkDIIhp4~4lyNQnHM)JDJa?OkQx7e_S#bl^u1TgN=oYW&oTMN@ zL_9_)N6H`AzLvaGwLa482bFraQuO>f&qsOQ%X2T!)%;G*Z^ir-&X2kL5Pj&O-&*L} zN7E`gvCy%G1~NJjQ4b@+p*liUgmi=?ggzi-nt9Ho_-5 literal 0 HcmV?d00001 diff --git a/action/pics/i_ctf1.pcx b/action/pics/i_ctf1.pcx new file mode 100644 index 0000000000000000000000000000000000000000..91082ae715e7a6c3c1c401569b8bca0740685642 GIT binary patch literal 1293 zcmbVKA&=Wg6n+a%KAkHK7%-Iv7z{8l7+~N?1_KNX3=9km3=9GS3<3-S3<4Ge7z8W` z2nY-aI8YD}Pyo8HW)c>^G zUA3=1SG(KC>Pxk&9^W5vT;upyJ$&~+RqJa1?hLtJSD&7JTu-L;8vpp$$HuRi@cJoWej>G5uB#ocz6ZTIzF$|HYJc;DyggQ% zw}|2Vbmo+LfvMk|WBYgO#yNm&s?8k#bFlh&5{2={3dNk)!HPTF`{OhtgLUJX0WI*y8LD!63>W~5xU%WxRT7Zm~B!i zsAW)-6Tt|lIwLwI8qu{hZ6`@9iPAVsq96+Wz*nAfUDt7Jd~cRznx+tfbIur}l-6hV zVGxW9w`|FFWFAK5vJ^jejH@Q^ge-B1GIhbUv1^T_7+AbCY1be*(_2i7DFg&O0xkht z2TKQ`1J{6RP^3W~ds(Dfp_}?n?Aei$fn#~LsibgBE(J3=H5fq&$iMBf)M1fip2Zw- z5{T*$f0cx4c8&C7WIOz@jPAOYm7PV>6B3`1aNOJ;BD$TUP zuz(prC}=u1zl`I{Ff4pO_q@z?o3@=u8CzB$gfa}9bBj`g5UT6Crs4U)sOS&!UYT_- zo1G%f^Ek`GX46j+FAQDJvmM8h^3+jmCn5SMP*-J!Zg zG(*>z1}8QEo#I0~pSQAUvpG(ZVHEd+u#4=JYB^3UWnh|07?LwV2}4T5Nzos*dxK_C z#`$H?>Ue3}jazn*m@4FsN2RL^sc`|6fldHw5PuT2;?c}Z$1?5PX<`3;=c~FN*LAn9 zqq;WhgHhkH`ZcL1T73myyRgq-6~n@ZsS6Veh6eN*bP41dqyRpE1mFN)0UW2UPf}{j g!gqy_iW~;rc^-{oyHNbFS}Z2RaoOo*@hFM@0?njt0Rs*kI8d-3A?M6|g1Z+*{@ID&`@g^a*|;)62h-1& zt1nj?2_d#^ODUC7aUAD)Ue|Towq4h)*X!MGcXxMpI-TC$-Y%C5T>S>arvDo+BNL5Y zj8CH<#>o0%T~;b7oD ztnoq9gE5+%EInM0o{VQ}Vm@j*8j~k$a(rJylcQxmy`%Zv!XKnFP7c=OV?uOoWazr@ zdmoL_z4duWYvCc+qcNVCI-xa89t=Fz^39>#i*ar07kzrU9&2y@lc!}Y^3YC|odi7c zxsoh!ndeX`sB2S)6Tt|lm=R0~A{Z6LG|#I%EwUs}<1~rlNQEj00^j%aF}kkfI6?@{ zIb)1cx}2qL88&%PCGzJg-nKzk$!Y3bHqNONzt8O3lFt&B2ShnoFtiQajT9@FcMhG~ zq+z&XD5DS)2nhrPJPa-dfq^5S2-+fUvan87l>|lPXQ7uW8T)SNIZ6uO;ZiV%Q=1Xp zg4RC`SmCo&ve0F|7#h^w>GiL9@~xeo*0-;B`_t=RUytX_{;=5Yo7Fa7tfN^ct2s-j zG_FXPW0fL5Mp7Zi1KR~=1EGMhHowcVnlx39XVUh%4==r`Y<>08clMuX#)oti^ft>r!Op4m& zrC^y&6HY>geTrRzY>XI!K@GsH{W)z8RlP6EZJw{wY#AqWtxl=R_cJMD$5FzToC!*p zmbBir%guDLD%&n=ZsKYd7SkZByf}AM!u^oS01Jt@0LlOpfF8s@M4fqh=M-C6EWM)j z`XSDJzwP^Z-=}@=^cTB-W&JnO?@)gWzvl3F4d)Dw5$prlxv;ij$zV>PK~Mlh01|)$ p`~vX(z8_lYU02^LeW}Q0H9s`zCi7awFX!WNx88QMMV)Q(^k1;6F&qE@ literal 0 HcmV?d00001 diff --git a/action/pics/i_ctf1t.pcx b/action/pics/i_ctf1t.pcx new file mode 100644 index 0000000000000000000000000000000000000000..0dda6b7c8548ff686b084e1578f848b0350c1c90 GIT binary patch literal 1121 zcmdUtp^xJ@6vp3xlUM7L3=F)Ifq{X6fkA*lfI&b&KtMo%K|nx2KtVu3K)`^4fPw)9 z1%(9#3l+z4{ z%j5g;`|;)Y{@Y*2_v717-n|Bzw?Os%@$J`;%YO>A&ZJL7;Y1$u)N*H7tXZU}kyHz+ zIFXESiW$L_Ac9d<&Ae}XQI)wbvLa8j)Fm#CV{4rkQcBJ_V~kQd{Y&3g*|v|z z#>@(J*{IVfKhMQo&F4AuF>wk@hPJWZ7`f7XRCExeW4K|cq>vFv2*d;;3>t&Pz!6Xc zeU)`((iX1C<0`df5*5y5Rwt2iMq0&_VSjr1{ma97v%g<#_uXpi7wdFBnr2|djAji6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/pics/i_ctf2d.pcx b/action/pics/i_ctf2d.pcx new file mode 100755 index 0000000000000000000000000000000000000000..4534e0ecbe0a896b03046b5acc912df002cac24b GIT binary patch literal 1294 zcmbVKA(Nv>6n;}=XrPm6f#|D3Ah-%$$5gdIAP@*d1OkCTAP@)yA_5VCh!`;f5iuen zA~GW4#EKCkwua2@>|Vr*soIDUCswS8*q^|4CbK`_?m>0G`ue@s-~0M$mlp~MqvE^1 zxV{iBA;dII&Up|7Q50oaR#jEgG;Q0iR;%rH`|$9v-|t^vUysKlT>J*7r2lK*6vUKo z(zPOeVMvxfvy=Oojj=p9`+u>4s-Y_+YwD-B!zU(dvLrtqWW>gr{PYIygu7$f<6)qR zO_sWMR2%HrNT7Jf1|r~8ykqid8FwO9-mwmyV>_mb6Lr`f7#gW#iD|!ZSy2_4AtGPx z4kJ}Q!*Zv7QZ(ERN8%r1`BYh?kI3#ol;Nes#z;Yfw+or1493ccs1mvAuzD$}(mN4F z+>cb&9gyI!oTCN(Cx%%^1aochs}H0K${HiX9TR2G`AO0i6 zb#Cd7soMszC^2+Q3DyZB80Gmi%gQXt(>P0_B%VZ*APjup_dHh&oa5NGZCRFK7?e_7 z*B7&-DZ)DQ%b0&%Mw`ZOOFm8PMN`f->^NE*+ZW8dwb3)e+xaHXfx2SFCCMBW; z!GG%0+@lGnp+i0E)S&22;2*O1V>8{a?hX(4`@=sUo?q7YyW7ouz1(ECtI4e8<(wu{ zJt|3<;UGa?gm{2#7fc7J2}B2kh4^in-o$Y;nbcue`F`QL8Rx0vM3xnprfV3EuA7AD z7-NJ){Bqqami290&2NfXlh<`xRdG>FvMh{aKMY;Zb2vXKD&!;tucP2D^je?KJ$uHj z$}tLyrluYnB&68Wu|trF5k+uf12Aj8PU~G+-513s%T`Ich~l|m7X+o}rJP5$9atte zsHGDsD6O{5Vm-ZG7EPPhH&HnY^QoVfZj{+UYHAIJ&-*^hf1_rKVU|?WiU=UysU=R>s5D*Yx5D*X$Fxp0HS_K3w7!WXE z!GM5)0|p!@7%-q-7qEZ{hAX^^^K~|L*<$*Z$*u z|JVCpalQ+0e|Y)$CfNTW?%D~T^1|{wVVPl8(?ro&5-o`0L^3QWW&}};2#zp{s%qtZ zofs3hPpq!X9uX<05S%83R{9IbZ@23gJ&>`H<&BmUU^`CP^K~Uh7gR zCxqoZW=s*nF(wG1C<^`C-LTnq>!}?d>VBxYu58=9t~2k`JWtXzHb!ZEOEkoZG1@u% zn3^fkqmjK9tzxyHB_}zBPE|)~n?t!S?lNJ;d6a?I#;Ep%icEOSB8+okvzW3&R???CTRR^>% zeSXzP3n9cX3@N2jDhPrkNwO>}ilQvb*=)90ELN-4@pycCdODxaaP+Hd<4&jAKKz$Q>ksSu^>KZu@9T$`?4R-a zp}t=q*85G1-D~YaYpa3v{Z_lL_s?M(dPY8XTVQk4QSEooQ5RsVf%cKQ1rN10KiBu^ zdaE+9SV4{`Q6R9`#Y$mkF&eS)$;iG z=ZD>?T5fLV%X~UdZfE|el!GyghBO!uFTpB8PJpCBrVWM#%m6|GVcq;Pj&H)S@crEL zGS}_fb|PhLS%DDBFl^2(N)19N#uy>(Kdp+%G`}sg@lAhJqv^{0 zSW>nf)$Jq%SAn|ooYIwJ#~ewKS-dY;Y|xMsk70*miy#9dhM;8wFe<(b^UWYz_WScB znMLs=2*;Y8QUk|{r3_3{2}5!wC}En?Y*9?A;qA0vlyQC&3`Sl$bmM^?B&G_v<5B5i zArTiq8DIj?fcTrJ6OUGAI+y9hP7C{a=BK8aH_fDo(FeZ>ANCA8R3BUoq0XR<6w52qbrT0oN6**7Gn>?ywyHNam+U*vz Mc{#ex;wp*$0WmeUzW@LL literal 0 HcmV?d00001 diff --git a/action/pics/sbfctf1.pcx b/action/pics/sbfctf1.pcx new file mode 100644 index 0000000000000000000000000000000000000000..238a300d7ed032cc3b6b49e0291dec589bbb9490 GIT binary patch literal 967 zcmb7CA&(n75S{~1uIeWl7#J8B7#O&c!2(xWV6b4pf&~i}7%W(@U_rowfCT{u0u}@u z2nYxa2q-8B2&i)Rq^lHA5OCmh?UNpOAfTuF1N)qNpe^iJvc{utX1;GcZ~wS@1Hn)@ z@2=imUHsp&kpBBgKRSGVK0M#O9R9w2IRx+a<@V`+@BLp-hi|`z4lnO-pAKISugGwJ z4anEm=jT5|K|+WS!WiS6%d*US-?nYv_rox(R;%rHyWj7R$K&JUeRAugqtjN5qQaTX>LgOmNUOM!OmQk02@(STNz5wC3d0i3 zth@*e>O%aZ&p-8(9=ihB3>k-1 z1VRH7Kqw#_hCh_$b)NTW+9gRF$8{8WV@j@Q8^>ZC)p!=; z0~AF74^+mE+ze`~w$&BqUKA3rR3*Zh9>>5AXB z3;wnHWuX&7gb>CU=UkE`-ut?)`@SE?ahj(6et$e3Pp8xC>+9umDHG`MyqdId{8Wyq5qabr8u{sKrGtw$#Je zF7B55&p!RqH>c_0<#;%~{Q3F$ygfXvc87Me^Q$TDMpF$eYiLrD$YYlwn;_$m3P5OJ z0tf|!m+;3tzfaRXj@u}z!>|kjZ%nRrBBc`|;9OHG2%#8bgqHW4ZNJ{MtFa#L%dRil zHm~cnEMxDZG!3IDuvQy$O>~Kq5WG#?LuAL$3|4hU)|!`+<$|W1L=0PsH9-O+hTy6O zpzFUi?Ne1B%5vxZl;!Ir9Ts)YRo3RlBuY6c3}=!Own&=x{d(K1Hf2BN?R`>pQPG5X z6(nA{l-r1!5KDu&1j+ytfEnc9#GQF|RK?B|>!9d^fB*bE&%1dZ<~f^ZHDAR1!sahz z-lO>ez7OzM4d)!5V>pDc*DwiKGZ+Z82nv80zyNT7?*P`$^VQ0%^)gq>P?5`Kcxtn4 U9`uf1&d<;LX*YJOI^X*2A6ohaSO5S3 literal 0 HcmV?d00001 diff --git a/action/players/actionmale/ctf_b.pcx b/action/players/actionmale/ctf_b.pcx new file mode 100755 index 0000000000000000000000000000000000000000..c18106e67c2cf0ecd9d6ef93921c9ae7c8774a7c GIT binary patch literal 39455 zcmbWgJ8WEMw&%+UNZO+V=(I_cKnQ3_hqOpjEC}if6ev*Oz<~m@-c=$22cqP_L8Dn! ztSXk<4mxPu>n1ulXwxD=R@i}q3I-Udtur9_?Y-+kQlcJqyWKuL=gd99#lX0jgk*37 z>;C@hD^ic{J7+L9i>%tU_xJtRTL1O<9((IQ4*rKX2Kete{}22J{vQ}PefspdbLTE! zzI^@q^&2;C%+1Yx^wCF+M&q;3K3iX3|MJT(A3S)lySw}S_uv2U!w=pi)cex{mOfQ}4rd=vXXC7uVootSy znS3lMTR<-ny#z}gQ19kxBh0h`_p~oYSXNI_vCuBMLbup106K~- z1hHTtRI@Su@H?_k?U-B8E6@pgmvQO5(;1w~5jPfauKEu_9xm-v+VmYRwR!F2QN~Ut z66A+;j$+&DyN!fFA)lowN2jzhW_{>`Kj0Q(qjX_yFL1O?XJbMp00{x;@~xxy_ChS3 z$zeU;d56wBs5@Nh<)ynsm`;Q!&E(Bvn5yhJ}14ol4TSi4-C$9h$_n z9&@9yF&7)UT&;8F+(Ov-OT+F1foH}62SlNO#% zzR<=_OESGdYIAaD3BnW$kENVkI!MtRVuMF;cxxtzV!^2M@J2g24%j*A#(53t{7f1h z1^P15^+Lv{bOw%y$3{iS4Y0>cQ9c@xQC%ub29PYu1le2&F?&mu9w+N8G%DvP>_Qlq z&+25^iJ#!uFoKGjM%@VSMg?c|vEg-NGO&l#nF^Qi~<`` z!0h!ZIZT$Da#FE+2Uzl_?owEMgaLts3Df;6xZGN>>qVK-U?eu0%j8UPK=eJpcsOL} z(xMa;L$CB1EJT8sGl+6n2gZ<)5JKQKAweKTRIwpI!{TqG3sKrqWvPozL(DDdLR1(S zMqD8iq&u7jI8&WE8_lF$A~Sj`Hj>WFrW0@d1l7bwu|_6Z=!k2W&TK1$y%;BA&TcH3 z%4C6u8UoP4EKuR_yjSeXzXd!~hW#-Dbm$a#9dCPoU4S&5B`5lMh{TBm{?vvuTpnP~ zTsoObyoL6XKTEh-mrl53#*HR;^Al4u=OFzYFpYLDoyxgbN9G!fjiTY$m1G)!aD}|! znP)DkPcvV*6z1QOE@UbGDByterq1u7M4g3jA$G30>Iq2c(V~>7NAJQpnoeFxX3%D` zJ)2A?XOk&FCP8UD3OOS*FCcmd;=-78ivh6VPK+x&MpRj@JX4U8QiFVU1ZW;0z(TQ- z_8@)U@Nr({8(el*fkZAFSU~ggC^$+kIF(KYv*}!_H=7IrHtSGn8g-4);;77v7h~RB zOu<|p?PA+MLcl05GgbD@Lh?iL5bNd)75D`XTfK}6_cZQ#XvM+~4l8VOTEHoWh4eu< z`-xDw-f9iw@;6v3AlVC3z6RVqG8W{D=}Fgp_X ze8|V@MUqWHU|s++8}ymf*VaNJy*rzBv;0ot96E}slmH|#ic0O8)eKcG%1cMDpu#)l zT41o&fn`SYu*`TKRC}q1+q?ipBFLGNtVeM~;#=&O4R>cVE|uAtgB!578SnMi4Gofa^Q458p3C%!z7(cq@vHfO71uM3@XSvHah=d^+Xw z?MygJ>P)< zNK5@QuE+JTB?slv&l1ZkLX(D7QlL|9=1?A9Ey3)FA zOY9}fN^`TROs*r%MP&A{vS;Z)Jh9*~hTv8b5w(R2RCtIy85&?zMCg`Pmstr!hgl;y zVNAgQP?-c4%xph$w^Mmaq-2WNL9)Gl*4-Y9&F19+4s3Rn$%3}!5_5_JQsshojE+SQ z@=w3|AeiaPF$L-|g$;o9F=DDBNQ|i-V8u9l0#PggA-~Za7=MyT|0JUdU^bc05=25? z7&Uz}mLxVOei9k$gvY>HLYT9h()PiGvfJ$11)OS(D39^bmRqS z4QL%ujs!>U7Ah3hn+?+t&853Q16JB#@r8JTA;%0Da`fzldjgCx+exscC-J6BWDBtx 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ô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 literal 0 HcmV?d00001 diff --git a/action/players/actionmale/ctf_b_i.pcx b/action/players/actionmale/ctf_b_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fd0aa86502fa2d3326b7066fa60d54196e2be817 GIT binary patch literal 1294 zcmbVKuW#c>6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/actionmale/ctf_r.pcx b/action/players/actionmale/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..14ed1d451e119f507782add523ead2c0d8347a70 GIT binary patch literal 39455 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 z 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 xzH8T>J(DQUE?yo8HW)c>^G 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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/actionrally/ctf_r.pcx b/action/players/actionrally/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..7b713fd02de7d6babda805b425c5121f975513c2 GIT binary patch literal 35634 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 IU#sN*f9n2+>i_@% literal 0 HcmV?d00001 diff --git a/action/players/female/ctf_b_i.pcx b/action/players/female/ctf_b_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fd0aa86502fa2d3326b7066fa60d54196e2be817 GIT binary patch literal 1294 zcmbVKuW#c>6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/female/ctf_r.pcx b/action/players/female/ctf_r.pcx new file mode 100644 index 0000000000000000000000000000000000000000..a8a720d7d03211e878d4f3381b6cd096073b73a8 GIT binary patch literal 38382 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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/gijoe/ctf_r.pcx b/action/players/gijoe/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..cb349f1c7cd9ea931dce0494586057ea9da5a574 GIT binary patch literal 72043 zcmbT3JahwvFhE5~^Q?+QI4T>}KqE(0`HBoTQb#q(N%!|(p+{DuXKhC6Q{>)z{_~&z zoO|WL|K)!=|DVtN`@jAFx99)%+{-`tlRx?S&wu{aS6^LSUDb8{r+@mVfA(j8_UC{8 z=WW~m#b5lzFbx0UKm3Q^{`R+j`?r7lv>X+xdQJ#pAqNjq}v_ecaFEaeq7SZ};Q8|L|!) zZT8L6w9kKA%rA3&c$xdUUk$s%b6@4{*c@I4U)96mWmQ*m?~9es`|9xGsqxhK=;eEuWVjHuK)VZ~Pqd0ZhLK8n_UKz@}vEEd7B;kAtQe2huA#Sb3g5Go1v=vs@wZ~o{XO1 zQ}YOl$7yEhF!!sf->pEnn(bd_^VQ+yi*2vzzF4?>?5q0lJd70(j(x>gSPb=JlLikb zEym={H19Wiu6;DbGO!iHSI6xt?{~`+4}oLe-8}5B4f*EsnpLs?-v8b9etTcuUB;YP z3n(bm*hdF+o_f6fh>Gtpge-4M|ahkgV0#8l* zZn~Z0&vDmRKnDUkuz$_PcJHh04gvv3v%ACR9z-)P^hgG*5TZt-W*UO(;WTwxoi)6R z&*S0%VYD&ce}lb=(XUYKu457dgU98)P49hobAP%>5I3y0v_XZRfmBF%M zaTnLVqT=g($Et5cN4(5v#&0iH*UPJiJR!vnD)}fFB&34b9|11rr;mW4$yahzH5<7x zt~R`W>-lgR>h0n4uxY9mA|fHi4~LVdZf>fXSDB0|#~!0T_dA=P=3RSup1t<2-}(O- z-|4^KQUNU4|Av8f6^jKl_h)PG}VW@+ucUWb2#l zMI3(fFbfUw02^$8#?CL2@V`+6lr$YM1MW`%r0GAttNJlcV%u~(&GoJ#kwZ_#+QZ9! zs953K{dwU6kml1IX9A~e^mM+&~`zbqSSu-kuumo+GzQ^ zB#_lm_b~E$D0pA2kTl&q?keM;Dl1X(+z%l{5-0H(3$AHcVo=a?kV`Z>+5dc%7d$M zs<*p}?SiJ0>e^|5SiPH|J%C%jLO(WT2>g_akG7eT0^L9gu! zl0S*-6ek3mI;she?GE3?|IL=^3@i=pgt@3#yvL!T!b<+!T?UE^?;mb1wtwZt z!}acB`%QMuH&9)rf(UMW^H^w+yu(?M3m=CQ(uRC-+U{_mPG}2`df>f!lZt>!Zl?Wt zN%=2(IN*bd;ffn?TE+tnQbOcqIL3UL(!M%T<~+dR@>+NC@MWpHO4xuZHA84L_oHH> z13##zP^b{dLlm*#bd%W1J1z?lQ4KG#;{JFcOU}s}E4+ArF~7e@1PETpu6S^~xX;$B zhuG>fOf5{D1;n+&E$x&B`%=&_Vu#rKD<#mVWH54H9Cvkd_+h^}ANE}{b>c-Sad_d% zVcb)e_wtXRcQU;!pRo{w<5rK*<(8%*Cd)TM= zD^V3h;0KwQ!SV;KQiU{lQ>|D@A`wG0C>-A3>*fLkq~b9MNZWQNj|g8z6E+~1AX_-K zbhM%cLpS!@Vc!@yX*46)&Y2q+qvAoCoiRo+lsyXJlo7tgeh#f<%91({&k#ZqfW*XP z$jac2$q*iOTcwad5nI0uySif4<*FLUu=>0dlnWO^p+&=RD1!|SaVG>!5!lYx*Ow{b zX!v%n~q72%zXxn{Uk1 zt%-(m=4g;9_F!O2<&k>C^KUY*9O{-y+`lbad6(Q!Ds~2JuR}le(T{pCથ(wP0 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 literal 0 HcmV?d00001 diff --git a/action/players/gijoe/ctf_r_i.pcx b/action/players/gijoe/ctf_r_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..91082ae715e7a6c3c1c401569b8bca0740685642 GIT binary patch literal 1293 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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/m/ctf_r.pcx b/action/players/m/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..160ccf3e1bc7f0f34b9efb1d2d38f437ba2bb752 GIT binary patch literal 35563 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 literal 0 HcmV?d00001 diff --git a/action/players/m/ctf_r_i.pcx b/action/players/m/ctf_r_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..91082ae715e7a6c3c1c401569b8bca0740685642 GIT binary patch literal 1293 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 literal 0 HcmV?d00001 diff --git a/action/players/male/ctf_b_i.pcx b/action/players/male/ctf_b_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fd0aa86502fa2d3326b7066fa60d54196e2be817 GIT binary patch literal 1294 zcmbVKuW#c>6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/male/ctf_r.pcx b/action/players/male/ctf_r.pcx new file mode 100644 index 0000000000000000000000000000000000000000..77fc40d6d26c20b72a1a9973e233df37e436c1dc GIT binary patch literal 27596 zcmbWAJB(b{ndfgq=q_TpK#$SfTaP=<0-7)=PXpZn0|rEq)v8h}V8ANWO9cippg=S_ zP@q5o0V#0cK)^b1;6O0sOh7dn8%*H9!9eHSNA;60Jx8{8Y|nUr5wN?-4i(;H? zw{PFQdv|Yd@BaP!AAIn^haY~pzrX+MU;p~}`1tFuzy9W%Zz9goN&mn9xu-5Vspk!M zvsLFQagBO*v*B94@t}SZJ)C_RJ(=yNv*YNf@nv!|OO6{^^rxBI&u9C;nH7;vKdQ$F zNIA<+cQ@-E0gbG6QlCfJ{-fDvk(*@)ex{Q+b++$0GmQ@y64U6E4-Sgi!R+8jpK^%P zsNr?CKcDR%&$1|u=JoVu-Y6F1xQ1^vPFfk`ob4aZYH@Pbbcvg}gMD}4?QDmnl!5c; zFisnX38&sXmLf-uG>&o(%@`4I1w(@^)ySR~HVVTqFGtQ=?q=RfZzi5MS*y`Hyy+SG zF!HlQ;3Zp$TW|fa^+VtCKWsJE(}UUmK`JE#V78yl(&#vL4VT<(CHYN4A6>FjX52-u7zAWGYV!p*0WGejt(&Pq65Ykj!%&hh`E}n+m*&zQGqN`44_U7R8e>FAs~>3k0vtfVXKDTX+47} zbU}_>?{GE;S2JJA@LqQ12L)P!34(MsLBc=1-Yj^@Tea3{jL)3F5bg}O`(H>7gP)dk zxGD-slTrY_*ve7L#-5!WV4o$oUNGXp>`=<&FN~OJB;$jzJT8c=6Nq4lQ`259bFiNu zJd#aK<1-sKuZzM%gX(Ucw(4x<<`@p8I1Ae`^kWT+>znIIVGn=NiA;deO0N=jMM1&P z(Whp8felj1=*8Eha5PUi@VIsJVJjA2Su2ZO68(;Wa%3}td`4!%HjH!>PmLH%vG72~ zm3_clw2orZ$o?7X%H#1lIpOT!!Ax-~MWdo|+?pP@5(Xoj>T%0WV0$XQZRF3CNgglF z=KDBFiWswwhAwSt<^)49k1zwEXdT+f0jE6nND2#xfFHB}Sfl#}!Ge#*)5%~sX6R|$ zn#Zkp3WmwZ4HWi=W7eaEp_Zb_|Ze*0hXRT=%-q_a} z7@GJqw>zBe;|(XoY@{3_hj9!%9Jt}2=lTx^uHWN*YnaLT;>(nR&t{)4p@M>w40OC?;vEHXRRo0&&jsARUH( zeQk?_@lbvR|I#J6zr7>Pf3_rxC~>G@%-TAQR&wE2Ri7&Zf|C96HWIXV#fuH!{0`VyXWS}(#l+8q`{@9VvqWSlFEp+@# znA6tbP}TA%Btr9b5vYWyqGHw;F(wa@#yya;;efUW$U^|f^+u8O2P1br>ZQrl)#$6x zflr2Gwg&m|H--=~WdJLVpoIL*%Y6LF`DpNGAPEPUR=H@6J~__@vq9*nDBntk7LiDl z4~S@G#zmq~#-%?Pj%oxK*=an9T`f&~ob+L`e`zqM%DdWxkNkLAn~t&}#q;yo7lB3@ z46h<@&JT*o^pkVB$$~-!JZ2B*R0`9U@D~%%9m?La2gw2~aLBeZU6p;*X^Bio^ie(> zV43k`Je~NdtLFWr?$+u>+=vVIjNQ;JP<_6n6J~Sp90Wu?#rR(YL|PvAro{w?WZB>o z@*v$NT}tA->f*_0IPiROG|Y7Rrz8{f6x|0H zKp6`5qhc8MhW~eD#NxCV91DanlnKtTDUL(9(4s-5nN>WcRG6XbuU(3(Q$NXi(<)(I zEvB_upZIhMa5uB_J7V1`T5&M^R4lq4-$(KYXVTk8YqUP7m<%y z%*uw=-*Ds;f)%s*{$NPT8;r)&*q!_2lXJITow&iI8sjV4p2pMhh=fdK91a97RS~9Z zMJ*uWTKh+|4*@1wRHg)>J$=i!e2Zc>Erv3gHSHh^{=|zOz?7c}D)QBm5VTQ-!%`Z^ z#=~)3uetLCQ=Ypdc589s+4{guYxNjbN!cEJ1>#ke4c>`O5eUuquqZq(09w{F9w|}4 zmGhSX$I6hw`7voQr?6Q#$ko8DG6qHq4=VzK>6MKggpRQ+v?)K&r$j8Zlq!M_1`p|O zc7%_8Ml6u=<#IW8Dh%To?n@F4C@Se^f4j@KS+EYYNsaPe(Njw#WX|L~Pa-r1O8UPFHF$XR-P!ZSE zTgH$}L*{^T9mv!i;NN>9-AD}N36PyIYqEt!6+N~fAW52gLE?j11_}oRkN1Gk4JYH+ z)f+LrcEb_96BxApBtQcxv#4bkIy_OPF*3*yYIeQhBO^?i5q>AO+IaXJ8-Pf{nE-{= z3M2~XV}OmtouhmK;jh+6&vKzStuY?I5g%eViRmeH|oIVCj4U;xg-Ww-&;k=$CkjAkux{Chiqc)6=%{hy@x4Uzv_A^=pr zmolFmuEEV3Wq-|6U;S{6D!&GMYbc%usz6oXPyrHU9y_X&%HP!UO`vd8o9ueN2x-9h;Hy$rc!~!w;;UC zxneBLn@BQvfk#0ce3zE{Q|;@Ho$(IBB(xm0zez*8Qz{vG}7HPiqaWO-p` z*c{+SWBh=m$j-SYnH*4vDvMi|18k|{q@+k~q$*Z6PNy;bg=5A-R|cZoB68SA{FIf* zi7lU5hCj9BM7C5zq*<#0VVE#+^k%kUI+6=uv?KY@ctRN>Osu#7Ld$);NS<^F;REHf zVod)~fWy3yiKw+dWCAy3MTo2f5S;I^xpX*{v$g1Cs}`1x$g%w#XHny%hTWjxpGUz` zL>Re|iTaGyXbI!3Qre7mOP102Sb`v;N938-C{7i<<7qbJa8DYufU=&B$+@=~x>XOl zjs8&}AQJ&LD%&a>R=!VdEU`u%GUm+^Y-@v00}%AZ6SQnp#LO!OR1(<~By?9!P$t=c z9)tK17%^u)hT9!vyi-n2zXM6yFNP6W#mvq*sG~nidh; z3UWT1htV<2H1>6-snv{szUJ$A2_?%@uqUH@;0Rjf5L-Ra8x<=FRGNY*@+hAhHGdh5 zDlue`I}A@agVPW}evygrS~aoku;X&fq7cM$V5j1~}CHoC!98NMzFeq>TfK4#=mkvip9c`yipLA+h9A>6)m|J(^f+;sk*w4 zX#*I)R%b?QW5-p}LLRBd5;cr1su|k;EGv}(MiH>Fi89qvLRHL~?~4?Qp*HoeTvjqS z(@s*y7_}+C}l*bVz;8v%uY1h&0 zQ8ZR3Fc@CSCzp67;koL=2-_%6nB7z&J|&0HMk8GoO=l<3KPqf;6_^vFi>E*7jSPE) z=gf{I*$*=d#)j;8e#5dGzIM9+7$; zvWf@^S~?KTsE8RArl303{6#i3-YruF6Fi*#(xzD)y$Gl8ddKRR5b)>Z2$7#2n-GQ# zzQVKCnboRkP|bXeKb?drIZCgzP9bHGjYGW-9?02pH;2Bp%SoetP&+7UJh@szzB-tO ztFbR#7tOM%abeNP&~k4vEZ0Be3aH7F zIKuN&F5DU43&uqY<3gQ^3)ftwqMdF&t(8SRoR$V$jb3*i*FX>_WKxf%L)QGDVK=^3k?IlGd8%Is0v-o)`LSD3D^ltFl1yB$J!KEf=Z6(AeF$- za4m{Yyw8mcbq224A7Hciv>xa6IIFYMAf|L*28wIvEaOu!j~N_E)H7Cht!2;^nf2!5 zrDz>{sB_Sm*v{%d?xl1nKw|m0bU|`5G{8`ezg$nXdRB}7p0U;LIY!sOs=g&5BRV%6 zQGA%q<&j7;J4T88@PYXsjcm=mTr3wg8LV^Z7)rUtK{*BxASd0*t*H;Pusyl`I$Rit zPRT|M!(L%nOT%PbIW|{ zUS8&c>~gX2&y(Po^a?thERAMrhpZFSvzE7J?Z(#~ZgEL|k#)Kq-(7dzghwOqc8;{| z5`#c3vg{*CC@A02>LTw$9d!HA<-g#WFS}*;GUPAIPlIQ=OAzvcYm!t0XNbny4~$WX zwgh3WiVJdYMLq4fE)UlM-~8FT<8G(ucG%n1e&4;kz-&Gu11lQg!4R9gsQKq^IDJa{C0wH6P{^7Ig@=TD?)X4n_Te5}?YQ z0$BBrcfqSE3(}g77REsH2R%Cx2US4Rvw*SiMim1O7T{zN4^1*{vszD+hHtJXerq$^ zN|Vj4RIc0ZwoO;0ZEbRSaLcW`t)#i0H81;hmo!87G)&|meVUv=L2OR(>Z25wHVC(fJ96+(&p0TbZc{KX?@El-M>k7qU!`)I7$0&*uBN= zLR**JQr66qrjw7o_@_B3J1nn|U=(z1RF`X6gbG);b85cky1sMPpcal+? zx(y0E+M=`bP}DdX4|_fkQvS?{WK6}RWpk1_lJsY7q+%UEQ)(`n{_;|i8aX=ESR?A& z1sWP>Q09P^H$5weO|RYui>Xd&<^?b`Hm%EbA4FiUCx!-$UhhQ|7q)a_IM<$mLryPC z(0tbP^*J|lxdbcf$(T2<#Jx~T;G8~6x7L%T^~+0nGhreDUFJy&q7Zo)4s~r26T|Q! zqlq@?;Smf-#tm_q3LN(>K#6h8!5Isv=+<=gqo!ZlV!ULlT@n*$Z;K}!-sA0CNd_i= zxo9S=t_C!=Du6&TcF0*Wr|4HTH9X-N$bQzH{WI!8RDtkUP*@IU%3}kFn}#L95W;AK z0chSSgU!Zi)4>;RAK*z0e;&G^U4F6E$so(&fphHF#CXAv6w;xkusmvZDG4yx>JD{G zd%xTH)DW4#6l_kiL5cFO@Q&mGk-Q05O%kpVyd-P|!-r?{oTJ%ka|!6;4eTAEmIRar zI*zOnNiRu5$Pc+U6I$^&#OZuPXwkrCs9oH=zmfQCXY+uIynrabw?XdVBHcv;5OYBSQTvg*X~lraf77mws6a&v9;7(I&KOo&40*H zFTOt3#_X$L7e<%DVo%oQkIl!TFq_gKGB9=eOD=0S+gTW0LD3E;iG1GcInbtH*=o22 z03S{MjiEm4<|^DAU2x!UB)pjZD;GMOc^h6)X=!US2|^j2W5`RwlS(wUDTO{D=vh&i zs-a_1ja5A@a{L2*I@j`spKAm)G^WSn?kjQ^)L=c81?I{0CAvW7Fq3rr)vWKL=AzMy z+dFb0M$(zSotXd`$y>`u@1|P>{Fqxh=m0e+D=fNZtW|zpd;(H!J}U<8zb}0TTmPdC zo-;Vh_6+W)EakH%sVhV*#k0Pv%WW(XkJ!|b7&}1DO+n4gK0p&JsaV6SSa9i~osZa5_7rw5NNYunrPj$v%a_I~7hrUSf<<0DMWb;;8OkSkJ2&w)BB`~LkFcP)SB|X?NA4LBkv3Lb|$@5V=;N?Iq ze{u^B*QbFw%4B`6jEjqM7TFrzy@ ztbwGpA8~b#HF5*~@4Cl!-0n1hu8PhBQfx3@m<4rqU+5r(9Iq425^C&lkqQ4+B;!E_ zoAn`>k`$T`>inbbD^GBdh?w9;J3iF06J4<$aS;YR*%@4UOV%zG51YOsVFgZ7#1M5Cc!xR9YbqTqE2Q ztB@K{Vl+1}X?uu2ABC&$*+j8QrqO>3;4zX)$U7;_3CglfP+ z3TDk!)tyYs81PhZ)00Si2}}xoz)9Qy|K|QW7r!Jg)ip@2CUd_8Y$?Q5)?FJxAl-rX zqI+x!)ODKO^jio)7jq{bm$SI+D<7O{QZA2UR1lJdn1md4UjZB5V2&p}qi!@G_LU<@ zm8kg=m#|43x@4l*(={fX_>x5m9!ac^UF4g)&0RIL-IR(5#2qR6NOmaA0Rh0%WdQRn zRtW%92Vl-s2@^Aevrf&%|XM`<{nOh>pC z*W94e0HSLe=h0W3mQe^Kl9HYpTikBP$3g%WU42pxwYx#M2mwToK#U0y0$UvwUsBQIIcSq2CjIgd@anUaQS$Bu5fIBGqloV6Z=Jj9!r;Swu zLaLCGjx_UzkGsfp9oz0TQYtGy4j6F@(Px1SiPBZ!SZ)D*q&~lo~Lyh4NYV5SeCV8O2&OOq~wKFW^%oDFP?)^{!CLsa3jsXE)~w^rdjj zJZ5Y)5SG{h{0t;F57`A?K9groFHM2q245in>E?byNH{?On4|fKhMTXKDB`GqtTxsx zahR6eOQ(K+NH;BXZzQM zNFv2>xS^1AzMBCwo1Mv-N#&@_-9PHmNRFb5wHh&IE^Gkhgo~*NlHsuk?k1GDWav*8 zNa>EPp;AKP?i>yfpQM0Y1S=W%E(8iXv>t&HwYg%h+eB60EOk>d4QA-v3BnTQ$UYqn zx>3m8=E#5z7DgWMay$_tb@wm-^mnIT3@R|G7Jq_O$P?yEsbS(>EH zPMMyMuu=+4?iPYh-4h>xD(2*L0jPplIbPQ1HZiLq-vg(m8WIjcqXGd?AfkKL<-0iC z62oaDRa2TES8HT7Tv{ABW*DN5_EZ>!DaX(_4}fqO`JY^oGHuwtr-;Kd2ndw}`SUYq z*0xy-DN;zU%m$Fs?R<%VkYLJK1Q!pPm}y0qwkSa0);iJ^5z}Cn5K=4QY~Jk+pLJA6 zT_RW5gs!CTNq4Tb1SUxaETlV`M5NU%?*``)GHDAm##ZrwDY`Wz@&>dZghJ}w98GO# z5~`f#88C}L)g|r)5T7J&0~d%w#Jns15Esa^v{M!m9sB}DHALgImNxWiVMr^9&}=jy zi&(ak^CFY!2G2a;?Ibv!L}69~qFdY5-8NE69H4MM^@mac^vI**3h|!?^$=?fyqVh6 zR}mp?;VP)BVq8K)b(Kl^3UuJ;GDt8WaPYW=F^s$NNiK675{*1WAi-n67ATQpt%3!j z-Wf_24ox;yld!O|HA&m7f`Qdq;%{pI43=lLs(~HcM0ZmvaR=MF0sb0%x$Jhasf>N5 zIS|V@Asj+tVr*~I`6=Y7cMxTPR-XBOCgfQUr(k^mBxIpKb}4N_{*zsmm&X86vYXWM zdP5ve_=b|?W9HESvPdW9XO=**F%;q}Ji1IZG`0Z9wP0UJtfr+6*rQL^r}`QePRH7Y zP<;v<+(fFu5;4IdGkK0v4NM`kO{M}<*UXb{H*~E4%ErP&sx<$%a8fcC3gqjnU{YTH zh;s_7fgoM5Bk(4+OBe{!LKU7+f_W%s3VBOzt=0)1Cfsg~y{tx*?J4vUMM@FkRC>T} zAXvf_@MTB0NZN)81Oby!j+lqN07{L?f}2N3fMv`6=&mlEXQqFK0X(iwil{fLVH=s; zxu=PkZguP`AIih!&Bm@glLvVDzrS zBU`#c7_vf~=XBUpf`su|zz|#ftJ++e<)nICGDwTZr-(%jtW2K?Z&VKCloFq9Yfc6- zjV)OK#CIITPc%hm2lW7SSVqp9n?pTeil}s5)}pz>R*Wbi=tyqsis{>26%B1-D7#_= z0)ROw>p+ABTH(;vY6#ZC!}$bg7Sh4zHI2nhO);8G++MSQack@5R}tN2x)=r&8@$hKESoPJQy({TZ>r(cRGhPXD0ia zv&JaxrU45o@*S9*yC?$;&q}_rZRacCSSrH!Ff{HMOPo8>O?gS3j?Dn%Gy=Z?wJ9Ka zc>@Th1uOz6s}Z!Ej{dBjRGGWg_}-;v6gZUn7HbIRgDjH!ylP8`VMZl7)I`EG^Oao* zCiVuyxssRW0#%%vyD}5GkiMIAr0mV;w!~CAUocE)|CxNMe+Cc@7w3b3YXUy5vocTg z5JE#-n{U(PebGU+Bf@ZLL_)Bfkbk>^R79t`7ZX|1f?Ip0zMhybTP4v78yYR#f49%`U()Z4xwhOjyfqC36n>>p`qrIDGr)gVIwxAjj=^LZYUV1e9Frw{ zEsU?d#p$RU*}W*@07HdpTAGYq>V#-KiOsk_8BB(O#caq^fDnmk5<|?U!t;*?zy;BmgH>}n* z+=Oqj4X6CMu~i#p2O{b@v@w-`H0%sIV_B<2ri^Z#m33;$M=2S4A`dPX?Szbb9aO?- zjC3~+UsxHq&M=*DqB)?2ySOT|Mu3caMR|>{pi>d1$vDw!CH;(WF=L@{D-q$__Cyp7 zI)kZ@eSz&X84z{Wb_-+HrQ|~ifi+EHAk~gFdDNp`1sYca!3eZYI>;s=*zk0fJ(J5i)Z1&9G*wS1qd!A zPo@+M14l+WWYQ>HmIbDbT2ikiH95zipN$|4A)IYW={;h=3s@nwp8F6pvfw>tW{feR z&p6}Gc~QnyFWd`Wg^kH2Kijmkq${h4!W(kC&TSQri*iu`Hwpi^A_zpT&eC(&)*W+3 zE;UqRZqg8Oa|N!^6n!5R2CLgTrcy&9cp{Y9f_n6w{M% zIOb;tN6r+W00+O&YMC3x7~Im7;!ty@(3_a+FB)*gFiOPJYC>kgP}0stZe58OZEKhK z$AgZ=jcZ5Ij$#qo|4BsqNd8pQ>Le@q)HDcZT#OcRXB5HbstQKkphq5+03e0ln>I@~ zWr5Ecp68u+9dmSq-z7skL7 zS|TmL0Nr9${exm5YR@D51ZYaruAclerwA9Gh%DSZ^cv7n`P{DjjTU59)VaE?+FN^8SaSz zV^A|!5&hCd9K#`D(Mp6p=5gT{2p+P_GWQt45ot9^g6BYvwP6^lT6gsHj_mN7G! zIw4;94A4R!p7;pKxy)vimUKn!;|QmgGMu5mKbSh)hsEK)GwGlY+^L6GH*bO@2O;cJ zmEsd3!(4`8Q%X+MIgU`8alJu6Z7!Msjlfyp-20^+#;~sE zF(@OT8~%p|222funNa(IB0t5-!erW^L)bv^b`jaSf{nrl$Wx?fu?e;aO9X}f&k_J! zgtK7?8DVHUi=#>X8i50jRL12OCd09$iRzBIh>>n0?cH%01~ZW-jDJ2uT}o!@X(U|6 zL2l4YvlpU=qOn+;<5M1ALi`u}<0#uE#?Cw9!W-5yUjfy34CWc`z^4O_)K5b1Y4l?jwNnm7Ik|TCr;V*q<$G_rscw8A=O~3E1 z9%+};!}IajzrM4z-d@6(5t0=m;4YJlg4SqM`A$2s03*mBAptmi#yPS+3h_>XrqoDg z=S+a`V6)qKx&NfHmseJn?`^nBac}3|g-^~e+;bNy_dYq_tL*i5w%pP>c2bkKh#H8o zv5hd^Aqa60gJ)`G3kW>KxX1?+02n_2VN)c?L19L>l9aUg==FZ@t4iN3ulS0;H?O#R zJhIC7+v~1E~4{`8hvba>&W|7`5D03CKZni_Vw1b7@5oe8ql(%K+pR`?Z^1 ze+Y*j-jELq5sa&O6#e{I4lj@@?~TxRag zpJ90fve%Pe@Z-~FF-GPg{4e@=v;+P8xpQtyW^`cj0;dA=3?8(k>;sCra!5fWWT?>l zXmh!uv-*C+*>9=zH@JV?tO6MOArag0`Y$$5La-S#5CRgX=H}sWWraoPzAZD4vB5&p6l~?4kfLufJKP((`XngJs0J@N-rl!Z8<>B@h`|> ziUm0wz0uD^X3j6oY~bw%1bgXcX1L{Q17%+a2Py!C;Iae<5xV-Nu;G4peh5&mqL9H7 zxJ7>@eGvg6gcMvhIGXM|fy+OT!WR$@L7<-|!Xg`eK>!D)0u(zlZUL&SyuNiB!63tf zLpjVyHRn}L-?``U6n|bJge-PsBVlod1#J33AAV;PBVn@TJteKMEaia`e090cAGdRz zp|@Nd7%-8R;0}8mD}BLGuEWFZ*<76s`K_g6UPTkqMK~^u=l3eI27daCAxEkqH00C} zSn-(28>&4{4EakDMTm;@qrDB-MFRTkCynGg7C~k)IjlYdqke&EBgb6mfkyH>RTQ>l zKEso3JX*Hl1B?KvgjOOEiJW3*fu}T&ackJrdu_Ts2A2;hpI{kO!v_K&f?I&>N00W7Wg4#1PUth3*;-kaVvhg=g7bnl4*9KBEU-6EB#`5uN$FT ziOJ|ev_<|LIMJqGE+8f3;C_ms{IcMECOm3G>*p6J!#%%3 znzedOzRWL(8x23FJfaN^8E1+Ul||Xdb_$f$NJHWJz}N^sV0P-so8vuGbij88~?a8RbkAN5vAr#Cd51B}#lT>&7 zx)mLUB2O^PO29IBt`CewI5Crxh0q0|rctKEP}%68R#J>23;+QSm@ZH(<*OuL@u+xa zQEP!Cc#U>>SwsdWpXbSf6(_7jD4u&)ufQ_h5=w(XvXamF zjH-^Ghjd~~DKFX-27WxRQjmpLBm<5dC{9WgfS03l5)&MGn_SIwTJ%$_yZ%Uc1P4-I zfTL1PBwhf{1^qY~7-cKPx32by4y~3U6x$08j8I8%5h7@XqFcdV#I)aF4p3mTKKl-r zxz9fZ6~;105j$TZhn`9}WF=S8yoHeC*LMRo&#-_{SPY7)n@AzBgTJs%T_T(}=r$dm z(p%~AwxFwoTfb7F@-Fag^b|{_m3;SAxw2A=Wn&?XHpQkq7XlzBV^Eq6kz&XM(V3~- z#xleZ+3;XVMm_MWf$4=KOP*&g1RP8xWNxFEBamFNvH{MOfZ05HnT2W7$M?ILJ$K`;W;l z^mH2#D+&sPF{s8-;V}Zrc=o!rFpD-bhB;xlIa=$7>(u^=`!kf1zBZ(u>^h-5)Nt_R zSP;|su75_@A1-U~9OxH7YP!(v3Ff%a{a5%Qj9J znSxITMQAgkS|2UavQ*1`D_92;c|-!ft1gNm8DjH~jC!`5N30+mf-My;4d9Jgj?>*Y z|FoI?;C1(d*RvnwNM^m%SYmR?3QL6Al4k6Iit7}dZ26J(OMSZe6m4{4_B znCz#HPA53KO2W08rx8C&rBxiH>kzhV1p-4Up#)xZaI}l`rx9)WE=OiuRAFISO+~~d z7eV7=;n0N}Q<#GvY8xuhy!|AyXv4K4OV+mPsIOwbB zKl1+|`hWU=7rlA=%H2+LaI1OeM)KPA>T0rfvATM3 zZMCv`vGT?nZ{NN9*4@DylRI|@w+DA#yZzd&&aE3aZd|{9jsIQJ=H}I_SFc>Tk|asB zT3uUPd-@mt`{tvo#~)nzFYh*g_Ga?#o$B2itGBLRyi%=vd}H&Y?JGauOg^}}_I`8q zo$AH6s+GxFbbB@GtVTChqiYwV&5O~Mi&0XE)+*8cH*UW(>Ff<|y>;it8?RlTbgm6< zZQs7W*|~Q0*7lX_SCj24)vL)`v$~23;D7r@^^NP*!FIK?S-pNm3ZfnR|Mu>kzq|kT z@rOVA;@7`A{^Gy>ZSmxzU-_SW{Hu3<{_)+PeE8bCA8fz%es%En+U>ViJ9jVM8dR>| zu590`T)h@GH=}AZT3w4O7a{+H$>hg(?%aRvwRbw5y&E^)ymsyG_V#3R^Y)c1x0=ms zNwT@N)?8g(yLjzk2h{kMG|7a4`Al?K|&-?$)ihu3w*QZ{NOp_12ZfXKnk&#VgyD)*^^K3W-}vdZH|}3En?HH_^y8;b-+%ga@bu}`r{6T6eo=k; zcdJkLD^GtG{rC5yKkh|OCQyo8HW)c>^G zUA3=1SG(KC>Pxk&9^W5vT;upyJ$&~+RqJa1?hLtJSD&7JTu-L;8vpp$$HuRi@cJoWej>G5uB#ocz6ZTIzF$|HYJc;DyggQ% zw}|2Vbmo+LfvMk|WBYgO#yNm&s?8k#bFlh&5{2={3dNk)!HPTF`{OhtgLUJX0WI*y8LD!63>W~5xU%WxRT7Zm~B!i zsAW)-6Tt|lIwLwI8qu{hZ6`@9iPAVsq96+Wz*nAfUDt7Jd~cRznx+tfbIur}l-6hV zVGxW9w`|FFWFAK5vJ^jejH@Q^ge-B1GIhbUv1^T_7+AbCY1be*(_2i7DFg&O0xkht z2TKQ`1J{6RP^3W~ds(Dfp_}?n?Aei$fn#~LsibgBE(J3=H5fq&$iMBf)M1fip2Zw- z5{T*$f0cx4c8&C7WIOz@jPAOYm7PV>6B3`1aNOJ;BD$TUP zuz(prC}=u1zl`I{Ff4pO_q@z?o3@=u8CzB$gfa}9bBj`g5UT6Crs4U)sOS&!UYT_- zo1G%f^Ek`GX46j+FAQDJvmM8h^3+jmCn5SMP*-J!Zg zG(*>z1}8QEo#I0~pSQAUvpG(ZVHEd+u#4=JYB^3UWnh|07?LwV2}4T5Nzos*dxK_C z#`$H?>Ue3}jazn*m@4FsN2RL^sc`|6fldHw5PuT2;?c}Z$1?5PX<`3;=c~FN*LAn9 zqq;WhgHhkH`ZcL1T73myyRgq-6~n@ZsS6Veh6eN*bP41dqyRpE1mFN)0UW2UPf}{j g!gqy_iW~;rc^-{oyHNbFS}Z2RaoOo*@hFM@0?K!I*cYjaf!6ev(Ypnw2-W|-b90s;a82LuiX z6l~ytz`<614k*f@0|yse;K0Gv^o#@;V+{BoOMu0Cy@!9>tK0RR*YiDZ4;bwAUF~`A z?1P!<_phh>?f0kO9{At?8TgfgpTU9uQ+_}2`vHKdR4Sj(m&@f^t=4EX7-N%@lhf1F zGcz-@v$Gd2T)1@U($dn>#>U3(?yiD9)agTiqGtU2!(JOT-w*NTydcRNd+mCYH`gTP z@?N_DN(YqJ^7#tbLtroD^Et2=z#huy17NR#b>wpcSR~rOZ|=3%C1nYeRZys1k(6tI zu7E=AWl6aKXdV=5Tat1Y5WTM>Xaq}+@kC;e(MIBXabIFD(MIB>*pk@a(MIBL zVpC$T&_?2w=t%4}+DN<>@mwR}1BwK-cO*X&0U)AqaeI#v0wPK%ZthXqfQZr-v4)XY z07R4pu?Cs%gum@~BC!ta0KT@*>5`t;Vh-Tzf>s4`_kCbDfgQlh;Jgri&+qyje>)Pl z#PU1%QY^6*V7xnGiMV_4V$9j*9eVGESR~#ac2_JkgQk$O{!Yw2jJ3giF8s|_hq(8| z{2}bF_*vYo7G~6mdrJ5&;Gp?TYJ>M~(Wnb=;n&3#DY4M;@6#GBgB!yuSTE`uW2<8I z4ZMVv-{bTmu`F)H9KdZ@GH5;(es@v6Z(iIzg!%A>6HoU!n|~1^=Je2K zYgc?noIUtCIFIByEo0nAkUWe%jk#MZoyX!iaU?|J0N!5dJjE#c(4#l9&oGl%_8I0E zAlAt*ZsLB3n|E%BNAID1D<%D2)Cn+?JLtKFdF^8l!Ce>dkPdn023FxP_6@k#!F>&0 zS3Eofe+|wJsh_|314j2Z{p~HR=TB)nkh~2Ge&6wXJL2m@@O^OZfYXOd8h>fwAh}L~* z1JoTv0amE%_kKWJAI9!V?iPIEZ^-pKB8F~*+JpWp)Cck7Z@ogcc$ab$Qm{(z8s_Lj z3zGX$+QXb5 zo;*))>LLql^KM&?F^?YP~++e!w2K{+>`iiiT#M^iNuePAzoZB{axIX*nTb5mDu-)S*o+PB-Y0c7>PcjmHNFv zPN(ON#GWJjN8&joi1!G45IuJ#))S~jMGt!%@itJ|$>xPlKFi$$1;|O5iyYraJm^L!R9jHm=?2$kjZ8& zlErO87W!LJbNk=$7Mv`aSDPG)mJyh3mSKi&86Jj%8(<=)%OT1vof!d!zVpF-m!WeOuHqvO z20o)_+3)|>?*q&Q1Fc-A34k1pS=?|n+Z}aBO~V^C0-YQBqT%U=#&v_+MqryPutyoU znQ3XvHAW$I`#<^eC9K&OG)8>ZFb=$CYhg#jxHC=@HGckQFPLzf#Kb@7De zt{R*O)zyzS(jvZpAuqa2gA&|;>0llh3Axi^EonYxkW+%wBKyIZlxiI$UscB=pd^Ge z73iQFj;4m1jtdGSM&+d*YiY-HHPCFtG6H+`V-I*41)jr^TQO6z$!N%QgOnv-bsg7W zA=L7)ooS$QGAM&DVlfQc&^;SO2mMrAa{|K}HAdY44{!|6oj07&a3GLIZZ=HYwOeS> zjfgrQUHxPuFX4-3nwzSY*0r>utC~wwf{oy(w4ocOWt_Q4IW$&3$*5PLt$O6Ri%iH*Xf|Sv zv}HEx0h`ptFSFp5u$s=OWx59Y>5xEJ8n)-ya+|T!I(KcIswxBik03|YW$GaactCwC zK!s)MrWcr28!LmCtbMYPm!Y+Tl!&wmQ>Aqq8z+>3&Quv7&FD<`q~F7vHO?G<#G&%S zuo1)1*pacUA6GS9)uHknk~Y#Ca+*o1h9E8A28dt1Mat6!eEk!RdzHS^BS)$(HUsYJ zX(Sb0Gi=AR^_Igt$I~6MiS96+9lD;?RI9589}ZvJvkqw0mWM zWeh9R!il17-S}7oT|<7N%xDKlshWBm{z$2m9M@n0A{8|)Zc{tLfq)3>84e7KdI$*0 za~uqC>cSZkn?Ie*P)p5mD@2Wf>I!uz5m2GwJV3`JC`k`la)s8dPdN4}b{}Z#Ks)3d z*M?=tI-cop=!64>i9{@u53D4Us%gNIAd(m*X$ou`J;=bP5O(h)j&&WiGQ1S&)5LBH&u-xu55Ph#iOq)m8AineCq8J+ zj5g{_hKNVah55r=dHMkIi(fI1GfA5&AFA-^5aLm-PIF)e2Bf11^A>1ORDl-u532c) zS)_|#3TwZp_X0*YaTfCXzriH1zqYAxrYy{@d3kie)Z}E)#^$8}CUer)xexBTh*PS} z5IBNEm5}lz@;`k^p{$9~WiFsq002M3s7N6!#1|tRUdb|qlV8yvn*VLDqHhM63lejS zhKxsV)1a6KO^QCV0l~LEtQjS3nlLfo8}gVPHE;joH(a4FP5sP3oC@)oqMwr=DYE#E zfh-~O0_~SH67O`?zaQE&+zVn01d4-r;QC5 zzV7`21HJrNQuGBY{yqHv2*1X^Is8li9x>i1uzG%^mLINWhbq}x*{qh#!J=9&sHMDG z%%%!vDyt^VR8mbPQpv>F*h$7F*vObO%tjg`!$XasTD?}SRx6c3{6|KqR4f(?g+ex) zHBD1h)ib9?rpN13tU57VUOC%1KV6-fD4!fD?w%_y%@qE4D*x4Z_SCS+s;OErSuhjl ztEF@0!r4-Grl>aasVOr#VI~|^X{3~TN~xxl!K6}3DutwyO(<$YnI3CQIrYhr+Qe{m zY^dVY2S;k4gY!i zh;NLb$oD;mCH`4)F>2c`TSruTT<10Dy1fq$wUJ3 zXU|QaIXiWFX0rL^_^Ii!sVQf2a(H}vh_U+caJ60^tW-+nGOZ|lPa-&$R74qe*Sx{3ZrZjtD`pmhLr_YX0&p1~L#3*|(%ps1#jOespF6uwE-*0Up*ieu-?V`m1(rUxVXS&yRg5j->+iK1e(n~!dp z(VtV%g+%m~^0cY+CY5zZnIBRvR+S4SWj3#zF_mUYnMx>QiZY}qWkty<%2P$DRHBGf niAp8-6`U%yd$t)&jhu4^r)$~W_4)Y=v*%|{ot|{gv624=@gvbN literal 0 HcmV?d00001 diff --git a/action/players/male/flag2.pcx b/action/players/male/flag2.pcx new file mode 100644 index 0000000000000000000000000000000000000000..5e1a45efbd62e2959e0020d414504742da170ad8 GIT binary patch literal 11487 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 literal 0 HcmV?d00001 diff --git a/action/players/messiah/ctf_b.pcx b/action/players/messiah/ctf_b.pcx new file mode 100755 index 0000000000000000000000000000000000000000..ea119cfc7c5b103f52e9e315434be3f6a9e4f7af GIT binary patch literal 52016 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 CK44`4 literal 0 HcmV?d00001 diff --git a/action/players/messiah/ctf_b_i.pcx b/action/players/messiah/ctf_b_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fd0aa86502fa2d3326b7066fa60d54196e2be817 GIT binary patch literal 1294 zcmbVKuW#c>6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/messiah/ctf_r.pcx b/action/players/messiah/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..c2973c9af80ce3c3770373be88f46183c4988d79 GIT binary patch literal 54402 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 g!gqy_iW~;rc^-{oyHNbFS}Z2RaoOo*@hFM@0? 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 literal 0 HcmV?d00001 diff --git a/action/players/sas/ctf_b_i.pcx b/action/players/sas/ctf_b_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..fd0aa86502fa2d3326b7066fa60d54196e2be817 GIT binary patch literal 1294 zcmbVKuW#c>6n+IKx1CJ~7|corUdh0~U?Le97#J7?7#J8B7#IWuSULZgWPFuDKtX{) zK|w%3U_iiul`eY-BoO-R^d~=jZ4Beh(*a;pA}Zf5u~8 z@cgm7%*!vlVC83iIDY06#HPpdAAFjf7Z=65H2d_DyyNVgm8{PS{vl;|FIdYbY0K08 zG;6s?Hm~6MwB^126t#RR(ubED&JNq85v8or!3c=kJIXXB>Ni(WP<`S}l=n zf!Rgd?`7-=v+S(NPsNv_X51UU>5UOPyv|wH!)+hu<4wsYMP5u_e#*9JZUxZ{X7SddSrKcp0ypq+9&Zh06S7P)HR zsJ^WXEXAS3Hi)UqRFe%=))lM~tjLI9Bx4C9DUQ=98bv`I`cdEo{=geJu4CJ_Wtn2& z3`5s-P16)bA%w`X-2E%Q&AnA+*CUz+`hKbJYVFrjy&Wp0Pa+#TI?{-=vW+Fx=7v)1 z@=V2ypb?Qm89WRw1{;Hkz(AlOP$ZBgsA7+WZW1^n-;M`X=$e5;JRW%jG zkYyEP86hM|qJF-tZszQ|PG(obvWgiClf)km2T|nuzU{iEWf_zn92ItAjFz5r>sqx< zXO>=4Eisg#MnYBg73>mZ$;iNxiXAIt-R3Vy-iJ-JtS2xS_dOob`kX?DB(v4F)9GPCEJHBGMGPRLLB}D_70KxzrXn$kp zK?$~cyrS`q8CT{@&mX#O)pfJ33%XA4_Gm*!8!9NZ9 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/sas/ctf_r.pcx b/action/players/sas/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..e779df22ff4db0b46c85b0f485aa71e6158b8ec7 GIT binary patch literal 32907 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 P-g@I3uWr11YyJNKC^CeR literal 0 HcmV?d00001 diff --git a/action/players/sas/ctf_r_i.pcx b/action/players/sas/ctf_r_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..91082ae715e7a6c3c1c401569b8bca0740685642 GIT binary patch literal 1293 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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/suit/ctf_r.pcx b/action/players/suit/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..0cdec0cc0dc62baadc4daf4ba271fe3d81ec5778 GIT binary patch literal 31729 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 Ef|(`QAjDs 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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/sydney/ctf_r.pcx b/action/players/sydney/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..f4af04dbe6021ac352df661326cd829a3c344e19 GIT binary patch literal 31397 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

    DP(5cy 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≈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 BxeEXQ literal 0 HcmV?d00001 diff --git a/action/players/terror/ctf_r.pcx b/action/players/terror/ctf_r.pcx new file mode 100755 index 0000000000000000000000000000000000000000..1bbc93b12a5440a6d34fbd8ccac42913628fdf84 GIT binary patch literal 38709 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 OdhJIyuD?5f_5TC1nu^Q- literal 0 HcmV?d00001 diff --git a/action/players/terror/ctf_r_i.pcx b/action/players/terror/ctf_r_i.pcx new file mode 100644 index 0000000000000000000000000000000000000000..91082ae715e7a6c3c1c401569b8bca0740685642 GIT binary patch literal 1293 zcmbVKA&=Wg6n+a%KAkHK7%-Iv7z{8l7+~N?1_KNX3=9km3=9GS3<3-S3<4Ge7z8W` z2nY-aI8YD}Pyo8HW)c>^G zUA3=1SG(KC>Pxk&9^W5vT;upyJ$&~+RqJa1?hLtJSD&7JTu-L;8vpp$$HuRi@cJoWej>G5uB#ocz6ZTIzF$|HYJc;DyggQ% zw}|2Vbmo+LfvMk|WBYgO#yNm&s?8k#bFlh&5{2={3dNk)!HPTF`{OhtgLUJX0WI*y8LD!63>W~5xU%WxRT7Zm~B!i zsAW)-6Tt|lIwLwI8qu{hZ6`@9iPAVsq96+Wz*nAfUDt7Jd~cRznx+tfbIur}l-6hV zVGxW9w`|FFWFAK5vJ^jejH@Q^ge-B1GIhbUv1^T_7+AbCY1be*(_2i7DFg&O0xkht z2TKQ`1J{6RP^3W~ds(Dfp_}?n?Aei$fn#~LsibgBE(J3=H5fq&$iMBf)M1fip2Zw- z5{T*$f0cx4c8&C7WIOz@jPAOYm7PV>6B3`1aNOJ;BD$TUP zuz(prC}=u1zl`I{Ff4pO_q@z?o3@=u8CzB$gfa}9bBj`g5UT6Crs4U)sOS&!UYT_- zo1G%f^Ek`GX46j+FAQDJvmM8h^3+jmCn5SMP*-J!Zg zG(*>z1}8QEo#I0~pSQAUvpG(ZVHEd+u#4=JYB^3UWnh|07?LwV2}4T5Nzos*dxK_C z#`$H?>Ue3}jazn*m@4FsN2RL^sc`|6fldHw5PuT2;?c}Z$1?5PX<`3;=c~FN*LAn9 zqq;WhgHhkH`ZcL1T73myyRgq-6~n@ZsS6Veh6eN*bP41dqyRpE1mFN)0UW2UPf}{j g!gqy_iW~;rc^-{oyHNbFS}Z2RaoOo*@hFM@0? +e.g. : rules voice + +Possible subjects are: + +matchmode, darkmatch, mapvoting, +kickvoting, configvoting, +ignoring, voice, variables, punch, +stuffcmd, time, lens + +[ignoring] +usage: ignore +       ignorelist  +       ignorenum +       ignoreclear + +To ignore those people who +spam, flood and general +annoy you but aren't +worth votekicking yet, +there is an ignore feature. + +Either go into the menu and +go to the ignore menu and then +just select the player you wish +to ignore, or just type +'ignore playername' to ignore +'playername'. + +If there are people using hard +to type names or there are people +with the same name, you can use +'ignorelist' to get a list of all +players on the server, each with +a number. You can use that number +together with 'ignorenum ' to +ignore that player. + +If you want to stop ignoring people +either just select them again from +the menu or use the 'ignore(num)' +command again or type +'ignoreclear' in the console. + +[voice] +usage: voice + +With this command you have +the option to play a sound. +The sound will be hearable +to any player that is near +you. Be sure you have your +multiplayer downloadoptions +switched on, to get all the +cool sounds. They will be +stored in your action folder, +in the sound/user directory. +Only if you have the sounds +you may hear them and any +player that have them too +and is near you will hear +also. + +This option may be disabled +by the admin. + +[mapvoting] +usage: menu +       maplist +       votemap + +To change the map to one you, +and others, wish to play you +can use mapvoting. + +Either go into the menu and +go to the mapvote menu and then +just select the map you wish +to play, or type +'votemap mapname' to vote for +the map 'mapname'. + +To see a list of maps you can +vote for, type maplist. + +This option may be disabled +by the admin. + +[kickvoting] +usage: menu +       kicklist +       votekick +       votekicknum <#> + +Getting tired of those cheaters +and spammers? Then if you and +other want to get rid of a +person, you can votekick him. + +Either go into the menu and +go to the votekick menu and then +just select the player you wish +to kick, or type +'votekick playername' to vote to +kick player 'playername' off the +server. To see a list of players +you can votekick, type kicklist. + +You can also use 'votekicknum' +to kick a player by it's number. + +This option may be disabled +by the admin. + +[configvoting] +usage: menu + +Want to change the gameplay mode? +Then if you and others agree you +can vote for another config. + +Either go into the menu and +go to the configvote menu and then +just select the config you wish +to vote for, or type +'voteconfig configname' to vote +for the config 'playername'. +To see a list of configs +you can vote for, type configlist. + +This option may be disabled +by the admin. + +[ctf] +usage: drop flag +In this mode, there are two +teams, Red and Blue. + +Both have a flag on the map, +and they need to take the enemy +flag and bring it to their +own to score a point. + +If you are carrying the enemy flag +you can type 'drop flag' in the +console to drop this. + +This option may be disabled +by the admin. + +[matchmode] +usage: captain + sub + ready + matchadmin + teamname + teamskin + +Matchmode is a special form +of Teamplay, made for matches. + +It has several features which +can be usefull during matches +and league games. + +It's main feature is that it will +keep track of the time, so that +the game will be played for the +full timelimit. + +It also offers more control to +the teams: +they have to have a captain and +can have subs. + +Also see the following sections: +captain, sub and matchadmin + +This option may be disabled +by the admin. + +[captain] +usage: captain +       ready +       teamname +       teamskin + +Each team has to have one captain. +To become the captain of your +team if there is no captain yet, +type 'captain' in the console. + +Once you are a captain, you +can change the team's name, +the team's skin and set your +team to ready or unready. This is +only possible before the match has +started. + +To start a match, you can type +'ready' in the console. When both +teams are ready the match will start. +When 'ready' is typed again during +the match, the game will pause after +the round. + +Also see the following sections: +matchmode, sub and matchadmin + +This option may be disabled +by the admin. + +[sub] +usage: sub + +To become a sub for a team, +just join the team and type +'sub' in the console to +become one. Type 'sub' +again to become a player. + +Also see the following sections: +matchmode, captain, and matchadmin + +This option may be disabled +by the admin. + +[matchadmin] +usage: matchadmin + +To become an admin you need +to know the matchadmin password. +If you know it, type +'matchadmin password'. + +Also see the following sections: +matchmode, captain and sub + +This option may be disabled +by the admin. + +[lens] +usage: lens +       lens <#> +       lens in +       lens out + +Back from the old AQ2, the +lens command allows you +to adjust the zoom of the +sniper rifle's scope. + +The 'lens' commands acts +like the 'weapon' command +when used with the sniper rifle +but if supplied with a number +('lens 4' for example) it will +switch to that zoom level or +the one closest to it. + +'lens in' and 'lens out' will +allow you to change zoom +modes quickly. + +[variables] +usage: say + +Various say variables are +available: + +K - name(s) of the person(s) +     you killed last. +D - location where you hit +     your enemy last. This can +     either be head, chest, stomach +     or legs. +L - location where you are at that +     moment. +S - location where you are looking at. +E - name of the enemy you're +     looking at +F - shows the weapon of the enemy +     you are looking at. +me - shows your nickname like +     IRC's /me. + +The L and S variables only work when +the server has a location file loaded. + +To use them, you will have to type +a percentage sign followed by the letter. + +[darkmatch] +usage: flashlight + +Darkmatch is one of the gamemodes +that TNG offers. When it's on, +the map will either be dark, +pitch black or it will rotate +between normal and night settings. + +Ofcourse, playing in the dark and +at pitch black settings will +make things much more interesting +but if you can't see at that time +it can become quite boring too. + +So we have a 'flashlight' in the +game, available only during +matchmode, and everybody has it. + +By typing 'flashlight' in the +console, you will toggle it on +or off. When on it will create +a big circle of light where you +are aiming at, but it will also +reveal your location, so use it +wisely. + +[punch] +usage: punch + +Out of ammo? No knife left? And +no room to jump? No fear, you +can still defend yourself. + +By using the 'punch' command +you can try to fend off your +enemy. If you are lucky, you +will hit his or her weapon and +making it fall on the floor! + +[time] +usage: time +       roundtimeleft + +By typing 'roundtimeleft' in the +console during a round, it will +give a general indication of the +amount of time left that round. +This only works when there is +a roundtimelimit. + +By typing 'time' you will get +the time left on the map. +This works on all modes except +for matchmode. + +[stuffcmd] +usage: sv stuffcmd + +This feature will allow people +who got rcon to send commands +to the clients. + +rcon sv stuffcmd + can be: + +and can be any client +side command available. + +If you use a ! before a variable +you will use the client's setting. +(example: say !gl_modulate) +If you use a $ before a variable +you will use your setting +(example: say $gl_modulate) + +This only works for variables that +contain numbers. + +[end] \ No newline at end of file diff --git a/action/sound/misc/flashlight.wav b/action/sound/misc/flashlight.wav new file mode 100644 index 0000000000000000000000000000000000000000..26eef9062ba762a520d0e7a1ca2dcd0467ac9768 GIT binary patch literal 866 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 BWjO!< literal 0 HcmV?d00001 diff --git a/action/sound/tng/3_frags.wav b/action/sound/tng/3_frags.wav new file mode 100644 index 0000000000000000000000000000000000000000..162464a9fb4307759d8408b1a7dbaf9d9b569c76 GIT binary patch literal 8706 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 literal 0 HcmV?d00001 diff --git a/action/sound/tng/disabled.wav b/action/sound/tng/disabled.wav new file mode 100644 index 0000000000000000000000000000000000000000..b50d01093868f736382348c7abe73bb704129edf GIT binary patch literal 9234 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 literal 0 HcmV?d00001 diff --git a/action/sound/tng/enabled.wav b/action/sound/tng/enabled.wav new file mode 100644 index 0000000000000000000000000000000000000000..0087c972ca4331d7b049b3d4e66d8fe2e9e51791 GIT binary patch literal 7778 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 literal 0 HcmV?d00001 diff --git a/action/sound/tng/excellent.wav b/action/sound/tng/excellent.wav new file mode 100644 index 0000000000000000000000000000000000000000..e3ee5517dfeff7f10b0aa025887e47ee9a4ddaeb GIT binary patch literal 7304 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

      q16*& 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 of(|JxIR8OI66BXwBc;>16yF0?5u6j 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 K*H6Fl%KrgoP&mQ> literal 0 HcmV?d00001 diff --git a/action/sound/tng/flagret.wav b/action/sound/tng/flagret.wav new file mode 100644 index 0000000000000000000000000000000000000000..03f2a399535dcf57c1728cb09d21edbf967a6746 GIT binary patch literal 13524 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Ƨ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 E1)9cc-~a#s literal 0 HcmV?d00001 diff --git a/action/sound/tng/team1_wins.wav b/action/sound/tng/team1_wins.wav new file mode 100644 index 0000000000000000000000000000000000000000..2b0d810eeb7a96086a4fd8d59bd6b74538e72a2a GIT binary patch literal 15248 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 KC`@dc6aNQSCs7js literal 0 HcmV?d00001 diff --git a/action/sound/tng/team2_wins.wav b/action/sound/tng/team2_wins.wav new file mode 100644 index 0000000000000000000000000000000000000000..e1694fb77e9782b546bfea57542e104dc2004c41 GIT binary patch literal 13732 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

      yeAn?#1f!=mZARdL`mdn zDE0TWXfR)nBVFJT7jQky-tSnGCqMe}7hnG<1QPh*2OB+l@AjPoW%m5@pZ(9TPE}UZ zIYwYh@BaRGHke>^^6c3%j^`)G)6`-2KKNjdMgW+^s5>Xa9MuYyqeYtp%~|ThU{$u= zY&5Ee4Ya&|=M|K9gM zs@um;*Td1-tFoepFVy0s^6$;+}pqWukLk3BLK0*$){gm zs|TgS(%vDX$<2m!`s`vc>6@JfZwR%$y5z;f)zJj47{gehd+o~3UcKAwTFX~oef=kY z@m25EuX4_Nzxn>*<~{!9-~Zw-&i&=*PwCtDZ-497Tgp6>#rW#;&p-R(<8bqKRpxfz zyIrER{QUW&+{>n^g21tjR6Ztca(%#j$fY>+MX{Rb@lD?{;-OL9sVnIyCcemzT%o#Km3`k-2%Nf@k5c43 zK6kLOv9q_oU+J0iAO6|TfAPQkZ{Hb2Z{7ON!#kzy@%enVc>L9WJ8j(E+}&+)t^KN; zojqOmlQDn{sdDdrmA6CRaPr9rsR1-&o9HkiNehgagu^%~VHT^+-BM5WN2gcM*VOO4 zm#;hT*VJ}r^6}%zcsM*dkE?g@Z5{TQR#OZI%hN^LU!RX`v3Y3}a-$K8oc!SKZxj;}QvHi(x)HS> zgV-6LB$Am}6mKc5j_Nt^H7WF_^OA)MF}62I92fOaElfSB)0SkngKqUCGlHwO z4D@b-_^YcmQ0MKo;aq8+_$&Saf$)Jx` z*Vt2F?x4ofqJxYa5Fo(_(6y|)k!PSDPvU6P5OAsM=(q~qa6FB5Ba9sdJa^I5hR_d3 z_4p`8Oel0ESEE=PriRqlN7)KNCAtfsSk($5P)8DmWYm!viZ@7GfRA)P9wau}V=WPY z1H~C|tty(s5RjqS#naF)Ao8*W5L-on20KTyOyQ?w!@?;79>}XCeIjWU>i;ni$p~5& z6jb@qm_>6YFd6x3r3GY`5Rfa4k(KN}$ucs!<-&a22U`hR zlLTP!B1X{;0jU5zA^@~p3pGPeg)>6$7H&@q$QI&Vz@34Nvmwwx-jSsP0L6qT32dOJ zo<;ySRwzJ+0C=;9RF|~TAyGhNjp!daCM*CTmqj352Y!q|!N?&b;J~{M^ilDs zHcAo!-GFU4DBpMhb3;c(1P*iu!K>;6%)wruE zVu27?#r6?ap+^TVAaBGWLvjd3BBdf(AxUmYu7)(O$Rj~Y!c@{zyw@DVv2N*(8NR>7Q=C1d|1;Du$d%x##tdq3i*Ev zIPNil17TsnIC=E^pdJ9I2wi#L1^FTY?~oA!;36G)q^SxQE|AAi(^#GC~qa=V1 zEaaG=O5jo?jQD{h)dWC+1AwRndV`KEAU?=N3lk&k5=sQm0#%@#{|M)R`=TrY0dNW- z1!j=BWFnb_JQD{B1_s5!iG#ijCxQVmidX?v75p~^09qtt13(6s5=LZiAOTWwNDm@} zBuS958BjJs9AE_466C?4kDCgR0Q@<`B*=s+!@&FwC&3HydMFCIM?#B4mY^Y66G$7`7lOvzz&-?U#AuPbV>b~3BlRcZV-37c`g9?) zj&o5!1_eq~ASXzu!4LuRh^k3x2ir$7et3B-M9y%rc?608d{UsN1lb@+1wnoi;sL>G$nZCxLQZ&>0;C||5|)9jkjJnuh)9+I kMf^-w#qtHrS%5-h4~d=OfM5cdLJ%(~M2P5lYt^fc4 literal 0 HcmV?d00001 diff --git a/action/sound/tng/team3_wins.wav b/action/sound/tng/team3_wins.wav new file mode 100644 index 0000000000000000000000000000000000000000..ba28226517354e53e33fd30af13afb260191f01a GIT binary patch literal 16660 zcmZ{Le~8>xo@ehKcO1hV?EZ5*+_5{i!wfUS3^T)oOmIkWOnkP_#%|oWuiJD}RaZY4+)5gX&HnvGfLQKMJ$dDNl?uIOTWR^Wfs;rG|r&+o1M>$U&-XWF0r=C!po*Y(`f$NOhKtzLccg1&z7#ryK(m+c-=`J&5e zIQz}0|I-&=w1C=|418srvl{(n3mfspu&=a#*>?KrE3B+qzU=Ykuz&Uwb!4MY$APu4 zNXuAdO{atZ(OVjbh85q`fqJs%|ECz5>G?w)pX=`{&Cg$-{yio4yyO4#7GJ+II}_xe z_8_+YQAZ4No-3Qp>^yIH-u@LLXgY1BF{dAi+*h9G-5H(J_UCQX^?B!2+lmfj{8RyG z0qy6?A+t*Ip>2nO?YX8Kd8XroBB(fS z;8<<|{%mhGCH$bT@V^G~ksVSVrh+c3_+M=!T9RhlUfosuMYe>suBvXuFYX zc(G&np=kzzZCErch%F~@ii;-OxyQu z(~E4)OB@{x+K%fJrY0K;V)i5Oo-2Zz$iLCx4rLx0^|)n%XS@$WC!yg%P3%! z?_22MsAd!znh%cr$TowJ4KdKwguFDC*~cL@SWvHRV*=ap7(1&Od?*qxEYG(Ye_#UL ze9wj%$aaneg_}0G_B_ZBNgy8x!oz&qf;5%|wlNcFer5Di8A%oxhkh^vGa3mASj5M`%C-gDeGQ7~%Y;b474T%9i^1(GsVUgA`7Gxm> zqZfL92u^4oL)SC|%8lwu>!y!K7q1^Z=3mqHqOO`4LOeFR_M*`wuPQxFgD2~XYWbe2 zSXpMOVVQt@7{JFMyP+Dst7%T)*-n^7eh|l@2Qf`ekyKMx?6j)gw)*t#ogZYMj(&V; z?{K(fg`zP@TKw|Z891X+B=c4lNkh%^Y+W^?+*ho!c4ZF}nPg*K(QR8(tSGdcw2Zwl zOJQ8o)MZIk6w6N=|EBoP`c)Sr1qV-fXX#p<^@T2g1k1{$%psOy>#z4 z#@Bb=y>;zkP^oI9?V5*Eu3z37o1$Hh?G6{`dS4)~SM|6IHLIv2%gr)h*RkG3Os~mU z&j=DllSfHe1eT2^k>ey)GlQx&x#rLJ&aJ=mtKJ`r&t~Ud4<^P?H???{NMo)hOW1Tk$x6f{%CG%$Er)_I!dWo(*`#rKAi#EVv#)~V_} zdMcbte>r>U)yX6EmCdX%2Sc-LJS}L-Bnb?!Dx|hvhJ%)pB>GS(@XD{sBnpBw@f6b5 zaHxBhEW1VF+1a>?!X%A-Q%qYz8RR1`y(Bn#0a zF7u*{EGJ2ANirNu5v?qg)vO6EFUFb(0v$?sHN{R6OSa;~)e$lgmJK(H6w%8=O>xo` zQ7TCt1SA)ZhdAaV!iJ6;$Cj0*rs_wgV!%gT1g&hI@?t)VBt0*5PIDbyBWJ}wKTaYK z96E;WJG$XGhGt=nI%si%C_VLOTA zyzq%lUG)=dzk_V#FRV z1%av|OvHKUCUx#3$m^yfw&l4jn93QC0;`aS?-(@uY|l zmLm%OjwZ-ZsHAz=IgM~sSZ3bD6p#(wFcjHz9hC(gCnu)}5zTC{4T^?o5H|>{Fu&)Q zO{hC*ph}t{^O9j|x|?KKR?TOjsOP!P$)+ZViiTa(b|SoJVk;WOk`nlGf2f*>D0UEs zL0acp3fx!|yDazsiK@M9U%xxpcfk^|B=b>LNm!+d)1JVUv(`nhn zwwD%ZoR3Bcf<0D>uE~Oczz83SqqHoOI7?lGV^uR$sKJMe*M)0_1?|`woYIXw9nr@# z3^<=`CxK=pg{9~7!i^ED6A%~tD<*^sDFz;#*P{Y!6!lHs#V~dHAhakLb*n??{ z4?=9FVH_Bm>w|27Rf@gJ@q!?r9W!vSt3icuB_t7nPg@Zd9(F0;(y=Tc9QNptEi&@l zz{GG@)sWyoOPIH&!~#G{%2t7il$Nv)r=VopH=#C4agd(Eqge6%iQ2#}+Or*AKA* zh#4r)Kn`SLCLgBu*@tau2w&B}|h%hyl2K>l}3&A=o zuV%`z$wXvQN@7V^ax1tCg;H8)XoL=Fp^&S@4F#`lannt?D z-bk*+m?t%oPr#&tRpwYc3m|!b{$(0@yI#|U`x%i zBu-MSHpfsUQQ$@BF-Y>F8jY(wjci5Wx~+DXSDk!ua_6wdf>HUQXpo!Q3Xe_F3i4(; z8>L`Q6?j2V$PlE@I8D# zZfOkXrJ<-jI1gSr0xW#-qDMOtfa)N|h z$tv)BY^{ux*e6t#M2EDEfr6q1>y<_K-aW#MPv%~gT?%;M48atOZcQ=&>@6|Ah zGkrLbja0wcv*0$v&2D+JEa4L=tebf4a~C_B6QyBPHP!TJFVhF@-EOZBm86q9@7x{y z=*GdLgdg0z>Exn%a$nx`7vufC{+Z5fWEonot7v+2e_!iblf7vy@un>d6kX_cm25fB z^Tq9xMJ#Mxy?l8W%hVs;e(!Prr~YTp`1PCDI!$u^ay{XkCr>Ate7Se`9ryL~LN-c- zj$TB1e6VmtBbhbA^^RL4mL%gWW|hmuc(xoly^T$dZx5Yt@g#kv^V#1e=QeqFzk20Q zw1-(cyj7cCT;DzEe(U1wQFP?QnjT`C*Ba8zIRS z+e5{gEw*3W`~1IO`GGhZ*Zpr_xc|O<$*K*bnN6obM_lf=zIA^7)mR&)@}?zECzh!CMXYevY`*(~c)JqpEE>;^VO$~xLvRbJq&+&wG?$j=KuSxX zG^VBk-Rg&b@$83RoBmaPLy?D*>6MGqTe`8oG=^s8^J+P1;?0+&qqyreUc6t*R#`>z z&L9f8uINtoBAmN2OYTaek;o@04()Pi^bY?*THF4+pX2N;m{abh!7^9VWhseS-tX<* zkH!9#-tqCUh1fows!BcfrLHn+23rb3tIu2WsjJ9FQS>+cy$oUAulXCtzuH}s|NhO* zVX8=}wIk+vl;*DFOoOZKJ8uNTVcVF88{NFj8ebdTE!sEw$ym8IC`;^*&iEi4h#n47 zU9p6>>##VGfaepkl{W~bvOj+>9Ch;m3RRKCY)mSz`)pEbQ}cmKDzw)^>qufCS2TBx7D zI(>K2?pC)?CWDs;Z{A&hVUSj(*)a|dGZU-lIO}!H)bDJ_O&uk%n<7fuc_0h~83{-+ z@|?Z#M(e{5zPGmb`RLzYKP*H?+teQ&^?K8VSKZ=%D7_Va|MF;7&ZD+=>)37&lA}@o zN-N#>H(H~^9BEX2m~{GX5?bO=)P0=lvXdj>ob%b^um7u~zjb~*Y-~GMTh4S;mA2L# z2Ny3y`|^cuF&fo6Uq7h%q3Vs&t*gc3@b#DF(*!rC+_pY9 zaFkh+=w!YqC|WcLJJSB$Svq)4_~`K;t<|4D{obpy<1BY?>@FUstsS|(KfU^b^~P*# z-JTyz^j-64Dc|IiX|j7&+aF7PZ&KM-J_-$-?!!vmHD`$>3`1&PT1atN-V( zE`B$lV{q7m=(cRXo(*AKFY#X;u{9aoqkG#vfixX`_TI?AeW4?^I zmO7dn+a0f|oxUTo(}(W*u%2eF zEN6Q;*HN;0ym>8oSZ-{VcS;0?g+J)XS#5WEcGDO;!|~mtT0GnN`NwD0gwH=X`y*>_ zZ^T_R-#igJs$}dRw$GSvAGNP=`N2}%G*0$}Z82(UE?$_(zmL{Wleq(+9 zes;N2rT#d4{l?vgo%8x=v9zxV_ipo7#KocC9jM_n-`So&(AKq4RV2#L2h`CQgDRBN zY%;IfFUp_X`_2#E_|@PQZ|?Yww(UIJ+r8#ZeLLV=<%zMS*UiY;8t&b4+C6t#avcYm ztloF`M{-|{r(?4Rn`iP(VX`y_cF~OWbKQ3z{^Xmtf192k#(q{SJI3adx0>%2}S;U2%TP*jDFzsm=}ED&l%(2Am@ujSF$Zew_Vi{oQAs zt4R$cd>~Z~4ndMLin?4i2gC>Pr}FxIHu6PYPp7%W$0MJYoDj!4!_Gz#Qccattertp zXL%LKYICIhc;m@4>8djUp4XR)%CgH)ux3eHIh^xd9NrRNP?AW)RfX>*MTjGks>H!y zn2oKW0W1~g#9*4VckCP}P8w;w@Ron(<{NJeub6e>>3wB14|SkI<2irTy*KW)wYVwd zwgFrPV0ti_hrC>t^3e8hBDWPcPuw64y(o1>8RtUVP`zo@yII_gT1uU}mNfJlU$pWR z=MBT4Qy=}{j=-~2#g1ZZF{81o;m#$n+U;i8 zDNiCWnFgYQBQe6Y7AJI>fGpp)g2ER>0CGr%0k#_iF*yO^(aw3KIV^zWN1%N}EBGK14uk?Vlg;lC6 z;V4%IYEp%w5H&GwEt1sJRNXCdp%v`s!q7?q=j$O(Xew_Nh0Tk}Jn8q7sn2UT=7)i9 zmdJvXD0M_Bs1w78s~A8g00$iqJ5VH?&_m!8IG<_*XPRh1fQI_W=Hq45 z>X$P|Fk^trCYbPX4l?6V;jJ8g1V|$^R3`?+3?H%x0>x<@C*9bWwX#tLUK4?9ugc|t z-^98}z>+P<5e^?Z&eO;jf-DA1uG+CZkh99S;54zVyP*Yq8~2ImL0Etr*|L>pV9Ee( zO*TRTMnTs^a$A_PgWDiqMIH&fGK>j8z^z;4NJ^Mewiw4ilH)`XqSD4`#Rn3ABOI<8 zaYN(fxEjd7ZJd2heDc;BJ&6FuWQCL}Ea z=YVpOSX09cFd+;ENeQF26x6fE}RME zSV2F42OZ5xLc}`)We7ghp-$Hf;0!=j{LrFAo~~DcaRRm_)9M~vS@Y8n=oh-sA_4e8 zz!6YDAXy16q;Zo$;1(gD04?DCA;1A2*b1&~9hd^Q8Ubuo_GR)DT*O?}9k|7`lrhj6qWb4HpVcT9^FV0Jj@0Ghl0Rx_c$VAU=|vK2ap;Fy+d>c%m(ny zz-(oFa;b75yU{G1n36psH(^j&gn`G5g$Gb%#9FnoWjr?$K?ZgC}2m7z$DZSYJ}>DAM#Ad2b!c}V4=+X zBrlVi{11jjT?9;uVhI=@!*IjMl#8Xo+!{_p{z7XW7Nj{LC)ff>>P%*YIhdB{mc$~b z#dp?^*e0FOFhIHF!{C)rQ`jI3O;ng5){y>~14FOm2v8=e75zwlS`LH{gH^}_y^|v{ z*0IWH7N#J=Mwv(HHliO`C9T2xSHj{CIV~a3a8DACU{qQi?7HNXFug?j0?EKBn+~dh zACkYaYZK}YE@+hlFe8v2zi1LNJY-;{ChQe;p?A!gF%=ntSfRdf6-Yx`LWu}z0?H#- zq8krVCVeO7$;F^J2*MP{xFX$w1}HsWr9=>9iM&UPA0HUlPSew_j6WE@`oTbugJi@S zk;kPOw9V#%gMfjRMX8BR0a}<1#6gJMjaC~Oo~%o>@QeD<^%Jfo7@Op1P$bx75@0;p zhgcx-$peU6MwCVXH^*4UE4=_q)D4`2JXmAH@s`C%h)Vh+9!ay*72~0z6*;o{=Lv(<~$%K9MDf1e1aI!8e+ZMvzxf z6I4JCNk{%kgQ$fu0#X#_@d_l_IO@en(=Wz21`u0Bn8c*{nDU4y*`3&UKAO>hV}m61 z2TdYIli^_>!9Qb#L?*4QBZwOQ!$jbK7$ynWBzQxFXas#^uU2}(1v4`P zCc&~`Qqtm}s6Z1Dwbe=_p@6xMYML(xPg{bpr1*brdN{ z0$rS-xS&MyxGKs5R}Zc#^xHeTodGHpqI|SCI-HN{47;nLN!*~{?GA7a5NGA6DR3b* zuFKR_xlXIy>vJ+hby2J|UGDGCstjqTO4m=F-JRAzvBMJB;C!)ME@w^V2p!x}tlzlV z;Up`W9zS~by(fD`XbA&R9&B%1yRyE6lj`37aOt0FT8 zyF2Tzyl{3~vcqaNZ6?R}Po}xA_Im=VJy1JG=a$$O2SX9Hb>(P&{Kh9A&#sdnh9uPer2S847%ymNB*FxCBWqqjCUH?HkkRfZa= zW;CA7Ql&4Tn1*g}y>h-l>5D$x-EFsaZte{1=Fa<{eDXg&9iM&KbYA)9mCbMd{)@$X z&mKQ{_x*Ps-M)Kly!?Y}ol8G?ZEN##clxuBp6u_P9M30=f zzR%mmxJu)s7}b$<^YX>Z>sz~hqq_HtUw`)7zqxbnyXtuTYk%_UpZ?1inxDUSa{Tn; zzy9>eEb9F9g&pbo_r7<2SE=s4{diF>9z5Dh)Xt5Kj$$iqR0q|g8pS~=zz==cFd0|41kwhb){V=TZ*qFP_wdQnXKzg{zjC);fBo#) z8wvtgQqE5v+*#oGZzrKHA}onm`??|;5dt8XA5W*F5*Jp3VFxRm=Yeh{^>UggxT!`_ zimEEW0xgMCa3$mxnIDhmO;Jyp+~qc}Z+8Zq;^6*ybn9e{z^!wV?P|PWp;$En)Y?^% zCgTjAWc74fI2z-dSnl4{g6llcNyAf6+4wl+4lyC@1PC$s6|)}5IJG-a?O8m;vKY(fVJKZLna zP@W{!IFw~XEMD%ncarZsZE&Qjj-yF3mFES7gJC1X;gOIqKrcv3bjc|LPD&F^3p_~a@C@)VhwX2is2fZ6lS=_s4O`<7(0I9v|6eSNFQ)kT+QYqScf@Yuick% zaxE8!`?GPA8iP)M&~5clZyGm?y}fBw)~SuHV{@~s0+$8WJwLfKkFZzb?rL|tJ(TTY zI>J@nY+1pp#T#uy>vT{->y!&PYn~?#Y(HQ{8*u@TuW`>8>u~tqKv0oCgf+IhNt4=| zI2`tOw|hWi)7f%5-P<1}DE`DYyS?4z0o>O0cyX||m}Cyo8}@cOylf?->HhJut{YTb z%Y3UXD57Me6lIF-Gjuf7#GNn7OH3!JQbbouBW+#8b_)Cfc|!`U9;ZTG;RZvLN&p#w zO&7}s>a}Ho@3mTe5tZrrc(IsHMku1-2BNBPLrIm*d_KwZ3b2`vVooF^05)+7A`lEf z50WQjN`9K4swG7f(E;6|h8VRx2x zAabY+LA4{*S^+P^`JCZL$W3r42Bd~uA_im|!T7*|QDBRV2}Q>mhN5s8b*iYS$+HmX zyP_iUh^TxtI$<{W?mpg*K`4i!+M6qJ;Rh*HL3mQg0? zsv=xng;ouKq*;s=gZU$rmZ28YL@kV>t4Q?xw5oCxX{J~UD8?CzDpiEy;1s1ro~85# z0J(;AO;S)Pjl)e=)hL{ak(kY~+$l$2N7>74oMHw?vZg%iPAtg#pY;5-- z^3ov1Jp@WP>3=DKmI-!oQRWgPMFG$c`5*8{O@vz$ra{FvRMQ7r053w}7eOG{+*zK9 z%Y8RP050Xs$Xf9RFc83t1lHe=h*gDx=&9DI?Cp5wuf{b09prs$uOw`-mq#&i6>q z!6v1tfH$aA53WS#fEB2L44H}|QOOHD3vd}~Uy&lyU{p|Hxf2+xi9C-*gFKxou&Oc# zx(1sVs*IR{phaf|;ulg=g&O$+t9}IA8r4C8b1a;Yd@@7`f_A_mDxUx%dtq9jnhFaZ zkeVX!yolN>7lpd0^eO>!gCvrC9j2h*h%!E$jPMnED*OlkYY3=euO;4!cLHK5 z)yJDyUaSlaI)%tM_dp~Rcd_DYOoTHE=^|n;P|{0t=qOQxi{gMn#~BC)GSE03iRgre zb2`mR(-N+>vH%Wm=m3_ezLE~FSd3V+fTt)xz(cUy$aV1+QJjEOP{oM|UPGrv9CFZ+ z`4OBRQ3I1;F&b<}&c5!0L=K0VY3V$A7bl#Zx`^O@DA& z1tmOma>Sy5VzKmaxCbe$Q{tXU2cAh!D~T`(>6cawI}XAzX?lyX3*A5fu!V8#{7v8q zW?|=FB8W>6ydz@J2;-OJrdd|35njfWh4U)zABaz^9V`@tKQbgZ1ILK4*ihkBG>rJ7 zMgS7@!xTtL1QQH9!2~!Dqn-F)^`~+8M=X;nDY`I%NHS;=(lO~CzDg%-^u|9BXUZa) z#1!$&5CM?H7@D1&iUyJ6FzGP`c%y|wumKE$>pd(|5}j%-2|9;Yz|nEECTmbLo0xG= zE0NTMPSg*!q{Tyu#X$A~e2^j0Yt;r4wDnLoCMf9-R2jq6fCt~{VcKAxK%NKYApyxm zUFZ+GKq&GJnws2?77FnI4#}f2FFcR-CBnf-lXyYPj+zLQau zBGL@wj_i;9hMbv9N5;W;=Hw(e9sTrwyzZsSI44Iy6%Qv51H1 zpA7{^1R>G%)J)PKjF2IiGcwcQ3tGqo^ocm8h(z=d4{02m1)DG0$?}BBtdvdEU@!bo zJ3JQc#5%Q*n=yr~6bCjyoUBQI@oFU*QG>s;sfpOi`m~i%7(5k)+R3Wm5-n^V#uKTM zT!!qx+DQt=K25Tki8%x2rD4$bS0*Q($x_5MwKG)`Tf`;3A)T?6K!zYu@SvR*G0`TS z(Vx|`QVo%!naF8~Inn{lObWv&Mva_;@eQle8egdrOMxErU&%Ein^nj@dJ-!qA~ulY`{4adG&`nlJ&`( zATv$FRL?$P7NQR@gz1CL%svsbpvP?Se3s`jkO^1XTbXL5N*Itx;$f4o8qtj!&`fXe z8e>@pViFxO2^+Ff85x9)Td9f$5W9#MWRO*V#xX(|eIw5z`_jZ9g-MV|!vd&cvBmNh}}<0?P@JBZMM^gaqP(KY-8osp>Ol>{wB9fyCS0)$jYf z&+^-z-}e0Kz1P3;l`p*Z(o176UwU<6HT!da`=ys&`Wb$H{?be4?`K~6TQAK`XQzLb zKK|(PeBXQFdj7zT?mzqXtmE@uXAM8`e>m>+bP8Wk}HLxwucS?n#SF1<9=SLOaw#ub)u~7CZ&E9&i*YHZ&wM;sl&C?XHRuB~9 zvoovF{_WGF8}~o@_*;(;+Vw`5y870eV=E=Ql+6?!-z{fTskK}=XmkgAH|{>Tz0)YK zrBdv0gV&wCMk{i}KgPp^hw@y!v_6D7(l!`4aEiEpt zq;h53@xp4e-R^YPHwJ_L#(J++3#>vu3n|yK1F z_}+utV6fS(LB?ESX?`(LaF8>{L0F^4`sVglA4%x8>yc-RM7fgfdOp~%_tw`p*1L_+ zv24dKb2-Qd*hIeTI$pKeYPH+lPO}<%t{-^iLaAIX=8{XXWHPq6w35hKjuUkDZ``_n z|K5WKr@P%|Ewr=o`I))J#rgTA)l_;d5sPO;wp`JPYSn6^)9W-Ufggl^xxkgnW*{fb zFO(r^7)G^5y;7-GqDm!jUF08u4S9(8sB%>*wOWq6f=`AFVjg`+Ai^Z~KIS`5N2(Img$d4a3LcdxGDxp`2fFKB}fm^RQjmWB3 zymrm6M)gLeS+BRN)n=_;3u{r(i3(1w?gmXZNU7HJsx>!kG#X&tsQIffcxZ)Abu3vgdlCRdh=wD__WF%lVR@KVA6CLz zwO$LVFtA<`&1hm@M35qItD((I)xe{bAq)&*xbOP>^KBSialHx*4YjZf3myI>!j|h5 z-2lNvn%R_&Q}vz5)0AFdS#__7Q2UN=+f}O^L3amH4J|ivnG{4lAA)&q6gXj2v}%@B zwJpnTI8MW|5g5sk@6}ko>;$&&*;dK-O048qzFP`iE3)01<5fKNcs+mtKA8Jq;5j8b z(EM|YddTx+Yf93O{4AlWiND*F3+pFWn|VxWP<>u zWP3i6@$FHCF_tI;?so&-hz$~pppc~jU1OxkP#LzFGNU$ zAHot@jwp`IyS^yx!@~fOys8&MT7l=VlJ9zagkH7k`s`j-hrWf&oJ?$$>TjuV5qXfbPTWvM}Nq7SJQKkPV(4q@#kp zGKPZf#PpgfF#8fU##H4>t?bq;2g2u2@dpc3_+f-V6{eAOT_z z76>hyMEluBO4;m?i1qARt>mGqUDt+i?EXs037ooL3fYaq5k4T|Rv1D+cmM)^07+QF z^-+L`Sj8_#z7v*#gIh)*Sr9U$duVZkh=T`KEEImw6zTHWX^#3q2Sas<0AfRnAt9WP z0v2_QZqWoroq`dUo*dvs5}-NAL0&$fxq3;9d3HtUG8G8Rb_Fa1Ha4&U7nst(pEwq{ zqK&{p7z}U9QRILT`!QlG@d{bND3%*3!{VXp*M^NeES6M4BYA_9( z0=wdtP~~ooJuBJZlBPjtYdeV#fiyTwKvdawP#n!bVXUIWyG zpAiNTzT%kWebGmPf-=-(;v+bt`cW3xVbnQF9~xjZu<`*iV+S?c5ST&!$gF9pB9O12(9oPUT zAtTemCy57~kbPozx|Ws@olyd)8Ysc`0jX*^UuUxczK=~e%&9tlRpwGGgKW?cR(kk0 zj5J~o!Ep|J>>&J-W|LHxBxle?I>`tplmi8s3YKBFz#&Nsy4`F{Ga07DqZ9HbI8n$_ zTzI{v4Qm9g@WVzNw$Ti1HTrtO1JRDKk`jOIV7ZsRRV{Pf&vrpUGn?9B4a_?64oC#M(V@kFjz4x_LR*iN}v_M+C-!O@L_t!~XNWHZ@9 z88;VHa@kxilTIb#EAiEI(Fq78d+Y5+qh4z?n=MGuX%S*});mr7Qol^+RAS-!*rl=Q zq}@5XdF%0`)6>01z0=)3y?yJ!qx+AZJ$`WeXlK3drsu{lU!IsFu!_x3oPYBxue|)q z8yCix(phwT)M$4$`mJWS+32nJkM7)mc>i>_SFv*GRB|<$N+wqq=I3YU2^I*B7GsIL z8?`rg_V;(UHwhhy4tI99x3{-8dfh0h;?%Fjm!>Yg{no2rK7W0sv2*h9@#9BNpPb%2 z-oO3u@v|q-o;~}`Pd@&wZ`|2!d#Ty6x8HvE;`sFB_}IC(UVHVGFTMP)Uw!w=OgvqT zI$OuLZk_CJZEma&HhMe9r+4ohZ?(hH+A6W*%>2UA(!%`Q;xe(|f})(YQc&ye>>u4c z**`kk-rrr{+Ul-vwyWKyRjFi)`T5DIYj3>#^1t}}do%gn$M65f@BYrWfBU1Ghd1s# z{Q7s^fAozHzy8VhzWw3-?P@7@>C2!0+!tQGG%-1O?b5k7-hAzqSKoZ+YAlmwfx#)@ zKfSxZ-LK;zgdGqc5W8pMb2C#DlQVM*%Pa9jrcf#s@~|>bkkB3M?jGDYKDl{#d}Dia zyHl+N1;;IT&RU_6O2-#wrzftRfBAF2^snAZH1B`!kN@+3`=9^)PygWA@r~2_4{sms z9-KaS_UztnE6mNj^DqDY-}$@$=ySh*{@UEqN<6kOb^Y3Oq7)FmAKrcP(Qp372M-Th zUOqN6y|7w}2zlLna%p;E;`;dXLVS&6L7gbF->rvE2}RW&>>M25JUKqxUT@W0*UBVQ zEAiY~D!Z1)JU4@~m5%3A*Dt*J<@aKhnFdh)dop5EK3sY2*3oV)ms`E`%0K^mKl@L9 z`StfMoF7{#_{2K_aa^v|eGum%@%5vqQS3*J;);>Dg2wl_kSa zskN2==(WfI*iJFEmRic%sXPX!)2nrQ5z(!ePtK2xo%_PCzH!;T_d7rMvp@UuAN|+A z`1c>&=(hVEe|7BQqCGh1*GuWb+SHrB_z!;m^Y2bhPE0PlyZ0a6y?K1|*2#95jwedh zb{#WlS?Sqpm(QQSeEHpXFHgp-MsN4l!)G5pJlWsg+8T86k_o|ciRIYpnqv5((o=)= zZoAd2dc_RNu9VNh`bKv{nyfRZmHlk2l$pBtg@5|FmuJ>b9)I%vKlxw(=YRj`x1K$D z^mxBG@$wfgEN3zGe)o9Koqyw(zxdTl7hZe$*UvA7n+G=^eDvKN9Ict&Pp?et&DD zg#|6+pIAkVPuS@8x@fe1%PXf=GwDoW_N^~{{#VY$!mXR{fASxG^cR2rhrj*t_rCY7 z2kqF{yI1C8iFC%TcbbKT%jeEtxq9LCmtTJE;*!2LrX+{dDx{ZYXXX=m40(nmz$lW6k)qj#|~h` zWW%tiHCcs-j^rbgnedYDBRCDjqcRu;IKlss)e(=YC^!<h#=lGPAad>hQ@; z%}g#VOuqM(SHAd_OVcYEmcH}!+2i}SPmi}d{@TLyXYjb5Ky zEmr$`$G7f1xPSNb#^L_Xpj*SwFyR@#BB?VAaw^+_3s6Qf$p(>Ff*HiQ5s4=x6bz6hs<$>z9z1z`|K6h~4{vW*3#bBeYxz>pXf^N^aiwyxsVkSRj89C?#!@98 z??jdwkBYsy5{oC-m;|zr*Xy)!y=1%=7h}n6$*nZoy^Vf8`Jc(LKgsQ7vZTifB#WU8mq-AyO47O|WRvXO@B+rOBp*pS!KoBq zM6!<@YfXrQ2;^nB@_M#%ICzivedOctYtlK%JxUe_tI_Io+MRYyk3qH3XtuEf8ymfi z%|W+8G+qQUa(1~~F1xmt%kzV)lUQ9%q}SH6MG}0>tr!lra>oKZDXmC03!osK+*KU3 zDxhg{<*-=+V$|>US*g-!BSkeq%!DL{xz^VKxrfal zp=gy!ngVOyFda8#ExEcHkK^Z&aL1xC5gsuV0U3OlqVizyU0fd06}lm+P1Iv=12SNY z+6Go59DpB|&C{YN~xX`{T<`c#fl8Ks%9VPr+zE22=;7Jh5g~Z&%_;iBfyUu!l7uWpG z>Ai=y_BX;@J{11du-21no#PB|;p)Y)tCIv)Y<)K%k{qg+G7hZAw!w0unXE!4xth*! z&X&%UNy!s_BguS3oeey>!S=!7-k{r(g(ne%S&(=Tts|7EWT;5?5Df-Z5dg)Gf{Q)p)vu3vcX(v?f+ zFODx}{YI-+2}Kl4PrylLk29bKhal;N@#~W_vvc!HDVuW_B6TuP98ht1*y#3mPVU}0 z+3O>zH8P_?9dcj^I0fNAa}7?AebA%M2~j+`927;38b1aJDv_k8S4VHzvzM+- ztZba@o<6+&{U*>OZbUZtI~W-sO1af4fquK) zt~)wMk}dITEl$#q90Z>eBdo3x9_VV`aoAKSu+=~UX>mArt*>v^t%Y-A3+atJ$H%vC z-hTg^@88(n+w3vi@P!h@DvAM|R-i(QsCLYiI@TmfSF zT5t|sCZR_Ef)6Q4)B~t+D(0XGY&(Jot#+?rFTD0|#^!5JpWMB>)xZDIH*RmWeS&v1 z%KFXI8{PEyg=-7Tixcm>eL3dVA~zF9pjQ`{vf<91)4l%2_QAo16`RB3B5#3N$Cr&Q z;F2X0>6{BA`c0BS#9IwC5iZH*=H@z4WsaWcKqZqfSDb#K98h?5{90rgiz|yx4hB&I z0U2<1GTA)N5^xk+q~6xXU;pCUbJeH!_fKlp{{5$qcberwx8EnZaO>91etv2E@^UU6 zpM2+?*^D6avU}FoCvlqU4VKUbtvh~ZA{^1dlQw)0t_a59mJ=z~MD^9+I&BX;? zGMFIfRp$IN%lSmFKNz|)M98Q;Vuxm{5pfDH&knnYSH}^l{4D4f z)^Q8F7(RRe886wuRkL;%xVRLVwYAL1^_DARd<66^;;O{5 zq*(p2aHa`8&eX=MZbDToBj$z+U)}uIVzQ7jjq>J(6UZhSD~|XJ8`cL4&2Rh?Gr1Hl zmj=+w+@t*kgmL)bs1CE`eaZVB8Va0l1Dces@xg=vW95S?)yPXTqb4-8gk);dv@(a1 z6U{3_DkAY`?y&A7+c{cJ+%b-nIVzVS6*mR0`5cmG~(FXv&YdMhiVB9c!3bz>q0*$tZq$F`=2vkb5|>p^&^3p@y%5vjSGaX?W3M2sw1M zj37Z27>}pTBCG-mW>K-hECtWRHggSGLl<3V@C-3im^t)LwT9u6)S);nf{$n=2I%5b zQ|bXA%q5P%Q^G0UKxN6QUYH5Y+=gHxk`V*Lm(i0%g0sC^r&B3&l2Nfa7LL3U8d2G2 z`8rakVu%rM_SF!cA%)d8VxXyjFl)=I8v+xB5xo)Zv!P6+KST~(g%{flftLD0XAGK zW+N@aO00zvz|H0qTIiH*lIq%HI>qA;H6ozoIhTTZlUf%JR6r@h%3{Krz0J*D1K+Vq z#;Vsg*>IIS5O(1`<93$G8!aa?MZ;>ta7FCkRwF~*rFx1a9_}qJsIeIElfk6Z<^Q3O zr1<3Vaa2ekAYTPCn&?{6Ak$(>ui%jOyxF<%N6HToP#A!^($>HcGI5#2BDXj(oASE@ zT#IaxgCfo!o4p=BZ)fj#r{m^I(Z=E77KtUOK{QPql1;@EBowpp+41rDR2ixeZ$TK& zRhu~Wy^Wp2!yWQuL1Tlgc?X9J2b2g>=46eeMlrpzv_f_ia8Rf0B6NPAOxgO@-ri=X zQI+G0AaYJZtVxwrHl0+$h_!LmF-{CKl^qBU`Bc1VG))s<7Zp-UOkcjT>~uCePCAz2 z#LX*`qb38nJ~+5_+>Z*S#{Rwghi$i5#NSEB$!D*|sfSuwU75drX>2NKS0XE$&e@nX zQlf+1gF_PAH^?0ZQG5I7aI20}M^Kp}gS~Yp#xZpi9e)~60kN0jOxG}~pbzTT_bNzye7vr|)~rwBV&Q~6RUhXITR zM>lUC9~^-F;qmF6d$$e-gcqHNv|u^DUWNPc!%<~Y03%XWZY*t*t#}3zp_>)`oPiYZ}08z?;YH{ z|K#bTJIA{l?V8D=_~c)g7nhdtw3nzI92p)?JsZ8jCUG&Yg&ZY5DRuNnQIi(S<;qq* zmyE3}uMk!eCZnSYlp;lT$;uUS_>BJ6(e2aCPPMwRwY}5d+&eru-0YHWXoUG>BIB@G zk_*$*vy@v-&#w^Y;GPjv;BmRt-pt z@1$BbIg%j5x3zzGu(!3|CX}Q~iSmS!DFY>w!-m19mWm%bhUBW6Ix=mL0VH%F>1Dz& z9LaP>$u*PsLt`moCgWzw1II*Q|3oYt(az4%ja?-T`kOmDyN5@|M|(S4n-VB?HuX?e zCN@7caqa5W>(iVp5|NXDP^9Emx?2aw^xg!kp|2)il?)_>pL6qQc=BaX7%hp1-0bx? zx6uZByITXoUtzhSeJS5EuRJ(;G(gP|c)_oFtE;mFh|GUEP$Wj=Wa5pJR?+!sGJ!r+ z1t|TY5W1aXd1aW$gJMUyB?QlB5+v>OZneWc)(*iW=NQNxrJLkH$g?^mQwW3zYo(hrk|Poqz5w6o z4>q?pr2*D;s~iV;E)_DO{)euR&YuXARj_896=SB1J1!T8Fb`MBxHFhALRiu%`jx?S zie;xjwSlQh;)GqHA%wB9;$S2#Sc2gb9dE`X7gV=EaUU{qDgq{bT%apmAjTnr{; zU=&y253q`%sELg56NVuY?Zy*U+F2MfN=23z#=@PorNY0LpEeN;_ zynu{Q$Twzf7J@*dxi$X?S%VxpQ!cUL9;_B+2>_8Ib+a7RUiRZ3lT!$bnvlmTgg5Mz1n@Tx@uNh6>LD z4vrWCfSG}r216GmOf8C@cnxHx_{h}FshU*G&~Bo1i;_5UAwrhwG|yI17;_3-8;)uOO$DP_pD!VJlrU4`2XxBeFd;oOF#}OzPyk%fTy)ZOrp{6K zKqv+Zc0PHgqxn?n73zf7kcp+ZqfQX+F`R{{K95uvDAIFmDJ&&zRE+Tem?95!!xpLB zk(DJ3l0R_^jJOpf;*IFRgQ~*VcfhH_Fq;wCs#aH1e zQ5V3hOpU0R!dlchvL^MZOfH$gy`j76s<~l^!Cp`a3gK-eLPQuciMa?kF@Ds~jXJBS zSzyu0Ai?=A-GK@uRL(piA8&;eYicBu4`p+-4tR)aNU_Kb-ex%la8bX9Y_bBAu@shG z#k-V2!dbepp;Rc!oRSm74RVY8Lx=$uu!M{_#nPfO!}JIlRO`rFpip5bW$To3iyGQT zMm|QnQ-zJxq)UPjCgG>0Oc^OmH3*3hk~7GHEn>Ksj zcmzpYSz>jMf=P-PsohjXA{$L#a-qJ3%1hiDqm4uy2txrkrJDez?ZnjVF-9{8E|P^2 zZlwFQ_nklHHxZQ?+vEz~3w@LYBOYN1e<;#9sxfY3n@>3`6}a#$2pA}k>c0B`7| z?`%*yFKO4FgVL&+2V|C)9yD5#bcA1|QWT(xh)D)PLKfoSrs|a=0B{SHuuLkZO5G?~ zVW~8sD!BO}0j36|p05BPFK8f=P8Gfq0Rv4{EfR&CV5e(Qz7iIpaUFLBMOX`ps3(am zGKDZA@RD!wty~TH1`VvU+(y}|v;XoCUw$Og%(Ql>5^Gh+8i9l0SWceswUPBgT_!-GKFxPf?=s7 zRX#WQf(jMi7GHJ2)H08Z51d0AFjGTLNtN%xPz$eb!{G3F)P8cF33(}*msLQ|)e%0_ zG;2zzpgOItU*ii9RF@+fuvz9onhMq#2J$T>Mx$mp_~iQ`kOCB-5(VJ2RjP{Oge`T@ z2q@bI$T+;!))21=;1L5U8tQAw_(?4|4quB<%{``Y! z^a@KB-~OnPj?n7hp&cj|iWz(ZhC*s?ftLzn3W{Y1Xv64PrJ8HiZJY9LIXLVQDxRSe ziU6qCB20wRP|Imm0sSoa!xt6U?$||(!Z2uzeAmnth3E*hUa^ksjq;$dkAZyD+#&+O z7G%%yQuf*Bn1?}}Mj|YdVJLzZVnJzGCrb`L(GJ>fd^G~F*gPZ~`3e(>S9m0QX!>ZE zqoJTQh|4BO*0r!E)uxnl%Upnmu0%Y5jS(m;kq>@jl9*fS!ss{PW=5UZ>+3}12dIK# zIz$ed8=grXjD9jEhmrc)m^7!>XQP82>*6_}Ka~4r`!m1124G`(nQrnz%!%pH6X*dk zGA%fa7XaqiLnF-y6>s@ufJ(9@`uYH1b^tU)LM5qO%w*(<#qSb*qotV@-K?byI6$c(nhY`mk)gfDFkHG` zQ}R}96rFkIjamRfHd9b=X>v`XUv7=E6@uL9L!{7p=xZr8>=Dfj2EpO_jMhAK61rlG z=0fgR5STT!SyrE#R1Y)@Z9ZuOG_~mi&{$3s(Df%AetKmnpdPyA8r^7eT@XkAMid(oR4bU#dNaI+BQ;jfXOox-N0XXI!m6t7aHZBH9^jhZC5&bvwGu^Nkf7W3*)7g_*Q5}Zs55XReG9697Ve9DA zfI)Ar;V5&*hekc0R`>cfy>)wbjV2tnpEVAA|y4_fGGCK|qeakyz7zEiUXjC!8UWg17X)HdukTt$z=HZy2AdNfci z=AF8#-&s%dJZ${*_Qkuij%GYV^=@?kX}6zf`1F;!KY#k{of-9M({SD)pk{=CHkaN! z@Av%f?44FHZ-)(k>E{RvGtH>O8C-u!f3u#Z)Vz9cZeL)#8FTiAm&0o`)2B~ogP((Z z*7L>x!?|8;(Y-Hd7sr|my}H|+Q0t$+3N-*DV#$@x>3F*JDL z-5E_@Xwt1HW0-dKel+e3w4sQc~^~pewm5SKx6pXt!aPpWAx%|g%@A_^g*BL z_5b_n`y(><^91?-{K{zUQPqBk8aci2no?|37W`zv_VR>;M1& literal 0 HcmV?d00001 diff --git a/action/sound/user/cdtrust.wav b/action/sound/user/cdtrust.wav new file mode 100644 index 0000000000000000000000000000000000000000..7f5a3e1bf2729f0353a0c4d1cde32d7a032f3daf GIT binary patch literal 34184 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 PzWi@*|E&i8rUw2uj@qIs literal 0 HcmV?d00001 diff --git a/action/sound/user/childic.wav b/action/sound/user/childic.wav new file mode 100644 index 0000000000000000000000000000000000000000..915927d716c5b170f618a7a1731d96372c877d04 GIT binary patch literal 50094 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/clint.wav b/action/sound/user/clint.wav new file mode 100644 index 0000000000000000000000000000000000000000..25ce5c6e6c612d7d21fb8fd983ab1e168b39dfb8 GIT binary patch literal 32300 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 z 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/comeback.wav b/action/sound/user/comeback.wav new file mode 100644 index 0000000000000000000000000000000000000000..95a36b6e1765856c80f9aa4e81467830974906a8 GIT binary patch literal 24078 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 Y{-~<$!bffY_WO|ZQQPl-7YdsG3pRxrGynhq literal 0 HcmV?d00001 diff --git a/action/sound/user/grimley.wav b/action/sound/user/grimley.wav new file mode 100644 index 0000000000000000000000000000000000000000..272e077d2abf975947724abf688067fee69d32e2 GIT binary patch literal 18784 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


      ;P z1USjm_%UAyjD)#Nzkg#D5j;6|z z&u>?KtI-m8f2MJ6Xb;9Mwzsf_7J@pdCKzHvW-Ce{l^b5yZWS9}f7?Y&NGHa3>DraJ z+E9zCo_KLPTiFS9embbC4~y`!FASGcN(_UpUdXFvy<9E}c8d};JL)NNG!1M+wIiEm z;4TWaVnb`NlEmvhh;DyuNf_EuL_;jtKQ|IIBJ|>|N{x(dWyrRM#aF{GwQVPm!Vy!y zywsgYwXcU91_O4=OPVS%l;TKrxa0`Y1kVr^g;QLW6NqmnNiLSkZ~y`*g5k)bEb{o` zctx+*`nT1!`BGT#-?h}RUJcZ|qP#9ll~pDb#gWVQK1Kc_v~Hes#viC+t5#@0n96lQ zX;j5tSG5tnjY7@nnu^w98!eG#8v-1emgYDn;t7PwhN2sARm29x_)+Fn?@N8tiUc#D zYF&*s<(=BRo2|JpvSudAVF>N4O0h#?$oO_X%U)>|rs9EunrEphf01X+k9 z`;)@j;CFG`8i<-p(RQQeDX)rG-Y(TO0AVS^x!b=9wCTMknZBcN9E=-HS71+p)V4fG zeb9qzg&+;WZfS@CL@ZECgy#sYk1s?hf~aW%U1<#b-Sy!oI%T+`)w_4f_{ur6;qG{DZ}cZAiL{MxT0)3VYg*Z#?i3n3~mOp8bsK>yrN>& z7*=R8Wx=sqqS}o-9T8#F)vhWL0G~HFmd3o@w@Nd2|yF?jmjR*%REr zH03G>42>6Rf+aATr8%Cg^g;x?@O1T-X~P?D2$q37n563hC#vw?g_7{;ehV5_cxg{L zC8(z4M!v-**2ezpT~)Pb1~d3F9o0C)^NO}56DzT!^$ z4m@+yAQ8FLY5;OkB$mbI5GdvTbLHiH>Ao}{L?+@^YvOKAOeEadYC`o|gRx+Ly&rtK z*9Xf-wlRuqwXiE$g;cGO)y!PZ3M}7Md!NR3f9UkDjG7hzxX4!xF;^8>R+m`K=f$7D zh-!6 z^#b3YT#vhcKX7fC$3oE6njo_!AV)wv1V(=Nok~oB=ZSw9rKp9pL?+pIk+5#o5=vim zg1YhTA)GHvVrVySsuW2Uh$L$LUNe+yTKTl#>v-DA=0_Mo-SIxECSeZhNk3?0G(LoaAYW$ zgKIQ6*)khtT4|M$7%pUzspL+Ms$>f+B%c#4R|9$h<;RPvCL`|)pvUpTVc+uxLu86w z8#>M$jDy~I3Wt3*zqtlH2P`p^%5o?si*kc)P&w9YRde~OP+IvunOb}CUE;;o&f3a$ zk!5Mthz7lAe0g`fm@KZZXT!m;AN4)ki-5qmaF?C#AnJ}LVSh377uUYKn7NQGLkGkn z7KW%@>RHmSP6|@nwv#-90?s-QCQm3*cV; zus0qCel#3JQ9tVS$C1|^46seRo(^jbo4{16G*tjTRV@K3ESJiqOo6JUlZ9d~ol0f0 z*=#;vr0M{eR1+{!I2eu*x6N-p-Cy54K7GPqkN0<;)DJwQ(Ghx~YmTiJ zij`72ozEpx*$n9{m(LeUCHPu3s5o>ATL)_+LZe~VLuSwx;TB@p2u}QXG@LGG6U0;V z#bg+V!#zKUx|ZeHaTF}YXdog=1_2my^9Ws$qo-*K7ma6?!?*u9&yb$2L5R3{43{WM4uV9h{@`-Ch2m%DW2n~U*;KC0STf~T!f}|jJ z2uvx#a}aqF;?X>YfnvxGN(r3+LW?0fg2CVss9z0?A-%=`35Z8C(gl2g7;VDH0C)(v zL$D{(Cz_!ZECB&*2!SzvLl_16;=c%RL@HXyw`VK~(?mnI*uq|d1x%X$!F>6ToF;C-($Fs#J5X!h~0!v~zLNDnxrU}jB!DH?{xBjzdGAy}E zm`tvC!sH%l3I~R8@~@_7NY)dA5l=dn4m5in72io~LK2uAqwsi&Oolx0=ad2Z5ohOl zIWX{Z-()Ev^3OyQ+z|J%8%axo1`?3YCbd#zU{WOo+&zXA(K;TT%$&@bjEu(dP9f+o z8vofGrA@dg@Z@R$lf=9r&d?)Xd?DkE-a#025xGxra;P;z9+9({pG3L{_X$D?z}@S1 zyQrciX{C;nVE` zMOGMQZh+q=D*j>iL^SR*WqI~YaL1Yp}_2wm)`JXNigbLlLQ zQFQC}fJLHTbUTi^a8`3WYkzf{c=38GPd8A%Gn@g9yuG=)T1;VBL3Yp|4T5M2QpqGx zC&i;6sgf(z8+A_79Qam9t=-*Tj)%j@5|Ob={SVae)n+nVVp^u_`OySy8qVMqjE8=A zd^7cf%TJ4FFd6j%S8brkjwu!xfk!&cbfeK|dU#aOc;d7Cqecded1yis0{vidxSVC52;U%?#j=Oc%HJ*TcbV;v%Xl zXLnxxHUC#zyV**;Wd~uvARiba4>efyB=h#MXGGIs z4~P{}iuD{KX0}E;_5F}6@3hzL^=%mAN;b8%va+`N^403=-9ousmk>?^dj>Oy!vO)G zUBj7O*^S`w%Ijho2BCpHt4ZZ_Rh8gKx8O+HPBdLiBR3cZ3dd08-1f^CtFKqE@VmJ} zkrE9ckC+RAe+T0k94KEmr*oAbeV&-XWDL*EM3kv1rJaf*l**il2*-ANlgmZq_NG0Q z=P=d$&gzSoE3bg=Zsc<5V#9EPFdX09UR?q&yj=9LLH(;)gBpDsDc%Un5g=&QZ7IFQ z*=(kS;Mp@d5#^@@LNXSbiSJt++UVWcjJjv7G*BG7a6qWgq3K z$na)&Qpt@q?Ejr&9$=cHBPTGtT8tNWm&55af-(s_Rg6A$OWLpZh`vXb;~^5JXrc0I zOBAzdxN$%x)DWrx_vSMb&J4%ZcDFLw-JLC9gLJ+mAff4WC&cq9C*WHEyprxMK$)Uay8S`3$sq!Yt z`G?cH>nZ$Ep!$9nNdnUpB}*#lH}+2B%N5NJLKtkGWwK?lnAnuLoeWHX<6Hi8GP{|( z{?%A!TM|;uySv$33aREIuZRt!5A89%ytyGA!~gV9l&g~5ajR7?iSr3lo_y_rye_Ib zd8$xgYO9HSqmnKmrU`w&KfS!aiQLf~cr%>p{07!7n@*)m611{8g!eq1&TnsLgI*7! z4|}4(FNYq{WU)1T=+}*}KU?Zx0JF>&vzbCImH4hE7SpH_3P(MEFu%SY+h)|0tEFN- zv%3xjlPZ)7sOvH8XpC@ZFqi=K_Z_E;^o9n@U>PnvXc&hfqL@#1&HuSyYj|2iptFVI zZo0Ca*lBU;QcJgj0J)6uU=*lE=+K2Kg%aBxBt3}r5p1AgE}|s(&HX`uieX$Bq=y(L z8b7NxR6WRQZzLC_X^&$ynQOHW(jfxNzS^McxhxIr4tWI!qyTIPLY1lW^=hS*Ltcfd z)#@U=XW)sj2N589c>cYp2QlpS$9)vpB9TVY?k->vIa=s>Y0+n@a*Jh|LME5Zm$nnD zHEL@uQ{@1ai7xbPfPFD@5G5Ii(Q9=UK><}moq=XSYKOhSVm{~%M}61~VsRrtRjAn@ zWz+c58rp0?SEGeW0T!%N+3k&$7wgc$FOX%~Dsmc%pvO0#o}R9!w_k1pWY86XRCbgL z`BDXS45|*hIT??p2riMVn;@y?ggqM(8|*a}29{oL7?&f89?ylEs+9Ak^!n;|iP!6i z#7-r-OUts2Ox*nT{_~gX(It?3q-SWloJ1IyEmTl%g*q5j zJ4H8IfYQ1HbP(#AYt=L9LVkDs_112tNKuU9K^{j?Ve`oXluuw7hgfb{D4;7yg<1OK zvCmnbuGN_1?zkjur!lI&c*Nqm7$sCG!I-f=% zb{C1fv93|SHynD-{U1Gkw7hMZvk;yvjWVK4_Eln|UQ6Z#&GLI;Z-UfOpaL^5Z6{Nk zuVCI^ZEbI-E1YT=$lYN*VTHeDD!*YSs=LEm>~f84dSum1{_Xbfy!$S2e3 zZ1Vd=nyaMBG64F1H$>#$w^3ClWp=V@;2kf(msC1eL502Mj%FlkM`jf@W1bUuhS?i} z@zDrQCARPU*K0}r`HwEZK~oaxOfr+sr@l`VTeW0GgAnyye{wnLYET1>^mZn@v$4Lq zwo^c*4x=G>cL4Sbf^LL_uYrRGv1u8S5obVvu)l)g!=k}2p3G{CK+%g) zu%XZn@1_iM^HndXki**KAePx=>&6jBufQDDwUoj0filVhP0{%l@EMk|u*j z-RP=N5x`843foN5wd8i8j$Tk~O^`O|=DvjrGeg9O(9zJEb)>_1wd)y1IDqJkPz?dZ zZ7?E52!Py4(iWgXOSJnIEBA*6ARWY9h%!pWQfB>irkvTyU~8h9!tg>&)*tj;gcrz$ zvot`WY6&<#>Y?BZ_^_dHw2=;;AbSfog)lQ%ZiHzh(9r~AJZ{zftDe}hQO3_#GU;qS z{rbDjQek_i%r!^}U3V~_^`Q&agn^}LcU9F|ll%xWX^rFce)dc0P`m2W3b6lJBIYRt*YH_qbmDq1?x*yD~Q*! zxl(3hmtl*AItrpt_0oeC8TTRkzN(tK1a}yrAVbj{P7-M*Vm8>W;ds=8r%7Nwk}@Fq z43SuPfvVMVxtJ@^(4nl} zphQHeu(AW}*GVrNi~$d;nuziXhDAM&&_a2mgG~+v*@OQQN7f|j*BnbTJVd0JkZXgt zGT^srRo2Q$))I;p)NU|LrC2J{^-`*$Ds|3q5b2Qs3L2VJdUzmKh7AX13K48bCZq|j zOo$*N1oj7P6gX9k6r#2fFk?Il>g@r_)r-6M>Slp)f9FX7^mg6tNcFvt(7zq4RR33|{HsgmgM zsiI-xOpZ{;zO0}ql%;Y-QsryFBu?kE#m)WQVvJ}UQ6}o2s3JgMWTmh`4&Y;d2<7O7 zA+{(A5|K-S_}~I2NG3>Xyjn5}5^;1UD|F>zMGyeTR`S_=mCo-JH3O-Q0pi`u>zkX$ z$4kVWW2gX$r>jNiuzC$%6*i~SMF>soXcXAMGf1TuiZ^jm3OPiAcMx<8E}-ILn+Smfh9DYP+&q51n;_2as{-5TC^#Jio4Cg6g3$QBz!mxEvRNO_@*R*6#c9ddC)@iUCNwaJ=l}s1Q z*;G}QIRP9&Dq%syc>?v@M-evikl4gP)fuJ^2g5P|x)3)9Y9AOBMnZ)qDFr5qjTGn+ zbwdC(xc_E7N24sMP@@1uA#;(2#z|&4MdUSFQouNtPGn5e;}Sn;8?tQy)rpK~%f+IsQH2UsDduyT_-_IHEMf?-bxc&MVyEILBb)&66&V8p ztSfxO(CKnRWs7BoDnd^~5OVoUs>Dk?pcvHKU0vPW6S!(PiY%CFjv@yrQAy455;@e^ zg`tO2NmN^m(nEtH1xgB}9S&KwNjy6%w%%#Gf*W5I-C$f zeS<3MhdabBQDBNZpbX@Qie(CgjmYYEyD%7tX$Db%eWC;XB`IlO!%)?T*kD~rK7t^g zh~*nX1xN;m*dsphS;`eCo~ckW8e*MMI)}8<)$Kfjd9VScL4($i?n1So0Q3VsOa#M& zJXU~mNW=p&Qg=v)qIMLsfe(&Jq2eE^ibr%;E>^jEA;-vKt%d`eJkLQFiz3GWcri4) zhh!3>*TvcBoEUra02W^t5pR)mwJ;Sl|Ac37G)<7Xe5e-NTMg&ZYl57H-LnZ-NBLzj%42Ssuw}efEg8}Rf1<*QL0JNpc zWt8?yl1>Wy$0XZ0AAonL>VS7eS4t%?6p*6oz)6e8B$+4;+k_OUqB0#-1quu;5k(lM zcq%xhOUlR5ia_D2;dT+M0pcRy9O_UhY$G^sT#KA02m#O{Hbit7p_WO-vR5mpZpGPN z4%OF?^VmewpobxW1xETUHcteF|Ff8w;P%+` zWZjS_gY!#reUJq3AF(sIV34sP)mQ?+?pQn0DWFLe{X%U(oDk9=nsfoP6BHeFDR4A7 z5RNPkNh)9$#JmKyL~wnA|6|Ss{D(J4E+o3ZTyh=&06xBv-$~AcBr0GFk)g(FKc`pF zg^2qtfaru<0g|rR9D_cQ13dGbql7SHe5|QNdV*>uc?tM%=#BJAJ|V-Bna3F((2c|v z$inb{W;|eEN%kjBwh*BPC(sGu5m`A>P>-_&c!cafxEvmq)sh%6Dgk0DXk|J)n+L=r$?F=8|* zBT`t6Jxh`_*sx@iAk{%~vyd_}ZIZ`Y?-1=;aW!mH=spJ{79 zQ%Ro3MQ9;he!j>bpyN;SF@8J;l`!G?aq=CVk*?$3o@v4yNa~FA0XqI189(Cw|LQt! z7|V|=H~-BTJWmG2sO0*q+qfxd{Xfs?c~I>?(SrMgBwWwEkQQ-Qq$fh+^SwW5Agm`J zka1!OB9A;j@t;VDw{$FIF-`IPxK-Tje=_0MlH4dlUR8t}_=d%~~hU*j2)S(0DK z&G_j*DSv*RP!`|%_q}*&pBX{!5s8YsK$m|_{<)*)nLIOs^!dz_XAb>282Le(Jzp^y zgikT62)Trwzb^8}^HGOhT}KO6tm#lJgy?v2d#`69CL%$fM__P=}4+VLt7I{u^p z1U%1*bo!sj0A-pU4@78blX?TfTp}$*VDK4{ZNjiv#z}Y2W4FnM`SXwXBhv446HJ2i zNHm11kZwrVDo6qeq~qtu2!;3++aKL4?KZR;X+*jq4KTC#N%YbtR-)aGRS~&|mSmam z9ND|13I1cCxQv5LLBX@n^c&BV5dOz122Y}GOw;pJNo4{aH9>}9fO*(8b_ltGJvdK^pC+R#Mg zAvD7%WP-%pYS88Fc9U;v+6mtTm@KzRg$_uvOwy3}#>7fNSDGkD#DM@8WwM}112%Ki zRNJsZ+;Q7(i$XKD&p2cQ@WWPRvjgNEM>*ij>$cHs<1BiJa7_CYh&>isDjL?)V)Q$1NS)z{C)8sv=$nAi)vjKr1{4@WfP`Z4;E@BoB&d zHKzl9AmZ{p`B*`eCn-QsPV|l;2yMLyH>HUXAIqbhs9;LR(?oo8EYg9Yx}JL6_AIzp za?`}iM{E^^X~xNk*5q~jjxILcjsz@* z95({BvgxQNnkltArl9!7vFzigtK;ks1P(Hy8Wv_{T5V*hq_)#R&vILrB%{-$CEw+b z%~P4zQBaCCf?Q$rW)lO0IkrN$&~6gp(`~a2k1X~LAtLeKX|}Bnka0&pAxHUetP)R3 zqzb8t(NF+ItSO8YUQ}V9qIeYL#Sqix30Xe@^y37pZF0yRYBIJ=8&Rp}NPOGxfH^7- z!l1@Z0SnKZ6PyADe1YX|Vj`*_A*wvKY>8`*;r-rL> zCvK-Hv^xkEnp($c;`J{lis!Wj_>XW$Wh_D4vw>dV@C>B$1gITWN8l}AWlpdM@Dc~n zP)|%xp#CG~0qW>mA61jG|-L zXahVoPAwH1LI;~Yg=^Yov+e4bP-q-Wwjt@RcHDFw;Yjl>?$`i_4c}qu_IU@`2bL@f zMY`EBW!<+3 z&A{b(*OO(WZ%kL=z<_pBP;3JMhM`EtX-AgYrqPrg9QD>tbsmBbRo%q9UXIx&Q5qt9 z(zZSOgfjw5(#`_;SaxsIj}tAqUoB}fDg8VZ7}Hcy~VB%$p(5=!^MF6BhlJl_`B zwvRM0*o)Duwje-UWW$sdATA{O2EIkg>!DDUri`fx65MD5MgL&1ZHt`VX{*@H`l;On z&45#&QlOj2m=Ic$?`vCaVA(28pg|u&`)TcFv)vRG{0XTo(uCL(EHso6&<`BsSDOl8H+)VGdt)mQ*OPd2 zZ3J?l2ZO`ehOgfw2XJEzMm)kc=?#87G;jPNi8wGQF;1imi5LU;wQ)5f{vtaY9}%67 z=i;x}++f;-P@=+#7KaTb8w>Oi`$9g&Oh7MbMpQhWB36qSYcfRani8X9kR}+Gw8fJo z=0FJ|p@^6=(idz1mIOD6af_*nKOvuyanT1++p+J8o=N{?C}Q-`2Ax0mf?s0ej~f^U zmhhP{@)IACK}c`paq^AWRQv>8r0NkK^mCujw?QBn9D4)Ly^x1-flSg1%o-sZ?Fd!G zLx=~AA0WLGnIRw&XvGl31t5$g9L8^C4ag_>LN~E(f9{!#ME-nkfG6UKpg&@sF)jus zt_$(=VU|e@fc`Ny*@nb0ljljVkOI=r^M9B;1)@hrz-WY-#8Q)0dLAZr9msTI(#Rr` z#lxC`+~;^WUU`s)E@Fa+>=6zVLgV?7o}Wj1z7-Ee!~h&6!w?=2%J4fri=BHcC)rQX z0Ei3zqAPv^QvrX-E+^p{JP-^kpYX>5f*6p5_P`2po46b?1u=MGsL!DjT0-6+K~3od zu@Bl{uE2FkSC})Q0F>CL!5JcGRgzAQ4=Xq(2^XV7azQvlCG$wC;avzFoW>js&O0(o zQHKmC!mIGl`)3`MKRhT^aIzB?#XmzN?m~GhCp^y`H=TDs{_^<4$kFgtmg45>=3DVJ zx|p5^(YbH1#l8Fi(rl=ko-M}bws7z^_4a_~q&BHn3v_`l6_C=Rp})E(T^_9JNof{aj!tL$Et-X zKu;Z3K`u>0yP6&E6p?BN<$q-FAu;N@r{UF?yZ2K^YKuqwK{CHrv3$I^%5+Yl_?QEV z5<56RJ3^|#vxEb@YmJ8OvY=a+f3z98O>)`v-2}oJ)x32J1H7ROV7PdM?I#pZ{F-= z=o9mN`u_dJ;LK@D$HxF_&)>bf7@b9ZAC`?dIG+FZ{%oq|g^qgkb~m|m5KhBW3r>?w zj!u?1&A)huH!MIN#3STv(Om~F5_TC!9TZE4Fk&d53(wC_I~~FkqjMU*8&BWA!&%Z3 z883{$QCEGw{Kw$pthhrR7uNrtFIK$k_vd(l381;^UN+CV@8-Aj5dMi!E$yZ97;@Af zj4aiZ52=H_{X>{ke}u!gK59Q8wX${UM}xETGce5c?USa{fltXZ?k`VBN0v5Avh;TI zZ6Pb154e>0ZbJCQLwE%k%J|3*V z3F3p(2*otOdX0_?j2Uwe2WKXM$D}4IR=DG8vT*+8;zT{!NEF)Cip)?7r(wR&X=HQ^bF1y>_2n_ zY7GLMtPxq^QJSr2sl)3*zL9iwPpIQaIEN9VngL5?;N}P9coSk}&&J~b6^{-qj_Ta~etUjy9_05c`Rz9aIK)8|v`?DQ zriDWBfK}Vgb{nq=;F?GK`+KOUIf6#xWAGgAB)mYgGng-~ez^Jp7<_mZ`seTZqYGEn zFP6)XgHybgNoiMhQ>9}w7>#T=uT3C@d)Y(SKD^HlnGxpjAoB**g8TcW;@iDLfR6$% zKxYFgM<#wey||dp-Y>4+&)&Z~^R1@!<8t|QHMZd!A16@~sdcc_t{wPo?vUEA@`xBw z45^V!d1ViotKIb5!rRPVMrjy6ziroXp?%P#|_bh&=!OB2-QU7I7=Q^v-z{Si*kk_~bC3 z+D*MJQ3u&lR0D%u-oAoNDc$HgF`|-w!HoO z@?ZTkY_?N(|GU3aZRAp+dZTGyJAzapk*DeIyZ3#(j8Sf);Ha=y#Cw)-Z0k@wX6e1R zC7L~i`ti=rP94*OB{bU*5X6M-#d3KcP2bs0z`XeGE!Red%JK#u-g^S8AR@vGx58FpLL4v7->&Vo=>jWv+WZ9-NxUCqZYS@~?A`p1r{$vXs4- zYpHz^3uY~ToJZK4P?pr*zT}0cNKK6+q#XCsnM`{3%|3SmH%dN6_3hhC{!nb{?&{WI-)3) zrM^zSJvfFZZgecMQh@m7DGu7}{fGC94;Qcy1W+NqBYOT4j(_`d-lVtw>)%tw{Zv-F z`sL$?AFc;J-cPXerphtAKDhh%@#<8O`Tb42*J3+ctRA;JX8~TZie&iyKFzn?{&ak? zn7=!R-j=XWMb`YXeDbHif17rs{My=^eCG8!H+#Cf{&;tFHFl1WTt$&9t)0Jne|>T4 z1$yQ6%GNfDmXrI(C@;b>Y1=ra4h6FP%+AG!`MYQUpTP4Jn!jHDsM#NX{~@rCGuxT! z{`%iar}O^l#g8BFZ!awVSUo8vQxq&cVmBwca4I-TTzLIrbz>Xp{k=-l4-FZ178P=) z6P%xQtn>Nw3}WYaAD7pfbN|a&mzd(){6TSZy<(l3?eGe3w!&Gy(=&(LuL{j$`tX=P z(HxaILY?03<{O;0*x1^7vkw?UaYl2zqU!v7@D4{R;I*AaqrPKYe0>U?4?oR1o_n%i z5&6{X)X~W?tGMU=i}~#Qd^Yu(B3v@+V85_;aLg5-Yg45o=riV+=K$p35Iu|(a>D@G;I{0c zBrcyVRo*5G(01AVYVqv>%O9hL9)^T?+t6_zZm&Nq-s3pO#TmGRU3m((9Ig|LB|==~ zgkz5mtMoC+@xZ8_;x_i+346$LOy!_@0DlzvF7q~@-z${%tH%hGb!_VBV*cU!?#H_y zez?8+;r+XJ;|uU8Ag}#_k;BO;oV??Ya9jqS6uMSmmQhOQd8ZyMPxAz(|ETo#Z9e^G zXXnlKHVPw2s*&THdM7x4hl3CIPhWp|e0cov4zy39QO{5>g}h@E+5kl%s6C($58;3# zapj+#A$xa@k^nfI2tSzpz0~&R#@fplFaEardSfT`wn7eHI?PHbv}6R^573c2tnMW@|Gu{JUvaeKyO*o$ z+u8l2CSHaU4rYtRhbz1;?T3#){q*?o`O5=dVsmj$UfOerVvI^@pGFQAs3m9!LG0)q z4rfMZr$AW5V`l#?(Hk3Ut1Ex|+sew{RyWfKSO8R>p^9j64c=beJ$^>7zdZc#!`0P{slMAU-@#fQ5E>Q|J5JdKk) zGytA({C*5~e+2IxdJobFB=iJ2N;o{oCLyO;(i?eW1Nh9{?$1ggbU8 zgwsWijQY@7?D^x<$Lotw6P+`qlzNlitG3U5vvW4O`e8QnkD23Ry#u&F&>^U&s21qr`RBHa{{N?3_g7P>0ySWKeBC}(AhUTZ}yESI!B4_ zd4KxOI!0+B$_)^H!xaa{DRXy+1_iOo+Da5^NfyfG{vLpevy{i)@cv`1C`i=z0A($)|>p{F{)+} z_q)L<60;x{ZXR|j5s}mW`|F4Q_;P)&A)q;A4-3hy?Y(0yoL^tfP)DU8<-X0vueF}Jce>OvnHkDdmd;RzIH*h7{rj8=3DKvHm;e(E@UBDCJ z=)=S3|M=HGe)9_lp3Rb3bj$LYt2Rkb` zi~f3duZnF+-U2ov&IGVTB&%V*f_IDCU;gorZ$JGoa+`<61sNsXbJ3n*3oYyR*Kw`uELb{s69`4xxuSJP$~cJsN*__~ncg;?4h69x6P>SRC zw@;sc`RT{mna;!A!_;V?KgQ|B4?q2}T>j(B?WiLk;l*nE&{IV9BX~JNwd0$O*Q;x< zH{ZYsJqEUa3e*J72jY*Jzth#2T? zws3$-U?2|&OY+&&&erYciX$lm|>)0b~Q-Ax1S zxVi`JjO9Hs{K1FE-Y{U87Z}p(ijabwX)F~E1S{p=YG=eobP+i z*-n4tec$KT{oMC;U-$K(p#Nc>SF+k1i*B#q&w97e+JE~0diB@9;OMkv5zi94k4Y(2 zKepQKGsZR+WE->1yF9^IGFSQ7x&85*yM4cDAL84299AKCTP?&x78^l~jj}h5D5kv{ z&`et0EAiCse)Z(j=bO7*JjVTG#5)h}Wp$y=l4X`(e*Goy_WQPVm?3^$B#N+EOooXG z-Ma}`-7H6Foowgh1RBWCc|)Lk`zP)0*SnkZBN8Vze#{~WtjikyF1OeIxtY3E-NfXy!D9bF;E~Fgpw(9Pvs-Hc7dV-j%AL?gx;H=6y&i6w z|nd{y`1kGoC- zkc(Y!ooQ>t9>UBA(M<9blD501cjLq7s72au@?Mbp;mb0?I_Gjdtf71@3MS)*Z0d$ zhr3%SOPQeGPEMPM+uUw$;AX}ydIQ)Yup)9Z#Lk9^w|_Iy`ZbfYp0!N<^3OdUAHIAl zZv#lZfA{X4p11$NcH@T*L)0dLHvm11qk!9Pnbr=ye(~b3zcrKQ58<71^M)|^>AGu` zwt0>Bll0ldw29$%!7&q5c{?o}`9eCewrro)4m=gn4o=#Zw@R%)qx1OqzygW6&C{b? zNb?#^bcml6C#DXhH^0A&Qj^~`J1w@G>(xJ6qbro$|9B(op?7^hub@^uYrxeP&+G5E zlSYh^Kf)@YFpn5L34UucjN|bQU=Q^3%ZLD-efiT9zq)7DTrw6~lpRt#inpcFXx_dv zSQkkq0yTq)B4D{_o248<=Fj3va+itUTj25vcjrWs5qV49TRy3kfAj{s%Y#$3g6`}h zaK^mJI7!J4&Kq*|`;~@l0KJP{?DNTfCLVCiiqRU?yw!|~6~rg=dl#WJ&)Y3VJ-3`R z%q}iR;>q1|z1>@OzwYn9c0ae9V9;cUBF4O)p(Rn=OT{qtm)mz&_6@(n9{E0 zwN$C`8C&w>;rrLGSKanWX%|7RnXNSfdTYk8@6cPO~Q?gv=@Sx!)bo6!w()rbaQ$Zu5hOI=}&+C z`0}}Vl;0*P!TPq@%%;iF=~;Iq3TW;RhNIka_Wk%M9?&SYUuFWOy~Ebcb7y_OYT!Aq zEn_(5+4tX1Od8(NSMWqZ3Xx19W3=qCvu?CtFZ5wJkw5u#d*A!`?;qVd!E-3+v=Ok3 zPXJZTnk-hEZQkl2z*%&YPLPxyYzh*KggPNu0qhNjM~(B&<(HeQn@^vrrE(T^9Z`#U zZ=SZzj@$KChhfoTcH8VuP;(ZdKNMVB+o9Shm)-@v&GPpa8Hs$UlxrT98`b>oKCU?6 zM$8^Y2ia^l*$qw<1N2UV-8O48+c5$j7i5Ga%J8HqGTZwk4DinmYjt!`Pj+|Fyxhs` zW_H-XDMDV3hd)HE0X{wJ(AwS`+5wAZMTwK|*hup3eqyPQ# zyw>GeG^9P=OeB;1Slc`=XFjDW_1I2hIhk5KvRd{`}E z%i`LkJ)rek$K!g_mUS_@>4v@#32a5&o9hYddeWNpZpMR2a?Cs1A+V60)P5qH+5GHH z)~cqtSoF&=c|Qv2;khTN9%Fc=8djG2Th;g9MLZT4Nr#{s`Pv2Mtv%{eLy67l8z z=t^pPen0Hr^Dd|S{yoE1!m_uvoQe6<>+9R$-2nLJ#{7vZkvCP6!L@>?6bzgMLu)BV zDjHp^x|UOGap!JeCAqkqcEi({&#ZV-@s)7Zy&VaqoG~hIJV^&k3|B7d%&zQ2?xDyGa7PDc0*}JhDkEFIjHS>8me7Ly1vbnfx-%P)+dbhoa zc;L)XkGU%!c2bVDoHZ8o+Y%P{mchU2a29Llu89d5K`8c<7;xZ z%Ekt4psm*xnm>8Gt$scKc>k&Om>Zf6&vR%JHQ=F2=j#0I_WSweV`szVag#eFT{CK3CiN#} zJYc1Is_xmiCQQ?lX4{9gvtOMPLa{dmrsTskE^lm#XKm2>_+Mk zAT|V&_jd3}@ONNTKW(%c-haq{ENmyBp&|K5U9xUSj|ZDOdD`(FKDADE*Bla;wZ3$( zZZE%osYLDXUb92L==*C>Pt>v!@`T)mAw#^9OKr^i*KPh?b00O%<6eX&_Pg>v51&c| z8yhRuNq6dy{rLCS?onmOsrd&la%jS!H+q*ptnDT~*maymbC}E@I_6Vrp*_xJLAl|> z*6!{hAUjuwHbBC5&j+@u^7U^D*_IdeqTc(rJq-OL?1wz(nB*I-T=KBxx3AfQ1-sm) z&yf7|F@GYTdaR?1YKkJzQ9D%K^&=m#;$q<@|7}X-_iMXQ@7$hQI%;|&eRcMkz=6%df`4>ymGwN0=}^KvBL>D;Q@o~*zBdyr$MjQaU#GE;8UA?{dp1AWtn<;LSt zG&wuzvGjCP9P#>(YUi>0`|)Hyrulo+9Evx-epYvB?S2^TN3g*%ZFMg$ryBKgX3af2 z(jWa;`}_;@qSekCdJfKP?Ch~~`@7bC?6h~SdhPgfw)OZ!-K4V@d<))WpiYe(=Z}r$b=_ai*>TmJ@#W;>P3Q6B$wTwYRoLU2u=}&BEuXs0T*^B>)Hn3r z^daB?rH28v6ETl2A0B;qY-ZMn{svo6UeaH-B(con$HwQw+Z()5qb*oaF8}9G*?7Wj z)V}`v?K|6|-R2@e6bZQJjfrOC>+h3NblUv>?T~s8oq79~H^1+8+n+zClVPhPl2b0v z?MX5in%8Oi`}+E|dedSk7>hvznD&+rPi{W$?>a1A%S5j~Ml5@u@3_a;@4p{7uO{kt zZyhP$@9HQWaU)4B6aTLv9WcfES`dVFyIyZKGFxuF)oz+pP3hAvR~o#>15W3|=T-rT zZ~KIMKK^cHVk>~LIzjRJUcIHDl7!ZWSm~&9_i=Z_q~YBrdQ#eh!SvCW?>`=t8}Ox7 z%ccs|^T*$JS08aD;5ZnhEnII6PL4W=?w0D^%cFeAG&%HoVCe0j{Ah4~om>3={^eer z-|tt@0*+djm)%e0+!lmEk8N80+JF`%9dBaq@T_rE-VV5Cb^rYJniqW0ubD6{#>;1& ztGh2(w{nl)x99c!e5qC~=eJSP_KEtC40)QpS_4h_D3Jv46m5Z#d+70?3( zHy+FQ(BMBBJ;`-|6VV>}e1%)}>j5wFveP_)$C2DvTk+V8lUjaJ#$K==zeN5M5JfXYX|$~4g)|G6DIp2&`K(m&h3)AiSi!C zSsY}v;&%!kukO{tU~+x!Hmik9GEOV(=1EXzKHv8j+6o363>Rd|iOT2;DOLm$UvxSa zLu>3)<&T|SB=GzD=a1C_m>30AcDog~Tg@s)^zF3Og@9;=dhpDSs-zUgG4wdtJS6M( zO4Uza)LT4sn-w9^kbNUqAj<4%8xD?WNm0@fAm96iTpDa-3q}*ZE>t)Qv9k0bM|n+vx0pOiIDi39=TS(35W> z47Cf@#N#ke0-OND{4+AD12eGLcm={!s5QH!K$%@h;#7!7^loy8)-kg_f@(J!Cv~=W z=2Uo@X+ul?{%44>Bn4n0K7>1%ObOnUlj)g&>#+iSEJan+NR}#962Uc9*mhVhkoSrb z#e&?8at46(0|@#eJP08}L27~*zyya#4Obi1#a2r4_Z7LvVVR_Xi&n35kp zxV?xZs*)CBcwwReg44znlb^Yl2DBoc1sZ^v)?=mdDDY+27I4}i!Kj-k7(g=Y6v)Vv zyb_klL`kT!99aj#&*+W3)Kk-kAawvA+ezpbVSF*#MMw$FjF*+SU0jszg2p1;Oku!D z2^4+B`w_Ik_~7j_8Hhj1;DEUigK=YLu2RuFWk?lVsa3|@U9KcR^!B&W^lnYW;dI$ zhNqeQ)%nHGa5_Um5t`(BN>4bDuc z&?w&CUi`XjKGqJZ@%4l=9O5*?RkGz9iNZ#)y6%p8R~JUd`UeKlsq52vjq7{i;~!06 z?#JuvyJ|7xip2~r<4n}Qlv+ZwoTHbzphi=S|C00?g$5Au5( ziO9sXUR<4buQEG1I0dGSvVXIeR|tDYwc|3Sh$u@C6YWS+4g_ zPoK*$LsIYN{Pe0z)|x{k6}SAh@v+x0ln&#dWqfYczkPIof+cbZ`7|t}cwjZN882q{PLGmyAC9N<#qz;%lL?QC($O*E5ABQd z_T_n_oZs2-+h#NpL96T4=#c@&uT|!$F<|5<0g_R2V`wY2RTg>BLnh*7cY^ZG%rz3lG;O0vUz&e zY9bES5NW4_a)}Uv1srsNo{zj%BmAO&P;0b%0TK%bjkET7=jxJ_Rg34e8nnKc-`&WjyQ7s*} z&M&XJS42Hr7kr9S^=f76_!6_TXdaL^rT!HKNub&tuA$M18LMw2Ne2J?>YK`xs*l}W zcTcN1_Ajr)G(%uIIy^EuG%)aHXt@84W^7`T7hIyg4b`G5E&KN4r!?*N&mZ4N2oYaxbeU#MrYYUn zq{U(~8lC7UxV=aZc|9JV*TuPzz925#2E-zaT8l?t-m(Xrv7pjhF+`1K^ai70OfzYk zn=@IK0zpV|9^aDPs$Sl{;6e%^o=Ay`FWjvAhaaTT?{6z-_UDZSyh8+9Yf1iTs;8X1EYN!1$8$c`Kd z76^FhLd)Eo6`ll|$;AGbch4!nhucnLFA;K?$Dgh2i&ul=W|y48gS$mhLj;Br6$yGy zfLp-WZKf-yLPmJL?mNeOn;!Gn^T(OI+9|U$L=8($_{!9pB4J{+7^Fw6P}Wn0+Sz|H zkiWb2+@{~E`?F%$&?n6PSC;X##mu_t*VAeb@EcyGD*6iOZu&{2w zefxHOAsy{7jt%w}`s-1HE1Ird{I?~Kx6N$8ghTW1^?KYINLJ4Uw&VKIZI+V(D;xCF z^nETmqkDN)NB1(ejd*qT|Q4Od6{3=&AqtSr2Wr*2FBSA zIJ*brqzH4WRLrOFi~=wVg-0WVU<^zvtoSZ!GzbZ`bEX(5{wD z|EgWf&0de{ZJyQ5l&sdv>)TscLapNia{AH0lGQk+16eW&eP%0HY5b?1k3VnP)eN%Q z`E^;r^y`c2r_z_#w+rO#oCGEDK&rH!97`f0OHvzu0FWomj^H zuc7Rh<512hEI7Noxx<3p-CmtHYsIu2ImB9r0R`cPIyBsZO8xADeSx;p)T@hA+&;AV ztba!R`6UNNr{-L+cS`IAw?BUNrV@QkR~INe%?ryt0 zy2NSm9_vI8J%7F>hCq+@EehBFnybf0bDJ<*kviXLPZA$4_Ai( zii&2B147u-dV9L<%OR>Tf*Yw){o?k&tLFE^ZMS_~VKT-zAJ}0v&rG3%It4G2J%)yqp?p)})9#qO|$s&#>)s=!(vESe%EHDk{VASzM zxztlFb8N#f^HAaeu~Y{S>=9*vry}XaUIU+-RuYZNVGw{30_G4KV`ixL!2v$1RV%O| zQy?PI)u7Kq7tn1ipK|$~?-4zSOKwh-Pl3!+Pf1rXHWs=``bkECArfR%1dl+fXiSa{t4I(; zP{4(se?TB1rzZex_IlroN|#*nLFvhVFZoi~E({SXj}e+mYw zK?Ja%iDDR}Fk3)xrl)i#Gk`H<`tVqp8uTs`RT%`}Yj9U7>0l#cTp3e_m$|9nS7sTO zs&~Y1zFa$QU0im%mlv&L>VUJUByYYXIB(29ME!v_(;j$N@U?9AM0P zo{@}>4=EIxY=uIau$Q7Ct_AOUhQmWHuJ7*e@2}hBVyHXh#ZnnkN21vX17AXgG3`jt zXMQz2X^{XKXobU(T{e!awAfsMSUg4g0;9GWTC&4RwmE%tB-CkyZ#gQ2ZYq|De_HJh zzD4KaoCBFDoyrP|Pl+zPq2Q89`yA)@YfUau<>QQ5=bTtP{Q|xR=Wt;S<7~Z{CxgXH z`i0US>S_puI1DVwZhk5-1)T&)A!io@3mi9_b;?}QLaUDXtS|!p-C3NdcZVnellDC}OMmYoHoY^ZQ5qo~w;r5L>sV5ZB`RkxkMI=5? zZHQRMpWq(Dgf^*&j-xQRmrASxLG7UXVtj7Mz@Sb)MRD9Jpy|w*W=M;7Ts&^I zjw|9+p#6$eDuGNMXRO<<7G6#hss=rjHCV@KzFu^pVfDyLdkOT>3nDuWE>Yptu> ztJXng9gkhYU18T#yPT8K0yIC_G@%d$Jr9Lx$<1e5HNEdY*D$iHM6dumhv zgo6sh>T#oa3XoPUWpK9Ku-FrOfIyvgqllycBE2>+4V1Vg7sps2H!L?M6G`a7c8mLm zM<+~Jb~=tnvd@XfwImXePSaE1{ZeD{uBRx@I&SbV@Va6aUM*+EN4N5|({{T_qx^8| zadlToU9$tpPDeVM<<8L1#=Y_Zh~3U6elgpNm5sa4rRJYBktts7_3 ziqnByy4Sk|lA*AOmxE)*C87+B6s!9fM+4sr`K?u+gxEN-ACUovJjgAi>`v08y<8+N z4-XN~j_5F(CdW)IGf7Kv_btYm8I$C~885qeN(=P)dQdZE^+tD!)uv#Vc>lN@P|1}- zCz);W2jqAt{F~l>Ax~SUx(gfn(y&RN9+Sx5mI8H84 z0h&S-T0f=;ZLh&hB}mEBNjS)KH!{h2wnLV<+MiY4m^77yBJ8j%AmKvbSPS;dN4W5b zkx%_!4Y;%ITNlgx$n3?!xbg2K;%a!;z>PM1Gh(99+4SSI+)1WN-u*dpY|RkE;b+NwFi${LJ^bHTbPW*VVa?V*RT5D zfMy!zdbI>%9B^~7_!3}<<1Rkce|$9MP|-$Nqh-v9tb{U;5TBc0CO?LT3n7mQAB9V= z=iey~{Tw51(?X_T`}a~&$sPubDd;zLzZc3pJw=>3ap7>G@RsPzBol#IE#1tl7R3~P zPVO1lm@P$`WSo&lz;!pwNLZ#Eda6wwZYfMqJ#viUKR*|nESo*ADNbI`AHypWzfRmc z@wdbpVT@GEl`F}Jim8+6iu2icoa#cVpd?ms5i4*m<&y(0gb;~K3gv@ZU5;RT?niN7 zWXK5BM0i()e1sYQw}7`L^)2mUrHhX(Kn$R7?*Yp3RS}8!n#xUk}S zOcM!G7!(z;!T81kup5d`2Z{_rVY4Gnt3q65H!vJ>(e?6j;^^SpD(A2lG$`j*IrQS; z%Bx{daK}YEnX*`6uBee=)i9rxm!_B$(qBB2Ct}}JPk||k1~D&5T-KXdGT_QX$JypJ z<;EC7KW0Ygsw(0T`yiYFE@|&`bBQLzjB_jYJ!Cal;cBfR!;A&yB0cOC4_b8(J%)wE z#7I;qzY1?b0vU;)u%gl&iTA`AmS4nVGZbnpU_>YjDz^5?EcGHKOs(7uii`3a#nF}~ zBR0#$z^WM`fuNdG68p%UrnPjVgcd#5UxgMjeln&+AaY4RO9&*dC-I#^17un;x_gAo zENJOzv1!P`5sp*C!>q(^Smq?kcQZ0d-=3VKyrR6L!e4my^8kzUuL2AezXvE+TJ(_LcuC!>>2#0wZ%GB93Qp7*zyFA zQ?s41Q0-E4LE?E?;xci$WJeaqNmhXLC;hHwxIi*$ASHNEtP)u!GA+1Zb%-jibf{XZ zOgf%MnnuE5Tp>=<;DHhlNl)V0iF>2Af7y}5@MnNSl#P=o%AN<@;bP7zOM(W3%m~6a zMRa?fJVbNI%86~jekfZ+?Crol@HiPZL2e|DWG3-QY9pV(5rysc%%)mxYP4mw@z(UI zf}9|T_oktgJhLRZB#I@9PRQ0GgP@M56!eP4qWV)s#IjC>N7C~^WIZxR#RHLDhrq07 z@_KVw3<7%Xa*H17!BiJp#*oe&x zK@ev`%2szkq7DnuGf={bBTQZbT*0JBxu=R_L&CHZPMUxmS?H%~$&veFHNuh;h(Uz2 zy`rH{1fyVAwv`zx3W#MaYF#2YN<^q$h6u}F^s zx~Kjr6bU*Ey@YYUMn5MZdJ)g(m}tXCR&RqmFwrXB2}e_Hg+ro0%3O76$`~LJVpy*zi$w@rDWTC!T`BQN7t6il$4|Q_=<5b$*LuGqbqm zq$1?p(VeKz&Y5kl$WBQjpPY!`v+2OT#4&?Z&~ETeTnn}lAkH9*o`p$;r*Y2Z3`8JG zD=rAB6Wx@JBN4KNTDx`HC{U+tC&`+SM9FErUe4#(ab0s}6oXciCBn@{8Q=q`h~1K8 zaS@dG{ETue!W{K{<_zEuyRK)66$v-Tp?hl3@TAEbilp|6jdt_2o{dMBY)+?VeLH`A zir_~X)^UuJ?nKbc8ug>F57SBNtaIO~i$ z`=^sjU+4^;SZrsH1QkImCHEl#D81`j#Rez2f?*>xf$KwHyX1q&TO#~9uI7``AR9F( z4u|_xk7~68M388?xaP8fW8$eIMy-lN&UJ5k;dH;o8s5m(Dz(#kqkVi(rj~qxlY9bk z67N@?vsz{oWj|1Umb}}$N=0p*bqz>Q*SI*}0I@BcO}tV;UXYX!2$z33sRCiRtyC8}c}Xh1RBbkn4svip zB#ezF$<5ryvdN%pfr@Vmp<$p?Nq=-)FDaEU&U7ayj7$&H6S*_g?w;Z&6DPK#eX)wj zL^^D zymMC5jLR3HFurrqqH=W``pg^%HkljnH^J3%vS+J^I*GiDcpW_&CtI6Tm7C`l?Cw>P ziUd`YqiQX^936LZ_}1Ck)ph&)tcKZvNKaaheKZhD7LS_kvtw#DH_?)z%y9{mimD1) z4*;P&3;zeJph5xmVLW)Y7?+FRbUEB1e=z1F}2N+ zLhuJ23m%cQE!CN5%wKds@ZNw40^lszcS5w1xFTYRw;@onEnHRsIW5E}WupxmZ z5@`v3L-?dD5W0jo;^K+W3aB4;SAeq*;Y(UfV^0%r%Hl~Ej<2t8@4M%1&>vXZ>|ulo z4sR%o^q3?n4hkI7833fREx1{YnD{i#6&e~?S~O8myh9EKz%mTv;q|!}Ku{?#3xUlB zSA%mF-7B@6`e&@cOtE=$efMzHy{H`&ppwFj;AD>_{CNgnwtEsGR0rU~^Ig(RK9q4m ztt*v-BLD(|Tp^h7(;$d56ly#xtKGHWp^68+tdP^LA7Q)TKuo>GznyP%uOEJX`*zvn zWL(6W0c8Yx=A;?WFZNWNcw%cv=@btyIP5Ute8SD($gJafjf@)^DnW~6JI1${h|Ox{ zI53J#c~;Ehu}@BE`iI{P>5a}%g>yP3fUWO014Y(HkU{6DNSHNK%Ztm{p%kym?ZK%y}sdz?mXsm4KH`f(zfBWkusy zS$J@4B7hDF?u2Z_#N@;n3JRgvPUWb5eRX?#Nyhu2kOHd0X8V>fn(ja_is0E^4#6MT zI{+Q51w2WT{&8gX_6X(3q{0ONrxNU+e~6JJ+k>|W;^HVq=&%)*jC|R2_{BN22%qCYC&H)&W6gCjDoO0 z$iQk7D!&h74g3W#MDS8T5PWB-BEkk59ic2{#_r$99Gt>Gd-y^69(Pkzs)+6JU2%gE zc7UTsbWg-7Xp=k*z8yLZBHTk>824UP;z%}XNz$}D4Dbr0#t=xdiQ$L0AQBBz=$8p) zqJ8!4{vIYo6C$(Veqq5`2`V_Kk5Q%@1xg_HBW92whk%A8hPcpNg2&1H@N;gh!h2M< zwwFI8xbre10Gyyc*~B@;sVA@AQ@6o!Xp-hOIjxRU$y?QoD#0^uqQ|Zuck)=0fgsayb}j)@g9=g zCvFC1C(l)`!`P%{kQtD)k{il=6jGiN~YFS~#cq&UOhq7|@fC+4ZpzH=XN;=LbbW(j+s4-X~bOe`< z)E$CtO5}nL==GhzB{Ja@i36jmAa9UPVj-z>>pe$x9r+8frIItC&&>J>Eu6fOu_?1F z6wff-yEix2LI@I~l!!4YFv#g4MFvD&siE3>0DLahQovA2Tr`!F0Jq-jg4O!qKpst+VQ2XGR2TLl6NcocMB&}|_v;01Dj zNv1R2LQ7zylmWp`@(9C(cK8iSd#~SUbjAf=l;gB`SISeLV!D*2_c0(!Sj&Da(^WK#Ltw`P-R#Oh7z0=5bBAfVlvIZ1r|76F33tk zsuS{7T!535f==L_$``Us1TI&%=K(@gd4{+Y4#`WX#|XA5hKh|{uwgE%P}pRhTChQC zwSpC6KolS-ckf;5Z|0DCk}L~}HAnRQfjm{GVZ<2m9{i)$3v)~^aS;Tp z6^urqwE*x^jRw#rjZ$a^3e9S|5>wDes*?nxl1qWA3ZfEZ^9j)vIG0NhSiu_Ae8aDh zy;5ijLgeT{$^w+hTdJ#f!{-*}(cWuk@1K;(6Fb@4N3_Qy)Ez8iGY`W!z?f!Uqbhy8m zj=FIa`i`YH4LQR!4bu&xHd`z7J=x7+D-xOE%_KIh3_AX^wlQQV4S=E7&W1FN>VrOiTI zPmad&7kCakElZA>h9x(5;aJd$Y@=StrMI`UHE(uyd3$rbKOc|hlfj@9MlIH_<+G`D zscwdy-eB14wgX7SoGnw=WQC<-u~gL^KZ+T`G@%23LJv#V>eWiE-e5F{ECgoIAYw)# zWMFvCv6-PvUlt+vAM|_O4g{3r_aX%ue6`9BhhU0h8BI;opsJ-f?E2gKr@YgTKf(X@?fy-}$&G|gyM%Y{s#>GqaK$LFW}%l+kYIUSD& z9j4&w)p9XkD3u$Ik1zzRFNot-8xGipVF(W!Fjfd@%ZiaN;jiyOV_~;Qu)}BA%~a%I zh&&0$xJkRkri6tronF7+XX!AQtpzJ(`EWikD2}abLX}*hTyGk18A6yG5(c3Su8E>Rc$uC9pO~pg zhOAe_S5`V4jYhCpxQsA!Z<)PtP?SRvXSG~eILacLDkq>5kdk|f5Gm1iEgh=6mKRvI zQLHw~x>GaFLQ|{z4bv{vQ_pvwXXDXFU*4Sby!PeE)7c<)G~~O2d>1RVTCI>uCUTk`tA((j29q=(8XHfZXpw;yk$ScVy+Se-cXIcfiJv7B`yqf$bd5ui#oxxKZU zOr{IfCJu}h1?}N%|M=|k;&^{L98bpc#dz3@BhSQgxM8c?=A&VG?ZIfaV2OiX7ut;` zxV#R^*3|Sy6K|(iin(;AP|F$uepO@$rYB|M?f6e{uc(Cnx*+-LOBMEv6kiyO((MZ0p(DUN)6jd-uD4 z@`I-@GPP0_TO)qQ?T#1c*C+e4wiwB%)9;3^mfqRf*h;2z`C=)b%chgrGEy>Kpn#Jx zH$A<+{pjPHv;A>Td})6?>bCtxwNkD&u-`-g$_e{ndpKdaohXdsHV!xPJzc{HU_9$u zwN$C_37h5^M&MduUm30=xQ8hcCFFbPk;8= z7dIClefY^Ir}Km5^5*vVxKk^o5{Zqqo!vxs>)r4F<3IT3TaR{jcJ`8KxW;I9dvbhn zezq75;;28K9~~b|Vxx-LEta5YshH0sHa9ksMa@N-AhaL$=lf?jFK=JHyf|IX76+&2 zXBQ{SX~!jaA-V}&qgms!e7S~ww@uq>;=965yFWlaF+OhG>-5@UvxOFT5yLh0MzL6} zHQdN{?W$I{EZwWyVI%g;K(}he;`(;F+Mb{9_r0=a53X)Ly2h_u-&|i4LoCmbul;G; zZ4`5vOtD_y}cfR}94<9~y{PfBCb|RfhrSerX>P;7Omf7!57N=L27l+eW{|#(XfXX!W9ii0|F9kcMY0YQ z@~Nzq8n)Z(4917?Ir?Pj4|*h`^oYtJ4(c<|u8 zcOE=`@_chEkG`v*tZ-sODM@5U0h5HvNlYB8JI+shUTHLYCL zDmC=X3A>}oY(8HiH^*n!AHKT1y*fWVST2z2VIN;%YSlb5E|d!C)ZXsyUMf>yFxCTi z!=TsiE|!yneVou}G#!s7-4+D$t$G7LuGMqJN-mSHl=6*Ys%h3Ow^|9B#>}>chSSCH zN9{q}8HK|c*3pgoZ7%k^1dXN_d94;92w{)bG-?g2R;d??>V#qJIH5;iQ?F{pQln(FSYA0-$d}zX z4jPq)6%M+7)aqe57vssSi~jY7ZA_pe=>S&0*=&^SdebuM5~5-)b;I)|p7#S=H=Iz# zc&Sk?y3QefAsRxh(X?`NE@S#P>-rPNBu={cyX7ddV(a)wc1d{B%peRtk<@Lheac|I<8UF|qd zb4nalw3(gmI_XX~nOjC%Z-w)+ zvDCK*!@b_doZ->08l94}#mh&aBYxSpA@+&!cP@ z*>S!-`)|Gf<4^y=|GNB#<6z_5d@&8v-Tm77$s+Xhf=`6LJB?m{l&d25QNj*4E<=&4sqi$rTPNsj>`{ZHe zRV8yg-0_a9Q73!c+dLR1=F4Kx4`Z!&xtn`Gu&nH1zu{z$3=00EctA3@|+wF{sO|Rz^NId$L>M|^}ePga|UH2cHFV;t) zb(T(k8GrBg!|(b(e&K!jz2aY&lF@Xqem?x8zc~K;KmYN6aPn7w82|J;#h-0uK3*$) zzEeHj86G@|mX*LX`!(L`~zKp+hcJ*E3v)Y5`1N()0pzR*C-#t5ft9SF%{Isz3vh(EN^of6) zE#CM)I{oZh)-S(X{g;pb>wos@4{AS6l)rrX=znbeXzn|4{!)ARmxFIzUOtFFO>W#y zGp(UkSnRKB3(L1_{>a^GAMTn5JJ}E1cMp!=F<;f!dPh~$ublVZncuGI$9BnC8r2E$ zwmB;{U*&f{bT&`h8PwTqEb6-#qlewkU%dCTm)}l)Ie$02P1FxHKT3^H6S>R&mU~>% z53e`V+FXc?{PxABI3)<~UqAGAI$KRx_-?cra1 z_J_OwXYRlHSJ8j`zg_&N7cZaB+K)Py&UXHz&Yv89{+->g;ti|injJ6e^#}S!@4C9) zKdDyj-Yj997fbV5=J7A4|JfHe-#%>=K2BNv!nD>KeEXA+{=xB2{&@NFt=9YHW@NN_ zhS?4p;XIxG^Tr?la`vBpdi~9V{_b^s`+e)d?b#peUlw+zt*X{J%RP91{-f#gVQ^l| zP5N8G!A>-P9Dlf5ILrs`;PjKN zsz2=ScIFk$I`kjxzkJ+ySt?C!zttE<8{H2JYs<6Z=JM#7b*@F_%1l++XIu`PKP1oiCiH zt*cG*RnnPl9}OQY_S5zLtW=!@t!n+OQ2Z?a&Ic!3^=_-L7smZYyI8#q_Ik0=^=k1z z*ZRX;{i^xo^x_AVU%vS6zv%ua7k~Nq#noc1abjhov(l31R*2e(F6qY9(5eL3Zz?R~q_N+naQX)z;Ln;nG5PwwL^6^;`el>BUCsC-p^(HRdb zjdrQCC}-P;)#^dc{Pbb!pC{k@xR+_fM~zI^?Y1*ha_M+4#fHmk$bv(dxO z=h_c~&!atD@6=40$3dB}zq2$RwQgJaoyB#sICh(UWcVFidC*Hm?{B5A&Bi=Y_$+!D zEv-P)_S>bV*X=hG!>gUj2dV03>${&dYlC!unK7r${8{g<#mh(WdD0jePVDEqAG}EZ zy#C;${db$MnyGy=f6;#7FHNf*SF0ECMyIc}ys|SkaKO32tezO0Br6AY!8>X{4X*Rr zxYR!0$bS?hA}c&9Z5$o#REFI+QU1VwzMSrOC&kpEojI><+{Q(>au{gSXWnOt%26{p z`eOY%U!3kly+ZZn;O)-Yi}=;E?Jq|Uy;&m~7B>%Xp42W2#Gp?}N^U9rb;^Q*cfSwWHGBN7HY$|7l|F za*#GgjoP7++K-a`Y1+R{J^JwSQT3*{_sV+G-tVv1K5Ujf=Q8*7^apXZ(m^nX zL1P>g>`}W`je5jGmDa=tjXj=j8M7d;D&f?ux6E>HX{5a|QCTfGaB`t-?%Uhe(X5aU z!$o`Bomsj?!0yHkCvLQQH7oX_b}n}@Ni=+K80M;zalPT|?NP3=jC9>=HR5)49BfXH z3k@G@;DpVn?YnvqTbAdAgo<`M49sF{YHGSW7&i7g%W6G}`_PcX5Xj}BHh!izEg@uW<6-x4W}E{HFq>B zRl0uL%S}gnVc+hUM#l?1gU~oKqR6gep-V--+i&FjfulKXBpfW!A)8{Pmf;eq^3@xd z^=@bdrrmDaRc~a~Lb7&4^E#dZ$kuV|e%H2$xhmfVXWiNk1S)CR4F z4IUN`?UK9nlFqml8Z~#+(VUK9cI=wh0st)6dR{pqB#r?Z(Nx!oI)UxCK_pzSJ8V;5 z=yhDL*YZL1L(c_KB(L{eY7l0lPD&@`1`_iMxTSy_wR*D#dZ=5v0etFumMyppv1qH) z3P4UeQFqXe05QQlx+Gj}zfCF>bq7eFoGT!N!?fu z%a!)BTd#-1s0qQ1Ak_VysX3Z8uu87i1WX3RtT}!|vuwSl+u)8($2Z-`F{73f04YX( zcM!(_!<`7Mj8O$k3w%QdX(1g1Hmuicr82b!a?EA zML=3!E3hID@ef_6<=dVawRO`2;I-R=X3w$L2GZVCkoU|K1$wY`%n7Sel3P{>3wmC5D6E7(A2>P!h*0+j+? z4ghK6&UiXz8PoaVa5)+br)2(9mMDvZih>;X`Y}ZhQb&{)KoA7arltnU%%YG9%_Id! z>~zQ048Q`N}ZFhJ!ZJ<@;5lus8&N0gezll}eu!~HqP22wH{@lWs#umCB3$l^>}3X+sL41`6> z9a4yF=mrHI5Dh9Y226E88364l|G-?4K2*X%(RFG<7Db$zjgfSW{GJ((4r0fw$D_rv z9oW&q>E-zlZCIYYe0ex(bw<#? zfK@$oTX1t+jzaU`1ZB#4Qw)o&i6R+f0@FkwSTTF08cQ8dpg&542Bj7q7{t=*dcBTu z@T_vRk}iW=#;m_qZsJfoVgZI2v$!=nIKRG_Gp&Owsy&DE#eBA$4TfDY>>i+dE>}VV zQ(NoLpS;-E+}U1#vG!tZYjbml`d^_`EP$IeOx(HHh`0~LNGXuzNi}f)@c85irJJDM zpsb?LR54%o) z!?3fXQ7Tu<8WjoGsFN18hdofVxIdYVdhNmThXo4zL_(KT?*~dE`_xP@ zCD>}Q9f%$koCaL6kSoaJONkDL&F_KgCeniD-hD2diy5gH~O;Ws<8n)Ec5kaiU?p`{R z*-Pvu(^-I3d^=qO2n+L~G7NY~xeCiP8Dl=MEWAl=3O5JGq{c=GRj@~-3`k#iCk(`W zsrt04A4^@D5+sp9Ks^pLnj&T#*a)gBbStBy%T)v!O@q<_e{waqC1jyI>vB7sf`=(U zwmnLqVFcBDtaOVzwkRLrZ`%}VD5-RyR-{$+cLcj$Z@_ojMUZkxF0^Okek?ig z;fI86%!R?Fd?oD|4w@o1kKjYGMoQsy9N;qe*X{QxN%GR~LcgxmMwk|tGifQ<(#4=! z9-tc?CG?btct~wWjV!$rEJRi3S&kH&<#IK9Rv_I9vQ}9KX9`W^beW&5Ud~`1ybJMU zy_~qB^PNC3UnNh<56e=8Ao3yvU#(dNVWTWA*UJzDtuPij!4ViXemuk;B6C<;KyW1+ z1TA7xq<}14JJN$A^;N=HEU2^>h}A&{M2cvvP-DpqWwonUS%rLEO`XrO0`)}>+=&Ep z5h*}Sls-UGsXECQksw)(Fi+N`=)-7SAy*?FLJr4~o66-V0>(}xN^Bxs8co6yvK0a$ z2=2?m@I0bmFmP4!7pxz{!vLtfv`tVrpz?RmLQ!%hk4U1h3~@jR7KAn^hk)q z>_oI}d07b(PD@rJ-7(BeCWo#oBcsz_Ku?CQ;b5u_`i~GAnH$7Jaa@)q@=9=I*ztJO z@%5(Dna;-DsI~H`ac?@C3}|eUIul_cQ>Fi@NZAhGMePT}#pG=_z^h=wCDmvv@~Co{ zP=ibkOHF?RsfqM7$=Q^`@Im6eq@pK9J!~_guIM7BQFYEq_ncJ22MsOM4HE6ZY`4HJtd8$>OozW#)9?uP>P@Yplf)1_ zHX%6JM7xEj=?`dF7>vd^L(FHJ&In?5dKa2aaW!PeECfHJT2f#Y+*Bfjfu*CTCA}+H zWmZh~D*a?s8zGOZS?9EXOLY?2jVZgG$m4ttkCM0q()3!pGoay zi&ZiSX`vv5pq5GGcY1udoJuZ9WF%a4$id;YxHNJQF}#QmJ{U%idfj1BP%j>W0fk*JL) zbCOV+1CVOUyMYrL6~)uIzNyl!0smKw+Ys6|EG_B+1a(X&=POD|nD zTSz_I+E}Z%7cVbokzvG#?|=T&FK({R&#$h|7sEacM}whPFKn!DKHFG(x=94PvG(}g zM=z2Z_<>~aefp3H$@h;A7h~F->cAo39=T+4YlDA@G(C7am3}MgPWBJzZ96}sr{M79 zq z>Gb>*IkH^D!^x-D-+B1(?FSFueY};-WHui?c(7TE#s>?snckEo9!X=w!K@oL$|V5@ z@&vufz3sKN^+ZVrhr$=d!}@X|_{yB13PalKI4LaW8Gw z%66$>)r>Z9P$^R{*C!X(|KzLFMb96f-F*7_&H34Uyg#Bi+%)JXWGd<1#M;A058qi! zWgB`amt5Z|G(wn-L7EOn;|}N$y=WZ-Tq~y1sohk*SSRr)WO60VX?5tPTO}-0Qk8@K z`KX7m;s*&72`OkeCJM*xb?HPSr|!_sGMO*vL?bI9E)pz6kh2;QHN6qiV+?RBRtLq! z8Y0m&cDRuTNB~$u*ByPj*-Sp2t>n^Nw8T4nzm)Ga# zm#4?)i*DPlLB>jzE}KFwv-SAh@4xllTAD>v@~Lc@Mv|~Qm>wP<9RuNXakPRQlOAZr z#fV$%xJZCO+#7&Pcu5MnQA1?ZlTQBTwZf(5i*|-_Zs~6A@WAtMF-JnJo`bFsF1}Gzhf_W^tLc%@i`b+gm_eSk-in z1}T}J_yn4RyTkF6%vyR}rAd&;W#v1OL5XGYoZ@N0pC#Z^fhe>Z0qrgK4-NrPl%r7{ z-zs7dyb+64m-cN5EhJHpBu*MeyBK50NB17^Ch0e>(>9bCsqB$)KvI!)@luc<;*dBZ zbl6dG48?3VkB#6kcp-PZgUiHNU@MkO)_^wkJGA0LW@*Pks_5n{7V^M&72E^9oCHMV zmi{>`riA5$Ed(Aa1Ckh#4d*1WC2pQ6asojqqN|cN3C3Y5VU=uzsS-g-L#+xScp+Vu zo*>CMnFkSzN)Bi&lI9{oku(j|g?H6CPa32*Z0Z%D3jqyIkdEXWZ4gA9NkWTN1|fka z=B!5hJKfkaX;w(m0<0{-uqRwxwXxo%7t)(WW6~;w1I22R)Jl9Pk1R;bKUq2q5>hL+ zsysmjHfSAsE78BCQS!%sMGn>^`AQf`uRHOGbh?X3uYz5{>f6#}+M+NahhT!N-m>X5 zmI91CWMOJ`pijwi1mERNZy@MI7!o(aa;8HpW$DzJz+6GuNEU(AI0~4E%|xo|pfrko zkU{96Hc6|9Ug`W4v`eh05J%=N+g(g5We7>=B^i*>)DiBiro&z#I~gkSs#YwC(;FM9 z#$8e*X@>}yLKP%m!5x#`6HO62^BLDbb|)=sa*06j5_I9#7(#LvY2C!$(W=Nl3;~J3 zYW>WF;FQx?sZ>qqqZ68O36~)%Oj&5u#7>Gs;7OGgr9~EC3@=2pm_5})iIoytgaRVw zDyO9wLtY9vg9wNQ$Z}VNfV~)KS>KB1DuZM!(Lrda_DYH_%$IBe;fBE|#oZq0(+N*F)>g!@7OG9?~o zK#xl`AWvEZyMnYa48eYn{?FNxUd-de{UuOmNANO1XJIXYVM#Dz!bnOLSTZyKd4eab zjaR0KI2#%#TwP?{ebO%&O>QFeX z)t^!$c9HF32!xM_Ija+9lM5r8MqM}Q>!tcfMl`pR@ zU*6nYogU2weQFqnJi$UnR4=D@UOasF-FF^5d9j(G*H%DGUJw(!Xm2z>I-%k9i06WO zWH@3$pd6@etyCb1z_;b)VUSgFAb0_R*<}CZ;`-&))dfg09put|ilZTTK`P||5=uXk zMM_eIGoffnotX!KR#_y;N`$!(BMdJ{J&4bXxpFC2F6Ya1KNt44)*e3C*e!*F*~!_- z$uZFhmA1p{4?p|Kk3ae7gX;^z!#*Wl)>o;PbGgj!`r{vc`yc<&AN|SqA8zcXa`ek) zGx!6F#^e3t)3ejFi;K&v^W%dV^JE)>qlb2*l-}9gA~+MqWB>d9VPVgZE&iBxR)MB-U^r>FZ-=ru|Nz8Y(ep)3$Lh z)ndJrs#KEYY9f<){(SA>i)_W85zQY?CnJg34=+CY>Z>omdUbhqxZDTlMNuFp(QQ} z?*J&%6)tgqj^~_c(?vCVBNE5c^Q)WpZ*N{+Ua+{6qXRmXNwZK73e9C{@XZ(UbahvF zP>3=g={ljD=i~p8Y0zgL9~%tmZw0_H8kDMZOEcX@(<&Eh*>o(DdeZB9 z6ySOXXV+hR^~D#jP8K6pJENg{PQlxvF?e@#{mGN34<9~whwa_>-jkPSn<>)%mA?%; z!};;q)$Q%etJ4FDg;=rqgj%*#FY8qS(o1D{2>TFs(j{8m$BTnQ0+8haj6ANp+u0dp`8;@_BXnX(Qn z!OasbNINn45sb&S$#!yncB+#5QID2rVj@{2$^`fFD1_9V3n-&^bO)dt zct@-iYz+jUme9&`q*t9hEuT(oZ|~9O-RhEH)p%wn>|;*h9d#Bm*P|oRuH(ZQM3C|1 z350YyvGL-`dvCw}-ovMB>zf-d)+kIRD7k@{avv(Bf<-J2PtGsTC6T3`K|zBy{a!nu zQYg-cC5xZq6Hys)9w0dt4-%^WfGXV#{DQMlVR392RbV%l-QHnyJ6Mq<-K$=Ni~;m)Z|NzhRLaYgq-3#!Q+xA5wIhN zWV5tWpd&mVrImAgJ6mh(>o3;Up1*kh?D_NO>l<6UJTSv!Qz~w1^>`fM0M48o9UmUx zf6!D>)`?2w#OUKaf#?Ku!$Q)*A@D4IMi4he6u>sT$cUN`2p8Fq2ZvZJw5gWMg$n+# z4$*NGgz`;2^jn<((>fn|opwkqfpm*zcU)c+IJ)C$mQ~|n&+_gbX13O-A~bsh#bu&9(Kd7cVwnJbC`~*^Bi=I$I)?$NTcs zBqsA+Oqf6eXGYvF3QCm0qhJky4H5u>cJb4APM*4w3LrIP zfGLR+=unBMs1gXuQ=L@cNVm96KI?m(a2)keW&XyG>c0W*T|~c;-UdMyP=Dqz0u!DHcL6v_`iYKyE3@#>w5Coh|(M#4!p5{w|jr^mIpAbk|VkzoG zs)_-n=u+XK91(ibdjQ6E7rRS5Nq{9`oOpbIU*KOXjgFwvi$yaO?uxF9ejjAj4G&?-M3)L4VVI3I za2yRATBAdvl*@ya5e1S}LT`Dlh+>wo1Nao*hU>tCBVuj-6BXgbLD@)zD05ISxle$o z!1LGx(GY$@X`Y1tw8bcNltcn>lr9w>LL$;7lp?ZJ>6XGz3C_V+G5k_!mY|ckU&VY# zkmM<3IsB=b7%RtL6P94HFcXp!;Em`KqM%Qmo{bDcntdc`rXnC!0cx3IUZgaq?(dN( zFdt%eu_UX=9fKjM41tgYe((~-lzc$t0=!CGEGY<4B==K=1{MAat}jupyo+2*s?3dm zC%6@-QSu{7R)H@A@h7B!fN-BOy=;h<#QSs<5ERQsAS{UiF)pT%P7sUQ^{Q$_y+ocl zk@>voR5ErI^ z_u#hpSE@56gdBbOXyIpq8owq_#1i1PSn#Td{0#GwJ_gaB4K%SD+&K{R7*J6~A2ACX&jc zr6Hx&GHrMjgr-fCfKnW2N%N@`>PUM;F?o`KxijkkoB;lDDWvvVwjReOR6z>RLs9U+ zw%eW_(^eSTE(V&j;UGqd20XYS)000Y&%)6uq=$82esm*pn_X}1t39%)4?1eE~PL9!SZ7fs|u!A+SbL=T3A#OI)HXc?b5$@1x0Wwl(2=@l-* zX48j0p6*8`(1Z!Fh(-!Om=ho5tB{waIs#oVx!7y)1U4P3ObMh0?Xh4k%Z4a{n3>SR zQd>MvDncPq2TiGvhHmW!UGR``SqV!nj8b|B{pKEFRAiYv6)377+6I;fa+)nVf*~LLhiI${hlBG{7kc#5i6XL3R>fHe zNs|ZXV2x-cvw>E0WsBJ{L;9%$p5Q|<(RlbFJYgV=fCbZ8JX&i+2&u0lP)ImOAuu>S z$60&iH=?k~;xH2k6^pzVM415-Lo1UJAc%Kd3FW-?d@GmcR+8|Jn&Yiw&2kkDV|Z?Sa2C14UVOaRsM>+P_{ zgM$OZ?jXa60gc|xL96D+7-TD^-J0Ggp>l|!7Q-T~r6`Tf+NwPF$1;dIg+H1PxD~hMGDs}Ka<_Q7oDn%)+>8=Vu4NSdEAc~xAUo3+qKI_L4^FTy zF}5;_FpU37R^dF?@|88oVUc!@NPxrLMe5aLS+qnlyvpUgNacnX>fwwl^yDjt&`B}4 z9Dpx!3B!o(lY64q+>;ean8SJmX5){2;!Yqnr>Mn4E)H-R#8J{E!y-jO48~OM+VkiT z7SpF*iFydO<@sBpH)3Q7Mq{E+>9ml{kGNbSJh6>nt^zMh+X%6`zeYW)DM!V+``lO-*ZRHkT0rkxbv zkfMOVlM<=Z+R8emFIgZgL2t>oY2hHz9#F7=w}=}81rem{ji6iD#v64UewGHj*3l^& z=gUDcqi;XUrP8$V5+{-e@G$@U;Pm+P^7#1rc)A>S`$Wf-ksH-QDy5pK)4Rz%dXCe1 za6l9O2{zFN-T=v^ufsCyC4PxQFpLZ>=Bf0a6#wbs#9s#_WYl+NOF+m8T?Ld5DF;iH zft-aJBhUdk3-AW`zqBvYV@jhheWqX}((p>EK_*|N$AstQ>yl59ml_097&pQs#2CL~ zRoi~G6J(oqv1Zqj`Rs!y&mJbt_ITDseqA8S4{rEfr5o6{KU?&A3^M9QPP0szcrR0- zCu48p`C1}Vpy?4yL3$wyMT3L1gEpxkrddy=R*?pJY2KBeR1(LFIsrxvCUdgOA$>HE zQi`NJ-61hJ;!Gu(>LPV*8udYfNktXB31k2ajS%8SMP{YsMj;q180l4zisVdKBylrH zJW?;ZWooWn_uPj3I7AROZ4XFx<7qNgiHDOhkA5`$$|7emxMm|kuTkg5Cfoc9l00mIea7O^N(FC3 zo^i~id^DOVh<0rFd1m^((-{R*R03&pOQhJmViCunrg*1!t`K<%#h{Yt5Y)M-hDT!@1BsJ%J zF&UDs1BFRPlGtzci&N;EG)f6jr5^ndHuK2LnEGjVl`6(P9{Zp}z~aFm&`Git{3J$} zjykjwH-I*ZQARCAL**-vKG3G45?3mQHl-1%ZAq5_59kob*gR@Q7d0SJsu>@eMJ z)A<1SpvO}(gTZ*rPZN0^dANwyw@6$xp|2{K@?ey-^zm?%{QeTk&BoJHMXWwW7b%8H z>oo?AM|H@d$%eU_{7pG};2~WzeOfK6Suoo|8NcTGh7|?v$@1{@>I{4ylwSQ59u9#t zpZ+(BLXCO}92plR4c%q#u3Wl=4Okn6k_cMCCZ%*$>{pDJ0xJ)Ls7z8^2cAoQTuVF+ z*dMi7>hpkN(lkhK7R^&JOcpmZ>>}Tx6psTiP4pStN7liOh~;G*!AHb1Lvm;?bsQLv z>`Up1$1W%q0u%|di$;=Ef|(FGvzL@as#QFQfi>hg8{oPcltRLNo>l{$!=%!|FcqUJ zXr0W4xiS$+wFHw_0fdwsuq{ABsJrkP*iDd|RRafRfRE}#MGQE?2DA+}Bf$?L7k)rJ zJ|j*3{NNN0icc7HKGAf+p}VdHWCm1eRaTc`yF^=rP25Ea1n4%dOz178d}{cVM5GxC z$R8yXs!0DQbAxgysRXucif4$eL}&6{A~2~)TM`bd78_|CTouhG!XSbI!J|PgQB&q1 z9a$1aVQN?wl$Os@bX1s?OzUof$fNLwR}8@_;F2ru%(B=)3RA*;ZqMrF$v2$&*AxNp zUv-{}k;H6BlLP)+^dIFHH>UoC*chA@1t@)}+OB#K4RVMLVr*rH74266txy6A2rtky z`7bdY1VE^n7*Wy6E@OZ&Py9&_tTcTn6_%KiC<0MpWaVl_21GEGfT$0GM@iJA_Q**> z3pOzkT*lc#a^e{RM%Aq#M3$l|)E7ux)lAg8`ljr~YIG$KNCU)BK1WbW!TLlJN0&toDu$>WhT6#IP&;QXo=xFpSJkpk!26TyR^xifF1i z2-SogG7A=kyW>p}LO@KhPeK`WQ0hC7LS#VVb~ce2nLMOWbjOdZYU(I~7)QaLWkX0r zA0?<$d!@lZ=7U(=%LPl3X~;x`D@s5WEg%F$;6LAl6p9Nl_rIj*YC6uDQO0k3*l2S=HA|&85Q(^!q-w?J*`XFbh7g8{^MyV&42#~H~g4Klr z*{n!R`p3P+R*J-`O-!lKMG*(8@JUoe5lUH-6@O4o=Ciu55L?thXd|1dQ7Acq9w;kf zA_^-;V@8%@ITZN7CLVaL-9m-vD(Ck zeZ9#(;S#4Qk(D_Kab!~>A!4%<85v=$&DNW#l$*Y%)K#d-BQMSHr%(xybQ~tFR9~D!+^HhyaOpD`U6P z0~V%MpytdV5bcg`LOHdG+{gwUSMRIcLR+Qd!b|3J=YyF3{XFj9zh2w@9$BMoLa_UN zzw$031zAKL<)ctpePUhASg~XErk1*5i8>J?%ckg~XtojrB?-)S^(RYIYgIE>gp|pw zwz~m^)r=zw@Y^;Ki#zghsc-?Na7#s)J8H=z8pN=){>h*;D@sHU9YxX<`H0{|MwkS| zm$Df-CYxf4EKY{t|7x8wpsG{JB$cUJ5okpsMd#Ip>K?qXC|TEPaq=pZQ3R5guQv`z zA%=shA}19kn8Tzb&QdS3<8J$PeyTy4^!*uX)cXtW-@iV1|4GJQNiPGlekIgOnABIr zKXE{^32}u}P*_f05nDc}#mUNE&rfcz2qXfeHu)x-eBp&pZ%$UORzk0+%Ig!n7iezh<-=< zH!i)Kfb5Y8-)};hd&1zg@2Dh{`t|KK3DtnBJFG^KS4AkHl_J-QX0M6(CfUBeWJSBX zsjSAk;}~Crdh#kSU%$%9?2t9wvG9%C-Q7viYPDOac}G~;@y6#jKB`kyWb+|mq{66n8=hff+@)S{iWy%bs=JEB54E=i6e7>LEzi}>a zo*>r=XJoo^!`1fsi`@V2`28{7U!N+|RQuJeUY{ZR?nw4}->+V^+Wq=IGWz|-iE1|Y z-+%KvH@IK#-3c-)S;F5qU+sHi7QcG@SHJy@OaBdD<)&W`Cu|K8;Lw|0SWKv_6>yZe7Pq2Ft1+~fB;|Mx$6b^kkR z-A(%UB!gPt8zZlTekFr%OkeJ*c%ok3IIy}@4z3QX;koI*t?mDD8osgi|8IR1dJ9oy zR$o)|jgMdN{MUVw3;4gf?#(;h@47$yw_5#g8d;6Vyw$Pabk~)%{H7znZ`iw?vlG3{J-^fH@`PlFEf#8zvid1yGvKsude(5 JUw;=0{NGU;yv6_k literal 0 HcmV?d00001 diff --git a/action/sound/user/killme.wav b/action/sound/user/killme.wav new file mode 100644 index 0000000000000000000000000000000000000000..2a09b035c2a1a3412dc58df50cd5aad3ab0f08ac GIT binary patch literal 168588 zcmY&=1-Mm3_x{9=vrpY1L@+=>It4D>4bmtnNC?v1-Jqy|Acz5qlz?=1cZmr4i6}_j zI(^RGd*VNHHuv{^|K~pMvu4NHd-j?&Gi%Me=5}n^tl8XU2)x(u-KHN78(TCL006~b zT3Gkc7Lc=aK5aqdxw-T-RcRAChn{3J_(_J@Nd|a*KAj&; zVhT6}Oo1t&CY%WOp$VuajKjTXJd(&j<8XC64(~}KjH}@=uA+<;axC7560jDWBw&MD zP}a+Nek1V?i8FK_Jum7YafUj_&FDhXjOz3P(dmE7i^zF8k2U5HoMwnO05d=Xz(8F% z71lyFJOZZ)NNK}^XfiqiC!^YkzWy@!^vE23rTb|cRGa{kTMxtF$FvJ@08N7jL>m$Gh(kANspDj#8`-PqC z|G)Z}@DL=RHk<~I^O;<}uVGRG(dM$fXNfj(%6Z3*?I7_UE+F5>Swkl61n^_xd+UJd z!p5cxN%>)5fjYuQq^;BjlfeODBh^tHN-6DTKQuuNH~}6M*N$+;>Ly|HdW_S0Y#KYl z>?CReiy!8bKrLW%`P*7x63Bp2xKE%e8V@tzcp;-nDa#o!A?}^Z>+-4m7z3<6n?+C1 z>FflZMNgAi^f-g80b}eqn+oLj9|ckeQWsN_{b;{vD`~5{@hGLDI$W)!gwPJ0i!0-i z0%MT#E|VYPQ_EQrB%2P7!0BR~k$p+_Ehdg7WS^M{ z8UhKF>>o`z1}DL6&=^2?3d{iw;T&*Elp8<@8v{k0pDnHfC?WgQ3BX@K_Aw~>?hJ4Y zOoer!lzEV2n8cL-v0(HfTREmVc7Iyv%(4C1$?@tKm+dNT(gKctR7Vyw(aj;RSD zB_=^xXXp2f%iqa9-%#|!qul9}&NwQ^#M%5L2O!T5oW7lBPn_3`Q_XAUC)PXGXXZPW-@0OX>?_t{`+sdx zj_s%T%p{J%8SogS0#nf;JQdYNHadtWDAm-MvR|2mGnDa)gGu-Ro`mHXUR#X&&e$() z*s&wo*Xt$wyEE>|G4h~j6KCw1D#o6f^c0yxB+l9r^X=xgN}Ah?tP9pWa)xLEQet|N z0wz(96JAf~{-}_0tg8WIVr(0S_MkATf=A)qXcU&?#|}~6frl&Glq{u!l8Y+{xkVkK zmQ%A82Se0t;`|OA5@SpZ><1G;br=UVfz9f&8MH2Av;l=lG$x9un{KzXbo+wkw%V9J zvyJ(&c{SlRFPRdC)xw%@pS2azh|0Ro5aX*Q#$bs!tcGJKZ+@<#$T?b$cM>67Ss8=( zq6m^>^!*n9w&rMdFC3psig55C2Yj7#Yfw$pkxCewmWi$p>g`?5iD2OWI zAlii@a2E>TU1$v2iAUqALRQ9O;ch6$SxabQmpVNeakcs22yDljb0NsI&4 zL7eXe8l&LIuyOi`XmkBKq; zFt@lfca6uV@N|4cnWoeesI5*`YpbSuNS&hAQK?d20m?}{8#l)DP&4F$t-vC10k}a+ zfh)|<-VwM?9oe2PBR7deDs4xXl8z)FxosD;yV&`XWqB+iuS;B`p9%RHmCv}s7W3A8 zF~7)Npc$M88zTjs19M>`gi#|j3!Ok*0HKCSDUT6q0_TI~z$M_wR@}oSF7gGS1#q5s zg~?-ikA%TnGlQS!3wcX{|4I!sfpehqJQd3Ko`w#ZCZ#;5itBkm=>NYikDY7hfPA-v z0WNVbZ_B*N?`qALu(m9P-JmJ#D*II6W8Q{KnIg`6`6X_E%goI$vqk(oSeVp7GpNI6 za3MGkRCEq5fGq^hK@~MYbI>WFpR-UwjnO=DPoBP9An#i*@{d^?=3}xQByNy&@{3)- zzGD@zf3d%?JCeNik!>Q|>(l>38}>1i&vCE-G=+I>FbmZewo)HYQ;sMM*ABd|y+9@ku;GIi^shj9~$UlcsoW`9C16E}6>OP1S^kI5zCV$InidV%WfEK~TYKVi zSwA!BYya0rlQFNnU&c0*lJSj{^7|#E|1V=HSy$n;<1Ht~mb#Qa zX&n(u$rw_`QUB{R9tP9+5pHuQ#>tDDWIQA1EyiRlbzH}s>29; z8;yh2;W)S-$hmw1s0PQvYA}|Z8#BZlxfeu|bMa0wAMb=CP$e`P%DK5Jh;oU2e1e#> zO)-~Ka*R+?M@WhK$+ATcQ;Qy8<9P<3#161HlXLM-I0ney?gk;S1BQT<5|!adxE&3} zdQ@As)JF%mj z+oeBpm|02G6#hz0K1Cq!cnUj836+pO%W=X;1A!xykvc5z+7xz3_$mj5&mjGVT0Ft` z37zc&QfCgTpiyWC$`$n=19pLtumT!}Dxh5GeCOEz`_|oH9N)vo@wdTfuv1_U5A!O( zK{Xy@5(nrcdVt1hO*(}fB2&mwo7lDJRC1VDNz|kkIY47f{#HI~Kg-i;E|hwzfU;0o zJQ#1r1C>(BGyD^JjyB;BmE!7Sya_#rKS7D1usqBGKZ78Dn~!7F**Nw#kFdRL0^LW) zvTAHBtHQ$q0q)3Aydntjo#NacCeKSm=q@&rSLPumv4iLGZD2Uq2C_g|I2e{f15qjT z7;QuY@Ou1EU;y5T2B9r*2;2e(q0(pwEC+`J`P_;i8~g;bML8R62O|WsL0OaqE5MOp z7azlSiMmy1j!x>@+^lEKG>@B1V5Zs7L;~DAYE8F}T2t(r#1e>;y;RmO$Ry-*%O&6Y z6qUpSl=aF(1-M@8ZZE{&g~a%ec*0Y-r;0z1HH zzK6wV4H~2S=_FE%BuEC8kmX6_02xmY2r0|*{6R9A)E3B-wTUU((X17v$_g@e&%G!SpV191tZzfwYZf;OQKadG7z}^N;a*rLU-eKjlaK z2>*y5if{T5mqL!-OT!_c92m~G@ho0}2k1^3pxar1R-s{1jf9CKV|GCC<8x&CDDhd zIPQl_p#JE4{6P6ueW0vU9w@I1`MUZy{vP*HUQ_>4iYfnKdEfU~o_mDeLIVT_!7U)0 zRb<($93Rehk`c5#8$m145wtSNCEI8g-NLeHS@w*T;*a@8K8U{s9)UOE03gd7ljlBS zWoZ`KMusO*QOI(12rW-??D8a&lxBn27W#~qVguO*)}Oxt9`beI0eBrc_!jm=g>g@` z21#_oFDc#dD*PKRsB}>ZDtFOJ+*v80+`(Vsd*~~44;8{a;H%(1e--ouuY$kWI@*`M zDNvkzX#Z$Gvfm_+?6>Se)<$c9DEB4n?Z?)7dw{*({?Pi->Sq@tf01J3ANvQpzx_S= zn|w$6(AQW`zJ~wFz7?`C=muVb??Qirf9{?*)pHKIAp}mq0ICgmvRDfp@_Q&{hNI4{n7&LGPeX&}HPtZP2Ii21rSw16a;)@%;Q|_th5Q(`#0&9xY~U7nF}ea5qt<8%yaGMwD)}zY#47eK&kug(%fT%mkp{2BC8#ZO&>DNOEMIwX z+!m#v8!#RI0+vE4Z}S!WUH%fk%T}}QNqjAIF7^Hz{SPh7|6qmrU7`QGd?k1f6o8#! z0r(620y^!HAKrq?#ku9M14>7~z;x6BrQo)B3BHa#M-sPS8fuT0!A@{RvJHRZt9Umq zpShZM=U+4Ve0eO}_*al0=AjeJFWw>B{Uso;|A&4EFNvKy0FC9p66ihZp*5holaj-`*6zzYUhboA5L7z3Jkf6wwAL z$hlYQ`wJl7-Ic$b#2-}FyNJ+v5%wP;9XQ`7zfs~(`U?A<6$1D8SNt9?l+?e}@eL&V zK?;_#9rh{L@Mrj@=ywiMamVEKrKlsyhi=1sLM}y8pTB~YLdUWlSBmyr&EEs7MLWng zcG^LpJ9!<^s9?{ai@hfDoRy*ToQ z(1oKXN7ugqsUL|vd-bFC@H2Q#;2rcCya|?pn;=yjr^1dxwi8mGleX&EYaw=z7GmA$ z*W?dUnEpXl)A!hF+MPOhj}>Chv6L$Xq%6QYbBX0i*-1RFljtk$VIG|K%X+kj%YZ!p z3r`0U9e|WyaHs5yAy=VS^i|nsJ;{E15lQLv-FKiLUI%{I9()FF@_f8w60#3}N$-&_ z=^a{#b|;0X95>z*WqGbka!iqZd2U$;9A3wSdmx6ZChuVm7;}Vi% z#sb__!Ri@BQ_f>eX`-s?ImM-1#EWrjyhPmpxfl!5pd1%EgMz#(bH-Ga{oz>fZ^@UEqCbGZW6Me3$==-} z9dJ7CjFzKYC=Fl7pX0X4`Rr5W240G9;1s2;@;R2*$tSI}Neoy<+;`Fh-(f*hGK>sBh$UstxJSHXRW3rwOpl=B{@)3Q5KV<7!U%rm_ z2H%3-peXp8zYhO|--`Kv9rz2bh3}(pkVFss75>i)c~6w@;V%`5E^0xwt5Q(yqJE+N zq83o!Q&!{d_-iEb7g!7a0_(&(O0a=+106_9lYz7peMYvBAulk9=9RbES@sqpuRS9r z*}tqLdrIHp{lO2QKlmQ@LEoYKs3`gyeh(cVXFYfb-hll<32|NCvw;uf5}VoJ7ygjr z6G=a)IQ$oE0RMvGuwU}`Z-D_o{{Bb)gh{-`2k?!;7b?x3)6y)HICYVA%d*P~Y^DQQ z3I3REWCQq{K;i@VI(h)t!H44gkNFSqANYfKhkTbT7l$8$P5dLii9cthz++GXK7=LU z6XBl?0P=b<^bcGwezzX=idT@c?byq zZ5Mvvb~=QY21EE3K7^O!&v_~M7?y$q;Rg5!y(#=i=}&&A^ijT3?klgV_m!fG^e-J0 zRsKOgpnqX;)DQg#AHlcaAg~b(0$cbqUY2LEZEQH(D*VnJbQIl9Ld21y=+8<2cnr<+ z%`5Ss@QWifFJ{}#qRDt}chXl5F)8;G)0Xi`UYwFa$J44W?%TzKBGwA>N+KTF3Wq_7 zvPi}xGH&`;#3BD;M}8<`jwd39doE(QAxWQq8yLZN3g27$VL_wrksyZ5bCG zwk`XxJ@o}Fan6aC4kTl>@iZ@9s|ZGk*g!(Yl1}_k2FbW#C@g~p;f?sQh>Zr~QutXi z#*lHw2K-diaWE<;;t(eu846{L;l#KjL51XZJVWwZN{Qb)@t+e%Z4}Ruv1VB`OvIa` z_}j_YB0{UviDW-+VU87x6FSd{JN$e4Hps3c-gdHmlvT*k*v zyuO=_;=5nO?R!K#FJpQctIJqE&km}I_;n8pCF@WbII=bJfZ-Y^=N^%WG zN{2s@e9C?ws7N^$RflqY zW@3`plItjv->=1;wV9ecF8G@`KOkf^7=E#)BiE`N-pN_JlH8NLUt$uMYdSUgL@w8I z>WK0IJ|4)ms|?`qO8W%2dYn`^{T1^ES*>W98Vj6YoIf?K8 zKYfG+e>Fncre>+z)FE1FUE;a+lO~a^mJ_I`21MRLK&hfc#G0by;U!>KwsG{=+NKjw0+xGB?^lNa+b zxoUrEw@V^b$W;5LwamJ0=CeB3pWD|3ZduE%TUIJ*M?NK21=`S$sl+9^ke%am!8yKw zpX1IN`Y8y}aWqr#R+77#Das5rKr>zd3JHyqGVSF#P;f5|6sVlSP&uy56ky5;%yB)1W64$Il^fzYf`^j7k=)2aAUT%{q7GN6pGs~* z@)RACoRj3AB;O}_LCJkdE+dZYspXYhs`3b#D-RuI> zc{47#1eMF2rgK7`V+#Z)u|V+kZow}sqOIsBf`{-4PQu|TWV^IvPTMqs7&ZkOIL{^2 zB+qwF=+WWLB!@TWe=r+KPUM*2NE#u@ZAnhhsq00-RZ32;b&_Mzpu;~o-y``y`Mg$w zk9vnmj&m7t_^(1q?n`oTodstjxvy(NHx6GTIV;Ig&Igi*lQNb!O6@Pd=SLP zF%vwkEx0#ZIf@CctxQ&GD+Eja?IZ*O7&d~lk~j_Kf~EpeN_)J>qc)6fw#S>#_N za4l?$@-$Q%o4AHD86OdO4t3ECkvAjrAEd*8$|K1#MfH)BtJ752!YPrz;n>5%k7F95VN9A#El&sIvm{`FZ@2lZ!}`T+*>*u7#6#Ci8S8HzjkH_KJL^>R9G4$>SJ4 zkjz8$>$vvaHP!3vY?Y6BXemS91>%q%(ps0xyZG0Ai4Te$=rzf zb`z1q(S%5eUfe_2Q8q>7Fi0+BJlvb)K_nLO0MXrQ|%yEOs z{FsJ9=Q2k^%Dh~?|MDYpZg+A;WZuYrAbkRdCz0=#^LrH(c){TW#Jn3o6_jBjCr0LS z$h?@5BG1Fg`*<5k9Y~(a@hhbMYI7oVDRb?de7lCpddU0?nXjXW9FEyZoD}jT0kjdB zMNW|E!bh1->kHIj|Jw#K?<52(3+|@8l7pSLmu;S@Y!>amRY-YW%B@0H!XpG69Xj74 zbtgGAQ`F0;&wuSv+K0?1f=L@_L@<$cX+-7cAOgUp+ezJue_j1#_85bwf4TuI4M%Bz`b8SNu&llDa5c`|?KCv~X0T?r)hCUa$E z-jIZ2Z<4E;Aj)=<>yrMOgURe5llC)-NuNmiIER?y>(qGR>m3mJI<;QhtrpR`5Efk{qJ;wPMq$_Z3N0pU-M5}a5r+=hnWEjSBpOY(e@ zvl=P5t32DC@Pc2Qz-uy#N?!4p;1%l=Ku+4T?ZyHp?HQylo%(+~<6a&SoLYqM0zps- zN^UKXL=_-8t%%^Z9PTEMyUOD&ocKWc?=sGi9E`&~N&ZR35)MC=$7xAk>whr}NX}u$ z|8NsBUZ_c?*mXp_P{*Eb)v>8n-=db}fohXUNk3ou{nGcY401tvG!&J=gOs<_C(0(} ziSnb`Pb;SXrN3(QG+s6CyVmOeXluo>ls{6%KKWhpzHKlI%GgIfqk<^Q_$n_JlJQVQDBDNI8j=H* z9Ogt;oy)pd$u>PK+E%vtF)~Bc*QvW~|N3M)IZ6nrO-)*Z#n^t9$E60qb~qH3N0~yO zA7RONmQPlSF4s1obdhXRk4jml%CggQ_ujWgl4Bo`%Prx>YeTh;-4yx_nj z_Y|U4=>+?bIW=)OLF32bGvajJjEWc;UPa4s2W2__6@3Zsfg<2f_B#83zRvz+ zYq-OVe1UE$?X?a1K)tj!RH=Y=05PwURn|ME-@KmiS*>jsy9GMpY>{)Bi6vhw?R6hD zMa(>jR2O!;i;ooFm4yeZ8Cpc&X#|X2uBf(OS&KwuBUa?zC9CXi_LsCdn1O##RqeR$ zcfF$hfx3fg6p&17m37dZZPh2!*cU)X>LYk%Hb#6^lvJf5T1hZuKZ_NE-Qsv3nP}HC zkHja&t0%(NQ955_Lv)2dpuh3!>IcdX_%SREo`BL|7#V5aj@PvsQ=89KJ~S@+YWnth zy1RF1O>rl_!5$PJ8_o%~4i7imf_+K@cVDmS8|{7UUg?^nmBMfEc2?iSE7oS4(0pKw zh<4Azl4^Usq0z=G3w0lv!bksH?K*n@Dx;FFwAxyM4s{9AcjDGmp)2fQj|I9tEqZt9Nf|Ed?13u;T{ zKJ3JPN;C_0%&D4tCQ`|sz}w>9>Pe*_UWsxAAJYuHPtKU{m=@ECdx ze2IT4auVLb_tekuDQ3sxiF;-(k^w(9esTBFyK0{c7JCB@u{q|F;J9b+J#PBsjqGQ! zp>_pyQ+=xCs6Mr^l29(fY}VGQ9lsE-M=zrSs56Pjn#YPIo>+a*F?ZCL?y2S`?pE&S z?z`$)em^!a(ERD3%xEyf4#58INuHVRwZ?C*hnp%Jv7h3%=$o*T^+DijFn@S{>;@0J2c{fJE#XV|8osjL z{n|~~H*q^V<;mTD+h!Jz^dXz!Y-N*JkK3!y@=r_o%5NC$SQ&GVRgiS1$KrT0_0wS2DhS5j)~ z_xW|O2|fm|l3vlik&B6rY>B?nKRLZiTF>;6X#o<7BG6P1qtZ4CKgit~S`@w>4cIMlJ2V`ApuXpB;O%ehfD3I9)55!>g%i8&iHSjx zL^PGGMRQ#j+~Yj!i~`0S-!V_7+Ll#|zm}**{^m24UyY*1G*@?zo$^asO3G^2M!w5j zn^S;YSawu4Wj6KR$HC{e*_tLwAR%y zUEeEVyJ>W5@I+{TctxaFbW3DOtdD8)Li%k38O>Y`-S^zK(vH@*rpJGZJxnYyvsfBr z=zucLv(!H$rM+i~(tv%LIBN~Hx0o*_zKuUjZmjW`7_h=Ke|@%!6b* z?JD-6^xy}@YHEAst}EBw&fQo06;!cx^Mk~?SW0w5xNqoms6_O2a~*3fGRPO>bY&I( z3S`m|=KJxQ=6SQZwbmZZuCe^^H9T1zto@D4fri!qvyAnJRma+9mKWK`!%J zWd2ckIFeUkV?~~Z%p)1i_Rw)OgUER{ZdVsLU{A7Z*;9oTPPGo%&YEk@q%RetRlzQi zQC|a2PG;6u6|?$YWwcgF4{EzKiII9`J*4f{#;F-%C%|DuVI4Fbo)9}z&WM$%rXm7v z#OE@(bF8I^jOB`zlR-HX$W^T4Vs}ezu?y@tn8h12MPxKOkUM3X3p5eC%NlaTn=nme z7pln}XEN)u4w`}_4&uqlNZonoEYJu@V6h8go>*;>J74537+Dv&Tg|y9cfrV=3^JlW z28mb&0I)GvS#!Eb>@4#Txntsr?YFO6awTSwSh;aCnJs#mF645><$qV7P9m9oI!9!i z%1qNa0vd11HGW>K+O%W~xLgsDt4mkO$5gIDGzD^nO?|Plv4Ecyt0ouO$7F;3+%6?p zA;}1pC4XzV0R<+HtegfHoo*+Gl+5?&cbC} z7hUg(xp%}5wtZ7YsIvw{8w@fY;;{R zwkr9+YT6F|psdsD8kcnqe=hR$<}z%bNt}rN6m1=c@eYX;vqQpfyWnEatEr8Q@@yl| zbv^Rm@)gp3@DN*#E#XHsa=nl4&j)fyZ9wHr9XA5aQDfhrjoyBacL=i;ps4`XfPBN8{P ze!QpqyOd-4uOerqpHU~ZfcuPg3BLyC;ZI!`Jr7-J@TOIf)xzm^>tMg^PjmJLH-xSR z-NA#w%aK#2_-6OJ{td1fFiYR*>!0>v${4LRM{FY8r2UDz!&PFenS)9w7pWa785?5v z5^;YmvxWVWwTN;2iLt<+p*5y^L5}{pH%-ll66jNK5dEYacQrMtqhHt~d{wz>y91MQ zc7`J1C4uSLgK}!+W`?R;vw0I`k?X9!NBvCS?px|F=PjiNaW9;uG|)RL0g;8mcmX&F zV){?GWptH2-ufv1Q)F7Sf;pXR29wnW9>wd%n_*Y?&%U4C{gsutow6B!q#CZJ`cw2b zC?5bRrf>}F zL^q2(nsxLcDQSKXTpkL=?bwf@`@vpO#Xirj0l!+;m*eqiMf9V-)~S=ct+i7w(`~!@ zxtKA>c!JL;>s_B}<>|Jl7T;j?kIxSMom)55Gr}T8t!L~6*r)E)3+u}?=ql+xYs7`c zAYNAdY7BV5hl=dwKJYXQ;4*Ld86lqBcZ3t=ge!(T@_Y$kyPJ+=_vR!HLlxWHa9YUQ)NZqechg zUt^x{UP^xN=lUtNkAB7YTkWat(N8Pi<2LwR{(WL|yt$QX7mMu<4h);I@*?iP!7k&T zMz+Xw8mB)Mo_2FJ02iTrc%E`g-K4w%u7m382E|JkN7hA`C32!x@NRBS_T@nT$Ru-z z-Hp7YY|&R~->WmU-tII{KyRcS!F^B-wWv~Do0u@TFROVc73ziAv`N| zF8Eroe9o6yso57ZL11>YZfv1B2yNDWLj=EWJoN1Mnx3|vBKlJ8U(Zk8w~dv!i_%Ri zihxxl)>MojUx&|xP^fdTbQs3RTc6X#xTbrfSnufS9^DHj+xWsbrL9B5l$f%C?@3&UM`G1t^MZ47e7Um%!y-m}R@@?=DNFE=XuoKu zO}=3%kNuN<-y3B;6MTMOZI^+2X))~y8<%(|{$65GJR=+n{1B`cN{P)*gc1c=Rke{a z2%mIacR%!foRZ@E#*2)ly4w$ZGqg^^T8`tF>A*<+aLdTlpqaBTt5hU25YBx!`(`LR z@*xYL|FHa^H}0ezcQ5wbcCXca`WAOlPj%N=zHWOegaOyFIB@R=3VZ|=UMBn>2`Uc?+tHN*AjG1WYQ}3kFj^7 zr6WbcaytBdU{`Lr>;cb)W$w%!9?A;gSj^rJ zO0jX^39jQR=3VD$;r`p0XH0j$=1Ukq;~gS*>lKF0*TYZ4Uq>ngCg+fx9)Xp?XOZsF z1Lkh9L->M!qBOOqYo<5h8SO3Oxge}8oKn;?SpUel;kpgSlF!Ux)&{d=Y(ex+xMOH= z>`ik+Vhx$AG{nF2XK0!pG0J*pdltI5{;5_}>*w}sw^@Maf=!kh9T*&&TQ#^SyJF^; ztoE5Hxt{2=Xv4%gbW?Au^>MHA{O$iWZGU=J+QpO-Da-sN)4oaT?dhwf8y>YFU2D#b zt&HXicgS6z^GnvaoIBxfBORh=*gIm4{VVmM>$x=BIT-k zjfd)Y!6y69gq~;`c@$_F*qBo$r*3d)sD11bD*;lh1$?B^L2YIbBi+?aJBobjw?-%Q z%sNe7Xfw$dHNzzIa%hjR=59IPWHkuP4!;zx8P~Z>jbMVlRA(52{6#rzjWf$%)Y-US1meJvu%1Rk(Dthxyzb!AIe{`~ocNDr95}UH#@A<-6tS z3Cd1 zFJ?S*|LBjUFUsf3KeE8~BHLbBlYe`G<%MRYPxpN0_PQ>h5BXd68mm+yf7Bo6(VB^% zT?gH<1N=MOLiyh4>K^XetJTy`YqQ+gcn1Z@NjQxD71^BgN%kjM8#32C>HPGA$Fax9 zvQ}i@%|&7z?>BP*$i{{Api#|l`i-=ZZ>HbMPt%IKmKno*oAg0oGGEX7ldbWz_^w35 z*v;tDL`K3S|Iyl@T#&B!aSIKntxW$bEtKN-UC)=ALTsGx)UYjO|z9XfOcZD&`D*O>e75*pM4l@!9%V6;min#6$sbNEdQKk zoqdX*`k#+}cJ)d9tbTzu@uPy#%pn8SPd$J6@1@PlpH*=EOM42mC}CoPaZ z$X`G&tt^6H(YoOfK>KGgr8En1HE1E0WNW4Uw;I@3X+*e$C^{^)+CD*tF zsvx4SK%?#Zu^sVA(bA!%L051|zz(bm916c>ia3T%r`g&P_Ya{_|6Mpx3et-7?4zR$JJ`&MfEe2wzA z^QWhLTd+&MQQk|&3U_PIb*&=%oWE~g4PTASh))Ptirk5uivPfKRaN9P)YlidKQV^8 zYkDsDb)$+}!hhELnsNzT7OV7rn@77v&qQL`C^wXQFmp;)gW!bJn5g= zA^0Gg5`CV#C{mAsA20rtA`VKu6#!`e*K-QCi=uy`mp=*AUUxI+62`qYQ>WSiW!~+CAJN>%Ghw z&uctg{G|7@%xC?w+~IG@BWj2k;XHU|JoM#Hx$k*X9cy&<-7_+jxUw4drnkvZvrfEO zbWXTej6^>Tc8bm7PjGvB+N=t%8g+ctyd#a3zMCmiQ-2mc?V$S~&&z3LQlDxS!8Nf5 zD2?qjpC&$tP6>C4{U<&(R>k@nmQibIJ;Z-V_{PZZ+wIf5Gu3IRAHJcMM{DRz^RL+0 z=+w}PK;@j9&zofL$qqapoShk1k{A;zldC6Mfr)sC@;j`hFV+UQ=K25dG|}!EslLvh zMfe&og-a`UX}QGc__S#Kc)(g4pB1fT7u6%K)$Z4gEBZ-aJoQ|GiutN~2a7u2Ny)Dp zSnLyF>uqdqjJ*~PaVwS6*TI5rudh6E-1gK zbKPyV8T>6XW|kz`k?pzP1q$Ze&1o2_lv_VqhCYgiVr@V{{l3U#KBa~|o&C>rzy7cL zw(GEZ%so(F!`GNUTPx_y_^R;Bagrz`1XZ&%lI z?YuHxe@&l+t|vOhHz&r&(?fN#3+Mck(=T)?I4113%EBl3Dvu~n-3wedb?jN`vGg1G zedTxkTNR1yjVdr#Wbh0}jD941Yo9&IikiD)CCJZs3+tBnkc81v^`bIO{XoxeEEKU+ zb^TwQ$&abk@b6Yeyr@_YpAeZFxSqQaFx`nXy0Ph zSR%68s(Z?*r-Vl^!SxWN*w_kNMXYAwj{_{!IMgbT8d@0qInh`hiLbyKT4nbKo)!MZ zDIccV?t{uhS252KJWuQyE=l*rYew!x7R7kzSZ}i zA3?0|)K9rT@^X={{iadKonLu__OgB#`ToxmPoh=B=Y==eHQFLJD%yhou2pw2!K?nQ zEi|5c)_RF&lUj@Kh6Ba!`4cdc3`vx)EqtRH^E! zqkk)6@w!Ggy@Thj_mHcCYm@P^dx6#stg`>)IvE}57Va6E6xwr4j@Zc=(mvFFHQe4pT78@&{LqQmAR67lThr!=?y+;x3^CqqkNM*tnPK{?%jfT9 zbVbFq%bvUb!rmU*+c1M)7kfGWnOf3ZqgrQ(aDIf=r8 z_dX7K_?-AHNun;eYEAMu!=k_U3XNMtD^^KRoN$rRrb%;JHj8|n!7i1 zNLIPf#K?uPkM?($_4d^>KtAO^#$)%(t~w}c_K2-AXOcfaUwcDXiKmc~cHAz-DvAG5 z)RS$8QS_Ix$#p~dkPW8a;lWBVb()f)U3E>?mbym!c6l}LI`1#8v3Ml;!JKSmBu)qB zX7>)w47LdMk9-nO0Y*x-w2}JHY9oD?r$b74zt=bpkV)*C=oY_d-AlX@-ko&x6u`vseiO-LU_+IQtd5MmV4~e`LuVeQT z87F-O=dsc*MXte0MjzHUYz9A|mhugrB6h_r!0XWvPra16Mjxew-r7?@^Aa`kMd+{a zzR*v>!+~l!lLBAHHe2PCX`b@#AGBTiF|k@OS-T{%YP{SNe%W zoJej;?w`5EbEoG#i4^C{wH59P{!6Y$>M6}q)8Q6kS`)0o=10-2$a!-g`$+j&Td(v4 zPeCtbf<8%o1>Cadn70ydCx|rz&Oku$KV8HKqIrw@o2A`JYm<`etLMI?PY{;70*~N- zL_0;sBz`uRCsszACbn7S#0(ByyIo(nAGx|JMZi1cX?$8DRg7lqV%5S^qu<%%m2U3V z#*e5V_#HeKyM^AN8xotN&w{gZ7le8wCMs*tO8OFC;yP`(eDRcmDF?i#^ySJjV3Cn@ zHZM&kN8b$OM&`!vmf+0FT+(XFZj%w%D1T#wNvVc+AnH5^$j?I$LRsF zYwtGL#Ae3ZhU$h3n}3o@kTcm91LMSz$G+U zui^Q`{ffHNu4k2C8{jOq%^n>u8?7EcOuFF~o?7nh@MEx19pqY}-_>U8`_(vH&C0S1 zv=|G6d`P2x;#H$1U&GWs&T{4hs$dHaZP2sHdfgnxRM{m zDq~-DiR+|NfxR5B9BX1_i~maTra2^fHC}^#sQ&KR?nQX1{e;W}Lj_~DlIHNCL`f8( z)o@dNtZ~)V8J)0enBx=oMF!6``#rOJ^j1W*hw8D^0j^!3Jx78C?d~3H==vYZPP&nd z<==>1fl+)`IZjqaMh8p8PSDe=x^+4}JyDo`jcd6l`uF-i)(fZ)&{BSpu0nH_Aa2F? zn?XCM2E_Q8>6*$G#?L1<#kX54?0=(|1Ha~kLl3N5uH$H*=Z(ZEe+#;V6-=pwYrSr@mKei+hR=;|{%t8!{dbg) z*k7=!Si>k}6j097>9P6IwRRDhffv#yc3ZK7>Ky1yH?pskMxLd(tF_GDO7@tmtV?i( z)*PKR7bX6*e`lTX5p@TM#VX}q&R!lHFS6{GvfmRn6|A{7+q@p{PiBLv%2DI6`!@r@ zR%na&TIyHsMtTEO(#nj5qiw7%Vn@*fx{j%69V(B%aOLy&_C3JWV_WUzz*cm4mp>J` zSq+pTu4SIRhRZcyT?PtS7o*RE{iAae(fIhpArVh{)Lhqa@6wd_jm@-CVtuSo0+9-| z7-?-DkG(|tXsiACQb!qu;R*YGVoBtD_|GT}rv?^hU(0VK0|KtpA=rw zsQ6z~XM`vIF!2i6K}O?)?n_2JltJ5*V=NmyMCaI>#cOMF?ucb%-l_CDs?=$zMiQqJ+CX7 ztiI)qwMz_QcY%)6#Q$9z8G|+ znyZ79liHuI=|(@LlJ!~Q20E>+P|v!6@<*aY;8yTxBEr&uCw^B%vqz#YStE$84NAGA zud?IjaQjtD1J!Z5mZ4U&zl=2%U%eKsQx7ZY{H1uC;Jcxbu)Upn|KO%Rpqw5EOs}zGo&YOlGouscpz-aS}D*|!hJ}| zPZz`&M@B@`Yz0r&dgDAjRG!uPufj;O!>KdRm`Op4=s|L&^x(OujvxVvj`mtesH!QI{6T^4tDhaii) z2Mg{HSQ~Bcs^>KO&Hr~jFY20sEZLr#ma22^`*XAPVFhQ1?}dNaWr8X}XV+LC?XE3= z-ZK=NmFCgMk;%q7CZjOWQ`b98s6!rUNnuZzDp4~pT}HmBiCSf(Nh#j-F>5{d(dnp~ z)G;N8cFv9mcY9CML^-53pbPqkC%6xNL;G;ug$>-_^c-?Y5h;)KyXz3FC)Pk8ag4r` ztf2A05B(clQI|<2CXs*W-Q%jyoY%Um&4N8cRm|+D0XK*0R$bZ#bi^THy(pu3NN~q8D7Y?#7go*Aka&=&SbQg{?OO5i{52%9I zG;W#Cz<52zN%t~HVP>!N+TD{A z&0dsLF`;GBdF7GWm0^Jz85R{`CY~27&^B;>cRoTjD2tUtT!!uAABNq0A#9U~V zb={n5y)cJZ1(|u!y?%r$b6Y{1x=8LMEfX%Hztl|Olfj|UI(8$V@%@Q+qvp_2`yh6Z z4zjO|t;rX@=SfZ*YHQ4}>){hXa6Kdxl83?GX_(nh86TOW)gpax5g<98VpmEj;z7BP zOxX+CrIftMlR|gGP1QYQEoh6+;al7*QS!G-)6?CR{c4tsehj=&ukeFCLxr7OZQ&s| zo$V-3@$8rPk_Vw0$s1A{hq0N+X5+u32s4NMho1{7=^X58qN+(+O;C$HMJL%=0?R)~ zHSihnrO+Prw62)nwbs$i>S2ApSq=z*Gtnw(z1-C^Bj&NpGvBmnp-#b-S_Qis`i17S z+oPu97x|_)gEzqCQ}ZOXO(-51tcLB8;M>f@ca@j$)7e$RFX9oJ%W6#h4r9nY);k#m5VG#No zr{PAD@#wM0iW(UasTG(WETmSkY6%-%$Al-$UVAE2NUG?5zdA zmjE>`mPPmtmtTG^RHf6@70GNuh2$4X8I(zSCB*ab+z|1Vyu!6b5>avUarkN^BYDN8 zxXSrT`hOKOasgqITw44ScQlGe(uC87+G)4(deJNY?waoYLmDjZb|-u9h=pxCvNG^B zSlW=K0lw*Qu0Q0>m@gaQ1>WxS7r*KVXG|tV=#i#AP%KTs` z@mf+KT8#`s6@)ud3Xm8(ig$&&Of@|*ls(iv*gl-!dMr(MiJtD>!k$>S;QQ`7FXjbx zX1z#<@OZ1A)Hx=-=bme)JeHf!o)T4|9JwB8o>(cNK(ZTZZ}<59+-6i#IKz6tiLubS zsM27SphrnhZlfH078iAO<}(PJT_?rYG@W`Zd^of^>{eFLAA*8!K!^D_|AI@$%@upe zG9r-@f%xR%Nvop2^4C3rpwN6oW~XqE_)hX&`cZk8{7+KTNEV|J==~15?@J-^v~=H9 z!u18aP1%?pJr>zcKf7DGzDp-PP2|c#K5?Vm(RB=+j3xy-h0+F#>g%|FJ^kJ3T<@j3 z;wPwuWtF-xI8q_BE>I|CZ}c_y&V60Z&fVv$qxX~(a)@Qf$LNAUT*~l3NLz)LxvRVO z3g^TZoW%~5XSlnt>9v(gvidf7z#J^r@q657xZF}2{x>{W$mM#98|%FTwNn}f+~EmE zcX;L<2M6tPJRbJ7{o(HAg3dwRKuW@(P>MB2F6Z9Iw#0Mb**1V(C2!?5<5jeFa9gOI zl9i-`ZsltEgs@Dk#^-16Nb}_ZG-G62s7-Kch?-r+h%dh+Ru*=Q>lPkh%aeo26!f(bvkK+J>ag!U=~^QF zAZ64?(dCiCMrqViUgFs$yP&Hck5|zn+*#ocsi7z#BltSR8k4!Kp7XA}u*MScO1m5B z#eO2?6>sQ%ATbzL($UA#DbFY20(*!X1uFLE{BB_dIi>AYpG3N~|>%^)sug7}kEdlw#4LdLQ-|U!64R zJ+p;bqMPOEY`&-P)ZU=hj?@fZiuO1AvkTn=q;qU}6e48|(emPf^rpgvx25b1l!-p3 zb=+q?d4-Bh12oMF=RWbfx`@(YOkX zrIw7`2;2?&jY2{gR>Sp$kyxOgbl8#LlZew>ht4G5Pno0bWnx^9yhB|*;OVSc$LwUz zk+$}x_bUy1Scfym@(ddzXVfx-Nhoxp>dUV!~XZ&J!^vpODc@StFsc&}Xo_I3E zly%3#K5~tHjkyAD&3fd3z5sMkJB^OQE8m|nbv@aH70h~Y@J<8WVqS7l%@$rCcoAJq zSjp$pJej0Z>`l7Ie#$)LO3)|jOJzi8Z}79RPb?O*(>KTUOh`|2YIlu(KtnjK)d*z> zMv``f*4RB|CFY~&hA@F$Ob*yP(RN`78#SkCeWRtcqx7ijP?{X+_Ip2x>zN0jqWjKP zWv|-h)aRiIfofV;{MapfugdQ^#k#LwG#DV<4bi8*W;t_o}p-7v#adEYJnJa=)vgq;mM#YJgvbjHf96pg+M)Kd;I z`}})j-}!6%@^U+XR`(ZLz_IwKm89+nGoilvP~n@utnYy55nsxFr+?7e+ZX8x<6{H` zXD00oq|xJC4ZX+xCFI}Og@&Q^Fv^nlw666idLTF~C688^E9RLLQ^tSV-5Rd|cl-%v zAiDuiwksGJl>O>Gvc)?#-M}<;(jArRq5#c<{7e*gWWE}V@+uTmv*PjISFwfD*7ttk ztI%#_gMEO`MsutW>QU%B6j9%^eW9nd$TMHc!<;v+>0h;jdN<>EBq{Ld2l_ERe1Hk? zdt5c-&)E6o9n>ef&+1{8R#!!<1@{C-Ylo#`F)7}Vf2OM^=1FT>0N8y6K`(bz4@Z_p zKihLW-_!h^E^FEez9nLNGzyPI9l@EBmgKOWDS4E2w3^rFKNOQc%@TJbwlW@RueBN* zmXT8@q05Pv0z0(>%wvA7G)O+mBXrx!0BmgC*dU={T&lbs(ZtQF@&UwNbX z(zvB1D|Mn*q5~p#1NGtgRNu;gAMg`hpFzR=gDnOGkN2<&cLhV5vpq0X1qBbJY>YI z3QWqX_GGh~Wm@N8`~@o2Gv+q^o4&*N48*ffrbC5sNHSTNS3oyw$3EpJapQ4LoF534 zN6}#R05_N|g@@o$I1G-rL2OYj`6n4Ol~|dC9j9BW>&`tGO~!|C5IH~V&^w)f zdywPoY{8V zTaJS)2#&6iK&2c8gwOvd63(0q{y8s&xxSzva~y#`*xm5nc`XF4xBRfbcaFCR9BXkL z0HQ@I?Xj{o!HhR18_uX{AQ;4`3Pj8^;F>r|9FoRR#&N3^fq(0eE(>zKxt*X7FTnL> z3$u=y}k1_$2NMVN&Uj{eBWx9O_CcVdXy;;D2ty zRL7w6JvzTHm2BcTH66OB_Y9L2-yE&ipz5#`zhCW}15LILBMr zPQA+i`_5C(SLd8fbxfB63avv-Jpk8><3x2_sg6@M^?Y?)tIl=noX11qx^cd1=ep6r zHJf@3re2E^;Cgfh0Y|Dc{{IeH=bUn!6s6!EatI|O$Dz0%=&%l<)j8jtSLe^E*X{qg zV9PS2=zoseYPREobX>3h4aW)TIAWc1*sv@9r16gaNxV#~_T9OtbwYQo>uHaY0| zI?ifprIP*~SFz(L{_ozBN~Lw?H}$?W@aN~=51+sAPxp!A%yfq1L~-UMbzX|YT%`U? zs+04-^Tu(RlmfSDs{3TrPil_y{W{lEHOskgGUg>?nsHTUVF-EyZMJq@M1K;FE0g~_2U6#2x_Q}59d&>XaK*${9W&L9o=PfkoZ=QqO;Y&JeV_X3 zPznAUMNGoK&B)KMB<~BTY3Dj|{_G6LZ9N(83(n6vgrqU>oj62>;Xoa5XaWv-B7jq`#lX+^ z&YzF`{H`3T^iW&|k}(|GgY%k7FmfmjsV;tp6i^+!zK&bkxffLW>7f4ae4Ow$hI1Uw>nS=C2uKbM=|2a!vwjHB&A{;QM*oC9YIpl3_YZ&HF2t4rYF`Du zEqt@knWwu+3>^prkYw{68Nn0>-cddB$nIlL2ljd&AZPwg{-cbpqOGBB{g_#Tf1{6> zFdIO5nBKS^>u}V^f^G&h>`*o9!p)=Ga5L^c%=B3HDf=t)2<79vIEhJtGzXwS;N`&H zO~i}n9BU=2NekJHnO963atl1vX{_47osZYs02@7z>cvypcy$Hj|NNq*XR@Fp#xUlR zmB|``TGF2OK=hqiMfNfC@pICZ9_G42^{o&PNyykWjH_E zj$O}nVK;zt=?$#Nl4%>7kv+(C2M5+8Cf@2|`{*q5wjDH97!|E$dK>dkqnB36>S0{a zO2Zw$lQ{_Pub<6Y^cr1db|G(>O+=uJ&}?9ZK42VfT1)RCn`zAuXkZFdS4|9^nt}4SFQ(;Rd0J{9PbKXXX}D%5_Bp=}UgNy_9Sa zuY&LV7&p&aW%q{fbRrr?yIXBx&lfQk0F&spaT18B2Y?V=)*43glaKZ$`q4^929x=2!(nawdG(p3cbl1(}1>F+I@*zBRhSG-s~@W#s`oiZ_5V zHJm@nJG7}0+)+qIE6-^-l{%#Xc}l~TvHAa!p)LV+rUu@DUo(HBtnkgvL$}a;x)c0} zr_3DYJ>UgrHfozcjNkQA#t{9e7Su}WgZ0!gOgn7cunhB@G1jhc#gGWo7X1NF`{`_L zp5rg`)A^d>Wbs$-B41j#2K(e+xK+3m@RjmI_Q-U5w(-QgW)HBl**)xhqzB1IK9Sta zAHZjP%`One@dbHKs3)-C7hUK|BhD2v@%LDTtH}0d>cc+eB-w5p(Spio=!_;rn<(3q zDUtrl9<_fotM*xMs4dgF89B|@_De`27|DD=YhnM91fGL3>{WgqZUW?N#_`56+et5Q z^B1uDTDwR`l98nHKK*P*b`ieAHxxHZPvvdyE^-^Ofc)9D!}U$h4`a8S2z`+;tc{PN zOEkf(XDDEu!_)ifDcH!g_+X%i2zB0z<1E(*Xa*UPc|5O}HJn6aJ*h z`XKu$kerU1lF|&kDi4)v(Z9pJ!cC*wqoL^j=nti!{*T$7yhoFSw$d%Jk$6u0#nZ;` ziyi0d4o-@~avSd%cV}rMyAXH~QyI?wsK=`LwQG8QwME#BOw=b@CFnL-d*2m*_dRjX z6P7{E_u7*)O+$Y>Ig6`q442^#|67(bZ_y;7oRpXOqGeUqX|Ifl>hr++lzK_t zZ&&wG*F|q!Y;ON#DFXedtf-2RU+jzy7zj9TGxb&K zkq8dtP5wLOw_q7{w!IRql{0xO$Mp5q@=f+W@??rlNjKeJpI^k}rIYv{(sRBS-JmCj zT;P1Ws8kEROUUv)F5zzY4|S!{gAV3Z@4?umF-!gLJl|r9$5ioY(jGjLl(tqvo#zT| zZRIvPt4);IT2$YtD1ldr3j))Xv{oX!QQqz<%Q>kWcjX$f+0xC6)%XESdc0Vu?e%+h z3g7ID(e|c9C*ASFB<4Nnzv`iB zpw{Te9_r?qc`z@V&@Lj4az_oVtrIiei%E$w_L938RC8= zZU+rxdizqiX`=FDS@MJ6=}263n>Cy!fe*b9ALly7ucEumPSzPyFt(7c{7~G`s>2!V}s5(8K`uug~Z*HV?U#uts zffjTQf#A!)R((9(3f-FKLK<L!iJoN3fa|-SFpleP#Rp2I zbO~?PtJAvJ22N9BxwbowJ#25W#?#H9d+5qVd3rseJx0H%cJ-%dQKCuai|p|3o+skYi+2__uaufdXgPs z6I^{`SH%1-nRLCe9D1YI$X?bbZIoP)d9;k@rH-5`op)Wsmm@2G#0KhVN3B+DUHNmG z^|3X?mG&F`qIMC;B|(n!Ug5V}4{U|WLk=(#xO=SIZV(v#^-{ut@OFEZ(ArZ~W<4*& z9H4lzj->! z-?^Mt6YX{6X&8~ga9!`DuW@czpZ3R%;BH)v-)E1q%Ifu$!lXa@i*(I@*!76C(~qeQ z!nM>Wy5mbo+rr;ZDklC(|J3%A<2XI7q?ZZJN;;GnO75++#M8v0o~7O<;$F0cWH69Y zFPa=ZrWR3ms5kX$&?#*%H1Qnp<#1=<0@f_0H&n`7Drw0s^p-ma9?UZANxH)7L8H7S z9e2<0^bp6QxpqIZr7ePAzpJrP%@r;RhjPU$>5Pd-Ka*(NC)KKB}%s&PohVZws?noPfX$T{e8(i%y_79 zq?KAn%dWrIhbu9v%R0bR7K=$6dGJArzcOw0;o2@WO@u~g0P$rqIF4>fbA+kfWBU&I z%)PK1s6&;~YQbnZZ6LE*{uFz{7nC!|C)rN=2{k*;DI{`?AoE9uZGtZD0lPEwX^y9yPi_~yr8F@5Lj|-6S}6_oHuFW@oB8%u zg}@&Pe<}@Ef*9#u5QvhTpze}_t_O(D^h^=L^33GfD_y{S@>nO;jaGz6H$n-EAUN9S`%59 zYl)|&cbPPZdf@*4MB4@a;1Oy?t)bD)KE=EJBivP_w)`x+n&Q$IgZgC!4@riC!1pIL z%`d1OHGwEwPPwAg(pKvah$N%|CTxPoc8`}UbJJ;zF(;f9=&58Ur`e0rE}#Ew%UhM}W)vY;jm@CLeXJ}| zKN^h;nY4m6>?UrIyT7NIH;XTWJcRzG-VMYil}N6v?xHc^_1YmV_G}X`@GGUX60nf; zouQG5dlRapEK#$tw_JVXBGMD?Z*!CJ%07Z;xE28~CyR8OCPv1Ei-WR#lG0LtX_n(C z{ue#x%Hn6VqHq;d(1pYeNHOLr1tLw9MCiu-<6D-dvcJ4gj(KR@G`kbGl@I8bVY8?9 z8!6@LB4O7+UlIPLHA7vjw@QBsow;9_m$*3>Z%@|tMLo$GE-J={?lQ&f-0DqpE9%bY_mxWXIQ{u_U%ia5m2ld%uupkuyu?_} z?#k+>ZX))Cr~7xIB5u#jvsq)8wn+i^6*ka0^Y{# z81+JEYVwgEPZR!4-W$9dr9dMaX%isduB1D-%&xnxJz{A(NJ~uFnHZPiGGDNl-CRs5 zPZ5r%HLdedQ(_p*HfMrnY2ypI$={R?%9%amq?52K>x6#cecmGJ(xv~J{;sziYXHrS z*P@Yy`T;v&g`gW&);eT0!&`(tVk6HF&sa`0)yRXdTR+Er{Svxud_c$ee}oe#NV1cU z<`QNJaKL(58`UeJUdgROZ}c-pU)PH9TU{t|PR&u?~)|0wj&4b6_PtA}xCUts{3u~Co55!F{nmDJ*c=56 ztcW>BOHjM0&&Xx_YK@>EZvhJKQeNNNq{?a9!Pp{GGj7#5Ht{)tJ7wjvKq6zO~-S zYn;~m(LwS@*9!TfYqgjb>OEDgF(=aj)A(UzDTWp=2I&xdB$e( zw)J5*@`Tx;kO?u*njdYe&9v$m0i!q_W%0%u<~9F=yTXrS50boklCmKj2fCZf%yza4 zP?uN}DN`c_!(Ad<)ODI|O~DGeWp0BUhgr;NVL!OdyMyxOD`>Gh1JSN7pTnEoQ{H`3 zYD%%zQlDh(v72*mg)G8tVKhEMi}H)ag;0gP%BCR=wF#hVjhAM7v&P)FP*>{tjIE$*hNmd>KyvWKxUG2j#u7a>ic4)UvdMxASVL3g+Sk|UCV8r^~{ zCQfy$0iKN;q6PGU?WK)TNt}nTD}EAQ@yXmgwh=gouJRZh+dQl)c%eQobGKQEzs}DF zV&QzYDPBSgLylxU$P(+pIGGxI?Ez*kb1tMBmxua#^`8V?28abW&6z;Ann7v--={8_ zW8F4?(y|E841&zl^Uz;ALWe`j-yoa_^;(r(2aMz|C!XIvzhzkEXB;Pgadv;EcqQW7OFt4Hy{4hUTBRX5}irD(R;Wz zAImO7-*ExXNm1y{O(4bS z4`v^eY@dVV*{kG|HOeew4~5K`(c~{%g}!zwnY0|7C#8U(?9}a#{_NooVV&e^Cp`f4 z06?rR#kS_(q2-W#ni*Y%jzv4XlKa4o!IhXtK)gOk?=ZR9fouvbYONxdn3{HiQPjFe za@gxk#e!T8tEj$H-wqzWrsfN)0}w=a+i%UjkaH4eJ++TOw#*ygj^88mm?qQ*+QS=M z2#8^8NNLDb&5!C~lmCN@q8>Oqe-*M{i*gRrKZFZ_-=-y35EEtxQyzCg8<@7K1abDDxc^Wd_4644?-g-EJS8fBVT`Gufb!BSpdeZ0Qq?pi#&= zMOdh~9)rZbqco1GMJI!Hcq`ivh)tz|=n;eV(?rJUUhhSzxqF4tzi21!!RBL~(HlD% zBD+vNptL%@z2ZQlE5r@JhnNBI`h(pMxgDvj_&Nx~xi<~fC07YTccFH-0g}^Vp(>Tf zOt5B=47QWZ4lWb3K8Rc|K@#mXOM=7$&aP+8urC3HErI-N)v<$6xBkPw=kbc0V@isyWg#e<^|K~u@HE5`ms)5*0RqKrx!C0I!i^EL8J(ZvI6kJPLPaD z8|WeZfkv}u*-4Omo11OR&B2S&3veWSqM7ZFBsb({E+dD`c|d)8VQ#e_ppUd5S!b08 z#msZ~%v%{7)j;qRGF#gZK!tW4Qh)oSPjJP)q;PK{cbPVPbvBdvTvrVPc1hQ<#}C~Xz17N&+PT=0{Cz%-xFCBTIsj%< z?HnWFa?#Bx+ar0El14AHg;B#U$Y7AR$# z#l@t#ij`MpHWXlw(a&ac+E|<^Ay$TD(Ok?tYYY3?lhfBl=tI-vkN7#HTCRem$pT7& zK*!|L;rZ%68f#pR9E-NGCkRh)5i>tf@yDqpwVP;Vn>CvuS=CSUd35iinOS_ORsS6r9{*XC}k zf!+-dktnXNEec;zV+i51dtdpV2$H$h+5_z3>1ZO;P05^?@88(u9cqGhUA>^p(AyhJ zX+5d2XA~M6i4XNrSs=3v;M&=@k`}{ikKz$I&Y<9-2S&LkyZ^04SiVu1U z`I70nz*Oa)xtKB0BR&mtD$-dw#tQNvu{P7&s$u5V+C~?WC3ull2>f{WH9(it#J^go}~exY9CHl}wbEkBQxBNyzG2YShx&7$s3D=CnD}T7ss4Zey=B#dleJ zZ#SpK*d4+;&{RwVRqAQlTd3vUF0Dp$a6Hr~PebRk5O<21MkX?g&@Hx$@C7YXH-#ia zB$%5GJ3pvs+Cx!oxsIyGv!5X3OU?S2s9`4#)YvAGBHFDawxo`Y8=#jAWxs{1|BaISsTFWBawR5B#R}xfp zGnfO~LtSMS^S5Yv=-B4B#^^TeL(8I;_9k%dwgiv&c6pKX%FGb@U4`C0>?hl^704B} zhS7)r&DTPxX3eo#^bvAo3!(w6V$N4a=}#abq6fQCjJIP0v!fQ%gHOZN;!AOpV9k7k zYvW?^BK3=w2sw2Ic$#O@RmNfMzTJ%PA#@REdZ~Dm98n9h=fy6}LMRFPK5#yRK zod-pJ*{DB2;xB6x>it&reI5 zTh+%m=~RDdw+)0(R+ zRO@F$ooJ%G%avmE3>Pt4aFal(?xif-Mq6WLk&b#6+7aamlV2!K1oeZli`fXg`j>Wg zc7f}rHwRlEScseHIB|^dhgd?YY#YRqJ+Yg3-DqbmwQB=max$o}4(aPn9f1o$tmRoQ zE+r?ED;Z0;6QGNqi>HtgnxAC{zPKyRdUQ(GBj%)Y=^Y>HOU7UCrD6n3VX z6n%$PA^$yBds0l9B+u7-HPQHrgH}6b5PrYIm6!%Lj!GrNlu;@Fcx?7APyk+Gj z#pz4~YqgD4`ZaaG6#&ZlAae!G)l0H9G+lj6ri1=pq_^1aMq}jzi4zXnJ))hsSD_O?t3#K>v0Xw$RObxM;Fi?9KJZ<{WE}Q}KaFKmaZAX5UD{_1EW?IDb*jbE9ps>mZ z3YRMO16mpl!SjR@uA(j#m$kcLQJl@)fc4c>dfTc*U0geMurRv%+;N>CL-pUF(^8(MA!XRXjG+Z>op+04EJSt#2QyJmj3$`*XmQfQ&PZ(BLxfa^ z=$3H2nH^|}VR*uKFoSA6akEd5iWw2JlaZutR|neNt(Q@k(VCwH%88QlS${zu{O_t? zsLpi&wZ>I$5`2`OhbnFNl2yt<*EF^*|r>j14`B^$4qHT-OY4Xsf|+f}SwW@fzEz7r{D zeiF~f&y6pUYKBbzGFa%6Kh!T6Eg)~R5;q?@EV@Sg z-+-Lqma%`3A&~Af6DZC5*q3}1zP0aiEB6oDQp-!bFm)hFC;^`{UUo>}GTmx6GBDMxvjL`nWb!obw95vg6G?%wj^F+%CNp%5je%O|mNU88pb_m`WtKwUtib7UD@H6X>+tpo8WOqnw=r z{rkmmrJc4GF-5@9bzU=-_vD^rLTdIKt|mAQ`+B~5Un8%*!Ti^X1>$pCEqA1+vYZ*k z=O^Wi?&bsFKb#cq(vJEEWi`}AR&xwmueDMK(XHGQWHU9{iYOlV)n2p>1?|rCcbtc$ zHCwXtJ)ck*=-qSJp02|3Km1Fs80-!m`u;v-Ygvq}s2o$&TA+PZ3)^Wxd-fg_M;qu> z{-iVq_b}&aJ@m@97qq!U&5zOdMm*P?9S%8WebGje6$l^QnPsqBXaR{l`A9r&An+t- zw6-#dZsX4j7x-!P1lcI;_HFk5MSZ$o9b$RN3H@PoN94BpCs~ghgHFC6=tkl#s@Ar4 z;vd#0vkNZGPS6d*C+?LNkec=d@pr*(&eNMRA$B5JV1{WcT$=flHQei5f}K@WXo5IV zNCP{r2rXN2me%KJx%Kmq({dOR1Qj^T`=agIm*DK^ZSAzV+Nx|ER&GVgTjjVs=%Dd8 znTtzP#=K5)v6X>X{)x}a4`(LG54~r(YQ{ivMy~Gr>^b1>BaP#mvoC5b}ze7>~P{Z*VvN2yX|pN;km+kPR3hB6MzZN4r>mu{ZG+ zc8ItO57pC!+M4yz13jP|B5~$JO-Bvf&G;Q~kAGsHG!oSG>Qi8XZnRFIi}Vm);d<`Q zXg5&CFq>pUT)@r}!|WPTOAMs#=X-|xnCb8VzA@7p^zy*epub2xWh7+x3cmB`hg#Oo z#UE!Oe~#txT=N)BaGms&CyOFg?AzqF%0+ssJ(Tg$a^?r}nkglG724QKQevWOnVj@t z6scXTg=#+IKHt;*o7Ge~0hRtTRs(GWR9i-v$EgRL^4;)qIh#MPt0hz$n)1Vi@nTL- zL_9^mGv_?KyCU<9_e1($Ph( z+tOissq&9{RM{IHs)eE_18E{TLGky79_61)Ypi}L@73wxbXlV9)wftBwQKfFshVpr zouXBNzQ|(WHdKrDR}UE)twXa}o4EWwS-PYzR{!Ahie2zsc9*!Ej^4C44bqGq_PfoFy@EXPgDQipBgwu>`(gO3W1LjC_k0);k#e%}vU9 zwU$;yX&M;=wZo-A3HTG=H+F=MgbP{?AcMgG3Vv?mJSY`U_-^wz?E7RHif0(64b5Zj zHv>>D-oW>lh~M&cM{kX-+!whBp9mc~fribD!U%5}ZknEK{DwZ*r?nmWLoHUjX5FIc z?A~kzR~D|7HbnVKrU(Zp?4r2Z(0f=aHTM;eYnr|E9^|X_UYlt3F(+!TtPXfI_eIL@ zzauTN)*HRpJdzt#vP1ZR?Ka1WL%q|vhVaZTgHG9#v_X1ZV}`y6ltg|q71a>ea7Q#N z6oy^;ZtII(6;-w3)hf8D%geS_uYuy>v=#@I^clKOt*uv}t5GxTmpZ!oplj+lvj$re z$SrZ~0Mrn)mO$C`Z$uTWSTqx_BRA+1hS>RRj=Kz*o?GNPzF9(ly_NZv?afam6>%H3 zjWyVuE4K3%MKzQWMisJLn;jjZY>f5|ch-LZ9X~TyM{da^g_lReG{ma5iy|gPKWUWZ zHI&ytP1M+3w6f8F{o7x_x0C&D<%DEI zzjc_jwr*<$wNuO^&Mlb!-M;0hL8PZ%O6+ zy+unRmFq11E;hE)g}3VOQ8G>sdgu1cH!Hx!#H{qK#TX66XV{=^LY@A#?tJ?r#0P9` zIakaC?X)fgEdPnoIQ1K~i&m%$Ojjk%0mE3j;J!?*|LTqPw zEps)zCHgyhiKn2GxVGIzYsh?ZmvybOW`uidZ}f#Q-wTyn!48po)_71YO_zGf9DN$j zqZLFg;K`7iX-q{U2h-3~#WxAQg?DHjD7%M4|G^JA61UKGelt*Qce@|>GmG^gv-Kt} z&jpfj+0DB!+=mqez7u9)e$IJ}f} zwnOS0qp~o@y@5TikJZ-eTa8nCb#-2NelSTnh?t?p*_W|=pJ!IVyCh~3VF-#v+ z(l(fq?!P?~`EMixQxEFJxvjtT>BdI8A-zT0QVmyh9rOOitujwoSJ;NqSg`?6db*oS zxdyRiJ&Vu?I}hY~)S=zTN}DmZ8wg!z8zRhikV5zuEfP9qfXf8(Q}3DS^)LDdZnt+6 zo@bs>-)dj{>F?k*BIT* z^l&a+(n~A5ln0PGu>sPmdrGDFrPhzo7UezO#rfDv;L&Ps?c+bk{F$bSPy=!~GLzM4 z0rTA4p#Gs%MZfV`xV7@knC7nbWW81vB}x@2(b`)b=}xN^zUgl5T1;N)Uzn{ls=tCG z`KV`xUgc`7t~m}D@Q3+WHA9rxHop+4-!o|yt%>?CYkO0;1$qWeu*%qZp;OY%aA|L> z?_7St#Y?W!o>%M(qa}&sX9{ibD)7+_C-d00-fzCDxTt*^bz+WKx9$5@H|+{I=C81G zxC^dNzDoRa)vv^oU-|B6r8&=7r(e~waD}}hf7=+Rb9P!%$&4twB5JsaR?0d8cM>0H z;4_Nn#g^!f(acWEt&o1;bKnbjj<$%mg()Z-5T$!S{`)BMirluJS&Nwg>_^ldFP0X{ zXX)qA+Q?uW6)BzvcaeMWO#SAI_jcsgL2Ar2yE0SAS{aRoJ}di(4|6n$CP`IXdnpO| zw0(FL8KYe>Pm}4|X>}Op-Bp>E`eNg|{jX6@6!-c%Uq>$CW#Q9ePys z3O|@`w3<22nkJO=#>r>w#abEr50s0!Vx8BYX|qU1{*>5>Z{nJl=DaV7?zC^Srsp(Q zm8~LfXX}v1t_kVxyFbJ1jfRz16KkoSUHu-JqffHF8jb98?B7CIdzJJw+=bE61!Ef- zYv)te6V_c`{$y`d^4Tp(A+rLc3Di*c8wc@smg6m%Pcz$<#VQd!42r^9+(mkaEySdy zH{FZU-|)nv*324o9{lzpa?kJ^F;;En1L}St^0>cGr80mQFwDSI`MugVrNH(D*>eih_T%duRd z@hsBJsA+ex9x1U&`-WUe-6Aw`43&)i7&*WR6 zjC>`bD7%(SF{+x&jRkfE$N`)O?(VN#PjMg9(|BOLMKkbP$SK}z?bkEdwV61pyh_xs z)wY?S%f>d3JBeBO;nz&;wt;!#B63C7>nr>b|HRXt%~l6Of*j6 zf7v6}Veb`})Uq{9b;j8B?JJ~cW6>@kM!{>YeDpAJjYL)~q z^&6y#A!&hqBeKh?=uzbRbPGCVl_VJ<18fnu*1TxlwCix!fX%bddSkAFPLoNd(j`JU zPj-H|S|^HNxAB#|Wo?9Kzz{OQ<&NpWCF^y~6YOUEM(?8ZgY>@ZmKWStU+ow8rc{Gn zqkU24lRY>J6s$V+v*azTn z80F1j%%A9{emb%_I#kJO^a2-MT~M>6VQadwGIyhWt*pS->;vk7%%qh$o*NOf-+hBj zV#-O6g-k5ihVdA($8Lg_a9hw5vjZtE9g#=dt)fe4PxlPAE=|wwrr=G%9i{$2G#hMH zL#54%5v)!FRY9^EHYS0lelyjCT6j@(R7zFrgxH9c!Gqt(s$mb7T(Ok|aBHCG{Y8I6kL*j zfMh#rtN@?nbgi&@-$;*Zu+Q}jN=2p)w}m-{mPmc^8C_(e;xh)aoN)y%1(FswSflMC z=6EBsxy#IFPav7`Xy_?DlU}14WEPz1aij%gNVOwZEf#vl-(w!24;?(SJ%+}#Q87BpxeK#)Ly zKyXiRcL@?C1lQmm+%>o)1eXOCShu4y)7@Xy8TQG0eLw2zo+C3o)mB|yecu6PgBrnC zYR6cAsh+G@4dt;u*7t*+&M)bo{FlHp`8+$@TajOoFDSc>7Cu7^c3AB5&dN?>t-ebej*GJV8KC(e=kKJV zBH4GvtMEN=?(L##2T#+0zXjR2eORK}EaAldPr@VmuhsYBq1_QKI2@}RWC9jf(F z|Fz6hdnn8p=RUx9hW_IUx3pp}d8T?1Fn7NfwF3pgv$+tgg?0>(VEBGp8OJm^5l#Tc z%WnIRuDtdt!0S248g6;2T$0}#BQj5Vx5A0JC2n7Oocp$~ke-j+lCJV&fsyj4h-c2L z#;L3e`g4d@I3_=4fM=0WfhRbq<-C5|*G0cBZB@I`uD(-Q`P~2dwu|i$mEO)15ceG~ zhEN+xi=eGaA^shDX}lvbFvyKBo2K-{_4pZ>68f-WoWp z{Q@Vs)=2ZnD?m8f7qQ1SPX8rPRyrJqLI>h(fMvsA4hw8+Nm@MO*&2 z(FYI(uCiTZ21F%x(msjfb|ZAPb&*&>Qfx=UkGh802dFowO>N<*XlqUz1Fy&h*kkSG zTMt}PV?E6ScQnEe({jMl*I8<0`6KjZ*h0Cto)~z^0Ig2{qq=YQ9uMFAmyd=Hh7HcWT zN_ihkHsZD4yr7(Z=XI|`Pq5UT3f&4 zNoBRvlJ<>iEq*P#an?LOT#hrQda^R#X8fD^*jJBT4-EC>@vS#%N)4?O9A|AsEqCOz zmVP0*T~22voNjBtuCZKnHhs+R!HMPkBOj!t*$AUAzso9u ze=ttkq_h)XvW_HDUZ8HX)wjhwmtGF` zdQ$X6Sm)Fh&G<6uye-w*l&<5PEK{nf6Xh4;XWox0v^^W|U*K)#=d76Y1N~0#?(=AW zkzEk=6(_Y6=O6;JD*Z`WC93H|G!J_rh7vD$@l&NGQcY#5HPu$lQ7A-(vs}+boaLS6 z72OVJo{#eB+F&?Cu}{>}TLu>StiE2rHd;|H1Qz*ye?Hg~Zejf>KjvwyrCPzU(E1yo ztql}~=x!+lxV08S#9lqHf;RG0eR|-2AWrK7*tCCxhrT@zB{$@2mU@zqMw&XQ!F3+OPVVA56w1OxRLS+6(ER%bv7v#25 zaWaqmO0US1FUD;a#!%WDzp&e4l4K}tRTVJ*h;mk)V0&YWvlg~Ag-F6{z$=Xrg(x=^ zr*a1(mO79~^0$a(x%A)s-+DW{o!M=&mH*5<}}(s?X=gz zbWHET`Qw{xA-f4Y&w$|yE9-O0R6y{Ws$7JKou<-UdV?$!cX&upZi;!NC2^3y;N;X_ zkhT^JMQafXQIyM!j`~Us>L^3Td@dsALhwJmD;M$a>6p+ zQrqIUV99D&F+bjN8njwZ<;r=5!_y_XDVNv zL65@pyfj%pFHz|-nFSo2D02nAPwSwBLh1m%&V{UvvBce9-!kU2zu0`% zoU7s*hZPB*Es$5V37HY(88y=xKM93YOI>{^H0@ zY6YHC^PMge3GRv9qW`^P3t@+yss$Xy)Q^kZVGBR&$U?3MIZ0JRk@i0(!S z1CJ5*0w-_9=Ruv>MGG-kG$k@baL%9?K}V>kilRn0FSzirD$Td>eszCusw`<|y|#j%rH) zNg*6$IqS%xfwA*f_K-#JhwN9@iN%0#B!>OXBKhBJ1^b4@fr=M6?%S zMrpy^5zI7M_-njO6WK z(w2aqh|fT2LFdw@k_FHuOu_3G)DD8zm|s9{5F7MnOc#*bpr6~I7ff5h370mYKNM59 z3&l-=`VJ9ai3I|6|5Y%LpP4=WF2~=aZcRPk7K`Ct3&>wkroX5I-WlH;Q?uNy$uFSC zPc4AG!FuQd{>?7n-~3l!t^XTLS3uO~GKON70o(Qb;E3A{ESV+}1=hl8 zAd{8zGMDQqla$kn2Ew4Lo@kL4x@l$Db{$bx`l+C)Ek+N!3ixz)WVoCzUTrGo>$~Tv$dFv;8jq zU!U#(9Le`s1bb-wYCJSBHS?X|x|4yrjR~fhH$O9f*FN~42>9<}STkc=$M$U}*NKH* zff@CpL_nWYh}f`3%QdOmoIZGi?~W|5WLm#6YG>IBqtO2&6J?OmRHN zaTNu5s4WENhMO?%wiGxYEQEa`D``gX+5zJ?S}Zt@n`tXp3*6=lV6R|7u&>|d&S0Ck z#nHk*G3OKga{EbyKx+X6DtJTC_TZ4}@)_B*h439O(wX3KL48}` zxoG#y3)()djbHRT`aH)ov+aS;X>VXZ3xyNC4vzfn zXb*i!XVOd1U(N%6`Z;;(XTdQO?GBXbU`3;#Wsf!o_KC$D`$QWa25U$hXO^^^hx4lFX5-iH)dSNDQrA=)H3o-PrHGxVQ?&^u%y zy&)}-Zd1e}a!Rcvo7`NUBVP^5Oy!b7)rM-0y&)@CA(&3LBgh7%C648QMc{H{OMa{;F4YLICDh^=B>Ksxc| zqCE)%Y@`T+DPkktkmkzQWE%*kbLFOrtTt8V$}MC&$Xxjblo{XQD=9>}12IGGK<>lp zrjuC3yYLv%NyPFe|L1w%>UngO=h`~$8LY(rD{B;gz)k(MBcb#zNNc)~-lTTvChYIE z`D|bO|8|9GTcC}A^Y21}HiT(6q0NSKwWKssrYV;IC%h5N-R2xVLvA9YC_!l^;ap>b zcpcMLy2qEXj>by8vyPHW>lVo8f9c!k?d>h#d*RFL@98Vx>*-tP>jtO0JA>!-Ya^UJ zFkJk$aKL#pi~OY`sf~bfx*kMk)v}MXo1)q3+BCSHXgh12YH6fO>SblNjCLd1IB47C zw3qfgl9_WIwnd!dR_Om~tF^9Ltlm}2tvw5@4ZQT{3p@jEmT$E<{fV|ldjjbO$QAkNYFObe_F%rzpeW3azsSy~3g0qX%=BP}YHs&Y)f0 zoX!E;1O;uBITUqs?XwMzHlt}j{$~%j4BE(xMZ5p$2lW*7Kf4dtHMq(~o9_yhf_Bmc z88}3OD}5PK{CqYwb%r(z>cq6uZwf2}u8Yj}i|=EWX-;q*h4vlRWi!Bk%XtR=AMO3S zY$^zznvWSOz2yjs=$W7IB`H+M%N~&NfYaa;wTm{r~wn^$Q(r~4K zrMj94>?|I-3(!;d11jZVkwLy7W8p+4a8rFocCEoD=+XLc{Rg8MY0TF6EAcd*=sy z%_nt}*T`LzRSL=@V5Ip*S)p{WL;;IS9AM_H0>rr(h`Po!H~k@qR)~n6rvP~nkx3D$ z@*E%v*JDC&1bb{3H4|AF(S;E~vq2DXcrsuoPhfQo&8WpDg2#0NI|=D2K;1pga*%gT zwBV`&(REFP-5fOAigXwt$f73WXiedPlVvmHMbbgQz^)4CXireDbX*!MAD03Y@uRBC zzM!0dGm@vNMo&l+02R>u?i6KkdQ~HJ$rN!G&Q+e{Q{nU{;paHvmki397>d!vn5{Q4 zW@hXgE*TWdW`-|PXfMXE%Gz1Ndu%Noj^`PY;hepJcjqEE{&sjzv_}(IxLTt zP00ihFXGu2u@%h(uV8%^%P?_8_ZBjiT-ewvo9@16G$Hb4}5ZlN*vMrcT1brmJneIU#L)igiL{MLQ*-%yiumUO@ zqoIr#Q;X#rqgMktqGuWh*igQg4-#cX3f~Qg>VH7_O3;s_G#M;PlMfKNJ6M#W1L!W` z11ur^ApHRdmL;YBz_*hu%8-GiB<)X2fj)La8!HF0Tl@(6G4Fx**(SY{ipcMzZSdc1 z(p$N((pN4fgrh@?sGF4O*nG8p&1AuLb@&y}X95J%~Cshp(oQ4_6D)OPC zEJ+djL<*z>=}!7V+9vl=HY;zGg6d0kt>s&597NFNu|But$T~|;OF=arP733*6W|?w zq`QOn7zWQBCJyo}Ru%B5{xn8I>M@QPh!H|6UFGQe#dW^VYc z8zP!A8UEia%TRHEr;4(0iWX%r8A|q&WKss+vkXm!{7_N>yvr5f9O*tXj2skM{Gb>{ zDnh;@$@q+Cor5iHo_R;?X%pjmB%E3w#ZeCN3=qtZWi<@I&|;(6pT^JnNo|652BPL` z>Erb329^`wCmYUFjDTmRi$ic;y^IeCE@@A0Y@(1|-2YK7)-JdZvEPVlGla&QKHC&Big4E-WG*l%}=L976rBdE^Y?3m%#v3;D>b*(laGS-0W z+PV)`F@NcKj0NDsS;E^uoN9Y++SKTu`-{#8LEBpSY(LJBuSm0{E0SqPqs@)>v*}9; z0e?~mpcdr#*3d?r557gT2hj(C_8!`hXrJLV+IOa%mtzZ>_D+s(0)5No+RpTMqc3|V z_`GLQ^mCt+CqX>@q}f~Hm1|T|8MRGTmydv z`Wn#BV6Oemv=yAoYy*1;ZD1D+_Zo1`dncGjpFRq%-Eq%=&~uc5Op@w>uj8aVK|Tq- zjJgtjW(wx(N>k_sf+CSdAeUj?ex2FbO*j*J9oFqPjS%B2h>vK&=CY=I7H`Jp8P^RI z3-DgfHEzJp?N_?p_zKb%##~^h#m{Vz&SIC@EclJg@f`m}F2n^PfDvmNh(XQ*`*SU6 zBCShN&_?h{wIo1R2#N;Z`J^2C;WV8@YlAI;n6YQ!zxCmqJ>Khrm@Y1Jm0$Z@wx%3? zJ@fFqQ_$ao@6s52HYm7vatiiLYN+UQ`APj#b*oh@qbyY{9;F8Oduk}-flqzBe1=Y@ z=Lsd3AWFI+q?h5H@fppT%D)UgYZk}%Z^o>w8AF7Pc}6RE-{!__b_K4luo=7wyl+Fn z1-@6#JDvjjrzY4(btsm9nm8|}iAF(@;2lsXoC4t>IVmUg;JwcgMjAjYd;>BCbT$Q2 z3jgKe0x;?|5CrUCnK$OsVIO!pzrZEl2+kBDUP2Q#BPbV{4Dl_qAfA-+hMe;Ae6q-q zI>ZpCK+f?=qMqRVjL=1npj~v5_~;30N_7cstWk0mIVW`kjxE|(h+96M)CPp*dVu>~ zPYBkC%`_mY2U3ZBX{ZKbI$Q(6GF)QQpp00~slZvMlao{@XCTG;t}eUfDoUnuSQ)KU z1vvx}%@rZ){g9ds`QesImQiYzAeOLCM!(!yIt7puu#RR2>jLZKFJr#Z3a|lg06)oX z9eq!@=N1b7rkwmD@Y$Ky=3X4`i(y|vztAkwIOyX+f87-MjDkMyiE3To3^}V9AQRP7 zY5;g9v0qP6>Ok5+nht)kS>!tOJ>27K!51*}bG3tKn9t35Hufbu?Dx$E{~r4I6nNHj z@S)=WCGf9d-t=kVo|U=Bg+5Nxe|3}F$Tgw@axtzqQRc!m?xCp^e~J@#VxL1n)OYGN8#Y|vHEe2Q-IhJL9rH8Us zd8Xu6yDRyE=`*L=|79GKUlT{0WBjbXz`-uZd$=3jKg}SZd&nU;k5H8 z$PYIT=^6TdBh}c$lGslEk#868fC=h*T76MgEc?r)!s}?QaK0(VD zE#sn60zqpUt@|B-9G7FUmI0g^(?T4|EAVtyfup53Sd@p^ahLccc8Nj2J~ROSGk{=< zQk-HsNSHGy{@?uXL6&7y206$^!kOlgfS%#j4;z_A6~nFnsgDkRR~gPq?)&@a8s3&ioBtRHQqas?_sG% zxnNsLVx^%yl>i3PqHxM}3+YR?klu6)YzWFv_eMa2)i1b@$S((SxIF9|C{Tq*A0{X}ty(BCS) zCj}|`BA!XP0f#tNK_3Ll-%1Req(xaRca?L?h&6=L6XezB`@iSg;r=D~N4Ap4MP2fr&S@cCGlK4KeStQY4W;9P4zNc-^;?4z;MKp6`Q31qxFo}J_GjC;sp|st zxwJ-pBF8CDtgF|YZ!20E?3&COVsv|c9K^~kEB(f z`LEWC*Ss(w@NWe?`W?o5R+Rt1N*bT^9Y(+4XT{kEz%F^mitz7w5nwtfKwgVY!j$#o z1zAtog)Y&+<+L{NvOZp#=yvOid&vD-*AN(Srx6WhhmOE7>J7noujG~3-qIM=aO zekSLWzmp0|FC|PjQEX$cf#;zgVDsmA8GZ!3$rOE`o}!lxrpZPbhW8x=f^GW;fwE1! z1H7_#!M0W$^4s7u6T2*tm1RT0Ly>Ic{C=D0Lvo%~KI<9 zOpn4Ij`PDuVR{qLGhuo;(BrXLdL!XDZ>BG09QSej&o5(Hdr5`qTj-l_NiotN_%sFv z`+G$rL$3&P$o}Abg7XS`U2v{RGfYpyh|lFQy$S~n)3Z<+ybYBhkDi8uMjFHMYaf)` z98+)%GUo-HS8&WSQ!KmbQ9_T$cJhvzrAm?_xqOM~ENW_~ct14+!ZFU-`;_YZoZ{_8*A2gh~v?&XZ% zSXR^9XO89gUd5p8VH?~;zmwKWFC^@1!$ih9N8?g2iG z3P!rV4?KkXAVrU5MLiWxrVL~_ZkT#{P4Y|KVSF$px6)O~k>BMP5=vhAsr);9kNU@X ztWdBH@EvfzL2o5~)`t`&I7Z-{*N1EldNO;_&EyT)0P|5n`W-d({XM}tECfhvg@Mn- zykD>2^#;I)+zgx@`K2CE=UCtPxjAl`b#9J5+XU(b>m6-4bI!&5&d<8XIw;Ktv+{7B zwi00Fl{bbM`}Gl8Wot{Xa9`m`CXY#t>9G%l<&AA=RVhYyX2VRONvQoVNXU}pnc$S9*>jcyP-&<>XOVOj4 zV~?S?aM)*W%Rpj!UIvR@d)!b!3QQ*^3s@R3sbvP)>5hm@f~;zlK=w+=u7>HL zARAy=8QJYjma%eD8hHEC1$z9@qgO?Ykd#tFk`;BHYhI5&cM{H`+G;|MU*RfhYaC$S7*9qy8nDWJ@deH2;Ukj>3xdpkh? zV>8*rD=iODav0Z;9TC~+P-m5)Jcpq? zl}QdGAMR%%!(tYc5e3WUg&j1^Bb#EtI1bk)Q{R93sSX}G++jNgx~&12fXA2{-VGV~ z#sO=f4=@n30Zkzr7$UO)$t;7IjC;reIXGC(Ka>O&+15&`$P_tHDXD$}cDBL5Uy5~{ z3CxTrW5n?w%b_1wAb;k_-gr8=SET_b@J~TzxRXIcSD)b8t=z+0N>-X~2LtQ_o?!I}0ebr??+j zyvESO(g?sNHKhW?pO>)=w4m$^%17XC`4M=X2Lm$g@E`+UMHv}5ku@&BOg2p{+Z5x1 zF2Q~rGKEbFvVG#dTmyZ|XPFK-A$a{Cmnm|X)&i!zYQeH2^V#SiGAS~LRr$;^T7_l; zUThYy4|;$l6j=z7*)oTXFbfzuhe>+@k+fov`7lK)qoA&ny> z>);6Vo$9c2YVLPxMm-(bIk|TJvs?(w@Lvdszy-keJ*!OtWED&e?Ue2ZZmF^SIP>r- z1X)qj;Q7d~IT-Ltlax}H0TxpdRn%E2>j&#j%OG`^nyi#jhkzsl$45G_ua2gF2HD=+ zz=~$FZz7u~qHi|VW&Mg~%0(T~IH&84fsqs05|JJ4G%z)uhV(>`1rS-!kl8Vl9*~9t zL+T)om~aCF9I{X% zvtlMiW=E4XE>+&63;|r5!RkI`m}0WljgTuS8Bq2yq*{>i726y#SK@PVAK1@p0OKk4 zjnnMEwvX-qPvRzr0oUt*WU`#*Nz$pegCVc4s2&=CD{+4qkf*_BCV^H2da6 zek!PI)ISFlLk{eEs4ryYoCf`9nqD8ubRN=1(67D>GO(HqdDuoS0)yoRgTpzS|5y@h z03+WKKy^TNKTHowST3x4Wa}L%<$Q+as|47`<$}6S4>ANITOjTh*Q67HnR_aX8z_?i zhZx5YOvj7k1la>eNmV3dcSM#!bIdT=1ap`Y1ELNv5+Xz71vr0v7FZoGfLF8;o5s$w z$sBjJFT(CN-m@OC86vx**(Oa!#416CLSz#(Sp_S}neaShO3bE}0r_ITk_qoI24Vs- z;62??&p#0y_ssT;?1(x)3rwTu0KvAln8eQ;(+p%6L`F+wHLUkP7E%%X9LL2*##B}h z5b^85I9VIYgZHQ{d;}RRt5Y92K`@WI>)C)PHB$ZpQvB{Pz?oK(M+52=j=$IsbJ~>2 zMu?2c`=tzEUL6DN4f`kdQ?o6bZEsw#Utv6h*`KkWU^|=~Y>!y?*e1>R2Bw?y1I90S zII{aCXDu1XySc+QXts8BxY=vxntlKOdKjveNEdPo(prM61wf5Tllb7v0I9iM$_CFu{6xhwo5Eg`)JZoK|dNhtw# z_&&kT9PaE6Ci}#2jy}Jtz6 za0V(;?jXCAw#riZYiXHuPX_)hd8zzRiUvN&Xz3A+qfY?c>j}v#Ub2Ghje$}G*wS}s z1GM6LKfSpAPA>weVnq!6?mL!`cN2L3-$hq~_y1LTC`HKa;l6DYy!T?jhgl35XTQkp z2;`cKq$q!r736CgMgJj@ zQU~c*+KI-{f5~cMiuwHRA|GJWh@P4OuPz z6)~g}StXvpbFh9;9*H=(jsx76mGG`8%gFfB~lwXOuh$fo*h8$ zNjZ75&0%})`dOX^`&F)B-I#sYZ0F|Zc?I@6d=A!)SwFF)t5_|tZROOJSzlOZzmkU} zf}*siX8PYXp8ty^9mrC0FBoyV6!Q4lvd`^yrFa}{%fIn&Iktc72Ppa3E5qy)__-
      2l{pr5slJVWKZJX`;zxV!|uAn$JJrQx_iNHF_BcAhhyeEiRfBAVY;DJM( zqpr>IXseF(`#RVL*26et3byO6B1iX8pW7bxt5Df|uGzQR2m2W2bNX6NTVF*UeQqD- z7?d+ct$}{B26VngJQ1s)oN@3w92>A5tt2^P6^?7D>kf2jP}dy^_UmxzYhahay1?)8 zzBtA`1zz2k#s;I8fn#3*_D0{VzXmi!j65_~S4YLz;OfepagfW`=<>KY}at!@<=ht};=OgB@4MxY{*Wy%;Z3j;50RfTdan z@Z!rzL+E~zO7On>V1<4VR*?HB;`*B@u6mJ|{QwyX{PyYM3y$$HC}SXw17myw`YFKh z!PV{wi0wJ4`2mr}ZG6G8JR^Apc+MW0L`zFQLX3>*bwKYzPP~lCE`aeTILl#tORCr} zGC0NrVN49ho9~*J6t+MarHEs{0WwGO|S-P!mM9Sn5)qm+yg7m@xgjH!8EW87(`S~)f?(mzoeO# zC`Me?H>T-m>E~GBct6wH!Iek4v@f_CLe6$v6`2xY%^;U}0IXU3eNH+Nc)KTlzBa8~v_P@UWz>PHDai_Q&L5D`!X7CdB|eU; zf}GXE`C!y3>Si)nP-p=mUp20NeiEqbKLzR?S3c&77grv~!E!azoD~+X;(z|1;OYuF zv(dUd`Z>kbglXlQXW$07$AIwSV=!#whu+Jx(bT zNYvH?;>ks^fzJ%A@t-ipve9sUuO}0MqyBB$2mikT2hXkSr|XQ-o`x9}Gvkbj3RBZ) z3$VV*@p%wwUrcGH){yS%T>>-s9rcB!3Q5!3K#a{=shM(3Ic^{3m~Ja3?b9m-`t#wo ze=KJVD_^J$^qr=)<+rpku$D9gu1-caX=Rz8TvS6GUjcj865kZ>Xx}*Y8az)SN-ya- z_>5mlHKp++MqVtB1}i9x905k3quOqxuvjk@R*DKAj3GT?e`T-`e2{byB1MUEQWVg? z4LtBy(K|Dh^n{4HaH&1-pg+^=2&$yg2Yip8d#bZcIU3Hr*Ce?h9<7vI*J=lD1LnAI zpQC&-lGzTDN1kMK@E-I()GBJ9^f7FloNXTwa@4j5{JU1Asa3Jpl0Ly+K_&hZnI>J= zpZX1+EIHJo@=7gR-y$-lH2F*Vnzz)u1+Mvf={=-l>Jd0+Qpo79uaXSs8Am?VMdrYM zLoe_r6k-#!T>d(MIC5U2-(`iBxwgE-9Qv1pW#f*VplC5F;b#8@aMH zPGkUgh>PlSTOzS$z=QIJ%E~tB6VK097-hAyfx4`_R0vQt8u_vVbxEXJRdH$GdsYSB zie_?mTXIP5ka21lZ9?j+%|fc%H!DkZ-F+wfrF%;tnYESY+Pgan*$P^ct@G{0EmKHw z*wNX<&gwUOb=-wA_GT2zs^i(6IVE{y%0HguK!#Gy^*;2^klFT~mR%uhqKbrWcX-Gg zZzWGZZ+>#sxi!|7>#x{S5wVW%tf|1oc$tjSBRzj-tjcVf^)~C9tY_(!GFPQ```GSN zmo#@E%hJ+yGc?@#J20_#QN}q2gpIMMi@cmL?Vq@-`4<)ZC2o4?1ckz>9Sx!m zUKoe7mM7&;bf*5A@xmi#pHF-G@ye&inGyO)b(bqWd|GH*%R&0D8euzQo3G4a0Z&AB z$-o=sT+GCLQwz9q?+l$pYUw)y3G7e0jPwnpXH7_+`Jqzcy6nM#v79G+L;9|a@!6sJ z7mn7k<8s|{sDL^guO9+l*J8>%aV9X`H=E73|DC5$eplYB5$B{c{w~>Ww=C7ME|flb zCa1bTwM+UjBg*gN>#56_>VNLDvGvM{a2_)#`s>gEil4k=_c%sk_f)*fPOCfgcI1E< zS5%2Gk6M&X^bL2H^)=Ng@IF7u$epqpmzXf@pJ5y9-sX(7@2i9>uu`X`a=Nx;g8PQOr6toJ6NZ zhJ=p|U1^_ceQjxPxucAf!Ly@J@c!i~tJM?B_y=ED_Abw1Ls-T|RLXTPZ@<{n_T0wP z^yX1!NYmIZJZMAi#wzg zXl#$yo|T%N;G4qkD_fj$cudF?y47P#+nw@0b**O_8ED%R@&j=4z6R9oAz~M-0gEWJ zq!N69aVu~sYs)7+@txa8_c^9Ueu&B+-iuDn?3-L8b%f_{BcEk-*sh2p;eA5ZIpbVE zhwX9}u;y0!P+_cKZDBkgt1V0pNqC#IF?+mT*0w1;uPZm1kh1lCtG63I<;tS`iZd;` zWmLMYm6V%WNd!cy_E76N*4q~=%Pni{8|2@-6;eXe#&}g=A@2}AFalzkJr6!D`>-$L za$vNrZsd%(Ymo;4e|IbRmAtGutqU9rxm_6{UxmC5DQw-KtxtK7^e)TJF57+$*CIAs zgeN6&(7Psyvpn}Ly`u_7IGpM9LZGW>x3{LYm-e#t4J+k}4jmh^&<3$q*(H+RB+twE zN?WEzIxLp_x-G3kLi~r58Os8pmIAKHq2(=W#nHfa{{Xh1hFRY_&xTeEJrG(XY`O!m zmi@cki5>!Yp3fsPBFj0L)-GL1{E&3PJ3?6;`cq_R*j>P|AF8+Jm#tgF282Gh#uJypME0@|oKJo40#5!3m_&i6RkVB4cN(M<1fI@Ej zE&O^!XU9LHihns^`ZV*j%edmMASzpDIPZk4jhx^bMvrE$|2Q;pdFEUH7!mFGI&8J; zt^I{9(>mC(Jw$W;8h+ZjMH$4Lus>cVa5%Gj#!dfxX{aT|-Y{Z#*kw8=z4yCc5=JC9 zaF;R~%UkVLETOQM*Fu?XSsdaCs~0xV)`2hfp3wwgcg}IY&nW438{^0(xmd_TR~O}w z$M)&^+dq;L(r)-%(g#~1$694GAmk<~eVhX$PDeEgPp}L&F0($g4Vf6o&iX$%9j?XY~sVmgYK^Ij`6nSf;4)w(emha%F7Gh&lFx?1XQ*Vc}DK zTQWa*+tLRR*|@}N4~=pBZLrklpIG9WG%dZnw<@WwtcTNL%lRKlLC2=hYvI+ySKBJ; z^#W6T;eki)scAoZugVY9wxp4|CuFZ}x}KF;C*y@%1`ddVdJ+DZ+%){Wu5`#Y-DwTG z8!<8RnlqgCH>iFgkl!7hc`ti7VD|malf`58y3$nJmL`vB6lmz*sb@g!;J;c1OA|sr=FzfF_`el`-jq_5mdYTy zKM?T5_#%vjq&#q)Tyt5&dO8$9PZ}coS!a!D=Zyl|J#VyM4>a=^_Fi;P_D6`va$(yC z+iLqHOE;3B=hE-%4$)XCWUn7`C!~erk-eUyggrrNPZmS`?eE4Fe?8w}UrXOePl4>U z?nS=(#ttzNqO_NYvBqov5B`V#bJ|nZ57tO{Q-%F z1Mb5p4!FOtPRS(0VXtr)ImAZ;Ll?&30}>nAAYW1wEfs;^YrC3Y`9rx1KC$bxqVit) zK@h-`{Zrou7(h$iXT1CUR=`QU>vMQ-XHUz%k}bO%d8PuxLm4&-P7VJl&d84}p~_+k zcp%nY_6%EJWvf(LzCdsBqQ(T{v1lwWS1Krjq<_Fd#+3p34v&xwd6>me1HfU*j9hmZz}dX5s0iGvBLf!pHN>th2b-;w zKh`(VbJerL_qBg%pfzg=o`iwmpKm0swiLH+S6a)btyvC;bfOh`e(;eVW*xK?Z9A~E z?d3;|bj=7bUoGFy{%V2I+99JHy#P^~HR)AgaV_K+Yu{yUZr>A1Tfr%}E4rjb1Y&)EWjD)S;<@R44_L=TfmLm`=*1sPR{Jx1Ny`JvZTrTs z!y(-)=V)y})R{;p@mEF*F$R1jUyBQ@3-CCc@;3Ed@taZ>Bi>Oj`j=e$q8EltbB+jE<7{VH3cKvoGSxC0 zSStpI!A2GTJ^wGdh2PT$>MpWX9!mNdVZik8*0x>R24h)gdt2K>Dqv-t448;^_pOXS z!A5%S`IvPjb7i7~_Z-SD=-IC|08X&RS`?+W0@gwhty3*zdt`anM{6E+ zy|t%pi`)nNOtq9{mRG7KHb%U(Ew z#KecJhIPSI^(5errh9gI>+Ao(dh3;Eq{rp)W-iX`pMBEv$Zc~&9Rd>RN*@nw05o(L z@H|Lr^Uw)lsg~k!vi}x+3&^xt?n40&>i}m^r)&ANZ~Q-bdizT0>$Cz|b@mr&L^X&% z`N}fHam>=&dL?96^xc>>VFhfhtae*h`GoOrpcJ z?vYs;nFT%T1IvMHs-Ax&J1un)Gua5KMM&}JMAuS#2}>dMJ82bQ*;h6C5dv}g#igNQ zrM}dg;5pD%htr(J~8E;cs6xF`>__q5!h3#|nrCgr-DyMC-Qs$tl5rG!4p*V)?< zuuq4`R_kObS-X<8C4G6upv;5W<@KA=6}c$A&Vjw1778p*FX^9QSsXDx>S9Fqs8;r= z{uRlM6T2i=Pu=LLE$_BDtfxfjfbMeyZnH0}7s7`{7mBE5Yc1s!Q@lT?EzTSv0%7jx zGEtqw@>&P!vodBUwoFaS624yijCxU7&BFqh1JlJb#pb*jPNOWLjg?VEWf_4jnP;$cQEFMElQS&lzN>y%W$Q_ASW=yLJrfsY-O@^{_nozEOSnI4Xl4)pYCZ&* z1-gg1tWC%V{fWD2TK;sue~0~YOh#N(y&^D1E*!Bswsx+EQR&v%-ZzPBKW6|g0*s;>dhYnNE)&G+%!#J=ev#t`e0=ytKA!;*R7bdpfvUAN@r z{=v3DWOT$5+YPOyC*9kH@33wP{UzkSs&d2V=)aM1JNqO%?C2cDbOLs9YQc{g zDP^));E`P1(H!`M*VB{gAp1qzw|2K8mr6v|Dt9qiSTeh!XbD zfc0bCo3d|h@2{kP&AtUpw~_3F_qy?jEtl#!y1FhqLP#Ecm$w}&U|jSb_tm9Ij&)J) zSWoWVQH!m0v$8*(POh6c#yy``cgBVPWqYFZwso-XA~Wf?Y704=|G?kU`Sn;X)+w_mqGMeii<&V%SVHd5> z*n+?wPo=DOo-^!Qxw|r2ZlcVSiM}*tT+-X@lVU*V%-A>4b3!>6$phZ+`|v*FG;3<# z8r3ecw0g(4Gpo7p3*OiM#<9R&fS%Ad^J!v?f0pkCspe=DelX@r>hx5VDl z*8tkHCHa1K1@&gcs$3Hz52)iAF#@7Az2Vp#R?c}t6yY00w%?mo$@@3i>b&i;#wJF; za9#_vPoDj*Qv9CeXa2*=H?G6s`E30mF26OLa_R;g>~Dd`BZDoJmfGA>DZiGr+4mcN zVec97V@x-fUHp|29$)sIE1{cvofH$*ET*SxsBi^*+CJke8gLv5D`d|{+Ch}*S{?Y5 zjYihJt~zn6a-~H_IJ@~0Qu`-ZQ`@KSXCEvTTzkR|y8*Fkgd_nP>RK^{-Vq&XBWoGk z3q4obi}YK*6h6vPF!EgZYPF$f+xv#E%Dm~AP&YeCwz?el$;JrxTTcaFSAV?x0c^m{ zj0Z-F8OCb&c28Np(~&#;RK%CzhwWXp`-!99EsW2TbThLSe_(GBma5D(*0PxpjbB=d zv{qCfv60$jc2vD>$!6D#ipsf=QIYF&_sW+M9d4`R?Uq#YlP`6Jce*kqVsb2X{iX(J zm~2&kv|V=8wHkb@Kh*eAbtoN;&N|_h)PVh1#DVZ`w&KQ)Pm`ZzJ-+iQJaJdxxU$#L zQT^c6+zqoo`l5};idRkGi~KzTKhi|y3b0zgRE|17MSYjMZ?0OA8P>VpcZpH&zDVqm z^_GY7@eUEqbuQ`rmu{h2-hyS}mXlXaK!6RD$ga(o$eF?VvF12J8k_qF?J zT|c!*`pG*_`4HM6_HJ|=X9x0}mnDD7SCsqm-z?GFE-RP+E7+SE?)UpUuz7$$-rTvs zdY4-=NPN%7xt_j$Gb(kivC7deWUjRf8x&ZjU4UJQC(NQf&9-Kj(g(}+$V5Ga50M+% zB3w#zw}@r7`C7A-g6}58KS;jhYi#$$G{}`JvWl8&jPu_Kd?7i*szxmjd#2mPL$%M1LOd*$B&XF1ikJL63H zCXLF|XbIATZdTT+)Zq?gwxMEo=D+XPzKD6{OzP`jWp~8nj(Z+jN4lyF3pm*C%2@jo zOHbMeBE#G9QvtzhSO&Q|#Jns}q|mdNIrcHWPDz1}aY+Mxz2zzqKjyiZt4EkcwF5>) zDko}ZYd!T>@sL-OX6X+yzRN0Nd{C~3youQz9TM7^oJo`39emOAZId+Fm=+QnRW|er zx$FNUGb8Iye_rMLkOap6S3sf^k!@0;?tLfMumc~VqqRK3tsHaSk9r?% zL=3RS>vI$5#}|A3K5)6N2XF@O){Fbw={>bXqrDn{)x`IPO;oTv z4k-{ZIO-qgN7_Am>HAl&^mj+o;`D76J?c`-D#v1AWGD@hRQ+ME11RW8P4$^&rr>&S z`Gd2KYhtbv`F@ICXCJB$PTKfxXTqJd*4hg2x_uFo6y8hyS3c*MZvT@W_I}9xIV0KI zjh+LhtGw32j>jR^kiD+{A?KBtZ14MSFQcBHDp1P0KB{_5Y3EngD~_dV3;j{X zy_B`dO6Cssoi)FGk=1Rx5VGERJiK*Gec%-S&a>gu?03$DZD|#aYQ?fTk8Ay;dTMoN`Ir^qnU;I{)Z{;2&v_a1F;8~1 z@?&^tTzE{OkX*`OTOsQXW3Ok3yLI-r{*ub8@OrLmp>eLn(5E3ABF0B9bRJ<_vW~s) z_S*9%Bh{f5a?Xppl>1^-8S7#-kMaefez&)F*7uq9{T{W1t-d5HzpHQUWtZ^YLw-4MSrr_B~y8ZaoV4hek1Ks=4(CCSuf0IjkR2}Hnwzej1BMM8gCgDsFu?H&A;)7 zKGpMPSN#fv*5bVRdM3Nth^icjk3vP?8=X_>Klq9ywb0({er-o6<787$Xc2L?Nqe|$rd{6QWi-~dEmGVQp+b{Y9?;hVU|4FGq*wv65 z;JeywyRLp`yA8~F<@`UVWWHMPH080JP&0FkCRu}l2QkLnT9#LSeJ<9Ui$IjYk&Nd^m6`=_DgwwE<7S{C1+34RtiF)pZ{d6~nK^J`Z2$ylUy9eqr~8y>M=%m9#Qx=U?xRZpxH5MsN4z*m{~uLv868F1wc(a)YlIl??#$r9-JQYR87#oy?(Pg0 z90s@G?t=~vK|&Iu9cd}Ivm3tmoOOQG>Lz4$lIpIir}p0Wb=}fi%iFy*gj;%BzFEj? zx;nNHq2TZTjh%1XNhWEf^HRS({g{(HDtEUzn5!HW7PC6!6(7+1%Z|&`Z}Rb+y`TGjnvt|W^SV~jQ761tM7-P0#t~t7lHNgTpFbjR zg1@t=+FhwlHF@Vr5?cdg=4?A9cLF(d+KU1^Y+03|~W!HXi`J z`b6N1(90-|4X{^ZCLlIgPn|wj$w(IvZhK_ym@knIxHf$- zC*o`LkMam}C~k9(3maf70crN_^g-qi`vTiOObGmzG4tcZ#Kx)J^Hej*5f&92ahV@w zMkuG$p`w^q$U9j*NV-EW*r#C>%qq>q&ojg9^XSrgsDD`M?oY#0Cgs-B7I60?J;lyN zO=b@n%d|g@%UV@60=>^t1!BXT>}?_iIYA}yt6T}r5k!9t&F8)p`J{iJ>T8CscPxt5 zBYQiWSsx^~>e8F5-?iFECw7LryFF)uKWHi_pO;= zZyK>G{DXa$xyHW&EU&GVkJ1%w9`n%s$kqW9kqX!tvYLYpOLZ-wbS);QeR9K8(c{)M zzF|baVl5&!vfs2q>L;LoPlA0r7dvVz;YcLXU=sfmHj(_w1#Ihamy+u#lepo-l5gHj zUaCa335zN)g+6EG2m1N_-rD}Yfu-7P`izTn9z(n8MPRxJXN$W(uqTa8`7cr%eCqHu zA-lU0!g^z-6fPGk5H;oZN+a_(Y#2V3=uU?^d549TLQ0Uk*#q`tp<`S}=vV5497po% zq>kzJe5J|rp{rtQL>+PrMqB7N)i35=V~IW=EpDF|a>~9LPa-%foPNlka)03*RuA8y zlrJCmB)-iImxfZu!pcYIySLK&wJ%~@d80B`UaD0_?EGb0Eo`b5W%b2+;IVu=+X-^7 zdNo&1nwvO2>1s}K^^IdtWP_OHp(ef8xDT~juhz!MK~^%o-EZx&;AFmzM&h@qy0+i! zv+;*wPFBmLDxbR~Ps(j;wsWUO?k+IfbqFsg?-g4@QeuQria2Kf&;2*}JZdAa@HFBS z|H%E#-W6XeeM@`zsqObt=^k-Exv#+3LXAUkiq8QEs)6Z%Fcr#53QwM7t2K8>FYCLOk^H$|q9=8X&p|wM_KBVv>2p@XP^FLbPW~7;;wP2e z_)rICe@ack0Zw|abz7{{`eRO_bo25we*~F+o%(9TScr9ZH^ml4&EA%Vy zUQXjJ+W@=-7K_(ovbbEwZ;n~SY_UR4y_lkXy!_==`YtiRzAf;e(29s@++|2S zZ7}-)^`jO{Bi5qd+rQIW$rjW$ZkogIn(vy;IrYr!Ro|t=ZmFNVAy{>HdUQDuW@H#2 zyMQn@ndm@u=DXNEbP2=lEt>NFW5o|glc#%^AT~#h=mU}U zS-Z*WAB_ZToK{%%$zaGJDCb~TdFMAi(bm!(5;EVBN)%Vm=M+qiPf{~0OFM`{VUr5C zFSGe0Hp)>YZP`pe{eo{@+WoI`@`LP4`bno&pnO!#h(zw5bx6)p1{f_>hmnl!q53$7 zgywTq&=hMh9?wL(Zx%S@gqdAwTXt+}t&9cvvyp)zO`@tt^a?-AHq=}CW8~FJV|k3S z4nF%;X`P#epHc?N4b9;w$w>A+Ob>He-sSIsZ!5pA&OWXbw~dS37oFgGN^jAJ38%!_ zz(3j!iRa(Z6HHUrJg}5i(+Mk&XvC4u>)ae{w>U5(^(&HkB>S;io>L=kMk!$p$d}4p zDO=bmak>qOMZ01#Y&GX)wz^eHJ!wuS*Ymu)h;2A_T9}mfM^dHa{#ol)hTk4OH)3$e zHsZSUudjKa3H10%$c(nn2*U@mU#QJyIkADHT2D!euf^n;PknRJ8zimxel>HxQkI`p zU~wcHx`J-11_CR@SLzBl13xvkAmgYLj+u5pSq0ed_wnhx)9rBO(bdhxzJ2L*X8XK= zan1fYqEn%hF{9iW*g$!fxLGTYEJr&ad+@EyJI9~k7M!R@p{+=kJ?c2-oJU_U{>ZzV zv@iKYdMiOj_qYZ{7Y>i)`WW8=z5N#h!{vF(4862INQXe3$2=CofU z?aI*Xy~#CGTV`JrXV8lx&qY5Ei({4}W1vn~(yR|NS_`Pgw7?H>Cvjhl$J$VIJf+xg zg)I*~!=@VBJkB4_lT$KUDI1tUA^$|cCx~pPo)@|W)(M5QXww1)StC3Gjy+k33Cz+h z#7-9GY3?`F>#d%4Fmc5fN7^fYH=?Vvom=HG!cf-wNBXu&jrDsbVGcvxOr|Z2+^*zF zW34!R9-He(cM?pT{zp!ml*k`qW`y*XS`?9VVbnkoJg0kcMrSl+(o%%+mA)b|E7Af9!l}l_(*4gW6mgQp7Br^8Ym{$#8%=x zvD@@IhmWsm9uW zrlj2dkurz=O9lAvwgmfh{wlUtB!wn=KXQd#E6^aayQ?DE!#^a=m8yI%l-JgrWLp+~ zz}1P3Gu8>ybF1fM`wFU;%_>xRb~bg)*qFD+TUA zKeqUOppmx5;X6Z{vXZq&S}krBD$7oNrF9&eL26_Kk|%u>FKeIBaNBd&P^XAD5octs zNexdib6XnQxl*CGA|f5rkPTu3?>ir+PC*(_eb_c!Q9g+e!>a`tU!t@TS;_wvnHJS0 zq!(T^&?oEfjBYs@`VCupfh&>EL+7xYjYC46z#eq~vYf2Qq5NGYf@z2h5E$=r$&P*E zONQX?EH+m6dy1$0NIjStC%z&bu2lDawuzN1^z@DPHd%OkJQ%LwAW=g9n-=aLAS3L(ov4|>VXo*6qx&b#;)bc z(%-QX$Q9+a?}C4doD^nY&EbVD{jDxav7?(F|&-n}pS3wmKT= zO~%rGCXU^RC8>GBHn~5T;xF0H*y>Yp+NZpc88!0CfYrJBs|>e{T;8GW**`8HY4`9q-%L$dgjXbCw> z#N~iK6dgz|rpqy#X%N|4&XfxsSmfO(RkiZj8}3q}A8jSfwz<_aE+rpG&j`FEMmjsX ztJvlfAJyVgtQ@Odx5{GOz?ZR>45#MMrBOnzr_9#};BI>xH|+{%{!^EE*z}Vbq4}&{ zjsDF&H{`Ny2wuVXPkE(wwQiw9$X@gvsug{k9)eaC*9Cm?0;@1r)g2dd+cv?9@}{N7 zrk%^WAjXhq9bZC=+J=JbwT;+H*rlDYy5QTWw?rfSGCm1Ar;iU@lg_DUp?_S+B|GlY zSG4KAvRVIRIDE6!^~|u)$?jUVxpchoMmYn7;Rag(%`POBwAWp;mh_HQOgkX z?a}V9_7-S@AJ3kgc{FFH=*4z8XNJYNaw#6Yqh-p|wYSJ2@;Wnu%V5_tcga3TdsUV` z={NCP%q0E*SBQ8n$K;R6e&s0wlccqX4%~2l2J|7j>Q&Wda!Ktnup}da$6FH|+SM$# zv?1Rem~XVBYB(CWPuWHxCw*Tti~OkPxhhS;-aBTyc-vL1lobLjv;lZix-UDE{+Db) zTp>m<0@=^10qmSed@f(sIWc6m^A1^C%+Jcq7?8V0e2;{4xa*azCQ(Tbhe;Ph{@u*L z5@1^YCc43@Y-K22eeLqz`k(0S@ltFxZU$LiH}bxx_sDFT^Fca~uH#}|CHX6aU|!cl zbq6>dqnLcY5uZg(r)8p^`B8oeoaiiOsF2n@D7U2VpxiFABTSRcukF4YAT3ti;K!@m+kh>yBf!T_S53XO(<&8QB5# z%bnodT1cVMgsaa27cmC%<^iyVIEQAbri6Q8H; z){o$8iDYwx;PdbIEtXkqk{u1H%$Gzy3Jvp4XQ$+jRf-Yw$Zpmn<*}S9WoWI*dA29^ zy|yiUDy1Vg&{E`b;w;ip;k?(f=jHb@F7mPN^pFXTtCmA(pEo^FP;nt0V0x{P9x9fV zXgw2;bIuOaLu%VLGVRG>$RaBRok)%`8wf<8qf`}{$~>^|b7eETjFEz#-_h4Zz77-r z448+<1hU1W>L~oJEh%KT^9D?Rp~Q5^7>UFHrUS_v6U8RNX0-;M$3!@$vIkAeH{V-6 zznZv1n+`dqWwO`%*tb~>H_wst9R_!qOu>30o2-3kB|3}#oA?u1rIz+Oa;o|}SeuD1 z>~$`SN;A)cS-X(d%jj+StO%{Sz>8VxN~A5l#Xi(=kNb}q&c3Gm&=2Y1bZcy_K1M2= zZ+fnH!?dsHQgRP<4qc!XQ^v?W)husoKO9EC=#fK?=%L zBgj{-4H>LDYL3zry8d@GLc60OMiYI$e$z0ZKQ;sT9a!z>@kvA#$^*6YOr`>p16{Cr zSQGRYEAp{n10VA`wTnDXx+|k_CSD>nlQHFH4~frEv=i~1>I>%0 z4|D~FW81MSxOS|A=}c`RdVmR}1GWx*ZEdo;Ae+q(kOk^w{GnG-4k{_?A}~<(g!ze! z;F#EE-9fhkscjSfiY!cZCN~nV2sinP*a-V766=HPHj|8fdeDZLq8(I+t7X(oiID!2 z)+iknH%z9jQiBB62l_%YNN8<}_%RZ)n{$btbPRQkyh@E>AFwZ(DE2)QO@G1r;~wlH z+6P<-<&d6cq}c;#PC{idkKatL`|S{;uM}k zR3!hzg9M41s0a*{AjLXJiN37PRBlLybVcn3)BmT$cT#1j!A+HZ*KlK)_Fa2o^#Rg@ z1Di}<0M|xw>K@d8KhuTjL?)X3Oz)v`?Brf?}?5T0>wyJXP(c zvsx={v57gWxmzzs4EvyC9a9CS7&DFqyYKyKU-hs(oq91dad^(44&4V6esx6S}+EWhd zC9$4(jTgW^Akj!7N^Ku*5o(UO&hN3Pu6fNNz z*x|#qBI;VrV=f3XbYUIirB~3E=`ze0I*jf?h0(8QH}i^K$2@0UY*(rn`2eO824a7s zy|68KFD%ISD`E{c4(Q3o9^eKB`FeY-en=533GB{&VGlM%=R+mVz;6P#`3|}WX#vif zAV)9R2r>cp8AE}Fa#zK);1(n({GyKpNhCqaN05dw%?gqiYNB53)K3aTkj(Il9CL)q zA}f+P@b5o~s^9`WiwCzUSS@fA{30hvNF782`a>}1H^;c8TR>pIwfo8vrKQ5a^@UMd zsEgI$u^jx_cYZ$3HvUJT_+M@>nB@ynA%11ug4x0#5h6&r2|}Y9VXF38Te5=m^u9) zeMtba5}d6JNv{J$rAxY|UQ_>q$KW>A2%eYDn?7X1PlE7iVj^)KXu>sMPgEr)0Z}-} zlE}xa0OusgIypg%fjt&vp5y_C$Bza%I6+=X@QAwrY?#Y1b9^0$)9v-OT5#(O^&y+3 z8br|V1ink!x`!+V57zRZ{>!$;aP8r3bNTZfQN50 zIQM>eEnofgTn1g2!EKA)6Si*PyL_t$y-F_Y6+C}sthIu#7xYX9zxK;D`Rnzc|DXHv z*Vk76^ez5!Mh4w7!F?5U0S0;S!F?C>+5B=|y@Y-K%YPMObOR4x@cjvTuzu~+px-bQ zyo*8K&8DC4U-0{%fuE=2|7}6DY-{k}vTktkM zMtSrpvKm~U!TlWce!8s};Qo98k6q1QUaW5BMzgcE(R>Auui$I1Oc(MTS%tPoSHT1` zhdsiUfbVG`aR;_$1PL2P-Xbyb*K?A1h*9`G948(i;EKkcSPAA{w7B_P3&TRK`?3R} z&`Js~&M}J+lqLm6s)Mx!svhX8Wf&XeMPgrhg4xBougp{f=4@%dd<=|F1+YkKmr4WU z=DC(^Z6j(SLyaGXhBmcc=}WLfXoB(9!ti$Ru9d@bkanm*^ur68lks`vHAwvaAbTN? z^>gMWtQk?snqZEH70NAano-Ym0Ko~v78%+4ZL}bouH8oVV7;|z`V3$zZUEXuvi=_! z13h{#14kn9I!Ferd{`t7t&Hr0gm)Vx+iC=>@jB#bq7Rq{yQ5)Xn~8@chufHGJwlwY z-a7;=Fw3|GwzZwe21s59S$ZR|bMQTzkoXvj$DUcN={f?sDO*$HoM1+OS$q31gheGANN3QP){fRHf_ zax&G(IrvSanpp`x8)e`)s*8Ly23j`}+!(FLqB{^8*@6~9Y3rc&!-%ok02kq!ZZm%w z8H2Ve3cZU_kS$1qT>Ub*TP$NN0~6a;__V%-?EF`Alo6zA>;SUv7c&HGB$a`bc?`+? z>EI}f{eX;T8u~Yyk5wkd!J23SeuAn1$>tH@%?(iB2?BoxYm%~PKkNsw2ODMKW(;!G zItwO&fiQu-41TAs`XB0HqmmU0JiOBIGZXb*&`ZB$bk+VcMp*wC8DQ1%=tq!)_)B2c z^}=4Gd*KM)fxO8sH^P3heS2bI#5-&w(F6TxXkhh|jTEE>`p#@_tcC<> z@OXFqJhq>i!E>P9te}=PbTl3tZ^+79t)=!5I)Mct0vULF%N!9THe8z%YDlh(y=mTtsroh@cj*JEe)G&0P6(nB{vGxKfw-_3v zb*8}cAn`ItOio6^V2Z_t9Dsa#UD8RtAXm{Z$WZJnzM3jYY#>JB&5%Br23MA0uy*N> z#9Lp1=1?2$n2srhKR$lLDt$>8eOr?miU!SV3w$jkSCJy|&`oR4q zh(vM${TuzA+)aOGC1wrfCsxum=*M^)`2FtyhkSx%8bPM*Syhnw$caLuz)Rm6-%~#) zv=(^(P2XL}i8ob}_1?w=SPO>W|B~D3I&2l%%^h?6V>`wcV^{J8`SR=~swo)pI^k{6 z8P-ajQcj9dk}jwF-{sBr#|hs8t^G$}vVFDK9O@^EvELkv(BSL(LQvEit`Q&4`ItEF zKfWw;oG!@ZQQyg2qCPql)_3EKcSa*E*h^_CY!}A+Uiy=T#$sdTu!5<><+19&#%OT< zOhva~&55VX7+VFlfbEFAs3Y86JLE4%u6?4t5r2S>W1iq6k-FG$Q&JBp?Szqj#XrsO z@plhM{{8+=;&b_qat}JXkM+e!I=Pz~0bY-@Y-L-zbAwB9{^?9{b#a$=wX-#$*JBbo z4h_>Ig!aBCd3@f*ycyXaGi&Fq@iz9A47?S3h&y3wQU|NlJoE@L2eR}w$3sUEN3wHi zh(B~m=uY=0*I}p4)`I<)RIT~i9BH!8lS5|LOKtJvx3sZY$1;0oRq|xz_Vuj~Tvir= zL1nDj7JZM^C8lvx`98KmE;IB%Se@{j;lo3>J45UVd>#52Ho}OIYI(i6tFz~42yn3~R3<7+#g zhB?CQA&p&Y?S*VFxejz)bgcPVe45ky$M%dx*+V@yb6fZ_e1G{JQdP}s{AnHrJIO7> zvbKX|#YV<+?Vuuj*4;DYuDfe!j@xj&=ibmF2Hq$o&387F$=;j$#d|^Qq%4!ii<^|E z)_g3FC`^CAuUehZWz=)#12=__vhU$twtdbR*GcDH`!=R5TGecCWT+`ZZO@kU8fkxe zng?1-_kBI{cjcw{mZ|r!JhA}33|t9e)_$S~%*S10n{x>~YM<+R6*3}(awhRF$q1v7 zIS5^E8e)3RSR7*EnZydEtk6{r)u6hVZ0s!PSp)?-S8s&=ts8m_GOO8 z_QLi>HivzIqo%zOlV{AcmQy#eXX+mBg6!Wj>ZRAryC-w%4sp7-et=c$V5_(kXRf2S zEst`86EB*xIE8vnOd)H~Cm_ie#oe;?=N^%d)cL{)<0$&hXpR=sU;3RMdro_QHe{JD zYr3*ZyKf0(tgWSMa>z6HcSj#bf~$>d8^==u`kx*fAhO?PmdVTY=PBKR`2I}#oNLc- z>UYZpf#N&|QZTAVWG zD{IwV$`>QrSR=Gg@A0|A*Ig;=veyX1l|=m(us9kJhJAMUvZz_1c1Kfkl+_pLzs=0m z$UJkQn3*#)(ogWO`L zE39N>LR9<6Ug3wFQ)mlSu|YHj)z78U2#9pQ${G{6fUf0A+b(k*iH3R!<)Bp#ChMB< zL$JO|V{eJv6M-gZ7yi1dZ-|?}!`7o*`0vOAtOU~qzhgL5mmHP%JZo{L;h}OPa|GW- zF;ZP)MN>6xs~tyOjBA?HW82Rap;n+{4b(cNnPMH^Ti-(|L>q=wXS%Ts30z+$Zt~~k zcko|U+UO3$Pz#tEI*SUoKMpMrnHV`F{GoF-x01O|Jw~SKCDo=va{dNiVX2SyQf;pp zN(1R$Ko?`RTNt#y$@0X0^8{3&;??%(1j^3W;pS7X(K=dhd7$|(-i#j1oo7m+&Bdag z*I8zE1Mhq3E~Hpz=wGzmrh_QYr#UBu)Cw)*-ogJ#Jw~gcPq7*#kJmFc%eN#%>L#vH zcY{0dku^e_F7*~J2Hq>Z(Pz{nx+Em?o`W^35|PSpah`D{*kjrHd9y{^_fazD%oE-2J#E3jX=ANG8d_JVa*&HBmr^}w7ZXDr zvT8}a^4ew9%wFzmWr%2BJQ|yyTl4d`;e32$HG1`Gvr#Cjne1*sF&5<+f*H?b&-cltF)F@2l9sVLB#J--NP3; zW2l~nq$sLe{jQ#pCwZ%R$h<({g+>uc_LpuCzn4lvcNiVCTV^=E6yvN6EkeQMTvfM@ z@QvIp3p@zJ9Sg{>MvC4BZAjMQ3fn}AH7j}YQbwdzO)Z^!T=kHRxV}_%vJCM%{wG}r zj@@l8zhe!ZrN0)Uq=s^Hh1D0Ew}6*5%Zf(VfQ6ie#D~oqsEw7913iR65_JCL;aZY0 z&El|I_#!HToj~6uiXj)3Ny1I(rE*MLW7)75aL0lJks~y>Vt$wuV~!%;QS0d6nP*I0 zdMlg24|IgPy4au5L-2RTIH?@0J{aRFahC4G-L$o%KIpp8#y8AAP>k0bVj`@_MZKeP zQKivHW}>5H*r?FC_P4|eSTzsRk18|N-+%_VT-1C&0=vZ!Ax`Y8)X`Gat?F7M!8(Wz z#@CXq$e^c~Chijp$@?(>a39}lJ~8sGKZ(w?#&>fab(E%Wn2fM4cSO#lTuexVy!sl% zXKg_lvIx`C{x(DobA%?_bBSk2q@hDiw!M5&H5Hq@S8l6su|6Re!IJ@ZKwwhz80Dcl zwa6Hx-PK~ijTC_tAZ}94skQVM;)JzSd#p~gnxfya=9Eknv|a|f=C}0}$nKF_S|E(F z$QNQbU6z>vi4liwi9>SSx4&aYVw#zz6;}?48A2n)rj1uVY5S0_XdE!^hXV<06|B`f z$TL*3E}JufhC9=^gw!IdLxrv@%P=*uQ`$#0+IWS2B91Vn*opYR%0u5wPef)3&tl&X z<+(WpJmUiPjYwsd*%RH1L-FwH&Jy$kG{tPIkCda8`nm_!+n@ENhzTjLCVIL;>cjQx zda~IBA*=^_E3FLNf9^wGl0*3z+f5tE_n}&vF1ZrWiBgbUVme=lUT8M=KgzzD9>`de z-%1MwN6{5>JFyo0rH9dG%y^g(KjHYDyN9ucU!j#1Vslk6@4`y|1XxlAT8p*!N>7+{ zTw*>$V{jLV5i1bV9AwqO8c?0NJlhC+8~zWWon!JOPMP& zE@ih1wAKH@hY^ht+UyGsP>guRHgI-z$2lu7Yq90lYki1XQ(g_zK$nz~+8DjNX-95Y z&kQFxi`N)!t>tJuo{9(kfI+ke4|80wpJqbv>6RC$A$$CDg-X(C*`}uIjlmxF zOr51Pl;=q|l<|5lSVE?l2lWiK4A^&DqDSB-Y-1~Oo9QL!7oFChVsl9kjobgRv(zhN zvY4B*G;2%FVM#R8u%}ESi_&GNY-$HRmF;6sbiK6wptfPH5LQhR3du!)s=vxmtybu5 zW2v%L;Jt^uE&a!(ZEAD#nQ=;^m9B6`7@+UNGnkwFHd`b5y!B345^%}0tYDHl&oz@9 zfKC@y^&gW-;!)d4J6-iSDfEMD#JBkEj=A3)Ncco;jmUu8vtg? zhGq-sL5xIyHzT3kYi2C5o14a0W{c5n$m>LX zvM_epXsoP|Xlb@oM>}WDLjA}BD_%q8onoS-X^MFiJRa|X6YhJ+UR= zYAAy(qK5HH96j9s+Roz>^fUS2GNz|D&i3R-sMV2~_%vcOE|Djw?aUrqeb)o$QpZ2+ zpJtq}JKycwDBneDbBkO>?HWE+8<%%Ftw}~l$XJZV>a#f02^pqb6MF^vDj0s5OL7!; zAx@b-jP_O6`Oo-vi1~V1RHe^x@zi?rq8#U6)O?7Uh__#R#AzJIB2zWGUYb?<^I}9+mcAdWqaV@=oG`ts`%;;lvMw1#eMH zqA;s*%Xx-PAsb`)`f}-{uf6}Xe}LFPzm2>g;;}wfLv4bPovgor5X zKcLOu!C-2OXB`u4zf-;NS=dt|3AidPTsxdY`4;$A=|$du86jDdb2f+zAfI^2wu&Fj z9K?rM@#ab74s=D*spG^n>yY@;C;E#jca5fGBU>Lnfo!Xn_Hmit(hKKY@-xOInWN7cqxTCva>d~UopuUiGtFGx|- zCyRl>-W&Oyz21PO9=6`$TZk-R2cLws52A`gzHG4x4t0$%cG$0z{*7yN(4 z+^PyntehY=61&Ki)CKBuH3Wz}A=(=yRPC&-)q^(Emgr)<4j%OWe8YE=Ux_`$7iv9K zmWW4O<{j;{5+U7|Kd1$P0T`vf*SD)N$^d1Twm{D|U!X0>&9p_w)A5vtddhIzFLRZP z>V}mvm+KLlTRtgWmX9iflp=C}d6%3d?h^(`JK=gh(QJY)AdXQE`T}*3uEZ^~{b}E0 zOXiC4Lm7;Ci>Ss#{deV=JYJ5HcZfa3En+Vr+W*{lB9JYVlYYRQ$9e?EDiV+J8*t`H zqc<=T^!J|8JIT4oIz3Sf0N?1Mn4^@_9O`9dm)aY8n~Cx^Z7^h!o%j;!EtAgOwcX{v zayM+6^DlRCSB|5#ld#ukEj$BdkVt*2#se)oUTvqM>hH=7n2mBv$K_?pK)osOcM>rl z5sG(*N@7#m#$I7?wmC~PZHY|tu3ki633t7dft}M$JE&Zj-;3LW93QE>Qdiv%>y95t z9ef#H5*rFx;=(Y|=3o_i8(Wg8M{F~DfseC>c0`RbMw#`2HTPNDq&-#E$WPU9bD?zz zt|2$5HcSzA09_2`Bi6I$`8C`UHiR$A4kRC$uZ>tOPfips%ZoKa{YP#pr%7S}_1_I_ zm#)Z>nghHxxaLL zKNaRl_o$!s6ToJ=h1Y~>$GVhE{l@sAIx~T<#!u!hurhsxoCwU0lV&?0$9^|o1JU*u z!59`clV6bmZ9EEOd3KiZm^4SX3z^ikRa)vhd;>xfW%)B)3+^D@6klmx(W`(-vx(MLSt@x2!G8+4ffWQ#puBVts^gb{ z9f|^%sxNgC=<1QoLFWlai0zZDjBO@W4SfcVvFkv)J!2$jb(Fo*UZHnjD=@rfDz)|J zKm|j9(@LYq$sue4aS>e)>$`If4}aT+**eoLO`CK}$Pz{AvAS1#E1mW3%t`W8$e%0h zkzS}T%@zi)#;MPZ*~A+1y}3-?PE2sVacy>vf;4**nA#E4WxCJ!-C795rX|8* zyXosGn8;LqGuxbhY?H~kazgI({C@HyWS?;?H4 zXY(5h`9^y>om)U2g}!eRz7YsbEHi`HV^qy?3tPx4t^y$eTQ8=pQY^P%KBge(pV)dc zPOO-xc$X==sUx;&j;-uK%WZa~F4)(&&O3W@1X9i$DMislf!v7uAw0jz{NWoC_zP)I zO2{{f^F8&<_r6dS>J2}Q&P5XBdrDQ}k)xz*8y$l+lVUxc{R5~S;om~$@>7s`YFXiq zcpguJsl0XpTkdG@9CaWWZ|llGK^`is7QkX`HA1o-(L}6{%g4P36+czQeViYIqk0dc zq~6ae0+o~~@s8)uTt&E{H-RH@BDzpnuXeI@%En3D2kM+zR_kQ~tw5-)*M>Dm1R0O) zCs_1=w#po14U*6Jmjyb)B-jqK7y1g@h3qp&BRTjC>LWdYPDB4P60KHxzQ4ARjuzxg zQ)f^Le~lKG$0~KKXnl>q`r8SoIJS*sno1v<1xPY&Di6mGEim4lf}TR+k|bb`2kmyt3wEH<=6U2WAZ3hAGeW zrN2O*<$zXPKrqNAajb2*UB&=E!ylVmGC6yY!;U2_rXiKPg$;RHOr6` z6@_gyF`xq$ph$c==FkS`&yk{-!(3$xQPzNc>9RQ(TylT&F4dOX&Xh=-N;6Vgfki7b(P@+P*i`!3m0+Ja<5#`+vG$GRfy@l5j-L~3Dc^cj*R zzqMqt1>2SQZneP5aog-un0NSTOOqD`)*_taBa^6_asu%?6=%hoPh~x?toM!%YZ_yQ zSVG-xEuhM=)ySGiQS5K_4`)^GEWVWZ7w$mrn7`YXI!$D__7LyDuOz0MHMDl(5&r{q zB>Krzq!hU=Qin{U!|4Thd!#!NWlQAjxE(8lHkXG7N~5E=F{CW@G=`8~{F8cGStSa2 zRRSyYZrXPtJ>bwbBNu>BHw(!&La;K-GcJ~F0=C8^y@D8LZQ}~E1&z*fA!Z~O4fB>M z^2oqV;l4&#wdAc(-EIskiaq#6tObIi9_llfL1vp0TF9pX{O55G4cMWizms z`g!f5+#v8-d5ZQlBc$)j2y`T|06&KEjdu>@ zZt81Ig`L1}h2HQsy{HPV4+%` z9m6*?#%Z6aCOm@`1e5M?`HfIpb0HVCv*JLlGCqiiBUb~Zj3#<EZUJ)=siW zBfKxNUVfnzq^9!|@!rZSs}0;?IgFFaVKL9YANY&sW5$*s?dSkS@O{dFS_q1E;AAv~q3KC}a7D_9H(fZT@bQ-LHzT$S~ z8k~E+AsOg7VwUzau*=$F8$mAz=V&!zJv|o@wZ+P?KtGW;zvvTyVEw{6M2#g2fpPNz zS%K-y9cMe>%aKz=0Lc&+%7d6wTzBNQ8iVhkzaaIDQhEpBgzvsG1i2~4dlQw*_$cg~ znF}k|awNi@X1`(SR&SydIaIAL)T8#>4T?5~VdJ@IYByAyx@hs@2lW`*Q4{^2w* zFMu{c5HJRsG1+ED% zkWH9@th3-wkeI3{W^nqT_Ix69vF>#Xwu zukWp~2`h*8fMo1?v?|qui(;;V|9U#{9c?5+U!5kLDcDO@CT8*b$QEXIBSLvDO1c~U zQzgWk=5G2nvmd_%39eXrBUh1cMrueKydTNy+XA9dg&#-$p`@t?!Ij6FIP`w%z)G>J zzCybTD|H)^4%Nl_;LhHFcVO1ot~1rp)>ti?K+lN#%o%)D`*>`rc7`eqDYK{AB{kpw zJkUZNs8c%sam~Utj}Z9(EmaQ|pAQwNi{xmc`ZRd9Ua`x^7K&v-v( z58TtYmq+_~v4jzB-V6-&|6v{>FF{8&){4UVQ^%OUsVCNRvmyQoi56c;7fH#{9Z#0G zS`_n;(2RaT`k+8*-H@8Y84>z^-RMz-b_ss`fBK2$W;%Djj^h0m^?>&Ui-%FI%_EwM?fq6)NN zU(Yl*cB9w0^=u5%P0yD-Qb%(X_DFjRHJB}U4B6Ur8GVti_+46}79q9uWu}Fe(Q5k2 z%i&ZV<__eSdKeuEyE#rCE_{Iw*AHchp6K81d#E?3_F!d<;^rzyfYgJ^=s2Pub{eLA zkHYzJKQ+L;m7fN-ffw8w_8)Y+{#6+xMQ9Xy*!&=>Qf1u9oWQSJE%7N-2evGiNHh3o ztRwXT61bE#-$Qk4z$pY&!Qz}NCv3zXY3k! zO5bYy13AMxFvmU&pM%aOL)mf|BDT{uvNQNa)_Z9VdWtTJmCz20aZ;GJ*GkY+g?u19 zzapm-!^~so2I@CWK((rQcm^+QiWKxtocUD z^IjBIo0-7>ljIbmBX*il@drqnu@GHBr(@TZQF0~xcl$%&yDULs*?wGC95pt=oo)=# z1y}V+K;mpn_k;xAE@Ukc&EQaBIf=hRUr9tr|g@+-0P=ddetDRwO0^=VY+B zOl@p7(m#krMZY!(&i8ihnO4CXhEwEvJl-mebR)y4gJz`U)tb>5>oi&Eo3(T1t0q;cqM-3rXSoh3E*k0;2-WJI- zAEMu|Y-_NdrS*WU#X#_02Yq*@Q6K3F^K1Wsw|xm#j5ve!gGr+1$YQi5vH~XLRvN8= zb^F8!Zp+O_=5n)*X$OYsP4yXelv-y_(mvu7*-=;;qz#<~)7!1gf# zI|TFV=~VDu%?r~?0+|4O<_5?#`5n@SS%#ayjC_u_C9||Np@d?9X{V-g8feC=vhWG*ef;CoIC@)6~K+o#598hW!>q$*Np`4a8%=6Z1^{(7g z!OU;qqR9p3^eE&5uq2Ps>&fa=3O$Tr=yND5osjd%`g}2@w75!7qfn%~R9mcVWLa~K zadJhWsltIfx`-8NWSOhrp5Pt5g2|+Rqb^aE=q2)KrO=_XaA6f5|J=5qgN(AChP9fE(GsHi%iyZsZO?=OBqFpl9Vh3?!pP z=u&!XUn%VnbZ$DLy3$_z4INHgHZV~Ov@u>`Z@^Yt0!c*HakZVhTz_zN+4@{bKES8p zj5OX`$+*cSpuNS^z$s(}GYc)P#R(hLZs3!iDK+&r7T(D{V1n_2H3(*fngi>uB-@54 zM|#P(xEGDFX3Jgk#tA3!Cfq1wowU%J&pzfT@}W^oX>ZukQ$|C1u@nl(zvEaI^#}Ea z$Yp$vb?$@qqFh~O31t!Scnr{;_W4rPf0#$yRHKJn0KZ{dMcp=UD~WOoZM6zHgZy{C z`SLxWBUeLn@HlKNRnHb`zfbokmSd^NXsf1SlhgA{dsYeW(3{j0eU4laFJ`|^SxCB3 zRBNkp+G*)8e_*w`2^HWNQZg4+rhWdt2E?FEd3o{D0*RRQA$c?N5oC z$Tr(bca%MvT||~b8X1Mn$J#8tps`A~|35^%1(XxX`~BV3-W{EcFt`niJ1p+HxVyW% zyR*11?kCPnSBqUv3_0+xhV@QyxJfCe#Z{yb3 zGr1kqRIHWyR?02q7Yc{+Wi(0^LT{{DC>GZ3`=jmTT`S9^wanoBz^&lEz&T-pyjMyW zXIg!2GORc>;LkXY*{|CC)ET3oS`j>gtg&8d8GI6sw|;nNcYSsppKQO+=jJvs38pT6 z6F*6&C;~Tt0isiJ$cp&l7{%pcTX5Cb4%DB<4E>oT1}lO8G+nsh-5<=t4%=7opU4u# zUbN7thC1NAq1NyuUy%pAtr z$Ug#33=AAOu_Yj5Zt0Gh9>j)GiMW;hYC z&H=IGrc^=tZah+_g&qr6$y4qX&ReXVX<*F|E{h+fN1^*^qrP=XTM_U{%Ydt0iD*E5 zp%z10Gfh^uXnm~aheYZQ>cvPU#fk6MJ_>n_cw5bcs_tieMJ|9IK{SKetT+cRy0M6&5n%^ zHH?hVCG~Q!f^S|pr=Eu_&*Wvp>{*(}Yho#=F_{ZzjpJ%xZIStpR!}OfHbbOYA$&8^ zlw9dr>_}k8(Rr+`fwQ6h@(d$gc7!eqjkH?W3_6GXsl6%t1Ld{0qwW+(S0#soOQHzU zta(7IJcr*PS8GKj2hlR7Vr-&wrTr+iQ7GwaqBJDF8*XJFBNJ5o;^6=HJoX=sn@at+Y9{KmK(ml+jT^}}j6Z6vW3GNf{u&6Qc$1*bpmZbIJp-ONPk z@2sBUVmv!IDzDN#h)Sv@zr}g`9JUg%LT@4)LRq~rRg-H?Y?2cF+XELPJ&cY_SE8yo zC$LwTNu0I!a(=a!Fe~}DeXrpwrB9>^Fgw|b)@AXa&&4V z;b+->yd9O0uST-8&&&YFXqG}<)FMhB{WqctH-_zGb%J#L$?9sz>#{kL*c197sjwbo zC&U<@-1dd$!ANI+OQ|$j22yC(IzGc(=doDb64+PvuAB|;Vmw#>(mG-84_{_}zIMr}I3?HK`!aaotsmS^Qq-cd6cnjU7&P@0Q%rIGl0rU0jq zm&_f;PO}iPhPuj>BF=!WVz_=5O`#WZUzyn$r2Z%{%A%lZP<2;JJL&XE7sD10p2kXub<=?%n*Qez^;Ho!53zGX}gCS`>JKMaH0!(^lJ2hTTKF(2FSt%$3v)o5~Mlwxb*1l_`zAMP1-}_lLe0R^kNg zJ9(R~Om8yd#gY<bX%b`adqfgwRH#j@H$v39g)G7G>PkzUhn20r+xSATOA?#lxYmil3Tcn?>__ z#GfzoXnM&2VYvA7w#tyh*I&&ixl|Q(<67`edOzeTr^-)_$yj~RH$skxbr7Q?sdi($kv z6b&PGARo|z9mWdmBsG$|&Yrh+KrT#SRN7fS_87ZJ9}_9*{g@dSxrr7)rb!0Z2rp(L zxvkb1Z^PH-M#JcT9%!`^3<518zzbvKGe(&TD$ESxj=)^8l61v5%r|w{uO(pT;&Bk5%2Q!93?TF*ooLpd9J1H8yf$Td~^UO$k|b@J#YJJ%KEN z?Ub8gi4M2x8aS{IMr!*WWXOJ-Zs#1%74!nUHTt5Zt4*vBQ=VH+Of!AT6zO-MTy!um zfvzvF+)#-Hst!Ybuu>#C0(wXOva1c-N^c#hlhG%&bk;eg73+1>vY%!46PESEyiJL) zB65?V@B^^kx>4R>`~_99``AabpaD!kd?0b%m>|v32Gd(SxA@#<^U#0Z{#hk5UBXi0 z9aoy14!N#-@ge9udDM27UrPkkyUJC0vBsOXVGVjO<}=0^yR72GZX(I7B2#)TT8fKz zUBQ#3LE$z2f?0zFi~y1a(-TJRAtpOlfcM%VM~bRqJd^IpzZo;Ea##XBAE{=FI!h11 zT353CRH$s#aNdqfbFRa?Mh^R=tWlW{rS`-HK7pKrtwn9vUe42Q!o9=Rk{qEA)7t3+ zu>$BW>W#kvj7p!*7j@W zy(7^vfjVuRlV3=$wJ}y7Btn|=D_}eHHr9%*!?|>W-Q>)&_o6nMd%~TvhNK+`IOS93 za&Yjx#Y$3PW)%O0k7JYZZTcUorsg(}07qpp7Q%Q~p+}(UTuJR``ogzy2-A#N;NV?i zd^6S>TVUkAQG02&#aEIN`Gu$ltHs6fbl`V-v9d-5wWn}CvRx{rtWiyMo9dUJh&!c^ z%5`;?TIXk*t6Wtut)|i5DuDGuxgkY9JJk`=2irkix)bE{rr>$-UT7EA7aYxfvAxy+ zD+%j^^5LJ+F4PbE%^GALGKLyO!Sj=Zb3}7uDba-dlitpDWjBEnIWPEjc9Ffnf!&MR zMScg~^bpX!rej4=hIK?Qt-h8=%ki>PnWO%pF?s`oF#j_x>>DbAr=vqC3o8vyqEaXs zSc_4jePwg1eo@o(C=ap*5VUK9hwM5$H|m+as82D^Vq4KP;vDsaiDzFjYuRV4hkMK| z=UYNerY&RxzXB~@S7H;1f6nq=%iE`sT!IksP+y`pIVp^J3SWnY_Gy0hYtlrjktUC}M zyTNliLY_-o(0je4*3vJj^;9P+JJpTIi|quRN;0@24I=_>ufa;Pq66sn&8Ke`jIa~b@w`q!~ZaN=$1XYmsG0-RS-AOBGHDD9<>(!-debb$Js90#-OVki?U1&+FM@D3^|RiqE%Qt^ej zNA9g|*TH{h9mh*iJ1BW_Bgy~y13TP8Q%i?~U=hF|p65h!K*riA zz8ANNpM-52de-s zeY7$_PLLl-F8RK)NV~1^T4O!x*GI?#-&8Uo5GRT8s3 zAG4bv%JZ>gEg>`|Tvgf{T<^Oqb~YiATI{Nyg?tN&E@9u{9A+QQA7<?z!Qx(3yc z{=^6Euer_mBlQua5hbWA!jZs*;Kna|j0Tn3LCkOL5OR!0 zsORKXzFt4F!@TiZKuC22n_Jb4{nByzq&p{B&l<)p@pR!6si$0bU?}zHaDD~v=I21Q z`5q*t)uQ_mFU(JJn=l)xDmg zSZ%gEB)FjeVibk>j2Wt?Le+5C$h=2$>EAm2m7eHLk5BYj+mxdnCXllFrSk8Lkx0_}ry zps`#d)HXaYaLfBgc%M>M9|<|b<=8v!Y;k#Gs=I626Zzt-i03kEf*W&~brai6$V7;= z=plT7KSr-ZBkh}G(DcZ!Fri;+(^66j#vx7W4X*Z`U2sU_mr<}c)Jahr` z$^IKXlYQ=NW*-9;RSkX6?)o|3wNS}Oqs+HISpS~z;}9?Oh54UPziX5w4g#<7KAKL? z;zV`|dw}0+KkS%jyUlcFUoqcN8)KHH+s6U;9FWf;}R1^~;c zu&p(}lwQNyooyU;2*}*wYGH57o#uwyzHk?*9B2-BwR7Qpng98Y`Cp{R zrH)O{9r(@vUocfWMKs43m>G!S@~{V}u~13+my6O6Td*0nXY>}*$7Hv6VWW8^7l@W- zT_HaBA`%hO17&@`N&Ai8)Evr0!gADLN}z}A0asnyc{o!i+9!bWpb(qa@rc_4DR!%E zh3rGAR(MU~yml@!Od7AP4SfEwDr;T%kv|x4%e(Z)+ERTsUfnj!{)qm}74|q{^76P1 zbDaUMZx81qdnd?#vZGPPE8MP!gpJ|p!Hxb@Z$Iz)Ku+PR@FcWGdJKN&L$HEV%QlSP zV{hST>OStQ=$z#m9dpu!*=mp-i42Ss?*^s{4T3_(v-B{iki40RK~8&U9RljdT5>D( z3~x)t*-Jsr;53eRitcLmPW(t$F840}PwF%Imt{p-gr7wwgombuQ`0lfrF*l^fKH~J zOhIOk4YgoW$%{bp84XpHbKIXWfw{pOwl4M_wpLtMs-HO=wZ~1haD)+RM8;;d$s8D% z;qRBZ(OV@HQsx*f)$G&-BgwSU1g{;b<bF{4>Gr`Qhm4 zp!gL;J8(K=Q@Z;H`3dY6#~{y2_d45Z zN8Y#|u4VW}pnw*o?n=1$P@N%u%ov*0Go0T$|Hr~KhxcM=Vz_MJkcc2*rUq53(`# zBlb4V>z=fPUJ1qBEgYX*d)*Ih({KwrXu0%*;(y_!;Ni>_>18s$W)Ak=_N8Z5^}iMR zT6xUH=rlXg7If^7Y3(^1(+(=E9>!z}*|X@6dVOt!bxNxgWW0@iMZATwhNaiYcojGs zyx~0``a}PK{ikQ(pY0W#{aq8?<+9z$o*rM@`B%)EglwM8%ym3Bvl)vQk4DDJ4?}-v zifJc&JwyKl^90{SJmA@#kG;Sr@^>6}?UP;8VizW2aevs3+DFI7czomsvn6>HA1=QR z_7%#97G&1=fu~K(T3w?#cmak!EvtT}`h}TuS8fPdc=j4|<%f&s-cEG*J5sqJ; zbEan(6UQm`B<6vYVz$+%3s*xe1G}@DdSm>R)30Uz7P&8-689+^&<%1M=$9wko5V== zIBuo$0%+ziI!H`|WV7zrA?1U*Q@R-%;+vI0`bLJzWi`pH5xJmNlS+$+%vNL`*MsV5PlLV6K0Pyj4$?36-$Y zt#){r+DISg-Rx)l0JlE&H_voBKxWvAvW>B7T0#A9Wp$)+P|Qs5EeS34rKauiT@o{; zE1)Az(JG_HL@I&ta?D+41-gQBT(+yR>zJq1RYz&pJ|Z`M0h?(R(TgY_LaCyxJ&J7c zQ}Q-rlX_C=WlbSR(KNG+IqZDs+Dtd%`y}j$8$>r}2Rc64>f<@|Vb*9}mAZwl`plqH zY!!IyT^@O@4wp|!b@fu#e^@a*FZF~^V0+*yd%#mWW(Z4A1?^RArSO{OFi_t2S6+%( zxK(Jc_&(g-e>GB7+be&Bn(PkjGo;dOWLoeCT{moTg_fo_ zR%kIXEM$EPyb=Zk2YC~ODq5zLCg->E5-*7<dv~JXIyPOCo{=5gcP(F z>Ib8cHbUs(UF7>Nyb{a%D+L;=7qt75BG)%7SvS!Zsu`Wrk?71p7h{e>ZT%hi30iSF zL*s|_odypz_u9&~a1Y-t;jKu83I~gWZ>^rp>O(CW3z2DbZ%1oq2Ux#7>X{kmI6t`Bam16Ot~nDd^JGQtIfa`lC`PXYyx-9Ud{0d$jSvgd`vmE z1-_A4%Dx9($07ZdmS3I}ZtQ>J&nI+^%=K>%eNnE<*CmfR6T4xZLx^eb$jdzkm76EN zc+5V!Hsx|2<`<$?kUcWNe5$>dT0{-}~O$&bj4{VC^#`+DfM7Cnu z+A-i4c82enaOZDmtdpl|P3%*NOX^gHYzm7TGhS%M@MjkF1`3QfqOGv)(Dtz&Rr$g6x(Y4CMT zK(Dd$WF`Ax2gfE+!`-{$UplIx2J}7t99_}$s+yD{SJMWD&w8JQYAAVx)X?v0L#++y zSjT}rrZ)W>f6#rfD2M%cVwu=|OmVsh-^I4nQb1SLJ+fJt9+~XDnQ#C&k>Dw1n|MNat;Fkj@QX|V;Ou_m>U*}tuVdHXt8E8j2iWbVq_w5?60u6t&~D#( z|K7;8Fqe5Fos8s?M@0S<=EJIFA)KT};T4G&{QbD6?$h9JSr=0-;RaX6Otxs~pYxiB zq?BMR=zJZboK?~%NOhHLGB0UThH-;DN>wI?Q?zq;+~3ZpSOo9kUKGED*$&#!9E^u3 zCx-+3{4<1?dgsW+q;M&-&T~+fN0-0fd%Qc|-aWmrTCuA_C@bb=#_{w%pU#irB z6qh&prN~5Yzo4XDlDcGl@D7&ZwNiRn?N9l%RhhqR+r}S0!+k@sw~D zv8u{r%~n`b?M9gJ?h7=Mn@fHCqXH#_vy$7GftA*ap*!4A_XNH+ljcf}?`$uQc2SnI zzpcCRO*{=WxU0%ip}K!p=wHZhsu8Rc7$ZK1+WaMpGG`kT$f?e${4|oXzjbqvPrVAK z9mj0L&3$q&m9^fe3DWrBq!2DWm8u3i2R;gGMNVsj24i7kE!ot0%GHCL%^!%Vov^^B zSVPGH?ya_0nj9&p9mRUf-oQG4x$sDFpkxa_3wH!I{X~<-r{W$wg`VMf%}-=MI~k{g zoQDtQ@;P!7w;+#aw7Ep9Ahif@hSxkP=Z{>7yw(D`srXPkZYsT#T4>K3v(FjgH@ZqB z{N*@hnpgu{ZbxaeUZ9*(1rcgEkSjC{9K*C+GtyYRsY!<4szvo+%8_>Flw%ivhI{SU zW)pDH?8poOR%DF$K$@aAl4nQq1ak(aimO#292dT&@kR-=0g=pA1ZUSu_tzMoErnkf zQ!;J|yFmLy^mYcxo1u^XOmGsNkGT9-1AXOLdMA+ys|t<>uv8QTUCK@xx0Q~W>72yx zwwH++0YUBcw07JA#wY*o9~)_+*Ag;)&Oj02qS8=W8%`DL>w_VOI6HfnJw|nby7hZ| zalSkE5}$4g#0Rzjwspm_MGgrU^oiP3aN!pZ?-f2uACy>mv6QI}M?1lb-<`-qdhMrT zw>diSwOn6gaQd`-fb8qKLLC;$h2JYF+P9F)H{1UpuuD8DDWUu!ukg33nosd|cy25k zebb%^O4(#`F!cDVm{e(p;3GzH>(xtH^L-~2)6gPwArVH!CgL|WRqurt=bEv5`J&E0 z9WQJhp`!NA)t-NCZo&Sw{pQ$)K8UsC0YnB$6Iy~AdmCg7*A9;f?28PO$EgkV9N2nL z%D!UO*;)Pqb02CQ7a(tG9dEIQiNqTkMeJ2K2O9W~fY0tU*jX0A&O)P>Y+;*QugbGuMcXSDUvq1hJ9 zbK5FMCU=d5ECzNG*AZ2hFKUf|Q#erT6`H2rK(CD*!s^I8!6SduUMg3`A+RNY!IKR? zMXV+#vGLAMyiWI_uaSc-&_hFwd62m=%n7r#PAVU+8ptDlS1Nuo}?COgTtrQIWPuxYNbMj)z;{&9uUik3zWR-IwhwvNL>xQ@*dU~ z>MMJPQ`s%{dXDz|Bd)6L40Q|Eb57DV$OGyh;kr@<15+yqZn2M?qMZYk+Fi4T)c{?f z-`ICKa`98RHumTCp7z`J>P(h$Pbx{ZrdghynY6K>%vjXC~r>4$_`*d}0pRzrCqZ$1U; zVCDInd{O2#RJ8K32}D&jD08R~IEn^E618+Hk7FV_rDWIM6BqHlpi{^$Q9LFTyy(i+e62EXEE0$SBUvnuVgldwAXLul~Bt_3viE?P;x|Gh<_;4wY%mO zR0MmX3+8$9n{%jR4ELFe=kl_3nZGzMniQ@TtY&Q`+baXpI|VMPoAou43leg62niZv z9)^*5ao}4$=4v~t*e_9ah#Ax@CY3G+X%+t&U!)aYSE!815{l5!D+V{3FvGB}8t;f} zkkfGjJ4jYxo70W(XG9Y1CdZqlWgD8p^s#vHirK`mz%_*$r?*BMSO@+>xdrFfM4@E3 zn$S{Ck-GvfrX6{dTWK%s>g`PBX>KB$MZ8m6=uhxNY!0Jq@QuH%qUi0wRra5}M0p|8 zdPlqtkp$VihgjJ`Ixesy>C4n6$dma3G`dyleg9-7FIT~qXtoKx6T4z-PBurbtCd&t#*Ye0N4I-(M6G4Q6fQd_|&n3L`k`>3|Y`%s0*9^Hi}Sqt>f z=3iDN$b@VOS+SXrWOj_X&duVkQ>IbI8jP?*{6sUKL!qH9jUD-TlihzqnIo^r8$tBwnttk z4Oipv?8JPtqh(N~?W+A1--Vt`2>9>xc;*E&4*Uf+UkYyc0rED14yH7 zt<%y?jmJI`kI-4O5B7nM=E1cmp6MryljL>cqdYypXcd?k`v-JduC6yR#zJ<_KCC@j zqOO-}st+NDpCB(#ow)^$IOkgK4y5S)4$8)s^kDq7x;xO!pQP3z3$lBS58ac^@qLJ$W*JD|Gv$u{wSju5KKGH%sqlScW5*>t zTuGX16bD{$N9Kq=Ir2@OZH=LT&IW$0{oEQxMCbGoP;;Jcjs#U#0!8J4vXt^LE z(=U9EoRvO`e?{sGkJX9j8+D9+&Te-889y$;ayscVMt6Ct@GZ0|FiKcsbb#+@Fy5NT zPZHKSZKgq+_vA?MeJCOpQO4_#_h~EaY2*CFzveR8MB*K`lv;!jRl})G0w0Khww>fs z@MC7vwkv-bUm#PviMkik`TY8E@)&iOTu)tL=0UDjIl}|oTu{-%dfH}lU%0=ouK0k; z7K7XBm>zMlG)o(SzOw-)6Md4}iw%ugti^S-on>~@hp2mGGYf+i=lwb-UC}Qy&bZdD z(^i@s1RmUEDJ(ZsTSuN{o%Z|H)kGEc1-*{{-Ca3mmh(QFOpMmcNcrVt^`n>&>Miay zCwiK>{-ev2z>$~g3-#1Xh}PDG+WKn(1>`NBpS$k7<*I6%1KF4q)=s_^$)?)0twAdaw3PQ^j4Ac|z`oWSa7iFh$90l_4|f<<|8``S4KbKHiHbxz3am&u%5FP2~5Q$NHi) z3at|+k=@-9t}A$p%82(P_F(Z)S>a$n&e-moC{G4wg39LRtGQY^U)qXLe)F>QDN;gN zW6spSiTRZ>#G#n=vHhJ}>1X=z$&2WG5tND=r3N+rL_NnYbP~E*& zuflp7gAG9k@a5)qVXE(Y2GlQbNw=9&}J!#2wdh5pQZpH)+c0mru{{oz~a-=G|# zPB~all2fCW>v!eB!Z2~G`31R@DZVfMju_@>=yFkCwHoRRU>AH;s#`0>TY;}(9Ba=V zfHawkFzQ-DoY0->Fg-tLQ@dD*n#erI+6gPdDX6TgZQLrZpz$u!Gu&IcWliI5@ollS zVmqOX@dum7k!IjL87i{zB3i{c|@xb?BYbhJHfvBxt2foE~i-WQ6VWt%}G}10T z?T0Ls#z_38(q3qz97hi3gKdIy4gVca(Bi~WLKWqWJ`mxseq4n5ELRFAT955bJn{TB z>s2_;KRdwdcWEzwo}4P55@%{lD8p9Bk<&hs`C`46gOa6@}IdrHhuKUR2tevzQgmZk|K- zSyVqQSMZ+yj8M#Au<=G!Gt*>nCo{x(HTHprx1C3XnXD!&tJSeYQQWBp#EVKBdQhC< z+(ES0ZwqBIZ>8@GZBzGXUuCy^!Hke0Hqk~oMngZ+LC-B-3*8j5>3xV&yu&sUFD@O{ zX7a1DUyp@qn^Gn)$H#?hTYZ@8whEM>r)uNL&kW|sbll~3pp#l}vAwiY%R@e98sY7f zg7Qt&*74pcFbyp{oFij)+6jLvEk&OMTKpH*NAe~mlAGD~;QPxmn!wn?p$^vyT05=5 z#v`K?QI2ysy4h<33GzWi^A8KANdvSlY7_0LvDU0>wZ^-#2EUV?LvJRxVye0&yfd^? zJ#Tzb+sXf72i*{i>_{S>8izuA(`IFT7oQkw^eReC@JL;?owILrJh3MeyY*D#x_-y_ zs5|tRN=ZdjGtmjh6Hk}8agOG=FS5>WcuNX)WuZP@YiiEKn{rQp29xU8#*4Ve_^!NG z|JHn}slPS_(0MkoF4(7LTNWRn7Hj20kJ4Lb-iv%v?E}|0AfOK}(%BvVAxl3>Hk2$%FnAobk9!Wh znW97)td!g-GDG-RpKakr17%fYS+H(orFMnC8gtM!5w8O4HwS*K&hm$ssOPl-Vppk` z@sYCG3pV;9j%hDs?CYMabz~Xs~u%Yqx{b^WM2Vw6{?2q z(*H3=(ADjY92?oIkUF(bSt}%K?TJ=oHy~kLM(4THYy&(Iyx$S~N+t)oufG&#c>84a z5R$MlFmn0^sW}yyj%c+}%bW#c&B@vcwW7J!AdK010c{Da-_&AW$JB|l47wDI<-1|{kZq-4DVPtlghPZ6D4V^*gnlGFh>aYBOgk40fgDmg^ z($(-GITwD`)6zYKdL!lZ_s@8kc_iXBDdMaCm$FoELl1FOk9pwOOpgII^xu$H*n?}w zUj+8YEi};Bs1~$LI$w4=*L^O#xjyXkR?kWmD-%iV4eU4djL{xAeqWuLu3=n;o)P*7 zGCx@|z>I)o@;sP?<3yXSp+M ze|!>r*B)gW{vW%OS&dI27$nK#g(tejw@awv9!6wIGs5%yZJ|Orn~>2>_>?VRwD|?} z;H~ZR=p-XmtOs*ehFp&VMrG828V~sv-<6Xz8(%iIHQCFYB24p^O0O7-m~+SlmQ%iF zmSaZqYpEc&fq7${6?#VA>PWz)?LWG+P2n2QMXAzeKG7+hH?G;+CT92W z6oE|$jZAOm8?5bSJoN8+Nd19J)Mt91y|eoia{*khPT38k{=95i<|sKF@1#40gvb#H zemop|#Bq{Lu^xx^rdCMXCB4L-VYj7$$}J-Sw17*g?9O9+Z=+CHi&Qs`q9fEtdLMBN zkH_Xpx5GV^k?iEST?v`?nb33C-e8TpCI=aSfax)5F>JXNZK z%CZx-(m1bASF?pb__vEYiBg_cZZFdx%MzESZ~pNl>_rcW?%Es0XKX}&L#n`EF%9hR ztt9oGwwyS{gxD7h_>qX&NK&_m^Hm2`DXw}#F$YP#l9pvYOT7?ih|*vl`yCRp;`P;r zmu~N><2-<}MY@EFt7VWv=A@lOE9|XNNm?2CQ+|ME#I#E+<7q>=jFbL{srP;Bfh#(g zcyCnDk7A%ZAtd{_*rv9l#sI02at<}3GsqJ}e{7uAQ4cHCm9ctWY6J8ZcWs04mC|^x zoH`-Mn?vDIh0Pw;c*wBNgAcG5im~xHcqDskt+2ajBG%BV04e$V)m!3;h)0=$TDvEC z4%>AiD6Gp``XhIUL1l@qpl)r0od-ppz!!=0d!Ezt)$Dp%f?z&ScR*3;*4t=@%zD;z zEvQc*uJNlJN0_%pgRtYr@b8EG@8te^2d%GK1L!6mGcSG5InFi?cY&9RHGAS4(02^C zO6z--%SL(QHu%*h;X~Y`Ji2`eYAD%#Gc!DqWuT2XZ6Uoau!ay(jsNZ$3F)PSjRocc z$Yf-RmUuU;fqofyp*^iL`cRW3Y_MACv}H$y<=g?vJ3D+(--nI2`a*X5N_{N0fNK_O z^Za1Onf;);-bfx{_6A*JC8MkQL>?)17lP^{dYsD{qtTtTjp5x{U%i)w*Vs5bxA_>t zIP;;G)L6%SkK~YGMQ@+_*w_yq?nBgjEI}tGhvl1`N7aFKjV7=yd z{1g6xTF%ORK}Wo6x82YDX6{mSb*Fh2_{q(wWN>UdLDO1?&u-7dJOfwiHuwa)%#g9t zJggRv6c-#;7osxLiygvE1%BLOq7u-i67gfkYUMN3%DzStl&(f1whrrUZU)t|U#+YS zQIh4sP^D~#2Z*6$U3>)O0eC|pX{`RqoQvP0D6${=kI2ja?rP?o;<&>-B@XL9B)ilZ z6uu3NfH?}?!PlaLkU+kILHtP2?BzC(X@hmQvCk-Jo-!I41@$z`PqpH++xqj@I2SvP z*b3SG2k>?%s85qKq~VYR@frGvOIQ+eV?~W|>c7AR($!k#RW!${t*(otgfA$2jn1ZO zHZqQa&Ue1rovdo#?MQR}&e`#?Mnh9at*A33iyt?p$OFXsaw+``exBMzS0JYl6|wDB zqBR%T0u`(dvQ1Ewe~1Vq_IKwya+RzspfQ!ucN25y9?Vhh87UizJ_flVE9W&8#0$gv z!St-H-aP?BbZhstXUZ*oxb+BSBTnPp@ilP9can}pVx^$E#wq}xaUVB@+7D{y?@F{`Z#k5eETN2+6o%A)z8Wj ztr}3lbalSuk_6?RiIF5f)PB-dl4%4imPEP>Q-%JM{0sZ8@WSxWP0;u4lH04F^rF`9 zs2){;J;KzX8xUXdsaPBH8Ss1Kz_$aT!|!MTXbz)ENpU|Uu?AgLmnU56wDTE9jPR(P>&eNR!Ja~VN4#ro9(>qJpU9D zh|;a;Mrpm8x>g-**s+Vigc(5Wq1pkNFOJ*8w4&0`7_%ze|4CU6O8TSfNcD&|1bUK= z)KsP&%p;zl$>0?UYK1}nyjkh2WCQ+ujI|uch=D{!Y8_FraZGj`wmqcln2Xf1QaLqIYoe}F zGUdZ+Q)4|gkPOn(>H5^)xE(kaWwk2u-AFRbz8Cv02UrrdZKJ}FOYoN5 zK)r)A>@3#DDrgNb4#4cEkogt#B=3nX%zdUji}-f@cy2v>x<@FN*+os0J1GAu9#{)L zZ+=4Gh?A7U5d0Nx4pW7Eg$fv*Wd~?_G6Pc}pLIgm5@FTJoQ+hn75xu5k}r|X2n@e! zc??WH3cQH>`XcidmW-AVcgO|IJgz9WoN2(Q)J43um2RYJ`{e23CvltXlRKz+%!24G ztfbr|!_;@O710i@!rEHv^bP7Gc{=dXD{B*=kAH^!;=VPb9H7iz4Q}jMybT_QI)L}? zm9^1)XJm(L{CFUe$Kh@9)%X)U%7=49eq0RR&RVRuQF!^QxI|0@PQYDlp-~^(M=qo5 zu{YW3TruV;aSEFQE`sO!Lv^n5pE?85Wrv}P#5n3AC6TA82~;I;+MmZw>1ANFSymebtMgg<5q$(?)djQ@+8FJ) zKEfz%1oUVtr;pRh8Y6)+vjKaB#p7*>Rpeu!*R>|s5HIjV)CJps+C!pSJ77`1B(g*5 zRszwU$cDecHe21z?M83VSohZRnBA->D{qU{1F}ZL(gY4#Xn7DYB!cXc^W5b3yyXfx3eh;x~x@@thXoe;@{Lh|eP$z>H#%)xe^m z%>{K2V@2~5{xG8iGy+`*3fe8}PwS8WYf<*k1LGG1=&orun*t@bv9-i_pxewlCTra> zZRUN$X*|-G8TSnyo)>+*%V+@^3B~HTX#oWAQtP#T(%Z@aTO}+F8q=)UGJ?EJ{@SMdW&> zM@e3*_2(M+TGe&x3v~^o@ceo&kANi*eSf#DMZkWtL7J7#x??Rs_3`L?0)HeT$=|6Y zG6%JoZcHzM^i+aUs28-0eoU>R+CozCbHYQlBi9i?;>6!#o3Xc8cGMA%hpgjGST`UW z{%V_n^|{UL2^o@K^#aBxBd;0#=DutDv|lah=YBK?d$4*y9iSD4zbEObYEj7XNitqT zjo^)$Xmte6=L>5M)*7uukB}3;kJ}(`lA&(H%1;A&A>_c*bbV$4-H2X5-=G=#4rwFr z6U&JEpkHc2E+tw(Ud97hk8eh-K&>IMs=c}1=wxJvwh3}X^MaRkhw>3xZ>50pRo)}_ zf%TrR@=m3XvQy~^F0%KK^4dk;0G_c}?75Wy>5m@N3U`A0YX!^4e@W+S4SU*H#W-XKXXx z8=KAd#x|p;k=y)WY&W`_dH?TB*lv7)zr8cJz^{F?_14Ocb;1%*^trFF?ATlD*Z*~t zDEU>}sYjpR6TFx`%q>>*?0IKx#9m|BK$Eo*>w+bsj%XdU=r(#gdR9drlLNe5(c|Ya zQ_Sdo^sI_*H^cY2340A|*{{(?a1?EUl!}cgx({Akl!E1iZ6e+gPsF3QyTeGRm$e=E zka@6PR(|WFwFA_-dyGEj4%pvm_BE1>?|MPAk6FSWi(_WDhJa23a0J zrOk=Iftt=6d=sR;ttUDWo8Wdf@&&n`c#AeeesC_hP9_pv@Ga;AwjI_=3tD}Ek`lcN z4bl&3L$#uCMcQNb2QPlAkz@@p3!C4~eP%yvH)L_=$G)0-%r7ub`H1bndPDn&3*%qp-3-Iyh;KvwjD@>KQ`>dSuwFXo|+K7G?EDpBH8K{^IwZHp8%P9JVT2A~=h~ z)+uWY%0oMWjRd~gNOTlMj~hLH^gTr1Zzei|`K?k|z&v4&w2ojSu;bQf>!dl_ zDh>Okv7lMT8fhK1hGB=X4D0~4T6labgKxKitWeJVNE{W z`gMDVbGTSA;=afZbcuTX_hp7Ml$f~ zid)go1zd^$J4W=mzpk;-p0BuPJ&u2>O(DUjW)}f|J#l>k6AwJm=*m#gRnmwD+S;CaqCx$?*IBd3`0k;kw1SM zMS*U52#>aOyf{7_l|otQ2=f2m?{f4{B5L$@IrzOqf3JT7DYb$nn3bTuS{`zE%EQe0 z*KckZUJ@UPN&*A-IQ*u6J!XpjuRcYu23f|{f!lFOHK59@!0qYU1x?ec7?WUbA2Fln zMfBKF;%ZTH5P675CyE14_u&8629ia{=-<+b{cwL#G7~>YWWwwDAQ{gGzxgp(1sFqJ zH|Im$g|r$Oi}c(2BE6BZ0A`$=ehWO5Q7`2^l{4BK1O%wGF7azgjnJo7KzA4LxdZtS3v={Ad?Kb-X^Df#W{;ww-WE9i0)Ps7O zeo!9*ZKzgKjku2r4dg4(R^mhCMzJ>UJ=OVk3 zTY&?;12R){QYqwCGKI`TeuVMl7d$@@9(Uori2RVAo}Y-GfzdPW8!8OH;Di5n78Hdu ztSFWN{6VjI)EsG+fp)?etDn}!>SceP(IKr2oYiIZFnCBtLi&{#aw;>Rz8LLUQ_Vt< zl3Bz|H}+Wrts+*cRS5eEUiPox*zOC-q&@L%csF7j{vOXoyayWJTOt>;P_6$2AT(rfzZPbgyRn}iW})h)bV}D7-aqixc~>u zAJDu1fc@zE=x;^;7Ildi1{Tr2pDoRd?kBg30MU75C`k68YnSEi5P^p1YF(QOa36~5!0(ZnmbN=APx+hDs5 z>xo7G_N(?<_4<+7KfgqH9y;n|J!f>bJP=yqT82NJk}0NusWIR&6m(RnF&CC{|iP|(GgZG z_5^dIHqct47_1ex9Jb5R17ydW;&%KYP+ISyMfg4BK+T{%#QxV-z++cnPps9}6DztO zeQpe-14r8$E4pn3#M8FcD(o@l!CGT0u}7E_J^Hy_hFYLy;LVFR8y;=VQS?}DtR?2g z+E}r0%+=O&Gr?>R?Uk{?=nPl&*TzO@>&%YkMkC5We`Dk{x)>YG4p!X%W9q8orMS6v zlCjmhcdu}9C|;zvyA*dQ6pB-dLveR^FHo#NDNr1WJH@R93N0|H|jaHlwfc5k%J8 zLc4=+dUrFA)g2<*J%O@obl1Um9?6iAX$|`SbPGa>~^1e>XA*Q)UXa@{zE)OuT zb25(g3-6me))Vtv>v6O`d96R;ec|`x_3CEkwz`?T+yD}*EB~wu#F6V_t+O5hBF~?P z-@g-D$M}E#&T>HCuZ)g?4wyf)m&A4>PO%llqi6-YklO;^k$6c5yb|;J^Rdz%$CEaY zsl5$Z8kIXx-XVO46ua)0umpA}e3ueO+QIwLk@0c9#_DXY2M?A!w7aoI@1<|ipMi8Y z@*DpE1GES1jGn>{shhC@@?flkJQ!=t-^?{;2gv@;GcDg^9{elvMBm-!!UD*B%EboR zDHaM`nu-vEPMj|`7o70<|KBAohrF(&l`^c7w;N<2fw9{ zDc{X;0o40{vI*it@b7Y({QJi=#p+}wu};<+s{>@9jLi+Pd4Rv~cM+ds7Q*`^lBiMQkR-=8)U)J)g5$5I#r6N9U#v zFc0y20|I4shW6#hkIc1ZY*(7Y`VH#F>&NF9Ue|}#Hx`%9?<76%S z9CG1(&iMPsajO;Y-x>bC@b`p|)eo{*swa&SD~Vz702wVDBjZRlG7;CrQ_y*aA>+Yx zb`DL4a+cKyq#l}v>LH9SG2jLufi(c8+C?^#HL_&$8l7WaHo44&d|@*El*;s)G2ggm z%%P1TNAXR4KA`ur%}-5*#`GkI!6TpW@A&Wd_gAS142&7<0%PnfGH`Wayl`9yL8Si> z1j{#Xv&5Kl^myJ0_Ly92|#Y{LB=tVk{)-$J>=SzA2(J3~G zRfiaQKar6_IngIn6oTX^8HX#AQ9>o)eO4tC@CmTTR3&4{39O^jXaYWsEUOlph)+T$ z5nhM0jItWoK)hc6)lEm$pv|k}Nr?Zp8quKbDLM=8X&mq4at?99fYb5knGF9ug`J1z zsml<%XmRWgvxe3jvvIWTXF?Pi0Wvl7HU>V2F%KflD<;=dn^PyfK^5zYC9=lWe6yLU zSdHQM5<~bRni-9jauH3#waH|14kKXCA$$%`Ca2-N4(9zl4b{ezpp7+BlT3w}OdslF zvbvC0q@g*}tY=MSHQ{+r!gHQMQ$TcFO&BLs6DHxaXez3UOjHY-P|kolixwYO_1Sb* zpUpO}Qps#&&NBHk@$!<1t$Nl>^MYlu^YELPOo9Bt6G-eHv9>*lRcLn&okEj9#^Ym< z>5liuamXvk$1-oz;~=~b{%hkYs1CGo9YoPd$p2kU&;eVWjQM|8h4?4O1P%IcGV~R1 zZ(drgzBTKA^^3LleA8K7i$VrHVz{hD#GLf7RCV;(ctaw7uNiFc(;p6=nWR5*1j)&h&g3qR6{&{W8ps#0{ zSEvZ>!pnKaO>L3ZOrNh`(`DmR7{3juXk0R;({t~~Fdx5l&8aY!b&HSZ^VFhe%qft) zdICGacqT!J8U%Tte!?TkA&4?`7>~l`giP@Gcqi?VhKi-6K@cr>u((_5FPD%9N&AFs zyq^pa_6YBUy(A0r_VNAO2MpkAbi@kNpG?114)%P9*eI(~^n46NtqGVFActWUs_G~8 zh*8-bZ61TX^2exZRDp;#$7$?0Kbc`#$?`+C^i06=IR-Kmmm&j&V$vJ2nAA@yCJhjG ziv6U*;5YHT^p#j#LXCExG*VmrT~WG@*Car|8;FRnpt#NQCuu|WLI8I!{?ZEhov;uLB`&ZDs+rq9lwzDdVzC_#bD^v`yhaXrB ze<+IIFn+!bGJ$j9yQuGQ55jS?ys@}b(H-nY{3ppvdcuzOIbdmhSrHIk?y%mNyJ?!S zoA!rY>YHeMsNLXu`Z0ZH>@ia5kCaO~_~Z^Se}JrseAmTyfU(_PZ(IoXj^2~+O!=;_ zAnAh(V;--I?*N~YEw~rnf}i1yU`5RjJHPx8+v_9T7j|ZUL&be2LJsreg`<*KwK*NSsE*ehOiih#xW_5rwz5Z?Iwn~rCI#5y}$_N z!f`N;f5mX6FL4Zi0^1Q_PTPT9QkXoC;%YnoANIyEsS@a&^)Va3cET=wC$wj2Gt($T zV{*Xo>4){8c39_Swo#UjFplVcy&TPi=vW`F>xX!s77(5ao8QX@u^ZSXx=6XDZX*9I zulQ8ppXC#K3I!n(Lq0g>=bMF|!d75AY$bg_USeLKo$wqVo`vIb{Q3EFb9}B8%ZTEQ zrL7F`R2&TWBbOLXx6d4GeZV2Por||kDY$+gwDkvktvL8_mPEYG-y)7{^7b!@I3^jx zH~+&GIX3y6aJyO&XuItYgYP?Thl{o)Z_nPO5S$l+xXB#rD-7?}77`olAK{nmd&Y&M z4;+6^qa^{0-fgBEp8(gQJjB{Mtc`+5V=<|uh14UOPd{MrHXj1NjUgBOKMK9azB#%O zLzOtX#4-0jg!MvKArHiG+eCVR@Hle(n7=oFV}71Xh^?23OF?`+ zj!&1yF^swbMip>2E=z`y(y;dLf=KAQ#IK~{(g*yszqlKojjy2G{_r1mI~MR;)jVmcW>sr~ z$?JaxJVj4a(>Molxoemc%o^}r6&7TN0pI2JiSjs;>=lLr!;nAIP@$AK6ynjP3A@EX z;$9&Oe?rp9k3u@xkIKQX`4yE}1ftgmt)rGoPZ{HZ9XuL(ay;d|c!GvueV#z8nkvXx zlS?Iw_f)|8$r=L^>$RW$X9Qg|!y z9^M0}_Fnvsd@Q_+_HtQ>-+l;j;T98KazvZ|2iKT*508bO21FT9ZvNku7{4F4%R;Do)Z@(Do&aFWFnMQ$j_cd4!V%CJ1MPV2#jWXB= z3&SU9DB24WvoK_`yAU zKHd()!9tc1m6!!BJ30pV?<=ruuujF!55Y0FuI&NK+K1oo!2^LeHjtDg>8KRU96v%n zqMhV5DFN|Z`7FXeD-Q7oxwY=Yb)`rKI$#;rb+!-~0NnD=SpyDs%W_*hn`jHR#Ax)GES&)P#L=vuoG!kUe6x;DF zxaJmfpj*t%xYhpwO^8Z6@JM)Qq!^uyBq&$Wb`bxBf7kJU-!HS;T3+iuaQI?6VO+Ex zx1lb#!B4j-o`+*1lgpr6#`MeinDZpA<5>NgVF%`V`=CO~$J zMDros`;ne(JTMaIuXF`%AC-8sb(D`}N7vB&_iP8weh^7x5MDNf*p3&85Y>Bs#)Q<5 z#_Wv6?yOIy2^Zn}dcsU{84IKV{H_6+@xJ`;8m=FIie-Ea+0jkL@3RQvExRFGPCT$9 zxNvqvj3L3#@0lyj4iGPz%R`!Ewzm?k-^@g-y%o>eFwPy}ypa#G5H}?Y-*ZY_5HWW? zX+|7T1o{^6!niaei|`%f!XNl1ts z6m6}^(JGFp#p=(?oF;#cKWJ_cj*#=;^LobvqP7?}2kvkaVQy5eK*^<%$j>G5s=&Xq zLEO`MLJUd!fWE~=(fD7YC_dk-EN8!f+_$`}E%Qdp4v;UTlbHfMk=*nzW25nxk(LzXNyJBGig4H`|%Z&Gs~reg=7I`S@=~6X=8J@e0}o za?Q6f-PT=@TNa-axbXKuhB3?cdOWl7RN<^Zqf%3(5W9AYa0Z0OudO2>auLtOmyrxi zXBmBp=0#abw;*cwUAm06jfxk_l|~06-sJbWYr0r#Yl+nYcz{iSY1AmX$D9Rz+S7$P z;uNu_M8$K$G@%~IIf#>c3C~7XnFPXFObe~1jNfBEFrnsK*FbKXb|@v(gw17F*gWe> zlr1$2H9`_<7_A#;O3gtZSW}IWg1B5~GQNxzbOYE?{Ms9)&AMSa&D(UL*%CaZ?iy~& z%a(LeR5(HvLr^$6B|}8IIS|8Ezzs03BPQnr1oMFq-9KN1cH?saZ@XIJRN(?)zTb{pEIwdDa>V%@gltd=mJwq%Rgo#kFVL#rO*zgU++g=o|ka_qj zmI#+dL?TV1C1+YK1lASb&wsf8QmD&Pi{GOKIV&!@pXYOJYuMNG`MNnUv^YzP zuN}7;pO4+>R&>7MB|qoCTL|k47haEfnAi6M(=2vvZ2jTu2%jVGnTh6al*>w!AAc5Q zk*zU0P_Nn6^qTjh>Cs8f$_iz$0RF_XT09SWB8JUVv~HF(FXS5E>Fb$Md6>UR4 z&$nO;A)77#EQX12T!gPF4GD&OOSloNC5`Yb$nHzX2mItJ;_Jjb$bdc=^epLpN!ws*nWJs((l9EXVsU!SjGzGialWf5wLma>-b z*UMWd4lo(MZq6rsUgj2rR(KJ)8MO|$NNc7=pv&S6 z!q(9CF{U189{%@^@^_>$F2t6x`&~lWPTLS95M|4j_QBsbSjunmy5mKFu2jK`W zM=3daE1B1f1^RV;f!-W)T-;UN+AYnYbNT;>Wqs3vxF1{$b`o(kJph^8vxTzaFp*1X z>7Dek{6_v*9xUya2gxOrLGo_-E%?Z%N*tXm1t{ewVm6?c0bCh&GL^x%rW*L%aI}-7 zp!G~_Hl%YP-}D@#v7s2(bE=v0gK5I!7r2-{lY=88C!)`yJk4*c&p}t=y443Ah3i@!tsKDCSq0Wf&J^JK&I;BBta)5- zx`Vg{&jq?1FXw??VvFh}+^Tm2^cXHz1+FhC0%ze~1wD%EP`446C8!PPeD|!FPWcF8 z)pVrE^pUX|B&X5Y_{NCo=qbh@`ZsW%OgUS(eRRI%bMG4Zz+3^k^h&cW;I^@Sf)o9x zgNs5#VTRa16hM$vSDFg!aSFo33*uC%9^|RLD9i$#IyR@p<}$ud@S1#n?GT-3`IuS- zu^AJA8O-P2WhS4OTSeD7KCjzQW5A?8=;jSz9_KhU$EE8>G3x)nj`4XgHW%JC`TxY` zc>aI4qkEp1j>E@a)2Ob)*X@}wKgaa`OE6Ez^pDs&{y`6_E8yr_Uk`HFHpC*x9MC0f zjO!px!E?4HjWgQnUj4q7q;=5NsGZalwM%3}_|NdVNGFwlwo>~|U7@!D-J*rL&}<6+ zYg~6~gt5Tyd0w#I-*>WjTAVD_elPzWbG;~5H(v6!qzT~mSEBmNf~fx1(&9QD*R|q7 z?^^-7UIJ|kZNS&Z*g6+ywT1mg8*3%y`;GQyycJt#dCAwA*qX!j1&-6-WwAA!uj730 zz}IoUj$Frl&EUfI^aZ#H;QX=i7Ta^&#CBLuVq>(au#mJMcA=?Y7jBZpn6EE)FlWMW ziDCP{S&8f+ykif|M9|MyuwP-H!F6@M_QduT{QI@^5nXNaeI0*4JDRIZUbX`}0@pz} z%beR8TC?Tw*($Rm>}mM_bd2s>R)HMlpo0eK*{(%`W?jA_)6Ff*%nFyL$o9w0!Z}`w4Y@Fdo+XX0la{-NXww%?2t9m z;w;mHQG8$&c%6nRw=f+6scep;Cn1_u%*r$#@FC4OPRBzAMQ*7Y8D*m$Hb+66pcpHa zvmW;%e(w*cAx92h2^>+}D*huDiX!&Cp?oP66$aov>>b+$h%QHUdC6Iv2cwAmXh7vd zdNpkvMDfzps(MJTWQ?ST=y32)OS5*Neq_7wLMS5q3utBR9>s(Kgfo2i0K>O5JU?#( zj!u>Z+#-f6qyygb0nZo=?KTX5K-)MT#PJo5Zykkau0}PZrZLetqfgO!$=l#0lr`b` zt448?k*plBc1x3Ygd>whrI*t8@1>CZQYs|>BNvjtl!{6p7?GSMnFjadHnNfLnZhcq zX-v`Uz&+~d)3y5AbjWElORcXFIGzS9%*jS=K>SYA@sO=<3^0fTz$hLGEKVOf0GWn3 zqjE1928^2j5UCg{#reb^Y=|5!;uz|G2u=*eDn(MGeA3txaMX>rNDP1ENEJuQDq3UA zDrN+bzzD5kazt>FQOlS}s{zVZ+2Z)>*eHrskp-hD6~`?(Uz+o;c}pKaA9$*Kbn>U? z^@!oQd!o4Q5V8kakRxox#8+Z5kz>4XgdK4F1Ne3A2DB}P!f}L+qllc(IsvfIu~s#6 z9AM3pj8g_jCMVDufKr}_;?9$eQ&gj;sA`^~6pqy>q88)1a-=YZ4<80D>xcU8k8-iO zb&_MtLjWHgEbJp`QQUN|kV&`=w~R1E_yqEB>;wF?lrR*sd4Cl0Wqxr z&wCIJ!@PfZ-|%+%7?4pe90M-}7%s2RLF`B6L5@JKpCf1lt_V_=@KG`x^U)IHo5y(Q z$1M%+H^Dk#>1J(e8MUbab>a1iVazq4PW<>N;K|3V7-GxORNnRnQH(Fl|92nZ?Zew4 zRoDqU-<{&?_rf{B{PRJ;0S1g2+LM>DV_qMQ;Epp-Qa*Ncc)O-*XEj6TNOLWN_vLsP zXMC)21e&9fd<>3Y9Hr%b#P3}OP+D$H=JzN~hC!ckdjapeY~Vcew#!EQ;kW{D@40aP z{qZQ)uR~v0dOaP3%!CNUbJWY~Oo;ZuAObFb?s3ouqv0BUJs+1bI|CmJKf-hJvb2x^ z_&q=GFO`sD=ljWUvY%v-a)6Vq!Ya$lg1EMI+4vi*Tc4m+R&6~~Ujp}?W{uR}YAwy# zYGuufk|5{X9bo&E1U^j-YZUnzRi&fBt7|3T3J;m_zSfqGWW&t^ zz&id6_aghu4CV(vm0I#Q(lngTnvfKEz1&`GLV62d05y-}HsLX}Y1;v+UTuVq6o#<2c_V8J*5mKgqBS;FVKq0y243G_NY zj=g>S^RIDv%-=eWqjNlc9L!3iquBNln7K|Ed>sqJTt4yroXxog5y~-ojxgK5+? zrbgwg@&D%>Zodh#3U~y(r+khWfw@g)kN7TXi{bBWc?>=!W&z_Ecu$vBpNETtUWC32 zKZ|Ts|5Dd$os2|l4Cw}0k!RcPJ5IPC#?5eTbbjiX?YQI=osAs}Y_}8#h~0im@rVhy zHf^h~R;9?#frr^gvwCLj%~E`q{WF5+A~lVD#xXq3=5aQ4l(j8Wm^@Yf*_I;h0ZZj{ z@+V17-D)^;jM^?yyfxkJrCRLE(4)-2Tm0@~rWE)y+(J*mF0sDj+oWE({3&z1g_Kd| zg`h7zJ9T?{THq&rF}b3Qb?uEG5J#M|rA&y$*EEot-s9bd)P9+V{JSHY&C>7urk01uzy%<4_kME0p}Dgm@{we z^Fg+o*AQbS4fY#D0WEB6Jzz^2N5k5iNyeAzSgk8{LR@nJxT*_<+r)$0vkwUL8mm28 zj$4x@q&11hoJG^gN(3%9=WQ<~ZSg9Y^%C(4!o4NBN_j=@b?}mLFNW_~AK=V9BU?x> z@)6-44E=$fc+`mKM~$FSk&ZAAntpnOj;0mN5#|B#vq^<~a;cD4ZV;6HaZ%D2_#=Hm zv*hv>G7e%h|hpr58;|1*D$#y^@AldW*g`F%qKV-{PTKQ+st0p7OOksUJEgQ zL+-uM1+N3)K8stdXYhS5D?jTFUJEhrV1E8l)W@)owH5M;=Yv=uo54GR%U19a>kn<9zsEe((H@|KAJqJBaQQ^WOQ#+6H;~Fy&0U zv!-EHM>?y;CZZEKf=?o9)r?x;RdfdA6t}hBLMJ4ljlfa?8?E zc8BrO&ANkD7y*XoM5`K%wIC@@cwYkK)OZ?m4$$CNE|8t1m#Zx=T~yxs*cV?pLimmV=?|~Z8jZz&zTuR z_a>o}c&u;&xR{))$+@9t8NY6tbrCqD7focd3IpLTlGEzR*+j$7)XPvHGq0hqhkptS3>9B|k8@-Eq0eF%$5V7TYK{ zly1s7<*F^&e%H2CxuYyqew9~>`=WOHn09suY}$M$P{mS>=YS>L)8h>;ca235`HOpM zFJK%?<6TfkcL zGwi^-qXgJzFGU;|>IAmNN5)zshOcw{hudj$82nja?F0-c2kmGLMi;@R(g<+63-4{6 z^@V9-ebNncb{+Ry+5&6r2C<8@F3N&>LP`VAG#zz|j^Op=cOi$^h2#O>9`56l8+>{P zf=}Tfvjb#sY(*W`Z5l^g(Z!S_DmP7+*~Z{#iOY;3EF3N2$O|uL2(z%j1^_NR*SbL$ z8O`W?%2~&afdPBTLWncP8(5s-O%NhqK#bKfq>+dq`$$J}Rj35*AW6w$ZNUJZErtB- zwNX0R4A#Sg*b4}xfPZ9TtXzN__yB2D@lhcFd4R{^;^Iz-eO*)>49w%bVj4O@8$it0 zp2i~jiUxogs9G~2`r2N-n!3q1GP7S+w@^ElD%fpR<2racJDy6v|Op{reg+mPPuBpT;@k8#*7_b?Je)o%IQY-vwS@__t`Lf#vdE$oohBO1TOrEpatLy}-&j&p1X41=?ht_Y1V3GR1o@;XAJindQ!j*QES- zwKhNVWNOp5CDViIUVFn_4f8ZksOWH_d0IDhpf+Be@2{9qENx`g$w+f~n`?gD1DC?e zo4;GlAj|W1D;&&DyZ+AO_oDNj>&f4wjQ2E<#%ULWCBqTG+KvVCXZf-|_TSU*$~WS= zx~JNcSP}KPF#%5zuj5JjwXEuy1N}$LTF$yjxpV#I^~yD^3Xw7D6XSRNqOl_q3}lC< z(4f2|Zbw2SuD;UMDrI&reGXw9C>ZZ?7CyQcIVN@wqbD+B^!G+oi;6@sjJLkQek@m;9scg;E4HQELVQA;?MVESF%YmFUU#mY2^$^t8HtQCzGii-c3@`<8fA>}_%kJ^^NMqhG8~mOi$DaHqTpA#+}Q4~ zl-0~Xir$dD@eRChouA=v!lV2p!!xuQ5R)F$Nue&`Osk|c$2QkqNhk(>iq+Z2-1niBU9KCUJ}wB!d=v})kNbg5yWKPAJq8zNpM<9F6_Hi@{! zOK7m(HMGTlEDRCr9L2or-2EL8XD!k>;EeRtdugBQ4(eoS2FG2?5v*C`9g7m&>(UiGQP)hDWxzJbV|A@Y}WT=aoB8RB0?YO%e}6z_P?C${>K`*WnR9x}lmP+8=$Hp^gELD<3Gg(xM1fv5k4 zxIun}ZTbq|PXBiStkxp+-P61W-Bfu>hSHX5Lih|sM|~ElV+=IMk=tS}@g#UJZ(!TW zLgl5r0YB5Ghi67==oxId{e1j^q#JQPl}#iLRinlA1)8BwqTjLaacS|kR2p(s%mTm9 ze<6oo7S5^v6O8wN6Z}W(jB`7&XS(wX+YYGF7Ne@#C;TYXF@HUn2;H7ZT3s>$Jzp-uWPsHs5ZAC%oPLayV6{WUf2egrC z%0s7HK1wHt3k8aX$I^?!G{}%S&+$;&C%=UFF}KWRAuaP(#w2Z}RYaI6HWf6cK>S!g zx-L4{!N9r9h8b%!%lHj*sVqv*P+n3)s1B@}Ldaz_@Ey)tsP!T)TVeNe4`jr$T^5hH ze@_|;8C8m!L$xdPy0FiEHsx8O7xJY%ckWZtA-3Z|*3#cKdu?dA`6Fh^6Zs;Zs#OT* zWM3*iHZ*iIBlGpgZwvYptSXQ*aR%%k`&#RbYfK@V^iAnw-!;q*s+Z6zM>~6l^albD zK_TcIYI|on!(07_LIvnFTi=9@@mp;j#ZA%}ai@K>YpvYMXbky?hFB4?th;vHbbB4C zyVZp0_ASnuVqLJ zYmd!#a*E_r*QNjQrcvs(;A(Qkd0!quN2>KA9n@qtSeVOfq2{T#-YxUZpj+*|oIlDN zSz|3?m9h1=|0@VuUSE&UW3VhKptt@o&|>K=*8k zu6j~;VA#Kv?Y49j7s^O$_HKP}Cb|{(%9VlYYR@8TjAFQ$t)9}$s*o+cTaop5f_{Flunrs5UWX|ms+;SLr=U%DD4b$4V~Uw~i80M^Cc-hI*j)F|W288~i?6)L&| z@oU^OAlg)-`>L}JhBqj1+n=g!P?jdFjjtx=(dO&F8Y}SMj&1he@gx8DX}SH^3=3sC zKXcC#A6u%l(D66x6BIHIrfu`hWs~Fj$6r?-X^nzep`m68@wzfs{5|r?yWv?Sf;Ei- zVqTZso@(_*RK6o6=u3S{_O8s9krUFd@%Q6e5JzNvC z=gP2c6(-3S9K9vGF$S_~`hus}kFJN_oQ~Ui;owIQfm>x2l{eDquqUmnuU4Rqn##_| zVP!NKA-qtIpa#K%?*^pXedlzaqkl>jS3doP-xYeH7ex7;N9~u59T2-KXJ{CegltJ_ss`B_{+ z7K(dqLGh0HL;Bm)p6Oplx=GKx4da^%CjvkCW(KOVQ*xs1W0U$5va*7$sU0_!oN_nm zjbn*-js2}@_m9hH6Ko-_jo+TrbAjZ{kOnYAgSt6#@Yy>AjPIBKzfgDV5?j3v@ncAvC`4i3+cEQC8HNVmYQGSKnP zUB~v8{t-BuRw~0%+dH0lH+sIrpQ>L3ehYqWd?X(gD`@k5Z~dFXel5$|g6-lDN_p2v z$2xK|cr9~v_8#?u6!6YU{8M%tv%-HzbaR|ASUx~+_~U|?wCC1#d|8sEgVK2Sk8wqm zZ}iOUAN`#p=j04;Fr~fo6uqH4v~%VSagFUe^i}9eprm@%YD&gRgXCq>0Q+CAIkr-$ zXs~nE&OlRE!+ks9mbhiGMo!!OfI`_saTHAq}0%>zHTP4;KDU#z8py#4`! z*XF10NMhLARpR>nNmfUgX&Y_Njn;?nMOvEm&+bq?JkN0b?CS>SR2l|ZEX}6x~dlq9ty88bwPG_itCCq zB9HwYeI?b42oOu5xmM0UHc(#cWbKk4CnS0OQj+EiObYd*DN-TN{p4?LB@HXIUH`>e zMj(%gLRigEX5j0{EvtuG;ve5+M-4D&Yf{D23A{s>DR&5q z2!W}Q*~Uk#7pbk}@`RiXl-#y9kn#JANG5w^zwO@STH>7Px+;#;dRxDeB4U6@qDN?I zu5?5XwT|IlksK70Q>;CB4^1?Q(TUDC z+p$7ovABbtE4G|QNuS>z(rr?5+{?soT+PUBhzr#Pt%vMP70{AM%W&yX1Ny0aQrYet z=NM$K=V~qgsdo>NNEd7<9UynWJ=s*p1@ec!4S%aMa)o&#jeY$fns2K1qy94*CFgWh z5Ou9#Nb&WEY{8#62e_t?7vRsd4RXTIC4J<1c&K(H`=0M_{WY3w>+ftXbqC-5mCnjJ zcDjzzE}^1kExR`Viv^X6`46E0l*M zDcn0qXjj|Q#J&l)g-Tkh$WU5FY3p3C6wts)ElX7$cnY}*zSKl+jvopK9R=mF#=@{S z)HmFb-WK-CJCHMUH7$2=wYA@IHetH6A9)^mp1m{JN^c83G1G1B5|+k~lcpP1*4WHk z>L<1(Nmp~+w$-5nBMb3xDbsck;&tB(t}>>{PaHlu0lx+7MHRfs{?KvNnxh3G0{hy5 zJg*&(*}~A1@DN=VpD2UG`s|A8^B>7R?#~YI&{TF^F6Qj+sDP&hhh)9UI2CMb4zrhY zj*@55!r`Wo?s_3}3wv$+6WSLpX;u+Z6eMjy%Z$dTsBKF;u{X3j2Np)UAh)Njx46<% z9~DXuFEDG_X1Vs+WctWIIry*fisb<7M}Cu zwS=sb8JpBAWS=}!s)vS{5x`Ae%3gUg6%1-+iiK>%`OL|bkcA<;#U{EF(B~fDJ5@km z5jYoA^nYj(oX7bnIT&|e7(ma5mIO1j#bOsv#9dz+1{p|aYvouI@jKzZUNrMz)}Y8f zp^cP^LXejxm%4#8bMK1Fhr0x3ziXUTU%Mubb|pF^`l`%3*>}Rdg?(`~+?k!`-c zV0~FaXDTPXi*0`Mm^M;v6Pb%vIt|Ymc?jTsBatk;kju*{)-!)EUk){&HDCD3COT_6 zT1rpRA7TqK(8#9>xQ)H1ql2@otG47dwud+AcfkU?*~|hTu>G)JoMwtp(t*^5<`e~mllh8zn19$GVFG|B}zK!1^{MOd1VIxy=Fy#1=}rcxiQdk4uS$q`q? z(**SkHuQ^XqWG(`k~@N@`SN5f2~}dBJ3mS+LSWQde8= z$V4$p zpDML-P$`oZh$Mx-r}If8G$K+hE%D{pH_Zd>tut(~u*f+`9;R3EWxgHx=63cSQo(yY z@t&)S^(a_8a9j__kDQ~FdR78N*((DK>;Xbfu{OCyTF8Z5)sk8yuXU9mm5i~G1ha&# zQv5S#8T6%EA!yQ9uqUi2WvI?TN3{;1|9#O;@FRpEx^XC>meN6Qo_RifOn8r6EH2e~ zTU+iw9bBom6-qhtiig$0!ReuGMiKlGNj35r2+%ytYSA3(U!jUxW%7wiHT@96)LXJkZ`$h*|SXJfuiYW}$J;6$$n&Ch6 z9q4mKa@-~9EFzVc+5=Od7eo&`L>Dl~>nwU8TJEo*m0?4h4%xi=TQ4Hn;c`a6_*biB z&5$QK-pYrCGvpBMp+7c(A87vv|3>rbEwvZyn(~`1uh=Kj#Fr;LNl3dr4QsQ?YfA-sV$J4tQmaR@)Z+R*`px)PB(L(x0+CV<$=!0*8 zefk0Tm!=2>y>pyDv)V>$mSXk9b!`Rh4DSf}vv&IGg?sC>q>%kP+c2dv@OvF{SN%fp zy4Kpd4%s+6B#v=myeF8RRWA4u$U} z9d`UI%{Lp-ec(UXOuD43CL7dk*`3qB%NP)NVC7df*w%>_swp-U%7@dlZu=$$3gF+} zrxPaFs5UNZRmPA0=K4Rv3dKQEz_0T<1veEA=2wdJhkEETnbA-Gg!AU8Wy0NuP9=XHQi% z+$eM_JOF)q+XDk-A!{{*`r>rPJ4_zwLIyKh9E+M`@)#-q@sc@Ol$-**y3*>qkek`Svt- z7X>^X)!o54fs3?PeRASJJFE1^h7;wlej~5N4A72VeIPbE~(@) zlOV_7F?9?M0B7oiywx=(F_(K1xfXbn@kyYzB|7uRPq6jSZu&0;H=4_&-|d#vR=XJ} z>|YC!pg*yKx~X*p1fZ~6a#`|7ZFA<{^ns!ND7X7oTm>anADxvBdSYcVL9QT;X5Hv; zqY<#ce+$G>6;2O6(5K4bxJmK7T^(>(>k?QH#Pk;038TM*UL|rR(#*)x zQXvn(c*k;A2_eOr9^RjIHwyk6uY0=G-9k zW$O*Z%GjF5t(PwuF9Q?&57eBHTkNuX2KqL8a%TD9S-RYjBfg;XG>X?7K$h=jkefRh z_f=Xae4cX0^9r5yy-6$LYs7}TUL;jj#_KCWxq=?`3-pWfr~C+fr+NW9KgnE86^NX9 z1l3eVxn4@&(W-$VL4lr;w|Q&D4hgK);8AjN!)FrOQe_o_wYRPBRp5$EHq?Yjh&D$e660#Xla!K zo_KX-jyKy=Q205pC1XsMV05=N@Q!mWX8v&h(9Q5{)>Y{uE2yi!fR=+O_OBpzS2o>G zK2>tshT)f?<=I_Av*|uZC~1)Ame84YROf4%){ijjZ4lmBU($klNm>;cPh-IY!>8zW z;x{JTwFT78{*&2tv>c9hISM4TkxgJy?JyI>`H%Q(Z&$y)!foH1qhxHlx zN4n`e;yNa*(tG#^2ON4|p)}--&&l3|k|PcDG*nI+BI>B5UNQ1i^J-NMf}e`Hoon6w z$g|LAP-ro{;%MbOY;k2OBI7dzk3HX)vhhBvJMla}3!2dqYYy>%pKa*xE zOQa?Uyvd9NbY0%*oaa)M$L1#Oh5i@Ol+%uYc!So|U`;m9u(iS-skyw=T{JO|<0&1G zwLjzQP&qcvqvtr~Ty4$>77EYP&j~y2Yi(s|6WuaK>j~OzdcZh9*O+yLjyA)#!fq*V zP-*|$EFoNi^-zAdRVCA5{KZFxdGUm z{$?j@F1e|caxQoDCF6mCaTqNS`iNVEM7l1#I)dmeVC^?!1g{ki+WNW%k@#T8%uVSX zgDZvYao@!sQtD_E0u@7lX%A6OrJAi2&y7jvseebl*M?Zn$r-7-)Y37^^QY|B#lSk> zu+RiE)}5Cs@;OQ_G2_^QJNeA|SM;yVT{8nR+oZ+nk2y+SY*mu_cWUe&c@^ z_>@+0?1+Ens6xKh>xE}T3Y*o$)sAWMYt|6>qq8DIwBu|HVXU}x!`>l2A(`5@=>xJ? zX5J5%6gS5Ahg}NRyha*L#XVie+>NC<5S``(z#cw zTu{i0FX@rR;t}E>pIuzY+-HulFb7*()t$$MR zORKkQL5_IuTB)hgF7i?MG&^8>XkUaLQNJ0~Cxl#C)0v8G!qS zL+Dq>w{g4VFSV|bvRVOawlv+IE;Xiu)kcwCdMSt>bqW8b)ORG>eidtoZ(@OW;IZGuD{{Iij((%~bs=?4=wi4eJm+;T&5_x+t6FZ)_)Aer2F_Ca^WD zeZXT7a?iO~vC--JR_hrXDh+a-vOh(`^u}6Sy^r>IY;7^xC>#dH%ULPjo)6Us%<^ejufw^> zBG(JIOYo~dL?(wfYe8X*6Y@UG4qTiH;hyR_>tA!1HBy)jnTswbjBqTafBW*Mp9$_I zKYI2gm31UoD7+*XZ)7P8-A|nD#CpiiR%na$uSJ$8^_Mw#RsN;7RCh zBp*Hmd6!?FMj&qYDK(doWZf5%U2D8Y<*DjDe}3PQ z&|UH4q>pl(wQsb>gRMAVbXF|eLb0;2nbn5&++==fy#nvD2}}{5+CTG5mBC(=KEh{( z8WAJjm#|S@U^Iy=QJY#D?Tg}yE3+VaXhchn^oOk1HuGcjJMJoUvIU(-ZDH0U&?)mz ze`>gx{aHQb;rkISAAWk zk`W(Naj~RR-p4XUmDO$G<7O%QA;)U4xpg!905ke0v|9LD%um)ys(qk66t^zvoV}p_ zG9dXqkYQVQP zmC~9JEF~2Ub#!zWTC$`IdUnm+E|NTnSW?~(K&IaZ56D+&Et33 zJ`XR?D4)I5KL`gvb80K?Fb3!kgO4Kfg?X+6%1|_ib~XCbF2Gw0S#JTYnr?sKm`46q zUuT_%e1q*>^Kz9)scn0t?+aCnWTTQ`SzLisCLhy}Oai zY3=cUmwgkmRQC|;x_3G^9Pri*larRi6e6vtzb?u7BirxDIowNK=4MHVtChrwJpK+0K6P zBjXNAlLN_VTm1vHP0FeG6wfVjvC$23WPU{cv0s;4qC&K<(NnvkP6Y0^FY=jMnKs6U zY>9ELl}~jkI3ROWz=cn`yLcQnhr;I*}|n^F$*<4ARwe+Pt5s39w_@R`l& zjZb!Zbo84KrJv6@saCa(OC0GvC$6Q9LNLHlKSyin0ITG}3z&ZCg;>_wU_8=`>T|V-I$pnLJTjXJ z8{_)ehg-aD)@J5ahdZh#dSDm45?2XM^LIo4O6lM^bOn_%uS7Oe4Q;jZK}4DnCc;mY z&h8fR13gL7mGINxs&HOa1dG^?xH4ol_%|;HKZs9kf7pAH-NsY8N&7|*T5YfZagL_T zJv~jFwGj>+&u9r~*x&Y@Ilf4GC;dQoMM{R3u|W>iHi5L!OGc`PN9mWrA5o#Js14O6 zcIklqP~2r#UU9H~Fr3?$FSrelaP^5RZXYeK*Dv@EYtKm)0Ic3nk&X(lQ&Y@);8XU< z%p>M^ib5;xp}$MkME@exG`@3=M(#iH81qH=r_cb=gyMWpEa^#ogT(S=`+gcU|0dVR6^7dgD3C`()<3*YEw~TrHh4 zlr}v%InVRCZ*>L`6q{i0u->prUd1F>a*)U66~Xr*R_=kFx7Kpqria5Swrk8Td$Hck zX>5)BN;n|;g$BYqVWF(Rj%`Wep<}2p2?*kg5Fz^@@z2W(v8en5VS+AH2S_ zNHAgnbsM+YmSlX0w&JN+DgGcva&y?b*dXd9CsJ|fnV2*BF;F->Kt1Kwd{$2qR}C$V zw3ND2sq|dxHs+T;hNV!dJOEpPx7TLMUBIK(gu<)^UAa6pEt{oA{#xlCe^Y+CrJJWe zSBFT#Ix0o@yyS5EHES*OLewG)!UvUV^mgVOJC&M9e&RI8d%B)lCe|x7C;S=nc#meg z?fgt1QFer8#=g-totJELv5AU}-|0aJRk0>9Kn1Pi?5-rmLd3LxwSAp+9uXS zkFz(3Z{z-HsZD&8_XwSg>s(>mAodAd}3sQ+L1cPo@R;?o2cqgS$j-r znw1X)=fvJ&+q`<*7Ux&XXRQ?fhj0ZiV}EV^muiI8M=t+NPyZ{nPy;6|HI;0Rw>Prm z52*lHv#uJm`PLcx(-I=X$y@Fcz5$jEM5>$=$t^34)nl-qjhSkPNYU^iWhq&Qt7e%% zhuPh>HkKeUSsO37!}DVwQA_7BcLQrD%TeP|OqXurg<%(OFgT7&L|&v7$#6${iK5XT zSo9l#OL@%bN%e9(ah4!kOI^b2{FS5Kh&H~0p2oK6#75;WD3iW1l88=F9`M$cW2ynWuZ69obDb?asfnvY>oS%{W@8Ion;b(dyRd!ocYeNl z%yPronk$5V5{UHSsXe1Dw06`44kO>_J}RC$h|PkG?c3;I!QUkp+lp&%E9gMfMa2PK zad(KNY#!@g=tip&teyTiuw0qV*enN`-@#$j0pG~hatw4W=f0tTLq&so{exq>m_Du> z_S19`bv%DPI$3{joo)A8uIU@Y&XgUYj#7PtXIt5tQS0#f>^Cb#|3wrvVnT^%FSMDf zZQJe&yIV2U;k>&O@~wHeW|n-YVytWi`t~7ZFTa;)OFbj*0kh_yz7{{r##!^xn^Yn6 zE@MY}muN*I-g$%#;qBxjvDZ>OwZfKaZ_3R#s)h>&FUGFRP4TPN{?4$)N>#M#)=_MI zsvmw>9xV+eJY0S27S}%K5b_van|zIDC(m0>(5Kbq;eGz88M}gMYFFC9&Zmdsdx$0Y zJnFi&xZ@ZzMSUM>1O2auB$gg%T}9`kcAyFTX6Y@}$hq3?w#~wqhbyK%3H>d^X}_@W z-GcnjX=*mxmh|A+$Zg0YmPSwM^xWXa zE`)k%&6Q&@KGYQE&`Pl0lOdUD1M+WG@&+-C`Iqa*y#wm@G<=-_eU${q++~Z=SIE0q zKK)-~IJJY3X_8zFYxiDS8Y%?&nPR}fI6$=_|Dc*P%jk#jKYd6q2c~-k`T^CDh~S0j ze+UndT2GV5@iV9Z+M*4Ww)6SLvXDxw4cVz*)fdt{X}Z!#BMlt#4HNV>C>iJupY-kc zHnJf};e}y$aEJa`--4O+N7E(o!N_ZTK|9fBFsYgzk`G2sFfDBXBhwE3H+_fx4Q)4i zfCcF@tk`;CTX1ujRMk($T8)NOHUc*B4fV731)MDHv=#a|Bd`A6SWlLxMqpon*3lFQ zbe}PqY)j7|3IYSW6ww*nRh{tF#49XGpQ#tt7i&pscQDMJL_4&YRtxf-mmq&y1$zST z(-Ez&x=i~Cq=+LxHToT&LzDymUJc-uUjUBt1$?T}A6?XSR8O0s)zTH$$!`p(Mz{R?u?-xweRI(rX8Rd27D0 zLdUTV#6X+^3c)PGBnjNaNN^|2BCe1+b%l6~uf_i*5{YEspeJGPPy-+f?}iTBG&L2S zFnSwZ(Q2(NFr4>5CI>RH`cza$o2r)rN`67e#$>W}9LMY8(}5dB!c=R*--22O|kFTXJtBiWh}vRYrMXKnhuPp$=W7t2HTce4!O!DuzI`(y^L$| z+CU!J0!Nnwob18+Ijxw~NhqppQ{7sEQb;BB&gu@igt`~CG&W=sp#RGw4WJ6qed)bq z76GsXJ`8AoCYjL8IGGIV7&6oF6~VLE6Pzlaz^ri=`5|o+(e`O(Vn;AefVJ}lH3LW! zX6h0r8(;@8$T(qFP*b!3XsE?umm@ zc}R#>qb8GCG#~>Q?ZD3s z1cJ~7ataAK4){GLSx5t3{0wjcn6C}TelckMKL+|kWts9&S*+eu&7=qgZ2uYXf6(Fo zq!3jpfjUD?fFV#PX@ROr$z%<35)6c!gd)?ibW@*?n!syora9C{iUVx8R_!)2+4WW! zjB&%5g_(4t+VC1`kXk0;$oxOn0?I%^;5qLxQuO_>n_XHPrj^!$YB@Ecn8)CM!(=t@ z*Ujf-F`EnGUov^S+lVjtHel}V%w+GHUabM(+!}xt0}ol2-^=6?n_~dB8;*0+YLt)c zN#&(_Qd?ja$w_ymH&Z>JccKT_kb06k@V>ywHtj?v^VoD``oUlnP|E^`_b+&DN5G$1 z4qn$GZ3sF5OkQ(j`QuExk;&%m3*Ywd&>PYlGdaCxZY|61la*mB08Tp7d$+^rmC4vP zGi&j;WCEC%-{n8DxUIU!HGj(kb3BfDgd z9OPSKb7pSN^z8M(wq-i}dc&*_wH@?RGe>dYbpNFU$Rp%(^6y~54avvkpz;@- z!%Xh)KE1!O3ugX&kZsIOz9To2AK^BO%WSfl^O9e1lh?czj8XZ>k7RBzNad#9QAzY` zdPAmFsw<}uWhfry-cNfP~@csBtVmCPehDrMPmdOk@S-~cW!z6>2H_SZ0nanl` z8whBxG?Fa;nMolv9epM})g+6S!A8RG%5s~T?1qXk$^m&b ziwGOZq}iJJeUlbzG60SP3*i4)1tycgOp<4%$V(F@FW6*Cn3UBCz>1ih$s@i1S?(-G z!bC{si$-NEhE>4FVo)1^+fl#=4&x^0-(&=z!bIZ~HX5&p^S}$140D__%-eBrU0|No zG<05{0O<@7&Nb$rC&JIh;%6`cy78j;Nqj7%r(^gjJO;O;hzgMRuL!@}{Cy@)49%3_s9${d)}8TF3Lg7Y@Z;c-A#EPVuPZYt`A>x(c z_x^YP9Htu8|G#lbp9Y;YI`}(Gmxl~HAmiaRjVDf#F(97B$jU%HGbw0i2?aOlXVoE7 zc@{VGlv(Z%lNNT#FraEb&A0+i4AYNs9nDn-YlR`TmS6u0ez>nd8TbN;(_Wctt6Z6G zuB?=5Zdi-$L|-6f`ccms|Ns50b(raOGfDbc-_kckb~4KWw+Zh;ZY17k+9|hzr7|bv z@HS_5rJYu-#-W$5`AvuFPQ`A7|j) zcoG|nRWT&JioqKdVNHDo%xXvA{Ap&HOM(fmWF}pwKg_LPiS4AhI_pj5hkSA_rW>=B z`T}c4ld0dAC| zppAm6v6+n?4rGYmk$HO*Qq^WY`oFxic^}e)keAMSJtpzT%vtXy%xtxpz5Y&^6d#l3 zvy=QxWs!pVP}}G}l=++@MApxX5QA`&wSN#BW|Y7(VEFYTnautmR56EwgVD@Z4>OMF z0lg$P1Xc+d;9?Ad1uujSW!^@$liDa$0fo^KJ&caSZ3Q$6nU5WX%ym}odZd0-4;Ur! z-;J{1XEcXNPCooUg0o3?8*PTI1+Up^Bg=OdZ?reOnZ#O? zTKfVBZ0(JBQ35!kO&^@;dNTb^rsa1z_}^Awk8#sP z;r^fhZE>cPtp#q9VYB>8PxU{H=D10_HHVdGfw_zpm`R>mNZcW8_(Q`61S&IEZBl!) z$XYEhlVY_5R=O|YT;2kBSpV7GvY59%;9vQSHkqDapV0w+ZrbEp;LCvmVu2(tkUNn1 zzMAX7^14Z#GT-A7hWUAz{!G*SQ3^|kt5%a>H4O6hrtfqRUIeZ@_7DSrB5l6+{mCMv zNsu;~^rpweZAsK?P-pxef0lDS#~E!QO6n)KY3Kq!7>nD3dDXhJTA`-_2eXXeCl zq9Q(ujKQoYfSJgg^|EHa^O$)}B;)@X$g5)o{^tZO1CG!N;0iV0gL1I{T3H(l$7@up zqzP))domO+ZG6$IX3huW;G8iIuL|dps*vrDft#@_m6Q4iGgAgQw(aCFyZ}CeI04MN zea3OUKJ+Ly)-8HdWY_L#^WjtY1>MJL=r<8*{D7;Uyhb~H1-1`7dZvqQd8SYAIkrZh z48|Zr{~J80KCCXaiChIK!yDi)u13rSp58*^x-v$61T*thJO_CKt{pD{&9510q3=OM z<7=?xES*fe!dnntpdC#F zC$TI&mFvG~1&s(l0s*SFUcVGk%wR!?2dh zwlKGxYiRq6abl&6owAveI~T1K{U`i4?6xnASYrv&eS9?Iq#u!oiNEl@wj7QD&bo2; ztTMixT@B2?PxkM&I$RTagEE=_5}FmP7F{MR72n5Rgz#uR;T2*X-Cg56k8PdF_f&Nz zg&XV|Z68h5$Nx}NF`zeK*ICzd3-Po3>C}=b%Tk;8JBoiv`yzjamqhkR&k4VKS;90= z8V5BUswP>E*FCR5Wn&Q-!w(3+;th}KDM`TccDqfq|n2|f_( zEmbh`GX=!D*gfrb zip&2q)GfA9E~7J4Bl|DjsqteHa(O#?UO1w*-)yhg7eobPyHZ7ch3w=rri%49OAq3j zqz9_}Nc>(n?P%b7WT?op%{LZ zZb*;8x``|Nzy2)xqr}g1=?PIJZdZy!xo)@PqIZw)xp%X>h2xaHr2Qs$g$Y6Z{ugyN z`iv#f7n!YDhKDM1ilqdA^cs#&xC^L40Vk=V4Y<9hh0QP$w|~hHpngI zuG43Xw(0_OR&L z2)aY)T9(U}KcJiaAu6CumPcq+VNXt`lCdYsvB;>jhClP9^$qlpa*>4fkoT_dfjy@+ zju}fl#%o$`I-7V{SJ={!JgenUMr*y`U;P(+@7*+>&&cTZqs;e7Xo=R z+4eH;f#*<&EMf29{?nt{!qhM)Yg%^3fke zbLXOLClW4u+E`oZt%YsMe)5j(r}Kr&a2{h`s2gLgqK||(;z<1<@lo$72Es<5TSj63 z{AeR=q4Q{>En%}qwbsLO$%1x{cuse;zwy2C3YJuToYYcmAb8}6euijoypbyjtwOys z+Jyg*WoolaabI!mah_*OYWt?zVdH*stJ$`A5j03X8EeU}jpPrvjZTk^ zjFywukjL!pofo~goKMwXw2Q>#m-YIt%~AcJW`t z@_Yj!UJdF5-j%Kg-;M$FZ{%C-F8LDgXS^Yj$%^3KdyLK3TLSMlSyhx>(BqL!O;)xD z50z6`V{#3>pS7|D$(`6*tvhy`s7yM!&b}9M(|ygnc2{TWuyH~f9(j;)H+@#1TI{R1 zSURMwCT}uN>8G{>u1fYbHj8aO+lRV>jR*E&4`r=9Q(u7$9dT`*U56wA*l_K_r&5Nt-@F&iN7T@ z5-7BsT2EGFYC1|d_t=KHa=05g7uk|5cbMOpO%z3rqAJr9iN^R2sQ4ZdpGD`ydJ92e z6dDIRwXC0`6ppJe;2C47X>>iz})2C zT0e6*H;r5bEB#hfeWI-K66oqc3q=gQ`IWxWGLyT8;~vERaL0-lWafjc}ae?gaM*~(d0*k3w-xAn4g zAWj-n!JhLRUqt%oAQ1bXDU9)=*uJO)Rjy#t2xJ$8% zg@jeoRpm5LHx1wbz1B~m*6KLqz@LzETw z9uDaW!T6|_mkvn%#4(Xlp*7LH!fEXT(a%!e-7m+Lqy;(ZBzAQtkdvjG&>@&8Pf%*e zxuslDCOw+^IwKH?$+?+Ht_O~WTt&LM?N8@=$7GnV@~{ig!^p63IjE$)4$nXi>wcTz zYUUYd%R>z|ateDgMx=L1YvQlKAJe_o!*SMxp57zY^Xye)q>>x|*CIK(a+T?YYOe6G zv~B67qJ?7DgdFmnNX7KJp=wG!cA&3f(uSO!lhT}DX$D-FCGoxJDfZDwmAAriuYqCZ zl{`y2#kUUO8AHQAVvOWNlc=K3>)EH~`;>oY&L!UKBrDg6DzV+NO}!D@oc70$&fg!V zEfLFe`?8}v*>bOnzt4u1X7YY=hpW0LXivtM3UO&SKUd9=l*9CBTVmX<_|LX2bZw$0 zK1cUypXDa}27ia|jlM{H)j~Z6PFF{LCtpEMhU7Z>zqg&srOB7l_$P zN?>iMsB{Rw!}@@KSBo9RT%>z3b-4%Dt;}Y;i9SW!B@T}1vDtj8P+V#%?^TP#b#+Pb zNFY=bdR@#t=!x{7)N(8tZBu=aVDMmG<^$7+?1ZPoivK53l;RkJ2xxIqB@v0)l{?0A zas$2Bvc|E+){-ehH6ex@-%&$gL0;4^%VOwWteVsvJtx~h&&qe~J8G<5KsUkYIgu*I zjj`<}K0rVD-|8{_9aV!3Lmz5U(^VpL18}>(Zamc(ijnsowc?2wElZ21_S}D!A zXD^U2HZILRj7(8R@O`AV*iY z`cb~C_*$K*pJRd>&@or$&JRoYX7Ja$g@rn2lA}oRmTy}*t?c- z)GDkYUKr{GJuK%rH*-n8&j3K?Jq!D!|j_nY+|Cs0?Q z(wA551AD^twMx=#UKDVJS097t{a>K)T#y$j=d>ewhBi~b3GYX|)#fYcYC?piE$|JT zg5{&C(!XgB!YlkgqbG@l+%#4N8~Yo*3|5@^jhn^{#Xiu*?adiVAE>kkD$w72b@etq zo_(uLSDjSYQQvjOHsA6FUxCibtUOSBQ`SV|` zmB^P;M=`rzl8Ps*YRzI)PzYx5m8JRONT`n<&`UY**}Bss$WwR`aKyB;jHlrBI#)0|SO)LicgwS(JaV=6!HH+E8UTvf95cdumb6Xlpv^zTF|%R5^De=79l zyXk|Nk-!$2uFeuqN`(F`@e*;dvAl$xu=yNe ze7-VI&j~&A)h*j-Cwd_a(i~3eX2i~u8M24{7f_Jaz=)3&xXc&`KVr>oPqCFdh%_A z;^F||ET6bE9s6F>Hm^< zs7Lw_Q5A;hZMYxSdq9ZaM?SH9WzSkBGfBoKv5WqeJwq;)a)%#?4-j0RV&#qVG7;G< z9wnMl9`vW+#a~<3aifd}7)e})T}?mvne}4t_>epWG68mS8?4Gcg7Y>3gQ|yKP%6kX zMtLTM8iDu6I^YFSKO-Miob5t>#$Up9UQLl)hM54M+NS>@a(q*{Ixz%@$_i=|pbhq`E`;%qL5XF`oQ{&O;4vF;<#71C{im z`b4dy_ERma4OEM%LzDx`K()A%F7HLVuq151{ttGY$O%;BzVseuAT^uJpuaH7f!Ebp zC$U**oE)c4LoT#brj#9e3*#p+1uu}TsrzI*vH?@XatrwSUit~`Vx9u2t0SD9o4_3R z44lial;uhr)vLBw;+1F03f1&^j^wLGw{V zeU4raGwWzGu**bE>UXl#|4h84sSIjAGk`6|q*J9y)5PZomTVZ@aV7`W^v0R)HIqj> z20IN@*GjOfUyc|774aZ(h)f6LUl908!^xvW6sv@ZdNqAAItSH(>Tn%w)^4i;N2(@L zGVfeS3?bL~uv@Fcw^nvALejTwH2F&=8}6N&1C z>Fk~im2j|&fZzRMroZ_AYI=2W4FAQL1$3)<=q8%2Uj={oHS`{KReGU(=!>=ueMGtS z?s}5(4o0$(WV}aP(0h~zebVy4ZBG3?+Ni&VI_*0>2khnKz+Pc%pqo98_!sw)ZOPU6 zODq9KcI=Ij9ebO^<;gs_73Xzuk;*x7cjn?jMR;JZYh~tZ{5Nwac0Dq8V_%{5C>d?ix@&pV z?phM+uH}Y)n_TEE+Jt&&d9+XJcJ-6GL;0-aRX?bkv}AbxTP+7L;u4Gw#-E0{W7`TS z4Ub^lCl&%FHEY}BbZX*BPeg%k*uSzOw(kW#;`X|5x`cGt}Nao}z>af>IL8N zJ@_|p>-L4Z!DliL{h8bekD1*c`N*&E+^=|k;uB%kBD3m|J;{8;Z+Jnd37fZOjd7no zNG}fO)G}&F$*L!h23PF}wRENfI72IeQjuA=EvgUBtalc~`h!zAMgKoXvgyh-|BX+C z>99?Lx@K;w7rBGz1$EEQL_z$AQ3%}1X5G5DJ`^3$Q}w+@mT%a+Eus72egCh1JX|yD z#b*7wIE=#BPooexnD^@gjNQOQHLE_`Fmv?4w;R9dd9e>rF?x&V#=2uUpvS}&>ge`eMTn#bd&K0+X#$RldGCYyarC@Rl@*>kU4wJfmzJ- z37H)SWTxr7fx+mv5e>WkrpqGBq1Oyw2+3=6H{Nttn8Wl}EX13EpXxEV2wUsR3~-%j zaWKa`M)Q!_jbJ)uu48ks>zOW@sbmexbjg@5v`Mf7IXSbN!L0UQhCUV3Tz?fa&GvKk ztgeQ6dV|cFmC>7^#p+$nbcrp3zN~xdel!Gnas0{w$R893Tdrx&-J^fQ%0gFCR%L1= zQJNY-mM2D#S;o}iWSLBJYC2Vl%9>ZrxwNuQfw6ikx(-#>%j#rQ6}qEjtcNj6zmeH9 zG0V8A-BwPae)v)13t5Zmf{WNOa8o@sOf#b! zY!d&1m!<xZ&tukF|xFuQl};4E1B-_w*psa9qy^Zj183GOeu@ z2G7KL<*~d}X{IiPTZeiNmDT@6BaCZg6*ie(L3pSwWEC<$F_uUp6=S8^Pfs^?q4jDB z{Q;h&zmgZLUZ^Bk^!|D;FkIv@lA-T>Ef7q16Dsta7}yDH23`;HNR9Lb=$^Jjg+37I zNmvF)E2ZDdB(l!OOtaT*{U(+dPax)zH3$u_jn6d9j+tL{=n+>I%gteZ+*n(v-dCLR z6QM1CLENg~swD51H>&1)+<|xowvQgfdBOoMc9Yf%$EwjEs>0JX4O`Bhpz;BM^&s>> zSD;Q%e_Og+NoFn6*jCJP)>6+=%euu<4AM9EsdiLlI5&+px?}gj?Rb^EL@m$@Ln7Iy zm6M-n_rOGO5XnkklneOp7l?nU2ytGkEbUUB!Rlp@@|KytrKa3B)2X2g9q25RL zjWTpS#|F=5`edwfD68r`j1;f&X_xwvgq0hvTh`A(j^$ z>P&49UB#{2ma1a(wxUr_nQ@jdw*#9Vz2)yDSoM9-%LalueYRZEa9V36G`GE0b3?yl zSo#Oa)^qk{dgt(Pp&-@Jk?L&k8w3oT!BIioMbh*$y*N?a_ypO+9Apu9kNA;RLcJ+n z6IK9Wy{PlGeIdFU{t)enrV)wkcuNz)DO?Dhlnkwj{sg|sx3vb!eB%Iz#s6e0NdEAn z=m)qOARYH?Gtr#LZ`wo4Y6s~$@902&5S9lB*-MWn8MyM;qYUM1NtH<2HPfMxL(~P~ zDxti>6TG?gmSv1qCw42+PtHLlI3L*yVKJcve^tJME|QO_G(3k=4b5a#TO*p5>PF^; ze36&xR&JY%Bg;WnwJ6h$?Pd9$9iZj&XG{MxbXO{hO~!|_-I;UfBXCE00^w8>nsc#SCqiofKP3-W6CJ$}YbpYtjasDZk()Ee_9h*GX(js7_k0 zjM=el#5HGrhhfwalZ@ibaQ2n;7W+))1Fih$Vh)|725~Lz*9ns}-J84Q%;26IHvkZu z5L=)XwGB+@?A}FImyXH1&{8s&Df(t(8}ufWAeO3$q4t5eD2L#j$=t9tq3@ylkbyc1>E62%A)S)4 zqon|`s^K2Q)J46;-zAT6fw{tV#Qm}!`^=A3$76LFnmx_7bS$^OC#Nfig30O6!p*du zwzl3jc0UtOTtGYVsq7-lKjc&e3s;F85!Y!8D7&+@%T3qP#~U|^Yh*S&3GEYhg{;B| z%xhobe#s5Q4~V709p&qUl}lrvV>=~0tc43p4U8Z36$a6%PSzJ-SEI17Hn2ZfS1E2A zome}*2RlLksEts+;g~g#<0REX`V&%~mw{&7&f&Lh!4l*VO1yrK-ew&~t(S5K{|U`k zf3mObJ!~!_HHJqEOK*&u%yerZHkbAz)G_i<@?+I4#kif!2*(0@C3=#wB3L324)@SE zIdjA}ayiJ#>K5%YT1Kt{H~1#ycA%m^mpBn!V>dd}tRmJ)NkMhVQTica7e8Fupd~YJ z92HzexFz^CsV?6_NYQFBEv;3l<=VMuxzHq@)eSnAt(&C`H^6q8xrGe>sdP+OtiC22 zd#1YTQ1wM5?L~IuF;NzF0!piN*dM#CaM)hvyVXL;@@(k@zLs6Ud@{;PG^E1H%5GY= zpRwFE#`B4>`yxiYvyXO;pgu|^!nFS^PY_*M-qO$d&h50pbzt;ZYOi#G-^x~szu_%w z??kKw2Kh2HpE=An)7aSJ$fxLgxgTz^U-j9n&-7*hmC2#(ldi}7e0?;XOZ8Uu?6>#E zCaGT0qP8dJGRx@(cujF*q-uCvU`swH-quOE z`&*vr)r35JymEjhY?T?tSScm)&6Tr6drL>yqt5Bx#!QuK#d>Gl^IuZax%~;vvXRa^ z)HJQ6$SIS^*VIwHs9Z|$h`$0=p)#9bvk)HjIqR+T!)NL)OOB4l!?JQiu<4lCIO!Y`3|h(2aY6inqkkxAdvv@leN%KG9RCl5>N* zkaZGvU(6M4E$$}DS(n(>V!ebTVik1;)&;(C9i4=?wRNhxD|j<}INC_7=UAP+N8$)> z29`wJK~pu=XvUt$5@Jb0Suk!g#wNN5cLtAuqje8%Q-1jO2d^j}@Iqu6TVrRCo+3UC zJ&BZ)>R@ZA$(EMbjaXuIU2KokO!Jc;AT!K++S!)qbwYoq9SgdYWZS-M4H6dEuF%_L zJ{DE06S=KPT%z%h_%k{bSbP<^>+XTBj|_o4vZNO#vXixm0s0eRR&<0ilGsHbftuGm z%_$#}V^9UV1x%6o!p_LUXi%7i&f^yLi))_KgZGW*4V(x@qEo0!o}D>+ddJbzFrP3; zd}@5h-oy3qe07-I8m^fp6U*7XOgTz}E~1>;cs@1KP^yIgYb88CTz;m6M)1vq>v9tL zf;mec$8HMkA-mR7IfD0Mepsq{hIuD5os{9hn(0_@sJhQKAz^-88%r0|O0e_il=pZe z%N%03m^<=KXe<4#=jWO@mT`aMm*IMjLsg(RXbtv86=aw3h}=OZ(Ax;;gV2tva79h` zVEn`jNO`}8)yD)QC-|?%TFThFQyt{ofx{VpK(fDsV=+*eCIIE+FXD-j zQ_3%|F-$+`Y`QgbmY4>8u@kXz^5*b%V97nAo7wNV_Sjnz_r%N5SgfB?8{bL4rW~lB zI8s?4Co2}cBktmQd93ys_}S>9v~MZ(qf4o`o;q=}xsOI=adG4lRKrTJPwCd^FaB5z zNXJ@srUJ}cP6`V?k3)$)-&jRxTU{;_`ANz9>`WMZXo+jZ>f|Ox@kv^gK#Wd1v_ePeKd4&_oH{S zlx?eRJb6J(3w}?j7(A$UvYhpmj_<)$Qn5hGj1QsGs1N-R2np3Cme=^az=a)7EuxoV z`wTD91+S=3{A1Bj>*^{o(sG8aW*n2|ig4bQm!Y0`8-q{}Xa%uUl&TKX8#|h^qS7C>p zPLfUqZbp;UFVuHev$#`^w)&Jvw@{*RQ18iHWm)Qy*f&yMyvC1@HB|beil!M8e@-qp zx{4(NC&Ie0)L6*wvtP39C2k9r$mnPzvAMCr($F5ZNQN%o77oZ|$t3F|Z=Y=4Tm^`Q zQfhE)7$M5RCe_I9cibfFp#kE4&Cag0&15ybz1Szlh^q3Km}XgKYvKIDa7vNT^R$|& zd4<)q;d>Ro(2+o%l-~tf0OO(wK7p=6y})j%)kLdQ0tk>z$htO#6M;IO82Te^elWjU z$+q2d%C&{u!+S!P!gb^$#AVB5`hwyRUWK>vjn%%yM(YXNRm&+K`P8O@X$QoIre zyM;4s$L$^1ImB4)u-Z$TWwaySfR~yyt^qs!idIZ-gh_NKpoG;zBVzM2BK~%<>SSYI z$M^*M7)((c^83Ob@haYu1UrOO5gdBcv=!J^%RO!(HHmJ=;COBcf~BylY;`2Z{~Gr< zC#qwG$FZiudZVj#k8>AYQ*8x%!)0QX>E2}lp=Layzl~E2zqeQ=Up4$PL&`XxF)Mjjn)FPZ1+n{a)LRLp^v!xIu zjPnR3#cyhU$Zxy?s`ovynYdjVs_xN4#yQ&MY|69+D&^daNB#;@MXNL0;v79(?dem} zrjQ()rJg4-t{5{@A0SVaV!#>B!A!BFQbV!DC?8gbp2$uDkIy>2j*=|uiW`4VvScG- zJzkO6kLRRXF;|F1=$d?1Tq5^IZ}s(58)t&G3N8wR{ADtZg_r96?Z3t!^X=ny!|P6t z77-||p&rpw!2lA)DPttsO$=p9*lycal6{n0!3F7`!^8B_ww(zj;{CRn+OE*-K)qN~ zjI@-rhv=W{JPO^?`3vF}9cgLr$ccTMRr- zamx>2tyPCCZ!2K^l|4@lBLLAURE;i$^%095ptm_D*sb_*J{)b#PZE}io%9%%3RcPw zkfQsB+*%WAvg1$Iq8}3L#v1YGfv9`SS;M!~UWB@?rvruXn0_BSOuRLksDCK~(JktL z^_=Y%kPGjD$*Bx~FYR8Sq)3vRY_;9-_HN{_@}Chg&^wkNZHFGv-8wkdVee1|26O9lv!SHprT2=1#aw*hV0!2%f5fO^yJ7Eubyq*hi=!Px{{Rh!U#{hhI#Dae&2Udy}^MXz8t@Dw%5iMcB?eWe}|A)|Bp ze10CY-dER?VI74Bq|UMGq5eWIu!_gYg!GTbtJ~yU2yD4bbsJ*-)!Rt7{k<~Ii7koR zo-2tzJ$vZg%9`k#*b?PBnQXUn1+WUrhgfQKsxT7mU=2?@UsuNo@`TbNbObWkN13ud zBk`WQ0NqgeByHE~5ltXV_)fbh_LWYdc(Aoyw(RvZaaW*IMOUzC+RDHYjkVQ?Yv_8# z&eG3C2L+o%2dJB%4|SqiSuQPNGG+88i$Ut;I+H;*L*B^9(DRr@KVbRWMS0FM`H(E^ zi|!Ei>o_ERdK0tsq1prNHN6b-!Vzj5*cS<-jCNL^WX#1M(ywjj?G@SDgv;oHzUsr^ zv%dpG=OJLA7W9!&A#OtsWh+6h{fZLizXuP7F38i!diLIq_LeSsg;=vtL7|_T3wFzj zpd4yg>#KdyZsOme8Y_?!sXNSRD!(yKdn*(W_AA}V9F8|OolH}jioK-f>LFtj*@pUL zl!Wf1U(|~7Lai0?&auUJ-%g@TORDVU?u{VnA+&muHrd8;nWlrIuH{ zYP6u|urI7n9Cztj%F;;5a4&Hn{<~vTd_GSKv07>wltSYpEA=lBo}0W#*RTa`$H}G=a4?j`x`Q$- zRa_6^s(4nghvZ?Q8-d!Gfh_YZOE0c7 zabImD6XFr^H@!T)oU6o+0f%uLWW_Gi1GwRAPU5@tD7=k#$#t<)OgZkG>yEn{Q$P(x zzohjDyq4csUU_%MJ#g)%gFr}J5N#z^HyRO(3=HyeceDXYALz6Ghz}u?@Xtz0l#K3> z@*1g331@a!ZB9g8!WV+sVztm1dW+>RwynNEyaGn6=Sp3pDtU*|fS_}n7!L%)4zU&T z7ULsx!j_%eP5!NSR_@5n)qX}xio3ZAh`-3ZOb4)4tkWan;iyfn1*Fho z4#l>RdV^NT52PgN1nR@Su}vX-N)a`WR#z=%Or=T74NHRMDS1qL7MUHsCATH3+5?`q z&NI|Fv3TUq5Eh9k&FNv>O-z?CA&GAh>kii`KdjB|OC5)utJ!qf2=Jks@=<)Zjq`qW zFQQ3h8W?Ww5k)LkTV3*n_&(A~X{%@86So8CzSLC6%fBI=(v;AV)SqdJaFuD`2_%em-XSX}>%%7^ zyX7`oZq&%wt~+2?xx6atGsu8Ncf4l0%K4%T{gs24<;|9XzG~SGUn+e|>Js`I8>kNe zw)tyr75QsEAwYsN?tsl~azW#&hvxLWVnmg6I!0`)Fo41B02O2@QV3;HSYSI1LlS=yuQkNg#=AMxpP97lW$;%hi=(`V(MvChIEqXylGS&jFE zRCh@I9nVG|o(5EwQdXxTCdMn>kklv8m$L4pm zapkle(ZBO}z!Qx_#jT&ceQ9c+S5SZi^XeKKwEL*zy3c zql}O4NI#T;gz@-F#{jR}nM{ljYeAQ2VWmD^f=I>>q2ua4y)JyhL)e_o`o4Si_P~A| z5GJCz(0$8c--pB;aVMbb0gH}_twm$VnG~3_QGWF)RLlnAeaSF)+=;Un)uqu28FT$B z6+5-up6EPlkE6bdmf+UVO|h)L8{dTS>S(!wQdK*MH=|#3!>#MdTG|f2MaDkAU0r5h z>f0TUyGv0^<#Bv|p#D7|$1?lr-Pl>Zx$zpa(LU=aPfh1OQc>gh?%|WMQ}}7y@wj;3 zMJGWZ(`Z?Y|0bGriZba#9Z#H{XRZNC>1%JUMhH68O{jq*4}?O0;>}nl(9WDNiJfP zH1o)EQ*ncGQy&G1t!~^xXDjNfwluojAC9a-6KxmbyuJ;#%0zduK+nN_hiEv}m>!4!s*YC< zYn@SB%mckfj9{d)Q~QFgBH~Gs z>+hm0C5#MVcCdTIp|qi|yGz7Z@+@RlYrCS;BPZoD*k52>PB&5@ll9R^Cq^=h?8&YK zrkn9Y$m<^#h)I9anq#*2ysbQ0L>v^z6YVM%Rafe}a67ob1*owRBx8B%SmRtpe^7Vx z&X6m74@hCfJoDl=dN{V5k{%u}G(yw$b;f+EEp$*EG*}}QUr5ij?{sWsPHQ_OB{Mqs zzbozOh}CdSajMiZ@sWR7aESO^+Nn9Ppi)ZOtMr4jmM4+oYn%U>=npnWs z)cO$R2u}>&6D}wX_0fl^d-c zO#kT*i(QZij>B_!MZGY}rgtV*Ayyqz{kooigg&yo#cUwb%RN%IK#GqQ(DqVZPUJ1_35^%ONt+8V49?kQE* zFd`>pE*b(0coDjTA7++;0pl8dUjN2d4K<8*MANO;6FO%5#q*5LDg7#R4_}ay;d9i; za>gdD4`c^h0qJRxZL>W<&Xw0h-Ucc~>O+lTmG^_s>D){8mOqB)g;oo_!Rxu?x z%hfk%A@D-RF}v-F>@)N#N`&48swubV!fr72xj!?%suM#A!Do?u%5lhQFV=s8-TQ#t z6&=K8P&uv5xzmJOc10eB&PZR$TCPpL7T&Wgr>=>3!^QYt)w|dgG7&21h4h(dDPEOv zIXE|N>8bN#`ACiECHWVok^5l$Gxq_eofH=tpYb6!Si6ZdAa|r{tX5Nvs0EF&^haAR zW`>r;j|=P#j#7cXZrkAP?peVt(~E^_2L?q8!`@Cl<_2*M*b+7%Ra_wYSR1-OTB;F? zl@8&gP<3swWt975+;HDXM*}?&E(E048R`P^9jlO2AoDy^?_v}nw^~0tY-|zZfmklw zIdoH9YPsqz8DGIu&(;GC3QWt$2{kW2DnUI0Z{2(4kUCInP4wkDSO-xK?T=_uAU@bc zd%?7E4~u{4t`1d*E8(FbELKJ2Q4=C+?1m1?)llJXK=riNgC1`J8zA2d-;M0kN7{D# za>w8IO|Y#-KY-g;Sh%nJjZdZX61gFfdK=Bs@8P33#l8ZV&{qC=y3=1uo<=Wo%bvo{ z{LEEpTwrs?y0BMBR8FAV`Ukb1Tmm&VO5le$&Nh{JqDs zleJ)zz=wz?RF$rwt3cXls18RxjD|!GYqD(;>>ad=oz7SvCQx&(v}c|7j?>HCL2LM2 zvGdA7U~T+?6#x@%6|f6UwISngB?Q~*+ab8MJ>MqD)hcUNupklHwUGrUA9 zujNqlD7ED=QgOK2h|~9B>)ENy98{R!<$sn^#Q#*0M6AN>`-(>py2DoFN}`O?JPQQpxskN~EN5PMGc~lw(BV zPS;N1nZ7WZ5)0D8LS>javYB>AitEv__2fYzlc;Ts*ZI(AnuFhQx z-_2?iZpSjY4WzYM*bvl5`d~Cp>M55}zJVKNoVhI6G|)(!g?_Lgga0P>Avl&y$I|k+u42>uBo5k47f?W;rKQFH}47QD6ry=sC#2=%%}L7 z*afw;ekaweKS*V|iW^TqA}SWg$AN^A$2g<@OV0Bzm7qLPY{u<0C#W|f4Weh^y(S%g zuCv%w%cfn_c;ctvT5`*o&OT^s>|g)L@L{$_DQ!Qmd=SPN zbHkS+`LxogiagQY$y3q0LT*GZME(h+1{doMC9k)Ydyv$cO*Jn@pK6HjXCExZ(SNlK zW>a+z5N}m+g1v(8loFuBv=5mFQWvW`#g=iUlJ3N9gXnadP>swF>L|FfQ^|YiuMY%9 z-V|^z4dLp^)tm!yL$gI_NyfD>wfZW36K=%)r{uNzMMq_o4mBoylq=E*yZ{;pVk{Q> zogNUK&cX3ZojFi5txq6t#wN9|xXe8>-s3qh_O(_)2ds)(fc61G$qdM({c4%HsrdC`yv-i8Ncb*$q2;Bf4OeKbm4<_Jk}^M zA{eC8UD@M@g2i2?#KZ9K6g{he`<*5s>5z%O<9g-vE!=uYr%jqV&|-9=3t`HWZY9};Op$TfO?so!*v5|G+xdXmouSF zTmySuD_?M>zlUA~OW>f%;L4e9cnf{Yrb~9)9a|pT4`@kvXJ#mLLVZCW%AY(h-H_eZ z^98s0_lJKZE6@^LjNUP`o2Sf?7Gt`&((ZJZ^X0?vB;oG`eW*hIz?Yd4Y1~#N| z;pJ8*AunGUGFCn-mtG5YD_6wb&X7_Hl{dNu(zDXk&B7Dc-MF^Cm|VnalgX!73Ra*M z`QvaxcNw>0DbcX`fvx3o*|)>~c%ptGcq0o(E$%n_#%$iiE{;5~wN4M_3p?3Sxv+Er z91WYT4WvD@i#r{P_vbjlxeu?Su2?jZVvSbjc{;{-apcBx^p*bFSu0~snq*Z6vubNH z%HYg7q&e?#U3An$HH_iGIjPGthG?b5-mYiv_x2TdMd(vnnatU$&l(7Q-DRd<6`-S2 z4{sJ!dpXa08%AT*tKlYkY5dZ8FWbVz>~WW*g|wi0KYGn<#Aj1xh+EJZk{h;=ZCGyU zlLbSBnp3dfrD+lbu z6oiUKnSXI`3b@BwIi$D;-s-k_^la>+e|~s~ITH`&*8#EN6Pv0?6|uIUZ^9rjRTV(Jj2FR6 zf&I}>_>gZ+{0CnZ`$J33Qc`Qh`2+7^17F*VxuJCy>*3!-%>mJd9h4(^B?@a(o%;I?S-{ui0QM*9dgyj!y!Pgv&J zi;hQJFfmT&f|8`<7SD5!$s+48*B{&L1-;Lm7&o;l$6otaL_gx{zBss_sLm+2Jajih z4<3iBEkDqcDw*FuPiu2Eb&ES3C&fP2m&kbk)^C@xRvQ}}Z(NTZU8MZ5 zNSvkDm_;6FD|A1JFhSnxzAdM+WtunO&vM4j@PE5kCp?JrxT~QC(Y=8-#%%66|6DpJ z=fqiT71seHDc&`~+uc?ewNlRqe)nhB9dbz@jqBk{kSm%$22NyUk3KX~t-rXkG-8JI z^X3BfiQnL?a4e53Ft3y5%jbwkdCYY zS5%5RYucNka$5IL?#ze56ykCgh->Q|mJC}=J#-)%)bO)E6>nAVu zw6SY!oOV1oGtkm-OW0Q}QH^izwxegU?!nc%#)X7=${6_%?BaUkIQ|Hq$63sKL{7DC zM%HKS%FNUY%X0iI-(LG%p=c~udcM?+(ZSH>TZePAFM4*hrI`S%I>lDUUWrbMUJ5MC z=oolOR@;QcE#7_3KEkqCM*8kxb>ju_T~t0%bkm|%3SP?JmqxiW>?2WLb9A(8V68R@ zw|7^`b|>K%_m6@jIx%Zx;6C}8>f`~~$#0R9R#ixQ+T@n*h4L{rRC^QLmuW=n@fW<| z!~qEdUBEbx%uU}GPK8Or7-5$9mbQhQ@@#G$ze=g*a@!L* zjo_E)0XS{75<1IUgcA4*E#wwH6@Hnf~oo_^y_pi!;Kl%oHFWYvKX8u)~ZfZk;J;-24W6p9nV60d30Re7W_A~P6tPlFMCqI#FMTLC|=(iq@kWB@EFi$ zNZA))b7%$bsyNb7-1!&3#OM@K)9z$0HSQ@bVC!_jcVGO=I1q>g2S@jkHmEQjK>yN@ zkqu~qTw7Txr%TnOmC#M|-)Kqm4Xz?pl{0KJWIKHs4*17~Pirg9d8m`v1e(Jmfeq8o zT4Ut2TB1C1A#Rr0NnICi5XnoPIO@6+ozD=*UJ^ehQGGFq zpb_E{SCecb+&#p0Y*}=7aE4k?d=ek=y>m7cRqbk^cIc(Hlz$*+x9t*pkgCBe89B4w zYi@3qw8_2FH4G(ahy2CU=Vo*wy-a$WNElD6u{BelZ6MP8F<&e;i-9o}_++01)3X6GRyVY0PMyZC3p4@Ty z5STXRy81hwphD`^=#t3D@Brf$^!MQ|!W8&_ot7tXWzE{g4B7@b z&PC9V(r8;YAv+L~im{3MpW1!ytn`<7QfMh#!Wv^8I8}z2G8-lIkSU&F{ibE9JM@A^ z13Flo<*OH8RcTFU7!?EAGfJrwgto4w$|I+#v?STXZfy#i!pF&lls5b&eM7V}*ivKs zerc^|Qan|L6HjERzkJrAm`#4?Uf>*RpRa5tFS8E8>98BP!QMi1$E%$U`(j^7~;C(FH*FWfJYZn1dqNPXnC z3#Z(DUD>%-#!BsL_?Ulzwc63wwLpoNi}E#NuQJktv&kB+wy;>5@RjuQ178pUUh}ng;c$c)2I}>wiO}I{AXbS>WbCUtd*a#%<*i|`A zakytBoVFE!KUH0{E}>P?TtcseT<)6AX1=4+C3Ri23tKOgkOqSLa0psue2G;siU6~7 zfMCdOkL(ukBr;vK2gHm~k=;TqIZZ%vJ~1o$%HJkB8Q3?U@G&%xK2x6si$r=GrMS<0 zO}Uo$uKRbcw=qvk4*Z=p&06g27`NVQ_dK;D%Zv>m0x-q~z^P{-x?QdZVG3? zo9&aXNn_|n!%d5zy~d1KRqmKfxNG2rX=Wdc8!&?(h4PUSbUaQ)lgJOoF6%Luhi#-A zg!Asca&9>FSB%ccnjgu}J@!IH-S;AHxx86z8SMi`tqfx{%T9T%SD;aJlxcFg`Pa%t z&vdz;xi0iF^U{|eGX>&s=5U8RP2F#}?vcg*^RXR9K3W1A5TDHrV4`H!RVgT!bKQ<# z?kOzvR>y~|KoydfK< zD`K2KBW-oo3sS|IKkf@~N=Au7?1leGaDS|*_Kc3kz0f?e%UsTU@C?kfjduSi4};eB zxWM6z)sd^b*Rj#}r>BQ~3{DC08Tmu8@MrkBr%5Np>a-yc#=g;hxS_3~^P2pXj?=8j z@yyzhhuk-J`-D*9@4gcJ{K%i7j*;ilJ>)9(Nr~b+nn-(D&Dm5TyR)3Djx?Hdh)xfl zPuD~5xFzyJS9{NNdrxj(2O);B{(~@WYH&f!9`Z zCDj#+8{-Sf*|aNxScnFi$J}NowuLKZdb zJDzYZt{=?Q1ocd`wBCgp_#VGZ_yBwTvhptFkv+@vyKAtp*f<-zo^>hnh`toOxPDK4 z-)AMsnj0}f>w-^U61N|ylQHPSeY7S~Cs#+5Z5i0mr-pZD&Pl7{|7tp?>qjCz0@IAS%1htEcz5=uzD`m_ zT@5dahGOT8QQQP{oGt*Q3V zAMEevsu#&>pu3vT`Y8l*UPtESxE1x8)EaK(_>r7uiQu) z$I{RYajE^FXP3|8tL#|C4>LXnH)PEZ3+$zQ+Y_Hq-MfPCWt`BO2e*YP$42RswJv&I zIF#?hVtsZWuHw|x0os{`m-He_(+a(xr3+z|9_r$3l^8& zIO}^4c@H`MMwXr!{yFVz<|Qr0OYYCU|GWzvHP9v96@DJf3VjXFgS=83nCVu)=cGsW z>F#;XJ^X1sHTos<*cZWH#*~DZBYXUP-&lDW*TWE1F_Jf2L~XA(GGCet*2`85E29;lMy74V|ED#K*M&_z5v~u7}j-XBQHf53Rgks7E zh0ErjkzU~{p*rEw);+m^?SwPSQ^pps&S{B}$vI78Y@LNfXF)>_-(*6Mi?p zgAP)wki}dZNRr?5=8HRJ>m+P5Q=-=*tJNQj4^|p$#a=*9*iK)NGWd<{D87L<;c>=@ z%y$0D<{6TUma`qO_ebTq{A^N82)iOrV_o(2W~LFbV0O)#qMxNHuHE)rNv`+Arf||z@lb~Wb$J!&hMQheqZwumn(Rd&6K`_Gmn?ckn8->2p9ht#g- zRJ`0Nx)bbQl=Zv}6Q%9iap08HA!%fe)d>BI!Py}B9Xp&C?02Oas3W}bbdCOI$to2eO45j7vzHWynKF&%yO@2H1%Lex-K_FEZDV^ZFqs zi8}tmmI3ABsr8g~huP~8z9>Hw-pxlFqrePph)>We;MlA&2Sk3c(zt)jI{2aOj+7|y zFyU(h1lOqQ22W}My@guD+)M|M{?y5DhX%z8Nf1A=vijUux^^ixNX^4F7aV+Xp(sy{ z)9PMB(GEn%0qLWTT1?w)4A#2S>h}E3;r6H_UK~cU>j_k+{m^xLC!1ZV06d4OOu)^A z58MenUKEgrp5O`rUwsOv3d@{(T>G8KUeQ)o$ZeGaLl2|}h3;|@+c+sdDq-|jm&fjC zU_XU-vU*{bz}6U9Dql>LG~2!G*RFiZ9U|Goii?+f6{z(KsruIYv*(tR`=Jp(MH z4E+)iQZL4OsROLW;xMJBZJyHDRzp64OfH7YpzAol)L0o|Yb&kB31D~r2Z+#(!P|sz zS=Qd#rYGs`xuMD;#{_4@R!+JEIfogfmsOZf6sT00f5?`b>G}Y3gLRXhw62g#;N?84 z&r~{ex~Nx^)altB1Kuu$M0c1kMvM-xwuqQ<9V$ z_=Q3MII)jyqmo}l)&V|@;7jtcUH@ImW< zHaSj=XYgm>Ps1i?1{OuLpsW2H zTH>A2256aYH5ck{j16F8$Olcxg8T(YkoTa3>&$zk%XWwTBR&i>;WFAtu+hG-_MvRz zE+IR5OkM-mVv(_iR3YV9DYTzoB5fC{VlTJCs*2w8-G%8=E~y{<6Bl)A&os`Q%T5b+ zp(L&Ytf*$@IIXdf%od=XJmy=Yv%s`AxxpeQ{i%qK(GIWUU%OvkCp6&WFb5oA=mm41 z@C|5O9$?q#D4-Y*=S~7e^(YtSj$2g?qL$T*TjhZ#*dIs97~>xxa{!OPlGy~-jOH>6 zX|-bw)Z5xaQ>0Ib&lJfzXv}PEJ(b)V7JGGX;Wat2dgPFo7Xo^R1 z5E$KI@sJ#n%F1J;isD%DxDXY}OQWUIN>Dn=SLO`30wBAMMCeP(?3qzRzo~AIu2hd3 z`OUh(>2Ijtgk*GW^KYvT@D3E~Dtt7y=F$4V4L=8Yi27DZb2h1C%>eHuXVoKuS>NR7 z1}h4w@oCV?`c5LA2t?ztc!|(O%ptzyH}P*^#@JO{D<|5TD2wIWl3Qva+m#lwTWTx( zCZzDIxdb#6rvOEG3EN;5H>(*TU{m|`gT@G>1Q|j~kipQ`{@%A9M2nNjdVRA9T?7_{ zGSIay#tq_%ao_p%1JG_RjUFJ^%-q%s_6Kf?+oRL84MC&{Swu&$n!x9%fhaqTCi2I{ zjZ$Z6jr5$~javX6cqJGcHcBJqGSV2nE}qD}1|~A_X2~bBDEUGPS^cf=)`fzku#o_K zBSULw%pvy(Cg&`RR);y$aXgl+$CA)@N%P_n4axUHzqWH}sh+Lz=iS zP5_2zGVsB2;hc~jxq-`ai{L9B0A2P$v@q+BvZEd-4|{CZ2g>aea1th1s=32lKqdoO zqbY}RBCsP)a#PrM_H#Muu+&Wq3u}en;3@ozaF`#1k3bW<9FF`Sv$`Vwhkt~!)0X53 zX$w^8yXJFJ41S*%tl8Fi%3Jj;M(ff!a5c`c&U3Z!YJMFsJxgHV!lFCK1EkI+d`s*G zX5e@7fg6AS>$c2qv^3rtJ)jd_hxD|1P%kvKiM~ev$=qU127dK)V2|#lU%4{EKA|r^ z0eA{|@BsXs{l1;=C2oVajGcUceuwa0*oh0FMr;u&Kq>;&qZT9p8nZ>TIjLa0(Q+Bx z%ys02nak{MZZJF3j-0_-0D~XWuhtoJ`ge9WQ@P%JC*cb&$`9m=;ep^v|IXxvzlW*b zPWbCx{Aa!>PJ?dy-_|G~y=H<#W)QcVOJyb45Vn_ngO7CjzhA#!y&rLY`0D<^KLa6o z2rGgH@H6ptt}nM8$Sb+=8?+U@fpf|mv<-~Yt6@ev3|)r(;X^QPKO%9oE%m^;#tZz6 z<)p2-oU|css|9tFUOKODF)CSa$WWRI*2Q7K5qQtGpr6oo?kz3>&ZH&K+nz7fmu5?s zMOkQuzf*Ya_!e9#P4QCT0xacj0UzKN_rG87S!tvW{Z0a(%FfamU|vVGHnZ4iZX!O- zP35W~!m1$+odV81{FKRH(%Ibm{j^KI(~zUaCH=6rTTe2(n(NG`&_Mqm6T-{Az!`ik z;Vw|!E>IWDZhZVw-ryhL;(Si%cjp(r^Q2!&o5go{GE9Kq@TY`DQZva3jMe3Q8-6AJ z&I5VGt%j~bPS}io=Z0*cuOWR4*M<4o%mv(wb+ntc+5E}K4d>`hW;c?=4uebZjkVf( zM>nH;cq}kb+;HZw)24JGy~^gG1_;c!%?dDZ*6#9QZmQ*8mm-FhH;u z|3{4|K?k$qa6J@8gV_Nx*)*&@)@@RZ`;*n7GvNEU8CbwAaDMm->f_RMxz(F{&(*}^ z`5ov0^|5?Fp&bv;>S@DmByUAtX*v{p@2#>G^Jmo>$ zSgVzpVXQLplR>1YIaDuZ3^I!AUtwC{q1VU&;)3saX*v+@JE!6OyuIZEO2lv0UAmOr zMAO0I@)>{S4&hbeN4y051>@0iZVWC9&8$k$F0Trk`md=|=EJMI0=D53H0_Y6)+rn^{N)n$6>x8btCYa;oMeo4eFu>{uC-x@X zFm{5Q$o)q;T5qiBhS#WO{%-Cxx*K`OOGqG&;OnxB##HhHkPd2ddGTbSkJwli>_hBR zBo|ON`{8XYKf7fOux^00;4gB)tP7Lwo@hDD*QatnlWTfweHTzArWuu02e*=MMn{=P z!BMlvG*}rqLCS$Bx`pS!)AJUj{Hj__aTT&WmSi1aH_cDhZ(y?d5hsgj@*F(c>_;|p zW8iZpv%I*e5^q}|ZsE%a2Z7mA6^tjZ!SuLSzi+JC{9^|aI}jN(DJaCU(>I1K*4v1eMtIJ< zDoDejPuShF&wI_@haZ7gp}}G``&V(5^^H!)EBLd(CEKoNX^UZxb|cFco*v4XImTZ` zT?)C~9D=GhFvr^qXLBX=cQ3-a)tR;ST=t!oZg6>}`9f6OFTA7m{8zs6X`aBe=_|D~4Uc8O=XRmQmRmO#m{ z+qgseMkiz)jO^gb+9xXqt@(Nl+|t?3o5urNRdQQXAY1#RGfw^o>>Nw^3g9a`PM52P zzZq!-g9l?`bgEuK+3e=Ur!cJ>?I>se&9%~f1OKacHP?tmoku(a>^sFcpx^!^G(s8L zmy8za(?TKbhPo%xNpCD1ah7)EmdEiW;Y$BW%!Gk%2eK=&)T|iUw+G2HSkG&CWzei|g`B%U>ZlW}iZ{YKE5;&F= zQaI8f{Z!Vq;JnCI?I1tL`7>%@9N-kkZreEiwshMzn-}#R#&xi^)sf3fQxKeBfSKOk zs1i=~e+lGO(}7F54yU;qIRmgciYSu3IbT#*4R8L3qN(9Tme=+~{!5yQ9uU(!3taP( zYK~x;P?9<(QZxFIRq-U*>X26IaVjbgrO)zpyM!8t;aE!&T}J zSMuM^+!&OjY-oYh+CsVtwXsHZpn6zi!G@CEedODL`7qvr6?MWcqe3T$F;6emtMf754z)8Vw* zHZsMG=QdcaP;*CuwgXV z-jKEUH*~^Lz*|X3i}Dc*wgXl8faFpfRvm2w8>eJBKG~C$h1~e)%W!FJioQEMCy=DJ zF_#%JtB^e1T}NDH6a^d1Tk9EV&leF!8G4Ato}oC~Rk^WH03Fbls+)jQUq?5Ar`XeM zh@-CE9wO}1Bz3eol%18Ucvef>)b`Q6ETn9<=W@m?547&AK2yJ+M11&(BSta&-68;wu}TQhfWS688w$r(+7;TUJM(tDZ3vD2~M-PCrIRxw_{ zdr%5G>n!T%NGs}Rjl7U`%x+I|R6)sVyYS7}H~p#6&Dw(hgg3>IG8fV{lL534jCh>1MK&1>qlsN z_LKQQZKw^RwISKB+A52WOg?%u{4w%EJ)`9}--sW5UA()bfR#Iz=)V~ZSxX(QrE*%^ z7}DnHE`GD)xbp?N8~c>8CH-PXIGmHt$IuzD5ArmL`^S?k&!^{N7s3zCu6!-&4}Lp& zq<1Fyr9bUQ-5$pV@xGBeb}S;BBj_~ro8wO6D_;%R4(=pbK)QS&zl!_j9L{YuJ=PWA z1MgHm+OD&G;fGnhGvhN>`yKi#WI^ImOT3k|*3($*rT-Evnz=0e&AKDxC!HdlqL1`R ztc%?iKiN0lo`=l|j`n`OgA`iAYL}q4<%~T?FwY)ebt~PjOvxGhDlDa=&SJTBC&aF~^Yp9kLTNg#t;hZ0L z%sHD|7M-6}@!O{K8A zcOWU@QW-@vH)$b3@;=OUJjsG4XbtUQ;7P^?wU4~tR?j>U{Vz~0R6F)X4EU~jyi)gA zVqkRI#c!Ph?b&j7r|he<^;Z7D<&7=T+L1v({X~wvSklj`O(Xr)tw?bG4R_GZTrs0p zq_{svKs1N(*!N4e<-RWRO|G8xJbF7eg`Tzbb|^vuuo~i{l|muC6XeJ>c`qxcO$$y) zKc4lo-att4mdUhTgb zNo32UKjJ1P)%2dVrSg-Ezk>Hds@2kV0hlITsT!*uni3n#{ottTp{`m|OS5F)Q+mtL z0kXrLk!@5$Gh0J;F!V69R4CnIj;+3xP6M5ZuFZ7$KN=b0V~^;Rg#Wbqna{ozPphHM z7v9FBoHG(4$`^wj!OQS9x|Y4e{*-z;}Tqh%eRJqH{QabZghpAFe!>2#&- zq3c)YBWV@bVV{TZ8ry`gN^8$JZ!<>%+$Z-(cbO7;A^t1%6|&JPni_2uU96u(XX4i8 zteO2U*FbJ!)=rAOjf19RK1*#>>lsoozJAMTr!ZbE*U*d3U|^WhGa|w>eX1%n7bjS*^9*uOIni< zbYA1T#%2cUgc@3Nz|Z}!w2QA~<%`@4{in6XN0g_w3sO_?6W7pIsDw=BI|~@6v*us| zi~xaq3AS~f%`90AE?bGi}60CwQo&Q*!NiJz|D@S{##ke zvEy`rFjhV$glSzf-JFaIgT2*bqtXy`(s-=bAPELf;-Kx(iA!g#`HOsao?-#-5mMxV zvMg)-LHciWXk<-vN35FlRCvTMmw4M-z7BiJW`ZRoo>qg&%W>Kfe*~)FfB0W`4|u8< zjV_9e(+JV@5&B2tnATSNLMF*A;~ylP@@}xtphu%>pl|4b4g^ZDS_oL zp4Vp%d1A1%%D@~gbyJnmMpP?-OCpY2^ zQp32aE{|4;6=BV#-{m|4j}o~$K=bUymlAs;f#lV;MWzK^p-r)Pa)(@`4&gW1?%M(z z#j7)L$dn=`9pgT8L`J45jWZ6g$^v4>qgSF%f?06!kh#WlAl(zF4D7N zc3Tc71BOEynG>`{TE>jnU2U{sXp6L*q!G?gHh8x|uCSiGhFhcU(&v#r;Do#a z-pg+EnYmW)qx}F*@d3z0%lM0m)BTg{2U}zDCAZ9!HOz8KkCi5lrS`gVZhioJ$fdwJ z{}#GzmC!P{I-%h1M6UiamtGQa`z_umUHEr@;<f~I2Wxh{5N#kNh+Sd2nE~@FyKhzeSQ6$~cJy&IP19@xgoUf9 zE?x$k0|z$2-}(;bgZ)?`I*9CtO-_FxhHeH|Pb%kPC&*xy2^)yT+yH*3V8cf&*?LGv zaWNo|I@t!;7JdX4=^*z1*`+0|R7miQ6w-tp{0W$n{|!^G29STh4t@XnScI;ahuk-R zgpX*ll8{M#e2eY{t3`>Q&<8J zb$=E9Q}W5@fdTBtGq`(P66(S4<$vVXvL9&(2=H&fez=q^fX%{ov!8x|lL@0Jt3=UL;9b2$`}sL;+5n9@xb(K2r9{?{hz<WfkDb49ucsZhOWQ{sXA&!3j!m#loiqZu~P8vHH6M#jV#_Cv&=v|hK zhNI&ASD`pRg4H%(l1|n)G7lzn4&d1S4jj#O#uGDP#Q zyegZyp}=#^3HGyQC_y*|rp~`1gVLOTj<(_+Ktg&CzxKvExdAjyFR$k_YiNJRc<5d< zr8XiMnOYrviQdQDLUVEZ*c6->zW_I6G3yJ^Ph#M-y2zb^Jl;^8O<2Ph;Iq(k*faRp zMQa8(4L`$e*eSCNxe8muN%Rt`33TL7Y$?n~i?FZOpR^AcWqYCLz?22+Fjxl90x7;O z>_PI=d)7eMFO39NlbiRTqcEqO%c_E_peA8PU4z$V=>#(S36Qf|0|qq)p4!E17x)ND zp zC(`+6;(GBldPh6c^IS3-4}GEbI1IC=5vU%_WK+y8IyFB+Z-oXZ=@rRW=OB*0W2W#?eVf+wTG ztOx9svyh)rnCyyd3L9+kTsJ`!tMVIBS+)%<*lU=BUoQVDY=#L>ZQ%B1>d8PBXvpV4 z0sV{iSl@4C5s42wYP+(F*MLz0`w3csFXHI!JS1FT*&$K2f!2c0^N+eNaxd6AFRp}#UK_*VvQ2j}a9m0vxtgnFh)wjqJPEai<) zOW0xCKswRS?1(wi*d(YTHs5CE%{ZKy)BiQJUfZg_i8*4$(8+|_xsT+05I@hhlGKl! zGN;4AK(YV`$t1&vu{w&!o;p9 zou(VH^VU0Y0{1ESHH~HDNxz#h(OAjvrv*6PnGruLE}t`}@YpC5dX<%8-V`|@Vs25} z#UAK|(JY7HZD8w4iQde-2Yaz~Tvt~=&u{pA_*!sjI2z2Q_j6A4Y)6IIbMdvqYp*Eo zmCoWR;WJ--SxeMs(YBFfI5U*PC1mDZ>5+xb;II3OPL`%PO1UpOuJaabl>dQd)EL~2 z3<>86ZH_)ShNCEdLU>^hxEnb3NdNF@G#N4=siI`>An`mBIsilaIj(?p(kiM3)zIGP zVf_+4&fSEq>>&0*&X!Qg6NgjC58B&sB|`?M(;TwH-!ynk>&;ejxA`x?sB~&;DD0HD z0@gw@7U{ODiD|A1=!{k>vN&qzFMFpr;*F(h4?0=UZCe}#?WL(b+(G>UW~yF%5$leA zM@!{yIp?|$@R@qa;Nr+0bltPg=f@==54sDiG)_E8tVxb$oyhVHfGbH`(WjZst&fh74YtqWTxc2OLc-uRuLjIMP3z42h09h)wN$LR{wr^~sBg7xiRp!+ zUp3+qJ_DJlr`1G%iaN+Y3I76FJ{25T^{o^9M||8UtX9^Lm1?V=5VLEh8SP z6>b$(iCwh9wnXt55Xj${T|Q0h8)qYf z^;9aMtC||wYdl25q+jjL9W88gQKrR#>-aHOf^Q(^$0Ion&&5x{5j7um=3ZM%ttG4h zmB=Y`Jy7lwkt$@lzVYp~H~KAZC0NDJNS&3QW^&AlMCBs-phK4y?vQ(okLU?k!)O`F zsddK>UEQ2_tqE!|>u<4$a1DQT4Rhv#JjZ{o4{=z$Y@UPt+hQ|IEny~5L+#<;5(R^? z^tW6a_q0rHI(;p)!*ggkmX8!wCm0V{T~Z}lGgegnI}!?K>1ozC?QryrJ{O&ER&~r5 z(zqgmSN6*%6kfT*rs_pmN99*rJ*gEx46NBZNn`7x@J&2GOKDw5g0#Z5&^8S`*dvfz zcq%Lv|Kxu#8%G8kc`?j_jpJr>HWP@sE7UJ=wv)i?ScKGoKEhd&se>I1$k;t3#h#C= z6|uA%;xzd)zCstlb9s-MtPY{QSzDdNyxdb;c1Q@9!)K&rat~3Ebfv9SoSiY>akmjO z>zeD~_f}u85k6;*h2LXA70RKGX3j6jFb)Ds`#NhXtaMxxM(L$ZfgdI~^cj(srbZ`& z;XAk0j+8PM=|hN(`-7$me$PVRL|H%&q?fWwT!^nBRVy8O9GzzU0#7c^c%ynvzqJvl z?pLgv(NmEh(LsAmDNP%wv$YrIYp%H5T^eUrkGbdvdB1~`Qn?b;g01vgI3ct%qOqfp z13n^mFqUdvtUX{wpQdbbv_;SKH`YnTWMmUW%09J`Q>@W zVXdOD&Jh#(p>g<>`FFH3?PW_7zG<_J!)PGaM60DbjRk6hXfQeyPCIY4`{o(GO2Xg1 zGhn@3fJ*U4&|z_gFfSGtIHkW8+X!u}#qjL?%dEk*7uNE-tPSXyy@B%zSBkWR^HV9V zh+I#qU^LbS3O?Hb@SiQC1KBa_)9xdXqJ zROOz?5xkOi2jl%-b#vrTdeOn#71lL0L`6V~Z-eejZ_!@3Ro+peuA0(9a@ER*a+2~U zCh4)RA%EzZQB}I*T8jGW*R5V~CD-7x^I<*+6Q3<|Eq7aCpjnZdCwOTfYQ`2?*sKoh z_aC?xuswD=-@A9)JBfS1j8TC*0?(Zx?67W_m$@R)%+JA@#EALTA#mdA!B(rQv={W4 zT)<77yLfPR(RSii{5#lUx>%oLtIVfDY3Sgz)~cJESY5E$on-GJGuX-OByDyq>` z86~)aK*e7p^e3ZYH_XOLd3$d98;MvISR!?sG4qr9Pqevlo!iKd5l%^iF>;mTGD-+;TL z9WW20K5V|xpOj$-$!4vsxe5Q`$tPX1>a$}?oJja|$u7->*#^&zl24&UMoTLv|CW0L zZ&V+_G``jZ>J~b!Jdsl%Ep}FzCcWnup~*r{0kP3YQHsjb!6>th`vd)=UkmQmnwuF~ z&uD+L*S1x@VHDJ7@sEYqGy$h53OcL~GpC9VrJrdLGYn6~rdn0qXWUXRMAxYQ()CKJ ztr0r}toyU}>F6Z3OCDY_Bl<$YCzW8jcA2}3CYz6=%VPI{3iC$K1?TVXd=uMK*F?b?{C84cP`+g)@+b93Jx;LDme*_QDP?Ua!9-e}WB*P%yk$mtgO{5=<_I zn4kHu-89t!Fn4%i4pl2@yJ!NidljiUZE1B77b<(ixl%d7Z{-AHdI!e_p&+Sd<;0WO z5_6xmf~_%SeX8DzB%|kYGiOU>C`U*>{2AI>PgpV-IrACat)b|OHO&}d-L-z!Pin*2 z&)gzyK(vbXh(&afA@)S~%mccjUoaY)bLd)V5b73G9Tqp0$^%@W)9dBWCO{che>OIz;t{S~7QCS^`cj z52Z!{brakdm&US%_Jlg=Yw>yIPx%nH3YC+)+ZOT{Ar%k=5`GHcBLFD0AARMN?mCSdd#ZPhh`ot3%}w;+R)euJN}nsY4k+wjh?9` zXqhyo%#u6Pxxl4+C4WZwQCa?HG9!A}e1Q{bk`@4_%Lr)Ux7X&I1K`8;fnjT08| zhjB=#%yrNf>lRSqnu0573tv~L!<~Vd);sMJm>~Xz1l<7k4wn%d!xXI^HxBJ#4*CEk zaFeX})a2gaQs^NViOPVJGzI?6XXJ*C;2n9i_=7!YFDbkMi`NZno^eSVMI>`i7Q(( z@FQ1?EHTp!EjCGAVNL?tmmxP5Brx9R!H?Mns)3op)GwKhASH52&oJk4S=e48tNbK9z}igaq5Y5yYX`KUG`xWguqu**=0i9Ij3(ttz)I#C;`88D z=pkPrt)NMEif*5Q*PU#x<9dpT=n1f*59QDC_r&$m zD`6|j&w8ON=y(1L-vkwcykKe01b<4%m=Dk1wu;B8p*rSU+DvH6ALb^buvJT&XGl0V zU!QE#shNjXBs6D^`&t4T~*K?kD&qmh>)2yJK@jRU<^8-g!F#8%Wy8XDyhP?~6ssnM#4 zNlk5QEp1YDba)CVF%8ynFoyVoTBotfy=Sl0Z*#qqI_b~;*t5>vXYX_N+4tnyMag2AnQy zn>|Z6`obvEYSW($E=9k*1r3&)p@lWl%4U$?(vM{<#_hpzFc zF7JLi{{UtPhPpKUH8KB)3$}}D2wCirQ$~u7lI|3e^c!>jA z@}JI3y;fC*ZnSSZAAo~2&DyULt;1C3ggGzJkL_-7@-$Mf`iOqRb9J@x*F>;0p3mU>e#Kl>&kf>@a^g6G&3?q+J#rF6v=Ywxh27a_0i(OxhO zUFBqF55H*Lf$!1d&iguAQek794}LeldK=jG#rAoc3Qo0|U|1`mo!sl}l%>|2w3vZ+ z25Z)AFmcV6a($9tRF~bYp>6g=X_b0=qDpbb`@W}7I7!Yjnq$J4DJxF!K}74LFrm;XUd0~%@tM%I(oRrPJy1kULqFpoXU_3|Uj z@2+wkrpeIIUa?wLD`ulkiPWPc%}R&n{etXA3kz$?a&xPOGAb7$5#v83s z_9l9QF92KB2VSjy{)V;RewnhR2FS04)F)@H)mV$CaJ3a{ua{7X0n1nkuXKK{zYY$j zJI)luIlSq9(p{(G}5g$HD1`(+)Pe)UVTJI_gt#ubrwWQ#U zS;iN24rp*jJHMi_uE(rr<*t?NPO$s%#kE6WhIYQggX)j2Ick|RMyG+ZEgQSxe(aOq z(G%c{6s=FA zOE)Lz^@xYPNm39e%5wX_mDVE`Y*4xI0`c<^XQ$pypQ=fCb8;z8-Jk>Ns%nsDgnJTF?^JBNYPBvs?qg|olV~Y zl4&6=N1UpsfaCtVeo*r0HTphQ>>dvAspY};dyb;O{r3ql!Y8>!Em2P+-rZhA%ZsIH z)@_wYYp~aQK_yBqVlSq`d*u|kLQioypQH%aR`{AUaFinJ4Z0vZrB}a&D3FU(4dqC! zWKlI`0V92uqdAvrrIRYbidZe9IFqui7HI2xu!lGgByp+g!)hGIdB7PKvKy?Ar?8&v zMJ(*GU=3V{H+rqS4`#nFlVN%50q%Gj5C}y;w5`NU-X-sHh{W*udn|Ehl|iv@rFk!C(dq=z-Iium+VE}?jQUp_)F>3MPmjCQ#)8Xi}5 zx>u)DvYH`2PNYF-OSNv4LHLF^{3zn``c;w)=X3Hknk&QbeuVQp*ijzE=-wsQBtaFc zKhPR6cbS3_8%*(PrCnidmk_$8wyOwQ1>VwsVO1TVIBS3o$t^x4|B$z-h$hP__;dEt zH!U3VPx*|P-mH=NG)m6!NN5ymB!_FcizB%kEYIEG&C9|04)6(gP>pm-70rk2pa_*M zTVPkKQd!WBbV3u~3G`^TjG#^=!%x^wo8cK3;45rXl{jzey^8ydmMmohQ(l76VQ}SZ zFh=gkpD;JVFG>AQl}HVLz_rwk`8kK`pt<}7cT)_!ypLf8D?s#O(@J#uMC#*#scdu2DQo#Utv9MAc%diL>nDFCP4MK#90s2&0`cr5%a zjNC;9GM+DCjxy!)amRerH^~bvin;z}c?j_!_VH5QOSdRex}+Mj!WQn55nu#~0=MR? zv_Q79dB(Hg+BAK;09>5y__{HDZ2UDkVEgNk2l)vWOd2Vt#RC~AV|Y1D-x_J^7->@1kbg;)v`e^3S02g~54BJQ?0_DO@)6*MjG%d# z_l$+?^iZAQs9VD(zYVs>HX0$OP6kyD$#$udxwLtxZab`^=J)*nO6FV(g{f2^E9S!L z-hvd69%<)rN~dtlWb~kxaYIGZ zY2c7v3I_uT3oHx_oWdEE$|<8%UX|5GOKo*qEwR7F49#`|NY?`%Yzdk@0yV zGBQ5?dE%TCjsI4y{`dct$^1Y6<-h;e|2OFU=l?U4$^0At{?q@JvA=)I{2zq;&;QZ; zAOAP0`hVp$=nqs}a_bMw_g7ZnU%ae;XYJAZjpiJ@x`wYVd+)tow;TAKR;$@;wc72D z?{`ISFc^+T!$H5->p?-;M)K2T@Qg-dU4)Fr`e#>4n)pFKXw|)PsZ_}4OSK@JZ#JuW z*lX9TRj=6{$BWf+HW_x?je4UM3@7n?9#6-EuHSC^-C-Eb;&?LV9rSwr!Eij8OvBND zYT~8z!Jrp(nqD!Ry~&lE{b;q>tmosPSuIs+?ZITef_B*Vo3(ng(;vt4#XO!2dywt~ zgYh(qBT?&jyWJk7rxST-fo{N4^K?PG;S~$HY`)wX%{RBZ<)q*C%9Uz^TCX(shCn$8xB*>u$Fv|DYzKbk~w9EGD{Pm~7Za5|lgheo;EwQBc+PNP!H=kmpBXB4lu z+tpO0X^>tREmy1gWDqo5rKd9*WCH0{tJ58XR_U>tAn2Vc-3=&6b*9ptMy;eum)gU4 zz2C2*L90@#RG}O%*J~(u>Rzoub40WG0?OTXv)K-y9M9ruIP3+`>W{*3G6|_Pl=Td} zPug02gGa1W^?a$-pU$`2MLh1*Jje$$!fHh$1Wn##yF21d&LS$Fv^H%LO~N4+@6iV1 zvD(Bny}d_j-t9DNm2#<&y)M*yv)%pUdeW;G%asZZJX>vV7h%6q@w`UR598T#8BYf7 zMx)W`ji@2)mBiKbKfJH)2(iZ-Mu^*ir=-(Qh?des9Pd=`bAILBMDWI;{q7`a;kh4gF5L(HxA2f#3A%9Xc`38q{kZ zjnrv(r_0T9(#YRb{mF7QryEUswMuiidVE|CyB&Wp>i34z#bz<~Yn4WSGNlFSvUHz% zdmP8{m>yVb@*b&di+FRIeN6+yZqVmBnKPIbaA*eM zc{Et{e3}}PiNq!mjb+Uw^Z;G1tGNIrrQhjFZq8%BmfCV8u4AZkh(!ATnD)0Fs_F6K z4Zreh3gS{cKZ#2oOXw2wo8)roTym;$r25;Avm(i&`YXMhr+lusAF4(I_s^wIk|()S zpKD=S$0({vEw%YyHJ+YAZ^Q~crb@jDIYpi)vpT9;Mvo^+YOgddY4RABNx8YsXyH@t zE|dL3yp_hw#2C+VP|%gRcr8T}-yMI=88dR@&VA_+Xjodx_P z(<$B02)>FvmUkYSz77|e_>ONWsnaiS=Olg^`*(-@LuCJ<%zt;4{{v+FN%#E!<*9z> z1^rI68;>@^1Ss8=j5c*xciH4_NYX%K$mL+9<|Xn49oB5G3UDk}8~%XhbG@1l{DxPr zw}30N`En7B`z(#1mmZL7W@sbmbbP^=DJ$5Nm2|*@Ga9qVPA9@+05bgp;(EoakMAz? z&GGu->3KIBGzG`1?csd0zuV5kpk6JPYwbP@k9YH4qY4yd)t{`k z``ZPew_a-i5rL+=+s$mm3eOTe0aq;M(U@f&kWvSS7cMW}XT9<5w=Ykde!X1CmrAwH zXmx*ozZv=Ua^a@uRvYVB^k-0rsP zSq!@9w3=-{2>e#Hcyq}&@kQ|Ia5&s|3zr{q`FzFe@9^JE>epAE}&b+_Nn2CZ`03zpmGyNBf(OzO95?cNj+*9NMV!GZ6foT&2i0wCj(D-GSOQ z?)jZ&D;ULmZ_N7De6i7wmkSVHqv6%+{%{&ay(W0R6GoF>Prcu3^d|8l=Fu9wI#%88 zU_2glgwnc0AX<|VLp=q$2crzphxjrQX*I`mar@>5L@;>6J`s z@|rUZKldhS&f{^PQYz=kcXYqPA*XnAS$g*fKg7K9x`(wER`@AH+6+l**CyskVv-1J zHOqI*nNANZzk|A?a&5-OsD?3nJ%{Z|*;*1&-tL6}j|< zydFo#P?z@9HDAA}UrlD&l$EiZylsd;+06rWt`U;GQP((NH(oC!`h8u7hb{0Ju#o zJAM~dR~I}3&qHg9VMf(s1X~5}i7vyAq6o}PFQKb2-ToUhiq^*ro0grop>%jHUZr2yt9&};#(XAy;Pk673!D}+1Nsur$3 zycc}9s>Xs3+p<7PxjqQzFNd$6*3InucljDjrRDx^zl(ddJg9QKT<>mon?)ox$TVEe zR?EeFBD__|!xp^}8p(Vdi{9Ne&dYpt*qs~>-wwM5k@-r06|Eok4|nrH^)g$ij^@kV z4w$!|4jMqIhv(0a+s!EK_uI7^yuO-OzPU1*nLsqZtB#q=0uVsnmkyDaS${oWzdSuX zufyuc%bP-P4!7{`VY^XU?1SsUG{ZOo z^tM2FUZc_M4JY&vuTZWu>6ajxT8&SG;b1&!H>;IuYc!8}%U)*`v>IJta@fojT6|+f zey!Yq`&{#2Cr7hxy#!+!R5culeY%PVbOi{C5e5_1?{r~pg|N!%@Fjag^$>WCaIU+I zzaG3Xx(_a75$FMul!}e7Ro+)A6EfYdC2{xdGtrr-bIR@#Sb} z;6jc+#6GDShU7Pi(i(ks=@FHiyK@{DMN6c-IE+Mk zw=fGiMmgW{mx{Vto5LoLA45oMk(P(V$rCFZ#hs#lNz)<~Orm3~9H;uddIp!~7$ov( zkeo*27?wnFI}1uPoFdO+Po*mQCrQt8{kqfD;@9K<_L#IL>HQUjp9a24@z;3%>UzBH zFzGY?82Y0>P4}z!A3EZ%rhAL{y%tD2#M$UO&*BD~-p?-**h?dOGApE$gWX;5@huT% z?qr@h;;tmz>X0Ocs=P|VyqUNZrt)1z3pXe9n9<2;ganwF`I+8zv(-W<$+qKIdn&Mk zYW0@y_xd3_YiuO346$DW$^sirLP}t_P-ux=9__rbzZb(mo3jHd?^wH^<6f&$EEJ0X z9Ie6^`Sybr?YTTRweIS6_4)9$oRr^X-d9J{&C_mqdmD|~YzEfD!`swlMX&r?ijP1kYZZ-09AKsJo1JFYBGNpy+tpyyi_3muHeim=2inlUZ?4E7` zaI0~%T*@~lTi9|hFVEi}AJ}XiEjOEZ(9T2VIw2);1sFhwLuk(W_+hm=9KP;HfTefA zWbyK}S+BSAZq3UV{e?A+0NQ34wx@&Htjp$Q?&9jBGP~>DQq;c6bJc9!eqI4<9_PY( z_1WzC+rw_V+YH!r%ry=0khaz=Ty_hiMsGCgds(BBe=k_Y)f|Z0;j@}@`I$y+)WhB3 zE^gIZtCz#4sFLr%&U#t)c9yON!izqAdk8D##;_$Cg?PW3K=eJlBoV1^=gY8^`H;;8 z;|ZINLAlc(OxF+5ER2TTIlIzN59@I~UmAQ7#r3$=V}n}*`UqMvAVLh$&HDm zZYn$lx}w9Y>%zIk!-oNEM>eM!40LIIQyALOU@$ynBPAJ#HflA(3}tDQaZ!d1DBFUx zVeLl0jeQ$oWDrq(gK&qeLT$8ZGEjjKQIH%ii5ym=)M2~ZM#c?kOX?~>W{r3aeO+WT zhwgGH5qHB)I2@4bn`8oMNs`7`TIe_&nbNcDHl--BJ^PWc(yHXsJ#i_8%X3zPG_BHG z0=qj`O2*ZH)wPY#HzJ96ti_H2I7COc~#IL(T@VEF?+eQzom0@=$`@`(k;r4uKqp$A-Ma`TeH{ z=sm83^3BJa&f@-lzrDSMg>k={&6bP(?fr5b)XIe-8+1wKGVhEWD>-l`KY5wm;lC5O z$xJs7UtVrk%iC24aie^1{rJ3jejKqg_U-PeJF3sN!*11Mo2f7MQF7UC*HRv@-g|j4 zoY0qd@bLNj0cOwb?Kpp(%k?&&zdhWozN+*^?_;Jp-VG268jk0_F_j7hgC>SYR?2_n zHwN2VC7;M!muo-km6y@rCR6KeA@0d2X*8O3$%FFalrtD zhr<{u6|Ws@E0rxp5se-X4})N}dyIFr5Y+d$syD9Nv;D(-KJM4bAKv8#cMluC`tE(Z zJ87^jHXd~$F$hO~>H4NI*zDo}p)8;X8(?e&ju1AhBBd4%+c%X`voo54o1>xMge^D* z3lF<&@4`Jncu#$F)Pv0yfvaIk_KnFy=Vnj1E?9x&6Z>N@hFGS3ScmMo(cvW-<@g~u zH6kot(sUpME%uB^?)uTV6xI-qK>o{dO*K?BTHMY3Iv!9GVuML3#6#63Lgq4u11fIV zYZw!G9RC{Ls(|jN>BfPKW0V`Y`n5E6Pk@M?)bV&+R7%IK-7e)T(#KU@#LiNyO2_2q zai>&?>vS^;$BcT0BLzKFa{a`03Q7~XyK9zX&SPDV4%bd=Lo9mYq=X}c(d_5SvfB?n7vp#@DXvMw+&!7*kgm`a=es1*BV)NCs8CLalSfXUV6%}pi#bolxl zH?H0ns`X|+Uf(_5&%0i!P-zWd0wI(f!}zHq(jn=i6%c1RXFm=>lf`m2k0wLE4IC>= zbSUdkPD_jO;{a4sEf#w5_VL?c-}Q3W*WPF{jy4D%MZx0y_S?36&Bwd> zYB`_9^RQh;La2h+elFpbR7COMv&c)E=H5*#mjmBRH!{-a@(OlBsMV!m7i)%KHE7~k9YGS%fH;yYh< zi-G5r{qaUDpRczeGRsIv0~gU2@T!e|7w+>&f#d; zP)Q0oQCEOs?2beI+J#t{QB0{g^7>T{s^r=ut%C(ARxyHEXK8+!RtfF0#J_MI<9=|6 zxN7DKOKa*9{p9zU+U5I63I*ANxM#vA#~-`W=l^4=kEyJ|kG?m@TMPVe*aFANUD0%HJ!oo-# zPltX3nQ7sa2*o|=MX??Nmc%82VnmP)CY9Uae4x>%#45`2#pc)E-r@uCMKfNzC3EtN@c+LXei_RT^?A z3D&$S0!KHOA1-p04ze&WyJ7o1dpxop(ClUwA~?LMhVP zY6M{vQ$jr3@)Xf@Bn3ZV8g%>u^wO%e8k;r_!ewl0>ZJ~dsa4_cJUxE>w(${Pzicjc z+vQ?6A4<=w;!mc?qtC+G%l>}5+yvv<@^*?$RMbb_ywzw`J+D;Gr$h_X6D|)#8`VnvI5q8htpzmh4?7U9H)0pViSm#n+AFk$+wX24~%+30rCy+r-9rJW`) zl^Urfk!eA0bO!~-dOaV3XGF{lSRo1B#)%3WC=Ey^o~0NR0{L`OrvfPnAW~}~w=nfP zhJ&fLnM<$$hqybq9Khmk@XOHFF))ow2AI4jOlCQ4t0 znksP);LQJegzE`NmpqHbov?&Gpb~I7cY04^6qh=WjRk*}G^qYiQg_ zr>C7cRC2))Y^#Sd+hk#Ojl34yH>Tm zc-ig8t!fkGjSiq6EoWjf2FN>$xflY@1;nSsq27{1^?J@G8WbClMJcr*YAIf@>!}A>9h>)!UAW7p2f&{ zj7h@(irX*I$zM_GTN!UH@YVwV`?kQ5>qQeBC-+}z4q+;rOn(4Zznm>aDY@Jl!|I|d zo#PBbIR&*&2lXNu!ZEx77eZE#;5M-gq9_E^7?~9%(;)&lz^IZSsTAOdv^^TPJ7b%L zaH|BISUiv_5!gW@yIhlDEVwzQ$KSw|l~`a1rC@(YAOzJkvt1=N1;9=0Fz}7-{Fvnf zHWTGd9>6$lm}o*t43h`Navh@t(Q3Oyubho9vAV?eGNV5&4El&qCuW*#Dsp^R!g#*Hu$mA}5qVP@c_GC>mhOH_XM zArDTQ#*5pB{UWI3b5*d-Tu2PvfA-n@0S^_it2&LwaL4A8Wq%&;*70I8p}{~ng23pt zH~3$p4A?+krBcdgv#^^omx65fMO?vTINQYV;~v_`jaTFSEgUw~_UFS^t2ak`e+8)Ya06j4S)okpc9EOCDVJP7#={K6-kkBH1NYF`~rzC}90myo>5*xD4 zN7oSh$@N6KsJea9F%_!o?4~i1#^Q46ZJbz2M>269LvV;hGN6vJDJ>#g*y(No$)F?d zZ1`!oy$VRYzZ`ODCIe2&VvaO4xhGKQERE>%XXX5m_}rV;!R`r_^Z3+#jQV*6t*B#~ zG}Axze%{*u)QY?n{?-C-E$|Pvz&V`xEmA)q7)_RTxjZN4k6ZOmzsc;Zw0x;22*y6_ zVE{?T2G)!%NyY$T4da7eOOLodEDGeZ<;Qf<5;UX*MwgM+%3v-xEg|Y+Pvk0Rd;^v` zSQbb@VKP7#6=%-6C1#rpE@2i@tOjk!$VPyPHnIH4hM-tE$Utc;1?EjEUZoP5)rFXZ z#CRk)MRhiLU>UXgAqz0=1@$HroLHsXoqi0a8T*Z<&x2Ak;5O4`vh|(v#T3(mPhn!c=%uJ)_Liux!qqVPQG1AMCXsr3d^NwTJ`s8$X`>ic z;6?EQh)7pK^%A*XFLI>#Lzie9f#dDuwQ^|*yH1IYMW;1L?|-Pkw?uC(@YVunE#M~C zvw%P3d_BHxMo>PR1Qh3HgCzXbEuAME1^v45o76+&-Tu1>HyfDyxG3PWOX(z&!F8N8 z2qQG1sJnfCSQ64OQ)~WG3WBH!@lZ%TNj9xT`mDC>RFJ#*ET0L=iiUg=2$2wFpFbwe zPg}=bEL0ynL#V9aYWm`uTr2^V!QR9U}6PO{K`VD)~7<9=-b+j7ty~^^1 zL)Ft#Ohy?!q0;Pl2qN%clq#Y;x84s6RG`PCT(tm;mDY1UczGZ`g!Rs_a)=T-uLX7j z*(JWLb=XH^hKHuP)x6yLZ| z7;Xq+MOc6d0$!Nk!T@n!wjoK>Y-3}u-GNlUA0oLk=pizKMP#(k*wj)HYDILJG3m&Y zH=2!B+w3fNI)tJ0Jiv0E*cI5BUPps)EK4&8iHyfRS^Ms{yhf|V&Xm{cVoioN_x%B; z_a@Oel1MR3k9dT=nQ(}8M)r-cS2^q=>4c4DjO{g9(Ay0Zp~k~8Hn}MRAr@Fjut6p? z8f_tSdq9qk-)>=pvPmXbKtPXPZ-nMpG?{IX*onsRGPZ{yQy0CYE*y-cT3!p$7pjF` z(s@4(stDFMY8cOxK_e^^6lGl$ z1LL($3lj6{pqWd&HcmOt@QT3Jw5LCyBQL17=6TwH+DR)KVIhYx$JeW zTrL*NRk(PA5yJSSdp0u$kN5jq>{R-#&$7nj#JR~Hx8C5czH=n&J{a>EYe zSCiF3sSatkX89&pZLytL#)y6;`|-nd9zIy9uDL{pRl%&7hgxCr;pywAuirmEZ87Ru z&6R4^s$Nr@-ARm!7HM7jD)luKJY?r}!Uu#gKbdyzV?r~UrczBoJg;U%)Ei`*g5BEz z-H%yE6ATj>0}>9P#xt0{1xRM%FbGe)7@N$6Os;Z4McmI&B7@7tWFRIRZ%{_cHBHM@ z6bv|-5|cK+P1j64$)%Z#KQ0&G+$X=LHj@0#u-xUgts+Q3i0u?$pGqPYDcyB5a6atX2*D%%X^5GSTQ@0b>jE zT6GwLv`TEhmob!=g$>Mt)gsJq(J_KX;pXc7btm3EK4X7&HSJdEdvxrGG4OD|nT=t2 z_q7#|?brQUbMGRY*>gnOrV&o}7X(m%2 zu!SGP+D#_kiSKMUe46*_O zU7^FQ9hQo>wC>&aI9jhZ4CE>z4AqNl*;$@xTPBexy{`-#*ugt|G9xiqdRr{E`|Zm^ z&1-uy_JS?uFdR&S3Ub11HhPw_JQ4=Npyb z6C_rv?y%dR$mDXp(&;yfxf|;M?nwsFz!33d&{KoXWtF8h7;U26e)#ajdOto8zK+XY zz0Bb9>)6ljw##~PT-EeJGPO+YVPvmvQ2&^D7x}mM_XpabkL9>$C_K%WZA&O1cM(JG z&c;m^A?*i;=|=i_OjH9S7E1AEC#Jwr-K0Vu$0AEvv5g(UE*F>s%eO> zfa{pZM(D~34~EQw0Ap060Gz{qn>QiM3o07)JEmgA4jlX?prCLlQz*a^C~uz*EEmj- zfK7w^Osh;&z*Bub5bqK(vug4stGV>UpFTRYLMV<%F77T^`B@;Tj=osjj*YOhty6dj zN|3%HDGD+oNq(xrEUXHt5Yov7rlyWsjm$+Mu2?FhWx^7(80aVs7Cu6?4(gZsgK23W zc`ZoPaajy2u!@lfD)t;MyVeywJthqMRL^{v;<1`g3?!6Cv?*VnvDK0iol~DCybf4w zQky4C52n^6lp9|iY8G%mkpvhj;F(+vRTXPso`&QKW+~!}M9rhu#0`hV=!;{(0WyeK z^#S3uxbTF?(n5T)saQo@o9F{cMUmIOSon}2oV0`~K)qu~4P7JRRgBWo0l1hFD*b#Np_+9k7F8ZW9= zsbjP`T(K^tB{&ASO1h>KWh>hePtv5tSgd|5k=4-oe)v@le(Q2Me@t}@{R!by+LP}pig@-TXasA$x6`cnK8huNK$H= z;Zs20hLW97g`_}(B6X#dlR)L<>)m0s%w=?NR1+g`4`1&lVeJw_{q4?p9WmMyb!INHpf~2KMQhz_n)}sY=#ZUTybvJP7c<_X}R! z?{DVOAYZFq`NM!O{PlRb8ph!y9>VuWyE&djJP~Jb75TQj`G5i5-68&ZlPO=^G=sq| z{Bn4D*j*Q{t~R~#7rqB~Pjhe3tW2isyBS}2_v`g0T*b4^YKIhrBrHbSE`cTC0i-h# ztNqL8dZtnaCq$ds=flG;ESIzOMYyw=akO2s`Hk|Gep;bbZsA zE_eGiXM1$(Uf^Th8C~?Kn5zup`F1%05BQCCjXJ{#DkdKcLVrMXEG&Di+wJ!2;d#=l zU6*dZe_c=NnM>Kv17YsM#!Y#!`SkU12rB7DvvIr5XR^=93!Twqjt*5eTLiUp_DImo ze&7&|YJa*x$fRGo#EkLke!CiXa@Xb7?Cvwii}gLPA8ziq+gXH~QmrxCa>z)loaK~= zN)=XCHFtI6kC&TiaFKbR$07|Jpf2pHun+&G#{mv}Ov9_|!+q*t#CRGnBfnB;cBgOw z(TAG^?P@1nMjU`5v&IYxElC5-))i zz81CCXo<3=MfgC=l~!<#Zk9nV8_~E(s!*+l3QA`hMtd?-*o_%PJ6B8`TEaAcxM@Pk z-GyI^pu!2zN@_QYgt9A7i?M?Fg@&!8c);1LlB7_5`Cw(yr-GWNs5<;c)?gk>U%8BH ztdfHW`6PyrWU@dLphOT1Njid7CZ48kZHmC`3jB_VK8#G5}CGyF;bJGzDbP$~vbF7;E`-<0WB|2nG=} zT|vM`VLEPNiAp+&=YvUyY!9hOVN= zQ$Nv*r=J)w7d;Z{ZPCB<{3KAvaCZt+TN5FS1=h0nsg6qHyAtiqvU-qEikNCFhcbS`ouPf#CO-3z26>lxt^(-hQ}GOaRdMjo2VQMhm-6x3FhKz>yYuigw4 zxx0~ZywkK6ue;)3-PJkI(~38+!-YrMafV}Za;WmNB+aUzl}Oo z{D-b|7Wge9YKtG*;8<)@{O{HxDaU3P*V>e5t!yQ~ihf-ey$&NU%yRVTy5{3Vv8GTty<8wF?tXnMb3JC3CIc#6x znpBEq-;c*nho?o9%U@?MSqetZ9NdpASEQpkFkS#zj%x|Aocn&O>8L!rR#^9W;$jf}}gxCN?IyPwJFHfd}6+0qp zsgemtj*)l}o3Xd=D2i(|4@zO<-g$PzY-zlvxwd0Y0xK6$K<)s2I(5T78J_d zz}5`xnsK*u{qg;Wt4a`t5>9Dw^ekH(-JlN3zUleh&8NesZ9JN7AJ)-uwBBqN-MWsN z*kU>FrqqC6Q->=wu3l9Gohp_8P#JQxR_o@&WoP~H5H)Y!-82^<5)K5TG#Kwl>$EF{ zqCe%}wk~+56d(W`4;$!gv7oX$GvK&fq3}t3y zecx-U)uheL$rS5`w#I8ZQEMhw89IDwc$bBUG~ORt2->o}trUrE7)XztN0XsPhnR0N-nm>=Slp39QD z3CmG%S&n6EMs>L@I;CyFL?k4vc3Oi(kKbgXbg9q0NjmpZ%ws^%mM<5l#A>+IcXoHA zRe~$`6h*+RQqvft;)sjp8P#*Qdo8DExze2GPJgUosmE3G=)>_k`gP3fxKoLiit47B zt>&WT!p?8UP?siY#TGOC%U&6Zy9TZ# zM5=UJ9{4bjUNN7@4NnJ7-eilVR7EaZ^aNmn>D_gug_;8#&++g zelhdDfr+^F7M{y&+^HkK3O@^oP z=Lk4?Ggs+IC(xS7ZT%S_v7tTPq-mnv0&J_aiQoG+Q6kA=tUa3?H*>`T!&cQ86WvT;_)%6|A zg4YhhJ?wT4ojq^5Uad1(BEzu5ejvMV#aahO<9yJ{SI50!v(~JmX0?9`iyXm96ry3_ zd>dA-ud}`Ba#YS^zj!teht*QKeEs3N1-~-%uijtOLj+R*he+S};}yrIPI^^@V-U{h zc;#F@7)@aqG_K3AAv&Bwn=cgmvvDJbV1tJa6S7M5=+Z@Xz_IK+A{+pAty}RXMokJ+ zgKV_%9S}B)70zg1hK=lHu@hmik<%v-VL^-wGsQC5*hDFw&);sqevfvW=@VBMfT@@d!{cinZOMPii<8;(;ih(F8T^l_ZzOH_61#;AS&{jqB8>NEfVe zsgWoMm8_3$&Pax+K`V_Fb0oJUP)X9!oVYEoLX9ecuAHP46`?}xQV#PbcNgjkx9Q0x zuxZmxWJ)2gMy~!_RN_u@|3K4`IMzW9mHF!T=og73LF#lhl#k2h;`TGIGA6WLQanjY zYXi%aBsiv3-9=YFawWg!bpG==m*kYnWj-ZN%Tvg)Xp27POv8T&CHR!Y;{N)de|iP} z`bT~%@vm)x-|b?*ISg!wX!d!P%K2fCFjY@oUY5} zH#d7Q|AVStU51fThL^VQoRAs9r`o?ma2XytGck7&!qOVK<>L}HmND&wYgurMi@v8> zQBlB|3Vp#{g$ZSJ$19B|Kpe5lz-lbDmOFv(J>RK4-TQ9;oEm!9*BqaXl2U~E;^`>Ry3P?sVz_Vu#A*a@E*!I2 z1}qetnU$BF1VL9s);Pi4t5?c5hy%U*d_dwU$Xs2Os@3tVJv}__cEjS;MPty3w^7K^ zqwKZS%gwKOTh>}(%{;4GYQ>}S1lATK3*dAHWSYI3eV@V zQ0i`G{?p-ZwW@Nu70tT4n{XILOx-F~A2fWoL9Fhuo=UO?sor_mZ?$Qdo*l`g7u^kz z`{6>u{KT|FO)*?A<<^&Udr0>-RkYw(pOv z4>#4(7`-T@4twQnzTAy=>p`=WuQZ3~MYE|`@miCJ_rXa~4bE#tA-vr*xjf_9J?O}+ zekqJQ^7h18>psmms=VFXqP>lf(qbLeE5)k6xSfx>^=cpSlF<|yW9%ohaWaZ~_0|Y3 zWM4AiLD(M!Y?iHuNH({JQ8eOs2=vQqg>tai--q?99J`9sKD^A~YBQ-9%Y)fM$AwHE~P(tZzTMX>ior)3`v@n+r#TO&X$gr?!=vri$JDz@G_@ND_lL;O^! zH6++CXf-6_lzwe0NN+Y_SfB+vuf{UhtHc^k3UHUjrJmP!m(iw!*AeDR3QuE1 zH6g2;E5s?9~st2(=Y2xIb()r2#S5r{PZ&d#$l-`2gTHvh({)4wbI$fmq(`={7 z!p%OvJ#8d2=2<(Xlh%3A$^Da9NSt4mmKpO5&Jk^JPT)qoG2?AHG>6;mYf#yQ3Y+C% zIkD9evo*jK?WgGLKbh}YykL?MVBho9$yOi5y9GvT5KIJ-Hq8qGIKe-mxKn{iiLN;K zcECE66!bl|6>qL{2zgaN zDThNob9n_DX*z4I4$nKpl`cMX`kiPEI^uM8F{&EFINl+$efz!NZ6Ogq6#$-vLmy$b zhBT-onamc}CkK3?xktvP3!{zP6-Qm+dtSyjj z`g9L0Y@ydP4JU{WRcfLXL&(GV(Ml+wkx<=3m^m@1*S3VZ{+bv>K*`hCdkw~*}s%N!{Il?iz?I1BY zbKu9(NGE(YIw6W6sE)K{pt^^h!7(SMwZ)d2OxVD#WV6){Er9Gf+&mgun}6Lw7v09m zY`fWje-TxYsUz?&;KzsQ%Fdxyf4V%@57AL`Z8nqzpWYC_k1|~wpvGyoNKnDEL%xrE z5_z!>Dnml;0FaGHAH)czD~Sz;G%n)KBfc0w?`%$*WhTrg$x4%q4cMk2J>D2*>U7Ft zJ8m{2ZF~`?5I53rYP8_7Pc4lldNWgm%JJuhmmJB1m{U>mTWN;Dl6W@< zO3qy4ow8J0uB29Oig0g8m9rK~>XXz!RUyQ6bt_X(0v%P|XxWdIe)5`&yZb2i z&2`pXE|;}5RNbXHPN%Bq(XCC~3(#w`GAT+8PxH8fcv1t+E;cDQO?j+_SflESR#DAQ zc9BetW$l?{Io^_Ili!@FpL_D!jyKm0OzaL3NH5XD6VN1qs${L^qjjrQ>9n4Wy=dRpOk04a;nkpQc!& zl&mb|gb^Y|A6$~1GdHulW>T#WLURkYOx5oSZroUlU%v&WY&-*@c!Hk(Br zr9XeTUqu5iU-iKt6GYnPt8uT}0J*XNZSOvPyL}0IUcKL*PrXMk|B$b+G^Gc!6=yCKRwLHK{i|O4TIrKwxU-9zw5Po-Ka0d*~8-ixT+PZ(dka- zlX0h3suJs0(I%)OZB9GP|EdbxM@S#vG z#?ObZU+;%(gqMQoaSx*Wx|qUk3l^M=f<*<)3j~#`kM(KP&7;*n3)l0ok+1n*Oy)&<$aimT?8u8^%yVa*kovS_(TagpM_4zZsr6b zN6uNHZyA)b&mHv=eK!CP#f)Pi<~u{$DC)*EyKGEwvG!@~Xdg6n)3xd27Nn7pq#^d` zb7`%^C!IcV+e&KgAJ$Z|5}ZW4IE^C_J)^3Kq*8LEBiDlTqx8w$-T2k8PHu9#JkFJR zozGG#K8bvl=`1{nPSU9yh3M|!)Tv%B&|(~Nw5GClbk+Q2ZIw8A1UW?^O(cKnJE!cz z-EUHlQ$^AMMW>mpw#SIH?ng3dvSYxxD0nf5&2oBOQ={#~}f*dzq4W#u_% zI$9lUAwrI$C#l$J3N=?U^Bx~SAEJU4Qp?t7m!>V}uxUxt7IQkK8jc~GeZsrB*v-2~ zcQOr=5{{H~Zm@m9W+Q8k01bp}r$+NPU;S1r1@lSD1)iY-`mcKQG0THMnOlIQ<>Xjz zjy=HCFZ`8EuaxN)4$Y689ftfC=7&I7@LnVdC!sS%C66Q(c-U236a_$mA%d-k(qOi| z-On*RfZ!WjR(*uG<}qihY4PN7P)Lx8L;*VCIhb=;7J~04lSWgq%hB2T4jf9~k zth@lkP_|@uvQsN!OQQLB|LN;}r<`kba_uggjJFT#c+x~=F+eYhbGp_G7(uYV@o zx4YY2JOOpV3dC?h#K~URQ9v9GJ{y)>P}_lhRI8UOxh$##8+IZO^LJMtTh+_Kur_)A z^m#D}F0UIrBwN0-^<5kc5mj$tv|%>cK7BuImna^#r@SWw08uw=aat_Gd_V!zOS^5P z^So-US}963Aa778pTX40Re2ith~9r%H;M3D-3eBs?!Vke-A1L}W0O{x?aMy6$$A5J z7h_aaF^g2Kc>V}mE0U2%&Ry(GIFmZSBHu z_o@~6d9#?-!K?vlVeLLLh07HuimH5ywP77Az~Y;qp4GhFU6&9tZ|Vz z@ySUz9o+#G=XX8S`k@;`NpLz6lR&&_M4Im>_fshkr+ItMt+FhqC=z zqCbbqPpNCRJBxVbISbd@c)Q+O;H?GTTHv4D0yY!5iTP|&N+;@{R4&P&6v<@g5+`Ub zA$3OUCo~jfjH16~B78lyzM^_uUydtHvceIOX5%in$R(GXnQ_}%NQ}^;w3xM*B;dqW zg)MV%x8QA=H(lf?T1b=iR|rj86j1A;4bF0{2z-ztC=vM4!`hmu#a2X;%30AV(eyrM z6LUP(9t)NN#KluZC@-NPN<5RE!l+TMHcKS;<+HuX#wHk5a0QVkj!R|33SDTbW9F*+ zu)un-zyKs5Q>uuPIo5V~II9(HL5;`D#dbNH2E#xmBr#SHV+)QbZ59JD^mOdyjs`vl zwzb++10q4sv^*MY;B-`k$$J#GYz^_Z_kRT{S z|McV5W}FBR=}C*hvHqW*nLxQ;d6tnAwyDEVU3H5`Byb7M|zB7@@H7 zlpRr^uLEJ?a>8*XyG%%9m%LH+YOSVMgq7F3LmPA7%4Ms?deEqK7I)7t`-zvUR$9Qj z{&=-S!?cen@MdoSlXNBX8DB>AkJ;J)8{jd5d$Ta`P}k^VcEqoHNVu6H6%Z+Q);SW+ zR6XEq1syn_`45?Q7hVYW@bEP7vS{}XM$v4g(^yaoSneMmIEc5^VKWg$VYp|o@US4t zt~Afe#;+`AA^+#VBYGGz8D%@*_ExoBDDckfGqeouloo*PV-ujn0*OM8dcS(XxW)B# zU5fWZjOkj+X{>2pr@c43yB;TJ(ofad&VgO(bDhrhyfgPNoa#bcB9YwAsaZd90aoO( zcsU49{71(rUfuJLUVls4KT7j0-CGO1wZK~oytTmJ-2&mqGkI>ssN zl_yQ~<6ZTW#G2h5$4LR+}Wxk@|_XB8U&6q*C^A(zO(f_ehI z8``AKK|%3!?$3gVGk`Eo0vJYl7dc6xJW@hWoJlJ&oU!wco{0?qv0DpARY&niCqe4I z8W(C&(yH0DJ4=T!1p*9pQX(u`^#gTB0eK9mv40N#lg)YZ>ZlexvLZ@WZG2CaoB9$H zVjGkjjp@OYp*a0plESlyQ(`*$P(HD9va_lV^HD=iHTZ=o}|!<(wre zSU6jbINLaYOej6r&-u=G&b?lQSD_!l;_QgmgOLWN2AHMNZ#>dQ6=hV6X zzx_bs|MQ_JvVuca{?Ez&ukU_bPN%H@H%~it!&Cp;<1ckRn}o{$-ws@tBvBLvLEr_R z4x9kQ3fqQgNMrT`- zk~nX5-uquKrW7-2=_O9SGZ|B{&mPWWRAiKw($fRZH{Lx{H7A9WTBf(7YzhqyToIi* za~wzY7V*4OHk9gY-ZDLa?C={6#IZP#*g1F}>FK~boECF_AxFv*g`r2C`cqc;fx}74 z)61xp(cbt#N>Sudd@8-F8Fn(A$!1cvq2sMizaR}#iO2#ipriT<RG&(LGV78ea2&(2 z0yP6w5iiFH9>ZXxGR-uAsOW}a(t8a}5d{wKKnlotVq0od{ILyn=e%=l~OKco2Fq{ zNjsIzXOf!CqgMo8l2lb!ku4fanCUjnPG`{o=}fNNKTyf1ZIw$zLIGdEAMp9ZF;0+m z(=;{Ru<$NTj0gQbuh$m{M-mM6G=oo`6SxGEKvU>eCS53$`YVOBsiCiQ(=tUSkQz{LV zvPo0H(3Y)SCYiEQ8M`z%v2EYZz5Dj>*)lybIxsR+9>^FPT2>X*WGb04vgPvF=1r5+ zBSV$`Vxdwj6?554-p*$WgXLT?W1~wlww6ZIrx+;~jE2H-o>!BmEF}_R3=OJc#G8hm zR17O^=WX4>Nav!Vh&Sl-g#zJNB!V%c+FB-;&lk~%RvIbf1QSQQu{0PM27^^rc#LI3 z(G$Pa2W5C^rWFEAG!n$vjl>lrmBX+u z3{(cnrEJQwl1aldO_gL1mk4`%T}=&jYu42^)YorVv%aCRt+k`0v!}1OyQ{0qbXCWatJuBOV6A?RV-h5*VJN~JvVO(k_*N<=)aw&uphrj{OG2;IX#V8ucq zzc1+axcxM$>`b9t%vh$blcVn3It}z~>DFNgP4iM8c3toS>LiDpwpB8X6iv zQlu?c%K2Q{!tCawp@65SyQ`zq)$R8A{g4(Iln`D#y2a502@<-JqFd=~E{lWO&?N1L zVc-Zk67Uf9kX!-@=OhMK0TYFUw62m$;8`?fIEvyx{zbyUKoBD}0m;KbPLn1uEko5H zpCDXiypu^VC^T;2P@!-Q0}YZ*r<18PWIT${zrIWT{LN*z?0G*`2b6zWx`kguOTBs`y zqaRvPltq*kWg~?N#f6E-2~Zvhh9(JzImojp3xEV#E168C@GpZS4e3!V6$*uX0S$xy zNE1?9LibU>psf>_yB>6|yQizOv%PKO+Eq)ItlZEmX8T9Rr?+exE+#Qtq1|*VR~{T1 zs35mo%1|U3Dg(o^P^t_J4i68H3=KkV&@jNiTsECFFyx{kue+Ncidsece6-iH2XFkvHcbeRGi z1)U91k4$kur9u>x`}@nqEDA31iLl@6argCfb+vW1wKvwUsa?K!(fnnrJLB1bamgJ?Jw3r85iRvA)79DyN>P%h+myQ7>0(WZ!;%*Hs;E@zWT-AwB; z6BRPWO5RjWs7WTlqlKZf*jSw9X=q~D$(Vc$r&-Xzxk4sYFgRH>Q?{-E zo(WN(KcNAzNi3(zy2OzRNh(4#4rFF%=vZhpCK3zzLYR>;QkG1Nmj#td1pyi-02>?Svp^AniH0L2)OdVp`#(aTlNm;nA zsG_K8nBK@On>K+7Rfsy8mL#Sz6igEjseoasm4YzDyfLBZX_f;S>42rO1EZm+1uhbZ zgd;JC6MTh&Ve~+EZ!p55hXBZu77fsJzDN@{3%QR$iLwg7f+7b*MnHf@F$52!4ge_L zKxh!>cmoD73jsrrm5@GEJhTfXv@|>pAc_222MoB!T^C@K$8RS(H%$AIJKsMOzl7;2Fj9Jj}&qQM+~Kc z?!a}Fvu_(KY$JN6Qj*Byl7-#yg);uV@Pk3I>bSiBWxt}BGqID z7hu9cF*{=yZ-UlyfQcg-N%cCzm?}-hr9ZNppwt{I6TfuiPNRbAfg+&EP$^tMdde4< zV6T$(i&n;U=oVlc8YHBk8H{Ku7(^Y$B}P743R)UhSeS=ob&`ak4j{Nea{@Jjct8#i z6?g@v7-W(ND22IS%3g>g;p}r2OzWjGr~Rx#W)$29qmocjSVg9 zH`F${^udv#A`q#m#Cp0y2{W65r8qJ^kd}dRq2t2|!OEk2Kz*3gnj}YHe?6s9U$bt2{bBl1-J8niXvB@v((eroT`aA00?W z{b7G32D4hoq6ch@O-^8f!I%KWS%B3)HcGZ0Svdf5xm@1T*tpl%>*?<4>Tq?q+uQ2b zuUy4%8Q)e&=PN={YiaK5DHN^2iZwVkQsLb`Z!jLmh_Qf@G7ugyL5zi>tPUjz+_q_C zqL2fEhTOE1#qwY&uSxN!-|zPLAgjD!6MDPr*R5Q?V9u=U&Z8$c4NYs(Sa9vy>iXH83bMwSusAgthq+o%B038#HOUdF;J{fn_)i?M0eNm+_FpJD31Mlcbp6;{k9DLs_6-$Pxk4h(;rTvH=F7lt2hUHxYM8S!pJ~68aJ7 zibO4R9V|7-agKooX4fn;1*0bNGiiJ zBnS!;36c(Z2TLAfo=gl$HPK+`U(r;-j6lrVnxvSNDMT;mA81G@HAeuOI%NT%Xr_qE zwUX(yAwi!LdeW05RUl3c)vz-*ZUcNNDyO4Dl)nYFpRp|vW!Y>wm9=cc7EKNa5TyiD zNvan~O#x~LBS~a{Y9N`DK%Ee?oS^`|K`RKFj0$OnWJx)cSp)J$&PiLA!TLxEG0t)l z4|ORs1Ps_HEJY529C>Jh#LBt~D5u83>ZnKulmvY#vI>K4fd;iL(X_!Xrc_oiB@s0c zSVd-F$IBf4>53w#0tfN}f>GxrRza^pSpdjmN>O^<_> zl46X^IZZ4|yew&gjg}Tfe8`MU_B8{2f+hoJ$%Vy41oD=NGJHa1A_=I)I4deMFdAiJ zAdVSmSs?*XkFgIO2=w$^k(!%&5F4z^Up1lFPOIF`T{Fcj%vbPzZr znRsKsKm3*%fu(lh;*7+h>cl2t#;Zv(EkHiR*eHM)p%Z`upa?-mHlhU623Q^gkZcVT z27^pYr=eOFm=Vkp)c_K}GiZJ)GEog^LjiLc6tikTyJEKJ01Zw?fMj@q4BDMw3J3=t z?GIxSP*dbFG-MbyfJ*}GIS}hW@ZfO?3SnH(TaX7vDw$P8O2AqmYFfnbLm44E2MrkP-OgJb%7y?S(4*rKpbHX&51NE#1(gZ?jhf?a1Oy3~0hOUdXe$gx zswwW{Ak0S1w2=t2Acja5;y$_yz6U%C5D8gER73(Z7h|0!CW;3C1B4~c z;VMXw;R1^TqLwTv*yMoxK>Cn>Gw77X4|FV8Ce#2tlQRd=We&4P?Ta3wwuIaf!O@X@ z2}}fxj}rh5*e!rbCb|Ga16{_DJs68c0{%XCU%(#>db&Cq8XKG1+#$U@wPo{IC1*-( zI1ox`>GJUC*!bvB(UxFWv4T+;*|KZz?(I_}1YvC8lfj|>VpgFByTQLsTVv;-`4HLx!+IoP|Fq z9QS*>`@k3ZePOSPsue6n0RIw(&b4x-!BU!NMlKQ!MuBHQv7$RbokW2+K}9>*%uwK4 zg65DcVDntgv>am{LKAM4OcL}AMhhk;W+TQJeqa(((M7N=U{PRn`UCzD#!fKM*%eP! z#!G3)KrlQ(Z%{B{sDl-RF683D5CbwCrUMR5N2xuI?_@8r-&J z(BfH%3r7RJ5j8(DSV)oeD8zi>m}D191>4}$c40a5u*3bAhR!(XF<2Ou5RG!S z&h@tXQrq_LEU}Tkh-QOGN|kbEqPwA~H%bZzsx{HFc(q?oN%62av2(k!ep!pO^*hH# z^az@r?Ev81e{d+Vp$DoU(CKH=n{s`tT0OzS5ZrX>J;Q#nd!wEoJ~UmN+A*ldV|;32 z*M!_1gcYuZy1mB4(1yAQzv)ogvvjt+b>DPSl`8vp4zgTnS2b+~@YXz4++yM-qVM@byFk{umPvd)nDEbM$4YEzO-AR%4hWtodHs!84lUP)#Y0b>Qy zPb5B3Nx~7wCwLwsm|;~JN}IA(Yze4G4fDKWg8qb_l?@O~fGL>XSu2;|RbG)(dBI2* zjW~dene2y>f!Sh_!bt+QT2M!<80P_F!F?q{5+(|?MUY8|vS|QLfrux@2fwU*#)AIQ z(;6nfrP-LcQ2sQtb%TR{5EgRYE&>!MFb#oJ)TEWw1&cF{WFc=^Kv=pC3n-<7$_D0v z>d*u&-Ji8sUe@fQWI_zY4YOqDEH2BbA)mq|rL2~$Sovs(3$W0&vPzCa3yNM+;<2zx zHgf~;PMb+F=nn^FEmzbEIt$RO1Vz)zroh#TIV+j5Y&D_?W?Il;9)Nb^Sxo}#tulcG ztaGuDDW>4GPAFoSVUztu(_~XXIbw{HQerZ#WTZ?Mpdu>Cg@S1S(8G@8F}O1p0ACc& zk+_yF4HgAVFBYQ#x-$v5i>6HQzKDd!QfDQkC%;Zf;&Xy}_(0+^x%MidXo$iNi0emLuAVK2_dR;Oy#JOaI zl#~boNb$aCL@QTvd9+kO3X7a3r$v+}1sJ0!B=vPGe(z$Ac&45SIr!w{sz)WsMACP$_Kgc7;JD9nSR2$K+*5PH(+5zKwkfB=F7_Xr)r2Ev2zNI-c5{elm3 za3z_+coYu<1@o|*5p+QXCDsi1#DJ^xFp3J0 zNIWZYC5R0B2BjBaKcRpQ{-mM<^`S`g5I*q_&pF>H5r!JI40Jb<|!cc=PDFE@nMT0SqLcn}gO%7C_7E&UBR9OI! zfW$!7#l5_mN!ut<7641plG(v>e-1Py5Khv7cT$EUJ`jb02&5YIwzqb6xLSKU+MC;Y zmoL74&RuJ2o7xVYeEp?;m4LssJJ!(RHFnTaod62yLRq6y#Mg@ zKuR5)Jb3Wnfz4YtPYoCxf+^tRZ>d|nYFX3rCAVKY>xMbkUwx_d);k})FdlNPsAX#J zSk*Ir|0_Rxc_bylqf@`GrG=k5_0YaaXj&oO-P+X2jXd_!?_Yg+YBbNMwSw8d?ZAnB zqeAPlyJpv}sBO5%b!qcew_W?Ci?4d(`M3Y`h|=r5r!R5a{QAhw$6xs6Ni`?-am=dr z)~@WXQx8rJRrsJ8?riRK8U4GrJbLJ{Q&W$)hPusE#dz!bAzi@7`yN3lj8$Qw>#d` z+SL~+P4C}7HL7+K{))>xvlV1BbV4Rs7+? z>Eq8H&k6CYJqa|$o7rMT>sYyR*^V45fT@7pIl{P*2(I0j(8}7ck zPjnlB!1&}FujRM-X05B|)j*Mxwr_i2`>?UPp|JVk?`#{`Fj_qP(D7{nhK15G`&|Ag z3xd$=?dj-jUA%gJ{kIoi__sAZ?>$$o9=)FVyMM6S8duJl-gCwV(5Lty;cpZL6|*&(0A&=JR&eEuXV^MK6r?~sLa(`-iVB5Y-?>c_!fyu0o*)+XVj)2vHCu@ZmY#}bK9oC%^| zvw{{25X4|WjS5L#^G9^hei1>phO$t&q7q4nOgd|4Raiqt4lvM6atYH5q-n8giUnI< zPMZce1Qw_&13LtGBE(txU_r{G6}(I+YJqr;=%P+W8oUKErvi4zcs8s>fT(OMKcw5N z2d0@2jLLc@tFmGQ8D)fs&M10ZjR#Y3BLpScE+Iq{?n(}>E+MJGmPU5~)SGHLYnc*& zwPuUD>d@dY{4*9>O@~vRg+D_>#4QLADV`8{V8|>+JSGex)PMjvi)d9uzot#HYhV!r zp@?8VaS>At6jUym(g`|&ujCMcl2j~$kQ|s)0Wld#%?XYa5{ThKM}x3JkOx9xK(GKy zs>J65Ns(6r2uLQMCm;~m4DcT?lrS|g5rF_5hbaqJvqJ@v-NI>U70wq~CD#DbK@?zG5IKz5fNXIaC=XG_f&%{qxJxjJK-w6~us#WWf`A3A zArcgC1$Re;CLur2GAM-O45ln-;ya-ZK&X%+yx31_!}R zWD6iijHr_gEkLa}gC{ACBTSqZ0Gpyj4u6GL5Eykb09AuF0(_;2G87doCj=R_49*D! zBPI0_BobgEU@3_@ngozXXIae|+>0D2TQmaQC$Z_Y018VEcf!kr#c>aJoP;y^q4`iv zB8Lg>I;VIc0cRA@$&y+GjX)(ptLg;sbqSQW4#5g+Y697!uMqnJF$+$PyzKD3pr$A` z#5{xWs2H3dA=p)jy9oO|J-zKs8$$zA2X~D31DC4NPM1$i4^B~F*gzTVPds51hDJuF zCa0&y1`rerQ2}-k2BebkceOUQv~~2fh8hDKo7Y9OriWM+8t&Rr8_m7#F+ zV8|EoGcI=rD67?rSKW2<)t47fpZ@*fi45aPxEFtWc3XP*nWqnJ9?$0V@Y>a@)-mO6 z+eUVb=EpKo|C+|u=+M}a(>qUW-9MBz!x1hyJh5$LsE`!n?xxkN+FkMPNPBa8ZT+oR z*W9&r>X)w`no>g%wR7q9^ZM8w&px_mvXW1xyp8J@v_=Q_jBVbY9jKU6YvYCutWlUA z9Uht7y?@gboA4!y=`9Ew9Go8M*Mr_Jzb77oK2;cBckTT7vu^*VFQ32Z#H-aa`|Zx! zw$AytUfC^ge*I_P-8!InqrSz>ch@t?GpBZ*EEz^XXj$F1GMXDbw0rmF(upn8AV zD4;kam2#D6$C`ypZoK)cpZZkI&`*C*eQ$er$5-xYTzJpt*KXSNUiCM-!+{0!6Z2iG zRu5Uv{QkhHg3EhPIJ7d-&X4SP@%ZlTaXr<)+t_1Rx$y(r_e^KOCxwIV*7_(jkliBp zt)4UQ(plfQ;BRZ(|Ni~zM-Q&)`}CJP=C8Wsn%!I8uKxMaaLb}EMbCKni zal3sq>aLqE z9;(|iz3=VfdoO(N`+IkF2`WFh_t@sG2c|Q*c%Unoh-S@rue+_aH|ldsxru_=arcUb zfIi%xE@$;n@4`8k-Fb7f+`oPF(Z^qXYC0DQxH{EHCTga$!$YH4p{=>A(`QZZ*)zFg zC@q$TrnhX{Gg)9-*U!CndH=)TduCVLC6_GBKKa=3-J@LVg85(h!up9vo_jFuy6vmq z++b9urzZT%mVax7r`0bGD7np%hNU+x&ffRt4`1c(TA4n1WK!k!y!3+?cC5Xv_`us| z3d^rs&rjvtw=7MLYoR-9YCgTvw|{fLE!BVF^EdbA?)(04-+yImAu#u{u1(*4@#QBf z9xj`i$c{{pSR7(6Bau+M$Hs?j!At_{7~nMx#T0OgqPvg0h2VBzbR%XZBEbJiLK+eVT2CfL1eyk{ z0bILq@c|wxB$*MJgXmNEwH+%S>ei+eGjNZCf}vPPthmwPe5EK6XH5%OHiVqQa{*dR zQ6T8ac?EA!B1SP`aO)%}tQrpg7ztnmULq6f5Q|w;{J7C|ZOg5~U9}3OqrG z*F!3ZX!wPwv?v(V6nyL8;%o#|k_yK98xHI|5F&UbXg7#{ixxBx%K(H2ZzFM38r%p_ zG9Xj1$u-PD3%W)cR|wds)+D}G>E4-M}EhIA*3sy3Im+N zS`5g`ECNMzIE@lO87BCAD@|$^wPLi0ETxL@K>%%Va2qF;qN!y-9THDtXMnQ=@Eq{0 zA;tnBC_ok&FlIpz)L?6&n332F;k79NUVKBzr1?lJY>4ouq4ftZrkIL^oo_Ui#pP|jsxFc`Vvq9z0hw1Rfd3>OtG z2Ac@nVNw}PN(N8mj4-^E&uIvv29bxD53q=-LKalMkJ00@C0ThJ5m3RX#)>I81=F^{ zc;f{A5$T*nG>xL?fpiHfVysD84xkW> zq!JH`F*A=mL5)dS?hlgOg5)uSH1M9T_#@f1#GvPNQx zRluuNv;n|8MIzw>z%(*-E;}$4<)ikZ!VnO3pu2;k(Mi;ds2hMSAUnx#JdX=AAQ{OC z`qP_H6nb)oa;7RnAmU+M#u*(mpVSNvvrA+GwH76$Hgw)XeC^CZ5S&PEx=D#}hVtVR zKPWuHA;rPblVyNk1k}-G7;;WoU=x830MQHgJP3}s7>S2N34hFoRe;_gX8-6Ae9_=^ znJ6o#N+WRQ4>)T*u+pe7fOD+gFktQ>+zZaKkgvNF>x;a7UEYms6Vv7DxOe?+_SlXm zrpMv05LjPheV$~g=1G7zG+0(BvT!}d zV_i*mFPwGb*S}ojditsAzdT&_-@B+~`TWb5@?%fG``!bCYI_~KsA>Ijmf!dK@zXnG zr6cI8_tbY=gGY}XdaS&6a&kgR^~KfHb+vtnuZl$S}X5(@xAIk!T5S@_g(2*c(d=x$6h|IjxJpjTC2NamT&)oBgdyI@y5~N zL&qwcn}_Y;ougwh8NnJ^Gve~F0V^SR{l2#DrVVp%`}*fEbeA35>-JE46W=ZN^$2XL5&Z4sAVsa5Beu z&Ek&jTS{QwGJ3pweO+5f&!-Hgx1+JCuC;B=ihn%sQx6<{yZWv>)Kuf!V66OXB(nFZ zpI2)uy*1k|-0i;Mk<0c?JoRtA&opSxg44&M6Y|pWmo=X_vg+1M+SP=^{`5{`6QWx+gz&bWJwN%j|jnrAN1ph(WKbr#lgcdlT`($;s1?erHp-_U2`c`ov&P9?6Xu%!0e?+O)Bw z{o|mRvRmwm)i3I;cil3tsmJEyp^-h0KRDWc^Rl*)Z70XerQEU84@Iy1<{jdFukTJQ zUSU1`%H%cY)_f)U%)3wa)tp=Nsg`Fx`t#&xYd#VA@DEQctvRRuPybOp^oeuM%T+(D zK62TYUDGc=Z_K)ddGI%nw#}KfXhYHckC!YyR;~Uzbm=)?o_%0~~ln|yHoxccC8q5au)jxc9<0orsKFR*9 zTK(v^KY84~=Yr4Nlm2D(gD1+%?`_N8_skPhk)(3ZjjI>lkQf->mh{cLw`uVL#t`=( zJU+GVyz{SQPd<8hPlmBJ4`v4X_YXkla?03&9c8VlF=j_4)+LT?Eo%#}yS+}G*fU}D z1h95zbwi(+-FU9QBex``FJ2| zBoWU@lwTAyvTS3ufW*NMs)5K*K@b@dNH~dNDJh5$5Se1~X5=-5Ff=3i@qqJ)xIoN5 zxL=C2K_D7PF|cG%FHoqYc^$P$R0f1Nu}J_K6x~1!2;v+;tAIhGP)QR1q&%S1px^L= zG$`HxUWiWd4~huhT<9qv42lT^Y#{XnhzrEvtUYIOjUjr8e2dWMj)p}pfc9XnXCMj& zC^Q|?fTtpg2GD_4MG(IQph|!URsy{Y?{M@Vp$=L#fgEXh1l)l$I+F4MoL7sDn1ge(xDhjkn>3~P?F~bOdB~*gN z?-_W4vA6?45*Rix$*?dS6(=YH)T?6^ugb-MS*e3%7^X>ZstH2E=zd*CyBc6z9sG-G zS#}1l5SX5zPtmS;nN5*kSc-#209pngjr!9b5X7I77urC44FZAaK1fUn5tRs=MUM0f z5kClYptUz3I31#vd?{d#DLjp`M2RSTfv98(JHYe!M+_THo#P9?oj(G9G@;HO_<<5q#xse|eo>;cm+}3VG-n^A_s(SO6r7%) zDKW}PZ>Q4J3kdQ%6~QkeSE)frC^-pc%mKo+Xj=lT^pEmEL!i#+1M*N&bD_pWibMPn z_m6mVbO;D&3Q}-d3=iTbSr}9*d^*CC1{_Y1m4m9OIMfJ~Vx~%D$&es)DoZkpTmooF zupbz^c$D3W9rgtRL3hZP;KNF%d*kZ1&SY-; z*2$4HoJ$dRXSY|fhDYHUCVU(Ad3`Z8Swxs47H}$j1PlCm>=Kg2rUNM~oxs|9P#3wO zU8S97S%CYR*rg<%n#gk5G3<6@{t;4dNXei_fxIA6HK9^_Bx;eMb9Upn} zrSSu9!`rT{TeY?}RoL|W@Xl-`>ytw3dpmk^`9qUKlPWx?B6f4=R8p0R!Lf{ubxPP7 zB(7xo$EHRH3IpYG-VhK0X`+~z!wAp|(Gp554{GgM**bsC&6l4$G<>K!_*z#m{HIU{1P_P0WVW53MkH5R6yQ!_MuJ)?G`SxD9`qZO4>dZSkQg^r9zG%GX znYRXCYqWjqhU?w_6^d{Bfw(LB)uaZr0 z3}$+S@d`40TY|sc+Z}MVv@B~{)w2B7PygLP`KRwcaHhU*$t<hI_owpU2*5Ui)+60$jC3M&)?TsfBT({k?ZF#6ORm5pPD>;bJx0ScHHC> z7Vlx+dT#u2f4p@;rX^%_<<0%4Pd)MSj$_&o-!teNREH+Z`=>VNlF^=eSEnn+WMUa3 z*4tROVoAfDcYgWvH8;L_`ah~~nxSjHwsb!K&(}7kAAF+vee3X-=56@&$$t)53+_+< z`nBQb*1A{SHoPij1&1n+{`yzdmv*a**>%Q1c~f!!v6F|##hx|Ijb1gu^dNd#<}E{1 zTfGhKwX2%y>%ARq-~5Lf_1%Z7t6eo~|LLmVB&+WG{`?c5!#_Q(buHR`=ts|g_sy}F z|D*cB>(6Ef9{A}WK6rgcn8{U+K5(QMV;dXSu5Rg?I`I6n4-d7jpS^5_x7*(aFkSAS zs*I<2Y-i9BRFnM^qb2OtqS~>R)pyUi=I*)k!q%3WtxqG}5;Ci|5zeWS@TZwfif+U|AB^ z-(6ps+A-z7`qQ_zmJZ+FUrKk(YyD zM8rhk?$uq=$afzqMQ^z9a<{qV)V56A_IeuTt#0fLmrm`9_RPEF>iUkvu&Cumb!_ip zj_l4P6W;mtUVmR753jzhtwlMrqcu#Z(c6bcPxMWNd` z1>Qm2DuVo>LlFZ8MF>I^G7LD@fc=|t<%!7kC-2?QHBO9P!7@QDI<0;fW^ zIfxYE)u9={00EPNxTF&YltPvRenF_yWgs!aR27|4F+aXCa|0Fpt4owU z{+wgxhEp}CtjOK@p7}v9LRryaPOXN!?B-Glil8Iy4g=r&gIM5Y-+j zPz$<3y@{523IS#uk0eAU6VScz`N;_6r%!q?vhuKIymfM^B&q`9FVAQD>f{qMUtzUi$H~XHTdQ zPA*79Z^REeoKt*L_D)&oYzC*zTqKMI)QOT4_?meLcN|D|wlOI=h8{)*2^xqL;5K>z z;6Jv_fOsHOM{P*&BdZG`NpRj!Tr7U<2cEKt;+IrY|l6;sc8V zG2$GhLSm#Q@U6=x#wac+2t&rpO$z_Tq$N|5;*OCFFL&Ng4e12`()=UQgF;b|07`)# zL3hG<#1cM4&Li>%hAUiSSO7^O2-qni77IogHB-WtGKjcxb}E23q=;Q+u!a&e%?xMJsur7X5xp)D`qqyQ4c3MZ^ef@_8L=|Cs~Y(cET#CAcn z3XHZ7AsYw&GlYyFl178TRx)FQ6PE79)>M|3RbbK~@K%k-5O0rt2}EAuu`~grkwZJs z_68gbCEP%`TMVo)^2Jsbk)X%bwXvXaTIy9-N#UOpA#qHiL@9d3$tx-SvLs5yVEC(`BVZEde@TrJrw1zV2RkOfPNQy0t%*z|LzSP&%Sj9v&YofvgE51Gq`Bjn6?cdA&sT$!1&;O z!cnYzZ|m!!eM2HxlZ$OaRGC4Pp%Y>ar+AhK2n32@PH5Q9D2=5ZFdbnAkrIaHrEMLY zxq#nLyLcDHt&o+ZnYnBci$bt+g*NW9Xu|`HHVk!GO;D~->H=r zu3(A-LS#}I*wB*7O7z?lr+|4(WXAwAP>QipccZ|mk6r?B7xo>SD&jd z5JL>PjM#$qrj~X$R{!U);ZiXzg*{?^=gw_uPyO2V5c(iP4&72gj|cowJ%^=FaESv# zz!|QLZa;BwZwcHrwD@?MU~?>%$Ss9(A^aq#&krovrOEWYurUU>Uj&hl(*TNj)>bRg|n zvS5C5kC00w$EORG%20p4I5?1ngC-ameDJ%cM*D^3YngDbVhU2l4mGrF>^0TC9^Znc z!QshD#^2Fuj2zw-Zt*Ersi+6)`-b)nrj=NqpSLt`TM$E@g{YKsX3W(Pz+$OVF6zgk znusrCmax*3_xLenI8y-UBw+hO21*;t3Rw+<6kU&TPplo>_86HYNx?{hv4Zy%gBIiu zJoq4du+|Y zH$Z=&rXYn8uZl?v3IvNGv1kS=4?H853xRYq!03?%1+s;5ppaDqu3s>wcqj3z0M!VT zfC2@tK>@pH9q2~TZqRbjMYK5^F<`=0Eq>sjcAqh z3_2j~3vvyt7|uwq(+9~8q@XKQW_--hvgCXwO^Y6KF3hCBeLMt!Kyt@X?q@H|002tu zWI|Ab@FX=GUPD=XFXs=nz#o0!xAPPM&$Gu&5&|DiTDtE1pf}P* z=K?-vF4EIZHgwBLLht+dH8U6gE9;q`XFn;;Uk*Ayo6ael^J^vna&wNqe&D~I|JB?6 z>$>xdQ#d^96r37@@|meH&S$d4=gf)oj{pB3PV-Zl{+9>MYurGG&m0({|E~vSfM?HU z^gn(5^~?XN4M+r`Cq6zf`W&g`{3hXtle3p-5Y9*~`o@TG{+!mP7RRNtoj^aR$?2Qk ziUCj40|$yn#wVt&Gp$i^2ZUmd(i|qz*hxO~D6WyUg*O2VQ{Ds#Fh^malEsfQoOKTY z*}~yM+%$4Q06pXe1Px#W0Rl`*WZ~Fg^a|RZgh*!0IZSalTA&^Ta+s(x@#2mn1R|55 zGGTZW&jLY9hzyPng!w!BUXTk$QfWs?1N1(Y>4mW=2Bb2DNMq|#xM0Ycg(b6C3J;$R zR07nCg_UU7e*k_k)SA{6LjF==&Pp_FX#-gl({y+~5K?0zTWlr?g@M>I16y`elt5k>DCng6NZ*8Z~HhNaA?+!4*j{44y z5N*xb*%tC_Xh;NLz*zlSYN}G6p5B#{w9*jzqLR(qBWXR(X9^)R7B{t$C1nbE%@5ZV z>kfyp7FT5b0&lWyoe@u@)5ArrbRkp0I<%A!i@TJh&UoyRepBufu;m!bg}j>NnxTCrhNaH#*-z9aWN@$i$6J^9*^gSmc0xK>heHCIlw)q4W1U2Xp6 z-et9`J2tMFdv7pm`8ry9+9SP<8-hJyjYTg`ofy&dO_M{F(%9tIJ^e{LKLr0+msy%f zV>hkhP%;;3>~*hlw>J0HuId%^bZDa-PES?3qptGyJ?XNV*s_0DC3LfKw3Ow#3zJed z)8(?_nRHiMDU$FuZR7)MT3YUD@Oz>wmU&`!pAc^8tK?IXFICKLIk0!n@uyxqIq~w3 zAIkHCV|t${xO-$PvGCg)+7~ypZ)jQ_SiAD?=gzO2zrLr{v$3(4>q+p~QLHq&Ma~WH z+`OeSyyNix6T9zw?9`^hi9K6}M#rtj4c%RHuf6S_=8pD_wRhL~ni}t(*U{~2Yi`?w zaK`bYZy!GM*!@5Kx9>mopnUqF9ocx-c-~|6cJo3*$J|*P=iYqvb>D1UT(|Jbi`T4g zu3J#s!KrD_hCu&`yw<(>%)7^qJ^1_w)mQG{efr&xw)BQKnI{H0(0@}(EvdgZP6w&X(D zlhzGa-gu+XU4LcGWt_5p-dAq#sBc_-*_|8Wyz}^UdjA8D>?uaf zT&3K(#P3>j=S6c{n&vIMaz(pu{ViX<*0r&-ePeT=mCq;ZMkY=iI`W-oo_pk#-@N(&OD%+b@w&fI_SvFBfWVzfWa$JNHAvu|7<=v;Qk zHyhfPEV$y5o35R;=!%PPS-jC(=S`=^jz0YM`_+$}zv}yE-hA`Hy>|D)8!o>7?`zIE z=L=U|apjV=&7o}n?#JG#{;B%s>W9Do&3g}T8YzbbBiyolWn+;(-`pn7cwZ?zx!$ryMn(zH-*=dhE~=adr20dE@?skel3h@PU`#{{0_6 z`1w08J^#qwA+>kI@;SF%d(F3()VKGBW$cWJsBAGB3kMNmjm1*Qq2k2o^tS0DwnI$w zKDV#S-Gdb*eQ_4MW+~Y66?+j@hQ=l)rnc|dy<^k(==kKOiJ`%*M~@ypxZks)G1@-s zD_1V9YwHb1r1Xx%zMzAQ23qj#{7(_!A%KP@W$S<|G7aeb z><2IhnT2HALG25bA7F=O<-3<3w2v>)f~eA5RVPKGmWI&(U6`QySl75}ef|E+DEr_ZLQFLWBVZ$O9( z96QA5p-5;r?7d|@Ysmpu)5>UBe&Ij z>h2r3kIuYV{Ue0!BkLAi^~J9(ZjBB<_4=Pa`tY%zJ-_+%6MM~rR_dka9^LzB8Sc53 z)$_jb)q8GRG|yXm)qYke)iP%fB)s_mFLxb z@~d;yxOQOoBh`OBIGyb4J$CHWW4nfqzWL(G-I)!wEu9T(W`E&|RgHXMr0y03^3{B@ z=8B*G5CXLD+^GvjgEcj`U9#|vSHJ(Ke|_>Nub*nZ{GxAOa21xseDA5}tM8o}v76@P zcRujO^S^lU-8UcFCN^Ah&x*w>n_D(0rSZv(Z|?fb?>zT&XuW0CUsbDbtb5R?N!QlY z+%-r0=yxCd;$M!OIzBx2+;eZcaM82d9{Blp-+cSWJEc8oare>JPn>z?;L#mcTg%d~ zUb=K`t*wq_GkTrpwy&RaZp}G=^W1-;>8A5Py5!EmFVtMP%>Bsks=s{urK9^NVwcxk zb7PFT*h{_ZI@=1H-ueBLfB4yR_f2s(U31+(-`ehrCdUpPK7Ajv^2RUz z)8F3m#f!dt{<(*%A67pse~MjQ)3KuFGu|EFtN!_STZiMFT`T7N?Rj#m{LxGAo!Wmo zTp8})z5Vd%lhwEO?M*dbf9HbBZ(q8^oO=GmzJuz@*^4i^?C-AoyDM+_`-{#%V^zm$ z0xdPQ{_`(Lo_Xz~kKQjZQf=L`1y|Q#6-M>#w+{55cIO}JKYjR>Hy--qDIu}$#tU!S zuq0U6@|{Ocme+O7yYX9h-gU{c)?l~ix|*r#55M>8O*N}47p*+KxJG&YarWk|41$P@)6|48Ql6JM)-h1y=tFl`4Zp*SI*^&#Adj$t;*%$b=pXU@zsZ+YI%1BWLX9JZXNetFAp7Q_a2wz%a*A-gB< zfh#$;oJjoRy?;h;jT|kVWqE?L_NxOU-+d%c8Q)*osh&4W`(gZ4;_J`;@ZEtt!!ph- z_uPA1)%Hz0-#y)D^{l&>GmUd|Uf;2OC*va9@OUgNTAUu}>x(N>7ApLz%GozPcOvm! z;=Ro2nFXt&=BE}Iw;lZC``6kwpE&;Ndt>DYKoS@l)r1y5 z-iq#+f8o2t7v1xD-k@aOqpQ7V6Ms5WHH|a7zv+A;@#&Gnt2NJYN;199=B`-lp}&0n z@pdWi7guuTM*fmGm(Ag1ptD@~{>6WuUw<{{T8=pJ?RU+~GCka-oZIgo#TfJUJkFK( zo!;O1Le1Gk;(ftH=111)R0`U2CI;kxIR z7d1?DO4Rxc^GJKND+sDru3S$P>b6(waA()9Vy zrKDx1t<6kN%P1(y&Bo=G>(dHqYbByuevMWws+0%nEUHQ_B(Vx{RRv$D(d#g&XjFW0 zt$+tAFSDqcC$AMMkl~ComZ3Y)(>pi)fUjoWcse&fi4i=1+kDG>uwLbW6hglaf@7{kM(Ts-Vkf^!8_O> z3OT!b8)H^orAlFLXl-tDg7mu1fiMcSQcrzbcT-o;4<(yGs%eO|N1{%!a+w4YU468- zv%A5lRtG?&G zDG&q&s52CZLX8H|jvmYjbs&@Sx|{7rh0g1YYa_NGF_0%wXoAKj89Ol=5nwxjF38$-(mIbQ|PK9sCfj zN~gCm-D+D|woraT_LIU+^ zFy?aszrYa%o?=n5ptI?%N~Kl~_B4p^HF~ia46*oulbOS$lPloX1rZ^fTNHB889~l$ z@|vw6N4D05LPoVr4M7sfnu0zeJ_o9XGngB)e7F~afk3U(yF&f|;*e+|(I{az==`=gz;e51ziT|B_K0{9FDveA$F(x)>@1X7?qMR9Xdh~$^ueD)#8Cl zmZ%+!WU)x#4Kg&6+X03XX$VQz9Z*cbu@YNweJ68Aa{Fg`ZHV*<0HPS+U9!PsR+zXu zhA1H&q;7#zki9`&Le?(e`yj0Vi4{ThL^?>^3{)RzThhOR92hDaoM(`p$|kK1YC(D% zcuK*Rg;)}>WFW;mWezJE(j!3di6$jQy#Y>d2IyuWh6Fn>@$R8*j5^rmg4X~Y4hGRg zxrbO-cpwn$5z9UVrKmaaev>YTGQu|1nxHM1^>K&b@^j(@q?vF}1l2B4okF083rGt} zRGT0SrocGx$ON_~-Yp0oNwx{2Bh(ZsA=0BD&!pl75?S0V?h9Pm(9yu06%PP5Phe{E zk!l}=D&XoyR52Skq@m_#s$4S2hk8e)1u+bwfa63Xm4}G!mBa(2ll`F!8<4SJin>3M#GHQJ5-k0|5e12Q2fw%I$hi5&$;K@1n-}3-K=XIPs%&g!4cdcA= z)4I}YIQPrOzeyzid7!CBBNRT!c_g!P$A$Cny#0KCvrjnp?ir7*i0s{S{MCb_2Tx9n zj>71zchA^~$$_@+h}W#iOif+3VE&?oGgjXJ@PAx^I;#k6f09#IbWL^1m7E8RyZ(_# zd^;9v%_p^Dq8#`W;(!L5jq^eSG`QJzaIu^*`raapiPg+qU6$@xuG2|BQnr z&B8xk0DN+m3+|j=Jd1OCUhi9YjC(AG%r!YFGiRikVy6m47CTw{oAj zE}eVhwY;OBV|RRQ=E|8X^Hb*J4eUCR`2DCSzkJP1eQ}D;xcRN`6W_l1o>W)s?u(3e z_QW~@l7&y*`EaFcv?-)qS5RQoV=NYl^4CB3i>t5TTzTCQbnL{N*Znf*4o)VQGwTi1 zJaKO2Z7ECdN>k)z2LAKpsl-uTRho8j+T<(eQ0*_i{>UI)x#YeXwR!=(BQ#3CKn34< zYuHj;R3g{A>l9fte|_W6u?pqf`So{L=}9>cIyn+kf8PUT8e{ z{^@@tKDPItnfU#MginI#Nh`_#>7l_#r)Bux`0M$GpK-2yEUiYJT4YtV+dQj&^^9pa&~Nj}`~wqv!s#gp zmyu=gM*2 zkh1E&f>dqP9akuFbF(s~HIO-Z^c8cLtria+8E+A1l@|hM+12izT@fQsDiFbt1RR`B zgINv=Yr9IQwJ0&D8ZBCx(q8Y45fPF`1CO6*a{yE z40`-9CIbzq9xm&lh}(zACw^$)9bh&FWNPSYk2#g#mp~{y7s3yrRrE%0r~ww`ZXybG zA%tgGqBcjGn((b;=q3I1FHfe#(6CP2Yi+cV0iM`5pcBC z@6kyW`d~2bhnUY7@|yK7A4PLB`~C4Ir%7bAfv71EfDeLIC6T#dw*uNHr#A?;eU}-` zr4DD<+XQ-QM0bP&3>H>Ft4yVGBjAA@anrmmwcYA-!7j<+Xarv^cnKhnS3!GLA21+t zRedlD_aJwjC#ZK?BPbo-c@d(_bq2zaV!8YcexF6F1|2}aWY&Uc7JluZUUp;hg|Dy& zxWVT1gM9+6=!d%(YytrbY<`!|VukA@MeT&%5oQ>~j2#4=^LguhVA%lMg4L#VA>Nza z07i*Arvuj0ur2ps8V8B4S7R}F-G0RA1lWYLuG$)m_|0HfB^FkqYz@JCLr<)|z^%S8 z$Z0{N29Ra|J^*XE70N8X%L^c84|#nMwnD||1Ctqi{5=rB!qkjHsac@l#IFjwfFdlU z08IP9Fl8c*E?F=VN&+B+Zk8mo0LgU52`GiI6Mm=10)PT(BaVS23%sg$0f=~mpdNYZ z6EMORWXJ(uGc*g{4KP&znUIM!iw$BYb4ju>h8rCOy>t!mAh`^ZRvAc!ne;-2i3{;M zAz!#sQXBy^G!lab3os3Ulk~`N0s`{@$Pj=y@sHw(63T_GoKa^~gIf#L0?i3YR-sG; zv|@%6Op=TpTu%2$6(AKj;XL>bq9_=IkQyJD5^F&Mt^_NSwFM+bkKdQk**vNvQCo%(Cgvy*M4IEB3O+dvEfCER8#S0)JvSjH1$A~_aYK9Z2E{=mu zk1_>}1*8QQMTKE#goC((@+8;Zn> z%Siwa2Rk$oT92|IG*7`8$QP41j$!RXHHYaSxsT!vK$ig}APTj!;40(*3ei4FK@?gF zO$9i*)`PnT zAk+vtxtZBjy1KDrFTC*b@%@UUebd0kf$peFUsITtx~`y9SX*74nYm`^ynAj3 z6lwe(vdNR8QPVQ%QqC1iJX=4w@aNMTw@!L$GVZ$SIi6rXH9AL+!TAXZU3Q;=7A2kMpm&ZMQZW)ZRzS9==3`< z_jHWz-SzxrYiro8m#Rc^yuCV=uplQVCpE8D=craJH^~ueqk{}ciQ_mZQR;xhSwRFE0GE`4IQ1m-9fP0*YYKDvBuXN zZ*Ok^bG1^`(l;{PGpG$2?NFc#WgyDNLQN^IEz4P-kyE5}#Rs;H_JOvZR|#@cXAse2 z{ThDuvRMzUsz36=v6nX03M|5OXth&an+G>P->v*4oz}P^)wjolP#JwJPGzR1z}%(XzcZquN>DeEHqW;YBj4f;}aueyBiBLYGgdAP0iJG^tJVO>dSao z{}}BmRS?7vEdjYe3bSITGx+?AZyw$iElF9rKC4=*6PD&yii7>5Bk?*DOna?h^RY%c z`rEyFu}}(z9j%O4nzicTyK9bq^5(m5=pS7v%U&#Tdb3OM+6Vg=`E%?;CD+%{)-B60r9*KWL$Q=6~&`#(mr(v>5X@ zeS*5z0iNH>+xh3O5--F;>39FOI^MKzQ-iqFHFK4rVtJ8g!{L9tJpBsJqw2BGPMnCz zdNC<{V205re z+gd&FXm@mWhfH!zwr&KRG8;T#Mh2y^oI+TBlR!^$7Uw(1;~Qh=<@_4m4nt3pJ30c$A3eg{Tc?o!jcQoB1^| zI1&UkFg@}*)gr0U9&+f#Mubte$i#AsGhheNZOG{b;|mP23^Kg|?qhldjOF10X|O_z zM$yWN zh7l~Rpo+3Y;*D-SqM%!0zol2Z>w<_=UY~v3P=Ay&;b_>E!gPEPYv)7adqJ; z4vjiOLt(b0xgEw~{zw20TVDkPS#- z5qZo+vByYZ8gK#yil2wP&5s;iRT4m2A*^Ed*SKz_pgVY2t zQ4bQ1(icT=ker|~fo2LgfSy7_13Cul8T25i+Eg02 zuAOKPEts@*psj%G0YYd}a5RLJaJUdgkYu{Z5q79VR!DKE5G)EoJ3}li6$QV9ZvnkB z#9O76D@77vRaJg*PG0W%bqk+;>XFPNZ=Lz{#i*mHq+0sygU<=;$M?Q^tgCg%uhXnw zkX|B+ZX6lv8y;y2`YH=V>S+7UEn^eg$2auWgQUY9ZSU%cw@1TvT~$Rnx1_2xcWuh* zWlukT@68Ybwf^nvL}Fjf=Bn9>Wt?jl$~M0Lw+pYejgBfsRrlPmEKR)qwPQ#29o`9! z)dlx2ey%XwKRmJj;P!3%2kR|9jlO4ad}7!3zJX33eE9_+uagM{Wu;Z6nd?@~d20II zqz(W3KNH{oE#K61?+V%7oVx^V5WRgf7#gx=3Q{@ul2A{>jd+kuJBl%h@)zZ*pX~1-(!sQ^`sy#9~B~ywycO;9Z1c&Te(`?0fc_$rP3ES`fu`0?awuRDmOpt7=Zc$7B>;yk9HafVdm?i<3npIu^1H;r70+5SXBaGV zR=BqBKKA*`>N2%>Ri(coN87Sv^u(KOokIt95ATkaXNj~jOF&#)sg;S;DyhB+y(u0h zl8NeqwNF2EEr;_{2!a2R2y%YwoyReCUw7y0iLbs&ygHkcE50Rfr+!uNOR?``Y7>HV!?o@)M9i=RF88wH_gqH%hIM527O+^y_`F&AE7G06W-0a{x|8d zJ%7A#rn}_cD>*mJf2P_#{PwwH;m0{wPMhP|zpcp|)$+|jRW^`;zbBfWxw5*#KHwEB zpLM_O*zu5&w^CepxWk-1ue|Nh$8T<_nlqcf<9M^#zvbi~{;RX-j;m&<#@+du^B-OB zj7oEJ7UyZBVy;Tv;%U?g_1s*k#N+IL`Dl!{V7_{=x24`uD-Jb9M4GkBc@|rw&*u%B zE0+}Kl*rz9*hE2T@wS){20YIC&TfL&Z%qn|VU(BYe6saIh zfO#NH%(Y5AxJ&&WQ2PV3!AA==VMi zwKz)gWXKPlW+L^dT_{CLBGyR>9UolOi~2aWTnEaKAJaLFj}Zh^-)N zL1F+jOjZ!pl1~;O9L;7BYhf;C>86A{WB^HX5zVM{4O7*UO(coZjNljrJ}@De6X{-& zF-`0MBZOK2DLG_Bn1U&w zAZ-9ElIj;<_>X#H)`nCQkQY^E-gOKE5gcM^2zbI?6LmpipkR7hKA>jeUtmYz_6g97 zsJ4VAfqtRo1ulfM1i@&ClNiAmaU3M|t)$Qd7KX;s1rIrVyIiO}Z6H5vz@8Ho1~Nhg z=>+xx!(a-EYNEYx4tGmc#|G;F1U#q-7!Cm(CO$8^QKTmQHZ>uGwYVWX0n{J)q1g#R zz&{07QGL-+=omm}xNp`VxIgMZ1XPfg!4;grGa*EV4{c(H)rUPUmM3aK+F^o20KG(* z#^iOlXIw{*5Pv8MH-Kz#chq-L28yODE|!OcGMS>r`Qjj zM6!zs@QYqfyHo~4Z&TM(ChWt0DSa|)Y)}rU^HfO}3#1D!UBa%$7dw{xVd8B& zO>W{m%L?DPW!j{o=xFjxvU%w+H4JNF*5H&l`A0vj&5#BusJpYgs8*CAzHyA|gb(|s zqsf*{9%T)kY!XVsfa|5ibR}!MWZGn&$z#-lQ(3YUKa`VfUOJn6sZj+aL{?~R3@_Nd zSSA}}&@;(Li`=s42PX+ev*c6+D~(-C*V9@EodY1BfqClq7!5FV;EiFNYV4&Z+zg^I z8rIl=NKHYx)5?!tZ?f|Mss@?28}B}I!A2#RodO6Yp%GIlDS)R)SXG>|@PXpJldrsW zKv-cY7V>!&1%l4KTX${u@F`%s6hUtS8~VHZ>U1(hRYV*TrKPd8B_4I@)Owvl1#1Lf zLsNYODi$pq)nU2<4)pq_HaI|gL0^biqTqUh`3}IpL8~W~gwp}kt%NJT0>Mfphz4Jn zvE=DHp5A!$^n33I*3}dt5G=Q>!n60__UGGq1(h`-iOQfdbqyl`ml+Y3;CT-nhrgu* zL1FcXwW9?Ofz=ghjv>-LVn>_Zq3*toecf@78{t&c2Dlr;yc4|3aH3Z!;kA$8JQ|2? zYWTJMqP!)~R<`dxbncFR9g>!VrH=~n_OOJ86dkTSt&En)B3& zZ_a%Nu=sS<;^}-7PaJ>y)4f|aWZu3)z?B4AqL!{h!;ye4XEhg2ymc|3vtvUe!oO6P zs3C)>3$#6d>g>d3(Tay>7I00z##r;l(G87;%Cxi!l`90LVKi267Z4o;}p(UoqqU^?ZrHrMGw6{*eY1yoHJl!B}^981fp0PyiMQNmcrtoYW65 zy!Q5ma?XPdH8)DOl|3`^&57R8@SN+`1yq$iqbl{TqwRX9a?uiVqublxWbh93IMkk+ zd|{+%*K6mGZt&$Tdt}wx97W^S9S3)J>MC+ptt#N_g8jq8Jq->Od>dq9#3MC2JX*o( zUvjcP`}TLc-^=4XxydzMzNKjH;GZVF_4ZkJ$_KTv*SG7k>)eg)!~QvQN^63fUjOKe z#FrB}zrrG8W^n)6^RH|Sd5vXhi{>roMMm~d4!UHr>WX5J4~tFpPNP;*mS^ibvTw|h zxp1v^;FU8UotbQ|&YU-QUWqO?x}`lDf?ZHp6)cL1t&!o6U-_b2cgGF4!NOvG?a1E} z-~RpN4p+yHcu$}%Fmd{*BkTU_$@+bo=)gChgzx73N^$D*H!?X_OtT#M=xBQS>h|G- z<9gd#fn(Q>4s)?WP!&8dJ|HVwommO}YkkMy#0GDem%n)3x{RT{|Go30pdvq~j5~Dn z`0)d+H5uy`q>Jp<=&pSYDYNfXEo7Zq z6@t+s0+t!1IBf1=&if$1he;wL(_@Z8iP(dT5Fw;dIq*7wg8<5GGR(syP0aTYq!I%x z&5fAxPz+h9LDz{wsPa%r!cq+g)#?%4K z26RsmrJxUmL>2dE0(k_2ky5-UjI)^isDh9Y5fcDZ*Qf(cx&-?G+==iLdQdj2qa_)p z!xww7&~Fo|1eBhbBcWW!T7yXwsm&+>p*J)F1C2<4)i`-FS)m)IxWQ;z6b&m_2xqN; zl(<6@97DZI!dzMyVP1yaAQl2tFU-@JAy5}W7zAmkiGZoG;9&;{n4^Ap6qxxLz9G0t zjYJ5Q{u2^Hd*BF3R2lxI6NHQjLjeyF$YPrS0R)e*JRsmn@PmrOhY$ztVT1k9FM$&J zVJWAu5cPUGLD#SkuEUW_pC4^rwsR@L#R4z;rQDL$NM6A5qe3a?=cydTfAp^i+uwgBB>Xq?=mV;iY9~@kr@iK{iwd zS&lLq4vZGmHE3x_@dH>B5yGSe9(%gfPUuJsAESb_HleYB-3ls0T?>SxupLfHc4?gp zJ5d#+aQup5iMz3DV}HZ}du0i1n)IFqgklO=VTh!ppuDbNB!c+ButO$`1TqwXEh>f- z5)Gp!==WHv6S#!S7f5#zCIvh~WPU^V2;&<1C>nwm>huUuG+T~ijMCv9htVz;+*ok~ z5~?-CK8X86je!NsV7~+5H!vH5rXYG83I`PetZY~eTf)>>`XW?2 z1;8edO-oj^0!AGsqo7K#e#aW0-nHZhkB+^um-g9H!X7#qy{TA}TcfSr8(Nz+ii%2s z#B4^iJ4CiMt0f3m4s$LtR7NW)H5#a(pujRC5;`6X2{eHHVN;Uio643#+8f;%RH?iG<})WMix65onfE^!3GVGct&KLPAUWFjG!ZfK^?sP*=h!h6dDw9hHAwIJX9Q4 z(QfjgU243k)6_uJ&y%~X@z7dRX=w`+bPQ8}NGGikD8Ot0A5}0In27EPW@;Fn(J`SK zgdI1O5JXCei54m`HLUs3jnNO;bdKi<^&}=b;B(l|z}|?2Of*lx0-T;SBm~rZY3+n) zg~1@W#qegKTCjjZLaRH{{t!R7^5fV{FWzd~UPG(;i+ zw^7B*4Ggxh@j$UX7N0|69=|6>Ln6680nOV zdSMqleba(1Fs?ouyyxE8>()rXGQRP}$)N^wdZxe?XzFW<_nwzn+y_6ZQ^7_q_1V>l5*UT&}XrBGt%baZLL# zzw2o2F#0^tRoH`$gQqt32U;|yZ7L71qO{yxls+SEVVM^JW1}K@r6HeNlT{pietVCm zb`{rzP)ud>OGOTmgr6mA93N`3R_9mwoQ~E_hjwgk<}J-u#0H0ajcrB zL0JhW0kt!r4mWwL^YSVj4P8!2#9iLAVY~0DC^H zUx_yy&IE*9%+gvZBA8+x#$aI3Lfz~|Y$k-?LmV!83*cyy96<<2;JtK!R~6xm05m{B zf=PD(6pQ`tZa zha~iZ(@c~6u(xLF^#X zX-ub6>>!e{x0(&}c)c)RVGyOEn|)nAOw$XE*C;ud(=dovD)~*d#$WQqXOMk) z4V{#dG>oVq9bpIP8@mh-;};hIkz%wqhY9CY-FJ?wuM`v<8;sL zR`5+aPxgmwc)$<1U-68NRLgxY{aaV#Bnl9+)mpu!1p zP=)ZrHj_2FxXBVFQ&R1cmA-UoGH;|!9-*vR3bsLcQr=WX@;ch4TW3EE>Lg1>jEE7tb%%5uZ zLsh4av!AIi%9rImb?zcw!|qgzQSC2FGIbq0e`y^A(&3XV;qvWE`A!x7!@1k?OP+gOmQJI$t$16|eQs%6D%8q(D-C43Y8i5E~5&$da%nP#spm zP9NAV0ZVD%L$EVnFbv}=KgbP$W*O@wFfEN3Q1^h0UMdvw1$;iYEI&IdPt?D4$7GLN zXXFcOip$E?O+CHC-61z@86|u^PvxzPcemCBy$-PL3k7nsJKWR+T6=`OP)dX{7+{5? zNiu#7t+WYuY4g$RQnhNlszf>1Fg2J%!ROozE8;t7Ub zL0!ZG<6N(a*fQZ1M(aAX0xetdp>;gQC)@*Ea6vzdXv%OOlgaq7WlYPIHui5H?SW!W z1iOK1z9!h-+usH{6{QlmU#WLTWAUZ{2pw@1pqtGXiAIA6UP9!skQcz&fb7&EKgJ%! zpQr%r*vLK`NF6g6Dh`N9OMJLdV6Mb;2FW*Uh)qr#;v0Ca9uQf1f(}?nc@bM3l!Yu@ z2946tYEjeE3Wcmr*w{$QG+;86O$=K`2u;*F1hv*`um>}7HDZmC-8;;+l5T^rT%vIs zAJWYctOnr60*d?ynd&DwW1^SY-Jzzw<_>=lmeDZuM|7T0D1gv;V3>tL3Hp!*T(N46 zf?PWUd^sSNJsgfVMy*=8NUA`5EKeA{hb-@v5by$9l2r+j?4qVDoHPW*KyK)bSRm4z zjCmb*N%ba`+7zV3B*Jhw&H3o_G)bU8Cns$*2~)(%k{y>L5|0gcZT18-3cWh)40#du++|YN z*1`vwd_CcWBZr_G!H&?Kp%6e63or#^p{G%sVJuPS)61~J0(Cg=uLF%PgaVRvuf+>Hn+w7SWrVq2XAIX<;3eiEYEn@=r!o82xqT?(!m13 zm>kmT%Cbs{!Wu#lJkW>EF< zf(ln55zAC2Z=^or(Wxa;l^RmPaL5Ta6eDh>s11* z5|tXc%^Z$}d^&|lk5>u~MBzX{smEP`X4i%&vq8VZXb9HB3kZ+Z&U9mxrM6HrgelQ2oq@*P>SBy9R)(I3 zu{7rY-Jk0oXk#l@YAmtLOB#dGvam(Um6On&`P++_!8^Wuz6znAve!5@7k(_K3B zqg|Fu^6UT0B3S}u%?XlfRR-939g7R&zwi2LuF literal 0 HcmV?d00001 diff --git a/action/sound/user/letsgo.wav b/action/sound/user/letsgo.wav new file mode 100644 index 0000000000000000000000000000000000000000..ae0ddddf35143b7515434a4bbc42aa8d8c398bfa GIT binary patch literal 4770 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 VwuvSKV+PqLBbnE91(cE~{yzxCHNXG> literal 0 HcmV?d00001 diff --git a/action/sound/user/letsrock.wav b/action/sound/user/letsrock.wav new file mode 100644 index 0000000000000000000000000000000000000000..6e6f64f635b8acd9a18fafe0815ce3f75d5dcea5 GIT binary patch literal 9514 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 V)!uI}+pD$f9Lx6FyL6f*{}(JmP@n(+ literal 0 HcmV?d00001 diff --git a/action/sound/user/lookwhat.wav b/action/sound/user/lookwhat.wav new file mode 100644 index 0000000000000000000000000000000000000000..7a974bd024ae3ab2ff6414198b3e9ee18d285c33 GIT binary patch literal 18882 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/newbie.wav b/action/sound/user/newbie.wav new file mode 100644 index 0000000000000000000000000000000000000000..4b7fa96d22c7056bdc413f4417b838599862fb88 GIT binary patch literal 16348 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/pressluck.wav b/action/sound/user/pressluck.wav new file mode 100644 index 0000000000000000000000000000000000000000..aad54e363b1f57bc213b1ae0e44112f0e704dd9c GIT binary patch literal 31948 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 E15`fyc>n+a literal 0 HcmV?d00001 diff --git a/action/sound/user/s1.wav b/action/sound/user/s1.wav new file mode 100644 index 0000000000000000000000000000000000000000..57498254dd5e6fa53c7f9d4b9511e5ffcccfec25 GIT binary patch literal 88848 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�Xdzj<+bZplC>3Ezm_ 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/s2.wav b/action/sound/user/s2.wav new file mode 100644 index 0000000000000000000000000000000000000000..59afed0c438188132c6b71cb7709f0fe65f49054 GIT binary patch literal 133744 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�SmF5gHzfOwCXV&QmNQOIaO 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/s3.wav b/action/sound/user/s3.wav new file mode 100644 index 0000000000000000000000000000000000000000..b04206541074c0659f44438995cfb451a542fc1e GIT binary patch literal 87638 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/terminat.wav b/action/sound/user/terminat.wav new file mode 100644 index 0000000000000000000000000000000000000000..9210a55ca39229652ac1d53e738b535f033a6448 GIT binary patch literal 18220 zcmeI3S+8VSR^N*^Kl&R077z9lNXQmK(59`Pn`*kItg6hsnR(}!=i$uri8wLO_r@Lb z=2VqgRjzVRAhnGEBfujD%LrS-SmFU&vi!ye@NexCH;3v%vMl+{CnMuVoU@0u)?UMZ zt-a&TZ+`7-Klk<5UVHb;H@IOXoF;S3r^93)QLckLi> zbx72sb~~RJVJP2 z1d(p{UBlCzp5EwmP21C(M&P-jXJ}@(Z?!z%aST7Pbr{I!<*J@*kRZW z+NNzJQN>%h6@TD0jjr!3MkQ?=IKz&cM4oST`eroHj6qaugt6;breRxc2a@>UvHnpM zk9zfLFKKt?k)OCtE$H?Vq~LjtdNM#Xp^5B#J05zvA2^oR4NcE$S+Qlt4q|HSo@bb0 z>=;PJjpNYug18g<0auhnzVG;<6CgIr&|TlsZPzkPlrgX@xNO^j!%CTLzSt-rZ=p+% z4z$L;;fk90t_9(?sf*lA*2NK>woKGo=;4NCslJi#1aTPn(Ch^$kL$P|jj#b9P94kf zT(pYz+>$X|7YSejp5vhd(!=v)e$)aQSR2pvJ=<1IJn19zI~H2ZCn$ml7rO~VA2NK) zWnx6GhW2bP_H93My~wkXg=_mEv@s!%9SDFPKV)zSaq0VHk;s zSn4ilceH5d$_XwVnTL|N6JS{rVekzxl?k-My-*Ii0va8jMDR$#Steo=?a99s<&#v!&Im)pEH~ zD&+I|a-)q_M9FYEn=g+q9zA*b(bH$2eDuKwk1ozHPLEg1alhXU@j<#57%r{_V^Ru~ z48z{S)8b~PQ8=hHyy58N^7#08F&)q5gYntv@uREddOe$VBe$;GEd!6;svX{Y@0-8& ztH1W;U;E0pU;pln+c)37@&0>vGx<`(3Wuwcv$K=+Vmj%DVbJMzB3G-GGCOzPf9Jhh zTRZy)hxtma(Q50K-yP15&K^8|_R-T1pFDi{@WF$ti?j2Sqvf<4qB)+ep*PqCOAtkq zBwWqp7l+c0rjmX{>Mle3ev^?VRIdZmmotCdQAe|P)V z`#0Ws@8+GuV%_w6ljZ5z$@#^_)x*b69)Ixc@q?>lmLZZLX1liK+C~^TonAZ`BmMDo z+#4^413`5%4#8?S4#(UX7i>9fzCp3mot-sF6J!dO4}!N(t-2KD{7 zzVfB7eC=D?<#MB$yYu$9zxw4befe8&-9D_l-P!rogRAw(ZIpA_iq7}td>9(#{r&A* zTlcniGsQ}+-sGwliiNV-nO;8q>^FbwH^2Y!lgs1fV%+ZrcpuX=T1{Q6H`QLcpMLxR_5?qKmF5x|7U+S=N$d$_x|0V|Jk4Z!S_CSe6*g6 ziLReM`}EVRsk!~_U;Nph`MIC}`g{9r9EelP9p1gQb@Sbu+eK^g@UtKO=J!81j!L(` z^NsJ^xP4gB+_2O2v}WaCXXo~ft;3Su9gQZ_VeAl%yy5cd*+2c>4}b94v#Vu4Xcu$) zTie-E-NG~UhP`AsnXInvpFMtb_TbU^*?in<8P>hLb&%Bx`*+{n`o=GO^EbY`5%7gcmCPs(I~O2_=wJ69u{-kzy6i4edWtvdE@roy=n{R5lR4PUu(K}?hxQ%7zEk`2N8>t7)++~qt)tYu~@H;L{={?otx@u&AkZee?Gw}7kks@d&#-uU)6UVr_K+xIfHwjK2+i{-3m zSNB=te9Hm_O~!++-K>@Jncdqr-hOv$uYiNXp-MRMyS?FfG@dOM%LUhTI!-!CZ@`V& zbUKK9tJSD+HQPpjnOs~xc>Lh}bS=7?3=`Wj>+M#dT0STg_w(7?@4o&kzwi&g`rYd3 z<B z2fNz`m13?LbfVs51k7`rrM(+(ymfQuAd{^$YNpd@5DNQ)xHDcHU0hw9U!JX(g9wn( z>2%_5AAdgRO58EqwT5no{n2E3w7$4FJy}c^(_TN&EW53>tJO-QRjHNsYUTIte)s48 z!9V`hH~rP@@gM#BKlxvO^H=}%FaPj|&w7=^_utv6RO(^W)I+^cX&v6&zWtpy?(SvV zes6YsJny^pdOepb?cL2ZO|Lghpt$Q)5$-$hy?^7an+LhN3lzgRlECtN(7RZkUfjRJ z0THh{0dT7$e!0h6FR-;X(bo^--t>5VdbV1fE+><&13)m_jbgD`t?AVYuCsdk*6Tn2 z%isK!H*oAve)Rp{|BL_j7ysk0e*d>V8(Vha&OxbMcVnX+6XU|#;lZ6Zzx(cuclYvT zFOEjTFiHZ$Fv^)~qtqbyhTWJr*(hc2Zryt8&3E2<>*n@;36)5Oqv?Ecyk4zOk5+35 zRnzfkfKMf+Bjnydgxu?Oh`tUO$PTgo*_@cRm=0(bftFj9a=lV&)@xd;-mVui_ul@- zH@@?oyS17#o?ZRmpa0RH{PDm3gWvw_!Fuc(w%*jVW)roD1|8SdONZMxZ$Qrbx3|UB zhTUW`8-T=srp-!~CsQQrN$$CH-v(xp_(ed$` zP_SAp7qc1hZ6K!#h)8FAF>b(tO52OK0qS(d3cs^#3_{{G(Xo;YY|Y-zft$*|ROp-`&U z8f{$$5~+cFOEnq4A$yU&0A#dCYgQ|z zVzJuP3`1`NON1r7kAzemaNNcDPoa1TpqY+9dx0O3x5{vVr`9-GMml3#H?I5wNkCOuS} zC1btOIUjLb7JQfT$a;V8Fo#Mo2sDG(>(w$axKJwNVZ0Nt6Q7ENrYR4nB3MXa6)Sz%Hz5;PNKCrm{V7=4JI zOy={^aD>03w-{xJ!h|8i=fD2NgF314%XB)46oThA#h^MQ9#GV zsc9%9Ysw6!fzi8&97R+shQ`kE>W0;UP%~5BZ^K3TRc#)&y6F`!vju76(E1)Gx+Bg9+QBjh% zrnfad=&f=-v%Pia&Kv$R>%~x#L^lxMKOWDPxRbzuCR88?iBO3BHL8Vd zCMzq}(y03|QRG^Bqg*T^eq`U)xmcBahF0Zz+vyA@i=#EyQliO3WPHrGqvdRXcSi}y zuEr|Pbh|#HGf0Io2xPBZqD48R-XeAqJ?nb4#7yObd)se+>&+Xtigs^x^~sNZ{CmIm z;~#!>e?7u`_{j)0pCE6umfO9%wVkgvO?-rp_s;KcZ*6VeJ**NFiPc2pT7eZf$d+3s z^|Qz~>-pV#cfc~GrrjCN7V}}ROKmC$hx6kHPoI7C@zY0_tAY3^g2iZtBOE0Hsw@(F zzAu*ZalhkR?K-(bOQXi*xrm&=fV9v|(*&tis#?8{qQggSZ|l~*-ICW^KKbN_zx%tt z{p0UFyT6?E(bHH2mH2kMn%m#KyPdDK9Vum))y(d#cS%r4psXlKDBv|Jg={Wg25KR) z4gk8G-QB*oo2zPWl=KJ4LDoM=2D7tAAAJ1D{o#}6M}p{i#W=SysBd3OKuWI4sUe9LIH zK#D|=2#fJ3gpd*8G6?X#ivzD!!Rqa1wNWqQ;t3!P76?B;QO*P$w(XwL41FT`o$ubb zdAm*yeR6sA!DpX7xte$Io$Irc6SQ_d3hKooYTamR4)(0)cW&Nz|JI$|{cPQ^gV5ER z^$Lb!1l>MvYuNRTdRZJO)}kwP23%;FWN(A{(aFWx3O_R(&W?|cF2L;PCktvOI5>2X zyf=(}>>j5Zb*Yt%MoBkzaf_HSrrd1cvc+}c;wnw8p%6DFRy(Z~F<}2z-o20cQXt z)V5R@@gGe?`s30hz%3}%k66<}(#BBpT>Jk=kR5&ZVRgGl?ngFZBf`MJ2390?16s3fvH0hb4+nz*GfMcMOjRi11M(z4p-ia4K6r{^l?)4-76=N?m7#?%77FE5 zVNAXftf=DsVy6ktkPNH!+sDm;~nlJ8In!}Ex|1ncOg zV0nNsg9`Gu;Z0y$%9-rfbQoF`T4@=2#s^7HV2=etIV92xSFnV$W{FS=wFau#s+g%V zxw0sPC~6^kfhLGfh+$zyGC##Y(OVQZX268pN^}ocgbXnyv22+O)P>CTR_e#RT1NaJeEqWlq6YTEtN}5)cUA&3s;R14F)8RBy}KkVM^c>;t7m1>E)!<+m;;nRf@Z5KP190`p_m{_u!8s$Kavhv~|B;zA#gaDL7_!@{gN!lWi zR5>8BEm^CQjGk&AfKSAh4lM?Sw-6|t$_%NA3JPOz!MNgqU{OB9lzc*x{ayzNcSpU5 zT@(Iei%1`PLc1x8*HA(M+a=_vj2L6ku%Tw7O$tx-Ung`)=0){5}9=9)XG{vOdC#z{@Tk-O>%+#2&?GigBWUI0>{%vZs23NM-6Ry$w<% z=(XCmVFsQZd2Gs=KKp9oaRrf=@`byHjjBE9Pex=|qobp<^VP}4#fMKHJ~$fmLYxS7 ztT?dsMx|WH?B6Bc=0sfi%mFn8gK7aDK%lyqc0{Zi_9F+`;R@5NLVQ2Mv)r(U!zOcO ztpwo`sTRY2pSVG(%x6a{iqHg6V&IP;e$ibLOnI>#Wsy!b8sg;Y$^nu)!?4&ZyFF}B zQNWsXf(X4gKmrtTLd~|ymE6w$ey(ivr)TF#Go<+XaK(V>RM zzV--DA%R1@AC`-e_1Oi*)8J>>9Ag^{j~kLo_XlK+;Ep)LJ@zqs+?LKDZBk<@PG~#4 zf#5`VC@cGwY`3y&)n%D=D*y~vYFeRfmbG%msukVRUSY3LEhI;yhaXZxm|R>u`t0iT z@#WQ%)8h#+Swe7|l(h!hy0e?f;t^}rhKYR>)6FR92}S@tNpwezUG|u$6Xf%Gsm5sR z*^=F~LEvPhlGjObnblKOF4<_XBqQQNO$-gx1Fna~fw)NwQHo>$0?byEEJ@{%R7h&& z5{&8?q=^98Y>xY3)WaFF-D~+S7OJ;O&33-nYBX$%RJ2$xR<)`(KmGX8@~AhsfA#d) z$<>3?4?kGXyS|T>;Y`qs(&0VI{s(#7ZJh$0-rcA01KN&O%jAQSWmX#(9jNk8=*+i zhaf>H5kApY7BNK)Od-aoN`HWYBxW<0^-nte(CG$dD2t<&a+QMvR$Qy(vIhs5x)sdN z&q*-G)6?^-XCFR#c=hnX<@vlD*(^Lhxmhbv0@&KRcaW8e=3$m#A&HCI5qBfuga`vV z93_;Pah`%6fUp#EbJPNAO~XY9gi?}5b|%SQPEU@g<-tQYB#KbJuCj5~XbHfj=pZRi zUjiYcB1MUZJyC7I5vWUic`={IiGWpbYSjEG0RZVqU`LaFZx~0z$nQHXr(S7fYK`5t zwx`wZR1fbovXw!!d^|lq4(I2`Po6$}`snJz2j>?gQn3gdREb;Ne*f0?-Tmy|!Oqt9 z_D&&RY_dwI;)ogwc>ehK^nAS<%W;^r-pZcf;Vy3YFjJs@!~ud3H#u3IUR+*1xPNg* z8J!$@JfcM5+ZwBsuQsZf5HJY!WwRfXBtYY*0B&N&QY4^!+{RI}JlSkP7PbJmmFgVr zyNG3p(UatLCjD>{wEb$)C={(?#iEFH=idHywoR`24s&G|O6mYZs*A@b=g@z7dNk=v%@P>Us#B)f z+u7NM*KDC!BU}Wq$u0yxB&42g;Kp**Eo#iv-<~r825(@_R+##x0OVto|lcy1Fw3wxp%X= zx7V&&iQntml+YmK$;H{D%lnT`*C+GYigdXbdsd^6A+g=t-M@G9=DTmdf9uvg(%f>x zpkPLgc0g@|rFwXNa%N6o$t5;hb;*bjFVzpkMUYwJGpP!!` zAI(O6Fe9~SOV!>naOzqlR#MEyPjLgegSkVi2 ztl;oa&^?qYNJ!8na^WSwy#lILwjec5^^wFSO&mBuL%u9I48DxC5)>m~5ch?$bEL;Y zV49p8sW*+L=@{fhY+e$A(1AfhP63A#d;-#BtpVwPEzSaI1`|`mEyFZ5j!LPFm&XH9 zgeMat@t5j3C1AmOU~fLCoKBKK?(>p+>JnHeK~iN&f)792FEwO23B-SHq{V3@ zAPEPsKwiATyCk&|zQFwgcF8NCK!Ifm%UtwGOBkwhz&B(fcO`7oR4yS+reR6}sj{Oa ziPxs0uI@>T^syp;Qzw^PhVn@IE(EDLm_a6%T??M_Ur6AIKydX^6%Dc!l>`ZQk|Zl> zAPd<-P+vtblJ!ZhrDjk6h)m?-+lJH)gEBmrU-pmW+GKmoD7U2YEB2u-T!Yk9Lu&{l*LkO1Z6@T0|+g$+-hQ} z>q|0^^dUnmRMi!vR>7sTHI*Ym7Ily>&&48AQP+lTS#OazoGYajD$;vQE4_JFghQen zC&KnA43g8jl30{5D8XJjh+m{6A;}>?M{O08SSw=}Oe3{P zuru@X4{y&$Dmo+-*dijqJ%zX@SvS@ahsD^a5;0Wjx4;B;Pe{VqDH4OiVS1L=@q5EA z=W&*U-h50^c)eU41Cpu4#9f>w#}|nOlK5q_`E2IkV1JMQY_kxH3B-}?#Sr`^gtLX5 zs#`6Vh>HE0KmkfAF94c}W>T?}jY&3`NJvyF%-+{*!ATtUOw;}7E>TTYMexN$AE{WX z+L5T5q)i+llf@AwMnNdCqFd-C_J9ne=qX8Qt%}d4GHrW?wWhi$5tN&YR=7ZOn{9d zPF`>@=(wVxl!WfbYrVkBNamBWP43ALCbsdccGP|js`w-0uA54Z2` zZSQ1>`el-GAeYo;W~=4uWX&nNqZJi8a2I+_8qXdF_JF8zg(5i``L5bWOxT56ffEoC zYrI;4i^iOM>c9gILlv9Q=DO@yV-ZS)VUdN!93+HFh{b!jn67ee7&-NVW{YhXVH+Lh z%(D|YE^6=la{kiLe9M#$R7kXj)hru&&2-srMCU>|u0|riJYKEO=CkE!JRc4?20;Gk zusufMql8}XA7&|q>>gw?MXJCYQb5NAShG>SULUjFdBmu`Y>^9ge68b~iKX}2#;|RV0N!oSs0WRD*lgnELYJ*UG6A-msppXW z&S#t`o04hu!L%I?`vMtBXi?Y0EQf3NciUvjcKoLCTMkoQq}4MGa7tT^1hg zPG1s#WhX<-T3i(yKuC-e2Ph~}&e3(3oqEzi4!m1X6WAObw>z9MrdFx(6NK0^63_0r zLErE7qv@zek!Z%M#Qk2xxu|~Va`xSX3~CkylEz9dS3=Ws6hcuLs{w z<-!GAg7AQ!61&(baX2nbxh4WMisqv(ge(^^pzgBEA*W~vABY60Q9H#M$v(4qLve|u zw;ex7{0^t8V~>q)z-_F~+Q&RoMiK-uvSlXD%#u4$)~J#Fwot7atSBJIdf;|q0kjnA zD2{VT4xOYBM>RqGkkoHwe}c?R0Y5rO0HT1x?u1-~F4A<#yd|261VDk%$IMb#7c*|* z+LZ*^lR|lLdY~2spa3aStQMFoCx;z5kq-8!{x4t;&D1GyvgJsHnuna~=5)I{rB7h- zaW4Gafi#>g%|IOhRU|W(A{>iCdoe44K2VG=C92?Gh>{A^3UXrk1tp6ECcHqhKxse{ zV{Aa$b1b7kzqqONfj}sUPAQZUk1ddoz)Vk~yf}23l>dlW9o1s#1+>Vtf~Hji9t*@) zQWe03bfuZ>g^3+t4Jd#BULMIM?FB<9>)8;ZzRG7Fq;v?^3W^CI%GQKfd8Nd|1VpA0 zRhCmF6ax8PTyl>GGM`{IVMeh2W=b`cOefby42&)jI=$#{jecpMXi@H7A#}7OrAjDL zeoq9&+%TzL*U($+pIBTwrEj(8^)jB2rrCytlvLN(H<+$PWWg6718?yNfcWN$rq^zW5@vd|N z3re+1Mwgr1mC+Q>vUn1WxTVOKwYa{NY&O%&klYu((uxc&IBw8>$AA|wR5 zmJhBjI9#XyCWPVL+XNeF>|f zQ7@yu2PV>G!-c$z5Up8ql00T$? zxg?(g9B^co0S<=zFk*OW0UX*EdTf4?p?>BT@CSj&0YW)L7zz7?b~fJlO&YNZmbmX& z;@xa3?#M6F5P&RsfNWR`WGZV%!CTIiLZz&J$kC^WDmz1ka?IIPKV_2$Cdo0f?Oc145ipZWjQZ*6# z`+OHgE<`B8rHMpn>GipW*VI6Ong;oaZ&6@|4&u=nh56I#y180?=2sG;6<(ju5?iF{ zW)2!FeN#g-22?7V#cj!C&quu;eKUzjPvj*Yk1lcv8$ZG0wd908nN{AUy%ML0l zo59ul=BtdRuII$aIEqAWtB2{pLcClrO?cf^$yxd<{-r_MPrb`f8#dL*G*h=^sF%hf zI;8V_?mca@8A;YL9a}nI_m>G|G&S;z?&%%%E=#K}X{lPv-%ZnWX}B#zUymguZmt(2 ze(sZWq>ZpG)5}xolFlUWa!a18JDb*k6$5Y+ajdTR*r(T=k(tZrLX`{Yxo(WHz z>!l&(&P!jv;G-f!HIt_D;R`#c2QMC_UuEi7J4j!3Cmn0^ZPWR=RQN8GyckZtNxv67 zTz64VUU`$w>S;RBE6+Bzmk##o@Ye(WpZt|EUv2k_3BK}(mamfVxo@vDPQP5Yco$Me-?Wd2_0`_AzrK-v`uqSdo~8{y|NL{`UuE+8;pZiN zJ?N`{uUr4b-&dQx{Ou>2rZSC8->zp#d%xgA2}n)+yxa2+FUGvSwYhWs^$Y*L=%t2z{`CtRb2Fvv cc{9~XZYtKEcjET-=T~`_5&r-6w@ToD11;QSBLDyZ literal 0 HcmV?d00001 diff --git a/action/sound/user/thanku.wav b/action/sound/user/thanku.wav new file mode 100644 index 0000000000000000000000000000000000000000..02d67a8d3a7260868479f89b6fe9e6815f6c2c4c GIT binary patch literal 10350 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 PWb$3Jk^&>cZ}@)zKKXGr literal 0 HcmV?d00001 diff --git a/action/sound/user/wrong.wav b/action/sound/user/wrong.wav new file mode 100644 index 0000000000000000000000000000000000000000..2a0ac917658c55ab3231fc188df93e32fdd23e78 GIT binary patch literal 32892 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/wtsamat.wav b/action/sound/user/wtsamat.wav new file mode 100644 index 0000000000000000000000000000000000000000..04cfedabcf5e34343e8c0eeba964ba6ae81b0a27 GIT binary patch literal 16550 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 literal 0 HcmV?d00001 diff --git a/action/sound/user/yuck2.wav b/action/sound/user/yuck2.wav new file mode 100644 index 0000000000000000000000000000000000000000..512aeb080d006f77d6f5de67e9a1a9f714eddc47 GIT binary patch literal 29994 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 Plqxx=KDJWr9RK|f^$%7* literal 0 HcmV?d00001 diff --git a/action/sound/weapons/machgf1b.wav b/action/sound/weapons/machgf1b.wav new file mode 100644 index 0000000000000000000000000000000000000000..95d13f9496146350259d40840214dc4abbbaee76 GIT binary patch literal 16692 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 literal 0 HcmV?d00001 diff --git a/action/sound/weapons/rocklr1b.wav b/action/sound/weapons/rocklr1b.wav new file mode 100644 index 0000000000000000000000000000000000000000..59c8e62acca175bb4de4cca7a55e94a9a6e18545 GIT binary patch literal 44 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 CnPEr( literal 0 HcmV?d00001 diff --git a/action/sound/weapons/shotgr1b.wav b/action/sound/weapons/shotgr1b.wav new file mode 100644 index 0000000000000000000000000000000000000000..59c8e62acca175bb4de4cca7a55e94a9a6e18545 GIT binary patch literal 44 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 W2lSD08{R-zDr)$g^0DPsMCE_e6c#oB literal 0 HcmV?d00001 diff --git a/action/terrain/actcity3.ltk b/action/terrain/actcity3.ltk new file mode 100644 index 0000000000000000000000000000000000000000..f73cb28dcb27fb9c74c6c61d347419a54c5793c2 GIT binary patch literal 34780 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 QbzK~ul4~xH=(SP(KLOX)jsO4v literal 0 HcmV?d00001 diff --git a/action/terrain/actdam.ltk b/action/terrain/actdam.ltk new file mode 100644 index 0000000000000000000000000000000000000000..957a3716b54193b05b1b1856a3b8505c875e74a6 GIT binary patch literal 91674 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 literal 0 HcmV?d00001 diff --git a/action/terrain/asylum.ltk b/action/terrain/asylum.ltk new file mode 100644 index 0000000000000000000000000000000000000000..18960d700ac7888b8e1fe429af405e2833317f12 GIT binary patch literal 19138 zcmdViYitx%6bJA_DUVW$l}B4Bw2M+&-sN51+uia|@dZYVUu=vTjm7}MFBr^-(GU$v ze1=a}jS)nYfExT@y6mD+_+SM!MvbW&BLq#Vn1Dt({{NlM-JQ0eAMWg4dowwm=`_D{ z&)uD!-JY~qM6SYCu&-1{T83^pDyjA~a~OcF2wTMiMM+g25wRU5{z6)sZZal4x-CmM$1aZ=tm+hOK_a>$BolNk!r9+q2P{b8a9I$VCnh-g0 zBrxgFjlv2Zi_*F>Bk_UBz$}3`4BPIamDcB%HIo_f9!Uo#J-ReY5!ar)-nuQRg;>in zGfz5nX$?ngZ`)_zjMpZ`6Jye$OREg=_|8@K&CY}H&Lx7{(xFRh1mf(cpR{itjvF`cB`57)Z$GNWJWKI>sqHrD(4{pJ@zt}3?3*3aS|t7wyoZi}5IS^ejY8bJe6Y2e znrp({^_b_(1fDOr28z(3OKUXZ=zA8}H*b*^X)ED#wA-XZm)01>&vvEk*Y-e1QsPC% zq(hfh1!C!;o9xdswHD?drnYqG(yBz1u7mc?*jT)-WKBABX;mS3JMOb@cCN$oq&<$t!l)biR1Ro&byOx z!*i&w|6HL%m)3a14KLnh-;`~*@B6`+bm-EWfVl0oFYKEg&taa|91Z44hi(*>x%S0x z&Pdk|^BAguSBveH2kl?;jF~!%>-%Z*7@|ivQq$X>kWSCcGX`%WwqMGJTVF>r&-f|K z@q(ErJ-RfnKrCLr$Nn`wz6Nv5Ui;|KjlweXOs&R!-%eB8E8zt{izoeel_jr@BJyz3heI0|kNyo?*k#ssR>CvTGk9gzy66=p&`EoSO@0z9a zw(fb-3G#_5O2c&A-+qJVsBbpFoPsSg<5+aHJ;mDoXiR!^X--8vcRFsrwyhuFar?6| z>CmM$4e_n22<{W4GAXTipWRbiI&^7GNBn)$3)X8aFVIZD2i{z4 zGe5UKKV!;MNom7#w8x}JH&WBvn~*NJJ?1=k^Rd;GL^x0H06wSr$$VBrk8Y%<{FRo* z-DaK(;4Q@V?g9Is^JUj_5CuhU?0h{VslAxzKHOW2G+|qWt^Vrm)}NPV zp59hm``XPs>CuhU-1uQqVz>wEsqJETORx>P?vCi@+13CP*P$Mh9$lJC5hpbLY26m@ zNJ4IjnZG;Gp&Nx|)-ygC*S?#LxeVTNZ1eZuYJGmj^s4bbd#W+%(WSWpanqDJ)@?~` zNJ>?mG3n5y6-TU?v)aDd^>$LaRvD8HU0ThELvC-0!2BMcCu}w*op?l+S*D-I4xhCL^gsjANcu{3^^DKWjDZ8FACOx_| zS0V1Mnrq#b*jM6s& zq&ft>4Q|wkS@`uC>QF|mRVa6@hM{-jzl_}9QT2Z#e9X>X3D&I?y%qmH!k&MA_?VqN z&Q*0ddN2NMggyW4@G)C^oULjZdNVFP!k&L__?W#t&R2B=dN(dL!k&L-_?XQ-&RF#_ z^mbf)ggyVf@G-l4oU>{iui1j_Nq{&T#a9PQH(_ zI;z#^t*Le-Bfmnb+jbr%)wVH_Dpx}EKVhq__ z;Z#PhRmg=^`hVpx89iDqWz2U*^-g8vQgy<)OhIo{c!YnR@4W`B+f?*U<&J#k5cXId z)oJLh%8u|E`QKx8RHvi&Dj50Cp|{BDsLnudmOpy`yb$}Wj_OSGZgCVMgT5xKqdE({ zU9%j?$ghxUBYMC3=3A&Eetl*;l#y!{%3YbcPSr_uj$`=*wVLl(Uh^EwC#cgx$MRa> zSUy3W7CDwzlVkY=by|Yny5M^k;$md8)TxYIt59~8mZNu%BYUK<-z`HXE704w!kLWx z>cr9eSJ=wr>VAEi9m+`6tqSEK}Uy0sCcd}7!K{l(L%E+|}xwKk+ GbNL5FdODZ@ literal 0 HcmV?d00001 diff --git a/action/terrain/beer.ltk b/action/terrain/beer.ltk new file mode 100644 index 0000000000000000000000000000000000000000..984e336f43c9a544083c80b45a9742c422d8e9d1 GIT binary patch literal 209580 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 E14(U{H~;_u literal 0 HcmV?d00001 diff --git a/action/terrain/country.ltk b/action/terrain/country.ltk new file mode 100644 index 0000000000000000000000000000000000000000..524f946b2c596b39a3ffe5aff47d9d0108d55f2c GIT binary patch literal 17148 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 literal 0 HcmV?d00001 diff --git a/action/terrain/countryside.ltk b/action/terrain/countryside.ltk new file mode 100644 index 0000000000000000000000000000000000000000..7e733deb9d4fde3548bdd57e95e116b02413d8eb GIT binary patch literal 22530 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 CX|0O@ literal 0 HcmV?d00001 diff --git a/action/terrain/matrix2.ltk b/action/terrain/matrix2.ltk new file mode 100644 index 0000000000000000000000000000000000000000..3b89c9022d3627d0cc81511bfb45a66818333029 GIT binary patch literal 15628 zcmeI2Yls|G7ROJ_XeO`8n1`9XJFm(6F_~l@iL;q>H5yRNNHio%Tt-Foi%VD$M8&No zF|5WV0}CSSLM9N_)eY+?ib4#Tq#Hp5@x!;K=kPW7v*rzd@T zc0WvCs!r9tx6beWPj%PSJ$LFvq#N7+?sGUPy~k7HPfFEe5R4V$Tx>&Mec)W5e(z=AoF@axlqA+>ndFg6<~-Q1Z+gt*_qX}+UpgCu`2Y3f z&v-WpnBHr44LGROP0pMwUxwoo*c&Cm2MkbY9f=m znqdDuxyRY_N5?LMe@V;s*Fq*aeDchb^<*akYuA(yO$u%}vI^%lW4 zfB29yw#e0s((_cvB!^tGG#0L{Y`v70`aL0&n#iQC#jwvDc-XrY_SluB&c=YhV~FjP zkVy`?WG#Wc^80~g7wjjKXSrkcyqA{V&+zxmes9Skm#n3*-`qA7JmXzHZ;z*?|5H5s zER!5^$yx^c?WDeBE9~j6Ke*HB-=3D?%OR5-a>+`-zSHxtGnRfArGH;q2JiKx@sHW( zEji?pwH)@b^(WlN)_)%NvnOPdlL%NVU~l|B>0W2wb7}b)v27g6>LiC;vR1;LJ$S4J zI`2+PA9w+i9CFE81)JHs-@VTMpWFLcUx)dF2sz}EwHo%@1J~T^9C;PYUwINR$sw1l zX4oxPUv#hY$Y0aaRG(5NIpmVH2KKYWYHu@a$6arD+h9{&JFEWNhL_olG&Q88(Z-e> za>-f?`@rBiZyoHFE61Gam67{0^6t!#Ne;PW>2KJzm4nm3+!r#biA?HR54(T=l+*^; zH3XQZ(!WRgQJS+4b2fBpsO`ynka{WfG$6PeVt5%${Y_q|Q9Lr44E>1?_` zBlXXPOmfI2t2R2}Tn?q>>7yZ&n#iOs{zeWxc*Z$f2A{nomvi@%9R61LdnBLCEwCq~ z*BM*<7n5@E7okr2Czp(^um^SzCR<<+CEj$$?49b#6L{|no(bfTOIFS9XaCHwl>6^c_e)9Hdw<9zhg`BcV6RR)pUs2q``0VkJj$1M zoyz7m`X0Idv53DH`nc|3b&^A_vL5*LNA7j@4|p=%5;8l%WA2UlJ+i5f<8a9qxpR+n z%n6yyLoq+ZTojp%rT(s5S@CCh`Ap`am|Dm)T$>oPQzvcYXdbwuO1$3bAWz4-tljgd7Ns1Y z?1t>tAe~sPbyL)nwefsLdp?11I5o1&hqjpsLl-H3FT-CJAQoAs4_=&P`lzivA` zZ^wLOR$zHndPP)SX)o5$S7C|nr?s;|n~ir_dv4FP?fNXe<(Kkac7eQ0xTnqS%U@Nl zmEQ78xxU8C1#>Rupjrygw6_|ke5_MynTsQ1 zZVAt|zB+3;Q6nhUZ*>;Nq&n@qI$nPh+M2Lts%0LI>=jz$!jciC-k1fbUx2ktEj+Wa zF09w~>$8>VYT;Ra53AFdWmv1Nq^O^qwGmxb;5A1mr{DInI*mzS z<#zN^8Wrm$5yNMz9UIlzzQud1+nD88#~rO$FS$smujMhmwY|Kxx{X&d-Xf z_^REQm005~SK|09tK_S8V^(3sH@?L2S5?hd?Z&Le`fpr`J#x(zo$I*0`wVNcr7_+hHJesx1*~m1~YK_q&+Gsla#I=mIelu1E3yU$i z^Ehvto$M#%#^j#_?O6@l|QDwK0g}yVJi-;)*fbVpYbs z#Em$vKKk#Dt&O{ef3c$5H2eGh$HmnUZXGF`h;(obz z*-DPAKl-wz*jwRwasH#)Ob4$o&7s8SKibi|9qZJLKP}G+N60hYrOH0mkM&7^QQq=B zb-;6naNj07QFxZ$KGtK#yd!%b>D78H=c5y}PUKzL$Bxz>m%FBXTRXg|r_uFm#@pt< z+nr^=>4`qA$M_NJ{b!Gxv5p^o|KqPOZK39Nc(U5*vD_!0Z?72b8&l33+bm!AqxFu; zzu0SZ{$H)NxtF;7i_Y;iqmiGfC~ng}W3|*nU#>s9@qYr9`mDTXr5^eAJC?WFp8yrG B5MTfR literal 0 HcmV?d00001 diff --git a/action/terrain/terminal.ltk b/action/terrain/terminal.ltk new file mode 100644 index 0000000000000000000000000000000000000000..751c1cfb32f73ff30aa763d5fb2c767414597bd1 GIT binary patch literal 168858 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 literal 0 HcmV?d00001 diff --git a/action/terrain/urban.ltk b/action/terrain/urban.ltk new file mode 100644 index 0000000000000000000000000000000000000000..58d471e40f784c7911f14d73366c753926700846 GIT binary patch literal 95130 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 literal 0 HcmV?d00001 diff --git a/action/terrain/urban2.ltk b/action/terrain/urban2.ltk new file mode 100644 index 0000000000000000000000000000000000000000..4133cb41b401f19caa5f2d0186d73afc1e49c0f0 GIT binary patch literal 82380 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
      fe*roFJ3XCK2nyiB4>dm5evI*yG-)hC4V zY`JMK%yFy}RhP?hzrWVDL)5Pg|5sh%_t#o~qkg4lW3o87Ka}4AzXKlaz)DG!M@!vL zWj!N}wVx94Bz7hCBc7Fbd9u*`R203d`kXY>eoDoY*r;<=%OxgH7P_B`a(t`0N*wK{ zR6L1o=-Sm1l_v|`PenPNWmH`w)P73EqgcnajH+v8t>0hETRQ4m$Ggw_{k7cRsB7t5 z-@Vpb+vI@jWxem>COc!}%5~^%h2LKrt!#1(-@Q$?#rl;SWuxz6>s@sQ+<@M0^84$w g!+P}HYrUoZfcvEbFUpJlJmG5Rf8qDn)&8pfKh~Z-od5s; literal 0 HcmV?d00001 diff --git a/action/terrain/urban3.ltk b/action/terrain/urban3.ltk new file mode 100644 index 0000000000000000000000000000000000000000..cdecfafdf3b195ad6de1933499f35b2e7079f09e GIT binary patch literal 76738 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 Hmx}y9;FIn| literal 0 HcmV?d00001 diff --git a/action/terrain/village2.ltk b/action/terrain/village2.ltk new file mode 100644 index 0000000000000000000000000000000000000000..d592e1e648b555d35ddb35d9f1f4f97007b07001 GIT binary patch literal 30588 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 literal 0 HcmV?d00001 diff --git a/action/terrain/wfall.ltk b/action/terrain/wfall.ltk new file mode 100644 index 0000000000000000000000000000000000000000..0f0f10ea8f07fdf7c01194902f1dc405319c1dae GIT binary patch literal 99580 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 literal 0 HcmV?d00001 diff --git a/action/tng/act2r2.esp b/action/tng/act2r2.esp new file mode 100644 index 000000000..aae081fb5 --- /dev/null +++ b/action/tng/act2r2.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Casino Cashout + +[respawn] +red=8 +blue=8 + +[spawns] +red=<1160 986 334 -82>,<1058 718 430 -45>,<1558 -234 238 130>,<1157 -216 336 55> +blue=<-1086 -683 35 -45>,<-444 -778 30 154>,<-107 1461 430 -100>,<-412 942 62 104> + +[target] +escort=<-871 807 174> +name=Briefcase full of money + +[red_team] +name=Thugs +leader_name=Pepper Jack +skin=male/aqgthug +leader_skin=male/pimp + +[blue_team] +name=Casino Security +leader_name=Frank The Cop +skin=male/sabotage +leader_skin=male/bravo diff --git a/action/tng/actcity2.aqg b/action/tng/actcity2.aqg new file mode 100644 index 000000000..716257348 --- /dev/null +++ b/action/tng/actcity2.aqg @@ -0,0 +1,54 @@ +# +# Map: actcity2.aqg +# Build: 2 +# Creator:-RS-Phreedh +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# You need ML version 3.19 or later, for this to work. +# + -268: 1459: 508: 270: 117: 125:in the fenced area on the ledge + -8: 1063: 263: 513: 294: 265:in the fenced area + -91: 386: 124: 164: 379: 132:at the junction + -648: -310: 270: 738: 298: 273:on Casino Square +-1522: -220: 73: 130: 99: 56:in the small room by Casino Square +-1442: -355: 409: 80: 251: 133:on the roof by Casino Square + -800: 81: 218: 349: 301: 185:in the Casino Royale stairs + -847: 524: 199: 300: 132: 69:inside Casino Royale + -741: -790: 262: 374: 181: 269:at the construction site +-1032: -919: 61: 85: 35: 85:behind the rubble at construction site + -615: -781: 213: 227: 174: 58:at the construction site, lvl 2 + -613: -785: 360: 229: 170: 51:at the construction site, lvl 3 + -623: -782: 498: 234: 173: 53:at the construction site, top lvl + 58: 653: 471: 110: 129: 70:on the roof by the fenced area + 51: 419: 362: 250: 106: 86:in the room above the junction + 914: 378: 304: 243: 128: 113:on Eastern Ave by the freeway + 148: 353: 136: 179: 147: 120:on Eastern Ave by the junction + 549: 369: 221: 574: 139: 207:on Eastern Ave + 861: 639: 294: 128: 121: 79:in Crossor bldg, lobby + 840: 871: 381: 79: 79: 160:in Crossor bldg, spiral stairs + 873: 651: 451: 135: 121: 79:in Crossor bldg, 2nd floor + 1076: 693: 443: 64: 140: 63:in Crossor bldg, small room + 499: 642: 338: 177: 100: 166:in the secret room + 496: 628: 479: 172: 124: 64:on the roof above the secret room + 822: 88: 352: 177: 105: 55:in the Cult Cola room + 894: 119: 482: 236: 116: 76:on the roof above the Cult Cola room + 1382: 250: 375: 266: 582: 191:on the freeway + 1208: 953: 351: 136: 106: 64:in the room above the freeway + 1213: -209: 368: 81: 80: 185:outside Motel Mancis + 857: -224: 231: 262: 132: 59:inside Motel Mancis diff --git a/action/tng/actcity2.esp b/action/tng/actcity2.esp new file mode 100644 index 000000000..235ef3bdd --- /dev/null +++ b/action/tng/actcity2.esp @@ -0,0 +1,37 @@ +[esp] +type=etv +author=darksaint +name=Show No Mercy + +[respawn] +red=10 +blue=10 +green=10 + +[spawns] +red=<-1087 1552 302 -90>,<8 952 46 -90>,<200 1369 430 -92> +blue=<-1311 -831 62 0>,<160 -1183 814 135>,<-1445 -1264 494 87> +green=<-1311 -831 62 0>,<160 -1183 814 135>,<-1445 -1264 494 87> + +[target] +escort=<-1311 -831 62 0> +name=Casino + +[red_team] +name=The B-Team +leader_name=Mister Bigtime +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Peacekeepers +leader_name=Frankie Blueshoes +skin=male/ctf_b +leader_skin=male/ctf_vip_b + +[green_team] +name=Landscaping Crew +leader_name=Professor G +skin=male/ctf_g +leader_skin=male/ctf_vip_g + diff --git a/action/tng/actcity2.flg b/action/tng/actcity2.flg new file mode 100644 index 000000000..5a1c9c628 --- /dev/null +++ b/action/tng/actcity2.flg @@ -0,0 +1,4 @@ +#actcity2 +<1376 -146 222> +<-858 248 174> + diff --git a/action/tng/actcity3.aqg b/action/tng/actcity3.aqg new file mode 100644 index 000000000..c835420ae --- /dev/null +++ b/action/tng/actcity3.aqg @@ -0,0 +1,29 @@ +# Map: actcity3.aqg +# Build: 2 +# Creator:[AMC]Mloclam +249: 1172: -117: 149: 90: 80:in the bathroom +-334: 998: -111: 149: 268: 86:in the lounge +-20: 1003: -111: 148: 265: 77:in the bedroom +-284: 1344: -63: 223: 67: 134:on the steps by the bedroom +-1697: 328: 200: 545: 595: 270:by the bridge +-2502: -76: 303: 251: 189: 89:by the boarded-up door +-3350: 195: 175: 298: 363: 128:in the pillar room +-3159: -235: 153: 67: 176: 75:on the steps by the pillar room +-2857: -333: 225: 252: 59: 165:on the steps by the pillar room +388: 777: -42: 258: 71: 200:on the ladder from the bedroom +-3004: 408: 323: 255: 599: 142:by the cola sign +-944: 1405: -82: 479: 621: 130:in the courtyard +-892: 1463: 272: 897: 555: 241:overlooking the courtyard +-3226: 955: 158: 179: 393: 71:in the cafe +838: 512: 257: 193: 324: 242:above the fire escape ladder +-319: 127: 329: 191: 63: 137:in the courtyard by the office +190: -191: 275: 324: 385: 234:in the courtyard by the office +-1339: -366: 341: 200: 90: 168:by the bridge +-356: 1134: -131: 49: 55: 77:tabledancing in the lounge +-2224: 1190: 263: 588: 419: 283:by the opening windows +-1112: 452: -308: 1901: 757: 792:in the chasm +-633: -155: 359: 516: 384: 167:near the office +-267: -50: 257: 126: 103: 78:in the office +-1568: 1878: -12: 180: 175: 141:praising [AMC]Mloclam +-1569: 1895: 323: 184: 187: 209:overlooking the courtyard +-2973: 1190: 283: 217: 185: 257:by the cafe diff --git a/action/tng/actcity3.dom b/action/tng/actcity3.dom new file mode 100644 index 000000000..8aa8bd631 --- /dev/null +++ b/action/tng/actcity3.dom @@ -0,0 +1,2 @@ +[dom] +flags=<-3142 466 344>,<930 512 152>,<-1983 1024 24> diff --git a/action/tng/actcity3.flg b/action/tng/actcity3.flg new file mode 100644 index 000000000..f178da9d0 --- /dev/null +++ b/action/tng/actcity3.flg @@ -0,0 +1,3 @@ +#actcity3 +<-3156 391 366> +<710 507 174> diff --git a/action/tng/aggression.aqg b/action/tng/aggression.aqg new file mode 100644 index 000000000..4cd2c7189 --- /dev/null +++ b/action/tng/aggression.aqg @@ -0,0 +1,42 @@ +# +# Map: agression.aqg +# Build: 1 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +0: 1624: 213: 384: 154: 229:=AR= building roof +4: 1497: -106: 637: 282: 55:road (by =AR= building) +-771: 1456: -95: 116: 162: 66:newbies camp here room +771: 1535: 250: 132: 257: 241:police building (roof) +7: 1200: 288: 888: 51: 90:long strip of roof +-729: 1487: 98: 152: 209: 129:top of newbies camping room +-1310: 0: 113: 226: 370: 83:grass area +-1368: 0: 325: 268: 354: 75:grass area (ledge) +-1127: 690: 312: 40: 321: 39:secret tunnell +-961: 960: 295: 198: 48: 55:secret tunnell +-821: 1033: 280: 58: 116: 40:secret tunnell +449: 1600: -391: 46: 48: 216:sewer (by =AR= building) +378: 515: -439: 119: 1136: 170:sewer +-120: 1536: -390: 387: 113: 204:sewer +-15: -640: -252: 228: 243: 132:sniper building (basement) +0: -639: -4: 238: 239: 90:sniper building (1st floor) +-68: -636: 176: 177: 241: 64:sniper building (2nd floor) +0: -28: 95: 193: 351: 159:road (by sniper building) +-640: -2: 18: 445: 125: 85:road (between grass & sniper building) +0: 767: -40: 125: 443: 116:road (between =AR= building & sniper building) \ No newline at end of file diff --git a/action/tng/aggression.flg b/action/tng/aggression.flg new file mode 100644 index 000000000..d33654f71 --- /dev/null +++ b/action/tng/aggression.flg @@ -0,0 +1,3 @@ +#aggression +<-1200 5 62> +<-487 1360 -129> diff --git a/action/tng/aq2ctf1.ctf b/action/tng/aq2ctf1.ctf new file mode 100644 index 000000000..dca787800 --- /dev/null +++ b/action/tng/aq2ctf1.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=4 +blue=4 + +[flags] +red=<1565 3538 254> +blue=<2323 -329 254> + +[spawns] +red=<1680 3152 254 -40>,<2304 2624 254 0>,<2096 2176 254 60> +blue=<2240 48 254 0>,<1616 576 254 0>,<1824 1024 254 0> \ No newline at end of file diff --git a/action/tng/arabtown.ctf b/action/tng/arabtown.ctf new file mode 100644 index 000000000..4c4e6fca8 --- /dev/null +++ b/action/tng/arabtown.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<1632 175 320> +blue=<-562 -2255 192> + +[spawns] +red=<1325 463 320 -90>,<1382 -46 56 -178>,<-200 708 192 -88> +blue=<-767 -1622 192 2>,<-317 -2034 56 85>,<700 -2257 320 87> \ No newline at end of file diff --git a/action/tng/arabtown2.ctf b/action/tng/arabtown2.ctf new file mode 100644 index 000000000..d23fb55e4 --- /dev/null +++ b/action/tng/arabtown2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<1391 1132 78> +blue=<-989 -1084 48> + +[spawns] +red=<1091 831 -4 -179>,<911 1344 78 161>,<1468 828 258 -177>,<125 173 136 175> +blue=<-416 -1317 210 90>,<-1235 -665 264 1>,<-1041 -415 48 134>,<125 173 136 1> \ No newline at end of file diff --git a/action/tng/armyterr.flg b/action/tng/armyterr.flg new file mode 100644 index 000000000..245e67b79 --- /dev/null +++ b/action/tng/armyterr.flg @@ -0,0 +1,3 @@ +#armyterr +<190 1831 110> +<105 525 46> diff --git a/action/tng/artmuseum.ctf b/action/tng/artmuseum.ctf new file mode 100644 index 000000000..1fe89200f --- /dev/null +++ b/action/tng/artmuseum.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=2 +blue=2 + +[flags] +red=<-592 895 370> +blue=<-591 -1241 370> + +[spawns] +red=<-1048 989 508 -89>,<-156 985 248 -90>,<-945 359 -16 -90> +blue=<-167 -1359 508 89>,<-1041 -1356 248 90>,<-262 -728 -16 90> \ No newline at end of file diff --git a/action/tng/assassin2.ctf b/action/tng/assassin2.ctf new file mode 100644 index 000000000..6f2b27649 --- /dev/null +++ b/action/tng/assassin2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=3 +blue=4 + +[flags] +red=<1200 1092 574> +blue=<3071 2971 862> + +[spawns] +red=<890 1450 574 90>,<1320 963 574 -0>,<896 2490 430 -89> +blue=<2510 2852 910 90>,<1849 1966 862 -180>,<3133 1983 430 -179> \ No newline at end of file diff --git a/action/tng/asylum.aqg b/action/tng/asylum.aqg new file mode 100644 index 000000000..f548e8045 --- /dev/null +++ b/action/tng/asylum.aqg @@ -0,0 +1,49 @@ +# +# Map: asylum.aqg +# Build: 1 +# Creator:Marx (AFH^BiG PIG) +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + 163: 125: 77: 608: 68: 76:in hallway at level 3 + 384: -148: -66: 130: 200: 65:in jail at level 2 + 148: 120: -63: 599: 71: 67:level 2 hallway + 152: 186: -194: 597: 133: 62:inside level 1's hallway + 224: -150: -201: 178: 187: 68:in level 1 first room to the left + -544: -174: -200: 256: 225: 71:level 1's biggest room + 155: 122: -66: 593: 63: 62:inside level 2's hallway + 90: -694: -65: 481: 83: 63:inside level 2's 2nd hallway + 125: -278: -65: 136: 332: 65:near jail's at level 2 + 0: 287: -64: 394: 94: 61:inside one of the room's at right side at level 2 + 128: -893: -65: 392: 135: 65:inside one of the room's to the left at level 2 + 388: -766: 65: 381: 168: 64:in computer hallway at level 3 + 380: -280: 60: 386: 318: 67:near sofa's at level 3 + 258: 296: 63: 132: 104: 62:in room 27 at level 3 + 1: 286: 72: 130: 98: 73:in room 26 at level 3 + -263: 285: 65: 130: 100: 67:in room 25 at level 3 + -200: -215: 94: 194: 267: 97:inside hospital at level 3 + 57: -39: 63: 66: 95: 67:inside reception at level 3 + -203: -170: -66: 193: 224: 63:in post-mortem room at level 2 + 585: 257: -222: 52: 61: 99:inside elevator shaft (level 1) + 585: 257: -62: 50: 55: 62:inside elevator shaft (level 2) + 585: 257: 129: 50: 60: 126:inside elevator shaft (level 3) + 638: -59: -190: 77: 115: 53:inside stairway area at level 1 + 639: -64: -74: 76: 116: 63:inside stairway area at level 2 + 640: -59: 63: 76: 112: 64:inside stairway area at level 3 diff --git a/action/tng/asylum.flg b/action/tng/asylum.flg new file mode 100644 index 000000000..5149eeb15 --- /dev/null +++ b/action/tng/asylum.flg @@ -0,0 +1,3 @@ +#asylum +<49 144 -209> +<240 -221 46> diff --git a/action/tng/avenue1.aqg b/action/tng/avenue1.aqg new file mode 100644 index 000000000..e0f041b4a --- /dev/null +++ b/action/tng/avenue1.aqg @@ -0,0 +1,51 @@ +# +# Map: avenue1.aqg +# Build: 2 +# Creator:Lurker [NW], Mike [NW] +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +862:329:388:985:497:418:0:in the Main Area +352:28:15:84:121:86:in Steam Room - Main building +233:261:248:207:107:324:in Stairs - Main building +322:481:718:354:356:76:on Roof - Main building +773:275:305:117:126:95:by Pool - Main Area +312:600:330:306:211:58:on 1st Floor - Main building +321:608:553:315:218:60:on 2nd Floor - Main building +315:604:98:311:221:118:by Limmo +1240:351:51:609:484:67:in Garage - Main Area +1576:-446:184:235:326:224:in Tunnel to Garage +790:482:285:119:77:102:by Entrance to Main building +1072:-525:166:138:342:133:in Tunnel to Main Area +1359:-2132:483:458:655:310:in the Industrial Area +1075:-1173:166:141:305:133:in Tunnel to Industrial Area +1234:-2132:676:385:315:80:on Tall Roof - Factory +1035:-2602:497:137:166:68:on Lower Roof - Factory +1371:-2581:236:94:54:44:inside Cargo Truck +1234:-2142:362:338:263:201:inside the Factory +1024:-2582:282:107:147:123:inside the Factory +1139:-1081:509:494:195:205:by the Music Store +1695:-1222:326:63:255:153:in Stairs to Music Store +676:-1532:366:130:243:83:in Office by Music Store +701:-640:389:194:353:218:in Tunnel from Music Store +817:-582:507:63:313:235:in Tunnel to Music Store +186:-1420:220:360:999:328:in the Bank Area +192:-1980:216:287:71:78:on the Bank-Balcony +254:-305:234:253:114:61:in Tunnel to Bank Area diff --git a/action/tng/avenue1.flg b/action/tng/avenue1.flg new file mode 100644 index 000000000..7824951ad --- /dev/null +++ b/action/tng/avenue1.flg @@ -0,0 +1,3 @@ +#avenue1 +<77 480 334> +<1027 -2340 510> \ No newline at end of file diff --git a/action/tng/backyard.ctf b/action/tng/backyard.ctf new file mode 100644 index 000000000..387131523 --- /dev/null +++ b/action/tng/backyard.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=3 +blue=3 + +[flags] +red=<2954 964 222> +blue=<973 2589 94> + +[spawns] +red=<2688 1024 94 135>,<2633 1559 94 -47> +blue=<1018 2146 94 92>,<1400 2596 94 -179> \ No newline at end of file diff --git a/action/tng/bank.aqg b/action/tng/bank.aqg new file mode 100644 index 000000000..ecb4e9e23 --- /dev/null +++ b/action/tng/bank.aqg @@ -0,0 +1,11 @@ +# Map: bank.aqg +# Build: 1 +# Creator:[AMC]Mloclam +150: 527: 271: 705: 1528: 684:outside +126: -14: -91: 245: 519: 116:in the lobby +129: 396: 202: 689: 1686: 59:on the roof +98: -343: 35: 189: 87: 115:by the stairs +13: 10: 65: 352: 515: 83:on the top floor +-215: -81: -79: 152: 433: 105:by the vault +-201: 364: -55: 128: 124: 102:in the vault +4: -469: 20: 348: 28: 172:on the ramp diff --git a/action/tng/barnyard.esp b/action/tng/barnyard.esp new file mode 100644 index 000000000..00769f68d --- /dev/null +++ b/action/tng/barnyard.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Get the Hay out of here! + +[respawn] +red=6 +blue=6 + +[spawns] +red=<1039 -1974 -742 180>,<330 -542 -870 -42>,<833 -258 -850 -90><787 -697 -870 145> +blue=<2351 -1650 -742 -180>,<1674 -2372 -742 85>,<2355 -959 -615 -140><2104 -431 -326 -142> + +[target] +escort=<1428 -1090 -694> +name=Haybale on the truck + +[red_team] +name=Ranchers +leader_name=Old MacDonald +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Farmers +leader_name=John Deere +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/beach.ctf b/action/tng/beach.ctf new file mode 100644 index 000000000..765e7e5ec --- /dev/null +++ b/action/tng/beach.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=4 +blue=4 + +[flags] +red=<2536 -1485 270> +blue=<88 -368 406> + +[spawns] +red=<2457 -425 306 180>,<2440 -1216 94 -180>,<2272 -400 94 -90> +blue=<709 -43 350 -90>,<116 -732 94 0>,<1352 -144 190 -90> \ No newline at end of file diff --git a/action/tng/beer.aqg b/action/tng/beer.aqg new file mode 100644 index 000000000..569fbf38c --- /dev/null +++ b/action/tng/beer.aqg @@ -0,0 +1,40 @@ +# +# Map: beer.aqg +# Build: 1 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +260: -370: 540: 527: 511: 246:roof of house +1077: -318: 218: 264: 319: 61:see through roof +1524: -616: 530: 143: 777: 203:camper roof (by consortium billboard) +1099:-1056: 519: 240: 308: 313:roof with 4 boxes +1297: 321: 516: 421: 112: 223:telephone pole (about to fall to my death) +1083: -339: 70: 313: 424: 62:grass area (by picnic tables) +624: -332: 80: 126: 291: 63:(house) (1st floor...room with 2 tables) +194: -333: 76: 281: 288: 59:(house) (1st floor...by bar) +267: -586: 222: 495: 254: 61:(house) (2nd floor...by couches) +247: -346: 227: 499: 70: 62:(house) (2nd floor...south side) +184: 210: 218: 593: 79: 68:(house) (2nd floor...on tin roof) +-328: -370: 239: 79: 494: 47:(house) (2nd floor...on tin roof) +-144:-1154: 382: 530: 292: 379:outside by keg +-375: -164: 83: 267: 690: 65:(road...south side) +792: 341: 67: 873: 195: 69:(road...by tall bushes) +618:-1258: 107: 234: 130: 103:outside by R.I.P. +567: -985: 72: 179: 121: 64:garage (I don't know why) +336: -738: 73: 415: 98: 57:(house) (1st floor...long hallway) \ No newline at end of file diff --git a/action/tng/beer.flg b/action/tng/beer.flg new file mode 100644 index 000000000..d82885a52 --- /dev/null +++ b/action/tng/beer.flg @@ -0,0 +1,3 @@ +#beer +<-483 355 30> +<1233 -1217 262> \ No newline at end of file diff --git a/action/tng/blockwar.aqg b/action/tng/blockwar.aqg new file mode 100644 index 000000000..1668f29d4 --- /dev/null +++ b/action/tng/blockwar.aqg @@ -0,0 +1,63 @@ +# +# Map: BLOCKWAR.aqg +# Build: 2 +# Creator:Mike [NW] +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + -603:-3356: 304: 35: 34: 85:inside hotel room on 2nd floor + 444:-3826: 311: 66: 79: 72:at the base of stairs leading up to roof in the hotel +-1663: 1060: 377: 41: 38: 37:on gunshack roof +-1661: 1190: 206: 60: 54: 82:in the corridor inside gunshack + -183:-3590: 318: 1162: 300: 93:on 2nd floor in hotel + 824:-3517: 219: 151: 223: 158:in the stairs between 1st and 2nd floor in the hotel + 390:-3536: 330: 144: 200: 90:in the stairs between roof and 2nd in the hotel + -785:-3308: 312: 564: 567: 78:on 2nd floor in hotel + -365:-3573: 124: 1177: 302: 70:on 1st floor in hotel + 189:-3422: 514: 854: 485: 104:on hotel roof + 635:-2997: 298: 828: 283: 301:outside the hotel + 290:-2319: 17: 490: 390: 248:on the garage ramp + 353:-3238: 106: 79: 76: 69:near front door on the hotel + 1164:-2378: 203: 300: 488: 185:near the gate + 1169:-1677: 305: 294: 243: 231:near the gate + 1165: -770: 321: 278: 744: 231:on the road near gate + -29: -320: 311: 897: 361: 229:on the road between gate and gunshack + -642: -222: 349: 264: 485: 244:on road near gunshack +-1254: 996: 322: 892: 800: 234:outside the gunshack +-1313: 983: 438: 727: 541: 129:on gunshack roof +-1607: 549: 208: 66: 79: 95:outside gunshack, near the container +-1590: -157: 314: 80: 391: 206:in the alley near gunshack +-1916: -473: 322: 406: 75: 210:in the alley near gunshack +-2245: -795: 31: 70: 246: 207:in the alley above the sewer ladder +-1571: -886: 205: 556: 329: 90:in the corridor outside generator room +-1214: -289: 217: 239: 424: 121:in generator room +-1324:-1233: 101: 238: 191: 187:in the corridor stairs +-1615:-1238: -234: 236: 188: 124:in the stairs to the sewers +-1615:-1404: -289: 232: 370: 116:below stairs to the sewers +-1508: -695: -528: 983: 1203: 330:in the sewers +-2235: -827: -176: 55: 63: 286:in sewers near the ladder up +-1015:-1352: 0: 221: 632: 95:in corridor behind the garage + 16:-1411: -27: 823: 551: 203:in the garage +-1097: 866: 194: 462: 388: 104:inside the gunshack +-1872: 1313: 199: 149: 197: 97:inside storage room in gunshack +-1796: 1146: 207: 80: 365: 99:inside storage room in gunshack +-1653: 1068: 207: 73: 439: 117:in the corridor inside gunshack +-1800: 1001: 388: 71: 385: 133:in corridor leading to the roof in gunshack +-1728: 1309: 396: 144: 68: 103:near the door leading to the roof in gunshack diff --git a/action/tng/border.flg b/action/tng/border.flg new file mode 100644 index 000000000..905ada53f --- /dev/null +++ b/action/tng/border.flg @@ -0,0 +1,3 @@ +#border +<35 82 14> +<202 -1602 14> diff --git a/action/tng/bwcity2.ctf b/action/tng/bwcity2.ctf new file mode 100644 index 000000000..b3b5cc148 --- /dev/null +++ b/action/tng/bwcity2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<3147 3478 438> +blue=<770 994 366> + +[spawns] +red=<3456 3136 430 -180>,<3624 2449 438 -180>,<3678 2837 774 -180> +blue=<768 1088 366 90>,<958 3028 838 -90>,<1186 1972 374 180> \ No newline at end of file diff --git a/action/tng/bxtrain2.flg b/action/tng/bxtrain2.flg new file mode 100644 index 000000000..c6254e344 --- /dev/null +++ b/action/tng/bxtrain2.flg @@ -0,0 +1,3 @@ +#bxtrain2 +<-55 -728 50> +<-4 1746 50> diff --git a/action/tng/chasm.aqg b/action/tng/chasm.aqg new file mode 100644 index 000000000..0cf983405 --- /dev/null +++ b/action/tng/chasm.aqg @@ -0,0 +1,38 @@ +# +# Map: chasm.aqg +# Build: 1 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +-3: 137: -4: 247: 150: 59:bridge +-387: 117: 47: 123: 150: 104:entry way (low side) +-574: 114: 41: 47: 143: 107:deadman's bottleneck (low side) +384: 147: -9: 110: 64: 52:entry way (high side) +403: -96: 119: 100: 159: 182:green room of death (high side) +331: 160: 245: 166: 94: 53:lookout tower (high side) +191: -40: 244: 80: 118: 50:lookout tower (high side) +278: 491: 41: 202: 156: 103:purple room (high side) +-325: 380: 92: 188: 118: 48:(platform of death (low side) +-400: -153: -19: 222: 105: 171:cubby hole (low side) +-384: 171: -139: 128: 205: 55:camper room (low side) + + + + + diff --git a/action/tng/city.aqg b/action/tng/city.aqg new file mode 100644 index 000000000..d3d488b42 --- /dev/null +++ b/action/tng/city.aqg @@ -0,0 +1,54 @@ +# +# Map: city.aqg +# Build: 1 +# Creator:Ellusion [NW] +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +-12:-995:97:712:562:137:outside the Police Station +3:-1212:410:322:255:144:on the Police Station roof +-4:-258:270:264:185:306:near the big Karlsson Sign +-191:513:383:516:192:208:on the Chinatown roof +4:315:78:708:380:99:outside Chinatown +9:910:256:718:210:276:behind Chinatown +1055:281:225:289:232:114:in the Blue apartment +971:615:277:256:112:282:in the Blue apartment alley +857:405:399:344:101:159:on the roof above Blue apartment +-934:-195:135:223:258:168:in the alley from Pizzeria Square +-1603:-962:265:517:517:286:in Pizzeria Square +1294:-530:274:410:201:297:in the Road-end by Backyard +1085:-255:270:191:188:293:in the Backyard +995:-35:275:290:36:297:in the small alley behind Backyard +1980:-900:264:260:575:292:near the big PG Sign +1210:-1276:280:506:193:277:in Clint E alley by the Police +-870:385:251:167:272:286:in Stairs to Chinatown roof +-861:683:459:157:202:97:on the Chinatown roof near Stairs +-637:-279:439:122:156:77:in the Karlsson room +-1536:-288:471:383:158:107:on the Pizzeria Square roof +-1677:-318:50:271:123:63:near Stairs to Pizzeria roofs +-2098:-249:242:144:120:273:in Stairs to Pizzeria roofs +-1055:-961:359:30:511:193:on low roof in Pizzeria Square +1376:-1659:213:164:127:277:in Stairs by PG Sign +1667:-1570:53:127:98:80:close to Stairs by PG Sign +1874:-1619:469:307:143:105:on Roof above Clint E alley +1796:-1649:294:252:150:59:in the apartment by Clint E alley +1373:-808:442:349:276:80:on roof above PG Sign +-895:-1284:38:186:201:72:close to Pizzeria Square +-920:-273:439:165:143:76:in apartment near the Karlsson room diff --git a/action/tng/city.flg b/action/tng/city.flg new file mode 100644 index 000000000..2fb5060a0 --- /dev/null +++ b/action/tng/city.flg @@ -0,0 +1,3 @@ +#city +<-1564 -949 46> +<1892 -899 38> diff --git a/action/tng/cliff.aqg b/action/tng/cliff.aqg new file mode 100644 index 000000000..3f78590f3 --- /dev/null +++ b/action/tng/cliff.aqg @@ -0,0 +1,44 @@ +# +# Map: cliff.aqg +# Build: 1 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# +1175:830:174:219:71:157:behind highbase trainstation +1175:699:186:245:71:118:inside highbase trainstation +1076:502:473:373:136:361:outside highbase trainstation +745:-408:248:792:788:564:up on highbase mountain +602:-483:-197:288:630:105:down in highbase entry +563:-288:-101:72:100:238:up on highbase entry ladder +643:552:-187:434:451:245:down in highbase hole +745:625:-103:78:29:263:up on highbase hole ladder +1126:608:422:232:165:129:up on highbase trainstation +196:501:391:1053:57:71:up on the train line +79:529:10:309:346:196:up middle mountain +-591:-168:-173:334:1097:152:down on lowbase ground level +-143:-248:197:156:517:299:up on the ledges +-1093:43:340:266:954:347:up on lowbase mountain +-748:840:155:111:74:160:behind lowbase trainstation +-646:536:155:319:210:185:outside lowbase trainstation +-760:702:142:188:65:56:inside lowbase trainstation +-752:581:341:187:169:101:up on lowbase trainstation +184:510:165:771:74:104:inside the train +134:-846:178:241:261:28:up on ledge over higbase entry +-12:-868:-195:381:132:162:down in highbase entry passage diff --git a/action/tng/cliff.dom b/action/tng/cliff.dom new file mode 100644 index 000000000..400884f83 --- /dev/null +++ b/action/tng/cliff.dom @@ -0,0 +1,2 @@ +[dom] +flags=<800 416 160>,<-400 416 88>,<64 -928 -224> diff --git a/action/tng/cliff2.flg b/action/tng/cliff2.flg new file mode 100644 index 000000000..9ac75d4f8 --- /dev/null +++ b/action/tng/cliff2.flg @@ -0,0 +1,3 @@ +#cliff2 +<-312 79 227> +<-394 -1692 506> diff --git a/action/tng/cloud.aqg b/action/tng/cloud.aqg new file mode 100644 index 000000000..2a46624a2 --- /dev/null +++ b/action/tng/cloud.aqg @@ -0,0 +1,37 @@ +# +# Map: cloud.aqg +# Build: 1 +# Creator: =OG=7 +# Comments: map is preety bland descriptions wise...I did my best :P +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +1407: 126: 377: 425: 161: 206:roof (middle building) +1108: -488: -24: 293: 126: 143:brick camper building +1712: -528: 350: 310: 157: 202:roof (tallest building) +1834: 1046: 200: 205: 251: 372:roof (brick building w/ramp) +973: 916: 317: 249: 380: 280:roof (no way down buildings) +1407: 919: 356: 131: 378: 285:roof (building w/overpass) +1370: 431: -122: 655: 132: 51:road (middle) +1927: -16: -99: 94: 305: 71:road (fenced alley) +1266: -194: -174: 558: 163: 1:road (by brick camper building) +1268: -192: -114: 563: 156: 60:road (by brick building) +1127: 931: -142: 149: 377: 30:road (middle) +1650: 1051: -130: 371: 244: 40:road (by building w/ramp) +1609: 676: -127: 75: 130: 43:road + diff --git a/action/tng/colt.aqg b/action/tng/colt.aqg new file mode 100644 index 000000000..4151e43e8 --- /dev/null +++ b/action/tng/colt.aqg @@ -0,0 +1,80 @@ +# +# Map: colt.aqg +# Build: 1 +# Creator: [DP] Deathwatch +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +-1581:-1060: -225: 674: 62: 73:long hall in front of the toilets +-1983:-1352: -221: 257: 230: 74:women's toilets +-1680:-1363: -220: 46: 137: 77:inside women's toilets +-1321:-1366: -234: 300: 215: 68:men's toilets + -975:-1361: -233: 47: 137: 67:inside men's toilets +-1246: -835: -230: 336: 158: 71:blue pump room +-2001: -833: -224: 258: 159: 76:blue storage room +-1664: -553: -206: 66: 453: 95:hall to the toilets +-1643: 12: -174: 91: 107: 126:ladder down to the toilet area +-1395: 381: -111: 156: 476: 63:glass room - toilet side +-1228: 764: -127: 0: 31: 48:glass room - door +-1196: 315: -112: 33: 177: 61:glass room - opening in the glass +-1038: -48: -111: 124: 186: 63:glass room - outside door side +-1038: 565: -110: 124: 427: 64:glass room - office side + -670: 867: -94: 223: 120: 81:small office with desk + -389: 874: -95: 56: 127: 79:small office with desk + -63: 765: -94: 257: 236: 79:soda machine room + -604: 627: -95: 290: 107: 79:hall with office and outside doors + 315: 132: 37: 105: 318: 211:red room + 312: -225: 36: 108: 39: 212:red room - gas chamber door + 320: 472: 33: 108: 27: 209:red room - crate room door + 447: 764: 98: 239: 224: 144:crate room - high + 443: 759: -109: 235: 219: 63:crate room - low + 299: 524: 96: 90: 15: 142:crate room - ledge above door + -349: 761: 134: 547: 233: 106:locker room +-1011: 747: 159: 103: 127: 67:upper level entrance +-1081: 935: 163: 33: 60: 64:upper level - sloped corner +-1227: 256: 162: 104: 499: 67:upper level +-1018: -161: 163: 104: 81: 66:upper level - outside entrance +-1432: 377: 95: 105: 375: 1:upper level - offices +-1433: -120: 94: 106: 122: 0:upper level - corner opposite outside entrance +-1013: 272: 163: 104: 347: 64:office with balcony + -353: 137: 63: 541: 374: 238:outside + -832: 196: 176: 81: 83: 70:balcony + -808: 401: 114: 56: 122: 131:outside + -335: 134: 31: 529: 371: 205:outside + -349: 127: -207: 415: 191: 31:outside - in the water + -849: 20: 12: 47: 87: 91:outside + -856: -154: 52: 45: 65: 179:outside - ladder leading to upper level +-1014: -182: 26: 101: 54: 64:mid level - dark corner +-1012: 367: 23: 103: 495: 68:mid level - offices (locker room side) +-1221: 315: 24: 105: 442: 67:mid level - hallway +-1429: 380: 21: 103: 378: 64:mid level - offices (above glass room) +-1332: -120: 22: 212: 126: 64:mid level - stairs leading to glass room (outside door side) +-1323: 877: 22: 207: 119: 64:mid level - stairs leading to glass room (locker room side) +-1010: 934: 29: 105: 69: 64:mid level - stairs leading to locker room + -773: -903: -228: 120: 234: 75:gas chamber - outside doors + -115: -989: -233: 551: 108: 70:gas chamber - inside + -118: -765: -234: 548: 115: 69:gas chamber - outside (glass wall) + 312: -476: -164: 113: 173: 137:gas chamber - entrance to red room + -902: 671: -120: 5: 64: 52:double doors leading to soda machines hall + -903:-1066: -239: 9: 63: 58:double door leading to gas chamber +-1067: -46: -108: 155: 189: 62:glass room - stairs side +-1067: 746: -111: 154: 242: 63:glass room - double door side +-1339: 933: -114: 119: 55: 72:mid level - stairs to glass room (locker side) +-1340: -188: -89: 117: 67: 45:mid level - stairs to glass room (outside door side) \ No newline at end of file diff --git a/action/tng/colt.flg b/action/tng/colt.flg new file mode 100644 index 000000000..bd27f7d85 --- /dev/null +++ b/action/tng/colt.flg @@ -0,0 +1,3 @@ +#colt +<-1663 -1020 -253> +<-189 740 -127> diff --git a/action/tng/country.ctf b/action/tng/country.ctf new file mode 100644 index 000000000..fd45f4c7a --- /dev/null +++ b/action/tng/country.ctf @@ -0,0 +1,17 @@ +[ctf] +type=offdef +offence=red +author=hifi +comment=country off/def CTF. Enjoy! + +[respawn] +red=4 +blue=8 + +[flags] +red=<1607 -2445 -878> +blue=<308 -394 -337> + +[spawns] +red=<900 -2666 -504 180>,<1782 -1493 -593 90> +blue=<91 -425 -493 0>,<72 -53 -401 -90> diff --git a/action/tng/country2.ctf b/action/tng/country2.ctf new file mode 100644 index 000000000..1b5c2d2b5 --- /dev/null +++ b/action/tng/country2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=4 +blue=4 + +[flags] +red=<-1394 -1794 334> +blue=<216 46 270> + +[spawns] +red=<-863 -1503 46 45>,<-1311 -1695 222 45>,<-1384 -1892 334 90> +blue=<448 448 46 -135>,<-287 608 220 -90>,<326 119 270 -120> \ No newline at end of file diff --git a/action/tng/ctbcity.aqg b/action/tng/ctbcity.aqg new file mode 100644 index 000000000..aa5d669d3 --- /dev/null +++ b/action/tng/ctbcity.aqg @@ -0,0 +1,60 @@ +# +# Map: ctbcity.aqg +# Build: 1 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +9:-951:333:719:520:353:outside the Tenement house +0:-1152:299:318:320:344:in the Tenement house +15:-1527:275:525:62:300:in the Tenement house +-7:-1194:500:346:387:141:on the Tenement house roof +-4:-258:270:264:185:306:in mid-city area +-191:513:383:516:192:208:on the Chinatown roof +4:315:78:708:380:99:outside Chinatown +49:1117:251:750:412:316:behind Chinatown +1055:281:225:289:232:114:in the Blue apartment +971:615:277:256:112:282:in the Blue apartment alley +857:405:399:344:101:159:on the roof above Blue apartment +-934:-195:135:223:258:168:in the alley from Pizzeria Square +-1917:-1072:440:836:939:435:in Pizzeria Square +1294:-530:274:410:201:297:in the Road-end by Backyard +1085:-255:270:191:188:293:in the Backyard +995:-35:275:290:36:297:in the small alley behind Backyard +1980:-900:264:260:575:292:near the big PG Sign +1249:-1236:152:542:233:174:in Clint E alley by the Tenement house +-870:385:251:167:272:286:in Stairs to Chinatown roof +-861:683:459:157:202:97:on the Chinatown roof near Stairs +-637:-279:439:122:156:77:in the Karlsson room +-1682:-930:514:590:820:132:on the Pizzeria Square roof +-1677:-318:50:271:123:63:near Stairs to Pizzeria roofs +-2098:-249:242:144:120:273:in Stairs to Pizzeria roofs +-1483:-1122:329:695:673:54:on low roof in Pizzeria Square +1376:-1659:213:164:127:277:in Stairs by PG Sign +1667:-1570:53:127:98:80:close to Stairs by PG Sign +1874:-1619:469:307:143:105:on Roof above Clint E alley +1796:-1649:294:252:150:59:in the apartment by Clint E alley +1373:-808:442:349:276:80:on roof above PG Sign +-895:-1284:38:186:201:72:close to Pizzeria Square +-920:-273:439:165:143:76:in apartment near the Karlsson room +-8:-54:390:746:462:36:on mid-city rooftops +1289:-768:227:308:266:90:in Clint E alley middle building +1865:-744:325:586:553:209:on Clint E alley roofs +1681:-1018:351:807:949:402:in Clint E alley diff --git a/action/tng/ctbcity.flg b/action/tng/ctbcity.flg new file mode 100644 index 000000000..c91fbcde5 --- /dev/null +++ b/action/tng/ctbcity.flg @@ -0,0 +1,3 @@ +#ctbcity +<-1596 -832 174> +<1665 -892 174> \ No newline at end of file diff --git a/action/tng/ctfsludge.ctf b/action/tng/ctfsludge.ctf new file mode 100644 index 000000000..22bdb80a8 --- /dev/null +++ b/action/tng/ctfsludge.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<1154 192 62> +blue=<-1949 -1922 62> + +[spawns] +red=<256 -512 46 67>,<64 128 430 -89>,<720 -616 302 123>,<320 -1216 302 53> +blue=<-1049 -1217 46 -113>,<-857 -1857 430 91>,<-1513 -1113 302 -57>,<-1113 -513 302 -127> \ No newline at end of file diff --git a/action/tng/deepcanyon.aqg b/action/tng/deepcanyon.aqg new file mode 100644 index 000000000..2b83ca1cb --- /dev/null +++ b/action/tng/deepcanyon.aqg @@ -0,0 +1,22 @@ +# Map: deepcanyon.aqg +# Build: 1 +# Creator:porkchop[piNk] +1165: 1030: 52: 780: 719: 31:in the river +1341: 1238: 178: 579: 110: 51:in the tunnel area +1512: 1971: 279: 389: 187: 172:around the fallen spire +696: 1679: 173: 92: 169: 94:near the central bridge +463: 302: 303: 434: 280: 201:in the valley +902: 317: 977: 103: 281: 159:on the large boulder ledge +400: 832: 700: 45: 341: 184:on the mario ledges +184: 904: 939: 168: 240: 218:on the high plateau +1041: 1327: 597: 243: 122: 255:near the double-spires +1486: 1683: 1032: 421: 517: 174:on the sky walkway system +1751: 1636: 160: 81: 196: 94:near the waterfall bridge +2046: 1702: 469: 96: 453: 307:on the waterfall ledges +2325: 1659: 481: 120: 141: 210:in the waterfall room +2034: 1671: 657: 117: 119: 522:on the waterfall +534: 1941: 1037: 508: 201: 117:in covered overhang ledge +336: 1841: 481: 189: 177: 363:on the main spire +209: 159: 820: 364: 317: 26:in blueroom area +940: 678: 193: 90: 91: 99:in the BACON hidey-hole, eating BACON!! +582: 1290: 872: 95: 176: 142:doing grenade-jumps, like an MPELP f00!! diff --git a/action/tng/default.esp b/action/tng/default.esp new file mode 100644 index 000000000..7d8ab7eaa --- /dev/null +++ b/action/tng/default.esp @@ -0,0 +1,33 @@ +[esp] +type=atl +author=darksaint +name=Show No Mercy + +[respawn] +red=7 +blue=7 +green=7 + +[spawns] +red=<-1087 1552 302 -90>,<8 952 46 -90>,<200 1369 430 -92> +blue=<-1311 -831 62 0>,<160 -1183 814 135>,<-1445 -1264 494 87> +green=<-1311 -831 62 0>,<160 -1183 814 135>,<-1445 -1264 494 87> + +[red_team] +name=The B-Team +leader_name=Mr. Tabasco +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Peacekeepers +leader_name=Serpico +skin=male/ctf_b +leader_skin=male/ctf_vip_b + +[green_team] +name=Gardening Crew +leader_name=Dr. Cannabis +skin=male/ctf_g +leader_skin=male/ctf_vip_g + diff --git a/action/tng/desert.aqg b/action/tng/desert.aqg new file mode 100644 index 000000000..6fc312f43 --- /dev/null +++ b/action/tng/desert.aqg @@ -0,0 +1,44 @@ +# +# Map: desert.aqg +# Build: 1 +# Creator:EzekielRage +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 +vector +# points) +# "rZ" is the height range for the location marker (if 0 then not +use +# it) +# "name" is the name of the location marker and is used in "%L" +and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box +coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to +be alive or on a team +# aqg_location checkS - works like %S, but you don't need to +be alive or on a team +# +# You need ML version 3.19 or later, for this to work. +# +-1895: -828: -12: 700: 735: 216:at the scull base +-1889:-1588: -10: 214: 76: 205:at the scull base +-2578: -932: -38: 82: 309: 240:at the scull base +-1856: 761: 24: 676: 864: 202:at the scull base cannon + 101: 1099: -25: 1235: 412: 249:in no mans land + 19: 1231: -3: 561: 459: 257:in no mans land + 796: 1527: -12: 261: 214: 184:in no mans land + -272: 1277: 26: 657: 364: 222:in no mans land +-1033: 1111: 3: 121: 425: 235:in no mans land +-1039: 1004: 3: 166: 469: 163:in no mans land + 1492: 875: -38: 348: 196: 207:at the good base cannon + 1900: 694: 26: 796: 718: 233:at the good base cannon + 1822: -882: 9: 724: 847: 224:at the good base + 2570: -653: 0: 45: 497: 172:at the good base diff --git a/action/tng/desert.flg b/action/tng/desert.flg new file mode 100644 index 000000000..7a95b4739 --- /dev/null +++ b/action/tng/desert.flg @@ -0,0 +1,3 @@ +#desert +<1883 -1294 -84> +<-1851 -1358 -84> diff --git a/action/tng/ffrontal.aqg b/action/tng/ffrontal.aqg new file mode 100644 index 000000000..201d2595e --- /dev/null +++ b/action/tng/ffrontal.aqg @@ -0,0 +1,34 @@ +# +# Map: ffrontal.aqg +# Build: 2 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +-2: -51: 3512: 405: 303: 54:building (roof) +-213: -56: 3349: 190: 302: 35:building (2nd floor) (east side) +216: -53: 3349: 195: 302: 35:building (2nd floor) (west side) +-1: -92: 3246: 104: 263: 48:building (1st floor) +16: 499: 3240: 368: 251: 43:grass area outside +-18: 219: 3260: 131: 34: 57:grass area outside +-300: 1017: 3234: 71: 211: 35:rock tunnell (ground level) +-16: 1051: 3199: 136: 256: 0:grass area by rocks +228: 945: 3390: 142: 260: 31:rock tunnell (west side) +-212: 980: 3481: 144: 216: 66:rock tunnell (east side) +0: 266: 3407: 351: 19: 58:building (balcony) +-102: 1240: 3418: 183: 54: 4:retreat hole diff --git a/action/tng/ffrontal.flg b/action/tng/ffrontal.flg new file mode 100644 index 000000000..9d26d13b7 --- /dev/null +++ b/action/tng/ffrontal.flg @@ -0,0 +1,3 @@ +#ffrontal +<1 1229 3246> +<0 -256 3396> \ No newline at end of file diff --git a/action/tng/flooded.ctf b/action/tng/flooded.ctf new file mode 100644 index 000000000..60590b892 --- /dev/null +++ b/action/tng/flooded.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-1248 368 176> +blue=<1248 -369 176> + +[spawns] +red=<-1248 112 238 0>,<-904 -152 238 90>,<-1028 556 174 0> +blue=<1028 -556 174 -180>,<1248 -112 238 -180>,<904 152 238 -90> \ No newline at end of file diff --git a/action/tng/hidden3.ctf b/action/tng/hidden3.ctf new file mode 100644 index 000000000..4812eef39 --- /dev/null +++ b/action/tng/hidden3.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<756 2942 -481> +blue=<3662 1351 -481> + +[spawns] +red=<915 2723 -481 145>,<546 2825 -481 0>,<838 3269 -326 -90> +blue=<3297 1569 -481 -30>,<3273 1072 -328 90>,<3695 1561 -289 -180> \ No newline at end of file diff --git a/action/tng/jkduel2.ctf b/action/tng/jkduel2.ctf new file mode 100644 index 000000000..e0fbcfd2a --- /dev/null +++ b/action/tng/jkduel2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=Raptor007 +grapple=no + +[respawn] +red=6 +blue=6 + +[flags] +red=<-768 -768 145> +blue=<768 768 145> + +[spawns] +red=<768 -768 145 150> +blue=<-768 768 145 -30> diff --git a/action/tng/jungle1.flg b/action/tng/jungle1.flg new file mode 100644 index 000000000..f89754468 --- /dev/null +++ b/action/tng/jungle1.flg @@ -0,0 +1,3 @@ +#jungle1 +<1670 -452 -145> +<-516 -706 -145> diff --git a/action/tng/kumanru.aqg b/action/tng/kumanru.aqg new file mode 100644 index 000000000..08449411d --- /dev/null +++ b/action/tng/kumanru.aqg @@ -0,0 +1,30 @@ +# Map: kumanru.aqg +# Build: 1 +# Creator:porkchop[piNk] +609:-1521: 357: 414: 269: 98:in the virginal sacrifice chamber +772:-1050: 261: 63: 177: 131:on the virginal sacrifice chamber ramp +766: -504: 258: 255: 375: 125:in the red pit room +770: -507: 92: 94: 215: 30:in the red pit +759: -498: 234: 84: 205: 94:above the red pit +437: -69: 195: 71: 188: 60:passing through the very dark corner +-282: -284: 178: 467: 467: 168:in the alter room +-552: 325: 318: 660: 56: 194:on the staircase +-1084: 94: 449: 126: 160: 63:in the upper lift room +-1055: -381: 403: 282: 249: 203:in the bell room +-1185: -861: 527: 180: 152: 127:in the bell tower +-1145: -888: 220: 263: 181: 76:below the bell tower +-1154:-1114: 331: 345: 71: 192:along the courtyard wall +-754:-1031: 286: 583: 307: 146:in the courtyard +-254: -851: 409: 141: 93: 121:in the sniper tower +18:-1090: 325: 109: 326: 51:in the dark hallway +0:-1597: 322: 120: 64: 66:above the ladder leading down to the sewers +64:-1601: 4: 63: 63: 259:on the ladder leading to the sewers +61:-1264: -159: 63: 260: 159:on the dark ramp in sewers +59: -709: -356: 277: 324: 190:in the sewers, on the ladder side +-286: -165: -388: 390: 161: 112:in the sewers, on the lift side +-922: -194: -378: 221: 63: 75:in the lift entrance +-1089: 59: -67: 69: 71: 64:in the lower lift room +-989: 66: 113: 34: 60: 272:on the lift +277: -732: 40: 177: 167: 29:in the secret BACON! room, eating BACON!! +364: -513: 35: 108: 123: 35:in the secret MPELP hidey hole +-1042: -888: 607: 369: 174: 130:doing grenade-jumps, like an MPELP f00!! diff --git a/action/tng/lavatube.flg b/action/tng/lavatube.flg new file mode 100644 index 000000000..6aabff47d --- /dev/null +++ b/action/tng/lavatube.flg @@ -0,0 +1,3 @@ +#lavatube +<2259 714 230> +<-3708 637 141> diff --git a/action/tng/leaf.esp b/action/tng/leaf.esp new file mode 100644 index 000000000..6c1d86242 --- /dev/null +++ b/action/tng/leaf.esp @@ -0,0 +1,29 @@ +[esp] +type=etv +author=darksaint +name=Get the Fish! + +[respawn] +red=8 +blue=8 +green=8 + +[spawns] +red=<1079 206 -660 180>,<1043 -496 -570 140> +blue=<-1495 94 -550 90>,<-1574 684 -550 -50> + +[target] +escort=<-1467 466 -154> +name=The Bait + +[red_team] +name=Master Baiters +leader_name=Captain Handy +skin=male/aqgthug +leader_skin=male/aqgvillain + +[blue_team] +name=The EPA +leader_name=Gordon +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/lighthouse.aqg b/action/tng/lighthouse.aqg new file mode 100644 index 000000000..180eeaff0 --- /dev/null +++ b/action/tng/lighthouse.aqg @@ -0,0 +1,53 @@ +# +# Map: lighthouse.aqg +# Build: 3 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +-1015: 1368: 504: 241: 258: 57:house (1st floor) +-1070: 1819: 502: 189: 171: 39:kitchen +-1072: 961: 508: 192: 145: 47:tv room +-1078: 955: 635: 196: 145: 56:bedroom above tv +-1069: 1825: 633: 191: 175: 54:bedroom above kitchen +-1210: 1377: 634: 52: 251: 51:house (2nd floor) +-1035: 1601: 614: 106: 28: 38:house (2nd floor) +-847: 1379: 621: 61: 255: 46:outside house (2nd floor) +-679: 1413: 484: 87: 226: 37:grass (by house) +-355: 1368: 515: 192: 190: 65:fountain +-1036: 1180: 633: 106: 58: 55:bathroom +-112: 1963: 571: 157: 112: 124:inside barn +-576: 1909: 484: 283: 257: 39:grass (by barn) +-111: 2117: 512: 158: 39: 64:behind barn +-109: 1717: 513: 158: 118: 66:grass (by barn) +-165: 862: 512: 10: 9: 66:by doors of death (house side) +-44: 1196: 513: 90: 343: 65:by doors of death (house side) +-520: 990: 492: 341: 110: 44:by doors of death (house side) +-231: -182: 215: 893: 181: 87:beach +-429: 681: 565: 173: 183: 110:hall of death (wide) +159: 831: 559: 80: 193: 48:hall of death (narrow) +385: 816: 610: 124: 333: 106:grass by hall of death (road side) +966:-1088: 504: 189: 324: 357:lighthouse +986: -383: 197: 42: 332: 29:dock +537: -833: 193: 215: 54: 26:dock +822: 574: 412: 298: 576: 269:road +800: 1104: 674: 175: 48: 31:road (camping overpass) +-28: -809: 60: 1127: 600: 51:water +-76: -809: 149: 258: 121: 25:boat (bottom) +-72: -818: 322: 264: 143: 106:boat (top) +-304: 237: 383: 813: 236: 124:sloped hill of death \ No newline at end of file diff --git a/action/tng/lighthouse.dom b/action/tng/lighthouse.dom new file mode 100644 index 000000000..b3ca17dea --- /dev/null +++ b/action/tng/lighthouse.dom @@ -0,0 +1,2 @@ +[dom] +flags=<960 -860 664>,<-1184 1456 600>,<464 512 536> diff --git a/action/tng/lighthouse.esp b/action/tng/lighthouse.esp new file mode 100644 index 000000000..9d4eb50a6 --- /dev/null +++ b/action/tng/lighthouse.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Turn out the lights! + +[respawn] +red=6 +blue=6 + +[spawns] +red=<-1185 969 520 -37>,<-465 2062 510 -82>,<-352 1368 616 90> +blue=<-70 -858 305 58>,<556 176 248 0>,<306 528 648 90> + +[target] +escort=<934 -873 686> +name=Lighthouse Beacon + +[red_team] +name=No Good Teenagers +leader_name=Simi the Slasher +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Lightkeepers +leader_name=Sir Per Kele +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/lighthouse.flg b/action/tng/lighthouse.flg new file mode 100644 index 000000000..a6488bc98 --- /dev/null +++ b/action/tng/lighthouse.flg @@ -0,0 +1,3 @@ +#lighthouse +<-534 1758 494> +<-29 142 381> diff --git a/action/tng/mesto.esp b/action/tng/mesto.esp new file mode 100644 index 000000000..17eacd7e4 --- /dev/null +++ b/action/tng/mesto.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Let's See Paul Allen's Card + +[respawn] +red=6 +blue=6 + +[spawns] +red=<813 982 1078 -127>,<-143 824 713 176>,<-794 593 591 -17> +blue=<930 -789 954 145>,<319 -891 684 111>,<-887 -522 1076 -19> + +[target] +escort=<-358 -524 1191> +name=Axe in the Briefcase + +[red_team] +name=Yuppies +leader_name=Patrick Bateman +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Innocent Victims +leader_name=Mohoney +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/metropol.ctf b/action/tng/metropol.ctf new file mode 100644 index 000000000..93d9ca72e --- /dev/null +++ b/action/tng/metropol.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-1177 795 -18> +blue=<1171 -1194 430> + +[spawns] +red=<-1166 406 -210 -1>,<-771 -257 -146 88>,<189 813 -210 -36> +blue=<1200 -960 174 -180>,<290 -929 -210 -38>,<-190 -1191 -210 -88> \ No newline at end of file diff --git a/action/tng/monastery.aqg b/action/tng/monastery.aqg new file mode 100644 index 000000000..ac466d4bc --- /dev/null +++ b/action/tng/monastery.aqg @@ -0,0 +1,53 @@ +# +# Map: monastery.aqg +# Build: 2 +# Creator:-RS-Phreedh +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + -573:-3019: 61: 202: 173: 210:in the courtyard, in the dark corner + -582:-2389: 8: 54: 275: 250:in the courtyard, on the dark ledge + 99:-1975: -8: 678: 141: 255:in the courtyard, on the terrace + -156:-2758: 57: 330: 62: 234:in the courtyard, on the high ledge + 60:-2213: -134: 832: 1000: 421:in the courtyard + -449:-3032: -318: 71: 342: 189:in the small corridor from the courtyard + -635:-3261: -22: 120: 69: 213:in the small corridor from the courtyard + 649:-2585: 72: 164: 175: 164:in the watch tower + 650:-2990: 52: 130: 194: 201:in the dark room by the watch tower + 56:-3288: 95: 472: 491: 272:in the watch tower corridor + -791:-3460: 111: 375: 319: 256:in the watch tower corridor + -388:-3074: -35: 22: 208: 91:in the watch tower corridor +-1105:-2512: -267: 468: 649: 275:in the crystal room +-1161:-2533: -87: 391: 663: 340:in the crystal room + -732:-2508: 42: 31: 640: 65:in the crystal room +-1188:-1548: -162: 454: 374: 117:near the small rooms by the crystal room + 863: -591: -490: 395: 118: 102:in the tunnel below the pool room + 396:-1398: -452: 269: 711: 57:in the tunnel below the pool room + 1110: -858: -188: 611: 615: 199:in the pool room + 1290: -556: -428: 267: 327: 178:in the pit in the pool room + 968: -42: -59: 402: 204: 228:in the dark room by the pool room + 209: -20: 86: 468: 201: 105:in the high corridor + -73: -726: -28: 516: 452: 350:in the bright hall + -120: -185: -186: 136: 136: 74:in the bright hall + -32: -401: 107: 526: 166: 119:in the bright hall, on the ledge + -53:-1342: -176: 139: 222: 118:in the main corridor + -13:-1595: -114: 817: 199: 165:in the main corridor + -653:-1265: -109: 131: 141: 162:in the main corridor + -5:-1744: -196: 775: 65: 68:in the main corridor diff --git a/action/tng/murder.aqg b/action/tng/murder.aqg new file mode 100644 index 000000000..e52352048 --- /dev/null +++ b/action/tng/murder.aqg @@ -0,0 +1,51 @@ +# +# Map: murder.aqg +# Build: 3 +# Creator: =OG=7 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +-61: 1827: 611: 732: 305: 89:top of red roof building +-535: 1089: 397: 382: 193: 53:roof of petes crash repair +-613: 705: 59: 229: 192: 58:dark campers hideout +1565: 1152: 758: 87: 1068: 230:long gray roof +1250: 1061: 785: 289: 234: 251:long gray roof +1075: 1040: 197: 85: 135: 196:elevator room +770: 1063: 626: 170: 114: 376:skybridge +367: 1139: 785: 216: 212: 240:roof with 4 boxes +-161: 2542: 935: 737: 269: 73:yellow grenade jumper building +-715: 1778: 149: 198: 504: 152:dumpster alley +523: 1478: 58: 1008: 208: 69:street (middle) +766: 1107: 71: 178: 173: 72:street (middle) +404: 2272: 50: 834: 192: 47:long alley +805: 2638: 67: 234: 168: 62:long alley +973: 1940: 487: 189: 252: 47:roof (with 3 bars) +1358: 2256: 45: 195: 560: 48:street (by neon light spawn) +1194: 2632: 671: 184: 183: 326:little building roof +-48: 1887: 35: 180: 179: 44:alley (under cover) +673: 1879: 56: 111: 175: 60:back alley +337: 286: 736: 329: 194: 216:roof (aq2 sign) +808: 265: 46: 123: 188: 49:newbies bench +626: 674: 39: 1003: 216: 40:street (by bench) +1: 1113: 45: 166: 212: 45:road by crash repair shop +1199: 302: 574: 259: 196: 48:camper room (with boxes) +1207: 289: 898: 262: 194: 226:roof of camper building +-169: 276: 579: 173: 174: 421:roof of joes tavern +-647: 477: 650: 264: 377: 421:roof by [tdm] sign +-179: 272: 53: 171: 156: 58:in joes tavern +370: 1137: 338: 189: 188: 58:room with 3 windows in middle \ No newline at end of file diff --git a/action/tng/museum.aqg b/action/tng/museum.aqg new file mode 100644 index 000000000..550c74a29 --- /dev/null +++ b/action/tng/museum.aqg @@ -0,0 +1,26 @@ +# +# Map: museum.aqg +# Build: 2 +# Creator:FragBait & Mike +# +# dedicated to the ML mod and Mats hus av aj aj! +# sprayers.stinker.nu:27910 +# +# We love ML.. =) +# + + -7: 82: 123: 440: 669: 127:in the big hall + 683: 355: 89: 275: 160: 121:at wet floors + 892: -110: 112: 449: 335: 111:inside room behind wet floors + 84: 217: 370: 1258: 664: 113:at the roof + -787: 383: 400: 375: 495: 79:at the roof above Monet room + 896: 223: 376: 447: 671: 118:at the roof above wet floors + 706: 647: 81: 256: 122: 82:between the double doors + -936: -378: 160: 465: 510: 147:in the large staircase room + -142: 876: 125: 299: 165: 130:at the entrance + 559: 1024: 54: 398: 255: 58:inside ground-floor statues corridor + -372: 1476: 53: 525: 389: 56:inside ground-floor statues room + -487: 1473: 190: 647: 386: 69:above the ground-floor statues room + 303: 1038: 193: 143: 243: 70:at the entrance ladder corridor + -863: 602: 118: 417: 475: 116:inside Monet room + -540: -288: 68: 97: 414: 67:at the main corridor \ No newline at end of file diff --git a/action/tng/museum.flg b/action/tng/museum.flg new file mode 100644 index 000000000..983cd495f --- /dev/null +++ b/action/tng/museum.flg @@ -0,0 +1,3 @@ +#museum +<-1 -175 46> +<-262 1470 214> diff --git a/action/tng/nine2.esp b/action/tng/nine2.esp new file mode 100644 index 000000000..193adce1c --- /dev/null +++ b/action/tng/nine2.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Escape 1984 + +[respawn] +red=7 +blue=7 + +[spawns] +red=<-2859 -814 16 90>,<-1608 -559 0 148>,<-2208 -34 0 121> +blue=<188 -145 74 138>,<-737 164 74 -40>,<-897 1780 -38 -97> + +[target] +escort=<-208 1877 -34> +name=Proof of Innocence + +[red_team] +name=Free Minds +leader_name=Winston Smith +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Thought Police +leader_name=O'Brien +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/ninja.aqg b/action/tng/ninja.aqg new file mode 100644 index 000000000..87530dce3 --- /dev/null +++ b/action/tng/ninja.aqg @@ -0,0 +1,11 @@ +# Map: ninja.aqg +# Build: 1 +# Creator:[AMC]Mloclam +76: 1516: 866: 81: 46: 142:on a platform +-238: 560: 891: 65: 86: 82:on a platform +78: -212: 866: 113: 50: 114:on a platform +921: 562: 886: 60: 98: 122:on a platform +346: 640: 749: 741: 1005: 259:on the high wires +337: 668: 97: 642: 920: 331:plummeting to doom +347: 668: -308: 653: 920: 74:far down on the ground +394: 638: 1079: 716: 934: 99:on top of the walls diff --git a/action/tng/p1_lightbeam.esp b/action/tng/p1_lightbeam.esp new file mode 100644 index 000000000..1e558aa2d --- /dev/null +++ b/action/tng/p1_lightbeam.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Conspiracy to wreck the Loveboat + +[respawn] +red=7 +blue=7 + +[spawns] +red=<-947 -2534 120 141>,<40 -2734 120 114>,<-1464 -1815 120 90> +blue=<-1666 -605 120 -24>,<-768 -1982 -52 35>,<447 -1884 -56 90> + +[target] +escort=<88 -663 -82> +name=Proof of Guilt + +[red_team] +name=Lawyers +leader_name=Mister Chow +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Lighthouse Keepers +leader_name=Lightmaster +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/pier.ctf b/action/tng/pier.ctf new file mode 100644 index 000000000..3aafc57ec --- /dev/null +++ b/action/tng/pier.ctf @@ -0,0 +1,19 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=4 +blue=3 + +[flags] +red=<690 -1491 460> +blue=<-804 -5 198> + +[spawns] +red=<843 -1374 210 179>,<936 -1803 617 138> +blue=<-479 -447 54 90>,<-756 240 197 1> + +[objects] +barrels=<862 -1062 270 -138>,<325 -1285 387 -86> \ No newline at end of file diff --git a/action/tng/rhcity1.aqg b/action/tng/rhcity1.aqg new file mode 100644 index 000000000..de4500331 --- /dev/null +++ b/action/tng/rhcity1.aqg @@ -0,0 +1,57 @@ +# +# Map: rhcity1.aqg +# Build: 2 +# Creator:EzekielRage +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# You need ML version 3.19 or later, for this to work. +# + 1590:-1071: 187: 55: 136: 63:on 1st balcony on Palast hotel + 1585:-1058: 298: 57: 124: 49:on 2nd balcony on Palast hotel + 1306:-1251: 181: 405: 97: 52:inside Palast hotel + 1774:-1260: 287: 49: 59: 193:in the elevator shaft + 807: -975: 123: 139: 200: 155:in the parking lot + -202: -170: 397: 120: 111: 442:at the trash cans + -268:-1321: 546: 190: 172: 274:on the roof of the extension + -206:-1272: 197: 128: 130: 78:inside extension on 1st floor + -248: -679: 529: 171: 404: 326:on the roof of the school + -230:-1104: 145: 109: 48: 159:in the alley + 699: 165: 118: 380: 572: 159:inside church area + 1290: -976: 117: 349: 200: 166:inside fenced area in front of Palast hotel + 1683: -114: 111: 136: 322: 104:at Dausbraue bier + 1303: -918: 585: 612: 487: 320:on Palast hotel roof + 1871: -120: 502: 148: 309: 283:on Drausbrau bier roof + 426: -159: 536: 146: 259: 362:on the roof of the church + 1734: 455: 308: 202: 267: 54:inside the old house + 1569: 466: 183: 53: 153: 57:on 1st balcony on the old house + 1541: 440: 323: 27: 130: 72:on 2nd balcony on the old house + 896: -156: 425: 194: 254: 238:inside the midle building + 482: 486: 536: 200: 306: 338:on the top roof of the red house + 549: 397: 433: 110: 185: 57:inside the room on 3rd floor, red house + 947: 561: 520: 257: 137: 319:on the roof of the red house + 666: -160: 386: 40: 155: 258:in the fireescape, middle building + 492: 169: 273: 100: 40: 147:in the fireescape, red house + 61: 195: 106: 237: 604: 123:in the street near church + 132:-1207: 116: 276: 441: 159:in the street near school + 733: -591: 89: 900: 189: 112:on main street + 1366: 216: 92: 280: 624: 137:in the street near Drausbrau bier + 806: -595: -63: 615: 71: 47:in the sewer under main street + -94: -368: -64: 281: 1050: 46:in the northern end of the sewer + 1405: -878: -74: 213: 217: 63:in the sewer under Palast hotel + 1286: 110: -75: 550: 626: 62:in the southern end of the sewer diff --git a/action/tng/rhcity1.flg b/action/tng/rhcity1.flg new file mode 100644 index 000000000..b54dc7de3 --- /dev/null +++ b/action/tng/rhcity1.flg @@ -0,0 +1,3 @@ +#rhcity1 +<988.00 -1258.50 408.12> +<486.88 462.12 536.12> diff --git a/action/tng/ricochet.ctf b/action/tng/ricochet.ctf new file mode 100644 index 000000000..a427b0d99 --- /dev/null +++ b/action/tng/ricochet.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=4 +blue=4 + +[flags] +red=<-2515 -1888 414> +blue=<-2757 -1879 -114> + +[spawns] +red=<-784 -832 286 90>,<-2536 -1416 414 0>,<-1535 -584 54 -45> +blue=<-2096 -1036 -114 -90>,<-2766 -2272 -122 0>,<-3036 -808 -82 0> \ No newline at end of file diff --git a/action/tng/ricochet.esp b/action/tng/ricochet.esp new file mode 100644 index 000000000..f9cbbe59e --- /dev/null +++ b/action/tng/ricochet.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Prevent the Picnic! + +[respawn] +red=6 +blue=6 + +[spawns] +red=<-2925 -973 -92 40>,<-1611 -336 -148 -90>,<-1037 -942 -156 170> +blue=<-1844 -2690 -66 -149>,<-2673 -2958 -124 90>,<-1995 -2521 -124 131> + +[target] +escort=<-953 -1781 54> +name=The Picnic Basketcase + +[red_team] +name=Hungry Reds +leader_name=Fatty McGee +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Famished Blues +leader_name=Starvin Marvin +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/riot.aqg b/action/tng/riot.aqg new file mode 100644 index 000000000..71830d40b --- /dev/null +++ b/action/tng/riot.aqg @@ -0,0 +1,48 @@ +# +# Map: riot.aqg +# Build: 1 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +1035:-1189:320:308:566:401:in Beer alley +1211:-995:470:135:166:139:up on Beer roof +1327:-175:471:221:471:134:up on Gandhi roof +1395:139:134:265:138:147:on Gandhi square +1854:-121:272:319:516:342:in El. Const. alley +2501:70:249:317:126:87:in El. Const. apartment +2680:-129:273:122:375:81:in El. Const. apartment +2787:-512:276:227:130:301:in NW alley +2886:-961:459:323:318:143:up on NW roof +2729:-830:243:163:180:70:in NW apartment +2911:-1567:321:340:293:342:in alley below Main roof +2365:-1839:448:316:277:146:up on Main roof +2173:-2213:134:77:95:308:in the ladder room +1664:-2180:419:133:135:167:up on little brown roof +1338:-2052:404:134:134:230:up on low roof +1693:-2210:32:226:238:202:in alley below brown roof +1963:-1697:-47:636:404:155:on street under Main roof +1486:-1699:85:265:226:94:above stairs from Main roof street +1951:-1056:149:607:420:165:on Main Street +2436:-727:135:264:674:168:on Main street +1980:-1343:114:194:193:65:in the entrance room +1936:-937:252:242:719:73:in the blue galleria +1920:-530:454:250:165:122:in apartment by High roof +1940:-904:537:102:218:110:up on High roof diff --git a/action/tng/riot.flg b/action/tng/riot.flg new file mode 100644 index 000000000..3897269fe --- /dev/null +++ b/action/tng/riot.flg @@ -0,0 +1,3 @@ +#riot +<1643 -1958 -81> +<2429 -158 94> \ No newline at end of file diff --git a/action/tng/riot2.aqg b/action/tng/riot2.aqg new file mode 100644 index 000000000..85a7245ed --- /dev/null +++ b/action/tng/riot2.aqg @@ -0,0 +1,53 @@ +# +# Map: riot2.aqg +# Build: 1 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +618:573:206:1195:732:394:outside Post Office +-97:116:153:346:142:285:behind Post Office +-367:387:156:113:287:319:behind Post Office +-88:749:351:409:499:180:up over Post Office +1106:670:-13:398:222:119:inside Le Blue Wolf +1307:1590:-15:213:431:133:inside Lurker Imports +1007:1950:-48:91:81:67:inside Lurker Imports +1132:2136:-14:443:88:124:behind Lurker Imports +31:2697:65:566:856:238:outside McMike's Kebab +168: 1689:11:584:170:200:on Sigma-Systems Street +-799:2090:66:443:596:296:outside Gangster Productions +-982:2297:-96:144:249:96:inside Spray&Pray gunshop +-426:3126:45:126:475:207:behind McMike's Kebab +229:3171:-33:540:215:114:inside McMike's Kebab +731:2754:-53:151:188:105:inside McMike's Kebab +-633:3270:-190:170:392:235:on subway exit at McMike's Kebab +66:3264:-292:820:771:142:at newsstand subway station +-619:2445:-300:156:384:113:inside subway station newsstand +-283:2321:-331:170:225:109:inside subway station newsstand +63:1665:-331:120:1155:171:on the train +1094:2595:-164:193:384:223:on subway exit at Lurker Imports +-475:1376:-55:296:144:113:on subway exit at Post Office +-510:887:-169:262:365:258:on subway exit at Post Office +201:210:-322:688:799:67:at subway station +1263:484:-311:358:845:166:in Lurker Imports basement +942:1626:31:181:558:172:outside Lurker Imports +310:1325:315:200:42:122:on the balcony over Post Office +-62:1396:149:78:132:293:outside Post Office +-99:2265:313:288:428:147:up on roofs near Spray&Pray diff --git a/action/tng/riotx.aqg b/action/tng/riotx.aqg new file mode 100644 index 000000000..b602d68d4 --- /dev/null +++ b/action/tng/riotx.aqg @@ -0,0 +1,51 @@ +# +# Map: riotx.aqg +# Build: 1 +# Creator:taipAn, EzekielRage +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or +on a team +# aqg_location checkS - works like %S, but you don't need to be alive or +on a team +# +# + +1035:-1189:320:308:566:401:in Beer alley +1211:-995:470:135:166:139:up on Beer roof +1327:-175:471:221:471:134:up on Gandhi roof +1413:126:232:251:120:235:on Gandhi square +1854:-121:272:319:516:342:in El. Const. alley +2501:70:249:317:126:87:in El. Const. apartment +2680:-129:273:122:375:81:in El. Const. apartment +2787:-512:276:227:130:301:in NW alley +2886:-961:459:323:318:143:up on NW roof +2729:-830:243:163:180:70:in NW apartment +2911:-1567:321:340:293:342:in alley below Main roof +2365:-1839:448:316:277:146:up on Main roof +2173:-2213:134:77:95:308:in the ladder room +1664:-2180:419:133:135:167:up on little brown roof +1338:-2052:404:134:134:230:up on low roof +1693:-2210:32:226:238:202:in alley below brown roof +1963:-1697:-47:636:404:155:on street under Main roof +1486:-1699:85:265:226:94:above stairs from Main roof street +1951:-1056:149:607:420:165:on Main Street +2436:-727:135:264:674:168:on Main street +1980:-1343:114:194:193:65:in the entrance room +1936:-937:252:242:719:73:in the blue galleria +1920:-530:454:250:165:122:in apartment by High roof +1940:-904:537:102:218:110:up on High roof +1276:-1315:185:305:148:134:in the Beer apartment diff --git a/action/tng/rok.ctf b/action/tng/rok.ctf new file mode 100644 index 000000000..351168923 --- /dev/null +++ b/action/tng/rok.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=stric&matic +grapple=no + +[respawn] +red=4 +blue=4 + +[flags] +red=<402 146 326> +blue=<-1892 2332 46> + +[spawns] +red=<-488 550 350 -126>,<-55 195 46 3>,<-1100 660 46 90> +blue=<-1191 1834 81 -132>,<-745 2411 382 -179>,<-1100 660 46 90> \ No newline at end of file diff --git a/action/tng/rok.esp b/action/tng/rok.esp new file mode 100644 index 000000000..a67338d40 --- /dev/null +++ b/action/tng/rok.esp @@ -0,0 +1,35 @@ +[esp] +type=etv +author=darksaint +name=Finger Lickin' Good + +[respawn] +red=7 +blue=7 +green=7 + +[spawns] +red=<-1722 86 76 38>,<208 124 66 149>,<767 924 122 -137> +blue=<748 2202 346 -127>,<322 2214 650 -136>,<-1432 2166 480 -51> + +[target] +escort=<-764 905 290> +name=Bucket of Chicken Wings + +[red_team] +name=KFC +leader_name=The Colonel +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Popeyes +leader_name=Popeye +skin=male/ctf_b +leader_skin=male/ctf_vip_b + +[green_team] +name=Raisin' Canes +leader_name=Cane the Dog +skin=male/ctf_g +leader_skin=male/ctf_vip_g \ No newline at end of file diff --git a/action/tng/rooftops.ctf b/action/tng/rooftops.ctf new file mode 100644 index 000000000..6a12019fe --- /dev/null +++ b/action/tng/rooftops.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<1433 -1306 670> +blue=<-156 453 878> + +[spawns] +red=<752 -1184 366 90>,<1312 -1104 670 -90>,<1084 -552 670 0> +blue=<432 244 750 90>,<-416 416 878 0>,<-64 280 686 -90> \ No newline at end of file diff --git a/action/tng/ruins3.flg b/action/tng/ruins3.flg new file mode 100644 index 000000000..c46419929 --- /dev/null +++ b/action/tng/ruins3.flg @@ -0,0 +1,3 @@ +#ruins3 +<-546 -757 -393> +<792 363 -385> diff --git a/action/tng/santa.esp b/action/tng/santa.esp new file mode 100644 index 000000000..ed6944d6b --- /dev/null +++ b/action/tng/santa.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Reach the Big Christmas tree! + +[respawn] +red=5 +blue=5 + +[spawns] +red=<1399 -922 -146 160>,<462 -1551 -408 138>,<-460 -750 -408 -35> +blue=<-637 -324 56 51>,<-446 -1241 56 28>,<424 -980 -200 -164> + +[target] +escort=<-1023 -307 -82> +name=The Christmas Spirit + +[red_team] +name=Santa Force +leader_name=Santa +skin=male/ctf_r +leader_skin=male/santa + +[blue_team] +name=Elf Patrol +leader_name=Legolas +skin=male/ctf_g +leader_skin=male/ctf_vip_g diff --git a/action/tng/siege2.aqg b/action/tng/siege2.aqg new file mode 100644 index 000000000..8a2d2a1b5 --- /dev/null +++ b/action/tng/siege2.aqg @@ -0,0 +1,51 @@ +# +# Map: siege2.aqg +# Build: 1 +# Creator:Bobert 1-15-2002 +# +# format is: +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# + -860: -200: 256: 122: 122: 256:in the Van Gogh tower + -860: -200: 736: 122: 122: 224:above the Van Gogh tower + 106: -200: 256: 122: 122: 256:in the Da Vinci tower + 106: -200: 736: 122: 122: 224:above of the Da Vinci tower + -860: -987: 192: 122: 122: 192:in the rear tower + -860: -987: 480: 122: 122: 96:above the rear tower + 106: -987: -96: 122: 122: 352:on the spiral staircase + 106: -987: 352: 122: 122: 96:above the spiral staircase + -377: -590: 130: 402: 309: 130:in the courtyard + -381: 8: 95: 93: 262: 95:by the drawbridge + -136: -979: 196: 120: 49: 60:near the button + -40: -608: -95: 34: 34: 135:in the well + 258: 16: -245: 226: 256: 243:in the chasm by the tree + 364: -711: -245: 120: 471: 243:in the chasm by the castle entrance + 364:-1278: -245: 120: 97: 243:on the chasm ledges + -665: 99: -245: 697: 173: 243:in the chasm under the drawbridge +-1233: 435: -245: 130: 163: 243:in the chasm cave enterance + -119: -981: -380: 169: 125: 108:below the spiral staircase + -23: -683: -380: 267: 176: 108:below the castle +-1208: 722: -133: 212: 124: 325:in the caves + -380: -163: 224: 355: 91: 224:around the front castle wall + -380: -986: 352: 355: 70: 96:above the rear castle wall + -380: -980: 68: 355: 60: 68:in the rear castle wall + -864: -593: 224: 65: 265: 224:around Van Gogh's castle wall + 104: -593: 224: 65: 265: 224:around Da Vinci's castle wall + -446:-1278: 192: 1003: 110: 192:behind the castle +-1379:-1308: 480: 90: 81: 96:camping like a wuss + 619:-1279: 288: 82: 111: 288:pitching a tent in the rear corner +-1223: -503: 256: 230: 605: 256:outside by the trees + 538: -538: 256: 163: 630: 256:outside by the ledges +-1270: 350: 256: 274: 249: 256:between the caves & castle + -154: 508: 256: 842: 407: 256:in front of the castle +-1051: 769: 478: 418: 161: 290:above the caves + + + diff --git a/action/tng/siege2.ctf b/action/tng/siege2.ctf new file mode 100644 index 000000000..e9ced0f22 --- /dev/null +++ b/action/tng/siege2.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-449 -520 183> +blue=<-896 758 606> + +[spawns] +red=<-398 -208 310 -90>,<-86 -984 182 135>,<-366 -1328 301 90> +blue=<-1358 -664 54 45>,<-1262 760 422 -45>,<170 808 54 -110> \ No newline at end of file diff --git a/action/tng/siege2.flg b/action/tng/siege2.flg new file mode 100644 index 000000000..852511039 --- /dev/null +++ b/action/tng/siege2.flg @@ -0,0 +1,3 @@ +#siege2 +<-754 747 606> +<-458 -532 183> diff --git a/action/tng/sludge1.flg b/action/tng/sludge1.flg new file mode 100644 index 000000000..b6210062f --- /dev/null +++ b/action/tng/sludge1.flg @@ -0,0 +1,3 @@ +#sludge1 +<1016 -612 -145> +<520 -1122 302> diff --git a/action/tng/soho.aqg b/action/tng/soho.aqg new file mode 100644 index 000000000..1d43ce3d0 --- /dev/null +++ b/action/tng/soho.aqg @@ -0,0 +1,53 @@ +# +# Map: soho.aqg +# Build: 1 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# +-191:19:191:489:596:170:in the back alley +-80:-417:117:163:171:98:in back alley tree square +201:-386:111:86:130:97:in back alley ramp house +151:315:310:133:107:47:in back alley top room +94:738:271:343:103:101:inside Chang Imports +-209:1228:187:43:192:171:at trashcan underground entrance +-475:821:149:181:198:140:in the back alley +-773:491:188:69:187:51:inside the secret room +88:944:188:373:75:183:in back alley, near crates area +542:1369:261:303:417:112:in the crates area +1224:1724:141:375:154:122:in little square by crates area +1394:1968:101:181:71:80:in little square by crates area +1090:2215:47:377:186:83:in the narrow passage at little square +40:2374:212:57:274:148:thrashcan alley behind the restaurant +403:2232:46:288:216:81:in the alley behind the restaurant +-616:2160:272:583:567:212:outside the restaurant +-180:2469:118:129:229:52:inside the restaurant +-993:2262:207:196:209:139:inside laundry house +-995:2259:424:210:223:61:up on laundry house roof +-996:2596:122:202:107:58:near the restaurant underground entrance +129:1679:214:162:108:51:in the crates area +-1052:2204:-117:61:446:117:underground, near exit by the restaurant +-554:1245:-180:554:506:52:underground, near the pipes +-316:1301:-126:314:144:107:underground, near the pipes +-297:608:-41:168:120:189:undeground, near back alley exit +136:671:-175:262:58:43:underground, near broken stone room +185:413:-155:216:170:110:underground, in broken stone room +71:-107:-93:144:316:84:underground, near tree square exit +-38:-171:-168:36:256:37:underground, near tree square exit +-167:-136:80:79:80:72:inside back alley small cottage diff --git a/action/tng/spring.esp b/action/tng/spring.esp new file mode 100644 index 000000000..e01ea7e84 --- /dev/null +++ b/action/tng/spring.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=Escape! + +[respawn] +red=7 +blue=7 + +[spawns] +red=<1779 1382 1608 23>,<3293 1102 1832 94>,<2611 2342 1480 -64> +blue=<1862 3392 1992 -60>,<3491 2391 1864 -9>,<1878 2475 2120 52> + +[target] +escort=<2795 2455 1838> +name=Travel documents + +[red_team] +name=Convicts +leader_name=Mastermind +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=The Company +leader_name=Supervisor +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/subway2.flg b/action/tng/subway2.flg new file mode 100644 index 000000000..051437e16 --- /dev/null +++ b/action/tng/subway2.flg @@ -0,0 +1,3 @@ +#subway2 +<0 1744 174> +<-179 645 174> diff --git a/action/tng/teacher.aqg b/action/tng/teacher.aqg new file mode 100644 index 000000000..e1825dade --- /dev/null +++ b/action/tng/teacher.aqg @@ -0,0 +1,50 @@ +# +# Map: teacher.aqg +# Build: 2 +# Creator:Ellusion [NW], Mike [NW] +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + +0:-1990:339:194:200:77:in Principal Office +70:-1527:327:663:253:78:outside Principal Office +-12:-1159:488:617:523:78:on the 3rd Floor +244:-1771:377:46:298:22:in Office Vents +511:-726:334:95:746:82:in east Hallway on 2nd floor +-511:-726:334:95:746:82:in west Hallway on 2nd floor +-5:-770:320:384:448:76:in the 2nd floor Classrooms +-10:65:202:396:323:197:in the northern Main Hall +11:863:98:390:448:180:in the Auditorium +2:-2032:69:650:512:133:in the the Gym +10:-1357:-9:394:169:63:in the the Gym Lockers +0:961:-143:403:361:50:in northern Basement +0:735:-134:232:135:61:in Basement Storage +0:317:-137:389:258:59:in Basement +-250:-211:-137:126:348:59:near the Janitor's Desk +-258:-605:-121:128:67:62:in stairs by Big-Fan room +0:-750:69:253:145:133:in Big-Fan Room +-38:-727:5:638:479:81:on the Ground Floor around Fan room +0:-1367:148:443:156:83:in 1st floor Hallway near Gym +524:-1328:102:83:141:167:in East staircase near Gym +-524:-1328:102:83:141:167:in West staircase near Gym +0:-767:275:25:510:22:in 2nd floor Airvents +0:-767:275:377:60:37:in 2nd floor Airvents +-4:-368:154:51:59:169:in Central Airvent +9:-303:-44:57:493:34:in 1st floor Airvents +4:167:-23:365:28:35:in 1st floor airvents diff --git a/action/tng/teamdepo.aqg b/action/tng/teamdepo.aqg new file mode 100644 index 000000000..a59ab2d4e --- /dev/null +++ b/action/tng/teamdepo.aqg @@ -0,0 +1,58 @@ +# +# Map: teamdepo.aqg +# Build: 2 +# Creator:taipAn +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# +-545:-2127:-34:777:451:236:outside Generator house +-672:-2037:-33:184:162:156:inside Generator house +-604:-1457:-321:417:236:100:in the water +-32:-1482:-336:170:280:69:under the bridge +-127:-725:11:439:492:271:by the big silo +-383:-974:-3:190:205:193:in the generator base +-627:-1447:-146:408:246:122:over the water +-751:-876:-138:265:113:96:on the street from generator base +-1150:-1383:-144:130:300:93:in the garage +-826:-1153:-56:193:40:131:on the stairs from garage +-504:-671:18:102:80:65:in the small book room +-505:-420:33:112:159:101:in the broken door room +-671:-711:10:58:380:82:in the corridor +-866:-859:16:141:52:62:in the corridor, by the hole +-1252:-876:45:230:114:260:in the hole +-822:-303:-68:88:249:144:on stairs from the lobby +-677:-185:-140:52:133:69:in the small storage room +-1095:-356:45:188:187:107:in the archives +-1357:-594:30:68:160:75:in the little room by the hole +-1345:-134:27:59:159:76:in the little room by the radio base +-889:116:-154:262:168:57:in the lobby +-614:-377:3:14:44:67:in the corridor +-612:-633:-1:11:47:54:in the corridor +-1017:-859:38:8:59:75:in the hole +-1335:-765:24:57:12:81:in the hole +-1284:-501:-3:11:55:50:in the archives +-1283:-231:-2:10:39:48:in the archives +-1377:-196:-177:158:516:117:on the street from radio base +-1386:-712:-163:207:63:125:on the street from radio base +-1434:528:-33:725:490:186:in the radio base +-1065:176:71:126:140:104:at the lines in radio base +-1879:203:-129:279:167:69:at the crates in radio base +-1998:991:-138:174:29:82:behind radio house +-2014:742:-39:153:54:167:inside radio house +-45:-1459:-134:177:271:91:on the bridge diff --git a/action/tng/teamdepo.ctf b/action/tng/teamdepo.ctf new file mode 100644 index 000000000..7e5b63738 --- /dev/null +++ b/action/tng/teamdepo.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-57 -1526 -178> +blue=<-2046 506 -178> + +[spawns] +red=<-528 -1944 6 -180>,<-512 -2288 -178 -90>,<-120 -1952 -178 90> +blue=<-1856 160 -162 90>,<-1344 800 -178 -90>,<-984 176 6 -180> \ No newline at end of file diff --git a/action/tng/teamjungle.aqg b/action/tng/teamjungle.aqg new file mode 100644 index 000000000..2b33da390 --- /dev/null +++ b/action/tng/teamjungle.aqg @@ -0,0 +1,32 @@ +# +# Map: teamjungle +# Build: 3 +# Creators: Rootless & Capone of [PAP] + +-1819: 1813: -33: 413: 331: 162:far back on the North side + -896: 1930: 116: 404: 253: 239:high campingpoint on North side + -851: 1258: -213: 331: 398: 175:around the corner near the sucky tree +-1493: 739: -343: 626: 866: 75:open grounds on the North side +-1187: -210: -312: 523: 254: 94:open grounds on the North side +-2052: 644: -211: 239: 959: 123:close to the pointy cliff + -795: 410: 77: 242: 418: 199:North of newbies-cant-get-here point + -134: 337: 210: 448: 234: 139:South of newbies-cant-get-here point + -206: 110: -101: 39: 73: 65:sucky tree in the middle (prob stuck ;P) +-1258: 1582: -286: 28: 72: 115:sucky tree on the North side (prob stuck...huhu) + -340: -8: -349: 48: 185: 43:crack in the middle + -789: 341: -262: 399: 529: 36:low advancingpoint on the North side +-1892: -69: -121: 333: 232: 94:close to the pointy cliff + -789: 338: -139: 426: 566: 117:high advancingpoint on the North side +-1889: -531: 141: 313: 296: 148:high advancingpoint on the North side +-1172: -564: 59: 512: 265: 201:high advancingpoint on the North side + -434: -734: 149: 237: 79: 116:small ledge in the middle + -455: -663: -300: 329: 156: 84:low grounds with the tjockis-tree + -309: -355: -319: 503: 108: 60:open ground in the middle + 56: 76: -251: 304: 342: 114:eastern advancingpoint on the South side + 788: -189: -264: 425: 616: 101:open grounds on South side + 1523: 163: -66: 145: 268: 66:camperpoint on the South side + 1456: -555: -135: 217: 511: 69:far back on the South side + 561: -737: 98: 677: 368: 228:western advancingpoint on the South side + 1187:-1587: 25: 477: 391: 239:pit of desperation on the South side +-1664: -90: -212: 150: 127: 114:the huge cratestair on the North side +-1340: 1991: -107: 197: 174: 92:small bandage point on the North side diff --git a/action/tng/teamjungle.flg b/action/tng/teamjungle.flg new file mode 100644 index 000000000..c109f25b0 --- /dev/null +++ b/action/tng/teamjungle.flg @@ -0,0 +1,3 @@ +#teamjungle +<1564 -1861 -177> +<-675 1962 46> diff --git a/action/tng/temhole.aqg b/action/tng/temhole.aqg new file mode 100644 index 000000000..ff0363adb --- /dev/null +++ b/action/tng/temhole.aqg @@ -0,0 +1,8 @@ +# Map: temhole.aqg +# Build: 1 +# Creator:[AMC]Mloclam +-140: 242: -38: 84: 34: 64:in the garden +167: -233: -43: 79: 25: 64:in the garden +-4: 1: 0: 315: 230: 224:inside the house +-3: 1: -95: 60: 39: 92:staring into Tem's hole +-37: 27: 90: 71: 194: 122:in the rafters diff --git a/action/tng/tjt.ctf b/action/tng/tjt.ctf new file mode 100644 index 000000000..fa25102a3 --- /dev/null +++ b/action/tng/tjt.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-1441 1113 -314> +blue=<1073 -444 -255> + +[spawns] +red=<-1759 1440 -337 -180>,<-1170 754 -337 120>,<-2171 2133 -120 -45> +blue=<416 168 -337 -180>,<1545 -262 -169 -180>,<990 -1643 -81 90> \ No newline at end of file diff --git a/action/tng/tokyo.flg b/action/tng/tokyo.flg new file mode 100644 index 000000000..9cfa38d27 --- /dev/null +++ b/action/tng/tokyo.flg @@ -0,0 +1,3 @@ +#tokyo +<-186 -759 174> +<1345 -2066 206> \ No newline at end of file diff --git a/action/tng/torg.esp b/action/tng/torg.esp new file mode 100644 index 000000000..df55ec1b0 --- /dev/null +++ b/action/tng/torg.esp @@ -0,0 +1,28 @@ +[esp] +type=etv +author=darksaint +name=AQ2Open Masters + +[respawn] +red=7 +blue=7 + +[spawns] +red=<560 1210 238 -11>,<419 1059 446 0>,<510 107 398 68> +blue=<2982 83 640 139>,<2436 2632 640 -74>,<2667 728 238 151> + +[target] +escort=<2833 1288 682> +name=The Grand Prize + +[red_team] +name=maiJaw +leader_name=The Grand Champion +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=SPACE Bartenders +leader_name=The Man +skin=male/ctf_b +leader_skin=male/ctf_vip_b diff --git a/action/tng/urban.aqg b/action/tng/urban.aqg new file mode 100644 index 000000000..e253ba3bf --- /dev/null +++ b/action/tng/urban.aqg @@ -0,0 +1,40 @@ +# +# Map: urban.aqg +# Build: 1 +# Creator:Mike [NW] +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + 9: -886: 841: 475: 570: 213:at top roof + -964: -980: 488: 583: 378: 544:near Ladder building +-1354: -542: 524: 313: 378: 544:at the Coke sign +-1154: 296: 481: 444: 498: 517:near the Construction site + -484: 129: 714: 299: 418: 325:on Middle roof + 9: 194: 539: 448: 391: 550:on the Bridge + -976: 1244: 543: 595: 364: 615:in AQ-sign Alley + 67: 1167: 480: 420: 459: 542:near AQ-Sign + 97: 120: 71: 362: 472: 98:on Street at Bridge + 12: 66: 398: 239: 312: 64:on Bridge roof + 322: 155: 771: 129: 429: 316:on Bridge building roof + 23: 897: 86: 281: 328: 107:on Street at AQ-sign + -694: 884: 221: 86: 228: 224:at Fence at AQ-sign + -547: -711: 139: 1053: 526: 190:on Street below top roof + -962: 19: 10: 268: 800: 107:on Street infront of Const. site + 126: 73: 70: 361: 448: 125:Below bridge diff --git a/action/tng/urban.ctf b/action/tng/urban.ctf new file mode 100644 index 000000000..bf15dfe49 --- /dev/null +++ b/action/tng/urban.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=6 + +[flags] +red=<-1388 1513 366> +blue=<-312 -781 814> + +[spawns] +red=<-1087 1552 302 -90>,<8 952 46 -90>,<200 1369 430 -90> +blue=<-1311 -831 62 0>,<160 -1183 814 135>,<-1445 -1264 494 90> \ No newline at end of file diff --git a/action/tng/urban2.aqg b/action/tng/urban2.aqg new file mode 100644 index 000000000..948e5707c --- /dev/null +++ b/action/tng/urban2.aqg @@ -0,0 +1,50 @@ +# +# Map: urban2.aqg +# Build: 1 +# Creator:MOTSer (Neg,Smiley) +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + -543: 501: 45: 644: 527: 99:in streets around middle building + -450: 320: 203: 267: 254: 63:inside middle building + -436: 316: 588: 310: 299: 298:on middle roof +-1052: -214: 165: 332: 260: 194:around tall ladder building +-1106: -271: 642: 403: 333: 284:on tall ladder roof +-1298: 372: 601: 175: 305: 300:on red tower roof +-1283: 387: 131: 155: 272: 135:at red tower +-1255: 729: 100: 211: 125: 145:at dark spot next to red tower + -497: -515: 71: 205: 391: 77:at dead end + 362: -316: 69: 652: 285: 152:infront of fenced building + 9: -692: 45: 281: 238: 60:at fence area + 319: -645: 307: 608: 271: 200:in fenced building + 548: 330: 98: 429: 468: 114:outside red building + 501: 349: 132: 204: 229: 176:inside red building + 629: 347: 581: 419: 405: 285:on red building roof + 675: 1125: 678: 421: 377: 274:on corner roof + -17: 1241: 640: 285: 272: 247:on top roof + 36: 1166: 444: 233: 177: 60:in top roof hole + 679: 966: 49: 284: 201: 71:in suckyspawn, on groundlevel + 685: 962: 216: 205: 179: 105:in suckyspawn, second floor + 579: 693: 70: 333: 69: 82:at suckyspawn exit + -307: 1240: 187: 69: 269: 249:at boxes and ladder + -778: 1228: 579: 434: 239: 231:on wide building, roof + -781: 1076: 64: 236: 169: 88:at wide building, ground room + -853: 1225: 328: 325: 217: 88:at wide building, sniper room +-1109: 1044: 86: 71: 134: 105:at dark spot, next to wide building diff --git a/action/tng/urban2.flg b/action/tng/urban2.flg new file mode 100644 index 000000000..44bb67926 --- /dev/null +++ b/action/tng/urban2.flg @@ -0,0 +1,3 @@ +#urban2 +<-188 -828 174> +<-751 1269 430> diff --git a/action/tng/urban3.aqg b/action/tng/urban3.aqg new file mode 100644 index 000000000..dba94980e --- /dev/null +++ b/action/tng/urban3.aqg @@ -0,0 +1,54 @@ +# +# Map: urban3.aqg +# Build: 1 +# Creator:SICK1 +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + 1360: 60: 733: 232: 440: 224:at the brown roof + 1375: -639: 754: 217: 251: 110:at the sniper roof, low + 1563:-1252: 762: 93: 353: 152:at the higher crate area + 1037:-1189: 707: 436: 289: 330:at the crate area + 628:-1304: 308: 73: 167: 184:at the foot of the small ladder + 288: -924: 727: 742: 677: 347:on the shelf leading to the roof of death + -393: -896: 728: 317: 257: 346:at the roof of death + -447: -409: 327: 250: 355: 314:at the fence area + -570: 389: 746: 119: 445: 192:by the crates behind the tall sniper plazas + 123: 441: 818: 401: 197: 288:on the sniper plaza with crates + 808: 400: 826: 300: 147: 247:on the tallest sniper plaza + 1358: 448: 549: 239: 189: 418:at the area by the tallest ladder + 1338: 773: 742: 260: 132: 231:on the double jump roof + 737: 580: 559: 353: 317: 175:at the top of the tallest ladder + 126: 759: 734: 579: 122: 219:behind the sniper plazas + -357: 704: 619: 93: 195: 153:behind the sniper plazas + 671: -450: 599: 359: 203: 475:on the middle roof + 478: 0: 66: 670: 254: 59:on the street near the tallest ladder + 467: -885: 63: 663: 246: 62:on the street near the small building + 125:-1312: 359: 305: 158: 234:on the top of the small building + 218:-1292: 54: 215: 134: 62:inside the small building + 1276: -3: 332: 119: 224: 143:inside the brown building + 752: 443: 191: 360: 188: 193:at the foot of the tallest ladder + -392: 142: 91: 200: 398: 88:in the alley + -472: -833: 445: 136: 63: 61:inside by the roof of death + 25: 403: 571: 269: 134: 59:inside the higher sniper room + 1304: -581: 566: 137: 181: 51:inside the lower sniper room + -6:-1550: 293: 457: 70: 63:under the deathroof shelf + -324:-1382: 291: 128: 230: 64:under the deathroof shelf + 886: -446: 58: 688: 192: 61:around the middle roof + 497:-1308: 60: 48: 156: 62:in the alley by the small building diff --git a/action/tng/urban3.flg b/action/tng/urban3.flg new file mode 100644 index 000000000..bddafbb30 --- /dev/null +++ b/action/tng/urban3.flg @@ -0,0 +1,3 @@ +#urban3 +<-481 -1009 430> +<1330 455 302> diff --git a/action/tng/urban4.aqg b/action/tng/urban4.aqg new file mode 100644 index 000000000..2fcb37167 --- /dev/null +++ b/action/tng/urban4.aqg @@ -0,0 +1,55 @@ +# +# Map: urban4.aqg +# Build: 1 +# Creator:Marx (AFH^BiG PIG) +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# + + -20: -21: 270: 688: 239: 113:at Interstate 666 +-1212: -189: 986: 509: 317: 97:on top roof +-1028: 37: 454: 323: 153: 77:at the platform under top roof + -962: -5: 635: 245: 116: 245:inside ladder building + -830: 703: 820: 123: 186: 52:at scout tower's roof + -572: 699: 635: 131: 194: 131:at the platform outside scout tower + -710: 710: 305: 247: 187: 321:inside scout tower + -262: 2: 84: 436: 189: 82:under Insterstate 666 +-1601: 1089: 430: 122: 252: 47:at the red lone platform near scout tower + -256: 318: 67: 447: 192: 133:outside Interstate 666 + -259: -352: 665: 445: 96: 136:at platform beside top roof, near Interstate 666 + -253: -313: 385: 447: 120: 123:in room beside Ladder building, facing Interstate 666 + -774: -192: 449: 63: 195: 64:in room beside Ladder building, facing Interstate 666 + 598: -101: 265: 254: 296: 45:Interstate 666 + 289: -286: 822: 95: 161: 56:on big red platform beside near top roof, at Interstate 666 + 636: -101: 740: 195: 410: 103:at the great sniping place + 487: 610: 666: 31: 285: 80:over platform near sniping place + 194: 768: 462: 255: 256: 71:at platform with cubes near Interstate 666 + -160: 1122: 488: 550: 91: 96:in hallway near Interstate 666 + -246: 765: 120: 178: 262: 132:outside Scout tower + -699: 960: 322: 256: 68: 312:outside scout tower + -503: 839: 318: 74: 73: 191:in the area with a cube outside scout tower + 32: 1182: 821: 480: 159: 185:over hallway near scout tower + 187: 612: 190: 257: 106: 189:in the apartment near Interstate 666 +-1602: 997: 83: 114: 151: 73:inside room under red house +-1599: 798: 203: 125: 35: 197:in alley to red house with ladder +-1583: 575: 853: 142: 189: 90:at big white Block o' flats roof top +-1586: 14: 723: 141: 144: 160:at red house roof top, next to ladder house +-1603: 258: 489: 121: 129: 106:in the red area with a cube near ladderhouse +-1212: 605: 213: 262: 739: 206:in the big alley +-1244: 1410: 138: 224: 61: 141:in small apartment, facing Ladder house diff --git a/action/tng/urban4.ctf b/action/tng/urban4.ctf new file mode 100644 index 000000000..e36c7b5da --- /dev/null +++ b/action/tng/urban4.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=Raptor007 +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<366 640 289> +blue=<-1152 -64 417> + +[spawns] +red=<-670 -87 33 0>,<432 -336 233 90>,<-128 -300 417 180> +blue=<-1536 1120 33 270>,<-1184 1440 161 180>,<-1664 1280 417 270> diff --git a/action/tng/urban4.dom b/action/tng/urban4.dom new file mode 100644 index 000000000..042015bc9 --- /dev/null +++ b/action/tng/urban4.dom @@ -0,0 +1,2 @@ +[dom] +flags=<0 96 224>,<-576 1088 408>,<-1400 256 24> diff --git a/action/tng/urban4.esp b/action/tng/urban4.esp new file mode 100644 index 000000000..f7e043e69 --- /dev/null +++ b/action/tng/urban4.esp @@ -0,0 +1,36 @@ +[esp] +type=etv +author=darksaint +name=Urban Gang Wars + +[respawn] +red=7 +blue=7 +green=7 + +[spawns] +red=<274 1165 446 -148>,<553 -293 277 90>,<-783 -64 444 -60>,<342 632 319 180> +blue=<-1404 -13 59 90>,<-1548 1077 61 -90>,<-1387 1402 179 0>,<-1601 206 452 0> +green=<-635 592 65 45>,<-900 573 433 0>,<-660 -112 46 0>,<-904 826 473 -45> + +[target] +escort=<-774 70 814> +name=Tower Window + +[red_team] +name=The B-Team +leader_name=Mister Rogers +skin=male/ctf_r +leader_skin=male/ctf_vip_r + +[blue_team] +name=Peacekeepers +leader_name=Mister McFeely +skin=male/ctf_b +leader_skin=male/ctf_vip_b + +[green_team] +name=Landscaping Crew +leader_name=King Friday +skin=male/ctf_g +leader_skin=male/ctf_vip_g diff --git a/action/tng/urbanjungle.ctf b/action/tng/urbanjungle.ctf new file mode 100644 index 000000000..9d19d4981 --- /dev/null +++ b/action/tng/urbanjungle.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-219 1023 -754> +blue=<3711 1452 -642> + +[spawns] +red=<-224 2192 -523 0>,<32 320 -533 0>,<1342 46 -882 90> +blue=<3920 944 -882 -180>,<3872 1376 -450 -180>,<2473 2359 -882 -90> \ No newline at end of file diff --git a/action/tng/waldoworldarena.dom b/action/tng/waldoworldarena.dom new file mode 100644 index 000000000..4257dc9fb --- /dev/null +++ b/action/tng/waldoworldarena.dom @@ -0,0 +1,2 @@ +[dom] +flags=<1152 608 128> diff --git a/action/tng/wfall.ctf b/action/tng/wfall.ctf new file mode 100644 index 000000000..9b4630785 --- /dev/null +++ b/action/tng/wfall.ctf @@ -0,0 +1,15 @@ +[ctf] +type=balanced +author=TgT + +[respawn] +red=6 +blue=6 + +[flags] +red=<944 -1480 238> +blue=<-592 695 302> + +[spawns] +red=<1113 -1895 238 136>,<-447 -1495 238 0>,<-942 -1813 366 48> +blue=<-952 624 302 -20>,<861 767 403 -88>,<-908 6 46 -2> diff --git a/action/tng/winter.aqg b/action/tng/winter.aqg new file mode 100644 index 000000000..e153f13b1 --- /dev/null +++ b/action/tng/winter.aqg @@ -0,0 +1,36 @@ +# +# Map: winter.aqg +# Build: 1 +# Creator:EzekielRage +# +# format is: +# +# x:y:z:rX:rY:rZ:name +# +# where "x:y:z" is the location coordinate +# "rX,rY" is the range for the location marker (if 0 then 1500 vector +# points) +# "rZ" is the height range for the location marker (if 0 then not use +# it) +# "name" is the name of the location marker and is used in "%L" and "%S" +# +# aqg_location 1 - Set corner 1 on the location box. +# aqg_location 2 - Set corner 2 on the location box. +# aqg_location calc - Displays the location box coordinates/ranges. +# aqg_location checkL - works like %L, but you don't need to be alive or on a team +# aqg_location checkS - works like %S, but you don't need to be alive or on a team +# +# +-1671: -798: 98: 897: 676: 267:at the pit +-1074: -495: 86: 210: 131: 216:at the pit +-1360: -351: -6: 184: 157: 114:at the pit + -812: 1168: 112: 338: 173: 69:in the cave + -949: 1034: 115: 120: 312: 95:in the cave + 249:-1789: -67: 151: 361: 133:in the cave at the waterfall + -90:-1813: -90: 294: 362: 129:in the cave at the waterfall + -316: 510: -344: 138: 487: 169:in the waterhole + -357: -243: 60: 435: 1249: 301:near the waterfall +-1704: 246: 42: 902: 793: 261:in the area near the cave +-1159: -252: 75: 391: 302: 264:in the area near the cave + 504: -232: 23: 531: 1259: 281:in the area near waterfall cave +-1966:-1052: -556: 407: 750: 324:in the black pit diff --git a/action/tng/winter.ctf b/action/tng/winter.ctf new file mode 100644 index 000000000..f94bd5525 --- /dev/null +++ b/action/tng/winter.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-2283 812 97> +blue=<820 -810 -75> + +[spawns] +red=<-1776 -336 -114 0>,<-2112 784 99 0>,<-2416 32 -114 0> +blue=<420 552 -114 0>,<400 -1184 -114 0>,<966 855 110 -180> \ No newline at end of file diff --git a/action/tng/winter.flg b/action/tng/winter.flg new file mode 100644 index 000000000..ba745f4ae --- /dev/null +++ b/action/tng/winter.flg @@ -0,0 +1,3 @@ +# winter +<-2320 276 -20> +<786 -622 -75> \ No newline at end of file diff --git a/action/tng/zoo.ctf b/action/tng/zoo.ctf new file mode 100644 index 000000000..f0ebc0ee7 --- /dev/null +++ b/action/tng/zoo.ctf @@ -0,0 +1,16 @@ +[ctf] +type=balanced +author=matic +grapple=no + +[respawn] +red=5 +blue=5 + +[flags] +red=<-101 880 206> +blue=<-1533 -1725 110> + +[spawns] +red=<152 881 206 -135>,<-181 375 39 90>,<731 -222 214 120> +blue=<-1714 -1494 112 0>,<-982 -1287 110 120>,<-2127 -979 270 0> \ No newline at end of file From 11815fe50240033ea34d682f463194fc098946df Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 16 Aug 2024 16:23:11 -0400 Subject: [PATCH 555/974] Added last_bot_chat_time to reduce bot chat spam with lots of bots --- src/action/botlib/botlib_communication.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index b0b319b78..183dc4af3 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -3,6 +3,8 @@ #include "botlib.h" #include "../m_player.h" // For waving frame types, i.e: FRAME_flip01 +// Bot chat +int last_bot_chat_time; // So the chat isn't super spammy with lots of bots // // Delayed chat, somewhat more realistic @@ -288,13 +290,17 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) } else if (randval > 0.2) { // 80% do not chat //gi.dprintf("Skipping chat due to random chance (%f)\n", randval); return; // Don't chat too often + } else if (level.framenum - last_bot_chat_time < 5 * HZ) { // Check if the last chat was within the last so many frames + return; // Slow down the spam! } // Add the message to the queue if delayed - if (delayed) + if (delayed) { AddMessageToChatQueue(bot, text, level.framenum); - else // instant message, such as a goodbye before leaving + } else { // instant message, such as a goodbye before leaving BOTLIB_Say(bot, text, false); + last_bot_chat_time = level.framenum; + } // Sets the current level time as the last chat time so the bot doesn't spam chat bot->bot.lastChatTime = level.framenum; From 3ad194357df70d49f3dcf1d4883f22fb7092dcaa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 19:58:48 +0300 Subject: [PATCH 556/974] More refresh code style cleanup. --- src/refresh/gl.h | 6 +- src/refresh/images.c | 244 +++++++++++++++++------------------------- src/refresh/legacy.c | 46 ++++---- src/refresh/main.c | 158 ++++++++++----------------- src/refresh/mesh.c | 61 +++++------ src/refresh/models.c | 75 +++++-------- src/refresh/shader.c | 20 ++-- src/refresh/sky.c | 45 ++++---- src/refresh/state.c | 99 ++++++++--------- src/refresh/surf.c | 25 ++--- src/refresh/texture.c | 35 +++--- 11 files changed, 333 insertions(+), 481 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index a528977ed..19cd81967 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -562,16 +562,14 @@ static inline void GL_ArrayBits(GLbitfield bits) static inline void GL_LockArrays(GLsizei count) { - if (qglLockArraysEXT) { + if (qglLockArraysEXT) qglLockArraysEXT(0, count); - } } static inline void GL_UnlockArrays(void) { - if (qglUnlockArraysEXT) { + if (qglUnlockArraysEXT) qglUnlockArraysEXT(); - } } static inline void GL_ForceMatrix(const GLfloat *matrix) diff --git a/src/refresh/images.c b/src/refresh/images.c index 75f1b251d..ff1379782 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -216,15 +216,13 @@ static int load_pcx(const byte *rawdata, size_t rawlen, // // parse the PCX file // - if (rawlen < sizeof(dpcx_t)) { + if (rawlen < sizeof(dpcx_t)) return Q_ERR_FILE_TOO_SMALL; - } pcx = (const dpcx_t *)rawdata; - if (pcx->manufacturer != 10 || pcx->version != 5) { + if (pcx->manufacturer != 10 || pcx->version != 5) return Q_ERR_UNKNOWN_FORMAT; - } if (pcx->encoding != 1 || pcx->bits_per_pixel != 8) { Com_SetLastError("Unsupported encoding or bits per pixel"); @@ -253,9 +251,8 @@ static int load_pcx(const byte *rawdata, size_t rawlen, // get palette // if (palette) { - if (rawlen < PCX_PALETTE_SIZE) { + if (rawlen < PCX_PALETTE_SIZE) return Q_ERR_FILE_TOO_SMALL; - } memcpy(palette, rawdata + rawlen - PCX_PALETTE_SIZE, PCX_PALETTE_SIZE); } @@ -342,9 +339,8 @@ IMG_LOAD(WAL) const miptex_t *mt; unsigned w, h, offset; - if (rawlen < sizeof(miptex_t)) { + if (rawlen < sizeof(miptex_t)) return Q_ERR_FILE_TOO_SMALL; - } mt = (const miptex_t *)rawdata; @@ -696,14 +692,12 @@ static int IMG_SaveTGA(const screenshot_t *s) WL16(&header[14], s->height); header[16] = 24; // pixel size - if (!fwrite(&header, sizeof(header), 1, s->fp)) { + if (!fwrite(&header, sizeof(header), 1, s->fp)) return Q_ERR_FAILURE; - } byte *row = malloc(s->width * 3); - if (!row) { + if (!row) return Q_ERR(ENOMEM); - } int ret = Q_ERR_SUCCESS; for (int i = 0; i < s->height; i++) { @@ -776,9 +770,8 @@ static int my_jpeg_start_decompress(j_decompress_ptr cinfo, const byte *rawdata, { my_error_ptr jerr = (my_error_ptr)cinfo->err; - if (setjmp(jerr->setjmp_buffer)) { + if (setjmp(jerr->setjmp_buffer)) return Q_ERR_LIBRARY_ERROR; - } jpeg_create_decompress(cinfo); jpeg_mem_src(cinfo, rawdata, rawlen); @@ -803,7 +796,7 @@ static int my_jpeg_start_decompress(j_decompress_ptr cinfo, const byte *rawdata, return Q_ERR_INVALID_FORMAT; } - return 0; + return Q_ERR_SUCCESS; } static int my_jpeg_finish_decompress(j_decompress_ptr cinfo, JSAMPARRAY row_pointers) @@ -817,7 +810,7 @@ static int my_jpeg_finish_decompress(j_decompress_ptr cinfo, JSAMPARRAY row_poin jpeg_read_scanlines(cinfo, &row_pointers[cinfo->output_scanline], cinfo->output_height - cinfo->output_scanline); jpeg_finish_decompress(cinfo); - return 0; + return Q_ERR_SUCCESS; } IMG_LOAD(JPG) @@ -863,9 +856,8 @@ static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, const { my_error_ptr jerr = (my_error_ptr)cinfo->err; - if (setjmp(jerr->setjmp_buffer)) { + if (setjmp(jerr->setjmp_buffer)) return Q_ERR_LIBRARY_ERROR; - } jpeg_create_compress(cinfo); jpeg_stdio_dest(cinfo, s->fp); @@ -882,7 +874,7 @@ static int my_jpeg_compress(j_compress_ptr cinfo, JSAMPARRAY row_pointers, const jpeg_write_scanlines(cinfo, row_pointers, s->height); jpeg_finish_compress(cinfo); - return 0; + return Q_ERR_SUCCESS; } static int IMG_SaveJPG(const screenshot_t *s) @@ -898,12 +890,11 @@ static int IMG_SaveJPG(const screenshot_t *s) jerr.filename = s->async ? NULL : s->filename; row_pointers = malloc(sizeof(JSAMPROW) * s->height); - if (!row_pointers) { + if (!row_pointers) return Q_ERR(ENOMEM); - } - for (i = 0; i < s->height; i++) { + + for (i = 0; i < s->height; i++) row_pointers[i] = (JSAMPROW)(s->pixels + (s->height - i - 1) * s->rowbytes); - } ret = my_jpeg_compress(&cinfo, row_pointers, s); free(row_pointers); @@ -971,17 +962,15 @@ static int my_png_read_header(png_structp png_ptr, png_infop info_ptr, png_uint_32 w, h, has_tRNS; int bitdepth, colortype; - if (setjmp(err->setjmp_buffer)) { + if (setjmp(err->setjmp_buffer)) return Q_ERR_LIBRARY_ERROR; - } png_set_read_fn(png_ptr, io_ptr, my_png_read_fn); png_read_info(png_ptr, info_ptr); - if (!png_get_IHDR(png_ptr, info_ptr, &w, &h, &bitdepth, &colortype, NULL, NULL, NULL)) { + if (!png_get_IHDR(png_ptr, info_ptr, &w, &h, &bitdepth, &colortype, NULL, NULL, NULL)) return Q_ERR_FAILURE; - } if (check_image_size(w, h)) { Com_SetLastError("Invalid image dimensions"); @@ -993,25 +982,22 @@ static int my_png_read_header(png_structp png_ptr, png_infop info_ptr, png_set_palette_to_rgb(png_ptr); break; case PNG_COLOR_TYPE_GRAY: - if (bitdepth < 8) { + if (bitdepth < 8) png_set_expand_gray_1_2_4_to_8(png_ptr); - } // fall through case PNG_COLOR_TYPE_GRAY_ALPHA: png_set_gray_to_rgb(png_ptr); break; } - if (bitdepth < 8) { + if (bitdepth < 8) png_set_packing(png_ptr); - } else if (bitdepth == 16) { + else if (bitdepth == 16) png_set_strip_16(png_ptr); - } has_tRNS = png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS); - if (has_tRNS) { + if (has_tRNS) png_set_tRNS_to_alpha(png_ptr); - } png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER); @@ -1028,20 +1014,19 @@ static int my_png_read_header(png_structp png_ptr, png_infop info_ptr, if (!has_tRNS && !(colortype & PNG_COLOR_MASK_ALPHA)) image->flags |= IF_OPAQUE; - return 0; + return Q_ERR_SUCCESS; } static int my_png_read_image(png_structp png_ptr, png_infop info_ptr, png_bytepp row_pointers) { my_png_error *err = png_get_error_ptr(png_ptr); - if (setjmp(err->setjmp_buffer)) { + if (setjmp(err->setjmp_buffer)) return Q_ERR_LIBRARY_ERROR; - } png_read_image(png_ptr, row_pointers); png_read_end(png_ptr, info_ptr); - return 0; + return Q_ERR_SUCCESS; } IMG_LOAD(PNG) @@ -1086,9 +1071,8 @@ IMG_LOAD(PNG) rowbytes = image->width * 4; pixels = IMG_AllocPixels(h * rowbytes); - for (row = 0; row < h; row++) { + for (row = 0; row < h; row++) row_pointers[row] = (png_bytep)(pixels + row * rowbytes); - } ret = my_png_read_image(png_ptr, info_ptr, row_pointers); if (ret < 0) { @@ -1107,9 +1091,8 @@ static int my_png_write_image(png_structp png_ptr, png_infop info_ptr, { my_png_error *err = png_get_error_ptr(png_ptr); - if (setjmp(err->setjmp_buffer)) { + if (setjmp(err->setjmp_buffer)) return Q_ERR_LIBRARY_ERROR; - } png_init_io(png_ptr, s->fp); png_set_IHDR(png_ptr, info_ptr, s->width, s->height, 8, PNG_COLOR_TYPE_RGB, @@ -1117,7 +1100,7 @@ static int my_png_write_image(png_structp png_ptr, png_infop info_ptr, png_set_compression_level(png_ptr, Q_clip(s->param, 0, 9)); png_set_rows(png_ptr, info_ptr, row_pointers); png_write_png(png_ptr, info_ptr, s->bpp == 4 ? PNG_TRANSFORM_STRIP_FILLER_AFTER : 0, NULL); - return 0; + return Q_ERR_SUCCESS; } static int IMG_SavePNG(const screenshot_t *s) @@ -1148,9 +1131,9 @@ static int IMG_SavePNG(const screenshot_t *s) ret = Q_ERR(ENOMEM); goto fail; } - for (i = 0; i < s->height; i++) { + + for (i = 0; i < s->height; i++) row_pointers[i] = (png_bytep)(s->pixels + (s->height - i - 1) * s->rowbytes); - } ret = my_png_write_image(png_ptr, info_ptr, row_pointers, s); free(row_pointers); @@ -1217,31 +1200,31 @@ static int create_screenshot(char *buffer, size_t size, FILE **f, if (name && *name) { // save to user supplied name - if (FS_NormalizePathBuffer(temp, name, sizeof(temp)) >= sizeof(temp)) { + if (FS_NormalizePathBuffer(temp, name, sizeof(temp)) >= sizeof(temp)) return Q_ERR(ENAMETOOLONG); - } + FS_CleanupPath(temp); - if (Q_snprintf(buffer, size, "%s/screenshots/%s%s", fs_gamedir, temp, ext) >= size) { + + if (Q_snprintf(buffer, size, "%s/screenshots/%s%s", fs_gamedir, temp, ext) >= size) return Q_ERR(ENAMETOOLONG); - } - if ((ret = FS_CreatePath(buffer)) < 0) { + + if ((ret = FS_CreatePath(buffer)) < 0) return ret; - } - if (!(*f = fopen(buffer, "wb"))) { + + if (!(*f = fopen(buffer, "wb"))) return Q_ERRNO; - } - return 0; + + return Q_ERR_SUCCESS; } width = parse_template(r_screenshot_template, temp, sizeof(temp)); // create the directory - if (Q_snprintf(buffer, size, "%s/screenshots/%s", fs_gamedir, temp) >= size) { + if (Q_snprintf(buffer, size, "%s/screenshots/%s", fs_gamedir, temp) >= size) return Q_ERR(ENAMETOOLONG); - } - if ((ret = FS_CreatePath(buffer)) < 0) { + + if ((ret = FS_CreatePath(buffer)) < 0) return ret; - } count = 1; for (i = 0; i < width; i++) @@ -1249,16 +1232,15 @@ static int create_screenshot(char *buffer, size_t size, FILE **f, // find a file name to save it to for (i = 0; i < count; i++) { - if (Q_snprintf(buffer, size, "%s/screenshots/%s%0*d%s", fs_gamedir, temp, width, i, ext) >= size) { + if (Q_snprintf(buffer, size, "%s/screenshots/%s%0*d%s", fs_gamedir, temp, width, i, ext) >= size) return Q_ERR(ENAMETOOLONG); - } - if ((*f = Q_fopen(buffer, "wxb"))) { - return 0; - } + + if ((*f = Q_fopen(buffer, "wxb"))) + return Q_ERR_SUCCESS; + ret = Q_ERRNO; - if (ret != Q_ERR(EEXIST)) { + if (ret != Q_ERR(EEXIST)) return ret; - } } return Q_ERR_OUT_OF_SLOTS; @@ -1357,11 +1339,11 @@ static void IMG_ScreenShot_f(void) return; } - if (Cmd_Argc() > 1) { + if (Cmd_Argc() > 1) s = Cmd_Argv(1); - } else { + else s = r_screenshot_format->string; - } +#endif #if USE_JPG if (*s == 'j') { @@ -1380,7 +1362,6 @@ static void IMG_ScreenShot_f(void) return; } #endif -#endif // USE_JPG || USE_PNG #if USE_TGA make_screenshot(NULL, ".tga", IMG_SaveTGA, false, 0); @@ -1420,11 +1401,10 @@ static void IMG_ScreenShotJPG_f(void) return; } - if (Cmd_Argc() > 2) { + if (Cmd_Argc() > 2) quality = Q_atoi(Cmd_Argv(2)); - } else { + else quality = r_screenshot_quality->integer; - } make_screenshot(Cmd_Argv(1), ".jpg", IMG_SaveJPG, r_screenshot_async->integer > 1, quality); @@ -1441,11 +1421,10 @@ static void IMG_ScreenShotPNG_f(void) return; } - if (Cmd_Argc() > 2) { + if (Cmd_Argc() > 2) compression = Q_atoi(Cmd_Argv(2)); - } else { + else compression = r_screenshot_compression->integer; - } make_screenshot(Cmd_Argv(1), ".png", IMG_SavePNG, r_screenshot_async->integer > 0, compression); @@ -1524,21 +1503,21 @@ IMG_List_f static void IMG_List_f(void) { static const char types[8] = "PFMSWY??"; - image_t *image; - const char *wildcard = NULL; - bool missing = false; - int paletted = 0; - int i, c, mask = 0, count; - size_t texels; + const image_t *image; + const char *wildcard = NULL; + bool missing = false; + int paletted = 0; + int i, c, mask = 0, count; + size_t texels; while ((c = Cmd_ParseOptions(o_imagelist)) != -1) { switch (c) { - case 'p': mask |= 1 << IT_PIC; break; - case 'f': mask |= 1 << IT_FONT; break; - case 'm': mask |= 1 << IT_SKIN; break; - case 's': mask |= 1 << IT_SPRITE; break; - case 'w': mask |= 1 << IT_WALL; break; - case 'y': mask |= 1 << IT_SKY; break; + case 'p': mask |= BIT(IT_PIC); break; + case 'f': mask |= BIT(IT_FONT); break; + case 'm': mask |= BIT(IT_SKIN); break; + case 's': mask |= BIT(IT_SPRITE); break; + case 'w': mask |= BIT(IT_WALL); break; + case 'y': mask |= BIT(IT_SKY); break; case '8': paletted = 1; break; case 'r': paletted = -1; break; case 'x': missing = true; break; @@ -1575,7 +1554,7 @@ static void IMG_List_f(void) for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { if (!image->name[0]) continue; - if (mask && !(mask & (1 << image->type))) + if (mask && !(mask & BIT(image->type))) continue; if (wildcard && !Com_WildCmp(wildcard, image->name)) continue; @@ -1642,15 +1621,12 @@ static image_t *lookup_image(const char *name, // look for it LIST_FOR_EACH(image_t, image, &r_imageHash[hash], entry) { - if (image->type != type) { + if (image->type != type) continue; - } - if (image->baselen != baselen) { + if (image->baselen != baselen) continue; - } - if (!FS_pathcmpn(image->name, name, baselen)) { + if (!FS_pathcmpn(image->name, name, baselen)) return image; - } } return NULL; @@ -1658,14 +1634,13 @@ static image_t *lookup_image(const char *name, static int try_image_format(imageformat_t fmt, image_t *image, byte **pic) { - byte *data; + void *data; int ret; // load the file - ret = FS_LoadFile(image->name, (void **)&data); - if (!data) { + ret = FS_LoadFile(image->name, &data); + if (!data) return ret; - } // decompress the image ret = img_loaders[fmt].load(data, ret, image, pic); @@ -1693,21 +1668,18 @@ static int try_other_formats(imageformat_t orig, image_t *image, byte **pic) // search through all the 32-bit formats for (i = 0; i < img_total; i++) { fmt = img_search[i]; - if (fmt == orig) { + if (fmt == orig) continue; // don't retry twice - } ret = try_replace_ext(fmt, image, pic); - if (ret != Q_ERR(ENOENT)) { + if (ret != Q_ERR(ENOENT)) return ret; // found something - } } // fall back to 8-bit formats fmt = (image->type == IT_WALL) ? IM_WAL : IM_PCX; - if (fmt == orig) { + if (fmt == orig) return Q_ERR(ENOENT); // don't retry twice - } return try_replace_ext(fmt, image, pic); } @@ -1722,9 +1694,8 @@ static void get_image_dimensions(imageformat_t fmt, image_t *image) memcpy(buffer + image->baselen + 1, img_loaders[fmt].ext, 4); FS_OpenFile(buffer, &f, FS_MODE_READ | FS_FLAG_LOADFILE); - if (!f) { + if (!f) return; - } w = h = 0; if (fmt == IM_WAL) { @@ -1743,9 +1714,8 @@ static void get_image_dimensions(imageformat_t fmt, image_t *image) FS_CloseFile(f); - if (check_image_size(w, h)) { + if (check_image_size(w, h)) return; - } image->width = w; image->height = h; @@ -1868,15 +1838,13 @@ static int load_image_data(image_t *image, imageformat_t fmt, bool need_dimensio // if we are replacing 8-bit texture with a higher resolution 32-bit // texture, we need to recover original image dimensions - if (need_dimensions && fmt <= IM_WAL && ret > IM_WAL) { + if (need_dimensions && fmt <= IM_WAL && ret > IM_WAL) get_image_dimensions(fmt, image); - } #else - if (fmt == IM_MAX) { + if (fmt == IM_MAX) ret = Q_ERR_INVALID_PATH; - } else { + else ret = try_image_format(fmt, image, pic); - } #endif return ret; @@ -1983,11 +1951,9 @@ static image_t *find_or_load_image(const char *name, size_t len, image->registration_sequence = r_registration_sequence; // find out original extension - for (fmt = 0; fmt < IM_MAX; fmt++) { - if (!Q_stricmp(image->name + image->baselen + 1, img_loaders[fmt].ext)) { + for (fmt = 0; fmt < IM_MAX; fmt++) + if (!Q_stricmp(image->name + image->baselen + 1, img_loaders[fmt].ext)) break; - } - } // load the pic from disk pic = NULL; @@ -2041,9 +2007,9 @@ image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) Q_assert(name); - if ((image = find_or_load_image(name, strlen(name), type, flags))) { + if ((image = find_or_load_image(name, strlen(name), type, flags))) return image; - } + return R_NOTEXTURE; } @@ -2072,14 +2038,12 @@ qhandle_t R_RegisterImage(const char *name, imagetype_t type, imageflags_t flags Q_assert(name); // empty names are legal, silently ignore them - if (!*name) { + if (!*name) return 0; - } // no images = not initialized - if (!r_numImages) { + if (!r_numImages) return 0; - } if (type == IT_SKIN || type == IT_SPRITE) { len = FS_NormalizePathBuffer(fullname, name, sizeof(fullname)); @@ -2098,9 +2062,9 @@ qhandle_t R_RegisterImage(const char *name, imagetype_t type, imageflags_t flags return 0; } - if ((image = find_or_load_image(fullname, len, type, flags))) { + if ((image = find_or_load_image(fullname, len, type, flags))) return image - r_images; - } + return 0; } @@ -2111,14 +2075,13 @@ R_GetPicSize */ bool R_GetPicSize(int *w, int *h, qhandle_t pic) { - image_t *image = IMG_ForHandle(pic); + const image_t *image = IMG_ForHandle(pic); - if (w) { + if (w) *w = image->width; - } - if (h) { + if (h) *h = image->height; - } + return image->flags & IF_TRANSPARENT; } @@ -2153,9 +2116,8 @@ void IMG_FreeUnused(void) count++; } - if (count) { + if (count) Com_DPrintf("%s: %i images freed\n", __func__, count); - } } void IMG_FreeAll(void) @@ -2173,13 +2135,11 @@ void IMG_FreeAll(void) count++; } - if (count) { + if (count) Com_DPrintf("%s: %i images freed\n", __func__, count); - } - for (i = 0; i < RIMAGES_HASH; i++) { + for (i = 0; i < RIMAGES_HASH; i++) List_Init(&r_imageHash[i]); - } // &r_images[0] == R_NOTEXTURE r_numImages = 1; @@ -2198,21 +2158,18 @@ void IMG_GetPalette(void) // get the palette ret = FS_LoadFile(R_COLORMAP_PCX, (void **)&data); - if (!data) { + if (!data) goto fail; - } ret = load_pcx(data, ret, NULL, pal, NULL); FS_FreeFile(data); - if (ret < 0) { + if (ret < 0) goto fail; - } - for (i = 0, src = pal; i < 255; i++, src += 3) { + for (i = 0, src = pal; i < 255; i++, src += 3) d_8to24table[i] = MakeColor(src[0], src[1], src[2], 255); - } // 255 is transparent d_8to24table[i] = MakeColor(src[0], src[1], src[2], 0); @@ -2272,9 +2229,8 @@ void IMG_Init(void) Cmd_Register(img_cmd); - for (i = 0; i < RIMAGES_HASH; i++) { + for (i = 0; i < RIMAGES_HASH; i++) List_Init(&r_imageHash[i]); - } // &r_images[0] == R_NOTEXTURE r_numImages = 1; diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index ba5556d2b..22190e706 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -23,25 +23,22 @@ static void legacy_state_bits(GLbitfield bits) { GLbitfield diff = bits ^ gls.state_bits; - if (diff & GLS_COMMON_MASK) { + if (diff & GLS_COMMON_MASK) GL_CommonStateBits(bits); - } if (diff & GLS_ALPHATEST_ENABLE) { - if (bits & GLS_ALPHATEST_ENABLE) { + if (bits & GLS_ALPHATEST_ENABLE) qglEnable(GL_ALPHA_TEST); - } else { + else qglDisable(GL_ALPHA_TEST); - } } if (diff & GLS_TEXTURE_REPLACE) { GL_ActiveTexture(0); - if (bits & GLS_TEXTURE_REPLACE) { + if (bits & GLS_TEXTURE_REPLACE) qglTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); - } else { + else qglTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); - } } if (diff & GLS_SCROLL_MASK) { @@ -58,11 +55,10 @@ static void legacy_state_bits(GLbitfield bits) if (diff & GLS_LIGHTMAP_ENABLE) { GL_ActiveTexture(1); - if (bits & GLS_LIGHTMAP_ENABLE) { + if (bits & GLS_LIGHTMAP_ENABLE) qglEnable(GL_TEXTURE_2D); - } else { + else qglDisable(GL_TEXTURE_2D); - } } if ((diff & GLS_WARP_ENABLE) && gl_static.programs[0]) { @@ -78,11 +74,10 @@ static void legacy_state_bits(GLbitfield bits) } if (diff & GLS_SHADE_SMOOTH) { - if (bits & GLS_SHADE_SMOOTH) { + if (bits & GLS_SHADE_SMOOTH) qglShadeModel(GL_SMOOTH); - } else { + else qglShadeModel(GL_FLAT); - } } } @@ -91,37 +86,33 @@ static void legacy_array_bits(GLbitfield bits) GLbitfield diff = bits ^ gls.array_bits; if (diff & GLA_VERTEX) { - if (bits & GLA_VERTEX) { + if (bits & GLA_VERTEX) qglEnableClientState(GL_VERTEX_ARRAY); - } else { + else qglDisableClientState(GL_VERTEX_ARRAY); - } } if (diff & GLA_TC) { GL_ClientActiveTexture(0); - if (bits & GLA_TC) { + if (bits & GLA_TC) qglEnableClientState(GL_TEXTURE_COORD_ARRAY); - } else { + else qglDisableClientState(GL_TEXTURE_COORD_ARRAY); - } } if (diff & GLA_LMTC) { GL_ClientActiveTexture(1); - if (bits & GLA_LMTC) { + if (bits & GLA_LMTC) qglEnableClientState(GL_TEXTURE_COORD_ARRAY); - } else { + else qglDisableClientState(GL_TEXTURE_COORD_ARRAY); - } } if (diff & GLA_COLOR) { - if (bits & GLA_COLOR) { + if (bits & GLA_COLOR) qglEnableClientState(GL_COLOR_ARRAY); - } else { + else qglDisableClientState(GL_COLOR_ARRAY); - } } } @@ -213,9 +204,8 @@ static void legacy_init(void) { GLuint prog = 0; - if (!qglGenProgramsARB) { + if (!qglGenProgramsARB) return; - } GL_ClearErrors(); diff --git a/src/refresh/main.c b/src/refresh/main.c index 7806e72f0..30340e5be 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -131,19 +131,16 @@ glCullResult_t GL_CullBox(const vec3_t bounds[2]) int i, bits; glCullResult_t cull; - if (!gl_cull_models->integer) { + if (!gl_cull_models->integer) return CULL_IN; - } cull = CULL_IN; for (i = 0; i < 4; i++) { bits = BoxOnPlaneSide(bounds[0], bounds[1], &glr.frustumPlanes[i]); - if (bits == BOX_BEHIND) { + if (bits == BOX_BEHIND) return CULL_OUT; - } - if (bits != BOX_INFRONT) { + if (bits != BOX_INFRONT) cull = CULL_CLIP; - } } return cull; @@ -152,24 +149,21 @@ glCullResult_t GL_CullBox(const vec3_t bounds[2]) glCullResult_t GL_CullSphere(const vec3_t origin, float radius) { float dist; - cplane_t *p; - int i; + const cplane_t *p; glCullResult_t cull; + int i; - if (!gl_cull_models->integer) { + if (!gl_cull_models->integer) return CULL_IN; - } radius *= glr.entscale; cull = CULL_IN; for (i = 0, p = glr.frustumPlanes; i < 4; i++, p++) { dist = PlaneDiff(origin, p); - if (dist < -radius) { + if (dist < -radius) return CULL_OUT; - } - if (dist <= radius) { + if (dist <= radius) cull = CULL_CLIP; - } } return cull; @@ -178,15 +172,14 @@ glCullResult_t GL_CullSphere(const vec3_t origin, float radius) glCullResult_t GL_CullLocalBox(const vec3_t origin, const vec3_t bounds[2]) { vec3_t points[8]; - cplane_t *p; + const cplane_t *p; int i, j; vec_t dot; bool infront; glCullResult_t cull; - if (!gl_cull_models->integer) { + if (!gl_cull_models->integer) return CULL_IN; - } for (i = 0; i < 8; i++) { VectorCopy(origin, points[i]); @@ -202,19 +195,16 @@ glCullResult_t GL_CullLocalBox(const vec3_t origin, const vec3_t bounds[2]) dot = DotProduct(points[j], p->normal); if (dot >= p->dist) { infront = true; - if (cull == CULL_CLIP) { + if (cull == CULL_CLIP) break; - } } else { cull = CULL_CLIP; - if (infront) { + if (infront) break; - } } } - if (!infront) { + if (!infront) return CULL_OUT; - } } return cull; @@ -232,12 +222,10 @@ bool GL_AllocBlock(int width, int height, uint16_t *inuse, max_inuse = 0; for (j = 0; j < w; j++) { k = inuse[i + j]; - if (k >= min_inuse) { + if (k >= min_inuse) break; - } - if (max_inuse < k) { + if (max_inuse < k) max_inuse = k; - } } if (j == w) { x = i; @@ -245,13 +233,11 @@ bool GL_AllocBlock(int width, int height, uint16_t *inuse, } } - if (y + h > height) { + if (y + h > height) return false; - } - for (i = 0; i < w; i++) { + for (i = 0; i < w; i++) inuse[x + i] = y + h; - } *s = x; *t = y; @@ -301,23 +287,23 @@ void GL_SetEntityAxis(void) void GL_RotationMatrix(GLfloat *matrix) { - matrix[0] = glr.entaxis[0][0]; - matrix[4] = glr.entaxis[1][0]; - matrix[8] = glr.entaxis[2][0]; + matrix[ 0] = glr.entaxis[0][0]; + matrix[ 4] = glr.entaxis[1][0]; + matrix[ 8] = glr.entaxis[2][0]; matrix[12] = glr.ent->origin[0]; - matrix[1] = glr.entaxis[0][1]; - matrix[5] = glr.entaxis[1][1]; - matrix[9] = glr.entaxis[2][1]; + matrix[ 1] = glr.entaxis[0][1]; + matrix[ 5] = glr.entaxis[1][1]; + matrix[ 9] = glr.entaxis[2][1]; matrix[13] = glr.ent->origin[1]; - matrix[2] = glr.entaxis[0][2]; - matrix[6] = glr.entaxis[1][2]; + matrix[ 2] = glr.entaxis[0][2]; + matrix[ 6] = glr.entaxis[1][2]; matrix[10] = glr.entaxis[2][2]; matrix[14] = glr.ent->origin[2]; - matrix[3] = 0; - matrix[7] = 0; + matrix[ 3] = 0; + matrix[ 7] = 0; matrix[11] = 0; matrix[15] = 1; } @@ -336,17 +322,16 @@ static void GL_DrawSpriteModel(const model_t *model) const entity_t *e = glr.ent; const mspriteframe_t *frame = &model->spriteframes[e->frame % model->numframes]; const image_t *image = frame->image; - const float alpha = (e->flags & RF_TRANSLUCENT) ? e->alpha : 1; + const float alpha = (e->flags & RF_TRANSLUCENT) ? e->alpha : 1.0f; glStateBits_t bits = GLS_DEPTHMASK_FALSE; vec3_t up, down, left, right; - if (alpha == 1) { + if (alpha == 1.0f) { if (image->flags & IF_TRANSPARENT) { - if (image->flags & IF_PALETTED) { + if (image->flags & IF_PALETTED) bits |= GLS_ALPHATEST_ENABLE; - } else { + else bits |= GLS_BLEND_BLEND; - } } } else { bits |= GLS_BLEND_BLEND; @@ -495,9 +480,8 @@ static void GL_DrawEntities(int musthave, int canthave) model_t *model; int i; - if (!gl_drawentities->integer) { + if (!gl_drawentities->integer) return; - } for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (ent->flags & RF_BEAM) { @@ -512,9 +496,8 @@ static void GL_DrawEntities(int musthave, int canthave) continue; } - if ((ent->flags & musthave) != musthave || (ent->flags & canthave)) { + if ((ent->flags & musthave) != musthave || (ent->flags & canthave)) continue; - } glr.ent = ent; @@ -526,15 +509,13 @@ static void GL_DrawEntities(int musthave, int canthave) const bsp_t *bsp = gl_static.world.cache; int index = ~ent->model; - if (glr.fd.rdflags & RDF_NOWORLDMODEL) { + if (glr.fd.rdflags & RDF_NOWORLDMODEL) Com_Error(ERR_DROP, "%s: inline model without world", __func__); - } - if (index < 1 || index >= bsp->nummodels) { + if (index < 1 || index >= bsp->nummodels) Com_Error(ERR_DROP, "%s: inline model %d out of range", __func__, index); - } GL_DrawBspModel(&bsp->models[index]); continue; @@ -559,9 +540,8 @@ static void GL_DrawEntities(int musthave, int canthave) Q_assert(!"bad model type"); } - if (gl_showorigins->integer) { + if (gl_showorigins->integer) GL_DrawNullModel(); - } } } @@ -571,11 +551,10 @@ static void GL_DrawTearing(void) // alternate colors to make tearing obvious i++; - if (i & 1) { + if (i & 1) qglClearColor(1, 1, 1, 1); - } else { + else qglClearColor(1, 0, 0, 0); - } qglClear(GL_COLOR_BUFFER_BIT); qglClearColor(0, 0, 0, 1); @@ -583,10 +562,8 @@ static void GL_DrawTearing(void) static const char *GL_ErrorString(GLenum err) { - const char *str; - switch (err) { -#define E(x) case GL_##x: str = "GL_"#x; break; +#define E(x) case GL_##x: return "GL_"#x; E(NO_ERROR) E(INVALID_ENUM) E(INVALID_VALUE) @@ -594,11 +571,10 @@ static const char *GL_ErrorString(GLenum err) E(STACK_OVERFLOW) E(STACK_UNDERFLOW) E(OUT_OF_MEMORY) - default: str = "UNKNOWN ERROR"; #undef E } - return str; + return "UNKNOWN ERROR"; } void GL_ClearErrors(void) @@ -613,14 +589,12 @@ bool GL_ShowErrors(const char *func) { GLenum err = qglGetError(); - if (err == GL_NO_ERROR) { + if (err == GL_NO_ERROR) return false; - } do { - if (gl_showerrors->integer) { + if (gl_showerrors->integer) Com_EPrintf("%s: %s\n", func, GL_ErrorString(err)); - } } while ((err = qglGetError()) != GL_NO_ERROR); return true; @@ -669,9 +643,8 @@ void R_RenderFrame(const refdef_t *fd) glr.num_beams = 0; glr.num_flares = 0; - if (gl_dynamic->integer != 1 || gl_vertexlight->integer) { + if (gl_dynamic->integer != 1 || gl_vertexlight->integer) glr.fd.num_dlights = 0; - } if (lm.dirty) { GL_RebuildLighting(); @@ -689,17 +662,15 @@ void R_RenderFrame(const refdef_t *fd) waterwarp = glr.framebuffer_ok; } - if (waterwarp) { + if (waterwarp) qglBindFramebuffer(GL_FRAMEBUFFER, gl_static.warp_framebuffer); - } GL_Setup3D(waterwarp); GL_SetupFrustum(); - if (!(glr.fd.rdflags & RDF_NOWORLDMODEL) && gl_drawworld->integer) { + if (!(glr.fd.rdflags & RDF_NOWORLDMODEL) && gl_drawworld->integer) GL_DrawWorld(); - } GL_DrawEntities(0, RF_TRANSLUCENT); @@ -713,31 +684,26 @@ void R_RenderFrame(const refdef_t *fd) GL_DrawFlares(); - if (!(glr.fd.rdflags & RDF_NOWORLDMODEL)) { + if (!(glr.fd.rdflags & RDF_NOWORLDMODEL)) GL_DrawAlphaFaces(); - } GL_DrawEntities(RF_TRANSLUCENT | RF_WEAPONMODEL, 0); - if (waterwarp) { + if (waterwarp) qglBindFramebuffer(GL_FRAMEBUFFER, 0); - } // go back into 2D mode GL_Setup2D(); - if (waterwarp) { + if (waterwarp) GL_WaterWarp(); - } - if (gl_polyblend->integer) { + if (gl_polyblend->integer) GL_Blend(); - } #if USE_DEBUG - if (gl_lightmap->integer > 1) { + if (gl_lightmap->integer > 1) Draw_Lightmaps(); - } #endif GL_ShowErrors(__func__); @@ -747,15 +713,13 @@ void R_BeginFrame(void) { memset(&c, 0, sizeof(c)); - if (gl_finish->integer) { + if (gl_finish->integer) qglFinish(); - } GL_Setup2D(); - if (gl_clear->integer) { + if (gl_clear->integer) qglClear(GL_COLOR_BUFFER_BIT); - } GL_ShowErrors(__func__); } @@ -767,15 +731,13 @@ void R_EndFrame(void) GL_Flush2D(); Draw_Stats(); } - if (gl_showscrap->integer) { + if (gl_showscrap->integer) Draw_Scrap(); - } #endif GL_Flush2D(); - if (gl_showtearing->integer) { + if (gl_showtearing->integer) GL_DrawTearing(); - } GL_ShowErrors(__func__); @@ -1013,9 +975,8 @@ static void GL_InitTables(void) gl_static.latlngtab[i][1] = (int)(lng * (255 / (2 * M_PIf))) & 255; } - for (int i = 0; i < 256; i++) { + for (int i = 0; i < 256; i++) gl_static.sintab[i] = sinf(i * (2 * M_PIf / 255)); - } } static void GL_PostInit(void) @@ -1080,14 +1041,12 @@ bool R_Init(bool total) // initialize OS-specific parts of OpenGL // create the window and set up the context - if (!vid->init()) { + if (!vid->init()) return false; - } // initialize our QGL dynamic bindings - if (!QGL_Init()) { + if (!QGL_Init()) goto fail; - } // get various limits from OpenGL GL_SetupConfig(); @@ -1129,9 +1088,8 @@ void R_Shutdown(bool total) GL_ShutdownImages(); MOD_Shutdown(); - if (!total) { + if (!total) return; - } GL_ShutdownState(); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 8edc4c77a..0058c9fa7 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -71,17 +71,16 @@ static inline vec_t shadedot(const vec3_t normal) vec_t d = DotProduct(normal, shadedir); // matches the anormtab.h precalculations - if (d < 0) { + if (d < 0) d *= 0.3f; - } return d + 1; } static inline vec_t *get_static_normal(vec3_t normal, const maliasvert_t *vert) { - unsigned int lat = vert->norm[0]; - unsigned int lng = vert->norm[1]; + unsigned lat = vert->norm[0]; + unsigned lng = vert->norm[1]; normal[0] = TAB_SIN(lat) * TAB_COS(lng); normal[1] = TAB_SIN(lat) * TAB_SIN(lng); @@ -342,25 +341,20 @@ static void setup_color(void) if (flags & RF_SHELL_MASK) { VectorClear(color); - if (flags & RF_SHELL_LITE_GREEN) { + if (flags & RF_SHELL_LITE_GREEN) VectorSet(color, 0.56f, 0.93f, 0.56f); - } - if (flags & RF_SHELL_HALF_DAM) { + if (flags & RF_SHELL_HALF_DAM) VectorSet(color, 0.56f, 0.59f, 0.45f); - } if (flags & RF_SHELL_DOUBLE) { color[0] = 0.9f; color[1] = 0.7f; } - if (flags & RF_SHELL_RED) { + if (flags & RF_SHELL_RED) color[0] = 1; - } - if (flags & RF_SHELL_GREEN) { + if (flags & RF_SHELL_GREEN) color[1] = 1; - } - if (flags & RF_SHELL_BLUE) { + if (flags & RF_SHELL_BLUE) color[2] = 1; - } } else if (flags & RF_FULLBRIGHT) { VectorSet(color, 1, 1, 1); } else if ((flags & RF_IR_VISIBLE) && (glr.fd.rdflags & RDF_IRGOGGLES)) { @@ -391,11 +385,10 @@ static void setup_color(void) color[2] = Q_clipf(color[2], 0, 1); } - if (flags & RF_TRANSLUCENT) { + if (flags & RF_TRANSLUCENT) color[3] = glr.ent->alpha; - } else { + else color[3] = 1; - } } static void setup_celshading(void) @@ -429,23 +422,23 @@ static void draw_celshading(const QGL_INDEX_TYPE *indices, int num_indices) static void proj_matrix(GLfloat *matrix, const cplane_t *plane, const vec3_t dir) { - matrix[0] = plane->normal[1] * dir[1] + plane->normal[2] * dir[2]; - matrix[4] = -plane->normal[1] * dir[0]; - matrix[8] = -plane->normal[2] * dir[0]; - matrix[12] = plane->dist * dir[0]; - - matrix[1] = -plane->normal[0] * dir[1]; - matrix[5] = plane->normal[0] * dir[0] + plane->normal[2] * dir[2]; - matrix[9] = -plane->normal[2] * dir[1]; - matrix[13] = plane->dist * dir[1]; - - matrix[2] = -plane->normal[0] * dir[2]; - matrix[6] = -plane->normal[1] * dir[2]; - matrix[10] = plane->normal[0] * dir[0] + plane->normal[1] * dir[1]; - matrix[14] = plane->dist * dir[2]; - - matrix[3] = 0; - matrix[7] = 0; + matrix[ 0] = plane->normal[1] * dir[1] + plane->normal[2] * dir[2]; + matrix[ 4] = -plane->normal[1] * dir[0]; + matrix[ 8] = -plane->normal[2] * dir[0]; + matrix[12] = plane->dist * dir[0]; + + matrix[ 1] = -plane->normal[0] * dir[1]; + matrix[ 5] = plane->normal[0] * dir[0] + plane->normal[2] * dir[2]; + matrix[ 9] = -plane->normal[2] * dir[1]; + matrix[13] = plane->dist * dir[1]; + + matrix[ 2] = -plane->normal[0] * dir[2]; + matrix[ 6] = -plane->normal[1] * dir[2]; + matrix[10] = plane->normal[0] * dir[0] + plane->normal[1] * dir[1]; + matrix[14] = plane->dist * dir[2]; + + matrix[ 3] = 0; + matrix[ 7] = 0; matrix[11] = 0; matrix[15] = DotProduct(plane->normal, dir); } diff --git a/src/refresh/models.c b/src/refresh/models.c index 515edd7a8..068326a29 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -41,16 +41,13 @@ static model_t *MOD_Alloc(void) model_t *model; int i; - for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) { + for (i = 0, model = r_models; i < r_numModels; i++, model++) + if (!model->type) break; - } - } if (i == r_numModels) { - if (r_numModels == MAX_RMODELS) { + if (r_numModels == MAX_RMODELS) return NULL; - } r_numModels++; } @@ -63,12 +60,10 @@ static model_t *MOD_Find(const char *name) int i; for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) { + if (!model->type) continue; - } - if (!FS_pathcmp(model->name, name)) { + if (!FS_pathcmp(model->name, name)) return model; - } } return NULL; @@ -85,9 +80,9 @@ static void MOD_List_f(void) bytes = count = 0; for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) { + if (!model->type) continue; - } + size_t model_size = model->hunk.mapped; int flag = ' '; #if USE_MD5 @@ -111,9 +106,9 @@ void MOD_FreeUnused(void) int i; for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) { + if (!model->type) continue; - } + if (model->registration_sequence == r_registration_sequence) { // make sure it is paged in Com_PageInMemory(model->hunk.base, model->hunk.cursize); @@ -138,9 +133,8 @@ void MOD_FreeAll(void) int i; for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) { + if (!model->type) continue; - } Hunk_Free(&model->hunk); #if USE_MD5 @@ -273,9 +267,8 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) vec3_t mins, maxs; const char *err; - if (length < sizeof(header)) { + if (length < sizeof(header)) return Q_ERR_FILE_TOO_SMALL; - } // byte swap the header LittleBlock(&header, rawdata, sizeof(header)); @@ -308,9 +301,8 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) uint16_t idx_st = LittleShort(src_tri->index_st[j]); // some broken models have 0xFFFF indices - if (idx_xyz >= header.num_xyz || idx_st >= header.num_st) { + if (idx_xyz >= header.num_xyz || idx_st >= header.num_st) break; - } vertIndices[numindices + j] = idx_xyz; tcIndices[numindices + j] = idx_st; @@ -327,17 +319,15 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) return Q_ERR_INVALID_FORMAT; } - for (i = 0; i < numindices; i++) { + for (i = 0; i < numindices; i++) remap[i] = 0xFFFF; - } // remap all triangle indices numverts = 0; src_tc = (dmd2stvert_t *)((byte *)rawdata + header.ofs_st); for (i = 0; i < numindices; i++) { - if (remap[i] != 0xFFFF) { + if (remap[i] != 0xFFFF) continue; // already remapped - } for (j = i + 1; j < numindices; j++) { if (vertIndices[i] == vertIndices[j] && @@ -379,14 +369,12 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) OOM_CHECK(mesh->skinnames = MOD_Malloc(header.num_skins * sizeof(mesh->skinnames[0]))); #endif - if (mesh->numtris != header.num_tris) { + if (mesh->numtris != header.num_tris) Com_DPrintf("%s has %d bad triangles\n", model->name, header.num_tris - mesh->numtris); - } // store final triangle indices - for (i = 0; i < numindices; i++) { + for (i = 0; i < numindices; i++) mesh->indices[i] = finalIndices[i]; - } // load all skins src_skin = (char *)rawdata + header.ofs_skins; @@ -411,9 +399,8 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) scale_s = 1.0f / header.skinwidth; scale_t = 1.0f / header.skinheight; for (i = 0; i < numindices; i++) { - if (remap[i] != i) { + if (remap[i] != i) continue; - } dst_tc[finalIndices[i]].st[0] = (int16_t)LittleShort(src_tc[tcIndices[i]].s) * scale_s; dst_tc[finalIndices[i]].st[1] = @@ -430,9 +417,9 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) // load frame vertices ClearBounds(mins, maxs); for (i = 0; i < numindices; i++) { - if (remap[i] != i) { + if (remap[i] != i) continue; - } + src_vert = &src_frame->verts[vertIndices[i]]; dst_vert = &mesh->verts[j * numverts + finalIndices[i]]; @@ -1194,10 +1181,9 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) mdl->num_frames = num_frames; // warn on mismatched frame counts (not fatal) - if (mdl->num_frames != model->numframes) { + if (mdl->num_frames != model->numframes) Com_WPrintf("%s doesn't match frame count for %s (%i vs %i)\n", path, model->name, mdl->num_frames, model->numframes); - } MD5_EXPECT("numJoints"); MD5_UINT(num_joints); @@ -1367,22 +1353,18 @@ static void MOD_Reference(model_t *model) case MOD_ALIAS: for (i = 0; i < model->nummeshes; i++) { maliasmesh_t *mesh = &model->meshes[i]; - for (j = 0; j < mesh->numskins; j++) { + for (j = 0; j < mesh->numskins; j++) mesh->skins[j]->registration_sequence = r_registration_sequence; - } } #if USE_MD5 - if (model->skeleton) { - for (j = 0; j < model->skeleton->num_skins; j++) { + if (model->skeleton) + for (j = 0; j < model->skeleton->num_skins; j++) model->skeleton->skins[j]->registration_sequence = r_registration_sequence; - } - } #endif break; case MOD_SPRITE: - for (i = 0; i < model->numframes; i++) { + for (i = 0; i < model->numframes; i++) model->spriteframes[i].image->registration_sequence = r_registration_sequence; - } break; case MOD_EMPTY: break; @@ -1440,9 +1422,8 @@ qhandle_t R_RegisterModel(const char *name) ret = FS_LoadFile(normalized, (void **)&rawdata); if (!rawdata) { // don't spam about missing models - if (ret == Q_ERR(ENOENT)) { + if (ret == Q_ERR(ENOENT)) return 0; - } goto fail1; } @@ -1509,15 +1490,13 @@ model_t *MOD_ForHandle(qhandle_t h) { model_t *model; - if (!h) { + if (!h) return NULL; - } Q_assert(h > 0 && h <= r_numModels); model = &r_models[h - 1]; - if (!model->type) { + if (!model->type) return NULL; - } return model; } diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 26b7e929b..6a0d7dddf 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -290,35 +290,31 @@ static void shader_array_bits(GLbitfield bits) GLbitfield diff = bits ^ gls.array_bits; if (diff & GLA_VERTEX) { - if (bits & GLA_VERTEX) { + if (bits & GLA_VERTEX) qglEnableVertexAttribArray(VERT_ATTR_POS); - } else { + else qglDisableVertexAttribArray(VERT_ATTR_POS); - } } if (diff & GLA_TC) { - if (bits & GLA_TC) { + if (bits & GLA_TC) qglEnableVertexAttribArray(VERT_ATTR_TC); - } else { + else qglDisableVertexAttribArray(VERT_ATTR_TC); - } } if (diff & GLA_LMTC) { - if (bits & GLA_LMTC) { + if (bits & GLA_LMTC) qglEnableVertexAttribArray(VERT_ATTR_LMTC); - } else { + else qglDisableVertexAttribArray(VERT_ATTR_LMTC); - } } if (diff & GLA_COLOR) { - if (bits & GLA_COLOR) { + if (bits & GLA_COLOR) qglEnableVertexAttribArray(VERT_ATTR_COLOR); - } else { + else qglDisableVertexAttribArray(VERT_ATTR_COLOR); - } } } diff --git a/src/refresh/sky.c b/src/refresh/sky.c index fa5a7e609..4260451f2 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -21,7 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc., static float skyrotate; static bool skyautorotate; static vec3_t skyaxis; -static int sky_images[6]; +static GLuint sky_images[6]; static const vec3_t skyclip[6] = { { 1, 1, 0 }, @@ -62,19 +62,19 @@ static int skyfaces; static const float sky_min = 1.0f / 512.0f; static const float sky_max = 511.0f / 512.0f; -static void DrawSkyPolygon(int nump, vec3_t vecs) +static void DrawSkyPolygon(int nump, const vec3_t vecs) { - int i, j; - vec3_t v, av; - float s, t, dv; - int axis; - float *vp; + int i, j; + vec3_t v, av; + float s, t, dv; + int axis; + const float *vp; // decide which face it maps to VectorClear(v); - for (i = 0, vp = vecs; i < nump; i++, vp += 3) { + for (i = 0, vp = vecs; i < nump; i++, vp += 3) VectorAdd(vp, v, v); - } + av[0] = fabsf(v[0]); av[1] = fabsf(v[1]); av[2] = fabsf(v[2]); @@ -135,15 +135,14 @@ static void DrawSkyPolygon(int nump, vec3_t vecs) static void ClipSkyPolygon(int nump, vec3_t vecs, int stage) { - const float *norm; - float *v; - bool front, back; - float d, e; - float dists[MAX_CLIP_VERTS]; - int sides[MAX_CLIP_VERTS]; - vec3_t newv[2][MAX_CLIP_VERTS]; - int newc[2]; - int i, j; + const float *v, *norm; + bool front, back; + float d, e; + float dists[MAX_CLIP_VERTS]; + int sides[MAX_CLIP_VERTS]; + vec3_t newv[2][MAX_CLIP_VERTS]; + int newc[2]; + int i, j; if (nump > MAX_CLIP_VERTS - 2) { Com_DPrintf("%s: too many verts\n", __func__); @@ -350,6 +349,7 @@ void R_DrawSkyBox(void) MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); MakeSkyVec(skymaxs[0][i], skymaxs[1][i], i, tess.vertices + 10); MakeSkyVec(skymins[0][i], skymaxs[1][i], i, tess.vertices + 15); + GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_UnlockArrays(); @@ -361,9 +361,8 @@ static void R_UnsetSky(void) int i; skyrotate = 0; - for (i = 0; i < 6; i++) { + for (i = 0; i < 6; i++) sky_images[i] = TEXNUM_BLACK; - } } /* @@ -373,9 +372,9 @@ R_SetSky */ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis) { - int i; - char pathname[MAX_QPATH]; - image_t *image; + int i; + char pathname[MAX_QPATH]; + const image_t *image; if (!gl_drawsky->integer) { R_UnsetSky(); diff --git a/src/refresh/state.c b/src/refresh/state.c index 663d96c4f..765b59858 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -27,9 +27,8 @@ void GL_ForceTexture(GLuint tmu, GLuint texnum) { GL_ActiveTexture(tmu); - if (gls.texnums[tmu] == texnum) { + if (gls.texnums[tmu] == texnum) return; - } qglBindTexture(GL_TEXTURE_2D, texnum); gls.texnums[tmu] = texnum; @@ -41,14 +40,12 @@ void GL_ForceTexture(GLuint tmu, GLuint texnum) void GL_BindTexture(GLuint tmu, GLuint texnum) { #if USE_DEBUG - if (gl_nobind->integer && !tmu) { + if (gl_nobind->integer && !tmu) texnum = TEXNUM_DEFAULT; - } #endif - if (gls.texnums[tmu] == texnum) { + if (gls.texnums[tmu] == texnum) return; - } GL_ActiveTexture(tmu); @@ -65,40 +62,36 @@ void GL_CommonStateBits(GLbitfield bits) if (diff & GLS_BLEND_MASK) { if (bits & GLS_BLEND_MASK) { qglEnable(GL_BLEND); - if (bits & GLS_BLEND_BLEND) { + if (bits & GLS_BLEND_BLEND) qglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } else if (bits & GLS_BLEND_ADD) { + else if (bits & GLS_BLEND_ADD) qglBlendFunc(GL_SRC_ALPHA, GL_ONE); - } else if (bits & GLS_BLEND_MODULATE) { + else if (bits & GLS_BLEND_MODULATE) qglBlendFunc(GL_DST_COLOR, GL_ONE); - } } else { qglDisable(GL_BLEND); } } if (diff & GLS_DEPTHMASK_FALSE) { - if (bits & GLS_DEPTHMASK_FALSE) { + if (bits & GLS_DEPTHMASK_FALSE) qglDepthMask(GL_FALSE); - } else { + else qglDepthMask(GL_TRUE); - } } if (diff & GLS_DEPTHTEST_DISABLE) { - if (bits & GLS_DEPTHTEST_DISABLE) { + if (bits & GLS_DEPTHTEST_DISABLE) qglDisable(GL_DEPTH_TEST); - } else { + else qglEnable(GL_DEPTH_TEST); - } } if (diff & GLS_CULL_DISABLE) { - if (bits & GLS_CULL_DISABLE) { + if (bits & GLS_CULL_DISABLE) qglDisable(GL_CULL_FACE); - } else { + else qglEnable(GL_CULL_FACE); - } } } @@ -128,27 +121,27 @@ void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat zn GLfloat width, height, depth; GLfloat matrix[16]; - width = xmax - xmin; + width = xmax - xmin; height = ymax - ymin; - depth = zfar - znear; + depth = zfar - znear; - matrix[0] = 2 / width; - matrix[4] = 0; - matrix[8] = 0; + matrix[ 0] = 2 / width; + matrix[ 4] = 0; + matrix[ 8] = 0; matrix[12] = -(xmax + xmin) / width; - matrix[1] = 0; - matrix[5] = 2 / height; - matrix[9] = 0; + matrix[ 1] = 0; + matrix[ 5] = 2 / height; + matrix[ 9] = 0; matrix[13] = -(ymax + ymin) / height; - matrix[2] = 0; - matrix[6] = 0; + matrix[ 2] = 0; + matrix[ 6] = 0; matrix[10] = -2 / depth; matrix[14] = -(zfar + znear) / depth; - matrix[3] = 0; - matrix[7] = 0; + matrix[ 3] = 0; + matrix[ 7] = 0; matrix[11] = 0; matrix[15] = 1; @@ -195,27 +188,27 @@ void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) ymax = znear * tanf(fov_y * (M_PIf / 360)); ymin = -ymax; - width = xmax - xmin; + width = xmax - xmin; height = ymax - ymin; - depth = zfar - znear; + depth = zfar - znear; - matrix[0] = reflect_x * 2 * znear / width; - matrix[4] = 0; - matrix[8] = (xmax + xmin) / width; + matrix[ 0] = reflect_x * 2 * znear / width; + matrix[ 4] = 0; + matrix[ 8] = (xmax + xmin) / width; matrix[12] = 0; - matrix[1] = 0; - matrix[5] = 2 * znear / height; - matrix[9] = (ymax + ymin) / height; + matrix[ 1] = 0; + matrix[ 5] = 2 * znear / height; + matrix[ 9] = (ymax + ymin) / height; matrix[13] = 0; - matrix[2] = 0; - matrix[6] = 0; + matrix[ 2] = 0; + matrix[ 6] = 0; matrix[10] = -(zfar + znear) / depth; matrix[14] = -2 * zfar * znear / depth; - matrix[3] = 0; - matrix[7] = 0; + matrix[ 3] = 0; + matrix[ 7] = 0; matrix[11] = -1; matrix[15] = 0; @@ -228,23 +221,23 @@ static void GL_RotateForViewer(void) AnglesToAxis(glr.fd.viewangles, glr.viewaxis); - matrix[0] = -glr.viewaxis[1][0]; - matrix[4] = -glr.viewaxis[1][1]; - matrix[8] = -glr.viewaxis[1][2]; + matrix[ 0] = -glr.viewaxis[1][0]; + matrix[ 4] = -glr.viewaxis[1][1]; + matrix[ 8] = -glr.viewaxis[1][2]; matrix[12] = DotProduct(glr.viewaxis[1], glr.fd.vieworg); - matrix[1] = glr.viewaxis[2][0]; - matrix[5] = glr.viewaxis[2][1]; - matrix[9] = glr.viewaxis[2][2]; + matrix[ 1] = glr.viewaxis[2][0]; + matrix[ 5] = glr.viewaxis[2][1]; + matrix[ 9] = glr.viewaxis[2][2]; matrix[13] = -DotProduct(glr.viewaxis[2], glr.fd.vieworg); - matrix[2] = -glr.viewaxis[0][0]; - matrix[6] = -glr.viewaxis[0][1]; + matrix[ 2] = -glr.viewaxis[0][0]; + matrix[ 6] = -glr.viewaxis[0][1]; matrix[10] = -glr.viewaxis[0][2]; matrix[14] = DotProduct(glr.viewaxis[0], glr.fd.vieworg); - matrix[3] = 0; - matrix[7] = 0; + matrix[ 3] = 0; + matrix[ 7] = 0; matrix[11] = 0; matrix[15] = 1; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 4ae277708..48bbbaa4c 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -101,8 +101,9 @@ static float blocklights[MAX_BLOCKLIGHTS * 3]; static void put_blocklights(const mface_t *surf) { - float *bl, add, modulate, scale = lm.scale; + float add, modulate, scale = lm.scale; int i, j, smax, tmax, stride = 1 << lm.block_shift; + const float *bl; byte *out; if (gl_static.use_shaders) { @@ -133,13 +134,13 @@ static void put_blocklights(const mface_t *surf) static void add_dynamic_lights(const mface_t *surf) { - dlight_t *light; - vec3_t point; - vec2_t local; - vec_t s_scale, t_scale, sd, td; - vec_t dist, rad, minlight, scale, frac; - float *bl; - int i, smax, tmax, s, t; + const dlight_t *light; + vec3_t point; + vec2_t local; + vec_t s_scale, t_scale, sd, td; + vec_t dist, rad, minlight, scale, frac; + float *bl; + int i, smax, tmax, s, t; smax = surf->lm_width; tmax = surf->lm_height; @@ -190,8 +191,8 @@ static void add_dynamic_lights(const mface_t *surf) static void add_light_styles(mface_t *surf) { - lightstyle_t *style; - byte *src; + const lightstyle_t *style; + const byte *src; float *bl; int i, j, size = surf->lm_width * surf->lm_height; @@ -263,7 +264,7 @@ static void update_dynamic_lightmap(mface_t *surf) // updates lightmaps in RAM void GL_PushLights(mface_t *surf) { - lightstyle_t *style; + const lightstyle_t *style; int i; if (!surf->light_m) @@ -903,7 +904,7 @@ static void upload_world_surfaces(void) static void set_world_size(void) { - mnode_t *node = gl_static.world.cache->nodes; + const mnode_t *node = gl_static.world.cache->nodes; vec_t size, temp; int i; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 92f47aede..d34d7a7db 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -94,10 +94,9 @@ static void gl_texturemode_changed(cvar_t *self) { int i; - for (i = 0; i < numFilterModes; i++) { + for (i = 0; i < numFilterModes; i++) if (!Q_stricmp(filterModes[i].name, self->string)) break; - } if (i == numFilterModes) { Com_WPrintf("Bad texture mode: %s\n", self->string); @@ -180,7 +179,7 @@ IMAGE PROCESSING static void IMG_ResampleTexture(const byte *in, int inwidth, int inheight, byte *out, int outwidth, int outheight) { - int i, j; + int i, j; const byte *inrow1, *inrow2; unsigned frac, fracstep; unsigned p1[MAX_TEXTURE_SIZE], p2[MAX_TEXTURE_SIZE]; @@ -270,9 +269,8 @@ void Scrap_Upload(void) byte *data; int maxlevel; - if (!scrap_dirty) { + if (!scrap_dirty) return; - } GL_ForceTexture(0, TEXNUM_SCRAP); @@ -402,16 +400,12 @@ static void GL_ColorInvertTexture(byte *in, int inwidth, int inheight, imagetype static bool GL_TextureHasAlpha(const byte *data, int width, int height) { - int i, c; - const byte *scan; + int i, c; c = width * height; - scan = data + 3; - for (i = 0; i < c; i++, scan += 4) { - if (*scan != 255) { + for (i = 0, data += 3; i < c; i++, data += 4) + if (*data != 255) return true; - } - } return false; } @@ -505,9 +499,8 @@ static void GL_Upload32(byte *data, int width, int height, int baselevel, imaget upload_alpha = GL_TextureHasAlpha(scaled, scaled_width, scaled_height); } - if (upload_alpha) { + if (upload_alpha) comp = gl_tex_alpha_format; - } qglTexImage2D(GL_TEXTURE_2D, baselevel, comp, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); @@ -535,9 +528,8 @@ static void GL_Upload32(byte *data, int width, int height, int baselevel, imaget } } - if (scaled != data) { + if (scaled != data) FS_FreeTempMem(scaled); - } } static int GL_UpscaleLevel(int width, int height, imagetype_t type, imageflags_t flags) @@ -715,9 +707,8 @@ void IMG_Load(image_t *image, byte *pic) GL_SetFilterAndRepeat(image->type, image->flags); - if (upload_alpha) { + if (upload_alpha) image->flags |= IF_TRANSPARENT; - } image->upload_width = upload_width << maxlevel; // after power of 2 and scales image->upload_height = upload_height << maxlevel; image->sl = 0; @@ -1036,17 +1027,15 @@ void GL_InitImages(void) IMG_GetPalette(); - if (gl_upscale_pcx->integer) { + if (gl_upscale_pcx->integer) HQ2x_Init(); - } GL_BuildIntensityTable(); - if (r_config.flags & QVF_GAMMARAMP) { + if (r_config.flags & QVF_GAMMARAMP) gl_gamma_changed(gl_gamma); - } else { + else GL_BuildGammaTables(); - } // FIXME: the name 'saturation' is misleading in this context colorscale = Cvar_ClampValue(gl_saturation, 0, 1); From 698e554b5787ef8eee2c3e050a032db8ef7440f7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 15 Aug 2024 21:45:13 +0300 Subject: [PATCH 557/974] Improve IMG_ReadPixels() robustness. Check for integer overflows. Use glReadnPixels() if available. Check for GL errors. --- src/refresh/images.c | 7 ++++++- src/refresh/images.h | 2 +- src/refresh/qgl.c | 12 ++++++++++++ src/refresh/qgl.h | 3 +++ src/refresh/texture.c | 38 +++++++++++++++++++++++++++++++------- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index ff1379782..223cd8571 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1302,7 +1302,12 @@ static void make_screenshot(const char *name, const char *ext, .async = async, }; - IMG_ReadPixels(&s); + ret = IMG_ReadPixels(&s); + if (ret < 0) { + s.status = ret; + screenshot_done_cb(&s); + return; + } if (async) { asyncwork_t work = { diff --git a/src/refresh/images.h b/src/refresh/images.h index a1bdd938a..ed3b0919f 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -105,4 +105,4 @@ struct screenshot_s { bool async; }; -void IMG_ReadPixels(screenshot_t *s); +int IMG_ReadPixels(screenshot_t *s); diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 3728224d5..d11ca2639 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -305,6 +305,18 @@ static const glsection_t sections[] = { } }, + // GL 4.5 + // GL_ARB_robustness + { + .extension = "GL_ARB_robustness", + .ver_gl = QGL_VER(4, 5), + .ver_es = QGL_VER(3, 2), + .functions = (const glfunction_t []) { + QGL_FN(ReadnPixels), + { NULL } + } + }, + // GL 4.6 // GL_EXT_texture_filter_anisotropic { diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index 7671ca4ca..61c900aa7 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -166,6 +166,9 @@ QGLAPI void (APIENTRYP qglDepthRangef)(GLfloat n, GLfloat f); // GL 4.3 QGLAPI void (APIENTRYP qglDebugMessageCallback)(GLDEBUGPROC callback, const void *userParam); +// GL 4.5 +QGLAPI void (APIENTRYP qglReadnPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void *data); + // GL_ARB_fragment_program QGLAPI void (APIENTRYP qglBindProgramARB)(GLenum target, GLuint program); QGLAPI void (APIENTRYP qglDeleteProgramsARB)(GLsizei n, const GLuint *programs); diff --git a/src/refresh/texture.c b/src/refresh/texture.c index d34d7a7db..782ce488a 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -731,21 +731,45 @@ void IMG_Unload(image_t *image) } // for screenshots -void IMG_ReadPixels(screenshot_t *s) +int IMG_ReadPixels(screenshot_t *s) { int format = gl_config.ver_es ? GL_RGBA : GL_RGB; - int align = 4; + int align = 4, bpp = format == GL_RGBA ? 4 : 3; + + if (r_config.width < 1 || r_config.height < 1) + return Q_ERR(EINVAL); qglGetIntegerv(GL_PACK_ALIGNMENT, &align); - s->bpp = format == GL_RGBA ? 4 : 3; - s->rowbytes = Q_ALIGN(r_config.width * s->bpp, align); - s->pixels = Z_Malloc(s->rowbytes * r_config.height); + if (r_config.width > (INT_MAX - align + 1) / bpp) + return Q_ERR(EOVERFLOW); + + int rowbytes = Q_ALIGN(r_config.width * bpp, align); + + if (r_config.height > INT_MAX / rowbytes) + return Q_ERR(EOVERFLOW); + + int buf_size = rowbytes * r_config.height; + + s->bpp = bpp; + s->rowbytes = rowbytes; + s->pixels = Z_Malloc(buf_size); s->width = r_config.width; s->height = r_config.height; - qglReadPixels(0, 0, r_config.width, r_config.height, - format, GL_UNSIGNED_BYTE, s->pixels); + GL_ClearErrors(); + + if (qglReadnPixels) + qglReadnPixels(0, 0, r_config.width, r_config.height, + format, GL_UNSIGNED_BYTE, buf_size, s->pixels); + else + qglReadPixels(0, 0, r_config.width, r_config.height, + format, GL_UNSIGNED_BYTE, s->pixels); + + if (GL_ShowErrors("Failed to read pixels")) + return Q_ERR_FAILURE; + + return Q_ERR_SUCCESS; } static void GL_BuildIntensityTable(void) From e1fc08916e62f81e2db63b31bf39d7d3d7a39df0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 01:22:01 +0300 Subject: [PATCH 558/974] Print warning if multitexture is not supported. --- src/refresh/surf.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 48bbbaa4c..816228144 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -836,6 +836,16 @@ static void upload_surface_vbo(int lastvert) tess.numverts = 0; } +static void check_multitexture(void) +{ + if (gl_vertexlight->integer) + return; + if (qglActiveTexture && (qglClientActiveTexture || gl_static.use_shaders)) + return; + Com_WPrintf("OpenGL doesn't support multitexturing, forcing vertex lighting.\n"); + Cvar_Set("gl_vertexlight", "1"); +} + static void upload_world_surfaces(void) { const bsp_t *bsp = gl_static.world.cache; @@ -844,8 +854,7 @@ static void upload_world_surfaces(void) int i, currvert, lastvert; // force vertex lighting if multitexture is not supported - if (!qglActiveTexture || (!qglClientActiveTexture && !gl_static.use_shaders)) - Cvar_Set("gl_vertexlight", "1"); + check_multitexture(); // begin building lightmaps LM_BeginBuilding(); From e54874735ce6f58200b3194439be5d157a89b9e6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 01:22:49 +0300 Subject: [PATCH 559/974] Disable dynamic lights if unpack subimage is not supported. --- src/refresh/gl.h | 1 + src/refresh/main.c | 4 ++++ src/refresh/qgl.c | 9 +++++++++ 3 files changed, 14 insertions(+) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 19cd81967..ceb9d8330 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -148,6 +148,7 @@ typedef enum { QGL_CAP_TEXTURE_LOD_BIAS = BIT(5), QGL_CAP_TEXTURE_NON_POWER_OF_TWO = BIT(6), QGL_CAP_TEXTURE_ANISOTROPY = BIT(7), + QGL_CAP_UNPACK_SUBIMAGE = BIT(8), } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) diff --git a/src/refresh/main.c b/src/refresh/main.c index 30340e5be..0c2d66bf0 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -801,6 +801,10 @@ static size_t GL_ViewCluster_m(char *buffer, size_t size) static void gl_lightmap_changed(cvar_t *self) { + if (gl_dynamic->integer && !(gl_config.caps & QGL_CAP_UNPACK_SUBIMAGE)) { + Com_WPrintf("OpenGL doesn't support dynamic lights, forcing them off.\n"); + Cvar_Set("gl_dynamic", "0"); + } lm.scale = Cvar_ClampValue(gl_coloredlightmaps, 0, 1); lm.comp = !(gl_config.caps & QGL_CAP_TEXTURE_BITS) ? GL_RGBA : lm.scale ? GL_RGB : GL_LUMINANCE; lm.add = 255 * Cvar_ClampValue(gl_brightness, -1, 1); diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index d11ca2639..1c71c93e6 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -127,6 +127,15 @@ static const glsection_t sections[] = { } }, + // GL 1.1, ES 3.0 + // EXT_unpack_subimage + { + .extension = "GL_EXT_unpack_subimage", + .ver_gl = QGL_VER(1, 1), + .ver_es = QGL_VER(3, 0), + .caps = QGL_CAP_UNPACK_SUBIMAGE, + }, + // ES 1.1 { .ver_es = QGL_VER(1, 1), From b7a258bfbbc7d8d586f6af01203668834af2d1da Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 10:59:21 +0300 Subject: [PATCH 560/974] Precalculate inverse of lightmap block size. --- src/refresh/surf.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 816228144..93c9ba024 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -751,7 +751,7 @@ static void build_surface_light(mface_t *surf, vec_t *vbo) // normalizes and stores lightmap texture coordinates in vertices static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) { - float s, t; + float s, t, scale = 1.0f / lm.block_size; int i; s = surf->light_s + 0.5f; @@ -760,8 +760,8 @@ static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) for (i = 0; i < surf->numsurfedges; i++) { vbo[6] += s; vbo[7] += t; - vbo[6] *= 1.0f / lm.block_size; - vbo[7] *= 1.0f / lm.block_size; + vbo[6] *= scale; + vbo[7] *= scale; vbo += VERTEX_SIZE; } From 0843934cc3b61f8768581aa1b8b80d8e9941497f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 11:00:11 +0300 Subject: [PATCH 561/974] Simplify setting world size. --- src/refresh/surf.c | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 93c9ba024..1410e6e86 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -911,17 +911,13 @@ static void upload_world_surfaces(void) gl_vertexlight->modified = false; } -static void set_world_size(void) +static void set_world_size(const mnode_t *node) { - const mnode_t *node = gl_static.world.cache->nodes; - vec_t size, temp; + vec_t size; int i; - for (i = 0, size = 0; i < 3; i++) { - temp = node->maxs[i] - node->mins[i]; - if (temp > size) - size = temp; - } + for (i = 0, size = 0; i < 3; i++) + size = max(size, node->maxs[i] - node->mins[i]); if (size > 4096) gl_static.world.size = 8192; @@ -1011,7 +1007,7 @@ void GL_LoadWorld(const char *name) gl_static.world.cache = bsp; // calculate world size for far clip plane and sky box - set_world_size(); + set_world_size(bsp->nodes); // register all texinfo for (i = 0, info = bsp->texinfo; i < bsp->numtexinfo; i++, info++) { From 1ed5ad945fcc3edd92273a39103a8382827eecc1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 11:00:48 +0300 Subject: [PATCH 562/974] Format full BSP path in GL_LoadWorld(). --- src/refresh/main.c | 7 +------ src/refresh/surf.c | 8 ++++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 0c2d66bf0..70d07c9c8 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1145,18 +1145,13 @@ R_BeginRegistration */ void R_BeginRegistration(const char *name) { - char fullname[MAX_QPATH]; - gl_static.registering = true; r_registration_sequence++; memset(&glr, 0, sizeof(glr)); glr.viewcluster1 = glr.viewcluster2 = -2; - if (name) { - Q_concat(fullname, sizeof(fullname), "maps/", name, ".bsp"); - GL_LoadWorld(fullname); - } + GL_LoadWorld(name); } /* diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 1410e6e86..ddd3f0fa6 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -976,10 +976,14 @@ void GL_LoadWorld(const char *name) mface_t *surf; int i, n64surfs, ret; - ret = BSP_Load(name, &bsp); + if (!name || !*name) + return; + + Q_concat(buffer, sizeof(buffer), "maps/", name, ".bsp"); + ret = BSP_Load(buffer, &bsp); if (!bsp) Com_Error(ERR_DROP, "%s: couldn't load %s: %s", - __func__, name, BSP_ErrorString(ret)); + __func__, buffer, BSP_ErrorString(ret)); // check if the required world model was already loaded if (gl_static.world.cache == bsp) { From 796a95c29db6e67d6ab52847399d0e0bdf39ca40 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 16:04:42 +0300 Subject: [PATCH 563/974] Optimize writing GLSL shaders. --- src/refresh/shader.c | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 6a0d7dddf..c8ca2a89b 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -17,11 +17,12 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #include "gl.h" +#include "common/sizebuf.h" #define MAX_SHADER_CHARS 4096 -#define GLSL(x) Q_strlcat(buf, #x "\n", MAX_SHADER_CHARS); -#define GLSF(x) Q_strlcat(buf, x, MAX_SHADER_CHARS) +#define GLSL(x) SZ_Write(buf, CONST_STR_LEN(#x "\n")); +#define GLSF(x) SZ_Write(buf, CONST_STR_LEN(x)) enum { VERT_ATTR_POS, @@ -32,9 +33,8 @@ enum { static void upload_u_block(void); -static void write_header(char *buf) +static void write_header(sizebuf_t *buf) { - *buf = 0; if (gl_config.ver_es) { GLSF("#version 300 es\n"); } else if (gl_config.ver_sl >= QGL_VER(1, 40)) { @@ -45,7 +45,7 @@ static void write_header(char *buf) } } -static void write_block(char *buf) +static void write_block(sizebuf_t *buf) { GLSF("layout(std140) uniform u_block {\n"); GLSL( @@ -64,7 +64,7 @@ static void write_block(char *buf) GLSF("};\n"); } -static void write_vertex_shader(char *buf, GLbitfield bits) +static void write_vertex_shader(sizebuf_t *buf, GLbitfield bits) { write_header(buf); write_block(buf); @@ -92,7 +92,7 @@ static void write_vertex_shader(char *buf, GLbitfield bits) GLSF("}\n"); } -static void write_fragment_shader(char *buf, GLbitfield bits) +static void write_fragment_shader(sizebuf_t *buf, GLbitfield bits) { write_header(buf); @@ -158,21 +158,25 @@ static void write_fragment_shader(char *buf, GLbitfield bits) GLSF("}\n"); } -static GLuint create_shader(GLenum type, const char *src) +static GLuint create_shader(GLenum type, const sizebuf_t *buf) { + const GLchar *data = (const GLchar *)buf->data; + GLint size = buf->cursize; + GLuint shader = qglCreateShader(type); if (!shader) { Com_EPrintf("Couldn't create shader\n"); return 0; } - qglShaderSource(shader, 1, &src, NULL); + qglShaderSource(shader, 1, &data, &size); qglCompileShader(shader); - GLint status; + GLint status = 0; qglGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (!status) { - char buffer[MAX_STRING_CHARS] = { 0 }; + char buffer[MAX_STRING_CHARS]; + buffer[0] = 0; qglGetShaderInfoLog(shader, sizeof(buffer), NULL, buffer); qglDeleteShader(shader); @@ -189,6 +193,7 @@ static GLuint create_shader(GLenum type, const char *src) static GLuint create_and_use_program(GLbitfield bits) { char buffer[MAX_SHADER_CHARS]; + sizebuf_t sb; GLuint program = qglCreateProgram(); if (!program) { @@ -196,13 +201,15 @@ static GLuint create_and_use_program(GLbitfield bits) return 0; } - write_vertex_shader(buffer, bits); - GLuint shader_v = create_shader(GL_VERTEX_SHADER, buffer); + SZ_Init(&sb, buffer, sizeof(buffer), "GLSL"); + write_vertex_shader(&sb, bits); + GLuint shader_v = create_shader(GL_VERTEX_SHADER, &sb); if (!shader_v) return program; - write_fragment_shader(buffer, bits); - GLuint shader_f = create_shader(GL_FRAGMENT_SHADER, buffer); + SZ_Clear(&sb); + write_fragment_shader(&sb, bits); + GLuint shader_f = create_shader(GL_FRAGMENT_SHADER, &sb); if (!shader_f) { qglDeleteShader(shader_v); return program; @@ -226,8 +233,9 @@ static GLuint create_and_use_program(GLbitfield bits) GLint status = 0; qglGetProgramiv(program, GL_LINK_STATUS, &status); if (!status) { - char buffer[MAX_STRING_CHARS] = { 0 }; + char buffer[MAX_STRING_CHARS]; + buffer[0] = 0; qglGetProgramInfoLog(program, sizeof(buffer), NULL, buffer); if (buffer[0]) From c07f190445de1939ba6755c45ab8437c961dea49 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 16 Aug 2024 17:15:55 +0300 Subject: [PATCH 564/974] Simplify enabling vertex attrib arrays. --- src/refresh/shader.c | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index c8ca2a89b..e3bf8866f 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -24,11 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GLSL(x) SZ_Write(buf, CONST_STR_LEN(#x "\n")); #define GLSF(x) SZ_Write(buf, CONST_STR_LEN(x)) +// must match glArrayBits_t order!!! enum { VERT_ATTR_POS, VERT_ATTR_TC, VERT_ATTR_LMTC, - VERT_ATTR_COLOR + VERT_ATTR_COLOR, + + VERT_ATTR_COUNT }; static void upload_u_block(void); @@ -297,32 +300,13 @@ static void shader_array_bits(GLbitfield bits) { GLbitfield diff = bits ^ gls.array_bits; - if (diff & GLA_VERTEX) { - if (bits & GLA_VERTEX) - qglEnableVertexAttribArray(VERT_ATTR_POS); - else - qglDisableVertexAttribArray(VERT_ATTR_POS); - } - - if (diff & GLA_TC) { - if (bits & GLA_TC) - qglEnableVertexAttribArray(VERT_ATTR_TC); - else - qglDisableVertexAttribArray(VERT_ATTR_TC); - } - - if (diff & GLA_LMTC) { - if (bits & GLA_LMTC) - qglEnableVertexAttribArray(VERT_ATTR_LMTC); - else - qglDisableVertexAttribArray(VERT_ATTR_LMTC); - } - - if (diff & GLA_COLOR) { - if (bits & GLA_COLOR) - qglEnableVertexAttribArray(VERT_ATTR_COLOR); + for (int i = 0; i < VERT_ATTR_COUNT; i++) { + if (!(diff & BIT(i))) + continue; + if (bits & BIT(i)) + qglEnableVertexAttribArray(i); else - qglDisableVertexAttribArray(VERT_ATTR_COLOR); + qglDisableVertexAttribArray(i); } } @@ -418,10 +402,8 @@ static void shader_clear_state(void) qglActiveTexture(GL_TEXTURE0); qglBindTexture(GL_TEXTURE_2D, 0); - qglDisableVertexAttribArray(VERT_ATTR_POS); - qglDisableVertexAttribArray(VERT_ATTR_TC); - qglDisableVertexAttribArray(VERT_ATTR_LMTC); - qglDisableVertexAttribArray(VERT_ATTR_COLOR); + for (int i = 0; i < VERT_ATTR_COUNT; i++) + qglDisableVertexAttribArray(i); if (gl_static.programs[0]) qglUseProgram(gl_static.programs[0]); From a8f57e125779a8c888ace8fff16687f342ca5e61 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 17 Aug 2024 01:18:44 +0300 Subject: [PATCH 565/974] Properly invalidate texture bindings. Glowmap texture can be bound to TMU 0 as well, so search all TMUs when deleting textures. --- src/refresh/texture.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 782ce488a..8c9a1fe0c 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -721,11 +721,14 @@ void IMG_Load(image_t *image, byte *pic) void IMG_Unload(image_t *image) { if (image->texnum && !(image->flags & IF_SCRAP)) { - if (gls.texnums[0] == image->texnum) - gls.texnums[0] = 0; - if (gls.texnums[2] == image->glow_texnum) - gls.texnums[2] = 0; - qglDeleteTextures(2, (GLuint[2]){ image->texnum, image->glow_texnum }); + GLuint tex[2] = { image->texnum, image->glow_texnum }; + + // invalidate bindings + for (int i = 0; i < MAX_TMUS; i++) + if (gls.texnums[i] == tex[0] || gls.texnums[i] == tex[1]) + gls.texnums[i] = 0; + + qglDeleteTextures(tex[1] ? 2 : 1, tex); image->texnum = image->glow_texnum = 0; } } From 36164007225c1d914b38475074b8cd5dd04384ee Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 17 Aug 2024 01:22:16 +0300 Subject: [PATCH 566/974] Reduce number of GL calls to bind texture. --- src/refresh/qgl.c | 18 ++++++++++++++++++ src/refresh/qgl.h | 4 ++++ src/refresh/state.c | 9 ++++++--- src/refresh/tess.c | 21 ++++++++++++++++----- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 1c71c93e6..7fce6b9eb 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -314,6 +314,24 @@ static const glsection_t sections[] = { } }, + // GL 4.4 + { + .ver_gl = QGL_VER(4, 4), + .functions = (const glfunction_t []) { + QGL_FN(BindTextures), + { NULL } + } + }, + + // GL 4.5 + { + .ver_gl = QGL_VER(4, 5), + .functions = (const glfunction_t []) { + QGL_FN(BindTextureUnit), + { NULL } + } + }, + // GL 4.5 // GL_ARB_robustness { diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index 61c900aa7..d23295e79 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -166,7 +166,11 @@ QGLAPI void (APIENTRYP qglDepthRangef)(GLfloat n, GLfloat f); // GL 4.3 QGLAPI void (APIENTRYP qglDebugMessageCallback)(GLDEBUGPROC callback, const void *userParam); +// GL 4.4 +QGLAPI void (APIENTRYP qglBindTextures)(GLuint first, GLsizei count, const GLuint *textures); + // GL 4.5 +QGLAPI void (APIENTRYP qglBindTextureUnit)(GLuint unit, GLuint texture); QGLAPI void (APIENTRYP qglReadnPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, void *data); // GL_ARB_fragment_program diff --git a/src/refresh/state.c b/src/refresh/state.c index 765b59858..5ee9a22a1 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -47,9 +47,12 @@ void GL_BindTexture(GLuint tmu, GLuint texnum) if (gls.texnums[tmu] == texnum) return; - GL_ActiveTexture(tmu); - - qglBindTexture(GL_TEXTURE_2D, texnum); + if (qglBindTextureUnit) { + qglBindTextureUnit(tmu, texnum); + } else { + GL_ActiveTexture(tmu); + qglBindTexture(GL_TEXTURE_2D, texnum); + } gls.texnums[tmu] = texnum; c.texSwitches++; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index a7372ee34..dba0dd8ec 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -455,10 +455,10 @@ void GL_Flush3D(void) if (q_unlikely(gl_lightmap->integer)) state &= ~GLS_INTENSITY_ENABLE; - } - if (tess.texnum[2]) - state |= GLS_GLOWMAP_ENABLE; + if (tess.texnum[2]) + state |= GLS_GLOWMAP_ENABLE; + } if (!(state & GLS_TEXTURE_REPLACE)) array |= GLA_COLOR; @@ -466,9 +466,20 @@ void GL_Flush3D(void) GL_StateBits(state); GL_ArrayBits(array); - for (int i = 0; i < MAX_TMUS; i++) - if (tess.texnum[i]) + if (qglBindTextures) { + int count = 0; + for (int i = 0; i < MAX_TMUS && tess.texnum[i]; i++) { + if (gls.texnums[i] != tess.texnum[i]) { + gls.texnums[i] = tess.texnum[i]; + count = i + 1; + } + } + if (count) + qglBindTextures(0, count, tess.texnum); + } else { + for (int i = 0; i < MAX_TMUS && tess.texnum[i]; i++) GL_BindTexture(i, tess.texnum[i]); + } if (gl_static.world.vertices) GL_LockArrays(tess.numverts); From 1a482cf6b6c69330407d8bd106f086ff82c38236 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 17 Aug 2024 01:22:42 +0300 Subject: [PATCH 567/974] Allow fetching query result with single call. --- src/refresh/gl.h | 1 + src/refresh/qgl.c | 1 + src/refresh/tess.c | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index ceb9d8330..516e47151 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -149,6 +149,7 @@ typedef enum { QGL_CAP_TEXTURE_NON_POWER_OF_TWO = BIT(6), QGL_CAP_TEXTURE_ANISOTROPY = BIT(7), QGL_CAP_UNPACK_SUBIMAGE = BIT(8), + QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(9), } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 7fce6b9eb..1ba1c031e 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -317,6 +317,7 @@ static const glsection_t sections[] = { // GL 4.4 { .ver_gl = QGL_VER(4, 4), + .caps = QGL_CAP_QUERY_RESULT_NO_WAIT, .functions = (const glfunction_t []) { QGL_FN(BindTextures), { NULL } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index dba0dd8ec..f5ee8b398 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -356,11 +356,20 @@ void GL_DrawFlares(void) continue; if (q->pending) { - qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_AVAILABLE, &result); - if (result) { - qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT, &result); - q->visible = result; - q->pending = false; + if (gl_config.caps & QGL_CAP_QUERY_RESULT_NO_WAIT) { + result = -1; + qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_NO_WAIT, &result); + if (result != -1) { + q->visible = result; + q->pending = false; + } + } else { + qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_AVAILABLE, &result); + if (result) { + qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT, &result); + q->visible = result; + q->pending = false; + } } } From 2b5a77196c89ab01cfef6b5c7d600b6cd4dd035f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 17 Aug 2024 19:12:43 -0400 Subject: [PATCH 568/974] Converted __FUNCTION__ to __func__, added EspGetLeader() --- src/action/a_esp.c | 108 +++++++++++++++++++-------------- src/action/botlib/botlib_esp.c | 3 + 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index d7192d9c6..386bc8db3 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -99,7 +99,7 @@ void EspTimedMessageHandler(int teamNum, edict_t *ent, int seconds, int timedMsg if (teamNum > teamCount || teamNum < 0) return; if (ent && !ent->client){ - gi.dprintf("%s: Invalid entity passed\n", __FUNCTION__); + gi.dprintf("%s: Invalid entity passed\n", __func__); return; } // Convert frames to seconds @@ -353,9 +353,9 @@ void EspScoreBonuses(edict_t * targ, edict_t * attacker) if (esp_debug->value){ int bonusEligible = (level.realFramenum - ESP_BONUS_COOLDOWN); - gi.dprintf("%s: lpc is %d, lpl is %d, lhl is %d\n", __FUNCTION__, lpc, lpl, lhl); - gi.dprintf("%s: attacker is %s, targ is %s\n", __FUNCTION__, attacker->client->pers.netname, targ->client->pers.netname); - gi.dprintf("%s: level.realFramenum is %i, subtracted ESP_BONUS_COOLDOWN is %i\n", __FUNCTION__, level.realFramenum, bonusEligible); + gi.dprintf("%s: lpc is %d, lpl is %d, lhl is %d\n", __func__, lpc, lpl, lhl); + gi.dprintf("%s: attacker is %s, targ is %s\n", __func__, attacker->client->pers.netname, targ->client->pers.netname); + gi.dprintf("%s: level.realFramenum is %i, subtracted ESP_BONUS_COOLDOWN is %i\n", __func__, level.realFramenum, bonusEligible); } // If the attacker has already received a bonus in the last ESP_BONUS_COOLDOWN frames @@ -464,7 +464,7 @@ void EspMakeCapturePoint(edict_t *flag) trace_t tr = {0}; if (esp_debug->value) - gi.dprintf("%s: Creating a new capturepoint\n", __FUNCTION__); + gi.dprintf("%s: Creating a new capturepoint\n", __func__); VectorSet( flag->mins, -15, -15, -15 ); VectorSet( flag->maxs, 15, 15, 15 ); @@ -510,8 +510,8 @@ void EspMakeCapturePoint(edict_t *flag) gi.linkentity(flag->obj_arrow); if (esp_debug->value){ - gi.dprintf("%s: ** Indicator arrow spawned at <%f %f %f>\n", __FUNCTION__, flag->obj_arrow->s.origin[0], flag->obj_arrow->s.origin[1], flag->obj_arrow->s.origin[2]); - gi.dprintf("%s: ** Flag coordinates are: <%f %f %f>\n", __FUNCTION__, flag->s.origin[0], flag->s.origin[1], flag->s.origin[2]); + gi.dprintf("%s: ** Indicator arrow spawned at <%f %f %f>\n", __func__, flag->obj_arrow->s.origin[0], flag->obj_arrow->s.origin[1], flag->obj_arrow->s.origin[2]); + gi.dprintf("%s: ** Flag coordinates are: <%f %f %f>\n", __func__, flag->s.origin[0], flag->s.origin[1], flag->s.origin[2]); } } #endif @@ -562,7 +562,7 @@ void EspSetTeamSpawns(int team, char *str) do { if (sscanf(next, "<%f %f %f %f>", &pos[0], &pos[1], &pos[2], &angle) != 4) { if (esp_debug->value) - gi.dprintf("%s: invalid spawn point: %s, expected \n", __FUNCTION__, next); + gi.dprintf("%s: invalid spawn point: %s, expected \n", __func__, next); continue; } @@ -572,13 +572,13 @@ void EspSetTeamSpawns(int team, char *str) spawn->classname = ED_NewString (team_spawn_name); if (esp_debug->value) - gi.dprintf("%s: Created spawnpoint for %s at <%f %f %f %f>\n", __FUNCTION__, team_spawn_name, pos[0], pos[1], pos[2], angle); + gi.dprintf("%s: Created spawnpoint for %s at <%f %f %f %f>\n", __func__, team_spawn_name, pos[0], pos[1], pos[2], angle); es->custom_spawns[team][esp_potential_spawns] = spawn; esp_potential_spawns++; if (esp_potential_spawns >= MAX_SPAWNS) { if (esp_debug->value) - gi.dprintf("%s: Warning: MAX_SPAWNS exceeded\n", __FUNCTION__); + gi.dprintf("%s: Warning: MAX_SPAWNS exceeded\n", __func__); break; } @@ -595,7 +595,7 @@ void EspEnforceDefaultSettings(char *defaulttype) int i = 0; if (esp_debug->value) - gi.dprintf("%s: defaulttype is %s\n", __FUNCTION__, defaulttype); + gi.dprintf("%s: defaulttype is %s\n", __func__, defaulttype); if(default_author) { EspForceEspionage(ESPMODE_ATL); @@ -960,7 +960,7 @@ qboolean EspLoadConfig(const char *mapname) // Load skin indexes if (!no_file || !loaded_default_file) { if (esp_debug->value) - gi.dprintf("%s: *** Loading skin indexes\n", __FUNCTION__); + gi.dprintf("%s: *** Loading skin indexes\n", __func__); Q_snprintf(teams[TEAM1].skin_index, sizeof(teams[TEAM1].skin_index), "../players/%s_i", teams[TEAM1].skin); Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); @@ -1004,12 +1004,12 @@ void EspRespawnLCA(edict_t *ent) // Print out all conditions below as debug prints // This is massively noisy so I'm setting it so that esp_debug must be 2 to see it // if (esp_debug->value == 2) - // gi.dprintf("%s: ent->inuse is %d\n", __FUNCTION__, ent->inuse); - // gi.dprintf("%s: ent->client->resp.team is %d\n", __FUNCTION__, ent->client->resp.team); - // gi.dprintf("%s: ent->client->respawn_framenum is %d\n", __FUNCTION__, ent->client->respawn_framenum); - // gi.dprintf("%s: IS_LEADER(ent) is %d\n", __FUNCTION__, IS_LEADER(ent)); - // gi.dprintf("%s: ent->is_bot is %d\n", __FUNCTION__, ent->is_bot); - // gi.dprintf("%s: team_round_going is %d\n", __FUNCTION__, team_round_going); + // gi.dprintf("%s: ent->inuse is %d\n", __func__, ent->inuse); + // gi.dprintf("%s: ent->client->resp.team is %d\n", __func__, ent->client->resp.team); + // gi.dprintf("%s: ent->client->respawn_framenum is %d\n", __func__, ent->client->respawn_framenum); + // gi.dprintf("%s: IS_LEADER(ent) is %d\n", __func__, IS_LEADER(ent)); + // gi.dprintf("%s: ent->is_bot is %d\n", __func__, ent->is_bot); + // gi.dprintf("%s: team_round_going is %d\n", __func__, team_round_going); // Basically we just want real, dead players who are in the respawn waiting period if (!ent->inuse || @@ -1025,7 +1025,7 @@ void EspRespawnLCA(edict_t *ent) if (esp_debug->value) gi.dprintf("%s: Level framenum is %d, respawn timer was %d for %s, timercalc is %i, esp_respawn_sounds was %i\n", - __FUNCTION__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname, timercalc, ent->client->resp.esp_respawn_sounds); + __func__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname, timercalc, ent->client->resp.esp_respawn_sounds); // Subtract current framenum from respawn_timer to get a countdown if (timercalc <= 0){ @@ -1063,7 +1063,7 @@ void EspRespawnPlayer(edict_t *ent) if (team_round_going) { // Don't respawn until the current framenum is more than the respawn timer's framenum if (esp_debug->value) - gi.dprintf("%s: Level framenum is %d, respawn timer was %d for %s\n", __FUNCTION__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname); + gi.dprintf("%s: Level framenum is %d, respawn timer was %d for %s\n", __func__, level.framenum, ent->client->respawn_framenum, ent->client->pers.netname); if (level.framenum > ent->client->respawn_framenum) { if (esp_debug->value) { @@ -1157,14 +1157,14 @@ edict_t *SelectEspCustomSpawnPoint(edict_t * ent) // If we count zero custom spawns, then we need to safely return a better function // so we can spawn teams apart if (esp_debug->value) - gi.dprintf("%s: With zero spawnpoints, I am calling SelectRandomDeathmatchSpawnPoint()\n", __FUNCTION__); + gi.dprintf("%s: With zero spawnpoints, I am calling SelectRandomDeathmatchSpawnPoint()\n", __func__); return SelectRandomDeathmatchSpawnPoint(); } // Keep track of which spawn was last chosen esp_last_chosen_spawn = random_index; esp_spawnpoint_index[teamNum] = esp_last_chosen_spawn; if (esp_debug->value) - gi.dprintf("%s: For team %d, random index is %d\n", __FUNCTION__, teamNum, esp_spawnpoint_index[teamNum]); + gi.dprintf("%s: For team %d, random index is %d\n", __func__, teamNum, esp_spawnpoint_index[teamNum]); } // Everyone on each team spawns on the same spawnpoint index (spawn together) edict_t *spawn_point = es->custom_spawns[teamNum][esp_spawnpoint_index[teamNum]]; @@ -1177,7 +1177,7 @@ edict_t *SelectEspCustomSpawnPoint(edict_t * ent) if (spawn_point) return spawn_point; else { - gi.dprintf("%s: No spawnpoint found, safely return NULL so we can try another one\n", __FUNCTION__); + gi.dprintf("%s: No spawnpoint found, safely return NULL so we can try another one\n", __func__); return NULL; } } @@ -1251,8 +1251,8 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) return SelectDeathmatchSpawnPoint(); if (esp_debug->value){ - gi.dprintf("%s: Is team round going? %d\n", __FUNCTION__, team_round_going); - gi.dprintf("%s: Is the team leader alive? %d\n", __FUNCTION__, _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())); + gi.dprintf("%s: Is team round going? %d\n", __func__, team_round_going); + gi.dprintf("%s: Is the team leader alive? %d\n", __func__, _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())); } // Time to respawn on the leader! @@ -1266,7 +1266,7 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) // but if there are none, then we go back to old faithful } else { if (esp_debug->value) - gi.dprintf("%s: No custom spawns, defaulting to teamplay spawn\n", __FUNCTION__); + gi.dprintf("%s: No custom spawns, defaulting to teamplay spawn\n", __func__); // NULL check, if there are no teamplay (info_player_team...) spawns, then default to deathmatch spawns if (SelectTeamplaySpawnPoint(ent)) @@ -1277,7 +1277,7 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) } // All else fails, use deathmatch spawn points if (esp_debug->value) - gi.dprintf("%s: Defaulted all the way down here\n", __FUNCTION__); + gi.dprintf("%s: Defaulted all the way down here\n", __func__); return SelectFarthestDeathmatchSpawnPoint(); } @@ -1432,7 +1432,7 @@ qboolean EspCheckETVRules(void) // Print all condition states if (esp_debug->value) { - gi.dprintf("-- Debug start %s --\n", __FUNCTION__); + gi.dprintf("-- Debug start %s --\n", __func__); gi.dprintf("Roundlimit is %f\n", roundlimit->value); gi.dprintf("Team 1 score is %d\n", teams[TEAM1].score); gi.dprintf("Team 2 score is %d\n", teams[TEAM2].score); @@ -1442,7 +1442,7 @@ qboolean EspCheckETVRules(void) gi.dprintf("ETV is %f\n", etv->value); gi.dprintf("Use warnings is %f\n", use_warnings->value); gi.dprintf("ETV halftime is %f\n", esp_etv_halftime->value); - gi.dprintf("-- Debug end %s --\n", __FUNCTION__); + gi.dprintf("-- Debug end %s --\n", __func__); } if (!roundlimit->value && esp_etv_halftime->value){ @@ -1558,11 +1558,25 @@ qboolean AllTeamsHaveLeaders(void) return false; } if (esp_debug->value) - gi.dprintf("%s: Leadercount: %d\n", __FUNCTION__, teamsWithLeaders); + gi.dprintf("%s: Leadercount: %d\n", __func__, teamsWithLeaders); return false; } -qboolean EspSetLeader( int teamNum, edict_t *ent ) +edict_t* EspGetLeader(int teamNum) +{ + if (teamNum == NOTEAM) + return NULL; + + if (teams[teamNum].leader == NULL){ + if (esp_debug->value) + gi.dprintf("%s: No leader found for team %d\n", __func__, teamNum); + return NULL; + } + + return teams[teamNum].leader; +} + +qboolean EspSetLeader(int teamNum, edict_t *ent) { edict_t *oldLeader = teams[teamNum].leader; char temp[128]; @@ -1633,7 +1647,7 @@ qboolean EspChooseRandomLeader(int teamNum) if (team_round_going) { if (esp_debug->value) - gi.dprintf("%s: I was called because someone disconnected\n", __FUNCTION__); + gi.dprintf("%s: I was called because someone disconnected\n", __func__); if (matchmode->value && !TeamsReady()) return false; @@ -1655,13 +1669,13 @@ qboolean EspChooseRandomLeader(int teamNum) return false; if (esp_debug->value) - gi.dprintf("%s: players on team %d: %d\n", __FUNCTION__, teamNum, players[teamNum]); + gi.dprintf("%s: players on team %d: %d\n", __func__, teamNum, players[teamNum]); // Choose a random player from the playerList ent = playerList[rand() % numPlayers]; if (esp_debug->value) - gi.dprintf("%s: Randomly selected player on team %d: %s\n", __FUNCTION__, teamNum, ent->client->pers.netname); + gi.dprintf("%s: Randomly selected player on team %d: %s\n", __func__, teamNum, ent->client->pers.netname); // Set the selected player as the leader EspSetLeader(teamNum, ent); @@ -1703,7 +1717,7 @@ qboolean EspLeaderCheck(void) for (i = TEAM1; i <= teamCount; i++) { if (teams[i].leader && !IS_ALIVE(teams[i].leader) && team_round_going && !holding_on_tie_check) { if (esp_debug->value) - gi.dprintf("%s: Team %i leader is dead, but the round is still going...?\n", __FUNCTION__, teams[i].leader->client->resp.team); + gi.dprintf("%s: Team %i leader is dead, but the round is still going...?\n", __func__, teams[i].leader->client->resp.team); athl = false; } } @@ -1718,11 +1732,11 @@ qboolean EspLeaderCheck(void) // If we all have leaders, then we're good if (athl) { if (esp_debug->value) - gi.dprintf("%s: I don't need a leader!\n", __FUNCTION__); + gi.dprintf("%s: I don't need a leader!\n", __func__); return true; } else { if (esp_debug->value) - gi.dprintf("%s: I need a leader!\n", __FUNCTION__); + gi.dprintf("%s: I need a leader!\n", __func__); // We do not all have leaders, so we must cycle through each team for (i = TEAM1; i <= teamCount; i++) { if (!HAVE_LEADER(i)) { // If this team does not have a leader, get one @@ -1730,13 +1744,13 @@ qboolean EspLeaderCheck(void) if (newLeader) { if (EspSetLeader(i, newLeader)) { if (esp_debug->value) - gi.dprintf("%s: I found a leader!\n", __FUNCTION__); + gi.dprintf("%s: I found a leader!\n", __func__); return true; } } else { // Oops, no volunteers, then we force someone to be a leader if (EspChooseRandomLeader(i)) { if (esp_debug->value) - gi.dprintf("%s: I need a random leader!\n", __FUNCTION__); + gi.dprintf("%s: I need a random leader!\n", __func__); return true; } } @@ -1910,8 +1924,8 @@ void GenerateMedKit(qboolean instant) ent->client->medkit++; if (esp_debug->value){ - gi.dprintf("%s: I generated a medkit for %s, %d seconds since the round started, %d frames in\n", __FUNCTION__, ent->client->pers.netname, roundseconds, roundseconds); - gi.dprintf("%s: %s has %d medkits now\n", __FUNCTION__, ent->client->pers.netname, ent->client->medkit); + gi.dprintf("%s: I generated a medkit for %s, %d seconds since the round started, %d frames in\n", __func__, ent->client->pers.netname, roundseconds, roundseconds); + gi.dprintf("%s: %s has %d medkits now\n", __func__, ent->client->pers.netname, ent->client->medkit); } } } @@ -1997,13 +2011,13 @@ void EspCleanUp(void) int intervalAdd = 20; if (esp_debug->value) - gi.dprintf("%s: Cleanupinterval is %d, the time is now %d\n", __FUNCTION__, cleanupInterval, roundseconds); + gi.dprintf("%s: Cleanupinterval is %d, the time is now %d\n", __func__, cleanupInterval, roundseconds); if (roundseconds >= cleanupInterval) { CleanBodies(); cleanupInterval = cleanupInterval + intervalAdd; if (esp_debug->value) - gi.dprintf("%s: Bodies cleaned, cleanupinterval is now %d\n", __FUNCTION__, cleanupInterval); + gi.dprintf("%s: Bodies cleaned, cleanupinterval is now %d\n", __func__, cleanupInterval); } //gi.dprintf("the time is now %d\n", roundseconds); @@ -2070,13 +2084,13 @@ int EspSpawnpointCount(int teamNum) if (es->custom_spawns[teamNum][i] == NULL) break; if (esp_debug->value) - gi.dprintf("%s: Team %d spawncount %d coordinates are: %f, %f, %f\n", __FUNCTION__, teamNum, i, es->custom_spawns[teamNum][i]->s.origin[0], es->custom_spawns[teamNum][i]->s.origin[1], es->custom_spawns[teamNum][i]->s.origin[2]); + gi.dprintf("%s: Team %d spawncount %d coordinates are: %f, %f, %f\n", __func__, teamNum, i, es->custom_spawns[teamNum][i]->s.origin[0], es->custom_spawns[teamNum][i]->s.origin[1], es->custom_spawns[teamNum][i]->s.origin[2]); spawn_count++; } } if (esp_debug->value) - gi.dprintf("%s: Team %d has %d custom spawnpoints\n", __FUNCTION__, teamNum, spawn_count); + gi.dprintf("%s: Team %d has %d custom spawnpoints\n", __func__, teamNum, spawn_count); return spawn_count; } @@ -2087,9 +2101,9 @@ void EspDebug(void) for (i = TEAM1; i <= teamCount; i++) { if (HAVE_LEADER(i)) { - gi.dprintf("%s: Team %i leader: %s\n", __FUNCTION__, i, teams[i].leader->client->pers.netname); + gi.dprintf("%s: Team %i leader: %s\n", __func__, i, teams[i].leader->client->pers.netname); } else { - gi.dprintf("%s: Team %d does not have a leader\n", __FUNCTION__, i); + gi.dprintf("%s: Team %d does not have a leader\n", __func__, i); } } edict_t *ent; diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index 9ee802a35..faafef32e 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -4,6 +4,9 @@ esp_status_t bot_esp_status; + + + // Get the int BOTLIB_ESP_Get_Target_Node(edict_t *ent) { From a450f315cda9745a1bf9347ea4e27b142a24da98 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 17 Aug 2024 20:49:10 -0400 Subject: [PATCH 569/974] Some work towards botlib Espionage --- src/action/a_esp.h | 2 + src/action/botlib/botlib_ai.c | 5 + src/action/botlib/botlib_esp.c | 407 +++++++++++++++------------------ 3 files changed, 196 insertions(+), 218 deletions(-) diff --git a/src/action/a_esp.h b/src/action/a_esp.h index 616ebaa7e..ef3a8731d 100644 --- a/src/action/a_esp.h +++ b/src/action/a_esp.h @@ -93,6 +93,8 @@ typedef enum { #define ESP_ATTACKER_HARASS_RADIUS 400 // the radius around an object being defended where an attacker will get extra frags when assaulting the leader #define ESP_BONUS_COOLDOWN 500 // the number of frames after a bonus is awarded before another bonus can be awarded +int EspModeCheck(void); +edict_t* EspGetLeader(int teamNum); void EspForceEspionage(int espmode); void EspSetTeamSpawns(int, char *); int EspGetRespawnTime(edict_t *ent); diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 40332b4c4..b9638f05a 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -721,6 +721,11 @@ void BOTLIB_Think(edict_t* self) { BOTLIB_CTF_Goals(self); } + else if (esp->value) // ESP Goals + { + BOTLIB_ESP_Goals(self); + } + // Check if the bot is in a NAV state (I need a nav) or if NONE else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) { diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index faafef32e..b1b365b18 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -4,11 +4,8 @@ esp_status_t bot_esp_status; - - - -// Get the -int BOTLIB_ESP_Get_Target_Node(edict_t *ent) +// Get the target in ETV mode +int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader) { vec3_t mins = { -16, -16, -24 }; vec3_t maxs = { 16, 16, 32 }; @@ -24,9 +21,13 @@ int BOTLIB_ESP_Get_Target_Node(edict_t *ent) // Reset escortcap value espsettings.escortcap = false; - // Find the target - while ((tmp = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { - target = tmp; + // If leader is null, it means we're looking for an ETV target + if(leader == NULL) { + while ((tmp = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { + target = tmp; + } + } else { // Target is leader + target = leader; } int cloest_node_num = INVALID; @@ -111,75 +112,198 @@ int BOTLIB_ESP_Get_Target_Node(edict_t *ent) return nodes[node].nodenum; } + if(leader == NULL) + gi.dprintf("%s: there are no nodes near ETV target\n", __func__); + else + gi.dprintf("%s: there are no nodes near the leader\n", __func__); return INVALID; } -// Intercept flag carrier (good guy and bad guy) -// [OPTIONAL] distance: if bot is within distance -// Returns the node nearest to the carrier -int BOTLIB_InterceptLeader(edict_t* self, int team, float distance) +// Returns the distance to leader (friendly or enemy) +float BOTLIB_DistanceToLeader(edict_t* self, edict_t* leader) { - if (distance > 0) - { - if (team == TEAM1) - { - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } + if (leader == NULL) + return 9999999; + + float distanceToLeader = VectorDistance(leader->s.origin, self->s.origin); + + if(distanceToLeader < 9999999) + return distanceToLeader; + else + return 9999999;// Could not find distance +} + +// int BOTLIB_WhereIsTheLeader(edict_t* self, edict_t* leader) +// { +// int myTeam = self->client->resp.team; +// return BOTLIB_DistanceToLeader(self, leader); + +// if (myTeam == TEAM1) +// { +// if (BOTLIB_DistanceToLeader(self, FLAG_T2_NUM) <= distance) +// { +// //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); +// return bot_ctf_status.player_has_flag2->bot.current_node; +// } +// if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) +// { +// //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); +// return bot_ctf_status.player_has_flag1->bot.current_node; +// } +// } + +// if (myTeam == TEAM2) +// { +// if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) +// { +// //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); +// return bot_ctf_status.player_has_flag1->bot.current_node; +// } +// if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) +// { +// //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); +// return bot_ctf_status.player_has_flag2->bot.current_node; +// } +// } +// } +// else +// { +// if (myTeam == TEAM1) +// { +// if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) +// { +// //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); +// return bot_ctf_status.player_has_flag2->bot.current_node; +// } +// if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) +// { +// //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); +// return bot_ctf_status.player_has_flag1->bot.current_node; +// } +// } +// if (myTeam == TEAM2) +// { +// if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) +// { +// //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); +// return bot_ctf_status.player_has_flag1->bot.current_node; +// } +// if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) +// { +// //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); +// return bot_ctf_status.player_has_flag2->bot.current_node; +// } +// } +// } + +// return INVALID; +// } + + + +int BOTLIB_FindMyLeaderNode(edict_t* self) +{ + int myTeam = self->client->resp.team; + edict_t* leader = teams[myTeam].leader; + //float distance; + + if (IS_ALIVE(leader)) { + if (leader->is_bot) { // No need to do fancy stuff if leader is a bot + return leader->bot.current_node; + } else { + //distance = BOTLIB_DistanceToLeader(self, leader); + return BOTLIB_ESPGetTargetNode(self, leader); } + } + return INVALID; +} - if (team == TEAM2) - { - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T1_NUM) <= distance) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false && BOTLIB_DistanceToFlag(self, FLAG_T2_NUM) <= distance) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } +int BOTLIB_FindEnemyLeaderNode(edict_t* self, int teamNum) +{ + edict_t* enemy_leader = teams[teamNum].leader; + + if (IS_ALIVE(enemy_leader)) { + if (enemy_leader->is_bot) { // No need to do fancy stuff if leader is a bot + return enemy_leader->bot.current_node; + } else { + //distance = BOTLIB_DistanceToLeader(self, leader); + return BOTLIB_ESPGetTargetNode(self, enemy_leader); } } - else - { - if (team == TEAM1) - { - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } + return INVALID; +} + +int BOTLIB_InterceptLeader_3Team(edict_t* self) +{ + int teamTarget = -1; + int myTeam = self->client->resp.team; + int teams[] = {TEAM1, TEAM2, TEAM3}; + int aliveTeams[2]; + int aliveCount = 0; + + // Check which teams' leaders are alive + for (int i = 0; i < 3; i++) + { + if (teams[i] != myTeam && IS_ALIVE(EspGetLeader(teams[i]))) + { + aliveTeams[aliveCount++] = teams[i]; + } + } + + // Determine the target team + if (aliveCount == 2) + { + teamTarget = aliveTeams[rand() % 2]; // Randomize between the two alive teams + } + else if (aliveCount == 1) + { + teamTarget = aliveTeams[0]; // Only one team leader is alive + } + else + { + return INVALID; // No team leaders are alive, so no node to intercept + } + + return BOTLIB_FindEnemyLeaderNode(self, teamTarget); +} + +int BOTLIB_InterceptLeader_2Team(edict_t* self) +{ + int myTeam = self->client->resp.team; + + if (myTeam == TEAM1) { + if (IS_ALIVE(EspGetLeader(TEAM2))) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM2); } - if (team == TEAM2) - { - if (bot_ctf_status.player_has_flag1 && bot_ctf_status.flag1_is_home == false) - { - //Com_Printf("%s %s intercepting RED flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T1_NUM), bot_ctf_status.player_has_flag1->bot.current_node); - return bot_ctf_status.player_has_flag1->bot.current_node; - } - if (bot_ctf_status.player_has_flag2 && bot_ctf_status.flag2_is_home == false) - { - //Com_Printf("%s %s intercepting BLUE flag carrier %s dist[%f] node[%i] \n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, BOTLIB_DistanceToFlag(self, FLAG_T2_NUM), bot_ctf_status.player_has_flag2->bot.current_node); - return bot_ctf_status.player_has_flag2->bot.current_node; - } + } else { + if (IS_ALIVE(EspGetLeader(TEAM1))) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM1); } } + return INVALID; // No team leaders are alive, so no node to intercept +} - return INVALID; +int BOTLIB_InterceptLeader_ETV(edict_t* self) +{ + +} + +// Intercept enemy team leader (bad guy) +// [OPTIONAL] distance: if bot is within distance +// Returns the node nearest to the leader +int BOTLIB_InterceptEnemyLeader(edict_t* self) +{ + if (EspModeCheck() == ESPMODE_ATL) { + if (use_3teams->value) { + return BOTLIB_InterceptLeader_3Team(self); + } else { + return BOTLIB_InterceptLeader_2Team(self); + } + } else if (EspModeCheck() == ESPMODE_ETV) { + return BOTLIB_InterceptLeader_ETV(self); + } else { + return INVALID; + } } float BOTLIB_DistanceToEnemyLeader(edict_t* self, int flagType) @@ -244,159 +368,6 @@ float BOTLIB_DistanceToEnemyLeader(edict_t* self, int flagType) return 9999999; // Could not find distance } -// Get the status of flags -void BOTLIB_Update_Flags_Status(void) -{ - // Check if any players are carrying a flag - // Returns INVALID if no enemy has the flag - bot_ctf_status.player_has_flag1 = NULL; - bot_ctf_status.player_has_flag2 = NULL; - for (int p = 0; p < num_players; p++) - { - if (players[p]->client->inventory[items[FLAG_T1_NUM].index]) - bot_ctf_status.player_has_flag1 = players[p]; - - if (players[p]->client->inventory[items[FLAG_T2_NUM].index]) - bot_ctf_status.player_has_flag2 = players[p]; - } - - bot_ctf_status.flag1 = NULL; - bot_ctf_status.flag2 = NULL; - bot_ctf_status.flag1_is_home = true; - bot_ctf_status.flag2_is_home = true; - bot_ctf_status.flag1_is_dropped = false; - bot_ctf_status.flag2_is_dropped = false; - bot_ctf_status.flag1_is_carried = false; - bot_ctf_status.flag2_is_carried = false; - - int base = 1 + game.maxclients + BODY_QUEUE_SIZE; - edict_t* ent = g_edicts + base; - for (int i = base; i < globals.num_edicts; i++, ent++) - { - if (ent->inuse == false) continue; - if (!ent->classname) continue; - - // When at home: - // Spawned: 0 0 1 0 -- 0, 0, SOLID_TRIGGER, 0 - // Returned: 80000000 0 1 40000 -- FL_RESPAWN, 0, SOLID_TRIGGER, ITEM_TARGETS_USED - // - // When picked up: - // Picked: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED - // - // When dropped after being picked up: - // Dropped: 80000000 1 0 40000 -- FL_RESPAWN, SVF_NOCLIENT, SOLID_NOT, ITEM_TARGETS_USED - // Dropped: 0 0 1 10000 -- 0, 0, SOLID_TRIGGER, DROPPED_ITEM - - - if (ent->typeNum == FLAG_T1_NUM) - { - //Com_Printf("%s flag FLAG_T1_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); - - bot_ctf_status.flag1 = ent; - bot_ctf_status.flag1_curr_node = BOTLIB_ESP_Get_Target_Node(ent); - - // Home - never touched (set once per map) - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag1_home_node == INVALID) - { - /* - // Add a flag node here - trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); - int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); - if (node_added != INVALID) - { - BOTLIB_LinkNodesNearbyNode(ent, node_added); - bot_ctf_status.flag1_home_node = node_added; - Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); - } - */ - - bot_ctf_status.flag1_is_home = true; - bot_ctf_status.flag1_home_node = BOTLIB_ESP_Get_Target_Node(ent); - //Com_Printf("%s Red flag home to node %i\n", __func__, bot_ctf_status.flag1_home_node); - } - // Returned - if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag1_is_home = true; - } - - // Picked up - if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag1_is_home = false; - bot_ctf_status.flag1_is_carried = true; - if (bot_ctf_status.flag2_home_node != INVALID && bot_ctf_status.player_has_flag1 != NULL) - bot_ctf_status.team2_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, bot_ctf_status.player_has_flag1->s.origin); - } - // Dropped - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) - { - bot_ctf_status.flag1_is_home = false; - bot_ctf_status.flag1_is_dropped = true; - } - } - - if (ent->typeNum == FLAG_T2_NUM) - { - //Com_Printf("%s flag FLAG_T2_NUM %x %x %d %x\n", __func__, ent->flags, ent->svflags, ent->solid, ent->spawnflags); - - bot_ctf_status.flag2 = ent; - bot_ctf_status.flag2_curr_node = BOTLIB_ESP_Get_Target_Node(ent); - - // Home - never touched (set once per map) - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == 0 && bot_ctf_status.flag2_home_node == INVALID) - { - /* - // Add a flag node here - trace_t tr = gi.trace(ent->s.origin, tv(-16, -16, -24), tv(16, 16, 32), ent->s.origin, ent, MASK_PLAYERSOLID); - int node_added = BOTLIB_AddNode(ent->s.origin, tr.plane.normal, NODE_MOVE); - if (node_added != INVALID) - { - BOTLIB_LinkNodesNearbyNode(ent, node_added); - bot_ctf_status.flag2_home_node = node_added; - Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); - } - */ - - bot_ctf_status.flag2_is_home = true; - bot_ctf_status.flag2_home_node = BOTLIB_ESP_Get_Target_Node(ent); - Com_Printf("%s Blue flag home to node %i\n", __func__, bot_ctf_status.flag2_home_node); - } - // Returned - if (ent->flags == FL_RESPAWN && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag2_is_home = true; - } - - // Picked - if (ent->flags == FL_RESPAWN && ent->svflags == SVF_NOCLIENT && ent->solid == SOLID_NOT && ent->spawnflags == ITEM_TARGETS_USED) - { - bot_ctf_status.flag2_is_home = false; - bot_ctf_status.flag2_is_carried = true; - if (bot_ctf_status.flag1_home_node != INVALID && bot_ctf_status.player_has_flag2 != NULL) - bot_ctf_status.team1_carrier_dist_to_home = VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, bot_ctf_status.player_has_flag2->s.origin); - } - // Dropped - if (ent->flags == 0 && ent->svflags == 0 && ent->solid == SOLID_TRIGGER && ent->spawnflags == DROPPED_ITEM && VectorLength(ent->velocity) == 0) - { - bot_ctf_status.flag2_is_home = false; - bot_ctf_status.flag2_is_dropped = true; - } - - // Flag home moved - if (bot_ctf_status.flag2_is_home && bot_ctf_status.flag2_home_node != BOTLIB_ESP_Get_Target_Node(ent)) - { - //Com_Printf("%s Blue flag home was moved to node %i\n", __func__, bot_ctf_status.flag2_home_node); - bot_ctf_status.flag2_home_node = BOTLIB_ESP_Get_Target_Node(ent); // Update home node - } - } - - - - //if (ent->solid == SOLID_NOT) continue; // picked up - } - //Com_Printf("%s\n", __func__); -} void BOTLIB_CTF_Goals(edict_t* self) { From 0ac672a142667c487d11e182f6d1f675bde1438b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 14:29:31 +0300 Subject: [PATCH 570/974] Get rid of tess.colors. Store colors in tess.vertices instead. --- inc/common/intreadwrite.h | 6 +++ src/refresh/draw.c | 55 +++++++++++------------ src/refresh/gl.h | 2 +- src/refresh/main.c | 28 +++++++----- src/refresh/tess.c | 92 ++++++++++++++++++--------------------- 5 files changed, 90 insertions(+), 93 deletions(-) diff --git a/inc/common/intreadwrite.h b/inc/common/intreadwrite.h index bbe4eb124..81c6514e8 100644 --- a/inc/common/intreadwrite.h +++ b/inc/common/intreadwrite.h @@ -48,6 +48,12 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #define WN32(p, v) (*(uint32_t *)(p) = (v)) #define WN64(p, v) (*(uint64_t *)(p) = (v)) +#else + +#define WN16(p, v) memcpy(p, &(uint16_t){ v }, sizeof(uint16_t)) +#define WN32(p, v) memcpy(p, &(uint32_t){ v }, sizeof(uint32_t)) +#define WN64(p, v) memcpy(p, &(uint64_t){ v }, sizeof(uint64_t)) + #endif #if USE_LITTLE_ENDIAN diff --git a/src/refresh/draw.c b/src/refresh/draw.c index d73a78ef7..e22302484 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -26,7 +26,6 @@ static inline void GL_StretchPic_( uint32_t color, int texnum, int flags) { vec_t *dst_vert; - uint32_t *dst_color; QGL_INDEX_TYPE *dst_indices; if (tess.numverts + 4 > TESS_MAX_VERTICES || @@ -36,17 +35,16 @@ static inline void GL_StretchPic_( tess.texnum[0] = texnum; - dst_vert = tess.vertices + tess.numverts * 4; + dst_vert = tess.vertices + tess.numverts * 5; Vector4Set(dst_vert, x, y, s1, t1); - Vector4Set(dst_vert + 4, x + w, y, s2, t1); - Vector4Set(dst_vert + 8, x + w, y + h, s2, t2); - Vector4Set(dst_vert + 12, x, y + h, s1, t2); + Vector4Set(dst_vert + 5, x + w, y, s2, t1); + Vector4Set(dst_vert + 10, x + w, y + h, s2, t2); + Vector4Set(dst_vert + 15, x, y + h, s1, t2); - dst_color = (uint32_t *)tess.colors + tess.numverts; - dst_color[0] = color; - dst_color[1] = color; - dst_color[2] = color; - dst_color[3] = color; + WN32(dst_vert + 4, color); + WN32(dst_vert + 9, color); + WN32(dst_vert + 14, color); + WN32(dst_vert + 19, color); dst_indices = tess.indices + tess.numindices; dst_indices[0] = tess.numverts + 0; @@ -76,7 +74,6 @@ static inline void GL_StretchPic_( static void GL_DrawVignette(int distance, color_t outer, color_t inner) { vec_t *dst_vert; - uint32_t *dst_color; QGL_INDEX_TYPE *dst_indices; if (tess.numverts + 8 > TESS_MAX_VERTICES || @@ -90,17 +87,16 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) int w = glr.fd.width, h = glr.fd.height; // outer vertices - dst_vert = tess.vertices + tess.numverts * 4; + dst_vert = tess.vertices + tess.numverts * 5; Vector4Set(dst_vert, x, y, 0, 0); - Vector4Set(dst_vert + 4, x + w, y, 0, 0); - Vector4Set(dst_vert + 8, x + w, y + h, 0, 0); - Vector4Set(dst_vert + 12, x, y + h, 0, 0); + Vector4Set(dst_vert + 5, x + w, y, 0, 0); + Vector4Set(dst_vert + 10, x + w, y + h, 0, 0); + Vector4Set(dst_vert + 15, x, y + h, 0, 0); - dst_color = (uint32_t *)tess.colors + tess.numverts; - dst_color[0] = outer.u32; - dst_color[1] = outer.u32; - dst_color[2] = outer.u32; - dst_color[3] = outer.u32; + WN32(dst_vert + 4, outer.u32); + WN32(dst_vert + 9, outer.u32); + WN32(dst_vert + 14, outer.u32); + WN32(dst_vert + 19, outer.u32); // inner vertices x += distance; @@ -108,17 +104,16 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) w -= distance * 2; h -= distance * 2; - dst_vert += 16; + dst_vert += 20; Vector4Set(dst_vert, x, y, 0, 0); - Vector4Set(dst_vert + 4, x + w, y, 0, 0); - Vector4Set(dst_vert + 8, x + w, y + h, 0, 0); - Vector4Set(dst_vert + 12, x, y + h, 0, 0); - - dst_color += 4; - dst_color[0] = inner.u32; - dst_color[1] = inner.u32; - dst_color[2] = inner.u32; - dst_color[3] = inner.u32; + Vector4Set(dst_vert + 5, x + w, y, 0, 0); + Vector4Set(dst_vert + 10, x + w, y + h, 0, 0); + Vector4Set(dst_vert + 15, x, y + h, 0, 0); + + WN32(dst_vert + 4, inner.u32); + WN32(dst_vert + 9, inner.u32); + WN32(dst_vert + 14, inner.u32); + WN32(dst_vert + 19, inner.u32); /* 0 1 diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 516e47151..280de930f 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/cvar.h" #include "common/files.h" #include "common/hash_map.h" +#include "common/intreadwrite.h" #include "common/math.h" #include "client/video.h" #include "client/client.h" @@ -706,7 +707,6 @@ extern cvar_t *gl_intensity; typedef struct { GLfloat vertices[VERTEX_SIZE * TESS_MAX_VERTICES]; QGL_INDEX_TYPE indices[TESS_MAX_INDICES]; - GLubyte colors[4 * TESS_MAX_VERTICES]; GLuint texnum[MAX_TMUS]; int numverts; int numindices; diff --git a/src/refresh/main.c b/src/refresh/main.c index 70d07c9c8..ba2574c87 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -367,27 +367,31 @@ static void GL_DrawSpriteModel(const model_t *model) static void GL_DrawNullModel(void) { - static const uint32_t colors[6] = { - U32_RED, U32_RED, - U32_GREEN, U32_GREEN, - U32_BLUE, U32_BLUE - }; const entity_t *e = glr.ent; VectorCopy(e->origin, tess.vertices + 0); - VectorCopy(e->origin, tess.vertices + 6); - VectorCopy(e->origin, tess.vertices + 12); + VectorCopy(e->origin, tess.vertices + 8); + VectorCopy(e->origin, tess.vertices + 16); - VectorMA(e->origin, 16, glr.entaxis[0], tess.vertices + 3); - VectorMA(e->origin, 16, glr.entaxis[1], tess.vertices + 9); - VectorMA(e->origin, 16, glr.entaxis[2], tess.vertices + 15); + VectorMA(e->origin, 16, glr.entaxis[0], tess.vertices + 4); + VectorMA(e->origin, 16, glr.entaxis[1], tess.vertices + 12); + VectorMA(e->origin, 16, glr.entaxis[2], tess.vertices + 20); + + WN32(tess.vertices + 3, U32_RED); + WN32(tess.vertices + 7, U32_RED); + + WN32(tess.vertices + 11, U32_GREEN); + WN32(tess.vertices + 15, U32_GREEN); + + WN32(tess.vertices + 19, U32_BLUE); + WN32(tess.vertices + 23, U32_BLUE); GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte *)colors); - GL_VertexPointer(3, 0, tess.vertices); + GL_VertexPointer(3, 4, tess.vertices); + GL_ColorBytePointer(4, 4, (GLubyte *)(tess.vertices + 3)); GL_LockArrays(6); qglDrawArrays(GL_LINES, 0, 6); GL_UnlockArrays(); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index f5ee8b398..4ccaf4759 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -45,9 +45,9 @@ void GL_Flush2D(void) GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - GL_VertexPointer(2, 4, tess.vertices); - GL_TexCoordPointer(2, 4, tess.vertices + 2); - GL_ColorBytePointer(4, 0, tess.colors); + GL_VertexPointer(2, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 2); + GL_ColorBytePointer(4, 5, (GLubyte *)(tess.vertices + 4)); GL_LockArrays(tess.numverts); @@ -78,7 +78,6 @@ void GL_DrawParticles(void) color_t color; int numverts; vec_t *dst_vert; - uint32_t *dst_color; glStateBits_t bits; if (!glr.fd.num_particles) @@ -86,9 +85,9 @@ void GL_DrawParticles(void) GL_LoadMatrix(glr.viewmatrix); - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); - GL_ColorBytePointer(4, 0, tess.colors); + GL_VertexPointer(3, 6, tess.vertices); + GL_TexCoordPointer(2, 6, tess.vertices + 3); + GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE; @@ -103,7 +102,6 @@ void GL_DrawParticles(void) total -= count; dst_vert = tess.vertices; - dst_color = (uint32_t *)tess.colors; numverts = count * 3; do { VectorSubtract(p->origin, glr.fd.vieworg, transformed); @@ -116,14 +114,12 @@ void GL_DrawParticles(void) VectorMA(p->origin, scale2, glr.viewaxis[1], dst_vert); VectorMA(dst_vert, -scale2, glr.viewaxis[2], dst_vert); - VectorMA(dst_vert, scale, glr.viewaxis[2], dst_vert + 5); - VectorMA(dst_vert, -scale, glr.viewaxis[1], dst_vert + 10); + VectorMA(dst_vert, scale, glr.viewaxis[2], dst_vert + 6); + VectorMA(dst_vert, -scale, glr.viewaxis[1], dst_vert + 12); dst_vert[ 3] = 0; dst_vert[ 4] = 0; - dst_vert[ 8] = 0; dst_vert[ 9] = PARTICLE_SIZE; - dst_vert[13] = PARTICLE_SIZE; dst_vert[14] = 0; - - dst_vert += 15; + dst_vert[ 9] = 0; dst_vert[10] = PARTICLE_SIZE; + dst_vert[15] = PARTICLE_SIZE; dst_vert[16] = 0; if (p->color == -1) color.u32 = p->rgba.u32; @@ -131,11 +127,11 @@ void GL_DrawParticles(void) color.u32 = d_8to24table[p->color & 0xff]; color.u8[3] *= p->alpha; - dst_color[0] = color.u32; - dst_color[1] = color.u32; - dst_color[2] = color.u32; - dst_color += 3; + WN32(dst_vert + 5, color.u32); + WN32(dst_vert + 11, color.u32); + WN32(dst_vert + 17, color.u32); + dst_vert += 18; p++; } while (--count); @@ -174,7 +170,6 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col { vec3_t d1, d2, d3; vec_t *dst_vert; - uint32_t *dst_color; QGL_INDEX_TYPE *dst_indices; VectorSubtract(end, start, d1); @@ -188,22 +183,21 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col tess.numindices + 6 > TESS_MAX_INDICES)) GL_FlushBeamSegments(); - dst_vert = tess.vertices + tess.numverts * 5; + dst_vert = tess.vertices + tess.numverts * 6; VectorAdd(start, d3, dst_vert); - VectorSubtract(start, d3, dst_vert + 5); - VectorSubtract(end, d3, dst_vert + 10); - VectorAdd(end, d3, dst_vert + 15); + VectorSubtract(start, d3, dst_vert + 6); + VectorSubtract(end, d3, dst_vert + 12); + VectorAdd(end, d3, dst_vert + 18); dst_vert[ 3] = 0; dst_vert[ 4] = 0; - dst_vert[ 8] = 1; dst_vert[ 9] = 0; - dst_vert[13] = 1; dst_vert[14] = 1; - dst_vert[18] = 0; dst_vert[19] = 1; + dst_vert[ 9] = 1; dst_vert[10] = 0; + dst_vert[15] = 1; dst_vert[16] = 1; + dst_vert[21] = 0; dst_vert[22] = 1; - dst_color = (uint32_t *)tess.colors + tess.numverts; - dst_color[0] = color.u32; - dst_color[1] = color.u32; - dst_color[2] = color.u32; - dst_color[3] = color.u32; + WN32(dst_vert + 5, color.u32); + WN32(dst_vert + 11, color.u32); + WN32(dst_vert + 17, color.u32); + WN32(dst_vert + 23, color.u32); dst_indices = tess.indices + tess.numindices; dst_indices[0] = tess.numverts + 0; @@ -273,9 +267,9 @@ void GL_DrawBeams(void) GL_LoadMatrix(glr.viewmatrix); - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); - GL_ColorBytePointer(4, 0, tess.colors); + GL_VertexPointer(3, 6, tess.vertices); + GL_TexCoordPointer(2, 6, tess.vertices + 3); + GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_BEAM)) @@ -328,7 +322,6 @@ void GL_DrawFlares(void) vec3_t up, down, left, right; color_t color; vec_t *dst_vert; - uint32_t *dst_color; QGL_INDEX_TYPE *dst_indices; GLuint result, texnum; const entity_t *ent; @@ -343,9 +336,9 @@ void GL_DrawFlares(void) GL_LoadMatrix(glr.viewmatrix); - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); - GL_ColorBytePointer(4, 0, tess.colors); + GL_VertexPointer(3, 6, tess.vertices); + GL_TexCoordPointer(2, 6, tess.vertices + 3); + GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_FLARE)) @@ -393,26 +386,25 @@ void GL_DrawFlares(void) VectorScale(glr.viewaxis[2], -scale, down); VectorScale(glr.viewaxis[2], scale, up); - dst_vert = tess.vertices + tess.numverts * 5; + dst_vert = tess.vertices + tess.numverts * 6; VectorAdd3(ent->origin, down, left, dst_vert); - VectorAdd3(ent->origin, up, left, dst_vert + 5); - VectorAdd3(ent->origin, up, right, dst_vert + 10); - VectorAdd3(ent->origin, down, right, dst_vert + 15); + VectorAdd3(ent->origin, up, left, dst_vert + 6); + VectorAdd3(ent->origin, up, right, dst_vert + 12); + VectorAdd3(ent->origin, down, right, dst_vert + 18); dst_vert[ 3] = 0; dst_vert[ 4] = 1; - dst_vert[ 8] = 0; dst_vert[ 9] = 0; - dst_vert[13] = 1; dst_vert[14] = 0; - dst_vert[18] = 1; dst_vert[19] = 1; + dst_vert[ 9] = 0; dst_vert[10] = 0; + dst_vert[15] = 1; dst_vert[16] = 0; + dst_vert[21] = 1; dst_vert[22] = 1; color.u32 = ent->rgba.u32; color.u8[3] = 128 * (ent->alpha * q->frac); - dst_color = (uint32_t *)tess.colors + tess.numverts; - dst_color[0] = color.u32; - dst_color[1] = color.u32; - dst_color[2] = color.u32; - dst_color[3] = color.u32; + WN32(dst_vert + 5, color.u32); + WN32(dst_vert + 11, color.u32); + WN32(dst_vert + 17, color.u32); + WN32(dst_vert + 23, color.u32); dst_indices = tess.indices + tess.numindices; dst_indices[0] = tess.numverts + 0; From aa14371ea7f544472c76f47c4075e527ae85644c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 15:39:55 +0300 Subject: [PATCH 571/974] Add caching of vertex array bindings. Similar to VAOs, but doesn't actually use them for compatibility. Reduces number of array bindings per frame. --- src/refresh/draw.c | 3 +- src/refresh/gl.h | 20 ++++++++- src/refresh/main.c | 14 +++---- src/refresh/mesh.c | 12 +++--- src/refresh/sky.c | 3 +- src/refresh/surf.c | 4 ++ src/refresh/tess.c | 99 ++++++++++++++++++++++++++++++--------------- src/refresh/world.c | 4 +- 8 files changed, 106 insertions(+), 53 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index e22302484..4fb38157d 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -436,7 +436,7 @@ void Draw_Stats(void) int x = 10, y = 10; R_SetScale(1.0f / get_auto_scale()); - R_DrawFill8(8, 8, 25*8, 22*10+2, 4); + R_DrawFill8(8, 8, 25*8, 23*10+2, 4); Draw_Stringf(x, y, "Nodes visible : %i", glr.nodes_visible); y += 10; Draw_Stringf(x, y, "Nodes culled : %i", c.nodesCulled); y += 10; @@ -459,6 +459,7 @@ void Draw_Stats(void) Draw_Stringf(x, y, "Total dlights : %i", glr.fd.num_dlights); y += 10; Draw_Stringf(x, y, "Total particles: %i", glr.fd.num_particles); y += 10; Draw_Stringf(x, y, "Uniform uploads: %i", c.uniformUploads); y += 10; + Draw_Stringf(x, y, "Array binds : %i", c.vertexArrayBinds); y += 10; Draw_Stringf(x, y, "Occl. queries : %i", c.occlusionQueries); y += 10; R_SetScale(1.0f); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 280de930f..6c6527c7e 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -85,6 +85,22 @@ typedef struct { bool visible; } glquery_t; +typedef enum { + VAO_NONE, + + VAO_SPRITE, + VAO_EFFECT, + VAO_NULLMODEL, + VAO_OCCLUDE, + VAO_WATERWARP, + VAO_MESH_SHADE, + VAO_MESH_FLAT, + VAO_2D, + VAO_3D, + + VAO_TOTAL +} glVertexArray_t; + typedef struct { bool registering; bool use_shaders; @@ -194,6 +210,7 @@ typedef struct { int rotatedBoxesCulled; int batchesDrawn2D; int uniformUploads; + int vertexArrayBinds; int occlusionQueries; } statCounters_t; @@ -511,6 +528,7 @@ typedef struct { GLuint texnums[MAX_TMUS]; GLbitfield state_bits; GLbitfield array_bits; + glVertexArray_t currentvao; const GLfloat *currentmatrix; struct { GLfloat view[16]; @@ -720,7 +738,7 @@ void GL_DrawParticles(void); void GL_DrawBeams(void); void GL_DrawFlares(void); -void GL_BindArrays(void); +void GL_BindArrays(glVertexArray_t vao); void GL_Flush3D(void); void GL_AddAlphaFace(mface_t *face, entity_t *ent); diff --git a/src/refresh/main.c b/src/refresh/main.c index ba2574c87..c1bd36c46 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -339,6 +339,7 @@ static void GL_DrawSpriteModel(const model_t *model) GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, image->texnum); + GL_BindArrays(VAO_SPRITE); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC); GL_Color(1, 1, 1, alpha); @@ -358,8 +359,6 @@ static void GL_DrawSpriteModel(const model_t *model) tess.vertices[13] = 1; tess.vertices[14] = 1; tess.vertices[18] = 1; tess.vertices[19] = 0; - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_UnlockArrays(); @@ -388,10 +387,10 @@ static void GL_DrawNullModel(void) GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); + GL_BindArrays(VAO_NULLMODEL); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_VertexPointer(3, 4, tess.vertices); - GL_ColorBytePointer(4, 4, (GLubyte *)(tess.vertices + 3)); + GL_LockArrays(6); qglDrawArrays(GL_LINES, 0, 6); GL_UnlockArrays(); @@ -455,11 +454,11 @@ static void GL_OccludeFlares(void) if (!set) { GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(0, TEXNUM_WHITE); + GL_BindArrays(VAO_OCCLUDE); GL_StateBits(GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX); qglColorMask(0, 0, 0, 0); - GL_BindTexture(0, TEXNUM_WHITE); - GL_VertexPointer(3, 0, tess.vertices); set = true; } @@ -609,6 +608,7 @@ static void GL_WaterWarp(void) float x0, x1, y0, y1; GL_ForceTexture(0, gl_static.warp_texture); + GL_BindArrays(VAO_WATERWARP); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | GLS_TEXTURE_REPLACE | GLS_WARP_ENABLE); GL_ArrayBits(GLA_VERTEX | GLA_TC); @@ -624,8 +624,6 @@ static void GL_WaterWarp(void) Vector4Set(tess.vertices + 8, x1, y0, 1, 1); Vector4Set(tess.vertices + 12, x1, y1, 1, 0); - GL_VertexPointer(2, 4, tess.vertices); - GL_TexCoordPointer(2, 4, tess.vertices + 2); GL_LockArrays(4); qglDrawArrays(GL_TRIANGLE_STRIP, 0, 4); GL_UnlockArrays(); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 0058c9fa7..8e66c30b0 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -551,12 +551,13 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX); GL_BindTexture(0, TEXNUM_WHITE); - GL_VertexPointer(3, dotshading ? VERTEX_SIZE : 4, tess.vertices); - GL_LockArrays(num_verts); qglColorMask(0, 0, 0, 0); + + GL_LockArrays(num_verts); GL_DrawTriangles(num_indices, indices); - qglColorMask(1, 1, 1, 1); GL_UnlockArrays(); + + qglColorMask(1, 1, 1, 1); } if (dotshading) @@ -577,11 +578,8 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, if (dotshading) { GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); - GL_ColorFloatPointer(4, VERTEX_SIZE, tess.vertices + 4); } else { GL_ArrayBits(GLA_VERTEX | GLA_TC); - GL_VertexPointer(3, 4, tess.vertices); GL_Color(color[0], color[1], color[2], color[3]); } @@ -809,6 +807,8 @@ void GL_DrawAliasModel(const model_t *model) GL_RotateForEntity(); + GL_BindArrays(dotshading ? VAO_MESH_SHADE : VAO_MESH_FLAT); + if (ent->flags & RF_WEAPONMODEL) setup_weaponmodel(); diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 4260451f2..f67ee5713 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -333,10 +333,9 @@ void R_DrawSkyBox(void) if (!skyfaces) return; // nothing visible + GL_BindArrays(VAO_SPRITE); GL_StateBits(GLS_TEXTURE_REPLACE); GL_ArrayBits(GLA_VERTEX | GLA_TC); - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); for (i = 0; i < 6; i++) { if (skymins[0][i] >= skymaxs[0][i] || diff --git a/src/refresh/surf.c b/src/refresh/surf.c index ddd3f0fa6..4f5221e06 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -964,6 +964,10 @@ void GL_FreeWorld(void) else if (qglDeleteBuffers) qglDeleteBuffers(1, &gl_static.world.bufnum); + // invalidate binding + if (gls.currentvao == VAO_3D) + gls.currentvao = VAO_NONE; + memset(&gl_static.world, 0, sizeof(gl_static.world)); } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 4ccaf4759..ddd2432f7 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -42,13 +42,10 @@ void GL_Flush2D(void) Scrap_Upload(); GL_BindTexture(0, tess.texnum[0]); + GL_BindArrays(VAO_2D); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - GL_VertexPointer(2, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 2); - GL_ColorBytePointer(4, 5, (GLubyte *)(tess.vertices + 4)); - GL_LockArrays(tess.numverts); GL_DrawTriangles(tess.numindices, tess.indices); @@ -84,10 +81,7 @@ void GL_DrawParticles(void) return; GL_LoadMatrix(glr.viewmatrix); - - GL_VertexPointer(3, 6, tess.vertices); - GL_TexCoordPointer(2, 6, tess.vertices + 3); - GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); + GL_BindArrays(VAO_EFFECT); bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE; @@ -266,10 +260,7 @@ void GL_DrawBeams(void) return; GL_LoadMatrix(glr.viewmatrix); - - GL_VertexPointer(3, 6, tess.vertices); - GL_TexCoordPointer(2, 6, tess.vertices + 3); - GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); + GL_BindArrays(VAO_EFFECT); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_BEAM)) @@ -335,10 +326,7 @@ void GL_DrawFlares(void) return; GL_LoadMatrix(glr.viewmatrix); - - GL_VertexPointer(3, 6, tess.vertices); - GL_TexCoordPointer(2, 6, tess.vertices + 3); - GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); + GL_BindArrays(VAO_EFFECT); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_FLARE)) @@ -421,25 +409,70 @@ void GL_DrawFlares(void) GL_FlushFlares(); } -void GL_BindArrays(void) +// Fake VAOs. This is the only place where vertex arrays are bound. +void GL_BindArrays(glVertexArray_t vao) { - if (gl_static.world.vertices) { - GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); - GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); - GL_TexCoordPointer(2, VERTEX_SIZE, tess.vertices + 4); - if (lm.nummaps) - GL_LightCoordPointer(2, VERTEX_SIZE, tess.vertices + 6); - } else { - qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); - - GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); - GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); - GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); - if (lm.nummaps) - GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); + if (gls.currentvao == vao) + return; - qglBindBuffer(GL_ARRAY_BUFFER, 0); + switch (vao) { + case VAO_SPRITE: + GL_VertexPointer(3, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 3); + break; + case VAO_EFFECT: + GL_VertexPointer(3, 6, tess.vertices); + GL_TexCoordPointer(2, 6, tess.vertices + 3); + GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); + break; + case VAO_NULLMODEL: + GL_VertexPointer(3, 4, tess.vertices); + GL_ColorBytePointer(4, 4, (GLubyte *)(tess.vertices + 3)); + break; + case VAO_OCCLUDE: + GL_VertexPointer(3, 0, tess.vertices); + break; + case VAO_WATERWARP: + GL_VertexPointer(2, 4, tess.vertices); + GL_TexCoordPointer(2, 4, tess.vertices + 2); + break; + case VAO_MESH_SHADE: + GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); + GL_ColorFloatPointer(4, VERTEX_SIZE, tess.vertices + 4); + break; + case VAO_MESH_FLAT: + GL_VertexPointer(3, 4, tess.vertices); + break; + case VAO_2D: + GL_VertexPointer(2, 5, tess.vertices); + GL_TexCoordPointer(2, 5, tess.vertices + 2); + GL_ColorBytePointer(4, 5, (GLubyte *)(tess.vertices + 4)); + break; + case VAO_3D: + if (gl_static.world.vertices) { + GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); + GL_TexCoordPointer(2, VERTEX_SIZE, tess.vertices + 4); + if (lm.nummaps) + GL_LightCoordPointer(2, VERTEX_SIZE, tess.vertices + 6); + GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); + } else { + qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); + + GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); + GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); + if (lm.nummaps) + GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); + GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); + + qglBindBuffer(GL_ARRAY_BUFFER, 0); + } + break; + default: + Q_assert(!"bad array"); } + + gls.currentvao = vao; + c.vertexArrayBinds++; } void GL_Flush3D(void) @@ -597,7 +630,7 @@ void GL_DrawAlphaFaces(void) glr.ent = NULL; - GL_BindArrays(); + GL_BindArrays(VAO_3D); for (const mface_t *face = faces_alpha; face; face = face->next) { if (glr.ent != face->entity) { diff --git a/src/refresh/world.c b/src/refresh/world.c index e21906170..90e4f0742 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -442,7 +442,7 @@ void GL_DrawBspModel(mmodel_t *model) GL_RotateForEntity(); - GL_BindArrays(); + GL_BindArrays(VAO_3D); GL_ClearSolidFaces(); @@ -593,7 +593,7 @@ void GL_DrawWorld(void) GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(); + GL_BindArrays(VAO_3D); GL_ClearSolidFaces(); From fe58141a99aca76c8086329a7daf3a3ce36dd296 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 15:56:07 +0300 Subject: [PATCH 572/974] Avoid doing occlusion queries too often. --- src/refresh/gl.h | 1 + src/refresh/main.c | 17 ++++++++++------- src/refresh/tess.c | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 6c6527c7e..c7feb43be 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -81,6 +81,7 @@ typedef struct { typedef struct { GLuint query; float frac; + unsigned timestamp; bool pending; bool visible; } glquery_t; diff --git a/src/refresh/main.c b/src/refresh/main.c index c1bd36c46..d52ec482e 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -438,19 +438,19 @@ static void GL_OccludeFlares(void) continue; // not visible } - c.occlusionQueries++; - - if (!q) { + if (q) { + if (q->pending) + continue; + if (com_eventTime - q->timestamp <= 33) + continue; + } else { glquery_t new = { 0 }; uint32_t map_size = HashMap_Size(gl_static.queries); Q_assert(map_size < MAX_EDICTS); qglGenQueries(1, &new.query); Q_assert(!HashMap_Insert(gl_static.queries, &e->skinnum, &new)); q = HashMap_GetValue(glquery_t, gl_static.queries, map_size); - } - - if (q->pending) - continue; + } if (!set) { GL_LoadMatrix(glr.viewmatrix); @@ -470,7 +470,10 @@ static void GL_OccludeFlares(void) qglEndQuery(gl_static.samples_passed); GL_UnlockArrays(); + q->timestamp = com_eventTime; q->pending = true; + + c.occlusionQueries++; } if (set) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index ddd2432f7..8c93010a6 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -336,7 +336,7 @@ void GL_DrawFlares(void) if (!q) continue; - if (q->pending) { + if (q->pending && q->timestamp != com_eventTime) { if (gl_config.caps & QGL_CAP_QUERY_RESULT_NO_WAIT) { result = -1; qglGetQueryObjectuiv(q->query, GL_QUERY_RESULT_NO_WAIT, &result); From 8e9fd218546c3059c6e7096cc372bb3177070c1c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 17:29:51 +0300 Subject: [PATCH 573/974] Allow lightmap block size to be configurable. --- doc/client.asciidoc | 6 ++++++ src/refresh/gl.h | 1 + src/refresh/main.c | 3 +++ src/refresh/surf.c | 11 +++++++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 067c2a9de..2a0a44e09 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -740,6 +740,12 @@ gl_coloredlightmaps:: lightmaps to grayscale format, any value in between reduces colorfulness. Default value is 1 (keep original colors). +gl_lightmap_bits:: + Specifies size of lightmap textures, in base 2 logarithm units. Default + value is 0 (auto, typically 8 for regular maps and 10 for re-release maps). + Allowed range is 7-10. Older hardware may need this adjusted for optimal + dynamic lighting speed. + gl_modulate:: Specifies a primary modulation factor that each pixel of world lightmaps is multiplied by. This cvar affects entity lighting as well. Default value is diff --git a/src/refresh/gl.h b/src/refresh/gl.h index c7feb43be..a5c73f999 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -226,6 +226,7 @@ extern cvar_t *gl_shadows; extern cvar_t *gl_modulate; extern cvar_t *gl_modulate_world; extern cvar_t *gl_coloredlightmaps; +extern cvar_t *gl_lightmap_bits; extern cvar_t *gl_brightness; extern cvar_t *gl_dynamic; extern cvar_t *gl_dlight_falloff; diff --git a/src/refresh/main.c b/src/refresh/main.c index d52ec482e..999902c20 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -43,6 +43,7 @@ cvar_t *gl_shadows; cvar_t *gl_modulate; cvar_t *gl_modulate_world; cvar_t *gl_coloredlightmaps; +cvar_t *gl_lightmap_bits; cvar_t *gl_brightness; cvar_t *gl_dynamic; cvar_t *gl_dlight_falloff; @@ -864,6 +865,8 @@ static void GL_Register(void) gl_modulate_world->changed = gl_lightmap_changed; gl_coloredlightmaps = Cvar_Get("gl_coloredlightmaps", "1", 0); gl_coloredlightmaps->changed = gl_lightmap_changed; + gl_lightmap_bits = Cvar_Get("gl_lightmap_bits", "0", 0); + gl_lightmap_bits->changed = gl_lightmap_changed; gl_brightness = Cvar_Get("gl_brightness", "0", 0); gl_brightness->changed = gl_lightmap_changed; gl_dynamic = Cvar_Get("gl_dynamic", "1", 0); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 4f5221e06..23d8935df 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -409,7 +409,13 @@ static void LM_BeginBuilding(void) return; // use larger lightmaps for DECOUPLED_LM maps - bits = min(8 + bsp->lm_decoupled * 2, gl_config.max_texture_size_log2); + if (gl_lightmap_bits->integer) + bits = Cvar_ClampInteger(gl_lightmap_bits, 7, 10); + else + bits = 8 + bsp->lm_decoupled * 2; + + // clamp to maximum texture size + bits = min(bits, gl_config.max_texture_size_log2); lm.block_size = 1 << bits; lm.block_shift = bits + 2; @@ -909,6 +915,7 @@ static void upload_world_surfaces(void) gl_fullbright->modified = false; gl_vertexlight->modified = false; + gl_lightmap_bits->modified = false; } static void set_world_size(const mnode_t *node) @@ -943,7 +950,7 @@ void GL_RebuildLighting(void) return; // rebuild all surfaces if doing vertex lighting (and not fullbright) - if (gl_vertexlight->integer) { + if (gl_vertexlight->integer || gl_lightmap_bits->modified) { upload_world_surfaces(); return; } From 41a882d100a6a8c61f00b245b27beeedf076960b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 19:04:13 +0300 Subject: [PATCH 574/974] Init newly allocated lightmap block to black. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not strictly needed, but makes ‘gl_lightmap 2’ look nicer. --- src/refresh/gl.h | 2 +- src/refresh/surf.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index a5c73f999..7a44d6b3f 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -467,7 +467,7 @@ typedef struct { bool dirty; int comp, block_size, block_shift; float add, modulate, scale; - int nummaps, maxmaps; + int nummaps, maxmaps, block_bytes; uint16_t inuse[LM_MAX_BLOCK_WIDTH]; GLuint texnums[LM_MAX_LIGHTMAPS]; lightmap_t lightmaps[LM_MAX_LIGHTMAPS]; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 23d8935df..df1a5b7c2 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -344,6 +344,7 @@ LIGHTMAPS BUILDING static void LM_InitBlock(void) { memset(lm.inuse, 0, sizeof(lm.inuse)); + memset(lm.lightmaps[lm.nummaps].buffer, 0, lm.block_bytes); } static void LM_UploadBlock(void) @@ -421,6 +422,7 @@ static void LM_BeginBuilding(void) lm.block_shift = bits + 2; size_shift = bits * 2 + 2; + lm.block_bytes = 1 << size_shift; lm.maxmaps = min(sizeof(lm.buffer) >> size_shift, LM_MAX_LIGHTMAPS); for (int i = 0; i < lm.maxmaps; i++) @@ -439,7 +441,6 @@ static void LM_EndBuilding(void) // upload the last lightmap LM_UploadBlock(); - LM_InitBlock(); // now build the real lightstyle map build_style_map(gl_dynamic->integer); From 7047302a3e0f2b5874f825645091e3b256809c39 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 18 Aug 2024 19:36:29 +0300 Subject: [PATCH 575/974] Make lightmap buffer a global variable. --- src/refresh/gl.h | 1 - src/refresh/surf.c | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 7a44d6b3f..555f064d0 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -471,7 +471,6 @@ typedef struct { uint16_t inuse[LM_MAX_BLOCK_WIDTH]; GLuint texnums[LM_MAX_LIGHTMAPS]; lightmap_t lightmaps[LM_MAX_LIGHTMAPS]; - byte buffer[0x4000000]; } lightmap_builder_t; extern lightmap_builder_t lm; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index df1a5b7c2..6ea065bbc 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/mdfour.h" lightmap_builder_t lm; +static byte lm_buffer[0x4000000]; /* ============================================================================= @@ -423,10 +424,10 @@ static void LM_BeginBuilding(void) size_shift = bits * 2 + 2; lm.block_bytes = 1 << size_shift; - lm.maxmaps = min(sizeof(lm.buffer) >> size_shift, LM_MAX_LIGHTMAPS); + lm.maxmaps = min(sizeof(lm_buffer) >> size_shift, LM_MAX_LIGHTMAPS); for (int i = 0; i < lm.maxmaps; i++) - lm.lightmaps[i].buffer = lm.buffer + (i << size_shift); + lm.lightmaps[i].buffer = lm_buffer + (i << size_shift); Com_DPrintf("%s: %d lightmaps max, %d block size\n", __func__, lm.maxmaps, lm.block_size); From d255bf7ee2ea3bf355a81e7477685e5bea6a4ea5 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 18 Aug 2024 17:39:11 -0400 Subject: [PATCH 576/974] Some espionage stuff coming along but still bad bugs --- meson.build | 1 + src/action/a_esp.c | 9 +- src/action/a_esp.h | 5 +- src/action/a_team.c | 55 ++- src/action/a_team.h | 4 + src/action/botlib/botlib.h | 10 + src/action/botlib/botlib_ai.c | 9 +- src/action/botlib/botlib_esp.c | 651 +++++++++------------------------ 8 files changed, 259 insertions(+), 485 deletions(-) diff --git a/meson.build b/meson.build index 730d7c16c..24de5da44 100644 --- a/meson.build +++ b/meson.build @@ -236,6 +236,7 @@ botlib_src = [ 'src/action/botlib/botlib_cmd.c', 'src/action/botlib/botlib_communication.c', 'src/action/botlib/botlib_ctf.c', + 'src/action/botlib/botlib_esp.c', 'src/action/botlib/botlib_items.c', 'src/action/botlib/botlib_math.c', 'src/action/botlib/botlib_movement.c', diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 386bc8db3..8b9465ecf 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1136,7 +1136,7 @@ randomly choose a spawnpoint for each team, based on the es->custom_spawns[index This is reset back to default in EspEndOfRoundCleanup() */ -edict_t *SelectEspCustomSpawnPoint(edict_t * ent) +edict_t *SelectEspCustomSpawnPoint(edict_t* ent) { espsettings_t *es = &espsettings; int teamNum = ent->client->resp.team; @@ -1174,9 +1174,10 @@ edict_t *SelectEspCustomSpawnPoint(edict_t * ent) // gi.dprintf("For team %d, random index is %d, but the spawn point is NULL\n", teamNum, esp_spawnpoint_index[teamNum]); // } - if (spawn_point) + if (spawn_point) { + espsettings.round_spawnpoint[teamNum] = spawn_point; return spawn_point; - else { + } else { gi.dprintf("%s: No spawnpoint found, safely return NULL so we can try another one\n", __func__); return NULL; } @@ -2045,8 +2046,10 @@ void EspEndOfRoundCleanup(void) // Reset the last spawnpoint index, this is reset to // -1 because 0 is a valid index + // Also reset last round's spawnpoint edict for (i = TEAM1; i <= teamCount; i++) { esp_spawnpoint_index[i] = -1; + espsettings.round_spawnpoint[i] = NULL; } // Reset these three timed stats for the next round for each player diff --git a/src/action/a_esp.h b/src/action/a_esp.h index ef3a8731d..c671e15e3 100644 --- a/src/action/a_esp.h +++ b/src/action/a_esp.h @@ -59,6 +59,8 @@ typedef struct espsettings_s char target_name[MAX_ESP_STRLEN]; edict_t *capturepoint; edict_t *lastkilledleader; + qboolean esp_live_round; + edict_t *round_spawnpoint[TEAM_TOP]; } espsettings_t; extern espsettings_t espsettings; @@ -129,4 +131,5 @@ void EspEndOfRoundCleanup(void); void EspRespawnLCA(edict_t *ent); void EspCleanUp(void); void EspDebug(void); -extern qboolean esp_punishment_phase; \ No newline at end of file +extern qboolean esp_punishment_phase; +extern int esp_spawnpoint_index[TEAM_TOP]; diff --git a/src/action/a_team.c b/src/action/a_team.c index fb2d2f4e2..194602d1d 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2363,9 +2363,11 @@ static void StartLCA(void) if ((gameSettings & (GS_WEAPONCHOOSE|GS_ROUNDBASED))) CleanLevel(); - if (esp->value) + if (esp->value) { // Re-skin everyone to ensure only one leader skin EspSkinCheck(); + espsettings.esp_live_round = true; + } if (use_tourney->value && !tourney_lca->value) { @@ -3053,6 +3055,7 @@ int CheckTeamRules (void) if (EspCheckRules()){ EndDMLevel(); team_round_going = team_round_countdown = team_game_going = 0; + espsettings.esp_live_round = false; return 1; } GenerateMedKit(false); @@ -4227,4 +4230,54 @@ int OtherTeam(int teamNum) // Returns zero if teamNum is not 1 or 2 return 0; +} + +/* +Return the total amount of players on a team +*/ + +int TotalPlayersOnTeam(int teamNum) +{ + int players[TEAM_TOP] = { 0 }; + int i = 0; + edict_t *ent; + + for (i = 0; i < game.maxclients; i++){ + ent = &g_edicts[1 + i]; + if (!ent->inuse || ent->solid == SOLID_NOT) + continue; + + int currentTeam = game.clients[i].resp.team; + if (currentTeam == NOTEAM) + continue; + + players[currentTeam]++; + } + + return players[teamNum]; +} + +/* +Return the total players on a team that are alive +*/ + +int TotalPlayersAliveOnTeam(int teamNum) +{ + int count = 0; + edict_t *ent; + + for (int i = 0; i < game.maxclients; i++) + { + ent = &g_edicts[i + 1]; + if (!ent->inuse) + continue; + if (game.clients[i].resp.subteam) + continue; + if (game.clients[i].resp.team == teamNum && IS_ALIVE(ent)) + { + count++; + } + } + + return count; } \ No newline at end of file diff --git a/src/action/a_team.h b/src/action/a_team.h index 909633282..f5a2e8305 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -107,6 +107,10 @@ void OpenPMItemMenu (edict_t * ent); //Expose auto-join functionality void JoinTeamAutobalance (edict_t * ent); +//Utility functions +int TotalPlayersOnTeam(int teamNum); +int TotalPlayersAliveOnTeam(int teamNum); + typedef struct spawn_distances_s { float distance; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 460cf43ee..66bb849a7 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -151,6 +151,15 @@ int BOTLIB_NearestFlag(edict_t* self); float BOTLIB_DistanceToFlag(edict_t* self, int flagType); void BOTLIB_CTF_Goals(edict_t* self); +// =========================================================================== +// botlib_esp.c +// =========================================================================== +int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader); +float BOTLIB_DistanceToLeader(edict_t* self, edict_t* leader); +int BOTLIB_FindMyLeaderNode(edict_t* self); +int BOTLIB_FindEnemyLeaderNode(edict_t* self, int teamNum); +void BOTLIB_ESP_Goals(edict_t* self); + typedef struct ctf_status_s { edict_t* flag1; // Red flag @@ -198,6 +207,7 @@ typedef enum BOT_ESP_ATTACK_TARGET, // Attack the target (enemy leader or target area) BOT_ESP_DEFEND_TARGET, // Defend the target (team leader or target area) BOT_ESP_RETREAT, // Retreat from the target (enemy leader or target area) + BOT_ESP_WANDER, // Wander around, find enemies, equipment, etc } bot_esp_state_t; // Get flag, retrieve flag, intercept flag carrier, etc. diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index b9638f05a..fdff48d85 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -721,10 +721,11 @@ void BOTLIB_Think(edict_t* self) { BOTLIB_CTF_Goals(self); } - else if (esp->value) // ESP Goals - { - BOTLIB_ESP_Goals(self); - } + // else if (esp->value) // ESP Goals + // { + // // Do nothing until this is fixed + // BOTLIB_ESP_Goals(self); + // } // Check if the bot is in a NAV state (I need a nav) or if NONE else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index b1b365b18..368b8c65f 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -369,522 +369,221 @@ float BOTLIB_DistanceToEnemyLeader(edict_t* self, int flagType) } -void BOTLIB_CTF_Goals(edict_t* self) +void BOTLIB_ESP_Goals(edict_t* self) { - if (team_round_going == false || lights_camera_action) return; // Only allow during a real match (after LCA and before win/loss announcement) + if (!lights_camera_action && !espsettings.esp_live_round) return; // Only allow during a real match (after LCA and before win/loss announcement) - // Get flag - if (self->bot.bot_ctf_state != BOT_CTF_STATE_GET_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 == NULL && bot_ctf_status.flag2_is_home) - { - int n = bot_ctf_status.flag2_home_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s heading for enemy blue flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin)); - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 == NULL && bot_ctf_status.flag2_is_home) - { - int n = bot_ctf_status.flag1_home_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s heading for enemy red flag at node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin)); - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - // Take flag home, if home flag is home - if (BOTLIB_Carrying_Flag(self)) - { - if (self->bot.bot_ctf_state != BOT_CTF_STATE_CAPTURE_ENEMY_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_ATTACK_ENEMY_CARRIER && - self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG && - self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (self->client->resp.team == TEAM1 && VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin) > 128) - n = bot_ctf_status.flag1_home_node; - else if (self->client->resp.team == TEAM2 && VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin) > 128) - n = bot_ctf_status.flag2_home_node; - - // If team flag is dropped and nearby, don't head to home. Allow bot to pickup the dropped team flag. - if (self->client->resp.team == TEAM1 && bot_ctf_status.flag1_is_dropped && VectorDistance(bot_ctf_status.flag1->s.origin, self->s.origin) < 768) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, red flag was dropped nearby\n", __func__, self->client->pers.netname); - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.flag2_is_dropped && VectorDistance(bot_ctf_status.flag2->s.origin, self->s.origin) < 768) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, blue flag was dropped nearby\n", __func__, self->client->pers.netname); - } - - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && bot_connections.total_team1 <= 1) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && bot_connections.total_team2 <= 1) - { - n = INVALID; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - //Com_Printf("%s %s goal_node[%d] flag1_home_node[%i] state[%d]\n", __func__, self->client->pers.netname, self->bot.goal_node, bot_ctf_status.flag1_home_node, self->state); - //if (n != INVALID) Com_Printf("%s %s self->bot.goal_node %i flag node %i\n", __func__, self->client->pers.netname, self->bot.goal_node, n); + espsettings_t *es = &espsettings; + // Team related variables + int myTeam = self->client->resp.team; + int totalTeammates = TotalPlayersOnTeam(myTeam); + int aliveTeammates = TotalPlayersAliveOnTeam(myTeam); + float percentAlive = (float)aliveTeammates / (float)totalTeammates * 100; + + // Keep track of current round's spawnpoint if we need to flee + //edict_t* roundSpawnPoint = espsettings.round_spawnpoint[myTeam]; + edict_t *roundSpawnPoint = es->custom_spawns[myTeam][esp_spawnpoint_index[myTeam]]; + int closestSpawnPointNode = ACEND_FindClosestReachableNode(roundSpawnPoint, NODE_DENSITY, NODE_ALL); + + // Logic time: Discerning between leader vs non-leader + // TODO: Better logic later on when we utilize bot.skill! + if (IS_LEADER(self)) + goto bot_leader_think; + else + goto bot_crew_think; - if (BOTLIB_CanGotoNode(self, n, false)) - { - if (self->client->resp.team == TEAM1) - { - //Com_Printf("%s %s Taking blue flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag1_home_node].origin, self->s.origin)); - } - else - { - //Com_Printf("%s %s Taking red flag home to node %i dist[%f]\n", __func__, self->client->pers.netname, n, VectorDistance(nodes[bot_ctf_status.flag2_home_node].origin, self->s.origin)); +// Logic splits here, between ATL and ETV modes +bot_leader_think: + if (EspModeCheck() == ESPMODE_ATL) { + // If percentage of teammates alive is < 50% then : + if (percentAlive < 50) { + // 50% chance to go for the enemy leader + if (rand() % 2 == 0) { + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + if (enemyNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); + return; } - self->bot.bot_ctf_state = BOT_CTF_STATE_CAPTURE_ENEMY_FLAG; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); - return; - } - } - } - - // Bot was on its way to the enemy flag, but got picked up by a team member before the bot could reach it. Therefore, try to support the flag carrrier who got it. - if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG && BOTLIB_Carrying_Flag(self) == false) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2) - { - //Com_Printf("%s %s Red flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); - - // Support ally if they're close - if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin) < 1024) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - self->bot.ctf_support_time = level.framenum + 10.0 * HZ; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else { + // 50% chance to flee (run back to original spawnpoint) + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_RETREAT; + BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); return; } } - - //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - //self->bot.state = BOT_MOVE_STATE_NAV; - } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1) - { - //Com_Printf("%s %s Blue flag was picked first up by another player [%s]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); - - // Support ally if they're close - if (VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin) < 1024) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - self->bot.ctf_support_time = level.framenum + 10.0 * HZ; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else { + // 33% chance to go for the enemy leader + if (rand() % 3 == 0) { + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + if (enemyNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); return; } - } - - //self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - //self->bot.state = BOT_MOVE_STATE_NAV; - } - } - - // Continue supporting flag carrier - if (self->bot.bot_ctf_state != BOT_CTF_STATE_COVER_FLAG_CARRIER && BOTLIB_Carrying_Flag(self) == false) - { - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag2 && self->bot.ctf_support_time < level.framenum) - { - self->bot.ctf_support_time = level.framenum + 5.0 * HZ; - - //if (bot_ctf_status.team1_carrier_dist_to_home > 512) - // Com_Printf("%s %s continue supporting blue flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, bot_ctf_status.team1_carrier_dist_to_home); - - - // Support flag carrier if they're away from the home flag - float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); - if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s continue supporting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else if (rand() % 3 == 1) { + // 33% chance to flee + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_RETREAT; + BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); return; } - } - } - - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag1 && self->bot.ctf_support_time < level.framenum) - { - self->bot.ctf_support_time = level.framenum + 5.0 * HZ; - - //if (bot_ctf_status.team2_carrier_dist_to_home > 512) - // Com_Printf("%s %s continue supporting red flag carrier %s dist from home [%f]\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, bot_ctf_status.team2_carrier_dist_to_home); - - // Support flag carrier if they're around - float dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); - if (dist > 256 && dist < 4024 && bot_ctf_status.team1_carrier_dist_to_home > 512) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; - if (BOTLIB_CanGotoNode(self, n, false)) - { - //Com_Printf("%s %s continue supporting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_COVER_FLAG_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else if (rand() % 3 == 2) { + // 33% chance to wander + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); return; } } } - } - - - - - // Retrieve dropped T1/T2 flags if nearby - if (self->client->resp.team == TEAM1) - { - int flag_to_get = 0; // Flag that is dropped - - // If both team and ememy flag is dropped, go for closest flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) - { - // If team flag is closer - if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)) - { - flag_to_get = FLAG_T1_NUM; // Dropped team flag - //Com_Printf("%s %s [team] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - } - else - { - flag_to_get = FLAG_T2_NUM; // Dropped enemy flag - //Com_Printf("%s %s [enemy] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - } - } - else if (bot_ctf_status.flag1_is_dropped) // Dropped team flag - flag_to_get = FLAG_T1_NUM; - else if (bot_ctf_status.flag2_is_dropped) // Dropped enemy flag - flag_to_get = FLAG_T2_NUM; - - if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (flag_to_get == FLAG_T1_NUM) - n = bot_ctf_status.flag1_curr_node; - if (flag_to_get == FLAG_T2_NUM) - n = bot_ctf_status.flag2_curr_node; - - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) - { - if (flag_to_get == FLAG_T1_NUM) // Dropped team flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; - //Com_Printf("%s %s retrieving [team] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } else if (EspModeCheck() == ESPMODE_ETV && myTeam == TEAM1) { + // If percentage of teammates alive is < 50% then : + if (percentAlive < 50) { + // 50% chance to go for the ETV target + if (rand() % 2 == 0) { + int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); + if (targetNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); + return; } - if (flag_to_get == FLAG_T2_NUM) // Dropped enemy flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; - //Com_Printf("%s %s retrieving [enemy] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); + } else { + // 50% chance to flee + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_RETREAT; + BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); + return; } - - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, nodes[n].nodenum); - return; - } - } - } - if (self->client->resp.team == TEAM2) - { - int flag_to_get = 0; // Flag that is dropped - - // If both team and ememy flag is dropped, go for closest flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag2_is_dropped) - { - // If team flag is closer - if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) - { - flag_to_get = FLAG_T2_NUM; // Dropped team flag - Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); } - else - { - flag_to_get = FLAG_T1_NUM; // Dropped enemy flag - Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - } - } - else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag - flag_to_get = FLAG_T2_NUM; - else if (bot_ctf_status.flag1_is_dropped) // Dropped enemy flag - flag_to_get = FLAG_T1_NUM; - - if (flag_to_get && self->bot.bot_ctf_state != BOT_CTF_STATE_RETURN_TEAM_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG - && self->bot.bot_ctf_state != BOT_CTF_STATE_FORCE_MOVE_TO_FLAG) - { - int n = INVALID; - if (flag_to_get == FLAG_T2_NUM) - n = bot_ctf_status.flag2_curr_node; - if (flag_to_get == FLAG_T1_NUM) - n = bot_ctf_status.flag1_curr_node; - - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) - { - if (flag_to_get == FLAG_T2_NUM) // Dropped team flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; - //Com_Printf("%s %s retrieving [team] dropped blue flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag2->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - if (flag_to_get == FLAG_T1_NUM) // Dropped enemy flag - { - self->bot.bot_ctf_state = BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG; - //Com_Printf("%s %s retrieving [enemy] dropped red flag %s at node %d dist[%f]\n", __func__, self->client->pers.netname, bot_ctf_status.flag1->classname, nodes[n].nodenum, VectorDistance(self->s.origin, nodes[n].origin)); - } - - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, nodes[n].nodenum); + } else { + // We shouldn't get here but in case we do, do something + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); return; } } - } - - - // When the bot is close to the flag from following nodes, flags are not always on a node exactly, - // therefore when the bot gets close enough to the flag, force move the bot toward the flag. - if (self->bot.goal_node == INVALID) - { - - //if (self->bot.bot_ctf_state == BOT_CTF_STATE_GET_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_CAPTURE_ENEMY_FLAG || self->bot.bot_ctf_state == BOT_CTF_STATE_RETURN_TEAM_FLAG) - { - // TEAM 1 - Walk to home flag (Capture) - if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag1_is_home - && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 2 - Walk to home flag (Capture) - if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) && bot_ctf_status.flag2_is_home - && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 1 - Walk to enemy flag (Take) - if (self->client->resp.team == TEAM1 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag2 == false && bot_ctf_status.flag2_is_home - && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward enemy home blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; - return; - } - // TEAM 2 - Walk to enemy flag (Take) - if (self->client->resp.team == TEAM2 && BOTLIB_Carrying_Flag(self) == false && bot_ctf_status.player_has_flag1 == false && bot_ctf_status.flag1_is_home - && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward enemy home red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + } else if (EspModeCheck() == ESPMODE_ETV && myTeam == TEAM2) { + // Defend target + if (rand() % 2 == 0) { + int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); + if (targetNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_DEFEND_TARGET; + BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); return; } - // Walk to dropped RED flag - if (bot_ctf_status.flag1_is_dropped && bot_ctf_status.flag1 && VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward dropped red flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag1->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + } else if (rand() % 2 == 1) { + // Attack team1's leader + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + if (enemyNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); return; } - // Walk to dropped BLUE flag - if (bot_ctf_status.flag2_is_dropped && bot_ctf_status.flag2 && VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < 256) - { - //Com_Printf("%s %s is being forced toward dropped blue flag [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); - vec3_t walkdir; - VectorSubtract(bot_ctf_status.flag2->s.origin, self->s.origin, walkdir); // Head to flag - VectorNormalize(walkdir); - vec3_t hordir; //horizontal direction - hordir[0] = walkdir[0]; - hordir[1] = walkdir[1]; - hordir[2] = 0; - VectorNormalize(hordir); - VectorCopy(hordir, self->bot.bi.dir); - self->bot.bi.speed = 400; - self->bot.bot_ctf_state = BOT_CTF_STATE_FORCE_MOVE_TO_FLAG; + } else { + // We shouldn't get here but in case we do, do something + if (roundSpawnPoint) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); return; } } - // Check if bot is closer to enemy flag carrier or the enemy flag at home - int flag_to_get = 0; - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1) - { - if (bot_ctf_status.flag2 && bot_ctf_status.flag2_is_home) // If enemy flag is home - { - // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier - if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin)) - { - flag_to_get = FLAG_T2_NUM; // Go after enemy flag - //Com_Printf("%s %s closer to blue flag than blue enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname); +// Main difference between crew and leader is that the crew are +// expendable, protect the leader at all costs! +bot_crew_think: + if (EspModeCheck() == ESPMODE_ATL) { + // If percentage of teammates alive is < 50% then : + if (percentAlive < 50) { + // 50% chance to stay with team leader + if (rand() % 2 == 0) { + int leaderNode = BOTLIB_FindMyLeaderNode(self); + if (leaderNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; + BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); + return; } - else - flag_to_get = FLAG_T1_NUM; // Go after enemy carrier - } - else - flag_to_get = FLAG_T1_NUM; // If both flags gone, go after enemy carrier - } - else if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2) - { - if (bot_ctf_status.flag1 && bot_ctf_status.flag1_is_home) // If enemy flag is home - { - // If we're closer to the enemy flag, go after that instead of intercepting enemy flag carrier - if (VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin)) - { - flag_to_get = FLAG_T1_NUM; // Go after enemy flag - //Com_Printf("%s %s closer to red flag than red enemy flag carrier %s\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname); + } else { + // 50% chance to attack enemy leader + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + if (enemyNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); + return; } - else - flag_to_get = FLAG_T2_NUM; // Go after enemy carrier } - else - flag_to_get = FLAG_T2_NUM; // If both flags gone, go after enemy carrier - } - - // Intercept enemy flag carrier if we're closer to them than the enemy flag - if (self->client->resp.team == TEAM1 && bot_ctf_status.player_has_flag1 && flag_to_get == FLAG_T1_NUM) - { - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - qboolean force_incercept = false; - if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team1 <= 1) - { - force_incercept = true; - //Com_Printf("%s %s abort capturing flag, other team has our red flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - self->bot.state = BOT_MOVE_STATE_MOVE; - - float dist = 999999999; - dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag1->s.origin); - if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) - { - int n = bot_ctf_status.player_has_flag1->bot.current_node; //bot_ctf_status.flag2_curr_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s intercepting blue flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag1->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else { + // 33% chance to stay with team leader + if (rand() % 3 == 0) { + int leaderNode = BOTLIB_FindMyLeaderNode(self); + if (leaderNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; + BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); + return; + } + } else if (rand() % 3 == 1) { + // 33% chance to attack enemy leader + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + if (enemyNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); return; } + } else if (rand() % 3 == 2) { + // 33% chance to wander + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); + return; } } - if (self->client->resp.team == TEAM2 && bot_ctf_status.player_has_flag2 && flag_to_get == FLAG_T2_NUM) - { - // If both teams have the flag, and there's no other players/bots to go attack the flag carrier - qboolean force_incercept = false; - if (BOTLIB_Carrying_Flag(self) && bot_connections.total_team2 <= 1) - { - force_incercept = true; - //Com_Printf("%s %s abort capturing flag, other team has our blue flag and there's no support to go get it back\n", __func__, self->client->pers.netname); - } - - self->bot.state = BOT_MOVE_STATE_MOVE; - - float dist = 999999999; - dist = VectorDistance(self->s.origin, bot_ctf_status.player_has_flag2->s.origin); - if (dist > 256 && (BOTLIB_Carrying_Flag(self) == false || force_incercept)) - { - int n = bot_ctf_status.player_has_flag2->bot.current_node; //bot_ctf_status.flag1_curr_node; - if (BOTLIB_CanGotoNode(self, n, false)) // Make sure we can visit the node they're at - { - //Com_Printf("%s %s intercepting red flag carrier %s at node %i\n", __func__, self->client->pers.netname, bot_ctf_status.player_has_flag2->client->pers.netname, n); - self->bot.bot_ctf_state = BOT_CTF_STATE_ATTACK_ENEMY_CARRIER; - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, n); + } else if (EspModeCheck() == ESPMODE_ETV) { + // If percentage of teammates alive is < 50% then : + if (percentAlive < 50) { + // 50% chance to go for the ETV target + if (rand() % 2 == 0) { + int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); + if (targetNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); + return; + } + } else { + // 50% chance to stay with team leader + int leaderNode = BOTLIB_FindMyLeaderNode(self); + if (leaderNode != INVALID) { + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; + BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); return; } } + } else { + // We shouldn't get here but in case we do, do something + self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); + return; } - - self->bot.bot_ctf_state = BOT_CTF_STATE_NONE; - self->bot.ctf_support_time = 0; - + } } /* From 8874951fd90675401192563369256f06d8200c9f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 19 Aug 2024 10:50:26 -0400 Subject: [PATCH 577/974] Renamed bot_teamplay to bot_countashuman, timelimit works in bot-only DM games now --- src/action/a_vote.c | 4 ++-- src/action/acesrc/acebot.h | 5 +---- src/action/acesrc/bot_collision.c | 11 ++++++++--- src/action/g_main.c | 3 ++- src/action/g_save.c | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/action/a_vote.c b/src/action/a_vote.c index ea58f348c..163b2e5f5 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -114,8 +114,8 @@ int _numclients (void) if (!other->inuse || !other->client || !other->client->pers.connected || other->client->pers.mvdspec) continue; #ifndef NO_BOTS - // If bot_teamplay is enabled, then do not continue/ignore bots - if(!bot_teamplay->value) { + // If bot_countashuman is enabled, then do not continue/ignore bots + if(!bot_countashuman->value) { if (other->is_bot) continue; } diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index acace1d59..e3fc29b92 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -545,13 +545,10 @@ extern cvar_t* bot_randname; extern cvar_t* bot_chat; extern cvar_t* bot_personality; extern cvar_t* bot_ragequit; -extern cvar_t* bot_teamplay; +extern cvar_t* bot_countashuman; extern cvar_t* bot_debug; //extern cvar_t* bot_randteamskin; - - - #define MAX_BOT_NAMES 64 typedef struct { char name[16]; // Name diff --git a/src/action/acesrc/bot_collision.c b/src/action/acesrc/bot_collision.c index 67aa3ada9..a32fe6cfc 100644 --- a/src/action/acesrc/bot_collision.c +++ b/src/action/acesrc/bot_collision.c @@ -235,7 +235,7 @@ qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles) { vec3_t dir, angle, dest1, dest2; trace_t trace; - //float this_dist; + float this_dist; VectorClear(angle); angle[1] = angles[1]; @@ -268,9 +268,14 @@ qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles) //BOTUT_TempLaser (trace.endpos, dest2); trace = gi.trace(trace.endpos, VEC_ORIGIN, VEC_ORIGIN, dest2, self, MASK_PLAYERSOLID | MASK_DEADLY); + // Calculate the distance between the bot's current position and the destination position + this_dist = sqrt(pow(dest2[0] - self->s.origin[0], 2) + pow(dest2[1] - self->s.origin[1], 2) + pow(dest2[2] - self->s.origin[2], 2)); + + gi.dprintf("Distance to destination: %f\n", this_dist); if( (trace.fraction == 1.0) // long drop! - || (trace.contents & MASK_DEADLY) ) // avoid SLIME or LAVA - { + || (trace.contents & MASK_DEADLY) // avoid SLIME or LAVA + || (this_dist > (NODE_MAX_FALL_HEIGHT * 3)) ) // avoid falling too far + { return (false); } else diff --git a/src/action/g_main.c b/src/action/g_main.c index 3cc610977..022bed5de 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -496,7 +496,7 @@ cvar_t* bot_randname; // Allow bots to pick a random name cvar_t* bot_chat; // Enable generic bot chat cvar_t* bot_personality; // Enable bot personality functionality [0 disable, 1 enable, 2 mixed with random bots] cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) -cvar_t* bot_teamplay; // Allows bots to play teamplay games without humans (see a_vote.c _numclients() ) +cvar_t* bot_countashuman; // Allows bots to play teamplay games without humans (see a_vote.c _numclients() ) also enforces timelimit on DM games cvar_t* bot_debug; // Enable bot debug mode //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e @@ -1235,6 +1235,7 @@ void G_RunFrame (void) // If the server is empty, don't wait at intermission. empty = ! _numclients(); + if( level.intermission_framenum && empty ) level.intermission_exit = 1; diff --git a/src/action/g_save.c b/src/action/g_save.c index 84b59c72b..efd0ad16f 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -698,7 +698,7 @@ void InitGame( void ) bot_chat = gi.cvar("bot_chat", "0", 0); bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); bot_ragequit = gi.cvar("bot_ragequit", "0", 0); - bot_teamplay = gi.cvar("bot_teamplay", "0", 0); + bot_countashuman = gi.cvar("bot_countashuman", "0", 0); bot_debug = gi.cvar("bot_debug", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e From abf9e3dfdc2afabe03da08ac501dc42f69d12a52 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 02:53:01 +0300 Subject: [PATCH 578/974] Generalize vertex array bindings more. --- src/refresh/gl.h | 79 +++++++++++++-------------- src/refresh/legacy.c | 46 ++++++++-------- src/refresh/main.c | 8 +-- src/refresh/mesh.c | 4 +- src/refresh/shader.c | 45 +++++----------- src/refresh/sky.c | 2 +- src/refresh/surf.c | 4 +- src/refresh/tess.c | 125 +++++++++++++++++++++---------------------- src/refresh/world.c | 4 +- 9 files changed, 148 insertions(+), 169 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 555f064d0..9b492d234 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -55,6 +55,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_PROGRAMS 128 #define NUM_TEXNUMS 7 +typedef struct { + uint8_t size; + bool type; + uint8_t stride; + uint8_t offset; +} glVaDesc_t; + typedef struct { const char *name; @@ -70,11 +77,9 @@ typedef struct { void (*state_bits)(GLbitfield bits); void (*array_bits)(GLbitfield bits); - void (*vertex_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*tex_coord_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*light_coord_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*color_byte_pointer)(GLint size, GLsizei stride, const GLubyte *pointer); - void (*color_float_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); + void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); + void (*tex_coord_pointer)(const GLfloat *ptr); + void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); } glbackend_t; @@ -87,19 +92,19 @@ typedef struct { } glquery_t; typedef enum { - VAO_NONE, - - VAO_SPRITE, - VAO_EFFECT, - VAO_NULLMODEL, - VAO_OCCLUDE, - VAO_WATERWARP, - VAO_MESH_SHADE, - VAO_MESH_FLAT, - VAO_2D, - VAO_3D, - - VAO_TOTAL + VA_NONE, + + VA_SPRITE, + VA_EFFECT, + VA_NULLMODEL, + VA_OCCLUDE, + VA_WATERWARP, + VA_MESH_SHADE, + VA_MESH_FLAT, + VA_2D, + VA_3D, + + VA_TOTAL } glVertexArray_t; typedef struct { @@ -515,12 +520,21 @@ typedef enum { GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; +typedef enum { + VERT_ATTR_POS, + VERT_ATTR_TC, + VERT_ATTR_LMTC, + VERT_ATTR_COLOR, + + VERT_ATTR_COUNT +} glVertexAttr_t; + typedef enum { GLA_NONE = 0, - GLA_VERTEX = BIT(0), - GLA_TC = BIT(1), - GLA_LMTC = BIT(2), - GLA_COLOR = BIT(3), + GLA_VERTEX = BIT(VERT_ATTR_POS), + GLA_TC = BIT(VERT_ATTR_TC), + GLA_LMTC = BIT(VERT_ATTR_LMTC), + GLA_COLOR = BIT(VERT_ATTR_COLOR), } glArrayBits_t; typedef struct { @@ -529,7 +543,7 @@ typedef struct { GLuint texnums[MAX_TMUS]; GLbitfield state_bits; GLbitfield array_bits; - glVertexArray_t currentvao; + glVertexArray_t currentva; const GLfloat *currentmatrix; struct { GLfloat view[16]; @@ -624,23 +638,6 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) qglDepthRange(n, f); } -#define VBO_OFS(n) ((void *)(sizeof(GLfloat) * (n))) - -#define GL_VertexPointer(size, stride, ptr) \ - gl_backend->vertex_pointer((size), (stride) * sizeof(GLfloat), (ptr)) - -#define GL_TexCoordPointer(size, stride, ptr) \ - gl_backend->tex_coord_pointer((size), (stride) * sizeof(GLfloat), (ptr)) - -#define GL_LightCoordPointer(size, stride, ptr) \ - gl_backend->light_coord_pointer((size), (stride) * sizeof(GLfloat), (ptr)) - -#define GL_ColorBytePointer(size, stride, ptr) \ - gl_backend->color_byte_pointer((size), (stride) * sizeof(GLfloat), (ptr)) - -#define GL_ColorFloatPointer(size, stride, ptr) \ - gl_backend->color_float_pointer((size), (stride) * sizeof(GLfloat), (ptr)) - #define GL_Color(r, g, b, a) gl_backend->color(r, g, b, a) #define GL_DrawTriangles(num_indices, indices) \ @@ -739,7 +736,7 @@ void GL_DrawParticles(void); void GL_DrawBeams(void); void GL_DrawFlares(void); -void GL_BindArrays(glVertexArray_t vao); +void GL_BindArrays(glVertexArray_t va); void GL_Flush3D(void); void GL_AddAlphaFace(mface_t *face, entity_t *ent); diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index 22190e706..1d0e6a4ec 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -116,31 +116,35 @@ static void legacy_array_bits(GLbitfield bits) } } -static void legacy_vertex_pointer(GLint size, GLsizei stride, const GLfloat *pointer) +static void legacy_array_pointers(const glVaDesc_t *desc, const GLfloat *ptr) { - qglVertexPointer(size, GL_FLOAT, stride, pointer); -} + uintptr_t base = (uintptr_t)ptr; -static void legacy_tex_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) -{ - GL_ClientActiveTexture(0); - qglTexCoordPointer(size, GL_FLOAT, stride, pointer); -} + qglVertexPointer(desc->size, GL_FLOAT, desc->stride, (void *)(base + desc->offset)); + desc++; -static void legacy_light_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) -{ - GL_ClientActiveTexture(1); - qglTexCoordPointer(size, GL_FLOAT, stride, pointer); -} + if (desc->size) { + GL_ClientActiveTexture(0); + qglTexCoordPointer(desc->size, GL_FLOAT, desc->stride, (void *)(base + desc->offset)); + } + desc++; -static void legacy_color_byte_pointer(GLint size, GLsizei stride, const GLubyte *pointer) -{ - qglColorPointer(size, GL_UNSIGNED_BYTE, stride, pointer); + if (desc->size && lm.nummaps) { + GL_ClientActiveTexture(1); + qglTexCoordPointer(desc->size, GL_FLOAT, desc->stride, (void *)(base + desc->offset)); + } + desc++; + + if (desc->size) { + const GLenum type = desc->type ? GL_UNSIGNED_BYTE : GL_FLOAT; + qglColorPointer(desc->size, type, desc->stride, (void *)(base + desc->offset)); + } } -static void legacy_color_float_pointer(GLint size, GLsizei stride, const GLfloat *pointer) +static void legacy_tex_coord_pointer(const GLfloat *ptr) { - qglColorPointer(size, GL_FLOAT, stride, pointer); + GL_ClientActiveTexture(0); + qglTexCoordPointer(2, GL_FLOAT, 0, ptr); } static void legacy_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) @@ -245,10 +249,8 @@ const glbackend_t backend_legacy = { .state_bits = legacy_state_bits, .array_bits = legacy_array_bits, - .vertex_pointer = legacy_vertex_pointer, + .array_pointers = legacy_array_pointers, .tex_coord_pointer = legacy_tex_coord_pointer, - .light_coord_pointer = legacy_light_coord_pointer, - .color_byte_pointer = legacy_color_byte_pointer, - .color_float_pointer = legacy_color_float_pointer, + .color = legacy_color, }; diff --git a/src/refresh/main.c b/src/refresh/main.c index 999902c20..d501dd896 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -340,7 +340,7 @@ static void GL_DrawSpriteModel(const model_t *model) GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, image->texnum); - GL_BindArrays(VAO_SPRITE); + GL_BindArrays(VA_SPRITE); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC); GL_Color(1, 1, 1, alpha); @@ -388,7 +388,7 @@ static void GL_DrawNullModel(void) GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); - GL_BindArrays(VAO_NULLMODEL); + GL_BindArrays(VA_NULLMODEL); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); @@ -456,7 +456,7 @@ static void GL_OccludeFlares(void) if (!set) { GL_LoadMatrix(glr.viewmatrix); GL_BindTexture(0, TEXNUM_WHITE); - GL_BindArrays(VAO_OCCLUDE); + GL_BindArrays(VA_OCCLUDE); GL_StateBits(GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX); qglColorMask(0, 0, 0, 0); @@ -612,7 +612,7 @@ static void GL_WaterWarp(void) float x0, x1, y0, y1; GL_ForceTexture(0, gl_static.warp_texture); - GL_BindArrays(VAO_WATERWARP); + GL_BindArrays(VA_WATERWARP); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | GLS_TEXTURE_REPLACE | GLS_WARP_ENABLE); GL_ArrayBits(GLA_VERTEX | GLA_TC); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 8e66c30b0..270244d19 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -583,7 +583,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_Color(color[0], color[1], color[2], color[3]); } - GL_TexCoordPointer(2, 0, tcoords->st); + gl_backend->tex_coord_pointer(tcoords->st); GL_LockArrays(num_verts); @@ -807,7 +807,7 @@ void GL_DrawAliasModel(const model_t *model) GL_RotateForEntity(); - GL_BindArrays(dotshading ? VAO_MESH_SHADE : VAO_MESH_FLAT); + GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); if (ent->flags & RF_WEAPONMODEL) setup_weaponmodel(); diff --git a/src/refresh/shader.c b/src/refresh/shader.c index e3bf8866f..3e36e7800 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -24,16 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GLSL(x) SZ_Write(buf, CONST_STR_LEN(#x "\n")); #define GLSF(x) SZ_Write(buf, CONST_STR_LEN(x)) -// must match glArrayBits_t order!!! -enum { - VERT_ATTR_POS, - VERT_ATTR_TC, - VERT_ATTR_LMTC, - VERT_ATTR_COLOR, - - VERT_ATTR_COUNT -}; - static void upload_u_block(void); static void write_header(sizebuf_t *buf) @@ -310,29 +300,22 @@ static void shader_array_bits(GLbitfield bits) } } -static void shader_vertex_pointer(GLint size, GLsizei stride, const GLfloat *pointer) -{ - qglVertexAttribPointer(VERT_ATTR_POS, size, GL_FLOAT, GL_FALSE, stride, pointer); -} - -static void shader_tex_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) -{ - qglVertexAttribPointer(VERT_ATTR_TC, size, GL_FLOAT, GL_FALSE, stride, pointer); -} - -static void shader_light_coord_pointer(GLint size, GLsizei stride, const GLfloat *pointer) +static void shader_array_pointers(const glVaDesc_t *desc, const GLfloat *ptr) { - qglVertexAttribPointer(VERT_ATTR_LMTC, size, GL_FLOAT, GL_FALSE, stride, pointer); -} + uintptr_t base = (uintptr_t)ptr; -static void shader_color_byte_pointer(GLint size, GLsizei stride, const GLubyte *pointer) -{ - qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_UNSIGNED_BYTE, GL_TRUE, stride, pointer); + for (int i = 0; i < VERT_ATTR_COUNT; i++) { + const glVaDesc_t *d = &desc[i]; + if (d->size) { + const GLenum type = d->type ? GL_UNSIGNED_BYTE : GL_FLOAT; + qglVertexAttribPointer(i, d->size, type, d->type, d->stride, (void *)(base + d->offset)); + } + } } -static void shader_color_float_pointer(GLint size, GLsizei stride, const GLfloat *pointer) +static void shader_tex_coord_pointer(const GLfloat *ptr) { - qglVertexAttribPointer(VERT_ATTR_COLOR, size, GL_FLOAT, GL_FALSE, stride, pointer); + qglVertexAttribPointer(VERT_ATTR_TC, 2, GL_FLOAT, GL_FALSE, 0, ptr); } static void shader_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) @@ -451,10 +434,8 @@ const glbackend_t backend_shader = { .state_bits = shader_state_bits, .array_bits = shader_array_bits, - .vertex_pointer = shader_vertex_pointer, + .array_pointers = shader_array_pointers, .tex_coord_pointer = shader_tex_coord_pointer, - .light_coord_pointer = shader_light_coord_pointer, - .color_byte_pointer = shader_color_byte_pointer, - .color_float_pointer = shader_color_float_pointer, + .color = shader_color, }; diff --git a/src/refresh/sky.c b/src/refresh/sky.c index f67ee5713..67a3d0164 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -333,7 +333,7 @@ void R_DrawSkyBox(void) if (!skyfaces) return; // nothing visible - GL_BindArrays(VAO_SPRITE); + GL_BindArrays(VA_SPRITE); GL_StateBits(GLS_TEXTURE_REPLACE); GL_ArrayBits(GLA_VERTEX | GLA_TC); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 6ea065bbc..82073dd64 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -974,8 +974,8 @@ void GL_FreeWorld(void) qglDeleteBuffers(1, &gl_static.world.bufnum); // invalidate binding - if (gls.currentvao == VAO_3D) - gls.currentvao = VAO_NONE; + if (gls.currentva == VA_3D) + gls.currentva = VA_NONE; memset(&gl_static.world, 0, sizeof(gl_static.world)); } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 8c93010a6..0743fa4c2 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -42,7 +42,7 @@ void GL_Flush2D(void) Scrap_Upload(); GL_BindTexture(0, tess.texnum[0]); - GL_BindArrays(VAO_2D); + GL_BindArrays(VA_2D); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); @@ -81,7 +81,7 @@ void GL_DrawParticles(void) return; GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(VAO_EFFECT); + GL_BindArrays(VA_EFFECT); bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE; @@ -260,7 +260,7 @@ void GL_DrawBeams(void) return; GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(VAO_EFFECT); + GL_BindArrays(VA_EFFECT); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_BEAM)) @@ -326,7 +326,7 @@ void GL_DrawFlares(void) return; GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(VAO_EFFECT); + GL_BindArrays(VA_EFFECT); for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_FLARE)) @@ -409,69 +409,68 @@ void GL_DrawFlares(void) GL_FlushFlares(); } -// Fake VAOs. This is the only place where vertex arrays are bound. -void GL_BindArrays(glVertexArray_t vao) +#define ATTR_FLOAT(a, b, c) { a, false, b * sizeof(GLfloat), c * sizeof(GLfloat) } +#define ATTR_UBYTE(a, b, c) { a, true, b * sizeof(GLfloat), c * sizeof(GLfloat) } + +static const glVaDesc_t arraydescs[VA_TOTAL][VERT_ATTR_COUNT] = { + [VA_SPRITE] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, 5, 0), + [VERT_ATTR_TC] = ATTR_FLOAT(2, 5, 3), + }, + [VA_EFFECT] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, 6, 0), + [VERT_ATTR_TC] = ATTR_FLOAT(2, 6, 3), + [VERT_ATTR_COLOR] = ATTR_UBYTE(4, 6, 5), + }, + [VA_NULLMODEL] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, 4, 0), + [VERT_ATTR_COLOR] = ATTR_UBYTE(4, 4, 3), + }, + [VA_OCCLUDE] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, 3, 0), + }, + [VA_WATERWARP] = { + [VERT_ATTR_POS] = ATTR_FLOAT(2, 4, 0), + [VERT_ATTR_TC] = ATTR_FLOAT(2, 4, 2), + }, + [VA_MESH_SHADE] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, VERTEX_SIZE, 0), + [VERT_ATTR_COLOR] = ATTR_FLOAT(4, VERTEX_SIZE, 4), + }, + [VA_MESH_FLAT] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, 4, 0), + }, + [VA_2D] = { + [VERT_ATTR_POS] = ATTR_FLOAT(2, 5, 0), + [VERT_ATTR_TC] = ATTR_FLOAT(2, 5, 2), + [VERT_ATTR_COLOR] = ATTR_UBYTE(4, 5, 4), + }, + [VA_3D] = { + [VERT_ATTR_POS] = ATTR_FLOAT(3, VERTEX_SIZE, 0), + [VERT_ATTR_TC] = ATTR_FLOAT(2, VERTEX_SIZE, 4), + [VERT_ATTR_LMTC] = ATTR_FLOAT(2, VERTEX_SIZE, 6), + [VERT_ATTR_COLOR] = ATTR_UBYTE(4, VERTEX_SIZE, 3), + }, +}; + +void GL_BindArrays(glVertexArray_t va) { - if (gls.currentvao == vao) + const GLfloat *ptr = tess.vertices; + + if (gls.currentva == va) return; - switch (vao) { - case VAO_SPRITE: - GL_VertexPointer(3, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 3); - break; - case VAO_EFFECT: - GL_VertexPointer(3, 6, tess.vertices); - GL_TexCoordPointer(2, 6, tess.vertices + 3); - GL_ColorBytePointer(4, 6, (GLubyte *)(tess.vertices + 5)); - break; - case VAO_NULLMODEL: - GL_VertexPointer(3, 4, tess.vertices); - GL_ColorBytePointer(4, 4, (GLubyte *)(tess.vertices + 3)); - break; - case VAO_OCCLUDE: - GL_VertexPointer(3, 0, tess.vertices); - break; - case VAO_WATERWARP: - GL_VertexPointer(2, 4, tess.vertices); - GL_TexCoordPointer(2, 4, tess.vertices + 2); - break; - case VAO_MESH_SHADE: - GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); - GL_ColorFloatPointer(4, VERTEX_SIZE, tess.vertices + 4); - break; - case VAO_MESH_FLAT: - GL_VertexPointer(3, 4, tess.vertices); - break; - case VAO_2D: - GL_VertexPointer(2, 5, tess.vertices); - GL_TexCoordPointer(2, 5, tess.vertices + 2); - GL_ColorBytePointer(4, 5, (GLubyte *)(tess.vertices + 4)); - break; - case VAO_3D: - if (gl_static.world.vertices) { - GL_VertexPointer(3, VERTEX_SIZE, tess.vertices); - GL_TexCoordPointer(2, VERTEX_SIZE, tess.vertices + 4); - if (lm.nummaps) - GL_LightCoordPointer(2, VERTEX_SIZE, tess.vertices + 6); - GL_ColorBytePointer(4, VERTEX_SIZE, (GLubyte *)(tess.vertices + 3)); - } else { - qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); - - GL_VertexPointer(3, VERTEX_SIZE, VBO_OFS(0)); - GL_TexCoordPointer(2, VERTEX_SIZE, VBO_OFS(4)); - if (lm.nummaps) - GL_LightCoordPointer(2, VERTEX_SIZE, VBO_OFS(6)); - GL_ColorBytePointer(4, VERTEX_SIZE, VBO_OFS(3)); - - qglBindBuffer(GL_ARRAY_BUFFER, 0); - } - break; - default: - Q_assert(!"bad array"); + if (va == VA_3D && !gl_static.world.vertices) { + qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); + ptr = NULL; } - gls.currentvao = vao; + gl_backend->array_pointers(arraydescs[va], ptr); + + if (!ptr) + qglBindBuffer(GL_ARRAY_BUFFER, 0); + + gls.currentva = va; c.vertexArrayBinds++; } @@ -630,7 +629,7 @@ void GL_DrawAlphaFaces(void) glr.ent = NULL; - GL_BindArrays(VAO_3D); + GL_BindArrays(VA_3D); for (const mface_t *face = faces_alpha; face; face = face->next) { if (glr.ent != face->entity) { diff --git a/src/refresh/world.c b/src/refresh/world.c index 90e4f0742..397ab587f 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -442,7 +442,7 @@ void GL_DrawBspModel(mmodel_t *model) GL_RotateForEntity(); - GL_BindArrays(VAO_3D); + GL_BindArrays(VA_3D); GL_ClearSolidFaces(); @@ -593,7 +593,7 @@ void GL_DrawWorld(void) GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(VAO_3D); + GL_BindArrays(VA_3D); GL_ClearSolidFaces(); From c8aedd907d0625588def5ed96ee420ba557f529c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 19 Aug 2024 16:59:12 -0400 Subject: [PATCH 579/974] Adding more Esp functionality --- meson.build | 3 +- src/action/a_esp.c | 26 +- src/action/a_esp.h | 2 + src/action/a_team.c | 22 ++ src/action/a_team.h | 1 + src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib.h | 5 + src/action/botlib/botlib_ai.c | 10 +- src/action/botlib/botlib_communication.c | 7 +- src/action/botlib/botlib_esp.c | 290 +++++++++++++---------- src/action/botlib/botlib_personality.c | 16 +- 11 files changed, 239 insertions(+), 144 deletions(-) diff --git a/meson.build b/meson.build index 24de5da44..a1232d792 100644 --- a/meson.build +++ b/meson.build @@ -245,7 +245,8 @@ botlib_src = [ 'src/action/botlib/botlib_spawn.c', 'src/action/botlib/botlib_spawnpoints.c', 'src/action/botlib/botlib_weapons.c', - 'src/action/botlib/botlib_personality.c' + 'src/action/botlib/botlib_personality.c', + 'src/action/botlib/botlib_utils.c', ] cc = meson.get_compiler('c') diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 8b9465ecf..1cee193ac 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -14,6 +14,8 @@ espsettings_t espsettings; int esp_last_chosen_spawn = 0; int esp_spawnpoint_index[TEAM_TOP] = {-1}; +edict_t* chosenSpawnpoint[TEAM_TOP] = {NULL}; +edict_t* etvTarget = NULL; unsigned int esp_team_effect[] = { EF_BLASTER | EF_ROTATE | EF_TELEPORTER, @@ -515,6 +517,9 @@ void EspMakeCapturePoint(edict_t *flag) } } #endif + + // Globally accessible capturepoint + etvTarget = flag; esp_flag_count ++; } @@ -1214,6 +1219,7 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) char *cname; int teamNum = ent->client->resp.team; + edict_t* chosen_spawn_point = NULL; ent->client->resp.esp_state = ESP_STATE_PLAYING; @@ -1262,7 +1268,10 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) } else { // Custom spawns take precedence over standard spawns if ((EspSpawnpointCount(teamNum) > 0)) { - return SelectEspCustomSpawnPoint(ent); + chosen_spawn_point = SelectEspCustomSpawnPoint(ent); + // Keeping track of the chosen spawnpoint + chosenSpawnpoint[teamNum] = chosen_spawn_point; + return chosen_spawn_point; // but if there are none, then we go back to old faithful } else { @@ -1270,10 +1279,17 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) gi.dprintf("%s: No custom spawns, defaulting to teamplay spawn\n", __func__); // NULL check, if there are no teamplay (info_player_team...) spawns, then default to deathmatch spawns - if (SelectTeamplaySpawnPoint(ent)) - return SelectTeamplaySpawnPoint(ent); - else - return SelectDeathmatchSpawnPoint(); + if (SelectTeamplaySpawnPoint(ent)) { + chosen_spawn_point = SelectTeamplaySpawnPoint(ent); + // Keeping track of the chosen spawnpoint + chosenSpawnpoint[teamNum] = chosen_spawn_point; + return chosen_spawn_point; + } else { + chosen_spawn_point = SelectDeathmatchSpawnPoint(); + // Keeping track of the chosen spawnpoint + chosenSpawnpoint[teamNum] = chosen_spawn_point; + return chosen_spawn_point; + } } } // All else fails, use deathmatch spawn points diff --git a/src/action/a_esp.h b/src/action/a_esp.h index c671e15e3..493ef4a93 100644 --- a/src/action/a_esp.h +++ b/src/action/a_esp.h @@ -133,3 +133,5 @@ void EspCleanUp(void); void EspDebug(void); extern qboolean esp_punishment_phase; extern int esp_spawnpoint_index[TEAM_TOP]; +extern edict_t* chosenSpawnpoint[TEAM_TOP]; +extern edict_t* etvTarget; \ No newline at end of file diff --git a/src/action/a_team.c b/src/action/a_team.c index 194602d1d..8b892bce7 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2860,6 +2860,9 @@ int CheckTeamRules (void) gi.sound (&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex ("world/10_0.wav"), 1.0, ATTN_NONE, 0.0); + // Advertizing Discord and the forums + //PrintAdNotification(NULL); + #if USE_AQTION // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ @@ -4280,4 +4283,23 @@ int TotalPlayersAliveOnTeam(int teamNum) } return count; +} + +void PrintAdNotification(edict_t* ent) +{ + // Don't spam this in matchmode + if (matchmode->value) + return; + + // Do not send to bots + if (ent->is_bot) + return; + + const char *msg = "Get in on the Action! Join us in Discord at\n https://discord.aq2world.com\nand the forums at\n https://www.aq2world.com\n"; + + // If ent is null, send to all clients, else send to client + if (ent == NULL) + gi.bprintf(PRINT_MEDIUM, "%s", msg); + else + gi.cprintf(ent, PRINT_MEDIUM, "%s", msg); } \ No newline at end of file diff --git a/src/action/a_team.h b/src/action/a_team.h index f5a2e8305..ffa1d4a76 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -110,6 +110,7 @@ void JoinTeamAutobalance (edict_t * ent); //Utility functions int TotalPlayersOnTeam(int teamNum); int TotalPlayersAliveOnTeam(int teamNum); +void PrintAdNotification(edict_t* ent); typedef struct spawn_distances_s { diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index e3fc29b92..f61f2fe7d 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -386,6 +386,7 @@ void ACESP_RemoveBot(char *name); void safe_cprintf (edict_t *ent, int printlevel, const char *fmt, ...); void safe_centerprintf (edict_t *ent, const char *fmt, ...); void debug_printf (char *fmt, ...); +int ACEND_FindClosestReachableNode(edict_t *self, int range, int type); // bot_ai.c protos qboolean BOTAI_NeedToBandage(edict_t *bot); diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 66bb849a7..7694f116e 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -524,6 +524,11 @@ int BOTLIB_LocateFloorItem(edict_t* self, int* items_to_get, int items_counter); int BOTLIB_GetEquipment(edict_t* self); //rekkie -- collecting weapons, items, ammo -- e +// =========================================================================== +// botlib_debug.c +// =========================================================================== +void BOTLIB_Debug(const char *debugmsg, ...); + // =========================================================================== // botlib_personality.c // =========================================================================== diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index fdff48d85..eee3c5fb6 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -721,11 +721,11 @@ void BOTLIB_Think(edict_t* self) { BOTLIB_CTF_Goals(self); } - // else if (esp->value) // ESP Goals - // { - // // Do nothing until this is fixed - // BOTLIB_ESP_Goals(self); - // } + else if (esp->value) // ESP Goals + { + // Do nothing until this is fixed + BOTLIB_ESP_Goals(self); + } // Check if the bot is in a NAV state (I need a nav) or if NONE else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 183dc4af3..a9a334c72 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -151,7 +151,7 @@ char *botchat_victory[DBC_VICTORY] = "too ez" }; -#define DBC_RAGE 8 +#define DBC_RAGE 9 char *botchat_rage[DBC_RAGE] = { "f this map", @@ -161,12 +161,13 @@ char *botchat_rage[DBC_RAGE] = "yeah gg", "that's enough for me today", "ASDKDDKJFJK", - "PERKELEEeeeee" + "PERKELEEeeeee", + "FYFAAAAN" }; // This function is used to mix up chat so that multiple // phrases can be combined to create a more unique message -// Currently onlt doing this for CHAT_KILLED +// Currently only doing this for CHAT_KILLED char* _chatMix(char* msg, bot_chat_types_t chattype, int randval) { char* text = NULL; diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index 368b8c65f..f31ed2f27 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -15,7 +15,6 @@ int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader) if (ent == NULL) return INVALID; - edict_t *tmp = NULL; edict_t *target = NULL; // Reset escortcap value @@ -23,9 +22,7 @@ int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader) // If leader is null, it means we're looking for an ETV target if(leader == NULL) { - while ((tmp = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { - target = tmp; - } + target = etvTarget; } else { // Target is leader target = leader; } @@ -207,7 +204,7 @@ int BOTLIB_FindMyLeaderNode(edict_t* self) edict_t* leader = teams[myTeam].leader; //float distance; - if (IS_ALIVE(leader)) { + if (leader != NULL && IS_ALIVE(leader)) { if (leader->is_bot) { // No need to do fancy stuff if leader is a bot return leader->bot.current_node; } else { @@ -270,14 +267,23 @@ int BOTLIB_InterceptLeader_3Team(edict_t* self) int BOTLIB_InterceptLeader_2Team(edict_t* self) { int myTeam = self->client->resp.team; + int espMode = EspModeCheck(); - if (myTeam == TEAM1) { - if (IS_ALIVE(EspGetLeader(TEAM2))) { - return BOTLIB_FindEnemyLeaderNode(self, TEAM2); + if (espMode == ESPMODE_ATL) { + if (myTeam == TEAM1) { + if (IS_ALIVE(EspGetLeader(TEAM2))) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM2); + } + } else { + if (IS_ALIVE(EspGetLeader(TEAM1))) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM1); + } } - } else { - if (IS_ALIVE(EspGetLeader(TEAM1))) { - return BOTLIB_FindEnemyLeaderNode(self, TEAM1); + } else if (espMode == ESPMODE_ETV) { + if (myTeam == TEAM2) { + if (IS_ALIVE(EspGetLeader(TEAM1))) { + return BOTLIB_ESPGetTargetNode(self, NULL); + } } } return INVALID; // No team leaders are alive, so no node to intercept @@ -285,7 +291,7 @@ int BOTLIB_InterceptLeader_2Team(edict_t* self) int BOTLIB_InterceptLeader_ETV(edict_t* self) { - + return BOTLIB_InterceptLeader_2Team(self); } // Intercept enemy team leader (bad guy) @@ -369,22 +375,94 @@ float BOTLIB_DistanceToEnemyLeader(edict_t* self, int flagType) } +/* + +BOT Espionage Behaviors + +*/ + +void _EspWander(edict_t* self) +{ + self->bot.bot_esp_state = BOT_ESP_WANDER; + BOTLIB_PickLongRangeGoal(self); + BOTLIB_Debug("%s: ATL Leader %s is wandering\n", __func__, self->client->pers.netname); + +} + +qboolean _EspFlee(edict_t* self, int closestSpawnPointNode) +{ + char* debugName = "leader"; + + if (!IS_LEADER(self)) { + debugName = "crew"; + } + + self->bot.bot_esp_state = BOT_ESP_RETREAT; + if (BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false)) { + BOTLIB_Debug("%s: ATL %s %s is fleeing back to %i)\n", __func__, debugName, self->client->pers.netname, closestSpawnPointNode); + return true; + } + return false; // Could not flee +} + +qboolean _EspAttackLeader(edict_t* self, int enemyNode) +{ + char* debugName = "leader"; + + if (!IS_LEADER(self)) { + debugName = "crew"; + } + + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + if (BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false)) { + BOTLIB_Debug("%s: ATL %s %s going for enemy leader at node %i\n", __func__, debugName, self->client->pers.netname, enemyNode); + return true; + } + return false; +} + +qboolean _EspAttackTarget(edict_t* self, int targetNode) +{ + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + if (BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false)) { + BOTLIB_Debug("%s: ETV Leader %s going for ETV target at node %i\n", __func__, self->client->pers.netname, targetNode); + return true; + } + return false; +} + +qboolean _EspDefendLeader(edict_t* self, int leaderNode) +{ + + self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; + if (BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false)) { + BOTLIB_Debug("%s: ATL %s crew going for enemy leader at node %i\n", __func__, self->client->pers.netname, leaderNode); + return true; + } + return false; +} + + void BOTLIB_ESP_Goals(edict_t* self) { if (!lights_camera_action && !espsettings.esp_live_round) return; // Only allow during a real match (after LCA and before win/loss announcement) - espsettings_t *es = &espsettings; + int espMode = EspModeCheck(); // Team related variables int myTeam = self->client->resp.team; int totalTeammates = TotalPlayersOnTeam(myTeam); int aliveTeammates = TotalPlayersAliveOnTeam(myTeam); float percentAlive = (float)aliveTeammates / (float)totalTeammates * 100; - // Keep track of current round's spawnpoint if we need to flee - //edict_t* roundSpawnPoint = espsettings.round_spawnpoint[myTeam]; - edict_t *roundSpawnPoint = es->custom_spawns[myTeam][esp_spawnpoint_index[myTeam]]; - int closestSpawnPointNode = ACEND_FindClosestReachableNode(roundSpawnPoint, NODE_DENSITY, NODE_ALL); - + // Refactor this so it is only calculated once, at the beginning of each round + int closestSpawnPointNode = ACEND_FindClosestReachableNode(chosenSpawnpoint[myTeam], NODE_DENSITY, NODE_ALL); + // End Refactor + + // Relevant node info + int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); + int enemyNode = BOTLIB_InterceptEnemyLeader(self); + int leaderNode = BOTLIB_FindMyLeaderNode(self); + // Logic time: Discerning between leader vs non-leader // TODO: Better logic later on when we utilize bot.skill! if (IS_LEADER(self)) @@ -394,196 +472,164 @@ void BOTLIB_ESP_Goals(edict_t* self) // Logic splits here, between ATL and ETV modes bot_leader_think: - if (EspModeCheck() == ESPMODE_ATL) { + if (espMode == ESPMODE_ATL) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the enemy leader if (rand() % 2 == 0) { - int enemyNode = BOTLIB_InterceptEnemyLeader(self); if (enemyNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); + _EspAttackLeader(self, enemyNode); + return; + } else { + // Can't find enemy leader? Wander time + _EspWander(self); return; } } else { // 50% chance to flee (run back to original spawnpoint) - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_RETREAT; - BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); - return; + if (chosenSpawnpoint[myTeam] != NULL) { + _EspFlee(self, closestSpawnPointNode); } } } else { // 33% chance to go for the enemy leader if (rand() % 3 == 0) { - int enemyNode = BOTLIB_InterceptEnemyLeader(self); if (enemyNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); + _EspAttackLeader(self, enemyNode); return; } } else if (rand() % 3 == 1) { // 33% chance to flee - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_RETREAT; - BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); + if (chosenSpawnpoint[myTeam] != NULL) { + _EspFlee(self, closestSpawnPointNode); return; } } else if (rand() % 3 == 2) { // 33% chance to wander - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_WANDER; - BOTLIB_PickLongRangeGoal(self); + if (chosenSpawnpoint[myTeam] != NULL) { + _EspWander(self); return; } } } - } else if (EspModeCheck() == ESPMODE_ETV && myTeam == TEAM1) { + } else if (espMode == ESPMODE_ETV && myTeam == TEAM1) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the ETV target if (rand() % 2 == 0) { - int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); if (targetNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); + _EspAttackTarget(self, targetNode); return; } } else { // 50% chance to flee - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_RETREAT; - BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false); + if (chosenSpawnpoint[myTeam] != NULL) { + _EspFlee(self, closestSpawnPointNode); + return; + } else { + _EspWander(self); return; } } - } else { - // We shouldn't get here but in case we do, do something - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_WANDER; - BOTLIB_PickLongRangeGoal(self); - return; - } - } - } else if (EspModeCheck() == ESPMODE_ETV && myTeam == TEAM2) { - // Defend target - if (rand() % 2 == 0) { - int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); + } else { // More than 50% alive, let's attack the target! if (targetNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_DEFEND_TARGET; - BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); - return; - } - } else if (rand() % 2 == 1) { - // Attack team1's leader - int enemyNode = BOTLIB_InterceptEnemyLeader(self); - if (enemyNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); - return; - } - } else { - // We shouldn't get here but in case we do, do something - if (roundSpawnPoint) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_WANDER; - BOTLIB_PickLongRangeGoal(self); + _EspAttackTarget(self, targetNode); return; } } + // We shouldn't get here but in case we do, do something + _EspWander(self); + return; + } // Main difference between crew and leader is that the crew are // expendable, protect the leader at all costs! bot_crew_think: - if (EspModeCheck() == ESPMODE_ATL) { + if (espMode == ESPMODE_ATL) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to stay with team leader if (rand() % 2 == 0) { - int leaderNode = BOTLIB_FindMyLeaderNode(self); if (leaderNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; - BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); + _EspDefendLeader(self, leaderNode); return; } } else { // 50% chance to attack enemy leader - int enemyNode = BOTLIB_InterceptEnemyLeader(self); if (enemyNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); - return; + _EspAttackLeader(self, enemyNode); } } - } else { + } else { // More than 50% alive, let's figure something out // 33% chance to stay with team leader if (rand() % 3 == 0) { - int leaderNode = BOTLIB_FindMyLeaderNode(self); if (leaderNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; - BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); + _EspDefendLeader(self, leaderNode); return; } } else if (rand() % 3 == 1) { // 33% chance to attack enemy leader - int enemyNode = BOTLIB_InterceptEnemyLeader(self); if (enemyNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false); + _EspAttackLeader(self, enemyNode); return; } } else if (rand() % 3 == 2) { // 33% chance to wander - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_WANDER; - BOTLIB_PickLongRangeGoal(self); + _EspWander(self); return; } } - } else if (EspModeCheck() == ESPMODE_ETV) { + } else if (espMode == ESPMODE_ETV && myTeam == TEAM1) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the ETV target if (rand() % 2 == 0) { - int targetNode = BOTLIB_ESPGetTargetNode(self, NULL); if (targetNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; - BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false); + _EspAttackTarget(self, targetNode); return; } - } else { + } else{ // 50% chance to stay with team leader - int leaderNode = BOTLIB_FindMyLeaderNode(self); if (leaderNode != INVALID) { - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_COVER_TEAM_LEADER; - BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false); + _EspDefendLeader(self, leaderNode); return; } } - } else { - // We shouldn't get here but in case we do, do something - self->bot.state = BOT_MOVE_STATE_MOVE; - self->bot.bot_esp_state = BOT_ESP_WANDER; - BOTLIB_PickLongRangeGoal(self); - return; + } else { // More than 50% alive, let's attack the target! + if (targetNode != INVALID) { + _EspAttackTarget(self, targetNode); + return; + } else { + _EspWander(self); + return; + } } - } + } else if (espMode == ESPMODE_ETV && myTeam == TEAM2) { + // If percentage of teammates alive is < 50% then : + if (percentAlive < 50) { + // 50% chance to go for the ETV target + if (rand() % 2 == 0) { + if (targetNode != INVALID) { + _EspAttackTarget(self, targetNode); + return; + } + } else { + // 50% chance to attack enemy leader + if (enemyNode != INVALID) { + _EspAttackLeader(self, enemyNode); + return; + } + } + } else { // More than 50%, attack that leader! + if (enemyNode != INVALID) { + _EspAttackLeader(self, enemyNode); + return; + } + } + } else { + // We shouldn't get here but in case we do, do something + _EspWander(self); + return; } /* @@ -594,7 +640,7 @@ void BOTLIB_ESP_Goals(edict_t* self) if (item_node != INVALID) { { - self->bot.state = BOT_MOVE_STATE_MOVE; + self->bot.state = BOT_MOVE_STATE_NAV; BOTLIB_SetGoal(self, nodes[item_node].nodenum); self->bot.bot_ctf_state == BOT_CTF_STATE_GET_DROPPED_ITEMS; //Com_Printf("%s %s going for %s at node %d\n", __func__, self->client->pers.netname, self->bot.get_item->classname, nodes[item_node].nodenum); diff --git a/src/action/botlib/botlib_personality.c b/src/action/botlib/botlib_personality.c index 7a4972054..634d4f3e0 100644 --- a/src/action/botlib/botlib_personality.c +++ b/src/action/botlib/botlib_personality.c @@ -760,13 +760,13 @@ qboolean BOTLIB_SpawnRush(edict_t* bot) { bool isRushing = randomValue <= probabilityThreshold; // Decide based on the probability threshold - if (bot_debug->value) { - gi.dprintf("%s: %s's probabilityThreshold = %f, randomValue = %f, isRushing = %s\n", - __func__, - bot->client->pers.netname, - probabilityThreshold, - randomValue, - isRushing ? "YES" : "NO"); - } + // if (bot_debug->value) { + // gi.dprintf("%s: %s's probabilityThreshold = %f, randomValue = %f, isRushing = %s\n", + // __func__, + // bot->client->pers.netname, + // probabilityThreshold, + // randomValue, + // isRushing ? "YES" : "NO"); + // } return isRushing; } \ No newline at end of file From 2063e69b5e39299426295883260609f3a3b3d8d5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 19 Aug 2024 19:49:59 -0400 Subject: [PATCH 580/974] Added botlib_utils --- meson.build | 2 +- src/action/botlib/botlib_utils.c | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/action/botlib/botlib_utils.c diff --git a/meson.build b/meson.build index a1232d792..9c855ac2f 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=release', + 'buildtype=debug', ], ) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c new file mode 100644 index 000000000..ec833799d --- /dev/null +++ b/src/action/botlib/botlib_utils.c @@ -0,0 +1,19 @@ +#include "../g_local.h" +#include "../acesrc/acebot.h" +#include "botlib.h" + +/* +This file is for common utilties that are used by the botlib functions + +*/ + +void BOTLIB_Debug(const char *debugmsg, ...) +{ + if (!bot_debug->value) + return; + + va_list argptr; + va_start(argptr, debugmsg); + Com_Printf(debugmsg, argptr); + va_end(argptr); +} \ No newline at end of file From 59d3c9ccb1889e343ee12ab1ff6b00cc0df7703e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 19 Aug 2024 20:14:36 -0400 Subject: [PATCH 581/974] Some else statements in esp botlib logic, teamplay_spawn_node add --- src/action/a_team.c | 7 ++++++- src/action/a_team.h | 1 + src/action/botlib/botlib_esp.c | 23 +++++++++++++++++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 8b892bce7..a709e82c5 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -331,6 +331,7 @@ edict_t *potential_spawns[MAX_SPAWNS]; int num_potential_spawns; edict_t *teamplay_spawns[MAX_TEAMS]; trace_t trace_t_temp; // used by our trace replace macro in ax_team.h +int teamplay_spawn_node[MAX_TEAMS]; // used to keep track of the closest BOTLIB node to the spawnpoint // int NS_num_used_farteamplay_spawns[MAX_TEAMS]; @@ -2214,8 +2215,9 @@ static void SpawnPlayers(void) int i; edict_t *ent; - if (esp->value) + if (esp->value) { NS_SetupTeamSpawnPoints (); + } if (gameSettings & GS_ROUNDBASED) { @@ -4204,6 +4206,9 @@ void NS_SetupTeamSpawnPoints (void) for (l = 0; l < MAX_TEAMS; l++) { teamplay_spawns[l] = NULL; teams_assigned[l] = false; + #ifndef NO_BOTS + teamplay_spawn_node[l] = ACEND_FindClosestReachableNode(chosenSpawnpoint[l], NODE_DENSITY, NODE_ALL); + #endif } if (NS_SelectRandomTeamplaySpawnPoint (NS_randteam, teams_assigned) == false) diff --git a/src/action/a_team.h b/src/action/a_team.h index ffa1d4a76..4cdce90af 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -141,6 +141,7 @@ extern int day_cycle_at; extern int teamCount; extern int in_warmup; extern qboolean teams_changed; +extern int teamplay_spawn_node[MAX_TEAMS]; typedef struct menu_list_weapon { diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index f31ed2f27..4980e1d6d 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -385,8 +385,7 @@ void _EspWander(edict_t* self) { self->bot.bot_esp_state = BOT_ESP_WANDER; BOTLIB_PickLongRangeGoal(self); - BOTLIB_Debug("%s: ATL Leader %s is wandering\n", __func__, self->client->pers.netname); - + BOTLIB_Debug("%s: ATL %s is wandering\n", __func__, self->client->pers.netname); } qboolean _EspFlee(edict_t* self, int closestSpawnPointNode) @@ -401,6 +400,10 @@ qboolean _EspFlee(edict_t* self, int closestSpawnPointNode) if (BOTLIB_CanVisitNode(self, closestSpawnPointNode, true, INVALID, false)) { BOTLIB_Debug("%s: ATL %s %s is fleeing back to %i)\n", __func__, debugName, self->client->pers.netname, closestSpawnPointNode); return true; + } else { + BOTLIB_Debug("%s: ATL %s %s could not flee back to %i, picking another goal\n", __func__, debugName, self->client->pers.netname, closestSpawnPointNode); + BOTLIB_PickLongRangeGoal(self); + return false; } return false; // Could not flee } @@ -415,8 +418,12 @@ qboolean _EspAttackLeader(edict_t* self, int enemyNode) self->bot.bot_esp_state = BOT_ESP_ATTACK_TARGET; if (BOTLIB_CanVisitNode(self, enemyNode, true, INVALID, false)) { - BOTLIB_Debug("%s: ATL %s %s going for enemy leader at node %i\n", __func__, debugName, self->client->pers.netname, enemyNode); + BOTLIB_Debug("%s: %s %s going for enemy leader at node %i\n", __func__, debugName, self->client->pers.netname, enemyNode); return true; + } else { + BOTLIB_Debug("%s: %s %s could not attack enemy leader at %i, picking another goal\n", __func__, debugName, self->client->pers.netname, enemyNode); + BOTLIB_PickLongRangeGoal(self); + return false; } return false; } @@ -427,6 +434,10 @@ qboolean _EspAttackTarget(edict_t* self, int targetNode) if (BOTLIB_CanVisitNode(self, targetNode, true, INVALID, false)) { BOTLIB_Debug("%s: ETV Leader %s going for ETV target at node %i\n", __func__, self->client->pers.netname, targetNode); return true; + } else { + BOTLIB_Debug("%s: %s could attack ETV target at %i, picking another goal\n", __func__, self->client->pers.netname, targetNode); + BOTLIB_PickLongRangeGoal(self); + return false; } return false; } @@ -438,6 +449,10 @@ qboolean _EspDefendLeader(edict_t* self, int leaderNode) if (BOTLIB_CanVisitNode(self, leaderNode, true, INVALID, false)) { BOTLIB_Debug("%s: ATL %s crew going for enemy leader at node %i\n", __func__, self->client->pers.netname, leaderNode); return true; + } else { + BOTLIB_Debug("%s: %s could reach leader at %i, picking another goal\n", __func__, self->client->pers.netname, leaderNode); + BOTLIB_PickLongRangeGoal(self); + return false; } return false; } @@ -455,7 +470,7 @@ void BOTLIB_ESP_Goals(edict_t* self) float percentAlive = (float)aliveTeammates / (float)totalTeammates * 100; // Refactor this so it is only calculated once, at the beginning of each round - int closestSpawnPointNode = ACEND_FindClosestReachableNode(chosenSpawnpoint[myTeam], NODE_DENSITY, NODE_ALL); + int closestSpawnPointNode = teamplay_spawn_node[myTeam]; // End Refactor // Relevant node info From 02f0b983325a0f83118434eeb6719687debc61fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 18:51:50 +0300 Subject: [PATCH 582/974] Move glPolygonMode to core profile. --- src/refresh/qgl.c | 5 +---- src/refresh/qgl.h | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 1ba1c031e..42f87395d 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -112,6 +112,7 @@ static const glsection_t sections[] = { .functions = (const glfunction_t []) { QGL_FN(ClearDepth), QGL_FN(DepthRange), + QGL_FN(PolygonMode), { NULL } } }, @@ -121,10 +122,6 @@ static const glsection_t sections[] = { .ver_gl = QGL_VER(1, 1), .excl_gl = QGL_VER(3, 1), .caps = QGL_CAP_TEXTURE_BITS, - .functions = (const glfunction_t []) { - QGL_FN(PolygonMode), - { NULL } - } }, // GL 1.1, ES 3.0 diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index d23295e79..657cbd53a 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -93,8 +93,6 @@ QGLAPI void (APIENTRYP qglVertexPointer)(GLint size, GLenum type, GLsizei stride // GL 1.1, not ES QGLAPI void (APIENTRYP qglClearDepth)(GLdouble depth); QGLAPI void (APIENTRYP qglDepthRange)(GLdouble near, GLdouble far); - -// GL 1.1, not ES, compat QGLAPI void (APIENTRYP qglPolygonMode)(GLenum face, GLenum mode); // GL 1.3 From b8abcdd64bac47b75e442286457313229986effd Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 18:53:13 +0300 Subject: [PATCH 583/974] Check for ARB_multi_bind/ARB_direct_state_access. --- src/refresh/qgl.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 42f87395d..5828c9f49 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -315,6 +315,13 @@ static const glsection_t sections[] = { { .ver_gl = QGL_VER(4, 4), .caps = QGL_CAP_QUERY_RESULT_NO_WAIT, + }, + + // GL 4.4 + // ARB_multi_bind + { + .ver_gl = QGL_VER(4, 4), + .extension = "GL_ARB_multi_bind", .functions = (const glfunction_t []) { QGL_FN(BindTextures), { NULL } @@ -322,8 +329,10 @@ static const glsection_t sections[] = { }, // GL 4.5 + // GL_ARB_direct_state_access { .ver_gl = QGL_VER(4, 5), + .extension = "GL_ARB_direct_state_access", .functions = (const glfunction_t []) { QGL_FN(BindTextureUnit), { NULL } From 2adb0db361aafba5445113bc7bc59375a4ccf0ef Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 19:11:33 +0300 Subject: [PATCH 584/974] Add support for OpenGL 3.2 core profile. --- src/refresh/gl.h | 39 ++++++++------ src/refresh/main.c | 44 +++++++++++----- src/refresh/mesh.c | 7 ++- src/refresh/models.c | 104 ++++++++++++++++++++++++++++++------ src/refresh/qgl.c | 27 +++++----- src/refresh/qgl.h | 4 ++ src/refresh/state.c | 6 +-- src/refresh/surf.c | 14 ++--- src/refresh/tess.c | 123 ++++++++++++++++++++++++++++--------------- 9 files changed, 257 insertions(+), 111 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 9b492d234..c37a99df1 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -120,6 +120,9 @@ typedef struct { GLuint warp_renderbuffer; GLuint warp_framebuffer; GLuint u_bufnum; + GLuint array_object; + GLuint index_buffer; + GLuint vertex_buffer; GLuint programs[MAX_PROGRAMS]; GLuint texnums[NUM_TEXNUMS]; GLenum samples_passed; @@ -173,6 +176,7 @@ typedef enum { QGL_CAP_TEXTURE_ANISOTROPY = BIT(7), QGL_CAP_UNPACK_SUBIMAGE = BIT(8), QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(9), + QGL_CAP_CLIENT_VA = BIT(10), } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) @@ -438,6 +442,8 @@ typedef struct { maliasframe_t *frames; mspriteframe_t *spriteframes; }; + + GLuint buffer; } model_t; // xyz[3] | color[1] | st[2] | lmst[2] @@ -543,6 +549,7 @@ typedef struct { GLuint texnums[MAX_TMUS]; GLbitfield state_bits; GLbitfield array_bits; + GLuint currentbuffer[2]; glVertexArray_t currentva; const GLfloat *currentmatrix; struct { @@ -596,18 +603,6 @@ static inline void GL_ArrayBits(GLbitfield bits) } } -static inline void GL_LockArrays(GLsizei count) -{ - if (qglLockArraysEXT) - qglLockArraysEXT(0, count); -} - -static inline void GL_UnlockArrays(void) -{ - if (qglUnlockArraysEXT) - qglUnlockArraysEXT(); -} - static inline void GL_ForceMatrix(const GLfloat *matrix) { gl_backend->load_view_matrix(matrix); @@ -622,6 +617,15 @@ static inline void GL_LoadMatrix(const GLfloat *matrix) } } +static inline void GL_BindBuffer(GLenum target, GLuint buffer) +{ + const int i = target == GL_ELEMENT_ARRAY_BUFFER; + if (gls.currentbuffer[i] != buffer) { + qglBindBuffer(target, buffer); + gls.currentbuffer[i] = buffer; + } +} + static inline void GL_ClearDepth(GLfloat d) { if (qglClearDepthf) @@ -643,18 +647,18 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) #define GL_DrawTriangles(num_indices, indices) \ qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices) -enum { +typedef enum { SHOWTRIS_WORLD = BIT(0), SHOWTRIS_MESH = BIT(1), SHOWTRIS_PIC = BIT(2), SHOWTRIS_FX = BIT(3), -}; +} showtris_t; void GL_ForceTexture(GLuint tmu, GLuint texnum); void GL_BindTexture(GLuint tmu, GLuint texnum); void GL_CommonStateBits(GLbitfield bits); void GL_ScrollSpeed(vec2_t scroll, GLbitfield bits); -void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices); +void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices, bool indexed); void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar); void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x); void GL_Setup2D(void); @@ -737,6 +741,11 @@ void GL_DrawBeams(void); void GL_DrawFlares(void); void GL_BindArrays(glVertexArray_t va); +void GL_InitArrays(void); +void GL_ShutdownArrays(void); +void GL_LockArrays(GLsizei count); +void GL_UnlockArrays(void); + void GL_Flush3D(void); void GL_AddAlphaFace(mface_t *face, entity_t *ent); diff --git a/src/refresh/main.c b/src/refresh/main.c index d501dd896..8f01799d0 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -955,19 +955,33 @@ static void GL_SetupConfig(void) gl_config.max_texture_size_log2 = Q_log2(min(integer, MAX_TEXTURE_SIZE)); gl_config.max_texture_size = 1U << gl_config.max_texture_size_log2; - gl_config.colorbits = 0; - qglGetIntegerv(GL_RED_BITS, &integer); - gl_config.colorbits += integer; - qglGetIntegerv(GL_GREEN_BITS, &integer); - gl_config.colorbits += integer; - qglGetIntegerv(GL_BLUE_BITS, &integer); - gl_config.colorbits += integer; - - qglGetIntegerv(GL_DEPTH_BITS, &integer); - gl_config.depthbits = integer; - - qglGetIntegerv(GL_STENCIL_BITS, &integer); - gl_config.stencilbits = integer; + if (gl_config.caps & QGL_CAP_CLIENT_VA) { + qglGetIntegerv(GL_RED_BITS, &integer); + gl_config.colorbits = integer; + qglGetIntegerv(GL_GREEN_BITS, &integer); + gl_config.colorbits += integer; + qglGetIntegerv(GL_BLUE_BITS, &integer); + gl_config.colorbits += integer; + + qglGetIntegerv(GL_DEPTH_BITS, &integer); + gl_config.depthbits = integer; + + qglGetIntegerv(GL_STENCIL_BITS, &integer); + gl_config.stencilbits = integer; + } else if (qglGetFramebufferAttachmentParameteriv) { + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &integer); + gl_config.colorbits = integer; + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE, &integer); + gl_config.colorbits += integer; + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE, &integer); + gl_config.colorbits += integer; + + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_DEPTH, GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &integer); + gl_config.depthbits = integer; + + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_STENCIL, GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &integer); + gl_config.stencilbits = integer; + } if (qglDebugMessageCallback && qglIsEnabled(GL_DEBUG_OUTPUT)) { Com_Printf("Enabling GL debug output.\n"); @@ -1066,6 +1080,8 @@ bool R_Init(bool total) // register our variables GL_Register(); + GL_InitArrays(); + GL_InitState(); GL_InitTables(); @@ -1105,6 +1121,8 @@ void R_Shutdown(bool total) GL_ShutdownState(); + GL_ShutdownArrays(); + // shutdown our QGL subsystem QGL_Shutdown(); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 270244d19..fcb68c86b 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -28,6 +28,7 @@ static vec3_t newscale; static vec3_t translate; static vec_t shellscale; static vec4_t color; +static GLuint buffer; static vec3_t shadedir; static bool dotshading; @@ -583,6 +584,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, GL_Color(color[0], color[1], color[2], color[3]); } + GL_BindBuffer(GL_ARRAY_BUFFER, buffer); gl_backend->tex_coord_pointer(tcoords->st); GL_LockArrays(num_verts); @@ -593,7 +595,7 @@ static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, draw_celshading(indices, num_indices); if (gl_showtris->integer & SHOWTRIS_MESH) - GL_DrawOutlines(num_indices, indices); + GL_DrawOutlines(num_indices, indices, true); // FIXME: unlock arrays before changing matrix? draw_shadow(indices, num_indices); @@ -809,6 +811,9 @@ void GL_DrawAliasModel(const model_t *model) GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); + buffer = model->buffer; + GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, model->buffer); + if (ent->flags & RF_WEAPONMODEL) setup_weaponmodel(); diff --git a/src/refresh/models.c b/src/refresh/models.c index 068326a29..29b04b8c5 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -100,6 +100,24 @@ static void MOD_List_f(void) Com_Printf("Total resident: %zu\n", bytes); } +static void MOD_Free(model_t *model) +{ + Hunk_Free(&model->hunk); +#if USE_MD5 + Hunk_Free(&model->skeleton_hunk); +#endif + + if (model->buffer) { + // invalidate bindings + for (int i = 0; i < 2; i++) + if (gls.currentbuffer[i] == model->buffer) + gls.currentbuffer[i] = 0; + qglDeleteBuffers(1, &model->buffer); + } + + memset(model, 0, sizeof(*model)); +} + void MOD_FreeUnused(void) { model_t *model; @@ -118,11 +136,7 @@ void MOD_FreeUnused(void) #endif } else { // don't need this model - Hunk_Free(&model->hunk); -#if USE_MD5 - Hunk_Free(&model->skeleton_hunk); -#endif - memset(model, 0, sizeof(*model)); + MOD_Free(model); } } } @@ -132,16 +146,9 @@ void MOD_FreeAll(void) model_t *model; int i; - for (i = 0, model = r_models; i < r_numModels; i++, model++) { - if (!model->type) - continue; - - Hunk_Free(&model->hunk); -#if USE_MD5 - Hunk_Free(&model->skeleton_hunk); -#endif - memset(model, 0, sizeof(*model)); - } + for (i = 0, model = r_models; i < r_numModels; i++, model++) + if (model->type) + MOD_Free(model); r_numModels = 0; } @@ -1375,6 +1382,70 @@ static void MOD_Reference(model_t *model) model->registration_sequence = r_registration_sequence; } +static void MOD_UploadBuffer(model_t *model) +{ + size_t verts_size = 0; + size_t index_size = 0; + int i; + + for (i = 0; i < model->nummeshes; i++) { + const maliasmesh_t *mesh = &model->meshes[i]; + verts_size += sizeof(mesh->tcoords[0]) * mesh->numverts; + index_size += sizeof(mesh->indices[0]) * mesh->numindices; + } + +#if USE_MD5 + const md5_model_t *skel = model->skeleton; + if (skel) { + for (i = 0; i < skel->num_meshes; i++) { + const md5_mesh_t *mesh = &skel->meshes[i]; + verts_size += sizeof(mesh->tcoords[0]) * mesh->num_verts; + index_size += sizeof(mesh->indices[0]) * mesh->num_indices; + } + } +#endif + + GL_ClearErrors(); + + qglGenBuffers(1, &model->buffer); + GL_BindBuffer(GL_ARRAY_BUFFER, model->buffer); + qglBufferData(GL_ARRAY_BUFFER, verts_size + index_size, NULL, GL_STATIC_DRAW); + Com_DDPrintf("%s: %zu bytes buffer\n", model->name, verts_size + index_size); + + size_t verts_offset = 0; + size_t index_offset = verts_size; + + for (i = 0; i < model->nummeshes; i++) { + maliasmesh_t *mesh = &model->meshes[i]; + verts_size = sizeof(mesh->tcoords[0]) * mesh->numverts; + index_size = sizeof(mesh->indices[0]) * mesh->numindices; + qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); + qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); + mesh->tcoords = (maliastc_t *)verts_offset; + mesh->indices = (QGL_INDEX_TYPE *)index_offset; + verts_offset += verts_size; + index_offset += index_size; + } + +#if USE_MD5 + if (skel) { + for (i = 0; i < skel->num_meshes; i++) { + md5_mesh_t *mesh = &skel->meshes[i]; + verts_size = sizeof(mesh->tcoords[0]) * mesh->num_verts; + index_size = sizeof(mesh->indices[0]) * mesh->num_indices; + qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); + qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); + mesh->tcoords = (maliastc_t *)verts_offset; + mesh->indices = (QGL_INDEX_TYPE *)index_offset; + verts_offset += verts_size; + index_offset += index_size; + } + } +#endif + + GL_ShowErrors(__func__); +} + qhandle_t R_RegisterModel(const char *name) { char normalized[MAX_QPATH]; @@ -1475,6 +1546,9 @@ qhandle_t R_RegisterModel(const char *name) MOD_LoadMD5(model); #endif + if (model->type == MOD_ALIAS && !(gl_config.caps & QGL_CAP_CLIENT_VA)) + MOD_UploadBuffer(model); + done: index = (model - r_models) + 1; return index; diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 5828c9f49..30d525b55 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -86,7 +86,7 @@ static const glsection_t sections[] = { .ver_es = QGL_VER(1, 0), .excl_gl = QGL_VER(3, 1), .excl_es = QGL_VER(2, 0), - .caps = QGL_CAP_LEGACY, + .caps = QGL_CAP_LEGACY | QGL_CAP_CLIENT_VA, .functions = (const glfunction_t []) { QGL_FN(AlphaFunc), QGL_FN(Color4f), @@ -136,7 +136,7 @@ static const glsection_t sections[] = { // ES 1.1 { .ver_es = QGL_VER(1, 1), - .caps = QGL_CAP_TEXTURE_CLAMP_TO_EDGE, + .caps = QGL_CAP_TEXTURE_CLAMP_TO_EDGE | QGL_CAP_CLIENT_VA, }, // GL 1.2 @@ -254,6 +254,7 @@ static const glsection_t sections[] = { QGL_FN(GenFramebuffers), QGL_FN(GenRenderbuffers), QGL_FN(GenerateMipmap), + QGL_FN(GetFramebufferAttachmentParameteriv), QGL_FN(RenderbufferStorage), { NULL } } @@ -267,6 +268,9 @@ static const glsection_t sections[] = { // ensure full hardware support, including mipmaps. .caps = QGL_CAP_TEXTURE_MAX_LEVEL | QGL_CAP_TEXTURE_NON_POWER_OF_TWO, .functions = (const glfunction_t []) { + QGL_FN(BindVertexArray), + QGL_FN(DeleteVertexArrays), + QGL_FN(GenVertexArrays), QGL_FN(GetStringi), { NULL } } @@ -637,25 +641,18 @@ bool QGL_Init(void) // don't ever attempt to use shaders with GL ES < 3.0, or GLSL ES < 3.0 if (gl_config.ver_es < QGL_VER(3, 0) || gl_config.ver_sl < QGL_VER(3, 0)) gl_config.caps &= ~QGL_CAP_SHADER; - - // GL ES 3.0+ deprecates, but still supports client vertex arrays, thus - // pure QGL_CAP_SHADER mode will work. - if (!(gl_config.caps & (QGL_CAP_LEGACY | QGL_CAP_SHADER))) { - Com_EPrintf("Unsupported OpenGL ES version\n"); - return false; - } } else { // don't ever attempt to use shaders with GL < 3.0, or GLSL < 1.30 if (gl_config.ver_gl < QGL_VER(3, 0) || gl_config.ver_sl < QGL_VER(1, 30)) gl_config.caps &= ~QGL_CAP_SHADER; + } - // MUST have QGL_CAP_LEGACY bit, because Q2PRO still uses client vertex - // arrays removed by GL 3.1+. - if (!(gl_config.caps & QGL_CAP_LEGACY)) { - Com_EPrintf("Unsupported OpenGL version/profile\n"); - return false; - } + // reject unsupported configurations, such as GL ES 2.0 + if (!(gl_config.caps & (QGL_CAP_LEGACY | QGL_CAP_SHADER))) { + Com_EPrintf("Unsupported OpenGL version\n"); + return false; } + Com_DPrintf("Detected OpenGL capabilities: %#x\n", gl_config.caps); return true; } diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index 657cbd53a..57bf86fe6 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -140,14 +140,18 @@ QGLAPI void (APIENTRYP qglVertexAttribPointer)(GLuint index, GLint size, GLenum // GL 3.0 QGLAPI void (APIENTRYP qglBindFramebuffer)(GLenum target, GLuint framebuffer); QGLAPI void (APIENTRYP qglBindRenderbuffer)(GLenum target, GLuint renderbuffer); +QGLAPI void (APIENTRYP qglBindVertexArray)(GLuint array); QGLAPI GLenum (APIENTRYP qglCheckFramebufferStatus)(GLenum target); QGLAPI void (APIENTRYP qglDeleteFramebuffers)(GLsizei n, const GLuint *framebuffers); QGLAPI void (APIENTRYP qglDeleteRenderbuffers)(GLsizei n, const GLuint *renderbuffers); +QGLAPI void (APIENTRYP qglDeleteVertexArrays)(GLsizei n, const GLuint *arrays); QGLAPI void (APIENTRYP qglFramebufferRenderbuffer)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); QGLAPI void (APIENTRYP qglFramebufferTexture2D)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); QGLAPI void (APIENTRYP qglGenFramebuffers)(GLsizei n, GLuint *framebuffers); QGLAPI void (APIENTRYP qglGenRenderbuffers)(GLsizei n, GLuint *renderbuffers); +QGLAPI void (APIENTRYP qglGenVertexArrays)(GLsizei n, GLuint *arrays); QGLAPI void (APIENTRYP qglGenerateMipmap)(GLenum target); +QGLAPI void (APIENTRYP qglGetFramebufferAttachmentParameteriv)(GLenum target, GLenum attachment, GLenum pname, GLint *params); QGLAPI const GLubyte *(APIENTRYP qglGetStringi)(GLenum name, GLuint index); QGLAPI void (APIENTRYP qglRenderbufferStorage)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); diff --git a/src/refresh/state.c b/src/refresh/state.c index 5ee9a22a1..ef84251b3 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -268,7 +268,7 @@ void GL_Setup3D(bool waterwarp) qglClear(GL_DEPTH_BUFFER_BIT | gl_static.stencil_buffer_bit); } -void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices) +void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices, bool indexed) { GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE); @@ -278,7 +278,7 @@ void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices) if (qglPolygonMode) { qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - if (indices) + if (indexed) GL_DrawTriangles(count, indices); else qglDrawArrays(GL_TRIANGLES, 0, count); @@ -287,7 +287,7 @@ void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices) } else { GLsizei i; - if (indices) { + if (indexed) { for (i = 0; i < count / 3; i++) qglDrawElements(GL_LINE_LOOP, 3, QGL_INDEX_ENUM, &indices[i * 3]); } else { diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 82073dd64..3c85df783 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -819,11 +819,11 @@ static bool create_surface_vbo(size_t size) GL_ClearErrors(); qglGenBuffers(1, &buf); - qglBindBuffer(GL_ARRAY_BUFFER, buf); + GL_BindBuffer(GL_ARRAY_BUFFER, buf); qglBufferData(GL_ARRAY_BUFFER, size, NULL, GL_STATIC_DRAW); if (GL_ShowErrors("Failed to create world model VBO")) { - qglBindBuffer(GL_ARRAY_BUFFER, 0); + GL_BindBuffer(GL_ARRAY_BUFFER, 0); qglDeleteBuffers(1, &buf); return false; } @@ -868,7 +868,7 @@ static void upload_world_surfaces(void) LM_BeginBuilding(); if (!gl_static.world.vertices) - qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); + GL_BindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); currvert = 0; lastvert = 0; @@ -907,10 +907,8 @@ static void upload_world_surfaces(void) } // upload the last VBO chunk - if (!gl_static.world.vertices) { + if (!gl_static.world.vertices) upload_surface_vbo(lastvert); - qglBindBuffer(GL_ARRAY_BUFFER, 0); - } // end building lightmaps LM_EndBuilding(); @@ -973,9 +971,11 @@ void GL_FreeWorld(void) else if (qglDeleteBuffers) qglDeleteBuffers(1, &gl_static.world.bufnum); - // invalidate binding + // invalidate bindings if (gls.currentva == VA_3D) gls.currentva = VA_NONE; + if (gls.currentbuffer[0] == gl_static.world.bufnum) + gls.currentbuffer[0] = 0; memset(&gl_static.world, 0, sizeof(gl_static.world)); } diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 0743fa4c2..8404aaadf 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -28,6 +28,8 @@ static mface_t *faces_head[FACE_HASH_SIZE]; static mface_t **faces_next[FACE_HASH_SIZE]; static mface_t *faces_alpha; +static void GL_DrawIndexed(showtris_t showtris); + void GL_Flush2D(void) { glStateBits_t bits; @@ -45,15 +47,7 @@ void GL_Flush2D(void) GL_BindArrays(VA_2D); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - - GL_LockArrays(tess.numverts); - - GL_DrawTriangles(tess.numindices, tess.indices); - - if (gl_showtris->integer & SHOWTRIS_PIC) - GL_DrawOutlines(tess.numindices, tess.indices); - - GL_UnlockArrays(); + GL_DrawIndexed(SHOWTRIS_PIC); c.batchesDrawn2D++; @@ -133,7 +127,7 @@ void GL_DrawParticles(void) qglDrawArrays(GL_TRIANGLES, 0, numverts); if (gl_showtris->integer & SHOWTRIS_FX) - GL_DrawOutlines(numverts, NULL); + GL_DrawOutlines(numverts, NULL, false); GL_UnlockArrays(); } while (total); @@ -147,15 +141,7 @@ static void GL_FlushBeamSegments(void) GL_BindTexture(0, TEXNUM_BEAM); GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - - GL_LockArrays(tess.numverts); - - GL_DrawTriangles(tess.numindices, tess.indices); - - if (gl_showtris->integer & SHOWTRIS_FX) - GL_DrawOutlines(tess.numindices, tess.indices); - - GL_UnlockArrays(); + GL_DrawIndexed(SHOWTRIS_FX); tess.numverts = tess.numindices = 0; } @@ -294,15 +280,7 @@ static void GL_FlushFlares(void) GL_BindTexture(0, tess.texnum[0]); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - - GL_LockArrays(tess.numverts); - - GL_DrawTriangles(tess.numindices, tess.indices); - - if (gl_showtris->integer & SHOWTRIS_FX) - GL_DrawOutlines(tess.numindices, tess.indices); - - GL_UnlockArrays(); + GL_DrawIndexed(SHOWTRIS_FX); tess.numverts = tess.numindices = 0; tess.texnum[0] = 0; @@ -456,24 +434,94 @@ static const glVaDesc_t arraydescs[VA_TOTAL][VERT_ATTR_COUNT] = { void GL_BindArrays(glVertexArray_t va) { const GLfloat *ptr = tess.vertices; + GLuint buffer = 0; if (gls.currentva == va) return; if (va == VA_3D && !gl_static.world.vertices) { - qglBindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); + buffer = gl_static.world.bufnum; + ptr = NULL; + } else if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { + buffer = gl_static.vertex_buffer; ptr = NULL; } + GL_BindBuffer(GL_ARRAY_BUFFER, buffer); gl_backend->array_pointers(arraydescs[va], ptr); - if (!ptr) - qglBindBuffer(GL_ARRAY_BUFFER, 0); - gls.currentva = va; c.vertexArrayBinds++; } +void GL_LockArrays(GLsizei count) +{ + if (gls.currentva == VA_3D && !gl_static.world.vertices) + return; + if (gl_config.caps & QGL_CAP_CLIENT_VA) { + if (qglLockArraysEXT) + qglLockArraysEXT(0, count); + } else { + const glVaDesc_t *desc = &arraydescs[gls.currentva][VERT_ATTR_POS]; + GL_BindBuffer(GL_ARRAY_BUFFER, gl_static.vertex_buffer); + qglBufferData(GL_ARRAY_BUFFER, count * desc->stride, tess.vertices, GL_STREAM_DRAW); + } +} + +void GL_UnlockArrays(void) +{ + if (gls.currentva == VA_3D && !gl_static.world.vertices) + return; + if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) + return; + if (qglUnlockArraysEXT) + qglUnlockArraysEXT(); +} + +static void GL_DrawIndexed(showtris_t showtris) +{ + const QGL_INDEX_TYPE *indices = tess.indices; + + GL_LockArrays(tess.numverts); + + if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { + GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_static.index_buffer); + qglBufferData(GL_ELEMENT_ARRAY_BUFFER, tess.numindices * sizeof(indices[0]), indices, GL_STREAM_DRAW); + indices = NULL; + } + + GL_DrawTriangles(tess.numindices, indices); + + if (gl_showtris->integer & showtris) + GL_DrawOutlines(tess.numindices, indices, true); + + GL_UnlockArrays(); +} + +void GL_InitArrays(void) +{ + if (gl_config.caps & QGL_CAP_CLIENT_VA) + return; + + qglGenVertexArrays(1, &gl_static.array_object); + qglBindVertexArray(gl_static.array_object); + + qglGenBuffers(1, &gl_static.index_buffer); + GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_static.index_buffer); + + qglGenBuffers(1, &gl_static.vertex_buffer); +} + +void GL_ShutdownArrays(void) +{ + if (gl_config.caps & QGL_CAP_CLIENT_VA) + return; + + qglDeleteVertexArrays(1, &gl_static.array_object); + qglDeleteBuffers(1, &gl_static.index_buffer); + qglDeleteBuffers(1, &gl_static.vertex_buffer); +} + void GL_Flush3D(void) { glStateBits_t state = tess.flags; @@ -514,16 +562,7 @@ void GL_Flush3D(void) GL_BindTexture(i, tess.texnum[i]); } - if (gl_static.world.vertices) - GL_LockArrays(tess.numverts); - - GL_DrawTriangles(tess.numindices, tess.indices); - - if (gl_showtris->integer & SHOWTRIS_WORLD) - GL_DrawOutlines(tess.numindices, tess.indices); - - if (gl_static.world.vertices) - GL_UnlockArrays(); + GL_DrawIndexed(SHOWTRIS_WORLD); c.batchesDrawn++; From 85e1ad907e49263ff38fad3aa527f618d4ee6b0b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 23:59:17 +0300 Subject: [PATCH 585/974] Rename buffers for consistency. --- src/refresh/gl.h | 4 ++-- src/refresh/shader.c | 13 ++++++------- src/refresh/surf.c | 8 ++++---- src/refresh/tess.c | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index c37a99df1..80dedff6c 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -113,13 +113,13 @@ typedef struct { struct { bsp_t *cache; vec_t *vertices; - GLuint bufnum; + GLuint buffer; vec_t size; } world; GLuint warp_texture; GLuint warp_renderbuffer; GLuint warp_framebuffer; - GLuint u_bufnum; + GLuint uniform_buffer; GLuint array_object; GLuint index_buffer; GLuint vertex_buffer; diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 3e36e7800..7ddfc1efe 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -396,9 +396,9 @@ static void shader_clear_state(void) static void shader_init(void) { - qglGenBuffers(1, &gl_static.u_bufnum); - qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.u_bufnum); - qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.u_bufnum); + qglGenBuffers(1, &gl_static.uniform_buffer); + qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); + qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.uniform_buffer); qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), NULL, GL_DYNAMIC_DRAW); } @@ -412,10 +412,9 @@ static void shader_shutdown(void) } } - qglBindBuffer(GL_UNIFORM_BUFFER, 0); - if (gl_static.u_bufnum) { - qglDeleteBuffers(1, &gl_static.u_bufnum); - gl_static.u_bufnum = 0; + if (gl_static.uniform_buffer) { + qglDeleteBuffers(1, &gl_static.uniform_buffer); + gl_static.uniform_buffer = 0; } } diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 3c85df783..5e74a45b1 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -829,7 +829,7 @@ static bool create_surface_vbo(size_t size) } gl_static.world.vertices = NULL; - gl_static.world.bufnum = buf; + gl_static.world.buffer = buf; return true; } @@ -868,7 +868,7 @@ static void upload_world_surfaces(void) LM_BeginBuilding(); if (!gl_static.world.vertices) - GL_BindBuffer(GL_ARRAY_BUFFER, gl_static.world.bufnum); + GL_BindBuffer(GL_ARRAY_BUFFER, gl_static.world.buffer); currvert = 0; lastvert = 0; @@ -969,12 +969,12 @@ void GL_FreeWorld(void) if (gl_static.world.vertices) Z_Free(gl_static.world.vertices); else if (qglDeleteBuffers) - qglDeleteBuffers(1, &gl_static.world.bufnum); + qglDeleteBuffers(1, &gl_static.world.buffer); // invalidate bindings if (gls.currentva == VA_3D) gls.currentva = VA_NONE; - if (gls.currentbuffer[0] == gl_static.world.bufnum) + if (gls.currentbuffer[0] == gl_static.world.buffer) gls.currentbuffer[0] = 0; memset(&gl_static.world, 0, sizeof(gl_static.world)); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 8404aaadf..e56b21622 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -440,7 +440,7 @@ void GL_BindArrays(glVertexArray_t va) return; if (va == VA_3D && !gl_static.world.vertices) { - buffer = gl_static.world.bufnum; + buffer = gl_static.world.buffer; ptr = NULL; } else if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { buffer = gl_static.vertex_buffer; From 493d0949ea54d7417a492558a9e234ad4b79fe7e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 10:28:11 +0300 Subject: [PATCH 586/974] Allow disabling world model VBO for testing. --- src/refresh/gl.h | 1 + src/refresh/main.c | 2 ++ src/refresh/surf.c | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 80dedff6c..73e94f09d 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -258,6 +258,7 @@ extern cvar_t *gl_drawsky; extern cvar_t *gl_showtris; #if USE_DEBUG extern cvar_t *gl_nobind; +extern cvar_t *gl_novbo; extern cvar_t *gl_test; #endif extern cvar_t *gl_cull_nodes; diff --git a/src/refresh/main.c b/src/refresh/main.c index 8f01799d0..453831a04 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -74,6 +74,7 @@ cvar_t *gl_showtearing; cvar_t *gl_showstats; cvar_t *gl_showscrap; cvar_t *gl_nobind; +cvar_t *gl_novbo; cvar_t *gl_test; #endif cvar_t *gl_cull_nodes; @@ -902,6 +903,7 @@ static void GL_Register(void) gl_showstats = Cvar_Get("gl_showstats", "0", 0); gl_showscrap = Cvar_Get("gl_showscrap", "0", 0); gl_nobind = Cvar_Get("gl_nobind", "0", CVAR_CHEAT); + gl_novbo = Cvar_Get("gl_novbo", "0", CVAR_FILES); gl_test = Cvar_Get("gl_test", "0", 0); #endif gl_cull_nodes = Cvar_Get("gl_cull_nodes", "1", 0); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 5e74a45b1..c9f7d1752 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -816,6 +816,11 @@ static bool create_surface_vbo(size_t size) return false; #endif +#if USE_DEBUG + if (gl_novbo->integer) + return false; +#endif + GL_ClearErrors(); qglGenBuffers(1, &buf); From 83b83c9a38f1d94c81866166c448ab39ed8caef7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 10:28:40 +0300 Subject: [PATCH 587/974] Fix old rendering bug with world VBO disabled. --- src/refresh/tess.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index e56b21622..0d8840d50 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -622,11 +622,6 @@ static void GL_DrawFace(const mface_t *surf) tess.numindices + numindices > TESS_MAX_INDICES) GL_Flush3D(); - tess.texnum[0] = texnum[0]; - tess.texnum[1] = texnum[1]; - tess.texnum[2] = texnum[2]; - tess.flags = surf->statebits; - if (q_unlikely(gl_static.world.vertices)) j = GL_CopyVerts(surf); else @@ -641,6 +636,11 @@ static void GL_DrawFace(const mface_t *surf) } tess.numindices += numindices; + tess.texnum[0] = texnum[0]; + tess.texnum[1] = texnum[1]; + tess.texnum[2] = texnum[2]; + tess.flags = surf->statebits; + c.trisDrawn += numtris; c.facesTris += numtris; c.facesDrawn++; From 955c26a4542af9e47b44a3d65d3eaed1a31a4a76 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 16:43:50 +0300 Subject: [PATCH 588/974] Scale flares if in solid when occluding. Fixes flares disappearing on mguhub after 5b731eaa. --- src/refresh/main.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 453831a04..bb2a3a605 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -415,6 +415,7 @@ static void make_flare_quad(const entity_t *e, float scale) static void GL_OccludeFlares(void) { + const bsp_t *bsp = gl_static.world.cache; const entity_t *e; glquery_t *q; int i, j; @@ -464,7 +465,10 @@ static void GL_OccludeFlares(void) set = true; } - make_flare_quad(e, 2.5f); + if (bsp && BSP_PointLeaf(bsp->nodes, e->origin)->contents == CONTENTS_SOLID) + make_flare_quad(e, 2.5f * e->scale); + else + make_flare_quad(e, 2.5f); GL_LockArrays(4); qglBeginQuery(gl_static.samples_passed, q->query); From d836e588e1fcd8ea170ffcb459f91d726bec6816 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 21:14:14 +0300 Subject: [PATCH 589/974] Avoid binding non-existent textures. Prevents GL errors with core profile. --- src/refresh/texture.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 8c9a1fe0c..dcc36b7df 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -77,6 +77,8 @@ static void update_image_params(unsigned mask) const image_t *image; for (i = 0, image = r_images; i < r_numImages; i++, image++) { + if (!image->name[0]) + continue; if (!(mask & BIT(image->type))) continue; @@ -145,7 +147,8 @@ static void gl_bilerp_pics_changed(cvar_t *self) { // change all the existing pic texture objects update_image_params(BIT(IT_PIC)); - GL_InitRawTexture(); + if (r_numImages) + GL_InitRawTexture(); } static void gl_texturebits_changed(cvar_t *self) @@ -856,6 +859,7 @@ static void GL_InitDefaultTexture(void) // fill in notexture image ntx = R_NOTEXTURE; + strcpy(ntx->name, "NOTEXTURE"); ntx->width = ntx->upload_width = 8; ntx->height = ntx->upload_height = 8; ntx->type = IT_WALL; @@ -1050,6 +1054,10 @@ void GL_InitImages(void) else gl_intensity->flags |= CVAR_FILES; + gl_texturemode_changed(gl_texturemode); + gl_texturebits_changed(gl_texturebits); + gl_anisotropy_changed(gl_anisotropy); + IMG_Init(); IMG_GetPalette(); @@ -1068,12 +1076,6 @@ void GL_InitImages(void) colorscale = Cvar_ClampValue(gl_saturation, 0, 1); lightscale = !(gl_gamma->value == 1.0f && (gl_static.use_shaders || gl_intensity->value == 1.0f)); - gl_texturemode_changed(gl_texturemode); - gl_texturebits_changed(gl_texturebits); - gl_anisotropy_changed(gl_anisotropy); - gl_bilerp_chars_changed(gl_bilerp_chars); - gl_bilerp_pics_changed(gl_bilerp_pics); - qglGenTextures(NUM_TEXNUMS, gl_static.texnums); qglGenTextures(LM_MAX_LIGHTMAPS, lm.texnums); From 77b29de33c0a2212d32c5fe3a823604d06238c17 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 21:26:36 +0300 Subject: [PATCH 590/974] Clear auto textures on shutdown. --- src/refresh/images.c | 1 + src/refresh/texture.c | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/refresh/images.c b/src/refresh/images.c index 223cd8571..f54dff891 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -2244,5 +2244,6 @@ void IMG_Init(void) void IMG_Shutdown(void) { Cmd_Deregister(img_cmd); + memset(r_images, 0, sizeof(r_images[0])); // clear R_NOTEXTURE r_numImages = 0; } diff --git a/src/refresh/texture.c b/src/refresh/texture.c index dcc36b7df..081329bd6 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -1119,6 +1119,9 @@ void GL_ShutdownImages(void) qglDeleteTextures(NUM_TEXNUMS, gl_static.texnums); qglDeleteTextures(LM_MAX_LIGHTMAPS, lm.texnums); + memset(gl_static.texnums, 0, sizeof(gl_static.texnums)); + memset(lm.texnums, 0, sizeof(lm.texnums)); + GL_DeleteWarpTexture(); #if USE_DEBUG From 12a5012fde51194d9d72ccb05356a5d55c06ad2c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 20 Aug 2024 23:41:43 +0300 Subject: [PATCH 591/974] =?UTF-8?q?Make=20=E2=80=98gl=5Fnobind=201?= =?UTF-8?q?=E2=80=99=20work=20with=20multi=20bind.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/refresh/tess.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 0d8840d50..2afe33f9e 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -548,6 +548,10 @@ void GL_Flush3D(void) GL_ArrayBits(array); if (qglBindTextures) { +#if USE_DEBUG + if (q_unlikely(gl_nobind->integer)) + tess.texnum[0] = TEXNUM_DEFAULT; +#endif int count = 0; for (int i = 0; i < MAX_TMUS && tess.texnum[i]; i++) { if (gls.texnums[i] != tess.texnum[i]) { From 9937c5862f699b52ebd4c84906f2ef323d182876 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Aug 2024 02:38:31 +0300 Subject: [PATCH 592/974] Fix OpenGL ES 3.0 compatibility issue. --- src/refresh/texture.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 081329bd6..49f0d7967 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -963,6 +963,8 @@ static void GL_InitRawTexture(void) bool GL_InitWarpTexture(void) { + GL_ClearErrors(); + GL_ForceTexture(0, gl_static.warp_texture); qglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, glr.fd.width, glr.fd.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); qglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -974,7 +976,7 @@ bool GL_InitWarpTexture(void) qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl_static.warp_texture, 0); qglBindRenderbuffer(GL_RENDERBUFFER, gl_static.warp_renderbuffer); - qglRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_STENCIL, glr.fd.width, glr.fd.height); + qglRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, glr.fd.width, glr.fd.height); qglBindRenderbuffer(GL_RENDERBUFFER, 0); qglFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, gl_static.warp_renderbuffer); @@ -982,6 +984,8 @@ bool GL_InitWarpTexture(void) GLenum status = qglCheckFramebufferStatus(GL_FRAMEBUFFER); qglBindFramebuffer(GL_FRAMEBUFFER, 0); + GL_ShowErrors(__func__); + if (status != GL_FRAMEBUFFER_COMPLETE) { if (gl_showerrors->integer) Com_EPrintf("%s: framebuffer status %#x\n", __func__, status); From 147d7678a07f590b75ab5224f2f306fcfabda728 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Aug 2024 03:19:28 +0300 Subject: [PATCH 593/974] Stop nvidia driver debug notification spam. --- src/refresh/main.c | 2 ++ src/refresh/qgl.c | 1 + src/refresh/qgl.h | 1 + 3 files changed, 4 insertions(+) diff --git a/src/refresh/main.c b/src/refresh/main.c index bb2a3a605..dff7ae0a2 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -992,6 +992,8 @@ static void GL_SetupConfig(void) if (qglDebugMessageCallback && qglIsEnabled(GL_DEBUG_OUTPUT)) { Com_Printf("Enabling GL debug output.\n"); qglEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + if (Cvar_VariableInteger("gl_debug") < 2) + qglDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION, 0, NULL, GL_FALSE); qglDebugMessageCallback(myDebugProc, NULL); } } diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 30d525b55..58f8505de 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -311,6 +311,7 @@ static const glsection_t sections[] = { .ver_es = QGL_VER(3, 2), .functions = (const glfunction_t []) { QGL_FN(DebugMessageCallback), + QGL_FN(DebugMessageControl), { NULL } } }, diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index 57bf86fe6..bf28b7f21 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -167,6 +167,7 @@ QGLAPI void (APIENTRYP qglDepthRangef)(GLfloat n, GLfloat f); // GL 4.3 QGLAPI void (APIENTRYP qglDebugMessageCallback)(GLDEBUGPROC callback, const void *userParam); +QGLAPI void (APIENTRYP qglDebugMessageControl)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled); // GL 4.4 QGLAPI void (APIENTRYP qglBindTextures)(GLuint first, GLsizei count, const GLuint *textures); From 195959349716fd0c87a1f015f2114ca0e839d655 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 21 Aug 2024 15:58:12 +0300 Subject: [PATCH 594/974] Pass GL config struct by value. --- inc/refresh/refresh.h | 12 ++++++------ src/refresh/main.c | 34 +++++++++++++++++++--------------- src/unix/video/sdl.c | 3 +-- src/unix/video/wayland.c | 17 ++++++++--------- src/unix/video/x11.c | 17 ++++++++--------- src/windows/egl.c | 17 ++++++++--------- src/windows/wgl.c | 28 ++++++++++++++-------------- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index d4b8bd046..6f0bbbf37 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -110,11 +110,11 @@ typedef struct { } refdef_t; typedef struct { - int colorbits; - int depthbits; - int stencilbits; - int multisamples; - bool debug; + uint8_t colorbits; + uint8_t depthbits; + uint8_t stencilbits; + uint8_t multisamples; + bool debug; } r_opengl_config_t; typedef enum { @@ -221,4 +221,4 @@ void R_BeginFrame(void); void R_EndFrame(void); void R_ModeChanged(int width, int height, int flags); -void R_GetGLConfig(r_opengl_config_t *cfg); +r_opengl_config_t R_GetGLConfig(void); diff --git a/src/refresh/main.c b/src/refresh/main.c index dff7ae0a2..9fc263101 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1148,28 +1148,32 @@ void R_Shutdown(bool total) R_GetGLConfig =============== */ -void R_GetGLConfig(r_opengl_config_t *cfg) +r_opengl_config_t R_GetGLConfig(void) { - memset(cfg, 0, sizeof(*cfg)); +#define GET_CVAR(name, def, min, max) \ + Cvar_ClampInteger(Cvar_Get(name, def, CVAR_REFRESH), min, max) - cfg->colorbits = Cvar_ClampInteger(Cvar_Get("gl_colorbits", "0", CVAR_REFRESH), 0, 32); - cfg->depthbits = Cvar_ClampInteger(Cvar_Get("gl_depthbits", "0", CVAR_REFRESH), 0, 32); - cfg->stencilbits = Cvar_ClampInteger(Cvar_Get("gl_stencilbits", "8", CVAR_REFRESH), 0, 8); - cfg->multisamples = Cvar_ClampInteger(Cvar_Get("gl_multisamples", "0", CVAR_REFRESH), 0, 32); + r_opengl_config_t cfg = { + .colorbits = GET_CVAR("gl_colorbits", "0", 0, 32), + .depthbits = GET_CVAR("gl_depthbits", "0", 0, 32), + .stencilbits = GET_CVAR("gl_stencilbits", "8", 0, 8), + .multisamples = GET_CVAR("gl_multisamples", "0", 0, 32), + .debug = GET_CVAR("gl_debug", "0", 0, 2), + }; - if (cfg->colorbits == 0) - cfg->colorbits = 24; + if (cfg.colorbits == 0) + cfg.colorbits = 24; - if (cfg->depthbits == 0) - cfg->depthbits = cfg->colorbits > 16 ? 24 : 16; + if (cfg.depthbits == 0) + cfg.depthbits = cfg.colorbits > 16 ? 24 : 16; - if (cfg->depthbits < 24) - cfg->stencilbits = 0; + if (cfg.depthbits < 24) + cfg.stencilbits = 0; - if (cfg->multisamples < 2) - cfg->multisamples = 0; + if (cfg.multisamples < 2) + cfg.multisamples = 0; - cfg->debug = Cvar_Get("gl_debug", "0", CVAR_REFRESH)->integer; + return cfg; } /* diff --git a/src/unix/video/sdl.c b/src/unix/video/sdl.c index b9ef5ba96..4a30d417d 100644 --- a/src/unix/video/sdl.c +++ b/src/unix/video/sdl.c @@ -59,8 +59,7 @@ OPENGL STUFF static void set_gl_attributes(void) { - r_opengl_config_t cfg; - R_GetGLConfig(&cfg); + r_opengl_config_t cfg = R_GetGLConfig(); int colorbits = cfg.colorbits > 16 ? 8 : 5; SDL_GL_SetAttribute(SDL_GL_RED_SIZE, colorbits); diff --git a/src/unix/video/wayland.c b/src/unix/video/wayland.c index a87e04312..8db148414 100644 --- a/src/unix/video/wayland.c +++ b/src/unix/video/wayland.c @@ -719,7 +719,7 @@ static void egl_error(const char *what) Com_EPrintf("%s failed with error %#x\n", what, eglGetError()); } -static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) +static bool choose_config(r_opengl_config_t cfg, EGLConfig *config) { EGLint cfg_attr[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, @@ -727,10 +727,10 @@ static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) EGL_RED_SIZE, 5, EGL_GREEN_SIZE, 5, EGL_BLUE_SIZE, 5, - EGL_DEPTH_SIZE, cfg->depthbits, - EGL_STENCIL_SIZE, cfg->stencilbits, - EGL_SAMPLE_BUFFERS, (bool)cfg->multisamples, - EGL_SAMPLES, cfg->multisamples, + EGL_DEPTH_SIZE, cfg.depthbits, + EGL_STENCIL_SIZE, cfg.stencilbits, + EGL_SAMPLE_BUFFERS, (bool)cfg.multisamples, + EGL_SAMPLES, cfg.multisamples, EGL_NONE }; @@ -781,14 +781,13 @@ static bool init(void) CHECK_EGL(eglBindAPI(EGL_OPENGL_API), "eglBindAPI"); - r_opengl_config_t cfg; - R_GetGLConfig(&cfg); + r_opengl_config_t cfg = R_GetGLConfig(); EGLConfig config; - if (!choose_config(&cfg, &config)) { + if (!choose_config(cfg, &config)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; - if (!choose_config(&failsafe, &config)) + if (!choose_config(failsafe, &config)) goto fail; } diff --git a/src/unix/video/x11.c b/src/unix/video/x11.c index 297e7c795..f5cdaca17 100644 --- a/src/unix/video/x11.c +++ b/src/unix/video/x11.c @@ -183,7 +183,7 @@ static int error_handler(Display *dpy, XErrorEvent *event) return 0; } -static bool choose_fb_config(const r_opengl_config_t *cfg, GLXFBConfig *fbc) +static bool choose_fb_config(r_opengl_config_t cfg, GLXFBConfig *fbc) { int glx_attr[] = { GLX_X_RENDERABLE, True, @@ -192,14 +192,14 @@ static bool choose_fb_config(const r_opengl_config_t *cfg, GLXFBConfig *fbc) GLX_GREEN_SIZE, 5, GLX_BLUE_SIZE, 5, GLX_DOUBLEBUFFER, True, - GLX_DEPTH_SIZE, cfg->depthbits, - GLX_STENCIL_SIZE, cfg->stencilbits, + GLX_DEPTH_SIZE, cfg.depthbits, + GLX_STENCIL_SIZE, cfg.stencilbits, GLX_SAMPLE_BUFFERS, 1, - GLX_SAMPLES, cfg->multisamples, + GLX_SAMPLES, cfg.multisamples, None }; - if (!cfg->multisamples) + if (!cfg.multisamples) glx_attr[16] = None; int num_configs; @@ -292,8 +292,7 @@ static bool init(void) x11.extensions = glx_parse_extension_string(glXQueryExtensionsString(x11.dpy, x11.screen)); - r_opengl_config_t cfg; - R_GetGLConfig(&cfg); + r_opengl_config_t cfg = R_GetGLConfig(); if (cfg.multisamples && !(x11.extensions & QGLX_ARB_multisample)) { Com_WPrintf("GLX_ARB_multisample not found for %d multisamples\n", cfg.multisamples); @@ -301,10 +300,10 @@ static bool init(void) } GLXFBConfig fbc; - if (!choose_fb_config(&cfg, &fbc)) { + if (!choose_fb_config(cfg, &fbc)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; - if (!choose_fb_config(&failsafe, &fbc)) + if (!choose_fb_config(failsafe, &fbc)) goto fail; } diff --git a/src/windows/egl.c b/src/windows/egl.c index 748d2edc3..18f717a38 100644 --- a/src/windows/egl.c +++ b/src/windows/egl.c @@ -53,7 +53,7 @@ static void print_error(const char *what) Com_EPrintf("%s failed with error %#x\n", what, qeglGetError()); } -static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) +static bool choose_config(r_opengl_config_t cfg, EGLConfig *config) { EGLint cfg_attr[] = { EGL_SURFACE_TYPE, EGL_WINDOW_BIT, @@ -61,10 +61,10 @@ static bool choose_config(const r_opengl_config_t *cfg, EGLConfig *config) EGL_RED_SIZE, 5, EGL_GREEN_SIZE, 5, EGL_BLUE_SIZE, 5, - EGL_DEPTH_SIZE, cfg->depthbits, - EGL_STENCIL_SIZE, cfg->stencilbits, - EGL_SAMPLE_BUFFERS, (bool)cfg->multisamples, - EGL_SAMPLES, cfg->multisamples, + EGL_DEPTH_SIZE, cfg.depthbits, + EGL_STENCIL_SIZE, cfg.stencilbits, + EGL_SAMPLE_BUFFERS, (bool)cfg.multisamples, + EGL_SAMPLES, cfg.multisamples, EGL_NONE }; @@ -117,14 +117,13 @@ static bool egl_init(void) goto fail; } - r_opengl_config_t cfg; - R_GetGLConfig(&cfg); + r_opengl_config_t cfg = R_GetGLConfig(); EGLConfig config; - if (!choose_config(&cfg, &config)) { + if (!choose_config(cfg, &config)) { Com_Printf("Falling back to failsafe config\n"); r_opengl_config_t failsafe = { .depthbits = 24 }; - if (!choose_config(&failsafe, &config)) + if (!choose_config(failsafe, &config)) goto fail; } diff --git a/src/windows/wgl.c b/src/windows/wgl.c index 0eb8ff101..06b44e80e 100644 --- a/src/windows/wgl.c +++ b/src/windows/wgl.c @@ -77,7 +77,7 @@ static void print_error(const char *what) #define FAIL_SOFT -1 #define FAIL_HARD -2 -static int wgl_setup_gl(const r_opengl_config_t *cfg) +static int wgl_setup_gl(r_opengl_config_t cfg) { PIXELFORMATDESCRIPTOR pfd; int pixelformat; @@ -86,17 +86,17 @@ static int wgl_setup_gl(const r_opengl_config_t *cfg) Win_Init(); // choose pixel format - if (wgl.ChoosePixelFormatARB && cfg->multisamples) { + if (wgl.ChoosePixelFormatARB && cfg.multisamples) { int attr[] = { WGL_DRAW_TO_WINDOW_ARB, TRUE, WGL_SUPPORT_OPENGL_ARB, TRUE, WGL_DOUBLE_BUFFER_ARB, TRUE, WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, - WGL_COLOR_BITS_ARB, cfg->colorbits, - WGL_DEPTH_BITS_ARB, cfg->depthbits, - WGL_STENCIL_BITS_ARB, cfg->stencilbits, + WGL_COLOR_BITS_ARB, cfg.colorbits, + WGL_DEPTH_BITS_ARB, cfg.depthbits, + WGL_STENCIL_BITS_ARB, cfg.stencilbits, WGL_SAMPLE_BUFFERS_ARB, 1, - WGL_SAMPLES_ARB, cfg->multisamples, + WGL_SAMPLES_ARB, cfg.multisamples, 0 }; UINT num_formats; @@ -106,7 +106,7 @@ static int wgl_setup_gl(const r_opengl_config_t *cfg) goto soft; } if (num_formats == 0) { - Com_EPrintf("No suitable OpenGL pixelformat found for %d multisamples\n", cfg->multisamples); + Com_EPrintf("No suitable OpenGL pixelformat found for %d multisamples\n", cfg.multisamples); goto soft; } } else { @@ -115,9 +115,9 @@ static int wgl_setup_gl(const r_opengl_config_t *cfg) .nVersion = 1, .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, .iPixelType = PFD_TYPE_RGBA, - .cColorBits = cfg->colorbits, - .cDepthBits = cfg->depthbits, - .cStencilBits = cfg->stencilbits, + .cColorBits = cfg.colorbits, + .cDepthBits = cfg.depthbits, + .cStencilBits = cfg.stencilbits, .iLayerType = PFD_MAIN_PLANE, }; @@ -152,7 +152,7 @@ static int wgl_setup_gl(const r_opengl_config_t *cfg) } // startup the OpenGL subsystem by creating a context and making it current - if (wgl.CreateContextAttribsARB && cfg->debug) { + if (wgl.CreateContextAttribsARB && cfg.debug) { int attr[] = { WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB, 0 @@ -279,7 +279,7 @@ static bool wgl_init(void) return false; } - R_GetGLConfig(&cfg); + cfg = R_GetGLConfig(); // check for extensions by creating a fake window if (cfg.multisamples || cfg.debug) @@ -303,7 +303,7 @@ static bool wgl_init(void) } // create window, choose PFD, setup OpenGL context - ret = wgl_setup_gl(&cfg); + ret = wgl_setup_gl(cfg); // attempt to recover if (ret == FAIL_SOFT) { @@ -312,7 +312,7 @@ static bool wgl_init(void) .colorbits = 24, .depthbits = 24, }; - ret = wgl_setup_gl(&failsafe); + ret = wgl_setup_gl(failsafe); } if (ret) { From f820deed19d2c2d44e750a6ce58e72227dd5d951 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 19 Aug 2024 19:33:06 +0300 Subject: [PATCH 595/974] Allow selecting custom OpenGL profile and version. --- doc/client.asciidoc | 16 +++++++++++++++ inc/refresh/refresh.h | 9 +++++++++ src/refresh/main.c | 23 +++++++++++++++++++++ src/unix/video/sdl.c | 15 +++++++------- src/unix/video/x11.c | 41 ++++++++++++++++++++++++++++--------- src/windows/wgl.c | 47 ++++++++++++++++++++++++++++++++----------- 6 files changed, 122 insertions(+), 29 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 2a0a44e09..cd9830136 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -830,6 +830,22 @@ gl_texturebits:: (should be typically 0, 8, 16 or 32). Default value is 0 (choose the best internal format automatically). +gl_debug:: + Create debug OpenGL context, if supported by OpenGL implementation. + This option may hurt performance and is intended for developers only. + Default value is 0. + +gl_profile:: + Specifies custom profile and version of OpenGL context to create. Default + value is empty string, which means to create highest version compatibility + profile context. Can be set to "gl" to create OpenGL core profile context, + or "es" to create OpenGL ES context. May be followed by desired minimum + OpenGL version number. For example, "gl4.6" or "es3.0". Not all OpenGL + implementations support this. + +NOTE: Using OpenGL compatibility profile is recommended. Some features may not +work in more restricted profiles, or performance may be suboptimal. + gl_screenshot_format:: Specifies image format ‘screenshot’ command uses. Possible values are "png", "jpg" and "tga". Default value is "jpg". diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 6f0bbbf37..6dbd8b229 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -109,12 +109,21 @@ typedef struct { particle_t *particles; } refdef_t; +enum { + QGL_PROFILE_NONE, + QGL_PROFILE_CORE, + QGL_PROFILE_ES, +}; + typedef struct { uint8_t colorbits; uint8_t depthbits; uint8_t stencilbits; uint8_t multisamples; bool debug; + uint8_t profile; + uint8_t major_ver; + uint8_t minor_ver; } r_opengl_config_t; typedef enum { diff --git a/src/refresh/main.c b/src/refresh/main.c index 9fc263101..a079e4afa 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1173,6 +1173,29 @@ r_opengl_config_t R_GetGLConfig(void) if (cfg.multisamples < 2) cfg.multisamples = 0; + const char *s = Cvar_Get("gl_profile", "", CVAR_REFRESH)->string; + + if (!Q_stricmpn(s, "gl", 2)) + cfg.profile = QGL_PROFILE_CORE; + else if (!Q_stricmpn(s, "es", 2)) + cfg.profile = QGL_PROFILE_ES; + + if (cfg.profile) { + int major = 0, minor = 0; + + sscanf(s + 2, "%d.%d", &major, &minor); + if (major >= 1 && minor >= 0) { + cfg.major_ver = major; + cfg.minor_ver = minor; + } else if (cfg.profile == QGL_PROFILE_CORE) { + cfg.major_ver = 3; + cfg.minor_ver = 2; + } else if (cfg.profile == QGL_PROFILE_ES) { + cfg.major_ver = 3; + cfg.minor_ver = 0; + } + } + return cfg; } diff --git a/src/unix/video/sdl.c b/src/unix/video/sdl.c index 4a30d417d..57cfa3227 100644 --- a/src/unix/video/sdl.c +++ b/src/unix/video/sdl.c @@ -74,15 +74,16 @@ static void set_gl_attributes(void) SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples); } - if (cfg.debug) { + + if (cfg.debug) SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - } -#if USE_GLES - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1); -#endif + if (cfg.profile) { + if (cfg.profile == QGL_PROFILE_ES) + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, cfg.major_ver); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, cfg.minor_ver); + } } static void *get_proc_addr(const char *sym) diff --git a/src/unix/video/x11.c b/src/unix/video/x11.c index f5cdaca17..09a730824 100644 --- a/src/unix/video/x11.c +++ b/src/unix/video/x11.c @@ -78,10 +78,11 @@ static struct { } x11; enum { - QGLX_ARB_create_context = BIT(0), - QGLX_ARB_multisample = BIT(1), - QGLX_EXT_swap_control = BIT(2), - QGLX_EXT_swap_control_tear = BIT(3), + QGLX_ARB_create_context = BIT(0), + QGLX_ARB_multisample = BIT(1), + QGLX_EXT_create_context_es_profile = BIT(2), + QGLX_EXT_swap_control = BIT(3), + QGLX_EXT_swap_control_tear = BIT(4), }; static unsigned glx_parse_extension_string(const char *s) @@ -89,6 +90,7 @@ static unsigned glx_parse_extension_string(const char *s) static const char *const extnames[] = { "GLX_ARB_create_context", "GLX_ARB_multisample", + "GLX_EXT_create_context_es_profile", "GLX_EXT_swap_control", "GLX_EXT_swap_control_tear", NULL @@ -379,20 +381,39 @@ static bool init(void) XFree(list); } - if (cfg.debug) { + if (cfg.profile == QGL_PROFILE_ES && !(x11.extensions & QGLX_EXT_create_context_es_profile)) { + Com_WPrintf("GLX_EXT_create_context_es_profile not found\n"); + cfg.profile = QGL_PROFILE_NONE; + } + + if (cfg.debug || cfg.profile) { PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB = NULL; if (x11.extensions & QGLX_ARB_create_context) glXCreateContextAttribsARB = get_proc_addr("glXCreateContextAttribsARB"); if (glXCreateContextAttribsARB) { - int ctx_attr[] = { - GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, - None - }; + int ctx_attr[9]; + int i = 0; + + if (cfg.profile) { + ctx_attr[i++] = GLX_CONTEXT_MAJOR_VERSION_ARB; + ctx_attr[i++] = cfg.major_ver; + ctx_attr[i++] = GLX_CONTEXT_MINOR_VERSION_ARB; + ctx_attr[i++] = cfg.minor_ver; + } + if (cfg.profile == QGL_PROFILE_ES) { + ctx_attr[i++] = GLX_CONTEXT_PROFILE_MASK_ARB; + ctx_attr[i++] = GLX_CONTEXT_ES_PROFILE_BIT_EXT; + } + if (cfg.debug) { + ctx_attr[i++] = GLX_CONTEXT_FLAGS_ARB; + ctx_attr[i++] = GLX_CONTEXT_DEBUG_BIT_ARB; + } + ctx_attr[i] = None; if (!(x11.ctx = glXCreateContextAttribsARB(x11.dpy, fbc, NULL, True, ctx_attr))) - Com_EPrintf("Failed to create debug GL context\n"); + Com_EPrintf("Failed to create GL context with attributes\n"); } else { Com_WPrintf("GLX_ARB_create_context not found\n"); } diff --git a/src/windows/wgl.c b/src/windows/wgl.c index 06b44e80e..4c526924a 100644 --- a/src/windows/wgl.c +++ b/src/windows/wgl.c @@ -33,11 +33,12 @@ static struct { static cvar_t *gl_allow_software; enum { - QWGL_ARB_create_context = BIT(0), - QWGL_ARB_multisample = BIT(1), - QWGL_ARB_pixel_format = BIT(2), - QWGL_EXT_swap_control = BIT(3), - QWGL_EXT_swap_control_tear = BIT(4), + QWGL_ARB_create_context = BIT(0), + QWGL_ARB_multisample = BIT(1), + QWGL_ARB_pixel_format = BIT(2), + QWGL_EXT_create_context_es_profile = BIT(3), + QWGL_EXT_swap_control = BIT(4), + QWGL_EXT_swap_control_tear = BIT(5), }; static unsigned wgl_parse_extension_string(const char *s) @@ -46,6 +47,7 @@ static unsigned wgl_parse_extension_string(const char *s) "WGL_ARB_create_context", "WGL_ARB_multisample", "WGL_ARB_pixel_format", + "WGL_EXT_create_context_es_profile", "WGL_EXT_swap_control", "WGL_EXT_swap_control_tear", NULL @@ -152,11 +154,26 @@ static int wgl_setup_gl(r_opengl_config_t cfg) } // startup the OpenGL subsystem by creating a context and making it current - if (wgl.CreateContextAttribsARB && cfg.debug) { - int attr[] = { - WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB, - 0 - }; + if (wgl.CreateContextAttribsARB && (cfg.debug || cfg.profile)) { + int attr[9]; + int i = 0; + + if (cfg.profile) { + attr[i++] = WGL_CONTEXT_MAJOR_VERSION_ARB; + attr[i++] = cfg.major_ver; + attr[i++] = WGL_CONTEXT_MINOR_VERSION_ARB; + attr[i++] = cfg.minor_ver; + } + if (cfg.profile == QGL_PROFILE_ES) { + attr[i++] = WGL_CONTEXT_PROFILE_MASK_ARB; + attr[i++] = WGL_CONTEXT_ES_PROFILE_BIT_EXT; + } + if (cfg.debug) { + attr[i++] = WGL_CONTEXT_FLAGS_ARB; + attr[i++] = WGL_CONTEXT_DEBUG_BIT_ARB; + } + attr[i] = 0; + if (!(wgl.context = wgl.CreateContextAttribsARB(win.dc, NULL, attr))) { print_error("wglCreateContextAttribsARB"); goto soft; @@ -282,7 +299,7 @@ static bool wgl_init(void) cfg = R_GetGLConfig(); // check for extensions by creating a fake window - if (cfg.multisamples || cfg.debug) + if (cfg.multisamples || cfg.debug || cfg.profile) fake_extensions = get_fake_window_extensions(); if (cfg.multisamples) { @@ -297,9 +314,15 @@ static bool wgl_init(void) } } - if (cfg.debug && !wgl.CreateContextAttribsARB) { + if ((cfg.debug || cfg.profile) && !wgl.CreateContextAttribsARB) { Com_WPrintf("WGL_ARB_create_context not found\n"); cfg.debug = false; + cfg.profile = QGL_PROFILE_NONE; + } + + if (cfg.profile == QGL_PROFILE_ES && !(fake_extensions & QWGL_EXT_create_context_es_profile)) { + Com_WPrintf("WGL_EXT_create_context_es_profile not found\n"); + cfg.profile = QGL_PROFILE_NONE; } // create window, choose PFD, setup OpenGL context From fa975da2090b55aeb916f4a794c48c12ef52894f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 22 Aug 2024 15:18:52 -0400 Subject: [PATCH 596/974] Added a bot count manager, untested --- src/action/a_team.c | 9 +++-- src/action/acesrc/acebot.h | 3 ++ src/action/botlib/botlib_spawn.c | 63 ++++++++++++++++++++++++++++---- src/action/botlib/botlib_utils.c | 8 +--- src/action/g_main.c | 3 ++ src/action/g_save.c | 3 ++ 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index a709e82c5..6d6f6d1c7 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2385,6 +2385,12 @@ static void StartLCA(void) SpawnPlayers(); if (esp->value) { + #ifndef NO_BOTS + int l; + for (l = 1; l < MAX_TEAMS; l++) { + teamplay_spawn_node[l] = ACEND_FindClosestReachableNode(chosenSpawnpoint[l], NODE_DENSITY, NODE_ALL); + } + #endif esp_punishment_phase = false; EspResetCapturePoint(); EspAnnounceDetails(false); @@ -4206,9 +4212,6 @@ void NS_SetupTeamSpawnPoints (void) for (l = 0; l < MAX_TEAMS; l++) { teamplay_spawns[l] = NULL; teams_assigned[l] = false; - #ifndef NO_BOTS - teamplay_spawn_node[l] = ACEND_FindClosestReachableNode(chosenSpawnpoint[l], NODE_DENSITY, NODE_ALL); - #endif } if (NS_SelectRandomTeamplaySpawnPoint (NS_randteam, teams_assigned) == false) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index f61f2fe7d..d32d4dc9f 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -548,6 +548,9 @@ extern cvar_t* bot_personality; extern cvar_t* bot_ragequit; extern cvar_t* bot_countashuman; extern cvar_t* bot_debug; +extern cvar_t* bot_count_min; +extern cvar_t* bot_count_max; +extern cvar_t* bot_rotate; //extern cvar_t* bot_randteamskin; #define MAX_BOT_NAMES 64 diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 968f9ca4d..d940f6874 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -26,7 +26,7 @@ edict_t* BOTLIB_FindFreeEntity(void) qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) { FILE* f; - int comments_num = 0; // Keep track of how many comments the file has + //int comments_num = 0; // Keep track of how many comments the file has int line_num = 0; // Keep track of lines int curr_len; // Current length of the line char curr_line[1024]; // Accounts for reading lines that could be fairly long (comments) @@ -35,8 +35,8 @@ qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib char filename[MAX_QPATH]; // Filename to load from - int i; // Keep track where we are in the filename array #ifdef _WIN32 + int i; // Keep track where we are in the filename array i = sprintf(filename, ".\\"); i += sprintf(filename + i, game_dir->string); i += sprintf(filename + i, "\\"); @@ -63,7 +63,7 @@ qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) // Find how many lines the file has int random_line; - while (fgets(curr_line, sizeof(curr_line), f)) + while (fgets(curr_line, sizeof(curr_line), f) != NULL) { line_num++; } @@ -79,7 +79,7 @@ qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) line_num = -1; // Read each line - while (fgets(curr_line, sizeof(curr_line), f)) + while (fgets(curr_line, sizeof(curr_line), f) != NULL) { line_num++; // Advance forward @@ -189,6 +189,49 @@ qboolean BOTLIB_SaveBotsFromPreviousMap(void) return true; } +int BOTLIB_BotCountManager(void) +{ + // If we aren't rotating bots, just return existing desire_bots value + if (!bot_rotate->value) + return bot_connections.desire_bots; + + int bcmin = bot_count_min->value; + int bcmax = bot_count_max->value; + int bc = bot_connections.desire_bots; + // int bc1 = bot_connections.desire_team1; + // int bc2 = bot_connections.desire_team2; + // int bc3 = bot_connections.desire_team3; + // int tbots = bot_connections.total_bots; + + // Basic value validation + if (bcmin < 0) { + gi.dprintf("%s: bot_count_min < 0, setting it to 0\n", __func__); + bcmin = 0; + } + if (bcmax > maxclients->value) { + gi.dprintf("%s: bot_count_max > maxclients, setting it to maxclients\n", __func__); + bcmax = maxclients->value; + } + if (bcmin > bcmax) { + gi.dprintf("%s: bot_count_min > bot_count_max, setting them to be the same\n", __func__); + gi.cvar_forceset(bot_count_min->string, va("%s", bot_count_max->string)); + bcmin = bcmax; + } + + // Choose a random number between bcmin and bcmax, minimum count of bots is bcmin + int bot_count_rotate = bcmin + rand() % (bcmax - bcmin + 1); + + + // Make the change + if (bot_count_rotate == bc){ + return bc; // No change + } else { // Set desired bots to be how many we rotate in or out + bot_connections.desire_bots = bot_count_rotate; + return bot_count_rotate; + } + +} + // Add bots from the previous map from file // Parameters: // percent of bots to keep from previous map @@ -216,8 +259,9 @@ qboolean BOTLIB_AddBotsFromPreviousMap(float percent) cvar_t* game_dir = gi.cvar("game", "action", 0); // Directory of the gamelib cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib char filename[MAX_QPATH]; // Filename to load from - int i; // Keep track where we are in the filename array + #ifdef _WIN32 + int i; // Keep track where we are in the filename array i = sprintf(filename, ".\\"); i += sprintf(filename + i, game_dir->string); i += sprintf(filename + i, "\\"); @@ -407,7 +451,7 @@ void BOTLIB_RandomizeTeamNames(edict_t* bot) int len = 0; const int max_name_size = sizeof(teams[TEAM1].name); // Team names are limited to 20 characters (19 chars + terminator) - qboolean same_name = false; + //qboolean same_name = false; for (int i = 0; i < MAX_TEAMS; i++) // For each team { // Try for random team names that fit within 18 characters @@ -2012,7 +2056,12 @@ void BOTLIB_CheckBotRules(void) //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); - int bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + int bots_to_spawn = 0; + if (bot_rotate->value) { + bots_to_spawn = BOTLIB_BotCountManager(); + } else { + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + } // Shuffle bots around //if (teamplay->value && bots_to_spawn == 0) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index ec833799d..beee25718 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -11,9 +11,5 @@ void BOTLIB_Debug(const char *debugmsg, ...) { if (!bot_debug->value) return; - - va_list argptr; - va_start(argptr, debugmsg); - Com_Printf(debugmsg, argptr); - va_end(argptr); -} \ No newline at end of file + gi.dprintf(debugmsg); +} diff --git a/src/action/g_main.c b/src/action/g_main.c index 022bed5de..5bed12941 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -498,6 +498,9 @@ cvar_t* bot_personality; // Enable bot personality functionality [0 disable, 1 cvar_t* bot_ragequit; // Enable bot rage quitting (requires bot_personality to be enabled as well) cvar_t* bot_countashuman; // Allows bots to play teamplay games without humans (see a_vote.c _numclients() ) also enforces timelimit on DM games cvar_t* bot_debug; // Enable bot debug mode +cvar_t* bot_count_min; // Minimum number of bots to keep on the server (will range between this and bot_count_max) +cvar_t* bot_count_max; // Maximum number of bots to keep on the server (will range between this and bot_count_min) +cvar_t* bot_rotate; // Disable/enable rotating bots on the server //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index efd0ad16f..d04c1e71b 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -700,6 +700,9 @@ void InitGame( void ) bot_ragequit = gi.cvar("bot_ragequit", "0", 0); bot_countashuman = gi.cvar("bot_countashuman", "0", 0); bot_debug = gi.cvar("bot_debug", "0", 0); + bot_count_min = gi.cvar("bot_count_min", "0", 0); + bot_count_max = gi.cvar("bot_count_max", "0", 0); + bot_rotate = gi.cvar("bot_rotate", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From f458fa69b75c8d8e176808ca30cbde6ab84c7c4b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 11:37:50 +0300 Subject: [PATCH 597/974] Re-enable dynamic lights w/o unpack subimage. --- src/refresh/main.c | 4 ---- src/refresh/surf.c | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index a079e4afa..2f9eaa472 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -812,10 +812,6 @@ static size_t GL_ViewCluster_m(char *buffer, size_t size) static void gl_lightmap_changed(cvar_t *self) { - if (gl_dynamic->integer && !(gl_config.caps & QGL_CAP_UNPACK_SUBIMAGE)) { - Com_WPrintf("OpenGL doesn't support dynamic lights, forcing them off.\n"); - Cvar_Set("gl_dynamic", "0"); - } lm.scale = Cvar_ClampValue(gl_coloredlightmaps, 0, 1); lm.comp = !(gl_config.caps & QGL_CAP_TEXTURE_BITS) ? GL_RGBA : lm.scale ? GL_RGB : GL_LUMINANCE; lm.add = 255 * Cvar_ClampValue(gl_brightness, -1, 1); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index c9f7d1752..a35253363 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -313,7 +313,10 @@ void GL_UploadLightmaps(void) w = m->maxs[0] - x; h = m->maxs[1] - y; - if (!set) { + if (!(gl_config.caps & QGL_CAP_UNPACK_SUBIMAGE)) { + x = 0; + w = lm.block_size; + } else if (!set) { qglPixelStorei(GL_UNPACK_ROW_LENGTH, lm.block_size); set = true; } From d7b3a0d162623932696c9f0c08ee417890a18221 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 11:39:42 +0300 Subject: [PATCH 598/974] Check for GL_UNSIGNED_INT indices support for GLES 1. --- src/refresh/gl.h | 5 +++-- src/refresh/qgl.c | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 73e94f09d..487d0ddef 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -175,8 +175,9 @@ typedef enum { QGL_CAP_TEXTURE_NON_POWER_OF_TWO = BIT(6), QGL_CAP_TEXTURE_ANISOTROPY = BIT(7), QGL_CAP_UNPACK_SUBIMAGE = BIT(8), - QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(9), - QGL_CAP_CLIENT_VA = BIT(10), + QGL_CAP_ELEMENT_INDEX_UINT = BIT(9), + QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(10), + QGL_CAP_CLIENT_VA = BIT(11), } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 58f8505de..12a532de6 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -133,6 +133,15 @@ static const glsection_t sections[] = { .caps = QGL_CAP_UNPACK_SUBIMAGE, }, + // GL 1.1, ES 3.0 + // GL_OES_element_index_uint + { + .extension = "GL_OES_element_index_uint", + .ver_gl = QGL_VER(1, 1), + .ver_es = QGL_VER(3, 0), + .caps = QGL_CAP_ELEMENT_INDEX_UINT, + }, + // ES 1.1 { .ver_es = QGL_VER(1, 1), @@ -654,6 +663,14 @@ bool QGL_Init(void) return false; } +#if !USE_GLES + // reject if GL_UNSIGNED_INT indices are not supported + if (!(gl_config.caps & QGL_CAP_ELEMENT_INDEX_UINT)) { + Com_EPrintf("UINT indices are not supported, recompile with USE_GLES.\n"); + return false; + } +#endif + Com_DPrintf("Detected OpenGL capabilities: %#x\n", gl_config.caps); return true; } From 406ee9250b23c5a46fb083ebdc7ee5f660983ede Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 12:07:59 +0300 Subject: [PATCH 599/974] Add typedef for GL indices. --- src/refresh/draw.c | 4 ++-- src/refresh/gl.h | 18 +++++++++--------- src/refresh/mesh.c | 6 +++--- src/refresh/models.c | 6 +++--- src/refresh/state.c | 4 ++-- src/refresh/tess.c | 8 ++++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 4fb38157d..993d19e74 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -26,7 +26,7 @@ static inline void GL_StretchPic_( uint32_t color, int texnum, int flags) { vec_t *dst_vert; - QGL_INDEX_TYPE *dst_indices; + glIndex_t *dst_indices; if (tess.numverts + 4 > TESS_MAX_VERTICES || tess.numindices + 6 > TESS_MAX_INDICES || @@ -74,7 +74,7 @@ static inline void GL_StretchPic_( static void GL_DrawVignette(int distance, color_t outer, color_t inner) { vec_t *dst_vert; - QGL_INDEX_TYPE *dst_indices; + glIndex_t *dst_indices; if (tess.numverts + 8 > TESS_MAX_VERTICES || tess.numindices + 24 > TESS_MAX_INDICES || diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 487d0ddef..5eaf4a343 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -40,11 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc., */ #if USE_GLES -#define QGL_INDEX_TYPE GLushort -#define QGL_INDEX_ENUM GL_UNSIGNED_SHORT +#define QGL_INDEX_TYPE GL_UNSIGNED_SHORT +typedef GLushort glIndex_t; #else -#define QGL_INDEX_TYPE GLuint -#define QGL_INDEX_ENUM GL_UNSIGNED_INT +#define QGL_INDEX_TYPE GL_UNSIGNED_INT +typedef GLuint glIndex_t; #endif #define MAX_TMUS 3 @@ -342,7 +342,7 @@ typedef struct { int numtris; int numindices; int numskins; - QGL_INDEX_TYPE *indices; + glIndex_t *indices; maliasvert_t *verts; maliastc_t *tcoords; #if USE_MD5 @@ -401,7 +401,7 @@ typedef struct { md5_vertex_t *vertices; maliastc_t *tcoords; - QGL_INDEX_TYPE *indices; + glIndex_t *indices; md5_weight_t *weights; } md5_mesh_t; @@ -647,7 +647,7 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) #define GL_Color(r, g, b, a) gl_backend->color(r, g, b, a) #define GL_DrawTriangles(num_indices, indices) \ - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_ENUM, indices) + qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_TYPE, indices) typedef enum { SHOWTRIS_WORLD = BIT(0), @@ -660,7 +660,7 @@ void GL_ForceTexture(GLuint tmu, GLuint texnum); void GL_BindTexture(GLuint tmu, GLuint texnum); void GL_CommonStateBits(GLbitfield bits); void GL_ScrollSpeed(vec2_t scroll, GLbitfield bits); -void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices, bool indexed); +void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar); void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x); void GL_Setup2D(void); @@ -728,7 +728,7 @@ extern cvar_t *gl_intensity; typedef struct { GLfloat vertices[VERTEX_SIZE * TESS_MAX_VERTICES]; - QGL_INDEX_TYPE indices[TESS_MAX_INDICES]; + glIndex_t indices[TESS_MAX_INDICES]; GLuint texnum[MAX_TMUS]; int numverts; int numindices; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index fcb68c86b..571604ca4 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -402,7 +402,7 @@ static void setup_celshading(void) celscale = 1.0f - Distance(origin, glr.fd.vieworg) / 700.0f; } -static void draw_celshading(const QGL_INDEX_TYPE *indices, int num_indices) +static void draw_celshading(const glIndex_t *indices, int num_indices) { if (celscale < 0.01f) return; @@ -475,7 +475,7 @@ static void setup_shadow(void) GL_MultMatrix(shadowmatrix, tmp, matrix); } -static void draw_shadow(const QGL_INDEX_TYPE *indices, int num_indices) +static void draw_shadow(const glIndex_t *indices, int num_indices) { if (shadowmatrix[15] < 0.5f) return; @@ -536,7 +536,7 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) return skins[ent->skinnum]; } -static void draw_alias_mesh(const QGL_INDEX_TYPE *indices, int num_indices, +static void draw_alias_mesh(const glIndex_t *indices, int num_indices, const maliastc_t *tcoords, int num_verts, image_t **skins, int num_skins) { diff --git a/src/refresh/models.c b/src/refresh/models.c index 29b04b8c5..ed74864a7 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -506,7 +506,7 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, uint32_t *src_idx; maliasvert_t *dst_vert; maliastc_t *dst_tc; - QGL_INDEX_TYPE *dst_idx; + glIndex_t *dst_idx; uint32_t index; int i, j, k, ret; const char *err; @@ -1422,7 +1422,7 @@ static void MOD_UploadBuffer(model_t *model) qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); mesh->tcoords = (maliastc_t *)verts_offset; - mesh->indices = (QGL_INDEX_TYPE *)index_offset; + mesh->indices = (glIndex_t *)index_offset; verts_offset += verts_size; index_offset += index_size; } @@ -1436,7 +1436,7 @@ static void MOD_UploadBuffer(model_t *model) qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); mesh->tcoords = (maliastc_t *)verts_offset; - mesh->indices = (QGL_INDEX_TYPE *)index_offset; + mesh->indices = (glIndex_t *)index_offset; verts_offset += verts_size; index_offset += index_size; } diff --git a/src/refresh/state.c b/src/refresh/state.c index ef84251b3..3e707c79b 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -268,7 +268,7 @@ void GL_Setup3D(bool waterwarp) qglClear(GL_DEPTH_BUFFER_BIT | gl_static.stencil_buffer_bit); } -void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices, bool indexed) +void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed) { GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE); @@ -289,7 +289,7 @@ void GL_DrawOutlines(GLsizei count, const QGL_INDEX_TYPE *indices, bool indexed) if (indexed) { for (i = 0; i < count / 3; i++) - qglDrawElements(GL_LINE_LOOP, 3, QGL_INDEX_ENUM, &indices[i * 3]); + qglDrawElements(GL_LINE_LOOP, 3, QGL_INDEX_TYPE, &indices[i * 3]); } else { for (i = 0; i < count / 3; i++) qglDrawArrays(GL_LINE_LOOP, i * 3, 3); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 2afe33f9e..aa9f8cad2 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -150,7 +150,7 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col { vec3_t d1, d2, d3; vec_t *dst_vert; - QGL_INDEX_TYPE *dst_indices; + glIndex_t *dst_indices; VectorSubtract(end, start, d1); VectorSubtract(glr.fd.vieworg, start, d2); @@ -291,7 +291,7 @@ void GL_DrawFlares(void) vec3_t up, down, left, right; color_t color; vec_t *dst_vert; - QGL_INDEX_TYPE *dst_indices; + glIndex_t *dst_indices; GLuint result, texnum; const entity_t *ent; glquery_t *q; @@ -480,7 +480,7 @@ void GL_UnlockArrays(void) static void GL_DrawIndexed(showtris_t showtris) { - const QGL_INDEX_TYPE *indices = tess.indices; + const glIndex_t *indices = tess.indices; GL_LockArrays(tess.numverts); @@ -606,7 +606,7 @@ static void GL_DrawFace(const mface_t *surf) int numtris = surf->numsurfedges - 2; int numindices = numtris * 3; GLuint texnum[MAX_TMUS]; - QGL_INDEX_TYPE *dst_indices; + glIndex_t *dst_indices; int i, j; if (q_unlikely(gl_lightmap->integer && surf->texnum[1])) { From d6acad61f15562bc8fb2b8546bd0fc6725b5eca7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 13:33:49 +0300 Subject: [PATCH 600/974] Add Meson option to build GLES 1 renderer. --- meson.build | 7 +++++++ meson_options.txt | 5 +++++ src/refresh/main.c | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 63ee3c046..810a8904d 100644 --- a/meson.build +++ b/meson.build @@ -471,6 +471,7 @@ config.set_quoted('VID_MODELIST', get_option('vid-modelist')) config.set10('USE_AUTOREPLY', get_option('auto-reply')) config.set10('USE_DEBUG', get_option('debug')) config.set10('USE_FPS', get_option('variable-fps')) +config.set10('USE_GLES', get_option('opengl-es1')) config.set10('USE_ICMP', get_option('icmp-errors').require(win32 or cc.has_header('linux/errqueue.h')).allowed()) config.set10('USE_MD3', get_option('md3')) config.set10('USE_MD5', get_option('md5')) @@ -478,6 +479,12 @@ config.set10('USE_PACKETDUP', get_option('packetdup-hack')) config.set10('USE_TGA', get_option('tga')) config.set10('USE_' + host_machine.endian().to_upper() + '_ENDIAN', true) +if get_option('opengl-es1') + config.set_quoted('DEFGLPROFILE', 'es1') +else + config.set_quoted('DEFGLPROFILE', '') +endif + # protocol extensions are always on config.set10('USE_PROTOCOL_EXTENSIONS', true) diff --git a/meson_options.txt b/meson_options.txt index ec8367a95..f964e001c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -101,6 +101,11 @@ option('openal', value: 'auto', description: 'OpenAL sound backend') +option('opengl-es1', + type: 'boolean', + value: false, + description: 'Build OpenGL ES 1 compatible renderer') + option('packetdup-hack', type: 'boolean', value: false, diff --git a/src/refresh/main.c b/src/refresh/main.c index 2f9eaa472..da73c466c 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1169,7 +1169,7 @@ r_opengl_config_t R_GetGLConfig(void) if (cfg.multisamples < 2) cfg.multisamples = 0; - const char *s = Cvar_Get("gl_profile", "", CVAR_REFRESH)->string; + const char *s = Cvar_Get("gl_profile", DEFGLPROFILE, CVAR_REFRESH)->string; if (!Q_stricmpn(s, "gl", 2)) cfg.profile = QGL_PROFILE_CORE; From d024f6ce5a9398ca6bc7c874b793954d2cdacf1a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 14:04:11 +0300 Subject: [PATCH 601/974] Reword line in README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f0d56775..4f87a80af 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Q2PRO Q2PRO is an enhanced Quake 2 client and server for Windows and Linux. Supported features include: -* unified OpenGL renderer with support for GL 1.1–1.4, 3.0+, GL ES 3.0+ +* unified OpenGL renderer with support for wide range of OpenGL versions * enhanced console command completion * persistent and searchable console command history * rendering / physics / packet rate separation From 31b4f2ce432efe22717d6d57165762facecf0801 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 16:51:00 +0300 Subject: [PATCH 602/974] Remove GL_ prefix from extension names in comments. --- src/refresh/qgl.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 12a532de6..d56dd59be 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -134,7 +134,7 @@ static const glsection_t sections[] = { }, // GL 1.1, ES 3.0 - // GL_OES_element_index_uint + // OES_element_index_uint { .extension = "GL_OES_element_index_uint", .ver_gl = QGL_VER(1, 1), @@ -155,7 +155,7 @@ static const glsection_t sections[] = { }, // GL 1.3 - // GL_ARB_multitexture + // ARB_multitexture { .extension = "GL_ARB_multitexture", .ver_gl = QGL_VER(1, 3), @@ -167,7 +167,7 @@ static const glsection_t sections[] = { }, // GL 1.3, compat - // GL_ARB_multitexture + // ARB_multitexture { .extension = "GL_ARB_multitexture", .ver_gl = QGL_VER(1, 3), @@ -188,7 +188,7 @@ static const glsection_t sections[] = { }, // GL 1.5 - // GL_ARB_vertex_buffer_object + // ARB_vertex_buffer_object { .extension = "GL_ARB_vertex_buffer_object", .ver_gl = QGL_VER(1, 5), @@ -204,7 +204,7 @@ static const glsection_t sections[] = { }, // GL 1.5, ES 3.0 - // GL_ARB_occlusion_query + // ARB_occlusion_query { .extension = "GL_ARB_occlusion_query", .ver_gl = QGL_VER(1, 5), @@ -286,7 +286,7 @@ static const glsection_t sections[] = { }, // GL 3.1 - // GL_ARB_uniform_buffer_object + // ARB_uniform_buffer_object { .extension = "GL_ARB_uniform_buffer_object", .ver_gl = QGL_VER(3, 1), @@ -313,7 +313,7 @@ static const glsection_t sections[] = { }, // GL 4.3 - // GL_KHR_debug + // KHR_debug { .extension = "GL_KHR_debug", .ver_gl = QGL_VER(4, 3), @@ -343,7 +343,7 @@ static const glsection_t sections[] = { }, // GL 4.5 - // GL_ARB_direct_state_access + // ARB_direct_state_access { .ver_gl = QGL_VER(4, 5), .extension = "GL_ARB_direct_state_access", @@ -354,7 +354,7 @@ static const glsection_t sections[] = { }, // GL 4.5 - // GL_ARB_robustness + // ARB_robustness { .extension = "GL_ARB_robustness", .ver_gl = QGL_VER(4, 5), @@ -366,14 +366,14 @@ static const glsection_t sections[] = { }, // GL 4.6 - // GL_EXT_texture_filter_anisotropic + // EXT_texture_filter_anisotropic { .extension = "GL_EXT_texture_filter_anisotropic", .ver_gl = QGL_VER(4, 6), .caps = QGL_CAP_TEXTURE_ANISOTROPY }, - // GL_ARB_fragment_program + // ARB_fragment_program { .extension = "GL_ARB_fragment_program", .functions = (const glfunction_t []) { @@ -386,7 +386,7 @@ static const glsection_t sections[] = { } }, - // GL_EXT_compiled_vertex_array + // EXT_compiled_vertex_array { .extension = "GL_EXT_compiled_vertex_array", .functions = (const glfunction_t []) { From fd0c105608ef62a4cb085318157a669acbd39694 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 21:05:17 +0300 Subject: [PATCH 603/974] Implement failsafe config fallback for SDL. --- src/unix/video/sdl.c | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/unix/video/sdl.c b/src/unix/video/sdl.c index 57cfa3227..3742cbd90 100644 --- a/src/unix/video/sdl.c +++ b/src/unix/video/sdl.c @@ -252,6 +252,26 @@ static void shutdown(void) memset(&sdl, 0, sizeof(sdl)); } +static bool create_window_and_context(const vrect_t *rc) +{ + sdl.window = SDL_CreateWindow(PRODUCT, rc->x, rc->y, rc->width, rc->height, + SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI); + if (!sdl.window) { + Com_EPrintf("Couldn't create SDL window: %s\n", SDL_GetError()); + return false; + } + + sdl.context = SDL_GL_CreateContext(sdl.window); + if (!sdl.context) { + Com_EPrintf("Couldn't create OpenGL context: %s\n", SDL_GetError()); + SDL_DestroyWindow(sdl.window); + sdl.window = NULL; + return false; + } + + return true; +} + static bool init(void) { vrect_t rc; @@ -270,11 +290,13 @@ static bool init(void) rc.y = SDL_WINDOWPOS_UNDEFINED; } - sdl.window = SDL_CreateWindow(PRODUCT, rc.x, rc.y, rc.width, rc.height, - SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI); - if (!sdl.window) { - Com_EPrintf("Couldn't create SDL window: %s\n", SDL_GetError()); - goto fail; + if (!create_window_and_context(&rc)) { + Com_Printf("Falling back to failsafe config\n"); + SDL_GL_ResetAttributes(); + if (!create_window_and_context(&rc)) { + shutdown(); + return false; + } } SDL_SetWindowMinimumSize(sdl.window, 320, 240); @@ -292,12 +314,6 @@ static bool init(void) SDL_FreeSurface(icon); } - sdl.context = SDL_GL_CreateContext(sdl.window); - if (!sdl.context) { - Com_EPrintf("Couldn't create OpenGL context: %s\n", SDL_GetError()); - goto fail; - } - cvar_t *vid_hwgamma = Cvar_Get("vid_hwgamma", "0", CVAR_REFRESH); if (vid_hwgamma->integer) { Uint16 gamma[3][256]; @@ -318,10 +334,6 @@ static bool init(void) sdl.wayland = !strcmp(SDL_GetCurrentVideoDriver(), "wayland"); return true; - -fail: - shutdown(); - return false; } /* From 2dd39ef5fd12c1584af2a353fa3092f5241b8a91 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 22:47:13 +0300 Subject: [PATCH 604/974] Set default GL profile to 3.2 core on macOS. By default macOS only provides pre-3.0 compatibility context. To get shaders backend working, set profile to 3.2 core by default. Untested. --- meson.build | 4 +++- src/unix/video/sdl.c | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 810a8904d..96e503f0c 100644 --- a/meson.build +++ b/meson.build @@ -479,7 +479,9 @@ config.set10('USE_PACKETDUP', get_option('packetdup-hack')) config.set10('USE_TGA', get_option('tga')) config.set10('USE_' + host_machine.endian().to_upper() + '_ENDIAN', true) -if get_option('opengl-es1') +if host_machine.system() == 'darwin' + config.set_quoted('DEFGLPROFILE', 'gl3.2') +elif get_option('opengl-es1') config.set_quoted('DEFGLPROFILE', 'es1') else config.set_quoted('DEFGLPROFILE', '') diff --git a/src/unix/video/sdl.c b/src/unix/video/sdl.c index 3742cbd90..954120ebf 100644 --- a/src/unix/video/sdl.c +++ b/src/unix/video/sdl.c @@ -81,6 +81,8 @@ static void set_gl_attributes(void) if (cfg.profile) { if (cfg.profile == QGL_PROFILE_ES) SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + else + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, cfg.major_ver); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, cfg.minor_ver); } From 787afb1227b84e66aea81893d7a259fb31d3e54f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 22 Aug 2024 22:57:53 +0300 Subject: [PATCH 605/974] Skip empty strings when completing arguments. --- src/common/prompt.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/prompt.c b/src/common/prompt.c index 55948101a..73a11fe77 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -126,6 +126,8 @@ void Prompt_AddMatch(genctx_t *ctx, const char *s) { int r; + if (!*s) + return; if (ctx->count >= ctx->size) return; From 9b8e04bf554de55d328e0e26b2f3b86483f5ab78 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 23 Aug 2024 15:28:45 +0300 Subject: [PATCH 606/974] Be more strict about GL extension entry point suffixes. Always get extension entry points with their defined stuffix, and never add suffix to entry points defined without one. Add special case for GL_KHR_debug which has KHR suffix in ES context only. --- src/refresh/qgl.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index d56dd59be..6d4d37484 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -30,6 +30,7 @@ typedef struct { uint16_t excl_gl; uint16_t excl_es; uint32_t caps; + char suffix[4]; const char *extension; const glfunction_t *functions; } glsection_t; @@ -158,6 +159,7 @@ static const glsection_t sections[] = { // ARB_multitexture { .extension = "GL_ARB_multitexture", + .suffix = "ARB", .ver_gl = QGL_VER(1, 3), .ver_es = QGL_VER(1, 0), .functions = (const glfunction_t []) { @@ -170,6 +172,7 @@ static const glsection_t sections[] = { // ARB_multitexture { .extension = "GL_ARB_multitexture", + .suffix = "ARB", .ver_gl = QGL_VER(1, 3), .ver_es = QGL_VER(1, 0), .excl_gl = QGL_VER(3, 1), @@ -191,6 +194,7 @@ static const glsection_t sections[] = { // ARB_vertex_buffer_object { .extension = "GL_ARB_vertex_buffer_object", + .suffix = "ARB", .ver_gl = QGL_VER(1, 5), .ver_es = QGL_VER(1, 1), .functions = (const glfunction_t []) { @@ -207,6 +211,7 @@ static const glsection_t sections[] = { // ARB_occlusion_query { .extension = "GL_ARB_occlusion_query", + .suffix = "ARB", .ver_gl = QGL_VER(1, 5), .ver_es = QGL_VER(3, 0), .functions = (const glfunction_t []) { @@ -316,6 +321,7 @@ static const glsection_t sections[] = { // KHR_debug { .extension = "GL_KHR_debug", + .suffix = "?KHR", .ver_gl = QGL_VER(4, 3), .ver_es = QGL_VER(3, 2), .functions = (const glfunction_t []) { @@ -357,6 +363,7 @@ static const glsection_t sections[] = { // ARB_robustness { .extension = "GL_ARB_robustness", + .suffix = "ARB", .ver_gl = QGL_VER(4, 5), .ver_es = QGL_VER(3, 2), .functions = (const glfunction_t []) { @@ -603,21 +610,24 @@ bool QGL_Init(void) } if (sec->functions) { + const char *suffix = sec->suffix; + + // GL_KHR_debug weirdness + if (*suffix == '?') { + if (gl_config.ver_es) + suffix++; + else + suffix = ""; + } + for (func = sec->functions; func->name; func++) { const char *name = func->name; - void *addr = vid->get_proc_addr(name); - // try with XYZ suffix if this is a GL_XYZ_extension - if (!addr && !core) { - char suf[4]; - memcpy(suf, sec->extension + 3, 3); - suf[3] = 0; - if (!strstr(name, suf)) { - name = va("%s%s", name, suf); - addr = vid->get_proc_addr(name); - } - } + // add suffix if this is an extension + if (!core && *suffix) + name = va("%s%.3s", name, suffix); + void *addr = vid->get_proc_addr(name); if (!addr) { Com_EPrintf("Couldn't get entry point: %s\n", name); break; From ef48e3d4bc218ebbcc55933b7d676a9cde574045 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 23 Aug 2024 15:28:51 +0300 Subject: [PATCH 607/974] Add check for forward compatible GL contexts. --- src/refresh/mesh.c | 2 +- src/refresh/qgl.c | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 571604ca4..9e230c4ed 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -396,7 +396,7 @@ static void setup_celshading(void) { float value = Cvar_ClampValue(gl_celshading, 0, 10); - if (value == 0 || (glr.ent->flags & (RF_TRANSLUCENT | RF_SHELL_MASK)) || !qglPolygonMode) + if (value == 0 || (glr.ent->flags & (RF_TRANSLUCENT | RF_SHELL_MASK)) || !qglPolygonMode || !qglLineWidth) celscale = 0; else celscale = 1.0f - Distance(origin, glr.fd.vieworg) / 700.0f; diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 6d4d37484..538cf5c6d 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -564,24 +564,33 @@ bool QGL_Init(void) } } - bool compatible = false; + int non_compat_ver = 0; if (gl_config.ver_gl >= QGL_VER(3, 2)) { // Profile is correctly set by Mesa, but can be 0 for compatibility // context on NVidia. Thus only check for core bit. GLint profile = 0; qglGetIntegerv(GL_CONTEXT_PROFILE_MASK, &profile); - compatible = !(profile & GL_CONTEXT_CORE_PROFILE_BIT); + if (profile & GL_CONTEXT_CORE_PROFILE_BIT) + non_compat_ver = gl_config.ver_gl; } else if (gl_config.ver_gl == QGL_VER(3, 1)) { - compatible = extension_present("GL_ARB_compatibility"); + if (!extension_present("GL_ARB_compatibility")) + non_compat_ver = gl_config.ver_gl; + } else if (gl_config.ver_gl == QGL_VER(3, 0)) { + // 3.0 deprecates functionality removed by 3.1. If forward compatible + // bit is set, it also removes this functionality. + GLint flags = 0; + qglGetIntegerv(GL_CONTEXT_FLAGS, &flags); + if (flags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT) + non_compat_ver = QGL_VER(3, 1); } if (gl_config.ver_es) { Com_DPrintf("Detected OpenGL ES %d.%d\n", QGL_UNPACK_VER(gl_config.ver_es)); - } else if (gl_config.ver_gl >= QGL_VER(3, 2)) { + } else if (gl_config.ver_gl >= QGL_VER(3, 2) || non_compat_ver) { Com_DPrintf("Detected OpenGL %d.%d (%s profile)\n", QGL_UNPACK_VER(gl_config.ver_gl), - compatible ? "compatibility" : "core"); + non_compat_ver ? "core" : "compatibility"); } else { Com_DPrintf("Detected OpenGL %d.%d\n", QGL_UNPACK_VER(gl_config.ver_gl)); @@ -592,7 +601,7 @@ bool QGL_Init(void) const glfunction_t *func; bool core; - if (sec->excl_gl && gl_config.ver_gl >= sec->excl_gl && !compatible) + if (sec->excl_gl && non_compat_ver >= sec->excl_gl) continue; if (sec->excl_es && gl_config.ver_es >= sec->excl_es) continue; @@ -681,6 +690,16 @@ bool QGL_Init(void) } #endif + // disable qglLineWidth in forward compatible core profile contexts + if (non_compat_ver >= QGL_VER(3, 1)) { + GLint flags = 0; + qglGetIntegerv(GL_CONTEXT_FLAGS, &flags); + if (flags & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT) { + Com_DPrintf("Detected forward compatible context\n"); + qglLineWidth = NULL; + } + } + Com_DPrintf("Detected OpenGL capabilities: %#x\n", gl_config.caps); return true; } From 8e0c77654375f2e8e6473015c06c26bf660314d4 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 23 Aug 2024 09:59:29 -0400 Subject: [PATCH 608/974] Fixed check on teamplay rounds played --- src/action/tng_stats.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index ffa963fed..28ac773b8 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -722,7 +722,8 @@ void G_RegisterScore(void) return; } - if (teamplay->value && game.roundNum > 0){ + if (teamplay->value && game.roundNum == 0){ + gi.dprintf("No rounds were played, so no highscore will be recorded\n"); return; // No rounds were played, so skip } From 999f765f5ed6b1f06bf1b5ae1bf0b5050494f430 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 23 Aug 2024 17:15:00 +0300 Subject: [PATCH 609/974] Use correct attachment for framebuffer query. --- src/refresh/main.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index da73c466c..580416f70 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -971,11 +971,13 @@ static void GL_SetupConfig(void) qglGetIntegerv(GL_STENCIL_BITS, &integer); gl_config.stencilbits = integer; } else if (qglGetFramebufferAttachmentParameteriv) { - qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &integer); + GLenum backbuf = gl_config.ver_es ? GL_BACK : GL_BACK_LEFT; + + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, backbuf, GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &integer); gl_config.colorbits = integer; - qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE, &integer); + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, backbuf, GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE, &integer); gl_config.colorbits += integer; - qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_BACK, GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE, &integer); + qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, backbuf, GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE, &integer); gl_config.colorbits += integer; qglGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER, GL_DEPTH, GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &integer); @@ -992,6 +994,8 @@ static void GL_SetupConfig(void) qglDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION, 0, NULL, GL_FALSE); qglDebugMessageCallback(myDebugProc, NULL); } + + GL_ShowErrors(__func__); } static void GL_InitTables(void) From d1c2679ce4a746b0549722f10039e2ff7bd84d19 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 24 Aug 2024 00:17:13 +0300 Subject: [PATCH 610/974] Unbind buffers when clearing state. --- src/refresh/state.c | 6 ++++++ src/refresh/tess.c | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/refresh/state.c b/src/refresh/state.c index 3e707c79b..999210926 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -314,6 +314,12 @@ void GL_ClearState(void) qglCullFace(GL_BACK); qglEnable(GL_CULL_FACE); + // unbind buffers + if (qglBindBuffer) { + qglBindBuffer(GL_ARRAY_BUFFER, 0); + qglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + } + gl_backend->clear_state(); qglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | gl_static.stencil_buffer_bit); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index aa9f8cad2..580dee48d 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -507,8 +507,6 @@ void GL_InitArrays(void) qglBindVertexArray(gl_static.array_object); qglGenBuffers(1, &gl_static.index_buffer); - GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_static.index_buffer); - qglGenBuffers(1, &gl_static.vertex_buffer); } From 8fea803fd0ee56036a5ea0452768e1ad339f3338 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 24 Aug 2024 13:31:31 +0300 Subject: [PATCH 611/974] Fix broken GL_TextureAnimation(). Broken by commit 704d747e. It still worked, but the code was wrong. --- src/refresh/tess.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 580dee48d..7b2ff0ce4 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -592,9 +592,11 @@ static int GL_CopyVerts(const mface_t *surf) static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) { - if (q_unlikely(tex->next)) - for (int i = 0; i < glr.ent->frame % tex->numframes; i++) + if (q_unlikely(tex->next)) { + int c = glr.ent->frame % tex->numframes; + while (c--) tex = tex->next; + } return tex->image; } From 930cd0b702d9d3204149faf336ae0751eb0d68ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 24 Aug 2024 13:34:36 +0300 Subject: [PATCH 612/974] Push flare towards viewer rather than scaling. --- src/refresh/main.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 580416f70..2c39af0f0 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -398,7 +398,7 @@ static void GL_DrawNullModel(void) GL_UnlockArrays(); } -static void make_flare_quad(const entity_t *e, float scale) +static void make_flare_quad(const vec3_t origin, float scale) { vec3_t up, down, left, right; @@ -407,10 +407,10 @@ static void make_flare_quad(const entity_t *e, float scale) VectorScale(glr.viewaxis[2], -scale, down); VectorScale(glr.viewaxis[2], scale, up); - VectorAdd3(e->origin, down, left, tess.vertices + 0); - VectorAdd3(e->origin, up, left, tess.vertices + 3); - VectorAdd3(e->origin, down, right, tess.vertices + 6); - VectorAdd3(e->origin, up, right, tess.vertices + 9); + VectorAdd3(origin, down, left, tess.vertices + 0); + VectorAdd3(origin, up, left, tess.vertices + 3); + VectorAdd3(origin, down, right, tess.vertices + 6); + VectorAdd3(origin, up, right, tess.vertices + 9); } static void GL_OccludeFlares(void) @@ -465,10 +465,14 @@ static void GL_OccludeFlares(void) set = true; } - if (bsp && BSP_PointLeaf(bsp->nodes, e->origin)->contents == CONTENTS_SOLID) - make_flare_quad(e, 2.5f * e->scale); - else - make_flare_quad(e, 2.5f); + if (bsp && BSP_PointLeaf(bsp->nodes, e->origin)->contents == CONTENTS_SOLID) { + vec3_t dir, org; + VectorSubtract(glr.fd.vieworg, e->origin, dir); + VectorNormalize(dir); + VectorMA(e->origin, 5.0f, dir, org); + make_flare_quad(org, 2.5f); + } else + make_flare_quad(e->origin, 2.5f); GL_LockArrays(4); qglBeginQuery(gl_static.samples_passed, q->query); From 83dd4cb9938da547f7ac461bc950d8525f928764 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 01:17:46 +0300 Subject: [PATCH 613/974] Support drawing original style laser beams. --- doc/client.asciidoc | 5 ++ inc/common/math.h | 1 + src/common/math.c | 13 ++++ src/refresh/gl.h | 2 + src/refresh/main.c | 2 + src/refresh/tess.c | 144 ++++++++++++++++++++++++++++++++++---------- 6 files changed, 134 insertions(+), 33 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index cd9830136..5764c4436 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -703,6 +703,11 @@ gl_partshape:: - 1 — square - 2 — fuller, less faded circle +gl_beamstyle:: + Specifies drawing style of laser beams. Default value is 0. + - 0 — textured billboard-type beam (Q2PRO style, very fast to draw) + - 1 — flat color polygonal beam (original Quake 2 style) + gl_celshading:: Enables drawing black contour lines around 3D models (aka ‘celshading’). Value of this variable specifies thickness of the lines drawn. Default diff --git a/inc/common/math.h b/inc/common/math.h index 118f2bd7c..08e5d7b04 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -67,6 +67,7 @@ static inline vec_t PlaneDiffFast(const vec3_t v, const cplane_t *p) #if USE_REF void SetupRotationMatrix(vec3_t matrix[3], const vec3_t dir, float degrees); +void RotatePointAroundVector(vec3_t out, const vec3_t dir, const vec3_t in, float degrees); // quaternion routines, for MD5 skeletons #if USE_MD5 diff --git a/src/common/math.c b/src/common/math.c index c2ad40c01..a3ad1e196 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -387,6 +387,19 @@ void SetupRotationMatrix(vec3_t matrix[3], const vec3_t dir, float degrees) matrix[2][2] = (one_c * zz) + c; } +void RotatePointAroundVector(vec3_t out, const vec3_t dir, const vec3_t in, float degrees) +{ + vec3_t matrix[3]; + vec3_t tmp; + + SetupRotationMatrix(matrix, dir, degrees); + + VectorCopy(in, tmp); + out[0] = DotProduct(tmp, matrix[0]); + out[1] = DotProduct(tmp, matrix[1]); + out[2] = DotProduct(tmp, matrix[2]); +} + #if USE_MD5 #define X 0 diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 5eaf4a343..5f45acdda 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -230,6 +230,7 @@ extern statCounters_t c; // regular variables extern cvar_t *gl_partscale; extern cvar_t *gl_partstyle; +extern cvar_t *gl_beamstyle; extern cvar_t *gl_celshading; extern cvar_t *gl_dotshading; extern cvar_t *gl_shadows; @@ -274,6 +275,7 @@ extern cvar_t *gl_showerrors; #define GL_rand() Q_rand_state(&glr.rand_seed) #define GL_frand() ((int32_t)GL_rand() * 0x1p-32f + 0.5f) +#define GL_crand() ((int32_t)GL_rand() * 0x1p-31f) typedef enum { CULL_OUT, diff --git a/src/refresh/main.c b/src/refresh/main.c index 2c39af0f0..655bbbd13 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -37,6 +37,7 @@ unsigned r_registration_sequence; // regular variables cvar_t *gl_partscale; cvar_t *gl_partstyle; +cvar_t *gl_beamstyle; cvar_t *gl_celshading; cvar_t *gl_dotshading; cvar_t *gl_shadows; @@ -861,6 +862,7 @@ static void GL_Register(void) // regular variables gl_partscale = Cvar_Get("gl_partscale", "2", 0); gl_partstyle = Cvar_Get("gl_partstyle", "0", 0); + gl_beamstyle = Cvar_Get("gl_beamstyle", "0", 0); gl_celshading = Cvar_Get("gl_celshading", "0", 0); gl_dotshading = Cvar_Get("gl_dotshading", "1", 0); gl_shadows = Cvar_Get("gl_shadows", "0", CVAR_ARCHIVE); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 7b2ff0ce4..b6f931783 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -138,15 +138,78 @@ static void GL_FlushBeamSegments(void) if (!tess.numindices) return; - GL_BindTexture(0, TEXNUM_BEAM); + glArrayBits_t array = GLA_VERTEX | GLA_COLOR; + GLuint texnum = TEXNUM_BEAM; + + if (gl_beamstyle->integer) + texnum = TEXNUM_WHITE; + else + array |= GLA_TC; + + GL_BindTexture(0, texnum); GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); - GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + GL_ArrayBits(array); GL_DrawIndexed(SHOWTRIS_FX); tess.numverts = tess.numindices = 0; } -static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t color, float width) +#define BEAM_POINTS 12 + +static void GL_DrawPolyBeam(const vec3_t *segments, int num_segments, color_t color, float width) +{ + int i, j, k, firstvert; + vec3_t points[BEAM_POINTS]; + vec3_t dir, right, up; + vec_t *dst_vert; + glIndex_t *dst_indices; + + VectorSubtract(segments[num_segments], segments[0], dir); + if (VectorNormalize(dir) < 0.1f) + return; + + MakeNormalVectors(dir, right, up); + VectorScale(right, width, right); + + if (q_unlikely(tess.numverts + BEAM_POINTS * (num_segments + 1) > TESS_MAX_VERTICES || + tess.numindices + BEAM_POINTS * 6 * num_segments > TESS_MAX_INDICES)) + GL_FlushBeamSegments(); + + dst_vert = tess.vertices + tess.numverts * 4; + + for (i = 0; i < BEAM_POINTS; i++) { + RotatePointAroundVector(points[i], dir, right, (360.0f / BEAM_POINTS) * i); + VectorAdd(points[i], segments[0], dst_vert); + WN32(dst_vert + 3, color.u32); + dst_vert += 4; + } + + dst_indices = tess.indices + tess.numindices; + firstvert = tess.numverts; + + for (i = 1; i <= num_segments; i++) { + for (j = 0; j < BEAM_POINTS; j++) { + VectorAdd(points[j], segments[i], dst_vert); + WN32(dst_vert + 3, color.u32); + dst_vert += 4; + + k = (j + 1) % BEAM_POINTS; + dst_indices[0] = firstvert + j; + dst_indices[1] = firstvert + j + BEAM_POINTS; + dst_indices[2] = firstvert + k + BEAM_POINTS; + dst_indices[3] = firstvert + j; + dst_indices[4] = firstvert + k + BEAM_POINTS; + dst_indices[5] = firstvert + k; + dst_indices += 6; + } + firstvert += BEAM_POINTS; + } + + tess.numverts += BEAM_POINTS * (num_segments + 1); + tess.numindices += BEAM_POINTS * 6 * num_segments; +} + +static void GL_DrawSimpleBeam(const vec3_t start, const vec3_t end, color_t color, float width) { vec3_t d1, d2, d3; vec_t *dst_vert; @@ -197,48 +260,54 @@ static void GL_DrawBeamSegment(const vec3_t start, const vec3_t end, color_t col static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t color, float width) { - vec3_t d1, segments[MAX_LIGHTNING_SEGMENTS - 1]; + vec3_t dir, segments[MAX_LIGHTNING_SEGMENTS + 1]; + vec3_t right, up; vec_t length, segment_length; int i, num_segments, max_segments; - VectorSubtract(end, start, d1); - length = VectorNormalize(d1); + VectorSubtract(end, start, dir); + length = VectorNormalize(dir); - max_segments = length / MIN_SEGMENT_LENGTH; - if (max_segments <= 1) { - GL_DrawBeamSegment(start, end, color, width); - return; - } + max_segments = Q_clip(length / MIN_SEGMENT_LENGTH, 1, MAX_LIGHTNING_SEGMENTS); - if (max_segments <= MIN_LIGHTNING_SEGMENTS) { + if (max_segments <= MIN_LIGHTNING_SEGMENTS) num_segments = max_segments; - } else { - max_segments = min(max_segments, MAX_LIGHTNING_SEGMENTS); + else num_segments = MIN_LIGHTNING_SEGMENTS + GL_rand() % (max_segments - MIN_LIGHTNING_SEGMENTS + 1); - } + + if (num_segments > 1) + MakeNormalVectors(dir, right, up); segment_length = length / num_segments; - for (i = 0; i < num_segments - 1; i++) { - int dir = GL_rand() % q_countof(bytedirs); - float offs = GL_frand() * (segment_length * 0.5f); - float dist = (i + 1) * segment_length; - VectorMA(start, dist, d1, segments[i]); - VectorMA(segments[i], offs, bytedirs[dir], segments[i]); + for (i = 1; i < num_segments; i++) { + vec3_t point; + float offs; + + VectorMA(start, i * segment_length, dir, point); + + offs = GL_crand() * (segment_length * 0.35f); + VectorMA(point, offs, right, point); + + offs = GL_crand() * (segment_length * 0.35f); + VectorMA(point, offs, up, segments[i]); } - for (i = 0; i < num_segments; i++) { - const float *seg_start = (i == 0) ? start : segments[i - 1]; - const float *seg_end = (i == num_segments - 1) ? end : segments[i]; + VectorCopy(start, segments[0]); + VectorCopy(end, segments[i]); - GL_DrawBeamSegment(seg_start, seg_end, color, width); + if (gl_beamstyle->integer) { + GL_DrawPolyBeam(segments, num_segments, color, width); + } else { + for (i = 0; i < num_segments; i++) + GL_DrawSimpleBeam(segments[i], segments[i + 1], color, width); } } void GL_DrawBeams(void) { - const vec_t *start, *end; + vec3_t segs[2]; color_t color; - float width; + float width, scale; const entity_t *ent; int i; @@ -246,14 +315,21 @@ void GL_DrawBeams(void) return; GL_LoadMatrix(glr.viewmatrix); - GL_BindArrays(VA_EFFECT); + + if (gl_beamstyle->integer) { + GL_BindArrays(VA_NULLMODEL); + scale = 0.5f; + } else { + GL_BindArrays(VA_EFFECT); + scale = 1.2f; + } for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_BEAM)) continue; - start = ent->origin; - end = ent->oldorigin; + VectorCopy(ent->origin, segs[0]); + VectorCopy(ent->oldorigin, segs[1]); if (ent->skinnum == -1) color.u32 = ent->rgba.u32; @@ -261,12 +337,14 @@ void GL_DrawBeams(void) color.u32 = d_8to24table[ent->skinnum & 0xff]; color.u8[3] *= ent->alpha; - width = abs((int16_t)ent->frame) * 1.2f; + width = abs((int16_t)ent->frame) * scale; if (ent->flags & RF_GLOW) - GL_DrawLightningBeam(start, end, color, width); + GL_DrawLightningBeam(segs[0], segs[1], color, width); + else if (gl_beamstyle->integer) + GL_DrawPolyBeam(segs, 1, color, width); else - GL_DrawBeamSegment(start, end, color, width); + GL_DrawSimpleBeam(segs[0], segs[1], color, width); } GL_FlushBeamSegments(); From b9889864a20f3c5c821036fdc4215f11e2e8e5a2 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 13:17:13 +0300 Subject: [PATCH 614/974] Add and use Com_SlowRand(). Start with better seed than just timestamp. Fixes bad beam color randomization since 34e89db5. --- inc/common/utils.h | 6 ++++++ inc/shared/shared.h | 11 ----------- src/client/client.h | 4 ---- src/client/entities.c | 4 ++-- src/client/main.c | 2 -- src/client/tent.c | 12 ++++++------ src/common/utils.c | 41 +++++++++++++++++++++++++++++++++++++++++ src/refresh/gl.h | 5 ----- src/refresh/main.c | 1 - src/refresh/tess.c | 6 +++--- 10 files changed, 58 insertions(+), 34 deletions(-) diff --git a/inc/common/utils.h b/inc/common/utils.h index f6e84a474..a0316fc5f 100644 --- a/inc/common/utils.h +++ b/inc/common/utils.h @@ -78,6 +78,12 @@ size_t Com_EscapeString(char *dst, const char *src, size_t size); char *Com_MakePrintable(const char *s); +#if USE_CLIENT +uint32_t Com_SlowRand(void); +#define Com_SlowFrand() ((int32_t)Com_SlowRand() * 0x1p-32f + 0.5f) +#define Com_SlowCrand() ((int32_t)Com_SlowRand() * 0x1p-31f) +#endif + // Bitmap chunks (for sparse bitmaps) #define BC_BITS (sizeof(size_t) * CHAR_BIT) #define BC_COUNT(n) (((n) + BC_BITS - 1) / BC_BITS) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index d7ddf565f..c77e00e7d 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -374,17 +374,6 @@ void Q_srand(uint32_t seed); uint32_t Q_rand(void); uint32_t Q_rand_uniform(uint32_t n); -static inline uint32_t Q_rand_state(uint32_t *seed) -{ - uint32_t x = *seed; - - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - - return *seed = x; -} - static inline int Q_clip(int a, int b, int c) { if (a < b) diff --git a/src/client/client.h b/src/client/client.h index 18ef57227..46e83b6d7 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -236,8 +236,6 @@ typedef struct { float keylerpfrac; #endif - uint32_t rand_seed; - refdef_t refdef; float fov_x; // interpolated float fov_y; // derived from fov_x assuming 4/3 aspect ratio @@ -615,8 +613,6 @@ static inline void CL_AdvanceValue(float *restrict val, float target, float spee // main.c // -#define CL_rand() Q_rand_state(&cl.rand_seed) - void CL_Init(void); void CL_Quit_f(void); void CL_Disconnect(error_type_t type); diff --git a/src/client/entities.c b/src/client/entities.c index 94d3bfa42..e26eea67a 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -666,7 +666,7 @@ static void CL_AddPacketEntities(void) if (renderfx & RF_BEAM) { // the four beam colors are encoded in 32 bits of skinnum (hack) ent.alpha = 0.30f; - ent.skinnum = (s1->skinnum >> ((CL_rand() % 4) * 8)) & 0xff; + ent.skinnum = (s1->skinnum >> ((Com_SlowRand() % 4) * 8)) & 0xff; ent.model = 0; } else { // set skin @@ -977,7 +977,7 @@ static void CL_AddPacketEntities(void) } else if (effects & EF_TRAP) { ent.origin[2] += 32; CL_TrapParticles(cent, ent.origin); - i = (CL_rand() % 100) + 100; + i = (Com_SlowRand() % 100) + 100; V_AddLight(ent.origin, i, 1, 0.8f, 0.1f); } else if (effects & EF_FLAG1) { CL_FlagTrail(cent->lerp_origin, ent.origin, 242); diff --git a/src/client/main.c b/src/client/main.c index b3e6940e7..6a4aadc34 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -3265,8 +3265,6 @@ unsigned CL_Frame(unsigned msec) if (cls.state == ca_active && !sv_paused->integer) CL_SetClientTime(); - cl.rand_seed = cl.time / 16; - #if USE_AUTOREPLY // check for version reply CL_CheckForReply(); diff --git a/src/client/tent.c b/src/client/tent.c index 4249b176b..848dd5f66 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -793,7 +793,7 @@ void CL_DrawBeam(const vec3_t start, const vec3_t end, qhandle_t model) ent.flags = RF_FULLBRIGHT; ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; V_AddEntity(&ent); return; } @@ -809,11 +809,11 @@ void CL_DrawBeam(const vec3_t start, const vec3_t end, qhandle_t model) ent.flags = RF_FULLBRIGHT; ent.angles[0] = -angles[0]; ent.angles[1] = angles[1] + 180.0f; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; } else { ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; } V_AddEntity(&ent); @@ -975,7 +975,7 @@ static void CL_AddPlayerBeams(void) ent.flags = RF_FULLBRIGHT; ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; V_AddEntity(&ent); continue; } @@ -997,11 +997,11 @@ static void CL_AddPlayerBeams(void) ent.flags = RF_FULLBRIGHT; ent.angles[0] = -angles[0]; ent.angles[1] = angles[1] + 180.0f; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; } else { ent.angles[0] = angles[0]; ent.angles[1] = angles[1]; - ent.angles[2] = CL_rand() % 360; + ent.angles[2] = Com_SlowRand() % 360; } V_AddEntity(&ent); diff --git a/src/common/utils.c b/src/common/utils.c index f419ad74b..7fdce9256 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -622,3 +622,44 @@ char *Com_MakePrintable(const char *s) Com_EscapeString(buffer, s, sizeof(buffer)); return buffer; } + +#if USE_CLIENT + +/* +=============== +Com_SlowRand + +`Slow' PRNG that begins consecutive frames with the same seed. Reseeded each 16 +ms. Used for random effects that shouldn't cause too much tearing without vsync. +=============== +*/ +uint32_t Com_SlowRand(void) +{ + static uint32_t com_rand_ts; + static uint32_t com_rand_base; + static uint32_t com_rand_seed; + static uint32_t com_rand_frame; + + // see if it's time to reseed + if (com_rand_ts != com_eventTime / 16 && !sv_paused->integer) { + com_rand_ts = com_eventTime / 16; + com_rand_base = Q_rand(); + } + + // reset if started new frame + if (com_rand_frame != com_framenum) { + com_rand_frame = com_framenum; + com_rand_seed = com_rand_base; + } + + // xorshift RNG + uint32_t x = com_rand_seed; + + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + + return com_rand_seed = x; +} + +#endif diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 5f45acdda..7fd3622e4 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -145,7 +145,6 @@ typedef struct { unsigned visframe; unsigned drawframe; unsigned dlightframe; - unsigned rand_seed; unsigned timestamp; float frametime; int viewcluster1; @@ -273,10 +272,6 @@ extern cvar_t *gl_vertexlight; extern cvar_t *gl_lightgrid; extern cvar_t *gl_showerrors; -#define GL_rand() Q_rand_state(&glr.rand_seed) -#define GL_frand() ((int32_t)GL_rand() * 0x1p-32f + 0.5f) -#define GL_crand() ((int32_t)GL_rand() * 0x1p-31f) - typedef enum { CULL_OUT, CULL_IN, diff --git a/src/refresh/main.c b/src/refresh/main.c index 655bbbd13..1a469b7b4 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -653,7 +653,6 @@ void R_RenderFrame(const refdef_t *fd) glr.timestamp = com_eventTime; glr.drawframe++; - glr.rand_seed = fd->time * 20; glr.fd = *fd; glr.num_beams = 0; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index b6f931783..18a14fa70 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -273,7 +273,7 @@ static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t c if (max_segments <= MIN_LIGHTNING_SEGMENTS) num_segments = max_segments; else - num_segments = MIN_LIGHTNING_SEGMENTS + GL_rand() % (max_segments - MIN_LIGHTNING_SEGMENTS + 1); + num_segments = MIN_LIGHTNING_SEGMENTS + Com_SlowRand() % (max_segments - MIN_LIGHTNING_SEGMENTS + 1); if (num_segments > 1) MakeNormalVectors(dir, right, up); @@ -285,10 +285,10 @@ static void GL_DrawLightningBeam(const vec3_t start, const vec3_t end, color_t c VectorMA(start, i * segment_length, dir, point); - offs = GL_crand() * (segment_length * 0.35f); + offs = Com_SlowCrand() * (segment_length * 0.35f); VectorMA(point, offs, right, point); - offs = GL_crand() * (segment_length * 0.35f); + offs = Com_SlowCrand() * (segment_length * 0.35f); VectorMA(point, offs, up, segments[i]); } From e224008785e7e241196151c314a79fc4560007a6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 13:25:12 +0300 Subject: [PATCH 615/974] Remove unused MakeNormalVectors() call. --- src/client/newfx.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/newfx.c b/src/client/newfx.c index 15993575d..e0f0bbe0c 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -58,14 +58,11 @@ void CL_DebugTrail(const vec3_t start, const vec3_t end) float len; cparticle_t *p; float dec; - vec3_t right, up; VectorCopy(start, move); VectorSubtract(end, start, vec); len = VectorNormalize(vec); - MakeNormalVectors(vec, right, up); - dec = 3; VectorScale(vec, dec, vec); VectorCopy(start, move); From 414c2e547f75e17ecfe7f2808c591f64410dc99e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 13:25:35 +0300 Subject: [PATCH 616/974] Count 2D, beam triangles, etc for debug stats. --- src/refresh/tess.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 18a14fa70..20a0a7f73 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -569,6 +569,7 @@ static void GL_DrawIndexed(showtris_t showtris) } GL_DrawTriangles(tess.numindices, indices); + c.trisDrawn += tess.numindices / 3; if (gl_showtris->integer & showtris) GL_DrawOutlines(tess.numindices, indices, true); @@ -723,7 +724,6 @@ static void GL_DrawFace(const mface_t *surf) tess.texnum[2] = texnum[2]; tess.flags = surf->statebits; - c.trisDrawn += numtris; c.facesTris += numtris; c.facesDrawn++; } From abe74eedfc67746cca71f971eae9585dd7ae8de0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 16:23:42 +0300 Subject: [PATCH 617/974] Avoid member access within null pointer. --- src/refresh/mesh.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 9e230c4ed..d58ca9de5 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -585,7 +585,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, } GL_BindBuffer(GL_ARRAY_BUFFER, buffer); - gl_backend->tex_coord_pointer(tcoords->st); + gl_backend->tex_coord_pointer((const GLfloat *)tcoords); GL_LockArrays(num_verts); From 124e8152bbfb173a1aa97da35f0fd5420dd19d10 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Aug 2024 09:14:44 -0400 Subject: [PATCH 618/974] Added BOTLIB_BotCountManager but will continue in another branch --- src/action/botlib/botlib_spawn.c | 114 +++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index d940f6874..2b627ac53 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -189,47 +189,111 @@ qboolean BOTLIB_SaveBotsFromPreviousMap(void) return true; } +// This will return when we should rotate bots, default false +qboolean BotCountManagerTimer(void) +{ + // Consider rotating when the map has freshly loaded + if (level.framenum < 100) { + BOTLIB_Debug("%s: BotCountManagerTimer: framenum\n"); + return true; + } + // Consider rotating when the map has ended + if (level.intermission_framenum) { + BOTLIB_Debug("%s: BotCountManagerTimer: intermission\n"); + return true; + } + // Consider rotating at 5% chance + if (rand() % 100 < 5) { + BOTLIB_Debug("%s: BotCountManagerTimer: percent\n"); + return true; + } + + return false; +} + int BOTLIB_BotCountManager(void) { + int bots_desired = bot_connections.desire_bots; + gi.dprintf("%s: desire_bots[%i]\n", __func__, bot_connections.desire_bots); + // If we aren't rotating bots, just return existing desire_bots value - if (!bot_rotate->value) - return bot_connections.desire_bots; + if (!BotCountManagerTimer() || !bot_rotate->value) + return (bots_desired - bot_connections.total_bots); int bcmin = bot_count_min->value; int bcmax = bot_count_max->value; - int bc = bot_connections.desire_bots; - // int bc1 = bot_connections.desire_team1; - // int bc2 = bot_connections.desire_team2; - // int bc3 = bot_connections.desire_team3; - // int tbots = bot_connections.total_bots; + + // Percent is used in tandem with BotCountManagerTimer to determine when to rotate bots + float percent = (float)(bot_connections.total_humans_playing + bot_connections.total_bots) / maxclients->value * 100; + + // This value determines if we add, remove or keep the same amount of bots + // 1 = add, -1 = remove, 0 = keep the same + int up_down_same = 0; // Basic value validation if (bcmin < 0) { gi.dprintf("%s: bot_count_min < 0, setting it to 0\n", __func__); + gi.cvar_forceset(bot_count_min->name, "0"); bcmin = 0; } if (bcmax > maxclients->value) { gi.dprintf("%s: bot_count_max > maxclients, setting it to maxclients\n", __func__); + gi.cvar_forceset(bot_count_max->name, va("%s", maxclients->name)); bcmax = maxclients->value; } if (bcmin > bcmax) { gi.dprintf("%s: bot_count_min > bot_count_max, setting them to be the same\n", __func__); - gi.cvar_forceset(bot_count_min->string, va("%s", bot_count_max->string)); + gi.cvar_forceset(bot_count_min->name, va("%s", bot_count_max->name)); bcmin = bcmax; } + if (bcmin == 0 && bcmax == 0) + { + gi.dprintf("%s: bot_count_min and bot_count_max are both 0, not rotating bots, setting bot_rotate to 0\n", __func__); + gi.cvar_forceset(bot_rotate->name, "0"); + return (bots_desired - bot_connections.total_bots); + } - // Choose a random number between bcmin and bcmax, minimum count of bots is bcmin - int bot_count_rotate = bcmin + rand() % (bcmax - bcmin + 1); + //The following logic only applies if BotCountManagerTimer() returns true + + /* + If we have more than 80% of the server filled with players and bots, remove bots + else if we're low on clients, consider adding bots + else keep the same amount of bots + */ + + // Don't add more bots if we're at the max + BOTLIB_Debug("%s: desire_bots[%i] total_bots[%i] total_humans[%i] percent[%f]\n", __func__, bots_desired, bot_connections.total_bots, bot_connections.total_humans_playing, percent); + if (bot_connections.total_bots < bcmax) { + if (rand() % 2 == 0) { + if (percent > 80) + up_down_same = -1; + else if (percent < 30) + up_down_same = 1; + else + up_down_same = 0; + } else { + // Bot just feels like leaving + if (rand() % 100 < 5) + up_down_same = -1; + } + } else { + return (bot_connections.desire_bots - bot_connections.total_bots); + } + // This will add, remove or keep the same amount of bots + bot_connections.desire_bots += up_down_same; + return (bot_connections.desire_bots); - // Make the change - if (bot_count_rotate == bc){ - return bc; // No change - } else { // Set desired bots to be how many we rotate in or out - bot_connections.desire_bots = bot_count_rotate; - return bot_count_rotate; - } + // // Choose a random number between bcmin and bcmax, minimum count of bots is bcmin + // int bot_count_rotate = bcmin + rand() % (bcmax - bcmin + 1); + // // Make the change + // if (bot_count_rotate == bc){ + // return bc; // No change + // } else { // Set desired bots to be how many we rotate in or out + // bot_connections.desire_bots = bot_count_rotate; + // return bot_count_rotate; + // } } // Add bots from the previous map from file @@ -2056,12 +2120,18 @@ void BOTLIB_CheckBotRules(void) //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); + int bots_to_spawn = 0; - if (bot_rotate->value) { - bots_to_spawn = BOTLIB_BotCountManager(); - } else { - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; - } + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + + // TODO: Fix BOTLIB_BotCountManager() to work + // if (!teamplay->value) { + // if (bot_rotate->value) { + // bots_to_spawn = BOTLIB_BotCountManager(); + // } else { + // bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + // } + // } // Shuffle bots around //if (teamplay->value && bots_to_spawn == 0) From 4574f2d8ffb70dff60f752e055582cc3cdb6c8aa Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 26 Aug 2024 14:43:50 -0400 Subject: [PATCH 619/974] Added debug info, cleaned up some comments --- inc/common/bsp.h | 4 ++-- inc/shared/game.h | 10 ---------- src/server/game.c | 4 ++-- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index bd85a2db0..0cb320e7a 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -575,9 +575,9 @@ typedef struct { //#if USE_REF lightgrid_t lightgrid; - bool lm_decoupled; + qboolean lm_decoupled; //#endif - bool extended; + qboolean extended; char name[1]; } bsp_t; diff --git a/inc/shared/game.h b/inc/shared/game.h index 9739d3e56..61ff26b01 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -230,27 +230,17 @@ typedef struct { #if AQTION_EXTENSION void *(*CheckForExtension)(char *text); #endif -//rekkie -- BSP -- s bsp_t* (*Bsp)(void); - //rekkie -- BSP -- e - //rekkie -- surface data -- s nav_t* (*Nav)(void); - //rekkie -- debug drawing -- s #if DEBUG_DRAWING debug_draw_t* (*Draw)(void); -//#if USE_REF -// void (*GL_DrawArrow)(vec3_t start, vec3_t end, const uint32_t color, float line_width); -//#endif #endif surface_data_t* (*SurfaceData)(void); - //rekkie -- surface data -- e - //rekkie -- Fake Bot Client -- s void (*SV_BotUpdateInfo)(char* name, int ping, int score); void (*SV_BotConnect)(char* name); void (*SV_BotDisconnect)(char* name); void (*SV_BotClearClients)(void); - //rekkie -- Fake Bot Client -- e } game_import_t; // // functions exported by the game subsystem diff --git a/src/server/game.c b/src/server/game.c index cd861aced..c6364b99d 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1170,12 +1170,12 @@ void SV_InitGameProgs(void) unsigned max_size = INT_MAX / svs.csr.max_edicts; if (ge->edict_size < min_size || ge->edict_size > max_size || ge->edict_size % q_alignof(edict_t)) { - Com_Error(ERR_DROP, "Game library returned bad size of edict_t"); + Com_Error(ERR_DROP, "Game library returned bad size of edict_t: %i", ge->edict_size); } // sanitize max_edicts if (ge->max_edicts <= sv_maxclients->integer || ge->max_edicts > svs.csr.max_edicts) { - Com_Error(ERR_DROP, "Game library returned bad number of max_edicts"); + Com_Error(ERR_DROP, "Game library returned bad number of max_edicts: %i", ge->max_edicts); } #if AQTION_EXTENSION From 4afbbd6f3842f3e6c17c24a0080b17a664d04a9a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 25 Aug 2024 18:31:13 +0300 Subject: [PATCH 620/974] Clean up surface texnums and TMU management. Use symbolic names for TMU constants. Avoid storing GL texnums in mface_t struct. --- inc/common/bsp.h | 1 - src/refresh/draw.c | 10 ++++----- src/refresh/gl.h | 9 ++++++-- src/refresh/legacy.c | 16 ++++++------- src/refresh/main.c | 8 +++---- src/refresh/mesh.c | 10 ++++----- src/refresh/shader.c | 6 ++--- src/refresh/sky.c | 2 +- src/refresh/state.c | 2 +- src/refresh/surf.c | 21 ++++++++--------- src/refresh/tess.c | 52 +++++++++++++++++++++---------------------- src/refresh/texture.c | 22 +++++++++--------- 12 files changed, 79 insertions(+), 80 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index e00912227..c0cbb6684 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -98,7 +98,6 @@ typedef struct mface_s { uint16_t lm_width; uint16_t lm_height; - int texnum[3]; // FIXME MAX_TMUS int drawflags; // DSURF_PLANEBACK, etc int statebits; int firstvert; diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 993d19e74..a73574abf 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -30,10 +30,10 @@ static inline void GL_StretchPic_( if (tess.numverts + 4 > TESS_MAX_VERTICES || tess.numindices + 6 > TESS_MAX_INDICES || - (tess.numverts && tess.texnum[0] != texnum)) + (tess.numverts && tess.texnum[TMU_TEXTURE] != texnum)) GL_Flush2D(); - tess.texnum[0] = texnum; + tess.texnum[TMU_TEXTURE] = texnum; dst_vert = tess.vertices + tess.numverts * 5; Vector4Set(dst_vert, x, y, s1, t1); @@ -78,10 +78,10 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) if (tess.numverts + 8 > TESS_MAX_VERTICES || tess.numindices + 24 > TESS_MAX_INDICES || - (tess.numverts && tess.texnum[0] != TEXNUM_WHITE)) + (tess.numverts && tess.texnum[TMU_TEXTURE] != TEXNUM_WHITE)) GL_Flush2D(); - tess.texnum[0] = TEXNUM_WHITE; + tess.texnum[TMU_TEXTURE] = TEXNUM_WHITE; int x = 0, y = 0; int w = glr.fd.width, h = glr.fd.height; @@ -340,7 +340,7 @@ void R_DrawStretchRaw(int x, int y, int w, int h) void R_UpdateRawPic(int pic_w, int pic_h, const uint32_t *pic) { - GL_ForceTexture(0, TEXNUM_RAW); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_RAW); qglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pic_w, pic_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pic); } diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 7fd3622e4..becf38ecb 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -47,8 +47,6 @@ typedef GLushort glIndex_t; typedef GLuint glIndex_t; #endif -#define MAX_TMUS 3 - #define TAB_SIN(x) gl_static.sintab[(x) & 255] #define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] @@ -542,6 +540,13 @@ typedef enum { GLA_COLOR = BIT(VERT_ATTR_COLOR), } glArrayBits_t; +typedef enum { + TMU_TEXTURE, + TMU_LIGHTMAP, + TMU_GLOWMAP, + MAX_TMUS +} glTmu_t; + typedef struct { GLuint client_tmu; GLuint server_tmu; diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index 1d0e6a4ec..d31d07482 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -34,7 +34,7 @@ static void legacy_state_bits(GLbitfield bits) } if (diff & GLS_TEXTURE_REPLACE) { - GL_ActiveTexture(0); + GL_ActiveTexture(TMU_TEXTURE); if (bits & GLS_TEXTURE_REPLACE) qglTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); else @@ -42,7 +42,7 @@ static void legacy_state_bits(GLbitfield bits) } if (diff & GLS_SCROLL_MASK) { - GL_ActiveTexture(0); + GL_ActiveTexture(TMU_TEXTURE); qglMatrixMode(GL_TEXTURE); qglLoadIdentity(); @@ -54,7 +54,7 @@ static void legacy_state_bits(GLbitfield bits) } if (diff & GLS_LIGHTMAP_ENABLE) { - GL_ActiveTexture(1); + GL_ActiveTexture(TMU_LIGHTMAP); if (bits & GLS_LIGHTMAP_ENABLE) qglEnable(GL_TEXTURE_2D); else @@ -93,7 +93,7 @@ static void legacy_array_bits(GLbitfield bits) } if (diff & GLA_TC) { - GL_ClientActiveTexture(0); + GL_ClientActiveTexture(TMU_TEXTURE); if (bits & GLA_TC) qglEnableClientState(GL_TEXTURE_COORD_ARRAY); else @@ -101,7 +101,7 @@ static void legacy_array_bits(GLbitfield bits) } if (diff & GLA_LMTC) { - GL_ClientActiveTexture(1); + GL_ClientActiveTexture(TMU_LIGHTMAP); if (bits & GLA_LMTC) qglEnableClientState(GL_TEXTURE_COORD_ARRAY); else @@ -124,13 +124,13 @@ static void legacy_array_pointers(const glVaDesc_t *desc, const GLfloat *ptr) desc++; if (desc->size) { - GL_ClientActiveTexture(0); + GL_ClientActiveTexture(TMU_TEXTURE); qglTexCoordPointer(desc->size, GL_FLOAT, desc->stride, (void *)(base + desc->offset)); } desc++; if (desc->size && lm.nummaps) { - GL_ClientActiveTexture(1); + GL_ClientActiveTexture(TMU_LIGHTMAP); qglTexCoordPointer(desc->size, GL_FLOAT, desc->stride, (void *)(base + desc->offset)); } desc++; @@ -143,7 +143,7 @@ static void legacy_array_pointers(const glVaDesc_t *desc, const GLfloat *ptr) static void legacy_tex_coord_pointer(const GLfloat *ptr) { - GL_ClientActiveTexture(0); + GL_ClientActiveTexture(TMU_TEXTURE); qglTexCoordPointer(2, GL_FLOAT, 0, ptr); } diff --git a/src/refresh/main.c b/src/refresh/main.c index 1a469b7b4..17ba51740 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -341,7 +341,7 @@ static void GL_DrawSpriteModel(const model_t *model) } GL_LoadMatrix(glr.viewmatrix); - GL_BindTexture(0, image->texnum); + GL_BindTexture(TMU_TEXTURE, image->texnum); GL_BindArrays(VA_SPRITE); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC); @@ -389,7 +389,7 @@ static void GL_DrawNullModel(void) WN32(tess.vertices + 23, U32_BLUE); GL_LoadMatrix(glr.viewmatrix); - GL_BindTexture(0, TEXNUM_WHITE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_BindArrays(VA_NULLMODEL); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); @@ -458,7 +458,7 @@ static void GL_OccludeFlares(void) if (!set) { GL_LoadMatrix(glr.viewmatrix); - GL_BindTexture(0, TEXNUM_WHITE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_BindArrays(VA_OCCLUDE); GL_StateBits(GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX); @@ -621,7 +621,7 @@ static void GL_WaterWarp(void) { float x0, x1, y0, y1; - GL_ForceTexture(0, gl_static.warp_texture); + GL_ForceTexture(TMU_TEXTURE, gl_static.warp_texture); GL_BindArrays(VA_WATERWARP); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | GLS_TEXTURE_REPLACE | GLS_WARP_ENABLE); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index d58ca9de5..6be738ff0 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -407,7 +407,7 @@ static void draw_celshading(const glIndex_t *indices, int num_indices) if (celscale < 0.01f) return; - GL_BindTexture(0, TEXNUM_BLACK); + GL_BindTexture(TMU_TEXTURE, TEXNUM_BLACK); GL_StateBits(GLS_BLEND_BLEND); GL_ArrayBits(GLA_VERTEX); @@ -491,7 +491,7 @@ static void draw_shadow(const glIndex_t *indices, int num_indices) } GL_StateBits(GLS_BLEND_BLEND); - GL_BindTexture(0, TEXNUM_WHITE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_ArrayBits(GLA_VERTEX); qglEnable(GL_POLYGON_OFFSET_FILL); @@ -551,7 +551,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, if ((glr.ent->flags & (RF_TRANSLUCENT | RF_WEAPONMODEL | RF_FULLBRIGHT)) == (RF_TRANSLUCENT | RF_WEAPONMODEL)) { GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX); - GL_BindTexture(0, TEXNUM_WHITE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); qglColorMask(0, 0, 0, 0); GL_LockArrays(num_verts); @@ -572,10 +572,10 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, GL_StateBits(state); - GL_BindTexture(0, skin->texnum); + GL_BindTexture(TMU_TEXTURE, skin->texnum); if (skin->glow_texnum) - GL_BindTexture(2, skin->glow_texnum); + GL_BindTexture(TMU_GLOWMAP, skin->glow_texnum); if (dotshading) { GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 7ddfc1efe..e7b77e08b 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -255,11 +255,11 @@ static GLuint create_and_use_program(GLbitfield bits) qglUseProgram(program); - qglUniform1i(qglGetUniformLocation(program, "u_texture"), 0); + qglUniform1i(qglGetUniformLocation(program, "u_texture"), TMU_TEXTURE); if (bits & GLS_LIGHTMAP_ENABLE) - qglUniform1i(qglGetUniformLocation(program, "u_lightmap"), 1); + qglUniform1i(qglGetUniformLocation(program, "u_lightmap"), TMU_LIGHTMAP); if (bits & GLS_GLOWMAP_ENABLE) - qglUniform1i(qglGetUniformLocation(program, "u_glowmap"), 2); + qglUniform1i(qglGetUniformLocation(program, "u_glowmap"), TMU_GLOWMAP); return program; } diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 67a3d0164..527e69917 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -342,7 +342,7 @@ void R_DrawSkyBox(void) skymins[1][i] >= skymaxs[1][i]) continue; - GL_BindTexture(0, sky_images[i]); + GL_BindTexture(TMU_TEXTURE, sky_images[i]); MakeSkyVec(skymaxs[0][i], skymins[1][i], i, tess.vertices); MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); diff --git a/src/refresh/state.c b/src/refresh/state.c index 999210926..c2b712a21 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -270,7 +270,7 @@ void GL_Setup3D(bool waterwarp) void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed) { - GL_BindTexture(0, TEXNUM_WHITE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE); GL_ArrayBits(GLA_VERTEX); GL_DepthRange(0, 0); diff --git a/src/refresh/surf.c b/src/refresh/surf.c index a35253363..36014109b 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -322,7 +322,7 @@ void GL_UploadLightmaps(void) } // upload lightmap subimage - GL_ForceTexture(1, lm.texnums[i]); + GL_ForceTexture(TMU_LIGHTMAP, lm.texnums[i]); qglTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, LM_PIXELS(m, x, y)); clear_dirty_region(m); @@ -361,7 +361,7 @@ static void LM_UploadBlock(void) lightmap_t *m = &lm.lightmaps[lm.nummaps]; clear_dirty_region(m); - GL_ForceTexture(1, lm.texnums[lm.nummaps]); + GL_ForceTexture(TMU_LIGHTMAP, lm.texnums[lm.nummaps]); qglTexImage2D(GL_TEXTURE_2D, 0, lm.comp, lm.block_size, lm.block_size, 0, GL_RGBA, GL_UNSIGNED_BYTE, m->buffer); @@ -493,7 +493,6 @@ static void LM_BuildSurface(mface_t *surf) surf->light_s = s; surf->light_t = t; surf->light_m = &lm.lightmaps[lm.nummaps]; - surf->texnum[1] = lm.texnums[lm.nummaps]; // build the primary lightmap build_primary_lightmap(surf); @@ -517,7 +516,7 @@ static void LM_RebuildSurfaces(void) // upload all lightmaps for (i = 0, m = lm.lightmaps; i < lm.nummaps; i++, m++) { - GL_ForceTexture(1, lm.texnums[i]); + GL_ForceTexture(TMU_LIGHTMAP, lm.texnums[i]); qglTexImage2D(GL_TEXTURE_2D, 0, lm.comp, lm.block_size, lm.block_size, 0, GL_RGBA, GL_UNSIGNED_BYTE, m->buffer); @@ -560,15 +559,9 @@ static void build_surface_poly(mface_t *surf, vec_t *vbo) const mvertex_t *src_vert; const medge_t *src_edge; const mtexinfo_t *texinfo = surf->texinfo; + const uint32_t color = color_for_surface(surf); vec2_t scale, tc, mins, maxs; int i, bmins[2], bmaxs[2]; - uint32_t color; - - surf->texnum[0] = texinfo->image->texnum; - surf->texnum[1] = 0; - surf->texnum[2] = texinfo->image->glow_texnum; - - color = color_for_surface(surf); // convert surface flags to state bits surf->statebits = GLS_DEFAULT; @@ -794,7 +787,11 @@ static void duplicate_surface_lmtc(const mface_t *surf, vec_t *vbo) static void calc_surface_hash(mface_t *surf) { - uint32_t args[] = { surf->texnum[0], surf->texnum[1], surf->texnum[2], surf->statebits }; + uint32_t args[] = { + surf->texinfo->image - r_images, + surf->light_m ? surf->light_m - lm.lightmaps : 0, + surf->statebits + }; struct mdfour md; uint8_t out[16]; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 20a0a7f73..7222d88a1 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -43,7 +43,7 @@ void GL_Flush2D(void) Scrap_Upload(); - GL_BindTexture(0, tess.texnum[0]); + GL_BindTexture(TMU_TEXTURE, tess.texnum[TMU_TEXTURE]); GL_BindArrays(VA_2D); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); @@ -53,7 +53,7 @@ void GL_Flush2D(void) tess.numindices = 0; tess.numverts = 0; - tess.texnum[0] = 0; + tess.texnum[TMU_TEXTURE] = 0; tess.flags = 0; } @@ -82,7 +82,7 @@ void GL_DrawParticles(void) p = glr.fd.particles; total = glr.fd.num_particles; do { - GL_BindTexture(0, TEXNUM_PARTICLE); + GL_BindTexture(TMU_TEXTURE, TEXNUM_PARTICLE); GL_StateBits(bits); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); @@ -146,7 +146,7 @@ static void GL_FlushBeamSegments(void) else array |= GLA_TC; - GL_BindTexture(0, texnum); + GL_BindTexture(TMU_TEXTURE, texnum); GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(array); GL_DrawIndexed(SHOWTRIS_FX); @@ -355,13 +355,13 @@ static void GL_FlushFlares(void) if (!tess.numindices) return; - GL_BindTexture(0, tess.texnum[0]); + GL_BindTexture(TMU_TEXTURE, tess.texnum[TMU_TEXTURE]); GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); GL_DrawIndexed(SHOWTRIS_FX); tess.numverts = tess.numindices = 0; - tess.texnum[0] = 0; + tess.texnum[TMU_TEXTURE] = 0; } void GL_DrawFlares(void) @@ -418,10 +418,10 @@ void GL_DrawFlares(void) if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || tess.numindices + 6 > TESS_MAX_INDICES) || - (tess.numindices && tess.texnum[0] != texnum)) + (tess.numindices && tess.texnum[TMU_TEXTURE] != texnum)) GL_FlushFlares(); - tess.texnum[0] = texnum; + tess.texnum[TMU_TEXTURE] = texnum; scale = 25.0f * (ent->scale * q->frac); @@ -607,14 +607,14 @@ void GL_Flush3D(void) if (!tess.numindices) return; - if (q_likely(tess.texnum[1])) { + if (q_likely(tess.texnum[TMU_LIGHTMAP])) { state |= GLS_LIGHTMAP_ENABLE; array |= GLA_LMTC; if (q_unlikely(gl_lightmap->integer)) state &= ~GLS_INTENSITY_ENABLE; - if (tess.texnum[2]) + if (tess.texnum[TMU_GLOWMAP]) state |= GLS_GLOWMAP_ENABLE; } @@ -627,7 +627,7 @@ void GL_Flush3D(void) if (qglBindTextures) { #if USE_DEBUG if (q_unlikely(gl_nobind->integer)) - tess.texnum[0] = TEXNUM_DEFAULT; + tess.texnum[TMU_TEXTURE] = TEXNUM_DEFAULT; #endif int count = 0; for (int i = 0; i < MAX_TMUS && tess.texnum[i]; i++) { @@ -647,7 +647,7 @@ void GL_Flush3D(void) c.batchesDrawn++; - tess.texnum[0] = tess.texnum[1] = tess.texnum[2] = 0; + memset(tess.texnum, 0, sizeof(tess.texnum)); tess.numindices = 0; tess.numverts = 0; tess.flags = 0; @@ -682,25 +682,25 @@ static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) static void GL_DrawFace(const mface_t *surf) { + const image_t *image = GL_TextureAnimation(surf->texinfo); int numtris = surf->numsurfedges - 2; int numindices = numtris * 3; - GLuint texnum[MAX_TMUS]; + GLuint texnum[MAX_TMUS] = { 0 }; glIndex_t *dst_indices; int i, j; - if (q_unlikely(gl_lightmap->integer && surf->texnum[1])) { - texnum[0] = TEXNUM_WHITE; - texnum[2] = 0; - } else { - const image_t *tex = GL_TextureAnimation(surf->texinfo); - texnum[0] = tex->texnum; - texnum[2] = surf->texnum[1] ? tex->glow_texnum : 0; + texnum[TMU_TEXTURE] = image->texnum; + if (q_likely(surf->light_m)) { + texnum[TMU_LIGHTMAP] = lm.texnums[surf->light_m - lm.lightmaps]; + texnum[TMU_GLOWMAP ] = image->glow_texnum; + + if (q_unlikely(gl_lightmap->integer)) { + texnum[TMU_TEXTURE] = TEXNUM_WHITE; + texnum[TMU_GLOWMAP] = 0; + } } - texnum[1] = surf->texnum[1]; - if (tess.texnum[0] != texnum[0] || - tess.texnum[1] != texnum[1] || - tess.texnum[2] != texnum[2] || + if (memcmp(tess.texnum, texnum, sizeof(texnum)) || tess.flags != surf->statebits || tess.numindices + numindices > TESS_MAX_INDICES) GL_Flush3D(); @@ -719,9 +719,7 @@ static void GL_DrawFace(const mface_t *surf) } tess.numindices += numindices; - tess.texnum[0] = texnum[0]; - tess.texnum[1] = texnum[1]; - tess.texnum[2] = texnum[2]; + memcpy(tess.texnum, texnum, sizeof(texnum)); tess.flags = surf->statebits; c.facesTris += numtris; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 49f0d7967..0d9def578 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -82,11 +82,11 @@ static void update_image_params(unsigned mask) if (!(mask & BIT(image->type))) continue; - GL_ForceTexture(0, image->texnum); + GL_ForceTexture(TMU_TEXTURE, image->texnum); GL_SetFilterAndRepeat(image->type, image->flags); if (image->glow_texnum) { - GL_ForceTexture(0, image->glow_texnum); + GL_ForceTexture(TMU_TEXTURE, image->glow_texnum); GL_SetFilterAndRepeat(image->type, image->flags); } } @@ -275,7 +275,7 @@ void Scrap_Upload(void) if (!scrap_dirty) return; - GL_ForceTexture(0, TEXNUM_SCRAP); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_SCRAP); // make a copy so that effects like gamma scaling don't accumulate data = FS_AllocTempMem(sizeof(scrap_data)); @@ -698,7 +698,7 @@ void IMG_Load(image_t *image, byte *pic) scrap_dirty = true; } else { qglGenTextures(1, &image->texnum); - GL_ForceTexture(0, image->texnum); + GL_ForceTexture(TMU_TEXTURE, image->texnum); maxlevel = GL_UpscaleLevel(width, height, image->type, image->flags); if (maxlevel) { @@ -853,7 +853,7 @@ static void GL_InitDefaultTexture(void) } } - GL_ForceTexture(0, TEXNUM_DEFAULT); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_DEFAULT); GL_Upload32(pixels, 8, 8, 0, IT_WALL, IF_TURBULENT); GL_SetFilterAndRepeat(IT_WALL, IF_TURBULENT); @@ -910,7 +910,7 @@ static void GL_InitParticleTexture(void) } } - GL_ForceTexture(0, TEXNUM_PARTICLE); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_PARTICLE); GL_Upload32(pixels, 16, 16, 0, IT_SPRITE, flags); GL_SetFilterAndRepeat(IT_SPRITE, flags); } @@ -920,12 +920,12 @@ static void GL_InitWhiteImage(void) uint32_t pixel; pixel = U32_WHITE; - GL_ForceTexture(0, TEXNUM_WHITE); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_Upload32((byte *)&pixel, 1, 1, 0, IT_SPRITE, IF_REPEAT | IF_NEAREST); GL_SetFilterAndRepeat(IT_SPRITE, IF_REPEAT | IF_NEAREST); pixel = U32_BLACK; - GL_ForceTexture(0, TEXNUM_BLACK); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_BLACK); GL_Upload32((byte *)&pixel, 1, 1, 0, IT_SPRITE, IF_REPEAT | IF_NEAREST); GL_SetFilterAndRepeat(IT_SPRITE, IF_REPEAT | IF_NEAREST); } @@ -950,14 +950,14 @@ static void GL_InitBeamTexture(void) } } - GL_ForceTexture(0, TEXNUM_BEAM); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_BEAM); GL_Upload32(pixels, 16, 16, 0, IT_SPRITE, IF_NONE); GL_SetFilterAndRepeat(IT_SPRITE, IF_NONE); } static void GL_InitRawTexture(void) { - GL_ForceTexture(0, TEXNUM_RAW); + GL_ForceTexture(TMU_TEXTURE, TEXNUM_RAW); GL_SetFilterAndRepeat(IT_PIC, IF_NONE); } @@ -965,7 +965,7 @@ bool GL_InitWarpTexture(void) { GL_ClearErrors(); - GL_ForceTexture(0, gl_static.warp_texture); + GL_ForceTexture(TMU_TEXTURE, gl_static.warp_texture); qglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, glr.fd.width, glr.fd.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); qglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); qglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); From 82a2b3dbaefa6e7094e1a1dc567843f39b5a6514 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 26 Aug 2024 16:18:27 +0300 Subject: [PATCH 621/974] Replace memcpy() with WN32(). --- src/refresh/surf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 36014109b..5254e3a16 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -625,7 +625,7 @@ static void build_surface_poly(mface_t *surf, vec_t *vbo) VectorCopy(src_vert->point, vbo); // vertex color - memcpy(vbo + 3, &color, sizeof(color)); + WN32(vbo + 3, color); // texture0 coordinates tc[0] = DotProductDouble(vbo, texinfo->axis[0]) + texinfo->offset[0]; From 0c97edaa484b66c5ac0f7bf69083ac50fd5e8aa0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 26 Aug 2024 16:19:01 +0300 Subject: [PATCH 622/974] Add typedef for BoxOnPlaneSide() result. --- inc/common/math.h | 12 +++++++----- src/common/cmodel.c | 4 +--- src/common/math.c | 8 ++++---- src/refresh/main.c | 4 ++-- src/refresh/world.c | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/inc/common/math.h b/inc/common/math.h index 08e5d7b04..91ce8c4be 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -32,13 +32,15 @@ int DirToByte(const vec3_t dir); void SetPlaneType(cplane_t *plane); void SetPlaneSignbits(cplane_t *plane); -#define BOX_INFRONT 1 -#define BOX_BEHIND 2 -#define BOX_INTERSECTS 3 +typedef enum { + BOX_INFRONT = 1, + BOX_BEHIND = 2, + BOX_INTERSECTS = 3 +} box_plane_t; -int BoxOnPlaneSide(const vec3_t emins, const vec3_t emaxs, const cplane_t *p); +box_plane_t BoxOnPlaneSide(const vec3_t emins, const vec3_t emaxs, const cplane_t *p); -static inline int BoxOnPlaneSideFast(const vec3_t emins, const vec3_t emaxs, const cplane_t *p) +static inline box_plane_t BoxOnPlaneSideFast(const vec3_t emins, const vec3_t emaxs, const cplane_t *p) { // fast axial cases if (p->type < 3) { diff --git a/src/common/cmodel.c b/src/common/cmodel.c index 31e97f075..d3ea14200 100644 --- a/src/common/cmodel.c +++ b/src/common/cmodel.c @@ -345,10 +345,8 @@ static const mnode_t *leaf_topnode; static void CM_BoxLeafs_r(const mnode_t *node) { - int s; - while (node->plane) { - s = BoxOnPlaneSideFast(leaf_mins, leaf_maxs, node->plane); + box_plane_t s = BoxOnPlaneSideFast(leaf_mins, leaf_maxs, node->plane); if (s == BOX_INFRONT) { node = node->children[0]; } else if (s == BOX_BEHIND) { diff --git a/src/common/math.c b/src/common/math.c index a3ad1e196..53659ca81 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -319,7 +319,7 @@ BoxOnPlaneSide Returns 1, 2, or 1 + 2 ================== */ -int BoxOnPlaneSide(const vec3_t emins, const vec3_t emaxs, const cplane_t *p) +box_plane_t BoxOnPlaneSide(const vec3_t emins, const vec3_t emaxs, const cplane_t *p) { const vec_t *bounds[2] = { emins, emaxs }; int i = p->signbits & 1; @@ -331,9 +331,9 @@ int BoxOnPlaneSide(const vec3_t emins, const vec3_t emaxs, const cplane_t *p) p->normal[1] * bounds[j][1] + \ p->normal[2] * bounds[k][2] - vec_t dist1 = P(i ^ 1, j ^ 1, k ^ 1); - vec_t dist2 = P(i, j, k); - int sides = 0; + vec_t dist1 = P(i ^ 1, j ^ 1, k ^ 1); + vec_t dist2 = P(i, j, k); + box_plane_t sides = 0; #undef P diff --git a/src/refresh/main.c b/src/refresh/main.c index 17ba51740..b12e53481 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -131,14 +131,14 @@ static void GL_SetupFrustum(void) glCullResult_t GL_CullBox(const vec3_t bounds[2]) { - int i, bits; + box_plane_t bits; glCullResult_t cull; if (!gl_cull_models->integer) return CULL_IN; cull = CULL_IN; - for (i = 0; i < 4; i++) { + for (int i = 0; i < 4; i++) { bits = BoxOnPlaneSide(bounds[0], bounds[1], &glr.frustumPlanes[i]); if (bits == BOX_BEHIND) return CULL_OUT; diff --git a/src/refresh/world.c b/src/refresh/world.c index 397ab587f..efc3a0868 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -488,12 +488,12 @@ void GL_DrawBspModel(mmodel_t *model) static inline bool GL_ClipNode(const mnode_t *node, int *clipflags) { int flags = *clipflags; - int i, bits, mask; + box_plane_t bits; if (flags == NODE_UNCLIPPED) return true; - for (i = 0, mask = 1; i < 4; i++, mask <<= 1) { + for (int i = 0, mask = 1; i < 4; i++, mask <<= 1) { if (flags & mask) continue; bits = BoxOnPlaneSide(node->mins, node->maxs, From 27187f840a7709b810811f88ea23b609fc998496 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 26 Aug 2024 17:13:18 +0300 Subject: [PATCH 623/974] Clean up some GL types usage. --- src/refresh/gl.h | 153 +++++++++++++++++++++---------------------- src/refresh/legacy.c | 8 +-- src/refresh/shader.c | 14 ++-- src/refresh/state.c | 15 +++-- 4 files changed, 96 insertions(+), 94 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index becf38ecb..26aa6dbac 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -53,34 +53,6 @@ typedef GLuint glIndex_t; #define MAX_PROGRAMS 128 #define NUM_TEXNUMS 7 -typedef struct { - uint8_t size; - bool type; - uint8_t stride; - uint8_t offset; -} glVaDesc_t; - -typedef struct { - const char *name; - - void (*init)(void); - void (*shutdown)(void); - void (*clear_state)(void); - void (*setup_2d)(void); - void (*setup_3d)(void); - - void (*load_proj_matrix)(const GLfloat *matrix); - void (*load_view_matrix)(const GLfloat *matrix); - - void (*state_bits)(GLbitfield bits); - void (*array_bits)(GLbitfield bits); - - void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); - void (*tex_coord_pointer)(const GLfloat *ptr); - - void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); -} glbackend_t; - typedef struct { GLuint query; float frac; @@ -89,22 +61,6 @@ typedef struct { bool visible; } glquery_t; -typedef enum { - VA_NONE, - - VA_SPRITE, - VA_EFFECT, - VA_NULLMODEL, - VA_OCCLUDE, - VA_WATERWARP, - VA_MESH_SHADE, - VA_MESH_FLAT, - VA_2D, - VA_3D, - - VA_TOTAL -} glVertexArray_t; - typedef struct { bool registering; bool use_shaders; @@ -540,6 +496,22 @@ typedef enum { GLA_COLOR = BIT(VERT_ATTR_COLOR), } glArrayBits_t; +typedef enum { + VA_NONE, + + VA_SPRITE, + VA_EFFECT, + VA_NULLMODEL, + VA_OCCLUDE, + VA_WATERWARP, + VA_MESH_SHADE, + VA_MESH_FLAT, + VA_2D, + VA_3D, + + VA_TOTAL +} glVertexArray_t; + typedef enum { TMU_TEXTURE, TMU_LIGHTMAP, @@ -548,34 +520,64 @@ typedef enum { } glTmu_t; typedef struct { - GLuint client_tmu; - GLuint server_tmu; - GLuint texnums[MAX_TMUS]; - GLbitfield state_bits; - GLbitfield array_bits; - GLuint currentbuffer[2]; - glVertexArray_t currentva; - const GLfloat *currentmatrix; - struct { - GLfloat view[16]; - GLfloat proj[16]; - GLfloat time; - GLfloat modulate; - GLfloat add; - GLfloat intensity; - GLfloat intensity2; - GLfloat pad; - GLfloat w_amp[2]; - GLfloat w_phase[2]; - GLfloat scroll[2]; - } u_block; + GLfloat view[16]; + GLfloat proj[16]; + GLfloat time; + GLfloat modulate; + GLfloat add; + GLfloat intensity; + GLfloat intensity2; + GLfloat pad; + GLfloat w_amp[2]; + GLfloat w_phase[2]; + GLfloat scroll[2]; +} glUniformBlock_t; + +typedef struct { + glTmu_t client_tmu; + glTmu_t server_tmu; + GLuint texnums[MAX_TMUS]; + glStateBits_t state_bits; + glArrayBits_t array_bits; + GLuint currentbuffer[2]; + glVertexArray_t currentva; + const GLfloat *currentmatrix; + glUniformBlock_t u_block; } glState_t; extern glState_t gls; +typedef struct { + uint8_t size; + bool type; + uint8_t stride; + uint8_t offset; +} glVaDesc_t; + +typedef struct { + const char *name; + + void (*init)(void); + void (*shutdown)(void); + void (*clear_state)(void); + void (*setup_2d)(void); + void (*setup_3d)(void); + + void (*load_proj_matrix)(const GLfloat *matrix); + void (*load_view_matrix)(const GLfloat *matrix); + + void (*state_bits)(glStateBits_t bits); + void (*array_bits)(glArrayBits_t bits); + + void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); + void (*tex_coord_pointer)(const GLfloat *ptr); + + void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); +} glbackend_t; + extern const glbackend_t *gl_backend; -static inline void GL_ActiveTexture(GLuint tmu) +static inline void GL_ActiveTexture(glTmu_t tmu) { if (gls.server_tmu != tmu) { qglActiveTexture(GL_TEXTURE0 + tmu); @@ -583,7 +585,7 @@ static inline void GL_ActiveTexture(GLuint tmu) } } -static inline void GL_ClientActiveTexture(GLuint tmu) +static inline void GL_ClientActiveTexture(glTmu_t tmu) { if (gls.client_tmu != tmu) { qglClientActiveTexture(GL_TEXTURE0 + tmu); @@ -591,7 +593,7 @@ static inline void GL_ClientActiveTexture(GLuint tmu) } } -static inline void GL_StateBits(GLbitfield bits) +static inline void GL_StateBits(glStateBits_t bits) { if (gls.state_bits != bits) { gl_backend->state_bits(bits); @@ -599,7 +601,7 @@ static inline void GL_StateBits(GLbitfield bits) } } -static inline void GL_ArrayBits(GLbitfield bits) +static inline void GL_ArrayBits(glArrayBits_t bits) { if (gls.array_bits != bits) { gl_backend->array_bits(bits); @@ -658,10 +660,10 @@ typedef enum { SHOWTRIS_FX = BIT(3), } showtris_t; -void GL_ForceTexture(GLuint tmu, GLuint texnum); -void GL_BindTexture(GLuint tmu, GLuint texnum); -void GL_CommonStateBits(GLbitfield bits); -void GL_ScrollSpeed(vec2_t scroll, GLbitfield bits); +void GL_ForceTexture(glTmu_t tmu, GLuint texnum); +void GL_BindTexture(glTmu_t tmu, GLuint texnum); +void GL_CommonStateBits(glStateBits_t bits); +void GL_ScrollSpeed(vec2_t scroll, glStateBits_t bits); void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar); void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x); @@ -671,9 +673,6 @@ void GL_ClearState(void); void GL_InitState(void); void GL_ShutdownState(void); -extern const glbackend_t backend_legacy; -extern const glbackend_t backend_shader; - /* * gl_draw.c * @@ -734,7 +733,7 @@ typedef struct { GLuint texnum[MAX_TMUS]; int numverts; int numindices; - int flags; + glStateBits_t flags; } tesselator_t; extern tesselator_t tess; diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index d31d07482..63fdfd3c0 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -19,9 +19,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gl.h" #include "arbfp.h" -static void legacy_state_bits(GLbitfield bits) +static void legacy_state_bits(glStateBits_t bits) { - GLbitfield diff = bits ^ gls.state_bits; + glStateBits_t diff = bits ^ gls.state_bits; if (diff & GLS_COMMON_MASK) GL_CommonStateBits(bits); @@ -81,9 +81,9 @@ static void legacy_state_bits(GLbitfield bits) } } -static void legacy_array_bits(GLbitfield bits) +static void legacy_array_bits(glArrayBits_t bits) { - GLbitfield diff = bits ^ gls.array_bits; + glArrayBits_t diff = bits ^ gls.array_bits; if (diff & GLA_VERTEX) { if (bits & GLA_VERTEX) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index e7b77e08b..6a829b0fe 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -57,7 +57,7 @@ static void write_block(sizebuf_t *buf) GLSF("};\n"); } -static void write_vertex_shader(sizebuf_t *buf, GLbitfield bits) +static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) { write_header(buf); write_block(buf); @@ -85,7 +85,7 @@ static void write_vertex_shader(sizebuf_t *buf, GLbitfield bits) GLSF("}\n"); } -static void write_fragment_shader(sizebuf_t *buf, GLbitfield bits) +static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) { write_header(buf); @@ -183,7 +183,7 @@ static GLuint create_shader(GLenum type, const sizebuf_t *buf) return shader; } -static GLuint create_and_use_program(GLbitfield bits) +static GLuint create_and_use_program(glStateBits_t bits) { char buffer[MAX_SHADER_CHARS]; sizebuf_t sb; @@ -264,9 +264,9 @@ static GLuint create_and_use_program(GLbitfield bits) return program; } -static void shader_state_bits(GLbitfield bits) +static void shader_state_bits(glStateBits_t bits) { - GLbitfield diff = bits ^ gls.state_bits; + glStateBits_t diff = bits ^ gls.state_bits; if (diff & GLS_COMMON_MASK) GL_CommonStateBits(bits); @@ -286,9 +286,9 @@ static void shader_state_bits(GLbitfield bits) } } -static void shader_array_bits(GLbitfield bits) +static void shader_array_bits(glArrayBits_t bits) { - GLbitfield diff = bits ^ gls.array_bits; + glArrayBits_t diff = bits ^ gls.array_bits; for (int i = 0; i < VERT_ATTR_COUNT; i++) { if (!(diff & BIT(i))) diff --git a/src/refresh/state.c b/src/refresh/state.c index c2b712a21..64fd73600 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -23,7 +23,7 @@ glState_t gls; const glbackend_t *gl_backend; // for uploading -void GL_ForceTexture(GLuint tmu, GLuint texnum) +void GL_ForceTexture(glTmu_t tmu, GLuint texnum) { GL_ActiveTexture(tmu); @@ -37,10 +37,10 @@ void GL_ForceTexture(GLuint tmu, GLuint texnum) } // for drawing -void GL_BindTexture(GLuint tmu, GLuint texnum) +void GL_BindTexture(glTmu_t tmu, GLuint texnum) { #if USE_DEBUG - if (gl_nobind->integer && !tmu) + if (gl_nobind->integer && tmu == TMU_TEXTURE) texnum = TEXNUM_DEFAULT; #endif @@ -58,9 +58,9 @@ void GL_BindTexture(GLuint tmu, GLuint texnum) c.texSwitches++; } -void GL_CommonStateBits(GLbitfield bits) +void GL_CommonStateBits(glStateBits_t bits) { - GLbitfield diff = bits ^ gls.state_bits; + glStateBits_t diff = bits ^ gls.state_bits; if (diff & GLS_BLEND_MASK) { if (bits & GLS_BLEND_MASK) { @@ -98,7 +98,7 @@ void GL_CommonStateBits(GLbitfield bits) } } -void GL_ScrollSpeed(vec2_t scroll, GLbitfield bits) +void GL_ScrollSpeed(vec2_t scroll, glStateBits_t bits) { float speed = 1.6f; @@ -328,6 +328,9 @@ void GL_ClearState(void) GL_ShowErrors(__func__); } +extern const glbackend_t backend_legacy; +extern const glbackend_t backend_shader; + void GL_InitState(void) { gl_static.use_shaders = gl_shaders->integer > 0; From 1af3bed176efe56bde7e8582757dd711ba18c5c6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 26 Aug 2024 17:46:32 +0300 Subject: [PATCH 624/974] Assert GL_BindBuffer() target is valid. --- src/refresh/gl.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 26aa6dbac..2c934554a 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -626,6 +626,8 @@ static inline void GL_LoadMatrix(const GLfloat *matrix) static inline void GL_BindBuffer(GLenum target, GLuint buffer) { const int i = target == GL_ELEMENT_ARRAY_BUFFER; + Q_assert(i || target == GL_ARRAY_BUFFER); + if (gls.currentbuffer[i] != buffer) { qglBindBuffer(target, buffer); gls.currentbuffer[i] = buffer; From e11d5e9459cb7b9dee8ffd401d55361f12d39ae7 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 26 Aug 2024 20:19:31 +0300 Subject: [PATCH 625/974] Count texture switches with multi bind too. --- src/refresh/tess.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 7222d88a1..afd14daa8 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -634,6 +634,7 @@ void GL_Flush3D(void) if (gls.texnums[i] != tess.texnum[i]) { gls.texnums[i] = tess.texnum[i]; count = i + 1; + c.texSwitches++; } } if (count) From 5feb14731570da33c01f1822d5f020fb4eb9dfee Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 00:28:17 +0300 Subject: [PATCH 626/974] =?UTF-8?q?Allow=20toggling=20=E2=80=98gl=5Fshader?= =?UTF-8?q?s=E2=80=99=20without=20video=20restart.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also make it always on by default. If OpenGL doesn't support shaders a warning is acceptable. --- doc/client.asciidoc | 3 +-- src/refresh/legacy.c | 23 +++++++++++++++-------- src/refresh/main.c | 6 +++++- src/refresh/shader.c | 9 ++++++++- src/refresh/state.c | 8 ++++++-- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 5764c4436..cff0cc813 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -805,8 +805,7 @@ gl_dlight_falloff:: gl_shaders:: Enables GLSL rendering backend. This requires at least OpenGL 3.0 and changes how ‘gl_modulate’, ‘gl_brightness’ and ‘intensity’ parameters work - to prevent ‘washed out’ colors. Default value is 1 (enabled) on systems - that support shaders, 0 otherwise. + to prevent ‘washed out’ colors. Default value is 1 (enabled). gl_colorbits:: Specifies desired size of color buffer, in bits, requested from OpenGL diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index 63fdfd3c0..ef8855928 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -168,12 +168,8 @@ static void legacy_load_proj_matrix(const GLfloat *matrix) qglLoadMatrixf(matrix); } -static void legacy_clear_state(void) +static void legacy_disable_state(void) { - qglDisable(GL_ALPHA_TEST); - qglAlphaFunc(GL_GREATER, 0.666f); - qglShadeModel(GL_FLAT); - if (qglActiveTexture && qglClientActiveTexture) { qglActiveTexture(GL_TEXTURE1); qglBindTexture(GL_TEXTURE_2D, 0); @@ -192,9 +188,6 @@ static void legacy_clear_state(void) qglDisableClientState(GL_TEXTURE_COORD_ARRAY); } - qglMatrixMode(GL_TEXTURE); - qglLoadIdentity(); - qglDisableClientState(GL_VERTEX_ARRAY); qglDisableClientState(GL_COLOR_ARRAY); @@ -204,6 +197,18 @@ static void legacy_clear_state(void) } } +static void legacy_clear_state(void) +{ + qglDisable(GL_ALPHA_TEST); + qglAlphaFunc(GL_GREATER, 0.666f); + qglShadeModel(GL_FLAT); + + legacy_disable_state(); + + qglMatrixMode(GL_TEXTURE); + qglLoadIdentity(); +} + static void legacy_init(void) { GLuint prog = 0; @@ -230,6 +235,8 @@ static void legacy_init(void) static void legacy_shutdown(void) { + legacy_disable_state(); + if (gl_static.programs[0]) { qglDeleteProgramsARB(1, gl_static.programs); gl_static.programs[0] = 0; diff --git a/src/refresh/main.c b/src/refresh/main.c index b12e53481..15d3fd521 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -884,7 +884,7 @@ static void GL_Register(void) gl_glowmap_intensity = Cvar_Get("gl_glowmap_intensity", "0.75", 0); gl_flarespeed = Cvar_Get("gl_flarespeed", "8", 0); gl_fontshadow = Cvar_Get("gl_fontshadow", "0", 0); - gl_shaders = Cvar_Get("gl_shaders", (gl_config.caps & QGL_CAP_SHADER) ? "1" : "0", CVAR_REFRESH); + gl_shaders = Cvar_Get("gl_shaders", "1", CVAR_FILES); #if USE_MD5 gl_md5_load = Cvar_Get("gl_md5_load", "1", CVAR_FILES); gl_md5_use = Cvar_Get("gl_md5_use", "1", 0); @@ -1022,6 +1022,10 @@ static void GL_PostInit(void) { r_registration_sequence = 1; + if (gl_shaders->modified) { + GL_ShutdownState(); + GL_InitState(); + } GL_ClearState(); GL_InitImages(); GL_InitQueries(); diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 6a829b0fe..42ca40a15 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -374,7 +374,7 @@ static void shader_setup_3d(void) gls.u_block.w_phase[1] = 4; } -static void shader_clear_state(void) +static void shader_disable_state(void) { qglActiveTexture(GL_TEXTURE2); qglBindTexture(GL_TEXTURE_2D, 0); @@ -387,6 +387,11 @@ static void shader_clear_state(void) for (int i = 0; i < VERT_ATTR_COUNT; i++) qglDisableVertexAttribArray(i); +} + +static void shader_clear_state(void) +{ + shader_disable_state(); if (gl_static.programs[0]) qglUseProgram(gl_static.programs[0]); @@ -404,6 +409,8 @@ static void shader_init(void) static void shader_shutdown(void) { + shader_disable_state(); + qglUseProgram(0); for (int i = 0; i < MAX_PROGRAMS; i++) { if (gl_static.programs[i]) { diff --git a/src/refresh/state.c b/src/refresh/state.c index 64fd73600..2292d5f71 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -349,6 +349,8 @@ void GL_InitState(void) } } + gl_shaders->modified = false; + gl_backend = gl_static.use_shaders ? &backend_shader : &backend_legacy; gl_backend->init(); @@ -357,6 +359,8 @@ void GL_InitState(void) void GL_ShutdownState(void) { - gl_backend->shutdown(); - gl_backend = NULL; + if (gl_backend) { + gl_backend->shutdown(); + gl_backend = NULL; + } } From 9e6cb09e0a2dd729abc0e8a20443d85513640097 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 01:47:26 +0300 Subject: [PATCH 627/974] Interpolate shadedot rather than normal. --- src/refresh/mesh.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 6be738ff0..f8498e8dd 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -206,7 +206,9 @@ static void tess_lerped_shade(const maliasmesh_t *mesh) vec3_t normal; while (count--) { - vec_t d = shadedot(get_lerped_normal(normal, src_oldvert, src_newvert)); + vec_t oldd = shadedot(get_static_normal(normal, src_oldvert)); + vec_t newd = shadedot(get_static_normal(normal, src_newvert)); + vec_t d = oldd * backlerp + newd * frontlerp; dst_vert[0] = src_oldvert->pos[0] * oldscale[0] + From 7786e41c9541db3d3d03ad29dcb4221dae67db1f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 10:58:16 +0300 Subject: [PATCH 628/974] Use macros to parse BSP vectors. --- src/common/bsp.c | 50 +++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 3e234c5e9..1ea1ed9ff 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -65,6 +65,14 @@ static cvar_t *map_visibility_patch; #define BSP_ExtLong() (bsp->extended ? BSP_Long() : BSP_Short()) #define BSP_ExtNull (bsp->extended ? (uint32_t)-1 : (uint16_t)-1) +#define BSP_VectorAdd(v, add) \ + ((v)[0] = BSP_Float() add, (v)[1] = BSP_Float() add, (v)[2] = BSP_Float() add) + +#define BSP_ExtVector(v) \ + ((v)[0] = BSP_ExtFloat(), (v)[1] = BSP_ExtFloat(), (v)[2] = BSP_ExtFloat()) + +#define BSP_Vector(v) BSP_VectorAdd(v,) + LOAD(Visibility) { if (!count) @@ -106,9 +114,7 @@ LOAD(Texinfo) for (int i = 0; i < count; i++, out++) { #if USE_REF for (int j = 0; j < 2; j++) { - out->axis[j][0] = BSP_Float(); - out->axis[j][1] = BSP_Float(); - out->axis[j][2] = BSP_Float(); + BSP_Vector(out->axis[j]); out->offset[j] = BSP_Float(); } #else @@ -160,9 +166,7 @@ LOAD(Planes) bsp->planes = out = ALLOC(sizeof(*out) * count); for (int i = 0; i < count; i++, in += 4, out++) { - out->normal[0] = BSP_Float(); - out->normal[1] = BSP_Float(); - out->normal[2] = BSP_Float(); + BSP_Vector(out->normal); out->dist = BSP_Float(); SetPlaneType(out); SetPlaneSignbits(out); @@ -251,11 +255,8 @@ LOAD(Vertices) bsp->numvertices = count; bsp->vertices = out = ALLOC(sizeof(*out) * count); - for (int i = 0; i < count; i++, out++) { - out->point[0] = BSP_Float(); - out->point[1] = BSP_Float(); - out->point[2] = BSP_Float(); - } + for (int i = 0; i < count; i++, out++) + BSP_Vector(out->point); return Q_ERR_SUCCESS; } @@ -392,11 +393,8 @@ LOAD(Leafs) out->area = area; #if USE_REF - for (int j = 0; j < 3; j++) - out->mins[j] = BSP_ExtFloat(); - for (int j = 0; j < 3; j++) - out->maxs[j] = BSP_ExtFloat(); - + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); uint32_t firstleafface = BSP_ExtLong(); uint32_t numleaffaces = BSP_ExtLong(); ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); @@ -448,11 +446,8 @@ LOAD(Nodes) } #if USE_REF - for (int j = 0; j < 3; j++) - out->mins[j] = BSP_ExtFloat(); - for (int j = 0; j < 3; j++) - out->maxs[j] = BSP_ExtFloat(); - + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); uint32_t firstface = BSP_ExtLong(); uint32_t numfaces = BSP_ExtLong(); ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); @@ -481,12 +476,9 @@ LOAD(SubModels) for (int i = 0; i < count; i++, out++) { // spread the mins / maxs by a pixel - for (int j = 0; j < 3; j++) - out->mins[j] = BSP_Float() - 1; - for (int j = 0; j < 3; j++) - out->maxs[j] = BSP_Float() + 1; - for (int j = 0; j < 3; j++) - out->origin[j] = BSP_Float(); + BSP_VectorAdd(out->mins, -1); + BSP_VectorAdd(out->maxs, +1); + BSP_Vector(out->origin); uint32_t headnode = BSP_Long(); if (headnode & BIT(31)) { @@ -929,9 +921,7 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) out->lightmap = NULL; for (int j = 0; j < 2; j++) { - out->lm_axis[j][0] = BSP_Float(); - out->lm_axis[j][1] = BSP_Float(); - out->lm_axis[j][2] = BSP_Float(); + BSP_Vector(out->lm_axis[j]); out->lm_offset[j] = BSP_Float(); } } From 0362b2a44876f6fcb7b6ad7db324c8968ca2dd6e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Aug 2024 11:31:16 -0400 Subject: [PATCH 629/974] Fix extra footstep sound being added --- .gitignore | 4 ++++ src/client/entities.c | 5 ++--- src/client/tent.c | 13 ++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index ba9c5317a..b58d1222e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ /.vs/* /debug/* /.vscode +build.sh +/build_win +clang-build.sh +/bots \ No newline at end of file diff --git a/src/client/entities.c b/src/client/entities.c index 2bac9ca9c..b0a1d6a4a 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -223,9 +223,8 @@ static void parse_entity_event(int number) CL_TeleportParticles(cent->current.origin); break; case EV_FOOTSTEP: - if (cl_footsteps->integer) { + if (cl_footsteps->integer) CL_PlayFootstepSfx(-1, number, 1.0f, ATTN_NORM); - } break; case EV_OTHER_FOOTSTEP: if (cl.csr.extended && cl_footsteps->integer) @@ -236,7 +235,7 @@ static void parse_entity_event(int number) CL_PlayFootstepSfx(FOOTSTEP_ID_LADDER, number, 0.5f, ATTN_IDLE); break; case EV_FALLSHORT: - if (strcmp(cl_enhanced_footsteps->string, "0") == 0) { + if (cl_enhanced_footsteps->value) { S_StartSound(NULL, number, CHAN_AUTO, cl_sfx_landing[0], 1, ATTN_NORM, 0); } else { S_StartSound(NULL, number, CHAN_BODY, cl_sfx_landing[Q_rand() % 8], 1, ATTN_NORM, 0); diff --git a/src/client/tent.c b/src/client/tent.c index 96c745a82..98d33c2f3 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -161,8 +161,9 @@ void CL_PlayFootstepSfx(int step_id, int entnum, float volume, float attenuation if (!sfx->num_sfx) return; // no footsteps, not even fallbacks + // bAron's additional footsteps // check if cl_enhanced_footsteps->string is "0" and sfx->num_sfx has more than four items - if (strcmp(cl_enhanced_footsteps->string, "0") == 0 && sfx->num_sfx > 4) { + if (!cl_enhanced_footsteps->value && sfx->num_sfx > 4) { // pick a random footstep sound from the first four items, important for compatibility with players using the old four stepsounds sfx_num = Q_rand_uniform(4); } else { @@ -171,8 +172,14 @@ void CL_PlayFootstepSfx(int step_id, int entnum, float volume, float attenuation } // avoid playing the same one twice in a row footstep_sfx = sfx->sfx[sfx_num]; - if (footstep_sfx == cl_last_footstep) - footstep_sfx = sfx->sfx[(sfx_num + 1) % sfx->num_sfx]; + if (footstep_sfx == cl_last_footstep) { + if (sfx_num != 0) + footstep_sfx = sfx->sfx[(sfx_num - 1) % sfx->num_sfx]; + else + footstep_sfx = sfx->sfx[(sfx_num + 1) % sfx->num_sfx]; + } + + Com_Printf("Playing footstep sound %i\n", footstep_sfx); S_StartSound(NULL, entnum, CHAN_BODY, footstep_sfx, volume, attenuation, 0); cl_last_footstep = footstep_sfx; From 6dbe9e462e0adda672cfe0f9fac929f5b1052c3b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 19:41:18 +0300 Subject: [PATCH 630/974] Help compiler produce better code. --- src/common/bsp.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 1ea1ed9ff..9c6195291 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -49,7 +49,8 @@ static cvar_t *map_visibility_patch; Hunk_Alloc(&bsp->hunk, size) #define LOAD(func) \ - static int BSP_Load##func(bsp_t *bsp, const byte *in, size_t count) + static int BSP_Load##func(bsp_t *const bsp, const byte *in, \ + const size_t count, const bool extended) #define DEBUG(msg) \ Com_SetLastError(va("%s: %s", __func__, msg)) @@ -61,9 +62,9 @@ static cvar_t *map_visibility_patch; #define BSP_Long() (in += 4, RL32(in - 4)) #define BSP_Float() LongToFloat(BSP_Long()) -#define BSP_ExtFloat() (bsp->extended ? BSP_Float() : (int16_t)BSP_Short()) -#define BSP_ExtLong() (bsp->extended ? BSP_Long() : BSP_Short()) -#define BSP_ExtNull (bsp->extended ? (uint32_t)-1 : (uint16_t)-1) +#define BSP_ExtFloat() (extended ? BSP_Float() : (int16_t)BSP_Short()) +#define BSP_ExtLong() (extended ? BSP_Long() : BSP_Short()) +#define BSP_ExtNull (extended ? (uint32_t)-1 : (uint16_t)-1) #define BSP_VectorAdd(v, add) \ ((v)[0] = BSP_Float() add, (v)[1] = BSP_Float() add, (v)[2] = BSP_Float() add) @@ -572,7 +573,7 @@ typedef struct { } xlump_info_t; typedef struct { - int (*load)(bsp_t *, const byte *, size_t); + int (*load)(bsp_t *const, const byte *, const size_t, const bool); const char *name; uint8_t lump; uint8_t disksize[2]; @@ -1320,7 +1321,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) // load all lumps for (i = 0; i < q_countof(bsp_lumps); i++) { - ret = bsp_lumps[i].load(bsp, buf + lump_ofs[i], lump_count[i]); + ret = bsp_lumps[i].load(bsp, buf + lump_ofs[i], lump_count[i], extended); if (ret) { goto fail1; } From ca712e542e4a2b92e61034837ecd59ad71899ba8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 19:41:37 +0300 Subject: [PATCH 631/974] Small BSP loading code cleanup. --- src/common/bsp.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 9c6195291..f24d8d209 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -307,7 +307,7 @@ LOAD(Faces) bsp->numfaces = count; bsp->faces = out = ALLOC(sizeof(*out) * count); - for (int i = 0; i < count; i++, out++) { + for (int i = 0, j; i < count; i++, out++) { uint32_t planenum = BSP_ExtLong(); ENSURE(planenum < bsp->numplanes, "Bad planenum"); out->plane = bsp->planes + planenum; @@ -325,13 +325,12 @@ LOAD(Faces) ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); out->texinfo = bsp->texinfo + texinfo; - int j; - for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) { + for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) out->styles[j] = in[j]; - } - for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) { + + for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) out->styles[j] = 255; - } + in += MAX_LIGHTMAPS; uint32_t lightofs = BSP_Long(); @@ -1164,7 +1163,7 @@ static size_t BSP_ParseExtensionHeader(bsp_t *bsp, lump_t *out, const byte *buf, } size_t extrasize = 0; - xlump_t *l = (xlump_t *)(buf + pos); + const xlump_t *l = (const xlump_t *)(buf + pos); for (int i = 0; i < numlumps; i++, l++) { for (int j = 0; j < q_countof(bspx_lumps); j++) { const xlump_info_t *e = &bspx_lumps[j]; From 2e333ddf416d6b033f1795255c0018389d66332d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 10:58:18 +0300 Subject: [PATCH 632/974] Clean up BSP_PrintStats(). --- src/common/bsp.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index f24d8d209..4a466c01f 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -645,16 +645,15 @@ static void BSP_PrintStats(const bsp_t *bsp) { bool extended = bsp->extended; - for (int i = 0; i < q_countof(bsp_stats); i++) { - const bsp_stat_t *s = &bsp_stats[i]; - Com_Printf("%8d : %s\n", *(int *)((byte *)bsp + s->ofs), s->name); - } + for (int i = 0; i < q_countof(bsp_stats); i++) + Com_Printf("%8d : %s\n", *(int *)((byte *)bsp + bsp_stats[i].ofs), bsp_stats[i].name); + if (bsp->vis) Com_Printf("%8u : clusters\n", bsp->vis->numclusters); #if USE_REF - if (bsp->lightgrid.numleafs) { - const lightgrid_t *grid = &bsp->lightgrid; + const lightgrid_t *grid = &bsp->lightgrid; + if (grid->numleafs) { Com_Printf( "%8u : lightgrid styles\n" "%8u : lightgrid nodes\n" @@ -673,7 +672,7 @@ static void BSP_PrintStats(const bsp_t *bsp) #if USE_REF if (bsp->lm_decoupled) Com_Printf(" DECOUPLED_LM"); - if (bsp->lightgrid.numleafs) + if (grid->numleafs) Com_Printf(" LIGHTGRID_OCTREE"); #endif Com_Printf("\n"); From ab7194827a5b1e35ef6135a6e750344f7f8e44cb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 10:58:18 +0300 Subject: [PATCH 633/974] Reduce underwater warp effect amplitude. --- src/refresh/shader.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 42ca40a15..efb64dcd0 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -354,8 +354,8 @@ static void shader_setup_2d(void) gls.u_block.intensity = 1.0f; gls.u_block.intensity2 = 1.0f; - gls.u_block.w_amp[0] = 0.00666f; - gls.u_block.w_amp[1] = 0.00666f; + gls.u_block.w_amp[0] = 0.0025f; + gls.u_block.w_amp[1] = 0.0025f; gls.u_block.w_phase[0] = M_PIf * 10; gls.u_block.w_phase[1] = M_PIf * 10; } From 6e24103124a3228154d87bb32ba8eb512c2d61cd Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Aug 2024 12:57:00 -0400 Subject: [PATCH 634/974] Landing sound fix --- meson.build | 2 +- src/client/entities.c | 2 +- src/client/tent.c | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 9c855ac2f..a1232d792 100644 --- a/meson.build +++ b/meson.build @@ -4,7 +4,7 @@ project('q2pro', 'c', meson_version: '>= 0.59.0', default_options: [ 'c_std=c11', - 'buildtype=debug', + 'buildtype=release', ], ) diff --git a/src/client/entities.c b/src/client/entities.c index b0a1d6a4a..66236011b 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -235,7 +235,7 @@ static void parse_entity_event(int number) CL_PlayFootstepSfx(FOOTSTEP_ID_LADDER, number, 0.5f, ATTN_IDLE); break; case EV_FALLSHORT: - if (cl_enhanced_footsteps->value) { + if (!cl_enhanced_footsteps->value) { S_StartSound(NULL, number, CHAN_AUTO, cl_sfx_landing[0], 1, ATTN_NORM, 0); } else { S_StartSound(NULL, number, CHAN_BODY, cl_sfx_landing[Q_rand() % 8], 1, ATTN_NORM, 0); diff --git a/src/client/tent.c b/src/client/tent.c index 98d33c2f3..90dca1b2d 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -179,8 +179,6 @@ void CL_PlayFootstepSfx(int step_id, int entnum, float volume, float attenuation footstep_sfx = sfx->sfx[(sfx_num + 1) % sfx->num_sfx]; } - Com_Printf("Playing footstep sound %i\n", footstep_sfx); - S_StartSound(NULL, entnum, CHAN_BODY, footstep_sfx, volume, attenuation, 0); cl_last_footstep = footstep_sfx; } From 3b1ade385d8ccdde5c5face24bee713c2cb4e627 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Aug 2024 13:50:57 -0400 Subject: [PATCH 635/974] Merging cl_enhanced_footsteps with cl_footsteps behavior --- src/client/entities.c | 2 +- src/client/tent.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/entities.c b/src/client/entities.c index 66236011b..7405cae28 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -235,7 +235,7 @@ static void parse_entity_event(int number) CL_PlayFootstepSfx(FOOTSTEP_ID_LADDER, number, 0.5f, ATTN_IDLE); break; case EV_FALLSHORT: - if (!cl_enhanced_footsteps->value) { + if (cl_footsteps->integer >= 2) { S_StartSound(NULL, number, CHAN_AUTO, cl_sfx_landing[0], 1, ATTN_NORM, 0); } else { S_StartSound(NULL, number, CHAN_BODY, cl_sfx_landing[Q_rand() % 8], 1, ATTN_NORM, 0); diff --git a/src/client/tent.c b/src/client/tent.c index 90dca1b2d..1dc3361b6 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -162,8 +162,8 @@ void CL_PlayFootstepSfx(int step_id, int entnum, float volume, float attenuation return; // no footsteps, not even fallbacks // bAron's additional footsteps - // check if cl_enhanced_footsteps->string is "0" and sfx->num_sfx has more than four items - if (!cl_enhanced_footsteps->value && sfx->num_sfx > 4) { + // check if cl_footsteps is >= 2 and sfx->num_sfx has more than four items + if (cl_footsteps->integer >= 2 && sfx->num_sfx > 4) { // pick a random footstep sound from the first four items, important for compatibility with players using the old four stepsounds sfx_num = Q_rand_uniform(4); } else { From d1bcfad9029af1c74dd36a76e2396e4c63afd14f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Aug 2024 17:19:52 -0400 Subject: [PATCH 636/974] Exposed net_port to gamelib, added msgflags --- src/action/g_local.h | 14 +++++++++++--- src/action/g_main.c | 28 ++++++++++++++++------------ src/action/g_save.c | 5 +++++ src/action/p_client.c | 8 ++++++++ src/action/tng_net.c | 8 +++++++- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 7cdff6213..73a4a9bc1 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -427,9 +427,12 @@ ammo_t; //tng_net.c typedef enum { - NOTIFY_NONE, - SERVER_WARMING_UP, - NOTIFY_MAX + NOTIFY_NONE = BIT(0), + SERVER_WARMING_UP = BIT(1), + DEATH_MSG = BIT(2), + CHAT_MSG = BIT(3), + SERVER_MSG = BIT(4), + NOTIFY_MAX = BIT(5) } Discord_Notifications; //deadflag @@ -1090,6 +1093,7 @@ extern edict_t *g_edicts; #define crandom() (2.0 * (random() - 0.5)) #define DMFLAGS(x) (((int)dmflags->value & x) != 0) +#define MSGFLAGS(x) (((int)msgflags->value & x) != 0) #ifndef NO_BOTS #define AQ2WTEAMSIZE 46 @@ -1121,6 +1125,7 @@ extern cvar_t *hud_noscore; extern cvar_t *use_newscore; extern cvar_t *scoreboard; extern cvar_t *actionversion; +extern cvar_t *net_port; #ifndef NO_BOTS extern cvar_t *ltk_jumpy; #endif @@ -1334,6 +1339,9 @@ extern cvar_t *server_ip; extern cvar_t *server_port; extern cvar_t *sv_last_announce_interval; extern cvar_t *sv_last_announce_time; +extern cvar_t *msgflags; +//end cUrl integration + extern cvar_t *training_mode; // Sets training mode vars extern cvar_t *g_highscores_dir; // Sets the highscores directory extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA diff --git a/src/action/g_main.c b/src/action/g_main.c index 9f3f168a2..97039e8cc 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -305,6 +305,7 @@ cvar_t *hud_noscore; cvar_t *use_newscore; cvar_t *scoreboard; cvar_t *actionversion; +cvar_t *net_port; cvar_t *needpass; cvar_t *use_voice; cvar_t *ppl_idletime; @@ -555,18 +556,21 @@ cvar_t *sv_killgib; // Gibs on 'kill' command // 2024 cvar_t *warmup_unready; // Toggles warmup if captains unready // cURL integration / tng_net.c -cvar_t *sv_curl_enable; -cvar_t *sv_discord_announce_enable; -cvar_t *sv_curl_stat_enable; -cvar_t *sv_aws_access_key; -cvar_t *sv_aws_secret_key; -cvar_t *sv_curl_discord_chat_url; -cvar_t *sv_curl_discord_server_url; -cvar_t *server_ip; -cvar_t *server_port; -cvar_t *sv_last_announce_interval; -cvar_t *sv_last_announce_time; -cvar_t *server_announce_url; +cvar_t *sv_curl_enable; // Enable cURL integration +cvar_t *sv_discord_announce_enable; // Enable Discord announcements +cvar_t *sv_curl_stat_enable; // Enable cURL stat logging +cvar_t *sv_aws_access_key; // AWS Access Key (stat logs) +cvar_t *sv_aws_secret_key; // AWS Secret Key (stat logs) +cvar_t *sv_curl_discord_chat_url; // Discord chat webhook URL (for game chat) +cvar_t *sv_curl_discord_server_url; // Discord server URL +cvar_t *server_ip; // Server IP +cvar_t *server_port; // Server port +cvar_t *sv_last_announce_interval; // Interval between announcements +cvar_t *sv_last_announce_time; // Last announcement time +cvar_t *server_announce_url; // Server announce URL +cvar_t *msgflags; // Message flags (like dmflags) +// end cURL integration cvars + cvar_t *training_mode; // Sets training mode vars cvar_t *g_highscores_dir; // Sets the highscores directory cvar_t *lca_grenade; // Allows grenade pin pulling during LCA diff --git a/src/action/g_save.c b/src/action/g_save.c index 7819b960b..25a6de4fc 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -354,6 +354,8 @@ void InitGame( void ) actionversion = gi.cvar( "actionversion", "TNG " VERSION, CVAR_SERVERINFO | CVAR_NOSET ); gi.cvar_set( "actionversion", "TNG " VERSION ); + net_port = gi.cvar( "net_port", "27910", CVAR_NOSET ); + maxclients = gi.cvar( "maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH ); maxentities = gi.cvar( "maxentities", "1024", CVAR_LATCH ); @@ -663,6 +665,9 @@ void InitGame( void ) sv_last_announce_time = gi.cvar("sv_last_announce_time", "0", 0); sv_last_announce_interval = gi.cvar("sv_last_announce_interval", "1800", 0); server_announce_url = gi.cvar("server_announce_url", "disabled", 0); + msgflags = gi.cvar("msgflags", "0", 0); + + training_mode = gi.cvar("training_mode", "0", CVAR_LATCH); if (training_mode->value){ gi.cvar_forceset("item_respawnmode", "1"); diff --git a/src/action/p_client.c b/src/action/p_client.c index 59f670b91..56b62ceab 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1174,6 +1174,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s %s %s\n", self->client->pers.netname, special_message, self->client->attacker->client->pers.netname); PrintDeathMessage(death_msg, self); + //Using discord webhook for death messaging + lc_discord_webhook(death_msg); IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); @@ -1205,6 +1207,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) else { sprintf( death_msg, "%s %s\n", self->client->pers.netname, message ); + //Using discord webhook for death messaging + lc_discord_webhook(death_msg); PrintDeathMessage(death_msg, self ); IRC_printf( IRC_T_DEATH, death_msg ); @@ -1557,6 +1561,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s%s %s%s\n", self->client->pers.netname, message, attacker->client->pers.netname, message2); PrintDeathMessage(death_msg, self); + //Using discord webhook for death messaging + lc_discord_webhook(death_msg); IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); @@ -1601,6 +1607,8 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s died\n", self->client->pers.netname); PrintDeathMessage(death_msg, self); + //Using discord webhook for death messaging + lc_discord_webhook(death_msg); IRC_printf(IRC_T_DEATH, death_msg); #if USE_AQTION diff --git a/src/action/tng_net.c b/src/action/tng_net.c index a3d8d9d21..b7ffa6ab2 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -155,11 +155,17 @@ void announce_server_populating(void) Call this with a string containing the message you want to send to the webhook. Limited to 1024 chars. */ -void lc_discord_webhook(char* message) +void lc_discord_webhook(char* message, Discord_Notifications msgtype) { request_t *request; char json_payload[1024]; + // Check if the msgtype is within the allowed message flags + // Look at the Discord_Notifications enum in g_local.h for the flags + if (!(msgtype & (int)msgflags->value)) { + return; + } + // Don't run this if curl is disabled or the webhook URL is set to "disabled" if (!sv_curl_enable->value || strcmp(sv_curl_discord_chat_url->string, "disabled") == 0) return; From 212d3f5af5231c0fdc8718f02dd76f8659d5892b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 27 Aug 2024 17:22:34 -0400 Subject: [PATCH 637/974] Added comment --- src/action/g_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_main.c b/src/action/g_main.c index 97039e8cc..9af95c589 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -561,7 +561,7 @@ cvar_t *sv_discord_announce_enable; // Enable Discord announcements cvar_t *sv_curl_stat_enable; // Enable cURL stat logging cvar_t *sv_aws_access_key; // AWS Access Key (stat logs) cvar_t *sv_aws_secret_key; // AWS Secret Key (stat logs) -cvar_t *sv_curl_discord_chat_url; // Discord chat webhook URL (for game chat) +cvar_t *sv_curl_discord_chat_url; // Discord message URL (sending messages to Discord via webhook) cvar_t *sv_curl_discord_server_url; // Discord server URL cvar_t *server_ip; // Server IP cvar_t *server_port; // Server port From d0d447173c7ed2a77d6157464fee676cf96dd40c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 28 Aug 2024 14:59:55 -0400 Subject: [PATCH 638/974] Added optional match reporting json payload --- src/action/a_team.c | 37 ++++---- src/action/a_team.h | 1 + src/action/g_cmds.c | 3 + src/action/g_local.h | 19 ++-- src/action/p_client.c | 8 +- src/action/tng_net.c | 201 ++++++++++++++++++++++++++++++++++++++++- src/action/tng_stats.c | 24 ++--- 7 files changed, 249 insertions(+), 44 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 6d6f6d1c7..67b1ca277 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -3180,25 +3180,25 @@ void A_Scoreboard (edict_t * ent) } -static int G_PlayerCmp( const void *p1, const void *p2 ) +int G_PlayerCmp(const void *p1, const void *p2) { - gclient_t *a = *(gclient_t * const *)p1; - gclient_t *b = *(gclient_t * const *)p2; - - if (a->resp.score != b->resp.score) - return b->resp.score - a->resp.score; - - if (a->resp.deaths < b->resp.deaths) - return -1; - if (a->resp.deaths > b->resp.deaths) - return 1; - - if (a->resp.damage_dealt > b->resp.damage_dealt) - return -1; - if (a->resp.damage_dealt < b->resp.damage_dealt) - return 1; - - return 0; + gclient_t *a = *(gclient_t * const *)p1; + gclient_t *b = *(gclient_t * const *)p2; + + if (a->resp.score != b->resp.score) + return b->resp.score - a->resp.score; + + if (a->resp.deaths < b->resp.deaths) + return -1; + if (a->resp.deaths > b->resp.deaths) + return 1; + + if (a->resp.damage_dealt > b->resp.damage_dealt) + return -1; + if (a->resp.damage_dealt < b->resp.damage_dealt) + return 1; + + return (byte *)a - (byte *)b; } int G_SortedClients( gclient_t **sortedList ) @@ -3898,6 +3898,7 @@ void TallyEndOfLevelTeamScores (void) LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match logs } + lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG); #endif // Stats: Reset roundNum game.roundNum = 0; diff --git a/src/action/a_team.h b/src/action/a_team.h index 4cdce90af..dfdb87f72 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -111,6 +111,7 @@ void JoinTeamAutobalance (edict_t * ent); int TotalPlayersOnTeam(int teamNum); int TotalPlayersAliveOnTeam(int teamNum); void PrintAdNotification(edict_t* ent); +int G_PlayerCmp( const void *p1, const void *p2 ); typedef struct spawn_distances_s { diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 8448c0dd0..2e1ee41b7 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1466,6 +1466,9 @@ void Cmd_Say_f (edict_t * ent, qboolean team, qboolean arg0, qboolean partner_ms } } + // Send message to Discord -- this must come before the newline add or it screws up formatting in-game + lc_discord_webhook(text, CHAT_MSG); + Q_strncatz(text, "\n", sizeof(text)); if (FloodCheck(ent)) diff --git a/src/action/g_local.h b/src/action/g_local.h index 73a4a9bc1..7fa92211f 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -427,14 +427,19 @@ ammo_t; //tng_net.c typedef enum { - NOTIFY_NONE = BIT(0), - SERVER_WARMING_UP = BIT(1), - DEATH_MSG = BIT(2), - CHAT_MSG = BIT(3), - SERVER_MSG = BIT(4), - NOTIFY_MAX = BIT(5) + NOTIFY_NONE = BIT(0), // 1 + SERVER_WARMING_UP = BIT(1), // 2 + DEATH_MSG = BIT(2), // 4 + CHAT_MSG = BIT(3), // 8 + SERVER_MSG = BIT(4), // 16 + MATCH_END_MSG = BIT(5), // 32 + NOTIFY_MAX = BIT(6) // 64 } Discord_Notifications; +// Default messages +#define MM_MATCH_END_MSG "Match Results" + + //deadflag #define DEAD_NO 0 #define DEAD_DYING 1 @@ -2920,5 +2925,5 @@ void FireTimedMessages(void); void lc_shutdown_function(void); qboolean lc_init_function(void); void lc_once_per_gameframe(void); -void lc_discord_webhook(char* message); +void lc_discord_webhook(char* message, Discord_Notifications msgtype); void lc_start_request_function(request_t* request); \ No newline at end of file diff --git a/src/action/p_client.c b/src/action/p_client.c index 56b62ceab..19a481385 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1175,7 +1175,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) self->client->pers.netname, special_message, self->client->attacker->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - lc_discord_webhook(death_msg); + lc_discord_webhook(death_msg, DEATH_MSG); IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); @@ -1208,7 +1208,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) { sprintf( death_msg, "%s %s\n", self->client->pers.netname, message ); //Using discord webhook for death messaging - lc_discord_webhook(death_msg); + lc_discord_webhook(death_msg, DEATH_MSG);; PrintDeathMessage(death_msg, self ); IRC_printf( IRC_T_DEATH, death_msg ); @@ -1562,7 +1562,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) message, attacker->client->pers.netname, message2); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - lc_discord_webhook(death_msg); + lc_discord_webhook(death_msg, DEATH_MSG);; IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); @@ -1608,7 +1608,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s died\n", self->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - lc_discord_webhook(death_msg); + lc_discord_webhook(death_msg, DEATH_MSG);; IRC_printf(IRC_T_DEATH, death_msg); #if USE_AQTION diff --git a/src/action/tng_net.c b/src/action/tng_net.c index b7ffa6ab2..658d05946 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -151,6 +151,182 @@ void announce_server_populating(void) gi.cvar_forceset("sv_last_announce_time", va("%d", (int)time(NULL))); } +static char* DMConstructPlayerScoreString(void) { + gclient_t **ranks = (gclient_t **)malloc(game.maxclients * sizeof(gclient_t *)); + int total = G_CalcRanks(ranks); + + // Calculate the required buffer size + int buffer_size = 0; + for (int i = 0; i < total; i++) { + buffer_size += snprintf(NULL, 0, "%s: %d\n", ranks[i]->pers.netname, ranks[i]->resp.score); + } + + // Allocate the buffer + char *result = (char *)malloc(buffer_size + 1); + if (!result) { + return NULL; // Handle memory allocation failure + } + + // Construct the string + char *ptr = result; + for (int i = 0; i < total; i++) { + ptr += sprintf(ptr, "%s: %d\n", ranks[i]->pers.netname, ranks[i]->resp.score); + } + + return result; +} + +int FilterPlayersByTeam(gclient_t **filtered, int team) { + int total = 0; + for (int i = 0; i < game.maxclients; i++) { + if (game.clients[i].pers.connected == 1 && game.clients[i].resp.team == team) { + filtered[total] = &game.clients[i]; + total++; + } + } + return total; +} + +char* TeamConstructPlayerScoreString(int team) { + gclient_t **filtered = (gclient_t **)malloc(game.maxclients * sizeof(gclient_t *)); + if (!filtered) { + return NULL; // Handle memory allocation failure + } + + int total = FilterPlayersByTeam(filtered, team); + + // Sort the filtered players + qsort(filtered, total, sizeof(gclient_t *), G_PlayerCmp); + + // Calculate the required buffer size + int buffer_size = 0; + for (int i = 0; i < total; i++) { + buffer_size += snprintf(NULL, 0, "%s: %d\n", filtered[i]->pers.netname, filtered[i]->resp.score); + } + + // Allocate the buffer + char *result = (char *)malloc(buffer_size + 1); + if (!result) { + free(filtered); + return NULL; // Handle memory allocation failure + } + + // Construct the string + char *ptr = result; + for (int i = 0; i < total; i++) { + ptr += sprintf(ptr, "%s: %d\n", filtered[i]->pers.netname, filtered[i]->resp.score); + } + + free(filtered); + return result; +} + +static char *discord_MatchEndMsg(void) +{ + // Create the root object + json_t *root = json_object(); + + json_object_set_new(root, "content", json_string("")); + + // Create the "embeds" array + json_t *embeds = json_array(); + json_object_set_new(root, "embeds", embeds); + + // Create the embed object + json_t *embed = json_object(); + json_array_append_new(embeds, embed); + + // Add fields to the embed object + json_object_set_new(embed, "description", json_string("Match results")); + json_object_set_new(embed, "title", json_string(level.mapname)); + json_object_set_new(embed, "color", json_integer(16711680)); + + // Create the "thumbnail" object + json_t *thumbnail = json_object(); + json_object_set_new(embed, "thumbnail", thumbnail); + + char mapimgurl[512]; + snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); + json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + + // Create the "fields" array + json_t *fields = json_array(); + json_object_set_new(embed, "fields", fields); + + // Create field objects and add them to the "fields" array + if (teamplay->value) { + char *t1name = TeamName(TEAM1); + char *t2name = TeamName(TEAM2); + int t1score = teams[TEAM1].score; + int t2score = teams[TEAM2].score; + char field1content[64]; + snprintf(field1content, sizeof(field1content), "%s: %d", t1name, t1score); + char field2content[64]; + snprintf(field2content, sizeof(field2content), "%s: %d", t2name, t2score); + char *t1_player_scores = TeamConstructPlayerScoreString(TEAM1); + char *t2_player_scores = TeamConstructPlayerScoreString(TEAM2); + + // Add triple backticks for Discord formatting + char formatted_t1_player_scores[256]; + snprintf(formatted_t1_player_scores, sizeof(formatted_t1_player_scores), "```%s```", t1_player_scores); + char formatted_t2_player_scores[256]; + snprintf(formatted_t2_player_scores, sizeof(formatted_t2_player_scores), "```%s```", t2_player_scores); + + + if(teamCount == 2) { + json_t *field1 = json_object(); + json_object_set_new(field1, "name", json_string(field1content)); + json_object_set_new(field1, "value", json_string(formatted_t1_player_scores)); + json_object_set_new(field1, "inline", json_true()); + json_array_append_new(fields, field1); + + json_t *field2 = json_object(); + json_object_set_new(field2, "name", json_string(field2content)); + json_object_set_new(field2, "value", json_string(formatted_t2_player_scores)); + json_object_set_new(field2, "inline", json_true()); + json_array_append_new(fields, field2); + } else { + char *t3name = TeamName(TEAM3); + int t3score = teams[TEAM3].score; + char field3content[64]; + snprintf(field3content, sizeof(field3content), "%s: %d", t3name, t3score); + char *t3_player_scores = TeamConstructPlayerScoreString(TEAM3); + // Add triple backticks for Discord formatting + char formatted_t3_player_scores[256]; + snprintf(formatted_t3_player_scores, sizeof(formatted_t3_player_scores), "```%s```", t3_player_scores); + + json_t *field1 = json_object(); + json_object_set_new(field1, "name", json_string(field1content)); + json_object_set_new(field1, "value", json_string(formatted_t1_player_scores)); + json_object_set_new(field1, "inline", json_true()); + json_array_append_new(fields, field1); + + json_t *field2 = json_object(); + json_object_set_new(field2, "name", json_string(field2content)); + json_object_set_new(field2, "value", json_string(formatted_t2_player_scores)); + json_object_set_new(field2, "inline", json_true()); + json_array_append_new(fields, field2); + + json_t *field3 = json_object(); + json_object_set_new(field3, "name", json_string(field3content)); + json_object_set_new(field3, "value", json_string(formatted_t3_player_scores)); + json_object_set_new(field3, "inline", json_true()); + json_array_append_new(fields, field3); + } + } + + // Add "username" field to the root object + json_object_set_new(root, "username", json_string(hostname->string)); + + // Convert the JSON object to a string + char *json_payload = json_dumps(root, JSON_INDENT(4)); + + // Decrement the reference count of the root object to free memory + json_decref(root); + + return json_payload; +} + /* Call this with a string containing the message you want to send to the webhook. Limited to 1024 chars. @@ -158,11 +334,16 @@ Limited to 1024 chars. void lc_discord_webhook(char* message, Discord_Notifications msgtype) { request_t *request; - char json_payload[1024]; + char json_payload[2048]; + + // Debug prints + gi.dprintf("msgflags value: %d\n", (int)msgflags->value); + gi.dprintf("msgtype value: %d\n", msgtype); // Check if the msgtype is within the allowed message flags // Look at the Discord_Notifications enum in g_local.h for the flags if (!(msgtype & (int)msgflags->value)) { + gi.dprintf("Message type not allowed by msgflags\n"); return; } @@ -187,8 +368,22 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) *newline = '\0'; } - // Format the message as a JSON payload - snprintf(json_payload, sizeof(json_payload), "{\"content\":\"```%s```\"}", message); + // Format the message as a JSON payload based on msgtype + if (msgtype == CHAT_MSG || msgtype == DEATH_MSG) { + snprintf(json_payload, sizeof(json_payload), + "{\"content\":\"```%s```\"}", + message); + } else if (msgtype == MATCH_END_MSG){ + char *matchendmsg = discord_MatchEndMsg(); + if (matchendmsg) { + // Print the JSON payload + gi.dprintf("%s\n", matchendmsg); + snprintf(json_payload, sizeof(json_payload), "%s", matchendmsg); + + free(matchendmsg); + } + } + request->url = url; request->payload = strdup(json_payload); diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 49833865e..770006165 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -672,18 +672,18 @@ static void G_SaveScores(void) fclose(fp); } -static int G_PlayerCmp(const void *p1, const void *p2) -{ - gclient_t *a = *(gclient_t * const *)p1; - gclient_t *b = *(gclient_t * const *)p2; - - int r = b->resp.score - a->resp.score; - if (!r) - r = a->resp.deaths - b->resp.deaths; - if (!r) - r = (byte *)a - (byte *)b; - return r; -} +// static int G_PlayerCmp(const void *p1, const void *p2) +// { +// gclient_t *a = *(gclient_t * const *)p1; +// gclient_t *b = *(gclient_t * const *)p2; + +// int r = b->resp.score - a->resp.score; +// if (!r) +// r = a->resp.deaths - b->resp.deaths; +// if (!r) +// r = (byte *)a - (byte *)b; +// return r; +// } int G_CalcRanks(gclient_t **ranks) { From 4cb80ca8cb0753889bbde58a0a45726e31567a04 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 28 Aug 2024 16:06:40 -0400 Subject: [PATCH 639/974] Reformatted a bit to make it more readable --- src/action/a_match.c | 1 + src/action/a_team.c | 2 +- src/action/g_local.h | 5 +- src/action/p_hud.c | 2 + src/action/tng_net.c | 137 +++++++++++++++++++++++++------------------ 5 files changed, 87 insertions(+), 60 deletions(-) diff --git a/src/action/a_match.c b/src/action/a_match.c index bac84ae54..1bc32c01f 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -91,6 +91,7 @@ void SendScores(void) LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match stats } + lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG); #endif // Stats: Reset roundNum game.roundNum = 0; diff --git a/src/action/a_team.c b/src/action/a_team.c index 67b1ca277..641586701 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -3898,7 +3898,7 @@ void TallyEndOfLevelTeamScores (void) LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match logs } - lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG); + lc_discord_webhook(TP_MATCH_END_MSG, MATCH_END_MSG); #endif // Stats: Reset roundNum game.roundNum = 0; diff --git a/src/action/g_local.h b/src/action/g_local.h index 7fa92211f..811efef78 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -437,8 +437,9 @@ typedef enum { } Discord_Notifications; // Default messages -#define MM_MATCH_END_MSG "Match Results" - +#define MM_MATCH_END_MSG "Matchmode Results" +#define TP_MATCH_END_MSG "Teamplay Results" +#define DM_MATCH_END_MSG "Deathmatch Results" //deadflag #define DEAD_NO 0 diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 65c01a90a..29558f8ed 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -138,6 +138,8 @@ void BeginIntermission(edict_t *targ) LogMatch(); LogEndMatchStats(); // Generates end of match logs } + if(!teamplay->value) + lc_discord_webhook(DM_MATCH_END_MSG, MATCH_END_MSG); #endif // respawn any dead clients diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 658d05946..51cb3b722 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -236,10 +236,20 @@ static char *discord_MatchEndMsg(void) json_t *embed = json_object(); json_array_append_new(embeds, embed); + // Adjust description and color based on mode + if (teamplay->value) { // Green + json_object_set_new(embed, "description", json_string(TP_MATCH_END_MSG)); + json_object_set_new(embed, "color", json_integer(65280)); + } else if (matchmode->value) { // Light blue + json_object_set_new(embed, "description", json_string(MM_MATCH_END_MSG)); + json_object_set_new(embed, "color", json_integer(65535)); + } else { // Red + json_object_set_new(embed, "description", json_string(DM_MATCH_END_MSG)); + json_object_set_new(embed, "color", json_integer(16711680)); + } + // Add fields to the embed object - json_object_set_new(embed, "description", json_string("Match results")); json_object_set_new(embed, "title", json_string(level.mapname)); - json_object_set_new(embed, "color", json_integer(16711680)); // Create the "thumbnail" object json_t *thumbnail = json_object(); @@ -254,70 +264,83 @@ static char *discord_MatchEndMsg(void) json_object_set_new(embed, "fields", fields); // Create field objects and add them to the "fields" array + // Checks teamplay first, then deathmatch if (teamplay->value) { - char *t1name = TeamName(TEAM1); - char *t2name = TeamName(TEAM2); - int t1score = teams[TEAM1].score; - int t2score = teams[TEAM2].score; - char field1content[64]; - snprintf(field1content, sizeof(field1content), "%s: %d", t1name, t1score); - char field2content[64]; - snprintf(field2content, sizeof(field2content), "%s: %d", t2name, t2score); - char *t1_player_scores = TeamConstructPlayerScoreString(TEAM1); - char *t2_player_scores = TeamConstructPlayerScoreString(TEAM2); + for (int team = TEAM1; team <= teamCount; team++) { + char *team_name = TeamName(team); + int team_score = teams[team].score; + char field_content[64]; + snprintf(field_content, sizeof(field_content), "%s: %d", team_name, team_score); + char *team_player_scores = TeamConstructPlayerScoreString(team); + + // Format team player scores + char formatted_team_player_scores[1024] = ""; // Initialize buffer to empty string + char line[64]; // Buffer for each formatted line + char *token; + int offset = 0; + + // Tokenize the team_player_scores string and format each line + token = strtok(team_player_scores, "\n"); + while (token != NULL) { + char player[17]; // Buffer for player name (16 characters + null terminator) + int score; + sscanf(token, "%16[^:]: %d", player, &score); // Read player name and score + snprintf(line, sizeof(line), "%-16s %4d\n", player, score); // Format with fixed width + offset += snprintf(formatted_team_player_scores + offset, sizeof(formatted_team_player_scores) - offset, "%s", line); + token = strtok(NULL, "\n"); + } - // Add triple backticks for Discord formatting - char formatted_t1_player_scores[256]; - snprintf(formatted_t1_player_scores, sizeof(formatted_t1_player_scores), "```%s```", t1_player_scores); - char formatted_t2_player_scores[256]; - snprintf(formatted_t2_player_scores, sizeof(formatted_t2_player_scores), "```%s```", t2_player_scores); - - - if(teamCount == 2) { - json_t *field1 = json_object(); - json_object_set_new(field1, "name", json_string(field1content)); - json_object_set_new(field1, "value", json_string(formatted_t1_player_scores)); - json_object_set_new(field1, "inline", json_true()); - json_array_append_new(fields, field1); - - json_t *field2 = json_object(); - json_object_set_new(field2, "name", json_string(field2content)); - json_object_set_new(field2, "value", json_string(formatted_t2_player_scores)); - json_object_set_new(field2, "inline", json_true()); - json_array_append_new(fields, field2); - } else { - char *t3name = TeamName(TEAM3); - int t3score = teams[TEAM3].score; - char field3content[64]; - snprintf(field3content, sizeof(field3content), "%s: %d", t3name, t3score); - char *t3_player_scores = TeamConstructPlayerScoreString(TEAM3); // Add triple backticks for Discord formatting - char formatted_t3_player_scores[256]; - snprintf(formatted_t3_player_scores, sizeof(formatted_t3_player_scores), "```%s```", t3_player_scores); - - json_t *field1 = json_object(); - json_object_set_new(field1, "name", json_string(field1content)); - json_object_set_new(field1, "value", json_string(formatted_t1_player_scores)); - json_object_set_new(field1, "inline", json_true()); - json_array_append_new(fields, field1); - - json_t *field2 = json_object(); - json_object_set_new(field2, "name", json_string(field2content)); - json_object_set_new(field2, "value", json_string(formatted_t2_player_scores)); - json_object_set_new(field2, "inline", json_true()); - json_array_append_new(fields, field2); - - json_t *field3 = json_object(); - json_object_set_new(field3, "name", json_string(field3content)); - json_object_set_new(field3, "value", json_string(formatted_t3_player_scores)); - json_object_set_new(field3, "inline", json_true()); - json_array_append_new(fields, field3); + char discord_formatted_team_player_scores[1280]; + snprintf(discord_formatted_team_player_scores, sizeof(discord_formatted_team_player_scores), "```%s```", formatted_team_player_scores); + + json_t *field = json_object(); + json_object_set_new(field, "name", json_string(field_content)); + json_object_set_new(field, "value", json_string(discord_formatted_team_player_scores)); + json_object_set_new(field, "inline", json_false()); + json_array_append_new(fields, field); } + } else { // Deathmatch + char *player_scores = DMConstructPlayerScoreString(); + char formatted_player_scores[1024]; // Increase buffer size to accommodate formatted scores + char line[64]; // Buffer for each formatted line + char *token; + int offset = 0; + + // Tokenize the player_scores string and format each line + token = strtok(player_scores, "\n"); + while (token != NULL) { + char player[17]; // Buffer for player name (16 characters + null terminator) + int score; + // Read player name and score, allowing spaces in player names + sscanf(token, "%16[^:]: %d", player, &score); + // Format with fixed width + snprintf(line, sizeof(line), "%-16s %4d\n", player, score); + // Append formatted line to the result + offset += snprintf(formatted_player_scores + offset, sizeof(formatted_player_scores) - offset, "%s", line); + token = strtok(NULL, "\n"); + } + + // Add triple backticks for Discord formatting + char discord_formatted_scores[1280]; + snprintf(discord_formatted_scores, sizeof(discord_formatted_scores), "```%s```", formatted_player_scores); + + json_t *field = json_object(); + json_object_set_new(field, "name", json_string("")); // Empty on purpose + json_object_set_new(field, "value", json_string(discord_formatted_scores)); + json_object_set_new(field, "inline", json_true()); + json_array_append_new(fields, field); + } // Add "username" field to the root object json_object_set_new(root, "username", json_string(hostname->string)); + // Create the "author" object (hostname) + json_t *author = json_object(); + json_object_set_new(author, "name", json_string(hostname->string)); + json_object_set_new(embed, "author", author); + // Convert the JSON object to a string char *json_payload = json_dumps(root, JSON_INDENT(4)); From ca2afadce74c412c1d9426520440dcc474ab9200 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 28 Aug 2024 17:02:22 -0400 Subject: [PATCH 640/974] Some cvar renaming, added message_timer_check, five minute warning option --- src/action/a_game.c | 15 +++++++++++++++ src/action/a_game.h | 3 +++ src/action/a_team.c | 4 ++++ src/action/g_cmds.c | 1 + src/action/g_local.h | 7 ++++--- src/action/g_main.c | 6 +++--- src/action/g_save.c | 4 ++-- src/action/tng_net.c | 27 ++++++++++++++++++--------- src/action/tng_net.h | 3 ++- 9 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index e44bbb6b1..48a540d74 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1454,3 +1454,18 @@ void GetNearbyTeammates( edict_t *self, char *buf ) Q_strncatz( buf, nearby_teammates[l]->client->pers.netname, PARSE_BUFSIZE ); } } + +void Cmd_Pickup_f(edict_t* ent) +{ + char *playername = ent->client->pers.netname; + char msg[256]; + snprintf(msg, sizeof(msg), "%s is requesting a pickup at %s (%s:%s)", playername, hostname->string, server_ip->string, net_port->string); + + // 5 minute timer + if(message_timer_check(300)) { + lc_discord_webhook(msg, SERVER_MSG); + gi.cprintf(ent, PRINT_HIGH, "Pickup request sent.\n"); + } else { + gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); + } +} \ No newline at end of file diff --git a/src/action/a_game.h b/src/action/a_game.h index 10b820490..024c7520d 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -148,3 +148,6 @@ void MakeAllLivePlayersObservers( void ); //a_cmds.c void Cmd_NextMap_f( edict_t * ent ); void Cmd_PrintRules_f(edict_t *ent); + +//a_game.c +void Cmd_Pickup_f( edict_t * ent ); \ No newline at end of file diff --git a/src/action/a_team.c b/src/action/a_team.c index 641586701..e2460e7ac 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2576,6 +2576,10 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; } + else if( timewarning < 1 && timelimit->value > 5 && level.matchTime >= ((timelimit->value - 5) * 60) && !during_countdown) + { + lc_discord_webhook(NULL, FIVE_MIN_WARN); + } } } diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 2e1ee41b7..08690e73e 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -2002,6 +2002,7 @@ static cmdList_t commandList[] = { "volunteer", Cmd_Volunteer_f, 0}, { "leader", Cmd_Volunteer_f, 0}, { "highscores", Cmd_HighScores_f, 0}, + { "pickup", Cmd_Pickup_f, 0}, }; #define MAX_COMMAND_HASH 64 diff --git a/src/action/g_local.h b/src/action/g_local.h index 811efef78..3c54d420d 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -433,7 +433,8 @@ typedef enum { CHAT_MSG = BIT(3), // 8 SERVER_MSG = BIT(4), // 16 MATCH_END_MSG = BIT(5), // 32 - NOTIFY_MAX = BIT(6) // 64 + FIVE_MIN_WARN = BIT(6), // 64 + NOTIFY_MAX = BIT(7) // 128 } Discord_Notifications; // Default messages @@ -1339,8 +1340,8 @@ extern cvar_t *sv_discord_announce_enable; extern cvar_t *sv_curl_stat_enable; extern cvar_t *sv_aws_access_key; extern cvar_t *sv_aws_secret_key; -extern cvar_t *sv_curl_discord_chat_url; -extern cvar_t *sv_curl_discord_server_url; +extern cvar_t *sv_curl_discord_info_url; +extern cvar_t *sv_curl_discord_pickup_url; extern cvar_t *server_ip; extern cvar_t *server_port; extern cvar_t *sv_last_announce_interval; diff --git a/src/action/g_main.c b/src/action/g_main.c index 9af95c589..7a30fd396 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -561,14 +561,14 @@ cvar_t *sv_discord_announce_enable; // Enable Discord announcements cvar_t *sv_curl_stat_enable; // Enable cURL stat logging cvar_t *sv_aws_access_key; // AWS Access Key (stat logs) cvar_t *sv_aws_secret_key; // AWS Secret Key (stat logs) -cvar_t *sv_curl_discord_chat_url; // Discord message URL (sending messages to Discord via webhook) -cvar_t *sv_curl_discord_server_url; // Discord server URL +cvar_t *sv_curl_discord_info_url; // Discord webhook (#info-feed channel) +cvar_t *sv_curl_discord_pickup_url; // Discord webhook (#pickup channel) cvar_t *server_ip; // Server IP cvar_t *server_port; // Server port cvar_t *sv_last_announce_interval; // Interval between announcements cvar_t *sv_last_announce_time; // Last announcement time cvar_t *server_announce_url; // Server announce URL -cvar_t *msgflags; // Message flags (like dmflags) +cvar_t *msgflags; // Message flags (like dmflags) see Discord_Notifications enum for more info // end cURL integration cvars cvar_t *training_mode; // Sets training mode vars diff --git a/src/action/g_save.c b/src/action/g_save.c index 25a6de4fc..22a25a3ec 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -658,8 +658,8 @@ void InitGame( void ) sv_curl_stat_enable = gi.cvar("sv_curl_stat_enable", "0", 0); sv_aws_access_key = gi.cvar("sv_aws_access_key", "disabled", 0); // Never include this in serverinfo! sv_aws_secret_key = gi.cvar("sv_aws_secret_key", "disabled", 0); // Never include this in serverinfo! - sv_curl_discord_chat_url = gi.cvar("sv_curl_discord_chat_url", "disabled", 0); - sv_curl_discord_server_url = gi.cvar("sv_curl_discord_server_url", "disabled", 0); + sv_curl_discord_info_url = gi.cvar("sv_curl_discord_info_url", "disabled", 0); + sv_curl_discord_pickup_url = gi.cvar("sv_curl_discord_pickup_url", "disabled", 0); server_ip = gi.cvar("server_ip", "", 0); // Never include this in serverinfo! server_port = gi.cvar("server_port", "", 0); // Never include this in serverinfo! sv_last_announce_time = gi.cvar("sv_last_announce_time", "0", 0); diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 51cb3b722..57bb73f0e 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -139,7 +139,7 @@ void announce_server_populating(void) json_t *root = json_object(); json_object_set_new(root, "srv_announce", srv_announce); - json_object_set_new(root, "webhook_url", json_string(sv_curl_discord_server_url->string)); + json_object_set_new(root, "webhook_url", json_string(sv_curl_discord_pickup_url->string)); char *message = json_dumps(root, 0); // 0 is the flags parameter, you can set it to JSON_INDENT(4) for pretty printing @@ -352,7 +352,7 @@ static char *discord_MatchEndMsg(void) /* Call this with a string containing the message you want to send to the webhook. -Limited to 1024 chars. +Limited to 2048 chars. */ void lc_discord_webhook(char* message, Discord_Notifications msgtype) { @@ -371,12 +371,12 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) } // Don't run this if curl is disabled or the webhook URL is set to "disabled" - if (!sv_curl_enable->value || strcmp(sv_curl_discord_chat_url->string, "disabled") == 0) + if (!sv_curl_enable->value || strcmp(sv_curl_discord_info_url->string, "disabled") == 0) return; // Use webhook.site to test curl, it's very handy! //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; - char* url = sv_curl_discord_chat_url->string; + char* url = sv_curl_discord_info_url->string; // Get a new request object request = new_request(); @@ -392,10 +392,14 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) } // Format the message as a JSON payload based on msgtype - if (msgtype == CHAT_MSG || msgtype == DEATH_MSG) { + if (msgtype == CHAT_MSG || msgtype == DEATH_MSG || msgtype == SERVER_MSG) { snprintf(json_payload, sizeof(json_payload), "{\"content\":\"```%s```\"}", message); + } else if (msgtype == SERVER_MSG) { + snprintf(json_payload, sizeof(json_payload), + "{\"content\":\"%s\"}", + message); } else if (msgtype == MATCH_END_MSG){ char *matchendmsg = discord_MatchEndMsg(); if (matchendmsg) { @@ -453,7 +457,7 @@ void lc_server_announce(char *path, char *message) // Don't run this if curl is disabled or the webhook URLs are set to "disabled" or empty, or if no server IP or port is set if (!cvar_check(sv_curl_enable) || !cvar_check(server_announce_url) - || !cvar_check(sv_curl_discord_server_url) + || !cvar_check(sv_curl_discord_pickup_url) || !cvar_check(server_ip) || !cvar_check(server_port)) { @@ -658,9 +662,14 @@ void lc_once_per_gameframe(void) current_requests = 0; } -qboolean ready_to_announce(void) +qboolean message_timer_check(int delay) { - if (level.framenum % 100 == 0 && level.lc_recently_sent) + static time_t last_announcement_time = 0; + time_t current_time = time(NULL); + + if (difftime(current_time, last_announcement_time) >= delay) { + last_announcement_time = current_time; return true; + } return false; -} +} \ No newline at end of file diff --git a/src/action/tng_net.h b/src/action/tng_net.h index 2df650c17..4dedce63c 100644 --- a/src/action/tng_net.h +++ b/src/action/tng_net.h @@ -26,4 +26,5 @@ extern size_t current_requests; void lc_server_announce(char *path, char *message); void announce_server_populating(void); -void lc_aqtion_stat_send(char *stats); \ No newline at end of file +void lc_aqtion_stat_send(char *stats); +qboolean message_timer_check(int delay); \ No newline at end of file From aa88f1f0007191977e37b87912fcc1227c2492d0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 16:35:53 +0300 Subject: [PATCH 641/974] Simplify parsing GL version, reject GL 1.0. --- src/refresh/qgl.c | 71 ++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 538cf5c6d..dc7ccaf84 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -404,10 +404,29 @@ static const glsection_t sections[] = { }, }; -static bool parse_version(void) +static const char *const es_prefixes[] = { + "OpenGL ES-CM ", + "OpenGL ES-CL ", + "OpenGL ES " +}; + +static int parse_version(const char *s) +{ + int major, minor; + + if (sscanf(s, "%d.%d", &major, &minor) < 2) + return 0; + + if (major < 1 || major > (INT_MAX - 99) / 100 || minor < 0 || minor > 99) + return 0; + + return QGL_VER(major, minor); +} + +static bool parse_gl_version(void) { const char *s; - int major, minor, ver; + int ver; bool gl_es = false; qglGetString = vid->get_proc_addr("glGetString"); @@ -425,37 +444,33 @@ static bool parse_version(void) Com_DPrintf("GL_VERSION: %s\n", s); // parse ES profile prefix - if (!strncmp(s, "OpenGL ES", 9)) { - s += 9; - if (s[0] == '-' && s[1] && s[2] && s[3] == ' ') - s += 4; - else if (s[0] == ' ') - s += 1; - else - return false; - gl_es = true; + for (int i = 0; i < q_countof(es_prefixes); i++) { + size_t len = strlen(es_prefixes[i]); + if (!strncmp(s, es_prefixes[i], len)) { + s += len; + gl_es = true; + } } // parse version - if (sscanf(s, "%d.%d", &major, &minor) < 2) - return false; - - if (major < 1 || minor < 0 || minor > 99) - return false; - - ver = QGL_VER(major, minor); - if (gl_es) + ver = parse_version(s); + if (gl_es) { gl_config.ver_es = ver; - else + return ver; + } + + // reject GL 1.0 + if (ver >= QGL_VER(1, 1)) { gl_config.ver_gl = ver; + return true; + } - return true; + return false; } static bool parse_glsl_version(void) { const char *s; - int major, minor; if (gl_config.ver_gl < QGL_VER(2, 0) && gl_config.ver_es < QGL_VER(2, 0)) return true; @@ -469,14 +484,8 @@ static bool parse_glsl_version(void) if (gl_config.ver_es && !strncmp(s, "OpenGL ES GLSL ES ", 18)) s += 18; - if (sscanf(s, "%d.%d", &major, &minor) < 2) - return false; - - if (major < 1 || minor < 0 || minor > 99) - return false; - - gl_config.ver_sl = QGL_VER(major, minor); - return true; + gl_config.ver_sl = parse_version(s); + return gl_config.ver_sl; } static bool extension_blacklisted(const char *search) @@ -545,7 +554,7 @@ void QGL_Shutdown(void) bool QGL_Init(void) { - if (!parse_version()) { + if (!parse_gl_version()) { Com_EPrintf("OpenGL returned invalid version string\n"); return false; } From c660e41c8cc28192a671f2ea0d505bfb93297e91 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 17:14:20 +0300 Subject: [PATCH 642/974] Fix GL entry point sorting. --- src/refresh/qgl.c | 2 +- src/refresh/qgl.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index dc7ccaf84..55ea09763 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -299,8 +299,8 @@ static const glsection_t sections[] = { .caps = QGL_CAP_SHADER, .functions = (const glfunction_t []) { QGL_FN(BindBufferBase), - QGL_FN(GetUniformBlockIndex), QGL_FN(GetActiveUniformBlockiv), + QGL_FN(GetUniformBlockIndex), QGL_FN(UniformBlockBinding), { NULL } } diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index bf28b7f21..6207ad5cc 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -157,8 +157,8 @@ QGLAPI void (APIENTRYP qglRenderbufferStorage)(GLenum target, GLenum internalfor // GL 3.1 QGLAPI void (APIENTRYP qglBindBufferBase)(GLenum target, GLuint index, GLuint buffer); -QGLAPI GLuint (APIENTRYP qglGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); QGLAPI void (APIENTRYP qglGetActiveUniformBlockiv)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params); +QGLAPI GLuint (APIENTRYP qglGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); QGLAPI void (APIENTRYP qglUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); // GL 4.1 From b65f1d5c090927b852198f502996f27d7f68f33c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 22:38:29 +0300 Subject: [PATCH 643/974] Simplify working with matrices. Use single load_matrix() method to load both modelview and projection matrices. Use pre-multiplied view-projection matrix for shader backend. --- src/refresh/gl.h | 12 ++++++------ src/refresh/legacy.c | 17 +++-------------- src/refresh/shader.c | 32 +++++++++++++++----------------- src/refresh/state.c | 8 +++++--- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 2c934554a..8a9742529 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -520,8 +520,7 @@ typedef enum { } glTmu_t; typedef struct { - GLfloat view[16]; - GLfloat proj[16]; + GLfloat mvp[16]; GLfloat time; GLfloat modulate; GLfloat add; @@ -542,6 +541,8 @@ typedef struct { GLuint currentbuffer[2]; glVertexArray_t currentva; const GLfloat *currentmatrix; + GLfloat view_matrix[16]; + GLfloat proj_matrix[16]; glUniformBlock_t u_block; } glState_t; @@ -563,8 +564,7 @@ typedef struct { void (*setup_2d)(void); void (*setup_3d)(void); - void (*load_proj_matrix)(const GLfloat *matrix); - void (*load_view_matrix)(const GLfloat *matrix); + void (*load_matrix)(GLenum mode, const GLfloat *matrix); void (*state_bits)(glStateBits_t bits); void (*array_bits)(glArrayBits_t bits); @@ -611,14 +611,14 @@ static inline void GL_ArrayBits(glArrayBits_t bits) static inline void GL_ForceMatrix(const GLfloat *matrix) { - gl_backend->load_view_matrix(matrix); + gl_backend->load_matrix(GL_MODELVIEW, matrix); gls.currentmatrix = matrix; } static inline void GL_LoadMatrix(const GLfloat *matrix) { if (gls.currentmatrix != matrix) { - gl_backend->load_view_matrix(matrix); + gl_backend->load_matrix(GL_MODELVIEW, matrix); gls.currentmatrix = matrix; } } diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index ef8855928..a86b1a03d 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -152,19 +152,9 @@ static void legacy_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) qglColor4f(r, g, b, a); } -static void legacy_load_view_matrix(const GLfloat *matrix) +static void legacy_load_matrix(GLenum mode, const GLfloat *matrix) { - qglMatrixMode(GL_MODELVIEW); - - if (matrix) - qglLoadMatrixf(matrix); - else - qglLoadIdentity(); -} - -static void legacy_load_proj_matrix(const GLfloat *matrix) -{ - qglMatrixMode(GL_PROJECTION); + qglMatrixMode(mode); qglLoadMatrixf(matrix); } @@ -250,8 +240,7 @@ const glbackend_t backend_legacy = { .shutdown = legacy_shutdown, .clear_state = legacy_clear_state, - .load_proj_matrix = legacy_load_proj_matrix, - .load_view_matrix = legacy_load_view_matrix, + .load_matrix = legacy_load_matrix, .state_bits = legacy_state_bits, .array_bits = legacy_array_bits, diff --git a/src/refresh/shader.c b/src/refresh/shader.c index efb64dcd0..edd6dbaef 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -42,8 +42,7 @@ static void write_block(sizebuf_t *buf) { GLSF("layout(std140) uniform u_block {\n"); GLSL( - mat4 m_view; - mat4 m_proj; + mat4 m_vp; float u_time; float u_modulate; float u_add; @@ -81,7 +80,7 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(v_lmtc = a_lmtc;) if (!(bits & GLS_TEXTURE_REPLACE)) GLSL(v_color = a_color;) - GLSL(gl_Position = m_proj * m_view * a_pos;) + GLSL(gl_Position = m_vp * a_pos;) GLSF("}\n"); } @@ -329,20 +328,20 @@ static void upload_u_block(void) c.uniformUploads++; } -static void shader_load_view_matrix(const GLfloat *matrix) +static void shader_load_matrix(GLenum mode, const GLfloat *matrix) { - static const GLfloat identity[16] = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; - - if (!matrix) - matrix = identity; - - memcpy(gls.u_block.view, matrix, sizeof(gls.u_block.view)); - upload_u_block(); -} + switch (mode) { + case GL_MODELVIEW: + memcpy(gls.view_matrix, matrix, sizeof(gls.view_matrix)); + break; + case GL_PROJECTION: + memcpy(gls.proj_matrix, matrix, sizeof(gls.proj_matrix)); + break; + default: + Q_assert(!"bad mode"); + } -static void shader_load_proj_matrix(const GLfloat *matrix) -{ - memcpy(gls.u_block.proj, matrix, sizeof(gls.u_block.proj)); + GL_MultMatrix(gls.u_block.mvp, gls.proj_matrix, gls.view_matrix); upload_u_block(); } @@ -434,8 +433,7 @@ const glbackend_t backend_shader = { .setup_2d = shader_setup_2d, .setup_3d = shader_setup_3d, - .load_proj_matrix = shader_load_proj_matrix, - .load_view_matrix = shader_load_view_matrix, + .load_matrix = shader_load_matrix, .state_bits = shader_state_bits, .array_bits = shader_array_bits, diff --git a/src/refresh/state.c b/src/refresh/state.c index 2292d5f71..61f9a5189 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -22,6 +22,8 @@ glState_t gls; const glbackend_t *gl_backend; +static const GLfloat identity[16] = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; + // for uploading void GL_ForceTexture(glTmu_t tmu, GLuint texnum) { @@ -148,7 +150,7 @@ void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat zn matrix[11] = 0; matrix[15] = 1; - gl_backend->load_proj_matrix(matrix); + gl_backend->load_matrix(GL_PROJECTION, matrix); } void GL_Setup2D(void) @@ -169,7 +171,7 @@ void GL_Setup2D(void) if (gl_backend->setup_2d) gl_backend->setup_2d(); - gl_backend->load_view_matrix(NULL); + gl_backend->load_matrix(GL_MODELVIEW, identity); } void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) @@ -215,7 +217,7 @@ void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) matrix[11] = -1; matrix[15] = 0; - gl_backend->load_proj_matrix(matrix); + gl_backend->load_matrix(GL_PROJECTION, matrix); } static void GL_RotateForViewer(void) From 4a18bec7240906f59da5cc3d3ac1d289801d8b15 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 22:47:11 +0300 Subject: [PATCH 644/974] Clean up write_vertex_shader(). --- src/refresh/shader.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index edd6dbaef..653bef55d 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -60,26 +60,33 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) { write_header(buf); write_block(buf); + GLSL(in vec4 a_pos;) GLSL(in vec2 a_tc;) GLSL(out vec2 v_tc;) + if (bits & GLS_LIGHTMAP_ENABLE) { GLSL(in vec2 a_lmtc;) GLSL(out vec2 v_lmtc;) } + if (!(bits & GLS_TEXTURE_REPLACE)) { GLSL(in vec4 a_color;) GLSL(out vec4 v_color;) } + GLSF("void main() {\n"); - GLSL(vec2 tc = a_tc;) if (bits & GLS_SCROLL_ENABLE) - GLSL(tc += u_time * u_scroll;) - GLSL(v_tc = tc;) + GLSL(v_tc = a_tc + u_time * u_scroll;) + else + GLSL(v_tc = a_tc;) + if (bits & GLS_LIGHTMAP_ENABLE) GLSL(v_lmtc = a_lmtc;) + if (!(bits & GLS_TEXTURE_REPLACE)) GLSL(v_color = a_color;) + GLSL(gl_Position = m_vp * a_pos;) GLSF("}\n"); } From fbf0b1ab4575f508287bb39d6825264dc7354d9f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 27 Aug 2024 23:16:14 +0300 Subject: [PATCH 645/974] Pre-multiply scroll pos. --- src/refresh/gl.h | 2 +- src/refresh/legacy.c | 4 ++-- src/refresh/shader.c | 4 ++-- src/refresh/state.c | 4 +++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 8a9742529..ec47a2211 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -665,7 +665,7 @@ typedef enum { void GL_ForceTexture(glTmu_t tmu, GLuint texnum); void GL_BindTexture(glTmu_t tmu, GLuint texnum); void GL_CommonStateBits(glStateBits_t bits); -void GL_ScrollSpeed(vec2_t scroll, glStateBits_t bits); +void GL_ScrollPos(vec2_t scroll, glStateBits_t bits); void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar); void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x); diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index a86b1a03d..6f141dcba 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -48,8 +48,8 @@ static void legacy_state_bits(glStateBits_t bits) if (bits & GLS_SCROLL_ENABLE) { vec2_t scroll; - GL_ScrollSpeed(scroll, bits); - qglTranslatef(scroll[0] * glr.fd.time, scroll[1] * glr.fd.time, 0); + GL_ScrollPos(scroll, bits); + qglTranslatef(scroll[0], scroll[1], 0); } } diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 653bef55d..0b28f7a31 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -77,7 +77,7 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) GLSF("void main() {\n"); if (bits & GLS_SCROLL_ENABLE) - GLSL(v_tc = a_tc + u_time * u_scroll;) + GLSL(v_tc = a_tc + u_scroll;) else GLSL(v_tc = a_tc;) @@ -287,7 +287,7 @@ static void shader_state_bits(glStateBits_t bits) } if (diff & GLS_SCROLL_MASK && bits & GLS_SCROLL_ENABLE) { - GL_ScrollSpeed(gls.u_block.scroll, bits); + GL_ScrollPos(gls.u_block.scroll, bits); upload_u_block(); } } diff --git a/src/refresh/state.c b/src/refresh/state.c index 61f9a5189..ebfde37fd 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -100,7 +100,7 @@ void GL_CommonStateBits(glStateBits_t bits) } } -void GL_ScrollSpeed(vec2_t scroll, glStateBits_t bits) +void GL_ScrollPos(vec2_t scroll, glStateBits_t bits) { float speed = 1.6f; @@ -112,6 +112,8 @@ void GL_ScrollSpeed(vec2_t scroll, glStateBits_t bits) if (bits & GLS_SCROLL_FLIP) speed = -speed; + speed *= glr.fd.time; + if (bits & GLS_SCROLL_Y) { scroll[0] = 0; scroll[1] = speed; From 054f08787bc23826b95c68166304d375215f28b3 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 28 Aug 2024 01:10:27 +0300 Subject: [PATCH 646/974] Skip zero width beams. --- src/refresh/tess.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index afd14daa8..7889b231d 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -327,6 +327,8 @@ void GL_DrawBeams(void) for (i = 0, ent = glr.fd.entities; i < glr.fd.num_entities; i++, ent++) { if (!(ent->flags & RF_BEAM)) continue; + if (!ent->frame) + continue; VectorCopy(ent->origin, segs[0]); VectorCopy(ent->oldorigin, segs[1]); From 7c43bccbf88085013938418525354debc41f5b3a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 28 Aug 2024 11:22:48 +0300 Subject: [PATCH 647/974] Rename NUM_TEXNUMS define for clarity. --- src/refresh/gl.h | 10 +++++----- src/refresh/texture.c | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index ec47a2211..0178ea7fc 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -47,11 +47,11 @@ typedef GLushort glIndex_t; typedef GLuint glIndex_t; #endif -#define TAB_SIN(x) gl_static.sintab[(x) & 255] -#define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] +#define TAB_SIN(x) gl_static.sintab[(x) & 255] +#define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] -#define MAX_PROGRAMS 128 -#define NUM_TEXNUMS 7 +#define MAX_PROGRAMS 128 +#define NUM_AUTO_TEXTURES 7 typedef struct { GLuint query; @@ -78,7 +78,7 @@ typedef struct { GLuint index_buffer; GLuint vertex_buffer; GLuint programs[MAX_PROGRAMS]; - GLuint texnums[NUM_TEXNUMS]; + GLuint texnums[NUM_AUTO_TEXTURES]; GLenum samples_passed; GLbitfield stencil_buffer_bit; float entity_modulate; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 0d9def578..8de9b1fd0 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -1080,7 +1080,7 @@ void GL_InitImages(void) colorscale = Cvar_ClampValue(gl_saturation, 0, 1); lightscale = !(gl_gamma->value == 1.0f && (gl_static.use_shaders || gl_intensity->value == 1.0f)); - qglGenTextures(NUM_TEXNUMS, gl_static.texnums); + qglGenTextures(NUM_AUTO_TEXTURES, gl_static.texnums); qglGenTextures(LM_MAX_LIGHTMAPS, lm.texnums); if (gl_static.use_shaders) { @@ -1120,7 +1120,7 @@ void GL_ShutdownImages(void) gl_partshape->changed = NULL; // delete auto textures - qglDeleteTextures(NUM_TEXNUMS, gl_static.texnums); + qglDeleteTextures(NUM_AUTO_TEXTURES, gl_static.texnums); qglDeleteTextures(LM_MAX_LIGHTMAPS, lm.texnums); memset(gl_static.texnums, 0, sizeof(gl_static.texnums)); From ea8acb6775c47f8d7ebb8b5488177a853cb3d551 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:26:39 -0400 Subject: [PATCH 648/974] Working on awardstats printing as well --- src/action/a_cmds.c | 1 + src/action/a_team.c | 8 +-- src/action/g_local.h | 4 +- src/action/p_client.c | 3 +- src/action/tng_net.c | 149 ++++++++++++++++++++++++++++++++++++----- src/action/tng_stats.c | 17 ++--- 6 files changed, 145 insertions(+), 37 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index b54d0679b..59cfcd1bb 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -1313,6 +1313,7 @@ void Cmd_Ghost_f(edict_t * ent) memcpy(ent->client->resp.hitsLocations, ghost->hitsLocations, sizeof(ent->client->resp.hitsLocations)); memcpy(ent->client->resp.gunstats, ghost->gunstats, sizeof(ent->client->resp.gunstats)); + memcpy(ent->client->resp.awardstats, ghost->awardstats, sizeof(ent->client->resp.awardstats)); //Remove it from the list for (i += 1; i < num_ghost_players; i++) { diff --git a/src/action/a_team.c b/src/action/a_team.c index e2460e7ac..fb69a33ac 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2576,10 +2576,10 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; } - else if( timewarning < 1 && timelimit->value > 5 && level.matchTime >= ((timelimit->value - 5) * 60) && !during_countdown) - { - lc_discord_webhook(NULL, FIVE_MIN_WARN); - } + } + else if ( use_warnings->value && matchmode->value ){ + if( timelimit->value > 5 && level.matchTime >= ((timelimit->value - 5) * 60) && !during_countdown) + lc_discord_webhook(MM_FIVE_MIN_WARN, FIVE_MIN_WARN); } } diff --git a/src/action/g_local.h b/src/action/g_local.h index 3c54d420d..16fcb31c8 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -441,6 +441,7 @@ typedef enum { #define MM_MATCH_END_MSG "Matchmode Results" #define TP_MATCH_END_MSG "Teamplay Results" #define DM_MATCH_END_MSG "Deathmatch Results" +#define MM_FIVE_MIN_WARN "5 minutes remaining in the map" //deadflag #define DEAD_NO 0 @@ -1929,7 +1930,7 @@ typedef struct int hitsLocations[LOC_MAX]; //Number of hits for different locations gunStats_t gunstats[MOD_TOTAL]; //Number of shots/hits for different guns, adjusted to MOD_TOTAL to allow grenade, kick and punch stats - + int awardstats[AWARD_MAX]; //Number of impressive, excellent and accuracy awards //AQ2:TNG - Slicer: Video Checking and further Cheat cheking vars char vidref[16]; char gldriver[16]; @@ -2703,6 +2704,7 @@ typedef struct int hitsTotal; int hitsLocations[LOC_MAX]; gunStats_t gunstats[MOD_TOTAL]; + int awardstats[AWARD_MAX]; int team; gitem_t *weapon; gitem_t *item; diff --git a/src/action/p_client.c b/src/action/p_client.c index 19a481385..ed4186536 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -388,7 +388,7 @@ void Announce_Reward(edict_t *ent, int rewardType) { gi.dprintf("%s: Unknown reward type %d for %s\n", __FUNCTION__, rewardType, playername); return; // Something didn't jive here? } - + ent->client->resp.awardstats[rewardType]++; CenterPrintAll(buf); gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); @@ -3823,6 +3823,7 @@ void CreateGhost(edict_t * ent) memcpy(ghost->hitsLocations, ent->client->resp.hitsLocations, sizeof(ghost->hitsLocations)); memcpy(ghost->gunstats, ent->client->resp.gunstats, sizeof(ghost->gunstats)); + memcpy(ghost->awardstats, ent->client->resp.awardstats, sizeof(ghost->awardstats)); } //============================================================== diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 57bb73f0e..aaa559b44 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -8,6 +8,8 @@ Special thanks to Phatman for a bulk of the core code #include "g_local.h" #include +qboolean curldebug = false; + // You will need one of these for each of the requests ... // ... if you allow concurrent requests to be sent at the same time request_list_t *active_requests, *unused_requests; @@ -151,6 +153,79 @@ void announce_server_populating(void) gi.cvar_forceset("sv_last_announce_time", va("%d", (int)time(NULL))); } +static char* ConstructPlayerAwardStatString(void) { + gclient_t **ranks = (gclient_t **)malloc(game.maxclients * sizeof(gclient_t *)); + int total = G_CalcRanks(ranks); + + // Calculate the required buffer size + int buffer_size = 0; + for (int award = 0; award < AWARD_MAX; award++) { + for (int i = 0; i < total; i++) { + if (ranks[i]->resp.awardstats[award] > 0) { + buffer_size += snprintf(NULL, 0, "%s: %s (%d)\n", + award == ACCURACY ? "Accuracy" : + award == IMPRESSIVE ? "Impressive" : + award == EXCELLENT ? "Excellent" : + award == DOMINATING ? "Domination" : + award == UNSTOPPABLE ? "Unstoppable" : "Unknown", + ranks[i]->pers.netname, ranks[i]->resp.awardstats[award]); + } + } + } + + // Allocate the buffer + char *result = (char *)malloc(buffer_size + 1); + if (!result) { + free(ranks); + return NULL; // Handle memory allocation failure + } + + // Construct the string + char *ptr = result; + for (int award = 0; award < AWARD_MAX; award++) { + for (int i = 0; i < total; i++) { + if (ranks[i]->resp.awardstats[award] > 0) { + ptr += sprintf(ptr, "%s: %s (%d)\n", + award == ACCURACY ? "Accuracy" : + award == IMPRESSIVE ? "Impressive" : + award == EXCELLENT ? "Excellent" : + award == DOMINATING ? "Dominating" : + award == UNSTOPPABLE ? "Unstoppable" : "Unknown", + ranks[i]->pers.netname, ranks[i]->resp.awardstats[award]); + } + } + } + + // Construct the string for additional stats + double accuracy; + int shots; + // Variables to track the highest accuracy + double highest_accuracy = 0.0; + gclient_t *highest_accuracy_player = NULL; + + for (int i = 0; i < total; i++) { + shots = min(ranks[i]->resp.shotsTotal, 9999); + if (shots) + accuracy = (double)ranks[i]->resp.hitsTotal * 100.0 / (double)ranks[i]->resp.shotsTotal; + else + accuracy = 0; + + if (accuracy > highest_accuracy) { + highest_accuracy = accuracy; + highest_accuracy_player = ranks[i]; + } + } + + // Print the player with the highest accuracy + if (highest_accuracy_player) { + ptr += sprintf(ptr, "Highest accuracy: %s (%.2f%%)\n", highest_accuracy_player->pers.netname, highest_accuracy); + } + + + free(ranks); + return result; +} + static char* DMConstructPlayerScoreString(void) { gclient_t **ranks = (gclient_t **)malloc(game.maxclients * sizeof(gclient_t *)); int total = G_CalcRanks(ranks); @@ -259,6 +334,14 @@ static char *discord_MatchEndMsg(void) snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + // Add "username" field to the root object + json_object_set_new(root, "username", json_string(hostname->string)); + + // Create the "author" object (hostname) + json_t *author = json_object(); + json_object_set_new(author, "name", json_string(hostname->string)); + json_object_set_new(embed, "author", author); + // Create the "fields" array json_t *fields = json_array(); json_object_set_new(embed, "fields", fields); @@ -328,18 +411,40 @@ static char *discord_MatchEndMsg(void) json_t *field = json_object(); json_object_set_new(field, "name", json_string("")); // Empty on purpose json_object_set_new(field, "value", json_string(discord_formatted_scores)); - json_object_set_new(field, "inline", json_true()); + json_object_set_new(field, "inline", json_true()); // Inline true here because it's only one list json_array_append_new(fields, field); - } - // Add "username" field to the root object - json_object_set_new(root, "username", json_string(hostname->string)); + // TODO: Testing out player award stats printing, broken for now + + // char *player_stats = ConstructPlayerAwardStatString(); + // // Format stats + // char formatted_player_stats[1024] = ""; // Initialize buffer to empty string + // char line[128]; // Buffer for each formatted line + // char *token; + // int offset = 0; + + // // Tokenize the player_stats string and format each line + // token = strtok(player_stats, "\n"); + // while (token != NULL) { + // char award[32]; // Buffer for award name + // char player[17]; // Buffer for player name (16 characters + null terminator) + // int count; + // sscanf(token, "Most %31[^:]: %16[^ ] (%d)", award, player, &count); // Read award, player name, and count + // snprintf(line, sizeof(line), "Most %-16s %s (%d)\n", award, player, count); // Format with fixed width + // offset += snprintf(formatted_player_stats + offset, sizeof(formatted_player_stats) - offset, "%s", line); + // token = strtok(NULL, "\n"); + // } - // Create the "author" object (hostname) - json_t *author = json_object(); - json_object_set_new(author, "name", json_string(hostname->string)); - json_object_set_new(embed, "author", author); + // // Add triple backticks for Discord formatting + // char discord_formatted_scores[1280]; + // snprintf(discord_formatted_scores, sizeof(discord_formatted_scores), "```%s```", formatted_player_stats); + + // json_t *gamesettings = json_object(); + // json_object_set_new(gamesettings, "name", json_string("Awards / Stats")); + // json_object_set_new(gamesettings, "value", json_string(discord_formatted_scores)); + // json_object_set_new(gamesettings, "inline", json_true()); + // json_array_append_new(fields, gamesettings); // Convert the JSON object to a string char *json_payload = json_dumps(root, JSON_INDENT(4)); @@ -359,24 +464,32 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) request_t *request; char json_payload[2048]; - // Debug prints - gi.dprintf("msgflags value: %d\n", (int)msgflags->value); - gi.dprintf("msgtype value: %d\n", msgtype); + if (curldebug) { + // Debug prints + gi.dprintf("msgflags value: %d\n", (int)msgflags->value); + gi.dprintf("msgtype value: %d\n", msgtype); - // Check if the msgtype is within the allowed message flags - // Look at the Discord_Notifications enum in g_local.h for the flags - if (!(msgtype & (int)msgflags->value)) { - gi.dprintf("Message type not allowed by msgflags\n"); - return; + // Check if the msgtype is within the allowed message flags + // Look at the Discord_Notifications enum in g_local.h for the flags + if (!(msgtype & (int)msgflags->value)) { + gi.dprintf("Message type not allowed by msgflags\n"); + return; + } } // Don't run this if curl is disabled or the webhook URL is set to "disabled" if (!sv_curl_enable->value || strcmp(sv_curl_discord_info_url->string, "disabled") == 0) return; + // Default url is to the #info-feed channel + char* url = sv_curl_discord_info_url->string; + // Change webhook URL based on message type + if (msgtype == FIVE_MIN_WARN) + url = sv_curl_discord_pickup_url->string; + // Use webhook.site to test curl, it's very handy! //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; - char* url = sv_curl_discord_info_url->string; + //char* url = sv_curl_discord_info_url->string; // Get a new request object request = new_request(); @@ -396,7 +509,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) snprintf(json_payload, sizeof(json_payload), "{\"content\":\"```%s```\"}", message); - } else if (msgtype == SERVER_MSG) { + } else if (msgtype == SERVER_MSG || msgtype == FIVE_MIN_WARN) { snprintf(json_payload, sizeof(json_payload), "{\"content\":\"%s\"}", message); diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 770006165..ef6da4065 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -86,6 +86,9 @@ void ResetStats(edict_t *ent) for (i = 0; iclient->resp.hitsLocations[i] = 0; + for (i = 0; iclient->resp.awardstats[i] = 0; + memset(ent->client->resp.gunstats, 0, sizeof(ent->client->resp.gunstats)); } @@ -672,19 +675,6 @@ static void G_SaveScores(void) fclose(fp); } -// static int G_PlayerCmp(const void *p1, const void *p2) -// { -// gclient_t *a = *(gclient_t * const *)p1; -// gclient_t *b = *(gclient_t * const *)p2; - -// int r = b->resp.score - a->resp.score; -// if (!r) -// r = a->resp.deaths - b->resp.deaths; -// if (!r) -// r = (byte *)a - (byte *)b; -// return r; -// } - int G_CalcRanks(gclient_t **ranks) { int i, total; @@ -1516,6 +1506,7 @@ void LogEndMatchStats(void) // Copy all gunstats and hit locations to this dummy entity memcpy(ghlient->resp.hitsLocations, ghost->hitsLocations, sizeof(ghlient->resp.hitsLocations)); memcpy(ghlient->resp.gunstats, ghost->gunstats, sizeof(ghlient->resp.gunstats)); + memcpy(ghlient->resp.awardstats, ghost->awardstats, sizeof(ghlient->resp.awardstats)); WriteLogEndMatchStats(ghlient); } From e9b2e8dc00dfc711c777fd680f880535414501be Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:33:28 -0400 Subject: [PATCH 649/974] Removed duplicate jansson option in meson --- meson_options.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/meson_options.txt b/meson_options.txt index 014042b5f..ca9365458 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -78,11 +78,6 @@ option('libcurl', type: 'feature', value: 'auto', description: 'libcurl support') - -option('jansson', - type: 'feature', - value: 'auto', - description: 'AQtion jansson support') option('libjpeg', type: 'feature', From 29f719e577b8925f693d741a2ae97498d61a6ba8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:43:29 -0400 Subject: [PATCH 650/974] Included curl deps in meson for gamelib --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index f16811b76..53617df7e 100644 --- a/meson.build +++ b/meson.build @@ -396,7 +396,7 @@ endif common_deps = [zlib] client_deps = [png, curl, sdl2] server_deps = [curl] -game_deps = [zlib] +game_deps = [zlib, curl] jpeg = dependency('libjpeg', required: get_option('libjpeg'), From 105923d0b4cb52a3f1f7b0ddc900b02c634f5ea9 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:53:22 -0400 Subject: [PATCH 651/974] Windows always has the most problems somehow --- meson.build | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 53617df7e..fbeb68f05 100644 --- a/meson.build +++ b/meson.build @@ -393,10 +393,10 @@ if sdl2.found() config.set('USE_SDL', 'USE_CLIENT') endif -common_deps = [zlib] -client_deps = [png, curl, sdl2] -server_deps = [curl] -game_deps = [zlib, curl] +common_deps = [zlib, curl] +client_deps = [png, sdl2] +server_deps = [] +game_deps = [zlib] jpeg = dependency('libjpeg', required: get_option('libjpeg'), From 1a3dd69bea79e7465f8aae8d1e074b4b874b2d1f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:58:07 -0400 Subject: [PATCH 652/974] No Windows gamelib curl support for now --- meson.build | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index fbeb68f05..470a89d8f 100644 --- a/meson.build +++ b/meson.build @@ -205,7 +205,6 @@ action_src = [ 'src/action/tng_flashlight.c', 'src/action/tng_ini.c', 'src/action/tng_irc.c', - 'src/action/tng_net.c', 'src/action/tng_jump.c', 'src/action/tng_stats.c', 'src/shared/shared.c' @@ -384,6 +383,10 @@ curl = dependency('libcurl', if curl.found() client_src += 'src/client/http.c' + if not win32 + action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib + game_deps += curl + endif config.set10('USE_CURL', true) endif @@ -393,8 +396,8 @@ if sdl2.found() config.set('USE_SDL', 'USE_CLIENT') endif -common_deps = [zlib, curl] -client_deps = [png, sdl2] +common_deps = [zlib] +client_deps = [png, curl, sdl2] server_deps = [] game_deps = [zlib] From d4d24f97f9261b4ee82e98b97ad8a9bd9746a2dd Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 13:59:08 -0400 Subject: [PATCH 653/974] No Windows gamelib curl support for now --- meson.build | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/meson.build b/meson.build index 470a89d8f..505d3bf76 100644 --- a/meson.build +++ b/meson.build @@ -381,15 +381,6 @@ curl = dependency('libcurl', default_options: fallback_opt, ) -if curl.found() - client_src += 'src/client/http.c' - if not win32 - action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib - game_deps += curl - endif - config.set10('USE_CURL', true) -endif - sdl2 = dependency('sdl2', required: get_option('sdl2')) if sdl2.found() client_src += 'src/unix/video/sdl.c' @@ -401,6 +392,15 @@ client_deps = [png, curl, sdl2] server_deps = [] game_deps = [zlib] +if curl.found() + client_src += 'src/client/http.c' + if not win32 + action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib + game_deps += curl + endif + config.set10('USE_CURL', true) +endif + jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ] From dd1a731abf129d06889de00fb944c184fa9f40de Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:06:43 -0400 Subject: [PATCH 654/974] Trying a different approach --- meson.build | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 505d3bf76..1a4755f4b 100644 --- a/meson.build +++ b/meson.build @@ -381,6 +381,11 @@ curl = dependency('libcurl', default_options: fallback_opt, ) +if curl.found() + client_src += 'src/client/http.c' + config.set10('USE_CURL', true) +endif + sdl2 = dependency('sdl2', required: get_option('sdl2')) if sdl2.found() client_src += 'src/unix/video/sdl.c' @@ -392,15 +397,6 @@ client_deps = [png, curl, sdl2] server_deps = [] game_deps = [zlib] -if curl.found() - client_src += 'src/client/http.c' - if not win32 - action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib - game_deps += curl - endif - config.set10('USE_CURL', true) -endif - jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ] @@ -445,7 +441,6 @@ jansson_dep = dependency('jansson', ) if jansson_dep.found() game_deps += jansson_dep - #dll_link_args += '-ljansson' endif if not win32 @@ -462,6 +457,11 @@ if not win32 endif endif +if not win32 + action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib + game_deps += curl +endif + if host_machine.system() == 'darwin' # Darwin is warning-spamming because of its SDL_config defines add_project_arguments('-Wno-macro-redefined', language : 'c') From b3284727625e881980d5fe389a2c498232e54acb Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:17:30 -0400 Subject: [PATCH 655/974] Gotdamn I hate windows --- meson.build | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 1a4755f4b..731f19bde 100644 --- a/meson.build +++ b/meson.build @@ -457,10 +457,10 @@ if not win32 endif endif -if not win32 - action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib - game_deps += curl -endif +# if not win32 +# action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib +# game_deps += curl +# endif if host_machine.system() == 'darwin' # Darwin is warning-spamming because of its SDL_config defines From c2a62c1f350de9035a98bfccbb195e253a755097 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:40:00 -0400 Subject: [PATCH 656/974] Made aqtion-curl a thing --- .github/workflows/build.yml | 1 + meson.build | 6 ++++++ meson_options.txt | 5 +++++ src/action/a_game.c | 4 +++- src/action/a_game.h | 4 +++- src/action/a_match.c | 2 ++ src/action/a_team.c | 4 ++++ src/action/g_cmds.c | 4 ++++ src/action/g_local.h | 4 +++- src/action/g_main.c | 6 ++++-- src/action/g_save.c | 2 ++ src/action/p_client.c | 16 +++++++++++++--- src/action/p_hud.c | 2 ++ src/action/tng_net.c | 12 ++++++------ src/action/tng_stats.c | 4 ++++ 15 files changed, 62 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 093f36c20..82aa0f08a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,7 @@ env: -Dwayland=disabled -Dwrap_mode=forcefallback -Dx11=disabled + -Daqtion-curl=false MESON_ARGS_LINUX_WAYLAND: >- -Dwindows-crash-dumps=disabled diff --git a/meson.build b/meson.build index 731f19bde..f569f8f6c 100644 --- a/meson.build +++ b/meson.build @@ -564,6 +564,12 @@ if get_option('aqtion-build') dll_link_args += '-lcurl' endif +if get_option('aqtion-curl') + config.set10('AQTION_CURL', true) + action_src += 'src/action/tng_net.c' + game_deps += curl +endif + if get_option('discord-sdk') #if host_machine.system() == 'linux' if host_machine.cpu_family() == 'aarch64' # I am Linux ARM 64-bit diff --git a/meson_options.txt b/meson_options.txt index ca9365458..895385a2d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,11 @@ option('aqtion-build', value: true, description: 'Build-specific for AQtion enhancements') +option('aqtion-curl', + type: 'boolean', + value: true, + description: 'Add cURL support for AQtion gamelib') + option('anticheat-server', type: 'boolean', value: false, diff --git a/src/action/a_game.c b/src/action/a_game.c index 48a540d74..6f85d45cb 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1455,6 +1455,7 @@ void GetNearbyTeammates( edict_t *self, char *buf ) } } +#if AQTION_CURL void Cmd_Pickup_f(edict_t* ent) { char *playername = ent->client->pers.netname; @@ -1468,4 +1469,5 @@ void Cmd_Pickup_f(edict_t* ent) } else { gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/action/a_game.h b/src/action/a_game.h index 024c7520d..53d2e88aa 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -150,4 +150,6 @@ void Cmd_NextMap_f( edict_t * ent ); void Cmd_PrintRules_f(edict_t *ent); //a_game.c -void Cmd_Pickup_f( edict_t * ent ); \ No newline at end of file +#if AQTION_CURL +void Cmd_Pickup_f( edict_t * ent ); +#endif \ No newline at end of file diff --git a/src/action/a_match.c b/src/action/a_match.c index 1bc32c01f..0813cab11 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -91,7 +91,9 @@ void SendScores(void) LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match stats } + #if AQTION_CURL lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG); + #endif #endif // Stats: Reset roundNum game.roundNum = 0; diff --git a/src/action/a_team.c b/src/action/a_team.c index fb69a33ac..6f53f912b 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2577,10 +2577,12 @@ qboolean CheckTimelimit( void ) timewarning = 1; } } + #if AQTION_CURL else if ( use_warnings->value && matchmode->value ){ if( timelimit->value > 5 && level.matchTime >= ((timelimit->value - 5) * 60) && !during_countdown) lc_discord_webhook(MM_FIVE_MIN_WARN, FIVE_MIN_WARN); } + #endif } return false; @@ -3902,7 +3904,9 @@ void TallyEndOfLevelTeamScores (void) LogMatch(); // Generates end of game stats LogEndMatchStats(); // Generates end of match logs } + #if AQTION_CURL lc_discord_webhook(TP_MATCH_END_MSG, MATCH_END_MSG); + #endif #endif // Stats: Reset roundNum game.roundNum = 0; diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 08690e73e..1477aa74e 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1467,7 +1467,9 @@ void Cmd_Say_f (edict_t * ent, qboolean team, qboolean arg0, qboolean partner_ms } // Send message to Discord -- this must come before the newline add or it screws up formatting in-game + #if AQTION_CURL lc_discord_webhook(text, CHAT_MSG); + #endif Q_strncatz(text, "\n", sizeof(text)); @@ -2002,7 +2004,9 @@ static cmdList_t commandList[] = { "volunteer", Cmd_Volunteer_f, 0}, { "leader", Cmd_Volunteer_f, 0}, { "highscores", Cmd_HighScores_f, 0}, +#if AQTION_CURL { "pickup", Cmd_Pickup_f, 0}, +#endif }; #define MAX_COMMAND_HASH 64 diff --git a/src/action/g_local.h b/src/action/g_local.h index 16fcb31c8..630ec7920 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2926,8 +2926,10 @@ void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); void FireTimedMessages(void); //tng_net.c +#if AQTION_CURL void lc_shutdown_function(void); qboolean lc_init_function(void); void lc_once_per_gameframe(void); void lc_discord_webhook(char* message, Discord_Notifications msgtype); -void lc_start_request_function(request_t* request); \ No newline at end of file +void lc_start_request_function(request_t* request); +#endif diff --git a/src/action/g_main.c b/src/action/g_main.c index 7a30fd396..6f1feac0f 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -1274,10 +1274,12 @@ void G_RunFrame (void) CycleLights (); //Run pending curl requests - #ifdef USE_CURL + #if USE_CURL + #if AQTION_CURL if (sv_curl_enable->value) lc_once_per_gameframe(); #endif + #endif // // treat each object in turn @@ -1433,4 +1435,4 @@ edict_t *ChooseRandomPlayer(int teamNum, qboolean allowBot) ent = plist[j]; // Returns a random player return ent; -} \ No newline at end of file +} diff --git a/src/action/g_save.c b/src/action/g_save.c index 22a25a3ec..af8064694 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -727,9 +727,11 @@ void InitGame( void ) // Initialize libcurl capabilities if enabled #ifdef USE_CURL + #if AQTION_CURL if (sv_curl_enable->value) lc_init_function(); #endif + #endif // items InitItems(); diff --git a/src/action/p_client.c b/src/action/p_client.c index ed4186536..fecf818b6 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1175,7 +1175,9 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) self->client->pers.netname, special_message, self->client->attacker->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging + #if AQTION_CURL lc_discord_webhook(death_msg, DEATH_MSG); + #endif IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); @@ -1208,7 +1210,9 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) { sprintf( death_msg, "%s %s\n", self->client->pers.netname, message ); //Using discord webhook for death messaging - lc_discord_webhook(death_msg, DEATH_MSG);; + #if AQTION_CURL + lc_discord_webhook(death_msg, DEATH_MSG); + #endif PrintDeathMessage(death_msg, self ); IRC_printf( IRC_T_DEATH, death_msg ); @@ -1562,7 +1566,9 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) message, attacker->client->pers.netname, message2); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - lc_discord_webhook(death_msg, DEATH_MSG);; + #if AQTION_CURL + lc_discord_webhook(death_msg, DEATH_MSG); + #endif IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); @@ -1608,7 +1614,9 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s died\n", self->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - lc_discord_webhook(death_msg, DEATH_MSG);; + #if AQTION_CURL + lc_discord_webhook(death_msg, DEATH_MSG); + #endif IRC_printf(IRC_T_DEATH, death_msg); #if USE_AQTION @@ -3239,9 +3247,11 @@ void PutClientInServer(edict_t * ent) // Tell the world! #ifdef USE_CURL + #if AQTION_CURL if (sv_curl_enable->value && sv_discord_announce_enable->value) announce_server_populating(); #endif + #endif } /* diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 29558f8ed..cfa80cd26 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -138,9 +138,11 @@ void BeginIntermission(edict_t *targ) LogMatch(); LogEndMatchStats(); // Generates end of match logs } + #if AQTION_CURL if(!teamplay->value) lc_discord_webhook(DM_MATCH_END_MSG, MATCH_END_MSG); #endif + #endif // respawn any dead clients for (i = 0, ent = g_edicts + 1; i < game.maxclients; i++, ent++) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index aaa559b44..45a1e0683 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -468,13 +468,13 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) // Debug prints gi.dprintf("msgflags value: %d\n", (int)msgflags->value); gi.dprintf("msgtype value: %d\n", msgtype); - - // Check if the msgtype is within the allowed message flags - // Look at the Discord_Notifications enum in g_local.h for the flags - if (!(msgtype & (int)msgflags->value)) { + } + // Check if the msgtype is within the allowed message flags + // Look at the Discord_Notifications enum in g_local.h for the flags + if (!(msgtype & (int)msgflags->value)) { + if (curldebug) gi.dprintf("Message type not allowed by msgflags\n"); - return; - } + return; } // Don't run this if curl is disabled or the webhook URL is set to "disabled" diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index ef6da4065..83423bc7b 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -889,7 +889,11 @@ void Write_Stats(const char* msg, ...) // Send stats to API, else write to local file if (sv_curl_enable->value && sv_curl_stat_enable->value) { + #if AQTION_CURL lc_aqtion_stat_send(stat_cpy); + #else + return; + #endif } else { logfile_name = gi.cvar("logfile_name", "", CVAR_NOSET); sprintf(logpath, "action/logs/%s.stats", logfile_name->string); From f82db0e1117b2405ffe673e88d7e1be1d4b7c132 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:43:09 -0400 Subject: [PATCH 657/974] Missed one --- meson.build | 5 ----- 1 file changed, 5 deletions(-) diff --git a/meson.build b/meson.build index f569f8f6c..42868d162 100644 --- a/meson.build +++ b/meson.build @@ -457,11 +457,6 @@ if not win32 endif endif -# if not win32 -# action_src += 'src/action/tng_net.c' ## Not supporting libcurl on Windows for gamelib -# game_deps += curl -# endif - if host_machine.system() == 'darwin' # Darwin is warning-spamming because of its SDL_config defines add_project_arguments('-Wno-macro-redefined', language : 'c') From 0a8add85c6536c1ae71211798c419a6b51cded02 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:47:51 -0400 Subject: [PATCH 658/974] asjdkasjd --- src/action/g_local.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/action/g_local.h b/src/action/g_local.h index 630ec7920..8cfa259c9 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -291,7 +291,9 @@ #include "tng_jump.h" #include "g_grapple.h" #include "p_antilag.h" +#if AQTION_CURL #include "tng_net.h" +#endif #ifndef NO_BOTS //#include "acesrc/botnav.h" From fff3a7b14b18949666f1043f5445d03df71b7c37 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:52:09 -0400 Subject: [PATCH 659/974] Windows.. --- meson.build | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/meson.build b/meson.build index 42868d162..ed16f86f8 100644 --- a/meson.build +++ b/meson.build @@ -375,6 +375,17 @@ endif config.set10('USE_ZLIB', zlib.found()) config.set10('USE_PNG', png.found()) +sdl2 = dependency('sdl2', required: get_option('sdl2')) +if sdl2.found() + client_src += 'src/unix/video/sdl.c' + config.set('USE_SDL', 'USE_CLIENT') +endif + +common_deps = [zlib] +client_deps = [png, sdl2] +server_deps = [] +game_deps = [zlib] + curl = dependency('libcurl', version: '>= 7.68.0', required: get_option('libcurl'), @@ -384,19 +395,10 @@ curl = dependency('libcurl', if curl.found() client_src += 'src/client/http.c' config.set10('USE_CURL', true) + client_deps += curl + game_deps += curl endif -sdl2 = dependency('sdl2', required: get_option('sdl2')) -if sdl2.found() - client_src += 'src/unix/video/sdl.c' - config.set('USE_SDL', 'USE_CLIENT') -endif - -common_deps = [zlib] -client_deps = [png, curl, sdl2] -server_deps = [] -game_deps = [zlib] - jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ] From ae053e591f609832cee5d38ad9c293cb9bd0fc09 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 14:58:39 -0400 Subject: [PATCH 660/974] Windows..again --- meson.build | 1 - 1 file changed, 1 deletion(-) diff --git a/meson.build b/meson.build index ed16f86f8..36e0d1330 100644 --- a/meson.build +++ b/meson.build @@ -554,7 +554,6 @@ if get_option('aqtion-build') # Win32 winsock compat if win32 dll_link_args += '-lws2_32' - dll_link_args += '-lcurl' dll_link_args += '-lcrypt32' dll_link_args += '-lwsock32' endif From 1660de10cb90fb4840daa39083181b2903c90ad2 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 16:02:28 -0400 Subject: [PATCH 661/974] Removed -lcurl from aqtion-build --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 36e0d1330..b6b3a76e7 100644 --- a/meson.build +++ b/meson.build @@ -557,12 +557,12 @@ if get_option('aqtion-build') dll_link_args += '-lcrypt32' dll_link_args += '-lwsock32' endif - dll_link_args += '-lcurl' endif if get_option('aqtion-curl') config.set10('AQTION_CURL', true) action_src += 'src/action/tng_net.c' + dll_link_args += '-lcurl' game_deps += curl endif From 45a0029eed3e0adef90f7197d37b7eb107a3f5f7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 16:06:08 -0400 Subject: [PATCH 662/974] Re-added curl to client deps --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index b6b3a76e7..2e394e2e6 100644 --- a/meson.build +++ b/meson.build @@ -382,7 +382,7 @@ if sdl2.found() endif common_deps = [zlib] -client_deps = [png, sdl2] +client_deps = [png, curl, sdl2] server_deps = [] game_deps = [zlib] From cedb46ccb835fe2cd2cfe86b74ab8f41b7d20625 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 29 Aug 2024 16:08:02 -0400 Subject: [PATCH 663/974] Re-added curl to client deps --- meson.build | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/meson.build b/meson.build index 2e394e2e6..427f236dc 100644 --- a/meson.build +++ b/meson.build @@ -381,11 +381,6 @@ if sdl2.found() config.set('USE_SDL', 'USE_CLIENT') endif -common_deps = [zlib] -client_deps = [png, curl, sdl2] -server_deps = [] -game_deps = [zlib] - curl = dependency('libcurl', version: '>= 7.68.0', required: get_option('libcurl'), @@ -395,10 +390,13 @@ curl = dependency('libcurl', if curl.found() client_src += 'src/client/http.c' config.set10('USE_CURL', true) - client_deps += curl - game_deps += curl endif +common_deps = [zlib] +client_deps = [png, curl, sdl2] +server_deps = [] +game_deps = [zlib] + jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ] From d2b24f1a8b5c12f590d089336d1b0106b24c5a4d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 28 Aug 2024 23:34:40 +0300 Subject: [PATCH 664/974] Add debug line drawing game API extension. Based on code from: Jonathan --- inc/client/client.h | 8 + inc/shared/debug.h | 38 +++ meson.build | 4 + src/refresh/debug.c | 601 ++++++++++++++++++++++++++++++++++++++++++++ src/refresh/gl.h | 21 +- src/refresh/main.c | 6 + src/refresh/qgl.c | 8 + src/refresh/tess.c | 4 +- src/server/game.c | 10 + src/server/init.c | 2 + src/server/main.c | 2 + 11 files changed, 699 insertions(+), 5 deletions(-) create mode 100644 inc/shared/debug.h create mode 100644 src/refresh/debug.c diff --git a/inc/client/client.h b/inc/client/client.h index 1d4375958..13235427a 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -96,6 +96,12 @@ void Con_Print(const char *text); void Con_Printf(const char *fmt, ...) q_printf(1, 2); void Con_Close(bool force); +#if USE_DEBUG +void R_ClearDebugLines(void); +#else +#define R_ClearDebugLines() (void)0 +#endif + void SCR_BeginLoadingPlaque(void); void SCR_EndLoadingPlaque(void); @@ -150,6 +156,8 @@ float V_CalcFov(float fov_x, float width, float height); #define Con_SkipNotify(skip) (void)0 #define Con_Print(text) (void)0 +#define R_ClearDebugLines() (void)0 + #define SCR_BeginLoadingPlaque() (void)0 #define SCR_EndLoadingPlaque() (void)0 diff --git a/inc/shared/debug.h b/inc/shared/debug.h new file mode 100644 index 000000000..c1fa423b0 --- /dev/null +++ b/inc/shared/debug.h @@ -0,0 +1,38 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#define DEBUG_DRAW_API_V1 "DEBUG_DRAW_API_V1" + +typedef struct { + void (*ClearDebugLines)(void); + void (*AddDebugLine)(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugPoint)(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugAxis)(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test); + void (*AddDebugBounds)(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugSphere)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugCircle)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugCylinder)(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugArrow)(const vec3_t start, const vec3_t end, float size, uint32_t line_color, + uint32_t arrow_color, uint32_t time, qboolean depth_test); + void (*AddDebugCurveArrow)(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, + uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test); + void (*AddDebugText)(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test); +} debug_draw_api_v1_t; diff --git a/meson.build b/meson.build index 96e503f0c..74d33ffbb 100644 --- a/meson.build +++ b/meson.build @@ -417,6 +417,10 @@ if get_option('tests') config.set10('USE_TESTS', true) endif +if get_option('debug') + refresh_src += 'src/refresh/debug.c' +endif + if get_option('game-abi-hack').require(x86 and cc.get_id() == 'gcc' and cc.has_argument('-mstackrealign')).allowed() config.set10('USE_GAME_ABI_HACK', true) engine_args += '-mstackrealign' diff --git a/src/refresh/debug.c b/src/refresh/debug.c new file mode 100644 index 000000000..8da73c887 --- /dev/null +++ b/src/refresh/debug.c @@ -0,0 +1,601 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "gl.h" +#include "shared/list.h" +#include "shared/debug.h" +#include + +#define MAX_DEBUG_LINES TESS_MAX_VERTICES +#define MAX_DEBUG_TEXTS 1024 + +typedef struct { + list_t entry; + vec3_t start, end; + uint32_t color; + uint32_t time; + glStateBits_t bits; +} debug_line_t; + +static debug_line_t debug_lines[MAX_DEBUG_LINES]; +static list_t debug_lines_free; +static list_t debug_lines_active; + +typedef struct { + list_t entry; + vec3_t origin, angles; + float size; + uint32_t color; + uint32_t time; + glStateBits_t bits; + char text[MAX_QPATH]; +} debug_text_t; + +static debug_text_t debug_texts[MAX_DEBUG_TEXTS]; +static list_t debug_texts_free; +static list_t debug_texts_active; + +static cvar_t *gl_debug_linewidth; +static cvar_t *gl_debug_distfrac; + +void R_ClearDebugLines(void) +{ + List_Init(&debug_lines_free); + List_Init(&debug_lines_active); + + List_Init(&debug_texts_free); + List_Init(&debug_texts_active); +} + +static void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test) +{ + debug_line_t *l = LIST_FIRST(debug_line_t, &debug_lines_free, entry); + + if (LIST_EMPTY(&debug_lines_free)) { + if (LIST_EMPTY(&debug_lines_active)) { + for (int i = 0; i < MAX_DEBUG_LINES; i++) + List_Append(&debug_lines_free, &debug_lines[i].entry); + } else { + debug_line_t *next; + LIST_FOR_EACH_SAFE(debug_line_t, l, next, &debug_lines_active, entry) { + if (l->time <= com_localTime) { + List_Remove(&l->entry); + List_Insert(&debug_lines_free, &l->entry); + } + } + } + + if (LIST_EMPTY(&debug_lines_free)) + l = LIST_FIRST(debug_line_t, &debug_lines_active, entry); + else + l = LIST_FIRST(debug_line_t, &debug_lines_free, entry); + } + + // unlink from freelist + List_Remove(&l->entry); + List_Append(&debug_lines_active, &l->entry); + + VectorCopy(start, l->start); + VectorCopy(end, l->end); + l->color = color; + l->time = com_localTime + time; + if (l->time < com_localTime) + l->time = UINT32_MAX; + l->bits = GLS_DEPTHMASK_FALSE; + if (!depth_test) + l->bits |= GLS_DEPTHTEST_DISABLE; + if (gl_config.caps & QGL_CAP_LINE_SMOOTH) + l->bits |= GLS_BLEND_BLEND; +} + +#define GL_DRAWLINE(sx, sy, sz, ex, ey, ez) \ + R_AddDebugLine((const vec3_t) { (sx), (sy), (sz) }, (const vec3_t) { (ex), (ey), (ez) }, color, time, depth_test) + +#define GL_DRAWLINEV(s, e) \ + R_AddDebugLine(s, e, color, time, depth_test) + +static void R_AddDebugPoint(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test) +{ + size *= 0.5f; + GL_DRAWLINE(point[0] - size, point[1], point[2], point[0] + size, point[1], point[2]); + GL_DRAWLINE(point[0], point[1] - size, point[2], point[0], point[1] + size, point[2]); + GL_DRAWLINE(point[0], point[1], point[2] - size, point[0], point[1], point[2] + size); +} + +static void R_AddDebugAxis(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test) +{ + vec3_t axis[3], end; + uint32_t color; + + if (angles) { + AnglesToAxis(angles, axis); + } else { + VectorSet(axis[0], 1, 0, 0); + VectorSet(axis[1], 0, 1, 0); + VectorSet(axis[2], 0, 0, 1); + } + + color = U32_RED; + VectorMA(origin, size, axis[0], end); + GL_DRAWLINEV(origin, end); + + color = U32_GREEN; + VectorMA(origin, size, axis[1], end); + GL_DRAWLINEV(origin, end); + + color = U32_BLUE; + VectorMA(origin, size, axis[2], end); + GL_DRAWLINEV(origin, end); +} + +static void R_AddDebugBounds(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test) +{ + for (int i = 0; i < 4; i++) { + // draw column + float x = ((i > 1) ? mins : maxs)[0]; + float y = ((((i + 1) % 4) > 1) ? mins : maxs)[1]; + GL_DRAWLINE(x, y, mins[2], x, y, maxs[2]); + + // draw bottom & top + int n = (i + 1) % 4; + float x2 = ((n > 1) ? mins : maxs)[0]; + float y2 = ((((n + 1) % 4) > 1) ? mins : maxs)[1]; + GL_DRAWLINE(x, y, mins[2], x2, y2, mins[2]); + GL_DRAWLINE(x, y, maxs[2], x2, y2, maxs[2]); + } +} + +// https://danielsieger.com/blog/2021/03/27/generating-spheres.html +static void R_AddDebugSphere(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) +{ + vec3_t verts[160]; + const int n_stacks = min(4 + radius / 32, 10); + const int n_slices = min(6 + radius / 32, 16); + const int v0 = 0; + int v1 = 1; + + for (int i = 0; i < n_stacks - 1; i++) { + float phi = M_PIf * (i + 1) / n_stacks; + for (int j = 0; j < n_slices; j++) { + float theta = 2 * M_PIf * j / n_slices; + vec3_t v = { + sinf(phi) * cosf(theta), + sinf(phi) * sinf(theta), + cosf(phi) + }; + VectorMA(origin, radius, v, verts[v1]); + v1++; + } + } + + VectorCopy(origin, verts[v0]); + VectorCopy(origin, verts[v1]); + + verts[v0][2] += radius; + verts[v1][2] -= radius; + + for (int i = 0; i < n_slices; i++) { + int i0 = i + 1; + int i1 = (i + 1) % n_slices + 1; + GL_DRAWLINEV(verts[v0], verts[i1]); + GL_DRAWLINEV(verts[i1], verts[i0]); + GL_DRAWLINEV(verts[i0], verts[v0]); + i0 = i + n_slices * (n_stacks - 2) + 1; + i1 = (i + 1) % n_slices + n_slices * (n_stacks - 2) + 1; + GL_DRAWLINEV(verts[v1], verts[i0]); + GL_DRAWLINEV(verts[i0], verts[i1]); + GL_DRAWLINEV(verts[i1], verts[v1]); + } + + for (int j = 0; j < n_stacks - 2; j++) { + int j0 = j * n_slices + 1; + int j1 = (j + 1) * n_slices + 1; + for (int i = 0; i < n_slices; i++) { + int i0 = j0 + i; + int i1 = j0 + (i + 1) % n_slices; + int i2 = j1 + (i + 1) % n_slices; + int i3 = j1 + i; + GL_DRAWLINEV(verts[i0], verts[i1]); + GL_DRAWLINEV(verts[i1], verts[i2]); + GL_DRAWLINEV(verts[i2], verts[i3]); + GL_DRAWLINEV(verts[i3], verts[i0]); + } + } +} + +static void R_AddDebugCircle(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) +{ + int vert_count = min(5 + radius / 8, 16); + float rads = (2 * M_PIf) / vert_count; + + for (int i = 0; i < vert_count; i++) { + float a = i * rads; + float c = cosf(a); + float s = sinf(a); + float x = c * radius + origin[0]; + float y = s * radius + origin[1]; + + a = ((i + 1) % vert_count) * rads; + c = cosf(a); + s = sinf(a); + float x2 = c * radius + origin[0]; + float y2 = s * radius + origin[1]; + + GL_DRAWLINE(x, y, origin[2], x2, y2, origin[2]); + } +} + +static void R_AddDebugCylinder(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, qboolean depth_test) +{ + int vert_count = min(5 + radius / 8, 16); + float rads = (2 * M_PIf) / vert_count; + + for (int i = 0; i < vert_count; i++) { + float a = i * rads; + float c = cosf(a); + float s = sinf(a); + float x = c * radius + origin[0]; + float y = s * radius + origin[1]; + + a = ((i + 1) % vert_count) * rads; + c = cosf(a); + s = sinf(a); + float x2 = c * radius + origin[0]; + float y2 = s * radius + origin[1]; + + GL_DRAWLINE(x, y, origin[2] - half_height, x2, y2, origin[2] - half_height); + GL_DRAWLINE(x, y, origin[2] + half_height, x2, y2, origin[2] + half_height); + GL_DRAWLINE(x, y, origin[2] - half_height, x, y, origin[2] + half_height); + } +} + +static void R_DrawArrowCap(const vec3_t apex, const vec3_t dir, float size, + uint32_t color, uint32_t time, qboolean depth_test) +{ + vec3_t cap_end; + VectorMA(apex, size, dir, cap_end); + R_AddDebugLine(apex, cap_end, color, time, depth_test); + + vec3_t right, up; + MakeNormalVectors(dir, right, up); + + vec3_t l; + VectorMA(apex, size, right, l); + R_AddDebugLine(l, cap_end, color, time, depth_test); + + VectorMA(apex, -size, right, l); + R_AddDebugLine(l, cap_end, color, time, depth_test); +} + +static void R_AddDebugArrow(const vec3_t start, const vec3_t end, float size, uint32_t line_color, + uint32_t arrow_color, uint32_t time, qboolean depth_test) +{ + vec3_t dir; + VectorSubtract(end, start, dir); + float len = VectorNormalize(dir); + + if (len > size) { + vec3_t line_end; + VectorMA(start, len - size, dir, line_end); + R_AddDebugLine(start, line_end, line_color, time, depth_test); + R_DrawArrowCap(line_end, dir, size, arrow_color, time, depth_test); + } else { + R_DrawArrowCap(end, dir, len, arrow_color, time, depth_test); + } +} + +static void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, + uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test) +{ + int num_points = Q_clip(Distance(start, end) / 32, 3, 24); + vec3_t last_point; + + for (int i = 0; i <= num_points; i++) { + float t = i / (float)num_points; + float it = 1.0f - t; + + float a = it * it; + float b = 2.0f * t * it; + float c = t * t; + + vec3_t p = { + a * start[0] + b * ctrl[0] + c * end[0], + a * start[1] + b * ctrl[1] + c * end[1], + a * start[2] + b * ctrl[2] + c * end[2] + }; + + if (i == num_points) + R_AddDebugArrow(last_point, p, size, line_color, arrow_color, time, depth_test); + else if (i) + R_AddDebugLine(last_point, p, line_color, time, depth_test); + + VectorCopy(p, last_point); + } +} + +static void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test) +{ + debug_text_t *t = LIST_FIRST(debug_text_t, &debug_texts_free, entry); + + if (LIST_EMPTY(&debug_texts_free)) { + if (LIST_EMPTY(&debug_texts_active)) { + for (int i = 0; i < MAX_DEBUG_TEXTS; i++) + List_Append(&debug_texts_free, &debug_texts[i].entry); + } else { + debug_text_t *next; + LIST_FOR_EACH_SAFE(debug_text_t, t, next, &debug_texts_active, entry) { + if (t->time <= com_localTime) { + List_Remove(&t->entry); + List_Insert(&debug_texts_free, &t->entry); + } + } + } + + if (LIST_EMPTY(&debug_texts_free)) + t = LIST_FIRST(debug_text_t, &debug_texts_active, entry); + else + t = LIST_FIRST(debug_text_t, &debug_texts_free, entry); + } + + // unlink from freelist + List_Remove(&t->entry); + List_Append(&debug_texts_active, &t->entry); + + VectorCopy(origin, t->origin); + if (angles) + VectorCopy(angles, t->angles); + t->size = size; + t->color = color; + t->time = com_localTime + time; + if (t->time < com_localTime) + t->time = UINT32_MAX; + t->bits = GLS_DEPTHMASK_FALSE | GLS_BLEND_BLEND; + if (!depth_test) + t->bits |= GLS_DEPTHTEST_DISABLE; + if (angles) + t->bits |= GLS_CULL_DISABLE; + Q_strlcpy(t->text, text, sizeof(t->text)); +} + +static void GL_DrawDebugLines(void) +{ + glStateBits_t bits = -1; + debug_line_t *l, *next; + GLfloat *dst_vert; + int numverts; + + if (LIST_EMPTY(&debug_lines_active)) + return; + + GL_LoadMatrix(glr.viewmatrix); + GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); + GL_BindArrays(VA_NULLMODEL); + GL_ArrayBits(GLA_VERTEX | GLA_COLOR); + + if (qglLineWidth) + qglLineWidth(gl_debug_linewidth->value); + + if (gl_config.caps & QGL_CAP_LINE_SMOOTH) + qglEnable(GL_LINE_SMOOTH); + + static_assert(q_countof(debug_lines) <= q_countof(tess.vertices) / 8, "Too many debug lines"); + + dst_vert = tess.vertices; + numverts = 0; + LIST_FOR_EACH_SAFE(debug_line_t, l, next, &debug_lines_active, entry) { + if (l->time < com_localTime) { // expired + List_Remove(&l->entry); + List_Insert(&debug_lines_free, &l->entry); + continue; + } + + if (bits != l->bits) { + if (numverts) { + GL_LockArrays(numverts); + qglDrawArrays(GL_LINES, 0, numverts); + GL_UnlockArrays(); + } + + GL_StateBits(l->bits); + bits = l->bits; + + dst_vert = tess.vertices; + numverts = 0; + } + + VectorCopy(l->start, dst_vert); + VectorCopy(l->end, dst_vert + 4); + WN32(dst_vert + 3, l->color); + WN32(dst_vert + 7, l->color); + dst_vert += 8; + + numverts += 2; + } + + if (numverts) { + GL_LockArrays(numverts); + qglDrawArrays(GL_LINES, 0, numverts); + GL_UnlockArrays(); + numverts = 0; + } + + if (gl_config.caps & QGL_CAP_LINE_SMOOTH) + qglDisable(GL_LINE_SMOOTH); + + if (qglLineWidth) + qglLineWidth(1.0f); +} + +static void GL_FlushDebugChars(void) +{ + if (!tess.numindices) + return; + + GL_BindTexture(TMU_TEXTURE, IMG_ForHandle(r_charset)->texnum); + GL_StateBits(tess.flags); + GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + GL_DrawIndexed(SHOWTRIS_NONE); + + tess.numverts = tess.numindices = 0; + tess.flags = 0; +} + +static void GL_DrawDebugChar(const vec3_t pos, const vec3_t right, const vec3_t down, + glStateBits_t bits, uint32_t color, int c) +{ + GLfloat *dst_vert; + glIndex_t *dst_indices; + float s, t; + + if ((c & 127) == 32) + return; + + if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || + tess.numindices + 6 > TESS_MAX_INDICES) || + (tess.numindices && bits != tess.flags)) + GL_FlushDebugChars(); + + dst_vert = tess.vertices + tess.numverts * 6; + + VectorCopy(pos, dst_vert); + VectorAdd(dst_vert, right, dst_vert + 6); + VectorAdd(dst_vert + 6, down, dst_vert + 12); + VectorAdd(dst_vert, down, dst_vert + 18); + + s = (c & 15) * 0.0625f; + t = (c >> 4) * 0.0625f; + + dst_vert[ 3] = s; + dst_vert[ 4] = t; + dst_vert[ 9] = s + 0.0625f; + dst_vert[10] = t; + dst_vert[15] = s + 0.0625f; + dst_vert[16] = t + 0.0625f; + dst_vert[21] = s; + dst_vert[22] = t + 0.0625f; + + WN32(dst_vert + 5, color); + WN32(dst_vert + 11, color); + WN32(dst_vert + 17, color); + WN32(dst_vert + 23, color); + + dst_indices = tess.indices + tess.numindices; + dst_indices[0] = tess.numverts + 0; + dst_indices[1] = tess.numverts + 2; + dst_indices[2] = tess.numverts + 3; + dst_indices[3] = tess.numverts + 0; + dst_indices[4] = tess.numverts + 1; + dst_indices[5] = tess.numverts + 2; + + tess.numverts += 4; + tess.numindices += 6; + tess.flags = bits; +} + +static void GL_DrawDebugTexts(void) +{ + debug_text_t *text, *next; + + if (LIST_EMPTY(&debug_texts_active)) + return; + + GL_LoadMatrix(glr.viewmatrix); + GL_BindArrays(VA_EFFECT); + + LIST_FOR_EACH_SAFE(debug_text_t, text, next, &debug_texts_active, entry) { + vec3_t right, down, pos; + const char *s; + float radius; + int i; + + if (text->time < com_localTime) { // expired + List_Remove(&text->entry); + List_Insert(&debug_lines_free, &text->entry); + continue; + } + + // distance cull + VectorSubtract(text->origin, glr.fd.vieworg, pos); + if (text->size < DotProduct(pos, glr.viewaxis[0]) * gl_debug_distfrac->value) + continue; + + // frustum cull + radius = strlen(text->text) * text->size; + for (i = 0; i < 4; i++) + if (PlaneDiff(text->origin, &glr.frustumPlanes[i]) < -radius) + break; + if (i != 4) + continue; + + if (text->bits & GLS_CULL_DISABLE) { // oriented + vec3_t up; + AngleVectors(text->angles, NULL, right, up); + VectorScale(right, text->size, right); + VectorScale(up, -text->size, down); + } else { + VectorScale(glr.viewaxis[1], -text->size, right); + VectorScale(glr.viewaxis[2], -text->size, down); + } + VectorCopy(text->origin, pos); + + s = text->text; + while (*s) { + byte c = *s++; + GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); + VectorAdd(pos, right, pos); + } + } + + GL_FlushDebugChars(); +} + +void GL_DrawDebugObjects(void) +{ + GL_DrawDebugLines(); + GL_DrawDebugTexts(); +} + +void GL_InitDebugDraw(void) +{ + R_ClearDebugLines(); + + gl_debug_linewidth = Cvar_Get("gl_debug_linewidth", "2", 0); + gl_debug_distfrac = Cvar_Get("gl_debug_distfrac", "0.004", 0); + + Cmd_AddCommand("cleardebuglines", R_ClearDebugLines); +} + +void GL_ShutdownDebugDraw(void) +{ + Cmd_RemoveCommand("cleardebuglines"); +} + +const debug_draw_api_v1_t debug_draw_api_v1 = { + .ClearDebugLines = R_ClearDebugLines, + .AddDebugLine = R_AddDebugLine, + .AddDebugPoint = R_AddDebugPoint, + .AddDebugAxis = R_AddDebugAxis, + .AddDebugBounds = R_AddDebugBounds, + .AddDebugSphere = R_AddDebugSphere, + .AddDebugCircle = R_AddDebugCircle, + .AddDebugCylinder = R_AddDebugCylinder, + .AddDebugArrow = R_AddDebugArrow, + .AddDebugCurveArrow = R_AddDebugCurveArrow, + .AddDebugText = R_AddDebugText, +}; diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 0178ea7fc..e790aa3eb 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -131,6 +131,7 @@ typedef enum { QGL_CAP_ELEMENT_INDEX_UINT = BIT(9), QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(10), QGL_CAP_CLIENT_VA = BIT(11), + QGL_CAP_LINE_SMOOTH = BIT(12), } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) @@ -656,6 +657,7 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_TYPE, indices) typedef enum { + SHOWTRIS_NONE = 0, SHOWTRIS_WORLD = BIT(0), SHOWTRIS_MESH = BIT(1), SHOWTRIS_PIC = BIT(2), @@ -746,10 +748,11 @@ void GL_DrawBeams(void); void GL_DrawFlares(void); void GL_BindArrays(glVertexArray_t va); -void GL_InitArrays(void); -void GL_ShutdownArrays(void); void GL_LockArrays(GLsizei count); void GL_UnlockArrays(void); +void GL_DrawIndexed(showtris_t showtris); +void GL_InitArrays(void); +void GL_ShutdownArrays(void); void GL_Flush3D(void); @@ -790,3 +793,17 @@ void GL_DrawAliasModel(const model_t *model); void HQ2x_Render(uint32_t *output, const uint32_t *input, int width, int height); void HQ4x_Render(uint32_t *output, const uint32_t *input, int width, int height); void HQ2x_Init(void); + +/* + * debug.c + * + */ +#if USE_DEBUG +void GL_InitDebugDraw(void); +void GL_ShutdownDebugDraw(void); +void GL_DrawDebugObjects(void); +#else +#define GL_InitDebugDraw() (void)0 +#define GL_ShutdownDebugDraw() (void)0 +#define GL_DrawDebugObjects() (void)0 +#endif diff --git a/src/refresh/main.c b/src/refresh/main.c index 15d3fd521..cb4a7eee9 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -704,6 +704,8 @@ void R_RenderFrame(const refdef_t *fd) GL_DrawEntities(RF_TRANSLUCENT | RF_WEAPONMODEL, 0); + GL_DrawDebugObjects(); + if (waterwarp) qglBindFramebuffer(GL_FRAMEBUFFER, 0); @@ -1103,6 +1105,8 @@ bool R_Init(bool total) GL_InitTables(); + GL_InitDebugDraw(); + GL_PostInit(); GL_ShowErrors(__func__); @@ -1136,6 +1140,8 @@ void R_Shutdown(bool total) if (!total) return; + GL_ShutdownDebugDraw(); + GL_ShutdownState(); GL_ShutdownArrays(); diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 55ea09763..4c14938b8 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -143,6 +143,14 @@ static const glsection_t sections[] = { .caps = QGL_CAP_ELEMENT_INDEX_UINT, }, + // GL 1.1, ES 1.0 up to 2.0 + { + .ver_gl = QGL_VER(1, 1), + .ver_es = QGL_VER(1, 0), + .excl_es = QGL_VER(2, 0), + .caps = QGL_CAP_LINE_SMOOTH, + }, + // ES 1.1 { .ver_es = QGL_VER(1, 1), diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 7889b231d..0e43ed409 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -28,8 +28,6 @@ static mface_t *faces_head[FACE_HASH_SIZE]; static mface_t **faces_next[FACE_HASH_SIZE]; static mface_t *faces_alpha; -static void GL_DrawIndexed(showtris_t showtris); - void GL_Flush2D(void) { glStateBits_t bits; @@ -558,7 +556,7 @@ void GL_UnlockArrays(void) qglUnlockArraysEXT(); } -static void GL_DrawIndexed(showtris_t showtris) +void GL_DrawIndexed(showtris_t showtris) { const glIndex_t *indices = tess.indices; diff --git a/src/server/game.c b/src/server/game.c index 1fb493f7b..07f50ae21 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -18,6 +18,7 @@ with this program; if not, write to the Free Software Foundation, Inc., // sv_game.c -- interface to the game dll #include "server.h" +#include "shared/debug.h" const game_export_t *ge; const game_export_ex_t *gex; @@ -863,8 +864,17 @@ static void *PF_GetExtension(const char *name) { if (!name) return NULL; + if (!strcmp(name, "FILESYSTEM_API_V1")) return (void *)&filesystem_api_v1; + +#if USE_REF && USE_DEBUG + if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer) { + extern const debug_draw_api_v1_t debug_draw_api_v1; + return (void *)&debug_draw_api_v1; + } +#endif + return NULL; } diff --git a/src/server/init.c b/src/server/init.c index 0eacf8b4f..b74dc92f9 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -108,6 +108,7 @@ void SV_SpawnServer(const mapcmd_t *cmd) client_t *client; SCR_BeginLoadingPlaque(); // for local system + R_ClearDebugLines(); Com_Printf("------- Server Initialization -------\n"); Com_Printf("SpawnServer: %s\n", cmd->server); @@ -384,6 +385,7 @@ void SV_InitGame(unsigned mvd_spawn) // make sure the client is down CL_Disconnect(ERR_RECONNECT); SCR_BeginLoadingPlaque(); + R_ClearDebugLines(); CM_FreeMap(&sv.cm); memset(&sv, 0, sizeof(sv)); diff --git a/src/server/main.c b/src/server/main.c index c10407b5f..8d3c8cdd7 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2372,6 +2372,8 @@ void SV_Shutdown(const char *finalmsg, error_type_t type) if (!sv_registered) return; + R_ClearDebugLines(); // for local system + #if USE_MVD_CLIENT if (ge != &mvd_ge && !(type & MVD_SPAWN_INTERNAL)) { // shutdown MVD client now if not already running the built-in MVD game module From 1691bfdf4013f8a1449e37fc79a04a77015c15c1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 29 Aug 2024 00:54:40 +0300 Subject: [PATCH 665/974] Add define for filesystem API extension string. --- inc/shared/files.h | 2 ++ src/server/game.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/inc/shared/files.h b/inc/shared/files.h index c2bdc3423..fa76aeef7 100644 --- a/inc/shared/files.h +++ b/inc/shared/files.h @@ -68,6 +68,8 @@ typedef struct { #define FS_FLAG_LOADFILE 0x00001000 // open non-unique handle, must be closed very quickly #define FS_FLAG_MASK 0x0000ff00 +#define FILESYSTEM_API_V1 "FILESYSTEM_API_V1" + typedef struct { int64_t (*OpenFile)(const char *path, qhandle_t *f, unsigned mode); // returns file length int (*CloseFile)(qhandle_t f); diff --git a/src/server/game.c b/src/server/game.c index 07f50ae21..a027fda52 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -865,7 +865,7 @@ static void *PF_GetExtension(const char *name) if (!name) return NULL; - if (!strcmp(name, "FILESYSTEM_API_V1")) + if (!strcmp(name, FILESYSTEM_API_V1)) return (void *)&filesystem_api_v1; #if USE_REF && USE_DEBUG From 86611919036784a7ccc3596f2056b2acf1e519f6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 12:02:20 -0400 Subject: [PATCH 666/974] Added match start messaging --- src/action/a_team.c | 4 ++ src/action/g_local.h | 11 +-- src/action/p_client.c | 5 ++ src/action/tng_net.c | 162 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 173 insertions(+), 9 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 6f53f912b..c22eb5aa8 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2995,6 +2995,10 @@ int CheckTeamRules (void) } else { sprintf( buf, "The round will begin in %d seconds!", warmup_length ); } + #if AQTION_CURL + lc_discord_webhook(buf, MATCH_START_MSG); + #endif + CenterPrintAll( buf ); team_round_countdown = warmup_length * 10 + 2; diff --git a/src/action/g_local.h b/src/action/g_local.h index 8cfa259c9..106f4ba3e 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -433,10 +433,12 @@ typedef enum { SERVER_WARMING_UP = BIT(1), // 2 DEATH_MSG = BIT(2), // 4 CHAT_MSG = BIT(3), // 8 - SERVER_MSG = BIT(4), // 16 - MATCH_END_MSG = BIT(5), // 32 - FIVE_MIN_WARN = BIT(6), // 64 - NOTIFY_MAX = BIT(7) // 128 + AWARD_MSG = BIT(4), // 16 + SERVER_MSG = BIT(5), // 32 + MATCH_START_MSG = BIT(6), // 64 + MATCH_END_MSG = BIT(7), // 128 + FIVE_MIN_WARN = BIT(8), // 256 + NOTIFY_MAX = BIT(9) // 512 (enable all) } Discord_Notifications; // Default messages @@ -444,6 +446,7 @@ typedef enum { #define TP_MATCH_END_MSG "Teamplay Results" #define DM_MATCH_END_MSG "Deathmatch Results" #define MM_FIVE_MIN_WARN "5 minutes remaining in the map" +#define TP_MATCH_START_MSG "Match is about to begin!" //deadflag #define DEAD_NO 0 diff --git a/src/action/p_client.c b/src/action/p_client.c index fecf818b6..f9b2ffc19 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -389,6 +389,11 @@ void Announce_Reward(edict_t *ent, int rewardType) { return; // Something didn't jive here? } ent->client->resp.awardstats[rewardType]++; + + #if AQTION_CURL + lc_discord_webhook(buf, AWARD_MSG); + #endif + CenterPrintAll(buf); gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 45a1e0683..fdea77b5c 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -8,7 +8,7 @@ Special thanks to Phatman for a bulk of the core code #include "g_local.h" #include -qboolean curldebug = false; +qboolean curldebug = true; // You will need one of these for each of the requests ... // ... if you allow concurrent requests to be sent at the same time @@ -296,6 +296,61 @@ char* TeamConstructPlayerScoreString(int team) { return result; } +char* TeamConstructPlayerList(int team) { + gclient_t **filtered = (gclient_t **)malloc(game.maxclients * sizeof(gclient_t *)); + if (!filtered) { + return NULL; // Handle memory allocation failure + } + + int total = FilterPlayersByTeam(filtered, team); + + // Calculate the required buffer size + int buffer_size = 0; + for (int i = 0; i < total; i++) { + buffer_size += snprintf(NULL, 0, "%s\n", filtered[i]->pers.netname); + } + + // Allocate the buffer + char *result = (char *)malloc(buffer_size + 1); + if (!result) { + free(filtered); + return NULL; // Handle memory allocation failure + } + + // Construct the string + char *ptr = result; + for (int i = 0; i < total; i++) { + ptr += sprintf(ptr, "%s\n", filtered[i]->pers.netname); + } + + free(filtered); + return result; +} + +static char* ConstructGameSettingsString(void) { + // Calculate the required buffer size + int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nuse_xerp %s\n", + tgren->string, + timelimit->string, + use_xerp->string + ); + + // Allocate the buffer + char *result = (char *)malloc(buffer_size + 1); + if (!result) { + return NULL; // Handle memory allocation failure + } + + // Construct the string + sprintf(result, "```tgren %s\ntimelimit %s\nuse_xerp %s\n```", + tgren->string, + timelimit->string, + use_xerp->string + ); + + return result; +} + static char *discord_MatchEndMsg(void) { // Create the root object @@ -382,6 +437,8 @@ static char *discord_MatchEndMsg(void) json_object_set_new(field, "value", json_string(discord_formatted_team_player_scores)); json_object_set_new(field, "inline", json_false()); json_array_append_new(fields, field); + + free(team_player_scores); // Free the allocated memory for team_player_scores } } else { // Deathmatch char *player_scores = DMConstructPlayerScoreString(); @@ -413,6 +470,8 @@ static char *discord_MatchEndMsg(void) json_object_set_new(field, "value", json_string(discord_formatted_scores)); json_object_set_new(field, "inline", json_true()); // Inline true here because it's only one list json_array_append_new(fields, field); + + free(player_scores); // Free the allocated memory for player_scores } // TODO: Testing out player award stats printing, broken for now @@ -455,6 +514,88 @@ static char *discord_MatchEndMsg(void) return json_payload; } +static char *discord_MatchStartMsg(void) +{ + // Create the root object + json_t *root = json_object(); + + json_object_set_new(root, "content", json_string("")); + + // Create the "embeds" array + json_t *embeds = json_array(); + json_object_set_new(root, "embeds", embeds); + + // Create the embed object + json_t *embed = json_object(); + json_array_append_new(embeds, embed); + + // Adjust description and color based on mode + if (teamplay->value) { // Green + json_object_set_new(embed, "description", json_string(TP_MATCH_START_MSG)); + json_object_set_new(embed, "color", json_integer(65280)); + } + + // Add fields to the embed object + json_object_set_new(embed, "title", json_string(level.mapname)); + + // Create the "thumbnail" object + json_t *thumbnail = json_object(); + json_object_set_new(embed, "thumbnail", thumbnail); + + char mapimgurl[512]; + snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); + json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + + // Add "username" field to the root object + json_object_set_new(root, "username", json_string(hostname->string)); + + // Create the "author" object (hostname) + json_t *author = json_object(); + json_object_set_new(author, "name", json_string(hostname->string)); + json_object_set_new(embed, "author", author); + + // Create the "fields" array + json_t *fields = json_array(); + json_object_set_new(embed, "fields", fields); + + // Create field objects and add them to the "fields" array + for (int team = TEAM1; team <= teamCount; team++) { + char *team_name = TeamName(team); + char field_content[64]; + snprintf(field_content, sizeof(field_content), "%s", team_name); + char *team_players = TeamConstructPlayerList(team); + + // Add triple backticks for Discord formatting + char discord_formatted_players[1280]; + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```%s```", team_players); + + + json_t *field = json_object(); + json_object_set_new(field, "name", json_string(field_content)); + json_object_set_new(field, "value", json_string(discord_formatted_players)); + json_object_set_new(field, "inline", json_true()); + json_array_append_new(fields, field); + + free(team_players); // Free the allocated memory for team_players + } + + // Game Settings + json_t *gamesettings = json_object(); + json_object_set_new(gamesettings, "name", json_string("Game Settings")); + json_object_set_new(gamesettings, "value", json_string(ConstructGameSettingsString())); + json_object_set_new(gamesettings, "inline", json_true()); + json_array_append_new(fields, gamesettings); + + // Convert the JSON object to a string + char *json_payload = json_dumps(root, JSON_INDENT(4)); + + gi.dprintf("%s\n", json_payload); + // Decrement the reference count of the root object to free memory + json_decref(root); + + return json_payload; +} + /* Call this with a string containing the message you want to send to the webhook. Limited to 2048 chars. @@ -505,19 +646,30 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) } // Format the message as a JSON payload based on msgtype - if (msgtype == CHAT_MSG || msgtype == DEATH_MSG || msgtype == SERVER_MSG) { + if (msgtype == CHAT_MSG || msgtype == DEATH_MSG || msgtype == AWARD_MSG || msgtype == SERVER_MSG) { snprintf(json_payload, sizeof(json_payload), "{\"content\":\"```%s```\"}", message); - } else if (msgtype == SERVER_MSG || msgtype == FIVE_MIN_WARN) { + } else if (msgtype == FIVE_MIN_WARN) { snprintf(json_payload, sizeof(json_payload), "{\"content\":\"%s\"}", message); + } else if (msgtype == MATCH_START_MSG) { + char *matchstartmsg = discord_MatchStartMsg(); + if (matchstartmsg) { + // Print the JSON payload + if (curldebug) + gi.dprintf("%s\n", matchstartmsg); + snprintf(json_payload, sizeof(json_payload), "%s", matchstartmsg); + + free(matchstartmsg); + } } else if (msgtype == MATCH_END_MSG){ char *matchendmsg = discord_MatchEndMsg(); - if (matchendmsg) { + if (matchendmsg) { // Print the JSON payload - gi.dprintf("%s\n", matchendmsg); + if (curldebug) + gi.dprintf("%s\n", matchendmsg); snprintf(json_payload, sizeof(json_payload), "%s", matchendmsg); free(matchendmsg); From a3ba24dd4cd865afff8db395fdcedce10f771ddd Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 12:05:10 -0400 Subject: [PATCH 667/974] Added dmflags to gamesettings string --- src/action/tng_net.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index fdea77b5c..df4c3c28f 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -329,10 +329,11 @@ char* TeamConstructPlayerList(int team) { static char* ConstructGameSettingsString(void) { // Calculate the required buffer size - int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nuse_xerp %s\n", + int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nuse_xerp %s\ndmflags %s\n", tgren->string, timelimit->string, - use_xerp->string + use_xerp->string, + dmflags->string ); // Allocate the buffer @@ -342,10 +343,11 @@ static char* ConstructGameSettingsString(void) { } // Construct the string - sprintf(result, "```tgren %s\ntimelimit %s\nuse_xerp %s\n```", + sprintf(result, "```tgren %s\ntimelimit %s\nuse_xerp %s\ndmflags %s\n```", tgren->string, timelimit->string, - use_xerp->string + use_xerp->string, + dmflags->string ); return result; From 3756694e8381b6f69158f942117a334b46a653a9 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 13:15:35 -0400 Subject: [PATCH 668/974] Cleaned up old attract mode stuff, 'am' now tells if server has bots or not --- src/action/a_team.c | 12 +++++------- src/action/botlib/botlib_spawn.c | 6 ++++++ src/action/g_save.c | 7 ------- src/action/p_client.c | 13 +------------ src/action/tng_net.c | 30 ++++++++++++++++++------------ src/client/ui/servers.c | 23 +++-------------------- 6 files changed, 33 insertions(+), 58 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index c22eb5aa8..c18adcd66 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2346,9 +2346,7 @@ void RunWarmup (void) } #if USE_AQTION if (warmup_bots->value){ - gi.cvar_forceset("am", "1"); - gi.cvar_forceset("am_botcount", warmup_bots->string); - //attract_mode_bot_check(); + bot_connections.desire_bots = warmup_bots->value; } #endif } @@ -2881,9 +2879,8 @@ int CheckTeamRules (void) // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ gi.cvar_forceset("am", "0"); - gi.cvar_forceset("am_botcount", "0"); - //attract_mode_bot_check(); - ACESP_RemoveBot("all"); + bot_connections.desire_bots = 0; + //ACESP_RemoveBot("all"); CenterPrintAll("All bots removed, good luck and have fun!"); //Re-enable stats now that the bots are gone @@ -2996,7 +2993,8 @@ int CheckTeamRules (void) sprintf( buf, "The round will begin in %d seconds!", warmup_length ); } #if AQTION_CURL - lc_discord_webhook(buf, MATCH_START_MSG); + if (game.roundNum == 0) // Only announce on game start, so match pauses don't send the msg + lc_discord_webhook(buf, MATCH_START_MSG); #endif CenterPrintAll( buf ); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 2b627ac53..bf778a1da 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1868,6 +1868,12 @@ int bot_teamcheckfrequency = 0; int bot_teamchangefrequency = 0; void BOTLIB_CheckBotRules(void) { + // This is so we automatically report when a server has bots or not + if (bot_connections.desire_bots == 0) + gi.cvar_forceset("am", "0"); // Turn off attract mode + else + gi.cvar_forceset("am", "1"); // Turn on attract mode + if (matchmode->value) // Bots never allowed in matchmode return; diff --git a/src/action/g_save.c b/src/action/g_save.c index af8064694..0ae4e431a 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -635,13 +635,6 @@ void InitGame( void ) // 2023 use_killcounts = gi.cvar("use_killcounts", "0", 0); am = gi.cvar("am", "0", CVAR_LATCH | CVAR_SERVERINFO); - am_newnames = gi.cvar("am_newnames", "1", CVAR_LATCH); - am_botcount = gi.cvar("am_botcount", "6", CVAR_LATCH | CVAR_SERVERINFO); - if (am_botcount->value < 0){ - gi.cvar_forceset("am_botcount", "0"); - } - am_delay = gi.cvar("am_delay", "30", 0); - am_team = gi.cvar("am_team", "0", 0); zoom_comp = gi.cvar("zoom_comp", "1", 0); item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); diff --git a/src/action/p_client.c b/src/action/p_client.c index f9b2ffc19..ef0ef53bf 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -389,7 +389,7 @@ void Announce_Reward(edict_t *ent, int rewardType) { return; // Something didn't jive here? } ent->client->resp.awardstats[rewardType]++; - + #if AQTION_CURL lc_discord_webhook(buf, AWARD_MSG); #endif @@ -3345,11 +3345,6 @@ void ClientBeginDeathmatch(edict_t * ent) ACEIT_RebuildPlayerList(); #if USE_AQTION StatBotCheck(); - #if USE_AQTION - if(am->value){ - //attract_mode_bot_check(); - } - #endif #endif #endif @@ -3770,12 +3765,6 @@ void ClientDisconnect(edict_t * ent) ACEIT_RebuildPlayerList(); #if USE_AQTION StatBotCheck(); - - #if USE_AQTION - if(am->value){ - //attract_mode_bot_check(); - } - #endif #endif #endif } diff --git a/src/action/tng_net.c b/src/action/tng_net.c index df4c3c28f..ef72a9ef7 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -353,7 +353,7 @@ static char* ConstructGameSettingsString(void) { return result; } -static char *discord_MatchEndMsg(void) +static char *discord_MatchEndMsg(char* msg) { // Create the root object json_t *root = json_object(); @@ -368,15 +368,13 @@ static char *discord_MatchEndMsg(void) json_t *embed = json_object(); json_array_append_new(embeds, embed); - // Adjust description and color based on mode + // Adjust color based on mode + json_object_set_new(embed, "description", json_string(msg)); if (teamplay->value) { // Green - json_object_set_new(embed, "description", json_string(TP_MATCH_END_MSG)); json_object_set_new(embed, "color", json_integer(65280)); } else if (matchmode->value) { // Light blue - json_object_set_new(embed, "description", json_string(MM_MATCH_END_MSG)); json_object_set_new(embed, "color", json_integer(65535)); } else { // Red - json_object_set_new(embed, "description", json_string(DM_MATCH_END_MSG)); json_object_set_new(embed, "color", json_integer(16711680)); } @@ -465,7 +463,7 @@ static char *discord_MatchEndMsg(void) // Add triple backticks for Discord formatting char discord_formatted_scores[1280]; - snprintf(discord_formatted_scores, sizeof(discord_formatted_scores), "```%s```", formatted_player_scores); + snprintf(discord_formatted_scores, sizeof(discord_formatted_scores), "```\n%s```", formatted_player_scores); json_t *field = json_object(); json_object_set_new(field, "name", json_string("")); // Empty on purpose @@ -516,7 +514,7 @@ static char *discord_MatchEndMsg(void) return json_payload; } -static char *discord_MatchStartMsg(void) +static char *discord_MatchStartMsg(char* msg) { // Create the root object json_t *root = json_object(); @@ -533,7 +531,7 @@ static char *discord_MatchStartMsg(void) // Adjust description and color based on mode if (teamplay->value) { // Green - json_object_set_new(embed, "description", json_string(TP_MATCH_START_MSG)); + json_object_set_new(embed, "description", json_string(msg)); json_object_set_new(embed, "color", json_integer(65280)); } @@ -556,6 +554,15 @@ static char *discord_MatchStartMsg(void) json_object_set_new(author, "name", json_string(hostname->string)); json_object_set_new(embed, "author", author); + // Create the "footer" object (server ip and port) if server_ip is valid + if (is_valid_ipv4(server_ip->string)) { + json_t *footer = json_object(); + char footerinfo[64]; + snprintf(footerinfo, sizeof(footerinfo), "Server: %s:%s", server_ip->string, net_port->string); + json_object_set_new(footer, "text", json_string(footerinfo)); + json_object_set_new(embed, "footer", footer); + } + // Create the "fields" array json_t *fields = json_array(); json_object_set_new(embed, "fields", fields); @@ -569,7 +576,7 @@ static char *discord_MatchStartMsg(void) // Add triple backticks for Discord formatting char discord_formatted_players[1280]; - snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```%s```", team_players); + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); json_t *field = json_object(); @@ -591,7 +598,6 @@ static char *discord_MatchStartMsg(void) // Convert the JSON object to a string char *json_payload = json_dumps(root, JSON_INDENT(4)); - gi.dprintf("%s\n", json_payload); // Decrement the reference count of the root object to free memory json_decref(root); @@ -657,7 +663,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) "{\"content\":\"%s\"}", message); } else if (msgtype == MATCH_START_MSG) { - char *matchstartmsg = discord_MatchStartMsg(); + char *matchstartmsg = discord_MatchStartMsg(message); if (matchstartmsg) { // Print the JSON payload if (curldebug) @@ -667,7 +673,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) free(matchstartmsg); } } else if (msgtype == MATCH_END_MSG){ - char *matchendmsg = discord_MatchEndMsg(); + char *matchendmsg = discord_MatchEndMsg(message); if (matchendmsg) { // Print the JSON payload if (curldebug) diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index b5309c7a4..7fc85cfa5 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -62,7 +62,6 @@ typedef struct { uint32_t color; char name[1]; bool hasBots; - size_t numBots; } serverslot_t; typedef struct { @@ -262,31 +261,15 @@ void UI_StatusEvent(const serverStatus_t *status) const char *am = "No"; #if USE_AQTION - size_t ambci; - + // This checks if the server has bots, if so, turn the color of the server to MAGENTA const char *hasBotsCheck = Info_ValueForKey(status->infostring, "am"); - const char *botsCountCheck = Info_ValueForKey(status->infostring, "am_botcount"); if (hasBotsCheck == NULL || COM_IsWhite(hasBotsCheck) || *hasBotsCheck == '0') { am = "No"; slot->hasBots = false; } else { - if (slot) { - ambci = atoi(botsCountCheck); - if (ambci < 0) { - ambci = 0; - } - slot->numBots = ambci; - - // Don't count bots if humans equal or outnumber ambci - if (playerCount >= slot->numBots) { - playerCount = status->numPlayers; - } else { - playerCount = status->numPlayers + slot->numBots; - } - slot->hasBots = true; - am = "Yes"; - } + am = "Yes"; + slot->hasBots = true; } #endif From 0bfcf2c7bb8dddb68d27a6ccef10583443e1e814 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 15:31:17 -0400 Subject: [PATCH 669/974] Refactored awards, server msg in general, match formatting --- src/action/a_game.c | 2 +- src/action/a_match.c | 2 +- src/action/a_team.c | 22 +++--- src/action/g_cmds.c | 2 +- src/action/g_local.h | 19 +++--- src/action/g_main.c | 4 -- src/action/p_client.c | 10 +-- src/action/p_hud.c | 2 +- src/action/tng_net.c | 153 +++++++++++++++++++++++++++++++++--------- 9 files changed, 155 insertions(+), 61 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 6f85d45cb..e17c85e40 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1464,7 +1464,7 @@ void Cmd_Pickup_f(edict_t* ent) // 5 minute timer if(message_timer_check(300)) { - lc_discord_webhook(msg, SERVER_MSG); + lc_discord_webhook(msg, SERVER_MSG, AWARD_NONE); gi.cprintf(ent, PRINT_HIGH, "Pickup request sent.\n"); } else { gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); diff --git a/src/action/a_match.c b/src/action/a_match.c index 0813cab11..9a60ac38a 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -92,7 +92,7 @@ void SendScores(void) LogEndMatchStats(); // Generates end of match stats } #if AQTION_CURL - lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG); + lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); #endif #endif // Stats: Reset roundNum diff --git a/src/action/a_team.c b/src/action/a_team.c index c18adcd66..777e4714e 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2552,6 +2552,12 @@ qboolean CheckTimelimit( void ) CenterPrintAll( "3 MINUTES LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; + #if AQTION_CURL + if (!game.time_warning) { + lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); + game.time_warning = true; + } + #endif } } // Deathmatch and Team Deathmatch warnings @@ -2573,14 +2579,14 @@ qboolean CheckTimelimit( void ) CenterPrintAll( "3 MINUTES LEFT..." ); gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; + #if AQTION_CURL + if (!game.time_warning) { + lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); + game.time_warning = true; + } + #endif } } - #if AQTION_CURL - else if ( use_warnings->value && matchmode->value ){ - if( timelimit->value > 5 && level.matchTime >= ((timelimit->value - 5) * 60) && !during_countdown) - lc_discord_webhook(MM_FIVE_MIN_WARN, FIVE_MIN_WARN); - } - #endif } return false; @@ -2994,7 +3000,7 @@ int CheckTeamRules (void) } #if AQTION_CURL if (game.roundNum == 0) // Only announce on game start, so match pauses don't send the msg - lc_discord_webhook(buf, MATCH_START_MSG); + lc_discord_webhook(buf, MATCH_START_MSG, AWARD_NONE); #endif CenterPrintAll( buf ); @@ -3907,7 +3913,7 @@ void TallyEndOfLevelTeamScores (void) LogEndMatchStats(); // Generates end of match logs } #if AQTION_CURL - lc_discord_webhook(TP_MATCH_END_MSG, MATCH_END_MSG); + lc_discord_webhook(TP_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); #endif #endif // Stats: Reset roundNum diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 1477aa74e..464907c44 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1468,7 +1468,7 @@ void Cmd_Say_f (edict_t * ent, qboolean team, qboolean arg0, qboolean partner_ms // Send message to Discord -- this must come before the newline add or it screws up formatting in-game #if AQTION_CURL - lc_discord_webhook(text, CHAT_MSG); + lc_discord_webhook(text, CHAT_MSG, AWARD_NONE); #endif Q_strncatz(text, "\n", sizeof(text)); diff --git a/src/action/g_local.h b/src/action/g_local.h index 106f4ba3e..69e8f2840 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -437,16 +437,15 @@ typedef enum { SERVER_MSG = BIT(5), // 32 MATCH_START_MSG = BIT(6), // 64 MATCH_END_MSG = BIT(7), // 128 - FIVE_MIN_WARN = BIT(8), // 256 - NOTIFY_MAX = BIT(9) // 512 (enable all) + NOTIFY_MAX = BIT(8) // 256 (enable all) } Discord_Notifications; // Default messages #define MM_MATCH_END_MSG "Matchmode Results" -#define TP_MATCH_END_MSG "Teamplay Results" #define DM_MATCH_END_MSG "Deathmatch Results" -#define MM_FIVE_MIN_WARN "5 minutes remaining in the map" +#define MM_3_MIN_WARN "3 minutes remaining in the map" #define TP_MATCH_START_MSG "Match is about to begin!" +#define TP_MATCH_END_MSG "Match has ended!" //deadflag #define DEAD_NO 0 @@ -809,6 +808,11 @@ typedef struct char* bot_file_path[MAX_QPATH]; int used_bot_personalities; #endif + + #if AQTION_CURL + // Discord Webhook limits + qboolean time_warning; + #endif } game_locals_t; @@ -1047,6 +1051,7 @@ typedef enum { // Awards typedef enum { + AWARD_NONE, ACCURACY, IMPRESSIVE, EXCELLENT, @@ -1326,10 +1331,6 @@ extern cvar_t *esp_debug; // Enable or disable debug mode (very spammy) // 2023 extern cvar_t *use_killcounts; // Adjust how kill streaks are counted extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) -extern cvar_t *am_newnames; // Enable or disable new names for Attract Mode (ltk bots) -extern cvar_t *am_botcount; // Number of bots in Attract Mode -extern cvar_t *am_delay; // Delay between bot spawns after players leave in Attract Mode (not implemented) -extern cvar_t *am_team; // Set which team the bots will join in Attract Mode extern cvar_t *zoom_comp; // Enable or disable zoom compensation extern cvar_t *item_kit_mode; // Enable or disable item kit mode extern cvar_t *gun_dualmk23_enhance; // Enable or disable enhanced dual mk23s (laser + silencer) @@ -2935,6 +2936,6 @@ void FireTimedMessages(void); void lc_shutdown_function(void); qboolean lc_init_function(void); void lc_once_per_gameframe(void); -void lc_discord_webhook(char* message, Discord_Notifications msgtype); +void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awardtype); void lc_start_request_function(request_t* request); #endif diff --git a/src/action/g_main.c b/src/action/g_main.c index 6f1feac0f..1c33f6716 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -541,10 +541,6 @@ cvar_t *g_spawn_items; // 2023 cvar_t *use_killcounts; // Display kill counts in console to clients on frag cvar_t *am; // Attract mode toggle -cvar_t *am_newnames; // Attract mode new names, use new LTK bot names -cvar_t *am_botcount; // Attract mode botcount, how many bots at minimum at all times -cvar_t *am_delay; // Attract mode delay, unused at the moment -cvar_t *am_team; // Attract mode team, which team do you want the bots to join cvar_t *zoom_comp; // Compensates zoom-in frames with ping (high ping = fewer frames) cvar_t *item_kit_mode; // Toggles item kit mode cvar_t *gun_dualmk23_enhance; // Enables laser sight for dual mk23 pistols diff --git a/src/action/p_client.c b/src/action/p_client.c index ef0ef53bf..8be7a0aae 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -391,7 +391,7 @@ void Announce_Reward(edict_t *ent, int rewardType) { ent->client->resp.awardstats[rewardType]++; #if AQTION_CURL - lc_discord_webhook(buf, AWARD_MSG); + lc_discord_webhook(buf, AWARD_MSG, rewardType); #endif CenterPrintAll(buf); @@ -1181,7 +1181,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG); + lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); #endif IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); @@ -1216,7 +1216,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf( death_msg, "%s %s\n", self->client->pers.netname, message ); //Using discord webhook for death messaging #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG); + lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); #endif PrintDeathMessage(death_msg, self ); IRC_printf( IRC_T_DEATH, death_msg ); @@ -1572,7 +1572,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG); + lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); #endif IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); @@ -1620,7 +1620,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG); + lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); #endif IRC_printf(IRC_T_DEATH, death_msg); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index cfa80cd26..1f904d896 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -140,7 +140,7 @@ void BeginIntermission(edict_t *targ) } #if AQTION_CURL if(!teamplay->value) - lc_discord_webhook(DM_MATCH_END_MSG, MATCH_END_MSG); + lc_discord_webhook(DM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); #endif #endif diff --git a/src/action/tng_net.c b/src/action/tng_net.c index ef72a9ef7..6b0300d96 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -390,13 +390,22 @@ static char *discord_MatchEndMsg(char* msg) json_object_set_new(thumbnail, "url", json_string(mapimgurl)); // Add "username" field to the root object - json_object_set_new(root, "username", json_string(hostname->string)); + json_object_set_new(root, "username", json_string(TP_MATCH_END_MSG)); // Create the "author" object (hostname) json_t *author = json_object(); json_object_set_new(author, "name", json_string(hostname->string)); json_object_set_new(embed, "author", author); + // Create the "footer" object (server ip and port) if server_ip is valid + if (is_valid_ipv4(server_ip->string)) { + json_t *footer = json_object(); + char footerinfo[64]; + snprintf(footerinfo, sizeof(footerinfo), "%s: %s:%s", hostname->string, server_ip->string, net_port->string); + json_object_set_new(footer, "text", json_string(footerinfo)); + json_object_set_new(embed, "footer", footer); + } + // Create the "fields" array json_t *fields = json_array(); json_object_set_new(embed, "fields", fields); @@ -547,7 +556,7 @@ static char *discord_MatchStartMsg(char* msg) json_object_set_new(thumbnail, "url", json_string(mapimgurl)); // Add "username" field to the root object - json_object_set_new(root, "username", json_string(hostname->string)); + json_object_set_new(root, "username", json_string(TP_MATCH_START_MSG)); // Create the "author" object (hostname) json_t *author = json_object(); @@ -558,7 +567,7 @@ static char *discord_MatchStartMsg(char* msg) if (is_valid_ipv4(server_ip->string)) { json_t *footer = json_object(); char footerinfo[64]; - snprintf(footerinfo, sizeof(footerinfo), "Server: %s:%s", server_ip->string, net_port->string); + snprintf(footerinfo, sizeof(footerinfo), "%s: %s:%s", hostname->string, server_ip->string, net_port->string); json_object_set_new(footer, "text", json_string(footerinfo)); json_object_set_new(embed, "footer", footer); } @@ -604,11 +613,79 @@ static char *discord_MatchStartMsg(char* msg) return json_payload; } +static char *discord_ServerMsg(char* msg, Discord_Notifications msgtype, Awards awardtype) +{ + // Create the root object + json_t *root = json_object(); + + json_object_set_new(root, "content", json_string("")); + + // Create the "embeds" array + json_t *embeds = json_array(); + json_object_set_new(root, "embeds", embeds); + + // Create the embed object + json_t *embed = json_object(); + json_array_append_new(embeds, embed); + + // Adjust description and color based on mode + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(65280)); + + // Add fields to the embed object + json_object_set_new(embed, "title", json_string(level.mapname)); + + // Create the "thumbnail" object + json_t *thumbnail = json_object(); + json_object_set_new(embed, "thumbnail", thumbnail); + char mapimgurl[512]; + snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); + json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + + // Add "username" field to the root object + json_object_set_new(root, "username", json_string(hostname->string)); + + // Adjust the icon based on msgtype + switch (awardtype) { + case EXCELLENT: + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/excellent.png")); + break; + case IMPRESSIVE: + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/impressive.png")); + break; + default: + break; +} + + if (awardtype == EXCELLENT) { + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/excellent.png")); + } else if (msgtype == SERVER_MSG) { + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/icons/server.png")); + } + + // Create the "footer" object (server ip and port) if server_ip is valid + if (is_valid_ipv4(server_ip->string)) { + json_t *footer = json_object(); + char footerinfo[64]; + snprintf(footerinfo, sizeof(footerinfo), "%s: %s:%s", hostname->string, server_ip->string, net_port->string); + json_object_set_new(footer, "text", json_string(footerinfo)); + json_object_set_new(embed, "footer", footer); + } + + // Convert the JSON object to a string + char *json_payload = json_dumps(root, JSON_INDENT(4)); + + // Decrement the reference count of the root object to free memory + json_decref(root); + + return json_payload; +} + /* Call this with a string containing the message you want to send to the webhook. Limited to 2048 chars. */ -void lc_discord_webhook(char* message, Discord_Notifications msgtype) +void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awardtype) { request_t *request; char json_payload[2048]; @@ -633,8 +710,8 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) // Default url is to the #info-feed channel char* url = sv_curl_discord_info_url->string; // Change webhook URL based on message type - if (msgtype == FIVE_MIN_WARN) - url = sv_curl_discord_pickup_url->string; + // if (msgtype == SERVER_MSG) + // url = sv_curl_discord_pickup_url->string; // Use webhook.site to test curl, it's very handy! //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; @@ -653,37 +730,51 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype) *newline = '\0'; } - // Format the message as a JSON payload based on msgtype - if (msgtype == CHAT_MSG || msgtype == DEATH_MSG || msgtype == AWARD_MSG || msgtype == SERVER_MSG) { - snprintf(json_payload, sizeof(json_payload), - "{\"content\":\"```%s```\"}", - message); - } else if (msgtype == FIVE_MIN_WARN) { - snprintf(json_payload, sizeof(json_payload), - "{\"content\":\"%s\"}", - message); - } else if (msgtype == MATCH_START_MSG) { - char *matchstartmsg = discord_MatchStartMsg(message); - if (matchstartmsg) { - // Print the JSON payload - if (curldebug) - gi.dprintf("%s\n", matchstartmsg); - snprintf(json_payload, sizeof(json_payload), "%s", matchstartmsg); + switch (msgtype) { + case CHAT_MSG: + case DEATH_MSG: + snprintf(json_payload, sizeof(json_payload), + "{\"content\":\"```%s```\"}", + message); + break; + + case AWARD_MSG: + case SERVER_MSG: { + char *srvmsg = discord_ServerMsg(message, msgtype, awardtype); + if (srvmsg) { + snprintf(json_payload, sizeof(json_payload), "%s", srvmsg); + free(srvmsg); + } + break; + } - free(matchstartmsg); + case MATCH_START_MSG: { + char *matchstartmsg = discord_MatchStartMsg(message); + if (matchstartmsg) { + snprintf(json_payload, sizeof(json_payload), "%s", matchstartmsg); + free(matchstartmsg); + } + break; } - } else if (msgtype == MATCH_END_MSG){ - char *matchendmsg = discord_MatchEndMsg(message); - if (matchendmsg) { - // Print the JSON payload - if (curldebug) - gi.dprintf("%s\n", matchendmsg); - snprintf(json_payload, sizeof(json_payload), "%s", matchendmsg); - free(matchendmsg); + case MATCH_END_MSG: { + char *matchendmsg = discord_MatchEndMsg(message); + if (matchendmsg) { + snprintf(json_payload, sizeof(json_payload), "%s", matchendmsg); + free(matchendmsg); + } + break; } + + default: + if (curldebug) + gi.dprintf("%s: Unknown message type\n", __func__); + break; } + if (curldebug) + gi.dprintf("%s\n", json_payload); + request->url = url; request->payload = strdup(json_payload); From ead999a533d9c3cfb5340b2ed5b1416d67a591ee Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 15:46:37 -0400 Subject: [PATCH 670/974] Added constraint to 3 minute warning to also be matchmode --- src/action/a_team.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 777e4714e..a32a4c56f 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2553,7 +2553,7 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; #if AQTION_CURL - if (!game.time_warning) { + if (!game.time_warning && matchmode->value) { lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); game.time_warning = true; } @@ -2580,7 +2580,7 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; #if AQTION_CURL - if (!game.time_warning) { + if (!game.time_warning && matchmode->value) { lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); game.time_warning = true; } From 7b9f03a299707e8084f29a1448846b700d2280fe Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 15:55:02 -0400 Subject: [PATCH 671/974] Removed redundant code I forgot --- src/action/tng_net.c | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 6b0300d96..e5124d23f 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -647,22 +647,15 @@ static char *discord_ServerMsg(char* msg, Discord_Notifications msgtype, Awards // Adjust the icon based on msgtype switch (awardtype) { - case EXCELLENT: - json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/excellent.png")); - break; - case IMPRESSIVE: - json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/impressive.png")); - break; - default: - break; -} - - if (awardtype == EXCELLENT) { - json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/excellent.png")); - } else if (msgtype == SERVER_MSG) { - json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/icons/server.png")); + case EXCELLENT: + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/excellent.png")); + break; + case IMPRESSIVE: + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/impressive.png")); + break; + default: + break; } - // Create the "footer" object (server ip and port) if server_ip is valid if (is_valid_ipv4(server_ip->string)) { json_t *footer = json_object(); @@ -709,9 +702,13 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa // Default url is to the #info-feed channel char* url = sv_curl_discord_info_url->string; - // Change webhook URL based on message type - // if (msgtype == SERVER_MSG) - // url = sv_curl_discord_pickup_url->string; + // Change webhook URL based on message type. If pickup url is disabled, use info url + if (msgtype == SERVER_MSG) { + if (strcmp(sv_curl_discord_pickup_url->string, "disabled") == 0) + url = sv_curl_discord_info_url->string; + else + url = sv_curl_discord_pickup_url->string; + } // Use webhook.site to test curl, it's very handy! //char *url = "https://webhook.site/4de34388-9f3b-47fc-9074-7bdcd3cfa346"; From 5d3f44480ad40866993ef8424b33d3e968f049fa Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 30 Aug 2024 16:12:41 -0400 Subject: [PATCH 672/974] Forgot to set curldebug to false --- src/action/tng_net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index e5124d23f..c6bca8a23 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -8,7 +8,7 @@ Special thanks to Phatman for a bulk of the core code #include "g_local.h" #include -qboolean curldebug = true; +qboolean curldebug = false; // You will need one of these for each of the requests ... // ... if you allow concurrent requests to be sent at the same time From bc762b1dcb2106da937a23aa5acb9a966a9f39cf Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 30 Aug 2024 19:52:44 -0400 Subject: [PATCH 673/974] Pushed all notification bits up by one --- src/action/g_local.h | 18 +++++++++--------- src/action/tng_net.c | 18 +++--------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 69e8f2840..43ef7c61a 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -429,15 +429,15 @@ ammo_t; //tng_net.c typedef enum { - NOTIFY_NONE = BIT(0), // 1 - SERVER_WARMING_UP = BIT(1), // 2 - DEATH_MSG = BIT(2), // 4 - CHAT_MSG = BIT(3), // 8 - AWARD_MSG = BIT(4), // 16 - SERVER_MSG = BIT(5), // 32 - MATCH_START_MSG = BIT(6), // 64 - MATCH_END_MSG = BIT(7), // 128 - NOTIFY_MAX = BIT(8) // 256 (enable all) + SERVER_WARMING_UP = BIT(0), // 1 + DEATH_MSG = BIT(1), // 2 + CHAT_MSG = BIT(2), // 4 + AWARD_MSG = BIT(3), // 8 + SERVER_MSG = BIT(4), // 16 + MATCH_START_MSG = BIT(5), // 32 + MATCH_END_MSG = BIT(6), // 64 + PICKUP_REQ_MSG = BIT(7), // 128 + NOTIFY_MAX = BIT(8) // 256 (enable all) } Discord_Notifications; // Default messages diff --git a/src/action/tng_net.c b/src/action/tng_net.c index c6bca8a23..298c0e49f 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -627,20 +627,8 @@ static char *discord_ServerMsg(char* msg, Discord_Notifications msgtype, Awards // Create the embed object json_t *embed = json_object(); json_array_append_new(embeds, embed); - - // Adjust description and color based on mode json_object_set_new(embed, "description", json_string(msg)); - json_object_set_new(embed, "color", json_integer(65280)); - - // Add fields to the embed object - json_object_set_new(embed, "title", json_string(level.mapname)); - - // Create the "thumbnail" object - json_t *thumbnail = json_object(); - json_object_set_new(embed, "thumbnail", thumbnail); - char mapimgurl[512]; - snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); - json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + json_object_set_new(embed, "color", json_integer(16766720)); // Add "username" field to the root object json_object_set_new(root, "username", json_string(hostname->string)); @@ -683,8 +671,8 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa request_t *request; char json_payload[2048]; + // Debug prints if (curldebug) { - // Debug prints gi.dprintf("msgflags value: %d\n", (int)msgflags->value); gi.dprintf("msgtype value: %d\n", msgtype); } @@ -692,7 +680,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa // Look at the Discord_Notifications enum in g_local.h for the flags if (!(msgtype & (int)msgflags->value)) { if (curldebug) - gi.dprintf("Message type not allowed by msgflags\n"); + gi.dprintf("Message type (%d) not allowed by msgflags (%d)\n", msgtype, (int)msgflags->value); return; } From 43829486468aa96025d6f0f79d17917cd6640d9a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 30 Aug 2024 20:08:55 -0400 Subject: [PATCH 674/974] Renamed time_warning to time_warning_sent --- src/action/a_team.c | 8 ++++---- src/action/g_local.h | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index a32a4c56f..8439f0d43 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2553,9 +2553,9 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; #if AQTION_CURL - if (!game.time_warning && matchmode->value) { + if (!game.time_warning_sent && matchmode->value) { lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); - game.time_warning = true; + game.time_warning_sent = true; } #endif } @@ -2580,9 +2580,9 @@ qboolean CheckTimelimit( void ) gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/3_minutes.wav"), 1.0, ATTN_NONE, 0.0 ); timewarning = 1; #if AQTION_CURL - if (!game.time_warning && matchmode->value) { + if (!game.time_warning_sent && matchmode->value) { lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); - game.time_warning = true; + game.time_warning_sent = true; } #endif } diff --git a/src/action/g_local.h b/src/action/g_local.h index 43ef7c61a..81b3d8fa0 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -811,7 +811,8 @@ typedef struct #if AQTION_CURL // Discord Webhook limits - qboolean time_warning; + qboolean time_warning_sent; // This is set to true when the time warning has been sent, resets every map + #endif } game_locals_t; From 49e757c360e5d92e9c697422396dac4fb965edd4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 30 Aug 2024 19:46:35 +0300 Subject: [PATCH 675/974] Remove duplicate_surface_lmtc(). No longer needed. --- src/refresh/surf.c | 61 +++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 5254e3a16..f8facc73d 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -712,6 +712,25 @@ static void sample_surface_verts(mface_t *surf, vec_t *vbo) surf->statebits |= GLS_SHADE_SMOOTH; } +// normalizes and stores lightmap texture coordinates in vertices +static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) +{ + float s, t, scale = scale = 1.0f / lm.block_size; + int i; + + s = surf->light_s + 0.5f; + t = surf->light_t + 0.5f; + + for (i = 0; i < surf->numsurfedges; i++) { + vbo[6] += s; + vbo[7] += t; + vbo[6] *= scale; + vbo[7] *= scale; + + vbo += VERTEX_SIZE; + } +} + // validates and processes surface lightmap static void build_surface_light(mface_t *surf, vec_t *vbo) { @@ -746,42 +765,11 @@ static void build_surface_light(mface_t *surf, vec_t *vbo) return; } - if (gl_vertexlight->integer) + if (gl_vertexlight->integer) { sample_surface_verts(surf, vbo); - else + } else { LM_BuildSurface(surf); -} - -// normalizes and stores lightmap texture coordinates in vertices -static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) -{ - float s, t, scale = 1.0f / lm.block_size; - int i; - - s = surf->light_s + 0.5f; - t = surf->light_t + 0.5f; - - for (i = 0; i < surf->numsurfedges; i++) { - vbo[6] += s; - vbo[7] += t; - vbo[6] *= scale; - vbo[7] *= scale; - - vbo += VERTEX_SIZE; - } -} - -// duplicates normalized texture0 coordinates for non-lit surfaces in texture1 -// to make them render properly when gl_lightmap hack is used -static void duplicate_surface_lmtc(const mface_t *surf, vec_t *vbo) -{ - int i; - - for (i = 0; i < surf->numsurfedges; i++) { - vbo[6] = vbo[4]; - vbo[7] = vbo[5]; - - vbo += VERTEX_SIZE; + normalize_surface_lmtc(surf, vbo); } } @@ -901,11 +889,6 @@ static void upload_world_surfaces(void) build_surface_poly(surf, vbo); build_surface_light(surf, vbo); - if (surf->light_m) - normalize_surface_lmtc(surf, vbo); - else - duplicate_surface_lmtc(surf, vbo); - calc_surface_hash(surf); currvert += surf->numsurfedges; From 5060cb7d557e541a4932034388c122a750fbb892 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 30 Aug 2024 19:46:59 +0300 Subject: [PATCH 676/974] Add statebits_for_surface(). --- src/refresh/surf.c | 87 ++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index f8facc73d..0d7c2d6c3 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -552,48 +552,78 @@ static uint32_t color_for_surface(const mface_t *surf) return U32_WHITE; } -static void build_surface_poly(mface_t *surf, vec_t *vbo) +static bool enable_intensity_for_surface(const mface_t *surf) { - const bsp_t *bsp = gl_static.world.cache; - const msurfedge_t *src_surfedge; - const mvertex_t *src_vert; - const medge_t *src_edge; - const mtexinfo_t *texinfo = surf->texinfo; - const uint32_t color = color_for_surface(surf); - vec2_t scale, tc, mins, maxs; - int i, bmins[2], bmaxs[2]; + // enable for any surface with a lightmap in DECOUPLED_LM maps + if (surf->lightmap && gl_static.world.cache->lm_decoupled) + return true; + + // enable for non-transparent, non-warped surfaces + if (!(surf->drawflags & SURF_COLOR_MASK)) + return true; + + // enable for non-transparent lava (hack) + if (!(surf->drawflags & SURF_TRANS_MASK) && strstr(surf->texinfo->name, "lava")) + return true; + + return false; +} + +static glStateBits_t statebits_for_surface(const mface_t *surf) +{ + glStateBits_t statebits = GLS_DEFAULT; - // convert surface flags to state bits - surf->statebits = GLS_DEFAULT; if (gl_static.use_shaders) { // no inverse intensity if (!(surf->drawflags & SURF_TRANS_MASK)) - surf->statebits |= GLS_TEXTURE_REPLACE; - - // always use intensity on lightmapped surfaces - if ((surf->lightmap && bsp->lm_decoupled) || - !(surf->drawflags & SURF_COLOR_MASK) || - (!(surf->drawflags & SURF_TRANS_MASK) && strstr(texinfo->name, "lava"))) - surf->statebits |= GLS_INTENSITY_ENABLE; + statebits |= GLS_TEXTURE_REPLACE; + if (enable_intensity_for_surface(surf)) + statebits |= GLS_INTENSITY_ENABLE; } else { if (!(surf->drawflags & SURF_COLOR_MASK)) - surf->statebits |= GLS_TEXTURE_REPLACE; + statebits |= GLS_TEXTURE_REPLACE; } if (surf->drawflags & SURF_WARP) - surf->statebits |= GLS_WARP_ENABLE; + statebits |= GLS_WARP_ENABLE; if (surf->drawflags & SURF_TRANS_MASK) - surf->statebits |= GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE; + statebits |= GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE; else if (surf->drawflags & SURF_ALPHATEST) - surf->statebits |= GLS_ALPHATEST_ENABLE; + statebits |= GLS_ALPHATEST_ENABLE; if (surf->drawflags & SURF_FLOWING) { - surf->statebits |= GLS_SCROLL_ENABLE; + statebits |= GLS_SCROLL_ENABLE; if (surf->drawflags & SURF_WARP) - surf->statebits |= GLS_SCROLL_SLOW; + statebits |= GLS_SCROLL_SLOW; } + if (surf->drawflags & SURF_N64_SCROLL_X) + statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_X; + + if (surf->drawflags & SURF_N64_SCROLL_Y) + statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_Y; + + if (surf->drawflags & SURF_N64_SCROLL_FLIP) + statebits |= GLS_SCROLL_FLIP; + + return statebits; +} + +static void build_surface_poly(mface_t *surf, vec_t *vbo) +{ + const bsp_t *bsp = gl_static.world.cache; + const msurfedge_t *src_surfedge; + const mvertex_t *src_vert; + const medge_t *src_edge; + const mtexinfo_t *texinfo = surf->texinfo; + const uint32_t color = color_for_surface(surf); + vec2_t scale, tc, mins, maxs; + int i, bmins[2], bmaxs[2]; + + // convert surface flags to state bits + surf->statebits = statebits_for_surface(surf); + // normalize texture coordinates scale[0] = 1.0f / texinfo->image->width; scale[1] = 1.0f / texinfo->image->height; @@ -603,15 +633,6 @@ static void build_surface_poly(mface_t *surf, vec_t *vbo) scale[1] *= 0.5f; } - if (surf->drawflags & SURF_N64_SCROLL_X) - surf->statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_X; - - if (surf->drawflags & SURF_N64_SCROLL_Y) - surf->statebits |= GLS_SCROLL_ENABLE | GLS_SCROLL_Y; - - if (surf->drawflags & SURF_N64_SCROLL_FLIP) - surf->statebits |= GLS_SCROLL_FLIP; - mins[0] = mins[1] = 99999; maxs[0] = maxs[1] = -99999; From d11d87e42c27755bf54cccceb66d6cf7575789d7 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 31 Aug 2024 21:42:33 -0400 Subject: [PATCH 677/974] Added highscore centerprint --- src/action/tng_stats.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 28ac773b8..8eb83cb6a 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -775,6 +775,7 @@ void G_RegisterScore(void) gi.dprintf("Added highscore entry for %s with %d score\n", c->pers.netname, score); + CenterPrintAll(va("New Highscore for map %s: %s with %i score and %.2f%% accuracy", level.mapname, c->pers.netname, score, accuracy)); G_SaveScores(); } From 616140ce40443ccae69ef17bc7533b8a50212f05 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 31 Aug 2024 22:18:25 -0400 Subject: [PATCH 678/974] Added bot_playercount to set a specific number of bots+humans --- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib_cmd.c | 3 +++ src/action/botlib/botlib_spawn.c | 6 ++++++ src/action/g_main.c | 1 + src/action/g_save.c | 1 + 5 files changed, 12 insertions(+) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index d32d4dc9f..4ddab4a65 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -539,6 +539,7 @@ extern cvar_t* gib_heads; //rekkie -- DEV_1 -- Gib Players -- e extern cvar_t* bot_maxteam; +extern cvar_t* bot_playercount; extern cvar_t* bot_rush; extern cvar_t* bot_randvoice; extern cvar_t* bot_randskill; diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index f93cba9fb..757d2d46e 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -12,6 +12,9 @@ qboolean BOTLIB_SV_Cmds(void) { int cc = gi.argc(); + if (bot_playercount->value > 0) + gi.dprintf("bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added // sv bots diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 2b627ac53..7c556b8ad 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2121,7 +2121,13 @@ void BOTLIB_CheckBotRules(void) //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); + int bots_to_spawn = 0; + // Update desire_bots based on bot_playercount + if (bot_playercount->value != bot_connections.desire_bots) { + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; + } + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; // TODO: Fix BOTLIB_BotCountManager() to work diff --git a/src/action/g_main.c b/src/action/g_main.c index 5bed12941..c91d54a13 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -489,6 +489,7 @@ cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold cvar_t* bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight cvar_t* bot_maxteam; // Max bots allowed in autoteam +cvar_t* bot_playercount; // Preferred number of total clients (bots + humans) on the server cvar_t* bot_rush; // Bots rush players by going directly for them cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 max: 100] cvar_t* bot_randskill; // When random bot join a game, they pick a random skill [min: 1, max: 10]. Using 0 will turn this off. diff --git a/src/action/g_save.c b/src/action/g_save.c index d04c1e71b..f02cb81cd 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -691,6 +691,7 @@ void InitGame( void ) bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); bot_maxteam = gi.cvar("bot_maxteam", "0", 0); + bot_playercount = gi.cvar("bot_playercount", "0", 0); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); bot_randskill = gi.cvar("bot_randskill", "10", 0); From 049a06432f14fe7500ed030d766bb90c01a5df15 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 1 Sep 2024 11:14:45 -0400 Subject: [PATCH 679/974] Re-arranged BOTLIB_CheckBotRules, fixed bots bandaging at 100 health, working on bot+human management --- src/action/a_esp.c | 2 - src/action/a_team.c | 2 + src/action/botlib/botlib_cmd.c | 12 +- src/action/botlib/botlib_communication.c | 2 +- src/action/botlib/botlib_spawn.c | 530 ++++++++--------------- src/action/p_client.c | 10 +- 6 files changed, 208 insertions(+), 350 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 1cee193ac..c50f21717 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -2036,8 +2036,6 @@ void EspCleanUp(void) if (esp_debug->value) gi.dprintf("%s: Bodies cleaned, cleanupinterval is now %d\n", __func__, cleanupInterval); } - //gi.dprintf("the time is now %d\n", roundseconds); - } /* diff --git a/src/action/a_team.c b/src/action/a_team.c index 6d6f6d1c7..45c39715f 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2264,6 +2264,8 @@ static void SpawnPlayers(void) #ifndef NO_BOTS if (ent->bot.bot_type == BOT_TYPE_BOTLIB) { + // Reset bandage state + ent->bot.radioBandaging = false; BOTLIB_PutClientInServer(ent, true, ent->client->resp.team); } #endif diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 757d2d46e..083f6bcb8 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -12,9 +12,6 @@ qboolean BOTLIB_SV_Cmds(void) { int cc = gi.argc(); - if (bot_playercount->value > 0) - gi.dprintf("bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); - gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added // sv bots @@ -22,6 +19,10 @@ qboolean BOTLIB_SV_Cmds(void) { bot_connections.desire_bots = atoi(gi.argv(2)); // How many bots bot_connections.auto_balance_bots = true; + + if (bot_playercount->value > 0) + gi.dprintf("bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); + return true; } @@ -76,6 +77,11 @@ qboolean BOTLIB_SV_Cmds(void) gi.cprintf(NULL, PRINT_HIGH, "CTF bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); else if (teamplay->value) gi.cprintf(NULL, PRINT_HIGH, "TP bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); + + if (bot_playercount->value > 0) { + gi.cprintf(NULL, PRINT_HIGH, "**---------------------------**\n"); + gi.cprintf(NULL, PRINT_HIGH, "bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); + } gi.cprintf(NULL, PRINT_HIGH, "-------------------------------\n"); } diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index a9a334c72..98e8736cd 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -791,7 +791,7 @@ void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) // Bandaging if (self->bot.radioBandaging) // If bandaging, flag the bot to radio this in { - if (IS_ALIVE(self)) + if (IS_ALIVE(self) && self->health < 100) { sprintf(buffer, " %c Bandaging [ %d%c ] %c", '\x04', self->health, '\x05', '\x04'); // Build string - 0x04 bandage symbol, 0x05 heart symbol BOTLIB_Say(self, buffer, true); // Say it diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 7c556b8ad..68ad2a3a4 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -211,89 +211,106 @@ qboolean BotCountManagerTimer(void) return false; } -int BOTLIB_BotCountManager(void) +void BOTLIB_BotCountAdd(int bots_to_spawn) { - int bots_desired = bot_connections.desire_bots; - gi.dprintf("%s: desire_bots[%i]\n", __func__, bot_connections.desire_bots); - - // If we aren't rotating bots, just return existing desire_bots value - if (!BotCountManagerTimer() || !bot_rotate->value) - return (bots_desired - bot_connections.total_bots); - - int bcmin = bot_count_min->value; - int bcmax = bot_count_max->value; - - // Percent is used in tandem with BotCountManagerTimer to determine when to rotate bots - float percent = (float)(bot_connections.total_humans_playing + bot_connections.total_bots) / maxclients->value * 100; - - // This value determines if we add, remove or keep the same amount of bots - // 1 = add, -1 = remove, 0 = keep the same - int up_down_same = 0; - - // Basic value validation - if (bcmin < 0) { - gi.dprintf("%s: bot_count_min < 0, setting it to 0\n", __func__); - gi.cvar_forceset(bot_count_min->name, "0"); - bcmin = 0; - } - if (bcmax > maxclients->value) { - gi.dprintf("%s: bot_count_max > maxclients, setting it to maxclients\n", __func__); - gi.cvar_forceset(bot_count_max->name, va("%s", maxclients->name)); - bcmax = maxclients->value; - } - if (bcmin > bcmax) { - gi.dprintf("%s: bot_count_min > bot_count_max, setting them to be the same\n", __func__); - gi.cvar_forceset(bot_count_min->name, va("%s", bot_count_max->name)); - bcmin = bcmax; - } - if (bcmin == 0 && bcmax == 0) - { - gi.dprintf("%s: bot_count_min and bot_count_max are both 0, not rotating bots, setting bot_rotate to 0\n", __func__); - gi.cvar_forceset(bot_rotate->name, "0"); - return (bots_desired - bot_connections.total_bots); + bots_to_spawn = abs(bots_to_spawn); + for (int i = 0; i < bots_to_spawn; i++) { + if (teamplay->value) // Add a bot to a team with the lowest player count + { + if (use_3teams->value) // 3TEAMS + { + if (bot_connections.total_team1 <= bot_connections.total_team2 && bot_connections.total_team1 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team2 <= bot_connections.total_team1 && bot_connections.total_team2 <= bot_connections.total_team3) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else if (bot_connections.total_team3 <= bot_connections.total_team1 && bot_connections.total_team3 <= bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + else // If all teams are equal size, randomly add bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (choice == 1) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + } + } + else // 2-team Teamplay + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + else // If both teams are equal size, randomly add bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + else + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + } + } + } + else // DM + BOTLIB_SpawnBot(0, INVALID, NULL, NULL); } +} - //The following logic only applies if BotCountManagerTimer() returns true - - /* - If we have more than 80% of the server filled with players and bots, remove bots - else if we're low on clients, consider adding bots - else keep the same amount of bots - */ - - // Don't add more bots if we're at the max - BOTLIB_Debug("%s: desire_bots[%i] total_bots[%i] total_humans[%i] percent[%f]\n", __func__, bots_desired, bot_connections.total_bots, bot_connections.total_humans_playing, percent); - if (bot_connections.total_bots < bcmax) { - if (rand() % 2 == 0) { - if (percent > 80) - up_down_same = -1; - else if (percent < 30) - up_down_same = 1; +void BOTLIB_BotCountRemove(int bots_to_spawn) +{ + bots_to_spawn = abs(bots_to_spawn); + for (int i = 0; i < bots_to_spawn; i++) { + if (teamplay->value) // Remove a bot from team with the highest player count + { + if (use_3teams->value) + { + if (bot_connections.total_team1 >= bot_connections.total_team2 && bot_connections.total_team1 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team2 >= bot_connections.total_team1 && bot_connections.total_team2 >= bot_connections.total_team3) + BOTLIB_RemoveTeamplayBot(TEAM2); + else if (bot_connections.total_team3 >= bot_connections.total_team1 && bot_connections.total_team3 >= bot_connections.total_team2) + + BOTLIB_RemoveTeamplayBot(TEAM3); + else // If all teams are equal size, randomly remove bot to one of the teams + { + int choice = rand() % 3; + if (choice == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (choice == 1) + BOTLIB_RemoveTeamplayBot(TEAM2); + else + BOTLIB_RemoveTeamplayBot(TEAM3); + } + } else - up_down_same = 0; - } else { - // Bot just feels like leaving - if (rand() % 100 < 5) - up_down_same = -1; + { + if (bot_connections.total_team1 > bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM1); + else if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_RemoveTeamplayBot(TEAM2); + else // If both teams are equal size, randomly remove bot to one of the teams + { + if (rand() % 2 == 0) + BOTLIB_RemoveTeamplayBot(TEAM1); + else + BOTLIB_RemoveTeamplayBot(TEAM2); + } + } } - } else { - return (bot_connections.desire_bots - bot_connections.total_bots); + else // DM + BOTLIB_RemoveBot(""); } +} - // This will add, remove or keep the same amount of bots - bot_connections.desire_bots += up_down_same; - return (bot_connections.desire_bots); - - // // Choose a random number between bcmin and bcmax, minimum count of bots is bcmin - // int bot_count_rotate = bcmin + rand() % (bcmax - bcmin + 1); - - // // Make the change - // if (bot_count_rotate == bc){ - // return bc; // No change - // } else { // Set desired bots to be how many we rotate in or out - // bot_connections.desire_bots = bot_count_rotate; - // return bot_count_rotate; - // } +void BOTLIB_BotCountManager(int bots_to_spawn) +{ + if (bots_to_spawn < 0) { + BOTLIB_BotCountRemove(bots_to_spawn); + } else if (bots_to_spawn > 0) { + BOTLIB_BotCountAdd(bots_to_spawn); + } else { // bots_to_spawn == 0 - no bots to add or remove + return; + } } // Add bots from the previous map from file @@ -1622,64 +1639,70 @@ void BOTLIB_RemoveBot(char* name) int bot_to_kick = 0; int num_bots = 0; int bot_count = 0; - for (i = 0; i < game.maxclients; i++) - { + + // First, try to find a dead bot to remove + for (int i = 0; i < game.maxclients; i++) { bot = g_edicts + i + 1; - if (bot->inuse) - { - if (bot->is_bot) - { + + if (bot->inuse && !IS_ALIVE(bot) && bot->is_bot) { + num_bots++; + } + } + + // If no dead bots were found, find any bot to remove + if (num_bots == 0) { + for (int i = 0; i < game.maxclients; i++) { + bot = g_edicts + i + 1; + + if (bot->inuse && bot->is_bot) { num_bots++; } } + + // If still no bots were found, return + if (num_bots == 0) { + return; + } } - if (num_bots == 0) - { - return; - } - else if (num_bots > 1) - { - bot_to_kick = (rand() % num_bots); + + // If more than one bot is found, select a random bot to kick + if (num_bots > 1) { + bot_to_kick = rand() % num_bots; } - for (i = 0; i < game.maxclients; i++) - { + + // Iterate again to find the bot to kick + for (int i = 0; i < game.maxclients; i++) { bot = g_edicts + i + 1; - if (bot->inuse) - { - if (bot->is_bot && bot_to_kick == bot_count) - { - //darksaint -- Bot Chat -- s - // Generates chat message (goodbyes) - if (bot_chat->value) + + if (bot->inuse && bot->is_bot) { + if (bot_to_kick == bot_count) { + // Bot Chat - Generates chat message (goodbyes) + if (bot_chat->value) { BOTLIB_Chat(bot, CHAT_GOODBYE); - //darksaint -- Bot Chat -- e + } - //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client - //rekkie -- Fake Bot Client -- e + // Fake Bot Client - Disconnect the bot + gi.SV_BotDisconnect(bot->client->pers.netname); bot->health = 0; player_die(bot, bot, bot, 100000, vec3_origin); - // don't even bother waiting for death frames - //bot->deadflag = DEAD_DEAD; - //bot->inuse = false; - freed = true; ClientDisconnect(bot); game.bot_count--; - if (bot_personality->value && bot->bot.personality.isActive) + + if (bot_personality->value && bot->bot.personality.isActive) { BOTLIB_FreeBotPersonality(bot); - //gi.bprintf (PRINT_MEDIUM, "%s removed\n", bot->client->pers.netname); + } + + freed = true; break; } - if (bot->is_bot) - bot_count--; + bot_count++; } } - if (!freed) { - if (debug_mode) { - gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); - } + // If no bot was removed, print debug message + if (!freed && debug_mode) { + gi.bprintf(PRINT_MEDIUM, "No bot removed\n"); } } @@ -1741,9 +1764,9 @@ void BOTLIB_ChangeBotTeam(int from_team, int to_team) { if (bot->is_bot) // Is a bot { - // Only kick when the bot isn't actively in a match - //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) - if (bot->client->resp.team == from_team && team_round_going == 0) + // Only kick when the bot isn't actively in a match or dead + if ((bot->client->resp.team == from_team && team_round_going == 0) || + (bot->client->resp.team == from_team && !IS_ALIVE(bot))) { if (from_team == TEAM1) bot_connections.total_team1--; else if (from_team == TEAM2) bot_connections.total_team2--; @@ -1862,6 +1885,37 @@ void BOTLIB_GetTotalPlayers(bot_connections_t* bc) bc->total_humans_playing = bc->total_humans - bc->spec_humans; } +static void BOTLIB_TeamBotShuffle(void) +{ + if (use_3teams->value && (bot_connections.total_team1 != bot_connections.total_team2 || bot_connections.total_team1 != bot_connections.total_team3 || bot_connections.total_team2 != bot_connections.total_team3)) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1 && bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + if (abs(bot_connections.total_team1 - bot_connections.total_team3) > 1 && bot_connections.total_team1 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM1); + + if (abs(bot_connections.total_team2 - bot_connections.total_team1) > 1 && bot_connections.total_team2 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + if (abs(bot_connections.total_team2 - bot_connections.total_team3) > 1 && bot_connections.total_team2 < bot_connections.total_team3) + BOTLIB_ChangeBotTeam(TEAM3, TEAM2); + + if (abs(bot_connections.total_team3 - bot_connections.total_team1) > 1 && bot_connections.total_team3 < bot_connections.total_team1) + BOTLIB_ChangeBotTeam(TEAM1, TEAM3); + if (abs(bot_connections.total_team3 - bot_connections.total_team2) > 1 && bot_connections.total_team3 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM3); + } + else if (bot_connections.total_team1 != bot_connections.total_team2) + { + if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1) + { + if (bot_connections.total_team1 < bot_connections.total_team2) + BOTLIB_ChangeBotTeam(TEAM2, TEAM1); + else + BOTLIB_ChangeBotTeam(TEAM1, TEAM2); + } + } +} + // Bots auto join teams to keep them equal in teamplay. // Empty servers have a bot join a team upon a player connecting. int bot_teamcheckfrequency = 0; @@ -1897,7 +1951,8 @@ void BOTLIB_CheckBotRules(void) BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } - if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; // Turn on team balance if bot_maxteam is used + // Turn on team balance if bot_maxteam is used or use_balancer is enabled + if (bot_maxteam->value > 0 || use_balancer->value) bot_connections.auto_balance_bots = true; // ======================================================================================================== // Manually add bots to a select team @@ -1978,83 +2033,6 @@ void BOTLIB_CheckBotRules(void) BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } } - - /* - // Sanity check - if (bot_connections.desire_team1 < -1) bot_connections.desire_team1 = 0; - if (bot_connections.desire_team2 < -1) bot_connections.desire_team2 = 0; - if (bot_connections.desire_team3 < -1) bot_connections.desire_team3 = 0; - - int count = (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); - if (count == -1) // Remove all bots from team - { - // Remove all bots from team - if (bot_connections.desire_team1 == -1) - { - while (bot_connections.team1_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM1); - bot_connections.team1_bots--; - } - } - else if (bot_connections.desire_team2 == -1) - { - while (bot_connections.team2_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM2); - bot_connections.team2_bots--; - } - } - else if (use_3teams->value && bot_connections.desire_team3 == -1) - { - while (bot_connections.team3_bots) - { - BOTLIB_RemoveTeamplayBot(TEAM3); - bot_connections.team3_bots--; - } - } - } - if (count > 0) // Add bots to team - { - for (int i = 0; i < count; i++) - { - if (bot_connections.total_bots + bot_connections.total_humans + 1 < maxclients->value) // Max allowed - { - if (bot_connections.desire_team1) - { - if (bot_connections.team1_bots < bot_connections.desire_team1) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.team1_bots > bot_connections.desire_team1) - BOTLIB_RemoveTeamplayBot(TEAM1); - else - break; - } - else if (bot_connections.desire_team2) - { - if (bot_connections.team2_bots < bot_connections.desire_team2) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else if (bot_connections.team2_bots > bot_connections.desire_team2) - BOTLIB_RemoveTeamplayBot(TEAM2); - else - break; - } - else if (use_3teams->value && bot_connections.desire_team3) - { - if (bot_connections.team3_bots < bot_connections.desire_team3) - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - else if (bot_connections.team3_bots > bot_connections.desire_team3) - BOTLIB_RemoveTeamplayBot(TEAM3); - else - break; - } - } - } - bot_connections.desire_team1 = 0; - bot_connections.desire_team2 = 0; - bot_connections.desire_team3 = 0; - return; - } - */ } // ======================================================================================================== @@ -2111,67 +2089,33 @@ void BOTLIB_CheckBotRules(void) } } - - // Always leave room for another player to join - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - bot_connections.desire_bots = (maxclients->value - 1); - // Sanity check - safety limits //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); +int bots_to_spawn = 0; - int bots_to_spawn = 0; - // Update desire_bots based on bot_playercount - if (bot_playercount->value != bot_connections.desire_bots) { - bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; - } +// Only proceed if bot_playercount is greater than 0 +if (bot_playercount->value > 0) { + // Calculate the desired number of bots based on bot_playercount and total_humans + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + // Ensure desire_bots does not exceed maxclients - total_humans + if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { + bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); + } - // TODO: Fix BOTLIB_BotCountManager() to work - // if (!teamplay->value) { - // if (bot_rotate->value) { - // bots_to_spawn = BOTLIB_BotCountManager(); - // } else { - // bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; - // } - // } + // Sanity check - safety limits + if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; +} else { + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; +} // Shuffle bots around - //if (teamplay->value && bots_to_spawn == 0) if (teamplay->value && bot_connections.auto_balance_bots) - { - if (use_3teams->value && (bot_connections.total_team1 != bot_connections.total_team2 || bot_connections.total_team1 != bot_connections.total_team3 || bot_connections.total_team2 != bot_connections.total_team3)) - { - if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1 && bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM1); - if (abs(bot_connections.total_team1 - bot_connections.total_team3) > 1 && bot_connections.total_team1 < bot_connections.total_team3) - BOTLIB_ChangeBotTeam(TEAM3, TEAM1); - - if (abs(bot_connections.total_team2 - bot_connections.total_team1) > 1 && bot_connections.total_team2 < bot_connections.total_team1) - BOTLIB_ChangeBotTeam(TEAM1, TEAM2); - if (abs(bot_connections.total_team2 - bot_connections.total_team3) > 1 && bot_connections.total_team2 < bot_connections.total_team3) - BOTLIB_ChangeBotTeam(TEAM3, TEAM2); - - if (abs(bot_connections.total_team3 - bot_connections.total_team1) > 1 && bot_connections.total_team3 < bot_connections.total_team1) - BOTLIB_ChangeBotTeam(TEAM1, TEAM3); - if (abs(bot_connections.total_team3 - bot_connections.total_team2) > 1 && bot_connections.total_team3 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM3); - } - else if (bot_connections.total_team1 != bot_connections.total_team2) - { - if (abs(bot_connections.total_team1 - bot_connections.total_team2) > 1) - { - if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_ChangeBotTeam(TEAM2, TEAM1); - else - BOTLIB_ChangeBotTeam(TEAM1, TEAM2); - } - } - - } + BOTLIB_TeamBotShuffle(); // Remove ALL bots if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) @@ -2182,99 +2126,7 @@ void BOTLIB_CheckBotRules(void) bot_connections.total_team3 = 0; } - // Remove bots - //if (bot_connections.total_bots > bot_connections.desire_bots) - if (bots_to_spawn < 0) - { - bots_to_spawn = abs(bots_to_spawn); - for (int i = 0; i < bots_to_spawn; i++) - { - if (teamplay->value) // Remove a bot from team with the highest player count - { - if (use_3teams->value) - { - if (bot_connections.total_team1 >= bot_connections.total_team2 && bot_connections.total_team1 >= bot_connections.total_team3) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (bot_connections.total_team2 >= bot_connections.total_team1 && bot_connections.total_team2 >= bot_connections.total_team3) - BOTLIB_RemoveTeamplayBot(TEAM2); - else if (bot_connections.total_team3 >= bot_connections.total_team1 && bot_connections.total_team3 >= bot_connections.total_team2) - - BOTLIB_RemoveTeamplayBot(TEAM3); - else // If all teams are equal size, randomly remove bot to one of the teams - { - int choice = rand() % 3; - if (choice == 0) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (choice == 1) - BOTLIB_RemoveTeamplayBot(TEAM2); - else - BOTLIB_RemoveTeamplayBot(TEAM3); - } - } - else - { - if (bot_connections.total_team1 > bot_connections.total_team2) - BOTLIB_RemoveTeamplayBot(TEAM1); - else if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_RemoveTeamplayBot(TEAM2); - else // If both teams are equal size, randomly remove bot to one of the teams - { - if (rand() % 2 == 0) - BOTLIB_RemoveTeamplayBot(TEAM1); - else - BOTLIB_RemoveTeamplayBot(TEAM2); - } - } - } - else // DM - BOTLIB_RemoveBot(""); - } - return; - } - //else if (bot_connections.total_bots < bots_to_spawn) - else if (bots_to_spawn > 0) - { - for (int i = 0; i < bots_to_spawn; i++) - { - if (teamplay->value) // Add a bot to a team with the lowest player count - { - if (use_3teams->value) // 3TEAMS - { - if (bot_connections.total_team1 <= bot_connections.total_team2 && bot_connections.total_team1 <= bot_connections.total_team3) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.total_team2 <= bot_connections.total_team1 && bot_connections.total_team2 <= bot_connections.total_team3) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else if (bot_connections.total_team3 <= bot_connections.total_team1 && bot_connections.total_team3 <= bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - else // If all teams are equal size, randomly add bot to one of the teams - { - int choice = rand() % 3; - if (choice == 0) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (choice == 1) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - } - } - else // TP - { - if (bot_connections.total_team1 < bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else if (bot_connections.total_team1 > bot_connections.total_team2) - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - else // If both teams are equal size, randomly add bot to one of the teams - { - if (rand() % 2 == 0) - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - else - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - } - } - } - else // DM - BOTLIB_SpawnBot(0, INVALID, NULL, NULL); - } - } + // Time to add or remove bots + BOTLIB_BotCountManager(bots_to_spawn); } //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/p_client.c b/src/action/p_client.c index ef619c309..f343283bd 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3055,11 +3055,11 @@ void PutClientInServer(edict_t * ent) else BOTLIB_BotPersonalityChooseItem(ent); } - gi.dprintf("Bot %s has chosen %s and %s\n", - ent->client->pers.netname, - PrintWeaponName(ent->client->pers.chosenWeapon->typeNum), - PrintItemName(ent->client->pers.chosenItem->typeNum) - ); + // gi.dprintf("Bot %s has chosen %s and %s\n", + // ent->client->pers.netname, + // PrintWeaponName(ent->client->pers.chosenWeapon->typeNum), + // PrintItemName(ent->client->pers.chosenItem->typeNum) + // ); } else // LTK bots { From 35dbfec00beb48b699bd629775a5932211cafacf Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 1 Sep 2024 12:12:58 -0400 Subject: [PATCH 680/974] bot_playercount appears to work as intended --- src/action/acesrc/bot_collision.c | 3 +- src/action/botlib/botlib_cmd.c | 4 +- src/action/botlib/botlib_spawn.c | 68 +++++++++++++++++++------------ src/action/g_main.c | 2 +- src/action/g_save.c | 2 +- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/action/acesrc/bot_collision.c b/src/action/acesrc/bot_collision.c index a32fe6cfc..c4b19a061 100644 --- a/src/action/acesrc/bot_collision.c +++ b/src/action/acesrc/bot_collision.c @@ -271,7 +271,8 @@ qboolean BOTCOL_CanMoveSafely(edict_t *self, vec3_t angles) // Calculate the distance between the bot's current position and the destination position this_dist = sqrt(pow(dest2[0] - self->s.origin[0], 2) + pow(dest2[1] - self->s.origin[1], 2) + pow(dest2[2] - self->s.origin[2], 2)); - gi.dprintf("Distance to destination: %f\n", this_dist); + if (bot_debug->value) + gi.dprintf("Distance to destination: %f\n", this_dist); if( (trace.fraction == 1.0) // long drop! || (trace.contents & MASK_DEADLY) // avoid SLIME or LAVA || (this_dist > (NODE_MAX_FALL_HEIGHT * 3)) ) // avoid falling too far diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 083f6bcb8..3aac838d5 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -20,7 +20,7 @@ qboolean BOTLIB_SV_Cmds(void) bot_connections.desire_bots = atoi(gi.argv(2)); // How many bots bot_connections.auto_balance_bots = true; - if (bot_playercount->value > 0) + if (bot_playercount->value > 0 && !(gameSettings & GS_ROUNDBASED)) gi.dprintf("bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); return true; @@ -78,7 +78,7 @@ qboolean BOTLIB_SV_Cmds(void) else if (teamplay->value) gi.cprintf(NULL, PRINT_HIGH, "TP bots on T1[%d] T2[%d] Total[%d]\n", bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.total_bots); - if (bot_playercount->value > 0) { + if (bot_playercount->value > 0 && !(gameSettings & GS_ROUNDBASED)) { gi.cprintf(NULL, PRINT_HIGH, "**---------------------------**\n"); gi.cprintf(NULL, PRINT_HIGH, "bot_playercount is set to %i, sv bots has no effect\n", (int)bot_playercount->value); } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 68ad2a3a4..5d7cba246 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1942,7 +1942,10 @@ void BOTLIB_CheckBotRules(void) } BOTLIB_GetTotalPlayers(&bot_connections); - //Com_Printf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots); + + // All bots are counted as being on team 1 if in DM mode + + //gi.dprintf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d] tb[%d] th[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots, bot_connections.total_humans); if (bot_connections.tried_adding_prev_bots == false) { @@ -1951,8 +1954,8 @@ void BOTLIB_CheckBotRules(void) BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } - // Turn on team balance if bot_maxteam is used or use_balancer is enabled - if (bot_maxteam->value > 0 || use_balancer->value) bot_connections.auto_balance_bots = true; + // Turn on team balance if bot_maxteam is used + if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; // ======================================================================================================== // Manually add bots to a select team @@ -2037,9 +2040,43 @@ void BOTLIB_CheckBotRules(void) // ======================================================================================================== - // Auto balance bots - if (bot_connections.auto_balance_bots == false) - return; + // Manage bot counts in DM mode + int bots_to_spawn = 0; + if (bot_playercount->value > 0 && (gameSettings & GS_DEATHMATCH)) { + // Calculate the desired number of bots based on bot_playercount and total_humans + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; + + // Ensure desire_bots does not exceed maxclients - total_humans + if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { + bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); + } + + // Sanity check - safety limits + if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + int bots_adj = 0; + int total_players = bot_connections.total_bots + bot_connections.total_humans; + + if (total_players > bot_playercount->value) { + //gi.dprintf("I should remove a bot\n"); + bots_adj = -1; + } else if (total_players < bot_playercount->value) { + // gi.dprintf("I should add a bot\n"); + // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); + bots_adj = 1; + } else { + bots_to_spawn = (int)bot_playercount->value; + bot_connections.desire_bots = bots_to_spawn; + } + + bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed + //gi.dprintf("bots_adj: %d\n", bots_to_spawn); + } else { + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + } + + // // Auto balance bots + // if (bot_connections.auto_balance_bots == false) + // return; //bot_connections.desire_bots += (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); @@ -2094,25 +2131,6 @@ void BOTLIB_CheckBotRules(void) //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); -int bots_to_spawn = 0; - -// Only proceed if bot_playercount is greater than 0 -if (bot_playercount->value > 0) { - // Calculate the desired number of bots based on bot_playercount and total_humans - bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; - - // Ensure desire_bots does not exceed maxclients - total_humans - if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { - bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); - } - - // Sanity check - safety limits - if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; - - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; -} else { - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; -} // Shuffle bots around if (teamplay->value && bot_connections.auto_balance_bots) BOTLIB_TeamBotShuffle(); diff --git a/src/action/g_main.c b/src/action/g_main.c index c91d54a13..2a7548406 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -488,7 +488,7 @@ cvar_t* bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit cvar_t* bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight -cvar_t* bot_maxteam; // Max bots allowed in autoteam +cvar_t* bot_maxteam; // Max bots allowed in autoteam, default 10 (10 max bots per team) cvar_t* bot_playercount; // Preferred number of total clients (bots + humans) on the server cvar_t* bot_rush; // Bots rush players by going directly for them cvar_t* bot_randvoice; // Bots use random user voice wavs - percentage [min: 0 max: 100] diff --git a/src/action/g_save.c b/src/action/g_save.c index f02cb81cd..c99724ba3 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -690,7 +690,7 @@ void InitGame( void ) bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); - bot_maxteam = gi.cvar("bot_maxteam", "0", 0); + bot_maxteam = gi.cvar("bot_maxteam", "10", 0); bot_playercount = gi.cvar("bot_playercount", "0", 0); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); From 93cc2705db823a29471dfc79cd251ba92be7c6ba Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 31 Aug 2024 21:08:42 +0300 Subject: [PATCH 681/974] Cull shadows separately from model. Fixes old bug when shadows disappear when alias model goes out of view. --- src/refresh/gl.h | 1 + src/refresh/mesh.c | 124 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 25 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index e790aa3eb..ca84fb353 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -218,6 +218,7 @@ extern cvar_t *gl_novbo; extern cvar_t *gl_test; #endif extern cvar_t *gl_cull_nodes; +extern cvar_t *gl_cull_models; extern cvar_t *gl_clear; extern cvar_t *gl_novis; extern cvar_t *gl_lockpvs; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index f8498e8dd..e06585e87 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -18,6 +18,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gl.h" +typedef enum { + SHADOW_NO, + SHADOW_YES, + SHADOW_ONLY +} drawshadow_t; + static unsigned oldframenum; static unsigned newframenum; static float frontlerp; @@ -35,7 +41,8 @@ static bool dotshading; static float celscale; -static GLfloat shadowmatrix[16]; +static drawshadow_t drawshadow; +static GLfloat shadowmatrix[16]; #if USE_MD5 static md5_joint_t temp_skeleton[MD5_MAX_JOINTS]; @@ -54,6 +61,9 @@ static void setup_dotshading(void) if (glr.ent->flags & RF_SHELL_MASK) return; + if (drawshadow == SHADOW_ONLY) + return; + dotshading = true; // matches the anormtab.h precalculations @@ -285,9 +295,6 @@ static glCullResult_t cull_static_model(const model_t *model) } } - VectorCopy(newframe->scale, newscale); - VectorCopy(newframe->translate, translate); - return cull; } @@ -325,13 +332,24 @@ static glCullResult_t cull_lerped_model(const model_t *model) } } - VectorScale(oldframe->scale, backlerp, oldscale); - VectorScale(newframe->scale, frontlerp, newscale); + return cull; +} - LerpVector2(oldframe->translate, newframe->translate, - backlerp, frontlerp, translate); +static void setup_frame_scale(const model_t *model) +{ + const maliasframe_t *newframe = &model->frames[newframenum]; + const maliasframe_t *oldframe = &model->frames[oldframenum]; - return cull; + if (oldframenum == newframenum) { + VectorCopy(newframe->scale, newscale); + VectorCopy(newframe->translate, translate); + } else { + VectorScale(oldframe->scale, backlerp, oldscale); + VectorScale(newframe->scale, frontlerp, newscale); + + LerpVector2(oldframe->translate, newframe->translate, + backlerp, frontlerp, translate); + } } static void setup_color(void) @@ -423,6 +441,50 @@ static void draw_celshading(const glIndex_t *indices, int num_indices) qglLineWidth(1); } +static drawshadow_t cull_shadow(const model_t *model) +{ + const cplane_t *plane; + float radius, d, w; + vec3_t point; + + if (!gl_shadows->integer) + return SHADOW_NO; + + if (glr.ent->flags & (RF_WEAPONMODEL | RF_NOSHADOW)) + return SHADOW_NO; + + setup_color(); + + if (!glr.lightpoint.surf) + return SHADOW_NO; + + // check steepness + plane = &glr.lightpoint.plane; + w = plane->normal[2]; + if (glr.lightpoint.surf->drawflags & DSURF_PLANEBACK) + w = -w; + if (w < 0.5f) + return SHADOW_NO; // too steep + + if (!gl_cull_models->integer) + return SHADOW_YES; + + // project on plane + d = PlaneDiffFast(origin, plane); + VectorMA(origin, -d, plane->normal, point); + + radius = max(model->frames[newframenum].radius, model->frames[oldframenum].radius) / w; + + for (int i = 0; i < 4; i++) { + if (PlaneDiff(point, &glr.frustumPlanes[i]) < -radius) { + c.spheresCulled++; + return SHADOW_NO; // culled out + } + } + + return SHADOW_YES; +} + static void proj_matrix(GLfloat *matrix, const cplane_t *plane, const vec3_t dir) { matrix[ 0] = plane->normal[1] * dir[1] + plane->normal[2] * dir[2]; @@ -451,15 +513,7 @@ static void setup_shadow(void) GLfloat matrix[16], tmp[16]; vec3_t dir; - shadowmatrix[15] = 0; - - if (!gl_shadows->integer) - return; - - if (glr.ent->flags & (RF_WEAPONMODEL | RF_NOSHADOW)) - return; - - if (!glr.lightpoint.surf) + if (!drawshadow) return; // position fake light source straight over the model @@ -479,7 +533,7 @@ static void setup_shadow(void) static void draw_shadow(const glIndex_t *indices, int num_indices) { - if (shadowmatrix[15] < 0.5f) + if (!drawshadow) return; // load shadow projection matrix @@ -542,8 +596,16 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, const maliastc_t *tcoords, int num_verts, image_t **skins, int num_skins) { - glStateBits_t state = GLS_INTENSITY_ENABLE; - const image_t *skin = skin_for_mesh(skins, num_skins); + glStateBits_t state; + const image_t *skin; + + // if the model was culled, just draw the shadow + if (drawshadow == SHADOW_ONLY) { + GL_LockArrays(num_verts); + draw_shadow(indices, num_indices); + GL_UnlockArrays(); + return; + } // fall back to entity matrix GL_LoadMatrix(glr.entmatrix); @@ -563,12 +625,14 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, qglColorMask(1, 1, 1, 1); } + state = GLS_INTENSITY_ENABLE; if (dotshading) state |= GLS_SHADE_SMOOTH; if (glr.ent->flags & RF_TRANSLUCENT) state |= GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE; + skin = skin_for_mesh(skins, num_skins); if (skin->glow_texnum) state |= GLS_GLOWMAP_ENABLE; @@ -781,20 +845,30 @@ void GL_DrawAliasModel(const model_t *model) VectorCopy(ent->origin, origin); - // cull the model, setup scale and translate vectors + // cull the shadow + drawshadow = cull_shadow(model); + + // cull the model if (newframenum == oldframenum) cull = cull_static_model(model); else cull = cull_lerped_model(model); - if (cull == CULL_OUT) - return; + if (cull == CULL_OUT) { + if (!drawshadow) + return; + drawshadow = SHADOW_ONLY; // still need to draw the shadow + } // setup parameters common for all meshes - setup_color(); + if (!drawshadow) + setup_color(); setup_celshading(); setup_dotshading(); setup_shadow(); + // setup scale and translate vectors + setup_frame_scale(model); + // select proper tessfunc if (ent->flags & RF_SHELL_MASK) { shellscale = (ent->flags & RF_WEAPONMODEL) ? From 69f40d0387d74656f90d30c46b7341c12500f9c1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 31 Aug 2024 23:51:47 +0300 Subject: [PATCH 682/974] Initialize shell texture once. --- src/refresh/gl.h | 1 + src/refresh/mesh.c | 5 +---- src/refresh/texture.c | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index ca84fb353..4c907e984 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -724,6 +724,7 @@ bool GL_InitWarpTexture(void); extern cvar_t *gl_intensity; +extern image_t shell_texture; /* * gl_tess.c diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index e06585e87..dd6680a50 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -569,11 +569,8 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) { const entity_t *ent = glr.ent; - if (ent->flags & RF_SHELL_MASK) { - static image_t shell_texture; - shell_texture.texnum = TEXNUM_WHITE; + if (ent->flags & RF_SHELL_MASK) return &shell_texture; - } if (ent->skin) return IMG_ForHandle(ent->skin); diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 8de9b1fd0..87ae8e603 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -47,6 +47,7 @@ static cvar_t *gl_invert; static cvar_t *gl_partshape; cvar_t *gl_intensity; +image_t shell_texture; static int GL_UpscaleLevel(int width, int height, imagetype_t type, imageflags_t flags); static void GL_Upload32(byte *data, int width, int height, int baselevel, imagetype_t type, imageflags_t flags); @@ -1082,6 +1083,7 @@ void GL_InitImages(void) qglGenTextures(NUM_AUTO_TEXTURES, gl_static.texnums); qglGenTextures(LM_MAX_LIGHTMAPS, lm.texnums); + shell_texture.texnum = TEXNUM_WHITE; if (gl_static.use_shaders) { qglGenTextures(1, &gl_static.warp_texture); @@ -1125,6 +1127,7 @@ void GL_ShutdownImages(void) memset(gl_static.texnums, 0, sizeof(gl_static.texnums)); memset(lm.texnums, 0, sizeof(lm.texnums)); + memset(&shell_texture, 0, sizeof(shell_texture)); GL_DeleteWarpTexture(); From bbb8b1f6cf431b3b139121cd9cb617786e821b28 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 1 Sep 2024 14:32:33 +0300 Subject: [PATCH 683/974] Don't call glGetError() each frame by default. --- src/refresh/main.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index cb4a7eee9..5113c7b50 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -723,7 +723,8 @@ void R_RenderFrame(const refdef_t *fd) Draw_Lightmaps(); #endif - GL_ShowErrors(__func__); + if (gl_showerrors->integer > 1) + GL_ShowErrors(__func__); } void R_BeginFrame(void) @@ -738,7 +739,8 @@ void R_BeginFrame(void) if (gl_clear->integer) qglClear(GL_COLOR_BUFFER_BIT); - GL_ShowErrors(__func__); + if (gl_showerrors->integer > 1) + GL_ShowErrors(__func__); } void R_EndFrame(void) @@ -756,7 +758,8 @@ void R_EndFrame(void) if (gl_showtearing->integer) GL_DrawTearing(); - GL_ShowErrors(__func__); + if (gl_showerrors->integer > 1) + GL_ShowErrors(__func__); vid->swap_buffers(); } From 0d88ed7186d41a3337cddd49408a0fda4b142350 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 1 Sep 2024 14:46:24 +0300 Subject: [PATCH 684/974] =?UTF-8?q?Don't=20allow=20=E2=80=98gl=5Fdamageble?= =?UTF-8?q?nd=5Ffrac=E2=80=99=20higher=20than=200.5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/refresh/draw.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index a73574abf..e5eb50299 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -71,7 +71,7 @@ static inline void GL_StretchPic_( #define GL_StretchPic(x,y,w,h,s1,t1,s2,t2,color,image) \ GL_StretchPic_(x,y,w,h,s1,t1,s2,t2,color,(image)->texnum,(image)->flags) -static void GL_DrawVignette(int distance, color_t outer, color_t inner) +static void GL_DrawVignette(float frac, color_t outer, color_t inner) { vec_t *dst_vert; glIndex_t *dst_indices; @@ -85,6 +85,7 @@ static void GL_DrawVignette(int distance, color_t outer, color_t inner) int x = 0, y = 0; int w = glr.fd.width, h = glr.fd.height; + int distance = min(w, h) * frac; // outer vertices dst_vert = tess.vertices + tess.numverts * 5; @@ -183,7 +184,7 @@ void GL_Blend(void) inner.u32 = outer.u32 & U32_RGB; if (gl_damageblend_frac->value > 0) - GL_DrawVignette(min(glr.fd.width, glr.fd.height) * gl_damageblend_frac->value, outer, inner); + GL_DrawVignette(Cvar_ClampValue(gl_damageblend_frac, 0, 0.5f), outer, inner); else GL_StretchPic_(glr.fd.x, glr.fd.y, glr.fd.width, glr.fd.height, 0, 0, 1, 1, outer.u32, TEXNUM_WHITE, 0); From 4b3d34d2e3acba675bbe9f86c0138e004d952bfc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 1 Sep 2024 14:58:44 +0300 Subject: [PATCH 685/974] Simplify GL_DrawVignette(). --- src/refresh/draw.c | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index e5eb50299..0d0b08f6c 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -73,6 +73,9 @@ static inline void GL_StretchPic_( static void GL_DrawVignette(float frac, color_t outer, color_t inner) { + static const byte indices[24] = { + 0, 5, 4, 0, 1, 5, 1, 6, 5, 1, 2, 6, 6, 2, 3, 6, 3, 7, 0, 7, 3, 0, 4, 7 + }; vec_t *dst_vert; glIndex_t *dst_indices; @@ -125,33 +128,8 @@ static void GL_DrawVignette(float frac, color_t outer, color_t inner) */ dst_indices = tess.indices + tess.numindices; - dst_indices[0] = tess.numverts + 0; - dst_indices[1] = tess.numverts + 5; - dst_indices[2] = tess.numverts + 4; - dst_indices[3] = tess.numverts + 0; - dst_indices[4] = tess.numverts + 1; - dst_indices[5] = tess.numverts + 5; - - dst_indices[6] = tess.numverts + 1; - dst_indices[7] = tess.numverts + 6; - dst_indices[8] = tess.numverts + 5; - dst_indices[9] = tess.numverts + 1; - dst_indices[10] = tess.numverts + 2; - dst_indices[11] = tess.numverts + 6; - - dst_indices[12] = tess.numverts + 6; - dst_indices[13] = tess.numverts + 2; - dst_indices[14] = tess.numverts + 3; - dst_indices[15] = tess.numverts + 6; - dst_indices[16] = tess.numverts + 3; - dst_indices[17] = tess.numverts + 7; - - dst_indices[18] = tess.numverts + 0; - dst_indices[19] = tess.numverts + 7; - dst_indices[20] = tess.numverts + 3; - dst_indices[21] = tess.numverts + 0; - dst_indices[22] = tess.numverts + 4; - dst_indices[23] = tess.numverts + 7; + for (int i = 0; i < 24; i++) + dst_indices[i] = tess.numverts + indices[i]; tess.flags |= GLS_BLEND_BLEND | GLS_SHADE_SMOOTH; From 5dd1a427fa775d9c5f5c88abf683ead2e5200e62 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 1 Sep 2024 15:45:25 +0300 Subject: [PATCH 686/974] Simplify expression. --- src/refresh/draw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/draw.c b/src/refresh/draw.c index 0d0b08f6c..95c90e159 100644 --- a/src/refresh/draw.c +++ b/src/refresh/draw.c @@ -362,7 +362,7 @@ static inline void draw_char(int x, int y, int flags, int c, const image_t *imag t = (c >> 4) * 0.0625f; if (gl_fontshadow->integer > 0 && c != 0x83) { - uint32_t black = MakeColor(0, 0, 0, draw.colors[0].u8[3]); + uint32_t black = draw.colors[0].u32 & U32_ALPHA; GL_StretchPic(x + 1, y + 1, CHAR_WIDTH, CHAR_HEIGHT, s, t, s + 0.0625f, t + 0.0625f, black, image); From 8743b37cb51d86edb6227d5e9a6c2d2aa0d79997 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 1 Sep 2024 21:14:22 -0400 Subject: [PATCH 687/974] Added a bunch of skill checks --- src/action/acesrc/acebot_ai.c | 23 +++++-- src/action/botlib/botlib.h | 6 +- src/action/botlib/botlib_ai.c | 73 ++++++++------------ src/action/botlib/botlib_communication.c | 21 ++---- src/action/botlib/botlib_items.c | 2 +- src/action/botlib/botlib_movement.c | 10 +-- src/action/botlib/botlib_spawn.c | 5 +- src/action/botlib/botlib_utils.c | 87 +++++++++++++++++++++++- src/action/g_local.h | 26 ++++++- src/action/p_client.c | 2 + 10 files changed, 177 insertions(+), 78 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index cb8642b43..3e6d9a489 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -1119,15 +1119,20 @@ qboolean BOTLIB_FindEnemy(edict_t *self) trace_t tr = gi.trace(eyes, NULL, NULL, enemy_eyes, self, MASK_SHOT); if (tr.ent && tr.ent->health > 0 && tr.ent->solid == SOLID_BBOX) { + // Minimum time to remember an enemy is 15 seconds, maximum is 30 seconds + float remember_time = min((BOTLIB_SkillMultiplier(self->bot.skill.overall, true) * 15), 15); + if (remember_time > 30) + remember_time = 30; + VectorCopy(self->enemy->s.origin, self->bot.enemy_seen_loc); // Update last seen location - self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_seen_time = level.framenum + remember_time * HZ; // Update the last time we saw the enemy self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); //Com_Printf("%s %s see %s R[%d vs %d]L\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time, level.framenum); - if (self->bot.reaction_time > level.framenum) + if (self->bot.skill.reaction > level.framenum) { //Com_Printf("%s %s no fire R[%d vs %d]L\n", __func__, self->client->pers.netname, self->bot.reaction_time, level.framenum); return false; @@ -1278,13 +1283,18 @@ qboolean BOTLIB_FindEnemy(edict_t *self) // If we found an enemy if (bestenemy != NULL) { + // Minimum time to remember an enemy is 15 seconds, maximum is 30 seconds + float remember_time = min((BOTLIB_SkillMultiplier(self->bot.skill.overall, true) * 15), 15); + if (remember_time > 30) + remember_time = 30; + self->enemy = bestenemy; - self->bot.enemy_seen_time = level.framenum + bot_remember->value * HZ; // Update the last time we saw the enemy + self->bot.enemy_seen_time = level.framenum + remember_time * HZ; // Update the last time we saw the enemy self->bot.enemy_in_xhair = BOTLIB_IsAimingAt(self, self->enemy); self->bot.enemy_dist = VectorDistance(self->s.origin, self->enemy->s.origin); self->bot.enemy_height_diff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); - self->bot.reaction_time = level.framenum + 0.75 * HZ; + self->bot.reaction_time = BOTLIB_SKILL_Reaction(self->bot.skill.reaction) * HZ; //Com_Printf("%s %s see %s R[%d vs %d]L\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->bot.reaction_time, level.framenum); /* @@ -1450,13 +1460,14 @@ qboolean BOTLIB_FindEnemy(edict_t *self) //R #endif + float reaction_multiplier = BOTLIB_SKILL_Reaction(self->bot.skill.reaction); if (0) { self->bot.enemy_in_xhair = false; self->bot.enemy_dist = 0; self->bot.enemy_height_diff = 0; self->bot.enemy_seen_time = 0; - self->bot.reaction_time = bot_reaction->value; + self->bot.reaction_time = reaction_multiplier * HZ; self->enemy = NULL; // Reset any enemies, including any the bot remembered... return false; } @@ -1483,7 +1494,7 @@ qboolean BOTLIB_FindEnemy(edict_t *self) self->bot.enemy_dist = 0; self->bot.enemy_height_diff = 0; self->bot.enemy_seen_time = 0; - self->bot.reaction_time = bot_reaction->value; + self->bot.reaction_time = reaction_multiplier; self->enemy = NULL; // Reset any enemies, including any the bot remembered... } diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 7694f116e..1a727601c 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -525,8 +525,12 @@ int BOTLIB_GetEquipment(edict_t* self); //rekkie -- collecting weapons, items, ammo -- e // =========================================================================== -// botlib_debug.c +// botlib_utils.c // =========================================================================== +void BOTLIB_SKILL_Init(edict_t* bot); +qboolean BOTLIB_SkillChance(float skilltype); +float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill); +float BOTLIB_SKILL_Reaction(float reaction_skill); void BOTLIB_Debug(const char *debugmsg, ...); // =========================================================================== diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index eee3c5fb6..bd3b4f305 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -23,15 +23,14 @@ void BOTLIB_Init(edict_t* self) self->client->resp.radio.gender = (self->client->pers.gender == GENDER_FEMALE) ? 1 : 0; - - // Save previous data - float prev_skill = self->bot.skill; + // Save previous data (TODO: copy entire bot.skill struct) + float prev_skill = self->bot.skill.overall; int pId = self->bot.personality.pId; memset(&self->bot, 0, sizeof(bot_t)); // Restore previous data - self->bot.skill = prev_skill; + self->bot.skill.overall = prev_skill; self->bot.personality.pId = pId; // Ping @@ -44,9 +43,6 @@ void BOTLIB_Init(edict_t* self) else self->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - - - // Enemies self->bot.old_enemy = NULL; self->bot.enemy_in_xhair = false; @@ -54,7 +50,6 @@ void BOTLIB_Init(edict_t* self) self->bot.enemy_height_diff = 0; self->bot.enemy_seen_time = 0; self->bot.enemy_chase_time = 0; - self->bot.reaction_time = bot_reaction->value; //memset(&self->bot.noises, 0, sizeof(int) * MAX_CLIENTS); //memset(&self->bot.noise_time, 0, sizeof(int) * MAX_CLIENTS); @@ -106,9 +101,9 @@ void BOTLIB_Init(edict_t* self) // Simple adjustments for now int skillPlus = 0; skillPlus += self->bot.personality.map_prefs; - self->bot.skill += skillPlus; // This will decrease skill if map_prefs is negative, max -1 - if (self->bot.skill > MAX_BOTSKILL) - self->bot.skill = MAX_BOTSKILL; + self->bot.skill.overall += skillPlus; // This will decrease skill if map_prefs is negative, max -1 + if (self->bot.skill.overall > MAX_BOTSKILL) + self->bot.skill.overall = MAX_BOTSKILL; } } @@ -474,20 +469,6 @@ void BOTLIB_BotInputToUserCommand(edict_t* ent, bot_input_t* bi, usercmd_t* ucmd ent->bot.bi.actionflags = 0; } -// Automatic adjustment of bot skills based on their current score compared to all other bots -/* -typedef struct { - int playernum; - int score; -} PlayerScores; -// Comparison function used by qsort() -int BOTLIB_ScoreCompare(const void* a, const void* b) -{ - const PlayerScores* p1 = (PlayerScores*)a; - const PlayerScores* p2 = (PlayerScores*)b; - return p2->score - p1->score; // Sort ascendingly by score -} -*/ int BOTLIB_AutoAdjustSkill(edict_t * self) { int highest_score = 0; // Highest player score @@ -495,8 +476,8 @@ int BOTLIB_AutoAdjustSkill(edict_t * self) float score_percent_diff = 1.0; // Difference in percent between player and best player's score //Init the variable bot skill - if (self->bot.skill < 1) - self->bot.skill = bot_skill->value; + if (self->bot.skill.overall < 1) + self->bot.skill.overall = bot_skill->value; if (self->client == NULL) return 0; @@ -525,36 +506,36 @@ int BOTLIB_AutoAdjustSkill(edict_t * self) if (score_percent_diff >= 0.75) { - self->bot.skill = bot_skill->value; + self->bot.skill.overall = bot_skill->value; //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); return 0; // Normal } else if (score_percent_diff >= 0.5) { - self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + self->bot.skill.overall = self->bot.skill.overall + 0.25; // Increase bot skill slower - if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill - self->bot.skill = bot_skill->value; // Scale skill back down to base level + if (self->bot.skill.overall > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill.overall = bot_skill->value; // Scale skill back down to base level - //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill.overall); return 1; // Okay } else if (score_percent_diff < 0.5) { - self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + self->bot.skill.overall = self->bot.skill.overall + 0.5; // Increase bot skill faster - if (self->bot.skill > (MAX_BOTSKILL - 1)) // Max skill - self->bot.skill = bot_skill->value; // Scale skill back down to base level + if (self->bot.skill.overall > (MAX_BOTSKILL - 1)) // Max skill + self->bot.skill.overall = bot_skill->value; // Scale skill back down to base level - //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill); + //Com_Printf("%s %s adjusting bot skill [+] [%f]\n", __func__, self->client->pers.netname, self->bot.skill.overall); return 2; // Poor } } } - self->bot.skill = bot_skill->value; + self->bot.skill.overall = bot_skill->value; //Com_Printf("%s %s adjusting bot skill [-] [%f]\n", __func__, self->client->pers.netname, bot_skill->value); return 0; // Normal @@ -616,25 +597,25 @@ int BOTLIB_AutoAdjustSkill(edict_t * self) // If bot score is in the bottom half (50% or greater) adjust its skill so it can aim to be in the top 10% if (percent >= 0.75) // Higher is worse { - self->bot.skill = self->bot.skill + 0.5; // Increase bot skill faster + self->bot.skill.overall = self->bot.skill.overall + 0.5; // Increase bot skill faster - if (self->bot.skill > MAX_BOTSKILL) // Max skill - self->bot.skill = MAX_BOTSKILL; + if (self->bot.skill.overall > MAX_BOTSKILL) // Max skill + self->bot.skill.overall = MAX_BOTSKILL; - //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill.overall); } else if (percent >= 0.5) // Higher is worse { - self->bot.skill = self->bot.skill + 0.25; // Increase bot skill slower + self->bot.skill.overall = self->bot.skill.overall + 0.25; // Increase bot skill slower - if (self->bot.skill > MAX_BOTSKILL) // Max skill - self->bot.skill = MAX_BOTSKILL; + if (self->bot.skill.overall > MAX_BOTSKILL) // Max skill + self->bot.skill.overall = MAX_BOTSKILL; - //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill); + //Com_Printf("%s %s adjusting bot skill [+] [%d]\n", __func__, self->client->pers.netname, (int)self->bot.skill.overall); } else // Return skill back to base level { - self->bot.skill = bot_skill->value; + self->bot.skill.overall = bot_skill->value; //Com_Printf("%s %s adjusting bot skill [-] [%d]\n", __func__, self->client->pers.netname, (int)bot_skill->value); } diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 98e8736cd..96705f012 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -373,31 +373,20 @@ void BOTLIB_RadioBroadcast(edict_t* ent, edict_t* partner, char* msg) qboolean found = false; radio_t* radio; + // Dead shit doesn't broadcast if (!IS_ALIVE(ent)) return; + // Check if the bot should communicate based on skill level + if (!BOTLIB_SkillChance((ent->bot.skill.communication + ent->bot.skill.teamwork))) + return; + if (!teamplay->value) { if (!DMFLAGS((DF_MODELTEAMS | DF_SKINTEAMS))) return; // don't allow in a non-team setup... } - /* - //TempFile END - //AQ2:TNG Slicer - if (radio_repeat->value) - { //SLIC2 Optimization - if (CheckForRepeat(ent, i) == false) - return; - } - - if (radio_max->value) - { - if (CheckForFlood(ent) == false) - return; - } - */ - radio = &ent->client->resp.radio; if (radio->gender) { diff --git a/src/action/botlib/botlib_items.c b/src/action/botlib/botlib_items.c index 89a17017c..65791c0e3 100644 --- a/src/action/botlib/botlib_items.c +++ b/src/action/botlib/botlib_items.c @@ -12,7 +12,7 @@ void BOTLIB_SmartWeaponSelection(edict_t *self) if (bot_skill_threshold->value > 0) weapon_skill_choice = BOTLIB_AutoAdjustSkill(self); else - self->bot.skill = bot_skill->value; + self->bot.skill.overall = bot_skill->value; // Allowed weapons: ALL // Allowed items: ALL diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index bf6fb81ed..7c43f7681 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5237,7 +5237,7 @@ void BOTLIB_CrouchFire(edict_t* self) gitem_t* clweapon = self->client->weapon; // More skillful bots will crouch when firing - if (self->bot.skill >= 5) { + if (self->bot.skill.aim >= 0) { if ((self->bot.bi.actionflags & ACTION_MOVELEFT) == 0 && (self->bot.bi.actionflags & ACTION_MOVERIGHT) == 0) { @@ -5903,14 +5903,14 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) self->just_spawned_timeout = 0; // No wait } else { // bot_personality is enabled, let's make it more realistic int rnd_rng = rand() % 4; - float skill_factor = 1.0f - (self->bot.skill / 10.0f); // Scale factor based on skill (0 to 1) + float skill_factor = (self->bot.skill.movement / 10.0f); // Scale factor based on skill (0 to 1) if (rnd_rng == 0) - self->just_spawned_timeout = level.framenum + (random() * 10 * skill_factor) * HZ; // Long wait + self->just_spawned_timeout = level.framenum + (random() * 10 * (1.0f - skill_factor)) * HZ; // Long wait else if (rnd_rng == 1) - self->just_spawned_timeout = level.framenum + (random() * 5 * skill_factor) * HZ; // Medium wait + self->just_spawned_timeout = level.framenum + (random() * 5 * (1.0f - skill_factor)) * HZ; // Medium wait else if (rnd_rng == 2) - self->just_spawned_timeout = level.framenum + (random() * 2 * skill_factor) * HZ; // Short wait + self->just_spawned_timeout = level.framenum + (random() * 2 * (1.0f - skill_factor)) * HZ; // Short wait else self->just_spawned_timeout = 0; // No wait } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 5d7cba246..be2a6f569 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1516,7 +1516,10 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for } bot->is_bot = true; - bot->yaw_speed = 1000; // aiming degrees per second + int base_yaw_speed = 1000; + int aim_speed = (base_yaw_speed * BOTLIB_SkillMultiplier(bot->bot.skill.aim, true)); + bot->yaw_speed = aim_speed; + //bot->yaw_speed = 1000; // aiming degrees per second if (team == TEAM1) bot_connections.total_team1++; diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index beee25718..aebf7b8d0 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -4,9 +4,94 @@ /* This file is for common utilties that are used by the botlib functions - */ +void BOTLIB_SKILL_Init(edict_t* bot) +{ + // Initialize random skill levels + if (!bot_personality->value) { + bot->bot.skill.overall = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.aim = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.reaction = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.movement = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.teamwork = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.communication = ((float)rand() / RAND_MAX) * 2 - 1; + bot->bot.skill.map_skill = ((float)rand() / RAND_MAX) * 2 - 1; + int i; + for (i = 0; i < WEAPON_COUNT; i++) { + bot->bot.skill.weapon_skill[i] = ((float)rand() / RAND_MAX) * 2 - 1; + } + } else { // We know what values to provide if we have a personality + //copy the personality data + } + gi.dprintf("%s's bot skills:\noverall: %f\naim: %f\nreaction: %f\nmovement: %f\nteamwork: %f\ncommunication: %f\nmap_skill: %f\n", + bot->client->pers.netname, bot->bot.skill.overall, bot->bot.skill.aim, bot->bot.skill.reaction, bot->bot.skill.movement, bot->bot.skill.teamwork, bot->bot.skill.communication, bot->bot.skill.map_skill); + +} + +// Returns true or false depending on if the bot passes the skill check +// Higher values of skilltype will have better chance of returning true +qboolean BOTLIB_SkillChance(float skill_level) +{ + // Normalize skill_level from range [-1, 1] to [0, 1] + float normalized_skill_level = (skill_level + 1) / 2.0; + + // Generate a random float between 0 and 1 + float random_value = (float)rand() / RAND_MAX; + + // Return true if random_value is less than or equal to normalized_skill_level + return random_value <= normalized_skill_level; +} + +// This function will return a multiplier based on the skill level +float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill) +{ + // Seed the random number generator + srand(time(NULL)); + + // Normalize the skill level to be between 0.0 and 1.0 + if (skill_level < -1.0) + skill_level = -1.0; + else if (skill_level > 1.0) + skill_level = 1.0; + + // Define the base multiplier range + float min_multiplier = 1.0; + float max_multiplier = 2.0; + + // Generate a random base multiplier + float base_multiplier = min_multiplier + ((float)rand() / RAND_MAX) * (max_multiplier - min_multiplier); + + // Adjust the multiplier based on the normalized skill level + float final_multiplier; + if (increase_with_skill) + { + final_multiplier = base_multiplier * (1.0 + skill_level); + } + else + { + final_multiplier = base_multiplier * (1.0 - skill_level); + } + + return final_multiplier; +} + +float BOTLIB_SKILL_Reaction(float reaction_skill) +{ + // Normalize skill level to be between 0 and 1 + float normalized_skill = (reaction_skill + 1.0) / 2.0; + + // Use BOTLIB_SkillMultiplier for randomization + float reaction_multiplier = BOTLIB_SkillMultiplier(normalized_skill, false); + + // Scale to fit within the range of 0.2 to 1 + float floor = 0.2; + float ceiling = 1.0; + reaction_multiplier = floor + (reaction_multiplier * (ceiling - floor)); + + return reaction_multiplier; +} +// Generic debug function in relation to bots void BOTLIB_Debug(const char *debugmsg, ...) { if (!bot_debug->value) diff --git a/src/action/g_local.h b/src/action/g_local.h index da8da91c8..8b201dbb8 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2220,6 +2220,30 @@ typedef struct bot_personality_s } bot_personality_t; +typedef enum bot_skill_types +{ + BOT_SKILL_OVERALL, + BOT_SKILL_AIM, + BOT_SKILL_REACTION, + BOT_SKILL_MOVEMENT, + BOT_SKILL_TEAMWORK, + BOT_SKILL_COMMUNICATION, + BOT_SKILL_WEAPON, + BOT_SKILL_MAP, +} bot_skill_types_t; + +typedef struct bot_skill_s +{ + float overall; // Overall skill + float aim; // Aim skill + float reaction; // Reaction skill + float movement; // Movement skill + float teamwork; // Teamwork skill + float communication; // Communication skill + float weapon_skill[WEAPON_COUNT]; // Weapon skill + float map_skill; // Map skill +} bot_skill_t; + typedef struct bot_s { int bot_type; @@ -2295,7 +2319,7 @@ typedef struct bot_s int last_weapon_reload_time; // Delay time between reloading weapons // Skill - float skill; // Variable bot skill. Allow the bot to increase or decrease its own skill based on its score (kills) and bot_skill + bot_skill_t skill; // Variable bot skill levels // Items edict_t *get_item; // The current item the bot wants and is located next or or inside of a node diff --git a/src/action/p_client.c b/src/action/p_client.c index f343283bd..74d603915 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3314,6 +3314,8 @@ void ClientBeginDeathmatch(edict_t * ent) #ifndef NO_BOTS ACEIT_RebuildPlayerList(); + BOTLIB_SKILL_Init(ent); // Initialize the skill levels + #if USE_AQTION StatBotCheck(); #if USE_AQTION From 889d3ec9b251812f7202b77940a5a175176c10ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 2 Sep 2024 16:44:59 +0300 Subject: [PATCH 688/974] Fix nasty debug texts bug. --- src/refresh/debug.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 8da73c887..ff9515d6f 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -526,7 +526,7 @@ static void GL_DrawDebugTexts(void) if (text->time < com_localTime) { // expired List_Remove(&text->entry); - List_Insert(&debug_lines_free, &text->entry); + List_Insert(&debug_texts_free, &text->entry); continue; } From 60519b2dfb110948933f519035a7a26b015c9e6c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 2 Sep 2024 16:46:00 +0300 Subject: [PATCH 689/974] Allow multi-line debug texts. --- src/refresh/debug.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index ff9515d6f..db13aa23d 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -554,9 +554,15 @@ static void GL_DrawDebugTexts(void) } VectorCopy(text->origin, pos); + i = 0; s = text->text; while (*s) { byte c = *s++; + if (c == '\n') { + i++; + VectorMA(text->origin, i, down, pos); + continue; + } GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); VectorAdd(pos, right, pos); } From c0533ebf388f50a768695976fa6a4dd8af152502 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:08:00 -0400 Subject: [PATCH 690/974] Trying an x11 disabled build for wayland --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4acf2358..8b084bf27 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,7 @@ env: MESON_ARGS_LINUX_WAYLAND: >- -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback + -Dx11=disabled MESON_ARGS_LINUX_X11: >- -Dwindows-crash-dumps=disabled From 3f69ccb9acd01dfd96a1b6b56207f33f7ade10cb Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:15:18 -0400 Subject: [PATCH 691/974] Trying adding -lGL to non-x11 builds? --- src/unix/meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/unix/meson.build b/src/unix/meson.build index 924f9263a..90ad54647 100644 --- a/src/unix/meson.build +++ b/src/unix/meson.build @@ -30,6 +30,10 @@ if not config.has('USE_SDL') and not config.has('USE_X11') and not config.has('U client_deps += disabler() endif +if not config.has('USE_X11') + exe_link_args += '-lGL' +endif + test_funcs = [ 'memccpy', 'strchrnul', From 97b45b99405cab33b328f605bf2d340add481488 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:19:45 -0400 Subject: [PATCH 692/974] Restricting -lGL linker to Linux builds --- src/unix/meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unix/meson.build b/src/unix/meson.build index 90ad54647..9013367f3 100644 --- a/src/unix/meson.build +++ b/src/unix/meson.build @@ -30,7 +30,8 @@ if not config.has('USE_SDL') and not config.has('USE_X11') and not config.has('U client_deps += disabler() endif -if not config.has('USE_X11') +# This is needed due to specific OpenGL-specific symbols being used in the client for navmesh drawing +if not config.has('USE_X11') and host_machine.system() == 'linux' exe_link_args += '-lGL' endif From 1a656b388a2682377a902cdea95b470ef168f2ce Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:38:27 -0400 Subject: [PATCH 693/974] Some cleanup and added more comments --- src/action/botlib/botlib_utils.c | 4 +++- src/action/p_client.c | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index aebf7b8d0..75fb1c520 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -22,7 +22,7 @@ void BOTLIB_SKILL_Init(edict_t* bot) bot->bot.skill.weapon_skill[i] = ((float)rand() / RAND_MAX) * 2 - 1; } } else { // We know what values to provide if we have a personality - //copy the personality data + //copy the personality data when it's implemented } gi.dprintf("%s's bot skills:\noverall: %f\naim: %f\nreaction: %f\nmovement: %f\nteamwork: %f\ncommunication: %f\nmap_skill: %f\n", bot->client->pers.netname, bot->bot.skill.overall, bot->bot.skill.aim, bot->bot.skill.reaction, bot->bot.skill.movement, bot->bot.skill.teamwork, bot->bot.skill.communication, bot->bot.skill.map_skill); @@ -44,6 +44,8 @@ qboolean BOTLIB_SkillChance(float skill_level) } // This function will return a multiplier based on the skill level +// Set increase_with_skill to true if you want the multiplier to increase with skill level (ex: aim) +// Set increase_with_skill to false if you want the multiplier to decrease with skill level (ex: reaction time) float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill) { // Seed the random number generator diff --git a/src/action/p_client.c b/src/action/p_client.c index 74d603915..26462e288 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3314,15 +3314,11 @@ void ClientBeginDeathmatch(edict_t * ent) #ifndef NO_BOTS ACEIT_RebuildPlayerList(); - BOTLIB_SKILL_Init(ent); // Initialize the skill levels + if (ent->is_bot) + BOTLIB_SKILL_Init(ent); // Initialize the skill levels #if USE_AQTION StatBotCheck(); - #if USE_AQTION - if(am->value){ - //attract_mode_bot_check(); - } - #endif #endif #endif From 80e527841ec9efea28a72b4748cdc99f48fc8f9a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:52:56 -0400 Subject: [PATCH 694/974] Ensuring GL is linked for the new symbols --- src/unix/meson.build | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/unix/meson.build b/src/unix/meson.build index 9013367f3..23c43bf25 100644 --- a/src/unix/meson.build +++ b/src/unix/meson.build @@ -31,8 +31,12 @@ if not config.has('USE_SDL') and not config.has('USE_X11') and not config.has('U endif # This is needed due to specific OpenGL-specific symbols being used in the client for navmesh drawing -if not config.has('USE_X11') and host_machine.system() == 'linux' +if not config.has('USE_X11') + if host_machine.system() == 'linux' exe_link_args += '-lGL' + # else + # exe_link_args += ['-framework', 'OpenGL'] # MacOS does not need this, but in case you ever wanted to know how + endif endif test_funcs = [ From 3cb66a6f4bb8c1a926f923ac8ad68bbc742c342e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 2 Sep 2024 17:56:09 -0400 Subject: [PATCH 695/974] Trying one build with x11 disabled --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b084bf27..0752b61bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,7 @@ env: MESON_ARGS_LINUX_X11: >- -Dwindows-crash-dumps=disabled + -Dx11=disabled -Dwayland=disabled -Dwrap_mode=nofallback From f1082aeedf2d62b3e5fb62d6bc198d83e3c5738a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 3 Sep 2024 16:55:34 +0300 Subject: [PATCH 696/974] Don't crash if game returns NULL extended API exports. --- src/server/game.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/game.c b/src/server/game.c index a027fda52..68ab7cc9e 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1007,10 +1007,10 @@ void SV_InitGameProgs(void) game_entry_ex_t entry_ex = Sys_GetProcAddress(game_library, "GetGameAPIEx"); if (entry_ex) { gex = entry_ex(&game_import_ex); - if (gex->apiversion < GAME_API_VERSION_EX_MINIMUM) - gex = NULL; - else + if (gex && gex->apiversion >= GAME_API_VERSION_EX_MINIMUM) Com_DPrintf("Game supports Q2PRO extended API version %d.\n", gex->apiversion); + else + gex = NULL; } // initialize From 270fd5e2b2d8d98e68b8c693e9231f667107f415 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Tue, 3 Sep 2024 19:23:06 -0400 Subject: [PATCH 697/974] Testing out a Windows EGL build and combined Linux build --- .github/workflows/build.yml | 62 +++++++------------------------------ 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0752b61bb..36fa27244 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,19 +23,23 @@ env: MESON_ARGS_WIN: >- -Dsdl2=disabled + -Dx11=disabled -Dwayland=disabled + -Dwindows-egl=enabled -Dwrap_mode=forcefallback - -Dx11=disabled - MESON_ARGS_LINUX_WAYLAND: >- + MESON_ARGS_LINUX: >- + -Dx11=enabled + -Dwayland=enabled + -Dsdl2=enabled -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback - -Dx11=disabled MESON_ARGS_LINUX_X11: >- - -Dwindows-crash-dumps=disabled -Dx11=disabled -Dwayland=disabled + -Dsdl2=enabled + -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback MESON_ARGS_MAC: >- @@ -118,51 +122,7 @@ jobs: builddir/q2proded.exe builddir/gamex86_64.dll - linux64-x11: - runs-on: ubuntu-20.04 - #runs-on: [self-hosted, x86] - strategy: - matrix: - cc: [gcc, clang] - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y python3 python3-pip libsdl2-dev libopenal-dev \ - libpng-dev libjpeg-dev zlib1g-dev mesa-common-dev \ - libcurl4-openssl-dev libx11-dev libxi-dev \ - libavcodec-dev libavformat-dev libavutil-dev \ - libswresample-dev libswscale-dev \ - uuid-dev patchelf libjansson-dev - pip3 install meson ninja - - - name: Build - run: | - meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX_X11 }} builddir - meson compile -C builddir - env: - CC: ${{ matrix.cc }} - - - name: patchelf path to discord_game_sdk.so - run: | - patchelf --replace-needed /home/runner/work/q2pro/q2pro/extern/discord/lib/x86_64/discord_game_sdk.so ./discord_game_sdk.so builddir/q2pro - - - name: Set binaries as executable - run: | - chmod +x builddir/q2pro* - - - name: Generate Linux x64 archives - uses: actions/upload-artifact@v4 - with: - name: q2pro-lin-${{ matrix.cc }}-x86_64 - path: | - builddir/q2pro - builddir/q2proded - builddir/gamex86_64.so - - linux64-wayland: + linux64: runs-on: ubuntu-22.04 #runs-on: [self-hosted, x86] strategy: @@ -184,7 +144,7 @@ jobs: - name: Build run: | - meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX_WAYLAND }} builddir + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX }} builddir meson compile -C builddir env: CC: ${{ matrix.cc }} @@ -225,7 +185,7 @@ jobs: - name: Build run: | - meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX_X11 }} builddir + meson setup ${{ env.MESON_ARGS }} ${{ env.MESON_ARGS_LINUX }} builddir meson compile -vC builddir env: CC: ${{ matrix.cc }} From 7f974cacfe9707a5a0333bb59e9c67d8f379c06f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Tue, 3 Sep 2024 19:27:30 -0400 Subject: [PATCH 698/974] Testing out a Windows EGL build and combined Linux build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36fa27244..c37c42a4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ env: -Dsdl2=disabled -Dx11=disabled -Dwayland=disabled - -Dwindows-egl=enabled + -Dwindows-egl=true -Dwrap_mode=forcefallback MESON_ARGS_LINUX: >- From 4e5667bef62cb8f691b1bca384077e3ae5aa1420 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 4 Sep 2024 13:47:11 -0400 Subject: [PATCH 699/974] Simplified build/release by combining all Linux --- .github/workflows/build.yml | 9 +-------- .github/workflows/release.yml | 16 ---------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c37c42a4f..3561cf952 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,13 +35,6 @@ env: -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback - MESON_ARGS_LINUX_X11: >- - -Dx11=disabled - -Dwayland=disabled - -Dsdl2=enabled - -Dwindows-crash-dumps=disabled - -Dwrap_mode=nofallback - MESON_ARGS_MAC: >- -Dwindows-crash-dumps=disabled -Dwrap_mode=nofallback @@ -160,7 +153,7 @@ jobs: - name: Generate Linux x64 archives uses: actions/upload-artifact@v4 with: - name: q2pro-lin-wayland-${{ matrix.cc }}-x86_64 + name: q2pro-lin-${{ matrix.cc }}-x86_64 path: | builddir/q2pro builddir/q2proded diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8ed8a523..ec6d83f12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,20 +44,6 @@ jobs: workflow: build.yml name: q2pro-lin-gcc-x86_64 skip_unpack: true - - - name: Download Linux Wayland x64 clang build artifacts - uses: dawidd6/action-download-artifact@v2 - with: - workflow: build.yml - name: q2pro-lin-wayland-clang-x86_64 - skip_unpack: true - - - name: Download Linux Wayland x64 gcc build artifacts - uses: dawidd6/action-download-artifact@v2 - with: - workflow: build.yml - name: q2pro-lin-wayland-gcc-x86_64 - skip_unpack: true - name: Download Linux ARM x64 build artifacts uses: dawidd6/action-download-artifact@v2 @@ -121,7 +107,5 @@ jobs: q2pro-lin-arm64.zip q2pro-lin-gcc-x86_64.zip q2pro-lin-clang-x86_64.zip - q2pro-lin-wayland-gcc-x86_64.zip - q2pro-lin-wayland-clang-x86_64.zip q2pro-darwin-x86_64.zip q2pro-darwin-arm64.zip From f0cf753d24ac562876b8c55d798fe79755f8890a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 4 Sep 2024 14:43:22 -0400 Subject: [PATCH 700/974] Set bot skill prints behind bot_debug --- src/action/botlib/botlib_utils.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 75fb1c520..1d8022a64 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -24,9 +24,10 @@ void BOTLIB_SKILL_Init(edict_t* bot) } else { // We know what values to provide if we have a personality //copy the personality data when it's implemented } - gi.dprintf("%s's bot skills:\noverall: %f\naim: %f\nreaction: %f\nmovement: %f\nteamwork: %f\ncommunication: %f\nmap_skill: %f\n", - bot->client->pers.netname, bot->bot.skill.overall, bot->bot.skill.aim, bot->bot.skill.reaction, bot->bot.skill.movement, bot->bot.skill.teamwork, bot->bot.skill.communication, bot->bot.skill.map_skill); - + if (bot_debug->value) { + gi.dprintf("%s's bot skills:\noverall: %f\naim: %f\nreaction: %f\nmovement: %f\nteamwork: %f\ncommunication: %f\nmap_skill: %f\n", + bot->client->pers.netname, bot->bot.skill.overall, bot->bot.skill.aim, bot->bot.skill.reaction, bot->bot.skill.movement, bot->bot.skill.teamwork, bot->bot.skill.communication, bot->bot.skill.map_skill); + } } // Returns true or false depending on if the bot passes the skill check From d01907496bd763e5b3cd0ba1ebd584eb33bf96eb Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 3 Sep 2024 16:56:30 +0300 Subject: [PATCH 701/974] Snap initial client prediction position. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes client prediction can get stuck on entity, resulting in jerky movement. For example, this happens when standing on top of a monster in negative Z space due to 1/8 entity origin quantization. Set ‘pm.snapinitial’ on the client to help it get unstuck. --- src/client/predict.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/predict.c b/src/client/predict.c index aac6ff5fa..2fd887ea8 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -231,11 +231,13 @@ void CL_PredictMovement(void) pm.trace = CL_PMTrace; pm.pointcontents = CL_PointContents; pm.s = cl.frame.ps.pmove; + pm.snapinitial = qtrue; // run frames while (++ack <= current) { pm.cmd = cl.cmds[ack & CMD_MASK]; PmoveNew(&pm, &cl.pmp); + pm.snapinitial = qfalse; // save for debug checking VectorCopy(pm.s.origin, cl.predicted_origins[ack & CMD_MASK]); From 191b9022fd48898826804753796001a17665f618 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 3 Sep 2024 22:23:56 +0300 Subject: [PATCH 702/974] Add com_localTime2 that doesn't run if paused. Use it for slow random and debug drawing. --- inc/common/common.h | 1 + src/common/common.c | 2 ++ src/common/utils.c | 4 ++-- src/refresh/debug.c | 16 ++++++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/inc/common/common.h b/inc/common/common.h index f5d7fbc4b..cb4d9c268 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -199,6 +199,7 @@ extern const char com_version_string[]; extern unsigned com_framenum; extern unsigned com_eventTime; // system time of the last event extern unsigned com_localTime; // milliseconds since Q2 startup +extern unsigned com_localTime2; // milliseconds since Q2 startup, but doesn't run if paused extern bool com_initialized; extern time_t com_startTime; diff --git a/src/common/common.c b/src/common/common.c index 6d96bbd44..4e3d76ca8 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -131,6 +131,7 @@ const char com_version_string[] = unsigned com_framenum; unsigned com_eventTime; unsigned com_localTime; +unsigned com_localTime2; bool com_initialized; time_t com_startTime; @@ -1112,6 +1113,7 @@ void Qcommon_Frame(void) // run local time com_localTime += msec; + com_localTime2 += msec & (sv_paused->integer - 1); com_framenum++; #if USE_CLIENT diff --git a/src/common/utils.c b/src/common/utils.c index 7fdce9256..6567d0297 100644 --- a/src/common/utils.c +++ b/src/common/utils.c @@ -641,8 +641,8 @@ uint32_t Com_SlowRand(void) static uint32_t com_rand_frame; // see if it's time to reseed - if (com_rand_ts != com_eventTime / 16 && !sv_paused->integer) { - com_rand_ts = com_eventTime / 16; + if (com_rand_ts != com_localTime2 / 16) { + com_rand_ts = com_localTime2 / 16; com_rand_base = Q_rand(); } diff --git a/src/refresh/debug.c b/src/refresh/debug.c index db13aa23d..9ba942301 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -73,7 +73,7 @@ static void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, } else { debug_line_t *next; LIST_FOR_EACH_SAFE(debug_line_t, l, next, &debug_lines_active, entry) { - if (l->time <= com_localTime) { + if (l->time <= com_localTime2) { List_Remove(&l->entry); List_Insert(&debug_lines_free, &l->entry); } @@ -93,8 +93,8 @@ static void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, VectorCopy(start, l->start); VectorCopy(end, l->end); l->color = color; - l->time = com_localTime + time; - if (l->time < com_localTime) + l->time = com_localTime2 + time; + if (l->time < com_localTime2) l->time = UINT32_MAX; l->bits = GLS_DEPTHMASK_FALSE; if (!depth_test) @@ -340,7 +340,7 @@ static void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char } else { debug_text_t *next; LIST_FOR_EACH_SAFE(debug_text_t, t, next, &debug_texts_active, entry) { - if (t->time <= com_localTime) { + if (t->time <= com_localTime2) { List_Remove(&t->entry); List_Insert(&debug_texts_free, &t->entry); } @@ -362,8 +362,8 @@ static void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char VectorCopy(angles, t->angles); t->size = size; t->color = color; - t->time = com_localTime + time; - if (t->time < com_localTime) + t->time = com_localTime2 + time; + if (t->time < com_localTime2) t->time = UINT32_MAX; t->bits = GLS_DEPTHMASK_FALSE | GLS_BLEND_BLEND; if (!depth_test) @@ -399,7 +399,7 @@ static void GL_DrawDebugLines(void) dst_vert = tess.vertices; numverts = 0; LIST_FOR_EACH_SAFE(debug_line_t, l, next, &debug_lines_active, entry) { - if (l->time < com_localTime) { // expired + if (l->time < com_localTime2) { // expired List_Remove(&l->entry); List_Insert(&debug_lines_free, &l->entry); continue; @@ -524,7 +524,7 @@ static void GL_DrawDebugTexts(void) float radius; int i; - if (text->time < com_localTime) { // expired + if (text->time < com_localTime2) { // expired List_Remove(&text->entry); List_Insert(&debug_texts_free, &text->entry); continue; From f5f33d182fb9a391934bc8a63096e8e6a09c1a4c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 4 Sep 2024 16:24:26 +0300 Subject: [PATCH 703/974] Remove dead assignment. --- src/refresh/debug.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 9ba942301..e87e6270f 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -432,7 +432,6 @@ static void GL_DrawDebugLines(void) GL_LockArrays(numverts); qglDrawArrays(GL_LINES, 0, numverts); GL_UnlockArrays(); - numverts = 0; } if (gl_config.caps & QGL_CAP_LINE_SMOOTH) From 139deee3b3b7b5ce428c45f25b8db749842fedc9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 3 Sep 2024 17:56:40 +0300 Subject: [PATCH 704/974] =?UTF-8?q?Move=20game=20API=20extensions=20to=20?= =?UTF-8?q?=E2=80=98shared/gameext.h=E2=80=99.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Better to have all API extensions defined in single file. Move debug_draw_api_v1 definition to server to avoid including game headers in refresh code. --- inc/client/client.h | 30 +++++++--- inc/shared/debug.h | 38 ------------ inc/shared/files.h | 20 ------- inc/shared/game.h | 66 -------------------- inc/shared/gameext.h | 139 +++++++++++++++++++++++++++++++++++++++++++ src/refresh/debug.c | 45 +++++--------- src/server/game.c | 21 +++++-- src/server/server.h | 1 + 8 files changed, 194 insertions(+), 166 deletions(-) delete mode 100644 inc/shared/debug.h create mode 100644 inc/shared/gameext.h diff --git a/inc/client/client.h b/inc/client/client.h index 13235427a..71f1854a3 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -96,12 +96,6 @@ void Con_Print(const char *text); void Con_Printf(const char *fmt, ...) q_printf(1, 2); void Con_Close(bool force); -#if USE_DEBUG -void R_ClearDebugLines(void); -#else -#define R_ClearDebugLines() (void)0 -#endif - void SCR_BeginLoadingPlaque(void); void SCR_EndLoadingPlaque(void); @@ -156,8 +150,6 @@ float V_CalcFov(float fov_x, float width, float height); #define Con_SkipNotify(skip) (void)0 #define Con_Print(text) (void)0 -#define R_ClearDebugLines() (void)0 - #define SCR_BeginLoadingPlaque() (void)0 #define SCR_EndLoadingPlaque() (void)0 @@ -165,3 +157,25 @@ float V_CalcFov(float fov_x, float width, float height); #define SCR_Cinematic_g(ctx) (void)0 #endif // !USE_CLIENT + +#if USE_REF && USE_DEBUG +void R_ClearDebugLines(void); +void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugPoint(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugAxis(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test); +void R_AddDebugBounds(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugSphere(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugCircle(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugCylinder(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, + qboolean depth_test); +void R_DrawArrowCap(const vec3_t apex, const vec3_t dir, float size, + uint32_t color, uint32_t time, qboolean depth_test); +void R_AddDebugArrow(const vec3_t start, const vec3_t end, float size, uint32_t line_color, + uint32_t arrow_color, uint32_t time, qboolean depth_test); +void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, + uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test); +void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test); +#else +#define R_ClearDebugLines() (void)0 +#endif diff --git a/inc/shared/debug.h b/inc/shared/debug.h deleted file mode 100644 index c1fa423b0..000000000 --- a/inc/shared/debug.h +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright (C) 1997-2001 Id Software, Inc. - -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 the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -*/ - -#pragma once - -#define DEBUG_DRAW_API_V1 "DEBUG_DRAW_API_V1" - -typedef struct { - void (*ClearDebugLines)(void); - void (*AddDebugLine)(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugPoint)(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugAxis)(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test); - void (*AddDebugBounds)(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugSphere)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugCircle)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugCylinder)(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, qboolean depth_test); - void (*AddDebugArrow)(const vec3_t start, const vec3_t end, float size, uint32_t line_color, - uint32_t arrow_color, uint32_t time, qboolean depth_test); - void (*AddDebugCurveArrow)(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, - uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test); - void (*AddDebugText)(const vec3_t origin, const vec3_t angles, const char *text, - float size, uint32_t color, uint32_t time, qboolean depth_test); -} debug_draw_api_v1_t; diff --git a/inc/shared/files.h b/inc/shared/files.h index fa76aeef7..276298b1e 100644 --- a/inc/shared/files.h +++ b/inc/shared/files.h @@ -67,23 +67,3 @@ typedef struct { #define FS_FLAG_DEFLATE 0x00000800 // if compressed, read raw deflate data, fail otherwise #define FS_FLAG_LOADFILE 0x00001000 // open non-unique handle, must be closed very quickly #define FS_FLAG_MASK 0x0000ff00 - -#define FILESYSTEM_API_V1 "FILESYSTEM_API_V1" - -typedef struct { - int64_t (*OpenFile)(const char *path, qhandle_t *f, unsigned mode); // returns file length - int (*CloseFile)(qhandle_t f); - int (*LoadFile)(const char *path, void **buffer, unsigned flags, unsigned tag); - - int (*ReadFile)(void *buffer, size_t len, qhandle_t f); - int (*WriteFile)(const void *buffer, size_t len, qhandle_t f); - int (*FlushFile)(qhandle_t f); - int64_t (*TellFile)(qhandle_t f); - int (*SeekFile)(qhandle_t f, int64_t offset, int whence); - int (*ReadLine)(qhandle_t f, char *buffer, size_t size); - - void **(*ListFiles)(const char *path, const char *filter, unsigned flags, int *count_p); - void (*FreeFileList)(void **list); - - const char *(*ErrorString)(int error); -} filesystem_api_v1_t; diff --git a/inc/shared/game.h b/inc/shared/game.h index a13c19aa8..bc44762dc 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -290,69 +290,3 @@ typedef struct { } game_export_t; typedef game_export_t *(*game_entry_t)(game_import_t *); - -//=============================================================== - -/* - * GetGameAPIEx() is guaranteed to be called after GetGameAPI() and before - * ge->Init(). - * - * Unlike GetGameAPI(), passed game_import_ex_t * is valid as long as game - * library is loaded. Pointed to structure can be used directly without making - * a copy of it. If copying is neccessary, no more than structsize bytes must - * be copied. - * - * New fields can be safely added at the end of game_import_ex_t and - * game_export_ex_t structures, provided GAME_API_VERSION_EX is also bumped. - * - * API version history: - * 1 - Initial release. - * 2 - Added CustomizeEntity(). - * 3 - Added EntityVisibleToClient(), renamed CustomizeEntity() to - * CustomizeEntityToClient() and changed the meaning of return value. - */ - -#define GAME_API_VERSION_EX_MINIMUM 1 -#define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 -#define GAME_API_VERSION_EX_ENTITY_VISIBLE 3 -#define GAME_API_VERSION_EX 3 - -typedef enum { - VIS_PVS = 0, - VIS_PHS = 1, - VIS_NOAREAS = 2 // can be OR'ed with one of above -} vis_t; - -typedef struct { - entity_state_t s; -#if USE_PROTOCOL_EXTENSIONS - entity_state_extension_t x; -#endif -} customize_entity_t; - -typedef struct { - uint32_t apiversion; - uint32_t structsize; - - void (*local_sound)(edict_t *target, const vec3_t origin, edict_t *ent, int channel, int soundindex, float volume, float attenuation, float timeofs); - const char *(*get_configstring)(int index); - trace_t (*q_gameabi clip)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, edict_t *clip, int contentmask); - qboolean (*inVIS)(const vec3_t p1, const vec3_t p2, vis_t vis); - - void *(*GetExtension)(const char *name); - void *(*TagRealloc)(void *ptr, size_t size); -} game_import_ex_t; - -typedef struct { - uint32_t apiversion; - uint32_t structsize; - - void *(*GetExtension)(const char *name); - qboolean (*CanSave)(void); - void (*PrepFrame)(void); - void (*RestartFilesystem)(void); // called when fs_restart is issued - qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // if true is returned, `temp' must be initialized - qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); -} game_export_ex_t; - -typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h new file mode 100644 index 000000000..26016a12d --- /dev/null +++ b/inc/shared/gameext.h @@ -0,0 +1,139 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +/* + * GetGameAPIEx() is guaranteed to be called after GetGameAPI() and before + * ge->Init(). + * + * Unlike GetGameAPI(), passed game_import_ex_t * is valid as long as game + * library is loaded. Pointed to structure can be used directly without making + * a copy of it. If copying is neccessary, no more than structsize bytes must + * be copied. + * + * New fields can be safely added at the end of game_import_ex_t and + * game_export_ex_t structures, provided GAME_API_VERSION_EX is also bumped. + * + * Game is not required to implement all entry points in game_export_ex_t. + * Non-implemented entry points must be set to NULL. Server, however, must + * implement all entry points in game_import_ex_t for advertised API version. + * + * Only upstream Q2PRO engine may define new extended API versions. Mods should + * never do this on their own, but may implement private extensions obtainable + * via GetExtension() callback. + * + * API version history: + * 1 - Initial release. + * 2 - Added CustomizeEntity(). + * 3 - Added EntityVisibleToClient(), renamed CustomizeEntity() to + * CustomizeEntityToClient() and changed the meaning of return value. + */ + +#define GAME_API_VERSION_EX_MINIMUM 1 +#define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 +#define GAME_API_VERSION_EX_ENTITY_VISIBLE 3 +#define GAME_API_VERSION_EX 3 + +typedef enum { + VIS_PVS = 0, + VIS_PHS = 1, + VIS_NOAREAS = 2 // can be OR'ed with one of above +} vis_t; + +typedef struct { + entity_state_t s; +#if USE_PROTOCOL_EXTENSIONS + entity_state_extension_t x; +#endif +} customize_entity_t; + +typedef struct { + uint32_t apiversion; + uint32_t structsize; + + void (*local_sound)(edict_t *target, const vec3_t origin, edict_t *ent, int channel, int soundindex, float volume, float attenuation, float timeofs); + const char *(*get_configstring)(int index); + trace_t (*q_gameabi clip)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, edict_t *clip, int contentmask); + qboolean (*inVIS)(const vec3_t p1, const vec3_t p2, vis_t vis); + + void *(*GetExtension)(const char *name); + void *(*TagRealloc)(void *ptr, size_t size); +} game_import_ex_t; + +typedef struct { + uint32_t apiversion; + uint32_t structsize; + + void *(*GetExtension)(const char *name); + qboolean (*CanSave)(void); + void (*PrepFrame)(void); + void (*RestartFilesystem)(void); // called when fs_restart is issued + qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // if true is returned, `temp' must be initialized + qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); +} game_export_ex_t; + +typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); + +/* +============================================================================== + +SERVER API EXTENSIONS + +============================================================================== +*/ + +#define FILESYSTEM_API_V1 "FILESYSTEM_API_V1" + +typedef struct { + int64_t (*OpenFile)(const char *path, qhandle_t *f, unsigned mode); // returns file length + int (*CloseFile)(qhandle_t f); + int (*LoadFile)(const char *path, void **buffer, unsigned flags, unsigned tag); + + int (*ReadFile)(void *buffer, size_t len, qhandle_t f); + int (*WriteFile)(const void *buffer, size_t len, qhandle_t f); + int (*FlushFile)(qhandle_t f); + int64_t (*TellFile)(qhandle_t f); + int (*SeekFile)(qhandle_t f, int64_t offset, int whence); + int (*ReadLine)(qhandle_t f, char *buffer, size_t size); + + void **(*ListFiles)(const char *path, const char *filter, unsigned flags, int *count_p); + void (*FreeFileList)(void **list); + + const char *(*ErrorString)(int error); +} filesystem_api_v1_t; + +#define DEBUG_DRAW_API_V1 "DEBUG_DRAW_API_V1" + +typedef struct { + void (*ClearDebugLines)(void); + void (*AddDebugLine)(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugPoint)(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugAxis)(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test); + void (*AddDebugBounds)(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugSphere)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugCircle)(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test); + void (*AddDebugCylinder)(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, + qboolean depth_test); + void (*AddDebugArrow)(const vec3_t start, const vec3_t end, float size, uint32_t line_color, + uint32_t arrow_color, uint32_t time, qboolean depth_test); + void (*AddDebugCurveArrow)(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, + uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test); + void (*AddDebugText)(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test); +} debug_draw_api_v1_t; diff --git a/src/refresh/debug.c b/src/refresh/debug.c index e87e6270f..8449781e3 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -18,7 +18,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "gl.h" #include "shared/list.h" -#include "shared/debug.h" #include #define MAX_DEBUG_LINES TESS_MAX_VERTICES @@ -62,7 +61,7 @@ void R_ClearDebugLines(void) List_Init(&debug_texts_active); } -static void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32_t time, qboolean depth_test) { debug_line_t *l = LIST_FIRST(debug_line_t, &debug_lines_free, entry); @@ -109,7 +108,7 @@ static void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, #define GL_DRAWLINEV(s, e) \ R_AddDebugLine(s, e, color, time, depth_test) -static void R_AddDebugPoint(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugPoint(const vec3_t point, float size, uint32_t color, uint32_t time, qboolean depth_test) { size *= 0.5f; GL_DRAWLINE(point[0] - size, point[1], point[2], point[0] + size, point[1], point[2]); @@ -117,7 +116,7 @@ static void R_AddDebugPoint(const vec3_t point, float size, uint32_t color, uint GL_DRAWLINE(point[0], point[1], point[2] - size, point[0], point[1], point[2] + size); } -static void R_AddDebugAxis(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test) +void R_AddDebugAxis(const vec3_t origin, const vec3_t angles, float size, uint32_t time, qboolean depth_test) { vec3_t axis[3], end; uint32_t color; @@ -143,7 +142,7 @@ static void R_AddDebugAxis(const vec3_t origin, const vec3_t angles, float size, GL_DRAWLINEV(origin, end); } -static void R_AddDebugBounds(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugBounds(const vec3_t mins, const vec3_t maxs, uint32_t color, uint32_t time, qboolean depth_test) { for (int i = 0; i < 4; i++) { // draw column @@ -161,7 +160,7 @@ static void R_AddDebugBounds(const vec3_t mins, const vec3_t maxs, uint32_t colo } // https://danielsieger.com/blog/2021/03/27/generating-spheres.html -static void R_AddDebugSphere(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugSphere(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) { vec3_t verts[160]; const int n_stacks = min(4 + radius / 32, 10); @@ -218,7 +217,7 @@ static void R_AddDebugSphere(const vec3_t origin, float radius, uint32_t color, } } -static void R_AddDebugCircle(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugCircle(const vec3_t origin, float radius, uint32_t color, uint32_t time, qboolean depth_test) { int vert_count = min(5 + radius / 8, 16); float rads = (2 * M_PIf) / vert_count; @@ -240,7 +239,7 @@ static void R_AddDebugCircle(const vec3_t origin, float radius, uint32_t color, } } -static void R_AddDebugCylinder(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugCylinder(const vec3_t origin, float half_height, float radius, uint32_t color, uint32_t time, qboolean depth_test) { int vert_count = min(5 + radius / 8, 16); float rads = (2 * M_PIf) / vert_count; @@ -264,8 +263,8 @@ static void R_AddDebugCylinder(const vec3_t origin, float half_height, float rad } } -static void R_DrawArrowCap(const vec3_t apex, const vec3_t dir, float size, - uint32_t color, uint32_t time, qboolean depth_test) +void R_DrawArrowCap(const vec3_t apex, const vec3_t dir, float size, + uint32_t color, uint32_t time, qboolean depth_test) { vec3_t cap_end; VectorMA(apex, size, dir, cap_end); @@ -282,8 +281,8 @@ static void R_DrawArrowCap(const vec3_t apex, const vec3_t dir, float size, R_AddDebugLine(l, cap_end, color, time, depth_test); } -static void R_AddDebugArrow(const vec3_t start, const vec3_t end, float size, uint32_t line_color, - uint32_t arrow_color, uint32_t time, qboolean depth_test) +void R_AddDebugArrow(const vec3_t start, const vec3_t end, float size, uint32_t line_color, + uint32_t arrow_color, uint32_t time, qboolean depth_test) { vec3_t dir; VectorSubtract(end, start, dir); @@ -299,8 +298,8 @@ static void R_AddDebugArrow(const vec3_t start, const vec3_t end, float size, ui } } -static void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, - uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test) +void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t end, float size, + uint32_t line_color, uint32_t arrow_color, uint32_t time, qboolean depth_test) { int num_points = Q_clip(Distance(start, end) / 32, 3, 24); vec3_t last_point; @@ -328,8 +327,8 @@ static void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const ve } } -static void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, - float size, uint32_t color, uint32_t time, qboolean depth_test) +void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test) { debug_text_t *t = LIST_FIRST(debug_text_t, &debug_texts_free, entry); @@ -590,17 +589,3 @@ void GL_ShutdownDebugDraw(void) { Cmd_RemoveCommand("cleardebuglines"); } - -const debug_draw_api_v1_t debug_draw_api_v1 = { - .ClearDebugLines = R_ClearDebugLines, - .AddDebugLine = R_AddDebugLine, - .AddDebugPoint = R_AddDebugPoint, - .AddDebugAxis = R_AddDebugAxis, - .AddDebugBounds = R_AddDebugBounds, - .AddDebugSphere = R_AddDebugSphere, - .AddDebugCircle = R_AddDebugCircle, - .AddDebugCylinder = R_AddDebugCylinder, - .AddDebugArrow = R_AddDebugArrow, - .AddDebugCurveArrow = R_AddDebugCurveArrow, - .AddDebugText = R_AddDebugText, -}; diff --git a/src/server/game.c b/src/server/game.c index 68ab7cc9e..204c19bf8 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -18,7 +18,6 @@ with this program; if not, write to the Free Software Foundation, Inc., // sv_game.c -- interface to the game dll #include "server.h" -#include "shared/debug.h" const game_export_t *ge; const game_export_ex_t *gex; @@ -860,6 +859,22 @@ static const filesystem_api_v1_t filesystem_api_v1 = { .ErrorString = Q_ErrorString, }; +#if USE_REF && USE_DEBUG +static const debug_draw_api_v1_t debug_draw_api_v1 = { + .ClearDebugLines = R_ClearDebugLines, + .AddDebugLine = R_AddDebugLine, + .AddDebugPoint = R_AddDebugPoint, + .AddDebugAxis = R_AddDebugAxis, + .AddDebugBounds = R_AddDebugBounds, + .AddDebugSphere = R_AddDebugSphere, + .AddDebugCircle = R_AddDebugCircle, + .AddDebugCylinder = R_AddDebugCylinder, + .AddDebugArrow = R_AddDebugArrow, + .AddDebugCurveArrow = R_AddDebugCurveArrow, + .AddDebugText = R_AddDebugText, +}; +#endif + static void *PF_GetExtension(const char *name) { if (!name) @@ -869,10 +884,8 @@ static void *PF_GetExtension(const char *name) return (void *)&filesystem_api_v1; #if USE_REF && USE_DEBUG - if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer) { - extern const debug_draw_api_v1_t debug_draw_api_v1; + if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer) return (void *)&debug_draw_api_v1; - } #endif return NULL; diff --git a/src/server/server.h b/src/server/server.h index e3638419c..819c420fb 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -22,6 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "shared/shared.h" #include "shared/list.h" #include "shared/game.h" +#include "shared/gameext.h" #include "common/bsp.h" #include "common/cmd.h" From 600328d0ac7eb4b0b69ad2ebbc58d77ebab20df6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 5 Sep 2024 12:02:33 +0300 Subject: [PATCH 705/974] Support more line editing shortcuts. Ctrl-Left and Ctrl-Right to move between words, Ctrl-Backspace and Ctrl-Delete to delete words to the left and to the right. Closes #355. --- src/common/field.c | 79 ++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/common/field.c b/src/common/field.c index 657c708dc..9bdf1e096 100644 --- a/src/common/field.c +++ b/src/common/field.c @@ -81,27 +81,25 @@ bool IF_KeyEvent(inputField_t *field, int key) } Q_assert(field->cursorPos < field->maxChars); - if (key == K_DEL) { - if (field->text[field->cursorPos]) { - memmove(field->text + field->cursorPos, - field->text + field->cursorPos + 1, - sizeof(field->text) - field->cursorPos - 1); + if (key == K_DEL && Key_IsDown(K_CTRL)) { + size_t pos = field->cursorPos; + + // kill leading whitespace + while (field->text[pos] && field->text[pos] <= 32) { + pos++; } - return true; - } - if (key == K_BACKSPACE || (key == 'h' && Key_IsDown(K_CTRL))) { - if (field->cursorPos > 0) { - memmove(field->text + field->cursorPos - 1, - field->text + field->cursorPos, - sizeof(field->text) - field->cursorPos); - field->cursorPos--; + // kill this word + while (field->text[pos] > 32) { + pos++; } + memmove(field->text + field->cursorPos, field->text + pos, + sizeof(field->text) - pos); return true; } - if (key == 'w' && Key_IsDown(K_CTRL)) { - size_t oldpos = field->cursorPos; + if ((key == K_BACKSPACE || key == 'w') && Key_IsDown(K_CTRL)) { + size_t pos = field->cursorPos; // kill trailing whitespace while (field->cursorPos > 0 && field->text[field->cursorPos - 1] <= 32) { @@ -112,8 +110,27 @@ bool IF_KeyEvent(inputField_t *field, int key) while (field->cursorPos > 0 && field->text[field->cursorPos - 1] > 32) { field->cursorPos--; } - memmove(field->text + field->cursorPos, field->text + oldpos, - sizeof(field->text) - oldpos); + memmove(field->text + field->cursorPos, field->text + pos, + sizeof(field->text) - pos); + return true; + } + + if (key == K_DEL) { + if (field->text[field->cursorPos]) { + memmove(field->text + field->cursorPos, + field->text + field->cursorPos + 1, + sizeof(field->text) - field->cursorPos - 1); + } + return true; + } + + if (key == K_BACKSPACE || (key == 'h' && Key_IsDown(K_CTRL))) { + if (field->cursorPos > 0) { + memmove(field->text + field->cursorPos - 1, + field->text + field->cursorPos, + sizeof(field->text) - field->cursorPos); + field->cursorPos--; + } return true; } @@ -135,35 +152,35 @@ bool IF_KeyEvent(inputField_t *field, int key) return true; } - if (key == K_LEFTARROW || (key == 'b' && Key_IsDown(K_CTRL))) { - if (field->cursorPos > 0) { + if ((key == K_LEFTARROW && Key_IsDown(K_CTRL)) || (key == 'b' && Key_IsDown(K_ALT))) { + while (field->cursorPos > 0 && field->text[field->cursorPos - 1] <= 32) { + field->cursorPos--; + } + while (field->cursorPos > 0 && field->text[field->cursorPos - 1] > 32) { field->cursorPos--; } return true; } - if (key == K_RIGHTARROW || (key == 'f' && Key_IsDown(K_CTRL))) { - if (field->text[field->cursorPos]) { + if ((key == K_RIGHTARROW && Key_IsDown(K_CTRL)) || (key == 'f' && Key_IsDown(K_ALT))) { + while (field->text[field->cursorPos] && field->text[field->cursorPos] <= 32) { + field->cursorPos++; + } + while (field->text[field->cursorPos] > 32) { field->cursorPos++; } goto check; } - if (key == 'b' && Key_IsDown(K_ALT)) { - while (field->cursorPos > 0 && field->text[field->cursorPos - 1] <= 32) { - field->cursorPos--; - } - while (field->cursorPos > 0 && field->text[field->cursorPos - 1] > 32) { + if (key == K_LEFTARROW || (key == 'b' && Key_IsDown(K_CTRL))) { + if (field->cursorPos > 0) { field->cursorPos--; } return true; } - if (key == 'f' && Key_IsDown(K_ALT)) { - while (field->text[field->cursorPos] && field->text[field->cursorPos] <= 32) { - field->cursorPos++; - } - while (field->text[field->cursorPos] > 32) { + if (key == K_RIGHTARROW || (key == 'f' && Key_IsDown(K_CTRL))) { + if (field->text[field->cursorPos]) { field->cursorPos++; } goto check; From 93ab30de5f50778f5c1da051294eb09ecbb32715 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 5 Sep 2024 12:14:02 +0300 Subject: [PATCH 706/974] Simplify drawing cursor. --- src/common/field.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/common/field.c b/src/common/field.c index 9bdf1e096..8e1ac8ecf 100644 --- a/src/common/field.c +++ b/src/common/field.c @@ -276,12 +276,10 @@ int IF_Draw(const inputField_t *field, int x, int y, int flags, qhandle_t font) // draw text ret = R_DrawString(x, y, flags, field->visibleChars, text + offset, font); - if (flags & UI_DRAWCURSOR) { - // draw blinking cursor - if ((com_localTime >> 8) & 1) { - int c = Key_GetOverstrikeMode() ? 11 : '_'; - R_DrawChar(x + cursorPos * CHAR_WIDTH, y, flags, c, font); - } + // draw blinking cursor + if (flags & UI_DRAWCURSOR && com_localTime & BIT(8)) { + R_DrawChar(x + cursorPos * CHAR_WIDTH, y, flags, + Key_GetOverstrikeMode() ? 11 : '_', font); } return ret; From 17e38fd9a17a69dc45296154f75320d3dc8dd172 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 5 Sep 2024 18:13:06 +0300 Subject: [PATCH 707/974] Lookup some files in homedir only. Some files that are written by engine, like savegames, should never be loaded from base directory. Add FS_DIR_* family of flags to specify whether to lookup file in basedir or homedir only. Also simplify management of FS_PATH_* flags. Add default_lookup_flags() to avoid manipulating searchpath flags in setup_game_paths(). --- inc/shared/files.h | 10 ++++-- src/client/ui/demos.c | 2 +- src/common/files.c | 72 ++++++++++++++++++++++--------------------- src/common/prompt.c | 2 +- src/server/save.c | 15 +++++---- 5 files changed, 56 insertions(+), 45 deletions(-) diff --git a/inc/shared/files.h b/inc/shared/files.h index 276298b1e..9ec210e1b 100644 --- a/inc/shared/files.h +++ b/inc/shared/files.h @@ -39,13 +39,13 @@ typedef struct { #define FS_BUF_NONE 0x0000000c // unbuffered #define FS_BUF_MASK 0x0000000c -// where to open file from +// where to open file from (packfile vs disk) #define FS_TYPE_ANY 0x00000000 // open from anywhere #define FS_TYPE_REAL 0x00000010 // open from disk only #define FS_TYPE_PAK 0x00000020 // open from pack only #define FS_TYPE_MASK 0x00000030 -// where to look for a file +// where to look for a file (baseq2 vs gamedir) #define FS_PATH_ANY 0x00000000 // look in any search paths #define FS_PATH_BASE 0x00000040 // look in base search paths #define FS_PATH_GAME 0x00000080 // look in game search paths @@ -67,3 +67,9 @@ typedef struct { #define FS_FLAG_DEFLATE 0x00000800 // if compressed, read raw deflate data, fail otherwise #define FS_FLAG_LOADFILE 0x00001000 // open non-unique handle, must be closed very quickly #define FS_FLAG_MASK 0x0000ff00 + +// where to look for a file (basedir vs homedir) +#define FS_DIR_ANY 0x00000000 // look anywhere +#define FS_DIR_BASE 0x00010000 // look in basedir +#define FS_DIR_HOME 0x00020000 // look in homedir +#define FS_DIR_MASK 0x00030000 diff --git a/src/client/ui/demos.c b/src/client/ui/demos.c index 47fb1c5cd..0d26d0b7b 100644 --- a/src/client/ui/demos.c +++ b/src/client/ui/demos.c @@ -167,7 +167,7 @@ static char *LoadCache(void **list) if (Q_concat(buffer, sizeof(buffer), m_demos.browse, "/" COM_DEMOCACHE_NAME) >= sizeof(buffer)) { return NULL; } - len = FS_LoadFileEx(buffer, (void **)&cache, FS_TYPE_REAL | FS_PATH_GAME, TAG_FILESYSTEM); + len = FS_LoadFileEx(buffer, (void **)&cache, FS_TYPE_REAL | FS_PATH_GAME | FS_DIR_HOME, TAG_FILESYSTEM); if (!cache) { return NULL; } diff --git a/src/common/files.c b/src/common/files.c index 076169a91..8b6c467cd 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -1333,10 +1333,9 @@ static int64_t open_file_read(file_t *file, const char *normalized, size_t namel // search through the path, one element at a time for (search = fs_searchpaths; search; search = search->next) { - if (file->mode & FS_PATH_MASK) { - if ((file->mode & search->mode & FS_PATH_MASK) == 0) { - continue; - } + if ((file->mode & search->mode & FS_PATH_MASK) == 0 || + (file->mode & search->mode & FS_DIR_MASK ) == 0) { + continue; } // is the element a pak file? @@ -1637,6 +1636,17 @@ int FS_Write(const void *buf, size_t len, qhandle_t f) return len; } +static unsigned default_lookup_flags(unsigned flags) +{ + if (!(flags & FS_PATH_MASK) || !fs_game->string[0]) + flags |= FS_PATH_MASK; + + if (!(flags & FS_DIR_MASK) || !sys_homedir->string[0]) + flags |= FS_DIR_MASK; + + return flags; +} + /* ============ FS_OpenFile @@ -1663,7 +1673,7 @@ int64_t FS_OpenFile(const char *name, qhandle_t *f, unsigned mode) return Q_ERR(EMFILE); } - file->mode = mode; + file->mode = default_lookup_flags(mode); if ((mode & FS_MODE_MASK) == FS_MODE_READ) { ret = expand_open_file_read(file, name); @@ -1841,13 +1851,17 @@ int FS_LoadFileEx(const char *path, void **buffer, unsigned flags, memtag_t tag) return Q_ERR(EAGAIN); // not yet initialized } + if (flags & FS_MODE_MASK) { + return Q_ERR(EINVAL); + } + // allocate new file handle file = alloc_handle(&f); if (!file) { return Q_ERR(EMFILE); } - file->mode = (flags & ~FS_MODE_MASK) | FS_MODE_READ | FS_FLAG_LOADFILE; + file->mode = default_lookup_flags(flags) | FS_MODE_READ | FS_FLAG_LOADFILE; // look for it in the filesystem or pack files len = expand_open_file_read(file, path); @@ -2756,6 +2770,10 @@ void **FS_ListFiles(const char *path, const char *filter, unsigned flags, int *c *count_p = 0; } + if (!fs_searchpaths) { + return NULL; // not yet initialized + } + if (!path) { path = ""; pathlen = 0; @@ -2774,11 +2792,12 @@ void **FS_ListFiles(const char *path, const char *filter, unsigned flags, int *c return NULL; } + flags = default_lookup_flags(flags); + for (search = fs_searchpaths; search; search = search->next) { - if (flags & FS_PATH_MASK) { - if ((flags & search->mode & FS_PATH_MASK) == 0) { - continue; - } + if ((flags & search->mode & FS_PATH_MASK) == 0 || + (flags & search->mode & FS_DIR_MASK ) == 0) { + continue; } if (search->pack) { if ((flags & FS_TYPE_MASK) == FS_TYPE_REAL) { @@ -3509,7 +3528,7 @@ static void add_game_kpf(const char *dir) return; search = FS_Malloc(sizeof(*search)); - search->mode = FS_PATH_BASE | FS_PATH_GAME; + search->mode = FS_PATH_BASE | FS_DIR_BASE; search->filename[0] = 0; search->pack = pack_get(pack); search->next = fs_searchpaths; @@ -3519,14 +3538,11 @@ static void add_game_kpf(const char *dir) static void setup_base_paths(void) { - // base paths have both BASE and GAME bits set by default - // the GAME bit will be removed once gamedir is set, - // and will be put back once gamedir is reset to basegame add_game_kpf(sys_basedir->string); - add_game_dir(FS_PATH_BASE | FS_PATH_GAME, "%s/"BASEGAME, sys_basedir->string); + add_game_dir(FS_PATH_BASE | FS_DIR_BASE, "%s/"BASEGAME, sys_basedir->string); if (sys_homedir->string[0]) { - add_game_dir(FS_PATH_BASE | FS_PATH_GAME, "%s/"BASEGAME, sys_homedir->string); + add_game_dir(FS_PATH_BASE | FS_DIR_HOME, "%s/"BASEGAME, sys_homedir->string); } fs_base_searchpaths = fs_searchpaths; @@ -3535,33 +3551,19 @@ static void setup_base_paths(void) // Sets the gamedir and path to a different directory. static void setup_game_paths(void) { - searchpath_t *path; - if (fs_game->string[0]) { // add system path first - add_game_dir(FS_PATH_GAME, "%s/%s", sys_basedir->string, fs_game->string); + add_game_dir(FS_PATH_GAME | FS_DIR_BASE, "%s/%s", sys_basedir->string, fs_game->string); // home paths override system paths if (sys_homedir->string[0]) { - add_game_dir(FS_PATH_GAME, "%s/%s", sys_homedir->string, fs_game->string); + add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "%s/%s", sys_homedir->string, fs_game->string); } - - // remove the game bit from base paths - for (path = fs_base_searchpaths; path; path = path->next) { - path->mode &= ~FS_PATH_GAME; - } - - // this var is set for compatibility with server browsers, etc - Cvar_FullSet("gamedir", fs_game->string, CVAR_ROM | CVAR_SERVERINFO, FROM_CODE); - } else { - // add the game bit to base paths - for (path = fs_base_searchpaths; path; path = path->next) { - path->mode |= FS_PATH_GAME; - } - - Cvar_FullSet("gamedir", "", CVAR_ROM, FROM_CODE); } + // this var is set for compatibility with server browsers, etc + Cvar_FullSet("gamedir", fs_game->string, CVAR_ROM | CVAR_SERVERINFO, FROM_CODE); + // this var is used by the game library to find it's home directory Cvar_FullSet("fs_gamedir", fs_gamedir, CVAR_ROM, FROM_CODE); } diff --git a/src/common/prompt.c b/src/common/prompt.c index 73a11fe77..1acd2fdbe 100644 --- a/src/common/prompt.c +++ b/src/common/prompt.c @@ -545,7 +545,7 @@ void Prompt_LoadHistory(commandPrompt_t *prompt, const char *filename) qhandle_t f; unsigned i; - FS_OpenFile(filename, &f, FS_MODE_READ | FS_TYPE_REAL | FS_PATH_BASE); + FS_OpenFile(filename, &f, FS_MODE_READ | FS_TYPE_REAL | FS_PATH_BASE | FS_DIR_HOME); if (!f) { return; } diff --git a/src/server/save.c b/src/server/save.c index d7757d063..a6bac3437 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -25,6 +25,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #define SAVE_CURRENT ".current" #define SAVE_AUTO "save0" +// only load saves from home dir +#define SAVE_LOOKUP_FLAGS (FS_TYPE_REAL | FS_PATH_GAME | FS_DIR_HOME) + typedef enum { SAVE_MANUAL, // manual save SAVE_LEVEL_START, // autosave at level start @@ -203,7 +206,7 @@ static int remove_file(const char *dir, const char *name) static void **list_save_dir(const char *dir, int *count) { return FS_ListFiles(va("save/%s", dir), ".ssv;.sav;.sv2", - FS_TYPE_REAL | FS_PATH_GAME | FS_SEARCH_RECURSIVE, count); + SAVE_LOOKUP_FLAGS | FS_SEARCH_RECURSIVE, count); } static int wipe_save_dir(const char *dir) @@ -241,7 +244,7 @@ static int read_binary_file(const char *name) qhandle_t f; int64_t len; - len = FS_OpenFile(name, &f, FS_MODE_READ | FS_TYPE_REAL | FS_PATH_GAME); + len = FS_OpenFile(name, &f, SAVE_LOOKUP_FLAGS | FS_MODE_READ); if (!f) return -1; @@ -406,7 +409,7 @@ static int read_level_file(void) if (Q_snprintf(name, MAX_QPATH, "save/" SAVE_CURRENT "/%s.sv2", sv.name) >= MAX_QPATH) return -1; - len = FS_LoadFileEx(name, &data, FS_TYPE_REAL | FS_PATH_GAME, TAG_SERVER); + len = FS_LoadFileEx(name, &data, SAVE_LOOKUP_FLAGS, TAG_SERVER); if (!data) return -1; @@ -592,7 +595,7 @@ void SV_CheckForEnhancedSavegames(void) static void SV_Savegame_c(genctx_t *ctx, int argnum) { if (argnum == 1) { - FS_File_g("save", NULL, FS_SEARCH_DIRSONLY | FS_TYPE_REAL | FS_PATH_GAME, ctx); + FS_File_g("save", NULL, SAVE_LOOKUP_FLAGS | FS_SEARCH_DIRSONLY, ctx); } } @@ -612,8 +615,8 @@ static void SV_Loadgame_f(void) } // make sure the server files exist - if (!FS_FileExistsEx(va("save/%s/server.ssv", dir), FS_TYPE_REAL | FS_PATH_GAME) || - !FS_FileExistsEx(va("save/%s/game.ssv", dir), FS_TYPE_REAL | FS_PATH_GAME)) { + if (!FS_FileExistsEx(va("save/%s/server.ssv", dir), SAVE_LOOKUP_FLAGS) || + !FS_FileExistsEx(va("save/%s/game.ssv", dir), SAVE_LOOKUP_FLAGS)) { Com_Printf("No such savegame: %s\n", dir); return; } From b685a2295f5f3fb258c77eb0e66599442d71c20e Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 6 Sep 2024 16:24:53 +0300 Subject: [PATCH 708/974] Rename glow_texnum to texnum2. --- src/refresh/images.c | 4 ++-- src/refresh/images.h | 2 +- src/refresh/mesh.c | 6 +++--- src/refresh/tess.c | 2 +- src/refresh/texture.c | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index f54dff891..fb668a536 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1573,7 +1573,7 @@ static void IMG_List_f(void) Com_Printf("%c%c%c%c %4i %4i %s: %s\n", types[image->type > IT_MAX ? IT_MAX : image->type], (image->flags & IF_TRANSPARENT) ? 'T' : ' ', - (image->flags & IF_SCRAP) ? 'S' : image->glow_texnum ? 'G' : ' ', + (image->flags & IF_SCRAP) ? 'S' : image->texnum2 ? 'G' : ' ', (image->flags & IF_PERMANENT) ? '*' : ' ', image->upload_width, image->upload_height, @@ -1906,7 +1906,7 @@ static void check_for_glow_map(image_t *image) } IMG_Load(&temporary, glow_pic); - image->glow_texnum = temporary.texnum; + image->texnum2 = temporary.texnum; Z_Free(glow_pic); } diff --git a/src/refresh/images.h b/src/refresh/images.h index ed3b0919f..889258266 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -64,7 +64,7 @@ typedef struct image_s { uint16_t width, height; // source image uint16_t upload_width, upload_height; // after power of two and picmip unsigned registration_sequence; - unsigned texnum, glow_texnum; // gl texture binding + unsigned texnum, texnum2; // gl texture binding float sl, sh, tl, th; float aspect; } image_t; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index dd6680a50..b4bf25176 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -630,15 +630,15 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, state |= GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE; skin = skin_for_mesh(skins, num_skins); - if (skin->glow_texnum) + if (skin->texnum2) state |= GLS_GLOWMAP_ENABLE; GL_StateBits(state); GL_BindTexture(TMU_TEXTURE, skin->texnum); - if (skin->glow_texnum) - GL_BindTexture(TMU_GLOWMAP, skin->glow_texnum); + if (skin->texnum2) + GL_BindTexture(TMU_GLOWMAP, skin->texnum2); if (dotshading) { GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 0e43ed409..8a309a8a1 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -693,7 +693,7 @@ static void GL_DrawFace(const mface_t *surf) texnum[TMU_TEXTURE] = image->texnum; if (q_likely(surf->light_m)) { texnum[TMU_LIGHTMAP] = lm.texnums[surf->light_m - lm.lightmaps]; - texnum[TMU_GLOWMAP ] = image->glow_texnum; + texnum[TMU_GLOWMAP ] = image->texnum2; if (q_unlikely(gl_lightmap->integer)) { texnum[TMU_TEXTURE] = TEXNUM_WHITE; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 87ae8e603..eec063a01 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -86,8 +86,8 @@ static void update_image_params(unsigned mask) GL_ForceTexture(TMU_TEXTURE, image->texnum); GL_SetFilterAndRepeat(image->type, image->flags); - if (image->glow_texnum) { - GL_ForceTexture(TMU_TEXTURE, image->glow_texnum); + if (image->texnum2) { + GL_ForceTexture(TMU_TEXTURE, image->texnum2); GL_SetFilterAndRepeat(image->type, image->flags); } } @@ -725,7 +725,7 @@ void IMG_Load(image_t *image, byte *pic) void IMG_Unload(image_t *image) { if (image->texnum && !(image->flags & IF_SCRAP)) { - GLuint tex[2] = { image->texnum, image->glow_texnum }; + GLuint tex[2] = { image->texnum, image->texnum2 }; // invalidate bindings for (int i = 0; i < MAX_TMUS; i++) @@ -733,7 +733,7 @@ void IMG_Unload(image_t *image) gls.texnums[i] = 0; qglDeleteTextures(tex[1] ? 2 : 1, tex); - image->texnum = image->glow_texnum = 0; + image->texnum = image->texnum2 = 0; } } From c430f18fcc9fd3a0a6bb7fac05c45807a4386d60 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 7 Sep 2024 21:34:42 +0300 Subject: [PATCH 709/974] Store GL programs in a hash map. --- src/refresh/gl.h | 4 ++-- src/refresh/legacy.c | 14 +++++++------- src/refresh/shader.c | 43 +++++++++++++++++++++++++------------------ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 4c907e984..0e4452683 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -50,7 +50,6 @@ typedef GLuint glIndex_t; #define TAB_SIN(x) gl_static.sintab[(x) & 255] #define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] -#define MAX_PROGRAMS 128 #define NUM_AUTO_TEXTURES 7 typedef struct { @@ -77,7 +76,7 @@ typedef struct { GLuint array_object; GLuint index_buffer; GLuint vertex_buffer; - GLuint programs[MAX_PROGRAMS]; + GLuint warp_program; GLuint texnums[NUM_AUTO_TEXTURES]; GLenum samples_passed; GLbitfield stencil_buffer_bit; @@ -90,6 +89,7 @@ typedef struct { byte latlngtab[NUMVERTEXNORMALS][2]; byte lightstylemap[MAX_LIGHTSTYLES]; hash_map_t *queries; + hash_map_t *programs; } glStatic_t; typedef struct { diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index 6f141dcba..dcec55066 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -61,11 +61,11 @@ static void legacy_state_bits(glStateBits_t bits) qglDisable(GL_TEXTURE_2D); } - if ((diff & GLS_WARP_ENABLE) && gl_static.programs[0]) { + if ((diff & GLS_WARP_ENABLE) && gl_static.warp_program) { if (bits & GLS_WARP_ENABLE) { vec4_t param = { glr.fd.time, glr.fd.time }; qglEnable(GL_FRAGMENT_PROGRAM_ARB); - qglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, gl_static.programs[0]); + qglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, gl_static.warp_program); qglProgramLocalParameter4fvARB(GL_FRAGMENT_PROGRAM_ARB, 0, param); } else { qglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); @@ -181,7 +181,7 @@ static void legacy_disable_state(void) qglDisableClientState(GL_VERTEX_ARRAY); qglDisableClientState(GL_COLOR_ARRAY); - if (gl_static.programs[0]) { + if (gl_static.warp_program) { qglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); qglDisable(GL_FRAGMENT_PROGRAM_ARB); } @@ -220,16 +220,16 @@ static void legacy_init(void) } qglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); - gl_static.programs[0] = prog; + gl_static.warp_program = prog; } static void legacy_shutdown(void) { legacy_disable_state(); - if (gl_static.programs[0]) { - qglDeleteProgramsARB(1, gl_static.programs); - gl_static.programs[0] = 0; + if (gl_static.warp_program) { + qglDeleteProgramsARB(1, &gl_static.warp_program); + gl_static.warp_program = 0; } } diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 0b28f7a31..c5a1ef0ab 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -270,6 +270,18 @@ static GLuint create_and_use_program(glStateBits_t bits) return program; } +static void shader_use_program(glStateBits_t key) +{ + GLuint *prog = HashMap_Lookup(GLuint, gl_static.programs, &key); + + if (prog) { + qglUseProgram(*prog); + } else { + GLuint val = create_and_use_program(key); + HashMap_Insert(gl_static.programs, &key, &val); + } +} + static void shader_state_bits(glStateBits_t bits) { glStateBits_t diff = bits ^ gls.state_bits; @@ -277,14 +289,8 @@ static void shader_state_bits(glStateBits_t bits) if (diff & GLS_COMMON_MASK) GL_CommonStateBits(bits); - if (diff & GLS_SHADER_MASK) { - GLuint i = (bits >> 6) & (MAX_PROGRAMS - 1); - - if (gl_static.programs[i]) - qglUseProgram(gl_static.programs[i]); - else - gl_static.programs[i] = create_and_use_program(bits); - } + if (diff & GLS_SHADER_MASK) + shader_use_program(bits & GLS_SHADER_MASK); if (diff & GLS_SCROLL_MASK && bits & GLS_SCROLL_ENABLE) { GL_ScrollPos(gls.u_block.scroll, bits); @@ -398,15 +404,13 @@ static void shader_disable_state(void) static void shader_clear_state(void) { shader_disable_state(); - - if (gl_static.programs[0]) - qglUseProgram(gl_static.programs[0]); - else - gl_static.programs[0] = create_and_use_program(GLS_DEFAULT); + shader_use_program(GLS_DEFAULT); } static void shader_init(void) { + gl_static.programs = HashMap_Create(glStateBits_t, GLuint, HashInt32, NULL); + qglGenBuffers(1, &gl_static.uniform_buffer); qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.uniform_buffer); @@ -416,13 +420,16 @@ static void shader_init(void) static void shader_shutdown(void) { shader_disable_state(); - qglUseProgram(0); - for (int i = 0; i < MAX_PROGRAMS; i++) { - if (gl_static.programs[i]) { - qglDeleteProgram(gl_static.programs[i]); - gl_static.programs[i] = 0; + + if (gl_static.programs) { + uint32_t map_size = HashMap_Size(gl_static.programs); + for (int i = 0; i < map_size; i++) { + GLuint *prog = HashMap_GetValue(GLuint, gl_static.programs, i); + qglDeleteProgram(*prog); } + HashMap_Destroy(gl_static.programs); + gl_static.programs = NULL; } if (gl_static.uniform_buffer) { From 09c652dc2178dbd97b5f11cf2717a8b850913ba6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 6 Sep 2024 16:33:42 +0300 Subject: [PATCH 710/974] =?UTF-8?q?Add=20support=20for=20N64=20=E2=80=98cl?= =?UTF-8?q?assic=E2=80=99=20skies.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on code from: Jonathan --- doc/client.asciidoc | 7 +++-- inc/refresh/refresh.h | 1 + src/refresh/gl.h | 21 ++++++++----- src/refresh/images.c | 25 +++++++++++++-- src/refresh/shader.c | 72 +++++++++++++++++++++++++++++++++---------- src/refresh/sky.c | 17 +++++++--- src/refresh/surf.c | 25 ++++++++++++--- 7 files changed, 132 insertions(+), 36 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index cff0cc813..abbcff7a9 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -674,8 +674,11 @@ gl_downsample_skins:: Default value is 1 (downsampling enabled). gl_drawsky:: - Enable skybox texturing. 0 means to draw sky box in solid black color. - Default value is 1 (enabled). + Enables skybox texturing and N64 skies. N64 skies only work if + ‘gl_shaders’ is enabled. Default value is 1. + - 0 — draw solid black sky + - 1 — draw normal sky, or N64 sky if present + - 2 — draw normal sky, ignore N64 sky gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 6dbd8b229..09827a7a8 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -158,6 +158,7 @@ typedef enum { // not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found IF_DIRECT = BIT(17), // don't override extension + IF_CLASSIC_SKY = BIT(18), // split in two halves } imageflags_t; typedef enum { diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 0e4452683..6651d531c 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -90,6 +90,7 @@ typedef struct { byte lightstylemap[MAX_LIGHTSTYLES]; hash_map_t *queries; hash_map_t *programs; + image_t *classic_sky; } glStatic_t; typedef struct { @@ -461,6 +462,7 @@ typedef enum { GLS_BLEND_BLEND = BIT(3), GLS_BLEND_ADD = BIT(4), GLS_BLEND_MODULATE = BIT(5), + GLS_ALPHATEST_ENABLE = BIT(6), GLS_TEXTURE_REPLACE = BIT(7), GLS_SCROLL_ENABLE = BIT(8), @@ -468,16 +470,19 @@ typedef enum { GLS_WARP_ENABLE = BIT(10), GLS_INTENSITY_ENABLE = BIT(11), GLS_GLOWMAP_ENABLE = BIT(12), - GLS_SHADE_SMOOTH = BIT(13), - GLS_SCROLL_X = BIT(14), - GLS_SCROLL_Y = BIT(15), - GLS_SCROLL_FLIP = BIT(16), - GLS_SCROLL_SLOW = BIT(17), + GLS_CLASSIC_SKY = BIT(13), + + GLS_SHADE_SMOOTH = BIT(14), + GLS_SCROLL_X = BIT(15), + GLS_SCROLL_Y = BIT(16), + GLS_SCROLL_FLIP = BIT(17), + GLS_SCROLL_SLOW = BIT(18), GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | - GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE, + GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | + GLS_CLASSIC_SKY, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; @@ -528,10 +533,12 @@ typedef struct { GLfloat add; GLfloat intensity; GLfloat intensity2; - GLfloat pad; + GLfloat pad_1; GLfloat w_amp[2]; GLfloat w_phase[2]; GLfloat scroll[2]; + GLfloat vieworg[3]; + GLfloat pad_2; } glUniformBlock_t; typedef struct { diff --git a/src/refresh/images.c b/src/refresh/images.c index fb668a536..290da7ac3 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1993,8 +1993,29 @@ static image_t *find_or_load_image(const char *name, size_t len, if (r_glowmaps->integer && (type == IT_SKIN || type == IT_WALL)) check_for_glow_map(image); - // upload the image - IMG_Load(image, pic); + if (type == IT_SKY && flags & IF_CLASSIC_SKY) { + // upload the top half of the image (solid) + image->height /= 2; + image->upload_height /= 2; + + IMG_Load(image, pic + (image->width * image->height * 4)); + + // upload the bottom half (alpha) + image_t temporary = { + .type = type, + .flags = flags, + .width = image->width, + .height = image->height, + .upload_width = image->upload_width, + .upload_height = image->upload_height + }; + + IMG_Load(&temporary, pic); + image->texnum2 = temporary.texnum; + } else { + // upload the image + IMG_Load(image, pic); + } // don't need pics in memory after GL upload Z_Free(pic); diff --git a/src/refresh/shader.c b/src/refresh/shader.c index c5a1ef0ab..9e7acf108 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -48,10 +48,12 @@ static void write_block(sizebuf_t *buf) float u_add; float u_intensity; float u_intensity2; - float pad; + float pad_1; vec2 w_amp; vec2 w_phase; vec2 u_scroll; + vec3 u_vieworg; + float pad_2; ) GLSF("};\n"); } @@ -62,8 +64,12 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) write_block(buf); GLSL(in vec4 a_pos;) - GLSL(in vec2 a_tc;) - GLSL(out vec2 v_tc;) + if (bits & GLS_CLASSIC_SKY) { + GLSL(out vec3 v_dir;) + } else { + GLSL(in vec2 a_tc;) + GLSL(out vec2 v_tc;) + } if (bits & GLS_LIGHTMAP_ENABLE) { GLSL(in vec2 a_lmtc;) @@ -76,10 +82,14 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) } GLSF("void main() {\n"); - if (bits & GLS_SCROLL_ENABLE) + if (bits & GLS_CLASSIC_SKY) { + GLSL(v_dir = a_pos.xyz - u_vieworg.xyz;) + GLSL(v_dir[2] *= 3.0;) + } else if (bits & GLS_SCROLL_ENABLE) { GLSL(v_tc = a_tc + u_scroll;) - else + } else { GLSL(v_tc = a_tc;) + } if (bits & GLS_LIGHTMAP_ENABLE) GLSL(v_lmtc = a_lmtc;) @@ -98,11 +108,19 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) if (gl_config.ver_es) GLSL(precision mediump float;) - if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE)) + if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_CLASSIC_SKY)) write_block(buf); - GLSL(uniform sampler2D u_texture;) - GLSL(in vec2 v_tc;) + if (bits & GLS_CLASSIC_SKY) { + GLSL( + uniform sampler2D u_texture1; + uniform sampler2D u_texture2; + in vec3 v_dir; + ) + } else { + GLSL(uniform sampler2D u_texture;) + GLSL(in vec2 v_tc;) + } if (bits & GLS_LIGHTMAP_ENABLE) { GLSL(uniform sampler2D u_lightmap;) @@ -118,12 +136,24 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(out vec4 o_color;) GLSF("void main() {\n"); - GLSL(vec2 tc = v_tc;) - - if (bits & GLS_WARP_ENABLE) - GLSL(tc += w_amp * sin(tc.ts * w_phase + u_time);) - - GLSL(vec4 diffuse = texture(u_texture, tc);) + if (bits & GLS_CLASSIC_SKY) { + GLSL( + float len = length(v_dir); + vec2 dir = v_dir.xy * (3.0 / len); + vec2 tc1 = dir + vec2(u_time * 0.0625); + vec2 tc2 = dir + vec2(u_time * 0.1250); + vec4 solid = texture(u_texture1, tc1); + vec4 alpha = texture(u_texture2, tc2); + vec4 diffuse = vec4((solid.rgb - alpha.rgb * 0.25) * 0.65, 1.0); + ) + } else { + GLSL(vec2 tc = v_tc;) + + if (bits & GLS_WARP_ENABLE) + GLSL(tc += w_amp * sin(tc.ts * w_phase + u_time);) + + GLSL(vec4 diffuse = texture(u_texture, tc);) + } if (bits & GLS_ALPHATEST_ENABLE) GLSL(if (diffuse.a <= 0.666) discard;) @@ -218,7 +248,8 @@ static GLuint create_and_use_program(glStateBits_t bits) qglAttachShader(program, shader_f); qglBindAttribLocation(program, VERT_ATTR_POS, "a_pos"); - qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); + if (!(bits & GLS_CLASSIC_SKY)) + qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); if (bits & GLS_LIGHTMAP_ENABLE) qglBindAttribLocation(program, VERT_ATTR_LMTC, "a_lmtc"); if (!(bits & GLS_TEXTURE_REPLACE)) @@ -261,7 +292,12 @@ static GLuint create_and_use_program(glStateBits_t bits) qglUseProgram(program); - qglUniform1i(qglGetUniformLocation(program, "u_texture"), TMU_TEXTURE); + if (bits & GLS_CLASSIC_SKY) { + qglUniform1i(qglGetUniformLocation(program, "u_texture1"), TMU_TEXTURE); + qglUniform1i(qglGetUniformLocation(program, "u_texture2"), TMU_LIGHTMAP); + } else { + qglUniform1i(qglGetUniformLocation(program, "u_texture"), TMU_TEXTURE); + } if (bits & GLS_LIGHTMAP_ENABLE) qglUniform1i(qglGetUniformLocation(program, "u_lightmap"), TMU_LIGHTMAP); if (bits & GLS_GLOWMAP_ENABLE) @@ -370,6 +406,8 @@ static void shader_setup_2d(void) gls.u_block.w_amp[1] = 0.0025f; gls.u_block.w_phase[0] = M_PIf * 10; gls.u_block.w_phase[1] = M_PIf * 10; + + VectorClear(gls.u_block.vieworg); } static void shader_setup_3d(void) @@ -384,6 +422,8 @@ static void shader_setup_3d(void) gls.u_block.w_amp[1] = 0.0625f; gls.u_block.w_phase[0] = 4; gls.u_block.w_phase[1] = 4; + + VectorCopy(glr.fd.vieworg, gls.u_block.vieworg); } static void shader_disable_state(void) diff --git a/src/refresh/sky.c b/src/refresh/sky.c index 527e69917..f4afe21e3 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -334,15 +334,24 @@ void R_DrawSkyBox(void) return; // nothing visible GL_BindArrays(VA_SPRITE); - GL_StateBits(GLS_TEXTURE_REPLACE); - GL_ArrayBits(GLA_VERTEX | GLA_TC); + + if (gl_static.classic_sky && gl_drawsky->integer == 1) { + GL_StateBits(GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY); + GL_ArrayBits(GLA_VERTEX); + GL_BindTexture(TMU_TEXTURE, gl_static.classic_sky->texnum); + GL_BindTexture(TMU_LIGHTMAP, gl_static.classic_sky->texnum2); + } else { + GL_StateBits(GLS_TEXTURE_REPLACE); + GL_ArrayBits(GLA_VERTEX | GLA_TC); + } for (i = 0; i < 6; i++) { if (skymins[0][i] >= skymaxs[0][i] || skymins[1][i] >= skymaxs[1][i]) continue; - GL_BindTexture(TMU_TEXTURE, sky_images[i]); + if (!gl_static.classic_sky || gl_drawsky->integer != 1) + GL_BindTexture(TMU_TEXTURE, sky_images[i]); MakeSkyVec(skymaxs[0][i], skymins[1][i], i, tess.vertices); MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); @@ -375,7 +384,7 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis char pathname[MAX_QPATH]; const image_t *image; - if (!gl_drawsky->integer) { + if (!gl_drawsky->integer || (gl_static.classic_sky && gl_drawsky->integer == 1)) { R_UnsetSky(); return; } diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 0d7c2d6c3..df3d0b026 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -974,6 +974,7 @@ void GL_FreeWorld(void) return; BSP_Free(gl_static.world.cache); + gl_static.classic_sky = NULL; if (gl_static.world.vertices) Z_Free(gl_static.world.vertices); @@ -1031,16 +1032,30 @@ void GL_LoadWorld(const char *name) GL_InitQueries(); gl_static.world.cache = bsp; + gl_static.classic_sky = NULL; // calculate world size for far clip plane and sky box set_world_size(bsp->nodes); // register all texinfo for (i = 0, info = bsp->texinfo; i < bsp->numtexinfo; i++, info++) { - imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; - Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); - FS_NormalizePath(buffer); - info->image = IMG_Find(buffer, IT_WALL, flags); + if (gl_static.use_shaders && info->c.flags & SURF_SKY && + Q_stricmpn(info->name, CONST_STR_LEN("n64/env/sky")) == 0) { + if (gl_static.classic_sky) { + info->image = gl_static.classic_sky; + } else { + Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".tga"); + FS_NormalizePath(buffer); + info->image = IMG_Find(buffer, IT_SKY, IF_REPEAT | IF_CLASSIC_SKY); + if (info->image != R_NOTEXTURE) + gl_static.classic_sky = info->image; + } + } else { + imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; + Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); + FS_NormalizePath(buffer); + info->image = IMG_Find(buffer, IT_WALL, flags); + } } // calculate vertex buffer size in bytes @@ -1071,7 +1086,7 @@ void GL_LoadWorld(const char *name) // only supported in DECOUPLED_LM maps because vanilla maps have broken // lightofs for liquids/alphas. legacy renderer doesn't support lightmapped // liquids too. - if ((bsp->lm_decoupled || n64surfs > 100) && gl_static.use_shaders) + if ((bsp->lm_decoupled || n64surfs > 100 || gl_static.classic_sky) && gl_static.use_shaders) gl_static.nolm_mask = SURF_NOLM_MASK_REMASTER; glr.fd.lightstyles = &(lightstyle_t){ 1 }; From 8d5f01098f3e05576c357f29135ac9d0291d854f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 6 Sep 2024 16:47:11 +0300 Subject: [PATCH 711/974] Print which shader failed to compile. --- src/refresh/shader.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 9e7acf108..e7b7d2d57 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -212,7 +212,8 @@ static GLuint create_shader(GLenum type, const sizebuf_t *buf) if (buffer[0]) Com_Printf("%s", buffer); - Com_EPrintf("Error compiling shader\n"); + Com_EPrintf("Error compiling %s shader\n", + type == GL_VERTEX_SHADER ? "vertex" : "fragment"); return 0; } From c7cf5e25d42943976dac3cab93d55e540798d24a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 7 Sep 2024 13:58:19 +0300 Subject: [PATCH 712/974] Improve default flare drawing. Also support flares with unlocked angle and color shell. --- inc/refresh/refresh.h | 27 ++++++----- src/client/tent.c | 2 +- src/refresh/gl.h | 13 ++--- src/refresh/images.c | 3 +- src/refresh/shader.c | 6 +++ src/refresh/tess.c | 108 +++++++++++++++++++++++++++--------------- 6 files changed, 99 insertions(+), 60 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 09827a7a8..8e0be95f1 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -144,21 +144,22 @@ typedef struct { } clipRect_t; typedef enum { - IF_NONE = 0, - IF_PERMANENT = BIT(0), // not freed by R_EndRegistration() - IF_TRANSPARENT = BIT(1), // known to be transparent - IF_PALETTED = BIT(2), // loaded from 8-bit paletted format - IF_UPSCALED = BIT(3), // upscaled - IF_SCRAP = BIT(4), // put in scrap texture - IF_TURBULENT = BIT(5), // turbulent surface (don't desaturate, etc) - IF_REPEAT = BIT(6), // tiling image - IF_NEAREST = BIT(7), // don't bilerp - IF_OPAQUE = BIT(8), // known to be opaque + IF_NONE = 0, + IF_PERMANENT = BIT(0), // not freed by R_EndRegistration() + IF_TRANSPARENT = BIT(1), // known to be transparent + IF_PALETTED = BIT(2), // loaded from 8-bit paletted format + IF_UPSCALED = BIT(3), // upscaled + IF_SCRAP = BIT(4), // put in scrap texture + IF_TURBULENT = BIT(5), // turbulent surface (don't desaturate, etc) + IF_REPEAT = BIT(6), // tiling image + IF_NEAREST = BIT(7), // don't bilerp + IF_OPAQUE = BIT(8), // known to be opaque + IF_DEFAULT_FLARE = BIT(9), // default flare hack // not stored in image - IF_OPTIONAL = BIT(16), // don't warn if not found - IF_DIRECT = BIT(17), // don't override extension - IF_CLASSIC_SKY = BIT(18), // split in two halves + IF_OPTIONAL = BIT(16), // don't warn if not found + IF_DIRECT = BIT(17), // don't override extension + IF_CLASSIC_SKY = BIT(18), // split in two halves } imageflags_t; typedef enum { diff --git a/src/client/tent.c b/src/client/tent.c index 848dd5f66..0724d7035 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -311,7 +311,7 @@ void CL_RegisterTEntModels(void) for (int i = 0; i < MFLASH_TOTAL; i++) cl_mod_muzzles[i] = R_RegisterModel(va("models/weapons/%s/flash/tris.md2", muzzlenames[i])); - cl_img_flare = R_RegisterSprite("misc/flare.tga"); + cl_img_flare = R_RegisterImage("misc/flare.tga", IT_SPRITE, IF_DEFAULT_FLARE); // check for remaster powerscreen model (ugly!) len = FS_LoadFile("models/items/armor/effect/tris.md2", &data); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 6651d531c..f5018aa05 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -471,18 +471,19 @@ typedef enum { GLS_INTENSITY_ENABLE = BIT(11), GLS_GLOWMAP_ENABLE = BIT(12), GLS_CLASSIC_SKY = BIT(13), + GLS_DEFAULT_FLARE = BIT(14), - GLS_SHADE_SMOOTH = BIT(14), - GLS_SCROLL_X = BIT(15), - GLS_SCROLL_Y = BIT(16), - GLS_SCROLL_FLIP = BIT(17), - GLS_SCROLL_SLOW = BIT(18), + GLS_SHADE_SMOOTH = BIT(15), + GLS_SCROLL_X = BIT(16), + GLS_SCROLL_Y = BIT(17), + GLS_SCROLL_FLIP = BIT(18), + GLS_SCROLL_SLOW = BIT(19), GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | - GLS_CLASSIC_SKY, + GLS_CLASSIC_SKY | GLS_DEFAULT_FLARE, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; diff --git a/src/refresh/images.c b/src/refresh/images.c index 290da7ac3..55483fe65 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -398,7 +398,8 @@ static uint32_t tga_unpack_pixel(const byte *in, int bpp) switch (bpp) { case 1: - return MakeColor(255, 255, 255, in[0]); + r = in[0]; + return MakeColor(r, r, r, 255); case 2: r = (in[1] & 0x7C) >> 2; g = ((in[1] & 0x03) << 3) | ((in[0] & 0xE0) >> 5); diff --git a/src/refresh/shader.c b/src/refresh/shader.c index e7b7d2d57..24a8169ad 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -172,6 +172,12 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) if (bits & GLS_INTENSITY_ENABLE) GLSL(diffuse.rgb *= u_intensity;) + if (bits & GLS_DEFAULT_FLARE) + GLSL( + diffuse.rgb *= (diffuse.r + diffuse.g + diffuse.b) / 3.0; + diffuse.rgb *= v_color.a; + ) + if (!(bits & GLS_TEXTURE_REPLACE)) GLSL(diffuse *= v_color;) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 8a309a8a1..83287d9fe 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -356,25 +356,30 @@ static void GL_FlushFlares(void) return; GL_BindTexture(TMU_TEXTURE, tess.texnum[TMU_TEXTURE]); - GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD); + GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_BLEND_ADD | tess.flags); GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); GL_DrawIndexed(SHOWTRIS_FX); tess.numverts = tess.numindices = 0; tess.texnum[TMU_TEXTURE] = 0; + tess.flags = 0; } void GL_DrawFlares(void) { + static const byte indices[12] = { 0, 2, 3, 0, 3, 4, 0, 4, 1, 0, 1, 2 }; + static const float tcoords[10] = { 0.5f, 0.5f, 0, 1, 0, 0, 1, 0, 1, 1 }; vec3_t up, down, left, right; - color_t color; + color_t inner, outer; vec_t *dst_vert; glIndex_t *dst_indices; - GLuint result, texnum; + GLuint result; const entity_t *ent; + const image_t *image; glquery_t *q; float scale; - int i; + bool def; + int i, j; if (!glr.num_flares) return; @@ -414,52 +419,77 @@ void GL_DrawFlares(void) if (!q->frac) continue; - texnum = IMG_ForHandle(ent->skin)->texnum; + image = IMG_ForHandle(ent->skin); - if (q_unlikely(tess.numverts + 4 > TESS_MAX_VERTICES || - tess.numindices + 6 > TESS_MAX_INDICES) || - (tess.numindices && tess.texnum[TMU_TEXTURE] != texnum)) + if (q_unlikely(tess.numverts + 5 > TESS_MAX_VERTICES || + tess.numindices + 12 > TESS_MAX_INDICES) || + (tess.numindices && tess.texnum[TMU_TEXTURE] != image->texnum)) GL_FlushFlares(); - tess.texnum[TMU_TEXTURE] = texnum; - - scale = 25.0f * (ent->scale * q->frac); - - VectorScale(glr.viewaxis[1], scale, left); - VectorScale(glr.viewaxis[1], -scale, right); - VectorScale(glr.viewaxis[2], -scale, down); - VectorScale(glr.viewaxis[2], scale, up); + tess.texnum[TMU_TEXTURE] = image->texnum; + + def = image->flags & IF_DEFAULT_FLARE; + if (def) + tess.flags |= GLS_DEFAULT_FLARE; + + scale = (25 << def) * (ent->scale * q->frac); + + if (ent->flags & RF_FLARE_LOCK_ANGLE) { + VectorScale(glr.viewaxis[1], scale, left); + VectorScale(glr.viewaxis[1], -scale, right); + VectorScale(glr.viewaxis[2], -scale, down); + VectorScale(glr.viewaxis[2], scale, up); + } else { + vec3_t dir, r, u; + VectorSubtract(ent->origin, glr.fd.vieworg, dir); + VectorNormalize(dir); + MakeNormalVectors(dir, r, u); + VectorScale(r, -scale, left); + VectorScale(r, scale, right); + VectorScale(u, -scale, down); + VectorScale(u, scale, up); + } dst_vert = tess.vertices + tess.numverts * 6; - VectorAdd3(ent->origin, down, left, dst_vert); - VectorAdd3(ent->origin, up, left, dst_vert + 6); - VectorAdd3(ent->origin, up, right, dst_vert + 12); - VectorAdd3(ent->origin, down, right, dst_vert + 18); + VectorCopy(ent->origin, dst_vert); + VectorAdd3(ent->origin, down, left, dst_vert + 6); + VectorAdd3(ent->origin, up, left, dst_vert + 12); + VectorAdd3(ent->origin, up, right, dst_vert + 18); + VectorAdd3(ent->origin, down, right, dst_vert + 24); - dst_vert[ 3] = 0; dst_vert[ 4] = 1; - dst_vert[ 9] = 0; dst_vert[10] = 0; - dst_vert[15] = 1; dst_vert[16] = 0; - dst_vert[21] = 1; dst_vert[22] = 1; + for (j = 0; j < 5; j++) { + dst_vert[j * 6 + 3] = tcoords[j * 2 + 0]; + dst_vert[j * 6 + 4] = tcoords[j * 2 + 1]; + } - color.u32 = ent->rgba.u32; - color.u8[3] = 128 * (ent->alpha * q->frac); + inner.u32 = ent->rgba.u32; + inner.u8[3] = (128 + def * 32) * (ent->alpha * q->frac); + outer.u32 = inner.u32; + + if (ent->flags & (RF_SHELL_RED | RF_SHELL_GREEN | RF_SHELL_BLUE)) { + VectorClear(outer.u8); + if (ent->flags & RF_SHELL_RED) + outer.u8[0] = 255; + if (ent->flags & RF_SHELL_GREEN) + outer.u8[1] = 255; + if (ent->flags & RF_SHELL_BLUE) + outer.u8[2] = 255; + tess.flags |= GLS_SHADE_SMOOTH; + } - WN32(dst_vert + 5, color.u32); - WN32(dst_vert + 11, color.u32); - WN32(dst_vert + 17, color.u32); - WN32(dst_vert + 23, color.u32); + WN32(dst_vert + 5, inner.u32); + WN32(dst_vert + 11, outer.u32); + WN32(dst_vert + 17, outer.u32); + WN32(dst_vert + 23, outer.u32); + WN32(dst_vert + 29, outer.u32); dst_indices = tess.indices + tess.numindices; - dst_indices[0] = tess.numverts + 0; - dst_indices[1] = tess.numverts + 2; - dst_indices[2] = tess.numverts + 3; - dst_indices[3] = tess.numverts + 0; - dst_indices[4] = tess.numverts + 1; - dst_indices[5] = tess.numverts + 2; - - tess.numverts += 4; - tess.numindices += 6; + for (j = 0; j < 12; j++) + dst_indices[j] = tess.numverts + indices[j]; + + tess.numverts += 5; + tess.numindices += 12; } GL_FlushFlares(); From 0079cc7da1b339094be782f42d5fe6a8db2c64ca Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 8 Sep 2024 05:09:03 +0300 Subject: [PATCH 713/974] Drop unneeded assert. --- src/refresh/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 5113c7b50..e87772a63 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -452,9 +452,9 @@ static void GL_OccludeFlares(void) uint32_t map_size = HashMap_Size(gl_static.queries); Q_assert(map_size < MAX_EDICTS); qglGenQueries(1, &new.query); - Q_assert(!HashMap_Insert(gl_static.queries, &e->skinnum, &new)); + HashMap_Insert(gl_static.queries, &e->skinnum, &new); q = HashMap_GetValue(glquery_t, gl_static.queries, map_size); - } + } if (!set) { GL_LoadMatrix(glr.viewmatrix); From e7161631790d9ae1429fef89c03e6854a533db4f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 9 Jul 2024 17:43:37 +0300 Subject: [PATCH 714/974] Conflict-riddled cherry pick --- inc/common/intreadwrite.h | 17 + inc/common/msg.h | 47 +- inc/common/pmove.h | 6 +- inc/common/protocol.h | 42 +- inc/shared/game.h | 37 +- inc/shared/shared.h | 118 +++- meson.build | 12 +- meson_options.txt | 5 + src/client/client.h | 6 +- src/client/demo.c | 10 +- src/client/gtv.c | 23 +- src/client/parse.c | 71 ++- src/client/predict.c | 4 +- src/common/msg.c | 674 ++++++++++++++++------- src/common/pmove/common.c | 52 ++ src/common/pmove/new.c | 27 + src/common/pmove/old.c | 27 + src/common/{pmove.c => pmove/template.c} | 59 +- src/game/g_local.h | 8 + src/game/g_misc.c | 2 +- src/game/g_save.c | 11 + src/game/p_client.c | 8 +- src/server/commands.c | 2 +- src/server/entities.c | 16 +- src/server/game.c | 28 +- src/server/init.c | 6 + src/server/main.c | 32 +- src/server/mvd.c | 69 ++- src/server/mvd/client.c | 20 +- src/server/mvd/game.c | 38 +- src/server/mvd/parse.c | 17 +- src/server/save.c | 2 +- src/server/send.c | 7 +- src/server/server.h | 56 +- src/server/user.c | 3 + 35 files changed, 1169 insertions(+), 393 deletions(-) create mode 100644 src/common/pmove/common.c create mode 100644 src/common/pmove/new.c create mode 100644 src/common/pmove/old.c rename src/common/{pmove.c => pmove/template.c} (96%) diff --git a/inc/common/intreadwrite.h b/inc/common/intreadwrite.h index 320e559f2..bbe4eb124 100644 --- a/inc/common/intreadwrite.h +++ b/inc/common/intreadwrite.h @@ -73,6 +73,13 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); #define RL16(p) ((((const uint8_t *)(p))[1] << 8) | ((const uint8_t *)(p))[0]) #endif +#ifndef RL24 +#define RL24(p) \ + (((uint32_t)((const uint8_t *)(p))[2] << 16) | \ + ((uint32_t)((const uint8_t *)(p))[1] << 8) | \ + ((uint32_t)((const uint8_t *)(p))[0])) +#endif + #ifndef RL32 #define RL32(p) \ (((uint32_t)((const uint8_t *)(p))[3] << 24) | \ @@ -102,6 +109,16 @@ struct unaligned64 { uint64_t u; } __attribute__((packed, may_alias)); } while (0) #endif +#ifndef WL24 +#define WL24(p, v) \ + do { \ + uint32_t _v = (v); \ + ((uint8_t *)p)[0] = _v & 0xff; \ + ((uint8_t *)p)[1] = (_v >> 8) & 0xff; \ + ((uint8_t *)p)[2] = (_v >> 16) & 0xff; \ + } while (0) +#endif + #ifndef WL32 #define WL32(p, v) \ do { \ diff --git a/inc/common/msg.h b/inc/common/msg.h index 9065d6ca4..b54f22dc2 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -22,15 +22,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/protocol.h" #include "common/sizebuf.h" -#define MAX_PACKETENTITY_BYTES 64 // 62 bytes worst case + margin +#define MAX_PACKETENTITY_BYTES 70 // 68 bytes worst case + 2 byte eof // entity and player states are pre-quantized before sending to make delta // comparsion easier typedef struct { uint16_t number; - int16_t origin[3]; int16_t angles[3]; - int16_t old_origin[3]; + int32_t origin[3]; + int32_t old_origin[3]; uint16_t modelindex; uint16_t modelindex2; uint16_t modelindex3; @@ -50,18 +50,19 @@ typedef struct { } entity_packed_t; typedef struct { - pmove_state_t pmove; - int16_t viewangles[3]; - int8_t viewoffset[3]; - int8_t kick_angles[3]; - int8_t gunangles[3]; - int8_t gunoffset[3]; - uint16_t gunindex; - uint8_t gunframe; - uint8_t blend[4]; - uint8_t fov; - uint8_t rdflags; - int16_t stats[MAX_STATS]; + pmove_state_new_t pmove; + int16_t viewangles[3]; + int8_t viewoffset[3]; + int8_t kick_angles[3]; + int8_t gunangles[3]; + int8_t gunoffset[3]; + uint16_t gunindex; + uint8_t gunframe; + uint8_t blend[4]; + uint8_t damage_blend[4]; + uint8_t fov; + uint8_t rdflags; + int16_t stats[MAX_STATS_NEW]; } player_packed_t; typedef enum { @@ -72,8 +73,9 @@ typedef enum { MSG_PS_IGNORE_DELTAANGLES = BIT(4), // ignore delta_angles MSG_PS_IGNORE_PREDICTION = BIT(5), // mutually exclusive with IGNORE_VIEWANGLES MSG_PS_EXTENSIONS = BIT(6), // enable protocol extensions - MSG_PS_FORCE = BIT(7), // send even if unchanged (MVD stream only) - MSG_PS_REMOVE = BIT(8), // player is removed (MVD stream only) + MSG_PS_EXTENSIONS_2 = BIT(7), // enable more protocol extensions + MSG_PS_FORCE = BIT(8), // send even if unchanged (MVD stream only) + MSG_PS_REMOVE = BIT(9), // player is removed (MVD stream only) } msgPsFlags_t; typedef enum { @@ -85,7 +87,8 @@ typedef enum { MSG_ES_BEAMORIGIN = BIT(5), // client has RF_BEAM old_origin fix MSG_ES_SHORTANGLES = BIT(6), // higher precision angles encoding MSG_ES_EXTENSIONS = BIT(7), // enable protocol extensions - MSG_ES_REMOVE = BIT(8), // entity is removed (MVD stream only) + MSG_ES_EXTENSIONS_2 = BIT(8), // enable more protocol extensions + MSG_ES_REMOVE = BIT(9), // entity is removed (MVD stream only) } msgEsFlags_t; extern sizebuf_t msg_write; @@ -107,7 +110,8 @@ void MSG_WriteShort(int c); void MSG_WriteLong(int c); void MSG_WriteLong64(int64_t c); void MSG_WriteString(const char *s); -void MSG_WritePos(const vec3_t pos); +void MSG_WritePos(const vec3_t pos, bool extended); +void MSG_WriteIntPos(const int32_t pos[3], bool extended); void MSG_WriteAngle(float f); #if USE_CLIENT void MSG_FlushBits(void); @@ -118,7 +122,8 @@ int MSG_WriteDeltaUsercmd_Enhanced(const usercmd_t *from, const usercmd_t *c void MSG_WriteDir(const vec3_t vector); void MSG_PackEntity(entity_packed_t *out, const entity_state_t *in, const entity_state_extension_t *ext); void MSG_WriteDeltaEntity(const entity_packed_t *from, const entity_packed_t *to, msgEsFlags_t flags); -void MSG_PackPlayer(player_packed_t *out, const player_state_t *in); +void MSG_PackPlayerOld(player_packed_t *out, const player_state_old_t *in); +void MSG_PackPlayerNew(player_packed_t *out, const player_state_new_t *in); void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags); int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags); int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags); @@ -146,7 +151,7 @@ int64_t MSG_ReadLong64(void); size_t MSG_ReadString(char *dest, size_t size); size_t MSG_ReadStringLine(char *dest, size_t size); #if USE_CLIENT -void MSG_ReadPos(vec3_t pos); +void MSG_ReadPos(vec3_t pos, bool extended); void MSG_ReadDir(vec3_t vector); #endif int MSG_ReadBits(int bits); diff --git a/inc/common/pmove.h b/inc/common/pmove.h index e9071ddca..e55d6b153 100644 --- a/inc/common/pmove.h +++ b/inc/common/pmove.h @@ -34,6 +34,8 @@ typedef struct { bool strafehack; bool flyhack; bool waterhack; + byte time_shift; + byte coord_bits; float speedmult; float watermult; float maxspeed; @@ -42,7 +44,9 @@ typedef struct { float flyfriction; } pmoveParams_t; -void Pmove(pmove_t *pmove, const pmoveParams_t *params); +void PmoveOld(pmove_old_t *pmove, const pmoveParams_t *params); +void PmoveNew(pmove_new_t *pmove, const pmoveParams_t *params); void PmoveInit(pmoveParams_t *pmp); void PmoveEnableQW(pmoveParams_t *pmp); +void PmoveEnableExt(pmoveParams_t *pmp); diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 8cad36d90..6554b2b3c 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_MSGLEN 0x8000 // max length of a message, 32 KiB +<<<<<<< HEAD #define PROTOCOL_VERSION_OLD 26 #define PROTOCOL_VERSION_DEFAULT 34 #define PROTOCOL_VERSION_R1Q2 35 @@ -31,28 +32,39 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PROTOCOL_VERSION_MVD 37 // not used for UDP connections #define PROTOCOL_VERSION_AQTION 38 #define PROTOCOL_VERSION_EXTENDED 3434 +======= +#define PROTOCOL_VERSION_OLD 26 +#define PROTOCOL_VERSION_DEFAULT 34 +#define PROTOCOL_VERSION_R1Q2 35 +#define PROTOCOL_VERSION_Q2PRO 36 +#define PROTOCOL_VERSION_MVD 37 // not used for UDP connections +#define PROTOCOL_VERSION_EXTENDED_OLD 3434 +#define PROTOCOL_VERSION_EXTENDED 3435 +>>>>>>> 9b15c229 (Support more protocol extensions.) #define PROTOCOL_VERSION_R1Q2_MINIMUM 1903 // b6377 #define PROTOCOL_VERSION_R1Q2_UCMD 1904 // b7387 #define PROTOCOL_VERSION_R1Q2_LONG_SOLID 1905 // b7759 #define PROTOCOL_VERSION_R1Q2_CURRENT 1905 // b7759 -#define PROTOCOL_VERSION_Q2PRO_MINIMUM 1015 // r335 -#define PROTOCOL_VERSION_Q2PRO_RESERVED 1016 // r364 -#define PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN 1017 // r1037-8 -#define PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES 1018 // r1037-44 -#define PROTOCOL_VERSION_Q2PRO_SERVER_STATE 1019 // r1302 -#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LAYOUT 1020 // r1354 -#define PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS 1021 // r1358 -#define PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT 1022 // r2161 -#define PROTOCOL_VERSION_Q2PRO_CINEMATICS 1023 // r2263 -#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS 1024 // r2894 -#define PROTOCOL_VERSION_Q2PRO_CURRENT 1024 // r2894 +#define PROTOCOL_VERSION_Q2PRO_MINIMUM 1015 // r335 +#define PROTOCOL_VERSION_Q2PRO_RESERVED 1016 // r364 +#define PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN 1017 // r1037-8 +#define PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES 1018 // r1037-44 +#define PROTOCOL_VERSION_Q2PRO_SERVER_STATE 1019 // r1302 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LAYOUT 1020 // r1354 +#define PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS 1021 // r1358 +#define PROTOCOL_VERSION_Q2PRO_CLIENTNUM_SHORT 1022 // r2161 +#define PROTOCOL_VERSION_Q2PRO_CINEMATICS 1023 // r2263 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS 1024 // r2894 +#define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 1025 // r3300 +#define PROTOCOL_VERSION_Q2PRO_CURRENT 1025 // r3300 #define PROTOCOL_VERSION_MVD_MINIMUM 2009 // r168 #define PROTOCOL_VERSION_MVD_DEFAULT 2010 // r177 #define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS 2011 // r2894 -#define PROTOCOL_VERSION_MVD_CURRENT 2011 // r2894 +#define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2 2012 // r3300 +#define PROTOCOL_VERSION_MVD_CURRENT 2012 // r3300 #define PROTOCOL_VERSION_AQTION_MINIMUM 3011 // minimum is equivalent to PROTOCOL_VERSION_Q2PRO_ZLIB_DOWNLOADS @@ -92,6 +104,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define Q2PRO_PF_QW_MODE BIT(1) #define Q2PRO_PF_WATERJUMP_HACK BIT(2) #define Q2PRO_PF_EXTENSIONS BIT(3) +#define Q2PRO_PF_EXTENSIONS_2 BIT(4) //========================================= @@ -204,11 +217,12 @@ typedef enum { mvd_num_types } mvd_ops_t; -// MVD stream flags (only 3 bits can be used) +// MVD stream flags typedef enum { MVF_NOMSGS = BIT(0), MVF_SINGLEPOV = BIT(1), - MVF_EXTLIMITS = BIT(2) + MVF_EXTLIMITS = BIT(2), + MVF_EXTLIMITS_2 = BIT(3), } mvd_flags_t; //============================================== diff --git a/inc/shared/game.h b/inc/shared/game.h index 61ff26b01..190d9367a 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -25,10 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc., // game.h -- game dll information visible to server // +<<<<<<< HEAD #if AQTION_EXTENSION #define GAME_API_VERSION 4 #else #define GAME_API_VERSION 3 +======= +#define GAME_API_VERSION_OLD 3 // game uses gclient_old_t +#define GAME_API_VERSION_NEW 3300 // game uses gclient_new_t + +#if USE_NEW_GAME_API +#define GAME_API_VERSION GAME_API_VERSION_NEW +#else +#define GAME_API_VERSION GAME_API_VERSION_OLD +>>>>>>> 9b15c229 (Support more protocol extensions.) #endif // edict->svflags @@ -90,9 +100,24 @@ typedef struct gclient_s gclient_t; #ifndef GAME_INCLUDE -struct gclient_s { - player_state_t ps; // communicated by server to clients - int ping; +typedef struct gclient_old_s gclient_old_t; +typedef struct gclient_new_s gclient_new_t; + +struct gclient_old_s { + player_state_old_t ps; // communicated by server to clients + int ping; + + // set to (client POV entity number) - 1 by game, + // only valid if g_features has GMF_CLIENTNUM bit + int clientNum; + + // the game dll can add anything it wants after + // this point in the structure +}; + +struct gclient_new_s { + player_state_new_t ps; // communicated by server to clients + int ping; // set to (client POV entity number) - 1 by game, // only valid if g_features has GMF_CLIENTNUM bit @@ -108,7 +133,7 @@ struct gclient_s { struct edict_s { entity_state_t s; - struct gclient_s *client; + void *client; qboolean inuse; int linkcount; @@ -191,7 +216,11 @@ typedef struct { void (*linkentity)(edict_t *ent); void (*unlinkentity)(edict_t *ent); // call before removing an interactive edict int (*BoxEdicts)(const vec3_t mins, const vec3_t maxs, edict_t **list, int maxcount, int areatype); +#ifdef GAME_INCLUDE void (*Pmove)(pmove_t *pmove); // player movement code common with client prediction +#else + void (*Pmove)(void *pmove); +#endif // network messaging void (*multicast)(const vec3_t origin, multicast_t to); diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 1a2f21b29..4c6149cf5 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1047,6 +1047,7 @@ typedef enum { //KEX #define PMF_IGNORE_PLAYER_COLLISION BIT(7) +#define PMF_ON_LADDER BIT(8) //KEX @@ -1071,12 +1072,30 @@ typedef struct { short gravity; short delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters +<<<<<<< HEAD #if AQTION_EXTENSION short pm_aq2_flags; // limping, bandaging, etc unsigned short pm_timestamp; // timestamp, resets every 60 seconds byte pm_aq2_leghits; // number of leg hits #endif } pmove_state_t; +======= +} pmove_state_old_t; + +#if USE_NEW_GAME_API +typedef struct { + pmtype_t pm_type; + + int32_t origin[3]; // 19.3 + int32_t velocity[3]; // 19.3 + uint16_t pm_flags; // ducked, jump_held, etc + uint16_t pm_time; // in msec + int16_t gravity; + int16_t delta_angles[3]; // add to command angles to get view direction + // changed by spawns, rotating objects, and teleporters +} pmove_state_new_t; +#endif +>>>>>>> 9b15c229 (Support more protocol extensions.) // // button bits @@ -1096,16 +1115,17 @@ typedef struct { } usercmd_t; #define MAXTOUCH 32 + typedef struct { // state (in / out) - pmove_state_t s; + pmove_state_old_t s; // command (in) usercmd_t cmd; qboolean snapinitial; // if s has been changed outside pmove // results (out) - int numtouch; + int numtouch; struct edict_s *touchents[MAXTOUCH]; vec3_t viewangles; // clamped @@ -1114,13 +1134,41 @@ typedef struct { vec3_t mins, maxs; // bounding box size struct edict_s *groundentity; - int watertype; - int waterlevel; + int watertype; + int waterlevel; // callbacks to test the world trace_t (* q_gameabi trace)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end); int (*pointcontents)(const vec3_t point); -} pmove_t; +} pmove_old_t; + +#if USE_NEW_GAME_API +typedef struct { + // state (in / out) + pmove_state_new_t s; + + // command (in) + usercmd_t cmd; + qboolean snapinitial; // if s has been changed outside pmove + + // results (out) + int numtouch; + struct edict_s *touchents[MAXTOUCH]; + + vec3_t viewangles; // clamped + float viewheight; + + vec3_t mins, maxs; // bounding box size + + struct edict_s *groundentity; + int watertype; + int waterlevel; + + // callbacks to test the world + trace_t (* q_gameabi trace)(const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end); + int (*pointcontents)(const vec3_t point); +} pmove_new_t; +#endif // entity_state_t->effects // Effects are things handled on the client side (lights, particles, frame animations) @@ -1409,6 +1457,7 @@ enum { STAT_LAYOUTS, STAT_FRAGS, STAT_FLASHES, // cleared each frame, 1 = health, 2 = armor +<<<<<<< HEAD STAT_CLIP_ICON, STAT_CLIP, STAT_SNIPER_ICON, @@ -1431,6 +1480,14 @@ enum { #define STAT_TEAM1_HEADER 30 #define STAT_TEAM2_HEADER 31 +======= + STAT_CHASE, + STAT_SPECTATOR, +}; + +#define MAX_STATS_OLD 32 +#define MAX_STATS_NEW 64 +>>>>>>> 9b15c229 (Support more protocol extensions.) // STAT_LAYOUTS flags #define LAYOUTS_LAYOUT BIT(0) @@ -1648,7 +1705,7 @@ typedef struct { // but the number of pmove_state_t changes will be reletive to client // frame rates typedef struct { - pmove_state_t pmove; // for prediction + pmove_state_old_t pmove; // for prediction // these fields do not need to be communicated bit-precise @@ -1668,8 +1725,37 @@ typedef struct { int rdflags; // refdef flags - short stats[MAX_STATS]; // fast status bar updates -} player_state_t; + short stats[MAX_STATS_OLD]; // fast status bar updates +} player_state_old_t; + +#if USE_NEW_GAME_API +typedef struct { + pmove_state_new_t pmove; // for prediction + + // these fields do not need to be communicated bit-precise + + vec3_t viewangles; // for fixed views + vec3_t viewoffset; // add to pmovestate->origin + vec3_t kick_angles; // add to view direction to get render angles + // set by weapon kicks, pain effects, etc + + vec3_t gunangles; + vec3_t gunoffset; + int gunindex; + int gunframe; + + vec4_t blend; // rgba full screen effect + vec4_t damage_blend; + + float fov; // horizontal field of view + + int rdflags; // refdef flags + + int reserved[4]; + + int16_t stats[MAX_STATS_NEW]; // fast status bar updates +} player_state_new_t; +#endif // Reki : Cvar Sync info shared between engine and game @@ -1700,3 +1786,19 @@ typedef struct { } entity_state_extension_t; #endif + +#if USE_NEW_GAME_API + +#define MAX_STATS MAX_STATS_NEW +typedef pmove_new_t pmove_t; +typedef pmove_state_new_t pmove_state_t; +typedef player_state_new_t player_state_t; + +#else + +#define MAX_STATS MAX_STATS_OLD +typedef pmove_old_t pmove_t; +typedef pmove_state_old_t pmove_state_t; +typedef player_state_old_t player_state_t; + +#endif diff --git a/meson.build b/meson.build index a1232d792..5c3e36767 100644 --- a/meson.build +++ b/meson.build @@ -24,7 +24,9 @@ common_src = [ 'src/common/msg.c', 'src/common/net/chan.c', 'src/common/net/net.c', - 'src/common/pmove.c', + 'src/common/pmove/common.c', + 'src/common/pmove/new.c', + 'src/common/pmove/old.c', 'src/common/prompt.c', 'src/common/sizebuf.c', 'src/common/utils.c', @@ -294,11 +296,16 @@ else common_args += '-D_GNU_SOURCE' endif -engine_args = [] +engine_args = ['-DUSE_NEW_GAME_API=1'] if win32 engine_args += '-D_WIN32_WINNT=0x0601' endif +game_args = [] +if get_option('game-new-api') + game_args += '-DUSE_NEW_GAME_API=1' +endif + common_link_args = [] exe_link_args = [] dll_link_args = [] @@ -617,6 +624,7 @@ shared_library('game' + cpu, action_src, include_directories: inc_dirs, gnu_symbol_visibility: 'hidden', link_args: dll_link_args, + c_args: game_args, install: system_wide, install_dir: libdir / get_option('base-game'), override_options: get_option('game-build-options'), diff --git a/meson_options.txt b/meson_options.txt index 433ad7908..115a0cd7c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -59,6 +59,11 @@ option('game-build-options', value: [], description: 'Custom game build options for LTO, etc') +option('game-new-api', + type: 'boolean', + value: false, + description: 'Build game using new API for big maps support') + option('homedir', type: 'string', value: '~/.q2pro', diff --git a/src/client/client.h b/src/client/client.h index 2df97b140..af7b959d3 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -171,7 +171,7 @@ typedef struct { usercmd_t cmd; usercmd_t cmds[CMD_BACKUP]; // each mesage will send several old cmds unsigned cmdNumber; - short predicted_origins[CMD_BACKUP][3]; // for debug comparing against server + int predicted_origins[CMD_BACKUP][3]; // for debug comparing against server client_history_t history[CMD_BACKUP]; unsigned initialSeq; @@ -491,6 +491,7 @@ typedef struct { bool seeking; bool eof; msgEsFlags_t esFlags; // for snapshots/recording + msgPsFlags_t psFlags; } demo; #if USE_CLIENT_GTV @@ -717,6 +718,9 @@ void CL_SendCmd(void); #define CL_ES_EXTENDED_MASK \ (MSG_ES_LONGSOLID | MSG_ES_UMASK | MSG_ES_BEAMORIGIN | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS) +#define CL_ES_EXTENDED_MASK_2 (CL_ES_EXTENDED_MASK | MSG_ES_EXTENSIONS_2) +#define CL_PS_EXTENDED_MASK_2 (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) + typedef struct { int type; vec3_t pos1; diff --git a/src/client/demo.c b/src/client/demo.c index b73b4ab46..e33940898 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -174,12 +174,12 @@ static void emit_delta_frame(const server_frame_t *from, const server_frame_t *t // delta encode the playerstate MSG_WriteByte(svc_playerinfo); - MSG_PackPlayer(&newpack, &to->ps); + MSG_PackPlayerNew(&newpack, &to->ps); if (from) { - MSG_PackPlayer(&oldpack, &from->ps); - MSG_WriteDeltaPlayerstate_Default(&oldpack, &newpack, cl.psFlags); + MSG_PackPlayerNew(&oldpack, &from->ps); + MSG_WriteDeltaPlayerstate_Default(&oldpack, &newpack, cls.demo.psFlags); } else { - MSG_WriteDeltaPlayerstate_Default(NULL, &newpack, cl.psFlags); + MSG_WriteDeltaPlayerstate_Default(NULL, &newpack, cls.demo.psFlags); } // delta encode the entities @@ -1178,7 +1178,7 @@ bool CL_GetDemoInfo(const char *path, demoInfo_t *info) goto fail; } c = MSG_ReadLong(); - if (c == PROTOCOL_VERSION_EXTENDED) { + if (c == PROTOCOL_VERSION_EXTENDED || c == PROTOCOL_VERSION_EXTENDED_OLD) { csr = &cs_remap_new; } else if (c < PROTOCOL_VERSION_OLD || c > PROTOCOL_VERSION_DEFAULT) { goto fail; diff --git a/src/client/gtv.c b/src/client/gtv.c index c8c6b1d91..1c18f7c39 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -37,7 +37,7 @@ static void build_gamestate(void) memset(cls.gtv.entities, 0, sizeof(cls.gtv.entities)); // set base player states - MSG_PackPlayer(&cls.gtv.ps, &cl.frame.ps); + MSG_PackPlayerNew(&cls.gtv.ps, &cl.frame.ps); // set base entity states for (i = 1; i < cl.csr.max_edicts; i++) { @@ -51,9 +51,7 @@ static void build_gamestate(void) } // set protocol flags - cls.gtv.esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN; - if (cl.csr.extended) - cls.gtv.esFlags |= MSG_ES_LONGSOLID | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; + cls.gtv.esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN | (cl.esFlags & CL_ES_EXTENDED_MASK_2); } static void emit_gamestate(void) @@ -65,14 +63,19 @@ static void emit_gamestate(void) // send the serverdata flags = MVF_SINGLEPOV; - if (cl.csr.extended) + if (cl.csr.extended) { flags |= MVF_EXTLIMITS; - MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - if (cl.csr.extended) + if (cl.esFlags & MSG_ES_EXTENSIONS_2) + flags |= MVF_EXTLIMITS_2; + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_CURRENT); - else + MSG_WriteShort(flags); + } else { + MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_DEFAULT); + } MSG_WriteLong(cl.servercount); MSG_WriteString(cl.gamedir); MSG_WriteShort(-1); @@ -131,7 +134,7 @@ void CL_GTV_EmitFrame(void) MSG_WriteByte(0); // send player state - MSG_PackPlayer(&newps, &cl.frame.ps); + MSG_PackPlayerNew(&newps, &cl.frame.ps); MSG_WriteDeltaPlayerstate_Packet(&cls.gtv.ps, &newps, cl.clientNum, cl.psFlags | MSG_PS_FORCE); diff --git a/src/client/parse.c b/src/client/parse.c index ee6a74002..51f282da7 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -574,13 +574,19 @@ static void CL_ParseServerData(void) cls.serverProtocol, protocol); } // BIG HACK to let demos from release work with the 3.0x patch!!! - if (protocol == PROTOCOL_VERSION_EXTENDED) { + if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_EXTENDED_OLD) { cl.csr = cs_remap_new; +<<<<<<< HEAD protocol = PROTOCOL_VERSION_DEFAULT; } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_AQTION) { +======= + cls.serverProtocol = PROTOCOL_VERSION_DEFAULT; + } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_DEFAULT) { +>>>>>>> 9b15c229 (Support more protocol extensions.) Com_Error(ERR_DROP, "Demo uses unsupported protocol version %d.", protocol); + } else { + cls.serverProtocol = protocol; } - cls.serverProtocol = protocol; } // game directory @@ -742,6 +748,16 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO protocol extensions enabled\n"); cl.csr = cs_remap_new; } + if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 && + i & Q2PRO_PF_EXTENSIONS_2) { + if (!cl.csr.extended) { + Com_Error(ERR_DROP, "Q2PRO_PF_EXTENSIONS_2 without Q2PRO_PF_EXTENSIONS"); + } + Com_DPrintf("Q2PRO protocol extensions v2 enabled\n"); + cl.esFlags |= MSG_ES_EXTENSIONS_2; + cl.psFlags |= MSG_PS_EXTENSIONS_2; + PmoveEnableExt(&cl.pmp); + } } else { if (MSG_ReadByte()) { Com_DPrintf("Q2PRO strafejump hack enabled\n"); @@ -773,9 +789,17 @@ static void CL_ParseServerData(void) if (cl.csr.extended) { cl.esFlags |= CL_ES_EXTENDED_MASK; cl.psFlags |= MSG_PS_EXTENSIONS; + + // hack for demo playback + if (protocol == PROTOCOL_VERSION_EXTENDED) { + cl.esFlags |= MSG_ES_EXTENSIONS_2; + cl.psFlags |= MSG_PS_EXTENSIONS_2; + } } - cls.demo.esFlags = cl.csr.extended ? CL_ES_EXTENDED_MASK : 0; + // use full extended flags unless writing backward compatible demo + cls.demo.esFlags = cl.csr.extended ? CL_ES_EXTENDED_MASK_2 : 0; + cls.demo.psFlags = cl.csr.extended ? CL_PS_EXTENDED_MASK_2 : 0; if (cinematic) { SCR_PlayCinematic(levelname); @@ -812,6 +836,11 @@ tent_params_t te; mz_params_t mz; snd_params_t snd; +static void CL_ReadPos(vec3_t pos) +{ + MSG_ReadPos(pos, cl.esFlags & MSG_ES_EXTENSIONS_2); +} + static void CL_ParseTEntPacket(void) { te.type = MSG_ReadByte(); @@ -834,7 +863,7 @@ static void CL_ParseTEntPacket(void) case TE_ELECTRIC_SPARKS: case TE_BLUEHYPERBLASTER_2: case TE_BERSERK_SLAM: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); break; @@ -843,7 +872,7 @@ static void CL_ParseTEntPacket(void) case TE_WELDING_SPARKS: case TE_TUNNEL_SPARKS: te.count = MSG_ReadByte(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); te.color = MSG_ReadByte(); break; @@ -856,8 +885,8 @@ static void CL_ParseTEntPacket(void) case TE_BUBBLETRAIL2: case TE_BFG_LASER: case TE_BFG_ZAP: - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_GRENADE_EXPLOSION: @@ -881,7 +910,7 @@ static void CL_ParseTEntPacket(void) case TE_NUKEBLAST: case TE_EXPLOSION1_NL: case TE_EXPLOSION2_NL: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); break; case TE_PARASITE_ATTACK: @@ -891,39 +920,39 @@ static void CL_ParseTEntPacket(void) case TE_GRAPPLE_CABLE_2: case TE_LIGHTNING_BEAM: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_GRAPPLE_CABLE: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); - MSG_ReadPos(te.offset); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); + CL_ReadPos(te.offset); break; case TE_LIGHTNING: te.entity1 = MSG_ReadShort(); te.entity2 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); break; case TE_FLASHLIGHT: - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); te.entity1 = MSG_ReadShort(); break; case TE_FORCEWALL: - MSG_ReadPos(te.pos1); - MSG_ReadPos(te.pos2); + CL_ReadPos(te.pos1); + CL_ReadPos(te.pos2); te.color = MSG_ReadByte(); break; case TE_STEAM: te.entity1 = MSG_ReadShort(); te.count = MSG_ReadByte(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); MSG_ReadDir(te.dir); te.color = MSG_ReadByte(); te.entity2 = MSG_ReadShort(); @@ -934,7 +963,7 @@ static void CL_ParseTEntPacket(void) case TE_WIDOWBEAMOUT: te.entity1 = MSG_ReadShort(); - MSG_ReadPos(te.pos1); + CL_ReadPos(te.pos1); break; case TE_POWER_SPLASH: @@ -1014,7 +1043,7 @@ static void CL_ParseStartSoundPacket(void) // positioned in space if (flags & SND_POS) - MSG_ReadPos(snd.pos); + CL_ReadPos(snd.pos); SHOWNET(2, " %s\n", cl.configstrings[cl.csr.sounds + snd.index]); } diff --git a/src/client/predict.c b/src/client/predict.c index d868e1bd3..f10776478 100644 --- a/src/client/predict.c +++ b/src/client/predict.c @@ -235,7 +235,7 @@ void CL_PredictMovement(void) // run frames while (++ack <= current) { pm.cmd = cl.cmds[ack & CMD_MASK]; - Pmove(&pm, &cl.pmp); + PmoveNew(&pm, &cl.pmp); // save for debug checking VectorCopy(pm.s.origin, cl.predicted_origins[ack & CMD_MASK]); @@ -247,7 +247,7 @@ void CL_PredictMovement(void) pm.cmd.forwardmove = cl.localmove[0]; pm.cmd.sidemove = cl.localmove[1]; pm.cmd.upmove = cl.localmove[2]; - Pmove(&pm, &cl.pmp); + PmoveNew(&pm, &cl.pmp); frame = current; // save for debug checking diff --git a/src/common/msg.c b/src/common/msg.c index 9a75af6c4..caa92a066 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -172,13 +172,18 @@ void MSG_WriteString(const char *string) /* ============= -MSG_WriteCoord +MSG_WriteDeltaInt23 ============= */ - -static inline void MSG_WriteCoord(float f) +static void MSG_WriteDeltaInt23(int32_t from, int32_t to) { - MSG_WriteShort(COORD2SHORT(f)); + int32_t delta = to - from; + if (delta >= -0x4000 && delta <= 0x3fff) { + MSG_WriteShort((uint32_t)delta << 1); + } else { + byte *buf = SZ_GetSpace(&msg_write, 3); + WL24(buf, ((uint32_t)to << 1) | 1); + } } /* @@ -186,11 +191,35 @@ static inline void MSG_WriteCoord(float f) MSG_WritePos ============= */ -void MSG_WritePos(const vec3_t pos) +void MSG_WritePos(const vec3_t pos, bool extended) { - MSG_WriteCoord(pos[0]); - MSG_WriteCoord(pos[1]); - MSG_WriteCoord(pos[2]); + if (extended) { + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[0])); + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[1])); + MSG_WriteDeltaInt23(0, COORD2SHORT(pos[2])); + } else { + MSG_WriteShort(COORD2SHORT(pos[0])); + MSG_WriteShort(COORD2SHORT(pos[1])); + MSG_WriteShort(COORD2SHORT(pos[2])); + } +} + +/* +============= +MSG_WriteIntPos +============= +*/ +void MSG_WriteIntPos(const int32_t pos[3], bool extended) +{ + if (extended) { + MSG_WriteDeltaInt23(0, pos[0]); + MSG_WriteDeltaInt23(0, pos[1]); + MSG_WriteDeltaInt23(0, pos[2]); + } else { + MSG_WriteShort(pos[0]); + MSG_WriteShort(pos[1]); + MSG_WriteShort(pos[2]); + } } /* @@ -725,9 +754,15 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, else if (bits & U_RENDERFX16) MSG_WriteShort(to->renderfx); - if (bits & U_ORIGIN1) MSG_WriteShort(to->origin[0]); - if (bits & U_ORIGIN2) MSG_WriteShort(to->origin[1]); - if (bits & U_ORIGIN3) MSG_WriteShort(to->origin[2]); + if (flags & MSG_ES_EXTENSIONS_2) { + if (bits & U_ORIGIN1) MSG_WriteDeltaInt23(from->origin[0], to->origin[0]); + if (bits & U_ORIGIN2) MSG_WriteDeltaInt23(from->origin[1], to->origin[1]); + if (bits & U_ORIGIN3) MSG_WriteDeltaInt23(from->origin[2], to->origin[2]); + } else { + if (bits & U_ORIGIN1) MSG_WriteShort(to->origin[0]); + if (bits & U_ORIGIN2) MSG_WriteShort(to->origin[1]); + if (bits & U_ORIGIN3) MSG_WriteShort(to->origin[2]); + } if (bits & U_ANGLE16) { if (bits & U_ANGLE1) MSG_WriteShort(to->angles[0]); @@ -739,11 +774,8 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, if (bits & U_ANGLE3) MSG_WriteChar(to->angles[2] >> 8); } - if (bits & U_OLDORIGIN) { - MSG_WriteShort(to->old_origin[0]); - MSG_WriteShort(to->old_origin[1]); - MSG_WriteShort(to->old_origin[2]); - } + if (bits & U_OLDORIGIN) + MSG_WriteIntPos(to->old_origin, flags & MSG_ES_EXTENSIONS_2); if (bits & U_SOUND) { if (flags & MSG_ES_EXTENSIONS) { @@ -802,7 +834,33 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, out[2] = BLEND2BYTE(in[2]), \ out[3] = BLEND2BYTE(in[3])) -void MSG_PackPlayer(player_packed_t *out, const player_state_t *in) +void MSG_PackPlayerOld(player_packed_t *out, const player_state_old_t *in) +{ + out->pmove.pm_type = in->pmove.pm_type; + VectorCopy(in->pmove.origin, out->pmove.origin); + VectorCopy(in->pmove.velocity, out->pmove.velocity); + out->pmove.pm_flags = in->pmove.pm_flags; + out->pmove.pm_time = in->pmove.pm_time; + out->pmove.gravity = in->pmove.gravity; + VectorCopy(in->pmove.delta_angles, out->pmove.delta_angles); + + PACK_ANGLES(out->viewangles, in->viewangles); + PACK_OFFSET(out->viewoffset, in->viewoffset); + PACK_OFFSET(out->kick_angles, in->kick_angles); + PACK_OFFSET(out->gunoffset, in->gunoffset); + PACK_OFFSET(out->gunangles, in->gunangles); + + out->gunindex = in->gunindex; + out->gunframe = in->gunframe; + PACK_BLEND(out->blend, in->blend); + out->fov = Q_clip_uint8(in->fov); + out->rdflags = in->rdflags; + + for (int i = 0; i < MAX_STATS_OLD; i++) + out->stats[i] = in->stats[i]; +} + +void MSG_PackPlayerNew(player_packed_t *out, const player_state_new_t *in) { out->pmove = in->pmove; @@ -815,18 +873,80 @@ void MSG_PackPlayer(player_packed_t *out, const player_state_t *in) out->gunindex = in->gunindex; out->gunframe = in->gunframe; PACK_BLEND(out->blend, in->blend); + PACK_BLEND(out->damage_blend, in->damage_blend); out->fov = Q_clip_uint8(in->fov); out->rdflags = in->rdflags; - for (int i = 0; i < MAX_STATS; i++) + for (int i = 0; i < MAX_STATS_NEW; i++) out->stats[i] = in->stats[i]; } +static uint64_t MSG_CalcStatBits(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags) +{ + int numstats = (flags & MSG_PS_EXTENSIONS_2) ? MAX_STATS_NEW : MAX_STATS_OLD; + uint64_t statbits = 0; + + for (int i = 0; i < numstats; i++) + if (to->stats[i] != from->stats[i]) + statbits |= BIT_ULL(i); + + return statbits; +} + +static void MSG_WriteVarInt64(uint64_t v) +{ + do { + int c = v & 0x7f; + v >>= 7; + if (v) + c |= 0x80; + MSG_WriteByte(c); + } while (v); +} + +static void MSG_WriteStats(const player_packed_t *to, uint64_t statbits, msgPsFlags_t flags) +{ + int numstats; + + if (flags & MSG_PS_EXTENSIONS_2) { + MSG_WriteVarInt64(statbits); + numstats = MAX_STATS_NEW; + } else { + MSG_WriteLong(statbits); + numstats = MAX_STATS_OLD; + } + + for (int i = 0; i < numstats; i++) + if (statbits & BIT_ULL(i)) + MSG_WriteShort(to->stats[i]); +} + +static void MSG_WriteDeltaBlend(const player_packed_t *from, const player_packed_t *to) +{ + int i, bflags = 0; + + for (i = 0; i < 4; i++) { + if (to->blend[i] != from->blend[i]) + bflags |= BIT(i); + if (to->damage_blend[i] != from->damage_blend[i]) + bflags |= BIT(4 + i); + } + + MSG_WriteByte(bflags); + + for (i = 0; i < 4; i++) + if (bflags & BIT(i)) + MSG_WriteByte(to->blend[i]); + + for (i = 0; i < 4; i++) + if (bflags & BIT(4 + i)) + MSG_WriteByte(to->damage_blend[i]); +} + void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags) { - int i; - int pflags; - int statbits; + int pflags; + uint64_t statbits; Q_assert(to); @@ -870,6 +990,9 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (!Vector4Compare(to->blend, from->blend)) pflags |= PS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PS_BLEND; if (to->fov != from->fov) pflags |= PS_FOV; @@ -896,23 +1019,43 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (pflags & PS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - MSG_WriteShort(to->pmove.origin[2]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); + } - if (pflags & PS_M_VELOCITY) { - MSG_WriteShort(to->pmove.velocity[0]); - MSG_WriteShort(to->pmove.velocity[1]); - MSG_WriteShort(to->pmove.velocity[2]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteDeltaInt23(from->pmove.velocity[0], to->pmove.velocity[0]); + MSG_WriteDeltaInt23(from->pmove.velocity[1], to->pmove.velocity[1]); + MSG_WriteDeltaInt23(from->pmove.velocity[2], to->pmove.velocity[2]); + } - if (pflags & PS_M_TIME) - MSG_WriteByte(to->pmove.pm_time); + if (pflags & PS_M_TIME) + MSG_WriteShort(to->pmove.pm_time); - if (pflags & PS_M_FLAGS) - MSG_WriteByte(to->pmove.pm_flags); + if (pflags & PS_M_FLAGS) + MSG_WriteShort(to->pmove.pm_flags); + } else { + if (pflags & PS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + MSG_WriteShort(to->pmove.origin[2]); + } + + if (pflags & PS_M_VELOCITY) { + MSG_WriteShort(to->pmove.velocity[0]); + MSG_WriteShort(to->pmove.velocity[1]); + MSG_WriteShort(to->pmove.velocity[2]); + } + + if (pflags & PS_M_TIME) + MSG_WriteByte(to->pmove.pm_time); + + if (pflags & PS_M_FLAGS) + MSG_WriteByte(to->pmove.pm_flags); + } if (pflags & PS_M_GRAVITY) MSG_WriteShort(to->pmove.gravity); @@ -951,8 +1094,12 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteData(to->gunangles, sizeof(to->gunangles)); } - if (pflags & PS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -961,24 +1108,16 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteByte(to->rdflags); // send stats - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); + statbits = MSG_CalcStatBits(from, to, flags); + MSG_WriteStats(to, statbits, flags); } int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags) { - int i; - int pflags, eflags; - int statbits; + int pflags, eflags; + uint64_t statbits; Q_assert(to); @@ -1054,9 +1193,13 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (!(flags & MSG_PS_IGNORE_BLEND)) { if (!Vector4Compare(from->blend, to->blend)) pflags |= PS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PS_BLEND; } else { // save previous state Vector4Copy(from->blend, to->blend); + Vector4Copy(from->damage_blend, to->damage_blend); } if (from->fov != to->fov) @@ -1089,11 +1232,7 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, VectorCopy(from->gunangles, to->gunangles); } - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - + statbits = MSG_CalcStatBits(from, to, flags); if (statbits) eflags |= EPS_STATS; @@ -1108,27 +1247,51 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (pflags & PS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + } - if (eflags & EPS_M_ORIGIN2) - MSG_WriteShort(to->pmove.origin[2]); + if (eflags & EPS_M_ORIGIN2) + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); - if (pflags & PS_M_VELOCITY) { - MSG_WriteShort(to->pmove.velocity[0]); - MSG_WriteShort(to->pmove.velocity[1]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteDeltaInt23(from->pmove.velocity[0], to->pmove.velocity[0]); + MSG_WriteDeltaInt23(from->pmove.velocity[1], to->pmove.velocity[1]); + } - if (eflags & EPS_M_VELOCITY2) - MSG_WriteShort(to->pmove.velocity[2]); + if (eflags & EPS_M_VELOCITY2) + MSG_WriteDeltaInt23(from->pmove.velocity[2], to->pmove.velocity[2]); - if (pflags & PS_M_TIME) - MSG_WriteByte(to->pmove.pm_time); + if (pflags & PS_M_TIME) + MSG_WriteShort(to->pmove.pm_time); - if (pflags & PS_M_FLAGS) - MSG_WriteByte(to->pmove.pm_flags); + if (pflags & PS_M_FLAGS) + MSG_WriteShort(to->pmove.pm_flags); + } else { + if (pflags & PS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + } + + if (eflags & EPS_M_ORIGIN2) + MSG_WriteShort(to->pmove.origin[2]); + + if (pflags & PS_M_VELOCITY) { + MSG_WriteShort(to->pmove.velocity[0]); + MSG_WriteShort(to->pmove.velocity[1]); + } + + if (eflags & EPS_M_VELOCITY2) + MSG_WriteShort(to->pmove.velocity[2]); + + if (pflags & PS_M_TIME) + MSG_WriteByte(to->pmove.pm_time); + + if (pflags & PS_M_FLAGS) + MSG_WriteByte(to->pmove.pm_flags); + } if (pflags & PS_M_GRAVITY) MSG_WriteShort(to->pmove.gravity); @@ -1172,8 +1335,12 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (eflags & EPS_GUNANGLES) MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1182,12 +1349,8 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, MSG_WriteByte(to->rdflags); // send stats - if (eflags & EPS_STATS) { - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); - } + if (eflags & EPS_STATS) + MSG_WriteStats(to, statbits, flags); return eflags; } @@ -1478,9 +1641,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, int number, msgPsFlags_t flags) { - int i; - int pflags; - int statbits; + int pflags; + uint64_t statbits; // this can happen with client GTV if (number < 0 || number >= CLIENTNUM_NONE) @@ -1523,8 +1685,13 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (!VectorCompare(from->kick_angles, to->kick_angles)) pflags |= PPS_KICKANGLES; - if (!(flags & MSG_PS_IGNORE_BLEND) && !Vector4Compare(from->blend, to->blend)) - pflags |= PPS_BLEND; + if (!(flags & MSG_PS_IGNORE_BLEND)) { + if (!Vector4Compare(from->blend, to->blend)) + pflags |= PPS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PPS_BLEND; + } if (from->fov != to->fov) pflags |= PPS_FOV; @@ -1546,11 +1713,7 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, pflags |= PPS_GUNANGLES; } - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= BIT(i); - + statbits = MSG_CalcStatBits(from, to, flags); if (statbits) pflags |= PPS_STATS; @@ -1572,13 +1735,23 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_M_TYPE) MSG_WriteByte(to->pmove.pm_type); - if (pflags & PPS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PPS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + } + + if (pflags & PPS_M_ORIGIN2) + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); + } else { + if (pflags & PPS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + } - if (pflags & PPS_M_ORIGIN2) - MSG_WriteShort(to->pmove.origin[2]); + if (pflags & PPS_M_ORIGIN2) + MSG_WriteShort(to->pmove.origin[2]); + } // // write the rest of the player_state_t @@ -1613,8 +1786,12 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (pflags & PPS_GUNANGLES) MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PPS_BLEND) - MSG_WriteData(to->blend, sizeof(to->blend)); + if (pflags & PPS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } if (pflags & PPS_FOV) MSG_WriteByte(to->fov); @@ -1623,12 +1800,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, MSG_WriteByte(to->rdflags); // send stats - if (pflags & PPS_STATS) { - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - MSG_WriteShort(to->stats[i]); - } + if (pflags & PPS_STATS) + MSG_WriteStats(to, statbits, flags); } #endif // USE_MVD_SERVER || USE_MVD_CLIENT || USE_CLIENT_GTV @@ -1790,16 +1963,6 @@ static inline float MSG_ReadCoord(void) return SHORT2COORD(MSG_ReadShort()); } -#if USE_SERVER -static inline -#endif -void MSG_ReadPos(vec3_t pos) -{ - pos[0] = MSG_ReadCoord(); - pos[1] = MSG_ReadCoord(); - pos[2] = MSG_ReadCoord(); -} - static inline float MSG_ReadAngle(void) { return BYTE2ANGLE(MSG_ReadChar()); @@ -1810,8 +1973,47 @@ static inline float MSG_ReadAngle16(void) return SHORT2ANGLE(MSG_ReadShort()); } +static void MSG_ReadDeltaInt23(int32_t *to) +{ + uint32_t v = MSG_ReadWord(); + if (v & 1) { + v |= (uint32_t)MSG_ReadByte() << 16; + *to = SignExtend(v >> 1, 23); + } else { + *to += SignExtend(v >> 1, 15); + } +} + +static void MSG_ReadDeltaCoord(float *to) +{ + uint32_t v = MSG_ReadWord(); + if (v & 1) { + v |= (uint32_t)MSG_ReadByte() << 16; + *to = SHORT2COORD(SignExtend(v >> 1, 23)); + } else { + *to += SHORT2COORD(SignExtend(v >> 1, 15)); + } +} + #endif +#if USE_SERVER +static inline +#endif +void MSG_ReadPos(vec3_t pos, bool extended) +{ + if (extended) { + VectorClear(pos); + MSG_ReadDeltaCoord(&pos[0]); + MSG_ReadDeltaCoord(&pos[1]); + MSG_ReadDeltaCoord(&pos[2]); + } else { + pos[0] = MSG_ReadCoord(); + pos[1] = MSG_ReadCoord(); + pos[2] = MSG_ReadCoord(); + } +} + #if USE_CLIENT void MSG_ReadDir(vec3_t dir) { @@ -2260,9 +2462,15 @@ void MSG_ParseDeltaEntity(entity_state_t *to, else if (bits & U_RENDERFX16) to->renderfx = MSG_ReadWord(); - if (bits & U_ORIGIN1) to->origin[0] = MSG_ReadCoord(); - if (bits & U_ORIGIN2) to->origin[1] = MSG_ReadCoord(); - if (bits & U_ORIGIN3) to->origin[2] = MSG_ReadCoord(); + if (flags & MSG_ES_EXTENSIONS_2) { + if (bits & U_ORIGIN1) MSG_ReadDeltaCoord(&to->origin[0]); + if (bits & U_ORIGIN2) MSG_ReadDeltaCoord(&to->origin[1]); + if (bits & U_ORIGIN3) MSG_ReadDeltaCoord(&to->origin[2]); + } else { + if (bits & U_ORIGIN1) to->origin[0] = MSG_ReadCoord(); + if (bits & U_ORIGIN2) to->origin[1] = MSG_ReadCoord(); + if (bits & U_ORIGIN3) to->origin[2] = MSG_ReadCoord(); + } if (flags & MSG_ES_SHORTANGLES && bits & U_ANGLE16) { if (bits & U_ANGLE1) to->angles[0] = MSG_ReadAngle16(); @@ -2275,7 +2483,7 @@ void MSG_ParseDeltaEntity(entity_state_t *to, } if (bits & U_OLDORIGIN) - MSG_ReadPos(to->old_origin); + MSG_ReadPos(to->old_origin, flags & MSG_ES_EXTENSIONS_2); if (bits & U_SOUND) { if (flags & MSG_ES_EXTENSIONS) { @@ -2323,6 +2531,63 @@ void MSG_ParseDeltaEntity(entity_state_t *to, #endif // USE_CLIENT || USE_MVD_CLIENT +static uint64_t MSG_ReadVarInt64(void) +{ + uint64_t v = 0; + int c, bits = 0; + + do { + c = MSG_ReadByte(); + if (c == -1) + break; + v |= (c & UINT64_C(0x7f)) << bits; + bits += 7; + } while (c & 0x80 && bits < 64); + + return v; +} + +static void MSG_ReadStats(player_state_t *to, msgPsFlags_t flags) +{ + uint64_t statbits; + int numstats; + + if (flags & MSG_PS_EXTENSIONS_2) { + statbits = MSG_ReadVarInt64(); + numstats = MAX_STATS_NEW; + } else { + statbits = MSG_ReadLong(); + numstats = MAX_STATS_OLD; + } + + if (!statbits) + return; + + for (int i = 0; i < numstats; i++) + if (statbits & BIT_ULL(i)) + to->stats[i] = MSG_ReadShort(); +} + +static void MSG_ReadBlend(player_state_t *to, msgPsFlags_t psflags) +{ + if (psflags & MSG_PS_EXTENSIONS_2) { + int bflags = MSG_ReadByte(); + + for (int i = 0; i < 4; i++) + if (bflags & BIT(i)) + to->blend[i] = MSG_ReadByte() / 255.0f; + + for (int i = 0; i < 4; i++) + if (bflags & BIT(4 + i)) + to->damage_blend[i] = MSG_ReadByte() / 255.0f; + } else { + to->blend[0] = MSG_ReadByte() / 255.0f; + to->blend[1] = MSG_ReadByte() / 255.0f; + to->blend[2] = MSG_ReadByte() / 255.0f; + to->blend[3] = MSG_ReadByte() / 255.0f; + } +} + #if USE_CLIENT /* @@ -2335,9 +2600,6 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, int flags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -2353,23 +2615,43 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, if (flags & PS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - to->pmove.origin[2] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + MSG_ReadDeltaInt23(&to->pmove.origin[2]); + } - if (flags & PS_M_VELOCITY) { - to->pmove.velocity[0] = MSG_ReadShort(); - to->pmove.velocity[1] = MSG_ReadShort(); - to->pmove.velocity[2] = MSG_ReadShort(); - } + if (flags & PS_M_VELOCITY) { + MSG_ReadDeltaInt23(&to->pmove.velocity[0]); + MSG_ReadDeltaInt23(&to->pmove.velocity[1]); + MSG_ReadDeltaInt23(&to->pmove.velocity[2]); + } - if (flags & PS_M_TIME) - to->pmove.pm_time = MSG_ReadByte(); + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadWord(); - if (flags & PS_M_FLAGS) - to->pmove.pm_flags = MSG_ReadByte(); + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadWord(); + } else { + if (flags & PS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + to->pmove.origin[2] = MSG_ReadShort(); + } + + if (flags & PS_M_VELOCITY) { + to->pmove.velocity[0] = MSG_ReadShort(); + to->pmove.velocity[1] = MSG_ReadShort(); + to->pmove.velocity[2] = MSG_ReadShort(); + } + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadByte(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadByte(); + } if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); @@ -2418,12 +2700,8 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2432,15 +2710,9 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - statbits = MSG_ReadLong(); - if (statbits) { - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + MSG_ReadStats(to, psflags); } - /* =================== MSG_ParseDeltaPlayerstate_Default @@ -2452,9 +2724,6 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, int extraflags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -2470,11 +2739,53 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, if (flags & PS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + } + + if (extraflags & EPS_M_ORIGIN2) + MSG_ReadDeltaInt23(&to->pmove.origin[2]); + + if (flags & PS_M_VELOCITY) { + MSG_ReadDeltaInt23(&to->pmove.velocity[0]); + MSG_ReadDeltaInt23(&to->pmove.velocity[1]); + } + + if (extraflags & EPS_M_VELOCITY2) + MSG_ReadDeltaInt23(&to->pmove.velocity[2]); + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadWord(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadWord(); + } else { + if (flags & PS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + } + + if (extraflags & EPS_M_ORIGIN2) + to->pmove.origin[2] = MSG_ReadShort(); + + if (flags & PS_M_VELOCITY) { + to->pmove.velocity[0] = MSG_ReadShort(); + to->pmove.velocity[1] = MSG_ReadShort(); + } + + if (extraflags & EPS_M_VELOCITY2) + to->pmove.velocity[2] = MSG_ReadShort(); + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadByte(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadByte(); } +<<<<<<< HEAD if (extraflags & EPS_M_ORIGIN2) to->pmove.origin[2] = MSG_ReadShort(); @@ -2498,6 +2809,8 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->pmove.pm_flags = MSG_ReadByte(); } +======= +>>>>>>> 9b15c229 (Support more protocol extensions.) if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); @@ -2552,12 +2865,8 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2566,12 +2875,8 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - if (extraflags & EPS_STATS) { - statbits = MSG_ReadLong(); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + if (extraflags & EPS_STATS) + MSG_ReadStats(to, psflags); } @@ -2754,9 +3059,6 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, int flags, msgPsFlags_t psflags) { - int i; - int statbits; - Q_assert(to); // clear to old value before delta parsing @@ -2772,13 +3074,23 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, if (flags & PPS_M_TYPE) to->pmove.pm_type = MSG_ReadByte(); - if (flags & PPS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PPS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + } - if (flags & PPS_M_ORIGIN2) - to->pmove.origin[2] = MSG_ReadShort(); + if (flags & PPS_M_ORIGIN2) + MSG_ReadDeltaInt23(&to->pmove.origin[2]); + } else { + if (flags & PPS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + } + + if (flags & PPS_M_ORIGIN2) + to->pmove.origin[2] = MSG_ReadShort(); + } // // parse the rest of the player_state_t @@ -2825,12 +3137,8 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, to->gunangles[2] = MSG_ReadChar() * 0.25f; } - if (flags & PPS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (flags & PPS_BLEND) + MSG_ReadBlend(to, psflags); if (flags & PPS_FOV) to->fov = MSG_ReadByte(); @@ -2839,12 +3147,8 @@ void MSG_ParseDeltaPlayerstate_Packet(const player_state_t *from, to->rdflags = MSG_ReadByte(); // parse stats - if (flags & PPS_STATS) { - statbits = MSG_ReadLong(); - for (i = 0; i < MAX_STATS; i++) - if (statbits & BIT(i)) - to->stats[i] = MSG_ReadShort(); - } + if (flags & PPS_STATS) + MSG_ReadStats(to, psflags); } #endif // USE_MVD_CLIENT diff --git a/src/common/pmove/common.c b/src/common/pmove/common.c new file mode 100644 index 000000000..e2d09cd63 --- /dev/null +++ b/src/common/pmove/common.c @@ -0,0 +1,52 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +void PmoveInit(pmoveParams_t *pmp) +{ + // set up default pmove parameters + memset(pmp, 0, sizeof(*pmp)); + + pmp->speedmult = 1; + pmp->watermult = 0.5f; + pmp->maxspeed = 300; + pmp->friction = 6; + pmp->waterfriction = 1; + pmp->flyfriction = 9; + pmp->time_shift = 3; + pmp->coord_bits = 16; +} + +void PmoveEnableQW(pmoveParams_t *pmp) +{ + pmp->qwmode = true; + pmp->watermult = 0.7f; + pmp->maxspeed = 320; + //pmp->upspeed = (sv_qwmod->integer > 1) ? 310 : 350; + pmp->friction = 4; + pmp->waterfriction = 4; + pmp->airaccelerate = true; +} + +void PmoveEnableExt(pmoveParams_t *pmp) +{ + pmp->time_shift = 0; + pmp->coord_bits = 23; +} diff --git a/src/common/pmove/new.c b/src/common/pmove/new.c new file mode 100644 index 000000000..a47f3f4c2 --- /dev/null +++ b/src/common/pmove/new.c @@ -0,0 +1,27 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +#define PMOVE_NEW 1 +#define PMOVE_TYPE pmove_new_t +#define PMOVE_FUNC PmoveNew +#define PMOVE_TIME_SHIFT pmp->time_shift +#define PMOVE_C2S(x) SignExtend(COORD2SHORT(x), pmp->coord_bits) +#include "template.c" diff --git a/src/common/pmove/old.c b/src/common/pmove/old.c new file mode 100644 index 000000000..6060e7ef1 --- /dev/null +++ b/src/common/pmove/old.c @@ -0,0 +1,27 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "shared/shared.h" +#include "common/pmove.h" + +#define PMOVE_OLD 1 +#define PMOVE_TYPE pmove_old_t +#define PMOVE_FUNC PmoveOld +#define PMOVE_TIME_SHIFT 3 +#define PMOVE_C2S(x) COORD2SHORT(x) +#include "template.c" diff --git a/src/common/pmove.c b/src/common/pmove/template.c similarity index 96% rename from src/common/pmove.c rename to src/common/pmove/template.c index 9472b28e9..facbc7681 100644 --- a/src/common/pmove.c +++ b/src/common/pmove/template.c @@ -16,9 +16,6 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "shared/shared.h" -#include "common/pmove.h" - #define STEPSIZE 18 // all of the locals will be zeroed before each @@ -36,11 +33,11 @@ typedef struct { cplane_t groundplane; int groundcontents; - short previous_origin[3]; + int previous_origin[3]; bool ladder; } pml_t; -static pmove_t *pm; +static PMOVE_TYPE *pm; static pml_t pml; static const pmoveParams_t *pmp; @@ -596,9 +593,9 @@ static void PM_CategorizePosition(void) pm->s.pm_flags |= PMF_TIME_LAND; // don't allow another jump for a little while if (pml.velocity[2] < -400) - pm->s.pm_time = 25; + pm->s.pm_time = 200 >> PMOVE_TIME_SHIFT; else - pm->s.pm_time = 18; + pm->s.pm_time = 144 >> PMOVE_TIME_SHIFT; } } } @@ -741,7 +738,7 @@ static void PM_CheckSpecialMovement(void) pml.velocity[2] = 350; pm->s.pm_flags |= PMF_TIME_WATERJUMP; - pm->s.pm_time = 255; + pm->s.pm_time = 2040 >> PMOVE_TIME_SHIFT; } /* @@ -924,20 +921,20 @@ static void PM_SnapPosition(void) { int sign[3]; int i, j, bits; - short base[3]; + int base[3]; // try all single bits first - static const byte jitterbits[8] = {0, 4, 1, 2, 3, 5, 6, 7}; + static const byte jitterbits[8] = { 0, 4, 1, 2, 3, 5, 6, 7 }; // snap velocity to eigths for (i = 0; i < 3; i++) - pm->s.velocity[i] = (int)(pml.velocity[i] * 8); + pm->s.velocity[i] = PMOVE_C2S(pml.velocity[i]); for (i = 0; i < 3; i++) { if (pml.origin[i] >= 0) sign[i] = 1; else sign[i] = -1; - pm->s.origin[i] = (int)(pml.origin[i] * 8); + pm->s.origin[i] = PMOVE_C2S(pml.origin[i]); if (pm->s.origin[i] * 0.125f == pml.origin[i]) sign[i] = 0; } @@ -968,8 +965,8 @@ PM_InitialSnapPosition static void PM_InitialSnapPosition(void) { int x, y, z; - short base[3]; - static const short offset[3] = { 0, -1, 1 }; + int base[3]; + static const int offset[3] = { 0, -1, 1 }; VectorCopy(pm->s.origin, base); @@ -1024,7 +1021,7 @@ Pmove Can be called by either the server or the client ================ */ -void Pmove(pmove_t *pmove, const pmoveParams_t *params) +void PMOVE_FUNC(PMOVE_TYPE *pmove, const pmoveParams_t *params) { pm = pmove; pmp = params; @@ -1109,7 +1106,7 @@ void Pmove(pmove_t *pmove, const pmoveParams_t *params) if (pm->s.pm_time) { int msec; - msec = pm->cmd.msec >> 3; + msec = pm->cmd.msec >> PMOVE_TIME_SHIFT; if (!msec) msec = 1; if (msec >= pm->s.pm_time) { @@ -1156,28 +1153,12 @@ void Pmove(pmove_t *pmove, const pmoveParams_t *params) PM_CategorizePosition(); PM_SnapPosition(); -} - -void PmoveInit(pmoveParams_t *pmp) -{ - // set up default pmove parameters - memset(pmp, 0, sizeof(*pmp)); - - pmp->speedmult = 1; - pmp->watermult = 0.5f; - pmp->maxspeed = 300; - pmp->friction = 6; - pmp->waterfriction = 1; - pmp->flyfriction = 9; -} -void PmoveEnableQW(pmoveParams_t *pmp) -{ - pmp->qwmode = true; - pmp->watermult = 0.7f; - pmp->maxspeed = 320; - //pmp->upspeed = (sv_qwmod->integer > 1) ? 310 : 350; - pmp->friction = 4; - pmp->waterfriction = 4; - pmp->airaccelerate = true; +#ifdef PMOVE_NEW + // export "on ladder" flag to game + if (pml.ladder) + pm->s.pm_flags |= PMF_ON_LADDER; + else + pm->s.pm_flags &= ~PMF_ON_LADDER; +#endif } diff --git a/src/game/g_local.h b/src/game/g_local.h index 23ff67cdd..124d53561 100644 --- a/src/game/g_local.h +++ b/src/game/g_local.h @@ -252,6 +252,14 @@ typedef struct precache_s { void (*func)(void); } precache_t; +// new game API can be used w/o protocol extensions, +// so this needs to be dynamic +#if USE_NEW_GAME_API +#define PM_TIME_SHIFT (game.csr.extended ? 0 : 3) +#else +#define PM_TIME_SHIFT 3 +#endif + // // this structure is left intact through an entire game // it should be initialized at dll load time, and read/written to diff --git a/src/game/g_misc.c b/src/game/g_misc.c index ee3c9aec4..bcfbc1778 100644 --- a/src/game/g_misc.c +++ b/src/game/g_misc.c @@ -1635,7 +1635,7 @@ void teleporter_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t // clear the velocity and hold them in place briefly VectorClear(other->velocity); - other->client->ps.pmove.pm_time = 160 >> 3; // hold time + other->client->ps.pmove.pm_time = 160 >> PM_TIME_SHIFT; // hold time other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT; // draw the teleport splash at source and on the player diff --git a/src/game/g_save.c b/src/game/g_save.c index dfa4053c3..e6af7e1b5 100644 --- a/src/game/g_save.c +++ b/src/game/g_save.c @@ -305,10 +305,17 @@ static const save_field_t clientfields[] = { #define _OFS CLOFS I(ps.pmove.pm_type), +#if USE_NEW_GAME_API + IA(ps.pmove.origin, 3), + IA(ps.pmove.velocity, 3), + S(ps.pmove.pm_flags), + S(ps.pmove.pm_time), +#else SA(ps.pmove.origin, 3), SA(ps.pmove.velocity, 3), B(ps.pmove.pm_flags), B(ps.pmove.pm_time), +#endif S(ps.pmove.gravity), SA(ps.pmove.delta_angles, 3), @@ -799,7 +806,11 @@ static void read_fields(gzFile f, const save_field_t *fields, void *base) #define SAVE_MAGIC1 MakeLittleLong('S','S','V','1') #define SAVE_MAGIC2 MakeLittleLong('S','A','V','1') +#if USE_NEW_GAME_API +#define SAVE_VERSION 0x100 +#else #define SAVE_VERSION 8 +#endif static void check_gzip(int magic) { diff --git a/src/game/p_client.c b/src/game/p_client.c index 43c204863..a36f9db2d 100644 --- a/src/game/p_client.c +++ b/src/game/p_client.c @@ -938,7 +938,7 @@ void respawn(edict_t *self) // hold in place briefly self->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - self->client->ps.pmove.pm_time = 14; + self->client->ps.pmove.pm_time = 112 >> PM_TIME_SHIFT; self->client->respawn_framenum = level.framenum; @@ -1018,7 +1018,7 @@ void spectator_respawn(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 14; + ent->client->ps.pmove.pm_time = 112 >> PM_TIME_SHIFT; } ent->client->respawn_framenum = level.framenum; @@ -1236,7 +1236,7 @@ void ClientBeginDeathmatch(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 200 >> 3; + ent->client->ps.pmove.pm_time = 200 >> PM_TIME_SHIFT; } gi.bprintf(PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname); @@ -1284,7 +1284,7 @@ void ClientBegin(edict_t *ent) // hold in place briefly ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; - ent->client->ps.pmove.pm_time = 200 >> 3; + ent->client->ps.pmove.pm_time = 200 >> PM_TIME_SHIFT; } if (level.intermission_framenum) { diff --git a/src/server/commands.c b/src/server/commands.c index 220ebf184..a9755d2fa 100644 --- a/src/server/commands.c +++ b/src/server/commands.c @@ -532,7 +532,7 @@ static void dump_clients(void) } Com_Printf("%3i %5i %s %-15.15s %7u %-21s %5i %2i %3i\n", client->number, - client->edict->client->ps.stats[STAT_FRAGS], + SV_GetClient_Stat(client, STAT_FRAGS), ping, client->name, svs.realtime - client->lastmessage, NET_AdrToString(&client->netchan.remote_address), client->rate, client->protocol, client->moves_per_sec); diff --git a/src/server/entities.c b/src/server/entities.c index 44546a1b4..edcba524f 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -423,7 +423,7 @@ bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) MSG_WriteData(frame->areabits, frame->areabytes); // ignore some parts of playerstate if not recording demo - psFlags = 0; + psFlags = client->psFlags; if (!client->settings[CLS_RECORDING]) { if (client->settings[CLS_NOGUN]) { psFlags |= MSG_PS_IGNORE_GUNFRAMES; @@ -456,9 +456,6 @@ bool SV_WriteFrameToClient_Enhanced(client_t *client, unsigned maxsize) } else { suppressed = client->suppress_count; } - if (client->csr->extended) { - psFlags |= MSG_PS_EXTENSIONS; - } // delta encode the playerstate extraflags = MSG_WriteDeltaPlayerstate_Enhanced(oldstate, &frame->ps, psFlags); @@ -730,7 +727,6 @@ void SV_BuildClientFrame(client_t *client) edict_t *clent; client_frame_t *frame; entity_packed_t *state; - player_state_t *ps; const mleaf_t *leaf; int clientarea, clientcluster; byte clientphs[VIS_MAX_BYTES]; @@ -758,8 +754,7 @@ void SV_BuildClientFrame(client_t *client) client->frames_sent++; // find the client's PVS - ps = &clent->client->ps; - VectorMA(ps->viewoffset, 0.125f, ps->pmove.origin, org); + SV_GetClient_ViewOrg(client, org); leaf = CM_PointLeaf(client->cm, org); clientarea = leaf->area; @@ -773,11 +768,14 @@ void SV_BuildClientFrame(client_t *client) } // grab the current player_state_t - MSG_PackPlayer(&frame->ps, ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&frame->ps, clent->client); + else + MSG_PackPlayerOld(&frame->ps, clent->client); // grab the current clientNum if (g_features->integer & GMF_CLIENTNUM) { - frame->clientNum = clent->client->clientNum; + frame->clientNum = SV_GetClient_ClientNum(client); if (!VALIDATE_CLIENTNUM(client->csr, frame->clientNum)) { Com_WPrintf("%s: bad clientNum %d for client %d\n", __func__, frame->clientNum, client->number); diff --git a/src/server/game.c b/src/server/game.c index c6364b99d..030853c23 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -440,6 +440,11 @@ static void PF_WriteFloat(float f) Com_Error(ERR_DROP, "PF_WriteFloat not implemented"); } +static void PF_WritePos(const vec3_t pos) +{ + MSG_WritePos(pos, svs.csr.extended && IS_NEW_GAME_API); +} + static qboolean PF_inVIS(const vec3_t p1, const vec3_t p2, vis_t vis) { const mleaf_t *leaf1, *leaf2; @@ -586,7 +591,7 @@ static void SV_StartSound(const vec3_t origin, edict_t *edict, MSG_WriteByte(ofs); MSG_WriteShort(sendchan); - MSG_WritePos(origin); + PF_WritePos(origin); // if the sound doesn't attenuate, send it to everyone // (global radio chatter, voiceovers, etc) @@ -704,13 +709,14 @@ static void PF_LocalSound(edict_t *target, const vec3_t origin, PF_Unicast(target, !!(channel & CHAN_RELIABLE)); } -void PF_Pmove(pmove_t *pm) +void PF_Pmove(void *pm) { - if (sv_client) { - Pmove(pm, &sv_client->pmp); - } else { - Pmove(pm, &sv_pmp); - } + const pmoveParams_t *pmp = sv_client ? &sv_client->pmp : &svs.pmp; + + if (IS_NEW_GAME_API) + PmoveNew(pm, pmp); + else + PmoveOld(pm, pmp); } static cvar_t *PF_cvar(const char *name, const char *value, int flags) @@ -813,7 +819,7 @@ static const game_import_t game_import = { .WriteLong = MSG_WriteLong, .WriteFloat = PF_WriteFloat, .WriteString = MSG_WriteString, - .WritePosition = MSG_WritePos, + .WritePosition = PF_WritePos, .WriteDir = MSG_WriteDir, .WriteAngle = MSG_WriteAngle, @@ -1141,9 +1147,11 @@ void SV_InitGameProgs(void) Com_Error(ERR_DROP, "Game library returned NULL exports"); } - if (ge->apiversion != GAME_API_VERSION) { + Com_DPrintf("Game API version: %d\n", ge->apiversion); + + if (ge->apiversion != GAME_API_VERSION_OLD && ge->apiversion != GAME_API_VERSION_NEW) { Com_Error(ERR_DROP, "Game library is version %d, expected %d", - ge->apiversion, GAME_API_VERSION); + ge->apiversion, GAME_API_VERSION_OLD); } // get extended api if present diff --git a/src/server/init.c b/src/server/init.c index a493901b4..a0f5fddd9 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -426,6 +426,9 @@ void SV_InitGame(unsigned mvd_spawn) svs.csr = cs_remap_old; + // set up default pmove parameters + PmoveInit(&svs.pmp); + // init game #if USE_MVD_CLIENT if (mvd_spawn) { @@ -442,6 +445,9 @@ void SV_InitGame(unsigned mvd_spawn) SV_MvdPostInit(); } + if (svs.csr.extended && IS_NEW_GAME_API) + PmoveEnableExt(&svs.pmp); + // send heartbeat very soon svs.last_heartbeat = -(HEARTBEAT_SECONDS - 5) * 1000; svs.heartbeat_index = 0; diff --git a/src/server/main.c b/src/server/main.c index 7836b6ea8..40afa535c 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -19,10 +19,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" #include "client/input.h" +<<<<<<< HEAD bot_client_t bot_clients[MAX_CLIENTS]; pmoveParams_t sv_pmp; +======= +>>>>>>> 9b15c229 (Support more protocol extensions.) master_t sv_masters[MAX_MASTERS]; // address of group servers LIST_DECL(sv_banlist); @@ -454,7 +457,7 @@ static size_t SV_StatusString(char *status) } len = Q_snprintf(entry, sizeof(entry), "%i %i \"%s\"\n", - cl->edict->client->ps.stats[STAT_FRAGS], + SV_GetClient_Stat(cl, STAT_FRAGS), cl->ping, cl->name); if (len >= sizeof(entry)) { continue; @@ -865,11 +868,23 @@ static bool parse_enhanced_params(conn_params_t *p) } - if (!CLIENT_COMPATIBLE(&svs.csr, p)) { + // verify protocol extensions compatibility + if (svs.csr.extended) { + int minimal = IS_NEW_GAME_API ? + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 : + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS; + + if (p->protocol == PROTOCOL_VERSION_Q2PRO && p->version >= minimal) + return true; + return reject("This is a protocol limit removing enhanced server.\n" "Your client version is not compatible. Make sure you are " +<<<<<<< HEAD "running latest Q2PRO client version.\nYour major protocol version: %d\n " "Your minor protocol version: %d\n", p->protocol, p->version); +======= + "running the latest Q2PRO client version.\n"); +>>>>>>> 9b15c229 (Support more protocol extensions.) } return true; @@ -1032,7 +1047,7 @@ static void init_pmove_and_es_flags(client_t *newcl) int force; // copy default pmove parameters - newcl->pmp = sv_pmp; + newcl->pmp = svs.pmp; newcl->pmp.airaccelerate = sv_airaccelerate->integer; // common extensions @@ -1068,6 +1083,12 @@ static void init_pmove_and_es_flags(client_t *newcl) } if (svs.csr.extended) { newcl->esFlags |= MSG_ES_EXTENSIONS; + newcl->psFlags |= MSG_PS_EXTENSIONS; + + if (IS_NEW_GAME_API) { + newcl->esFlags |= MSG_ES_EXTENSIONS_2; + newcl->psFlags |= MSG_PS_EXTENSIONS_2; + } } force = 1; } @@ -1593,7 +1614,7 @@ static void SV_CalcPings(void) } // let the game dll know about the ping - cl->edict->client->ping = cl->ping; + SV_SetClient_Ping(cl, cl->ping); } } @@ -2441,9 +2462,6 @@ void SV_Init(void) sv.frametime = Com_ComputeFrametime(sv.framerate); #endif - // set up default pmove parameters - PmoveInit(&sv_pmp); - #if USE_SYSCON SV_SetConsoleTitle(); #endif diff --git a/src/server/mvd.c b/src/server/mvd.c index fdceb8bb0..42367d605 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -479,7 +479,8 @@ should do it for us by providing some SVF_* flag or something. */ static bool player_is_active(const edict_t *ent) { - int num; + int num, pm_type, pm_flags; + float fov; if ((g_features->integer & GMF_PROPERINUSE) && !ent->inuse) { return false; @@ -503,8 +504,20 @@ static bool player_is_active(const edict_t *ent) } } + if (IS_NEW_GAME_API) { + const gclient_new_t *cl = ent->client; + pm_type = cl->ps.pmove.pm_type; + pm_flags = cl->ps.pmove.pm_flags; + fov = cl->ps.fov; + } else { + const gclient_old_t *cl = ent->client; + pm_type = cl->ps.pmove.pm_type; + pm_flags = cl->ps.pmove.pm_flags; + fov = cl->ps.fov; + } + // first of all, make sure player_state_t is valid - if (!ent->client->ps.fov) { + if (!fov) { return false; } @@ -514,7 +527,7 @@ static bool player_is_active(const edict_t *ent) } // never capture spectators - if (ent->client->ps.pmove.pm_type == PM_SPECTATOR) { + if (pm_type == PM_SPECTATOR) { return false; } @@ -532,12 +545,12 @@ static bool player_is_active(const edict_t *ent) } // they are likely following someone in case of PM_FREEZE - if (ent->client->ps.pmove.pm_type == PM_FREEZE) { + if (pm_type == PM_FREEZE) { return false; } // they are likely following someone if PMF_NO_PREDICTION is set - if (ent->client->ps.pmove.pm_flags & PMF_NO_PREDICTION) { + if (pm_flags & PMF_NO_PREDICTION) { return false; } @@ -574,7 +587,10 @@ static void build_gamestate(void) continue; } - MSG_PackPlayer(&mvd.players[i], &ent->client->ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&mvd.players[i], ent->client); + else + MSG_PackPlayerOld(&mvd.players[i], ent->client); PPS_INUSE(&mvd.players[i]) = true; } @@ -602,7 +618,7 @@ static void emit_gamestate(void) player_packed_t *ps; entity_packed_t *es; size_t length; - int flags, extra, portalbytes; + int flags, portalbytes; byte portalbits[MAX_MAP_PORTAL_BYTES]; // don't bother writing if there are no active MVD clients @@ -610,22 +626,25 @@ static void emit_gamestate(void) return; } - // pack MVD stream flags into extra bits - extra = 0; - if (sv_mvd_nomsgs->integer && mvd.dummy) { - extra |= MVF_NOMSGS << SVCMD_BITS; - } - if (svs.csr.extended) { - extra |= MVF_EXTLIMITS << SVCMD_BITS; - } + // setup MVD stream flags + flags = 0; + if (sv_mvd_nomsgs->integer && mvd.dummy) + flags |= MVF_NOMSGS; // send the serverdata - MSG_WriteByte(mvd_serverdata | extra); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - if (svs.csr.extended) + if (svs.csr.extended) { + flags |= MVF_EXTLIMITS; + if (IS_NEW_GAME_API) + flags |= MVF_EXTLIMITS_2; + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_CURRENT); - else + MSG_WriteShort(flags); + } else { + MSG_WriteByte(mvd_serverdata | (flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); MSG_WriteShort(PROTOCOL_VERSION_MVD_DEFAULT); + } MSG_WriteLong(sv.spawncount); MSG_WriteString(fs_game->string); if (mvd.dummy) @@ -746,7 +765,10 @@ static void emit_frame(void) } // quantize - MSG_PackPlayer(&newps, &ent->client->ps); + if (IS_NEW_GAME_API) + MSG_PackPlayerNew(&newps, ent->client); + else + MSG_PackPlayerOld(&newps, ent->client); if (PPS_INUSE(oldps)) { // delta update from old position @@ -2136,12 +2158,19 @@ void SV_MvdPostInit(void) if (sv_mvd_noblend->integer) { mvd.psFlags |= MSG_PS_IGNORE_BLEND; } + if (sv_mvd_nogun->integer) { mvd.psFlags |= MSG_PS_IGNORE_GUNINDEX | MSG_PS_IGNORE_GUNFRAMES; } + if (svs.csr.extended) { mvd.esFlags |= MSG_ES_LONGSOLID | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; mvd.psFlags |= MSG_PS_EXTENSIONS; + + if (IS_NEW_GAME_API) { + mvd.esFlags |= MSG_ES_EXTENSIONS_2; + mvd.psFlags |= MSG_PS_EXTENSIONS_2; + } } } diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index a3ffdd686..e876bd2d5 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -1860,7 +1860,7 @@ static void emit_base_frame(mvd_t *mvd) // send base player states for (i = 0; i < mvd->maxclients; i++) { player = &mvd->players[i]; - MSG_PackPlayer(&ps, &player->ps); + MSG_PackPlayerNew(&ps, &player->ps); MSG_WriteDeltaPlayerstate_Packet(NULL, &ps, i, player_flags(mvd, player)); } MSG_WriteByte(CLIENTNUM_NONE); @@ -1879,17 +1879,21 @@ static void emit_base_frame(mvd_t *mvd) static void emit_gamestate(mvd_t *mvd) { - int i, extra; + int i; char *s; size_t len; - // pack MVD stream flags into extra bits - extra = mvd->flags << SVCMD_BITS; - // send the serverdata - MSG_WriteByte(mvd_serverdata | extra); - MSG_WriteLong(PROTOCOL_VERSION_MVD); - MSG_WriteLong(mvd->version); + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2) { + MSG_WriteByte(mvd_serverdata); + MSG_WriteLong(PROTOCOL_VERSION_MVD); + MSG_WriteLong(mvd->version); + MSG_WriteShort(mvd->flags); + } else { + MSG_WriteByte(mvd_serverdata | (mvd->flags << SVCMD_BITS)); + MSG_WriteLong(PROTOCOL_VERSION_MVD); + MSG_WriteLong(mvd->version); + } MSG_WriteLong(mvd->servercount); MSG_WriteString(mvd->gamedir); MSG_WriteShort(mvd->clientNum); diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 881cb8e81..2014c8214 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -682,6 +682,8 @@ static void MVD_UpdateClient(mvd_client_t *client) Vector4Set(client->ps.blend, 0.5f, 0.3f, 0.2f, 0.4f); else Vector4Clear(client->ps.blend); + + Vector4Clear(client->ps.damage_blend); } else { // copy entire player state client->ps = target->ps; @@ -696,7 +698,7 @@ static void MVD_UpdateClient(mvd_client_t *client) if (target != mvd->dummy) { if (mvd_stats_hack->integer && mvd->dummy) { // copy stats of the dummy MVD observer - for (i = 0; i < MAX_STATS; i++) { + for (i = 0; i < MAX_STATS_OLD; i++) { if (mvd_stats_hack->integer & BIT(i)) { client->ps.stats[i] = mvd->dummy->ps.stats[i]; } @@ -773,12 +775,16 @@ void MVD_BroadcastPrintf(mvd_t *mvd, int level, int mask, const char *fmt, ...) SZ_Clear(&msg_write); } +#define ES_MASK (MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS | MSG_ES_EXTENSIONS_2) +#define PS_MASK (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) + static void MVD_SetServerState(client_t *cl, mvd_t *mvd) { if (cl->csr != mvd->csr) { Z_Freep(&cl->entities); cl->num_entities = 0; } + cl->gamedir = mvd->gamedir; cl->mapname = mvd->mapname; cl->configstrings = mvd->configstrings; @@ -788,10 +794,11 @@ static void MVD_SetServerState(client_t *cl, mvd_t *mvd) cl->ge = &mvd->ge; cl->spawncount = mvd->servercount; cl->maxclients = mvd->maxclients; - if (cl->csr->extended) - cl->esFlags |= MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS; - else - cl->esFlags &= ~(MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS); + + cl->esFlags &= ~ES_MASK; + cl->psFlags &= ~PS_MASK; + cl->esFlags |= mvd->esFlags & ES_MASK; + cl->psFlags |= mvd->psFlags & PS_MASK; } void MVD_SwitchChannel(mvd_client_t *client, mvd_t *mvd) @@ -846,6 +853,21 @@ static bool MVD_PartFilter(mvd_client_t *client) return delta < treshold; } +static bool MVD_ClientCompatible(client_t *cl, mvd_t *mvd) +{ + int minimal; + + if (!(mvd->flags & (MVF_EXTLIMITS | MVF_EXTLIMITS_2))) + return true; + if (cl->protocol != PROTOCOL_VERSION_Q2PRO) + return false; + + minimal = (mvd->flags & MVF_EXTLIMITS_2) ? + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 : + PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS; + return cl->version >= minimal; +} + static void MVD_TrySwitchChannel(mvd_client_t *client, mvd_t *mvd) { if (mvd == client->mvd) { @@ -854,7 +876,7 @@ static void MVD_TrySwitchChannel(mvd_client_t *client, mvd_t *mvd) "in the Waiting Room" : "on this channel"); return; // nothing to do } - if (!CLIENT_COMPATIBLE(mvd->csr, client->cl)) { + if (!MVD_ClientCompatible(client->cl, mvd)) { SV_ClientPrintf(client->cl, PRINT_HIGH, "[MVD] This channel is not compatible with your client version.\n"); return; @@ -1771,7 +1793,7 @@ static void MVD_GameInit(void) for (i = 0; i < sv_maxclients->integer; i++) { mvd_clients[i].cl = &svs.client_pool[i]; - edicts[i + 1].client = (gclient_t *)&mvd_clients[i]; + edicts[i + 1].client = &mvd_clients[i]; } mvd_ge.edicts = edicts; @@ -1865,7 +1887,7 @@ static qboolean MVD_GameClientConnect(edict_t *ent, char *userinfo) if (LIST_SINGLE(&mvd_channel_list)) { mvd = LIST_FIRST(mvd_t, &mvd_channel_list, entry); } - if (!mvd || !CLIENT_COMPATIBLE(mvd->csr, client->cl)) { + if (!mvd || !MVD_ClientCompatible(client->cl, mvd)) { mvd = &mvd_waitingRoom; } List_SeqAdd(&mvd->clients, &client->entry); diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index a7494c48b..6bd7c4861 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -488,7 +488,7 @@ static void MVD_ParseSound(mvd_t *mvd, int extrabits) MSG_WriteByte(offset); MSG_WriteShort(sendchan); - MSG_WritePos(origin); + MSG_WritePos(origin, mvd->esFlags & MSG_ES_EXTENSIONS_2); leaf1 = NULL; if (!(extrabits & 1)) { @@ -894,12 +894,18 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) "Current version is %d.\n", mvd->version, PROTOCOL_VERSION_MVD_CURRENT); } + // parse MVD stream flags + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2) { + mvd->flags = MSG_ReadWord(); + } else { + mvd->flags = extrabits; + } + mvd->servercount = MSG_ReadLong(); if (MSG_ReadString(mvd->gamedir, sizeof(mvd->gamedir)) >= sizeof(mvd->gamedir)) { MVD_Destroyf(mvd, "Oversize gamedir string"); } mvd->clientNum = MSG_ReadShort(); - mvd->flags = extrabits; mvd->esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN; mvd->psFlags = 0; mvd->csr = &cs_remap_old; @@ -909,6 +915,13 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) mvd->psFlags |= MSG_PS_EXTENSIONS; mvd->csr = &cs_remap_new; } + if (mvd->version >= PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2 && mvd->flags & MVF_EXTLIMITS_2) { + mvd->esFlags |= MSG_ES_EXTENSIONS_2; + mvd->psFlags |= MSG_PS_EXTENSIONS_2; + if (!(mvd->flags & MVF_EXTLIMITS)) { + MVD_Destroyf(mvd, "MVF_EXTLIMITS_2 without MVF_EXTLIMITS"); + } + } #if 0 // change gamedir unless playing a demo diff --git a/src/server/save.c b/src/server/save.c index d87c6baba..d7757d063 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -662,7 +662,7 @@ static void SV_Savegame_f(void) if (!gex->CanSave()) return; } else { - if (sv_maxclients->integer == 1 && svs.client_pool[0].edict->client->ps.stats[STAT_HEALTH] <= 0) { + if (sv_maxclients->integer == 1 && SV_GetClient_Stat(&svs.client_pool[0], STAT_HEALTH) <= 0) { Com_Printf("Can't savegame while dead!\n"); return; } diff --git a/src/server/send.c b/src/server/send.c index 64e50441e..191289047 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -556,11 +556,8 @@ static void emit_snd(const client_t *client, const message_packet_t *msg) MSG_WriteShort(msg->sendchan); - if (flags & SND_POS) { - for (int i = 0; i < 3; i++) { - MSG_WriteShort(msg->pos[i]); - } - } + if (flags & SND_POS) + MSG_WriteIntPos(msg->pos, client->esFlags & MSG_ES_EXTENSIONS_2); } static inline void write_snd(client_t *client, message_packet_t *msg, unsigned maxsize) diff --git a/src/server/server.h b/src/server/server.h index 463968bae..a73f68303 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -93,6 +93,10 @@ with this program; if not, write to the Free Software Foundation, Inc., GMF_IPV6_ADDRESS_AWARE | GMF_ALLOW_INDEX_OVERFLOW | \ GMF_PROTOCOL_EXTENSIONS) +// flag indicating if game uses gclient_new_t or gclient_old_t. +// doesn't enable protocol extensions by itself. +#define IS_NEW_GAME_API (ge->apiversion == GAME_API_VERSION_NEW) + // ugly hack for SV_Shutdown #define MVD_SPAWN_DISABLED 0 #define MVD_SPAWN_ENABLED BIT(30) @@ -176,6 +180,7 @@ typedef struct { #define MAX_TOTAL_ENT_LEAFS 128 +<<<<<<< HEAD // hack for smooth BSP model rotation #define Q2PRO_SHORTANGLES(c, e) \ ((((c)->protocol == PROTOCOL_VERSION_Q2PRO && \ @@ -187,6 +192,8 @@ typedef struct { (!(csr)->extended || (((c)->protocol == PROTOCOL_VERSION_Q2PRO && (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) \ || ((c)->protocol == PROTOCOL_VERSION_AQTION && (c)->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS))) +======= +>>>>>>> 9b15c229 (Support more protocol extensions.) #define ENT_EXTENSION(csr, ent) ((csr)->extended ? &(ent)->x : NULL) typedef enum { @@ -240,7 +247,7 @@ typedef struct { uint8_t volume; uint8_t attenuation; uint8_t timeofs; - int16_t pos[3]; // saved in case entity is freed + int32_t pos[3]; // saved in case entity is freed }; }; } message_packet_t; @@ -343,6 +350,7 @@ typedef struct client_s { pmoveParams_t pmp; // spectator speed, etc msgEsFlags_t esFlags; // entity protocol flags + msgPsFlags_t psFlags; // packetized messages list_t msg_free_list; @@ -493,6 +501,7 @@ typedef struct { #endif cs_remap_t csr; + pmoveParams_t pmp; unsigned last_heartbeat; unsigned last_timescale_check; @@ -529,8 +538,6 @@ extern list_t sv_clientlist; // linked list of non-free clients extern server_static_t svs; // persistant server info extern server_t sv; // local server -extern pmoveParams_t sv_pmp; - extern cvar_t *sv_hostname; extern cvar_t *sv_maxclients; extern cvar_t *sv_password; @@ -797,7 +804,7 @@ extern const game_export_ex_t *gex; void SV_InitGameProgs(void); void SV_ShutdownGameProgs(void); -void PF_Pmove(pmove_t *pm); +void PF_Pmove(void *pm); #if AQTION_EXTENSION void G_InitializeExtensions(void); @@ -820,6 +827,7 @@ void SV_RegisterSavegames(void); #define SV_RegisterSavegames() (void)0 #endif +<<<<<<< HEAD #if AQTION_EXTENSION // // sv_ghud.c @@ -841,6 +849,46 @@ void SV_Ghud_SetSize(edict_t *ent, int i, int x, int y); extern int(*GE_customizeentityforclient)(edict_t *client, edict_t *ent, entity_state_t *state); // 0 don't send, 1 send normally extern void(*GE_CvarSync_Updated)(int index, edict_t *clent); #endif +======= +// +// ugly gclient_(old|new)_t accessors +// + +static inline void SV_GetClient_ViewOrg(const client_t *client, vec3_t org) +{ + if (IS_NEW_GAME_API) { + const gclient_new_t *cl = client->edict->client; + VectorMA(cl->ps.viewoffset, 0.125f, cl->ps.pmove.origin, org); + } else { + const gclient_old_t *cl = client->edict->client; + VectorMA(cl->ps.viewoffset, 0.125f, cl->ps.pmove.origin, org); + } +} + +static inline int SV_GetClient_ClientNum(const client_t *client) +{ + if (IS_NEW_GAME_API) + return ((const gclient_new_t *)client->edict->client)->clientNum; + else + return ((const gclient_old_t *)client->edict->client)->clientNum; +} + +static inline int SV_GetClient_Stat(const client_t *client, int stat) +{ + if (IS_NEW_GAME_API) + return ((const gclient_new_t *)client->edict->client)->ps.stats[stat]; + else + return ((const gclient_old_t *)client->edict->client)->ps.stats[stat]; +} + +static inline void SV_SetClient_Ping(const client_t *client, int ping) +{ + if (IS_NEW_GAME_API) + ((gclient_new_t *)client->edict->client)->ping = ping; + else + ((gclient_old_t *)client->edict->client)->ping = ping; +} +>>>>>>> 9b15c229 (Support more protocol extensions.) //============================================================ diff --git a/src/server/user.c b/src/server/user.c index 51c9c4e44..87568e143 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -345,6 +345,9 @@ static int q2pro_protocol_flags(void) if (sv_client->csr->extended) flags |= Q2PRO_PF_EXTENSIONS; + if (sv_client->esFlags & MSG_ES_EXTENSIONS_2) + flags |= Q2PRO_PF_EXTENSIONS_2; + return flags; } From 1643c09d89e3dbbad85c823d9da50dc17c45e872 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 8 Sep 2024 13:05:44 +0300 Subject: [PATCH 715/974] Add fireball trail effect. --- src/client/client.h | 11 ++++++++- src/client/effects.c | 52 +++++++++++++++++-------------------------- src/client/entities.c | 10 +++++---- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 46e83b6d7..cb58317f3 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -858,9 +858,18 @@ typedef struct { float die; // stop lighting after this time } cdlight_t; +typedef enum { + DT_GIB, + DT_GREENGIB, + DT_SMOKE, + DT_FIREBALL, + + DT_COUNT +} diminishing_trail_t; + void CL_BigTeleportParticles(const vec3_t org); void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old); -void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, int flags); +void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, diminishing_trail_t type); void CL_FlyEffect(centity_t *ent, const vec3_t origin); void CL_BfgParticles(const entity_t *ent); void CL_ItemRespawnParticles(const vec3_t org); diff --git a/src/client/effects.c b/src/client/effects.c index 63bc8fd9c..c850019ba 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -1287,8 +1287,9 @@ CL_DiminishingTrail =============== */ -void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, int flags) +void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, diminishing_trail_t type) { + static const byte colors[DT_COUNT] = { 0xe8, 0xdb, 0x04, 0xd8 }; vec3_t move; vec3_t vec; float len; @@ -1324,40 +1325,27 @@ void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, i p = CL_AllocParticle(); if (!p) return; - VectorClear(p->accel); + VectorClear(p->accel); p->time = cl.time; - if (flags & EF_GIB) { - p->alpha = 1.0f; - p->alphavel = -1.0f / (1 + frand() * 0.4f); - p->color = 0xe8 + (Q_rand() & 7); - for (j = 0; j < 3; j++) { - p->org[j] = move[j] + crand() * orgscale; - p->vel[j] = crand() * velscale; - p->accel[j] = 0; - } - p->vel[2] -= PARTICLE_GRAVITY; - } else if (flags & EF_GREENGIB) { - p->alpha = 1.0f; - p->alphavel = -1.0f / (1 + frand() * 0.4f); - p->color = 0xdb + (Q_rand() & 7); - for (j = 0; j < 3; j++) { - p->org[j] = move[j] + crand() * orgscale; - p->vel[j] = crand() * velscale; - p->accel[j] = 0; - } - p->vel[2] -= PARTICLE_GRAVITY; - } else { - p->alpha = 1.0f; - p->alphavel = -1.0f / (1 + frand() * 0.2f); - p->color = 4 + (Q_rand() & 7); - for (j = 0; j < 3; j++) { - p->org[j] = move[j] + crand() * orgscale; - p->vel[j] = crand() * velscale; - } - p->accel[2] = 20; + p->alpha = 1.0f; + p->alphavel = -1.0f / (1 + frand() * (type == DT_SMOKE ? 0.2f : 0.4f)); + + for (j = 0; j < 3; j++) { + p->org[j] = move[j] + crand() * orgscale; + p->vel[j] = crand() * velscale; } + + if (type >= DT_SMOKE) + p->accel[2] = 20; + else + p->vel[2] -= PARTICLE_GRAVITY; + + if (type == DT_FIREBALL) + p->color = colors[type] + (1024 - old->trailcount) / 64; + else + p->color = colors[type] + (Q_rand() & 7); } old->trailcount -= 5; @@ -1383,7 +1371,7 @@ void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old) float dec; // smoke - CL_DiminishingTrail(start, end, old, EF_ROCKET); + CL_DiminishingTrail(start, end, old, DT_SMOKE); // fire VectorCopy(start, move); diff --git a/src/client/entities.c b/src/client/entities.c index e26eea67a..1291bd4d7 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -939,7 +939,9 @@ static void CL_AddPacketEntities(void) goto skip; if (effects & EF_ROCKET) { - if (!(cl_disable_particles->integer & NOPART_ROCKET_TRAIL)) + if (cl.csr.extended && effects & EF_GIB) + CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_FIREBALL); + else if (!(cl_disable_particles->integer & NOPART_ROCKET_TRAIL)) CL_RocketTrail(cent->lerp_origin, ent.origin, cent); if (cl_dlight_hacks->integer & DLHACK_ROCKET_COLOR) V_AddLight(ent.origin, 200, 1, 0.23f, 0); @@ -959,10 +961,10 @@ static void CL_AddPacketEntities(void) else V_AddLight(ent.origin, 200, 1, 1, 0); } else if (effects & EF_GIB) { - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, effects); + CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_GIB); } else if (effects & EF_GRENADE) { if (!(cl_disable_particles->integer & NOPART_GRENADE_TRAIL)) - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, effects); + CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_SMOKE); } else if (effects & EF_FLIES) { CL_FlyEffect(cent, ent.origin); } else if (effects & EF_BFG) { @@ -1000,7 +1002,7 @@ static void CL_AddPacketEntities(void) CL_TrackerTrail(cent->lerp_origin, ent.origin, 0); V_AddLight(ent.origin, 200, -1, -1, -1); } else if (effects & EF_GREENGIB) { - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, effects); + CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_GREENGIB); } else if (effects & EF_IONRIPPER) { CL_IonripperTrail(cent->lerp_origin, ent.origin); V_AddLight(ent.origin, 100, 1, 0.5f, 0.5f); From 36568722c5d20a37e1375aeb37e8083f84dd2390 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 9 Sep 2024 14:24:15 -0400 Subject: [PATCH 716/974] Added protocol 38 --- inc/common/protocol.h | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 6554b2b3c..14a5ae2fc 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -24,23 +24,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define MAX_MSGLEN 0x8000 // max length of a message, 32 KiB -<<<<<<< HEAD -#define PROTOCOL_VERSION_OLD 26 -#define PROTOCOL_VERSION_DEFAULT 34 -#define PROTOCOL_VERSION_R1Q2 35 -#define PROTOCOL_VERSION_Q2PRO 36 -#define PROTOCOL_VERSION_MVD 37 // not used for UDP connections -#define PROTOCOL_VERSION_AQTION 38 -#define PROTOCOL_VERSION_EXTENDED 3434 -======= #define PROTOCOL_VERSION_OLD 26 #define PROTOCOL_VERSION_DEFAULT 34 #define PROTOCOL_VERSION_R1Q2 35 #define PROTOCOL_VERSION_Q2PRO 36 #define PROTOCOL_VERSION_MVD 37 // not used for UDP connections +#define PROTOCOL_VERSION_AQTION 38 #define PROTOCOL_VERSION_EXTENDED_OLD 3434 #define PROTOCOL_VERSION_EXTENDED 3435 ->>>>>>> 9b15c229 (Support more protocol extensions.) #define PROTOCOL_VERSION_R1Q2_MINIMUM 1903 // b6377 #define PROTOCOL_VERSION_R1Q2_UCMD 1904 // b7387 From 5582326fda2cca14b716a36e5ead4b717ceedbd6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 8 Sep 2024 13:44:22 +0300 Subject: [PATCH 717/974] Clean up client effects code. --- src/client/client.h | 10 ++-- src/client/effects.c | 51 ++++++++------------ src/client/entities.c | 2 +- src/client/newfx.c | 105 ++++++++++++++---------------------------- 4 files changed, 58 insertions(+), 110 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index cb58317f3..6017a0504 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -832,15 +832,13 @@ void CL_Trace(trace_t *tr, const vec3_t start, const vec3_t end, const vec3_t mi // // effects.c // -#define PARTICLE_GRAVITY 40 -#define BLASTER_PARTICLE_COLOR 0xe0 +#define PARTICLE_GRAVITY 40 #define INSTANT_PARTICLE -10000.0f typedef struct cparticle_s { struct cparticle_s *next; - float time; - + int time; vec3_t org; vec3_t vel; vec3_t accel; @@ -855,7 +853,7 @@ typedef struct { vec3_t color; vec3_t origin; float radius; - float die; // stop lighting after this time + int die; // stop lighting after this time } cdlight_t; typedef enum { @@ -907,7 +905,7 @@ void CL_ForceWall(const vec3_t start, const vec3_t end, int color); void CL_BubbleTrail2(const vec3_t start, const vec3_t end, int dist); void CL_Heatbeam(const vec3_t start, const vec3_t end); void CL_ParticleSteamEffect(const vec3_t org, const vec3_t dir, int color, int count, int magnitude); -void CL_TrackerTrail(const vec3_t start, const vec3_t end, int particleColor); +void CL_TrackerTrail(const vec3_t start, const vec3_t end); void CL_TagTrail(const vec3_t start, const vec3_t end, int color); void CL_ColorFlash(const vec3_t pos, int ent, int intensity, float r, float g, float b); void CL_Tracker_Shell(const centity_t *cent, const vec3_t origin); diff --git a/src/client/effects.c b/src/client/effects.c index c850019ba..85e81d9f5 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -20,7 +20,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "client.h" #include "shared/m_flash.h" -static void CL_LogoutEffect(const vec3_t org, int type); +static void CL_LogoutEffect(const vec3_t org, int color); static vec3_t avelocities[NUMVERTEXNORMALS]; @@ -270,17 +270,17 @@ void CL_MuzzleFlash(void) case MZ_LOGIN: VectorSet(dl->color, 0, 1, 0); S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound("weapons/grenlf1a.wav"), 1, ATTN_NORM, 0); - CL_LogoutEffect(pl->current.origin, mz.weapon); + CL_LogoutEffect(pl->current.origin, 0xd0); // green break; case MZ_LOGOUT: VectorSet(dl->color, 1, 0, 0); S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound("weapons/grenlf1a.wav"), 1, ATTN_NORM, 0); - CL_LogoutEffect(pl->current.origin, mz.weapon); + CL_LogoutEffect(pl->current.origin, 0x40); // red break; case MZ_RESPAWN: VectorSet(dl->color, 1, 1, 0); S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound("weapons/grenlf1a.wav"), 1, ATTN_NORM, 0); - CL_LogoutEffect(pl->current.origin, mz.weapon); + CL_LogoutEffect(pl->current.origin, 0xe0); // yellow break; case MZ_PHALANX: VectorSet(dl->color, 1, 0.5f, 0.5f); @@ -1011,7 +1011,7 @@ CL_LogoutEffect =============== */ -static void CL_LogoutEffect(const vec3_t org, int type) +static void CL_LogoutEffect(const vec3_t org, int color) { int i, j; cparticle_t *p; @@ -1023,12 +1023,7 @@ static void CL_LogoutEffect(const vec3_t org, int type) p->time = cl.time; - if (type == MZ_LOGIN) - p->color = 0xd0 + (Q_rand() & 7); // green - else if (type == MZ_LOGOUT) - p->color = 0x40 + (Q_rand() & 7); // red - else - p->color = 0xe0 + (Q_rand() & 7); // yellow + p->color = color + (Q_rand() & 7); p->org[0] = org[0] - 16 + frand() * 32; p->org[1] = org[1] - 16 + frand() * 32; @@ -1229,7 +1224,6 @@ void CL_BlasterTrail(const vec3_t start, const vec3_t end) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand(); p->vel[j] = crand() * 5; - p->accel[j] = 0; } VectorAdd(move, vec, move); @@ -1274,7 +1268,6 @@ void CL_FlagTrail(const vec3_t start, const vec3_t end, int color) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand() * 16; p->vel[j] = crand() * 5; - p->accel[j] = 0; } VectorAdd(move, vec, move); @@ -1423,7 +1416,6 @@ void CL_OldRailTrail(void) int i; float d, c, s; vec3_t dir; - byte clr = 0x74; VectorCopy(te.pos1, move); VectorSubtract(te.pos2, te.pos1, vec); @@ -1448,7 +1440,7 @@ void CL_OldRailTrail(void) p->alpha = 1.0f; p->alphavel = -1.0f / (1 + frand() * 0.2f); - p->color = clr + (Q_rand() & 7); + p->color = 0x74 + (Q_rand() & 7); for (j = 0; j < 3; j++) { p->org[j] = move[j] + dir[j] * 3; p->vel[j] = dir[j] * 6; @@ -1478,7 +1470,6 @@ void CL_OldRailTrail(void) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand() * 3; p->vel[j] = crand() * 3; - p->accel[j] = 0; } VectorAdd(move, vec, move); @@ -1553,6 +1544,10 @@ static void CL_FlyParticles(const vec3_t origin, int count) ltime = cl.time * 0.001f; for (i = 0; i < count; i += 2) { + p = CL_AllocParticle(); + if (!p) + return; + angle = ltime * avelocities[i][0]; sy = sinf(angle); cy = cosf(angle); @@ -1564,10 +1559,6 @@ static void CL_FlyParticles(const vec3_t origin, int count) forward[1] = cp * sy; forward[2] = -sp; - p = CL_AllocParticle(); - if (!p) - return; - p->time = cl.time; dist = sinf(ltime + i) * 64; @@ -1630,6 +1621,10 @@ void CL_BfgParticles(const entity_t *ent) ltime = cl.time * 0.001f; for (i = 0; i < NUMVERTEXNORMALS; i++) { + p = CL_AllocParticle(); + if (!p) + return; + angle = ltime * avelocities[i][0]; sy = sinf(angle); cy = cosf(angle); @@ -1641,10 +1636,6 @@ void CL_BfgParticles(const entity_t *ent) forward[1] = cp * sy; forward[2] = -sp; - p = CL_AllocParticle(); - if (!p) - return; - p->time = cl.time; dist = sinf(ltime + i) * 64; @@ -1752,8 +1743,7 @@ void CL_AddParticles(void) { cparticle_t *p, *next; float alpha; - float time = 0, time2; - int color; + float time, time2; cparticle_t *active, *tail; particle_t *part; @@ -1773,6 +1763,7 @@ void CL_AddParticles(void) continue; } } else { + time = 0.0f; alpha = p->alpha; } @@ -1788,10 +1779,6 @@ void CL_AddParticles(void) tail = p; } - if (alpha > 1.0f) - alpha = 1; - color = p->color; - time2 = time * time; part->origin[0] = p->org[0] + p->vel[0] * time + p->accel[0] * time2; @@ -1799,8 +1786,8 @@ void CL_AddParticles(void) part->origin[2] = p->org[2] + p->vel[2] * time + p->accel[2] * time2; part->rgba = p->rgba; - part->color = color; - part->alpha = alpha; + part->color = p->color; + part->alpha = min(alpha, 1.0f); if (p->alphavel == INSTANT_PARTICLE) { p->alphavel = 0.0f; diff --git a/src/client/entities.c b/src/client/entities.c index 1291bd4d7..48cdffe5b 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -999,7 +999,7 @@ static void CL_AddPacketEntities(void) V_AddLight(ent.origin, 155, -1.0f, -1.0f, -1.0f); } } else if (effects & EF_TRACKER) { - CL_TrackerTrail(cent->lerp_origin, ent.origin, 0); + CL_TrackerTrail(cent->lerp_origin, ent.origin); V_AddLight(ent.origin, 200, -1, -1, -1); } else if (effects & EF_GREENGIB) { CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_GREENGIB); diff --git a/src/client/newfx.c b/src/client/newfx.c index e0f0bbe0c..287306957 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -114,10 +114,8 @@ void CL_ForceWall(const vec3_t start, const vec3_t end, int color) p->alpha = 1.0f; p->alphavel = -1.0f / (3.0f + frand() * 0.5f); p->color = color; - for (j = 0; j < 3; j++) { + for (j = 0; j < 3; j++) p->org[j] = move[j] + crand() * 3; - p->accel[j] = 0; - } p->vel[0] = 0; p->vel[1] = 0; p->vel[2] = -40 - (crand() * 10); @@ -264,9 +262,9 @@ void CL_ParticleSteamEffect(const vec3_t org, const vec3_t dir, int color, int c p->time = cl.time; p->color = color + (Q_rand() & 7); - for (j = 0; j < 3; j++) { + for (j = 0; j < 3; j++) p->org[j] = org[j] + magnitude * 0.1f * crand(); - } + VectorScale(dir, magnitude, p->vel); d = crand() * magnitude / 3; VectorMA(p->vel, d, r, p->vel); @@ -292,13 +290,12 @@ void CL_ParticleSteamEffect2(cl_sustain_t *self) CL_TrackerTrail =============== */ -void CL_TrackerTrail(const vec3_t start, const vec3_t end, int particleColor) +void CL_TrackerTrail(const vec3_t start, const vec3_t end) { vec3_t move; vec3_t vec; vec3_t forward, right, up, angle_dir; float len; - int j; cparticle_t *p; int dec; float dist; @@ -327,19 +324,23 @@ void CL_TrackerTrail(const vec3_t start, const vec3_t end, int particleColor) p->alpha = 1.0f; p->alphavel = -2.0f; - p->color = particleColor; - dist = DotProduct(move, forward); - VectorMA(move, 8 * cosf(dist), up, p->org); - for (j = 0; j < 3; j++) { - p->vel[j] = 0; - p->accel[j] = 0; - } - p->vel[2] = 5; + p->color = 0; + dist = 8 * cosf(DotProduct(move, forward)); + VectorMA(move, dist, up, p->org); + VectorSet(p->vel, 0, 0, 5); VectorAdd(move, vec, move); } } +static void RandomDir(vec3_t dir) +{ + dir[0] = crand(); + dir[1] = crand(); + dir[2] = crand(); + VectorNormalize(dir); +} + void CL_Tracker_Shell(const centity_t *ent, const vec3_t origin) { vec3_t org, dir, mid; @@ -368,11 +369,7 @@ void CL_Tracker_Shell(const centity_t *ent, const vec3_t origin) p->alphavel = INSTANT_PARTICLE; p->color = 0; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); - + RandomDir(dir); VectorMA(org, radius, dir, p->org); } } @@ -395,11 +392,7 @@ void CL_MonsterPlasma_Shell(const vec3_t origin) p->alphavel = INSTANT_PARTICLE; p->color = 0xe0; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); - + RandomDir(dir); VectorMA(origin, 10, dir, p->org); } } @@ -412,7 +405,7 @@ void CL_Widowbeamout(cl_sustain_t *self) cparticle_t *p; float ratio; - ratio = 1.0f - (((float)self->endtime - (float)cl.time) / 2100.0f); + ratio = 1.0f - (self->endtime - cl.time) / 2100.0f; for (i = 0; i < 300; i++) { p = CL_AllocParticle(); @@ -426,11 +419,7 @@ void CL_Widowbeamout(cl_sustain_t *self) p->alphavel = INSTANT_PARTICLE; p->color = colortable[Q_rand() & 3]; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); - + RandomDir(dir); VectorMA(self->org, (45.0f * ratio), dir, p->org); } } @@ -443,7 +432,7 @@ void CL_Nukeblast(cl_sustain_t *self) cparticle_t *p; float ratio; - ratio = 1.0f - (((float)self->endtime - (float)cl.time) / 1000.0f); + ratio = 1.0f - (self->endtime - cl.time) / 1000.0f; for (i = 0; i < 700; i++) { p = CL_AllocParticle(); @@ -457,11 +446,7 @@ void CL_Nukeblast(cl_sustain_t *self) p->alphavel = INSTANT_PARTICLE; p->color = colortable[Q_rand() & 3]; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); - + RandomDir(dir); VectorMA(self->org, (200.0f * ratio), dir, p->org); } } @@ -481,14 +466,11 @@ void CL_WidowSplash(void) p->time = cl.time; p->color = colortable[Q_rand() & 3]; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); + RandomDir(dir); VectorMA(te.pos1, 45.0f, dir, p->org); VectorScale(dir, 40.0f, p->vel); - p->accel[0] = p->accel[1] = 0; + VectorClear(p->accel); p->alpha = 1.0f; p->alphavel = -0.8f / (0.5f + frand() * 0.3f); @@ -533,7 +515,6 @@ void CL_TagTrail(const vec3_t start, const vec3_t end, int color) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand() * 16; p->vel[j] = crand() * 5; - p->accel[j] = 0; } VectorAdd(move, vec, move); @@ -593,16 +574,16 @@ void CL_ParticleSmokeEffect(const vec3_t org, const vec3_t dir, int color, int c p->time = cl.time; p->color = color + (Q_rand() & 7); - for (j = 0; j < 3; j++) { + for (j = 0; j < 3; j++) p->org[j] = org[j] + magnitude * 0.1f * crand(); - } + VectorScale(dir, magnitude, p->vel); d = crand() * magnitude / 3; VectorMA(p->vel, d, r, p->vel); d = crand() * magnitude / 3; VectorMA(p->vel, d, u, p->vel); - p->accel[0] = p->accel[1] = p->accel[2] = 0; + VectorClear(p->accel); p->alpha = 1.0f; p->alphavel = -1.0f / (0.5f + frand() * 0.3f); @@ -686,7 +667,6 @@ void CL_BlasterTrail2(const vec3_t start, const vec3_t end) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand(); p->vel[j] = crand() * 5; - p->accel[j] = 0; } VectorAdd(move, vec, move); @@ -698,18 +678,17 @@ void CL_BlasterTrail2(const vec3_t start, const vec3_t end) CL_IonripperTrail =============== */ -void CL_IonripperTrail(const vec3_t start, const vec3_t ent) +void CL_IonripperTrail(const vec3_t start, const vec3_t end) { vec3_t move; vec3_t vec; float len; - int j; cparticle_t *p; int dec; int left = 0; VectorCopy(start, move); - VectorSubtract(ent, start, vec); + VectorSubtract(end, start, vec); len = VectorNormalize(vec); dec = 5; @@ -728,21 +707,13 @@ void CL_IonripperTrail(const vec3_t start, const vec3_t ent) p->alphavel = -1.0f / (0.3f + frand() * 0.2f); p->color = 0xe4 + (Q_rand() & 3); - for (j = 0; j < 3; j++) { - p->org[j] = move[j]; - p->accel[j] = 0; - } - if (left) { - left = 0; - p->vel[0] = 10; - } else { - left = 1; - p->vel[0] = -10; - } + VectorCopy(move, p->org); + p->vel[0] = left ? 10 : -10; p->vel[1] = 0; p->vel[2] = 0; + left ^= 1; VectorAdd(move, vec, move); } } @@ -795,7 +766,6 @@ void CL_TrapParticles(centity_t *ent, const vec3_t origin) for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand(); p->vel[j] = crand() * 15; - p->accel[j] = 0; } p->accel[2] = PARTICLE_GRAVITY; @@ -947,10 +917,7 @@ void CL_PowerSplash(void) p->time = cl.time; p->color = colortable[Q_rand() & 3]; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); + RandomDir(dir); VectorMA(org, ent->radius, dir, p->org); VectorScale(dir, 40.0f, p->vel); @@ -980,11 +947,7 @@ void CL_TeleporterParticles2(const vec3_t org) p->time = cl.time; p->color = 0xdb; - dir[0] = crand(); - dir[1] = crand(); - dir[2] = crand(); - VectorNormalize(dir); - + RandomDir(dir); VectorMA(org, 30.0f, dir, p->org); p->org[2] += 20.0f; VectorScale(dir, -25.0f, p->vel); From 644a0dd6a49f21db6d72f51f6546e49a601f7e49 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 8 Sep 2024 23:54:01 +0300 Subject: [PATCH 718/974] Make particle trails FPS independent. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If there is a particle trail, step lerp_origin discretely by ‘dec’ amount, and never make partial steps. Fixes old Q2 bug when running the client at higher FPS causes more particles to be spawned than intended. Also fix EF_TRACKER being broken at 60 FPS (another old Q2 bug). --- src/client/client.h | 16 +++--- src/client/effects.c | 121 ++++++++++++++++++++-------------------- src/client/entities.c | 56 ++++++++++++------- src/client/newfx.c | 126 ++++++++++++++++++++++-------------------- 4 files changed, 171 insertions(+), 148 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 6017a0504..1a2fb509c 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -866,8 +866,8 @@ typedef enum { } diminishing_trail_t; void CL_BigTeleportParticles(const vec3_t org); -void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old); -void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, diminishing_trail_t type); +void CL_RocketTrail(centity_t *ent, const vec3_t end); +void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t type); void CL_FlyEffect(centity_t *ent, const vec3_t origin); void CL_BfgParticles(const entity_t *ent); void CL_ItemRespawnParticles(const vec3_t org); @@ -876,10 +876,10 @@ void CL_ClearEffects(void); void CL_BlasterParticles(const vec3_t org, const vec3_t dir); void CL_ExplosionParticles(const vec3_t org); void CL_BFGExplosionParticles(const vec3_t org); -void CL_BlasterTrail(const vec3_t start, const vec3_t end); +void CL_BlasterTrail(centity_t *ent, const vec3_t end); void CL_OldRailTrail(void); void CL_BubbleTrail(const vec3_t start, const vec3_t end); -void CL_FlagTrail(const vec3_t start, const vec3_t end, int color); +void CL_FlagTrail(centity_t *ent, const vec3_t end, int color); void CL_MuzzleFlash(void); void CL_MuzzleFlash2(void); void CL_TeleporterParticles(const vec3_t org); @@ -898,15 +898,15 @@ void CL_AddLightStyles(void); // void CL_BlasterParticles2(const vec3_t org, const vec3_t dir, unsigned int color); -void CL_BlasterTrail2(const vec3_t start, const vec3_t end); +void CL_BlasterTrail2(centity_t *ent, const vec3_t end); void CL_DebugTrail(const vec3_t start, const vec3_t end); void CL_Flashlight(int ent, const vec3_t pos); void CL_ForceWall(const vec3_t start, const vec3_t end, int color); void CL_BubbleTrail2(const vec3_t start, const vec3_t end, int dist); void CL_Heatbeam(const vec3_t start, const vec3_t end); void CL_ParticleSteamEffect(const vec3_t org, const vec3_t dir, int color, int count, int magnitude); -void CL_TrackerTrail(const vec3_t start, const vec3_t end); -void CL_TagTrail(const vec3_t start, const vec3_t end, int color); +void CL_TrackerTrail(centity_t *ent, const vec3_t end); +void CL_TagTrail(centity_t *ent, const vec3_t end, int color); void CL_ColorFlash(const vec3_t pos, int ent, int intensity, float r, float g, float b); void CL_Tracker_Shell(const centity_t *cent, const vec3_t origin); void CL_MonsterPlasma_Shell(const vec3_t origin); @@ -915,7 +915,7 @@ void CL_ParticleSmokeEffect(const vec3_t org, const vec3_t dir, int color, int c void CL_Widowbeamout(cl_sustain_t *self); void CL_Nukeblast(cl_sustain_t *self); void CL_WidowSplash(void); -void CL_IonripperTrail(const vec3_t start, const vec3_t end); +void CL_IonripperTrail(centity_t *ent, const vec3_t end); void CL_TrapParticles(centity_t *ent, const vec3_t origin); void CL_ParticleEffect3(const vec3_t org, const vec3_t dir, int color, int count); void CL_ParticleSteamEffect2(cl_sustain_t *self); diff --git a/src/client/effects.c b/src/client/effects.c index 85e81d9f5..c7563b25d 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -1191,29 +1191,26 @@ CL_BlasterTrail =============== */ -void CL_BlasterTrail(const vec3_t start, const vec3_t end) +void CL_BlasterTrail(centity_t *ent, const vec3_t end) { vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - int dec; + const int dec = 5; - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); - - dec = 5; - VectorScale(vec, 5, vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - // FIXME: this is a really silly way to have a loop - while (len > 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -1228,6 +1225,8 @@ void CL_BlasterTrail(const vec3_t start, const vec3_t end) VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* @@ -1236,28 +1235,26 @@ CL_FlagTrail =============== */ -void CL_FlagTrail(const vec3_t start, const vec3_t end, int color) +void CL_FlagTrail(centity_t *ent, const vec3_t end, int color) { vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - int dec; + const int dec = 5; - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); - - dec = 5; - VectorScale(vec, 5, vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - while (len > 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -1272,6 +1269,8 @@ void CL_FlagTrail(const vec3_t start, const vec3_t end, int color) VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* @@ -1280,29 +1279,29 @@ CL_DiminishingTrail =============== */ -void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, diminishing_trail_t type) +void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t type) { static const byte colors[DT_COUNT] = { 0xe8, 0xdb, 0x04, 0xd8 }; vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - float dec; + const float dec = 0.5f; float orgscale; float velscale; - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - dec = 0.5f; + VectorCopy(ent->lerp_origin, move); VectorScale(vec, dec, vec); - if (old->trailcount > 900) { + if (ent->trailcount > 900) { orgscale = 4; velscale = 15; - } else if (old->trailcount > 800) { + } else if (ent->trailcount > 800) { orgscale = 2; velscale = 10; } else { @@ -1310,14 +1309,12 @@ void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, d velscale = 5; } - while (len > 0) { - len -= dec; - + for (i = 0; i < count; i++) { // drop less particles as it flies - if ((Q_rand() & 1023) < old->trailcount) { + if ((Q_rand() & 1023) < ent->trailcount) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -1336,16 +1333,18 @@ void CL_DiminishingTrail(const vec3_t start, const vec3_t end, centity_t *old, d p->vel[2] -= PARTICLE_GRAVITY; if (type == DT_FIREBALL) - p->color = colors[type] + (1024 - old->trailcount) / 64; + p->color = colors[type] + (1024 - ent->trailcount) / 64; else p->color = colors[type] + (Q_rand() & 7); } - old->trailcount -= 5; - if (old->trailcount < 100) - old->trailcount = 100; + ent->trailcount -= 5; + if (ent->trailcount < 100) + ent->trailcount = 100; VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* @@ -1354,33 +1353,31 @@ CL_RocketTrail =============== */ -void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old) +void CL_RocketTrail(centity_t *ent, const vec3_t end) { vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - float dec; - - // smoke - CL_DiminishingTrail(start, end, old, DT_SMOKE); + const int dec = 1; - // fire - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - dec = 1; + VectorCopy(ent->lerp_origin, move); VectorScale(vec, dec, vec); - while (len > 0) { - len -= dec; + // smoke + CL_DiminishingTrail(ent, end, DT_SMOKE); + // fire + for (i = 0; i < count; i++) { if ((Q_rand() & 7) == 0) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -1396,6 +1393,8 @@ void CL_RocketTrail(const vec3_t start, const vec3_t end, centity_t *old) } VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* diff --git a/src/client/entities.c b/src/client/entities.c index 48cdffe5b..c33e3c650 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -496,7 +496,7 @@ static void CL_AddPacketEntities(void) int autoanim; clientinfo_t *ci; unsigned int effects, renderfx; - bool has_alpha; + bool has_alpha, has_trail; // bonus items rotate at a fixed rate autorotate = anglemod(cl.time * 0.1f); @@ -514,6 +514,8 @@ static void CL_AddPacketEntities(void) cent = &cl_entities[s1->number]; + has_trail = false; + effects = s1->effects; renderfx = s1->renderfx; @@ -939,32 +941,39 @@ static void CL_AddPacketEntities(void) goto skip; if (effects & EF_ROCKET) { - if (cl.csr.extended && effects & EF_GIB) - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_FIREBALL); - else if (!(cl_disable_particles->integer & NOPART_ROCKET_TRAIL)) - CL_RocketTrail(cent->lerp_origin, ent.origin, cent); + if (cl.csr.extended && effects & EF_GIB) { + CL_DiminishingTrail(cent, ent.origin, DT_FIREBALL); + has_trail = true; + } else if (!(cl_disable_particles->integer & NOPART_ROCKET_TRAIL)) { + CL_RocketTrail(cent, ent.origin); + has_trail = true; + } if (cl_dlight_hacks->integer & DLHACK_ROCKET_COLOR) V_AddLight(ent.origin, 200, 1, 0.23f, 0); else V_AddLight(ent.origin, 200, 1, 1, 0); } else if (effects & EF_BLASTER) { if (effects & EF_TRACKER) { - CL_BlasterTrail2(cent->lerp_origin, ent.origin); + CL_BlasterTrail2(cent, ent.origin); V_AddLight(ent.origin, 200, 0, 1, 0); } else { - CL_BlasterTrail(cent->lerp_origin, ent.origin); + CL_BlasterTrail(cent, ent.origin); V_AddLight(ent.origin, 200, 1, 1, 0); } + has_trail = true; } else if (effects & EF_HYPERBLASTER) { if (effects & EF_TRACKER) V_AddLight(ent.origin, 200, 0, 1, 0); else V_AddLight(ent.origin, 200, 1, 1, 0); } else if (effects & EF_GIB) { - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_GIB); + CL_DiminishingTrail(cent, ent.origin, DT_GIB); + has_trail = true; } else if (effects & EF_GRENADE) { - if (!(cl_disable_particles->integer & NOPART_GRENADE_TRAIL)) - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_SMOKE); + if (!(cl_disable_particles->integer & NOPART_GRENADE_TRAIL)) { + CL_DiminishingTrail(cent, ent.origin, DT_SMOKE); + has_trail = true; + } } else if (effects & EF_FLIES) { CL_FlyEffect(cent, ent.origin); } else if (effects & EF_BFG) { @@ -982,14 +991,17 @@ static void CL_AddPacketEntities(void) i = (Com_SlowRand() % 100) + 100; V_AddLight(ent.origin, i, 1, 0.8f, 0.1f); } else if (effects & EF_FLAG1) { - CL_FlagTrail(cent->lerp_origin, ent.origin, 242); + CL_FlagTrail(cent, ent.origin, 242); V_AddLight(ent.origin, 225, 1, 0.1f, 0.1f); + has_trail = true; } else if (effects & EF_FLAG2) { - CL_FlagTrail(cent->lerp_origin, ent.origin, 115); + CL_FlagTrail(cent, ent.origin, 115); V_AddLight(ent.origin, 225, 0.1f, 0.1f, 1); + has_trail = true; } else if (effects & EF_TAGTRAIL) { - CL_TagTrail(cent->lerp_origin, ent.origin, 220); + CL_TagTrail(cent, ent.origin, 220); V_AddLight(ent.origin, 225, 1.0f, 1.0f, 0.0f); + has_trail = true; } else if (effects & EF_TRACKERTRAIL) { if (effects & EF_TRACKER) { float intensity = 50 + (500 * (sinf(cl.time / 500.0f) + 1.0f)); @@ -999,23 +1011,29 @@ static void CL_AddPacketEntities(void) V_AddLight(ent.origin, 155, -1.0f, -1.0f, -1.0f); } } else if (effects & EF_TRACKER) { - CL_TrackerTrail(cent->lerp_origin, ent.origin); + CL_TrackerTrail(cent, ent.origin); V_AddLight(ent.origin, 200, -1, -1, -1); + has_trail = true; } else if (effects & EF_GREENGIB) { - CL_DiminishingTrail(cent->lerp_origin, ent.origin, cent, DT_GREENGIB); + CL_DiminishingTrail(cent, ent.origin, DT_GREENGIB); + has_trail = true; } else if (effects & EF_IONRIPPER) { - CL_IonripperTrail(cent->lerp_origin, ent.origin); + CL_IonripperTrail(cent, ent.origin); V_AddLight(ent.origin, 100, 1, 0.5f, 0.5f); + has_trail = true; } else if (effects & EF_BLUEHYPERBLASTER) { V_AddLight(ent.origin, 200, 0, 0, 1); } else if (effects & EF_PLASMA) { - if (effects & EF_ANIM_ALLFAST) - CL_BlasterTrail(cent->lerp_origin, ent.origin); + if (effects & EF_ANIM_ALLFAST) { + CL_BlasterTrail(cent, ent.origin); + has_trail = true; + } V_AddLight(ent.origin, 130, 1, 0.5f, 0.5f); } skip: - VectorCopy(ent.origin, cent->lerp_origin); + if (!has_trail) + VectorCopy(ent.origin, cent->lerp_origin); } } diff --git a/src/client/newfx.c b/src/client/newfx.c index 287306957..7dd4fedd7 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -290,34 +290,33 @@ void CL_ParticleSteamEffect2(cl_sustain_t *self) CL_TrackerTrail =============== */ -void CL_TrackerTrail(const vec3_t start, const vec3_t end) +void CL_TrackerTrail(centity_t *ent, const vec3_t end) { vec3_t move; vec3_t vec; - vec3_t forward, right, up, angle_dir; - float len; + vec3_t forward, up, angle_dir; + int i, count, sign; cparticle_t *p; - int dec; + const int dec = 3; float dist; - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; VectorCopy(vec, forward); vectoangles2(forward, angle_dir); - AngleVectors(angle_dir, forward, right, up); - - dec = 3; - VectorScale(vec, 3, vec); + AngleVectors(angle_dir, NULL, NULL, up); - // FIXME: this is a really silly way to have a loop - while (len > 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + sign = ent->trailcount; + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -325,12 +324,18 @@ void CL_TrackerTrail(const vec3_t start, const vec3_t end) p->alpha = 1.0f; p->alphavel = -2.0f; p->color = 0; - dist = 8 * cosf(DotProduct(move, forward)); + dist = 8 * cosf(DotProduct(move, forward) * M_PIf / 64); + if (sign & 1) + dist = -dist; VectorMA(move, dist, up, p->org); VectorSet(p->vel, 0, 0, 5); VectorAdd(move, vec, move); + sign ^= 1; } + + ent->trailcount = sign; + VectorCopy(move, ent->lerp_origin); } static void RandomDir(vec3_t dir) @@ -483,28 +488,26 @@ CL_TagTrail =============== */ -void CL_TagTrail(const vec3_t start, const vec3_t end, int color) +void CL_TagTrail(centity_t *ent, const vec3_t end, int color) { vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - int dec; - - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); + const int dec = 5; - dec = 5; - VectorScale(vec, 5, vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - while (len >= 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -519,6 +522,8 @@ void CL_TagTrail(const vec3_t start, const vec3_t end, int color) VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* @@ -634,29 +639,26 @@ CL_BlasterTrail2 Green! =============== */ -void CL_BlasterTrail2(const vec3_t start, const vec3_t end) +void CL_BlasterTrail2(centity_t *ent, const vec3_t end) { vec3_t move; vec3_t vec; - float len; - int j; + int i, j, count; cparticle_t *p; - int dec; + const int dec = 5; - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); - - dec = 5; - VectorScale(vec, 5, vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - // FIXME: this is a really silly way to have a loop - while (len > 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -671,6 +673,8 @@ void CL_BlasterTrail2(const vec3_t start, const vec3_t end) VectorAdd(move, vec, move); } + + VectorCopy(move, ent->lerp_origin); } /* @@ -678,28 +682,27 @@ void CL_BlasterTrail2(const vec3_t start, const vec3_t end) CL_IonripperTrail =============== */ -void CL_IonripperTrail(const vec3_t start, const vec3_t end) +void CL_IonripperTrail(centity_t *ent, const vec3_t end) { - vec3_t move; - vec3_t vec; - float len; + vec3_t move; + vec3_t vec; cparticle_t *p; - int dec; - int left = 0; - - VectorCopy(start, move); - VectorSubtract(end, start, vec); - len = VectorNormalize(vec); + const int dec = 5; + int i, count, sign; - dec = 5; - VectorScale(vec, 5, vec); + VectorSubtract(end, ent->lerp_origin, vec); + count = VectorNormalize(vec) / dec; + if (!count) + return; - while (len > 0) { - len -= dec; + VectorCopy(ent->lerp_origin, move); + VectorScale(vec, dec, vec); + sign = ent->trailcount; + for (i = 0; i < count; i++) { p = CL_AllocParticle(); if (!p) - return; + break; VectorClear(p->accel); p->time = cl.time; @@ -709,13 +712,16 @@ void CL_IonripperTrail(const vec3_t start, const vec3_t end) VectorCopy(move, p->org); - p->vel[0] = left ? 10 : -10; + p->vel[0] = (sign & 1) ? 10 : -10; p->vel[1] = 0; p->vel[2] = 0; - left ^= 1; VectorAdd(move, vec, move); + sign ^= 1; } + + ent->trailcount = sign; + VectorCopy(move, ent->lerp_origin); } /* From f56a21c350c938cd295232a032738c97e0813ff1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 9 Sep 2024 15:32:59 +0300 Subject: [PATCH 719/974] Don't add player lights twice in third person. --- src/client/entities.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/entities.c b/src/client/entities.c index c33e3c650..7aacca489 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -765,7 +765,7 @@ static void CL_AddPacketEntities(void) if (s1->morefx & EFX_GRENADE_LIGHT) V_AddLight(ent.origin, 100, 1, 1, 0); - if (s1->number == cl.frame.clientNum + 1) { + if (s1->number == cl.frame.clientNum + 1 && !cl.thirdPersonView) { if (effects & EF_FLAG1) V_AddLight(ent.origin, 225, 1.0f, 0.1f, 0.1f); else if (effects & EF_FLAG2) @@ -774,10 +774,7 @@ static void CL_AddPacketEntities(void) V_AddLight(ent.origin, 225, 1.0f, 1.0f, 0.0f); else if (effects & EF_TRACKERTRAIL) V_AddLight(ent.origin, 225, -1.0f, -1.0f, -1.0f); - if (!cl.thirdPersonView) { - //ent.flags |= RF_VIEWERMODEL; // only draw from mirrors - goto skip; - } + goto skip; } // if set to invisible, skip From b50098980abfc07c59852855882c94dda62ab2cd Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 9 Sep 2024 16:22:11 +0300 Subject: [PATCH 720/974] =?UTF-8?q?Don't=20let=20=E2=80=98cl=5Fgibs=200?= =?UTF-8?q?=E2=80=99=20disable=20fireballs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/entities.c | 8 ++++++-- src/server/entities.c | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/entities.c b/src/client/entities.c index 7aacca489..b521e3f16 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -611,8 +611,12 @@ static void CL_AddPacketEntities(void) ent.oldorigin[2] += autobob; } - if (effects & (EF_GIB | EF_GREENGIB) && !cl_gibs->integer) - goto skip; + if (!cl_gibs->integer) { + if (effects & EF_GIB && !(cl.csr.extended && effects & EF_ROCKET)) + goto skip; + if (effects & EF_GREENGIB) + goto skip; + } // create a new entity diff --git a/src/server/entities.c b/src/server/entities.c index e4780e192..b8984f811 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -642,9 +642,15 @@ void SV_BuildClientFrame(client_t *client) if (!HAS_EFFECTS(ent)) continue; - if (ent->s.effects & (EF_GIB | EF_GREENGIB) && client->settings[CLS_NOGIBS]) - continue; + // ignore gibs if client says so + if (client->settings[CLS_NOGIBS]) { + if (ent->s.effects & EF_GIB && !(client->csr->extended && ent->s.effects & EF_ROCKET)) + continue; + if (ent->s.effects & EF_GREENGIB) + continue; + } + // ignore flares if client says so if (client->csr->extended && ent->s.renderfx & RF_FLARE && client->settings[CLS_NOFLARES]) continue; From 664c1b4b40ef4ce1fd37b5691d9ae5718c533370 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 9 Sep 2024 16:15:11 -0400 Subject: [PATCH 721/974] Addressed merge conflicts, g_client_s changes --- inc/shared/game.h | 17 +++--- inc/shared/shared.h | 15 ++---- src/action/g_local.h | 10 ++-- src/client/parse.c | 7 +-- src/common/msg.c | 49 ------------------ src/server/game.c | 2 +- src/server/main.c | 7 --- src/server/server.h | 17 ------ src/server/user.c | 120 +++++++++++++++++++++++-------------------- 9 files changed, 86 insertions(+), 158 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 190d9367a..b348ffe35 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -25,20 +25,16 @@ with this program; if not, write to the Free Software Foundation, Inc., // game.h -- game dll information visible to server // -<<<<<<< HEAD -#if AQTION_EXTENSION -#define GAME_API_VERSION 4 -#else -#define GAME_API_VERSION 3 -======= #define GAME_API_VERSION_OLD 3 // game uses gclient_old_t +#define GAME_API_VERSION_AQTION 4 // game uses gclient_t with AQtion extension (aliased to gclient_old_t) #define GAME_API_VERSION_NEW 3300 // game uses gclient_new_t #if USE_NEW_GAME_API #define GAME_API_VERSION GAME_API_VERSION_NEW +#elif AQTION_EXTENSION +#define GAME_API_VERSION GAME_API_VERSION_AQTION #else #define GAME_API_VERSION GAME_API_VERSION_OLD ->>>>>>> 9b15c229 (Support more protocol extensions.) #endif // edict->svflags @@ -96,13 +92,15 @@ typedef struct link_s link_t; typedef struct edict_s edict_t; -typedef struct gclient_s gclient_t; #ifndef GAME_INCLUDE typedef struct gclient_old_s gclient_old_t; typedef struct gclient_new_s gclient_new_t; +// AQtion compatible +typedef gclient_old_t gclient_t; + struct gclient_old_s { player_state_old_t ps; // communicated by server to clients int ping; @@ -113,6 +111,9 @@ struct gclient_old_s { // the game dll can add anything it wants after // this point in the structure +#if AQTION_EXTENSION + cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; +#endif }; struct gclient_new_s { diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 4c6149cf5..bae5285be 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1072,14 +1072,11 @@ typedef struct { short gravity; short delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters -<<<<<<< HEAD #if AQTION_EXTENSION short pm_aq2_flags; // limping, bandaging, etc unsigned short pm_timestamp; // timestamp, resets every 60 seconds byte pm_aq2_leghits; // number of leg hits #endif -} pmove_state_t; -======= } pmove_state_old_t; #if USE_NEW_GAME_API @@ -1093,9 +1090,13 @@ typedef struct { int16_t gravity; int16_t delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters +#if AQTION_EXTENSION + short pm_aq2_flags; // limping, bandaging, etc + unsigned short pm_timestamp; // timestamp, resets every 60 seconds + byte pm_aq2_leghits; // number of leg hits +#endif } pmove_state_new_t; #endif ->>>>>>> 9b15c229 (Support more protocol extensions.) // // button bits @@ -1457,7 +1458,6 @@ enum { STAT_LAYOUTS, STAT_FRAGS, STAT_FLASHES, // cleared each frame, 1 = health, 2 = armor -<<<<<<< HEAD STAT_CLIP_ICON, STAT_CLIP, STAT_SNIPER_ICON, @@ -1480,14 +1480,9 @@ enum { #define STAT_TEAM1_HEADER 30 #define STAT_TEAM2_HEADER 31 -======= - STAT_CHASE, - STAT_SPECTATOR, -}; #define MAX_STATS_OLD 32 #define MAX_STATS_NEW 64 ->>>>>>> 9b15c229 (Support more protocol extensions.) // STAT_LAYOUTS flags #define LAYOUTS_LAYOUT BIT(0) diff --git a/src/action/g_local.h b/src/action/g_local.h index 8b201dbb8..1a7d12d42 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -297,6 +297,9 @@ #include "botlib/botlib.h" #endif +// 9b15 commit from upstream -- using player_state_old_t instead of player_state_new_t +typedef struct gclient_s gclient_t; + #define getEnt(entnum) (edict_t *)((char *)globals.edicts + (globals.edict_size * entnum)) //AQ:TNG Slicer - This was missing #define GAMEVERSION "action" // the "gameversion" client command will print this plus compile date @@ -1941,14 +1944,15 @@ client_respawn_t; // this structure is cleared on each PutClientInServer(), // except for 'client->pers' +// 9b15 commit from upstream -- using player_state_old_t instead of player_state_new_t struct gclient_s { // known to server - player_state_t ps; // communicated by server to clients - int ping; + player_state_old_t ps; // communicated by server to clients + int ping; // known to compatible server - int clientNum; + int clientNum; // Reki: cvar sync #if AQTION_EXTENSION diff --git a/src/client/parse.c b/src/client/parse.c index 51f282da7..e4b4b95e6 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -576,13 +576,8 @@ static void CL_ParseServerData(void) // BIG HACK to let demos from release work with the 3.0x patch!!! if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_EXTENDED_OLD) { cl.csr = cs_remap_new; -<<<<<<< HEAD - protocol = PROTOCOL_VERSION_DEFAULT; - } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_AQTION) { -======= cls.serverProtocol = PROTOCOL_VERSION_DEFAULT; - } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_DEFAULT) { ->>>>>>> 9b15c229 (Support more protocol extensions.) + } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_AQTION) { Com_Error(ERR_DROP, "Demo uses unsupported protocol version %d.", protocol); } else { cls.serverProtocol = protocol; diff --git a/src/common/msg.c b/src/common/msg.c index caa92a066..87e8262bc 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -2785,32 +2785,6 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, to->pmove.pm_flags = MSG_ReadByte(); } -<<<<<<< HEAD - if (extraflags & EPS_M_ORIGIN2) - to->pmove.origin[2] = MSG_ReadShort(); - - if (flags & PS_M_VELOCITY) { - to->pmove.velocity[0] = MSG_ReadShort(); - to->pmove.velocity[1] = MSG_ReadShort(); - } - - if (extraflags & EPS_M_VELOCITY2) - { - to->pmove.velocity[2] = MSG_ReadShort(); - } - - if (flags & PS_M_TIME) - { - to->pmove.pm_time = MSG_ReadByte(); - } - - if (flags & PS_M_FLAGS) - { - to->pmove.pm_flags = MSG_ReadByte(); - } - -======= ->>>>>>> 9b15c229 (Support more protocol extensions.) if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); @@ -2911,29 +2885,6 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, to->pmove.origin[1] = MSG_ReadShort(); } - if (extraflags & EPS_M_ORIGIN2) { - to->pmove.origin[2] = MSG_ReadShort(); - } - - if (flags & PS_M_VELOCITY) { - to->pmove.velocity[0] = MSG_ReadShort(); - to->pmove.velocity[1] = MSG_ReadShort(); - } - - if (extraflags & EPS_M_VELOCITY2) { - to->pmove.velocity[2] = MSG_ReadShort(); - } - - if (flags & PS_M_TIME) - { - to->pmove.pm_time = MSG_ReadByte(); - } - - if (flags & PS_M_FLAGS) - { - to->pmove.pm_flags = MSG_ReadByte(); - } - if (flags & PS_M_GRAVITY) to->pmove.gravity = MSG_ReadShort(); diff --git a/src/server/game.c b/src/server/game.c index 030853c23..5a0ed8916 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1149,7 +1149,7 @@ void SV_InitGameProgs(void) Com_DPrintf("Game API version: %d\n", ge->apiversion); - if (ge->apiversion != GAME_API_VERSION_OLD && ge->apiversion != GAME_API_VERSION_NEW) { + if (ge->apiversion != GAME_API_VERSION_OLD && ge->apiversion != GAME_API_VERSION_AQTION && ge->apiversion != GAME_API_VERSION_NEW) { Com_Error(ERR_DROP, "Game library is version %d, expected %d", ge->apiversion, GAME_API_VERSION_OLD); } diff --git a/src/server/main.c b/src/server/main.c index 40afa535c..327f79679 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -19,13 +19,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server.h" #include "client/input.h" -<<<<<<< HEAD bot_client_t bot_clients[MAX_CLIENTS]; pmoveParams_t sv_pmp; -======= ->>>>>>> 9b15c229 (Support more protocol extensions.) master_t sv_masters[MAX_MASTERS]; // address of group servers LIST_DECL(sv_banlist); @@ -879,12 +876,8 @@ static bool parse_enhanced_params(conn_params_t *p) return reject("This is a protocol limit removing enhanced server.\n" "Your client version is not compatible. Make sure you are " -<<<<<<< HEAD "running latest Q2PRO client version.\nYour major protocol version: %d\n " "Your minor protocol version: %d\n", p->protocol, p->version); -======= - "running the latest Q2PRO client version.\n"); ->>>>>>> 9b15c229 (Support more protocol extensions.) } return true; diff --git a/src/server/server.h b/src/server/server.h index a73f68303..a759bf232 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -180,20 +180,6 @@ typedef struct { #define MAX_TOTAL_ENT_LEAFS 128 -<<<<<<< HEAD -// hack for smooth BSP model rotation -#define Q2PRO_SHORTANGLES(c, e) \ - ((((c)->protocol == PROTOCOL_VERSION_Q2PRO && \ - (c)->version >= PROTOCOL_VERSION_Q2PRO_SHORT_ANGLES) || \ - (c)->protocol == PROTOCOL_VERSION_AQTION) && \ - sv.state == ss_game && \ - EDICT_POOL(c, e)->solid == SOLID_BSP) -#define CLIENT_COMPATIBLE(csr, c) \ - (!(csr)->extended || (((c)->protocol == PROTOCOL_VERSION_Q2PRO && (c)->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) \ - || ((c)->protocol == PROTOCOL_VERSION_AQTION && (c)->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS))) - -======= ->>>>>>> 9b15c229 (Support more protocol extensions.) #define ENT_EXTENSION(csr, ent) ((csr)->extended ? &(ent)->x : NULL) typedef enum { @@ -827,7 +813,6 @@ void SV_RegisterSavegames(void); #define SV_RegisterSavegames() (void)0 #endif -<<<<<<< HEAD #if AQTION_EXTENSION // // sv_ghud.c @@ -849,7 +834,6 @@ void SV_Ghud_SetSize(edict_t *ent, int i, int x, int y); extern int(*GE_customizeentityforclient)(edict_t *client, edict_t *ent, entity_state_t *state); // 0 don't send, 1 send normally extern void(*GE_CvarSync_Updated)(int index, edict_t *clent); #endif -======= // // ugly gclient_(old|new)_t accessors // @@ -888,7 +872,6 @@ static inline void SV_SetClient_Ping(const client_t *client, int ping) else ((gclient_old_t *)client->edict->client)->ping = ping; } ->>>>>>> 9b15c229 (Support more protocol extensions.) //============================================================ diff --git a/src/server/user.c b/src/server/user.c index 87568e143..0d7d42b90 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -531,27 +531,29 @@ void SV_New_f(void) MSG_WriteString(var->value); if (sv_client->edict) - { - strcpy(sv_client->edict->client->cl_cvar[i], var->value); + { + gclient_t *client = (gclient_t *)sv_client->edict->client; + strcpy(client->cl_cvar[i], var->value); - if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated - GE_CvarSync_Updated(i, sv_client->edict); - } + if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated + GE_CvarSync_Updated(i, sv_client->edict); + } } SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); } - else if (sv_client->edict) - { - for (int i = 0; i < svs.cvarsync_length; i++) - { - cvarsync_t *var = &svs.cvarsync_list[i]; - strcpy(sv_client->edict->client->cl_cvar[i], var->value); - - if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated - GE_CvarSync_Updated(i, sv_client->edict); - } - } + else if (sv_client->edict) + { + gclient_t *client = (gclient_t *)sv_client->edict->client; // Cast to the correct type + for (int i = 0; i < svs.cvarsync_length; i++) + { + cvarsync_t *var = &svs.cvarsync_list[i]; + strcpy(client->cl_cvar[i], var->value); + + if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated + GE_CvarSync_Updated(i, sv_client->edict); + } + } #endif } @@ -944,47 +946,50 @@ static void SV_CvarSync_f(void) return; if (Cmd_Argc() > 2) { - char varname[CVARSYNC_MAX]; - Q_strlcpy(varname, Cmd_Argv(1), CVARSYNC_MAX); - varname[CVARSYNC_MAX - 1] = 0; + char varname[CVARSYNC_MAX]; + Q_strlcpy(varname, Cmd_Argv(1), CVARSYNC_MAX); + varname[CVARSYNC_MAX - 1] = 0; - for (int i = 0; i < svs.cvarsync_length; i++) - { - cvarsync_t *var = &svs.cvarsync_list[i]; - if (strcmp(var->name, varname)) - continue; - - strcpy(sv_client->edict->client->cl_cvar[i], Cmd_Argv(2)); - - if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated - GE_CvarSync_Updated(i, sv_client->edict); + for (int i = 0; i < svs.cvarsync_length; i++) + { + cvarsync_t *var = &svs.cvarsync_list[i]; + if (strcmp(var->name, varname)) + continue; - SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: set %s to %s\n", varname, sv_client->edict->client->cl_cvar[i]); - } - } - else if (Cmd_Argc() == 2) - { - char varname[CVARSYNC_MAX]; - Q_strlcpy(varname, Cmd_Argv(1), CVARSYNC_MAX); - varname[CVARSYNC_MAX - 1] = 0; + gclient_t *client = (gclient_t *)sv_client->edict->client; // Cast to the correct type + strcpy(client->cl_cvar[i], Cmd_Argv(2)); - for (int i = 0; i < svs.cvarsync_length; i++) - { - cvarsync_t *var = &svs.cvarsync_list[i]; - if (strcmp(var->name, varname)) - continue; + if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated + GE_CvarSync_Updated(i, sv_client->edict); - SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: %s is currently set to %s\n", varname, sv_client->edict->client->cl_cvar[i]); - } - } - else - { - for (int i = 0; i < svs.cvarsync_length; i++) - { - cvarsync_t *var = &svs.cvarsync_list[i]; - SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: %s = %s\n", var->name, sv_client->edict->client->cl_cvar[i]); - } - } + SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: set %s to %s\n", varname, client->cl_cvar[i]); + } + } + else if (Cmd_Argc() == 2) + { + char varname[CVARSYNC_MAX]; + Q_strlcpy(varname, Cmd_Argv(1), CVARSYNC_MAX); + varname[CVARSYNC_MAX - 1] = 0; + + for (int i = 0; i < svs.cvarsync_length; i++) + { + cvarsync_t *var = &svs.cvarsync_list[i]; + if (strcmp(var->name, varname)) + continue; + + gclient_t *client = (gclient_t *)sv_client->edict->client; // Cast to the correct type + SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: %s is currently set to %s\n", varname, client->cl_cvar[i]); + } + } + else + { + for (int i = 0; i < svs.cvarsync_length; i++) + { + cvarsync_t *var = &svs.cvarsync_list[i]; + gclient_t *client = (gclient_t *)sv_client->edict->client; // Cast to the correct type + SV_ClientPrintf(sv_client, PRINT_HIGH, "cvarsync: %s = %s\n", var->name, client->cl_cvar[i]); + } + } } #endif @@ -1786,11 +1791,12 @@ void SV_ExecuteClientMessage(client_t *client) if (client->protocol != PROTOCOL_VERSION_AQTION) goto badbyte; #if AQTION_EXTENSION - index = MSG_ReadByte(); - MSG_ReadString(sv_player->client->cl_cvar[index], CVARSYNC_MAXSIZE); + index = MSG_ReadByte(); + gclient_t *client = (gclient_t *)sv_player->client; // Cast to the correct type + MSG_ReadString(client->cl_cvar[index], CVARSYNC_MAXSIZE); - if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated - GE_CvarSync_Updated(index, sv_player); + if (GE_CvarSync_Updated) // poke game dll to tell it a cvar was updated + GE_CvarSync_Updated(index, sv_player); #else goto badbyte; #endif From 161722c4d5f515e479c415d56692e0cad36369d0 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 10 Sep 2024 16:22:49 +0300 Subject: [PATCH 722/974] Merge CL_RocketTrail() into CL_DiminishingTrail(). Since both need to step lerp_origin at the same time, this is cleaner and more reliable. N.B. Since step is now 0.5, reduce particle spawn probability 2x. --- src/client/client.h | 4 ++-- src/client/effects.c | 51 ++++++++++--------------------------------- src/client/entities.c | 4 ++-- 3 files changed, 15 insertions(+), 44 deletions(-) diff --git a/src/client/client.h b/src/client/client.h index 1a2fb509c..6292f73a0 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -859,14 +859,14 @@ typedef struct { typedef enum { DT_GIB, DT_GREENGIB, - DT_SMOKE, + DT_ROCKET, + DT_GRENADE, DT_FIREBALL, DT_COUNT } diminishing_trail_t; void CL_BigTeleportParticles(const vec3_t org); -void CL_RocketTrail(centity_t *ent, const vec3_t end); void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t type); void CL_FlyEffect(centity_t *ent, const vec3_t origin); void CL_BfgParticles(const entity_t *ent); diff --git a/src/client/effects.c b/src/client/effects.c index c7563b25d..06a2a3e45 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -1277,11 +1277,13 @@ void CL_FlagTrail(centity_t *ent, const vec3_t end, int color) =============== CL_DiminishingTrail +Now combined with CL_RocketTrail(). =============== */ void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t type) { - static const byte colors[DT_COUNT] = { 0xe8, 0xdb, 0x04, 0xd8 }; + static const byte colors[DT_COUNT] = { 0xe8, 0xdb, 0x04, 0x04, 0xd8 }; + static const float alphas[DT_COUNT] = { 0.4f, 0.4f, 0.2f, 0.2f, 0.4f }; vec3_t move; vec3_t vec; int i, j, count; @@ -1320,14 +1322,14 @@ void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t t p->time = cl.time; p->alpha = 1.0f; - p->alphavel = -1.0f / (1 + frand() * (type == DT_SMOKE ? 0.2f : 0.4f)); + p->alphavel = -1.0f / (1 + frand() * alphas[type]); for (j = 0; j < 3; j++) { p->org[j] = move[j] + crand() * orgscale; p->vel[j] = crand() * velscale; } - if (type >= DT_SMOKE) + if (type >= DT_ROCKET) p->accel[2] = 20; else p->vel[2] -= PARTICLE_GRAVITY; @@ -1338,43 +1340,8 @@ void CL_DiminishingTrail(centity_t *ent, const vec3_t end, diminishing_trail_t t p->color = colors[type] + (Q_rand() & 7); } - ent->trailcount -= 5; - if (ent->trailcount < 100) - ent->trailcount = 100; - VectorAdd(move, vec, move); - } - - VectorCopy(move, ent->lerp_origin); -} - -/* -=============== -CL_RocketTrail - -=============== -*/ -void CL_RocketTrail(centity_t *ent, const vec3_t end) -{ - vec3_t move; - vec3_t vec; - int i, j, count; - cparticle_t *p; - const int dec = 1; - - VectorSubtract(end, ent->lerp_origin, vec); - count = VectorNormalize(vec) / dec; - if (!count) - return; - - VectorCopy(ent->lerp_origin, move); - VectorScale(vec, dec, vec); - - // smoke - CL_DiminishingTrail(ent, end, DT_SMOKE); - - // fire - for (i = 0; i < count; i++) { - if ((Q_rand() & 7) == 0) { + // rocket fire (non-diminishing) + if (type == DT_ROCKET && (Q_rand() & 15) == 0) { p = CL_AllocParticle(); if (!p) break; @@ -1391,6 +1358,10 @@ void CL_RocketTrail(centity_t *ent, const vec3_t end) } p->accel[2] = -PARTICLE_GRAVITY; } + + ent->trailcount -= 5; + if (ent->trailcount < 100) + ent->trailcount = 100; VectorAdd(move, vec, move); } diff --git a/src/client/entities.c b/src/client/entities.c index b521e3f16..acfe17da5 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -946,7 +946,7 @@ static void CL_AddPacketEntities(void) CL_DiminishingTrail(cent, ent.origin, DT_FIREBALL); has_trail = true; } else if (!(cl_disable_particles->integer & NOPART_ROCKET_TRAIL)) { - CL_RocketTrail(cent, ent.origin); + CL_DiminishingTrail(cent, ent.origin, DT_ROCKET); has_trail = true; } if (cl_dlight_hacks->integer & DLHACK_ROCKET_COLOR) @@ -972,7 +972,7 @@ static void CL_AddPacketEntities(void) has_trail = true; } else if (effects & EF_GRENADE) { if (!(cl_disable_particles->integer & NOPART_GRENADE_TRAIL)) { - CL_DiminishingTrail(cent, ent.origin, DT_SMOKE); + CL_DiminishingTrail(cent, ent.origin, DT_GRENADE); has_trail = true; } } else if (effects & EF_FLIES) { From aa93ba95780112df34ae90df24ceb21479d1b3ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 10 Sep 2024 16:23:16 +0300 Subject: [PATCH 723/974] Apply particle scale to distance too. Also tone down distance multiplier a bit. --- src/refresh/tess.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 83287d9fe..d7d4e080d 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -93,9 +93,10 @@ void GL_DrawParticles(void) VectorSubtract(p->origin, glr.fd.vieworg, transformed); dist = DotProduct(transformed, glr.viewaxis[0]); - scale = gl_partscale->value; + scale = 1.0f; if (dist > 20) - scale += dist * 0.01f; + scale += dist * 0.004f; + scale *= gl_partscale->value; scale2 = scale * PARTICLE_SCALE; VectorMA(p->origin, scale2, glr.viewaxis[1], dst_vert); From 45c44c8200f196362d06f0f50dd17dfa5a037dec Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 10 Sep 2024 13:52:39 -0400 Subject: [PATCH 724/974] Protocol 38 currently broken (not packetentities error) --- src/common/msg.c | 193 +++++++++++++++++++++--------------------- src/server/entities.c | 6 +- 2 files changed, 99 insertions(+), 100 deletions(-) diff --git a/src/common/msg.c b/src/common/msg.c index 87e8262bc..f4d87e590 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -1359,7 +1359,6 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags) { - int i; int pflags, eflags, aqtflags; int statbits; @@ -1437,13 +1436,16 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, pflags |= PS_KICKANGLES; if (!(flags & MSG_PS_IGNORE_BLEND)) { - if (!Vector4Compare(from->blend, to->blend)) - pflags |= PS_BLEND; - } - else { - // save previous state - Vector4Copy(from->blend, to->blend); - } + if (!Vector4Compare(from->blend, to->blend)) + pflags |= PS_BLEND; + else if (flags & MSG_PS_EXTENSIONS_2 && + !Vector4Compare(to->damage_blend, from->damage_blend)) + pflags |= PS_BLEND; + } else { + // save previous state + Vector4Copy(from->blend, to->blend); + Vector4Copy(from->damage_blend, to->damage_blend); + } if (from->fov != to->fov) pflags |= PS_FOV; @@ -1475,13 +1477,9 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, VectorCopy(from->gunangles, to->gunangles); } - statbits = 0; - for (i = 0; i < MAX_STATS; i++) - if (to->stats[i] != from->stats[i]) - statbits |= 1U << i; - - if (statbits) - eflags |= EPS_STATS; + statbits = MSG_CalcStatBits(from, to, flags); + if (statbits) + eflags |= EPS_STATS; // // aqtion extension checks @@ -1502,44 +1500,65 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, MSG_WriteShort(pflags); // - // write the pmove_state_t - // - if (pflags & PS_M_TYPE) - MSG_WriteByte(to->pmove.pm_type); + // write the pmove_state_t + // + if (pflags & PS_M_TYPE) + MSG_WriteByte(to->pmove.pm_type); - if (pflags & PS_M_ORIGIN) { - MSG_WriteShort(to->pmove.origin[0]); - MSG_WriteShort(to->pmove.origin[1]); - } + if (flags & MSG_PS_EXTENSIONS_2) { + if (pflags & PS_M_ORIGIN) { + MSG_WriteDeltaInt23(from->pmove.origin[0], to->pmove.origin[0]); + MSG_WriteDeltaInt23(from->pmove.origin[1], to->pmove.origin[1]); + } - if (eflags & EPS_M_ORIGIN2) - MSG_WriteShort(to->pmove.origin[2]); + if (eflags & EPS_M_ORIGIN2) + MSG_WriteDeltaInt23(from->pmove.origin[2], to->pmove.origin[2]); - if (pflags & PS_M_VELOCITY) { - MSG_WriteShort(to->pmove.velocity[0]); - MSG_WriteShort(to->pmove.velocity[1]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteDeltaInt23(from->pmove.velocity[0], to->pmove.velocity[0]); + MSG_WriteDeltaInt23(from->pmove.velocity[1], to->pmove.velocity[1]); + } - if (eflags & EPS_M_VELOCITY2) - MSG_WriteShort(to->pmove.velocity[2]); + if (eflags & EPS_M_VELOCITY2) + MSG_WriteDeltaInt23(from->pmove.velocity[2], to->pmove.velocity[2]); - if (pflags & PS_M_TIME) { - MSG_WriteByte(to->pmove.pm_time); - } + if (pflags & PS_M_TIME) + MSG_WriteShort(to->pmove.pm_time); - if (pflags & PS_M_FLAGS) { - MSG_WriteByte(to->pmove.pm_flags); - } + if (pflags & PS_M_FLAGS) + MSG_WriteShort(to->pmove.pm_flags); + } else { + if (pflags & PS_M_ORIGIN) { + MSG_WriteShort(to->pmove.origin[0]); + MSG_WriteShort(to->pmove.origin[1]); + } - if (pflags & PS_M_GRAVITY) - MSG_WriteShort(to->pmove.gravity); + if (eflags & EPS_M_ORIGIN2) + MSG_WriteShort(to->pmove.origin[2]); - if (pflags & PS_M_DELTA_ANGLES) { - MSG_WriteShort(to->pmove.delta_angles[0]); - MSG_WriteShort(to->pmove.delta_angles[1]); - MSG_WriteShort(to->pmove.delta_angles[2]); - } + if (pflags & PS_M_VELOCITY) { + MSG_WriteShort(to->pmove.velocity[0]); + MSG_WriteShort(to->pmove.velocity[1]); + } + + if (eflags & EPS_M_VELOCITY2) + MSG_WriteShort(to->pmove.velocity[2]); + + if (pflags & PS_M_TIME) + MSG_WriteByte(to->pmove.pm_time); + + if (pflags & PS_M_FLAGS) + MSG_WriteByte(to->pmove.pm_flags); + } + if (pflags & PS_M_GRAVITY) + MSG_WriteShort(to->pmove.gravity); + + if (pflags & PS_M_DELTA_ANGLES) { + MSG_WriteShort(to->pmove.delta_angles[0]); + MSG_WriteShort(to->pmove.delta_angles[1]); + MSG_WriteShort(to->pmove.delta_angles[2]); + } // // write aqtion extensions @@ -1555,77 +1574,59 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, MSG_WriteByte(to->pmove.pm_aq2_leghits); #endif + // + // write the rest of the player_state_t + // + if (pflags & PS_VIEWOFFSET) + MSG_WriteData(to->viewoffset, sizeof(to->viewoffset)); - // - // write the rest of the player_state_t - // - if (pflags & PS_VIEWOFFSET) { - MSG_WriteChar(to->viewoffset[0]); - MSG_WriteChar(to->viewoffset[1]); - MSG_WriteChar(to->viewoffset[2]); - } - - if (pflags & PS_VIEWANGLES) { - MSG_WriteShort(to->viewangles[0]); - MSG_WriteShort(to->viewangles[1]); - } + if (pflags & PS_VIEWANGLES) { + MSG_WriteShort(to->viewangles[0]); + MSG_WriteShort(to->viewangles[1]); + } - if (eflags & EPS_VIEWANGLE2) - MSG_WriteShort(to->viewangles[2]); + if (eflags & EPS_VIEWANGLE2) + MSG_WriteShort(to->viewangles[2]); - if (pflags & PS_KICKANGLES) { - MSG_WriteChar(to->kick_angles[0]); - MSG_WriteChar(to->kick_angles[1]); - MSG_WriteChar(to->kick_angles[2]); - } + if (pflags & PS_KICKANGLES) + MSG_WriteData(to->kick_angles, sizeof(to->kick_angles)); - if (pflags & PS_WEAPONINDEX) { + if (pflags & PS_WEAPONINDEX) { if (flags & MSG_PS_EXTENSIONS) MSG_WriteShort(to->gunindex); else MSG_WriteByte(to->gunindex); } - if (pflags & PS_WEAPONFRAME) - MSG_WriteByte(to->gunframe); + if (pflags & PS_WEAPONFRAME) + MSG_WriteByte(to->gunframe); - if (eflags & EPS_GUNOFFSET) { - MSG_WriteChar(to->gunoffset[0]); - MSG_WriteChar(to->gunoffset[1]); - MSG_WriteChar(to->gunoffset[2]); - } + if (eflags & EPS_GUNOFFSET) + MSG_WriteData(to->gunoffset, sizeof(to->gunoffset)); - if (eflags & EPS_GUNANGLES) { - MSG_WriteChar(to->gunangles[0]); - MSG_WriteChar(to->gunangles[1]); - MSG_WriteChar(to->gunangles[2]); - } + if (eflags & EPS_GUNANGLES) + MSG_WriteData(to->gunangles, sizeof(to->gunangles)); - if (pflags & PS_BLEND) { - MSG_WriteByte(to->blend[0]); - MSG_WriteByte(to->blend[1]); - MSG_WriteByte(to->blend[2]); - MSG_WriteByte(to->blend[3]); - } + if (pflags & PS_BLEND) { + if (flags & MSG_PS_EXTENSIONS_2) + MSG_WriteDeltaBlend(from, to); + else + MSG_WriteData(to->blend, sizeof(to->blend)); + } - if (pflags & PS_FOV) - MSG_WriteByte(to->fov); + if (pflags & PS_FOV) + MSG_WriteByte(to->fov); - if (pflags & PS_RDFLAGS) - MSG_WriteByte(to->rdflags); + if (pflags & PS_RDFLAGS) + MSG_WriteByte(to->rdflags); - // send stats - if (eflags & EPS_STATS) { - MSG_WriteLong(statbits); - for (i = 0; i < MAX_STATS; i++) - if (statbits & (1U << i)) - MSG_WriteShort(to->stats[i]); - } + // send stats + if (eflags & EPS_STATS) + MSG_WriteStats(to, statbits, flags); - return eflags; + return eflags; } - #if USE_MVD_SERVER || USE_MVD_CLIENT || USE_CLIENT_GTV /* diff --git a/src/server/entities.c b/src/server/entities.c index edcba524f..6765f529b 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -522,7 +522,7 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) MSG_WriteData(frame->areabits, frame->areabytes); // ignore some parts of playerstate if not recording demo - psFlags = 0; + psFlags = client->psFlags; if (!client->settings[CLS_RECORDING]) { if (client->settings[CLS_NOGUN]) { psFlags |= MSG_PS_IGNORE_GUNFRAMES; @@ -537,8 +537,7 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) if (!(frame->ps.pmove.pm_flags & PMF_NO_PREDICTION)) { psFlags |= MSG_PS_IGNORE_VIEWANGLES; } - } - else { + } else { // lying dead on a rotating platform? psFlags |= MSG_PS_IGNORE_DELTAANGLES; } @@ -585,7 +584,6 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) // delta encode the entities MSG_WriteByte(svc_packetentities); - return SV_EmitPacketEntities(client, oldframe, frame, clientEntityNum, maxsize); } From a3ba5de29688bfeb6b888ff0533c1dbf426acb82 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 10 Sep 2024 14:33:06 -0400 Subject: [PATCH 725/974] Seems to be working better now? --- src/common/msg.c | 211 ++++++++++++++++++++++++++--------------------- 1 file changed, 118 insertions(+), 93 deletions(-) diff --git a/src/common/msg.c b/src/common/msg.c index f4d87e590..911543b12 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -2716,7 +2716,7 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, /* =================== -MSG_ParseDeltaPlayerstate_Default +MSG_ParseDeltaPlayerstate_Enhanced =================== */ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, @@ -2854,50 +2854,91 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, MSG_ReadStats(to, psflags); } - - +/* +=================== +MSG_ParseDeltaPlayerstate_Aqtion +=================== +*/ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, - player_state_t *to, - int flags, - int extraflags, - msgPsFlags_t psflags) + player_state_t *to, + int flags, + int extraflags, + msgPsFlags_t psflags) { - int i; - int statbits; - int aqtflags; + Q_assert(to); - Q_assert(to); + int aqtflags; - // clear to old value before delta parsing - if (!from) { - memset(to, 0, sizeof(*to)); - } else if (to != from) { - memcpy(to, from, sizeof(*to)); - } + // clear to old value before delta parsing + if (!from) { + memset(to, 0, sizeof(*to)); + } else if (to != from) { + memcpy(to, from, sizeof(*to)); + } - // - // parse the pmove_state_t - // - if (flags & PS_M_TYPE) - to->pmove.pm_type = MSG_ReadByte(); + // + // parse the pmove_state_t + // + if (flags & PS_M_TYPE) + to->pmove.pm_type = MSG_ReadByte(); - if (flags & PS_M_ORIGIN) { - to->pmove.origin[0] = MSG_ReadShort(); - to->pmove.origin[1] = MSG_ReadShort(); - } + if (psflags & MSG_PS_EXTENSIONS_2) { + if (flags & PS_M_ORIGIN) { + MSG_ReadDeltaInt23(&to->pmove.origin[0]); + MSG_ReadDeltaInt23(&to->pmove.origin[1]); + } + + if (extraflags & EPS_M_ORIGIN2) + MSG_ReadDeltaInt23(&to->pmove.origin[2]); - if (flags & PS_M_GRAVITY) - to->pmove.gravity = MSG_ReadShort(); + if (flags & PS_M_VELOCITY) { + MSG_ReadDeltaInt23(&to->pmove.velocity[0]); + MSG_ReadDeltaInt23(&to->pmove.velocity[1]); + } - if (flags & PS_M_DELTA_ANGLES) { - to->pmove.delta_angles[0] = MSG_ReadShort(); - to->pmove.delta_angles[1] = MSG_ReadShort(); - to->pmove.delta_angles[2] = MSG_ReadShort(); - } + if (extraflags & EPS_M_VELOCITY2) + MSG_ReadDeltaInt23(&to->pmove.velocity[2]); - // - // parse the aqtion extensions - // + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadWord(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadWord(); + } else { + if (flags & PS_M_ORIGIN) { + to->pmove.origin[0] = MSG_ReadShort(); + to->pmove.origin[1] = MSG_ReadShort(); + } + + if (extraflags & EPS_M_ORIGIN2) + to->pmove.origin[2] = MSG_ReadShort(); + + if (flags & PS_M_VELOCITY) { + to->pmove.velocity[0] = MSG_ReadShort(); + to->pmove.velocity[1] = MSG_ReadShort(); + } + + if (extraflags & EPS_M_VELOCITY2) + to->pmove.velocity[2] = MSG_ReadShort(); + + if (flags & PS_M_TIME) + to->pmove.pm_time = MSG_ReadByte(); + + if (flags & PS_M_FLAGS) + to->pmove.pm_flags = MSG_ReadByte(); + } + + if (flags & PS_M_GRAVITY) + to->pmove.gravity = MSG_ReadShort(); + + if (flags & PS_M_DELTA_ANGLES) { + to->pmove.delta_angles[0] = MSG_ReadShort(); + to->pmove.delta_angles[1] = MSG_ReadShort(); + to->pmove.delta_angles[2] = MSG_ReadShort(); + } + // + // parse the aqtion extensions + // #if AQTION_EXTENSION aqtflags = MSG_ReadByte(); @@ -2922,81 +2963,65 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, MSG_ReadByte(); #endif + // + // parse the rest of the player_state_t + // + if (flags & PS_VIEWOFFSET) { + to->viewoffset[0] = MSG_ReadChar() * 0.25f; + to->viewoffset[1] = MSG_ReadChar() * 0.25f; + to->viewoffset[2] = MSG_ReadChar() * 0.25f; + } + if (flags & PS_VIEWANGLES) { + to->viewangles[0] = MSG_ReadAngle16(); + to->viewangles[1] = MSG_ReadAngle16(); + } - // - // parse the rest of the player_state_t - // - if (flags & PS_VIEWOFFSET) { - to->viewoffset[0] = MSG_ReadChar() * 0.25f; - to->viewoffset[1] = MSG_ReadChar() * 0.25f; - to->viewoffset[2] = MSG_ReadChar() * 0.25f; - } - - if (flags & PS_VIEWANGLES) { - to->viewangles[0] = MSG_ReadAngle16(); - to->viewangles[1] = MSG_ReadAngle16(); - } - - if (extraflags & EPS_VIEWANGLE2) { - to->viewangles[2] = MSG_ReadAngle16(); - } + if (extraflags & EPS_VIEWANGLE2) + to->viewangles[2] = MSG_ReadAngle16(); - if (flags & PS_KICKANGLES) { - to->kick_angles[0] = MSG_ReadChar() * 0.25f; - to->kick_angles[1] = MSG_ReadChar() * 0.25f; - to->kick_angles[2] = MSG_ReadChar() * 0.25f; - } + if (flags & PS_KICKANGLES) { + to->kick_angles[0] = MSG_ReadChar() * 0.25f; + to->kick_angles[1] = MSG_ReadChar() * 0.25f; + to->kick_angles[2] = MSG_ReadChar() * 0.25f; + } - if (flags & PS_WEAPONINDEX) { + if (flags & PS_WEAPONINDEX) { if (psflags & MSG_PS_EXTENSIONS) to->gunindex = MSG_ReadWord(); else to->gunindex = MSG_ReadByte(); } - if (flags & PS_WEAPONFRAME) { - to->gunframe = MSG_ReadByte(); - } - - if (extraflags & EPS_GUNOFFSET) { - to->gunoffset[0] = MSG_ReadChar() * 0.25f; - to->gunoffset[1] = MSG_ReadChar() * 0.25f; - to->gunoffset[2] = MSG_ReadChar() * 0.25f; - } + if (flags & PS_WEAPONFRAME) + to->gunframe = MSG_ReadByte(); - if (extraflags & EPS_GUNANGLES) { - to->gunangles[0] = MSG_ReadChar() * 0.25f; - to->gunangles[1] = MSG_ReadChar() * 0.25f; - to->gunangles[2] = MSG_ReadChar() * 0.25f; - } + if (extraflags & EPS_GUNOFFSET) { + to->gunoffset[0] = MSG_ReadChar() * 0.25f; + to->gunoffset[1] = MSG_ReadChar() * 0.25f; + to->gunoffset[2] = MSG_ReadChar() * 0.25f; + } - if (flags & PS_BLEND) { - to->blend[0] = MSG_ReadByte() / 255.0f; - to->blend[1] = MSG_ReadByte() / 255.0f; - to->blend[2] = MSG_ReadByte() / 255.0f; - to->blend[3] = MSG_ReadByte() / 255.0f; - } + if (extraflags & EPS_GUNANGLES) { + to->gunangles[0] = MSG_ReadChar() * 0.25f; + to->gunangles[1] = MSG_ReadChar() * 0.25f; + to->gunangles[2] = MSG_ReadChar() * 0.25f; + } - if (flags & PS_FOV) - to->fov = MSG_ReadByte(); + if (flags & PS_BLEND) + MSG_ReadBlend(to, psflags); - if (flags & PS_RDFLAGS) - to->rdflags = MSG_ReadByte(); + if (flags & PS_FOV) + to->fov = MSG_ReadByte(); - // parse stats - if (extraflags & EPS_STATS) { - statbits = MSG_ReadLong(); - for (i = 0; i < MAX_STATS; i++) { - if (statbits & (1U << i)) { - to->stats[i] = MSG_ReadShort(); - } - } - } + if (flags & PS_RDFLAGS) + to->rdflags = MSG_ReadByte(); + // parse stats + if (extraflags & EPS_STATS) + MSG_ReadStats(to, psflags); } - #endif // USE_CLIENT #if USE_MVD_CLIENT From 33cf2a286aafb2043872b0220a4ccd963dd1fd15 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 10 Sep 2024 17:26:28 -0400 Subject: [PATCH 726/974] ClientThink pmove old vs new fuckery --- inc/shared/game.h | 2 +- inc/shared/shared.h | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index f5c40688a..612534c04 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -99,7 +99,7 @@ typedef struct gclient_old_s gclient_old_t; typedef struct gclient_new_s gclient_new_t; // AQtion compatible -typedef gclient_old_t gclient_t; +typedef gclient_new_t gclient_t; struct gclient_old_s { player_state_old_t ps; // communicated by server to clients diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 424305350..27efe8ccb 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -927,7 +927,6 @@ typedef struct cvar_s { struct cvar_s *next; // ------ new stuff ------ -#if USE_CLIENT || USE_SERVER #if AQTION_EXTENSION int sync_index; #endif @@ -1086,7 +1085,6 @@ typedef enum { #define PMF_ON_LADDER BIT(8) //KEX - #if AQTION_EXTENSION // pmove->pm_aq2_flags #define PMF_AQ2_LIMP 0x01 // used to predict limping @@ -1790,9 +1788,6 @@ typedef struct { } player_state_new_t; #endif -#endif - - // Reki : Cvar Sync info shared between engine and game #define CVARSYNC_MAXSIZE 64 #define CVARSYNC_MAX 32 From da3a896f02e75b253a97c19cc979bf346d956ae0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 10 Sep 2024 17:32:15 -0400 Subject: [PATCH 727/974] Compiles, runs, some errors --- inc/shared/game.h | 2 +- src/refresh/main.c | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index 612534c04..f5c40688a 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -99,7 +99,7 @@ typedef struct gclient_old_s gclient_old_t; typedef struct gclient_new_s gclient_new_t; // AQtion compatible -typedef gclient_new_t gclient_t; +typedef gclient_old_t gclient_t; struct gclient_old_s { player_state_old_t ps; // communicated by server to clients diff --git a/src/refresh/main.c b/src/refresh/main.c index 3c4a673ae..300dc88e2 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1414,7 +1414,7 @@ static void GL_DrawSelection(void) } } -void GL_InitDebugDraw(void) +void GL_InitDebugDrawRK(void) { // Malloc nav if (sv.cm.draw == NULL) @@ -2124,6 +2124,18 @@ void GL_DeleteQueries(void) gl_static.queries = NULL; } +static void GL_ClearQueries(void) +{ + if (!gl_static.queries) + return; + + uint32_t map_size = HashMap_Size(gl_static.queries); + for (int i = 0; i < map_size; i++) { + glquery_t *q = HashMap_GetValue(glquery_t, gl_static.queries, i); + q->pending = q->visible = false; + } +} + // ============================================================================== /* @@ -2164,7 +2176,7 @@ bool R_Init(bool total) GL_InitTables(); - GL_InitDebugDraw(); + GL_InitDebugDrawRK(); GL_PostInit(); @@ -2287,7 +2299,7 @@ void R_BeginRegistration(const char *name) glr.viewcluster1 = glr.viewcluster2 = -2; //rekkie -- surface data -- s #if DEBUG_DRAWING - GL_InitDebugDraw(); // Init debug drawing functions + GL_InitDebugDrawRK(); // Init debug drawing functions #endif //rekkie -- surface data -- e GL_ClearQueries(); From ec8fba3e239ef9896e507c81f9fbf1d3f7d4fe56 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 11:53:59 -0400 Subject: [PATCH 728/974] Check for empty soundname, renamed BOTLIB-specific GL_InitDebugDraw --- src/client/effects.c | 12 +++++++----- src/refresh/main.c | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/client/effects.c b/src/client/effects.c index 04892b62e..c398b9bf0 100644 --- a/src/client/effects.c +++ b/src/client/effects.c @@ -163,7 +163,7 @@ void CL_MuzzleFlash(void) cdlight_t *dl; centity_t *pl; float volume; - char soundname[MAX_QPATH]; + char soundname[MAX_QPATH] = ""; // Should fix any \x01 issues? #if USE_DEBUG if (developer->integer) @@ -295,10 +295,12 @@ void CL_MuzzleFlash(void) // Play the sound defined in the case statement above // Normal attenuation for all guns except handcannon - if (mz.weapon != MZ_SSHOTGUN) { - S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound(soundname), volume, ATTN_NORM, 0); - } else { - S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound(soundname), volume, ATTN_LOUD, 0); + if (soundname != NULL && strcmp(soundname, "\x01") != 0) { + if (mz.weapon != MZ_SSHOTGUN) { + S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound(soundname), volume, ATTN_NORM, 0); + } else { + S_StartSound(NULL, mz.entity, CHAN_WEAPON, S_RegisterSound(soundname), volume, ATTN_LOUD, 0); + } } if (cl_dlight_hacks->integer & DLHACK_NO_MUZZLEFLASH) { diff --git a/src/refresh/main.c b/src/refresh/main.c index 300dc88e2..1a349757a 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1414,7 +1414,7 @@ static void GL_DrawSelection(void) } } -void GL_InitDebugDrawRK(void) +void GL_BOTLIBInitDebugDraw(void) { // Malloc nav if (sv.cm.draw == NULL) @@ -2176,7 +2176,7 @@ bool R_Init(bool total) GL_InitTables(); - GL_InitDebugDrawRK(); + GL_BOTLIBInitDebugDraw(); GL_PostInit(); @@ -2299,7 +2299,7 @@ void R_BeginRegistration(const char *name) glr.viewcluster1 = glr.viewcluster2 = -2; //rekkie -- surface data -- s #if DEBUG_DRAWING - GL_InitDebugDrawRK(); // Init debug drawing functions + GL_BOTLIBInitDebugDraw(); // Init debug drawing functions #endif //rekkie -- surface data -- e GL_ClearQueries(); From 192caceb1cc1922d8540fb0ff560de3b218ef321 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 12:55:14 -0400 Subject: [PATCH 729/974] Added some comments --- src/client/screen.c | 47 +++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index bead754c0..761f3e14d 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -82,9 +82,6 @@ static cvar_t *scr_lag_max; static cvar_t *scr_lag_draw_scale; static cvar_t *scr_alpha; -static cvar_t *scr_hudborder_x; -static cvar_t *scr_hudborder_y; - static cvar_t *scr_demobar; static cvar_t *scr_font; static cvar_t *scr_scale; @@ -1427,8 +1424,6 @@ void SCR_Init(void) scr_lag_max = Cvar_Get("scr_lag_max", "200", 0); scr_alpha = Cvar_Get("scr_alpha", "1", 0); - scr_hudborder_x = Cvar_Get("scr_hudborder_x", "0", 0); - scr_hudborder_y = Cvar_Get("scr_hudborder_y", "0", 0); #if USE_DEBUG scr_showstats = Cvar_Get("scr_showstats", "0", 0); scr_showpmove = Cvar_Get("scr_showpmove", "0", 0); @@ -2233,6 +2228,8 @@ static xhair_state_t XHAIR_ApplyFiringInaccuracy(xhair_state_t xh, int wep_index //Con_Printf("%d\n",cl.frame.ps.gunframe); return xh; } + +// mikota's xhair static void SCR_DrawXhair(void) { //Con_Printf("deltatime_factor: %g\n",deltatime_factor); R_SetColor(scr.crosshair_color.u32); @@ -2324,6 +2321,8 @@ static void SCR_DrawXhair(void) { } } } + +// mikota's xhair static void SCR_DrawClassicCrosshair(void) { R_SetColor(scr.crosshair_color.u32); } @@ -2362,9 +2361,11 @@ static void SCR_DrawCrosshair(void) if (cl.frame.ps.stats[STAT_LAYOUTS] & (LAYOUTS_HIDE_HUD | LAYOUTS_HIDE_CROSSHAIR)) return; - x = scr.hud_x + (scr.hud_width - scr.crosshair_width) / 2; - y = scr.hud_y + (scr.hud_height - scr.crosshair_height) / 2; - + x = (scr.hud_width - scr.crosshair_width) / 2; + y = (scr.hud_height - scr.crosshair_height) / 2; + + R_SetColor(scr.crosshair_color.u32); + R_DrawStretchPic(x + ch_x->integer, y + ch_y->integer, scr.crosshair_width, @@ -2373,6 +2374,8 @@ static void SCR_DrawCrosshair(void) SCR_DrawHitMarker(); } + + #if AQTION_EXTENSION void CL_Clear3DGhudQueue(void) { @@ -2615,6 +2618,7 @@ static void SCR_DrawLayout(void) SCR_ExecuteLayoutString(cl.layout); } + static void SCR_Draw2D(void) { if (scr_draw2d->integer <= 0) @@ -2623,23 +2627,19 @@ static void SCR_Draw2D(void) if (cls.key_dest & KEY_MENU) return; - if (xhair_enabled->integer) - SCR_DrawXhair(); - R_SetScale(scr.hud_scale); + scr.hud_height = Q_rint(scr.hud_height * scr.hud_scale); + scr.hud_width = Q_rint(scr.hud_width * scr.hud_scale); - scr.hud_x = Q_rint(scr_hudborder_x->integer); - scr.hud_y = Q_rint(scr_hudborder_y->integer); - scr.hud_width = Q_rint((scr.hud_width - scr.hud_x) * scr.hud_scale); - scr.hud_height = Q_rint((scr.hud_height - scr.hud_y) * scr.hud_scale); - scr.hud_x *= scr.hud_scale / 2; - scr.hud_y *= scr.hud_scale / 2; +// mikota's xhair +// if (!xhair_enabled->integer) { +// SCR_DrawClassicCrosshair(); +// } + + // crosshair has its own color and alpha + SCR_DrawCrosshair(); - if (!xhair_enabled->integer) { - SCR_DrawClassicCrosshair(); - } - // the rest of 2D elements share common alpha R_ClearColor(); R_SetAlpha(Cvar_ClampValue(scr_alpha, 0, 1)); @@ -2647,7 +2647,6 @@ static void SCR_Draw2D(void) SCR_DrawStats(); SCR_DrawLayout(); - #if AQTION_EXTENSION // Draw game defined hud elements SCR_DrawGhud(); @@ -2656,7 +2655,6 @@ static void SCR_Draw2D(void) R_ClearColor(); R_SetAlpha(Cvar_ClampValue(scr_alpha, 0, 1)); #endif - SCR_DrawInventory(); SCR_DrawCenterString(); @@ -2669,7 +2667,7 @@ static void SCR_Draw2D(void) SCR_DrawTurtle(); - SCR_DrawPause(); + SCR_DrawPause(); // debug stats have no alpha R_ClearColor(); @@ -2678,7 +2676,6 @@ static void SCR_Draw2D(void) SCR_DrawDebugStats(); SCR_DrawDebugPmove(); #endif - R_ClearColor(); R_SetScale(1.0f); } From a47e9bbf2584deb8b97ca0e30fead20a23b68858 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 15:50:09 -0400 Subject: [PATCH 730/974] Removed two sound files --- src/action/g_items.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/g_items.c b/src/action/g_items.c index 8e6e80824..70ff1438f 100644 --- a/src/action/g_items.c +++ b/src/action/g_items.c @@ -1387,7 +1387,7 @@ world_model_flags int copied to 'ent->s.effects' (see s.effects fo IT_WEAPON, NULL, 0, - "weapons/m4a1fire.wav weapons/m4a1in.wav weapons/m4a1out.wav weapons/m4a1slide.wav weapons/rocklf1a.wav weapons/rocklr1b.wav", + "weapons/m4a1fire.wav weapons/m4a1in.wav weapons/m4a1out.wav weapons/m4a1slide.wav weapons/rocklf1a.wav", M4_NUM} , { @@ -1409,7 +1409,7 @@ world_model_flags int copied to 'ent->s.effects' (see s.effects fo IT_WEAPON, NULL, 0, - "weapons/m3in.wav weapons/shotgr1b.wav weapons/shotgf1b.wav", + "weapons/m3in.wav weapons/shotgf1b.wav", M3_NUM} , { From bf9c6f45b1ce6711a0bad609a4617531cec9cfef Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 16:59:17 -0400 Subject: [PATCH 731/974] Starting to add GetExtensions --- inc/shared/gameext.h | 4 ++++ src/action/g_local.h | 3 +++ src/action/g_main.c | 21 +++++++++++++++++++++ src/server/game.c | 36 ++++++++++++------------------------ 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index 26016a12d..570836a54 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -49,6 +49,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 #define GAME_API_VERSION_EX_ENTITY_VISIBLE 3 #define GAME_API_VERSION_EX 3 +#define GAME_API_VERSION_EX_REKTEK_BOTS 400 +#define GAME_API_VERSION_EX 400 typedef enum { VIS_PVS = 0, @@ -137,3 +139,5 @@ typedef struct { void (*AddDebugText)(const vec3_t origin, const vec3_t angles, const char *text, float size, uint32_t color, uint32_t time, qboolean depth_test); } debug_draw_api_v1_t; + +#define REKTEK_BOTS_API_V1 "REKTEK_BOTS_API_V1" diff --git a/src/action/g_local.h b/src/action/g_local.h index 1a7d12d42..6ff95d942 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -272,6 +272,7 @@ // because we define the full size ones in this file #define GAME_INCLUDE #include "shared/game.h" +#include "shared/gameext.h" #include "a_team.h" #include "a_game.h" @@ -960,6 +961,8 @@ extern game_locals_t game; extern level_locals_t level; extern game_import_t gi; extern game_export_t globals; +extern const game_import_ex_t *gix; +extern const game_export_ex_t gex; extern spawn_temp_t st; extern int sm_meat_index; diff --git a/src/action/g_main.c b/src/action/g_main.c index 2a7548406..06253b4b9 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -275,6 +275,8 @@ game_locals_t game; level_locals_t level; game_import_t gi; game_export_t globals; +const game_import_ex_t *gix; +game_export_ex_t gex; spawn_temp_t st; int sm_meat_index; @@ -688,6 +690,25 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) return &globals; } +const game_export_ex_t gex = { + .apiversion = GAME_API_VERSION_EX, + // .structsize = sizeof(game_export_ex_t); + + // // Functionality examples? + // // https://github.com/skullernet/q2pro/issues/294#issuecomment-1476818818 + .GetExtension = GetExtension, +}; + +q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) +{ + gix = importx; // assign pointer, don't copy! + gex.apiversion = GAME_API_VERSION_EX; + gex.structsize = sizeof(game_export_ex_t); + gex.GetExtension = GetExtension; + + return &gex; +} + #ifndef GAME_HARD_LINKED // this is only here so the functions in q_shared.c can link void Com_LPrintf(print_type_t type, const char *fmt, ...) diff --git a/src/server/game.c b/src/server/game.c index 8e8700956..dad1c2f4f 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -875,6 +875,18 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { }; #endif +static const rektek_bots_api_v1 rektek_bots_v1 = { + .Bsp = SV_BSP, + .Nav = CS_NAV, +#if DEBUG_DRAWING + .Draw = CS_DebugDraw, +#endif + .SV_BotUpdateInfo = SV_BotUpdateInfo, + .SV_BotConnect = SV_BotConnect, + .SV_BotDisconnect = SV_BotDisconnect, + .SV_BotClearClients = SV_BotClearClients, +}; + static void *PF_GetExtension(const char *name) { if (!name) @@ -1141,30 +1153,6 @@ void SV_InitGameProgs(void) import.CheckForExtension = G_CheckForExtension; #endif -//rekkie -- BSP -- s - //#ifdef ACTION_DLL - import.Bsp = SV_BSP; - //#endif - //rekkie -- BSP -- e - //rekkie -- surface data -- s - import.Nav = CS_NAV; - //rekkie -- debug drawing -- s -#if DEBUG_DRAWING -//#if USE_REF - import.Draw = CS_DebugDraw; -//#endif -#endif -//rekkie -- debug drawing -- e - //import.SurfaceData = SV_SURFACE_DATA; - //rekkie -- surface data -- e - - //rekkie -- Fake Bot Client -- s - import.SV_BotUpdateInfo = SV_BotUpdateInfo; - import.SV_BotConnect = SV_BotConnect; - import.SV_BotDisconnect = SV_BotDisconnect; - import.SV_BotClearClients = SV_BotClearClients; - //rekkie -- Fake Bot Client -- e - ge = entry(&import); if (!ge) { Com_Error(ERR_DROP, "Game library returned NULL exports"); From e9aff9042e3872ff75ba4c8c25408e92b8da5070 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 17:45:39 -0400 Subject: [PATCH 732/974] Bit by bit --- inc/shared/game.h | 10 ---------- inc/shared/gameext.h | 15 ++++++++++++++- src/action/botlib/botlib_nodes.c | 31 +++++++++---------------------- src/action/g_main.c | 12 +++++++----- src/action/g_spawn.c | 4 +++- src/action/p_client.c | 4 +++- src/server/game.c | 2 +- src/server/world.c | 28 ++++++++++++++++++++++------ 8 files changed, 59 insertions(+), 47 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index f5c40688a..f09c5d036 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -259,17 +259,7 @@ typedef struct { #if AQTION_EXTENSION void *(*CheckForExtension)(char *text); #endif - bsp_t* (*Bsp)(void); - nav_t* (*Nav)(void); -#if DEBUG_DRAWING - debug_draw_t* (*Draw)(void); -#endif - surface_data_t* (*SurfaceData)(void); - void (*SV_BotUpdateInfo)(char* name, int ping, int score); - void (*SV_BotConnect)(char* name); - void (*SV_BotDisconnect)(char* name); - void (*SV_BotClearClients)(void); } game_import_t; // // functions exported by the game subsystem diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index 570836a54..ff99567d0 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -48,7 +48,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GAME_API_VERSION_EX_MINIMUM 1 #define GAME_API_VERSION_EX_CUSTOMIZE_ENTITY 2 #define GAME_API_VERSION_EX_ENTITY_VISIBLE 3 -#define GAME_API_VERSION_EX 3 #define GAME_API_VERSION_EX_REKTEK_BOTS 400 #define GAME_API_VERSION_EX 400 @@ -141,3 +140,17 @@ typedef struct { } debug_draw_api_v1_t; #define REKTEK_BOTS_API_V1 "REKTEK_BOTS_API_V1" +typedef struct { + bsp_t* (*Bsp)(void); + nav_t* (*Nav)(void); +#if DEBUG_DRAWING + debug_draw_t* (*Draw)(void); +#endif + surface_data_t* (*SurfaceData)(void); + + void (*SV_BotUpdateInfo)(char* name, int ping, int score); + void (*SV_BotConnect)(char* name); + void (*SV_BotDisconnect)(char* name); + void (*SV_BotClearClients)(void); +} rektek_bots_api_v1_t; +extern rektek_bots_api_v1_t rektek_bots_api_v1; diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index b15bddb7f..23dad22ef 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3038,15 +3038,6 @@ void BOTLIB_LoadNavCompressed(void) fclose(fIn); // Close the file return; } - /* - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - fclose(fIn); // Close the file - return; - } - */ } // Init Nodes @@ -3591,15 +3582,6 @@ void BOTLIB_LoadNav(void) fclose(fIn); // Close the file return; } - /* - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - fclose(fIn); // Close the file - return; - } - */ } // Init @@ -3915,7 +3897,8 @@ void ACEND_LoadAAS(qboolean force) fclose(fIn); // Close the file } - bsp_t* bsp = gi.Bsp(); + rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bsp_t* bsp = bsp_api->Bsp(); fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum if (bsp_checksum != bsp->checksum) { @@ -5345,7 +5328,8 @@ qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t no void BOTLIB_InitNavigation(edict_t* ent) { - bsp_t* bsp = gi.Bsp(); + rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bsp_t* bsp = bsp_api->Bsp(); if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); @@ -5374,7 +5358,8 @@ void BOTLIB_InitNavigation(edict_t* ent) void ACEND_BSP(edict_t* ent) { - bsp_t* bsp = gi.Bsp(); + rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bsp_t* bsp = bsp_api->Bsp(); if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); @@ -5411,7 +5396,9 @@ void ACEND_BSP(edict_t* ent) if (1) { - ent->nav = gi.Nav(); // Grant access to navigation data + rektek_bots_api_v1_t *nav_api = gex.GetExtension(REKTEK_BOTS_API_V1); + nav_t* nav = nav_api->Nav(); + //ent->nav = gi.Nav(); // Grant access to navigation data if (ent->nav) { for (int f = 0; f < ent->nav->faces_total; f++) diff --git a/src/action/g_main.c b/src/action/g_main.c index 06253b4b9..d082da3c3 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -276,7 +276,6 @@ level_locals_t level; game_import_t gi; game_export_t globals; const game_import_ex_t *gix; -game_export_ex_t gex; spawn_temp_t st; int sm_meat_index; @@ -690,6 +689,13 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) return &globals; } +static void *GetExtension(const char *name) +{ + if (!Q_stricmp(name, REKTEK_BOTS_API_V1)) + return (void *)&rektek_bots_api_v1; + return NULL; +} + const game_export_ex_t gex = { .apiversion = GAME_API_VERSION_EX, // .structsize = sizeof(game_export_ex_t); @@ -702,10 +708,6 @@ const game_export_ex_t gex = { q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) { gix = importx; // assign pointer, don't copy! - gex.apiversion = GAME_API_VERSION_EX; - gex.structsize = sizeof(game_export_ex_t); - gex.GetExtension = GetExtension; - return &gex; } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index fced5a9e9..218658f12 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1592,7 +1592,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn memset(&botlib_noises, 0, sizeof(botlib_noises)); //rekkie -- Fake Bot Client -- s - gi.SV_BotClearClients(); // So the server can clear all fake bot clients + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotClearClients(); + //gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e if(bot_personality->value) diff --git a/src/action/p_client.c b/src/action/p_client.c index 26462e288..0f4e11148 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3267,7 +3267,9 @@ void ClientBeginDeathmatch(edict_t * ent) //rekkie -- debug drawing -- s #if DEBUG_DRAWING - ent->client->pers.draw = gi.Draw(); + rektek_bots_api_v1_t *draw = gex.GetExtension(REKTEK_BOTS_API_V1); + + ent->client->pers.draw = draw->Draw(); //if (ent->client->pers.draw) { // Default all to off state diff --git a/src/server/game.c b/src/server/game.c index dad1c2f4f..1a4a4365c 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -875,7 +875,7 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { }; #endif -static const rektek_bots_api_v1 rektek_bots_v1 = { +static const rektek_bots_api_v1_t rektek_bots_v1 = { .Bsp = SV_BSP, .Nav = CS_NAV, #if DEBUG_DRAWING diff --git a/src/server/world.c b/src/server/world.c index c7537be6f..deb77551b 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -597,13 +597,21 @@ trace_t q_gameabi SV_Trace(const vec3_t start, const vec3_t mins, //============= bsp_t* SV_BSP(void) { - bsp_t* bsp = sv.cm.cache; - if (!bsp) { + rektek_bots_api_v1_t *bsp_api = gex->GetExtension(REKTEK_BOTS_API_V1); + + if (!bsp_api) { Com_Error(ERR_DROP, "%s: no map loaded", __func__); return NULL; } - return bsp; + // Ensure that bsp_api->Bsp is a function that returns bsp_t* + if (bsp_api->Bsp == NULL) { + Com_Error(ERR_DROP, "%s: Bsp function not set", __func__); + return NULL; + } + + // Call the Bsp function and return its result + return bsp_api->Bsp(); } //#endif //rekkie -- BSP -- e @@ -612,13 +620,21 @@ bsp_t* SV_BSP(void) // Share surface data with game library nav_t* CS_NAV(void) { - nav_t* nav = sv.cm.nav; - if (!nav) { + rektek_bots_api_v1_t *nav_api = gex->GetExtension(REKTEK_BOTS_API_V1); + + if (!nav_api) { //Com_Error(ERR_DROP, "%s: no nav data loaded", __func__); return NULL; } - return nav; + // Ensure that nav_api->Nav is a function that returns nav_t* + if (nav_api->Nav == NULL) { + //Com_Error(ERR_DROP, "%s: Nav function not set", __func__); + return NULL; + } + + // Call the Nav function and return its result + return nav_api->Nav(); } //rekkie -- debug drawing -- s From fdb688a7cbc2af6e34ad534a81a816b6b52ce6a5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 17:49:17 -0400 Subject: [PATCH 733/974] Bit by bit --- src/action/acesrc/acebot_spawn.c | 14 +++++++++++--- src/action/botlib/botlib_ai.c | 5 ++++- src/action/botlib/botlib_spawn.c | 17 +++++++++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 57311e70c..b9c437a0f 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -602,6 +602,7 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo { int team = 0; edict_t *bot = ACESP_FindFreeClient(); + if( ! bot ) { gi.bprintf( PRINT_MEDIUM, "Server is full, increase Maxclients.\n" ); @@ -637,7 +638,10 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo bot->bot.bot_baseline_ping = (int)(3 + (random() * 60)); // Country ping else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping - gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -695,7 +699,9 @@ void ACESP_RemoveBot(char *name) if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) { //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e bot->health = 0; @@ -766,7 +772,9 @@ void ACESP_RemoveTeamplayBot(int team) if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e game.bot_count--; diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index bd3b4f305..76e3f7394 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -649,7 +649,10 @@ void BOTLIB_Think(edict_t* self) self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); + + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' + //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index be2a6f569..a66298069 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1582,7 +1582,10 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for bot->bot.bot_baseline_ping = (int)(3 + (random() * 66)); // Average pinger else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -1614,7 +1617,9 @@ void BOTLIB_RemoveBot(char* name) if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) { //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e bot->health = 0; @@ -1685,7 +1690,9 @@ void BOTLIB_RemoveBot(char* name) } // Fake Bot Client - Disconnect the bot - gi.SV_BotDisconnect(bot->client->pers.netname); + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client bot->health = 0; player_die(bot, bot, bot, 100000, vec3_origin); @@ -1730,7 +1737,9 @@ void BOTLIB_RemoveTeamplayBot(int team) //if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e if (team == TEAM1) From ccf560b91b874ca7978eea86f006cbd3fefb3119 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 12 Sep 2024 01:00:03 +0300 Subject: [PATCH 734/974] Add command to play next track. --- doc/client.asciidoc | 3 ++- src/client/sound/ogg.c | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index abbcff7a9..579b45891 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -1593,11 +1593,12 @@ remotemode
      :: Put client console into rcon mode. All commands entered will be forwarded to remove server. Press Ctrl+D or close console to exit this mode. -ogg :: +ogg :: Execute OGG subcommand. Available subcommands: info::: Display information about currently playing background music track. play ::: Start playing background music track ‘music/_track_.ogg’. stop::: Stop playing background music track. + next::: Play next track if shuffling is enabled. whereis [all]:: Search for _path_ and print the name of packfile or directory where it is diff --git a/src/client/sound/ogg.c b/src/client/sound/ogg.c index d07cb2d17..4700a8a21 100644 --- a/src/client/sound/ogg.c +++ b/src/client/sound/ogg.c @@ -709,6 +709,7 @@ static void OGG_Cmd_c(genctx_t *ctx, int argnum) Prompt_AddMatch(ctx, "info"); Prompt_AddMatch(ctx, "play"); Prompt_AddMatch(ctx, "stop"); + Prompt_AddMatch(ctx, "next"); return; } @@ -726,8 +727,10 @@ static void OGG_Cmd_f(void) OGG_Play_f(); else if (!strcmp(cmd, "stop")) OGG_Stop(); + else if (!strcmp(cmd, "next")) + OGG_Play(); else - Com_Printf("Usage: %s \n", Cmd_Argv(0)); + Com_Printf("Usage: %s \n", Cmd_Argv(0)); } static void ogg_enable_changed(cvar_t *self) From 468d8a6cfc8a8b3f728361ef28cc4af5d485a082 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 11 Sep 2024 20:47:38 -0400 Subject: [PATCH 735/974] At this point I just have to assume it isn't loading GetExtension --- inc/shared/gameext.h | 2 +- src/action/acesrc/acebot_spawn.c | 6 +++--- src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_nodes.c | 11 ++++++----- src/action/botlib/botlib_spawn.c | 8 ++++---- src/action/g_local.h | 1 + src/action/g_main.c | 15 +++++---------- src/action/g_spawn.c | 2 +- src/action/p_client.c | 2 +- src/server/game.c | 19 +++++++++++++++---- 10 files changed, 38 insertions(+), 30 deletions(-) diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index ff99567d0..b4625783f 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -140,6 +140,7 @@ typedef struct { } debug_draw_api_v1_t; #define REKTEK_BOTS_API_V1 "REKTEK_BOTS_API_V1" + typedef struct { bsp_t* (*Bsp)(void); nav_t* (*Nav)(void); @@ -153,4 +154,3 @@ typedef struct { void (*SV_BotDisconnect)(char* name); void (*SV_BotClearClients)(void); } rektek_bots_api_v1_t; -extern rektek_bots_api_v1_t rektek_bots_api_v1; diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index b9c437a0f..13ec5b43d 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -639,7 +639,7 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; @@ -699,7 +699,7 @@ void ACESP_RemoveBot(char *name) if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -772,7 +772,7 @@ void ACESP_RemoveTeamplayBot(int team) if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 76e3f7394..06d6a4329 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -650,7 +650,7 @@ void BOTLIB_Think(edict_t* self) if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 23dad22ef..f1dee985c 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3897,8 +3897,9 @@ void ACEND_LoadAAS(qboolean force) fclose(fIn); // Close the file } - rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); bsp_t* bsp = bsp_api->Bsp(); + fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum if (bsp_checksum != bsp->checksum) { @@ -5328,9 +5329,9 @@ qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t no void BOTLIB_InitNavigation(edict_t* ent) { - rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); bsp_t* bsp = bsp_api->Bsp(); - if (bsp == NULL) + if (bsp_api->Bsp() == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); return; @@ -5358,7 +5359,7 @@ void BOTLIB_InitNavigation(edict_t* ent) void ACEND_BSP(edict_t* ent) { - rektek_bots_api_v1_t *bsp_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); bsp_t* bsp = bsp_api->Bsp(); if (bsp == NULL) { @@ -5396,7 +5397,7 @@ void ACEND_BSP(edict_t* ent) if (1) { - rektek_bots_api_v1_t *nav_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *nav_api = gix->GetExtension(REKTEK_BOTS_API_V1); nav_t* nav = nav_api->Nav(); //ent->nav = gi.Nav(); // Grant access to navigation data if (ent->nav) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index a66298069..555410e9d 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1583,7 +1583,7 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; @@ -1617,7 +1617,7 @@ void BOTLIB_RemoveBot(char* name) if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -1690,7 +1690,7 @@ void BOTLIB_RemoveBot(char* name) } // Fake Bot Client - Disconnect the bot - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client @@ -1737,7 +1737,7 @@ void BOTLIB_RemoveTeamplayBot(int team) //if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/g_local.h b/src/action/g_local.h index 6ff95d942..09b55a0fa 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2891,6 +2891,7 @@ typedef enum { #ifndef NO_BOTS #include "acesrc/acebot.h" + #endif typedef struct { diff --git a/src/action/g_main.c b/src/action/g_main.c index d082da3c3..dde078646 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -689,25 +689,20 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) return &globals; } -static void *GetExtension(const char *name) -{ - if (!Q_stricmp(name, REKTEK_BOTS_API_V1)) - return (void *)&rektek_bots_api_v1; - return NULL; -} - -const game_export_ex_t gex = { +static const game_export_ex_t gex = { .apiversion = GAME_API_VERSION_EX, - // .structsize = sizeof(game_export_ex_t); + .structsize = sizeof(game_export_ex_t), // // Functionality examples? // // https://github.com/skullernet/q2pro/issues/294#issuecomment-1476818818 - .GetExtension = GetExtension, + //.GetExtension = GetExtension, + }; q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) { gix = importx; // assign pointer, don't copy! + return &gex; } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 218658f12..edbea66be 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1592,7 +1592,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn memset(&botlib_noises, 0, sizeof(botlib_noises)); //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); bot_api->SV_BotClearClients(); //gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e diff --git a/src/action/p_client.c b/src/action/p_client.c index 0f4e11148..6cc9a50da 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3267,7 +3267,7 @@ void ClientBeginDeathmatch(edict_t * ent) //rekkie -- debug drawing -- s #if DEBUG_DRAWING - rektek_bots_api_v1_t *draw = gex.GetExtension(REKTEK_BOTS_API_V1); + rektek_bots_api_v1_t *draw = gix->GetExtension(REKTEK_BOTS_API_V1); ent->client->pers.draw = draw->Draw(); //if (ent->client->pers.draw) diff --git a/src/server/game.c b/src/server/game.c index 1a4a4365c..368feaa4e 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -875,7 +875,7 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { }; #endif -static const rektek_bots_api_v1_t rektek_bots_v1 = { +static const rektek_bots_api_v1_t rektek_bots_api_v1 = { .Bsp = SV_BSP, .Nav = CS_NAV, #if DEBUG_DRAWING @@ -895,6 +895,12 @@ static void *PF_GetExtension(const char *name) if (!strcmp(name, FILESYSTEM_API_V1)) return (void *)&filesystem_api_v1; +#ifdef AQTION_EXTENSION + if (!strcmp(name, REKTEK_BOTS_API_V1)) + return (void *)&rektek_bots_api_v1; +#endif + + #if USE_REF && USE_DEBUG if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer) return (void *)&debug_draw_api_v1; @@ -1167,12 +1173,17 @@ void SV_InitGameProgs(void) // get extended api if present game_entry_ex_t entry_ex = Sys_GetProcAddress(game_library, "GetGameAPIEx"); + Com_Printf("==== Extended Protocol ====\n"); if (entry_ex) { gex = entry_ex(&game_import_ex); - if (gex && gex->apiversion >= GAME_API_VERSION_EX_MINIMUM) - Com_DPrintf("Game supports Q2PRO extended API version %d.\n", gex->apiversion); - else + if (gex == NULL) { + Com_Printf("Disabled: Failed to get extended game API.\n"); + } else if (gex->apiversion < GAME_API_VERSION_EX_MINIMUM) { gex = NULL; + Com_Printf("Disabled: Extended game API version is too old.\n"); + } else { + Com_Printf("Game supports Q2PRO extended API version %d.\n", gex->apiversion); + } } // initialize From c2e31bb0311875a3f2e307c45401704beef8c3d1 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 11 Sep 2024 20:58:07 -0400 Subject: [PATCH 736/974] Included steamcloud in files --- src/common/files.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index f55a184d5..ebc8fe6fb 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -3559,7 +3559,6 @@ static void setup_game_paths(void) if (sys_homedir->string[0]) { add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "%s/%s", sys_homedir->string, fs_game->string); } - } // add SteamCloud sync dir to VFS if we are a client, steamcloud is enabled and steamID is found #if USE_CLIENT if (strcmp(steamid->string, "0") == 0){ @@ -3576,7 +3575,7 @@ static void setup_game_paths(void) // if SteamID, steamcloudappenabled and steamclouduserenabled is not 0, add steamcloud dir as new game_dir add_game_dir(FS_PATH_GAME, "./SteamCloud/%s/%s", steamid->string, fs_game->string); #endif - + } // this var is set for compatibility with server browsers, etc Cvar_FullSet("gamedir", fs_game->string, CVAR_ROM | CVAR_SERVERINFO, FROM_CODE); From 8917113b828b4c62846c7c42a97dda0f6ee2d1d0 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 11 Sep 2024 22:09:20 -0400 Subject: [PATCH 737/974] Steamcloud stuff broken, seeing if this helps --- src/common/files.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/files.c b/src/common/files.c index ebc8fe6fb..53c3dfaa7 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -3573,7 +3573,7 @@ static void setup_game_paths(void) return; } // if SteamID, steamcloudappenabled and steamclouduserenabled is not 0, add steamcloud dir as new game_dir - add_game_dir(FS_PATH_GAME, "./SteamCloud/%s/%s", steamid->string, fs_game->string); + add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "./SteamCloud/%s/%s", steamid->string, fs_game->string); #endif } // this var is set for compatibility with server browsers, etc From 2a5dffac6e2af03364a80dddb9e193c1c5b885ed Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 12 Sep 2024 13:18:41 -0400 Subject: [PATCH 738/974] This should fix the bots autospawning in DM mode --- src/action/botlib/botlib_spawn.c | 54 +++++++++++++++++--------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index be2a6f569..5c0c9b5de 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2045,36 +2045,38 @@ void BOTLIB_CheckBotRules(void) // Manage bot counts in DM mode int bots_to_spawn = 0; - if (bot_playercount->value > 0 && (gameSettings & GS_DEATHMATCH)) { - // Calculate the desired number of bots based on bot_playercount and total_humans - bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; + if ((gameSettings & GS_DEATHMATCH)) { + if (bot_playercount->value > 0) { + // Calculate the desired number of bots based on bot_playercount and total_humans + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; + + // Ensure desire_bots does not exceed maxclients - total_humans + if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { + bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); + } - // Ensure desire_bots does not exceed maxclients - total_humans - if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { - bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); - } + // Sanity check - safety limits + if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + int bots_adj = 0; + int total_players = bot_connections.total_bots + bot_connections.total_humans; + + if (total_players > bot_playercount->value) { + //gi.dprintf("I should remove a bot\n"); + bots_adj = -1; + } else if (total_players < bot_playercount->value) { + // gi.dprintf("I should add a bot\n"); + // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); + bots_adj = 1; + } else { + bots_to_spawn = (int)bot_playercount->value; + bot_connections.desire_bots = bots_to_spawn; + } - // Sanity check - safety limits - if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; - int bots_adj = 0; - int total_players = bot_connections.total_bots + bot_connections.total_humans; - - if (total_players > bot_playercount->value) { - //gi.dprintf("I should remove a bot\n"); - bots_adj = -1; - } else if (total_players < bot_playercount->value) { - // gi.dprintf("I should add a bot\n"); - // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); - bots_adj = 1; + bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed + //gi.dprintf("bots_adj: %d\n", bots_to_spawn); } else { - bots_to_spawn = (int)bot_playercount->value; - bot_connections.desire_bots = bots_to_spawn; + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; } - - bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed - //gi.dprintf("bots_adj: %d\n", bots_to_spawn); - } else { - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; } // // Auto balance bots From b1d332cc705c3818fb624f9bc35c9e92e94bba68 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 12 Sep 2024 16:58:01 -0400 Subject: [PATCH 739/974] Bot count in DM, TP and manual TP adding working? --- src/action/botlib/botlib.h | 1 + src/action/botlib/botlib_spawn.c | 386 +++++++++++++++++-------------- src/action/botlib/botlib_utils.c | 20 +- src/action/cgf_sfx_glass.c | 72 ++++-- src/action/g_local.h | 2 + src/action/g_main.c | 2 + src/action/g_save.c | 5 +- src/refresh/gl.h | 11 - src/refresh/main.c | 28 ++- 9 files changed, 307 insertions(+), 220 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 1a727601c..426752719 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -527,6 +527,7 @@ int BOTLIB_GetEquipment(edict_t* self); // =========================================================================== // botlib_utils.c // =========================================================================== +void seed_random_number_generator(void); void BOTLIB_SKILL_Init(edict_t* bot); qboolean BOTLIB_SkillChance(float skilltype); float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 5c0c9b5de..885420498 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -68,6 +68,8 @@ qboolean BOTLIB_GetRandomBotFileLine(const char* file, char* buffer) line_num++; } fclose(f); + + seed_random_number_generator(); random_line = rand() % line_num + 1; //random_line = 13; @@ -304,6 +306,8 @@ void BOTLIB_BotCountRemove(int bots_to_spawn) void BOTLIB_BotCountManager(int bots_to_spawn) { + if (bots_to_spawn == 0) return; // Don't even evaluate if no bots to add or remove + if (bots_to_spawn < 0) { BOTLIB_BotCountRemove(bots_to_spawn); } else if (bots_to_spawn > 0) { @@ -1919,226 +1923,265 @@ static void BOTLIB_TeamBotShuffle(void) } } -// Bots auto join teams to keep them equal in teamplay. -// Empty servers have a bot join a team upon a player connecting. int bot_teamcheckfrequency = 0; int bot_teamchangefrequency = 0; -void BOTLIB_CheckBotRules(void) +int BOTLIB_TPBotTeamScaling(void) { - if (matchmode->value) // Bots never allowed in matchmode - return; - - if (ctf->value) - { - BOTLIB_Update_Flags_Status(); - } + if (bot_maxteam->value) { + // Calculate the total number of bots needed + int total_bots_needed = bot_maxteam->value * teamCount; + + // Check if the total number of bots is equal to or greater than the total bots needed + if (bot_connections.total_bots == total_bots_needed) { + return 0; // Each team is populated, return 0 + } else if (bot_connections.total_bots > total_bots_needed) { + return -1; // Bots were removed + } - // check these rules every 1.0 seconds... (aka 10 frames) -- this will be faster if server hz is faster - if ((++bot_teamcheckfrequency % 10) != 0) - { - return; + // Calculate the percentage of bots relative to the max team size + float percent = (float)bot_connections.total_bots / total_bots_needed; + + // Initialize scaling direction if not already set + if (bot_connections.scale_dn == false && bot_connections.scale_up == false) { + bot_connections.scale_up = true; + } + + float min_percent = 1.0 / total_bots_needed; + + // Reached top end of the scale, so work back down + if (bot_connections.scale_up && percent >= 1.0) { + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } + // Reached bottom end of the scale, or randomly at 50%, so work back up + else if (bot_connections.scale_dn && (percent <= min_percent || random() < 0.05)) { + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } + + // Decrease bots + if (bot_connections.scale_dn) { + bot_connections.desire_bots--; + } + + // Increase bots + if (bot_connections.scale_up) { + bot_connections.desire_bots++; + } + + // Don't let bots be greater than total_bots_needed + if (bot_connections.desire_bots > total_bots_needed) { + bot_connections.desire_bots--; + } + + // Never go below 1 bot when bot_maxteam is active + if (bot_connections.desire_bots < 1) { + bot_connections.desire_bots = 1; + } + + // Remove excess bots if bot_maxteam is reduced + if (bot_connections.total_bots > total_bots_needed) { + bot_connections.total_bots = total_bots_needed; + return -1; // Bots were removed + } + + return 1; // Continue scaling + } else { // bot_maxteam 0, just add bots + if (bot_connections.desire_bots != bot_connections.total_bots){ + if (bot_connections.desire_bots > bot_connections.total_bots) { + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } else { + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } + } else { + bot_connections.scale_up = false; + bot_connections.scale_dn = false; + } } - else - { - bot_teamcheckfrequency = 0; - //Com_Printf("%s ltk_teamcheckfrequency\n", __func__); - } - - BOTLIB_GetTotalPlayers(&bot_connections); - - // All bots are counted as being on team 1 if in DM mode - - //gi.dprintf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d] tb[%d] th[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots, bot_connections.total_humans); - - if (bot_connections.tried_adding_prev_bots == false) - { - bot_connections.tried_adding_prev_bots = true; // Only called once per map - BOTLIB_AddBotsFromPreviousMap(100); // Try get bots from a previous map - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + if (bot_connections.scale_up){ + return 1; + } else if (bot_connections.scale_dn){ + return -1; + } else { + return 0; } +} - // Turn on team balance if bot_maxteam is used - if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; +void BOTLIB_TPBotCountManual(void) +{ + // Sanity check + if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; + if (bot_connections.desire_team2 < 0) bot_connections.desire_team2 = 0; + if (bot_connections.desire_team3 < 0) bot_connections.desire_team3 = 0; - // ======================================================================================================== - // Manually add bots to a select team - // sv bots - if (teamplay->value && bot_connections.auto_balance_bots == false) + // Remove bots + if (bot_connections.desire_team1 < bot_connections.team1_bots) { - // Sanity check - if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; - if (bot_connections.desire_team2 < 0) bot_connections.desire_team2 = 0; - if (bot_connections.desire_team3 < 0) bot_connections.desire_team3 = 0; - - // Remove bots - if (bot_connections.desire_team1 < bot_connections.team1_bots) + while (bot_connections.team1_bots != bot_connections.desire_team1) { - while (bot_connections.team1_bots != bot_connections.desire_team1) - { - BOTLIB_RemoveTeamplayBot(TEAM1); - bot_connections.team1_bots--; - } + BOTLIB_RemoveTeamplayBot(TEAM1); + bot_connections.team1_bots--; } - if (bot_connections.desire_team2 < bot_connections.team2_bots) + } + if (bot_connections.desire_team2 < bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) { - while (bot_connections.team2_bots != bot_connections.desire_team2) - { - BOTLIB_RemoveTeamplayBot(TEAM2); - bot_connections.team2_bots--; - } + BOTLIB_RemoveTeamplayBot(TEAM2); + bot_connections.team2_bots--; } - if (bot_connections.desire_team3 < bot_connections.team3_bots) + } + if (bot_connections.desire_team3 < bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) { - while (bot_connections.team3_bots != bot_connections.desire_team3) - { - BOTLIB_RemoveTeamplayBot(TEAM3); - bot_connections.team3_bots--; - } + BOTLIB_RemoveTeamplayBot(TEAM3); + bot_connections.team3_bots--; } + } - // Add bots - if (bot_connections.desire_team1 > bot_connections.team1_bots) + // Add bots + if (bot_connections.desire_team1 > bot_connections.team1_bots) + { + while (bot_connections.team1_bots != bot_connections.desire_team1) { - while (bot_connections.team1_bots != bot_connections.desire_team1) + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team1 = bot_connections.team1_bots; - break; - } - BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + bot_connections.desire_team1 = bot_connections.team1_bots; + break; } + BOTLIB_SpawnBot(TEAM1, INVALID, NULL, NULL); + bot_connections.desire_bots++; + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } - if (bot_connections.desire_team2 > bot_connections.team2_bots) + } + if (bot_connections.desire_team2 > bot_connections.team2_bots) + { + while (bot_connections.team2_bots != bot_connections.desire_team2) { - while (bot_connections.team2_bots != bot_connections.desire_team2) + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team2 = bot_connections.team2_bots; - break; - } - BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + bot_connections.desire_team2 = bot_connections.team2_bots; + break; } + BOTLIB_SpawnBot(TEAM2, INVALID, NULL, NULL); + bot_connections.desire_bots++; + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } - if (bot_connections.desire_team3 > bot_connections.team3_bots) + } + if (bot_connections.desire_team3 > bot_connections.team3_bots) + { + while (bot_connections.team3_bots != bot_connections.desire_team3) { - while (bot_connections.team3_bots != bot_connections.desire_team3) + // If adding bots goes over maxclients, limit what can be added. + if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) { - // If adding bots goes over maxclients, limit what can be added. - if (bot_connections.total_bots + bot_connections.total_humans + 1 > maxclients->value) - { - bot_connections.desire_team3 = bot_connections.team3_bots; - break; - } - BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); - BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats + bot_connections.desire_team3 = bot_connections.team3_bots; + break; } + BOTLIB_SpawnBot(TEAM3, INVALID, NULL, NULL); + bot_connections.desire_bots++; + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } } - // ======================================================================================================== +} +void BOTLIB_TPBotCountManager(void) +{ + int bots_to_spawn = BOTLIB_TPBotTeamScaling(); + BOTLIB_TeamBotShuffle(); + BOTLIB_BotCountManager(bots_to_spawn); +} +void BOTLIB_DMBotCountManager(void) +{ // Manage bot counts in DM mode int bots_to_spawn = 0; - if ((gameSettings & GS_DEATHMATCH)) { - if (bot_playercount->value > 0) { - // Calculate the desired number of bots based on bot_playercount and total_humans - bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; - - // Ensure desire_bots does not exceed maxclients - total_humans - if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { - bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); - } + if (bot_playercount->value > 0 && (gameSettings & GS_DEATHMATCH)) { + // Calculate the desired number of bots based on bot_playercount and total_humans + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; - // Sanity check - safety limits - if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; - int bots_adj = 0; - int total_players = bot_connections.total_bots + bot_connections.total_humans; - - if (total_players > bot_playercount->value) { - //gi.dprintf("I should remove a bot\n"); - bots_adj = -1; - } else if (total_players < bot_playercount->value) { - // gi.dprintf("I should add a bot\n"); - // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); - bots_adj = 1; - } else { - bots_to_spawn = (int)bot_playercount->value; - bot_connections.desire_bots = bots_to_spawn; - } + // Ensure desire_bots does not exceed maxclients - total_humans + if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { + bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); + } - bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed - //gi.dprintf("bots_adj: %d\n", bots_to_spawn); + // Sanity check - safety limits + if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + int bots_adj = 0; + int total_players = bot_connections.total_bots + bot_connections.total_humans; + + if (total_players > bot_playercount->value) { + //gi.dprintf("I should remove a bot\n"); + bots_adj = -1; + } else if (total_players < bot_playercount->value) { + // gi.dprintf("I should add a bot\n"); + // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); + bots_adj = 1; } else { - bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + bots_to_spawn = (int)bot_playercount->value; + bot_connections.desire_bots = bots_to_spawn; } - } - // // Auto balance bots - // if (bot_connections.auto_balance_bots == false) - // return; + bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed + //gi.dprintf("bots_adj: %d\n", bots_to_spawn); + } else { + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + } + BOTLIB_BotCountManager(bots_to_spawn); +} +// Bots auto join teams to keep them equal in teamplay. +// Empty servers have a bot join a team upon a player connecting. - //bot_connections.desire_bots += (bot_connections.desire_team1 + bot_connections.desire_team2 + bot_connections.desire_team3); +void BOTLIB_CheckBotRules(void) +{ + if (matchmode->value) // Bots never allowed in matchmode + return; - // Percent chance to add/remove bots every 5 seconds... inc/dec = [min: 1, max: bot_maxteam] - if ((++bot_teamchangefrequency % 5) == 0) + if (ctf->value) { - bot_teamchangefrequency = 0; - - if (bot_maxteam->value) - { - //scale_up - float percent = (float)bot_connections.total_bots / bot_maxteam->value; - if (bot_connections.scale_dn == false && bot_connections.scale_up == false) - bot_connections.scale_up = true; - - float min_percent = 1 / bot_maxteam->value; + BOTLIB_Update_Flags_Status(); + } - // Reached top end of the scale, so work back down - if (bot_connections.scale_up && percent >= 1.0) - { - bot_connections.scale_up = false; - bot_connections.scale_dn = true; - } - // Reached bottom end of the scale, or randomly at 50%, so work back up - else if (bot_connections.scale_dn && (percent <= min_percent || random() < 0.05)) // Never drop below this percent - { - bot_connections.scale_up = true; - bot_connections.scale_dn = false; - } + // check these rules every 1.0 seconds... (aka 10 frames) -- this will be faster if server hz is faster + if ((++bot_teamcheckfrequency % 10) != 0) + { + return; + } + else + { + bot_teamcheckfrequency = 0; + //Com_Printf("%s ltk_teamcheckfrequency\n", __func__); + } - // Decrease bots - if (bot_connections.scale_dn && random() < 0.050) - bot_connections.desire_bots--; + BOTLIB_GetTotalPlayers(&bot_connections); - // Increase bots - if (bot_connections.scale_up && random() < 0.250) - bot_connections.desire_bots++; + // All bots are counted as being on team 1 if in DM mode - // Don't let bots be greater than bot_maxteam - if ((float)bot_connections.desire_bots > bot_maxteam->value) - bot_connections.desire_bots--; + //gi.dprintf("%s hs[%d] ht1[%d] ht2[%d] ht3[%d] bs[%d] bt1[%d] bt2[%d] bt3[%d] tb[%d] th[%d]\n", __func__, bot_connections.spec_humans, bot_connections.team1_humans, bot_connections.team2_humans, bot_connections.team3_humans, bot_connections.spec_bots, bot_connections.team1_bots, bot_connections.team2_bots, bot_connections.team3_bots, bot_connections.total_bots, bot_connections.total_humans); - // Never go below 1 bot when bot_maxteam is active - if (bot_connections.desire_bots < 1) - bot_connections.desire_bots = 1; - } + if (bot_connections.tried_adding_prev_bots == false) + { + bot_connections.tried_adding_prev_bots = true; // Only called once per map + BOTLIB_AddBotsFromPreviousMap(100); // Try get bots from a previous map + BOTLIB_GetTotalPlayers(&bot_connections); // Update connection stats } - // Sanity check - safety limits - //if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; - - //int bots_to_spawn = abs(bot_connections.total_bots - bot_connections.desire_bots); + // Turn on team balance if bot_maxteam is used + if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; - // Shuffle bots around - if (teamplay->value && bot_connections.auto_balance_bots) - BOTLIB_TeamBotShuffle(); + if (teamplay->value && !bot_connections.auto_balance_bots) { + BOTLIB_TPBotCountManual(); + } else if (teamplay->value && bot_connections.auto_balance_bots) { + BOTLIB_TPBotCountManager(); + } else { + BOTLIB_DMBotCountManager(); + } // Remove ALL bots if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) @@ -2148,8 +2191,5 @@ void BOTLIB_CheckBotRules(void) bot_connections.total_team2 = 0; bot_connections.total_team3 = 0; } - - // Time to add or remove bots - BOTLIB_BotCountManager(bots_to_spawn); } //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 1d8022a64..707a714c2 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -2,10 +2,28 @@ #include "../acesrc/acebot.h" #include "botlib.h" +#if defined(_WIN32) || defined(_WIN64) +#include +#else +#include +#endif + /* This file is for common utilties that are used by the botlib functions */ +void seed_random_number_generator(void) { +#if _MSC_VER >= 1920 && !__INTEL_COMPILER + LARGE_INTEGER li; + QueryPerformanceCounter(&li); + srand((unsigned int)(li.QuadPart)); +#else + struct timeval tv; + gettimeofday(&tv, NULL); + srand(tv.tv_usec * tv.tv_sec); +#endif +} + void BOTLIB_SKILL_Init(edict_t* bot) { // Initialize random skill levels @@ -50,7 +68,7 @@ qboolean BOTLIB_SkillChance(float skill_level) float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill) { // Seed the random number generator - srand(time(NULL)); + seed_random_number_generator(); // Normalize the skill level to be between 0.0 and 1.0 if (skill_level < -1.0) diff --git a/src/action/cgf_sfx_glass.c b/src/action/cgf_sfx_glass.c index 360fdb57d..79475fd45 100644 --- a/src/action/cgf_sfx_glass.c +++ b/src/action/cgf_sfx_glass.c @@ -45,11 +45,11 @@ extern "C" #endif -// cvar for breaking glass -static cvar_t *breakableglass = 0; +// // cvar for breaking glass +// static cvar_t *breakableglass = 0; -// cvar for max glass fragment count -static cvar_t *glassfragmentlimit = 0; +// // cvar for max glass fragment count +// static cvar_t *glassfragmentlimit = 0; static int glassfragmentcount = 0; @@ -257,24 +257,52 @@ CGF_SFX_ShootBreakableGlass (edict_t * aGlassPane, edict_t * anAttacker, int destruct; // depending on mod, destroy window or emit fragments - switch (mod) - { - // break for ap, shotgun, handcannon, and kick, destory window - case MOD_M3: - case MOD_HC: - case MOD_SNIPER: - case MOD_KICK: - case MOD_GRENADE: - case MOD_G_SPLASH: - case MOD_HANDGRENADE: - case MOD_HG_SPLASH: - case MOD_KNIFE: // slash damage - destruct = true; - break; - default: - destruct = (rand () % 3 == 0); - break; - }; + + // Classic behavior + if (breakableglass->value == 1) { + switch (mod) + { + // break for ap, shotgun, handcannon, and kick, destory window + case MOD_M3: + case MOD_HC: + case MOD_SNIPER: + case MOD_KICK: + case MOD_GRENADE: + case MOD_G_SPLASH: + case MOD_HANDGRENADE: + case MOD_HG_SPLASH: + case MOD_KNIFE: // slash damage + destruct = true; + break; + default: + destruct = (rand () % 3 == 0); + break; + }; + } else if (breakableglass->value == 2) { + switch (mod) + { // break for all weapons + case MOD_MK23: + case MOD_DUAL: + case MOD_MP5: + case MOD_M4: + case MOD_M3: + case MOD_HC: + case MOD_SNIPER: + case MOD_KICK: + case MOD_GRENADE: + case MOD_G_SPLASH: + case MOD_HANDGRENADE: + case MOD_HG_SPLASH: + case MOD_KNIFE: // slash damage + destruct = true; + break; + default: + destruct = (rand () % 3 == 0); + break; + }; + } else { + destruct = false; + } if (destruct) { diff --git a/src/action/g_local.h b/src/action/g_local.h index 1a7d12d42..b0b7add6b 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1316,6 +1316,8 @@ extern cvar_t *warmup_unready; extern cvar_t *training_mode; // Sets training mode vars extern cvar_t *g_highscores_dir; // Sets the highscores directory extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA +extern cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) +extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit #if AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index 2a7548406..b70af0356 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -558,6 +558,8 @@ cvar_t *warmup_unready; // Toggles warmup if captains unready cvar_t *training_mode; // Sets training mode vars cvar_t *g_highscores_dir; // Sets the highscores directory cvar_t *lca_grenade; // Allows grenade pin pulling during LCA +cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) +cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit #if AQTION_EXTENSION cvar_t *use_newirvision; diff --git a/src/action/g_save.c b/src/action/g_save.c index c99724ba3..614b0e11e 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -536,7 +536,10 @@ void InitGame( void ) use_classic = gi.cvar( "use_classic", "0", 0 ); // Reset Grenade Strength to 1.52 use_gren_bonk = gi.cvar( "use_gren_bonk", "0", 0 ); // Grenade Bonk - CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) + //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) + breakableglass = gi.cvar("breakableglass", "0", 0); + glassfragmentlimit = gi.cvar("glassfragmentlimit", "30", 0); + //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) g_select_empty = gi.cvar( "g_select_empty", "0", CVAR_ARCHIVE ); g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 72cda8345..f5018aa05 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -582,11 +582,6 @@ typedef struct { void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); void (*tex_coord_pointer)(const GLfloat *ptr); - void (*vertex_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*light_coord_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*color_byte_pointer)(GLint size, GLsizei stride, const GLubyte *pointer); - void (*color_float_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); } glbackend_t; @@ -678,12 +673,6 @@ typedef enum { SHOWTRIS_FX = BIT(3), } showtris_t; -#define GL_VertexPointer gl_backend->vertex_pointer -#define GL_TexCoordPointer gl_backend->tex_coord_pointer -#define GL_LightCoordPointer gl_backend->light_coord_pointer -#define GL_ColorBytePointer gl_backend->color_byte_pointer -#define GL_ColorFloatPointer gl_backend->color_float_pointer - void GL_ForceTexture(glTmu_t tmu, GLuint texnum); void GL_BindTexture(glTmu_t tmu, GLuint texnum); void GL_CommonStateBits(glStateBits_t bits); diff --git a/src/refresh/main.c b/src/refresh/main.c index 1a349757a..a01b9ff21 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -422,8 +422,10 @@ static void GL_DrawNullModel(void) GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte *)colors); - GL_VertexPointer(3, 0, &points[0][0]); + // GL_ColorBytePointer(4, 0, (GLubyte *)colors); + // GL_VertexPointer(3, 0, &points[0][0]); + + GL_LockArrays(6); //rekkie -- allow NullModels to be seen behind walls -- s //if (gl_showtris->integer && gl_showtris->integer < 0) @@ -436,6 +438,8 @@ static void GL_DrawNullModel(void) //if (gl_showtris->integer && gl_showtris->integer < 0) GL_DepthRange(0, 1); // Set the depth buffer back to normal (NullModels are now obscured) //rekkie -- allow NullModels to be seen behind walls -- e + + GL_UnlockArrays(); } //rekkie -- gl_showedges -- e @@ -461,9 +465,9 @@ void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, con //GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); //GL_VertexPointer(3, 0, &verts[0][0]); - GL_VertexPointer(3, 0, &v[0][0]); + //GL_VertexPointer(3, 0, &v[0][0]); glLineWidth(line_width); // Set the line width - GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + //GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line if (occluded) GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) else @@ -502,8 +506,8 @@ void GL_DrawCross(vec3_t origin, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)colors); - GL_VertexPointer(3, 0, &points[0][0]); + //GL_ColorBytePointer(4, 0, (GLubyte*)colors); + //GL_VertexPointer(3, 0, &points[0][0]); glLineWidth(1); // Set the line width if (occluded) @@ -775,8 +779,8 @@ void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolea GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)colors); - GL_VertexPointer(3, 0, &points[0][0]); + //GL_ColorBytePointer(4, 0, (GLubyte*)colors); + //GL_VertexPointer(3, 0, &points[0][0]); glLineWidth(2); // Set the line width if (occluded) @@ -862,8 +866,8 @@ void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); - GL_VertexPointer(3, 0, &box_points[0][0]); + //GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); + //GL_VertexPointer(3, 0, &box_points[0][0]); glLineWidth(2); // Set the line width if (occluded) @@ -1189,8 +1193,8 @@ void GL_BatchDrawArrows(qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_VertexPointer(3, 0, &arrow_points[0][0]); - GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line + //GL_VertexPointer(3, 0, &arrow_points[0][0]); + //GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line //glLineWidth(line_width); // Set the line width if (occluded) From 4fc7bbc4f8c007937dcd28895577800f7da43c1f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 13 Sep 2024 10:54:21 -0400 Subject: [PATCH 740/974] GHUD team2 weapon update bug fix --- src/action/p_hud.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 65c01a90a..673964ca0 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1087,7 +1087,7 @@ void HUD_SpectatorUpdate(edict_t *clent) if (IS_ALIVE(cl_ent) && cl->curr_weap) Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); - else if (cl->resp.team == TEAM1 && weapNum) + else if (cl->resp.team == TEAM2 && weapNum) Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); else // no weapon, set to mk23 Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); From cbf51112b3450dc29cf4314d0ea2a6f6e8f9df33 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 13 Sep 2024 11:50:23 -0400 Subject: [PATCH 741/974] Re-adding old functionality --- src/refresh/gl.h | 10 ++++++++++ src/refresh/main.c | 25 +++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index f5018aa05..97cbc0690 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -582,11 +582,21 @@ typedef struct { void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); void (*tex_coord_pointer)(const GLfloat *ptr); + void (*vertex_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); + void (*light_coord_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); + void (*color_byte_pointer)(GLint size, GLsizei stride, const GLubyte *pointer); + void (*color_float_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); + void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); } glbackend_t; extern const glbackend_t *gl_backend; +#define GL_ColorBytePointer(size, stride, ptr) \ + gl_backend->color_byte_pointer((size), (stride) * sizeof(GLfloat), (ptr)) +#define GL_VertexPointer(size, stride, ptr) \ + gl_backend->vertex_pointer((size), (stride) * sizeof(GLfloat), (ptr)) + static inline void GL_ActiveTexture(glTmu_t tmu) { if (gls.server_tmu != tmu) { diff --git a/src/refresh/main.c b/src/refresh/main.c index a01b9ff21..c2c92909c 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -422,8 +422,8 @@ static void GL_DrawNullModel(void) GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - // GL_ColorBytePointer(4, 0, (GLubyte *)colors); - // GL_VertexPointer(3, 0, &points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte *)colors); + GL_VertexPointer(3, 0, &points[0][0]); GL_LockArrays(6); @@ -465,9 +465,10 @@ void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, con //GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); //GL_VertexPointer(3, 0, &verts[0][0]); - //GL_VertexPointer(3, 0, &v[0][0]); + GL_VertexPointer(3, 0, &v[0][0]); glLineWidth(line_width); // Set the line width - //GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + + GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line if (occluded) GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) else @@ -506,8 +507,8 @@ void GL_DrawCross(vec3_t origin, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - //GL_ColorBytePointer(4, 0, (GLubyte*)colors); - //GL_VertexPointer(3, 0, &points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); glLineWidth(1); // Set the line width if (occluded) @@ -779,8 +780,8 @@ void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolea GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - //GL_ColorBytePointer(4, 0, (GLubyte*)colors); - //GL_VertexPointer(3, 0, &points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); glLineWidth(2); // Set the line width if (occluded) @@ -866,8 +867,8 @@ void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - //GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); - //GL_VertexPointer(3, 0, &box_points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); + GL_VertexPointer(3, 0, &box_points[0][0]); glLineWidth(2); // Set the line width if (occluded) @@ -1193,8 +1194,8 @@ void GL_BatchDrawArrows(qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - //GL_VertexPointer(3, 0, &arrow_points[0][0]); - //GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line + GL_VertexPointer(3, 0, &arrow_points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line //glLineWidth(line_width); // Set the line width if (occluded) From 549c7e24835525b345ba0a885b93e2f701000a16 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 13 Sep 2024 12:49:33 -0400 Subject: [PATCH 742/974] Re-added old GL functionality for bot navmesh --- src/refresh/gl.h | 14 ++++++-------- src/refresh/legacy.c | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 97cbc0690..d6ce6b7d2 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -581,21 +581,19 @@ typedef struct { void (*array_pointers)(const glVaDesc_t *desc, const GLfloat *ptr); void (*tex_coord_pointer)(const GLfloat *ptr); - +// Re-added to support Rektek bot navmesh rendering void (*vertex_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - void (*light_coord_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); void (*color_byte_pointer)(GLint size, GLsizei stride, const GLubyte *pointer); - void (*color_float_pointer)(GLint size, GLsizei stride, const GLfloat *pointer); - +// End void (*color)(GLfloat r, GLfloat g, GLfloat b, GLfloat a); } glbackend_t; extern const glbackend_t *gl_backend; -#define GL_ColorBytePointer(size, stride, ptr) \ - gl_backend->color_byte_pointer((size), (stride) * sizeof(GLfloat), (ptr)) -#define GL_VertexPointer(size, stride, ptr) \ - gl_backend->vertex_pointer((size), (stride) * sizeof(GLfloat), (ptr)) +// Re-added to support Rektek bot navmesh rendering +#define GL_VertexPointer gl_backend->vertex_pointer +#define GL_ColorBytePointer gl_backend->color_byte_pointer +// End static inline void GL_ActiveTexture(glTmu_t tmu) { diff --git a/src/refresh/legacy.c b/src/refresh/legacy.c index dcec55066..8c0fe0d74 100644 --- a/src/refresh/legacy.c +++ b/src/refresh/legacy.c @@ -233,6 +233,18 @@ static void legacy_shutdown(void) } } +// Re-added to support Rektek bot navmesh rendering +static void legacy_vertex_pointer(GLint size, GLsizei stride, const GLfloat *pointer) +{ + qglVertexPointer(size, GL_FLOAT, sizeof(GLfloat) * stride, pointer); +} + +static void legacy_color_byte_pointer(GLint size, GLsizei stride, const GLubyte *pointer) +{ + qglColorPointer(size, GL_UNSIGNED_BYTE, sizeof(GLfloat) * stride, pointer); +} +// End + const glbackend_t backend_legacy = { .name = "legacy", @@ -248,5 +260,9 @@ const glbackend_t backend_legacy = { .array_pointers = legacy_array_pointers, .tex_coord_pointer = legacy_tex_coord_pointer, +// Re-added to support Rektek bot navmesh rendering + .vertex_pointer = legacy_vertex_pointer, + .color_byte_pointer = legacy_color_byte_pointer, +// End .color = legacy_color, }; From d3269fe3f3aabfa798073ab5cd81ed7e13fa0f4b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 13 Sep 2024 13:22:28 -0400 Subject: [PATCH 743/974] Added MOD_GRENADE_IMPACT to Stats_AddHit --- src/action/tng_stats.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 8eb83cb6a..2db8dc3a2 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -115,7 +115,12 @@ void Stats_AddHit( edict_t *ent, int gun, int hitPart ) return; // Adjusted logic to be inclusive rather than exclusive - if (((unsigned)gun <= MAX_GUNSTAT) || ((unsigned)gun == MOD_KICK) || ((unsigned)gun == MOD_PUNCH)) { + if ( + ((unsigned)gun <= MAX_GUNSTAT) || + ((unsigned)gun == MOD_KICK) || + ((unsigned)gun == MOD_PUNCH) || + ((unsigned)gun == MOD_GRENADE_IMPACT) + ) { if (!teamplay->value || team_round_going || stats_afterround->value) { ent->client->resp.hitsTotal++; From 9d0f781215c48ed3ef526e376cd30f8ee0679b3f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 13 Sep 2024 15:45:02 -0400 Subject: [PATCH 744/974] Cleaned up some warnings, set some funcs to static.. --- src/action/acesrc/acebot_ai.c | 13 ++- src/action/acesrc/acebot_nodes.c | 111 +-------------------- src/action/botlib/botlib_movement.c | 13 ++- src/action/botlib/botlib_nav.c | 2 +- src/action/botlib/botlib_weapons.c | 4 +- src/action/g_spawn.c | 149 ++++++++++++++-------------- src/action/p_client.c | 6 +- src/refresh/main.c | 53 +++++----- 8 files changed, 125 insertions(+), 226 deletions(-) diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 3e6d9a489..71f5f3c96 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -94,11 +94,11 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) { int i = 0; int node = 0; - float weight = 0.f, best_weight = 0.0; - int goal_node = 0; - edict_t* goal_ent = NULL; - float cost = INVALID; - int counter = 0; + //float weight = 0.f, best_weight = 0.0; + //int goal_node = 0; + //edict_t* goal_ent = NULL; + //float cost = INVALID; + //int counter = 0; int max_random_retries = 10; // Clear old Node -> Node movement types @@ -960,7 +960,6 @@ short BOTLIB_AlliesAlive(edict_t* self) short BOTLIB_FindVisibleAllies(edict_t* self) { int i; - edict_t* bestally = NULL; vec3_t eyes, ally_eyes; VectorCopy(self->s.origin, eyes); eyes[2] += self->viewheight; // Get our eye level (standing and crouching) @@ -1227,7 +1226,7 @@ qboolean BOTLIB_FindEnemy(edict_t *self) // 1) Target high priority enemies (flag carriers, briefcase, vip, etc) for (i = 0; i < self->bot.enemies_num; i++) { - if (self->bot.enemies_weight[i] > 300 && INV_AMMO(players[self->bot.enemies[i]], FLAG_T1_NUM) || INV_AMMO(players[self->bot.enemies[i]], FLAG_T2_NUM)) + if ((self->bot.enemies_weight[i] > 300 && INV_AMMO(players[self->bot.enemies[i]], FLAG_T1_NUM)) || INV_AMMO(players[self->bot.enemies[i]], FLAG_T2_NUM)) bestenemy = players[self->bot.enemies[i]]; } diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index 5d2241558..da22ee894 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -2174,7 +2174,7 @@ int BOTLIB_Reachability(int from, int to) // Test if speed is too high qboolean speed_within_spec = false; - float required_speed = VectorLength(velocity); + //float required_speed = VectorLength(velocity); if (target[2] > origin[2]) { //Com_Printf("%s ABOVE speed[%f] jump_height[%f]\n", __func__, required_speed, jump_height); @@ -2213,7 +2213,7 @@ int BOTLIB_Reachability(int from, int to) //distance *= 1.0; // Increasing this increases the jump height // Calculate the jump height to get to the target float jump_height = sqrt(2 * gravity * xyz_distance); - float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point + //float jump_height_headroom = jump_height / 8; // head space required to make the jump from point to point // Move the middle point up to the jump height (maximum parabola height) //end_50[2] += NODE_Z_HEIGHT_PLUS_STEPSIZE + jump_height; @@ -4114,113 +4114,6 @@ void ACEND_ReverseLink( edict_t *self, int from, int to ) } } } -//R - -/* -/////////////////////////////////////////////////////////////////////// -// Add/Update node connections (paths) -/////////////////////////////////////////////////////////////////////// -void ACEND_UpdateNodeEdge(edict_t *self, int from, int to) -{ - int i; - trace_t trace; - vec3_t min,max; - - if(from == INVALID || to == INVALID || from == to) - return; // safety - - // Try to stop bots creating impossible links! - // If it looks higher than a jump... - if( - ( nodes[to].origin[2] > nodes[from].origin[2]+36) - && self->is_bot - ) - { - // If we are coming from a move or jump node - if( (nodes[from].type == NODE_MOVE) || - (nodes[from].type == NODE_JUMP) ) - { - // No if the to node is the same, it's illegal - if( (nodes[to].type == NODE_MOVE) || - (nodes[to].type == NODE_JUMP) ) - { - // Too high - not possible!! - return; - } - } - } - // Do not allow creation of nodes where the falling distance would kill you! - if( (nodes[from].origin[2]) > (nodes[to].origin[2] + 180) ) - return; - - - /* - VectorCopy(self->mins, min); - // If going up -// if( (nodes[from].origin[2]) < (nodes[to].origin[2]) ) - min[2] = 0; // Allow for steps up etc. - VectorCopy(self->maxs, max); - // If going down -// if( (nodes[from].origin[2]) > (nodes[to].origin[2]) ) - max[2] = 0; // door node linking - */ - -/* - VectorCopy( vec3_origin, min); - VectorCopy( vec3_origin, max); - - // Now trace it - more safety stuff! - trace = gi.trace( nodes[from].origin, min, max, nodes[to].origin, self, MASK_SOLID); - - if( trace.fraction < 1.0) - { - // can't do it - if(debug_mode) - debug_printf("Warning: (NoTrace) Failed to Link %d -> %d\n", from, to); - return; - } - // Add the link - path_table[from][to] = to; - - // Checks if the link exists and then may create a new one - RiEvEr - for( i=0; i %d\n", from, to); - break; - } - } - - // Now for the self-referencing part, linear time for each link added - for(i=0;ivalue; // Gravity - float distance = VectorDistanceXY(self->s.origin, self->enemy->s.origin); // XY Distance to enemy - float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference - float knife_speed = 1200; + + // Commenting unused? + //float g = sv_gravity->value; // Gravity + //float distance = VectorDistanceXY(self->s.origin, self->enemy->s.origin); // XY Distance to enemy + //float height_diff = self->enemy->s.origin[2] - self->s.origin[2]; // Height difference + //float knife_speed = 1200; + // Using the distance to my enemy and the gravity of the knife what pitch would I need to set to reach my enemy? // Calculate total flight time diff --git a/src/action/botlib/botlib_nav.c b/src/action/botlib/botlib_nav.c index d063b252c..4de0dd80b 100644 --- a/src/action/botlib/botlib_nav.c +++ b/src/action/botlib/botlib_nav.c @@ -2314,7 +2314,7 @@ qboolean BOTLIB_DijkstraPath(edict_t* ent, int from, int to, qboolean path_rando qboolean AntFindPath(edict_t* ent, int from, int to) { int counter = 0; // Link array counter - int link_counter = 0; // Link counter + //int link_counter = 0; // Link counter int newNode = INVALID; // Stores the node being tested int atNode; // Structures for search botlib_sll_t openList; // Locally declared OPEN list diff --git a/src/action/botlib/botlib_weapons.c b/src/action/botlib/botlib_weapons.c index 2d25d1925..f29b6e1d1 100644 --- a/src/action/botlib/botlib_weapons.c +++ b/src/action/botlib/botlib_weapons.c @@ -1107,12 +1107,12 @@ int BOTLIB_GetEquipment(edict_t* self) { int primary_weapon = 0; int secondary_weapon = 0; - qboolean need_weapon = BOTLIB_Need_Weapons(self, &primary_weapon, &secondary_weapon); + //qboolean need_weapon = BOTLIB_Need_Weapons(self, &primary_weapon, &secondary_weapon); int ammo_type = BOTLIB_Need_Ammo(self, &primary_weapon, &secondary_weapon); int special = BOTLIB_Need_Special(self, &primary_weapon, &secondary_weapon); qboolean need_grenades = BOTLIB_Need_Grenades(self); qboolean need_dual_mk23 = BOTLIB_Need_Dual_MK23(self); - qboolean need_knives = BOTLIB_Need_Knives(self); + //qboolean need_knives = BOTLIB_Need_Knives(self); //Com_Printf("%s wep[%d][%d][%d] gren[%d] knife[%d] dual[%d] ammo[%d] special[%d]\n", __func__, need_weapon, primary_weapon, secondary_weapon, need_grenades, need_knives, need_dual_mk23, ammo_type, special); int items_to_get[32]; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index fced5a9e9..4b4f58b35 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -415,80 +415,81 @@ static const spawn_field_t temp_fields[] = { {NULL} }; -static const spawn_t spawns[] = { - {"item_health", SP_item_health}, - {"item_health_small", SP_item_health_small}, - {"item_health_large", SP_item_health_large}, - {"item_health_mega", SP_item_health_mega}, - {"info_player_start", SP_info_player_start}, - {"info_player_deathmatch", SP_info_player_deathmatch}, - {"info_player_intermission", SP_info_player_intermission}, - {"info_player_team1", SP_info_player_team1}, - {"info_player_team2", SP_info_player_team2}, - {"func_plat", SP_func_plat}, - {"func_button", SP_func_button}, - {"func_door", SP_func_door}, - {"func_door_secret", SP_func_door_secret}, - {"func_door_rotating", SP_func_door_rotating}, - {"func_rotating", SP_func_rotating}, - {"func_train", SP_func_train}, - {"func_water", SP_func_water}, - {"func_conveyor", SP_func_conveyor}, - {"func_areaportal", SP_func_areaportal}, - {"func_clock", SP_func_clock}, - {"func_wall", SP_func_wall}, - {"func_object", SP_func_object}, - {"func_timer", SP_func_timer}, - {"func_explosive", SP_func_explosive}, - {"func_killbox", SP_func_killbox}, - {"trigger_always", SP_trigger_always}, - {"trigger_once", SP_trigger_once}, - {"trigger_multiple", SP_trigger_multiple}, - {"trigger_relay", SP_trigger_relay}, - {"trigger_push", SP_trigger_push}, - {"trigger_hurt", SP_trigger_hurt}, - {"trigger_key", SP_trigger_key}, - {"trigger_counter", SP_trigger_counter}, - {"trigger_elevator", SP_trigger_elevator}, - {"trigger_gravity", SP_trigger_gravity}, - {"trigger_monsterjump", SP_trigger_monsterjump}, - {"target_temp_entity", SP_target_temp_entity}, - {"target_speaker", SP_target_speaker}, - {"target_explosion", SP_target_explosion}, - {"target_changelevel", SP_target_changelevel}, - {"target_splash", SP_target_splash}, - {"target_spawner", SP_target_spawner}, - {"target_blaster", SP_target_blaster}, - {"target_crosslevel_trigger", SP_target_crosslevel_trigger}, - {"target_crosslevel_target", SP_target_crosslevel_target}, - {"target_laser", SP_target_laser}, - {"target_earthquake", SP_target_earthquake}, - {"target_character", SP_target_character}, - {"target_string", SP_target_string}, - {"worldspawn", SP_worldspawn}, - {"viewthing", SP_viewthing}, - {"light_mine1", SP_light_mine1}, - {"light_mine2", SP_light_mine2}, - {"info_null", SP_info_null}, - {"func_group", SP_info_null}, - {"info_notnull", SP_info_notnull}, - {"path_corner", SP_path_corner}, - {"misc_banner", SP_misc_banner}, - {"misc_ctf_banner", SP_misc_ctf_banner}, - {"misc_ctf_small_banner", SP_misc_ctf_small_banner}, - {"misc_satellite_dish", SP_misc_satellite_dish}, - {"misc_viper", SP_misc_viper}, - {"misc_viper_bomb", SP_misc_viper_bomb}, - {"misc_bigviper", SP_misc_bigviper}, - {"misc_strogg_ship", SP_misc_strogg_ship}, - {"misc_teleporter", SP_misc_teleporter}, - {"misc_teleporter_dest", SP_misc_teleporter_dest}, - {"trigger_teleport", SP_trigger_teleport}, - {"info_teleport_destination", SP_info_teleport_destination}, - {"misc_blackhole", SP_misc_blackhole}, - - {NULL, NULL} -}; +// Unused legacy spawns +// static const spawn_t spawns[] = { +// {"item_health", SP_item_health}, +// {"item_health_small", SP_item_health_small}, +// {"item_health_large", SP_item_health_large}, +// {"item_health_mega", SP_item_health_mega}, +// {"info_player_start", SP_info_player_start}, +// {"info_player_deathmatch", SP_info_player_deathmatch}, +// {"info_player_intermission", SP_info_player_intermission}, +// {"info_player_team1", SP_info_player_team1}, +// {"info_player_team2", SP_info_player_team2}, +// {"func_plat", SP_func_plat}, +// {"func_button", SP_func_button}, +// {"func_door", SP_func_door}, +// {"func_door_secret", SP_func_door_secret}, +// {"func_door_rotating", SP_func_door_rotating}, +// {"func_rotating", SP_func_rotating}, +// {"func_train", SP_func_train}, +// {"func_water", SP_func_water}, +// {"func_conveyor", SP_func_conveyor}, +// {"func_areaportal", SP_func_areaportal}, +// {"func_clock", SP_func_clock}, +// {"func_wall", SP_func_wall}, +// {"func_object", SP_func_object}, +// {"func_timer", SP_func_timer}, +// {"func_explosive", SP_func_explosive}, +// {"func_killbox", SP_func_killbox}, +// {"trigger_always", SP_trigger_always}, +// {"trigger_once", SP_trigger_once}, +// {"trigger_multiple", SP_trigger_multiple}, +// {"trigger_relay", SP_trigger_relay}, +// {"trigger_push", SP_trigger_push}, +// {"trigger_hurt", SP_trigger_hurt}, +// {"trigger_key", SP_trigger_key}, +// {"trigger_counter", SP_trigger_counter}, +// {"trigger_elevator", SP_trigger_elevator}, +// {"trigger_gravity", SP_trigger_gravity}, +// {"trigger_monsterjump", SP_trigger_monsterjump}, +// {"target_temp_entity", SP_target_temp_entity}, +// {"target_speaker", SP_target_speaker}, +// {"target_explosion", SP_target_explosion}, +// {"target_changelevel", SP_target_changelevel}, +// {"target_splash", SP_target_splash}, +// {"target_spawner", SP_target_spawner}, +// {"target_blaster", SP_target_blaster}, +// {"target_crosslevel_trigger", SP_target_crosslevel_trigger}, +// {"target_crosslevel_target", SP_target_crosslevel_target}, +// {"target_laser", SP_target_laser}, +// {"target_earthquake", SP_target_earthquake}, +// {"target_character", SP_target_character}, +// {"target_string", SP_target_string}, +// {"worldspawn", SP_worldspawn}, +// {"viewthing", SP_viewthing}, +// {"light_mine1", SP_light_mine1}, +// {"light_mine2", SP_light_mine2}, +// {"info_null", SP_info_null}, +// {"func_group", SP_info_null}, +// {"info_notnull", SP_info_notnull}, +// {"path_corner", SP_path_corner}, +// {"misc_banner", SP_misc_banner}, +// {"misc_ctf_banner", SP_misc_ctf_banner}, +// {"misc_ctf_small_banner", SP_misc_ctf_small_banner}, +// {"misc_satellite_dish", SP_misc_satellite_dish}, +// {"misc_viper", SP_misc_viper}, +// {"misc_viper_bomb", SP_misc_viper_bomb}, +// {"misc_bigviper", SP_misc_bigviper}, +// {"misc_strogg_ship", SP_misc_strogg_ship}, +// {"misc_teleporter", SP_misc_teleporter}, +// {"misc_teleporter_dest", SP_misc_teleporter_dest}, +// {"trigger_teleport", SP_trigger_teleport}, +// {"info_teleport_destination", SP_info_teleport_destination}, +// {"misc_blackhole", SP_misc_blackhole}, + +// {NULL, NULL} +// }; /* =============== diff --git a/src/action/p_client.c b/src/action/p_client.c index 26462e288..143e950ac 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -4903,12 +4903,12 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) int nodes_touched = 0; int node = INVALID; int node_type = INVALID; - float node_to_node_dist = 99999; + //float node_to_node_dist = 99999; int from, to; trace_t tr = gi.trace(ent->s.origin, NULL, NULL, tv(ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] - 25), ent, MASK_PLAYERSOLID); - float height_diff = ent->s.origin[2] - tr.endpos[2]; // Height difference between player and ground - float distance = VectorDistance(ent->bot.walknode.last_ground_loc, tr.endpos); // Distance between ground touches + //float height_diff = ent->s.origin[2] - tr.endpos[2]; // Height difference between player and ground + //float distance = VectorDistance(ent->bot.walknode.last_ground_loc, tr.endpos); // Distance between ground touches float speed = VectorLength(ent->velocity); // Player speed //Com_Printf("%s height_diff %f\n", __func__, height_diff); diff --git a/src/refresh/main.c b/src/refresh/main.c index c2c92909c..52040ff92 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -793,7 +793,7 @@ void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolea } // Batch draw call: render min/max box -void GL_AddDrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) +static void GL_AddDrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolean occluded) { const vec3_t axis[3] = { {1,0,0}, {0,1,0}, {0,0,1} }; @@ -859,7 +859,8 @@ void GL_AddDrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboo } } -void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) + +static void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) { if (drawbox_total) { @@ -1010,7 +1011,7 @@ int drawarrow_total = 0; // Draw arrows total int drawarrow_count = 0; // Draw arrows counter vec3_t* arrow_points = NULL; uint32_t* arrow_colors = NULL; -qboolean ALLOC_Arrows(int num_arrows) +static qboolean ALLOC_Arrows(int num_arrows) { void* if_null_1 = NULL; void* if_null_2 = NULL; @@ -1058,7 +1059,7 @@ qboolean ALLOC_Arrows(int num_arrows) return true; } -void FREE_Arrows(void) +static void FREE_Arrows(void) { if (arrow_points != NULL) { @@ -1077,7 +1078,7 @@ void FREE_Arrows(void) //=========================================================================== // //=========================================================================== -void GL_DrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, qboolean occluded) +static void GL_DrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, qboolean occluded) { const uint32_t colors[2] = { color, color }; vec3_t points[2]; @@ -1186,7 +1187,7 @@ void GL_AddDrawArrow(vec3_t start, vec3_t end, uint32_t color, float line_width, } } } //end of the function GL_DrawArrow -void GL_BatchDrawArrows(qboolean occluded) +static void GL_BatchDrawArrows(qboolean occluded) { if (drawarrow_total) { @@ -1231,24 +1232,26 @@ void DrawCross(int number, vec3_t origin, int time, qboolean occluded) draw_crosses[number].occluded = occluded; } } -static void GL_DrawCrosses(void) -{ - qboolean crosses_in_use = false; // Crosses in use - if (sv.cm.draw && sv.cm.draw->boxes_inuse == true) // Only run when there's at least one or more in use - { - for (int i = 0; i < MAX_DRAW_CROSSES; i++) - { - if (draw_crosses[i].time > 0) - { - crosses_in_use = true; - draw_crosses[i].time--; - GL_DrawCross(draw_crosses[i].origin, draw_crosses[i].occluded); - } - } - } - if (sv.cm.draw && sv.cm.draw->crosses_inuse && crosses_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use - sv.cm.draw->crosses_inuse = false; -} + +// Unused +// static void GL_DrawCrosses(void) +// { +// qboolean crosses_in_use = false; // Crosses in use +// if (sv.cm.draw && sv.cm.draw->boxes_inuse == true) // Only run when there's at least one or more in use +// { +// for (int i = 0; i < MAX_DRAW_CROSSES; i++) +// { +// if (draw_crosses[i].time > 0) +// { +// crosses_in_use = true; +// draw_crosses[i].time--; +// GL_DrawCross(draw_crosses[i].origin, draw_crosses[i].occluded); +// } +// } +// } +// if (sv.cm.draw && sv.cm.draw->crosses_inuse && crosses_in_use == false) // Check if it's possible to reduce debug drawing calls when not in use +// sv.cm.draw->crosses_inuse = false; +// } // GL_DrawBoxes() is called to render the array void DrawBox(int number, vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, int time, qboolean occluded) @@ -1419,7 +1422,7 @@ static void GL_DrawSelection(void) } } -void GL_BOTLIBInitDebugDraw(void) +static void GL_BOTLIBInitDebugDraw(void) { // Malloc nav if (sv.cm.draw == NULL) From e5d1b73e8a3a9702e9bf22f688926d18ee5b77ea Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 14 Sep 2024 09:55:33 -0400 Subject: [PATCH 745/974] Rearranged some AQTION_EXTENSION preprocessors, crosshair adjustments, removal of am_* cvars --- doc/action.md | 13 ------------- meson.build | 7 +++++-- meson_options.txt | 5 +++++ src/action/a_team.c | 3 --- src/action/botlib/botlib_spawn.c | 6 ++++++ src/action/g_cmds.c | 2 +- src/action/g_local.h | 8 +------- src/action/g_main.c | 4 ---- src/action/g_save.c | 7 ------- src/client/parse.c | 13 ++++++++++--- src/client/screen.c | 9 +++++++++ src/client/ui/servers.c | 6 +++--- src/server/entities.c | 4 +++- src/server/user.c | 26 ++++++++++++++++++++++---- 14 files changed, 65 insertions(+), 48 deletions(-) diff --git a/doc/action.md b/doc/action.md index 8f91da761..2523b48ed 100644 --- a/doc/action.md +++ b/doc/action.md @@ -642,19 +642,6 @@ Antilag allows server operator to enable lag-compensation for aiming with hitsca ### Force Spawn items `g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for classic play. -### Attract Mode -`am` - server cvar, this will set the server up in 'attract mode', where bots spawn in but are replaced by humans, and vice versa. This is meant to keep a server 'populated' without being overrun by bots, when humans can take their place. It depends on several other cvars for proper operation. -- `am [0/1/2]` - - 0 is disabled, LTK bots will spawn normally from their configs. This is the default value. - - 1 is enabled, and human replace bots as they join, and bots replace humans as they leave. Depends on am_botcount for this threshold value - - 2 is enabled, and humans will not replace bots until the total number of players (real people + bots) is (maxclients - 2). -- `am_botcount [#]` - - This value is the number of players + bots you want at any given time. Default is 6. -- `am_team [0/1/2/3]` - - This value is which team you want bots to spawn in on. If you only want bots on team 1, set this value to 1. 0 means bots will autojoin teams via the autobalancer. If you try to set this to 3 without use_3teams, they will default to team 1. Default is 0. -- `am_newnames [0/1]` - - This enables new bot names, stemming from the AQ2World team, in case you want to change things up a bit. Default is 1. - ### Zoom Compensation `zoom_comp [0/1]` - server cvar, set to "1" to compensate zoom delay based on ping. Every 80ms ping reduces 1 frame, minimum of 1 frame. Default is "0", do not compensate diff --git a/meson.build b/meson.build index af7026bc6..c7b8ae29c 100644 --- a/meson.build +++ b/meson.build @@ -552,8 +552,6 @@ endif if get_option('aqtion-build') config.set10('USE_AQTION', true) - config.set10('AQTION_EXTENSION', true) - config.set10('AQTION_HUD', true) action_src += ltk_src action_src += botlib_src @@ -563,6 +561,11 @@ if get_option('aqtion-build') endif endif +if get_option('aqtion-extensions') + config.set10('AQTION_EXTENSION', true) + config.set10('AQTION_HUD', true) +endif + if get_option('discord-sdk') #if host_machine.system() == 'linux' if host_machine.cpu_family() == 'aarch64' # I am Linux ARM 64-bit diff --git a/meson_options.txt b/meson_options.txt index 433b28f16..fc987229a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,11 @@ option('aqtion-build', value: true, description: 'Build-specific for AQtion enhancements') +option('aqtion-extensions', + type: 'boolean', + value: true, + description: 'Build-specific for AQtion extensions') + option('anticheat-server', type: 'boolean', value: false, diff --git a/src/action/a_team.c b/src/action/a_team.c index 45c39715f..21342dc04 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2349,7 +2349,6 @@ void RunWarmup (void) #if USE_AQTION if (warmup_bots->value){ gi.cvar_forceset("am", "1"); - gi.cvar_forceset("am_botcount", warmup_bots->string); //attract_mode_bot_check(); } #endif @@ -2877,8 +2876,6 @@ int CheckTeamRules (void) // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ gi.cvar_forceset("am", "0"); - gi.cvar_forceset("am_botcount", "0"); - //attract_mode_bot_check(); ACESP_RemoveBot("all"); CenterPrintAll("All bots removed, good luck and have fun!"); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 885420498..bd9cdf2ea 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2191,5 +2191,11 @@ void BOTLIB_CheckBotRules(void) bot_connections.total_team2 = 0; bot_connections.total_team3 = 0; } + + // Update 'am' cvar to reflect in Server UI list if server has bots or not + if (bot_connections.total_bots > 0) + gi.cvar_forceset(am->string, "1"); + else + gi.cvar_forceset(am->string, "0"); } //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 9770ba741..89be80831 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1695,7 +1695,7 @@ static void Cmd_PrintSettings_f( edict_t * ent ) itmflagsSettings( text, sizeof( text ), (int)itm_flags->value ); length = strlen( text ); - #if USE_AQTION + #if AQTION_EXTENSION Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" "limchasecam %2d tgren %2d antilag_interp %2d\n" diff --git a/src/action/g_local.h b/src/action/g_local.h index b0b7add6b..bc08dd568 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1298,11 +1298,7 @@ extern cvar_t *esp_debug; // Enable or disable debug mode (very spammy) // 2023 extern cvar_t *use_killcounts; // Adjust how kill streaks are counted -extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) -extern cvar_t *am_newnames; // Enable or disable new names for Attract Mode (ltk bots) -extern cvar_t *am_botcount; // Number of bots in Attract Mode -extern cvar_t *am_delay; // Delay between bot spawns after players leave in Attract Mode (not implemented) -extern cvar_t *am_team; // Set which team the bots will join in Attract Mode +extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) (do not set this manually) extern cvar_t *zoom_comp; // Enable or disable zoom compensation extern cvar_t *item_kit_mode; // Enable or disable item kit mode extern cvar_t *gun_dualmk23_enhance; // Enable or disable enhanced dual mk23s (laser + silencer) @@ -1705,14 +1701,12 @@ void StatBotCheck(void); void G_RegisterScore(void); int G_CalcRanks(gclient_t **ranks); void G_LoadScores(void); -#if USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); void LogCapture(edict_t *capturer); void LogMatch(void); void LogAward(edict_t *ent, int award); void LogEndMatchStats(void); -#endif //============================================================================ diff --git a/src/action/g_main.c b/src/action/g_main.c index b70af0356..42a8d30ba 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -541,10 +541,6 @@ cvar_t *g_spawn_items; // 2023 cvar_t *use_killcounts; // Display kill counts in console to clients on frag cvar_t *am; // Attract mode toggle -cvar_t *am_newnames; // Attract mode new names, use new LTK bot names -cvar_t *am_botcount; // Attract mode botcount, how many bots at minimum at all times -cvar_t *am_delay; // Attract mode delay, unused at the moment -cvar_t *am_team; // Attract mode team, which team do you want the bots to join cvar_t *zoom_comp; // Compensates zoom-in frames with ping (high ping = fewer frames) cvar_t *item_kit_mode; // Toggles item kit mode cvar_t *gun_dualmk23_enhance; // Enables laser sight for dual mk23 pistols diff --git a/src/action/g_save.c b/src/action/g_save.c index 614b0e11e..57174eaf5 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -636,13 +636,6 @@ void InitGame( void ) // 2023 use_killcounts = gi.cvar("use_killcounts", "0", 0); am = gi.cvar("am", "0", CVAR_LATCH | CVAR_SERVERINFO); - am_newnames = gi.cvar("am_newnames", "1", CVAR_LATCH); - am_botcount = gi.cvar("am_botcount", "6", CVAR_LATCH | CVAR_SERVERINFO); - if (am_botcount->value < 0){ - gi.cvar_forceset("am_botcount", "0"); - } - am_delay = gi.cvar("am_delay", "30", 0); - am_team = gi.cvar("am_team", "0", 0); zoom_comp = gi.cvar("zoom_comp", "1", 0); item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); diff --git a/src/client/parse.c b/src/client/parse.c index 0fbb44871..dc75e03fb 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -353,19 +353,23 @@ static void CL_ParseFrame(int extrabits) #endif // parse clientNum if (extraflags & EPS_CLIENTNUM) { + #if AQTION_EXTENSION if (cls.protocolVersion < PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT) { frame.clientNum = MSG_ReadByte(); } else { frame.clientNum = MSG_ReadShort(); } + #else + frame.clientNum = MSG_ReadByte(); // Default behavior if AQTION_EXTENSION is not defined + #endif if (!VALIDATE_CLIENTNUM(&cl.csr, frame.clientNum)) { Com_Error(ERR_DROP, "%s: bad clientNum", __func__); } } else if (oldframe) { frame.clientNum = oldframe->clientNum; + } else if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { + MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); } - } else if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { - MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); #if USE_DEBUG if (cl_shownet->integer > 2 && (bits || extraflags)) { Com_LPrintf(PRINT_DEVELOPER, " "); @@ -669,6 +673,7 @@ static void CL_ParseServerData(void) cl.serverstate = i; cinematic = i == ss_pic || i == ss_cinematic; } + #if AQTION_EXTENSION if (cls.protocolVersion >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { i = MSG_ReadWord(); if (i & Q2PRO_PF_STRAFEJUMP_HACK) { @@ -688,6 +693,7 @@ static void CL_ParseServerData(void) cl.csr = cs_remap_new; } } else { + #endif if (MSG_ReadByte()) { Com_DPrintf("Q2PRO strafejump hack enabled\n"); cl.pmp.strafehack = true; @@ -700,8 +706,9 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO waterjump hack enabled\n"); cl.pmp.waterhack = true; } + #if AQTION_EXTENSION } - + #endif cl.esFlags |= MSG_ES_UMASK | MSG_ES_LONGSOLID; if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN) { cl.esFlags |= MSG_ES_BEAMORIGIN; diff --git a/src/client/screen.c b/src/client/screen.c index 761f3e14d..b280ba06d 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -61,6 +61,7 @@ static struct { int hud_width, hud_height; float hud_scale; int lag_draw_scale; + qboolean currently_scoped_in; } scr; static cvar_t *scr_viewsize; @@ -121,6 +122,8 @@ static cvar_t *ch_y; static cvar_t *scr_hit_marker_time; +static cvar_t *ch_hide_while_zoomed; + vrect_t scr_vrect; // position of render window on screen const uint32_t colorTable[8] = { @@ -1415,6 +1418,8 @@ void SCR_Init(void) ch_x = Cvar_Get("ch_x", "0", 0); ch_y = Cvar_Get("ch_y", "0", 0); + ch_hide_while_zoomed = Cvar_Get("ch_hide_while_zoomed", "0", 0); + scr_draw2d = Cvar_Get("scr_draw2d", "2", 0); scr_showturtle = Cvar_Get("scr_showturtle", "1", 0); scr_lag_x = Cvar_Get("scr_lag_x", "-1", 0); @@ -1807,6 +1812,7 @@ static void SCR_ExecuteLayoutString(const char *s) qhandle_t pic = cl.image_precache[value]; // hack for action mod scope scaling if (Com_WildCmp("scope?x", token) || Com_WildCmp("scopes/*/scope?x", token)) { + scr.currently_scoped_in = true; int x = scr.hud_x + (scr.hud_width - scr.scope_width) / 2; int y = scr.hud_y + (scr.hud_height - scr.scope_height) / 2; @@ -1816,6 +1822,7 @@ static void SCR_ExecuteLayoutString(const char *s) y + ch_y->integer, w, h, pic); } else { + scr.currently_scoped_in = false; R_DrawPic(x, y, pic); } } @@ -2360,6 +2367,8 @@ static void SCR_DrawCrosshair(void) return; if (cl.frame.ps.stats[STAT_LAYOUTS] & (LAYOUTS_HIDE_HUD | LAYOUTS_HIDE_CROSSHAIR)) return; + if (scr.currently_scoped_in && ch_hide_while_zoomed->integer) + return; x = (scr.hud_width - scr.crosshair_width) / 2; y = (scr.hud_height - scr.crosshair_height) / 2; diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index c37215786..4f80b1279 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -269,7 +269,9 @@ void UI_StatusEvent(const serverStatus_t *status) if (hasBotsCheck == NULL || COM_IsWhite(hasBotsCheck) || *hasBotsCheck == '0') { am = "No"; - slot->hasBots = false; + if (slot) { + slot->hasBots = false; + } } else { if (slot) { ambci = atoi(botsCountCheck); @@ -284,8 +286,6 @@ void UI_StatusEvent(const serverStatus_t *status) } else { playerCount = status->numPlayers + slot->numBots; } - slot->hasBots = true; - am = "Yes"; } } #endif diff --git a/src/server/entities.c b/src/server/entities.c index 5a6fb797d..2f9b53132 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -560,7 +560,8 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) MSG_WriteByte(svc_playerinfo); extraflags = MSG_WriteDeltaPlayerstate_Aqtion(oldstate, &frame->ps, psFlags); - if (client->protocol == PROTOCOL_VERSION_AQTION) { + #if AQTION_EXTENSION + if (client->protocol == PROTOCOL_VERSION_AQTION) { // delta encode the clientNum if ((oldframe ? oldframe->clientNum : 0) != frame->clientNum) { extraflags |= EPS_CLIENTNUM; @@ -571,6 +572,7 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) } } } + #endif // save 3 high bits of extraflags *b1 = svc_frame | (((extraflags & 0x70) << 1)); diff --git a/src/server/user.c b/src/server/user.c index 1794c2183..14f07584c 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -430,11 +430,18 @@ void SV_New_f(void) } break; case PROTOCOL_VERSION_AQTION: - MSG_WriteShort(sv_client->version); + MSG_WriteShort(sv_client->version); + + #if AQTION_EXTENSION if (sv.state == ss_cinematic && sv_client->version < PROTOCOL_VERSION_AQTION_CINEMATICS) MSG_WriteByte(ss_pic); else MSG_WriteByte(sv.state); + #else + MSG_WriteByte(sv.state); // Default behavior if AQTION_EXTENSION is not defined + #endif + + #if AQTION_EXTENSION if (sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { MSG_WriteShort(q2pro_protocol_flags()); } else { @@ -442,7 +449,13 @@ void SV_New_f(void) MSG_WriteByte(sv_client->pmp.qwmode); MSG_WriteByte(sv_client->pmp.waterhack); } - break; + #else + MSG_WriteByte(sv_client->pmp.strafehack); // Default behavior if AQTION_EXTENSION is not defined + MSG_WriteByte(sv_client->pmp.qwmode); + MSG_WriteByte(sv_client->pmp.waterhack); + #endif + + break; } SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); @@ -493,8 +506,11 @@ void SV_New_f(void) if (sv_client->netchan.type == NETCHAN_OLD) { write_configstrings(); write_baselines(); - } else if ((sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) || - (sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS)) { + } else if ((sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) + #if AQTION_EXTENSION + || (sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) + #endif + ) { write_configstring_stream(); write_baseline_stream(); } else { @@ -940,6 +956,7 @@ static void SV_PacketdupHack_f(void) #endif #if USE_AQTION +#if AQTION_EXTENSION static void SV_CvarSync_f(void) { if (!sv_client->edict->client) @@ -992,6 +1009,7 @@ static void SV_CvarSync_f(void) } } #endif +#endif static bool match_cvar_val(const char *s, const char *v) { From f9f1679aad819247a4fe366bbf50220d9888f63e Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 14 Sep 2024 10:08:04 -0400 Subject: [PATCH 746/974] Simplified Bots column in server browser UI, automatically set if bots are in --- src/action/botlib/botlib_spawn.c | 12 ++++++------ src/client/ui/servers.c | 21 +-------------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index bd9cdf2ea..a84f4d518 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2184,8 +2184,7 @@ void BOTLIB_CheckBotRules(void) } // Remove ALL bots - if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) - { + if (bot_connections.total_bots > 0 && bot_connections.desire_bots == 0) { BOTLIB_RemoveBot("ALL"); bot_connections.total_team1 = 0; bot_connections.total_team2 = 0; @@ -2193,9 +2192,10 @@ void BOTLIB_CheckBotRules(void) } // Update 'am' cvar to reflect in Server UI list if server has bots or not - if (bot_connections.total_bots > 0) - gi.cvar_forceset(am->string, "1"); - else - gi.cvar_forceset(am->string, "0"); + if (bot_connections.total_bots > 0) { + gi.cvar_forceset(am->name, "1"); + } else { + gi.cvar_forceset(am->name, "0"); + } } //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index 4f80b1279..0421d307f 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -262,31 +262,12 @@ void UI_StatusEvent(const serverStatus_t *status) const char *am = "No"; #if USE_AQTION - size_t ambci; - const char *hasBotsCheck = Info_ValueForKey(status->infostring, "am"); - const char *botsCountCheck = Info_ValueForKey(status->infostring, "am_botcount"); if (hasBotsCheck == NULL || COM_IsWhite(hasBotsCheck) || *hasBotsCheck == '0') { am = "No"; - if (slot) { - slot->hasBots = false; - } } else { - if (slot) { - ambci = atoi(botsCountCheck); - if (ambci < 0) { - ambci = 0; - } - slot->numBots = ambci; - - // Don't count bots if humans equal or outnumber ambci - if (playerCount >= slot->numBots) { - playerCount = status->numPlayers; - } else { - playerCount = status->numPlayers + slot->numBots; - } - } + am = "Yes"; } #endif From ae8f7c852af3c98957d81a761ac82a4dd302486b Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 14 Sep 2024 11:00:30 -0400 Subject: [PATCH 747/974] Fixed broken parts, reverted AQTION_EXTENSIONS exclusions --- meson.build | 7 ++----- meson_options.txt | 5 ----- src/action/g_local.h | 4 +++- src/client/parse.c | 13 +++---------- src/server/entities.c | 4 +--- src/server/user.c | 26 ++++---------------------- 6 files changed, 13 insertions(+), 46 deletions(-) diff --git a/meson.build b/meson.build index c7b8ae29c..af7026bc6 100644 --- a/meson.build +++ b/meson.build @@ -552,6 +552,8 @@ endif if get_option('aqtion-build') config.set10('USE_AQTION', true) + config.set10('AQTION_EXTENSION', true) + config.set10('AQTION_HUD', true) action_src += ltk_src action_src += botlib_src @@ -561,11 +563,6 @@ if get_option('aqtion-build') endif endif -if get_option('aqtion-extensions') - config.set10('AQTION_EXTENSION', true) - config.set10('AQTION_HUD', true) -endif - if get_option('discord-sdk') #if host_machine.system() == 'linux' if host_machine.cpu_family() == 'aarch64' # I am Linux ARM 64-bit diff --git a/meson_options.txt b/meson_options.txt index fc987229a..433b28f16 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,11 +3,6 @@ option('aqtion-build', value: true, description: 'Build-specific for AQtion enhancements') -option('aqtion-extensions', - type: 'boolean', - value: true, - description: 'Build-specific for AQtion extensions') - option('anticheat-server', type: 'boolean', value: false, diff --git a/src/action/g_local.h b/src/action/g_local.h index bc08dd568..b096fec76 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1298,7 +1298,7 @@ extern cvar_t *esp_debug; // Enable or disable debug mode (very spammy) // 2023 extern cvar_t *use_killcounts; // Adjust how kill streaks are counted -extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) (do not set this manually) +extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) extern cvar_t *zoom_comp; // Enable or disable zoom compensation extern cvar_t *item_kit_mode; // Enable or disable item kit mode extern cvar_t *gun_dualmk23_enhance; // Enable or disable enhanced dual mk23s (laser + silencer) @@ -1701,12 +1701,14 @@ void StatBotCheck(void); void G_RegisterScore(void); int G_CalcRanks(gclient_t **ranks); void G_LoadScores(void); +#if USE_AQTION void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); void LogWorldKill(edict_t *self); void LogCapture(edict_t *capturer); void LogMatch(void); void LogAward(edict_t *ent, int award); void LogEndMatchStats(void); +#endif //============================================================================ diff --git a/src/client/parse.c b/src/client/parse.c index dc75e03fb..0fbb44871 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -353,23 +353,19 @@ static void CL_ParseFrame(int extrabits) #endif // parse clientNum if (extraflags & EPS_CLIENTNUM) { - #if AQTION_EXTENSION if (cls.protocolVersion < PROTOCOL_VERSION_AQTION_CLIENTNUM_SHORT) { frame.clientNum = MSG_ReadByte(); } else { frame.clientNum = MSG_ReadShort(); } - #else - frame.clientNum = MSG_ReadByte(); // Default behavior if AQTION_EXTENSION is not defined - #endif if (!VALIDATE_CLIENTNUM(&cl.csr, frame.clientNum)) { Com_Error(ERR_DROP, "%s: bad clientNum", __func__); } } else if (oldframe) { frame.clientNum = oldframe->clientNum; - } else if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { - MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); } + } else if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { + MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); #if USE_DEBUG if (cl_shownet->integer > 2 && (bits || extraflags)) { Com_LPrintf(PRINT_DEVELOPER, " "); @@ -673,7 +669,6 @@ static void CL_ParseServerData(void) cl.serverstate = i; cinematic = i == ss_pic || i == ss_cinematic; } - #if AQTION_EXTENSION if (cls.protocolVersion >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { i = MSG_ReadWord(); if (i & Q2PRO_PF_STRAFEJUMP_HACK) { @@ -693,7 +688,6 @@ static void CL_ParseServerData(void) cl.csr = cs_remap_new; } } else { - #endif if (MSG_ReadByte()) { Com_DPrintf("Q2PRO strafejump hack enabled\n"); cl.pmp.strafehack = true; @@ -706,9 +700,8 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO waterjump hack enabled\n"); cl.pmp.waterhack = true; } - #if AQTION_EXTENSION } - #endif + cl.esFlags |= MSG_ES_UMASK | MSG_ES_LONGSOLID; if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_BEAM_ORIGIN) { cl.esFlags |= MSG_ES_BEAMORIGIN; diff --git a/src/server/entities.c b/src/server/entities.c index 2f9b53132..5a6fb797d 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -560,8 +560,7 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) MSG_WriteByte(svc_playerinfo); extraflags = MSG_WriteDeltaPlayerstate_Aqtion(oldstate, &frame->ps, psFlags); - #if AQTION_EXTENSION - if (client->protocol == PROTOCOL_VERSION_AQTION) { + if (client->protocol == PROTOCOL_VERSION_AQTION) { // delta encode the clientNum if ((oldframe ? oldframe->clientNum : 0) != frame->clientNum) { extraflags |= EPS_CLIENTNUM; @@ -572,7 +571,6 @@ bool SV_WriteFrameToClient_Aqtion(client_t *client, unsigned maxsize) } } } - #endif // save 3 high bits of extraflags *b1 = svc_frame | (((extraflags & 0x70) << 1)); diff --git a/src/server/user.c b/src/server/user.c index 14f07584c..1794c2183 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -430,18 +430,11 @@ void SV_New_f(void) } break; case PROTOCOL_VERSION_AQTION: - MSG_WriteShort(sv_client->version); - - #if AQTION_EXTENSION + MSG_WriteShort(sv_client->version); if (sv.state == ss_cinematic && sv_client->version < PROTOCOL_VERSION_AQTION_CINEMATICS) MSG_WriteByte(ss_pic); else MSG_WriteByte(sv.state); - #else - MSG_WriteByte(sv.state); // Default behavior if AQTION_EXTENSION is not defined - #endif - - #if AQTION_EXTENSION if (sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) { MSG_WriteShort(q2pro_protocol_flags()); } else { @@ -449,13 +442,7 @@ void SV_New_f(void) MSG_WriteByte(sv_client->pmp.qwmode); MSG_WriteByte(sv_client->pmp.waterhack); } - #else - MSG_WriteByte(sv_client->pmp.strafehack); // Default behavior if AQTION_EXTENSION is not defined - MSG_WriteByte(sv_client->pmp.qwmode); - MSG_WriteByte(sv_client->pmp.waterhack); - #endif - - break; + break; } SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); @@ -506,11 +493,8 @@ void SV_New_f(void) if (sv_client->netchan.type == NETCHAN_OLD) { write_configstrings(); write_baselines(); - } else if ((sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) - #if AQTION_EXTENSION - || (sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS) - #endif - ) { + } else if ((sv_client->protocol == PROTOCOL_VERSION_Q2PRO && sv_client->version >= PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS) || + (sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_EXTENDED_LIMITS)) { write_configstring_stream(); write_baseline_stream(); } else { @@ -956,7 +940,6 @@ static void SV_PacketdupHack_f(void) #endif #if USE_AQTION -#if AQTION_EXTENSION static void SV_CvarSync_f(void) { if (!sv_client->edict->client) @@ -1009,7 +992,6 @@ static void SV_CvarSync_f(void) } } #endif -#endif static bool match_cvar_val(const char *s, const char *v) { From e7d021b71fa5cc582d3208a45f0a079849b38532 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 14 Sep 2024 12:00:21 -0400 Subject: [PATCH 748/974] Half scale crosshairs --- src/client/screen.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index b280ba06d..4bbadd6a3 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1201,10 +1201,11 @@ static void ch_scale_changed(cvar_t *self) float scale; qhandle_t scope_pic; - scale = Cvar_ClampValue(self, 0.1f, 9.0f); + // Cut the scale in half to match the original AQ2 behavior + scale = Cvar_ClampValue(self, 0.1f, 9.0f) / 2.0f; // action mod scope scaling - scope_pic = R_RegisterPic("scope2x");; + scope_pic = R_RegisterPic("scope2x"); if (scope_pic) { R_GetPicSize(&w, &h, scope_pic); scr.scope_width = w * scale; From fe2b28f87fd74ef25d45a12eacd5b17a504c57a0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Sun, 15 Sep 2024 15:54:13 -0400 Subject: [PATCH 749/974] Update screen.c Make crosshairs even smaller? --- src/client/screen.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 4bbadd6a3..9cd7f37e0 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1201,8 +1201,8 @@ static void ch_scale_changed(cvar_t *self) float scale; qhandle_t scope_pic; - // Cut the scale in half to match the original AQ2 behavior - scale = Cvar_ClampValue(self, 0.1f, 9.0f) / 2.0f; + // Cut the scale by 1/4 to match the original AQ2 behavior? + scale = Cvar_ClampValue(self, 0.1f, 9.0f) / 4.0f; // action mod scope scaling scope_pic = R_RegisterPic("scope2x"); From b17607f61536de117c61717ef5b1a1754d693da5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Sun, 15 Sep 2024 16:20:25 -0400 Subject: [PATCH 750/974] Update screen.c Reverted back to half scale --- src/client/screen.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 9cd7f37e0..26bfd5895 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1201,8 +1201,8 @@ static void ch_scale_changed(cvar_t *self) float scale; qhandle_t scope_pic; - // Cut the scale by 1/4 to match the original AQ2 behavior? - scale = Cvar_ClampValue(self, 0.1f, 9.0f) / 4.0f; + // Cut the scale by half to try to match the original AQ2 behavior? + scale = Cvar_ClampValue(self, 0.1f, 9.0f) / 2.0f; // action mod scope scaling scope_pic = R_RegisterPic("scope2x"); From 5bebe7fa9771c6ef5c9d1cad61f2ba1c13148f60 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 16 Sep 2024 08:00:03 -0400 Subject: [PATCH 751/974] Added code for hit markers, some debug statements --- src/action/g_combat.c | 6 ++++++ src/action/g_local.h | 3 +++ src/action/p_view.c | 10 ++++++++++ src/client/tent.c | 6 +++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index ce179199b..04fcb0bea 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -870,6 +870,12 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve attacker->client->resp.gunstats[mod].damage += damage; } } + + if (targ != attacker && attacker->client && targ->health > 0 && + !(targ->svflags % SVF_DEADMONSTER) && !(targ->flags & FL_NO_DAMAGE_EFFECTS) && + mod != MOD_TARGET_LASER) { + attacker->client->damage_dealt += take + psave + asave; + } client->attacker = attacker; client->attacker_mod = mod; diff --git a/src/action/g_local.h b/src/action/g_local.h index b096fec76..32a3e49f6 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -355,6 +355,8 @@ typedef struct gclient_s gclient_t; #define FL_TEAMSLAVE BIT(10) // not the first on the team #define FL_NO_KNOCKBACK BIT(11) #define FL_POWER_ARMOR BIT(12) // power armor (if any) is active + +#define FL_NO_DAMAGE_EFFECTS BIT(20) // no damage effects #define FL_ACCELERATE BIT(29) // accelerative movement #define FL_RESPAWN BIT(31) // used for item respawning @@ -1984,6 +1986,7 @@ struct gclient_s int damage_blood; // damage taken out of health int damage_knockback; // impact damage vec3_t damage_from; // origin for vector calculation + int damage_dealt; // total damage dealt to other players (used for hit markers) float killer_yaw; // when dead, look at killer diff --git a/src/action/p_view.c b/src/action/p_view.c index af0ad6c75..5ce030af2 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1693,4 +1693,14 @@ void ClientEndServerFrame (edict_t * ent) */ RadioThink(ent); + + // Paril's hit markers + if (ent->client->damage_dealt > 0) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_DAMAGE_DEALT); + gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); + gi.unicast(ent, false); + ent->client->damage_dealt = 0; + } } diff --git a/src/client/tent.c b/src/client/tent.c index 4c6790097..30f3afcd9 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1664,14 +1664,18 @@ void CL_ParseTEnt(void) if (te.count > 0 && cl_hit_markers->integer > 0) { cl.hit_marker_time = cls.realtime; cl.hit_marker_count = te.count; - if (cl_hit_markers->integer > 1) + if (cl_hit_markers->integer > 1) { S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + Com_Printf("Hit marker\n"); + } } break; default: Com_Error(ERR_DROP, "%s: bad type", __func__); } + Com_Printf("Te count: %d\n", te.count); + Com_Printf("cl_hit_markers: %d\n", cl_hit_markers); } /* From 71bbcd5d00929e7bc8c4712375a70871979eff91 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 14:53:26 -0400 Subject: [PATCH 752/974] Removed debug statements --- src/client/tent.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/tent.c b/src/client/tent.c index 30f3afcd9..7d7735a02 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1666,7 +1666,6 @@ void CL_ParseTEnt(void) cl.hit_marker_count = te.count; if (cl_hit_markers->integer > 1) { S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); - Com_Printf("Hit marker\n"); } } break; @@ -1674,8 +1673,6 @@ void CL_ParseTEnt(void) default: Com_Error(ERR_DROP, "%s: bad type", __func__); } - Com_Printf("Te count: %d\n", te.count); - Com_Printf("cl_hit_markers: %d\n", cl_hit_markers); } /* From d9e65e68dcaaa97f5d92a61f59d47cecbb5d9203 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 15:20:15 -0400 Subject: [PATCH 753/974] Hit markers work --- src/action/g_combat.c | 4 ++++ src/action/p_view.c | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 04fcb0bea..aaea333fa 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -866,6 +866,8 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve { if (!friendlyFire && !in_warmup) { attacker->client->resp.damage_dealt += damage; + // Hit markers + attacker->client->damage_dealt += damage; if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; } @@ -929,6 +931,8 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve { if (!friendlyFire && !in_warmup) { attacker->client->resp.damage_dealt += damage; + // Hit markers + attacker->client->damage_dealt += damage; // All normal weapon damage if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; diff --git a/src/action/p_view.c b/src/action/p_view.c index 5ce030af2..8807b4c0e 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1694,8 +1694,8 @@ void ClientEndServerFrame (edict_t * ent) RadioThink(ent); - // Paril's hit markers - if (ent->client->damage_dealt > 0) + // Paril's hit markers (don't draw for bots!) + if (ent->client->damage_dealt > 0 && !ent->is_bot) { gi.WriteByte(svc_temp_entity); gi.WriteByte(TE_DAMAGE_DEALT); From 353ca91770f4f2e486a938f178a01d95ee8fe36e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 16:31:08 -0400 Subject: [PATCH 754/974] Expanded aq2 hitsounds --- inc/shared/shared.h | 7 +++++++ src/action/p_view.c | 20 +++++++++++++++++++- src/client/parse.c | 4 ++++ src/client/tent.c | 29 +++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 27efe8ccb..bca54864a 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1436,6 +1436,13 @@ typedef enum { TE_DAMAGE_DEALT = 128, + TE_AQ2_HEADSHOT, + TE_AQ2_HELMSHOT, + TE_AQ2_VESTSHOT, + TE_AQ2_CHESTSHOT, + TE_AQ2_STOMSHOT, + TE_AQ2_LEGSHOT, + TE_NUM_ENTITIES } temp_event_t; diff --git a/src/action/p_view.c b/src/action/p_view.c index 8807b4c0e..ac6a64918 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1698,7 +1698,25 @@ void ClientEndServerFrame (edict_t * ent) if (ent->client->damage_dealt > 0 && !ent->is_bot) { gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_DAMAGE_DEALT); + + switch (ent->client->attacker_loc) + { + case LOC_HDAM: + gi.WriteByte(TE_AQ2_HEADSHOT); + break; + case LOC_CDAM: + gi.WriteByte(TE_AQ2_CHESTSHOT); + break; + case LOC_SDAM: + gi.WriteByte(TE_AQ2_STOMSHOT); + break; + case LOC_LDAM: + gi.WriteByte(TE_AQ2_LEGSHOT); + break; + default: + gi.WriteByte(TE_DAMAGE_DEALT); + break; + } gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); gi.unicast(ent, false); ent->client->damage_dealt = 0; diff --git a/src/client/parse.c b/src/client/parse.c index 0fbb44871..3e156ae0c 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -969,6 +969,10 @@ static void CL_ParseTEntPacket(void) break; case TE_DAMAGE_DEALT: + case TE_AQ2_HEADSHOT: + case TE_AQ2_CHESTSHOT: + case TE_AQ2_STOMSHOT: + case TE_AQ2_LEGSHOT: te.count = MSG_ReadShort(); break; diff --git a/src/client/tent.c b/src/client/tent.c index 7d7735a02..0673162e6 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -38,6 +38,10 @@ qhandle_t cl_sfx_lightning; qhandle_t cl_sfx_disrexp; qhandle_t cl_sfx_hit_marker; +qhandle_t cl_sfx_headshot; +qhandle_t cl_sfx_chestshot; +qhandle_t cl_sfx_stomshot; +qhandle_t cl_sfx_legshot; //qhandle_t cl_mod_explode; qhandle_t cl_mod_smoke; @@ -320,7 +324,11 @@ void CL_RegisterTEntSounds(void) cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); - cl_sfx_hit_marker = S_RegisterSound("weapons/marker.wav"); + cl_sfx_hit_marker = S_RegisterSound("hitsounds/aphelmet.wav"); + cl_sfx_headshot = S_RegisterSound("hitsounds/headshot.wav"); + cl_sfx_chestshot = S_RegisterSound("hitsounds/chest.wav"); + cl_sfx_stomshot = S_RegisterSound("hitsounds/stomach.wav"); + cl_sfx_legshot = S_RegisterSound("hitsounds/leg.wav"); } static const char *const muzzlenames[MFLASH_TOTAL] = { @@ -1661,11 +1669,28 @@ void CL_ParseTEnt(void) break; case TE_DAMAGE_DEALT: + case TE_AQ2_HEADSHOT: + case TE_AQ2_CHESTSHOT: + case TE_AQ2_STOMSHOT: + case TE_AQ2_LEGSHOT: + qhandle_t hit_sfx; if (te.count > 0 && cl_hit_markers->integer > 0) { cl.hit_marker_time = cls.realtime; cl.hit_marker_count = te.count; if (cl_hit_markers->integer > 1) { - S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + if (te.type == TE_AQ2_HEADSHOT) + hit_sfx = cl_sfx_headshot; + else if (te.type == TE_AQ2_CHESTSHOT) + hit_sfx = cl_sfx_chestshot; + else if (te.type == TE_AQ2_STOMSHOT) + hit_sfx = cl_sfx_stomshot; + else if (te.type == TE_AQ2_LEGSHOT) + hit_sfx = cl_sfx_legshot; + else if (te.type == TE_DAMAGE_DEALT) + hit_sfx = cl_sfx_hit_marker; + else + hit_sfx = cl_sfx_hit_marker; + S_StartSound(NULL, listener_entnum, 257, hit_sfx, 1, ATTN_NONE, 0); } } break; From 99a1d9b55b4334c75ae2b58289b67d75e9efcdd5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 16:32:41 -0400 Subject: [PATCH 755/974] Removed vest damage type --- inc/shared/shared.h | 1 - 1 file changed, 1 deletion(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index bca54864a..d441a9507 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1438,7 +1438,6 @@ typedef enum { TE_AQ2_HEADSHOT, TE_AQ2_HELMSHOT, - TE_AQ2_VESTSHOT, TE_AQ2_CHESTSHOT, TE_AQ2_STOMSHOT, TE_AQ2_LEGSHOT, From 15d4ba4f24330153991ac620d5380f1264e08edc Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 16:37:20 -0400 Subject: [PATCH 756/974] Removed extra sound, renamed main hitsound --- inc/shared/shared.h | 1 - src/client/tent.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index d441a9507..861fc2e0c 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1437,7 +1437,6 @@ typedef enum { TE_DAMAGE_DEALT = 128, TE_AQ2_HEADSHOT, - TE_AQ2_HELMSHOT, TE_AQ2_CHESTSHOT, TE_AQ2_STOMSHOT, TE_AQ2_LEGSHOT, diff --git a/src/client/tent.c b/src/client/tent.c index 0673162e6..da8112d54 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -324,7 +324,7 @@ void CL_RegisterTEntSounds(void) cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); - cl_sfx_hit_marker = S_RegisterSound("hitsounds/aphelmet.wav"); + cl_sfx_hit_marker = S_RegisterSound("hitsounds/damage.wav"); cl_sfx_headshot = S_RegisterSound("hitsounds/headshot.wav"); cl_sfx_chestshot = S_RegisterSound("hitsounds/chest.wav"); cl_sfx_stomshot = S_RegisterSound("hitsounds/stomach.wav"); From 6a7e67bbbf3662572a01efca3973c49b5c8f0d2e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 16 Sep 2024 16:39:40 -0400 Subject: [PATCH 757/974] mingw didn't like where hit_sfx was --- src/client/tent.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/tent.c b/src/client/tent.c index da8112d54..61f114374 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1673,8 +1673,8 @@ void CL_ParseTEnt(void) case TE_AQ2_CHESTSHOT: case TE_AQ2_STOMSHOT: case TE_AQ2_LEGSHOT: - qhandle_t hit_sfx; if (te.count > 0 && cl_hit_markers->integer > 0) { + qhandle_t hit_sfx; cl.hit_marker_time = cls.realtime; cl.hit_marker_count = te.count; if (cl_hit_markers->integer > 1) { From 47656f88fed5cac9b395992cc0d9a63accd593d8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 12 Sep 2024 01:01:58 +0300 Subject: [PATCH 758/974] Add support for cubemap skies. Support loading cubemap textures for SURF_SKY faces. This allows multiple skybox textures per map. Also rewrite N64 skies to not use skybox drawing code at all. Also fully support skies on bmodels, but only enable them in DECOUPLED_LM maps. --- doc/client.asciidoc | 13 +-- inc/refresh/refresh.h | 1 + src/refresh/gl.h | 49 +++++----- src/refresh/images.c | 33 ++++--- src/refresh/images.h | 5 +- src/refresh/main.c | 6 +- src/refresh/mesh.c | 4 +- src/refresh/models.c | 3 - src/refresh/shader.c | 39 +++++--- src/refresh/sky.c | 115 ++++++++++++++++++++---- src/refresh/state.c | 32 +++++++ src/refresh/surf.c | 46 +++++++--- src/refresh/tess.c | 28 ++++-- src/refresh/texture.c | 203 +++++++++++++++++++++++++++++++++++++++--- src/refresh/world.c | 11 ++- 15 files changed, 481 insertions(+), 107 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 579b45891..120be0ed6 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -674,11 +674,14 @@ gl_downsample_skins:: Default value is 1 (downsampling enabled). gl_drawsky:: - Enables skybox texturing and N64 skies. N64 skies only work if - ‘gl_shaders’ is enabled. Default value is 1. - - 0 — draw solid black sky - - 1 — draw normal sky, or N64 sky if present - - 2 — draw normal sky, ignore N64 sky + Enables skybox texturing. 0 means to draw sky in solid black color. + Default value is 1 (enabled). + +gl_cubemaps:: + Enables use of cubemaps for skybox drawing. Cubemaps allow multiple skybox + textures per map, and help to avoid sky rendering bugs. Also enables N64 + skies (although these aren't technically cubemaps). Only effective if + ‘gl_shaders’ is enabled. Default value is 1 (enabled). gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 8e0be95f1..d2c6617c9 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -155,6 +155,7 @@ typedef enum { IF_NEAREST = BIT(7), // don't bilerp IF_OPAQUE = BIT(8), // known to be opaque IF_DEFAULT_FLARE = BIT(9), // default flare hack + IF_CUBEMAP = BIT(10), // cubemap (or part of it) // not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found diff --git a/src/refresh/gl.h b/src/refresh/gl.h index f5018aa05..e302eb6b2 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -50,7 +50,7 @@ typedef GLuint glIndex_t; #define TAB_SIN(x) gl_static.sintab[(x) & 255] #define TAB_COS(x) gl_static.sintab[((x) + 64) & 255] -#define NUM_AUTO_TEXTURES 7 +#define NUM_AUTO_TEXTURES 9 typedef struct { GLuint query; @@ -63,6 +63,8 @@ typedef struct { typedef struct { bool registering; bool use_shaders; + bool use_cubemaps; + bool use_bmodel_skies; struct { bsp_t *cache; vec_t *vertices; @@ -90,7 +92,6 @@ typedef struct { byte lightstylemap[MAX_LIGHTSTYLES]; hash_map_t *queries; hash_map_t *programs; - image_t *classic_sky; } glStatic_t; typedef struct { @@ -111,6 +112,7 @@ typedef struct { float entscale; vec3_t entaxis[3]; GLfloat entmatrix[16]; + GLfloat skymatrix[2][16]; lightpoint_t lightpoint; int num_beams; int num_flares; @@ -245,7 +247,7 @@ bool GL_AllocBlock(int width, int height, uint16_t *inuse, void GL_MultMatrix(GLfloat *restrict out, const GLfloat *restrict a, const GLfloat *restrict b); void GL_SetEntityAxis(void); void GL_RotationMatrix(GLfloat *matrix); -void GL_RotateForEntity(void); +void GL_RotateForEntity(bool skies); void GL_ClearErrors(void); bool GL_ShowErrors(const char *func); @@ -471,19 +473,21 @@ typedef enum { GLS_INTENSITY_ENABLE = BIT(11), GLS_GLOWMAP_ENABLE = BIT(12), GLS_CLASSIC_SKY = BIT(13), - GLS_DEFAULT_FLARE = BIT(14), + GLS_DEFAULT_SKY = BIT(14), + GLS_DEFAULT_FLARE = BIT(15), - GLS_SHADE_SMOOTH = BIT(15), - GLS_SCROLL_X = BIT(16), - GLS_SCROLL_Y = BIT(17), - GLS_SCROLL_FLIP = BIT(18), - GLS_SCROLL_SLOW = BIT(19), + GLS_SHADE_SMOOTH = BIT(16), + GLS_SCROLL_X = BIT(17), + GLS_SCROLL_Y = BIT(18), + GLS_SCROLL_FLIP = BIT(19), + GLS_SCROLL_SLOW = BIT(20), GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, + GLS_SKY_MASK = GLS_CLASSIC_SKY | GLS_DEFAULT_SKY, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | - GLS_CLASSIC_SKY | GLS_DEFAULT_FLARE, + GLS_SKY_MASK | GLS_DEFAULT_FLARE, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; @@ -529,6 +533,7 @@ typedef enum { typedef struct { GLfloat mvp[16]; + GLfloat msky[2][16]; GLfloat time; GLfloat modulate; GLfloat add; @@ -538,14 +543,13 @@ typedef struct { GLfloat w_amp[2]; GLfloat w_phase[2]; GLfloat scroll[2]; - GLfloat vieworg[3]; - GLfloat pad_2; } glUniformBlock_t; typedef struct { glTmu_t client_tmu; glTmu_t server_tmu; GLuint texnums[MAX_TMUS]; + GLuint texnumcube; glStateBits_t state_bits; glArrayBits_t array_bits; GLuint currentbuffer[2]; @@ -675,6 +679,8 @@ typedef enum { void GL_ForceTexture(glTmu_t tmu, GLuint texnum); void GL_BindTexture(glTmu_t tmu, GLuint texnum); +void GL_ForceCubemap(GLuint texnum); +void GL_BindCubemap(GLuint texnum); void GL_CommonStateBits(glStateBits_t bits); void GL_ScrollPos(vec2_t scroll, glStateBits_t bits); void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); @@ -715,13 +721,15 @@ void GL_Blend(void); */ // auto textures -#define TEXNUM_DEFAULT gl_static.texnums[0] -#define TEXNUM_SCRAP gl_static.texnums[1] -#define TEXNUM_PARTICLE gl_static.texnums[2] -#define TEXNUM_BEAM gl_static.texnums[3] -#define TEXNUM_WHITE gl_static.texnums[4] -#define TEXNUM_BLACK gl_static.texnums[5] -#define TEXNUM_RAW gl_static.texnums[6] +#define TEXNUM_DEFAULT gl_static.texnums[0] +#define TEXNUM_SCRAP gl_static.texnums[1] +#define TEXNUM_PARTICLE gl_static.texnums[2] +#define TEXNUM_BEAM gl_static.texnums[3] +#define TEXNUM_WHITE gl_static.texnums[4] +#define TEXNUM_BLACK gl_static.texnums[5] +#define TEXNUM_RAW gl_static.texnums[6] +#define TEXNUM_CUBEMAP_DEFAULT gl_static.texnums[7] +#define TEXNUM_CUBEMAP_BLACK gl_static.texnums[8] void Scrap_Upload(void); @@ -732,8 +740,6 @@ bool GL_InitWarpTexture(void); extern cvar_t *gl_intensity; -extern image_t shell_texture; - /* * gl_tess.c * @@ -788,6 +794,7 @@ void GL_LightPoint(const vec3_t origin, vec3_t color); void R_AddSkySurface(const mface_t *surf); void R_ClearSkyBox(void); void R_DrawSkyBox(void); +void R_RotateForSky(void); void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis); /* diff --git a/src/refresh/images.c b/src/refresh/images.c index 55483fe65..e6d6f83e7 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1557,7 +1557,7 @@ static void IMG_List_f(void) Com_Printf("------------------\n"); texels = count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; if (mask && !(mask & BIT(image->type))) @@ -1595,7 +1595,7 @@ static image_t *alloc_image(void) image_t *image, *placeholder = NULL; // find a free image_t slot - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) return image; if (!image->upload_width && !image->upload_height && !placeholder) @@ -2030,14 +2030,27 @@ static image_t *find_or_load_image(const char *name, size_t len, image_t *IMG_Find(const char *name, imagetype_t type, imageflags_t flags) { + char buffer[MAX_QPATH]; image_t *image; + size_t len; Q_assert(name); - if ((image = find_or_load_image(name, strlen(name), type, flags))) - return image; + // path MUST never overflow + len = FS_NormalizePathBuffer(buffer, name, sizeof(buffer)); + image = find_or_load_image(buffer, len, type, flags); - return R_NOTEXTURE; + // missing (or invalid) sky texture will use default sky + if (type == IT_SKY) { + if (!image) + return R_SKYTEXTURE; + if (~image->flags & flags & IF_CUBEMAP) + return R_SKYTEXTURE; + } + + if (!image) + return R_NOTEXTURE; + return image; } /* @@ -2125,7 +2138,7 @@ void IMG_FreeUnused(void) image_t *image; int i, count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot if (image->registration_sequence == r_registration_sequence) @@ -2152,7 +2165,7 @@ void IMG_FreeAll(void) image_t *image; int i, count = 0; - for (i = 1, image = r_images + 1; i < r_numImages; i++, image++) { + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot // free it @@ -2169,7 +2182,7 @@ void IMG_FreeAll(void) List_Init(&r_imageHash[i]); // &r_images[0] == R_NOTEXTURE - r_numImages = 1; + r_numImages = R_NUM_AUTO_IMG; } /* @@ -2260,12 +2273,12 @@ void IMG_Init(void) List_Init(&r_imageHash[i]); // &r_images[0] == R_NOTEXTURE - r_numImages = 1; + r_numImages = R_NUM_AUTO_IMG; } void IMG_Shutdown(void) { Cmd_Deregister(img_cmd); - memset(r_images, 0, sizeof(r_images[0])); // clear R_NOTEXTURE + memset(r_images, 0, R_NUM_AUTO_IMG * sizeof(r_images[0])); // clear R_NOTEXTURE r_numImages = 0; } diff --git a/src/refresh/images.h b/src/refresh/images.h index 889258266..c1646b5da 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -76,7 +76,10 @@ extern int r_numImages; extern unsigned r_registration_sequence; -#define R_NOTEXTURE &r_images[0] +#define R_NUM_AUTO_IMG 3 +#define R_NOTEXTURE (&r_images[0]) +#define R_SHELLTEXTURE (&r_images[1]) +#define R_SKYTEXTURE (&r_images[2]) extern uint32_t d_8to24table[256]; diff --git a/src/refresh/main.c b/src/refresh/main.c index e87772a63..0b70faef3 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -311,11 +311,15 @@ void GL_RotationMatrix(GLfloat *matrix) matrix[15] = 1; } -void GL_RotateForEntity(void) +void GL_RotateForEntity(bool skies) { GLfloat matrix[16]; GL_RotationMatrix(matrix); + if (skies) { + GL_MultMatrix(gls.u_block.msky[0], glr.skymatrix[0], matrix); + GL_MultMatrix(gls.u_block.msky[1], glr.skymatrix[1], matrix); + } GL_MultMatrix(glr.entmatrix, glr.viewmatrix, matrix); GL_ForceMatrix(glr.entmatrix); } diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index b4bf25176..64dd23f62 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -570,7 +570,7 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) const entity_t *ent = glr.ent; if (ent->flags & RF_SHELL_MASK) - return &shell_texture; + return R_SHELLTEXTURE; if (ent->skin) return IMG_ForHandle(ent->skin); @@ -880,7 +880,7 @@ void GL_DrawAliasModel(const model_t *model) tess_static_plain : tess_lerped_plain; } - GL_RotateForEntity(); + GL_RotateForEntity(false); GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); diff --git a/src/refresh/models.c b/src/refresh/models.c index ed74864a7..8976fdd34 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -213,7 +213,6 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) Com_WPrintf("%s has bad frame name\n", model->name); dst_frame->image = R_NOTEXTURE; } else { - FS_NormalizePath(buffer); dst_frame->image = IMG_Find(buffer, IT_SPRITE, IF_NONE); } @@ -395,7 +394,6 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) ret = Q_ERR_STRING_TRUNCATED; goto fail; } - FS_NormalizePath(skinname); mesh->skins[i] = IMG_Find(skinname, IT_SKIN, IF_NONE); src_skin += MD2_MAX_SKINNAME; } @@ -546,7 +544,6 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, #endif if (!Q_memccpy(skinname, src_skin->name, 0, sizeof(maliasskinname_t))) return Q_ERR_STRING_TRUNCATED; - FS_NormalizePath(skinname); mesh->skins[i] = IMG_Find(skinname, IT_SKIN, IF_NONE); src_skin++; } diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 24a8169ad..e216d97a1 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -43,6 +43,7 @@ static void write_block(sizebuf_t *buf) GLSF("layout(std140) uniform u_block {\n"); GLSL( mat4 m_vp; + mat4 m_sky[2]; float u_time; float u_modulate; float u_add; @@ -52,8 +53,6 @@ static void write_block(sizebuf_t *buf) vec2 w_amp; vec2 w_phase; vec2 u_scroll; - vec3 u_vieworg; - float pad_2; ) GLSF("};\n"); } @@ -64,7 +63,7 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) write_block(buf); GLSL(in vec4 a_pos;) - if (bits & GLS_CLASSIC_SKY) { + if (bits & GLS_SKY_MASK) { GLSL(out vec3 v_dir;) } else { GLSL(in vec2 a_tc;) @@ -83,8 +82,9 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) GLSF("void main() {\n"); if (bits & GLS_CLASSIC_SKY) { - GLSL(v_dir = a_pos.xyz - u_vieworg.xyz;) - GLSL(v_dir[2] *= 3.0;) + GLSL(v_dir = (m_sky[1] * a_pos).xyz;) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(v_dir = (m_sky[0] * a_pos).xyz;) } else if (bits & GLS_SCROLL_ENABLE) { GLSL(v_tc = a_tc + u_scroll;) } else { @@ -108,20 +108,25 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) if (gl_config.ver_es) GLSL(precision mediump float;) - if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_CLASSIC_SKY)) + if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_SKY_MASK)) write_block(buf); if (bits & GLS_CLASSIC_SKY) { GLSL( uniform sampler2D u_texture1; uniform sampler2D u_texture2; - in vec3 v_dir; ) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(uniform samplerCube u_texture;) } else { GLSL(uniform sampler2D u_texture;) - GLSL(in vec2 v_tc;) } + if (bits & GLS_SKY_MASK) + GLSL(in vec3 v_dir;) + else + GLSL(in vec2 v_tc;) + if (bits & GLS_LIGHTMAP_ENABLE) { GLSL(uniform sampler2D u_lightmap;) GLSL(in vec2 v_lmtc;) @@ -146,6 +151,8 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) vec4 alpha = texture(u_texture2, tc2); vec4 diffuse = vec4((solid.rgb - alpha.rgb * 0.25) * 0.65, 1.0); ) + } else if (bits & GLS_DEFAULT_SKY) { + GLSL(vec4 diffuse = texture(u_texture, v_dir);) } else { GLSL(vec2 tc = v_tc;) @@ -255,7 +262,7 @@ static GLuint create_and_use_program(glStateBits_t bits) qglAttachShader(program, shader_f); qglBindAttribLocation(program, VERT_ATTR_POS, "a_pos"); - if (!(bits & GLS_CLASSIC_SKY)) + if (!(bits & GLS_SKY_MASK)) qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); if (bits & GLS_LIGHTMAP_ENABLE) qglBindAttribLocation(program, VERT_ATTR_LMTC, "a_lmtc"); @@ -413,8 +420,6 @@ static void shader_setup_2d(void) gls.u_block.w_amp[1] = 0.0025f; gls.u_block.w_phase[0] = M_PIf * 10; gls.u_block.w_phase[1] = M_PIf * 10; - - VectorClear(gls.u_block.vieworg); } static void shader_setup_3d(void) @@ -430,7 +435,9 @@ static void shader_setup_3d(void) gls.u_block.w_phase[0] = 4; gls.u_block.w_phase[1] = 4; - VectorCopy(glr.fd.vieworg, gls.u_block.vieworg); + R_RotateForSky(); + + memcpy(gls.u_block.msky, glr.skymatrix, sizeof(glr.skymatrix)); } static void shader_disable_state(void) @@ -444,6 +451,8 @@ static void shader_disable_state(void) qglActiveTexture(GL_TEXTURE0); qglBindTexture(GL_TEXTURE_2D, 0); + qglBindTexture(GL_TEXTURE_CUBE_MAP, 0); + for (int i = 0; i < VERT_ATTR_COUNT; i++) qglDisableVertexAttribArray(i); } @@ -462,6 +471,9 @@ static void shader_init(void) qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.uniform_buffer); qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), NULL, GL_DYNAMIC_DRAW); + + if (gl_config.ver_gl >= QGL_VER(3, 2)) + qglEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } static void shader_shutdown(void) @@ -483,6 +495,9 @@ static void shader_shutdown(void) qglDeleteBuffers(1, &gl_static.uniform_buffer); gl_static.uniform_buffer = 0; } + + if (gl_config.ver_gl >= QGL_VER(3, 2)) + qglDisable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } const glbackend_t backend_shader = { diff --git a/src/refresh/sky.c b/src/refresh/sky.c index f4afe21e3..ef54248ee 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -334,24 +334,15 @@ void R_DrawSkyBox(void) return; // nothing visible GL_BindArrays(VA_SPRITE); - - if (gl_static.classic_sky && gl_drawsky->integer == 1) { - GL_StateBits(GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY); - GL_ArrayBits(GLA_VERTEX); - GL_BindTexture(TMU_TEXTURE, gl_static.classic_sky->texnum); - GL_BindTexture(TMU_LIGHTMAP, gl_static.classic_sky->texnum2); - } else { - GL_StateBits(GLS_TEXTURE_REPLACE); - GL_ArrayBits(GLA_VERTEX | GLA_TC); - } + GL_StateBits(GLS_TEXTURE_REPLACE); + GL_ArrayBits(GLA_VERTEX | GLA_TC); for (i = 0; i < 6; i++) { if (skymins[0][i] >= skymaxs[0][i] || skymins[1][i] >= skymaxs[1][i]) continue; - if (!gl_static.classic_sky || gl_drawsky->integer != 1) - GL_BindTexture(TMU_TEXTURE, sky_images[i]); + GL_BindTexture(TMU_TEXTURE, sky_images[i]); MakeSkyVec(skymaxs[0][i], skymins[1][i], i, tess.vertices); MakeSkyVec(skymins[0][i], skymins[1][i], i, tess.vertices + 5); @@ -364,13 +355,82 @@ void R_DrawSkyBox(void) } } +static void DefaultSkyMatrix(GLfloat *matrix) +{ + if (skyautorotate) { + SetupRotationMatrix(skymatrix, skyaxis, glr.fd.time * skyrotate); + TransposeAxis(skymatrix); + } + + matrix[ 0] = skymatrix[0][0]; + matrix[ 4] = skymatrix[0][1]; + matrix[ 8] = skymatrix[0][2]; + matrix[12] = -DotProduct(skymatrix[0], glr.fd.vieworg); + + matrix[ 1] = skymatrix[2][0]; + matrix[ 5] = skymatrix[2][1]; + matrix[ 9] = skymatrix[2][2]; + matrix[13] = -DotProduct(skymatrix[2], glr.fd.vieworg); + + matrix[ 2] = skymatrix[1][0]; + matrix[ 6] = skymatrix[1][1]; + matrix[10] = skymatrix[1][2]; + matrix[14] = -DotProduct(skymatrix[1], glr.fd.vieworg); + + matrix[ 3] = 0; + matrix[ 7] = 0; + matrix[11] = 0; + matrix[15] = 1; +} + +// classic skies don't rotate +static void ClassicSkyMatrix(GLfloat *matrix) +{ + matrix[ 0] = 1; + matrix[ 4] = 0; + matrix[ 8] = 0; + matrix[12] = -glr.fd.vieworg[0]; + + matrix[ 1] = 0; + matrix[ 5] = 1; + matrix[ 9] = 0; + matrix[13] = -glr.fd.vieworg[1]; + + matrix[ 2] = 0; + matrix[ 6] = 0; + matrix[10] = 3; + matrix[14] = -glr.fd.vieworg[2] * 3; + + matrix[ 3] = 0; + matrix[ 7] = 0; + matrix[11] = 0; + matrix[15] = 1; +} + +/* +============ +R_RotateForSky +============ +*/ +void R_RotateForSky(void) +{ + if (!gl_static.use_cubemaps) + return; + + DefaultSkyMatrix(glr.skymatrix[0]); + ClassicSkyMatrix(glr.skymatrix[1]); +} + static void R_UnsetSky(void) { int i; skyrotate = 0; + skyautorotate = false; for (i = 0; i < 6; i++) sky_images[i] = TEXNUM_BLACK; + + R_SKYTEXTURE->texnum = TEXNUM_CUBEMAP_BLACK; } /* @@ -383,8 +443,9 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis int i; char pathname[MAX_QPATH]; const image_t *image; + imageflags_t flags = IF_NONE; - if (!gl_drawsky->integer || (gl_static.classic_sky && gl_drawsky->integer == 1)) { + if (!gl_drawsky->integer) { R_UnsetSky(); return; } @@ -392,18 +453,36 @@ void R_SetSky(const char *name, float rotate, bool autorotate, const vec3_t axis skyrotate = rotate; skyautorotate = autorotate; VectorNormalize2(axis, skyaxis); - if (!skyautorotate) - SetupRotationMatrix(skymatrix, skyaxis, skyrotate); + SetupRotationMatrix(skymatrix, skyaxis, skyrotate); + if (gl_static.use_cubemaps) + TransposeAxis(skymatrix); + if (!skyrotate) + skyautorotate = false; + + // try to load cubemap image first + if (gl_static.use_cubemaps) { + if (Q_concat(pathname, sizeof(pathname), "sky/", name, ".tga") >= sizeof(pathname)) { + R_UnsetSky(); + return; + } + image = IMG_Find(pathname, IT_SKY, IF_CUBEMAP); + if (image != R_SKYTEXTURE) { + R_SKYTEXTURE->texnum = image->texnum; + return; + } + R_SKYTEXTURE->texnum = TEXNUM_CUBEMAP_DEFAULT; + flags = IF_CUBEMAP | IF_TURBULENT; // hack for IMG_Load() + } + // load legacy skybox for (i = 0; i < 6; i++) { if (Q_concat(pathname, sizeof(pathname), "env/", name, com_env_suf[i], ".tga") >= sizeof(pathname)) { R_UnsetSky(); return; } - FS_NormalizePath(pathname); - image = IMG_Find(pathname, IT_SKY, IF_NONE); - if (image == R_NOTEXTURE) { + image = IMG_Find(pathname, IT_SKY, flags); + if (image == R_SKYTEXTURE) { R_UnsetSky(); return; } diff --git a/src/refresh/state.c b/src/refresh/state.c index ebfde37fd..2684477a2 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -60,6 +60,38 @@ void GL_BindTexture(glTmu_t tmu, GLuint texnum) c.texSwitches++; } +void GL_ForceCubemap(GLuint texnum) +{ + GL_ActiveTexture(TMU_TEXTURE); + + if (gls.texnumcube == texnum) + return; + + qglBindTexture(GL_TEXTURE_CUBE_MAP, texnum); + gls.texnumcube = texnum; + + c.texSwitches++; +} + +void GL_BindCubemap(GLuint texnum) +{ + if (!gl_drawsky->integer) + texnum = TEXNUM_CUBEMAP_BLACK; + + if (gls.texnumcube == texnum) + return; + + if (qglBindTextureUnit) { + qglBindTextureUnit(TMU_TEXTURE, texnum); + } else { + GL_ActiveTexture(TMU_TEXTURE); + qglBindTexture(GL_TEXTURE_CUBE_MAP, texnum); + } + gls.texnumcube = texnum; + + c.texSwitches++; +} + void GL_CommonStateBits(glStateBits_t bits) { glStateBits_t diff = bits ^ gls.state_bits; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index df3d0b026..169a074aa 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -573,6 +573,13 @@ static glStateBits_t statebits_for_surface(const mface_t *surf) { glStateBits_t statebits = GLS_DEFAULT; + if (surf->drawflags & SURF_SKY) { + if (Q_stricmpn(surf->texinfo->name, CONST_STR_LEN("n64/env/sky")) == 0) + return GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY; + else + return GLS_TEXTURE_REPLACE | GLS_DEFAULT_SKY; + } + if (gl_static.use_shaders) { // no inverse intensity if (!(surf->drawflags & SURF_TRANS_MASK)) @@ -887,7 +894,9 @@ static void upload_world_surfaces(void) currvert = 0; lastvert = 0; for (i = 0, surf = bsp->faces; i < bsp->numfaces; i++, surf++) { - if (surf->drawflags & (SURF_SKY | SURF_NODRAW)) + if (surf->drawflags & SURF_SKY && !gl_static.use_cubemaps) + continue; + if (surf->drawflags & SURF_NODRAW) continue; Q_assert(surf->numsurfedges >= 3 && surf->numsurfedges <= TESS_MAX_VERTICES); @@ -974,7 +983,6 @@ void GL_FreeWorld(void) return; BSP_Free(gl_static.world.cache); - gl_static.classic_sky = NULL; if (gl_static.world.vertices) Z_Free(gl_static.world.vertices); @@ -1032,28 +1040,27 @@ void GL_LoadWorld(const char *name) GL_InitQueries(); gl_static.world.cache = bsp; - gl_static.classic_sky = NULL; // calculate world size for far clip plane and sky box set_world_size(bsp->nodes); // register all texinfo for (i = 0, info = bsp->texinfo; i < bsp->numtexinfo; i++, info++) { - if (gl_static.use_shaders && info->c.flags & SURF_SKY && - Q_stricmpn(info->name, CONST_STR_LEN("n64/env/sky")) == 0) { - if (gl_static.classic_sky) { - info->image = gl_static.classic_sky; - } else { + if (info->c.flags & SURF_SKY) { + if (!gl_static.use_cubemaps) { + info->image = R_NOTEXTURE; + } else if (Q_stricmpn(info->name, CONST_STR_LEN("n64/env/sky")) == 0) { Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".tga"); - FS_NormalizePath(buffer); info->image = IMG_Find(buffer, IT_SKY, IF_REPEAT | IF_CLASSIC_SKY); - if (info->image != R_NOTEXTURE) - gl_static.classic_sky = info->image; + } else if (Q_stricmpn(info->name, CONST_STR_LEN("sky/")) == 0) { + Q_concat(buffer, sizeof(buffer), info->name, ".tga"); + info->image = IMG_Find(buffer, IT_SKY, IF_CUBEMAP); + } else { + info->image = R_SKYTEXTURE; } } else { imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); - FS_NormalizePath(buffer); info->image = IMG_Find(buffer, IT_WALL, flags); } } @@ -1064,8 +1071,16 @@ void GL_LoadWorld(const char *name) // hack surface flags into drawflags for faster access surf->drawflags |= surf->texinfo->c.flags & ~DSURF_PLANEBACK; + // clear statebits from previous load + surf->statebits = GLS_DEFAULT; + // don't count sky surfaces - if (surf->drawflags & (SURF_SKY | SURF_NODRAW)) + if (surf->drawflags & SURF_SKY) { + if (!gl_static.use_cubemaps) + continue; + surf->drawflags &= ~SURF_NODRAW; + } + if (surf->drawflags & SURF_NODRAW) continue; if (surf->drawflags & SURF_N64_UV) n64surfs++; @@ -1082,12 +1097,15 @@ void GL_LoadWorld(const char *name) } gl_static.nolm_mask = SURF_NOLM_MASK_DEFAULT; + gl_static.use_bmodel_skies = false; // only supported in DECOUPLED_LM maps because vanilla maps have broken // lightofs for liquids/alphas. legacy renderer doesn't support lightmapped // liquids too. - if ((bsp->lm_decoupled || n64surfs > 100 || gl_static.classic_sky) && gl_static.use_shaders) + if ((bsp->lm_decoupled || n64surfs > 100) && gl_static.use_shaders) { gl_static.nolm_mask = SURF_NOLM_MASK_REMASTER; + gl_static.use_bmodel_skies = gl_static.use_cubemaps; + } glr.fd.lightstyles = &(lightstyle_t){ 1 }; diff --git a/src/refresh/tess.c b/src/refresh/tess.c index d7d4e080d..ce8450ceb 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -638,7 +638,9 @@ void GL_Flush3D(void) if (!tess.numindices) return; - if (q_likely(tess.texnum[TMU_LIGHTMAP])) { + if (q_unlikely(state & GLS_SKY_MASK)) { + array = GLA_VERTEX; + } else if (q_likely(tess.texnum[TMU_LIGHTMAP])) { state |= GLS_LIGHTMAP_ENABLE; array |= GLA_LMTC; @@ -655,7 +657,9 @@ void GL_Flush3D(void) GL_StateBits(state); GL_ArrayBits(array); - if (qglBindTextures) { + if (state & GLS_DEFAULT_SKY) { + GL_BindCubemap(tess.texnum[0]); + } else if (qglBindTextures) { #if USE_DEBUG if (q_unlikely(gl_nobind->integer)) tess.texnum[TMU_TEXTURE] = TEXNUM_DEFAULT; @@ -715,8 +719,9 @@ static const image_t *GL_TextureAnimation(const mtexinfo_t *tex) static void GL_DrawFace(const mface_t *surf) { const image_t *image = GL_TextureAnimation(surf->texinfo); - int numtris = surf->numsurfedges - 2; - int numindices = numtris * 3; + const int numtris = surf->numsurfedges - 2; + const int numindices = numtris * 3; + glStateBits_t state = surf->statebits; GLuint texnum[MAX_TMUS] = { 0 }; glIndex_t *dst_indices; int i, j; @@ -730,10 +735,17 @@ static void GL_DrawFace(const mface_t *surf) texnum[TMU_TEXTURE] = TEXNUM_WHITE; texnum[TMU_GLOWMAP] = 0; } + } else if (state & GLS_CLASSIC_SKY) { + if (q_likely(gl_drawsky->integer)) { + texnum[TMU_LIGHTMAP] = image->texnum2; + } else { + texnum[TMU_TEXTURE ] = TEXNUM_BLACK; + state &= ~GLS_CLASSIC_SKY; + } } if (memcmp(tess.texnum, texnum, sizeof(texnum)) || - tess.flags != surf->statebits || + tess.flags != state || tess.numindices + numindices > TESS_MAX_INDICES) GL_Flush3D(); @@ -752,7 +764,7 @@ static void GL_DrawFace(const mface_t *surf) tess.numindices += numindices; memcpy(tess.texnum, texnum, sizeof(texnum)); - tess.flags = surf->statebits; + tess.flags = state; c.facesTris += numtris; c.facesDrawn++; @@ -787,7 +799,9 @@ void GL_DrawAlphaFaces(void) glr.ent = face->entity; GL_Flush3D(); GL_SetEntityAxis(); - GL_RotateForEntity(); + GL_RotateForEntity(glr.ent == &gl_world ? + gl_static.use_cubemaps : + gl_static.use_bmodel_skies); } GL_DrawFace(face); } diff --git a/src/refresh/texture.c b/src/refresh/texture.c index eec063a01..4a5e501dd 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -29,6 +29,7 @@ static int gl_tex_solid_format; static int upload_width; static int upload_height; static bool upload_alpha; +static GLenum upload_target; static cvar_t *gl_noscrap; static cvar_t *gl_round_down; @@ -45,14 +46,15 @@ static cvar_t *gl_saturation; static cvar_t *gl_gamma; static cvar_t *gl_invert; static cvar_t *gl_partshape; +static cvar_t *gl_cubemaps; cvar_t *gl_intensity; -image_t shell_texture; static int GL_UpscaleLevel(int width, int height, imagetype_t type, imageflags_t flags); static void GL_Upload32(byte *data, int width, int height, int baselevel, imagetype_t type, imageflags_t flags); static void GL_Upscale32(byte *data, int width, int height, int maxlevel, imagetype_t type, imageflags_t flags); static void GL_SetFilterAndRepeat(imagetype_t type, imageflags_t flags); +static void GL_SetCubemapFilterAndRepeat(void); static void GL_InitRawTexture(void); typedef struct { @@ -82,13 +84,20 @@ static void update_image_params(unsigned mask) continue; if (!(mask & BIT(image->type))) continue; + if (!image->texnum) + continue; - GL_ForceTexture(TMU_TEXTURE, image->texnum); - GL_SetFilterAndRepeat(image->type, image->flags); - - if (image->texnum2) { - GL_ForceTexture(TMU_TEXTURE, image->texnum2); + if (image->flags & IF_CUBEMAP) { + GL_ForceCubemap(image->texnum); + GL_SetCubemapFilterAndRepeat(); + } else { + GL_ForceTexture(TMU_TEXTURE, image->texnum); GL_SetFilterAndRepeat(image->type, image->flags); + + if (image->texnum2) { + GL_ForceTexture(TMU_TEXTURE, image->texnum2); + GL_SetFilterAndRepeat(image->type, image->flags); + } } } } @@ -506,8 +515,12 @@ static void GL_Upload32(byte *data, int width, int height, int baselevel, imaget if (upload_alpha) comp = gl_tex_alpha_format; - qglTexImage2D(GL_TEXTURE_2D, baselevel, comp, scaled_width, - scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); + if (flags & IF_CUBEMAP) + qglTexImage2D(upload_target, baselevel, GL_RGBA, scaled_width, + scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); + else + qglTexImage2D(GL_TEXTURE_2D, baselevel, comp, scaled_width, + scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaled); c.texUploads++; @@ -660,6 +673,141 @@ static void GL_SetFilterAndRepeat(imagetype_t type, imageflags_t flags) } } +static const char gl_env_suf[6][2] = { + "rt", "lf", "up", "dn", "bk", "ft" +}; + +// format: 2 byte aspect ratio, then 6 pairs of (s, t) offsets +static const byte gl_env_ofs[6][14] = { + { 4, 3, 2, 1, 0, 1, 1, 0, 1, 2, 1, 1, 3, 1 }, // horizontal cross + { 3, 4, 2, 1, 0, 1, 1, 0, 1, 2, 1, 1, 1, 3 }, // vertical cross + { 6, 1, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0 }, // single row + { 1, 6, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5 }, // single column + { 3, 2, 0, 0, 0, 1, 1, 0, 1, 1, 2, 0, 2, 1 }, // double row + { 2, 3, 0, 0, 1, 0, 0, 1, 1, 1, 0, 2, 1, 2 }, // double column +}; + +static void GL_SetCubemapFilterAndRepeat(void) +{ + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, gl_filter_max); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, gl_filter_max); + + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); +} + +#define PIX(x, y) pic[(y) * n + (x)] + +static void GL_RotateImageCW(uint32_t *pic, int n) +{ + for (int i = 0; i < n / 2; i++) { + for (int j = i; j < n - i - 1; j++) { + uint32_t temp = PIX(i, j); + PIX(i, j) = PIX(n - j - 1, i); + PIX(n - j - 1, i) = PIX(n - i - 1, n - j - 1); + PIX(n - i - 1, n - j - 1) = PIX(j, n - i - 1); + PIX(j, n - i - 1) = temp; + } + } +} + +static void GL_RotateImageCCW(uint32_t *pic, int n) +{ + for (int i = 0; i < n / 2; i++) { + for (int j = i; j < n - i - 1; j++) { + uint32_t temp = PIX(i, j); + PIX(i, j) = PIX(j, n - i - 1); + PIX(j, n - i - 1) = PIX(n - i - 1, n - j - 1); + PIX(n - i - 1, n - j - 1) = PIX(n - j - 1, i); + PIX(n - j - 1, i) = temp; + } + } +} + +#undef PIX + +// upload one side of legacy skybox +static bool GL_UploadSkyboxSide(image_t *image, byte *pic) +{ + int width = image->upload_width; + int height = image->upload_height; + int i; + + // it should be safe to assume non-cube skyboxes don't exist, + // so don't bother with resampling. + if (width != height) + return false; + + Q_assert(image->baselen >= 2); + const char *s = image->name + image->baselen - 2; + + for (i = 0; i < 6; i++) + if (!memcmp(s, gl_env_suf[i], 2)) + break; + Q_assert(i < 6); + upload_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + + if (i == 2) + GL_RotateImageCW((uint32_t *)pic, width); + else if (i == 3) + GL_RotateImageCCW((uint32_t *)pic, width); + + GL_ForceCubemap(TEXNUM_CUBEMAP_DEFAULT); + GL_Upload32(pic, width, height, 0, image->type, image->flags); + + image->upload_width = upload_width; + image->upload_height = upload_height; + return true; +} + +static bool GL_UploadCubemap(image_t *image, byte *pic) +{ + int width = image->upload_width; + int height = image->upload_height; + const byte *ofs, *src; + byte *buffer, *dst; + int i, s, t, size; + + if (image->flags & IF_TURBULENT) + return GL_UploadSkyboxSide(image, pic); + + // figure out layout from aspect ratio + for (i = 0; i < q_countof(gl_env_ofs); i++) { + ofs = gl_env_ofs[i]; + if (width * ofs[1] == height * ofs[0]) + break; + } + if (i == q_countof(gl_env_ofs)) + return false; + + qglGenTextures(1, &image->texnum); + GL_ForceCubemap(image->texnum); + GL_SetCubemapFilterAndRepeat(); + + // need to make a copy here because of resampling + size = width / ofs[0]; + buffer = FS_AllocTempMem(size * size * 4); + + ofs += 2; + for (i = 0; i < 6; i++, ofs += 2) { + s = ofs[0] * size; + t = ofs[1] * size; + src = pic + ((t * width) + s) * 4; + dst = buffer; + for (int j = 0; j < size; j++) { + memcpy(dst, src, size * 4); + src += width * 4; + dst += size * 4; + } + upload_target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i; + GL_Upload32(buffer, size, size, 0, image->type, image->flags); + } + + FS_FreeTempMem(buffer); + return true; +} + /* ================ IMG_Load @@ -671,6 +819,13 @@ void IMG_Load(image_t *image, byte *pic) int i, s, t, maxlevel; int width, height; + if (image->flags & IF_CUBEMAP) { + if (GL_UploadCubemap(image, pic)) + return; + Com_WPrintf("%s has bad aspect ratio for cubemap\n", image->name); + image->flags &= ~IF_CUBEMAP; + } + width = image->upload_width; height = image->upload_height; @@ -732,6 +887,9 @@ void IMG_Unload(image_t *image) if (gls.texnums[i] == tex[0] || gls.texnums[i] == tex[1]) gls.texnums[i] = 0; + if (gls.texnumcube == tex[0]) + gls.texnumcube = 0; + qglDeleteTextures(tex[1] ? 2 : 1, tex); image->texnum = image->texnum2 = 0; } @@ -929,6 +1087,9 @@ static void GL_InitWhiteImage(void) GL_ForceTexture(TMU_TEXTURE, TEXNUM_BLACK); GL_Upload32((byte *)&pixel, 1, 1, 0, IT_SPRITE, IF_REPEAT | IF_NEAREST); GL_SetFilterAndRepeat(IT_SPRITE, IF_REPEAT | IF_NEAREST); + + // init shell texture (don't set name to keep it immutable) + R_SHELLTEXTURE->texnum = TEXNUM_WHITE; } static void GL_InitBeamTexture(void) @@ -962,6 +1123,28 @@ static void GL_InitRawTexture(void) GL_SetFilterAndRepeat(IT_PIC, IF_NONE); } +static void GL_InitCubemaps(void) +{ + gl_static.use_cubemaps = gl_static.use_shaders && gl_cubemaps->integer; + if (!gl_static.use_cubemaps) + return; + + // default cubemap for legacy skybox + GL_ForceCubemap(TEXNUM_CUBEMAP_DEFAULT); + GL_SetCubemapFilterAndRepeat(); + + // intentionally incomplete cubemap that will always return black + GL_ForceCubemap(TEXNUM_CUBEMAP_BLACK); + GL_SetCubemapFilterAndRepeat(); + + // init default sky texture + image_t *sky = R_SKYTEXTURE; + strcpy(sky->name, "SKYTEXTURE"); + sky->type = IT_SKY; + sky->flags = IF_CUBEMAP; + sky->texnum = TEXNUM_CUBEMAP_DEFAULT; +} + bool GL_InitWarpTexture(void) { GL_ClearErrors(); @@ -1046,6 +1229,7 @@ void GL_InitImages(void) gl_gamma = Cvar_Get("vid_gamma", "1", CVAR_ARCHIVE); gl_partshape = Cvar_Get("gl_partshape", "0", 0); gl_partshape->changed = gl_partshape_changed; + gl_cubemaps = Cvar_Get("gl_cubemaps", "1", CVAR_FILES); if (r_config.flags & QVF_GAMMARAMP) { gl_gamma->changed = gl_gamma_changed; @@ -1083,7 +1267,6 @@ void GL_InitImages(void) qglGenTextures(NUM_AUTO_TEXTURES, gl_static.texnums); qglGenTextures(LM_MAX_LIGHTMAPS, lm.texnums); - shell_texture.texnum = TEXNUM_WHITE; if (gl_static.use_shaders) { qglGenTextures(1, &gl_static.warp_texture); @@ -1098,6 +1281,7 @@ void GL_InitImages(void) GL_InitWhiteImage(); GL_InitBeamTexture(); GL_InitRawTexture(); + GL_InitCubemaps(); #if USE_DEBUG r_charset = R_RegisterFont("conchars"); @@ -1127,7 +1311,6 @@ void GL_ShutdownImages(void) memset(gl_static.texnums, 0, sizeof(gl_static.texnums)); memset(lm.texnums, 0, sizeof(lm.texnums)); - memset(&shell_texture, 0, sizeof(shell_texture)); GL_DeleteWarpTexture(); diff --git a/src/refresh/world.c b/src/refresh/world.c index efc3a0868..b204c9816 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -403,6 +403,7 @@ void GL_DrawBspModel(mmodel_t *model) vec3_t transformed, temp; entity_t *ent = glr.ent; glCullResult_t cull; + glStateBits_t skymask; int i; if (!model->numfaces) @@ -440,7 +441,9 @@ void GL_DrawBspModel(mmodel_t *model) GL_TransformLights(model); - GL_RotateForEntity(); + GL_RotateForEntity(gl_static.use_bmodel_skies); + + skymask = gl_static.use_bmodel_skies ? GLS_SKY_MASK : 0; GL_BindArrays(VA_3D); @@ -449,7 +452,9 @@ void GL_DrawBspModel(mmodel_t *model) // draw visible faces for (i = 0, face = model->firstface; i < model->numfaces; i++, face++) { // sky faces don't have their polygon built - if (face->drawflags & (SURF_SKY | SURF_NODRAW)) + if (face->drawflags & SURF_SKY && !(face->statebits & skymask)) + continue; + if (face->drawflags & SURF_NODRAW) continue; dot = PlaneDiffFast(transformed, face->plane); @@ -531,7 +536,7 @@ static inline void GL_DrawNode(const mnode_t *node) if (face->drawframe != glr.drawframe) continue; - if (face->drawflags & SURF_SKY) { + if (face->drawflags & SURF_SKY && !(face->statebits & GLS_SKY_MASK)) { R_AddSkySurface(face); continue; } From 2b3f6f603fbb7db29cdebf0df49a4baabfeb8e9d Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:51 +0300 Subject: [PATCH 759/974] Rename IF_DIRECT flag. --- inc/refresh/refresh.h | 5 +++-- src/common/tests.c | 2 +- src/refresh/images.c | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index d2c6617c9..d3196ea4b 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -157,9 +157,10 @@ typedef enum { IF_DEFAULT_FLARE = BIT(9), // default flare hack IF_CUBEMAP = BIT(10), // cubemap (or part of it) - // not stored in image + // these flags only affect R_RegisterImage() behavior, + // and are not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found - IF_DIRECT = BIT(17), // don't override extension + IF_KEEP_EXTENSION = BIT(17), // don't override extension IF_CLASSIC_SKY = BIT(18), // split in two halves } imageflags_t; diff --git a/src/common/tests.c b/src/common/tests.c index ae714bcdb..baf1a5d93 100644 --- a/src/common/tests.c +++ b/src/common/tests.c @@ -599,7 +599,7 @@ static void Com_TestImages_f(void) R_EndRegistration(); R_BeginRegistration(NULL); } - if (!R_RegisterImage(va("/%s", (char *)list[i]), IT_PIC, IF_DIRECT)) { + if (!R_RegisterImage(va("/%s", (char *)list[i]), IT_PIC, IF_KEEP_EXTENSION)) { errors++; continue; } diff --git a/src/refresh/images.c b/src/refresh/images.c index e6d6f83e7..86849fe31 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1964,7 +1964,7 @@ static image_t *find_or_load_image(const char *name, size_t len, // load the pic from disk pic = NULL; - if (flags & IF_DIRECT) { + if (flags & IF_KEEP_EXTENSION) { // direct load requested (for testing code) if (fmt == IM_MAX) ret = Q_ERR_INVALID_PATH; From ee0ec72a7d96e907f05973bc8e2ddb0e42c2bfb9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 760/974] Bump MAX_TEXTURE_SIZE. --- src/refresh/images.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/images.h b/src/refresh/images.h index c1646b5da..cd2d2067f 100644 --- a/src/refresh/images.h +++ b/src/refresh/images.h @@ -38,7 +38,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #define U32_RGB MakeColor(255, 255, 255, 0) // absolute limit for OpenGL renderer -#define MAX_TEXTURE_SIZE 4096 +#define MAX_TEXTURE_SIZE 8192 typedef enum { IM_PCX, From 34bd2f11e3fe2d5f93c948d6b9804276355f7044 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 761/974] =?UTF-8?q?Add=20=E2=80=98cl=5Fsurface=E2=80=99=20?= =?UTF-8?q?macro.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/main.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/client/main.c b/src/client/main.c index 6a4aadc34..cf1946658 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -2283,6 +2283,19 @@ static size_t CL_NumEntities_m(char *buffer, size_t size) return Q_snprintf(buffer, size, "%i", cl.frame.numEntities); } +static size_t CL_Surface_m(char *buffer, size_t size) +{ + trace_t trace; + vec3_t end; + + if (cls.state != ca_active) + return Q_strlcpy(buffer, "", size); + + VectorMA(cl.refdef.vieworg, 8192, cl.v_forward, end); + CL_Trace(&trace, cl.refdef.vieworg, end, vec3_origin, vec3_origin, MASK_SOLID | MASK_WATER); + return Q_strlcpy(buffer, trace.surface->name, size); +} + /* =============== CL_WriteConfig @@ -2812,6 +2825,7 @@ static void CL_InitLocal(void) Cmd_AddMacro("cl_armor", CL_Armor_m); Cmd_AddMacro("cl_weaponmodel", CL_WeaponModel_m); Cmd_AddMacro("cl_numentities", CL_NumEntities_m); + Cmd_AddMacro("cl_surface", CL_Surface_m); } static const cmdreg_t c_ignores[] = { From 4bbcb4ab6c57646a85c389cfd1e2101abb1171fa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 15 Sep 2024 11:19:52 +0300 Subject: [PATCH 762/974] Remove redundant argument. --- src/refresh/gl.h | 2 +- src/refresh/tess.c | 4 ++-- src/refresh/world.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index e302eb6b2..4230ac14c 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -772,7 +772,7 @@ void GL_ShutdownArrays(void); void GL_Flush3D(void); -void GL_AddAlphaFace(mface_t *face, entity_t *ent); +void GL_AddAlphaFace(mface_t *face); void GL_AddSolidFace(mface_t *face); void GL_DrawAlphaFaces(void); void GL_DrawSolidFaces(void); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index ce8450ceb..dd6380d37 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -819,10 +819,10 @@ void GL_AddSolidFace(mface_t *face) faces_next[face->hash] = &face->next; } -void GL_AddAlphaFace(mface_t *face, entity_t *ent) +void GL_AddAlphaFace(mface_t *face) { // draw back-to-front - face->entity = ent; + face->entity = glr.ent; face->next = faces_alpha; faces_alpha = face; } diff --git a/src/refresh/world.c b/src/refresh/world.c index b204c9816..682d39314 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -468,7 +468,7 @@ void GL_DrawBspModel(mmodel_t *model) if (face->drawflags & SURF_TRANS_MASK) { if (model->drawframe != glr.drawframe) - GL_AddAlphaFace(face, ent); + GL_AddAlphaFace(face); continue; } @@ -548,7 +548,7 @@ static inline void GL_DrawNode(const mnode_t *node) GL_PushLights(face); if (face->drawflags & SURF_TRANS_MASK) - GL_AddAlphaFace(face, &gl_world); + GL_AddAlphaFace(face); else GL_AddSolidFace(face); } From d06914c81ef27539c40867ad579589c3623c20f8 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 16 Sep 2024 01:48:28 +0300 Subject: [PATCH 763/974] Assert world vertex buffer doesn't overflow. --- src/refresh/gl.h | 1 + src/refresh/surf.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 4230ac14c..b0b4a8df3 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -69,6 +69,7 @@ typedef struct { bsp_t *cache; vec_t *vertices; GLuint buffer; + size_t buffer_size; vec_t size; } world; GLuint warp_texture; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 169a074aa..ba0f25e8d 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -878,6 +878,7 @@ static void check_multitexture(void) static void upload_world_surfaces(void) { const bsp_t *bsp = gl_static.world.cache; + size_t size = gl_static.world.buffer_size; vec_t *vbo; mface_t *surf; int i, currvert, lastvert; @@ -900,6 +901,8 @@ static void upload_world_surfaces(void) continue; Q_assert(surf->numsurfedges >= 3 && surf->numsurfedges <= TESS_MAX_VERTICES); + Q_assert(size >= surf->numsurfedges * VERTEX_SIZE * sizeof(vbo[0])); + size -= surf->numsurfedges * VERTEX_SIZE * sizeof(vbo[0]); if (gl_static.world.vertices) { vbo = gl_static.world.vertices + currvert * VERTEX_SIZE; @@ -1095,6 +1098,7 @@ void GL_LoadWorld(const char *name) gl_static.world.vertices = Z_TagMalloc(size, TAG_RENDERER); Com_DPrintf("%s: %zu bytes of vertex data on heap\n", __func__, size); } + gl_static.world.buffer_size = size; gl_static.nolm_mask = SURF_NOLM_MASK_DEFAULT; gl_static.use_bmodel_skies = false; From d3c1a67a0d5632ae8fa3375c75915b229226d5cf Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 17 Sep 2024 13:19:07 +0300 Subject: [PATCH 764/974] Don't register texinfo for SURF_NODRAW. Don't auto download it either. --- src/client/download.c | 2 ++ src/refresh/surf.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/client/download.c b/src/client/download.c index 031d551b3..dc5c99d4f 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -823,6 +823,8 @@ void CL_RequestNextDownload(void) if (allow_download_textures->integer) { for (i = 0; i < cl.bsp->numtexinfo; i++) { + if (cl.bsp->texinfo[i].c.flags & SURF_NODRAW) + continue; len = Q_concat(fn, sizeof(fn), "textures/", cl.bsp->texinfo[i].name, ".wal"); check_file_len(fn, len, DL_OTHER); } diff --git a/src/refresh/surf.c b/src/refresh/surf.c index ba0f25e8d..ff79651ad 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -1061,6 +1061,8 @@ void GL_LoadWorld(const char *name) } else { info->image = R_SKYTEXTURE; } + } else if (info->c.flags & SURF_NODRAW) { + info->image = R_NOTEXTURE; } else { imageflags_t flags = (info->c.flags & SURF_WARP) ? IF_TURBULENT : IF_NONE; Q_concat(buffer, sizeof(buffer), "textures/", info->name, ".wal"); From a097fcaa1e4a2864202f14a7c0f917b976752a15 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 07:56:19 -0400 Subject: [PATCH 765/974] Revert hit marker to only single sound --- inc/shared/shared.h | 5 ----- src/action/p_view.c | 20 +------------------- src/client/parse.c | 4 ---- src/client/tent.c | 24 +----------------------- 4 files changed, 2 insertions(+), 51 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 861fc2e0c..27efe8ccb 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1436,11 +1436,6 @@ typedef enum { TE_DAMAGE_DEALT = 128, - TE_AQ2_HEADSHOT, - TE_AQ2_CHESTSHOT, - TE_AQ2_STOMSHOT, - TE_AQ2_LEGSHOT, - TE_NUM_ENTITIES } temp_event_t; diff --git a/src/action/p_view.c b/src/action/p_view.c index ac6a64918..8807b4c0e 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1698,25 +1698,7 @@ void ClientEndServerFrame (edict_t * ent) if (ent->client->damage_dealt > 0 && !ent->is_bot) { gi.WriteByte(svc_temp_entity); - - switch (ent->client->attacker_loc) - { - case LOC_HDAM: - gi.WriteByte(TE_AQ2_HEADSHOT); - break; - case LOC_CDAM: - gi.WriteByte(TE_AQ2_CHESTSHOT); - break; - case LOC_SDAM: - gi.WriteByte(TE_AQ2_STOMSHOT); - break; - case LOC_LDAM: - gi.WriteByte(TE_AQ2_LEGSHOT); - break; - default: - gi.WriteByte(TE_DAMAGE_DEALT); - break; - } + gi.WriteByte(TE_DAMAGE_DEALT); gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); gi.unicast(ent, false); ent->client->damage_dealt = 0; diff --git a/src/client/parse.c b/src/client/parse.c index 3e156ae0c..0fbb44871 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -969,10 +969,6 @@ static void CL_ParseTEntPacket(void) break; case TE_DAMAGE_DEALT: - case TE_AQ2_HEADSHOT: - case TE_AQ2_CHESTSHOT: - case TE_AQ2_STOMSHOT: - case TE_AQ2_LEGSHOT: te.count = MSG_ReadShort(); break; diff --git a/src/client/tent.c b/src/client/tent.c index 61f114374..ab90185b3 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -325,11 +325,6 @@ void CL_RegisterTEntSounds(void) cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); cl_sfx_hit_marker = S_RegisterSound("hitsounds/damage.wav"); - cl_sfx_headshot = S_RegisterSound("hitsounds/headshot.wav"); - cl_sfx_chestshot = S_RegisterSound("hitsounds/chest.wav"); - cl_sfx_stomshot = S_RegisterSound("hitsounds/stomach.wav"); - cl_sfx_legshot = S_RegisterSound("hitsounds/leg.wav"); -} static const char *const muzzlenames[MFLASH_TOTAL] = { [MFLASH_MACHN] = "v_machn", @@ -1669,28 +1664,11 @@ void CL_ParseTEnt(void) break; case TE_DAMAGE_DEALT: - case TE_AQ2_HEADSHOT: - case TE_AQ2_CHESTSHOT: - case TE_AQ2_STOMSHOT: - case TE_AQ2_LEGSHOT: if (te.count > 0 && cl_hit_markers->integer > 0) { - qhandle_t hit_sfx; cl.hit_marker_time = cls.realtime; cl.hit_marker_count = te.count; if (cl_hit_markers->integer > 1) { - if (te.type == TE_AQ2_HEADSHOT) - hit_sfx = cl_sfx_headshot; - else if (te.type == TE_AQ2_CHESTSHOT) - hit_sfx = cl_sfx_chestshot; - else if (te.type == TE_AQ2_STOMSHOT) - hit_sfx = cl_sfx_stomshot; - else if (te.type == TE_AQ2_LEGSHOT) - hit_sfx = cl_sfx_legshot; - else if (te.type == TE_DAMAGE_DEALT) - hit_sfx = cl_sfx_hit_marker; - else - hit_sfx = cl_sfx_hit_marker; - S_StartSound(NULL, listener_entnum, 257, hit_sfx, 1, ATTN_NONE, 0); + S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); } } break; From e21083961fb29a273ed256b6e675ad79e84295d7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 12:57:56 -0400 Subject: [PATCH 766/974] de-static'd game_export_ex_t --- src/action/g_main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/g_main.c b/src/action/g_main.c index 0d577524f..d3521879a 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -687,7 +687,7 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) return &globals; } -static const game_export_ex_t gex = { +const game_export_ex_t gex = { .apiversion = GAME_API_VERSION_EX, .structsize = sizeof(game_export_ex_t), From e3a9be852e8291f0b470ff38aa9ac92baa5d1f81 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 13:07:40 -0400 Subject: [PATCH 767/974] de-static'd game_export_ex_t --- inc/shared/game.h | 5 ----- inc/shared/gameext.h | 2 ++ src/action/g_main.c | 36 ++++++++++++++++++------------------ src/server/game.c | 1 + 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index f09c5d036..f8b950407 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -303,11 +303,6 @@ typedef struct { // of the parameters void (*ServerCommand)(void); -#if AQTION_EXTENSION - void* (*FetchGameExtension)(char *name); -#endif - - // // global variables shared between game and server // diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index b4625783f..deba7dbf1 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -75,6 +75,7 @@ typedef struct { void *(*GetExtension)(const char *name); void *(*TagRealloc)(void *ptr, size_t size); + void *(*CheckForExtension)(char *text); } game_import_ex_t; typedef struct { @@ -87,6 +88,7 @@ typedef struct { void (*RestartFilesystem)(void); // called when fs_restart is issued qboolean (*CustomizeEntityToClient)(edict_t *client, edict_t *ent, customize_entity_t *temp); // if true is returned, `temp' must be initialized qboolean (*EntityVisibleToClient)(edict_t *client, edict_t *ent); + void *(*CheckForExtension)(char *text); } game_export_ex_t; typedef const game_export_ex_t *(*game_entry_ex_t)(const game_import_ex_t *); diff --git a/src/action/g_main.c b/src/action/g_main.c index d3521879a..ae116bed6 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -663,24 +663,7 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) #if AQTION_EXTENSION - G_InitExtEntrypoints(); - globals.FetchGameExtension = G_FetchGameExtension; - - engine_Client_GetProtocol = gi.CheckForExtension("Client_GetProtocol"); - engine_Client_GetVersion = gi.CheckForExtension("Client_GetVersion"); - - engine_Ghud_ClearForClient = gi.CheckForExtension("Ghud_ClearForClient"); - engine_Ghud_NewElement = gi.CheckForExtension("Ghud_NewElement"); - engine_Ghud_RemoveElement = gi.CheckForExtension("Ghud_RemoveElement"); - engine_Ghud_SetFlags = gi.CheckForExtension("Ghud_SetFlags"); - engine_Ghud_SetInt = gi.CheckForExtension("Ghud_SetInt"); - engine_Ghud_SetText = gi.CheckForExtension("Ghud_SetText"); - engine_Ghud_SetPosition = gi.CheckForExtension("Ghud_SetPosition"); - engine_Ghud_SetAnchor = gi.CheckForExtension("Ghud_SetAnchor"); - engine_Ghud_SetColor = gi.CheckForExtension("Ghud_SetColor"); - engine_Ghud_SetSize = gi.CheckForExtension("Ghud_SetSize"); - - engine_CvarSync_Set = gi.CheckForExtension("CvarSync_Set"); + #endif @@ -701,6 +684,23 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) { gix = importx; // assign pointer, don't copy! + G_InitExtEntrypoints(); + + engine_Client_GetProtocol = gi.CheckForExtension("Client_GetProtocol"); + engine_Client_GetVersion = gi.CheckForExtension("Client_GetVersion"); + + engine_Ghud_ClearForClient = gi.CheckForExtension("Ghud_ClearForClient"); + engine_Ghud_NewElement = gi.CheckForExtension("Ghud_NewElement"); + engine_Ghud_RemoveElement = gi.CheckForExtension("Ghud_RemoveElement"); + engine_Ghud_SetFlags = gi.CheckForExtension("Ghud_SetFlags"); + engine_Ghud_SetInt = gi.CheckForExtension("Ghud_SetInt"); + engine_Ghud_SetText = gi.CheckForExtension("Ghud_SetText"); + engine_Ghud_SetPosition = gi.CheckForExtension("Ghud_SetPosition"); + engine_Ghud_SetAnchor = gi.CheckForExtension("Ghud_SetAnchor"); + engine_Ghud_SetColor = gi.CheckForExtension("Ghud_SetColor"); + engine_Ghud_SetSize = gi.CheckForExtension("Ghud_SetSize"); + + engine_CvarSync_Set = gi.CheckForExtension("CvarSync_Set"); return &gex; } diff --git a/src/server/game.c b/src/server/game.c index 368feaa4e..335768910 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -920,6 +920,7 @@ static const game_import_ex_t game_import_ex = { .GetExtension = PF_GetExtension, .TagRealloc = PF_TagRealloc, + .CheckForExtension = G_CheckForExtension, }; static void *game_library; From 2329b7d6467c5924c60fe844a9887e13f5348d7d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 13:29:16 -0400 Subject: [PATCH 768/974] Compiles, but segfaults --- src/action/g_ext.c | 5 ----- src/action/g_main.c | 1 + src/server/game.c | 5 +++-- src/server/main.c | 2 ++ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/action/g_ext.c b/src/action/g_ext.c index a24328ab4..42f5fcb8c 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -303,11 +303,6 @@ void* G_FetchGameExtension(char *name) return NULL; } - - - - - // // new engine functions we can call from the game diff --git a/src/action/g_main.c b/src/action/g_main.c index ae116bed6..10889f1ee 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -684,6 +684,7 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) { gix = importx; // assign pointer, don't copy! + //gex.CheckForExtension = G_FetchGameExtension; G_InitExtEntrypoints(); engine_Client_GetProtocol = gi.CheckForExtension("Client_GetProtocol"); diff --git a/src/server/game.c b/src/server/game.c index 335768910..bd3018c30 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -908,6 +908,7 @@ static void *PF_GetExtension(const char *name) return NULL; } +static void* G_CheckForExtension(char *text); static const game_import_ex_t game_import_ex = { .apiversion = GAME_API_VERSION_EX, @@ -1210,7 +1211,7 @@ void SV_InitGameProgs(void) } #if AQTION_EXTENSION - GE_customizeentityforclient = ge->FetchGameExtension("customizeentityforclient"); - GE_CvarSync_Updated = ge->FetchGameExtension("CvarSync_Updated"); + GE_customizeentityforclient = gex->GetExtension("customizeentityforclient"); + GE_CvarSync_Updated = gex->GetExtension("CvarSync_Updated"); #endif } diff --git a/src/server/main.c b/src/server/main.c index c5006269e..d6f42f346 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2468,6 +2468,8 @@ void SV_Init(void) SV_SetConsoleTitle(); #endif + G_InitializeExtensions(); + sv_registered = true; } From a7589611b444c0e9b201aabbd499bc013460e617 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 13:30:59 -0400 Subject: [PATCH 769/974] segfaults on bsp stuff --- src/server/game.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/game.c b/src/server/game.c index bd3018c30..f886baaef 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1211,7 +1211,7 @@ void SV_InitGameProgs(void) } #if AQTION_EXTENSION - GE_customizeentityforclient = gex->GetExtension("customizeentityforclient"); - GE_CvarSync_Updated = gex->GetExtension("CvarSync_Updated"); + //GE_customizeentityforclient = gex->GetExtension("customizeentityforclient"); + //GE_CvarSync_Updated = gex->GetExtension("CvarSync_Updated"); #endif } From 68f36a0d7e01028e4964c667ac4b340760b49959 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 13:37:52 -0400 Subject: [PATCH 770/974] Moved extensions to extended import --- inc/shared/game.h | 4 ---- src/action/g_main.c | 30 +++++++++++++++--------------- src/server/game.c | 4 ---- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/inc/shared/game.h b/inc/shared/game.h index f8b950407..9a72dceda 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -256,10 +256,6 @@ typedef struct { void (*DebugGraph)(float value, int color); -#if AQTION_EXTENSION - void *(*CheckForExtension)(char *text); -#endif - } game_import_t; // // functions exported by the game subsystem diff --git a/src/action/g_main.c b/src/action/g_main.c index 10889f1ee..53bf0a5ce 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -686,22 +686,22 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) //gex.CheckForExtension = G_FetchGameExtension; G_InitExtEntrypoints(); + engine_Client_GetProtocol = gix->CheckForExtension("Client_GetProtocol"); + engine_Client_GetVersion = gix->CheckForExtension("Client_GetVersion"); + + engine_Ghud_ClearForClient = gix->CheckForExtension("Ghud_ClearForClient"); + engine_Ghud_NewElement = gix->CheckForExtension("Ghud_NewElement"); + engine_Ghud_RemoveElement = gix->CheckForExtension("Ghud_RemoveElement"); + engine_Ghud_SetFlags = gix->CheckForExtension("Ghud_SetFlags"); + engine_Ghud_SetInt = gix->CheckForExtension("Ghud_SetInt"); + engine_Ghud_SetText = gix->CheckForExtension("Ghud_SetText"); + engine_Ghud_SetPosition = gix->CheckForExtension("Ghud_SetPosition"); + engine_Ghud_SetAnchor = gix->CheckForExtension("Ghud_SetAnchor"); + engine_Ghud_SetColor = gix->CheckForExtension("Ghud_SetColor"); + engine_Ghud_SetSize = gix->CheckForExtension("Ghud_SetSize"); + + engine_CvarSync_Set = gix->CheckForExtension("CvarSync_Set"); - engine_Client_GetProtocol = gi.CheckForExtension("Client_GetProtocol"); - engine_Client_GetVersion = gi.CheckForExtension("Client_GetVersion"); - - engine_Ghud_ClearForClient = gi.CheckForExtension("Ghud_ClearForClient"); - engine_Ghud_NewElement = gi.CheckForExtension("Ghud_NewElement"); - engine_Ghud_RemoveElement = gi.CheckForExtension("Ghud_RemoveElement"); - engine_Ghud_SetFlags = gi.CheckForExtension("Ghud_SetFlags"); - engine_Ghud_SetInt = gi.CheckForExtension("Ghud_SetInt"); - engine_Ghud_SetText = gi.CheckForExtension("Ghud_SetText"); - engine_Ghud_SetPosition = gi.CheckForExtension("Ghud_SetPosition"); - engine_Ghud_SetAnchor = gi.CheckForExtension("Ghud_SetAnchor"); - engine_Ghud_SetColor = gi.CheckForExtension("Ghud_SetColor"); - engine_Ghud_SetSize = gi.CheckForExtension("Ghud_SetSize"); - - engine_CvarSync_Set = gi.CheckForExtension("CvarSync_Set"); return &gex; } diff --git a/src/server/game.c b/src/server/game.c index f886baaef..8e30ffd16 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1157,10 +1157,6 @@ void SV_InitGameProgs(void) // load a new game dll import = game_import; -#if AQTION_EXTENSION - import.CheckForExtension = G_CheckForExtension; -#endif - ge = entry(&import); if (!ge) { Com_Error(ERR_DROP, "Game library returned NULL exports"); From de0f9a539434d82b8baca2a7332fd11dd8894e0e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 17 Sep 2024 16:05:29 -0400 Subject: [PATCH 771/974] Broken state but I think I'm getting closer? --- src/action/acesrc/acebot_spawn.c | 6 ++--- src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_nodes.c | 11 ++++----- src/action/botlib/botlib_spawn.c | 9 ++++---- src/action/g_ext.c | 11 +++++++++ src/action/g_local.h | 9 ++++++++ src/action/g_main.c | 7 ++++++ src/action/g_spawn.c | 3 +-- src/server/game.c | 38 ++++++++++++++++++-------------- src/server/world.c | 26 +++++----------------- 10 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 13ec5b43d..f6a4fcc1d 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -640,7 +640,7 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -700,7 +700,7 @@ void ACESP_RemoveBot(char *name) { //rekkie -- Fake Bot Client -- s rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -773,7 +773,7 @@ void ACESP_RemoveTeamplayBot(int team) { //rekkie -- Fake Bot Client -- s rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e game.bot_count--; diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 06d6a4329..866a56c27 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -651,7 +651,7 @@ void BOTLIB_Think(edict_t* self) self->client->ping = self->bot.bot_ping; rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' + SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index f1dee985c..e062f8e4b 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3897,8 +3897,7 @@ void ACEND_LoadAAS(qboolean force) fclose(fIn); // Close the file } - rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bsp_t* bsp = bsp_api->Bsp(); + bsp_t* bsp = Bsp(); fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum if (bsp_checksum != bsp->checksum) @@ -5329,9 +5328,8 @@ qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t no void BOTLIB_InitNavigation(edict_t* ent) { - rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bsp_t* bsp = bsp_api->Bsp(); - if (bsp_api->Bsp() == NULL) + bsp_t* bsp = Bsp(); + if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); return; @@ -5359,8 +5357,7 @@ void BOTLIB_InitNavigation(edict_t* ent) void ACEND_BSP(edict_t* ent) { - rektek_bots_api_v1_t *bsp_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bsp_t* bsp = bsp_api->Bsp(); + bsp_t* bsp = Bsp(); if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index a0fb1e988..cdef09ebc 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1587,8 +1587,7 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -1622,7 +1621,7 @@ void BOTLIB_RemoveBot(char* name) { //rekkie -- Fake Bot Client -- s rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -1695,7 +1694,7 @@ void BOTLIB_RemoveBot(char* name) // Fake Bot Client - Disconnect the bot rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client bot->health = 0; @@ -1742,7 +1741,7 @@ void BOTLIB_RemoveTeamplayBot(int team) { //rekkie -- Fake Bot Client -- s rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 42f5fcb8c..2a1ca9a41 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -50,6 +50,16 @@ void(*engine_Ghud_SetSize)(edict_t *ent, int i, int x, int y); void(*engine_CvarSync_Set)(int index, const char *name, const char *val); +// botlib +bsp_t* (*SV_BSP)(void); +nav_t* (*CS_NAV)(void); +debug_draw_t* (*Draw)(void); +void (*SV_BotUpdateInfo)(char* name, int ping, int score); +void (*SV_BotConnect)(char* name); +void (*SV_BotDisconnect)(char* name); +void (*SV_BotClearClients)(void); + + // // optional new entrypoints the engine may want to call edict_t *xerp_ent; @@ -466,4 +476,5 @@ void CvarSync_Set(int index, const char *name, const char *val) engine_CvarSync_Set(index, name, val); } + #endif \ No newline at end of file diff --git a/src/action/g_local.h b/src/action/g_local.h index 56471de28..9e4f72bc2 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1360,6 +1360,15 @@ extern void(*engine_CvarSync_Set)(int index, const char *name, const char *val); void CvarSync_Set(int index, const char *name, const char *val); #endif +//botlib +extern bsp_t* (*SV_BSP)(void); +extern nav_t* (CS_NAV)(void); +extern void (*SV_BotUpdateInfo)(char* name, int ping, int score); +extern void (*SV_BotConnect)(char* name); +extern void (*SV_BotDisconnect)(char* name); +extern void (*SV_BotClearClients)(void); + + // 2022 extern cvar_t *sv_limp_highping; extern cvar_t *server_id; // Unique server_id diff --git a/src/action/g_main.c b/src/action/g_main.c index 53bf0a5ce..13dab1195 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -702,6 +702,13 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) engine_CvarSync_Set = gix->CheckForExtension("CvarSync_Set"); + SV_BSP = gix->CheckForExtension("Bsp"); + CS_NAV = gix->CheckForExtension("Nav"); + SV_BotConnect = gix->CheckForExtension("SV_BotConnect"); + SV_BotDisconnect = gix->CheckForExtension("SV_BotDisconnect"); + SV_BotUpdateInfo = gix->CheckForExtension("SV_BotUpdateInfo"); + SV_BotClearClients = gix->CheckForExtension("SV_BotClearClients"); + return &gex; } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 31ce6e46d..1c44f2c46 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1593,8 +1593,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn memset(&botlib_noises, 0, sizeof(botlib_noises)); //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); - bot_api->SV_BotClearClients(); + SV_BotClearClients(); //gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e diff --git a/src/server/game.c b/src/server/game.c index 8e30ffd16..be0753427 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -876,11 +876,11 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { #endif static const rektek_bots_api_v1_t rektek_bots_api_v1 = { - .Bsp = SV_BSP, - .Nav = CS_NAV, -#if DEBUG_DRAWING - .Draw = CS_DebugDraw, -#endif +// .Bsp = SV_BSP, +// .Nav = CS_NAV, +// #if DEBUG_DRAWING +// .Draw = CS_DebugDraw, +// #endif .SV_BotUpdateInfo = SV_BotUpdateInfo, .SV_BotConnect = SV_BotConnect, .SV_BotDisconnect = SV_BotDisconnect, @@ -889,21 +889,23 @@ static const rektek_bots_api_v1_t rektek_bots_api_v1 = { static void *PF_GetExtension(const char *name) { - if (!name) + if (!name){ return NULL; + } - if (!strcmp(name, FILESYSTEM_API_V1)) + if (!strcmp(name, FILESYSTEM_API_V1)){ return (void *)&filesystem_api_v1; + } -#ifdef AQTION_EXTENSION - if (!strcmp(name, REKTEK_BOTS_API_V1)) + if (!strcmp(name, REKTEK_BOTS_API_V1)){ return (void *)&rektek_bots_api_v1; -#endif + } #if USE_REF && USE_DEBUG - if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer) + if (!strcmp(name, DEBUG_DRAW_API_V1) && !dedicated->integer){ return (void *)&debug_draw_api_v1; + } #endif return NULL; @@ -1104,6 +1106,15 @@ void G_InitializeExtensions(void) // cvar sync g_addextension("CvarSync_Set", G_Ext_CvarSync_Set); + + // botlib + g_addextension("Bsp", SV_BSP); + g_addextension("Nav", CS_NAV); + g_addextension("SV_BotConnect", SV_BotConnect); + g_addextension("SV_BotDisconnect", SV_BotDisconnect); + g_addextension("SV_BotClearClients", SV_BotClearClients); + g_addextension("SV_BotUpdateInfo", SV_BotUpdateInfo); + } @@ -1205,9 +1216,4 @@ void SV_InitGameProgs(void) if (ge->max_edicts <= sv_maxclients->integer || ge->max_edicts > svs.csr.max_edicts) { Com_Error(ERR_DROP, "Game library returned bad number of max_edicts: %i", ge->max_edicts); } - -#if AQTION_EXTENSION - //GE_customizeentityforclient = gex->GetExtension("customizeentityforclient"); - //GE_CvarSync_Updated = gex->GetExtension("CvarSync_Updated"); -#endif } diff --git a/src/server/world.c b/src/server/world.c index deb77551b..1a6959d8d 100644 --- a/src/server/world.c +++ b/src/server/world.c @@ -597,21 +597,14 @@ trace_t q_gameabi SV_Trace(const vec3_t start, const vec3_t mins, //============= bsp_t* SV_BSP(void) { - rektek_bots_api_v1_t *bsp_api = gex->GetExtension(REKTEK_BOTS_API_V1); + bsp_t* bsp = sv.cm.cache; - if (!bsp_api) { + if (!bsp) { Com_Error(ERR_DROP, "%s: no map loaded", __func__); return NULL; } - // Ensure that bsp_api->Bsp is a function that returns bsp_t* - if (bsp_api->Bsp == NULL) { - Com_Error(ERR_DROP, "%s: Bsp function not set", __func__); - return NULL; - } - - // Call the Bsp function and return its result - return bsp_api->Bsp(); + return bsp; } //#endif //rekkie -- BSP -- e @@ -620,21 +613,14 @@ bsp_t* SV_BSP(void) // Share surface data with game library nav_t* CS_NAV(void) { - rektek_bots_api_v1_t *nav_api = gex->GetExtension(REKTEK_BOTS_API_V1); + nav_t* nav = sv.cm.nav; - if (!nav_api) { + if (!nav) { //Com_Error(ERR_DROP, "%s: no nav data loaded", __func__); return NULL; } - // Ensure that nav_api->Nav is a function that returns nav_t* - if (nav_api->Nav == NULL) { - //Com_Error(ERR_DROP, "%s: Nav function not set", __func__); - return NULL; - } - - // Call the Nav function and return its result - return nav_api->Nav(); + return nav; } //rekkie -- debug drawing -- s From 7a27ecdd69353185f1138e63bd2dcdc9e342ed95 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 18 Sep 2024 15:32:02 -0400 Subject: [PATCH 772/974] CS_NAV giving me grief --- src/action/botlib/botlib_nodes.c | 8 ++++---- src/action/g_local.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index e062f8e4b..414749182 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -5328,7 +5328,7 @@ qboolean BOTLIB_InsideFace(vec3_t *verts, int num_verts, vec3_t point, vec3_t no void BOTLIB_InitNavigation(edict_t* ent) { - bsp_t* bsp = Bsp(); + bsp_t* bsp = SV_BSP(); if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); @@ -5357,7 +5357,7 @@ void BOTLIB_InitNavigation(edict_t* ent) void ACEND_BSP(edict_t* ent) { - bsp_t* bsp = Bsp(); + bsp_t* bsp = SV_BSP(); if (bsp == NULL) { gi.dprintf("%s failed to import BSP data\n", __func__); @@ -5394,8 +5394,8 @@ void ACEND_BSP(edict_t* ent) if (1) { - rektek_bots_api_v1_t *nav_api = gix->GetExtension(REKTEK_BOTS_API_V1); - nav_t* nav = nav_api->Nav(); + nav_t* nav = Nav(); + //ent->nav = gi.Nav(); // Grant access to navigation data if (ent->nav) { diff --git a/src/action/g_local.h b/src/action/g_local.h index 9e4f72bc2..60674733c 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1362,7 +1362,7 @@ void CvarSync_Set(int index, const char *name, const char *val); //botlib extern bsp_t* (*SV_BSP)(void); -extern nav_t* (CS_NAV)(void); +extern nav_t* (*CS_NAV)(void); extern void (*SV_BotUpdateInfo)(char* name, int ping, int score); extern void (*SV_BotConnect)(char* name); extern void (*SV_BotDisconnect)(char* name); From e9af1a8d2ee474463e0075ffe4aecbe6518af444 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 18 Sep 2024 16:00:13 -0400 Subject: [PATCH 773/974] Compiles and runs --- inc/common/bsp.h | 2 -- src/action/botlib/botlib_nodes.c | 4 ++-- src/action/g_ext.c | 2 +- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/p_client.c | 4 +--- src/server/game.c | 6 +----- src/server/server.h | 2 ++ 8 files changed, 9 insertions(+), 13 deletions(-) diff --git a/inc/common/bsp.h b/inc/common/bsp.h index b3b1dcfa3..da55afe94 100644 --- a/inc/common/bsp.h +++ b/inc/common/bsp.h @@ -367,7 +367,6 @@ typedef struct nav_s short int ignored_faces_total; // Ignored faces: FACETYPE_IGNORED, FACETYPE_NONE } nav_t; extern nav_t* nav_; -nav_t* CS_NAV(void); void MoveAwayFromNormal(const int drawflags, const vec3_t normal, vec3_t out, const float distance); //rekkie -- surface data -- e @@ -391,7 +390,6 @@ typedef struct debug_draw_s void* DrawArrow; // DrawArrow function pointer int draw_arrow_num; // Current arrow being drawn: increments until MAX_DRAW_ARROWS, then resets to zero } debug_draw_t; -debug_draw_t* CS_DebugDraw(void); void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, const float line_width, qboolean occluded); diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 414749182..0ddfae20d 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3897,7 +3897,7 @@ void ACEND_LoadAAS(qboolean force) fclose(fIn); // Close the file } - bsp_t* bsp = Bsp(); + bsp_t* bsp = SV_BSP(); fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum if (bsp_checksum != bsp->checksum) @@ -5394,7 +5394,7 @@ void ACEND_BSP(edict_t* ent) if (1) { - nav_t* nav = Nav(); + nav_t* nav = CS_NAV(); //ent->nav = gi.Nav(); // Grant access to navigation data if (ent->nav) diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 2a1ca9a41..a614ff3c3 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -53,7 +53,7 @@ void(*engine_CvarSync_Set)(int index, const char *name, const char *val); // botlib bsp_t* (*SV_BSP)(void); nav_t* (*CS_NAV)(void); -debug_draw_t* (*Draw)(void); +debug_draw_t* (*CS_DebugDraw)(void); void (*SV_BotUpdateInfo)(char* name, int ping, int score); void (*SV_BotConnect)(char* name); void (*SV_BotDisconnect)(char* name); diff --git a/src/action/g_local.h b/src/action/g_local.h index 60674733c..ed7395cf3 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1363,6 +1363,7 @@ void CvarSync_Set(int index, const char *name, const char *val); //botlib extern bsp_t* (*SV_BSP)(void); extern nav_t* (*CS_NAV)(void); +extern debug_draw_t* (*CS_DebugDraw)(void); extern void (*SV_BotUpdateInfo)(char* name, int ping, int score); extern void (*SV_BotConnect)(char* name); extern void (*SV_BotDisconnect)(char* name); diff --git a/src/action/g_main.c b/src/action/g_main.c index 13dab1195..65e6c560c 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -704,6 +704,7 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) SV_BSP = gix->CheckForExtension("Bsp"); CS_NAV = gix->CheckForExtension("Nav"); + CS_DebugDraw = gix->CheckForExtension("DebugDraw"); SV_BotConnect = gix->CheckForExtension("SV_BotConnect"); SV_BotDisconnect = gix->CheckForExtension("SV_BotDisconnect"); SV_BotUpdateInfo = gix->CheckForExtension("SV_BotUpdateInfo"); diff --git a/src/action/p_client.c b/src/action/p_client.c index 9581fea07..b9ea1e352 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3267,9 +3267,7 @@ void ClientBeginDeathmatch(edict_t * ent) //rekkie -- debug drawing -- s #if DEBUG_DRAWING - rektek_bots_api_v1_t *draw = gix->GetExtension(REKTEK_BOTS_API_V1); - - ent->client->pers.draw = draw->Draw(); + ent->client->pers.draw = CS_DebugDraw(); //if (ent->client->pers.draw) { // Default all to off state diff --git a/src/server/game.c b/src/server/game.c index be0753427..b9152cc72 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -876,11 +876,6 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { #endif static const rektek_bots_api_v1_t rektek_bots_api_v1 = { -// .Bsp = SV_BSP, -// .Nav = CS_NAV, -// #if DEBUG_DRAWING -// .Draw = CS_DebugDraw, -// #endif .SV_BotUpdateInfo = SV_BotUpdateInfo, .SV_BotConnect = SV_BotConnect, .SV_BotDisconnect = SV_BotDisconnect, @@ -1110,6 +1105,7 @@ void G_InitializeExtensions(void) // botlib g_addextension("Bsp", SV_BSP); g_addextension("Nav", CS_NAV); + g_addextension("DebugDraw", CS_DebugDraw); g_addextension("SV_BotConnect", SV_BotConnect); g_addextension("SV_BotDisconnect", SV_BotDisconnect); g_addextension("SV_BotClearClients", SV_BotClearClients); diff --git a/src/server/server.h b/src/server/server.h index e9a0d4803..335f0de00 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -931,6 +931,8 @@ trace_t q_gameabi SV_Clip(const vec3_t start, const vec3_t mins, edict_t *clip, int contentmask); bsp_t* SV_BSP(void); +nav_t* CS_NAV(void); +debug_draw_t* CS_DebugDraw(void); void SV_BotInit(void); void SV_BotUpdateInfo(char* name, int ping, int score); void SV_BotConnect(char* name); From 4f72c24e29332e28866b5384b437c929cf366290 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 18 Sep 2024 16:15:41 -0400 Subject: [PATCH 774/974] Cleaned up old refs that weren't relevant --- src/action/acesrc/acebot_spawn.c | 6 +++--- src/action/botlib/botlib_ai.c | 2 +- src/action/botlib/botlib_spawn.c | 6 +++--- src/action/g_main.c | 6 ------ src/server/game.c | 3 +++ 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index f6a4fcc1d..766977bb8 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -639,7 +639,7 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; @@ -699,7 +699,7 @@ void ACESP_RemoveBot(char *name) if( bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname,name)==0 || (find_team && bot->client->resp.team==find_team)) ) { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -772,7 +772,7 @@ void ACESP_RemoveTeamplayBot(int team) if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index 866a56c27..e46c1e589 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -650,7 +650,7 @@ void BOTLIB_Think(edict_t* self) if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index cdef09ebc..c649b68e8 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1620,7 +1620,7 @@ void BOTLIB_RemoveBot(char* name) if (bot->is_bot && (remove_all || !strlen(name) || Q_stricmp(bot->client->pers.netname, name) == 0 || (find_team && bot->client->resp.team == find_team))) { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -1693,7 +1693,7 @@ void BOTLIB_RemoveBot(char* name) } // Fake Bot Client - Disconnect the bot - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client @@ -1740,7 +1740,7 @@ void BOTLIB_RemoveTeamplayBot(int team) //if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - rektek_bots_api_v1_t *bot_api = gix->GetExtension(REKTEK_BOTS_API_V1); + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/g_main.c b/src/action/g_main.c index 65e6c560c..44237b5c6 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -661,12 +661,6 @@ q_exported game_export_t *GetGameAPI(game_import_t *import) globals.edict_size = sizeof (edict_t); - -#if AQTION_EXTENSION - -#endif - - return &globals; } diff --git a/src/server/game.c b/src/server/game.c index b9152cc72..4a592f999 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -876,6 +876,9 @@ static const debug_draw_api_v1_t debug_draw_api_v1 = { #endif static const rektek_bots_api_v1_t rektek_bots_api_v1 = { + .Bsp = SV_BSP, + .Nav = CS_NAV, + .Draw = CS_DebugDraw, .SV_BotUpdateInfo = SV_BotUpdateInfo, .SV_BotConnect = SV_BotConnect, .SV_BotDisconnect = SV_BotDisconnect, From 84b4d6f79034ceea7fe422938633bcf4a4cc0a44 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 19 Sep 2024 12:19:08 +0300 Subject: [PATCH 775/974] Always enable blend for debug lines. --- src/refresh/debug.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 8449781e3..9398076ec 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -95,11 +95,9 @@ void R_AddDebugLine(const vec3_t start, const vec3_t end, uint32_t color, uint32 l->time = com_localTime2 + time; if (l->time < com_localTime2) l->time = UINT32_MAX; - l->bits = GLS_DEPTHMASK_FALSE; + l->bits = GLS_DEPTHMASK_FALSE | GLS_BLEND_BLEND; if (!depth_test) l->bits |= GLS_DEPTHTEST_DISABLE; - if (gl_config.caps & QGL_CAP_LINE_SMOOTH) - l->bits |= GLS_BLEND_BLEND; } #define GL_DRAWLINE(sx, sy, sz, ex, ey, ez) \ From 30f5792dc139a846017ae004098c65197d04eddf Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 19 Sep 2024 13:10:13 -0400 Subject: [PATCH 776/974] bot_navautogen introduced, maps without navmesh will autogenerate one if enabled --- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib_nodes.c | 299 ++++++++++++++++--------------- src/action/g_main.c | 1 + src/action/g_save.c | 1 + 4 files changed, 154 insertions(+), 148 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 4ddab4a65..6a2abe14d 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -552,6 +552,7 @@ extern cvar_t* bot_debug; extern cvar_t* bot_count_min; extern cvar_t* bot_count_max; extern cvar_t* bot_rotate; +extern cvar_t* bot_navautogen; //extern cvar_t* bot_randteamskin; #define MAX_BOT_NAMES 64 diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index b15bddb7f..cf894b352 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3005,6 +3005,7 @@ void BOTLIB_LoadNavCompressed(void) cvar_t* botdir = gi.cvar("botdir", "bots", 0); // Directory of the bot files in the gamelib const vec3_t mins = { -16, -16, -24 }; const vec3_t maxs = { 16, 16, 32 }; + qboolean navfilefound = true; #ifdef _WIN32 int f; @@ -3027,7 +3028,12 @@ void BOTLIB_LoadNavCompressed(void) if ((fIn = fopen(filename, "rb")) == NULL) // See if .nav file exists { - return; // No file + Com_Printf("%s WARNING: Failed to read or missing NAV file for %s, bots will not navigate the map properly\n", __func__, level.mapname); + navfilefound = false; + if (!bot_navautogen->value) { + gi.dprintf("%s: No NAV file found and bot_navautogen is disabled\n", __func__); + return; // No file and do not auto generate + } } else { @@ -3038,15 +3044,6 @@ void BOTLIB_LoadNavCompressed(void) fclose(fIn); // Close the file return; } - /* - bsp_t* bsp = gi.Bsp(); - fileSize += sizeof(unsigned) * fread(&bsp_checksum, sizeof(unsigned), 1, fIn); // Map checksum - if (bsp_checksum != bsp->checksum) - { - fclose(fIn); // Close the file - return; - } - */ } // Init Nodes @@ -3055,186 +3052,192 @@ void BOTLIB_LoadNavCompressed(void) BOTLIB_FreeAreaNodes(); //Soft map change. Free all area node memory used //memset(&nmesh, 0, sizeof(nmesh_t)); + if (!navfilefound && bot_navautogen->value){ + BOTLIB_SelfExpandNodesFromSpawnpoints(NULL); + BOTLIB_LinkAllNodesTogether(NULL); + Com_Printf("%s INFO: No NAV file found, generating nodes from spawnpoints\n", __func__); + } // Remove doors Remove_All_Doors(); Remove_All_Breakableglass(); - Com_Printf("%s Reading NAV file... detected version [%d]\n", __func__, version); - - // Read Compressed and uncompressed buffer sizes - long uncompressed_buff_len = 0; - fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size - long compressed_buff_len = 0; - fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size - - // Read compressed buffer - char* uncompressed_buff = (char*)malloc(uncompressed_buff_len); - char* compressed_buff = (char*)malloc(compressed_buff_len); - if (compressed_buff != NULL && uncompressed_buff != NULL) - { - fileSize += compressed_buff_len * fread(compressed_buff, compressed_buff_len, 1, fIn); // Compressed buffer + if (navfilefound) { + Com_Printf("%s INFO: Reading NAV file... detected version [%d]\n", __func__, version); - BOTLIB_DecompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); - //Com_Printf("%s compressed_buff_len:%i uncompressed_buff_len:%i\n", __func__, compressed_buff_len, uncompressed_buff_len); + // Read Compressed and uncompressed buffer sizes + long uncompressed_buff_len = 0; + fileSize += sizeof(int) * fread(&uncompressed_buff_len, sizeof(int), 1, fIn); // Uncompressed buffer size + long compressed_buff_len = 0; + fileSize += sizeof(int) * fread(&compressed_buff_len, sizeof(int), 1, fIn); // Compressed buffer size - // Read the uncompressed buffer + // Read compressed buffer + char* uncompressed_buff = (char*)malloc(uncompressed_buff_len); + char* compressed_buff = (char*)malloc(compressed_buff_len); + if (compressed_buff != NULL && uncompressed_buff != NULL) { - int buff_read = 0; - - // Total nodes - memcpy(&numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); - buff_read += sizeof(unsigned short); + fileSize += compressed_buff_len * fread(compressed_buff, compressed_buff_len, 1, fIn); // Compressed buffer - if (numnodes + 1 > MAX_PNODES) - { - Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); - return; - } + BOTLIB_DecompressBuffer(compressed_buff, compressed_buff_len, uncompressed_buff, &uncompressed_buff_len); + //Com_Printf("%s compressed_buff_len:%i uncompressed_buff_len:%i\n", __func__, compressed_buff_len, uncompressed_buff_len); - // Alloc nodes - nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory - if (nodes == NULL) + // Read the uncompressed buffer { - Com_Printf("%s failed to malloc nodes\n", __func__); - return; - } + int buff_read = 0; - // Total areas - //nav_area.total_areas = 0; + // Total nodes + memcpy(&numnodes, uncompressed_buff + buff_read, sizeof(unsigned short)); + buff_read += sizeof(unsigned short); - //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, version, numnodes); - - // Read node data - for (n = 0; n < numnodes; n++) - { - if (version > BOT_NAV_VERSION_1) + if (numnodes + 1 > MAX_PNODES) { - // Area/chunk/group this node belongs to -- optimise and diversify bot pathing - memcpy(&nodes[n].area, uncompressed_buff + buff_read, sizeof(int)); // Node area - buff_read += sizeof(int); - - nodes[n].area_color = 0; + Com_Printf("%s ERROR: Too many nodes in NAV file\n", __func__); + return; } - //VectorClear(nodes[n].normal); - //if (version > BOT_NAV_VERSION_2) - //{ - //memcpy(&nodes[n].normal, uncompressed_buff + buff_read, sizeof(vec3_t)); // Surface normal - //buff_read += sizeof(vec3_t); - //} - nodes[n].weight = 0; // Used to help diversify bot pathing - - memcpy(&nodes[n].origin, uncompressed_buff + buff_read, sizeof(vec3_t)); // Location - buff_read += sizeof(vec3_t); - memcpy(&nodes[n].type, uncompressed_buff + buff_read, sizeof(byte)); // Node type - buff_read += sizeof(byte); - - VectorCopy(mins, nodes[n].mins); // Box mins - VectorCopy(maxs, nodes[n].maxs); // Box maxs - if (nodes[n].type == NODE_CROUCH) - nodes[n].maxs[2] = CROUCHING_MAXS2; - else if (nodes[n].type == NODE_BOXJUMP) + // Alloc nodes + nodes = (node_t*)malloc(sizeof(node_t) * (numnodes + 1)); // Alloc memory + if (nodes == NULL) { - nodes[n].mins[0] = -8; - nodes[n].mins[1] = -8; - nodes[n].mins[2] = -12; - nodes[n].maxs[0] = 8; - nodes[n].maxs[1] = 8; - nodes[n].maxs[2] = 16; + Com_Printf("%s failed to malloc nodes\n", __func__); + return; } - VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world - VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world - memcpy(&nodes[n].nodenum, uncompressed_buff + buff_read, sizeof(short int)); // Node number - buff_read += sizeof(short int); - memcpy(&nodes[n].inuse, uncompressed_buff + buff_read, sizeof(qboolean)); // Node inuse - buff_read += sizeof(qboolean); + // Total areas + //nav_area.total_areas = 0; + + //Com_Printf("%s buff_version:%i buff_numnodes:%i\n", __func__, version, numnodes); - // Init MAXLINKS links - for (l = 0; l < MAXLINKS; l++) + // Read node data + for (n = 0; n < numnodes; n++) { - nodes[n].links[l].targetNode = INVALID; // Link - nodes[n].links[l].targetNodeType = INVALID; // Type - nodes[n].links[l].cost = INVALID; // Cost - } + if (version > BOT_NAV_VERSION_1) + { + // Area/chunk/group this node belongs to -- optimise and diversify bot pathing + memcpy(&nodes[n].area, uncompressed_buff + buff_read, sizeof(int)); // Node area + buff_read += sizeof(int); - // Read the num_links - memcpy(&nodes[n].num_links, uncompressed_buff + buff_read, sizeof(byte)); - buff_read += sizeof(byte); - //Com_Printf("%s origin:%f %f %f type:%i nodenum:%i inuse:%i num_links:%i\n", __func__, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2], nodes[n].type, nodes[n].nodenum, nodes[n].inuse, nodes[n].num_links); + nodes[n].area_color = 0; + } + //VectorClear(nodes[n].normal); + //if (version > BOT_NAV_VERSION_2) + //{ + //memcpy(&nodes[n].normal, uncompressed_buff + buff_read, sizeof(vec3_t)); // Surface normal + //buff_read += sizeof(vec3_t); + //} + + nodes[n].weight = 0; // Used to help diversify bot pathing + + memcpy(&nodes[n].origin, uncompressed_buff + buff_read, sizeof(vec3_t)); // Location + buff_read += sizeof(vec3_t); + memcpy(&nodes[n].type, uncompressed_buff + buff_read, sizeof(byte)); // Node type + buff_read += sizeof(byte); - for (l = 0; l < nodes[n].num_links; l++) // - { - memcpy(&nodes[n].links[l].targetNode, uncompressed_buff + buff_read, sizeof(short int)); + VectorCopy(mins, nodes[n].mins); // Box mins + VectorCopy(maxs, nodes[n].maxs); // Box maxs + if (nodes[n].type == NODE_CROUCH) + nodes[n].maxs[2] = CROUCHING_MAXS2; + else if (nodes[n].type == NODE_BOXJUMP) + { + nodes[n].mins[0] = -8; + nodes[n].mins[1] = -8; + nodes[n].mins[2] = -12; + nodes[n].maxs[0] = 8; + nodes[n].maxs[1] = 8; + nodes[n].maxs[2] = 16; + } + VectorAdd(nodes[n].origin, mins, nodes[n].absmin); // Update absolute box min/max in the world + VectorAdd(nodes[n].origin, maxs, nodes[n].absmax); // Update absolute box min/max in the world + + memcpy(&nodes[n].nodenum, uncompressed_buff + buff_read, sizeof(short int)); // Node number buff_read += sizeof(short int); - memcpy(&nodes[n].links[l].targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + memcpy(&nodes[n].inuse, uncompressed_buff + buff_read, sizeof(qboolean)); // Node inuse + buff_read += sizeof(qboolean); + + // Init MAXLINKS links + for (l = 0; l < MAXLINKS; l++) + { + nodes[n].links[l].targetNode = INVALID; // Link + nodes[n].links[l].targetNodeType = INVALID; // Type + nodes[n].links[l].cost = INVALID; // Cost + } + + // Read the num_links + memcpy(&nodes[n].num_links, uncompressed_buff + buff_read, sizeof(byte)); buff_read += sizeof(byte); - memcpy(&nodes[n].links[l].cost, uncompressed_buff + buff_read, sizeof(float)); - buff_read += sizeof(float); - //Com_Printf("%s targetNode:%i targetNodeType:%i cost:%f\n", __func__, nodes[n].links[l].targetNode, nodes[n].links[l].targetNodeType, nodes[n].links[l].cost); - } + //Com_Printf("%s origin:%f %f %f type:%i nodenum:%i inuse:%i num_links:%i\n", __func__, nodes[n].origin[0], nodes[n].origin[1], nodes[n].origin[2], nodes[n].type, nodes[n].nodenum, nodes[n].inuse, nodes[n].num_links); + + for (l = 0; l < nodes[n].num_links; l++) // + { + memcpy(&nodes[n].links[l].targetNode, uncompressed_buff + buff_read, sizeof(short int)); + buff_read += sizeof(short int); + memcpy(&nodes[n].links[l].targetNodeType, uncompressed_buff + buff_read, sizeof(byte)); + buff_read += sizeof(byte); + memcpy(&nodes[n].links[l].cost, uncompressed_buff + buff_read, sizeof(float)); + buff_read += sizeof(float); + //Com_Printf("%s targetNode:%i targetNodeType:%i cost:%f\n", __func__, nodes[n].links[l].targetNode, nodes[n].links[l].targetNodeType, nodes[n].links[l].cost); + } - // Paths - //for (p = 0; p < numnodes; p++) - //{ - //memcpy(&path_table[n][p], uncompressed_buff + buff_read, sizeof(short int)); // Path table - //buff_read += sizeof(short int); - //Com_Printf("%s path_table[%i][%i]:%i\n", __func__, n, p, path_table[n][p]); - //} + // Paths + //for (p = 0; p < numnodes; p++) + //{ + //memcpy(&path_table[n][p], uncompressed_buff + buff_read, sizeof(short int)); // Path table + //buff_read += sizeof(short int); + //Com_Printf("%s path_table[%i][%i]:%i\n", __func__, n, p, path_table[n][p]); + //} + } } } - } - // Free memory - if (compressed_buff != NULL) - free(compressed_buff); - if (uncompressed_buff != NULL) - free(uncompressed_buff); + // Free memory + if (compressed_buff != NULL) + free(compressed_buff); + if (uncompressed_buff != NULL) + free(uncompressed_buff); - // Fix any dangling or unusual links - if (numnodes) - { - for (int i = 0; i < numnodes; i++) + // Fix any dangling or unusual links + if (numnodes) { - if (nodes[i].inuse == false) - BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node - - if (nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) - BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node - - /* - for (int l = 0; l < nodes[i].num_links; l++) + for (int i = 0; i < numnodes; i++) { - int tnode = nodes[i].links[l].targetNode; + if (nodes[i].inuse == false) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node + + if (nodes[i].origin[0] == 0 && nodes[i].origin[1] == 0 && nodes[i].origin[2] == 0) + BOTLIB_RemoveAllNodeLinksFrom(nodes[i].nodenum); // Remove all links to and from this node - if (nodes[tnode].origin[0] == 0 && nodes[tnode].origin[1] == 0 && nodes[tnode].origin[2] == 0) + /* + for (int l = 0; l < nodes[i].num_links; l++) { - BOTLIB_RemoveAllNodeLinksFrom(nodes[tnode].nodenum); // Remove all links to and from this node + int tnode = nodes[i].links[l].targetNode; + + if (nodes[tnode].origin[0] == 0 && nodes[tnode].origin[1] == 0 && nodes[tnode].origin[2] == 0) + { + BOTLIB_RemoveAllNodeLinksFrom(nodes[tnode].nodenum); // Remove all links to and from this node + } } + */ } - */ } - } - fclose(fIn); + fclose(fIn); - Com_Printf("%s loaded %s containing %d nodes.\n", __func__, filename, numnodes); + Com_Printf("%s loaded %s containing %d nodes.\n", __func__, filename, numnodes); - // Update old versions to new format - if (version == BOT_NAV_VERSION_1) - { - for (int i = 0; i < numnodes; i++) + // Update old versions to new format + if (version == BOT_NAV_VERSION_1) + { + for (int i = 0; i < numnodes; i++) + { + nodes[i].area = 0; + } + } + // Save changes to file + if (version < BOT_NAV_VERSION) { - nodes[i].area = 0; + Com_Printf("%s Updating NAV file to new version [%d]\n", __func__, BOT_NAV_VERSION); + BOTLIB_SaveNavCompressed(); } } - // Save changes to file - if (version < BOT_NAV_VERSION) - { - Com_Printf("%s Updating NAV file to new version [%d]\n", __func__, BOT_NAV_VERSION); - BOTLIB_SaveNavCompressed(); - } - //BOTLIB_SetAllNodeNormals(); // Set all the normals // Init Areas (must be done after nodes are loaded) diff --git a/src/action/g_main.c b/src/action/g_main.c index 42a8d30ba..9325ee570 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -502,6 +502,7 @@ cvar_t* bot_debug; // Enable bot debug mode cvar_t* bot_count_min; // Minimum number of bots to keep on the server (will range between this and bot_count_max) cvar_t* bot_count_max; // Maximum number of bots to keep on the server (will range between this and bot_count_min) cvar_t* bot_rotate; // Disable/enable rotating bots on the server +cvar_t* bot_navautogen; // Enable/Disable automatic generation of navigation files //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 57174eaf5..ab58b11ee 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -700,6 +700,7 @@ void InitGame( void ) bot_count_min = gi.cvar("bot_count_min", "0", 0); bot_count_max = gi.cvar("bot_count_max", "0", 0); bot_rotate = gi.cvar("bot_rotate", "0", 0); + bot_navautogen = gi.cvar("bot_navautogen", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From 958598292de6d8b42468971fbf9fdaa4338f3d06 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 20 Sep 2024 18:59:04 +0300 Subject: [PATCH 777/974] Improve debug text drawing. Center text lines at origin. Frustum cull each line individually. Break oriented text into lines to allow for longer text. --- src/refresh/debug.c | 90 +++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index 9398076ec..e7a7338e2 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -42,7 +42,7 @@ typedef struct { uint32_t color; uint32_t time; glStateBits_t bits; - char text[MAX_QPATH]; + char text[128]; } debug_text_t; static debug_text_t debug_texts[MAX_DEBUG_TEXTS]; @@ -325,9 +325,13 @@ void R_AddDebugCurveArrow(const vec3_t start, const vec3_t ctrl, const vec3_t en } } -void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, - float size, uint32_t color, uint32_t time, qboolean depth_test) +static void R_AddDebugTextInternal(const vec3_t origin, const vec3_t angles, const char *text, + size_t len, float size, uint32_t color, uint32_t time, + qboolean depth_test) { + if (!len) + return; + debug_text_t *t = LIST_FIRST(debug_text_t, &debug_texts_free, entry); if (LIST_EMPTY(&debug_texts_free)) { @@ -367,7 +371,39 @@ void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, t->bits |= GLS_DEPTHTEST_DISABLE; if (angles) t->bits |= GLS_CULL_DISABLE; - Q_strlcpy(t->text, text, sizeof(t->text)); + len = min(len, sizeof(t->text) - 1); + memcpy(t->text, text, len); + t->text[len] = 0; +} + +void R_AddDebugText(const vec3_t origin, const vec3_t angles, const char *text, + float size, uint32_t color, uint32_t time, qboolean depth_test) +{ + vec3_t down, pos, up; + const char *s, *p; + + if (!angles) { + R_AddDebugTextInternal(origin, angles, text, strlen(text), size, color, time, depth_test); + return; + } + + AngleVectors(angles, NULL, NULL, up); + VectorScale(up, -size, down); + + VectorCopy(origin, pos); + + // break oriented text into lines to allow for longer text + s = text; + while (*s) { + p = strchr(s, '\n'); + if (!p) { + R_AddDebugTextInternal(pos, angles, s, strlen(s), size, color, time, depth_test); + break; + } + R_AddDebugTextInternal(pos, angles, s, p - s, size, color, time, depth_test); + VectorAdd(pos, down, pos); + s = p + 1; + } } static void GL_DrawDebugLines(void) @@ -504,6 +540,25 @@ static void GL_DrawDebugChar(const vec3_t pos, const vec3_t right, const vec3_t tess.flags = bits; } +static void GL_DrawDebugTextLine(const vec3_t origin, const vec3_t right, const vec3_t down, + const debug_text_t *text, const char *s, size_t len) +{ + // frustum cull + float radius = text->size * 0.5f * len; + for (int i = 0; i < 4; i++) + if (PlaneDiff(origin, &glr.frustumPlanes[i]) < -radius) + return; + + // draw it + vec3_t pos; + VectorMA(origin, -0.5f * len, right, pos); + while (*s && len--) { + byte c = *s++; + GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); + VectorAdd(pos, right, pos); + } +} + static void GL_DrawDebugTexts(void) { debug_text_t *text, *next; @@ -516,9 +571,7 @@ static void GL_DrawDebugTexts(void) LIST_FOR_EACH_SAFE(debug_text_t, text, next, &debug_texts_active, entry) { vec3_t right, down, pos; - const char *s; - float radius; - int i; + const char *s, *p; if (text->time < com_localTime2) { // expired List_Remove(&text->entry); @@ -531,14 +584,6 @@ static void GL_DrawDebugTexts(void) if (text->size < DotProduct(pos, glr.viewaxis[0]) * gl_debug_distfrac->value) continue; - // frustum cull - radius = strlen(text->text) * text->size; - for (i = 0; i < 4; i++) - if (PlaneDiff(text->origin, &glr.frustumPlanes[i]) < -radius) - break; - if (i != 4) - continue; - if (text->bits & GLS_CULL_DISABLE) { // oriented vec3_t up; AngleVectors(text->angles, NULL, right, up); @@ -550,17 +595,16 @@ static void GL_DrawDebugTexts(void) } VectorCopy(text->origin, pos); - i = 0; s = text->text; while (*s) { - byte c = *s++; - if (c == '\n') { - i++; - VectorMA(text->origin, i, down, pos); - continue; + p = strchr(s, '\n'); + if (!p) { + GL_DrawDebugTextLine(pos, right, down, text, s, strlen(s)); + break; } - GL_DrawDebugChar(pos, right, down, text->bits, text->color, c); - VectorAdd(pos, right, pos); + GL_DrawDebugTextLine(pos, right, down, text, s, p - s); + VectorAdd(pos, down, pos); + s = p + 1; } } From 3df324d5d2997feb747ce68ee1d7ae7b1da41270 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 20 Sep 2024 13:38:22 -0400 Subject: [PATCH 778/974] Changed info messaging slightly --- src/action/botlib/botlib_nodes.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index cf894b352..121720f8e 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -3031,7 +3031,7 @@ void BOTLIB_LoadNavCompressed(void) Com_Printf("%s WARNING: Failed to read or missing NAV file for %s, bots will not navigate the map properly\n", __func__, level.mapname); navfilefound = false; if (!bot_navautogen->value) { - gi.dprintf("%s: No NAV file found and bot_navautogen is disabled\n", __func__); + gi.dprintf("%s INFO: No NAV file found and bot_navautogen is disabled\n", __func__); return; // No file and do not auto generate } } @@ -3055,7 +3055,7 @@ void BOTLIB_LoadNavCompressed(void) if (!navfilefound && bot_navautogen->value){ BOTLIB_SelfExpandNodesFromSpawnpoints(NULL); BOTLIB_LinkAllNodesTogether(NULL); - Com_Printf("%s INFO: No NAV file found, generating nodes from spawnpoints\n", __func__); + Com_Printf("%s INFO: No NAV file found, bot_navautogen is enabled; generating nodes from spawnpoints\n", __func__); } // Remove doors Remove_All_Doors(); From d1a4ddf32775cf4dd36aba6329fccc956003aed8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 20 Sep 2024 14:01:12 -0400 Subject: [PATCH 779/974] Added botlib documentation --- doc/action.md | 243 ++++++++++++++++++++++++++------------------------ 1 file changed, 125 insertions(+), 118 deletions(-) diff --git a/doc/action.md b/doc/action.md index 2523b48ed..70ec6b395 100644 --- a/doc/action.md +++ b/doc/action.md @@ -6,126 +6,107 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team # Table of Contents -1. [Introduction](#introduction) -2. [Installation](#installation) -3. [Features](#features) +- [Action Quake 2: The Next Generation](#action-quake-2-the-next-generation) +- [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Installation](#installation) + - [Features](#features) - [Mapvoting](#mapvoting) - - [Commands](#commands) + - [Commands](#commands) - [Kickvoting](#kickvoting) - - [Commands](#commands-1) + - [Commands](#commands-1) - [Configvoting](#configvoting) - - [Commands](#commands-2) -5. [Teamplay](#teamplay) - - [Commands](#commands-3) -6. [Tourney](#tourney) - - [Commands](#commands-3) -7. [3 Teams Teamplay](#3-teams-teamplay) - - [Commands](#commands-4) -8. [Capture the Flag](#capture-the-flag) - - [Commands](#commands-5) -9. [Domination](#domination) - - [Commands](#commands-6) -10. [Matchmode](#matchmode) - - [Commands](#commands-7) -11. [Voice Command](#voice-command) - - [Commands](#commands-8) -12. [Low Lag Sounds](#low-lag-sounds) - - [Commands](#commands-9) -13. [Announcer](#announcer) - - [Commands](#commands-10) -14. [Kevlar Helmet](#kevlar-helmet) -15. [Single Barreled Handcannon](#single-barreled-handcannon) - - [Commands](#commands-11) -16. [Enemy Down Radio Reporting](#enemy-down-radio-reporting) -17. [Player Ignoring](#player-ignoring) - - [Commands](#commands-12) -18. [Video Setting Checking](#video-setting-checking) - - [Commands](#commands-13) -19. [Location Files](#location-files) -20. [Punching](#punching) - - [Commands](#commands-14) -21. [Lens command](#lens-command) - - [Commands](#commands-15) -22. [New Say Variables](#new-say-variables) -23. [Roundtimeleft](#roundtimeleft) - - [Commands](#commands-16) -24. [Time](#time) - - [Commands](#commands-17) -25. [sv stuffcmd](#sv-stuffcmd) - - [Commands](#commands-18) -26. [Grenade Strength](#grenade-strength) - - [Commands](#commands-19) -27. [Total Kills](#total-kills) -28. [Random Rotation](#random-rotation) - - [Commands](#commands-20) -29. [Vote Rotation](#vote-rotation) - - [Commands](#commands-21) -30. [MapVote Next](#mapvote-next) - - [Commands](#commands-22) -31. [Empty Rotate](#empty-rotate) - - [Commands](#commands-23) -32. [Bandage Text](#bandage-text) -33. [Deathmatch Weapon](#deathmatch-weapon) - - [Commands](#commands-24) -34. [Control Characters](#control-characters) - - [Commands](#commands-25) -35. [Anti Camping](#anti-camping) - - [Commands](#commands-26) -36. [Anti Idle](#anti-idle) - - [Commands](#commands-27) -37. [Gibs](#gibs) - - [Commands](#commands-28) -38. [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) -39. [Weapon Banning](#weapon-banning) - - [Commands](#commands-29) -40. [Item Banning](#item-banning) - - [Commands](#commands-30) -41. [Teamkilling after a Round](#teamkilling-after-a-round) - - [Commands](#commands-31) -42. [New IR Vision](#new-ir-vision) - - [Commands](#commands-32) -43. [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) - - [Commands](#commands-33) -44. [Darkmatch](#darkmatch) - - [Commands](#commands-34) -45. [Map Restarting](#map-restarting) - - [Commands](#commands-35) -46. [Statistics](#statistics) - - [Commands](#commands-36) -47. [Automatic Joining/Equipping](#automatic-joining-equipping) - - [Commands](#commands-37) -48. [Automatic Demo Recording](#automatic-demo-recording) - - [Commands](#commands-38) -49. [Spawn Code](#spawn-code) - - [Commands](#commands-39) -50. [Ghost](#ghost) - - [Commands](#commands-40) -51. [Bandolier behavior](#bandolier-behavior) - - [Commands](#commands-41) -52. [Bots](#bots) - - [Commands](#commands-42) -53. [Slap](#slap) - - [Commands](#commands-43) -54. [Variable Framerate Support](#variable-framerate-support) - - [Commands](#commands-44) -55. [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) -56. [Latency Compensation](#latency-compensation) - - [Commands](#commands-45) -57. [General quality of life improvements](#general-quality-of-life-improvements) -58. [Client Prediction](#client-prediction) -59. [Force Spawn items](#force-spawn-items) -60. [Attract Mode](#attract-mode) - - [Commands](#commands-46) -61. [Zoom Compensation](#zoom-compensation) -62. [Warmup](#warmup) - - [Commands](#commands-47) -63. [Item Kit Mode](#item-kit-mode) -64. [Print rules](#print-rules) - - [Commands](#commands-48) -65. [Espionage](#espionage) - - [Commands](#commands-49) -66. [Contact Information](#contact-information) -67. [Credits](#credits) + - [Commands](#commands-2) + - [Teamplay](#teamplay) + - [Commands](#commands-3) + - [Tourney](#tourney) + - [Commands](#commands-4) + - [3 Teams Teamplay](#3-teams-teamplay) + - [Commands](#commands-5) + - [Capture the Flag](#capture-the-flag) + - [Commands](#commands-6) + - [Domination](#domination) + - [Commands](#commands-7) + - [Matchmode](#matchmode) + - [Commands](#commands-8) + - [Voice Command](#voice-command) + - [Commands](#commands-9) + - [Low Lag Sounds](#low-lag-sounds) + - [Commands](#commands-10) + - [Announcer](#announcer) + - [Commands](#commands-11) + - [Kevlar Helmet](#kevlar-helmet) + - [Single Barreled Handcannon](#single-barreled-handcannon) + - [Commands](#commands-12) + - [Enemy Down Radio Reporting](#enemy-down-radio-reporting) + - [Player Ignoring](#player-ignoring) + - [Commands](#commands-13) + - [Video Setting Checking](#video-setting-checking) + - [Commands](#commands-14) + - [Location Files](#location-files) + - [Punching](#punching) + - [Commands](#commands-15) + - [Lens command](#lens-command) + - [Commands](#commands-16) + - [New Say Variables](#new-say-variables) + - [Time and Roundtimeleft](#time-and-roundtimeleft) + - [Commands](#commands-17) + - [sv stuffcmd](#sv-stuffcmd) + - [Commands](#commands-18) + - [Grenade Strength](#grenade-strength) + - [Commands](#commands-19) + - [Total Kills](#total-kills) + - [Random Rotation](#random-rotation) + - [Commands](#commands-20) + - [Vote Rotation](#vote-rotation) + - [Commands](#commands-21) + - [MapVote Next](#mapvote-next) + - [Commands](#commands-22) + - [Empty Rotate](#empty-rotate) + - [Commands](#commands-23) + - [Bandage Text](#bandage-text) + - [Deathmatch Weapon](#deathmatch-weapon) + - [Commands](#commands-24) + - [Control Characters](#control-characters) + - [Commands](#commands-25) + - [Anti Camping](#anti-camping) + - [Commands](#commands-26) + - [Anti Idle](#anti-idle) + - [Commands](#commands-27) + - [Gibs](#gibs) + - [Commands](#commands-28) + - [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) + - [Weapon Banning](#weapon-banning) + - [Item Banning](#item-banning) + - [Teamkilling after a Round](#teamkilling-after-a-round) + - [New IR Vision](#new-ir-vision) + - [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) + - [Darkmatch](#darkmatch) + - [Map Restarting](#map-restarting) + - [Statistics](#statistics) + - [Automatic Joining/Equipping/Menu](#automatic-joiningequippingmenu) + - [Automatic Demo Recording](#automatic-demo-recording) + - [Spawn Code](#spawn-code) + - [Ghost](#ghost) + - [Bandolier behavior](#bandolier-behavior) + - [Bots](#bots) + - [Legacy LTK Bots](#legacy-ltk-bots) + - [BOTLIB (Rektek) Bots](#botlib-rektek-bots) + - [Slap](#slap) + - [Variable Framerate Support](#variable-framerate-support) + - [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) + - [Latency Compensation](#latency-compensation) + - [General quality of life improvements](#general-quality-of-life-improvements) + - [Client Prediction](#client-prediction) + - [Force Spawn items](#force-spawn-items) + - [Zoom Compensation](#zoom-compensation) + - [Warmup](#warmup) + - [Item Kit Mode](#item-kit-mode) + - [Print rules](#print-rules) + - [Espionage](#espionage) + - [Gun mechanics/enhancements](#gun-mechanicsenhancements) + - [Contact Information](#contact-information) + - [Credits](#credits) --- ## Introduction @@ -590,7 +571,9 @@ TNG updates the way the bandolier behaves when dropping it. It will prevent peop `use_buggy_bandolier [0/1]` - if you wish to revert back to the old bandolier behavior, set this to 1 ### Bots -Now you can fill out your teams with bots, or create an entire team of bots to fight. You can define persistent bots in bots/botdata.cfg, or create and remove them on demand. Bots wander pretty stupidly unless you create a linked network of nodes for them to follow. + +#### Legacy LTK Bots +Now you can fill out your teams with bots, or create an entire team of bots to fight. You can define persistent bots in bots/botdata.cfg, or create and remove them on demand. Bots wander pretty stupidly unless you create a linked network of nodes for them to follow. This documentation exists for reference only, LTK bots have been replaced by the BOTLIB Bots as described below. **Commands:** - `sv addbot [team] [name]` - add a bot for the duration of the current map @@ -610,6 +593,30 @@ Now you can fill out your teams with bots, or create an entire team of bots to f - `sv loadnodes` - load saved nodes from terrain/.ltk - `sv savenodes` - save all nodes and links for this map to terrain/.ltk +#### BOTLIB (Rektek) Bots +Taking aspects of the existing bots and greatly enhancing their navigation and behavioral capabilities, the new botlib bots have superceded the legacy LTK bots. This is a very new system and as such will be in a regular flux of change, enhancements and adjustments. + +**Server Commands:** +- `sv bots <#> [#]` - Server command, if command is issued without arguments, it will print out the existing bot counts, bots on teams and other information. If provided with a single value (`sv bots 3`) this will add 3 bots to the game. If this is a team game, it should auto balance the bots across teams. If provided with two values (`sv bots 3 1`) this will assign 3 bots to team 1. Please be aware of your maxclients limits as the server console will alert you if you have added too many bots. +- `bot_remember <#>` - Server cvar, how long (in seconds) the bot remembers an enemy after visibility has been lost. This is experimental and being reevaluated for usability, and may be replaced in the future +- `bot_reaction <#>` - Server cvar, how long (in seconds) until the bot reacts to an enemy in sight. Lower values mean a faster reaction. This is experimental and being reevaluated for usability, and may be replaced in the future +- `bot_randvoice <#>` - Server cvar, percentage chance that bots use random user voice wavs [min: 0 max: 100]. Suggest disabling or setting to a very low value because user voice wavs take up limited sound slots on non-extended protocol servers (256 max sounds versus 2048) -- if a server runs out of sound slots and tries to cache another sound, it will crash. Looking to re-evaluate how to handle this on non-extended protocol servers. +- `bot_randname [0/1]` - Server cvar, allow bots to pick a random name. Suggest keeping enabled +- `bot_chat [0/1]` - Server cvar, enables bot chat. +- `bot_countashuman [0/1]` - Server cvar, enabling this will allow teamplay-based games to progress without humans being in the server. Set to 0 to force games to not count bots as clients in terms of teamplay +- `bot_navautogen [0/1]` - Server cvar, enabling this will auto generate a navmesh for any maps that do not already have one, on map load time. This automatic navmesh is far from perfect, but it does allow bots to traverse maps rather than stand still. A far superior option is to have a handcrafted navmesh for the map. +- `bot_debug [0/1]` - Server cvar, will enable debug messaging for BOTLIB functionality where enabled + +**Client Commands:** +These commands only work on local map loads, not connections to dedicated servers. They are meant to be used to create navmeshes. +- `nav_edit` - Enters nav edit mode. Enter again to toggle out of nav edit mode. +- `nav_toggle` - Toggles nav visibility: None, nodes, and bot paths +- `nav_autogen` - If in nav_edit mode, will automatically generate a navmesh that will interconnect nodes that are reachable via info_player_deathmatch entities. This performs 90% of the work for you, so that most flat surfaces will have a full mesh ready to use. +- `nav_save` - This saves the current navmesh to `bots/nav/mapname.nav` +- `nav_load` - This will load a navmesh from `bots/nav/mapname.nav`. Loading a map that already has a navmesh will perform this automatically, there is no need to run this command except to possibly revert to an earlier navmesh if you make a mistake and want to undo +**Nav Interface** +When generating a navmesh, you will use the `reload` key to cycle through different options (add node, remove node, link node, etc.). Use the `+attack` key to interact with the navmesh. More documentation around performing this is coming soon. + ### Slap Server admins can slap players into the air, and optionally deal damage. From 2e4b1bcee81393503b0f4729238202807fa5afa7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 20 Sep 2024 14:11:12 -0400 Subject: [PATCH 780/974] Removed unnecessary qhandles --- src/client/tent.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/tent.c b/src/client/tent.c index ab90185b3..51de931bb 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -38,10 +38,6 @@ qhandle_t cl_sfx_lightning; qhandle_t cl_sfx_disrexp; qhandle_t cl_sfx_hit_marker; -qhandle_t cl_sfx_headshot; -qhandle_t cl_sfx_chestshot; -qhandle_t cl_sfx_stomshot; -qhandle_t cl_sfx_legshot; //qhandle_t cl_mod_explode; qhandle_t cl_mod_smoke; @@ -325,6 +321,7 @@ void CL_RegisterTEntSounds(void) cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); cl_sfx_hit_marker = S_RegisterSound("hitsounds/damage.wav"); +}; static const char *const muzzlenames[MFLASH_TOTAL] = { [MFLASH_MACHN] = "v_machn", From cf407ef766df73d9e4c3159a5869def1d0cc030d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 20 Sep 2024 14:22:38 -0400 Subject: [PATCH 781/974] Set hit marker sound to weapons/marker.wav --- src/client/tent.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/tent.c b/src/client/tent.c index 51de931bb..a64642859 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -320,7 +320,7 @@ void CL_RegisterTEntSounds(void) cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); - cl_sfx_hit_marker = S_RegisterSound("hitsounds/damage.wav"); + cl_sfx_hit_marker = S_RegisterSound("weapons/marker.wav"); }; static const char *const muzzlenames[MFLASH_TOTAL] = { From 18c029e3f40280ed8d473141fe3a440cb6a66243 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 20 Sep 2024 16:22:48 -0400 Subject: [PATCH 782/974] Set cl_hit_markers default to 0 --- src/client/tent.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/tent.c b/src/client/tent.c index a64642859..a0dfc13c8 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1705,7 +1705,7 @@ void CL_ClearTEnts(void) void CL_InitTEnts(void) { cl_muzzleflashes = Cvar_Get("cl_muzzleflashes", "1", 0); - cl_hit_markers = Cvar_Get("cl_hit_markers", "2", 0); + cl_hit_markers = Cvar_Get("cl_hit_markers", "0", 0); cl_railtrail_type = Cvar_Get("cl_railtrail_type", "0", 0); cl_railtrail_time = Cvar_Get("cl_railtrail_time", "1.0", 0); cl_railtrail_time->changed = cl_timeout_changed; From 5ff82e3e2385893b08bd4fbe505f1b4a621d3913 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 21 Sep 2024 17:20:35 +0300 Subject: [PATCH 783/974] Disable cubemaps by default. They cause some performance hit. Better to enable them separately for re-release. --- doc/client.asciidoc | 4 ++-- src/refresh/texture.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 120be0ed6..c31ae79aa 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -680,8 +680,8 @@ gl_drawsky:: gl_cubemaps:: Enables use of cubemaps for skybox drawing. Cubemaps allow multiple skybox textures per map, and help to avoid sky rendering bugs. Also enables N64 - skies (although these aren't technically cubemaps). Only effective if - ‘gl_shaders’ is enabled. Default value is 1 (enabled). + skies in re-release maps. Only effective if ‘gl_shaders’ is enabled. + Default value is 0 (disabled). gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 4a5e501dd..13dc1ffdc 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -1229,7 +1229,7 @@ void GL_InitImages(void) gl_gamma = Cvar_Get("vid_gamma", "1", CVAR_ARCHIVE); gl_partshape = Cvar_Get("gl_partshape", "0", 0); gl_partshape->changed = gl_partshape_changed; - gl_cubemaps = Cvar_Get("gl_cubemaps", "1", CVAR_FILES); + gl_cubemaps = Cvar_Get("gl_cubemaps", "0", CVAR_FILES); if (r_config.flags & QVF_GAMMARAMP) { gl_gamma->changed = gl_gamma_changed; From 4208686974d5a2fb313bbcee7cb707ac151a740a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 22 Sep 2024 12:17:53 +0300 Subject: [PATCH 784/974] Fix double assignment. --- src/refresh/surf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/surf.c b/src/refresh/surf.c index ff79651ad..10b06a463 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -743,7 +743,7 @@ static void sample_surface_verts(mface_t *surf, vec_t *vbo) // normalizes and stores lightmap texture coordinates in vertices static void normalize_surface_lmtc(const mface_t *surf, vec_t *vbo) { - float s, t, scale = scale = 1.0f / lm.block_size; + float s, t, scale = 1.0f / lm.block_size; int i; s = surf->light_s + 0.5f; From f6fdb81b3850fc5f0cac9ab9abf15e5474623930 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 22 Sep 2024 12:35:05 +0300 Subject: [PATCH 785/974] Fix missing break statement. --- src/refresh/qgl.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 4c14938b8..486a02bc8 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -457,6 +457,7 @@ static bool parse_gl_version(void) if (!strncmp(s, es_prefixes[i], len)) { s += len; gl_es = true; + break; } } From 45b25f730df88312a18ec512f496fb9e6fbfc99c Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 23 Sep 2024 23:09:55 +0300 Subject: [PATCH 786/974] Make hash map allocations tagged. --- inc/common/hash_map.h | 11 +++++++++-- inc/common/zone.h | 2 +- src/common/hash_map.c | 15 +++++++++------ src/common/zone.c | 4 +++- src/refresh/main.c | 2 +- src/refresh/shader.c | 2 +- src/refresh/texture.c | 2 +- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/inc/common/hash_map.h b/inc/common/hash_map.h index 0d91b87bf..b409e9d3c 100644 --- a/inc/common/hash_map.h +++ b/inc/common/hash_map.h @@ -18,11 +18,16 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#pragma once + +#include "common/zone.h" + typedef struct hash_map_s hash_map_t; hash_map_t *HashMap_CreateImpl(const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher)(const void *const), - bool (*comp)(const void *const, const void *const)); + bool (*comp)(const void *const, const void *const), + memtag_t tag); void HashMap_Destroy(hash_map_t *map); void HashMap_Reserve(hash_map_t *map, uint32_t capacity); bool HashMap_InsertImpl(hash_map_t *map, const uint32_t key_size, const uint32_t value_size, const void *const key, const void *const value); @@ -32,7 +37,9 @@ uint32_t HashMap_Size(const hash_map_t *map); void *HashMap_GetKeyImpl(const hash_map_t *map, uint32_t index); void *HashMap_GetValueImpl(const hash_map_t *map, uint32_t index); -#define HashMap_Create(key_type, value_type, hasher, comp) HashMap_CreateImpl(sizeof(key_type), sizeof(value_type), hasher, comp) +#define HashMap_TagCreate(key_type, value_type, hasher, comp, tag) \ + HashMap_CreateImpl(sizeof(key_type), sizeof(value_type), hasher, comp, tag) +#define HashMap_Create(key_type, value_type, hasher, comp) HashMap_TagCreate(key_type, value_type, hasher, comp, TAG_GENERAL) #define HashMap_Insert(map, key, value) HashMap_InsertImpl(map, sizeof(*key), sizeof(*value), key, value) #define HashMap_Erase(map, key) HashMap_EraseImpl(map, sizeof(*key), key) #define HashMap_Lookup(type, map, key) ((type *)HashMap_LookupImpl(map, sizeof(*key), key)) diff --git a/inc/common/zone.h b/inc/common/zone.h index 0cada9337..774f15543 100644 --- a/inc/common/zone.h +++ b/inc/common/zone.h @@ -45,7 +45,7 @@ void Z_Init(void); void Z_Free(void *ptr); void Z_Freep(void *ptr); void *Z_Realloc(void *ptr, size_t size); -void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size); +void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size, memtag_t tag); q_malloc void *Z_Malloc(size_t size); q_malloc diff --git a/src/common/hash_map.c b/src/common/hash_map.c index 7d311acc1..42231e517 100644 --- a/src/common/hash_map.c +++ b/src/common/hash_map.c @@ -31,6 +31,7 @@ typedef struct hash_map_s { uint32_t key_value_storage_size; uint32_t key_size; uint32_t value_size; + memtag_t tag; uint32_t (*hasher)(const void *const); bool (*comp)(const void *const, const void *const); uint32_t *hash_to_index; @@ -69,7 +70,7 @@ static void HashMap_Rehash(hash_map_t *map, const uint32_t new_size) if (map->hash_size >= new_size) return; map->hash_size = new_size; - map->hash_to_index = Z_ReallocArray(map->hash_to_index, map->hash_size, sizeof(uint32_t)); + map->hash_to_index = Z_ReallocArray(map->hash_to_index, map->hash_size, sizeof(uint32_t), map->tag); memset(map->hash_to_index, 0xFF, map->hash_size * sizeof(uint32_t)); for (uint32_t i = 0; i < map->num_entries; ++i) { void *key = HashMap_GetKeyImpl(map, i); @@ -87,9 +88,9 @@ HashMap_ExpandKeyValueStorage */ static void HashMap_ExpandKeyValueStorage(hash_map_t *map, const uint32_t new_size) { - map->keys = Z_ReallocArray(map->keys, new_size, map->key_size); - map->values = Z_ReallocArray(map->values, new_size, map->value_size); - map->index_chain = Z_ReallocArray(map->index_chain, new_size, sizeof(uint32_t)); + map->keys = Z_ReallocArray(map->keys, new_size, map->key_size, map->tag); + map->values = Z_ReallocArray(map->values, new_size, map->value_size, map->tag); + map->index_chain = Z_ReallocArray(map->index_chain, new_size, sizeof(uint32_t), map->tag); map->key_value_storage_size = new_size; } @@ -100,13 +101,15 @@ HashMap_CreateImpl */ hash_map_t *HashMap_CreateImpl(const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher)(const void *const), - bool (*comp)(const void *const, const void *const)) + bool (*comp)(const void *const, const void *const), + memtag_t tag) { - hash_map_t *map = Z_Mallocz(sizeof(*map)); + hash_map_t *map = Z_TagMallocz(sizeof(*map), tag); map->key_size = key_size; map->value_size = value_size; map->hasher = hasher; map->comp = comp; + map->tag = tag; return map; } diff --git a/src/common/zone.c b/src/common/zone.c index e5f941fb7..2eeee0d00 100644 --- a/src/common/zone.c +++ b/src/common/zone.c @@ -197,9 +197,11 @@ void *Z_Realloc(void *ptr, size_t size) return z + 1; } -void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size) +void *Z_ReallocArray(void *ptr, size_t nmemb, size_t size, memtag_t tag) { Q_assert(!size || nmemb <= INT_MAX / size); + if (!ptr) + return Z_TagMalloc(nmemb * size, tag); return Z_Realloc(ptr, nmemb * size); } diff --git a/src/refresh/main.c b/src/refresh/main.c index 0b70faef3..804f74025 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1051,7 +1051,7 @@ void GL_InitQueries(void) gl_static.samples_passed = GL_ANY_SAMPLES_PASSED; Q_assert(!gl_static.queries); - gl_static.queries = HashMap_Create(int, glquery_t, HashInt32, NULL); + gl_static.queries = HashMap_TagCreate(int, glquery_t, HashInt32, NULL, TAG_RENDERER); } void GL_DeleteQueries(void) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index e216d97a1..c06060cd9 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -465,7 +465,7 @@ static void shader_clear_state(void) static void shader_init(void) { - gl_static.programs = HashMap_Create(glStateBits_t, GLuint, HashInt32, NULL); + gl_static.programs = HashMap_TagCreate(glStateBits_t, GLuint, HashInt32, NULL, TAG_RENDERER); qglGenBuffers(1, &gl_static.uniform_buffer); qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 13dc1ffdc..0e9e8771a 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -918,7 +918,7 @@ int IMG_ReadPixels(screenshot_t *s) s->bpp = bpp; s->rowbytes = rowbytes; - s->pixels = Z_Malloc(buf_size); + s->pixels = Z_TagMalloc(buf_size, TAG_RENDERER); s->width = r_config.width; s->height = r_config.height; From 0d674ae210731695996c85d7dd06191ab8726b85 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 787/974] Templatize BSP loading code. --- src/common/bsp.c | 557 +++----------------------------------- src/common/bsp_template.c | 549 +++++++++++++++++++++++++++++++++++++ 2 files changed, 581 insertions(+), 525 deletions(-) create mode 100644 src/common/bsp_template.c diff --git a/src/common/bsp.c b/src/common/bsp.c index 4a466c01f..4f93f3344 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -45,517 +45,20 @@ static cvar_t *map_visibility_patch; =============================================================================== */ -#define ALLOC(size) \ +#define BSP_ALLOC(size) \ Hunk_Alloc(&bsp->hunk, size) -#define LOAD(func) \ - static int BSP_Load##func(bsp_t *const bsp, const byte *in, \ - const size_t count, const bool extended) - -#define DEBUG(msg) \ +#define BSP_ERROR(msg) \ Com_SetLastError(va("%s: %s", __func__, msg)) -#define ENSURE(cond, msg) \ - do { if (!(cond)) { DEBUG(msg); return Q_ERR_INVALID_FORMAT; } } while (0) - -#define BSP_Short() (in += 2, RL16(in - 2)) -#define BSP_Long() (in += 4, RL32(in - 4)) -#define BSP_Float() LongToFloat(BSP_Long()) - -#define BSP_ExtFloat() (extended ? BSP_Float() : (int16_t)BSP_Short()) -#define BSP_ExtLong() (extended ? BSP_Long() : BSP_Short()) -#define BSP_ExtNull (extended ? (uint32_t)-1 : (uint16_t)-1) - -#define BSP_VectorAdd(v, add) \ - ((v)[0] = BSP_Float() add, (v)[1] = BSP_Float() add, (v)[2] = BSP_Float() add) - -#define BSP_ExtVector(v) \ - ((v)[0] = BSP_ExtFloat(), (v)[1] = BSP_ExtFloat(), (v)[2] = BSP_ExtFloat()) - -#define BSP_Vector(v) BSP_VectorAdd(v,) - -LOAD(Visibility) -{ - if (!count) - return Q_ERR_SUCCESS; - - ENSURE(count >= 4, "Too small header"); - - uint32_t numclusters = BSP_Long(); - ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); - - uint32_t hdrsize = 4 + numclusters * 8; - ENSURE(count >= hdrsize, "Too small header"); - - bsp->numvisibility = count; - bsp->vis = ALLOC(count); - bsp->vis->numclusters = numclusters; - bsp->visrowsize = (numclusters + 7) >> 3; - - for (int i = 0; i < numclusters; i++) { - for (int j = 0; j < 2; j++) { - uint32_t bitofs = BSP_Long(); - ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); - bsp->vis->bitofs[i][j] = bitofs; - } - } - - memcpy(bsp->vis->bitofs + numclusters, in, count - hdrsize); - - return Q_ERR_SUCCESS; -} - -LOAD(Texinfo) -{ - mtexinfo_t *out; - - bsp->numtexinfo = count; - bsp->texinfo = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { -#if USE_REF - for (int j = 0; j < 2; j++) { - BSP_Vector(out->axis[j]); - out->offset[j] = BSP_Float(); - } -#else - in += 32; -#endif - out->c.flags = BSP_Long(); - out->c.value = BSP_Long(); - - memcpy(out->c.name, in, sizeof(out->c.name) - 1); - memcpy(out->name, in, sizeof(out->name) - 1); - in += MAX_TEXNAME; - -#if USE_REF - int32_t next = (int32_t)BSP_Long(); - if (next > 0) { - ENSURE(next < count, "Bad anim chain"); - out->next = bsp->texinfo + next; - } else { - out->next = NULL; - } -#else - in += 4; -#endif - } - -#if USE_REF - // count animation frames - out = bsp->texinfo; - for (int i = 0; i < count; i++, out++) { - out->numframes = 1; - for (mtexinfo_t *step = out->next; step && step != out; step = step->next) { - if (out->numframes == count) { - DEBUG("Infinite anim chain"); - return Q_ERR_INFINITE_LOOP; - } - out->numframes++; - } - } -#endif - - return Q_ERR_SUCCESS; -} - -LOAD(Planes) -{ - cplane_t *out; - - bsp->numplanes = count; - bsp->planes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, in += 4, out++) { - BSP_Vector(out->normal); - out->dist = BSP_Float(); - SetPlaneType(out); - SetPlaneSignbits(out); - } - - return Q_ERR_SUCCESS; -} - -LOAD(BrushSides) -{ - mbrushside_t *out; - - bsp->numbrushsides = count; - bsp->brushsides = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t planenum = BSP_ExtLong(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - uint32_t texinfo = BSP_ExtLong(); - if (texinfo == BSP_ExtNull) { - out->texinfo = &nulltexinfo; - } else { - ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); - out->texinfo = bsp->texinfo + texinfo; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(Brushes) -{ - mbrush_t *out; - - bsp->numbrushes = count; - bsp->brushes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t firstside = BSP_Long(); - uint32_t numsides = BSP_Long(); - ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); - out->firstbrushside = bsp->brushsides + firstside; - out->numsides = numsides; - out->contents = BSP_Long(); - out->checkcount = 0; - } - - return Q_ERR_SUCCESS; -} - -LOAD(LeafBrushes) -{ - mbrush_t **out; - - bsp->numleafbrushes = count; - bsp->leafbrushes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t brushnum = BSP_ExtLong(); - ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); - *out = bsp->brushes + brushnum; - } - - return Q_ERR_SUCCESS; -} - - -#if USE_REF -LOAD(Lightmap) -{ - if (count) { - bsp->numlightmapbytes = count; - bsp->lightmap = ALLOC(count); - memcpy(bsp->lightmap, in, count); - } - - return Q_ERR_SUCCESS; -} - -LOAD(Vertices) -{ - mvertex_t *out; - - bsp->numvertices = count; - bsp->vertices = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) - BSP_Vector(out->point); - - return Q_ERR_SUCCESS; -} - -LOAD(Edges) -{ - medge_t *out; - - bsp->numedges = count; - bsp->edges = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - for (int j = 0; j < 2; j++) { - uint32_t vertnum = BSP_ExtLong(); - ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); - out->v[j] = vertnum; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(SurfEdges) -{ - msurfedge_t *out; - - bsp->numsurfedges = count; - bsp->surfedges = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t index = BSP_Long(); - uint32_t vert = index >> 31; - if (vert) - index = -index; - ENSURE(index < bsp->numedges, "Bad edgenum"); - out->edge = index; - out->vert = vert; - } - - return Q_ERR_SUCCESS; -} - -LOAD(Faces) -{ - mface_t *out; - - bsp->numfaces = count; - bsp->faces = out = ALLOC(sizeof(*out) * count); - - for (int i = 0, j; i < count; i++, out++) { - uint32_t planenum = BSP_ExtLong(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - out->drawflags = BSP_ExtLong() & DSURF_PLANEBACK; - - uint32_t firstedge = BSP_Long(); - uint32_t numedges = BSP_ExtLong(); - ENSURE(numedges >= 3 && numedges <= 4096 && - (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); - out->firstsurfedge = bsp->surfedges + firstedge; - out->numsurfedges = numedges; - - uint32_t texinfo = BSP_ExtLong(); - ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); - out->texinfo = bsp->texinfo + texinfo; - - for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) - out->styles[j] = in[j]; - - for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) - out->styles[j] = 255; - - in += MAX_LIGHTMAPS; - - uint32_t lightofs = BSP_Long(); - if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { - out->lightmap = NULL; - } else { - ENSURE(lightofs < bsp->numlightmapbytes, "Bad lightofs"); - out->lightmap = bsp->lightmap + lightofs; - } - } - - return Q_ERR_SUCCESS; -} - -LOAD(LeafFaces) -{ - mface_t **out; - - bsp->numleaffaces = count; - bsp->leaffaces = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t facenum = BSP_ExtLong(); - ENSURE(facenum < bsp->numfaces, "Bad facenum"); - *out = bsp->faces + facenum; - } - - return Q_ERR_SUCCESS; -} -#endif - -LOAD(Leafs) -{ - mleaf_t *out; - - ENSURE(count > 0, "Map with no leafs"); - - bsp->numleafs = count; - bsp->leafs = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - out->plane = NULL; - out->contents = BSP_Long(); - - uint32_t cluster = BSP_ExtLong(); - if (cluster == BSP_ExtNull) { - // solid leafs use special -1 cluster - out->cluster = -1; - } else if (bsp->vis == NULL) { - // map has no vis, use 0 as a default cluster - out->cluster = 0; - } else { - // validate cluster - ENSURE(cluster < bsp->vis->numclusters, "Bad cluster"); - out->cluster = cluster; - } - - uint32_t area = BSP_ExtLong(); - ENSURE(area < bsp->numareas, "Bad area"); - out->area = area; - -#if USE_REF - BSP_ExtVector(out->mins); - BSP_ExtVector(out->maxs); - uint32_t firstleafface = BSP_ExtLong(); - uint32_t numleaffaces = BSP_ExtLong(); - ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); - out->firstleafface = bsp->leaffaces + firstleafface; - out->numleaffaces = numleaffaces; - - out->parent = NULL; - out->visframe = -1; -#else - in += 16 * (bsp->extended + 1); -#endif - - uint32_t firstleafbrush = BSP_ExtLong(); - uint32_t numleafbrushes = BSP_ExtLong(); - ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); - out->firstleafbrush = bsp->leafbrushes + firstleafbrush; - out->numleafbrushes = numleafbrushes; - } - - ENSURE(bsp->leafs[0].contents == CONTENTS_SOLID, "Map leaf 0 is not CONTENTS_SOLID"); - - return Q_ERR_SUCCESS; -} - -LOAD(Nodes) -{ - mnode_t *out; - - ENSURE(count > 0, "Map with no nodes"); - - bsp->numnodes = count; - bsp->nodes = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t planenum = BSP_Long(); - ENSURE(planenum < bsp->numplanes, "Bad planenum"); - out->plane = bsp->planes + planenum; - - for (int j = 0; j < 2; j++) { - uint32_t child = BSP_Long(); - if (child & BIT(31)) { - child = ~child; - ENSURE(child < bsp->numleafs, "Bad leafnum"); - out->children[j] = (mnode_t *)(bsp->leafs + child); - } else { - ENSURE(child < count, "Bad nodenum"); - out->children[j] = bsp->nodes + child; - } - } - -#if USE_REF - BSP_ExtVector(out->mins); - BSP_ExtVector(out->maxs); - uint32_t firstface = BSP_ExtLong(); - uint32_t numfaces = BSP_ExtLong(); - ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); - out->firstface = bsp->faces + firstface; - out->numfaces = numfaces; - - out->parent = NULL; - out->visframe = -1; -#else - in += 16 * (bsp->extended + 1); -#endif - } - - return Q_ERR_SUCCESS; -} - -LOAD(SubModels) -{ - mmodel_t *out; - - ENSURE(count > 0, "Map with no models"); - ENSURE(count <= MAX_MODELS - 2, "Too many models"); - - bsp->nummodels = count; - bsp->models = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - // spread the mins / maxs by a pixel - BSP_VectorAdd(out->mins, -1); - BSP_VectorAdd(out->maxs, +1); - BSP_Vector(out->origin); - - uint32_t headnode = BSP_Long(); - if (headnode & BIT(31)) { - // be careful, some models have no nodes, just a leaf - headnode = ~headnode; - ENSURE(headnode < bsp->numleafs, "Bad headleaf"); - out->headnode = (mnode_t *)(bsp->leafs + headnode); - } else { - ENSURE(headnode < bsp->numnodes, "Bad headnode"); - out->headnode = bsp->nodes + headnode; - } -#if USE_REF - if (i == 0) { - in += 8; - continue; - } - uint32_t firstface = BSP_Long(); - uint32_t numfaces = BSP_Long(); - ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); - out->firstface = bsp->faces + firstface; - out->numfaces = numfaces; - - out->radius = RadiusFromBounds(out->mins, out->maxs); -#else - in += 8; -#endif - } - - return Q_ERR_SUCCESS; -} - -// These are validated after all the areas are loaded -LOAD(AreaPortals) -{ - mareaportal_t *out; - - bsp->numareaportals = count; - bsp->areaportals = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - out->portalnum = BSP_Long(); - out->otherarea = BSP_Long(); - } - - return Q_ERR_SUCCESS; -} - -LOAD(Areas) -{ - marea_t *out; - - ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); - - bsp->numareas = count; - bsp->areas = out = ALLOC(sizeof(*out) * count); - - for (int i = 0; i < count; i++, out++) { - uint32_t numareaportals = BSP_Long(); - uint32_t firstareaportal = BSP_Long(); - ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); - out->numareaportals = numareaportals; - out->firstareaportal = bsp->areaportals + firstareaportal; - out->floodvalid = 0; - } - - return Q_ERR_SUCCESS; -} +#define BSP_ENSURE(cond, msg) \ + do { if (!(cond)) { BSP_ERROR(msg); return Q_ERR_INVALID_FORMAT; } } while (0) -LOAD(EntString) -{ - bsp->numentitychars = count; - bsp->entitystring = ALLOC(count + 1); - memcpy(bsp->entitystring, in, count); - bsp->entitystring[count] = 0; +#define BSP_EXTENDED 0 +#include "bsp_template.c" - return Q_ERR_SUCCESS; -} +#define BSP_EXTENDED 1 +#include "bsp_template.c" /* =============================================================================== @@ -572,7 +75,7 @@ typedef struct { } xlump_info_t; typedef struct { - int (*load)(bsp_t *const, const byte *, const size_t, const bool); + int (*load[2])(bsp_t *const, const byte *, const size_t); const char *name; uint8_t lump; uint8_t disksize[2]; @@ -585,32 +88,36 @@ typedef struct { } bsp_stat_t; #define L(name, lump, mem_t, disksize1, disksize2) \ - { BSP_Load##name, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } + { { BSP_Load##name, BSP_Load##name }, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } + +#define E(name, lump, mem_t, disksize1, disksize2) \ + { { BSP_Load##name, BSP_Load##name##Ext }, #name, lump, { disksize1, disksize2 }, sizeof(mem_t) } static const lump_info_t bsp_lumps[] = { L(Visibility, 3, byte, 1, 1), L(Texinfo, 5, mtexinfo_t, 76, 76), L(Planes, 1, cplane_t, 20, 20), - L(BrushSides, 15, mbrushside_t, 4, 8), + E(BrushSides, 15, mbrushside_t, 4, 8), L(Brushes, 14, mbrush_t, 12, 12), - L(LeafBrushes, 10, mbrush_t *, 2, 4), + E(LeafBrushes, 10, mbrush_t *, 2, 4), L(AreaPortals, 18, mareaportal_t, 8, 8), L(Areas, 17, marea_t, 8, 8), #if USE_REF L(Lightmap, 7, byte, 1, 1), L(Vertices, 2, mvertex_t, 12, 12), - L(Edges, 11, medge_t, 4, 8), + E(Edges, 11, medge_t, 4, 8), L(SurfEdges, 12, msurfedge_t, 4, 4), - L(Faces, 6, mface_t, 20, 28), - L(LeafFaces, 9, mface_t *, 2, 4), + E(Faces, 6, mface_t, 20, 28), + E(LeafFaces, 9, mface_t *, 2, 4), #endif - L(Leafs, 8, mleaf_t, 28, 52), - L(Nodes, 4, mnode_t, 28, 44), + E(Leafs, 8, mleaf_t, 28, 52), + E(Nodes, 4, mnode_t, 28, 44), L(SubModels, 13, mmodel_t, 48, 48), L(EntString, 0, char, 1, 1), }; #undef L +#undef E #define F(x) { q_offsetof(bsp_t, num##x), #x } @@ -731,7 +238,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) // a face may never belong to more than one node for (i = 0, face = node->firstface; i < node->numfaces; i++, face++) { if (face->drawframe) { - DEBUG("Duplicate face"); + BSP_ERROR("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = key; @@ -740,7 +247,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[0]; if (child->parent) { - DEBUG("Cycle encountered"); + BSP_ERROR("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -750,7 +257,7 @@ static int BSP_SetParent(mnode_t *node, unsigned key) child = node->children[1]; if (child->parent) { - DEBUG("Cycle encountered"); + BSP_ERROR("Cycle encountered"); return Q_ERR_INFINITE_LOOP; } child->parent = node; @@ -771,7 +278,7 @@ static int BSP_ValidateTree(bsp_t *bsp) for (i = 0, mod = bsp->models; i < bsp->nummodels; i++, mod++) { if (i == 0 && mod->headnode != bsp->nodes) { - DEBUG("Map model 0 headnode is not the first node"); + BSP_ERROR("Map model 0 headnode is not the first node"); return Q_ERR_INVALID_FORMAT; } @@ -784,7 +291,7 @@ static int BSP_ValidateTree(bsp_t *bsp) // a face may never belong to more than one model for (j = 0, face = mod->firstface; j < mod->numfaces; j++, face++) { if (face->drawframe && face->drawframe != ~i) { - DEBUG("Duplicate face"); + BSP_ERROR("Duplicate face"); return Q_ERR_INFINITE_LOOP; } face->drawframe = ~i; @@ -804,8 +311,8 @@ static int BSP_ValidateAreaPortals(bsp_t *bsp) bsp->numportals = 0; for (i = 0, p = bsp->areaportals; i < bsp->numareaportals; i++, p++) { - ENSURE(p->portalnum < bsp->numareaportals, "Bad portalnum"); - ENSURE(p->otherarea < bsp->numareas, "Bad otherarea"); + BSP_ENSURE(p->portalnum < bsp->numareaportals, "Bad portalnum"); + BSP_ENSURE(p->otherarea < bsp->numareas, "Bad otherarea"); bsp->numportals = max(bsp->numportals, p->portalnum + 1); } @@ -1082,7 +589,7 @@ static void BSP_ParseLightgrid(bsp_t *bsp, const byte *in, size_t filelen) SZ_InitRead(&s, in, filelen); - grid->nodes = ALLOC(sizeof(grid->nodes[0]) * grid->numnodes); + grid->nodes = BSP_ALLOC(sizeof(grid->nodes[0]) * grid->numnodes); // load children first s.readcount = 45; @@ -1107,11 +614,11 @@ static void BSP_ParseLightgrid(bsp_t *bsp, const byte *in, size_t filelen) s.readcount += 32; } - grid->leafs = ALLOC(sizeof(grid->leafs[0]) * grid->numleafs); + grid->leafs = BSP_ALLOC(sizeof(grid->leafs[0]) * grid->numleafs); // init samples to fully occluded size = sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles; - grid->samples = sample = memset(ALLOC(size), 255, size); + grid->samples = sample = memset(BSP_ALLOC(size), 255, size); remaining = grid->numsamples; s.readcount += 4; @@ -1319,7 +826,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) // load all lumps for (i = 0; i < q_countof(bsp_lumps); i++) { - ret = bsp_lumps[i].load(bsp, buf + lump_ofs[i], lump_count[i], extended); + ret = bsp_lumps[i].load[extended](bsp, buf + lump_ofs[i], lump_count[i]); if (ret) { goto fail1; } diff --git a/src/common/bsp_template.c b/src/common/bsp_template.c new file mode 100644 index 000000000..9554f7233 --- /dev/null +++ b/src/common/bsp_template.c @@ -0,0 +1,549 @@ +/* +Copyright (C) 1997-2001 Id Software, Inc. +Copyright (C) 2008 Andrey Nazarov + +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 the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +// This file must be included twice, first with BSP_EXTENDED = 0 and then +// with BSP_EXTENDED = 1 (strictly in this order). +// +// This code doesn't use structs to allow for unaligned lumps reading. + +#if BSP_EXTENDED + +#undef BSP_LOAD +#undef BSP_ExtFloat +#undef BSP_ExtLong +#undef BSP_ExtNull + +#define BSP_LOAD(func) \ + static int BSP_Load##func##Ext(bsp_t *const bsp, const byte *in, const size_t count) + +#define BSP_ExtFloat() BSP_Float() +#define BSP_ExtLong() BSP_Long() +#define BSP_ExtNull (uint32_t)-1 + +#else + +#define BSP_Short() (in += 2, RL16(in - 2)) +#define BSP_Long() (in += 4, RL32(in - 4)) +#define BSP_Float() LongToFloat(BSP_Long()) + +#define BSP_LOAD(func) \ + static int BSP_Load##func(bsp_t *const bsp, const byte *in, const size_t count) + +#define BSP_ExtFloat() (int16_t)BSP_Short() +#define BSP_ExtLong() BSP_Short() +#define BSP_ExtNull (uint16_t)-1 + +#define BSP_Vector(v) \ + ((v)[0] = BSP_Float(), (v)[1] = BSP_Float(), (v)[2] = BSP_Float()) + +#define BSP_ExtVector(v) \ + ((v)[0] = BSP_ExtFloat(), (v)[1] = BSP_ExtFloat(), (v)[2] = BSP_ExtFloat()) + +BSP_LOAD(Visibility) +{ + if (!count) + return Q_ERR_SUCCESS; + + BSP_ENSURE(count >= 4, "Too small header"); + + uint32_t numclusters = BSP_Long(); + BSP_ENSURE(numclusters <= MAX_MAP_CLUSTERS, "Too many clusters"); + + uint32_t hdrsize = 4 + numclusters * 8; + BSP_ENSURE(count >= hdrsize, "Too small header"); + + bsp->numvisibility = count; + bsp->vis = BSP_ALLOC(count); + bsp->vis->numclusters = numclusters; + bsp->visrowsize = (numclusters + 7) >> 3; + + for (int i = 0; i < numclusters; i++) { + for (int j = 0; j < 2; j++) { + uint32_t bitofs = BSP_Long(); + BSP_ENSURE(bitofs >= hdrsize && bitofs < count, "Bad bitofs"); + bsp->vis->bitofs[i][j] = bitofs; + } + } + + memcpy(bsp->vis->bitofs + numclusters, in, count - hdrsize); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Texinfo) +{ + mtexinfo_t *out; + + bsp->numtexinfo = count; + bsp->texinfo = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { +#if USE_REF + for (int j = 0; j < 2; j++) { + BSP_Vector(out->axis[j]); + out->offset[j] = BSP_Float(); + } +#else + in += 32; +#endif + out->c.flags = BSP_Long(); + out->c.value = BSP_Long(); + + memcpy(out->c.name, in, sizeof(out->c.name) - 1); + memcpy(out->name, in, sizeof(out->name) - 1); + in += MAX_TEXNAME; + +#if USE_REF + int32_t next = (int32_t)BSP_Long(); + if (next > 0) { + BSP_ENSURE(next < count, "Bad anim chain"); + out->next = bsp->texinfo + next; + } else { + out->next = NULL; + } +#else + in += 4; +#endif + } + +#if USE_REF + // count animation frames + out = bsp->texinfo; + for (int i = 0; i < count; i++, out++) { + out->numframes = 1; + for (mtexinfo_t *step = out->next; step && step != out; step = step->next) { + if (out->numframes == count) { + BSP_ERROR("Infinite anim chain"); + return Q_ERR_INFINITE_LOOP; + } + out->numframes++; + } + } +#endif + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Planes) +{ + cplane_t *out; + + bsp->numplanes = count; + bsp->planes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, in += 4, out++) { + BSP_Vector(out->normal); + out->dist = BSP_Float(); + SetPlaneType(out); + SetPlaneSignbits(out); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Brushes) +{ + mbrush_t *out; + + bsp->numbrushes = count; + bsp->brushes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t firstside = BSP_Long(); + uint32_t numsides = BSP_Long(); + BSP_ENSURE((uint64_t)firstside + numsides <= bsp->numbrushsides, "Bad brushsides"); + out->firstbrushside = bsp->brushsides + firstside; + out->numsides = numsides; + out->contents = BSP_Long(); + out->checkcount = 0; + } + + return Q_ERR_SUCCESS; +} + +#if USE_REF +BSP_LOAD(Lightmap) +{ + if (count) { + bsp->numlightmapbytes = count; + bsp->lightmap = BSP_ALLOC(count); + memcpy(bsp->lightmap, in, count); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Vertices) +{ + mvertex_t *out; + + bsp->numvertices = count; + bsp->vertices = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) + BSP_Vector(out->point); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(SurfEdges) +{ + msurfedge_t *out; + + bsp->numsurfedges = count; + bsp->surfedges = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t index = BSP_Long(); + uint32_t vert = index >> 31; + if (vert) + index = -index; + BSP_ENSURE(index < bsp->numedges, "Bad edgenum"); + out->edge = index; + out->vert = vert; + } + + return Q_ERR_SUCCESS; +} +#endif + +BSP_LOAD(SubModels) +{ + mmodel_t *out; + + BSP_ENSURE(count > 0, "Map with no models"); + BSP_ENSURE(count <= MAX_MODELS - 2, "Too many models"); + + bsp->nummodels = count; + bsp->models = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + BSP_Vector(out->mins); + BSP_Vector(out->maxs); + BSP_Vector(out->origin); + + // spread the mins / maxs by a pixel + for (int j = 0; j < 3; j++) { + out->mins[j] -= 1; + out->maxs[j] += 1; + } + + uint32_t headnode = BSP_Long(); + if (headnode & BIT(31)) { + // be careful, some models have no nodes, just a leaf + headnode = ~headnode; + BSP_ENSURE(headnode < bsp->numleafs, "Bad headleaf"); + out->headnode = (mnode_t *)(bsp->leafs + headnode); + } else { + BSP_ENSURE(headnode < bsp->numnodes, "Bad headnode"); + out->headnode = bsp->nodes + headnode; + } +#if USE_REF + if (i == 0) { + in += 8; + continue; + } + uint32_t firstface = BSP_Long(); + uint32_t numfaces = BSP_Long(); + BSP_ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); + out->firstface = bsp->faces + firstface; + out->numfaces = numfaces; + + out->radius = RadiusFromBounds(out->mins, out->maxs); +#else + in += 8; +#endif + } + + return Q_ERR_SUCCESS; +} + +// These are validated after all the areas are loaded +BSP_LOAD(AreaPortals) +{ + mareaportal_t *out; + + bsp->numareaportals = count; + bsp->areaportals = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + out->portalnum = BSP_Long(); + out->otherarea = BSP_Long(); + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Areas) +{ + marea_t *out; + + BSP_ENSURE(count <= MAX_MAP_AREAS, "Too many areas"); + + bsp->numareas = count; + bsp->areas = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t numareaportals = BSP_Long(); + uint32_t firstareaportal = BSP_Long(); + BSP_ENSURE((uint64_t)firstareaportal + numareaportals <= bsp->numareaportals, "Bad areaportals"); + out->numareaportals = numareaportals; + out->firstareaportal = bsp->areaportals + firstareaportal; + out->floodvalid = 0; + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(EntString) +{ + bsp->numentitychars = count; + bsp->entitystring = BSP_ALLOC(count + 1); + memcpy(bsp->entitystring, in, count); + bsp->entitystring[count] = 0; + + return Q_ERR_SUCCESS; +} + +#endif // !BSP_EXTENDED + +BSP_LOAD(BrushSides) +{ + mbrushside_t *out; + + bsp->numbrushsides = count; + bsp->brushsides = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + uint32_t texinfo = BSP_ExtLong(); + if (texinfo == BSP_ExtNull) { + out->texinfo = &nulltexinfo; + } else { + BSP_ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); + out->texinfo = bsp->texinfo + texinfo; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(LeafBrushes) +{ + mbrush_t **out; + + bsp->numleafbrushes = count; + bsp->leafbrushes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t brushnum = BSP_ExtLong(); + BSP_ENSURE(brushnum < bsp->numbrushes, "Bad brushnum"); + *out = bsp->brushes + brushnum; + } + + return Q_ERR_SUCCESS; +} + +#if USE_REF +BSP_LOAD(Edges) +{ + medge_t *out; + + bsp->numedges = count; + bsp->edges = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + for (int j = 0; j < 2; j++) { + uint32_t vertnum = BSP_ExtLong(); + BSP_ENSURE(vertnum < bsp->numvertices, "Bad vertnum"); + out->v[j] = vertnum; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Faces) +{ + mface_t *out; + + bsp->numfaces = count; + bsp->faces = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0, j; i < count; i++, out++) { + uint32_t planenum = BSP_ExtLong(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + out->drawflags = BSP_ExtLong() & DSURF_PLANEBACK; + + uint32_t firstedge = BSP_Long(); + uint32_t numedges = BSP_ExtLong(); + BSP_ENSURE(numedges >= 3 && numedges <= 4096 && + (uint64_t)firstedge + numedges <= bsp->numsurfedges, "Bad surfedges"); + out->firstsurfedge = bsp->surfedges + firstedge; + out->numsurfedges = numedges; + + uint32_t texinfo = BSP_ExtLong(); + BSP_ENSURE(texinfo < bsp->numtexinfo, "Bad texinfo"); + out->texinfo = bsp->texinfo + texinfo; + + for (j = 0; j < MAX_LIGHTMAPS && in[j] != 255; j++) + out->styles[j] = in[j]; + + for (out->numstyles = j; j < MAX_LIGHTMAPS; j++) + out->styles[j] = 255; + + in += MAX_LIGHTMAPS; + + uint32_t lightofs = BSP_Long(); + if (lightofs == (uint32_t)-1 || bsp->numlightmapbytes == 0) { + out->lightmap = NULL; + } else { + BSP_ENSURE(lightofs < bsp->numlightmapbytes, "Bad lightofs"); + out->lightmap = bsp->lightmap + lightofs; + } + } + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(LeafFaces) +{ + mface_t **out; + + bsp->numleaffaces = count; + bsp->leaffaces = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t facenum = BSP_ExtLong(); + BSP_ENSURE(facenum < bsp->numfaces, "Bad facenum"); + *out = bsp->faces + facenum; + } + + return Q_ERR_SUCCESS; +} +#endif + +BSP_LOAD(Leafs) +{ + mleaf_t *out; + + BSP_ENSURE(count > 0, "Map with no leafs"); + + bsp->numleafs = count; + bsp->leafs = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + out->plane = NULL; + out->contents = BSP_Long(); + + uint32_t cluster = BSP_ExtLong(); + if (cluster == BSP_ExtNull) { + // solid leafs use special -1 cluster + out->cluster = -1; + } else if (bsp->vis == NULL) { + // map has no vis, use 0 as a default cluster + out->cluster = 0; + } else { + // validate cluster + BSP_ENSURE(cluster < bsp->vis->numclusters, "Bad cluster"); + out->cluster = cluster; + } + + uint32_t area = BSP_ExtLong(); + BSP_ENSURE(area < bsp->numareas, "Bad area"); + out->area = area; + +#if USE_REF + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); + uint32_t firstleafface = BSP_ExtLong(); + uint32_t numleaffaces = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstleafface + numleaffaces <= bsp->numleaffaces, "Bad leaffaces"); + out->firstleafface = bsp->leaffaces + firstleafface; + out->numleaffaces = numleaffaces; + + out->parent = NULL; + out->visframe = -1; +#else + in += 16 * (BSP_EXTENDED + 1); +#endif + + uint32_t firstleafbrush = BSP_ExtLong(); + uint32_t numleafbrushes = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstleafbrush + numleafbrushes <= bsp->numleafbrushes, "Bad leafbrushes"); + out->firstleafbrush = bsp->leafbrushes + firstleafbrush; + out->numleafbrushes = numleafbrushes; + } + + BSP_ENSURE(bsp->leafs[0].contents == CONTENTS_SOLID, "Map leaf 0 is not CONTENTS_SOLID"); + + return Q_ERR_SUCCESS; +} + +BSP_LOAD(Nodes) +{ + mnode_t *out; + + BSP_ENSURE(count > 0, "Map with no nodes"); + + bsp->numnodes = count; + bsp->nodes = out = BSP_ALLOC(sizeof(*out) * count); + + for (int i = 0; i < count; i++, out++) { + uint32_t planenum = BSP_Long(); + BSP_ENSURE(planenum < bsp->numplanes, "Bad planenum"); + out->plane = bsp->planes + planenum; + + for (int j = 0; j < 2; j++) { + uint32_t child = BSP_Long(); + if (child & BIT(31)) { + child = ~child; + BSP_ENSURE(child < bsp->numleafs, "Bad leafnum"); + out->children[j] = (mnode_t *)(bsp->leafs + child); + } else { + BSP_ENSURE(child < count, "Bad nodenum"); + out->children[j] = bsp->nodes + child; + } + } + +#if USE_REF + BSP_ExtVector(out->mins); + BSP_ExtVector(out->maxs); + uint32_t firstface = BSP_ExtLong(); + uint32_t numfaces = BSP_ExtLong(); + BSP_ENSURE((uint64_t)firstface + numfaces <= bsp->numfaces, "Bad faces"); + out->firstface = bsp->faces + firstface; + out->numfaces = numfaces; + + out->parent = NULL; + out->visframe = -1; +#else + in += 16 * (BSP_EXTENDED + 1); +#endif + } + + return Q_ERR_SUCCESS; +} + +#undef BSP_EXTENDED From 9593d90d4337fd7344f2e39097bdaae6c8660465 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 788/974] Update libjpeg-turbo to 3.0.4. --- subprojects/libjpeg-turbo.wrap | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/subprojects/libjpeg-turbo.wrap b/subprojects/libjpeg-turbo.wrap index 71f38a08c..f9eda5fb3 100644 --- a/subprojects/libjpeg-turbo.wrap +++ b/subprojects/libjpeg-turbo.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = libjpeg-turbo-3.0.3 -source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.3/libjpeg-turbo-3.0.3.tar.gz -source_filename = libjpeg-turbo-3.0.3.tar.gz -source_hash = 343e789069fc7afbcdfe44dbba7dbbf45afa98a15150e079a38e60e44578865d -patch_filename = libjpeg-turbo_3.0.3-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.3-1/get_patch -patch_hash = 8bbf4f205e54e73c48c14dae7fcc71f152cdc8fea6d55a1af1e3108d10c6a2e4 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.3-1/libjpeg-turbo-3.0.3.tar.gz -wrapdb_version = 3.0.3-1 +directory = libjpeg-turbo-3.0.4 +source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.4/libjpeg-turbo-3.0.4.tar.gz +source_filename = libjpeg-turbo-3.0.4.tar.gz +source_hash = 99130559e7d62e8d695f2c0eaeef912c5828d5b84a0537dcb24c9678c9d5b76b +patch_filename = libjpeg-turbo_3.0.4-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.4-2/get_patch +patch_hash = f12d14c6ff4ae83eddc1a2e8cfd96a9b7a40b6bdc8ff345ca8094abf4c3da928 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.4-2/libjpeg-turbo-3.0.4.tar.gz +wrapdb_version = 3.0.4-2 [provide] dependency_names = libjpeg, libturbojpeg From 61636aa512495b9a8363ce4e3b21ee8e61186f25 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 789/974] Update libpng to 1.6.44. --- subprojects/libpng.wrap | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index d32e859a4..6632152df 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = libpng-1.6.43 -source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz -source_filename = libpng-1.6.43.tar.xz -source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c -patch_filename = libpng_1.6.43-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-2/get_patch -patch_hash = 49951297edf03e81d925ab03726555f09994ad1ed78fb539a269216430eef3da -wrapdb_version = 1.6.43-2 +directory = libpng-1.6.44 +source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.44.tar.xz +source_filename = libpng-1.6.44.tar.xz +source_hash = 60c4da1d5b7f0aa8d158da48e8f8afa9773c1c8baa5d21974df61f1886b8ce8e +patch_filename = libpng_1.6.44-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.44-1/get_patch +patch_hash = 394b07614c45fbd1beac8b660386216a490fe12f841a1a445799b676c9c892fb +wrapdb_version = 1.6.44-1 [provide] libpng = libpng_dep From 53374e678e74aced9b7b40695063cbc529de7074 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 790/974] Update libcurl to 8.10.1. --- subprojects/libcurl.wrap | 12 +- subprojects/packagefiles/curl/meson.build | 232 ++++++++++++++++++ .../packagefiles/curl/meson_options.txt | 1 + 3 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 subprojects/packagefiles/curl/meson.build create mode 100644 subprojects/packagefiles/curl/meson_options.txt diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index df7c7745c..ce2622169 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,9 @@ [wrap-file] -directory = curl-8.9.1 -source_url = https://curl.se/download/curl-8.9.1.tar.xz -source_filename = curl-8.9.1.tar.xz -source_hash = f292f6cc051d5bbabf725ef85d432dfeacc8711dd717ea97612ae590643801e5 -patch_url = https://skuller.net/meson/libcurl_8.9.1-1_patch.zip -patch_filename = libcurl_8.9.1-1_patch.zip -patch_hash = b7f4ef775d912da335387b222dc1caf421fb434cc36aeee84a032fb28cac5c77 +directory = curl-8.10.1 +source_url = https://curl.se/download/curl-8.10.1.tar.xz +source_filename = curl-8.10.1.tar.xz +source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee +patch_directory = curl [provide] libcurl = libcurl_dep diff --git a/subprojects/packagefiles/curl/meson.build b/subprojects/packagefiles/curl/meson.build new file mode 100644 index 000000000..919fd9c99 --- /dev/null +++ b/subprojects/packagefiles/curl/meson.build @@ -0,0 +1,232 @@ +project('libcurl', 'c', version: '8.10.1', license: 'bsd') + +src = [ + 'lib/vauth/cleartext.c', + 'lib/vauth/cram.c', + 'lib/vauth/digest.c', + 'lib/vauth/digest_sspi.c', + 'lib/vauth/gsasl.c', + 'lib/vauth/krb5_gssapi.c', + 'lib/vauth/krb5_sspi.c', + 'lib/vauth/ntlm.c', + 'lib/vauth/ntlm_sspi.c', + 'lib/vauth/oauth2.c', + 'lib/vauth/spnego_gssapi.c', + 'lib/vauth/spnego_sspi.c', + 'lib/vauth/vauth.c', + + 'lib/vtls/bearssl.c', + 'lib/vtls/gtls.c', + 'lib/vtls/hostcheck.c', + 'lib/vtls/keylog.c', + 'lib/vtls/mbedtls.c', + 'lib/vtls/mbedtls_threadlock.c', + 'lib/vtls/openssl.c', + 'lib/vtls/rustls.c', + 'lib/vtls/schannel.c', + 'lib/vtls/schannel_verify.c', + 'lib/vtls/sectransp.c', + 'lib/vtls/vtls.c', + 'lib/vtls/wolfssl.c', + 'lib/vtls/x509asn1.c', + + 'lib/vquic/curl_msh3.c', + 'lib/vquic/curl_ngtcp2.c', + 'lib/vquic/curl_osslq.c', + 'lib/vquic/curl_quiche.c', + 'lib/vquic/vquic.c', + 'lib/vquic/vquic-tls.c', + + 'lib/vssh/libssh.c', + 'lib/vssh/libssh2.c', + 'lib/vssh/wolfssh.c', + + 'lib/altsvc.c', + 'lib/amigaos.c', + 'lib/asyn-ares.c', + 'lib/asyn-thread.c', + 'lib/base64.c', + 'lib/bufq.c', + 'lib/bufref.c', + 'lib/c-hyper.c', + 'lib/cf-h1-proxy.c', + 'lib/cf-h2-proxy.c', + 'lib/cf-haproxy.c', + 'lib/cf-https-connect.c', + 'lib/cf-socket.c', + 'lib/cfilters.c', + 'lib/conncache.c', + 'lib/connect.c', + 'lib/content_encoding.c', + 'lib/cookie.c', + 'lib/curl_addrinfo.c', + 'lib/curl_des.c', + 'lib/curl_endian.c', + 'lib/curl_fnmatch.c', + 'lib/curl_get_line.c', + 'lib/curl_gethostname.c', + 'lib/curl_gssapi.c', + 'lib/curl_memrchr.c', + 'lib/curl_multibyte.c', + 'lib/curl_ntlm_core.c', + 'lib/curl_path.c', + 'lib/curl_range.c', + 'lib/curl_rtmp.c', + 'lib/curl_sasl.c', + 'lib/curl_sha512_256.c', + 'lib/curl_sspi.c', + 'lib/curl_threads.c', + 'lib/curl_trc.c', + 'lib/cw-out.c', + 'lib/dict.c', + 'lib/dllmain.c', + 'lib/doh.c', + 'lib/dynbuf.c', + 'lib/dynhds.c', + 'lib/easy.c', + 'lib/easygetopt.c', + 'lib/easyoptions.c', + 'lib/escape.c', + 'lib/file.c', + 'lib/fileinfo.c', + 'lib/fopen.c', + 'lib/formdata.c', + 'lib/ftp.c', + 'lib/ftplistparser.c', + 'lib/getenv.c', + 'lib/getinfo.c', + 'lib/gopher.c', + 'lib/hash.c', + 'lib/headers.c', + 'lib/hmac.c', + 'lib/hostasyn.c', + 'lib/hostip.c', + 'lib/hostip4.c', + 'lib/hostip6.c', + 'lib/hostsyn.c', + 'lib/hsts.c', + 'lib/http.c', + 'lib/http1.c', + 'lib/http2.c', + 'lib/http_aws_sigv4.c', + 'lib/http_chunks.c', + 'lib/http_digest.c', + 'lib/http_negotiate.c', + 'lib/http_ntlm.c', + 'lib/http_proxy.c', + 'lib/idn.c', + 'lib/if2ip.c', + 'lib/imap.c', + 'lib/inet_ntop.c', + 'lib/inet_pton.c', + 'lib/krb5.c', + 'lib/ldap.c', + 'lib/llist.c', + 'lib/macos.c', + 'lib/md4.c', + 'lib/md5.c', + 'lib/memdebug.c', + 'lib/mime.c', + 'lib/mprintf.c', + 'lib/mqtt.c', + 'lib/multi.c', + 'lib/netrc.c', + 'lib/nonblock.c', + 'lib/noproxy.c', + 'lib/openldap.c', + 'lib/parsedate.c', + 'lib/pingpong.c', + 'lib/pop3.c', + 'lib/progress.c', + 'lib/psl.c', + 'lib/rand.c', + 'lib/rename.c', + 'lib/request.c', + 'lib/rtsp.c', + 'lib/select.c', + 'lib/sendf.c', + 'lib/setopt.c', + 'lib/sha256.c', + 'lib/share.c', + 'lib/slist.c', + 'lib/smb.c', + 'lib/smtp.c', + 'lib/socketpair.c', + 'lib/socks.c', + 'lib/socks_gssapi.c', + 'lib/socks_sspi.c', + 'lib/speedcheck.c', + 'lib/splay.c', + 'lib/strcase.c', + 'lib/strdup.c', + 'lib/strerror.c', + 'lib/strtok.c', + 'lib/strtoofft.c', + 'lib/system_win32.c', + 'lib/telnet.c', + 'lib/tftp.c', + 'lib/timediff.c', + 'lib/timeval.c', + 'lib/transfer.c', + 'lib/url.c', + 'lib/urlapi.c', + 'lib/version.c', + 'lib/version_win32.c', + 'lib/warnless.c', + 'lib/ws.c', +] + +if host_machine.system() != 'windows' + error('Only Windows supported') +endif + +cc = meson.get_compiler('c') + +args = [ + '-DBUILDING_LIBCURL', + '-DUSE_SCHANNEL', + '-DUSE_WINDOWS_SSPI', + '-DUSE_IPV6', + '-DCURL_STATICLIB', + '-DHTTP_ONLY', + '-DCURL_DISABLE_CRYPTO_AUTH', +] + +if get_option('windows-xp-compat') + args += '-D_WIN32_WINNT=0x0501' +else + args += '-D_WIN32_WINNT=0x0601' +endif + +zlib = dependency('zlib', required: false) +if zlib.found() + args += '-DHAVE_LIBZ' +endif + +deps = [ + cc.find_library('advapi32'), + cc.find_library('bcrypt'), + cc.find_library('crypt32'), + cc.find_library('ws2_32'), + zlib, +] + +nghttp2 = dependency('libnghttp2', required: false) +if nghttp2.found() and cc.has_header_symbol('sspi.h', 'SecApplicationProtocolNegotiationExt_ALPN', + prefix: '#include ', args: ['-DSECURITY_WIN32=1']) + args += ['-DUSE_NGHTTP2', '-DHAS_ALPN=1'] + deps += nghttp2 +endif + +libcurl = static_library('curl', src, + c_args: args, + dependencies: deps, + include_directories: ['include', 'lib'], +) + +libcurl_dep = declare_dependency( + compile_args: ['-DCURL_STATICLIB'], + dependencies: deps, + include_directories: 'include', + link_with: libcurl, +) diff --git a/subprojects/packagefiles/curl/meson_options.txt b/subprojects/packagefiles/curl/meson_options.txt new file mode 100644 index 000000000..0d5cff144 --- /dev/null +++ b/subprojects/packagefiles/curl/meson_options.txt @@ -0,0 +1 @@ +option('windows-xp-compat', type: 'boolean', value: false, yield: true) From 7c0b87da139dbea2a40d5c5f04f83fe1eadf9c3a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 791/974] Update openal-soft to 1.23.1. --- subprojects/openal-soft.wrap | 13 ++++++------- subprojects/packagefiles/openal-soft/meson.build | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 subprojects/packagefiles/openal-soft/meson.build diff --git a/subprojects/openal-soft.wrap b/subprojects/openal-soft.wrap index 8292a54cd..8701045bb 100644 --- a/subprojects/openal-soft.wrap +++ b/subprojects/openal-soft.wrap @@ -1,11 +1,10 @@ [wrap-file] -directory = openal-soft-1.22.2 -source_url = https://openal-soft.org/openal-releases/openal-soft-1.22.2.tar.bz2 -source_filename = openal-soft-1.22.2.tar.bz2 -source_hash = ae94cc95cda76b7cc6e92e38c2531af82148e76d3d88ce996e2928a1ea7c3d20 -patch_url = https://skuller.net/meson/openal-soft_1.22.2-1_patch.zip -patch_filename = openal-soft_1.22.2-1_patch.zip -patch_hash = 8f0df33a04aec9fbac053476a9d68fd201bc74ed6ba62fce00138b71eb583db0 +directory = openal-soft-1.23.1 +source_url = https://github.com/kcat/openal-soft/releases/download/1.23.1/openal-soft-1.23.1.tar.bz2 +source_fallback_url = https://openal-soft.org/openal-releases/openal-soft-1.23.1.tar.bz2 +source_filename = openal-soft-1.23.1.tar.bz2 +source_hash = 796f4b89134c4e57270b7f0d755f0fa3435b90da437b745160a49bd41c845b21 +patch_directory = openal-soft [provide] openal = openal_dep diff --git a/subprojects/packagefiles/openal-soft/meson.build b/subprojects/packagefiles/openal-soft/meson.build new file mode 100644 index 000000000..e33717f57 --- /dev/null +++ b/subprojects/packagefiles/openal-soft/meson.build @@ -0,0 +1,3 @@ +project('openal-soft', version: '1.23.1') + +openal_dep = declare_dependency(include_directories: 'include') From 013c8fb7e31b8f412c867ec5f47aad767a4272ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 792/974] Update khr-headers to 20240924. --- subprojects/khr-headers.wrap | 6 ++---- subprojects/packagefiles/khr-headers.tar.xz | Bin 0 -> 111740 bytes 2 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 subprojects/packagefiles/khr-headers.tar.xz diff --git a/subprojects/khr-headers.wrap b/subprojects/khr-headers.wrap index 3e3caa844..cedbb6bd9 100644 --- a/subprojects/khr-headers.wrap +++ b/subprojects/khr-headers.wrap @@ -1,5 +1,3 @@ [wrap-file] -directory = khr-headers-20220530 -source_url = https://skuller.net/meson/khr-headers-20220530.zip -source_filename = khr-headers-20220530.zip -source_hash = c90019de9f55afae8e6a8b15ae4e23d921b98c1571af9fcfd5525df059a160f3 +directory = khr-headers +source_filename = khr-headers.tar.xz diff --git a/subprojects/packagefiles/khr-headers.tar.xz b/subprojects/packagefiles/khr-headers.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..98bb44528dad5726d9329fce969c086ff54c39d0 GIT binary patch literal 111740 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 literal 0 HcmV?d00001 From ffec154e778a77fcc27d91fc76f807255a672750 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 16:04:36 +0300 Subject: [PATCH 793/974] Unignore subprojects/packagefiles. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4d3a07c8a..300eac43f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /subprojects/*/ +!/subprojects/packagefiles/ /tags /TAGS From f15a750acfee1640b6f05bd07e4e1a7bca2ac294 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Wed, 25 Sep 2024 22:45:30 +0300 Subject: [PATCH 794/974] Update README.md. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 4f87a80af..e4c4e4089 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,5 @@ For building Q2PRO, consult the INSTALL.md file. Q2PRO doesn't have releases. It is always recommended to use the git master version. -Nightly Windows builds are available at https://skuller.net/q2pro/ - For information on using and configuring Q2PRO, refer to client and server manuals available in doc/ subdirectory. From 27df08e00428c87255efc3e39b3e72b7303192fc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 26 Sep 2024 16:19:26 +0300 Subject: [PATCH 795/974] Silence compiler warning. --- src/server/mvd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/mvd.c b/src/server/mvd.c index b547035ee..a4eaa8952 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -1106,7 +1106,7 @@ SV_MvdMulticast */ void SV_MvdMulticast(const mleaf_t *leaf, multicast_t to, bool reliable) { - int leafnum; + int leafnum = 0; mvd_ops_t op; sizebuf_t *buf; From 287ef4608ee7567e0fd3c9a89f5ae71141d67739 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 26 Sep 2024 00:48:35 +0300 Subject: [PATCH 796/974] Add CI workflow for nightly builds. --- .ci/configure-ffmpeg.sh | 76 ++++++++++++++++++ .ci/i686-w64-mingw32.txt | 1 + .ci/nightly.sh | 145 ++++++++++++++++++++++++++++++++++ .ci/readme-template-rr.txt | 13 +++ .ci/readme-template.txt | 10 +++ .ci/x86_64-w64-mingw32.txt | 1 + .github/workflows/nightly.yml | 48 +++++++++++ 7 files changed, 294 insertions(+) create mode 100755 .ci/configure-ffmpeg.sh create mode 100755 .ci/nightly.sh create mode 100644 .ci/readme-template-rr.txt create mode 100644 .ci/readme-template.txt create mode 100644 .github/workflows/nightly.yml diff --git a/.ci/configure-ffmpeg.sh b/.ci/configure-ffmpeg.sh new file mode 100755 index 000000000..ee81dfab9 --- /dev/null +++ b/.ci/configure-ffmpeg.sh @@ -0,0 +1,76 @@ +#!/bin/sh -e + +OPTS_COMMON="--disable-everything \ + --enable-decoder=theora \ + --enable-decoder=vorbis \ + --enable-decoder=idcin \ + --enable-decoder=pcm_* \ + --disable-decoder=pcm_bluray \ + --disable-decoder=pcm_dvd \ + --disable-decoder=pcm_alaw_at \ + --disable-decoder=pcm_mulaw_at \ + --enable-demuxer=ogg \ + --enable-demuxer=idcin \ + --enable-demuxer=wav \ + --enable-parser=vp3 \ + --enable-parser=vorbis \ + --disable-protocols \ + --enable-protocol=file \ + --disable-avdevice \ + --disable-avfilter \ + --disable-postproc \ + --disable-programs \ + --disable-autodetect \ + --disable-network \ + --disable-doc \ + --disable-swscale-alpha \ + --enable-small \ + --disable-pthreads \ + --disable-w32threads" + +config_linux() { + ../configure --prefix="$1" $OPTS_COMMON +} + +config_win32() { + ../configure \ + --prefix="$1" \ + --cross-prefix=i686-w64-mingw32- \ + --arch=x86 \ + --target-os=mingw32 \ + --extra-cflags='-msse2 -mfpmath=sse' \ + $OPTS_COMMON +} + +config_win64() { + ../configure \ + --prefix="$1" \ + --cross-prefix=x86_64-w64-mingw32- \ + --arch=x86 \ + --target-os=mingw64 \ + $OPTS_COMMON +} + +usage() { + echo "Usage: $0 " + exit 1 +} + +if [ -z "$2" ] ; then + usage +fi + +case "$1" in + --win32) + config_win32 "$2" + ;; + --win64) + config_win64 "$2" + ;; + --linux) + config_linux "$2" + ;; + *) + usage + ;; +esac diff --git a/.ci/i686-w64-mingw32.txt b/.ci/i686-w64-mingw32.txt index 783095616..a28437cd5 100644 --- a/.ci/i686-w64-mingw32.txt +++ b/.ci/i686-w64-mingw32.txt @@ -5,6 +5,7 @@ ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' windres = 'i686-w64-mingw32-windres' nasm = 'nasm' +pkg-config = 'pkg-config' [host_machine] system = 'windows' diff --git a/.ci/nightly.sh b/.ci/nightly.sh new file mode 100755 index 000000000..a32f49e46 --- /dev/null +++ b/.ci/nightly.sh @@ -0,0 +1,145 @@ +#!/bin/sh -ex + +MESON_OPTS_COMMON="--auto-features=enabled --fatal-meson-warnings \ + -Dwerror=true -Dwrap_mode=forcefallback" + +MESON_OPTS="$MESON_OPTS_COMMON \ + -Dgame-build-options=optimization=s,b_lto=true \ + -Dsdl2=disabled -Dwayland=disabled -Dx11=disabled" + +SRC_DIR=`pwd` +CI=$SRC_DIR/.ci + +TMP_DIR=$SRC_DIR/q2pro-build +mkdir $TMP_DIR + +export MESON_PACKAGE_CACHE_DIR=$SRC_DIR/subprojects/packagecache + +### Source ### + +REV=$(git rev-list --count HEAD) +SHA=$(git rev-parse --short HEAD) +VER="r$REV~$SHA" +SRC="q2pro-r$REV" + +cd $TMP_DIR +GIT_DIR=$SRC_DIR/.git git archive --format=tar --prefix=$SRC/ HEAD | tar x +echo "$VER" > $SRC/VERSION +rm -rf $SRC/.gitignore $SRC/.ci $SRC/.github +fakeroot tar czf q2pro-source.tar.gz $SRC + +sed -e "s/##VER##/$VER/" -e "s/##DATE##/`date -R`/" $CI/readme-template.txt > README +sed -e "s/##VER##/$VER/" -e "s/##DATE##/`date -R`/" $CI/readme-template-rr.txt > README.rr + +### FFmpeg ### + +cd $TMP_DIR +git clone --depth=1 https://github.com/FFmpeg/FFmpeg.git ffmpeg +cd ffmpeg + +mkdir build-mingw-32 +cd build-mingw-32 +$CI/configure-ffmpeg.sh --win32 $TMP_DIR/ffmpeg-prefix-32 +make -j4 install +cd .. + +mkdir build-mingw-64 +cd build-mingw-64 +$CI/configure-ffmpeg.sh --win64 $TMP_DIR/ffmpeg-prefix-64 +make -j4 install +cd .. + +### Win32 ### + +export PKG_CONFIG_SYSROOT_DIR="$TMP_DIR/ffmpeg-prefix-32" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig" + +cd $TMP_DIR +meson setup --cross-file $CI/i686-w64-mingw32.txt $MESON_OPTS build-mingw-32 $SRC +cd build-mingw-32 +ninja +i686-w64-mingw32-strip q2pro.exe q2proded.exe gamex86.dll + +unix2dos -k -n ../$SRC/LICENSE LICENSE.txt ../$SRC/doc/client.asciidoc MANUAL.txt ../README README.txt +mkdir baseq2 +cp -a ../$SRC/src/client/ui/q2pro.menu baseq2/ +mv gamex86.dll baseq2/ + +zip -9 ../q2pro-client_win32_x86.zip \ + q2pro.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.menu \ + baseq2/gamex86.dll + +unix2dos -k -n ../$SRC/doc/server.asciidoc MANUAL.txt +zip -9 ../q2pro-server_win32_x86.zip \ + q2proded.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt + +### Win64 ### + +export PKG_CONFIG_SYSROOT_DIR="$TMP_DIR/ffmpeg-prefix-64" +export PKG_CONFIG_LIBDIR="$PKG_CONFIG_SYSROOT_DIR/lib/pkgconfig" + +cd $TMP_DIR +meson setup --cross-file $CI/x86_64-w64-mingw32.txt $MESON_OPTS build-mingw-64 $SRC +cd build-mingw-64 +ninja +x86_64-w64-mingw32-strip q2pro.exe q2proded.exe gamex86_64.dll + +unix2dos -k -n ../$SRC/LICENSE LICENSE.txt ../$SRC/doc/client.asciidoc MANUAL.txt ../README README.txt +mkdir baseq2 +cp -a ../$SRC/src/client/ui/q2pro.menu baseq2/ +mv gamex86_64.dll baseq2/ +mv q2pro.exe q2pro64.exe +mv q2proded.exe q2proded64.exe + +zip -9 ../q2pro-client_win64_x64.zip \ + q2pro64.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.menu \ + baseq2/gamex86_64.dll + +unix2dos -k -n ../$SRC/doc/server.asciidoc MANUAL.txt +zip -9 ../q2pro-server_win64_x64.zip \ + q2proded64.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt + +### Win64-rerelease ### + +cd $TMP_DIR +git clone https://github.com/skullernet/q2pro-rerelease-dll.git +cd q2pro-rerelease-dll +meson setup --cross-file $CI/x86_64-w64-mingw32.txt $MESON_OPTS_COMMON build-mingw +ninja -C build-mingw +x86_64-w64-mingw32-strip build-mingw/gamex86_64.dll +cd etc +zip -9 ../build-mingw/q2pro.pkz default.cfg q2pro.menu + +cd $TMP_DIR/build-mingw-64 + +mv q2pro64.exe q2pro.exe +cp -a ../q2pro-rerelease-dll/build-mingw/q2pro.pkz baseq2/ +cp -a ../q2pro-rerelease-dll/build-mingw/gamex86_64.dll baseq2/ +unix2dos -k -n ../$SRC/doc/client.asciidoc MANUAL.txt ../README.rr README.txt + +zip -9 ../q2pro-rerelease-client_win64_x64.zip \ + q2pro.exe \ + LICENSE.txt \ + MANUAL.txt \ + README.txt \ + baseq2/q2pro.pkz \ + baseq2/gamex86_64.dll + +### Version ### + +cd $TMP_DIR +echo $VER > version.txt diff --git a/.ci/readme-template-rr.txt b/.ci/readme-template-rr.txt new file mode 100644 index 000000000..ac6488334 --- /dev/null +++ b/.ci/readme-template-rr.txt @@ -0,0 +1,13 @@ +Welcome to Q2PRO, an enhanced Quake 2 client and server. + +Version ##VER## has been automatically built ##DATE## +from git master branch. + +Installation: extract this archive into new directory, then create shortcut to +q2pro.exe with the following command line: + + q2pro.exe +set basedir "" +set homedir "." + +For information on using and configuring Q2PRO, refer to MANUAL file. + +Project homepage: https://github.com/skullernet/q2pro diff --git a/.ci/readme-template.txt b/.ci/readme-template.txt new file mode 100644 index 000000000..57be9c249 --- /dev/null +++ b/.ci/readme-template.txt @@ -0,0 +1,10 @@ +Welcome to Q2PRO, an enhanced Quake 2 client and server. + +Version ##VER## has been automatically built ##DATE## +from git master branch. + +Installation: extract this archive into your Quake 2 directory. + +For information on using and configuring Q2PRO, refer to MANUAL file. + +Project homepage: https://github.com/skullernet/q2pro diff --git a/.ci/x86_64-w64-mingw32.txt b/.ci/x86_64-w64-mingw32.txt index c61705200..8a2b90c4a 100644 --- a/.ci/x86_64-w64-mingw32.txt +++ b/.ci/x86_64-w64-mingw32.txt @@ -5,6 +5,7 @@ ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' windres = 'x86_64-w64-mingw32-windres' nasm = 'nasm' +pkg-config = 'pkg-config' [host_machine] system = 'windows' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..e0c6988b8 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,48 @@ +name: nightly + +on: + push: + branches: [master] + +permissions: + contents: write + +jobs: + mingw: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/cache@v4 + with: + path: subprojects/packagecache + key: ${{ hashFiles('subprojects/*.wrap') }} + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64 dos2unix nasm meson ninja-build + + - name: Build + run: ./.ci/nightly.sh + + - name: Update release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release edit nightly -t "Nightly" -n "Latest nightly build `cat q2pro-build/version.txt`." --latest + gh release upload nightly q2pro-build/*.zip q2pro-build/*.tar.gz q2pro-build/version.txt --clobber + + - name: Update tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.updateRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: "tags/nightly", + sha: context.sha, + force: true + }) From 47ef036b4580b3d595b39b3346f6105a23405eef Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 27 Sep 2024 18:38:53 +0300 Subject: [PATCH 797/974] Use enum instead of magic constant. --- src/refresh/tess.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index dd6380d37..3d775dc67 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -658,7 +658,7 @@ void GL_Flush3D(void) GL_ArrayBits(array); if (state & GLS_DEFAULT_SKY) { - GL_BindCubemap(tess.texnum[0]); + GL_BindCubemap(tess.texnum[TMU_TEXTURE]); } else if (qglBindTextures) { #if USE_DEBUG if (q_unlikely(gl_nobind->integer)) From 014160f20cf70931e2f6cd44670ccdab5f315042 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 27 Sep 2024 18:52:50 +0300 Subject: [PATCH 798/974] Warn if DECOUPLED_LM lump has invalid offsets. --- src/common/bsp.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/bsp.c b/src/common/bsp.c index 4f93f3344..51ebe84ed 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -404,6 +404,7 @@ int BSP_LoadMaterials(bsp_t *bsp) static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) { mface_t *out; + bool errors; if (filelen % DECOUPLED_LM_BYTES) { Com_WPrintf("DECOUPLED_LM lump has odd size\n"); @@ -416,15 +417,20 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) } out = bsp->faces; + errors = false; for (int i = 0; i < bsp->numfaces; i++, out++) { out->lm_width = BSP_Short(); out->lm_height = BSP_Short(); uint32_t offset = BSP_Long(); - if (offset < bsp->numlightmapbytes) + if (offset == -1) + out->lightmap = NULL; + else if (offset < bsp->numlightmapbytes) out->lightmap = bsp->lightmap + offset; - else + else { out->lightmap = NULL; + errors = true; + } for (int j = 0; j < 2; j++) { BSP_Vector(out->lm_axis[j]); @@ -432,6 +438,9 @@ static void BSP_ParseDecoupledLM(bsp_t *bsp, const byte *in, size_t filelen) } } + if (errors) + Com_WPrintf("DECOUPLED_LM lump possibly corrupted\n"); + bsp->lm_decoupled = true; } From f9705becfa8a4ff3fa8b13d9dc71423ab89d33c8 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 20:58:10 -0400 Subject: [PATCH 799/974] Added check if roundtimelimit >= 1 to make announcement --- src/action/a_team.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index 21342dc04..49d3da224 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2611,7 +2611,7 @@ static qboolean CheckRoundTimeLimit( void ) return true; } - if (use_warnings->value && timewarning < 2) + if (use_warnings->value && timewarning < 2 && roundtimelimit->value >= 1) { roundLimitFrames -= current_round_length; From 2a36ea961d726186cee1ee230902a5932eb0abe6 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 21:23:58 -0400 Subject: [PATCH 800/974] Added bot_reportasclient cvar to de-list bots as players --- src/action/acesrc/acebot.h | 1 + src/action/acesrc/acebot_spawn.c | 29 +++++++++++++++++------------ src/action/botlib/botlib_ai.c | 5 +++-- src/action/botlib/botlib_spawn.c | 14 ++++++++------ src/action/botlib/botlib_utils.c | 2 +- src/action/g_main.c | 1 + src/action/g_save.c | 1 + 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 4ddab4a65..7eb4651d9 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -552,6 +552,7 @@ extern cvar_t* bot_debug; extern cvar_t* bot_count_min; extern cvar_t* bot_count_max; extern cvar_t* bot_rotate; +extern cvar_t* bot_reportasclient; //extern cvar_t* bot_randteamskin; #define MAX_BOT_NAMES 64 diff --git a/src/action/acesrc/acebot_spawn.c b/src/action/acesrc/acebot_spawn.c index 766977bb8..8de02189c 100644 --- a/src/action/acesrc/acebot_spawn.c +++ b/src/action/acesrc/acebot_spawn.c @@ -632,16 +632,18 @@ edict_t *ACESP_SpawnBot( char *team_str, char *name, char *skin, char *userinfo ACESP_PutClientInServer( bot, true, team ); - //rekkie -- Fake Bot Client -- s - // Set the average ping this bot will see - if (random() < 0.85) - bot->bot.bot_baseline_ping = (int)(3 + (random() * 60)); // Country ping - else - bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping + if (bot_reportasclient->value) { + //rekkie -- Fake Bot Client -- s + // Set the average ping this bot will see + if (random() < 0.85) + bot->bot.bot_baseline_ping = (int)(3 + (random() * 60)); // Country ping + else + bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // Country + overseas ping - - SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' - //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + + SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + } game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -700,7 +702,9 @@ void ACESP_RemoveBot(char *name) { //rekkie -- Fake Bot Client -- s - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) { + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + } //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -772,8 +776,9 @@ void ACESP_RemoveTeamplayBot(int team) if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) { + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + } //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e game.bot_count--; diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index e46c1e589..dde2a0058 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -650,8 +650,9 @@ void BOTLIB_Think(edict_t* self) if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; - - SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) { + SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' + } //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index c649b68e8..6431eb4da 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1587,7 +1587,8 @@ edict_t* BOTLIB_SpawnBot(int team, int force_gender, char* force_name, char* for else bot->bot.bot_baseline_ping = (int)(7 + (random() * 227)); // High ping bastard - SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) + SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotConnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' game.bot_count++; //rekkie -- Fake Bot Client -- e @@ -1621,7 +1622,8 @@ void BOTLIB_RemoveBot(char* name) { //rekkie -- Fake Bot Client -- s - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e @@ -1693,8 +1695,8 @@ void BOTLIB_RemoveBot(char* name) } // Fake Bot Client - Disconnect the bot - - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client bot->health = 0; @@ -1740,8 +1742,8 @@ void BOTLIB_RemoveTeamplayBot(int team) //if (random() < 0.20) // Randomly kick a bot { //rekkie -- Fake Bot Client -- s - - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + if (bot_reportasclient->value) + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client //rekkie -- Fake Bot Client -- e diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 707a714c2..15691d5a7 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -117,5 +117,5 @@ void BOTLIB_Debug(const char *debugmsg, ...) { if (!bot_debug->value) return; - gi.dprintf(debugmsg); + gi.dprintf("%s", debugmsg); } diff --git a/src/action/g_main.c b/src/action/g_main.c index 44237b5c6..2ff53b797 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -503,6 +503,7 @@ cvar_t* bot_debug; // Enable bot debug mode cvar_t* bot_count_min; // Minimum number of bots to keep on the server (will range between this and bot_count_max) cvar_t* bot_count_max; // Maximum number of bots to keep on the server (will range between this and bot_count_min) cvar_t* bot_rotate; // Disable/enable rotating bots on the server +cvar_t* bot_reportasclient; // Report bots as clients to the server browser //cvar_t* bot_randteamskin; // Bots can randomize team skins each map //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index 57174eaf5..a3e5f9053 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -700,6 +700,7 @@ void InitGame( void ) bot_count_min = gi.cvar("bot_count_min", "0", 0); bot_count_max = gi.cvar("bot_count_max", "0", 0); bot_rotate = gi.cvar("bot_rotate", "0", 0); + bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); //rekkie -- DEV_1 -- e #endif From 22beb0078efee752e4f775e58199629f197d2dd6 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 22:29:28 -0400 Subject: [PATCH 801/974] Revamped a lot of USE_AQTION macro conditional compilation, change to check if stats are enabled --- src/action/a_ctf.c | 8 ++----- src/action/a_game.c | 11 +++++---- src/action/a_match.c | 12 +++------- src/action/a_team.c | 20 +++++++---------- src/action/g_cmds.c | 4 +--- src/action/g_local.h | 25 +++++++++++++++++++-- src/action/g_spawn.c | 4 +--- src/action/p_client.c | 51 +++++++++--------------------------------- src/action/p_hud.c | 14 +++++------- src/action/tng_stats.c | 24 ++++++++++++++++++++ 10 files changed, 84 insertions(+), 89 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 42a6f6808..1dc4b5d39 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -931,13 +931,9 @@ void CTFCalcScores(void) ctfgame.total2 += game.clients[i].resp.score; } - #if USE_AQTION // Needed to add this here because this is called separately from TallyEndOfLevelTeamScores (teamplay) - if (stat_logs->value) { - LogMatch(); // Generates end of game stats - LogEndMatchStats(); // Generates end of match stats - } - #endif + LOG_MATCH(); // Generates end of game stats + LOG_END_MATCH_STATS(); // Generates end of match stats // Stats: Reset roundNum game.roundNum = 0; // Stats end diff --git a/src/action/a_game.c b/src/action/a_game.c index e17c85e40..ef2303cd7 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1458,13 +1458,16 @@ void GetNearbyTeammates( edict_t *self, char *buf ) #if AQTION_CURL void Cmd_Pickup_f(edict_t* ent) { - char *playername = ent->client->pers.netname; char msg[256]; - snprintf(msg, sizeof(msg), "%s is requesting a pickup at %s (%s:%s)", playername, hostname->string, server_ip->string, net_port->string); - + if (ent->client->pers.netname != NULL && hostname->string != NULL && server_ip->string != NULL && net_port->string != NULL) { + snprintf(msg, sizeof(msg), "%s is requesting a pickup at %s (%s:%s)", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); + } else { + gi.cprintf(ent, PRINT_HIGH, "Error: Unable to send pickup request; missing hostname, server_ip or net_port\n"); + return; + } // 5 minute timer if(message_timer_check(300)) { - lc_discord_webhook(msg, SERVER_MSG, AWARD_NONE); + CALL_DISCORD_WEBHOOK(msg, SERVER_MSG, AWARD_NONE); gi.cprintf(ent, PRINT_HIGH, "Pickup request sent.\n"); } else { gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); diff --git a/src/action/a_match.c b/src/action/a_match.c index 9a60ac38a..b3061a64e 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -85,16 +85,10 @@ void SendScores(void) } gi.bprintf(PRINT_HIGH, "Match is over, waiting for next map, please vote a new one..\n"); - #if USE_AQTION // Needed to add this here because Matchmode does not call BeginIntermission, but other teamplay modes do call it - if (stat_logs->value) { - LogMatch(); // Generates end of game stats - LogEndMatchStats(); // Generates end of match stats - } - #if AQTION_CURL - lc_discord_webhook(MM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); - #endif - #endif + LOG_MATCH(); // Generates end of game stats + LOG_END_MATCH_STATS(); // Generates end of match stats + CALL_DISCORD_WEBHOOK(MM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); // Stats: Reset roundNum game.roundNum = 0; // Stats end diff --git a/src/action/a_team.c b/src/action/a_team.c index 89d1211ff..1d8c6adaa 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2556,7 +2556,7 @@ qboolean CheckTimelimit( void ) timewarning = 1; #if AQTION_CURL if (!game.time_warning_sent && matchmode->value) { - lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); + CALL_DISCORD_WEBHOOK(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); game.time_warning_sent = true; } #endif @@ -2583,7 +2583,7 @@ qboolean CheckTimelimit( void ) timewarning = 1; #if AQTION_CURL if (!game.time_warning_sent && matchmode->value) { - lc_discord_webhook(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); + CALL_DISCORD_WEBHOOK(MM_3_MIN_WARN, SERVER_MSG, AWARD_NONE); game.time_warning_sent = true; } #endif @@ -3002,7 +3002,7 @@ int CheckTeamRules (void) } #if AQTION_CURL if (game.roundNum == 0) // Only announce on game start, so match pauses don't send the msg - lc_discord_webhook(buf, MATCH_START_MSG, AWARD_NONE); + CALL_DISCORD_WEBHOOK(buf, MATCH_START_MSG, AWARD_NONE); #endif CenterPrintAll( buf ); @@ -3909,15 +3909,11 @@ void TallyEndOfLevelTeamScores (void) } // Stats begin - #if USE_AQTION - if (stat_logs->value && !matchmode->value) { - LogMatch(); // Generates end of game stats - LogEndMatchStats(); // Generates end of match logs - } - #if AQTION_CURL - lc_discord_webhook(TP_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); - #endif - #endif + if (!matchmode->value) { + LOG_MATCH(); // Generates end of game stats + LOG_END_MATCH_STATS(); // Generates end of match stats + } + CALL_DISCORD_WEBHOOK(TP_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); // Stats: Reset roundNum game.roundNum = 0; // Stats end diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index f980c6fa1..b14b6dc00 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1467,9 +1467,7 @@ void Cmd_Say_f (edict_t * ent, qboolean team, qboolean arg0, qboolean partner_ms } // Send message to Discord -- this must come before the newline add or it screws up formatting in-game - #if AQTION_CURL - lc_discord_webhook(text, CHAT_MSG, AWARD_NONE); - #endif + CALL_DISCORD_WEBHOOK(text, CHAT_MSG, AWARD_NONE); Q_strncatz(text, "\n", sizeof(text)); diff --git a/src/action/g_local.h b/src/action/g_local.h index 216db47d1..ac45108af 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1721,7 +1721,10 @@ void G_UpdatePlayerStatusbar( edict_t *ent, int force ); int Gamemodeflag(void); int Gamemode(void); #if USE_AQTION +#define GENERATE_UUID() generate_uuid() void generate_uuid(void); +#else +#define GENERATE_UUID() #endif // // p_client.c @@ -1755,13 +1758,28 @@ void StatBotCheck(void); void G_RegisterScore(void); int G_CalcRanks(gclient_t **ranks); void G_LoadScores(void); + +// Compiler macros for stat logging #if USE_AQTION -void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker); -void LogWorldKill(edict_t *self); +#define LOG_KILL(ent, inflictor, attacker) LogKill(ent, inflictor, attacker) +void LogKill(edict_t *ent, edict_t *inflictor, edict_t *attacker); +#define LOG_WORLD_KILL(ent) LogWorldKill(ent) +void LogWorldKill(edict_t *ent); +#define LOG_CAPTURE(capturer) LogCapture(capturer) void LogCapture(edict_t *capturer); +#define LOG_MATCH() LogMatch() void LogMatch(void); +#define LOG_AWARD(ent, award) LogAward(ent, award) void LogAward(edict_t *ent, int award); +#define LOG_END_MATCH_STATS() LogEndMatchStats() void LogEndMatchStats(void); +#else +#define LOG_KILL(ent, inflictor, attacker) +#define LOG_WORLD_KILL(ent) +#define LOG_CAPTURE(capturer) +#define LOG_MATCH() +#define LOG_AWARD(ent, award) +#define LOG_END_MATCH_STATS() #endif //============================================================================ @@ -2970,6 +2988,9 @@ void FireTimedMessages(void); void lc_shutdown_function(void); qboolean lc_init_function(void); void lc_once_per_gameframe(void); +#define CALL_DISCORD_WEBHOOK(msg, type, award) lc_discord_webhook(msg, type, award) void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awardtype); void lc_start_request_function(request_t* request); +#else +#define CALL_DISCORD_WEBHOOK(msg, type, award) // Do nothing if AQTION_CURL is disabled #endif diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 4b4f58b35..bfd55f5c4 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1426,9 +1426,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn Gamemodeflag(); Gamemode(); - #if USE_AQTION - generate_uuid(); // Run this once every time a map loads to generate a unique id for stats (game.matchid) - #endif + GENERATE_UUID(); // Run this once every time a map loads to generate a unique id for stats (game.matchid) #ifndef NO_BOTS // Disconnect bots before we wipe entity data and lose track of is_bot. diff --git a/src/action/p_client.c b/src/action/p_client.c index 1dfa7b848..c8cd6b699 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -389,18 +389,12 @@ void Announce_Reward(edict_t *ent, int rewardType) { return; // Something didn't jive here? } ent->client->resp.awardstats[rewardType]++; - - #if AQTION_CURL - lc_discord_webhook(buf, AWARD_MSG, rewardType); - #endif + CALL_DISCORD_WEBHOOK(buf, AWARD_MSG, rewardType); CenterPrintAll(buf); gi.sound(&g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex(soundFile), 1.0, ATTN_NONE, 0.0); - #if USE_AQTION - if (stat_logs->value) - LogAward(ent, rewardType); - #endif + LOG_AWARD(ent, rewardType); } void Add_Frag(edict_t * ent, int mod) @@ -1180,17 +1174,11 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) self->client->pers.netname, special_message, self->client->attacker->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); - #endif + CALL_DISCORD_WEBHOOK(death_msg, DEATH_MSG, AWARD_NONE); IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(self->client->attacker, self); - #if USE_AQTION - if (stat_logs->value) { // Only create stats logs if stat_logs is 1 - LogKill(self, inflictor, self->client->attacker); - } - #endif + LOG_KILL(self, inflictor, self->client->attacker); self->client->attacker->client->radio_num_kills++; @@ -1215,9 +1203,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) { sprintf( death_msg, "%s %s\n", self->client->pers.netname, message ); //Using discord webhook for death messaging - #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); - #endif + CALL_DISCORD_WEBHOOK(death_msg, DEATH_MSG, AWARD_NONE); PrintDeathMessage(death_msg, self ); IRC_printf( IRC_T_DEATH, death_msg ); @@ -1227,12 +1213,7 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) } self->enemy = NULL; - - #if USE_AQTION - if (stat_logs->value) { // Only create stats logs if stat_logs is 1 - LogWorldKill(self); - } - #endif + LOG_WORLD_KILL(self); } return; } @@ -1571,17 +1552,11 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) message, attacker->client->pers.netname, message2); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); - #endif + CALL_DISCORD_WEBHOOK(death_msg, DEATH_MSG, AWARD_NONE); IRC_printf(IRC_T_KILL, death_msg); AddKilledPlayer(attacker, self); - #if USE_AQTION - if (stat_logs->value) { - LogKill(self, inflictor, attacker); - } - #endif + LOG_KILL(self, inflictor, attacker); if (friendlyFire) { if (!teamplay->value || team_round_going || !ff_afterround->value) @@ -1619,16 +1594,10 @@ void ClientObituary(edict_t * self, edict_t * inflictor, edict_t * attacker) sprintf(death_msg, "%s died\n", self->client->pers.netname); PrintDeathMessage(death_msg, self); //Using discord webhook for death messaging - #if AQTION_CURL - lc_discord_webhook(death_msg, DEATH_MSG, AWARD_NONE); - #endif + CALL_DISCORD_WEBHOOK(death_msg, DEATH_MSG, AWARD_NONE); IRC_printf(IRC_T_DEATH, death_msg); - #if USE_AQTION - if (stat_logs->value) { // Only create stats logs if stat_logs is 1 - LogWorldKill(self); - } - #endif + LOG_WORLD_KILL(self); Subtract_Frag(self); //self->client->resp.score--; Add_Death( self, true ); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index c675339e3..9c643940c 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -132,17 +132,13 @@ void BeginIntermission(edict_t *targ) } else if (teamplay->value) { TallyEndOfLevelTeamScores(); } - #if USE_AQTION + // Generates stats for non-CTF, Teamplay or Matchmode - else if (stat_logs->value && !matchmode->value) { - LogMatch(); - LogEndMatchStats(); // Generates end of match logs + if (!matchmode->value) { + LOG_MATCH(); // Generates end of game stats + LOG_END_MATCH_STATS(); // Generates end of match stats } - #if AQTION_CURL - if(!teamplay->value) - lc_discord_webhook(DM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); - #endif - #endif + CALL_DISCORD_WEBHOOK(DM_MATCH_END_MSG, MATCH_END_MSG, AWARD_NONE); // respawn any dead clients for (i = 0, ent = g_edicts + 1; i < game.maxclients; i++, ent++) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 6382a63db..b9dc2c4ee 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -951,6 +951,10 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } // Only record stats if there's more than one opponent if (gameSettings & GS_DEATHMATCH) // Only check if in DM @@ -1096,6 +1100,10 @@ void LogWorldKill(edict_t *self) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } // Only record stats if there's more than one opponent if (gameSettings & GS_DEATHMATCH) // Only check if in DM @@ -1198,6 +1206,10 @@ void LogCapture(edict_t *capturer) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } int mode = Gamemode(); switch (mode) { @@ -1258,6 +1270,10 @@ void LogMatch(void) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } // Check for scoreless teamplay, don't log if (teamplay->value && t1 == 0 && t2 == 0 && t3 == 0) { @@ -1300,6 +1316,10 @@ void LogAward(edict_t *ent, int award) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } Q_snprintf( msg, sizeof(msg), @@ -1461,6 +1481,10 @@ void LogEndMatchStats(void) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing + if (!stat_logs->value) { + return; + } // Write out the stats for each client still connected to the server for (i = 0; i < totalClients; i++){ From aef64a9551b4ce57bebcda3657711c0c8770d209 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 22:44:48 -0400 Subject: [PATCH 802/974] Removed redundant -lcurl from meson --- meson.build | 1 - src/action/g_local.h | 4 +++- src/action/p_client.c | 17 ++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/meson.build b/meson.build index 5dd77ca78..69ce7e145 100644 --- a/meson.build +++ b/meson.build @@ -567,7 +567,6 @@ endif if get_option('aqtion-curl') config.set10('AQTION_CURL', true) action_src += 'src/action/tng_net.c' - dll_link_args += '-lcurl' game_deps += curl endif diff --git a/src/action/g_local.h b/src/action/g_local.h index ac45108af..91bae137b 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1754,13 +1754,14 @@ void InitTookDamage(void); void ProduceShotgunDamageReport(edict_t*); //tng_stats.c -void StatBotCheck(void); void G_RegisterScore(void); int G_CalcRanks(gclient_t **ranks); void G_LoadScores(void); // Compiler macros for stat logging #if USE_AQTION +#define STAT_BOT_CHECK() StatBotCheck() +void StatBotCheck(void); #define LOG_KILL(ent, inflictor, attacker) LogKill(ent, inflictor, attacker) void LogKill(edict_t *ent, edict_t *inflictor, edict_t *attacker); #define LOG_WORLD_KILL(ent) LogWorldKill(ent) @@ -1774,6 +1775,7 @@ void LogAward(edict_t *ent, int award); #define LOG_END_MATCH_STATS() LogEndMatchStats() void LogEndMatchStats(void); #else +#define STAT_BOT_CHECK() #define LOG_KILL(ent, inflictor, attacker) #define LOG_WORLD_KILL(ent) #define LOG_CAPTURE(capturer) diff --git a/src/action/p_client.c b/src/action/p_client.c index c8cd6b699..b93b4c348 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3311,13 +3311,12 @@ void ClientBeginDeathmatch(edict_t * ent) vInitClient(ent); #ifndef NO_BOTS - ACEIT_RebuildPlayerList(); - if (ent->is_bot) - BOTLIB_SKILL_Init(ent); // Initialize the skill levels + ACEIT_RebuildPlayerList(); + if (ent->is_bot) + BOTLIB_SKILL_Init(ent); // Initialize the skill levels -#if USE_AQTION - StatBotCheck(); -#endif +// Check if bots are in the game, if so, disable stat collection +STAT_BOT_CHECK(); #endif // locate ent at a spawn point @@ -3735,9 +3734,9 @@ void ClientDisconnect(edict_t * ent) ent->is_bot = false; ent->think = NULL; ACEIT_RebuildPlayerList(); -#if USE_AQTION - StatBotCheck(); -#endif + +// Check if bots are in the game, if so, disable stat collection +STAT_BOT_CHECK(); #endif } From 66b861940c36dd69dbba8af4deb993d8d607fbf9 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 23:07:37 -0400 Subject: [PATCH 803/974] Re-arranged writing stats functionality --- src/action/g_local.h | 3 ++ src/action/tng_net.c | 11 ++++--- src/action/tng_net.h | 1 - src/action/tng_stats.c | 72 ++++++++++++++++++++++++++---------------- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 91bae137b..abc0cd066 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2992,7 +2992,10 @@ qboolean lc_init_function(void); void lc_once_per_gameframe(void); #define CALL_DISCORD_WEBHOOK(msg, type, award) lc_discord_webhook(msg, type, award) void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awardtype); +#define CALL_STATS_API(stats) lc_aqtion_stat_send(stats) +qboolean lc_aqtion_stat_send(const char *stats); void lc_start_request_function(request_t* request); #else #define CALL_DISCORD_WEBHOOK(msg, type, award) // Do nothing if AQTION_CURL is disabled +#define CALL_STATS_API(stats) // Do nothing if AQTION_CURL is disabled #endif diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 298c0e49f..33a1efab6 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -766,15 +766,15 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa lc_start_request_function(request); } -void lc_aqtion_stat_send(char *stats) +qboolean lc_aqtion_stat_send(const char* stats) { if (!sv_curl_stat_enable->value) - return; + return false; if (!sv_curl_enable->value) { gi.dprintf("%s: sv_curl_enable is disabled, disabling stat reporting to API\n", __func__); gi.cvar_forceset("sv_curl_stat_enable", "0"); - return; + return false; } request_t *request; @@ -786,7 +786,7 @@ void lc_aqtion_stat_send(char *stats) request = new_request(); if (request == NULL) { gi.dprintf("%s: Ran out of request slots\n", __func__); - return; + return false; } //gi.dprintf("%s: Sending stats to %s\n", __func__, url); // Use webhook.site to test curl, it's very handy! @@ -796,6 +796,9 @@ void lc_aqtion_stat_send(char *stats) request->url = full_url; request->payload = strdup(stats); lc_start_request_function(request); + + // Returning true here just means we sent the request to lc_start_request_function, it doesn't mean it worked + return true; } void lc_server_announce(char *path, char *message) diff --git a/src/action/tng_net.h b/src/action/tng_net.h index 4dedce63c..81162a922 100644 --- a/src/action/tng_net.h +++ b/src/action/tng_net.h @@ -26,5 +26,4 @@ extern size_t current_requests; void lc_server_announce(char *path, char *message); void announce_server_populating(void); -void lc_aqtion_stat_send(char *stats); qboolean message_timer_check(int delay); \ No newline at end of file diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index b9dc2c4ee..0b095547c 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -881,38 +881,56 @@ void StatBotCheck(void) } #endif +qboolean Write_Stats_to_API(const char* stats) +{ + if (!CALL_STATS_API(stats)) { + gi.dprintf("Error sending stats to API\n"); + return false; + } + return true; +} + cvar_t* logfile_name; +qboolean Write_Stats_to_Local(const char* stats) +{ + char logpath[MAX_QPATH]; + FILE* f; + + logfile_name = gi.cvar("logfile_name", "", CVAR_NOSET); + snprintf(logpath, sizeof(logpath), "action/logs/%s.stats", logfile_name->string); // Use snprintf for safety + + // Open log file in append mode + f = fopen(logpath, "a"); + if (f != NULL) { + // Write stats to file + fprintf(f, "%s", stats); + fclose(f); + return true; + } else { + // Log error if file cannot be opened + gi.dprintf("Error writing to %s.stats\n", logfile_name->string); + return false; + } +} + void Write_Stats(const char* msg, ...) { - va_list argptr; - char stat_cpy[1024]; - char logpath[MAX_QPATH]; - FILE* f; - - va_start(argptr, msg); - vsprintf(stat_cpy, msg, argptr); - va_end(argptr); - - // Send stats to API, else write to local file - if (sv_curl_enable->value && sv_curl_stat_enable->value) { - #if AQTION_CURL - lc_aqtion_stat_send(stat_cpy); - #else - return; - #endif - } else { - logfile_name = gi.cvar("logfile_name", "", CVAR_NOSET); - sprintf(logpath, "action/logs/%s.stats", logfile_name->string); + va_list argptr; + char stat_cpy[1024]; - if ((f = fopen(logpath, "a")) != NULL) - { - fprintf(f, "%s", stat_cpy); - fclose(f); - } - else - gi.dprintf("Error writing to %s.stats\n", logfile_name->string); - } + // Initialize variable argument list + va_start(argptr, msg); + vsnprintf(stat_cpy, sizeof(stat_cpy), msg, argptr); // Use vsnprintf for safety + va_end(argptr); + + // Send stats to API, else write to local file + if (sv_curl_enable->value && sv_curl_stat_enable->value) { + if (Write_Stats_to_API(stat_cpy)) + return; + } + // Fallback to writing stats to local file + Write_Stats_to_Local(stat_cpy); } /* From 5e0862dfc6f21d57a84a95dab1aafad9c402431f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sun, 29 Sep 2024 23:39:51 -0400 Subject: [PATCH 804/974] Some cleanup, formatting --- src/action/a_game.c | 10 ++++++---- src/action/tng_net.c | 3 ++- src/client/tent.c | 3 --- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index ef2303cd7..9c524f873 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1459,16 +1459,18 @@ void GetNearbyTeammates( edict_t *self, char *buf ) void Cmd_Pickup_f(edict_t* ent) { char msg[256]; - if (ent->client->pers.netname != NULL && hostname->string != NULL && server_ip->string != NULL && net_port->string != NULL) { - snprintf(msg, sizeof(msg), "%s is requesting a pickup at %s (%s:%s)", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); + if (hostname->string != NULL && strcmp(server_ip->string, "") != 0 && net_port->string != NULL) { + snprintf(msg, sizeof(msg), "%s is requesting a pickup match at %s (%s:%s)", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); } else { - gi.cprintf(ent, PRINT_HIGH, "Error: Unable to send pickup request; missing hostname, server_ip or net_port\n"); + gi.cprintf(ent, PRINT_HIGH, "Error: Unable to send pickup request; contact the server admin\n"); + gi.dprintf("%s: %s attempted to send a pickup request but the server is missing hostname, server_ip or net_port\n", __func__, ent->client->pers.netname); return; } // 5 minute timer if(message_timer_check(300)) { - CALL_DISCORD_WEBHOOK(msg, SERVER_MSG, AWARD_NONE); + CALL_DISCORD_WEBHOOK(msg, PICKUP_REQ_MSG, AWARD_NONE); gi.cprintf(ent, PRINT_HIGH, "Pickup request sent.\n"); + gi.dprintf("** Pickup request sent by %s **\n", ent->client->pers.netname); } else { gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); } diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 33a1efab6..64426c169 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -691,7 +691,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa // Default url is to the #info-feed channel char* url = sv_curl_discord_info_url->string; // Change webhook URL based on message type. If pickup url is disabled, use info url - if (msgtype == SERVER_MSG) { + if (msgtype == SERVER_MSG && msgtype == PICKUP_REQ_MSG) { if (strcmp(sv_curl_discord_pickup_url->string, "disabled") == 0) url = sv_curl_discord_info_url->string; else @@ -724,6 +724,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa break; case AWARD_MSG: + case PICKUP_REQ_MSG: case SERVER_MSG: { char *srvmsg = discord_ServerMsg(message, msgtype, awardtype); if (srvmsg) { diff --git a/src/client/tent.c b/src/client/tent.c index 30f3afcd9..7d7735a02 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1666,7 +1666,6 @@ void CL_ParseTEnt(void) cl.hit_marker_count = te.count; if (cl_hit_markers->integer > 1) { S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); - Com_Printf("Hit marker\n"); } } break; @@ -1674,8 +1673,6 @@ void CL_ParseTEnt(void) default: Com_Error(ERR_DROP, "%s: bad type", __func__); } - Com_Printf("Te count: %d\n", te.count); - Com_Printf("cl_hit_markers: %d\n", cl_hit_markers); } /* From 6d5c3f235d5cbeb76888ee23708c62d94bccf2f1 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Mon, 30 Sep 2024 00:29:51 -0400 Subject: [PATCH 805/974] Some cleanup, menu add, taking care for cases where the option isn't available --- src/action/a_game.c | 55 ++++++++++++++++++++++++++++-------------- src/action/a_game.h | 7 +++++- src/action/a_team.c | 2 ++ src/action/a_vote.h | 2 ++ src/action/a_xvote.c | 14 +++++++++++ src/action/tng_net.c | 2 +- src/action/tng_stats.c | 19 +++++++++++---- 7 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 9c524f873..438620339 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1455,24 +1455,43 @@ void GetNearbyTeammates( edict_t *self, char *buf ) } } -#if AQTION_CURL void Cmd_Pickup_f(edict_t* ent) { - char msg[256]; - if (hostname->string != NULL && strcmp(server_ip->string, "") != 0 && net_port->string != NULL) { - snprintf(msg, sizeof(msg), "%s is requesting a pickup match at %s (%s:%s)", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); - } else { - gi.cprintf(ent, PRINT_HIGH, "Error: Unable to send pickup request; contact the server admin\n"); - gi.dprintf("%s: %s attempted to send a pickup request but the server is missing hostname, server_ip or net_port\n", __func__, ent->client->pers.netname); - return; - } - // 5 minute timer - if(message_timer_check(300)) { - CALL_DISCORD_WEBHOOK(msg, PICKUP_REQ_MSG, AWARD_NONE); - gi.cprintf(ent, PRINT_HIGH, "Pickup request sent.\n"); - gi.dprintf("** Pickup request sent by %s **\n", ent->client->pers.netname); - } else { - gi.cprintf(ent, PRINT_HIGH, "It is too early to send another request, please wait.\n"); - } + #if AQTION_CURL + char msg[256]; + // Check if msgflags supports this + if (!(MSGFLAGS(PICKUP_REQ_MSG))) { + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_ERROR); + gi.dprintf("%s: %s attempted to send a pickup request but msgflags needs to be %d (currently %d)\n", __func__, ent->client->pers.netname, PICKUP_REQ_MSG, msgflags->value); + return; + } + + // Check if we have requisite information to send a pickup request + if (hostname->string != NULL && strcmp(server_ip->string, "") != 0 && net_port->string != NULL) { + snprintf(msg, sizeof(msg), "**%s** is requesting a pickup match at **%s (%s:%s)**", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); + } else { + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_ERROR); + gi.dprintf("%s: %s attempted to send a pickup request but the server is missing hostname, server_ip or net_port\n", __func__, ent->client->pers.netname); + return; + } + + // Check if we're within the 5 minute timer (spam prevention) + if(message_timer_check(300)) { + CALL_DISCORD_WEBHOOK(msg, PICKUP_REQ_MSG, AWARD_NONE); + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_SUCCESS); + gi.dprintf("** Pickup request sent by %s **\n", ent->client->pers.netname); + } else { + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_TOO_EARLY); + } + #else + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_UNSUPPORTED); + #endif } -#endif \ No newline at end of file + +// Menu item +void _PickupRequest (edict_t * ent, pmenu_t * p) +{ + PMenu_Close(ent); + + Cmd_Pickup_f(ent); +} \ No newline at end of file diff --git a/src/action/a_game.h b/src/action/a_game.h index 53d2e88aa..34f50c4b9 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -151,5 +151,10 @@ void Cmd_PrintRules_f(edict_t *ent); //a_game.c #if AQTION_CURL + +#define MSG_PICKUP_SERVER_ERROR "Unable to send pickup request; contact the server admin\n" +#define MSG_PICKUP_SERVER_SUCCESS "Pickup request sent\n" +#define MSG_PICKUP_TOO_EARLY "It is too early to send another request, please wait\n" void Cmd_Pickup_f( edict_t * ent ); -#endif \ No newline at end of file +#endif +#define MSG_PICKUP_UNSUPPORTED "This server does not support pickup requests.\n" diff --git a/src/action/a_team.c b/src/action/a_team.c index 1d8c6adaa..43b04940e 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1154,6 +1154,8 @@ pmenu_t joinmenu[] = { {"MOTD", PMENU_ALIGN_LEFT, NULL, ReprintMOTD}, {"Credits", PMENU_ALIGN_LEFT, NULL, CreditsMenu}, {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, + {"Request a Pickup Game", PMENU_ALIGN_LEFT, NULL, _PickupRequest}, + {NULL, PMENU_ALIGN_LEFT, NULL, NULL}, {"Use arrows to move cursor", PMENU_ALIGN_LEFT, NULL, NULL}, {"ENTER to select", PMENU_ALIGN_LEFT, NULL, NULL}, {"TAB to exit menu", PMENU_ALIGN_LEFT, NULL, NULL}, diff --git a/src/action/a_vote.h b/src/action/a_vote.h index 784b5422d..a82a3cde2 100644 --- a/src/action/a_vote.h +++ b/src/action/a_vote.h @@ -118,3 +118,5 @@ cvar_t *_InitScrambleVote(ini_t *ini); void _CheckScrambleVote(void); void _VoteScrambleSelected(edict_t *ent, pmenu_t *p); void Cmd_Votescramble_f(edict_t *ent); + +void _PickupRequest (edict_t * ent, pmenu_t * p); diff --git a/src/action/a_xvote.c b/src/action/a_xvote.c index 1f200d1ac..5bc0f190c 100644 --- a/src/action/a_xvote.c +++ b/src/action/a_xvote.c @@ -122,7 +122,21 @@ vote_t xvotelist[] = { "Team Scramble", // Votetitle _VoteScrambleSelected, // VoteSelected } + , + { + NULL, // cvar + NULL, // InitGame + NULL, // ExitGame + NULL, // InitLevel + NULL, // ExitLevel + NULL, // InitClient + NULL, // ClientDisconnect + NULL, // Newround + NULL, // CheckVote + "Pickup Request", // Votetitle + _PickupRequest, // VoteSelected + } }; static const int xvlistsize = (sizeof(xvotelist)/sizeof(vote_t)); diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 64426c169..7baa7adcb 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -691,7 +691,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa // Default url is to the #info-feed channel char* url = sv_curl_discord_info_url->string; // Change webhook URL based on message type. If pickup url is disabled, use info url - if (msgtype == SERVER_MSG && msgtype == PICKUP_REQ_MSG) { + if (msgtype == SERVER_MSG || msgtype == PICKUP_REQ_MSG) { if (strcmp(sv_curl_discord_pickup_url->string, "disabled") == 0) url = sv_curl_discord_info_url->string; else diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 0b095547c..b3f86d795 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -881,14 +881,23 @@ void StatBotCheck(void) } #endif +#if AQTION_CURL qboolean Write_Stats_to_API(const char* stats) { - if (!CALL_STATS_API(stats)) { - gi.dprintf("Error sending stats to API\n"); - return false; - } - return true; + if (!CALL_STATS_API(stats)) { + gi.dprintf("Error sending stats to API\n"); + return false; + } + return true; } +#else +qboolean Write_Stats_to_API(const char* stats) +{ + // AQTION_CURL is disabled, so this function does nothing + gi.dprintf("AQTION_CURL is disabled, stats not sent to API\n"); + return false; +} +#endif cvar_t* logfile_name; qboolean Write_Stats_to_Local(const char* stats) From 1a3556bbc00dbc3d9c71e3bc6429e6fe28c569d3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 14:37:49 -0400 Subject: [PATCH 806/974] Added documentation, use_pickup to enable/disable --- doc/action.md | 219 ++++++++++++++++++++----------------------- src/action/a_game.c | 9 +- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_save.c | 2 +- 5 files changed, 113 insertions(+), 119 deletions(-) diff --git a/doc/action.md b/doc/action.md index 2523b48ed..eef3539bf 100644 --- a/doc/action.md +++ b/doc/action.md @@ -6,126 +6,106 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team # Table of Contents -1. [Introduction](#introduction) -2. [Installation](#installation) -3. [Features](#features) +- [Action Quake 2: The Next Generation](#action-quake-2-the-next-generation) +- [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Installation](#installation) + - [Features](#features) - [Mapvoting](#mapvoting) - - [Commands](#commands) + - [Commands](#commands) - [Kickvoting](#kickvoting) - - [Commands](#commands-1) + - [Commands](#commands-1) - [Configvoting](#configvoting) - - [Commands](#commands-2) -5. [Teamplay](#teamplay) - - [Commands](#commands-3) -6. [Tourney](#tourney) - - [Commands](#commands-3) -7. [3 Teams Teamplay](#3-teams-teamplay) - - [Commands](#commands-4) -8. [Capture the Flag](#capture-the-flag) - - [Commands](#commands-5) -9. [Domination](#domination) - - [Commands](#commands-6) -10. [Matchmode](#matchmode) - - [Commands](#commands-7) -11. [Voice Command](#voice-command) - - [Commands](#commands-8) -12. [Low Lag Sounds](#low-lag-sounds) - - [Commands](#commands-9) -13. [Announcer](#announcer) - - [Commands](#commands-10) -14. [Kevlar Helmet](#kevlar-helmet) -15. [Single Barreled Handcannon](#single-barreled-handcannon) - - [Commands](#commands-11) -16. [Enemy Down Radio Reporting](#enemy-down-radio-reporting) -17. [Player Ignoring](#player-ignoring) - - [Commands](#commands-12) -18. [Video Setting Checking](#video-setting-checking) - - [Commands](#commands-13) -19. [Location Files](#location-files) -20. [Punching](#punching) - - [Commands](#commands-14) -21. [Lens command](#lens-command) - - [Commands](#commands-15) -22. [New Say Variables](#new-say-variables) -23. [Roundtimeleft](#roundtimeleft) - - [Commands](#commands-16) -24. [Time](#time) - - [Commands](#commands-17) -25. [sv stuffcmd](#sv-stuffcmd) - - [Commands](#commands-18) -26. [Grenade Strength](#grenade-strength) - - [Commands](#commands-19) -27. [Total Kills](#total-kills) -28. [Random Rotation](#random-rotation) - - [Commands](#commands-20) -29. [Vote Rotation](#vote-rotation) - - [Commands](#commands-21) -30. [MapVote Next](#mapvote-next) - - [Commands](#commands-22) -31. [Empty Rotate](#empty-rotate) - - [Commands](#commands-23) -32. [Bandage Text](#bandage-text) -33. [Deathmatch Weapon](#deathmatch-weapon) - - [Commands](#commands-24) -34. [Control Characters](#control-characters) - - [Commands](#commands-25) -35. [Anti Camping](#anti-camping) - - [Commands](#commands-26) -36. [Anti Idle](#anti-idle) - - [Commands](#commands-27) -37. [Gibs](#gibs) - - [Commands](#commands-28) -38. [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) -39. [Weapon Banning](#weapon-banning) - - [Commands](#commands-29) -40. [Item Banning](#item-banning) - - [Commands](#commands-30) -41. [Teamkilling after a Round](#teamkilling-after-a-round) - - [Commands](#commands-31) -42. [New IR Vision](#new-ir-vision) - - [Commands](#commands-32) -43. [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) - - [Commands](#commands-33) -44. [Darkmatch](#darkmatch) - - [Commands](#commands-34) -45. [Map Restarting](#map-restarting) - - [Commands](#commands-35) -46. [Statistics](#statistics) - - [Commands](#commands-36) -47. [Automatic Joining/Equipping](#automatic-joining-equipping) - - [Commands](#commands-37) -48. [Automatic Demo Recording](#automatic-demo-recording) - - [Commands](#commands-38) -49. [Spawn Code](#spawn-code) - - [Commands](#commands-39) -50. [Ghost](#ghost) - - [Commands](#commands-40) -51. [Bandolier behavior](#bandolier-behavior) - - [Commands](#commands-41) -52. [Bots](#bots) - - [Commands](#commands-42) -53. [Slap](#slap) - - [Commands](#commands-43) -54. [Variable Framerate Support](#variable-framerate-support) - - [Commands](#commands-44) -55. [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) -56. [Latency Compensation](#latency-compensation) - - [Commands](#commands-45) -57. [General quality of life improvements](#general-quality-of-life-improvements) -58. [Client Prediction](#client-prediction) -59. [Force Spawn items](#force-spawn-items) -60. [Attract Mode](#attract-mode) - - [Commands](#commands-46) -61. [Zoom Compensation](#zoom-compensation) -62. [Warmup](#warmup) - - [Commands](#commands-47) -63. [Item Kit Mode](#item-kit-mode) -64. [Print rules](#print-rules) - - [Commands](#commands-48) -65. [Espionage](#espionage) - - [Commands](#commands-49) -66. [Contact Information](#contact-information) -67. [Credits](#credits) + - [Commands](#commands-2) + - [Teamplay](#teamplay) + - [Commands](#commands-3) + - [Tourney](#tourney) + - [Commands](#commands-4) + - [3 Teams Teamplay](#3-teams-teamplay) + - [Commands](#commands-5) + - [Capture the Flag](#capture-the-flag) + - [Commands](#commands-6) + - [Domination](#domination) + - [Commands](#commands-7) + - [Matchmode](#matchmode) + - [Commands](#commands-8) + - [Voice Command](#voice-command) + - [Commands](#commands-9) + - [Low Lag Sounds](#low-lag-sounds) + - [Commands](#commands-10) + - [Announcer](#announcer) + - [Commands](#commands-11) + - [Kevlar Helmet](#kevlar-helmet) + - [Single Barreled Handcannon](#single-barreled-handcannon) + - [Commands](#commands-12) + - [Enemy Down Radio Reporting](#enemy-down-radio-reporting) + - [Player Ignoring](#player-ignoring) + - [Commands](#commands-13) + - [Video Setting Checking](#video-setting-checking) + - [Commands](#commands-14) + - [Location Files](#location-files) + - [Punching](#punching) + - [Commands](#commands-15) + - [Lens command](#lens-command) + - [Commands](#commands-16) + - [New Say Variables](#new-say-variables) + - [Time and Roundtimeleft](#time-and-roundtimeleft) + - [Commands](#commands-17) + - [sv stuffcmd](#sv-stuffcmd) + - [Commands](#commands-18) + - [Grenade Strength](#grenade-strength) + - [Commands](#commands-19) + - [Total Kills](#total-kills) + - [Random Rotation](#random-rotation) + - [Commands](#commands-20) + - [Vote Rotation](#vote-rotation) + - [Commands](#commands-21) + - [MapVote Next](#mapvote-next) + - [Commands](#commands-22) + - [Empty Rotate](#empty-rotate) + - [Commands](#commands-23) + - [Bandage Text](#bandage-text) + - [Deathmatch Weapon](#deathmatch-weapon) + - [Commands](#commands-24) + - [Control Characters](#control-characters) + - [Commands](#commands-25) + - [Anti Camping](#anti-camping) + - [Commands](#commands-26) + - [Anti Idle](#anti-idle) + - [Commands](#commands-27) + - [Gibs](#gibs) + - [Commands](#commands-28) + - [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) + - [Weapon Banning](#weapon-banning) + - [Item Banning](#item-banning) + - [Teamkilling after a Round](#teamkilling-after-a-round) + - [New IR Vision](#new-ir-vision) + - [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) + - [Darkmatch](#darkmatch) + - [Map Restarting](#map-restarting) + - [Statistics](#statistics) + - [Automatic Joining/Equipping/Menu](#automatic-joiningequippingmenu) + - [Automatic Demo Recording](#automatic-demo-recording) + - [Spawn Code](#spawn-code) + - [Ghost](#ghost) + - [Bandolier behavior](#bandolier-behavior) + - [Bots](#bots) + - [Slap](#slap) + - [Variable Framerate Support](#variable-framerate-support) + - [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) + - [Latency Compensation](#latency-compensation) + - [General quality of life improvements](#general-quality-of-life-improvements) + - [Client Prediction](#client-prediction) + - [Force Spawn items](#force-spawn-items) + - [Zoom Compensation](#zoom-compensation) + - [Warmup](#warmup) + - [Item Kit Mode](#item-kit-mode) + - [Print rules](#print-rules) + - [Espionage](#espionage) + - [Gun mechanics/enhancements](#gun-mechanicsenhancements) + - [Outbound messaging](#outbound-messaging) + - [Contact Information](#contact-information) + - [Credits](#credits) --- ## Introduction @@ -679,6 +659,11 @@ Inspired by AQ2:ETE, these additions are optional server vars to create a differ - `gun_dualmk23_enhance [0/1]` - server cvar, default 0. If enabled, this allows both the silencer and the laser sight to be used on the Dual MK23 Pistols. - `use_gren_bonk [0/1]` - server cvar, default 0. If enabled, this enables impact damage of the grenade to cause damage on direct contact with a player. The speed of which the grenade is thrown will determine the damage dealt. Thanks to JukS for the idea and the code. +### Outbound messaging +The latest versions enable the use of libcurl to send outbound communications over HTTP/HTTPS, such as to a Discord webhook or a JSON API. This requires some credentialed information to be stored as cvars, so as a server admin, you are responsible to maintaining these values as secrets, like rcon. +- `use_pickup [0/1]` - server cvar, default 0. This enables the "Request a Pickup Game" option in the menus, as well as the `pickup` command. Requires `sv_curl_enable` to be enabled, msgflags of `128` and `sv_curl_discord_info_url` or `sv_curl_discord_pickup_url` to have correct values +- `pickup` - client command, you can initiate this from the console or bind it to a key. This performs the same functionality as the menu option. Assuming the server is setup correctly, it will send a pickup match request with server information to a Discord channel. To limit spam, there is a 5 minute cooldown before anyone in the server can send another request. + ## Contact Information Contacting the AQ2World Team is easy, join our Discord or visit our forums, or leave a Github Issue. We are always looking for feedback and suggestions. - Discord Server: [https://discord.aq2world.com](https://discord.aq2world.com) diff --git a/src/action/a_game.c b/src/action/a_game.c index 438620339..f44f3e5c4 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1457,6 +1457,13 @@ void GetNearbyTeammates( edict_t *self, char *buf ) void Cmd_Pickup_f(edict_t* ent) { + // Check if pickup messaging is enabled first + + if (!use_pickup->value) { + gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_UNSUPPORTED); + return; + } + #if AQTION_CURL char msg[256]; // Check if msgflags supports this @@ -1478,7 +1485,7 @@ void Cmd_Pickup_f(edict_t* ent) // Check if we're within the 5 minute timer (spam prevention) if(message_timer_check(300)) { CALL_DISCORD_WEBHOOK(msg, PICKUP_REQ_MSG, AWARD_NONE); - gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_SUCCESS); + gi.bprintf(PRINT_HIGH, MSG_PICKUP_SERVER_SUCCESS); gi.dprintf("** Pickup request sent by %s **\n", ent->client->pers.netname); } else { gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_TOO_EARLY); diff --git a/src/action/g_local.h b/src/action/g_local.h index abc0cd066..b27896052 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1360,6 +1360,7 @@ extern cvar_t *server_port; extern cvar_t *sv_last_announce_interval; extern cvar_t *sv_last_announce_time; extern cvar_t *msgflags; +extern cvar_t *use_pickup; //end cUrl integration extern cvar_t *training_mode; // Sets training mode vars diff --git a/src/action/g_main.c b/src/action/g_main.c index 5928c1439..7eada9878 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -566,6 +566,7 @@ cvar_t *sv_last_announce_interval; // Interval between announcements cvar_t *sv_last_announce_time; // Last announcement time cvar_t *server_announce_url; // Server announce URL cvar_t *msgflags; // Message flags (like dmflags) see Discord_Notifications enum for more info +cvar_t *use_pickup; // Enable pickup notifications from the server // end cURL integration cvars cvar_t *training_mode; // Sets training mode vars diff --git a/src/action/g_save.c b/src/action/g_save.c index 666307642..bd1404b84 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -662,7 +662,7 @@ void InitGame( void ) sv_last_announce_interval = gi.cvar("sv_last_announce_interval", "1800", 0); server_announce_url = gi.cvar("server_announce_url", "disabled", 0); msgflags = gi.cvar("msgflags", "0", 0); - + use_pickup = gi.cvar("use_pickup", "0", 0); training_mode = gi.cvar("training_mode", "0", CVAR_LATCH); if (training_mode->value){ From ec19d7368c87ec3ab94710ea0fd8ca7504b1a283 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 14:50:25 -0400 Subject: [PATCH 807/974] Synced with skuller build changes --- .gitignore | 1 + subprojects/khr-headers.wrap | 6 +- subprojects/libcurl.wrap | 12 +- subprojects/packagefiles/curl/meson.build | 232 ++++++++++++++++++ .../packagefiles/curl/meson_options.txt | 1 + subprojects/packagefiles/khr-headers.tar.xz | Bin 0 -> 111740 bytes .../packagefiles/openal-soft/meson.build | 3 + 7 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 subprojects/packagefiles/curl/meson.build create mode 100644 subprojects/packagefiles/curl/meson_options.txt create mode 100644 subprojects/packagefiles/khr-headers.tar.xz create mode 100644 subprojects/packagefiles/openal-soft/meson.build diff --git a/.gitignore b/.gitignore index b58d1222e..5d3374bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /subprojects/*/ +!/subprojects/packagefiles/ /tags /TAGS /.vs/* diff --git a/subprojects/khr-headers.wrap b/subprojects/khr-headers.wrap index 3e3caa844..cedbb6bd9 100644 --- a/subprojects/khr-headers.wrap +++ b/subprojects/khr-headers.wrap @@ -1,5 +1,3 @@ [wrap-file] -directory = khr-headers-20220530 -source_url = https://skuller.net/meson/khr-headers-20220530.zip -source_filename = khr-headers-20220530.zip -source_hash = c90019de9f55afae8e6a8b15ae4e23d921b98c1571af9fcfd5525df059a160f3 +directory = khr-headers +source_filename = khr-headers.tar.xz diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index df7c7745c..ce2622169 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,9 @@ [wrap-file] -directory = curl-8.9.1 -source_url = https://curl.se/download/curl-8.9.1.tar.xz -source_filename = curl-8.9.1.tar.xz -source_hash = f292f6cc051d5bbabf725ef85d432dfeacc8711dd717ea97612ae590643801e5 -patch_url = https://skuller.net/meson/libcurl_8.9.1-1_patch.zip -patch_filename = libcurl_8.9.1-1_patch.zip -patch_hash = b7f4ef775d912da335387b222dc1caf421fb434cc36aeee84a032fb28cac5c77 +directory = curl-8.10.1 +source_url = https://curl.se/download/curl-8.10.1.tar.xz +source_filename = curl-8.10.1.tar.xz +source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee +patch_directory = curl [provide] libcurl = libcurl_dep diff --git a/subprojects/packagefiles/curl/meson.build b/subprojects/packagefiles/curl/meson.build new file mode 100644 index 000000000..919fd9c99 --- /dev/null +++ b/subprojects/packagefiles/curl/meson.build @@ -0,0 +1,232 @@ +project('libcurl', 'c', version: '8.10.1', license: 'bsd') + +src = [ + 'lib/vauth/cleartext.c', + 'lib/vauth/cram.c', + 'lib/vauth/digest.c', + 'lib/vauth/digest_sspi.c', + 'lib/vauth/gsasl.c', + 'lib/vauth/krb5_gssapi.c', + 'lib/vauth/krb5_sspi.c', + 'lib/vauth/ntlm.c', + 'lib/vauth/ntlm_sspi.c', + 'lib/vauth/oauth2.c', + 'lib/vauth/spnego_gssapi.c', + 'lib/vauth/spnego_sspi.c', + 'lib/vauth/vauth.c', + + 'lib/vtls/bearssl.c', + 'lib/vtls/gtls.c', + 'lib/vtls/hostcheck.c', + 'lib/vtls/keylog.c', + 'lib/vtls/mbedtls.c', + 'lib/vtls/mbedtls_threadlock.c', + 'lib/vtls/openssl.c', + 'lib/vtls/rustls.c', + 'lib/vtls/schannel.c', + 'lib/vtls/schannel_verify.c', + 'lib/vtls/sectransp.c', + 'lib/vtls/vtls.c', + 'lib/vtls/wolfssl.c', + 'lib/vtls/x509asn1.c', + + 'lib/vquic/curl_msh3.c', + 'lib/vquic/curl_ngtcp2.c', + 'lib/vquic/curl_osslq.c', + 'lib/vquic/curl_quiche.c', + 'lib/vquic/vquic.c', + 'lib/vquic/vquic-tls.c', + + 'lib/vssh/libssh.c', + 'lib/vssh/libssh2.c', + 'lib/vssh/wolfssh.c', + + 'lib/altsvc.c', + 'lib/amigaos.c', + 'lib/asyn-ares.c', + 'lib/asyn-thread.c', + 'lib/base64.c', + 'lib/bufq.c', + 'lib/bufref.c', + 'lib/c-hyper.c', + 'lib/cf-h1-proxy.c', + 'lib/cf-h2-proxy.c', + 'lib/cf-haproxy.c', + 'lib/cf-https-connect.c', + 'lib/cf-socket.c', + 'lib/cfilters.c', + 'lib/conncache.c', + 'lib/connect.c', + 'lib/content_encoding.c', + 'lib/cookie.c', + 'lib/curl_addrinfo.c', + 'lib/curl_des.c', + 'lib/curl_endian.c', + 'lib/curl_fnmatch.c', + 'lib/curl_get_line.c', + 'lib/curl_gethostname.c', + 'lib/curl_gssapi.c', + 'lib/curl_memrchr.c', + 'lib/curl_multibyte.c', + 'lib/curl_ntlm_core.c', + 'lib/curl_path.c', + 'lib/curl_range.c', + 'lib/curl_rtmp.c', + 'lib/curl_sasl.c', + 'lib/curl_sha512_256.c', + 'lib/curl_sspi.c', + 'lib/curl_threads.c', + 'lib/curl_trc.c', + 'lib/cw-out.c', + 'lib/dict.c', + 'lib/dllmain.c', + 'lib/doh.c', + 'lib/dynbuf.c', + 'lib/dynhds.c', + 'lib/easy.c', + 'lib/easygetopt.c', + 'lib/easyoptions.c', + 'lib/escape.c', + 'lib/file.c', + 'lib/fileinfo.c', + 'lib/fopen.c', + 'lib/formdata.c', + 'lib/ftp.c', + 'lib/ftplistparser.c', + 'lib/getenv.c', + 'lib/getinfo.c', + 'lib/gopher.c', + 'lib/hash.c', + 'lib/headers.c', + 'lib/hmac.c', + 'lib/hostasyn.c', + 'lib/hostip.c', + 'lib/hostip4.c', + 'lib/hostip6.c', + 'lib/hostsyn.c', + 'lib/hsts.c', + 'lib/http.c', + 'lib/http1.c', + 'lib/http2.c', + 'lib/http_aws_sigv4.c', + 'lib/http_chunks.c', + 'lib/http_digest.c', + 'lib/http_negotiate.c', + 'lib/http_ntlm.c', + 'lib/http_proxy.c', + 'lib/idn.c', + 'lib/if2ip.c', + 'lib/imap.c', + 'lib/inet_ntop.c', + 'lib/inet_pton.c', + 'lib/krb5.c', + 'lib/ldap.c', + 'lib/llist.c', + 'lib/macos.c', + 'lib/md4.c', + 'lib/md5.c', + 'lib/memdebug.c', + 'lib/mime.c', + 'lib/mprintf.c', + 'lib/mqtt.c', + 'lib/multi.c', + 'lib/netrc.c', + 'lib/nonblock.c', + 'lib/noproxy.c', + 'lib/openldap.c', + 'lib/parsedate.c', + 'lib/pingpong.c', + 'lib/pop3.c', + 'lib/progress.c', + 'lib/psl.c', + 'lib/rand.c', + 'lib/rename.c', + 'lib/request.c', + 'lib/rtsp.c', + 'lib/select.c', + 'lib/sendf.c', + 'lib/setopt.c', + 'lib/sha256.c', + 'lib/share.c', + 'lib/slist.c', + 'lib/smb.c', + 'lib/smtp.c', + 'lib/socketpair.c', + 'lib/socks.c', + 'lib/socks_gssapi.c', + 'lib/socks_sspi.c', + 'lib/speedcheck.c', + 'lib/splay.c', + 'lib/strcase.c', + 'lib/strdup.c', + 'lib/strerror.c', + 'lib/strtok.c', + 'lib/strtoofft.c', + 'lib/system_win32.c', + 'lib/telnet.c', + 'lib/tftp.c', + 'lib/timediff.c', + 'lib/timeval.c', + 'lib/transfer.c', + 'lib/url.c', + 'lib/urlapi.c', + 'lib/version.c', + 'lib/version_win32.c', + 'lib/warnless.c', + 'lib/ws.c', +] + +if host_machine.system() != 'windows' + error('Only Windows supported') +endif + +cc = meson.get_compiler('c') + +args = [ + '-DBUILDING_LIBCURL', + '-DUSE_SCHANNEL', + '-DUSE_WINDOWS_SSPI', + '-DUSE_IPV6', + '-DCURL_STATICLIB', + '-DHTTP_ONLY', + '-DCURL_DISABLE_CRYPTO_AUTH', +] + +if get_option('windows-xp-compat') + args += '-D_WIN32_WINNT=0x0501' +else + args += '-D_WIN32_WINNT=0x0601' +endif + +zlib = dependency('zlib', required: false) +if zlib.found() + args += '-DHAVE_LIBZ' +endif + +deps = [ + cc.find_library('advapi32'), + cc.find_library('bcrypt'), + cc.find_library('crypt32'), + cc.find_library('ws2_32'), + zlib, +] + +nghttp2 = dependency('libnghttp2', required: false) +if nghttp2.found() and cc.has_header_symbol('sspi.h', 'SecApplicationProtocolNegotiationExt_ALPN', + prefix: '#include ', args: ['-DSECURITY_WIN32=1']) + args += ['-DUSE_NGHTTP2', '-DHAS_ALPN=1'] + deps += nghttp2 +endif + +libcurl = static_library('curl', src, + c_args: args, + dependencies: deps, + include_directories: ['include', 'lib'], +) + +libcurl_dep = declare_dependency( + compile_args: ['-DCURL_STATICLIB'], + dependencies: deps, + include_directories: 'include', + link_with: libcurl, +) diff --git a/subprojects/packagefiles/curl/meson_options.txt b/subprojects/packagefiles/curl/meson_options.txt new file mode 100644 index 000000000..0d5cff144 --- /dev/null +++ b/subprojects/packagefiles/curl/meson_options.txt @@ -0,0 +1 @@ +option('windows-xp-compat', type: 'boolean', value: false, yield: true) diff --git a/subprojects/packagefiles/khr-headers.tar.xz b/subprojects/packagefiles/khr-headers.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..98bb44528dad5726d9329fce969c086ff54c39d0 GIT binary patch literal 111740 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 literal 0 HcmV?d00001 diff --git a/subprojects/packagefiles/openal-soft/meson.build b/subprojects/packagefiles/openal-soft/meson.build new file mode 100644 index 000000000..e33717f57 --- /dev/null +++ b/subprojects/packagefiles/openal-soft/meson.build @@ -0,0 +1,3 @@ +project('openal-soft', version: '1.23.1') + +openal_dep = declare_dependency(include_directories: 'include') From eb818441637629f7f55a6c84e2ee252777cf3696 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 14:54:17 -0400 Subject: [PATCH 808/974] Missed updating OpenAL-soft --- subprojects/openal-soft.wrap | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/subprojects/openal-soft.wrap b/subprojects/openal-soft.wrap index 89aefc3d9..8701045bb 100644 --- a/subprojects/openal-soft.wrap +++ b/subprojects/openal-soft.wrap @@ -1,11 +1,10 @@ [wrap-file] -directory = openal-soft-1.22.2 -source_url = https://github.com/kcat/openal-soft/releases/download/1.22.2/openal-soft-1.22.2.tar.bz2 -source_filename = openal-soft-1.22.2.tar.bz2 -source_hash = ae94cc95cda76b7cc6e92e38c2531af82148e76d3d88ce996e2928a1ea7c3d20 -patch_url = https://skuller.net/meson/openal-soft_1.22.2-1_patch.zip -patch_filename = openal-soft_1.22.2-1_patch.zip -patch_hash = 8f0df33a04aec9fbac053476a9d68fd201bc74ed6ba62fce00138b71eb583db0 +directory = openal-soft-1.23.1 +source_url = https://github.com/kcat/openal-soft/releases/download/1.23.1/openal-soft-1.23.1.tar.bz2 +source_fallback_url = https://openal-soft.org/openal-releases/openal-soft-1.23.1.tar.bz2 +source_filename = openal-soft-1.23.1.tar.bz2 +source_hash = 796f4b89134c4e57270b7f0d755f0fa3435b90da437b745160a49bd41c845b21 +patch_directory = openal-soft [provide] openal = openal_dep From d82b6cf72ec6e9f4f917635d7409a96e9998505c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 14:55:39 -0400 Subject: [PATCH 809/974] Updated libjpeg-turbo and linpng as well --- subprojects/libjpeg-turbo.wrap | 18 +++++++++--------- subprojects/libpng.wrap | 16 ++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/subprojects/libjpeg-turbo.wrap b/subprojects/libjpeg-turbo.wrap index 71f38a08c..f9eda5fb3 100644 --- a/subprojects/libjpeg-turbo.wrap +++ b/subprojects/libjpeg-turbo.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = libjpeg-turbo-3.0.3 -source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.3/libjpeg-turbo-3.0.3.tar.gz -source_filename = libjpeg-turbo-3.0.3.tar.gz -source_hash = 343e789069fc7afbcdfe44dbba7dbbf45afa98a15150e079a38e60e44578865d -patch_filename = libjpeg-turbo_3.0.3-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.3-1/get_patch -patch_hash = 8bbf4f205e54e73c48c14dae7fcc71f152cdc8fea6d55a1af1e3108d10c6a2e4 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.3-1/libjpeg-turbo-3.0.3.tar.gz -wrapdb_version = 3.0.3-1 +directory = libjpeg-turbo-3.0.4 +source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.4/libjpeg-turbo-3.0.4.tar.gz +source_filename = libjpeg-turbo-3.0.4.tar.gz +source_hash = 99130559e7d62e8d695f2c0eaeef912c5828d5b84a0537dcb24c9678c9d5b76b +patch_filename = libjpeg-turbo_3.0.4-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.4-2/get_patch +patch_hash = f12d14c6ff4ae83eddc1a2e8cfd96a9b7a40b6bdc8ff345ca8094abf4c3da928 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.4-2/libjpeg-turbo-3.0.4.tar.gz +wrapdb_version = 3.0.4-2 [provide] dependency_names = libjpeg, libturbojpeg diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index d32e859a4..6632152df 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = libpng-1.6.43 -source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz -source_filename = libpng-1.6.43.tar.xz -source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c -patch_filename = libpng_1.6.43-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-2/get_patch -patch_hash = 49951297edf03e81d925ab03726555f09994ad1ed78fb539a269216430eef3da -wrapdb_version = 1.6.43-2 +directory = libpng-1.6.44 +source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.44.tar.xz +source_filename = libpng-1.6.44.tar.xz +source_hash = 60c4da1d5b7f0aa8d158da48e8f8afa9773c1c8baa5d21974df61f1886b8ce8e +patch_filename = libpng_1.6.44-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.44-1/get_patch +patch_hash = 394b07614c45fbd1beac8b660386216a490fe12f841a1a445799b676c9c892fb +wrapdb_version = 1.6.44-1 [provide] libpng = libpng_dep From 65b6fa00ab3e1fe4bc2e250665326ca8b436ac17 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 15:07:43 -0400 Subject: [PATCH 810/974] Removed unneeded preprocessor --- src/action/g_cmds.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index b14b6dc00..9aab38558 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -2002,9 +2002,7 @@ static cmdList_t commandList[] = { "volunteer", Cmd_Volunteer_f, 0}, { "leader", Cmd_Volunteer_f, 0}, { "highscores", Cmd_HighScores_f, 0}, -#if AQTION_CURL { "pickup", Cmd_Pickup_f, 0}, -#endif }; #define MAX_COMMAND_HASH 64 From afe086487e572200c24d321bddd8424da8840133 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 15:11:27 -0400 Subject: [PATCH 811/974] Somehow git is not picking up changes to g_cmds.c --- src/action/g_cmds.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 9aab38558..3c48ea82c 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -2003,6 +2003,7 @@ static cmdList_t commandList[] = { "leader", Cmd_Volunteer_f, 0}, { "highscores", Cmd_HighScores_f, 0}, { "pickup", Cmd_Pickup_f, 0}, + }; #define MAX_COMMAND_HASH 64 From 810b0ec2250fd14c80244b2a09a0195b9e9d909c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 15:25:39 -0400 Subject: [PATCH 812/974] MSVC is a joke --- src/action/g_cmds.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 3c48ea82c..b5175dffe 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1185,11 +1185,9 @@ static void Cmd_Players_f (edict_t * ent) { int i; int count = 0; - char small[64]; - char large[1024]; + char small[64], large[1024]; gclient_t *sortedClients[MAX_CLIENTS], *cl; - if (!teamplay->value || !noscore->value) count = G_SortedClients( sortedClients ); else From 0fa8cf4d8134879295c69d0031aa0ad8e8a53b6d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 15:31:29 -0400 Subject: [PATCH 813/974] Windows is amazing --- src/action/g_cmds.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index b5175dffe..3db9f58dc 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1185,7 +1185,8 @@ static void Cmd_Players_f (edict_t * ent) { int i; int count = 0; - char small[64], large[1024]; + char _small[64]; + char _large[1024]; gclient_t *sortedClients[MAX_CLIENTS], *cl; if (!teamplay->value || !noscore->value) @@ -1194,28 +1195,28 @@ static void Cmd_Players_f (edict_t * ent) count = G_NotSortedClients( sortedClients ); // print information - large[0] = 0; + _large[0] = 0; for (i = 0; i < count; i++) { cl = sortedClients[i]; if (!teamplay->value || !noscore->value) - Q_snprintf (small, sizeof (small), "%3i %s\n", + Q_snprintf (_small, sizeof (_small), "%3i %s\n", cl->ps.stats[STAT_FRAGS], cl->pers.netname ); else - Q_snprintf (small, sizeof (small), "%s\n", + Q_snprintf (_small, sizeof (_small), "%s\n", cl->pers.netname); - if (strlen(small) + strlen(large) > sizeof (large) - 20) + if (strlen(_small) + strlen(_large) > sizeof (_large) - 20) { // can't print all of them in one packet - strcat (large, "...\n"); + strcat (_large, "...\n"); break; } - strcat (large, small); + strcat (_large, _small); } - gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); + gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", _large, count); } /* From 40cb4613230e99a808ae778c4a4514fcbd5f64ca Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 16:48:05 -0400 Subject: [PATCH 814/974] Added support for multiple game modes --- src/action/g_local.h | 1 + src/action/tng_net.c | 187 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 175 insertions(+), 13 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index b27896052..5e18177ee 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -449,6 +449,7 @@ typedef enum { #define MM_MATCH_END_MSG "Matchmode Results" #define DM_MATCH_END_MSG "Deathmatch Results" #define MM_3_MIN_WARN "3 minutes remaining in the map" +#define PICKUP_GAME_REQUEST "A pickup game has been started" #define TP_MATCH_START_MSG "Match is about to begin!" #define TP_MATCH_END_MSG "Match has ended!" diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 7baa7adcb..cddb56ffd 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -329,12 +329,27 @@ char* TeamConstructPlayerList(int team) { static char* ConstructGameSettingsString(void) { // Calculate the required buffer size - int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nuse_xerp %s\ndmflags %s\n", - tgren->string, - timelimit->string, - use_xerp->string, - dmflags->string - ); + int buffer_size; + + if (matchmode->value){ + int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n", + tgren->string, + timelimit->string, + roundtimelimit->string, + use_xerp->string, + dmflags->string + ); + } else { + int buffer_size = snprintf(NULL, 0, "gamemode %s\ntgren %s\nfraglimit %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n", + gm->string, + tgren->string, + fraglimit->string, + timelimit->string, + roundtimelimit->string, + use_xerp->string, + dmflags->string + ); + } // Allocate the buffer char *result = (char *)malloc(buffer_size + 1); @@ -343,12 +358,25 @@ static char* ConstructGameSettingsString(void) { } // Construct the string - sprintf(result, "```tgren %s\ntimelimit %s\nuse_xerp %s\ndmflags %s\n```", - tgren->string, - timelimit->string, - use_xerp->string, - dmflags->string - ); + if (matchmode->value) { + sprintf(result, "```tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", + tgren->string, + timelimit->string, + roundtimelimit->string, + use_xerp->string, + dmflags->string + ); + } else { + sprintf(result, "```gamemode %s\ntgren %s\nfraglimit %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", + gm->string, + tgren->string, + fraglimit->string, + timelimit->string, + roundtimelimit->string, + use_xerp->string, + dmflags->string + ); + } return result; } @@ -523,6 +551,131 @@ static char *discord_MatchEndMsg(char* msg) return json_payload; } +static char *discord_PickupReqMsg(char* msg) +{ + // Create the root object + json_t *root = json_object(); + + json_object_set_new(root, "content", json_string("")); + + // Create the "embeds" array + json_t *embeds = json_array(); + json_object_set_new(root, "embeds", embeds); + + // Create the embed object + json_t *embed = json_object(); + json_array_append_new(embeds, embed); + + // Adjust description and color based on mode + if (esp->value) { + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(65280)); // Green + } else if (teamdm->value) { // + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(65535)); // Light blue + } else if (ctf->value) { + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(16776960)); // Yellow + } else if (teamplay->value) { + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(8388736)); // Purple + } else if (use_tourney->value) { + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(16744192)); // Orange + } else { //Deathmatch + json_object_set_new(embed, "description", json_string(msg)); + json_object_set_new(embed, "color", json_integer(16711680)); // Red + } + + // Add fields to the embed object + json_object_set_new(embed, "title", json_string(level.mapname)); + + // Create the "thumbnail" object + json_t *thumbnail = json_object(); + json_object_set_new(embed, "thumbnail", thumbnail); + + char mapimgurl[512]; + snprintf(mapimgurl, sizeof(mapimgurl), "https://raw.githubusercontent.com/vrolse/AQ2-pickup-bot/main/thumbnails/%s.jpg", level.mapname); + json_object_set_new(thumbnail, "url", json_string(mapimgurl)); + + // Add "username" field to the root object + json_object_set_new(root, "username", json_string(PICKUP_GAME_REQUEST)); + + // Create the "author" object (hostname) + json_t *author = json_object(); + json_object_set_new(author, "name", json_string(hostname->string)); + json_object_set_new(embed, "author", author); + + // Create the "footer" object (server ip and port) if server_ip is valid + if (is_valid_ipv4(server_ip->string)) { + json_t *footer = json_object(); + char footerinfo[64]; + snprintf(footerinfo, sizeof(footerinfo), "%s: %s:%s", hostname->string, server_ip->string, net_port->string); + json_object_set_new(footer, "text", json_string(footerinfo)); + json_object_set_new(embed, "footer", footer); + } + + // Create the "fields" array + json_t *fields = json_array(); + json_object_set_new(embed, "fields", fields); + + // Create field objects and add them to the "fields" array + if (teamplay->value) { + for (int team = TEAM1; team <= teamCount; team++) { + char *team_name = TeamName(team); + char field_content[64]; + snprintf(field_content, sizeof(field_content), "%s", team_name); + char *team_players = TeamConstructPlayerList(team); + + // Add triple backticks for Discord formatting + char discord_formatted_players[1280]; + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); + + + json_t *field = json_object(); + json_object_set_new(field, "name", json_string(field_content)); + json_object_set_new(field, "value", json_string(discord_formatted_players)); + json_object_set_new(field, "inline", json_true()); + json_array_append_new(fields, field); + + free(team_players); // Free the allocated memory for team_players + } + } else { + char *team_name = "Players"; + char field_content[64]; + snprintf(field_content, sizeof(field_content), "%s", team_name); + char *team_players = TeamConstructPlayerList(0); + + // Add triple backticks for Discord formatting + char discord_formatted_players[1280]; + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); + + + json_t *field = json_object(); + json_object_set_new(field, "name", json_string(field_content)); + json_object_set_new(field, "value", json_string(discord_formatted_players)); + json_object_set_new(field, "inline", json_true()); + json_array_append_new(fields, field); + + free(team_players); // Free the allocated memory for team_players + } + + // Game Settings + json_t *gamesettings = json_object(); + json_object_set_new(gamesettings, "name", json_string("Game Settings")); + json_object_set_new(gamesettings, "value", json_string(ConstructGameSettingsString())); + json_object_set_new(gamesettings, "inline", json_true()); + json_array_append_new(fields, gamesettings); + + // Convert the JSON object to a string + char *json_payload = json_dumps(root, JSON_INDENT(4)); + + // Decrement the reference count of the root object to free memory + json_decref(root); + + return json_payload; +} + static char *discord_MatchStartMsg(char* msg) { // Create the root object @@ -724,7 +877,6 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa break; case AWARD_MSG: - case PICKUP_REQ_MSG: case SERVER_MSG: { char *srvmsg = discord_ServerMsg(message, msgtype, awardtype); if (srvmsg) { @@ -734,6 +886,15 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa break; } + case PICKUP_REQ_MSG: { + char *pickupreqmsg = discord_PickupReqMsg(message); + if (pickupreqmsg) { + snprintf(json_payload, sizeof(json_payload), "%s", pickupreqmsg); + free(pickupreqmsg); + } + break; + } + case MATCH_START_MSG: { char *matchstartmsg = discord_MatchStartMsg(message); if (matchstartmsg) { From 0d9d1978c12aa2e25e4929a05bb9a544c552facf Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 16:54:53 -0400 Subject: [PATCH 815/974] Changed some preprocessing items, reverting small/large in Cmd_Players_f to test --- src/action/botlib/botlib_utils.c | 3 ++- src/action/g_cmds.c | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 707a714c2..598ec57df 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -2,7 +2,8 @@ #include "../acesrc/acebot.h" #include "botlib.h" -#if defined(_WIN32) || defined(_WIN64) +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN #include #else #include diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 3db9f58dc..d4988bc89 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1185,8 +1185,8 @@ static void Cmd_Players_f (edict_t * ent) { int i; int count = 0; - char _small[64]; - char _large[1024]; + char small[64]; + char large[1024]; gclient_t *sortedClients[MAX_CLIENTS], *cl; if (!teamplay->value || !noscore->value) @@ -1195,28 +1195,28 @@ static void Cmd_Players_f (edict_t * ent) count = G_NotSortedClients( sortedClients ); // print information - _large[0] = 0; + large[0] = 0; for (i = 0; i < count; i++) { cl = sortedClients[i]; if (!teamplay->value || !noscore->value) - Q_snprintf (_small, sizeof (_small), "%3i %s\n", + Q_snprintf (small, sizeof (small), "%3i %s\n", cl->ps.stats[STAT_FRAGS], cl->pers.netname ); else - Q_snprintf (_small, sizeof (_small), "%s\n", + Q_snprintf (small, sizeof (small), "%s\n", cl->pers.netname); - if (strlen(_small) + strlen(_large) > sizeof (_large) - 20) + if (strlen(small) + strlen(large) > sizeof (large) - 20) { // can't print all of them in one packet - strcat (_large, "...\n"); + strcat (large, "...\n"); break; } - strcat (_large, _small); + strcat (large, small); } - gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", _large, count); + gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); } /* From ac462b16ba81b8c357612db89f10a0cc48c3d26f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:07:47 -0400 Subject: [PATCH 816/974] Moved Windows-specific botlib stuff to its own file --- meson.build | 1 + src/action/botlib/botlib_utils.c | 15 +++------------ src/action/tng_net.c | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/meson.build b/meson.build index 69ce7e145..41b2b3c6d 100644 --- a/meson.build +++ b/meson.build @@ -561,6 +561,7 @@ if get_option('aqtion-build') dll_link_args += '-lws2_32' dll_link_args += '-lcrypt32' dll_link_args += '-lwsock32' + botlib_src += 'src/action/botlib/botlib_win.c' endif endif diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 598ec57df..eb92b85ec 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -2,28 +2,19 @@ #include "../acesrc/acebot.h" #include "botlib.h" -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#else +#ifndef _WIN32 #include -#endif /* -This file is for common utilties that are used by the botlib functions +This file is for common utilities that are used by the botlib functions */ void seed_random_number_generator(void) { -#if _MSC_VER >= 1920 && !__INTEL_COMPILER - LARGE_INTEGER li; - QueryPerformanceCounter(&li); - srand((unsigned int)(li.QuadPart)); -#else struct timeval tv; gettimeofday(&tv, NULL); srand(tv.tv_usec * tv.tv_sec); -#endif } +#endif void BOTLIB_SKILL_Init(edict_t* bot) { diff --git a/src/action/tng_net.c b/src/action/tng_net.c index cddb56ffd..64fff8950 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -329,7 +329,7 @@ char* TeamConstructPlayerList(int team) { static char* ConstructGameSettingsString(void) { // Calculate the required buffer size - int buffer_size; + int buffer_size = 0; if (matchmode->value){ int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n", From bdf7b9920321b373459a5cc886e550c3cdbcd68a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:15:21 -0400 Subject: [PATCH 817/974] helps to add your source files --- src/action/botlib/botlib_win.c | 15 +++++++++++++++ src/action/tng_net.c | 1 + 2 files changed, 16 insertions(+) create mode 100644 src/action/botlib/botlib_win.c diff --git a/src/action/botlib/botlib_win.c b/src/action/botlib/botlib_win.c new file mode 100644 index 000000000..5fafb3751 --- /dev/null +++ b/src/action/botlib/botlib_win.c @@ -0,0 +1,15 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include + +void seed_random_number_generator(void) { +#if _MSC_VER >= 1920 && !__INTEL_COMPILER + LARGE_INTEGER li; + QueryPerformanceCounter(&li); + srand((unsigned int)(li.QuadPart)); +#else + // Fallback for other Windows compilers + srand((unsigned int)time(NULL)); +#endif +} +#endif \ No newline at end of file diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 64fff8950..93463b504 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -561,6 +561,7 @@ static char *discord_PickupReqMsg(char* msg) // Create the "embeds" array json_t *embeds = json_array(); json_object_set_new(root, "embeds", embeds); + json_object_set_new(root, "avatar_url", json_string("https://raw.githubusercontent.com/darkshade9/aq2world/master/docs/assets/img/common/aq2world_discord_circle.png")); // Create the embed object json_t *embed = json_object(); From 8eea98d7d1bcf9ed7870d637dc6447651b007bed Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:20:03 -0400 Subject: [PATCH 818/974] Removed windows.h from botlib --- src/action/botlib/botlib.h | 2 +- src/action/botlib/botlib_win.c | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index 426752719..c1f1a39f5 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -525,7 +525,7 @@ int BOTLIB_GetEquipment(edict_t* self); //rekkie -- collecting weapons, items, ammo -- e // =========================================================================== -// botlib_utils.c +// botlib_utils.c and botlib_win.c // =========================================================================== void seed_random_number_generator(void); void BOTLIB_SKILL_Init(edict_t* bot); diff --git a/src/action/botlib/botlib_win.c b/src/action/botlib/botlib_win.c index 5fafb3751..22677bd69 100644 --- a/src/action/botlib/botlib_win.c +++ b/src/action/botlib/botlib_win.c @@ -1,15 +1,8 @@ #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN -#include +#include void seed_random_number_generator(void) { -#if _MSC_VER >= 1920 && !__INTEL_COMPILER - LARGE_INTEGER li; - QueryPerformanceCounter(&li); - srand((unsigned int)(li.QuadPart)); -#else - // Fallback for other Windows compilers srand((unsigned int)time(NULL)); -#endif } #endif \ No newline at end of file From 5fd91761ed43d4b29001433a061bfad65d8566d9 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:21:41 -0400 Subject: [PATCH 819/974] Simpler method --- meson.build | 1 - src/action/botlib/botlib_utils.c | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 41b2b3c6d..69ce7e145 100644 --- a/meson.build +++ b/meson.build @@ -561,7 +561,6 @@ if get_option('aqtion-build') dll_link_args += '-lws2_32' dll_link_args += '-lcrypt32' dll_link_args += '-lwsock32' - botlib_src += 'src/action/botlib/botlib_win.c' endif endif diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index eb92b85ec..addeb1de8 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -4,18 +4,28 @@ #ifndef _WIN32 #include +#else +#define WIN32_LEAN_AND_MEAN +#include +#endif /* This file is for common utilities that are used by the botlib functions */ +#ifndef _WIN32 void seed_random_number_generator(void) { struct timeval tv; gettimeofday(&tv, NULL); srand(tv.tv_usec * tv.tv_sec); } +#else +void seed_random_number_generator(void) { + srand((unsigned int)time(NULL)); +} #endif + void BOTLIB_SKILL_Init(edict_t* bot) { // Initialize random skill levels From a068d17538ad6c6b28809b683973cf7f2f509ebe Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:27:57 -0400 Subject: [PATCH 820/974] Done with the windows nonsense, just renaming the vars --- src/action/botlib/botlib_utils.c | 16 ++++++++-------- src/action/g_cmds.c | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index addeb1de8..3a8408861 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -2,26 +2,26 @@ #include "../acesrc/acebot.h" #include "botlib.h" -#ifndef _WIN32 -#include -#else +#ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include +#else +#include #endif /* This file is for common utilities that are used by the botlib functions */ -#ifndef _WIN32 +#ifdef _WIN32 void seed_random_number_generator(void) { - struct timeval tv; - gettimeofday(&tv, NULL); - srand(tv.tv_usec * tv.tv_sec); + srand((unsigned int)time(NULL)); } #else void seed_random_number_generator(void) { - srand((unsigned int)time(NULL)); + struct timeval tv; + gettimeofday(&tv, NULL); + srand(tv.tv_usec * tv.tv_sec); } #endif diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index d4988bc89..3db9f58dc 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1185,8 +1185,8 @@ static void Cmd_Players_f (edict_t * ent) { int i; int count = 0; - char small[64]; - char large[1024]; + char _small[64]; + char _large[1024]; gclient_t *sortedClients[MAX_CLIENTS], *cl; if (!teamplay->value || !noscore->value) @@ -1195,28 +1195,28 @@ static void Cmd_Players_f (edict_t * ent) count = G_NotSortedClients( sortedClients ); // print information - large[0] = 0; + _large[0] = 0; for (i = 0; i < count; i++) { cl = sortedClients[i]; if (!teamplay->value || !noscore->value) - Q_snprintf (small, sizeof (small), "%3i %s\n", + Q_snprintf (_small, sizeof (_small), "%3i %s\n", cl->ps.stats[STAT_FRAGS], cl->pers.netname ); else - Q_snprintf (small, sizeof (small), "%s\n", + Q_snprintf (_small, sizeof (_small), "%s\n", cl->pers.netname); - if (strlen(small) + strlen(large) > sizeof (large) - 20) + if (strlen(_small) + strlen(_large) > sizeof (_large) - 20) { // can't print all of them in one packet - strcat (large, "...\n"); + strcat (_large, "...\n"); break; } - strcat (large, small); + strcat (_large, _small); } - gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); + gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", _large, count); } /* From fc80c2b2f9ab4ccd33dd6528f63aabd8e90655ce Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 17:50:33 -0400 Subject: [PATCH 821/974] Simplified allocating buffer room for request message --- src/action/tng_net.c | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 93463b504..7a7f871e8 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -329,37 +329,16 @@ char* TeamConstructPlayerList(int team) { static char* ConstructGameSettingsString(void) { // Calculate the required buffer size - int buffer_size = 0; - - if (matchmode->value){ - int buffer_size = snprintf(NULL, 0, "tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n", - tgren->string, - timelimit->string, - roundtimelimit->string, - use_xerp->string, - dmflags->string - ); - } else { - int buffer_size = snprintf(NULL, 0, "gamemode %s\ntgren %s\nfraglimit %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n", - gm->string, - tgren->string, - fraglimit->string, - timelimit->string, - roundtimelimit->string, - use_xerp->string, - dmflags->string - ); - } + int buffer_size = 256; // Allocate the buffer - char *result = (char *)malloc(buffer_size + 1); + char *result = (char *)malloc(buffer_size); if (!result) { return NULL; // Handle memory allocation failure } - // Construct the string if (matchmode->value) { - sprintf(result, "```tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", + snprintf(result, buffer_size, "```tgren %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", tgren->string, timelimit->string, roundtimelimit->string, @@ -367,7 +346,7 @@ static char* ConstructGameSettingsString(void) { dmflags->string ); } else { - sprintf(result, "```gamemode %s\ntgren %s\nfraglimit %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", + snprintf(result, buffer_size, "```gamemode %s\ntgren %s\nfraglimit %s\ntimelimit %s\nroundtimelimit %s\nuse_xerp %s\ndmflags %s\n```", gm->string, tgren->string, fraglimit->string, From 400cda05629d4f4b96979bbe3b47375459ed8fb4 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 30 Sep 2024 18:08:05 -0400 Subject: [PATCH 822/974] Simplified format for pickup requests --- src/action/tng_net.c | 68 +++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index 7a7f871e8..ba50d7815 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -600,45 +600,43 @@ static char *discord_PickupReqMsg(char* msg) json_object_set_new(embed, "fields", fields); // Create field objects and add them to the "fields" array - if (teamplay->value) { - for (int team = TEAM1; team <= teamCount; team++) { - char *team_name = TeamName(team); - char field_content[64]; - snprintf(field_content, sizeof(field_content), "%s", team_name); - char *team_players = TeamConstructPlayerList(team); - - // Add triple backticks for Discord formatting - char discord_formatted_players[1280]; - snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); - - - json_t *field = json_object(); - json_object_set_new(field, "name", json_string(field_content)); - json_object_set_new(field, "value", json_string(discord_formatted_players)); - json_object_set_new(field, "inline", json_true()); - json_array_append_new(fields, field); - - free(team_players); // Free the allocated memory for team_players - } - } else { - char *team_name = "Players"; - char field_content[64]; - snprintf(field_content, sizeof(field_content), "%s", team_name); - char *team_players = TeamConstructPlayerList(0); + // if (teamplay->value) { + // for (int team = TEAM1; team <= teamCount; team++) { + // char *team_name = TeamName(team); + // char field_content[64]; + // snprintf(field_content, sizeof(field_content), "%s", team_name); + // char *team_players = TeamConstructPlayerList(team); + + // // Add triple backticks for Discord formatting + // char discord_formatted_players[1280]; + // snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); + + // json_t *field = json_object(); + // json_object_set_new(field, "name", json_string(field_content)); + // json_object_set_new(field, "value", json_string(discord_formatted_players)); + // json_object_set_new(field, "inline", json_true()); + // json_array_append_new(fields, field); + + // free(team_players); // Free the allocated memory for team_players + // } + // } else { + char *team_name = "Players"; + char field_content[64]; + snprintf(field_content, sizeof(field_content), "%s", team_name); + char *team_players = TeamConstructPlayerList(0); - // Add triple backticks for Discord formatting - char discord_formatted_players[1280]; - snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); + // Add triple backticks for Discord formatting + char discord_formatted_players[1280]; + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); - json_t *field = json_object(); - json_object_set_new(field, "name", json_string(field_content)); - json_object_set_new(field, "value", json_string(discord_formatted_players)); - json_object_set_new(field, "inline", json_true()); - json_array_append_new(fields, field); + json_t *field = json_object(); + json_object_set_new(field, "name", json_string(field_content)); + json_object_set_new(field, "value", json_string(discord_formatted_players)); + json_object_set_new(field, "inline", json_true()); + json_array_append_new(fields, field); - free(team_players); // Free the allocated memory for team_players - } + free(team_players); // Free the allocated memory for team_players // Game Settings json_t *gamesettings = json_object(); From 1a4ae732e348e426c22f101bbef535f8347918db Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 1 Oct 2024 21:22:33 +0300 Subject: [PATCH 823/974] Add COM_ParseToken(). Allow parsing into user supplied buffer. Also add line number counter. --- inc/shared/shared.h | 4 ++++ src/client/ascii.c | 4 ++-- src/client/main.c | 2 +- src/client/screen.c | 12 +++++------ src/shared/shared.c | 51 +++++++++++++++++++++++++++++++-------------- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index c77e00e7d..abf69c6d7 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -594,6 +594,10 @@ bool COM_IsUint(const char *s); bool COM_IsPath(const char *s); bool COM_IsWhite(const char *s); +extern unsigned com_linenum; + +#define COM_SkipToken(data_p) COM_ParseToken(data_p, NULL, 0) +size_t COM_ParseToken(const char **data_p, char *buffer, size_t size); char *COM_Parse(const char **data_p); // data is an in/out parm, returns a parsed out token size_t COM_Compress(char *data); diff --git a/src/client/ascii.c b/src/client/ascii.c index dce044930..8070ac700 100644 --- a/src/client/ascii.c +++ b/src/client/ascii.c @@ -151,7 +151,7 @@ static void TH_DrawLayoutString(char *dst, const char *s) if (!strcmp(token, "pic")) { // draw a pic from a stat number - COM_Parse(&s); + COM_SkipToken(&s); continue; } @@ -223,7 +223,7 @@ static void TH_DrawLayoutString(char *dst, const char *s) if (!strcmp(token, "picn")) { // draw a pic from a name - COM_Parse(&s); + COM_SkipToken(&s); continue; } diff --git a/src/client/main.c b/src/client/main.c index cf1946658..5f4b6e88a 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -858,7 +858,7 @@ static void CL_ParseStatusResponse(serverStatus_t *status, const char *string) player = &status->players[status->numPlayers]; player->score = Q_atoi(COM_Parse(&s)); player->ping = Q_atoi(COM_Parse(&s)); - Q_strlcpy(player->name, COM_Parse(&s), sizeof(player->name)); + COM_ParseToken(&s, player->name, sizeof(player->name)); if (!s) break; status->numPlayers++; diff --git a/src/client/screen.c b/src/client/screen.c index 06ebd08e1..1a5a895b3 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1605,25 +1605,25 @@ static void SCR_SkipToEndif(const char **s) !strcmp(token, "yt") || !strcmp(token, "yb") || !strcmp(token, "yv") || !strcmp(token, "pic") || !strcmp(token, "picn") || !strcmp(token, "color") || strstr(token, "string")) { - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "client")) { for (i = 0; i < 6; i++) - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "ctf")) { for (i = 0; i < 5; i++) - COM_Parse(s); + COM_SkipToken(s); continue; } if (!strcmp(token, "num") || !strcmp(token, "health_bars")) { - COM_Parse(s); - COM_Parse(s); + COM_SkipToken(s); + COM_SkipToken(s); continue; } @@ -1632,7 +1632,7 @@ static void SCR_SkipToEndif(const char **s) if (!strcmp(token, "rnum")) continue; if (!strcmp(token, "if")) { - COM_Parse(s); + COM_SkipToken(s); skip++; continue; } diff --git a/src/shared/shared.c b/src/shared/shared.c index cb9f8b6a7..cf6423340 100644 --- a/src/shared/shared.c +++ b/src/shared/shared.c @@ -413,8 +413,7 @@ char *vtos(const vec3_t v) return str[index]; } -static char com_token[4][MAX_TOKEN_CHARS]; -static int com_tokidx; +unsigned com_linenum; /* ============== @@ -424,22 +423,20 @@ Parse a token out of a string. Handles C and C++ comments. ============== */ -char *COM_Parse(const char **data_p) +size_t COM_ParseToken(const char **data_p, char *buffer, size_t size) { int c; - int len; + size_t len; const char *data; - char *s = com_token[com_tokidx]; - - com_tokidx = (com_tokidx + 1) & 3; data = *data_p; len = 0; - s[0] = 0; + if (size) + *buffer = 0; if (!data) { *data_p = NULL; - return s; + return len; } // skip whitespace @@ -447,7 +444,10 @@ char *COM_Parse(const char **data_p) while ((c = *data) <= ' ') { if (c == 0) { *data_p = NULL; - return s; + return len; + } + if (c == '\n') { + com_linenum++; } data++; } @@ -468,6 +468,9 @@ char *COM_Parse(const char **data_p) data += 2; break; } + if (data[0] == '\n') { + com_linenum++; + } data++; } goto skipwhite; @@ -481,26 +484,42 @@ char *COM_Parse(const char **data_p) if (c == '\"' || !c) { goto finish; } - - if (len < MAX_TOKEN_CHARS - 1) { - s[len++] = c; + if (c == '\n') { + com_linenum++; } + if (len + 1 < size) { + *buffer++ = c; + } + len++; } } // parse a regular word do { - if (len < MAX_TOKEN_CHARS - 1) { - s[len++] = c; + if (len + 1 < size) { + *buffer++ = c; } + len++; data++; c = *data; } while (c > 32); finish: - s[len] = 0; + if (size) + *buffer = 0; *data_p = data; + return len; +} + +char *COM_Parse(const char **data_p) +{ + static char com_token[4][MAX_TOKEN_CHARS]; + static int com_tokidx; + char *s = com_token[com_tokidx]; + + COM_ParseToken(data_p, s, sizeof(com_token[0])); + com_tokidx = (com_tokidx + 1) & 3; return s; } From eb74eece369afaa23ab2012a2b59c5ea42f86457 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 10:39:36 -0400 Subject: [PATCH 824/974] Set bot_enable checks in various places --- src/action/acesrc/acebot.h | 5 +++-- src/action/botlib/botlib_cmd.c | 5 +++++ src/action/botlib/botlib_spawn.c | 4 ++++ src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_spawn.c | 15 +++++++++------ 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 4ddab4a65..a6396f4c7 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -118,10 +118,11 @@ typedef enum //rekkie -- BSP -- e #define MAX_BOTSKILL 10 +extern cvar_t *bot_enable; // Enable/disable bots toggle extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! -extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +extern cvar_t *bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost -extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +extern cvar_t *bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight extern cvar_t *bot_showpath; // Show bot paths //AQ2 ADD diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 3aac838d5..ab3f5967a 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -12,6 +12,11 @@ qboolean BOTLIB_SV_Cmds(void) { int cc = gi.argc(); + if (!bot_enable->value) { + gi.dprintf("bot_enable is 0; Bots are disabled\n"); + return true; + } + gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added // sv bots diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index c649b68e8..8b763bd9b 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2148,6 +2148,10 @@ void BOTLIB_DMBotCountManager(void) void BOTLIB_CheckBotRules(void) { + // Disable bot logic entirely + if (!bot_enable->value) + return; + if (matchmode->value) // Bots never allowed in matchmode return; diff --git a/src/action/g_main.c b/src/action/g_main.c index 44237b5c6..f0364db17 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -484,6 +484,7 @@ cvar_t *ltk_routing; cvar_t *ltk_botfile; cvar_t *ltk_loadbots; //rekkie -- DEV_1 -- s +cvar_t* bot_enable; // Enable/Disable bots cvar_t* bot_showpath; cvar_t* bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit diff --git a/src/action/g_save.c b/src/action/g_save.c index 57174eaf5..7add29a28 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -681,6 +681,7 @@ void InitGame( void ) ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); //rekkie -- DEV_1 -- s + bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH); bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 1c44f2c46..22affd39f 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1576,12 +1576,14 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- s #ifndef NO_BOTS - BOTLIB_InitNavigation(NULL); -#ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); -#else - BOTLIB_LoadNav(); -#endif + if (bot_enable->value) { + BOTLIB_InitNavigation(NULL); + #ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); + #else + BOTLIB_LoadNav(); + + #endif //ACEND_LoadAAS(false); // This will also generate AAS if it doesn't exist //ACEND_BSP(NULL); @@ -1599,6 +1601,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if(bot_personality->value) BOTLIB_PersonalityFile(); + } #endif //rekkie -- e } From 07585fb5ab13bde1e8853cccfc73b344a11bf1f9 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 10:53:05 -0400 Subject: [PATCH 825/974] Some documentation additions --- doc/action.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/action.md b/doc/action.md index 70ec6b395..d770803d3 100644 --- a/doc/action.md +++ b/doc/action.md @@ -597,6 +597,7 @@ Now you can fill out your teams with bots, or create an entire team of bots to f Taking aspects of the existing bots and greatly enhancing their navigation and behavioral capabilities, the new botlib bots have superceded the legacy LTK bots. This is a very new system and as such will be in a regular flux of change, enhancements and adjustments. **Server Commands:** +- `bot_enable [0/1]` - Server cvar, if enabled, will load all necessary components to enable bots. Default is disabled [0] - `sv bots <#> [#]` - Server command, if command is issued without arguments, it will print out the existing bot counts, bots on teams and other information. If provided with a single value (`sv bots 3`) this will add 3 bots to the game. If this is a team game, it should auto balance the bots across teams. If provided with two values (`sv bots 3 1`) this will assign 3 bots to team 1. Please be aware of your maxclients limits as the server console will alert you if you have added too many bots. - `bot_remember <#>` - Server cvar, how long (in seconds) the bot remembers an enemy after visibility has been lost. This is experimental and being reevaluated for usability, and may be replaced in the future - `bot_reaction <#>` - Server cvar, how long (in seconds) until the bot reacts to an enemy in sight. Lower values mean a faster reaction. This is experimental and being reevaluated for usability, and may be replaced in the future @@ -606,6 +607,7 @@ Taking aspects of the existing bots and greatly enhancing their navigation and b - `bot_countashuman [0/1]` - Server cvar, enabling this will allow teamplay-based games to progress without humans being in the server. Set to 0 to force games to not count bots as clients in terms of teamplay - `bot_navautogen [0/1]` - Server cvar, enabling this will auto generate a navmesh for any maps that do not already have one, on map load time. This automatic navmesh is far from perfect, but it does allow bots to traverse maps rather than stand still. A far superior option is to have a handcrafted navmesh for the map. - `bot_debug [0/1]` - Server cvar, will enable debug messaging for BOTLIB functionality where enabled +- `bot_reportasclient [0/1]` - Server cvar, if enabled, will report bots as real clients to server masters. Default is disabled [0] **Client Commands:** These commands only work on local map loads, not connections to dedicated servers. They are meant to be used to create navmeshes. From 5e64dac13653f240e0bc77534b7c89be4295ad9d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 11:55:05 -0400 Subject: [PATCH 826/974] Retrying reverting small and large --- extern/discord/c/discord_game_sdk.h | 1 + src/action/g_cmds.c | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/extern/discord/c/discord_game_sdk.h b/extern/discord/c/discord_game_sdk.h index f4c209acd..d8216f9dc 100644 --- a/extern/discord/c/discord_game_sdk.h +++ b/extern/discord/c/discord_game_sdk.h @@ -2,6 +2,7 @@ #define _DISCORD_GAME_SDK_H_ #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN #include #include #endif diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 3db9f58dc..d4988bc89 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1185,8 +1185,8 @@ static void Cmd_Players_f (edict_t * ent) { int i; int count = 0; - char _small[64]; - char _large[1024]; + char small[64]; + char large[1024]; gclient_t *sortedClients[MAX_CLIENTS], *cl; if (!teamplay->value || !noscore->value) @@ -1195,28 +1195,28 @@ static void Cmd_Players_f (edict_t * ent) count = G_NotSortedClients( sortedClients ); // print information - _large[0] = 0; + large[0] = 0; for (i = 0; i < count; i++) { cl = sortedClients[i]; if (!teamplay->value || !noscore->value) - Q_snprintf (_small, sizeof (_small), "%3i %s\n", + Q_snprintf (small, sizeof (small), "%3i %s\n", cl->ps.stats[STAT_FRAGS], cl->pers.netname ); else - Q_snprintf (_small, sizeof (_small), "%s\n", + Q_snprintf (small, sizeof (small), "%s\n", cl->pers.netname); - if (strlen(_small) + strlen(_large) > sizeof (_large) - 20) + if (strlen(small) + strlen(large) > sizeof (large) - 20) { // can't print all of them in one packet - strcat (_large, "...\n"); + strcat (large, "...\n"); break; } - strcat (_large, _small); + strcat (large, small); } - gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", _large, count); + gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); } /* From dd574c0970869de4a59cc21e4839cba84a8506e9 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 12:00:35 -0400 Subject: [PATCH 827/974] Trying adding LEAN_AND_MEAN to rpc.h --- src/action/a_cmds.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 59cfcd1bb..c884c59f4 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -135,6 +135,7 @@ #include #if USE_AQTION #ifdef _WIN32 +#include WIN32_LEAN_AND_MEAN #if _MSC_VER && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include From 2a5762580c523c502a33b5ce646b99aca33f7e05 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 12:34:45 -0400 Subject: [PATCH 828/974] Define, not include, dummy --- src/action/a_cmds.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index c884c59f4..5f6cdfb1f 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -135,7 +135,7 @@ #include #if USE_AQTION #ifdef _WIN32 -#include WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN #if _MSC_VER && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include From 102e219d19eee99372707d80d0c28af67fc9dd7a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 12:40:27 -0400 Subject: [PATCH 829/974] Replaced small/large with playerInfo/playerList in g_cmds.c --- src/action/a_cmds.c | 1 - src/action/g_cmds.c | 66 ++++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 5f6cdfb1f..59cfcd1bb 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -135,7 +135,6 @@ #include #if USE_AQTION #ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN #if _MSC_VER && !__INTEL_COMPILER #pragma comment(lib, "rpcrt4.lib") #include diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index d4988bc89..29b836aea 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1183,40 +1183,40 @@ Cmd_Players_f */ static void Cmd_Players_f (edict_t * ent) { - int i; - int count = 0; - char small[64]; - char large[1024]; - gclient_t *sortedClients[MAX_CLIENTS], *cl; - - if (!teamplay->value || !noscore->value) - count = G_SortedClients( sortedClients ); - else - count = G_NotSortedClients( sortedClients ); - - // print information - large[0] = 0; - - for (i = 0; i < count; i++) - { - cl = sortedClients[i]; - if (!teamplay->value || !noscore->value) - Q_snprintf (small, sizeof (small), "%3i %s\n", - cl->ps.stats[STAT_FRAGS], - cl->pers.netname ); - else - Q_snprintf (small, sizeof (small), "%s\n", - cl->pers.netname); - - if (strlen(small) + strlen(large) > sizeof (large) - 20) - { // can't print all of them in one packet - strcat (large, "...\n"); - break; - } - strcat (large, small); - } + int i; + int count = 0; + char playerInfo[64]; + char playerList[1024]; + gclient_t *sortedClients[MAX_CLIENTS], *cl; + + if (!teamplay->value || !noscore->value) + count = G_SortedClients( sortedClients ); + else + count = G_NotSortedClients( sortedClients ); + + // print information + playerList[0] = 0; + + for (i = 0; i < count; i++) + { + cl = sortedClients[i]; + if (!teamplay->value || !noscore->value) + Q_snprintf(playerInfo, sizeof(playerInfo), "%3i %s\n", + cl->ps.stats[STAT_FRAGS], + cl->pers.netname); + else + Q_snprintf(playerInfo, sizeof(playerInfo), "%s\n", + cl->pers.netname); + + if (strlen(playerInfo) + strlen(playerList) > sizeof(playerList) - 20) + { // can't print all of them in one packet + strcat(playerList, "...\n"); + break; + } + strcat(playerList, playerInfo); + } - gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", large, count); + gi.cprintf(ent, PRINT_HIGH, "%s\n%i players\n", playerList, count); } /* From 133b182eda516e4825b3d4b95a418bf9381abdba Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 2 Oct 2024 15:55:39 -0400 Subject: [PATCH 830/974] Some documentation editing --- doc/action.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/action.md b/doc/action.md index 5fa29d2c6..9ef61216e 100644 --- a/doc/action.md +++ b/doc/action.md @@ -97,12 +97,12 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Q2pro MVD server demo support](#q2pro-mvd-server-demo-support) - [Latency Compensation](#latency-compensation) - [General quality of life improvements](#general-quality-of-life-improvements) - - [Client Prediction](#client-prediction) - - [Force Spawn items](#force-spawn-items) - - [Zoom Compensation](#zoom-compensation) - - [Warmup](#warmup) - - [Item Kit Mode](#item-kit-mode) - - [Print rules](#print-rules) + - [Client Prediction](#client-prediction) + - [Force Spawn items](#force-spawn-items) + - [Zoom Compensation](#zoom-compensation) + - [Warmup](#warmup) + - [Item Kit Mode](#item-kit-mode) + - [Print rules](#print-rules) - [Espionage](#espionage) - [Gun mechanics/enhancements](#gun-mechanicsenhancements) - [Outbound messaging](#outbound-messaging) @@ -574,7 +574,7 @@ TNG updates the way the bandolier behaves when dropping it. It will prevent peop ### Bots #### Legacy LTK Bots -Now you can fill out your teams with bots, or create an entire team of bots to fight. You can define persistent bots in bots/botdata.cfg, or create and remove them on demand. Bots wander pretty stupidly unless you create a linked network of nodes for them to follow. This documentation exists for reference only, LTK bots have been replaced by the BOTLIB Bots as described below. +Now you can fill out your teams with bots, or create an entire team of bots to fight. You can define persistent bots in bots/botdata.cfg, or create and remove them on demand. Bots wander pretty stupidly unless you create a linked network of nodes for them to follow. This documentation exists for reference only, LTK bots have been replaced by the BOTLIB Bots as described below, use at your own risk. **Commands:** - `sv addbot [team] [name]` - add a bot for the duration of the current map @@ -601,7 +601,7 @@ Taking aspects of the existing bots and greatly enhancing their navigation and b - `sv bots <#> [#]` - Server command, if command is issued without arguments, it will print out the existing bot counts, bots on teams and other information. If provided with a single value (`sv bots 3`) this will add 3 bots to the game. If this is a team game, it should auto balance the bots across teams. If provided with two values (`sv bots 3 1`) this will assign 3 bots to team 1. Please be aware of your maxclients limits as the server console will alert you if you have added too many bots. - `bot_remember <#>` - Server cvar, how long (in seconds) the bot remembers an enemy after visibility has been lost. This is experimental and being reevaluated for usability, and may be replaced in the future - `bot_reaction <#>` - Server cvar, how long (in seconds) until the bot reacts to an enemy in sight. Lower values mean a faster reaction. This is experimental and being reevaluated for usability, and may be replaced in the future -- `bot_randvoice <#>` - Server cvar, percentage chance that bots use random user voice wavs [min: 0 max: 100]. Suggest disabling or setting to a very low value because user voice wavs take up limited sound slots on non-extended protocol servers (256 max sounds versus 2048) -- if a server runs out of sound slots and tries to cache another sound, it will crash. Looking to re-evaluate how to handle this on non-extended protocol servers. +- `bot_randvoice <#>` - Server cvar, percentage chance that bots use random user voice wavs [min: 0 max: 100]. Suggest disabling or setting to a very low value because user voice wavs take up limited sound slots on non-extended protocol servers (256 max sounds versus 2048) -- if a server runs out of sound slots and tries to cache another sound, it will crash. There is a need to re-evaluate how to handle this on non-extended protocol servers. - `bot_randname [0/1]` - Server cvar, allow bots to pick a random name. Suggest keeping enabled - `bot_chat [0/1]` - Server cvar, enables bot chat. - `bot_countashuman [0/1]` - Server cvar, enabling this will allow teamplay-based games to progress without humans being in the server. Set to 0 to force games to not count bots as clients in terms of teamplay @@ -628,7 +628,7 @@ Server admins can slap players into the air, and optionally deal damage. Although Quake 2 typically runs the server at a fixed 10 frames per second, some advanced Quake 2 servers can be built with variable framerate support to allow faster updating. AQ2-TNG now supports this. At higher fps, lag is reduced: the server responds more quickly to shooting and other client commands. **Commands:** -- `sv_fps [10/20/30/40/50/60]` - set server framerate (default is 10) +- `sv_fps [10/20/30/40/50/60]` - set server framerate (default is 10) -- suggested values are [10/20/30] - `sync_guns [0/1/2]` - 0 plays gun sounds on any frame, 1 syncs with recent shots, 2 delays all to 10fps (default 1) ### Q2pro MVD server demo support @@ -644,28 +644,28 @@ Antilag allows server operator to enable lag-compensation for aiming with hitsca ### General quality of life improvements `sv_limp_highping [#]` - server cvar, players above this ping threshold will have movement prediction disabled with leg damage to make things less jittery. Value is set in ping ms, players with a ping value equal or higher to this value will have less jittery movement. Default value is '70' -### Client Prediction +#### Client Prediction `limp_nopred [0/1]` - client cvar, clients can set this to force on or off movement prediction when taking leg damage. Set to "0" for classic behavior, set to "1" to fix the jitter -### Force Spawn items +#### Force Spawn items `g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for classic play. -### Zoom Compensation +#### Zoom Compensation `zoom_comp [0/1]` - server cvar, set to "1" to compensate zoom delay based on ping. Every 80ms ping reduces 1 frame, minimum of 1 frame. Default is "0", do not compensate -### Warmup -- `warmup [#]` - server cvar, set in seconds, minimum is 15, 20 is common. This is the amount of time in seconds after both captains ready up that warmups will continue until the first round begins. Set to 0 to disable warmup entirely. -- `warmup_bots [#]` - server cvar, set in number of bots to add. This adds this many bots to the warmup phase of matchmode. Set to '0' to disable warmup_bots, suggested value for usage is '4' to '6'. +#### Warmup +`warmup [#]` - server cvar, set in seconds, minimum is 15, 20 is common. This is the amount of time in seconds after both captains ready up that warmups will continue until the first round begins. Set to 0 to disable warmup entirely. +`warmup_bots [#]` - server cvar, set in number of bots to add. This adds this many bots to the warmup phase of matchmode. Set to '0' to disable warmup_bots, suggested value for usage is '4' to '6'. -### Item Kit Mode +#### Item Kit Mode `item_kit_mode [0/1]` - server cvar, default 0. Works in any mode where you choose a weapon (GS_WEAPONCHOOSE), it combines items into kits: - Commando Kit: `Bandolier + Kevlar Helm` - Stealth Kit: `Slippers + Silencer` - Assassin Kit: `Laser Sight + Silencer` Players may drop the items anytime (using `drop item`) but you can only pick one up at a time, assuming the server doesn't allow for more. On player entity death, both items will drop. LTK bots do not use kits, they will retain their normal behavior. esp_enhancedslippers settings are honored, boosting the effectiveness of the Stealth Kit even more. -### Print rules -- `printrules [0/1]` - server cvar, default 0. If enabled, a printout of the rules will display once the countdown begins in Teamplay modes. +#### Print rules +`printrules [0/1]` - server cvar, default 0. If enabled, a printout of the rules will display once the countdown begins in Teamplay modes. ### Espionage Inspired by AQ2:ETE, these additions are optional server vars to create a different variety of gameplay. Review the document called Espionage.md to understand how to run a server and how to create and edit scene files. From cd30ec4bc8c6ddb68246395e630935b9d4dd97a4 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 831/974] Use real JSON parser to parse MD5 scales. --- src/refresh/jsmn.h | 470 +++++++++++++++++++++++++++++++++++++++++++ src/refresh/models.c | 98 +++++---- 2 files changed, 530 insertions(+), 38 deletions(-) create mode 100644 src/refresh/jsmn.h diff --git a/src/refresh/jsmn.h b/src/refresh/jsmn.h new file mode 100644 index 000000000..88570f102 --- /dev/null +++ b/src/refresh/jsmn.h @@ -0,0 +1,470 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct jsmntok { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if (token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */ diff --git a/src/refresh/models.c b/src/refresh/models.c index 8976fdd34..4841f1c23 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -703,6 +703,10 @@ static void MOD_PrintError(const char *path, int err) #if USE_MD5 +#define JSMN_STATIC +#define JSMN_PARENT_LINKS +#include "jsmn.h" + #define MD5_Malloc(size) Hunk_TryAlloc(&model->skeleton_hunk, size) static bool MD5_ParseExpect(const char **buffer, const char *expect) @@ -1085,69 +1089,87 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, */ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t *joint_infos) { - void *buffer; - const char *s; - int ret; + const jsmntok_t *tok, *end; + jsmn_parser parser; + jsmntok_t tokens[4096]; + char *data; + int len, ret; + + len = FS_LoadFile(path, (void **)&data); + if (!data) { + if (len != Q_ERR(ENOENT)) + MOD_PrintError(path, len); + return; + } - ret = FS_LoadFile(path, &buffer); - if (!buffer) + jsmn_init(&parser); + ret = jsmn_parse(&parser, data, len, tokens, q_countof(tokens)); + if (ret < 0) goto fail; - s = buffer; + if (ret == 0) + goto skip; - MD5_EXPECT("{"); - while (s) { - int joint_id = -1; - char *tok, *tok2; + tok = &tokens[0]; + if (tok->type != JSMN_OBJECT) + goto fail; - tok = COM_Parse(&s); - if (!strcmp(tok, "}")) - break; + end = tokens + ret; + tok++; + + while (tok < end) { + if (tok->type != JSMN_STRING) + goto fail; + int joint_id = -1; + const char *joint_name = data + tok->start; + + data[tok->end] = 0; for (int i = 0; i < model->num_joints; i++) { - if (!strcmp(tok, joint_infos[i].name)) { + if (!strcmp(joint_name, joint_infos[i].name)) { joint_id = i; break; } } if (joint_id == -1) - Com_WPrintf("No such joint %s in %s\n", Com_MakePrintable(tok), path); + Com_WPrintf("No such joint \"%s\" in %s\n", Com_MakePrintable(joint_name), path); - MD5_EXPECT(":"); - MD5_EXPECT("{"); + if (++tok == end || tok->type != JSMN_OBJECT) + goto fail; - while (s) { - tok = COM_Parse(&s); - if (!strcmp(tok, "}") || !strcmp(tok, "},")) - break; - MD5_EXPECT(":"); + int num_keys = tok->size; + if (end - ++tok < num_keys * 2) + goto fail; - tok2 = COM_Parse(&s); - if (joint_id == -1) - continue; + for (int i = 0; i < num_keys; i++) { + const jsmntok_t *key = tok++; + const jsmntok_t *val = tok++; + if (key->type != JSMN_STRING || val->type != JSMN_PRIMITIVE) + goto fail; - if (!strcmp(tok, "scale_positions")) { - joint_infos[joint_id].scale_pos = !strncmp(tok2, "true", 4); + if (joint_id == -1) continue; - } - unsigned frame_id = Q_atoi(tok); - if (frame_id >= model->num_frames) { - Com_WPrintf("No such frame %d in %s\n", frame_id, path); - continue; + data[key->end] = 0; + if (!strcmp(data + key->start, "scale_positions")) { + joint_infos[joint_id].scale_pos = data[val->start] == 't'; + } else { + unsigned frame_id = Q_atoi(data + key->start); + if (frame_id < model->num_frames) + model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = Q_atof(data + val->start); + else + Com_WPrintf("No such frame %d in %s\n", frame_id, path); } - - model->skeleton_frames[frame_id * model->num_joints + joint_id].scale = Q_atof(tok2); } } - FS_FreeFile(buffer); +skip: + FS_FreeFile(data); return; fail: - if (ret != Q_ERR(ENOENT)) - MOD_PrintError(path, ret); - FS_FreeFile(buffer); + Com_EPrintf("Couldn't load %s: Invalid JSON data\n", path); + FS_FreeFile(data); } /** From da32d03e1943f13e2ab442757798013d50f2f008 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 832/974] Factor MD5 skin loading into separate function. --- src/refresh/models.c | 53 +++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index 4841f1c23..9469dac6a 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1318,6 +1318,38 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) return false; } +static bool MD5_LoadSkins(model_t *model) +{ + md5_model_t *mdl = model->skeleton; + const maliasmesh_t *mesh = &model->meshes[0]; + + mdl->num_skins = mesh->numskins; + mdl->skins = Hunk_TryAlloc(&model->skeleton_hunk, sizeof(mdl->skins[0]) * mdl->num_skins); + if (!mdl->skins) { + Com_EPrintf("Out of memory for MD5 skins\n"); + return false; + } + + for (int i = 0; i < mesh->numskins; i++) { + // because skins are actually absolute and not always relative to the + // model being used, we have to stick to the same behavior. + char skin_name[MAX_QPATH], skin_path[MAX_QPATH]; + + COM_SplitPath(mesh->skinnames[i], skin_name, sizeof(skin_name), skin_path, sizeof(skin_path), false); + + // build MD5 path + if (Q_strlcat(skin_path, "md5/", sizeof(skin_path)) < sizeof(skin_path) && + Q_strlcat(skin_path, skin_name, sizeof(skin_path)) < sizeof(skin_path)) { + mdl->skins[i] = IMG_Find(skin_path, IT_SKIN, IF_NONE); + } else { + Com_WPrintf("MD5 skin path too long: %s\n", skin_path); + mdl->skins[i] = R_NOTEXTURE; + } + } + + return true; +} + static void MOD_LoadMD5(model_t *model) { char model_name[MAX_QPATH], base_path[MAX_QPATH]; @@ -1338,28 +1370,9 @@ static void MOD_LoadMD5(model_t *model) if (!MOD_LoadMD5Anim(model, anim_path)) goto fail; - - model->skeleton->num_skins = model->meshes[0].numskins; - if (!(model->skeleton->skins = MD5_Malloc(sizeof(model->skeleton->skins[0]) * model->meshes[0].numskins))) + if (!MD5_LoadSkins(model)) goto fail; - for (int i = 0; i < model->meshes[0].numskins; i++) { - // because skins are actually absolute and not always relative to the - // model being used, we have to stick to the same behavior. - char skin_name[MAX_QPATH], skin_path[MAX_QPATH]; - - COM_SplitPath(model->meshes[0].skinnames[i], skin_name, sizeof(skin_name), skin_path, sizeof(skin_path), false); - - // build md5 path - if (Q_strlcat(skin_path, "md5/", sizeof(skin_path)) < sizeof(skin_path) && - Q_strlcat(skin_path, skin_name, sizeof(skin_path)) < sizeof(skin_path)) { - model->skeleton->skins[i] = IMG_Find(skin_path, IT_SKIN, IF_NONE); - } else { - Com_WPrintf("MD5 skin path too long: %s\n", skin_path); - model->skeleton->skins[i] = R_NOTEXTURE; - } - } - Hunk_End(&model->skeleton_hunk); return; From b711e8f830d33b1767b05f82690b560423547253 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:00:54 +0300 Subject: [PATCH 833/974] Refactor MD5 code more. Use longjmp instead of macros for error handling. Get rid of md5_joint_t::parent. Get rid of md5_model_t::base_skeleton. Verify joint parent has been processed before use. Reduce size of md5_vertex_t and md5_weight_t to 16 bytes. Allow joint names up to 48 chars. --- src/refresh/gl.h | 16 +- src/refresh/mesh.c | 14 +- src/refresh/models.c | 438 +++++++++++++++++++++---------------------- 3 files changed, 221 insertions(+), 247 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index b0b4a8df3..920868ea1 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -320,34 +320,30 @@ typedef struct { // the total amount of joints the renderer will bother handling #define MD5_MAX_JOINTS 256 -#define MD5_MAX_JOINTNAME 32 +#define MD5_MAX_JOINTNAME 48 #define MD5_MAX_MESHES 32 #define MD5_MAX_WEIGHTS 8192 #define MD5_MAX_FRAMES 1024 /* Joint */ typedef struct { - int parent; - vec3_t pos; - quat_t orient; float scale; + quat_t orient; } md5_joint_t; /* Vertex */ typedef struct { vec3_t normal; - uint32_t start; /* start weight */ - uint32_t count; /* weight count */ + uint16_t start; /* start weight */ + uint16_t count; /* weight count */ } md5_vertex_t; /* Weight */ typedef struct { - int joint; - float bias; - vec3_t pos; + float bias; } md5_weight_t; /* Mesh */ @@ -360,6 +356,7 @@ typedef struct { maliastc_t *tcoords; glIndex_t *indices; md5_weight_t *weights; + uint8_t *jointnums; } md5_mesh_t; /* MD5 model + animation structure */ @@ -370,7 +367,6 @@ typedef struct { int num_skins; md5_mesh_t *meshes; - md5_joint_t *base_skeleton; md5_joint_t *skeleton_frames; // [num_joints][num_frames] image_t **skins; } md5_model_t; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 64dd23f62..487c6f7cd 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -671,7 +671,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, // for the given vertex, set of weights & skeleton, calculate // the output vertex (and optionally normal). static inline void calc_skel_vert(const md5_vertex_t *vert, - const md5_weight_t *weights, + const md5_mesh_t *mesh, const md5_joint_t *skeleton, float *restrict out_position, float *restrict out_normal) @@ -682,8 +682,8 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, VectorClear(out_normal); for (int i = 0; i < vert->count; i++) { - const md5_weight_t *weight = &weights[vert->start + i]; - const md5_joint_t *joint = &skeleton[weight->joint]; + const md5_weight_t *weight = &mesh->weights[vert->start + i]; + const md5_joint_t *joint = &skeleton[mesh->jointnums[vert->start + i]]; vec3_t wv; Quat_RotatePoint(joint->orient, weight->pos, wv); @@ -701,7 +701,7 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, static void tess_plain_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) { for (int i = 0; i < mesh->num_verts; i++) - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, &tess.vertices[i * 4], NULL); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, &tess.vertices[i * 4], NULL); } static void tess_shade_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) @@ -710,7 +710,7 @@ static void tess_shade_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) for (int i = 0; i < mesh->num_verts; i++) { vec3_t normal; - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, dst_vert, normal); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, dst_vert, normal); vec_t d = shadedot(normal); dst_vert[4] = color[0] * d; @@ -726,7 +726,7 @@ static void tess_shell_skel(const md5_mesh_t *mesh, const md5_joint_t *skeleton) { for (int i = 0; i < mesh->num_verts; i++) { vec3_t position, normal; - calc_skel_vert(&mesh->vertices[i], mesh->weights, skeleton, position, normal); + calc_skel_vert(&mesh->vertices[i], mesh, skeleton, position, normal); VectorMA(position, shellscale, normal, &tess.vertices[i * 4]); } @@ -740,9 +740,7 @@ static void lerp_alias_skeleton(const md5_model_t *model) const md5_joint_t *skel_b = &model->skeleton_frames[frame_b * model->num_joints]; for (int i = 0; i < model->num_joints; i++) { - temp_skeleton[i].parent = skel_a[i].parent; temp_skeleton[i].scale = skel_b[i].scale; - LerpVector2(skel_a[i].pos, skel_b[i].pos, backlerp, frontlerp, temp_skeleton[i].pos); Quat_SLerp(skel_a[i].orient, skel_b[i].orient, backlerp, frontlerp, temp_skeleton[i].orient); } diff --git a/src/refresh/models.c b/src/refresh/models.c index 9469dac6a..580d0dcf6 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -703,75 +703,94 @@ static void MOD_PrintError(const char *path, int err) #if USE_MD5 +#include + #define JSMN_STATIC #define JSMN_PARENT_LINKS #include "jsmn.h" -#define MD5_Malloc(size) Hunk_TryAlloc(&model->skeleton_hunk, size) +static jmp_buf md5_jmpbuf; -static bool MD5_ParseExpect(const char **buffer, const char *expect) +q_noreturn +static void MD5_ParseError(const char *text) { - char *token = COM_Parse(buffer); + Com_SetLastError(va("Line %u: %s", com_linenum, text)); + longjmp(md5_jmpbuf, -1); +} - if (strcmp(token, expect)) { - Com_SetLastError(va("Expected %s, got %s", expect, Com_MakePrintable(token))); - return false; +static void *MD5_Malloc(model_t *model, size_t size) +{ + void *ptr = Hunk_TryAlloc(&model->skeleton_hunk, size); + if (!ptr) { + Com_SetLastError("Out of memory"); + longjmp(md5_jmpbuf, -1); } + return ptr; +} - return true; +static void MD5_ParseExpect(const char **buffer, const char *expect) +{ + char *token = COM_Parse(buffer); + + if (strcmp(token, expect)) + MD5_ParseError(va("Expected \"%s\", got \"%s\"", expect, Com_MakePrintable(token))); } -static bool MD5_ParseFloat(const char **buffer, float *output) +static float MD5_ParseFloat(const char **buffer) { char *token = COM_Parse(buffer); char *endptr; - *output = strtof(token, &endptr); - if (endptr == token || *endptr) { - Com_SetLastError(va("Expected float, got %s", Com_MakePrintable(token))); - return false; - } + float v = strtof(token, &endptr); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected float, got \"%s\"", Com_MakePrintable(token))); - return true; + return v; } -static bool MD5_ParseUint(const char **buffer, uint32_t *output) +static uint32_t MD5_ParseUint(const char **buffer, uint32_t min_v, uint32_t max_v) { char *token = COM_Parse(buffer); char *endptr; - *output = strtoul(token, &endptr, 10); - if (endptr == token || *endptr) { - Com_SetLastError(va("Expected int, got %s", Com_MakePrintable(token))); - return false; - } + unsigned long v = strtoul(token, &endptr, 10); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected uint, got \"%s\"", Com_MakePrintable(token))); + if (v < min_v || v > max_v) + MD5_ParseError(va("Value out of range: %lu", v)); - return true; + return v; } -static bool MD5_ParseVector(const char **buffer, vec3_t output) +static int32_t MD5_ParseInt(const char **buffer, int32_t min_v, int32_t max_v) { - return - MD5_ParseExpect(buffer, "(") && - MD5_ParseFloat(buffer, &output[0]) && - MD5_ParseFloat(buffer, &output[1]) && - MD5_ParseFloat(buffer, &output[2]) && - MD5_ParseExpect(buffer, ")"); -} + char *token = COM_Parse(buffer); + char *endptr; + + long v = strtol(token, &endptr, 10); + if (endptr == token || *endptr) + MD5_ParseError(va("Expected int, got \"%s\"", Com_MakePrintable(token))); + if (v < min_v || v > max_v) + MD5_ParseError(va("Value out of range: %ld", v)); -#define MD5_CHECK(x) \ - do { if (!(x)) { ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) + return v; +} -#define MD5_ENSURE(x, e) \ - do { if (!(x)) { Com_SetLastError(e); ret = Q_ERR_INVALID_FORMAT; goto fail; } } while (0) +static void MD5_ParseVector(const char **buffer, vec3_t output) +{ + MD5_ParseExpect(buffer, "("); + output[0] = MD5_ParseFloat(buffer); + output[1] = MD5_ParseFloat(buffer); + output[2] = MD5_ParseFloat(buffer); + MD5_ParseExpect(buffer, ")"); +} -#define MD5_EXPECT(x) MD5_CHECK(MD5_ParseExpect(&s, x)) -#define MD5_UINT(x) MD5_CHECK(MD5_ParseUint(&s, &x)) -#define MD5_FLOAT(x) MD5_CHECK(MD5_ParseFloat(&s, &x)) -#define MD5_VECTOR(x) MD5_CHECK(MD5_ParseVector(&s, x)) -#define MD5_SKIP() COM_Parse(&s) +typedef struct { + vec3_t pos; + quat_t orient; +} baseframe_joint_t; -static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleton) +static void MD5_ComputeNormals(md5_mesh_t *mesh, const baseframe_joint_t *base_skeleton) { vec3_t finalVerts[TESS_MAX_VERTICES]; md5_vertex_t *vert; @@ -786,7 +805,7 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto for (j = 0; j < vert->count; j++) { const md5_weight_t *weight = &mesh->weights[vert->start + j]; - const md5_joint_t *joint = &base_skeleton[weight->joint]; + const baseframe_joint_t *joint = &base_skeleton[mesh->jointnums[vert->start + j]]; /* Calculate transformed vertex for this weight */ vec3_t wv; @@ -837,11 +856,12 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto vec3_t *norm = HashMap_Lookup(vec3_t, pos_to_normal_map, &finalVerts[i]); if (norm) { // Put the bind-pose normal into joint-local space - // so the animated normal can be computed faster later - // Done by transforming the vertex normal by the inverse joint's orientation quaternion of the weight + // so the animated normal can be computed faster later. + // Done by transforming the vertex normal by the inverse + // joint's orientation quaternion of the weight. for (j = 0; j < vert->count; j++) { const md5_weight_t *weight = &mesh->weights[vert->start + j]; - const md5_joint_t *joint = &base_skeleton[weight->joint]; + const baseframe_joint_t *joint = &base_skeleton[mesh->jointnums[vert->start + j]]; vec3_t wv; quat_t orient_inv; Quat_Conjugate(joint->orient, orient_inv); @@ -854,182 +874,136 @@ static void MD5_ComputeNormals(md5_mesh_t *mesh, const md5_joint_t *base_skeleto HashMap_Destroy(pos_to_normal_map); } -static bool MOD_LoadMD5Mesh(model_t *model, const char *path) +static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) { + baseframe_joint_t base_skeleton[MD5_MAX_JOINTS]; md5_model_t *mdl; - int i, j, k, ret; - uint32_t num_joints, num_meshes; - void *buffer; - const char *s; + int i, j, k; - ret = FS_LoadFile(path, &buffer); - if (!buffer) - goto fail; - s = buffer; + if (setjmp(md5_jmpbuf)) + return false; + + com_linenum = 1; // parse header - MD5_EXPECT("MD5Version"); - MD5_EXPECT("10"); + MD5_ParseExpect(&s, "MD5Version"); + MD5_ParseExpect(&s, "10"); // allocate data storage, now that we're definitely an MD5 Hunk_Begin(&model->skeleton_hunk, 0x800000); - OOM_CHECK(model->skeleton = mdl = MD5_Malloc(sizeof(*mdl))); + model->skeleton = mdl = MD5_Malloc(model, sizeof(*mdl)); - MD5_EXPECT("commandline"); - MD5_SKIP(); + MD5_ParseExpect(&s, "commandline"); + COM_SkipToken(&s); - MD5_EXPECT("numJoints"); - MD5_UINT(num_joints); - MD5_ENSURE(num_joints > 0, "No joints"); - MD5_ENSURE(num_joints <= MD5_MAX_JOINTS, "Too many joints"); - OOM_CHECK(mdl->base_skeleton = MD5_Malloc(num_joints * sizeof(mdl->base_skeleton[0]))); - mdl->num_joints = num_joints; + MD5_ParseExpect(&s, "numJoints"); + mdl->num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); - MD5_EXPECT("numMeshes"); - MD5_UINT(num_meshes); - MD5_ENSURE(num_meshes > 0, "No meshes"); - MD5_ENSURE(num_meshes <= MD5_MAX_MESHES, "Too many meshes"); - OOM_CHECK(mdl->meshes = MD5_Malloc(num_meshes * sizeof(mdl->meshes[0]))); - mdl->num_meshes = num_meshes; + MD5_ParseExpect(&s, "numMeshes"); + mdl->num_meshes = MD5_ParseUint(&s, 1, MD5_MAX_MESHES); - MD5_EXPECT("joints"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "joints"); + MD5_ParseExpect(&s, "{"); - for (i = 0; i < num_joints; i++) { - md5_joint_t *joint = &mdl->base_skeleton[i]; + for (i = 0; i < mdl->num_joints; i++) { + baseframe_joint_t *joint = &base_skeleton[i]; // skip name - MD5_SKIP(); + COM_SkipToken(&s); - uint32_t parent; - MD5_UINT(parent); - MD5_ENSURE(parent == -1 || (parent < num_joints && parent != i), "Bad parent joint"); - joint->parent = parent; + // skip parent + COM_SkipToken(&s); - MD5_VECTOR(joint->pos); - MD5_VECTOR(joint->orient); + MD5_ParseVector(&s, joint->pos); + MD5_ParseVector(&s, joint->orient); Quat_ComputeW(joint->orient); - joint->scale = 1.0f; } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - for (i = 0; i < num_meshes; i++) { + mdl->meshes = MD5_Malloc(model, mdl->num_meshes * sizeof(mdl->meshes[0])); + for (i = 0; i < mdl->num_meshes; i++) { md5_mesh_t *mesh = &mdl->meshes[i]; - uint32_t num_verts, num_tris, num_weights; - MD5_EXPECT("mesh"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "mesh"); + MD5_ParseExpect(&s, "{"); - MD5_EXPECT("shader"); - MD5_SKIP(); + MD5_ParseExpect(&s, "shader"); + COM_SkipToken(&s); - MD5_EXPECT("numverts"); - MD5_UINT(num_verts); - MD5_ENSURE(num_verts <= TESS_MAX_VERTICES, "Too many verts"); - OOM_CHECK(mesh->vertices = MD5_Malloc(num_verts * sizeof(mesh->vertices[0]))); - OOM_CHECK(mesh->tcoords = MD5_Malloc(num_verts * sizeof(mesh->tcoords[0]))); - mesh->num_verts = num_verts; + MD5_ParseExpect(&s, "numverts"); + mesh->num_verts = MD5_ParseUint(&s, 0, TESS_MAX_VERTICES); + mesh->vertices = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->vertices[0])); + mesh->tcoords = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->tcoords [0])); - for (j = 0; j < num_verts; j++) { - MD5_EXPECT("vert"); + for (j = 0; j < mesh->num_verts; j++) { + MD5_ParseExpect(&s, "vert"); - uint32_t vert_index; - MD5_UINT(vert_index); - MD5_ENSURE(vert_index < num_verts, "Bad vert index"); + uint32_t vert_index = MD5_ParseUint(&s, 0, mesh->num_verts - 1); maliastc_t *tc = &mesh->tcoords[vert_index]; - MD5_EXPECT("("); - MD5_FLOAT(tc->st[0]); - MD5_FLOAT(tc->st[1]); - MD5_EXPECT(")"); + MD5_ParseExpect(&s, "("); + tc->st[0] = MD5_ParseFloat(&s); + tc->st[1] = MD5_ParseFloat(&s); + MD5_ParseExpect(&s, ")"); md5_vertex_t *vert = &mesh->vertices[vert_index]; - MD5_UINT(vert->start); - MD5_UINT(vert->count); + vert->start = MD5_ParseUint(&s, 0, UINT16_MAX); + vert->count = MD5_ParseUint(&s, 0, UINT16_MAX); } - MD5_EXPECT("numtris"); - MD5_UINT(num_tris); - MD5_ENSURE(num_tris <= TESS_MAX_INDICES / 3, "Too many tris"); - OOM_CHECK(mesh->indices = MD5_Malloc(num_tris * 3 * sizeof(mesh->indices[0]))); + MD5_ParseExpect(&s, "numtris"); + uint32_t num_tris = MD5_ParseUint(&s, 0, TESS_MAX_INDICES / 3); + mesh->indices = MD5_Malloc(model, num_tris * 3 * sizeof(mesh->indices[0])); mesh->num_indices = num_tris * 3; for (j = 0; j < num_tris; j++) { - MD5_EXPECT("tri"); - - uint32_t tri_index; - MD5_UINT(tri_index); - MD5_ENSURE(tri_index < num_tris, "Bad tri index"); - - for (k = 0; k < 3; k++) { - uint32_t vert_index; - MD5_UINT(vert_index); - MD5_ENSURE(vert_index < mesh->num_verts, "Bad tri vert"); - mesh->indices[tri_index * 3 + k] = vert_index; - } + MD5_ParseExpect(&s, "tri"); + uint32_t tri_index = MD5_ParseUint(&s, 0, num_tris - 1); + for (k = 0; k < 3; k++) + mesh->indices[tri_index * 3 + k] = MD5_ParseUint(&s, 0, mesh->num_verts - 1); } - MD5_EXPECT("numweights"); - MD5_UINT(num_weights); - MD5_ENSURE(num_weights <= MD5_MAX_WEIGHTS, "Too many weights"); - OOM_CHECK(mesh->weights = MD5_Malloc(num_weights * sizeof(mesh->weights[0]))); - mesh->num_weights = num_weights; + MD5_ParseExpect(&s, "numweights"); + mesh->num_weights = MD5_ParseUint(&s, 0, MD5_MAX_WEIGHTS); + mesh->weights = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->weights [0])); + mesh->jointnums = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->jointnums[0])); - for (j = 0; j < num_weights; j++) { - MD5_EXPECT("weight"); + for (j = 0; j < mesh->num_weights; j++) { + MD5_ParseExpect(&s, "weight"); - uint32_t weight_index; - MD5_UINT(weight_index); - MD5_ENSURE(weight_index < num_weights, "Bad weight index"); + uint32_t weight_index = MD5_ParseUint(&s, 0, mesh->num_weights - 1); + mesh->jointnums[weight_index] = MD5_ParseUint(&s, 0, mdl->num_joints - 1); md5_weight_t *weight = &mesh->weights[weight_index]; - - uint32_t joint; - MD5_UINT(joint); - MD5_ENSURE(joint < mdl->num_joints, "Bad weight joint"); - weight->joint = joint; - - MD5_FLOAT(weight->bias); - MD5_VECTOR(weight->pos); + weight->bias = MD5_ParseFloat(&s); + MD5_ParseVector(&s, weight->pos); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); // check integrity of data; this has to be done last // because of circular data dependencies - for (j = 0; j < num_verts; j++) { + for (j = 0; j < mesh->num_verts; j++) { md5_vertex_t *vert = &mesh->vertices[j]; - MD5_ENSURE((uint64_t)vert->start + vert->count <= num_weights, "Bad vert start/count"); + if (vert->start + vert->count > mesh->num_weights) + MD5_ParseError("Bad vert start/count"); } - MD5_ComputeNormals(mesh, mdl->base_skeleton); + MD5_ComputeNormals(mesh, base_skeleton); } - FS_FreeFile(buffer); return true; - -fail: - MOD_PrintError(path, ret); - FS_FreeFile(buffer); - return false; } typedef struct { char name[MD5_MAX_JOINTNAME]; - uint32_t parent; - uint32_t flags; - uint32_t start_index; + int parent, flags, start_index; bool scale_pos; } joint_info_t; -typedef struct { - vec3_t pos; - quat_t orient; -} baseframe_joint_t; - #define MD5_NUM_ANIMATED_COMPONENT_BITS 6 /** @@ -1057,21 +1031,21 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, Quat_ComputeW(animated_quat); - // parent should already be calculated md5_joint_t *thisJoint = &skeleton_frame[i]; if (joint_infos[i].scale_pos) VectorScale(animated_position, thisJoint->scale, animated_position); - int parent = thisJoint->parent = (int32_t)joint_infos[i].parent; + int parent = joint_infos[i].parent; if (parent < 0) { VectorCopy(animated_position, thisJoint->pos); Vector4Copy(animated_quat, thisJoint->orient); continue; } - Q_assert(parent < num_joints); - md5_joint_t *parentJoint = &skeleton_frame[parent]; + // parent should already be calculated + Q_assert(parent < i); + const md5_joint_t *parentJoint = &skeleton_frame[parent]; // add positions vec3_t rotated_pos; @@ -1087,7 +1061,7 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, /** * Parse some JSON vomit. Don't ask. */ -static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t *joint_infos) +static void MD5_LoadScales(const md5_model_t *model, const char *path, joint_info_t *joint_infos) { const jsmntok_t *tok, *end; jsmn_parser parser; @@ -1175,66 +1149,60 @@ static void MOD_LoadMD5Scale(md5_model_t *model, const char *path, joint_info_t /** * Load an MD5 animation from file. */ -static bool MOD_LoadMD5Anim(model_t *model, const char *path) +static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) { joint_info_t joint_infos[MD5_MAX_JOINTS]; baseframe_joint_t base_frame[MD5_MAX_JOINTS]; float anim_frame_data[MD5_MAX_JOINTS * MD5_NUM_ANIMATED_COMPONENT_BITS]; - uint32_t num_frames, num_joints, num_animated_components; + int num_joints, num_animated_components; md5_model_t *mdl = model->skeleton; - int i, j, ret; - void *buffer; - const char *s; + int i, j; - ret = FS_LoadFile(path, &buffer); - if (!buffer) - goto fail; - s = buffer; + if (setjmp(md5_jmpbuf)) + return false; + + com_linenum = 1; // parse header - MD5_EXPECT("MD5Version"); - MD5_EXPECT("10"); + MD5_ParseExpect(&s, "MD5Version"); + MD5_ParseExpect(&s, "10"); - MD5_EXPECT("commandline"); - MD5_SKIP(); + MD5_ParseExpect(&s, "commandline"); + COM_SkipToken(&s); - MD5_EXPECT("numFrames"); - MD5_UINT(num_frames); - // md5 replacements need at least 1 frame, because the + MD5_ParseExpect(&s, "numFrames"); + // MD5 replacements need at least 1 frame, because the // pose frame isn't used - MD5_ENSURE(num_frames > 0, "No frames"); - MD5_ENSURE(num_frames <= MD5_MAX_FRAMES, "Too many frames"); - mdl->num_frames = num_frames; + mdl->num_frames = MD5_ParseUint(&s, 1, MD5_MAX_FRAMES); // warn on mismatched frame counts (not fatal) if (mdl->num_frames != model->numframes) Com_WPrintf("%s doesn't match frame count for %s (%i vs %i)\n", path, model->name, mdl->num_frames, model->numframes); - MD5_EXPECT("numJoints"); - MD5_UINT(num_joints); - MD5_ENSURE(num_joints == mdl->num_joints, "Bad numJoints"); + MD5_ParseExpect(&s, "numJoints"); + num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); + if (num_joints != mdl->num_joints) + MD5_ParseError("Bad numJoints"); - MD5_EXPECT("frameRate"); - MD5_SKIP(); + MD5_ParseExpect(&s, "frameRate"); + COM_SkipToken(&s); - MD5_EXPECT("numAnimatedComponents"); - MD5_UINT(num_animated_components); - MD5_ENSURE(num_animated_components <= q_countof(anim_frame_data), "Bad numAnimatedComponents"); + MD5_ParseExpect(&s, "numAnimatedComponents"); + num_animated_components = MD5_ParseUint(&s, 0, q_countof(anim_frame_data)); - MD5_EXPECT("hierarchy"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "hierarchy"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_joints; i++) { joint_info_t *joint_info = &joint_infos[i]; - Q_strlcpy(joint_info->name, COM_Parse(&s), sizeof(joint_info->name)); + COM_ParseToken(&s, joint_info->name, sizeof(joint_info->name)); - MD5_UINT(joint_info->parent); - MD5_UINT(joint_info->flags); - MD5_UINT(joint_info->start_index); - - joint_info->scale_pos = false; + joint_info->parent = MD5_ParseInt (&s, -1, mdl->num_joints - 1); + joint_info->flags = MD5_ParseUint(&s, 0, UINT32_MAX); + joint_info->start_index = MD5_ParseUint(&s, 0, num_animated_components); + joint_info->scale_pos = false; // validate animated components int num_components = 0; @@ -1243,42 +1211,44 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) if (joint_info->flags & BIT(j)) num_components++; - MD5_ENSURE((uint64_t)joint_info->start_index + num_components <= num_animated_components, "Bad joint info"); + if (joint_info->start_index + num_components > num_animated_components) + MD5_ParseError("Bad joint info"); - // validate parents; they need to match the base skeleton - MD5_ENSURE(joint_info->parent == mdl->base_skeleton[i].parent, "Bad parent joint"); + // parent must be -1 or already processed joint + if (joint_info->parent >= i) + MD5_ParseError("Bad parent joint"); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); // bounds are ignored and are apparently usually wrong anyways - // so we'll just rely on them being "replacement" md2s/md3s. - // the md2/md3 ones are used instead. - MD5_EXPECT("bounds"); - MD5_EXPECT("{"); + // so we'll just rely on them being "replacement" MD2s/MD3s. + // the MD2/MD3 ones are used instead. + MD5_ParseExpect(&s, "bounds"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_frames * 2; i++) { vec3_t dummy; - MD5_VECTOR(dummy); + MD5_ParseVector(&s, dummy); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - MD5_EXPECT("baseframe"); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "baseframe"); + MD5_ParseExpect(&s, "{"); for (i = 0; i < mdl->num_joints; i++) { baseframe_joint_t *base_joint = &base_frame[i]; - MD5_VECTOR(base_joint->pos); - MD5_VECTOR(base_joint->orient); + MD5_ParseVector(&s, base_joint->pos); + MD5_ParseVector(&s, base_joint->orient); Quat_ComputeW(base_joint->orient); } - MD5_EXPECT("}"); + MD5_ParseExpect(&s, "}"); - OOM_CHECK(mdl->skeleton_frames = MD5_Malloc(sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints)); + mdl->skeleton_frames = MD5_Malloc(model, sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints); // initialize scales for (i = 0; i < mdl->num_frames * mdl->num_joints; i++) @@ -1288,34 +1258,45 @@ static bool MOD_LoadMD5Anim(model_t *model, const char *path) char scale_path[MAX_QPATH]; if (COM_StripExtension(scale_path, path, sizeof(scale_path)) < sizeof(scale_path) && Q_strlcat(scale_path, ".md5scale", sizeof(scale_path)) < sizeof(scale_path)) - MOD_LoadMD5Scale(model->skeleton, scale_path, joint_infos); + MD5_LoadScales(model->skeleton, scale_path, joint_infos); else Com_WPrintf("MD5 scale path too long: %s\n", scale_path); for (i = 0; i < mdl->num_frames; i++) { - MD5_EXPECT("frame"); + MD5_ParseExpect(&s, "frame"); - uint32_t frame_index; - MD5_UINT(frame_index); - MD5_ENSURE(frame_index < mdl->num_frames, "Bad frame index"); + uint32_t frame_index = MD5_ParseUint(&s, 0, mdl->num_frames - 1); - MD5_EXPECT("{"); + MD5_ParseExpect(&s, "{"); for (j = 0; j < num_animated_components; j++) - MD5_FLOAT(anim_frame_data[j]); - MD5_EXPECT("}"); + anim_frame_data[j] = MD5_ParseFloat(&s); + MD5_ParseExpect(&s, "}"); /* Build frame skeleton from the collected data */ MD5_BuildFrameSkeleton(joint_infos, base_frame, anim_frame_data, &mdl->skeleton_frames[frame_index * mdl->num_joints], mdl->num_joints); } - FS_FreeFile(buffer); return true; +} -fail: - MOD_PrintError(path, ret); - FS_FreeFile(buffer); - return false; +static bool MD5_LoadFile(model_t *model, const char *path, bool (*parse)(model_t *, const char *, const char *)) +{ + void *data; + int ret = FS_LoadFile(path, &data); + if (!data) { + MOD_PrintError(path, ret); + return false; + } + + ret = parse(model, data, path); + FS_FreeFile(data); + if (!ret) { + MOD_PrintError(path, Q_ERR_INVALID_FORMAT); + return false; + } + + return true; } static bool MD5_LoadSkins(model_t *model) @@ -1365,10 +1346,9 @@ static void MOD_LoadMD5(model_t *model) if (!FS_FileExists(mesh_path) || !FS_FileExists(anim_path)) return; - if (!MOD_LoadMD5Mesh(model, mesh_path)) + if (!MD5_LoadFile(model, mesh_path, MD5_ParseMesh)) goto fail; - - if (!MOD_LoadMD5Anim(model, anim_path)) + if (!MD5_LoadFile(model, anim_path, MD5_ParseAnim)) goto fail; if (!MD5_LoadSkins(model)) goto fail; From bfa623d48bdde02d9b9fa9fa0242089a98f5c416 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 834/974] Simplify Quat_SLerp() close angle check. --- src/common/math.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/math.c b/src/common/math.c index 53659ca81..4aca75c55 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -418,7 +418,7 @@ void Quat_ComputeW(quat_t q) } } -#define QUAT_EPSILON 0.000001f +#define DOT_THRESHOLD 0.9995f void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontlerp, quat_t out) { @@ -453,7 +453,7 @@ void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontler // compute interpolation fraction float k0, k1; - if (1.0f - cosOmega <= QUAT_EPSILON) { + if (cosOmega > DOT_THRESHOLD) { // very close - just use linear interpolation k0 = backlerp; k1 = frontlerp; From 782c15bfcec4cdea2591da6ee19afc00532fdf72 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 21:21:22 +0300 Subject: [PATCH 835/974] Add VectorRotate() macro. --- inc/shared/shared.h | 10 ++++++---- src/client/newfx.c | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index abf69c6d7..233384809 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -231,6 +231,11 @@ typedef struct { ((d)[0]=(a)[0]+(b)[0]*(c)[0], \ (d)[1]=(a)[1]+(b)[1]*(c)[1], \ (d)[2]=(a)[2]+(b)[2]*(c)[2]) +#define VectorRotate(in,axis,out) \ + ((out)[0]=DotProduct(in,(axis)[0]), \ + (out)[1]=DotProduct(in,(axis)[1]), \ + (out)[2]=DotProduct(in,(axis)[2])) + #define VectorEmpty(v) ((v)[0]==0&&(v)[1]==0&&(v)[2]==0) #define VectorCompare(v1,v2) ((v1)[0]==(v2)[0]&&(v1)[1]==(v2)[1]&&(v1)[2]==(v2)[2]) #define VectorLength(v) (sqrtf(DotProduct((v),(v)))) @@ -295,11 +300,8 @@ static inline void TransposeAxis(vec3_t axis[3]) static inline void RotatePoint(vec3_t point, const vec3_t axis[3]) { vec3_t temp; - VectorCopy(point, temp); - point[0] = DotProduct(temp, axis[0]); - point[1] = DotProduct(temp, axis[1]); - point[2] = DotProduct(temp, axis[2]); + VectorRotate(temp, axis, point); } static inline uint32_t Q_npot32(uint32_t k) diff --git a/src/client/newfx.c b/src/client/newfx.c index 7dd4fedd7..70650217d 100644 --- a/src/client/newfx.c +++ b/src/client/newfx.c @@ -990,9 +990,7 @@ void CL_HologramParticles(const vec3_t org) p->time = cl.time; p->color = 0xd0; - VectorCopy(bytedirs[i], dir); - RotatePoint(dir, axis); - + VectorRotate(bytedirs[i], axis, dir); VectorMA(org, 100.0f, dir, p->org); VectorClear(p->vel); From 81b2eb1d2e775e1ce4d48e44710fc62c75f273bc Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 836/974] Optimize MD5 interpolation more. Get rid of Quat_RotatePoint() in hot path. Replace it with matrix multiply. Gives some nice FPS boost. --- inc/common/math.h | 20 ++++++++++++++++++++ inc/shared/platform.h | 4 ++++ src/refresh/gl.h | 1 + src/refresh/mesh.c | 32 +++++++++++++++++++++----------- src/refresh/models.c | 3 +++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/inc/common/math.h b/inc/common/math.h index 91ce8c4be..69d1ade0a 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -124,6 +124,26 @@ static inline void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) out[Z] = output[Z]; } +static inline void Quat_ToAxis(const quat_t q, vec3_t axis[3]) +{ + float q0 = q[W]; + float q1 = q[X]; + float q2 = q[Y]; + float q3 = q[Z]; + + axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; + axis[0][1] = 2 * (q1 * q2 - q0 * q3); + axis[0][2] = 2 * (q1 * q3 + q0 * q2); + + axis[1][0] = 2 * (q1 * q2 + q0 * q3); + axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; + axis[1][2] = 2 * (q2 * q3 - q0 * q1); + + axis[2][0] = 2 * (q1 * q3 - q0 * q2); + axis[2][1] = 2 * (q2 * q3 + q0 * q1); + axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; +} + #undef X #undef Y #undef Z diff --git a/inc/shared/platform.h b/inc/shared/platform.h index 184c87337..9f074fd70 100644 --- a/inc/shared/platform.h +++ b/inc/shared/platform.h @@ -139,6 +139,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_unreachable() abort() #endif +#define q_forceinline inline __attribute__((always_inline)) + #else /* __GNUC__ */ #ifdef _MSC_VER @@ -147,12 +149,14 @@ with this program; if not, write to the Free Software Foundation, Inc., #define q_malloc __declspec(restrict) #define q_alignof(t) __alignof(t) #define q_unreachable() __assume(0) +#define q_forceinline __forceinline #else #define q_noreturn #define q_noinline #define q_malloc #define q_alignof(t) 1 #define q_unreachable() abort() +#define q_forceinline inline #endif #define q_printf(f, a) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 920868ea1..b9fcd5b02 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -330,6 +330,7 @@ typedef struct { vec3_t pos; float scale; quat_t orient; + vec3_t axis[3]; } md5_joint_t; /* Vertex */ diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 487c6f7cd..a30231513 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -668,13 +668,17 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, #if USE_MD5 +#if (defined __OPTIMIZE__) && (defined __GNUC__) && !(defined __clang__) +#pragma GCC optimize("O3") +#endif + // for the given vertex, set of weights & skeleton, calculate // the output vertex (and optionally normal). -static inline void calc_skel_vert(const md5_vertex_t *vert, - const md5_mesh_t *mesh, - const md5_joint_t *skeleton, - float *restrict out_position, - float *restrict out_normal) +static q_forceinline void calc_skel_vert(const md5_vertex_t *vert, + const md5_mesh_t *mesh, + const md5_joint_t *skeleton, + float *restrict out_position, + float *restrict out_normal) { VectorClear(out_position); @@ -686,13 +690,13 @@ static inline void calc_skel_vert(const md5_vertex_t *vert, const md5_joint_t *joint = &skeleton[mesh->jointnums[vert->start + i]]; vec3_t wv; - Quat_RotatePoint(joint->orient, weight->pos, wv); + VectorRotate(weight->pos, joint->axis, wv); VectorMA(joint->pos, joint->scale, wv, wv); VectorMA(out_position, weight->bias, wv, out_position); if (out_normal) { - Quat_RotatePoint(joint->orient, vert->normal, wv); + VectorRotate(vert->normal, joint->axis, wv); VectorMA(out_normal, weight->bias, wv, out_normal); } } @@ -738,14 +742,20 @@ static void lerp_alias_skeleton(const md5_model_t *model) unsigned frame_b = newframenum % model->num_frames; const md5_joint_t *skel_a = &model->skeleton_frames[frame_a * model->num_joints]; const md5_joint_t *skel_b = &model->skeleton_frames[frame_b * model->num_joints]; + md5_joint_t *out = temp_skeleton; - for (int i = 0; i < model->num_joints; i++) { - temp_skeleton[i].scale = skel_b[i].scale; - LerpVector2(skel_a[i].pos, skel_b[i].pos, backlerp, frontlerp, temp_skeleton[i].pos); - Quat_SLerp(skel_a[i].orient, skel_b[i].orient, backlerp, frontlerp, temp_skeleton[i].orient); + for (int i = 0; i < model->num_joints; i++, skel_a++, skel_b++, out++) { + out->scale = skel_b->scale; + LerpVector2(skel_a->pos, skel_b->pos, backlerp, frontlerp, out->pos); + Quat_SLerp(skel_a->orient, skel_b->orient, backlerp, frontlerp, out->orient); + Quat_ToAxis(out->orient, out->axis); } } +#if (defined __OPTIMIZE__) && (defined __GNUC__) && !(defined __clang__) +#pragma GCC reset_options +#endif + static void draw_skeleton_mesh(const md5_model_t *model, const md5_mesh_t *mesh, const md5_joint_t *skel) { if (glr.ent->flags & RF_SHELL_MASK) diff --git a/src/refresh/models.c b/src/refresh/models.c index 580d0dcf6..c747342e5 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1040,6 +1040,7 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, if (parent < 0) { VectorCopy(animated_position, thisJoint->pos); Vector4Copy(animated_quat, thisJoint->orient); + Quat_ToAxis(thisJoint->orient, thisJoint->axis); continue; } @@ -1055,6 +1056,8 @@ static void MD5_BuildFrameSkeleton(const joint_info_t *joint_infos, // concat rotations Quat_MultiplyQuat(parentJoint->orient, animated_quat, thisJoint->orient); Quat_Normalize(thisJoint->orient); + + Quat_ToAxis(thisJoint->orient, thisJoint->axis); } } From 943a00c997ea1b6b112dbfbcc40f3c1d552e2fec Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 00:01:00 +0300 Subject: [PATCH 837/974] Un-inline quaternion functions. --- inc/common/math.h | 77 +++-------------------------------------------- src/common/math.c | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/inc/common/math.h b/inc/common/math.h index 69d1ade0a..8546f852a 100644 --- a/inc/common/math.h +++ b/inc/common/math.h @@ -73,82 +73,15 @@ void RotatePointAroundVector(vec3_t out, const vec3_t dir, const vec3_t in, floa // quaternion routines, for MD5 skeletons #if USE_MD5 - -#define X 0 -#define Y 1 -#define Z 2 -#define W 3 - typedef vec4_t quat_t; - void Quat_ComputeW(quat_t q); void Quat_SLerp(const quat_t qa, const quat_t qb, float backlerp, float frontlerp, quat_t out); float Quat_Normalize(quat_t q); - -static inline void Quat_MultiplyQuat(const quat_t qa, const quat_t qb, quat_t out) -{ - out[W] = (qa[W] * qb[W]) - (qa[X] * qb[X]) - (qa[Y] * qb[Y]) - (qa[Z] * qb[Z]); - out[X] = (qa[X] * qb[W]) + (qa[W] * qb[X]) + (qa[Y] * qb[Z]) - (qa[Z] * qb[Y]); - out[Y] = (qa[Y] * qb[W]) + (qa[W] * qb[Y]) + (qa[Z] * qb[X]) - (qa[X] * qb[Z]); - out[Z] = (qa[Z] * qb[W]) + (qa[W] * qb[Z]) + (qa[X] * qb[Y]) - (qa[Y] * qb[X]); -} - -static inline void Quat_MultiplyVector(const quat_t q, const vec3_t v, quat_t out) -{ - out[W] = -(q[X] * v[X]) - (q[Y] * v[Y]) - (q[Z] * v[Z]); - out[X] = (q[W] * v[X]) + (q[Y] * v[Z]) - (q[Z] * v[Y]); - out[Y] = (q[W] * v[Y]) + (q[Z] * v[X]) - (q[X] * v[Z]); - out[Z] = (q[W] * v[Z]) + (q[X] * v[Y]) - (q[Y] * v[X]); -} - -// Conjugate quaternion. Also, inverse, for unit quaternions (which MD5 quats are) -static inline void Quat_Conjugate(const quat_t in, quat_t out) -{ - out[W] = in[W]; - out[X] = -in[X]; - out[Y] = -in[Y]; - out[Z] = -in[Z]; -} - -static inline void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) -{ - quat_t tmp, inv, output; - - // Assume q is unit quaternion - Quat_Conjugate(q, inv); - Quat_MultiplyVector(q, in, tmp); - Quat_MultiplyQuat(tmp, inv, output); - - out[X] = output[X]; - out[Y] = output[Y]; - out[Z] = output[Z]; -} - -static inline void Quat_ToAxis(const quat_t q, vec3_t axis[3]) -{ - float q0 = q[W]; - float q1 = q[X]; - float q2 = q[Y]; - float q3 = q[Z]; - - axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; - axis[0][1] = 2 * (q1 * q2 - q0 * q3); - axis[0][2] = 2 * (q1 * q3 + q0 * q2); - - axis[1][0] = 2 * (q1 * q2 + q0 * q3); - axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; - axis[1][2] = 2 * (q2 * q3 - q0 * q1); - - axis[2][0] = 2 * (q1 * q3 - q0 * q2); - axis[2][1] = 2 * (q2 * q3 + q0 * q1); - axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; -} - -#undef X -#undef Y -#undef Z -#undef W - +void Quat_MultiplyQuat(const float *restrict qa, const float *restrict qb, quat_t out); +void Quat_MultiplyVector(const float *restrict q, const float *restrict v, quat_t out); +void Quat_Conjugate(const quat_t in, quat_t out); +void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out); +void Quat_ToAxis(const quat_t q, vec3_t axis[3]); #endif #endif // USE_REF diff --git a/src/common/math.c b/src/common/math.c index 4aca75c55..94f071dd2 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -490,6 +490,65 @@ float Quat_Normalize(quat_t q) return length; } +void Quat_MultiplyQuat(const float *restrict qa, const float *restrict qb, quat_t out) +{ + out[W] = (qa[W] * qb[W]) - (qa[X] * qb[X]) - (qa[Y] * qb[Y]) - (qa[Z] * qb[Z]); + out[X] = (qa[X] * qb[W]) + (qa[W] * qb[X]) + (qa[Y] * qb[Z]) - (qa[Z] * qb[Y]); + out[Y] = (qa[Y] * qb[W]) + (qa[W] * qb[Y]) + (qa[Z] * qb[X]) - (qa[X] * qb[Z]); + out[Z] = (qa[Z] * qb[W]) + (qa[W] * qb[Z]) + (qa[X] * qb[Y]) - (qa[Y] * qb[X]); +} + +void Quat_MultiplyVector(const float *restrict q, const float *restrict v, quat_t out) +{ + out[W] = -(q[X] * v[X]) - (q[Y] * v[Y]) - (q[Z] * v[Z]); + out[X] = (q[W] * v[X]) + (q[Y] * v[Z]) - (q[Z] * v[Y]); + out[Y] = (q[W] * v[Y]) + (q[Z] * v[X]) - (q[X] * v[Z]); + out[Z] = (q[W] * v[Z]) + (q[X] * v[Y]) - (q[Y] * v[X]); +} + +// Conjugate quaternion. Also, inverse, for unit quaternions (which MD5 quats are) +void Quat_Conjugate(const quat_t in, quat_t out) +{ + out[W] = in[W]; + out[X] = -in[X]; + out[Y] = -in[Y]; + out[Z] = -in[Z]; +} + +void Quat_RotatePoint(const quat_t q, const vec3_t in, vec3_t out) +{ + quat_t tmp, inv, output; + + // Assume q is unit quaternion + Quat_Conjugate(q, inv); + Quat_MultiplyVector(q, in, tmp); + Quat_MultiplyQuat(tmp, inv, output); + + out[X] = output[X]; + out[Y] = output[Y]; + out[Z] = output[Z]; +} + +void Quat_ToAxis(const quat_t q, vec3_t axis[3]) +{ + float q0 = q[W]; + float q1 = q[X]; + float q2 = q[Y]; + float q3 = q[Z]; + + axis[0][0] = 2 * (q0 * q0 + q1 * q1) - 1; + axis[0][1] = 2 * (q1 * q2 - q0 * q3); + axis[0][2] = 2 * (q1 * q3 + q0 * q2); + + axis[1][0] = 2 * (q1 * q2 + q0 * q3); + axis[1][1] = 2 * (q0 * q0 + q2 * q2) - 1; + axis[1][2] = 2 * (q2 * q3 - q0 * q1); + + axis[2][0] = 2 * (q1 * q3 - q0 * q2); + axis[2][1] = 2 * (q2 * q3 + q0 * q1); + axis[2][2] = 2 * (q0 * q0 + q3 * q3) - 1; +} + #endif // USE_MD5 #endif // USE_REF From c2cb036f075e6d7826813201cd9597c21dfdfa48 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Thu, 3 Oct 2024 18:00:13 +0300 Subject: [PATCH 838/974] Only warn if MD5 has less frames than MD2. --- src/refresh/models.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index c747342e5..b93b22693 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1179,9 +1179,9 @@ static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) mdl->num_frames = MD5_ParseUint(&s, 1, MD5_MAX_FRAMES); // warn on mismatched frame counts (not fatal) - if (mdl->num_frames != model->numframes) - Com_WPrintf("%s doesn't match frame count for %s (%i vs %i)\n", - path, model->name, mdl->num_frames, model->numframes); + if (mdl->num_frames < model->numframes) + Com_WPrintf("%s has less frames than %s (%i < %i)\n", path, + model->name, mdl->num_frames, model->numframes); MD5_ParseExpect(&s, "numJoints"); num_joints = MD5_ParseUint(&s, 1, MD5_MAX_JOINTS); From 6e77efb457d7ed7a2f9c801ddf12773f47c117fa Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 4 Oct 2024 16:24:19 -0400 Subject: [PATCH 839/974] Added comments --- inc/shared/gameext.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index deba7dbf1..0658a065c 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -64,6 +64,15 @@ typedef struct { #endif } customize_entity_t; +/* + +game_import_ex_t + +These pointer functions exist in the engine (server), and this enables importing them to be available for use by the gamelib + + server->gamedll + +*/ typedef struct { uint32_t apiversion; uint32_t structsize; @@ -78,6 +87,15 @@ typedef struct { void *(*CheckForExtension)(char *text); } game_import_ex_t; +/* + +game_export_ex_t + +These pointer functions exist in the gamedll, and this enables importing them to be available for use by the engine (server) + + gamedll->server + +*/ typedef struct { uint32_t apiversion; uint32_t structsize; From 33599442cc087efc666858b7b28c5bfb70c180e7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 4 Oct 2024 16:44:01 -0400 Subject: [PATCH 840/974] Added sv_load_ent toggle cvar --- doc/server.md | 3 +++ src/server/init.c | 4 +++- src/server/main.c | 1 + src/server/server.h | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/server.md b/doc/server.md index 84c5ccfac..1e25e3a45 100644 --- a/doc/server.md +++ b/doc/server.md @@ -828,6 +828,9 @@ also create an alias for the map. How to create such files is out of scope of this manual (search the internet for ‘r1q2 map override file generator’). +This is togglable with `load_ent` -- default enabled (`1`), a setting of +`0` will disable loading overrides of either type + ### map\_visibility\_patch Attempt to patch miscalculated visibility data for some well-known maps (q2dm1, q2dm3 and q2dm8 are patched so far), fixing disappearing walls diff --git a/src/server/init.c b/src/server/init.c index 359e33352..93a3ffc4a 100644 --- a/src/server/init.c +++ b/src/server/init.c @@ -241,6 +241,7 @@ static server_state_t get_server_state(const char *s) return ss_game; } +cvar_t *sv_load_ent; static bool parse_and_check_server(mapcmd_t *cmd, const char *server, bool nextserver) { char expanded[MAX_QPATH], *ch; @@ -286,7 +287,8 @@ static bool parse_and_check_server(mapcmd_t *cmd, const char *server, bool nexts break; default: - CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); // may override server! + if (sv_load_ent->integer) + CM_LoadOverrides(&cmd->cm, cmd->server, sizeof(cmd->server)); // may override server! if (Q_concat(expanded, sizeof(expanded), "maps/", cmd->server, ".bsp") < sizeof(expanded)) ret = CM_LoadMap(&cmd->cm, expanded); if (ret < 0) diff --git a/src/server/main.c b/src/server/main.c index c5006269e..b52b9f71d 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -2455,6 +2455,7 @@ void SV_Init(void) g_view_predict = Cvar_Get("g_view_predict", "0", CVAR_ROM); g_view_low = Cvar_Get("g_view_low", "0", CVAR_ROM); g_view_high = Cvar_Get("g_view_high", "0", CVAR_ROM); + sv_load_ent = Cvar_Get("sv_load_ent", "1", CVAR_LATCH); init_rate_limits(); diff --git a/src/server/server.h b/src/server/server.h index e9a0d4803..76cce74ae 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -582,6 +582,7 @@ extern cvar_t *sv_ghostime; extern client_t *sv_client; extern edict_t *sv_player; +extern cvar_t *sv_load_ent; //=========================================================== // From 15bf62b13e5ec6ef45ebc3ac62a7fe87885a3ee6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 4 Oct 2024 16:44:48 -0400 Subject: [PATCH 841/974] Fixed documentation --- doc/server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/server.md b/doc/server.md index 1e25e3a45..805528e3a 100644 --- a/doc/server.md +++ b/doc/server.md @@ -828,7 +828,7 @@ also create an alias for the map. How to create such files is out of scope of this manual (search the internet for ‘r1q2 map override file generator’). -This is togglable with `load_ent` -- default enabled (`1`), a setting of +This is togglable with `sv_load_ent` -- default enabled (`1`), a setting of `0` will disable loading overrides of either type ### map\_visibility\_patch From 995432ba5d0a374efe4a2ad8b5890ea2a4082b28 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 4 Oct 2024 22:01:04 -0400 Subject: [PATCH 842/974] Fixed crashing bug that tried to find a null edict --- src/action/botlib/botlib_esp.c | 50 ++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index 4980e1d6d..b982e3ad2 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -110,9 +110,9 @@ int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader) } if(leader == NULL) - gi.dprintf("%s: there are no nodes near ETV target\n", __func__); + BOTLIB_Debug("%s: there are no nodes near ETV target\n", __func__); else - gi.dprintf("%s: there are no nodes near the leader\n", __func__); + BOTLIB_Debug("%s: there are no nodes near the leader\n", __func__); return INVALID; } @@ -266,27 +266,31 @@ int BOTLIB_InterceptLeader_3Team(edict_t* self) int BOTLIB_InterceptLeader_2Team(edict_t* self) { - int myTeam = self->client->resp.team; - int espMode = EspModeCheck(); - - if (espMode == ESPMODE_ATL) { - if (myTeam == TEAM1) { - if (IS_ALIVE(EspGetLeader(TEAM2))) { - return BOTLIB_FindEnemyLeaderNode(self, TEAM2); - } - } else { - if (IS_ALIVE(EspGetLeader(TEAM1))) { - return BOTLIB_FindEnemyLeaderNode(self, TEAM1); - } - } - } else if (espMode == ESPMODE_ETV) { - if (myTeam == TEAM2) { - if (IS_ALIVE(EspGetLeader(TEAM1))) { - return BOTLIB_ESPGetTargetNode(self, NULL); - } - } - } - return INVALID; // No team leaders are alive, so no node to intercept + int myTeam = self->client->resp.team; + int espMode = EspModeCheck(); + edict_t* leader; + + if (espMode == ESPMODE_ATL) { + if (myTeam == TEAM1) { + leader = EspGetLeader(TEAM2); + if (leader != NULL && IS_ALIVE(leader)) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM2); + } + } else { + leader = EspGetLeader(TEAM1); + if (leader != NULL && IS_ALIVE(leader)) { + return BOTLIB_FindEnemyLeaderNode(self, TEAM1); + } + } + } else if (espMode == ESPMODE_ETV) { + if (myTeam == TEAM2) { + leader = EspGetLeader(TEAM1); + if (leader != NULL && IS_ALIVE(leader)) { + return BOTLIB_ESPGetTargetNode(self, NULL); + } + } + } + return INVALID; // No team leaders are alive, so no node to intercept } int BOTLIB_InterceptLeader_ETV(edict_t* self) From 52d337cd06c2ef9d9418d758301ffb07d76dc791 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 4 Oct 2024 22:10:58 -0400 Subject: [PATCH 843/974] Build changes to fix sync/aq2-tng --- .gitignore | 1 + subprojects/khr-headers.wrap | 6 +- subprojects/libcurl.wrap | 12 +- subprojects/libjpeg-turbo.wrap | 18 +- subprojects/libpng.wrap | 16 +- subprojects/openal-soft.wrap | 13 +- subprojects/packagefiles/curl/meson.build | 232 ++++++++++++++++++ .../packagefiles/curl/meson_options.txt | 1 + subprojects/packagefiles/khr-headers.tar.xz | Bin 0 -> 111740 bytes .../packagefiles/openal-soft/meson.build | 3 + 10 files changed, 267 insertions(+), 35 deletions(-) create mode 100644 subprojects/packagefiles/curl/meson.build create mode 100644 subprojects/packagefiles/curl/meson_options.txt create mode 100644 subprojects/packagefiles/khr-headers.tar.xz create mode 100644 subprojects/packagefiles/openal-soft/meson.build diff --git a/.gitignore b/.gitignore index b58d1222e..5d3374bbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /subprojects/*/ +!/subprojects/packagefiles/ /tags /TAGS /.vs/* diff --git a/subprojects/khr-headers.wrap b/subprojects/khr-headers.wrap index 3e3caa844..cedbb6bd9 100644 --- a/subprojects/khr-headers.wrap +++ b/subprojects/khr-headers.wrap @@ -1,5 +1,3 @@ [wrap-file] -directory = khr-headers-20220530 -source_url = https://skuller.net/meson/khr-headers-20220530.zip -source_filename = khr-headers-20220530.zip -source_hash = c90019de9f55afae8e6a8b15ae4e23d921b98c1571af9fcfd5525df059a160f3 +directory = khr-headers +source_filename = khr-headers.tar.xz diff --git a/subprojects/libcurl.wrap b/subprojects/libcurl.wrap index df7c7745c..ce2622169 100644 --- a/subprojects/libcurl.wrap +++ b/subprojects/libcurl.wrap @@ -1,11 +1,9 @@ [wrap-file] -directory = curl-8.9.1 -source_url = https://curl.se/download/curl-8.9.1.tar.xz -source_filename = curl-8.9.1.tar.xz -source_hash = f292f6cc051d5bbabf725ef85d432dfeacc8711dd717ea97612ae590643801e5 -patch_url = https://skuller.net/meson/libcurl_8.9.1-1_patch.zip -patch_filename = libcurl_8.9.1-1_patch.zip -patch_hash = b7f4ef775d912da335387b222dc1caf421fb434cc36aeee84a032fb28cac5c77 +directory = curl-8.10.1 +source_url = https://curl.se/download/curl-8.10.1.tar.xz +source_filename = curl-8.10.1.tar.xz +source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee +patch_directory = curl [provide] libcurl = libcurl_dep diff --git a/subprojects/libjpeg-turbo.wrap b/subprojects/libjpeg-turbo.wrap index 71f38a08c..f9eda5fb3 100644 --- a/subprojects/libjpeg-turbo.wrap +++ b/subprojects/libjpeg-turbo.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = libjpeg-turbo-3.0.3 -source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.3/libjpeg-turbo-3.0.3.tar.gz -source_filename = libjpeg-turbo-3.0.3.tar.gz -source_hash = 343e789069fc7afbcdfe44dbba7dbbf45afa98a15150e079a38e60e44578865d -patch_filename = libjpeg-turbo_3.0.3-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.3-1/get_patch -patch_hash = 8bbf4f205e54e73c48c14dae7fcc71f152cdc8fea6d55a1af1e3108d10c6a2e4 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.3-1/libjpeg-turbo-3.0.3.tar.gz -wrapdb_version = 3.0.3-1 +directory = libjpeg-turbo-3.0.4 +source_url = https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.0.4/libjpeg-turbo-3.0.4.tar.gz +source_filename = libjpeg-turbo-3.0.4.tar.gz +source_hash = 99130559e7d62e8d695f2c0eaeef912c5828d5b84a0537dcb24c9678c9d5b76b +patch_filename = libjpeg-turbo_3.0.4-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libjpeg-turbo_3.0.4-2/get_patch +patch_hash = f12d14c6ff4ae83eddc1a2e8cfd96a9b7a40b6bdc8ff345ca8094abf4c3da928 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/libjpeg-turbo_3.0.4-2/libjpeg-turbo-3.0.4.tar.gz +wrapdb_version = 3.0.4-2 [provide] dependency_names = libjpeg, libturbojpeg diff --git a/subprojects/libpng.wrap b/subprojects/libpng.wrap index d32e859a4..6632152df 100644 --- a/subprojects/libpng.wrap +++ b/subprojects/libpng.wrap @@ -1,12 +1,12 @@ [wrap-file] -directory = libpng-1.6.43 -source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.43.tar.xz -source_filename = libpng-1.6.43.tar.xz -source_hash = 6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c -patch_filename = libpng_1.6.43-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.43-2/get_patch -patch_hash = 49951297edf03e81d925ab03726555f09994ad1ed78fb539a269216430eef3da -wrapdb_version = 1.6.43-2 +directory = libpng-1.6.44 +source_url = https://downloads.sourceforge.net/libpng/libpng-1.6.44.tar.xz +source_filename = libpng-1.6.44.tar.xz +source_hash = 60c4da1d5b7f0aa8d158da48e8f8afa9773c1c8baa5d21974df61f1886b8ce8e +patch_filename = libpng_1.6.44-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/libpng_1.6.44-1/get_patch +patch_hash = 394b07614c45fbd1beac8b660386216a490fe12f841a1a445799b676c9c892fb +wrapdb_version = 1.6.44-1 [provide] libpng = libpng_dep diff --git a/subprojects/openal-soft.wrap b/subprojects/openal-soft.wrap index 89aefc3d9..8701045bb 100644 --- a/subprojects/openal-soft.wrap +++ b/subprojects/openal-soft.wrap @@ -1,11 +1,10 @@ [wrap-file] -directory = openal-soft-1.22.2 -source_url = https://github.com/kcat/openal-soft/releases/download/1.22.2/openal-soft-1.22.2.tar.bz2 -source_filename = openal-soft-1.22.2.tar.bz2 -source_hash = ae94cc95cda76b7cc6e92e38c2531af82148e76d3d88ce996e2928a1ea7c3d20 -patch_url = https://skuller.net/meson/openal-soft_1.22.2-1_patch.zip -patch_filename = openal-soft_1.22.2-1_patch.zip -patch_hash = 8f0df33a04aec9fbac053476a9d68fd201bc74ed6ba62fce00138b71eb583db0 +directory = openal-soft-1.23.1 +source_url = https://github.com/kcat/openal-soft/releases/download/1.23.1/openal-soft-1.23.1.tar.bz2 +source_fallback_url = https://openal-soft.org/openal-releases/openal-soft-1.23.1.tar.bz2 +source_filename = openal-soft-1.23.1.tar.bz2 +source_hash = 796f4b89134c4e57270b7f0d755f0fa3435b90da437b745160a49bd41c845b21 +patch_directory = openal-soft [provide] openal = openal_dep diff --git a/subprojects/packagefiles/curl/meson.build b/subprojects/packagefiles/curl/meson.build new file mode 100644 index 000000000..919fd9c99 --- /dev/null +++ b/subprojects/packagefiles/curl/meson.build @@ -0,0 +1,232 @@ +project('libcurl', 'c', version: '8.10.1', license: 'bsd') + +src = [ + 'lib/vauth/cleartext.c', + 'lib/vauth/cram.c', + 'lib/vauth/digest.c', + 'lib/vauth/digest_sspi.c', + 'lib/vauth/gsasl.c', + 'lib/vauth/krb5_gssapi.c', + 'lib/vauth/krb5_sspi.c', + 'lib/vauth/ntlm.c', + 'lib/vauth/ntlm_sspi.c', + 'lib/vauth/oauth2.c', + 'lib/vauth/spnego_gssapi.c', + 'lib/vauth/spnego_sspi.c', + 'lib/vauth/vauth.c', + + 'lib/vtls/bearssl.c', + 'lib/vtls/gtls.c', + 'lib/vtls/hostcheck.c', + 'lib/vtls/keylog.c', + 'lib/vtls/mbedtls.c', + 'lib/vtls/mbedtls_threadlock.c', + 'lib/vtls/openssl.c', + 'lib/vtls/rustls.c', + 'lib/vtls/schannel.c', + 'lib/vtls/schannel_verify.c', + 'lib/vtls/sectransp.c', + 'lib/vtls/vtls.c', + 'lib/vtls/wolfssl.c', + 'lib/vtls/x509asn1.c', + + 'lib/vquic/curl_msh3.c', + 'lib/vquic/curl_ngtcp2.c', + 'lib/vquic/curl_osslq.c', + 'lib/vquic/curl_quiche.c', + 'lib/vquic/vquic.c', + 'lib/vquic/vquic-tls.c', + + 'lib/vssh/libssh.c', + 'lib/vssh/libssh2.c', + 'lib/vssh/wolfssh.c', + + 'lib/altsvc.c', + 'lib/amigaos.c', + 'lib/asyn-ares.c', + 'lib/asyn-thread.c', + 'lib/base64.c', + 'lib/bufq.c', + 'lib/bufref.c', + 'lib/c-hyper.c', + 'lib/cf-h1-proxy.c', + 'lib/cf-h2-proxy.c', + 'lib/cf-haproxy.c', + 'lib/cf-https-connect.c', + 'lib/cf-socket.c', + 'lib/cfilters.c', + 'lib/conncache.c', + 'lib/connect.c', + 'lib/content_encoding.c', + 'lib/cookie.c', + 'lib/curl_addrinfo.c', + 'lib/curl_des.c', + 'lib/curl_endian.c', + 'lib/curl_fnmatch.c', + 'lib/curl_get_line.c', + 'lib/curl_gethostname.c', + 'lib/curl_gssapi.c', + 'lib/curl_memrchr.c', + 'lib/curl_multibyte.c', + 'lib/curl_ntlm_core.c', + 'lib/curl_path.c', + 'lib/curl_range.c', + 'lib/curl_rtmp.c', + 'lib/curl_sasl.c', + 'lib/curl_sha512_256.c', + 'lib/curl_sspi.c', + 'lib/curl_threads.c', + 'lib/curl_trc.c', + 'lib/cw-out.c', + 'lib/dict.c', + 'lib/dllmain.c', + 'lib/doh.c', + 'lib/dynbuf.c', + 'lib/dynhds.c', + 'lib/easy.c', + 'lib/easygetopt.c', + 'lib/easyoptions.c', + 'lib/escape.c', + 'lib/file.c', + 'lib/fileinfo.c', + 'lib/fopen.c', + 'lib/formdata.c', + 'lib/ftp.c', + 'lib/ftplistparser.c', + 'lib/getenv.c', + 'lib/getinfo.c', + 'lib/gopher.c', + 'lib/hash.c', + 'lib/headers.c', + 'lib/hmac.c', + 'lib/hostasyn.c', + 'lib/hostip.c', + 'lib/hostip4.c', + 'lib/hostip6.c', + 'lib/hostsyn.c', + 'lib/hsts.c', + 'lib/http.c', + 'lib/http1.c', + 'lib/http2.c', + 'lib/http_aws_sigv4.c', + 'lib/http_chunks.c', + 'lib/http_digest.c', + 'lib/http_negotiate.c', + 'lib/http_ntlm.c', + 'lib/http_proxy.c', + 'lib/idn.c', + 'lib/if2ip.c', + 'lib/imap.c', + 'lib/inet_ntop.c', + 'lib/inet_pton.c', + 'lib/krb5.c', + 'lib/ldap.c', + 'lib/llist.c', + 'lib/macos.c', + 'lib/md4.c', + 'lib/md5.c', + 'lib/memdebug.c', + 'lib/mime.c', + 'lib/mprintf.c', + 'lib/mqtt.c', + 'lib/multi.c', + 'lib/netrc.c', + 'lib/nonblock.c', + 'lib/noproxy.c', + 'lib/openldap.c', + 'lib/parsedate.c', + 'lib/pingpong.c', + 'lib/pop3.c', + 'lib/progress.c', + 'lib/psl.c', + 'lib/rand.c', + 'lib/rename.c', + 'lib/request.c', + 'lib/rtsp.c', + 'lib/select.c', + 'lib/sendf.c', + 'lib/setopt.c', + 'lib/sha256.c', + 'lib/share.c', + 'lib/slist.c', + 'lib/smb.c', + 'lib/smtp.c', + 'lib/socketpair.c', + 'lib/socks.c', + 'lib/socks_gssapi.c', + 'lib/socks_sspi.c', + 'lib/speedcheck.c', + 'lib/splay.c', + 'lib/strcase.c', + 'lib/strdup.c', + 'lib/strerror.c', + 'lib/strtok.c', + 'lib/strtoofft.c', + 'lib/system_win32.c', + 'lib/telnet.c', + 'lib/tftp.c', + 'lib/timediff.c', + 'lib/timeval.c', + 'lib/transfer.c', + 'lib/url.c', + 'lib/urlapi.c', + 'lib/version.c', + 'lib/version_win32.c', + 'lib/warnless.c', + 'lib/ws.c', +] + +if host_machine.system() != 'windows' + error('Only Windows supported') +endif + +cc = meson.get_compiler('c') + +args = [ + '-DBUILDING_LIBCURL', + '-DUSE_SCHANNEL', + '-DUSE_WINDOWS_SSPI', + '-DUSE_IPV6', + '-DCURL_STATICLIB', + '-DHTTP_ONLY', + '-DCURL_DISABLE_CRYPTO_AUTH', +] + +if get_option('windows-xp-compat') + args += '-D_WIN32_WINNT=0x0501' +else + args += '-D_WIN32_WINNT=0x0601' +endif + +zlib = dependency('zlib', required: false) +if zlib.found() + args += '-DHAVE_LIBZ' +endif + +deps = [ + cc.find_library('advapi32'), + cc.find_library('bcrypt'), + cc.find_library('crypt32'), + cc.find_library('ws2_32'), + zlib, +] + +nghttp2 = dependency('libnghttp2', required: false) +if nghttp2.found() and cc.has_header_symbol('sspi.h', 'SecApplicationProtocolNegotiationExt_ALPN', + prefix: '#include ', args: ['-DSECURITY_WIN32=1']) + args += ['-DUSE_NGHTTP2', '-DHAS_ALPN=1'] + deps += nghttp2 +endif + +libcurl = static_library('curl', src, + c_args: args, + dependencies: deps, + include_directories: ['include', 'lib'], +) + +libcurl_dep = declare_dependency( + compile_args: ['-DCURL_STATICLIB'], + dependencies: deps, + include_directories: 'include', + link_with: libcurl, +) diff --git a/subprojects/packagefiles/curl/meson_options.txt b/subprojects/packagefiles/curl/meson_options.txt new file mode 100644 index 000000000..0d5cff144 --- /dev/null +++ b/subprojects/packagefiles/curl/meson_options.txt @@ -0,0 +1 @@ +option('windows-xp-compat', type: 'boolean', value: false, yield: true) diff --git a/subprojects/packagefiles/khr-headers.tar.xz b/subprojects/packagefiles/khr-headers.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..98bb44528dad5726d9329fce969c086ff54c39d0 GIT binary patch literal 111740 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 literal 0 HcmV?d00001 diff --git a/subprojects/packagefiles/openal-soft/meson.build b/subprojects/packagefiles/openal-soft/meson.build new file mode 100644 index 000000000..e33717f57 --- /dev/null +++ b/subprojects/packagefiles/openal-soft/meson.build @@ -0,0 +1,3 @@ +project('openal-soft', version: '1.23.1') + +openal_dep = declare_dependency(include_directories: 'include') From b60233c89f9ca5cd9ab30db69ac78f1a1df1521b Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 5 Oct 2024 14:37:19 -0400 Subject: [PATCH 844/974] Fixed ETV capture issue, client crash on GL_ColorBytePointer and GL_VertexPointer --- src/action/a_esp.c | 4 ++-- src/action/botlib/botlib_esp.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index c50f21717..c362178ab 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -412,7 +412,7 @@ void EspCapturePointThink( edict_t *flag ) // Escort point captured, end round and start again gi.sound( &g_edicts[0], CHAN_BODY | CHAN_NO_PHS_ADD, gi.soundindex("aqdt/aqg_bosswin.wav"), 1.0, ATTN_NONE, 0.0 ); - espsettings.escortcap = flag->owner->client->resp.team; + // espsettings.escortcap = flag->owner->client->resp.team; if (esp_punish->value) { esp_punishment_phase = true; EspPunishment(OtherTeam(flag->owner->client->resp.team)); @@ -532,7 +532,7 @@ void EspResetCapturePoint(void) edict_t *flag = NULL; // Reset escortcap value - espsettings.escortcap = false; + espsettings.escortcap = false; // Find the flag while ((ent = G_Find(ent, FOFS(classname), "item_flag")) != NULL) { diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index b982e3ad2..bb4f81639 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -18,7 +18,7 @@ int BOTLIB_ESPGetTargetNode(edict_t *ent, edict_t* leader) edict_t *target = NULL; // Reset escortcap value - espsettings.escortcap = false; + //espsettings.escortcap = false; // If leader is null, it means we're looking for an ETV target if(leader == NULL) { From b8b348409eddcf183e665f43e974750c5a5f6449 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 10:19:00 -0400 Subject: [PATCH 845/974] Refactored setting and determining espionage modes, removed atl/etv cvars --- src/action/a_esp.c | 110 +++++++++++++++------------------ src/action/a_esp.h | 31 +++++----- src/action/a_game.c | 10 +-- src/action/a_team.c | 24 +++---- src/action/botlib/botlib_esp.c | 20 +++--- src/action/g_chase.c | 2 +- src/action/g_spawn.c | 6 +- 7 files changed, 97 insertions(+), 106 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index c362178ab..8973ea28c 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -38,36 +38,16 @@ int esp_leader_pics[ TEAM_TOP ] = {0}; int esp_last_score = 0; qboolean esp_punishment_phase = false; - -/* -EspModeCheck - -Returns which mode we are running -*/ -int EspModeCheck(void) -{ - if (!esp->value) - return -1; // Espionage is disabled - if (atl->value) - return ESPMODE_ATL; // ATL mode - else if (etv->value) - return ESPMODE_ETV; // ETV mode - - return -1; // Espionage is disabled -} - /* Toggles between the two game modes */ -void EspForceEspionage(int espmode) +void EspForceEspionage(espmode_t espmode) { gi.cvar_forceset("esp", "1"); if (espmode == 0 || esp_atl->value) { - gi.cvar_forceset("atl", "1"); - gi.cvar_forceset("etv", "0"); + espsettings.esp_mode = ESPMODE_ATL; } else if (espmode == 1) { - gi.cvar_forceset("etv", "1"); - gi.cvar_forceset("atl", "0"); + espsettings.esp_mode = ESPMODE_ETV; } //gi.dprintf("Espionage mode set to %s\n", (espmode == 0) ? "ATL" : "ETV"); @@ -128,7 +108,7 @@ void _EspBonusDefendLeader(edict_t *targ, edict_t *attacker) edict_t *leader = NULL; // You can't defend a leader on a team that doesn't have one - if (attacker->client->resp.team == TEAM2 && etv->value) + if (attacker->client->resp.team == TEAM2 && espsettings.esp_mode == ESPMODE_ETV) return; if (IS_LEADER(targ)) @@ -165,7 +145,7 @@ void _EspBonusHarassLeader(edict_t *targ, edict_t *attacker) edict_t *leader = NULL; // You can't attack a leader on a team that doesn't have one - if (attacker->client->resp.team == TEAM1 && etv->value) + if (attacker->client->resp.team == TEAM1 && espsettings.esp_mode == ESPMODE_ETV) return; if (IS_LEADER(targ)) @@ -292,7 +272,7 @@ Espionage Defender Capture Point Bonus void _EspBonusDefendCapture(edict_t *targ, edict_t *attacker) { // Only ETV mode, and only for those on TEAM2 - if (!etv->value) + if (!(espsettings.esp_mode == ESPMODE_ETV)) return; if (attacker->client->resp.team != TEAM2) return; @@ -647,7 +627,7 @@ void EspEnforceDefaultSettings(char *defaulttype) gi.dprintf(" Red Team: %s -- Skin: %s\n", ESP_RED_TEAM, ESP_RED_SKIN); gi.dprintf(" Red Leader: %s -- Skin: %s\n", ESP_RED_LEADER_NAME, ESP_RED_LEADER_SKIN); gi.dprintf(" Blue Team: %s -- Skin: %s\n", ESP_BLUE_TEAM, ESP_BLUE_SKIN); - if(atl->value) + if(espsettings.esp_mode == ESPMODE_ATL) gi.dprintf(" Blue Leader: %s -- Skin: %s\n", ESP_BLUE_LEADER_NAME, ESP_BLUE_LEADER_SKIN); if(teamCount == 3){ gi.dprintf(" Green Team: %s -- Skin: %s\n", ESP_GREEN_TEAM, ESP_GREEN_SKIN); @@ -736,25 +716,29 @@ qboolean EspLoadConfig(const char *mapname) gi.dprintf("- Game type : %s\n", ESPMODE_ATL_NAME); EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; + espsettings.esp_mode = ESPMODE_ATL; } else { if(ptr) { if((strcmp(ptr, ESPMODE_ETV_SNAME) == 0 && esp_atl->value == 0)){ EspForceEspionage(ESPMODE_ETV); gametypename = ESPMODE_ETV_NAME; + espsettings.esp_mode = ESPMODE_ETV; } else { EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; + espsettings.esp_mode = ESPMODE_ATL; } } if (use_3teams->value) { // Only ATL available in 3 team mode EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; + espsettings.esp_mode = ESPMODE_ATL; } gi.dprintf("- Game type : %s\n", gametypename); } // Force ATL mode trying to get ETV mode going with 3teams - if (etv->value && use_3teams->value){ + if (espsettings.esp_mode == ESPMODE_ETV && use_3teams->value){ gi.dprintf("%s and use_3team are incompatible, defaulting to %s", ESPMODE_ETV_NAME, ESPMODE_ATL_NAME); EspForceEspionage(ESPMODE_ATL); } @@ -791,7 +775,7 @@ qboolean EspLoadConfig(const char *mapname) } // Only set the flag if the scenario is ETV - if(etv->value) { + if(espsettings.esp_mode == ESPMODE_ETV) { gi.dprintf("- Target\n"); ptr = INI_Find(fh, "target", "escort"); if(ptr) { @@ -970,13 +954,13 @@ qboolean EspLoadConfig(const char *mapname) Q_snprintf(teams[TEAM2].skin_index, sizeof(teams[TEAM2].skin_index), "../players/%s_i", teams[TEAM2].skin); Q_snprintf(teams[TEAM3].skin_index, sizeof(teams[TEAM3].skin_index), "../players/%s_i", teams[TEAM3].skin); Q_snprintf(teams[TEAM1].leader_skin_index, sizeof(teams[TEAM1].leader_skin_index), "../players/%s_i", teams[TEAM1].leader_skin); - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { Q_snprintf(teams[TEAM2].leader_skin_index, sizeof(teams[TEAM2].leader_skin_index), "../players/%s_i", teams[TEAM2].leader_skin); Q_snprintf(teams[TEAM3].leader_skin_index, sizeof(teams[TEAM3].leader_skin_index), "../players/%s_i", teams[TEAM3].leader_skin); } } - if((etv->value) && teamCount == 3){ + if((espsettings.esp_mode == ESPMODE_ETV) && teamCount == 3){ gi.dprintf("Warning: ETV mode requested with use_3teams enabled, forcing ATL mode"); EspForceEspionage(ESPMODE_ATL); } @@ -1072,13 +1056,13 @@ void EspRespawnPlayer(edict_t *ent) if (level.framenum > ent->client->respawn_framenum) { if (esp_debug->value) { - gi.dprintf("Is it ETV mode? %f\n", etv->value); + gi.dprintf("Which mode are we in? ATL:0, ETV:1 :: Mode: %d\n", espsettings.esp_mode); gi.dprintf("Is team 1 leader alive? %d\n", IS_ALIVE(teams[TEAM1].leader)); gi.dprintf("Is team 1's leader NULL? %d\n", teams[TEAM1].leader == NULL); } // Only respawn if leader(s) are still alive and the round is still going - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (teams[ent->client->resp.team].leader != NULL && IS_ALIVE(teams[ent->client->resp.team].leader)) { gi.centerprintf(ent, "ACTION!"); unicastSound(ent, gi.soundindex("atl/action.wav"), 1.0); @@ -1087,7 +1071,7 @@ void EspRespawnPlayer(edict_t *ent) respawn(ent); } - } else if (etv->value) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { if (teams[TEAM1].leader != NULL && IS_ALIVE(teams[TEAM1].leader)) { gi.centerprintf(ent, "ACTION!"); unicastSound(ent, gi.soundindex("atl/action.wav"), 1.0); @@ -1105,17 +1089,22 @@ Internally used only, spot check if leader is alive depending on the game mode Please run a NULL check on the leader before calling this */ -qboolean _EspLeaderAliveCheck(edict_t *ent, edict_t *leader, int espmode) +qboolean _EspLeaderAliveCheck(edict_t *ent, edict_t *leader) { - if (espmode < 0) { - gi.dprintf("Warning: Invalid espmode returned from EspModeCheck()\n"); + if(!leader){ + gi.dprintf("%s: Warning: Leader is NULL\n", __func__); + return false; + } + + if (espsettings.esp_mode < 0) { + gi.dprintf("%s: Warning: Invalid espsettings.esp_mode returned\n", __func__); return false; } if (!leader) return false; - if (espmode == ESPMODE_ATL) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (teams[leader->client->resp.team].leader && IS_ALIVE(teams[leader->client->resp.team].leader)) return true; @@ -1123,7 +1112,7 @@ qboolean _EspLeaderAliveCheck(edict_t *ent, edict_t *leader, int espmode) return false; } - if (espmode == ESPMODE_ETV) { + if (espsettings.esp_mode == ESPMODE_ETV) { if (ent->client->resp.team == TEAM1 && IS_ALIVE(teams[TEAM1].leader)) return true; @@ -1205,7 +1194,8 @@ edict_t *EspRespawnOnLeader(edict_t *ent, char *cname) spawn->nextthink = level.framenum + 1; //ED_CallSpawn(spawn); - //gi.dprintf("Respawn coordinates are %f %f %f %f\n", teamLeader->s.origin[0], teamLeader->s.origin[1], teamLeader->s.origin[2], teamLeader->s.angles[YAW]); + if (esp_debug->value) + gi.dprintf("%s: %s on team %d respawn coordinates are %f %f %f %f\n", __func__, ent->client->pers.netname, ent->client->resp.team, teamLeader->s.origin[0], teamLeader->s.origin[1], teamLeader->s.origin[2], teamLeader->s.angles[YAW]); return spawn; } @@ -1259,11 +1249,11 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) if (esp_debug->value){ gi.dprintf("%s: Is team round going? %d\n", __func__, team_round_going); - gi.dprintf("%s: Is the team leader alive? %d\n", __func__, _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())); + gi.dprintf("%s: Is the team leader alive? %d\n", __func__, _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader)); } // Time to respawn on the leader! - if (team_round_going && _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader, EspModeCheck())) { + if (team_round_going && _EspLeaderAliveCheck(ent, teams[ent->client->resp.team].leader)) { return EspRespawnOnLeader(ent, cname); } else { // Custom spawns take precedence over standard spawns @@ -1294,7 +1284,7 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) } // All else fails, use deathmatch spawn points if (esp_debug->value) - gi.dprintf("%s: Defaulted all the way down here\n", __func__); + gi.dprintf("%s: Defaulted all the way to using SelectFarthestDeathmatchSpawnPoint\n", __func__); return SelectFarthestDeathmatchSpawnPoint(); } @@ -1304,9 +1294,9 @@ void SetEspStats( edict_t *ent ) // GHUD team icons, ATL gets leader skin indexes, ETV gets team skin indexes for(i = TEAM1; i <= teamCount; i++) - if (etv->value) + if (espsettings.esp_mode == ESPMODE_ETV) level.pic_teamskin[i] = gi.imageindex(teams[i].skin_index); - else if (atl->value) + else if (espsettings.esp_mode == ESPMODE_ATL) level.pic_teamskin[i] = gi.imageindex(teams[i].leader_skin_index); // Load scoreboard images @@ -1320,7 +1310,7 @@ void SetEspStats( edict_t *ent ) level.pic_esp_leadericon[TEAM2] = gi.imageindex(teams[TEAM2].leader_skin_index); gi.imageindex("sbfctf2"); - if (atl->value && teamCount == 3) { + if (espsettings.esp_mode == ESPMODE_ATL && teamCount == 3) { level.pic_esp_teamtag[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); level.pic_esp_teamicon[TEAM3] = gi.imageindex(teams[TEAM3].skin_index); level.pic_esp_leadericon[TEAM3] = gi.imageindex(teams[TEAM3].leader_skin_index); @@ -1456,7 +1446,7 @@ qboolean EspCheckETVRules(void) gi.dprintf("Team 1 + Team 2 score is %d\n", t1 + t2); gi.dprintf("Roundlimit warning is %d\n", roundlimitwarn); gi.dprintf("Halftime is %d\n", espsettings.halftime); - gi.dprintf("ETV is %f\n", etv->value); + gi.dprintf("ETV is %d\n", espsettings.esp_mode); gi.dprintf("Use warnings is %f\n", use_warnings->value); gi.dprintf("ETV halftime is %f\n", esp_etv_halftime->value); gi.dprintf("-- Debug end %s --\n", __func__); @@ -1497,7 +1487,7 @@ qboolean EspCheckETVRules(void) qboolean EspCheckRules(void) { // Expand if we introduce other Espionage modes - if (etv->value) + if (espsettings.esp_mode == ESPMODE_ETV) return EspCheckETVRules(); return false; @@ -1565,11 +1555,13 @@ qboolean AllTeamsHaveLeaders(void) } // Only Team 1 needs a leader in ETV mode - if((etv->value) && HAVE_LEADER(TEAM1)) { - //gi.dprintf("ETV team has a leader\n"); + if((espsettings.esp_mode == ESPMODE_ETV) && HAVE_LEADER(TEAM1)) { + if (esp_debug->value) + gi.dprintf("ETV team has a leader\n"); return true; - } else if(atl->value && (teamsWithLeaders == teamCount)){ - //gi.dprintf("Teams with leaders is the same as the team count\n"); + } else if(espsettings.esp_mode == ESPMODE_ATL && (teamsWithLeaders == teamCount)){ + if (esp_debug->value) + gi.dprintf("Teams with leaders is the same as the team count\n"); return true; } else { return false; @@ -1603,7 +1595,7 @@ qboolean EspSetLeader(int teamNum, edict_t *ent) return false; } - if (etv->value && teamNum != TEAM1) { + if (espsettings.esp_mode == ESPMODE_ETV && teamNum != TEAM1) { gi.cprintf(ent, PRINT_MEDIUM, "** Only the Red team (team 1) has a leader in ETV mode **\n"); return false; } @@ -1851,14 +1843,14 @@ int EspReportLeaderDeath(edict_t *ent) teams[dead_leader_team].leader_dead = true; // This checks if leader was on TEAM 1 in ETV mode - if (etv->value) { + if (espsettings.esp_mode == ESPMODE_ETV) { if (dead_leader_team == TEAM1) { winner = TEAM2; } } // ATL mode - 2 team winner checks - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (teamCount == 2) { if (dead_leader_team == TEAM1) winner = TEAM2; @@ -1984,9 +1976,9 @@ void EspAnnounceDetails( qboolean timewarning ) ent = g_edicts + 1 + i; if (!ent->inuse || ent->is_bot || ent->client->resp.team == NOTEAM) continue; - if (atl->value){ + if (espsettings.esp_mode == ESPMODE_ATL){ CenterPrintAll("You're running low on time! Kill the enemy leader!\n"); - } else if (etv->value){ + } else if (espsettings.esp_mode == ESPMODE_ETV){ CenterPrintTeam(TEAM1, "Capture that briefcase or the other team wins!\n"); CenterPrintTeam(TEAM2, "Keep it up! If they can't cap, they can't win!\n"); } @@ -2005,9 +1997,9 @@ void EspAnnounceDetails( qboolean timewarning ) gi.cprintf(ent, PRINT_MEDIUM, "Take cover, you're the leader!\n"); } if (!IS_LEADER(ent)) { - if (atl->value){ + if (espsettings.esp_mode == ESPMODE_ATL){ gi.cprintf(ent, PRINT_MEDIUM, "Defend your leader and attack the other one to win!\n"); - } else if (etv->value){ + } else if (espsettings.esp_mode == ESPMODE_ETV){ if (ent->client->resp.team == TEAM1) gi.cprintf(ent, PRINT_HIGH, "Escort your leader to the briefcase!\n"); else diff --git a/src/action/a_esp.h b/src/action/a_esp.h index 493ef4a93..501e32d42 100644 --- a/src/action/a_esp.h +++ b/src/action/a_esp.h @@ -9,17 +9,6 @@ extern cvar_t *etv; #define HAVE_LEADER(teamNum) (teams[(teamNum)].leader) #define MAX_ESP_STRLEN 32 -// Game modes -#define ESPMODE_ATL 0 -#define ESPMODE_ETV 1 - -#define ESPMODE_ATL_NAME "Assassinate the Leader" -#define ESPMODE_ETV_NAME "Escort the VIP" - -#define ESPMODE_ATL_SNAME "atl" -#define ESPMODE_ETV_SNAME "etv" - - // Game default settings #define ESP_DEFAULT_RESPAWN_TIME 10 #define ESP_RED_SKIN "male/ctf_r" @@ -45,13 +34,26 @@ typedef enum { ESP_STATE_START, ESP_STATE_PLAYING -} -espstate_t; +} espstate_t; + +typedef enum +{ + ESPMODE_ATL, + ESPMODE_ETV, + ESPMODE_MAX +} espmode_t; + +#define ESPMODE_ATL_NAME "Assassinate the Leader" +#define ESPMODE_ETV_NAME "Escort the VIP" + +#define ESPMODE_ATL_SNAME "atl" +#define ESPMODE_ETV_SNAME "etv" typedef struct espsettings_s { char author[MAX_ESP_STRLEN*3]; char name[MAX_ESP_STRLEN]; + espmode_t esp_mode; edict_t *custom_spawns[TEAM_TOP][MAX_SPAWNS]; qboolean custom_skins; int halftime; @@ -95,9 +97,8 @@ typedef enum { #define ESP_ATTACKER_HARASS_RADIUS 400 // the radius around an object being defended where an attacker will get extra frags when assaulting the leader #define ESP_BONUS_COOLDOWN 500 // the number of frames after a bonus is awarded before another bonus can be awarded -int EspModeCheck(void); edict_t* EspGetLeader(int teamNum); -void EspForceEspionage(int espmode); +void EspForceEspionage(espmode_t espmode); void EspSetTeamSpawns(int, char *); int EspGetRespawnTime(edict_t *ent); diff --git a/src/action/a_game.c b/src/action/a_game.c index e44bbb6b1..bd395dd22 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -256,10 +256,10 @@ qboolean PrintGameMessage(edict_t *ent) */ if (esp->value) { if (!team_round_going && !AllTeamsHaveLeaders()) { - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for each team to have a leader\nType 'leader' in console to volunteer for duty.\n"); msg_ready = true; - } else if (etv->value) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { if (ent->client->resp.team == TEAM1) Q_snprintf(msg_buf, sizeof(msg_buf), "Your team needs a leader!\nType 'leader' in console to volunteer for duty."); else @@ -445,9 +445,9 @@ void PrintMOTD(edict_t * ent) strcat(msg_buf, "\n"); lines++; - if(atl->value) + if(espsettings.esp_mode == ESPMODE_ATL) sprintf(msg_buf + strlen(msg_buf), "Espionage Mode: Assassinate the Leader\n"); - else if(etv->value) + else if(espsettings.esp_mode == ESPMODE_ETV) sprintf(msg_buf + strlen(msg_buf), "Espionage Mode: Escort the VIP\n"); else strcat(msg_buf, "\n"); @@ -537,7 +537,7 @@ void PrintMOTD(edict_t * ent) strcat(msg_buf, " Roundtimelimit: none\n"); } - if (esp->value && etv->value) { + if (esp->value && espsettings.esp_mode == ESPMODE_ETV) { if ((int) capturelimit->value) // What is the capturelimit? sprintf(msg_buf + strlen(msg_buf), " Capturelimit: %d\n", (int) capturelimit->value); else diff --git a/src/action/a_team.c b/src/action/a_team.c index 49d3da224..0824dfbf3 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -431,7 +431,7 @@ char* PrintMatchRules(void) // Espionage rules if (esp->value) { int rndlimit = (int)roundlimit->value; - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (!teams[TEAM1].leader && !teams[TEAM2].leader && !teams[TEAM3].leader) { Q_snprintf( rulesmsg, sizeof( rulesmsg ), "Frag the other team's leader to win, but don't forget to protect your own!\n"); } @@ -450,7 +450,7 @@ char* PrintMatchRules(void) Q_snprintf(addmsg, sizeof(addmsg), "\n** The first team to %i points wins! **", (int)rndlimit); Q_strncatz( rulesmsg, addmsg, sizeof( rulesmsg ) ); - } else if (etv->value) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { if (!teams[TEAM1].leader) Q_snprintf( rulesmsg, sizeof( rulesmsg ), "\nTeam 1 is trying to escort their leader to the briefcase!\n\nTeam 2 is tasked with preventing that from happening!\n"); else if (teams[TEAM1].leader) { @@ -1259,13 +1259,13 @@ void AssignSkin (edict_t * ent, const char *s, qboolean nickChanged) break; case TEAM2: Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM2].skin); - if ((atl->value) && IS_LEADER(ent)){ + if ((espsettings.esp_mode == ESPMODE_ATL) && IS_LEADER(ent)){ Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM2].leader_skin); } break; case TEAM3: Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM3].skin); - if ((atl->value) && IS_LEADER(ent)){ + if ((espsettings.esp_mode == ESPMODE_ATL) && IS_LEADER(ent)){ Q_snprintf(skin, sizeof(skin), "%s\\%s", ent->client->pers.netname, teams[TEAM3].leader_skin); } break; @@ -2091,7 +2091,7 @@ int CheckForWinner(void) return WINNER_NONE; if (esp->value){ - if (atl->value) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (teamCount == TEAM2) { if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead) { return WINNER_TIE; @@ -2111,7 +2111,7 @@ int CheckForWinner(void) return TEAM1; } } - } else if (etv->value) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { // Check if this value is true, which means the escorting team wins // By default it is false if (espsettings.escortcap == true) { @@ -2147,7 +2147,7 @@ int CheckForForcedWinner(void) not captured the point, the defending team wins */ if (esp->value){ - if (atl->value){ + if (espsettings.esp_mode == ESPMODE_ATL){ if (teamCount == TEAM2){ if (IS_ALIVE(teams[TEAM1].leader) && IS_ALIVE(teams[TEAM2].leader)){ @@ -2160,7 +2160,7 @@ int CheckForForcedWinner(void) return WINNER_TIE; } } - } else if (etv->value){ + } else if (espsettings.esp_mode == ESPMODE_ETV){ if (espsettings.escortcap == false){ return TEAM2; } @@ -2463,7 +2463,7 @@ void MakeAllLivePlayersObservers (void) CTFResetFlags(); // Reset Espionage flag - if (esp->value && etv->value) + if (esp->value && espsettings.esp_mode == ESPMODE_ETV) EspResetCapturePoint(); for (i = 0; i < game.maxclients; i++) @@ -2835,9 +2835,9 @@ int CheckTeamRules (void) else { if (esp->value && !AllTeamsHaveLeaders()) { - if (atl->value){ + if (espsettings.esp_mode == ESPMODE_ATL){ CenterPrintAll ("All Teams Must Have a Leader!\nType 'leader' in console to volunteer!"); - } else if (etv->value) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { CenterPrintTeam(TEAM1, "Team 1 Must Have a Leader!\nType 'leader' in console to volunteer!"); CenterPrintTeam(TEAM2, "Waiting on Team 1 to select a leader..."); } @@ -2885,7 +2885,7 @@ int CheckTeamRules (void) } // Move spectators to the capturepoint - if (esp->value && etv->value) { + if (esp->value && espsettings.esp_mode == ESPMODE_ETV) { // find the capturepoint edict_t *capturepoint; edict_t *ent; diff --git a/src/action/botlib/botlib_esp.c b/src/action/botlib/botlib_esp.c index bb4f81639..de3b6303a 100644 --- a/src/action/botlib/botlib_esp.c +++ b/src/action/botlib/botlib_esp.c @@ -267,10 +267,9 @@ int BOTLIB_InterceptLeader_3Team(edict_t* self) int BOTLIB_InterceptLeader_2Team(edict_t* self) { int myTeam = self->client->resp.team; - int espMode = EspModeCheck(); edict_t* leader; - if (espMode == ESPMODE_ATL) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (myTeam == TEAM1) { leader = EspGetLeader(TEAM2); if (leader != NULL && IS_ALIVE(leader)) { @@ -282,7 +281,7 @@ int BOTLIB_InterceptLeader_2Team(edict_t* self) return BOTLIB_FindEnemyLeaderNode(self, TEAM1); } } - } else if (espMode == ESPMODE_ETV) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { if (myTeam == TEAM2) { leader = EspGetLeader(TEAM1); if (leader != NULL && IS_ALIVE(leader)) { @@ -303,13 +302,13 @@ int BOTLIB_InterceptLeader_ETV(edict_t* self) // Returns the node nearest to the leader int BOTLIB_InterceptEnemyLeader(edict_t* self) { - if (EspModeCheck() == ESPMODE_ATL) { + if (espsettings.esp_mode == ESPMODE_ATL) { if (use_3teams->value) { return BOTLIB_InterceptLeader_3Team(self); } else { return BOTLIB_InterceptLeader_2Team(self); } - } else if (EspModeCheck() == ESPMODE_ETV) { + } else if (espsettings.esp_mode == ESPMODE_ETV) { return BOTLIB_InterceptLeader_ETV(self); } else { return INVALID; @@ -466,7 +465,6 @@ void BOTLIB_ESP_Goals(edict_t* self) { if (!lights_camera_action && !espsettings.esp_live_round) return; // Only allow during a real match (after LCA and before win/loss announcement) - int espMode = EspModeCheck(); // Team related variables int myTeam = self->client->resp.team; int totalTeammates = TotalPlayersOnTeam(myTeam); @@ -491,7 +489,7 @@ void BOTLIB_ESP_Goals(edict_t* self) // Logic splits here, between ATL and ETV modes bot_leader_think: - if (espMode == ESPMODE_ATL) { + if (espsettings.esp_mode == ESPMODE_ATL) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the enemy leader @@ -531,7 +529,7 @@ void BOTLIB_ESP_Goals(edict_t* self) } } } - } else if (espMode == ESPMODE_ETV && myTeam == TEAM1) { + } else if (espsettings.esp_mode == ESPMODE_ETV && myTeam == TEAM1) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the ETV target @@ -564,7 +562,7 @@ void BOTLIB_ESP_Goals(edict_t* self) // Main difference between crew and leader is that the crew are // expendable, protect the leader at all costs! bot_crew_think: - if (espMode == ESPMODE_ATL) { + if (espsettings.esp_mode == ESPMODE_ATL) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to stay with team leader @@ -598,7 +596,7 @@ void BOTLIB_ESP_Goals(edict_t* self) return; } } - } else if (espMode == ESPMODE_ETV && myTeam == TEAM1) { + } else if (espsettings.esp_mode == ESPMODE_ETV && myTeam == TEAM1) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the ETV target @@ -623,7 +621,7 @@ void BOTLIB_ESP_Goals(edict_t* self) return; } } - } else if (espMode == ESPMODE_ETV && myTeam == TEAM2) { + } else if (espsettings.esp_mode == ESPMODE_ETV && myTeam == TEAM2) { // If percentage of teammates alive is < 50% then : if (percentAlive < 50) { // 50% chance to go for the ETV target diff --git a/src/action/g_chase.c b/src/action/g_chase.c index 7d808e439..b8aeee3bd 100644 --- a/src/action/g_chase.c +++ b/src/action/g_chase.c @@ -426,7 +426,7 @@ void EspionageChaseCam(edict_t *self, edict_t *attacker) int player_team = self->client->resp.team; // ATL is simple, chase your own leader - if (atl->value && team_leader != NULL) { + if (espsettings.esp_mode == ESPMODE_ATL && team_leader != NULL) { if (IS_ALIVE(team_leader)) last_target = team_leader; // Chase cam your own leader else diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 4b4f58b35..03dd591be 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1060,9 +1060,9 @@ int Gamemode(void) gamemode = GM_DOMINATION; } else if (deathmatch->value) { gamemode = GM_DEATHMATCH; - } else if (esp->value && atl->value) { + } else if (esp->value && espsettings.esp_mode == ESPMODE_ATL) { gamemode = GM_ASSASSINATE_THE_LEADER; - } else if (esp->value && etv->value) { + } else if (esp->value && espsettings.esp_mode == ESPMODE_ETV) { gamemode = GM_ESCORT_THE_VIP; } return gamemode; @@ -1243,7 +1243,7 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.cvar_forceset(teamplay->name, "1"); } // ETV mode doesn't support 3 teams, but ATL does - if (etv->value && use_3teams->value) { + if (espsettings.esp_mode == ESPMODE_ETV && use_3teams->value) { gi.dprintf ("Espionage ETV Enabled - Incompatible with 3 Teams, reverting to ATL mode\n"); EspForceEspionage(ESPMODE_ATL); } From c04b4b94317658465e22e14fe9c18d68870f8d4f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 10:28:44 -0400 Subject: [PATCH 846/974] Removed redundant settings --- src/action/a_esp.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 8973ea28c..4761d6adf 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -709,6 +709,7 @@ qboolean EspLoadConfig(const char *mapname) Q_strncpyz(espsettings.name, ptr, sizeof(espsettings.name)); } + // This is where we're setting which Espionage mode we want ptr = INI_Find(fh, "esp", "type"); char *gametypename = ESPMODE_ATL_NAME; if(!strcmp(ptr, ESPMODE_ATL_SNAME) && !strcmp(ptr, ESPMODE_ETV_SNAME)){ @@ -716,24 +717,20 @@ qboolean EspLoadConfig(const char *mapname) gi.dprintf("- Game type : %s\n", ESPMODE_ATL_NAME); EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; - espsettings.esp_mode = ESPMODE_ATL; } else { if(ptr) { if((strcmp(ptr, ESPMODE_ETV_SNAME) == 0 && esp_atl->value == 0)){ EspForceEspionage(ESPMODE_ETV); gametypename = ESPMODE_ETV_NAME; - espsettings.esp_mode = ESPMODE_ETV; } else { EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; - espsettings.esp_mode = ESPMODE_ATL; } } if (use_3teams->value) { // Only ATL available in 3 team mode EspForceEspionage(ESPMODE_ATL); gametypename = ESPMODE_ATL_NAME; - espsettings.esp_mode = ESPMODE_ATL; } gi.dprintf("- Game type : %s\n", gametypename); } @@ -743,6 +740,8 @@ qboolean EspLoadConfig(const char *mapname) EspForceEspionage(ESPMODE_ATL); } + // End of Espionage game mode forced settings + gi.dprintf("- Respawn times\n"); char *r_respawn_time, *b_respawn_time, *g_respawn_time; From c00920a6c5e2f88a29ea327af6170c5c1f5d764f Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 11:06:14 -0400 Subject: [PATCH 847/974] Added TestEntityPosition check on leader respawns --- src/action/a_esp.c | 34 ++++++++++++++++++---------------- src/action/g_local.h | 1 + 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 4761d6adf..9ce62c714 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1090,11 +1090,6 @@ Please run a NULL check on the leader before calling this */ qboolean _EspLeaderAliveCheck(edict_t *ent, edict_t *leader) { - if(!leader){ - gi.dprintf("%s: Warning: Leader is NULL\n", __func__); - return false; - } - if (espsettings.esp_mode < 0) { gi.dprintf("%s: Warning: Invalid espsettings.esp_mode returned\n", __func__); return false; @@ -1184,19 +1179,26 @@ edict_t *EspRespawnOnLeader(edict_t *ent, char *cname) teamLeader = teams[ent->client->resp.team].leader; VectorCopy(teamLeader->s.origin, respawn_coords); - spawn = G_Spawn(); - respawn_coords[2] += 9; // So they don't spawn in the floor - VectorCopy(respawn_coords, spawn->s.origin); - spawn->s.angles[YAW] = teamLeader->s.angles[YAW]; // Facing the same direction as the leader on respawn - spawn->classname = ED_NewString(cname); - spawn->think = G_FreeEdict; - spawn->nextthink = level.framenum + 1; - //ED_CallSpawn(spawn); + // Perform a trace to check if the spawn point is within a solid brush + if (SV_TestEntityPosition(teamLeader)) { + gi.dprintf("%s: %s: Team %d leader's spawn point was inside a solid brush, defaulting to original spawnpoint\n", __func__, ent->client->pers.netname, ent->client->resp.team); + gi.cprintf(ent, PRINT_HIGH, "Your Leader is in a tight spot, you're unable to spawn near them!\n"); + return chosenSpawnpoint[ent->client->resp.team]; + } else { + spawn = G_Spawn(); + respawn_coords[2] += 9; // So they don't spawn in the floor + VectorCopy(respawn_coords, spawn->s.origin); + spawn->s.angles[YAW] = teamLeader->s.angles[YAW]; // Facing the same direction as the leader on respawn + spawn->classname = ED_NewString(cname); + spawn->think = G_FreeEdict; + spawn->nextthink = level.framenum + 1; + //ED_CallSpawn(spawn); - if (esp_debug->value) - gi.dprintf("%s: %s on team %d respawn coordinates are %f %f %f %f\n", __func__, ent->client->pers.netname, ent->client->resp.team, teamLeader->s.origin[0], teamLeader->s.origin[1], teamLeader->s.origin[2], teamLeader->s.angles[YAW]); + if (esp_debug->value) + gi.dprintf("%s: %s on team %d respawn coordinates are %f %f %f %f\n", __func__, ent->client->pers.netname, ent->client->resp.team, teamLeader->s.origin[0], teamLeader->s.origin[1], teamLeader->s.origin[2], teamLeader->s.angles[YAW]); - return spawn; + return spawn; + } } edict_t *SelectEspSpawnPoint(edict_t *ent) diff --git a/src/action/g_local.h b/src/action/g_local.h index 32a3e49f6..58324fc61 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1635,6 +1635,7 @@ void PlayerNoise (edict_t * who, vec3_t where, int type); // g_phys.c // void G_RunEntity (edict_t * ent); +edict_t *SV_TestEntityPosition (edict_t * ent); // // g_main.c From 59e3668f61ed991393c6c4a9f3c5eadfa97c92e8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:09:24 -0400 Subject: [PATCH 848/974] Needed to rename VectorRotate because it was reintroduced..? --- inc/shared/shared.h | 2 +- src/action/a_doorkick.c | 4 ++-- src/action/acesrc/acebot.h | 5 +++-- src/action/acesrc/acebot_cmds.c | 5 +++++ src/action/botlib/botlib_spawn.c | 4 ++++ src/action/botlib/botlib_utils.c | 2 +- src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_spawn.c | 18 +++++++++++------- 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/inc/shared/shared.h b/inc/shared/shared.h index ec39e80bc..e2e453c11 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -528,7 +528,7 @@ void PerpendicularVector (vec3_t dst, const vec3_t src); void RotatePointAroundVector (vec3_t dst, const vec3_t dir, const vec3_t point, float degrees); -void VectorRotate( vec3_t in, vec3_t angles, vec3_t out ); // a_doorkick.c +//void VectorRotate( vec3_t in, vec3_t angles, vec3_t out ); // a_doorkick.c void VectorRotate2( vec3_t v, float degrees ); void Q_srand(uint32_t seed); diff --git a/src/action/a_doorkick.c b/src/action/a_doorkick.c index b848ddb2b..2648786cd 100644 --- a/src/action/a_doorkick.c +++ b/src/action/a_doorkick.c @@ -30,7 +30,7 @@ extern void door_use(edict_t * self, edict_t * other, edict_t * activator); // needed for KickDoor -void VectorRotate(vec3_t in, vec3_t angles, vec3_t out) +void VectorRotate3(vec3_t in, vec3_t angles, vec3_t out) { float cv, sv, angle, tv; @@ -102,7 +102,7 @@ int KickDoor(trace_t * tr_old, edict_t * ent, vec3_t forward) VectorNormalize(forward); VectorNormalize(d_forward); VectorSet(right, 0, 90, 0); - VectorRotate(d_forward, right, d_forward); + VectorRotate3(d_forward, right, d_forward); d = DotProduct(forward, d_forward); if (tr.ent->spawnflags & DOOR_REVERSE) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 6a2abe14d..d2ac9ba99 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -118,10 +118,11 @@ typedef enum //rekkie -- BSP -- e #define MAX_BOTSKILL 10 +extern cvar_t *bot_enable; // Enable/disable bots toggle extern cvar_t *bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! -extern cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit +extern cvar_t *bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit extern cvar_t *bot_remember; // How long (in seconds) the bot remembers an enemy after visibility has been lost -extern cvar_t* bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight +extern cvar_t *bot_reaction; // How long (in seconds) until the bot reacts to an enemy in sight extern cvar_t *bot_showpath; // Show bot paths //AQ2 ADD diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 54cb68997..5d21aeaab 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -44,6 +44,11 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); + if (!bot_enable->value) { + gi.dprintf("bot_enable is 0; Bots are disabled\n"); + return true; + } + //if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) // ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index a84f4d518..4ef693826 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2140,6 +2140,10 @@ void BOTLIB_DMBotCountManager(void) void BOTLIB_CheckBotRules(void) { + // Disable bot logic entirely + if (!bot_enable->value) + return; + if (matchmode->value) // Bots never allowed in matchmode return; diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 707a714c2..15691d5a7 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -117,5 +117,5 @@ void BOTLIB_Debug(const char *debugmsg, ...) { if (!bot_debug->value) return; - gi.dprintf(debugmsg); + gi.dprintf("%s", debugmsg); } diff --git a/src/action/g_main.c b/src/action/g_main.c index 9325ee570..e1a890a57 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -483,6 +483,7 @@ cvar_t *ltk_routing; cvar_t *ltk_botfile; cvar_t *ltk_loadbots; //rekkie -- DEV_1 -- s +cvar_t* bot_enable; // Enable/Disable bots cvar_t* bot_showpath; cvar_t* bot_skill; // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! cvar_t* bot_skill_threshold; // Dynamic skill adjustment kicks in if a threshold has been hit diff --git a/src/action/g_save.c b/src/action/g_save.c index ab58b11ee..9753cf3f8 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -681,6 +681,7 @@ void InitGame( void ) ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); //rekkie -- DEV_1 -- s + bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH); bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 4b4f58b35..4b1ad89b2 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1576,12 +1576,14 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn //rekkie -- s #ifndef NO_BOTS - BOTLIB_InitNavigation(NULL); -#ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); -#else - BOTLIB_LoadNav(); -#endif + if (bot_enable->value) { + BOTLIB_InitNavigation(NULL); + #ifdef USE_ZLIB + BOTLIB_LoadNavCompressed(); + #else + BOTLIB_LoadNav(); + + #endif //ACEND_LoadAAS(false); // This will also generate AAS if it doesn't exist //ACEND_BSP(NULL); @@ -1593,11 +1595,13 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn memset(&botlib_noises, 0, sizeof(botlib_noises)); //rekkie -- Fake Bot Client -- s - gi.SV_BotClearClients(); // So the server can clear all fake bot clients + gi.SV_BotClearClients(); + //gi.SV_BotClearClients(); // So the server can clear all fake bot clients //rekkie -- Fake Bot Client -- e if(bot_personality->value) BOTLIB_PersonalityFile(); + } #endif //rekkie -- e } From feb25fdb4c89fec43ddcef23a460103764d5efb8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:23:15 -0400 Subject: [PATCH 849/974] Returning if bots are disabled --- src/action/acesrc/acebot_cmds.c | 1 - src/action/botlib/botlib_cmd.c | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 5d21aeaab..65aaefe37 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -45,7 +45,6 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); if (!bot_enable->value) { - gi.dprintf("bot_enable is 0; Bots are disabled\n"); return true; } diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 3aac838d5..a35b0ac7b 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -10,6 +10,12 @@ qboolean BOTLIB_SV_Cmds(void) if (Q_stricmp(cmd, "bots") == 0) { + if (!bot_enable->value) { + gi.dprintf("%s: bot_enable is 0; Bots are disabled, desired bots: %d\n", __func__, bot_connections.desire_bots); + bot_connections.desire_bots = 0; + return true; + } + int cc = gi.argc(); gi.cvar_set("bot_maxteam", va("%d", 0)); // Override if bots manually added @@ -264,6 +270,7 @@ qboolean BOTLIB_Commands(edict_t* ent) // ACEND_BSP(ent); // return true; // } + if (Q_stricmp(cmd, "randomize_team_names") == 0) // Manually randomize team names { BOTLIB_RandomizeTeamNames(ent); @@ -492,11 +499,13 @@ qboolean BOTLIB_Commands(edict_t* ent) else if (Q_stricmp(cmd, "nav_load") == 0) // Load bot nav from file { #ifdef USE_ZLIB - BOTLIB_LoadNavCompressed(); + if (bot_enable->value) { + BOTLIB_LoadNavCompressed(); #else - BOTLIB_LoadNav(); + BOTLIB_LoadNav(); #endif - return true; + return true; + } } else if (Q_stricmp(cmd, "nav_save") == 0) // Save bot nav to file { From 6032d73466c9a4dd791d80d36c5bdd973db64da2 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 7 Oct 2024 15:50:15 -0400 Subject: [PATCH 850/974] Removed extra debug info --- src/action/acesrc/acebot_cmds.c | 4 ---- src/action/botlib/botlib_cmd.c | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 65aaefe37..54cb68997 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -44,10 +44,6 @@ qboolean ACECM_Commands(edict_t *ent) cmd = gi.argv(0); - if (!bot_enable->value) { - return true; - } - //if(Q_stricmp (cmd, "addnode") == 0 && debug_mode) // ent->last_node = ACEND_AddNode(ent,atoi(gi.argv(1))); diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index a35b0ac7b..8e5068f56 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -11,9 +11,8 @@ qboolean BOTLIB_SV_Cmds(void) if (Q_stricmp(cmd, "bots") == 0) { if (!bot_enable->value) { - gi.dprintf("%s: bot_enable is 0; Bots are disabled, desired bots: %d\n", __func__, bot_connections.desire_bots); + gi.dprintf("%s: bot_enable is 0; Bots are disabled\n", __func__); bot_connections.desire_bots = 0; - return true; } int cc = gi.argc(); From 2a2b256b95f10cfa84a3b9678a8afbf26d45b820 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 6 Oct 2024 21:25:38 +0300 Subject: [PATCH 851/974] Flush uniform buffer explicitly. --- src/refresh/debug.c | 1 + src/refresh/gl.h | 10 ++++++++++ src/refresh/main.c | 4 ++++ src/refresh/mesh.c | 2 ++ src/refresh/shader.c | 9 ++++----- src/refresh/tess.c | 3 +++ 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/refresh/debug.c b/src/refresh/debug.c index e7a7338e2..4d7f2ffc7 100644 --- a/src/refresh/debug.c +++ b/src/refresh/debug.c @@ -417,6 +417,7 @@ static void GL_DrawDebugLines(void) return; GL_LoadMatrix(glr.viewmatrix); + GL_LoadUniforms(); GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_BindArrays(VA_NULLMODEL); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index b9fcd5b02..d7f374fef 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -556,6 +556,7 @@ typedef struct { GLfloat view_matrix[16]; GLfloat proj_matrix[16]; glUniformBlock_t u_block; + bool u_block_dirty; } glState_t; extern glState_t gls; @@ -577,6 +578,7 @@ typedef struct { void (*setup_3d)(void); void (*load_matrix)(GLenum mode, const GLfloat *matrix); + void (*load_uniforms)(void); void (*state_bits)(glStateBits_t bits); void (*array_bits)(glArrayBits_t bits); @@ -635,6 +637,14 @@ static inline void GL_LoadMatrix(const GLfloat *matrix) } } +static inline void GL_LoadUniforms(void) +{ + if (gls.u_block_dirty && gl_backend->load_uniforms) { + gl_backend->load_uniforms(); + gls.u_block_dirty = false; + } +} + static inline void GL_BindBuffer(GLenum target, GLuint buffer) { const int i = target == GL_ELEMENT_ARRAY_BUFFER; diff --git a/src/refresh/main.c b/src/refresh/main.c index 804f74025..514b432c5 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -345,6 +345,7 @@ static void GL_DrawSpriteModel(const model_t *model) } GL_LoadMatrix(glr.viewmatrix); + GL_LoadUniforms(); GL_BindTexture(TMU_TEXTURE, image->texnum); GL_BindArrays(VA_SPRITE); GL_StateBits(bits); @@ -393,6 +394,7 @@ static void GL_DrawNullModel(void) WN32(tess.vertices + 23, U32_BLUE); GL_LoadMatrix(glr.viewmatrix); + GL_LoadUniforms(); GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_BindArrays(VA_NULLMODEL); GL_StateBits(GLS_DEFAULT); @@ -462,6 +464,7 @@ static void GL_OccludeFlares(void) if (!set) { GL_LoadMatrix(glr.viewmatrix); + GL_LoadUniforms(); GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_BindArrays(VA_OCCLUDE); GL_StateBits(GLS_DEPTHMASK_FALSE); @@ -630,6 +633,7 @@ static void GL_WaterWarp(void) GL_StateBits(GLS_DEPTHTEST_DISABLE | GLS_DEPTHMASK_FALSE | GLS_CULL_DISABLE | GLS_TEXTURE_REPLACE | GLS_WARP_ENABLE); GL_ArrayBits(GLA_VERTEX | GLA_TC); + GL_LoadUniforms(); x0 = glr.fd.x; x1 = glr.fd.x + glr.fd.width; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index a30231513..38a3ad02f 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -538,6 +538,7 @@ static void draw_shadow(const glIndex_t *indices, int num_indices) // load shadow projection matrix GL_LoadMatrix(shadowmatrix); + GL_LoadUniforms(); // eliminate z-fighting by utilizing stencil buffer, if available if (gl_config.stencilbits) { @@ -606,6 +607,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, // fall back to entity matrix GL_LoadMatrix(glr.entmatrix); + GL_LoadUniforms(); // avoid drawing hidden faces for transparent gun by pre-filling depth buffer // muzzle flashes are excluded by checking for RF_FULLBRIGHT bit diff --git a/src/refresh/shader.c b/src/refresh/shader.c index c06060cd9..a931d2f76 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -24,8 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GLSL(x) SZ_Write(buf, CONST_STR_LEN(#x "\n")); #define GLSF(x) SZ_Write(buf, CONST_STR_LEN(x)) -static void upload_u_block(void); - static void write_header(sizebuf_t *buf) { if (gl_config.ver_es) { @@ -344,7 +342,7 @@ static void shader_state_bits(glStateBits_t bits) if (diff & GLS_SCROLL_MASK && bits & GLS_SCROLL_ENABLE) { GL_ScrollPos(gls.u_block.scroll, bits); - upload_u_block(); + gls.u_block_dirty = true; } } @@ -385,7 +383,7 @@ static void shader_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) qglVertexAttrib4f(VERT_ATTR_COLOR, r, g, b, a); } -static void upload_u_block(void) +static void shader_load_uniforms(void) { qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), &gls.u_block, GL_DYNAMIC_DRAW); c.uniformUploads++; @@ -405,7 +403,7 @@ static void shader_load_matrix(GLenum mode, const GLfloat *matrix) } GL_MultMatrix(gls.u_block.mvp, gls.proj_matrix, gls.view_matrix); - upload_u_block(); + gls.u_block_dirty = true; } static void shader_setup_2d(void) @@ -510,6 +508,7 @@ const glbackend_t backend_shader = { .setup_3d = shader_setup_3d, .load_matrix = shader_load_matrix, + .load_uniforms = shader_load_uniforms, .state_bits = shader_state_bits, .array_bits = shader_array_bits, diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 3d775dc67..79f67da5b 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -73,6 +73,7 @@ void GL_DrawParticles(void) return; GL_LoadMatrix(glr.viewmatrix); + GL_LoadUniforms(); GL_BindArrays(VA_EFFECT); bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE; @@ -591,6 +592,8 @@ void GL_DrawIndexed(showtris_t showtris) { const glIndex_t *indices = tess.indices; + GL_LoadUniforms(); + GL_LockArrays(tess.numverts); if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { From efd8da1ee48f7b7f43ead83189c8ab5eaec930d1 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 00:29:59 +0300 Subject: [PATCH 852/974] Add GL_DeleteBuffer() function. --- src/refresh/gl.h | 1 + src/refresh/models.c | 8 +------- src/refresh/state.c | 14 ++++++++++++++ src/refresh/surf.c | 10 ++-------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index d7f374fef..07700bbbc 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -689,6 +689,7 @@ void GL_ForceTexture(glTmu_t tmu, GLuint texnum); void GL_BindTexture(glTmu_t tmu, GLuint texnum); void GL_ForceCubemap(GLuint texnum); void GL_BindCubemap(GLuint texnum); +void GL_DeleteBuffer(GLuint buffer); void GL_CommonStateBits(glStateBits_t bits); void GL_ScrollPos(vec2_t scroll, glStateBits_t bits); void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); diff --git a/src/refresh/models.c b/src/refresh/models.c index b93b22693..9a6b68ccc 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -107,13 +107,7 @@ static void MOD_Free(model_t *model) Hunk_Free(&model->skeleton_hunk); #endif - if (model->buffer) { - // invalidate bindings - for (int i = 0; i < 2; i++) - if (gls.currentbuffer[i] == model->buffer) - gls.currentbuffer[i] = 0; - qglDeleteBuffers(1, &model->buffer); - } + GL_DeleteBuffer(model->buffer); memset(model, 0, sizeof(*model)); } diff --git a/src/refresh/state.c b/src/refresh/state.c index 2684477a2..17d08f5b2 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -92,6 +92,20 @@ void GL_BindCubemap(GLuint texnum) c.texSwitches++; } +void GL_DeleteBuffer(GLuint buffer) +{ + if (!buffer) + return; + + Q_assert(qglDeleteBuffers); + qglDeleteBuffers(1, &buffer); + + // invalidate bindings + for (int i = 0; i < q_countof(gls.currentbuffer); i++) + if (gls.currentbuffer[i] == buffer) + gls.currentbuffer[i] = 0; +} + void GL_CommonStateBits(glStateBits_t bits) { glStateBits_t diff = bits ^ gls.state_bits; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index 10b06a463..b1ed6e167 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -986,17 +986,11 @@ void GL_FreeWorld(void) return; BSP_Free(gl_static.world.cache); + Z_Free(gl_static.world.vertices); + GL_DeleteBuffer(gl_static.world.buffer); - if (gl_static.world.vertices) - Z_Free(gl_static.world.vertices); - else if (qglDeleteBuffers) - qglDeleteBuffers(1, &gl_static.world.buffer); - - // invalidate bindings if (gls.currentva == VA_3D) gls.currentva = VA_NONE; - if (gls.currentbuffer[0] == gl_static.world.buffer) - gls.currentbuffer[0] = 0; memset(&gl_static.world, 0, sizeof(gl_static.world)); } From 44ba29ee56243e451a342f37eaae00cc72971535 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 00:51:19 +0300 Subject: [PATCH 853/974] Add R_Malloc() macro. --- src/refresh/gl.h | 3 +++ src/refresh/surf.c | 2 +- src/refresh/texture.c | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 07700bbbc..db97ea704 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -39,6 +39,9 @@ with this program; if not, write to the Free Software Foundation, Inc., * */ +#define R_Malloc(size) Z_TagMalloc(size, TAG_RENDERER) +#define R_Mallocz(size) Z_TagMallocz(size, TAG_RENDERER) + #if USE_GLES #define QGL_INDEX_TYPE GL_UNSIGNED_SHORT typedef GLushort glIndex_t; diff --git a/src/refresh/surf.c b/src/refresh/surf.c index b1ed6e167..bf832f990 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -1091,7 +1091,7 @@ void GL_LoadWorld(const char *name) if (create_surface_vbo(size)) { Com_DPrintf("%s: %zu bytes of vertex data as VBO\n", __func__, size); } else { - gl_static.world.vertices = Z_TagMalloc(size, TAG_RENDERER); + gl_static.world.vertices = R_Malloc(size); Com_DPrintf("%s: %zu bytes of vertex data on heap\n", __func__, size); } gl_static.world.buffer_size = size; diff --git a/src/refresh/texture.c b/src/refresh/texture.c index 0e9e8771a..58b8824cc 100644 --- a/src/refresh/texture.c +++ b/src/refresh/texture.c @@ -918,7 +918,7 @@ int IMG_ReadPixels(screenshot_t *s) s->bpp = bpp; s->rowbytes = rowbytes; - s->pixels = Z_TagMalloc(buf_size, TAG_RENDERER); + s->pixels = R_Malloc(buf_size); s->width = r_config.width; s->height = r_config.height; From 8a2080e5e5bca527d02c8462033ce0e3724d85ee Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 00:53:40 +0300 Subject: [PATCH 854/974] Prepare hunk API for GPU lerp. Add Hunk_FreeToWatermark(). Make Hunk_*Alloc() accept alignment parameter. --- inc/system/hunk.h | 5 +++-- src/common/bsp.c | 12 +++++++----- src/refresh/models.c | 6 +++--- src/unix/hunk.c | 16 +++++++++++----- src/windows/hunk.c | 24 +++++++++++++++++++----- 5 files changed, 43 insertions(+), 20 deletions(-) diff --git a/inc/system/hunk.h b/inc/system/hunk.h index cc15ef473..233456bf5 100644 --- a/inc/system/hunk.h +++ b/inc/system/hunk.h @@ -27,7 +27,8 @@ typedef struct { void Hunk_Init(void); void Hunk_Begin(memhunk_t *hunk, size_t maxsize); -void *Hunk_TryAlloc(memhunk_t *hunk, size_t size); -void *Hunk_Alloc(memhunk_t *hunk, size_t size); +void *Hunk_TryAlloc(memhunk_t *hunk, size_t size, size_t align); +void *Hunk_Alloc(memhunk_t *hunk, size_t size, size_t align); +void Hunk_FreeToWatermark(memhunk_t *hunk, size_t size); void Hunk_End(memhunk_t *hunk); void Hunk_Free(memhunk_t *hunk); diff --git a/src/common/bsp.c b/src/common/bsp.c index 51ebe84ed..3bc3a688f 100644 --- a/src/common/bsp.c +++ b/src/common/bsp.c @@ -45,8 +45,10 @@ static cvar_t *map_visibility_patch; =============================================================================== */ +#define BSP_ALIGN 64 + #define BSP_ALLOC(size) \ - Hunk_Alloc(&bsp->hunk, size) + Hunk_Alloc(&bsp->hunk, size, BSP_ALIGN) #define BSP_ERROR(msg) \ Com_SetLastError(va("%s: %s", __func__, msg)) @@ -544,9 +546,9 @@ static size_t BSP_ParseLightgridHeader(bsp_t *bsp, const byte *in, size_t filele } return - Q_ALIGN(sizeof(grid->nodes[0]) * grid->numnodes, 64) + - Q_ALIGN(sizeof(grid->leafs[0]) * grid->numleafs, 64) + - Q_ALIGN(sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles, 64); + Q_ALIGN(sizeof(grid->nodes[0]) * grid->numnodes, BSP_ALIGN) + + Q_ALIGN(sizeof(grid->leafs[0]) * grid->numleafs, BSP_ALIGN) + + Q_ALIGN(sizeof(grid->samples[0]) * grid->numsamples * grid->numstyles, BSP_ALIGN); } static bool BSP_ValidateLightgrid_r(const lightgrid_t *grid, uint32_t nodenum) @@ -812,7 +814,7 @@ int BSP_Load(const char *name, bsp_t **bsp_p) count++; // round to cacheline - memsize += Q_ALIGN(count * info->memsize, 64); + memsize += Q_ALIGN(count * info->memsize, BSP_ALIGN); maxpos = max(maxpos, ofs + len); } diff --git a/src/refresh/models.c b/src/refresh/models.c index 9a6b68ccc..5a216a9ab 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif #include "format/sp2.h" -#define MOD_Malloc(size) Hunk_TryAlloc(&model->hunk, size) +#define MOD_Malloc(size) Hunk_TryAlloc(&model->hunk, size, 64) #define OOM_CHECK(x) do { if (!(x)) { ret = Q_ERR(ENOMEM); goto fail; } } while (0) #define ENSURE(x, e) if (!(x)) return e @@ -714,7 +714,7 @@ static void MD5_ParseError(const char *text) static void *MD5_Malloc(model_t *model, size_t size) { - void *ptr = Hunk_TryAlloc(&model->skeleton_hunk, size); + void *ptr = Hunk_TryAlloc(&model->skeleton_hunk, size, 64); if (!ptr) { Com_SetLastError("Out of memory"); longjmp(md5_jmpbuf, -1); @@ -1302,7 +1302,7 @@ static bool MD5_LoadSkins(model_t *model) const maliasmesh_t *mesh = &model->meshes[0]; mdl->num_skins = mesh->numskins; - mdl->skins = Hunk_TryAlloc(&model->skeleton_hunk, sizeof(mdl->skins[0]) * mdl->num_skins); + mdl->skins = Hunk_TryAlloc(&model->skeleton_hunk, sizeof(mdl->skins[0]) * mdl->num_skins, 64); if (!mdl->skins) { Com_EPrintf("Out of memory for MD5 skins\n"); return false; diff --git a/src/unix/hunk.c b/src/unix/hunk.c index 4bc763ed7..7375df744 100644 --- a/src/unix/hunk.c +++ b/src/unix/hunk.c @@ -48,15 +48,15 @@ void Hunk_Begin(memhunk_t *hunk, size_t maxsize) hunk->mapped = hunk->maxsize; } -void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) +void *Hunk_TryAlloc(memhunk_t *hunk, size_t size, size_t align) { void *buf; - Q_assert(size <= SIZE_MAX - 63); + Q_assert(size <= SIZE_MAX - (align - 1)); Q_assert(hunk->cursize <= hunk->maxsize); // round to cacheline - size = Q_ALIGN(size, 64); + size = Q_ALIGN(size, align); if (size > hunk->maxsize - hunk->cursize) return NULL; @@ -65,14 +65,20 @@ void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) return buf; } -void *Hunk_Alloc(memhunk_t *hunk, size_t size) +void *Hunk_Alloc(memhunk_t *hunk, size_t size, size_t align) { - void *buf = Hunk_TryAlloc(hunk, size); + void *buf = Hunk_TryAlloc(hunk, size, align); if (!buf) Com_Error(ERR_FATAL, "%s: couldn't allocate %zu bytes", __func__, size); return buf; } +void Hunk_FreeToWatermark(memhunk_t *hunk, size_t size) +{ + Q_assert(size <= hunk->cursize); + hunk->cursize = size; +} + void Hunk_End(memhunk_t *hunk) { size_t newsize; diff --git a/src/windows/hunk.c b/src/windows/hunk.c index db438578d..cd9c00df1 100644 --- a/src/windows/hunk.c +++ b/src/windows/hunk.c @@ -45,15 +45,15 @@ void Hunk_Begin(memhunk_t *hunk, size_t maxsize) hunk->maxsize, GetLastError()); } -void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) +void *Hunk_TryAlloc(memhunk_t *hunk, size_t size, size_t align) { void *buf; - Q_assert(size <= SIZE_MAX - 63); + Q_assert(size <= SIZE_MAX - (align - 1)); Q_assert(hunk->cursize <= hunk->maxsize); // round to cacheline - size = Q_ALIGN(size, 64); + size = Q_ALIGN(size, align); if (size > hunk->maxsize - hunk->cursize) return NULL; @@ -69,14 +69,28 @@ void *Hunk_TryAlloc(memhunk_t *hunk, size_t size) return (byte *)hunk->base + hunk->cursize - size; } -void *Hunk_Alloc(memhunk_t *hunk, size_t size) +void *Hunk_Alloc(memhunk_t *hunk, size_t size, size_t align) { - void *buf = Hunk_TryAlloc(hunk, size); + void *buf = Hunk_TryAlloc(hunk, size, align); if (!buf) Com_Error(ERR_FATAL, "%s: couldn't allocate %zu bytes", __func__, size); return buf; } +void Hunk_FreeToWatermark(memhunk_t *hunk, size_t size) +{ + Q_assert(size <= hunk->cursize); + + size_t newsize = Q_ALIGN(size, pagesize); + if (newsize < hunk->cursize) { + Q_assert(hunk->base); + Q_assert(newsize <= hunk->maxsize); + VirtualFree((byte *)hunk->base + newsize, hunk->maxsize - newsize, MEM_DECOMMIT); + } + + hunk->cursize = size; +} + void Hunk_End(memhunk_t *hunk) { Q_assert(hunk->cursize <= hunk->maxsize); From 223508c91aa9a426999d2aec03a775331356d882 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 00:55:17 +0300 Subject: [PATCH 855/974] Add GL_BindingForTarget(). --- src/refresh/gl.h | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index db97ea704..a4f02eb15 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -532,6 +532,14 @@ typedef enum { MAX_TMUS } glTmu_t; +typedef enum { + GLB_VBO, + GLB_EBO, + GLB_UBO, + + GLB_COUNT +} glBufferBinding_t; + typedef struct { GLfloat mvp[16]; GLfloat msky[2][16]; @@ -553,7 +561,7 @@ typedef struct { GLuint texnumcube; glStateBits_t state_bits; glArrayBits_t array_bits; - GLuint currentbuffer[2]; + GLuint currentbuffer[GLB_COUNT]; glVertexArray_t currentva; const GLfloat *currentmatrix; GLfloat view_matrix[16]; @@ -648,11 +656,23 @@ static inline void GL_LoadUniforms(void) } } -static inline void GL_BindBuffer(GLenum target, GLuint buffer) +static inline glBufferBinding_t GL_BindingForTarget(GLenum target) { - const int i = target == GL_ELEMENT_ARRAY_BUFFER; - Q_assert(i || target == GL_ARRAY_BUFFER); + switch (target) { + case GL_ARRAY_BUFFER: + return GLB_VBO; + case GL_ELEMENT_ARRAY_BUFFER: + return GLB_EBO; + case GL_UNIFORM_BUFFER: + return GLB_UBO; + default: + q_unreachable(); + } +} +static inline void GL_BindBuffer(GLenum target, GLuint buffer) +{ + glBufferBinding_t i = GL_BindingForTarget(target); if (gls.currentbuffer[i] != buffer) { qglBindBuffer(target, buffer); gls.currentbuffer[i] = buffer; From 1a170a8afce2dc12e0beb1022b696b250c6fcdc6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 00:59:24 +0300 Subject: [PATCH 856/974] Allow binding VA_NONE. --- src/refresh/tess.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 79f67da5b..9124ac85d 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -543,22 +543,24 @@ static const glVaDesc_t arraydescs[VA_TOTAL][VERT_ATTR_COUNT] = { void GL_BindArrays(glVertexArray_t va) { - const GLfloat *ptr = tess.vertices; - GLuint buffer = 0; - if (gls.currentva == va) return; - if (va == VA_3D && !gl_static.world.vertices) { - buffer = gl_static.world.buffer; - ptr = NULL; - } else if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { - buffer = gl_static.vertex_buffer; - ptr = NULL; - } + if (va != VA_NONE) { + const GLfloat *ptr = tess.vertices; + GLuint buffer = 0; + + if (va == VA_3D && !gl_static.world.vertices) { + buffer = gl_static.world.buffer; + ptr = NULL; + } else if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { + buffer = gl_static.vertex_buffer; + ptr = NULL; + } - GL_BindBuffer(GL_ARRAY_BUFFER, buffer); - gl_backend->array_pointers(arraydescs[va], ptr); + GL_BindBuffer(GL_ARRAY_BUFFER, buffer); + gl_backend->array_pointers(arraydescs[va], ptr); + } gls.currentva = va; c.vertexArrayBinds++; @@ -566,6 +568,8 @@ void GL_BindArrays(glVertexArray_t va) void GL_LockArrays(GLsizei count) { + if (gls.currentva == VA_NONE) + return; if (gls.currentva == VA_3D && !gl_static.world.vertices) return; if (gl_config.caps & QGL_CAP_CLIENT_VA) { @@ -580,6 +584,8 @@ void GL_LockArrays(GLsizei count) void GL_UnlockArrays(void) { + if (gls.currentva == VA_NONE) + return; if (gls.currentva == VA_3D && !gl_static.world.vertices) return; if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) @@ -592,6 +598,8 @@ void GL_DrawIndexed(showtris_t showtris) { const glIndex_t *indices = tess.indices; + Q_assert(gls.currentva != VA_NONE); + GL_LoadUniforms(); GL_LockArrays(tess.numverts); From 0b2cd8eae907579610cdddfa69dfe6dcb55c3bd9 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 01:01:27 +0300 Subject: [PATCH 857/974] Reduce scope of variable. --- src/refresh/mesh.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 38a3ad02f..caaca3506 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -824,7 +824,6 @@ void GL_DrawAliasModel(const model_t *model) const entity_t *ent = glr.ent; glCullResult_t cull; void (*tessfunc)(const maliasmesh_t *); - int i; if (glr.fd.extended) { newframenum = ent->frame % model->numframes; @@ -911,9 +910,9 @@ void GL_DrawAliasModel(const model_t *model) draw_alias_skeleton(model->skeleton); else #endif - for (i = 0; i < model->nummeshes; i++) { + for (int i = 0; i < model->nummeshes; i++) { const maliasmesh_t *mesh = &model->meshes[i]; - (*tessfunc)(mesh); + tessfunc(mesh); draw_alias_mesh(mesh->indices, mesh->numindices, mesh->tcoords, mesh->numverts, mesh->skins, mesh->numskins); From 9c9818e137974237b10dbd9568de62a8f84d1e39 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 01:03:13 +0300 Subject: [PATCH 858/974] Move glBindBufferBase to GL 3.0. --- src/refresh/qgl.c | 2 +- src/refresh/qgl.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 486a02bc8..590ecf370 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -290,6 +290,7 @@ static const glsection_t sections[] = { // ensure full hardware support, including mipmaps. .caps = QGL_CAP_TEXTURE_MAX_LEVEL | QGL_CAP_TEXTURE_NON_POWER_OF_TWO, .functions = (const glfunction_t []) { + QGL_FN(BindBufferBase), QGL_FN(BindVertexArray), QGL_FN(DeleteVertexArrays), QGL_FN(GenVertexArrays), @@ -306,7 +307,6 @@ static const glsection_t sections[] = { .ver_es = QGL_VER(3, 0), .caps = QGL_CAP_SHADER, .functions = (const glfunction_t []) { - QGL_FN(BindBufferBase), QGL_FN(GetActiveUniformBlockiv), QGL_FN(GetUniformBlockIndex), QGL_FN(UniformBlockBinding), diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index 6207ad5cc..d7bfb8325 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -138,6 +138,7 @@ QGLAPI void (APIENTRYP qglVertexAttrib4f)(GLuint index, GLfloat x, GLfloat y, GL QGLAPI void (APIENTRYP qglVertexAttribPointer)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); // GL 3.0 +QGLAPI void (APIENTRYP qglBindBufferBase)(GLenum target, GLuint index, GLuint buffer); QGLAPI void (APIENTRYP qglBindFramebuffer)(GLenum target, GLuint framebuffer); QGLAPI void (APIENTRYP qglBindRenderbuffer)(GLenum target, GLuint renderbuffer); QGLAPI void (APIENTRYP qglBindVertexArray)(GLuint array); @@ -156,7 +157,6 @@ QGLAPI const GLubyte *(APIENTRYP qglGetStringi)(GLenum name, GLuint index); QGLAPI void (APIENTRYP qglRenderbufferStorage)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); // GL 3.1 -QGLAPI void (APIENTRYP qglBindBufferBase)(GLenum target, GLuint index, GLuint buffer); QGLAPI void (APIENTRYP qglGetActiveUniformBlockiv)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params); QGLAPI GLuint (APIENTRYP qglGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); QGLAPI void (APIENTRYP qglUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); From 2a513c2b546132c1e84afe50fd5ec4c91160023f Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 01:11:07 +0300 Subject: [PATCH 859/974] Print freed models count. --- src/refresh/models.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index 5a216a9ab..cadd697f6 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -115,7 +115,7 @@ static void MOD_Free(model_t *model) void MOD_FreeUnused(void) { model_t *model; - int i; + int i, count = 0; for (i = 0, model = r_models; i < r_numModels; i++, model++) { if (!model->type) @@ -131,18 +131,28 @@ void MOD_FreeUnused(void) } else { // don't need this model MOD_Free(model); + count++; } } + + if (count) + Com_DPrintf("%s: %i models freed\n", __func__, count); } void MOD_FreeAll(void) { model_t *model; - int i; + int i, count = 0; - for (i = 0, model = r_models; i < r_numModels; i++, model++) - if (model->type) + for (i = 0, model = r_models; i < r_numModels; i++, model++) { + if (model->type) { MOD_Free(model); + count++; + } + } + + if (count) + Com_DPrintf("%s: %i models freed\n", __func__, count); r_numModels = 0; } From a5e0aaab68f847f6a4afb8ba6aaac79f3c264c28 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 11 Oct 2024 13:49:31 -0400 Subject: [PATCH 860/974] Fixing bot scaling issues in teamplay --- src/action/botlib/botlib_spawn.c | 82 ++++++++++++++++---------------- src/action/g_save.c | 2 +- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 4ef693826..5a9ec5487 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1935,14 +1935,15 @@ int BOTLIB_TPBotTeamScaling(void) if (bot_connections.total_bots == total_bots_needed) { return 0; // Each team is populated, return 0 } else if (bot_connections.total_bots > total_bots_needed) { - return -1; // Bots were removed - } + bot_connections.desire_bots = total_bots_needed; + return -1; // Bots were removed + } // Calculate the percentage of bots relative to the max team size float percent = (float)bot_connections.total_bots / total_bots_needed; // Initialize scaling direction if not already set - if (bot_connections.scale_dn == false && bot_connections.scale_up == false) { + if (!bot_connections.scale_dn && !bot_connections.scale_up) { bot_connections.scale_up = true; } @@ -1959,56 +1960,56 @@ int BOTLIB_TPBotTeamScaling(void) bot_connections.scale_dn = false; } - // Decrease bots + // Adjust desire_bots based on scaling direction if (bot_connections.scale_dn) { bot_connections.desire_bots--; - } - - // Increase bots - if (bot_connections.scale_up) { + } else if (bot_connections.scale_up) { bot_connections.desire_bots++; } - // Don't let bots be greater than total_bots_needed + // Ensure desire_bots is within valid range if (bot_connections.desire_bots > total_bots_needed) { - bot_connections.desire_bots--; + bot_connections.desire_bots = total_bots_needed; } - - // Never go below 1 bot when bot_maxteam is active if (bot_connections.desire_bots < 1) { bot_connections.desire_bots = 1; } - // Remove excess bots if bot_maxteam is reduced - if (bot_connections.total_bots > total_bots_needed) { - bot_connections.total_bots = total_bots_needed; - return -1; // Bots were removed + return 1; // Continue scaling + } else { // bot_maxteam 0, just add bots + int total_players = bot_connections.total_bots + bot_connections.total_humans; + int desired_total_players = (int)bot_playercount->value; + + if (desired_total_players && total_players < desired_total_players) { + // Scale up bots when total players are less than desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } else if (total_players > desired_total_players) { + // Scale down bots when total players exceed desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } else { + // No scaling needed + bot_connections.scale_up = false; + bot_connections.scale_dn = false; } - return 1; // Continue scaling - } else { // bot_maxteam 0, just add bots - if (bot_connections.desire_bots != bot_connections.total_bots){ - if (bot_connections.desire_bots > bot_connections.total_bots) { - bot_connections.scale_up = true; - bot_connections.scale_dn = false; - } else { - bot_connections.scale_up = false; - bot_connections.scale_dn = true; - } - } else { - bot_connections.scale_up = false; - bot_connections.scale_dn = false; - } - } - if (bot_connections.scale_up){ - return 1; - } else if (bot_connections.scale_dn){ - return -1; - } else { - return 0; - } + // Ensure desire_bots is not negative + if (bot_connections.desire_bots < 0) { + bot_connections.desire_bots = 0; + } + } + + if (bot_connections.scale_up) { + return 1; + } else if (bot_connections.scale_dn) { + return -1; + } else { + return 0; + } } - void BOTLIB_TPBotCountManual(void) { // Sanity check @@ -2177,7 +2178,8 @@ void BOTLIB_CheckBotRules(void) } // Turn on team balance if bot_maxteam is used - if (bot_maxteam->value > 0) bot_connections.auto_balance_bots = true; + if (bot_maxteam->value > 0 || use_balancer->value) + bot_connections.auto_balance_bots = true; if (teamplay->value && !bot_connections.auto_balance_bots) { BOTLIB_TPBotCountManual(); diff --git a/src/action/g_save.c b/src/action/g_save.c index 9753cf3f8..a10dcad4c 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -687,7 +687,7 @@ void InitGame( void ) bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); - bot_maxteam = gi.cvar("bot_maxteam", "10", 0); + bot_maxteam = gi.cvar("bot_maxteam", "0", 0); bot_playercount = gi.cvar("bot_playercount", "0", 0); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); From 8cb2d94bfb29e32f03df215c1010b1dfb4134b7f Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 11 Oct 2024 20:23:59 -0400 Subject: [PATCH 861/974] Commenting out TE_DAMAGE_DEALT --- src/action/p_view.c | 16 ++++++++-------- src/client/tent.c | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/action/p_view.c b/src/action/p_view.c index 8807b4c0e..1a7d1b414 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1695,12 +1695,12 @@ void ClientEndServerFrame (edict_t * ent) RadioThink(ent); // Paril's hit markers (don't draw for bots!) - if (ent->client->damage_dealt > 0 && !ent->is_bot) - { - gi.WriteByte(svc_temp_entity); - gi.WriteByte(TE_DAMAGE_DEALT); - gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); - gi.unicast(ent, false); - ent->client->damage_dealt = 0; - } + // if (ent->client->damage_dealt > 0 && !ent->is_bot) + // { + // gi.WriteByte(svc_temp_entity); + // gi.WriteByte(TE_DAMAGE_DEALT); + // gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); + // gi.unicast(ent, false); + // ent->client->damage_dealt = 0; + // } } diff --git a/src/client/tent.c b/src/client/tent.c index a0dfc13c8..b0060cdd3 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1660,15 +1660,15 @@ void CL_ParseTEnt(void) CL_PowerSplash(); break; - case TE_DAMAGE_DEALT: - if (te.count > 0 && cl_hit_markers->integer > 0) { - cl.hit_marker_time = cls.realtime; - cl.hit_marker_count = te.count; - if (cl_hit_markers->integer > 1) { - S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); - } - } - break; + // case TE_DAMAGE_DEALT: + // if (te.count > 0 && cl_hit_markers->integer > 0) { + // cl.hit_marker_time = cls.realtime; + // cl.hit_marker_count = te.count; + // if (cl_hit_markers->integer > 1) { + // S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + // } + // } + // break; default: Com_Error(ERR_DROP, "%s: bad type", __func__); From 009f142ff132a8bbf5aa4dc22c05bd97c66e7928 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 28 Sep 2024 22:13:43 +0300 Subject: [PATCH 862/974] Support alias model interpolation on GPU. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Can work on ≥ GL 3.1, but only enable on ≥ GL 4.3 by default to avoid performance regressions. Depending on hardware capabilities, this can make MD5 rendering a lot (up to 10x) faster. --- doc/client.asciidoc | 9 ++ src/refresh/gl.h | 91 +++++++++++-- src/refresh/main.c | 25 ++++ src/refresh/mesh.c | 218 +++++++++++++++++++++++------- src/refresh/models.c | 314 ++++++++++++++++++++++++++----------------- src/refresh/qgl.c | 22 +++ src/refresh/qgl.h | 3 + src/refresh/shader.c | 312 +++++++++++++++++++++++++++++++++++++++--- src/refresh/state.c | 5 +- src/refresh/tess.c | 4 +- 10 files changed, 800 insertions(+), 203 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index c31ae79aa..2e3dcf766 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -894,6 +894,15 @@ gl_md5_distance:: the viewer, otherwise use original model. Default value is 2048. Setting this to 0 disables distance LOD. +gl_gpulerp:: + Enables alias model interpolation on GPU for potential rendering + speedup. Default value is 1 (auto). If using OpenGL core profile, this + option is always enabled. If not using GLSL backend, this option is always + disabled. + - 0 — disabled + - 1 — auto (enabled on OpenGL 4.3 and higher) + - 2 — force enabled + gl_glowmap_intensity:: Intensity factor for entity glowmaps. Default value is 0.75. diff --git a/src/refresh/gl.h b/src/refresh/gl.h index a4f02eb15..41fca4841 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -68,6 +68,7 @@ typedef struct { bool use_shaders; bool use_cubemaps; bool use_bmodel_skies; + bool use_gpu_lerp; struct { bsp_t *cache; vec_t *vertices; @@ -79,6 +80,10 @@ typedef struct { GLuint warp_renderbuffer; GLuint warp_framebuffer; GLuint uniform_buffer; +#if USE_MD5 + GLuint skeleton_buffer; + GLuint skeleton_tex[2]; +#endif GLuint array_object; GLuint index_buffer; GLuint vertex_buffer; @@ -91,6 +96,8 @@ typedef struct { uint32_t inverse_intensity_66; uint32_t inverse_intensity_100; int nolm_mask; + int hunk_align; + int hunk_maxsize; float sintab[256]; byte latlngtab[NUMVERTEXNORMALS][2]; byte lightstylemap[MAX_LIGHTSTYLES]; @@ -139,6 +146,9 @@ typedef enum { QGL_CAP_QUERY_RESULT_NO_WAIT = BIT(10), QGL_CAP_CLIENT_VA = BIT(11), QGL_CAP_LINE_SMOOTH = BIT(12), + QGL_CAP_BUFFER_TEXTURE = BIT(13), + QGL_CAP_SHADER_STORAGE = BIT(14), + QGL_CAP_SKELETON_MASK = QGL_CAP_BUFFER_TEXTURE | QGL_CAP_SHADER_STORAGE, } glcap_t; #define QGL_VER(major, minor) ((major) * 100 + (minor)) @@ -154,6 +164,7 @@ typedef struct { int stencilbits; int max_texture_size_log2; int max_texture_size; + int ssbo_align; } glConfig_t; extern glStatic_t gl_static; @@ -279,6 +290,14 @@ static inline void GL_AdvanceValue(float *restrict val, float target, float spee * */ +#define MOD_MAXSIZE_GPU 0x1000000 + +#if (defined _WIN32) && !(defined _WIN64) +#define MOD_MAXSIZE_CPU 0x400000 +#else +#define MOD_MAXSIZE_CPU 0x800000 +#endif + typedef struct { float st[2]; } maliastc_t; @@ -392,10 +411,9 @@ typedef struct { int nummeshes; int numframes; - maliasmesh_t *meshes; // md2 / md3 + maliasmesh_t *meshes; // MD2 / MD3 #if USE_MD5 - md5_model_t *skeleton; // md5 - memhunk_t skeleton_hunk; // md5 + md5_model_t *skeleton; // MD5 #endif union { maliasframe_t *frames; @@ -477,18 +495,26 @@ typedef enum { GLS_DEFAULT_SKY = BIT(14), GLS_DEFAULT_FLARE = BIT(15), - GLS_SHADE_SMOOTH = BIT(16), - GLS_SCROLL_X = BIT(17), - GLS_SCROLL_Y = BIT(18), - GLS_SCROLL_FLIP = BIT(19), - GLS_SCROLL_SLOW = BIT(20), + GLS_MESH_MD2 = BIT(16), + GLS_MESH_MD5 = BIT(17), + GLS_MESH_LERP = BIT(18), + GLS_MESH_SHELL = BIT(19), + GLS_MESH_SHADE = BIT(20), + + GLS_SHADE_SMOOTH = BIT(21), + GLS_SCROLL_X = BIT(22), + GLS_SCROLL_Y = BIT(23), + GLS_SCROLL_FLIP = BIT(24), + GLS_SCROLL_SLOW = BIT(25), GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, GLS_SKY_MASK = GLS_CLASSIC_SKY | GLS_DEFAULT_SKY, + GLS_MESH_ANY = GLS_MESH_MD2 | GLS_MESH_MD5, + GLS_MESH_MASK = GLS_MESH_ANY | GLS_MESH_LERP | GLS_MESH_SHELL | GLS_MESH_SHADE, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | - GLS_SKY_MASK | GLS_DEFAULT_FLARE, + GLS_SKY_MASK | GLS_DEFAULT_FLARE | GLS_MESH_MASK, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; @@ -497,8 +523,16 @@ typedef enum { VERT_ATTR_TC, VERT_ATTR_LMTC, VERT_ATTR_COLOR, + VERT_ATTR_COUNT, - VERT_ATTR_COUNT + // MD2 + VERT_ATTR_MESH_TC = 0, + VERT_ATTR_MESH_NEW_POS = 1, + VERT_ATTR_MESH_OLD_POS = 2, + + // MD5 + VERT_ATTR_MESH_NORM = 1, + VERT_ATTR_MESH_VERT = 2, } glVertexAttr_t; typedef enum { @@ -507,6 +541,8 @@ typedef enum { GLA_TC = BIT(VERT_ATTR_TC), GLA_LMTC = BIT(VERT_ATTR_LMTC), GLA_COLOR = BIT(VERT_ATTR_COLOR), + GLA_MESH_STATIC = MASK(2), + GLA_MESH_LERP = MASK(3), } glArrayBits_t; typedef enum { @@ -529,7 +565,11 @@ typedef enum { TMU_TEXTURE, TMU_LIGHTMAP, TMU_GLOWMAP, - MAX_TMUS + MAX_TMUS, + + // MD5 + TMU_SKEL_WEIGHTS, + TMU_SKEL_JOINTNUMS, } glTmu_t; typedef enum { @@ -540,15 +580,38 @@ typedef enum { GLB_COUNT } glBufferBinding_t; +enum { UBO_UNIFORMS, UBO_SKELETON }; +enum { SSBO_WEIGHTS, SSBO_JOINTNUMS }; + +typedef struct { + vec4_t oldscale; + vec4_t newscale; + vec4_t translate; + vec4_t shadedir; + vec4_t color; + vec4_t pad_0; + GLfloat pad_1; + GLfloat pad_2; + GLfloat pad_3; + GLuint weight_ofs; + GLuint jointnum_ofs; + GLfloat shellscale; + GLfloat backlerp; + GLfloat frontlerp; +} glMeshBlock_t; + typedef struct { GLfloat mvp[16]; - GLfloat msky[2][16]; + union { + GLfloat msky[2][16]; + glMeshBlock_t mesh; + }; GLfloat time; GLfloat modulate; GLfloat add; GLfloat intensity; GLfloat intensity2; - GLfloat pad_1; + GLfloat pad_4; GLfloat w_amp[2]; GLfloat w_phase[2]; GLfloat scroll[2]; @@ -572,6 +635,8 @@ typedef struct { extern glState_t gls; +#define VBO_OFS(n) ((void *)(n)) + typedef struct { uint8_t size; bool type; diff --git a/src/refresh/main.c b/src/refresh/main.c index 514b432c5..9f5318c5b 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -1013,6 +1013,31 @@ static void GL_SetupConfig(void) qglDebugMessageCallback(myDebugProc, NULL); } + if (gl_config.caps & QGL_CAP_SHADER_STORAGE) { + integer = 0; + qglGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &integer); + if (integer < 2) { + Com_DPrintf("Not enough shader storage blocks available\n"); + gl_config.caps &= ~QGL_CAP_SHADER_STORAGE; + } else { + integer = 1; + qglGetIntegerv(GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, &integer); + if (integer & (integer - 1)) + integer = Q_npot32(integer); + Com_DPrintf("SSBO alignment: %d\n", integer); + gl_config.ssbo_align = integer; + } + } + + if (gl_config.caps & QGL_CAP_BUFFER_TEXTURE) { + integer = 0; + qglGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, &integer); + if (integer < MOD_MAXSIZE_GPU) { + Com_DPrintf("Not enough buffer texture size available\n"); + gl_config.caps &= ~QGL_CAP_BUFFER_TEXTURE; + } + } + GL_ShowErrors(__func__); } diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index caaca3506..40c52bcd9 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -24,17 +24,18 @@ typedef enum { SHADOW_ONLY } drawshadow_t; -static unsigned oldframenum; -static unsigned newframenum; -static float frontlerp; -static float backlerp; -static vec3_t origin; -static vec3_t oldscale; -static vec3_t newscale; -static vec3_t translate; -static vec_t shellscale; -static vec4_t color; -static GLuint buffer; +static unsigned oldframenum; +static unsigned newframenum; +static float frontlerp; +static float backlerp; +static vec3_t origin; +static vec3_t oldscale; +static vec3_t newscale; +static vec3_t translate; +static vec_t shellscale; +static vec4_t color; +static glStateBits_t meshbits; +static GLuint buffer; static vec3_t shadedir; static bool dotshading; @@ -422,19 +423,32 @@ static void setup_celshading(void) celscale = 1.0f - Distance(origin, glr.fd.vieworg) / 700.0f; } +static void uniform_mesh_color(float r, float g, float b, float a) +{ + if (gls.currentva) { + GL_Color(r, g, b, a); + } else { + Vector4Set(gls.u_block.mesh.color, r, g, b, a); + gls.u_block_dirty = true; + } +} + static void draw_celshading(const glIndex_t *indices, int num_indices) { if (celscale < 0.01f) return; GL_BindTexture(TMU_TEXTURE, TEXNUM_BLACK); - GL_StateBits(GLS_BLEND_BLEND); - GL_ArrayBits(GLA_VERTEX); + GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE)); + if (gls.currentva) + GL_ArrayBits(GLA_VERTEX); + + uniform_mesh_color(0, 0, 0, color[3] * celscale); + GL_LoadUniforms(); qglLineWidth(gl_celshading->value * celscale); qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); qglCullFace(GL_FRONT); - GL_Color(0, 0, 0, color[3] * celscale); GL_DrawTriangles(num_indices, indices); qglCullFace(GL_BACK); qglPolygonMode(GL_FRONT_AND_BACK, GL_FILL); @@ -538,7 +552,6 @@ static void draw_shadow(const glIndex_t *indices, int num_indices) // load shadow projection matrix GL_LoadMatrix(shadowmatrix); - GL_LoadUniforms(); // eliminate z-fighting by utilizing stencil buffer, if available if (gl_config.stencilbits) { @@ -547,13 +560,16 @@ static void draw_shadow(const glIndex_t *indices, int num_indices) qglStencilOp(GL_KEEP, GL_KEEP, GL_INCR); } - GL_StateBits(GLS_BLEND_BLEND); GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); - GL_ArrayBits(GLA_VERTEX); + GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE)); + if (gls.currentva) + GL_ArrayBits(GLA_VERTEX); + + uniform_mesh_color(0, 0, 0, color[3] * 0.5f); + GL_LoadUniforms(); qglEnable(GL_POLYGON_OFFSET_FILL); qglPolygonOffset(-1.0f, -2.0f); - GL_Color(0, 0, 0, color[3] * 0.5f); GL_DrawTriangles(num_indices, indices); qglDisable(GL_POLYGON_OFFSET_FILL); @@ -590,6 +606,23 @@ static const image_t *skin_for_mesh(image_t **skins, int num_skins) return skins[ent->skinnum]; } +static void bind_alias_arrays(const maliasmesh_t *mesh) +{ + uintptr_t base = (uintptr_t)mesh->verts; + uintptr_t old_ofs = base + oldframenum * mesh->numverts * sizeof(mesh->verts[0]); + uintptr_t new_ofs = base + newframenum * mesh->numverts * sizeof(mesh->verts[0]); + + qglVertexAttribPointer(VERT_ATTR_MESH_TC, 2, GL_FLOAT, GL_FALSE, 0, mesh->tcoords); + qglVertexAttribIPointer(VERT_ATTR_MESH_NEW_POS, 4, GL_SHORT, 0, VBO_OFS(new_ofs)); + + if (oldframenum == newframenum) { + GL_ArrayBits(GLA_MESH_STATIC); + } else { + qglVertexAttribIPointer(VERT_ATTR_MESH_OLD_POS, 4, GL_SHORT, 0, VBO_OFS(old_ofs)); + GL_ArrayBits(GLA_MESH_LERP); + } +} + static void draw_alias_mesh(const glIndex_t *indices, int num_indices, const maliastc_t *tcoords, int num_verts, image_t **skins, int num_skins) @@ -607,13 +640,19 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, // fall back to entity matrix GL_LoadMatrix(glr.entmatrix); + + uniform_mesh_color(color[0], color[1], color[2], color[3]); GL_LoadUniforms(); // avoid drawing hidden faces for transparent gun by pre-filling depth buffer // muzzle flashes are excluded by checking for RF_FULLBRIGHT bit if ((glr.ent->flags & (RF_TRANSLUCENT | RF_WEAPONMODEL | RF_FULLBRIGHT)) == (RF_TRANSLUCENT | RF_WEAPONMODEL)) { - GL_StateBits(GLS_DEFAULT); - GL_ArrayBits(GLA_VERTEX); + if (gls.currentva) { + GL_StateBits(GLS_DEFAULT); + GL_ArrayBits(GLA_VERTEX); + } else { + GL_StateBits(meshbits & ~GLS_MESH_SHADE); + } GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); qglColorMask(0, 0, 0, 0); @@ -625,7 +664,9 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, } state = GLS_INTENSITY_ENABLE; - if (dotshading) + if (!gls.currentva) + state |= meshbits; + else if (dotshading) state |= GLS_SHADE_SMOOTH; if (glr.ent->flags & RF_TRANSLUCENT) @@ -642,16 +683,14 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, if (skin->texnum2) GL_BindTexture(TMU_GLOWMAP, skin->texnum2); - if (dotshading) { - GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); - } else { - GL_ArrayBits(GLA_VERTEX | GLA_TC); - GL_Color(color[0], color[1], color[2], color[3]); + if (gls.currentva) { + if (dotshading) + GL_ArrayBits(GLA_VERTEX | GLA_TC | GLA_COLOR); + else + GL_ArrayBits(GLA_VERTEX | GLA_TC); + gl_backend->tex_coord_pointer((const GLfloat *)tcoords); } - GL_BindBuffer(GL_ARRAY_BUFFER, buffer); - gl_backend->tex_coord_pointer((const GLfloat *)tcoords); - GL_LockArrays(num_verts); GL_DrawTriangles(num_indices, indices); @@ -758,9 +797,41 @@ static void lerp_alias_skeleton(const md5_model_t *model) #pragma GCC reset_options #endif +static void bind_skel_arrays(const md5_mesh_t *mesh, const md5_joint_t *skel) +{ + if (gl_config.caps & QGL_CAP_SHADER_STORAGE) { + qglBindBufferRange(GL_SHADER_STORAGE_BUFFER, SSBO_WEIGHTS, buffer, + (uintptr_t)mesh->weights, mesh->num_weights * sizeof(mesh->weights[0])); + qglBindBufferRange(GL_SHADER_STORAGE_BUFFER, SSBO_JOINTNUMS, buffer, + (uintptr_t)mesh->jointnums, Q_ALIGN(mesh->num_weights, sizeof(uint32_t))); + } else { + Q_assert(gl_config.caps & QGL_CAP_BUFFER_TEXTURE); + + gls.u_block.mesh.weight_ofs = (uintptr_t)mesh->weights / sizeof(mesh->weights[0]); + gls.u_block.mesh.jointnum_ofs = (uintptr_t)mesh->jointnums; + + GL_ActiveTexture(TMU_SKEL_WEIGHTS); + qglBindTexture(GL_TEXTURE_BUFFER, gl_static.skeleton_tex[0]); + qglTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, buffer); + + GL_ActiveTexture(TMU_SKEL_JOINTNUMS); + qglBindTexture(GL_TEXTURE_BUFFER, gl_static.skeleton_tex[1]); + qglTexBuffer(GL_TEXTURE_BUFFER, GL_R8UI, buffer); + } + + uintptr_t base = (uintptr_t)mesh->vertices; + qglVertexAttribPointer (VERT_ATTR_MESH_TC, 2, GL_FLOAT, GL_FALSE, 0, mesh->tcoords); + qglVertexAttribPointer (VERT_ATTR_MESH_NORM, 3, GL_FLOAT, GL_FALSE, sizeof(mesh->vertices[0]), VBO_OFS(base)); + qglVertexAttribIPointer(VERT_ATTR_MESH_VERT, 2, GL_UNSIGNED_SHORT, sizeof(mesh->vertices[0]), VBO_OFS(base + sizeof(vec3_t))); + + GL_ArrayBits(GLA_MESH_LERP); +} + static void draw_skeleton_mesh(const md5_model_t *model, const md5_mesh_t *mesh, const md5_joint_t *skel) { - if (glr.ent->flags & RF_SHELL_MASK) + if (buffer) + bind_skel_arrays(mesh, skel); + else if (glr.ent->flags & RF_SHELL_MASK) tess_shell_skel(mesh, skel); else if (dotshading) tess_shade_skel(mesh, skel); @@ -772,6 +843,11 @@ static void draw_skeleton_mesh(const md5_model_t *model, const md5_mesh_t *mesh, model->skins, model->num_skins); } +typedef struct { + vec4_t pos; + vec4_t axis[3]; +} glJoint_t; + static void draw_alias_skeleton(const md5_model_t *model) { const md5_joint_t *skel = temp_skeleton; @@ -781,6 +857,27 @@ static void draw_alias_skeleton(const md5_model_t *model) else lerp_alias_skeleton(model); + if (buffer) { + glJoint_t joints[MD5_MAX_JOINTS]; + + for (int i = 0; i < model->num_joints; i++) { + const md5_joint_t *in = &skel[i]; + glJoint_t *out = &joints[i]; + VectorCopy(in->pos, out->pos); + out->pos[3] = in->scale; + VectorCopy(in->axis[0], out->axis[0]); + VectorCopy(in->axis[1], out->axis[1]); + VectorCopy(in->axis[2], out->axis[2]); + } + + GL_BindBuffer(GL_UNIFORM_BUFFER, gl_static.skeleton_buffer); + qglBufferData(GL_UNIFORM_BUFFER, sizeof(joints), NULL, GL_STREAM_DRAW); + qglBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(joints[0]) * model->num_joints, joints); + + meshbits &= ~GLS_MESH_MD2; + meshbits |= GLS_MESH_MD5 | GLS_MESH_LERP; + } + for (int i = 0; i < model->num_meshes; i++) draw_skeleton_mesh(model, &model->meshes[i], skel); } @@ -875,27 +972,56 @@ void GL_DrawAliasModel(const model_t *model) // setup scale and translate vectors setup_frame_scale(model); - // select proper tessfunc - if (ent->flags & RF_SHELL_MASK) { - shellscale = (ent->flags & RF_WEAPONMODEL) ? - WEAPONSHELL_SCALE : POWERSUIT_SCALE; - tessfunc = newframenum == oldframenum ? - tess_static_shell : tess_lerped_shell; - } else if (dotshading) { - tessfunc = newframenum == oldframenum ? - tess_static_shade : tess_lerped_shade; + if (ent->flags & RF_SHELL_MASK) + shellscale = (ent->flags & RF_WEAPONMODEL) ? WEAPONSHELL_SCALE : POWERSUIT_SCALE; + + buffer = model->buffer; + GL_BindBuffer(GL_ARRAY_BUFFER, model->buffer); + GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, model->buffer); + + if (gl_static.use_gpu_lerp) { + Q_assert(buffer); + + GL_BindArrays(VA_NONE); + tessfunc = bind_alias_arrays; + + meshbits = GLS_MESH_MD2; + if (oldframenum != newframenum) + meshbits |= GLS_MESH_LERP; + if (glr.ent->flags & RF_SHELL_MASK) + meshbits |= GLS_MESH_SHELL; + else if (dotshading) + meshbits |= GLS_MESH_SHADE; + + VectorCopy(oldscale, gls.u_block.mesh.oldscale); + VectorCopy(newscale, gls.u_block.mesh.newscale); + VectorCopy(translate, gls.u_block.mesh.translate); + VectorCopy(shadedir, gls.u_block.mesh.shadedir); + Vector4Copy(color, gls.u_block.mesh.color); + gls.u_block.mesh.shellscale = shellscale; + gls.u_block.mesh.backlerp = backlerp; + gls.u_block.mesh.frontlerp = frontlerp; } else { - tessfunc = newframenum == oldframenum ? - tess_static_plain : tess_lerped_plain; + Q_assert(!buffer); + + GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); + meshbits = 0; + + // select proper tessfunc + if (ent->flags & RF_SHELL_MASK) { + tessfunc = newframenum == oldframenum ? + tess_static_shell : tess_lerped_shell; + } else if (dotshading) { + tessfunc = newframenum == oldframenum ? + tess_static_shade : tess_lerped_shade; + } else { + tessfunc = newframenum == oldframenum ? + tess_static_plain : tess_lerped_plain; + } } GL_RotateForEntity(false); - GL_BindArrays(dotshading ? VA_MESH_SHADE : VA_MESH_FLAT); - - buffer = model->buffer; - GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, model->buffer); - if (ent->flags & RF_WEAPONMODEL) setup_weaponmodel(); diff --git a/src/refresh/models.c b/src/refresh/models.c index cadd697f6..bc812a8a4 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -24,9 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #endif #include "format/sp2.h" -#define MOD_Malloc(size) Hunk_TryAlloc(&model->hunk, size, 64) +#define MOD_GpuMalloc(size) \ + Hunk_TryAlloc(&model->hunk, size, gl_static.hunk_align) + +#define MOD_CpuMalloc(size) \ + (gl_static.use_gpu_lerp ? R_Mallocz(size) : MOD_GpuMalloc(size)) -#define OOM_CHECK(x) do { if (!(x)) { ret = Q_ERR(ENOMEM); goto fail; } } while (0) #define ENSURE(x, e) if (!(x)) return e // this used to be MAX_MODELS * 2, but not anymore. MAX_MODELS is 8192 now and @@ -86,10 +89,8 @@ static void MOD_List_f(void) size_t model_size = model->hunk.mapped; int flag = ' '; #if USE_MD5 - if (model->skeleton) { - model_size += model->skeleton_hunk.mapped; + if (model->skeleton) flag = '*'; - } #endif Com_Printf("%c%c %8zu : %s\n", types[model->type], flag, model_size, model->name); @@ -100,14 +101,50 @@ static void MOD_List_f(void) Com_Printf("Total resident: %zu\n", bytes); } -static void MOD_Free(model_t *model) +#if USE_MD5 +static void MD5_Free(md5_model_t *mdl); +#endif + +static void MOD_FreeAlias(model_t *model) { Hunk_Free(&model->hunk); + + GL_DeleteBuffer(model->buffer); + + // all memory is allocated on hunk if not using GPU lerp + if (!gl_static.use_gpu_lerp) + return; + #if USE_MD5 - Hunk_Free(&model->skeleton_hunk); + MD5_Free(model->skeleton); #endif - GL_DeleteBuffer(model->buffer); + for (int i = 0; i < model->nummeshes; i++) { + Z_Free(model->meshes[i].skins); +#if USE_MD5 + Z_Free(model->meshes[i].skinnames); +#endif + } + + Z_Free(model->meshes); + Z_Free(model->frames); +} + +static void MOD_Free(model_t *model) +{ + switch (model->type) { + case MOD_SPRITE: + Z_Free(model->spriteframes); + break; + case MOD_ALIAS: + MOD_FreeAlias(model); + break; + case MOD_EMPTY: + case MOD_FREE: + break; + default: + Q_assert(!"bad model type"); + } memset(model, 0, sizeof(*model)); } @@ -124,10 +161,6 @@ void MOD_FreeUnused(void) if (model->registration_sequence == r_registration_sequence) { // make sure it is paged in Com_PageInMemory(model->hunk.base, model->hunk.cursize); -#if USE_MD5 - if (model->skeleton_hunk.base) - Com_PageInMemory(model->skeleton_hunk.base, model->skeleton_hunk.cursize); -#endif } else { // don't need this model MOD_Free(model); @@ -198,10 +231,8 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) return Q_ERR_INVALID_FORMAT; } - Hunk_Begin(&model->hunk, sizeof(model->spriteframes[0]) * header.numframes); model->type = MOD_SPRITE; - - model->spriteframes = MOD_Malloc(sizeof(model->spriteframes[0]) * header.numframes); + model->spriteframes = R_Malloc(sizeof(model->spriteframes[0]) * header.numframes); model->numframes = header.numframes; src_frame = (dsp2frame_t *)((byte *)rawdata + sizeof(dsp2header_t)); @@ -224,8 +255,6 @@ static int MOD_LoadSP2(model_t *model, const void *rawdata, size_t length) dst_frame++; } - Hunk_End(&model->hunk); - return Q_ERR_SUCCESS; } @@ -255,6 +284,25 @@ static const char *MOD_ValidateMD2(const dmd2header_t *header, size_t length) return NULL; } +static bool MOD_AllocMesh(model_t *model, maliasmesh_t *mesh) +{ + if (!(mesh->verts = MOD_GpuMalloc(sizeof(mesh->verts[0]) * mesh->numverts * model->numframes))) + return false; + if (!(mesh->tcoords = MOD_GpuMalloc(sizeof(mesh->tcoords[0]) * mesh->numverts))) + return false; + if (!(mesh->indices = MOD_GpuMalloc(sizeof(mesh->indices[0]) * mesh->numindices))) + return false; + if (!mesh->numskins) + return true; + if (!(mesh->skins = MOD_CpuMalloc(sizeof(mesh->skins[0]) * mesh->numskins))) + return false; +#if USE_MD5 + if (!(mesh->skinnames = MOD_CpuMalloc(sizeof(mesh->skinnames[0]) * mesh->numskins))) + return false; +#endif + return true; +} + static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) { dmd2header_t header; @@ -267,7 +315,7 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) maliasvert_t *dst_vert; maliastc_t *dst_tc; maliasmesh_t *mesh; - int i, j, k, val, ret; + int i, j, k, val; uint16_t remap[TESS_MAX_INDICES]; uint16_t vertIndices[TESS_MAX_INDICES]; uint16_t tcIndices[TESS_MAX_INDICES]; @@ -359,25 +407,22 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) return Q_ERR_INVALID_FORMAT; } - Hunk_Begin(&model->hunk, 0x400000); + Hunk_Begin(&model->hunk, gl_static.hunk_maxsize); model->type = MOD_ALIAS; model->nummeshes = 1; model->numframes = header.num_frames; - OOM_CHECK(model->meshes = MOD_Malloc(sizeof(model->meshes[0]))); - OOM_CHECK(model->frames = MOD_Malloc(header.num_frames * sizeof(model->frames[0]))); + model->meshes = MOD_CpuMalloc(sizeof(model->meshes[0])); + model->frames = MOD_CpuMalloc(sizeof(model->frames[0]) * header.num_frames); + if (!model->meshes || !model->frames) + return Q_ERR(ENOMEM); mesh = model->meshes; mesh->numtris = numindices / 3; mesh->numindices = numindices; mesh->numverts = numverts; mesh->numskins = header.num_skins; - OOM_CHECK(mesh->verts = MOD_Malloc(numverts * header.num_frames * sizeof(mesh->verts[0]))); - OOM_CHECK(mesh->tcoords = MOD_Malloc(numverts * sizeof(mesh->tcoords[0]))); - OOM_CHECK(mesh->indices = MOD_Malloc(numindices * sizeof(mesh->indices[0]))); - OOM_CHECK(mesh->skins = MOD_Malloc(header.num_skins * sizeof(mesh->skins[0]))); -#if USE_MD5 - OOM_CHECK(mesh->skinnames = MOD_Malloc(header.num_skins * sizeof(mesh->skinnames[0]))); -#endif + if (!MOD_AllocMesh(model, mesh)) + return Q_ERR(ENOMEM); if (mesh->numtris != header.num_tris) Com_DPrintf("%s has %d bad triangles\n", model->name, header.num_tris - mesh->numtris); @@ -394,10 +439,8 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) #else maliasskinname_t skinname; #endif - if (!Q_memccpy(skinname, src_skin, 0, sizeof(maliasskinname_t))) { - ret = Q_ERR_STRING_TRUNCATED; - goto fail; - } + if (!Q_memccpy(skinname, src_skin, 0, sizeof(maliasskinname_t))) + return Q_ERR_STRING_TRUNCATED; mesh->skins[i] = IMG_Find(skinname, IT_SKIN, IF_NONE); src_skin += MD2_MAX_SKINNAME; } @@ -466,12 +509,7 @@ static int MOD_LoadMD2(model_t *model, const void *rawdata, size_t length) dst_frame++; } - Hunk_End(&model->hunk); return Q_ERR_SUCCESS; - -fail: - Hunk_Free(&model->hunk); - return ret; } #if USE_MD3 @@ -510,7 +548,7 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, maliastc_t *dst_tc; glIndex_t *dst_idx; uint32_t index; - int i, j, k, ret; + int i, j, k; const char *err; if (length < sizeof(header)) @@ -530,13 +568,8 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, mesh->numindices = header.num_tris * 3; mesh->numverts = header.num_verts; mesh->numskins = header.num_skins; - OOM_CHECK(mesh->verts = MOD_Malloc(sizeof(mesh->verts[0]) * header.num_verts * model->numframes)); - OOM_CHECK(mesh->tcoords = MOD_Malloc(sizeof(mesh->tcoords[0]) * header.num_verts)); - OOM_CHECK(mesh->indices = MOD_Malloc(sizeof(mesh->indices[0]) * header.num_tris * 3)); - OOM_CHECK(mesh->skins = MOD_Malloc(sizeof(mesh->skins[0]) * header.num_skins)); -#if USE_MD5 - OOM_CHECK(mesh->skinnames = MOD_Malloc(sizeof(mesh->skinnames[0]) * header.num_skins)); -#endif + if (!MOD_AllocMesh(model, mesh)) + return Q_ERR(ENOMEM); // load all skins src_skin = (dmd3skin_t *)(rawdata + header.ofs_skins); @@ -598,9 +631,6 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, *offset_p = header.meshsize; return Q_ERR_SUCCESS; - -fail: - return ret; } static const char *MOD_ValidateMD3(const dmd3header_t *header, size_t length) @@ -645,12 +675,14 @@ static int MOD_LoadMD3(model_t *model, const void *rawdata, size_t length) return Q_ERR_INVALID_FORMAT; } - Hunk_Begin(&model->hunk, 0x400000); + Hunk_Begin(&model->hunk, gl_static.hunk_maxsize); model->type = MOD_ALIAS; model->numframes = header.num_frames; model->nummeshes = header.num_meshes; - OOM_CHECK(model->meshes = MOD_Malloc(sizeof(model->meshes[0]) * header.num_meshes)); - OOM_CHECK(model->frames = MOD_Malloc(sizeof(model->frames[0]) * header.num_frames)); + model->meshes = MOD_CpuMalloc(sizeof(model->meshes[0]) * header.num_meshes); + model->frames = MOD_CpuMalloc(sizeof(model->frames[0]) * header.num_frames); + if (!model->meshes || !model->frames) + return Q_ERR(ENOMEM); // load all frames src_frame = (dmd3frame_t *)((byte *)rawdata + header.ofs_frames); @@ -670,7 +702,7 @@ static int MOD_LoadMD3(model_t *model, const void *rawdata, size_t length) for (i = 0; i < header.num_meshes; i++) { ret = MOD_LoadMD3Mesh(model, &model->meshes[i], src_mesh, remaining, &offset); if (ret) - goto fail; + return ret; src_mesh += offset; remaining -= offset; } @@ -689,12 +721,7 @@ static int MOD_LoadMD3(model_t *model, const void *rawdata, size_t length) dst_frame++; } - Hunk_End(&model->hunk); return Q_ERR_SUCCESS; - -fail: - Hunk_Free(&model->hunk); - return ret; } #endif @@ -722,9 +749,9 @@ static void MD5_ParseError(const char *text) longjmp(md5_jmpbuf, -1); } -static void *MD5_Malloc(model_t *model, size_t size) +static void *MD5_GpuMalloc(model_t *model, size_t size) { - void *ptr = Hunk_TryAlloc(&model->skeleton_hunk, size, 64); + void *ptr = MOD_GpuMalloc(size); if (!ptr) { Com_SetLastError("Out of memory"); longjmp(md5_jmpbuf, -1); @@ -732,6 +759,11 @@ static void *MD5_Malloc(model_t *model, size_t size) return ptr; } +static void *MD5_CpuMalloc(model_t *model, size_t size) +{ + return gl_static.use_gpu_lerp ? R_Mallocz(size) : MD5_GpuMalloc(model, size); +} + static void MD5_ParseExpect(const char **buffer, const char *expect) { char *token = COM_Parse(buffer); @@ -893,10 +925,7 @@ static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "MD5Version"); MD5_ParseExpect(&s, "10"); - // allocate data storage, now that we're definitely an MD5 - Hunk_Begin(&model->skeleton_hunk, 0x800000); - - model->skeleton = mdl = MD5_Malloc(model, sizeof(*mdl)); + model->skeleton = mdl = MD5_CpuMalloc(model, sizeof(*mdl)); MD5_ParseExpect(&s, "commandline"); COM_SkipToken(&s); @@ -927,7 +956,7 @@ static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "}"); - mdl->meshes = MD5_Malloc(model, mdl->num_meshes * sizeof(mdl->meshes[0])); + mdl->meshes = MD5_CpuMalloc(model, mdl->num_meshes * sizeof(mdl->meshes[0])); for (i = 0; i < mdl->num_meshes; i++) { md5_mesh_t *mesh = &mdl->meshes[i]; @@ -939,8 +968,8 @@ static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "numverts"); mesh->num_verts = MD5_ParseUint(&s, 0, TESS_MAX_VERTICES); - mesh->vertices = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->vertices[0])); - mesh->tcoords = MD5_Malloc(model, mesh->num_verts * sizeof(mesh->tcoords [0])); + mesh->vertices = MD5_GpuMalloc(model, mesh->num_verts * sizeof(mesh->vertices[0])); + mesh->tcoords = MD5_GpuMalloc(model, mesh->num_verts * sizeof(mesh->tcoords [0])); for (j = 0; j < mesh->num_verts; j++) { MD5_ParseExpect(&s, "vert"); @@ -960,7 +989,7 @@ static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "numtris"); uint32_t num_tris = MD5_ParseUint(&s, 0, TESS_MAX_INDICES / 3); - mesh->indices = MD5_Malloc(model, num_tris * 3 * sizeof(mesh->indices[0])); + mesh->indices = MD5_GpuMalloc(model, num_tris * 3 * sizeof(mesh->indices[0])); mesh->num_indices = num_tris * 3; for (j = 0; j < num_tris; j++) { @@ -972,8 +1001,8 @@ static bool MD5_ParseMesh(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "numweights"); mesh->num_weights = MD5_ParseUint(&s, 0, MD5_MAX_WEIGHTS); - mesh->weights = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->weights [0])); - mesh->jointnums = MD5_Malloc(model, mesh->num_weights * sizeof(mesh->jointnums[0])); + mesh->weights = MD5_GpuMalloc(model, mesh->num_weights * sizeof(mesh->weights [0])); + mesh->jointnums = MD5_GpuMalloc(model, mesh->num_weights * sizeof(mesh->jointnums[0])); for (j = 0; j < mesh->num_weights; j++) { MD5_ParseExpect(&s, "weight"); @@ -1255,7 +1284,7 @@ static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "}"); - mdl->skeleton_frames = MD5_Malloc(model, sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints); + mdl->skeleton_frames = MD5_CpuMalloc(model, sizeof(mdl->skeleton_frames[0]) * mdl->num_frames * mdl->num_joints); // initialize scales for (i = 0; i < mdl->num_frames * mdl->num_joints; i++) @@ -1311,8 +1340,11 @@ static bool MD5_LoadSkins(model_t *model) md5_model_t *mdl = model->skeleton; const maliasmesh_t *mesh = &model->meshes[0]; + if (!mesh->numskins) + return true; + mdl->num_skins = mesh->numskins; - mdl->skins = Hunk_TryAlloc(&model->skeleton_hunk, sizeof(mdl->skins[0]) * mdl->num_skins, 64); + mdl->skins = MOD_CpuMalloc(sizeof(mdl->skins[0]) * mdl->num_skins); if (!mdl->skins) { Com_EPrintf("Out of memory for MD5 skins\n"); return false; @@ -1338,6 +1370,16 @@ static bool MD5_LoadSkins(model_t *model) return true; } +static void MD5_Free(md5_model_t *mdl) +{ + if (!mdl || !gl_static.use_gpu_lerp) + return; + Z_Free(mdl->meshes); + Z_Free(mdl->skeleton_frames); + Z_Free(mdl->skins); + Z_Free(mdl); +} + static void MOD_LoadMD5(model_t *model) { char model_name[MAX_QPATH], base_path[MAX_QPATH]; @@ -1353,6 +1395,8 @@ static void MOD_LoadMD5(model_t *model) if (!FS_FileExists(mesh_path) || !FS_FileExists(anim_path)) return; + size_t watermark = model->hunk.cursize; + if (!MD5_LoadFile(model, mesh_path, MD5_ParseMesh)) goto fail; if (!MD5_LoadFile(model, anim_path, MD5_ParseAnim)) @@ -1360,12 +1404,12 @@ static void MOD_LoadMD5(model_t *model) if (!MD5_LoadSkins(model)) goto fail; - Hunk_End(&model->skeleton_hunk); return; fail: + MD5_Free(model->skeleton); model->skeleton = NULL; - Hunk_Free(&model->skeleton_hunk); + Hunk_FreeToWatermark(&model->hunk, watermark); } #endif // USE_MD5 @@ -1401,68 +1445,46 @@ static void MOD_Reference(model_t *model) model->registration_sequence = r_registration_sequence; } -static void MOD_UploadBuffer(model_t *model) -{ - size_t verts_size = 0; - size_t index_size = 0; - int i; - - for (i = 0; i < model->nummeshes; i++) { - const maliasmesh_t *mesh = &model->meshes[i]; - verts_size += sizeof(mesh->tcoords[0]) * mesh->numverts; - index_size += sizeof(mesh->indices[0]) * mesh->numindices; - } - -#if USE_MD5 - const md5_model_t *skel = model->skeleton; - if (skel) { - for (i = 0; i < skel->num_meshes; i++) { - const md5_mesh_t *mesh = &skel->meshes[i]; - verts_size += sizeof(mesh->tcoords[0]) * mesh->num_verts; - index_size += sizeof(mesh->indices[0]) * mesh->num_indices; - } - } -#endif +#define FIXUP_OFFSET(ptr) ((ptr) = (void *)((uintptr_t)(ptr) - base)) +// upload hunk to GPU and free it +static bool MOD_UploadBuffer(model_t *model) +{ GL_ClearErrors(); - qglGenBuffers(1, &model->buffer); GL_BindBuffer(GL_ARRAY_BUFFER, model->buffer); - qglBufferData(GL_ARRAY_BUFFER, verts_size + index_size, NULL, GL_STATIC_DRAW); - Com_DDPrintf("%s: %zu bytes buffer\n", model->name, verts_size + index_size); + qglBufferData(GL_ARRAY_BUFFER, model->hunk.cursize, model->hunk.base, GL_STATIC_DRAW); + if (GL_ShowErrors(__func__)) + return false; - size_t verts_offset = 0; - size_t index_offset = verts_size; + const uintptr_t base = (uintptr_t)model->hunk.base; - for (i = 0; i < model->nummeshes; i++) { + for (int i = 0; i < model->nummeshes; i++) { maliasmesh_t *mesh = &model->meshes[i]; - verts_size = sizeof(mesh->tcoords[0]) * mesh->numverts; - index_size = sizeof(mesh->indices[0]) * mesh->numindices; - qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); - qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); - mesh->tcoords = (maliastc_t *)verts_offset; - mesh->indices = (glIndex_t *)index_offset; - verts_offset += verts_size; - index_offset += index_size; + FIXUP_OFFSET(mesh->verts); + FIXUP_OFFSET(mesh->tcoords); + FIXUP_OFFSET(mesh->indices); } #if USE_MD5 + const md5_model_t *skel = model->skeleton; if (skel) { - for (i = 0; i < skel->num_meshes; i++) { + for (int i = 0; i < skel->num_meshes; i++) { md5_mesh_t *mesh = &skel->meshes[i]; - verts_size = sizeof(mesh->tcoords[0]) * mesh->num_verts; - index_size = sizeof(mesh->indices[0]) * mesh->num_indices; - qglBufferSubData(GL_ARRAY_BUFFER, verts_offset, verts_size, mesh->tcoords); - qglBufferSubData(GL_ARRAY_BUFFER, index_offset, index_size, mesh->indices); - mesh->tcoords = (maliastc_t *)verts_offset; - mesh->indices = (glIndex_t *)index_offset; - verts_offset += verts_size; - index_offset += index_size; + FIXUP_OFFSET(mesh->vertices); + FIXUP_OFFSET(mesh->tcoords); + FIXUP_OFFSET(mesh->indices); + FIXUP_OFFSET(mesh->weights); + FIXUP_OFFSET(mesh->jointnums); } } #endif - GL_ShowErrors(__func__); + size_t mapped = model->hunk.mapped; + Hunk_Free(&model->hunk); + model->hunk.mapped = mapped; // for statistics + + return true; } qhandle_t R_RegisterModel(const char *name) @@ -1554,7 +1576,7 @@ qhandle_t R_RegisterModel(const char *name) FS_FreeFile(rawdata); if (ret < 0) { - memset(model, 0, sizeof(*model)); + MOD_Free(model); goto fail1; } @@ -1565,8 +1587,13 @@ qhandle_t R_RegisterModel(const char *name) MOD_LoadMD5(model); #endif - if (model->type == MOD_ALIAS && !(gl_config.caps & QGL_CAP_CLIENT_VA)) - MOD_UploadBuffer(model); + Hunk_End(&model->hunk); + + if (model->type == MOD_ALIAS && gl_static.use_gpu_lerp && !MOD_UploadBuffer(model)) { + MOD_Free(model); + ret = Q_ERR_LIBRARY_ERROR; + goto fail1; + } done: index = (model - r_models) + 1; @@ -1597,6 +1624,49 @@ model_t *MOD_ForHandle(qhandle_t h) void MOD_Init(void) { Q_assert(!r_numModels); + + // set defaults + gl_static.use_gpu_lerp = false; + gl_static.hunk_align = 64; + gl_static.hunk_maxsize = MOD_MAXSIZE_CPU; + + cvar_t *gl_gpulerp = Cvar_Get("gl_gpulerp", "1", 0); + gl_gpulerp->flags &= ~CVAR_FILES; + + if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { + // MUST use GPU lerp if using core profile + Q_assert(gl_static.use_shaders); + gl_static.use_gpu_lerp = true; + } else if (gl_static.use_shaders) { + // restrict `auto' to GL 4.3 and higher + int minval = 1 + !(gl_config.caps & QGL_CAP_SHADER_STORAGE); + gl_static.use_gpu_lerp = gl_gpulerp->integer >= minval; + gl_gpulerp->flags |= CVAR_FILES; + } + + // can reserve more space if using GPU lerp + if (gl_static.use_gpu_lerp) + gl_static.hunk_maxsize = MOD_MAXSIZE_GPU; + +#if USE_MD5 + // prefer shader storage, but support buffer textures as fallback. + // if neither are supported and GPU lerp is enabled, disable MD5. + if (gl_static.use_gpu_lerp && gl_md5_load->integer) { + if (gl_config.caps & QGL_CAP_SHADER_STORAGE) { + gl_static.hunk_align = max(16, gl_config.ssbo_align); + } else if (!(gl_config.caps & QGL_CAP_BUFFER_TEXTURE)) { + Com_WPrintf("Animating MD5 models on GPU is not supported " + "on this system. MD5 models will be disabled.\n"); + Cvar_Set("gl_md5_load", "0"); + } + } +#endif + + Com_DPrintf("GPU lerp %s\n", gl_static.use_gpu_lerp ? + (gl_config.caps & QGL_CAP_SHADER_STORAGE) ? "enabled (shader storage)" : + (gl_config.caps & QGL_CAP_BUFFER_TEXTURE) ? "enabled (buffer texture)" : + "enabled" : "disabled"); + Cmd_AddCommand("modellist", MOD_List_f); } diff --git a/src/refresh/qgl.c b/src/refresh/qgl.c index 590ecf370..42ba5acf1 100644 --- a/src/refresh/qgl.c +++ b/src/refresh/qgl.c @@ -291,10 +291,24 @@ static const glsection_t sections[] = { .caps = QGL_CAP_TEXTURE_MAX_LEVEL | QGL_CAP_TEXTURE_NON_POWER_OF_TWO, .functions = (const glfunction_t []) { QGL_FN(BindBufferBase), + QGL_FN(BindBufferRange), QGL_FN(BindVertexArray), QGL_FN(DeleteVertexArrays), QGL_FN(GenVertexArrays), QGL_FN(GetStringi), + QGL_FN(VertexAttribIPointer), + { NULL } + } + }, + + // GL 3.1 + // ES 3.2 + { + .ver_gl = QGL_VER(3, 1), + .ver_es = QGL_VER(3, 2), + .caps = QGL_CAP_BUFFER_TEXTURE, + .functions = (const glfunction_t []) { + QGL_FN(TexBuffer), { NULL } } }, @@ -339,6 +353,14 @@ static const glsection_t sections[] = { } }, + // GL 4.3 + // ES 3.1 + { + .ver_gl = QGL_VER(4, 3), + .ver_es = QGL_VER(3, 1), + .caps = QGL_CAP_SHADER_STORAGE, + }, + // GL 4.4 { .ver_gl = QGL_VER(4, 4), diff --git a/src/refresh/qgl.h b/src/refresh/qgl.h index d7bfb8325..b66e11547 100644 --- a/src/refresh/qgl.h +++ b/src/refresh/qgl.h @@ -139,6 +139,7 @@ QGLAPI void (APIENTRYP qglVertexAttribPointer)(GLuint index, GLint size, GLenum // GL 3.0 QGLAPI void (APIENTRYP qglBindBufferBase)(GLenum target, GLuint index, GLuint buffer); +QGLAPI void (APIENTRYP qglBindBufferRange)(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size); QGLAPI void (APIENTRYP qglBindFramebuffer)(GLenum target, GLuint framebuffer); QGLAPI void (APIENTRYP qglBindRenderbuffer)(GLenum target, GLuint renderbuffer); QGLAPI void (APIENTRYP qglBindVertexArray)(GLuint array); @@ -155,10 +156,12 @@ QGLAPI void (APIENTRYP qglGenerateMipmap)(GLenum target); QGLAPI void (APIENTRYP qglGetFramebufferAttachmentParameteriv)(GLenum target, GLenum attachment, GLenum pname, GLint *params); QGLAPI const GLubyte *(APIENTRYP qglGetStringi)(GLenum name, GLuint index); QGLAPI void (APIENTRYP qglRenderbufferStorage)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); +QGLAPI void (APIENTRYP qglVertexAttribIPointer)(GLuint index, GLint size, GLenum type, GLsizei stride, const void *pointer); // GL 3.1 QGLAPI void (APIENTRYP qglGetActiveUniformBlockiv)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint *params); QGLAPI GLuint (APIENTRYP qglGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); +QGLAPI void (APIENTRYP qglTexBuffer)(GLenum target, GLenum internalformat, GLuint buffer); QGLAPI void (APIENTRYP qglUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); // GL 4.1 diff --git a/src/refresh/shader.c b/src/refresh/shader.c index a931d2f76..302bbbac2 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -24,8 +24,16 @@ with this program; if not, write to the Free Software Foundation, Inc., #define GLSL(x) SZ_Write(buf, CONST_STR_LEN(#x "\n")); #define GLSF(x) SZ_Write(buf, CONST_STR_LEN(x)) -static void write_header(sizebuf_t *buf) +static void write_header(sizebuf_t *buf, glStateBits_t bits) { +#if USE_MD5 + if (bits & GLS_MESH_MD5 && gl_config.caps & QGL_CAP_SHADER_STORAGE) { + if (gl_config.ver_es) + GLSF("#version 310 es\n"); + else + GLSF("#version 430\n"); + } else +#endif if (gl_config.ver_es) { GLSF("#version 300 es\n"); } else if (gl_config.ver_sl >= QGL_VER(1, 40)) { @@ -36,18 +44,39 @@ static void write_header(sizebuf_t *buf) } } -static void write_block(sizebuf_t *buf) +static void write_block(sizebuf_t *buf, glStateBits_t bits) { GLSF("layout(std140) uniform u_block {\n"); + GLSL(mat4 m_vp;); + + if (bits & GLS_MESH_ANY) { + GLSL( + vec3 u_old_scale; + vec3 u_new_scale; + vec3 u_translate; + vec3 u_shadedir; + vec4 u_color; + vec4 pad_0; + float pad_1; + float pad_2; + float pad_3; + uint u_weight_ofs; + uint u_jointnum_ofs; + float u_shellscale; + float u_backlerp; + float u_frontlerp; + ) + } else { + GLSL(mat4 m_sky[2];) + } + GLSL( - mat4 m_vp; - mat4 m_sky[2]; float u_time; float u_modulate; float u_add; float u_intensity; float u_intensity2; - float pad_1; + float pad_4; vec2 w_amp; vec2 w_phase; vec2 u_scroll; @@ -55,10 +84,201 @@ static void write_block(sizebuf_t *buf) GLSF("};\n"); } +static void write_shadedot(sizebuf_t *buf) +{ + GLSL( + float shadedot(vec3 normal) { + float d = dot(normal, u_shadedir); + if (d < 0.0) + d *= 0.3; + return d + 1.0; + } + ) +} + +#if USE_MD5 +static void write_skel_shader(sizebuf_t *buf, glStateBits_t bits) +{ + GLSL( + struct Joint { + vec4 pos; + mat3x3 axis; + }; + layout(std140) uniform Skeleton { + Joint u_joints[256]; + }; + ) + + if (gl_config.caps & QGL_CAP_SHADER_STORAGE) { + GLSL( + layout(std430, binding = 0) readonly buffer Weights { + vec4 b_weights[]; + }; + + layout(std430, binding = 1) readonly buffer JointNums { + uint b_jointnums[]; + }; + ) + } else { + GLSL( + uniform samplerBuffer u_weights; + uniform usamplerBuffer u_jointnums; + ) + } + + GLSL( + in vec2 a_tc; + in vec3 a_norm; + in uvec2 a_vert; + + out vec2 v_tc; + out vec4 v_color; + ) + + if (bits & GLS_MESH_SHADE) + write_shadedot(buf); + + GLSF("void main() {\n"); + GLSL( + vec3 out_pos = vec3(0.0); + vec3 out_norm = vec3(0.0); + + uint start = a_vert[0]; + uint count = a_vert[1]; + ) + + GLSF("for (uint i = start; i < start + count; i++) {\n"); + if (gl_config.caps & QGL_CAP_SHADER_STORAGE) { + GLSL( + uint jointnum = b_jointnums[i / 4U]; + jointnum >>= (i & 3U) * 8U; + jointnum &= 255U; + + vec4 weight = b_weights[i]; + ) + } else { + GLSL( + uint jointnum = texelFetch(u_jointnums, int(u_jointnum_ofs + i)).r; + vec4 weight = texelFetch(u_weights, int(u_weight_ofs + i)); + ) + } + GLSL( + Joint joint = u_joints[jointnum]; + + vec3 wv = joint.pos.xyz + (weight.xyz * joint.axis) * joint.pos.w; + out_pos += wv * weight.w; + + out_norm += a_norm * joint.axis * weight.w; + ) + GLSF("}\n"); + + GLSL(v_tc = a_tc;) + + if (bits & GLS_MESH_SHADE) + GLSL(v_color = vec4(u_color.rgb * shadedot(out_norm), u_color.a);) + else + GLSL(v_color = u_color;) + + if (bits & GLS_MESH_SHELL) + GLSL(out_pos += out_norm * u_shellscale;) + + GLSL(gl_Position = m_vp * vec4(out_pos, 1.0);) + GLSF("}\n"); +} +#endif + +static void write_getnormal(sizebuf_t *buf) +{ + GLSL( + vec3 get_normal(int norm) { + const float pi = 3.14159265358979323846; + const float scale = pi * (2.0 / 255.0); + float lat = float( uint(norm) & 255U) * scale; + float lng = float((uint(norm) >> 8) & 255U) * scale; + return vec3( + sin(lat) * cos(lng), + sin(lat) * sin(lng), + cos(lat) + ); + } + ) +} + +static void write_mesh_shader(sizebuf_t *buf, glStateBits_t bits) +{ + GLSL( + in vec2 a_tc; + in ivec4 a_new_pos; + ) + + if (bits & GLS_MESH_LERP) + GLSL(in ivec4 a_old_pos;) + + GLSL( + out vec2 v_tc; + out vec4 v_color; + ) + + if (bits & (GLS_MESH_SHELL | GLS_MESH_SHADE)) + write_getnormal(buf); + + if (bits & GLS_MESH_SHADE) + write_shadedot(buf); + + GLSF("void main() {\n"); + GLSL(v_tc = a_tc;) + + if (bits & GLS_MESH_LERP) { + if (bits & (GLS_MESH_SHELL | GLS_MESH_SHADE)) + GLSL( + vec3 old_norm = get_normal(a_old_pos.w); + vec3 new_norm = get_normal(a_new_pos.w); + ) + + GLSL(vec3 pos = vec3(a_old_pos.xyz) * u_old_scale + vec3(a_new_pos.xyz) * u_new_scale + u_translate;) + + if (bits & GLS_MESH_SHELL) + GLSL(pos += normalize((old_norm * u_backlerp + new_norm * u_frontlerp)) * u_shellscale;) + + if (bits & GLS_MESH_SHADE) + GLSL(v_color = vec4(u_color.rgb * (shadedot(old_norm) * u_backlerp + shadedot(new_norm) * u_frontlerp), u_color.a);) + else + GLSL(v_color = u_color;) + } else { + if (bits & (GLS_MESH_SHELL | GLS_MESH_SHADE)) + GLSL(vec3 norm = get_normal(a_new_pos.w);) + + GLSL(vec3 pos = vec3(a_new_pos.xyz) * u_new_scale + u_translate;) + + if (bits & GLS_MESH_SHELL) + GLSL(pos += norm * u_shellscale;) + + if (bits & GLS_MESH_SHADE) + GLSL(v_color = vec4(u_color.rgb * shadedot(norm), u_color.a);) + else + GLSL(v_color = u_color;) + } + + GLSL(gl_Position = m_vp * vec4(pos, 1.0);) + GLSF("}\n"); +} + static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) { - write_header(buf); - write_block(buf); + write_header(buf, bits); + write_block(buf, bits); + +#if USE_MD5 + if (bits & GLS_MESH_MD5) { + write_skel_shader(buf, bits); + return; + } +#endif + + if (bits & GLS_MESH_MD2) { + write_mesh_shader(buf, bits); + return; + } GLSL(in vec4 a_pos;) if (bits & GLS_SKY_MASK) { @@ -101,13 +321,13 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) { - write_header(buf); + write_header(buf, bits); if (gl_config.ver_es) GLSL(precision mediump float;) if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_SKY_MASK)) - write_block(buf); + write_block(buf, bits); if (bits & GLS_CLASSIC_SKY) { GLSL( @@ -259,13 +479,27 @@ static GLuint create_and_use_program(glStateBits_t bits) qglAttachShader(program, shader_v); qglAttachShader(program, shader_f); - qglBindAttribLocation(program, VERT_ATTR_POS, "a_pos"); - if (!(bits & GLS_SKY_MASK)) - qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); - if (bits & GLS_LIGHTMAP_ENABLE) - qglBindAttribLocation(program, VERT_ATTR_LMTC, "a_lmtc"); - if (!(bits & GLS_TEXTURE_REPLACE)) - qglBindAttribLocation(program, VERT_ATTR_COLOR, "a_color"); +#if USE_MD5 + if (bits & GLS_MESH_MD5) { + qglBindAttribLocation(program, VERT_ATTR_MESH_TC, "a_tc"); + qglBindAttribLocation(program, VERT_ATTR_MESH_NORM, "a_norm"); + qglBindAttribLocation(program, VERT_ATTR_MESH_VERT, "a_vert"); + } else +#endif + if (bits & GLS_MESH_MD2) { + qglBindAttribLocation(program, VERT_ATTR_MESH_TC, "a_tc"); + if (bits & GLS_MESH_LERP) + qglBindAttribLocation(program, VERT_ATTR_MESH_OLD_POS, "a_old_pos"); + qglBindAttribLocation(program, VERT_ATTR_MESH_NEW_POS, "a_new_pos"); + } else { + qglBindAttribLocation(program, VERT_ATTR_POS, "a_pos"); + if (!(bits & GLS_SKY_MASK)) + qglBindAttribLocation(program, VERT_ATTR_TC, "a_tc"); + if (bits & GLS_LIGHTMAP_ENABLE) + qglBindAttribLocation(program, VERT_ATTR_LMTC, "a_lmtc"); + if (!(bits & GLS_TEXTURE_REPLACE)) + qglBindAttribLocation(program, VERT_ATTR_COLOR, "a_color"); + } qglLinkProgram(program); @@ -300,10 +534,27 @@ static GLuint create_and_use_program(glStateBits_t bits) return program; } - qglUniformBlockBinding(program, index, 0); + qglUniformBlockBinding(program, index, UBO_UNIFORMS); + +#if USE_MD5 + if (bits & GLS_MESH_MD5) { + index = qglGetUniformBlockIndex(program, "Skeleton"); + if (index == GL_INVALID_INDEX) { + Com_EPrintf("Skeleton block not found\n"); + return program; + } + qglUniformBlockBinding(program, index, UBO_SKELETON); + } +#endif qglUseProgram(program); +#if USE_MD5 + if (bits & GLS_MESH_MD5 && !(gl_config.caps & QGL_CAP_SHADER_STORAGE)) { + qglUniform1i(qglGetUniformLocation(program, "u_weights"), TMU_SKEL_WEIGHTS); + qglUniform1i(qglGetUniformLocation(program, "u_jointnums"), TMU_SKEL_JOINTNUMS); + } +#endif if (bits & GLS_CLASSIC_SKY) { qglUniform1i(qglGetUniformLocation(program, "u_texture1"), TMU_TEXTURE); qglUniform1i(qglGetUniformLocation(program, "u_texture2"), TMU_LIGHTMAP); @@ -385,6 +636,7 @@ static void shader_color(GLfloat r, GLfloat g, GLfloat b, GLfloat a) static void shader_load_uniforms(void) { + GL_BindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), &gls.u_block, GL_DYNAMIC_DRAW); c.uniformUploads++; } @@ -466,10 +718,21 @@ static void shader_init(void) gl_static.programs = HashMap_TagCreate(glStateBits_t, GLuint, HashInt32, NULL, TAG_RENDERER); qglGenBuffers(1, &gl_static.uniform_buffer); - qglBindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); - qglBindBufferBase(GL_UNIFORM_BUFFER, 0, gl_static.uniform_buffer); + GL_BindBuffer(GL_UNIFORM_BUFFER, gl_static.uniform_buffer); + qglBindBufferBase(GL_UNIFORM_BUFFER, UBO_UNIFORMS, gl_static.uniform_buffer); qglBufferData(GL_UNIFORM_BUFFER, sizeof(gls.u_block), NULL, GL_DYNAMIC_DRAW); +#if USE_MD5 + if (gl_config.caps & QGL_CAP_SKELETON_MASK) { + qglGenBuffers(1, &gl_static.skeleton_buffer); + GL_BindBuffer(GL_UNIFORM_BUFFER, gl_static.skeleton_buffer); + qglBindBufferBase(GL_UNIFORM_BUFFER, UBO_SKELETON, gl_static.skeleton_buffer); + + if ((gl_config.caps & QGL_CAP_SKELETON_MASK) == QGL_CAP_BUFFER_TEXTURE) + qglGenTextures(2, gl_static.skeleton_tex); + } +#endif + if (gl_config.ver_gl >= QGL_VER(3, 2)) qglEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } @@ -494,6 +757,17 @@ static void shader_shutdown(void) gl_static.uniform_buffer = 0; } +#if USE_MD5 + if (gl_static.skeleton_buffer) { + qglDeleteBuffers(1, &gl_static.skeleton_buffer); + gl_static.skeleton_buffer = 0; + } + if (gl_static.skeleton_tex[0] || gl_static.skeleton_tex[1]) { + qglDeleteTextures(2, gl_static.skeleton_tex); + gl_static.skeleton_tex[0] = gl_static.skeleton_tex[1] = 0; + } +#endif + if (gl_config.ver_gl >= QGL_VER(3, 2)) qglDisable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } diff --git a/src/refresh/state.c b/src/refresh/state.c index 17d08f5b2..2e2cc93dd 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -321,8 +321,9 @@ void GL_Setup3D(bool waterwarp) void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed) { GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); - GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE); - GL_ArrayBits(GLA_VERTEX); + GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE | (gls.state_bits & GLS_MESH_MASK)); + if (gls.currentva) + GL_ArrayBits(GLA_VERTEX); GL_DepthRange(0, 0); if (qglPolygonMode) { diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 9124ac85d..b01bd9af0 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -604,7 +604,9 @@ void GL_DrawIndexed(showtris_t showtris) GL_LockArrays(tess.numverts); - if (!(gl_config.caps & QGL_CAP_CLIENT_VA)) { + if (gl_config.caps & QGL_CAP_CLIENT_VA) { + GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + } else { GL_BindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_static.index_buffer); qglBufferData(GL_ELEMENT_ARRAY_BUFFER, tess.numindices * sizeof(indices[0]), indices, GL_STREAM_DRAW); indices = NULL; From 4bbaafac17c569d91fa0f1e0df17750fb3c4135b Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 7 Oct 2024 04:09:04 +0300 Subject: [PATCH 863/974] Always use 16-bit indices for meshes. --- src/refresh/gl.h | 9 +++------ src/refresh/mesh.c | 16 ++++++++-------- src/refresh/models.c | 2 +- src/refresh/state.c | 34 ++++++++++++++++++++++------------ src/refresh/tess.c | 6 +++--- 5 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 41fca4841..c911eb2ac 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -321,7 +321,7 @@ typedef struct { int numtris; int numindices; int numskins; - glIndex_t *indices; + uint16_t *indices; maliasvert_t *verts; maliastc_t *tcoords; #if USE_MD5 @@ -377,7 +377,7 @@ typedef struct { md5_vertex_t *vertices; maliastc_t *tcoords; - glIndex_t *indices; + uint16_t *indices; md5_weight_t *weights; uint8_t *jointnums; } md5_mesh_t; @@ -762,9 +762,6 @@ static inline void GL_DepthRange(GLfloat n, GLfloat f) #define GL_Color(r, g, b, a) gl_backend->color(r, g, b, a) -#define GL_DrawTriangles(num_indices, indices) \ - qglDrawElements(GL_TRIANGLES, num_indices, QGL_INDEX_TYPE, indices) - typedef enum { SHOWTRIS_NONE = 0, SHOWTRIS_WORLD = BIT(0), @@ -780,7 +777,7 @@ void GL_BindCubemap(GLuint texnum); void GL_DeleteBuffer(GLuint buffer); void GL_CommonStateBits(glStateBits_t bits); void GL_ScrollPos(vec2_t scroll, glStateBits_t bits); -void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed); +void GL_DrawOutlines(GLsizei count, GLenum type, const void *indices); void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar); void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x); void GL_Setup2D(void); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 40c52bcd9..ee60422fc 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -433,7 +433,7 @@ static void uniform_mesh_color(float r, float g, float b, float a) } } -static void draw_celshading(const glIndex_t *indices, int num_indices) +static void draw_celshading(const uint16_t *indices, int num_indices) { if (celscale < 0.01f) return; @@ -449,7 +449,7 @@ static void draw_celshading(const glIndex_t *indices, int num_indices) qglLineWidth(gl_celshading->value * celscale); qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); qglCullFace(GL_FRONT); - GL_DrawTriangles(num_indices, indices); + qglDrawElements(GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, indices); qglCullFace(GL_BACK); qglPolygonMode(GL_FRONT_AND_BACK, GL_FILL); qglLineWidth(1); @@ -545,7 +545,7 @@ static void setup_shadow(void) GL_MultMatrix(shadowmatrix, tmp, matrix); } -static void draw_shadow(const glIndex_t *indices, int num_indices) +static void draw_shadow(const uint16_t *indices, int num_indices) { if (!drawshadow) return; @@ -570,7 +570,7 @@ static void draw_shadow(const glIndex_t *indices, int num_indices) qglEnable(GL_POLYGON_OFFSET_FILL); qglPolygonOffset(-1.0f, -2.0f); - GL_DrawTriangles(num_indices, indices); + qglDrawElements(GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, indices); qglDisable(GL_POLYGON_OFFSET_FILL); // once we have drawn something to stencil buffer, continue to clear it for @@ -623,7 +623,7 @@ static void bind_alias_arrays(const maliasmesh_t *mesh) } } -static void draw_alias_mesh(const glIndex_t *indices, int num_indices, +static void draw_alias_mesh(const uint16_t *indices, int num_indices, const maliastc_t *tcoords, int num_verts, image_t **skins, int num_skins) { @@ -657,7 +657,7 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, qglColorMask(0, 0, 0, 0); GL_LockArrays(num_verts); - GL_DrawTriangles(num_indices, indices); + qglDrawElements(GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, indices); GL_UnlockArrays(); qglColorMask(1, 1, 1, 1); @@ -693,13 +693,13 @@ static void draw_alias_mesh(const glIndex_t *indices, int num_indices, GL_LockArrays(num_verts); - GL_DrawTriangles(num_indices, indices); + qglDrawElements(GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, indices); c.trisDrawn += num_indices / 3; draw_celshading(indices, num_indices); if (gl_showtris->integer & SHOWTRIS_MESH) - GL_DrawOutlines(num_indices, indices, true); + GL_DrawOutlines(num_indices, GL_UNSIGNED_SHORT, indices); // FIXME: unlock arrays before changing matrix? draw_shadow(indices, num_indices); diff --git a/src/refresh/models.c b/src/refresh/models.c index bc812a8a4..2b5b754e8 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -546,7 +546,7 @@ static int MOD_LoadMD3Mesh(model_t *model, maliasmesh_t *mesh, uint32_t *src_idx; maliasvert_t *dst_vert; maliastc_t *dst_tc; - glIndex_t *dst_idx; + uint16_t *dst_idx; uint32_t index; int i, j, k; const char *err; diff --git a/src/refresh/state.c b/src/refresh/state.c index 2e2cc93dd..f9f802756 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -318,7 +318,7 @@ void GL_Setup3D(bool waterwarp) qglClear(GL_DEPTH_BUFFER_BIT | gl_static.stencil_buffer_bit); } -void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed) +void GL_DrawOutlines(GLsizei count, GLenum type, const void *indices) { GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); GL_StateBits(GLS_DEPTHMASK_FALSE | GLS_TEXTURE_REPLACE | (gls.state_bits & GLS_MESH_MASK)); @@ -329,22 +329,32 @@ void GL_DrawOutlines(GLsizei count, const glIndex_t *indices, bool indexed) if (qglPolygonMode) { qglPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - if (indexed) - GL_DrawTriangles(count, indices); + if (type) + qglDrawElements(GL_TRIANGLES, count, type, indices); else qglDrawArrays(GL_TRIANGLES, 0, count); qglPolygonMode(GL_FRONT_AND_BACK, GL_FILL); - } else { - GLsizei i; - - if (indexed) { - for (i = 0; i < count / 3; i++) - qglDrawElements(GL_LINE_LOOP, 3, QGL_INDEX_TYPE, &indices[i * 3]); - } else { - for (i = 0; i < count / 3; i++) - qglDrawArrays(GL_LINE_LOOP, i * 3, 3); + } else if (type) { + uintptr_t base = (uintptr_t)indices; + uintptr_t size = 0; + + switch (type) { + case GL_UNSIGNED_INT: + size = 4 * 3; + break; + case GL_UNSIGNED_SHORT: + size = 2 * 3; + break; + default: + Q_assert(!"bad type"); } + + for (int i = 0; i < count / 3; i++, base += size) + qglDrawElements(GL_LINE_LOOP, 3, type, VBO_OFS(base)); + } else { + for (int i = 0; i < count / 3; i++) + qglDrawArrays(GL_LINE_LOOP, i * 3, 3); } GL_DepthRange(0, 1); diff --git a/src/refresh/tess.c b/src/refresh/tess.c index b01bd9af0..1a8eee154 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -127,7 +127,7 @@ void GL_DrawParticles(void) qglDrawArrays(GL_TRIANGLES, 0, numverts); if (gl_showtris->integer & SHOWTRIS_FX) - GL_DrawOutlines(numverts, NULL, false); + GL_DrawOutlines(numverts, 0, NULL); GL_UnlockArrays(); } while (total); @@ -612,11 +612,11 @@ void GL_DrawIndexed(showtris_t showtris) indices = NULL; } - GL_DrawTriangles(tess.numindices, indices); + qglDrawElements(GL_TRIANGLES, tess.numindices, QGL_INDEX_TYPE, indices); c.trisDrawn += tess.numindices / 3; if (gl_showtris->integer & showtris) - GL_DrawOutlines(tess.numindices, indices, true); + GL_DrawOutlines(tess.numindices, QGL_INDEX_TYPE, indices); GL_UnlockArrays(); } From e85d1d86798fead7cfcbd0eb9cd3d664633512aa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 8 Oct 2024 02:40:12 +0300 Subject: [PATCH 864/974] Update bug report template. --- .github/ISSUE_TEMPLATE/1_bug_report.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.md b/.github/ISSUE_TEMPLATE/1_bug_report.md index 0090d1900..97014e003 100644 --- a/.github/ISSUE_TEMPLATE/1_bug_report.md +++ b/.github/ISSUE_TEMPLATE/1_bug_report.md @@ -11,8 +11,7 @@ assignees: '' Make sure the bug is reproducible with latest Q2PRO version. If you compile Q2PRO yourself, update to the latest version from git master. If you are using -prebuilt Windows binaries, update to the latest version available from -https://skuller.net/q2pro/nightly/ +prebuilt Windows binaries, update to the latest nightly build. ### Important information @@ -51,3 +50,8 @@ Provide a link to the log file created by launching `q2pro +set developer 1 If Q2PRO crashes, provide a crash report (Windows) or a backtrace (Linux). On Linux, backtrace can be created by launching Q2PRO with `gdb q2pro --args [...]` and typing `bt` after the crash. + +### Compilation issues + +If reporting a building / compilation issue, provide `meson setup` command +line and full console output. From 7fdb039132aaf2d78ff0bb6b21842ab7bf5c1ac6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 8 Oct 2024 17:06:56 +0300 Subject: [PATCH 865/974] Update INSTALL.md. Mention that pkg-config search path must be updated in cross files. Closes #360. --- INSTALL.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 7c9f05291..09d678d64 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -124,11 +124,14 @@ with SIMD support: Meson needs correct cross build definition file for compilation. Example cross-files can be found in `.ci` subdirectory (available in git -repository, but not source tarball). +repository, but not source tarball). Note that these cross-files are specific +to CI scripts and shouldn't be used directly (you'll need, at least, to +customize default `pkg-config` search path). Refer to Meson documentation for +more info. Setup build directory: - meson setup --cross-file .ci/x86_64-w64-mingw32.txt -Dwrap_mode=forcefallback builddir + meson setup --cross-file x86_64-w64-mingw32.txt -Dwrap_mode=forcefallback builddir Build: From 46b1bfc9fe9a00a97bbc910588db28b79c873412 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 8 Oct 2024 17:09:56 +0300 Subject: [PATCH 866/974] Optimize skipping MD5 bounds. --- src/refresh/models.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/refresh/models.c b/src/refresh/models.c index 2b5b754e8..df05fecbd 100644 --- a/src/refresh/models.c +++ b/src/refresh/models.c @@ -1263,10 +1263,8 @@ static bool MD5_ParseAnim(model_t *model, const char *s, const char *path) MD5_ParseExpect(&s, "bounds"); MD5_ParseExpect(&s, "{"); - for (i = 0; i < mdl->num_frames * 2; i++) { - vec3_t dummy; - MD5_ParseVector(&s, dummy); - } + for (i = 0; i < mdl->num_frames * 2 * 5; i++) + COM_SkipToken(&s); MD5_ParseExpect(&s, "}"); From 25a54a6819223a4bf95b8f0f4acdc574f16d65ce Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Tue, 8 Oct 2024 21:01:46 +0300 Subject: [PATCH 867/974] Update FreeBSD VM to 14.1. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bf9a810f..38a0af4fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,11 +139,11 @@ jobs: - uses: actions/checkout@v4 - name: Build - uses: cross-platform-actions/action@v0.24.0 + uses: cross-platform-actions/action@v0.25.0 with: operating_system: freebsd architecture: x86-64 - version: '14.0' + version: '14.1' run: | sudo pkg update sudo pkg install -y git meson pkgconf openal-soft \ From 13c2c9b60bb276584ca934a9f9def522501b3c36 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 11 Oct 2024 15:52:34 +0300 Subject: [PATCH 868/974] Fix rendering error if classic sky texture is missing. Don't attempt to use cubemap as classic sky. Regression since 47656f88. --- inc/refresh/refresh.h | 2 +- src/refresh/surf.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index d3196ea4b..9302b4495 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -156,12 +156,12 @@ typedef enum { IF_OPAQUE = BIT(8), // known to be opaque IF_DEFAULT_FLARE = BIT(9), // default flare hack IF_CUBEMAP = BIT(10), // cubemap (or part of it) + IF_CLASSIC_SKY = BIT(11), // split in two halves // these flags only affect R_RegisterImage() behavior, // and are not stored in image IF_OPTIONAL = BIT(16), // don't warn if not found IF_KEEP_EXTENSION = BIT(17), // don't override extension - IF_CLASSIC_SKY = BIT(18), // split in two halves } imageflags_t; typedef enum { diff --git a/src/refresh/surf.c b/src/refresh/surf.c index bf832f990..648b5a733 100644 --- a/src/refresh/surf.c +++ b/src/refresh/surf.c @@ -574,7 +574,7 @@ static glStateBits_t statebits_for_surface(const mface_t *surf) glStateBits_t statebits = GLS_DEFAULT; if (surf->drawflags & SURF_SKY) { - if (Q_stricmpn(surf->texinfo->name, CONST_STR_LEN("n64/env/sky")) == 0) + if (surf->texinfo->image->flags & IF_CLASSIC_SKY) return GLS_TEXTURE_REPLACE | GLS_CLASSIC_SKY; else return GLS_TEXTURE_REPLACE | GLS_DEFAULT_SKY; From 61324ffc588fceabdffbe19d1f222b911ad2cf28 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Fri, 11 Oct 2024 15:53:19 +0300 Subject: [PATCH 869/974] Use VectorRotate() macro in more places. --- src/common/math.c | 8 +++----- src/refresh/world.c | 8 ++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/common/math.c b/src/common/math.c index 94f071dd2..81e13bcce 100644 --- a/src/common/math.c +++ b/src/common/math.c @@ -390,14 +390,12 @@ void SetupRotationMatrix(vec3_t matrix[3], const vec3_t dir, float degrees) void RotatePointAroundVector(vec3_t out, const vec3_t dir, const vec3_t in, float degrees) { vec3_t matrix[3]; - vec3_t tmp; + vec3_t temp; SetupRotationMatrix(matrix, dir, degrees); - VectorCopy(in, tmp); - out[0] = DotProduct(tmp, matrix[0]); - out[1] = DotProduct(tmp, matrix[1]); - out[2] = DotProduct(tmp, matrix[2]); + VectorCopy(in, temp); + VectorRotate(temp, matrix, out); } #if USE_MD5 diff --git a/src/refresh/world.c b/src/refresh/world.c index 682d39314..77618b830 100644 --- a/src/refresh/world.c +++ b/src/refresh/world.c @@ -275,9 +275,7 @@ static void GL_TransformLights(const mmodel_t *model) for (i = 0, light = glr.fd.dlights; i < glr.fd.num_dlights; i++, light++) { VectorSubtract(light->origin, glr.ent->origin, temp); - light->transformed[0] = DotProduct(temp, glr.entaxis[0]); - light->transformed[1] = DotProduct(temp, glr.entaxis[1]); - light->transformed[2] = DotProduct(temp, glr.entaxis[2]); + VectorRotate(temp, glr.entaxis, light->transformed); GL_MarkLights_r(model->headnode, light, BIT_ULL(i)); } } @@ -425,9 +423,7 @@ void GL_DrawBspModel(mmodel_t *model) } } VectorSubtract(glr.fd.vieworg, ent->origin, temp); - transformed[0] = DotProduct(temp, glr.entaxis[0]); - transformed[1] = DotProduct(temp, glr.entaxis[1]); - transformed[2] = DotProduct(temp, glr.entaxis[2]); + VectorRotate(temp, glr.entaxis, transformed); } else { VectorAdd(model->mins, ent->origin, bounds[0]); VectorAdd(model->maxs, ent->origin, bounds[1]); From 5dcc520f9cdc77f70841a521a29f931aa36e1cee Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 12 Oct 2024 22:39:45 +0300 Subject: [PATCH 870/974] Use system meson package. --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38a0af4fe..d77e21be1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,8 +58,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y gcc-mingw-w64 nasm python3-pip ninja-build - sudo python3 -m pip install meson + sudo apt-get install -y gcc-mingw-w64 nasm meson ninja-build - name: Build run: | From d54bdc6995025c272425d8c90f6912ddc303f037 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 14 Oct 2024 10:56:57 -0400 Subject: [PATCH 871/974] No longer precaching AQtion gun sounds, saves us memory and sound slots --- src/client/tent.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/tent.c b/src/client/tent.c index b0060cdd3..ebdc91d92 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -315,7 +315,9 @@ void CL_RegisterTEntSounds(void) } CL_RegisterFootsteps(); - CL_RegisterAQtionSounds(); + + // Commenting out until better support is added, else we're just wasting memory and sound slots + //CL_RegisterAQtionSounds(); cl_sfx_lightning = S_RegisterSound("weapons/tesla.wav"); cl_sfx_disrexp = S_RegisterSound("weapons/disrupthit.wav"); From c0966a90cf60e53ac56b1540364244935f316b19 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 13 Oct 2024 11:04:35 +0300 Subject: [PATCH 872/974] Allow message write buffer to overflow. With extended protocol limits, overflowing MAX_MSGLEN with single message is possible in MVD and client demo code. Allow msg_write to overflow and add explicit overflow checks to avoid crashing with fatal error. --- src/client/demo.c | 24 +++++++++++++++--------- src/client/gtv.c | 17 +++++++++++++++++ src/common/common.c | 2 +- src/common/msg.c | 11 ++++------- src/server/game.c | 3 +++ src/server/main.c | 3 +++ src/server/mvd.c | 41 +++++++++++++++++++++++++++++++++-------- src/server/mvd/client.c | 34 ++++++++++++++++++++++------------ src/server/save.c | 6 ++++++ src/server/send.c | 9 +++++++++ 10 files changed, 113 insertions(+), 37 deletions(-) diff --git a/src/client/demo.c b/src/client/demo.c index bfbd1aac2..ac6ea9ca6 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -224,7 +224,9 @@ void CL_EmitDemoFrame(void) // emit and flush frame emit_delta_frame(oldframe, &cl.frame, lastframe, FRAME_CUR); - if (cls.demo.buffer.cursize + msg_write.cursize > cls.demo.buffer.maxsize) { + if (msg_write.overflowed) { + Com_WPrintf("%s: message buffer overflowed\n", __func__); + } else if (cls.demo.buffer.cursize + msg_write.cursize > cls.demo.buffer.maxsize) { Com_DPrintf("Demo frame overflowed (%u + %u > %u)\n", cls.demo.buffer.cursize, msg_write.cursize, cls.demo.buffer.maxsize); cls.demo.frames_dropped++; @@ -850,16 +852,20 @@ void CL_EmitDemoSnapshot(void) MSG_WriteByte(svc_layout); MSG_WriteString(cl.layout); - snap = Z_Malloc(sizeof(*snap) + msg_write.cursize - 1); - snap->framenum = cls.demo.frames_read; - snap->filepos = pos; - snap->msglen = msg_write.cursize; - memcpy(snap->data, msg_write.data, msg_write.cursize); + if (msg_write.overflowed) { + Com_WPrintf("%s: message buffer overflowed\n", __func__); + } else { + snap = Z_Malloc(sizeof(*snap) + msg_write.cursize - 1); + snap->framenum = cls.demo.frames_read; + snap->filepos = pos; + snap->msglen = msg_write.cursize; + memcpy(snap->data, msg_write.data, msg_write.cursize); - cls.demo.snapshots = Z_Realloc(cls.demo.snapshots, sizeof(cls.demo.snapshots[0]) * Q_ALIGN(cls.demo.numsnapshots + 1, MIN_SNAPSHOTS)); - cls.demo.snapshots[cls.demo.numsnapshots++] = snap; + cls.demo.snapshots = Z_Realloc(cls.demo.snapshots, sizeof(cls.demo.snapshots[0]) * Q_ALIGN(cls.demo.numsnapshots + 1, MIN_SNAPSHOTS)); + cls.demo.snapshots[cls.demo.numsnapshots++] = snap; - Com_DPrintf("[%d] snaplen %u\n", cls.demo.frames_read, msg_write.cursize); + Com_DPrintf("[%d] snaplen %u\n", cls.demo.frames_read, msg_write.cursize); + } SZ_Clear(&msg_write); diff --git a/src/client/gtv.c b/src/client/gtv.c index 1c18f7c39..a281f2319 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -29,6 +29,8 @@ static byte gtv_send_buffer[MAX_GTS_MSGLEN*2]; static byte gtv_message_buffer[MAX_MSGLEN]; +static void drop_client(const char *reason); + static void build_gamestate(void) { centity_t *ent; @@ -177,6 +179,13 @@ void CL_GTV_EmitFrame(void) MSG_WriteShort(0); // end of packetentities + // check for overflow + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + drop_client("frame overflowed"); + return; + } + SZ_Write(&cls.gtv.message, msg_write.data, msg_write.cursize); SZ_Clear(&msg_write); } @@ -264,6 +273,14 @@ void CL_GTV_Resume(void) build_gamestate(); emit_gamestate(); + + // check for overflow + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + drop_client("gamestate overflowed"); + return; + } + write_message(GTS_STREAM_DATA); SZ_Clear(&msg_write); } diff --git a/src/common/common.c b/src/common/common.c index 4e3d76ca8..b20a4af22 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -530,7 +530,7 @@ void Com_Error(error_type_t code, const char *fmt, ...) // overlap with one of the arguments! memcpy(com_errorMsg, msg, len + 1); - // fix up drity message buffers + // fix up dirty message buffers MSG_Init(); // abort any console redirects diff --git a/src/common/msg.c b/src/common/msg.c index a0fe813e6..6efdb7b49 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -46,19 +46,16 @@ const usercmd_t nullUserCmd; ============= MSG_Init -Initialize default buffers, clearing allow overflow/underflow flags. - -This is the only place where writing buffer is initialized. Writing buffer is -never allowed to overflow. - -Reading buffer is reinitialized in many other places. Reinitializing will set -the allow underflow flag as appropriate. +Initialize default buffers (also called from Com_Error). +This is the only place where writing buffer is initialized. ============= */ void MSG_Init(void) { SZ_Init(&msg_read, msg_read_buffer, MAX_MSGLEN, "msg_read"); SZ_Init(&msg_write, msg_write_buffer, MAX_MSGLEN, "msg_write"); + msg_read.allowunderflow = true; + msg_write.allowoverflow = true; } diff --git a/src/server/game.c b/src/server/game.c index 204c19bf8..2f0647837 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -96,6 +96,9 @@ static void PF_Unicast(edict_t *ent, qboolean reliable) goto clear; } + if (msg_write.overflowed) + Com_Error(ERR_DROP, "%s: message buffer overflowed", __func__); + clientNum = NUM_FOR_EDICT(ent) - 1; if (clientNum < 0 || clientNum >= sv_maxclients->integer) { Com_WPrintf("%s to a non-client %d\n", __func__, clientNum); diff --git a/src/server/main.c b/src/server/main.c index 8d3c8cdd7..e3711c745 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1809,6 +1809,9 @@ static void SV_RunGameFrame(void) time_after_game = Sys_Milliseconds(); #endif + if (msg_write.overflowed) + Com_Error(ERR_DROP, "%s: message buffer overflowed", __func__); + if (msg_write.cursize) { Com_WPrintf("Game left %u bytes " "in multicast buffer, cleared.\n", diff --git a/src/server/mvd.c b/src/server/mvd.c index a4eaa8952..9cff1fae7 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -831,6 +831,13 @@ static void resume_streams(void) build_gamestate(); emit_gamestate(); + // check for overflow + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + mvd_error("gamestate overflowed"); + return; + } + FOR_EACH_ACTIVE_GTV(client) { // send gamestate write_message(client, GTS_STREAM_DATA); @@ -917,9 +924,6 @@ static bool mvd_enable(void) // don't timeout mvd.clients_active = svs.realtime; - // check for activation - check_players_activity(); - return true; } @@ -1040,7 +1044,7 @@ void SV_MvdEndFrame(void) emit_frame(); // if reliable message and frame update don't fit, kick all clients - if (mvd.message.cursize + msg_write.cursize >= MAX_MSGLEN) { + if (msg_write.overflowed || mvd.message.cursize + msg_write.cursize >= MAX_MSGLEN) { SZ_Clear(&msg_write); mvd_error("frame overflowed"); return; @@ -1573,6 +1577,11 @@ static void parse_stream_start(gtv_client_t *client) // send gamestate if active if (mvd.active) { emit_gamestate(); + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + drop_client(client, "gamestate overflowed"); + return; + } write_message(client, GTS_STREAM_DATA); SZ_Clear(&msg_write); } else { @@ -1955,7 +1964,7 @@ static void mvd_drop(gtv_serverop_t op) // something bad happened, remove all clients static void mvd_error(const char *reason) { - Com_EPrintf("Fatal MVD error: %s\n", reason); + Com_EPrintf("Fatal MVD server error: %s\n", reason); // stop recording rec_stop(); @@ -2010,6 +2019,13 @@ void SV_MvdMapChanged(void) build_gamestate(); emit_gamestate(); + // check for overflow + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + mvd_error("gamestate overflowed"); + return; + } + // send gamestate to all MVD clients FOR_EACH_ACTIVE_GTV(client) { write_message(client, GTS_STREAM_DATA); @@ -2246,11 +2262,20 @@ static void rec_start(qhandle_t demofile) magic = MVD_MAGIC; FS_Write(&magic, 4, demofile); - if (mvd.active) { - emit_gamestate(); - rec_write(); + if (!mvd.active) + return; + + emit_gamestate(); + + // check for overflow + if (msg_write.overflowed) { SZ_Clear(&msg_write); + mvd_error("gamestate overflowed"); + return; } + + rec_write(); + SZ_Clear(&msg_write); } /* diff --git a/src/server/mvd/client.c b/src/server/mvd/client.c index e876bd2d5..327a1db3d 100644 --- a/src/server/mvd/client.c +++ b/src/server/mvd/client.c @@ -626,19 +626,23 @@ static void demo_emit_snapshot(mvd_t *mvd) MSG_WriteString(mvd->layout); } - snap = MVD_Malloc(sizeof(*snap) + msg_write.cursize - 1); - snap->framenum = mvd->framenum; - snap->filepos = pos; - snap->msglen = msg_write.cursize; - memcpy(snap->data, msg_write.data, msg_write.cursize); - - if (!mvd->snapshots) - mvd->snapshots = MVD_Malloc(sizeof(mvd->snapshots[0]) * MIN_SNAPSHOTS); - else - mvd->snapshots = Z_Realloc(mvd->snapshots, sizeof(mvd->snapshots[0]) * Q_ALIGN(mvd->numsnapshots + 1, MIN_SNAPSHOTS)); - mvd->snapshots[mvd->numsnapshots++] = snap; + if (msg_write.overflowed) { + Com_WPrintf("%s: message buffer overflowed\n", __func__); + } else { + snap = MVD_Malloc(sizeof(*snap) + msg_write.cursize - 1); + snap->framenum = mvd->framenum; + snap->filepos = pos; + snap->msglen = msg_write.cursize; + memcpy(snap->data, msg_write.data, msg_write.cursize); + + if (!mvd->snapshots) + mvd->snapshots = MVD_Malloc(sizeof(mvd->snapshots[0]) * MIN_SNAPSHOTS); + else + mvd->snapshots = Z_Realloc(mvd->snapshots, sizeof(mvd->snapshots[0]) * Q_ALIGN(mvd->numsnapshots + 1, MIN_SNAPSHOTS)); + mvd->snapshots[mvd->numsnapshots++] = snap; - Com_DPrintf("[%d] snaplen %u\n", mvd->framenum, msg_write.cursize); + Com_DPrintf("[%d] snaplen %u\n", mvd->framenum, msg_write.cursize); + } SZ_Clear(&msg_write); @@ -1976,6 +1980,12 @@ void MVD_StreamedRecord_f(void) emit_gamestate(mvd); + // check for overflow + if (msg_write.overflowed) { + ret = Q_ERR(EMSGSIZE); + goto fail; + } + // write magic magic = MVD_MAGIC; ret = FS_Write(&magic, 4, f); diff --git a/src/server/save.c b/src/server/save.c index a6bac3437..b8bcaede1 100644 --- a/src/server/save.c +++ b/src/server/save.c @@ -74,6 +74,12 @@ static int write_server_file(savetype_t autosave) } MSG_WriteString(NULL); + // check for overflow + if (msg_write.overflowed) { + SZ_Clear(&msg_write); + return -1; + } + // write server state ret = FS_WriteFile("save/" SAVE_CURRENT "/server.ssv", msg_write.data, msg_write.cursize); diff --git a/src/server/send.c b/src/server/send.c index 0afb0e639..98e7eee77 100644 --- a/src/server/send.c +++ b/src/server/send.c @@ -271,6 +271,9 @@ void SV_Multicast(const vec3_t origin, multicast_t to) if (to && !origin) Com_Error(ERR_DROP, "%s: NULL origin", __func__); + if (msg_write.overflowed) + Com_Error(ERR_DROP, "%s: message buffer overflowed", __func__); + if (!msg_write.cursize) { Com_DPrintf("%s with empty data\n", __func__); return; @@ -389,6 +392,8 @@ void SV_ClientAddMessage(client_t *client, int flags) { int len; + Q_assert(!msg_write.overflowed); + if (!msg_write.cursize) { return; } @@ -755,6 +760,8 @@ static void write_datagram_old(client_t *client) } #endif + Q_assert(!msg_write.overflowed); + // send the datagram cursize = Netchan_Transmit(&client->netchan, msg_write.cursize, @@ -819,6 +826,8 @@ static void write_datagram_new(client_t *client) } #endif + Q_assert(!msg_write.overflowed); + // send the datagram cursize = Netchan_Transmit(&client->netchan, msg_write.cursize, From 70c1a334fb8cc9afb2c8622347f502813fa4ff7b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 14 Oct 2024 15:37:25 -0400 Subject: [PATCH 873/974] Fixed Espionage bugs --- src/action/a_esp.c | 32 ++++++++++++++++++++++++++++++-- src/action/a_game.c | 9 ++++++++- src/action/a_team.c | 35 +++++++++++++++++++++++++---------- src/action/g_ext.c | 14 +++++++------- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index c362178ab..4f19579e3 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1566,10 +1566,13 @@ qboolean AllTeamsHaveLeaders(void) // Only Team 1 needs a leader in ETV mode if((etv->value) && HAVE_LEADER(TEAM1)) { - //gi.dprintf("ETV team has a leader\n"); + teams[TEAM1].leader_dead = false; // Reset this in case the leader left and returned return true; } else if(atl->value && (teamsWithLeaders == teamCount)){ - //gi.dprintf("Teams with leaders is the same as the team count\n"); + teams[TEAM1].leader_dead = false; // Reset this in case the leader left and returned + teams[TEAM2].leader_dead = false; + if (teamCount == 3) + teams[TEAM3].leader_dead = false; return true; } else { return false; @@ -1729,6 +1732,27 @@ qboolean EspLeaderCheck(void) int i = 0; edict_t *newLeader; qboolean athl = AllTeamsHaveLeaders(); + int t1count = TotalPlayersOnTeam(TEAM1); + int t2count = TotalPlayersOnTeam(TEAM2); + + // Setting leader deadness to true if there's no one left on the team + // This is meant to end the round immediately if a team has no members on it + if (teamCount == 2){ + if (t1count == 0 && t2count > 0){ + teams[TEAM1].leader_dead = true; + } else if (t2count == 0 && t1count > 0){ + teams[TEAM2].leader_dead = true; + } + } else if (teamCount == 3){ + int t3count = TotalPlayersOnTeam(TEAM3); + if (t1count == 0 && t2count > 0 && t3count > 0){ + teams[TEAM1].leader_dead = true; + } else if (t2count == 0 && t1count > 0 && t3count > 0){ + teams[TEAM2].leader_dead = true; + } else if (t3count == 0 && t1count > 0 && t2count > 0){ + teams[TEAM3].leader_dead = true; + } + } // Quick sanity check in case this ever occurs as I've seen it on rare occasions for (i = TEAM1; i <= teamCount; i++) { @@ -1769,6 +1793,8 @@ qboolean EspLeaderCheck(void) if (esp_debug->value) gi.dprintf("%s: I need a random leader!\n", __func__); return true; + } else { + return false; } } @@ -2049,6 +2075,8 @@ void EspEndOfRoundCleanup(void) for (i = TEAM1; i <= teamCount; i++) { if (teams[i].leader && teams[i].leader->is_bot) teams[i].leader = NULL; + if (TotalPlayersOnTeam(i) == 0) //Clears leader edict if no one is left on the team + teams[i].leader = NULL; teams[i].leader_dead = false; } diff --git a/src/action/a_game.c b/src/action/a_game.c index e44bbb6b1..a0f5f5d94 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -255,9 +255,16 @@ qboolean PrintGameMessage(edict_t *ent) in order to begin the match. Once both teams have leaders, this message will no longer be printed. */ if (esp->value) { + edict_t* t1leader = HAVE_LEADER(TEAM1) ? teams[TEAM1].leader : NULL; + edict_t* t2leader = HAVE_LEADER(TEAM2) ? teams[TEAM2].leader : NULL; + edict_t* t3leader = HAVE_LEADER(TEAM3) ? teams[TEAM3].leader : NULL; + if (!team_round_going && !AllTeamsHaveLeaders()) { if (atl->value) { - Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for each team to have a leader\nType 'leader' in console to volunteer for duty.\n"); + if (teamCount == 2) + Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for each team to have a leader\nType 'leader' in console to volunteer for duty.\n\n Team 1 Leader: %s\n Team 2 Leader: %s", t1leader ? t1leader->client->pers.netname : "None", t2leader ? t2leader->client->pers.netname : "None"); + else if (teamCount == 3) + Q_snprintf(msg_buf, sizeof(msg_buf), "Waiting for each team to have a leader\nType 'leader' in console to volunteer for duty.\n\n Team 1 Leader: %s\n Team 2 Leader: %s\n Team 3 Leader: %s", t1leader ? t1leader->client->pers.netname : "None", t2leader ? t2leader->client->pers.netname : "None", t3leader ? t3leader->client->pers.netname : "None"); msg_ready = true; } else if (etv->value) { if (ent->client->resp.team == TEAM1) diff --git a/src/action/a_team.c b/src/action/a_team.c index 49d3da224..fdbd02b0f 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2026,7 +2026,7 @@ int TeamHasPlayers (int team) int _numclients( void ); // a_vote.c -qboolean BothTeamsHavePlayers(void) +qboolean AllTeamsHavePlayers(void) { int players[TEAM_TOP] = { 0 }, i, teamsWithPlayers; edict_t *ent; @@ -2094,20 +2094,34 @@ int CheckForWinner(void) if (atl->value) { if (teamCount == TEAM2) { if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead) { + if (esp_debug->value) + gi.dprintf("Both leaders are dead\n"); return WINNER_TIE; } else if (teams[TEAM1].leader_dead) { + if (esp_debug->value) + gi.dprintf("Team 2 leader is alive\n"); return TEAM2; } else if (teams[TEAM2].leader_dead) { + if (esp_debug->value) + gi.dprintf("Team 1 leader is alive\n"); return TEAM1; } } else if (teamCount == TEAM3) { if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead && teams[TEAM3].leader_dead) { + if (esp_debug->value) + gi.dprintf("All leaders are dead\n"); return WINNER_TIE; } else if (teams[TEAM1].leader_dead && teams[TEAM2].leader_dead) { + if (esp_debug->value) + gi.dprintf("Team 3 leader is alive\n"); return TEAM3; } else if (teams[TEAM1].leader_dead && teams[TEAM3].leader_dead) { + if (esp_debug->value) + gi.dprintf("Team 2 leader is alive\n"); return TEAM2; } else if (teams[TEAM2].leader_dead && teams[TEAM3].leader_dead) { + if (esp_debug->value) + gi.dprintf("Team 1 leader is alive\n"); return TEAM1; } } @@ -2818,16 +2832,16 @@ int CheckTeamRules (void) team_round_countdown--; if(!team_round_countdown) { - if (!esp->value && BothTeamsHavePlayers()) + if (!esp->value && AllTeamsHavePlayers()) { in_warmup = 0; team_game_going = 1; StartLCA(); } - else if (esp->value && AllTeamsHaveLeaders() && BothTeamsHavePlayers()) + else if (esp->value && AllTeamsHaveLeaders() && AllTeamsHavePlayers()) { if (esp_debug->value) - gi.dprintf("%s: Esp mode on, All teams have leaders, Both teams have players\n", __FUNCTION__); + gi.dprintf("%s: Esp mode on, All teams have leaders, all teams have players\n", __func__); in_warmup = 0; team_game_going = 1; StartLCA(); @@ -2971,7 +2985,7 @@ int CheckTeamRules (void) if (!team_round_countdown) { - if (BothTeamsHavePlayers() || (esp->value && AllTeamsHaveLeaders() && BothTeamsHavePlayers())) + if (AllTeamsHavePlayers() || (esp->value && AllTeamsHaveLeaders() && AllTeamsHavePlayers())) { if (use_tourney->value) { @@ -2983,7 +2997,7 @@ int CheckTeamRules (void) { int warmup_length = max( warmup->value, round_begin->value ); char buf[64] = ""; - if (esp->value && BothTeamsHavePlayers()) { + if (esp->value && AllTeamsHavePlayers()) { sprintf( buf, "All teams are ready!\nThe round will begin in %d seconds!", warmup_length ); } else { sprintf( buf, "The round will begin in %d seconds!", warmup_length ); @@ -3035,14 +3049,15 @@ int CheckTeamRules (void) return 1; } - if (!BothTeamsHavePlayers() || (esp->value && !AllTeamsHaveLeaders())) + if (!AllTeamsHavePlayers() || (esp->value && !AllTeamsHaveLeaders())) { - if (!matchmode->value || TeamsReady()) + if (!matchmode->value || TeamsReady()) { CenterPrintAll( "Not enough players to play!" ); - else if (esp->value && !AllTeamsHaveLeaders()) + } else if (esp->value && !AllTeamsHaveLeaders()) { CenterPrintAll ("Both Teams Must Have a Leader!\nType 'leader' in console to volunteer!"); - else + } else { CenterPrintAll( "Both Teams Must Be Ready!" ); + } team_round_going = team_round_countdown = team_game_going = 0; MakeAllLivePlayersObservers(); diff --git a/src/action/g_ext.c b/src/action/g_ext.c index a614ff3c3..af450a5ad 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -103,13 +103,13 @@ int G_customizeentityforclient(edict_t *clent, edict_t *ent, entity_state_t *sta return false; // Espionage allows indicators for leaders if set to 2 - if (esp->value && use_indicators->value == 2 && esp_showleader->value && clent->client->resp.team) { - // Quad is a blue glow, pent is a red glow - if (clent->client->resp.team == TEAM1 && IS_LEADER(clent)) - ent->s.effects = EF_PENT; - else if (clent->client->resp.team == TEAM2 && IS_LEADER(clent)) - ent->s.effects = EF_QUAD; - } + // if (esp->value && use_indicators->value == 2 && esp_showleader->value && clent->client->resp.team) { + // // Quad is a blue glow, pent is a red glow + // if (clent->client->resp.team == TEAM1 && IS_LEADER(clent)) + // ent->s.effects = EF_PENT; + // else if (clent->client->resp.team == TEAM2 && IS_LEADER(clent)) + // ent->s.effects = EF_QUAD; + // } if ((use_indicators->value == 2 && clent->client->resp.team) || (clent->client->resp.team && clent->client->pers.cl_indicators != 2)) // disallow indicators for players in use_indicators 2, and don't use them for players unless cl_indicators 2 return false; From ce2963d6f14a1a20d1d61507cdb4642e0f6b8807 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 16 Oct 2024 13:55:39 -0400 Subject: [PATCH 874/974] Added weapon options in docs --- doc/action.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/action.md b/doc/action.md index 70ec6b395..5d71e2358 100644 --- a/doc/action.md +++ b/doc/action.md @@ -78,6 +78,7 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) - [Weapon Banning](#weapon-banning) - [Item Banning](#item-banning) + - [Weapon/Item Counts](#weaponitem-counts) - [Teamkilling after a Round](#teamkilling-after-a-round) - [New IR Vision](#new-ir-vision) - [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) @@ -481,6 +482,13 @@ For all weapons: 63 (default) **Example:** You want to have slippers and silencers only on your server: you set itm_flags to 3 (1+2). +### Weapon/Item Counts +Normally, AQ2 allows one item and one weapon. These settings can adjust that so that you spawn with more, or can hold more in general. +- `allweapon [0/1]` - default 0, if enabled, all players spawn with all weapons, with exception of any weapon bans +- `allitem [0/1]` - default 0, if enabled, all players spawn with all items, with exception of any item bans +- `allow_hoarding [#]` - default 0, if enabled, allows players to pick up more than 1 special item or weapon, but does not spawn with them, like allweapon or allitem will do + + ### Teamkilling after a Round To add a little more fun in the game, we've added an option which will allow a team to TK each other after a round has ended during Teamplay. This is only available to servers where FF is off. From e49ad6e7dae0d83bb09b05dd82daecdbfea5b50d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 16 Oct 2024 14:38:34 -0400 Subject: [PATCH 875/974] Set timelimit and fraglimit to 0 if we issue nav_edit --- src/action/botlib/botlib_cmd.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index d159c1494..6ad238bd9 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -326,6 +326,9 @@ qboolean BOTLIB_Commands(edict_t* ent) if (dedicated->value) return true; ent->bot.walknode.enabled = !ent->bot.walknode.enabled; + // Turn off limits if we're in edit mode + gi.cvar_forceset(timelimit->name, "0"); + gi.cvar_forceset(fraglimit->name, "0"); return true; } From 3a8ad7eb89e32e097ffe6662d2432cc58762d6db Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 16 Oct 2024 14:46:08 -0400 Subject: [PATCH 876/974] Quick and dirty expose gl_shaders value to gamelib --- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib_cmd.c | 9 +++++++++ src/action/g_main.c | 4 ++++ src/action/g_save.c | 1 + 4 files changed, 15 insertions(+) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index ee5571420..2d7063511 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -556,6 +556,7 @@ extern cvar_t* bot_rotate; extern cvar_t* bot_reportasclient; extern cvar_t* bot_navautogen; //extern cvar_t* bot_randteamskin; +extern cvar_t* gl_shaders; #define MAX_BOT_NAMES 64 typedef struct { diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index 6ad238bd9..a21d4bc57 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -302,6 +302,11 @@ qboolean BOTLIB_Commands(edict_t* ent) if (dedicated->value) return true; + if (gl_shaders->value) { + gi.dprintf("Cannot toggle nav nodes with gl_shaders enabled, set gl_shaders to 0 to enable nav editing\n"); + return true; + } + //bot_showpath->value = !bot_showpath->value; // Toggle path display if (bot_showpath->value == 0 && ent->bot.walknode.enabled == false) { @@ -325,6 +330,10 @@ qboolean BOTLIB_Commands(edict_t* ent) { if (dedicated->value) return true; + if (gl_shaders->value) { + gi.dprintf("Cannot edit nodes with gl_shaders enabled, set gl_shaders to 0 to enable nav editing)\n"); + return true; + } ent->bot.walknode.enabled = !ent->bot.walknode.enabled; // Turn off limits if we're in edit mode gi.cvar_forceset(timelimit->name, "0"); diff --git a/src/action/g_main.c b/src/action/g_main.c index aee78e167..cbbded497 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -507,6 +507,10 @@ cvar_t* bot_rotate; // Disable/enable rotating bots on the server cvar_t* bot_reportasclient; // Report bots as clients to the server browser cvar_t* bot_navautogen; // Enable/Disable automatic generation of navigation files //cvar_t* bot_randteamskin; // Bots can randomize team skins each map + + +cvar_t* gl_shaders; // Temporarily adding gl_shaders so we can disable it for navmesh generation + //rekkie -- DEV_1 -- e #endif diff --git a/src/action/g_save.c b/src/action/g_save.c index d2226f5ff..36acf765e 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -704,6 +704,7 @@ void InitGame( void ) bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH); bot_navautogen = gi.cvar("bot_navautogen", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); + gl_shaders = gi.cvar("gl_shaders", "0", 0); //rekkie -- DEV_1 -- e #endif From 08151edd249152f1b3aaa51a2df5a04cf12e85be Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 16 Oct 2024 15:23:25 -0400 Subject: [PATCH 877/974] Reduced special weapon/item respawn timers if bots are in the game --- src/action/a_items.c | 9 ++++++++- src/action/p_weapon.c | 9 +++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/action/a_items.c b/src/action/a_items.c index 3a53f3c66..f15cdac3c 100644 --- a/src/action/a_items.c +++ b/src/action/a_items.c @@ -133,6 +133,8 @@ void SpecThink(edict_t * spec) G_FreeEdict(spec); } +// This function determines how long the special item will last until despawned +// The weapon version of this is SpecialWeaponRespawnTimer static void MakeTouchSpecThink(edict_t * ent) { ent->touch = Touch_Item; @@ -150,7 +152,12 @@ static void MakeTouchSpecThink(edict_t * ent) } if (gameSettings & GS_WEAPONCHOOSE) { - ent->nextthink = eztimer(6); + if (bot_enable->value && bot_connections.total_bots > 0) { + // Reduce time that items stick around if bots are loaded + ent->nextthink = eztimer(2); + } else { + ent->nextthink = eztimer(6); + } ent->think = G_FreeEdict; return; } diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 183e4fa53..5f304a136 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -715,9 +715,14 @@ void SpecialWeaponRespawnTimer(edict_t* ent) ent->think = PlaceHolder; return; } - // Deathmatch with weapon choose, weapons disappear in 6s + // Deathmatch with weapon choose, weapons disappear in 6s (w/bots, 2 seconds) if (gameSettings & GS_WEAPONCHOOSE) { - ent->nextthink = eztimer(6); + if (bot_enable->value && bot_connections.total_bots > 0) { + // Reduce time that items stick around if bots are loaded + ent->nextthink = eztimer(2); + } else { + ent->nextthink = eztimer(6); + } ent->think = ThinkSpecWeap; return; } From 5f972386f8432ead1e9c66f47fd04c60f2ec49f0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 18 Oct 2024 10:20:58 -0400 Subject: [PATCH 878/974] Added option to not include bots in highscores, ctf fix --- doc/action.md | 7 +++++++ src/action/g_local.h | 2 ++ src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/g_utils.c | 15 +++++++++++++++ src/action/tng_stats.c | 9 +++++++-- 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/doc/action.md b/doc/action.md index 5d71e2358..5662dc196 100644 --- a/doc/action.md +++ b/doc/action.md @@ -106,6 +106,7 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Print rules](#print-rules) - [Espionage](#espionage) - [Gun mechanics/enhancements](#gun-mechanicsenhancements) + - [Highscores](#highscores) - [Contact Information](#contact-information) - [Credits](#credits) --- @@ -694,6 +695,12 @@ Inspired by AQ2:ETE, these additions are optional server vars to create a differ - `gun_dualmk23_enhance [0/1]` - server cvar, default 0. If enabled, this allows both the silencer and the laser sight to be used on the Dual MK23 Pistols. - `use_gren_bonk [0/1]` - server cvar, default 0. If enabled, this enables impact damage of the grenade to cause damage on direct contact with a player. The speed of which the grenade is thrown will determine the damage dealt. Thanks to JukS for the idea and the code. +### Highscores +Borrowing code from OpenTDM (thank you Skuller!), high scores are stored in a local file on the server. Each time a new high score is achieved, it is registered in this file. The high scores are separated by map and by game mode. For example, `highscores/dm/wizs.txt` is the highscores file for the map `wizs` in `dm` mode. +- `g_highscores_dir` - serverr cvar, default is `highscores`; not much reason to change this default, but you can if you want +- `g_highscores_countbots` - server cvar, default is `0`, if enabled, bots will be included in high score recording if they achieve a high score +- `highscores` - client command, this will display the high scores in the console + ## Contact Information Contacting the AQ2World Team is easy, join our Discord or visit our forums, or leave a Github Issue. We are always looking for feedback and suggestions. - Discord Server: [https://discord.aq2world.com](https://discord.aq2world.com) diff --git a/src/action/g_local.h b/src/action/g_local.h index 58324fc61..06af4b623 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1313,6 +1313,7 @@ extern cvar_t *sv_killgib; // Enable or disable gibbing on kill command extern cvar_t *warmup_unready; extern cvar_t *training_mode; // Sets training mode vars extern cvar_t *g_highscores_dir; // Sets the highscores directory +extern cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bots extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA extern cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit @@ -1493,6 +1494,7 @@ qboolean infront( edict_t *self, edict_t *other ); void disablecvar(cvar_t *cvar, char *msg); int eztimer(int seconds); float sigmoid(float x); +edict_t* FindEdictByClient(gclient_t* client); // Re-enabled for bots float *tv (float x, float y, float z); diff --git a/src/action/g_main.c b/src/action/g_main.c index e1a890a57..dcf587c86 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -555,6 +555,7 @@ cvar_t *sv_killgib; // Gibs on 'kill' command cvar_t *warmup_unready; // Toggles warmup if captains unready cvar_t *training_mode; // Sets training mode vars cvar_t *g_highscores_dir; // Sets the highscores directory +cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bots cvar_t *lca_grenade; // Allows grenade pin pulling during LCA cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit diff --git a/src/action/g_save.c b/src/action/g_save.c index a10dcad4c..abfcc2b24 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -654,6 +654,7 @@ void InitGame( void ) gi.cvar_forceset("bholelimit", "30"); } g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); + g_highscores_countbots = gi.cvar("g_highscores_countbots", "0", 0); lca_grenade = gi.cvar("lca_grenade", "0", 0); diff --git a/src/action/g_utils.c b/src/action/g_utils.c index 44553061a..d3edad98e 100644 --- a/src/action/g_utils.c +++ b/src/action/g_utils.c @@ -766,4 +766,19 @@ int eztimer(int seconds){ float sigmoid(float x) { return 1 / (1 + exp(-x)); +} + +/* +Reverse-lookup, find the edict belonging to the gclient +*/ +edict_t* FindEdictByClient(gclient_t* client) +{ + int index = client - game.clients; // Calculate the index of the client in the clients array + + if (index >= 0 && index < game.maxclients) + { + return &g_edicts[index + 1]; // g_edicts[0] is the world entity, so players start from g_edicts[1] + } + + return NULL; // Return NULL if the client is not valid } \ No newline at end of file diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 2db8dc3a2..995c02782 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -727,7 +727,7 @@ void G_RegisterScore(void) return; } - if (teamplay->value && game.roundNum == 0){ + if ((teamplay->value && game.roundNum == 0) && !ctf->value){ gi.dprintf("No rounds were played, so no highscore will be recorded\n"); return; // No rounds were played, so skip } @@ -739,7 +739,7 @@ void G_RegisterScore(void) score = c->resp.score; // Calculate FPR, if mode is teamplay, else FPH - if (teamplay->value && game.roundNum > 0){ + if ((teamplay->value && game.roundNum > 0) || !ctf->value){ fragsper = c->resp.score / game.roundNum; } else { sec = (level.framenum - c->resp.enterframe) / HZ; @@ -777,6 +777,11 @@ void G_RegisterScore(void) qsort(level.scores, level.numscores, sizeof(highscore_t), ScoreCmp); + // Disable counting bot high scores if the cvar is set + edict_t* player = FindEdictByClient(c); + if (player->is_bot && !g_highscores_countbots->value) + return; + gi.dprintf("Added highscore entry for %s with %d score\n", c->pers.netname, score); From 4b9f386d937f5610baedf0962d6e00814a7875b4 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 18 Oct 2024 14:31:51 -0400 Subject: [PATCH 879/974] Synced with skuller build changes --- src/action/a_ctf.c | 4 ++++ src/action/botlib/botlib_ctf.c | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 42a6f6808..b16ee1eae 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -479,6 +479,10 @@ void CTFFragBonuses(edict_t * targ, edict_t * inflictor, edict_t * attacker) carrier = NULL; + // NULL checks + if (!targ || !inflictor || !attacker) + return; + // no bonus for fragging yourself if (!targ->client || !attacker->client || targ == attacker) return; diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 9a43776ad..9eb82d7c8 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -766,12 +766,14 @@ void BOTLIB_CTF_Goals(edict_t* self) if (VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin) < VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)) { flag_to_get = FLAG_T2_NUM; // Dropped team flag - Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); + if (bot_debug->value) + Com_Printf("%s %s [team] dropped blue flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin)); } else { flag_to_get = FLAG_T1_NUM; // Dropped enemy flag - Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); + if (bot_debug->value) + Com_Printf("%s %s [enemy] dropped red flag is closer. [%f] is less than [%f]\n", __func__, self->client->pers.netname, VectorDistance(self->s.origin, bot_ctf_status.flag1->s.origin), VectorDistance(self->s.origin, bot_ctf_status.flag2->s.origin)); } } else if (bot_ctf_status.flag2_is_dropped) // Dropped team flag From 612613ad46bc3fcf1a1b5daef16a42586c8b2f3a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 18 Oct 2024 14:54:00 -0400 Subject: [PATCH 880/974] Reverted mingw change, why fix if not broken --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b45193738..3561cf952 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,8 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y gcc-mingw-w64 nasm meson ninja-build + sudo apt-get install -y gcc-mingw-w64 nasm python3-pip ninja-build + sudo python3 -m pip install meson - name: Build run: | From f83b3840a60ca4ea4f45ff6f01e9771064dd1bec Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 18 Oct 2024 16:42:10 -0400 Subject: [PATCH 881/974] Changed bot radio to use streakKills instead of score --- src/action/botlib/botlib_communication.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 96705f012..7ac1f5b46 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -559,9 +559,9 @@ static int BOTLIB_ReadKilledPlayers(edict_t* ent) static void BOTLIB_GetLastKilledTargets(edict_t* self, int kills, char* buf) { if (kills > 1) - sprintf(buf, " %c [ %d ] %dx Enemies Down! [ %c ", '\x06', self->client->resp.score, kills, '\x07'); // Build bandage string + sprintf(buf, " %c [ %d ] %dx Enemies Down! [ %c ", '\x06', self->client->resp.streakKills, kills, '\x07'); // Build bandage string else - sprintf(buf, " %c [ %d ] Enemy Down! [ %c ", '\x06', self->client->resp.score, '\x07'); // Build bandage string + sprintf(buf, " %c [ %d ] Enemy Down! [ %c ", '\x06', self->client->resp.streakKills, '\x07'); // Build bandage string Q_strncatz(buf, self->client->last_killed_target[0]->client->pers.netname, PARSE_BUFSIZE); // First kill From 0957581264b7c3d8b5566252801a9292b9380df6 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 19 Oct 2024 19:31:53 +0300 Subject: [PATCH 882/974] Use mat4_t for GL matrices. --- src/refresh/gl.h | 20 ++++++++++---------- src/refresh/main.c | 6 +++--- src/refresh/mesh.c | 4 ++-- src/refresh/shader.c | 4 ++-- src/refresh/state.c | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/refresh/gl.h b/src/refresh/gl.h index c911eb2ac..9e1420acc 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -108,7 +108,7 @@ typedef struct { typedef struct { refdef_t fd; vec3_t viewaxis[3]; - GLfloat viewmatrix[16]; + mat4_t viewmatrix; unsigned visframe; unsigned drawframe; unsigned dlightframe; @@ -122,8 +122,8 @@ typedef struct { bool entrotated; float entscale; vec3_t entaxis[3]; - GLfloat entmatrix[16]; - GLfloat skymatrix[2][16]; + mat4_t entmatrix; + mat4_t skymatrix[2]; lightpoint_t lightpoint; int num_beams; int num_flares; @@ -601,9 +601,9 @@ typedef struct { } glMeshBlock_t; typedef struct { - GLfloat mvp[16]; + mat4_t m_vp; union { - GLfloat msky[2][16]; + mat4_t m_sky[2]; glMeshBlock_t mesh; }; GLfloat time; @@ -612,9 +612,9 @@ typedef struct { GLfloat intensity; GLfloat intensity2; GLfloat pad_4; - GLfloat w_amp[2]; - GLfloat w_phase[2]; - GLfloat scroll[2]; + vec2_t w_amp; + vec2_t w_phase; + vec2_t scroll; } glUniformBlock_t; typedef struct { @@ -627,8 +627,8 @@ typedef struct { GLuint currentbuffer[GLB_COUNT]; glVertexArray_t currentva; const GLfloat *currentmatrix; - GLfloat view_matrix[16]; - GLfloat proj_matrix[16]; + mat4_t view_matrix; + mat4_t proj_matrix; glUniformBlock_t u_block; bool u_block_dirty; } glState_t; diff --git a/src/refresh/main.c b/src/refresh/main.c index 9f5318c5b..95d4f71a2 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -313,12 +313,12 @@ void GL_RotationMatrix(GLfloat *matrix) void GL_RotateForEntity(bool skies) { - GLfloat matrix[16]; + mat4_t matrix; GL_RotationMatrix(matrix); if (skies) { - GL_MultMatrix(gls.u_block.msky[0], glr.skymatrix[0], matrix); - GL_MultMatrix(gls.u_block.msky[1], glr.skymatrix[1], matrix); + GL_MultMatrix(gls.u_block.m_sky[0], glr.skymatrix[0], matrix); + GL_MultMatrix(gls.u_block.m_sky[1], glr.skymatrix[1], matrix); } GL_MultMatrix(glr.entmatrix, glr.viewmatrix, matrix); GL_ForceMatrix(glr.entmatrix); diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index ee60422fc..dd48628d3 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -43,7 +43,7 @@ static bool dotshading; static float celscale; static drawshadow_t drawshadow; -static GLfloat shadowmatrix[16]; +static mat4_t shadowmatrix; #if USE_MD5 static md5_joint_t temp_skeleton[MD5_MAX_JOINTS]; @@ -524,7 +524,7 @@ static void proj_matrix(GLfloat *matrix, const cplane_t *plane, const vec3_t dir static void setup_shadow(void) { - GLfloat matrix[16], tmp[16]; + mat4_t matrix, tmp; vec3_t dir; if (!drawshadow) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 302bbbac2..a48afb7e8 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -654,7 +654,7 @@ static void shader_load_matrix(GLenum mode, const GLfloat *matrix) Q_assert(!"bad mode"); } - GL_MultMatrix(gls.u_block.mvp, gls.proj_matrix, gls.view_matrix); + GL_MultMatrix(gls.u_block.m_vp, gls.proj_matrix, gls.view_matrix); gls.u_block_dirty = true; } @@ -687,7 +687,7 @@ static void shader_setup_3d(void) R_RotateForSky(); - memcpy(gls.u_block.msky, glr.skymatrix, sizeof(glr.skymatrix)); + memcpy(gls.u_block.m_sky, glr.skymatrix, sizeof(glr.skymatrix)); } static void shader_disable_state(void) diff --git a/src/refresh/state.c b/src/refresh/state.c index f9f802756..309e9dfa6 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -22,7 +22,7 @@ glState_t gls; const glbackend_t *gl_backend; -static const GLfloat identity[16] = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; +static const mat4_t identity = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; // for uploading void GL_ForceTexture(glTmu_t tmu, GLuint texnum) @@ -172,7 +172,7 @@ void GL_ScrollPos(vec2_t scroll, glStateBits_t bits) void GL_Ortho(GLfloat xmin, GLfloat xmax, GLfloat ymin, GLfloat ymax, GLfloat znear, GLfloat zfar) { GLfloat width, height, depth; - GLfloat matrix[16]; + mat4_t matrix; width = xmax - xmin; height = ymax - ymin; @@ -226,7 +226,7 @@ void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) { GLfloat xmin, xmax, ymin, ymax, zfar, znear; GLfloat width, height, depth; - GLfloat matrix[16]; + mat4_t matrix; znear = gl_znear->value; From fbf0cd92429ad70177960d51a68745e288624613 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sat, 19 Oct 2024 19:41:09 +0300 Subject: [PATCH 883/974] Add re-release fog effect support. Keep fog parameters in player_state_t for proper delta compression. Height fog seems broken but that's how it is in re-release. --- doc/client.asciidoc | 6 ++ inc/common/msg.h | 14 ++- inc/common/protocol.h | 38 ++++++-- inc/refresh/refresh.h | 2 + inc/shared/game.h | 2 +- inc/shared/shared.h | 23 ++++- src/client/client.h | 3 +- src/client/demo.c | 4 +- src/client/entities.c | 21 +++++ src/client/gtv.c | 10 ++- src/client/parse.c | 17 +++- src/common/msg.c | 197 +++++++++++++++++++++++++++++++++++++---- src/refresh/gl.h | 21 ++++- src/refresh/main.c | 27 ++++-- src/refresh/mesh.c | 2 +- src/refresh/shader.c | 93 ++++++++++++++++++- src/refresh/sky.c | 2 +- src/refresh/state.c | 4 +- src/refresh/tess.c | 9 +- src/server/game.c | 4 +- src/server/main.c | 3 + src/server/mvd.c | 2 +- src/server/mvd/game.c | 5 +- src/server/mvd/parse.c | 8 ++ 24 files changed, 454 insertions(+), 63 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 2e3dcf766..ed028c26d 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -687,6 +687,12 @@ gl_waterwarp:: Enable screen warping effect when underwater. Only effective when using GLSL backend. Default value is 0 (disabled). +gl_fog:: + Enable re-release fog effect. Default value is 2. + - 0 — disable fog + - 1 — enable global fog + - 2 — enable global fog and height fog + gl_flarespeed:: Specifies flare fading effect speed. Default value is 8. Set this to 0 to disable fading. diff --git a/inc/common/msg.h b/inc/common/msg.h index 4fb524a58..a83bd9438 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -48,6 +48,14 @@ typedef struct { uint8_t loop_attenuation; } entity_packed_t; +typedef struct { + uint8_t color[3][3]; + uint32_t density; + uint16_t height_density; + uint16_t height_falloff; + int32_t height_dist[2]; +} player_packed_fog_t; + typedef struct { pmove_state_new_t pmove; int16_t viewangles[3]; @@ -59,6 +67,7 @@ typedef struct { uint8_t gunframe; uint8_t blend[4]; uint8_t damage_blend[4]; + player_packed_fog_t fog; uint8_t fov; uint8_t rdflags; int16_t stats[MAX_STATS_NEW]; @@ -73,8 +82,9 @@ typedef enum { MSG_PS_IGNORE_PREDICTION = BIT(5), // mutually exclusive with IGNORE_VIEWANGLES MSG_PS_EXTENSIONS = BIT(6), // enable protocol extensions MSG_PS_EXTENSIONS_2 = BIT(7), // enable more protocol extensions - MSG_PS_FORCE = BIT(8), // send even if unchanged (MVD stream only) - MSG_PS_REMOVE = BIT(9), // player is removed (MVD stream only) + MSG_PS_MOREBITS = BIT(8), // read more playerstate bits + MSG_PS_FORCE = BIT(9), // send even if unchanged (MVD stream only) + MSG_PS_REMOVE = BIT(10), // player is removed (MVD stream only) } msgPsFlags_t; typedef enum { diff --git a/inc/common/protocol.h b/inc/common/protocol.h index 54b9e1e16..015d82444 100644 --- a/inc/common/protocol.h +++ b/inc/common/protocol.h @@ -29,8 +29,11 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PROTOCOL_VERSION_R1Q2 35 #define PROTOCOL_VERSION_Q2PRO 36 #define PROTOCOL_VERSION_MVD 37 // not used for UDP connections -#define PROTOCOL_VERSION_EXTENDED_OLD 3434 -#define PROTOCOL_VERSION_EXTENDED 3435 + +#define PROTOCOL_VERSION_EXTENDED_MINIMUM 3434 // r2894 +#define PROTOCOL_VERSION_EXTENDED_LIMITS_2 3435 // r3300 +#define PROTOCOL_VERSION_EXTENDED_PLAYERFOG 3436 // r3579 +#define PROTOCOL_VERSION_EXTENDED_CURRENT 3436 // r3579 #define PROTOCOL_VERSION_R1Q2_MINIMUM 1903 // b6377 #define PROTOCOL_VERSION_R1Q2_UCMD 1904 // b7387 @@ -48,13 +51,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #define PROTOCOL_VERSION_Q2PRO_CINEMATICS 1023 // r2263 #define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS 1024 // r2894 #define PROTOCOL_VERSION_Q2PRO_EXTENDED_LIMITS_2 1025 // r3300 -#define PROTOCOL_VERSION_Q2PRO_CURRENT 1025 // r3300 +#define PROTOCOL_VERSION_Q2PRO_PLAYERFOG 1026 // r3579 +#define PROTOCOL_VERSION_Q2PRO_CURRENT 1026 // r3579 #define PROTOCOL_VERSION_MVD_MINIMUM 2009 // r168 #define PROTOCOL_VERSION_MVD_DEFAULT 2010 // r177 #define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS 2011 // r2894 #define PROTOCOL_VERSION_MVD_EXTENDED_LIMITS_2 2012 // r3300 -#define PROTOCOL_VERSION_MVD_CURRENT 2012 // r3300 +#define PROTOCOL_VERSION_MVD_PLAYERFOG 2013 // r3579 +#define PROTOCOL_VERSION_MVD_CURRENT 2013 // r3579 #define R1Q2_SUPPORTED(x) \ ((x) >= PROTOCOL_VERSION_R1Q2_MINIMUM && \ @@ -68,6 +73,10 @@ with this program; if not, write to the Free Software Foundation, Inc., ((x) >= PROTOCOL_VERSION_MVD_MINIMUM && \ (x) <= PROTOCOL_VERSION_MVD_CURRENT) +#define EXTENDED_SUPPORTED(x) \ + ((x) >= PROTOCOL_VERSION_EXTENDED_MINIMUM && \ + (x) <= PROTOCOL_VERSION_EXTENDED_CURRENT) + #define VALIDATE_CLIENTNUM(csr, x) \ ((x) >= -1 && (x) < (csr)->max_edicts - 1) @@ -213,6 +222,17 @@ typedef enum { //============================================== +typedef enum { + FOG_BIT_COLOR = BIT(0), + FOG_BIT_DENSITY = BIT(1), + FOG_BIT_HEIGHT_DENSITY = BIT(2), + FOG_BIT_HEIGHT_FALLOFF = BIT(3), + FOG_BIT_HEIGHT_START_COLOR = BIT(4), + FOG_BIT_HEIGHT_END_COLOR = BIT(5), + FOG_BIT_HEIGHT_START_DIST = BIT(6), + FOG_BIT_HEIGHT_END_DIST = BIT(7), +} fog_bits_t; + // player_state_t communication #define PS_M_TYPE BIT(0) @@ -231,7 +251,9 @@ typedef enum { #define PS_WEAPONINDEX BIT(12) #define PS_WEAPONFRAME BIT(13) #define PS_RDFLAGS BIT(14) -#define PS_RESERVED BIT(15) +#define PS_MOREBITS BIT(15) // read one additional byte + +#define PS_FOG BIT(16) // R1Q2 protocol specific extra flags #define EPS_GUNOFFSET BIT(0) @@ -264,7 +286,11 @@ typedef enum { #define PPS_GUNANGLES BIT(12) #define PPS_RDFLAGS BIT(13) #define PPS_STATS BIT(14) -#define PPS_REMOVE BIT(15) +#define PPS_MOREBITS BIT(15) // read one additional byte + // same as PPS_REMOVE for old demos!!! + +#define PPS_REMOVE BIT(16) +#define PPS_FOG BIT(17) // this is just a small hack to store inuse flag // in a field left otherwise unused by MVD code diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 9302b4495..87980fae5 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -91,6 +91,8 @@ typedef struct { vec3_t viewangles; vec4_t screen_blend; // rgba 0-1 full screen blend vec4_t damage_blend; // rgba 0-1 damage blend + player_fog_t fog; + player_heightfog_t heightfog; float time; // time is uesed to auto animate int rdflags; // RDF_UNDERWATER, etc bool extended; diff --git a/inc/shared/game.h b/inc/shared/game.h index bc44762dc..b8390e0fb 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., // #define GAME_API_VERSION_OLD 3 // game uses gclient_old_t -#define GAME_API_VERSION_NEW 3300 // game uses gclient_new_t +#define GAME_API_VERSION_NEW 3301 // game uses gclient_new_t #if USE_NEW_GAME_API #define GAME_API_VERSION GAME_API_VERSION_NEW diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 233384809..676cb40e5 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -1537,6 +1537,21 @@ typedef struct { } player_state_old_t; #if USE_NEW_GAME_API +typedef struct { + vec3_t color; + float density; + float sky_factor; +} player_fog_t; + +typedef struct { + struct { + vec3_t color; + float dist; + } start, end; + float density; + float falloff; +} player_heightfog_t; + typedef struct { pmove_state_new_t pmove; // for prediction @@ -1551,15 +1566,21 @@ typedef struct { vec3_t gunoffset; int gunindex; int gunframe; + int reserved_1; + int reserved_2; vec4_t blend; // rgba full screen effect vec4_t damage_blend; + player_fog_t fog; + player_heightfog_t heightfog; + float fov; // horizontal field of view int rdflags; // refdef flags - int reserved[4]; + int reserved_3; + int reserved_4; int16_t stats[MAX_STATS_NEW]; // fast status bar updates } player_state_new_t; diff --git a/src/client/client.h b/src/client/client.h index 6292f73a0..dcb85d518 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -495,6 +495,7 @@ typedef struct { player_packed_t ps; entity_packed_t entities[MAX_EDICTS]; msgEsFlags_t esFlags; // for writing + msgPsFlags_t psFlags; sizebuf_t message; } gtv; @@ -693,7 +694,7 @@ void CL_SendCmd(void); (MSG_ES_LONGSOLID | MSG_ES_UMASK | MSG_ES_BEAMORIGIN | MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS) #define CL_ES_EXTENDED_MASK_2 (CL_ES_EXTENDED_MASK | MSG_ES_EXTENSIONS_2) -#define CL_PS_EXTENDED_MASK_2 (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) +#define CL_PS_EXTENDED_MASK_2 (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2 | MSG_PS_MOREBITS) typedef struct { int type; diff --git a/src/client/demo.c b/src/client/demo.c index ac6ea9ca6..317b07334 100644 --- a/src/client/demo.c +++ b/src/client/demo.c @@ -420,7 +420,7 @@ static void CL_Record_f(void) // send the serverdata MSG_WriteByte(svc_serverdata); if (cl.csr.extended) - MSG_WriteLong(PROTOCOL_VERSION_EXTENDED); + MSG_WriteLong(PROTOCOL_VERSION_EXTENDED_CURRENT); else MSG_WriteLong(min(cls.serverProtocol, PROTOCOL_VERSION_DEFAULT)); MSG_WriteLong(cl.servercount); @@ -1185,7 +1185,7 @@ bool CL_GetDemoInfo(const char *path, demoInfo_t *info) goto fail; } c = MSG_ReadLong(); - if (c == PROTOCOL_VERSION_EXTENDED || c == PROTOCOL_VERSION_EXTENDED_OLD) { + if (EXTENDED_SUPPORTED(c)) { csr = &cs_remap_new; } else if (c < PROTOCOL_VERSION_OLD || c > PROTOCOL_VERSION_DEFAULT) { goto fail; diff --git a/src/client/entities.c b/src/client/entities.c index acfe17da5..e399bf346 100644 --- a/src/client/entities.c +++ b/src/client/entities.c @@ -1315,6 +1315,14 @@ static inline float lerp_client_fov(float ofov, float nfov, float lerp) return ofov + lerp * (nfov - ofov); } +static inline void lerp_values(const void *from, const void *to, float lerp, void *out, int count) +{ + float backlerp = 1.0f - lerp; + + for (int i = 0; i < count; i++) + ((float *)out)[i] = ((const float *)from)[i] * backlerp + ((const float *)to)[i] * lerp; +} + /* =============== CL_CalcViewValues @@ -1389,6 +1397,19 @@ void CL_CalcViewValues(void) Vector4Copy(ps->blend, cl.refdef.screen_blend); Vector4Copy(ps->damage_blend, cl.refdef.damage_blend); + // interpolate fog + if (cl.psFlags & MSG_PS_MOREBITS) { + lerp_values(&ops->fog, &ps->fog, lerp, + &cl.refdef.fog, sizeof(cl.refdef.fog) / sizeof(float)); + // no lerping if moved too far + if (fabsf(ps->heightfog.start.dist - ops->heightfog.start.dist) > 512 || + fabsf(ps->heightfog.end .dist - ops->heightfog.end .dist) > 512) + cl.refdef.heightfog = ps->heightfog; + else + lerp_values(&ops->heightfog, &ps->heightfog, lerp, + &cl.refdef.heightfog, sizeof(cl.refdef.heightfog) / sizeof(float)); + } + #if USE_FPS ps = &cl.keyframe.ps; ops = &cl.oldkeyframe.ps; diff --git a/src/client/gtv.c b/src/client/gtv.c index a281f2319..877d6fdee 100644 --- a/src/client/gtv.c +++ b/src/client/gtv.c @@ -54,6 +54,10 @@ static void build_gamestate(void) // set protocol flags cls.gtv.esFlags = MSG_ES_UMASK | MSG_ES_BEAMORIGIN | (cl.esFlags & CL_ES_EXTENDED_MASK_2); + cls.gtv.psFlags = MSG_PS_FORCE | (cl.psFlags & CL_PS_EXTENDED_MASK_2); + + if (cls.gtv.psFlags & MSG_PS_EXTENSIONS_2) + cls.gtv.psFlags |= MSG_PS_MOREBITS; } static void emit_gamestate(void) @@ -99,8 +103,7 @@ static void emit_gamestate(void) MSG_WriteByte(0); // send player state - MSG_WriteDeltaPlayerstate_Packet(NULL, &cls.gtv.ps, - cl.clientNum, cl.psFlags | MSG_PS_FORCE); + MSG_WriteDeltaPlayerstate_Packet(NULL, &cls.gtv.ps, cl.clientNum, cls.gtv.psFlags); MSG_WriteByte(CLIENTNUM_NONE); // send entity states @@ -138,8 +141,7 @@ void CL_GTV_EmitFrame(void) // send player state MSG_PackPlayerNew(&newps, &cl.frame.ps); - MSG_WriteDeltaPlayerstate_Packet(&cls.gtv.ps, &newps, - cl.clientNum, cl.psFlags | MSG_PS_FORCE); + MSG_WriteDeltaPlayerstate_Packet(&cls.gtv.ps, &newps, cl.clientNum, cls.gtv.psFlags); // shuffle current state to previous cls.gtv.ps = newps; diff --git a/src/client/parse.c b/src/client/parse.c index 62f420a53..216c86d9e 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -320,6 +320,9 @@ static void CL_ParseFrame(int extrabits) // parse playerstate bits = MSG_ReadWord(); + if (cl.psFlags & MSG_PS_MOREBITS && bits & PS_MOREBITS) + bits |= (uint32_t)MSG_ReadByte() << 16; + if (cls.serverProtocol > PROTOCOL_VERSION_DEFAULT) { MSG_ParseDeltaPlayerstate_Enhanced(from, &frame.ps, bits, extraflags, cl.psFlags); #if USE_DEBUG @@ -532,7 +535,7 @@ static void CL_ParseServerData(void) cls.serverProtocol, protocol); } // BIG HACK to let demos from release work with the 3.0x patch!!! - if (protocol == PROTOCOL_VERSION_EXTENDED || protocol == PROTOCOL_VERSION_EXTENDED_OLD) { + if (EXTENDED_SUPPORTED(protocol)) { cl.csr = cs_remap_new; cls.serverProtocol = PROTOCOL_VERSION_DEFAULT; } else if (protocol < PROTOCOL_VERSION_OLD || protocol > PROTOCOL_VERSION_DEFAULT) { @@ -649,6 +652,8 @@ static void CL_ParseServerData(void) Com_DPrintf("Q2PRO protocol extensions v2 enabled\n"); cl.esFlags |= MSG_ES_EXTENSIONS_2; cl.psFlags |= MSG_PS_EXTENSIONS_2; + if (cls.protocolVersion >= PROTOCOL_VERSION_Q2PRO_PLAYERFOG) + cl.psFlags |= MSG_PS_MOREBITS; PmoveEnableExt(&cl.pmp); } } else { @@ -684,9 +689,13 @@ static void CL_ParseServerData(void) cl.psFlags |= MSG_PS_EXTENSIONS; // hack for demo playback - if (protocol == PROTOCOL_VERSION_EXTENDED) { - cl.esFlags |= MSG_ES_EXTENSIONS_2; - cl.psFlags |= MSG_PS_EXTENSIONS_2; + if (EXTENDED_SUPPORTED(protocol)) { + if (protocol >= PROTOCOL_VERSION_EXTENDED_LIMITS_2) { + cl.esFlags |= MSG_ES_EXTENSIONS_2; + cl.psFlags |= MSG_PS_EXTENSIONS_2; + } + if (protocol >= PROTOCOL_VERSION_EXTENDED_PLAYERFOG) + cl.psFlags |= MSG_PS_MOREBITS; } } diff --git a/src/common/msg.c b/src/common/msg.c index 6efdb7b49..862873efd 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -819,12 +819,18 @@ void MSG_WriteDeltaEntity(const entity_packed_t *from, #define OFFSET2CHAR(x) Q_clip_int8((x) * 4) #define BLEND2BYTE(x) Q_clip_uint8((x) * 255) +#define FRAC2SHORT(x) Q_clip_uint16((x) * 65535) #define PACK_OFFSET(out, in) \ (out[0] = OFFSET2CHAR(in[0]), \ out[1] = OFFSET2CHAR(in[1]), \ out[2] = OFFSET2CHAR(in[2])) +#define PACK_COLOR(out, in) \ + (out[0] = BLEND2BYTE(in[0]), \ + out[1] = BLEND2BYTE(in[1]), \ + out[2] = BLEND2BYTE(in[2])) + #define PACK_BLEND(out, in) \ (out[0] = BLEND2BYTE(in[0]), \ out[1] = BLEND2BYTE(in[1]), \ @@ -871,6 +877,20 @@ void MSG_PackPlayerNew(player_packed_t *out, const player_state_new_t *in) out->gunframe = in->gunframe; PACK_BLEND(out->blend, in->blend); PACK_BLEND(out->damage_blend, in->damage_blend); + + PACK_COLOR(out->fog.color[0], in->fog.color); + PACK_COLOR(out->fog.color[1], in->heightfog.start.color); + PACK_COLOR(out->fog.color[2], in->heightfog.end.color); + + uint32_t lo = FRAC2SHORT(in->fog.density); + uint32_t hi = FRAC2SHORT(in->fog.sky_factor); + out->fog.density = lo | hi << 16; + + out->fog.height_density = FRAC2SHORT(in->heightfog.density); + out->fog.height_falloff = FRAC2SHORT(in->heightfog.falloff); + out->fog.height_dist[0] = COORD2SHORT(in->heightfog.start.dist); + out->fog.height_dist[1] = COORD2SHORT(in->heightfog.end.dist); + out->fov = Q_clip_uint8(in->fov); out->rdflags = in->rdflags; @@ -940,9 +960,64 @@ static void MSG_WriteDeltaBlend(const player_packed_t *from, const player_packed MSG_WriteByte(to->damage_blend[i]); } +static fog_bits_t MSG_CalcFogBits(const player_packed_fog_t *from, const player_packed_fog_t *to) +{ + fog_bits_t bits = 0; + + if (!memcmp(to, from, sizeof(*to))) + return 0; + + if (!VectorCompare(to->color[0], from->color[0])) + bits |= FOG_BIT_COLOR; + if (to->density != from->density) + bits |= FOG_BIT_DENSITY; + + if (to->height_density != from->height_density) + bits |= FOG_BIT_HEIGHT_DENSITY; + if (to->height_falloff != from->height_falloff) + bits |= FOG_BIT_HEIGHT_FALLOFF; + + if (!VectorCompare(to->color[1], from->color[1])) + bits |= FOG_BIT_HEIGHT_START_COLOR; + if (!VectorCompare(to->color[2], from->color[2])) + bits |= FOG_BIT_HEIGHT_END_COLOR; + + if (to->height_dist[0] != from->height_dist[0]) + bits |= FOG_BIT_HEIGHT_START_DIST; + if (to->height_dist[1] != from->height_dist[1]) + bits |= FOG_BIT_HEIGHT_END_DIST; + + return bits; +} + +static void MSG_WriteFog(const player_packed_fog_t *to, fog_bits_t bits) +{ + MSG_WriteByte(bits); + + if (bits & FOG_BIT_COLOR) + MSG_WriteData(to->color[0], sizeof(to->color[0])); + if (bits & FOG_BIT_DENSITY) + MSG_WriteLong(to->density); + if (bits & FOG_BIT_HEIGHT_DENSITY) + MSG_WriteShort(to->height_density); + if (bits & FOG_BIT_HEIGHT_FALLOFF) + MSG_WriteShort(to->height_falloff); + + if (bits & FOG_BIT_HEIGHT_START_COLOR) + MSG_WriteData(to->color[1], sizeof(to->color[1])); + if (bits & FOG_BIT_HEIGHT_END_COLOR) + MSG_WriteData(to->color[2], sizeof(to->color[2])); + + if (bits & FOG_BIT_HEIGHT_START_DIST) + MSG_WriteDeltaInt23(0, to->height_dist[0]); + if (bits & FOG_BIT_HEIGHT_END_DIST) + MSG_WriteDeltaInt23(0, to->height_dist[1]); +} + void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player_packed_t *to, msgPsFlags_t flags) { - int pflags; + int pflags = 0; + fog_bits_t fogbits = 0; uint64_t statbits; Q_assert(to); @@ -953,8 +1028,6 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player // // determine what needs to be sent // - pflags = 0; - if (to->pmove.pm_type != from->pmove.pm_type) pflags |= PS_M_TYPE; @@ -991,6 +1064,9 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player !Vector4Compare(to->damage_blend, from->damage_blend)) pflags |= PS_BLEND; + if (flags & MSG_PS_MOREBITS && (fogbits = MSG_CalcFogBits(&from->fog, &to->fog))) + pflags |= PS_FOG; + if (to->fov != from->fov) pflags |= PS_FOV; @@ -1005,10 +1081,15 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player if (to->gunindex != from->gunindex) pflags |= PS_WEAPONINDEX; + if (pflags & 0xff0000) + pflags |= PS_MOREBITS; + // // write it // - MSG_WriteShort(pflags); + MSG_WriteShort(pflags & 0xffff); + if (pflags & PS_MOREBITS) + MSG_WriteByte(pflags >> 16); // // write the pmove_state_t @@ -1098,6 +1179,9 @@ void MSG_WriteDeltaPlayerstate_Default(const player_packed_t *from, const player MSG_WriteData(to->blend, sizeof(to->blend)); } + if (pflags & PS_FOG) + MSG_WriteFog(&to->fog, fogbits); + if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1113,7 +1197,8 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, player_packed_t *to, msgPsFlags_t flags) { - int pflags, eflags; + int pflags = 0, eflags = 0; + fog_bits_t fogbits = 0; uint64_t statbits; Q_assert(to); @@ -1124,9 +1209,6 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, // // determine what needs to be sent // - pflags = 0; - eflags = 0; - if (to->pmove.pm_type != from->pmove.pm_type) pflags |= PS_M_TYPE; @@ -1199,6 +1281,9 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, Vector4Copy(from->damage_blend, to->damage_blend); } + if (flags & MSG_PS_MOREBITS && (fogbits = MSG_CalcFogBits(&from->fog, &to->fog))) + pflags |= PS_FOG; + if (from->fov != to->fov) pflags |= PS_FOV; @@ -1233,10 +1318,15 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, if (statbits) eflags |= EPS_STATS; + if (pflags & 0xff0000) + pflags |= PS_MOREBITS; + // // write it // - MSG_WriteShort(pflags); + MSG_WriteShort(pflags & 0xffff); + if (pflags & PS_MOREBITS) + MSG_WriteByte(pflags >> 16); // // write the pmove_state_t @@ -1339,6 +1429,9 @@ int MSG_WriteDeltaPlayerstate_Enhanced(const player_packed_t *from, MSG_WriteData(to->blend, sizeof(to->blend)); } + if (pflags & PS_FOG) + MSG_WriteFog(&to->fog, fogbits); + if (pflags & PS_FOV) MSG_WriteByte(to->fov); @@ -1367,7 +1460,8 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, int number, msgPsFlags_t flags) { - int pflags; + int pflags = 0; + fog_bits_t fogbits = 0; uint64_t statbits; // this can happen with client GTV @@ -1376,7 +1470,9 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (!to) { MSG_WriteByte(number); - MSG_WriteShort(PPS_REMOVE); + MSG_WriteShort(PPS_MOREBITS); // MOREBITS == REMOVE for old demos + if (flags & MSG_PS_MOREBITS) + MSG_WriteByte(PPS_REMOVE >> 16); return; } @@ -1386,8 +1482,6 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, // // determine what needs to be sent // - pflags = 0; - if (to->pmove.pm_type != from->pmove.pm_type) pflags |= PPS_M_TYPE; @@ -1419,6 +1513,9 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, pflags |= PPS_BLEND; } + if (flags & MSG_PS_MOREBITS && (fogbits = MSG_CalcFogBits(&from->fog, &to->fog))) + pflags |= PPS_FOG; + if (from->fov != to->fov) pflags |= PPS_FOV; @@ -1449,11 +1546,16 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, if (flags & MSG_PS_REMOVE) pflags |= PPS_REMOVE; // used for MVD stream only + if (pflags & 0xff0000) + pflags |= PPS_MOREBITS; + // // write it // MSG_WriteByte(number); - MSG_WriteShort(pflags); + MSG_WriteShort(pflags & 0xffff); + if (flags & MSG_PS_MOREBITS && pflags & PPS_MOREBITS) + MSG_WriteByte(pflags >> 16); // // write some part of the pmove_state_t @@ -1519,6 +1621,9 @@ void MSG_WriteDeltaPlayerstate_Packet(const player_packed_t *from, MSG_WriteData(to->blend, sizeof(to->blend)); } + if (pflags & PPS_FOG) + MSG_WriteFog(&to->fog, fogbits); + if (pflags & PPS_FOV) MSG_WriteByte(to->fov); @@ -1721,6 +1826,17 @@ static void MSG_ReadDeltaCoord(float *to) } } +static float MSG_ReadExtCoord(void) +{ + uint32_t v = MSG_ReadWord(); + if (v & 1) { + v |= (uint32_t)MSG_ReadByte() << 16; + return SHORT2COORD(SignExtend(v >> 1, 23)); + } else { + return SHORT2COORD(SignExtend(v >> 1, 15)); + } +} + #endif #if USE_SERVER @@ -1729,10 +1845,9 @@ static inline void MSG_ReadPos(vec3_t pos, bool extended) { if (extended) { - VectorClear(pos); - MSG_ReadDeltaCoord(&pos[0]); - MSG_ReadDeltaCoord(&pos[1]); - MSG_ReadDeltaCoord(&pos[2]); + pos[0] = MSG_ReadExtCoord(); + pos[1] = MSG_ReadExtCoord(); + pos[2] = MSG_ReadExtCoord(); } else { pos[0] = MSG_ReadCoord(); pos[1] = MSG_ReadCoord(); @@ -2187,6 +2302,40 @@ static void MSG_ReadBlend(player_state_t *to, msgPsFlags_t psflags) } } +static void MSG_ReadColor(vec3_t color) +{ + color[0] = MSG_ReadByte() / 255.0f; + color[1] = MSG_ReadByte() / 255.0f; + color[2] = MSG_ReadByte() / 255.0f; +} + +static void MSG_ReadFog(player_state_t *to) +{ + fog_bits_t bits = MSG_ReadByte(); + + if (bits & FOG_BIT_COLOR) + MSG_ReadColor(to->fog.color); + if (bits & FOG_BIT_DENSITY) { + to->fog.density = MSG_ReadWord() / 65535.0f; + to->fog.sky_factor = MSG_ReadWord() / 65535.0f; + } + + if (bits & FOG_BIT_HEIGHT_DENSITY) + to->heightfog.density = MSG_ReadWord() / 65535.0f; + if (bits & FOG_BIT_HEIGHT_FALLOFF) + to->heightfog.falloff = MSG_ReadWord() / 65535.0f; + + if (bits & FOG_BIT_HEIGHT_START_COLOR) + MSG_ReadColor(to->heightfog.start.color); + if (bits & FOG_BIT_HEIGHT_END_COLOR) + MSG_ReadColor(to->heightfog.end.color); + + if (bits & FOG_BIT_HEIGHT_START_DIST) + to->heightfog.start.dist = MSG_ReadExtCoord(); + if (bits & FOG_BIT_HEIGHT_END_DIST) + to->heightfog.end.dist = MSG_ReadExtCoord(); +} + #if USE_CLIENT /* @@ -2302,6 +2451,9 @@ void MSG_ParseDeltaPlayerstate_Default(const player_state_t *from, if (flags & PS_BLEND) MSG_ReadBlend(to, psflags); + if (flags & PS_FOG) + MSG_ReadFog(to); + if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2441,6 +2593,9 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, if (flags & PS_BLEND) MSG_ReadBlend(to, psflags); + if (flags & PS_FOG) + MSG_ReadFog(to); + if (flags & PS_FOV) to->fov = MSG_ReadByte(); @@ -2539,6 +2694,9 @@ void MSG_ParseDeltaPlayerstate_Packet(player_state_t *to, if (flags & PPS_BLEND) MSG_ReadBlend(to, psflags); + if (flags & PPS_FOG) + MSG_ReadFog(to); + if (flags & PPS_FOV) to->fov = MSG_ReadByte(); @@ -2583,6 +2741,7 @@ void MSG_ShowDeltaPlayerstateBits_Default(int flags) S(WEAPONINDEX, "gunindex"); S(WEAPONFRAME, "gunframe"); S(BLEND, "blend"); + S(FOG, "fog"); S(FOV, "fov"); S(RDFLAGS, "rdflags"); #undef S @@ -2610,6 +2769,7 @@ void MSG_ShowDeltaPlayerstateBits_Enhanced(int flags, int extraflags) SE(GUNOFFSET, "gunoffset"); SE(GUNANGLES, "gunangles"); SP(BLEND, "blend"); + SP(FOG, "fog"); SP(FOV, "fov"); SP(RDFLAGS, "rdflags"); SE(STATS, "stats"); @@ -2712,6 +2872,7 @@ void MSG_ShowDeltaPlayerstateBits_Packet(int flags) S(GUNOFFSET, "gunoffset"); S(GUNANGLES, "gunangles"); S(BLEND, "blend"); + S(FOG, "fog"); S(FOV, "fov"); S(RDFLAGS, "rdflags"); S(STATS, "stats"); diff --git a/src/refresh/gl.h b/src/refresh/gl.h index 9e1420acc..6753bde65 100644 --- a/src/refresh/gl.h +++ b/src/refresh/gl.h @@ -127,6 +127,7 @@ typedef struct { lightpoint_t lightpoint; int num_beams; int num_flares; + int fog_bits, fog_bits_sky; int framebuffer_width; int framebuffer_height; bool framebuffer_ok; @@ -507,14 +508,20 @@ typedef enum { GLS_SCROLL_FLIP = BIT(24), GLS_SCROLL_SLOW = BIT(25), + GLS_FOG_GLOBAL = BIT(26), + GLS_FOG_HEIGHT = BIT(27), + GLS_FOG_SKY = BIT(28), + GLS_BLEND_MASK = GLS_BLEND_BLEND | GLS_BLEND_ADD | GLS_BLEND_MODULATE, GLS_COMMON_MASK = GLS_DEPTHMASK_FALSE | GLS_DEPTHTEST_DISABLE | GLS_CULL_DISABLE | GLS_BLEND_MASK, GLS_SKY_MASK = GLS_CLASSIC_SKY | GLS_DEFAULT_SKY, + GLS_FOG_MASK = GLS_FOG_GLOBAL | GLS_FOG_HEIGHT | GLS_FOG_SKY, GLS_MESH_ANY = GLS_MESH_MD2 | GLS_MESH_MD5, GLS_MESH_MASK = GLS_MESH_ANY | GLS_MESH_LERP | GLS_MESH_SHELL | GLS_MESH_SHADE, GLS_SHADER_MASK = GLS_ALPHATEST_ENABLE | GLS_TEXTURE_REPLACE | GLS_SCROLL_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_WARP_ENABLE | GLS_INTENSITY_ENABLE | GLS_GLOWMAP_ENABLE | - GLS_SKY_MASK | GLS_DEFAULT_FLARE | GLS_MESH_MASK, + GLS_SKY_MASK | GLS_DEFAULT_FLARE | GLS_MESH_MASK | GLS_FOG_MASK, + GLS_UNIFORM_MASK = GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_SKY_MASK | GLS_FOG_MASK, GLS_SCROLL_MASK = GLS_SCROLL_ENABLE | GLS_SCROLL_X | GLS_SCROLL_Y | GLS_SCROLL_FLIP | GLS_SCROLL_SLOW, } glStateBits_t; @@ -602,6 +609,7 @@ typedef struct { typedef struct { mat4_t m_vp; + mat4_t m_model; union { mat4_t m_sky[2]; glMeshBlock_t mesh; @@ -611,10 +619,17 @@ typedef struct { GLfloat add; GLfloat intensity; GLfloat intensity2; - GLfloat pad_4; + GLfloat fog_sky_factor; vec2_t w_amp; vec2_t w_phase; vec2_t scroll; + vec4_t fog_color; + vec4_t heightfog_start; + vec4_t heightfog_end; + GLfloat heightfog_density; + GLfloat heightfog_falloff; + vec2_t pad_4; + vec4_t vieworg; } glUniformBlock_t; typedef struct { @@ -667,6 +682,8 @@ typedef struct { extern const glbackend_t *gl_backend; +extern const mat4_t gl_identity; + static inline void GL_ActiveTexture(glTmu_t tmu) { if (gls.server_tmu != tmu) { diff --git a/src/refresh/main.c b/src/refresh/main.c index 95d4f71a2..6e56855af 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -61,6 +61,7 @@ cvar_t *gl_md5_distance; #endif cvar_t *gl_damageblend_frac; cvar_t *gl_waterwarp; +cvar_t *gl_fog; cvar_t *gl_swapinterval; // development variables @@ -313,14 +314,12 @@ void GL_RotationMatrix(GLfloat *matrix) void GL_RotateForEntity(bool skies) { - mat4_t matrix; - - GL_RotationMatrix(matrix); + GL_RotationMatrix(gls.u_block.m_model); if (skies) { - GL_MultMatrix(gls.u_block.m_sky[0], glr.skymatrix[0], matrix); - GL_MultMatrix(gls.u_block.m_sky[1], glr.skymatrix[1], matrix); + GL_MultMatrix(gls.u_block.m_sky[0], glr.skymatrix[0], gls.u_block.m_model); + GL_MultMatrix(gls.u_block.m_sky[1], glr.skymatrix[1], gls.u_block.m_model); } - GL_MultMatrix(glr.entmatrix, glr.viewmatrix, matrix); + GL_MultMatrix(glr.entmatrix, glr.viewmatrix, gls.u_block.m_model); GL_ForceMatrix(glr.entmatrix); } @@ -330,7 +329,7 @@ static void GL_DrawSpriteModel(const model_t *model) const mspriteframe_t *frame = &model->spriteframes[e->frame % model->numframes]; const image_t *image = frame->image; const float alpha = (e->flags & RF_TRANSLUCENT) ? e->alpha : 1.0f; - glStateBits_t bits = GLS_DEPTHMASK_FALSE; + glStateBits_t bits = GLS_DEPTHMASK_FALSE | glr.fog_bits; vec3_t up, down, left, right; if (alpha == 1.0f) { @@ -663,12 +662,21 @@ void R_RenderFrame(const refdef_t *fd) glr.drawframe++; glr.fd = *fd; - glr.num_beams = 0; - glr.num_flares = 0; + glr.num_beams = glr.num_flares = 0; + glr.fog_bits = glr.fog_bits_sky = 0; if (gl_dynamic->integer != 1 || gl_vertexlight->integer) glr.fd.num_dlights = 0; + if (gl_fog->integer > 0) { + if (glr.fd.fog.density > 0) + glr.fog_bits |= GLS_FOG_GLOBAL; + if (glr.fd.heightfog.density > 0 && glr.fd.heightfog.falloff > 0 && gl_fog->integer > 1) + glr.fog_bits |= GLS_FOG_HEIGHT; + if (glr.fd.fog.sky_factor > 0) + glr.fog_bits_sky |= GLS_FOG_SKY; + } + if (lm.dirty) { GL_RebuildLighting(); lm.dirty = false; @@ -905,6 +913,7 @@ static void GL_Register(void) #endif gl_damageblend_frac = Cvar_Get("gl_damageblend_frac", "0.2", 0); gl_waterwarp = Cvar_Get("gl_waterwarp", "0", 0); + gl_fog = Cvar_Get("gl_fog", "2", 0); gl_swapinterval = Cvar_Get("gl_swapinterval", "1", CVAR_ARCHIVE); gl_swapinterval->changed = gl_swapinterval_changed; diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index dd48628d3..8e901519c 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -663,7 +663,7 @@ static void draw_alias_mesh(const uint16_t *indices, int num_indices, qglColorMask(1, 1, 1, 1); } - state = GLS_INTENSITY_ENABLE; + state = GLS_INTENSITY_ENABLE | glr.fog_bits; if (!gls.currentva) state |= meshbits; else if (dotshading) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index a48afb7e8..9ca0e9ef7 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -48,6 +48,7 @@ static void write_block(sizebuf_t *buf, glStateBits_t bits) { GLSF("layout(std140) uniform u_block {\n"); GLSL(mat4 m_vp;); + GLSL(mat4 m_model;); if (bits & GLS_MESH_ANY) { GLSL( @@ -76,10 +77,17 @@ static void write_block(sizebuf_t *buf, glStateBits_t bits) float u_add; float u_intensity; float u_intensity2; - float pad_4; + float u_fog_sky_factor; vec2 w_amp; vec2 w_phase; vec2 u_scroll; + vec4 u_fog_color; + vec4 u_heightfog_start; + vec4 u_heightfog_end; + float u_heightfog_density; + float u_heightfog_falloff; + vec2 pad_4; + vec3 u_vieworg; ) GLSF("};\n"); } @@ -135,6 +143,9 @@ static void write_skel_shader(sizebuf_t *buf, glStateBits_t bits) out vec4 v_color; ) + if (bits & GLS_FOG_HEIGHT) + GLSL(out vec3 v_world_pos;) + if (bits & GLS_MESH_SHADE) write_shadedot(buf); @@ -182,6 +193,9 @@ static void write_skel_shader(sizebuf_t *buf, glStateBits_t bits) if (bits & GLS_MESH_SHELL) GLSL(out_pos += out_norm * u_shellscale;) + if (bits & GLS_FOG_HEIGHT) + GLSL(v_world_pos = (m_model * vec4(out_pos, 1.0)).xyz;) + GLSL(gl_Position = m_vp * vec4(out_pos, 1.0);) GLSF("}\n"); } @@ -219,6 +233,9 @@ static void write_mesh_shader(sizebuf_t *buf, glStateBits_t bits) out vec4 v_color; ) + if (bits & GLS_FOG_HEIGHT) + GLSL(out vec3 v_world_pos;) + if (bits & (GLS_MESH_SHELL | GLS_MESH_SHADE)) write_getnormal(buf); @@ -259,6 +276,9 @@ static void write_mesh_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(v_color = u_color;) } + if (bits & GLS_FOG_HEIGHT) + GLSL(v_world_pos = (m_model * vec4(pos, 1.0)).xyz;) + GLSL(gl_Position = m_vp * vec4(pos, 1.0);) GLSF("}\n"); } @@ -298,6 +318,9 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(out vec4 v_color;) } + if (bits & GLS_FOG_HEIGHT) + GLSL(out vec3 v_world_pos;) + GLSF("void main() {\n"); if (bits & GLS_CLASSIC_SKY) { GLSL(v_dir = (m_sky[1] * a_pos).xyz;) @@ -315,10 +338,30 @@ static void write_vertex_shader(sizebuf_t *buf, glStateBits_t bits) if (!(bits & GLS_TEXTURE_REPLACE)) GLSL(v_color = a_color;) + if (bits & GLS_FOG_HEIGHT) + GLSL(v_world_pos = (m_model * a_pos).xyz;) + GLSL(gl_Position = m_vp * a_pos;) GLSF("}\n"); } +// XXX: this is very broken. but that's how it is in re-release. +static void write_height_fog(sizebuf_t *buf) +{ + GLSL({ + float dir_z = normalize(v_world_pos - u_vieworg).z; + float eye = u_vieworg.z - u_heightfog_start.w; + float pos = v_world_pos.z - u_heightfog_start.w; + float density = (exp(-u_heightfog_falloff * eye) - + exp(-u_heightfog_falloff * pos)) / (u_heightfog_falloff * dir_z); + float extinction = 1.0 - clamp(exp(-density), 0.0, 1.0); + float fraction = clamp((pos - u_heightfog_start.w) / (u_heightfog_end.w - u_heightfog_start.w), 0.0, 1.0); + vec3 fog_color = mix(u_heightfog_start.rgb, u_heightfog_end.rgb, fraction) * extinction; + float fog = (1.0 - exp(-(u_heightfog_density * frag_depth))) * extinction; + diffuse.rgb = mix(diffuse.rgb, fog_color.rgb, fog); + }) +} + static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) { write_header(buf, bits); @@ -326,7 +369,7 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) if (gl_config.ver_es) GLSL(precision mediump float;) - if (bits & (GLS_WARP_ENABLE | GLS_LIGHTMAP_ENABLE | GLS_INTENSITY_ENABLE | GLS_SKY_MASK)) + if (bits & GLS_UNIFORM_MASK) write_block(buf, bits); if (bits & GLS_CLASSIC_SKY) { @@ -358,6 +401,9 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(out vec4 o_color;) + if (bits & GLS_FOG_HEIGHT) + GLSL(in vec3 v_world_pos;) + GLSF("void main() {\n"); if (bits & GLS_CLASSIC_SKY) { GLSL( @@ -414,6 +460,22 @@ static void write_fragment_shader(sizebuf_t *buf, glStateBits_t bits) GLSL(diffuse.rgb += glowmap.rgb;) } + if (bits & (GLS_FOG_GLOBAL | GLS_FOG_HEIGHT)) + GLSL(float frag_depth = gl_FragCoord.z / gl_FragCoord.w;) + + if (bits & GLS_FOG_GLOBAL) + GLSL({ + float d = u_fog_color.a * frag_depth; + float fog = 1.0f - exp(-(d * d)); + diffuse.rgb = mix(diffuse.rgb, u_fog_color.rgb, fog); + }) + + if (bits & GLS_FOG_HEIGHT) + write_height_fog(buf); + + if (bits & GLS_FOG_SKY) + GLSL(diffuse.rgb = mix(diffuse.rgb, u_fog_color.rgb, u_fog_sky_factor);) + GLSL(o_color = diffuse;) GLSF("}\n"); } @@ -672,6 +734,25 @@ static void shader_setup_2d(void) gls.u_block.w_phase[1] = M_PIf * 10; } +static void shader_setup_fog(void) +{ + if (!glr.fog_bits) + return; + + VectorCopy(glr.fd.fog.color, gls.u_block.fog_color); + gls.u_block.fog_color[3] = glr.fd.fog.density / 64; + gls.u_block.fog_sky_factor = glr.fd.fog.sky_factor; + + VectorCopy(glr.fd.heightfog.start.color, gls.u_block.heightfog_start); + gls.u_block.heightfog_start[3] = glr.fd.heightfog.start.dist; + + VectorCopy(glr.fd.heightfog.end.color, gls.u_block.heightfog_end); + gls.u_block.heightfog_end[3] = glr.fd.heightfog.end.dist; + + gls.u_block.heightfog_density = glr.fd.heightfog.density; + gls.u_block.heightfog_falloff = glr.fd.heightfog.falloff; +} + static void shader_setup_3d(void) { gls.u_block.time = glr.fd.time; @@ -685,9 +766,15 @@ static void shader_setup_3d(void) gls.u_block.w_phase[0] = 4; gls.u_block.w_phase[1] = 4; + shader_setup_fog(); + R_RotateForSky(); - memcpy(gls.u_block.m_sky, glr.skymatrix, sizeof(glr.skymatrix)); + // setup default matrices for world + memcpy(gls.u_block.m_sky, glr.skymatrix, sizeof(gls.u_block.m_sky)); + memcpy(gls.u_block.m_model, gl_identity, sizeof(gls.u_block.m_model)); + + VectorCopy(glr.fd.vieworg, gls.u_block.vieworg); } static void shader_disable_state(void) diff --git a/src/refresh/sky.c b/src/refresh/sky.c index ef54248ee..b90236b9d 100644 --- a/src/refresh/sky.c +++ b/src/refresh/sky.c @@ -334,7 +334,7 @@ void R_DrawSkyBox(void) return; // nothing visible GL_BindArrays(VA_SPRITE); - GL_StateBits(GLS_TEXTURE_REPLACE); + GL_StateBits(GLS_TEXTURE_REPLACE | glr.fog_bits_sky); GL_ArrayBits(GLA_VERTEX | GLA_TC); for (i = 0; i < 6; i++) { diff --git a/src/refresh/state.c b/src/refresh/state.c index 309e9dfa6..ab66c5203 100644 --- a/src/refresh/state.c +++ b/src/refresh/state.c @@ -22,7 +22,7 @@ glState_t gls; const glbackend_t *gl_backend; -static const mat4_t identity = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; +const mat4_t gl_identity = { [0] = 1, [5] = 1, [10] = 1, [15] = 1 }; // for uploading void GL_ForceTexture(glTmu_t tmu, GLuint texnum) @@ -219,7 +219,7 @@ void GL_Setup2D(void) if (gl_backend->setup_2d) gl_backend->setup_2d(); - gl_backend->load_matrix(GL_MODELVIEW, identity); + gl_backend->load_matrix(GL_MODELVIEW, gl_identity); } void GL_Frustum(GLfloat fov_x, GLfloat fov_y, GLfloat reflect_x) diff --git a/src/refresh/tess.c b/src/refresh/tess.c index 1a8eee154..8fe3f08d2 100644 --- a/src/refresh/tess.c +++ b/src/refresh/tess.c @@ -76,7 +76,7 @@ void GL_DrawParticles(void) GL_LoadUniforms(); GL_BindArrays(VA_EFFECT); - bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE; + bits = (gl_partstyle->integer ? GLS_BLEND_ADD : GLS_BLEND_BLEND) | GLS_DEPTHMASK_FALSE | glr.fog_bits; p = glr.fd.particles; total = glr.fd.num_particles; @@ -147,7 +147,7 @@ static void GL_FlushBeamSegments(void) array |= GLA_TC; GL_BindTexture(TMU_TEXTURE, texnum); - GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); + GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE | glr.fog_bits); GL_ArrayBits(array); GL_DrawIndexed(SHOWTRIS_FX); @@ -667,6 +667,11 @@ void GL_Flush3D(void) if (!(state & GLS_TEXTURE_REPLACE)) array |= GLA_COLOR; + if (state & GLS_SKY_MASK) + state |= glr.fog_bits_sky; + else + state |= glr.fog_bits; + GL_StateBits(state); GL_ArrayBits(array); diff --git a/src/server/game.c b/src/server/game.c index 2f0647837..98bf90cee 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -1015,8 +1015,8 @@ void SV_InitGameProgs(void) Com_DPrintf("Game API version: %d\n", ge->apiversion); if (ge->apiversion != GAME_API_VERSION_OLD && ge->apiversion != GAME_API_VERSION_NEW) { - Com_Error(ERR_DROP, "Game library is version %d, expected %d", - ge->apiversion, GAME_API_VERSION_OLD); + Com_Error(ERR_DROP, "Game library is version %d, expected %d or %d", + ge->apiversion, GAME_API_VERSION_OLD, GAME_API_VERSION_NEW); } // get extended api if present diff --git a/src/server/main.c b/src/server/main.c index e3711c745..07bcd225b 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -1022,6 +1022,9 @@ static void init_pmove_and_es_flags(client_t *newcl) if (IS_NEW_GAME_API) { newcl->esFlags |= MSG_ES_EXTENSIONS_2; newcl->psFlags |= MSG_PS_EXTENSIONS_2; + if (newcl->version >= PROTOCOL_VERSION_Q2PRO_PLAYERFOG) { + newcl->psFlags |= MSG_PS_MOREBITS; + } } } force = 1; diff --git a/src/server/mvd.c b/src/server/mvd.c index 9cff1fae7..6886bbb03 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -2147,7 +2147,7 @@ void SV_MvdPostInit(void) if (IS_NEW_GAME_API) { mvd.esFlags |= MSG_ES_EXTENSIONS_2; - mvd.psFlags |= MSG_PS_EXTENSIONS_2; + mvd.psFlags |= MSG_PS_EXTENSIONS_2 | MSG_PS_MOREBITS; } } } diff --git a/src/server/mvd/game.c b/src/server/mvd/game.c index 1fdb46aba..509c4a6ab 100644 --- a/src/server/mvd/game.c +++ b/src/server/mvd/game.c @@ -775,7 +775,7 @@ void MVD_BroadcastPrintf(mvd_t *mvd, int level, int mask, const char *fmt, ...) } #define ES_MASK (MSG_ES_SHORTANGLES | MSG_ES_EXTENSIONS | MSG_ES_EXTENSIONS_2) -#define PS_MASK (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2) +#define PS_MASK (MSG_PS_EXTENSIONS | MSG_PS_EXTENSIONS_2 | MSG_PS_MOREBITS) static void MVD_SetServerState(client_t *cl, mvd_t *mvd) { @@ -798,6 +798,9 @@ static void MVD_SetServerState(client_t *cl, mvd_t *mvd) cl->psFlags &= ~PS_MASK; cl->esFlags |= mvd->esFlags & ES_MASK; cl->psFlags |= mvd->psFlags & PS_MASK; + + if (cl->protocol != PROTOCOL_VERSION_Q2PRO || cl->version < PROTOCOL_VERSION_Q2PRO_PLAYERFOG) + cl->psFlags &= ~MSG_PS_MOREBITS; } void MVD_SwitchChannel(mvd_client_t *client, mvd_t *mvd) diff --git a/src/server/mvd/parse.c b/src/server/mvd/parse.c index cc5e4395e..576bf811b 100644 --- a/src/server/mvd/parse.c +++ b/src/server/mvd/parse.c @@ -737,6 +737,12 @@ static void MVD_ParsePacketPlayers(mvd_t *mvd) player = &mvd->players[number]; bits = MSG_ReadWord(); + if (bits & PPS_MOREBITS) { + if (mvd->psFlags & MSG_PS_MOREBITS) + bits |= (uint32_t)MSG_ReadByte() << 16; + else + bits |= PPS_REMOVE; // MOREBITS == REMOVE for old demos + } #if USE_DEBUG if (mvd_shownet->integer > 2) { @@ -929,6 +935,8 @@ static void MVD_ParseServerData(mvd_t *mvd, int extrabits) if (!(mvd->flags & MVF_EXTLIMITS)) { MVD_Destroyf(mvd, "MVF_EXTLIMITS_2 without MVF_EXTLIMITS"); } + if (mvd->version >= PROTOCOL_VERSION_MVD_PLAYERFOG) + mvd->psFlags |= MSG_PS_MOREBITS; } #if 0 From 1d54743bba2fee4084a1da6f7df6bbb01b8cff30 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 20 Oct 2024 22:37:18 +0300 Subject: [PATCH 884/974] Set fog params too if only for sky factor. --- src/refresh/shader.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/refresh/shader.c b/src/refresh/shader.c index 9ca0e9ef7..1e0ae1b26 100644 --- a/src/refresh/shader.c +++ b/src/refresh/shader.c @@ -736,7 +736,7 @@ static void shader_setup_2d(void) static void shader_setup_fog(void) { - if (!glr.fog_bits) + if (!(glr.fog_bits | glr.fog_bits_sky)) return; VectorCopy(glr.fd.fog.color, gls.u_block.fog_color); From 887c468110c6d6bb9837b8c8e9192ece493f01fa Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Sun, 20 Oct 2024 22:40:31 +0300 Subject: [PATCH 885/974] Explicitly use ubuntu-24.04 for mingw CI. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d77e21be1..9eec5fed9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ env: jobs: mingw: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: arch: ["i686", "x86_64"] From 218da0a1b7b79f8d63790a226aa74bc7a4d3ec44 Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 21 Oct 2024 01:56:16 +0300 Subject: [PATCH 886/974] Enable fog for shadows/celshading. --- src/refresh/mesh.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/refresh/mesh.c b/src/refresh/mesh.c index 8e901519c..29fb16bab 100644 --- a/src/refresh/mesh.c +++ b/src/refresh/mesh.c @@ -43,7 +43,8 @@ static bool dotshading; static float celscale; static drawshadow_t drawshadow; -static mat4_t shadowmatrix; +static mat4_t m_shadow_view; +static mat4_t m_shadow_model; // fog hack #if USE_MD5 static md5_joint_t temp_skeleton[MD5_MAX_JOINTS]; @@ -439,7 +440,7 @@ static void draw_celshading(const uint16_t *indices, int num_indices) return; GL_BindTexture(TMU_TEXTURE, TEXNUM_BLACK); - GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE)); + GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE) | glr.fog_bits); if (gls.currentva) GL_ArrayBits(GLA_VERTEX); @@ -499,7 +500,7 @@ static drawshadow_t cull_shadow(const model_t *model) return SHADOW_YES; } -static void proj_matrix(GLfloat *matrix, const cplane_t *plane, const vec3_t dir) +static void proj_matrix(mat4_t matrix, const cplane_t *plane, const vec3_t dir) { matrix[ 0] = plane->normal[1] * dir[1] + plane->normal[2] * dir[2]; matrix[ 4] = -plane->normal[1] * dir[0]; @@ -524,7 +525,7 @@ static void proj_matrix(GLfloat *matrix, const cplane_t *plane, const vec3_t dir static void setup_shadow(void) { - mat4_t matrix, tmp; + mat4_t m_proj, m_rot; vec3_t dir; if (!drawshadow) @@ -537,12 +538,13 @@ static void setup_shadow(void) VectorSet(dir, 0, 0, 1); // project shadow on ground plane - proj_matrix(matrix, &glr.lightpoint.plane, dir); - GL_MultMatrix(tmp, glr.viewmatrix, matrix); + proj_matrix(m_proj, &glr.lightpoint.plane, dir); // rotate for entity - GL_RotationMatrix(matrix); - GL_MultMatrix(shadowmatrix, tmp, matrix); + GL_RotationMatrix(m_rot); + + GL_MultMatrix(m_shadow_model, m_proj, m_rot); + GL_MultMatrix(m_shadow_view, glr.viewmatrix, m_shadow_model); } static void draw_shadow(const uint16_t *indices, int num_indices) @@ -550,8 +552,12 @@ static void draw_shadow(const uint16_t *indices, int num_indices) if (!drawshadow) return; + // fog hack + if (glr.fog_bits) + memcpy(gls.u_block.m_model, m_shadow_model, sizeof(gls.u_block.m_model)); + // load shadow projection matrix - GL_LoadMatrix(shadowmatrix); + GL_LoadMatrix(m_shadow_view); // eliminate z-fighting by utilizing stencil buffer, if available if (gl_config.stencilbits) { @@ -561,7 +567,7 @@ static void draw_shadow(const uint16_t *indices, int num_indices) } GL_BindTexture(TMU_TEXTURE, TEXNUM_WHITE); - GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE)); + GL_StateBits(GLS_BLEND_BLEND | (meshbits & ~GLS_MESH_SHADE) | glr.fog_bits); if (gls.currentva) GL_ArrayBits(GLA_VERTEX); @@ -580,6 +586,10 @@ static void draw_shadow(const uint16_t *indices, int num_indices) qglDisable(GL_STENCIL_TEST); gl_static.stencil_buffer_bit |= GL_STENCIL_BUFFER_BIT; } + + // fog hack + if (glr.fog_bits) + GL_RotationMatrix(gls.u_block.m_model); } static const image_t *skin_for_mesh(image_t **skins, int num_skins) From 3e90b4da13359d22c4cf684a87f81c7d7effa82a Mon Sep 17 00:00:00 2001 From: Andrey Nazarov Date: Mon, 21 Oct 2024 02:00:08 +0300 Subject: [PATCH 887/974] =?UTF-8?q?Make=20=E2=80=98gl=5Ffog=E2=80=99=20a?= =?UTF-8?q?=20simple=20off/on=20toggle.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/client.asciidoc | 6 ++---- src/refresh/main.c | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index ed028c26d..94e37f7c8 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -688,10 +688,8 @@ gl_waterwarp:: GLSL backend. Default value is 0 (disabled). gl_fog:: - Enable re-release fog effect. Default value is 2. - - 0 — disable fog - - 1 — enable global fog - - 2 — enable global fog and height fog + Enable re-release fog effect. Only effective when using GLSL backend. + Default value is 1 (enabled). gl_flarespeed:: Specifies flare fading effect speed. Default value is 8. Set this to 0 diff --git a/src/refresh/main.c b/src/refresh/main.c index 6e56855af..84a3ace5d 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -668,10 +668,10 @@ void R_RenderFrame(const refdef_t *fd) if (gl_dynamic->integer != 1 || gl_vertexlight->integer) glr.fd.num_dlights = 0; - if (gl_fog->integer > 0) { + if (gl_static.use_shaders && gl_fog->integer > 0) { if (glr.fd.fog.density > 0) glr.fog_bits |= GLS_FOG_GLOBAL; - if (glr.fd.heightfog.density > 0 && glr.fd.heightfog.falloff > 0 && gl_fog->integer > 1) + if (glr.fd.heightfog.density > 0 && glr.fd.heightfog.falloff > 0) glr.fog_bits |= GLS_FOG_HEIGHT; if (glr.fd.fog.sky_factor > 0) glr.fog_bits_sky |= GLS_FOG_SKY; @@ -913,7 +913,7 @@ static void GL_Register(void) #endif gl_damageblend_frac = Cvar_Get("gl_damageblend_frac", "0.2", 0); gl_waterwarp = Cvar_Get("gl_waterwarp", "0", 0); - gl_fog = Cvar_Get("gl_fog", "2", 0); + gl_fog = Cvar_Get("gl_fog", "1", 0); gl_swapinterval = Cvar_Get("gl_swapinterval", "1", CVAR_ARCHIVE); gl_swapinterval->changed = gl_swapinterval_changed; From 6a45deb2a8b3c980083c57d05383ced28e7bd70d Mon Sep 17 00:00:00 2001 From: Lovro Grgic Date: Mon, 21 Oct 2024 19:21:06 +0200 Subject: [PATCH 888/974] rebased ch_hide_while_zoomed --- src/client/screen.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 940be77cb..82fa8279b 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1752,6 +1752,7 @@ static void SCR_ExecuteLayoutString(const char *s) x = scr.hud_x; y = scr.hud_y; + qboolean parsed_scope_pic = false; while (s) { token = COM_Parse(&s); @@ -1813,7 +1814,7 @@ static void SCR_ExecuteLayoutString(const char *s) qhandle_t pic = cl.image_precache[value]; // hack for action mod scope scaling if (Com_WildCmp("scope?x", token) || Com_WildCmp("scopes/*/scope?x", token)) { - scr.currently_scoped_in = true; + parsed_scope_pic = true; int x = scr.hud_x + (scr.hud_width - scr.scope_width) / 2; int y = scr.hud_y + (scr.hud_height - scr.scope_height) / 2; @@ -1823,13 +1824,13 @@ static void SCR_ExecuteLayoutString(const char *s) y + ch_y->integer, w, h, pic); } else { - scr.currently_scoped_in = false; R_DrawPic(x, y, pic); } } continue; } - + //save whether we parsed a scope pic + scr.currently_zoomed_in = parsed_scope_pic; if (!strcmp(token, "client")) { // draw a deathmatch client block int score, ping, time; From 2cc93fa62a9a459fb6db0721026815d7fdd2093a Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 13:36:23 -0400 Subject: [PATCH 889/974] Fixed some old typos --- src/action/a_ctf.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index b16ee1eae..3d40ffed5 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -519,10 +519,10 @@ void CTFFragBonuses(edict_t * targ, edict_t * inflictor, edict_t * attacker) // fragged a guy who hurt our flag carrier attacker->client->resp.score += CTF_CARRIER_DANGER_PROTECT_BONUS; gi.bprintf(PRINT_MEDIUM, - "%s defends %s's flag carrier against an agressive enemy\n", + "%s defends %s's flag carrier against an aggressive enemy\n", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.team)); IRC_printf(IRC_T_GAME, - "%n defends %n's flag carrier against an agressive enemy\n", + "%n defends %n's flag carrier against an aggressive enemy\n", attacker->client->pers.netname, CTFTeamName(attacker->client->resp.team)); return; From 9159e0d8ad83a73649f78301cceef3c598610fb5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 13:37:17 -0400 Subject: [PATCH 890/974] renamed to currently_scoped_in --- src/client/screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/screen.c b/src/client/screen.c index 82fa8279b..6208fcfbe 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1830,7 +1830,7 @@ static void SCR_ExecuteLayoutString(const char *s) continue; } //save whether we parsed a scope pic - scr.currently_zoomed_in = parsed_scope_pic; + scr.currently_scoped_in = parsed_scope_pic; if (!strcmp(token, "client")) { // draw a deathmatch client block int score, ping, time; From 74b47bae85c4d3c4baa6d2a604111f0b97b7a8ef Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 14:31:16 -0400 Subject: [PATCH 891/974] Set hc_boost default to 0, zoom crosshair default to 1 --- src/action/g_save.c | 2 +- src/action/g_weapon.c | 22 +++++++++++----------- src/client/screen.c | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/action/g_save.c b/src/action/g_save.c index 12f7db6dc..203e91e0b 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -457,7 +457,7 @@ void InitGame( void ) video_check_glclear = gi.cvar( "video_check_glclear", "0", 0 ); video_checktime = gi.cvar( "video_checktime", "15", 0 ); hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH ); //default ON - hc_boost = gi.cvar("hc_boost", "1", CVAR_LATCH); //rekkie -- allow HC to 'boost' the player + hc_boost = gi.cvar("hc_boost", "0", CVAR_LATCH); //rekkie -- allow HC to 'boost' the player hc_boost_percent = gi.cvar("hc_boost_percent", "100", 0); //rekkie -- allow HC to 'boost' the player hc_silencer = gi.cvar("hc_silencer", "0", 0); //rekkie -- allow HC to 'boost' the player wp_flags = gi.cvar( "wp_flags", WPF_DEFAULT_STR, 0 ); diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index e322825c6..0df108d3a 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -492,6 +492,7 @@ void ProduceShotgunDamageReport (edict_t *self) int i, total = 0, total_to_print, printed = 0; char *textbuf; gclient_t *cl; + int hc_boost_multiplier = 0; for (i = 0, cl = game.clients; i < game.maxclients; i++, cl++) { if (cl->took_damage) @@ -513,9 +514,11 @@ void ProduceShotgunDamageReport (edict_t *self) // Sanity check if (hc_boost_percent->value <= 0) - gi.cvar_set("hc_boost_multiplier", va("%d", 100)); + hc_boost_multiplier = 100; + //gi.cvar_set("hc_boost_multiplier", va("%d", 100)); if (hc_boost_percent->value > 1000) - gi.cvar_set("hc_boost_multiplier", va("%d", 1000)); + hc_boost_multiplier = 1000; + //gi.cvar_set("hc_boost_multiplier", va("%d", 1000)); float knockback = 300; // Double barrel if (self->client->pers.hc_mode) knockback = 150; // Single barrel @@ -529,17 +532,14 @@ void ProduceShotgunDamageReport (edict_t *self) VectorAdd(self->client->oldvelocity, kvel, self->client->oldvelocity); //if (self->is_bot == false) Com_Printf("%s %f %f %f\n", __func__, self->client->v_angle[0], self->client->v_angle[1], self->client->v_angle[2]); - - /* - if (hc_boost_multiplier->value < 0) - gi.cvar_set("hc_boost_multiplier", va("%d", 0)); - if (hc_boost_multiplier->value > 10) - gi.cvar_set("hc_boost_multiplier", va("%d", 10)); + // if (hc_boost_multiplier < 0) + // gi.cvar_set("hc_boost_multiplier", va("%d", 0)); + // if (hc_boost_multiplier->value > 10) + // gi.cvar_set("hc_boost_multiplier", va("%d", 10)); if (self->client->pers.hc_mode == 0) // Double barrel - self->velocity[2] += (300 * hc_boost_multiplier->value); + self->velocity[2] += (300 * hc_boost_multiplier); else // self->client->pers.hc_mode == 1 // Single barrel - self->velocity[2] += (150 * hc_boost_multiplier->value); - */ + self->velocity[2] += (150 * hc_boost_multiplier); } //rekkie -- allow HC to 'boost' the player -- e diff --git a/src/client/screen.c b/src/client/screen.c index 6208fcfbe..20f1ea13e 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1419,7 +1419,7 @@ void SCR_Init(void) ch_x = Cvar_Get("ch_x", "0", 0); ch_y = Cvar_Get("ch_y", "0", 0); - ch_hide_while_zoomed = Cvar_Get("ch_hide_while_zoomed", "0", 0); + ch_hide_while_zoomed = Cvar_Get("ch_hide_while_zoomed", "1", 0); scr_draw2d = Cvar_Get("scr_draw2d", "2", 0); scr_showturtle = Cvar_Get("scr_showturtle", "1", 0); From c7a801e010952544f73dd7b899083f15eb592426 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 15:09:25 -0400 Subject: [PATCH 892/974] Nade bonk is a little more accurate on hit location --- src/action/g_weapon.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index 0df108d3a..301763d9f 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -796,9 +796,14 @@ static void Grenade_Touch (edict_t *ent, edict_t *other, cplane_t *plane, csurfa vec3_t end; // Formula to handle damage amount. With /25-20 we'll get 0 dmg at minimum speed (500) int dmgBySpeed = (int)(grenSpeed) / 25 - 20; + + // Calculate the end position for the trace + VectorMA(ent->s.origin, 8192, ent->velocity, end); // Trace 8192 units in the direction of the velocity + PRETRACE(); - tr = gi.trace(ent->owner->s.origin, NULL, NULL, end, ent->owner, MASK_SHOT); + tr = gi.trace(ent->s.origin, NULL, NULL, end, ent->owner, MASK_SHOT); POSTTRACE(); + T_Damage(other, ent, ent->owner, ent->s.origin, tr.endpos, tr.plane.normal, dmgBySpeed, 0, DAMAGE_NO_ARMOR, MOD_GRENADE_IMPACT); } } From 5ec63e4bf37cc55cd98b18a82b96e5a7c94cdb4c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 16:52:13 -0400 Subject: [PATCH 893/974] Nade bonk is a little more accurate on hit location --- src/action/a_ctf.c | 2 ++ src/action/g_spawn.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 7b0ad5782..9259e68f7 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -1007,6 +1007,8 @@ void SetCTFStats(edict_t * ent) ent->client->ps.stats[STAT_TEAM1_PIC] = flagpic[TEAM1]; ent->client->ps.stats[STAT_TEAM2_PIC] = flagpic[TEAM2]; + gi.dprintf("flagpic[1] = %d, flagpic[2] = %d\n", flagpic[TEAM1], flagpic[TEAM2]); + ent->client->ps.stats[STAT_TEAM1_SCORE] = ctfgame.team1; ent->client->ps.stats[STAT_TEAM2_SCORE] = ctfgame.team2; diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 9e7f65fd6..06474e4d7 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1957,7 +1957,7 @@ void SP_worldspawn (edict_t * ent) } // Espionage HUD is setup in SetEspStats() - if (!esp->value) { + if (!esp->value && !ctf->value) { for(i = TEAM1; i <= teamCount; i++) { if (teams[i].skin_index[0] == 0) { From f59a56cead9b84a689345c9a8e889f30ac0c00ac Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 21 Oct 2024 17:14:09 -0400 Subject: [PATCH 894/974] Setup CTFSetupStatusbar --- src/action/a_ctf.c | 14 ++++++++++++-- src/action/a_ctf.h | 1 + src/action/g_spawn.c | 11 +---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 9259e68f7..33e62198d 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -1007,8 +1007,6 @@ void SetCTFStats(edict_t * ent) ent->client->ps.stats[STAT_TEAM1_PIC] = flagpic[TEAM1]; ent->client->ps.stats[STAT_TEAM2_PIC] = flagpic[TEAM2]; - gi.dprintf("flagpic[1] = %d, flagpic[2] = %d\n", flagpic[TEAM1], flagpic[TEAM2]); - ent->client->ps.stats[STAT_TEAM1_SCORE] = ctfgame.team1; ent->client->ps.stats[STAT_TEAM2_SCORE] = ctfgame.team2; @@ -1410,3 +1408,15 @@ void CTFCapReward(edict_t * ent) LogCapture(ent); } + +void CTFSetupStatusbar( void ) +{ + Q_strncatz(level.statusbar, + // Red Team + "yb -164 " "if 24 " "xr -24 " "pic 24 " "endif " "xr -60 " "num 2 26 " + // Blue Team + "yb -140 " "if 25 " "xr -24 " "pic 25 " "endif " "xr -60 " "num 2 27 " + // Flag carried + "if 23 " "yt 26 " "xr -24 " "pic 23 " "endif ", + sizeof(level.statusbar) ); +} \ No newline at end of file diff --git a/src/action/a_ctf.h b/src/action/a_ctf.h index c149b643f..844109559 100644 --- a/src/action/a_ctf.h +++ b/src/action/a_ctf.h @@ -133,3 +133,4 @@ void SP_info_teleport_destination (edict_t * ent); void ResetPlayers (void); void GetCTFScores(int *t1score, int *t2score); void CTFCapReward(edict_t *); +void CTFSetupStatusbar( void ); \ No newline at end of file diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 06474e4d7..8029fc01b 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1714,16 +1714,7 @@ void G_SetupStatusbar( void ) Q_strncatz(level.statusbar, "xr -50 yt 2 num 3 14 ", sizeof(level.statusbar)); if (ctf->value) - { - Q_strncatz(level.statusbar, - // Red Team - "yb -164 " "if 24 " "xr -24 " "pic 24 " "endif " "xr -60 " "num 2 26 " - // Blue Team - "yb -140 " "if 25 " "xr -24 " "pic 25 " "endif " "xr -60 " "num 2 27 " - // Flag carried - "if 23 " "yt 26 " "xr -24 " "pic 23 " "endif ", - sizeof(level.statusbar) ); - } + CTFSetupStatusbar(); else if( dom->value ) DomSetupStatusbar(); else if( esp->value ) From 112fead230075be73ae60639669c41e2daf04b36 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 10:01:43 -0400 Subject: [PATCH 895/974] Added documentation --- doc/action.md | 31 +++++++++++++++++++------------ doc/client.md | 5 +++++ doc/server.md | 7 +++++++ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/doc/action.md b/doc/action.md index 987066797..8cdb8adc4 100644 --- a/doc/action.md +++ b/doc/action.md @@ -46,7 +46,8 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Location Files](#location-files) - [Punching](#punching) - [Commands](#commands-15) - - [Lens command](#lens-command) + - [Sniper Zooming](#sniper-zooming) + - [Cvars](#cvars) - [Commands](#commands-16) - [New Say Variables](#new-say-variables) - [Time and Roundtimeleft](#time-and-roundtimeleft) @@ -100,7 +101,6 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [General quality of life improvements](#general-quality-of-life-improvements) - [Client Prediction](#client-prediction) - [Force Spawn items](#force-spawn-items) - - [Zoom Compensation](#zoom-compensation) - [Warmup](#warmup) - [Item Kit Mode](#item-kit-mode) - [Print rules](#print-rules) @@ -333,15 +333,24 @@ We have added the extra 'punch' attack, which basically is a weaker version of a - Client setting: - `punch` - this will preform a punch -### Lens command -The lens command is in TNG and offers increased control over the zooming of the sniper rifle. +### Sniper Zooming +The `lens` command is in TNG and offers increased control over the zooming of the sniper rifle. Zoom sensitivity can be adjusted based on the zoom level with these client cvars. Any `cl_zoom_xx` value set to `0` will not change the sensitivity for that zoom level. For example, if you have 2x set to `4` and 4x set to `0`, 4x will also be `4` since it is not changing from 2x's setting. Zooming all the way back out will restore your original sensitivity setting. + +#### Cvars +- Server cvars: + - `zoom_comp [0/1]` - server cvar, set to "1" to compensate zoom delay based on ping. Every 80ms ping reduces 1 frame, minimum of 1 frame. Default is "0", do not compensate +- Client cvars: + - `cl_zoom_autosens` - default 0, if 1, it enables the use of the cvars below + - `cl_zoom_2x` - default 0, set this to the sensitivity level you want to use at a 2x zoom + - `cl_zoom_4x` - default 0, set this to the sensitivity level you want to use at a 4x zoom + - `cl_zoom_6x` - default 0, set this to the sensitivity level you want to use at a 6x zoom #### Commands -- Client settings: - - `lens` - changes zoom on sniper rifle - - `lens [#]` - 1,2,4,6 - if you use any other number it'll go to the nearest zoom mode - - `lens in` - zooms the rifle in to the next zooming level - - `lens out` - zooms the rifle out to the previous zooming level +- Client commands: + - `lens` - changes zoom on sniper rifle + - `lens [#]` - 1,2,4,6 - if you use any other number it'll go to the nearest zoom mode + - `lens in` - zooms the rifle in to the next zooming level + - `lens out` - zooms the rifle out to the previous zooming level ### New Say Variables TNG offers several new variables clients can use in their text (Client settings): @@ -659,9 +668,6 @@ Antilag allows server operator to enable lag-compensation for aiming with hitsca #### Force Spawn items `g_spawn_items [0/1]` - server cvar, set to "1" to allow for item/ammo/weapon spawns in games that you choose your starting weapon, for example, dm_choose, teamplay, etc. Forced set to "0" for matchmode. Set "0" for classic play. -#### Zoom Compensation -`zoom_comp [0/1]` - server cvar, set to "1" to compensate zoom delay based on ping. Every 80ms ping reduces 1 frame, minimum of 1 frame. Default is "0", do not compensate - #### Warmup `warmup [#]` - server cvar, set in seconds, minimum is 15, 20 is common. This is the amount of time in seconds after both captains ready up that warmups will continue until the first round begins. Set to 0 to disable warmup entirely. `warmup_bots [#]` - server cvar, set in number of bots to add. This adds this many bots to the warmup phase of matchmode. Set to '0' to disable warmup_bots, suggested value for usage is '4' to '6'. @@ -695,6 +701,7 @@ Inspired by AQ2:ETE, these additions are optional server vars to create a differ ### Gun mechanics/enhancements - `gun_dualmk23_enhance [0/1]` - server cvar, default 0. If enabled, this allows both the silencer and the laser sight to be used on the Dual MK23 Pistols. - `use_gren_bonk [0/1]` - server cvar, default 0. If enabled, this enables impact damage of the grenade to cause damage on direct contact with a player. The speed of which the grenade is thrown will determine the damage dealt. Thanks to JukS for the idea and the code. +- `lca_grenade` - server cvar, default 0. If enabled, players can pull the grenade pin during Lights Camera Action, but they still cannot throw it until Action! ### Highscores Borrowing code from OpenTDM (thank you Skuller!), high scores are stored in a local file on the server. Each time a new high score is achieved, it is registered in this file. The high scores are separated by map and by game mode. For example, `highscores/dm/wizs.txt` is the highscores file for the map `wizs` in `dm` mode. diff --git a/doc/client.md b/doc/client.md index 91b31d9ba..fcaf17fd7 100644 --- a/doc/client.md +++ b/doc/client.md @@ -92,6 +92,7 @@ - [ch\_alpha](#ch_alpha) - [ch\_scale](#ch_scale) - [ch\_x; ch\_y](#ch_x-ch_y) + - [ch\_hide\_while\_zoomed](#ch_hide_while_zoomed) - [xhair\_enabled](#xhair_enabled) - [xhair\_firing\_error](#xhair_firing_error) - [xhair\_movement\_error](#xhair_movement_error) @@ -892,6 +893,10 @@ These variables specify the crosshair image offset, counted in pixels from the default position in center of the game screen. Default values are 0 (draw in center). +### ch\_hide\_while\_zoomed +AQtion-specific client cvar, enabling this will pervent the default crosshair +from being drawn under the sniper zoom crosshair + ### xhair_enabled Enables new xhair system which replaces the classic crosshair. diff --git a/doc/server.md b/doc/server.md index 805528e3a..9e478e580 100644 --- a/doc/server.md +++ b/doc/server.md @@ -96,6 +96,7 @@ - [map\_visibility\_patch](#map_visibility_patch) - [com\_fatal\_error](#com_fatal_error) - [com\_debug\_break](#com_debug_break) + - [g\_protocol\_extensions](#g_protocol_extensions) - [Commands](#commands) - [Generic](#generic-1) - [status \[mode\]](#status-mode) @@ -844,6 +845,12 @@ exit. Default value is 0 (disabled). Development variable that turns all errors into debug breakpoints. Default value is 0 (disabled). +### g\_protocol\_extensions +Server operators can enable protocol extensions, which is not compatible with +clients that do not have protocol extensions. The benefit is that +the limits around items such as sounds, images, map sizes and such are +significantly heightened. + # Commands ## Generic From adc1e93a89b99a11c45f177db49de13c71053e11 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 11:18:58 -0400 Subject: [PATCH 896/974] Statically linking some libraries --- meson.build | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 69ce7e145..ea638fb16 100644 --- a/meson.build +++ b/meson.build @@ -374,6 +374,7 @@ png = dependency('libpng', version: '>= 1.6.11', required: get_option('libpng'), default_options: fallback_opt, + static: true ) if png.found() texture_formats += 'png' @@ -406,7 +407,8 @@ game_deps = [zlib] jpeg = dependency('libjpeg', required: get_option('libjpeg'), - default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ] + default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], + static: true ) if jpeg.found() @@ -444,7 +446,8 @@ endif jansson_dep = dependency('jansson', version: '>= 2.8', fallback: ['jansson', 'jansson_dep'], - required: true # Or set to true/false as needed + required: true, # Or set to true/false as needed + static: true ) if jansson_dep.found() game_deps += jansson_dep @@ -455,6 +458,7 @@ if not win32 required: get_option('uuid'), allow_fallback: win32, default_options: fallback_opt, + static: true ) if uuid.found() From b6f9a27e700d74b96c3dee3d7e36e395bf24e7be Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 11:29:31 -0400 Subject: [PATCH 897/974] Building libjansson from source --- .github/workflows/build.yml | 9 ++++++++- meson.build | 2 -- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e85ccce19..a30ce0454 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,7 +133,14 @@ jobs: libwayland-dev wayland-protocols libdecor-0-dev \ libavcodec-dev libavformat-dev libavutil-dev \ libswresample-dev libswscale-dev \ - uuid-dev patchelf libjansson-dev + uuid-dev patchelf + + - name: Install libjansson libs from source + run: | + wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.gz + tar xfz jansson-2.14.tar.gz && cd jansson-2.14 + CFLAGS="-fPIC" ./configure + make && make install - name: Build run: | diff --git a/meson.build b/meson.build index ea638fb16..74d759f7f 100644 --- a/meson.build +++ b/meson.build @@ -374,7 +374,6 @@ png = dependency('libpng', version: '>= 1.6.11', required: get_option('libpng'), default_options: fallback_opt, - static: true ) if png.found() texture_formats += 'png' @@ -408,7 +407,6 @@ game_deps = [zlib] jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], - static: true ) if jpeg.found() From 43e84914444875b2cd1aa43bd758d58cee6dd5a2 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 11:32:36 -0400 Subject: [PATCH 898/974] sudo make install --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a30ce0454..fb4136776 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,7 +140,7 @@ jobs: wget https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.gz tar xfz jansson-2.14.tar.gz && cd jansson-2.14 CFLAGS="-fPIC" ./configure - make && make install + make && sudo make install - name: Build run: | From 5b12fae031aa882d1639c46fdc80f2ad4ffd652d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 11:54:11 -0400 Subject: [PATCH 899/974] Fixed wording on pickup command errors --- src/action/a_game.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 0ede17936..3bf074c53 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1476,7 +1476,7 @@ void Cmd_Pickup_f(edict_t* ent) // Check if msgflags supports this if (!(MSGFLAGS(PICKUP_REQ_MSG))) { gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_ERROR); - gi.dprintf("%s: %s attempted to send a pickup request but msgflags needs to be %d (currently %d)\n", __func__, ent->client->pers.netname, PICKUP_REQ_MSG, msgflags->value); + gi.dprintf("%s: %s attempted to send a pickup request but msgflags needs to be %d\n", __func__, ent->client->pers.netname, PICKUP_REQ_MSG); return; } @@ -1485,7 +1485,7 @@ void Cmd_Pickup_f(edict_t* ent) snprintf(msg, sizeof(msg), "**%s** is requesting a pickup match at **%s (%s:%s)**", ent->client->pers.netname, hostname->string, server_ip->string, net_port->string); } else { gi.cprintf(ent, PRINT_HIGH, MSG_PICKUP_SERVER_ERROR); - gi.dprintf("%s: %s attempted to send a pickup request but the server is missing hostname, server_ip or net_port\n", __func__, ent->client->pers.netname); + gi.dprintf("%s: %s attempted to send a pickup request but the server is missing hostname, server_ip and/or net_port\n", __func__, ent->client->pers.netname); return; } From 200a1217a0d0a4c705b0b2deaed44e74c88f9e24 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 16:08:35 -0400 Subject: [PATCH 900/974] Added ctf_dyn_respawn, sv bots fixed in TP mode, some optimizations.. --- doc/action.md | 3 +- src/action/a_ctf.c | 58 +++++++++ src/action/a_ctf.h | 3 + src/action/botlib/botlib.h | 1 + src/action/botlib/botlib_communication.c | 30 +---- src/action/botlib/botlib_spawn.c | 147 +++++++++++++++-------- src/action/botlib/botlib_utils.c | 45 +++++++ src/action/g_local.h | 1 + src/action/g_save.c | 1 + src/action/g_spawn.c | 3 + 10 files changed, 212 insertions(+), 80 deletions(-) diff --git a/doc/action.md b/doc/action.md index 6c7aab6cb..e81bb50fa 100644 --- a/doc/action.md +++ b/doc/action.md @@ -206,9 +206,10 @@ Flag locations and CTF player spawns should be specified in tng/mapname.ctf file - Server settings: - `ctf [0/1]` - This will turn CTF on (1) or off (0). It will automatically turn Teamplay on (1). - `capturelimit [#]` - The maximum number of captures before a map will change. Set to 0 to ignore that. - - `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. + - `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. Overrides respawn timers in .ctf files - `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. - `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) + - `ctf_dyn_respawn [0/1]` - Default 0, this will reduce the respawn timer for a losing team periodically. Will self-correct as that team mounts a comeback. - Client settings: - `drop flag` - Drop the flag if you're holding it diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 33e62198d..d3516be04 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -57,6 +57,7 @@ cvar_t *ctf_mode = NULL; cvar_t *ctf_dropflag = NULL; cvar_t *ctf_respawn = NULL; cvar_t *ctf_model = NULL; +cvar_t *ctf_dyn_respawn = NULL; //----------------------------------------------------------------------------- @@ -90,6 +91,8 @@ qboolean CTFLoadConfig(char *mapname) /* zero is perfectly acceptable respawn time, but we want to know if it came from the config or not */ ctfgame.spawn_red = -1; ctfgame.spawn_blue = -1; + ctfgame.spawn_red_default = -1; + ctfgame.spawn_blue_default = -1; sprintf (buf, "%s/tng/%s.ctf", GAMEVERSION, mapname); fh = fopen (buf, "r"); @@ -141,11 +144,13 @@ qboolean CTFLoadConfig(char *mapname) if(ptr) { gi.dprintf(" Red : %s\n", ptr); ctfgame.spawn_red = atoi(ptr); + ctfgame.spawn_red_default = atoi(ptr); } ptr = INI_Find(fh, "respawn", "blue"); if(ptr) { gi.dprintf(" Blue : %s\n", ptr); ctfgame.spawn_blue = atoi(ptr); + ctfgame.spawn_blue_default = atoi(ptr); } gi.dprintf(" Flags\n"); @@ -252,6 +257,51 @@ void CTFSetTeamSpawns(int team, char *str) } while(next != NULL); } +#define POINT_DIFFERENTIAL 3 +#define MIN_RESPAWN_TIME 1 + +static void AdjustRespawnTime(int* spawn_time, int time_reduction, int team) { + *spawn_time = max(MIN_RESPAWN_TIME, *spawn_time - time_reduction); + CenterPrintTeam(team, va("Dynamically adjusting respawn rates, your team is now respawning every %d seconds\n", *spawn_time)); + //gi.dprintf("%s: Respawn time for team %d adjusted to %d\n", __func__, team, *spawn_time); +} + +static void ResetRespawnTime(int team) { + if (team == TEAM1) { + ctfgame.spawn_red = ctfgame.spawn_red_default; + CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_red_default)); + } else if (team == TEAM2) { + ctfgame.spawn_blue = ctfgame.spawn_blue_default; + CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_blue_default)); + } + //gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, spawn_time); +} + +void CTFDynamicRespawnTimer(void) +{ + if (!ctf_dyn_respawn->value) + return; + + int score_diff = abs(ctfgame.team1 - ctfgame.team2); + int time_reduction = score_diff / POINT_DIFFERENTIAL; + + gi.dprintf("score diff: %d, time reduction: %d\n", score_diff, time_reduction); + + if (ctfgame.team1 > ctfgame.team2) { + if (score_diff >= POINT_DIFFERENTIAL) { + AdjustRespawnTime(&ctfgame.spawn_blue, time_reduction, TEAM2); + } else { + ResetRespawnTime(TEAM2); + } + } else if (ctfgame.team2 > ctfgame.team1) { + if (score_diff >= POINT_DIFFERENTIAL) { + AdjustRespawnTime(&ctfgame.spawn_red, time_reduction, TEAM1); + } else { + ResetRespawnTime(TEAM1); + } + } +} + /* returns the respawn time for this particular client */ int CTFGetRespawnTime(edict_t *ent) { @@ -339,6 +389,7 @@ void CTFSwapTeams(void) if (ent->inuse && ent->client->resp.team) { ent->client->resp.team = CTFOtherTeam(ent->client->resp.team); AssignSkin(ent, teams[ent->client->resp.team].skin, false); + ent->client->ctf_hasflag = false; } } @@ -622,6 +673,7 @@ void CTFResetFlag(int team) if (ent->client->inventory[ITEM_INDEX(teamFlag)]) { Drop_Item(ent, teamFlag); ent->client->inventory[ITEM_INDEX(teamFlag)] = 0; + ent->client->ctf_hasflag = false; } } @@ -684,6 +736,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) other->client->pers.netname, CTFOtherTeamName(team)); other->client->inventory[ITEM_INDEX(enemy_flag_item)] = 0; + other->client->ctf_hasflag = false; ctfgame.last_flag_capture = level.framenum; ctfgame.last_capture_team = team; @@ -692,6 +745,8 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) else ctfgame.team2++; + CTFDynamicRespawnTimer(); // Dynamic respawn time + gi.sound(ent, CHAN_RELIABLE + CHAN_NO_PHS_ADD + CHAN_VOICE, gi.soundindex("tng/flagcap.wav"), 1, ATTN_NONE, 0); @@ -767,6 +822,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) other->client->inventory[ITEM_INDEX(flag_item)] = 1; other->client->resp.ctf_flagsince = level.framenum; + other->client->ctf_hasflag = true; // pick up the flag // if it's not a dropped flag, we just make is disappear @@ -825,6 +881,7 @@ void CTFDeadDropFlag(edict_t * self) dropped->think = CTFDropFlagThink; dropped->nextthink = level.framenum + CTF_AUTO_FLAG_RETURN_TIMEOUT * HZ; dropped->touch = CTFDropFlagTouch; + self->client->ctf_hasflag = false; } } @@ -845,6 +902,7 @@ void CTFDrop_Flag(edict_t * ent, gitem_t * item) dropped->think = CTFDropFlagThink; dropped->nextthink = level.framenum + CTF_AUTO_FLAG_RETURN_TIMEOUT * HZ; dropped->touch = CTFDropFlagTouch; + ent->client->ctf_hasflag = false; } } else { if (rand() & 1) diff --git a/src/action/a_ctf.h b/src/action/a_ctf.h index 844109559..ac98b28a5 100644 --- a/src/action/a_ctf.h +++ b/src/action/a_ctf.h @@ -47,6 +47,8 @@ typedef struct ctfgame_s { /* team spawn times in seconds */ int spawn_red; int spawn_blue; + int spawn_red_default; + int spawn_blue_default; qboolean custom_spawns; char author[64]; char comment[128]; @@ -62,6 +64,7 @@ extern cvar_t *ctf_mode; extern cvar_t *ctf_dropflag; extern cvar_t *ctf_respawn; extern cvar_t *ctf_model; +extern cvar_t *ctf_dyn_respawn; #define CTF_TEAM1_SKIN "ctf_r" #define CTF_TEAM2_SKIN "ctf_b" diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index c1f1a39f5..d0a3184c0 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -533,6 +533,7 @@ qboolean BOTLIB_SkillChance(float skilltype); float BOTLIB_SkillMultiplier(float skill_level, bool increase_with_skill); float BOTLIB_SKILL_Reaction(float reaction_skill); void BOTLIB_Debug(const char *debugmsg, ...); +edict_t* BOTLIB_GetRandomBot(int team, qboolean filtered); // =========================================================================== // botlib_personality.c diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index 7ac1f5b46..d7e595bce 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -56,32 +56,6 @@ void UpdateBotChat(void) { ProcessChatQueue(level.framenum); } -// Function to get a random bot -edict_t* getRandomBot(void) -{ - edict_t* bots[MAX_CLIENTS]; - int botCount = 0; - - // Populate the bots array with pointers to bots - for (int i = 0; i < num_players; i++) - { - if (players[i]->is_bot) - { - bots[botCount++] = players[i]; - } - } - - // If we found any bots, return a random one - if (botCount > 0) - { - int randomIndex = rand() % botCount; // Generate a random index - return bots[randomIndex]; - } - - // If no bots were found, return NULL - return NULL; -} - // Borrowed from LTK bots #define DBC_WELCOMES 4 char *botchat_welcomes[DBC_WELCOMES] = @@ -278,8 +252,8 @@ void BOTLIB_Chat(edict_t* bot, bot_chat_types_t chattype) // Copy back to 'text' snprintf(text, sizeof(text), "%s", message); - if (getRandomBot() != NULL) - bot = getRandomBot(); + if (BOTLIB_GetRandomBot(0, false) != NULL) + bot = BOTLIB_GetRandomBot(0, false); } // bot_personality diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 3ed11333f..c6bfd5354 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -1728,45 +1728,74 @@ void BOTLIB_RemoveTeamplayBot(int team) int i; edict_t* bot; - for (i = 0; i < game.maxclients; i++) - { - bot = g_edicts + i + 1; - if (bot->inuse) // Ent in use - { - if (bot->is_bot) // Is a bot - { - // Only kick when the bot isn't actively in a match - //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) - if (bot->client->resp.team == team) // && team_round_going == 0) - { - //if (random() < 0.20) // Randomly kick a bot - { - //rekkie -- Fake Bot Client -- s - if (bot_reportasclient->value) - SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' - //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client - //rekkie -- Fake Bot Client -- e - - if (team == TEAM1) - bot_connections.total_team1--; - else if (team == TEAM2) - bot_connections.total_team2--; - else if (team == TEAM3) - bot_connections.total_team3--; - - game.bot_count--; - - if (bot->health) - player_die(bot, bot, bot, 100000, vec3_origin); - ClientDisconnect(bot); - break; - } - } - } - } + // Try the filtered approach first, if no bot returns, then do an unfiltered search + bot = BOTLIB_GetRandomBot(team, true); + if (!bot){ + bot = BOTLIB_GetRandomBot(team, false); } + + if (bot_reportasclient->value) + SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' + //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client + //rekkie -- Fake Bot Client -- e + + if (team == TEAM1) + bot_connections.total_team1--; + else if (team == TEAM2) + bot_connections.total_team2--; + else if (team == TEAM3) + bot_connections.total_team3--; + + game.bot_count--; + + if (bot->health) + player_die(bot, bot, bot, 100000, vec3_origin); + ClientDisconnect(bot); } +// for (i = 0; i < game.maxclients; i++) +// { +// bot = g_edicts + i + 1; +// if (bot->inuse) // Ent in use +// { +// if (bot->is_bot) // Is a bot +// { +// // Only kick when the bot isn't actively in a match +// //if (bot->client->resp.team == team && (team_round_going == 0 || bot->health <= 0 || bot->solid == SOLID_NOT)) +// if (bot->client->resp.team == team) // && team_round_going == 0) +// { +// if (ctf->value && bot_count > 1 && bot->client->ctf_hasflag) { +// // Don't remove the bot if they have the enemy flag and there are other bots on the team +// continue; +// } +// //if (random() < 0.20) // Randomly kick a bot +// { +// //rekkie -- Fake Bot Client -- s +// if (bot_reportasclient->value) +// SV_BotDisconnect(bot->client->pers.netname); // So the server can fake the bot as a 'client' +// //gi.SV_BotDisconnect(bot->client->pers.netname); // So the server can remove the fake client +// //rekkie -- Fake Bot Client -- e + +// if (team == TEAM1) +// bot_connections.total_team1--; +// else if (team == TEAM2) +// bot_connections.total_team2--; +// else if (team == TEAM3) +// bot_connections.total_team3--; + +// game.bot_count--; + +// if (bot->health) +// player_die(bot, bot, bot, 100000, vec3_origin); +// ClientDisconnect(bot); +// break; +// } +// } +// } +// } +// } +// } + // Change bot [from team] ==> [to team] // Conditions: Bot must be in a team. Change occurs after round ends. void BOTLIB_ChangeBotTeam(int from_team, int to_team) @@ -1990,21 +2019,37 @@ int BOTLIB_TPBotTeamScaling(void) int total_players = bot_connections.total_bots + bot_connections.total_humans; int desired_total_players = (int)bot_playercount->value; - if (desired_total_players && total_players < desired_total_players) { - // Scale up bots when total players are less than desired - bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; - bot_connections.scale_up = true; - bot_connections.scale_dn = false; - } else if (total_players > desired_total_players) { - // Scale down bots when total players exceed desired - bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; - bot_connections.scale_up = false; - bot_connections.scale_dn = true; - } else { - // No scaling needed - bot_connections.scale_up = false; - bot_connections.scale_dn = false; - } + if (bot_playercount->value) { + if (desired_total_players && total_players < desired_total_players) { + // Scale up bots when total players are less than desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } else if (total_players > desired_total_players) { + // Scale down bots when total players exceed desired + bot_connections.desire_bots = desired_total_players - bot_connections.total_humans; + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } else { + // No scaling needed + bot_connections.scale_up = false; + bot_connections.scale_dn = false; + } + } else { // just sv bots # here + if (bot_connections.total_bots < bot_connections.desire_bots) { + // Scale up bots when total bots are less than desired + bot_connections.scale_up = true; + bot_connections.scale_dn = false; + } else if (bot_connections.total_bots > bot_connections.desire_bots) { + // Scale down bots when total bots exceed desired + bot_connections.scale_up = false; + bot_connections.scale_dn = true; + } else { + // No scaling needed + bot_connections.scale_up = false; + bot_connections.scale_dn = false; + } + } // Ensure desire_bots is not negative if (bot_connections.desire_bots < 0) { diff --git a/src/action/botlib/botlib_utils.c b/src/action/botlib/botlib_utils.c index 6ebdec63e..3b9d5fee0 100644 --- a/src/action/botlib/botlib_utils.c +++ b/src/action/botlib/botlib_utils.c @@ -121,3 +121,48 @@ void BOTLIB_Debug(const char *debugmsg, ...) return; gi.dprintf("%s", debugmsg); } + + +// Function to get a random bot +edict_t* BOTLIB_GetRandomBot(int team, qboolean filtered) +{ + edict_t* bots[MAX_CLIENTS]; + int botCount = 0; + + // Validate team value + if (team < 0 || team > 3) { + return NULL; // Invalid team value + } + + // Populate the bots array with pointers to bots + for (int i = 0; i < num_players; i++) + { + if (players[i]->is_bot && players[i]->inuse) + { + // Check team if specified + if (team > 0 && players[i]->client->resp.team != team) { + continue; + } + + // Apply filtering criteria + if (filtered) { + if ((ctf->value && players[i]->client->ctf_hasflag) || + (esp->value && IS_LEADER(players[i]))) { + continue; + } + } + + bots[botCount++] = players[i]; + } + } + + // If we found any bots, return a random one + if (botCount > 0) + { + int randomIndex = rand() % botCount; // Generate a random index + return bots[randomIndex]; + } + + // If no bots were found, return NULL + return NULL; +} \ No newline at end of file diff --git a/src/action/g_local.h b/src/action/g_local.h index d7f233b7a..763bf2f48 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2238,6 +2238,7 @@ struct gclient_s edict_t *ctf_grapple; // entity of grapple int ctf_grapplestate; // true if pulling int ctf_grapplereleaseframe; // frame of grapple release + qboolean ctf_hasflag; // set to true if this client has the flag #if AQTION_EXTENSION //AQTION - Reki: Teammate indicators diff --git a/src/action/g_save.c b/src/action/g_save.c index 7a183c92d..d315a4aee 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -419,6 +419,7 @@ void InitGame( void ) ctf_dropflag = gi.cvar( "ctf_dropflag", "1", 0 ); ctf_respawn = gi.cvar( "ctf_respawn", "4", 0 ); ctf_model = gi.cvar( "ctf_model", "male", CVAR_LATCH ); + ctf_dyn_respawn = gi.cvar( "ctf_dyn_respawn", "0", 0 ); medkit_drop = gi.cvar( "medkit_drop", "0", 0 ); medkit_time = gi.cvar( "medkit_time", "30", 0 ); medkit_instant = gi.cvar( "medkit_instant", "0", 0 ); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 15434aca8..dc0ab71a6 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2151,6 +2151,9 @@ int LoadFlagsFromFile (const char *mapname) // FIXME: remove this functionality completely in the future gi.dprintf("Warning: .flg files are deprecated, use .ctf ones for more control!\n"); + ctfgame.spawn_red_default = ctf_respawn->value; + ctfgame.spawn_blue_default = ctf_respawn->value; + while (fgets(buf, 1000, fp) != NULL) { length = strlen(buf); From cbe9cbda9f51918bd1d8b5a66a8e78e0e42d5823 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Tue, 22 Oct 2024 16:27:42 -0400 Subject: [PATCH 901/974] Adjusted the respawn timer to use a static value to subtract from --- src/action/a_ctf.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index d3516be04..23a2b5a59 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -262,19 +262,27 @@ void CTFSetTeamSpawns(int team, char *str) static void AdjustRespawnTime(int* spawn_time, int time_reduction, int team) { *spawn_time = max(MIN_RESPAWN_TIME, *spawn_time - time_reduction); + if (team == TEAM1) { + ctfgame.spawn_red = *spawn_time; + } else if (team == TEAM2) { + ctfgame.spawn_blue = *spawn_time; + } else { + return; // invalid team passed + } CenterPrintTeam(team, va("Dynamically adjusting respawn rates, your team is now respawning every %d seconds\n", *spawn_time)); - //gi.dprintf("%s: Respawn time for team %d adjusted to %d\n", __func__, team, *spawn_time); + gi.dprintf("%s: Respawn time for team %d adjusted to %d\n", __func__, team, *spawn_time); } static void ResetRespawnTime(int team) { if (team == TEAM1) { ctfgame.spawn_red = ctfgame.spawn_red_default; CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_red_default)); + gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_red_default); } else if (team == TEAM2) { ctfgame.spawn_blue = ctfgame.spawn_blue_default; CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_blue_default)); + gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_blue_default); } - //gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, spawn_time); } void CTFDynamicRespawnTimer(void) @@ -285,17 +293,15 @@ void CTFDynamicRespawnTimer(void) int score_diff = abs(ctfgame.team1 - ctfgame.team2); int time_reduction = score_diff / POINT_DIFFERENTIAL; - gi.dprintf("score diff: %d, time reduction: %d\n", score_diff, time_reduction); - if (ctfgame.team1 > ctfgame.team2) { if (score_diff >= POINT_DIFFERENTIAL) { - AdjustRespawnTime(&ctfgame.spawn_blue, time_reduction, TEAM2); + AdjustRespawnTime(&ctfgame.spawn_blue_default, time_reduction, TEAM2); } else { ResetRespawnTime(TEAM2); } } else if (ctfgame.team2 > ctfgame.team1) { if (score_diff >= POINT_DIFFERENTIAL) { - AdjustRespawnTime(&ctfgame.spawn_red, time_reduction, TEAM1); + AdjustRespawnTime(&ctfgame.spawn_red_default, time_reduction, TEAM1); } else { ResetRespawnTime(TEAM1); } From c7cf76ed2e5d5db59d7495e5c6492f83c7571faa Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 23 Oct 2024 09:26:57 -0400 Subject: [PATCH 902/974] Check for invalid node on dropped flag and return to avoid segfault --- src/action/botlib/botlib_ctf.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 9eb82d7c8..6950f541d 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -4,6 +4,8 @@ ctf_status_t bot_ctf_status; +qboolean ctf_goal_node_warning = false; + // Does this bot have the flag? qboolean BOTLIB_Carrying_Flag(edict_t* self) { @@ -736,8 +738,20 @@ void BOTLIB_CTF_Goals(edict_t* self) if (flag_to_get == FLAG_T2_NUM) n = bot_ctf_status.flag2_curr_node; - if (BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) + if (n == INVALID && ctf_goal_node_warning == false){ + ctf_goal_node_warning = true; + gi.dprintf("%s: Warning: Flag location is at an INVALID node.\n", __func__); + return; + } + + // Extra safety check in case ctf_goal_node_warning is true and isn't caught above + // this is a segfault preventor + if (n == INVALID) + return; + + if (n != INVALID && BOTLIB_CanGotoNode(self, nodes[n].nodenum, false)) { + ctf_goal_node_warning = false; if (flag_to_get == FLAG_T1_NUM) // Dropped team flag { self->bot.bot_ctf_state = BOT_CTF_STATE_RETURN_TEAM_FLAG; From 821221687a00cde8fe111019f8ca12d405e84c86 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 23 Oct 2024 10:04:39 -0400 Subject: [PATCH 903/974] Adjusted messaging criteria, updated docs --- doc/action.md | 6 +++--- src/action/a_ctf.c | 39 ++++++++++++++++++++++------------ src/action/botlib/botlib_ctf.c | 3 ++- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/doc/action.md b/doc/action.md index e81bb50fa..a39fcbc1a 100644 --- a/doc/action.md +++ b/doc/action.md @@ -206,10 +206,10 @@ Flag locations and CTF player spawns should be specified in tng/mapname.ctf file - Server settings: - `ctf [0/1]` - This will turn CTF on (1) or off (0). It will automatically turn Teamplay on (1). - `capturelimit [#]` - The maximum number of captures before a map will change. Set to 0 to ignore that. - - `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. Overrides respawn timers in .ctf files + - `ctf_respawn [#]` - The time in seconds before a player will respawn after having died. Will not override respawn timers from .ctf files. - `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. - `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) - - `ctf_dyn_respawn [0/1]` - Default 0, this will reduce the respawn timer for a losing team periodically. Will self-correct as that team mounts a comeback. + - `ctf_dyn_respawn [0/1]` - Default 0, if enabled, this will reduce the respawn timer for a losing team periodically. Will self-correct as that team mounts a comeback. The score discrepancy gets evaluated when a flag is captured on either team. - Client settings: - `drop flag` - Drop the flag if you're holding it @@ -251,7 +251,7 @@ The voice command allows clients to play taunts for other players to hear. (as l #### Commands - Server settings: - - `use_voice [0/1]` - When on (1), it will allow the use of voice commands. + - `use_voice [0/1]` - When on (1), it will allow the use of voice commands. Requires a populated sndlist.ini file on the server end. - Client settings: - `voice "sound.wav"` - this will play sound.wav for all players to hear. (as long as the others have sound.wav) This command requires the .wav extension. (client side) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 23a2b5a59..e4ac18d7f 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -261,27 +261,36 @@ void CTFSetTeamSpawns(int team, char *str) #define MIN_RESPAWN_TIME 1 static void AdjustRespawnTime(int* spawn_time, int time_reduction, int team) { - *spawn_time = max(MIN_RESPAWN_TIME, *spawn_time - time_reduction); - if (team == TEAM1) { - ctfgame.spawn_red = *spawn_time; - } else if (team == TEAM2) { - ctfgame.spawn_blue = *spawn_time; - } else { - return; // invalid team passed - } - CenterPrintTeam(team, va("Dynamically adjusting respawn rates, your team is now respawning every %d seconds\n", *spawn_time)); - gi.dprintf("%s: Respawn time for team %d adjusted to %d\n", __func__, team, *spawn_time); + int new_spawn_time = max(MIN_RESPAWN_TIME, *spawn_time - time_reduction); + + // Only update and print if the respawn time has actually changed + if (*spawn_time != new_spawn_time) { + *spawn_time = new_spawn_time; + if (team == TEAM1) { + ctfgame.spawn_red = *spawn_time; + } else if (team == TEAM2) { + ctfgame.spawn_blue = *spawn_time; + } else { + return; // invalid team passed + } + CenterPrintTeam(team, va("Dynamically adjusting respawn rates, your team is now respawning every %d seconds\n", *spawn_time)); + gi.dprintf("%s: Respawn time for team %d adjusted to %d\n", __func__, team, *spawn_time); + } } static void ResetRespawnTime(int team) { if (team == TEAM1) { + if (ctfgame.spawn_red != ctfgame.spawn_red_default) { + CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_red_default)); + gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_red_default); + } ctfgame.spawn_red = ctfgame.spawn_red_default; - CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_red_default)); - gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_red_default); } else if (team == TEAM2) { + if (ctfgame.spawn_blue != ctfgame.spawn_blue_default) { + CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_blue_default)); + gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_blue_default); + } ctfgame.spawn_blue = ctfgame.spawn_blue_default; - CenterPrintTeam(team, va("Respawn rates reset, your team is now respawning every %d seconds\n", ctfgame.spawn_blue_default)); - gi.dprintf("%s: Respawn time for team %d reset to %d\n", __func__, team, ctfgame.spawn_blue_default); } } @@ -331,6 +340,8 @@ qboolean HasFlag(edict_t * ent) return false; if (ent->client->inventory[items[FLAG_T1_NUM].index] || ent->client->inventory[items[FLAG_T2_NUM].index]) return true; + if (ent->client->ctf_hasflag) + return true; return false; } diff --git a/src/action/botlib/botlib_ctf.c b/src/action/botlib/botlib_ctf.c index 6950f541d..e6479272d 100644 --- a/src/action/botlib/botlib_ctf.c +++ b/src/action/botlib/botlib_ctf.c @@ -740,7 +740,8 @@ void BOTLIB_CTF_Goals(edict_t* self) if (n == INVALID && ctf_goal_node_warning == false){ ctf_goal_node_warning = true; - gi.dprintf("%s: Warning: Flag location is at an INVALID node.\n", __func__); + if (bot_debug->value) + gi.dprintf("%s: Warning: Flag location is at an INVALID node.\n", __func__); return; } From 472d3098429be49d6471d062b087cf68e59495cb Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 23 Oct 2024 17:08:03 -0400 Subject: [PATCH 904/974] Moved SteamCloud definition before fs_game/fs_home --- src/common/files.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/common/files.c b/src/common/files.c index 53c3dfaa7..62c96faa5 100644 --- a/src/common/files.c +++ b/src/common/files.c @@ -3548,17 +3548,8 @@ static void setup_base_paths(void) fs_base_searchpaths = fs_searchpaths; } -// Sets the gamedir and path to a different directory. -static void setup_game_paths(void) +static void setup_steamcloud_paths(void) { - if (fs_game->string[0]) { - // add system path first - add_game_dir(FS_PATH_GAME | FS_DIR_BASE, "%s/%s", sys_basedir->string, fs_game->string); - - // home paths override system paths - if (sys_homedir->string[0]) { - add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "%s/%s", sys_homedir->string, fs_game->string); - } // add SteamCloud sync dir to VFS if we are a client, steamcloud is enabled and steamID is found #if USE_CLIENT if (strcmp(steamid->string, "0") == 0){ @@ -3575,6 +3566,22 @@ static void setup_game_paths(void) // if SteamID, steamcloudappenabled and steamclouduserenabled is not 0, add steamcloud dir as new game_dir add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "./SteamCloud/%s/%s", steamid->string, fs_game->string); #endif +} + +// Sets the gamedir and path to a different directory. +static void setup_game_paths(void) +{ + // add steamcloud path + setup_steamcloud_paths(); + + if (fs_game->string[0]) { + // add system path first + add_game_dir(FS_PATH_GAME | FS_DIR_BASE, "%s/%s", sys_basedir->string, fs_game->string); + + // home paths override system paths + if (sys_homedir->string[0]) { + add_game_dir(FS_PATH_GAME | FS_DIR_HOME, "%s/%s", sys_homedir->string, fs_game->string); + } } // this var is set for compatibility with server browsers, etc Cvar_FullSet("gamedir", fs_game->string, CVAR_ROM | CVAR_SERVERINFO, FROM_CODE); From ea40230bfc47045592c3c716de03ed14ad1f4e40 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 25 Oct 2024 09:45:13 -0400 Subject: [PATCH 905/974] Refined documentation around bots --- doc/action.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/action.md b/doc/action.md index a39fcbc1a..cbb21fed8 100644 --- a/doc/action.md +++ b/doc/action.md @@ -625,19 +625,21 @@ Taking aspects of the existing bots and greatly enhancing their navigation and b - `bot_randname [0/1]` - Server cvar, allow bots to pick a random name. Suggest keeping enabled - `bot_chat [0/1]` - Server cvar, enables bot chat. - `bot_countashuman [0/1]` - Server cvar, enabling this will allow teamplay-based games to progress without humans being in the server. Set to 0 to force games to not count bots as clients in terms of teamplay -- `bot_navautogen [0/1]` - Server cvar, enabling this will auto generate a navmesh for any maps that do not already have one, on map load time. This automatic navmesh is far from perfect, but it does allow bots to traverse maps rather than stand still. A far superior option is to have a handcrafted navmesh for the map. +- `bot_navautogen [0/1]` - Server cvar, enabling this will auto generate a navmesh for any maps that do not already have one, on map load time. This automatic navmesh is far from perfect, but it does allow bots to traverse maps rather than stand still. A far superior option is to have a handcrafted navmesh for the map, as described below in Client Commands. - `bot_debug [0/1]` - Server cvar, will enable debug messaging for BOTLIB functionality where enabled - `bot_reportasclient [0/1]` - Server cvar, if enabled, will report bots as real clients to server masters. Default is disabled [0] **Client Commands:** -These commands only work on local map loads, not connections to dedicated servers. They are meant to be used to create navmeshes. +These commands only work on local map loads, not connections to dedicated servers. They are meant to be used to create navmeshes. You need to have `gl_shaders 0` (GLSL disabled) for this to work, newer shader renderers are incompatible with navmesh generation at this time. - `nav_edit` - Enters nav edit mode. Enter again to toggle out of nav edit mode. - `nav_toggle` - Toggles nav visibility: None, nodes, and bot paths - `nav_autogen` - If in nav_edit mode, will automatically generate a navmesh that will interconnect nodes that are reachable via info_player_deathmatch entities. This performs 90% of the work for you, so that most flat surfaces will have a full mesh ready to use. - `nav_save` - This saves the current navmesh to `bots/nav/mapname.nav` - `nav_load` - This will load a navmesh from `bots/nav/mapname.nav`. Loading a map that already has a navmesh will perform this automatically, there is no need to run this command except to possibly revert to an earlier navmesh if you make a mistake and want to undo + **Nav Interface** -When generating a navmesh, you will use the `reload` key to cycle through different options (add node, remove node, link node, etc.). Use the `+attack` key to interact with the navmesh. More documentation around performing this is coming soon. + +When generating a navmesh, you will use the `reload` key to cycle through different options (add node, remove node, link node, etc.). Use the `+attack` key (normally MOUSE1 / left click) to interact with the navmesh. More documentation around performing this is coming soon. For a video demonstration, watch https://youtu.be/vva19y8EByE ### Slap Server admins can slap players into the air, and optionally deal damage. From 66ba1b0368f32037d29501ebd03649060ef27523 Mon Sep 17 00:00:00 2001 From: Reki Date: Sat, 26 Oct 2024 15:11:43 -0400 Subject: [PATCH 906/974] unified AQTION_EXTENSION check to an #ifdef instead of an #if --- inc/client/client.h | 2 +- inc/common/common.h | 2 +- inc/common/msg.h | 2 +- inc/shared/game.h | 4 ++-- inc/shared/shared.h | 10 +++++----- src/action/a_esp.c | 2 +- src/action/a_team.c | 4 ++-- src/action/g_cmds.c | 2 +- src/action/g_ext.c | 2 +- src/action/g_local.h | 22 +++++++++++----------- src/action/g_main.c | 2 +- src/action/g_save.c | 4 ++-- src/action/g_spawn.c | 2 +- src/action/p_client.c | 32 ++++++++++++++++---------------- src/action/p_hud.c | 2 +- src/action/p_view.c | 2 +- src/client/client.h | 2 +- src/client/ghud.c | 2 +- src/client/main.c | 4 ++-- src/client/parse.c | 2 +- src/client/screen.c | 4 ++-- src/common/common.c | 2 +- src/common/msg.c | 8 ++++---- src/common/pmove/template.c | 2 +- src/server/entities.c | 6 +++--- src/server/game.c | 6 +++--- src/server/ghud.c | 2 +- src/server/mvd.c | 2 +- src/server/server.h | 12 ++++++------ src/server/user.c | 8 ++++---- 30 files changed, 79 insertions(+), 79 deletions(-) diff --git a/inc/client/client.h b/inc/client/client.h index c52c4c023..d36af72b7 100644 --- a/inc/client/client.h +++ b/inc/client/client.h @@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "common/net/net.h" #include "common/utils.h" -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION #include "shared/ghud.h" #endif diff --git a/inc/common/common.h b/inc/common/common.h index 8e94b7a5e..ffa01ac63 100644 --- a/inc/common/common.h +++ b/inc/common/common.h @@ -128,7 +128,7 @@ void Sys_Printf(const char *fmt, ...) q_printf(1, 2); #define COM_DEDICATED 1 #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void G_InitializeExtensions(void); #endif diff --git a/inc/common/msg.h b/inc/common/msg.h index af37d37e6..790658f2c 100644 --- a/inc/common/msg.h +++ b/inc/common/msg.h @@ -166,7 +166,7 @@ void MSG_ParseDeltaPlayerstate_Enhanced(const player_state_t *from, player_st void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, player_state_t *to, int flags, int extraflags, msgPsFlags_t psflags); #endif void MSG_ParseDeltaPlayerstate_Packet(player_state_t *to, int flags, msgPsFlags_t psflags); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int MSG_DeltaGhud(ghud_element_t *from, ghud_element_t *to, int protocolmask); void MSG_WriteGhud(ghud_element_t *element, int flags); #if USE_CLIENT || USE_MVD_CLIENT diff --git a/inc/shared/game.h b/inc/shared/game.h index 9a72dceda..2d5675125 100644 --- a/inc/shared/game.h +++ b/inc/shared/game.h @@ -111,7 +111,7 @@ struct gclient_old_s { // the game dll can add anything it wants after // this point in the structure -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif }; @@ -126,7 +126,7 @@ struct gclient_new_s { // the game dll can add anything it wants after // this point in the structure -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif }; diff --git a/inc/shared/shared.h b/inc/shared/shared.h index e2e453c11..96754b5f1 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -138,7 +138,7 @@ typedef enum { PRINT_NOTICE // print in cyan color } print_type_t; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION #include "shared/ghud.h" #endif @@ -933,7 +933,7 @@ typedef struct cvar_s { struct cvar_s *next; // ------ new stuff ------ -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int sync_index; #endif #if USE_NEW_GAME_API @@ -1091,7 +1091,7 @@ typedef enum { #define PMF_ON_LADDER BIT(8) //KEX -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // pmove->pm_aq2_flags #define PMF_AQ2_LIMP 0x01 // used to predict limping #define PMF_AQ2_FRICTION 0x02 //rekkie -- Increase friction for bots @@ -1112,7 +1112,7 @@ typedef struct { short gravity; short delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION short pm_aq2_flags; // limping, bandaging, etc unsigned short pm_timestamp; // timestamp, resets every 60 seconds byte pm_aq2_leghits; // number of leg hits @@ -1130,7 +1130,7 @@ typedef struct { int16_t gravity; int16_t delta_angles[3]; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION short pm_aq2_flags; // limping, bandaging, etc unsigned short pm_timestamp; // timestamp, resets every 60 seconds byte pm_aq2_leghits; // number of leg hits diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 3007cfe35..5370eb51d 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -475,7 +475,7 @@ void EspMakeCapturePoint(edict_t *flag) /* Indicator arrow This appears regardless of indicator settings */ - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION if (!flag->obj_arrow){ flag->obj_arrow = G_Spawn(); flag->obj_arrow->solid = SOLID_NOT; diff --git a/src/action/a_team.c b/src/action/a_team.c index 406d060b4..ccc28eb0e 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1500,7 +1500,7 @@ void JoinTeam (edict_t * ent, int desired_team, int skip_menuclose) G_UpdatePlayerStatusbar(ent, 1); } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (desired_team == NOTEAM) HUD_SetType(ent, 1); else @@ -1566,7 +1566,7 @@ void LeaveTeam (edict_t * ent) ent->client->resp.team = NOTEAM; G_UpdatePlayerStatusbar(ent, 1); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION HUD_SetType(ent, 1); #endif diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 29b836aea..3d8bcbd0e 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1697,7 +1697,7 @@ static void Cmd_PrintSettings_f( edict_t * ent ) itmflagsSettings( text, sizeof( text ), (int)itm_flags->value ); length = strlen( text ); - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION Q_snprintf( text + length, sizeof( text ) - length, "\n" "timelimit %2d roundlimit %2d roundtimelimit %2d\n" "limchasecam %2d tgren %2d antilag_interp %2d\n" diff --git a/src/action/g_ext.c b/src/action/g_ext.c index af450a5ad..60f56b30a 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -8,7 +8,7 @@ #include "g_local.h" #include "q_ghud.h" -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // // Reki // Setup struct and macro for defining engine-callable entrypoints diff --git a/src/action/g_local.h b/src/action/g_local.h index 763bf2f48..82f19cb21 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -899,7 +899,7 @@ typedef struct int model_null; int model_lsight; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int model_arrow; #endif @@ -1374,7 +1374,7 @@ extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA extern cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); extern int (*engine_Client_GetProtocol)(edict_t *ent); @@ -1437,7 +1437,7 @@ extern cvar_t *gm; // Gamemode extern cvar_t *gmf; // Gamemodeflags extern cvar_t *sv_idleremove; // Remove idlers -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION extern cvar_t *use_newirvision; // enable new irvision (only highlight baddies) extern cvar_t *use_indicators; // enable/allow indicators extern cvar_t *use_xerp; // allow clients to use cl_xerp @@ -1656,7 +1656,7 @@ void ClientBeginServerFrame (edict_t * ent); // // g_ext.c // -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void G_InitExtEntrypoints(void); void* G_FetchGameExtension(char *name); #endif @@ -1890,7 +1890,7 @@ typedef struct int limp_nopred; int spec_flags; qboolean antilag_optout; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int cl_xerp; int cl_indicators; #endif @@ -2002,7 +2002,7 @@ typedef struct vec3_t jmp_teleport_v_angle; qboolean jmp_teleport_ducked; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int hud_items[128]; int hud_type; #endif @@ -2054,7 +2054,7 @@ struct gclient_s int clientNum; // Reki: cvar sync -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif @@ -2240,7 +2240,7 @@ struct gclient_s int ctf_grapplereleaseframe; // frame of grapple release qboolean ctf_hasflag; // set to true if this client has the flag -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION //AQTION - Reki: Teammate indicators edict_t *arrow; #endif @@ -2760,7 +2760,7 @@ struct edict_s vec3_t lastPosition; qboolean nameused[NUMNAMES][NUMNAMES]; qboolean newnameused[AQ2WTEAMSIZE]; - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION //AQTION - Reki: Entity indicators edict_t *obj_arrow; #endif @@ -2917,7 +2917,7 @@ typedef struct team_s char leader_name[MAX_SKINLEN]; char leader_skin[MAX_QPATH]; char leader_skin_index[MAX_QPATH]; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION #if AQTION_HUD int ghud_resettime; byte ghud_icon; @@ -2943,7 +2943,7 @@ extern int gameSettings; #include "a_dom.h" #include "a_esp.h" -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION #define HAS_CVARSYNC(ent) (Client_GetProtocol(ent) == 38 && Client_GetVersion(ent) >= 3013) // hud (through ghud extension) diff --git a/src/action/g_main.c b/src/action/g_main.c index dce86f8ba..55da79a2d 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -584,7 +584,7 @@ cvar_t *lca_grenade; // Allows grenade pin pulling during LCA cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION cvar_t *use_newirvision; cvar_t *use_indicators; cvar_t *use_xerp; diff --git a/src/action/g_save.c b/src/action/g_save.c index d315a4aee..24b7b4df4 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -678,7 +678,7 @@ void InitGame( void ) // new AQtion Extension cvars -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION use_newirvision = gi.cvar("use_newirvision", "1", 0); use_indicators = gi.cvar("use_indicators", "1", 0); use_xerp = gi.cvar("use_xerp", "1", 0); @@ -802,7 +802,7 @@ void InitGame( void ) gi.cvar_forceset("g_view_high", va("%d", STANDING_VIEWHEIGHT)); gi.cvar_forceset("g_view_low", va("%d", CROUCHING_VIEWHEIGHT)); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION CvarSync_Set(clcvar_cl_antilag, "cl_antilag", "1"); CvarSync_Set(clcvar_cl_indicators, "cl_indicators", "1"); CvarSync_Set(clcvar_cl_xerp, "cl_xerp", "0"); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index dc0ab71a6..a7690ae5c 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2077,7 +2077,7 @@ void SP_worldspawn (edict_t * ent) level.model_null = gi.modelindex("sprites/null.sp2"); // null sprite level.model_lsight = gi.modelindex("sprites/lsight.sp2"); // laser sight dot sprite -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION level.model_arrow = gi.modelindex("models/indicator/arrow_red.md2"); gi.modelindex("models/indicator/arrow_blue.md2"); gi.modelindex("models/indicator/arrow_green.md2"); diff --git a/src/action/p_client.c b/src/action/p_client.c index 723bde2dc..018c43368 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -341,7 +341,7 @@ static void FreeClientEdicts(gclient_t *client) client->ctf_grapple = NULL; } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION //remove arrow if (client->arrow) { G_FreeEdict(client->arrow); @@ -2874,7 +2874,7 @@ void PutClientInServer(edict_t * ent) client_persistant_t pers; client_respawn_t resp; gitem_t *item; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif @@ -2891,13 +2891,13 @@ void PutClientInServer(edict_t * ent) // deathmatch wipes most client data every spawn resp = client->resp; pers = client->pers; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION memcpy(cl_cvar, client->cl_cvar, sizeof(client->cl_cvar)); #endif memset(client, 0, sizeof(*client)); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION memcpy(client->cl_cvar, cl_cvar, sizeof(client->cl_cvar)); #endif client->pers = pers; @@ -2990,7 +2990,7 @@ void PutClientInServer(edict_t * ent) ent->s.skinnum = ent - g_edicts - 1; ent->s.modelindex = 255; // will use the skin specified model -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // teammate indicator arrows if (use_indicators->value && teamplay->value && !client->arrow && client->resp.team) { @@ -3122,7 +3122,7 @@ void PutClientInServer(edict_t * ent) ent->svflags |= SVF_NOCLIENT; ent->client->ps.gunindex = 0; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (!ent->client->resp.team) HUD_SetType(ent, 1); #endif @@ -3134,7 +3134,7 @@ void PutClientInServer(edict_t * ent) } // end if( respawn ) #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION HUD_SetType(ent, -1); #endif @@ -3247,7 +3247,7 @@ void ClientBeginDeathmatch(edict_t * ent) ent->client->resp.enterframe = level.framenum; ent->client->resp.gldynamic = 1; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (teamplay->value) { HUD_SetType(ent, 1); @@ -3485,7 +3485,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) // Reki - disable prediction on limping -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (Client_GetProtocol(ent) == 38) // if we're using AQTION protocol, we have limp prediction { client->pers.limp_nopred = 0; @@ -3501,12 +3501,12 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) client->pers.limp_nopred = 2 | (client->pers.limp_nopred & 256); // client doesn't specify, so use auto threshold else if (limp == 0) client->pers.limp_nopred = 0; // client explicity wants old behavior -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION } #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (!HAS_CVARSYNC(ent)) // only do these cl cvars if cvarsync isn't a thing, since it's much better than userinfo { #endif @@ -3517,7 +3517,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) else client->pers.spec_flags &= ~(SPECFL_SPECHUD | SPECFL_SPECHUD_NEW); - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION if (Client_GetProtocol(ent) == 38) // Reki: new clients get new spec hud client->pers.spec_flags &= ~SPECFL_SPECHUD; #endif @@ -3538,7 +3538,7 @@ void ClientUserinfoChanged(edict_t *ent, char *userinfo) if (sv_antilag->value && antilag_value != client->pers.antilag_optout) gi.cprintf(ent, PRINT_MEDIUM, "YOUR CL_ANTILAG IS NOW SET TO %i\n", !client->pers.antilag_optout); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION } #endif } @@ -3948,7 +3948,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) qboolean has_enhanced_slippers = esp_enhancedslippers->value && INV_AMMO(ent, SLIP_NUM); if( client->leg_damage && ent->groundentity && ! has_enhanced_slippers ) { - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION pm.s.pm_aq2_flags |= PMF_AQ2_LIMP; pm.s.pm_aq2_leghits = min(client->leghits, 255); #else @@ -3968,7 +3968,7 @@ void ClientThink(edict_t * ent, usercmd_t * ucmd) pm.s.pm_flags |= PMF_JUMP_HELD; #endif } - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION else { pm.s.pm_aq2_flags &= ~PMF_AQ2_LIMP; @@ -6140,7 +6140,7 @@ void ClientBeginServerFrame(edict_t * ent) Cmd_PMLCA_f(ent); } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // resync pm_timestamp so all limps are roughly synchronous, to try to maintain original behavior unsigned short world_timestamp = (int)(level.time * 1000) % 60000; client->ps.pmove.pm_timestamp = world_timestamp; diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 9c643940c..2bf04043d 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -741,7 +741,7 @@ void G_SetStats (edict_t * ent) } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void HUD_SetType(edict_t *clent, int type) { diff --git a/src/action/p_view.c b/src/action/p_view.c index 1a7d1b414..44deb98c4 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1398,7 +1398,7 @@ void ClientEndServerFrame (edict_t * ent) current_player = ent; current_client = ent->client; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (current_client->arrow) { // set new origin diff --git a/src/client/client.h b/src/client/client.h index 586933420..c56d43fe0 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -257,7 +257,7 @@ typedef struct { char layout[MAX_NET_STRING]; // general 2D overlay int inventory[MAX_ITEMS]; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION ghud_3delement_t *ghud_3dlist; ghud_element_t ghud[MAX_GHUDS]; #endif diff --git a/src/client/ghud.c b/src/client/ghud.c index 41361f88e..8c8405336 100644 --- a/src/client/ghud.c +++ b/src/client/ghud.c @@ -1,6 +1,6 @@ #include "client.h" -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void CL_Ghud_Clear() diff --git a/src/client/main.c b/src/client/main.c index 338d8044b..f31727059 100644 --- a/src/client/main.c +++ b/src/client/main.c @@ -421,7 +421,7 @@ void CL_CheckForResend(void) cls.serverProtocol = cl_protocol->integer; if (cls.serverProtocol < PROTOCOL_VERSION_DEFAULT || cls.serverProtocol > PROTOCOL_VERSION_AQTION) { - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION cls.serverProtocol = PROTOCOL_VERSION_AQTION; #else cls.serverProtocol = PROTOCOL_VERSION_Q2PRO; @@ -694,7 +694,7 @@ void CL_ClearState(void) SCR_ClearCenterPrints(); CL_ClearEffects(); CL_ClearTEnts(); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION CL_Clear3DGhudQueue(); #endif LOC_FreeLocations(); diff --git a/src/client/parse.c b/src/client/parse.c index 0fbb44871..496b42cd3 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -1581,7 +1581,7 @@ void CL_ParseServerMessage(void) continue; case svc_ghudupdate: -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (cls.serverProtocol != PROTOCOL_VERSION_AQTION) goto badbyte; //CL_ParseGhud(); diff --git a/src/client/screen.c b/src/client/screen.c index 20f1ea13e..3224a4488 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -2387,7 +2387,7 @@ static void SCR_DrawCrosshair(void) } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void CL_Clear3DGhudQueue(void) { ghud_3delement_t *link; @@ -2658,7 +2658,7 @@ static void SCR_Draw2D(void) SCR_DrawStats(); SCR_DrawLayout(); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // Draw game defined hud elements SCR_DrawGhud(); diff --git a/src/common/common.c b/src/common/common.c index 8681f0acc..2720d45d4 100644 --- a/src/common/common.c +++ b/src/common/common.c @@ -900,7 +900,7 @@ void Qcommon_Init(int argc, char **argv) CL_PreInit(); Prompt_Init(); Con_Init(); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION G_InitializeExtensions(); #endif diff --git a/src/common/msg.c b/src/common/msg.c index 716b7d67d..ccaaef8d4 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -1481,7 +1481,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, // // aqtion extension checks // -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (to->pmove.pm_aq2_flags != from->pmove.pm_aq2_flags) aqtflags |= AQPS_PMFLAGS; if (to->pmove.pm_timestamp != from->pmove.pm_timestamp) @@ -1562,7 +1562,7 @@ int MSG_WriteDeltaPlayerstate_Aqtion(const player_packed_t *from, // MSG_WriteByte(aqtflags); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (aqtflags & AQPS_PMFLAGS) MSG_WriteByte(to->pmove.pm_aq2_flags); if (aqtflags & AQPS_TIMESTAMP) @@ -2232,7 +2232,7 @@ void MSG_ReadDeltaUsercmd_Enhanced(const usercmd_t *from, usercmd_t *to) -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION int MSG_DeltaGhud(ghud_element_t *from, ghud_element_t *to, int protocolmask) { @@ -2936,7 +2936,7 @@ void MSG_ParseDeltaPlayerstate_Aqtion(const player_state_t *from, // // parse the aqtion extensions // -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION aqtflags = MSG_ReadByte(); if (aqtflags & AQPS_PMFLAGS) diff --git a/src/common/pmove/template.c b/src/common/pmove/template.c index facbc7681..2fba4e5fb 100644 --- a/src/common/pmove/template.c +++ b/src/common/pmove/template.c @@ -1037,7 +1037,7 @@ void PMOVE_FUNC(PMOVE_TYPE *pmove, const pmoveParams_t *params) // clear all pmove local vars memset(&pml, 0, sizeof(pml)); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // add msec to the timestamp pm->s.pm_timestamp = (pm->s.pm_timestamp + pm->cmd.msec) % 60000; diff --git a/src/server/entities.c b/src/server/entities.c index 5a6fb797d..0aaf97c2a 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -125,7 +125,7 @@ static bool SV_TruncPacketEntities(client_t *client, const client_frame_t *from, return ret; } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION static void SV_Ghud_SendUpdateToClient(client_t *client, const client_frame_t *oldframe, client_frame_t *frame) { if (client->protocol == PROTOCOL_VERSION_AQTION && client->version >= PROTOCOL_VERSION_AQTION_GHUD) @@ -296,7 +296,7 @@ static bool SV_EmitPacketEntities(client_t *client, const client_frame_t *from, // This was moved from SV_WriteFrameToClient_Aqtion() because its // return type changed from void to bool and it's now returning SV_EmitPacketEntities() // rather than just calling it -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if(from) SV_Ghud_SendUpdateToClient(client, from, to); #endif @@ -896,7 +896,7 @@ void SV_BuildClientFrame(client_t *client) entity_state_t ent_state; ent_state = ent->s; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (GE_customizeentityforclient) if (!GE_customizeentityforclient(client->edict, ent, &ent_state)) continue; diff --git a/src/server/game.c b/src/server/game.c index 0002e5321..886b6e803 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -952,7 +952,7 @@ void SV_ShutdownGameProgs(void) Cvar_Set("g_view_predict", "0"); Z_LeakTest(TAG_FREE); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION GE_customizeentityforclient = NULL; GE_CvarSync_Updated = NULL; #endif @@ -992,7 +992,7 @@ static void *SV_LoadGameLibrary(const char *libdir, const char *gamedir) } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION typedef struct extension_func_s { char name[MAX_QPATH]; @@ -1138,7 +1138,7 @@ void SV_InitGameProgs(void) // unload anything we have now SV_ShutdownGameProgs(); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION SV_Ghud_Clear(); SV_CvarSync_Clear(); #endif diff --git a/src/server/ghud.c b/src/server/ghud.c index 32f931b6d..b6fb6ecab 100644 --- a/src/server/ghud.c +++ b/src/server/ghud.c @@ -1,5 +1,5 @@ #include "server.h" -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void SV_Ghud_Clear(void) diff --git a/src/server/mvd.c b/src/server/mvd.c index 52c5eb035..58f57f5f3 100644 --- a/src/server/mvd.c +++ b/src/server/mvd.c @@ -767,7 +767,7 @@ static void emit_frame(void) ent_state = ent->s; ent_active = entity_is_active(ent); - #if AQTION_EXTENSION + #ifdef AQTION_EXTENSION if (ent_active && GE_customizeentityforclient) if (!GE_customizeentityforclient(NULL, ent, &ent_state)) ent_active = false; diff --git a/src/server/server.h b/src/server/server.h index 1471e0e82..a7edebffd 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -114,7 +114,7 @@ typedef struct { byte areabits[MAX_MAP_AREA_BYTES]; // portalarea visibility bits unsigned sentTime; // for ping calculations int latency; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION ghud_element_t ghud[MAX_GHUDS]; #endif } client_frame_t; @@ -388,7 +388,7 @@ typedef struct client_s { char *ac_token; #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION ghud_element_t ghud[MAX_GHUDS]; #endif } client_t; @@ -501,7 +501,7 @@ typedef struct { challenge_t challenges[MAX_CHALLENGES]; // to prevent invalid IPs from connecting -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // Reki: cvar sync entries cvarsync_t cvarsync_list[CVARSYNC_MAX]; byte cvarsync_length; @@ -795,7 +795,7 @@ void SV_ShutdownGameProgs(void); void PF_Pmove(void *pm); -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION void G_InitializeExtensions(void); #endif @@ -816,7 +816,7 @@ void SV_RegisterSavegames(void); #define SV_RegisterSavegames() (void)0 #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // // sv_ghud.c // @@ -833,7 +833,7 @@ void SV_Ghud_SetColor(edict_t *ent, int i, int r, int g, int b, int a); void SV_Ghud_SetSize(edict_t *ent, int i, int x, int y); #endif -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION extern int(*GE_customizeentityforclient)(edict_t *client, edict_t *ent, entity_state_t *state); // 0 don't send, 1 send normally extern void(*GE_CvarSync_Updated)(int index, edict_t *clent); #endif diff --git a/src/server/user.c b/src/server/user.c index 1794c2183..330152033 100644 --- a/src/server/user.c +++ b/src/server/user.c @@ -55,7 +55,7 @@ static void SV_CreateBaselines(void) } } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION // clear ghud from previous level memset(sv_client->ghud, 0, sizeof(ghud_element_t) * MAX_GHUDS); #endif @@ -518,7 +518,7 @@ void SV_New_f(void) SV_ClientAddMessage(sv_client, MSG_RELIABLE | MSG_CLEAR); } -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION if (svs.cvarsync_length && sv_client->protocol == PROTOCOL_VERSION_AQTION && sv_client->version >= PROTOCOL_VERSION_AQTION_CVARSYNC) { MSG_WriteByte(svc_extend); @@ -1140,7 +1140,7 @@ static const ucmd_t ucmds[] = { #endif { "aclist", SV_AC_List_f }, { "acinfo", SV_AC_Info_f }, -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION { "cvarsync", SV_CvarSync_f }, #endif @@ -1790,7 +1790,7 @@ void SV_ExecuteClientMessage(client_t *client) case clc_cvarsync: if (client->protocol != PROTOCOL_VERSION_AQTION) goto badbyte; -#if AQTION_EXTENSION +#ifdef AQTION_EXTENSION index = MSG_ReadByte(); gclient_t *client = (gclient_t *)sv_player->client; // Cast to the correct type MSG_ReadString(client->cl_cvar[index], CVARSYNC_MAXSIZE); From 3428ab6cd11f5666630a6f5e7fbdf825e75c73a0 Mon Sep 17 00:00:00 2001 From: Reki Date: Sat, 26 Oct 2024 16:06:37 -0400 Subject: [PATCH 907/974] Fixed AQTION_EXTENSION pmove fields being stripped when player state was packed --- src/common/msg.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/common/msg.c b/src/common/msg.c index ccaaef8d4..1cc8cc3ee 100644 --- a/src/common/msg.c +++ b/src/common/msg.c @@ -840,6 +840,11 @@ void MSG_PackPlayerOld(player_packed_t *out, const player_state_old_t *in) out->pmove.pm_time = in->pmove.pm_time; out->pmove.gravity = in->pmove.gravity; VectorCopy(in->pmove.delta_angles, out->pmove.delta_angles); + #ifdef AQTION_EXTENSION + out->pmove.pm_aq2_flags = in->pmove.pm_aq2_flags; + out->pmove.pm_timestamp = in->pmove.pm_timestamp; + out->pmove.pm_aq2_leghits = in->pmove.pm_aq2_leghits; + #endif PACK_ANGLES(out->viewangles, in->viewangles); PACK_OFFSET(out->viewoffset, in->viewoffset); From 1206f406e2d1d58e08be98e6eb835044ef3c87d0 Mon Sep 17 00:00:00 2001 From: Reki Date: Sat, 26 Oct 2024 16:15:41 -0400 Subject: [PATCH 908/974] removed hack to strip protocol 38 support on non-listen servers --- src/server/main.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/server/main.c b/src/server/main.c index 37e5fe5d2..e3683d12d 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -627,10 +627,6 @@ static void SVC_GetChallenge(void) svs.challenges[i].challenge = challenge; svs.challenges[i].time = com_eventTime; } - //rekkie -- force max protocol PROTOCOL_VERSION_Q2PRO until PROTOCOL_VERSION_AQTION is compatible again -- s - Netchan_OutOfBand(NS_SERVER, &net_from, "challenge %u p=%i,%i,%i", challenge, PROTOCOL_VERSION_DEFAULT, PROTOCOL_VERSION_R1Q2, PROTOCOL_VERSION_Q2PRO); - return; - //rekkie -- force max protocol PROTOCOL_VERSION_Q2PRO until PROTOCOL_VERSION_AQTION is compatible again -- e // send it back Netchan_OutOfBand(NS_SERVER, &net_from, From e88762b33f2d45b4f107d1452b22565b74d435df Mon Sep 17 00:00:00 2001 From: Reki Date: Sat, 26 Oct 2024 17:01:27 -0400 Subject: [PATCH 909/974] restored customizeentityforclient, restored CvarSync_Updated --- src/action/g_ext.c | 2 +- src/action/g_local.h | 2 +- src/action/g_main.c | 50 +++++++++++++++++++++---------------------- src/server/entities.c | 2 +- src/server/game.c | 10 +++++---- 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/action/g_ext.c b/src/action/g_ext.c index 60f56b30a..ed99a157e 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -297,7 +297,7 @@ void G_InitExtEntrypoints(void) } -void* G_FetchGameExtension(char *name) +void* G_FetchGameExtension(const char *name) { Com_Printf("Game: G_FetchGameExtension for %s\n", name); extension_func_t *ext; diff --git a/src/action/g_local.h b/src/action/g_local.h index 82f19cb21..f0fe2a607 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1658,7 +1658,7 @@ void ClientBeginServerFrame (edict_t * ent); // #ifdef AQTION_EXTENSION void G_InitExtEntrypoints(void); -void* G_FetchGameExtension(char *name); +void* G_FetchGameExtension(const char *name); #endif // diff --git a/src/action/g_main.c b/src/action/g_main.c index 55da79a2d..646148d88 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -696,7 +696,7 @@ const game_export_ex_t gex = { // // Functionality examples? // // https://github.com/skullernet/q2pro/issues/294#issuecomment-1476818818 - //.GetExtension = GetExtension, + .GetExtension = G_FetchGameExtension, }; @@ -704,31 +704,31 @@ q_exported const game_export_ex_t *GetGameAPIEx(game_import_ex_t *importx) { gix = importx; // assign pointer, don't copy! - //gex.CheckForExtension = G_FetchGameExtension; + //gex.GetExtension = G_FetchGameExtension; G_InitExtEntrypoints(); - engine_Client_GetProtocol = gix->CheckForExtension("Client_GetProtocol"); - engine_Client_GetVersion = gix->CheckForExtension("Client_GetVersion"); - - engine_Ghud_ClearForClient = gix->CheckForExtension("Ghud_ClearForClient"); - engine_Ghud_NewElement = gix->CheckForExtension("Ghud_NewElement"); - engine_Ghud_RemoveElement = gix->CheckForExtension("Ghud_RemoveElement"); - engine_Ghud_SetFlags = gix->CheckForExtension("Ghud_SetFlags"); - engine_Ghud_SetInt = gix->CheckForExtension("Ghud_SetInt"); - engine_Ghud_SetText = gix->CheckForExtension("Ghud_SetText"); - engine_Ghud_SetPosition = gix->CheckForExtension("Ghud_SetPosition"); - engine_Ghud_SetAnchor = gix->CheckForExtension("Ghud_SetAnchor"); - engine_Ghud_SetColor = gix->CheckForExtension("Ghud_SetColor"); - engine_Ghud_SetSize = gix->CheckForExtension("Ghud_SetSize"); - - engine_CvarSync_Set = gix->CheckForExtension("CvarSync_Set"); - - SV_BSP = gix->CheckForExtension("Bsp"); - CS_NAV = gix->CheckForExtension("Nav"); - CS_DebugDraw = gix->CheckForExtension("DebugDraw"); - SV_BotConnect = gix->CheckForExtension("SV_BotConnect"); - SV_BotDisconnect = gix->CheckForExtension("SV_BotDisconnect"); - SV_BotUpdateInfo = gix->CheckForExtension("SV_BotUpdateInfo"); - SV_BotClearClients = gix->CheckForExtension("SV_BotClearClients"); + engine_Client_GetProtocol = gix->GetExtension("Client_GetProtocol"); + engine_Client_GetVersion = gix->GetExtension("Client_GetVersion"); + + engine_Ghud_ClearForClient = gix->GetExtension("Ghud_ClearForClient"); + engine_Ghud_NewElement = gix->GetExtension("Ghud_NewElement"); + engine_Ghud_RemoveElement = gix->GetExtension("Ghud_RemoveElement"); + engine_Ghud_SetFlags = gix->GetExtension("Ghud_SetFlags"); + engine_Ghud_SetInt = gix->GetExtension("Ghud_SetInt"); + engine_Ghud_SetText = gix->GetExtension("Ghud_SetText"); + engine_Ghud_SetPosition = gix->GetExtension("Ghud_SetPosition"); + engine_Ghud_SetAnchor = gix->GetExtension("Ghud_SetAnchor"); + engine_Ghud_SetColor = gix->GetExtension("Ghud_SetColor"); + engine_Ghud_SetSize = gix->GetExtension("Ghud_SetSize"); + + engine_CvarSync_Set = gix->GetExtension("CvarSync_Set"); + + SV_BSP = gix->GetExtension("Bsp"); + CS_NAV = gix->GetExtension("Nav"); + CS_DebugDraw = gix->GetExtension("DebugDraw"); + SV_BotConnect = gix->GetExtension("SV_BotConnect"); + SV_BotDisconnect = gix->GetExtension("SV_BotDisconnect"); + SV_BotUpdateInfo = gix->GetExtension("SV_BotUpdateInfo"); + SV_BotClearClients = gix->GetExtension("SV_BotClearClients"); return &gex; } diff --git a/src/server/entities.c b/src/server/entities.c index 0aaf97c2a..72caaeeb8 100644 --- a/src/server/entities.c +++ b/src/server/entities.c @@ -910,7 +910,7 @@ void SV_BuildClientFrame(client_t *client) Q_assert(temp.s.number == e); MSG_PackEntity(state, &temp.s, ENT_EXTENSION(client->csr, &temp)); } else { - MSG_PackEntity(state, &ent->s, ENT_EXTENSION(client->csr, ent)); + MSG_PackEntity(state, &ent_state, ENT_EXTENSION(client->csr, ent)); } #if USE_FPS diff --git a/src/server/game.c b/src/server/game.c index 886b6e803..be27e43be 100644 --- a/src/server/game.c +++ b/src/server/game.c @@ -888,6 +888,7 @@ static const rektek_bots_api_v1_t rektek_bots_api_v1 = { .SV_BotClearClients = SV_BotClearClients, }; +static void* G_CheckForExtension(const char* text); static void *PF_GetExtension(const char *name) { if (!name){ @@ -909,9 +910,8 @@ static void *PF_GetExtension(const char *name) } #endif - return NULL; + return G_CheckForExtension(name); } -static void* G_CheckForExtension(char *text); static const game_import_ex_t game_import_ex = { .apiversion = GAME_API_VERSION_EX, @@ -924,7 +924,6 @@ static const game_import_ex_t game_import_ex = { .GetExtension = PF_GetExtension, .TagRealloc = PF_TagRealloc, - .CheckForExtension = G_CheckForExtension, }; static void *game_library; @@ -1021,7 +1020,7 @@ G_CheckForExtension Check for (and return) an extension function by name ================ */ -static void* G_CheckForExtension(char *text) +static void* G_CheckForExtension(const char *text) { Com_Printf("G_CheckForExtension for %s\n", text); extension_func_t *ext; @@ -1194,6 +1193,9 @@ void SV_InitGameProgs(void) Com_Printf("Disabled: Extended game API version is too old.\n"); } else { Com_Printf("Game supports Q2PRO extended API version %d.\n", gex->apiversion); + + GE_customizeentityforclient = gex->GetExtension("customizeentityforclient"); + GE_CvarSync_Updated = gex->GetExtension("CvarSync_Updated"); } } From ab3e4832148cd3fe08c978f35bc022dc6c442812 Mon Sep 17 00:00:00 2001 From: Reki Date: Sat, 26 Oct 2024 17:13:31 -0400 Subject: [PATCH 910/974] fixed double tempent parse and filtered hitmarker sending --- src/action/p_view.c | 19 +++++++++++-------- src/client/parse.c | 3 +-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/action/p_view.c b/src/action/p_view.c index 44deb98c4..4ead38292 100644 --- a/src/action/p_view.c +++ b/src/action/p_view.c @@ -1695,12 +1695,15 @@ void ClientEndServerFrame (edict_t * ent) RadioThink(ent); // Paril's hit markers (don't draw for bots!) - // if (ent->client->damage_dealt > 0 && !ent->is_bot) - // { - // gi.WriteByte(svc_temp_entity); - // gi.WriteByte(TE_DAMAGE_DEALT); - // gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); - // gi.unicast(ent, false); - // ent->client->damage_dealt = 0; - // } + if (ent->client->damage_dealt > 0 && !ent->is_bot) + { + if ((Client_GetProtocol(ent) == 36 && Client_GetVersion(ent) >= 1025) || (Client_GetProtocol(ent) == 38 && Client_GetVersion(ent) >= 3017)) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_DAMAGE_DEALT); + gi.WriteShort(min(ent->client->damage_dealt, INT16_MAX)); + gi.unicast(ent, false); + } + ent->client->damage_dealt = 0; + } } diff --git a/src/client/parse.c b/src/client/parse.c index 496b42cd3..0798e1d74 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -973,7 +973,7 @@ static void CL_ParseTEntPacket(void) break; default: - Com_Error(ERR_DROP, "%s: bad type %i", __func__, te.type); + Com_Error(ERR_DROP, "%s: bad type (%i)", __func__, te.type); } } @@ -1521,7 +1521,6 @@ void CL_ParseServerMessage(void) case svc_temp_entity: CL_ParseTEntPacket(); - CL_ParseTEnt(); break; case svc_muzzleflash: From 54ae37fba116409281991755e804cf31936a374c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 28 Oct 2024 13:05:55 -0400 Subject: [PATCH 911/974] Additional check on round number > 0 --- src/action/tng_stats.c | 2 +- src/client/tent.c | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 3cdc32bbc..93e4b083e 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -729,7 +729,7 @@ void G_RegisterScore(void) score = c->resp.score; // Calculate FPR, if mode is teamplay, else FPH - if ((teamplay->value && game.roundNum > 0) || !ctf->value){ + if ((teamplay->value && game.roundNum > 0) || !ctf->value || game.roundNum > 0){ fragsper = c->resp.score / game.roundNum; } else { sec = (level.framenum - c->resp.enterframe) / HZ; diff --git a/src/client/tent.c b/src/client/tent.c index ebdc91d92..3b241865b 100644 --- a/src/client/tent.c +++ b/src/client/tent.c @@ -1662,15 +1662,15 @@ void CL_ParseTEnt(void) CL_PowerSplash(); break; - // case TE_DAMAGE_DEALT: - // if (te.count > 0 && cl_hit_markers->integer > 0) { - // cl.hit_marker_time = cls.realtime; - // cl.hit_marker_count = te.count; - // if (cl_hit_markers->integer > 1) { - // S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); - // } - // } - // break; + case TE_DAMAGE_DEALT: + if (te.count > 0 && cl_hit_markers->integer > 0) { + cl.hit_marker_time = cls.realtime; + cl.hit_marker_count = te.count; + if (cl_hit_markers->integer > 1) { + S_StartSound(NULL, listener_entnum, 257, cl_sfx_hit_marker, 1, ATTN_NONE, 0); + } + } + break; default: Com_Error(ERR_DROP, "%s: bad type", __func__); From 3603b96d8c293f7453402d86c92966f3092ff3d6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 28 Oct 2024 13:38:26 -0400 Subject: [PATCH 912/974] Better CTF rules messages, fixed highscores crash --- src/action/a_team.c | 23 ++++++++++++++++++----- src/action/tng_stats.c | 14 +++++++++++--- src/client/download.c | 5 ----- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index ccc28eb0e..a600c9126 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -471,13 +471,26 @@ char* PrintMatchRules(void) // CTF rules else if (ctf->value) { - if (!capturelimit->value) { - Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\nNo capturelimit set!\n", - teams[TEAM1].name, teams[TEAM2].name ); - } else { - Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\nThe first team to %s captures wins!\n", + if (capturelimit->value && timelimit->value) + { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\n\nThe first team to %s captures wins!\nTime limit: %s minutes\n", + teams[TEAM1].name, teams[TEAM2].name, capturelimit->string, timelimit->string ); + } + else if (capturelimit->value) + { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\n\nThe first team to %s captures wins!\n", teams[TEAM1].name, teams[TEAM2].name, capturelimit->string ); } + else if (timelimit->value) + { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\n\nTime limit: %s minutes\n", + teams[TEAM1].name, teams[TEAM2].name, timelimit->string ); + } + else + { + Q_snprintf( rulesmsg, sizeof( rulesmsg ), "%s\nvs\n%s\n\nCapture the other team's flag!\n\nNo capturelimit or timelimit set!\n", + teams[TEAM1].name, teams[TEAM2].name ); + } } // Domination rules else if (dom->value) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 93e4b083e..45d2c6dc0 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -711,13 +711,19 @@ void G_RegisterScore(void) int score; int sec; float accuracy, fragsper; + qboolean roundbased; total = G_CalcRanks(ranks); if (!total) { return; } - if ((teamplay->value && game.roundNum == 0) && !ctf->value){ + if (strcmp(gm->string, "tp") == 0 || strcmp(gm->string, "esp") == 0) + roundbased = true; + else + roundbased = false; + + if (roundbased && game.roundNum == 0) { gi.dprintf("No rounds were played, so no highscore will be recorded\n"); return; // No rounds were played, so skip } @@ -729,7 +735,7 @@ void G_RegisterScore(void) score = c->resp.score; // Calculate FPR, if mode is teamplay, else FPH - if ((teamplay->value && game.roundNum > 0) || !ctf->value || game.roundNum > 0){ + if (roundbased && game.roundNum > 0) { fragsper = c->resp.score / game.roundNum; } else { sec = (level.framenum - c->resp.enterframe) / HZ; @@ -769,8 +775,10 @@ void G_RegisterScore(void) // Disable counting bot high scores if the cvar is set edict_t* player = FindEdictByClient(c); - if (player->is_bot && !g_highscores_countbots->value) + if (player->is_bot && !g_highscores_countbots->value) { + gi.dprintf("g_highscores_countbots is disabled, %s's score will not be recorded\n", player->client->pers.netname); return; + } gi.dprintf("Added highscore entry for %s with %d score\n", c->pers.netname, score); diff --git a/src/client/download.c b/src/client/download.c index 89bdd9a61..079255b72 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -675,11 +675,6 @@ void CL_RequestNextDownload(void) char fn[MAX_QPATH], *name; size_t len; int i; - static cvar_t *r_override_textures; - static cvar_t *r_texture_overrides; - - r_override_textures = Cvar_Get("r_override_textures", "1", CVAR_FILES); - r_texture_overrides = Cvar_Get("r_texture_overrides", "-1", CVAR_FILES); if (cls.state != ca_connected && cls.state != ca_loading) return; From f1508c15e88e9eb89febc9cf7007ddf0c753c0b6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 28 Oct 2024 14:49:02 -0400 Subject: [PATCH 913/974] Report all players in a pickup match in the Discord webhook --- src/action/tng_net.c | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/action/tng_net.c b/src/action/tng_net.c index ba50d7815..684f5ec8a 100644 --- a/src/action/tng_net.c +++ b/src/action/tng_net.c @@ -623,12 +623,37 @@ static char *discord_PickupReqMsg(char* msg) char *team_name = "Players"; char field_content[64]; snprintf(field_content, sizeof(field_content), "%s", team_name); - char *team_players = TeamConstructPlayerList(0); - // Add triple backticks for Discord formatting - char discord_formatted_players[1280]; - snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", team_players); + // Buffer to hold the concatenated player lists for all teams + // (16 max characters per name * 32 clients = 512 characters, give some extra buffer room) + char all_team_players[544] = ""; // Adjust size as needed + // Iterate through each team (0 to 3) and concatenate the player lists + int MAX_TEAM = 0; + int MIN_TEAM = 0; + + if (teamplay->value) { + MIN_TEAM = TEAM1; + MAX_TEAM = teamCount; + } else { + MIN_TEAM = 0; + MAX_TEAM = 0; + } + + for (int team = MIN_TEAM; team <= MAX_TEAM; team++) { + char *team_players = TeamConstructPlayerList(team); + if (team_players) { + // Check if adding the next player list would exceed 512 characters + if (strlen(all_team_players) + strlen(team_players) + 1 >= 512) { // +1 for the newline character + break; // Stop adding more players + } + strncat(all_team_players, team_players, sizeof(all_team_players) - strlen(all_team_players) - 1); + strncat(all_team_players, "\n", sizeof(all_team_players) - strlen(all_team_players) - 1); // Add a newline between team lists + } + } + // Add triple backticks for Discord formatting + char discord_formatted_players[1280]; // Adjust size as needed + snprintf(discord_formatted_players, sizeof(discord_formatted_players), "```\n%s```", all_team_players); json_t *field = json_object(); json_object_set_new(field, "name", json_string(field_content)); @@ -636,8 +661,6 @@ static char *discord_PickupReqMsg(char* msg) json_object_set_new(field, "inline", json_true()); json_array_append_new(fields, field); - free(team_players); // Free the allocated memory for team_players - // Game Settings json_t *gamesettings = json_object(); json_object_set_new(gamesettings, "name", json_string("Game Settings")); @@ -836,7 +859,7 @@ void lc_discord_webhook(char* message, Discord_Notifications msgtype, Awards awa // Get a new request object request = new_request(); if (request == NULL) { - gi.dprintf("Ran out of request slots\n"); + gi.dprintf("%s: Ran out of request slots\n", __func__); return; } From e6300ce42424669b2bb244c9f7a854eb7e7276b3 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 28 Oct 2024 15:40:14 -0400 Subject: [PATCH 914/974] Set bot ping to BOT if bot_reportasclient is 0 --- src/action/a_team.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/action/a_team.c b/src/action/a_team.c index a600c9126..75126ec0b 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -3878,7 +3878,10 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) #ifndef NO_BOTS //rekkie -- Fake Bot Client -- s if (cl_ent->is_bot) - Q_snprintf(buf, sizeof(buf), "%4i", min(9999, cl_ent->bot.bot_ping)); + if (bot_reportasclient->value) + Q_snprintf(buf, sizeof(buf), "%4i", min(9999, cl_ent->bot.bot_ping)); + else + Q_snprintf(buf, sizeof(buf), " BOT"); //if (0) //rekkie -- Fake Bot Client -- e //if( cl_ent->is_bot ) From a650c14b23f4159340bf8a38075e15d86b9145b5 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 28 Oct 2024 16:01:34 -0400 Subject: [PATCH 915/974] Re-added duplicate CL_ParseTEnt(); --- src/client/parse.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/parse.c b/src/client/parse.c index 0798e1d74..fdedff2e3 100644 --- a/src/client/parse.c +++ b/src/client/parse.c @@ -1521,6 +1521,7 @@ void CL_ParseServerMessage(void) case svc_temp_entity: CL_ParseTEntPacket(); + CL_ParseTEnt(); break; case svc_muzzleflash: From 646a9c1b57226ea31ba2d063b757537e8ee8f971 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 30 Oct 2024 17:17:42 -0400 Subject: [PATCH 916/974] Initial save point on player stat bar in GHUD --- inc/shared/gameext.h | 6 +- src/action/g_local.h | 6 ++ src/action/p_hud.c | 167 ++++++++++++++++++++++++++++++++++++++--- src/action/tng_stats.c | 17 +++++ src/action/tng_stats.h | 1 + 5 files changed, 183 insertions(+), 14 deletions(-) diff --git a/inc/shared/gameext.h b/inc/shared/gameext.h index 0658a065c..8def97850 100644 --- a/inc/shared/gameext.h +++ b/inc/shared/gameext.h @@ -68,7 +68,8 @@ typedef struct { game_import_ex_t -These pointer functions exist in the engine (server), and this enables importing them to be available for use by the gamelib +These pointer functions exist in the engine (server), +and this enables importing them to be available for use by the gamelib server->gamedll @@ -91,7 +92,8 @@ typedef struct { game_export_ex_t -These pointer functions exist in the gamedll, and this enables importing them to be available for use by the engine (server) +These pointer functions exist in the gamedll, +and this enables importing them to be available for use by the engine (server) gamedll->server diff --git a/src/action/g_local.h b/src/action/g_local.h index f0fe2a607..24a177f5e 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2955,6 +2955,12 @@ typedef enum { h_team_l_num, h_team_r, h_team_r_num, + h_spectator_stats, + h_spectator_timer, + h_spectator_time_tm, + h_spectator_time_mm, + h_spectator_time_ts, + h_spectator_time_ss, } huditem_t; void HUD_SetType(edict_t *clent, int type); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 2bf04043d..5b8bb98ed 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -781,7 +781,7 @@ void HUD_SpectatorSetup(edict_t *clent) int i; if (teamplay->value && spectator_hud->value) - { + { // Left-side nameplates for (i = 0; i < 6; i++) { int x, y; @@ -820,6 +820,7 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h], 0, 0); } + // Right side nameplates for (i = 0; i < 6; i++) { int x, y; @@ -858,17 +859,113 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h], 1, 0); } - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); - hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); - Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); - Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); - - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); - hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); + if (1) { + // GHUD bottom center stat display + int x, y; + int h_base = h_spectator_stats; + int h; + + x = -640; + y = 480; + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 288, 24); + Ghud_SetColor(clent, hud[h], 30, 60, 110, 255); + + h = h_base + 1; // name bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y - 16); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 20, 12); + Ghud_SetColor(clent, hud[h], 40, 80, 220, 255); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 2; // name print + hud[h] = Ghud_AddText(clent, x + 32, y + 32, ""); + Ghud_SetPosition(clent, hud[h], x, y - 16); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 20, 12); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 3; // stat table bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y + 16); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 0, 24); + Ghud_SetColor(clent, hud[h], 20, 40, 230, 255); + + h = h_base + 4; // stat table text + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 5; // frags + hud[h] = Ghud_AddText(clent, x + 32, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 6; // deaths + hud[h] = Ghud_AddText(clent, x + 96, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 7; // damage + hud[h] = Ghud_AddText(clent, x + 120, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 8; // accuracy + hud[h] = Ghud_AddText(clent, x + 185, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + + + + + + h = h_base + 10; // weapon select + hud[h] = Ghud_AddIcon(clent, x - 680, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 1, 0); + } + + if (timelimit->value) { + // GHUD top middle time display + int x, y; + int h_base = h_spectator_timer; + int h; + + x = -640; + y = 16; + + h = h_base; // timer area + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 288, 24); + Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); + hud[h_spectator_time_tm] = Ghud_AddNumber(clent, -128, 2, 0); + hud[h_spectator_time_mm] = Ghud_AddNumber(clent, -96, 2, 0); + hud[h_spectator_time_ts] = Ghud_AddNumber(clent, -64, 2, 0); + hud[h_spectator_time_ss] = Ghud_AddNumber(clent, -32, 2, 0); + + // GHUD top corner team icon + + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); + hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); + Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); + + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); + hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); + Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); + } } } @@ -1092,6 +1189,52 @@ void HUD_SpectatorUpdate(edict_t *clent) else // no weapon, set to mk23 Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } + + // If we're chasing a target, display their stats + if (clent->client->chase_target) { + // GHUD bottom center stat display + Ghud_SetFlags(clent, hud[h_spectator_stats], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 1], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 2], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 3], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 4], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 5], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 6], 0); + + char nm_s[17]; + char frags_s[24]; + char deaths_s[24]; + char dmg_s[24]; + char acc_s[10]; + int x, y; + int h = h_spectator_stats; + edict_t *targ = clent->client->chase_target; + memcpy(nm_s, targ->client->pers.netname, 16); + snprintf(frags_s, sizeof(frags_s), "%i", targ->client->resp.kills); + snprintf(deaths_s, sizeof(deaths_s), "%i", targ->client->resp.deaths); + snprintf(dmg_s, sizeof(dmg_s), "%i", targ->client->resp.damage_dealt); + snprintf(acc_s, sizeof(acc_s), "%.2f%%", CalculateAccuracy(targ)); + // update fields + Ghud_SetText(clent, hud[h + 2], nm_s); + Ghud_SetText(clent, hud[h + 4], "Frags Deaths Damage Acc."); + Ghud_SetText(clent, hud[h + 5], frags_s); + Ghud_SetText(clent, hud[h + 6], deaths_s); + Ghud_SetText(clent, hud[h + 7], dmg_s); + Ghud_SetText(clent, hud[h + 8], acc_s); + } + + + + + + // GHUD top middle time display + Ghud_SetFlags(clent, hud[h_spectator_timer], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_tm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], 0); + + } } diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 45d2c6dc0..c44691f60 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -837,6 +837,23 @@ void G_LoadScores(void) G_FreeFile(f); } +float CalculateAccuracy(edict_t* ent) +{ + int shots; + float accuracy; + + shots = ent->client->resp.shotsTotal; + + if (shots) + accuracy = (float)ent->client->resp.hitsTotal * 100.0f / (float)shots; + else + accuracy = 0.0f; + + // Round the accuracy to two decimal places + accuracy = roundf(accuracy * 100.0f) / 100.0f; + + return accuracy; +} #if USE_AQTION diff --git a/src/action/tng_stats.h b/src/action/tng_stats.h index 98303575d..3b62f7afc 100644 --- a/src/action/tng_stats.h +++ b/src/action/tng_stats.h @@ -3,6 +3,7 @@ void ResetStats(edict_t *ent); void Stats_AddShot(edict_t *ent, int gun); void Stats_AddHit(edict_t *ent, int gun, int hitPart); +float CalculateAccuracy(edict_t* ent); void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer); void Cmd_Stats_f (edict_t *targetent, char *arg); void Cmd_Statmode_f(edict_t *ent); From f973eafbaeaa7ff7bb538eb4af60ed866afa6a05 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 30 Oct 2024 20:29:22 -0400 Subject: [PATCH 917/974] Looking a little better --- src/action/g_local.h | 6 +- src/action/p_hud.c | 263 +++++++++++++++++++++++++++---------------- 2 files changed, 169 insertions(+), 100 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 24a177f5e..721a941bf 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2955,8 +2955,10 @@ typedef enum { h_team_l_num, h_team_r, h_team_r_num, - h_spectator_stats, - h_spectator_timer, + h_spectator_stats = 66, + h_spectator_stats_bar = 72, + h_spectator_name_bar = 74, + h_spectator_timer = 76, h_spectator_time_tm, h_spectator_time_mm, h_spectator_time_ts, diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 5b8bb98ed..8e9130696 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -771,12 +771,103 @@ void HUD_ClientUpdate(edict_t *clent) } +void HUD_SpectatorStatsSetup(edict_t *clent) +{ + Ghud_ClearForClient(clent); + clent->client->resp.hud_type = 1; + int *hud = clent->client->resp.hud_items; + + // GHUD bottom center stat display + int x, y; + int h_base = h_spectator_stats; + int h_nbar = h_spectator_name_bar; + int h_sbar = h_spectator_stats_bar; + int h, j, k; + + x = -640; + y = 480; + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y + 12); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 288, 12); + Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); + + h = h_base + 1; // frags + hud[h] = Ghud_AddText(clent, x + 32, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 2; // deaths + hud[h] = Ghud_AddText(clent, x + 96, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 3; // damage + hud[h] = Ghud_AddText(clent, x + 120, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 4; // accuracy + hud[h] = Ghud_AddText(clent, x + 185, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 10; // weapon selection + hud[h] = Ghud_AddIcon(clent, x - 680, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 1, 0); + + j = h_nbar; // name bar + hud[j] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[j], x, y - 12); + Ghud_SetAnchor(clent, hud[j], 1, 0); + Ghud_SetSize(clent, hud[j], 288, 12); + Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); + + j = h_nbar + 1; // name print + hud[j] = Ghud_AddText(clent, x, y + 12, ""); + Ghud_SetPosition(clent, hud[j], x + 120, y - 12); + Ghud_SetAnchor(clent, hud[j], 1, 0); + Ghud_SetSize(clent, hud[j], 20, 12); + Ghud_SetTextFlags(clent, hud[j], UI_CENTER); + + k = h_sbar; // stat table bar + hud[k] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[k], x, y); + Ghud_SetAnchor(clent, hud[k], 1, 0); + Ghud_SetSize(clent, hud[k], 0, 12); + Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); + + k = h_sbar + 1; // stat table text + hud[k] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[k], 1, 0); + Ghud_SetTextFlags(clent, hud[k], UI_LEFT); +} void HUD_SpectatorSetup(edict_t *clent) { Ghud_ClearForClient(clent); clent->client->resp.hud_type = 1; + int nameplate_alpha = 230; + + // Red team colors + int red_team_red = 220; + int red_team_green = 60; + int red_team_blue = 60; + int alt_red_team_red = 110; + int alt_red_team_green = 45; + int alt_red_team_blue = 45; + + // Blue team colors + int blue_team_red = 40; + int blue_team_green = 80; + int blue_team_blue = 220; + int alt_blue_team_red = 30; + int alt_blue_team_green = 60; + int alt_blue_team_blue = 110; + int *hud = clent->client->resp.hud_items; int i; @@ -796,14 +887,14 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetSize(clent, hud[h], 144, 24); - Ghud_SetColor(clent, hud[h], 110, 45, 45, 230); + Ghud_SetColor(clent, hud[h], alt_red_team_red, alt_red_team_green, alt_red_team_blue, nameplate_alpha); h = h_base + 1; // health bar hud[h] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetSize(clent, hud[h], 0, 24); - Ghud_SetColor(clent, hud[h], 220, 60, 60, 230); + Ghud_SetColor(clent, hud[h], red_team_red, red_team_green, red_team_blue, nameplate_alpha); h = h_base + 2; // name hud[h] = Ghud_AddText(clent, x + 142, y + 3, ""); @@ -835,14 +926,14 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetSize(clent, hud[h], 144, 24); - Ghud_SetColor(clent, hud[h], 30, 60, 110, 230); + Ghud_SetColor(clent, hud[h], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, nameplate_alpha); h = h_base + 1; // health bar hud[h] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetSize(clent, hud[h], 0, 24); - Ghud_SetColor(clent, hud[h], 40, 80, 220, 230); + Ghud_SetColor(clent, hud[h], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); h = h_base + 2; // name hud[h] = Ghud_AddText(clent, x + 2, y + 3, ""); @@ -859,79 +950,6 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h], 1, 0); } - if (1) { - // GHUD bottom center stat display - int x, y; - int h_base = h_spectator_stats; - int h; - - x = -640; - y = 480; - - h = h_base; // back bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 288, 24); - Ghud_SetColor(clent, hud[h], 30, 60, 110, 255); - - h = h_base + 1; // name bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y - 16); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 20, 12); - Ghud_SetColor(clent, hud[h], 40, 80, 220, 255); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 2; // name print - hud[h] = Ghud_AddText(clent, x + 32, y + 32, ""); - Ghud_SetPosition(clent, hud[h], x, y - 16); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 20, 12); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 3; // stat table bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y + 16); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 0, 24); - Ghud_SetColor(clent, hud[h], 20, 40, 230, 255); - - h = h_base + 4; // stat table text - hud[h] = Ghud_AddText(clent, x, y, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 5; // frags - hud[h] = Ghud_AddText(clent, x + 32, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 6; // deaths - hud[h] = Ghud_AddText(clent, x + 96, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 7; // damage - hud[h] = Ghud_AddText(clent, x + 120, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 8; // accuracy - hud[h] = Ghud_AddText(clent, x + 185, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - - - - - - h = h_base + 10; // weapon select - hud[h] = Ghud_AddIcon(clent, x - 680, y + 2, level.pic_items[M4_NUM], 20, 20); - Ghud_SetAnchor(clent, hud[h], 1, 0); - } - if (timelimit->value) { // GHUD top middle time display int x, y; @@ -1190,16 +1208,22 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } + int h_base = h_spectator_stats; + int h_nbar = h_spectator_name_bar; + int h_sbar = h_spectator_stats_bar; // If we're chasing a target, display their stats if (clent->client->chase_target) { + HUD_SpectatorStatsSetup(clent); // GHUD bottom center stat display Ghud_SetFlags(clent, hud[h_spectator_stats], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 1], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 2], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 3], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 4], 0); - Ghud_SetFlags(clent, hud[h_spectator_stats + 5], 0); - Ghud_SetFlags(clent, hud[h_spectator_stats + 6], 0); + Ghud_SetFlags(clent, hud[h_spectator_name_bar], 0); + Ghud_SetFlags(clent, hud[h_spectator_name_bar + 1], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats_bar], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats_bar + 1], 0); char nm_s[17]; char frags_s[24]; @@ -1207,7 +1231,6 @@ void HUD_SpectatorUpdate(edict_t *clent) char dmg_s[24]; char acc_s[10]; int x, y; - int h = h_spectator_stats; edict_t *targ = clent->client->chase_target; memcpy(nm_s, targ->client->pers.netname, 16); snprintf(frags_s, sizeof(frags_s), "%i", targ->client->resp.kills); @@ -1215,26 +1238,70 @@ void HUD_SpectatorUpdate(edict_t *clent) snprintf(dmg_s, sizeof(dmg_s), "%i", targ->client->resp.damage_dealt); snprintf(acc_s, sizeof(acc_s), "%.2f%%", CalculateAccuracy(targ)); // update fields - Ghud_SetText(clent, hud[h + 2], nm_s); - Ghud_SetText(clent, hud[h + 4], "Frags Deaths Damage Acc."); - Ghud_SetText(clent, hud[h + 5], frags_s); - Ghud_SetText(clent, hud[h + 6], deaths_s); - Ghud_SetText(clent, hud[h + 7], dmg_s); - Ghud_SetText(clent, hud[h + 8], acc_s); - } - - - - - - // GHUD top middle time display - Ghud_SetFlags(clent, hud[h_spectator_timer], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_tm], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_mm], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_ts], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_ss], 0); + // Change color based on team + int nameplate_alpha = 200; + + // Red team colors + int red_team_red = 220; + int red_team_green = 60; + int red_team_blue = 60; + int alt_red_team_red = 110; + int alt_red_team_green = 45; + int alt_red_team_blue = 45; + + // Blue team colors + int blue_team_red = 40; + int blue_team_green = 80; + int blue_team_blue = 220; + int alt_blue_team_red = 30; + int alt_blue_team_green = 60; + int alt_blue_team_blue = 110; + + // Green team colors + int green_team_red = 40; + int green_team_green = 220; + int green_team_blue = 40; + int alt_green_team_red = 30; + int alt_green_team_green = 140; + int alt_green_team_blue = 30; + + // target name + Ghud_SetText(clent, hud[h_nbar + 1], nm_s); + // target stat bar + Ghud_SetText(clent, hud[h_sbar + 1], "Frags Deaths Damage Acc."); + // target stat values + Ghud_SetText(clent, hud[h_base + 1], frags_s); + Ghud_SetText(clent, hud[h_base + 2], deaths_s); + Ghud_SetText(clent, hud[h_base + 3], dmg_s); + Ghud_SetText(clent, hud[h_base + 4], acc_s); + + // Color per team + if (clent->client->chase_target) { + if (targ->client->resp.team == TEAM1) { + Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_sbar], alt_red_team_red, alt_red_team_green, alt_red_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], red_team_red, red_team_green, red_team_blue, 255); + } else if (targ->client->resp.team == TEAM2) { + Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_sbar], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], blue_team_red, blue_team_green, blue_team_blue, 255); + } else if (targ->client->resp.team == TEAM3) { + Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_sbar], alt_green_team_red, alt_green_team_green, alt_green_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], green_team_red, green_team_green, green_team_blue, 255); + } + } + } + if (timelimit->value) { + // GHUD top middle time display + Ghud_SetFlags(clent, hud[h_spectator_timer], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_tm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], 0); + } } } From 2a108c65f32dfbae1d30347a3041f83f5b1d9dd6 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 31 Oct 2024 15:15:20 -0400 Subject: [PATCH 918/974] Re-colorized, fixed stat bar drawing --- src/action/p_hud.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 8e9130696..3bc04b919 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -836,7 +836,7 @@ void HUD_SpectatorStatsSetup(edict_t *clent) hud[k] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetPosition(clent, hud[k], x, y); Ghud_SetAnchor(clent, hud[k], 1, 0); - Ghud_SetSize(clent, hud[k], 0, 12); + Ghud_SetSize(clent, hud[k], 288, 12); Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); k = h_sbar + 1; // stat table text @@ -1240,7 +1240,7 @@ void HUD_SpectatorUpdate(edict_t *clent) // update fields // Change color based on team - int nameplate_alpha = 200; + int nameplate_alpha = 180; // Red team colors int red_team_red = 220; @@ -1279,17 +1279,17 @@ void HUD_SpectatorUpdate(edict_t *clent) // Color per team if (clent->client->chase_target) { if (targ->client->resp.team == TEAM1) { - Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_nbar], red_team_red, red_team_green, red_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_red_team_red, alt_red_team_green, alt_red_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], red_team_red, red_team_green, red_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); } else if (targ->client->resp.team == TEAM2) { - Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_nbar], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], blue_team_red, blue_team_green, blue_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); } else if (targ->client->resp.team == TEAM3) { - Ghud_SetColor(clent, hud[h_nbar], 150, 150, 150, nameplate_alpha); + Ghud_SetColor(clent, hud[h_nbar], green_team_red, green_team_green, green_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_green_team_red, alt_green_team_green, alt_green_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], green_team_red, green_team_green, green_team_blue, 255); + Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); } } } From a24112dc629defd73b7b0074cb63947956cd3dee Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 31 Oct 2024 15:46:06 -0400 Subject: [PATCH 919/974] Fixed team 1 bug --- src/action/p_hud.c | 53 ++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 3bc04b919..0ae5a2dd7 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -773,7 +773,6 @@ void HUD_ClientUpdate(edict_t *clent) void HUD_SpectatorStatsSetup(edict_t *clent) { - Ghud_ClearForClient(clent); clent->client->resp.hud_type = 1; int *hud = clent->client->resp.hud_items; @@ -792,7 +791,8 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetPosition(clent, hud[h], x, y + 12); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetSize(clent, hud[h], 288, 12); - Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); + Ghud_SetFlags(clent, hud[h], GHF_HIDE); + //Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); h = h_base + 1; // frags hud[h] = Ghud_AddText(clent, x + 32, y + 14, ""); @@ -823,7 +823,8 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetPosition(clent, hud[j], x, y - 12); Ghud_SetAnchor(clent, hud[j], 1, 0); Ghud_SetSize(clent, hud[j], 288, 12); - Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); + //Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); + Ghud_SetFlags(clent, hud[j], GHF_HIDE); j = h_nbar + 1; // name print hud[j] = Ghud_AddText(clent, x, y + 12, ""); @@ -837,7 +838,8 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetPosition(clent, hud[k], x, y); Ghud_SetAnchor(clent, hud[k], 1, 0); Ghud_SetSize(clent, hud[k], 288, 12); - Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); + //Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); + Ghud_SetFlags(clent, hud[k], GHF_HIDE); k = h_sbar + 1; // stat table text hud[k] = Ghud_AddText(clent, x, y, ""); @@ -950,6 +952,9 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h], 1, 0); } + // GHUD chase player stats + HUD_SpectatorStatsSetup(clent); + if (timelimit->value) { // GHUD top middle time display int x, y; @@ -969,21 +974,21 @@ void HUD_SpectatorSetup(edict_t *clent) hud[h_spectator_time_mm] = Ghud_AddNumber(clent, -96, 2, 0); hud[h_spectator_time_ts] = Ghud_AddNumber(clent, -64, 2, 0); hud[h_spectator_time_ss] = Ghud_AddNumber(clent, -32, 2, 0); - + } // GHUD top corner team icon - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); - hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); - Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); - Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); + hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); + Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); + + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); + hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); + Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); - hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); - } } } @@ -997,7 +1002,7 @@ void HUD_SpectatorUpdate(edict_t *clent) if (!(clent->client->pers.spec_flags & SPECFL_SPECHUD_NEW)) // hide all elements since client doesn't want them { - for (i = 0; i <= h_team_r_num; i++) + for (i = 0; i <= h_spectator_time_ss; i++) { Ghud_SetFlags(clent, hud[i], GHF_HIDE); } @@ -1212,8 +1217,10 @@ void HUD_SpectatorUpdate(edict_t *clent) int h_nbar = h_spectator_name_bar; int h_sbar = h_spectator_stats_bar; // If we're chasing a target, display their stats + + //gi.dprintf("chase target: %s\n", clent->client->chase_target ? clent->client->chase_target->client->pers.netname : "none"); + //gi.dprintf("chase target mode: %i\n", clent->client->chase_mode); if (clent->client->chase_target) { - HUD_SpectatorStatsSetup(clent); // GHUD bottom center stat display Ghud_SetFlags(clent, hud[h_spectator_stats], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 1], 0); @@ -1292,6 +1299,16 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); } } + } else { // Hide the stats if we're not chasing a target + Ghud_SetFlags(clent, hud[h_spectator_stats], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 2], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 3], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 4], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_name_bar], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_name_bar + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats_bar], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats_bar + 1], GHF_HIDE); } if (timelimit->value) { From 7c4cab9c490aa049660c6681c71348e766cacb5d Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 31 Oct 2024 17:05:27 -0400 Subject: [PATCH 920/974] Added center clock timer --- src/action/g_local.h | 9 +-- src/action/p_hud.c | 130 +++++++++++++++++++++++++++++++++---------- 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 721a941bf..bfaaf522c 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2955,14 +2955,15 @@ typedef enum { h_team_l_num, h_team_r, h_team_r_num, - h_spectator_stats = 66, - h_spectator_stats_bar = 72, - h_spectator_name_bar = 74, - h_spectator_timer = 76, + h_spectator_stats = 66, // display up to 5 stats at once + h_spectator_stats_bar = 73, // h_spectator_stats_bar text is 74 + h_spectator_name_bar = 75, // h_spectator_name_bar text is 76 + h_spectator_timer = 77, h_spectator_time_tm, h_spectator_time_mm, h_spectator_time_ts, h_spectator_time_ss, + h_spectator_time_sep, // Time seperator (:) } huditem_t; void HUD_SetType(edict_t *clent, int type); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 0ae5a2dd7..3ec31f56d 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -770,6 +770,100 @@ void HUD_ClientUpdate(edict_t *clent) { } +typedef enum { + tm = 0, + mm = 1, + ts = 2, + ss = 3 +} hud_time_digits; + +static int GetRemainingTimeDigits(hud_time_digits timeval) +{ + int remaining = 0, rmins = 0, rsecs = 0, gametime = 0; + + gametime = level.matchTime; + + remaining = (timelimit->value * 60) - gametime; + + rmins = remaining / 60; + rsecs = remaining % 60; + + switch (timeval) { + case tm: // Tens of minutes + return (rmins / 10) % 10; + case mm: // Minutes + return rmins % 10; + case ts: // Tens of seconds + return (rsecs / 10) % 10; + case ss: // Seconds + return rsecs % 10; + default: + return -1; // Invalid timeval + } +} + +static void HUD_UpdateSpectatorTimer(edict_t *clent) +{ + int *hud = clent->client->resp.hud_items; + + // Update the HUD elements with the individual digits + Ghud_SetInt(clent, hud[h_spectator_time_tm], GetRemainingTimeDigits(tm)); + Ghud_SetInt(clent, hud[h_spectator_time_mm], GetRemainingTimeDigits(mm)); + Ghud_SetInt(clent, hud[h_spectator_time_ts], GetRemainingTimeDigits(ts)); + Ghud_SetInt(clent, hud[h_spectator_time_ss], GetRemainingTimeDigits(ss)); + + if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 1) { + // Change bar color to red + Ghud_SetColor(clent, hud[h_spectator_timer], 255, 0, 0, 120); + } else if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 3) + { + // Change bar color to orange + Ghud_SetColor(clent, hud[h_spectator_timer], 255, 165, 0, 120); + } + +} + +void HUD_SpectatorTimerSetup(edict_t *clent) +{ + int h_base = h_spectator_timer; + int h; + h = h_base; // timer area + int *hud = clent->client->resp.hud_items; + + if (timelimit->value) { + // GHUD top middle time display + clent->client->resp.hud_type = 1; + + // GHUD bottom center stat display + int x, y; + + x = 450; + y = 16; + + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y + 12); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetSize(clent, hud[h], 90, -48); + Ghud_SetColor(clent, hud[h], 50, 130, 50, 120); + + // Add number elements for minutes + hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x, 2, 0); + hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x + 20, 2, 0); + + // Draw timer seperator + hud[h_spectator_time_sep] = Ghud_AddText(clent, x + 40, y - 4 , ":"); + + // Add number elements for seconds + hud[h_spectator_time_ts] = Ghud_AddNumber(clent, x + 50, 2, 0); + hud[h_spectator_time_ss] = Ghud_AddNumber(clent, x + 70, 2, 0); + } else { + Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_sep], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], GHF_HIDE); + } +} void HUD_SpectatorStatsSetup(edict_t *clent) { @@ -955,28 +1049,10 @@ void HUD_SpectatorSetup(edict_t *clent) // GHUD chase player stats HUD_SpectatorStatsSetup(clent); - if (timelimit->value) { - // GHUD top middle time display - int x, y; - int h_base = h_spectator_timer; - int h; - - x = -640; - y = 16; - - h = h_base; // timer area - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 288, 24); - Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); - hud[h_spectator_time_tm] = Ghud_AddNumber(clent, -128, 2, 0); - hud[h_spectator_time_mm] = Ghud_AddNumber(clent, -96, 2, 0); - hud[h_spectator_time_ts] = Ghud_AddNumber(clent, -64, 2, 0); - hud[h_spectator_time_ss] = Ghud_AddNumber(clent, -32, 2, 0); - } - // GHUD top corner team icon - + // GHUD top middle time display + HUD_SpectatorTimerSetup(clent); + + // GHUD top corner team icon hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); @@ -1311,14 +1387,8 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetFlags(clent, hud[h_spectator_stats_bar + 1], GHF_HIDE); } - if (timelimit->value) { - // GHUD top middle time display - Ghud_SetFlags(clent, hud[h_spectator_timer], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_tm], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_mm], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_ts], 0); - Ghud_SetFlags(clent, hud[h_spectator_time_ss], 0); - } + // Update the timer display + HUD_UpdateSpectatorTimer(clent); } } From a51878fd53ff24377bd15bfadd18e411fb0b99bd Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 31 Oct 2024 17:11:24 -0400 Subject: [PATCH 921/974] Hiding timer if timelimit is 0 --- src/action/p_hud.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 3ec31f56d..5422acd45 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1313,7 +1313,6 @@ void HUD_SpectatorUpdate(edict_t *clent) char deaths_s[24]; char dmg_s[24]; char acc_s[10]; - int x, y; edict_t *targ = clent->client->chase_target; memcpy(nm_s, targ->client->pers.netname, 16); snprintf(frags_s, sizeof(frags_s), "%i", targ->client->resp.kills); @@ -1388,7 +1387,16 @@ void HUD_SpectatorUpdate(edict_t *clent) } // Update the timer display - HUD_UpdateSpectatorTimer(clent); + if (timelimit->value) { + HUD_UpdateSpectatorTimer(clent); + } else { + Ghud_SetFlags(clent, hud[h_spectator_timer], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_sep], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], GHF_HIDE); + } } } From e2c2c732ebf4aa129cb9edfe65b4b1e1252ff912 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 17:50:13 -0400 Subject: [PATCH 922/974] Reduced timer border thickness --- src/action/g_local.h | 1 + src/action/p_hud.c | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index bfaaf522c..8386f22d4 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2959,6 +2959,7 @@ typedef enum { h_spectator_stats_bar = 73, // h_spectator_stats_bar text is 74 h_spectator_name_bar = 75, // h_spectator_name_bar text is 76 h_spectator_timer = 77, + h_spectator_timer_border, h_spectator_time_tm, h_spectator_time_mm, h_spectator_time_ts, diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 5422acd45..cee9d3e57 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -814,11 +814,14 @@ static void HUD_UpdateSpectatorTimer(edict_t *clent) if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 1) { // Change bar color to red - Ghud_SetColor(clent, hud[h_spectator_timer], 255, 0, 0, 120); + Ghud_SetColor(clent, hud[h_spectator_timer_border], 255, 0, 0, 120); } else if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 3) { // Change bar color to orange - Ghud_SetColor(clent, hud[h_spectator_timer], 255, 165, 0, 120); + Ghud_SetColor(clent, hud[h_spectator_timer_border], 255, 165, 0, 120); + } else { + // Change bar color to green + Ghud_SetColor(clent, hud[h_spectator_timer_border], 50, 150, 50, 120); } } @@ -837,18 +840,25 @@ void HUD_SpectatorTimerSetup(edict_t *clent) // GHUD bottom center stat display int x, y; - x = 450; + x = 470; y = 16; + hud[h_spectator_timer_border] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h_spectator_timer_border], x - 1, y - 18); + Ghud_SetAnchor(clent, hud[h_spectator_timer_border], 0, 0); + Ghud_SetSize(clent, hud[h_spectator_timer_border], 92, 31); + Ghud_SetColor(clent, hud[h_spectator_timer_border], 50, 150, 50, 120); + hud[h] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetPosition(clent, hud[h], x, y + 12); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetSize(clent, hud[h], 90, -48); - Ghud_SetColor(clent, hud[h], 50, 130, 50, 120); + Ghud_SetColor(clent, hud[h], 50, 50, 50, 255); // Add number elements for minutes hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x, 2, 0); hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x + 20, 2, 0); + // Draw timer seperator hud[h_spectator_time_sep] = Ghud_AddText(clent, x + 40, y - 4 , ":"); @@ -1390,6 +1400,7 @@ void HUD_SpectatorUpdate(edict_t *clent) if (timelimit->value) { HUD_UpdateSpectatorTimer(clent); } else { + Ghud_SetFlags(clent, hud[h_spectator_timer_border], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_timer], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); From 08ce49c585d2d619e83ebc1122c913af34c1b7d6 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 20:10:09 -0400 Subject: [PATCH 923/974] SetAnchor'd timer --- src/action/g_ext.c | 5 +++++ src/action/p_hud.c | 48 ++++++++++++++++++++++++---------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/action/g_ext.c b/src/action/g_ext.c index ed99a157e..df64bc866 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -413,6 +413,11 @@ void Ghud_SetSize(edict_t *ent, int i, int x, int y) engine_Ghud_SetSize(ent, i, x, y); } +// anchor is a float from 0 to 1, 0 being left/top, 1 being right/bottom +// for example, 0.5, 0.5 is center +// 0, 0 is top left +// 1, 1 is bottom right + void Ghud_SetAnchor(edict_t *ent, int i, float x, float y) { if (!engine_Ghud_SetAnchor) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index cee9d3e57..5e3262cb2 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -815,22 +815,18 @@ static void HUD_UpdateSpectatorTimer(edict_t *clent) if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 1) { // Change bar color to red Ghud_SetColor(clent, hud[h_spectator_timer_border], 255, 0, 0, 120); - } else if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 3) - { + } else if (GetRemainingTimeDigits(tm) == 0 && GetRemainingTimeDigits(mm) < 3){ // Change bar color to orange Ghud_SetColor(clent, hud[h_spectator_timer_border], 255, 165, 0, 120); } else { // Change bar color to green - Ghud_SetColor(clent, hud[h_spectator_timer_border], 50, 150, 50, 120); + Ghud_SetColor(clent, hud[h_spectator_timer_border], 20, 20, 20, 255); } } void HUD_SpectatorTimerSetup(edict_t *clent) { - int h_base = h_spectator_timer; - int h; - h = h_base; // timer area int *hud = clent->client->resp.hud_items; if (timelimit->value) { @@ -839,33 +835,39 @@ void HUD_SpectatorTimerSetup(edict_t *clent) // GHUD bottom center stat display int x, y; - - x = 470; - y = 16; + x = 0; + y = 0; hud[h_spectator_timer_border] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h_spectator_timer_border], x - 1, y - 18); - Ghud_SetAnchor(clent, hud[h_spectator_timer_border], 0, 0); - Ghud_SetSize(clent, hud[h_spectator_timer_border], 92, 31); + Ghud_SetAnchor(clent, hud[h_spectator_timer_border], 0.5, 0); + Ghud_SetPosition(clent, hud[h_spectator_timer_border], x - 51, y); + Ghud_SetSize(clent, hud[h_spectator_timer_border], 102, 31); Ghud_SetColor(clent, hud[h_spectator_timer_border], 50, 150, 50, 120); - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y + 12); - Ghud_SetAnchor(clent, hud[h], 0, 0); - Ghud_SetSize(clent, hud[h], 90, -48); - Ghud_SetColor(clent, hud[h], 50, 50, 50, 255); + hud[h_spectator_timer] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetAnchor(clent, hud[h_spectator_timer], 0.5, 0); + Ghud_SetPosition(clent, hud[h_spectator_timer], x - 50, y); + Ghud_SetSize(clent, hud[h_spectator_timer], 100, 30); + Ghud_SetColor(clent, hud[h_spectator_timer], 50, 50, 50, 255); // Add number elements for minutes - hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x, 2, 0); - hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x + 20, 2, 0); - + hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x - 50, 2, 0); + hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x - 30, 2, 0); // Draw timer seperator - hud[h_spectator_time_sep] = Ghud_AddText(clent, x + 40, y - 4 , ":"); + hud[h_spectator_time_sep] = Ghud_AddText(clent, x - 5, y + 12, ":"); // Add number elements for seconds - hud[h_spectator_time_ts] = Ghud_AddNumber(clent, x + 50, 2, 0); - hud[h_spectator_time_ss] = Ghud_AddNumber(clent, x + 70, 2, 0); + hud[h_spectator_time_ts] = Ghud_AddNumber(clent, x + 10, 2, 0); + hud[h_spectator_time_ss] = Ghud_AddNumber(clent, x + 30, 2, 0); + + // Anchor everything to the top middle + Ghud_SetAnchor(clent, hud[h_spectator_time_tm], 0.5, 0); + Ghud_SetAnchor(clent, hud[h_spectator_time_mm], 0.5, 0); + Ghud_SetAnchor(clent, hud[h_spectator_time_sep], 0.5, 0); + Ghud_SetAnchor(clent, hud[h_spectator_time_ts], 0.5, 0); + Ghud_SetAnchor(clent, hud[h_spectator_time_ss], 0.5, 0); + } else { Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); From e1bd23240e189e49cde6303bd47a0b6df47fdaa6 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 20:50:52 -0400 Subject: [PATCH 924/974] Using SetAnchor, center stat bar compatible with scr_scale --- src/action/g_ext.c | 10 ++++---- src/action/p_hud.c | 58 ++++++++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/action/g_ext.c b/src/action/g_ext.c index df64bc866..aded396fc 100644 --- a/src/action/g_ext.c +++ b/src/action/g_ext.c @@ -413,11 +413,11 @@ void Ghud_SetSize(edict_t *ent, int i, int x, int y) engine_Ghud_SetSize(ent, i, x, y); } -// anchor is a float from 0 to 1, 0 being left/top, 1 being right/bottom -// for example, 0.5, 0.5 is center -// 0, 0 is top left -// 1, 1 is bottom right - +/* anchor is a float from 0 to 1, 0 being left/top, 1 being right/bottom +for example, 0.5, 0.5 is center +0, 0 is top left +1, 1 is bottom right +*/ void Ghud_SetAnchor(edict_t *ent, int i, float x, float y) { if (!engine_Ghud_SetAnchor) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 5e3262cb2..fe8f22669 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -889,67 +889,75 @@ void HUD_SpectatorStatsSetup(edict_t *clent) int h_sbar = h_spectator_stats_bar; int h, j, k; - x = -640; - y = 480; + // All elements are anchored from the gray bar at the bottom + x = -160; + y = -60; - h = h_base; // back bar + h = h_base; // gray bar hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y + 12); - Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetSize(clent, hud[h], 288, 12); Ghud_SetFlags(clent, hud[h], GHF_HIDE); //Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); h = h_base + 1; // frags - hud[h] = Ghud_AddText(clent, x + 32, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); + hud[h] = Ghud_AddText(clent, x + 500, y + 200, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 33, y); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 2; // deaths - hud[h] = Ghud_AddText(clent, x + 96, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 97, y); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 3; // damage - hud[h] = Ghud_AddText(clent, x + 120, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 160, y); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 4; // accuracy - hud[h] = Ghud_AddText(clent, x + 185, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 192, y); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - h = h_base + 10; // weapon selection - hud[h] = Ghud_AddIcon(clent, x - 680, y + 2, level.pic_items[M4_NUM], 20, 20); - Ghud_SetAnchor(clent, hud[h], 1, 0); + // h = h_base + 10; // weapon selection + // Ghud_SetAnchor(clent, hud[h], 0.5, 1); + // hud[h] = Ghud_AddIcon(clent, x + 200, y + 200, level.pic_items[M4_NUM], 20, 20); + j = h_nbar; // name bar hud[j] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[j], x, y - 12); - Ghud_SetAnchor(clent, hud[j], 1, 0); + Ghud_SetAnchor(clent, hud[j], 0.5, 1); + Ghud_SetPosition(clent, hud[j], x, y - 24); Ghud_SetSize(clent, hud[j], 288, 12); //Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[j], GHF_HIDE); j = h_nbar + 1; // name print - hud[j] = Ghud_AddText(clent, x, y + 12, ""); - Ghud_SetPosition(clent, hud[j], x + 120, y - 12); - Ghud_SetAnchor(clent, hud[j], 1, 0); + hud[j] = Ghud_AddText(clent, x, y + 22, ""); + Ghud_SetAnchor(clent, hud[j], 0.5, 1); + Ghud_SetPosition(clent, hud[j], x + 140, y - 22); Ghud_SetSize(clent, hud[j], 20, 12); Ghud_SetTextFlags(clent, hud[j], UI_CENTER); k = h_sbar; // stat table bar hud[k] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[k], x, y); - Ghud_SetAnchor(clent, hud[k], 1, 0); + Ghud_SetAnchor(clent, hud[k], 0.5, 1); + Ghud_SetPosition(clent, hud[k], x, y - 12); Ghud_SetSize(clent, hud[k], 288, 12); //Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[k], GHF_HIDE); k = h_sbar + 1; // stat table text - hud[k] = Ghud_AddText(clent, x, y, ""); - Ghud_SetAnchor(clent, hud[k], 1, 0); + hud[k] = Ghud_AddText(clent, x, y - 10, ""); + Ghud_SetAnchor(clent, hud[k], 0.5, 1); + Ghud_SetPosition(clent, hud[k], x, y - 10); + Ghud_SetSize(clent, hud[k], 20, 12); Ghud_SetTextFlags(clent, hud[k], UI_LEFT); } From d77ffe276bf36e7f4a6e94e98e43816b049dc561 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 20:52:38 -0400 Subject: [PATCH 925/974] Using SetAnchor, center stat bar compatible with scr_scale --- src/action/p_hud.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index fe8f22669..6d432c5c8 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -904,7 +904,7 @@ void HUD_SpectatorStatsSetup(edict_t *clent) h = h_base + 1; // frags hud[h] = Ghud_AddText(clent, x + 500, y + 200, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 33, y); + Ghud_SetPosition(clent, hud[h], x + 33, y - 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 2; // deaths From 3a5500c237ceb168710c48e03bfd384bd51df0e4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 21:00:10 -0400 Subject: [PATCH 926/974] Fixed vertical alignment of stat values --- src/action/p_hud.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 6d432c5c8..79c34d998 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -904,25 +904,25 @@ void HUD_SpectatorStatsSetup(edict_t *clent) h = h_base + 1; // frags hud[h] = Ghud_AddText(clent, x + 500, y + 200, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 33, y - 2); + Ghud_SetPosition(clent, hud[h], x + 33, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 2; // deaths hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 97, y); + Ghud_SetPosition(clent, hud[h], x + 97, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 3; // damage hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 160, y); + Ghud_SetPosition(clent, hud[h], x + 160, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 4; // accuracy hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 192, y); + Ghud_SetPosition(clent, hud[h], x + 192, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); // h = h_base + 10; // weapon selection From a0baa5d67855b0eb8b405d72fde4e61db4dd5d02 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 21:39:48 -0400 Subject: [PATCH 927/974] Moved stats horizontally, added award tallies --- src/action/g_local.h | 6 +++--- src/action/p_hud.c | 51 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/action/g_local.h b/src/action/g_local.h index 8386f22d4..a1303504a 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2956,9 +2956,9 @@ typedef enum { h_team_r, h_team_r_num, h_spectator_stats = 66, // display up to 5 stats at once - h_spectator_stats_bar = 73, // h_spectator_stats_bar text is 74 - h_spectator_name_bar = 75, // h_spectator_name_bar text is 76 - h_spectator_timer = 77, + h_spectator_stats_bar = 74, // h_spectator_stats_bar text is 75 + h_spectator_name_bar = 76, // h_spectator_name_bar text is 77 + h_spectator_timer = 78, h_spectator_timer_border, h_spectator_time_tm, h_spectator_time_mm, diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 79c34d998..ea14ed783 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -892,37 +892,57 @@ void HUD_SpectatorStatsSetup(edict_t *clent) // All elements are anchored from the gray bar at the bottom x = -160; y = -60; + int bar_width = 305; + int bar_height = 12; h = h_base; // gray bar hud[h] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetSize(clent, hud[h], 288, 12); + Ghud_SetSize(clent, hud[h], bar_width, bar_height); Ghud_SetFlags(clent, hud[h], GHF_HIDE); //Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); h = h_base + 1; // frags hud[h] = Ghud_AddText(clent, x + 500, y + 200, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 33, y + 2); + Ghud_SetPosition(clent, hud[h], x + 16, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 2; // deaths hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 97, y + 2); + Ghud_SetPosition(clent, hud[h], x + 72, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 3; // damage hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 160, y + 2); + Ghud_SetPosition(clent, hud[h], x + 120, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 4; // accuracy hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); - Ghud_SetPosition(clent, hud[h], x + 192, y + 2); + Ghud_SetPosition(clent, hud[h], x + 184, y + 2); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 5; // accuracy awards + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 248, y + 2); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 6; // impressive awards + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 273, y + 2); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 7; // excellent awards + hud[h] = Ghud_AddText(clent, x, y, ""); + Ghud_SetAnchor(clent, hud[h], 0.5, 1); + Ghud_SetPosition(clent, hud[h], x + 296, y + 2); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); // h = h_base + 10; // weapon selection @@ -934,7 +954,7 @@ void HUD_SpectatorStatsSetup(edict_t *clent) hud[j] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetAnchor(clent, hud[j], 0.5, 1); Ghud_SetPosition(clent, hud[j], x, y - 24); - Ghud_SetSize(clent, hud[j], 288, 12); + Ghud_SetSize(clent, hud[j], bar_width, bar_height); //Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[j], GHF_HIDE); @@ -949,7 +969,7 @@ void HUD_SpectatorStatsSetup(edict_t *clent) hud[k] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetAnchor(clent, hud[k], 0.5, 1); Ghud_SetPosition(clent, hud[k], x, y - 12); - Ghud_SetSize(clent, hud[k], 288, 12); + Ghud_SetSize(clent, hud[k], bar_width, bar_height); //Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[k], GHF_HIDE); @@ -1323,6 +1343,9 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetFlags(clent, hud[h_spectator_stats + 2], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 3], 0); Ghud_SetFlags(clent, hud[h_spectator_stats + 4], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 5], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 6], 0); + Ghud_SetFlags(clent, hud[h_spectator_stats + 7], 0); Ghud_SetFlags(clent, hud[h_spectator_name_bar], 0); Ghud_SetFlags(clent, hud[h_spectator_name_bar + 1], 0); Ghud_SetFlags(clent, hud[h_spectator_stats_bar], 0); @@ -1333,12 +1356,18 @@ void HUD_SpectatorUpdate(edict_t *clent) char deaths_s[24]; char dmg_s[24]; char acc_s[10]; + char a_s[10]; + char i_s[10]; + char e_s[10]; edict_t *targ = clent->client->chase_target; memcpy(nm_s, targ->client->pers.netname, 16); snprintf(frags_s, sizeof(frags_s), "%i", targ->client->resp.kills); snprintf(deaths_s, sizeof(deaths_s), "%i", targ->client->resp.deaths); snprintf(dmg_s, sizeof(dmg_s), "%i", targ->client->resp.damage_dealt); snprintf(acc_s, sizeof(acc_s), "%.2f%%", CalculateAccuracy(targ)); + snprintf(a_s, sizeof(a_s), "%i", targ->client->resp.awardstats[ACCURACY]); + snprintf(i_s, sizeof(i_s), "%i", targ->client->resp.awardstats[IMPRESSIVE]); + snprintf(e_s, sizeof(e_s), "%i", targ->client->resp.awardstats[EXCELLENT]); // update fields // Change color based on team @@ -1371,12 +1400,15 @@ void HUD_SpectatorUpdate(edict_t *clent) // target name Ghud_SetText(clent, hud[h_nbar + 1], nm_s); // target stat bar - Ghud_SetText(clent, hud[h_sbar + 1], "Frags Deaths Damage Acc."); + Ghud_SetText(clent, hud[h_sbar + 1], "Frags Deaths Damage Acc. A I E"); // target stat values Ghud_SetText(clent, hud[h_base + 1], frags_s); Ghud_SetText(clent, hud[h_base + 2], deaths_s); Ghud_SetText(clent, hud[h_base + 3], dmg_s); Ghud_SetText(clent, hud[h_base + 4], acc_s); + Ghud_SetText(clent, hud[h_base + 5], a_s); + Ghud_SetText(clent, hud[h_base + 6], i_s); + Ghud_SetText(clent, hud[h_base + 7], e_s); // Color per team if (clent->client->chase_target) { @@ -1400,6 +1432,9 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetFlags(clent, hud[h_spectator_stats + 2], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_stats + 3], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_stats + 4], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 5], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 6], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_stats + 7], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_name_bar], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_name_bar + 1], GHF_HIDE); Ghud_SetFlags(clent, hud[h_spectator_stats_bar], GHF_HIDE); From d0d8c2e3b3648d98a087cebcdda0ac349ddd5a5a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 21:59:55 -0400 Subject: [PATCH 928/974] GHUD Player stat and timer Deathmatch compatible --- src/action/g_combat.c | 16 +- src/action/p_hud.c | 553 +++++++++++++++++++++--------------------- 2 files changed, 289 insertions(+), 280 deletions(-) diff --git a/src/action/g_combat.c b/src/action/g_combat.c index aaea333fa..4e0b8f5de 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -865,9 +865,11 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if (client && attacker->client) { if (!friendlyFire && !in_warmup) { - attacker->client->resp.damage_dealt += damage; - // Hit markers - attacker->client->damage_dealt += damage; + if (mod != MOD_TELEFRAG) { + attacker->client->resp.damage_dealt += damage; + // Hit markers + attacker->client->damage_dealt += damage; + } if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; } @@ -930,9 +932,11 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve if (attacker->client) { if (!friendlyFire && !in_warmup) { - attacker->client->resp.damage_dealt += damage; - // Hit markers - attacker->client->damage_dealt += damage; + if (mod != MOD_TELEFRAG) { + attacker->client->resp.damage_dealt += damage; + // Hit markers + attacker->client->damage_dealt += damage; + } // All normal weapon damage if (mod > 0 && mod < MAX_GUNSTAT) { attacker->client->resp.gunstats[mod].damage += damage; diff --git a/src/action/p_hud.c b/src/action/p_hud.c index ea14ed783..6b36a49a1 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1007,83 +1007,98 @@ void HUD_SpectatorSetup(edict_t *clent) int *hud = clent->client->resp.hud_items; int i; - if (teamplay->value && spectator_hud->value) - { // Left-side nameplates - for (i = 0; i < 6; i++) - { - int x, y; - int h_base = h_nameplate_l + (i * 5); - int h; - - x = 0; - y = 28 + (28 * i); - - h = h_base; // back bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 0, 0); - Ghud_SetSize(clent, hud[h], 144, 24); - Ghud_SetColor(clent, hud[h], alt_red_team_red, alt_red_team_green, alt_red_team_blue, nameplate_alpha); - - h = h_base + 1; // health bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 0, 0); - Ghud_SetSize(clent, hud[h], 0, 24); - Ghud_SetColor(clent, hud[h], red_team_red, red_team_green, red_team_blue, nameplate_alpha); - - h = h_base + 2; // name - hud[h] = Ghud_AddText(clent, x + 142, y + 3, ""); - Ghud_SetAnchor(clent, hud[h], 0, 0); - Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); - - h = h_base + 3; // k/d - hud[h] = Ghud_AddText(clent, x + 142, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 0, 0); - Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); - - h = h_base + 4; // weapon select - hud[h] = Ghud_AddIcon(clent, x + 2, y + 2, level.pic_items[M4_NUM], 20, 20); - Ghud_SetAnchor(clent, hud[h], 0, 0); - } + if (spectator_hud->value) { + // Left-side nameplates + if (teamplay->value) { + for (i = 0; i < 6; i++) + { + int x, y; + int h_base = h_nameplate_l + (i * 5); + int h; + + x = 0; + y = 28 + (28 * i); + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetSize(clent, hud[h], 144, 24); + Ghud_SetColor(clent, hud[h], alt_red_team_red, alt_red_team_green, alt_red_team_blue, nameplate_alpha); + + h = h_base + 1; // health bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetSize(clent, hud[h], 0, 24); + Ghud_SetColor(clent, hud[h], red_team_red, red_team_green, red_team_blue, nameplate_alpha); + + h = h_base + 2; // name + hud[h] = Ghud_AddText(clent, x + 142, y + 3, ""); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); + + h = h_base + 3; // k/d + hud[h] = Ghud_AddText(clent, x + 142, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 0, 0); + Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); + + h = h_base + 4; // weapon select + hud[h] = Ghud_AddIcon(clent, x + 2, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 0, 0); + } - // Right side nameplates - for (i = 0; i < 6; i++) - { - int x, y; - int h_base = h_nameplate_r + (i * 5); - int h; - - x = -144; - y = 28 + (28 * i); - - h = h_base; // back bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 144, 24); - Ghud_SetColor(clent, hud[h], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, nameplate_alpha); - - h = h_base + 1; // health bar - hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetSize(clent, hud[h], 0, 24); - Ghud_SetColor(clent, hud[h], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); - - h = h_base + 2; // name - hud[h] = Ghud_AddText(clent, x + 2, y + 3, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 3; // k/d - hud[h] = Ghud_AddText(clent, x + 2, y + 14, ""); - Ghud_SetAnchor(clent, hud[h], 1, 0); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); - - h = h_base + 4; // weapon select - hud[h] = Ghud_AddIcon(clent, x + 122, y + 2, level.pic_items[M4_NUM], 20, 20); - Ghud_SetAnchor(clent, hud[h], 1, 0); + // Right side nameplates + for (i = 0; i < 6; i++) + { + int x, y; + int h_base = h_nameplate_r + (i * 5); + int h; + + x = -144; + y = 28 + (28 * i); + + h = h_base; // back bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 144, 24); + Ghud_SetColor(clent, hud[h], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, nameplate_alpha); + + h = h_base + 1; // health bar + hud[h] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetSize(clent, hud[h], 0, 24); + Ghud_SetColor(clent, hud[h], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); + + h = h_base + 2; // name + hud[h] = Ghud_AddText(clent, x + 2, y + 3, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 3; // k/d + hud[h] = Ghud_AddText(clent, x + 2, y + 14, ""); + Ghud_SetAnchor(clent, hud[h], 1, 0); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + + h = h_base + 4; // weapon select + hud[h] = Ghud_AddIcon(clent, x + 122, y + 2, level.pic_items[M4_NUM], 20, 20); + Ghud_SetAnchor(clent, hud[h], 1, 0); + } + + // GHUD top corner team icon + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); + hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); + Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); + + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); + hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); + Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); } // GHUD chase player stats @@ -1091,20 +1106,6 @@ void HUD_SpectatorSetup(edict_t *clent) // GHUD top middle time display HUD_SpectatorTimerSetup(clent); - - // GHUD top corner team icon - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); - hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); - Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); - Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); - - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); - hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); - } } @@ -1112,221 +1113,221 @@ void HUD_SpectatorUpdate(edict_t *clent) { int i; - if (teamplay->value && spectator_hud->value) - { + if (spectator_hud->value) { int *hud = clent->client->resp.hud_items; - - if (!(clent->client->pers.spec_flags & SPECFL_SPECHUD_NEW)) // hide all elements since client doesn't want them - { - for (i = 0; i <= h_spectator_time_ss; i++) + if (teamplay->value) { + if (!(clent->client->pers.spec_flags & SPECFL_SPECHUD_NEW)) // hide all elements since client doesn't want them { - Ghud_SetFlags(clent, hud[i], GHF_HIDE); + for (i = 0; i <= h_spectator_time_ss; i++) + { + Ghud_SetFlags(clent, hud[i], GHF_HIDE); + } + return; } - return; - } - gclient_t *team1_players[6]; - gclient_t *team2_players[6]; - gclient_t *sortedClients[MAX_CLIENTS]; + gclient_t *team1_players[6]; + gclient_t *team2_players[6]; + gclient_t *sortedClients[MAX_CLIENTS]; - int totalClients, team1Clients, team2Clients; + int totalClients, team1Clients, team2Clients; - memset(team1_players, 0, sizeof(team1_players)); - memset(team2_players, 0, sizeof(team2_players)); + memset(team1_players, 0, sizeof(team1_players)); + memset(team2_players, 0, sizeof(team2_players)); - team1Clients = 0; - team2Clients = 0; - totalClients = G_SortedClients(sortedClients); + team1Clients = 0; + team2Clients = 0; + totalClients = G_SortedClients(sortedClients); - for (i = 0; i < totalClients; i++) - { - gclient_t *cl = sortedClients[i]; - - if (!cl->resp.team) - continue; - - if (cl->resp.subteam) - continue; - - if (cl->resp.team == TEAM1) + for (i = 0; i < totalClients; i++) { - if (team1Clients >= 6) - continue; + gclient_t *cl = sortedClients[i]; - team1_players[team1Clients] = cl; - team1Clients++; - continue; - } - if (cl->resp.team == TEAM2) - { - if (team2Clients >= 6) + if (!cl->resp.team) continue; - team2_players[team2Clients] = cl; - team2Clients++; - continue; - } - } + if (cl->resp.subteam) + continue; - // team 1 (red team) - Ghud_SetFlags(clent, hud[h_team_l], 0); - Ghud_SetFlags(clent, hud[h_team_l_num], 0); - Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); + if (cl->resp.team == TEAM1) + { + if (team1Clients >= 6) + continue; - for (i = 0; i < 6; i++) - { - int x, y; - int h = h_nameplate_l + (i * 5); - gclient_t *cl = team1_players[i]; + team1_players[team1Clients] = cl; + team1Clients++; + continue; + } + if (cl->resp.team == TEAM2) + { + if (team2Clients >= 6) + continue; - if (!cl) // if there is no player, hide all the elements for their plate - { - Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); - continue; + team2_players[team2Clients] = cl; + team2Clients++; + continue; + } } - x = 0; - y = 28 + (28 * i); - - // unhide our elements - Ghud_SetFlags(clent, hud[h + 0], 0); - Ghud_SetFlags(clent, hud[h + 1], 0); - Ghud_SetFlags(clent, hud[h + 2], 0); - Ghud_SetFlags(clent, hud[h + 3], 0); - Ghud_SetFlags(clent, hud[h + 4], 0); + // team 1 (red team) + Ghud_SetFlags(clent, hud[h_team_l], 0); + Ghud_SetFlags(clent, hud[h_team_l_num], 0); + Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); - // tint for deadness - edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); - if (!IS_ALIVE(cl_ent)) + for (i = 0; i < 6; i++) { - Ghud_SetSize(clent, hud[h + 1], 0, 24); - - Ghud_SetPosition(clent, hud[h + 0], x, y); - Ghud_SetSize(clent, hud[h + 0], 144, 24); - } - else - { - float hp_invfrac; - float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); - hp_invfrac = floorf((1 - hp_frac) * 144) / 144; - hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps - - Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); - Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); - Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); - } - - // generate strings - char nm_s[17]; - char kdr_s[24]; - memcpy(nm_s, cl->pers.netname, 16); - if (IS_ALIVE(cl_ent)) - snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); - else - snprintf(kdr_s, sizeof(kdr_s), "(%i)%c %5i", cl->resp.deaths, 06, cl->resp.kills); - - nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated - kdr_s[23] = 0; - - // update fields - Ghud_SetText(clent, hud[h + 2], nm_s); - Ghud_SetText(clent, hud[h + 3], kdr_s); + int x, y; + int h = h_nameplate_l + (i * 5); + gclient_t *cl = team1_players[i]; + + if (!cl) // if there is no player, hide all the elements for their plate + { + Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); + continue; + } - int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + x = 0; + y = 28 + (28 * i); - if (IS_ALIVE(cl_ent) && cl->curr_weap) - Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); - else if (cl->resp.team == TEAM1 && weapNum) - Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); - else // no weapon, set to mk23 - Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); - } + // unhide our elements + Ghud_SetFlags(clent, hud[h + 0], 0); + Ghud_SetFlags(clent, hud[h + 1], 0); + Ghud_SetFlags(clent, hud[h + 2], 0); + Ghud_SetFlags(clent, hud[h + 3], 0); + Ghud_SetFlags(clent, hud[h + 4], 0); + // tint for deadness + edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); + if (!IS_ALIVE(cl_ent)) + { + Ghud_SetSize(clent, hud[h + 1], 0, 24); - // team 2 (blue team) - Ghud_SetFlags(clent, hud[h_team_r], 0); - Ghud_SetFlags(clent, hud[h_team_r_num], 0); - Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); - if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes - Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); - else - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - - for (i = 0; i < 6; i++) - { - int x, y; - int h = h_nameplate_r + (i * 5); - gclient_t *cl = team2_players[i]; + Ghud_SetPosition(clent, hud[h + 0], x, y); + Ghud_SetSize(clent, hud[h + 0], 144, 24); + } + else + { + float hp_invfrac; + float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); + hp_invfrac = floorf((1 - hp_frac) * 144) / 144; + hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps + + Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); + Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); + Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); + } - if (!cl) // if there is no player, hide all the elements for their plate - { - Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); - Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); - continue; + // generate strings + char nm_s[17]; + char kdr_s[24]; + memcpy(nm_s, cl->pers.netname, 16); + if (IS_ALIVE(cl_ent)) + snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); + else + snprintf(kdr_s, sizeof(kdr_s), "(%i)%c %5i", cl->resp.deaths, 06, cl->resp.kills); + + nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated + kdr_s[23] = 0; + + // update fields + Ghud_SetText(clent, hud[h + 2], nm_s); + Ghud_SetText(clent, hud[h + 3], kdr_s); + + int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + + if (IS_ALIVE(cl_ent) && cl->curr_weap) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); + else if (cl->resp.team == TEAM1 && weapNum) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); + else // no weapon, set to mk23 + Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } - x = -144; - y = 28 + (28 * i); - - // unhide our elements - Ghud_SetFlags(clent, hud[h + 0], 0); - Ghud_SetFlags(clent, hud[h + 1], 0); - Ghud_SetFlags(clent, hud[h + 2], 0); - Ghud_SetFlags(clent, hud[h + 3], 0); - Ghud_SetFlags(clent, hud[h + 4], 0); - // tint for deadness - edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); - if (!IS_ALIVE(cl_ent)) - { - Ghud_SetSize(clent, hud[h + 1], 0, 24); - Ghud_SetSize(clent, hud[h + 0], 144, 24); - //Ghud_SetPosition(clent, hud[h + 0], x, y); - } - else - { - float hp_invfrac; - float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); - hp_invfrac = floorf((1 - hp_frac) * 144) / 144; - hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps - - Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); - Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); - Ghud_SetPosition(clent, hud[h + 1], x + (144 * hp_invfrac), y); - //Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); - } - - // generate strings - char nm_s[17]; - char kdr_s[24]; - memcpy(nm_s, cl->pers.netname, 16); - if (IS_ALIVE(cl_ent)) - snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); + // team 2 (blue team) + Ghud_SetFlags(clent, hud[h_team_r], 0); + Ghud_SetFlags(clent, hud[h_team_r_num], 0); + Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); + if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes + Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); else - snprintf(kdr_s, sizeof(kdr_s), "%-5i %c(%i)", cl->resp.kills, 06, cl->resp.deaths); + Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated - kdr_s[23] = 0; - - // update fields - Ghud_SetText(clent, hud[h + 2], nm_s); - Ghud_SetText(clent, hud[h + 3], kdr_s); + for (i = 0; i < 6; i++) + { + int x, y; + int h = h_nameplate_r + (i * 5); + gclient_t *cl = team2_players[i]; + + if (!cl) // if there is no player, hide all the elements for their plate + { + Ghud_SetFlags(clent, hud[h + 0], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 1], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 2], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 3], GHF_HIDE); + Ghud_SetFlags(clent, hud[h + 4], GHF_HIDE); + continue; + } - int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + x = -144; + y = 28 + (28 * i); + + // unhide our elements + Ghud_SetFlags(clent, hud[h + 0], 0); + Ghud_SetFlags(clent, hud[h + 1], 0); + Ghud_SetFlags(clent, hud[h + 2], 0); + Ghud_SetFlags(clent, hud[h + 3], 0); + Ghud_SetFlags(clent, hud[h + 4], 0); + + // tint for deadness + edict_t *cl_ent = g_edicts + 1 + (cl - game.clients); + if (!IS_ALIVE(cl_ent)) + { + Ghud_SetSize(clent, hud[h + 1], 0, 24); + Ghud_SetSize(clent, hud[h + 0], 144, 24); + //Ghud_SetPosition(clent, hud[h + 0], x, y); + } + else + { + float hp_invfrac; + float hp_frac = bound(0, ((float)cl_ent->health / 100.0f), 1); + hp_invfrac = floorf((1 - hp_frac) * 144) / 144; + hp_frac = ceilf(hp_frac * 144) / 144; // round so we don't get gaps + + Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); + Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); + Ghud_SetPosition(clent, hud[h + 1], x + (144 * hp_invfrac), y); + //Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); + } - if (IS_ALIVE(cl_ent) && cl->curr_weap) - Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); - else if (cl->resp.team == TEAM2 && weapNum) - Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); - else // no weapon, set to mk23 - Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); + // generate strings + char nm_s[17]; + char kdr_s[24]; + memcpy(nm_s, cl->pers.netname, 16); + if (IS_ALIVE(cl_ent)) + snprintf(kdr_s, sizeof(kdr_s), "%i", cl->resp.kills); + else + snprintf(kdr_s, sizeof(kdr_s), "%-5i %c(%i)", cl->resp.kills, 06, cl->resp.deaths); + + nm_s[sizeof(nm_s) - 1] = 0; // make sure strings are terminated + kdr_s[23] = 0; + + // update fields + Ghud_SetText(clent, hud[h + 2], nm_s); + Ghud_SetText(clent, hud[h + 3], kdr_s); + + int weapNum = cl->pers.chosenWeapon ? cl->pers.chosenWeapon->typeNum : 0; + + if (IS_ALIVE(cl_ent) && cl->curr_weap) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[cl->curr_weap]); + else if (cl->resp.team == TEAM2 && weapNum) + Ghud_SetInt(clent, hud[h + 4], level.pic_items[weapNum]); + else // no weapon, set to mk23 + Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); + } } int h_base = h_spectator_stats; @@ -1424,6 +1425,10 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetColor(clent, hud[h_nbar], green_team_red, green_team_green, green_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_green_team_red, alt_green_team_green, alt_green_team_blue, 255); Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); + } else if (targ->client->resp.team == 0) { // Deathmatch! + Ghud_SetColor(clent, hud[h_nbar], 220, 220, 220, nameplate_alpha); + Ghud_SetColor(clent, hud[h_sbar], 110, 110, 110, 255); + Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); } } } else { // Hide the stats if we're not chasing a target From ccfe259351ec098dda1e2ffddaf73945277a22d4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 22:00:16 -0400 Subject: [PATCH 929/974] Removed MOD_TELEFRAG from recording 100000 damage --- src/action/p_hud.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 6b36a49a1..5e5293344 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1112,7 +1112,6 @@ void HUD_SpectatorSetup(edict_t *clent) void HUD_SpectatorUpdate(edict_t *clent) { int i; - if (spectator_hud->value) { int *hud = clent->client->resp.hud_items; if (teamplay->value) { From 6eef5141b27cb2e3874e3d7853a05d47bd4cc39d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 22:27:15 -0400 Subject: [PATCH 930/974] Set timer values to 0 if negative (overtime) --- src/action/p_hud.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 5e5293344..4c90f69a3 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -788,6 +788,14 @@ static int GetRemainingTimeDigits(hud_time_digits timeval) rmins = remaining / 60; rsecs = remaining % 60; + // Ensure rmins and rsecs are non-negative + if (rmins < 0) { + rmins = 0; + } + if (rsecs < 0) { + rsecs = 0; + } + switch (timeval) { case tm: // Tens of minutes return (rmins / 10) % 10; @@ -798,7 +806,7 @@ static int GetRemainingTimeDigits(hud_time_digits timeval) case ss: // Seconds return rsecs % 10; default: - return -1; // Invalid timeval + return 0; // Invalid timeval } } @@ -931,19 +939,19 @@ void HUD_SpectatorStatsSetup(edict_t *clent) hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 248, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); h = h_base + 6; // impressive awards hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 273, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); h = h_base + 7; // excellent awards hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 296, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); // h = h_base + 10; // weapon selection // Ghud_SetAnchor(clent, hud[h], 0.5, 1); @@ -1445,8 +1453,8 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetFlags(clent, hud[h_spectator_stats_bar + 1], GHF_HIDE); } - // Update the timer display - if (timelimit->value) { + // Update the timer display if set and we're not in intermission + if (timelimit->value && !level.intermission_framenum) { HUD_UpdateSpectatorTimer(clent); } else { Ghud_SetFlags(clent, hud[h_spectator_timer_border], GHF_HIDE); From 468ae28dde960697a6de6eddff3c39d83291472a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 31 Oct 2024 23:20:33 -0400 Subject: [PATCH 931/974] Some cleanup, darkened gray bar --- src/action/p_hud.c | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 4c90f69a3..9a7ab6845 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -909,7 +909,6 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetPosition(clent, hud[h], x, y); Ghud_SetSize(clent, hud[h], bar_width, bar_height); Ghud_SetFlags(clent, hud[h], GHF_HIDE); - //Ghud_SetColor(clent, hud[h], 0, 0, 0, 0); h = h_base + 1; // frags hud[h] = Ghud_AddText(clent, x + 500, y + 200, ""); @@ -939,19 +938,19 @@ void HUD_SpectatorStatsSetup(edict_t *clent) hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 248, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 6; // impressive awards hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 273, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 7; // excellent awards hud[h] = Ghud_AddText(clent, x, y, ""); Ghud_SetAnchor(clent, hud[h], 0.5, 1); Ghud_SetPosition(clent, hud[h], x + 296, y + 2); - Ghud_SetTextFlags(clent, hud[h], UI_LEFT | UI_ALTCOLOR); + Ghud_SetTextFlags(clent, hud[h], UI_LEFT); // h = h_base + 10; // weapon selection // Ghud_SetAnchor(clent, hud[h], 0.5, 1); @@ -963,7 +962,6 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[j], 0.5, 1); Ghud_SetPosition(clent, hud[j], x, y - 24); Ghud_SetSize(clent, hud[j], bar_width, bar_height); - //Ghud_SetColor(clent, hud[j], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[j], GHF_HIDE); j = h_nbar + 1; // name print @@ -978,7 +976,6 @@ void HUD_SpectatorStatsSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[k], 0.5, 1); Ghud_SetPosition(clent, hud[k], x, y - 12); Ghud_SetSize(clent, hud[k], bar_width, bar_height); - //Ghud_SetColor(clent, hud[k], 0, 0, 0, 0); Ghud_SetFlags(clent, hud[k], GHF_HIDE); k = h_sbar + 1; // stat table text @@ -1423,15 +1420,15 @@ void HUD_SpectatorUpdate(edict_t *clent) if (targ->client->resp.team == TEAM1) { Ghud_SetColor(clent, hud[h_nbar], red_team_red, red_team_green, red_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_red_team_red, alt_red_team_green, alt_red_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); + Ghud_SetColor(clent, hud[h_base], 100, 100, 100, 255); } else if (targ->client->resp.team == TEAM2) { Ghud_SetColor(clent, hud[h_nbar], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); + Ghud_SetColor(clent, hud[h_base], 100, 100, 100, 255); } else if (targ->client->resp.team == TEAM3) { Ghud_SetColor(clent, hud[h_nbar], green_team_red, green_team_green, green_team_blue, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], alt_green_team_red, alt_green_team_green, alt_green_team_blue, 255); - Ghud_SetColor(clent, hud[h_base], 150, 150, 150, 255); + Ghud_SetColor(clent, hud[h_base], 100, 100, 100, 255); } else if (targ->client->resp.team == 0) { // Deathmatch! Ghud_SetColor(clent, hud[h_nbar], 220, 220, 220, nameplate_alpha); Ghud_SetColor(clent, hud[h_sbar], 110, 110, 110, 255); From f5d67a0a075f22356992d4ee915dc2e99ab2377b Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 1 Nov 2024 00:07:28 -0400 Subject: [PATCH 932/974] A/I/E icons --- src/action/p_hud.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 9a7ab6845..124d17dc2 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1405,7 +1405,7 @@ void HUD_SpectatorUpdate(edict_t *clent) // target name Ghud_SetText(clent, hud[h_nbar + 1], nm_s); // target stat bar - Ghud_SetText(clent, hud[h_sbar + 1], "Frags Deaths Damage Acc. A I E"); + Ghud_SetText(clent, hud[h_sbar + 1], "Frags Deaths Damage Acc. \x07 \x0E \x0F"); // target stat values Ghud_SetText(clent, hud[h_base + 1], frags_s); Ghud_SetText(clent, hud[h_base + 2], deaths_s); From 577621bf14f2c660c2a86c21f830552db061eff1 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Nov 2024 09:01:43 -0400 Subject: [PATCH 933/974] Unhide timer elements --- src/action/p_hud.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 124d17dc2..2b8d39cd7 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -840,6 +840,15 @@ void HUD_SpectatorTimerSetup(edict_t *clent) if (timelimit->value) { // GHUD top middle time display clent->client->resp.hud_type = 1; + + // Unhide the other HUD elements + Ghud_SetFlags(clent, hud[h_spectator_timer_border], 0); + Ghud_SetFlags(clent, hud[h_spectator_timer], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_tm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_sep], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], 0); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], 0); // GHUD bottom center stat display int x, y; From 8cd7596193f6900c53a24c9a8af8347e18bd48d8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Nov 2024 09:15:49 -0400 Subject: [PATCH 934/974] Re-draw the GHUD if going spectator --- src/action/p_client.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/action/p_client.c b/src/action/p_client.c index 018c43368..40d811e48 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -6275,6 +6275,10 @@ void ClientBeginServerFrame(edict_t * ent) // then Action! EspRespawnPlayer(ent); } + #ifdef AQTION_EXTENSION + // Redraw GHUD + HUD_SetType(ent, 1); + #endif } else { From 0b80b9e08db7c0e794c735ea16e5dc99f8e2032e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Nov 2024 09:59:33 -0400 Subject: [PATCH 935/974] Support for CTF scores in GHUD --- src/action/p_hud.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 2b8d39cd7..7bb0f8764 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1184,7 +1184,10 @@ void HUD_SpectatorUpdate(edict_t *clent) // team 1 (red team) Ghud_SetFlags(clent, hud[h_team_l], 0); Ghud_SetFlags(clent, hud[h_team_l_num], 0); - Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); + if (ctf->value) + Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team1); + else + Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); for (i = 0; i < 6; i++) { @@ -1263,7 +1266,11 @@ void HUD_SpectatorUpdate(edict_t *clent) // team 2 (blue team) Ghud_SetFlags(clent, hud[h_team_r], 0); Ghud_SetFlags(clent, hud[h_team_r_num], 0); - Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); + if (ctf->value) + Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team2); + else + Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM2].score); + if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); else From 8b9011e18bd1be414e2a2689f331b80d809c261b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 1 Nov 2024 15:33:01 -0400 Subject: [PATCH 936/974] Moved HUD score a bit, CTF/Esp team logos render in GHUD now --- src/action/a_ctf.c | 3 +++ src/action/g_spawn.c | 11 +++++++++-- src/action/p_hud.c | 19 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index e4ac18d7f..e2c6fd4da 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -1486,6 +1486,9 @@ void CTFCapReward(edict_t * ent) void CTFSetupStatusbar( void ) { + // Frags closer to the team scores + Q_strncatz(level.statusbar, "xr -76 yb -188 num 3 14 ", sizeof(level.statusbar)); + Q_strncatz(level.statusbar, // Red Team "yb -164 " "if 24 " "xr -24 " "pic 24 " "endif " "xr -60 " "num 2 26 " diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index a7690ae5c..156287163 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1710,8 +1710,15 @@ void G_SetupStatusbar( void ) { Q_strncpyz(level.statusbar, STATBAR_COMMON, sizeof(level.statusbar)); - if(!((noscore->value || hud_noscore->value) && teamplay->value)) // frags - Q_strncatz(level.statusbar, "xr -50 yt 2 num 3 14 ", sizeof(level.statusbar)); + // if(!((noscore->value || hud_noscore->value) && teamplay->value) && !ctf->value) // frags + // Q_strncatz(level.statusbar, "xr -50 yt 2 num 3 14 ", sizeof(level.statusbar)); + + // Display frags in top-right corner if teamplay, but not in CTF (moved that to CTFSetupStatusbar()) + if (!(noscore->value || hud_noscore->value) || !teamplay->value) { + if (!ctf->value) { + Q_strncatz(level.statusbar, "xr -50 yt 2 num 3 14 ", sizeof(level.statusbar)); + } + } if (ctf->value) CTFSetupStatusbar(); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 7bb0f8764..5e0be8df7 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -1101,14 +1101,27 @@ void HUD_SpectatorSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h], 1, 0); } - // GHUD top corner team icon - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[1], 24, 24); + // GHUD top corner team icons + + // Team 1 + if (ctf->value) // CTF + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_ctf_flagbase[TEAM1], 24, 24); + else if (esp->value) // Espionage + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_esp_teamicon[TEAM1], 24, 24); + else // Teamplay/Matchmode/Domination + hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[TEAM1], 24, 24); Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[2], 24, 24); + // Team 2 + if (ctf->value) // CTF + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_ctf_flagbase[TEAM2], 24, 24); + else if (esp->value) // Espionage + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_esp_teamicon[TEAM2], 24, 24); + else // Teamplay/Matchmode/Domination + hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[TEAM2], 24, 24); Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); From 12536545bc30f934477077445099320e6d1d135b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 6 Nov 2024 16:45:08 -0500 Subject: [PATCH 937/974] Team scores moved to center, matchmode has its own format --- src/action/p_hud.c | 272 ++++++++++++++++++++++++--------------------- 1 file changed, 143 insertions(+), 129 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 5e0be8df7..1eb8926c3 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -51,6 +51,29 @@ #include "g_local.h" +// Red team colors +int red_team_red = 220; +int red_team_green = 60; +int red_team_blue = 60; +int alt_red_team_red = 110; +int alt_red_team_green = 45; +int alt_red_team_blue = 45; + +// Blue team colors +int blue_team_red = 40; +int blue_team_green = 40; +int blue_team_blue = 220; +int alt_blue_team_red = 30; +int alt_blue_team_green = 60; +int alt_blue_team_blue = 110; + +// Green team colors +int green_team_red = 40; +int green_team_green = 220; +int green_team_blue = 40; +int alt_green_team_red = 30; +int alt_green_team_green = 140; +int alt_green_team_blue = 30; /* ====================================================================== @@ -827,10 +850,29 @@ static void HUD_UpdateSpectatorTimer(edict_t *clent) // Change bar color to orange Ghud_SetColor(clent, hud[h_spectator_timer_border], 255, 165, 0, 120); } else { - // Change bar color to green - Ghud_SetColor(clent, hud[h_spectator_timer_border], 20, 20, 20, 255); + // Change bar color to gray + Ghud_SetColor(clent, hud[h_spectator_timer_border], 20, 20, 20, 120); } - + + // Score bug update + + // team 1 (red team) + Ghud_SetFlags(clent, hud[h_team_l], 0); + Ghud_SetFlags(clent, hud[h_team_l_num], 0); + if (ctf->value) + Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team1); + else + //Ghud_SetInt(clent, hud[h_team_l_num], 13); + Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); + + // team 2 (blue team) + Ghud_SetFlags(clent, hud[h_team_r], 0); + Ghud_SetFlags(clent, hud[h_team_r_num], 0); + if (ctf->value) + Ghud_SetInt(clent, hud[h_team_r_num], ctfgame.team2); + else + //Ghud_SetInt(clent, hud[h_team_r_num], 25); + Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); } void HUD_SpectatorTimerSetup(edict_t *clent) @@ -853,30 +895,30 @@ void HUD_SpectatorTimerSetup(edict_t *clent) // GHUD bottom center stat display int x, y; x = 0; - y = 0; + y = 30; hud[h_spectator_timer_border] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetAnchor(clent, hud[h_spectator_timer_border], 0.5, 0); - Ghud_SetPosition(clent, hud[h_spectator_timer_border], x - 51, y); - Ghud_SetSize(clent, hud[h_spectator_timer_border], 102, 31); + Ghud_SetPosition(clent, hud[h_spectator_timer_border], x - 66, y - 1); + Ghud_SetSize(clent, hud[h_spectator_timer_border], 130, 56); Ghud_SetColor(clent, hud[h_spectator_timer_border], 50, 150, 50, 120); hud[h_spectator_timer] = Ghud_NewElement(clent, GHT_FILL); Ghud_SetAnchor(clent, hud[h_spectator_timer], 0.5, 0); - Ghud_SetPosition(clent, hud[h_spectator_timer], x - 50, y); - Ghud_SetSize(clent, hud[h_spectator_timer], 100, 30); - Ghud_SetColor(clent, hud[h_spectator_timer], 50, 50, 50, 255); + Ghud_SetPosition(clent, hud[h_spectator_timer], x - 65, y); + Ghud_SetSize(clent, hud[h_spectator_timer], 128, 54); + Ghud_SetColor(clent, hud[h_spectator_timer], 50, 50, 50, 120); // Add number elements for minutes - hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x - 50, 2, 0); - hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x - 30, 2, 0); + hud[h_spectator_time_tm] = Ghud_AddNumber(clent, x - 50, y, 0); + hud[h_spectator_time_mm] = Ghud_AddNumber(clent, x - 30, y, 0); // Draw timer seperator hud[h_spectator_time_sep] = Ghud_AddText(clent, x - 5, y + 12, ":"); // Add number elements for seconds - hud[h_spectator_time_ts] = Ghud_AddNumber(clent, x + 10, 2, 0); - hud[h_spectator_time_ss] = Ghud_AddNumber(clent, x + 30, 2, 0); + hud[h_spectator_time_ts] = Ghud_AddNumber(clent, x + 10, y, 0); + hud[h_spectator_time_ss] = Ghud_AddNumber(clent, x + 30, y, 0); // Anchor everything to the top middle Ghud_SetAnchor(clent, hud[h_spectator_time_tm], 0.5, 0); @@ -885,12 +927,68 @@ void HUD_SpectatorTimerSetup(edict_t *clent) Ghud_SetAnchor(clent, hud[h_spectator_time_ts], 0.5, 0); Ghud_SetAnchor(clent, hud[h_spectator_time_ss], 0.5, 0); - } else { - Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); - Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); - Ghud_SetFlags(clent, hud[h_spectator_time_sep], GHF_HIDE); - Ghud_SetFlags(clent, hud[h_spectator_time_ts], GHF_HIDE); - Ghud_SetFlags(clent, hud[h_spectator_time_ss], GHF_HIDE); + } else { + Ghud_SetFlags(clent, hud[h_spectator_time_tm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_mm], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_sep], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ts], GHF_HIDE); + Ghud_SetFlags(clent, hud[h_spectator_time_ss], GHF_HIDE); + } + + // GHUD team icons and scores + + // Team 1 + if (ctf->value) // CTF + hud[h_team_l] = Ghud_AddIcon(clent, 2, 28, level.pic_ctf_flagbase[TEAM1], 24, 24); + else if (esp->value) // Espionage + hud[h_team_l] = Ghud_AddIcon(clent, 2, 28, level.pic_esp_teamicon[TEAM1], 24, 24); + else if (matchmode->value) { // Matchmode + hud[h_team_l] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetAnchor(clent, hud[h_team_l], 0.5, 0); + Ghud_SetPosition(clent, hud[h_team_l], -65, 54); + Ghud_SetSize(clent, hud[h_team_l], 60, 5); + Ghud_SetColor(clent, hud[h_team_l], red_team_red, red_team_green, red_team_blue, 255); + //hud[h_team_l] = Ghud_AddIcon(clent, -70, 28, level.pic_teamskin[TEAM1], 24, 24); + } else // Teamplay/Domination + hud[h_team_l] = Ghud_AddIcon(clent, -30, 60, level.pic_teamskin[TEAM1], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_l], 0.5, 0); + hud[h_team_l_num] = Ghud_AddNumber(clent, -70, 60, 0); + Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_l_num], 0.5, 0); + Ghud_SetFlags(clent, hud[h_team_l_num], UI_RIGHT); + + // Team 2 + if (ctf->value) // CTF + hud[h_team_r] = Ghud_AddIcon(clent, -26, 28, level.pic_ctf_flagbase[TEAM2], 24, 24); + else if (esp->value) // Espionage + hud[h_team_r] = Ghud_AddIcon(clent, -26, 28, level.pic_esp_teamicon[TEAM2], 24, 24); + else if (matchmode->value) { // Matchmode + hud[h_team_r] = Ghud_NewElement(clent, GHT_FILL); + Ghud_SetAnchor(clent, hud[h_team_r], 0.5, 0); + Ghud_SetPosition(clent, hud[h_team_r], 2, 54); + Ghud_SetSize(clent, hud[h_team_r], 60, 5); + Ghud_SetColor(clent, hud[h_team_r], blue_team_red, blue_team_green, blue_team_blue, 255); + } else // Teamplay/Domination + hud[h_team_r] = Ghud_AddIcon(clent, 10, 60, level.pic_teamskin[TEAM2], 24, 24); + Ghud_SetAnchor(clent, hud[h_team_r], 0.5, 0); + + if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes + hud[h_team_r_num] = Ghud_AddNumber(clent, 35, 60, 0); + else + hud[h_team_r_num] = Ghud_AddNumber(clent, 0, 60, 0); + + Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); + Ghud_SetAnchor(clent, hud[h_team_r_num], 0.5, 0); + Ghud_SetFlags(clent, hud[h_team_r_num], UI_LEFT); + + if (matchmode->value) { // Move the scores closer together slightly + int t2_x = 0; + Ghud_SetPosition(clent, hud[h_team_l_num], -55, 60); + + if (teams[TEAM2].score >= 10) + Ghud_SetPosition(clent, hud[h_team_r_num], (t2_x + 20), 60); + else + Ghud_SetPosition(clent, hud[h_team_r_num], t2_x, 60); } } @@ -1001,22 +1099,6 @@ void HUD_SpectatorSetup(edict_t *clent) clent->client->resp.hud_type = 1; int nameplate_alpha = 230; - - // Red team colors - int red_team_red = 220; - int red_team_green = 60; - int red_team_blue = 60; - int alt_red_team_red = 110; - int alt_red_team_green = 45; - int alt_red_team_blue = 45; - - // Blue team colors - int blue_team_red = 40; - int blue_team_green = 80; - int blue_team_blue = 220; - int alt_blue_team_red = 30; - int alt_blue_team_green = 60; - int alt_blue_team_blue = 110; int *hud = clent->client->resp.hud_items; int i; @@ -1026,108 +1108,83 @@ void HUD_SpectatorSetup(edict_t *clent) if (teamplay->value) { for (i = 0; i < 6; i++) { - int x, y; + int x, y, yy; int h_base = h_nameplate_l + (i * 5); int h; x = 0; - y = 28 + (28 * i); + y = 56; + yy = y + (28 * i); h = h_base; // back bar hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetPosition(clent, hud[h], x, yy); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetSize(clent, hud[h], 144, 24); Ghud_SetColor(clent, hud[h], alt_red_team_red, alt_red_team_green, alt_red_team_blue, nameplate_alpha); h = h_base + 1; // health bar hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetPosition(clent, hud[h], x, yy); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetSize(clent, hud[h], 0, 24); Ghud_SetColor(clent, hud[h], red_team_red, red_team_green, red_team_blue, nameplate_alpha); h = h_base + 2; // name - hud[h] = Ghud_AddText(clent, x + 142, y + 3, ""); + hud[h] = Ghud_AddText(clent, x + 142, yy + 3, ""); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); h = h_base + 3; // k/d - hud[h] = Ghud_AddText(clent, x + 142, y + 14, ""); + hud[h] = Ghud_AddText(clent, x + 142, yy + 14, ""); Ghud_SetAnchor(clent, hud[h], 0, 0); Ghud_SetTextFlags(clent, hud[h], UI_RIGHT); h = h_base + 4; // weapon select - hud[h] = Ghud_AddIcon(clent, x + 2, y + 2, level.pic_items[M4_NUM], 20, 20); + hud[h] = Ghud_AddIcon(clent, x + 2, yy + 2, level.pic_items[M4_NUM], 20, 20); Ghud_SetAnchor(clent, hud[h], 0, 0); } // Right side nameplates for (i = 0; i < 6; i++) { - int x, y; + int x, y, yy; int h_base = h_nameplate_r + (i * 5); int h; x = -144; - y = 28 + (28 * i); + y = 56; + yy = y + (28 * i); h = h_base; // back bar hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetPosition(clent, hud[h], x, yy); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetSize(clent, hud[h], 144, 24); Ghud_SetColor(clent, hud[h], alt_blue_team_red, alt_blue_team_green, alt_blue_team_blue, nameplate_alpha); h = h_base + 1; // health bar hud[h] = Ghud_NewElement(clent, GHT_FILL); - Ghud_SetPosition(clent, hud[h], x, y); + Ghud_SetPosition(clent, hud[h], x, yy); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetSize(clent, hud[h], 0, 24); Ghud_SetColor(clent, hud[h], blue_team_red, blue_team_green, blue_team_blue, nameplate_alpha); h = h_base + 2; // name - hud[h] = Ghud_AddText(clent, x + 2, y + 3, ""); + hud[h] = Ghud_AddText(clent, x + 2, yy + 3, ""); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 3; // k/d - hud[h] = Ghud_AddText(clent, x + 2, y + 14, ""); + hud[h] = Ghud_AddText(clent, x + 2, yy + 14, ""); Ghud_SetAnchor(clent, hud[h], 1, 0); Ghud_SetTextFlags(clent, hud[h], UI_LEFT); h = h_base + 4; // weapon select - hud[h] = Ghud_AddIcon(clent, x + 122, y + 2, level.pic_items[M4_NUM], 20, 20); + hud[h] = Ghud_AddIcon(clent, x + 122, yy + 2, level.pic_items[M4_NUM], 20, 20); Ghud_SetAnchor(clent, hud[h], 1, 0); } - - // GHUD top corner team icons - - // Team 1 - if (ctf->value) // CTF - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_ctf_flagbase[TEAM1], 24, 24); - else if (esp->value) // Espionage - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_esp_teamicon[TEAM1], 24, 24); - else // Teamplay/Matchmode/Domination - hud[h_team_l] = Ghud_AddIcon(clent, 2, 2, level.pic_teamskin[TEAM1], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_l], 0, 0); - hud[h_team_l_num] = Ghud_AddNumber(clent, 96, 2, 0); - Ghud_SetSize(clent, hud[h_team_l_num], 2, 0); - Ghud_SetAnchor(clent, hud[h_team_l_num], 0, 0); - - // Team 2 - if (ctf->value) // CTF - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_ctf_flagbase[TEAM2], 24, 24); - else if (esp->value) // Espionage - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_esp_teamicon[TEAM2], 24, 24); - else // Teamplay/Matchmode/Domination - hud[h_team_r] = Ghud_AddIcon(clent, -26, 2, level.pic_teamskin[TEAM2], 24, 24); - Ghud_SetAnchor(clent, hud[h_team_r], 1, 0); - hud[h_team_r_num] = Ghud_AddNumber(clent, -128, 2, 0); - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - Ghud_SetAnchor(clent, hud[h_team_r_num], 1, 0); } - // GHUD chase player stats HUD_SpectatorStatsSetup(clent); @@ -1194,17 +1251,10 @@ void HUD_SpectatorUpdate(edict_t *clent) } } - // team 1 (red team) - Ghud_SetFlags(clent, hud[h_team_l], 0); - Ghud_SetFlags(clent, hud[h_team_l_num], 0); - if (ctf->value) - Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team1); - else - Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); - + for (i = 0; i < 6; i++) { - int x, y; + int x, y, yy; int h = h_nameplate_l + (i * 5); gclient_t *cl = team1_players[i]; @@ -1219,7 +1269,8 @@ void HUD_SpectatorUpdate(edict_t *clent) } x = 0; - y = 28 + (28 * i); + y = 56; + yy = y + (28 * i); // unhide our elements Ghud_SetFlags(clent, hud[h + 0], 0); @@ -1234,7 +1285,7 @@ void HUD_SpectatorUpdate(edict_t *clent) { Ghud_SetSize(clent, hud[h + 1], 0, 24); - Ghud_SetPosition(clent, hud[h + 0], x, y); + Ghud_SetPosition(clent, hud[h + 0], x, yy); Ghud_SetSize(clent, hud[h + 0], 144, 24); } else @@ -1246,7 +1297,7 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); - Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); + Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), yy); } // generate strings @@ -1275,23 +1326,9 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetInt(clent, hud[h + 4], level.pic_items[MK23_NUM]); } - - // team 2 (blue team) - Ghud_SetFlags(clent, hud[h_team_r], 0); - Ghud_SetFlags(clent, hud[h_team_r_num], 0); - if (ctf->value) - Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team2); - else - Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM2].score); - - if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes - Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); - else - Ghud_SetSize(clent, hud[h_team_r_num], 1, 0); - for (i = 0; i < 6; i++) { - int x, y; + int x, y, yy; int h = h_nameplate_r + (i * 5); gclient_t *cl = team2_players[i]; @@ -1306,7 +1343,8 @@ void HUD_SpectatorUpdate(edict_t *clent) } x = -144; - y = 28 + (28 * i); + y = 56; + yy = y + (28 * i); // unhide our elements Ghud_SetFlags(clent, hud[h + 0], 0); @@ -1332,7 +1370,7 @@ void HUD_SpectatorUpdate(edict_t *clent) Ghud_SetSize(clent, hud[h + 1], 144 * hp_frac, 24); Ghud_SetSize(clent, hud[h + 0], 144 * hp_invfrac, 24); - Ghud_SetPosition(clent, hud[h + 1], x + (144 * hp_invfrac), y); + Ghud_SetPosition(clent, hud[h + 1], x + (144 * hp_invfrac), yy); //Ghud_SetPosition(clent, hud[h + 0], x + (144 * (hp_frac)), y); } @@ -1407,30 +1445,6 @@ void HUD_SpectatorUpdate(edict_t *clent) // Change color based on team int nameplate_alpha = 180; - // Red team colors - int red_team_red = 220; - int red_team_green = 60; - int red_team_blue = 60; - int alt_red_team_red = 110; - int alt_red_team_green = 45; - int alt_red_team_blue = 45; - - // Blue team colors - int blue_team_red = 40; - int blue_team_green = 80; - int blue_team_blue = 220; - int alt_blue_team_red = 30; - int alt_blue_team_green = 60; - int alt_blue_team_blue = 110; - - // Green team colors - int green_team_red = 40; - int green_team_green = 220; - int green_team_blue = 40; - int alt_green_team_red = 30; - int alt_green_team_green = 140; - int alt_green_team_blue = 30; - // target name Ghud_SetText(clent, hud[h_nbar + 1], nm_s); // target stat bar From 29c49fbef0d00361c28431dd5cbefdd0b98e94a0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 6 Nov 2024 16:54:24 -0500 Subject: [PATCH 938/974] Save point --- src/action/p_hud.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 1eb8926c3..389411ca6 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -862,7 +862,7 @@ static void HUD_UpdateSpectatorTimer(edict_t *clent) if (ctf->value) Ghud_SetInt(clent, hud[h_team_l_num], ctfgame.team1); else - //Ghud_SetInt(clent, hud[h_team_l_num], 13); + //Ghud_SetInt(clent, hud[h_team_l_num], 13); // Testing double digits Ghud_SetInt(clent, hud[h_team_l_num], teams[TEAM1].score); // team 2 (blue team) @@ -871,7 +871,7 @@ static void HUD_UpdateSpectatorTimer(edict_t *clent) if (ctf->value) Ghud_SetInt(clent, hud[h_team_r_num], ctfgame.team2); else - //Ghud_SetInt(clent, hud[h_team_r_num], 25); + //Ghud_SetInt(clent, hud[h_team_r_num], 25); // Testing double digits Ghud_SetInt(clent, hud[h_team_r_num], teams[TEAM2].score); } @@ -975,7 +975,7 @@ void HUD_SpectatorTimerSetup(edict_t *clent) if (teams[TEAM2].score >= 10) // gotta readjust size for justifying purposes hud[h_team_r_num] = Ghud_AddNumber(clent, 35, 60, 0); else - hud[h_team_r_num] = Ghud_AddNumber(clent, 0, 60, 0); + hud[h_team_r_num] = Ghud_AddNumber(clent, 20, 60, 0); Ghud_SetSize(clent, hud[h_team_r_num], 2, 0); Ghud_SetAnchor(clent, hud[h_team_r_num], 0.5, 0); From a22904881273acefc43a8c8495c1534ecf9a4b05 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 8 Nov 2024 12:37:45 -0500 Subject: [PATCH 939/974] Static link libjpeg --- meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/meson.build b/meson.build index 74d759f7f..70fe4dbf2 100644 --- a/meson.build +++ b/meson.build @@ -407,6 +407,7 @@ game_deps = [zlib] jpeg = dependency('libjpeg', required: get_option('libjpeg'), default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], + static: true ) if jpeg.found() From 98005fcb8fe012848759417c1800aadec949527a Mon Sep 17 00:00:00 2001 From: mikota Date: Thu, 5 Dec 2024 14:56:04 +0100 Subject: [PATCH 940/974] re-add xhair --- doc/client.asciidoc | 3 + src/client/screen.c | 440 ++++++++++++++++++++++---------------------- 2 files changed, 224 insertions(+), 219 deletions(-) diff --git a/doc/client.asciidoc b/doc/client.asciidoc index 431ac4f73..3b9bfc304 100644 --- a/doc/client.asciidoc +++ b/doc/client.asciidoc @@ -565,6 +565,9 @@ xhair_thickness:: xhair_elasticity:: Specifies how 'elastic' the xhair is, i.e. how easily it adapts. Lower values will result in a more stiff xhair, while higher values will have a more bouncy effect. + +xhair_fast_transition:: + When switching weapons, the xhair will transition faster to the new gap/length. xhair_x:: xhair_y:: diff --git a/src/client/screen.c b/src/client/screen.c index 3224a4488..e25539806 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -57,7 +57,7 @@ static struct { qhandle_t net_pic; qhandle_t font_pic; - int hud_x, hud_y; + int hud_x, hud_y; int hud_width, hud_height; float hud_scale; int lag_draw_scale; @@ -102,11 +102,11 @@ static cvar_t *xhair_firing_error; static cvar_t *xhair_movement_error; static cvar_t *xhair_deployed_weapon_gap; static cvar_t *xhair_thickness; -static cvar_t *xhair_scale; static cvar_t *xhair_x; static cvar_t *xhair_y; static cvar_t *xhair_elasticity; static cvar_t *xhair_enabled; +static cvar_t *xhair_fast_transition; static cvar_t *r_maxfps; @@ -1395,11 +1395,11 @@ void SCR_Init(void) xhair_movement_error = Cvar_Get("xhair_movement_error","1",0); xhair_deployed_weapon_gap = Cvar_Get("xhair_deployed_weapon_gap","1",0); xhair_thickness = Cvar_Get("xhair_thickness","1",0); - xhair_scale = Cvar_Get("xhair_scale","1",0); xhair_x = Cvar_Get("xhair_x","0",0); xhair_y = Cvar_Get("xhair_y","0",0); xhair_elasticity = Cvar_Get("xhair_elasticity","1",0); xhair_enabled = Cvar_Get("xhair_enabled","0",0); + xhair_fast_transition = Cvar_Get("xhair_fast_transition","1",0); r_maxfps = Cvar_Get("r_maxfps","0",0); @@ -2161,6 +2161,8 @@ typedef struct { #define XHAIR_MAX_GAP 1024 #define XHAIR_MAX_LENGTH 648 +#define XHAIR_FAST_TRANS_GAP 200 +#define XHAIR_FAST_TRANS_LENGTH 50 static float deltatime_factor = 0.001; static xhair_weapon_cfg_t xhair_weapon_cfgs[9] = { @@ -2252,8 +2254,8 @@ static void SCR_DrawXhair(void) { if (deltatime_ms > 8) deltatime_ms = 8; // Con_Printf("%d ",deltatime_ms); last_rtime = cls.realtime; - int xh_center_x = scr.hud_width/2 - xhair_thickness->integer/2; - int xh_center_y = scr.hud_height/2 - xhair_thickness->integer/2; + int xh_center_x = r_config.width/2 - xhair_thickness->integer/2; + int xh_center_y = r_config.height/2 - xhair_thickness->integer/2; if (xhair_dot->integer) { R_DrawFill32(xh_center_x+xhair_x->integer, xh_center_y+xhair_y->integer, @@ -2296,6 +2298,10 @@ static void SCR_DrawXhair(void) { length += (xh.length - length) * xh_elasticity * deltatime_factor*500; repeat *= 2; } while(repeatinteger) { + if (fabs(gap - xh.gap) > XHAIR_FAST_TRANS_GAP) gap = xh.gap; + if (fabs(length - xh.length) > XHAIR_FAST_TRANS_LENGTH) length = xh.length; + } int rgap = (int)(round(gap)); int rlength = (int)(round(length)); @@ -2329,11 +2335,7 @@ static void SCR_DrawXhair(void) { xh_w,xh_h,scr.crosshair_color.u32); } } -} - -// mikota's xhair -static void SCR_DrawClassicCrosshair(void) { - R_SetColor(scr.crosshair_color.u32); + R_SetScale(scr.hud_scale); } static void SCR_DrawHitMarker(void) @@ -2363,6 +2365,11 @@ static void SCR_DrawHitMarker(void) static void SCR_DrawCrosshair(void) { + if (xhair_enabled->value) { + SCR_DrawXhair(); + return; + } + int x, y; if (!scr_crosshair->integer) @@ -2390,216 +2397,216 @@ static void SCR_DrawCrosshair(void) #ifdef AQTION_EXTENSION void CL_Clear3DGhudQueue(void) { - ghud_3delement_t *link; - ghud_3delement_t *hold; - for (link = cl.ghud_3dlist; link != NULL; hold = link, link = link->next, free(hold)); + ghud_3delement_t *link; + ghud_3delement_t *hold; + for (link = cl.ghud_3dlist; link != NULL; hold = link, link = link->next, free(hold)); } static void SCR_DrawGhudElement(ghud_element_t *element, float alpha_base, color_t color_base, int x, int y, int sizex, int sizey) { - byte alpha = element->color[3]; - if (element->flags & GHF_BLINK) - alpha = min((element->color[3] * 0.85) + (element->color[3] * 0.25 * sin((float)cls.realtime / 125)), 255); - - color_base.u8[0] = element->color[0] * (color_base.u8[2] / 0xFF); - color_base.u8[1] = element->color[1] * (color_base.u8[2] / 0xFF); - color_base.u8[2] = element->color[2] * (color_base.u8[2] / 0xFF); - color_base.u8[3] = (alpha_base * alpha); - R_SetColor(color_base.u32); - - switch (element->type) - { - case GHT_TEXT:; - int length = strlen(element->text); - int uiflags = element->size[0] | (element->size[1] << 16); - if ((uiflags & UI_CENTER) == UI_CENTER) - x -= (length * CHAR_WIDTH * 0.5); - else if (uiflags & UI_RIGHT) - x -= (length * CHAR_WIDTH); - - if ((uiflags & UI_MIDDLE) == UI_MIDDLE) - y -= (length * CHAR_HEIGHT * 0.5); - else if (uiflags & UI_BOTTOM) - y -= (length * CHAR_HEIGHT); - - uiflags &= ~(UI_LEFT | UI_RIGHT | UI_TOP | UI_BOTTOM); - - R_DrawString(x, y, uiflags, MAX_STRING_CHARS, element->text, scr.font_pic); - break; - case GHT_IMG: - if (!element->val) - break; - - R_DrawStretchPic(x, y, sizex, sizey, cl.image_precache[element->val]); - break; - case GHT_NUM:; - int numsize = element->size[0]; - if (numsize <= 0) - { - double val = element->val; - if (val <= 0) - val = 0; - else - val = log10(val); - - numsize = val + 1; - } - - HUD_DrawNumber(x, y, 0, numsize, element->val); - break; + byte alpha = element->color[3]; + if (element->flags & GHF_BLINK) + alpha = min((element->color[3] * 0.85) + (element->color[3] * 0.25 * sin((float)cls.realtime / 125)), 255); + + color_base.u8[0] = element->color[0] * (color_base.u8[2] / 0xFF); + color_base.u8[1] = element->color[1] * (color_base.u8[2] / 0xFF); + color_base.u8[2] = element->color[2] * (color_base.u8[2] / 0xFF); + color_base.u8[3] = (alpha_base * alpha); + R_SetColor(color_base.u32); + + switch (element->type) + { + case GHT_TEXT:; + int length = strlen(element->text); + int uiflags = element->size[0] | (element->size[1] << 16); + if ((uiflags & UI_CENTER) == UI_CENTER) + x -= (length * CHAR_WIDTH * 0.5); + else if (uiflags & UI_RIGHT) + x -= (length * CHAR_WIDTH); + + if ((uiflags & UI_MIDDLE) == UI_MIDDLE) + y -= (length * CHAR_HEIGHT * 0.5); + else if (uiflags & UI_BOTTOM) + y -= (length * CHAR_HEIGHT); + + uiflags &= ~(UI_LEFT | UI_RIGHT | UI_TOP | UI_BOTTOM); + + R_DrawString(x, y, uiflags, MAX_STRING_CHARS, element->text, scr.font_pic); + break; + case GHT_IMG: + if (!element->val) + break; + + R_DrawStretchPic(x, y, sizex, sizey, cl.image_precache[element->val]); + break; + case GHT_NUM:; + int numsize = element->size[0]; + if (numsize <= 0) + { + double val = element->val; + if (val <= 0) + val = 0; + else + val = log10(val); + + numsize = val + 1; + } + + HUD_DrawNumber(x, y, 0, numsize, element->val); + break; case GHT_FILL:; - R_DrawFill32(x, y, element->size[0], element->size[1], color_base.u32); - } + R_DrawFill32(x, y, element->size[0], element->size[1], color_base.u32); + } } static void SCR_DrawGhud(void) { - int x, y; - int i; - - float alpha_base = Cvar_ClampValue(scr_alpha, 0, 1); - color_t color_base; - color_base.u32 = 0xFFFFFFFF; - - - if (cl.ghud_3dlist) - { - /*build view and projection matricies*/ - float modelview[16]; - float proj[16]; - - Matrix4x4_CM_ModelViewMatrix(modelview, cl.refdef.viewangles, cl.refdef.vieworg); - Matrix4x4_CM_Projection2(proj, cl.refdef.fov_x, cl.refdef.fov_y, 4); - - /*build the vp matrix*/ - Matrix4_Multiply(proj, modelview, r_viewmatrix); - - ghud_element_t *element; - ghud_3delement_t *link; - ghud_3delement_t *hold; - for (link = cl.ghud_3dlist; link; hold = link, link = link->next, free(hold)) - { - element = link->element; - element->color[3] = 200; - - float v[4], tempv[4], out[4]; - - // get position - v[0] = element->pos[0]; - v[1] = element->pos[1]; - v[2] = element->pos[2]; - v[3] = 1; - - Matrix4x4_CM_Transform4(r_viewmatrix, v, tempv); - - if (tempv[3] < 0) // the element is behind us - continue; - - tempv[0] /= tempv[3]; - tempv[1] /= tempv[3]; - tempv[2] /= tempv[3]; - - out[0] = (1 + tempv[0]) / 2; - out[1] = 1 - (1 + tempv[1]) / 2; - out[2] = tempv[2]; - - x = scr.hud_x + out[0] * scr.hud_width; - y = scr.hud_y + out[1] * scr.hud_height; - // - - float mult = 300 / link->distance; - Q_clipf(mult, 0.25, 5); - - int sizex = element->size[0] * mult; - int sizey = element->size[1] * mult; - - x -= (sizex / 2); - y -= (sizey / 2); - - float alpha_mult = 1; - alpha_mult = min(1 / mult, 1); - - - vec3_t pos, xhair; - pos[0] = x; - pos[1] = y; - pos[2] = 0; - xhair[0] = scr.hud_width / 2; - xhair[1] = scr.hud_height / 2; - xhair[2] = 0; - - float scale_dimension = min(scr.hud_width, scr.hud_height) / 6; - VectorSubtract(pos, xhair, pos); - float len = VectorLength(pos); - if (len < scale_dimension) - { - alpha_mult *= len / scale_dimension; - } - - SCR_DrawGhudElement(element, alpha_base * alpha_mult, color_base, x, y, sizex, sizey); - } - - cl.ghud_3dlist = NULL; - } - - for (i = 0; i < MAX_GHUDS; i++) - { - ghud_element_t *element = &(cl.ghud[i]); - if (!(element->flags & GHF_INUSE) || (element->flags & GHF_HIDE)) - continue; - - if (element->color[3] <= 0) // totally transparent - continue; - - if (element->flags & GHF_3DPOS) - { - ghud_3delement_t *link = malloc(sizeof(ghud_3delement_t)); - link->element = element; - - vec3_t org; - org[0] = element->pos[0]; - org[1] = element->pos[1]; - org[2] = element->pos[2]; - VectorSubtract(org, cl.refdef.vieworg, org); - link->distance = VectorLength(org); - link->next = NULL; - - ///* - if (cl.ghud_3dlist == NULL) - cl.ghud_3dlist = link; - else if (cl.ghud_3dlist->distance < link->distance) - { - link->next = cl.ghud_3dlist; - cl.ghud_3dlist = link; - } - else - { - ghud_3delement_t *hold, *list; - list = cl.ghud_3dlist; - hold = list; - while (list && list->distance >= link->distance) - { - hold = list; - list = list->next; - } - - link->next = hold->next; - hold->next = link; - } - //*/ - - continue; - } - else - { - x = scr.hud_x + element->pos[0] + (scr.hud_width * element->anchor[0]); - y = scr.hud_y + element->pos[1] + (scr.hud_height * element->anchor[1]); - } - - SCR_DrawGhudElement(element, alpha_base, color_base, x, y, element->size[0], element->size[1]); - } + int x, y; + int i; + + float alpha_base = Cvar_ClampValue(scr_alpha, 0, 1); + color_t color_base; + color_base.u32 = 0xFFFFFFFF; + + + if (cl.ghud_3dlist) + { + /*build view and projection matricies*/ + float modelview[16]; + float proj[16]; + + Matrix4x4_CM_ModelViewMatrix(modelview, cl.refdef.viewangles, cl.refdef.vieworg); + Matrix4x4_CM_Projection2(proj, cl.refdef.fov_x, cl.refdef.fov_y, 4); + + /*build the vp matrix*/ + Matrix4_Multiply(proj, modelview, r_viewmatrix); + + ghud_element_t *element; + ghud_3delement_t *link; + ghud_3delement_t *hold; + for (link = cl.ghud_3dlist; link; hold = link, link = link->next, free(hold)) + { + element = link->element; + element->color[3] = 200; + + float v[4], tempv[4], out[4]; + + // get position + v[0] = element->pos[0]; + v[1] = element->pos[1]; + v[2] = element->pos[2]; + v[3] = 1; + + Matrix4x4_CM_Transform4(r_viewmatrix, v, tempv); + + if (tempv[3] < 0) // the element is behind us + continue; + + tempv[0] /= tempv[3]; + tempv[1] /= tempv[3]; + tempv[2] /= tempv[3]; + + out[0] = (1 + tempv[0]) / 2; + out[1] = 1 - (1 + tempv[1]) / 2; + out[2] = tempv[2]; + + x = scr.hud_x + out[0] * scr.hud_width; + y = scr.hud_y + out[1] * scr.hud_height; + // + + float mult = 300 / link->distance; + Q_clipf(mult, 0.25, 5); + + int sizex = element->size[0] * mult; + int sizey = element->size[1] * mult; + + x -= (sizex / 2); + y -= (sizey / 2); + + float alpha_mult = 1; + alpha_mult = min(1 / mult, 1); + + + vec3_t pos, xhair; + pos[0] = x; + pos[1] = y; + pos[2] = 0; + xhair[0] = scr.hud_width / 2; + xhair[1] = scr.hud_height / 2; + xhair[2] = 0; + + float scale_dimension = min(scr.hud_width, scr.hud_height) / 6; + VectorSubtract(pos, xhair, pos); + float len = VectorLength(pos); + if (len < scale_dimension) + { + alpha_mult *= len / scale_dimension; + } + + SCR_DrawGhudElement(element, alpha_base * alpha_mult, color_base, x, y, sizex, sizey); + } + + cl.ghud_3dlist = NULL; + } + + for (i = 0; i < MAX_GHUDS; i++) + { + ghud_element_t *element = &(cl.ghud[i]); + if (!(element->flags & GHF_INUSE) || (element->flags & GHF_HIDE)) + continue; + + if (element->color[3] <= 0) // totally transparent + continue; + + if (element->flags & GHF_3DPOS) + { + ghud_3delement_t *link = malloc(sizeof(ghud_3delement_t)); + link->element = element; + + vec3_t org; + org[0] = element->pos[0]; + org[1] = element->pos[1]; + org[2] = element->pos[2]; + VectorSubtract(org, cl.refdef.vieworg, org); + link->distance = VectorLength(org); + link->next = NULL; + + ///* + if (cl.ghud_3dlist == NULL) + cl.ghud_3dlist = link; + else if (cl.ghud_3dlist->distance < link->distance) + { + link->next = cl.ghud_3dlist; + cl.ghud_3dlist = link; + } + else + { + ghud_3delement_t *hold, *list; + list = cl.ghud_3dlist; + hold = list; + while (list && list->distance >= link->distance) + { + hold = list; + list = list->next; + } + + link->next = hold->next; + hold->next = link; + } + //*/ + + continue; + } + else + { + x = scr.hud_x + element->pos[0] + (scr.hud_width * element->anchor[0]); + y = scr.hud_y + element->pos[1] + (scr.hud_height * element->anchor[1]); + } + + SCR_DrawGhudElement(element, alpha_base, color_base, x, y, element->size[0], element->size[1]); + } } #endif @@ -2643,11 +2650,6 @@ static void SCR_Draw2D(void) scr.hud_height = Q_rint(scr.hud_height * scr.hud_scale); scr.hud_width = Q_rint(scr.hud_width * scr.hud_scale); -// mikota's xhair -// if (!xhair_enabled->integer) { -// SCR_DrawClassicCrosshair(); -// } - // crosshair has its own color and alpha SCR_DrawCrosshair(); @@ -2659,12 +2661,12 @@ static void SCR_Draw2D(void) SCR_DrawLayout(); #ifdef AQTION_EXTENSION - // Draw game defined hud elements - SCR_DrawGhud(); + // Draw game defined hud elements + SCR_DrawGhud(); - // gotta redo the colors because the ghud messes with them, sadly. - R_ClearColor(); - R_SetAlpha(Cvar_ClampValue(scr_alpha, 0, 1)); + // gotta redo the colors because the ghud messes with them, sadly. + R_ClearColor(); + R_SetAlpha(Cvar_ClampValue(scr_alpha, 0, 1)); #endif SCR_DrawInventory(); From 1d0605ca8c090a4d2fd2f44b1965d18c48b945c3 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Tue, 7 Jan 2025 12:25:11 -0500 Subject: [PATCH 941/974] Added nice features from TNG upstream, thanks Raptor007 --- src/action/a_vote.c | 8 ++++++++ src/action/g_local.h | 1 + src/action/g_main.c | 2 ++ src/action/g_save.c | 2 ++ src/action/p_client.c | 11 +++++++++++ src/action/p_weapon.c | 2 +- 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/action/a_vote.c b/src/action/a_vote.c index 163b2e5f5..31347f42c 100644 --- a/src/action/a_vote.c +++ b/src/action/a_vote.c @@ -113,6 +113,14 @@ int _numclients (void) { if (!other->inuse || !other->client || !other->client->pers.connected || other->client->pers.mvdspec) continue; + + // Idle players do not count towards voting pool (unless they already voted). -- Raptor007 + if (sv_idleremove->value && ! (other->client->resp.mapvote || other->client->resp.cvote)){ + int idleframes = other->client->resp.idletime ? (level.framenum - other->client->resp.idletime) : 0; + if (idleframes > sv_idleremove->value * HZ) + continue; + } + #ifndef NO_BOTS // If bot_countashuman is enabled, then do not continue/ignore bots if(!bot_countashuman->value) { diff --git a/src/action/g_local.h b/src/action/g_local.h index a1303504a..e6c02c36e 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1373,6 +1373,7 @@ extern cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA extern cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit +extern cvar_t *grenade_drop; // Allows grenades to be dropped on death #ifdef AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index 646148d88..3b7731bb3 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -583,6 +583,8 @@ cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bot cvar_t *lca_grenade; // Allows grenade pin pulling during LCA cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit +cvar_t *grenade_drop; // Allows grenades to be dropped on death + #ifdef AQTION_EXTENSION cvar_t *use_newirvision; diff --git a/src/action/g_save.c b/src/action/g_save.c index 24b7b4df4..cd5d0afeb 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -544,6 +544,8 @@ void InitGame( void ) glassfragmentlimit = gi.cvar("glassfragmentlimit", "30", 0); //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) + grenade_drop = gi.cvar( "grenade_drop", "0", 0 ); // Raptor007 - added grenade drop + g_select_empty = gi.cvar( "g_select_empty", "0", CVAR_ARCHIVE ); g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH); diff --git a/src/action/p_client.c b/src/action/p_client.c index 40d811e48..5767fa910 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -1704,6 +1704,17 @@ void TossItemsOnDeath(edict_t * ent) if (ent->client->inventory[ITEM_INDEX(item)] > 0) { EjectItem(ent, item); } + + // Grenade drop option -- Raptor007 + if (grenade_drop->value > 0) { + item = GET_ITEM(GRENADE_NUM); + int drop_count = ent->client->inventory[ITEM_INDEX(item)]; + if (grenade_drop->value < drop_count) + drop_count = grenade_drop->value; + for(i = 0; i < drop_count; i++) { + EjectItem(ent, item); + } + } // special items // Quad is unused in AQ2 diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 5f304a136..9b002b354 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -396,7 +396,7 @@ qboolean Pickup_Weapon(edict_t* ent, edict_t* other) return false; case GRENADE_NUM: - if (!(gameSettings & GS_DEATHMATCH) && ctf->value != 2 && !band) + if (!(gameSettings & GS_DEATHMATCH) && ctf->value != 2 && !band && !(grenade_drop->value)) return false; if (other->client->inventory[index] >= other->client->grenade_max) From 9406cda16830d50efb7af2b5e8150be6c273b73d Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Tue, 7 Jan 2025 13:38:38 -0500 Subject: [PATCH 942/974] Updated doc on missing stats_mode code --- action/doc/manual.txt | 3 ++- doc/action.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/action/doc/manual.txt b/action/doc/manual.txt index 36aa4a5a1..89cbb284f 100644 --- a/action/doc/manual.txt +++ b/action/doc/manual.txt @@ -446,7 +446,8 @@ Features stats [#] - this will display the stats for the player with the id given. (client side) stats_mode [0/1/2] - when set to 1, it will automatically display the stats of the player at the end of each round. When set to 2, it will automatically display the stats of the player at the end of the map. By default - this is set to 0 (off). (client side) + this is set to 0 (off). (client side) // This is a legacy entry that has no code associated with it, I'm + only keeping it in this doc for historical reasons. * Automatic Joining/Equipping For the lazy players under us, we have created two new client commands to make things easier. diff --git a/doc/action.md b/doc/action.md index cbb21fed8..ea93c4716 100644 --- a/doc/action.md +++ b/doc/action.md @@ -556,6 +556,7 @@ To see how well players are doing, we have implemented statistics into TNG. This - `stats list` - this will display a list of all players and their ID. (client side) - `stats [#]` - this will display the stats for the player with the id given. (client side) - `stats_mode [0/1/2]` - when set to 1, it will automatically display the stats of the player at the end of each round. When set to 2, it will automatically display the stats of the player at the end of the map. By default this is set to 0 (off). (client side) + - ^ This is a legacy entry that has no code associated with it, I'm only keeping it in this doc for historical reasons. ### Automatic Joining/Equipping/Menu For the lazy players under us, we have created three new commands to make things easier. From b17ee95908bef614f8748a6030ec0f979da94324 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 8 Jan 2025 16:15:02 -0500 Subject: [PATCH 943/974] Added teamnone command --- src/action/a_match.c | 72 ++++++++++++++++++++++++++++++++++++++++++++ src/action/a_match.h | 1 + src/action/g_cmds.c | 32 +++++++++++++++++--- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/src/action/a_match.c b/src/action/a_match.c index b3061a64e..9e66b3b62 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -465,6 +465,78 @@ void Cmd_Teamskin_f(edict_t * ent) gi.cprintf(ent, PRINT_HIGH, "New team skin: %s\n", team->skin); } +void Cmd_Teamnone_f(edict_t *ent) +{ + int i; + edict_t *other, *target; + + if (!matchmode->value) { + gi.cprintf(ent, PRINT_HIGH, "This command needs matchmode to be enabled\n"); + return; + } + + if (ent->client->resp.team == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "You need to be on a team for that...\n"); + return; + } + + if (!IS_CAPTAIN(ent)) { + gi.cprintf(ent, PRINT_HIGH, "You are not the captain of your team\n"); + return; + } + + if (gi.argc() < 1) { + gi.cprintf(ent, PRINT_HIGH, "You need to provide a playernum for this command\nUse 'playerlist' to get a list of playernums\n"); + return; + } + + if (gi.argc() > 2) { + gi.cprintf(ent, PRINT_HIGH, "You can only specify one playernum for this command\n"); + return; + } + + int playernum = atoi(gi.argv(1)); + + // Get entity of the playernum + target = NULL; + for (i = 0, other = &g_edicts[1]; i < game.maxclients; i++, other++) { + if (!other->inuse || !other->client || other->is_bot) // Do not count bots + continue; + if (other->client->clientNum == playernum) { + target = other; + break; + } + } + + if (target != NULL) { + if (target == ent || target->client->clientNum == ent->client->clientNum) { + gi.cprintf(ent, PRINT_HIGH, "If you want to leave so badly, just do it the old fashioned way!\n"); + return; + } + + if (target->client->resp.team == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "Player %i (%s) is not on a team\n", playernum, target->client->pers.netname); + return; + } + + if (target->client->resp.team != ent->client->resp.team) { + gi.cprintf(ent, PRINT_HIGH, "You cannot remove a player from the other team\n"); + return; + } + + if (target->is_bot) { + gi.cprintf(ent, PRINT_HIGH, "You cannot remove bots in this way, use the sv bot commands\n"); + return; + } + // Finally, after all the checks, remove the player from your team + gi.bprintf(PRINT_HIGH, "%s removed %s (clientNum %i) from team %i\n", ent->client->pers.netname, target->client->pers.netname, playernum, target->client->resp.team); + JoinTeam(target, NOTEAM, 1); + } else { + gi.cprintf(ent, PRINT_HIGH, "Player %i not found, check `playerlist`\n", playernum); + return; + } +} + void Cmd_TeamLock_f(edict_t *ent, int a_switch) { char msg[128], *s; diff --git a/src/action/a_match.h b/src/action/a_match.h index 49fb6aaf9..c05f80fca 100644 --- a/src/action/a_match.h +++ b/src/action/a_match.h @@ -33,6 +33,7 @@ void Cmd_Ready_f (edict_t * ent); void Cmd_Sub_f (edict_t * ent); void Cmd_Teamname_f (edict_t * ent); void Cmd_Teamskin_f (edict_t * ent); +void Cmd_Teamnone_f (edict_t * ent); void Cmd_TeamLock_f (edict_t * ent, int a_switch); int CheckForCaptains (int cteam); diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 3d8bcbd0e..5f80e5199 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1523,9 +1523,27 @@ static void Cmd_PlayerList_f (edict_t * ent) char st[64]; char text[1024] = { 0 }; edict_t *e2; + int header_settings = 0; // connect time, ping, score, name + // Set appropriate header based on settings + if (limchasecam->value) { + Q_snprintf(st, sizeof(st), "%-5s %-3s %-3s %-16s\n", "Time", "Ping", "Team", "Name"); + header_settings = 1; + } else if (matchmode->value && IS_CAPTAIN(ent)) { + Q_snprintf(st, sizeof(st), "%-5s %-3s %-3s %-16s\n", "Time", "Ping", "Num", "Name"); + header_settings = 2; + } else if (!teamplay->value || !noscore->value) { + Q_snprintf(st, sizeof(st), "%-5s %-3s %-5s %-16s\n", "Time", "Ping", "Score", "Name"); + header_settings = 3; + } else { + Q_snprintf(st, sizeof(st), "%-5s %-3s %-16s\n", "Time", "Ping", "Name"); + header_settings = 0; + } + // Print the header + gi.cprintf(ent, PRINT_HIGH, "%s", st); + // Set the lines: for (i = 0, e2 = g_edicts + 1; i < game.maxclients; i++, e2++) { @@ -1535,12 +1553,15 @@ static void Cmd_PlayerList_f (edict_t * ent) if (!e2->inuse || !e2->client || e2->client->pers.mvdspec) continue; - if(limchasecam->value) - Q_snprintf (st, sizeof (st), "%02d:%02d %4d %3d %s\n", minutes, seconds, e2->client->ping, e2->client->resp.team, e2->client->pers.netname); // This shouldn't show player's being 'spectators' during games with limchasecam set and/or during matchmode - else if (!teamplay->value || !noscore->value) - Q_snprintf (st, sizeof (st), "%02d:%02d %4d %3d %s%s\n", minutes, seconds, e2->client->ping, e2->client->resp.score, e2->client->pers.netname, (e2->solid == SOLID_NOT && e2->deadflag != DEAD_DEAD) ? " (dead)" : ""); // replaced 'spectator' with 'dead' + // Set the lines with fixed width columns: + if(header_settings == 1) + Q_snprintf(st, sizeof(st), "%02d:%02d %-3d %-3d %-16s\n", minutes, seconds, e2->client->ping, e2->client->resp.team, e2->client->pers.netname); + else if (header_settings == 2) + Q_snprintf(st, sizeof(st), "%02d:%02d %-3d %-3d %-16s\n", minutes, seconds, e2->client->ping, e2->client->clientNum, e2->client->pers.netname); + else if (header_settings == 3) + Q_snprintf(st, sizeof(st), "%02d:%02d %-3d %-5d %-16s%s\n", minutes, seconds, e2->client->ping, e2->client->resp.score, e2->client->pers.netname, (e2->solid == SOLID_NOT && e2->deadflag != DEAD_DEAD) ? " (dead)" : ""); else - Q_snprintf (st, sizeof (st), "%02d:%02d %4d %s%s\n", minutes, seconds, e2->client->ping, e2->client->pers.netname, (e2->solid == SOLID_NOT && e2->deadflag != DEAD_DEAD) ? " (dead)" : ""); // replaced 'spectator' with 'dead' + Q_snprintf(st, sizeof(st), "%02d:%02d %-3d %-16s%s\n", minutes, seconds, e2->client->ping, e2->client->pers.netname, (e2->solid == SOLID_NOT && e2->deadflag != DEAD_DEAD) ? " (dead)" : ""); if (strlen(text) + strlen(st) > sizeof(text) - 6) { @@ -1960,6 +1981,7 @@ static cmdList_t commandList[] = { "ready", Cmd_Ready_f, 0 }, { "teamname", Cmd_Teamname_f, 0 }, { "teamskin", Cmd_Teamskin_f, 0 }, + { "teamnone", Cmd_Teamnone_f, 0 }, { "lock", Cmd_LockTeam_f, 0 }, { "unlock", Cmd_UnlockTeam_f, 0 }, { "entcount", Cmd_Ent_Count_f, 0 }, From 479ce4fcf8a818c46e3017fc441eeeae410a9376 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 8 Jan 2025 17:02:20 -0500 Subject: [PATCH 944/974] Added check for Espionage leader and documentation --- doc/action.md | 3 ++- src/action/a_match.c | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/action.md b/doc/action.md index cbb21fed8..0df0d107a 100644 --- a/doc/action.md +++ b/doc/action.md @@ -241,7 +241,8 @@ Clients will have a few more things to do during matchmode: they have to have a - `sub` - this will make you a sub for the team or remove you from the subs and back in the team - `ready` - this will ready/unready the team. A new round won't start if a team isn't read - `teamname "name"` - allows the captain to set the name of his/her team - - `teamskin "male/resdog"` - allows the captain to set the name of his/her team + - `teamskin "male/resdog"` - allows the captain to set the name of his/her team + - `teamnone <#>` - Using 'playerlist' to determine player numbers, use this to remove players from your team (send them to team 0), usable by Captains - `matchadmin ` - this will allow a player to get admin status - `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map - `unlock` - allows a captain to unlock his team diff --git a/src/action/a_match.c b/src/action/a_match.c index 9e66b3b62..b05c4939c 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -528,6 +528,12 @@ void Cmd_Teamnone_f(edict_t *ent) gi.cprintf(ent, PRINT_HIGH, "You cannot remove bots in this way, use the sv bot commands\n"); return; } + // This should never happen but just in case... + if (esp->value && IS_LEADER(target)){ + gi.cprintf(ent, PRINT_HIGH, "You cannot remove the leader of your team\n"); + return; + } + // Finally, after all the checks, remove the player from your team gi.bprintf(PRINT_HIGH, "%s removed %s (clientNum %i) from team %i\n", ent->client->pers.netname, target->client->pers.netname, playernum, target->client->resp.team); JoinTeam(target, NOTEAM, 1); From f37212efe17ed41e9b6873c4eec8f2fb71c30bb4 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 8 Jan 2025 17:12:04 -0500 Subject: [PATCH 945/974] Updated documentation --- doc/action.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/action.md b/doc/action.md index ea93c4716..93e667066 100644 --- a/doc/action.md +++ b/doc/action.md @@ -708,6 +708,7 @@ Inspired by AQ2:ETE, these additions are optional server vars to create a differ - `gun_dualmk23_enhance [0/1]` - server cvar, default 0. If enabled, this allows both the silencer and the laser sight to be used on the Dual MK23 Pistols. - `use_gren_bonk [0/1]` - server cvar, default 0. If enabled, this enables impact damage of the grenade to cause damage on direct contact with a player. The speed of which the grenade is thrown will determine the damage dealt. Thanks to JukS for the idea and the code. - `lca_grenade` - server cvar, default 0. If enabled, players can pull the grenade pin during Lights Camera Action, but they still cannot throw it until Action! +- `grenade_drop [0/#]` - server cvar, default 0. If enabled, players who have grenades in their inventory when they die will drop unspent grenades on the ground. This value can be set to any number, but it probably makes sense to keep it below 3 ### Highscores Borrowing code from OpenTDM (thank you Skuller!), high scores are stored in a local file on the server. Each time a new high score is achieved, it is registered in this file. The high scores are separated by map and by game mode. For example, `highscores/dm/wizs.txt` is the highscores file for the map `wizs` in `dm` mode. From cb47268344e580b7d622da05a2fe79af9bcd0fad Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 10 Jan 2025 10:29:01 -0500 Subject: [PATCH 946/974] A potential fix for crashing when refreshing due to crosshair loading --- src/client/screen.c | 2 +- src/refresh/images.c | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index e25539806..82fb3cfc9 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1335,7 +1335,7 @@ static void scr_scale_changed(cvar_t *self) { scr.hud_scale = R_ClampScale(self); - scr_crosshair_changed(scr_crosshair); + //scr_crosshair_changed(scr_crosshair); } static void scr_lag_draw_scale_changed(cvar_t *self) { diff --git a/src/refresh/images.c b/src/refresh/images.c index d55546193..edeff71d1 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1536,6 +1536,7 @@ static void IMG_List_f(void) Cmd_PrintHelp(o_imagelist); Com_Printf( "Types legend:\n" + "H: handle\n" "P: pics\n" "F: fonts\n" "M: skins\n" @@ -1574,7 +1575,8 @@ static void IMG_List_f(void) if (paletted == -1 && (image->flags & IF_PALETTED)) continue; - Com_Printf("%c%c%c%c %4i %4i %s: %s\n", + Com_Printf("[%3i] %c%c%c%c %4i %4i %s: %s\n", + i, // Add this line to show the handle number types[image->type > IT_MAX ? IT_MAX : image->type], (image->flags & IF_TRANSPARENT) ? 'T' : ' ', (image->flags & IF_SCRAP) ? 'S' : image->texnum2 ? 'G' : ' ', @@ -2063,6 +2065,11 @@ IMG_ForHandle */ image_t *IMG_ForHandle(qhandle_t h) { + if (h < 0 || h >= r_numImages) + Com_EPrintf("Invalid image handle: %i (max %i) called from %s\nCurrent r_numImages: %d\nCaller address: %p\n", + h, r_numImages - 1, __func__, r_numImages, __builtin_return_address(0)); + + Q_assert(h >= 0 && h < r_numImages); return &r_images[h]; } @@ -2168,6 +2175,8 @@ void IMG_FreeAll(void) image_t *image; int i, count = 0; + Com_Printf("IMG_FreeAll: Starting cleanup with r_numImages: %d\n", r_numImages); + for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot @@ -2186,6 +2195,9 @@ void IMG_FreeAll(void) // &r_images[0] == R_NOTEXTURE r_numImages = R_NUM_AUTO_IMG; + + Com_Printf("IMG_FreeAll: Completed with r_numImages: %d\n", r_numImages); + } /* @@ -2241,6 +2253,7 @@ static const cmdreg_t img_cmd[] = { void IMG_Init(void) { int i; + Com_Printf("IMG_Init: Starting image system initialization\n"); Q_assert(!r_numImages); @@ -2277,6 +2290,9 @@ void IMG_Init(void) // &r_images[0] == R_NOTEXTURE r_numImages = R_NUM_AUTO_IMG; + + Com_Printf("IMG_Init: Completed with r_numImages: %d\n", r_numImages); + } void IMG_Shutdown(void) From 9bccd1c379995c702f58224bb73daeb3a360b848 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 10 Jan 2025 10:35:07 -0500 Subject: [PATCH 947/974] MSVC always bitches about something --- src/refresh/images.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index edeff71d1..e45d010ce 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -2065,9 +2065,9 @@ IMG_ForHandle */ image_t *IMG_ForHandle(qhandle_t h) { - if (h < 0 || h >= r_numImages) - Com_EPrintf("Invalid image handle: %i (max %i) called from %s\nCurrent r_numImages: %d\nCaller address: %p\n", - h, r_numImages - 1, __func__, r_numImages, __builtin_return_address(0)); + // if (h < 0 || h >= r_numImages) + // Com_EPrintf("Invalid image handle: %i (max %i) called from %s\nCurrent r_numImages: %d\nCaller address: %p\n", + // h, r_numImages - 1, __func__, r_numImages, __builtin_return_address(0)); Q_assert(h >= 0 && h < r_numImages); From e17f2ba721663898693e0b4d0d3a3ffc0afc5b84 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 10 Jan 2025 12:27:07 -0500 Subject: [PATCH 948/974] Removed debug statements --- src/refresh/images.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/refresh/images.c b/src/refresh/images.c index e45d010ce..3d7cb1fa6 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -2065,11 +2065,6 @@ IMG_ForHandle */ image_t *IMG_ForHandle(qhandle_t h) { - // if (h < 0 || h >= r_numImages) - // Com_EPrintf("Invalid image handle: %i (max %i) called from %s\nCurrent r_numImages: %d\nCaller address: %p\n", - // h, r_numImages - 1, __func__, r_numImages, __builtin_return_address(0)); - - Q_assert(h >= 0 && h < r_numImages); return &r_images[h]; } @@ -2175,8 +2170,6 @@ void IMG_FreeAll(void) image_t *image; int i, count = 0; - Com_Printf("IMG_FreeAll: Starting cleanup with r_numImages: %d\n", r_numImages); - for (i = R_NUM_AUTO_IMG, image = r_images + i; i < r_numImages; i++, image++) { if (!image->name[0]) continue; // free image_t slot @@ -2195,9 +2188,6 @@ void IMG_FreeAll(void) // &r_images[0] == R_NOTEXTURE r_numImages = R_NUM_AUTO_IMG; - - Com_Printf("IMG_FreeAll: Completed with r_numImages: %d\n", r_numImages); - } /* @@ -2253,8 +2243,6 @@ static const cmdreg_t img_cmd[] = { void IMG_Init(void) { int i; - Com_Printf("IMG_Init: Starting image system initialization\n"); - Q_assert(!r_numImages); #if USE_PNG || USE_JPG || USE_TGA @@ -2290,9 +2278,6 @@ void IMG_Init(void) // &r_images[0] == R_NOTEXTURE r_numImages = R_NUM_AUTO_IMG; - - Com_Printf("IMG_Init: Completed with r_numImages: %d\n", r_numImages); - } void IMG_Shutdown(void) From 949d47e95c58aef5ca420b81ac7fd25a4ee35807 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 10 Jan 2025 12:28:21 -0500 Subject: [PATCH 949/974] Removed scr_crosshair_changed in scr_scale_changed --- src/client/screen.c | 2 -- src/refresh/images.c | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client/screen.c b/src/client/screen.c index 82fb3cfc9..48273855e 100644 --- a/src/client/screen.c +++ b/src/client/screen.c @@ -1334,8 +1334,6 @@ void SCR_RegisterMedia(void) static void scr_scale_changed(cvar_t *self) { scr.hud_scale = R_ClampScale(self); - - //scr_crosshair_changed(scr_crosshair); } static void scr_lag_draw_scale_changed(cvar_t *self) { diff --git a/src/refresh/images.c b/src/refresh/images.c index 3d7cb1fa6..88b205dbe 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1576,7 +1576,7 @@ static void IMG_List_f(void) continue; Com_Printf("[%3i] %c%c%c%c %4i %4i %s: %s\n", - i, // Add this line to show the handle number + i, // Image handle number types[image->type > IT_MAX ? IT_MAX : image->type], (image->flags & IF_TRANSPARENT) ? 'T' : ' ', (image->flags & IF_SCRAP) ? 'S' : image->texnum2 ? 'G' : ' ', @@ -2243,6 +2243,7 @@ static const cmdreg_t img_cmd[] = { void IMG_Init(void) { int i; + Q_assert(!r_numImages); #if USE_PNG || USE_JPG || USE_TGA From 5a50b54aff234d5fb645cc90e102bfb6ecce018a Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 11 Jan 2025 12:32:40 -0500 Subject: [PATCH 950/974] Fix for Espionage not starting in ATL mode --- src/action/a_esp.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 5370eb51d..04b1dabb0 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1568,6 +1568,8 @@ qboolean AllTeamsHaveLeaders(void) teams[TEAM2].leader_dead = false; if (teamCount == 3) teams[TEAM3].leader_dead = false; + + return true; } else { return false; } From 0309382f6dfe76e94e446b872870b38a835b8f07 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Sat, 11 Jan 2025 20:59:05 -0500 Subject: [PATCH 951/974] Set chosenItem to 0 if no item to log --- src/action/tng_stats.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index c44691f60..f0b38c81c 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -1074,7 +1074,11 @@ void LogKill(edict_t *self, edict_t *inflictor, edict_t *attacker) // Item identifier, taking item kit mode into account if (!item_kit_mode->value) { - chosenItem = attacker->client->pers.chosenItem->typeNum; + if (!attacker->client->pers.chosenItem) { + chosenItem = 0; + } else { + chosenItem = attacker->client->pers.chosenItem->typeNum; + } } else { if (attacker->client->pers.chosenItem->typeNum == KEV_NUM) { chosenItem = KEV_NUM; From caf3e0accee728146426d1acd703a4c2d27d51db Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Wed, 15 Jan 2025 08:25:12 -0500 Subject: [PATCH 952/974] Check gl_shaders value before using legacy gl in DrawNullModel --- src/refresh/main.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/refresh/main.c b/src/refresh/main.c index 280b6e3af..75ecdb741 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -428,8 +428,10 @@ static void GL_DrawNullModel(void) GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte *)colors); - GL_VertexPointer(3, 0, &points[0][0]); + if(!gl_shaders->value) { + GL_ColorBytePointer(4, 0, (GLubyte *)colors); + GL_VertexPointer(3, 0, &points[0][0]); + } GL_LockArrays(6); From 83529ad174a619c42929ff401431d6f8da851a94 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 Jan 2025 08:53:24 -0500 Subject: [PATCH 953/974] Client will not try to download jpg textures if r_override_textures is 0 (#245) * Client will not try to download jpg textures if r_override_textures is 0 * Lovely how Mac built it fine but Linux did not * Compiler warnings * Non-static declarations, also checking for r_texture_overrides * Non-static declarations, also checking for r_texture_overrides * Removed commented cvars * Setting r_override_textures to 2 for downloading jpg files now --- inc/refresh/refresh.h | 8 ++++++++ src/client/download.c | 9 +++++++-- src/refresh/images.c | 2 -- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/inc/refresh/refresh.h b/inc/refresh/refresh.h index 87980fae5..d8215ddb5 100644 --- a/inc/refresh/refresh.h +++ b/inc/refresh/refresh.h @@ -237,3 +237,11 @@ void R_EndFrame(void); void R_ModeChanged(int width, int height, int flags); r_opengl_config_t R_GetGLConfig(void); + + +// This used to be in images.c but I moved it because +// download.c in src/client needs to know what to do +#if USE_PNG || USE_JPG || USE_TGA +extern cvar_t *r_override_textures; +extern cvar_t *r_texture_overrides; +#endif \ No newline at end of file diff --git a/src/client/download.c b/src/client/download.c index 079255b72..b18a2934a 100644 --- a/src/client/download.c +++ b/src/client/download.c @@ -35,6 +35,9 @@ static precache_t precache_check; static int precache_sexed_sounds[MAX_SOUNDS]; static int precache_sexed_total; +cvar_t *r_override_textures; +cvar_t *r_texture_overrides; + /* =============== CL_QueueDownload @@ -825,8 +828,10 @@ void CL_RequestNextDownload(void) for (i = 0; i < cl.bsp->numtexinfo; i++) { if (cl.bsp->texinfo[i].c.flags & SURF_NODRAW) continue; - len = Q_concat(fn, sizeof(fn), "textures/", cl.bsp->texinfo[i].name, ".jpg"); - check_file_len(fn, len, DL_OTHER); + if (r_override_textures->integer == 2 || (r_texture_overrides->integer & 16)) { + len = Q_concat(fn, sizeof(fn), "textures/", cl.bsp->texinfo[i].name, ".jpg"); + check_file_len(fn, len, DL_OTHER); + } len = Q_concat(fn, sizeof(fn), "textures/", cl.bsp->texinfo[i].name, ".wal"); check_file_len(fn, len, DL_OTHER); } diff --git a/src/refresh/images.c b/src/refresh/images.c index 88b205dbe..229f2a600 100644 --- a/src/refresh/images.c +++ b/src/refresh/images.c @@ -1478,9 +1478,7 @@ static const struct { static imageformat_t img_search[IM_MAX]; static int img_total; -static cvar_t *r_override_textures; static cvar_t *r_texture_formats; -static cvar_t *r_texture_overrides; #endif static cvar_t *r_glowmaps; From 7fe7aa0ff8bb7285cb475468170a78d904df488c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 Jan 2025 08:55:49 -0500 Subject: [PATCH 954/974] Variety of null/value checks (#244) --- src/action/a_ctf.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index e2c6fd4da..8a495d93a 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -235,6 +235,9 @@ void CTFSetTeamSpawns(int team, char *str) if(team == TEAM2) team_spawn_name = "info_player_team2"; + if (team != TEAM1 && team != TEAM2) + return; + /* find and remove all team spawns for this team */ while ((spawn = G_Find(spawn, FOFS(classname), team_spawn_name)) != NULL) { G_FreeEdict (spawn); @@ -296,7 +299,10 @@ static void ResetRespawnTime(int team) { void CTFDynamicRespawnTimer(void) { - if (!ctf_dyn_respawn->value) + if (!ctf_dyn_respawn || !ctf_dyn_respawn->value) + return; + + if (ctfgame.team1 < 0 || ctfgame.team2 < 0) return; int score_diff = abs(ctfgame.team1 - ctfgame.team2); @@ -548,7 +554,7 @@ void CTFFragBonuses(edict_t * targ, edict_t * inflictor, edict_t * attacker) carrier = NULL; // NULL checks - if (!targ || !inflictor || !attacker) + if (!targ || !targ->client || !targ->inuse || !inflictor || !attacker || !attacker->client || !attacker->inuse) return; // no bonus for fragging yourself @@ -721,6 +727,9 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) /* FIXME: players shouldn't be able to touch flags before LCA! */ if(!team_round_going) return false; + + if (!ent || !other || !other->client) + return false; // figure out what team this flag is if (strcmp(ent->classname, "item_flag_team1") == 0) @@ -974,6 +983,9 @@ void CTFFlagSetup(edict_t * ent) void CTFEffects(edict_t * player) { + if (!player || !player->client || !player->inuse) + return; + player->s.effects &= ~(EF_FLAG1 | EF_FLAG2); // megahealth players glow anyway @@ -1337,6 +1349,9 @@ void CTFCapReward(edict_t * ent) if(!ctf_mode->value) return; + if (!ent || !ent->client || !ent->inuse) + return; + if(ctf_mode->value > 1) ent->client->resp.ctf_capstreak++; else /* capstreak is used as a multiplier so default it to one */ @@ -1486,6 +1501,9 @@ void CTFCapReward(edict_t * ent) void CTFSetupStatusbar( void ) { + if (!level.statusbar) + return; + // Frags closer to the team scores Q_strncatz(level.statusbar, "xr -76 yb -188 num 3 14 ", sizeof(level.statusbar)); From cc33a552303186d6200b5b1045cd821e2f33bdaa Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 15 Jan 2025 08:56:48 -0500 Subject: [PATCH 955/974] Added knife_catch (#224) --- doc/action.md | 6 ++++++ src/action/g_local.h | 2 ++ src/action/g_main.c | 2 +- src/action/g_save.c | 1 + src/action/g_weapon.c | 15 ++++++++++++++- 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/doc/action.md b/doc/action.md index 9b1d7618d..a0e326708 100644 --- a/doc/action.md +++ b/doc/action.md @@ -80,6 +80,7 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Weapon Banning](#weapon-banning) - [Item Banning](#item-banning) - [Weapon/Item Counts](#weaponitem-counts) + - [Knife Catching](#knife-catching) - [Teamkilling after a Round](#teamkilling-after-a-round) - [New IR Vision](#new-ir-vision) - [Radio and Voice Flood Protection](#radio-and-voice-flood-protection) @@ -501,6 +502,11 @@ Normally, AQ2 allows one item and one weapon. These settings can adjust that so - `allitem [0/1]` - default 0, if enabled, all players spawn with all items, with exception of any item bans - `allow_hoarding [#]` - default 0, if enabled, allows players to pick up more than 1 special item or weapon, but does not spawn with them, like allweapon or allitem will do +### Knife Catching +Ever wish you could catch a knife thrown at you like they did in all the cool action movies? Well now you can! Ask your server operator to enable `knife_catch`! You only have a couple of frames to make this happen, so timing is important! +- `knife_catch [0/1/2]` - default `0` (classic, no catching possible). + - If set to `1`, use the `punch` command to catch a knife in-flight. This will add 1 knife to your inventory and prevent all damage. + - If set to `2`, use the `punch` command to catch a knife in-flight, and immediately throw it back where your crosshair is, just like in Big Trouble in Little China! This prevents all damage, but you do not get an extra knife in your inventory (because you just threw it back!) ### Teamkilling after a Round To add a little more fun in the game, we've added an option which will allow a team to TK each other after a round has ended during Teamplay. This is only available to servers where FF is off. diff --git a/src/action/g_local.h b/src/action/g_local.h index e6c02c36e..505b78d75 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1373,6 +1373,7 @@ extern cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA extern cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit +extern cvar_t *knife_catch; // Enables or disables knife catching extern cvar_t *grenade_drop; // Allows grenades to be dropped on death #ifdef AQTION_EXTENSION @@ -1766,6 +1767,7 @@ void Weapon_Generic( edict_t * ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST int *pause_frames, int *fire_frames, void( *fire ) (edict_t * ent) ); void PlayWeaponSound( edict_t *ent ); +int Knife_Fire(edict_t* ent); void P_ProjectSource(gclient_t *client, vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result); void weapon_grenade_fire(edict_t* ent, qboolean held); diff --git a/src/action/g_main.c b/src/action/g_main.c index 3b7731bb3..5350a3e33 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -583,9 +583,9 @@ cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bot cvar_t *lca_grenade; // Allows grenade pin pulling during LCA cvar_t *breakableglass; // Moved from cgf_sfx_glass, enables breakable glass (0,1,2) cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment limit +cvar_t *knife_catch; // Enables knife catching cvar_t *grenade_drop; // Allows grenades to be dropped on death - #ifdef AQTION_EXTENSION cvar_t *use_newirvision; cvar_t *use_indicators; diff --git a/src/action/g_save.c b/src/action/g_save.c index cd5d0afeb..1db9cfec5 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -677,6 +677,7 @@ void InitGame( void ) g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); g_highscores_countbots = gi.cvar("g_highscores_countbots", "0", 0); lca_grenade = gi.cvar("lca_grenade", "0", 0); + knife_catch = gi.cvar("knife_catch", "0", 0); // new AQtion Extension cvars diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index 301763d9f..a83d38be2 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -1131,7 +1131,20 @@ void knife_touch(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf if( other->client && (INV_AMMO(other,KNIFE_NUM) < other->client->knife_max) ) INV_AMMO(other,KNIFE_NUM) ++; - T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, plane->normal, ent->dmg, 0, 0, MOD_KNIFE_THROWN); + if (knife_catch->value && !other->is_bot) { // 3 frames to catch the knife + if (other->client->punch_framenum >= (level.framenum - 3)) { + gi.cprintf(other, PRINT_HIGH, "You caught a knife!\n"); + gi.cprintf(ent->owner, PRINT_HIGH, "%s caught your knife!\n", other->client->pers.netname); + + if (knife_catch->value == 2) + //Throw it back! + Knife_Fire(other); + } else { + T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, plane->normal, ent->dmg, 0, 0, MOD_KNIFE_THROWN); + } + } else { + T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, plane->normal, ent->dmg, 0, 0, MOD_KNIFE_THROWN); + } } else { From d2efa5ba1aa4400f70cb85030dea9deeabb831b7 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Wed, 14 May 2025 09:38:20 -0400 Subject: [PATCH 956/974] Fix uncommon spawnpoints (#255) * Fixed one more warning * This should fix maps with only one info_player_start spawn * trying to fix a meson bug * trying to fix a meson bug * trying to fix a meson bug * trying to fix a meson bug * trying to fix a meson bug --- meson.build | 16 ++++++-- src/action/p_client.c | 90 +++++++++++++++++++++++++------------------ src/refresh/main.c | 35 +++++++++++------ 3 files changed, 89 insertions(+), 52 deletions(-) diff --git a/meson.build b/meson.build index 70fe4dbf2..3fd39b7bc 100644 --- a/meson.build +++ b/meson.build @@ -404,11 +404,19 @@ client_deps = [png, curl, sdl2] server_deps = [] game_deps = [zlib] +# Statically link if we're MacOS +if host_machine.system() == 'darwin' + jpeg = dependency('libjpeg', + required: get_option('libjpeg'), + default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], + static: true + ) +else jpeg = dependency('libjpeg', - required: get_option('libjpeg'), - default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], - static: true -) + required: get_option('libjpeg'), + default_options: fallback_opt + [ 'jpeg-turbo=disabled', 'tests=disabled' ], + ) +endif if jpeg.found() if jpeg.type_name() == 'internal' diff --git a/src/action/p_client.c b/src/action/p_client.c index 5767fa910..20a45821a 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -2176,53 +2176,69 @@ cannot spawn. edict_t *UncommonSpawnPoint(void) { edict_t *spot = NULL; + edict_t *first_valid_spot = NULL; - if (!spot) { - gi.dprintf("Warning: failed to find deathmatch spawn point, unexpected spawns will be utilized\n"); - - /* - Try all possible classes of spawn points, and use DM weapon spawns as a last resort. - */ - char* spawnpoints[] = { - "info_player_start", - "info_player_coop", - "info_player_team1", - "info_player_team2", - "info_player_team3", - "info_player_deathmatch", - "weapon_bfg", - "weapon_chaingun", - "weapon_machinegun", - "weapon_rocketlauncher", - "weapon_shotgun", - "weapon_supershotgun", - "weapon_railgun" - }; - size_t num_spawnpoints = sizeof(spawnpoints) / sizeof(spawnpoints[0]); - int i; - for (i = 0; i < num_spawnpoints; ++i) { - while ((spot = G_Find(spot, FOFS(classname), spawnpoints[i])) != NULL) { - if (!game.spawnpoint[0] && !spot->targetname) - break; - - if (!game.spawnpoint[0] || !spot->targetname) - continue; + /* + Try all possible classes of spawn points, and use DM weapon spawns as a last resort. + */ + char* spawnpoints[] = { + "info_player_start", + "info_player_coop", + "info_player_team1", + "info_player_team2", + "info_player_team3", + "info_player_deathmatch", + "weapon_bfg", + "weapon_chaingun", + "weapon_machinegun", + "weapon_rocketlauncher", + "weapon_shotgun", + "weapon_supershotgun", + "weapon_railgun" + }; + size_t num_spawnpoints = sizeof(spawnpoints) / sizeof(spawnpoints[0]); + + // Try each spawn point type in order + for (int i = 0; i < num_spawnpoints; ++i) { + spot = NULL; // Reset spot for each new spawn point type + + // Find all entities of this type + while ((spot = G_Find(spot, FOFS(classname), spawnpoints[i])) != NULL) { + // Save the first valid spot we find of any type as a fallback + if (!first_valid_spot) { + first_valid_spot = spot; + } - if (Q_stricmp(game.spawnpoint, spot->targetname) == 0) - break; + // If no specific spawn point is requested, any entity without a targetname will do + if (!game.spawnpoint[0] && !spot->targetname) { + gi.dprintf("Found spawn point of class %s\n", spawnpoints[i]); + return spot; } - if (spot) { - gi.dprintf("Warning: Uncommon spawn point of class %s\n", spawnpoints[i]); - gi.dprintf("**If you are the map author, you need to be utilizing MULTIPLE info_player_deathmatch or info_player_team entities**\n"); - break; + // If a specific spawn point is requested, match by targetname + if (game.spawnpoint[0] && spot->targetname && + (Q_stricmp(game.spawnpoint, spot->targetname) == 0)) { + gi.dprintf("Found requested spawn point %s of class %s\n", + game.spawnpoint, spawnpoints[i]); + return spot; } } } - return spot; + // If we get here, we didn't find an ideal spawn point, but we might have a fallback + if (first_valid_spot) { + gi.dprintf("Warning: failed to find ideal deathmatch spawn point, using fallback spawn\n"); + gi.dprintf("**If you are the map author, you need to be utilizing MULTIPLE info_player_deathmatch or info_player_team entities**\n"); + return first_valid_spot; + } + + // Truly no spawn points found + gi.dprintf("Warning: failed to find ANY spawn point, map is not playable\n"); + return NULL; } + + edict_t *SelectCoopSpawnPoint(edict_t *ent) { int index; diff --git a/src/refresh/main.c b/src/refresh/main.c index be18accf9..ddf0ba9d7 100644 --- a/src/refresh/main.c +++ b/src/refresh/main.c @@ -472,10 +472,15 @@ void GL_DrawLine(vec3_t verts, const int num_points, const uint32_t* colors, con //GL_StateBits(GLS_BLEND_BLEND | GLS_DEPTHMASK_FALSE); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); //GL_VertexPointer(3, 0, &verts[0][0]); - GL_VertexPointer(3, 0, &v[0][0]); + if(!gl_shaders->value) { + GL_VertexPointer(3, 0, &v[0][0]); + } + glLineWidth(line_width); // Set the line width - GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + if(!gl_shaders->value) { + GL_ColorBytePointer(4, 0, (GLubyte*)colors); // Set the color of the line + } if (occluded) GL_DepthRange(0, 1); // Set the far clipping plane to 1 (obscured behind walls) else @@ -514,8 +519,10 @@ void GL_DrawCross(vec3_t origin, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)colors); - GL_VertexPointer(3, 0, &points[0][0]); + if(!gl_shaders->value) { + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); + } glLineWidth(1); // Set the line width if (occluded) @@ -667,7 +674,7 @@ static qboolean ALLOC_BoxPoints(int num_boxes) } else if (num_boxes != drawbox_total) { - if (box_points[0] == NULL || box_colors[0] == NULL) + if (box_points == NULL || box_colors == NULL) return false; if_null_1 = box_points; @@ -787,8 +794,10 @@ void GL_DrawBox(vec3_t origin, uint32_t color, vec3_t mins, vec3_t maxs, qboolea GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)colors); - GL_VertexPointer(3, 0, &points[0][0]); + if(!gl_shaders->value) { + GL_ColorBytePointer(4, 0, (GLubyte*)colors); + GL_VertexPointer(3, 0, &points[0][0]); + } glLineWidth(2); // Set the line width if (occluded) @@ -875,8 +884,10 @@ static void GL_BatchDrawBoxes(int num_boxes, qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); - GL_VertexPointer(3, 0, &box_points[0][0]); + if(!gl_shaders->value) { + GL_ColorBytePointer(4, 0, (GLubyte*)box_colors); + GL_VertexPointer(3, 0, &box_points[0][0]); + } glLineWidth(2); // Set the line width if (occluded) @@ -1202,8 +1213,10 @@ static void GL_BatchDrawArrows(qboolean occluded) GL_BindTexture(0, TEXNUM_WHITE); GL_StateBits(GLS_DEFAULT); GL_ArrayBits(GLA_VERTEX | GLA_COLOR); - GL_VertexPointer(3, 0, &arrow_points[0][0]); - GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line + if(!gl_shaders->value) { + GL_VertexPointer(3, 0, &arrow_points[0][0]); + GL_ColorBytePointer(4, 0, (GLubyte*)arrow_colors); // Set the color of the line + } //glLineWidth(line_width); // Set the line width if (occluded) From ab4bc80257fd325c0a47d2032e7b24ae66ec0945 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 18 Dec 2025 12:39:15 -0500 Subject: [PATCH 957/974] Gamemode fixes (#260) * Moved gamemode server settings to Gamemode() * Fixed some gamemodes, bots serverinfo * Fixed 'atl' and 'etv' settings --- src/action/a_esp.c | 9 ++++++++ src/action/a_team.c | 6 ------ src/action/botlib/botlib_spawn.c | 10 ++------- src/action/g_local.h | 5 ++++- src/action/g_main.c | 4 +++- src/action/g_save.c | 7 ++----- src/action/g_spawn.c | 35 ++++++++++++++++++++------------ src/action/p_client.c | 2 +- src/action/tng_stats.c | 2 +- src/client/ui/servers.c | 4 ++-- 10 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 04b1dabb0..c7aa8ec3c 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -46,8 +46,10 @@ void EspForceEspionage(espmode_t espmode) gi.cvar_forceset("esp", "1"); if (espmode == 0 || esp_atl->value) { espsettings.esp_mode = ESPMODE_ATL; + gi.cvar_forceset(gm->name, "atl"); } else if (espmode == 1) { espsettings.esp_mode = ESPMODE_ETV; + gi.cvar_forceset(gm->name, "etv"); } //gi.dprintf("Espionage mode set to %s\n", (espmode == 0) ? "ATL" : "ETV"); @@ -964,6 +966,13 @@ qboolean EspLoadConfig(const char *mapname) EspForceEspionage(ESPMODE_ATL); } + // Config load happens AFTER g_spawn, updates must occur here + if (espsettings.esp_mode == ESPMODE_ATL) { + gi.cvar_forceset(gm->name, "atl"); + } else if (espsettings.esp_mode == ESPMODE_ETV) { + gi.cvar_forceset(gm->name, "etv"); + } + return true; } diff --git a/src/action/a_team.c b/src/action/a_team.c index 75126ec0b..fc6c98d55 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2375,11 +2375,6 @@ void RunWarmup (void) gi.centerprintf(ent, "WARMUP"); } } - #if USE_AQTION - if (warmup_bots->value){ - gi.cvar_forceset("am", "1"); - } - #endif } void StartRound (void) @@ -2915,7 +2910,6 @@ int CheckTeamRules (void) #if USE_AQTION // Cleanup and remove all bots, it's go time! if (warmup_bots->value){ - gi.cvar_forceset("am", "0"); bot_connections.desire_bots = 0; ACESP_RemoveBot("all"); CenterPrintAll("All bots removed, good luck and have fun!"); diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index c6bfd5354..350c331ee 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2203,12 +2203,6 @@ void BOTLIB_CheckBotRules(void) if (matchmode->value) // Bots never allowed in matchmode return; - - // This is so we automatically report when a server has bots or not - if (bot_connections.desire_bots == 0) - gi.cvar_forceset("am", "0"); // Turn off attract mode - else - gi.cvar_forceset("am", "1"); // Turn on attract mode if (ctf->value) { @@ -2261,9 +2255,9 @@ void BOTLIB_CheckBotRules(void) // Update 'am' cvar to reflect in Server UI list if server has bots or not if (bot_connections.total_bots > 0) { - gi.cvar_forceset(am->name, "1"); + gi.cvar_forceset(bots->name, "1"); } else { - gi.cvar_forceset(am->name, "0"); + gi.cvar_forceset(bots->name, "0"); } } //rekkie -- DEV_1 -- e \ No newline at end of file diff --git a/src/action/g_local.h b/src/action/g_local.h index 505b78d75..475a221da 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1080,6 +1080,7 @@ typedef enum { GM_DOMINATION, GM_ASSASSINATE_THE_LEADER, GM_ESCORT_THE_VIP, + GM_JUMP, GM_MAX } GameMode; @@ -1340,7 +1341,6 @@ extern cvar_t *esp_debug; // Enable or disable debug mode (very spammy) // 2023 extern cvar_t *use_killcounts; // Adjust how kill streaks are counted -extern cvar_t *am; // Enable or disable Attract Mode (ltk bots) extern cvar_t *zoom_comp; // Enable or disable zoom compensation extern cvar_t *item_kit_mode; // Enable or disable item kit mode extern cvar_t *gun_dualmk23_enhance; // Enable or disable enhanced dual mk23s (laser + silencer) @@ -1376,6 +1376,9 @@ extern cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragm extern cvar_t *knife_catch; // Enables or disables knife catching extern cvar_t *grenade_drop; // Allows grenades to be dropped on death +// 2025 +extern cvar_t *bots; // If bots are enabled and in the server + #ifdef AQTION_EXTENSION extern int (*engine_Client_GetVersion)(edict_t *ent); extern int (*engine_Client_GetProtocol)(edict_t *ent); diff --git a/src/action/g_main.c b/src/action/g_main.c index 5350a3e33..61dd7deb5 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -549,7 +549,6 @@ cvar_t *g_spawn_items; // 2023 cvar_t *use_killcounts; // Display kill counts in console to clients on frag -cvar_t *am; // Attract mode toggle cvar_t *zoom_comp; // Compensates zoom-in frames with ping (high ping = fewer frames) cvar_t *item_kit_mode; // Toggles item kit mode cvar_t *gun_dualmk23_enhance; // Enables laser sight for dual mk23 pistols @@ -586,6 +585,9 @@ cvar_t *glassfragmentlimit; // Moved from cgf_sfx_glass, sets glass fragment lim cvar_t *knife_catch; // Enables knife catching cvar_t *grenade_drop; // Allows grenades to be dropped on death +// 2025 +cvar_t *bots; // If bots are enabled and in the server + #ifdef AQTION_EXTENSION cvar_t *use_newirvision; cvar_t *use_indicators; diff --git a/src/action/g_save.c b/src/action/g_save.c index 1db9cfec5..52b17380f 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -594,10 +594,6 @@ void InitGame( void ) // BEGIN AQ2 ETE esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) - if (esp->value) { - atl = gi.cvar( "atl", "1", CVAR_LATCH ); - etv = gi.cvar( "etv", "0", CVAR_LATCH ); - }; esp_atl = gi.cvar( "esp_atl", "0", 0 ); // This forces ATL mode even if ETV mode is set in the .esp file esp_punish = gi.cvar("esp_punish", "0", 0); esp_etv_halftime = gi.cvar("esp_etv_halftime", "0", CVAR_LATCH); @@ -640,7 +636,6 @@ void InitGame( void ) // 2023 use_killcounts = gi.cvar("use_killcounts", "0", 0); - am = gi.cvar("am", "0", CVAR_LATCH | CVAR_SERVERINFO); zoom_comp = gi.cvar("zoom_comp", "1", 0); item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); @@ -679,6 +674,8 @@ void InitGame( void ) lca_grenade = gi.cvar("lca_grenade", "0", 0); knife_catch = gi.cvar("knife_catch", "0", 0); + // 2025 + bots = gi.cvar("bots", "0", CVAR_SERVERINFO); // new AQtion Extension cvars #ifdef AQTION_EXTENSION diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 156287163..7ef06d41f 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1050,20 +1050,34 @@ int Gamemode(void) int gamemode = 0; if (teamdm->value) { gamemode = GM_TEAMDM; + gi.cvar_forceset(gm->name, "tdm"); } else if (ctf->value) { gamemode = GM_CTF; + gi.cvar_forceset(gm->name, "ctf"); } else if (use_tourney->value) { gamemode = GM_TOURNEY; - } else if (teamplay->value) { - gamemode = GM_TEAMPLAY; + gi.cvar_forceset(gm->name, "tourney"); } else if (dom->value) { gamemode = GM_DOMINATION; - } else if (deathmatch->value) { - gamemode = GM_DEATHMATCH; + gi.cvar_forceset(gm->name, "dom"); } else if (esp->value && espsettings.esp_mode == ESPMODE_ATL) { gamemode = GM_ASSASSINATE_THE_LEADER; + //gi.cvar_forceset(gm->name, "atl"); + // Config load happens AFTER g_spawn, updates must occur in a_esp.c } else if (esp->value && espsettings.esp_mode == ESPMODE_ETV) { gamemode = GM_ESCORT_THE_VIP; + //gi.cvar_forceset(gm->name, "etv"); + // Config load happens AFTER g_spawn, updates must occur in a_esp.c + } else if (jump->value) { + gamemode = GM_JUMP; + gi.cvar_forceset(gm->name, "jump"); + } else if (teamplay->value) { + gamemode = GM_TEAMPLAY; + gi.cvar_forceset(gm->name, "tp"); + } else { + // Default to deathmatch if no other matches + gamemode = GM_DEATHMATCH; + gi.cvar_forceset(gm->name, "dm"); } return gamemode; } @@ -1140,7 +1154,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.cvar_forceset(dm_choose->name, "0"); // Turn off dm_choose for jump mode gi.cvar_forceset(uvtime->name, "0"); // Turn off uvtime in jump mode gi.cvar_forceset(unique_items->name, "6"); // Enables holding all items at once, if toggled - gi.cvar_forceset(am->name, "0"); // Turns off attract mode gi.cvar_forceset(ltk_loadbots->name, "0"); // Turns off bots // if (teamplay->value) @@ -1181,7 +1194,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (ctf->value) { - gi.cvar_forceset(gm->name, "ctf"); if (ctf->value == 2) gi.cvar_forceset(ctf->name, "1"); //for now @@ -1232,7 +1244,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (esp->value) { - gi.cvar_forceset(gm->name, "esp"); //gameSettings |= GS_WEAPONCHOOSE; gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); @@ -1281,7 +1292,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (dom->value) { - gi.cvar_forceset(gm->name, "dom"); gameSettings |= GS_WEAPONCHOOSE; if (!teamplay->value) { @@ -1315,7 +1325,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if(teamdm->value) { - gi.cvar_forceset(gm->name, "tdm"); gameSettings |= GS_DEATHMATCH; if (dm_choose->value) @@ -1334,7 +1343,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (use_3teams->value) { - gi.cvar_forceset(gm->name, "tp"); gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); if (!teamplay->value) @@ -1374,7 +1382,6 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (use_tourney->value) { - gi.cvar_forceset(gm->name, "tourney"); gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); if (!teamplay->value) @@ -1385,11 +1392,9 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } else if (teamplay->value) { - gi.cvar_forceset(gm->name, "tp"); gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); } else { //Its deathmatch - gi.cvar_forceset(gm->name, "dm"); gameSettings |= GS_DEATHMATCH; if (dm_choose->value) gameSettings |= GS_WEAPONCHOOSE; @@ -1417,6 +1422,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } } + // Reset unique items if changing from jump to any other mode + if (!jump->value) { + gi.cvar_forceset(unique_items->name, "1"); + } gi.cvar_forceset(maptime->name, "0:00"); diff --git a/src/action/p_client.c b/src/action/p_client.c index 20a45821a..b69cac1d3 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3379,7 +3379,7 @@ STAT_BOT_CHECK(); PrintMOTD(ent); } - if(am->value && game.bot_count > 0){ + if(game.bot_count > 0){ char msg[128]; Q_snprintf(msg, sizeof(msg), "** This server contains BOTS for you to play with until real players join up! Enjoy! **"); gi.centerprintf(ent, "%s", msg); diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index f0b38c81c..9e2d7418d 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -718,7 +718,7 @@ void G_RegisterScore(void) return; } - if (strcmp(gm->string, "tp") == 0 || strcmp(gm->string, "esp") == 0) + if (strcmp(gm->string, "tp") == 0 || strcmp(gm->string, "atl") == 0 || strcmp(gm->string, "etv") == 0) roundbased = true; else roundbased = false; diff --git a/src/client/ui/servers.c b/src/client/ui/servers.c index c870fea0b..795f8547c 100644 --- a/src/client/ui/servers.c +++ b/src/client/ui/servers.c @@ -202,7 +202,7 @@ static uint32_t ColorForStatus(const serverStatus_t *status, unsigned ping) if (Q_stricmp(Info_ValueForKey(status->infostring, "NoFake"), "ENABLED") == 0) return uis.color.disabled.u32; - if (atoi(Info_ValueForKey(status->infostring, "am")) > 0) + if (atoi(Info_ValueForKey(status->infostring, "bots")) > 0) return U32_MAGENTA; if (ping > (ui_colorpingmax->value * 3)) @@ -262,7 +262,7 @@ void UI_StatusEvent(const serverStatus_t *status) const char *am = "No"; #if USE_AQTION // This checks if the server has bots, if so, turn the color of the server to MAGENTA - const char *hasBotsCheck = Info_ValueForKey(status->infostring, "am"); + const char *hasBotsCheck = Info_ValueForKey(status->infostring, "bots"); if (hasBotsCheck == NULL || COM_IsWhite(hasBotsCheck) || *hasBotsCheck == '0') { am = "No"; From 313a9a975f32877f805d5917482fd9b906b7f08c Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 18 Dec 2025 12:39:30 -0500 Subject: [PATCH 958/974] Add missing stats (#258) * Fixed one more warning * Added shot stats for punches and grenade throws --- src/action/g_weapon.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/action/g_weapon.c b/src/action/g_weapon.c index a83d38be2..6778b2a84 100644 --- a/src/action/g_weapon.c +++ b/src/action/g_weapon.c @@ -850,6 +850,9 @@ void fire_grenade2 (edict_t * self, vec3_t start, vec3_t aimdir, int damage, grenade->spawnflags = 1; //grenade->s.sound = gi.soundindex("weapons/hgrenc1b.wav"); + // Track grenade thrown stat + self->client->resp.gunstats[MOD_HG_SPLASH].shots++; + if (timer <= 0) { Grenade_Explode(grenade); } else { @@ -1035,6 +1038,7 @@ void punch_attack(edict_t * ent) } } gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/swish.wav"), 1, ATTN_NORM, 0); + Stats_AddShot(ent, MOD_PUNCH); // animate the punch // can't animate a punch when ducked From 89b2956369e6102919222121b53f2d7438948f3b Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 18 Dec 2025 12:40:15 -0500 Subject: [PATCH 959/974] auto_menu 2 and gl_shader check (#250) * Fixed one more warning * auto_menu 2 prints the menu AFTER the motd is printed --- src/action/a_game.c | 9 +++++++-- src/action/p_client.c | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 3bf074c53..1156087c0 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -702,10 +702,15 @@ void PrintMOTD(edict_t * ent) } } - if (!auto_menu->value || ent->client->pers.menu_shown) { + // Print the MOTD + if (auto_menu->value == 2) { gi.centerprintf(ent, "%s", msg_buf); } else { - gi.cprintf(ent, PRINT_LOW, "%s", msg_buf); + if (!auto_menu->value || ent->client->pers.menu_shown) { + gi.centerprintf(ent, "%s", msg_buf); + } else { + gi.cprintf(ent, PRINT_LOW, "%s", msg_buf); + } } } diff --git a/src/action/p_client.c b/src/action/p_client.c index b69cac1d3..25b8e8909 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -6214,8 +6214,15 @@ void ClientBeginServerFrame(edict_t * ent) } } - // show team or weapon menu immediately when connected - if (auto_menu->value && ent->client->layout != LAYOUT_MENU && !client->pers.menu_shown && (teamplay->value || dm_choose->value)) { + //show team or weapon menu immediately when connected + //gi.dprintf("last refresh: %d, mod refresh: %d, realframenum: %d\n", client->resp.last_motd_refresh, (client->resp.last_motd_refresh * 2), level.realFramenum); + if (auto_menu->value == 2) { + if (level.realFramenum == (ent->client->resp.last_motd_refresh * 2)) { + if (auto_menu->value && ent->client->layout != LAYOUT_MENU && !client->pers.menu_shown && (teamplay->value || dm_choose->value)) { + Cmd_Inven_f( ent ); + } + } + } else if (auto_menu->value == 1 && ent->client->layout != LAYOUT_MENU && !client->pers.menu_shown && (teamplay->value || dm_choose->value)) { Cmd_Inven_f( ent ); } From d66e778cace8585ccd9ed715d8d920ea0ae4ab54 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 18 Dec 2025 12:40:52 -0500 Subject: [PATCH 960/974] Added IS_BOT macro, bot_reportpings cvar (#246) --- doc/action.md | 1 + src/action/a_team.c | 36 +++++++++++++++++++++++++++++++----- src/action/acesrc/acebot.h | 1 + src/action/botlib/botlib.h | 2 ++ src/action/g_main.c | 1 + src/action/g_save.c | 1 + src/action/p_hud.c | 17 +++++++++++++++-- 7 files changed, 52 insertions(+), 7 deletions(-) diff --git a/doc/action.md b/doc/action.md index a0e326708..c556c2e48 100644 --- a/doc/action.md +++ b/doc/action.md @@ -636,6 +636,7 @@ Taking aspects of the existing bots and greatly enhancing their navigation and b - `bot_navautogen [0/1]` - Server cvar, enabling this will auto generate a navmesh for any maps that do not already have one, on map load time. This automatic navmesh is far from perfect, but it does allow bots to traverse maps rather than stand still. A far superior option is to have a handcrafted navmesh for the map, as described below in Client Commands. - `bot_debug [0/1]` - Server cvar, will enable debug messaging for BOTLIB functionality where enabled - `bot_reportasclient [0/1]` - Server cvar, if enabled, will report bots as real clients to server masters. Default is disabled [0] +- `bot_reportpings [0/1]` - Server cvar, if enabled, will simulate bot pings, else it prints BOT instead. Default is disabled [0] **Client Commands:** These commands only work on local map loads, not connections to dedicated servers. They are meant to be used to create navmeshes. You need to have `gl_shaders 0` (GLSL disabled) for this to work, newer shader renderers are incompatible with navmesh generation at this time. diff --git a/src/action/a_team.c b/src/action/a_team.c index fc6c98d55..40b00f8ed 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -3338,13 +3338,24 @@ void A_NewScoreboardMessage(edict_t * ent) cl_ent = g_edicts + 1 + (cl - game.clients); alive = IS_ALIVE(cl_ent); - Q_snprintf( buf, sizeof( buf ), "xv 44 yv %d string%c \"%-15s %3d %3d %3d\"", + char pingstr[8]; + Q_snprintf(pingstr, sizeof(pingstr), "%d", min(cl->ping, 999)); + + #ifndef NO_BOTS + if (IS_BOT(cl_ent)) { + if (!bot_reportasclient->value || !bot_reportpings->value) { + Q_snprintf(pingstr, sizeof(pingstr), "BOT"); + } + } + #endif + + Q_snprintf( buf, sizeof( buf ), "xv 44 yv %d string%c \"%-15s %3d %3d %3s\"", line++ * lineh, (alive && dead ? '2' : ' '), cl->pers.netname, cl->resp.score, (level.framenum - cl->resp.enterframe) / 600 / FRAMEDIV, - min(cl->ping, 999) ); + pingstr ); Q_strncatz( string, buf, sizeof( string ) ); printCount++; if (printCount >= maxPlayers) @@ -3617,15 +3628,26 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) playername[1] = 0; } Q_strncatz(playername, cl->pers.netname, sizeof(playername)); + + char pingstr[8]; + Q_snprintf(pingstr, sizeof(pingstr), "%d", min(cl->ping, 999)); + + #ifndef NO_BOTS + if (IS_BOT(cl_ent)) { + if (!bot_reportasclient->value || !bot_reportpings->value) { + Q_snprintf(pingstr, sizeof(pingstr), "BOT"); + } + } + #endif if (showExtra) { sprintf( string + len, - "yv %d string%s \"%-15s %3d %3d %3d\" ", + "yv %d string%s \"%-15s %3d %3d %3s\" ", line_y, (deadview && cl_ent->solid != SOLID_NOT) ? "2" : "", playername, cl->resp.score, (level.framenum - cl->resp.enterframe) / (60 * HZ), - min(cl->ping, 999) ); + pingstr ); } else { sprintf( string + len, "yv %i string%s \"%s\" ", @@ -3871,11 +3893,15 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer) { #ifndef NO_BOTS //rekkie -- Fake Bot Client -- s - if (cl_ent->is_bot) + if (IS_BOT(cl_ent)) { if (bot_reportasclient->value) Q_snprintf(buf, sizeof(buf), "%4i", min(9999, cl_ent->bot.bot_ping)); else Q_snprintf(buf, sizeof(buf), " BOT"); + + if (!bot_reportpings->value) + Q_snprintf(buf, sizeof(buf), " BOT"); + } //if (0) //rekkie -- Fake Bot Client -- e //if( cl_ent->is_bot ) diff --git a/src/action/acesrc/acebot.h b/src/action/acesrc/acebot.h index 2d7063511..871a0d355 100644 --- a/src/action/acesrc/acebot.h +++ b/src/action/acesrc/acebot.h @@ -554,6 +554,7 @@ extern cvar_t* bot_count_min; extern cvar_t* bot_count_max; extern cvar_t* bot_rotate; extern cvar_t* bot_reportasclient; +extern cvar_t* bot_reportpings; extern cvar_t* bot_navautogen; //extern cvar_t* bot_randteamskin; extern cvar_t* gl_shaders; diff --git a/src/action/botlib/botlib.h b/src/action/botlib/botlib.h index d0a3184c0..1fa1d6ad2 100644 --- a/src/action/botlib/botlib.h +++ b/src/action/botlib/botlib.h @@ -8,6 +8,8 @@ #define BOT_NAV_VERSION_2 2 #define BOT_NAV_VERSION_MAX BOT_NAV_VERSION_2 +#define IS_BOT(ent) (ent->is_bot) + // Bot move states typedef enum { diff --git a/src/action/g_main.c b/src/action/g_main.c index 61dd7deb5..df1bfc7bd 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -506,6 +506,7 @@ cvar_t* bot_count_min; // Minimum number of bots to keep on the server (will ran cvar_t* bot_count_max; // Maximum number of bots to keep on the server (will range between this and bot_count_min) cvar_t* bot_rotate; // Disable/enable rotating bots on the server cvar_t* bot_reportasclient; // Report bots as clients to the server browser +cvar_t* bot_reportpings; // Report bots simulated pings cvar_t* bot_navautogen; // Enable/Disable automatic generation of navigation files //cvar_t* bot_randteamskin; // Bots can randomize team skins each map diff --git a/src/action/g_save.c b/src/action/g_save.c index 52b17380f..ceca11ea3 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -722,6 +722,7 @@ void InitGame( void ) bot_count_max = gi.cvar("bot_count_max", "0", 0); bot_rotate = gi.cvar("bot_rotate", "0", 0); bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH); + bot_reportpings = gi.cvar("bot_reportpings", "0", 0); bot_navautogen = gi.cvar("bot_navautogen", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); gl_shaders = gi.cvar("gl_shaders", "0", 0); diff --git a/src/action/p_hud.c b/src/action/p_hud.c index 389411ca6..125a5d7a0 100644 --- a/src/action/p_hud.c +++ b/src/action/p_hud.c @@ -370,10 +370,23 @@ void DeathmatchScoreboardMessage (edict_t * ent, edict_t * killer) stringlength += j; } + // Convert ping to string so we can report pings as BOT if needed + char pingstr[8]; + Q_snprintf(pingstr, sizeof(pingstr), "%d", min(cl->ping, 999)); + + #ifndef NO_BOTS + if (IS_BOT(cl_ent)) { + if (!bot_reportasclient->value || !bot_reportpings->value) { + Q_snprintf(pingstr, sizeof(pingstr), "BOT"); + } + } + #endif + // send the layout Q_snprintf (entry, sizeof (entry), - "client %i %i %i %i %i %i ", - x, y, (int)(cl - game.clients), cl->resp.score, cl->ping, + "client %i %i %i %i %s %i ", + x, y, (int)(cl - game.clients), cl->resp.score, + pingstr, (level.framenum - cl->resp.enterframe) / 600 / FRAMEDIV); j = strlen (entry); if (stringlength + j > 1023) From 51904605a3b4e285af7fbe6c2da91c5d4a08921e Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Thu, 18 Dec 2025 12:42:58 -0500 Subject: [PATCH 961/974] Minor CTF revamp (#247) * ctf_rewards added, replacing ctf_mode, fixed stat collection * Models disappeared somehow * vwep works * Limiting briefcase pickup to guns isn't working yet * Added two todos --- src/action/a_ctf.c | 246 +++++++++++++++++++++++++++++------------ src/action/a_dom.c | 2 +- src/action/a_esp.c | 2 +- src/action/g_items.c | 47 +++++++- src/action/g_local.h | 1 + src/action/g_main.c | 1 + src/action/g_misc.c | 40 ++++--- src/action/g_save.c | 3 +- src/action/g_spawn.c | 24 +++- src/action/p_weapon.c | 16 +++ src/action/tng_stats.c | 5 +- 11 files changed, 286 insertions(+), 101 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 8a495d93a..324a14296 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -70,8 +70,15 @@ gitem_t *team_flag[TEAM_TOP]; void CTFInit(void) { - team_flag[TEAM1] = FindItemByClassname("item_flag_team1"); - team_flag[TEAM2] = FindItemByClassname("item_flag_team2"); + + if (ctf_mode->value) { // Capture the Briefcase + team_flag[TEAM1] = FindItemByClassname("item_bcase_team1"); + team_flag[TEAM2] = FindItemByClassname("item_bcase_team2"); + } + else { // Traditional CTF + team_flag[TEAM1] = FindItemByClassname("item_flag_team1"); + team_flag[TEAM2] = FindItemByClassname("item_flag_team2"); + } memset(&ctfgame, 0, sizeof(ctfgame)); } @@ -197,9 +204,15 @@ void CTFSetFlag(int team, char *str) vec3_t position; if(team == TEAM1) - flag_name = "item_flag_team1"; + if (ctf_mode->value) + flag_name = "item_bcase_team1"; + else + flag_name = "item_flag_team1"; else if(team == TEAM2) - flag_name = "item_flag_team2"; + if (ctf_mode->value) + flag_name = "item_bcase_team2"; + else + flag_name = "item_flag_team2"; else return; @@ -723,6 +736,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) int team, i; edict_t *player; gitem_t *flag_item, *enemy_flag_item; + char flag_name[16] = "flag"; /* FIXME: players shouldn't be able to touch flags before LCA! */ if(!team_round_going) @@ -731,13 +745,20 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) if (!ent || !other || !other->client) return false; + if (ctf_mode->value) + strcpy(flag_name, "briefcase"); + // figure out what team this flag is - if (strcmp(ent->classname, "item_flag_team1") == 0) + if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team1") == 0) + team = TEAM1; + else if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team2") == 0) + team = TEAM2; + else if (strcmp(ent->classname, "item_flag_team1") == 0) team = TEAM1; else if (strcmp(ent->classname, "item_flag_team2") == 0) team = TEAM2; else { - gi.cprintf(ent, PRINT_HIGH, "Don't know what team the flag is on.\n"); + gi.cprintf(ent, PRINT_HIGH, "Don't know what team the %s is on.\n", flag_name); return false; } @@ -756,11 +777,10 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // the flag is at home base. if the player has the enemy // flag, he's just won! if (other->client->inventory[ITEM_INDEX(enemy_flag_item)]) { - gi.bprintf(PRINT_HIGH, "%s captured the %s flag!\n", - other->client->pers.netname, CTFOtherTeamName(team)); - IRC_printf(IRC_T_GAME, "%n captured the %n flag!\n", - other->client->pers.netname, - CTFOtherTeamName(team)); + gi.bprintf(PRINT_HIGH, "%s captured the %s %s!\n", + other->client->pers.netname, CTFOtherTeamName(team), flag_name); + IRC_printf(IRC_T_GAME, "%n captured the %n %s!\n", + other->client->pers.netname, CTFOtherTeamName(team), flag_name); other->client->inventory[ITEM_INDEX(enemy_flag_item)] = 0; other->client->ctf_hasflag = false; @@ -779,8 +799,12 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // other gets another 10 frag bonus other->client->resp.score += CTF_CAPTURE_BONUS; other->client->resp.ctf_caps++; + other->client->resp.ctf_capstreak++; + + LOG_CAPTURE(other); // other is the player who capped the flag - CTFCapReward(other); + if(ctf_rewards->value) // extra ctf awards! + CTFCapReward(other); // Ok, let's do the player loop, hand out the bonuses for (i = 1; i <= game.maxclients; i++) { @@ -797,34 +821,34 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) if (player->client->resp.ctf_lastreturnedflag + CTF_RETURN_FLAG_ASSIST_TIMEOUT * HZ > level.framenum) { gi.bprintf(PRINT_HIGH, - "%s gets an assist for returning the flag!\n", - player->client->pers.netname); + "%s gets an assist for returning the %s!\n", + player->client->pers.netname, flag_name); IRC_printf(IRC_T_GAME, - "%n gets an assist for returning the flag!\n", - player->client->pers.netname); + "%n gets an assist for returning the %s!\n", + player->client->pers.netname, flag_name); player->client->resp.score += CTF_RETURN_FLAG_ASSIST_BONUS; } if (player->client->resp.ctf_lastfraggedcarrier + CTF_FRAG_CARRIER_ASSIST_TIMEOUT * HZ > level.framenum) { gi.bprintf(PRINT_HIGH, - "%s gets an assist for fragging the flag carrier!\n", - player->client->pers.netname); + "%s gets an assist for fragging the %s carrier!\n", + player->client->pers.netname, flag_name); IRC_printf(IRC_T_GAME, - "%n gets an assist for fragging the flag carrier!\n", - player->client->pers.netname); + "%n gets an assist for fragging the %s carrier!\n", + player->client->pers.netname, flag_name); player->client->resp.score += CTF_FRAG_CARRIER_ASSIST_BONUS; } } } - CTFResetFlags(); return false; } return false; // its at home base already } + // hey, its not home. return it by teleporting it back - gi.bprintf(PRINT_HIGH, "%s returned the %s flag!\n", other->client->pers.netname, CTFTeamName(team)); - IRC_printf(IRC_T_GAME, "%n returned the %s flag!\n", other->client->pers.netname, CTFTeamName(team)); + gi.bprintf(PRINT_HIGH, "%s returned the %s %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); + IRC_printf(IRC_T_GAME, "%n returned the %s %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); other->client->resp.score += CTF_RECOVERY_BONUS; other->client->resp.ctf_lastreturnedflag = level.framenum; @@ -834,16 +858,36 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) CTFResetFlag(team); return false; } + // AQ2:TNG - JBravo adding UVtime + + // Get the flag, go go go! + + // TODO: Make this work + // in CTB mode, you must have a hand free to pick up the briefcase... + if (ctf_mode->value && + other->client->curr_weap != MK23_NUM || + other->client->curr_weap != KNIFE_NUM || + other->client->curr_weap != GRENADE_NUM ){ + gi.centerprintf(other, "You must have a free hand to pick up the %s!\n", flag_name); + return false; + } + if (other->client->uvTime) { other->client->uvTime = 0; - gi.centerprintf(other, "Flag taken! Shields are DOWN! Run for it!"); + if (ctf_mode->value) + gi.centerprintf(other, "Flag taken! Shields are DOWN! Run for it!"); + else + gi.centerprintf(other, "Briefcase taken! Shields are DOWN! Run for it!"); } else { - gi.centerprintf(other, "You've got the ENEMY FLAG! Run for it!"); + if (ctf_mode->value) + gi.centerprintf(other, "You've got the ENEMY BRIEFCASE! Run for it!"); + else + gi.centerprintf(other, "You've got the ENEMY FLAG! Run for it!"); } // hey, its not our flag, pick it up - gi.bprintf(PRINT_HIGH, "%s got the %s flag!\n", other->client->pers.netname, CTFTeamName(team)); - IRC_printf(IRC_T_GAME, "%n got the %n flag!\n", other->client->pers.netname, CTFTeamName(team)); + gi.bprintf(PRINT_HIGH, "%s got the %s %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); + IRC_printf(IRC_T_GAME, "%n got the %n %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); other->client->resp.score += CTF_FLAG_BONUS; other->client->inventory[ITEM_INDEX(flag_item)] = 1; @@ -858,6 +902,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) ent->svflags |= SVF_NOCLIENT; ent->solid = SOLID_NOT; } + return true; } @@ -874,14 +919,26 @@ static void CTFDropFlagThink(edict_t * ent) { // auto return the flag // reset flag will remove ourselves - if (strcmp(ent->classname, "item_flag_team1") == 0) { - CTFResetFlag(TEAM1); - gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM1)); - IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM1)); - } else if (strcmp(ent->classname, "item_flag_team2") == 0) { - CTFResetFlag(TEAM2); - gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM2)); - IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM2)); + if (ctf_mode->value) { + if (strcmp(ent->classname, "item_bcase_team1") == 0) { + CTFResetFlag(TEAM1); + gi.bprintf(PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName(TEAM1)); + IRC_printf(IRC_T_GAME, "The %n briefcase has returned!\n", CTFTeamName(TEAM1)); + } else if (strcmp(ent->classname, "item_bcase_team2") == 0) { + CTFResetFlag(TEAM2); + gi.bprintf(PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName(TEAM2)); + IRC_printf(IRC_T_GAME, "The %n briefcase has returned!\n", CTFTeamName(TEAM2)); + } + } else { + if (strcmp(ent->classname, "item_flag_team1") == 0) { + CTFResetFlag(TEAM1); + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM1)); + IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM1)); + } else if (strcmp(ent->classname, "item_flag_team2") == 0) { + CTFResetFlag(TEAM2); + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM2)); + IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM2)); + } } } @@ -890,17 +947,31 @@ void CTFDeadDropFlag(edict_t * self) { edict_t *dropped = NULL; - if (self->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { - dropped = Drop_Item(self, team_flag[TEAM1]); - self->client->inventory[ITEM_INDEX(team_flag[TEAM1])] = 0; - gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", self->client->pers.netname, CTFTeamName(TEAM1)); - IRC_printf(IRC_T_GAME, "%n lost the %n flag!\n", self->client->pers.netname, CTFTeamName(TEAM1)); - - } else if (self->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) { - dropped = Drop_Item(self, team_flag[TEAM2]); - self->client->inventory[ITEM_INDEX(team_flag[TEAM2])] = 0; - gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", self->client->pers.netname, CTFTeamName(TEAM2)); - IRC_printf(IRC_T_GAME, "%n lost the %n flag!\n", self->client->pers.netname, CTFTeamName(TEAM2)); + if (ctf_mode->value) { + if (self->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { + dropped = Drop_Item(self, team_flag[TEAM1]); + self->client->inventory[ITEM_INDEX(team_flag[TEAM1])] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s!\n", self->client->pers.netname, team_flag[TEAM1]->pickup_name); + IRC_printf(IRC_T_GAME, "%n lost the %n!\n", self->client->pers.netname, team_flag[TEAM1]->pickup_name); + } else if (self->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) { + dropped = Drop_Item(self, team_flag[TEAM2]); + self->client->inventory[ITEM_INDEX(team_flag[TEAM2])] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s!\n", self->client->pers.netname, team_flag[TEAM2]->pickup_name); + IRC_printf(IRC_T_GAME, "%n lost the %n!\n", self->client->pers.netname, team_flag[TEAM2]->pickup_name); + } + } else { + if (self->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { + dropped = Drop_Item(self, team_flag[TEAM1]); + self->client->inventory[ITEM_INDEX(team_flag[TEAM1])] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", self->client->pers.netname, CTFTeamName(TEAM1)); + IRC_printf(IRC_T_GAME, "%n lost the %n flag!\n", self->client->pers.netname, CTFTeamName(TEAM1)); + + } else if (self->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) { + dropped = Drop_Item(self, team_flag[TEAM2]); + self->client->inventory[ITEM_INDEX(team_flag[TEAM2])] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", self->client->pers.netname, CTFTeamName(TEAM2)); + IRC_printf(IRC_T_GAME, "%n lost the %n flag!\n", self->client->pers.netname, CTFTeamName(TEAM2)); + } } if (dropped) { @@ -993,17 +1064,37 @@ void CTFEffects(edict_t * player) player->s.effects |= EF_TAGTRAIL; player->s.modelindex3 = 0; - if (player->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) - { - player->s.modelindex3 = gi.modelindex("models/flags/flag1.md2"); - if (player->health > 0) - player->s.effects |= EF_FLAG1; - } - else if (player->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) - { - player->s.modelindex3 = gi.modelindex("models/flags/flag2.md2"); - if (player->health > 0) - player->s.effects |= EF_FLAG2; + char *model = Info_ValueForKey(player->client->pers.userinfo, "skin"); + char *slash = strchr(model, '/'); + if (slash) + *slash = '\0'; + + char t1modelpath[MAX_QPATH] = ""; + char t2modelpath[MAX_QPATH] = ""; + + // This sets the briefcase vwep in place of your weapon in CTB mode + if (ctf_mode->value) { + if (player->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { + Q_snprintf(t1modelpath, sizeof(t1modelpath), "players/%s/w_bc1.md2", model); + player->s.modelindex2 = gi.modelindex(t1modelpath); + if (player->health > 0) + player->s.effects |= EF_FLAG1; + } else if (player->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) { + Q_snprintf(t2modelpath, sizeof(t2modelpath), "players/%s/w_bc2.md2", model); + player->s.modelindex2 = gi.modelindex(t2modelpath); + if (player->health > 0) + player->s.effects |= EF_FLAG2; + } + } else { + if (player->client->inventory[ITEM_INDEX(team_flag[TEAM1])]){ + player->s.modelindex3 = gi.modelindex("models/flags/flag1.md2"); + if (player->health > 0) + player->s.effects |= EF_FLAG1; + } else if (player->client->inventory[ITEM_INDEX(team_flag[TEAM2])]) { + player->s.modelindex3 = gi.modelindex("models/flags/flag2.md2"); + if (player->health > 0) + player->s.effects |= EF_FLAG2; + } } } @@ -1320,17 +1411,32 @@ void CTFDestroyFlag(edict_t * self) { //flags are important if (ctf->value) { - if (strcmp(self->classname, "item_flag_team1") == 0) { - CTFResetFlag(TEAM1); // this will free self! - gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM1)); - IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM1)); - return; - } - if (strcmp(self->classname, "item_flag_team2") == 0) { - CTFResetFlag(TEAM2); // this will free self! - gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM2)); - IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM2)); - return; + if (ctf_mode->value) { + if (strcmp(self->classname, "item_bcase_team1") == 0) { + CTFResetFlag(TEAM1); // this will free self! + gi.bprintf(PRINT_HIGH, "The %s %s has returned!\n", CTFTeamName(TEAM1), team_flag[TEAM1]->pickup_name); + IRC_printf(IRC_T_GAME, "The %n %n has returned!\n", CTFTeamName(TEAM1), team_flag[TEAM1]->pickup_name); + return; + } + if (strcmp(self->classname, "item_bcase_team2") == 0) { + CTFResetFlag(TEAM2); // this will free self! + gi.bprintf(PRINT_HIGH, "The %s %s has returned!\n", CTFTeamName(TEAM2), team_flag[TEAM2]->pickup_name); + IRC_printf(IRC_T_GAME, "The %n %n has returned!\n", CTFTeamName(TEAM2), team_flag[TEAM2]->pickup_name); + return; + } + } else { + if (strcmp(self->classname, "item_flag_team1") == 0) { + CTFResetFlag(TEAM1); // this will free self! + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM1)); + IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM1)); + return; + } + if (strcmp(self->classname, "item_flag_team2") == 0) { + CTFResetFlag(TEAM2); // this will free self! + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName(TEAM2)); + IRC_printf(IRC_T_GAME, "The %n flag has returned!\n", CTFTeamName(TEAM2)); + return; + } } } // just release it. @@ -1346,7 +1452,7 @@ void CTFCapReward(edict_t * ent) int band; int player_weapon; - if(!ctf_mode->value) + if(!ctf_rewards->value) return; if (!ent || !ent->client || !ent->inuse) @@ -1495,8 +1601,6 @@ void CTFCapReward(edict_t * ent) Announce_Reward(ent, UNSTOPPABLE); } else gi.cprintf(ent, PRINT_MEDIUM, "CAPTURED!\n\nYou have been rewarded.\n\nNow go get some more!"); - - LogCapture(ent); } void CTFSetupStatusbar( void ) diff --git a/src/action/a_dom.c b/src/action/a_dom.c index bb9c5e1bf..72d61678c 100644 --- a/src/action/a_dom.c +++ b/src/action/a_dom.c @@ -161,7 +161,7 @@ void DomFlagThink( edict_t *flag ) flag->owner->client->resp.dom_capstreak++; if (flag->owner->client->resp.dom_capstreak > flag->owner->client->resp.dom_capstreakbest) flag->owner->client->resp.dom_capstreakbest = flag->owner->client->resp.dom_capstreak; - LogCapture(flag->owner); + LOG_CAPTURE(flag->owner); if( (dom_team_flags[ flag->owner->client->resp.team ] == dom_flag_count) && (dom_flag_count > 1) ) gi.bprintf( PRINT_HIGH, "%s TEAM IS DOMINATING!\n", diff --git a/src/action/a_esp.c b/src/action/a_esp.c index c7aa8ec3c..643aa7140 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -244,7 +244,7 @@ void _EspBonusCapture(edict_t *attacker, edict_t *flag) flag->owner->client->resp.esp_capstreak++; if (flag->owner->client->resp.esp_capstreak > flag->owner->client->resp.esp_capstreakbest) flag->owner->client->resp.esp_capstreakbest = flag->owner->client->resp.esp_capstreak; - LogCapture(flag->owner); + LOG_CAPTURE(flag->owner); // Bonus points awarded flag->owner->client->resp.score += ESP_LEADER_CAPTURE_BONUS; diff --git a/src/action/g_items.c b/src/action/g_items.c index 70ff1438f..f166b7f7f 100644 --- a/src/action/g_items.c +++ b/src/action/g_items.c @@ -1850,7 +1850,7 @@ always owned, never in the world^M NULL, 0, /* precache */ "tng/flagcap.wav tng/flagret.wav", - FLAG_T1_NUM + FLAG_T1_NUM } , @@ -1878,6 +1878,51 @@ always owned, never in the world^M } , + /*QUAKED item_bcase_team1 (1 0.2 0) (-16 -16 -24) (16 16 32) + */ + { + "item_bcase_team1", // classname + CTFPickup_Flag, // pickup function + NULL, // use function + CTFDrop_Flag, // drop function + NULL, // weaponthink function + "tng/flagtk.wav", // pickup sound + "models/items/bcase/g_bc1.md2", EF_FLAG1|EF_ROTATE, // world model and effects + "w_bc1", // weapon model (vwep) + "i_bc1", // icon name + "Black Briefcase", // pickup name + 2, // width + 0, // quantity + NULL, // ammo + IT_FLAG, // item type + NULL, // info + 0, // tag + "tng/flagcap.wav tng/flagret.wav", // precache + FLAG_T1_NUM // index + } + , + { + "item_bcase_team2", // classname + CTFPickup_Flag, // pickup function + NULL, // use function + CTFDrop_Flag, // drop function + NULL, // weaponthink function + "tng/flagtk.wav", // pickup sound + "models/items/bcase/g_bc2.md2", EF_FLAG2|EF_ROTATE, // world model and effects + "w_bc2", // weapon model (vwep) + "i_bc2", // icon name + "Silver Briefcase", // pickup name + 2, // width + 0, // quantity + NULL, // ammo + IT_FLAG, // item type + NULL, // info + 0, // tag + "tng/flagcap.wav tng/flagret.wav", // precache + FLAG_T1_NUM // index + } + , + // end of list marker {NULL} }; diff --git a/src/action/g_local.h b/src/action/g_local.h index 475a221da..ae7407a77 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1377,6 +1377,7 @@ extern cvar_t *knife_catch; // Enables or disables knife catching extern cvar_t *grenade_drop; // Allows grenades to be dropped on death // 2025 +extern cvar_t *ctf_rewards; // Enables CTF awards extern cvar_t *bots; // If bots are enabled and in the server #ifdef AQTION_EXTENSION diff --git a/src/action/g_main.c b/src/action/g_main.c index df1bfc7bd..d7984510a 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -587,6 +587,7 @@ cvar_t *knife_catch; // Enables knife catching cvar_t *grenade_drop; // Allows grenades to be dropped on death // 2025 +cvar_t *ctf_rewards; // Enables CTF awards cvar_t *bots; // If bots are enabled and in the server #ifdef AQTION_EXTENSION diff --git a/src/action/g_misc.c b/src/action/g_misc.c index b2a403d2d..e807f14c1 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -329,22 +329,30 @@ void BecomeExplosion1 (edict_t * self) { //flags are important - if (ctf->value) - { - if (strcmp (self->classname, "item_flag_team1") == 0) - { - CTFResetFlag (TEAM1); // this will free self! - gi.bprintf (PRINT_HIGH, "The %s flag has returned!\n", - CTFTeamName (TEAM1)); - return; - } - if (strcmp (self->classname, "item_flag_team2") == 0) - { - CTFResetFlag (TEAM2); // this will free self! - gi.bprintf (PRINT_HIGH, "The %s flag has returned!\n", - CTFTeamName (TEAM1)); - return; - } + if (ctf->value) { + if (ctf_mode->value) { + if (strcmp (self->classname, "item_bcase_team1") == 0){ + CTFResetFlag (TEAM1); // this will free self! + gi.bprintf (PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName (TEAM1)); + return; + } + if (strcmp (self->classname, "item_bcase_team2") == 0){ + CTFResetFlag (TEAM2); // this will free self! + gi.bprintf (PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName (TEAM2)); + return; + } + } else { + if (strcmp (self->classname, "item_flag_team1") == 0){ + CTFResetFlag (TEAM1); // this will free self! + gi.bprintf (PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName (TEAM1)); + return; + } + if (strcmp (self->classname, "item_flag_team2") == 0){ + CTFResetFlag (TEAM2); // this will free self! + gi.bprintf (PRINT_HIGH, "The %s flag has returned!\n", CTFTeamName (TEAM2)); + return; + } + } } gi.WriteByte (svc_temp_entity); gi.WriteByte (TE_EXPLOSION1); diff --git a/src/action/g_save.c b/src/action/g_save.c index ceca11ea3..4d5413173 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -415,7 +415,7 @@ void InitGame( void ) use_scramblevote = gi.cvar( "use_scramblevote", "1", 0 ); //slicer ctf = gi.cvar( "ctf", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) ctf_forcejoin = gi.cvar( "ctf_forcejoin", "", 0 ); - ctf_mode = gi.cvar( "ctf_mode", "0", 0 ); + ctf_mode = gi.cvar( "ctf_mode", "0", CVAR_LATCH ); // Repurposed in 2025 -- now toggles between CTF and CTB modes. Rewards now handled via `ctf_rewards` cvar ctf_dropflag = gi.cvar( "ctf_dropflag", "1", 0 ); ctf_respawn = gi.cvar( "ctf_respawn", "4", 0 ); ctf_model = gi.cvar( "ctf_model", "male", CVAR_LATCH ); @@ -675,6 +675,7 @@ void InitGame( void ) knife_catch = gi.cvar("knife_catch", "0", 0); // 2025 + ctf_rewards = gi.cvar("ctf_rewards", "1", 0); bots = gi.cvar("bots", "0", CVAR_SERVERINFO); // new AQtion Extension cvars diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 7ef06d41f..57336aa35 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2198,10 +2198,17 @@ int LoadFlagsFromFile (const char *mapname) VectorCopy(position, ent->s.origin); - if (!flagCount) // Red Flag - ent->classname = ED_NewString ("item_flag_team1"); - else // Blue Flag - ent->classname = ED_NewString ("item_flag_team2"); + if (!flagCount) { // Red Flag / Black Briefcase + if (ctf_mode->value) + ent->classname = ED_NewString ("item_bcase_team1"); + else + ent->classname = ED_NewString ("item_flag_team1"); + } else { // Blue Flag / Silver Briefcase + if (ctf_mode->value) + ent->classname = ED_NewString ("item_bcase_team2"); + else + ent->classname = ED_NewString ("item_flag_team2"); + } ED_CallSpawn (ent); flagCount++; @@ -2229,8 +2236,13 @@ void ChangePlayerSpawns (void) range1 = range2 = range3 = range4 = 99999; spot = spot1 = spot2 = spot3 = spot4 = NULL; - flag1 = G_Find (flag1, FOFS(classname), "item_flag_team1"); - flag2 = G_Find (flag2, FOFS(classname), "item_flag_team2"); + if (ctf_mode->value){ + flag1 = G_Find (flag1, FOFS(classname), "item_bcase_team1"); + flag2 = G_Find (flag2, FOFS(classname), "item_bcase_team2"); + } else { + flag1 = G_Find (flag1, FOFS(classname), "item_flag_team1"); + flag2 = G_Find (flag2, FOFS(classname), "item_flag_team2"); + } if(!flag1 || !flag2) { gi.dprintf("Warning: ChangePlayerSpawns() requires both flags!\n"); diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 9b002b354..7424eaa29 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -499,6 +499,22 @@ void ChangeWeapon(edict_t* ent) // zucc - prevent reloading queue for previous weapon from doing anything ent->client->reload_attempts = 0; + // TODO: Make this work + // CTB prevents changing to a non-mk23/knife/grenade if carrying a briefcase + if (ctf_mode->value && + (ent->client->inventory[ITEM_INDEX(team_flag[TEAM1])] || + ent->client->inventory[ITEM_INDEX(team_flag[TEAM2])])) + { + if (ent->client->weapon->typeNum != KNIFE_NUM && + ent->client->weapon->typeNum != GRENADE_NUM && + ent->client->weapon->typeNum != DUAL_NUM) + { + ent->client->newweapon = NULL; + ent->client->weapon = NULL; + return; + } + } + ent->client->lastweapon = ent->client->weapon; ent->client->weapon = ent->client->newweapon; ent->client->newweapon = NULL; diff --git a/src/action/tng_stats.c b/src/action/tng_stats.c index 9e2d7418d..d14dd766a 100644 --- a/src/action/tng_stats.c +++ b/src/action/tng_stats.c @@ -1267,11 +1267,11 @@ void LogCapture(edict_t *capturer) if (game.ai_ent_found) { return; } + // If stats aren't enabled, do nothing if (!stat_logs->value) { return; } - int mode = Gamemode(); switch (mode) { case GM_CTF: @@ -1293,9 +1293,6 @@ void LogCapture(edict_t *capturer) break; } - // (matchid,capturer,capturername,team,gamemode,gamemodeflag,cap,capstreak) - // (mid,steamid,cn,t,team,gm,gmf,c,cs,ttc) - Q_snprintf( msg, sizeof(msg), "{\"capture\":{\"mid\":\"%s\",\"steamid\":\"%s\",\"cn\":\"%s\",\"t\":\"%d\",\"team\":\"%i\",\"gm\":%i,\"gmf\":%i,\"c\":%i,\"cs\":%i,\"ttc\":%i}}\n", From e63a4f3f28d94d400ac2d194208e589533bab9da Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Thu, 18 Dec 2025 13:32:27 -0500 Subject: [PATCH 962/974] Updated to macos-15-intel --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77989550a..66853c737 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -204,7 +204,7 @@ jobs: builddir/gamearm64.so darwin: - runs-on: macos-13 + runs-on: macos-15-intel steps: - uses: actions/checkout@v4 From 072004a4fae93aa1e3ea50428691af80031cb372 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 16:33:31 -0500 Subject: [PATCH 963/974] Added lrcon support directly to the gamelib (#261) --- doc/action.md | 206 +++++++++++++++++-- meson.build | 1 + src/action/Makefile | 2 +- src/action/a_game.c | 121 +++++++++++ src/action/a_game.h | 3 + src/action/g_cmds.c | 2 + src/action/g_local.h | 40 +++- src/action/g_lrcon.c | 456 ++++++++++++++++++++++++++++++++++++++++++ src/action/g_lrcon.h | 42 ++++ src/action/g_main.c | 20 ++ src/action/g_save.c | 10 + src/action/p_client.c | 24 +++ 12 files changed, 905 insertions(+), 22 deletions(-) create mode 100644 src/action/g_lrcon.c create mode 100644 src/action/g_lrcon.h diff --git a/doc/action.md b/doc/action.md index c556c2e48..b2a37a9e4 100644 --- a/doc/action.md +++ b/doc/action.md @@ -29,53 +29,55 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team - [Commands](#commands-7) - [Matchmode](#matchmode) - [Commands](#commands-8) - - [Voice Command](#voice-command) + - [Limited Remote Console (LRCON)](#limited-remote-console-lrcon) - [Commands](#commands-9) + - [Voice Command](#voice-command) + - [Commands](#commands-11) - [Low Lag Sounds](#low-lag-sounds) - - [Commands](#commands-10) + - [Commands](#commands-12) - [Announcer](#announcer) - - [Commands](#commands-11) + - [Commands](#commands-13) - [Kevlar Helmet](#kevlar-helmet) - [Single Barreled Handcannon](#single-barreled-handcannon) - - [Commands](#commands-12) + - [Commands](#commands-14) - [Enemy Down Radio Reporting](#enemy-down-radio-reporting) - [Player Ignoring](#player-ignoring) - - [Commands](#commands-13) + - [Commands](#commands-15) - [Video Setting Checking](#video-setting-checking) - - [Commands](#commands-14) + - [Commands](#commands-16) - [Location Files](#location-files) - [Punching](#punching) - - [Commands](#commands-15) + - [Commands](#commands-17) - [Sniper Zooming](#sniper-zooming) - [Cvars](#cvars) - - [Commands](#commands-16) + - [Commands](#commands-18) - [New Say Variables](#new-say-variables) - [Time and Roundtimeleft](#time-and-roundtimeleft) - - [Commands](#commands-17) + - [Commands](#commands-19) - [sv stuffcmd](#sv-stuffcmd) - - [Commands](#commands-18) + - [Commands](#commands-20) - [Grenade Strength](#grenade-strength) - - [Commands](#commands-19) + - [Commands](#commands-21) - [Total Kills](#total-kills) - [Random Rotation](#random-rotation) - - [Commands](#commands-20) + - [Commands](#commands-22) - [Vote Rotation](#vote-rotation) - - [Commands](#commands-21) + - [Commands](#commands-23) - [MapVote Next](#mapvote-next) - - [Commands](#commands-22) + - [Commands](#commands-24) - [Empty Rotate](#empty-rotate) - - [Commands](#commands-23) + - [Commands](#commands-25) - [Bandage Text](#bandage-text) - [Deathmatch Weapon](#deathmatch-weapon) - - [Commands](#commands-24) + - [Commands](#commands-26) - [Control Characters](#control-characters) - - [Commands](#commands-25) + - [Commands](#commands-27) - [Anti Camping](#anti-camping) - - [Commands](#commands-26) + - [Commands](#commands-28) - [Anti Idle](#anti-idle) - - [Commands](#commands-27) + - [Commands](#commands-29) - [Gibs](#gibs) - - [Commands](#commands-28) + - [Commands](#commands-30) - [Automatic Reloading of Pistol](#automatic-reloading-of-pistol) - [Weapon Banning](#weapon-banning) - [Item Banning](#item-banning) @@ -248,6 +250,170 @@ Clients will have a few more things to do during matchmode: they have to have a - `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map - `unlock` - allows a captain to unlock his team +### Limited Remote Console (LRCON) +Limited Remote Console (LRCON) provides controlled admin access through a claim/release system. One player at a time can claim temporary admin rights and execute restricted server commands without needing full rcon access. The claim persists across map changes and reconnects (matched by player name and IP). + +This is a native implementation of the popular q2admin lrcon functionality, built directly into the action gamelib for better integration and performance. + +#### Features +- **Claim/Release System**: One player claims admin access at a time +- **Persistence**: Claims survive map changes and player reconnects (matched by name + IP) +- **Player Management**: View player list with IPs, kick players, remove from teams +- **Map Control**: Change maps (hard or soft), switch server modes via configured exec commands +- **Cvar Management**: Query and modify whitelisted cvars only +- **Client Commands**: Execute commands on individual players or broadcast to all +- **Config File**: INI-format configuration for allowed cvars and server modes +- **Auto-Quit**: Optional auto-shutdown when server is empty for 5+ seconds +- **Broadcast Notifications**: All admin actions announced to all players + +#### Setup and Configuration + +**Step 1: Enable LRCON in config** +Create or edit `action/lrcon.cfg` with the following format: + +```ini +[settings] +enabled 1 +quit_on_empty 0 + +[allowed_cvars] +timelimit +fraglimit +teamdm +ctf +maxclients +hostname +dmflags +roundlimit +matchmode +teamplay +password +g_select_empty +sv_gravity +sv_fps +sv_antilag + +[modes] +teamdm|exec cfg/teamdm.cfg +ctf|exec cfg/ctf.cfg +ffa|exec cfg/ffa.cfg +duel|exec cfg/1v1.cfg +``` + +**Configuration Explanation:** +- `[settings]` section: + - `enabled 1` - Turn LRCON on/off (1=on, 0=off) + - `quit_on_empty 0` - Auto-quit server after 5 seconds empty (1=on, 0=off) + +- `[allowed_cvars]` section: + - List cvars (one per line) that players can query and modify via LRCON + - Only whitelisted cvars can be changed, preventing abuse + +- `[modes]` section: + - Define server configuration modes players can switch between + - Format: `mode_name|exec command_to_run` + - Example: `ctf|exec cfg/ctf.cfg` loads CTF config when mode is selected + +**Step 2: Server cvars** +Add to your server config if needed: + +``` +lrcon_config "lrcon.cfg" # Config file location (relative to action dir) +``` + +These cvars track the claimer and persist across maps: +``` +lrcon_claimer_name "" # Automatically set when someone claims +lrcon_claimer_ip "" # Automatically set when someone claims +``` + +#### Commands (In-Game) + +**Claiming the Server:** +- `lrcon claim` - Claim server control (only one player at a time) +- `lrcon release` - Release your control of the server +- `lrcon` - Show help with all available commands + +**Admin Commands (when claimed):** +- `lrcon status` - Display all players with ID numbers and IP addresses +- `lrcon kick ` - Kick a player by their ID number +- `lrcon teamnone ` - Remove a player from their team (send to team 0) +- `lrcon map ` - Change to a specific map (hard change, disconnects all) +- `lrcon softmap ` - Change map while keeping player scores/state +- `lrcon mode ` - Switch server mode (e.g., `lrcon mode ctf`) + - `lrcon mode list` - Show all available modes +- `lrcon stuffcmd ` - Send command(s) to client(s) + - Example: `lrcon stuffcmd 3 say I am an admin` - Send message as player 3 + - Example: `lrcon stuffcmd all record demo` - Record demos on all clients + +**Cvar Management:** +- `lrcon ` - Query a cvar value (must be whitelisted) + - Example: `lrcon timelimit` - Show current timelimit +- `lrcon ` - Set a cvar value (must be whitelisted) + - Example: `lrcon timelimit 25` - Set timelimit to 25 minutes + +#### Permission and Security + +- **Claim tied to player identity**: Matched by both name AND IP address + - Prevents simple impersonation or claim stealing + - Claimer must reconnect with same name and IP to restore claim + +- **Cvar whitelist enforcement**: Only configured cvars can be modified + - Prevents dangerous cvar changes (e.g., rcon password) + - Admin must explicitly whitelist each allowed cvar + +- **Self-protection**: Players cannot kick or remove themselves + +- **Client-side only**: Requires being connected to server + - No remote UDP access like traditional rcon + - More intuitive in-game command experience + +#### User Experience Examples + +**Example 1: Simple Mode Switch** +``` +Player 1: lrcon claim +[BROADCAST] Player 1 claimed server control +Player 1: lrcon mode list +[CHAT] Available modes: teamdm, ctf, ffa, duel +Player 1: lrcon mode ctf +[BROADCAST] Player 1 switched server to ctf mode +Player 1: lrcon release +[BROADCAST] Player 1 released server control +``` + +**Example 2: Admin Actions During Game** +``` +Player 1: lrcon claim +[BROADCAST] Player 1 claimed server control +Player 1: lrcon status +[CHAT] 1: Player1 (192.168.1.10) +[CHAT] 2: Player2 (192.168.1.11) +[CHAT] 3: Griefer (192.168.1.12) +Player 1: lrcon kick 3 +[BROADCAST] Griefer was kicked by Admin +``` + +**Example 3: Persistence Across Reconnect** +``` +Player 1: lrcon claim +[BROADCAST] Player 1 claimed server control +Player 1: [Disconnects] +[Map changes] +Player 1: [Reconnects from same IP with same name] +[BROADCAST] LRCON: Player 1 reconnected, claim restored +Player 1: lrcon release +``` + +#### Notes + +- Only one player can claim the server at any time +- If the claimer disconnects, the claim is automatically released +- The claim information is stored in cvars and persists across map changes +- If a player changes their name or connects from a different IP, they lose the claim +- The `quit_on_empty` feature counts down from when the last player leaves; a 5+ second empty period triggers server shutdown +- Use `lrcon softmap` when you want to keep game state (scores, items), use `lrcon map` for a fresh map start + ### Voice Command The voice command allows clients to play taunts for other players to hear. (as long as they have the sound file) diff --git a/meson.build b/meson.build index 3fd39b7bc..2f882049c 100644 --- a/meson.build +++ b/meson.build @@ -188,6 +188,7 @@ action_src = [ 'src/action/g_func.c', 'src/action/g_grapple.c', 'src/action/g_items.c', + 'src/action/g_lrcon.c', 'src/action/g_main.c', 'src/action/g_misc.c', 'src/action/g_phys.c', diff --git a/src/action/Makefile b/src/action/Makefile index 97978000d..ec6c4d9c8 100644 --- a/src/action/Makefile +++ b/src/action/Makefile @@ -144,7 +144,7 @@ GAME_OBJS = \ a_cmds.o a_ctf.o a_doorkick.o a_game.o a_items.o a_match.o \ a_menu.o a_radio.o a_team.o a_tourney.o a_vote.o a_xcmds.o a_xgame.o \ a_xmenu.o a_xvote.o cgf_sfx_glass.o g_chase.o g_cmds.o \ - g_combat.o g_func.o g_items.o g_main.o g_misc.o g_ext.o \ + g_combat.o g_func.o g_items.o g_lrcon.o g_main.o g_misc.o g_ext.o \ g_phys.o g_save.o g_spawn.o g_svcmds.o g_target.o g_trigger.o \ g_utils.o g_weapon.o \ p_antilag.o p_client.o p_hud.o p_view.o p_weapon.o q_shared.o \ diff --git a/src/action/a_game.c b/src/action/a_game.c index 1156087c0..1f94b7cc2 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1513,4 +1513,125 @@ void _PickupRequest (edict_t * ent, pmenu_t * p) PMenu_Close(ent); Cmd_Pickup_f(ent); +} + +void ReadLrconConfig(void) +{ + FILE *config_file; + char buf[MAX_STR_LEN], reading_section[MAX_STR_LEN], cfgpath[MAX_STR_LEN]; + cvar_t *lrcon_config_cvar; + int lines_into_section = -1; + + // Initialize defaults + game.lrcon_config.enabled = 0; + game.lrcon_config.quit_on_empty = 0; + game.lrcon_config.allowed_cvars_count = 0; + game.lrcon_config.modes_count = 0; + + // Get config filename from cvar + lrcon_config_cvar = gi.cvar("lrcon_config", "lrcon.cfg", 0); + if (lrcon_config_cvar->string && *(lrcon_config_cvar->string)) + sprintf(cfgpath, "%s/%s", GAMEVERSION, lrcon_config_cvar->string); + else + sprintf(cfgpath, "%s/%s", GAMEVERSION, "lrcon.cfg"); + + // Try to open config file + config_file = fopen(cfgpath, "r"); + if (config_file == NULL) { + gi.dprintf("LRCON: Unable to read %s (lrcon disabled)\n", cfgpath); + return; + } + + // Parse config file + while (fgets(buf, MAX_STR_LEN - 10, config_file) != NULL) { + int bs; + char *space, *key, *value; + + // Strip newlines/carriage returns + bs = strlen(buf); + while (bs > 0 && (buf[bs - 1] == '\r' || buf[bs - 1] == '\n')) { + buf[bs - 1] = 0; + bs--; + } + + // Skip empty lines and comments + if ((buf[0] == '/' && buf[1] == '/') || buf[0] == 0) { + continue; + } + + // Handle section headers + if (buf[0] == '[') { + char *p; + + p = strchr(buf, ']'); + if (p == NULL) + continue; + *p = 0; + strcpy(reading_section, buf + 1); + lines_into_section = 0; + continue; + } + + // Skip special markers + if (buf[0] == '#' && buf[1] == '#' && buf[2] == '#') { + lines_into_section = -1; + continue; + } + + // Process section content + if (lines_into_section > -1) { + if (!strcmp(reading_section, "settings")) { + // Parse key-value pairs in settings section + space = strchr(buf, ' '); + if (space != NULL) { + *space = 0; + key = buf; + value = space + 1; + + if (!strcmp(key, "enabled")) { + game.lrcon_config.enabled = atoi(value) ? 1 : 0; + gi.dprintf("LRCON: enabled = %d\n", game.lrcon_config.enabled); + } else if (!strcmp(key, "quit_on_empty")) { + game.lrcon_config.quit_on_empty = atoi(value) ? 1 : 0; + gi.dprintf("LRCON: quit_on_empty = %d\n", game.lrcon_config.quit_on_empty); + } + } + } else if (!strcmp(reading_section, "allowed_cvars")) { + // Each line is a cvar name + if (game.lrcon_config.allowed_cvars_count < MAX_LRCON_CVARS) { + Q_strncpyz(game.lrcon_config.allowed_cvars[game.lrcon_config.allowed_cvars_count], + buf, sizeof(game.lrcon_config.allowed_cvars[0])); + gi.dprintf("LRCON: allowed cvar %d = %s\n", + game.lrcon_config.allowed_cvars_count, + game.lrcon_config.allowed_cvars[game.lrcon_config.allowed_cvars_count]); + game.lrcon_config.allowed_cvars_count++; + } + } else if (!strcmp(reading_section, "modes")) { + // Format: name|command + char *pipe = strchr(buf, '|'); + if (pipe != NULL && game.lrcon_config.modes_count < MAX_LRCON_MODES) { + *pipe = 0; + Q_strncpyz(game.lrcon_config.modes[game.lrcon_config.modes_count].name, + buf, sizeof(game.lrcon_config.modes[0].name)); + Q_strncpyz(game.lrcon_config.modes[game.lrcon_config.modes_count].command, + pipe + 1, sizeof(game.lrcon_config.modes[0].command)); + gi.dprintf("LRCON: mode %d = %s -> %s\n", + game.lrcon_config.modes_count, + game.lrcon_config.modes[game.lrcon_config.modes_count].name, + game.lrcon_config.modes[game.lrcon_config.modes_count].command); + game.lrcon_config.modes_count++; + } + } + lines_into_section++; + } + } + + fclose(config_file); + + if (game.lrcon_config.enabled) { + gi.dprintf("LRCON: Configuration loaded successfully (%d cvars, %d modes)\n", + game.lrcon_config.allowed_cvars_count, game.lrcon_config.modes_count); + } else { + gi.dprintf("LRCON: Not enabled in config\n"); + } } \ No newline at end of file diff --git a/src/action/a_game.h b/src/action/a_game.h index 34f50c4b9..b7093c45d 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -158,3 +158,6 @@ void Cmd_PrintRules_f(edict_t *ent); void Cmd_Pickup_f( edict_t * ent ); #endif #define MSG_PICKUP_UNSUPPORTED "This server does not support pickup requests.\n" + +/* g_lrcon.c */ +void ReadLrconConfig(void); diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index 5f80e5199..a0ce1704f 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -213,6 +213,7 @@ #include "g_local.h" #include "m_player.h" +#include "g_lrcon.h" #ifndef NO_BOTS //void Cmd_Placenode_f( edict_t *ent ); @@ -2024,6 +2025,7 @@ static cmdList_t commandList[] = { "leader", Cmd_Volunteer_f, 0}, { "highscores", Cmd_HighScores_f, 0}, { "pickup", Cmd_Pickup_f, 0}, + { "lrcon", Cmd_Lrcon_f, 0 }, }; diff --git a/src/action/g_local.h b/src/action/g_local.h index ae7407a77..c5ba291cd 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -764,6 +764,38 @@ typedef struct precache_s { void (*func)(void); } precache_t; +/* + * LRCON (Limited Remote Console) data structures + */ + +#define MAX_LRCON_CVARS 32 +#define MAX_LRCON_MODES 16 + +/* LRCON state - tracks current server claim */ +typedef struct { + qboolean claimed; /* Is server currently claimed? */ + char claimer_name[16]; /* Name of current claimer */ + char claimer_ip[64]; /* IP address of current claimer */ + int claim_time; /* Frame number when claimed */ + edict_t *claimer_ent; /* Pointer to claimer entity (NULL if disconnected) */ +} lrcon_state_t; + +/* Server mode definition */ +typedef struct { + char name[64]; /* Mode name (e.g., "teamdm", "ctf") */ + char command[256]; /* Config command to execute (e.g., "exec cfg/teamdm.cfg") */ +} lrcon_mode_t; + +/* LRCON configuration */ +typedef struct { + qboolean enabled; /* Is LRCON enabled? */ + qboolean quit_on_empty; /* Quit server when last player leaves? */ + int allowed_cvars_count; /* Number of whitelisted cvars */ + char allowed_cvars[MAX_LRCON_CVARS][64]; /* Whitelisted cvar names */ + int modes_count; /* Number of available modes */ + lrcon_mode_t modes[MAX_LRCON_MODES]; /* Available server modes */ +} lrcon_config_t; + // // this structure is left intact through an entire game // it should be initialized at dll load time, and read/written to @@ -819,8 +851,11 @@ typedef struct #if AQTION_CURL // Discord Webhook limits qboolean time_warning_sent; // This is set to true when the time warning has been sent, resets every map - + #endif + + // LRCON configuration + lrcon_config_t lrcon_config; } game_locals_t; @@ -930,6 +965,9 @@ typedef struct int lc_recently_sent[NOTIFY_MAX]; // Used to prevent spamming of the endpoint // Map features map_features_t map_features; + + // LRCON state + lrcon_state_t lrcon; } level_locals_t; diff --git a/src/action/g_lrcon.c b/src/action/g_lrcon.c new file mode 100644 index 000000000..ee06aa0cd --- /dev/null +++ b/src/action/g_lrcon.c @@ -0,0 +1,456 @@ +/* + * g_lrcon.c + * + * Limited Remote Console (LRCON) - Client-side admin command system + * Allows players to claim temporary admin rights and execute restricted commands. + * + * Features: + * - Claim/release system: One player can claim admin access at a time + * - Persistence: Claim persists across map changes and reconnects (name+IP matched) + * - Commands: Player management, map control, cvar control, mode switching + * - Config: INI-format configuration file for allowed cvars and modes + * - Broadcast: All admin actions announced to all players + */ + +#include "g_local.h" +#include "g_lrcon.h" + +/* External declarations */ +extern cvar_t *lrcon_config; +extern cvar_t *lrcon_claimer_name; +extern cvar_t *lrcon_claimer_ip; +extern int dosoft; + +/* + * Lrcon_CheckClaimer + * + * Verify that the caller is the current server claimer. + * Returns true if caller has admin rights, false otherwise. + */ +qboolean Lrcon_CheckClaimer(edict_t *ent) +{ + if (!level.lrcon.claimed) { + gi.cprintf(ent, PRINT_HIGH, + "Server not claimed. Use 'lrcon claim' first.\n"); + return false; + } + + /* Check if this is the claimer (by name + IP) */ + if (Q_stricmp(ent->client->pers.netname, level.lrcon.claimer_name) != 0 || + Q_stricmp(ent->client->pers.ip, level.lrcon.claimer_ip) != 0) { + gi.cprintf(ent, PRINT_HIGH, + "Server is claimed by %s. Only they can use admin commands.\n", + level.lrcon.claimer_name); + return false; + } + + /* Update entity pointer (in case of reconnect) */ + level.lrcon.claimer_ent = ent; + return true; +} + +/* + * Lrcon_ClearClaim + * + * Clear the server claim and reset state. + */ +void Lrcon_ClearClaim(void) +{ + level.lrcon.claimed = false; + level.lrcon.claimer_name[0] = '\0'; + level.lrcon.claimer_ip[0] = '\0'; + level.lrcon.claimer_ent = NULL; + level.lrcon.claim_time = 0; + + gi.cvar_forceset("lrcon_claimer_name", ""); + gi.cvar_forceset("lrcon_claimer_ip", ""); +} + +/* + * Lrcon_PrintHelp + * + * Display help text with all available commands. + */ +void Lrcon_PrintHelp(edict_t *ent) +{ + gi.cprintf(ent, PRINT_HIGH, "LRCON Commands:\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon claim - Claim server control\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon release - Release control\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon status - Show player list with IPs\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon kick - Kick player\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon teamnone - Remove from team\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon map - Change map\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon softmap - Change map keeping state\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon mode - Change server mode\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon stuffcmd - Send command to client(s)\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon - Query cvar value\n"); + gi.cprintf(ent, PRINT_HIGH, " lrcon - Set cvar value\n"); +} + +/* + * Lrcon_Claim + * + * Claim server control for this player. + */ +void Lrcon_Claim(edict_t *ent) +{ + if (level.lrcon.claimed) { + gi.cprintf(ent, PRINT_HIGH, + "Server already claimed by %s. Wait for them to release.\n", + level.lrcon.claimer_name); + return; + } + + level.lrcon.claimed = true; + Q_strncpyz(level.lrcon.claimer_name, ent->client->pers.netname, + sizeof(level.lrcon.claimer_name)); + Q_strncpyz(level.lrcon.claimer_ip, ent->client->pers.ip, + sizeof(level.lrcon.claimer_ip)); + level.lrcon.claim_time = level.framenum; + level.lrcon.claimer_ent = ent; + + gi.cprintf(ent, PRINT_HIGH, "You have claimed server control!\n"); + gi.bprintf(PRINT_HIGH, "%s claimed server control\n", + ent->client->pers.netname); + + /* Store in cvars for persistence across map changes */ + gi.cvar_forceset("lrcon_claimer_name", level.lrcon.claimer_name); + gi.cvar_forceset("lrcon_claimer_ip", level.lrcon.claimer_ip); +} + +/* + * Lrcon_Release + * + * Release server control. + */ +void Lrcon_Release(edict_t *ent) +{ + if (!Lrcon_CheckClaimer(ent)) return; + + gi.cprintf(ent, PRINT_HIGH, "You released server control.\n"); + gi.bprintf(PRINT_HIGH, "%s released server control\n", + ent->client->pers.netname); + + Lrcon_ClearClaim(); +} + +/* + * Lrcon_Status + * + * Display list of connected players with IPs. + */ +void Lrcon_Status(edict_t *ent) +{ + int i; + edict_t *cl; + + if (!Lrcon_CheckClaimer(ent)) return; + + gi.cprintf(ent, PRINT_HIGH, "ID Name IP Address\n"); + gi.cprintf(ent, PRINT_HIGH, "--- --------------- ---------------\n"); + + for (i = 0; i < game.maxclients; i++) { + cl = g_edicts + 1 + i; + if (!cl->inuse || !cl->client) continue; + + gi.cprintf(ent, PRINT_HIGH, "%-3d %-15s %s\n", + i, cl->client->pers.netname, cl->client->pers.ip); + } +} + +/* + * Lrcon_Kick + * + * Kick a player from the server. + */ +void Lrcon_Kick(edict_t *ent) +{ + edict_t *target; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 3) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon kick \n"); + return; + } + + target = LookupPlayer(ent, gi.argv(2), true, false); + if (!target) return; + + if (target == ent) { + gi.cprintf(ent, PRINT_HIGH, "You cannot kick yourself\n"); + return; + } + + gi.bprintf(PRINT_HIGH, "%s was kicked by %s\n", + target->client->pers.netname, ent->client->pers.netname); + + Kick_Client(target); +} + +/* + * Lrcon_Teamnone + * + * Remove a player from their team. + */ +void Lrcon_Teamnone(edict_t *ent) +{ + edict_t *target; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 3) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon teamnone \n"); + return; + } + + target = LookupPlayer(ent, gi.argv(2), true, false); + if (!target) return; + + if (target->client->resp.team == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "%s is not on a team\n", + target->client->pers.netname); + return; + } + + gi.bprintf(PRINT_HIGH, "%s removed %s from their team\n", + ent->client->pers.netname, target->client->pers.netname); + + JoinTeam(target, NOTEAM, 0); +} + +/* + * Lrcon_Map + * + * Change to a new map. + */ +void Lrcon_Map(edict_t *ent) +{ + const char *mapname; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 3) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon map \n"); + return; + } + + mapname = gi.argv(2); + + gi.bprintf(PRINT_HIGH, "%s is changing map to %s\n", + ent->client->pers.netname, mapname); + + Q_strncpyz(level.nextmap, mapname, sizeof(level.nextmap)); + EndDMLevel(); +} + +/* + * Lrcon_Softmap + * + * Soft map change - changes map while keeping score/state. + */ +void Lrcon_Softmap(edict_t *ent) +{ + const char *mapname; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 3) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon softmap \n"); + return; + } + + mapname = gi.argv(2); + + gi.bprintf(PRINT_HIGH, "%s is soft-changing map to %s\n", + ent->client->pers.netname, mapname); + + /* Use existing softmap functionality from g_svcmds.c */ + Q_strncpyz(level.nextmap, mapname, sizeof(level.nextmap)); + dosoft = 1; /* Global flag for soft map change */ + EndDMLevel(); +} + +/* + * Lrcon_Mode + * + * Change server configuration mode. + */ +void Lrcon_Mode(edict_t *ent) +{ + const char *mode_arg; + int i; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 3) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon mode \n"); + return; + } + + mode_arg = gi.argv(2); + + if (!Q_stricmp(mode_arg, "list")) { + gi.cprintf(ent, PRINT_HIGH, "Available modes:\n"); + for (i = 0; i < game.lrcon_config.modes_count; i++) { + gi.cprintf(ent, PRINT_HIGH, " %s\n", + game.lrcon_config.modes[i].name); + } + return; + } + + /* Find and execute mode */ + for (i = 0; i < game.lrcon_config.modes_count; i++) { + if (!Q_stricmp(mode_arg, game.lrcon_config.modes[i].name)) { + gi.bprintf(PRINT_HIGH, "%s is changing mode to %s\n", + ent->client->pers.netname, mode_arg); + gi.AddCommandString(game.lrcon_config.modes[i].command); + gi.AddCommandString("\n"); + return; + } + } + + gi.cprintf(ent, PRINT_HIGH, "Unknown mode '%s'. Use 'lrcon mode list'\n", + mode_arg); +} + +/* + * Lrcon_Stuffcmd + * + * Send a command to a specific client or all clients. + */ +void Lrcon_Stuffcmd(edict_t *ent) +{ + const char *target_arg; + const char *command; + const char *cmd_start; + edict_t *target; + int i, skip_count; + + if (!Lrcon_CheckClaimer(ent)) return; + + if (gi.argc() < 4) { + gi.cprintf(ent, PRINT_HIGH, "Usage: lrcon stuffcmd \n"); + return; + } + + target_arg = gi.argv(2); + command = gi.args(); + + /* Skip "stuffcmd " to get the actual command */ + cmd_start = command; + skip_count = 0; + while (*cmd_start && skip_count < 2) { + if (*cmd_start == ' ') skip_count++; + cmd_start++; + } + + if (!Q_stricmp(target_arg, "all")) { + /* Send to all clients */ + for (i = 0; i < game.maxclients; i++) { + target = g_edicts + 1 + i; + if (!target->inuse || !target->client) continue; + stuffcmd(target, va("%s\n", cmd_start)); + } + gi.bprintf(PRINT_HIGH, "%s sent command to all players\n", + ent->client->pers.netname); + } else { + /* Send to specific client */ + target = LookupPlayer(ent, target_arg, true, false); + if (!target) return; + + stuffcmd(target, va("%s\n", cmd_start)); + gi.bprintf(PRINT_HIGH, "%s sent command to %s\n", + ent->client->pers.netname, target->client->pers.netname); + } +} + +/* + * Lrcon_Cvar + * + * Query or set a whitelisted cvar value. + */ +void Lrcon_Cvar(edict_t *ent, const char *cvar_name) +{ + cvar_t *var; + int i; + qboolean allowed = false; + const char *new_value; + + if (!Lrcon_CheckClaimer(ent)) return; + + /* Check if cvar is whitelisted */ + for (i = 0; i < game.lrcon_config.allowed_cvars_count; i++) { + if (!Q_stricmp(cvar_name, game.lrcon_config.allowed_cvars[i])) { + allowed = true; + break; + } + } + + if (!allowed) { + gi.cprintf(ent, PRINT_HIGH, "Cvar '%s' is not whitelisted\n", + cvar_name); + return; + } + + var = gi.cvar(cvar_name, "", 0); + + if (gi.argc() < 3) { + /* Query cvar value */ + gi.cprintf(ent, PRINT_HIGH, "%s = \"%s\"\n", cvar_name, var->string); + } else { + /* Set cvar value */ + new_value = gi.argv(2); + gi.cvar_forceset(cvar_name, new_value); + + gi.cprintf(ent, PRINT_HIGH, "%s -> \"%s\"\n", cvar_name, new_value); + gi.bprintf(PRINT_HIGH, "%s changed %s to \"%s\"\n", + ent->client->pers.netname, cvar_name, new_value); + } +} + +/* + * Cmd_Lrcon_f + * + * Main LRCON command router. Dispatches to appropriate handler based on subcommand. + */ +void Cmd_Lrcon_f(edict_t *ent) +{ + const char *subcmd; + + if (!game.lrcon_config.enabled) { + gi.cprintf(ent, PRINT_HIGH, + "LRCON is not enabled on this server.\n"); + return; + } + + if (gi.argc() < 2) { + Lrcon_PrintHelp(ent); + return; + } + + subcmd = gi.argv(1); + + if (!Q_stricmp(subcmd, "claim")) { + Lrcon_Claim(ent); + } else if (!Q_stricmp(subcmd, "release")) { + Lrcon_Release(ent); + } else if (!Q_stricmp(subcmd, "status")) { + Lrcon_Status(ent); + } else if (!Q_stricmp(subcmd, "kick")) { + Lrcon_Kick(ent); + } else if (!Q_stricmp(subcmd, "teamnone")) { + Lrcon_Teamnone(ent); + } else if (!Q_stricmp(subcmd, "map")) { + Lrcon_Map(ent); + } else if (!Q_stricmp(subcmd, "softmap")) { + Lrcon_Softmap(ent); + } else if (!Q_stricmp(subcmd, "mode")) { + Lrcon_Mode(ent); + } else if (!Q_stricmp(subcmd, "stuffcmd")) { + Lrcon_Stuffcmd(ent); + } else { + /* Try cvar get/set */ + Lrcon_Cvar(ent, subcmd); + } +} diff --git a/src/action/g_lrcon.h b/src/action/g_lrcon.h new file mode 100644 index 000000000..aec745710 --- /dev/null +++ b/src/action/g_lrcon.h @@ -0,0 +1,42 @@ +/* + * g_lrcon.h + * + * Limited Remote Console (LRCON) - Client-side admin command system + * Allows players to claim temporary admin rights and execute restricted commands + */ + +#ifndef G_LRCON_H +#define G_LRCON_H + +/* Data structures defined in g_local.h: + * - lrcon_state_t: Runtime state (claimed, claimer info) + * - lrcon_config_t: Configuration (enabled, allowed cvars, modes) + * - lrcon_mode_t: Server mode definition + */ + +/* Main command entry point */ +void Cmd_Lrcon_f(edict_t *ent); + +/* Claim/Release operations */ +void Lrcon_Claim(edict_t *ent); +void Lrcon_Release(edict_t *ent); + +/* Admin commands (require claim) */ +void Lrcon_Status(edict_t *ent); /* Show player list */ +void Lrcon_Kick(edict_t *ent); /* Kick a player */ +void Lrcon_Teamnone(edict_t *ent); /* Remove from team */ +void Lrcon_Map(edict_t *ent); /* Change map */ +void Lrcon_Softmap(edict_t *ent); /* Soft map change (keeps state) */ +void Lrcon_Mode(edict_t *ent); /* Change server mode */ +void Lrcon_Stuffcmd(edict_t *ent); /* Send command to client(s) */ +void Lrcon_Cvar(edict_t *ent, const char *cvar_name); /* Query/set cvar */ + +/* Helper functions */ +qboolean Lrcon_CheckClaimer(edict_t *ent); /* Verify caller is claimer */ +void Lrcon_ClearClaim(void); /* Clear claim state */ +void Lrcon_PrintHelp(edict_t *ent); /* Display help text */ + +/* Config loading (in a_game.c) */ +void ReadLrconConfig(void); + +#endif /* G_LRCON_H */ diff --git a/src/action/g_main.c b/src/action/g_main.c index d7984510a..dd49acdee 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -286,6 +286,11 @@ int stopAP; edict_t *g_edicts; //FIREBLADE +/* LRCON cvars */ +cvar_t *lrcon_config; +cvar_t *lrcon_claimer_name; +cvar_t *lrcon_claimer_ip; + cvar_t *hostname; cvar_t *teamplay; cvar_t *radiolog; @@ -1301,6 +1306,21 @@ void G_RunFrame (void) return; } + // LRCON quit_on_empty logic + if (game.lrcon_config.quit_on_empty) { + if (empty) { + if (level.emptyTime == 0) { + level.emptyTime = level.time; + gi.dprintf("LRCON: Server empty, will quit in 5 seconds\n"); + } else if (level.time - level.emptyTime > 5.0) { + gi.dprintf("LRCON: Quitting server (empty for 5+ seconds)\n"); + gi.AddCommandString("quit\n"); + } + } else { + level.emptyTime = 0; + } + } + // TNG Darkmatch Cycle if(!level.pauseFrames) { diff --git a/src/action/g_save.c b/src/action/g_save.c index 4d5413173..4102daa0e 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -255,6 +255,9 @@ #include "g_local.h" #include "cgf_sfx_glass.h" +extern cvar_t *lrcon_config; +extern cvar_t *lrcon_claimer_name; +extern cvar_t *lrcon_claimer_ip; void InitCommandList( void ); @@ -335,6 +338,7 @@ void InitGame( void ) ReadConfigFile(); ReadMOTDFile(); + ReadLrconConfig(); gun_x = gi.cvar( "gun_x", "0", 0 ); gun_y = gi.cvar( "gun_y", "0", 0 ); @@ -669,6 +673,12 @@ void InitGame( void ) gi.cvar_forceset("dmweapon", "Combat Knife"); gi.cvar_forceset("bholelimit", "30"); } + + /* LRCON persistence cvars */ + lrcon_config = gi.cvar("lrcon_config", "lrcon.cfg", 0); + lrcon_claimer_name = gi.cvar("lrcon_claimer_name", "", 0); + lrcon_claimer_ip = gi.cvar("lrcon_claimer_ip", "", 0); + g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); g_highscores_countbots = gi.cvar("g_highscores_countbots", "0", 0); lca_grenade = gi.cvar("lca_grenade", "0", 0); diff --git a/src/action/p_client.c b/src/action/p_client.c index 25b8e8909..c26071024 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -319,7 +319,10 @@ #include "g_local.h" #include "m_player.h" #include "cgf_sfx_glass.h" +#include "g_lrcon.h" +extern cvar_t *lrcon_claimer_name; +extern cvar_t *lrcon_claimer_ip; static void FreeClientEdicts(gclient_t *client) { @@ -3645,6 +3648,21 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) IRC_printf(IRC_T_SERVER, "%n@%s connected", value, ipaddr_buf); } + // LRCON: Check if reconnecting claimer and restore claim + value = Info_ValueForKey(userinfo, "name"); + if (game.lrcon_config.enabled && lrcon_claimer_name->string && *lrcon_claimer_name->string && + !strcmp(lrcon_claimer_name->string, value) && + !strcmp(lrcon_claimer_ip->string, ipaddr_buf)) { + level.lrcon.claimed = true; + Q_strncpyz(level.lrcon.claimer_name, lrcon_claimer_name->string, + sizeof(level.lrcon.claimer_name)); + Q_strncpyz(level.lrcon.claimer_ip, lrcon_claimer_ip->string, + sizeof(level.lrcon.claimer_ip)); + level.lrcon.claimer_ent = ent; + level.lrcon.claim_time = level.framenum; + gi.bprintf(PRINT_HIGH, "LRCON: %s reconnected, claim restored\n", value); + } + //rekkie -- silence ban -- s if (SV_FilterSBPacket(ipaddr_buf, NULL)) // Check if player has been silenced { @@ -3710,6 +3728,12 @@ void ClientDisconnect(edict_t * ent) gi.bprintf(PRINT_HIGH, "%s disconnected\n", ent->client->pers.netname); IRC_printf(IRC_T_SERVER, "%n disconnected", ent->client->pers.netname); + // LRCON: Clear claim if claimer disconnects + if (level.lrcon.claimed && level.lrcon.claimer_ent == ent) { + gi.bprintf(PRINT_HIGH, "LRCON: Released (claimer disconnected)\n"); + Lrcon_ClearClaim(); + } + if( !teamplay->value && !ent->client->pers.spectator ) { // send effect From e362225dbe44667c70c9cd77410d82e106700158 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 16:33:45 -0500 Subject: [PATCH 964/974] Support for Extended Status and Rules queries (#262) --- .gitignore | 4 +- doc/action.md | 2 +- inc/common/cvar.h | 2 +- inc/shared/shared.h | 13 +- src/action/a_ctf.c | 4 + src/action/g_save.c | 160 ++++++++--------- src/server/main.c | 421 ++++++++++++++++++++++++++++++++++++++++++++ src/server/server.h | 1 + 8 files changed, 518 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 5d3374bbb..a49facb52 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ build.sh /build_win clang-build.sh -/bots \ No newline at end of file +/bots +/dev +.DS_Store \ No newline at end of file diff --git a/doc/action.md b/doc/action.md index b2a37a9e4..31ea3e7ba 100644 --- a/doc/action.md +++ b/doc/action.md @@ -737,7 +737,7 @@ For the lazy players under us, we have created three new commands to make things **Commands:** - `auto_join [0/1/2]` - when on (1), the players will automatically join the team he was in during the last map. If set to 2, it will automatically join the team with the least players, or randomly if equal. - `auto_equip [0/1]` - when on (1), the players will automatically be given the equipment he had during the last map. -- `auto_menu [0/1]` - when on (1), the menu will automatically open when you join the server. +- `auto_menu [0/1/2]` - when on (1), the menu will automatically open when you join the server. when set to (2), it will automatically render the menu AFTER the MOTD renders ### Automatic Demo Recording To ease the recording of demos by a client over several maps, we have implemented a feature which will let the client automatically record a demo on each new map. When enabled, the server will keep track of the recording of the client, and will create a demo of the map with the following name: date_time-map.dm2 In matchmode, the demo's name will be: date_time-team1_vs_team2-map.dm2 diff --git a/inc/common/cvar.h b/inc/common/cvar.h index 4c6253617..21b37300e 100644 --- a/inc/common/cvar.h +++ b/inc/common/cvar.h @@ -49,7 +49,7 @@ interface from being ambiguous. #define CVAR_INFOMASK (CVAR_USERINFO | CVAR_SERVERINFO) #define CVAR_MODIFYMASK (CVAR_INFOMASK | CVAR_FILES | CVAR_REFRESH | CVAR_SOUND) #define CVAR_NOARCHIVEMASK (CVAR_NOSET | CVAR_CHEAT | CVAR_PRIVATE | CVAR_ROM | CVAR_NOARCHIVE) -#define CVAR_EXTENDED_MASK (~MASK(5)) +#define CVAR_EXTENDED_MASK ((~MASK(5)) & ~CVAR_SERVERINFO_EXT) extern cvar_t *cvar_vars; extern int cvar_modified; diff --git a/inc/shared/shared.h b/inc/shared/shared.h index 51a0b7bd5..44773af2a 100644 --- a/inc/shared/shared.h +++ b/inc/shared/shared.h @@ -907,12 +907,13 @@ CVARS (console variables) #ifndef CVAR #define CVAR -#define CVAR_ARCHIVE BIT(0) // set to cause it to be saved to vars.rc -#define CVAR_USERINFO BIT(1) // added to userinfo when changed -#define CVAR_SERVERINFO BIT(2) // added to serverinfo when changed -#define CVAR_NOSET BIT(3) // don't allow change from console at all, - // but can be set from the command line -#define CVAR_LATCH BIT(4) // save changes until server restart +#define CVAR_ARCHIVE BIT(0) // set to cause it to be saved to vars.rc +#define CVAR_USERINFO BIT(1) // added to userinfo when changed +#define CVAR_SERVERINFO BIT(2) // added to serverinfo when changed +#define CVAR_NOSET BIT(3) // don't allow change from console at all, + // but can be set from the command line +#define CVAR_LATCH BIT(4) // save changes until server restart +#define CVAR_SERVERINFO_EXT BIT(16) // queryable via rulesext protocol #if USE_CLIENT || USE_SERVER struct cvar_s; diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 324a14296..7cb149601 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -791,6 +791,10 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) else ctfgame.team2++; + // Update team scores + teams[TEAM1].score = ctfgame.team1; + teams[TEAM2].score = ctfgame.team2; + CTFDynamicRespawnTimer(); // Dynamic respawn time gi.sound(ent, CHAN_RELIABLE + CHAN_NO_PHS_ADD + CHAN_VOICE, diff --git a/src/action/g_save.c b/src/action/g_save.c index 4102daa0e..3fde1f2a8 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -352,15 +352,15 @@ void InitGame( void ) dedicated = gi.cvar( "dedicated", "0", CVAR_NOSET ); steamid = gi.cvar( "steamid", "0", CVAR_NOSET ); - sv_cheats = gi.cvar( "cheats", "0", CVAR_SERVERINFO | CVAR_LATCH ); - gi.cvar( "gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_NOSET ); // Removed it from Serverinfo, we already have game and gamedir - gi.cvar( "gamedate", __DATE__, CVAR_SERVERINFO | CVAR_NOSET ); - actionversion = gi.cvar( "actionversion", "TNG " VERSION, CVAR_SERVERINFO | CVAR_NOSET ); + sv_cheats = gi.cvar( "cheats", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); + gi.cvar( "gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); // Removed it from Serverinfo, we already have game and gamedir + gi.cvar( "gamedate", __DATE__, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); + actionversion = gi.cvar( "actionversion", "TNG " VERSION, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET ); gi.cvar_set( "actionversion", "TNG " VERSION ); net_port = gi.cvar( "net_port", "27910", CVAR_NOSET ); - maxclients = gi.cvar( "maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH ); + maxclients = gi.cvar( "maxclients", "8", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); maxentities = gi.cvar( "maxentities", "1024", CVAR_LATCH ); deathmatch = gi.cvar( "deathmatch", "1", CVAR_LATCH ); @@ -368,25 +368,25 @@ void InitGame( void ) gi.dprintf( "Turning deathmatch on.\n" ); gi.cvar_forceset( "deathmatch", "1" ); } - coop = gi.cvar( "coop", "0", CVAR_LATCH ); + coop = gi.cvar( "coop", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); // if (cv->value) { // gi.dprintf( "Turning coop off.\n" ); // gi.cvar_forceset( "coop", "0" ); // } - dmflags = gi.cvar( "dmflags", "0", CVAR_SERVERINFO ); - fraglimit = gi.cvar( "fraglimit", "0", CVAR_SERVERINFO ); - timelimit = gi.cvar( "timelimit", "0", CVAR_SERVERINFO ); - maptime = gi.cvar("maptime", "0:00", CVAR_SERVERINFO | CVAR_NOSET); - capturelimit = gi.cvar( "capturelimit", "0", CVAR_SERVERINFO ); + dmflags = gi.cvar( "dmflags", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + fraglimit = gi.cvar( "fraglimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + timelimit = gi.cvar( "timelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + maptime = gi.cvar("maptime", "0:00", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + capturelimit = gi.cvar( "capturelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); password = gi.cvar( "password", "", CVAR_USERINFO ); filterban = gi.cvar( "filterban", "1", 0 ); silenceban = gi.cvar( "silenceban", "1", 0); //rekkie -- silence ban - needpass = gi.cvar( "needpass", "0", CVAR_SERVERINFO ); + needpass = gi.cvar( "needpass", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); radiolog = gi.cvar( "radiolog", "0", 0 ); - teamplay = gi.cvar( "teamplay", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + teamplay = gi.cvar( "teamplay", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) motd_time = gi.cvar( "motd_time", "2", 0 ); - hostname = gi.cvar( "hostname", "unnamed", CVAR_SERVERINFO ); + hostname = gi.cvar( "hostname", "unnamed", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); strtwpn = gi.cvar( "dmweapon", MK23_NAME, 0 ); actionmaps = gi.cvar( "actionmaps", "1", 0 ); if (actionmaps->value && num_maps < 1) @@ -397,10 +397,10 @@ void InitGame( void ) nohud = gi.cvar( "nohud", "0", CVAR_LATCH ); hud_team_icon = gi.cvar( "hud_team_icon", "0", 0 ); hud_items_cycle = gi.cvar( "hud_items_cycle", "20", 0 ); - roundlimit = gi.cvar( "roundlimit", "0", CVAR_SERVERINFO ); - limchasecam = gi.cvar( "limchasecam", "0", CVAR_LATCH ); + roundlimit = gi.cvar( "roundlimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); + limchasecam = gi.cvar( "limchasecam", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); skipmotd = gi.cvar( "skipmotd", "0", 0 ); - roundtimelimit = gi.cvar( "roundtimelimit", "0", CVAR_SERVERINFO ); + roundtimelimit = gi.cvar( "roundtimelimit", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); maxteamkills = gi.cvar( "maxteamkills", "0", 0 ); twbanrounds = gi.cvar( "twbanrounds", "2", 0 ); tkbanrounds = gi.cvar( "tkbanrounds", "2", 0 ); @@ -411,15 +411,15 @@ void InitGame( void ) use_voice = gi.cvar( "use_voice", "1", 0 ); //slicer ppl_idletime = gi.cvar( "ppl_idletime", "15", 0 ); use_buggy_bandolier = gi.cvar( "use_buggy_bandolier", "0", 0 ); - use_tourney = gi.cvar( "use_tourney", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) - use_3teams = gi.cvar( "use_3teams", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gmf' (gamemodeflags) - use_randoms = gi.cvar( "use_randoms", "0", CVAR_SERVERINFO | CVAR_LATCH ); // Random weapons and items mode + use_tourney = gi.cvar( "use_tourney", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT ); //Removed in favor of 'gm' (gamemode) + use_3teams = gi.cvar( "use_3teams", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gmf' (gamemodeflags) + use_randoms = gi.cvar( "use_randoms", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH | CVAR_SERVERINFO_EXT); // Random weapons and items mode use_kickvote = gi.cvar( "use_kickvote", "1", 0 ); //slicer use_mapvote = gi.cvar( "use_mapvote", "1", 0 ); //slicer use_scramblevote = gi.cvar( "use_scramblevote", "1", 0 ); //slicer - ctf = gi.cvar( "ctf", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + ctf = gi.cvar( "ctf", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) ctf_forcejoin = gi.cvar( "ctf_forcejoin", "", 0 ); - ctf_mode = gi.cvar( "ctf_mode", "0", CVAR_LATCH ); // Repurposed in 2025 -- now toggles between CTF and CTB modes. Rewards now handled via `ctf_rewards` cvar + ctf_mode = gi.cvar( "ctf_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); // Repurposed in 2025 -- now toggles between CTF and CTB modes. Rewards now handled via `ctf_rewards` cvar ctf_dropflag = gi.cvar( "ctf_dropflag", "1", 0 ); ctf_respawn = gi.cvar( "ctf_respawn", "4", 0 ); ctf_model = gi.cvar( "ctf_model", "male", CVAR_LATCH ); @@ -429,8 +429,8 @@ void InitGame( void ) medkit_instant = gi.cvar( "medkit_instant", "0", 0 ); medkit_max = gi.cvar( "medkit_max", "3", 0 ); medkit_value = gi.cvar( "medkit_value", "25", 0 ); - dom = gi.cvar( "dom", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gmf' (gamemodeflags) - use_grapple = gi.cvar( "use_grapple", "0", 0 ); + dom = gi.cvar( "dom", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gmf' (gamemodeflags) + use_grapple = gi.cvar( "use_grapple", "0", CVAR_SERVERINFO_EXT ); mv_public = gi.cvar( "mv_public", "0", 0 ); //slicer vk_public = gi.cvar( "vk_public", "0", 0 ); //slicer punishkills = gi.cvar( "punishkills", "1", 0 ); //slicer @@ -443,7 +443,7 @@ void InitGame( void ) rrot = gi.cvar( "rrot", "0", 0 ); empty_rotate = gi.cvar( "empty_rotate", "0", 0 ); empty_exec = gi.cvar( "empty_exec", "", 0 ); - llsound = gi.cvar( "llsound", "1", 0 ); + llsound = gi.cvar( "llsound", "1", CVAR_SERVERINFO_EXT ); loud_guns = gi.cvar( "loud_guns", "0", 0 ); sync_guns = gi.cvar( "sync_guns", "1", 0 ); silentwalk = gi.cvar( "silentwalk", "0", 0 ); @@ -461,18 +461,18 @@ void InitGame( void ) video_check_lockpvs = gi.cvar( "video_check_lockpvs", "0", 0 ); video_check_glclear = gi.cvar( "video_check_glclear", "0", 0 ); video_checktime = gi.cvar( "video_checktime", "15", 0 ); - hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH ); //default ON - hc_boost = gi.cvar("hc_boost", "0", CVAR_LATCH); //rekkie -- allow HC to 'boost' the player + hc_single = gi.cvar( "hc_single", "1", CVAR_LATCH | CVAR_SERVERINFO_EXT ); //default ON + hc_boost = gi.cvar("hc_boost", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); //rekkie -- allow HC to 'boost' the player hc_boost_percent = gi.cvar("hc_boost_percent", "100", 0); //rekkie -- allow HC to 'boost' the player hc_silencer = gi.cvar("hc_silencer", "0", 0); //rekkie -- allow HC to 'boost' the player wp_flags = gi.cvar( "wp_flags", WPF_DEFAULT_STR, 0 ); itm_flags = gi.cvar( "itm_flags", ITF_DEFAULT_STR, 0 ); - matchmode = gi.cvar( "matchmode", "0", CVAR_SERVERINFO | CVAR_LATCH ); + matchmode = gi.cvar( "matchmode", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); hearall = gi.cvar( "hearall", "0", 0 ); // used in matchmode deadtalk = gi.cvar( "deadtalk", "0", 0 ); force_skin = gi.cvar( "force_skin", "", 0 ); - teamdm = gi.cvar( "teamdm", "0", CVAR_LATCH ); + teamdm = gi.cvar( "teamdm", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); teamdm_respawn = gi.cvar( "teamdm_respawn", "2", 0 ); respawn_effect = gi.cvar( "respawn_effect", "0", 0 ); @@ -490,26 +490,26 @@ void InitGame( void ) mm_pausecount = gi.cvar( "mm_allowcount", "3", CVAR_LATCH ); mm_pausetime = gi.cvar( "mm_pausetime", "2", CVAR_LATCH ); - teams[TEAM1].teamscore = gi.cvar("t1", "0", CVAR_SERVERINFO | CVAR_NOSET); - teams[TEAM2].teamscore = gi.cvar("t2", "0", CVAR_SERVERINFO | CVAR_NOSET); - teams[TEAM3].teamscore = gi.cvar("t3", "0", CVAR_SERVERINFO | CVAR_NOSET); + teams[TEAM1].teamscore = gi.cvar("t1", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + teams[TEAM2].teamscore = gi.cvar("t2", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); + teams[TEAM3].teamscore = gi.cvar("t3", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); stats_endmap = gi.cvar( "stats_endmap", "1", 0 ); stats_afterround = gi.cvar( "stats_afterround", "0", 0 ); auto_join = gi.cvar( "auto_join", "0", 0 ); auto_equip = gi.cvar( "auto_equip", "0", 0 ); auto_menu = gi.cvar( "auto_menu", "0", 0 ); - eventeams = gi.cvar( "eventeams", "0", 0 ); - use_balancer = gi.cvar( "use_balancer", "0", 0 ); - dm_choose = gi.cvar( "dm_choose", "0", CVAR_LATCH ); - dm_shield = gi.cvar( "dm_shield", "0", 0 ); + eventeams = gi.cvar( "eventeams", "0", CVAR_SERVERINFO_EXT ); + use_balancer = gi.cvar( "use_balancer", "0", CVAR_SERVERINFO_EXT ); + dm_choose = gi.cvar( "dm_choose", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); + dm_shield = gi.cvar( "dm_shield", "0", CVAR_SERVERINFO_EXT ); tourney_lca = gi.cvar( "tourney_lca", "0", 0 ); - use_punch = gi.cvar( "use_punch", "1", 0 ); + use_punch = gi.cvar( "use_punch", "1", CVAR_SERVERINFO_EXT); //TNG:Freud - new spawning system use_oldspawns = gi.cvar( "use_oldspawns", "0", CVAR_LATCH ); //TNG:Freud - ghosts - use_ghosts = gi.cvar( "use_ghosts", "0", CVAR_LATCH ); + use_ghosts = gi.cvar( "use_ghosts", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); radio_max = gi.cvar( "radio_max", "3", 0 ); radio_time = gi.cvar( "radio_time", "2", 0 ); @@ -517,14 +517,14 @@ void InitGame( void ) //SLIC2 radio_repeat_time = gi.cvar( "radio_repeat_time", "1", 0 ); radio_repeat = gi.cvar( "radio_repeat", "2", 0 ); - unique_weapons = gi.cvar( "weapons", "1", CVAR_SERVERINFO | CVAR_LATCH ); // zucc changed teamplay to 1 - unique_items = gi.cvar( "items", "1", CVAR_SERVERINFO | CVAR_LATCH ); + unique_weapons = gi.cvar( "weapons", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); // zucc changed teamplay to 1 + unique_items = gi.cvar( "items", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_LATCH ); ir = gi.cvar( "ir", "1", 0 ); knifelimit = gi.cvar( "knifelimit", "40", 0 ); - allweapon = gi.cvar( "allweapon", "0", 0 ); // Removed it from Serverinfo - allitem = gi.cvar( "allitem", "0", 0 ); // Removed it from Serverinfo + allweapon = gi.cvar( "allweapon", "0", CVAR_SERVERINFO_EXT ); // Removed it from Serverinfo + allitem = gi.cvar( "allitem", "0", CVAR_SERVERINFO_EXT); // Removed it from Serverinfo allow_hoarding = gi.cvar( "allow_hoarding", "0", CVAR_LATCH ); - tgren = gi.cvar( "tgren", "0", CVAR_SERVERINFO ); + tgren = gi.cvar( "tgren", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); //SLIC2 /*flashgren = gi.cvar ("flashgren", "1", 0); flashradius = gi.cvar ("flashradius", "300", 0); @@ -541,17 +541,17 @@ void InitGame( void ) day_cycle = gi.cvar( "day_cycle", "10", 0 ); // Darkmatch cycle time. use_flashlight = gi.cvar( "use_flashlight", "0", 0 ); use_classic = gi.cvar( "use_classic", "0", 0 ); // Reset Grenade Strength to 1.52 - use_gren_bonk = gi.cvar( "use_gren_bonk", "0", 0 ); // Grenade Bonk + use_gren_bonk = gi.cvar( "use_gren_bonk", "0", CVAR_SERVERINFO_EXT ); // Grenade Bonk //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) - breakableglass = gi.cvar("breakableglass", "0", 0); + breakableglass = gi.cvar("breakableglass", "0", CVAR_SERVERINFO_EXT); glassfragmentlimit = gi.cvar("glassfragmentlimit", "30", 0); //CGF_SFX_InstallGlassSupport(); // william for CGF (glass fx) - grenade_drop = gi.cvar( "grenade_drop", "0", 0 ); // Raptor007 - added grenade drop + grenade_drop = gi.cvar( "grenade_drop", "0", CVAR_SERVERINFO_EXT ); // Raptor007 - added grenade drop g_select_empty = gi.cvar( "g_select_empty", "0", CVAR_ARCHIVE ); - g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH); + g_protocol_extensions = gi.cvar("g_protocol_extensions", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); run_pitch = gi.cvar( "run_pitch", "0.002", 0 ); run_roll = gi.cvar( "run_roll", "0.005", 0 ); @@ -587,17 +587,17 @@ void InitGame( void ) game.csr = cs_remap_old; } } - jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO|*/ CVAR_LATCH); // jumping mod -- removed from serverinfo 2022 + jump = gi.cvar ("jump", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT|*/ CVAR_LATCH | CVAR_SERVERINFO_EXT); // jumping mod -- removed from serverinfo 2022 - warmup = gi.cvar( "warmup", "0", CVAR_LATCH ); + warmup = gi.cvar( "warmup", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT ); warmup_bots = gi.cvar( "warmup_bots", "0", CVAR_LATCH ); - round_begin = gi.cvar( "round_begin", "15", 0 ); - spectator_hud = gi.cvar( "spectator_hud", "1", CVAR_LATCH ); + round_begin = gi.cvar( "round_begin", "15", CVAR_SERVERINFO_EXT ); + spectator_hud = gi.cvar( "spectator_hud", "1", CVAR_LATCH | CVAR_SERVERINFO_EXT); - use_mvd2 = gi.cvar( "use_mvd2", "0", 0 ); // JBravo: q2pro MVD2 recording. 0 = off, 1 = on + use_mvd2 = gi.cvar( "use_mvd2", "0", CVAR_SERVERINFO_EXT ); // JBravo: q2pro MVD2 recording. 0 = off, 1 = on // BEGIN AQ2 ETE - esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | */ CVAR_LATCH ); //Removed in favor of 'gm' (gamemode) + esp = gi.cvar( "esp", "0", /*CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | */ CVAR_LATCH | CVAR_SERVERINFO_EXT); //Removed in favor of 'gm' (gamemode) esp_atl = gi.cvar( "esp_atl", "0", 0 ); // This forces ATL mode even if ETV mode is set in the .esp file esp_punish = gi.cvar("esp_punish", "0", 0); esp_etv_halftime = gi.cvar("esp_etv_halftime", "0", CVAR_LATCH); @@ -621,32 +621,32 @@ void InitGame( void ) // 2022 server_id = gi.cvar( "server_id", "", 0 ); // Removed it from Serverinfo - stat_logs = gi.cvar( "stat_logs", "0", CVAR_SERVERINFO); + stat_logs = gi.cvar( "stat_logs", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); if (stat_logs->value) { gi.dprintf( "stat_logs is enabled, forcing logfile_flush to 2\n" ); gi.cvar_forceset("logfile_flush", "2"); } - sv_antilag = gi.cvar("sv_antilag", "1", CVAR_SERVERINFO); - sv_antilag_interp = gi.cvar("sv_antilag_interp", "0", CVAR_SERVERINFO); - sv_limp_highping = gi.cvar("sv_limp_highping", "70", 0); // Removed it from Serverinfo + sv_antilag = gi.cvar("sv_antilag", "1", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_antilag_interp = gi.cvar("sv_antilag_interp", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_limp_highping = gi.cvar("sv_limp_highping", "70", CVAR_SERVERINFO_EXT); // Removed it from Serverinfo mapvote_next_limit = gi.cvar( "mapvote_next_limit", "0", 0); stat_apikey = gi.cvar("stat_apikey", "none", 0); // Never include this in serverinfo! stat_url = gi.cvar("stat_url", "https://apigateway.aq2world.com/api/v1/stats", 0); - gm = gi.cvar("gm", "dm", CVAR_SERVERINFO); - gmf = gi.cvar("gmf", "0", CVAR_SERVERINFO); - sv_idleremove = gi.cvar("sv_idleremove", "0", 0); + gm = gi.cvar("gm", "dm", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + gmf = gi.cvar("gmf", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); + sv_idleremove = gi.cvar("sv_idleremove", "0", CVAR_SERVERINFO_EXT); g_spawn_items = gi.cvar("g_spawn_items", "0", CVAR_LATCH); // 2023 - use_killcounts = gi.cvar("use_killcounts", "0", 0); - zoom_comp = gi.cvar("zoom_comp", "1", 0); - item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH); - gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", 0); + use_killcounts = gi.cvar("use_killcounts", "0", CVAR_SERVERINFO_EXT); + zoom_comp = gi.cvar("zoom_comp", "1", CVAR_SERVERINFO_EXT); + item_kit_mode = gi.cvar("item_kit_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); + gun_dualmk23_enhance = gi.cvar("gun_dualmk23_enhance", "0", CVAR_SERVERINFO_EXT); printrules = gi.cvar("printrules", "0", 0); timedmsgs = gi.cvar("timedmsgs", "0", 0); - mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", 0); - sv_killgib = gi.cvar("sv_killgib", "0", 0); + mm_captain_teamname = gi.cvar("mm_captain_teamname", "0", CVAR_SERVERINFO_EXT); + sv_killgib = gi.cvar("sv_killgib", "0", CVAR_SERVERINFO_EXT); // 2024 warmup_unready = gi.cvar("warmup_unready", "0", 0); @@ -666,7 +666,7 @@ void InitGame( void ) msgflags = gi.cvar("msgflags", "0", 0); use_pickup = gi.cvar("use_pickup", "0", 0); - training_mode = gi.cvar("training_mode", "0", CVAR_LATCH); + training_mode = gi.cvar("training_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); if (training_mode->value){ gi.cvar_forceset("item_respawnmode", "1"); gi.cvar_forceset("items", "2"); @@ -681,18 +681,18 @@ void InitGame( void ) g_highscores_dir = gi.cvar("g_highscores_dir", "highscores", 0); g_highscores_countbots = gi.cvar("g_highscores_countbots", "0", 0); - lca_grenade = gi.cvar("lca_grenade", "0", 0); - knife_catch = gi.cvar("knife_catch", "0", 0); + lca_grenade = gi.cvar("lca_grenade", "0", CVAR_SERVERINFO_EXT); + knife_catch = gi.cvar("knife_catch", "0", CVAR_SERVERINFO_EXT); // 2025 - ctf_rewards = gi.cvar("ctf_rewards", "1", 0); - bots = gi.cvar("bots", "0", CVAR_SERVERINFO); + ctf_rewards = gi.cvar("ctf_rewards", "1", CVAR_SERVERINFO_EXT); + bots = gi.cvar("bots", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT); // new AQtion Extension cvars #ifdef AQTION_EXTENSION - use_newirvision = gi.cvar("use_newirvision", "1", 0); - use_indicators = gi.cvar("use_indicators", "1", 0); - use_xerp = gi.cvar("use_xerp", "1", 0); + use_newirvision = gi.cvar("use_newirvision", "1", CVAR_SERVERINFO_EXT); + use_indicators = gi.cvar("use_indicators", "1", CVAR_SERVERINFO_EXT); + use_xerp = gi.cvar("use_xerp", "1", CVAR_SERVERINFO_EXT); #endif // Discord SDK integration with Q2Pro @@ -712,14 +712,14 @@ void InitGame( void ) ltk_botfile = gi.cvar( "ltk_botfile", "botdata", 0); ltk_loadbots = gi.cvar( "ltk_loadbots", "1", 0); //rekkie -- DEV_1 -- s - bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH); + bot_enable = gi.cvar("bot_enable", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); bot_skill = gi.cvar("bot_skill", "7", 0); // Skill setting for bots, range 0-10. 0 = easy, 10 = aimbot! bot_skill_threshold = gi.cvar("bot_skill_threshold", "0", 0); // Dynamic skill adjustment kicks in if a threshold has been hit bot_remember = gi.cvar("bot_remember", "15", 0); // How long (in seconds) the bot remembers an enemy after visibility has been lost bot_reaction = gi.cvar("bot_reaction", "0.5", 0); // How long (in seconds) until the bot reacts to an enemy in sight bot_showpath = gi.cvar("bot_showpath", "0", 0); bot_maxteam = gi.cvar("bot_maxteam", "0", 0); - bot_playercount = gi.cvar("bot_playercount", "0", 0); + bot_playercount = gi.cvar("bot_playercount", "0", CVAR_SERVERINFO_EXT); bot_rush = gi.cvar("bot_rush", "0", 0); bot_randvoice = gi.cvar("bot_randvoice", "5", 0); bot_randskill = gi.cvar("bot_randskill", "10", 0); @@ -727,13 +727,13 @@ void InitGame( void ) bot_chat = gi.cvar("bot_chat", "0", 0); bot_personality = gi.cvar("bot_personality", "0", CVAR_LATCH); bot_ragequit = gi.cvar("bot_ragequit", "0", 0); - bot_countashuman = gi.cvar("bot_countashuman", "0", 0); + bot_countashuman = gi.cvar("bot_countashuman", "0", CVAR_SERVERINFO_EXT); bot_debug = gi.cvar("bot_debug", "0", 0); bot_count_min = gi.cvar("bot_count_min", "0", 0); bot_count_max = gi.cvar("bot_count_max", "0", 0); bot_rotate = gi.cvar("bot_rotate", "0", 0); - bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH); - bot_reportpings = gi.cvar("bot_reportpings", "0", 0); + bot_reportasclient = gi.cvar("bot_reportasclient", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); + bot_reportpings = gi.cvar("bot_reportpings", "0", CVAR_SERVERINFO_EXT); bot_navautogen = gi.cvar("bot_navautogen", "0", 0); //bot_randteamskin = gi.cvar("bot_randteamskin", "0", 0); gl_shaders = gi.cvar("gl_shaders", "0", 0); @@ -783,7 +783,7 @@ void InitGame( void ) { gi.dprintf ("...server supports GMF_VARIABLE_FPS\n"); - cv = gi.cvar( "sv_fps", NULL, CVAR_SERVERINFO ); + cv = gi.cvar( "sv_fps", NULL, CVAR_SERVERINFO | CVAR_SERVERINFO_EXT ); if( cv ) { int framediv = (int) cv->value / BASE_FRAMERATE; diff --git a/src/server/main.c b/src/server/main.c index 14832f484..8e69764da 100644 --- a/src/server/main.c +++ b/src/server/main.c @@ -100,6 +100,7 @@ cvar_t *sv_enhanced_setplayer; cvar_t *sv_iplimit; cvar_t *sv_status_limit; cvar_t *sv_status_show; +cvar_t *sv_status_ext; cvar_t *sv_uptime; cvar_t *sv_auth_limit; cvar_t *sv_rcon_limit; @@ -523,6 +524,422 @@ static void SVC_Status(void) NET_SendPacket(NS_SERVER, buffer, len, &net_from); } +/* +================ +Extended Status Protocol (statusx) + +Multi-packet extended status response with player statistics +================ +*/ + +#define STATUSX_CHUNK_SIZE 1200 +#define STATUSX_MAX_CHUNKS 32 +#define STATUSX_CACHE_TIME 5000 // 5 seconds + +typedef struct { + char data[STATUSX_CHUNK_SIZE * STATUSX_MAX_CHUNKS]; + size_t total_size; + int chunk_count; + unsigned timestamp; +} statusx_cache_t; + +static statusx_cache_t statusx_cache; + +/* +================ +SV_BuildExtendedStatus + +Builds the complete extended status response with server info, +team data, and detailed player statistics. +Returns the total size written to buffer. +================ +*/ +static size_t SV_BuildExtendedStatus(char *buffer, size_t max_size) +{ + size_t pos = 0; + size_t len; + char entry[1024]; + client_t *cl; + int i, num_clients, num_bots; + + // Count real clients and bots + num_clients = SV_CountClients(); + num_bots = 0; + for (i = 0; i < MAX_CLIENTS; i++) { + if (bot_clients[i].in_use) { + num_bots++; + } + } + + // [SERVER_INFO] + len = Q_scnprintf(entry, sizeof(entry), "[SERVER_INFO]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Server info fields + len = Q_scnprintf(entry, sizeof(entry), "hostname\\%s\n", sv_hostname->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "mapname\\%s\n", sv.name); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "clients\\%i\n", num_clients); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "bots\\%i\n", num_bots); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + len = Q_scnprintf(entry, sizeof(entry), "maxclients\\%i\n", + sv_maxclients->integer - sv_reserved_slots->integer); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Game mode and settings from CVAR_SERVERINFO + // Add common game cvars that clients need to know about + cvar_t *gamename = Cvar_FindVar("gamename"); + cvar_t *fraglimit = Cvar_FindVar("fraglimit"); + cvar_t *timelimit = Cvar_FindVar("timelimit"); + cvar_t *dmflags = Cvar_FindVar("dmflags"); + cvar_t *teamplay = Cvar_FindVar("teamplay"); + + if (gamename) { + len = Q_scnprintf(entry, sizeof(entry), "gamename\\%s\n", gamename->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (fraglimit) { + len = Q_scnprintf(entry, sizeof(entry), "fraglimit\\%s\n", fraglimit->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (timelimit) { + len = Q_scnprintf(entry, sizeof(entry), "timelimit\\%s\n", timelimit->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (dmflags) { + len = Q_scnprintf(entry, sizeof(entry), "dmflags\\%s\n", dmflags->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + if (teamplay) { + len = Q_scnprintf(entry, sizeof(entry), "teamplay\\%s\n", teamplay->string); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + } + + // [PLAYERS] + len = Q_scnprintf(entry, sizeof(entry), "[PLAYERS]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Real clients + FOR_EACH_CLIENT(cl) { + if (cl->state == cs_zombie) { + continue; + } + + int frags = SV_GetClient_Stat(cl, STAT_FRAGS); + int ping = cl->ping; + + len = Q_scnprintf(entry, sizeof(entry), + "%i %i \"%s\"\n", + frags, ping, cl->name); + if (len >= sizeof(entry)) { + continue; + } + if (pos + len >= max_size) { + break; + } + memcpy(buffer + pos, entry, len); + pos += len; + } + + // Bot clients + for (i = 0; i < MAX_CLIENTS; i++) { + if (bot_clients[i].in_use) { + len = Q_scnprintf(entry, sizeof(entry), + "%i %i \"%s\" (BOT)\n", + bot_clients[i].score, bot_clients[i].ping, + bot_clients[i].name); + if (len >= sizeof(entry)) { + continue; + } + if (pos + len >= max_size) { + break; + } + memcpy(buffer + pos, entry, len); + pos += len; + } + } + + // [END] + len = Q_scnprintf(entry, sizeof(entry), "[END]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + return pos; +} + +/* +================ +SV_SendStatusChunk + +Sends a single chunk of the extended status data. +================ +*/ +static void SV_SendStatusChunk(int chunk_num) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + size_t chunk_start; + size_t chunk_data_size; + + if (chunk_num < 0 || chunk_num >= statusx_cache.chunk_count) { + return; + } + + chunk_start = chunk_num * STATUSX_CHUNK_SIZE; + chunk_data_size = (chunk_num == statusx_cache.chunk_count - 1) ? + (statusx_cache.total_size - chunk_start) : STATUSX_CHUNK_SIZE; + + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffstatusxdata %d/%d %zu\n", + chunk_num, statusx_cache.chunk_count, chunk_data_size); + + if (len + chunk_data_size >= sizeof(buffer)) { + Com_DPrintf("Chunk %d too large to send\n", chunk_num); + return; + } + + memcpy(buffer + len, statusx_cache.data + chunk_start, chunk_data_size); + len += chunk_data_size; + + NET_SendPacket(NS_SERVER, buffer, len, &net_from); +} + +/* +================ +SVC_StatusExt + +Handles extended status queries (statusx command). +Supports requesting metadata (-1) or specific chunks. +================ +*/ +static void SVC_StatusExt(void) +{ + int chunk_num; + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + unsigned now; + + if (!sv_status_ext->integer) { + return; // Feature disabled + } + + if (SV_RateLimited(&svs.ratelimit_status)) { + Com_DPrintf("Dropping statusx request from %s\n", + NET_AdrToString(&net_from)); + return; + } + + now = Sys_Milliseconds(); + + // Rebuild cache if expired or first time + if (!statusx_cache.chunk_count || (now - statusx_cache.timestamp) > STATUSX_CACHE_TIME) { + statusx_cache.total_size = SV_BuildExtendedStatus(statusx_cache.data, + sizeof(statusx_cache.data)); + statusx_cache.chunk_count = (statusx_cache.total_size + STATUSX_CHUNK_SIZE - 1) / + STATUSX_CHUNK_SIZE; + statusx_cache.timestamp = now; + } + + // Get requested chunk number (default to 0) + chunk_num = (Cmd_Argc() > 1) ? Q_atoi(Cmd_Argv(1)) : 0; + + // Handle metadata request (-1) + if (chunk_num == -1) { + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffstatusxmeta %d %zu\n", + statusx_cache.chunk_count, statusx_cache.total_size); + NET_SendPacket(NS_SERVER, buffer, len, &net_from); + return; + } + + // Send requested chunk + SV_SendStatusChunk(chunk_num); +} + +/* +================ +Extended Rules Protocol (rulesext) + +Multi-packet extended rules response with CVARs marked CVAR_SERVERINFO_EXT +================ +*/ + +#define RULESEXT_CHUNK_SIZE 1200 +#define RULESEXT_MAX_CHUNKS 32 +#define RULESEXT_CACHE_TIME 5000 // 5 seconds + +typedef struct { + char data[RULESEXT_CHUNK_SIZE * RULESEXT_MAX_CHUNKS]; + size_t total_size; + int chunk_count; + unsigned timestamp; +} rulesext_cache_t; + +static rulesext_cache_t rulesext_cache; + +/* +================ +SV_BuildExtendedRules + +Builds the complete extended rules response with all CVARs +marked with CVAR_SERVERINFO_EXT flag. +Returns the total size written to buffer. +================ +*/ +static size_t SV_BuildExtendedRules(char *buffer, size_t max_size) +{ + size_t pos = 0; + size_t len; + char entry[1024]; + cvar_t *cv; + int count = 0; + + // [EXTENDED_RULES] + len = Q_scnprintf(entry, sizeof(entry), "[EXTENDED_RULES]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + // Add all CVARs marked with CVAR_SERVERINFO_EXT flag + for (cv = cvar_vars; cv; cv = cv->next) { + if (cv->flags & CVAR_SERVERINFO_EXT) { + count++; + len = Q_scnprintf(entry, sizeof(entry), "%s\\%s\n", cv->name, cv->string); + if (len < sizeof(entry) && pos + len < max_size) { + memcpy(buffer + pos, entry, len); + pos += len; + } + } + } + + // [END] + len = Q_scnprintf(entry, sizeof(entry), "[END]\n"); + if (pos + len >= max_size) return pos; + memcpy(buffer + pos, entry, len); + pos += len; + + return pos; +} + +/* +================ +SV_SendRulesChunk + +Sends a single chunk of the extended rules data. +================ +*/ +static void SV_SendRulesChunk(int chunk_num) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + size_t len; + size_t chunk_start; + size_t chunk_data_size; + + if (chunk_num < 0 || chunk_num >= rulesext_cache.chunk_count) { + return; + } + + chunk_start = chunk_num * RULESEXT_CHUNK_SIZE; + chunk_data_size = (chunk_num == rulesext_cache.chunk_count - 1) ? + (rulesext_cache.total_size - chunk_start) : RULESEXT_CHUNK_SIZE; + + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffrulesxdata %d/%d %zu\n", + chunk_num, rulesext_cache.chunk_count, chunk_data_size); + + if (len + chunk_data_size >= sizeof(buffer)) { + Com_DPrintf("Rules chunk %d too large to send\n", chunk_num); + return; + } + + memcpy(buffer + len, rulesext_cache.data + chunk_start, chunk_data_size); + NET_SendPacket(NS_SERVER, buffer, len + chunk_data_size, &net_from); +} + +/* +================ +SVC_RulesExt + +Extended rules query handler +================ +*/ +static void SVC_RulesExt(void) +{ + char buffer[MAX_PACKETLEN_DEFAULT]; + int chunk_num = 0; + size_t len; + + if (!sv_status_ext->integer) { + return; + } + + if (SV_RateLimited(&svs.ratelimit_status)) { + Com_DPrintf("Dropping rulesext request from %s\n", + NET_AdrToString(&net_from)); + return; + } + + // Check if we need to rebuild the cache + if (svs.realtime - rulesext_cache.timestamp > RULESEXT_CACHE_TIME) { + len = SV_BuildExtendedRules(rulesext_cache.data, sizeof(rulesext_cache.data)); + rulesext_cache.total_size = len; + rulesext_cache.chunk_count = (len + RULESEXT_CHUNK_SIZE - 1) / RULESEXT_CHUNK_SIZE; + rulesext_cache.timestamp = svs.realtime; + } + + // Parse chunk number from command + if (Cmd_Argc() > 1) { + chunk_num = atoi(Cmd_Argv(1)); + } + + // Handle metadata request + if (chunk_num == -1) { + len = Q_scnprintf(buffer, sizeof(buffer), + "\xff\xff\xff\xffrulesxmeta %d %zu\n", + rulesext_cache.chunk_count, rulesext_cache.total_size); + NET_SendPacket(NS_SERVER, buffer, len, &net_from); + return; + } + + // Send requested chunk + SV_SendRulesChunk(chunk_num); +} + /* ================ SVC_Ack @@ -1448,6 +1865,8 @@ static const ucmd_t svcmds[] = { { "ping", SVC_Ping }, { "ack", SVC_Ack }, { "status", SVC_Status }, + { "statusx", SVC_StatusExt }, + { "rulesx", SVC_RulesExt }, { "info", SVC_Info }, { "getchallenge", SVC_GetChallenge }, { "connect", SVC_DirectConnect }, @@ -2434,6 +2853,8 @@ void SV_Init(void) sv_status_show = Cvar_Get("sv_status_show", "2", 0); + sv_status_ext = Cvar_Get("sv_status_ext", "0", 0); + sv_status_limit = Cvar_Get("sv_status_limit", "15", 0); sv_status_limit->changed = sv_status_limit_changed; diff --git a/src/server/server.h b/src/server/server.h index a7edebffd..19426db91 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -564,6 +564,7 @@ extern cvar_t *sv_enhanced_setplayer; extern cvar_t *sv_status_limit; extern cvar_t *sv_status_show; +extern cvar_t *sv_status_ext; extern cvar_t *sv_auth_limit; extern cvar_t *sv_rcon_limit; extern cvar_t *sv_uptime; From 38aa123622c10c38ece9cf6b2b013856b6147d73 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 18:56:43 -0500 Subject: [PATCH 965/974] Autoghost (#259) * Fixed one more warning * use_ghosts 2 to autoghost restore * Fail early if ghosts isnt even enabled * NULL check and doc update --- doc/action.md | 2 +- src/action/a_cmds.c | 18 ++++++++++++++++++ src/action/p_client.c | 10 ++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/doc/action.md b/doc/action.md index 31ea3e7ba..f3f5ddc6d 100644 --- a/doc/action.md +++ b/doc/action.md @@ -755,7 +755,7 @@ TNG offers a new spawn code for Teamplay. This mode prevents teams to spawn twic With the use of the ghost command, the client can get back when he has been disconnected and get your frags and stats back. Ghost does require the client to have the same nick and ip when using the ghost command. **Commands:** -- `use_ghosts [0/1]` - when set to on (1), the server allows the clients to use the ghost command. +- `use_ghosts [0/1/2]` - when set to on (1), the server allows the clients to use the ghost command. When set to (2), this will automatically restore ghost data on rejoin (if the ghost exists) - `ghost` - this will restore the scores, stats, teams and items of the client. (client side) ### Bandolier behavior diff --git a/src/action/a_cmds.c b/src/action/a_cmds.c index 59cfcd1bb..f66655a54 100644 --- a/src/action/a_cmds.c +++ b/src/action/a_cmds.c @@ -1258,6 +1258,24 @@ Cmd_Ghost_f Person gets frags/kills/damage/weapon/item/team/stats back if he disconnected */ + +qboolean Ghost_Exist(edict_t *ent) +{ + int i; + gghost_t *ghost; + + if (!use_ghosts->value || num_ghost_players == 0) + return false; + + for (i = 0, ghost = ghost_players; i < num_ghost_players; i++, ghost++) { + if (ghost->ip && ent->client->pers.ip && ghost->netname && ent->client->pers.netname && + !strcmp(ghost->ip, ent->client->pers.ip) && !strcmp(ghost->netname, ent->client->pers.netname)) { + return true; + } + } + return false; +} + void Cmd_Ghost_f(edict_t * ent) { int i = 0, frames_since = 0; diff --git a/src/action/p_client.c b/src/action/p_client.c index c26071024..936b3d56d 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3678,11 +3678,21 @@ qboolean ClientConnect(edict_t * ent, char *userinfo) //guarantee a client is actually making it all the way into the game. //ent->client->pers.connected = true; + qboolean is_bot = false; #ifndef NO_BOTS if(bot_chat->value) BOTLIB_Chat(ent, CHAT_WELCOME); + + if (IS_BOT(ent)) + is_bot = true; #endif + if (!is_bot && use_ghosts->value == 2) { + if (Ghost_Exist(ent)) { + Cmd_Ghost_f(ent); + } + } + return true; } From 689955c7eabcff526da987d3d4645165e91acebe Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 19:35:53 -0500 Subject: [PATCH 966/974] Keeping track of CTF/Dom/TeamDM scores in t1/t2/t3 cvars now (#263) --- src/action/a_ctf.c | 2 ++ src/action/a_dom.c | 8 ++++---- src/action/a_team.c | 10 ++++++++++ src/action/g_local.h | 3 +++ src/action/p_client.c | 6 +++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 7cb149601..9de99f8ce 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -794,6 +794,8 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // Update team scores teams[TEAM1].score = ctfgame.team1; teams[TEAM2].score = ctfgame.team2; + gi.cvar_forceset(teams[TEAM1].teamscore->name, va("%i", ctfgame.team1)); + gi.cvar_forceset(teams[TEAM2].teamscore->name, va("%i", ctfgame.team2)); CTFDynamicRespawnTimer(); // Dynamic respawn time diff --git a/src/action/a_dom.c b/src/action/a_dom.c index 72d61678c..1177928e9 100644 --- a/src/action/a_dom.c +++ b/src/action/a_dom.c @@ -47,9 +47,9 @@ qboolean DomCheckRules( void ) { dom_last_score = level.time; - teams[ TEAM1 ].score += dom_team_flags[ TEAM1 ]; - teams[ TEAM2 ].score += dom_team_flags[ TEAM2 ]; - teams[ TEAM3 ].score += dom_team_flags[ TEAM3 ]; + UpdateTeamScore( TEAM1, teams[ TEAM1 ].score + dom_team_flags[ TEAM1 ] ); + UpdateTeamScore( TEAM2, teams[ TEAM2 ].score + dom_team_flags[ TEAM2 ] ); + UpdateTeamScore( TEAM3, teams[ TEAM3 ].score + dom_team_flags[ TEAM3 ] ); } dom_winner = NOTEAM; @@ -76,7 +76,7 @@ qboolean DomCheckRules( void ) if( winning_teams == 1 ) { // Winner: just show that they hit the score limit, not how far beyond they went. - teams[ dom_winner ].score = max_score; + UpdateTeamScore( dom_winner, max_score ); } else if( winning_teams > 1 ) { diff --git a/src/action/a_team.c b/src/action/a_team.c index 40b00f8ed..adc2d0f06 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1914,6 +1914,16 @@ void CleanLevel (void) void MakeAllLivePlayersObservers(void); +// UpdateTeamScore: Updates team score and syncs with t1/t2/t3 CVARs +void UpdateTeamScore(int team_index, int new_score) +{ + if (team_index < TEAM1 || team_index >= TEAM_TOP) + return; + + teams[team_index].score = new_score; + gi.cvar_forceset(teams[team_index].teamscore->name, va("%i", new_score)); +} + void ResetScores (qboolean playerScores) { int i; diff --git a/src/action/g_local.h b/src/action/g_local.h index c5ba291cd..7cbec2135 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -3057,6 +3057,9 @@ typedef struct { } Message; extern Message *timedMessages; +// Team score management +void UpdateTeamScore(int team_index, int new_score); + void addTimedMessage(int teamNum, edict_t *ent, int seconds, char *msg); void FireTimedMessages(void); diff --git a/src/action/p_client.c b/src/action/p_client.c index 936b3d56d..2d1238f37 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -440,7 +440,7 @@ void Add_Frag(edict_t * ent, int mod) // Increment team score if TeamDM is enabled if(teamdm->value) - teams[ent->client->resp.team].score++; + UpdateTeamScore(ent->client->resp.team, teams[ent->client->resp.team].score + 1); // Streak kill rewards in Deathmatch mode if (deathmatch->value && !teamplay->value) { @@ -468,7 +468,7 @@ void Add_Frag(edict_t * ent, int mod) // Award team with appropriate streak reward count if(teamdm->value) - teams[ent->client->resp.team].score += frags; + UpdateTeamScore(ent->client->resp.team, teams[ent->client->resp.team].score + frags); // AQ:TNG Igor[Rock] changing sound dir if (fraglimit->value && use_warnings->value) { @@ -830,7 +830,7 @@ void Subtract_Frag(edict_t * ent) ent->client->resp.streakKills = 0; ent->client->resp.roundStreakKills = 0; if(teamdm->value) - teams[ent->client->resp.team].score--; + UpdateTeamScore(ent->client->resp.team, teams[ent->client->resp.team].score - 1); } void Add_Death( edict_t *ent, qboolean end_streak ) From c35c6863b1d17027286b698a0358b5e6308b96cf Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 19 Dec 2025 19:38:54 -0500 Subject: [PATCH 967/974] Fixed implicit declaration --- src/action/g_local.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/action/g_local.h b/src/action/g_local.h index 7cbec2135..c11d48578 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -2944,6 +2944,7 @@ extern char ml_creator[101]; void Cmd_Ghost_f (edict_t * ent); void Cmd_AutoRecord_f(edict_t * ent); +qboolean Ghost_Exist(edict_t *ent); typedef struct team_s { From a800fe16155e6e39bc02f9f77980725e4126a433 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 19:43:12 -0500 Subject: [PATCH 968/974] Automatic demo recording, including deathmatch, without lua needed (#243) * Automatic demo recording, including deathmatch, without lua needed * Set ServerAutoRecordDemo to static * Auto demo recording and sv_status_ext doc --- doc/server.md | 19 +++++ src/action/a_game.c | 134 +++++++++++++++++++++++++++++++- src/action/a_game.h | 5 ++ src/action/a_team.c | 16 +--- src/action/acesrc/acebot_cmds.c | 2 +- src/action/g_local.h | 5 +- src/action/g_main.c | 19 ++++- src/action/g_spawn.c | 71 +++++++++++++++++ src/action/p_client.c | 4 + 9 files changed, 255 insertions(+), 20 deletions(-) diff --git a/doc/server.md b/doc/server.md index 9e478e580..1c7abcb64 100644 --- a/doc/server.md +++ b/doc/server.md @@ -255,6 +255,25 @@ Default burst value is 5. Custom *burst* can be specified after an asterisk. Burst specifies initial number of extra packets that are permitted even if they arrive at rate higher than allowed. +### sv\_status\_ext + +Enables extended query protocols for detailed server information. The +standard Quake 2 status and rules commands provide basic server info. +These extended protocols deliver the same information with support for +larger responses via automatic chunking, allowing game mods to expose +additional configuration variables to server browsers and admin tools. +Default value is 0. + +- 0 — extended protocols disabled (default) + +- 1 — extended protocols enabled (statusx and rulesext) + +When enabled, clients can query statusx for enhanced player rosters and +rulesext for extended game rules. Both protocols cache responses for 5 +seconds and share rate limiting with `sv_status_limit`. Game mods can +expose custom configuration by marking cvars with the CVAR_SERVERINFO_EXT +flag, allowing server browsers to query mod-specific settings. + ### sv\_auth\_limit Limits the rate of client connection attempts with invalid password. Default value is 1 invalid authentication attempt per second. diff --git a/src/action/a_game.c b/src/action/a_game.c index 1f94b7cc2..0ca35ce09 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -82,6 +82,8 @@ int num_maps, cur_map, rand_map, num_allvotes; // num_allvotes added by Igor[Roc char motd_lines[MAX_TOTAL_MOTD_LINES][40]; int motd_num_lines; +qboolean is_demo_recording = false; + /* * ReadConfigFile() * Config file format is backwards compatible with Action's, but doesn't need @@ -1515,6 +1517,136 @@ void _PickupRequest (edict_t * ent, pmenu_t * p) Cmd_Pickup_f(ent); } +// Count active (non-spectator) players +static int CountActivePlayers(void) { + int count = 0; + int i; + edict_t *other; + + for (i = 0, other = g_edicts + 1; i < game.maxclients; i++, other++) { + if (!other->inuse || !other->client || !other->client->pers.connected) + continue; + + // Skip MVD spectators + if (other->client->pers.mvdspec) + continue; + + // Skip regular spectators + if (other->client->pers.spectator) + continue; + + count++; + } + return count; +} + +static void ServerAutoRecordDemo(void){ + time_t tnow = 0; + struct tm *now = NULL; + char ltm[MAX_QPATH] = ""; + char mvdstring[MAX_INFO_STRING] = ""; + char filename[MAX_INFO_STRING] = ""; + + // JBravo: Autostart q2pro MVD2 recording on the server + // darksaint: Moved to its own function and enhanced + + // Determine game mode names (short names) + char *gamemode = GamemodeName(true); + char *gamemodeflag = GamemodeFlagName(true); + + // Determine team names + char t1name[MAX_QPATH]; + char t2name[MAX_QPATH]; + char t3name[MAX_QPATH]; + + strcpy(t1name, teams[TEAM1].name); + strcpy(t2name, teams[TEAM2].name); + strcpy(t3name, teams[TEAM3].name); + + + // Cleanup team names + if (teamCount == 3) { + RemoveSpaces(t1name); + RemoveSpaces(t2name); + RemoveSpaces(t3name); + } else if (teamCount == 2) { + RemoveSpaces(t1name); + RemoveSpaces(t2name); + } + + // Construct filename + if (strcmp(gamemodeflag, "NONE") == 0) { + if (teamCount == 3) // 3 Teams + Q_snprintf(filename, sizeof(filename), "%s-%s_%s_%s-%s-%s", gamemode, t1name, t2name, t3name, net_port->string, level.mapname); + else if (teamCount == 2) // Teamplay modes + Q_snprintf(filename, sizeof(filename), "%s-%s_%s-%s-%s", gamemode, t1name, t2name, net_port->string, level.mapname); + else // Anything else (DM?) + Q_snprintf(filename, sizeof(filename), "%s-%s-%s", gamemode, net_port->string, level.mapname); + } else { + if (teamCount == 3) + Q_snprintf(filename, sizeof(filename), "%s-%s-%s_%s_%s-%s-%s", gamemode, gamemodeflag, t1name, t2name, t3name, net_port->string, level.mapname); + else if (teamCount == 2) + Q_snprintf(filename, sizeof(filename), "%s-%s-%s_%s-%s-%s", gamemode, gamemodeflag, t1name, t2name, net_port->string, level.mapname); + else // Anything else (GMFlags don't apply to non-teamplay but this is here just in case) + Q_snprintf(filename, sizeof(filename), "%s-%s-%s", gamemode, net_port->string, level.mapname); + } + + tnow = time(NULL); + now = localtime(&tnow); + strftime( ltm, 64, "%Y%m%d-%H%M%S", now ); + Q_snprintf( mvdstring, sizeof(mvdstring), "mvdrecord -z %s-%s\n", ltm, filename ); + gi.AddCommandString( mvdstring ); + gi.bprintf( PRINT_HIGH, "Starting MVD recording to file %s-%s.mvd2.gz\n", ltm, filename ); + is_demo_recording = true; + // JBravo: End MVD2 +} + +void StartAutoRecordDemo(void){ + // Skip this if demo recording is disabled + if(!use_mvd2->value) { + gi.dprintf("use_mvd2 is not enabled, not automatically recording demo\n"); + return; + } + + // Demo is already recording + if (is_demo_recording) { + return; + } + + // TODO #1: For deathmatch servers, only record if timelimit or fraglimit is set + // This prevents infinite demos on servers without limits + qboolean is_deathmatch = (!teamplay->value && !ctf->value && !use_tourney->value && + !dom->value && !esp->value && !jump->value); + + if (is_deathmatch && timelimit->value == 0 && fraglimit->value == 0) { + gi.dprintf("Deathmatch server has no timelimit or fraglimit, not recording demo\n"); + return; + } + + ServerAutoRecordDemo(); +} + +void StopAutoRecordDemo(void){ + // Skip this if demo recording is disabled + if(!use_mvd2->value && is_demo_recording) { + gi.dprintf("use_mvd2 is not enabled, but stopping in-progress demo anyway\n"); + gi.AddCommandString( "mvdstop\n" ); + return; + } + + if(!use_mvd2->value) { + gi.dprintf("use_mvd2 is not enabled, we can't stop what isn't started\n"); + return; + } + + if (!is_demo_recording) { + gi.dprintf("Demo is/was not currently recording!\n"); + return; + } + + gi.AddCommandString( "mvdstop\n" ); + is_demo_recording = false; +} void ReadLrconConfig(void) { FILE *config_file; @@ -1634,4 +1766,4 @@ void ReadLrconConfig(void) } else { gi.dprintf("LRCON: Not enabled in config\n"); } -} \ No newline at end of file +} diff --git a/src/action/a_game.h b/src/action/a_game.h index b7093c45d..d103e55c2 100644 --- a/src/action/a_game.h +++ b/src/action/a_game.h @@ -149,6 +149,11 @@ void MakeAllLivePlayersObservers( void ); void Cmd_NextMap_f( edict_t * ent ); void Cmd_PrintRules_f(edict_t *ent); +// Demo recording +void StartAutoRecordDemo(void); +void StopAutoRecordDemo(void); +int CountActivePlayers(void); + //a_game.c #if AQTION_CURL diff --git a/src/action/a_team.c b/src/action/a_team.c index adc2d0f06..4d3df444d 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -2818,10 +2818,6 @@ int CheckTeamRules (void) int winner = WINNER_NONE, i; int checked_tie = 0; char buf[1024]; - struct tm *now = NULL; - time_t tnow = 0; - char ltm[64] = ""; - char mvdstring[512] = ""; if (round_delay_time && use_tourney->value) { @@ -3041,17 +3037,7 @@ int CheckTeamRules (void) CenterPrintAll( buf ); team_round_countdown = warmup_length * 10 + 2; - // JBravo: Autostart q2pro MVD2 recording on the server - if( use_mvd2->value ) - { - tnow = time(NULL); - now = localtime(&tnow); - strftime( ltm, 64, "%Y%m%d-%H%M%S", now ); - Q_snprintf( mvdstring, sizeof(mvdstring), "mvdrecord %s-%s\n", ltm, level.mapname ); - gi.AddCommandString( mvdstring ); - gi.bprintf( PRINT_HIGH, "Starting MVD recording to file %s-%s.mvd2\n", ltm, level.mapname ); - } - // JBravo: End MVD2 + StartAutoRecordDemo(); } } } diff --git a/src/action/acesrc/acebot_cmds.c b/src/action/acesrc/acebot_cmds.c index 54cb68997..f8c59841d 100644 --- a/src/action/acesrc/acebot_cmds.c +++ b/src/action/acesrc/acebot_cmds.c @@ -141,7 +141,7 @@ qboolean ACECM_Commands(edict_t *ent) /////////////////////////////////////////////////////////////////////// // Called when the level changes, store maps and bots (disconnected) /////////////////////////////////////////////////////////////////////// -void ACECM_Store() +void ACECM_Store(void) { return; //rekkie -- DEV_1 -- don't call this anymore, nodes will be auto generated by the new nav system diff --git a/src/action/g_local.h b/src/action/g_local.h index c11d48578..3607be9ae 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -1138,7 +1138,7 @@ typedef enum { #define GMN_DOMINATION "Domination" #define GMN_ESPIONAGE "Espionage" #define GMN_JUMP "Jump" -#define GMN_3TEAMS "3 Teams" +#define GMN_3TEAMS "3Teams" //#define GMN_NEW_MODE 2 // If new game mode flags are created, use 2 for its value first #define GMN_DARKMATCH "Darkmatch" #define GMN_MATCHMODE "Matchmode" @@ -1652,6 +1652,7 @@ void door_use(edict_t* self, edict_t* other, edict_t* activator); // from a_cmds.c void _SetSniper(edict_t * ent, int zoom); +void RemoveSpaces(char *s); //rekkie -- DEV_1 -- e // @@ -1782,6 +1783,8 @@ void G_UpdateSpectatorStatusbar( void ); void G_UpdatePlayerStatusbar( edict_t *ent, int force ); int Gamemodeflag(void); int Gamemode(void); +char* GamemodeName(qboolean shortname); +char* GamemodeFlagName(qboolean shortname); #if USE_AQTION #define GENERATE_UUID() generate_uuid() void generate_uuid(void); diff --git a/src/action/g_main.c b/src/action/g_main.c index dd49acdee..c7d10e8fe 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -270,6 +270,10 @@ #include #include "g_local.h" +#include "a_game.h" + +// Demo recording state +extern qboolean is_demo_recording; game_locals_t game; level_locals_t level; @@ -929,8 +933,10 @@ void EndDMLevel (void) // JBravo: Stop q2pro MVD2 recording if (use_mvd2->value) { - Q_snprintf( mvdstring, sizeof(mvdstring), "mvdstop\n" ); - gi.AddCommandString( mvdstring ); + // If we were reecording a demo, stop it here + StopAutoRecordDemo(); + // Q_snprintf( mvdstring, sizeof(mvdstring), "mvdstop\n" ); + // gi.AddCommandString( mvdstring ); gi.bprintf( PRINT_HIGH, "Ending MVD recording.\n" ); } // JBravo: End MVD2 @@ -1299,6 +1305,15 @@ void G_RunFrame (void) if( level.intermission_framenum && empty ) level.intermission_exit = 1; + // TODO #2: Stop demo if no active players (all spectators or empty server) + if (use_mvd2->value && is_demo_recording) { + int active_players = CountActivePlayers(); + if (active_players == 0) { + gi.dprintf("No active players, stopping demo recording\n"); + StopAutoRecordDemo(); + } + } + // exit intermissions if (level.intermission_exit) { diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 57336aa35..38ee4ac78 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1106,6 +1106,77 @@ int Gamemodeflag(void) return gamemodeflag; } +char* GamemodeName(qboolean shortname) +{ + static char gamemode[64] = ""; + + if (esp->value) + if (!shortname) + strcpy(gamemode, GMN_ESPIONAGE); + else + strcpy(gamemode, "ESP"); + else if (ctf->value) + if (!shortname) + strcpy(gamemode, GMN_CTF); + else + strcpy(gamemode, "CTF"); + else if (dom->value) + if (!shortname) + strcpy(gamemode, GMN_DOMINATION); + else + strcpy(gamemode, "DOM"); + else if (use_tourney->value) + if (!shortname) + strcpy(gamemode, GMN_TOURNEY); + else + strcpy(gamemode, "TO"); + else if (teamdm->value) + if (!shortname) + strcpy(gamemode, GMN_TEAMDM); + else + strcpy(gamemode, "TDM"); + else if (teamplay->value) + if (!shortname) + strcpy(gamemode, GMN_TEAMPLAY); + else + strcpy(gamemode, "TP"); + else if (jump->value) + // "Jump" is already a short name + strcpy(gamemode, GMN_JUMP); + else + if (!shortname) + strcpy(gamemode, GMN_DEATHMATCH); + else + strcpy(gamemode, "DM"); + + return gamemode; +} + +char* GamemodeFlagName(qboolean shortname) +{ + static char gamemode[64] = ""; + if (matchmode->value) + if (!shortname) + strcpy(gamemode, GMN_MATCHMODE); + else + strcpy(gamemode, "MM"); + else if (use_3teams->value) + if (!shortname) + strcpy(gamemode, GMN_3TEAMS); + else + strcpy(gamemode, "3T"); + else if (darkmatch->value) + if (!shortname) + strcpy(gamemode, GMN_DARKMATCH); + else + strcpy(gamemode, "DARK"); + else + strcpy(gamemode, "NONE"); + + return gamemode; +} + + /* ============== SpawnEntities diff --git a/src/action/p_client.c b/src/action/p_client.c index 2d1238f37..695037072 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -3398,6 +3398,10 @@ STAT_BOT_CHECK(); G_UpdatePlayerStatusbar(ent, 1); + // Begin recording a demo if we're setup correctly + if (use_mvd2->value == 2) // Must be set to 2, 1 is classic behavior of teamplay-only recording + StartAutoRecordDemo(); // Even though this is called on every player join, only the 'first' player initiates the demo + // make sure all view stuff is valid ClientEndServerFrame(ent); } From 6b2d54484b0a25b2b83fb0981a30e84bdee64160 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 19:44:07 -0500 Subject: [PATCH 969/974] training mode (#254) * training mode bot spawnpoint initial support * Added spawn settings for training mode * A few more behavior flags, logic gates * Initial minor success? * Still some debug statements, but bots spawn in.. player doesn't * Renamed training_mode to just training * Segfaults look to be addressed * Botflags are properly set * Fixed a few edge cases * Fixed respawning issue * Added BOT_DUMMY and BOT_IGNORE_ALL aliases * Minor cleanup * Adjusted some training mode specific settings, and botflags * Smarter about auto adding bots * Keep the doors around in training mode * Blocking idle sounds and auto disabling use_rewards * Item drop timer reduce, giving bots only a knife to start with * Fixed idle sounds played in training mode * Reduced complexity of item/weapon respawn * More elegant way to force item respawns * Should finally silence insane sounds in training mode * Testing if doors are causing bglass to pop * Reverted door removal code * Set glass to no_knockback to prevent movement * Added comment in glass * Experimental trigger_gravity changes * Set the if statement above the trigger statement * Trying CONTENTS_MIST as the 'vacuum of space' option * Trying CONTENTS_MIST as the 'vacuum of space' option * Added CONTENTS_MIST as part of MASK_WATER * Revert "Added comment in glass" This reverts commit 8efba3d99f983026a3875d8e620dcd97cb796417. * Reverted * Fixed some training mode bugs --- src/action/a_esp.c | 2 +- src/action/a_team.c | 10 + src/action/a_team.h | 6 + src/action/acesrc/acebot_ai.c | 13 + src/action/acesrc/acebot_movement.c | 2 + src/action/acesrc/acebot_nodes.c | 30 ++ src/action/botlib/botlib_ai.c | 368 +++++++++++------------ src/action/botlib/botlib_cmd.c | 7 +- src/action/botlib/botlib_communication.c | 3 +- src/action/botlib/botlib_movement.c | 4 + src/action/botlib/botlib_nodes.c | 4 + src/action/botlib/botlib_spawn.c | 51 +++- src/action/botlib/botlib_spawnpoints.c | 20 ++ src/action/cgf_sfx_glass.c | 3 +- src/action/g_combat.c | 2 +- src/action/g_items.c | 22 +- src/action/g_local.h | 37 ++- src/action/g_main.c | 2 +- src/action/g_save.c | 5 +- src/action/g_spawn.c | 47 ++- src/action/p_client.c | 207 ++++++++++++- src/action/p_weapon.c | 9 +- 22 files changed, 616 insertions(+), 238 deletions(-) diff --git a/src/action/a_esp.c b/src/action/a_esp.c index 643aa7140..faf801cd5 100644 --- a/src/action/a_esp.c +++ b/src/action/a_esp.c @@ -1231,7 +1231,7 @@ edict_t *SelectEspSpawnPoint(edict_t *ent) cname = "info_player_team2"; break; case TEAM3: - cname = "info_player_deathmatch"; + cname = "info_player_team3"; break; default: cname = "info_player_deathmatch"; diff --git a/src/action/a_team.c b/src/action/a_team.c index 4d3df444d..295682440 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -347,6 +347,10 @@ static size_t transparentEntryCount = 0; transparent_list_t *transparent_list = NULL; static transparent_list_t *transparentlistFree = NULL; +void SP_info_player_team3(edict_t * self) +{ +} + void InitTransparentList( void ) { transparent_list = NULL; @@ -4009,6 +4013,12 @@ void GetSpawnPoints (void) potential_spawns[num_potential_spawns] = spot; num_potential_spawns++; } + + if ((spot = G_Find (spot, FOFS (classname), "info_player_team3")) != NULL) + { + potential_spawns[num_potential_spawns] = spot; + num_potential_spawns++; + } spot = NULL; while ((spot = G_Find (spot, FOFS (classname), "info_player_deathmatch")) != NULL) diff --git a/src/action/a_team.h b/src/action/a_team.h index dfdb87f72..85fc4ef91 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -45,6 +45,12 @@ #define WINNER_NONE NOTEAM #define WINNER_TIE TEAM_TOP +/*QUAKED info_player_team2 (0 0 1) (-16 -16 -24) (16 16 32) +potential team3 spawning position for 3team games +*/ +//Prototype +void SP_info_player_team3(edict_t * self); + // Pre- and post-trace code for our teamplay anti-stick stuff. If there are // still "transparent" (SOLID_TRIGGER) players, they need to be set to // SOLID_BBOX before a trace is performed, then changed back again diff --git a/src/action/acesrc/acebot_ai.c b/src/action/acesrc/acebot_ai.c index 71f5f3c96..fc384b9b7 100644 --- a/src/action/acesrc/acebot_ai.c +++ b/src/action/acesrc/acebot_ai.c @@ -251,6 +251,11 @@ void BOTLIB_PickLongRangeGoal(edict_t* self) VectorCopy(spot->s.origin, sp_origin[sp_counter]); sp_counter++; } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team3")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + sp_counter++; + } if (sp_counter) // If we found spawn points { byte spot_picked = rand() % sp_counter; // Pick a random spot @@ -1008,8 +1013,16 @@ qboolean BOTLIB_FindEnemy(edict_t *self) // If a player enables notarget, disable search for ALL enemies for (i = 0; i <= num_players; i++) { + if (players[i] == NULL || !players[i]->inuse) // obviously ignore dead players + continue; + if (!self->bot_spawnpoint) // if we don't have a spawnpoint, don't think anymore + return false; if (players[i] != NULL && players[i]->flags & FL_NOTARGET) return false; + if (players[i]->is_bot && self->bot_spawnpoint && self->bot_spawnpoint->botflags & BOT_IGNORE_BOTS) // Ignore bots + return false; + if ((!players[i]->is_bot && self->bot_spawnpoint && self->bot_spawnpoint->botflags & BOT_IGNORE_PLAYERS)) // Ignore players + return false; } // Update previous enemy diff --git a/src/action/acesrc/acebot_movement.c b/src/action/acesrc/acebot_movement.c index 5fef11690..242b6ee14 100644 --- a/src/action/acesrc/acebot_movement.c +++ b/src/action/acesrc/acebot_movement.c @@ -2926,6 +2926,8 @@ void BOTLIB_Attack(edict_t* self, usercmd_t* ucmd) if (self->enemy == NULL) return; if (self->enemy && self->enemy->deadflag != DEAD_NO) return; if (teamplay->value && lights_camera_action > 0) return; + if (training->value && + (self->bot_spawnpoint && self->bot_spawnpoint->botflags & BOT_NOSHOOT)) return; //Com_Printf("%s [%d] %s attack enemy %s\n", __func__, level.framenum, self->client->pers.netname, self->enemy->client->pers.netname); diff --git a/src/action/acesrc/acebot_nodes.c b/src/action/acesrc/acebot_nodes.c index da22ee894..5918b1b79 100644 --- a/src/action/acesrc/acebot_nodes.c +++ b/src/action/acesrc/acebot_nodes.c @@ -1115,6 +1115,36 @@ void BOTLIB_SelfExpandNodesFromSpawnpoints(edict_t *ent) sp_counter++; } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team3")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + + // Try crawling down + sp_origin[sp_counter][2] += 0.01; + tr = gi.trace(sp_origin[sp_counter], mins, maxs, tv(sp_origin[sp_counter][0], sp_origin[sp_counter][1], sp_origin[sp_counter][2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + else + sp_origin[sp_counter][2] = tr.endpos[2]; + + sp_counter++; + } + + /// Remove this later? + while ((spot = G_Find(spot, FOFS(classname), "info_bot_deathmatch")) != NULL) + { + VectorCopy(spot->s.origin, sp_origin[sp_counter]); + + // Try crawling down + sp_origin[sp_counter][2] += 0.01; + tr = gi.trace(sp_origin[sp_counter], mins, maxs, tv(sp_origin[sp_counter][0], sp_origin[sp_counter][1], sp_origin[sp_counter][2] - NODE_MAX_CROUCH_FALL_HEIGHT_UNSAFE), ent, MASK_PLAYERSOLID); + if (tr.startsolid) + continue; + else + sp_origin[sp_counter][2] = tr.endpos[2]; + + sp_counter++; + } // Qsort in order of highest to lowest z value qsort(sp_origin, sp_counter, sizeof(vec3_t), SortByHeight); diff --git a/src/action/botlib/botlib_ai.c b/src/action/botlib/botlib_ai.c index dde2a0058..58d6bcb5b 100644 --- a/src/action/botlib/botlib_ai.c +++ b/src/action/botlib/botlib_ai.c @@ -16,6 +16,12 @@ void BOTLIB_Init(edict_t* self) self->show_node_links = INVALID; self->show_node_links_time = 0; + // Create dummy spawnpoint in non-training modes + if (!training->value) { + self->bot_spawnpoint = G_Spawn(); + self->bot_spawnpoint->botflags = 0; + } + //RiEvEr - new node pathing system memset(&(self->pathList), 0, sizeof(self->pathList)); self->pathList.head = self->pathList.tail = NULL; @@ -629,14 +635,42 @@ int BOTLIB_AutoAdjustSkill(edict_t * self) */ } -/////////////////////////////////////////////////////////////////////// -// Main Think function for bot -/////////////////////////////////////////////////////////////////////// -void BOTLIB_Think(edict_t* self) +static void BOTLIB_Think_Training(edict_t* self) { - usercmd_t ucmd; + int botflags = 0; + + if (self->bot_spawnpoint) + botflags = self->bot_spawnpoint->botflags; + + // Do not move if botflags includes BOT_NOMOVE + if (botflags & BOT_NOMOVE) { + self->bot.state = BOT_MOVE_STATE_STAND; + } - //rekkie -- Fake Bot Client -- s + if (botflags & BOT_NOSHOOT) { + // Does not attack + self->enemy = NULL; + self->bot.bi.actionflags &= ~ACTION_ATTACK; + } + + if (botflags & BOT_IGNORE_PLAYERS) { + if (self->enemy && !self->enemy->is_bot) { + self->enemy = NULL; + self->bot.bi.actionflags &= ~ACTION_ATTACK; + } + } + + if (botflags & BOT_IGNORE_BOTS) { + if (self->enemy && self->enemy->is_bot) { + self->enemy = NULL; + self->bot.bi.actionflags &= ~ACTION_ATTACK; + } + } + +} + +static void BOTLIB_Think_Client(edict_t* self) +{ if (level.framenum % 10 == 0) // Update bot info every 10th frame { // Set self->client->ping to random bot->bot_baseline_ping, then vary by +-3 (ping jitter) @@ -644,8 +678,6 @@ void BOTLIB_Think(edict_t* self) if (ping_jitter && (rand() % 1) == 0) ping_jitter -= (ping_jitter * 2); // Negative jitter - //Com_Printf("%s %s [%d + %d]\n", __func__, self->client->pers.netname, self->bot.bot_baseline_ping, ping_jitter); - self->bot.bot_ping = self->bot.bot_baseline_ping + ping_jitter; if (self->bot.bot_ping < 5) self->bot.bot_ping = 1; // Min ping self->client->ping = self->bot.bot_ping; @@ -653,8 +685,61 @@ void BOTLIB_Think(edict_t* self) if (bot_reportasclient->value) { SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); // So the server can fake the bot as a 'client' } - //gi.SV_BotUpdateInfo(self->client->pers.netname, self->bot.bot_ping, self->client->resp.score); } +} + +static void BOTLIB_Think_Respawn(edict_t* self, usercmd_t* ucmd) +{ + + // If the bot is dead and we're not respawning, then we're leaving the server + if (self->bot_spawnpoint && self->bot_spawnpoint->botflags & BOT_NORESPAWN) { + BOTLIB_RemoveBot(self->client->pers.netname); + } + // Let's respawn! + self->client->buttons = 0; + ucmd->buttons = BUTTON_ATTACK; +} + + +static void BOTLIB_Think_Gamemode(edict_t* self) +{ + if (ctf->value) { // CTF Goals + BOTLIB_CTF_Goals(self); + } else if (esp->value) { // ESP Goals + // Do nothing until this is fixed + BOTLIB_ESP_Goals(self); + } + // If nothing else, bot will go into search & destroy mode + // Check if the bot is in a NAV state (I need a nav) or if NONE + else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE){ + BOTLIB_PickLongRangeGoal(self); + } + + // Non-teamplay stuck suicide and no training mode + if (!teamplay->value) { + if (self->bot.node_travel_time > 120) { + if (!training->value) { + killPlayer(self, true); + } + } + // Too often teamplay bots will suicide because there's a bit of waiting around + } else if (self->bot.node_travel_time > 160 && + current_round_length > 60 && + !lights_camera_action && + !holding_on_tie_check) { + BOTLIB_PickLongRangeGoal(self); + } +} + +/////////////////////////////////////////////////////////////////////// +// Main Think function for bot +/////////////////////////////////////////////////////////////////////// +void BOTLIB_Think(edict_t* self) +{ + usercmd_t ucmd; + + //rekkie -- Fake Bot Client -- s + BOTLIB_Think_Client(self); //rekkie -- Fake Bot Client -- e // Set up client movement @@ -675,11 +760,9 @@ void BOTLIB_Think(edict_t* self) goto end_think; } - // Force respawn - if (self->deadflag == DEAD_DEAD) - { - self->client->buttons = 0; - ucmd.buttons = BUTTON_ATTACK; + // Respawn logic + if (self->deadflag == DEAD_DEAD) { + BOTLIB_Think_Respawn(self, &ucmd); } // Don't execute thinking code if not alive @@ -701,104 +784,8 @@ void BOTLIB_Think(edict_t* self) if (level.framenum < 15) // Wait for a little before processing AI on a new map goto end_think; // Skip bot logic - //gi.dprintf("%s: My bot state is %d\n", __func__, self->bot.state); - if (ctf->value) // CTF Goals - { - BOTLIB_CTF_Goals(self); - } - else if (esp->value) // ESP Goals - { - // Do nothing until this is fixed - BOTLIB_ESP_Goals(self); - } - - // Check if the bot is in a NAV state (I need a nav) or if NONE - else if (self->bot.state == BOT_MOVE_STATE_NAV || self->bot.state == BOT_MOVE_STATE_NONE) - { - /* - if (rand() % 2) // Area 1 - { - BOTLIB_CanGotoNode(self, nodes[64].nodenum, 0); - } - else // Area 9 - { - BOTLIB_CanGotoNode(self, nodes[3344].nodenum, 0); - } - */ - - /* - //int curr_node = ACEND_FindClosestReachableNode(self, NODE_DENSITY, NODE_ALL); - //if (curr_node != 2505) - if (1 && rand() % 2) // Area 0 - { - if (rand() % 2) - BOTLIB_CanGotoNode(self, nodes[2505].nodenum, 0); - else - BOTLIB_CanGotoNode(self, nodes[3346].nodenum, 0); - } - else // Area 1 - { - if (rand() % 2) - BOTLIB_CanGotoNode(self, nodes[999].nodenum, 0); - else - BOTLIB_CanGotoNode(self, nodes[38].nodenum, 0); - } - */ - - //else // DM and TP goals - { - //self->bot.pause_time = 100; - //Com_Printf("\n%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() -------------------- \n", __func__, self->client->pers.netname, level.framenum); - - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal()\n", __func__, self->client->pers.netname, level.framenum); - - //nav_area.total_areas = 0; // Turn off area based nav - - // THIS IS WHERE THE bot.current_node is set // - BOTLIB_PickLongRangeGoal(self); - - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV BOTLIB_PickLongRangeGoal() curr[%d] goal[%d] -------------------- \n", __func__, self->client->pers.netname, level.framenum, self->bot.current_node, self->bot.goal_node); - } - } - /* - if (self->bot.state == BOT_MOVE_STATE_NAV_NEXT) // If map has nodes grouped into areas - { - //self->bot.pause_time = 100; - //Com_Printf("%s %s [%d] BOT_MOVE_STATE_NAV_NEXT BOTLIB_GetNextAreaNode() -------------------- \n", __func__, self->client->pers.netname, level.framenum); - BOTLIB_GetNextAreaNode(self); // Get next area - } - */ - - - - // Kill the bot if completely stuck somewhere - //if(VectorLength(self->velocity) > 37) // - // self->suicide_timeout = level.framenum + 10.0 * HZ; - //if( self->suicide_timeout < level.framenum && !teamplay->value ) - // killPlayer( self, true ); - - // Kill the bot if they've not moved between nodes in a timely manner, stuck! - //gi.dprintf("%s is currently at node %i\n", self->client->pers.netname, self->bot.current_node); - - // Non-teamplay stuck suicide - if (!teamplay->value) { - if (self->bot.node_travel_time > 120) { - killPlayer(self, true); - } - // Too often teamplay bots will suicide because there's a bit of waiting around - } else if (self->bot.node_travel_time > 160 && - current_round_length > 60 && - !lights_camera_action && - !holding_on_tie_check) { - BOTLIB_PickLongRangeGoal(self); - //killPlayer(self, true); - } - - // Find any short range goal - //ACEAI_PickShortRangeGoal(self); - - //BOTLIB_GetWeaponsAndAmmo(self); //rekkie -- Locate and pickup a primary weapon if we need one - + // Make decisions based on gamemode + BOTLIB_Think_Gamemode(self); if (0) // Always follow player -- && (level.framenum % HZ == 0)) { @@ -820,115 +807,96 @@ void BOTLIB_Think(edict_t* self) } } + BOTLIB_FindVisibleAllies(self); // Find visible allies + BOTLIB_Radio(self, &ucmd); + self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies + BOTLIB_Reload(self); // Reload the weapon if needed - - - //if (1) + // This doesn't mean that the bot sees itself as an enemy + // self->enemy is which bot the current self->bot is targeting + if (self->enemy) { - BOTLIB_FindVisibleAllies(self); // Find visible allies - BOTLIB_Radio(self, &ucmd); - self->bot.see_enemies = BOTLIB_FindEnemy(self); // Find visible enemies - BOTLIB_Reload(self); // Reload the weapon if needed - - // This doesn't mean that the bot sees itself as an enemy - // self->enemy is which bot the current self->bot is targeting - if (self->enemy) + // Chase after the new enemy + if (self->bot.enemy_chase_time < level.framenum && self->enemy->bot.current_node != self->bot.goal_node) { - // Chase after the new enemy - if (self->bot.enemy_chase_time < level.framenum && self->enemy->bot.current_node != self->bot.goal_node) + qboolean chase_enemy = false; + if ((FindItem(HC_NAME) == self->client->weapon || + FindItem(M3_NAME) == self->client->weapon || + FindItem(DUAL_NAME) == self->client->weapon || + FindItem(KNIFE_NAME) == self->client->weapon) + && self->bot.enemy_dist > 200) { - qboolean chase_enemy = false; - if ((FindItem(HC_NAME) == self->client->weapon || - FindItem(M3_NAME) == self->client->weapon || - FindItem(DUAL_NAME) == self->client->weapon || - FindItem(KNIFE_NAME) == self->client->weapon) - && self->bot.enemy_dist > 200) - { - chase_enemy = true; - self->bot.enemy_chase_time = level.framenum + 1 * HZ; // Delay next call - } - else if (FindItem(SNIPER_NAME) == self->client->weapon)// && self->bot.enemy_dist > 1500) - { - chase_enemy = false; - self->bot.enemy_chase_time = level.framenum + (((rand() % 20) + 10) * HZ); // Delay next call - } - else if ((FindItem(M4_NAME) == self->client->weapon || FindItem(MP5_NAME) == self->client->weapon) && self->bot.enemy_dist > 1024) - { - chase_enemy = true; - self->bot.enemy_chase_time = level.framenum + (((rand() % 10) + 10) * HZ); // Delay next call - } + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + 1 * HZ; // Delay next call + } + else if (FindItem(SNIPER_NAME) == self->client->weapon)// && self->bot.enemy_dist > 1500) + { + chase_enemy = false; + self->bot.enemy_chase_time = level.framenum + (((rand() % 20) + 10) * HZ); // Delay next call + } + else if ((FindItem(M4_NAME) == self->client->weapon || FindItem(MP5_NAME) == self->client->weapon) && self->bot.enemy_dist > 1024) + { + chase_enemy = true; + self->bot.enemy_chase_time = level.framenum + (((rand() % 10) + 10) * HZ); // Delay next call + } - if (chase_enemy) + if (chase_enemy) + { + // Get enemy node + if (BOTLIB_CanGotoNode(self, self->enemy->bot.current_node, false)) // Make sure we can visit the node they're at { - // Get enemy node - if (BOTLIB_CanGotoNode(self, self->enemy->bot.current_node, false)) // Make sure we can visit the node they're at - { - //self->bot.state = BOT_MOVE_STATE_MOVE; - //BOTLIB_SetGoal(self, self->enemy->bot.current_node); - //Com_Printf("%s %s visiting enemy %s node %i [delay: %i vs %i] [wep: %s]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->bot.current_node, self->bot.enemy_chase_time, level.framenum, self->client->weapon->pickup_name); - } + //self->bot.state = BOT_MOVE_STATE_MOVE; + //BOTLIB_SetGoal(self, self->enemy->bot.current_node); + //Com_Printf("%s %s visiting enemy %s node %i [delay: %i vs %i] [wep: %s]\n", __func__, self->client->pers.netname, self->enemy->client->pers.netname, self->enemy->bot.current_node, self->bot.enemy_chase_time, level.framenum, self->client->weapon->pickup_name); } } } + } - // If the bot is on a slope, raise it up depending on the slope normal and the bot mins/maxs hit box - { - self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 48), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - - /* - self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 128), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - vec3_t exp_up; - VectorCopy(self->bot.touch_ground.plane.normal, exp_up); - exp_up[2] = 0; - VectorNormalize(exp_up); - exp_up[2] = 1; - VectorScale(exp_up, 24, exp_up); - // Feed the raised up position back into the trace - VectorAdd(self->bot.touch_ground.endpos, exp_up, self->bot.touch_ground.endpos); - self->bot.touch_ground = gi.trace(self->bot.touch_ground.endpos, NULL, NULL, self->bot.touch_ground.endpos, self, (MASK_PLAYERSOLID | MASK_OPAQUE)); - */ - - } + // If the bot is on a slope, raise it up depending on the slope normal and the bot mins/maxs hit box + { + self->bot.touch_ground = gi.trace(self->s.origin, self->mins, self->maxs, tv(self->s.origin[0], self->s.origin[1], self->s.origin[2] - 48), self, (MASK_PLAYERSOLID | MASK_OPAQUE)); + } - if (self->bot.state == BOT_MOVE_STATE_MOVE || self->bot.state == BOT_MOVE_STATE_WANDER || self->bot.state == BOT_MOVE_STATE_STAND) - { - BOTLIB_FollowPath(self); // Get current and next node back from nav code. - BOTLIB_Wander(self, &ucmd); - } + if (self->bot.state == BOT_MOVE_STATE_MOVE || self->bot.state == BOT_MOVE_STATE_WANDER || self->bot.state == BOT_MOVE_STATE_STAND) + { + BOTLIB_FollowPath(self); // Get current and next node back from nav code. + BOTLIB_Wander(self, &ucmd); + } - BOTLIB_TouchingLadder(self); - BOTLIB_Look(self, &ucmd); + BOTLIB_TouchingLadder(self); + BOTLIB_Look(self, &ucmd); - BOTLIB_ChooseWeapon(self); + BOTLIB_ChooseWeapon(self); - // When out of sight of enemies - if (self->bot.see_enemies == false) - { - BOTLIB_Healing(self, &ucmd); // Check if bot needs to heal - BOTLIB_ReadyWeapon(self); // Change to a better weapon + // When out of sight of enemies + if (self->bot.see_enemies == false) + { + BOTLIB_Healing(self, &ucmd); // Check if bot needs to heal + BOTLIB_ReadyWeapon(self); // Change to a better weapon - // Sniper bots should zoom in before an encounter - if ((rand() % 10) == 0) - BOTLIB_SniperZoom(self); + // Sniper bots should zoom in before an encounter + if ((rand() % 10) == 0) + BOTLIB_SniperZoom(self); - } + } - //if (self->bot.see_enemies == true && self->bot.enemy_in_xhair && //(self->enemy && self->enemy->deadflag == DEAD_NO) && - // (self->client->weaponstate != WEAPON_RELOADING) && (self->client->bandaging == 0) && - // (teamplay->value && lights_camera_action <= 1) || teamplay->value == 0) - if (self->bot.see_enemies) - { - if (self->bot.enemy_in_xhair) - BOTLIB_Attack(self, &ucmd); + if (self->bot.see_enemies) + { + if (self->bot.enemy_in_xhair) + BOTLIB_Attack(self, &ucmd); - else if (self->client->weapon == FindItemByNum(HC_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) - BOTLIB_Attack(self, &ucmd); + else if (self->client->weapon == FindItemByNum(HC_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); - else if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) - BOTLIB_Attack(self, &ucmd); - } + else if (self->client->weapon == FindItemByNum(GRENADE_NUM) && BOTLIB_Infront(self, self->enemy, 0.3)) + BOTLIB_Attack(self, &ucmd); } + // Training mode overrides + if (training->value) { + BOTLIB_Think_Training(self); + } // Remember where we were, to check if we got stuck. //VectorCopy( self->s.origin, self->lastPosition ); diff --git a/src/action/botlib/botlib_cmd.c b/src/action/botlib/botlib_cmd.c index a21d4bc57..2b9a5036d 100644 --- a/src/action/botlib/botlib_cmd.c +++ b/src/action/botlib/botlib_cmd.c @@ -10,15 +10,11 @@ qboolean BOTLIB_SV_Cmds(void) if (Q_stricmp(cmd, "bots") == 0) { - if (!bot_enable->value) { - gi.dprintf("%s: bot_enable is 0; Bots are disabled\n", __func__); - bot_connections.desire_bots = 0; - } - int cc = gi.argc(); if (!bot_enable->value) { gi.dprintf("bot_enable is 0; Bots are disabled\n"); + bot_connections.desire_bots = 0; return true; } @@ -71,7 +67,6 @@ qboolean BOTLIB_SV_Cmds(void) return true; } } - else { gi.cprintf(NULL, PRINT_HIGH, "----------------------------------------------\n"); diff --git a/src/action/botlib/botlib_communication.c b/src/action/botlib/botlib_communication.c index d7e595bce..ea409c928 100644 --- a/src/action/botlib/botlib_communication.c +++ b/src/action/botlib/botlib_communication.c @@ -616,7 +616,8 @@ static void BOTLIB_GetNearbyTeamList(edict_t* self, char* buf) void BOTLIB_Radio(edict_t* self, usercmd_t* ucmd) { char buffer[256]; // Chat buffer - + + if (training->value) return; // Don't radio in training mode if (teamplay->value == 0) return; // Only radio in TP games if (team_round_going == false || lights_camera_action) return; // Only allow radio during a real match (after LCA and before win/loss announcement) //if (team_round_going && lights_camera_action == 0) diff --git a/src/action/botlib/botlib_movement.c b/src/action/botlib/botlib_movement.c index 38d6c2c00..de6d054c2 100644 --- a/src/action/botlib/botlib_movement.c +++ b/src/action/botlib/botlib_movement.c @@ -5273,6 +5273,10 @@ void BOTLIB_Wander(edict_t* self, usercmd_t* ucmd) //self->bot.bi.actionflags = 0; //self->enemy = NULL; + // Do not move if botflags is BOT_NOMOVE (bitwise flags) + if (self->bot_spawnpoint && self->bot_spawnpoint->botflags & BOT_NOMOVE) + return; + // Prevent stuck suicide if holding position if ((self->bot.bi.actionflags & ACTION_HOLDPOS)) self->suicide_timeout = level.framenum + 10; diff --git a/src/action/botlib/botlib_nodes.c b/src/action/botlib/botlib_nodes.c index 69d40e0d5..6d34bcaf9 100644 --- a/src/action/botlib/botlib_nodes.c +++ b/src/action/botlib/botlib_nodes.c @@ -2108,6 +2108,10 @@ void kill_door(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, // Removes all door types void Remove_All_Doors(void) { + if (training->value) + return; // Do not remove doors in training mode + + edict_t* door; for (door = g_edicts; door < &g_edicts[globals.num_edicts]; door++) { diff --git a/src/action/botlib/botlib_spawn.c b/src/action/botlib/botlib_spawn.c index 350c331ee..956c199be 100644 --- a/src/action/botlib/botlib_spawn.c +++ b/src/action/botlib/botlib_spawn.c @@ -2065,7 +2065,8 @@ int BOTLIB_TPBotTeamScaling(void) return 0; } } -void BOTLIB_TPBotCountManual(void) + +static void BOTLIB_TPBotCountManual(void) { // Sanity check if (bot_connections.desire_team1 < 0) bot_connections.desire_team1 = 0; @@ -2146,14 +2147,52 @@ void BOTLIB_TPBotCountManual(void) } } -void BOTLIB_TPBotCountManager(void) +static void BOTLIB_TPBotCountManager(void) { int bots_to_spawn = BOTLIB_TPBotTeamScaling(); BOTLIB_TeamBotShuffle(); BOTLIB_BotCountManager(bots_to_spawn); } -void BOTLIB_DMBotCountManager(void) +static void BOTLIB_TrainingBotCountManager(void) +{ + // Manage bot counts in training mode + int bots_to_spawn = num_bot_spawns; + if (bot_playercount->value > 0 && (gameSettings & GS_DEATHMATCH)) { + // Calculate the desired number of bots based on bot_playercount and total_humans + bot_connections.desire_bots = (int)bot_playercount->value - bot_connections.total_humans; + + // Ensure desire_bots does not exceed maxclients - total_humans + if (bot_connections.desire_bots + bot_connections.total_humans > maxclients->value) { + bot_connections.desire_bots = (int)(maxclients->value - bot_connections.total_humans); + } + + // Sanity check - safety limits + if (bot_connections.desire_bots < 0) bot_connections.desire_bots = 0; + int bots_adj = 0; + int total_players = bot_connections.total_bots + bot_connections.total_humans; + + if (total_players > bot_playercount->value) { + //gi.dprintf("I should remove a bot\n"); + bots_adj = -1; + } else if (total_players < bot_playercount->value) { + // gi.dprintf("I should add a bot\n"); + // gi.dprintf("total_players: %d, bot_playercount->value: %d\n", total_players, bot_playercount->value); + bots_adj = 1; + } else { + bots_to_spawn = (int)bot_playercount->value; + bot_connections.desire_bots = bots_to_spawn; + } + + bots_to_spawn = bots_adj; // Set bots_to_spawn to +1 or -1 based on the adjustment needed + //gi.dprintf("bots_adj: %d\n", bots_to_spawn); + } else { + bots_to_spawn = bot_connections.desire_bots - bot_connections.total_bots; + } + BOTLIB_BotCountManager(bots_to_spawn); +} + +static void BOTLIB_DMBotCountManager(void) { // Manage bot counts in DM mode int bots_to_spawn = 0; @@ -2241,6 +2280,12 @@ void BOTLIB_CheckBotRules(void) BOTLIB_TPBotCountManual(); } else if (teamplay->value && bot_connections.auto_balance_bots) { BOTLIB_TPBotCountManager(); + } else if (training->value) { + // We know exactly how many bots to spawn, remove all and start over if needed + if (bot_connections.total_bots != num_bot_spawns) { + BOTLIB_RemoveBot("ALL"); + BOTLIB_BotCountManager(num_bot_spawns); + } } else { BOTLIB_DMBotCountManager(); } diff --git a/src/action/botlib/botlib_spawnpoints.c b/src/action/botlib/botlib_spawnpoints.c index 6e7ad2f02..265c21c50 100644 --- a/src/action/botlib/botlib_spawnpoints.c +++ b/src/action/botlib/botlib_spawnpoints.c @@ -306,6 +306,26 @@ void BOTLIB_Show_Spawnpoints(void) gi.linkentity(ent); } + while ((spot = G_Find(spot, FOFS(classname), "info_player_team3")) != NULL) + { + // Show a visible spawn point + edict_t* ent; + ent = G_Spawn(); + ent->classname = "dc_spawnpoint_team3"; + ent->movetype = MOVETYPE_NONE; + ent->solid = SOLID_NOT; + ent->model = "models/objects/dmspot/tris.md2"; // deathmatch spawn point + ent->s.modelindex = gi.modelindex("models/objects/dmspot/tris.md2"); + ent->owner = ent; + ent->nextthink = level.framenum + free_time * HZ; + ent->think = G_FreeEdict; + VectorCopy(spot->s.origin, ent->s.origin); + VectorCopy(spot->s.angles, ent->s.angles); + VectorSet(ent->mins, -16, -16, -24); + VectorSet(ent->maxs, 16, 16, 32); + gi.linkentity(ent); + } + // Show custom user spawn points if (dc_sp_count && dc_sp != NULL) { diff --git a/src/action/cgf_sfx_glass.c b/src/action/cgf_sfx_glass.c index 79475fd45..54f0d7991 100644 --- a/src/action/cgf_sfx_glass.c +++ b/src/action/cgf_sfx_glass.c @@ -219,6 +219,7 @@ CGF_SFX_InstallBreakableGlass (edict_t * aGlassPane) aGlassPane->takedamage = DAMAGE_YES; aGlassPane->solid = SOLID_BSP; aGlassPane->movetype = MOVETYPE_FLYMISSILE; + aGlassPane->flags |= FL_NO_KNOCKBACK; // This prevents grenades from moving glass // for other movetypes, cannot move pane to hidden location and back // try to establish size @@ -274,7 +275,7 @@ CGF_SFX_ShootBreakableGlass (edict_t * aGlassPane, edict_t * anAttacker, case MOD_KNIFE: // slash damage destruct = true; break; - default: + default: //Otherwise, randomly break destruct = (rand () % 3 == 0); break; }; diff --git a/src/action/g_combat.c b/src/action/g_combat.c index 4e0b8f5de..2aaf2db89 100644 --- a/src/action/g_combat.c +++ b/src/action/g_combat.c @@ -153,7 +153,7 @@ qboolean CanDamage (edict_t * targ, edict_t * inflictor) // Allows grenades to destroy func_buttons if they have health (city radio room for example) // in coop mode or training mode only (training maps for example) - if (coop->value || training_mode->value) { + if (coop->value || training->value) { if ((0 == Q_stricmp("func_button", targ->classname)) && 0 == Q_stricmp("hgrenade", inflictor->classname)) { PRETRACE (); trace = gi.trace (inflictor->s.origin, vec3_origin, vec3_origin, targ->s.origin, inflictor, MASK_SOLID); diff --git a/src/action/g_items.c b/src/action/g_items.c index f166b7f7f..205ef06d1 100644 --- a/src/action/g_items.c +++ b/src/action/g_items.c @@ -194,6 +194,20 @@ void DoRespawn (edict_t * ent) void SetRespawn (edict_t * ent, float delay) { + // Safety check to prevent crashes with invalid entities + // if (!ent || !ent->inuse) { + // gi.dprintf("WARNING: SetRespawn called with invalid entity\n"); + // return; + // } + + // Additional safety check for linked list pointers + // if (ent->area.prev == NULL || ent->area.next == NULL) { + // gi.dprintf("WARNING: SetRespawn called with unlinked entity (classname: %s, item: %s)\n", + // ent->classname ? ent->classname : "NULL", + // ent->item ? ent->item->classname : "NULL"); + // return; + // } + ent->flags |= FL_RESPAWN; ent->svflags |= SVF_NOCLIENT; ent->solid = SOLID_NOT; @@ -325,8 +339,9 @@ qboolean Pickup_Special (edict_t * ent, edict_t * other) AddItem(other, ent->item); - if(!(ent->spawnflags & (DROPPED_ITEM | DROPPED_PLAYER_ITEM)) && item_respawnmode->value) + if (!(ent->spawnflags & (DROPPED_ITEM | DROPPED_PLAYER_ITEM)) && item_respawnmode->value) { SetRespawn (ent, item_respawn->value); + } return true; } @@ -915,6 +930,11 @@ static void drop_make_touchable (edict_t * ent) ent->nextthink = level.framenum + 6 * HZ; ent->think = G_FreeEdict; } + else if( training->value ) // All items disappear after 2 seconds in training mode + { + ent->nextthink = eztimer(2); + ent->think = G_FreeEdict; + } else { ent->nextthink = level.framenum + 119 * HZ; diff --git a/src/action/g_local.h b/src/action/g_local.h index 3607be9ae..0d608b4ab 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -344,6 +344,7 @@ typedef struct gclient_s gclient_t; #define SPAWNFLAG_NOT_HARD BIT(10) #define SPAWNFLAG_NOT_DEATHMATCH BIT(11) #define SPAWNFLAG_NOT_COOP BIT(12) +#define SPAWNFLAG_ONLY_BOTLIB_SPAWN BIT(13) // Only BOTLIB bots will spawn here // edict->flags #define FL_FLY BIT(0) @@ -433,6 +434,28 @@ typedef enum } ammo_t; +#ifndef NO_BOTS +// g_spawn.c +typedef enum +{ + BOT_NOMOVE = BIT(0), // Bot will not move from their spawnpoint, but will still shoot enemy entities + BOT_NOSHOOT = BIT(1), // Bot will not shoot enemy entities, but will still move/interact and behave as if they were + BOT_NORESPAWN = BIT(2), // Bot never respawns (does not use the player respawn method) + BOT_IGNORE_PLAYERS = BIT(3), // Bot ignores players + BOT_IGNORE_BOTS = BIT(4), // Bot ignores other bots +} + bot_spawn_behavior_t; +#endif + +#define BOT_DUMMY (BOT_NOMOVE | BOT_NOSHOOT) +#define BOT_IGNORE_ALL (BOT_IGNORE_PLAYERS | BOT_IGNORE_BOTS) + /* + The combination of BOT_IGNORE_PLAYERS and BOT_IGNORE_BOTS (BOT_IGNORE_ALL) should make the bot navigate freely, + but otherwise do nothing. + The difference between that and BOT_NOSHOOT is that the BOT_NOSHOOT bot will interact (avoid, strafe around, etc) with the player + as if they were going to attack, but does not + */ + //tng_net.c typedef enum { SERVER_WARMING_UP = BIT(0), // 1 @@ -1119,6 +1142,7 @@ typedef enum { GM_ASSASSINATE_THE_LEADER, GM_ESCORT_THE_VIP, GM_JUMP, + GM_TRAINING, GM_MAX } GameMode; @@ -1137,6 +1161,7 @@ typedef enum { #define GMN_DEATHMATCH "Deathmatch" #define GMN_DOMINATION "Domination" #define GMN_ESPIONAGE "Espionage" +#define GMN_TRAINING "Training" #define GMN_JUMP "Jump" #define GMN_3TEAMS "3Teams" //#define GMN_NEW_MODE 2 // If new game mode flags are created, use 2 for its value first @@ -1405,7 +1430,7 @@ extern cvar_t *msgflags; extern cvar_t *use_pickup; //end cUrl integration -extern cvar_t *training_mode; // Sets training mode vars +extern cvar_t *training; // Sets training mode vars extern cvar_t *g_highscores_dir; // Sets the highscores directory extern cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bots extern cvar_t *lca_grenade; // Allows grenade pin pulling during LCA @@ -1776,6 +1801,7 @@ void EspionageChaseCam(edict_t *self, edict_t *attacker); // // g_spawn.c // +void GetBotSpawnPoints(void); void ChangePlayerSpawns(void); void ED_CallSpawn( edict_t *ent ); char* ED_NewString(const char* string); @@ -1804,6 +1830,8 @@ void ClientUserinfoChanged(edict_t* ent, char* userinfo); void ClientDisconnect(edict_t* ent); void CopyToBodyQue(edict_t* ent); void Announce_Reward(edict_t *ent, int rewardType); +void FreeBotSpawnpoint(edict_t *ent); +extern int num_bot_spawns; //p_weapon.c void Weapon_Generic( edict_t * ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST, @@ -2780,13 +2808,14 @@ struct edict_s int node_timeout; int last_node; int tries; - - // AI related stuff int weaponchoice; int equipchoice; - float fLastZoomTime; // Time we last changed sniper zoom mode + float fLastZoomTime; // Time we last changed sniper zoom mode + // for info_bot_deathmatch spawnpoint BOTLIB botflags + int botflags; + edict_t *bot_spawnpoint; // Enemy related qboolean killchat; // Have we reported an enemy death and taunted him diff --git a/src/action/g_main.c b/src/action/g_main.c index c7d10e8fe..6d1cd2b49 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -586,7 +586,7 @@ cvar_t *msgflags; // Message flags (like dmflags) see Discord_Notifications cvar_t *use_pickup; // Enable pickup notifications from the server // end cURL integration cvars -cvar_t *training_mode; // Sets training mode vars +cvar_t *training; // Sets training mode vars cvar_t *g_highscores_dir; // Sets the highscores directory cvar_t *g_highscores_countbots; // Toggles if we save highscores achieved by bots cvar_t *lca_grenade; // Allows grenade pin pulling during LCA diff --git a/src/action/g_save.c b/src/action/g_save.c index 3fde1f2a8..21060523c 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -294,6 +294,7 @@ field_t fields[] = { {"volume", FOFS (volume), F_FLOAT}, {"attenuation", FOFS (attenuation), F_FLOAT}, {"map", FOFS (map), F_LSTRING}, + {"botflags", FOFS (botflags), F_INT}, // temp spawn vars -- only valid when the spawn function is called {"lip", STOFS (lip), F_INT, FFL_SPAWNTEMP}, @@ -666,8 +667,8 @@ void InitGame( void ) msgflags = gi.cvar("msgflags", "0", 0); use_pickup = gi.cvar("use_pickup", "0", 0); - training_mode = gi.cvar("training_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); - if (training_mode->value){ + training = gi.cvar("training_mode", "0", CVAR_LATCH | CVAR_SERVERINFO_EXT); + if (training->value){ gi.cvar_forceset("item_respawnmode", "1"); gi.cvar_forceset("items", "2"); gi.cvar_forceset("dmweapon", "Combat Knife"); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 38ee4ac78..f6ce4eb4f 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -173,7 +173,6 @@ typedef struct void (*spawn) (edict_t * ent); } spawn_t; - void SP_item_health (edict_t * self); void SP_item_health_small (edict_t * self); void SP_item_health_large (edict_t * self); @@ -182,6 +181,8 @@ void SP_item_health_mega (edict_t * self); void SP_info_player_start (edict_t * ent); void SP_info_player_deathmatch (edict_t * ent); void SP_info_player_intermission (edict_t * ent); +// Bot-specific Spawnpoint +void SP_info_bot_deathmatch (edict_t * ent); void SP_func_plat (edict_t * ent); void SP_func_rotating (edict_t * ent); @@ -280,6 +281,7 @@ static const spawn_func_t spawn_funcs[] = { {"info_player_team1", SP_info_player_team1}, {"info_player_team2", SP_info_player_team2}, + {"info_player_team3", SP_info_player_team3}, {"func_plat", SP_func_plat}, {"func_button", SP_func_button}, @@ -353,6 +355,9 @@ static const spawn_func_t spawn_funcs[] = { {"info_teleport_destination", SP_info_teleport_destination}, {"misc_blackhole", SP_misc_blackhole}, + #ifndef NO_BOTS + {"info_bot_deathmatch", SP_info_bot_deathmatch}, + #endif {NULL, NULL} }; @@ -389,7 +394,9 @@ static const spawn_field_t spawn_fields[] = { {"origin", FOFS(s.origin), F_VECTOR}, {"angles", FOFS(s.angles), F_VECTOR}, {"angle", FOFS(s.angles), F_ANGLEHACK}, - + #ifndef NO_BOTS + {"botflags", FOFS(botflags), F_INT}, + #endif {NULL} }; @@ -712,7 +719,6 @@ static bool ED_ParseField(const spawn_field_t* fields, const char* key, const ch // b = (byte *)ent; for (f = fields; f->name; f++) { if (!Q_stricmp(f->name, key)) { - switch (f->type) { case F_LSTRING: @@ -747,6 +753,7 @@ static bool ED_ParseField(const spawn_field_t* fields, const char* key, const ch return true; } } + return false; } @@ -1062,12 +1069,13 @@ int Gamemode(void) gi.cvar_forceset(gm->name, "dom"); } else if (esp->value && espsettings.esp_mode == ESPMODE_ATL) { gamemode = GM_ASSASSINATE_THE_LEADER; - //gi.cvar_forceset(gm->name, "atl"); // Config load happens AFTER g_spawn, updates must occur in a_esp.c } else if (esp->value && espsettings.esp_mode == ESPMODE_ETV) { gamemode = GM_ESCORT_THE_VIP; - //gi.cvar_forceset(gm->name, "etv"); // Config load happens AFTER g_spawn, updates must occur in a_esp.c + } else if (training->value) { + gamemode = GM_TRAINING; + gi.cvar_forceset(gm->name, "training"); } else if (jump->value) { gamemode = GM_JUMP; gi.cvar_forceset(gm->name, "jump"); @@ -1461,6 +1469,26 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn gi.cvar_forceset(teamplay->name, "1"); } } + else if (training->value) + { + gi.cvar_forceset(gm->name, "training"); + if (training->value == 1) { + gameSettings |= GS_DEATHMATCH; + gi.cvar_forceset("dm_choose", "0"); + } + else if (training->value == 2) { + gameSettings |= (GS_DEATHMATCH | GS_WEAPONCHOOSE); + } + // Training mode specific settings, overridable of course + gi.cvar_forceset("matchmode", "0"); + gi.cvar_forceset("item_respawnmode", "1"); + gi.cvar_forceset("items", "2"); + gi.cvar_forceset("dmweapon", "Combat Knife"); + gi.cvar_forceset("use_rewards", "0"); // No reward announcements in training mode + gi.cvar_forceset("weapon_respawn", "1"); // Near-instant weapon respawn + gi.cvar_forceset("item_respawn", "1"); // Near instant item respawn + gi.cvar_forceset("ammo_respawn", "1"); // Near instant ammo respawn + } else if (teamplay->value) { gameSettings |= (GS_ROUNDBASED | GS_WEAPONCHOOSE); @@ -1493,6 +1521,10 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn } } + // Global enforce training mode off unless it's explicitly set + if (!training->value) + gi.cvar_forceset("training", "0"); + // Reset unique items if changing from jump to any other mode if (!jump->value) { gi.cvar_forceset(unique_items->name, "1"); @@ -1640,6 +1672,11 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn if(!use_oldspawns->value) NS_GetSpawnPoints(); } + + if (training->value) + { + GetBotSpawnPoints(); + } G_LoadLocations(); diff --git a/src/action/p_client.c b/src/action/p_client.c index 695037072..540829ece 100644 --- a/src/action/p_client.c +++ b/src/action/p_client.c @@ -964,6 +964,11 @@ void SP_info_player_start( edict_t * self ) { } +// BOTLIB-specific spawnpoints +void SP_info_bot_deathmatch( edict_t * self ) +{ +} + /*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) potential spawning position for deathmatch games */ @@ -1687,6 +1692,9 @@ void TossItemsOnDeath(edict_t * ent) if (allweapon->value)// don't drop weapons if allweapons is on return; + if (training->value) // don't drop weapons if training is on + return; + if (WPF_ALLOWED(MK23_NUM) && WPF_ALLOWED(DUAL_NUM)) { // give the player a dual pistol so they can be sure to drop one item = GET_ITEM(DUAL_NUM); @@ -2276,6 +2284,159 @@ edict_t *SelectCoopSpawnPoint(edict_t *ent) return spot; } +// Define a structure to hold bot spawn information +typedef struct { + edict_t *spawnpoint; // The actual spawn point entity + qboolean in_use; // Whether this spawn point is currently being used + edict_t *assigned_bot; // Which bot is using this spawn point +} bot_spawn_t; + +// Global array to store bot spawn points +bot_spawn_t bot_spawns[MAX_SPAWNS]; +int num_bot_spawns; + +// GetBotSpawnPoints: +// Put the spawn points into our bot_spawns array so we can work with them easily. +void GetBotSpawnPoints(void) +{ + edict_t *spot = NULL; + num_bot_spawns = 0; + + gi.dprintf("GetBotSpawnPoints: Starting search for bot spawn points\n"); + + while ((spot = G_Find(spot, FOFS(classname), "info_bot_deathmatch")) != NULL) + { + if (num_bot_spawns < MAX_SPAWNS) { + bot_spawns[num_bot_spawns].spawnpoint = spot; + bot_spawns[num_bot_spawns].in_use = false; + bot_spawns[num_bot_spawns].assigned_bot = NULL; + gi.dprintf("GetBotSpawnPoints: Found bot spawn point %d at %s\n", + num_bot_spawns, vtos(spot->s.origin)); + num_bot_spawns++; + } else { + gi.dprintf("WARNING: Maximum number of bot spawn points exceeded (%d)\n", MAX_SPAWNS); + break; + } + } + + gi.dprintf("GetBotSpawnPoints: Found %d bot spawn points\n", num_bot_spawns); +} + + +// Free a bot's spawn point when it leaves +void FreeBotSpawnpoint(edict_t *ent) +{ + if (ent->bot_spawnpoint) { + for (int i = 0; i < num_bot_spawns; i++) { + if (bot_spawns[i].spawnpoint == ent->bot_spawnpoint) { + bot_spawns[i].in_use = false; + bot_spawns[i].assigned_bot = NULL; + break; + } + } + ent->bot_spawnpoint = NULL; + } +} + +edict_t *SelectBotSpawnPoint(edict_t *ent) +{ + edict_t *spot = NULL; + int bot_index = 0; + + gi.dprintf("SelectBotSpawnPoint: Called for bot %s\n", ent->client ? ent->client->pers.netname : "unknown"); + + // Set bot count to the amount of info_bot_deathmatch + bot_connections.desire_bots = num_bot_spawns; + gi.dprintf("SelectBotSpawnPoint: num_bot_spawns = %d\n", num_bot_spawns); + + // If this bot already has an assigned spawn point, use it + if (ent->bot_spawnpoint) { + gi.dprintf("SelectBotSpawnPoint: Bot has existing spawnpoint at %s\n", + vtos(ent->bot_spawnpoint->s.origin)); + + // Verify the spawn point is still valid + if (ent->bot_spawnpoint->inuse && + !strcmp(ent->bot_spawnpoint->classname, "info_bot_deathmatch")) { + // Find this spawn point in our array and mark it as used + for (int i = 0; i < num_bot_spawns; i++) { + if (bot_spawns[i].spawnpoint == ent->bot_spawnpoint) { + gi.dprintf("SelectBotSpawnPoint: Found existing spawnpoint in array at index %d\n", i); + bot_spawns[i].in_use = true; + bot_spawns[i].assigned_bot = ent; + + gi.dprintf("SelectBotSpawnPoint: %s botflags are %d\n", ent->client->pers.netname, ent->bot_spawnpoint->botflags); + + return ent->bot_spawnpoint; + } + } + // If we didn't find it in our array, it's still valid to use + gi.dprintf("SelectBotSpawnPoint: Existing spawnpoint not found in array, using anyway\n"); + return ent->bot_spawnpoint; + } + // If we get here, the spawn point is no longer valid + //gi.dprintf("SelectBotSpawnPoint: Existing spawnpoint is no longer valid\n"); + //ent->bot_spawnpoint = NULL; + } + + // If no bot spawn points are available, fall back to deathmatch spawns + if (num_bot_spawns == 0) { + gi.dprintf("SelectBotSpawnPoint: No bot spawn points found, falling back to deathmatch spawns\n"); + return SelectDeathmatchSpawnPoint(); + } + + // Determine which bot index this is (based on entity number or other unique identifier) + for (int i = 0; i < globals.num_edicts; i++) { + edict_t *e = &g_edicts[i]; + if (e->inuse && e->is_bot && e != ent) { + bot_index++; + } + } + + gi.dprintf("SelectBotSpawnPoint: This is bot index %d\n", bot_index); + + // Try to find a spawn point specifically assigned to this bot index + for (int i = 0; i < num_bot_spawns; i++) { + gi.dprintf("SelectBotSpawnPoint: Checking spawn point %d, count = %d, in_use = %d\n", + i, bot_spawns[i].spawnpoint->count, bot_spawns[i].in_use); + + if (bot_spawns[i].spawnpoint->count == bot_index + 1 && !bot_spawns[i].in_use) { // +1 because bot indices are 0-based but count is typically 1-based + gi.dprintf("SelectBotSpawnPoint: Found specific spawn point for this bot at index %d\n", i); + bot_spawns[i].in_use = true; + bot_spawns[i].assigned_bot = ent; + ent->bot_spawnpoint = bot_spawns[i].spawnpoint; + return bot_spawns[i].spawnpoint; + } + } + + // If no specific assignment, find any unused spawn point + for (int i = 0; i < num_bot_spawns; i++) { + if (!bot_spawns[i].in_use) { + gi.dprintf("SelectBotSpawnPoint: Assigning bot to unused spawn point at index %d\n", i); + bot_spawns[i].in_use = true; + bot_spawns[i].assigned_bot = ent; + ent->bot_spawnpoint = bot_spawns[i].spawnpoint; + return bot_spawns[i].spawnpoint; + } + } + + // Fall back to regular deathmatch spawn points if no bot spawn points are available + gi.dprintf("SelectBotSpawnPoint: All bot spawn points are in use, falling back to deathmatch spawns\n"); + return SelectDeathmatchSpawnPoint(); +} + + +edict_t *SelectTrainingModeSpawnPoint(edict_t *ent) +{ + if ((!ent) || (!ent->client)) // Do not spawn non-client entities + return NULL; + // Non-bot entities spawn on normal DM spawnpoints + if (!ent->is_bot) { + return SelectDeathmatchSpawnPoint(); + } + return SelectBotSpawnPoint(ent); +} + + /* =========== SelectSpawnPoint @@ -2293,6 +2454,8 @@ void SelectSpawnPoint(edict_t * ent, vec3_t origin, vec3_t angles) //FIREBLADE if (coop->value){ spot = SelectCoopSpawnPoint(ent); + } else if (training->value) { + spot = SelectTrainingModeSpawnPoint(ent); } else if (ctf->value) { spot = SelectCTFSpawnPoint(ent); } else if (esp->value) { @@ -2538,9 +2701,28 @@ void AllItems(edict_t * ent) int i; gitem_t *it; + // For bots in training mode, give items directly without using Pickup_Special + if (training->value && ent && ent->is_bot && ent->client && game.num_items > 0) { + //gi.dprintf("AllItems: Giving special items directly to bot %s\n", ent->client->pers.netname); + + // Give all special items directly + for (int i = 0; i < game.num_items; i++) { + gitem_t *it = itemlist + i; + if (!it || !it->pickup) + continue; + if (!(it->flags & IT_ITEM)) + continue; + + // Add the item directly to inventory + ent->client->inventory[ITEM_INDEX(it)] = 1; + } + + return; + } + for (i = 0; i < game.num_items; i++) { it = itemlist + i; - if (!it->pickup) + if (!it || !it->pickup) continue; if (!(it->flags & IT_ITEM)) continue; @@ -2908,9 +3090,6 @@ void PutClientInServer(edict_t * ent) cvarsyncvalue_t cl_cvar[CVARSYNC_MAX]; #endif - // find a spawn point - // do it before setting health back up, so farthest - // ranging doesn't count this client SelectSpawnPoint(ent, spawn_origin, spawn_angles); index = ent - g_edicts - 1; @@ -2936,11 +3115,18 @@ void PutClientInServer(edict_t * ent) client->clientNum = index; - //zucc give some ammo - // changed to mk23 - item = GET_ITEM( MK23_NUM ); - client->selected_item = ITEM_INDEX( item ); - client->inventory[client->selected_item] = 1; + // Give training mode bots a knife instead of a pistol + if (training->value && ent->is_bot) { + item = GET_ITEM( KNIFE_NUM ); + client->selected_item = ITEM_INDEX( item ); + client->inventory[client->selected_item] = 1; + } else { + //zucc give some ammo + // changed to mk23 + item = GET_ITEM( MK23_NUM ); + client->selected_item = ITEM_INDEX( item ); + client->inventory[client->selected_item] = 1; + } client->weapon = item; client->lastweapon = item; @@ -3799,6 +3985,7 @@ void ClientDisconnect(edict_t * ent) ent->is_bot = false; ent->think = NULL; ACEIT_RebuildPlayerList(); + FreeBotSpawnpoint(ent); // Check if bots are in the game, if so, disable stat collection STAT_BOT_CHECK(); @@ -6378,7 +6565,7 @@ void ClientBeginServerFrame(edict_t * ent) if( (ppl_idletime->value > 0) && idleframes && (idleframes % (int)(ppl_idletime->value * HZ) == 0) ) //plays a random sound/insane sound, insane1-11.wav - if (!jump->value) // Don't play insane sounds in jmod + if (!(jump->value || training->value)) // Don't play insane sounds in jmod or training mode gi.sound( ent, CHAN_VOICE, gi.soundindex(va( "insane/insane%i.wav", rand() % 11 + 1 )), 1, ATTN_NORM, 0 ); if( (sv_idleremove->value > 0) && (idleframes > (sv_idleremove->value * HZ)) && client->resp.team ) diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 7424eaa29..378f97aaf 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -448,7 +448,6 @@ qboolean Pickup_Weapon(edict_t* ent, edict_t* other) return true; } - // zucc vwep 3.17(?) vwep support void ShowGun(edict_t* ent) { @@ -708,7 +707,7 @@ void SpecialWeaponRespawnTimer(edict_t* ent) */ // Allweapon setting makes dropped weapons disappear in 1s - if (allweapon->value) { // allweapon set + if (allweapon->value || training->value) { // allweapon set ent->nextthink = eztimer(1); ent->think = G_FreeEdict; return; @@ -725,6 +724,12 @@ void SpecialWeaponRespawnTimer(edict_t* ent) ent->think = G_FreeEdict; return; } + // Training mode weapons disappear in 2 seconds to reduce clutter + if (training->value) { + ent->nextthink = eztimer(2); + ent->think = ThinkSpecWeap; + return; + } // Normal teamplay, weapons basically never disappear if (gameSettings & GS_ROUNDBASED) { ent->nextthink = eztimer(1000); From 9b3d89a713c554cfbc027248b7f750277ecbb802 Mon Sep 17 00:00:00 2001 From: Dino <8dino2@gmail.com> Date: Fri, 19 Dec 2025 20:04:06 -0500 Subject: [PATCH 970/974] Fixed static dec bug --- src/action/a_game.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/a_game.c b/src/action/a_game.c index 0ca35ce09..64aeff1db 100644 --- a/src/action/a_game.c +++ b/src/action/a_game.c @@ -1518,7 +1518,7 @@ void _PickupRequest (edict_t * ent, pmenu_t * p) } // Count active (non-spectator) players -static int CountActivePlayers(void) { +int CountActivePlayers(void) { int count = 0; int i; edict_t *other; From 05414de4c6ad68d007afe14d9a0a2dc2b87ddee8 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 20:04:19 -0500 Subject: [PATCH 971/974] Add timeout functionality for matchmode (#253) * Fixed one more warning * Draft timeout feature * Revert main.c changes * Optional ref whistle sound, some minor changes * Minor rearragement * Grant the timeout before the next round begins * Fixed a few critical bugs, added documentation --- doc/action.md | 19 +++++++++++++++ src/action/a_match.c | 58 ++++++++++++++++++++++++++++++++++++++++++++ src/action/a_match.h | 1 + src/action/a_team.c | 37 +++++++++++++++++++++++++++- src/action/a_team.h | 1 + src/action/g_cmds.c | 5 ++++ src/action/g_local.h | 5 ++++ src/action/g_main.c | 19 +++++++++++++++ src/action/g_save.c | 2 ++ src/action/g_spawn.c | 7 ++++++ 10 files changed, 153 insertions(+), 1 deletion(-) diff --git a/doc/action.md b/doc/action.md index f3f5ddc6d..a4896c7ae 100644 --- a/doc/action.md +++ b/doc/action.md @@ -249,6 +249,25 @@ Clients will have a few more things to do during matchmode: they have to have a - `matchadmin ` - this will allow a player to get admin status - `lock` - allows a captain to lock his team. When a team is locked, no one can join it. Locks are removed on a new map - `unlock` - allows a captain to unlock his team + - `timeout` - Request a timeout for your team. Must be a captain. The timeout will be granted at the end of the current round. Each team gets a limited number of timeouts per match. + +#### Timeout Settings + +Timeouts allow teams to pause the match for a strategic break. The following cvars control timeout behavior: + +- Server settings: + - `mm_timeoutcount [#]` - Number of timeouts each team gets per match (default: 2) + - `mm_timeouttime [#]` - Duration of each timeout in seconds (default: 60) + +#### Timeout Restrictions + +- Only team captains can call timeouts +- Cannot call a timeout during the last round of the match +- Cannot call a timeout while the match is paused or in intermission +- Cannot call a timeout while currently in a timeout +- Cannot call a timeout if your team has run out of timeouts for the match +- Timeout will be granted at the end of the current round +- All players receive a 10-second warning and final 5-second countdown before the match resumes ### Limited Remote Console (LRCON) Limited Remote Console (LRCON) provides controlled admin access through a claim/release system. One player at a time can claim temporary admin rights and execute restricted server commands without needing full rcon access. The claim persists across map changes and reconnects (matched by player name and IP). diff --git a/src/action/a_match.c b/src/action/a_match.c index b05c4939c..2a0698115 100644 --- a/src/action/a_match.c +++ b/src/action/a_match.c @@ -708,6 +708,64 @@ void Cmd_ResetScores_f(edict_t * ent) } +void Cmd_CallTimeout_f(edict_t * ent) +{ + int teamNum = ent->client->resp.team; + + if (!matchmode->value) { + gi.cprintf(ent, PRINT_HIGH, "This command needs matchmode to be enabled\n"); + return; + } + + if (mm_timeouttime->value < 1) { + gi.cprintf(ent, PRINT_HIGH, "Timeouts are disabled on this server\n"); + return; + } + + if (level.intermission_framenum) { + gi.cprintf(ent, PRINT_HIGH, "You cannot call a timeout between maps\n" ); + return; + } + + if (level.pauseFrames) { + gi.cprintf(ent, PRINT_HIGH, "You cannot call a timeout while the game is paused\n" ); + return; + } + + if (level.timeoutFrames) { + gi.cprintf(ent, PRINT_HIGH, "You cannot call a timeout while currently in timeout\n" ); + return; + } + + if (level.matchTime >= timelimit->value * 60) { + gi.cprintf(ent, PRINT_HIGH, "You cannot call for a timeout on the last round of the match\n"); + return; + } + + if (teamNum == NOTEAM) { + gi.cprintf(ent, PRINT_HIGH, "You need to be on a team for that...\n"); + return; + } + + if (!IS_CAPTAIN(ent)){ + gi.cprintf(ent, PRINT_HIGH, "You must be a Captain to call a timeout\n"); + return; + } + + if (teams[teamNum].timeout_count < 1){ + gi.cprintf(ent, PRINT_HIGH, "Your team is out of timeouts!\n"); + return; + } + + // If after all the checks pass, the timeout request is granted... + teams[teamNum].timeout_count--; + timeout_requested = true; + gi.bprintf(PRINT_HIGH, "%s (%s) has called for a %i second timeout\nat the end of this round\n", + TeamName(teamNum), + ent->client->pers.netname, + (int)(mm_timeouttime->value)); +} + void Cmd_TogglePause_f(edict_t * ent, qboolean pause) { static int lastPaused = 0; diff --git a/src/action/a_match.h b/src/action/a_match.h index c05f80fca..551fc472d 100644 --- a/src/action/a_match.h +++ b/src/action/a_match.h @@ -38,6 +38,7 @@ void Cmd_TeamLock_f (edict_t * ent, int a_switch); int CheckForCaptains (int cteam); void Cmd_SetAdmin_f (edict_t * ent); +void Cmd_CallTimeout_f (edict_t *ent); void Cmd_TogglePause_f(edict_t * ent, qboolean pause); void Cmd_ResetScores_f(edict_t * ent); void MM_EspDefaultSettings(void); \ No newline at end of file diff --git a/src/action/a_team.c b/src/action/a_team.c index 295682440..0c496d9d9 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -309,10 +309,13 @@ qboolean team_game_going = false; // is a team game going right now? qboolean team_round_going = false; // is an actual round of a team game going right now? qboolean during_countdown = false; // This is set to 1 when the 10..9..8.. countdown is going on +qboolean timeout_requested = false; // Active timeout requested +qboolean timeout_granted = false; // When the timeout is granted, that begins the countdown +qboolean timeout_whistle = false; // Has the timeout whistle sound played? (limits the whistle to only being played once per) int team_round_countdown = 0; // countdown variable for start of a round int rulecheckfrequency = 0; // accumulator variable for checking rules every 1.5 secs -int lights_camera_action = 0; // countdown variable for "lights...camera...action!" +int lights_camera_action = 0; // countdown variable for "lights...camera...action!" int timewarning = 0; // countdown variable for "x Minutes left" int fragwarning = 0; // countdown variable for "x Frags left" int holding_on_tie_check = 0; // when a team "wins", countdown for a bit and wait... @@ -1939,6 +1942,7 @@ void ResetScores (qboolean playerScores) timewarning = fragwarning = 0; level.pauseFrames = 0; + level.timeoutFrames = 0; level.matchTime = 0; num_ghost_players = 0; @@ -1949,6 +1953,7 @@ void ResetScores (qboolean playerScores) teams[i].score = teams[i].total = 0; teams[i].ready = teams[i].locked = 0; teams[i].pauses_used = teams[i].wantReset = 0; + teams[i].timeout_count = (int)mm_timeoutcount->value; gi.cvar_forceset(teams[i].teamscore->name, "0"); } @@ -2055,6 +2060,29 @@ int TeamHasPlayers (int team) int _numclients( void ); // a_vote.c +qboolean TimeoutStatus(void) +{ + if (timeout_requested && !timeout_granted) { + timeout_granted = true; + level.timeoutFrames = (int)(mm_timeouttime->value * HZ); + + if (level.timeoutFrames){ + // Play the ref whistle once, then set it to true so it only plays once + if (!timeout_whistle) { + gi.sound( &g_edicts[0], CHAN_VOICE | CHAN_NO_PHS_ADD, gi.soundindex("tng/ref_whistle.wav"), 1.0, ATTN_NONE, 0.0 ); + timeout_whistle = true; + } + return false; + } + } + + // Reset the whistle so it can be played again for the next timeout + timeout_whistle = false; + + timeout_granted = false; + return true; +} + qboolean AllTeamsHavePlayers(void) { int players[TEAM_TOP] = { 0 }, i, teamsWithPlayers; @@ -2068,6 +2096,10 @@ qboolean AllTeamsHavePlayers(void) if (use_tourney->value) return (LastOpponent > 1); + // We're in timeout, do not begin the next round yet + if (!TimeoutStatus()) + return false; + if( ! _numclients() ) return false; @@ -2395,6 +2427,9 @@ void StartRound (void) { team_round_going = 1; current_round_length = 0; + timeout_requested = false; + timeout_granted = false; + level.timeoutFrames = 0; } static void StartLCA(void) diff --git a/src/action/a_team.h b/src/action/a_team.h index 85fc4ef91..451a82750 100644 --- a/src/action/a_team.h +++ b/src/action/a_team.h @@ -136,6 +136,7 @@ transparent_list_t; extern qboolean team_game_going; extern qboolean team_round_going; +extern qboolean timeout_requested; extern int lights_camera_action; extern int holding_on_tie_check; extern int team_round_countdown; diff --git a/src/action/g_cmds.c b/src/action/g_cmds.c index a0ce1704f..eda73e2cb 100644 --- a/src/action/g_cmds.c +++ b/src/action/g_cmds.c @@ -1796,6 +1796,10 @@ static void Cmd_PrintStats_f (edict_t *ent) { Cmd_Stats_f(ent, gi.argv(1)); } +static void Cmd_Timeout_f (edict_t *ent) { + Cmd_CallTimeout_f(ent); +} + static void Cmd_PauseGame_f (edict_t *ent) { Cmd_TogglePause_f(ent, true); } @@ -1996,6 +2000,7 @@ static cmdList_t commandList[] = { "ghost", Cmd_Ghost_f, 0 }, { "pausegame", Cmd_PauseGame_f, 0 }, { "unpausegame", Cmd_UnpauseGame_f, 0 }, + { "timeout", Cmd_Timeout_f, 0 }, { "resetscores", Cmd_ResetScores_f, 0 }, { "gamesettings", Cmd_PrintSettings_f, 0 }, { "follow", Cmd_Follow_f, 0 }, diff --git a/src/action/g_local.h b/src/action/g_local.h index 0d608b4ab..fd4b0c54b 100644 --- a/src/action/g_local.h +++ b/src/action/g_local.h @@ -969,6 +969,7 @@ typedef struct int realFramenum; //when game paused, framenum stays the same int pauseFrames; + int timeoutFrames; float matchTime; float emptyTime; int weapon_sound_framenum; @@ -1274,6 +1275,8 @@ extern cvar_t *mm_adminpwd; extern cvar_t *mm_allowlock; extern cvar_t *mm_pausecount; extern cvar_t *mm_pausetime; +extern cvar_t *mm_timeoutcount; +extern cvar_t *mm_timeouttime; extern cvar_t *teamdm; extern cvar_t *teamdm_respawn; @@ -3002,6 +3005,8 @@ typedef struct team_s byte ghud_num; #endif #endif + // Timeout feature + int timeout_count; }team_t; extern team_t teams[TEAM_TOP]; diff --git a/src/action/g_main.c b/src/action/g_main.c index 6d1cd2b49..e86fed322 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -426,6 +426,8 @@ cvar_t *mm_adminpwd; cvar_t *mm_allowlock; cvar_t *mm_pausecount; cvar_t *mm_pausetime; +cvar_t *mm_timeoutcount; +cvar_t *mm_timeouttime; cvar_t *teamdm; cvar_t *teamdm_respawn; @@ -1229,6 +1231,7 @@ void ExitLevel (void) level.intermission_exit = 0; level.intermission_framenum = 0; level.pauseFrames = 0; + level.timeoutFrames = 0; ClientEndServerFrames (); return; } @@ -1239,6 +1242,7 @@ void ExitLevel (void) level.intermission_exit = 0; level.intermission_framenum = 0; level.pauseFrames = 0; + level.timeoutFrames = 0; ClientEndServerFrames (); // clear some things before going to next level @@ -1413,6 +1417,21 @@ void G_RunFrame (void) level.pauseFrames--; } + if (level.timeoutFrames && !team_round_going) { + if (level.timeoutFrames <= 5 * HZ) { + if (level.timeoutFrames % HZ == 0) + CenterPrintAll( va( "The match will continue in %i seconds!", level.timeoutFrames / HZ ) ); + } + else if (level.timeoutFrames == 10 * HZ) { + CenterPrintAll( "Prepare yourselves!\nThe match will continue in 10 seconds!" ); + timeout_requested = false; + } + else if ((level.timeoutFrames % (10 * HZ)) == 0) { + gi.bprintf( PRINT_HIGH, "Match is in timeout for %i:%02i.\n", (level.timeoutFrames / HZ) / 60, (level.timeoutFrames / HZ) % 60 ); + } + level.timeoutFrames--; + } + level.realFramenum++; if (!level.pauseFrames) { diff --git a/src/action/g_save.c b/src/action/g_save.c index 21060523c..bf03a0113 100644 --- a/src/action/g_save.c +++ b/src/action/g_save.c @@ -490,6 +490,8 @@ void InitGame( void ) mm_allowlock = gi.cvar( "mm_allowlock", "1", CVAR_LATCH ); mm_pausecount = gi.cvar( "mm_allowcount", "3", CVAR_LATCH ); mm_pausetime = gi.cvar( "mm_pausetime", "2", CVAR_LATCH ); + mm_timeoutcount = gi.cvar( "mm_timeoutcount", "1", CVAR_LATCH ); // 1 timeout + mm_timeouttime = gi.cvar( "mm_timeouttime", "60", CVAR_LATCH ); // 60 seconds teams[TEAM1].teamscore = gi.cvar("t1", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); teams[TEAM2].teamscore = gi.cvar("t2", "0", CVAR_SERVERINFO | CVAR_SERVERINFO_EXT | CVAR_NOSET); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index f6ce4eb4f..12900c117 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2254,6 +2254,13 @@ void SP_worldspawn (edict_t * ent) for (int i = 0; i < q_countof(lightstyles); i++) gi.configstring(game.csr.lights + i, lightstyles[i]); gi.configstring(game.csr.lights + 63, "a"); + + // Timeout feature + if (matchmode->value) { + for (int i = 0; i <= teamCount; i++) { + teams[i].timeout_count = (int)mm_timeoutcount->value; + } + } } int LoadFlagsFromFile (const char *mapname) From 354d48c139c5724d397f9f1f5ae8f8b6f48fed72 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Fri, 19 Dec 2025 20:54:27 -0500 Subject: [PATCH 972/974] CTF fixes, null checks (#264) --- src/action/a_ctf.c | 23 ++++++++++++++--------- src/action/a_team.c | 9 ++++++--- src/action/g_main.c | 3 ++- src/action/g_spawn.c | 3 ++- src/action/g_svcmds.c | 3 ++- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 9de99f8ce..c95379be6 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -339,6 +339,9 @@ void CTFDynamicRespawnTimer(void) /* returns the respawn time for this particular client */ int CTFGetRespawnTime(edict_t *ent) { + if (!ent || !ent->client) + return 0; + int spawntime = ctf_respawn->value; if(ent->client->resp.team == TEAM1 && ctfgame.spawn_red > -1) spawntime = ctfgame.spawn_red; @@ -355,7 +358,7 @@ int CTFGetRespawnTime(edict_t *ent) */ qboolean HasFlag(edict_t * ent) { - if (!ctf->value) + if (!ctf->value || !ent || !ent->client) return false; if (ent->client->inventory[items[FLAG_T1_NUM].index] || ent->client->inventory[items[FLAG_T2_NUM].index]) return true; @@ -422,7 +425,7 @@ void CTFSwapTeams(void) for (i = 0; i < game.maxclients; i++) { ent = &g_edicts[1 + i]; - if (ent->inuse && ent->client->resp.team) { + if (ent->inuse && ent->client && ent->client->resp.team) { ent->client->resp.team = CTFOtherTeam(ent->client->resp.team); AssignSkin(ent, teams[ent->client->resp.team].skin, false); ent->client->ctf_hasflag = false; @@ -456,7 +459,7 @@ void CTFAssignTeam(gclient_t * who) for (i = 1; i <= game.maxclients; i++) { player = &g_edicts[i]; - if (!player->inuse || player->client == who) + if (!player->inuse || !player->client || player->client == who) continue; switch (player->client->resp.team) { case TEAM1: @@ -593,7 +596,7 @@ void CTFFragBonuses(edict_t * targ, edict_t * inflictor, edict_t * attacker) // field on the other team for (i = 1; i <= game.maxclients; i++) { ent = g_edicts + i; - if (ent->inuse && ent->client->resp.team == otherteam) + if (ent->inuse && ent->client && ent->client->resp.team == otherteam) ent->client->resp.ctf_lasthurtcarrier = 0; } return; @@ -629,7 +632,7 @@ void CTFFragBonuses(edict_t * targ, edict_t * inflictor, edict_t * attacker) // find attacker's team's flag carrier for (i = 1; i <= game.maxclients; i++) { carrier = g_edicts + i; - if (carrier->inuse && carrier->client->inventory[ITEM_INDEX(flag_item)]) + if (carrier->inuse && carrier->client && carrier->client->inventory[ITEM_INDEX(flag_item)]) break; carrier = NULL; } @@ -703,7 +706,7 @@ void CTFResetFlag(int team) /* hifi: drop this team flag if a player is carrying one (so the next loop returns it correctly) */ for (i = 0, ent = &g_edicts[1]; i < game.maxclients; i++, ent++) { - if (!ent->inuse) + if (!ent->inuse || !ent->client) continue; if (ent->client->inventory[ITEM_INDEX(teamFlag)]) { @@ -794,8 +797,10 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // Update team scores teams[TEAM1].score = ctfgame.team1; teams[TEAM2].score = ctfgame.team2; - gi.cvar_forceset(teams[TEAM1].teamscore->name, va("%i", ctfgame.team1)); - gi.cvar_forceset(teams[TEAM2].teamscore->name, va("%i", ctfgame.team2)); + if (teams[TEAM1].teamscore) + gi.cvar_forceset(teams[TEAM1].teamscore->name, va("%i", ctfgame.team1)); + if (teams[TEAM2].teamscore) + gi.cvar_forceset(teams[TEAM2].teamscore->name, va("%i", ctfgame.team2)); CTFDynamicRespawnTimer(); // Dynamic respawn time @@ -815,7 +820,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // Ok, let's do the player loop, hand out the bonuses for (i = 1; i <= game.maxclients; i++) { player = &g_edicts[i]; - if (!player->inuse) + if (!player->inuse || !player->client) continue; if (player->client->resp.team != other->client->resp.team) diff --git a/src/action/a_team.c b/src/action/a_team.c index 0c496d9d9..16ba053e3 100644 --- a/src/action/a_team.c +++ b/src/action/a_team.c @@ -1928,7 +1928,8 @@ void UpdateTeamScore(int team_index, int new_score) return; teams[team_index].score = new_score; - gi.cvar_forceset(teams[team_index].teamscore->name, va("%i", new_score)); + if (teams[team_index].teamscore) + gi.cvar_forceset(teams[team_index].teamscore->name, va("%i", new_score)); } void ResetScores (qboolean playerScores) @@ -1954,7 +1955,8 @@ void ResetScores (qboolean playerScores) teams[i].ready = teams[i].locked = 0; teams[i].pauses_used = teams[i].wantReset = 0; teams[i].timeout_count = (int)mm_timeoutcount->value; - gi.cvar_forceset(teams[i].teamscore->name, "0"); + if (teams[i].teamscore) + gi.cvar_forceset(teams[i].teamscore->name, "0"); } ctfgame.team1 = 0; @@ -2807,7 +2809,8 @@ int WonGame (int winner) teams[winner].score++; if (esp->value) EspEndOfRoundCleanup(); - gi.cvar_forceset(teams[winner].teamscore->name, va("%i", teams[winner].score)); + if (teams[winner].teamscore) + gi.cvar_forceset(teams[winner].teamscore->name, va("%i", teams[winner].score)); PrintScores (); } diff --git a/src/action/g_main.c b/src/action/g_main.c index e86fed322..d125ad799 100644 --- a/src/action/g_main.c +++ b/src/action/g_main.c @@ -1252,7 +1252,8 @@ void ExitLevel (void) { teams[i].score = 0; // AQ2 TNG - Reset serverinfo score cvars too - gi.cvar_forceset(teams[i].teamscore->name, "0"); + if (teams[i].teamscore) + gi.cvar_forceset(teams[i].teamscore->name, "0"); } } } diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 12900c117..0abc597c6 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -1213,7 +1213,8 @@ void SpawnEntities (const char *mapname, const char *entities, const char *spawn teams[i].leader = NULL; teams[i].leader_dead = false; } - gi.cvar_forceset(teams[i].teamscore->name, "0"); + if (teams[i].teamscore) + gi.cvar_forceset(teams[i].teamscore->name, "0"); } day_cycle_at = 0; diff --git a/src/action/g_svcmds.c b/src/action/g_svcmds.c index 1f9c5e57f..080979a1d 100644 --- a/src/action/g_svcmds.c +++ b/src/action/g_svcmds.c @@ -881,7 +881,8 @@ void SVCmd_SetTeamScore_f( int team ) } teams[team].score = atoi(gi.argv(2)); - gi.cvar_forceset( teams[team].teamscore->name, va( "%i", teams[team].score ) ); + if (teams[team].teamscore) + gi.cvar_forceset( teams[team].teamscore->name, va( "%i", teams[team].score ) ); gi.bprintf( PRINT_HIGH, "Team %i score set to %i by console.\n", team, teams[team].score ); } From 40b9c673f6562d9860ccd0f77c4c83c6e937fbf0 Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 22 Dec 2025 21:33:20 -0500 Subject: [PATCH 973/974] More CTF Fixes (#265) * CTF fixes, null checks * More ctf_mode fixes --- src/action/a_ctf.c | 24 ++++++++++++------------ src/action/g_misc.c | 2 +- src/action/g_spawn.c | 6 +++--- src/action/p_weapon.c | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index c95379be6..23d4e6f1b 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -752,9 +752,9 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) strcpy(flag_name, "briefcase"); // figure out what team this flag is - if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team1") == 0) + if (ctf_mode->value == 2 && strcmp(ent->classname, "item_bcase_team1") == 0) team = TEAM1; - else if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team2") == 0) + else if (ctf_mode->value == 2 && strcmp(ent->classname, "item_bcase_team2") == 0) team = TEAM2; else if (strcmp(ent->classname, "item_flag_team1") == 0) team = TEAM1; @@ -876,7 +876,7 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // TODO: Make this work // in CTB mode, you must have a hand free to pick up the briefcase... - if (ctf_mode->value && + if (ctf_mode->value == 2 && other->client->curr_weap != MK23_NUM || other->client->curr_weap != KNIFE_NUM || other->client->curr_weap != GRENADE_NUM ){ @@ -886,15 +886,15 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) if (other->client->uvTime) { other->client->uvTime = 0; - if (ctf_mode->value) + if (ctf_mode->value == 1) gi.centerprintf(other, "Flag taken! Shields are DOWN! Run for it!"); else gi.centerprintf(other, "Briefcase taken! Shields are DOWN! Run for it!"); } else { - if (ctf_mode->value) - gi.centerprintf(other, "You've got the ENEMY BRIEFCASE! Run for it!"); - else + if (ctf_mode->value == 1) gi.centerprintf(other, "You've got the ENEMY FLAG! Run for it!"); + else + gi.centerprintf(other, "You've got the ENEMY BRIEFCASE! Run for it!"); } // hey, its not our flag, pick it up gi.bprintf(PRINT_HIGH, "%s got the %s %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); @@ -930,7 +930,7 @@ static void CTFDropFlagThink(edict_t * ent) { // auto return the flag // reset flag will remove ourselves - if (ctf_mode->value) { + if (ctf_mode->value == 2) { if (strcmp(ent->classname, "item_bcase_team1") == 0) { CTFResetFlag(TEAM1); gi.bprintf(PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName(TEAM1)); @@ -958,7 +958,7 @@ void CTFDeadDropFlag(edict_t * self) { edict_t *dropped = NULL; - if (ctf_mode->value) { + if (ctf_mode->value == 2) { if (self->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { dropped = Drop_Item(self, team_flag[TEAM1]); self->client->inventory[ITEM_INDEX(team_flag[TEAM1])] = 0; @@ -1084,7 +1084,7 @@ void CTFEffects(edict_t * player) char t2modelpath[MAX_QPATH] = ""; // This sets the briefcase vwep in place of your weapon in CTB mode - if (ctf_mode->value) { + if (ctf_mode->value == 2) { if (player->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { Q_snprintf(t1modelpath, sizeof(t1modelpath), "players/%s/w_bc1.md2", model); player->s.modelindex2 = gi.modelindex(t1modelpath); @@ -1422,7 +1422,7 @@ void CTFDestroyFlag(edict_t * self) { //flags are important if (ctf->value) { - if (ctf_mode->value) { + if (ctf_mode->value == 2) { if (strcmp(self->classname, "item_bcase_team1") == 0) { CTFResetFlag(TEAM1); // this will free self! gi.bprintf(PRINT_HIGH, "The %s %s has returned!\n", CTFTeamName(TEAM1), team_flag[TEAM1]->pickup_name); @@ -1469,7 +1469,7 @@ void CTFCapReward(edict_t * ent) if (!ent || !ent->client || !ent->inuse) return; - if(ctf_mode->value > 1) + if(ctf_rewards->value) ent->client->resp.ctf_capstreak++; else /* capstreak is used as a multiplier so default it to one */ ent->client->resp.ctf_capstreak = 1; diff --git a/src/action/g_misc.c b/src/action/g_misc.c index e807f14c1..66bcf73eb 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -330,7 +330,7 @@ BecomeExplosion1 (edict_t * self) { //flags are important if (ctf->value) { - if (ctf_mode->value) { + if (ctf_mode->value == 2) { if (strcmp (self->classname, "item_bcase_team1") == 0){ CTFResetFlag (TEAM1); // this will free self! gi.bprintf (PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName (TEAM1)); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index 0abc597c6..e753944a9 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2315,12 +2315,12 @@ int LoadFlagsFromFile (const char *mapname) VectorCopy(position, ent->s.origin); if (!flagCount) { // Red Flag / Black Briefcase - if (ctf_mode->value) + if (ctf_mode->value == 2) ent->classname = ED_NewString ("item_bcase_team1"); else ent->classname = ED_NewString ("item_flag_team1"); } else { // Blue Flag / Silver Briefcase - if (ctf_mode->value) + if (ctf_mode->value == 2) ent->classname = ED_NewString ("item_bcase_team2"); else ent->classname = ED_NewString ("item_flag_team2"); @@ -2352,7 +2352,7 @@ void ChangePlayerSpawns (void) range1 = range2 = range3 = range4 = 99999; spot = spot1 = spot2 = spot3 = spot4 = NULL; - if (ctf_mode->value){ + if (ctf_mode->value == 2){ flag1 = G_Find (flag1, FOFS(classname), "item_bcase_team1"); flag2 = G_Find (flag2, FOFS(classname), "item_bcase_team2"); } else { diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index 378f97aaf..d3fd49159 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -500,7 +500,7 @@ void ChangeWeapon(edict_t* ent) // TODO: Make this work // CTB prevents changing to a non-mk23/knife/grenade if carrying a briefcase - if (ctf_mode->value && + if (ctf_mode->value == 2 && (ent->client->inventory[ITEM_INDEX(team_flag[TEAM1])] || ent->client->inventory[ITEM_INDEX(team_flag[TEAM2])])) { From b6fcf8e9aac54fbdac9cc5d7e216823e6a56c8be Mon Sep 17 00:00:00 2001 From: Aaron Dean <8dino2@gmail.com> Date: Mon, 22 Dec 2025 23:29:06 -0500 Subject: [PATCH 974/974] Restored ctf_mode design, added docs, fixes (#266) * Restored ctf_mode design * If statement fix for CTB --- doc/action.md | 2 ++ src/action/a_ctf.c | 39 +++++++++++++++++++++------------------ src/action/g_misc.c | 2 +- src/action/g_spawn.c | 6 +++--- src/action/p_weapon.c | 2 +- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/doc/action.md b/doc/action.md index a4896c7ae..fa9f0871a 100644 --- a/doc/action.md +++ b/doc/action.md @@ -213,6 +213,8 @@ Flag locations and CTF player spawns should be specified in tng/mapname.ctf file - `ctf_dropflag [0/1]` - Allow clients to drop the flag or not. - `uvtime [#]` - The number of seconds *10 of the duration of the 'shield' effect. (for example 40 is 4 secs) - `ctf_dyn_respawn [0/1]` - Default 0, if enabled, this will reduce the respawn timer for a losing team periodically. Will self-correct as that team mounts a comeback. The score discrepancy gets evaluated when a flag is captured on either team. + - `ctf_mode [0/1]`, default 0 -- `0` means standard CTF Mode (with flags), `1` means CTB Mode (with briefcases and one-hand-free requirement to pick one up) + - `ctf_rewards [0/1]`, default 1 -- this enables rewards for CTF flag caps, such as restoring health and ammo - Client settings: - `drop flag` - Drop the flag if you're holding it diff --git a/src/action/a_ctf.c b/src/action/a_ctf.c index 23d4e6f1b..bf0059728 100644 --- a/src/action/a_ctf.c +++ b/src/action/a_ctf.c @@ -752,16 +752,16 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) strcpy(flag_name, "briefcase"); // figure out what team this flag is - if (ctf_mode->value == 2 && strcmp(ent->classname, "item_bcase_team1") == 0) + if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team1") == 0) team = TEAM1; - else if (ctf_mode->value == 2 && strcmp(ent->classname, "item_bcase_team2") == 0) + else if (ctf_mode->value && strcmp(ent->classname, "item_bcase_team2") == 0) team = TEAM2; else if (strcmp(ent->classname, "item_flag_team1") == 0) team = TEAM1; else if (strcmp(ent->classname, "item_flag_team2") == 0) team = TEAM2; else { - gi.cprintf(ent, PRINT_HIGH, "Don't know what team the %s is on.\n", flag_name); + gi.dprintf("ERROR: Don't know what team the %s is on.\n", flag_name); return false; } @@ -876,25 +876,28 @@ qboolean CTFPickup_Flag(edict_t * ent, edict_t * other) // TODO: Make this work // in CTB mode, you must have a hand free to pick up the briefcase... - if (ctf_mode->value == 2 && - other->client->curr_weap != MK23_NUM || - other->client->curr_weap != KNIFE_NUM || - other->client->curr_weap != GRENADE_NUM ){ + if (ctf_mode->value) { + if ( + other->client->curr_weap != MK23_NUM && + other->client->curr_weap != KNIFE_NUM && + other->client->curr_weap != GRENADE_NUM ){ gi.centerprintf(other, "You must have a free hand to pick up the %s!\n", flag_name); - return false; + return false; + } } if (other->client->uvTime) { other->client->uvTime = 0; - if (ctf_mode->value == 1) - gi.centerprintf(other, "Flag taken! Shields are DOWN! Run for it!"); - else + if (ctf_mode->value) gi.centerprintf(other, "Briefcase taken! Shields are DOWN! Run for it!"); - } else { - if (ctf_mode->value == 1) - gi.centerprintf(other, "You've got the ENEMY FLAG! Run for it!"); else + gi.centerprintf(other, "Flag taken! Shields are DOWN! Run for it!"); + + } else { + if (ctf_mode->value ) gi.centerprintf(other, "You've got the ENEMY BRIEFCASE! Run for it!"); + else + gi.centerprintf(other, "You've got the ENEMY FLAG! Run for it!"); } // hey, its not our flag, pick it up gi.bprintf(PRINT_HIGH, "%s got the %s %s!\n", other->client->pers.netname, CTFTeamName(team), flag_name); @@ -930,7 +933,7 @@ static void CTFDropFlagThink(edict_t * ent) { // auto return the flag // reset flag will remove ourselves - if (ctf_mode->value == 2) { + if (ctf_mode->value) { if (strcmp(ent->classname, "item_bcase_team1") == 0) { CTFResetFlag(TEAM1); gi.bprintf(PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName(TEAM1)); @@ -958,7 +961,7 @@ void CTFDeadDropFlag(edict_t * self) { edict_t *dropped = NULL; - if (ctf_mode->value == 2) { + if (ctf_mode->value) { if (self->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { dropped = Drop_Item(self, team_flag[TEAM1]); self->client->inventory[ITEM_INDEX(team_flag[TEAM1])] = 0; @@ -1084,7 +1087,7 @@ void CTFEffects(edict_t * player) char t2modelpath[MAX_QPATH] = ""; // This sets the briefcase vwep in place of your weapon in CTB mode - if (ctf_mode->value == 2) { + if (ctf_mode->value) { if (player->client->inventory[ITEM_INDEX(team_flag[TEAM1])]) { Q_snprintf(t1modelpath, sizeof(t1modelpath), "players/%s/w_bc1.md2", model); player->s.modelindex2 = gi.modelindex(t1modelpath); @@ -1422,7 +1425,7 @@ void CTFDestroyFlag(edict_t * self) { //flags are important if (ctf->value) { - if (ctf_mode->value == 2) { + if (ctf_mode->value) { if (strcmp(self->classname, "item_bcase_team1") == 0) { CTFResetFlag(TEAM1); // this will free self! gi.bprintf(PRINT_HIGH, "The %s %s has returned!\n", CTFTeamName(TEAM1), team_flag[TEAM1]->pickup_name); diff --git a/src/action/g_misc.c b/src/action/g_misc.c index 66bcf73eb..e807f14c1 100644 --- a/src/action/g_misc.c +++ b/src/action/g_misc.c @@ -330,7 +330,7 @@ BecomeExplosion1 (edict_t * self) { //flags are important if (ctf->value) { - if (ctf_mode->value == 2) { + if (ctf_mode->value) { if (strcmp (self->classname, "item_bcase_team1") == 0){ CTFResetFlag (TEAM1); // this will free self! gi.bprintf (PRINT_HIGH, "The %s briefcase has returned!\n", CTFTeamName (TEAM1)); diff --git a/src/action/g_spawn.c b/src/action/g_spawn.c index e753944a9..0abc597c6 100644 --- a/src/action/g_spawn.c +++ b/src/action/g_spawn.c @@ -2315,12 +2315,12 @@ int LoadFlagsFromFile (const char *mapname) VectorCopy(position, ent->s.origin); if (!flagCount) { // Red Flag / Black Briefcase - if (ctf_mode->value == 2) + if (ctf_mode->value) ent->classname = ED_NewString ("item_bcase_team1"); else ent->classname = ED_NewString ("item_flag_team1"); } else { // Blue Flag / Silver Briefcase - if (ctf_mode->value == 2) + if (ctf_mode->value) ent->classname = ED_NewString ("item_bcase_team2"); else ent->classname = ED_NewString ("item_flag_team2"); @@ -2352,7 +2352,7 @@ void ChangePlayerSpawns (void) range1 = range2 = range3 = range4 = 99999; spot = spot1 = spot2 = spot3 = spot4 = NULL; - if (ctf_mode->value == 2){ + if (ctf_mode->value){ flag1 = G_Find (flag1, FOFS(classname), "item_bcase_team1"); flag2 = G_Find (flag2, FOFS(classname), "item_bcase_team2"); } else { diff --git a/src/action/p_weapon.c b/src/action/p_weapon.c index d3fd49159..378f97aaf 100644 --- a/src/action/p_weapon.c +++ b/src/action/p_weapon.c @@ -500,7 +500,7 @@ void ChangeWeapon(edict_t* ent) // TODO: Make this work // CTB prevents changing to a non-mk23/knife/grenade if carrying a briefcase - if (ctf_mode->value == 2 && + if (ctf_mode->value && (ent->client->inventory[ITEM_INDEX(team_flag[TEAM1])] || ent->client->inventory[ITEM_INDEX(team_flag[TEAM2])])) {